diff --git a/config/config-sil-ocpp.yaml b/config/config-sil-ocpp.yaml index 5e068e9ce..98120a2d8 100644 --- a/config/config-sil-ocpp.yaml +++ b/config/config-sil-ocpp.yaml @@ -13,6 +13,8 @@ active_modules: config_module: device: auto supported_ISO15118_2: true + persistent_store: + module: PersistentStore evse_manager_1: evse: 1 module: EvseManager @@ -39,6 +41,9 @@ active_modules: hlc: - module_id: iso15118_charger implementation_id: charger + store: + - module_id: persistent_store + implementation_id: main evse_manager_2: module: EvseManager evse: 2 diff --git a/dependencies.yaml b/dependencies.yaml index 0758493fa..78a5d1398 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -60,7 +60,7 @@ libevse-security: # OCPP libocpp: git: https://github.com/EVerest/libocpp.git - git_tag: 1067cf3ddf27be3a43f8abc519b69da11c4ef921 + git_tag: aab1d5785bde141203b1a89b03d66fa91edf8093 cmake_condition: "EVEREST_DEPENDENCY_ENABLED_LIBOCPP" # Josev Josev: diff --git a/interfaces/powermeter.yaml b/interfaces/powermeter.yaml index f47da967f..5dbe92466 100644 --- a/interfaces/powermeter.yaml +++ b/interfaces/powermeter.yaml @@ -12,7 +12,11 @@ cmds: type: object $ref: /powermeter#/TransactionStartResponse stop_transaction: - description: Stop the transaction on the power meter and return the signed metering information + description: >- + Stop the transaction on the power meter and return the signed metering information. + If the transaction id is an empty string, all ongoing transaction should be cancelled. + This is used on start up to clear dangling transactions that might still be ongoing + in the power meter but are not known to the EvseManager. arguments: transaction_id: description: Transaction id diff --git a/modules/EvseManager/CMakeLists.txt b/modules/EvseManager/CMakeLists.txt index d7409f670..c072cf241 100644 --- a/modules/EvseManager/CMakeLists.txt +++ b/modules/EvseManager/CMakeLists.txt @@ -18,6 +18,7 @@ target_sources(${MODULE_NAME} IECStateMachine.cpp ErrorHandling.cpp backtrace.cpp + PersistentStore.cpp ) target_link_libraries(${MODULE_NAME} diff --git a/modules/EvseManager/Charger.cpp b/modules/EvseManager/Charger.cpp index d82339999..966a58c2e 100644 --- a/modules/EvseManager/Charger.cpp +++ b/modules/EvseManager/Charger.cpp @@ -25,12 +25,14 @@ namespace module { Charger::Charger(const std::unique_ptr& bsp, const std::unique_ptr& error_handling, const std::vector>& r_powermeter_billing, + const std::unique_ptr& _store, const types::evse_board_support::Connector_type& connector_type, const std::string& evse_id) : bsp(bsp), error_handling(error_handling), + r_powermeter_billing(r_powermeter_billing), + store(_store), connector_type(connector_type), - evse_id(evse_id), - r_powermeter_billing(r_powermeter_billing) { + evse_id(evse_id) { #ifdef EVEREST_USE_BACKTRACES Everest::install_backtrace_handler(); @@ -1169,6 +1171,8 @@ bool Charger::start_transaction() { } } + store->store_session(shared_context.session_uuid); + signal_transaction_started_event(shared_context.id_token); return true; } @@ -1191,11 +1195,51 @@ void Charger::stop_transaction() { } } + store->clear_session(); + signal_simple_event(types::evse_manager::SessionEventEnum::ChargingFinished); signal_transaction_finished_event(shared_context.last_stop_transaction_reason, shared_context.stop_transaction_id_token); } +void Charger::cleanup_transactions_on_startup() { + // See if we have an open transaction in persistent storage + auto session_uuid = store->get_session(); + if (not session_uuid.empty()) { + EVLOG_info << "Cleaning up transaction with UUID " << session_uuid << " on start up"; + store->clear_session(); + + types::evse_manager::TransactionFinished transaction_finished; + + // If yes, try to close nicely with the ID we remember and trigger a transaction finished event on success + for (const auto& meter : r_powermeter_billing) { + const auto response = meter->call_stop_transaction(session_uuid); + // If we fail to stop the transaction, it was probably just not active anymore + if (response.status == types::powermeter::TransactionRequestStatus::UNEXPECTED_ERROR) { + EVLOG_warning << "Failed to stop a transaction on the power meter " << response.error.value_or(""); + break; + } else if (response.status == types::powermeter::TransactionRequestStatus::OK) { + // Fill in OCMF from the recovered transaction + transaction_finished.start_signed_meter_value = response.start_signed_meter_value; + transaction_finished.signed_meter_value = response.signed_meter_value; + break; + } + } + + // Send out event to inform OCPP et al + std::optional id_token; + signal_transaction_finished_event(types::evse_manager::StopTransactionReason::PowerLoss, id_token); + } + + // Now we did what we could to clean up, so if there are still transactions going on in the power meter close them + // anyway. In this case we cannot generate a transaction finished event for OCPP et al since we cannot match it to + // our transaction anymore. + EVLOG_info << "Cleaning up any other transaction on start up"; + for (const auto& meter : r_powermeter_billing) { + meter->call_stop_transaction(""); + } +} + std::optional Charger::take_signed_meter_data(std::optional& in) { std::optional out; diff --git a/modules/EvseManager/Charger.hpp b/modules/EvseManager/Charger.hpp index 060357d66..85c509d90 100644 --- a/modules/EvseManager/Charger.hpp +++ b/modules/EvseManager/Charger.hpp @@ -45,6 +45,7 @@ #include "ErrorHandling.hpp" #include "EventQueue.hpp" #include "IECStateMachine.hpp" +#include "PersistentStore.hpp" #include "scoped_lock_timeout.hpp" #include "utils.hpp" @@ -57,6 +58,7 @@ class Charger { public: Charger(const std::unique_ptr& bsp, const std::unique_ptr& error_handling, const std::vector>& r_powermeter_billing, + const std::unique_ptr& store, const types::evse_board_support::Connector_type& connector_type, const std::string& evse_id); ~Charger(); @@ -149,6 +151,7 @@ class Charger { // Signal for EvseEvents sigslot::signal signal_simple_event; + sigslot::signal signal_session_resumed_event; sigslot::signal> signal_session_started_event; sigslot::signal signal_transaction_started_event; @@ -212,6 +215,8 @@ class Charger { connector_type = t; } + void cleanup_transactions_on_startup(); + private: utils::Stopwatch stopwatch; @@ -358,12 +363,11 @@ class Charger { const std::unique_ptr& bsp; const std::unique_ptr& error_handling; - + const std::vector>& r_powermeter_billing; + const std::unique_ptr& store; std::atomic connector_type{ types::evse_board_support::Connector_type::IEC62196Type2Cable}; - const std::string evse_id; - const std::vector>& r_powermeter_billing; // ErrorHandling events enum class ErrorHandlingEvents : std::uint8_t { diff --git a/modules/EvseManager/EvseManager.cpp b/modules/EvseManager/EvseManager.cpp index 798889a75..76c19a070 100644 --- a/modules/EvseManager/EvseManager.cpp +++ b/modules/EvseManager/EvseManager.cpp @@ -42,6 +42,9 @@ inline static types::authorization::ProvidedIdToken create_autocharge_token(std: } void EvseManager::init() { + + store = std::unique_ptr(new PersistentStore(r_store, info.id)); + random_delay_enabled = config.uk_smartcharging_random_delay_enable; random_delay_max_duration = std::chrono::seconds(config.uk_smartcharging_random_delay_max_duration); if (random_delay_enabled) { @@ -159,8 +162,8 @@ void EvseManager::ready() { error_handling = std::unique_ptr(new ErrorHandling(r_bsp, r_hlc, r_connector_lock, r_ac_rcd, p_evse, r_imd)); - charger = std::unique_ptr( - new Charger(bsp, error_handling, r_powermeter_billing(), hw_capabilities.connector_type, config.evse_id)); + charger = std::unique_ptr(new Charger(bsp, error_handling, r_powermeter_billing(), store, + hw_capabilities.connector_type, config.evse_id)); // Now incoming hardware capabilties can be processed hw_caps_mutex.unlock(); @@ -926,6 +929,17 @@ void EvseManager::ready() { [this] { return initial_powermeter_value_received; }); } + // Resuming left-over transaction from e.g. powerloss. This information allows other modules like to OCPP to be + // informed that the EvseManager is aware of previous sessions so that no individual cleanup is required + const auto session_id = store->get_session(); + if (!session_id.empty()) { + charger->signal_session_resumed_event(session_id); + } + + // By default cleanup left-over transaction from e.g. power loss + // TOOD: Add resume handling + charger->cleanup_transactions_on_startup(); + // start with a limit of 0 amps. We will get a budget from EnergyManager that is locally limited by hw // caps. charger->set_max_current(0.0F, date::utc_clock::now() + std::chrono::seconds(120)); diff --git a/modules/EvseManager/EvseManager.hpp b/modules/EvseManager/EvseManager.hpp index 70806e658..e3df3d2b0 100644 --- a/modules/EvseManager/EvseManager.hpp +++ b/modules/EvseManager/EvseManager.hpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -41,6 +42,7 @@ #include "CarManufacturer.hpp" #include "Charger.hpp" #include "ErrorHandling.hpp" +#include "PersistentStore.hpp" #include "SessionLog.hpp" #include "VarContainer.hpp" #include "scoped_lock_timeout.hpp" @@ -110,7 +112,8 @@ class EvseManager : public Everest::ModuleBase { std::vector> r_powermeter_car_side, std::vector> r_slac, std::vector> r_hlc, std::vector> r_imd, - std::vector> r_powersupply_DC, Conf& config) : + std::vector> r_powersupply_DC, + std::vector> r_store, Conf& config) : ModuleBase(info), mqtt(mqtt_provider), telemetry(telemetry), @@ -127,6 +130,7 @@ class EvseManager : public Everest::ModuleBase { r_hlc(std::move(r_hlc)), r_imd(std::move(r_imd)), r_powersupply_DC(std::move(r_powersupply_DC)), + r_store(std::move(r_store)), config(config){}; Everest::MqttProvider& mqtt; @@ -144,6 +148,7 @@ class EvseManager : public Everest::ModuleBase { const std::vector> r_hlc; const std::vector> r_imd; const std::vector> r_powersupply_DC; + const std::vector> r_store; const Conf& config; // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 @@ -191,6 +196,7 @@ class EvseManager : public Everest::ModuleBase { std::unique_ptr bsp; std::unique_ptr error_handling; + std::unique_ptr store; std::atomic_bool random_delay_enabled{false}; std::atomic_bool random_delay_running{false}; diff --git a/modules/EvseManager/PersistentStore.cpp b/modules/EvseManager/PersistentStore.cpp new file mode 100644 index 000000000..22655912d --- /dev/null +++ b/modules/EvseManager/PersistentStore.cpp @@ -0,0 +1,44 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2023 Pionix GmbH and Contributors to EVerest + +#include "PersistentStore.hpp" + +namespace module { + +PersistentStore::PersistentStore(const std::vector>& _r_store, const std::string module_id) : + r_store(_r_store) { + + if (r_store.size() > 0) { + active = true; + } + + session_key = module_id + "_session"; +} + +void PersistentStore::store_session(const std::string& session_uuid) { + if (active) { + r_store[0]->call_store(session_key, session_uuid); + } +} + +void PersistentStore::clear_session() { + if (active) { + r_store[0]->call_store(session_key, ""); + } +} + +std::string PersistentStore::get_session() { + if (active) { + auto r = r_store[0]->call_load(session_key); + try { + if (std::holds_alternative(r)) { + return std::get(r); + } + } catch (...) { + return {}; + } + } + return {}; +} + +} // namespace module diff --git a/modules/EvseManager/PersistentStore.hpp b/modules/EvseManager/PersistentStore.hpp new file mode 100644 index 000000000..f2c118e2a --- /dev/null +++ b/modules/EvseManager/PersistentStore.hpp @@ -0,0 +1,33 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2021 Pionix GmbH and Contributors to EVerest + +/* + The Persistent Store class is an abstraction layer to store any persistent information + (such as sessions) for the EvseManager. +*/ + +#ifndef EVSE_MANAGER_PERSISTENT_STORE_H_ +#define EVSE_MANAGER_PERSISTENT_STORE_H_ + +#include + +namespace module { + +class PersistentStore { +public: + // We need the r_bsp reference to be able to talk to the bsp driver module + explicit PersistentStore(const std::vector>& r_store, const std::string module_id); + + void store_session(const std::string& session_uuid); + void clear_session(); + std::string get_session(); + +private: + const std::vector>& r_store; + std::string session_key; + bool active{false}; +}; + +} // namespace module + +#endif // EVSE_MANAGER_PERSISTENT_STORE_H_ diff --git a/modules/EvseManager/evse/evse_managerImpl.cpp b/modules/EvseManager/evse/evse_managerImpl.cpp index 4dac48b94..bb4efa26d 100644 --- a/modules/EvseManager/evse/evse_managerImpl.cpp +++ b/modules/EvseManager/evse/evse_managerImpl.cpp @@ -323,6 +323,14 @@ void evse_managerImpl::ready() { publish_selected_protocol(this->mod->selected_protocol); }); + mod->charger->signal_session_resumed_event.connect([this](const std::string& session_id) { + types::evse_manager::SessionEvent session_event; + session_event.uuid = session_id; + session_event.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now()); + session_event.event = types::evse_manager::SessionEventEnum::SessionResumed; + publish_session_event(session_event); + }); + // Note: Deprecated. Only kept for Node red compatibility, will be removed in the future // Legacy external mqtt pubs mod->charger->signal_max_current.connect([this](float c) { @@ -339,7 +347,6 @@ void evse_managerImpl::ready() { mod->mqtt.publish(fmt::format("everest_external/nodered/{}/state/state", mod->config.connector_id), static_cast(s)); }); - // /Deprecated } types::evse_manager::Evse evse_managerImpl::handle_get_evse() { diff --git a/modules/EvseManager/manifest.yaml b/modules/EvseManager/manifest.yaml index 23fea72e0..31ce6c332 100644 --- a/modules/EvseManager/manifest.yaml +++ b/modules/EvseManager/manifest.yaml @@ -293,6 +293,10 @@ requires: interface: power_supply_DC min_connections: 0 max_connections: 1 + store: + interface: kvs + min_connections: 0 + max_connections: 1 enable_external_mqtt: true enable_telemetry: true metadata: diff --git a/modules/OCPP/OCPP.cpp b/modules/OCPP/OCPP.cpp index f98d60ffd..7fad97a52 100644 --- a/modules/OCPP/OCPP.cpp +++ b/modules/OCPP/OCPP.cpp @@ -253,6 +253,11 @@ void OCPP::init_evse_subscriptions() { return; } + if (session_event.event == types::evse_manager::SessionEventEnum::SessionResumed) { + this->resuming_session_ids.insert(session_event.uuid); + return; + } + if (!this->started) { EVLOG_info << "OCPP not fully initialized, but received a session event on evse_id: " << evse_id << " that will be queued up: " << session_event.event; @@ -770,7 +775,7 @@ void OCPP::ready() { } const auto boot_reason = conversions::to_ocpp_boot_reason_enum(this->r_system->call_get_boot_reason()); - if (this->charge_point->start({}, boot_reason)) { + if (this->charge_point->start({}, boot_reason, this->resuming_session_ids)) { // signal that we're started this->started = true; EVLOG_info << "OCPP initialized"; diff --git a/modules/OCPP/OCPP.hpp b/modules/OCPP/OCPP.hpp index 5557f3a49..ead6a60f0 100644 --- a/modules/OCPP/OCPP.hpp +++ b/modules/OCPP/OCPP.hpp @@ -141,6 +141,7 @@ class OCPP : public Everest::ModuleBase { connector_evse_index_map; // provides access to r_evse_manager index by using OCPP connector id std::map evse_ready_map; std::map> evse_soc_map; + std::set resuming_session_ids; std::mutex evse_ready_mutex; std::condition_variable evse_ready_cv; bool all_evse_ready(); diff --git a/types/evse_manager.yaml b/types/evse_manager.yaml index ca17fe7ab..d3499fc01 100644 --- a/types/evse_manager.yaml +++ b/types/evse_manager.yaml @@ -109,6 +109,7 @@ types: ReplugStarted: Signaled when the EVSE Manager virtually replugs without interrupting the session or transaction ReplugFinished: Signaled when the virtual replug process has finished PluginTimeout: Signaled when an EV has been Plugged in but no authorization is present within specified ConnectionTimeout + SessionResumed: Signaled when a session is resumed at startup (e.g. because of previous powerloss) type: string enum: - Authorized @@ -134,6 +135,7 @@ types: - ReplugFinished - PluginTimeout - SwitchingPhases + - SessionResumed SessionStarted: description: Data for the SessionStarted event type: object