// SPDX-License-Identifier: Apache-2.0 // Copyright Pionix GmbH and Contributors to EVerest #include "evse_managerImpl.hpp" #include #include #include #include #include #include "SessionLog.hpp" namespace module { namespace evse { bool str_to_bool(const std::string& data) { if (data == "true") { return true; } return false; } void evse_managerImpl::init() { limits.nr_of_phases_available = 1; limits.max_current = 0.; mod->signalNrOfPhasesAvailable.connect([this](const int n) { if (n >= 1 && n <= 3) { limits.nr_of_phases_available = n; publish_limits(limits); } }); // Interface to Node-RED debug UI mod->mqtt.subscribe(fmt::format("everest_external/nodered/{}/cmd/enable", mod->config.connector_id), [&charger = mod->charger](const std::string& data) { charger->enable_disable(0, {types::evse_manager::Enable_source::LocalAPI, types::evse_manager::Enable_state::Enable, 100}); }); mod->mqtt.subscribe(fmt::format("everest_external/nodered/{}/cmd/disable", mod->config.connector_id), [&charger = mod->charger](const std::string& data) { charger->enable_disable(0, {types::evse_manager::Enable_source::LocalAPI, types::evse_manager::Enable_state::Disable, 100}); }); mod->mqtt.subscribe( fmt::format("everest_external/nodered/{}/cmd/switch_three_phases_while_charging", mod->config.connector_id), [&charger = mod->charger](const std::string& data) { charger->switch_three_phases_while_charging(str_to_bool(data)); }); mod->mqtt.subscribe(fmt::format("everest_external/nodered/{}/cmd/pause_charging", mod->config.connector_id), [&charger = mod->charger](const std::string& data) { charger->pause_charging(); }); mod->mqtt.subscribe(fmt::format("everest_external/nodered/{}/cmd/resume_charging", mod->config.connector_id), [&charger = mod->charger](const std::string& data) { charger->resume_charging(); }); // /Interface to Node-RED debug UI if (mod->r_powermeter_billing().size() > 0) { mod->r_powermeter_billing()[0]->subscribe_powermeter([this](const types::powermeter::Powermeter& p) { // Republish data on proxy powermeter struct publish_powermeter(p); }); mod->r_powermeter_billing()[0]->subscribe_public_key_ocmf([this](const std::string& public_key_ocmf) { // Republish data on proxy powermeter public_key_ocmf publish_powermeter_public_key_ocmf(public_key_ocmf); }); } } void evse_managerImpl::ready() { // publish evse id at least once publish_evse_id(mod->config.evse_id); mod->r_bsp->subscribe_telemetry([this](types::evse_board_support::Telemetry telemetry) { // external Nodered interface mod->mqtt.publish(fmt::format("everest_external/nodered/{}/state/temperature", mod->config.connector_id), telemetry.evse_temperature_C); // external Nodered interface publish_telemetry(telemetry); }); // The module code generates the reservation events and we merely publish them here mod->signalReservationEvent.connect([this](types::evse_manager::SessionEvent j) { j.uuid.clear(); // Reservation is not part of a session publish_session_event(j); }); mod->charger->signal_session_started_event.connect( [this](const types::evse_manager::StartSessionReason& start_reason, const std::optional& provided_id_token) { types::evse_manager::SessionEvent se; se.event = types::evse_manager::SessionEventEnum::SessionStarted; this->mod->selected_protocol = "IEC61851-1"; types::evse_manager::SessionStarted session_started; se.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now()); session_started.meter_value = mod->get_latest_powermeter_data_billing(); if (mod->config.disable_authentication && start_reason == types::evse_manager::StartSessionReason::EVConnected) { // Free service, authorize immediately in a separate thread (to avoid dead lock with charger state // machine, this signal handler runs in state machine context) std::thread authorize_thread([this]() { types::authorization::ProvidedIdToken provided_token; provided_token.authorization_type = types::authorization::AuthorizationType::RFID; provided_token.id_token = {"FREESERVICE", types::authorization::IdTokenType::Local}; provided_token.prevalidated = true; mod->charger->authorize(true, provided_token, {types::authorization::AuthorizationStatus::Accepted}); mod->charger_was_authorized(); }); authorize_thread.detach(); } session_started.reason = start_reason; const auto session_uuid = this->mod->charger->get_session_id(); session_started.meter_value = mod->get_latest_powermeter_data_billing(); session_started.id_tag = provided_id_token; if (mod->is_reserved()) { session_started.reservation_id = mod->get_reservation_id(); if (start_reason == types::evse_manager::StartSessionReason::Authorized) { this->mod->cancel_reservation(false); } } const auto logging_path = session_log.startSession( mod->config.logfile_suffix == "session_uuid" ? session_uuid : mod->config.logfile_suffix); if (logging_path.has_value()) { session_started.logging_path = logging_path.value().string(); } session_log.evse(false, fmt::format("Session Started: {}", types::evse_manager::start_session_reason_to_string(start_reason))); mod->telemetry.publish("session", "events", { {"timestamp", Everest::Date::to_rfc3339(date::utc_clock::now())}, {"type", "session_started"}, {"session_id", session_uuid}, {"reason", types::evse_manager::start_session_reason_to_string(start_reason)}, }); se.session_started = session_started; se.uuid = session_uuid; publish_session_event(se); }); mod->charger->signal_transaction_started_event.connect([this]( const types::authorization::ProvidedIdToken& id_token) { types::evse_manager::SessionEvent se; se.event = types::evse_manager::SessionEventEnum::TransactionStarted; types::evse_manager::TransactionStarted transaction_started; se.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now()); transaction_started.meter_value = mod->get_latest_powermeter_data_billing(); if (mod->is_reserved()) { transaction_started.reservation_id.emplace(mod->get_reservation_id()); mod->cancel_reservation(false); // this allows OCPP1.6 to not move back to available. } transaction_started.id_tag = id_token; double energy_import = transaction_started.meter_value.energy_Wh_import.total; session_log.evse(false, fmt::format("Transaction Started ({} kWh)", energy_import / 1000.)); const auto session_uuid = this->mod->charger->get_session_id(); Everest::TelemetryMap telemetry_data = { {"timestamp", Everest::Date::to_rfc3339(date::utc_clock::now())}, {"type", "transaction_started"}, {"session_id", session_uuid}, {"energy_counter_import_wh", transaction_started.meter_value.energy_Wh_import.total}, {"id_tag", transaction_started.id_tag.id_token.value}}; if (transaction_started.meter_value.energy_Wh_export.has_value()) { telemetry_data["energy_counter_export_wh"] = transaction_started.meter_value.energy_Wh_export.value().total; } mod->telemetry.publish("session", "events", telemetry_data); se.transaction_started.emplace(transaction_started); se.uuid = session_uuid; publish_session_event(se); }); mod->charger->signal_transaction_finished_event.connect( [this](const types::evse_manager::StopTransactionReason& finished_reason, std::optional finish_token) { types::evse_manager::SessionEvent se; se.event = types::evse_manager::SessionEventEnum::TransactionFinished; this->mod->selected_protocol = "Unknown"; types::evse_manager::TransactionFinished transaction_finished; se.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now()); transaction_finished.meter_value = mod->get_latest_powermeter_data_billing(); transaction_finished.reason.emplace(finished_reason); transaction_finished.id_tag = finish_token; double energy_import = transaction_finished.meter_value.energy_Wh_import.total; session_log.evse(false, fmt::format("Transaction Finished: {} ({} kWh)", types::evse_manager::stop_transaction_reason_to_string(finished_reason), energy_import / 1000.)); const auto session_uuid = this->mod->charger->get_session_id(); Everest::TelemetryMap telemetry_data = { {"timestamp", Everest::Date::to_rfc3339(date::utc_clock::now())}, {"type", "transaction_finished"}, {"session_id", session_uuid}, {"energy_counter_import_wh", energy_import}, {"reason", types::evse_manager::stop_transaction_reason_to_string(finished_reason)}}; if (transaction_finished.meter_value.energy_Wh_export.has_value()) { telemetry_data["energy_counter_export_wh"] = transaction_finished.meter_value.energy_Wh_export.value().total; } transaction_finished.start_signed_meter_value = mod->charger->get_start_signed_meter_value(); transaction_finished.signed_meter_value = mod->charger->get_stop_signed_meter_value(); mod->telemetry.publish("session", "events", telemetry_data); se.transaction_finished.emplace(transaction_finished); se.uuid = session_uuid; publish_session_event(se); }); mod->charger->signal_charging_paused_evse_event.connect( [this](types::evse_manager::ChargingPausedEVSEReasons reason) { types::evse_manager::SessionEvent se; se.event = types::evse_manager::SessionEventEnum::ChargingPausedEVSE; se.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now()); const auto session_uuid = this->mod->charger->get_session_id(); types::evse_manager::ChargingStateChangedEvent charging_state_changed_event; charging_state_changed_event.meter_value = mod->get_latest_powermeter_data_billing(); se.charging_state_changed_event = charging_state_changed_event; se.uuid = session_uuid; se.charging_paused_evse = reason; publish_session_event(se); }); mod->charger->signal_simple_event.connect([this](const types::evse_manager::SessionEventEnum& e) { types::evse_manager::SessionEvent se; se.event = e; se.timestamp = Everest::Date::to_rfc3339(date::utc_clock::now()); const auto session_uuid = this->mod->charger->get_session_id(); if (e == types::evse_manager::SessionEventEnum::SessionFinished) { types::evse_manager::SessionFinished session_finished; session_finished.meter_value = mod->get_latest_powermeter_data_billing(); se.session_finished = session_finished; session_log.evse(false, fmt::format("Session Finished")); session_log.stopSession(); mod->telemetry.publish("session", "events", {{"timestamp", Everest::Date::to_rfc3339(date::utc_clock::now())}, {"type", "session_finished"}, {"session_id", session_uuid}}); } else if (e == types::evse_manager::SessionEventEnum::Enabled or e == types::evse_manager::SessionEventEnum::Disabled) { if (connector_status_changed) { se.connector_id = 1; } // Add source information (Who initiated this state change) se.source = mod->charger->get_last_enable_disable_source(); } else if (e == types::evse_manager::SessionEventEnum::ChargingPausedEV or e == types::evse_manager::SessionEventEnum::ChargingPausedEVSE or e == types::evse_manager::SessionEventEnum::ChargingStarted) { types::evse_manager::ChargingStateChangedEvent charging_state_changed_event; charging_state_changed_event.meter_value = mod->get_latest_powermeter_data_billing(); se.charging_state_changed_event = charging_state_changed_event; } else if (e == types::evse_manager::SessionEventEnum::Authorized or e == types::evse_manager::SessionEventEnum::Deauthorized) { types::evse_manager::AuthorizationEvent authorization_event; authorization_event.meter_value = mod->get_latest_powermeter_data_billing(); se.authorization_event = authorization_event; } se.uuid = session_uuid; publish_session_event(se); if (e == types::evse_manager::SessionEventEnum::SessionFinished) { this->mod->selected_protocol = "Unknown"; } // Cancel reservations if charger is disabled if (mod->is_reserved() and e == types::evse_manager::SessionEventEnum::Disabled) { mod->cancel_reservation(true); } 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) { mod->mqtt.publish(fmt::format("everest_external/nodered/{}/state/max_current", mod->config.connector_id), c); limits.uuid = mod->info.id; limits.max_current = c; publish_limits(limits); }); mod->charger->signal_state.connect([this](Charger::EvseState s) { mod->mqtt.publish(fmt::format("everest_external/nodered/{}/state/state_string", mod->config.connector_id), mod->charger->evse_state_to_string(s)); mod->mqtt.publish(fmt::format("everest_external/nodered/{}/state/state", mod->config.connector_id), static_cast(s)); }); } types::evse_manager::Evse evse_managerImpl::handle_get_evse() { types::evse_manager::Evse evse; evse.id = this->mod->config.connector_id; std::vector connectors; types::evse_manager::Connector connector; // EvseManager currently only supports a single connector with id: 1; connector.id = 1; connector.type = mod->connector_type; connectors.push_back(connector); evse.connectors = connectors; return evse; } bool evse_managerImpl::handle_enable_disable(int& connector_id, types::evse_manager::EnableDisableSource& cmd_source) { connector_status_changed = connector_id != 0; return mod->charger->enable_disable(connector_id, cmd_source); }; void evse_managerImpl::handle_authorize_response(types::authorization::ProvidedIdToken& provided_token, types::authorization::ValidationResult& validation_result) { const auto pnc = provided_token.authorization_type == types::authorization::AuthorizationType::PlugAndCharge; if (validation_result.authorization_status == types::authorization::AuthorizationStatus::Accepted) { if (this->mod->get_hlc_waiting_for_auth_pnc() && !pnc) { EVLOG_info << "EvseManager received Authorization other than PnC while waiting for PnC. This has no effect."; return; } this->mod->charger->authorize(true, provided_token, validation_result); mod->charger_was_authorized(); if (validation_result.reservation_id.has_value()) { EVLOG_debug << "Reserve evse manager reservation id for id " << validation_result.reservation_id.value(); // The validation result returns a reservation id. If this was a reservation for a specific evse, the // evse manager probably already stored the reservation id (and this call is not really necessary). But if // the reservation was not for a specific evse, the evse manager still has to send the reservation id in the // transaction event request. So that is why we call 'reserve' here, so the evse manager knows the // reservation id that belongs to this specific session and can send it accordingly. // As this is not a new reservation but an existing one, we don't signal a reservation event for this. mod->reserve(validation_result.reservation_id.value(), false); } } else if (pnc) { // we only send authorization responses to the HLC for PnC rejections. In case of EIM we could // still receive a successfull authorization later and therefore we don't inform the HLC this->mod->r_hlc[0]->call_authorization_response( validation_result.authorization_status, validation_result.certificate_status.value_or(types::authorization::CertificateStatus::Accepted)); } }; void evse_managerImpl::handle_withdraw_authorization() { // reservation_id might have been stored when reserved id token has been authorized, but timed out, so // we can consider the reservation as consumed if (mod->charger->get_authorized_eim() and mod->is_reserved()) { mod->cancel_reservation(true); } this->mod->charger->deauthorize(); }; bool evse_managerImpl::handle_reserve(int& reservation_id) { return mod->reserve(reservation_id, true); }; void evse_managerImpl::handle_cancel_reservation() { mod->cancel_reservation(true); }; bool evse_managerImpl::handle_pause_charging() { return mod->charger->pause_charging(); }; bool evse_managerImpl::handle_resume_charging() { return mod->charger->resume_charging(); }; bool evse_managerImpl::handle_stop_transaction(types::evse_manager::StopTransactionRequest& request) { return mod->charger->cancel_transaction(request); }; 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; } bool evse_managerImpl::handle_force_unlock(int& connector_id) { if (not mod->r_connector_lock.empty()) { types::evse_manager::StopTransactionRequest request; request.reason = types::evse_manager::StopTransactionReason::UnlockCommand; mod->charger->cancel_transaction(request); mod->bsp->connector_force_unlock(); return true; } return false; }; void evse_managerImpl::handle_set_plug_and_charge_configuration( types::evse_manager::PlugAndChargeConfiguration& plug_and_charge_configuration) { if (plug_and_charge_configuration.pnc_enabled.has_value()) { mod->set_pnc_enabled(plug_and_charge_configuration.pnc_enabled.value()); } if (plug_and_charge_configuration.central_contract_validation_allowed.has_value()) { mod->set_central_contract_validation_allowed( plug_and_charge_configuration.central_contract_validation_allowed.value()); } if (plug_and_charge_configuration.contract_certificate_installation_enabled.has_value()) { mod->set_contract_certificate_installation_enabled( plug_and_charge_configuration.contract_certificate_installation_enabled.value()); } } types::evse_manager::UpdateAllowedEnergyTransferModesResult evse_managerImpl::handle_update_allowed_energy_transfer_modes( std::vector& allowed_energy_transfer_modes) { std::vector filtered_energy_transfer_modes; if (mod->r_hlc.empty() or !mod->r_hlc[0]) { return types::evse_manager::UpdateAllowedEnergyTransferModesResult::NoHlc; } filtered_energy_transfer_modes.reserve(allowed_energy_transfer_modes.size()); // TODO(mlitre): Add check for incompatible type(s), for now we just transform DC stuff // in case of MCS and only if a connector type was configured at all; // also TODO: for DC we can check whether BPT can be supported in case DC supply supports it std::transform(allowed_energy_transfer_modes.begin(), allowed_energy_transfer_modes.end(), filtered_energy_transfer_modes.begin(), [&](types::iso15118::EnergyTransferMode m) { // for MCS we have to replace DC types with MCS types if (mod->connector_type.has_value() and mod->connector_type == types::evse_manager::ConnectorTypeEnum::cMCS) { if (m == types::iso15118::EnergyTransferMode::DC) { return types::iso15118::EnergyTransferMode::MCS; } if (m == types::iso15118::EnergyTransferMode::DC_BPT) { return types::iso15118::EnergyTransferMode::MCS_BPT; } } // everything else pass untouched return m; }); // check whether at least one mode has survived our filtering if (!filtered_energy_transfer_modes.size()) { return types::evse_manager::UpdateAllowedEnergyTransferModesResult::IncompatibleEnergyTransfer; } mod->r_hlc[0]->call_update_energy_transfer_modes(filtered_energy_transfer_modes); return types::evse_manager::UpdateAllowedEnergyTransferModesResult::Accepted; } } // namespace evse } // namespace module