// SPDX-License-Identifier: Apache-2.0 // Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest #include "SessionLog.hpp" #include "everest/logging.hpp" #include "v2gMessage.hpp" #include #include #include #include #include #include #include #include #include #include namespace module { SessionLog session_log; SessionLog::SessionLog() { session_active = false; enabled = false; xmloutput = true; } SessionLog::~SessionLog() { if (logfile_csv.is_open()) { logfile_csv.close(); } } void SessionLog::setPath(const std::string& path) { logpath_root = std::filesystem::weakly_canonical(std::filesystem::path(path)); } void SessionLog::setMqtt(mqtt_publish_ftor const& mqtt_provider) { mqtt = mqtt_provider; } void SessionLog::enable() { enabled = true; } std::optional SessionLog::startSession(const std::string& suffix_string) { if (enabled) { if (session_active) { stopSession(); } // create general log directory if it does not exist if (!std::filesystem::exists(logpath_root)) { try { std::filesystem::create_directories(logpath_root); } catch (std::filesystem::filesystem_error& e) { EVLOG_error << fmt::format("Cannot create logpath {}: {}", logpath_root.string(), e.what()); return std::nullopt; } } std::string ts = Everest::Date::to_rfc3339(date::utc_clock::now()); auto ts_suffix_string = fmt::format("{}-{}", ts, suffix_string); auto logpath_with_suffix = std::filesystem::weakly_canonical(logpath_root / ts_suffix_string); const auto [root_it, suffix_ix] = std::mismatch(logpath_root.begin(), logpath_root.end(), logpath_with_suffix.begin(), logpath_with_suffix.end()); if (root_it != logpath_root.end()) { EVLOG_error << fmt::format("Logpath with suffix ({}) is not a subdirectory of logpath root {}", logpath_with_suffix.string(), logpath_root.string()); return std::nullopt; } logpath = logpath_with_suffix; // create sessionlog directory if it does not exist if (!std::filesystem::exists(logpath)) { std::filesystem::create_directories(logpath); } // open new file fn = logpath / "incomplete-eventlog.csv"; fnhtml = logpath / "incomplete-eventlog.html"; fn_complete = logpath / "eventlog.csv"; fnhtml_complete = logpath / "eventlog.html"; try { logfile_csv.open(fn); logfile_html.open(fnhtml); session_active = true; } catch (const std::ofstream::failure& e) { EVLOG_error << fmt::format("Cannot open {} of {} for writing", fn.string(), fnhtml.string()); session_active = false; } logfile_html << fmt::format("EVerest log session {}\n", everest::helpers::escape_html(suffix_string)); logfile_html << ""; logfile_html << "\n"; sys("Session logging started."); return logpath; } return std::nullopt; } void SessionLog::stopSession() { if (enabled) { sys("Session logging stopped."); logfile_html << "
\n"; if (logfile_csv.is_open()) { logfile_csv.close(); } if (logfile_html.is_open()) { logfile_html.close(); } // rename files to indicate they are finished now try { std::filesystem::rename(fn, fn_complete); } catch (const std::filesystem::filesystem_error& fs_err) { EVLOG_error << "Could not rename " << fn << ": " << fs_err.what(); } try { std::filesystem::rename(fnhtml, fnhtml_complete); } catch (const std::filesystem::filesystem_error& fs_err) { EVLOG_error << "Could not rename " << fnhtml << ": " << fs_err.what(); } session_active = false; } } void SessionLog::evse(bool iso15118, const std::string& msg) { evse(iso15118, msg, "", "", "", ""); } void SessionLog::car(bool iso15118, const std::string& msg) { car(iso15118, msg, "", "", "", ""); } void SessionLog::evse(bool iso15118, const std::string& msg, const std::string& xml, const std::string& xml_hex, const std::string& xml_base64, const std::string& json_str) { output(0, iso15118, msg, xml, xml_hex, xml_base64, json_str); } void SessionLog::car(bool iso15118, const std::string& msg, const std::string& xml, const std::string& xml_hex, const std::string& xml_base64, const std::string& json_str) { output(1, iso15118, msg, xml, xml_hex, xml_base64, json_str); } void SessionLog::output(unsigned int typ, bool iso15118, const std::string& msg, const std::string& xml, const std::string& xml_hex, const std::string& xml_base64, const std::string& json_str) { if (enabled && session_active) { std::string ts = Everest::Date::to_rfc3339(date::utc_clock::now()); std::string xml_pretty; v2g_message v2g; if (!xml.empty()) { v2g.from_xml(xml); xml_pretty = v2g.to_xml(); } else if (!json_str.empty()) { v2g.from_json(json_str); xml_pretty = v2g.to_json(); } // output to EVerest log std::string log = msg; std::string origin, target; if (xmloutput) { log += xml_pretty; } if (typ == 0) { origin = "EVSE"; target = "CAR"; EVLOG_info << "\033[1;34mEVSE " << (iso15118 ? "ISO" : "IEC") << " " << log << "\033[1;0m"; } else if (typ == 1) { origin = "CAR"; target = "EVSE"; EVLOG_info << " \033[1;33mCAR " << (iso15118 ? "ISO" : "IEC") << " " << log << "\033[1;0m"; } else { origin = "SYS"; target = ""; EVLOG_info << "SYS " << msg; } // output to session log file logfile_csv << fmt::format("\"{}\",\"{}\",\"{}\",\"{}\"\n", ts, origin, msg, xml_pretty); logfile_csv.flush(); // output to session html file logfile_html << fmt::format(" {} {} {}{} " "
{}
{}
{}
\n", origin, ts, origin + ">" + target, (typ == 0 || typ == 2 ? msg : ""), (typ == 1 ? msg : ""), html_encode(xml_pretty), xml_hex, xml_base64); logfile_html.flush(); // output to api nlohmann::json data; data["origin"] = origin; data["target"] = target; data["iso15118"] = iso15118; data["msg"] = msg; this->mqtt(data); } } void SessionLog::xmlOutput(bool e) { xmloutput = e; } void SessionLog::sys(const std::string& msg) { output(2, false, msg, "", "", "", ""); } std::string SessionLog::html_encode(const std::string& msg) { std::string out = msg; boost::replace_all(out, "<", "<"); boost::replace_all(out, ">", ">"); return out; } } // namespace module