From 3609946044a6dc620fded6c31adfacd907de4280 Mon Sep 17 00:00:00 2001 From: Cornelius Claussen Date: Mon, 5 Feb 2024 12:30:17 +0100 Subject: [PATCH] Add random delay according to UK smart charging regulations to EvseManager and API modules. The feature is disabled by default. It can be controlled at runtime using the random_delay implementation. Includes changes required by changed type of composite schedules in libocpp from ChargingSchedule to EnhancedChargingSchedule, which includes stackLevel in EnhancedChargingSchedulePeriods Signed-off-by: Cornelius Claussen --- config/config-sil-energy-management.yaml | 9 +- interfaces/uk_random_delay.yaml | 23 ++ modules/API/API.cpp | 76 +++++- modules/API/API.hpp | 21 +- modules/API/README.md | 10 +- modules/API/manifest.yaml | 4 + modules/EnergyManager/EnergyManager.cpp | 29 +- modules/EnergyManager/EnergyManager.hpp | 5 +- modules/EnergyManager/Market.cpp | 2 +- modules/EvseManager/CMakeLists.txt | 1 + modules/EvseManager/Charger.cpp | 1 + modules/EvseManager/EvseManager.cpp | 11 + modules/EvseManager/EvseManager.hpp | 16 +- .../EvseManager/energy_grid/energyImpl.cpp | 257 ++++++++++++------ .../EvseManager/energy_grid/energyImpl.hpp | 7 +- modules/EvseManager/manifest.yaml | 20 ++ .../random_delay/uk_random_delayImpl.cpp | 35 +++ .../random_delay/uk_random_delayImpl.hpp | 64 +++++ types/energy.yaml | 5 + 19 files changed, 487 insertions(+), 109 deletions(-) create mode 100644 interfaces/uk_random_delay.yaml create mode 100644 modules/EvseManager/random_delay/uk_random_delayImpl.cpp create mode 100644 modules/EvseManager/random_delay/uk_random_delayImpl.hpp diff --git a/config/config-sil-energy-management.yaml b/config/config-sil-energy-management.yaml index 5b382294a..fcaa84c1e 100644 --- a/config/config-sil-energy-management.yaml +++ b/config/config-sil-energy-management.yaml @@ -27,6 +27,10 @@ active_modules: ac_hlc_enabled: true ac_hlc_use_5percent: false ac_enforce_hlc: false + request_zero_power_in_idle: false + uk_smartcharging_random_delay_at_any_change: false + uk_smartcharging_random_delay_max_duration: 100 + uk_smartcharging_random_delay_enable: true connections: bsp: - module_id: yeti_driver_1 @@ -148,7 +152,7 @@ active_modules: config_module: schedule_total_duration: 2 schedule_interval_duration: 15 - debug: true + debug: false connections: energy_trunk: - module_id: grid_connection_point @@ -174,4 +178,7 @@ active_modules: evse_manager: - module_id: evse_manager_1 implementation_id: evse + random_delay: + - module_id: evse_manager_1 + implementation_id: random_delay x-module-layout: {} diff --git a/interfaces/uk_random_delay.yaml b/interfaces/uk_random_delay.yaml new file mode 100644 index 000000000..311a50df3 --- /dev/null +++ b/interfaces/uk_random_delay.yaml @@ -0,0 +1,23 @@ +description: >- + This interface provides functions for a random delay feature as required by the UK smart charging regulations + The logic whether to use a random delay or not is not included in EvseManager, a different module can use this + interface to enable/disable the feature during runtime and cancel a running random delay. + This always applies to all connectors of this EVSE. + By default, on start up, random delays are disabled. +cmds: + enable: + description: Call to enable the random delay feature + disable: + description: Call to enable the random delay feature + cancel: + description: Cancels a running random delay. The effect is the same as if the time expired just now. + set_duration_s: + description: Set the maximum duration of the random delay. Default is 600 seconds. + arguments: + value: + description: Maximum duration in seconds + type: integer +vars: + countdown_s: + description: Countdown of the currently running random delay in seconds. If it is 0, no random delay is active. + type: integer \ No newline at end of file diff --git a/modules/API/API.cpp b/modules/API/API.cpp index 21fd9ae22..421c97c72 100644 --- a/modules/API/API.cpp +++ b/modules/API/API.cpp @@ -190,6 +190,11 @@ void SessionInfo::set_latest_total_w(double latest_total_w) { this->latest_total_w = latest_total_w; } +void SessionInfo::set_uk_random_delay_remaining(int seconds_remaining) { + std::lock_guard lock(this->session_info_mutex); + this->uk_random_delay_remaining_s = seconds_remaining; +} + static void to_json(json& j, const SessionInfo::Error& e) { j = json{{"type", e.type}, {"description", e.description}, {"severity", e.severity}}; } @@ -215,6 +220,9 @@ SessionInfo::operator std::string() { {"latest_total_w", this->latest_total_w}, {"charging_duration_s", charging_duration_s.count()}, {"datetime", Everest::Date::to_rfc3339(now)}}); + if (uk_random_delay_remaining_s > 0) { + session_info["uk_random_delay_remaining_s"] = uk_random_delay_remaining_s; + } return session_info.dump(); } @@ -423,18 +431,59 @@ void API::init() { } evse->call_force_unlock(connector_id); // }); + + // Check if a uk_random_delay is connected that matches this evse_manager + for (auto& random_delay : this->r_random_delay) { + if (random_delay->module_id == evse->module_id) { + + random_delay->subscribe_countdown_s( + [&session_info](int s) { session_info->set_uk_random_delay_remaining(s); }); + + std::string cmd_uk_random_delay = cmd_base + "uk_random_delay"; + this->mqtt.subscribe(cmd_uk_random_delay, [&random_delay](const std::string& data) { + if (data == "enable") { + random_delay->call_enable(); + } else if (data == "disable") { + random_delay->call_disable(); + } else if (data == "cancel") { + random_delay->call_cancel(); + } + }); + + std::string uk_random_delay_set_max_duration_s = cmd_base + "uk_random_delay_set_max_duration_s"; + this->mqtt.subscribe(uk_random_delay_set_max_duration_s, [&random_delay](const std::string& data) { + int seconds = 600; + try { + seconds = std::stoi(data); + } catch (const std::exception& e) { + EVLOG_error + << "Could not parse connector duration value for uk_random_delay_set_max_duration_s: " + << e.what(); + } + random_delay->call_set_duration_s(seconds); + }); + } + } } std::string var_ocpp_connection_status = api_base + "ocpp/var/connection_status"; + std::string var_ocpp_schedule = api_base + "ocpp/var/charging_schedules"; if (this->r_ocpp.size() == 1) { + this->r_ocpp.at(0)->subscribe_is_connected([this](bool is_connected) { + std::scoped_lock lock(ocpp_data_mutex); if (is_connected) { this->ocpp_connection_status = "connected"; } else { this->ocpp_connection_status = "disconnected"; } }); + + this->r_ocpp.at(0)->subscribe_charging_schedules([this, &var_ocpp_schedule](json schedule) { + std::scoped_lock lock(ocpp_data_mutex); + this->ocpp_charging_schedule = schedule; + }); } std::string var_info = api_base + "info/var/info"; @@ -450,18 +499,23 @@ void API::init() { } } - this->api_threads.push_back(std::thread([this, var_connectors, connectors, var_info, var_ocpp_connection_status]() { - auto next_tick = std::chrono::steady_clock::now(); - while (this->running) { - json connectors_array = connectors; - this->mqtt.publish(var_connectors, connectors_array.dump()); - this->mqtt.publish(var_info, this->charger_information.dump()); - this->mqtt.publish(var_ocpp_connection_status, this->ocpp_connection_status); + this->api_threads.push_back( + std::thread([this, var_connectors, connectors, var_info, var_ocpp_connection_status, var_ocpp_schedule]() { + auto next_tick = std::chrono::steady_clock::now(); + while (this->running) { + json connectors_array = connectors; + this->mqtt.publish(var_connectors, connectors_array.dump()); + this->mqtt.publish(var_info, this->charger_information.dump()); + { + std::scoped_lock lock(ocpp_data_mutex); + this->mqtt.publish(var_ocpp_connection_status, this->ocpp_connection_status); + this->mqtt.publish(var_ocpp_schedule, ocpp_charging_schedule.dump()); + } - next_tick += NOTIFICATION_PERIOD; - std::this_thread::sleep_until(next_tick); - } - })); + next_tick += NOTIFICATION_PERIOD; + std::this_thread::sleep_until(next_tick); + } + })); } void API::ready() { diff --git a/modules/API/API.hpp b/modules/API/API.hpp index fafbf147f..dc8acfb28 100644 --- a/modules/API/API.hpp +++ b/modules/API/API.hpp @@ -16,6 +16,7 @@ // headers for required interface implementations #include #include +#include // ev@4bf81b14-a215-475c-a1d3-0a484ae48918:v1 // insert your custom include headers here @@ -57,6 +58,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_uk_random_delay_remaining(int seconds_remaining); /// \brief Converts this struct into a serialized json object operator std::string(); @@ -66,10 +68,12 @@ class SessionInfo { std::vector active_permanent_faults; ///< Array of currently active permanent faults that prevent charging std::vector active_errors; ///< Array of currently active errors that do not prevent charging - int32_t start_energy_import_wh; ///< Energy reading (import) at the beginning of this charging session in Wh - int32_t end_energy_import_wh; ///< Energy reading (import) at the end of this charging session in Wh - int32_t start_energy_export_wh; ///< Energy reading (export) at the beginning of this charging session in Wh - int32_t end_energy_export_wh; ///< Energy reading (export) at the end of this charging session in Wh + int32_t start_energy_import_wh; ///< Energy reading (import) at the beginning of this charging session in Wh + int32_t end_energy_import_wh; ///< Energy reading (import) at the end of this charging session in Wh + int32_t start_energy_export_wh; ///< Energy reading (export) at the beginning of this charging session in Wh + int32_t end_energy_export_wh; ///< Energy reading (export) at the end of this charging session in Wh + int32_t uk_random_delay_remaining_s{0}; ///< Remaining time of a UK smart charging regs delay. Set to 0 if no delay + ///< is active std::chrono::time_point start_time_point; ///< Start of the charging session std::chrono::time_point end_time_point; ///< End of the charging session double latest_total_w; ///< Latest total power reading in W @@ -142,18 +146,20 @@ class API : public Everest::ModuleBase { API() = delete; API(const ModuleInfo& info, Everest::MqttProvider& mqtt_provider, std::unique_ptr p_main, std::vector> r_evse_manager, std::vector> r_ocpp, - Conf& config) : + std::vector> r_random_delay, Conf& config) : ModuleBase(info), mqtt(mqtt_provider), p_main(std::move(p_main)), r_evse_manager(std::move(r_evse_manager)), r_ocpp(std::move(r_ocpp)), + r_random_delay(std::move(r_random_delay)), config(config){}; Everest::MqttProvider& mqtt; const std::unique_ptr p_main; const std::vector> r_evse_manager; const std::vector> r_ocpp; + const std::vector> r_random_delay; const Conf& config; // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 @@ -179,8 +185,11 @@ class API : public Everest::ModuleBase { std::list hw_capabilities_str; std::string selected_protocol; json charger_information; - std::string ocpp_connection_status = "unknown"; std::unique_ptr limit_decimal_places; + + std::mutex ocpp_data_mutex; + json ocpp_charging_schedule; + std::string ocpp_connection_status = "unknown"; // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 }; diff --git a/modules/API/README.md b/modules/API/README.md index 7bef91468..69a4f1b4e 100644 --- a/modules/API/README.md +++ b/modules/API/README.md @@ -45,7 +45,8 @@ 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": [], + "uk_random_delay_remaining_s": 320 } ``` @@ -80,6 +81,7 @@ Example with permanent faults being active: - **datetime** contains a string representation of the current UTC datetime in RFC3339 format - **discharged_energy_wh** contains the energy fed into the power grid by the EV in Wh - **latest_total_w** contains the latest total power reading over all phases in Watt +- **uk_random_delay_remaining_s** Remaining time of a currently active random delay according to UK smart charging regulations. Not set if no delay is active. - **state** contains the current state of the charging session, from a list of the following possible states: - Unplugged - Disabled @@ -219,3 +221,9 @@ Command to set a watt limit for this EVSE that will be considered within the Ene ### everest_api/evse_manager/cmd/force_unlock Command to force unlock a connector on the EVSE. They payload should be a positive integer identifying the connector that should be unlocked. If the payload is empty or cannot be converted to an integer connector 1 is assumed. + +### everest_api/evse_manager/cmd/uk_random_delay +Command to control the UK Smart Charging random delay feature. The payload can be the following enum: "enable" and "disable" to enable/disable the feature entirely or "cancel" to cancel an ongoing delay. + +### everest_api/evse_manager/cmd/uk_random_delay_set_max_duration_s +Command to set the UK Smart Charging random delay maximum duration. Payload is an integer in seconds. diff --git a/modules/API/manifest.yaml b/modules/API/manifest.yaml index b246dc11f..ffde6af47 100644 --- a/modules/API/manifest.yaml +++ b/modules/API/manifest.yaml @@ -181,6 +181,10 @@ requires: interface: ocpp min_connections: 0 max_connections: 1 + random_delay: + interface: uk_random_delay + min_connections: 0 + max_connections: 128 enable_external_mqtt: true metadata: license: https://opensource.org/licenses/Apache-2.0 diff --git a/modules/EnergyManager/EnergyManager.cpp b/modules/EnergyManager/EnergyManager.cpp index 3c9b8f556..15dfde608 100644 --- a/modules/EnergyManager/EnergyManager.cpp +++ b/modules/EnergyManager/EnergyManager.cpp @@ -15,6 +15,11 @@ void EnergyManager::init() { // Received new energy object from a child. std::scoped_lock lock(energy_mutex); energy_flow_request = e; + + if (is_priority_request(e)) { + // trigger optimization now + mainloop_sleep_condvar.notify_all(); + } }); invoke_init(*p_main); @@ -30,11 +35,33 @@ void EnergyManager::ready() { config.slice_ampere, config.slice_watt, config.debug, energy_flow_request); auto optimized_values = run_optimizer(energy_flow_request); enforce_limits(optimized_values); - sleep(config.update_interval); + { + std::unique_lock lock(mainloop_sleep_mutex); + mainloop_sleep_condvar.wait_for(lock, std::chrono::seconds(config.update_interval)); + } } }).detach(); } +// Check if any node set the priority request flag +bool EnergyManager::is_priority_request(const types::energy::EnergyFlowRequest& e) { + bool prio = e.priority_request.has_value() and e.priority_request.value(); + + // If this node has priority, no need to travese the tree any longer + if (prio) { + return true; + } + + // recurse to all children + for (auto& c : e.children) { + if (is_priority_request(c)) { + return true; + } + } + + return false; +} + void EnergyManager::enforce_limits(const std::vector& limits) { for (const auto& it : limits) { if (globals.debug) diff --git a/modules/EnergyManager/EnergyManager.hpp b/modules/EnergyManager/EnergyManager.hpp index f5735653b..26e1366e5 100644 --- a/modules/EnergyManager/EnergyManager.hpp +++ b/modules/EnergyManager/EnergyManager.hpp @@ -65,7 +65,7 @@ class EnergyManager : public Everest::ModuleBase { // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 // insert your private definitions here - + bool is_priority_request(const types::energy::EnergyFlowRequest& e); std::mutex energy_mutex; // complete energy tree requests @@ -73,6 +73,9 @@ class EnergyManager : public Everest::ModuleBase { void enforce_limits(const std::vector& limits); std::vector run_optimizer(types::energy::EnergyFlowRequest request); + + std::condition_variable mainloop_sleep_condvar; + std::mutex mainloop_sleep_mutex; // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 }; diff --git a/modules/EnergyManager/Market.cpp b/modules/EnergyManager/Market.cpp index 8b1ac6b59..215415954 100644 --- a/modules/EnergyManager/Market.cpp +++ b/modules/EnergyManager/Market.cpp @@ -85,7 +85,7 @@ void globals_t::add_timestamps(const types::energy::EnergyFlowRequest& energy_fl } // recurse to all children - for (auto c : energy_flow_request.children) + for (auto& c : energy_flow_request.children) add_timestamps(c); } diff --git a/modules/EvseManager/CMakeLists.txt b/modules/EvseManager/CMakeLists.txt index 19358f472..8441b3d55 100644 --- a/modules/EvseManager/CMakeLists.txt +++ b/modules/EvseManager/CMakeLists.txt @@ -52,6 +52,7 @@ target_sources(${MODULE_NAME} "evse/evse_managerImpl.cpp" "energy_grid/energyImpl.cpp" "token_provider/auth_token_providerImpl.cpp" + "random_delay/uk_random_delayImpl.cpp" ) # ev@c55432ab-152c-45a9-9d2e-7281d50c69c3:v1 diff --git a/modules/EvseManager/Charger.cpp b/modules/EvseManager/Charger.cpp index b3c344ea0..f5d994904 100644 --- a/modules/EvseManager/Charger.cpp +++ b/modules/EvseManager/Charger.cpp @@ -87,6 +87,7 @@ Charger::~Charger() { } void Charger::main_thread() { + // Enable CP output bsp->enable(true); diff --git a/modules/EvseManager/EvseManager.cpp b/modules/EvseManager/EvseManager.cpp index fcfd12e3e..64bd6ac05 100644 --- a/modules/EvseManager/EvseManager.cpp +++ b/modules/EvseManager/EvseManager.cpp @@ -30,6 +30,13 @@ inline static types::authorization::ProvidedIdToken create_autocharge_token(std: void EvseManager::init() { local_three_phases = config.three_phases; + 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) { + EVLOG_info << "UK Smart Charging regulations: enabled random delay with a default of " + << random_delay_max_duration.load().count() << "s."; + } + session_log.setPath(config.session_logging_path); session_log.setMqtt([this](json data) { std::string hlc_log_topic = "everest_api/" + this->info.id + "/var/hlc_log"; @@ -43,6 +50,7 @@ void EvseManager::init() { invoke_init(*p_evse); invoke_init(*p_energy_grid); invoke_init(*p_token_provider); + invoke_init(*p_random_delay); // check if a slac module is connected to the optional requirement slac_enabled = not r_slac.empty(); @@ -733,6 +741,8 @@ void EvseManager::ready() { invoke_ready(*p_evse); invoke_ready(*p_energy_grid); invoke_ready(*p_token_provider); + invoke_ready(*p_random_delay); + if (config.ac_with_soc) { setup_fake_DC_mode(); } else { @@ -856,6 +866,7 @@ void EvseManager::ready() { } void EvseManager::ready_to_start_charging() { + timepoint_ready_for_charging = std::chrono::steady_clock::now(); charger->run(); charger->enable(0); diff --git a/modules/EvseManager/EvseManager.hpp b/modules/EvseManager/EvseManager.hpp index 4ebecbdd8..4e95c9c8e 100644 --- a/modules/EvseManager/EvseManager.hpp +++ b/modules/EvseManager/EvseManager.hpp @@ -14,6 +14,7 @@ #include #include #include +#include // headers for required interface implementations #include @@ -87,6 +88,9 @@ struct Conf { std::string sae_j2847_2_bpt_mode; bool request_zero_power_in_idle; bool external_ready_to_start_charging; + bool uk_smartcharging_random_delay_enable; + int uk_smartcharging_random_delay_max_duration; + bool uk_smartcharging_random_delay_at_any_change; }; class EvseManager : public Everest::ModuleBase { @@ -95,7 +99,8 @@ class EvseManager : public Everest::ModuleBase { EvseManager(const ModuleInfo& info, Everest::MqttProvider& mqtt_provider, Everest::TelemetryProvider& telemetry, std::unique_ptr p_evse, std::unique_ptr p_energy_grid, std::unique_ptr p_token_provider, - std::unique_ptr r_bsp, std::vector> r_ac_rcd, + std::unique_ptr p_random_delay, std::unique_ptr r_bsp, + std::vector> r_ac_rcd, std::vector> r_connector_lock, std::vector> r_powermeter_grid_side, std::vector> r_powermeter_car_side, @@ -108,6 +113,7 @@ class EvseManager : public Everest::ModuleBase { p_evse(std::move(p_evse)), p_energy_grid(std::move(p_energy_grid)), p_token_provider(std::move(p_token_provider)), + p_random_delay(std::move(p_random_delay)), r_bsp(std::move(r_bsp)), r_ac_rcd(std::move(r_ac_rcd)), r_connector_lock(std::move(r_connector_lock)), @@ -124,6 +130,7 @@ class EvseManager : public Everest::ModuleBase { const std::unique_ptr p_evse; const std::unique_ptr p_energy_grid; const std::unique_ptr p_token_provider; + const std::unique_ptr p_random_delay; const std::unique_ptr r_bsp; const std::vector> r_ac_rcd; const std::vector> r_connector_lock; @@ -177,6 +184,12 @@ class EvseManager : public Everest::ModuleBase { std::unique_ptr bsp; std::unique_ptr error_handling; + + std::atomic_bool random_delay_enabled{false}; + std::atomic_bool random_delay_running{false}; + std::chrono::time_point random_delay_end_time; + std::atomic random_delay_max_duration; + std::atomic> timepoint_ready_for_charging; // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 protected: @@ -259,7 +272,6 @@ class EvseManager : public Everest::ModuleBase { static constexpr auto CABLECHECK_CONTACTORS_CLOSE_TIMEOUT{std::chrono::seconds(5)}; std::atomic_bool current_demand_active{false}; - // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 }; diff --git a/modules/EvseManager/energy_grid/energyImpl.cpp b/modules/EvseManager/energy_grid/energyImpl.cpp index 744252475..c89929f32 100644 --- a/modules/EvseManager/energy_grid/energyImpl.cpp +++ b/modules/EvseManager/energy_grid/energyImpl.cpp @@ -23,6 +23,8 @@ static bool voltage_changed(float a, float b) { void energyImpl::init() { + charger_state = Charger::EvseState::Disabled; + // UUID must be unique also beyond this charging station -> will be handled on framework level and above later energy_flow_request.uuid = mod->info.id; energy_flow_request.node_type = types::energy::NodeType::Evse; @@ -90,122 +92,159 @@ void energyImpl::ready() { clear_request_schedules(); // request energy now - request_energy_from_energy_manager(); + request_energy_from_energy_manager(true); // request energy every second std::thread([this] { while (true) { - request_energy_from_energy_manager(); + request_energy_from_energy_manager(false); std::this_thread::sleep_for(std::chrono::seconds(1)); } }).detach(); // request energy at the start and end of a charging session mod->charger->signal_state.connect([this](Charger::EvseState s) { + charger_state = s; if (s == Charger::EvseState::WaitingForAuthentication || s == Charger::EvseState::Finished) { - std::thread request_energy_thread([this]() { request_energy_from_energy_manager(); }); + std::thread request_energy_thread([this]() { request_energy_from_energy_manager(true); }); request_energy_thread.detach(); } }); } -void energyImpl::request_energy_from_energy_manager() { - auto s = mod->charger->get_current_state(); - { - std::lock_guard lock(this->energy_mutex); - clear_import_request_schedule(); - clear_export_request_schedule(); - - // If we need energy, copy local limit schedules to energy_flow_request. - if (s == Charger::EvseState::Charging || s == Charger::EvseState::PrepareCharging || - s == Charger::EvseState::WaitingForEnergy || s == Charger::EvseState::WaitingForAuthentication || - s == Charger::EvseState::ChargingPausedEV || !mod->config.request_zero_power_in_idle) { - - // copy complete external limit schedules - if (mod->getLocalEnergyLimits().schedule_import.has_value() && - !mod->getLocalEnergyLimits().schedule_import.value().empty()) { - energy_flow_request.schedule_import = mod->getLocalEnergyLimits().schedule_import; - } +void energyImpl::request_energy_from_energy_manager(bool priority_request) { + std::lock_guard lock(this->energy_mutex); + clear_import_request_schedule(); + clear_export_request_schedule(); + + // If we need energy, copy local limit schedules to energy_flow_request. + if (charger_state == Charger::EvseState::Charging || charger_state == Charger::EvseState::PrepareCharging || + charger_state == Charger::EvseState::WaitingForEnergy || + charger_state == Charger::EvseState::WaitingForAuthentication || + charger_state == Charger::EvseState::ChargingPausedEV || !mod->config.request_zero_power_in_idle) { + + // copy complete external limit schedules + if (mod->getLocalEnergyLimits().schedule_import.has_value() && + !mod->getLocalEnergyLimits().schedule_import.value().empty()) { + energy_flow_request.schedule_import = mod->getLocalEnergyLimits().schedule_import; + } - // apply our local hardware limits on root side - for (auto& e : energy_flow_request.schedule_import.value()) { - if (!e.limits_to_root.ac_max_current_A.has_value() || - e.limits_to_root.ac_max_current_A.value() > hw_caps.max_current_A_import) { - e.limits_to_root.ac_max_current_A = hw_caps.max_current_A_import; - - // are we in EV pause mode? -> Reduce requested current to minimum just to see when car - // wants to start charging again. The energy manager may pause us externally to reduce to - // zero - if (s == Charger::EvseState::ChargingPausedEV && mod->config.request_zero_power_in_idle) { - e.limits_to_root.ac_max_current_A = hw_caps.min_current_A_import; - } + // apply our local hardware limits on root side + for (auto& e : energy_flow_request.schedule_import.value()) { + if (!e.limits_to_root.ac_max_current_A.has_value() || + e.limits_to_root.ac_max_current_A.value() > hw_caps.max_current_A_import) { + e.limits_to_root.ac_max_current_A = hw_caps.max_current_A_import; + + // are we in EV pause mode? -> Reduce requested current to minimum just to see when car + // wants to start charging again. The energy manager may pause us externally to reduce to + // zero + if (charger_state == Charger::EvseState::ChargingPausedEV && mod->config.request_zero_power_in_idle) { + e.limits_to_root.ac_max_current_A = hw_caps.min_current_A_import; } + } - if (!e.limits_to_root.ac_max_phase_count.has_value() || - e.limits_to_root.ac_max_phase_count.value() > hw_caps.max_phase_count_import) - e.limits_to_root.ac_max_phase_count = hw_caps.max_phase_count_import; + if (!e.limits_to_root.ac_max_phase_count.has_value() || + e.limits_to_root.ac_max_phase_count.value() > hw_caps.max_phase_count_import) + e.limits_to_root.ac_max_phase_count = hw_caps.max_phase_count_import; - // copy remaining hw limits on root side - e.limits_to_root.ac_min_phase_count = hw_caps.min_phase_count_import; - e.limits_to_root.ac_min_current_A = hw_caps.min_current_A_import; - e.limits_to_root.ac_supports_changing_phases_during_charging = - hw_caps.supports_changing_phases_during_charging; - } + // copy remaining hw limits on root side + e.limits_to_root.ac_min_phase_count = hw_caps.min_phase_count_import; + e.limits_to_root.ac_min_current_A = hw_caps.min_current_A_import; + e.limits_to_root.ac_supports_changing_phases_during_charging = + hw_caps.supports_changing_phases_during_charging; + } - if (mod->getLocalEnergyLimits().schedule_export.has_value() && - !mod->getLocalEnergyLimits().schedule_export.value().empty()) { - energy_flow_request.schedule_export = mod->getLocalEnergyLimits().schedule_export; - } + if (mod->getLocalEnergyLimits().schedule_export.has_value() && + !mod->getLocalEnergyLimits().schedule_export.value().empty()) { + energy_flow_request.schedule_export = mod->getLocalEnergyLimits().schedule_export; + } - // apply our local hardware limits on root side - for (auto& e : energy_flow_request.schedule_export.value()) { - if (!e.limits_to_root.ac_max_current_A.has_value() || - e.limits_to_root.ac_max_current_A.value() > hw_caps.max_current_A_export) { - e.limits_to_root.ac_max_current_A = hw_caps.max_current_A_export; - - // are we in EV pause mode? -> Reduce requested current to minimum just to see when car - // wants to start discharging again. The energy manager may pause us externally to reduce to - // zero - if (s == Charger::EvseState::ChargingPausedEV) { - e.limits_to_root.ac_max_current_A = hw_caps.min_current_A_export; - } + // apply our local hardware limits on root side + for (auto& e : energy_flow_request.schedule_export.value()) { + if (!e.limits_to_root.ac_max_current_A.has_value() || + e.limits_to_root.ac_max_current_A.value() > hw_caps.max_current_A_export) { + e.limits_to_root.ac_max_current_A = hw_caps.max_current_A_export; + + // are we in EV pause mode? -> Reduce requested current to minimum just to see when car + // wants to start discharging again. The energy manager may pause us externally to reduce to + // zero + if (charger_state == Charger::EvseState::ChargingPausedEV) { + e.limits_to_root.ac_max_current_A = hw_caps.min_current_A_export; } + } - if (!e.limits_to_root.ac_max_phase_count.has_value() || - e.limits_to_root.ac_max_phase_count.value() > hw_caps.max_phase_count_export) - e.limits_to_root.ac_max_phase_count = hw_caps.max_phase_count_export; + if (!e.limits_to_root.ac_max_phase_count.has_value() || + e.limits_to_root.ac_max_phase_count.value() > hw_caps.max_phase_count_export) + e.limits_to_root.ac_max_phase_count = hw_caps.max_phase_count_export; - // copy remaining hw limits on root side - e.limits_to_root.ac_min_phase_count = hw_caps.min_phase_count_export; - e.limits_to_root.ac_min_current_A = hw_caps.min_current_A_export; - e.limits_to_root.ac_supports_changing_phases_during_charging = - hw_caps.supports_changing_phases_during_charging; - } + // copy remaining hw limits on root side + e.limits_to_root.ac_min_phase_count = hw_caps.min_phase_count_export; + e.limits_to_root.ac_min_current_A = hw_caps.min_current_A_export; + e.limits_to_root.ac_supports_changing_phases_during_charging = + hw_caps.supports_changing_phases_during_charging; + } - if (mod->config.charge_mode == "DC") { - // For DC mode remove amp limit on leave side if any - for (auto& e : energy_flow_request.schedule_import.value()) { - e.limits_to_leaves.ac_max_current_A.reset(); - } - for (auto& e : energy_flow_request.schedule_export.value()) { - e.limits_to_leaves.ac_max_current_A.reset(); - } + if (mod->config.charge_mode == "DC") { + // For DC mode remove amp limit on leave side if any + for (auto& e : energy_flow_request.schedule_import.value()) { + e.limits_to_leaves.ac_max_current_A.reset(); } + for (auto& e : energy_flow_request.schedule_export.value()) { + e.limits_to_leaves.ac_max_current_A.reset(); + } + } + } else { + if (mod->config.charge_mode == "DC") { + // we dont need power at the moment + energy_flow_request.schedule_import.value()[0].limits_to_leaves.total_power_W = 0.; + energy_flow_request.schedule_export.value()[0].limits_to_leaves.total_power_W = 0.; } else { - if (mod->config.charge_mode == "DC") { - // we dont need power at the moment - energy_flow_request.schedule_import.value()[0].limits_to_leaves.total_power_W = 0.; - energy_flow_request.schedule_export.value()[0].limits_to_leaves.total_power_W = 0.; - } else { - energy_flow_request.schedule_import.value()[0].limits_to_leaves.ac_max_current_A = 0.; - energy_flow_request.schedule_export.value()[0].limits_to_leaves.ac_max_current_A = 0.; - } + energy_flow_request.schedule_import.value()[0].limits_to_leaves.ac_max_current_A = 0.; + energy_flow_request.schedule_export.value()[0].limits_to_leaves.ac_max_current_A = 0.; + } + } + + if (priority_request) { + energy_flow_request.priority_request = true; + } else { + energy_flow_request.priority_request = false; + } + + publish_energy_flow_request(energy_flow_request); + // EVLOG_info << "Outgoing request " << energy_flow_request; +} + +static bool almost(float a, float b) { + return a > b - 0.1 and a < b + 0.1; +} + +// This is the decision logic when limits are changing. +bool energyImpl::random_delay_needed(float last_limit, float limit) { + + if (mod->config.uk_smartcharging_random_delay_at_any_change) { + if (not almost(last_limit, limit)) { + return true; + } + } else { + if (almost(last_limit, 0.) and limit > 0.1) { + return true; + } else if (last_limit > 0.1 and almost(limit, 0.)) { + return true; } + } - publish_energy_flow_request(energy_flow_request); - // EVLOG_info << "Outgoing request " << energy_flow_request; + // Are we starting up with a car attached? This will need a random delay as well + if ((charger_state == Charger::EvseState::PrepareCharging or charger_state == Charger::EvseState::Charging or + charger_state == Charger::EvseState::WaitingForAuthentication or + charger_state == Charger::EvseState::WaitingForEnergy) and + std::chrono::steady_clock::now() - mod->timepoint_ready_for_charging.load() < + detect_startup_with_ev_attached_duration) { + last_enforced_limit = 0.; + return true; } + + return false; } void energyImpl::handle_enforce_limits(types::energy::EnforcedLimits& value) { @@ -252,6 +291,56 @@ void energyImpl::handle_enforce_limits(types::energy::EnforcedLimits& value) { } } + auto enforced_limit = limit; + + // check if we need to add a random delay for UK smart charging regs + if (mod->random_delay_enabled) { + + // Are we in a state where a random delay makes sense? + if (not(charger_state == Charger::EvseState::PrepareCharging or + charger_state == Charger::EvseState::Charging or + charger_state == Charger::EvseState::WaitingForAuthentication or + charger_state == Charger::EvseState::WaitingForEnergy)) { + mod->random_delay_running = false; + } + + // Is it running but expired? + if (mod->random_delay_running && std::chrono::steady_clock::now() > mod->random_delay_end_time) { + mod->random_delay_running = false; + } + + // Do we need to start a new random delay? + // Ignore changes of less then 0.1 amps + if (not mod->random_delay_running and random_delay_needed(last_enforced_limit, limit)) { + mod->random_delay_running = true; + auto random_delay_s = std::rand() % mod->random_delay_max_duration.load().count(); + mod->random_delay_end_time = std::chrono::steady_clock::now() + std::chrono::seconds(random_delay_s); + EVLOG_info << "UK Smart Charging regulations: Starting random delay of " << random_delay_s << "s"; + limit_when_random_delay_started = last_enforced_limit; + } + + // If a delay is running, replace the current limit with the stored value + if (mod->random_delay_running) { + // use limit from the time point when the random delay started + limit = limit_when_random_delay_started; + // publish the current random delay timer + auto seconds_left = std::chrono::duration_cast(mod->random_delay_end_time - + std::chrono::steady_clock::now()) + .count(); + mod->p_random_delay->publish_countdown_s(seconds_left); + EVLOG_debug << "Random delay running, " << seconds_left + << "s left. Applying the limit before the random delay (" << limit_when_random_delay_started + << "A) instead of requested limit (" << enforced_limit << "A)"; + if (seconds_left == 0) { + EVLOG_info << "UK Smart Charging regulations: Random delay elapsed."; + } + } else { + mod->p_random_delay->publish_countdown_s(0); + } + } + + last_enforced_limit = enforced_limit; + // update limit at the charger if (limit >= 0) { // import diff --git a/modules/EvseManager/energy_grid/energyImpl.hpp b/modules/EvseManager/energy_grid/energyImpl.hpp index d0e210126..c8617bdef 100644 --- a/modules/EvseManager/energy_grid/energyImpl.hpp +++ b/modules/EvseManager/energy_grid/energyImpl.hpp @@ -48,6 +48,7 @@ class energyImpl : public energyImplBase { // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 std::mutex energy_mutex; + bool random_delay_needed(float last_limit, float limit); // types::energy_price_information::PricePerkWh price_limit; // types::energy::OptimizerTarget optimizer_target; types::energy::EnergyFlowRequest energy_flow_request; @@ -57,8 +58,12 @@ class energyImpl : public energyImplBase { void clear_import_request_schedule(); void clear_export_request_schedule(); void clear_request_schedules(); - void request_energy_from_energy_manager(); + void request_energy_from_energy_manager(bool priority_request); types::evse_board_support::HardwareCapabilities hw_caps; + float last_enforced_limit{0.}; + float limit_when_random_delay_started{0.}; + std::atomic charger_state; + static constexpr std::chrono::seconds detect_startup_with_ev_attached_duration{5}; // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 }; diff --git a/modules/EvseManager/manifest.yaml b/modules/EvseManager/manifest.yaml index d014e5386..83427cd62 100644 --- a/modules/EvseManager/manifest.yaml +++ b/modules/EvseManager/manifest.yaml @@ -195,6 +195,23 @@ config: description: Enable the external ready to start charging signal that delays charging ready until it has been received type: boolean default: false + uk_smartcharging_random_delay_enable: + description: >- + "true: enable random_delays on start up, false: disable random delays on startup." + "They can also be enabled/disabled during runtime on the random_delay implementation." + type: boolean + default: false + uk_smartcharging_random_delay_max_duration: + description: >- + "Start up value for the maximum duration of a random delay." + "Can be modified during runtime on the random_delay implementation." + type: integer + default: 600 + uk_smartcharging_random_delay_at_any_change: + description: >- + "True: use random delays on any current change during charging. False: Only use if current is reduced to zero or increased from zero." + type: boolean + default: true provides: evse: interface: evse_manager @@ -205,6 +222,9 @@ provides: token_provider: description: Provides authtokens for autocharge or plug and charge interface: auth_token_provider + random_delay: + description: Provides control over UK smart charging regulation random delay feature + interface: uk_random_delay requires: bsp: interface: evse_board_support diff --git a/modules/EvseManager/random_delay/uk_random_delayImpl.cpp b/modules/EvseManager/random_delay/uk_random_delayImpl.cpp new file mode 100644 index 000000000..330a3f55d --- /dev/null +++ b/modules/EvseManager/random_delay/uk_random_delayImpl.cpp @@ -0,0 +1,35 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include "uk_random_delayImpl.hpp" + +namespace module { +namespace random_delay { + +void uk_random_delayImpl::init() { +} + +void uk_random_delayImpl::ready() { +} + +void uk_random_delayImpl::handle_enable() { + mod->random_delay_running = false; + mod->random_delay_enabled = true; +} + +void uk_random_delayImpl::handle_disable() { + mod->random_delay_running = false; + mod->random_delay_enabled = false; +} + +void uk_random_delayImpl::handle_cancel() { + mod->random_delay_running = false; +} + +void uk_random_delayImpl::handle_set_duration_s(int& value) { + mod->random_delay_max_duration = std::chrono::seconds(value); +} + +} // namespace random_delay + +} // namespace module diff --git a/modules/EvseManager/random_delay/uk_random_delayImpl.hpp b/modules/EvseManager/random_delay/uk_random_delayImpl.hpp new file mode 100644 index 000000000..4ab28cbd4 --- /dev/null +++ b/modules/EvseManager/random_delay/uk_random_delayImpl.hpp @@ -0,0 +1,64 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#ifndef RANDOM_DELAY_UK_RANDOM_DELAY_IMPL_HPP +#define RANDOM_DELAY_UK_RANDOM_DELAY_IMPL_HPP + +// +// AUTO GENERATED - MARKED REGIONS WILL BE KEPT +// template version 3 +// + +#include + +#include "../EvseManager.hpp" + +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 +// insert your custom include headers here +// ev@75ac1216-19eb-4182-a85c-820f1fc2c091:v1 + +namespace module { +namespace random_delay { + +struct Conf {}; + +class uk_random_delayImpl : public uk_random_delayImplBase { +public: + uk_random_delayImpl() = delete; + uk_random_delayImpl(Everest::ModuleAdapter* ev, const Everest::PtrContainer& mod, Conf& config) : + uk_random_delayImplBase(ev, "random_delay"), mod(mod), config(config){}; + + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + // insert your public definitions here + // ev@8ea32d28-373f-4c90-ae5e-b4fcc74e2a61:v1 + +protected: + // command handler functions (virtual) + virtual void handle_enable() override; + virtual void handle_disable() override; + virtual void handle_cancel() override; + virtual void handle_set_duration_s(int& value) override; + + // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 + // insert your protected definitions here + // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 + +private: + const Everest::PtrContainer& mod; + const Conf& config; + + virtual void init() override; + virtual void ready() override; + + // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 + // insert your private definitions here + // ev@3370e4dd-95f4-47a9-aaec-ea76f34a66c9:v1 +}; + +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 +// insert other definitions here +// ev@3d7da0ad-02c2-493d-9920-0bbbd56b9876:v1 + +} // namespace random_delay +} // namespace module + +#endif // RANDOM_DELAY_UK_RANDOM_DELAY_IMPL_HPP diff --git a/types/energy.yaml b/types/energy.yaml index 2df0946bf..74695ffd3 100644 --- a/types/energy.yaml +++ b/types/energy.yaml @@ -169,6 +169,11 @@ types: Node Type Enum type: string $ref: /energy#/NodeType + priority_request: + description: >- + If set to true, this is a high priority request that needs to be handled now. + Otherwise energymanager may merge multiple requests and address them later. + type: boolean optimizer_target: description: User defined optimizer targets for this evse type: object