From 12f58e83769b9e04ad1f99a74e4e97fbb27da7b3 Mon Sep 17 00:00:00 2001 From: Cornelius Claussen Date: Tue, 23 Jan 2024 09:29:34 +0100 Subject: [PATCH 1/5] Add new enable_disable commands to EvseManager It replaces the old enable/disable commands and adds a source and priority. See README of the API module for more details. Compile time breaking change on the evse_manager interface. All enable/disable calls need to be switched over to the new enable_disable call, for everest-core this is done in this PR. In some uses cases it is useful if e.g. a LocalAPI source disables a connector that only the same local source can enable again. Think of a service technician who has an app to disable the connector locally. Then it should be prevented that it is re-enabled by the CSMS until the service technician enables it again via the same app. Using priorities this is quite flexible and a lot of use-cases can be implemented without a change in EVerest code. The API module also has a new enable_disable command. It also has compatibility implementations for the old enable/disable commands. Signed-off-by: Cornelius Claussen fix: merge issues fix: updated README Signed-off-by: James Chapman --- interfaces/evse_manager.yaml | 26 ++- modules/API/API.cpp | 101 +++++++++--- modules/API/API.hpp | 6 + modules/API/README.md | 152 +++++++++++++++--- modules/EvseManager/Charger.cpp | 138 ++++++++++++++-- modules/EvseManager/Charger.hpp | 10 +- modules/EvseManager/EvseManager.cpp | 6 +- modules/EvseManager/evse/evse_managerImpl.cpp | 22 +-- modules/EvseManager/evse/evse_managerImpl.hpp | 4 +- modules/EvseManager/tests/EvseManagerStub.hpp | 3 + modules/OCPP/OCPP.cpp | 9 +- modules/OCPP/README.md | 7 + modules/OCPP201/OCPP201.cpp | 8 +- types/evse_manager.yaml | 40 ++++- 14 files changed, 434 insertions(+), 98 deletions(-) create mode 100644 modules/OCPP/README.md diff --git a/interfaces/evse_manager.yaml b/interfaces/evse_manager.yaml index 82225b19f..262d10d1d 100644 --- a/interfaces/evse_manager.yaml +++ b/interfaces/evse_manager.yaml @@ -8,29 +8,21 @@ cmds: description: Object that contains information of the EVSE including its connectors type: object $ref: /evse_manager#/Evse - enable: - description: Enables the evse. EVSE is available for charging after this operation + enable_disable: + description: Enables or disables the evse arguments: connector_id: description: Specifies the ID of the connector to enable. If 0, the whole EVSE should be enabled type: integer + cmd_source: + description: Source of the enable command + type: object + $ref: /evse_manager#/EnableDisableSource result: description: >- - Returns true if evse was enabled (or was enabled before), returns - false if enable failed e.g. due to permanent fault. - type: boolean - disable: - description: >- - Disables the evse. EVSE is not available for charging after this - operation - arguments: - connector_id: - description: Specifies the ID of the connector. If 0, the whole EVSE should be disabled - type: integer - result: - description: >- - Returns true if evse was disabled (or was disabled before), returns - false if it could not be disabled (i.e. due to communication error with hardware) + Returns true if evse is enabled after the command, false if it is disabled. + This may not be the same value as the request, since there may be a higher priority request + from another source that is actually deciding whether it is enabled or disabled. type: boolean authorize_response: description: >- diff --git a/modules/API/API.cpp b/modules/API/API.cpp index 2c1a7fea9..61e914203 100644 --- a/modules/API/API.cpp +++ b/modules/API/API.cpp @@ -223,6 +223,14 @@ void SessionInfo::set_uk_random_delay_remaining(const types::uk_random_delay::Co this->uk_random_delay_remaining = cd; } +void SessionInfo::set_enable_disable_source(const std::string& active_source, const std::string& active_state, + const int active_priority) { + std::lock_guard lock(this->session_info_mutex); + this->active_enable_disable_source = active_source; + this->active_enable_disable_state = active_state; + this->active_enable_disable_priority = active_priority; +} + static void to_json(json& j, const SessionInfo::Error& e) { j = json{{"type", e.type}, {"description", e.description}, {"severity", e.severity}}; } @@ -240,14 +248,23 @@ SessionInfo::operator std::string() { auto charging_duration_s = std::chrono::duration_cast(this->end_time_point - this->start_time_point); - json session_info = json::object({{"state", state_to_string(this->state)}, - {"active_permanent_faults", this->active_permanent_faults}, - {"active_errors", this->active_errors}, - {"charged_energy_wh", charged_energy_wh}, - {"discharged_energy_wh", discharged_energy_wh}, - {"latest_total_w", this->latest_total_w}, - {"charging_duration_s", charging_duration_s.count()}, - {"datetime", Everest::Date::to_rfc3339(now)}}); + json session_info = json::object({ + {"state", state_to_string(this->state)}, + {"active_permanent_faults", this->active_permanent_faults}, + {"active_errors", this->active_errors}, + {"charged_energy_wh", charged_energy_wh}, + {"discharged_energy_wh", discharged_energy_wh}, + {"latest_total_w", this->latest_total_w}, + {"charging_duration_s", charging_duration_s.count()}, + {"datetime", Everest::Date::to_rfc3339(now)}, + + }); + + json active_disable_enable = json::object({{"source", this->active_enable_disable_source}, + {"state", this->active_enable_disable_state}, + {"priority", this->active_enable_disable_priority}}); + session_info["active_enable_disable_source"] = active_disable_enable; + if (uk_random_delay_remaining.countdown_s > 0) { json random_delay = json::object({{"remaining_s", uk_random_delay_remaining.countdown_s}, @@ -346,6 +363,13 @@ void API::init() { session_info->update_state(session_event.event, error); + if (session_event.source.has_value()) { + session_info->set_enable_disable_source( + types::evse_manager::enable_source_to_string(session_event.source.value().enable_source), + types::evse_manager::enable_state_to_string(session_event.source.value().enable_state), + session_event.source.value().enable_priority); + } + if (session_event.event == types::evse_manager::SessionEventEnum::SessionStarted) { if (session_event.session_started.has_value()) { auto session_started = session_event.session_started.value(); @@ -388,34 +412,73 @@ void API::init() { // API commands std::string cmd_base = evse_base + "/cmd/"; - std::string cmd_enable = cmd_base + "enable"; - this->mqtt.subscribe(cmd_enable, [&evse](const std::string& data) { + std::string cmd_enable_disable = cmd_base + "enable_disable"; + this->mqtt.subscribe(cmd_enable_disable, [&evse](const std::string& data) { auto connector_id = 0; + types::evse_manager::EnableDisableSource enable_source{types::evse_manager::Enable_source::LocalAPI, + types::evse_manager::Enable_state::Enable, 100}; + if (!data.empty()) { try { - connector_id = std::stoi(data); + auto arg = json::parse(data); + if (arg.contains("connector_id")) { + connector_id = arg.at("connector_id"); + } + if (arg.contains("source")) { + enable_source.enable_source = types::evse_manager::string_to_enable_source(arg.at("source")); + } + if (arg.contains("state")) { + enable_source.enable_state = types::evse_manager::string_to_enable_state(arg.at("state")); + } + if (arg.contains("priority")) { + enable_source.enable_priority = arg.at("priority"); + } + } catch (const std::exception& e) { - EVLOG_error << "Could not parse connector id for enable connector, ignoring command, error: " - << e.what(); - return; + EVLOG_error << "enable: Cannot parse argument, command ignored: " << e.what(); } + } else { + EVLOG_error << "enable: No argument specified, ignoring command"; } - evse->call_enable(connector_id); + evse->call_enable_disable(connector_id, enable_source); }); std::string cmd_disable = cmd_base + "disable"; this->mqtt.subscribe(cmd_disable, [&evse](const std::string& data) { auto connector_id = 0; + types::evse_manager::EnableDisableSource enable_source{types::evse_manager::Enable_source::LocalAPI, + types::evse_manager::Enable_state::Disable, 100}; + + if (!data.empty()) { + try { + connector_id = std::stoi(data); + EVLOG_warning << "disable: Argument is an integer, using deprecated compatibility mode"; + } catch (const std::exception& e) { + EVLOG_error << "disable: Cannot parse argument, ignoring command"; + } + } else { + EVLOG_error << "disable: No argument specified, ignoring command"; + } + evse->call_enable_disable(connector_id, enable_source); + }); + + std::string cmd_enable = cmd_base + "enable"; + this->mqtt.subscribe(cmd_enable, [&evse](const std::string& data) { + auto connector_id = 0; + types::evse_manager::EnableDisableSource enable_source{types::evse_manager::Enable_source::LocalAPI, + types::evse_manager::Enable_state::Enable, 100}; + if (!data.empty()) { try { connector_id = std::stoi(data); + EVLOG_warning << "disable: Argument is an integer, using deprecated compatibility mode"; } catch (const std::exception& e) { - EVLOG_error << "Could not parse connector id for disable connector, ignoring command, error: " - << e.what(); - return; + EVLOG_error << "disable: Cannot parse argument, ignoring command"; } + } else { + EVLOG_error << "disable: No argument specified, ignoring command"; } - evse->call_disable(connector_id); + evse->call_enable_disable(connector_id, enable_source); }); std::string cmd_pause_charging = cmd_base + "pause_charging"; diff --git a/modules/API/API.hpp b/modules/API/API.hpp index 1e8854660..b861013cf 100644 --- a/modules/API/API.hpp +++ b/modules/API/API.hpp @@ -59,6 +59,8 @@ class SessionInfo { void set_latest_energy_export_wh(int32_t latest_export_energy_wh); void set_latest_total_w(double latest_total_w); void set_uk_random_delay_remaining(const types::uk_random_delay::CountDown& c); + void set_enable_disable_source(const std::string& active_source, const std::string& active_state, + const int active_priority); /// \brief Converts this struct into a serialized json object operator std::string(); @@ -95,6 +97,10 @@ class SessionInfo { bool is_state_charging(const SessionInfo::State current_state); std::string state_to_string(State s); + + std::string active_enable_disable_source{"Unspecified"}; + std::string active_enable_disable_state{"Enabled"}; + int active_enable_disable_priority{0}; }; } // namespace module // ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 diff --git a/modules/API/README.md b/modules/API/README.md index 1c90e82b9..4a44ccb01 100644 --- a/modules/API/README.md +++ b/modules/API/README.md @@ -37,22 +37,28 @@ This variable is published every second and contains the hardware capabilities i ### everest_api/evse_manager/var/session_info This variable is published every second and contains a json object with information relating to the current charging session in the following format: ```json - { - "charged_energy_wh": 0, - "charging_duration_s": 84, - "datetime": "2022-10-11T16:48:35.747Z", - "discharged_energy_wh": 0, - "latest_total_w": 0.0, - "state": "Unplugged", - "active_permanent_faults": [], - "active_errors": [], - "uk_random_delay": { - "remaining_s": 34, - "current_limit_after_delay_A": 16.0, - "current_limit_during_delay_A": 0.0, - "start_time": "2024-02-28T14:11:11.129Z" - } - } +{ + "charged_energy_wh": 0, + "charging_duration_s": 84, + "datetime": "2022-10-11T16:48:35.747Z", + "discharged_energy_wh": 0, + "latest_total_w": 0.0, + "state": "Unplugged", + "active_permanent_faults": [], + "active_errors": [], + "active_enable_disable_source": { + "source": "Unspecified", + "state": "Enable", + "priority": 5000 + }, + "uk_random_delay": { + "remaining_s": 34, + "current_limit_after_delay_A": 16.0, + "current_limit_during_delay_A": 0.0, + "start_time": "2024-02-28T14:11:11.129Z" + }, + "last_enable_disable_source": "Unspecified" +} ``` Example with permanent faults being active: @@ -77,7 +83,19 @@ Example with permanent faults being active: "datetime": "2024-01-15T14:58:15.172Z", "discharged_energy_wh": 0, "latest_total_w": 0, - "state": "Preparing" + "state": "Preparing", + "active_enable_disable_source": { + "source": "Unspecified", + "state": "Enable", + "priority": 5000 + }, + "uk_random_delay": { + "remaining_s": 34, + "current_limit_after_delay_A": 16.0, + "current_limit_during_delay_A": 0.0, + "start_time": "2024-02-28T14:11:11.129Z" + }, + "last_enable_disable_source": "Unspecified" } ``` @@ -135,10 +153,13 @@ Example with permanent faults being active: - PermanentFault - PowermeterTransactionStartFailed -- **active_errors** array of all active errors that do not block charging. This could be shown to the user but the current state should still be shown as it does not interfere with charging. The enum is the same as for active_permanent_faults. +- **active_errors** array of all active errors that do not block charging. +This could be shown to the user but the current state should still be shown +as it does not interfere with charging. The enum is the same as for active_permanent_faults. ### everest_api/evse_manager/var/limits -This variable is published every second and contains a json object with information relating to the current limits of this EVSE. +This variable is published every second and contains a json object with information +relating to the current limits of this EVSE. ```json { "max_current": 16.0, @@ -161,7 +182,8 @@ This variable is published every second and contains telemetry of the EVSE. ``` ### everest_api/evse_manager/var/powermeter -This variable is published every second and contains powermeter information of the EVSE. +This variable is published every second and contains powermeter information +of the EVSE. ```json { "current_A": { @@ -201,16 +223,96 @@ This variable is published every second and contains powermeter information of t ## Periodically published variables for OCPP ### everest_api/ocpp/var/connection_status -This variable is published every second and contains the connection status of the OCPP module. -If the OCPP module has not yet published its "is_connected" status or no OCPP module is configured "unknown" is published. Otherwise "connected" or "disconnected" are published. +This variable is published every second and contains the connection +status of the OCPP module. +If the OCPP module has not yet published its "is_connected" status or +no OCPP module is configured "unknown" is published. Otherwise "connected" +or "disconnected" are published. ## Commands and variables published in response +### everest_api/evse_manager/cmd/enable_disable +Command to enable or disable a connector on the EVSE. The payload should be +the following json: + +```json + { + "connector_id": 0, + "source": "LocalAPI", + "state": "Enable", + "priority": 42 + } +``` +connector_id is a positive integer identifying the connector that should be +enabled. If the connector_id is 0 the whole EVSE is enabled. + +The source is an enum of the following source types : + + - Unspecified + - LocalAPI + - LocalKeyLock + - ServiceTechnician + - RemoteKeyLock + - MobileApp + - FirmwareUpdate + - CSMS + +The state can be either "enable", "disable", or "unassigned". + +"enable" and "disable" enforce the state to be enable/disable, while unassigned means +that the source does not care about the state and other sources may decide. + +Each call to this command will update an internal table that looks like this: + +| Source | State | Priority | +| ------------ | ---------- | -------- | +| Unspecified | unassigned | 10000 | +| LocalAPI | disable | 42 | +| LocalKeyLock | enable | 0 | + +Evaluation will be done based on priorities. 0 is the highest priority, +10000 the lowest, so in this example the connector will be enabled regardless +of what other sources say. +Imagine LocalKeyLock sends a "unassigned, prio 0", the table will then look like this: + +| Source | State | Priority | +| ------------ | ---------- | -------- | +| Unspecified | unassigned | 10000 | +| LocalAPI | disable | 42 | +| LocalKeyLock | unassigned | 0 | + +So now the connector will be disabled, because the second highest priority (42) sets it to disabled. + +If all sources are unassigned, the connector is enabled. +If two sources have the same priority, "disabled" has priority over "enabled". + ### everest_api/evse_manager/cmd/enable -Command to enable a connector on the EVSE. They payload should be a positive integer identifying the connector that should be enabled. If the payload is 0 the whole EVSE is enabled. +Legacy command to enable a connector on the EVSE kept for compatibility reasons. +They payload should be a positive integer identifying the connector that should be enabled. +If the payload is 0 the whole EVSE is enabled. +It will actually call the following command on everest_api/evse_manager/cmd/enable_enable: +```json + { + "connector_id": 1, + "source": "LocalAPI", + "state": "enable", + "priority": 0 + } +``` ### everest_api/evse_manager/cmd/disable -Command to disable a connector on the EVSE. They payload should be a positive integer identifying the connector that should be disabled. If the payload is 0 the whole EVSE is disabled. +Legacy command to enable a connector on the EVSE kept for compatibility reasons. +Command to disable a connector on the EVSE. They payload should be a positive integer +identifying the connector that should be disabled. If the payload is 0 the whole EVSE is disabled. +It will actually call the following command on everest_api/evse_manager/cmd/enable_disable: +```json + { + "connector_id": 1, + "source": "LocalAPI", + "state": "disable", + "priority": 0 + } +``` ### everest_api/evse_manager/cmd/pause_charging If any arbitrary payload is published to this topic charging will be paused by the EVSE. @@ -219,7 +321,7 @@ If any arbitrary payload is published to this topic charging will be paused by t If any arbitrary payload is published to this topic charging will be paused by the EVSE. ### everest_api/evse_manager/cmd/set_limit_amps -Command to set an amps limit for this EVSE that will be considered within the EnergyManager. This does not automatically imply that this limit will be set by the EVSE because the energymanagement might consider limitations from other sources, too. The payload can be a positive or negative number. +Command to set an amps limit for this EVSE that will be considered within the EnergyManager. This does not automatically imply that this limit will be set by the EVSE because the energymanagement might consider limitations from other sources, too. The payload can be a positive or negative number. ### everest_api/evse_manager/cmd/set_limit_watts Command to set a watt limit for this EVSE that will be considered within the EnergyManager. This does not automatically imply that this limit will be set by the EVSE because the energymanagement might consider limitations from other sources, too. The payload can be a positive or negative number. diff --git a/modules/EvseManager/Charger.cpp b/modules/EvseManager/Charger.cpp index ea51bc117..3036eba33 100644 --- a/modules/EvseManager/Charger.cpp +++ b/modules/EvseManager/Charger.cpp @@ -1285,31 +1285,131 @@ bool Charger::deauthorize_internal() { return false; } -bool Charger::disable(int connector_id) { +bool Charger::enable_disable(int connector_id, const types::evse_manager::EnableDisableSource& source) { Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_disable); - if (connector_id not_eq 0) { - shared_context.connector_enabled = false; + + // insert the new request into the table + bool replaced = false; + for (auto& entry : enable_disable_source_table) { + if (entry.enable_source == source.enable_source) { + // replace this entry as it is an update from the same source + entry = source; + replaced = true; + } } - shared_context.current_state = EvseState::Disabled; - signal_simple_event(types::evse_manager::SessionEventEnum::Disabled); - return true; -} -bool Charger::enable(int connector_id) { - Everest::scoped_lock_timeout lock(state_machine_mutex, Everest::MutexDescription::Charger_enable); + if (not replaced) { + // Add to the table + enable_disable_source_table.push_back(source); + } + + // Find out if we are actually enabled or disabled at the moment + bool is_enabled = parse_enable_disable_source_table(); + + if (is_enabled) { + if (connector_id not_eq 0) { + shared_context.connector_enabled = true; + } - if (connector_id not_eq 0) { - shared_context.connector_enabled = true; + signal_simple_event(types::evse_manager::SessionEventEnum::Enabled); + if (shared_context.current_state == EvseState::Disabled) { + if (shared_context.connector_enabled) { + shared_context.current_state = EvseState::Idle; + } + return true; + } + } else { + if (connector_id not_eq 0) { + shared_context.connector_enabled = false; + } + shared_context.current_state = EvseState::Disabled; + signal_simple_event(types::evse_manager::SessionEventEnum::Disabled); } + return is_enabled; +} - signal_simple_event(types::evse_manager::SessionEventEnum::Enabled); - if (shared_context.current_state == EvseState::Disabled) { - if (shared_context.connector_enabled) { - shared_context.current_state = EvseState::Idle; +bool Charger::parse_enable_disable_source_table() { + /* Decide if we are enabled or disabled from the current table. + The logic is as follows: + + The source is an enum of the following source types : + + - Unspecified + - LocalAPI + - LocalKeyLock + - ServiceTechnician + - RemoteKeyLock + - MobileApp + - FirmwareUpdate + - CSMS + + The state can be either "enable", "disable", or "unassigned". + + "enable" and "disable" enforce the state to be enable/disable, while unassigned means that the source does not care + about the state and other sources may decide. + + Each call to this command will update an internal table that looks like this: + + | Source | State | Priority | + | ------------ | ------------- | -------- | + | Unspecified | unassigned | 10000 | + | LocalAPI | disable | 42 | + | LocalKeyLock | enable | 0 | + + Evaluation will be done based on priorities. 0 is the highest priority, 10000 the lowest, so in this example the + connector will be enabled regardless of what other sources say. Imagine LocalKeyLock sends a "unassigned, prio 0", + the table will then look like this: + + | Source | State | Priority | + | ------------ | ------------- | -------- | + | Unspecified | unassigned | 10000 | + | LocalAPI | disable | 42 | + | LocalKeyLock | unassigned | 0 | + + So now the connector will be disabled, because the second highest priority (42) sets it to disabled. + + If all sources are unassigned, the connector is enabled. + If two sources have the same priority, "disabled" has priority over "enabled". + */ + + bool is_enabled = true; // By default, it is enabled. + types::evse_manager::EnableDisableSource winning_source{types::evse_manager::Enable_source::Unspecified, + types::evse_manager::Enable_state::Unassigned, 10000}; + + // Walk through table + for (const auto& entry : enable_disable_source_table) { + // Ignore unassigned entries + if (entry.enable_state == types::evse_manager::Enable_state::Unassigned) { + continue; + } + + if (winning_source.enable_state == types::evse_manager::Enable_state::Unassigned) { + // Use the first entry that is not Unassigned + winning_source = entry; + if (entry.enable_state == types::evse_manager::Enable_state::Disable) { + is_enabled = false; + } else { + is_enabled = true; + } + } else if (entry.enable_priority == winning_source.enable_priority) { + // At the same priority, disable has higher priority then enable + if (entry.enable_state == types::evse_manager::Enable_state::Disable) { + is_enabled = false; + winning_source = entry; + } + + } else if (entry.enable_priority < winning_source.enable_priority) { + winning_source = entry; + if (entry.enable_state == types::evse_manager::Enable_state::Disable) { + is_enabled = false; + } else { + is_enabled = true; + } } - return true; } - return false; + + active_enable_disable_source = winning_source; + return is_enabled; } void Charger::set_faulted() { @@ -1644,4 +1744,8 @@ void Charger::clear_errors_on_unplug() { error_handling->clear_powermeter_transaction_start_failed_error(); } +types::evse_manager::EnableDisableSource Charger::get_last_enable_disable_source() { + return active_enable_disable_source; +} + } // namespace module diff --git a/modules/EvseManager/Charger.hpp b/modules/EvseManager/Charger.hpp index c24d2af57..a2269a151 100644 --- a/modules/EvseManager/Charger.hpp +++ b/modules/EvseManager/Charger.hpp @@ -101,8 +101,7 @@ class Charger { bool ac_hlc_enabled, 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); - bool enable(int connector_id); - bool disable(int connector_id); + bool enable_disable(int connector_id, const types::evse_manager::EnableDisableSource& source); void set_faulted(); void set_hlc_error(); @@ -199,6 +198,8 @@ class Charger { /// internal variable and is thus not idempotent. std::optional get_stop_signed_meter_value(); + types::evse_manager::EnableDisableSource get_last_enable_disable_source(); + private: std::optional take_signed_meter_data(std::optional& data); @@ -366,6 +367,11 @@ class Charger { // 4 seconds according to table 3 of ISO15118-3 static constexpr int T_STEP_EF = 4000; static constexpr int SOFT_OVER_CURRENT_TIMEOUT = 7000; + + types::evse_manager::EnableDisableSource active_enable_disable_source{ + types::evse_manager::Enable_source::Unspecified, types::evse_manager::Enable_state::Unassigned, 10000}; + std::vector enable_disable_source_table; + bool parse_enable_disable_source_table(); }; } // namespace module diff --git a/modules/EvseManager/EvseManager.cpp b/modules/EvseManager/EvseManager.cpp index c5ec6a4df..8f8cd9798 100644 --- a/modules/EvseManager/EvseManager.cpp +++ b/modules/EvseManager/EvseManager.cpp @@ -891,9 +891,11 @@ void EvseManager::ready_to_start_charging() { // this will publish a session event Enabled or Disabled that allows other modules the retrieve this state on // startup if (this->charger->get_current_state() == Charger::EvseState::Disabled) { - this->charger->disable(0); + charger->enable_disable( + 0, {types::evse_manager::Enable_source::Unspecified, types::evse_manager::Enable_state::Disable, 10000}); } else { - this->charger->enable(0); + charger->enable_disable( + 0, {types::evse_manager::Enable_source::Unspecified, types::evse_manager::Enable_state::Enable, 10000}); } this->p_evse->publish_ready(true); diff --git a/modules/EvseManager/evse/evse_managerImpl.cpp b/modules/EvseManager/evse/evse_managerImpl.cpp index fece7fa2f..f9da9882a 100644 --- a/modules/EvseManager/evse/evse_managerImpl.cpp +++ b/modules/EvseManager/evse/evse_managerImpl.cpp @@ -37,10 +37,16 @@ void evse_managerImpl::init() { [&charger = mod->charger, this](std::string data) { mod->updateLocalMaxWattLimit(std::stof(data)); }); mod->mqtt.subscribe(fmt::format("everest_external/nodered/{}/cmd/enable", mod->config.connector_id), - [&charger = mod->charger](const std::string& data) { charger->enable(0); }); + [&charger = mod->charger](const std::string& data) { + charger->enable_disable(0, {types::evse_manager::Enable_source::LocalAPI, + types::evse_manager::Enable_state::Enable, 100}); + }); mod->mqtt.subscribe(fmt::format("everest_external/nodered/{}/cmd/disable", mod->config.connector_id), - [&charger = mod->charger](const std::string& data) { charger->disable(0); }); + [&charger = mod->charger](const std::string& data) { + charger->enable_disable(0, {types::evse_manager::Enable_source::LocalAPI, + types::evse_manager::Enable_state::Disable, 100}); + }); mod->mqtt.subscribe(fmt::format("everest_external/nodered/{}/cmd/faulted", mod->config.connector_id), [&charger = mod->charger](const std::string& data) { charger->set_faulted(); }); @@ -331,6 +337,9 @@ void evse_managerImpl::ready() { types::evse_manager::AuthorizationEvent authorization_event; authorization_event.meter_value = mod->get_latest_powermeter_data_billing(); se.authorization_event = authorization_event; + + // Add source information (Who initiated this state change) + se.source = mod->charger->get_last_enable_disable_source(); } se.uuid = session_uuid; @@ -375,9 +384,9 @@ types::evse_manager::Evse evse_managerImpl::handle_get_evse() { return evse; } -bool evse_managerImpl::handle_enable(int& connector_id) { +bool evse_managerImpl::handle_enable_disable(int& connector_id, types::evse_manager::EnableDisableSource& cmd_source) { connector_status_changed = connector_id != 0; - return mod->charger->enable(connector_id); + return mod->charger->enable_disable(connector_id, cmd_source); }; void evse_managerImpl::handle_authorize_response(types::authorization::ProvidedIdToken& provided_token, @@ -415,11 +424,6 @@ void evse_managerImpl::handle_cancel_reservation() { mod->cancel_reservation(true); }; -bool evse_managerImpl::handle_disable(int& connector_id) { - connector_status_changed = connector_id != 0; - return mod->charger->disable(connector_id); -}; - void evse_managerImpl::handle_set_faulted() { mod->charger->set_faulted(); }; diff --git a/modules/EvseManager/evse/evse_managerImpl.hpp b/modules/EvseManager/evse/evse_managerImpl.hpp index 2b72f667f..48cca0931 100644 --- a/modules/EvseManager/evse/evse_managerImpl.hpp +++ b/modules/EvseManager/evse/evse_managerImpl.hpp @@ -35,8 +35,8 @@ class evse_managerImpl : public evse_managerImplBase { protected: // command handler functions (virtual) virtual types::evse_manager::Evse handle_get_evse() override; - virtual bool handle_enable(int& connector_id) override; - virtual bool handle_disable(int& connector_id) override; + virtual bool handle_enable_disable(int& connector_id, + types::evse_manager::EnableDisableSource& cmd_source) override; virtual void handle_authorize_response(types::authorization::ProvidedIdToken& provided_token, types::authorization::ValidationResult& validation_result) override; virtual void handle_withdraw_authorization() override; diff --git a/modules/EvseManager/tests/EvseManagerStub.hpp b/modules/EvseManager/tests/EvseManagerStub.hpp index 259d6c9e5..5f26c3e6a 100644 --- a/modules/EvseManager/tests/EvseManagerStub.hpp +++ b/modules/EvseManager/tests/EvseManagerStub.hpp @@ -26,6 +26,9 @@ struct evse_managerImplStub : public evse_managerImplBase { virtual bool handle_disable(int& connector_id) { return true; } + virtual bool handle_enable_disable(int& connector_id, types::evse_manager::EnableDisableSource& cmd_source) { + return true; + } virtual void handle_authorize_response(types::authorization::ProvidedIdToken& provided_token, types::authorization::ValidationResult& validation_result) { } diff --git a/modules/OCPP/OCPP.cpp b/modules/OCPP/OCPP.cpp index aac8907e7..342092fc3 100644 --- a/modules/OCPP/OCPP.cpp +++ b/modules/OCPP/OCPP.cpp @@ -80,6 +80,7 @@ static ErrorInfo get_error_info(const std::optional case types::evse_manager::ErrorEnum::EnergyManagement: case types::evse_manager::ErrorEnum::PermanentFault: case types::evse_manager::ErrorEnum::PowermeterTransactionStartFailed: + default: return {ocpp::v16::ChargePointErrorCode::InternalError, types::evse_manager::error_enum_to_string(error_code)}; } @@ -627,7 +628,9 @@ void OCPP::ready() { this->charge_point->register_disable_evse_callback([this](int32_t connector) { if (this->connector_evse_index_map.count(connector)) { - return this->r_evse_manager.at(this->connector_evse_index_map.at(connector))->call_disable(0); + return this->r_evse_manager.at(this->connector_evse_index_map.at(connector)) + ->call_enable_disable( + 0, {types::evse_manager::Enable_source::CSMS, types::evse_manager::Enable_state::Disable, 5000}); } else { return false; } @@ -638,7 +641,9 @@ void OCPP::ready() { this->charge_point->register_enable_evse_callback([this](int32_t connector) { if (this->connector_evse_index_map.count(connector)) { - return this->r_evse_manager.at(this->connector_evse_index_map.at(connector))->call_enable(0); + return this->r_evse_manager.at(this->connector_evse_index_map.at(connector)) + ->call_enable_disable( + 0, {types::evse_manager::Enable_source::CSMS, types::evse_manager::Enable_state::Enable, 5000}); } else { return false; } diff --git a/modules/OCPP/README.md b/modules/OCPP/README.md new file mode 100644 index 000000000..49bab291e --- /dev/null +++ b/modules/OCPP/README.md @@ -0,0 +1,7 @@ +# Interaction with EVSE Manager + +This module sets callbacks into libocpp to receive `ChangeAvailability.req` updates from the CSMS. + +These are sent to the EVSE Manager in `enable_disable` commands with a priority of 5000. ('types/energy_manager.yaml' contains the valid range.) + +5000 is mid-range. \ No newline at end of file diff --git a/modules/OCPP201/OCPP201.cpp b/modules/OCPP201/OCPP201.cpp index 854c8b842..3f71f4ad0 100644 --- a/modules/OCPP201/OCPP201.cpp +++ b/modules/OCPP201/OCPP201.cpp @@ -265,11 +265,15 @@ void OCPP201::ready() { callbacks.connector_effective_operative_status_changed_callback = [this](const int32_t evse_id, const int32_t connector_id, const ocpp::v201::OperationalStatusEnum new_status) { if (new_status == ocpp::v201::OperationalStatusEnum::Operative) { - if (this->r_evse_manager.at(evse_id - 1)->call_enable(connector_id)) { + if (this->r_evse_manager.at(evse_id - 1) + ->call_enable_disable(connector_id, {types::evse_manager::Enable_source::CSMS, + types::evse_manager::Enable_state::Enable, 5000})) { this->charge_point->on_enabled(evse_id, connector_id); } } else { - if (this->r_evse_manager.at(evse_id - 1)->call_disable(connector_id)) { + if (this->r_evse_manager.at(evse_id - 1) + ->call_enable_disable(connector_id, {types::evse_manager::Enable_source::CSMS, + types::evse_manager::Enable_state::Disable, 5000})) { this->charge_point->on_unavailable(evse_id, connector_id); } } diff --git a/types/evse_manager.yaml b/types/evse_manager.yaml index aa1f05495..afbc22607 100644 --- a/types/evse_manager.yaml +++ b/types/evse_manager.yaml @@ -324,6 +324,39 @@ types: vendor_error: description: The error code of the vendor type: string + EnableDisableSource: + description: >- + Source of a Enable or Disable command/event + type: object + required: + - enable_source + - enable_state + - enable_priority + properties: + enable_source: + description: Specifies the source + type: string + enum: + - Unspecified + - LocalAPI + - LocalKeyLock + - ServiceTechnician + - RemoteKeyLock + - MobileApp + - FirmwareUpdate + - CSMS + enable_state: + description: Specifies the state for this entry + type: string + enum: + - Unassigned + - Disable + - Enable + enable_priority: + description: Priority of this entry. The highest priority is 0. + type: integer + minimum: 0 + maximum: 10000 SessionEvent: description: Emits all events related to sessions type: object @@ -387,6 +420,11 @@ types: description: Details on error type type: object $ref: /evse_manager#/Error + source: + description: >- + Additional data for Enabled/Disabled events. Specifies the source of the command that changed the state. + type: object + $ref: /evse_manager#/EnableDisableSource Limits: description: Limits of this EVSE type: object @@ -553,4 +591,4 @@ types: type: object $ref: /evse_manager#/Connector minItems: 1 - maxItems: 128 + maxItems: 128 \ No newline at end of file From cff6f370155b8c0dfb519ef47fe30c8bba320e77 Mon Sep 17 00:00:00 2001 From: James Chapman Date: Fri, 17 May 2024 09:34:53 +0100 Subject: [PATCH 2/5] fix: merge error + missing blank line Signed-off-by: James Chapman --- modules/EvseManager/evse/evse_managerImpl.cpp | 6 +++--- types/evse_manager.yaml | 10 +++++----- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/modules/EvseManager/evse/evse_managerImpl.cpp b/modules/EvseManager/evse/evse_managerImpl.cpp index f9da9882a..4deda93d6 100644 --- a/modules/EvseManager/evse/evse_managerImpl.cpp +++ b/modules/EvseManager/evse/evse_managerImpl.cpp @@ -325,6 +325,9 @@ void evse_managerImpl::ready() { if (connector_status_changed) { se.connector_id = 1; } + + // Add source information (Who initiated this state change) + se.source = mod->charger->get_last_enable_disable_source(); } else if (e == types::evse_manager::SessionEventEnum::ChargingPausedEV or e == types::evse_manager::SessionEventEnum::ChargingPausedEVSE or e == types::evse_manager::SessionEventEnum::ChargingStarted or @@ -337,9 +340,6 @@ void evse_managerImpl::ready() { types::evse_manager::AuthorizationEvent authorization_event; authorization_event.meter_value = mod->get_latest_powermeter_data_billing(); se.authorization_event = authorization_event; - - // Add source information (Who initiated this state change) - se.source = mod->charger->get_last_enable_disable_source(); } se.uuid = session_uuid; diff --git a/types/evse_manager.yaml b/types/evse_manager.yaml index afbc22607..086787950 100644 --- a/types/evse_manager.yaml +++ b/types/evse_manager.yaml @@ -345,7 +345,7 @@ types: - MobileApp - FirmwareUpdate - CSMS - enable_state: + enable_state: description: Specifies the state for this entry type: string enum: @@ -377,7 +377,7 @@ types: format: date-time connector_id: description: >- - Id of the connector of this EVSE. + Id of the connector of this EVSE. If the connector_id is not specified, its assumed to be 1 type: integer event: @@ -406,13 +406,13 @@ types: $ref: /evse_manager#/TransactionFinished charging_state_changed_event: description: >- - Data for ChargingStateChangedEvent. Shall be set for the following SessionEventEnums: + Data for ChargingStateChangedEvent. Shall be set for the following SessionEventEnums: ChargingStarted, ChargingResumed, ChargingPausedEV, Charging type: object $ref: /evse_manager#/ChargingStateChangedEvent authorization_event: description: >- - Data for Authorization event. Shall be set for the following SessionEventEnums: + Data for Authorization event. Shall be set for the following SessionEventEnums: Authorized, Deauthorized type: object $ref: /evse_manager#/AuthorizationEvent @@ -591,4 +591,4 @@ types: type: object $ref: /evse_manager#/Connector minItems: 1 - maxItems: 128 \ No newline at end of file + maxItems: 128 From 9ffeba8168228737e39f17ae7b367b31fa578923 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piet=20G=C3=B6mpel?= <37657534+Pietfried@users.noreply.github.com> Date: Thu, 30 May 2024 09:50:04 +0200 Subject: [PATCH 3/5] Update interfaces/evse_manager.yaml MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Piet Gömpel <37657534+Pietfried@users.noreply.github.com> --- interfaces/evse_manager.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interfaces/evse_manager.yaml b/interfaces/evse_manager.yaml index 262d10d1d..d577a64b1 100644 --- a/interfaces/evse_manager.yaml +++ b/interfaces/evse_manager.yaml @@ -9,7 +9,7 @@ cmds: type: object $ref: /evse_manager#/Evse enable_disable: - description: Enables or disables the evse + description: Enables or disables the evse. Turns off PWM with error F. Charging is only possible if an EVSE is enabled. arguments: connector_id: description: Specifies the ID of the connector to enable. If 0, the whole EVSE should be enabled From e9d10432c3c4651fbdb4fee12a8d6d5aa8e1ad67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piet=20G=C3=B6mpel?= <37657534+Pietfried@users.noreply.github.com> Date: Thu, 30 May 2024 09:50:16 +0200 Subject: [PATCH 4/5] Update modules/API/API.cpp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: Piet Gömpel <37657534+Pietfried@users.noreply.github.com> --- modules/API/API.cpp | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/modules/API/API.cpp b/modules/API/API.cpp index 61e914203..ad4df9054 100644 --- a/modules/API/API.cpp +++ b/modules/API/API.cpp @@ -364,10 +364,11 @@ void API::init() { session_info->update_state(session_event.event, error); if (session_event.source.has_value()) { + const auto source = session_event.source.value(); session_info->set_enable_disable_source( - types::evse_manager::enable_source_to_string(session_event.source.value().enable_source), - types::evse_manager::enable_state_to_string(session_event.source.value().enable_state), - session_event.source.value().enable_priority); + types::evse_manager::enable_source_to_string(source.enable_source), + types::evse_manager::enable_state_to_string(source.enable_state), + source.enable_priority); } if (session_event.event == types::evse_manager::SessionEventEnum::SessionStarted) { From dbc85270111854268ed49be03202e362f407fb03 Mon Sep 17 00:00:00 2001 From: pietfried Date: Thu, 30 May 2024 09:51:55 +0200 Subject: [PATCH 5/5] replaced README.md with doc.rst Signed-off-by: pietfried --- modules/API/API.cpp | 3 +-- modules/OCPP/{README.md => doc.rst} | 5 +++-- 2 files changed, 4 insertions(+), 4 deletions(-) rename modules/OCPP/{README.md => doc.rst} (75%) diff --git a/modules/API/API.cpp b/modules/API/API.cpp index ad4df9054..7b398746c 100644 --- a/modules/API/API.cpp +++ b/modules/API/API.cpp @@ -367,8 +367,7 @@ void API::init() { const auto source = session_event.source.value(); session_info->set_enable_disable_source( types::evse_manager::enable_source_to_string(source.enable_source), - types::evse_manager::enable_state_to_string(source.enable_state), - source.enable_priority); + types::evse_manager::enable_state_to_string(source.enable_state), source.enable_priority); } if (session_event.event == types::evse_manager::SessionEventEnum::SessionStarted) { diff --git a/modules/OCPP/README.md b/modules/OCPP/doc.rst similarity index 75% rename from modules/OCPP/README.md rename to modules/OCPP/doc.rst index 49bab291e..171dad9df 100644 --- a/modules/OCPP/README.md +++ b/modules/OCPP/doc.rst @@ -1,7 +1,8 @@ -# Interaction with EVSE Manager +Interaction with EVSE Manager +============================= This module sets callbacks into libocpp to receive `ChangeAvailability.req` updates from the CSMS. These are sent to the EVSE Manager in `enable_disable` commands with a priority of 5000. ('types/energy_manager.yaml' contains the valid range.) -5000 is mid-range. \ No newline at end of file +5000 is mid-range.