From 06cf9836061ced0d8fa0b4b61061b3438a3ca364 Mon Sep 17 00:00:00 2001 From: florinmihut Date: Mon, 20 Jan 2025 17:57:50 +0100 Subject: [PATCH 1/4] =?UTF-8?q?Introduce=20EVSE=20Manager=20configuration?= =?UTF-8?q?=20option=20for=20failing=20charging=20if=20t=E2=80=A6=20(#993)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Introduce EVSE Manager configuration option for failing charging if the powermeter has errors * Add behavior in the error handling document * Update modules/EvseManager/manifest.yaml * Adding changes as requested during code review Signed-off-by: florinmihut Co-authored-by: Piet Gömpel <37657534+Pietfried@users.noreply.github.com> --- doc/ocmf/powermeter_start_transaction.md | 52 ++++++++++ doc/ocmf/powermeter_stop_transaction.md | 46 +++++++++ modules/EvseManager/Charger.cpp | 13 ++- modules/EvseManager/Charger.hpp | 4 +- modules/EvseManager/ErrorHandling.cpp | 23 ++++- modules/EvseManager/ErrorHandling.hpp | 5 +- modules/EvseManager/EvseManager.cpp | 15 ++- modules/EvseManager/EvseManager.hpp | 6 +- modules/EvseManager/doc.rst | 125 ++++++++++++----------- modules/EvseManager/manifest.yaml | 13 ++- 10 files changed, 221 insertions(+), 81 deletions(-) create mode 100644 doc/ocmf/powermeter_start_transaction.md create mode 100644 doc/ocmf/powermeter_stop_transaction.md diff --git a/doc/ocmf/powermeter_start_transaction.md b/doc/ocmf/powermeter_start_transaction.md new file mode 100644 index 000000000..efb8b7c0a --- /dev/null +++ b/doc/ocmf/powermeter_start_transaction.md @@ -0,0 +1,52 @@ +```mermaid +sequenceDiagram +autonumber +participant Powermeter +participant EvseManager +participant OCPP +participant CSMS + +title Start of a Transaction + +Note over EvseManager: User plugs in EV and authorizes + +EvseManager->>OCPP: Event(SessionStarted) + +OCPP->>CSMS: StatusNotification.req(Preparing) +CSMS-->>OCPP: StatusNotification.conf + +alt successful case + EvseManager->>Powermeter: startTransaction + Powermeter-->>EvseManager: startTransaction Response (OK/ID) + + EvseManager->>OCPP: Event(TransactionStarted) + OCPP->>CSMS: StartTransaction.req + CSMS-->>OCPP: StartTransaction.conf + + Note over EvseManager: Transaction started successfully + +else startTransaction failing due to power loss + EvseManager->>Powermeter: startTransaction + Powermeter-->>EvseManager: startTransaction Response (FAIL) + + EvseManager->>OCPP: Event(Deauthorized) + + OCPP->>CSMS: StatusNotification.req(Finishing) + CSMS-->>OCPP: StatusNotification.conf + + EvseManager->>OCPP: raiseError (PowermeterTransactionStartFailed) + OCPP->>CSMS: StatusNotification.req(Finishing, PowermeterTransactionStartFailed) + CSMS-->>OCPP: StatusNotification.conf + + Note over EvseManager: Transaction did not start +end + +alt EvseManager configured to become inoperative in case of Powermeter CommunicationError + Powermeter->>EvseManager: raise_error(CommunicationError) + Note over Powermeter,EvseManager: Powermeter raises a CommunicationError
and EvseManager is registered for notification + EvseManager->>OCPP: raise_error (Inoperative) + OCPP->>CSMS: StatusNotification.req(Faulted) + CSMS-->>OCPP: StatusNotification.conf +end + +``` \ No newline at end of file diff --git a/doc/ocmf/powermeter_stop_transaction.md b/doc/ocmf/powermeter_stop_transaction.md new file mode 100644 index 000000000..5c36d14b1 --- /dev/null +++ b/doc/ocmf/powermeter_stop_transaction.md @@ -0,0 +1,46 @@ +```mermaid +sequenceDiagram +autonumber +participant Powermeter +participant EvseManager +participant OCPP +participant CSMS + +title Stopping Transaction in Error + +Note over Powermeter, CSMS: Transaction is running + +Powermeter->>Powermeter: detects a
CommunicationError +Note over Powermeter,EvseManager: Powermeter raises a CommunicationError
and EvseManager is registered for notification +Powermeter->>EvseManager: raise_error (CommunicationFault) +Powermeter->>OCPP: raise_error (CommunicationFault) + +OCPP->>CSMS: StatusNotification.req(Charging, CommunicationFault) +CSMS-->>OCPP: StatusNotification.conf + +alt EvseManager configured to become inoperative in case of PowermeterCommError + EvseManager->>EvseManager: Pause charging + EvseManager->>OCPP: raiseError (Inoperative) + OCPP->>CSMS: StatusNotification.req(Faulted) + Note over EvseManager: Note that we would just continue charging otherwise +end + +Note over Powermeter, CSMS: User stops the transaction + +alt successful case (Powermeter has no CommunicationError) + EvseManager->>Powermeter: stopTransaction (ID) + Powermeter-->>EvseManager: stopTransaction Response (OK/OCMF) + EvseManager->>OCPP: Event(TransactionFinished(OCMF)) + + OCPP->>CSMS: StopTransaction.req(OCMF) + CSMS-->>OCPP: StopTransaction.conf +else stopTransaction failing due to subsequent power loss (this applies as well when Powermeter still in CommunicationError) + EvseManager->>Powermeter: stopTransaction (ID) + Powermeter->>EvseManager: stopTransaction Response (FAIL) + EvseManager->>OCPP: Event(TransactionFinished) + + Note right of OCPP: In this case we can't stop the transaction including the OCMF + OCPP->>CSMS: StopTransaction.req() + CSMS-->>OCPP: StopTransaction.conf +end +``` \ No newline at end of file diff --git a/modules/EvseManager/Charger.cpp b/modules/EvseManager/Charger.cpp index b17c44717..8e205ad76 100644 --- a/modules/EvseManager/Charger.cpp +++ b/modules/EvseManager/Charger.cpp @@ -1205,14 +1205,15 @@ bool Charger::start_transaction() { // we can't bill the customer. if (response.status == types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR) { EVLOG_error << "Failed to start a transaction on the power meter " << response.error.value_or(""); - error_handling->raise_powermeter_transaction_start_failed_error( - "Failed to start transaction on the power meter"); - return false; + if (true == config_context.fail_on_powermeter_errors) { + error_handling->raise_powermeter_transaction_start_failed_error( + "Failed to start transaction on the power meter"); + return false; + } } } store->store_session(shared_context.session_uuid); - signal_transaction_started_event(shared_context.id_token); return true; } @@ -1324,7 +1325,8 @@ void Charger::setup(bool has_ventilation, const ChargeMode _charge_mode, bool _a bool _ac_hlc_use_5percent, bool _ac_enforce_hlc, bool _ac_with_soc_timeout, float _soft_over_current_tolerance_percent, float _soft_over_current_measurement_noise_A, const int _switch_3ph1ph_delay_s, const std::string _switch_3ph1ph_cp_state, - const int _soft_over_current_timeout_ms, const int _state_F_after_fault_ms) { + const int _soft_over_current_timeout_ms, const int _state_F_after_fault_ms, + const bool fail_on_powermeter_errors) { // set up board support package bsp->setup(has_ventilation); @@ -1344,6 +1346,7 @@ void Charger::setup(bool has_ventilation, const ChargeMode _charge_mode, bool _a config_context.switch_3ph1ph_cp_state_F = _switch_3ph1ph_cp_state == "F"; config_context.state_F_after_fault_ms = _state_F_after_fault_ms; + config_context.fail_on_powermeter_errors = fail_on_powermeter_errors; if (config_context.charge_mode == ChargeMode::AC and config_context.ac_hlc_enabled) EVLOG_info << "AC HLC mode enabled."; diff --git a/modules/EvseManager/Charger.hpp b/modules/EvseManager/Charger.hpp index e2c5e5f1d..6b6918059 100644 --- a/modules/EvseManager/Charger.hpp +++ b/modules/EvseManager/Charger.hpp @@ -106,7 +106,7 @@ class Charger { bool ac_enforce_hlc, bool ac_with_soc_timeout, float soft_over_current_tolerance_percent, float soft_over_current_measurement_noise_A, const int switch_3ph1ph_delay_s, const std::string switch_3ph1ph_cp_state, const int soft_over_current_timeout_ms, - const int _state_F_after_fault_ms); + const int _state_F_after_fault_ms, const bool fail_on_powermeter_errors); bool enable_disable(int connector_id, const types::evse_manager::EnableDisableSource& source); @@ -327,6 +327,8 @@ class Charger { int soft_over_current_timeout_ms{7000}; // Switch to F for configured ms after a fatal error int state_F_after_fault_ms{300}; + // Fail on powermeter errors + bool fail_on_powermeter_errors; } config_context; // Used by different threads, but requires no complete state machine locking diff --git a/modules/EvseManager/ErrorHandling.cpp b/modules/EvseManager/ErrorHandling.cpp index 2f6ac049e..f27b4920f 100644 --- a/modules/EvseManager/ErrorHandling.cpp +++ b/modules/EvseManager/ErrorHandling.cpp @@ -15,6 +15,7 @@ static const struct IgnoreErrors { ErrorList ac_rcd{"ac_rcd/VendorWarning"}; ErrorList imd{"isolation_monitor/VendorWarning"}; ErrorList powersupply{"power_supply_DC/VendorWarning"}; + ErrorList powermeter{}; } ignore_errors; ErrorHandling::ErrorHandling(const std::unique_ptr& _r_bsp, @@ -23,14 +24,16 @@ ErrorHandling::ErrorHandling(const std::unique_ptr& _r_b const std::vector>& _r_ac_rcd, const std::unique_ptr& _p_evse, const std::vector>& _r_imd, - const std::vector>& _r_powersupply) : + const std::vector>& _r_powersupply, + const std::vector>& _r_powermeter) : r_bsp(_r_bsp), r_hlc(_r_hlc), r_connector_lock(_r_connector_lock), r_ac_rcd(_r_ac_rcd), p_evse(_p_evse), r_imd(_r_imd), - r_powersupply(_r_powersupply) { + r_powersupply(_r_powersupply), + r_powermeter(_r_powermeter) { // Subscribe to bsp driver to receive Errors from the bsp hardware r_bsp->subscribe_all_errors([this](const Everest::error::Error& error) { process_error(); }, @@ -59,6 +62,12 @@ ErrorHandling::ErrorHandling(const std::unique_ptr& _r_b r_powersupply[0]->subscribe_all_errors([this](const Everest::error::Error& error) { process_error(); }, [this](const Everest::error::Error& error) { process_error(); }); } + + // Subscribe to powermeter to receive errors from powermeter hardware + if (r_powermeter.size() > 0) { + r_powermeter[0]->subscribe_all_errors([this](const Everest::error::Error& error) { process_error(); }, + [this](const Everest::error::Error& error) { process_error(); }); + } } void ErrorHandling::raise_overcurrent_error(const std::string& description) { @@ -102,7 +111,8 @@ void ErrorHandling::process_error() { const int error_count = p_evse->error_state_monitor->get_active_errors().size() + r_bsp->error_state_monitor->get_active_errors().size() + number_of_active_errors(r_connector_lock) + number_of_active_errors(r_ac_rcd) + - number_of_active_errors(r_imd) + number_of_active_errors(r_powersupply); + number_of_active_errors(r_imd) + number_of_active_errors(r_powersupply) + + number_of_active_errors(r_powermeter); if (error_count == 0) { signal_all_errors_cleared(); @@ -159,6 +169,13 @@ std::optional ErrorHandling::errors_prevent_charging() { } } + if (r_powermeter.size() > 0) { + fatal = is_fatal(r_powermeter[0]->error_state_monitor->get_active_errors(), ignore_errors.powermeter); + if (fatal) { + return fatal; + } + } + return std::nullopt; } diff --git a/modules/EvseManager/ErrorHandling.hpp b/modules/EvseManager/ErrorHandling.hpp index 8db687eaf..056c4a757 100644 --- a/modules/EvseManager/ErrorHandling.hpp +++ b/modules/EvseManager/ErrorHandling.hpp @@ -30,6 +30,7 @@ #include #include #include +#include #include #include "Timeout.hpp" @@ -47,7 +48,8 @@ class ErrorHandling { const std::vector>& r_ac_rcd, const std::unique_ptr& _p_evse, const std::vector>& _r_imd, - const std::vector>& _r_powersupply); + const std::vector>& _r_powersupply, + const std::vector>& _r_powermeter); // Signal that error set has changed. Bool argument is true if it is preventing charging at the moment and false if // charging can continue. @@ -77,6 +79,7 @@ class ErrorHandling { const std::unique_ptr& p_evse; const std::vector>& r_imd; const std::vector>& r_powersupply; + const std::vector>& r_powermeter; }; } // namespace module diff --git a/modules/EvseManager/EvseManager.cpp b/modules/EvseManager/EvseManager.cpp index b269d19bf..46b79f8ce 100644 --- a/modules/EvseManager/EvseManager.cpp +++ b/modules/EvseManager/EvseManager.cpp @@ -161,8 +161,13 @@ void EvseManager::ready() { bsp->set_ev_simplified_mode_evse_limit(true); } + // we provide the powermeter interface to the ErrorHandling only if we need to react to powermeter errors + // otherwise we provide an empty vector of pointers to the powermeter interface + const std::vector> empty; error_handling = std::unique_ptr( - new ErrorHandling(r_bsp, r_hlc, r_connector_lock, r_ac_rcd, p_evse, r_imd, r_powersupply_DC)); + new ErrorHandling(r_bsp, r_hlc, r_connector_lock, r_ac_rcd, p_evse, r_imd, r_powersupply_DC, + config.fail_on_powermeter_errors ? r_powermeter_billing() : empty)); + if (not config.lock_connector_in_state_b) { EVLOG_warning << "Unlock connector in CP state B. This violates IEC61851-1:2019 D.6.5 Table D.9 line 4 and " "should not be used in public environments!"; @@ -888,7 +893,7 @@ void EvseManager::ready() { config.ac_hlc_use_5percent, config.ac_enforce_hlc, false, config.soft_over_current_tolerance_percent, config.soft_over_current_measurement_noise_A, config.switch_3ph1ph_delay_s, config.switch_3ph1ph_cp_state, config.soft_over_current_timeout_ms, - config.state_F_after_fault_ms); + config.state_F_after_fault_ms, config.fail_on_powermeter_errors); } telemetryThreadHandle = std::thread([this]() { @@ -1074,7 +1079,8 @@ void EvseManager::setup_fake_DC_mode() { charger->setup(config.has_ventilation, Charger::ChargeMode::DC, hlc_enabled, config.ac_hlc_use_5percent, config.ac_enforce_hlc, false, config.soft_over_current_tolerance_percent, config.soft_over_current_measurement_noise_A, config.switch_3ph1ph_delay_s, - config.switch_3ph1ph_cp_state, config.soft_over_current_timeout_ms, config.state_F_after_fault_ms); + config.switch_3ph1ph_cp_state, config.soft_over_current_timeout_ms, config.state_F_after_fault_ms, + config.fail_on_powermeter_errors); types::iso15118_charger::EVSEID evseid = {config.evse_id, config.evse_id_din}; @@ -1113,7 +1119,8 @@ void EvseManager::setup_AC_mode() { charger->setup(config.has_ventilation, Charger::ChargeMode::AC, hlc_enabled, config.ac_hlc_use_5percent, config.ac_enforce_hlc, true, config.soft_over_current_tolerance_percent, config.soft_over_current_measurement_noise_A, config.switch_3ph1ph_delay_s, - config.switch_3ph1ph_cp_state, config.soft_over_current_timeout_ms, config.state_F_after_fault_ms); + config.switch_3ph1ph_cp_state, config.soft_over_current_timeout_ms, config.state_F_after_fault_ms, + config.fail_on_powermeter_errors); types::iso15118_charger::EVSEID evseid = {config.evse_id, config.evse_id_din}; diff --git a/modules/EvseManager/EvseManager.hpp b/modules/EvseManager/EvseManager.hpp index fc4ba9825..0a2728ff8 100644 --- a/modules/EvseManager/EvseManager.hpp +++ b/modules/EvseManager/EvseManager.hpp @@ -102,6 +102,7 @@ struct Conf { int soft_over_current_timeout_ms; bool lock_connector_in_state_b; int state_F_after_fault_ms; + bool fail_on_powermeter_errors; }; class EvseManager : public Everest::ModuleBase { @@ -136,8 +137,7 @@ class EvseManager : public Everest::ModuleBase { r_imd(std::move(r_imd)), r_powersupply_DC(std::move(r_powersupply_DC)), r_store(std::move(r_store)), - config(config) { - } + config(config){}; Everest::MqttProvider& mqtt; Everest::TelemetryProvider& telemetry; @@ -194,7 +194,7 @@ class EvseManager : public Everest::ModuleBase { const std::vector>& r_powermeter_billing(); - // FIXME: this will be removed with proper intergration of BPT on ISO-20 + // FIXME: this will be removed with proper integration of BPT on ISO-20 // on DIN SPEC and -2 we claim a positive charging current on ISO protocol, // but the power supply switches to discharge if this flag is set. std::atomic_bool is_actually_exporting_to_grid{false}; diff --git a/modules/EvseManager/doc.rst b/modules/EvseManager/doc.rst index 4fb00fc31..08927b586 100644 --- a/modules/EvseManager/doc.rst +++ b/modules/EvseManager/doc.rst @@ -6,20 +6,20 @@ EvseManager See also module's :ref:`auto-generated reference `. -The module ``EvseManager`` is a central module that manages one EVSE +The module ``EvseManager`` is a central module that manages one EVSE (i.e. one connector to charge a car). It may control multiple physical connectors if they are not usable at the same time and share one connector id, -but one EvseManager always shows as one connector in OCPP for example. So in +but one EvseManager always shows as one connector in OCPP for example. So in general each connector should have a dedicated EvseManager module loaded. -The EvseManager contains the high level charging logic (Basic charging and -HLC/SLAC interaction), collects all relevant data for the charging session -(e.g. energy delivered during this charging session) and provides control over -the charging port/session. For HLC it uses two helper protocol modules that it +The EvseManager contains the high level charging logic (Basic charging and +HLC/SLAC interaction), collects all relevant data for the charging session +(e.g. energy delivered during this charging session) and provides control over +the charging port/session. For HLC it uses two helper protocol modules that it controls (SLAC and ISO15118). -Protocol modules such as OCPP or other APIs use EvseManagers to control the +Protocol modules such as OCPP or other APIs use EvseManagers to control the charging session and get all relevant data. The following charge modes are supported: @@ -32,7 +32,7 @@ Additional features: * Autocharge support (PnC coming soon) * Seamlessly integrates into EVerest Energy Management -* The lowest level IEC61851 state machine can be run on a dedicated +* The lowest level IEC61851 state machine can be run on a dedicated microcontroller for improved electrical safety * Support for seperate AC and DC side metering in DC application @@ -47,18 +47,18 @@ AC Configuration DC Configuration ---------------- -In DC applications, the EvseManager still has an AC side that behaves similar -to a normal AC charger. The board_support module therefore still has to report +In DC applications, the EvseManager still has an AC side that behaves similar +to a normal AC charger. The board_support module therefore still has to report AC capabilities which refer to the AC input of the AC/DC power supply. If an AC -side RCD is used it also belongs to the board_support driver. -An AC side power meter can be connected and it will be used for Energy +side RCD is used it also belongs to the board_support driver. +An AC side power meter can be connected and it will be used for Energy management. In addition, on the DC side the following hardware modules can be connected: -* A DC powermeter: This will be used for billing purposes if present. +* A DC powermeter: This will be used for billing purposes if present. If not connected, billing will fall back to the AC side power meter. -* Isolation monitoring: This will be used to monitor isolation during +* Isolation monitoring: This will be used to monitor isolation during CableCheck, PreCharge and CurrentDemand steps. * DC power supply: This is the AC/DC converter that actually charges the car. @@ -68,16 +68,16 @@ Published variables session_events -------------- -EvseManager publishes the session_events variable whenever an event happens. -It does not publish its internal state but merely events that happen that can +EvseManager publishes the session_events variable whenever an event happens. +It does not publish its internal state but merely events that happen that can be used to drive an state machine within another module. -Example: Write a simple module that lights up an LED if the evse is reserved. -This module requires an EvseManager and subscribes to the session_events -variable. Internally it has only two states: Reserved (LED on), NotReserved +Example: Write a simple module that lights up an LED if the evse is reserved. +This module requires an EvseManager and subscribes to the session_events +variable. Internally it has only two states: Reserved (LED on), NotReserved (LED off). -The state machine transitions are driven by the two events from EvseManager: +The state machine transitions are driven by the two events from EvseManager: ReservationStart and ReservationEnd. All other events are ignored in this module as they are not needed. @@ -85,10 +85,10 @@ All other events are ignored in this module as they are not needed. powermeter ---------- -EvseManager republishes the power meter struct that if it has a powermeter -connected. This struct should be used for OCPP and display purposes. It comes -from the power meter that can be used for billing (DC side on DC, AC side on -AC). If no powermeter is connected EvseManager will never publish this +EvseManager republishes the power meter struct that if it has a powermeter +connected. This struct should be used for OCPP and display purposes. It comes +from the power meter that can be used for billing (DC side on DC, AC side on +AC). If no powermeter is connected EvseManager will never publish this variable. @@ -96,9 +96,9 @@ Authentication ============== The Auth modules validates tokens and assignes tokens to EvseManagers, see Auth -documentation. It will call ``Authorize(id_tag, pnc)`` on EvseManager to -indicated that the EvseManager may start the charging session. -Auth module may revoke authorization (``withdraw_authorization`` command) if +documentation. It will call ``Authorize(id_tag, pnc)`` on EvseManager to +indicated that the EvseManager may start the charging session. +Auth module may revoke authorization (``withdraw_authorization`` command) if the charging session has not begun yet (typically due to timeout), but not once charging has started. @@ -107,79 +107,79 @@ Autocharge / PnC ---------------- Autocharge is fully supported, PnC support is coming soon and will use the same -logic. The car itself is a token provider that can provide an auth token to be -validated by the Auth system (see Auth documentation for more details). +logic. The car itself is a token provider that can provide an auth token to be +validated by the Auth system (see Auth documentation for more details). EvseManager provides a ``token_provider`` interface for that purpose. -If external identification (EIM) is used in HLC (no PnC) then Autocharge is +If external identification (EIM) is used in HLC (no PnC) then Autocharge is enabled by connecting the ``token_provider`` interface to Auth module. When the -car sends its EVCCID in the HLC protocol it is converted to Autocharge format +car sends its EVCCID in the HLC protocol it is converted to Autocharge format and published as Auth token. It is based on the following specification: https://github.com/openfastchargingalliance/openfastchargingalliance/blob/master/autocharge-final.pdf -To enable PnC the config option ``payment_enable_contract`` must be set to -true. If the car selects Contract instead of EIM PnC will be used instead of +To enable PnC the config option ``payment_enable_contract`` must be set to +true. If the car selects Contract instead of EIM PnC will be used instead of Autocharge. Reservation ----------- -Reservation handling logic is implemented in the Auth module. If the Auth -module wants to reserve a specific EvseManager (or cancel the reservation) it -needs to call the reserve/cancel_reservation commands. EvseManager does not -check reservation id against the token id when it should start charging, this -must be handled in Auth module. EvseManager only needs to know whether it is +Reservation handling logic is implemented in the Auth module. If the Auth +module wants to reserve a specific EvseManager (or cancel the reservation) it +needs to call the reserve/cancel_reservation commands. EvseManager does not +check reservation id against the token id when it should start charging, this +must be handled in Auth module. EvseManager only needs to know whether it is reserved or not to emit an ReservatonStart/ReservationEnd event to notify other -modules such as OCPP and API or e.g. switch on a specific LED signal on the +modules such as OCPP and API or e.g. switch on a specific LED signal on the charging port. Energy Management ================= -EvseManager seamlessly intergrates into the EVerest Energy Management. +EvseManager seamlessly intergrates into the EVerest Energy Management. For further details refer to the documentation of the EnergyManager module. -EvseManager has a grid facing Energy interface which the energy tree uses to -provide energy for the charging sessions. New energy needs to be provided on -regular intervals (with a timeout). +EvseManager has a grid facing Energy interface which the energy tree uses to +provide energy for the charging sessions. New energy needs to be provided on +regular intervals (with a timeout). If the supplied energy limits time out, EvseManager will stop charging. -This prevents e.g. overload conditions when the network connection drops +This prevents e.g. overload conditions when the network connection drops between the energy tree and EvseManager. -EvseManager will send out its wishes at regular intervals: It sends a -requested energy schedule into the energy tree that is merged from hardware -capabilities (as reported by board_support module), EvseManager module -configuration settings -(max_current, three_phases) and external limts (via ``set_external_limits`` +EvseManager will send out its wishes at regular intervals: It sends a +requested energy schedule into the energy tree that is merged from hardware +capabilities (as reported by board_support module), EvseManager module +configuration settings +(max_current, three_phases) and external limts (via ``set_external_limits`` command) e.g. set by OCPP module. Note that the ``set_external_limits`` should not be used by multiple modules, as the last one always wins. If you have multiple sources of exernal limits -that you want to combine, add extra EnergyNode modules in the chain and +that you want to combine, add extra EnergyNode modules in the chain and feed in limits via those. -The combined schedule sent to the energy tree is the minimum of all energy +The combined schedule sent to the energy tree is the minimum of all energy limits. -After traversing the energy tree the EnergyManager will use this information -to assign limits (and a schedule) -for this EvseManager and will call enforce_limits on the energy interface. +After traversing the energy tree the EnergyManager will use this information +to assign limits (and a schedule) +for this EvseManager and will call enforce_limits on the energy interface. These values will then be used -to configure PWM/DC power supplies to actually charge the car and must not +to configure PWM/DC power supplies to actually charge the car and must not be confused with the original wishes that -were sent to the energy tree. +were sent to the energy tree. -The EvseManager will never assign energy to itself, it always requests energy +The EvseManager will never assign energy to itself, it always requests energy from the energy manager and only charges if the energy manager responds with an assignment. Limits in the energy object can be specified in ampere (per phase) and/or watt. -Currently watt limits are unsupported, but it should behave according to that +Currently watt limits are unsupported, but it should behave according to that logic: -If both are specified also both limits will be applied, whichever is lower. +If both are specified also both limits will be applied, whichever is lower. With DC charging, ampere limits apply to the AC side and watt limits apply to both AC and DC side. @@ -230,12 +230,13 @@ This module subscribes to all errors of the following requirements: * ac_rcd * isolation_monitor * power_supply_DC +* powermeter (if the config option fail_on_powermeter_errors is set true) A raised error can cause the EvseManager to become Inoperative. This means that charging is not possible until the error is cleared. If no charging session is currently running, it will prevent sessions from being started. If a charging session is currently running and an error is raised this will interrupt the charging session. -Almost all errors that are reported from the requirements of this module cause the EvseManager to become Inoperative until the error is cleared. +Almost all errors that are reported from the requirements of this module cause the EvseManager to become Inoperative until the error is cleared. The following sections provide an overview of the errors that do **not** cause the EvseManager to become Inoperative. evse_board_support @@ -265,4 +266,8 @@ power_supply_DC * power_supply_DC/VendorWarning +powermeter +---------- + +* powermeter/CommunicationFault diff --git a/modules/EvseManager/manifest.yaml b/modules/EvseManager/manifest.yaml index 1c912d700..512b5405a 100644 --- a/modules/EvseManager/manifest.yaml +++ b/modules/EvseManager/manifest.yaml @@ -192,7 +192,7 @@ config: default: 0.5 hack_fix_hlc_integer_current_requests: description: >- - Some cars request only integer ampere values during DC charging. For low power DC charging that + Some cars request only integer ampere values during DC charging. For low power DC charging that means that they charge a few hundred watts slower then needed. If enabled, this will charge at full power if the difference between EV requested current (integer) and HLC current limit is less then 1.0 type: boolean @@ -257,8 +257,8 @@ config: default: 10 switch_3ph1ph_cp_state: description: >- - CP state to use for switching. - WARNING: Some EVs may be permanently destroyed when switching from 1ph to 3ph. + CP state to use for switching. + WARNING: Some EVs may be permanently destroyed when switching from 1ph to 3ph. It is the responsibiltiy of the evse_board_support implementation to ensure the EV is capable of performing the switch. If it is not, the capabilities must set the supports_changing_phases_during_charging to false. Phase switching is only possible in basic charging mode. @@ -283,7 +283,7 @@ config: description: >- Set state F after any fault that stops charging for the specified time in ms while in Charging mode (CX->F(300ms)->C1/B1). When a fault occurs in state B2, no state F is added (B2->B1 on fault). - Some (especially older hybrid vehicles) may go into a permanent fault mode once they detect state F, + Some (especially older hybrid vehicles) may go into a permanent fault mode once they detect state F, in this case EVerest cannot recover the charging session if the fault is cleared. In this case you can set this parameter to 0, which will avoid to use state F in case of a fault and only disables PWM (C2->C1) while switching off power. This will violate IEC 61851-1:2017 however. @@ -291,6 +291,11 @@ config: This setting is only active in BASIC charging mode. type: integer default: 300 + fail_on_powermeter_errors: + description: >- + Set the EVSE Manager to an inoperative state if the powermeter requirement is configured and has reported errors + type: boolean + default: true provides: evse: interface: evse_manager From 356a4cd0cbf9596682b28c9575cc98bca8b2e7b2 Mon Sep 17 00:00:00 2001 From: Sebastian Lukas <45936573+SebaLukas@users.noreply.github.com> Date: Wed, 22 Jan 2025 10:21:37 +0100 Subject: [PATCH 2/4] Adding a few unit tests for the EvseV2G module. This should serve as a starting point for further tests: (#1013) - First din_server unit test prototype - adds handle_din_contract_authentication tests - adds handle_din_contract_authentication tests - fix: added missing functions to module adapter stub - Declaring exiDocument, v2g_connection & v2g_context as test class members and adding them to SetUp(). Adding some potential test cases for session setup. Adding a first not complete din service discovery test - feat: added collecting logs for EvseV2G tests for unit tests - fix: EvseV2G test now uses unique pointer to ensure clean structures for every test - Updating and adding session_setup state test - feat: added first testcases for din_validate_response_code - din_validate_response_code_DIN_tests - feat: added remainig testcases for din_validate_response_code - fix: created a quiet module adapter stub, fix: removed TODO that has been addressed, feat: added some tests around v2g_context - feat: add test file for sdp - Adding first sdp write header test Signed-off-by: Sebastian Lukas Co-authored-by: MarzellT Co-authored-by: James Chapman Co-authored-by: Anton Kadelbach --- modules/EvseV2G/connection/tls_connection.cpp | 3 - modules/EvseV2G/din_server.cpp | 39 +- modules/EvseV2G/din_server.hpp | 13 + modules/EvseV2G/sdp.cpp | 15 +- modules/EvseV2G/sdp.hpp | 14 + modules/EvseV2G/tests/CMakeLists.txt | 109 ++++ .../tests/ISO15118_chargerImplStub.hpp | 7 +- modules/EvseV2G/tests/din_server_test.cpp | 570 ++++++++++++++++++ .../EvseV2G/tests/evse_securityIntfStub.hpp | 10 +- modules/EvseV2G/tests/log.cpp | 35 +- modules/EvseV2G/tests/sdp_test.cpp | 31 + modules/EvseV2G/tests/utest_log.hpp | 15 + modules/EvseV2G/tests/v2g_ctx_test.cpp | 169 ++++++ modules/EvseV2G/tests/v2g_main.cpp | 9 +- tests/include/ModuleAdapterStub.hpp | 81 ++- 15 files changed, 1078 insertions(+), 42 deletions(-) create mode 100644 modules/EvseV2G/tests/din_server_test.cpp create mode 100644 modules/EvseV2G/tests/sdp_test.cpp create mode 100644 modules/EvseV2G/tests/utest_log.hpp create mode 100644 modules/EvseV2G/tests/v2g_ctx_test.cpp diff --git a/modules/EvseV2G/connection/tls_connection.cpp b/modules/EvseV2G/connection/tls_connection.cpp index 25d374c0a..0a6e34508 100644 --- a/modules/EvseV2G/connection/tls_connection.cpp +++ b/modules/EvseV2G/connection/tls_connection.cpp @@ -61,9 +61,6 @@ void process_connection_thread(std::shared_ptr con, struc const auto result = con->accept(); switch (result) { case tls::Connection::result_t::success: - - // TODO(james-ctc) v2g_ctx->tls_key_logging - if (ctx->state == 0) { const auto rv = ::v2g_handle_connection(connection.get()); dlog(DLOG_LEVEL_INFO, "v2g_dispatch_connection exited with %d", rv); diff --git a/modules/EvseV2G/din_server.cpp b/modules/EvseV2G/din_server.cpp index aa10f7292..bbea3995e 100644 --- a/modules/EvseV2G/din_server.cpp +++ b/modules/EvseV2G/din_server.cpp @@ -69,14 +69,14 @@ static din_responseCodeType din_validate_state(int state, enum V2gMsgTypeId curr : din_responseCodeType_FAILED_SequenceError; } +namespace utils { /*! * \brief din_validate_response_code This function checks if an external error has occurred (sequence error, user * abort)... ). \param din_response_code is a pointer to the current response code. The value will be modified if an * external error has occurred. \param conn the structure with the external error information. \return Returns the next * v2g-event. */ -static v2g_event din_validate_response_code(din_responseCodeType* const din_response_code, - struct v2g_connection const* conn) { +v2g_event din_validate_response_code(din_responseCodeType* const din_response_code, struct v2g_connection const* conn) { enum v2g_event nextEvent = V2G_EVENT_NO_EVENT; din_responseCodeType response_code_tmp; @@ -118,6 +118,7 @@ static v2g_event din_validate_response_code(din_responseCodeType* const din_resp return nextEvent; } +} // namespace utils /*! * \brief publish_DIN_DcEvStatus This function is a helper function to publish EVStatusType. @@ -295,6 +296,8 @@ static void publish_din_current_demand_req(struct v2g_context* ctx, // Request Handling //============================================= +namespace states { + /*! * \brief handle_iso_session_setup This function handles the din_session_setup msg pair. It analyzes the request msg and * fills the response msg. The request and response msg based on the open V2G structures. This structures must be @@ -302,7 +305,7 @@ static void publish_din_current_demand_req(struct v2g_context* ctx, * \param conn holds the structure with the V2G msg pair. * \return Returns the next V2G-event. */ -static enum v2g_event handle_din_session_setup(struct v2g_connection* conn) { +enum v2g_event handle_din_session_setup(struct v2g_connection* conn) { struct din_SessionSetupReqType* req = &conn->exi_in.dinEXIDocument->V2G_Message.Body.SessionSetupReq; struct din_SessionSetupResType* res = &conn->exi_out.dinEXIDocument->V2G_Message.Body.SessionSetupRes; enum v2g_event nextEvent = V2G_EVENT_NO_EVENT; @@ -348,7 +351,7 @@ static enum v2g_event handle_din_session_setup(struct v2g_connection* conn) { res->DateTimeNow = time(NULL); /* Check the current response code and check if no external error has occurred */ - nextEvent = din_validate_response_code(&res->ResponseCode, conn); + nextEvent = utils::din_validate_response_code(&res->ResponseCode, conn); /* Set next expected req msg */ conn->ctx->state = WAIT_FOR_SERVICEDISCOVERY; // [V2G-DC-438] @@ -363,7 +366,7 @@ static enum v2g_event handle_din_session_setup(struct v2g_connection* conn) { * \param conn is the structure with the V2G msg pair. * \return Returns the next V2G-event. */ -static enum v2g_event handle_din_service_discovery(struct v2g_connection* conn) { +enum v2g_event handle_din_service_discovery(struct v2g_connection* conn) { struct din_ServiceDiscoveryReqType* req = &conn->exi_in.dinEXIDocument->V2G_Message.Body.ServiceDiscoveryReq; struct din_ServiceDiscoveryResType* res = &conn->exi_out.dinEXIDocument->V2G_Message.Body.ServiceDiscoveryRes; enum v2g_event nextEvent = V2G_EVENT_NO_EVENT; @@ -401,7 +404,7 @@ static enum v2g_event handle_din_service_discovery(struct v2g_connection* conn) res->PaymentOptions.PaymentOption.arrayLen = 1; /* Check the current response code and check if no external error has occurred */ - nextEvent = din_validate_response_code(&res->ResponseCode, conn); + nextEvent = utils::din_validate_response_code(&res->ResponseCode, conn); /* Set next expected req msg */ conn->ctx->state = WAIT_FOR_PAYMENTSERVICESELECTION; // [V2G-DC-441] @@ -409,6 +412,8 @@ static enum v2g_event handle_din_service_discovery(struct v2g_connection* conn) return nextEvent; } +} // namespace states + /*! * \brief handle_din_service_discovery This function handles the din service payment selection msg pair. It analyzes the * request msg and fills the response msg. The request and response msg based on the open V2G structures. This @@ -441,7 +446,7 @@ static enum v2g_event handle_din_service_payment_selection(struct v2g_connection // shall be limited to 1) /* Check the current response code and check if no external error has occurred */ - nextEvent = din_validate_response_code(&res->ResponseCode, conn); + nextEvent = utils::din_validate_response_code(&res->ResponseCode, conn); /* Set next expected req msg */ conn->ctx->state = WAIT_FOR_AUTHORIZATION; // [V2G-DC-444] @@ -456,7 +461,7 @@ static enum v2g_event handle_din_service_payment_selection(struct v2g_connection * \param conn is the structure with the V2G msg pair. * \return Returns the next V2G-event. */ -static enum v2g_event handle_din_contract_authentication(struct v2g_connection* conn) { +enum v2g_event states::handle_din_contract_authentication(struct v2g_connection* conn) { struct din_ContractAuthenticationResType* res = &conn->exi_out.dinEXIDocument->V2G_Message.Body.ContractAuthenticationRes; enum v2g_event nextEvent = V2G_EVENT_NO_EVENT; @@ -468,7 +473,7 @@ static enum v2g_event handle_din_contract_authentication(struct v2g_connection* : din_EVSEProcessingType_Ongoing; /* Check the current response code and check if no external error has occurred */ - nextEvent = din_validate_response_code(&res->ResponseCode, conn); + nextEvent = utils::din_validate_response_code(&res->ResponseCode, conn); /* Set next expected req msg */ conn->ctx->state = (res->EVSEProcessing == din_EVSEProcessingType_Ongoing) @@ -600,7 +605,7 @@ static enum v2g_event handle_din_charge_parameter(struct v2g_connection* conn) { res->SASchedules_isUsed = (unsigned int)0; /* Check the current response code and check if no external error has occurred */ - nextEvent = din_validate_response_code(&res->ResponseCode, conn); + nextEvent = utils::din_validate_response_code(&res->ResponseCode, conn); /* Set next expected req msg */ if (res->EVSEProcessing == din_EVSEProcessingType_Finished) { @@ -684,7 +689,7 @@ static enum v2g_event handle_din_power_delivery(struct v2g_connection* conn) { : res->ResponseCode; // [V2G-DC-401] /* Check the current response code and check if no external error has occurred */ - nextEvent = din_validate_response_code(&res->ResponseCode, conn); + nextEvent = utils::din_validate_response_code(&res->ResponseCode, conn); /* Set next expected req msg */ if ((req->ReadyToChargeState == (int)1) && (V2G_CURRENT_DEMAND_MSG != conn->ctx->last_v2g_msg)) { @@ -740,7 +745,7 @@ static enum v2g_event handle_din_cable_check(struct v2g_connection* conn) { } /* Check the current response code and check if no external error has occurred */ - nextEvent = din_validate_response_code(&res->ResponseCode, conn); + nextEvent = utils::din_validate_response_code(&res->ResponseCode, conn); /* Set next expected req msg */ if ((res->EVSEProcessing == din_EVSEProcessingType_Finished) && @@ -794,7 +799,7 @@ static enum v2g_event handle_din_pre_charge(struct v2g_connection* conn) { load_din_physical_value(&res->EVSEPresentVoltage, &conn->ctx->evse_v2g_data.evse_present_voltage); /* Check the current response code and check if no external error has occurred */ - nextEvent = din_validate_response_code(&res->ResponseCode, conn); + nextEvent = utils::din_validate_response_code(&res->ResponseCode, conn); /* Set next expected req msg */ conn->ctx->state = WAIT_FOR_PRECHARGE_POWERDELIVERY; // [V2G-DC-458] @@ -848,7 +853,7 @@ static enum v2g_event handle_din_current_demand(struct v2g_connection* conn) { res->EVSEVoltageLimitAchieved = conn->ctx->evse_v2g_data.evse_voltage_limit_achieved; /* Check the current response code and check if no external error has occurred */ - nextEvent = din_validate_response_code(&res->ResponseCode, conn); + nextEvent = utils::din_validate_response_code(&res->ResponseCode, conn); /* Set next expected req msg */ conn->ctx->state = WAIT_FOR_CURRENTDEMAND_POWERDELIVERY; // [V2G-DC-465] @@ -888,7 +893,7 @@ static enum v2g_event handle_din_welding_detection(struct v2g_connection* conn) load_din_physical_value(&res->EVSEPresentVoltage, &conn->ctx->evse_v2g_data.evse_present_voltage); /* Check the current response code and check if no external error has occurred */ - nextEvent = din_validate_response_code(&res->ResponseCode, conn); + nextEvent = utils::din_validate_response_code(&res->ResponseCode, conn); /* Set next expected req msg */ conn->ctx->state = WAIT_FOR_WELDINGDETECTION_SESSIONSTOP; // [V2G-DC-469] @@ -910,7 +915,7 @@ static enum v2g_event handle_din_session_stop(struct v2g_connection* conn) { res->ResponseCode = din_responseCodeType_OK; // [V2G-DC-388] /* Check the current response code and check if no external error has occurred */ - din_validate_response_code(&res->ResponseCode, conn); + utils::din_validate_response_code(&res->ResponseCode, conn); /* Setuo dlink action */ conn->dlink_action = MQTT_DLINK_ACTION_TERMINATE; @@ -923,6 +928,8 @@ static enum v2g_event handle_din_session_stop(struct v2g_connection* conn) { } enum v2g_event din_handle_request(v2g_connection* conn) { + using namespace states; + struct din_exiDocument* exi_in = conn->exi_in.dinEXIDocument; struct din_exiDocument* exi_out = conn->exi_out.dinEXIDocument; enum v2g_event next_v2g_event = V2G_EVENT_TERMINATE_CONNECTION; // ERROR_UNEXPECTED_REQUEST_MESSAGE; diff --git a/modules/EvseV2G/din_server.hpp b/modules/EvseV2G/din_server.hpp index 33150609d..276c178ac 100644 --- a/modules/EvseV2G/din_server.hpp +++ b/modules/EvseV2G/din_server.hpp @@ -104,4 +104,17 @@ static const struct din_state din_states[] = { */ enum v2g_event din_handle_request(v2g_connection* conn); +namespace utils { +enum v2g_event din_validate_response_code(din_responseCodeType* const din_response_code, + struct v2g_connection const* conn); +} // namespace utils + +namespace states { + +enum v2g_event handle_din_session_setup(struct v2g_connection* conn); +enum v2g_event handle_din_service_discovery(struct v2g_connection* conn); +enum v2g_event handle_din_contract_authentication(struct v2g_connection* conn); + +} // namespace states + #endif /* DIN_SERVER_HPP */ diff --git a/modules/EvseV2G/sdp.cpp b/modules/EvseV2G/sdp.cpp index dc48d1e8b..c2d89fd62 100644 --- a/modules/EvseV2G/sdp.cpp +++ b/modules/EvseV2G/sdp.cpp @@ -36,16 +36,6 @@ #define POLL_TIMEOUT 20 -enum sdp_security { - SDP_SECURITY_TLS = 0x00, - SDP_SECURITY_NONE = 0x10, -}; - -enum sdp_transport_protocol { - SDP_TRANSPORT_PROTOCOL_TCP = 0x00, - SDP_TRANSPORT_PROTOCOL_UDP = 0x10, -}; - /* link-local multicast address ff02::1 aka ip6-allnodes */ #define IN6ADDR_ALLNODES \ { 0xff, 0x02, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x01 } @@ -63,7 +53,7 @@ struct sdp_query { /* * Fills the SDP header into a given buffer */ -static int sdp_write_header(uint8_t* buffer, uint16_t payload_type, uint32_t payload_len) { +int sdp_write_header(uint8_t* buffer, uint16_t payload_type, uint32_t payload_len) { int offset = 0; buffer[offset++] = SDP_VERSION; @@ -82,7 +72,7 @@ static int sdp_write_header(uint8_t* buffer, uint16_t payload_type, uint32_t pay return offset; } -static int sdp_validate_header(uint8_t* buffer, uint16_t expected_payload_type, uint32_t expected_payload_len) { +int sdp_validate_header(uint8_t* buffer, uint16_t expected_payload_type, uint32_t expected_payload_len) { uint16_t payload_type; uint32_t payload_len; @@ -134,7 +124,6 @@ int sdp_create_response(uint8_t* buffer, struct sockaddr_in6* addr, enum sdp_sec return offset; } - /* * Sends a SDP response packet */ diff --git a/modules/EvseV2G/sdp.hpp b/modules/EvseV2G/sdp.hpp index a689d3097..3fb1ab001 100644 --- a/modules/EvseV2G/sdp.hpp +++ b/modules/EvseV2G/sdp.hpp @@ -6,6 +6,20 @@ #include "v2g.hpp" +enum sdp_security { + SDP_SECURITY_TLS = 0x00, + SDP_SECURITY_NONE = 0x10, +}; + +enum sdp_transport_protocol { + SDP_TRANSPORT_PROTOCOL_TCP = 0x00, + SDP_TRANSPORT_PROTOCOL_UDP = 0x10, +}; + +int sdp_write_header(uint8_t* buffer, uint16_t payload_type, uint32_t payload_len); +int sdp_validate_header(uint8_t* buffer, uint16_t expected_payload_type, uint32_t expected_payload_len); +int sdp_create_response(uint8_t* buffer, struct sockaddr_in6* addr, enum sdp_security security, + enum sdp_transport_protocol proto); int sdp_init(struct v2g_context* v2g_ctx); int sdp_listen(struct v2g_context* v2g_ctx); diff --git a/modules/EvseV2G/tests/CMakeLists.txt b/modules/EvseV2G/tests/CMakeLists.txt index 76a08b190..c1212b974 100644 --- a/modules/EvseV2G/tests/CMakeLists.txt +++ b/modules/EvseV2G/tests/CMakeLists.txt @@ -1,5 +1,6 @@ get_target_property(GENERATED_INCLUDE_DIR generate_cpp_files EVEREST_GENERATED_INCLUDE_DIR) find_package(libevent) +find_package(OpenSSL 3) set(TLS_TEST_FILES alt_openssl-pki.conf @@ -93,3 +94,111 @@ target_link_libraries(${V2G_MAIN_NAME} PRIVATE # runs fine locally, fails in CI add_test(${TLS_GTEST_NAME} ${TLS_GTEST_NAME}) ev_register_test_target(${TLS_GTEST_NAME}) + + +set(DIN_SERVER_NAME din_server_test) +add_executable(${DIN_SERVER_NAME}) + +target_include_directories(${DIN_SERVER_NAME} PRIVATE + .. ../connection ../../../tests/include + ${GENERATED_INCLUDE_DIR} + ${CMAKE_BINARY_DIR}/generated/modules/${MODULE_NAME} + ${CMAKE_BINARY_DIR}/generated/include +) + +target_compile_definitions(${DIN_SERVER_NAME} PRIVATE + -DUNIT_TEST + -DLIBEVSE_CRYPTO_SUPPLIER_OPENSSL +) + +target_sources(${DIN_SERVER_NAME} PRIVATE + din_server_test.cpp + log.cpp + ../din_server.cpp + ../tools.cpp # TODO: Maybe mock this one +) + +target_link_libraries(${DIN_SERVER_NAME} + PRIVATE + GTest::gtest_main + OpenSSL::SSL + OpenSSL::Crypto + cbv2g::din + cbv2g::iso2 + cbv2g::tp + everest::framework + everest::evse_security + everest::tls +) + +add_test(${DIN_SERVER_NAME} ${DIN_SERVER_NAME}) +ev_register_test_target(${DIN_SERVER_NAME}) + +set(SDP_NAME sdp_test) +add_executable(${SDP_NAME}) +target_include_directories(${SDP_NAME} PRIVATE + .. ../connection ../../../tests/include + ${GENERATED_INCLUDE_DIR} + ${CMAKE_BINARY_DIR}/generated/modules/${MODULE_NAME} + ${CMAKE_BINARY_DIR}/generated/include +) + +target_compile_definitions(${SDP_NAME} PRIVATE + -DUNIT_TEST +) + +target_sources(${SDP_NAME} PRIVATE + sdp_test.cpp + log.cpp + ../sdp.cpp +) + +target_link_libraries(${SDP_NAME} + PRIVATE + GTest::gtest_main + cbv2g::tp + everest::framework + everest::tls +) + +add_test(${SDP_NAME} ${SDP_NAME}) +ev_register_test_target(${SDP_NAME}) + +set(V2GCTX_NAME v2g_ctx_test) +add_executable(${V2GCTX_NAME}) + +target_include_directories(${V2GCTX_NAME} PRIVATE + .. ../connection ../../../tests/include + ${GENERATED_INCLUDE_DIR} + ${CMAKE_BINARY_DIR}/generated/modules/${MODULE_NAME} + ${CMAKE_BINARY_DIR}/generated/include +) + +target_compile_definitions(${V2GCTX_NAME} PRIVATE + -DUNIT_TEST + -DLIBEVSE_CRYPTO_SUPPLIER_OPENSSL +) + +target_sources(${V2GCTX_NAME} PRIVATE + v2g_ctx_test.cpp + log.cpp + ../v2g_ctx.cpp + ../tools.cpp # TODO: Maybe mock this one +) + +target_link_libraries(${V2GCTX_NAME} + PRIVATE + GTest::gtest_main + OpenSSL::SSL + OpenSSL::Crypto + cbv2g::din + cbv2g::iso2 + cbv2g::tp + everest::framework + everest::evse_security + everest::tls + -levent -lpthread -levent_pthreads +) + +add_test(${V2GCTX_NAME} ${V2GCTX_NAME}) +ev_register_test_target(${V2GCTX_NAME}) diff --git a/modules/EvseV2G/tests/ISO15118_chargerImplStub.hpp b/modules/EvseV2G/tests/ISO15118_chargerImplStub.hpp index d3a33f315..6093cc446 100644 --- a/modules/EvseV2G/tests/ISO15118_chargerImplStub.hpp +++ b/modules/EvseV2G/tests/ISO15118_chargerImplStub.hpp @@ -5,6 +5,9 @@ #define ISO15118_CHARGERIMPLSTUB_H_ #include +#include + +#include "ModuleAdapterStub.hpp" #include @@ -12,8 +15,8 @@ namespace module::stub { struct ISO15118_chargerImplStub : public ISO15118_chargerImplBase { -public: - ISO15118_chargerImplStub() : ISO15118_chargerImplBase(nullptr, "EvseV2G"){}; + ISO15118_chargerImplStub(ModuleAdapterStub& adapter) : ISO15118_chargerImplBase(&adapter, "EvseV2G"){}; + ISO15118_chargerImplStub(ModuleAdapterStub* adapter) : ISO15118_chargerImplBase(adapter, "EvseV2G"){}; virtual void init() { } diff --git a/modules/EvseV2G/tests/din_server_test.cpp b/modules/EvseV2G/tests/din_server_test.cpp new file mode 100644 index 000000000..8aee60287 --- /dev/null +++ b/modules/EvseV2G/tests/din_server_test.cpp @@ -0,0 +1,570 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#include + +#include +#include + +#include "ISO15118_chargerImplStub.hpp" +#include "cbv2g/din/din_msgDefDatatypes.h" +#include "utest_log.hpp" +#include "v2g.hpp" + +#include + +void publish_dc_ev_maximum_limits(struct v2g_context* ctx, const float& v2g_dc_ev_max_current_limit, + const unsigned int& v2g_dc_ev_max_current_limit_is_used, + const float& v2g_dc_ev_max_power_limit, + const unsigned int& v2g_dc_ev_max_power_limit_is_used, + const float& v2g_dc_ev_max_voltage_limit, + const unsigned int& v2g_dc_ev_max_voltage_limit_is_used) { +} + +void stop_timer(struct event** event_timer, char const* const timer_name, struct v2g_context* ctx) { +} + +void log_selected_energy_transfer_type(int selected_energy_transfer_mode) { +} + +uint64_t v2g_session_id_from_exi(bool is_iso, void* exi_in) { + return 0; +} + +void publish_dc_ev_target_voltage_current(struct v2g_context* ctx, const float& v2g_dc_ev_target_voltage, + const float& v2g_dc_ev_target_current) { +} + +void publish_dc_ev_remaining_time(struct v2g_context* ctx, const float& v2g_dc_ev_remaining_time_to_full_soc, + const unsigned int& v2g_dc_ev_remaining_time_to_full_soc_is_used, + const float& v2g_dc_ev_remaining_time_to_bulk_soc, + const unsigned int& v2g_dc_ev_remaining_time_to_bulk_soc_is_used) { +} + +namespace { +class DinServerTest : public testing::Test { +protected: + std::unique_ptr conn; + std::unique_ptr ctx; + std::unique_ptr exi_in; + std::unique_ptr exi_out; + + module::stub::ModuleAdapterStub adapter; + module::stub::ISO15118_chargerImplStub charger; + + DinServerTest() : charger(adapter) { + } + + void SetUp() override { + conn = std::make_unique(); + ctx = std::make_unique(); + exi_in = std::make_unique(); + exi_out = std::make_unique(); + + module::stub::clear_logs(); + conn->ctx = ctx.get(); + conn->ctx->p_charger = &charger; + + conn->exi_in.dinEXIDocument = exi_in.get(); + conn->exi_out.dinEXIDocument = exi_out.get(); + } + + void TearDown() override { + } +}; + +class DinServerTestValidateResponseCode + : public DinServerTest, + public testing::WithParamInterface< + std::tuple> { +}; + +// For all test cases: +// TODO: Define helper functions to set the conn and ctx variables + +// ---------------------------------------------------------------- + +// Potential test for SessionSetup: +// Bad Case: +// Setting no EvseID -> A check should be added -> But a default value is in ctx provided. +TEST_F(DinServerTest, session_setup_generating_new_session_id) { + // Setting up session_setup_req + auto& session_setup_req = exi_in->V2G_Message.Body.SessionSetupReq; + exi_in->V2G_Message.Body.SessionSetupReq_isUsed = true; + init_din_SessionSetupReqType(&session_setup_req); + + const uint8_t evcc_id[8] = {0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11}; + memcpy(session_setup_req.EVCCID.bytes, evcc_id, sizeof(evcc_id)); + session_setup_req.EVCCID.bytesLen = sizeof(evcc_id); + + // Setting up conn + ctx->current_v2g_msg = V2G_SESSION_SETUP_MSG; + ctx->com_setup_timeout = nullptr; + + ctx->evse_v2g_data.session_id = 0; + ctx->evse_v2g_data.date_time_now_is_used = 0; + + ctx->ev_v2g_data.received_session_id = 0; + + std::string evse_id = std::string("DE*PNX*TET1*234"); + strcpy(reinterpret_cast(ctx->evse_v2g_data.evse_id.bytes), evse_id.data()); + ctx->evse_v2g_data.evse_id.bytesLen = evse_id.size(); + + // Setting up session_setup_res + auto& session_setup_res = exi_out->V2G_Message.Body.SessionSetupRes; + exi_out->V2G_Message.Body.SessionSetupRes_isUsed = 1u; + init_din_SessionSetupResType(&session_setup_res); + + EXPECT_EQ(states::handle_din_session_setup(conn.get()), V2G_EVENT_NO_EVENT); + EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_ERROR).size(), 0); + EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_INFO).size(), 3); + + EXPECT_EQ(session_setup_res.DateTimeNow_isUsed, false); + // Checking if session id is generated + EXPECT_GT(ctx->evse_v2g_data.session_id, 0); + // Checking if evse id was set correctly + EXPECT_EQ(evse_id, std::string(reinterpret_cast(session_setup_res.EVSEID.bytes))); +} + +TEST_F(DinServerTest, session_setup_old_session_id) { + // Setting up session_setup_req + auto& session_setup_req = exi_in->V2G_Message.Body.SessionSetupReq; + exi_in->V2G_Message.Body.SessionSetupReq_isUsed = true; + init_din_SessionSetupReqType(&session_setup_req); + + const uint8_t evcc_id[8] = {0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11}; + memcpy(session_setup_req.EVCCID.bytes, evcc_id, sizeof(evcc_id)); + session_setup_req.EVCCID.bytesLen = sizeof(evcc_id); + + // Setting up conn + ctx->current_v2g_msg = V2G_SESSION_SETUP_MSG; + ctx->com_setup_timeout = nullptr; + + ctx->evse_v2g_data.session_id = 4158610156; + ctx->evse_v2g_data.date_time_now_is_used = 0; + + ctx->ev_v2g_data.received_session_id = 0; + + std::string evse_id = std::string("DE*PNX*TET1*234"); + strcpy(reinterpret_cast(ctx->evse_v2g_data.evse_id.bytes), evse_id.data()); + ctx->evse_v2g_data.evse_id.bytesLen = evse_id.size(); + + // Setting up session_setup_res + auto& session_setup_res = exi_out->V2G_Message.Body.SessionSetupRes; + exi_out->V2G_Message.Body.SessionSetupRes_isUsed = 1u; + init_din_SessionSetupResType(&session_setup_res); + + EXPECT_EQ(states::handle_din_session_setup(conn.get()), V2G_EVENT_NO_EVENT); + EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_ERROR).size(), 0); + EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_INFO).size(), 2); + + EXPECT_EQ(session_setup_res.DateTimeNow_isUsed, false); + // Checking if session id is generated + EXPECT_EQ(ctx->evse_v2g_data.session_id, 4158610156); + // Checking if evse id was set correctly + EXPECT_EQ(evse_id, std::string(reinterpret_cast(session_setup_res.EVSEID.bytes))); +} + +TEST_F(DinServerTest, session_setup_datetime_is_used) { + // Setting up session_setup_req + auto& session_setup_req = exi_in->V2G_Message.Body.SessionSetupReq; + exi_in->V2G_Message.Body.SessionSetupReq_isUsed = true; + init_din_SessionSetupReqType(&session_setup_req); + + const uint8_t evcc_id[8] = {0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x11}; + memcpy(session_setup_req.EVCCID.bytes, evcc_id, sizeof(evcc_id)); + session_setup_req.EVCCID.bytesLen = sizeof(evcc_id); + + // Setting up conn + ctx->current_v2g_msg = V2G_SESSION_SETUP_MSG; + ctx->com_setup_timeout = nullptr; + + ctx->evse_v2g_data.session_id = 0; + ctx->evse_v2g_data.date_time_now_is_used = true; + + ctx->ev_v2g_data.received_session_id = 0; + + std::string evse_id = std::string("DE*PNX*TET1*234"); + strcpy(reinterpret_cast(ctx->evse_v2g_data.evse_id.bytes), evse_id.data()); + ctx->evse_v2g_data.evse_id.bytesLen = evse_id.size(); + + // Setting up session_setup_res + auto& session_setup_res = exi_out->V2G_Message.Body.SessionSetupRes; + exi_out->V2G_Message.Body.SessionSetupRes_isUsed = 1u; + init_din_SessionSetupResType(&session_setup_res); + + EXPECT_EQ(states::handle_din_session_setup(conn.get()), V2G_EVENT_NO_EVENT); + EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_ERROR).size(), 0); + EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_INFO).size(), 3); + + EXPECT_EQ(session_setup_res.DateTimeNow_isUsed, true); + EXPECT_GT(session_setup_res.DateTimeNow, 0); + // Checking if session id is generated + EXPECT_GT(ctx->evse_v2g_data.session_id, 0); + // Checking if evse id was set correctly + EXPECT_EQ(evse_id, std::string(reinterpret_cast(session_setup_res.EVSEID.bytes))); +} + +TEST_F(DinServerTest, din_service_discovery_good_case) { + + // TODO(sl): Maybe add this to check exi_out proberly + exi_out->V2G_Message.Body.ServiceDiscoveryRes_isUsed = true; + init_din_ServiceDiscoveryResType(&exi_out->V2G_Message.Body.ServiceDiscoveryRes); + + // TODO: Setting the correct session_id + received_session_id via functions + + EXPECT_EQ(states::handle_din_service_discovery(conn.get()), V2G_EVENT_NO_EVENT); + EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_ERROR).size(), 1); + EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_INFO).size(), 0); +} + +TEST_F(DinServerTest, handle_din_contract_authentication_check_evse_processing_finished) { + + ctx->com_setup_timeout = nullptr; + + // TODO: set a prober session id + ctx->evse_v2g_data.session_id = 0; + ctx->evse_v2g_data.date_time_now_is_used = 0; + + ctx->current_v2g_msg = V2G_AUTHORIZATION_MSG; + ctx->ev_v2g_data.received_session_id = 0; + + ctx->evse_v2g_data.evse_processing[PHASE_AUTH] = 0; + EXPECT_EQ(states::handle_din_contract_authentication(conn.get()), V2G_EVENT_NO_EVENT); // TODO + + auto& res = exi_out->V2G_Message.Body.ContractAuthenticationRes; + + // EXPECT_EQ(res.ResponseCode, din_responseCodeType_OK); + EXPECT_EQ(res.EVSEProcessing, din_EVSEProcessingType_Finished); + EXPECT_EQ(ctx->state, WAIT_FOR_CHARGEPARAMETERDISCOVERY); + EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_ERROR).size(), 1); + EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_INFO).size(), 0); +} + +TEST_F(DinServerTest, handle_din_contract_authentication_check_evse_processing_ongoing) { + + ctx->com_setup_timeout = nullptr; + + // TODO: set a prober session id + ctx->evse_v2g_data.session_id = 0; + ctx->evse_v2g_data.date_time_now_is_used = 0; + + ctx->current_v2g_msg = V2G_AUTHORIZATION_MSG; + ctx->ev_v2g_data.received_session_id = 0; + + ctx->evse_v2g_data.evse_processing[PHASE_AUTH] = 1; + EXPECT_EQ(states::handle_din_contract_authentication(conn.get()), V2G_EVENT_NO_EVENT); // TODO + + auto& res = exi_out->V2G_Message.Body.ContractAuthenticationRes; + + // EXPECT_EQ(res.ResponseCode, din_responseCodeType_OK); + EXPECT_EQ(res.EVSEProcessing, din_EVSEProcessingType_Ongoing); + EXPECT_EQ(ctx->state, WAIT_FOR_AUTHORIZATION); + EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_ERROR).size(), 1); + EXPECT_EQ(module::stub::get_logs(dloglevel_t::DLOG_LEVEL_INFO).size(), 0); +} + +// if not otherwise specified, the following testcases are happy paths + +TEST_F(DinServerTest, din_validate_response_code_TERMINATE_CONNECTION) { + + // which response code is actually irrelevant here and was picked at random + auto tmp = din_responseCodeType_FAILED_TariffSelectionInvalid; + + // only this bool determines the outcome + ctx->is_connection_terminated = true; + + EXPECT_EQ(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_TERMINATE_CONNECTION); +} + +TEST_F(DinServerTest, din_validate_response_code_EVENT_NO_EVENT_failed_response_FAILED) { + + // which response code is actually irrelevant here and was picked at random + // FAILED code + auto tmp = din_responseCodeType_FAILED_ChallengeInvalid; + + ctx->is_connection_terminated = false; + + ctx->terminate_connection_on_failed_response = false; + + EXPECT_EQ(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_NO_EVENT); +} + +TEST_F(DinServerTest, din_validate_response_code_EVENT_NO_EVENT_failed_response_OK) { + + // which response code is actually irrelevant here and was picked at random + // OK code + auto tmp = din_responseCodeType_OK; + + ctx->is_connection_terminated = false; + + ctx->terminate_connection_on_failed_response = false; + + EXPECT_EQ(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_NO_EVENT); +} + +TEST_F(DinServerTest, din_validate_response_code_EVENT_NO_EVENT_OK) { + + // which response code is actually irrelevant here and was picked at random + // OK code + auto tmp = din_responseCodeType_OK; + + ctx->is_connection_terminated = false; + + ctx->stop_hlc = false; //|| + ctx->intl_emergency_shutdown = false; + + ctx->current_v2g_msg = V2G_SESSION_SETUP_MSG; // && + ctx->evse_v2g_data.session_id = 1; + ctx->ev_v2g_data.received_session_id = 2; + + ctx->terminate_connection_on_failed_response = true; + + EXPECT_EQ(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_NO_EVENT); +} + +TEST_F(DinServerTest, din_validate_response_code_EVENT_NO_EVENT_OK_bad_path_1) { + + // OK code + auto tmp = din_responseCodeType_OK; + + ctx->is_connection_terminated = false; + + ctx->stop_hlc = true; //|| + ctx->intl_emergency_shutdown = false; + + ctx->current_v2g_msg = V2G_SESSION_SETUP_MSG; // && + ctx->evse_v2g_data.session_id = 1; + ctx->ev_v2g_data.received_session_id = 2; + + ctx->terminate_connection_on_failed_response = true; + + EXPECT_NE(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_NO_EVENT); +} + +TEST_F(DinServerTest, din_validate_response_code_EVENT_NO_EVENT_OK_bad_path_2) { + + // OK code + auto tmp = din_responseCodeType_OK; + + ctx->is_connection_terminated = false; + + ctx->stop_hlc = false; //|| + ctx->intl_emergency_shutdown = true; + + ctx->current_v2g_msg = V2G_SESSION_SETUP_MSG; // && + ctx->evse_v2g_data.session_id = 1; + ctx->ev_v2g_data.received_session_id = 2; + + ctx->terminate_connection_on_failed_response = true; + + EXPECT_NE(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_NO_EVENT); +} + +TEST_F(DinServerTest, din_validate_response_code_EVENT_NO_EVENT_OK_bad_path_3) { + + // OK code + auto tmp = din_responseCodeType_OK; + + ctx->is_connection_terminated = false; + + ctx->stop_hlc = false; //|| + ctx->intl_emergency_shutdown = false; + + ctx->current_v2g_msg = V2G_CERTIFICATE_INSTALLATION_MSG; // && + ctx->evse_v2g_data.session_id = 1; + ctx->ev_v2g_data.received_session_id = 2; + + ctx->terminate_connection_on_failed_response = true; + + EXPECT_NE(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_NO_EVENT); +} + +TEST_F(DinServerTest, din_validate_response_code_EVENT_NO_EVENT_OK_bad_path_4) { + + // OK code + auto tmp = din_responseCodeType_OK; + + ctx->is_connection_terminated = false; + + ctx->stop_hlc = false; //|| + ctx->intl_emergency_shutdown = true; + + ctx->current_v2g_msg = V2G_SESSION_SETUP_MSG; // && + ctx->evse_v2g_data.session_id = 6; + ctx->ev_v2g_data.received_session_id = 6; + + ctx->terminate_connection_on_failed_response = true; + + EXPECT_NE(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_NO_EVENT); +} + +TEST_F(DinServerTest, din_validate_response_code_EVENT_SEND_AND_TERMINATE_1) { + + auto tmp = din_responseCodeType_FAILED_WrongEnergyTransferType; + + ctx->is_connection_terminated = false; + + ctx->stop_hlc = true; //|| + ctx->intl_emergency_shutdown = false; + + ctx->current_v2g_msg = V2G_METERING_RECEIPT_MSG; // && + ctx->evse_v2g_data.session_id = 6; + ctx->ev_v2g_data.received_session_id = 6; + + ctx->terminate_connection_on_failed_response = true; + + EXPECT_EQ(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_SEND_AND_TERMINATE); +} + +TEST_F(DinServerTest, din_validate_response_code_EVENT_SEND_AND_TERMINATE_2) { + + auto tmp = din_responseCodeType_FAILED_MeteringSignatureNotValid; + + ctx->is_connection_terminated = false; + + ctx->stop_hlc = false; //|| + ctx->intl_emergency_shutdown = true; + + ctx->current_v2g_msg = V2G_CHARGING_STATUS_MSG; // && + ctx->evse_v2g_data.session_id = 1; + ctx->ev_v2g_data.received_session_id = 6; + + ctx->terminate_connection_on_failed_response = true; + + EXPECT_EQ(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_SEND_AND_TERMINATE); +} + +TEST_F(DinServerTest, din_validate_response_code_EVENT_SEND_AND_TERMINATE_3) { + + auto tmp = din_responseCodeType_OK_CertificateExpiresSoon; + + ctx->is_connection_terminated = false; + + ctx->stop_hlc = false; //|| + ctx->intl_emergency_shutdown = false; + + ctx->current_v2g_msg = V2G_UNKNOWN_MSG; // && + ctx->evse_v2g_data.session_id = 1; + ctx->ev_v2g_data.received_session_id = 1; + + ctx->terminate_connection_on_failed_response = true; + + EXPECT_EQ(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_SEND_AND_TERMINATE); +} + +TEST_F(DinServerTest, din_validate_response_code_EVENT_SEND_AND_TERMINATE_4) { + + auto tmp = din_responseCodeType_OK_CertificateExpiresSoon; + + ctx->is_connection_terminated = false; + + ctx->stop_hlc = false; //|| + ctx->intl_emergency_shutdown = false; + + ctx->current_v2g_msg = V2G_UNKNOWN_MSG; // && + ctx->evse_v2g_data.session_id = 1; + ctx->ev_v2g_data.received_session_id = 1; + + ctx->terminate_connection_on_failed_response = true; + + EXPECT_EQ(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_SEND_AND_TERMINATE); +} + +TEST_F(DinServerTest, din_validate_response_code_EVENT_SEND_AND_TERMINATE_5) { + + auto tmp = din_responseCodeType_OK_CertificateExpiresSoon; + + ctx->is_connection_terminated = false; + + ctx->stop_hlc = false; //|| + ctx->intl_emergency_shutdown = false; + + ctx->current_v2g_msg = V2G_CABLE_CHECK_MSG; // && + ctx->evse_v2g_data.session_id = 1; + ctx->ev_v2g_data.received_session_id = 2; + + ctx->terminate_connection_on_failed_response = true; + + EXPECT_EQ(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_SEND_AND_TERMINATE); +} + +TEST_F(DinServerTest, din_validate_response_code_EVENT_SEND_AND_TERMINATE_6) { + + auto tmp = din_responseCodeType_FAILED_SequenceError; + + ctx->is_connection_terminated = false; + + ctx->stop_hlc = false; //|| + ctx->intl_emergency_shutdown = false; + + ctx->current_v2g_msg = V2G_SESSION_SETUP_MSG; // && + ctx->evse_v2g_data.session_id = 1; + ctx->ev_v2g_data.received_session_id = 2; + + ctx->terminate_connection_on_failed_response = true; + + EXPECT_EQ(utils::din_validate_response_code(&tmp, conn.get()), V2G_EVENT_SEND_AND_TERMINATE); +} +TEST_F(DinServerTest, din_validate_response_code_V2G_DC_390) { + // The response message shall contain the ResponseCode “FAILED_SequenceError” if the + // SECC has received an unexpected request message. + + auto given_response_code = din_responseCodeType_OK; + constexpr auto expected_response_code = din_responseCodeType_FAILED_SequenceError; + constexpr auto expected_response = V2G_EVENT_NO_EVENT; + + ctx->is_connection_terminated = false; + + ctx->terminate_connection_on_failed_response = false; + + ctx->current_v2g_msg = V2G_UNKNOWN_MSG; + + EXPECT_EQ(utils::din_validate_response_code(&given_response_code, conn.get()), expected_response); + // given response code should change in the function call + EXPECT_EQ(given_response_code, expected_response_code); +} + +TEST_F(DinServerTest, din_validate_response_code_V2G_DC_391) { + // The response message shall contain the ResponseCode “FAILED_UnknownSession” if the + // SessionID in a request message does not match the SessionID provided by the SECC in the SessionSetupRes + // message. + + auto given_response_code = din_responseCodeType_OK; + constexpr auto expected_response_code = din_responseCodeType_FAILED_UnknownSession; + constexpr auto expected_response = V2G_EVENT_NO_EVENT; + + ctx->is_connection_terminated = false; + + ctx->terminate_connection_on_failed_response = false; + ctx->evse_v2g_data.session_id = 1234; + ctx->ev_v2g_data.received_session_id = 5678; + + EXPECT_EQ(utils::din_validate_response_code(&given_response_code, conn.get()), expected_response); + // given response code should change in the function call + EXPECT_EQ(given_response_code, expected_response_code); +} + +TEST_F(DinServerTest, din_validate_response_code_V2G_DC_665) { + // If the SECC receives a request message that it expects according to the message sequence + // specified in this chapter, and if the SECC cannot process this request message, e. g. due to + // errors in the message parameters or due to impeding conditions in the EVSE, the SECC + // shall: + // [1.] without any delay, carry out an “EVSE-initiated emergency shutdown” as specified in + // IEC 61851-23, which includes turning off the CP oscillator, if it is turned on, + // [2.] respond with the corresponding response message with parameter ResponseCode + // equal to “FAILED”, if possible, and + // [3.] close the TCP connection according to [V2G-DC-116]. + + auto given_response_code = din_responseCodeType_FAILED; + constexpr auto min_expected_response_code = din_responseCodeType_FAILED; + constexpr auto expected_response = V2G_EVENT_SEND_AND_TERMINATE; + + ctx->is_connection_terminated = false; + + ctx->terminate_connection_on_failed_response = true; + + EXPECT_EQ(utils::din_validate_response_code(&given_response_code, conn.get()), expected_response); + EXPECT_GE(given_response_code, min_expected_response_code); +} + +} // namespace diff --git a/modules/EvseV2G/tests/evse_securityIntfStub.hpp b/modules/EvseV2G/tests/evse_securityIntfStub.hpp index 5768457c6..25200cd53 100644 --- a/modules/EvseV2G/tests/evse_securityIntfStub.hpp +++ b/modules/EvseV2G/tests/evse_securityIntfStub.hpp @@ -17,13 +17,19 @@ //----------------------------------------------------------------------------- namespace module::stub { -class evse_securityIntfStub : public ModuleAdapterStub, public evse_securityIntf { +class evse_securityIntfStub : public evse_securityIntf { private: std::map functions; public: - evse_securityIntfStub() : evse_securityIntf(this, Requirement{"", 0}, "EvseSecurity", std::nullopt) { + evse_securityIntfStub(ModuleAdapterStub* adapter) : + evse_securityIntf(adapter, Requirement{"", 0}, "EvseSecurity", std::nullopt) { + functions["get_verify_file"] = &evse_securityIntfStub::get_verify_file; + functions["get_leaf_certificate_info"] = &evse_securityIntfStub::get_leaf_certificate_info; + } + evse_securityIntfStub(ModuleAdapterStub& adapter) : + evse_securityIntf(&adapter, Requirement{"", 0}, "EvseSecurity", std::nullopt) { functions["get_verify_file"] = &evse_securityIntfStub::get_verify_file; functions["get_leaf_certificate_info"] = &evse_securityIntfStub::get_leaf_certificate_info; } diff --git a/modules/EvseV2G/tests/log.cpp b/modules/EvseV2G/tests/log.cpp index 2b06a3519..43169c16b 100644 --- a/modules/EvseV2G/tests/log.cpp +++ b/modules/EvseV2G/tests/log.cpp @@ -1,16 +1,45 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest -#include "log.hpp" +#include "utest_log.hpp" #include #include +#include +#include +#include + +namespace { +std::map> logged_events; + +void add_log(dloglevel_t loglevel, const std::string& event) { + logged_events[loglevel].push_back(event); +} +} // namespace + +namespace module::stub { +std::vector& get_logs(dloglevel_t loglevel) { + return logged_events[loglevel]; +} + +void clear_logs() { + logged_events.clear(); +} + +} // namespace module::stub + void dlog_func(const dloglevel_t loglevel, const char* filename, const int linenumber, const char* functionname, const char* format, ...) { va_list ap; + std::array buffer; va_start(ap, format); - (void)std::vfprintf(stderr, format, ap); + std::size_t len = std::vsnprintf(buffer.data(), buffer.size(), format, ap); va_end(ap); - (void)std::fprintf(stderr, "\n"); + if (len > 0) { + auto s_len = std::min(len, buffer.size()); + std::string event{buffer.data(), s_len}; + (void)std::fprintf(stderr, "log: %s\n", event.c_str()); + add_log(loglevel, event); + } } diff --git a/modules/EvseV2G/tests/sdp_test.cpp b/modules/EvseV2G/tests/sdp_test.cpp new file mode 100644 index 000000000..4ba052fbd --- /dev/null +++ b/modules/EvseV2G/tests/sdp_test.cpp @@ -0,0 +1,31 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#include +#include + +namespace { + +class SdpTest : public testing::Test { +protected: + SdpTest() { + } +}; + +TEST_F(SdpTest, sdp_write_header) { + uint8_t buffer[20]; + uint16_t payload_type = 0x9001; + uint32_t length = 367; + + EXPECT_EQ(sdp_write_header(buffer, payload_type, length), 8); + + EXPECT_EQ(buffer[0], 0x01); + EXPECT_EQ(buffer[1], 0xFE); + EXPECT_EQ(buffer[2], 0x90); + EXPECT_EQ(buffer[3], 0x01); + EXPECT_EQ(buffer[4], 0x00); + EXPECT_EQ(buffer[5], 0x00); + EXPECT_EQ(buffer[6], 0x01); + EXPECT_EQ(buffer[7], 0x6F); +} + +} // namespace diff --git a/modules/EvseV2G/tests/utest_log.hpp b/modules/EvseV2G/tests/utest_log.hpp new file mode 100644 index 000000000..925c78140 --- /dev/null +++ b/modules/EvseV2G/tests/utest_log.hpp @@ -0,0 +1,15 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#pragma once + +#include + +#include +#include + +namespace module::stub { +std::vector& get_logs(dloglevel_t loglevel); +void clear_logs(); + +} // namespace module::stub diff --git a/modules/EvseV2G/tests/v2g_ctx_test.cpp b/modules/EvseV2G/tests/v2g_ctx_test.cpp new file mode 100644 index 000000000..3d190d85b --- /dev/null +++ b/modules/EvseV2G/tests/v2g_ctx_test.cpp @@ -0,0 +1,169 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#include +#include + +#include "ISO15118_chargerImplStub.hpp" +#include "ModuleAdapterStub.hpp" +#include "evse_securityIntfStub.hpp" +#include "utest_log.hpp" +#include "v2g.hpp" + +#include + +namespace { + +struct v2g_contextDeleter { + void operator()(v2g_context* ptr) const { + v2g_ctx_free(ptr); + }; +}; + +class V2gCtxTest : public testing::Test { +protected: + std::unique_ptr ctx; + module::stub::QuietModuleAdapterStub adapter; + module::stub::ISO15118_chargerImplStub charger; + module::stub::evse_securityIntfStub security; + + V2gCtxTest() : charger(adapter), security(adapter) { + } + + void v2g_ctx_init_charging_state_cleared() { + // checks try to match the order is v2g.hpp + + EXPECT_EQ(ctx->com_setup_timeout, nullptr); + + EXPECT_EQ(ctx->last_v2g_msg, V2G_UNKNOWN_MSG); + EXPECT_EQ(ctx->current_v2g_msg, V2G_UNKNOWN_MSG); + EXPECT_EQ(ctx->state, 0); + + // not changed + // is_dc_charger + // debugMode + // supported_protocols + + EXPECT_EQ(ctx->selected_protocol, V2G_UNKNOWN_PROTOCOL); + EXPECT_FALSE(ctx->intl_emergency_shutdown); + EXPECT_FALSE(ctx->stop_hlc); + + // ctx->is_connection_terminated is updated rather than cleared + + // not changed + // terminate_connection_on_failed_response + // contactor_is_closed + + // many items in session not reset + EXPECT_FALSE(ctx->session.renegotiation_required); + EXPECT_FALSE(ctx->session.is_charging); + } + + void SetUp() override { + auto ptr = v2g_ctx_create(&charger, &security); + ctx = std::unique_ptr(ptr, v2g_contextDeleter()); + module::stub::clear_logs(); + } + + void TearDown() override { + } +}; + +TEST(RunFirst, v2g_ctx_init_charging_values) { + // must not be part of V2gCtxTest + // V2gCtxTest::SetUp() creates the v2g_context which would be the 1st + // call to v2g_ctx_init_charging_values() + + // only called from v2g_ctx_init_charging_session() + // which is called from v2g_ctx_create() + + // note v2g_ctx_init_charging_values() has a static bool so it + // performs different tidyup after the first time it is called + + v2g_context ctx; + ctx.evse_v2g_data.charge_service.FreeService = 9; + v2g_ctx_init_charging_values(&ctx); + EXPECT_EQ(ctx.evse_v2g_data.charge_service.FreeService, 0); + ctx.evse_v2g_data.charge_service.FreeService = 10; + v2g_ctx_init_charging_values(&ctx); + EXPECT_EQ(ctx.evse_v2g_data.charge_service.FreeService, 10); + + // reset back to a valid value as it will never be reset + ctx.evse_v2g_data.charge_service.FreeService = 0; +} + +TEST_F(V2gCtxTest, v2g_ctx_init_charging_stateTrue) { + // called on session start in v2g_handle_connection() + + ctx->com_setup_timeout = nullptr; // does not appear to ever be set + ctx->last_v2g_msg = V2G_CABLE_CHECK_MSG; + ctx->current_v2g_msg = V2G_CHARGE_PARAMETER_DISCOVERY_MSG; + ctx->state = 10; + ctx->selected_protocol = V2G_PROTO_DIN70121; + ctx->intl_emergency_shutdown = true; + ctx->stop_hlc = true; + ctx->session.renegotiation_required = true; + ctx->session.is_charging = true; + + v2g_ctx_init_charging_state(ctx.get(), true); + + v2g_ctx_init_charging_state_cleared(); + EXPECT_TRUE(ctx->is_connection_terminated); +} + +TEST_F(V2gCtxTest, v2g_ctx_init_charging_stateFalse) { + // called on session end in v2g_handle_connection() + + ctx->com_setup_timeout = nullptr; // does not appear to ever be set + ctx->last_v2g_msg = V2G_CABLE_CHECK_MSG; + ctx->current_v2g_msg = V2G_CHARGE_PARAMETER_DISCOVERY_MSG; + ctx->state = 10; + ctx->selected_protocol = V2G_PROTO_DIN70121; + ctx->intl_emergency_shutdown = true; + ctx->stop_hlc = true; + ctx->session.renegotiation_required = true; + ctx->session.is_charging = true; + + v2g_ctx_init_charging_state(ctx.get(), false); + + v2g_ctx_init_charging_state_cleared(); + EXPECT_FALSE(ctx->is_connection_terminated); +} + +#if 0 +// v2g_ctx_init_charging_session() is a trivial implementation +TEST_F(V2gCtxTest, v2g_ctx_init_charging_sessionTrue) { + // called in connection_teardown() + // calls v2g_ctx_init_charging_state + // calls v2g_ctx_init_charging_values +} + +TEST_F(V2gCtxTest, v2g_ctx_init_charging_sessionFalse) { + // called in connection_teardown() + // calls v2g_ctx_init_charging_state + // calls v2g_ctx_init_charging_values +} +#endif + +TEST(valgrind, memcheck) { + GTEST_SKIP() << "pthreads result in valgrind reporting errors"; + /* + * v2g_ctx_free() doesn't stop or wait for threads to finish (no join) + * hence there is access to free'd memory reported. + * + * ==2136== LEAK SUMMARY: + * ==2136== definitely lost: 0 bytes in 0 blocks + * ==2136== indirectly lost: 0 bytes in 0 blocks + * ==2136== possibly lost: 304 bytes in 1 blocks + * ==2136== still reachable: 80 bytes in 2 blocks + * ==2136== suppressed: 0 bytes in 0 blocks + */ + + // run via valgrind to ensure that malloc/free are working + module::stub::QuietModuleAdapterStub adapter; + module::stub::ISO15118_chargerImplStub charger(adapter); + module::stub::evse_securityIntfStub security(adapter); + auto ptr = v2g_ctx_create(&charger, &security); + v2g_ctx_free(ptr); +} + +} // namespace diff --git a/modules/EvseV2G/tests/v2g_main.cpp b/modules/EvseV2G/tests/v2g_main.cpp index c1c3e97c3..b9312df5d 100644 --- a/modules/EvseV2G/tests/v2g_main.cpp +++ b/modules/EvseV2G/tests/v2g_main.cpp @@ -18,6 +18,7 @@ #include #include "ISO15118_chargerImplStub.hpp" +#include "ModuleAdapterStub.hpp" #include "evse_securityIntfStub.hpp" #include @@ -79,6 +80,9 @@ void parse_options(int argc, char** argv) { // EvseSecurity "implementation" struct EvseSecurity : public module::stub::evse_securityIntfStub { + EvseSecurity(module::stub::ModuleAdapterStub& adapter) : module::stub::evse_securityIntfStub(&adapter) { + } + Result get_verify_file(const Requirement& req, const Parameters& args) override { return "client_root_cert.pem"; } @@ -119,8 +123,9 @@ int main(int argc, char** argv) { parse_options(argc, argv); tls::Server tls_server; - module::stub::ISO15118_chargerImplStub charger; - EvseSecurity security; + module::stub::ModuleAdapterStub adapter; + module::stub::ISO15118_chargerImplStub charger(adapter); + EvseSecurity security(adapter); auto* ctx = v2g_ctx_create(&charger, &security); if (ctx == nullptr) { diff --git a/tests/include/ModuleAdapterStub.hpp b/tests/include/ModuleAdapterStub.hpp index 70d76d191..e97eb904f 100644 --- a/tests/include/ModuleAdapterStub.hpp +++ b/tests/include/ModuleAdapterStub.hpp @@ -6,6 +6,7 @@ #include +#include #include #include #include @@ -31,16 +32,22 @@ struct ModuleAdapterStub : public Everest::ModuleAdapter { return this->get_error_state_monitor_impl_fn(str); }; get_error_factory = [this](const std::string& str) { return this->get_error_factory_fn(str); }; - this->get_error_manager_req = [this](const Requirement& req) { return this->get_error_manager_req_fn(req); }; + get_error_manager_req = [this](const Requirement& req) { return this->get_error_manager_req_fn(req); }; get_error_state_monitor_req = [this](const Requirement& req) { return this->get_error_state_monitor_req_fn(req); }; + get_global_error_manager = [this]() { return this->get_global_error_manager_fn(); }; + get_global_error_state_monitor = [this]() { return this->get_global_error_state_monitor_fn(); }; ext_mqtt_publish = [this](const std::string& s1, const std::string& s2) { this->ext_mqtt_publish_fn(s1, s2); }; ext_mqtt_subscribe = [this](const std::string& str, StringHandler sh) { return this->ext_mqtt_subscribe_fn(str, sh); }; + ext_mqtt_subscribe_pair = [this](const std::string& topic, const StringPairHandler& handler) { + return this->ext_mqtt_subscribe_pair_fn(topic, handler); + }; telemetry_publish = [this](const std::string& s1, const std::string& s2, const std::string& s3, const Everest::TelemetryMap& tm) { this->telemetry_publish_fn(s1, s2, s3, tm); }; + get_mapping = [this]() { return this->get_mapping_fn(); }; } virtual Result call_fn(const Requirement&, const std::string&, Parameters) { @@ -66,6 +73,14 @@ struct ModuleAdapterStub : public Everest::ModuleAdapter { return std::make_shared( std::make_shared()); } + virtual std::shared_ptr get_global_error_manager_fn() { + std::printf("get_global_error_manager_fn\n"); + return {}; + } + virtual std::shared_ptr get_global_error_state_monitor_fn() { + std::printf("get_global_error_state_monitor_fn\n"); + return {}; + } virtual std::shared_ptr get_error_factory_fn(const std::string&) { std::printf("get_error_factory_fn\n"); return std::make_shared(std::make_shared()); @@ -90,10 +105,74 @@ struct ModuleAdapterStub : public Everest::ModuleAdapter { std::printf("ext_mqtt_subscribe_fn\n"); return nullptr; } + virtual std::function ext_mqtt_subscribe_pair_fn(const std::string& topic, + const StringPairHandler& handler) { + std::printf("ext_mqtt_subscribe_pair_fn\n"); + return {}; + } virtual void telemetry_publish_fn(const std::string&, const std::string&, const std::string&, const Everest::TelemetryMap&) { std::printf("telemetry_publish_fn\n"); } + virtual std::optional get_mapping_fn() { + std::printf("get_mapping_fn\n"); + return {}; + } +}; + +struct QuietModuleAdapterStub : public ModuleAdapterStub { + Result call_fn(const Requirement&, const std::string&, Parameters) override { + return {}; + } + void publish_fn(const std::string&, const std::string&, Value) override { + } + void subscribe_fn(const Requirement&, const std::string& fn, ValueCallback) override { + } + std::shared_ptr get_error_manager_impl_fn(const std::string&) override { + return std::make_shared( + std::make_shared(), std::make_shared(), + std::list(), [](const Everest::error::Error&) {}, + [](const Everest::error::Error&) {}); + } + std::shared_ptr get_error_state_monitor_impl_fn(const std::string&) override { + return std::make_shared( + std::make_shared()); + } + std::shared_ptr get_global_error_manager_fn() override { + return {}; + } + std::shared_ptr get_global_error_state_monitor_fn() override { + return {}; + } + std::shared_ptr get_error_factory_fn(const std::string&) override { + return std::make_shared(std::make_shared()); + } + std::shared_ptr get_error_manager_req_fn(const Requirement&) override { + return std::make_shared( + std::make_shared(), std::make_shared(), + std::list(), + [](const Everest::error::ErrorType&, const Everest::error::ErrorCallback&, + const Everest::error::ErrorCallback&) { std::printf("subscribe_error\n"); }); + } + std::shared_ptr get_error_state_monitor_req_fn(const Requirement&) override { + return std::make_shared( + Everest::error::ErrorStateMonitor(std::make_shared())); + } + void ext_mqtt_publish_fn(const std::string&, const std::string&) override { + } + std::function ext_mqtt_subscribe_fn(const std::string&, StringHandler) override { + return nullptr; + } + std::function ext_mqtt_subscribe_pair_fn(const std::string& topic, + const StringPairHandler& handler) override { + return {}; + } + void telemetry_publish_fn(const std::string&, const std::string&, const std::string&, + const Everest::TelemetryMap&) override { + } + std::optional get_mapping_fn() override { + return {}; + } }; } // namespace module::stub From fbbfd8639f9e706e6912fdc49b8a783b49538972 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piet=20G=C3=B6mpel?= <37657534+Pietfried@users.noreply.github.com> Date: Wed, 22 Jan 2025 17:02:55 +0100 Subject: [PATCH 3/4] Added documentation for EnergyManager module (#1009) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Added documentation for EnergyManager module --------- Signed-off-by: Piet Gömpel Signed-off-by: Cornelius Claussen Signed-off-by: Krealyt Co-authored-by: Cornelius Claussen Co-authored-by: Krealyt --- .../doc/img/energy_tree.drawio.svg | 122 ++++ .../doc/img/energy_tree_complex.drawio.svg | 256 +++++++ ...y_tree_request_and_distribution.drawio.svg | 161 +++++ .../doc/img/single_node.drawio.svg | 36 + ...ingle_node_with_circuit_breaker.drawio.svg | 56 ++ .../doc/img/zoom_in_energy_node.drawio.svg | 135 ++++ modules/EnergyManager/doc/index.rst | 668 ++++++++++++++++++ 7 files changed, 1434 insertions(+) create mode 100644 modules/EnergyManager/doc/img/energy_tree.drawio.svg create mode 100644 modules/EnergyManager/doc/img/energy_tree_complex.drawio.svg create mode 100644 modules/EnergyManager/doc/img/energy_tree_request_and_distribution.drawio.svg create mode 100644 modules/EnergyManager/doc/img/single_node.drawio.svg create mode 100644 modules/EnergyManager/doc/img/single_node_with_circuit_breaker.drawio.svg create mode 100644 modules/EnergyManager/doc/img/zoom_in_energy_node.drawio.svg create mode 100644 modules/EnergyManager/doc/index.rst diff --git a/modules/EnergyManager/doc/img/energy_tree.drawio.svg b/modules/EnergyManager/doc/img/energy_tree.drawio.svg new file mode 100644 index 000000000..2beb664f8 --- /dev/null +++ b/modules/EnergyManager/doc/img/energy_tree.drawio.svg @@ -0,0 +1,122 @@ + + + + + + + + + +
+
+
+ + Circuit Breaker +
+
+ 63A, 3ph +
+
+
+
+ + Circuit Breaker... + +
+
+ + + + + +
+
+
+ + Circuit Breaker +
+
+ 32A, 3ph +
+
+
+
+ + Circuit Breaker... + +
+
+ + + + + +
+
+
+ + Circuit Breaker +
+
+ 32A, 3ph +
+
+
+
+ + Circuit Breaker... + +
+
+ + + + +
+
+
+ + EVSE1 +
+
+ 16A, 3ph +
+
+
+
+ + EVSE1... + +
+
+ + + + +
+
+
+ + EVSE2 +
+
+ 32A, 3ph +
+
+
+
+ + EVSE2... + +
+
+
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/modules/EnergyManager/doc/img/energy_tree_complex.drawio.svg b/modules/EnergyManager/doc/img/energy_tree_complex.drawio.svg new file mode 100644 index 000000000..53c1a8b6c --- /dev/null +++ b/modules/EnergyManager/doc/img/energy_tree_complex.drawio.svg @@ -0,0 +1,256 @@ + + + + + + + + + +
+
+
+ + EnergyNode +
+
+ grid_connection_point +
+
+
+
+ + EnergyNode... + +
+
+ + + + + +
+
+
+ + EnergyNode +
+
+ api_sink_1 +
+
+
+
+ + EnergyNode... + +
+
+ + + + + +
+
+
+ + EnergyNode +
+
+ api_sink_2 +
+
+
+
+ + EnergyNode... + +
+
+ + + + +
+
+
+ + EvseManager +
+
+ evse_manager_1 +
+
+
+
+ + EvseManager... + +
+
+ + + + +
+
+
+ + EvseManager +
+
+ evse_manager_2 +
+
+
+
+ + EvseManager... + +
+
+ + + + + +
+
+
+ + EnergyNode +
+
+ ocpp_sink_1 +
+
+
+
+ + EnergyNode... + +
+
+ + + + + +
+
+
+ + EnergyNode +
+
+ ocpp_sink_2 +
+
+
+
+ + EnergyNode... + +
+
+ + + + + +
+
+
+ API +
+
+
+
+ + API + +
+
+ + + + + +
+
+
+ OCPP +
+
+
+
+ + OCPP + +
+
+ + + + + +
+
+
+ OCPP +
+
+
+
+ + OCPP + +
+
+ + + + + +
+
+
+ API +
+
+
+
+ + API + +
+
+ + + + + +
+
+
+ OCPP +
+
+
+
+ + OCPP + +
+
+
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/modules/EnergyManager/doc/img/energy_tree_request_and_distribution.drawio.svg b/modules/EnergyManager/doc/img/energy_tree_request_and_distribution.drawio.svg new file mode 100644 index 000000000..e6053cea9 --- /dev/null +++ b/modules/EnergyManager/doc/img/energy_tree_request_and_distribution.drawio.svg @@ -0,0 +1,161 @@ + + + + + + + + + + + +
+
+
+ + Circuit Breaker +
+
+ 63A, 3ph +
+
+
+
+ + Circuit Breaker... + +
+
+ + + + + + + + + + + +
+
+
+ + Circuit Breaker +
+
+ 32A, 3ph +
+
+
+
+ + Circuit Breaker... + +
+
+ + + + + + + + + + + +
+
+
+ + Circuit Breaker +
+
+ 32A, 3ph +
+
+
+
+ + Circuit Breaker... + +
+
+ + + + + + +
+
+
+ + EVSE1 +
+
+ 16A, 3ph +
+
+
+
+ + EVSE1... + +
+
+ + + + + + +
+
+
+ + EVSE2 +
+
+ 32A, 3ph +
+
+
+
+ + EVSE2... + +
+
+ + + + + + +
+
+
+ + EnergyManager + +
+
+
+
+ + EnergyManager + +
+
+
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/modules/EnergyManager/doc/img/single_node.drawio.svg b/modules/EnergyManager/doc/img/single_node.drawio.svg new file mode 100644 index 000000000..d63947a06 --- /dev/null +++ b/modules/EnergyManager/doc/img/single_node.drawio.svg @@ -0,0 +1,36 @@ + + + + + + + +
+
+
+ + EVSE + +
+
+ 32A, 3ph +
+
+
+
+
+ + EVSE... + +
+
+
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/modules/EnergyManager/doc/img/single_node_with_circuit_breaker.drawio.svg b/modules/EnergyManager/doc/img/single_node_with_circuit_breaker.drawio.svg new file mode 100644 index 000000000..09813493a --- /dev/null +++ b/modules/EnergyManager/doc/img/single_node_with_circuit_breaker.drawio.svg @@ -0,0 +1,56 @@ + + + + + + + + +
+
+
+ + Circuit Breaker +
+
+ 16A, 3ph +
+
+
+
+ + Circuit Breaker... + +
+
+ + + + +
+
+
+ + EVSE +
+
+ 32A, 3ph +
+
+
+
+ + EVSE... + +
+
+
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/modules/EnergyManager/doc/img/zoom_in_energy_node.drawio.svg b/modules/EnergyManager/doc/img/zoom_in_energy_node.drawio.svg new file mode 100644 index 000000000..53b30eb97 --- /dev/null +++ b/modules/EnergyManager/doc/img/zoom_in_energy_node.drawio.svg @@ -0,0 +1,135 @@ + + + + + + + + +
+
+
+ EnergyNode +
+
+
+
+ + EnergyNode + +
+
+ + + + +
+
+
+ Grid +
+
+
+
+ + Grid + +
+
+ + + + +
+
+
+ EV +
+
+
+
+ + EV + +
+
+ + + + + +
+
+
+ Export +
+
+
+
+ + Export + +
+
+ + + + + +
+
+
+ Import +
+
+
+
+ + Import + +
+
+ + + + +
+
+
+ Leaf Side +
+
+
+
+ + Leaf S... + +
+
+ + + + +
+
+
+ Root Side +
+
+
+
+ + Root S... + +
+
+
+ + + + + Text is not SVG - cannot display + + + +
\ No newline at end of file diff --git a/modules/EnergyManager/doc/index.rst b/modules/EnergyManager/doc/index.rst new file mode 100644 index 000000000..4fc26c408 --- /dev/null +++ b/modules/EnergyManager/doc/index.rst @@ -0,0 +1,668 @@ +.. _everest_modules_handwritten_EnergyManager: + +EnergyManager +============= + +This module implements logic to distribute power to energy nodes based on +energy requests. +One of its central ideas is to represent the energy system for which power is +distributed as an energy tree containing energy nodes. +This enables the representation of arbitrarily complex configurations of +physical and logical components within the targeted energy system. + +The following sections present this concept in more detail. + +Energy nodes +------------ + +An energy node can be either a logical or physical component within the energy +system. + +Energy nodes can typically be classified into the following categories: + +* **Physical Components**: Circuit breakers, electrical fuses +* **Logical Components**: Limits from OCPP, EEBus, or other external sources +* **Charging Stations**: Unidirectional or bidirectional charging stations + (or in general any sink or source of power) + +An EVerest module becomes an energy node by implementing the +`energy <../interfaces/energy.yaml>`_ interface. + +.. note:: + + At the time of writing, two EVerest modules are considered energy nodes as + per the above definition: **EnergyNode** and **EvseManager**. + More may be added in the future. + The **EnergyNode** module fulfills central aspects of the energy management + concept. + When the term **EnergyNode** is used, it refers to the actual module, + whereas **energy node** refers to the general definition above. + +The **EnergyNode** module both requires and provides the +`energy <../interfaces/energy.yaml>`_ interface. +This design enables the representation of arbitrary energy tree configurations +within the EVerest configuration file as explained in detail in a later +section. + +Energy trees +------------ + +Energy trees are used to model various energy system configurations. +Below are examples demonstrating how energy systems can be represented in +EVerest. + +The simplest energy tree consists of a single leaf node representing an EVSE +with a physical hardware capability of 32 A on 3 phases. + +.. image:: img/single_node.drawio.svg + :name: single-node-label + :align: center + +Typically, the electrical connection of charging stations is protected by a +circuit breaker. +Adding this to the representation results in: + +.. image:: img/single_node_with_circuit_breaker.drawio.svg + :name: single-node-with-circuit-breaker-label + :align: center + +In this example, the circuit breaker limits the current to 16 A, even though +the EVSE supports 32 A. +The module managing power distribution must enforce this limitation. + +For a more complex setup, consider the following example: + +.. image:: img/energy_tree.drawio.svg + :name: energy-tree-label + :align: center + +Here, a top-level circuit breaker limits the line to 63 A. +Two additional circuit breakers protect the lines to two EVSEs, each fused at +32 A. +EVSE1 can consume 16 A on three phases, while EVSE2 can consume 32 A on three +phases. +This module accounts for all existing limitations when distributing power to +energy nodes. + +All the scenarios above can be represented within EVerest. +The power distribution to the EVSEs is managed by this module, considering the +limitations of each individual node. +How these setups above can be represented in EVerest is presented in section +:ref:`configuration-of-energy-trees-in-everest`. + +Energy requests and distribution +-------------------------------- + +The EnergyManager module requires exactly one module implementing the +`energy <../interfaces/energy.yaml>`_ interface. +This interface defines: + +* A single variable **energy_flow_request** of type **EnergyFlowRequest** +* A single command **enforce_limits** + +The concept of the usage of this interface is further described in the +following sections. + +Energy flow request variable +^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The **EnergyFlowRequest** type is recursive, containing a list of child +**EnergyFlowRequest**. +It defines the power and current requested by an energy node, along with its +limitations (e.g., hardware or software constraints). +In essence, a module specifies its requirements and limitations through this +type, which are then communicated to its parent node. +The parent node creates an aggregated **EnergyFlowRequest**, incorporating its +own limitations and the requests from its children. + +Energy flow requests are constructed from the leaves to the root of the energy +tree, resulting in a single **EnergyFlowRequest** that contains all child +requests. +This final request serves as input for this module, which calculates the limits +to enforce down the tree. + +The following diagram illustrates how energy nodes communicate requests, with +green arrows representing energy flow requests: + +.. image:: img/energy_tree_request_and_distribution.drawio.svg + :name: energy-tree-request-and-distribution-label + :align: center + +Enforcing limits +^^^^^^^^^^^^^^^^ + +The **enforce_limits** command propagates limits down the tree. +Each energy node calls this function on its child nodes to enforce calculated +limits. + +Note that the EnergyManager itself does not represent an energy node. +It communicates the resulting **EnergyFlowRequest** to a single connected +energy node, which then propagates the limits further down the tree. + +Details of the EnergyFlowRequest type +------------------------------------- + +Energy nodes may have varying types of limits. +To understand this better, consider a zoomed-in view of an energy node: + +.. image:: img/zoom_in_energy_node.drawio.svg + :name: zoom-in-energy-node-label + :align: center + +In reality, an energy node may have different limits for charging (import) and +discharging (export). +The **EnergyFlowRequest** type accounts for this distinction: + +* **Import**: Energy flow direction from the grid to the consumer/EV (charging) +* **Export**: Energy flow direction from the EV to the grid (discharging) + +Additionally, each direction may have separate limits for the root and leaf +sides of the energy node. +For example, a DC power supply may have AC limits on the root side (facing the +grid) and DC limits on the leaf side (facing the EV). + +Limits may also change over time, which is why the *schedule_import* and +*schedule_export* properties are lists containing multiple limit +specifications. + +Below is an example JSON representation of an **EnergyFlowRequest** for a leaf node: + +.. code-block:: json + + { + "children": [], + "evse_state": "Charging", + "node_type": "Evse", + "priority_request": false, + "schedule_export": [ + { + "limits_to_leaves": { + "ac_max_current_A": 0.0 + }, + "limits_to_root": { + "ac_max_current_A": 16.0, + "ac_max_phase_count": 3, + "ac_min_current_A": 0.0, + "ac_min_phase_count": 1, + "ac_number_of_active_phases": 3, + "ac_supports_changing_phases_during_charging": true + }, + "timestamp": "2024-12-17T13:08:36.479Z" + } + ], + "schedule_import": [ + { + "limits_to_leaves": { + "ac_max_current_A": 32.0 + }, + "limits_to_root": { + "ac_max_current_A": 32.0, + "ac_max_phase_count": 3, + "ac_min_current_A": 6.0, + "ac_min_phase_count": 1, + "ac_number_of_active_phases": 3, + "ac_supports_changing_phases_during_charging": true + }, + "timestamp": "2024-12-17T13:08:36.479Z" + } + ], + "uuid": "evse1" + } + +External limits +--------------- + +External limits can be added to the energy system using EVerest modules +implementing the +`external_energy_limits <../interfaces/external_energy_limits.yaml>`_ +interface. +At the time of writing, the **EnergyNode** module is the sole module that +provides this functionality. + +The `external_energy_limits` interface defines the **set_external_limits** +command, which modules like OCPP or API can use to specify external energy +limits. +These limits are then considered by the **EnergyNode** module when creating +its energy flow request. + +To apply external limits, a module must require the `external_energy_limits` +interface and invoke the **set_external_limits** command. +The next section details how to configure these limits in EVerest. + +Configuration of energy trees in EVerest +---------------------------------------- + +The following section describes how to configure the EVerest configuration file +in order to represent the targeted energy tree. +In order to do that we are using a complex energy tree example and implement +this in the configuration step by step. + +This is the energy tree that we are going to represent in the EVerest +configuration: + +.. image:: img/energy_tree_complex.drawio.svg + :name: energy-tree-complex-label + :align: center + +This energy tree represents a setup with two EVSEs. +There are two external sources that are able to provide external energy limits: +OCPP and the API module. + +OCPP is able to set external limits for each EVSE as well as for the whole +charging station. +This is indicated by the three arrows labeled with OCPP. +The API module is only able to set the limits for the two EVSEs, but not for +the whole charging station. + +.. note:: + + To improve readability, unrelated module configurations and connections are + omitted in the examples below. + +First, we add two EvseManager modules to the config file representing our +energy leaf nodes. + +.. code-block:: yaml + + active_modules: + evse_manager_1: + module: EvseManager + evse_manager_2: + module: EvseManager + +The two EVSEs can receive limits from OCPP. +Therefore, we add two **EnergyNode** modules that represent the sinks for the +external limits. +The **EnergyNode** module requires a connection to a module implementing the +`energy <../interfaces/energy.yaml>`_ interface. +This is implemented by connecting the previously added EvseManager modules to it. + +Any external limit applied to the added EnergyNode modules will be applied to +its energy child nodes (the EvseManager modules) now. + +.. code-block:: yaml + + active_modules: + evse_manager_1: + module: EvseManager + evse_manager_2: + module: EvseManager + ocpp_sink_1: + module: EnergyNode + connections: + energy_consumer: + - module_id: evse_manager_1 + implementation_id: energy_grid + ocpp_sink_2: + module: EnergyNode + connections: + energy_consumer: + - module_id: evse_manager_2 + implementation_id: energy_grid + +We continue with adding **EnergyNode** modules that represent the sinks for the +limits received by the API module. +Note that the **EnergyNode** module provides and requires the +`energy <../interfaces/energy.yaml>`_ interface at the same time. +This allows us to connect **EnergyNode** modules and therefore fullfill the +requirement of others. + +Note that the modules **ocpp_sink_1** and **ocpp_sink_2** are connected to the +**api_sink_1** and **api_sink_2**. +This means that both limits can be considered by this module without +overriding each other. + +.. code-block:: yaml + + active_modules: + evse_manager_1: + module: EvseManager + evse_manager_2: + module: EvseManager + ocpp_sink_1: + module: EnergyNode + connections: + energy_consumer: + - module_id: evse_manager_1 + implementation_id: energy_grid + ocpp_sink_2: + module: EnergyNode + connections: + energy_consumer: + - module_id: evse_manager_2 + implementation_id: energy_grid + api_sink_1: + module: EnergyNode + connections: + energy_consumer: + - module_id: ocpp_sink_1 + implementation_id: energy_grid + api_sink_2: + module: EnergyNode + connections: + energy_consumer: + - module_id: ocpp_sink_2 + implementation_id: energy_grid + +We are now only missing a represention for the complete charging station. +Therefore, we add another **EnergyNode** module with a fuse limit of 63 A and +we name it **grid_connection_point**. +We connect **api_sink_1** and **api_sink_2** to it. + +.. code-block:: yaml + + active_modules: + evse_manager_1: + module: EvseManager + evse_manager_2: + module: EvseManager + ocpp_sink_1: + module: EnergyNode + connections: + energy_consumer: + - module_id: evse_manager_1 + implementation_id: energy_grid + ocpp_sink_2: + module: EnergyNode + connections: + energy_consumer: + - module_id: evse_manager_2 + implementation_id: energy_grid + api_sink_1: + module: EnergyNode + connections: + energy_consumer: + - module_id: ocpp_sink_1 + implementation_id: energy_grid + api_sink_2: + module: EnergyNode + connections: + energy_consumer: + - module_id: ocpp_sink_2 + implementation_id: energy_grid + grid_connection_point: + module: EnergyNode + config_module: + fuse_limit_A: 63 + phase_count: 3 + connections: + energy_consumer: + - module_id: api_sink_1 + implementation_id: energy_grid + - module_id: api_sink_2 + implementation_id: energy_grid + + +Now we have the complete energy tree represented, but we're still missing to +include the modules that set the external energy limits, so the OCPP and API +module. +Since these modules require (optionally multiple) connections to modules +implementing the +`external_energy_limits <../interfaces/external_energy_limits.yaml>`_ +interface, we need to also add the connections to the **EnergyNode** modules we +have added previously. +Finally, we also add the **EnergyManager** module and connect the +**grid_connection_point** to it. + +.. code-block:: yaml + + active_modules: + evse_manager_1: + module: EvseManager + evse_manager_2: + module: EvseManager + ocpp_sink_1: + module: EnergyNode + connections: + energy_consumer: + - module_id: evse_manager_1 + implementation_id: energy_grid + ocpp_sink_2: + module: EnergyNode + connections: + energy_consumer: + - module_id: evse_manager_2 + implementation_id: energy_grid + api_sink_1: + module: EnergyNode + connections: + energy_consumer: + - module_id: ocpp_sink_1 + implementation_id: energy_grid + api_sink_2: + module: EnergyNode + connections: + energy_consumer: + - module_id: ocpp_sink_2 + implementation_id: energy_grid + grid_connection_point: + module: EnergyNode + config_module: + fuse_limit_A: 63 + phase_count: 3 + connections: + energy_consumer: + - module_id: api_sink_1 + implementation_id: energy_grid + - module_id: api_sink_2 + implementation_id: energy_grid + ocpp: + module: OCPP + connections: + evse_energy_sink: + - module_id: grid_connection_point + implementation_id: external_limits + - module_id: ocpp_sink_1 + implementation_id: external_limits + - module_id: ocpp_sink_2 + implementation_id: external_limits + api: + module: API + connections: + evse_energy_sink: + - module_id: api_sink_1 + implementation_id: external_limits + - module_id: api_sink_2 + implementation_id: external_limits + energy_manager: + module: EnergyManager + connections: + energy_trunk: + - module_id: grid_connection_point + implementation_id: energy_grid + + +We have now added all the required modules and connections to represent the +energy tree example of :ref:`energy-tree-complex-label`. +One important detail is still missing, which is the module mapping. +For detailed information about the module mapping please see +`3-tier module mappings `_. + +Since the connections of a module in the EVerest config does not automatically +map to a specific EVSE (or the whole charging station, represented by EVSE#0), +the **EnergyNode** modules must have a module mapping. +This allows the modules that make use of the **set_external_limits** command to +call it for the correct node. + +Modules like OCPP and API can only know at which requirement the command +**set_external_limit** shall be called in case the energy node that is +connected to it has a specified module mapping in the EVerest config. + +This is a full example including the module mappings: + +.. code-block:: yaml + + active_modules: + evse_manager_1: + module: EvseManager + mapping: + module: + evse: 1 + evse_manager_2: + module: EvseManager + mapping: + module: + evse: 2 + ocpp_sink_1: + module: EnergyNode + mapping: + module: + evse: 1 + connections: + energy_consumer: + - module_id: evse_manager_1 + implementation_id: energy_grid + ocpp_sink_2: + module: EnergyNode + mapping: + module: + evse: 2 + connections: + energy_consumer: + - module_id: evse_manager_2 + implementation_id: energy_grid + api_sink_1: + module: EnergyNode + mapping: + module: + evse: 1 + connections: + energy_consumer: + - module_id: ocpp_sink_1 + implementation_id: energy_grid + api_sink_2: + module: EnergyNode + mapping: + module: + evse: 2 + connections: + energy_consumer: + - module_id: ocpp_sink_2 + implementation_id: energy_grid + grid_connection_point: + module: EnergyNode + mapping: + module: + evse: 0 + config_module: + fuse_limit_A: 63 + phase_count: 3 + connections: + energy_consumer: + - module_id: api_sink_1 + implementation_id: energy_grid + - module_id: api_sink_2 + implementation_id: energy_grid + ocpp: + module: OCPP + connections: + evse_energy_sink: + - module_id: grid_connection_point + implementation_id: external_limits + - module_id: ocpp_sink_1 + implementation_id: external_limits + - module_id: ocpp_sink_2 + implementation_id: external_limits + api: + module: API + connections: + evse_energy_sink: + - module_id: api_sink_1 + implementation_id: external_limits + - module_id: api_sink_2 + implementation_id: external_limits + energy_manager: + module: EnergyManager + connections: + energy_trunk: + - module_id: grid_connection_point + implementation_id: energy_grid + +Energy distribution +------------------- + +The EnergyManager module implements an algorithm to distribute available power +to energy leaf nodes: + +* It calculates and enforces limits for each energy leaf node in the tree. +* It ensures that no node exceeds its specified limits for current, power, or + phase count. +* It distributes power equally among child nodes, if their collective request + exceeds the parent node's limits. +* The algorithm prefers charging over discharging if the specified limits allow + for both. +* It supports phase switching between single-phase and three-phase modes, + optimizing power usage for low-demand scenarios if + **switch_3ph1ph_while_charging_mode** is enabled. + +Phase switching +=============== + +This module supports switching between single-phase (1ph) and three-phase (3ph) +configurations during AC charging. + +.. warning:: + + Some vehicles (such as the first generation of Renault Zoe) may be + permanently damaged when switching from 1ph to 3ph during charging. + Use at your own risk! + +To use this feature, several configurations must be enabled across different +EVerest modules: + +- **EvseManager**: Adjust the following configuration options to your needs: + - ``switch_3ph1ph_delay_s`` + - ``switch_3ph1ph_cp_state`` +- **Module implementing the `evse_board_support <../interfaces/evse_board_support.yaml>`_ interface:** + - Set ``supports_changing_phases_during_charging`` to ``true`` in the reported capabilities. + - Define the minimum number of phases as 1 and the maximum as 3. + - Ensure the ``ac_switch_three_phases_while_charging`` command is implemented. +- **EnergyManager**: Adjust the following config options to your needs: + - switch_3ph1ph_while_charging_mode + - switch_3ph1ph_max_nr_of_switches_per_session + - switch_3ph1ph_switch_limit_stickyness + - switch_3ph1ph_power_hysteresis_W + - switch_3ph1ph_time_hysteresis_s + +Refer to the manifest.yaml for documentation of these configuration options. + +If all of these are properly configured, the EnergyManager will handle the +1ph/3ph switching. +To enable this, an external limit must be set. +There are two ways to configure the limit: + +1. **Watt-based limit (preferred option):** The limit is set in Watts (not + Amperes), even though this involves AC charging. + This provides the EnergyManager with the flexibility to decide when to + switch. + The limit can be defined by an OCPP schedule or through an additional + EnergyNode. +2. **Ampere-based limit:** The limit is defined in Amperes, along with a + restriction on the number of phases (e.g., ``min_phase=1`` and + ``max_phase=1``). + This enforces switching and allows external control over the switching time, + but the EnergyManager loses its ability to choose when to switch. + +Best practices +^^^^^^^^^^^^^^ + +In general, this feature works best in a configuration with 32 A per phase and +a Watt-based limit. +In this setup, there is an overlap between the single phase and three phase +domain: + +- Single-phase charging: 1.3 kW to 7.4 kW +- Three-phase charging: 4.2 kW to 22 kW (or 11 kW) + +This avoids switching too often in the most elegant way. +Other methods to reduce the number of switch cycles can be configured in the +EnergyManager, see config options above. + +Current limitations +------------------- + +* The algorithm does not account for real-time power meter readings from + individual nodes. +* It does not redistribute unused power when the actual consumption is below + the assigned target value. + From 170e06375eab241e5fe744d6305428cf90b4a5e8 Mon Sep 17 00:00:00 2001 From: Manuel Ziegler <113091917+krealyt@users.noreply.github.com> Date: Thu, 23 Jan 2025 16:10:06 +0100 Subject: [PATCH 4/4] Renamed docs directory for CI (#1023) Signed-off-by: Krealyt --- modules/EnergyManager/{doc => docs}/img/energy_tree.drawio.svg | 0 .../{doc => docs}/img/energy_tree_complex.drawio.svg | 0 .../img/energy_tree_request_and_distribution.drawio.svg | 0 modules/EnergyManager/{doc => docs}/img/single_node.drawio.svg | 0 .../{doc => docs}/img/single_node_with_circuit_breaker.drawio.svg | 0 .../{doc => docs}/img/zoom_in_energy_node.drawio.svg | 0 modules/EnergyManager/{doc => docs}/index.rst | 0 7 files changed, 0 insertions(+), 0 deletions(-) rename modules/EnergyManager/{doc => docs}/img/energy_tree.drawio.svg (100%) rename modules/EnergyManager/{doc => docs}/img/energy_tree_complex.drawio.svg (100%) rename modules/EnergyManager/{doc => docs}/img/energy_tree_request_and_distribution.drawio.svg (100%) rename modules/EnergyManager/{doc => docs}/img/single_node.drawio.svg (100%) rename modules/EnergyManager/{doc => docs}/img/single_node_with_circuit_breaker.drawio.svg (100%) rename modules/EnergyManager/{doc => docs}/img/zoom_in_energy_node.drawio.svg (100%) rename modules/EnergyManager/{doc => docs}/index.rst (100%) diff --git a/modules/EnergyManager/doc/img/energy_tree.drawio.svg b/modules/EnergyManager/docs/img/energy_tree.drawio.svg similarity index 100% rename from modules/EnergyManager/doc/img/energy_tree.drawio.svg rename to modules/EnergyManager/docs/img/energy_tree.drawio.svg diff --git a/modules/EnergyManager/doc/img/energy_tree_complex.drawio.svg b/modules/EnergyManager/docs/img/energy_tree_complex.drawio.svg similarity index 100% rename from modules/EnergyManager/doc/img/energy_tree_complex.drawio.svg rename to modules/EnergyManager/docs/img/energy_tree_complex.drawio.svg diff --git a/modules/EnergyManager/doc/img/energy_tree_request_and_distribution.drawio.svg b/modules/EnergyManager/docs/img/energy_tree_request_and_distribution.drawio.svg similarity index 100% rename from modules/EnergyManager/doc/img/energy_tree_request_and_distribution.drawio.svg rename to modules/EnergyManager/docs/img/energy_tree_request_and_distribution.drawio.svg diff --git a/modules/EnergyManager/doc/img/single_node.drawio.svg b/modules/EnergyManager/docs/img/single_node.drawio.svg similarity index 100% rename from modules/EnergyManager/doc/img/single_node.drawio.svg rename to modules/EnergyManager/docs/img/single_node.drawio.svg diff --git a/modules/EnergyManager/doc/img/single_node_with_circuit_breaker.drawio.svg b/modules/EnergyManager/docs/img/single_node_with_circuit_breaker.drawio.svg similarity index 100% rename from modules/EnergyManager/doc/img/single_node_with_circuit_breaker.drawio.svg rename to modules/EnergyManager/docs/img/single_node_with_circuit_breaker.drawio.svg diff --git a/modules/EnergyManager/doc/img/zoom_in_energy_node.drawio.svg b/modules/EnergyManager/docs/img/zoom_in_energy_node.drawio.svg similarity index 100% rename from modules/EnergyManager/doc/img/zoom_in_energy_node.drawio.svg rename to modules/EnergyManager/docs/img/zoom_in_energy_node.drawio.svg diff --git a/modules/EnergyManager/doc/index.rst b/modules/EnergyManager/docs/index.rst similarity index 100% rename from modules/EnergyManager/doc/index.rst rename to modules/EnergyManager/docs/index.rst