diff --git a/interfaces/evse_manager.yaml b/interfaces/evse_manager.yaml index 82225b19ff..9981f1bd54 100644 --- a/interfaces/evse_manager.yaml +++ b/interfaces/evse_manager.yaml @@ -14,6 +14,10 @@ cmds: 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 @@ -27,6 +31,10 @@ cmds: connector_id: description: Specifies the ID of the connector. If 0, the whole EVSE should be disabled type: integer + cmd_source: + description: Source of the enable command + type: object + $ref: /evse_manager#/EnableDisableSource result: description: >- Returns true if evse was disabled (or was disabled before), returns diff --git a/modules/API/API.cpp b/modules/API/API.cpp index 21fd9ae223..10e758ba4f 100644 --- a/modules/API/API.cpp +++ b/modules/API/API.cpp @@ -190,6 +190,12 @@ void SessionInfo::set_latest_total_w(double latest_total_w) { this->latest_total_w = latest_total_w; } +void SessionInfo::set_enable_disable_source(const std::string& last_source, bool allow_others_to_modify) { + std::lock_guard lock(this->session_info_mutex); + this->last_enable_disable_source = last_source; + this->enable_disable_other_sources_may_modify = allow_others_to_modify; +} + static void to_json(json& j, const SessionInfo::Error& e) { j = json{{"type", e.type}, {"description", e.description}, {"severity", e.severity}}; } @@ -207,14 +213,17 @@ 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)}, + {"last_enable_disable_source", this->last_enable_disable_source}, + {"enable_disable_other_sources_may_modify", this->enable_disable_other_sources_may_modify}}); return session_info.dump(); } @@ -305,6 +314,12 @@ 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::source_to_string(session_event.source.value().source), + session_event.source.value().other_sources_may_modify); + } + if (session_event.event == types::evse_manager::SessionEventEnum::SessionStarted) { if (session_event.session_started.has_value()) { auto session_started = session_event.session_started.value(); @@ -350,31 +365,70 @@ void API::init() { 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::Source::LocalAPI, true}; + if (!data.empty()) { try { connector_id = std::stoi(data); + EVLOG_warning << "enable: Argument is an integer, using deprecated compatibility mode"; + } catch (const std::exception& e) { + // argument is not just an integer, try to parse the full parameter object + } + + try { + auto arg = json::parse(data); + if (arg.contains("connector_id")) { + connector_id = arg.at("connector_id"); + } + if (arg.contains("source")) { + enable_source.source = types::evse_manager::string_to_source(arg.at("source")); + } + if (arg.contains("other_sources_may_modify")) { + enable_source.other_sources_may_modify = arg.at("other_sources_may_modify"); + } } 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(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 disable_source{types::evse_manager::Source::LocalAPI, true}; + 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; + // argument is not just an integer, try to parse the full parameter object } + + try { + auto arg = json::parse(data); + if (arg.contains("connector_id")) { + connector_id = arg.at("connector_id"); + } + if (arg.contains("source")) { + disable_source.source = types::evse_manager::string_to_source(arg.at("source")); + } + if (arg.contains("other_sources_may_modify")) { + disable_source.other_sources_may_modify = arg.at("other_sources_may_modify"); + } + } catch (const std::exception& e) { + EVLOG_error << "disable: Cannot parse argument, command ignored: " << e.what(); + } + } else { + EVLOG_error << "disable: No argument specified, ignoring command"; } - evse->call_disable(connector_id); + + evse->call_disable(connector_id, disable_source); }); std::string cmd_pause_charging = cmd_base + "pause_charging"; diff --git a/modules/API/API.hpp b/modules/API/API.hpp index fafbf147f6..9b016367fb 100644 --- a/modules/API/API.hpp +++ b/modules/API/API.hpp @@ -57,6 +57,7 @@ class SessionInfo { void set_end_energy_export_wh(int32_t end_energy_export_wh); void set_latest_energy_export_wh(int32_t latest_export_energy_wh); void set_latest_total_w(double latest_total_w); + void set_enable_disable_source(const std::string& last_source, bool allow_others_to_modify); /// \brief Converts this struct into a serialized json object operator std::string(); @@ -91,6 +92,9 @@ class SessionInfo { bool is_state_charging(const SessionInfo::State current_state); std::string state_to_string(State s); + + std::string last_enable_disable_source{"Unspecified"}; + bool enable_disable_other_sources_may_modify{true}; }; } // namespace module // ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 diff --git a/modules/API/README.md b/modules/API/README.md index 7bef914680..ed56d0d57b 100644 --- a/modules/API/README.md +++ b/modules/API/README.md @@ -45,7 +45,9 @@ This variable is published every second and contains a json object with informat "latest_total_w": 0.0, "state": "Unplugged", "active_permanent_faults": [], - "active_errors": [] + "active_errors": [], + "last_enable_disable_source": "Unspecified", + "enable_disable_allow_others_to_modify": true } ``` @@ -71,7 +73,9 @@ 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", + "last_enable_disable_source": "Unspecified", + "enable_disable_other_sources_may_modify": true } ``` @@ -200,10 +204,38 @@ If the OCPP module has not yet published its "is_connected" status or no OCPP mo ## Commands and variables published in response ### 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. +Command to enable a connector on the EVSE. The payload should be the following json: +```json + { + "connector_id": 0, + "source": "LocalAPI", + "other_sources_may_modify": true + } +``` +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 bool flag other_sources_may_modify works the following way when set to false: + If a source type A enables the connector, only the source type A may disable it again. The same is true for the opposite: if a source type A disables the connector, only source type A may enable it again. + + If other_sources_may_modify is set to true, any source may enable/disable regardless of the history. + + This API endpoint has a compatibility mode: If you just specify a positive integer instead of the object described above, + it will use it as connector_id and set source to LocalAPI and other_sources_may_modify to true. + ### 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. +Command to disable a connector on the EVSE. For payload specifications, see cmd/enable. ### everest_api/evse_manager/cmd/pause_charging If any arbitrary payload is published to this topic charging will be paused by the EVSE. diff --git a/modules/EvseManager/Charger.cpp b/modules/EvseManager/Charger.cpp index 5b995c2ea5..2ca2624799 100644 --- a/modules/EvseManager/Charger.cpp +++ b/modules/EvseManager/Charger.cpp @@ -1114,8 +1114,17 @@ bool Charger::DeAuthorize() { return false; } -bool Charger::disable(int connector_id) { +bool Charger::disable(int connector_id, const types::evse_manager::EnableDisableSource& disable_source) { std::lock_guard lock(stateMutex); + if (not last_enable_disable_source.other_sources_may_modify and + disable_source.source not_eq last_enable_disable_source.source) { + // we do not disable here as the last source did not allow modification by other sources + EVLOG_warning << "Ignoring disable command, source is not allowed to enable"; + return false; + } + + last_enable_disable_source = disable_source; + if (connector_id != 0) { connectorEnabled = false; } @@ -1124,8 +1133,16 @@ bool Charger::disable(int connector_id) { return true; } -bool Charger::enable(int connector_id) { +bool Charger::enable(int connector_id, const types::evse_manager::EnableDisableSource& enable_source) { std::lock_guard lock(stateMutex); + if (not last_enable_disable_source.other_sources_may_modify and + enable_source.source not_eq last_enable_disable_source.source) { + // we do not disable here as the last source did not allow modification by other sources + EVLOG_warning << "Ignoring enable command, source is not allowed to enable"; + return false; + } + + last_enable_disable_source = enable_source; if (connector_id != 0) { connectorEnabled = true; } @@ -1472,4 +1489,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 last_enable_disable_source; +} + } // namespace module diff --git a/modules/EvseManager/Charger.hpp b/modules/EvseManager/Charger.hpp index 9f3953d026..7c9986e84d 100644 --- a/modules/EvseManager/Charger.hpp +++ b/modules/EvseManager/Charger.hpp @@ -71,8 +71,8 @@ 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(int connector_id, const types::evse_manager::EnableDisableSource& source); + bool disable(int connector_id, const types::evse_manager::EnableDisableSource& source); void set_faulted(); void set_hlc_error(); void set_rcd_error(); @@ -181,6 +181,8 @@ class Charger { bool errors_prevent_charging(); + types::evse_manager::EnableDisableSource get_last_enable_disable_source(); + private: void bcb_toggle_reset(); void bcb_toggle_detect_start_pulse(); @@ -332,6 +334,8 @@ class Charger { constexpr static int legacy_wakeup_timeout{30000}; void clear_errors_on_unplug(); + + types::evse_manager::EnableDisableSource last_enable_disable_source{types::evse_manager::Source::Unspecified, true}; }; #define CHARGER_ABSOLUTE_MAX_CURRENT double(80.0F) diff --git a/modules/EvseManager/EvseManager.cpp b/modules/EvseManager/EvseManager.cpp index 173739d6b8..2102203712 100644 --- a/modules/EvseManager/EvseManager.cpp +++ b/modules/EvseManager/EvseManager.cpp @@ -843,7 +843,7 @@ void EvseManager::ready() { void EvseManager::ready_to_start_charging() { charger->run(); - charger->enable(0); + charger->enable(0, {types::evse_manager::Source::Unspecified, true}); this->p_evse->publish_ready(true); EVLOG_info << fmt::format(fmt::emphasis::bold | fg(fmt::terminal_color::green), diff --git a/modules/EvseManager/evse/evse_managerImpl.cpp b/modules/EvseManager/evse/evse_managerImpl.cpp index 73590e2d2b..0b7f869666 100644 --- a/modules/EvseManager/evse/evse_managerImpl.cpp +++ b/modules/EvseManager/evse/evse_managerImpl.cpp @@ -40,10 +40,14 @@ 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(0, {types::evse_manager::Source::LocalAPI, true}); + }); 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->disable(0, {types::evse_manager::Source::LocalAPI, true}); + }); mod->mqtt.subscribe(fmt::format("everest_external/nodered/{}/cmd/faulted", mod->config.connector_id), [&charger = mod->charger](const std::string& data) { charger->set_faulted(); }); @@ -277,6 +281,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(); } se.uuid = session_uuid; @@ -321,9 +328,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(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(connector_id, cmd_source); }; void evse_managerImpl::handle_authorize_response(types::authorization::ProvidedIdToken& provided_token, @@ -361,9 +368,9 @@ void evse_managerImpl::handle_cancel_reservation() { mod->cancel_reservation(true); }; -bool evse_managerImpl::handle_disable(int& connector_id) { +bool evse_managerImpl::handle_disable(int& connector_id, types::evse_manager::EnableDisableSource& cmd_source) { connector_status_changed = connector_id != 0; - return mod->charger->disable(connector_id); + return mod->charger->disable(connector_id, cmd_source); }; void evse_managerImpl::handle_set_faulted() { diff --git a/modules/EvseManager/evse/evse_managerImpl.hpp b/modules/EvseManager/evse/evse_managerImpl.hpp index a9af2dfa7c..db4b5f94d4 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(int& connector_id, types::evse_manager::EnableDisableSource& cmd_source) override; + virtual bool handle_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/OCPP/OCPP.cpp b/modules/OCPP/OCPP.cpp index 6e5659156a..0288a9b87d 100644 --- a/modules/OCPP/OCPP.cpp +++ b/modules/OCPP/OCPP.cpp @@ -112,6 +112,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)}; } } @@ -672,7 +673,8 @@ 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_disable(0, {types::evse_manager::Source::CSMS, true}); } else { return false; } @@ -680,7 +682,8 @@ 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(0, {types::evse_manager::Source::CSMS, true}); } else { return false; } diff --git a/modules/OCPP201/OCPP201.cpp b/modules/OCPP201/OCPP201.cpp index b90e81e6d2..68aa150201 100644 --- a/modules/OCPP201/OCPP201.cpp +++ b/modules/OCPP201/OCPP201.cpp @@ -669,11 +669,13 @@ 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(connector_id, {types::evse_manager::Source::CSMS, true})) { 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_disable(connector_id, {types::evse_manager::Source::CSMS, true})) { this->charge_point->on_unavailable(evse_id, connector_id); } } diff --git a/types/evse_manager.yaml b/types/evse_manager.yaml index 9e083f7391..f3e230bc5c 100644 --- a/types/evse_manager.yaml +++ b/types/evse_manager.yaml @@ -291,6 +291,29 @@ 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: + - source + - other_sources_may_modify + properties: + source: + description: Specifies the source + type: string + enum: + - Unspecified + - LocalAPI + - LocalKeyLock + - ServiceTechnician + - RemoteKeyLock + - MobileApp + - FirmwareUpdate + - CSMS + other_sources_may_modify: + description: If set to true, other sources may e.g. enable after it was disabled. If set to false, only the same source can modify the enabled/disabled state again. + type: boolean SessionEvent: description: Emits all events related to sessions type: object @@ -337,6 +360,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 @@ -503,4 +531,4 @@ types: type: object $ref: /evse_manager#/Connector minItems: 1 - maxItems: 128 + maxItems: 128 \ No newline at end of file