diff --git a/config/CMakeLists.txt b/config/CMakeLists.txt index f6349c26c8..dd44e92b4e 100644 --- a/config/CMakeLists.txt +++ b/config/CMakeLists.txt @@ -13,6 +13,7 @@ generate_config_run_script(CONFIG sil-ocpp-custom-extension) generate_config_run_script(CONFIG sil-ocpp-pnc) generate_config_run_script(CONFIG sil-ocpp201-pnc) generate_config_run_script(CONFIG example) +generate_config_run_script(CONFIG dash-20-sil) # install configs install( diff --git a/config/config-dash-20-sil.yaml b/config/config-dash-20-sil.yaml new file mode 100644 index 0000000000..1126ed934e --- /dev/null +++ b/config/config-dash-20-sil.yaml @@ -0,0 +1,131 @@ +active_modules: + iso15118_charger: + module: D20Evse + # config_module: + # private_key_password: "failed" + # enable_ssl_logging: true + # tls_negotiation_strategy: ENFORCE_NO_TLS + iso15118_car: + module: PyEvJosev + config_module: + device: auto + supported_DIN70121: true + supported_ISO15118_2: true + supported_ISO15118_20_DC: true + tls_active: true + evse_manager: + module: EvseManager + config_module: + connector_id: 1 + country_code: DE + evse_id: DE*PNX*E12345*1 + evse_id_din: 49A80737A45678 + session_logging: true + session_logging_xml: false + session_logging_path: /tmp/everest-logs + charge_mode: DC + hack_allow_bpt_with_iso2: true + payment_enable_contract: false + connections: + bsp: + - module_id: yeti_driver + implementation_id: board_support + powermeter_car_side: + - module_id: powersupply_dc + implementation_id: powermeter + slac: + - module_id: slac + implementation_id: evse + hlc: + - module_id: iso15118_charger + implementation_id: charger + powersupply_DC: + - module_id: powersupply_dc + implementation_id: main + imd: + - module_id: imd + implementation_id: main + powersupply_dc: + module: JsDCSupplySimulator + yeti_driver: + module: JsYetiSimulator + config_module: + connector_id: 1 + slac: + module: JsSlacSimulator + imd: + module: IMDSimulator + ev_manager: + module: JsEvManager + config_module: + connector_id: 1 + auto_enable: true + auto_exec: true + auto_exec_commands: sleep 3;iso_wait_slac_matched;iso_start_v2g_session DC;iso_wait_pwr_ready;iso_wait_for_stop 15;iso_wait_v2g_session_stopped;unplug; + dc_target_current: 20 + dc_target_voltage: 400 + connections: + ev_board_support: + - module_id: yeti_driver + implementation_id: ev_board_support + ev: + - module_id: iso15118_car + implementation_id: ev + slac: + - module_id: slac + implementation_id: ev + auth: + module: Auth + config_module: + connection_timeout: 10 + selection_algorithm: PlugEvents + connections: + token_provider: + - module_id: token_provider + implementation_id: main + token_validator: + - module_id: token_validator + implementation_id: main + evse_manager: + - module_id: evse_manager + implementation_id: evse + token_provider: + module: DummyTokenProvider + config_implementation: + main: + token: TOKEN1 + connections: + evse: + - module_id: evse_manager + implementation_id: evse + token_validator: + module: DummyTokenValidator + config_implementation: + main: + validation_result: Accepted + validation_reason: Token seems valid + sleep: 0.25 + energy_manager: + module: EnergyManager + config_module: + schedule_total_duration: 1 + schedule_interval_duration: 60 + debug: false + connections: + energy_trunk: + - module_id: grid_connection_point + implementation_id: energy_grid + grid_connection_point: + module: EnergyNode + config_module: + fuse_limit_A: 40.0 + phase_count: 3 + connections: + price_information: [] + energy_consumer: + - module_id: evse_manager + implementation_id: energy_grid + powermeter: + - module_id: yeti_driver + implementation_id: powermeter +x-module-layout: {} diff --git a/dependencies.yaml b/dependencies.yaml index 3cbe51a9d5..3651ea8dc6 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -39,6 +39,12 @@ libfsm: git_tag: v0.2.0 cmake_condition: "EVEREST_DEPENDENCY_ENABLED_LIBFSM" +# D20Evse module +libiso15118: + git: https://github.com/EVerest/libiso15118.git + git_tag: 61735ab20df675a77136eea69f84ca5bcc0a5a46 + # options: ['ISO15118_LINK_CUSTOM_MBEDTLS OFF'] + # LEM DCBM 400/600 module libcurl: git: https://github.com/curl/curl.git @@ -76,7 +82,7 @@ ext-mbedtls: options: - ENABLE_PROGRAMS OFF - ENABLE_TESTING OFF - - MBEDTLS_FATAL_WARNINGS OFF # disables setting warnings as errors FIXME: workaround until upstream-fixes are included + - MBEDTLS_FATAL_WARNINGS OFF # disables setting warnings as errors FIXME: workaround until upstream-fixes are included # everest-testing everest-utils: git: https://github.com/EVerest/everest-utils.git diff --git a/modules/CMakeLists.txt b/modules/CMakeLists.txt index 6a644589c7..a374a449d8 100644 --- a/modules/CMakeLists.txt +++ b/modules/CMakeLists.txt @@ -33,6 +33,8 @@ ev_add_module(DummyTokenProvider) ev_add_module(DummyTokenProviderManual) ev_add_module(PhyVersoBSP) +add_subdirectory(Iso15118) + add_subdirectory(examples) add_subdirectory(simulation) diff --git a/modules/Iso15118/CMakeLists.txt b/modules/Iso15118/CMakeLists.txt new file mode 100644 index 0000000000..a1c65e402c --- /dev/null +++ b/modules/Iso15118/CMakeLists.txt @@ -0,0 +1,2 @@ +ev_add_module(D20Evse) + diff --git a/modules/Iso15118/D20Evse/CMakeLists.txt b/modules/Iso15118/D20Evse/CMakeLists.txt new file mode 100644 index 0000000000..e0a6fc3a78 --- /dev/null +++ b/modules/Iso15118/D20Evse/CMakeLists.txt @@ -0,0 +1,29 @@ +# +# AUTO GENERATED - MARKED REGIONS WILL BE KEPT +# template version 3 +# + +# module setup: +# - ${MODULE_NAME}: module name +ev_setup_cpp_module() + +# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 +target_sources(${MODULE_NAME} + PRIVATE + charger/session_logger.cpp +) + +target_link_libraries(${MODULE_NAME} + PRIVATE + iso15118::iso15118 +) +# ev@bcc62523-e22b-41d7-ba2f-825b493a3c97:v1 + +target_sources(${MODULE_NAME} + PRIVATE + "charger/ISO15118_chargerImpl.cpp" +) + +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 +# insert other things like install cmds etc here +# ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 diff --git a/modules/Iso15118/D20Evse/D20Evse.cpp b/modules/Iso15118/D20Evse/D20Evse.cpp new file mode 100644 index 0000000000..401d3e0fbf --- /dev/null +++ b/modules/Iso15118/D20Evse/D20Evse.cpp @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#include "D20Evse.hpp" + +namespace module { + +void D20Evse::init() { + invoke_init(*p_charger); +} + +void D20Evse::ready() { + invoke_ready(*p_charger); +} + +} // namespace module diff --git a/modules/Iso15118/D20Evse/D20Evse.hpp b/modules/Iso15118/D20Evse/D20Evse.hpp new file mode 100644 index 0000000000..37b2e41a4e --- /dev/null +++ b/modules/Iso15118/D20Evse/D20Evse.hpp @@ -0,0 +1,67 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef D20EVSE_HPP +#define D20EVSE_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 2 +// + +#include "ld-ev.hpp" + +// headers for provided interface implementations +#include + +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 +// insert your custom include headers here +// ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 + +namespace module { + +struct Conf { + std::string device; + std::string certificate_path; + std::string logging_path; + std::string tls_negotiation_strategy; + std::string private_key_password; + bool enable_ssl_logging; +}; + +class D20Evse : public Everest::ModuleBase { +public: + D20Evse() = delete; + D20Evse(const ModuleInfo& info, Everest::MqttProvider& mqtt_provider, + std::unique_ptr p_charger, Conf& config) : + ModuleBase(info), mqtt(mqtt_provider), p_charger(std::move(p_charger)), config(config){}; + + Everest::MqttProvider& mqtt; + const std::unique_ptr p_charger; + const Conf& config; + + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + // insert your public definitions here + // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 + +protected: + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + // insert your protected definitions here + // ev@4714b2ab-a24f-4b95-ab81-36439e1478de:v1 + +private: + friend class LdEverest; + void init(); + void ready(); + + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 + // insert your private definitions here + // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 +}; + +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 +// insert other definitions here +// ev@087e516b-124c-48df-94fb-109508c7cda9:v1 + +} // namespace module + +#endif // D20EVSE_HPP diff --git a/modules/Iso15118/D20Evse/charger/ISO15118_chargerImpl.cpp b/modules/Iso15118/D20Evse/charger/ISO15118_chargerImpl.cpp new file mode 100644 index 0000000000..d92583145e --- /dev/null +++ b/modules/Iso15118/D20Evse/charger/ISO15118_chargerImpl.cpp @@ -0,0 +1,246 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include "ISO15118_chargerImpl.hpp" + +#include "session_logger.hpp" + +#include +#include +#include +#include + +std::unique_ptr controller; + +iso15118::TbdConfig tbd_config; +iso15118::session::feedback::Callbacks callbacks; + +std::unique_ptr session_logger; + +namespace module { +namespace charger { + +static std::filesystem::path get_cert_path(const std::filesystem::path& initial_path, const std::string& config_path) { + if (config_path.empty()) { + return initial_path; + } + + if (*config_path.begin() == '/') { + return config_path; + } else { + return initial_path / config_path; + } +} + +static iso15118::config::TlsNegotiationStrategy convert_tls_negotiation_strategy(const std::string& strategy) { + using Strategy = iso15118::config::TlsNegotiationStrategy; + if (strategy.compare("ACCEPT_CLIENT_OFFER") == 0) { + return Strategy::ACCEPT_CLIENT_OFFER; + } else if (strategy.compare("ENFORCE_TLS") == 0) { + return Strategy::ENFORCE_TLS; + } else if (strategy.compare("ENFORCE_NO_TLS") == 0) { + return Strategy::ENFORCE_NO_TLS; + } else { + EVLOG_AND_THROW(Everest::EverestConfigError("Invalid choice for tls_negotiation_strategy: " + strategy)); + // better safe than sorry + } +} + +void ISO15118_chargerImpl::init() { + // setup logging routine + iso15118::io::set_logging_callback([](const std::string& msg) { EVLOG_info << msg; }); + + session_logger = std::make_unique(mod->config.logging_path); + + callbacks.dc_charge_target = [this](const iso15118::session::feedback::DcChargeTarget& charge_target) { + types::iso15118_charger::DC_EVTargetValues tmp; + tmp.DC_EVTargetVoltage = charge_target.voltage; + tmp.DC_EVTargetCurrent = charge_target.current; + publish_DC_EVTargetVoltageCurrent(tmp); + }; + + callbacks.dc_max_limits = [this](const iso15118::session::feedback::DcMaximumLimits& max_limits) { + types::iso15118_charger::DC_EVMaximumLimits tmp; + tmp.DC_EVMaximumVoltageLimit = max_limits.voltage; + tmp.DC_EVMaximumCurrentLimit = max_limits.current; + tmp.DC_EVMaximumPowerLimit = max_limits.power; + publish_DC_EVMaximumLimits(tmp); + }; + + callbacks.signal = [this](iso15118::session::feedback::Signal signal) { + using Signal = iso15118::session::feedback::Signal; + switch (signal) { + case Signal::CHARGE_LOOP_STARTED: + publish_currentDemand_Started(nullptr); + break; + case Signal::SETUP_FINISHED: + publish_V2G_Setup_Finished(nullptr); + break; + case Signal::START_CABLE_CHECK: + publish_Start_CableCheck(nullptr); + break; + case Signal::REQUIRE_AUTH_EIM: + publish_Require_Auth_EIM(nullptr); + break; + case Signal::CHARGE_LOOP_FINISHED: + publish_currentDemand_Finished(nullptr); + break; + case Signal::DC_OPEN_CONTACTOR: + publish_DC_Open_Contactor(nullptr); + break; + case Signal::DLINK_TERMINATE: + publish_dlink_terminate(nullptr); + break; + case Signal::DLINK_PAUSE: + publish_dlink_pause(nullptr); + break; + case Signal::DLINK_ERROR: + publish_dlink_error(nullptr); + break; + } + }; + + const auto default_cert_path = mod->info.paths.etc / "certs"; + + const auto cert_path = get_cert_path(default_cert_path, mod->config.certificate_path); + + tbd_config = { + { + iso15118::config::CertificateBackend::EVEREST_LAYOUT, + cert_path.string(), + mod->config.private_key_password, + mod->config.enable_ssl_logging, + }, + mod->config.device, + convert_tls_negotiation_strategy(mod->config.tls_negotiation_strategy), + }; + + controller = std::make_unique(tbd_config, callbacks); +} + +void ISO15118_chargerImpl::ready() { + // FIXME (aw): this is just plain stupid ... + while (true) { + try { + controller->loop(); + } catch (const std::exception& e) { + EVLOG_error << "D20Evse chrashed: " << e.what(); + publish_dlink_error(nullptr); + } + + controller.reset(); + + const auto RETRY_INTERVAL = std::chrono::milliseconds(1000); + + EVLOG_info << "Trying to restart in " << std::to_string(RETRY_INTERVAL.count()) << " milliseconds"; + + std::this_thread::sleep_for(RETRY_INTERVAL); + + controller = std::make_unique(tbd_config, callbacks); + } +} + +void ISO15118_chargerImpl::handle_setup( + types::iso15118_charger::EVSEID& evse_id, + std::vector& supported_energy_transfer_modes, + types::iso15118_charger::SAE_J2847_Bidi_Mode& sae_j2847_mode, bool& debug_mode, + types::iso15118_charger::SetupPhysicalValues& physical_values) { + // your code for cmd setup goes here +} + +void ISO15118_chargerImpl::handle_session_setup(std::vector& payment_options, + bool& supported_certificate_service) { + + std::vector auth_services; + + for (auto& option : payment_options) { + if (option == types::iso15118_charger::PaymentOption::ExternalPayment) { + auth_services.push_back(iso15118::message_20::Authorization::EIM); + } else if (option == types::iso15118_charger::PaymentOption::Contract) { + auth_services.push_back(iso15118::message_20::Authorization::PnC); + } + } + + controller->setup_session(auth_services, supported_certificate_service); +} + +void ISO15118_chargerImpl::handle_certificate_response( + types::iso15118_charger::Response_Exi_Stream_Status& exi_stream_status) { + // your code for cmd certificate_response goes here +} + +void ISO15118_chargerImpl::handle_authorization_response( + types::authorization::AuthorizationStatus& authorization_status, + types::authorization::CertificateStatus& certificate_status) { + + // Todo(sl): Currently PnC is not supported + bool authorized = false; + + if (authorization_status == types::authorization::AuthorizationStatus::Accepted) { + authorized = true; + } + + controller->send_control_event(iso15118::d20::AuthorizationResponse{authorized}); +} + +void ISO15118_chargerImpl::handle_ac_contactor_closed(bool& status) { + // your code for cmd ac_contactor_closed goes here +} + +void ISO15118_chargerImpl::handle_dlink_ready(bool& value) { + // your code for cmd dlink_ready goes here +} + +void ISO15118_chargerImpl::handle_cable_check_finished(bool& status) { + controller->send_control_event(iso15118::d20::CableCheckFinished{status}); +} + +void ISO15118_chargerImpl::handle_receipt_is_required(bool& receipt_required) { + // your code for cmd receipt_is_required goes here +} + +void ISO15118_chargerImpl::handle_stop_charging(bool& stop) { + controller->send_control_event(iso15118::d20::StopCharging{stop}); +} + +void ISO15118_chargerImpl::handle_update_ac_max_current(double& max_current) { + // your code for cmd update_ac_max_current goes here +} + +void ISO15118_chargerImpl::handle_update_dc_maximum_limits( + types::iso15118_charger::DC_EVSEMaximumLimits& maximum_limits) { + // your code for cmd update_dc_maximum_limits goes here +} + +void ISO15118_chargerImpl::handle_update_dc_minimum_limits( + types::iso15118_charger::DC_EVSEMinimumLimits& minimum_limits) { + // your code for cmd update_dc_minimum_limits goes here +} + +void ISO15118_chargerImpl::handle_update_isolation_status(types::iso15118_charger::IsolationStatus& isolation_status) { + // your code for cmd update_isolation_status goes here +} + +void ISO15118_chargerImpl::handle_update_dc_present_values( + types::iso15118_charger::DC_EVSEPresentVoltage_Current& present_voltage_current) { + + float voltage = present_voltage_current.EVSEPresentVoltage; + float current = present_voltage_current.EVSEPresentCurrent.value_or(0); + + controller->send_control_event(iso15118::d20::PresentVoltageCurrent{voltage, current}); +} + +void ISO15118_chargerImpl::handle_update_meter_info(types::powermeter::Powermeter& powermeter) { + // your code for cmd update_meter_info goes here +} + +void ISO15118_chargerImpl::handle_send_error(types::iso15118_charger::EvseError& error) { + // your code for cmd send_error goes here +} + +void ISO15118_chargerImpl::handle_reset_error() { + // your code for cmd reset_error goes here +} + +} // namespace charger +} // namespace module diff --git a/modules/Iso15118/D20Evse/charger/ISO15118_chargerImpl.hpp b/modules/Iso15118/D20Evse/charger/ISO15118_chargerImpl.hpp new file mode 100644 index 0000000000..8dfe523bc7 --- /dev/null +++ b/modules/Iso15118/D20Evse/charger/ISO15118_chargerImpl.hpp @@ -0,0 +1,86 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef CHARGER_ISO15118_CHARGER_IMPL_HPP +#define CHARGER_ISO15118_CHARGER_IMPL_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 3 +// + +#include + +#include "../D20Evse.hpp" + +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 +// insert your custom include headers here +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 + +namespace module { +namespace charger { + +struct Conf {}; + +class ISO15118_chargerImpl : public ISO15118_chargerImplBase { +public: + ISO15118_chargerImpl() = delete; + ISO15118_chargerImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : + ISO15118_chargerImplBase(ev, "charger"), mod(mod), config(config){}; + + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + // insert your public definitions here + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + +protected: + // command handler functions (virtual) + virtual void handle_setup(types::iso15118_charger::EVSEID& evse_id, + std::vector& supported_energy_transfer_modes, + types::iso15118_charger::SAE_J2847_Bidi_Mode& sae_j2847_mode, bool& debug_mode, + types::iso15118_charger::SetupPhysicalValues& physical_values) override; + virtual void handle_session_setup(std::vector& payment_options, + bool& supported_certificate_service) override; + virtual void + handle_certificate_response(types::iso15118_charger::Response_Exi_Stream_Status& exi_stream_status) override; + virtual void handle_authorization_response(types::authorization::AuthorizationStatus& authorization_status, + types::authorization::CertificateStatus& certificate_status) override; + virtual void handle_ac_contactor_closed(bool& status) override; + virtual void handle_dlink_ready(bool& value) override; + virtual void handle_cable_check_finished(bool& status) override; + virtual void handle_receipt_is_required(bool& receipt_required) override; + virtual void handle_stop_charging(bool& stop) override; + virtual void handle_update_ac_max_current(double& max_current) override; + virtual void + handle_update_dc_maximum_limits(types::iso15118_charger::DC_EVSEMaximumLimits& maximum_limits) override; + virtual void + handle_update_dc_minimum_limits(types::iso15118_charger::DC_EVSEMinimumLimits& minimum_limits) override; + virtual void handle_update_isolation_status(types::iso15118_charger::IsolationStatus& isolation_status) override; + virtual void handle_update_dc_present_values( + types::iso15118_charger::DC_EVSEPresentVoltage_Current& present_voltage_current) override; + virtual void handle_update_meter_info(types::powermeter::Powermeter& powermeter) override; + virtual void handle_send_error(types::iso15118_charger::EvseError& error) override; + virtual void handle_reset_error() override; + + // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 + // insert your protected definitions here + // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 + +private: + const Everest::PtrContainer& mod; + const Conf& config; + + virtual void init() override; + virtual void ready() override; + + // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 + // insert your private definitions here + // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 +}; + +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 +// insert other definitions here +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 + +} // namespace charger +} // namespace module + +#endif // CHARGER_ISO15118_CHARGER_IMPL_HPP diff --git a/modules/Iso15118/D20Evse/charger/session_logger.cpp b/modules/Iso15118/D20Evse/charger/session_logger.cpp new file mode 100644 index 0000000000..b4ec1fd336 --- /dev/null +++ b/modules/Iso15118/D20Evse/charger/session_logger.cpp @@ -0,0 +1,141 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#include "session_logger.hpp" + +#include +#include +#include +#include +#include + +#include + +#include + +#include + +using LogEvent = iso15118::session::logging::Event; + +std::string get_filename_for_current_time() { + const auto now = std::chrono::system_clock::now(); + const auto now_t = std::chrono::system_clock::to_time_t(now); + + std::tm now_tm; + localtime_r(&now_t, &now_tm); + + char buffer[64]; + strftime(buffer, sizeof(buffer), "%y%m%d_%H-%M-%S.yaml", &now_tm); + return buffer; +} + +// static auto timepoint_to_string(const iso15118::session::logging::TimePoint& timepoint) { +// using namespace date; +// return static_cast(timepoint); +// } + +std::ostream& operator<<(std::ostream& os, const iso15118::session::logging::ExiMessageDirection& direction) { + using Direction = iso15118::session::logging::ExiMessageDirection; + switch (direction) { + case Direction::FROM_EV: + return os << "FROM_EV"; + case Direction::TO_EV: + return os << "TO_EV"; + } + + return os; +} + +class SessionLog { +public: + SessionLog(const std::string& file_name) : file(file_name.c_str(), std::ios::out) { + if (not file.good()) { + throw std::runtime_error("Failed to open file " + file_name + " for writing iso15118 session log"); + } + + printf("Created logfile at: %s\n", file_name.c_str()); + } + void operator()(const iso15118::session::logging::SimpleEvent& event) { + file << "- type: INFO\n"; + add_timestamp(event.time_point); + file << " info: \"" << event.info << "\"\n"; + } + + void operator()(const iso15118::session::logging::ExiMessageEvent& event) { + file << "- type: EXI\n"; + add_timestamp(event.time_point); + file << " direction: " << event.direction << "\n"; + file << " sdp_payload_type: " << event.payload_type << "\n"; + add_hex_encoded_data(event.data, event.len); + } + + void flush() { + file.flush(); + } + +private: + std::fstream file; + + void add_timestamp(const iso15118::session::logging::TimePoint& timestamp) { + if (not timestamp_initialized) { + last_timestamp = timestamp; + timestamp_initialized = true; + } + + const auto offset_ms = std::chrono::duration_cast(timestamp - last_timestamp); + file << " timestamp_offset: " << offset_ms.count() << "\n"; + + const auto dp = date::floor(timestamp); + const auto time = date::make_time(timestamp - dp); + const auto milliseconds = std::chrono::duration_cast(time.subseconds()); + file << " timestamp: \""; + file << std::setfill('0') << std::setw(2) << time.hours().count() << ":"; + file << std::setfill('0') << std::setw(2) << time.minutes().count() << ":"; + file << std::setfill('0') << std::setw(2) << time.seconds().count() << "."; + file << std::setfill('0') << std::setw(4) << milliseconds.count(); + file << "\"\n"; + + last_timestamp = timestamp; + } + + void add_hex_encoded_data(const uint8_t* data, size_t len) { + file << " data: \""; + + const auto flags = file.flags(); + + file << std::hex; + + for (int i = 0; i < len; ++i) { + file << std::setfill('0') << std::setw(2) << static_cast(data[i]); + } + + file.flags(flags); + file << "\"\n"; + } + + iso15118::session::logging::TimePoint last_timestamp; + bool timestamp_initialized{false}; +}; + +SessionLogger::SessionLogger(std::filesystem::path output_dir_) : output_dir(std::filesystem::absolute(output_dir_)) { + // FIXME (aw): this is quite brute force ... + if (not std::filesystem::exists(output_dir)) { + std::filesystem::create_directory(output_dir); + } + + iso15118::session::logging::set_session_log_callback([this](std::uintptr_t id, const LogEvent& event) { + auto log_it = logs.find(id); + if (log_it == logs.end()) { + const auto log_file_name = output_dir / get_filename_for_current_time(); + const auto emplaced = logs.emplace(id, std::make_unique(log_file_name.string())); + + log_it = emplaced.first; + } + + auto& log = *log_it->second; + + std::visit(log, event); + log.flush(); + }); +} + +SessionLogger::~SessionLogger() = default; diff --git a/modules/Iso15118/D20Evse/charger/session_logger.hpp b/modules/Iso15118/D20Evse/charger/session_logger.hpp new file mode 100644 index 0000000000..6bdc5982ae --- /dev/null +++ b/modules/Iso15118/D20Evse/charger/session_logger.hpp @@ -0,0 +1,21 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include +#include +#include + +// forward declare +class SessionLog; + +class SessionLogger { +public: + SessionLogger(std::filesystem::path output_dir); + ~SessionLogger(); + +private: + std::filesystem::path output_dir; + std::map> logs; +}; diff --git a/modules/Iso15118/D20Evse/manifest.yaml b/modules/Iso15118/D20Evse/manifest.yaml new file mode 100644 index 0000000000..51e52bc1ff --- /dev/null +++ b/modules/Iso15118/D20Evse/manifest.yaml @@ -0,0 +1,43 @@ +description: >- + This module is a draft implementation of iso15118-20 for the EVSE side +config: + device: + description: >- + Ethernet device used for HLC. Any local interface that has an ipv6 + link-local and a MAC addr will work + type: string + default: eth0 + certificate_path: + description: Path to certificate directories + type: string + default: "" + logging_path: + description: Path to logging directory (will be created if non existent) + type: string + default: "." + tls_negotiation_strategy: + description: Select strategy on how to negotiate connection encryption + type: string + enum: + - ACCEPT_CLIENT_OFFER + - ENFORCE_TLS + - ENFORCE_NO_TLS + default: ACCEPT_CLIENT_OFFER + private_key_password: + description: Password for private key files (USE ONLY FOR TESTING) + type: string + default: "123456" + enable_ssl_logging: + description: Verbosely log the ssl/tls connection + type: boolean + default: false +provides: + charger: + interface: ISO15118_charger + description: >- + This interface provides limited access to iso15118-20 +enable_external_mqtt: true +metadata: + license: https://opensource.org/licenses/Apache-2.0 + authors: + - aw diff --git a/modules/PyEvJosev/utilities.py b/modules/PyEvJosev/utilities.py index e0c57d30f6..1a88720e90 100644 --- a/modules/PyEvJosev/utilities.py +++ b/modules/PyEvJosev/utilities.py @@ -27,6 +27,7 @@ def emit(self, record): else: log.debug(msg) + def setup_everest_logging(): # remove all logging handler so that we'll have only our custom one # FIXME (aw): this is probably bad practice because if everyone does that, only the last one might survive @@ -40,6 +41,7 @@ def setup_everest_logging(): logging.getLogger().addHandler(handler) + def choose_first_ipv6_local() -> str: for iface in netifaces.interfaces(): if netifaces.AF_INET6 in netifaces.ifaddresses(iface): @@ -50,14 +52,17 @@ def choose_first_ipv6_local() -> str: log.warning('No necessary IPv6 link-local address was found!') return 'eth0' + def determine_network_interface(preferred_interface: str) -> str: if preferred_interface == "auto": return choose_first_ipv6_local() elif preferred_interface not in netifaces.interfaces(): - log.warning(f"The network interface {preferred_interface} was not found!") + log.warning( + f"The network interface {preferred_interface} was not found!") return preferred_interface + def patch_josev_config(josev_config: EVCCConfig, everest_config: dict) -> None: josev_config.use_tls = everest_config['tls_active'] @@ -93,5 +98,5 @@ def patch_josev_config(josev_config: EVCCConfig, everest_config: dict) -> None: josev_config.supported_protocols = load_requested_protocols(protocols) josev_config.supported_energy_services = load_requested_energy_services( - ['DC'] + ['DC','DC_BPT'] )