From cd1f60aa1e9e8de92602c2fc460fdb83061e8584 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piet=20G=C3=B6mpel?= <37657534+Pietfried@users.noreply.github.com> Date: Wed, 11 Sep 2024 11:46:32 +0200 Subject: [PATCH] Integration of OCPP2.0.1 SmartCharging (#854) * Implementation of OCPP201 smart charging composite schedules within EVerest: * Publishing ChargingSchedules and applying limits on change (by registering set_charging_profiles callback) * Publishing ChargingSchedules and applying limits periodically based on module configuration parameters Changed types::ocpp::ChargingSchedule: * Removed connector property since evse property is sufficient. The term connector (used in OCPP1.6) refers to the term evse in EVerest. Its not required to define evse_id and connector_id as part of a ChargingSchedule * Made stack_level optional * Added required changes due to changed type --------- Signed-off-by: pietfried --- config/config-sil-ocpp.yaml | 5 ++ config/config-sil-ocpp201.yaml | 5 ++ dependencies.yaml | 2 +- modules/OCPP/OCPP.cpp | 2 +- modules/OCPP/conversions.cpp | 1 - modules/OCPP201/OCPP201.cpp | 81 +++++++++++++++++++++++++++++++-- modules/OCPP201/OCPP201.hpp | 15 +++++- modules/OCPP201/conversions.cpp | 35 ++++++++++++++ modules/OCPP201/conversions.hpp | 12 +++++ modules/OCPP201/manifest.yaml | 26 +++++++++++ types/ocpp.yaml | 8 ++-- 11 files changed, 180 insertions(+), 12 deletions(-) diff --git a/config/config-sil-ocpp.yaml b/config/config-sil-ocpp.yaml index 765bcd8014..4a07f52568 100644 --- a/config/config-sil-ocpp.yaml +++ b/config/config-sil-ocpp.yaml @@ -28,6 +28,7 @@ active_modules: ac_hlc_use_5percent: false ac_enforce_hlc: false external_ready_to_start_charging: true + request_zero_power_in_idle: true connections: bsp: - module_id: yeti_driver_1 @@ -57,6 +58,7 @@ active_modules: ac_hlc_use_5percent: false ac_enforce_hlc: false external_ready_to_start_charging: true + request_zero_power_in_idle: true connections: bsp: - module_id: yeti_driver_2 @@ -159,6 +161,9 @@ active_modules: display_message: - module_id: display_message implementation_id: display_message + connector_zero_sink: + - module_id: grid_connection_point + implementation_id: external_limits display_message: module: TerminalCostAndPriceMessage connections: diff --git a/config/config-sil-ocpp201.yaml b/config/config-sil-ocpp201.yaml index 4a975edacf..f9471d17b5 100644 --- a/config/config-sil-ocpp201.yaml +++ b/config/config-sil-ocpp201.yaml @@ -25,6 +25,7 @@ active_modules: ac_hlc_enabled: false ac_hlc_use_5percent: false ac_enforce_hlc: false + request_zero_power_in_idle: true connections: bsp: - module_id: yeti_driver_1 @@ -50,6 +51,7 @@ active_modules: ac_hlc_enabled: false ac_hlc_use_5percent: false ac_enforce_hlc: false + request_zero_power_in_idle: true connections: bsp: - module_id: yeti_driver_2 @@ -125,6 +127,9 @@ active_modules: security: - module_id: evse_security implementation_id: main + connector_zero_sink: + - module_id: grid_connection_point + implementation_id: external_limits persistent_store: module: PersistentStore evse_security: diff --git a/dependencies.yaml b/dependencies.yaml index a3ba48800d..bb8c8952e4 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -60,7 +60,7 @@ libevse-security: # OCPP libocpp: git: https://github.com/EVerest/libocpp.git - git_tag: 3fef09231f0033a4af5d0af97d35851ecc00d399 + git_tag: 4a62b490fb89efd9c2f36d21d7949ee273d2c8b9 cmake_condition: "EVEREST_DEPENDENCY_ENABLED_LIBOCPP" # Josev Josev: diff --git a/modules/OCPP/OCPP.cpp b/modules/OCPP/OCPP.cpp index 1b4a38f7df..4457a18d69 100644 --- a/modules/OCPP/OCPP.cpp +++ b/modules/OCPP/OCPP.cpp @@ -122,7 +122,7 @@ void OCPP::publish_charging_schedules( types::ocpp::ChargingSchedules schedules; for (const auto& charging_schedule : charging_schedules) { types::ocpp::ChargingSchedule sch = conversions::to_charging_schedule(charging_schedule.second); - sch.connector = charging_schedule.first; + sch.evse = charging_schedule.first; schedules.schedules.emplace_back(std::move(sch)); } this->p_ocpp_generic->publish_charging_schedules(schedules); diff --git a/modules/OCPP/conversions.cpp b/modules/OCPP/conversions.cpp index 9fc2154be0..283323c435 100644 --- a/modules/OCPP/conversions.cpp +++ b/modules/OCPP/conversions.cpp @@ -418,7 +418,6 @@ types::ocpp::ChargingSchedule to_charging_schedule(const ocpp::v16::EnhancedChar 0, ocpp::v16::conversions::charging_rate_unit_to_string(schedule.chargingRateUnit), {}, - std::nullopt, schedule.duration, std::nullopt, schedule.minChargingRate}; diff --git a/modules/OCPP201/OCPP201.cpp b/modules/OCPP201/OCPP201.cpp index d4cfafd5ac..a40b4c26a4 100644 --- a/modules/OCPP201/OCPP201.cpp +++ b/modules/OCPP201/OCPP201.cpp @@ -235,6 +235,16 @@ std::optional get_authorized_id_token(const types::evse_man return std::nullopt; } +ocpp::v201::ChargingRateUnitEnum get_unit_or_default(const std::string& unit_string) { + try { + return ocpp::v201::conversions::string_to_charging_rate_unit_enum(unit_string); + } catch (const std::out_of_range& e) { + EVLOG_warning << "RequestCompositeScheduleUnit configured incorrectly with: " << unit_string + << ". Defaulting to using Amps."; + return ocpp::v201::ChargingRateUnitEnum::A; + } +} + bool OCPP201::all_evse_ready() { for (auto const& [evse, ready] : this->evse_ready_map) { if (!ready) { @@ -522,11 +532,19 @@ void OCPP201::ready() { this->p_ocpp_generic->publish_security_event(event); }; - callbacks.set_charging_profiles_callback = [this]() { - // TODO: implement once charging profiles are available in libocpp - return; + const auto composite_schedule_unit = get_unit_or_default(this->config.RequestCompositeScheduleUnit); + + // this callback publishes the schedules within EVerest and applies the schedules for the individual evse_manager(s) + // and the connector_zero_sink + const auto charging_schedules_callback = [this, composite_schedule_unit]() { + const auto composite_schedules = this->charge_point->get_all_composite_schedules( + this->config.RequestCompositeScheduleDurationS, composite_schedule_unit); + this->publish_charging_schedules(composite_schedules); + this->set_external_limits(composite_schedules); }; + callbacks.set_charging_profiles_callback = charging_schedules_callback; + const auto sql_init_path = this->ocpp_share_path / SQL_CORE_MIGRATIONS; std::map evse_connector_structure = this->get_connector_structure(); @@ -535,6 +553,14 @@ void OCPP201::ready() { device_model_config_path, this->ocpp_share_path.string(), this->config.CoreDatabasePath, sql_init_path.string(), this->config.MessageLogPath, std::make_shared(*this->r_security), callbacks); + // publish charging schedules at least once on startup + charging_schedules_callback(); + + if (this->config.CompositeScheduleIntervalS > 0) { + this->charging_schedules_timer.interval(charging_schedules_callback, + std::chrono::seconds(this->config.CompositeScheduleIntervalS)); + } + const auto ev_connection_timeout_request_value_response = this->charge_point->request_value( ocpp::v201::Component{"TxCtrlr"}, ocpp::v201::Variable{"EVConnectionTimeOut"}, ocpp::v201::AttributeEnum::Actual); @@ -1018,4 +1044,53 @@ void OCPP201::process_deauthorized(const int32_t evse_id, const int32_t connecto this->process_tx_event_effect(evse_id, tx_event_effect, session_event); } +void OCPP201::publish_charging_schedules(const std::vector& composite_schedules) { + const auto everest_schedules = conversions::to_everest_charging_schedules(composite_schedules); + this->p_ocpp_generic->publish_charging_schedules(everest_schedules); +} + +void OCPP201::set_external_limits(const std::vector& composite_schedules) { + const auto start_time = ocpp::DateTime(); + + // iterate over all schedules reported by the libocpp to create ExternalLimits + // for each connector + + for (const auto& composite_schedule : composite_schedules) { + types::energy::ExternalLimits limits; + std::vector schedule_import; + + for (const auto& period : composite_schedule.chargingSchedulePeriod) { + types::energy::ScheduleReqEntry schedule_req_entry; + types::energy::LimitsReq limits_req; + const auto timestamp = start_time.to_time_point() + std::chrono::seconds(period.startPeriod); + schedule_req_entry.timestamp = ocpp::DateTime(timestamp).to_rfc3339(); + if (composite_schedule.chargingRateUnit == ocpp::v201::ChargingRateUnitEnum::A) { + limits_req.ac_max_current_A = period.limit; + if (period.numberPhases.has_value()) { + limits_req.ac_max_phase_count = period.numberPhases.value(); + } + } else { + limits_req.total_power_W = period.limit; + } + schedule_req_entry.limits_to_leaves = limits_req; + schedule_import.push_back(schedule_req_entry); + } + limits.schedule_import = schedule_import; + + if (composite_schedule.evseId == 0) { + if (!this->r_connector_zero_sink.empty()) { + EVLOG_debug << "OCPP sets the following external limits for evse 0: \n" << limits; + this->r_connector_zero_sink.at(0)->call_set_external_limits(limits); + } else { + EVLOG_debug << "OCPP cannot set external limits for evse 0. No " + "sink is configured."; + } + } else { + EVLOG_debug << "OCPP sets the following external limits for evse " << composite_schedule.evseId << ": \n" + << limits; + this->r_evse_manager.at(composite_schedule.evseId - 1)->call_set_external_limits(limits); + } + } +} + } // namespace module diff --git a/modules/OCPP201/OCPP201.hpp b/modules/OCPP201/OCPP201.hpp index 05394a2534..0174378d13 100644 --- a/modules/OCPP201/OCPP201.hpp +++ b/modules/OCPP201/OCPP201.hpp @@ -21,6 +21,7 @@ #include #include #include +#include #include #include @@ -42,6 +43,9 @@ struct Conf { std::string DeviceModelConfigPath; bool EnableExternalWebsocketControl; int MessageQueueResumeDelay; + int CompositeScheduleIntervalS; + int RequestCompositeScheduleDurationS; + std::string RequestCompositeScheduleUnit; }; class OCPP201 : public Everest::ModuleBase { @@ -54,7 +58,7 @@ class OCPP201 : public Everest::ModuleBase { std::vector> r_evse_manager, std::unique_ptr r_system, std::unique_ptr r_security, std::vector> r_data_transfer, std::unique_ptr r_auth, - Conf& config) : + std::vector> r_connector_zero_sink, Conf& config) : ModuleBase(info), mqtt(mqtt_provider), p_main(std::move(p_main)), @@ -67,6 +71,7 @@ class OCPP201 : public Everest::ModuleBase { r_security(std::move(r_security)), r_data_transfer(std::move(r_data_transfer)), r_auth(std::move(r_auth)), + r_connector_zero_sink(std::move(r_connector_zero_sink)), config(config){}; Everest::MqttProvider& mqtt; @@ -80,6 +85,7 @@ class OCPP201 : public Everest::ModuleBase { const std::unique_ptr r_security; const std::vector> r_data_transfer; const std::unique_ptr r_auth; + const std::vector> r_connector_zero_sink; const Conf& config; // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 @@ -100,6 +106,7 @@ class OCPP201 : public Everest::ModuleBase { // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 // insert your private definitions here std::unique_ptr transaction_handler; + Everest::SteadyTimer charging_schedules_timer; std::filesystem::path ocpp_share_path; @@ -139,6 +146,12 @@ class OCPP201 : public Everest::ModuleBase { const types::evse_manager::SessionEvent& session_event); void process_deauthorized(const int32_t evse_id, const int32_t connector_id, const types::evse_manager::SessionEvent& session_event); + + /// \brief This function publishes the given \p composite_schedules via the ocpp interface + void publish_charging_schedules(const std::vector& composite_schedules); + + /// \brief This function applies given \p composite_schedules for each evse_manager and the connector_zero_sink + void set_external_limits(const std::vector& composite_schedules); // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 }; diff --git a/modules/OCPP201/conversions.cpp b/modules/OCPP201/conversions.cpp index 95a22d3ce3..94f1b7ec6e 100644 --- a/modules/OCPP201/conversions.cpp +++ b/modules/OCPP201/conversions.cpp @@ -1252,5 +1252,40 @@ to_everest_set_variable_status_enum_type(const ocpp::v201::SetVariableStatusEnum } } +types::ocpp::ChargingSchedules +to_everest_charging_schedules(const std::vector& composite_schedules) { + types::ocpp::ChargingSchedules charging_schedules; + for (const auto& composite_schedule : composite_schedules) { + charging_schedules.schedules.push_back(conversions::to_everest_charging_schedule(composite_schedule)); + } + return charging_schedules; +} + +types::ocpp::ChargingSchedule to_everest_charging_schedule(const ocpp::v201::CompositeSchedule& composite_schedule) { + types::ocpp::ChargingSchedule charging_schedule; + charging_schedule.evse = composite_schedule.evseId; + charging_schedule.charging_rate_unit = + ocpp::v201::conversions::charging_rate_unit_enum_to_string(composite_schedule.chargingRateUnit); + charging_schedule.evse = composite_schedule.evseId; + charging_schedule.duration = composite_schedule.duration; + charging_schedule.start_schedule = composite_schedule.scheduleStart.to_rfc3339(); + // min_charging_rate is not given as part of a OCPP2.0.1 composite schedule + for (const auto& charging_schedule_period : composite_schedule.chargingSchedulePeriod) { + charging_schedule.charging_schedule_period.push_back( + to_everest_charging_schedule_period(charging_schedule_period)); + } + return charging_schedule; +} + +types::ocpp::ChargingSchedulePeriod +to_everest_charging_schedule_period(const ocpp::v201::ChargingSchedulePeriod& period) { + types::ocpp::ChargingSchedulePeriod _period; + _period.start_period = period.startPeriod; + _period.limit = period.limit; + _period.number_phases = period.numberPhases; + _period.phase_to_use = period.phaseToUse; + return _period; +} + } // namespace conversions } // namespace module diff --git a/modules/OCPP201/conversions.hpp b/modules/OCPP201/conversions.hpp index 920e2f2872..2504545409 100644 --- a/modules/OCPP201/conversions.hpp +++ b/modules/OCPP201/conversions.hpp @@ -226,6 +226,18 @@ to_everest_get_variable_status_enum_type(const ocpp::v201::GetVariableStatusEnum types::ocpp::SetVariableStatusEnumType to_everest_set_variable_status_enum_type(const ocpp::v201::SetVariableStatusEnum set_variable_status); +/// \brief Converts a given vector of ocpp::v201::CompositeSchedule \p composite_schedules to a +/// types::ocpp::ChargingSchedules +types::ocpp::ChargingSchedules +to_everest_charging_schedules(const std::vector& composite_schedules); + +/// \brief Converts a given ocpp::v201::CompositeSchedule \p composite_schedule to a types::ocpp::ChargingSchedule +types::ocpp::ChargingSchedule to_everest_charging_schedule(const ocpp::v201::CompositeSchedule& composite_schedule); + +/// \brief Converst a given ocpp::v201::ChargingSchedulePeriod \p period to a types::ocpp::ChargingSchedulePeriod +types::ocpp::ChargingSchedulePeriod +to_everest_charging_schedule_period(const ocpp::v201::ChargingSchedulePeriod& period); + } // namespace conversions } // namespace module diff --git a/modules/OCPP201/manifest.yaml b/modules/OCPP201/manifest.yaml index 18493e090c..59c3a2a1d4 100644 --- a/modules/OCPP201/manifest.yaml +++ b/modules/OCPP201/manifest.yaml @@ -28,6 +28,28 @@ config: description: Time (seconds) to delay resuming the message queue after reconnecting type: integer default: 0 + CompositeScheduleIntervalS: + description: + Interval in seconds in which composite schedules are received from libocpp + are be published over MQTT and signalled to connected modules. If the value + is set to 0, composite schedules are only published when changed by CSMS + type: integer + default: 30 + RequestCompositeScheduleDurationS: + description: >- + Time (seconds) for which composite schedules are requested. + Schedules are requested from now until now + RequestCompositeScheduleDurationS + type: integer + default: 600 + RequestCompositeScheduleUnit: + description: >- + Unit in which composite schedules are requested and shared within EVerest. It is recommended to use + Amps for AC and Watts for DC charging stations. + Allowed values: + - 'A' for Amps + . 'W' for Watts + type: string + default: 'A' provides: main: description: This is a OCPP 2.0.1 charge point @@ -65,6 +87,10 @@ requires: interface: auth min_connections: 1 max_connections: 1 + connector_zero_sink: + interface: external_energy_limits + min_connections: 0 + max_connections: 1 enable_external_mqtt: true enable_global_errors: true metadata: diff --git a/types/ocpp.yaml b/types/ocpp.yaml index 0d11ab615c..9268296439 100644 --- a/types/ocpp.yaml +++ b/types/ocpp.yaml @@ -22,7 +22,6 @@ types: required: - start_period - limit - - stack_level properties: start_period: type: integer @@ -32,12 +31,14 @@ types: type: integer stack_level: type: integer + phase_to_use: + type: integer ChargingSchedule: description: >- Element providing information on an OCPP charging schedule. type: object required: - - connector + - evse - charging_rate_unit - charging_schedule_period properties: @@ -45,9 +46,6 @@ types: description: The OCPP 2.0.1 EVSE ID (not used in OCPP 1.6). type: integer minimum: 0 - connector: - type: integer - minimum: 0 charging_rate_unit: type: string charging_schedule_period: