diff --git a/config/config-sil-ocpp.yaml b/config/config-sil-ocpp.yaml index f57108785..bf3d22fe5 100644 --- a/config/config-sil-ocpp.yaml +++ b/config/config-sil-ocpp.yaml @@ -28,6 +28,7 @@ active_modules: ac_hlc_enabled: false ac_hlc_use_5percent: false ac_enforce_hlc: false + external_ready_to_start_charging: true connections: bsp: - module_id: yeti_driver_1 @@ -56,6 +57,7 @@ active_modules: ac_hlc_enabled: false ac_hlc_use_5percent: false ac_enforce_hlc: false + external_ready_to_start_charging: true connections: bsp: - module_id: yeti_driver_2 diff --git a/interfaces/evse_manager.yaml b/interfaces/evse_manager.yaml index ecc8ae5a8..c72275355 100644 --- a/interfaces/evse_manager.yaml +++ b/interfaces/evse_manager.yaml @@ -136,6 +136,13 @@ cmds: description: The response raw exi stream and the status from the CSMS system type: object $ref: /iso15118_charger#/Response_Exi_Stream_Status + external_ready_to_start_charging: + description: >- + There are situations where another module needs to do some initialization after evse manager is in principle ready to start charging. + This command can be used (optimally in combination with a configuration option) to delay charging ready until the external module is done with its initialization + result: + description: Returns true if the signal was used by the evse manager implementation + type: boolean vars: session_event: description: Emits all events related to sessions @@ -179,6 +186,11 @@ vars: description: Enforced limits for this node (coming from the EnergyManager) type: object $ref: /energy#/EnforcedLimits + waiting_for_external_ready: + description: >- + Signals that the EVSE Manager is in principle ready to start charging, + but delays sending its ready signal waiting for the external_ready_to_start_charging command. + type: boolean ready: description: Signals that the EVSE Manager is ready to start charging type: boolean diff --git a/modules/EvseManager/EvseManager.cpp b/modules/EvseManager/EvseManager.cpp index ad1fb380f..d65428d23 100644 --- a/modules/EvseManager/EvseManager.cpp +++ b/modules/EvseManager/EvseManager.cpp @@ -846,6 +846,14 @@ void EvseManager::ready() { // start with a limit of 0 amps. We will get a budget from EnergyManager that is locally limited by hw // caps. charger->setMaxCurrent(0.0F, date::utc_clock::now() + std::chrono::seconds(10)); + this->p_evse->publish_waiting_for_external_ready(config.external_ready_to_start_charging); + if (!config.external_ready_to_start_charging) { + // immediately ready, otherwise delay until we get the external signal + this->ready_to_start_charging(); + } +} + +void EvseManager::ready_to_start_charging() { charger->run(); charger->enable(0); diff --git a/modules/EvseManager/EvseManager.hpp b/modules/EvseManager/EvseManager.hpp index 2f24669d4..0206ac09c 100644 --- a/modules/EvseManager/EvseManager.hpp +++ b/modules/EvseManager/EvseManager.hpp @@ -83,6 +83,7 @@ struct Conf { bool sae_j2847_2_bpt_enabled; std::string sae_j2847_2_bpt_mode; bool request_zero_power_in_idle; + bool external_ready_to_start_charging; }; class EvseManager : public Everest::ModuleBase { @@ -163,6 +164,8 @@ class EvseManager : public Everest::ModuleBase { std::string selected_protocol = "Unknown"; std::atomic_bool sae_bidi_active{false}; + + void ready_to_start_charging(); // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 protected: diff --git a/modules/EvseManager/evse/evse_managerImpl.cpp b/modules/EvseManager/evse/evse_managerImpl.cpp index dce4c445d..8da41e77e 100644 --- a/modules/EvseManager/evse/evse_managerImpl.cpp +++ b/modules/EvseManager/evse/evse_managerImpl.cpp @@ -391,5 +391,19 @@ void evse_managerImpl::handle_set_get_certificate_response( mod->r_hlc[0]->call_certificate_response(certificate_reponse); } +bool evse_managerImpl::handle_external_ready_to_start_charging() { + if (mod->config.external_ready_to_start_charging) { + EVLOG_info << "Received external ready to start charging command."; + mod->ready_to_start_charging(); + return true; + } else { + EVLOG_warning + << "Ignoring external ready to start charging command, this could be a configuration issue. Please check " + "if 'external_ready_to_start_charging' is set to true if you want to use this feature."; + } + + return false; +} + } // namespace evse } // namespace module diff --git a/modules/EvseManager/evse/evse_managerImpl.hpp b/modules/EvseManager/evse/evse_managerImpl.hpp index 205519037..a9af2dfa7 100644 --- a/modules/EvseManager/evse/evse_managerImpl.hpp +++ b/modules/EvseManager/evse/evse_managerImpl.hpp @@ -52,6 +52,7 @@ class evse_managerImpl : public evse_managerImplBase { handle_switch_three_phases_while_charging(bool& three_phases) override; virtual void handle_set_get_certificate_response( types::iso15118_charger::Response_Exi_Stream_Status& certificate_response) override; + virtual bool handle_external_ready_to_start_charging() override; // ev@d2d1847a-7b88-41dd-ad07-92785f06f5c4:v1 // insert your protected definitions here diff --git a/modules/EvseManager/manifest.yaml b/modules/EvseManager/manifest.yaml index a96856cc0..b694eceaf 100644 --- a/modules/EvseManager/manifest.yaml +++ b/modules/EvseManager/manifest.yaml @@ -202,6 +202,10 @@ config: "EvseManager does not need to wait for energy from the energy manager after plug in." type: boolean default: false + external_ready_to_start_charging: + description: Enable the external ready to start charging signal that delays charging ready until it has been received + type: boolean + default: false provides: evse: interface: evse_manager diff --git a/modules/OCPP/OCPP.cpp b/modules/OCPP/OCPP.cpp index 6549327af..a7c43583a 100644 --- a/modules/OCPP/OCPP.cpp +++ b/modules/OCPP/OCPP.cpp @@ -141,6 +141,139 @@ void OCPP::publish_charging_schedules(const std::mapp_ocpp_generic->publish_charging_schedules(j); } +void OCPP::process_session_event(int32_t evse_id, const types::evse_manager::SessionEvent& session_event) { + auto event = types::evse_manager::session_event_enum_to_string(session_event.event); + + auto everest_connector_id = session_event.connector_id.value_or(1); + auto ocpp_connector_id = this->evse_connector_map[evse_id][everest_connector_id]; + + if (event == "Enabled") { + this->charge_point->on_enabled(evse_id); + } else if (event == "Disabled") { + EVLOG_debug << "EVSE#" << evse_id << ": " + << "Received Disabled"; + this->charge_point->on_disabled(evse_id); + } else if (event == "TransactionStarted") { + EVLOG_info << "EVSE#" << evse_id << ": " + << "Received TransactionStarted"; + const auto transaction_started = session_event.transaction_started.value(); + + const auto timestamp = ocpp::DateTime(transaction_started.timestamp); + const auto energy_Wh_import = transaction_started.meter_value.energy_Wh_import.total; + const auto session_id = session_event.uuid; + const auto id_token = transaction_started.id_tag.id_token; + const auto signed_meter_value = transaction_started.signed_meter_value; + std::optional reservation_id_opt = std::nullopt; + if (transaction_started.reservation_id) { + reservation_id_opt.emplace(transaction_started.reservation_id.value()); + } + this->charge_point->on_transaction_started(ocpp_connector_id, session_event.uuid, id_token, energy_Wh_import, + reservation_id_opt, timestamp, signed_meter_value); + } else if (event == "ChargingPausedEV") { + EVLOG_debug << "Connector#" << ocpp_connector_id << ": " + << "Received ChargingPausedEV"; + this->charge_point->on_suspend_charging_ev(ocpp_connector_id); + } else if (event == "ChargingPausedEVSE" or event == "WaitingForEnergy") { + EVLOG_debug << "Connector#" << ocpp_connector_id << ": " + << "Received ChargingPausedEVSE"; + this->charge_point->on_suspend_charging_evse(ocpp_connector_id); + } else if (event == "ChargingStarted" || event == "ChargingResumed") { + EVLOG_debug << "Connector#" << ocpp_connector_id << ": " + << "Received ChargingResumed"; + this->charge_point->on_resume_charging(ocpp_connector_id); + } else if (event == "TransactionFinished") { + EVLOG_debug << "Connector#" << ocpp_connector_id << ": " + << "Received TransactionFinished"; + const auto transaction_finished = session_event.transaction_finished.value(); + const auto timestamp = ocpp::DateTime(transaction_finished.timestamp); + const auto energy_Wh_import = transaction_finished.meter_value.energy_Wh_import.total; + const auto reason = ocpp::v16::conversions::string_to_reason( + types::evse_manager::stop_transaction_reason_to_string(transaction_finished.reason.value())); + const auto signed_meter_value = transaction_finished.signed_meter_value; + std::optional> id_tag_opt = std::nullopt; + if (transaction_finished.id_tag) { + id_tag_opt.emplace(ocpp::CiString<20>(transaction_finished.id_tag.value())); + } + this->charge_point->on_transaction_stopped(ocpp_connector_id, session_event.uuid, reason, timestamp, + energy_Wh_import, id_tag_opt, signed_meter_value); + // always triggered by libocpp + } else if (event == "SessionStarted") { + EVLOG_info << "Connector#" << ocpp_connector_id << ": " + << "Received SessionStarted"; + // ev side disconnect + auto session_started = session_event.session_started.value(); + this->charge_point->on_session_started( + ocpp_connector_id, session_event.uuid, + types::evse_manager::start_session_reason_to_string(session_started.reason), session_started.logging_path); + } else if (event == "SessionFinished") { + EVLOG_debug << "Connector#" << ocpp_connector_id << ": " + << "Received SessionFinished"; + // ev side disconnect + this->charge_point->on_session_stopped(ocpp_connector_id, session_event.uuid); + } else if (event == "Error") { + EVLOG_debug << "Connector#" << ocpp_connector_id << ": " + << "Received Error"; + const auto evse_error = types::evse_manager::error_enum_to_string(session_event.error.value().error_code); + ocpp::v16::ChargePointErrorCode ocpp_error_code = get_ocpp_error_code(evse_error); + this->charge_point->on_error(ocpp_connector_id, ocpp_error_code); + } else if (event == "AllErrorsCleared") { + this->charge_point->on_fault(ocpp_connector_id, ocpp::v16::ChargePointErrorCode::NoError); + } else if (event == "PermanentFault") { + const auto evse_error = types::evse_manager::error_enum_to_string(session_event.error.value().error_code); + ocpp::v16::ChargePointErrorCode ocpp_error_code = get_ocpp_error_code(evse_error); + this->charge_point->on_fault(ocpp_connector_id, ocpp_error_code); + } else if (event == "ReservationStart") { + this->charge_point->on_reservation_start(ocpp_connector_id); + } else if (event == "ReservationEnd") { + this->charge_point->on_reservation_end(ocpp_connector_id); + } else if (event == "ReservationAuthtokenMismatch") { + } else if (event == "PluginTimeout") { + this->charge_point->on_plugin_timeout(ocpp_connector_id); + } +} + +void OCPP::init_evse_subscriptions() { + int32_t evse_id = 1; + for (auto& evse : this->r_evse_manager) { + evse->subscribe_powermeter([this, evse_id](types::powermeter::Powermeter powermeter) { + json powermeter_json = powermeter; + this->charge_point->on_meter_values(evse_id, powermeter_json); // + }); + + evse->subscribe_limits([this, evse_id](types::evse_manager::Limits limits) { + double max_current = limits.max_current; + this->charge_point->on_max_current_offered(evse_id, max_current); + }); + + evse->subscribe_session_event([this, evse_id](types::evse_manager::SessionEvent session_event) { + if (this->ocpp_stopped) { + // dont call any on handler in case ocpp is stopped + 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; + std::scoped_lock lock(this->session_event_mutex); + this->session_event_queue[evse_id].push(session_event); + return; + } + + this->process_session_event(evse_id, session_event); + }); + + evse->subscribe_iso15118_certificate_request( + [this, evse_id](types::iso15118_charger::Request_Exi_Stream_Schema request) { + this->charge_point->data_transfer_pnc_get_15118_ev_certificate( + evse_id, request.exiRequest, request.iso15118SchemaVersion, + ocpp::v201::conversions::string_to_certificate_action_enum( + types::iso15118_charger::certificate_action_enum_to_string(request.certificateAction))); + }); + + evse_id++; + } +} + void OCPP::init_evse_connector_map() { int32_t ocpp_connector_id = 1; // this represents the OCPP connector id int32_t evse_id = 1; // this represents the evse id of EVerests evse manager @@ -212,9 +345,23 @@ void OCPP::init() { this->init_evse_ready_map(); for (size_t evse_id = 1; evse_id <= this->r_evse_manager.size(); evse_id++) { + this->r_evse_manager.at(evse_id - 1)->subscribe_waiting_for_external_ready([this, evse_id](bool ready) { + std::lock_guard lk(this->evse_ready_mutex); + if (ready) { + this->evse_ready_map[evse_id] = true; + this->evse_ready_cv.notify_one(); + } + }); + + // also use the the ready signal, TODO(kai): maybe warn about it's usage here` this->r_evse_manager.at(evse_id - 1)->subscribe_ready([this, evse_id](bool ready) { std::lock_guard lk(this->evse_ready_mutex); if (ready) { + if (!this->evse_ready_map[evse_id]) { + EVLOG_error << "Received EVSE ready without receiving waiting_for_external_ready first, this is " + "probably a bug in your evse_manager implementation / configuration. evse_id: " + << evse_id; + } this->evse_ready_map[evse_id] = true; this->evse_ready_cv.notify_one(); } @@ -283,6 +430,8 @@ void OCPP::init() { std::make_shared(*this->r_security)); this->charge_point->set_message_queue_resume_delay(std::chrono::seconds(config.MessageQueueResumeDelay)); + + this->init_evse_subscriptions(); // initialize EvseManager subscriptions as early as possible } void OCPP::ready() { @@ -443,6 +592,7 @@ void OCPP::ready() { this->r_system->call_allow_firmware_installation(); }); + // FIXME(kai): subscriptions should be as early as possible this->r_system->subscribe_log_status([this](types::system::LogStatus log_status) { this->charge_point->on_log_status_notification(log_status.request_id, types::system::log_status_enum_to_string(log_status.log_status)); @@ -568,134 +718,33 @@ void OCPP::ready() { }); } - int32_t evse_id = 1; - for (auto& evse : this->r_evse_manager) { - evse->subscribe_powermeter([this, evse_id](types::powermeter::Powermeter powermeter) { - json powermeter_json = powermeter; - this->charge_point->on_meter_values(evse_id, powermeter_json); // - }); - - evse->subscribe_limits([this, evse_id](types::evse_manager::Limits limits) { - double max_current = limits.max_current; - this->charge_point->on_max_current_offered(evse_id, max_current); - }); - - evse->subscribe_session_event([this, evse_id](types::evse_manager::SessionEvent session_event) { - if (this->ocpp_stopped) { - // dont call any on handler in case ocpp is stopped - return; - } + std::unique_lock lk(this->evse_ready_mutex); + while (!this->all_evse_ready()) { + this->evse_ready_cv.wait(lk); + } - auto event = types::evse_manager::session_event_enum_to_string(session_event.event); - - auto everest_connector_id = session_event.connector_id.value_or(1); - auto ocpp_connector_id = this->evse_connector_map[evse_id][everest_connector_id]; - - if (event == "Enabled") { - this->charge_point->on_enabled(evse_id); - } else if (event == "Disabled") { - EVLOG_debug << "EVSE#" << evse_id << ": " - << "Received Disabled"; - this->charge_point->on_disabled(evse_id); - } else if (event == "TransactionStarted") { - EVLOG_debug << "EVSE#" << evse_id << ": " - << "Received TransactionStarted"; - const auto transaction_started = session_event.transaction_started.value(); - - const auto timestamp = ocpp::DateTime(transaction_started.timestamp); - const auto energy_Wh_import = transaction_started.meter_value.energy_Wh_import.total; - const auto session_id = session_event.uuid; - const auto id_token = transaction_started.id_tag.id_token; - const auto signed_meter_value = transaction_started.signed_meter_value; - std::optional reservation_id_opt = std::nullopt; - if (transaction_started.reservation_id) { - reservation_id_opt.emplace(transaction_started.reservation_id.value()); - } - this->charge_point->on_transaction_started(ocpp_connector_id, session_event.uuid, id_token, - energy_Wh_import, reservation_id_opt, timestamp, - signed_meter_value); - } else if (event == "ChargingPausedEV") { - EVLOG_debug << "Connector#" << ocpp_connector_id << ": " - << "Received ChargingPausedEV"; - this->charge_point->on_suspend_charging_ev(ocpp_connector_id); - } else if (event == "ChargingPausedEVSE" or event == "WaitingForEnergy") { - EVLOG_debug << "Connector#" << ocpp_connector_id << ": " - << "Received ChargingPausedEVSE"; - this->charge_point->on_suspend_charging_evse(ocpp_connector_id); - } else if (event == "ChargingStarted" || event == "ChargingResumed") { - EVLOG_debug << "Connector#" << ocpp_connector_id << ": " - << "Received ChargingResumed"; - this->charge_point->on_resume_charging(ocpp_connector_id); - } else if (event == "TransactionFinished") { - EVLOG_debug << "Connector#" << ocpp_connector_id << ": " - << "Received TransactionFinished"; - const auto transaction_finished = session_event.transaction_finished.value(); - const auto timestamp = ocpp::DateTime(transaction_finished.timestamp); - const auto energy_Wh_import = transaction_finished.meter_value.energy_Wh_import.total; - const auto reason = ocpp::v16::conversions::string_to_reason( - types::evse_manager::stop_transaction_reason_to_string(transaction_finished.reason.value())); - const auto signed_meter_value = transaction_finished.signed_meter_value; - std::optional> id_tag_opt = std::nullopt; - if (transaction_finished.id_tag) { - id_tag_opt.emplace(ocpp::CiString<20>(transaction_finished.id_tag.value())); - } - this->charge_point->on_transaction_stopped(ocpp_connector_id, session_event.uuid, reason, timestamp, - energy_Wh_import, id_tag_opt, signed_meter_value); - // always triggered by libocpp - } else if (event == "SessionStarted") { - EVLOG_debug << "Connector#" << ocpp_connector_id << ": " - << "Received SessionStarted"; - // ev side disconnect - auto session_started = session_event.session_started.value(); - this->charge_point->on_session_started( - ocpp_connector_id, session_event.uuid, - types::evse_manager::start_session_reason_to_string(session_started.reason), - session_started.logging_path); - } else if (event == "SessionFinished") { - EVLOG_debug << "Connector#" << ocpp_connector_id << ": " - << "Received SessionFinished"; - // ev side disconnect - this->charge_point->on_session_stopped(ocpp_connector_id, session_event.uuid); - } else if (event == "Error") { - EVLOG_debug << "Connector#" << ocpp_connector_id << ": " - << "Received Error"; - const auto evse_error = - types::evse_manager::error_enum_to_string(session_event.error.value().error_code); - ocpp::v16::ChargePointErrorCode ocpp_error_code = get_ocpp_error_code(evse_error); - this->charge_point->on_error(ocpp_connector_id, ocpp_error_code); - } else if (event == "AllErrorsCleared") { - this->charge_point->on_fault(ocpp_connector_id, ocpp::v16::ChargePointErrorCode::NoError); - } else if (event == "PermanentFault") { - const auto evse_error = - types::evse_manager::error_enum_to_string(session_event.error.value().error_code); - ocpp::v16::ChargePointErrorCode ocpp_error_code = get_ocpp_error_code(evse_error); - this->charge_point->on_fault(ocpp_connector_id, ocpp_error_code); - } else if (event == "ReservationStart") { - this->charge_point->on_reservation_start(ocpp_connector_id); - } else if (event == "ReservationEnd") { - this->charge_point->on_reservation_end(ocpp_connector_id); - } else if (event == "ReservationAuthtokenMismatch") { - } else if (event == "PluginTimeout") { - this->charge_point->on_plugin_timeout(ocpp_connector_id); + if (this->charge_point->start()) { + // signal that we're started + this->started = true; + EVLOG_info << "OCPP initialized"; + + // process session event queue + std::scoped_lock lock(this->session_event_mutex); + for (auto& [evse_id, evse_session_event_queue] : this->session_event_queue) { + while (!evse_session_event_queue.empty()) { + auto queued_session_event = evse_session_event_queue.front(); + EVLOG_info << "Processing queued session event for evse_id: " << evse_id + << ", event: " << queued_session_event.event; + this->process_session_event(evse_id, queued_session_event); + evse_session_event_queue.pop(); } - }); - - evse->subscribe_iso15118_certificate_request( - [this, evse_id](types::iso15118_charger::Request_Exi_Stream_Schema request) { - this->charge_point->data_transfer_pnc_get_15118_ev_certificate( - evse_id, request.exiRequest, request.iso15118SchemaVersion, - ocpp::v201::conversions::string_to_certificate_action_enum( - types::iso15118_charger::certificate_action_enum_to_string(request.certificateAction))); - }); - - evse_id++; + } } - std::unique_lock lk(this->evse_ready_mutex); - while (!this->all_evse_ready()) { - this->evse_ready_cv.wait(lk); + // signal to the EVSEs that OCPP is initialized + for (const auto& evse : this->r_evse_manager) { + evse->call_external_ready_to_start_charging(); } - this->charge_point->start(); } } // namespace module diff --git a/modules/OCPP/OCPP.hpp b/modules/OCPP/OCPP.hpp index 881364072..c4f13fd79 100644 --- a/modules/OCPP/OCPP.hpp +++ b/modules/OCPP/OCPP.hpp @@ -33,7 +33,10 @@ #include #include #include +#include #include +#include + #include #include #include @@ -104,7 +107,6 @@ class OCPP : public Everest::ModuleBase { // insert your public definitions here std::unique_ptr charge_point; std::unique_ptr charging_schedules_timer; - bool started = false; bool ocpp_stopped = false; // ev@1fce4c5e-0ab8-41bb-90f7-14277703d2ac:v1 @@ -123,11 +125,8 @@ class OCPP : public Everest::ModuleBase { std::filesystem::path ocpp_share_path; void set_external_limits(const std::map& charging_schedules); void publish_charging_schedules(const std::map& charging_schedules); - std::thread upload_diagnostics_thread; - std::thread upload_logs_thread; - std::thread update_firmware_thread; - std::thread signed_update_firmware_thread; + void init_evse_subscriptions(); // initialize subscriptions to all EVSEs provided by r_evse_manager void init_evse_connector_map(); void init_evse_ready_map(); EvseConnectorMap evse_connector_map; // provides access to OCPP connector id by using EVerests evse and connector id @@ -137,6 +136,11 @@ class OCPP : public Everest::ModuleBase { std::mutex evse_ready_mutex; std::condition_variable evse_ready_cv; bool all_evse_ready(); + + std::atomic_bool started{false}; + std::mutex session_event_mutex; + std::map> session_event_queue; + void process_session_event(int32_t evse_id, const types::evse_manager::SessionEvent& session_event); // ev@211cfdbe-f69a-4cd6-a4ec-f8aaa3d1b6c8:v1 };