- CitrineOS core extracted (CSMS OCPP 2.0.1) - OpenOCPP extracted (firmware OCPP 1.6J/2.0.1) - ShapeShifter library installed (pip install -e) - ShapeShifter specification extracted - EVerest extracted TODO updated with progress
1855 lines
93 KiB
C++
1855 lines
93 KiB
C++
// SPDX-License-Identifier: Apache-2.0
|
|
// Copyright Pionix GmbH and Contributors to EVerest
|
|
#include "OCPP201.hpp"
|
|
|
|
#include <fmt/core.h>
|
|
#include <fstream>
|
|
|
|
#include <websocketpp_utils/uri.hpp>
|
|
|
|
#include <conversions.hpp>
|
|
#include <device_model/composed_device_model_storage.hpp>
|
|
#include <error_handling.hpp>
|
|
#include <everest/conversions/ocpp/evse_security_ocpp.hpp>
|
|
#include <everest/conversions/ocpp/ocpp_conversions.hpp>
|
|
#include <everest/external_energy_limits/external_energy_limits.hpp>
|
|
#include <ocpp/v2/utils.hpp>
|
|
|
|
namespace {
|
|
void update_evcc_id_token(ocpp::v2::IdToken& id_token, const std::string& evcc_id,
|
|
const ocpp::OcppProtocolVersion ocpp_protocol_version) {
|
|
if (ocpp_protocol_version != ocpp::OcppProtocolVersion::v21) {
|
|
return;
|
|
}
|
|
auto info_vector =
|
|
id_token.additionalInfo.has_value() ? id_token.additionalInfo.value() : std::vector<ocpp::v2::AdditionalInfo>{};
|
|
if (!id_token.additionalInfo.has_value() or
|
|
(id_token.additionalInfo.has_value() and
|
|
std::find_if(id_token.additionalInfo->cbegin(), id_token.additionalInfo->cend(),
|
|
[evcc_id](const ocpp::v2::AdditionalInfo& info) {
|
|
return info.additionalIdToken.get() == evcc_id;
|
|
}) == id_token.additionalInfo->cend())) {
|
|
ocpp::v2::AdditionalInfo info;
|
|
info.additionalIdToken = evcc_id;
|
|
info.type = "EVCCID";
|
|
info_vector.push_back(info);
|
|
id_token.additionalInfo = info_vector;
|
|
}
|
|
}
|
|
|
|
std::string ocpp_protocol_version_to_string(const ocpp::OcppProtocolVersion ocpp_protocol_version) {
|
|
switch (ocpp_protocol_version) {
|
|
case ocpp::OcppProtocolVersion::v16:
|
|
return "1.6";
|
|
case ocpp::OcppProtocolVersion::v201:
|
|
return "2.0.1";
|
|
case ocpp::OcppProtocolVersion::v21:
|
|
return "2.1";
|
|
case ocpp::OcppProtocolVersion::Unknown:
|
|
return "Unknown";
|
|
}
|
|
return "Unknown";
|
|
}
|
|
} // namespace
|
|
|
|
namespace module {
|
|
|
|
const std::string SQL_CORE_MIGRATIONS = "core_migrations";
|
|
const std::string CERTS_DIR = "certs";
|
|
|
|
// OCPP 2.0.1 specific configuration variable names
|
|
const std::string PNC_ENABLED_VAR_NAME = "PnCEnabled";
|
|
const std::string MASTER_PASS_GROUP_ID_VAR_NAME = "MasterPassGroupId";
|
|
const std::string EV_CONNECTION_TIMEOUT_VAR_NAME = "EVConnectionTimeOut";
|
|
const std::string CENTRAL_CONTRACT_VALIDATION_ALLOWED_VAR_NAME = "CentralContractValidationAllowed";
|
|
const std::string CONTRACT_CERTIFICATE_INSTALLATION_ENABLED_VAR_NAME = "ContractCertificateInstallationEnabled";
|
|
const std::string SETPOINT_PRIORITY_VAR_NAME = "SetpointPriority";
|
|
const std::string TX_START_POINT_VAR_NAME = "TxStartPoint";
|
|
const std::string TX_STOP_POINT_VAR_NAME = "TxStopPoint";
|
|
const std::string SETPOINT_SOURCE = "OCPP";
|
|
|
|
static constexpr int32_t LOWEST_SETPOINT_PRIORITY = 1000;
|
|
static constexpr int32_t HIGHEST_SETPOINT_PRIORITY = 0;
|
|
|
|
namespace fs = std::filesystem;
|
|
|
|
TxEvent get_tx_event(const ocpp::v2::ReasonEnum reason) {
|
|
switch (reason) {
|
|
case ocpp::v2::ReasonEnum::DeAuthorized:
|
|
case ocpp::v2::ReasonEnum::Remote:
|
|
case ocpp::v2::ReasonEnum::Local:
|
|
case ocpp::v2::ReasonEnum::MasterPass:
|
|
case ocpp::v2::ReasonEnum::StoppedByEV:
|
|
case ocpp::v2::ReasonEnum::ReqEnergyTransferRejected:
|
|
return TxEvent::DEAUTHORIZED;
|
|
case ocpp::v2::ReasonEnum::EVDisconnected:
|
|
return TxEvent::EV_DISCONNECTED;
|
|
case ocpp::v2::ReasonEnum::ImmediateReset:
|
|
return TxEvent::IMMEDIATE_RESET;
|
|
// FIXME(kai): these reasons definitely do not all map to NONE
|
|
case ocpp::v2::ReasonEnum::EmergencyStop:
|
|
case ocpp::v2::ReasonEnum::EnergyLimitReached:
|
|
case ocpp::v2::ReasonEnum::GroundFault:
|
|
case ocpp::v2::ReasonEnum::LocalOutOfCredit:
|
|
case ocpp::v2::ReasonEnum::Other:
|
|
case ocpp::v2::ReasonEnum::OvercurrentFault:
|
|
case ocpp::v2::ReasonEnum::PowerLoss:
|
|
case ocpp::v2::ReasonEnum::PowerQuality:
|
|
case ocpp::v2::ReasonEnum::Reboot:
|
|
case ocpp::v2::ReasonEnum::SOCLimitReached:
|
|
case ocpp::v2::ReasonEnum::TimeLimitReached:
|
|
case ocpp::v2::ReasonEnum::Timeout:
|
|
return TxEvent::NONE;
|
|
}
|
|
return TxEvent::NONE;
|
|
}
|
|
|
|
std::set<TxStartStopPoint> get_tx_start_stop_points(const std::string& tx_start_stop_point_csl) {
|
|
std::set<TxStartStopPoint> tx_start_stop_points;
|
|
std::vector<std::string> csv;
|
|
std::string str;
|
|
std::stringstream ss(tx_start_stop_point_csl);
|
|
while (std::getline(ss, str, ',')) {
|
|
csv.push_back(str);
|
|
}
|
|
|
|
for (const auto& tx_start_stop_point : csv) {
|
|
if (tx_start_stop_point == "ParkingBayOccupancy") {
|
|
tx_start_stop_points.insert(TxStartStopPoint::ParkingBayOccupancy);
|
|
} else if (tx_start_stop_point == "EVConnected") {
|
|
tx_start_stop_points.insert(TxStartStopPoint::EVConnected);
|
|
} else if (tx_start_stop_point == "Authorized") {
|
|
tx_start_stop_points.insert(TxStartStopPoint::Authorized);
|
|
} else if (tx_start_stop_point == "PowerPathClosed") {
|
|
tx_start_stop_points.insert(TxStartStopPoint::PowerPathClosed);
|
|
} else if (tx_start_stop_point == "EnergyTransfer") {
|
|
tx_start_stop_points.insert(TxStartStopPoint::EnergyTransfer);
|
|
} else if (tx_start_stop_point == "DataSigned") {
|
|
tx_start_stop_points.insert(TxStartStopPoint::DataSigned);
|
|
} else {
|
|
// default to PowerPathClosed for now
|
|
tx_start_stop_points.insert(TxStartStopPoint::PowerPathClosed);
|
|
}
|
|
}
|
|
return tx_start_stop_points;
|
|
}
|
|
|
|
ocpp::v2::TriggerReasonEnum stop_reason_to_trigger_reason_enum(const ocpp::v2::ReasonEnum& stop_reason) {
|
|
switch (stop_reason) {
|
|
case ocpp::v2::ReasonEnum::DeAuthorized:
|
|
return ocpp::v2::TriggerReasonEnum::Deauthorized;
|
|
case ocpp::v2::ReasonEnum::EmergencyStop:
|
|
return ocpp::v2::TriggerReasonEnum::AbnormalCondition;
|
|
case ocpp::v2::ReasonEnum::EnergyLimitReached:
|
|
return ocpp::v2::TriggerReasonEnum::EnergyLimitReached;
|
|
case ocpp::v2::ReasonEnum::EVDisconnected:
|
|
return ocpp::v2::TriggerReasonEnum::EVCommunicationLost;
|
|
case ocpp::v2::ReasonEnum::GroundFault:
|
|
return ocpp::v2::TriggerReasonEnum::AbnormalCondition;
|
|
case ocpp::v2::ReasonEnum::ImmediateReset:
|
|
return ocpp::v2::TriggerReasonEnum::ResetCommand;
|
|
case ocpp::v2::ReasonEnum::Local:
|
|
return ocpp::v2::TriggerReasonEnum::StopAuthorized;
|
|
case ocpp::v2::ReasonEnum::LocalOutOfCredit:
|
|
return ocpp::v2::TriggerReasonEnum::ChargingStateChanged;
|
|
case ocpp::v2::ReasonEnum::MasterPass:
|
|
return ocpp::v2::TriggerReasonEnum::StopAuthorized;
|
|
case ocpp::v2::ReasonEnum::PowerQuality:
|
|
return ocpp::v2::TriggerReasonEnum::AbnormalCondition;
|
|
case ocpp::v2::ReasonEnum::Reboot:
|
|
return ocpp::v2::TriggerReasonEnum::ResetCommand;
|
|
case ocpp::v2::ReasonEnum::Remote:
|
|
return ocpp::v2::TriggerReasonEnum::RemoteStop;
|
|
case ocpp::v2::ReasonEnum::SOCLimitReached:
|
|
return ocpp::v2::TriggerReasonEnum::EnergyLimitReached;
|
|
case ocpp::v2::ReasonEnum::StoppedByEV:
|
|
return ocpp::v2::TriggerReasonEnum::StopAuthorized;
|
|
case ocpp::v2::ReasonEnum::TimeLimitReached:
|
|
return ocpp::v2::TriggerReasonEnum::TimeLimitReached;
|
|
case ocpp::v2::ReasonEnum::Timeout:
|
|
return ocpp::v2::TriggerReasonEnum::EVConnectTimeout;
|
|
case ocpp::v2::ReasonEnum::Other:
|
|
case ocpp::v2::ReasonEnum::ReqEnergyTransferRejected:
|
|
case ocpp::v2::ReasonEnum::OvercurrentFault:
|
|
case ocpp::v2::ReasonEnum::PowerLoss:
|
|
default:
|
|
return ocpp::v2::TriggerReasonEnum::AbnormalCondition;
|
|
}
|
|
}
|
|
|
|
int32_t get_connector_id_from_error(const Everest::error::Error& error) {
|
|
if (error.origin.mapping.has_value() and error.origin.mapping.value().connector.has_value()) {
|
|
return error.origin.mapping.value().connector.value();
|
|
}
|
|
return 1;
|
|
}
|
|
|
|
void OCPP201::init_evse_maps() {
|
|
{
|
|
auto ready_handle = this->evse_ready_map.handle();
|
|
for (size_t evse_id = 1; evse_id <= this->r_evse_manager.size(); evse_id++) {
|
|
(*ready_handle)[evse_id] = false;
|
|
}
|
|
}
|
|
{
|
|
auto soc_handle = this->evse_soc_map.handle();
|
|
for (size_t evse_id = 1; evse_id <= this->r_evse_manager.size(); evse_id++) {
|
|
(*soc_handle)[evse_id] = std::nullopt;
|
|
}
|
|
}
|
|
{
|
|
auto evse_evcc_id_handle = this->evse_evcc_id.handle();
|
|
for (size_t evse_id = 1; evse_id <= this->r_evse_manager.size(); evse_id++) {
|
|
(*evse_evcc_id_handle)[evse_id] = "";
|
|
}
|
|
}
|
|
|
|
for (size_t evse_id = 1; evse_id <= this->r_evse_manager.size(); evse_id++) {
|
|
this->evse_hardware_capabilities_map[evse_id] = types::evse_board_support::HardwareCapabilities{};
|
|
this->evse_supported_energy_transfer_modes[evse_id] = {};
|
|
this->evse_service_renegotiation_supported[evse_id] = false;
|
|
}
|
|
}
|
|
|
|
void OCPP201::init_module_configuration() {
|
|
const auto ev_connection_timeout_request_value_response = this->charge_point->request_value<int32_t>(
|
|
ocpp::v2::ControllerComponents::TxCtrlr, ocpp::v2::Variable{EV_CONNECTION_TIMEOUT_VAR_NAME},
|
|
ocpp::v2::AttributeEnum::Actual);
|
|
if (ev_connection_timeout_request_value_response.status == ocpp::v2::GetVariableStatusEnum::Accepted and
|
|
ev_connection_timeout_request_value_response.value.has_value()) {
|
|
this->r_auth->call_set_connection_timeout(ev_connection_timeout_request_value_response.value.value());
|
|
}
|
|
|
|
const auto master_pass_group_id_response = this->charge_point->request_value<std::string>(
|
|
ocpp::v2::ControllerComponents::AuthCtrlr, ocpp::v2::Variable{MASTER_PASS_GROUP_ID_VAR_NAME},
|
|
ocpp::v2::AttributeEnum::Actual);
|
|
if (master_pass_group_id_response.status == ocpp::v2::GetVariableStatusEnum::Accepted and
|
|
master_pass_group_id_response.value.has_value()) {
|
|
this->r_auth->call_set_master_pass_group_id(master_pass_group_id_response.value.value());
|
|
}
|
|
|
|
types::evse_manager::PlugAndChargeConfiguration pnc_config;
|
|
const auto iso15118_pnc_enabled_response = this->charge_point->request_value<bool>(
|
|
ocpp::v2::ControllerComponents::ISO15118Ctrlr, ocpp::v2::Variable{PNC_ENABLED_VAR_NAME},
|
|
ocpp::v2::AttributeEnum::Actual);
|
|
if (iso15118_pnc_enabled_response.status == ocpp::v2::GetVariableStatusEnum::Accepted and
|
|
iso15118_pnc_enabled_response.value.has_value()) {
|
|
pnc_config.pnc_enabled = iso15118_pnc_enabled_response.value.value();
|
|
}
|
|
|
|
const auto central_contract_validation_allowed_response = this->charge_point->request_value<bool>(
|
|
ocpp::v2::ControllerComponents::ISO15118Ctrlr, ocpp::v2::Variable{CENTRAL_CONTRACT_VALIDATION_ALLOWED_VAR_NAME},
|
|
ocpp::v2::AttributeEnum::Actual);
|
|
if (central_contract_validation_allowed_response.status == ocpp::v2::GetVariableStatusEnum::Accepted and
|
|
central_contract_validation_allowed_response.value.has_value()) {
|
|
pnc_config.central_contract_validation_allowed = central_contract_validation_allowed_response.value.value();
|
|
}
|
|
|
|
const auto contract_certificate_installation_enabled_response = this->charge_point->request_value<bool>(
|
|
ocpp::v2::ControllerComponents::ISO15118Ctrlr,
|
|
ocpp::v2::Variable{CONTRACT_CERTIFICATE_INSTALLATION_ENABLED_VAR_NAME}, ocpp::v2::AttributeEnum::Actual);
|
|
if (contract_certificate_installation_enabled_response.status == ocpp::v2::GetVariableStatusEnum::Accepted and
|
|
contract_certificate_installation_enabled_response.value.has_value()) {
|
|
pnc_config.contract_certificate_installation_enabled =
|
|
contract_certificate_installation_enabled_response.value.value();
|
|
}
|
|
|
|
for (const auto& evse_manager : this->r_evse_manager) {
|
|
evse_manager->call_set_plug_and_charge_configuration(pnc_config);
|
|
}
|
|
}
|
|
|
|
std::map<int32_t, int32_t> OCPP201::get_connector_structure() {
|
|
std::map<int32_t, int32_t> evse_connector_structure;
|
|
int evse_id = 1;
|
|
for (const auto& evse : this->r_evse_manager) {
|
|
auto _evse = evse->call_get_evse();
|
|
int32_t num_connectors = _evse.connectors.size();
|
|
|
|
if (_evse.id != evse_id) {
|
|
throw std::runtime_error("Configured evse_id(s) must start with 1 counting upwards");
|
|
}
|
|
if (num_connectors > 0) {
|
|
int connector_id = 1;
|
|
for (const auto& connector : _evse.connectors) {
|
|
if (connector.id != connector_id) {
|
|
throw std::runtime_error("Configured connector_id(s) must start with 1 counting upwards");
|
|
}
|
|
connector_id++;
|
|
}
|
|
} else {
|
|
num_connectors = 1;
|
|
}
|
|
|
|
evse_connector_structure[evse_id] = num_connectors;
|
|
evse_id++;
|
|
}
|
|
return evse_connector_structure;
|
|
}
|
|
|
|
types::powermeter::Powermeter get_meter_value(const types::evse_manager::SessionEvent& session_event) {
|
|
const auto event_type = session_event.event;
|
|
if (event_type == types::evse_manager::SessionEventEnum::SessionStarted) {
|
|
if (!session_event.session_started.has_value()) {
|
|
throw std::runtime_error("SessionEvent SessionStarted does not contain session_started context");
|
|
}
|
|
return session_event.session_started.value().meter_value;
|
|
} else if (event_type == types::evse_manager::SessionEventEnum::SessionFinished) {
|
|
if (!session_event.session_finished.has_value()) {
|
|
throw std::runtime_error("SessionEvent SessionFinished does not contain session_finished context");
|
|
}
|
|
return session_event.session_finished.value().meter_value;
|
|
} else if (event_type == types::evse_manager::SessionEventEnum::TransactionStarted) {
|
|
if (!session_event.transaction_started.has_value()) {
|
|
throw std::runtime_error("SessionEvent TransactionStarted does not contain transaction_started context");
|
|
}
|
|
return session_event.transaction_started.value().meter_value;
|
|
} else if (event_type == types::evse_manager::SessionEventEnum::TransactionFinished) {
|
|
if (!session_event.transaction_finished.has_value()) {
|
|
throw std::runtime_error("SessionEvent TransactionFinished does not contain transaction_finished context");
|
|
}
|
|
return session_event.transaction_finished.value().meter_value;
|
|
} else if (event_type == types::evse_manager::SessionEventEnum::ChargingStarted or
|
|
event_type == types::evse_manager::SessionEventEnum::ChargingPausedEV or
|
|
event_type == types::evse_manager::SessionEventEnum::ChargingPausedEVSE) {
|
|
if (!session_event.charging_state_changed_event.has_value()) {
|
|
throw std::runtime_error("SessionEvent does not contain charging_state_changed_event context");
|
|
}
|
|
return session_event.charging_state_changed_event.value().meter_value;
|
|
} else if (event_type == types::evse_manager::SessionEventEnum::Authorized or
|
|
event_type == types::evse_manager::SessionEventEnum::Deauthorized) {
|
|
if (!session_event.authorization_event.has_value()) {
|
|
throw std::runtime_error(
|
|
"SessionEvent Authorized or Deauthorized does not contain authorization_event context");
|
|
}
|
|
return session_event.authorization_event.value().meter_value;
|
|
} else {
|
|
throw std::runtime_error("Could not retrieve meter value from SessionEvent");
|
|
}
|
|
}
|
|
|
|
std::optional<types::units_signed::SignedMeterValue>
|
|
get_signed_meter_value(const types::evse_manager::SessionEvent& session_event) {
|
|
const auto event_type = session_event.event;
|
|
if (event_type == types::evse_manager::SessionEventEnum::SessionStarted) {
|
|
if (!session_event.session_started.has_value()) {
|
|
throw std::runtime_error("SessionEvent SessionStarted does not contain session_started context");
|
|
}
|
|
return session_event.session_started.value().signed_meter_value;
|
|
} else if (event_type == types::evse_manager::SessionEventEnum::TransactionStarted) {
|
|
if (!session_event.transaction_started.has_value()) {
|
|
throw std::runtime_error("SessionEvent TransactionStarted does not contain transaction_started context");
|
|
}
|
|
return session_event.transaction_started.value().signed_meter_value;
|
|
} else if (event_type == types::evse_manager::SessionEventEnum::TransactionFinished) {
|
|
if (!session_event.transaction_finished.has_value()) {
|
|
throw std::runtime_error("SessionEvent TransactionFinished does not contain transaction_finished context");
|
|
}
|
|
return session_event.transaction_finished.value().signed_meter_value;
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
std::optional<ocpp::v2::IdToken> get_authorized_id_token(const types::evse_manager::SessionEvent& session_event) {
|
|
const auto event_type = session_event.event;
|
|
if (event_type == types::evse_manager::SessionEventEnum::SessionStarted) {
|
|
if (!session_event.session_started.has_value()) {
|
|
throw std::runtime_error("SessionEvent SessionStarted does not contain session_started context");
|
|
}
|
|
const auto session_started = session_event.session_started.value();
|
|
if (session_started.id_tag.has_value()) {
|
|
return conversions::to_ocpp_id_token(session_started.id_tag.value().id_token);
|
|
}
|
|
} else if (event_type == types::evse_manager::SessionEventEnum::TransactionStarted) {
|
|
if (!session_event.transaction_started.has_value()) {
|
|
throw std::runtime_error("SessionEvent TransactionStarted does not contain transaction_started context");
|
|
}
|
|
const auto transaction_started = session_event.transaction_started.value();
|
|
return conversions::to_ocpp_id_token(transaction_started.id_tag.id_token);
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
ocpp::v2::ChargingRateUnitEnum get_unit_or_default(const std::string& unit_string) {
|
|
try {
|
|
return ocpp::v2::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::v2::ChargingRateUnitEnum::A;
|
|
}
|
|
}
|
|
|
|
void OCPP201::init() {
|
|
invoke_init(*p_auth_provider);
|
|
invoke_init(*p_auth_validator);
|
|
|
|
source_ext_limit = info.id + "/OCPP_set_external_limits";
|
|
|
|
// ensure all evse_energy_sink(s) that are connected have an evse id mapping
|
|
for (const auto& evse_sink : this->r_evse_energy_sink) {
|
|
if (not evse_sink->get_mapping().has_value()) {
|
|
EVLOG_critical << "Please configure an evse mapping in your configuration file for the connected "
|
|
"r_evse_energy_sink with module_id: "
|
|
<< evse_sink->module_id;
|
|
throw std::runtime_error("At least one connected evse_energy_sink misses a mapping to an evse.");
|
|
}
|
|
}
|
|
|
|
this->init_evse_maps();
|
|
|
|
const auto error_handler = [this](const Everest::error::Error& error) {
|
|
if (error.type == EVSE_MANAGER_INOPERATIVE_ERROR) {
|
|
// handled by specific evse_manager error handler
|
|
return;
|
|
}
|
|
if (this->started) {
|
|
const auto event_data = get_event_data(error, false, this->event_id_counter++);
|
|
this->charge_point->on_event({event_data});
|
|
} else {
|
|
std::scoped_lock lock(this->session_event_mutex);
|
|
this->event_queue[get_component_from_error(error).evse.value_or(ocpp::v2::EVSE{0}).id].push(error);
|
|
}
|
|
};
|
|
|
|
const auto error_cleared_handler = [this](const Everest::error::Error& error) {
|
|
if (error.type == EVSE_MANAGER_INOPERATIVE_ERROR) {
|
|
// handled by specific evse_manager error handler
|
|
return;
|
|
}
|
|
if (this->started) {
|
|
const auto event_data = get_event_data(error, true, this->event_id_counter++);
|
|
this->charge_point->on_event({event_data});
|
|
} else {
|
|
std::scoped_lock lock(this->session_event_mutex);
|
|
this->event_queue[get_component_from_error(error).evse.value_or(ocpp::v2::EVSE{0}).id].push(error);
|
|
}
|
|
};
|
|
|
|
subscribe_global_all_errors(error_handler, error_cleared_handler);
|
|
|
|
r_system->subscribe_firmware_update_status([this](const types::system::FirmwareUpdateStatus status) {
|
|
if (this->started) {
|
|
this->charge_point->on_firmware_update_status_notification(
|
|
status.request_id, conversions::to_ocpp_firmware_status_enum(status.firmware_update_status));
|
|
} else {
|
|
std::scoped_lock lock(this->session_event_mutex);
|
|
this->event_queue[0].push(status);
|
|
}
|
|
});
|
|
|
|
r_system->subscribe_log_status([this](types::system::LogStatus status) {
|
|
if (this->started) {
|
|
this->charge_point->on_log_status_notification(
|
|
conversions::to_ocpp_upload_logs_status_enum(status.log_status), status.request_id);
|
|
} else {
|
|
std::scoped_lock lock(this->session_event_mutex);
|
|
this->event_queue[0].push(status);
|
|
}
|
|
});
|
|
|
|
if (!this->r_reservation.empty() && this->r_reservation.at(0) != nullptr) {
|
|
r_reservation.at(0)->subscribe_reservation_update(
|
|
[this](const types::reservation::ReservationUpdateStatus status) {
|
|
if (status.reservation_status == types::reservation::Reservation_status::Expired ||
|
|
status.reservation_status == types::reservation::Reservation_status::Removed) {
|
|
EVLOG_debug << "Received reservation status update for reservation " << status.reservation_id
|
|
<< ": "
|
|
<< (status.reservation_status == types::reservation::Reservation_status::Expired
|
|
? "Expired"
|
|
: "Removed");
|
|
try {
|
|
this->charge_point->on_reservation_status(
|
|
status.reservation_id,
|
|
conversions::to_ocpp_reservation_update_status_enum(status.reservation_status));
|
|
} catch (const std::out_of_range& e) {
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
this->init_evse_subscriptions();
|
|
}
|
|
|
|
void OCPP201::ready() {
|
|
invoke_ready(*p_auth_provider);
|
|
invoke_ready(*p_auth_validator);
|
|
|
|
this->ocpp_share_path = this->info.paths.share;
|
|
|
|
const auto device_model_database_path = [&]() {
|
|
const auto config_device_model_path = fs::path(this->config.DeviceModelDatabasePath);
|
|
if (config_device_model_path.is_relative()) {
|
|
return this->ocpp_share_path / config_device_model_path;
|
|
} else {
|
|
return config_device_model_path;
|
|
}
|
|
}();
|
|
|
|
const auto everest_device_model_database_path = [&]() {
|
|
const auto config_everest_device_model_path = fs::path(this->config.EverestDeviceModelDatabasePath);
|
|
if (config_everest_device_model_path.is_relative()) {
|
|
return this->ocpp_share_path / config_everest_device_model_path;
|
|
} else {
|
|
return config_everest_device_model_path;
|
|
}
|
|
}();
|
|
|
|
const auto device_model_database_migration_path = [&]() {
|
|
const auto config_device_model_database_migration_path =
|
|
fs::path(this->config.DeviceModelDatabaseMigrationPath);
|
|
if (config_device_model_database_migration_path.is_relative()) {
|
|
return this->ocpp_share_path / config_device_model_database_migration_path;
|
|
} else {
|
|
return config_device_model_database_migration_path;
|
|
}
|
|
}();
|
|
|
|
const auto device_model_config_path = [&]() {
|
|
const auto config_device_model_config_path = fs::path(this->config.DeviceModelConfigPath);
|
|
if (config_device_model_config_path.is_relative()) {
|
|
return this->ocpp_share_path / config_device_model_config_path;
|
|
} else {
|
|
return config_device_model_config_path;
|
|
}
|
|
}();
|
|
|
|
if (!fs::exists(this->config.MessageLogPath)) {
|
|
try {
|
|
fs::create_directory(this->config.MessageLogPath);
|
|
} catch (const fs::filesystem_error& e) {
|
|
EVLOG_AND_THROW(e);
|
|
}
|
|
}
|
|
|
|
ocpp::v2::Callbacks callbacks;
|
|
callbacks.is_reset_allowed_callback = [this](const std::optional<const int32_t> evse_id,
|
|
const ocpp::v2::ResetEnum&) {
|
|
if (evse_id.has_value()) {
|
|
return false; // Reset of EVSE is currently not supported
|
|
}
|
|
try {
|
|
return this->r_system->call_is_reset_allowed(types::system::ResetType::NotSpecified);
|
|
} catch (std::out_of_range& e) {
|
|
EVLOG_warning << "Could not convert OCPP ResetEnum to EVerest ResetType while executing "
|
|
"is_reset_allowed_callback.";
|
|
return false;
|
|
}
|
|
};
|
|
callbacks.reset_callback = [this](const std::optional<const int32_t> evse_id, const ocpp::v2::ResetEnum& type) {
|
|
if (evse_id.has_value()) {
|
|
EVLOG_warning << "Reset of EVSE is currently not supported";
|
|
return;
|
|
}
|
|
|
|
bool scheduled = type == ocpp::v2::ResetEnum::OnIdle;
|
|
|
|
// small delay before stopping the charge point to make sure all responses are received
|
|
std::this_thread::sleep_for(std::chrono::seconds(this->config.ResetStopDelay));
|
|
try {
|
|
this->r_system->call_reset(types::system::ResetType::NotSpecified, scheduled);
|
|
} catch (std::out_of_range& e) {
|
|
EVLOG_warning << "Could not convert OCPP ResetEnum to EVerest ResetType while executing reset_callack. No "
|
|
"reset will be executed.";
|
|
}
|
|
};
|
|
|
|
callbacks.connector_effective_operative_status_changed_callback =
|
|
[this](const int32_t evse_id, const int32_t connector_id, const ocpp::v2::OperationalStatusEnum new_status) {
|
|
if (new_status == ocpp::v2::OperationalStatusEnum::Operative) {
|
|
if (this->r_evse_manager.at(evse_id - 1)
|
|
->call_enable_disable(connector_id, {types::evse_manager::Enable_source::CSMS,
|
|
types::evse_manager::Enable_state::Enable, 5000})) {
|
|
this->charge_point->on_enabled(evse_id, connector_id);
|
|
}
|
|
} else {
|
|
if (this->r_evse_manager.at(evse_id - 1)
|
|
->call_enable_disable(connector_id, {types::evse_manager::Enable_source::CSMS,
|
|
types::evse_manager::Enable_state::Disable, 5000})) {
|
|
this->charge_point->on_unavailable(evse_id, connector_id);
|
|
}
|
|
}
|
|
};
|
|
|
|
callbacks.remote_start_transaction_callback = [this](const ocpp::v2::RequestStartTransactionRequest& request,
|
|
const bool authorize_remote_start) {
|
|
types::authorization::ProvidedIdToken provided_token;
|
|
provided_token.id_token = conversions::to_everest_id_token(request.idToken);
|
|
provided_token.authorization_type = types::authorization::AuthorizationType::OCPP;
|
|
provided_token.prevalidated = !authorize_remote_start;
|
|
provided_token.request_id = request.remoteStartId;
|
|
|
|
if (request.groupIdToken.has_value()) {
|
|
provided_token.parent_id_token = conversions::to_everest_id_token(request.groupIdToken.value());
|
|
}
|
|
|
|
if (request.evseId.has_value()) {
|
|
provided_token.connectors = std::vector<int32_t>{request.evseId.value()};
|
|
}
|
|
this->p_auth_provider->publish_provided_token(provided_token);
|
|
return ocpp::v2::RequestStartStopStatusEnum::Accepted;
|
|
};
|
|
|
|
callbacks.stop_transaction_callback = [this](const int32_t evse_id, const ocpp::v2::ReasonEnum& stop_reason) {
|
|
if (evse_id <= 0 or evse_id > this->r_evse_manager.size()) {
|
|
return ocpp::v2::RequestStartStopStatusEnum::Rejected;
|
|
}
|
|
|
|
types::evse_manager::StopTransactionRequest req;
|
|
req.reason = conversions::to_everest_stop_transaction_reason(stop_reason);
|
|
|
|
return this->r_evse_manager.at(evse_id - 1)->call_stop_transaction(req)
|
|
? ocpp::v2::RequestStartStopStatusEnum::Accepted
|
|
: ocpp::v2::RequestStartStopStatusEnum::Rejected;
|
|
};
|
|
|
|
callbacks.pause_charging_callback = [this](const int32_t evse_id) {
|
|
if (evse_id > 0 && evse_id <= this->r_evse_manager.size()) {
|
|
this->r_evse_manager.at(evse_id - 1)->call_pause_charging();
|
|
}
|
|
};
|
|
|
|
callbacks.unlock_connector_callback = [this](const int32_t evse_id, const int32_t connector_id) {
|
|
// FIXME: This needs to properly handle different connectors
|
|
ocpp::v2::UnlockConnectorResponse response;
|
|
if (evse_id > 0 && evse_id <= this->r_evse_manager.size()) {
|
|
if (this->r_evse_manager.at(evse_id - 1)->call_force_unlock(connector_id)) {
|
|
response.status = ocpp::v2::UnlockStatusEnum::Unlocked;
|
|
} else {
|
|
response.status = ocpp::v2::UnlockStatusEnum::UnlockFailed;
|
|
}
|
|
} else {
|
|
response.status = ocpp::v2::UnlockStatusEnum::UnknownConnector;
|
|
}
|
|
|
|
return response;
|
|
};
|
|
|
|
callbacks.get_log_request_callback = [this](const ocpp::v2::GetLogRequest& request) {
|
|
auto req = conversions::to_everest_upload_logs_request(request);
|
|
if (req.retries.has_value()) {
|
|
req.retries = req.retries.value() + 1;
|
|
}
|
|
const auto response = this->r_system->call_upload_logs(req);
|
|
return conversions::to_ocpp_get_log_response(response);
|
|
};
|
|
|
|
callbacks.is_reservation_for_token_callback = [this](const int32_t evse_id, const ocpp::CiString<255> idToken,
|
|
const std::optional<ocpp::CiString<255>> groupIdToken) {
|
|
if (this->r_reservation.empty() || this->r_reservation.at(0) == nullptr) {
|
|
return ocpp::ReservationCheckStatus::NotReserved;
|
|
}
|
|
|
|
types::reservation::ReservationCheck reservation_check_request;
|
|
reservation_check_request.evse_id = evse_id;
|
|
reservation_check_request.id_token = idToken.get();
|
|
if (groupIdToken.has_value()) {
|
|
reservation_check_request.group_id_token = groupIdToken.value().get();
|
|
}
|
|
|
|
const types::reservation::ReservationCheckStatus reservation_status =
|
|
this->r_reservation.at(0)->call_exists_reservation(reservation_check_request);
|
|
|
|
return ocpp_conversions::to_ocpp_reservation_check_status(reservation_status);
|
|
};
|
|
|
|
callbacks.update_firmware_request_callback = [this](const ocpp::v2::UpdateFirmwareRequest& request) {
|
|
auto req = conversions::to_everest_firmware_update_request(request);
|
|
if (req.retries.has_value()) {
|
|
req.retries = req.retries.value() + 1;
|
|
}
|
|
const auto response = this->r_system->call_update_firmware(req);
|
|
return conversions::to_ocpp_update_firmware_response(response);
|
|
};
|
|
|
|
callbacks.variable_changed_callback = [this](const ocpp::v2::SetVariableData& set_variable_data) {
|
|
if (set_variable_data.component == ocpp::v2::ControllerComponents::TxCtrlr and
|
|
set_variable_data.variable.name.get() == EV_CONNECTION_TIMEOUT_VAR_NAME) {
|
|
try {
|
|
auto ev_connection_timeout = std::stoi(set_variable_data.attributeValue.get());
|
|
this->r_auth->call_set_connection_timeout(ev_connection_timeout);
|
|
} catch (const std::exception& e) {
|
|
EVLOG_error << "Could not parse EVConnectionTimeOut and did not set it in Auth module, error: "
|
|
<< e.what();
|
|
return;
|
|
}
|
|
} else if (set_variable_data.component == ocpp::v2::ControllerComponents::AuthCtrlr and
|
|
set_variable_data.variable.name.get() == MASTER_PASS_GROUP_ID_VAR_NAME) {
|
|
this->r_auth->call_set_master_pass_group_id(set_variable_data.attributeValue.get());
|
|
} else if (set_variable_data.component == ocpp::v2::ControllerComponents::TxCtrlr and
|
|
set_variable_data.variable.name.get() == TX_START_POINT_VAR_NAME) {
|
|
const auto tx_start_points = get_tx_start_stop_points(set_variable_data.attributeValue.get());
|
|
if (tx_start_points.empty()) {
|
|
EVLOG_warning << "Could not set TxStartPoints";
|
|
return;
|
|
}
|
|
this->transaction_handler->set_tx_start_points(tx_start_points);
|
|
} else if (set_variable_data.component == ocpp::v2::ControllerComponents::TxCtrlr and
|
|
set_variable_data.variable.name.get() == TX_STOP_POINT_VAR_NAME) {
|
|
const auto tx_stop_points = get_tx_start_stop_points(set_variable_data.attributeValue.get());
|
|
if (tx_stop_points.empty()) {
|
|
EVLOG_warning << "Could not set TxStartPoints";
|
|
return;
|
|
}
|
|
this->transaction_handler->set_tx_stop_points(tx_stop_points);
|
|
} else if (set_variable_data.component == ocpp::v2::ControllerComponents::ISO15118Ctrlr and
|
|
set_variable_data.variable.name.get() == PNC_ENABLED_VAR_NAME) {
|
|
types::evse_manager::PlugAndChargeConfiguration pnc_config;
|
|
pnc_config.pnc_enabled = ocpp::conversions::string_to_bool(set_variable_data.attributeValue.get());
|
|
for (const auto& evse_manager : this->r_evse_manager) {
|
|
evse_manager->call_set_plug_and_charge_configuration(pnc_config);
|
|
}
|
|
} else if (set_variable_data.component == ocpp::v2::ControllerComponents::ISO15118Ctrlr and
|
|
set_variable_data.variable.name.get() == CENTRAL_CONTRACT_VALIDATION_ALLOWED_VAR_NAME) {
|
|
types::evse_manager::PlugAndChargeConfiguration pnc_config;
|
|
pnc_config.central_contract_validation_allowed =
|
|
ocpp::conversions::string_to_bool(set_variable_data.attributeValue.get());
|
|
for (const auto& evse_manager : this->r_evse_manager) {
|
|
evse_manager->call_set_plug_and_charge_configuration(pnc_config);
|
|
}
|
|
} else if (set_variable_data.component == ocpp::v2::ControllerComponents::ISO15118Ctrlr and
|
|
set_variable_data.variable.name.get() == CONTRACT_CERTIFICATE_INSTALLATION_ENABLED_VAR_NAME) {
|
|
types::evse_manager::PlugAndChargeConfiguration pnc_config;
|
|
pnc_config.contract_certificate_installation_enabled =
|
|
ocpp::conversions::string_to_bool(set_variable_data.attributeValue.get());
|
|
for (const auto& evse_manager : this->r_evse_manager) {
|
|
evse_manager->call_set_plug_and_charge_configuration(pnc_config);
|
|
}
|
|
}
|
|
};
|
|
|
|
callbacks.validate_network_profile_callback =
|
|
[this](const int32_t configuration_slot, const ocpp::v2::NetworkConnectionProfile& network_connection_profile) {
|
|
auto ws_uri = ocpp::uri(network_connection_profile.ocppCsmsUrl.get());
|
|
|
|
if (ws_uri.get_valid()) {
|
|
return ocpp::v2::SetNetworkProfileStatusEnum::Accepted;
|
|
} else {
|
|
return ocpp::v2::SetNetworkProfileStatusEnum::Rejected;
|
|
}
|
|
// TODO(piet): Add further validation of the NetworkConnectionProfile
|
|
};
|
|
|
|
callbacks.configure_network_connection_profile_callback =
|
|
[this](const int32_t configuration_slot, const ocpp::v2::NetworkConnectionProfile& network_connection_profile) {
|
|
std::promise<ocpp::ConfigNetworkResult> promise;
|
|
std::future<ocpp::ConfigNetworkResult> future = promise.get_future();
|
|
ocpp::ConfigNetworkResult result;
|
|
result.success = true;
|
|
promise.set_value(result);
|
|
return future;
|
|
};
|
|
|
|
callbacks.all_connectors_unavailable_callback = [this]() {
|
|
EVLOG_info << "All connectors unavailable, proceed with firmware installation";
|
|
this->r_system->call_allow_firmware_installation();
|
|
};
|
|
|
|
callbacks.transaction_event_callback = [this](const ocpp::v2::TransactionEventRequest& transaction_event) {
|
|
const auto ocpp_transaction_event = conversions::to_everest_ocpp_transaction_event(transaction_event);
|
|
this->p_ocpp_generic->publish_ocpp_transaction_event(ocpp_transaction_event);
|
|
};
|
|
|
|
callbacks.transaction_event_response_callback =
|
|
[this](const ocpp::v2::TransactionEventRequest& transaction_event,
|
|
const ocpp::v2::TransactionEventResponse& transaction_event_response) {
|
|
auto ocpp_transaction_event = conversions::to_everest_ocpp_transaction_event(transaction_event);
|
|
auto ocpp_transaction_event_response =
|
|
conversions::to_everest_transaction_event_response(transaction_event_response);
|
|
ocpp_transaction_event_response.original_transaction_event = ocpp_transaction_event;
|
|
this->p_ocpp_generic->publish_ocpp_transaction_event_response(ocpp_transaction_event_response);
|
|
if (transaction_event_response.idTokenInfo.has_value() and transaction_event.evse.has_value()) {
|
|
types::authorization::ValidationResultUpdate result_update;
|
|
result_update.validation_result =
|
|
conversions::to_everest_validation_result(transaction_event_response.idTokenInfo.value());
|
|
result_update.connector_id = transaction_event.evse->id;
|
|
p_auth_validator->publish_validate_result_update(result_update);
|
|
}
|
|
};
|
|
|
|
callbacks.boot_notification_callback =
|
|
[this](const ocpp::v2::BootNotificationResponse& boot_notification_response) {
|
|
const auto everest_boot_notification_response =
|
|
conversions::to_everest_boot_notification_response(boot_notification_response);
|
|
this->p_ocpp_generic->publish_boot_notification_response(everest_boot_notification_response);
|
|
};
|
|
|
|
callbacks.set_display_message_callback =
|
|
[this](const std::vector<ocpp::DisplayMessage>& messages) -> ocpp::v2::SetDisplayMessageResponse {
|
|
ocpp::v2::SetDisplayMessageResponse response;
|
|
if (this->r_display_message.empty()) {
|
|
response.status = ocpp::v2::DisplayMessageStatusEnum::Rejected;
|
|
return response;
|
|
}
|
|
|
|
std::vector<types::display_message::DisplayMessage> display_messages;
|
|
for (const ocpp::DisplayMessage& message : messages) {
|
|
const types::display_message::DisplayMessage m = ocpp_conversions::to_everest_display_message(message);
|
|
display_messages.push_back(m);
|
|
}
|
|
|
|
const types::display_message::SetDisplayMessageResponse display_message_response =
|
|
this->r_display_message.at(0)->call_set_display_message(display_messages);
|
|
response = conversions::to_ocpp_set_display_message_response(display_message_response);
|
|
|
|
return response;
|
|
};
|
|
|
|
callbacks.get_display_message_callback =
|
|
[this](const ocpp::v2::GetDisplayMessagesRequest& request) -> std::vector<ocpp::DisplayMessage> {
|
|
if (this->r_display_message.empty()) {
|
|
return {};
|
|
}
|
|
types::display_message::GetDisplayMessageRequest get_request;
|
|
|
|
types::display_message::GetDisplayMessageResponse response =
|
|
this->r_display_message.at(0)->call_get_display_messages(
|
|
conversions::to_everest_display_message_request(request));
|
|
|
|
if (!response.messages.has_value() || response.messages.value().empty()) {
|
|
return {};
|
|
}
|
|
|
|
std::vector<ocpp::DisplayMessage> ocpp_display_messages;
|
|
for (const auto& message : response.messages.value()) {
|
|
ocpp_display_messages.push_back(ocpp_conversions::to_ocpp_display_message(message));
|
|
}
|
|
|
|
return ocpp_display_messages;
|
|
};
|
|
|
|
callbacks.clear_display_message_callback =
|
|
[this](const ocpp::v2::ClearDisplayMessageRequest& request) -> ocpp::v2::ClearDisplayMessageResponse {
|
|
if (this->r_display_message.empty()) {
|
|
ocpp::v2::ClearDisplayMessageResponse response;
|
|
response.status = ocpp::v2::ClearMessageStatusEnum::Unknown;
|
|
return response;
|
|
}
|
|
|
|
types::display_message::ClearDisplayMessageResponse response =
|
|
this->r_display_message.at(0)->call_clear_display_message(
|
|
conversions::to_everest_clear_display_message_request(request));
|
|
return conversions::to_ocpp_clear_display_message_response(response);
|
|
};
|
|
|
|
if (this->p_session_cost != nullptr) {
|
|
callbacks.set_running_cost_callback = [this](const ocpp::RunningCost& running_cost,
|
|
const uint32_t number_of_decimals,
|
|
const std::optional<std::string>& currency_code) {
|
|
std::optional<types::money::CurrencyCode> currency;
|
|
if (currency_code.has_value()) {
|
|
try {
|
|
currency = types::money::string_to_currency_code(currency_code.value());
|
|
} catch (const std::out_of_range& e) {
|
|
// If conversion fails, we just don't add the currency code. But we want to see it in the
|
|
// logging.
|
|
EVLOG_error << e.what();
|
|
}
|
|
}
|
|
const types::session_cost::SessionCost cost =
|
|
ocpp_conversions::create_session_cost(running_cost, number_of_decimals, currency);
|
|
this->p_session_cost->publish_session_cost(cost);
|
|
};
|
|
|
|
callbacks.tariff_message_callback = [this](const ocpp::TariffMessage& message) {
|
|
const types::session_cost::TariffMessage m = ocpp_conversions::to_everest_tariff_message(message);
|
|
this->p_session_cost->publish_tariff_message(m);
|
|
};
|
|
|
|
callbacks.default_price_callback = [this](const std::vector<ocpp::DisplayMessageContent>& messages) {
|
|
const types::session_cost::DefaultPrice p = ocpp_conversions::to_everest_default_price(messages);
|
|
this->p_session_cost->publish_default_price(p);
|
|
};
|
|
}
|
|
|
|
if (!this->r_data_transfer.empty()) {
|
|
callbacks.data_transfer_callback = [this](const ocpp::v2::DataTransferRequest& request) {
|
|
types::ocpp::DataTransferRequest data_transfer_request =
|
|
conversions::to_everest_data_transfer_request(request);
|
|
types::ocpp::DataTransferResponse data_transfer_response =
|
|
this->r_data_transfer.at(0)->call_data_transfer(data_transfer_request);
|
|
ocpp::v2::DataTransferResponse response =
|
|
conversions::to_ocpp_data_transfer_response(data_transfer_response);
|
|
return response;
|
|
};
|
|
}
|
|
|
|
callbacks.connection_state_changed_callback =
|
|
[this](const bool is_connected, const int /*configuration_slot*/,
|
|
const ocpp::v2::NetworkConnectionProfile& /*network_connection_profile*/,
|
|
const ocpp::OcppProtocolVersion protocol_version) {
|
|
if (is_connected) {
|
|
ocpp_protocol_version = protocol_version;
|
|
} else {
|
|
ocpp_protocol_version = ocpp::OcppProtocolVersion::Unknown;
|
|
}
|
|
this->p_ocpp_generic->publish_is_connected(is_connected);
|
|
};
|
|
|
|
callbacks.security_event_callback = [this](const ocpp::CiString<50>& event_type,
|
|
const std::optional<ocpp::CiString<255>>& tech_info) {
|
|
types::ocpp::SecurityEvent event;
|
|
event.type = event_type.get();
|
|
EVLOG_info << "Security Event in OCPP occurred: " << event.type;
|
|
if (tech_info.has_value()) {
|
|
event.info = tech_info.value().get();
|
|
}
|
|
this->p_ocpp_generic->publish_security_event(event);
|
|
};
|
|
|
|
// this callback publishes the schedules within EVerest and applies the schedules for the individual
|
|
// r_evse_energy_sink
|
|
const auto charging_schedules_callback = [this]() { charging_schedules_timer_callback(); };
|
|
|
|
callbacks.set_charging_profiles_callback = charging_schedules_callback;
|
|
|
|
callbacks.time_sync_callback = [this](const ocpp::DateTime& current_time) {
|
|
this->r_system->call_set_system_time(current_time.to_rfc3339());
|
|
};
|
|
|
|
callbacks.reserve_now_callback =
|
|
[this](const ocpp::v2::ReserveNowRequest& request) -> ocpp::v2::ReserveNowStatusEnum {
|
|
ocpp::v2::ReserveNowResponse response;
|
|
if (this->r_reservation.empty() || this->r_reservation.at(0) == nullptr) {
|
|
EVLOG_info << "Reservation rejected because the interface r_reservation is a nullptr";
|
|
return ocpp::v2::ReserveNowStatusEnum::Rejected;
|
|
}
|
|
|
|
types::reservation::Reservation reservation;
|
|
reservation.reservation_id = request.id;
|
|
reservation.expiry_time = request.expiryDateTime.to_rfc3339();
|
|
reservation.id_token = request.idToken.idToken;
|
|
reservation.evse_id = request.evseId;
|
|
if (request.groupIdToken.has_value()) {
|
|
reservation.parent_id_token = request.groupIdToken.value().idToken;
|
|
}
|
|
if (request.connectorType.has_value()) {
|
|
reservation.connector_type =
|
|
types::evse_manager::string_to_connector_type_enum(request.connectorType.value());
|
|
}
|
|
|
|
types::reservation::ReservationResult result = this->r_reservation.at(0)->call_reserve_now(reservation);
|
|
return conversions::to_ocpp_reservation_status(result);
|
|
};
|
|
|
|
callbacks.cancel_reservation_callback = [this](const int32_t reservation_id) -> bool {
|
|
EVLOG_debug << "Received cancel reservation request for reservation id " << reservation_id;
|
|
if (this->r_reservation.empty() || this->r_reservation.at(0) == nullptr) {
|
|
return false;
|
|
}
|
|
|
|
return this->r_reservation.at(0)->call_cancel_reservation(reservation_id);
|
|
};
|
|
|
|
callbacks.update_allowed_energy_transfer_modes_callback =
|
|
[this](const std::vector<ocpp::v2::EnergyTransferModeEnum>& allowed_energy_transfer_modes,
|
|
const ocpp::CiString<36>& transaction_id) -> bool {
|
|
const int evse_id = transaction_handler->get_evse_id(transaction_id);
|
|
if (evse_id == -1 || evse_id > this->r_evse_manager.size()) {
|
|
return false;
|
|
}
|
|
auto& evse = this->r_evse_manager.at(evse_id - 1); // evse_id starts at 1 if valid
|
|
|
|
if (evse != nullptr) {
|
|
return evse->call_update_allowed_energy_transfer_modes(
|
|
conversions::to_everest_allowed_energy_transfer_modes(allowed_energy_transfer_modes)) ==
|
|
types::evse_manager::UpdateAllowedEnergyTransferModesResult::Accepted;
|
|
}
|
|
return false;
|
|
};
|
|
|
|
callbacks.ocpp_messages_callback = [this](const std::string& message, ocpp::MessageDirection direction) {
|
|
switch (direction) {
|
|
case ocpp::MessageDirection::CSMSToChargingStation: {
|
|
types::ocpp::Message ocpp_message;
|
|
ocpp_message.message = message;
|
|
ocpp_message.version = ocpp_protocol_version_to_string(this->ocpp_protocol_version);
|
|
ocpp_message.direction = types::ocpp::MessageDirection::CSMSToChargingStation;
|
|
p_ocpp_generic->publish_ocpp_message(ocpp_message);
|
|
break;
|
|
}
|
|
case ocpp::MessageDirection::ChargingStationToCSMS: {
|
|
types::ocpp::Message ocpp_message;
|
|
ocpp_message.message = message;
|
|
ocpp_message.version = ocpp_protocol_version_to_string(this->ocpp_protocol_version);
|
|
ocpp_message.direction = types::ocpp::MessageDirection::ChargingStationToCSMS;
|
|
p_ocpp_generic->publish_ocpp_message(ocpp_message);
|
|
break;
|
|
}
|
|
default:
|
|
// unknown message direction (ignored)
|
|
break;
|
|
}
|
|
};
|
|
|
|
{
|
|
auto ready_handle = this->evse_ready_map.handle();
|
|
ready_handle.wait([this, &ready_handle]() {
|
|
for (const auto& [evse, ready] : *ready_handle) {
|
|
if (!ready) {
|
|
return false;
|
|
}
|
|
}
|
|
EVLOG_info << "All EVSE ready. Starting OCPP2.X service";
|
|
return true;
|
|
});
|
|
}
|
|
|
|
const auto sql_init_path = this->ocpp_share_path / SQL_CORE_MIGRATIONS;
|
|
|
|
std::map<int32_t, int32_t> evse_connector_structure = this->get_connector_structure();
|
|
|
|
// initialize libocpp device model
|
|
auto libocpp_device_model_storage = std::make_shared<ocpp::v2::DeviceModelStorageSqlite>(
|
|
device_model_database_path, device_model_database_migration_path, device_model_config_path);
|
|
|
|
// initialize everest device model
|
|
this->everest_device_model_storage = std::make_shared<device_model::EverestDeviceModelStorage>(
|
|
r_evse_manager, r_extensions_15118, this->evse_hardware_capabilities_map,
|
|
this->evse_supported_energy_transfer_modes, this->evse_service_renegotiation_supported,
|
|
everest_device_model_database_path, device_model_database_migration_path, get_config_service_client());
|
|
|
|
// initialize composed device model, this will be provided to the ChargePoint constructor
|
|
auto composed_device_model_storage = std::make_unique<module::device_model::ComposedDeviceModelStorage>();
|
|
|
|
// register both device model storages
|
|
composed_device_model_storage->register_device_model_storage("OCPP", std::move(libocpp_device_model_storage));
|
|
composed_device_model_storage->register_device_model_storage("EVEREST", this->everest_device_model_storage);
|
|
|
|
this->charge_point = std::make_unique<ocpp::v2::ChargePoint>(
|
|
evse_connector_structure, std::move(composed_device_model_storage), this->ocpp_share_path.string(),
|
|
this->config.CoreDatabasePath, sql_init_path.string(), this->config.MessageLogPath,
|
|
std::make_shared<EvseSecurity>(*this->r_security), callbacks);
|
|
|
|
// publish charging schedules at least once on startup
|
|
charging_schedules_callback();
|
|
charging_schedules_timer_start();
|
|
|
|
this->init_module_configuration();
|
|
|
|
if (this->config.EnableExternalWebsocketControl) {
|
|
const std::string connect_topic = "everest_api/ocpp/cmd/connect";
|
|
this->mqtt.subscribe(connect_topic,
|
|
[this](const std::string& data) { this->charge_point->connect_websocket(); });
|
|
|
|
const std::string disconnect_topic = "everest_api/ocpp/cmd/disconnect";
|
|
this->mqtt.subscribe(disconnect_topic,
|
|
[this](const std::string& data) { this->charge_point->disconnect_websocket(); });
|
|
}
|
|
|
|
std::set<TxStartStopPoint> tx_start_points;
|
|
std::set<TxStartStopPoint> tx_stop_points;
|
|
|
|
const auto tx_start_point_request_value_response = this->charge_point->request_value<std::string>(
|
|
ocpp::v2::Component{"TxCtrlr"}, ocpp::v2::Variable{TX_START_POINT_VAR_NAME}, ocpp::v2::AttributeEnum::Actual);
|
|
if (tx_start_point_request_value_response.status == ocpp::v2::GetVariableStatusEnum::Accepted and
|
|
tx_start_point_request_value_response.value.has_value()) {
|
|
auto tx_start_point_csl =
|
|
tx_start_point_request_value_response.value.value(); // contains comma seperated list of TxStartPoints
|
|
tx_start_points = get_tx_start_stop_points(tx_start_point_csl);
|
|
EVLOG_info << "TxStartPoints from device model: " << tx_start_point_csl;
|
|
}
|
|
|
|
if (tx_start_points.empty()) {
|
|
tx_start_points = {TxStartStopPoint::PowerPathClosed};
|
|
}
|
|
|
|
const auto tx_stop_point_request_value_response = this->charge_point->request_value<std::string>(
|
|
ocpp::v2::Component{"TxCtrlr"}, ocpp::v2::Variable{TX_STOP_POINT_VAR_NAME}, ocpp::v2::AttributeEnum::Actual);
|
|
if (tx_stop_point_request_value_response.status == ocpp::v2::GetVariableStatusEnum::Accepted and
|
|
tx_stop_point_request_value_response.value.has_value()) {
|
|
auto tx_stop_point_csl =
|
|
tx_stop_point_request_value_response.value.value(); // contains comma seperated list of TxStartPoints
|
|
tx_stop_points = get_tx_start_stop_points(tx_stop_point_csl);
|
|
EVLOG_info << "TxStopPoints from device model: " << tx_stop_point_csl;
|
|
}
|
|
|
|
if (tx_stop_points.empty()) {
|
|
tx_stop_points = {TxStartStopPoint::EVConnected, TxStartStopPoint::Authorized};
|
|
}
|
|
|
|
this->transaction_handler =
|
|
std::make_unique<TransactionHandler>(this->r_evse_manager.size(), tx_start_points, tx_stop_points);
|
|
|
|
const auto boot_reason = conversions::to_ocpp_boot_reason(this->r_system->call_get_boot_reason());
|
|
this->charge_point->set_message_queue_resume_delay(std::chrono::seconds(this->config.MessageQueueResumeDelay));
|
|
// we can now initialize the charge point's state machine. It reads the connector availability from the internal
|
|
// database and potentially triggers enable/disable callbacks at the evse.
|
|
this->charge_point->start(boot_reason, false);
|
|
this->started = true;
|
|
|
|
// Signal to EVSEs to start their internal state machines
|
|
for (const auto& evse : this->r_evse_manager) {
|
|
evse->call_external_ready_to_start_charging();
|
|
}
|
|
|
|
// wait for potential events from the evses in order to start OCPP with the correct initial state (e.g. EV might
|
|
// be plugged in at startup)
|
|
std::this_thread::sleep_for(std::chrono::milliseconds(this->config.DelayOcppStart));
|
|
// start OCPP connection
|
|
this->charge_point->connect_websocket();
|
|
|
|
// process event queue
|
|
for (auto& [evse_id, evse_event_queue] : this->event_queue) {
|
|
while (!evse_event_queue.empty()) {
|
|
auto queued_event = evse_event_queue.front();
|
|
if (std::holds_alternative<types::evse_manager::SessionEvent>(queued_event)) {
|
|
const auto session_event = std::get<types::evse_manager::SessionEvent>(queued_event);
|
|
EVLOG_info << "Processing queued event for evse_id: " << evse_id << ", event: " << session_event.event;
|
|
this->process_session_event(evse_id, session_event);
|
|
} else if (std::holds_alternative<Everest::error::Error>(queued_event)) {
|
|
const auto& error = std::get<Everest::error::Error>(queued_event);
|
|
EVLOG_info << "Processing queued error event for evse_id: " << evse_id << ": " << error.type;
|
|
bool is_active = error.state == Everest::error::State::Active;
|
|
const auto event_data = get_event_data(error, !is_active, this->event_id_counter++);
|
|
this->charge_point->on_event({event_data});
|
|
|
|
// We do only report inoperative errors as faults
|
|
if (error.type == EVSE_MANAGER_INOPERATIVE_ERROR) {
|
|
if (is_active) {
|
|
this->charge_point->on_faulted(evse_id, get_connector_id_from_error(error));
|
|
} else {
|
|
this->charge_point->on_fault_cleared(evse_id, get_connector_id_from_error(error));
|
|
}
|
|
}
|
|
} else if (std::holds_alternative<ocpp::v2::MeterValue>(queued_event)) {
|
|
const auto meter_value = std::get<ocpp::v2::MeterValue>(queued_event);
|
|
EVLOG_info << "Processing queued meter value for evse_id: " << evse_id;
|
|
this->charge_point->on_meter_value(evse_id, meter_value);
|
|
} else if (std::holds_alternative<types::system::FirmwareUpdateStatus>(queued_event)) {
|
|
const auto fw_update_status = std::get<types::system::FirmwareUpdateStatus>(queued_event);
|
|
EVLOG_info << "Processing queued firmware update status";
|
|
this->charge_point->on_firmware_update_status_notification(
|
|
fw_update_status.request_id,
|
|
conversions::to_ocpp_firmware_status_enum(fw_update_status.firmware_update_status));
|
|
} else if (std::holds_alternative<types::system::LogStatus>(queued_event)) {
|
|
const auto log_status = std::get<types::system::LogStatus>(queued_event);
|
|
EVLOG_info << "Processing queued log status";
|
|
this->charge_point->on_log_status_notification(
|
|
conversions::to_ocpp_upload_logs_status_enum(log_status.log_status), log_status.request_id);
|
|
} else {
|
|
EVLOG_warning << "Unknown event type in queue for evse_id: " << evse_id;
|
|
}
|
|
evse_event_queue.pop();
|
|
}
|
|
}
|
|
}
|
|
|
|
void OCPP201::charging_schedules_timer_callback() {
|
|
// this callback publishes the schedules within EVerest and applies the schedules for the individual
|
|
// r_evse_energy_sink
|
|
const auto composite_schedule_unit = get_unit_or_default(config.RequestCompositeScheduleUnit);
|
|
const auto composite_schedules =
|
|
charge_point->get_all_composite_schedules(config.RequestCompositeScheduleDurationS, composite_schedule_unit);
|
|
publish_charging_schedules(composite_schedules);
|
|
set_external_limits(composite_schedules);
|
|
}
|
|
|
|
void OCPP201::charging_schedules_timer_start() {
|
|
if (config.CompositeScheduleIntervalS > 0) {
|
|
const auto charging_schedules_callback = [this]() { charging_schedules_timer_callback(); };
|
|
charging_schedules_timer.interval(charging_schedules_callback,
|
|
std::chrono::seconds(config.CompositeScheduleIntervalS));
|
|
}
|
|
}
|
|
|
|
void OCPP201::charging_schedules_timer_stop() {
|
|
charging_schedules_timer.stop();
|
|
}
|
|
|
|
void OCPP201::init_evse_subscriptions() {
|
|
int evse_id = 1;
|
|
for (const auto& evse : this->r_evse_manager) {
|
|
evse->subscribe_waiting_for_external_ready([this, evse_id](bool ready) {
|
|
if (ready) {
|
|
this->evse_ready_map.handle()->at(evse_id) = true;
|
|
this->evse_ready_map.notify_one();
|
|
}
|
|
});
|
|
|
|
evse->subscribe_ready([this, evse_id](bool ready) {
|
|
if (ready) {
|
|
EVLOG_info << "EVSE " << evse_id << " ready.";
|
|
this->evse_ready_map.handle()->at(evse_id) = true;
|
|
this->evse_ready_map.notify_one();
|
|
}
|
|
});
|
|
|
|
evse->subscribe_hw_capabilities(
|
|
[this, evse_id](const types::evse_board_support::HardwareCapabilities& hw_capabilities) {
|
|
this->evse_hardware_capabilities_map[evse_id] = hw_capabilities;
|
|
});
|
|
|
|
evse->subscribe_supported_energy_transfer_modes(
|
|
[this, evse_id](const std::vector<types::iso15118::EnergyTransferMode>& supported_energy_transfer_modes) {
|
|
this->evse_supported_energy_transfer_modes[evse_id] = supported_energy_transfer_modes;
|
|
});
|
|
|
|
evse->subscribe_session_event([this, evse_id](types::evse_manager::SessionEvent session_event) {
|
|
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->event_queue[evse_id].push(session_event);
|
|
return;
|
|
}
|
|
this->process_session_event(evse_id, session_event);
|
|
});
|
|
|
|
evse->subscribe_powermeter([this, evse_id](const types::powermeter::Powermeter& power_meter) {
|
|
ocpp::v2::MeterValue meter_value = conversions::to_ocpp_meter_value(
|
|
power_meter, ocpp::v2::ReadingContextEnum::Sample_Periodic, power_meter.signed_meter_value);
|
|
if (!this->started) {
|
|
std::scoped_lock lock(this->session_event_mutex);
|
|
this->event_queue[evse_id].push(meter_value);
|
|
return;
|
|
}
|
|
auto evse_soc_map_handle = this->evse_soc_map.handle();
|
|
if (evse_soc_map_handle->at(evse_id).has_value()) {
|
|
auto sampled_soc_value = conversions::to_ocpp_sampled_value(
|
|
ocpp::v2::ReadingContextEnum::Sample_Periodic, ocpp::v2::MeasurandEnum::SoC, "Percent",
|
|
std::nullopt, ocpp::v2::LocationEnum::EV);
|
|
sampled_soc_value.value = evse_soc_map_handle->at(evse_id).value();
|
|
meter_value.sampledValue.push_back(sampled_soc_value);
|
|
}
|
|
this->charge_point->on_meter_value(evse_id, meter_value);
|
|
const auto total_power_active_import = ocpp::v2::utils::get_total_power_active_import(meter_value);
|
|
if (total_power_active_import.has_value()) {
|
|
this->everest_device_model_storage->update_power(evse_id, total_power_active_import.value());
|
|
}
|
|
});
|
|
|
|
evse->subscribe_ev_info([this, evse_id](const types::evse_manager::EVInfo& ev_info) {
|
|
if (!this->started) {
|
|
EVLOG_info << "EV Info received from evse_manager before DM was instantiated, ignoring...";
|
|
EVLOG_info << "EV Info will be retrieved later";
|
|
return;
|
|
}
|
|
if (ev_info.soc.has_value()) {
|
|
this->evse_soc_map.handle()->at(evse_id) = ev_info.soc;
|
|
}
|
|
if (ev_info.evcc_id.has_value()) {
|
|
this->evse_evcc_id.handle()->at(evse_id) = ev_info.evcc_id.value();
|
|
this->everest_device_model_storage->update_connected_ev_vehicle_id(evse_id, ev_info.evcc_id.value());
|
|
}
|
|
});
|
|
|
|
auto fault_handler = [this, evse_id](const Everest::error::Error& error) {
|
|
if (this->started) {
|
|
const auto event_data = get_event_data(error, false, this->event_id_counter++);
|
|
this->charge_point->on_event({event_data});
|
|
this->charge_point->on_faulted(evse_id, get_connector_id_from_error(error));
|
|
} else {
|
|
std::scoped_lock lock(this->session_event_mutex);
|
|
this->event_queue[get_component_from_error(error).evse.value_or(ocpp::v2::EVSE{1}).id].push(error);
|
|
}
|
|
};
|
|
|
|
auto fault_cleared_handler = [this, evse_id](const Everest::error::Error& error) {
|
|
if (this->started) {
|
|
const auto event_data = get_event_data(error, true, this->event_id_counter++);
|
|
this->charge_point->on_event({event_data});
|
|
this->charge_point->on_fault_cleared(evse_id, get_connector_id_from_error(error));
|
|
} else {
|
|
std::scoped_lock lock(this->session_event_mutex);
|
|
this->event_queue[get_component_from_error(error).evse.value_or(ocpp::v2::EVSE{1}).id].push(error);
|
|
}
|
|
};
|
|
|
|
// A permanent fault from the evse requirement indicates that the evse should move to faulted state
|
|
evse->subscribe_error(EVSE_MANAGER_INOPERATIVE_ERROR, fault_handler, fault_cleared_handler);
|
|
|
|
evse_id++;
|
|
}
|
|
|
|
int32_t extensions_id = 0;
|
|
for (const auto& extension : this->r_extensions_15118) {
|
|
extension->subscribe_iso15118_certificate_request(
|
|
[this, extensions_id](const types::iso15118::RequestExiStreamSchema& certificate_request) {
|
|
if (!this->started) {
|
|
EVLOG_info << "ISO15118 certificate_request received before OCPP was initialized, ignoring";
|
|
return;
|
|
}
|
|
auto ocpp_response = this->charge_point->on_get_15118_ev_certificate_request(
|
|
conversions::to_ocpp_get_15118_certificate_request(certificate_request));
|
|
EVLOG_debug << "Received response from get_15118_ev_certificate_request: " << ocpp_response;
|
|
// transform response, inject action, send to associated EvseManager
|
|
types::iso15118::ResponseExiStreamStatus everest_response;
|
|
everest_response.status = conversions::to_everest_iso15118_status(ocpp_response.status);
|
|
everest_response.certificate_action = certificate_request.certificate_action;
|
|
if (not ocpp_response.exiResponse.get().empty()) {
|
|
// since exi_response is an optional in the EVerest type we only set it when not empty
|
|
everest_response.exi_response = ocpp_response.exiResponse.get();
|
|
}
|
|
|
|
this->r_extensions_15118.at(extensions_id)->call_set_get_certificate_response(everest_response);
|
|
});
|
|
|
|
extension->subscribe_charging_needs([this,
|
|
extensions_id](const types::iso15118::ChargingNeeds& charging_needs) {
|
|
if (!this->started) {
|
|
EVLOG_info << "ISO15118 charging_needs received before OCPP was initialized, ignoring";
|
|
return;
|
|
}
|
|
const auto& mapping = this->r_extensions_15118.at(extensions_id)->get_mapping();
|
|
if (mapping.has_value()) {
|
|
try {
|
|
ocpp::v2::NotifyEVChargingNeedsRequest charge_needs;
|
|
charge_needs.chargingNeeds = conversions::to_ocpp_charging_needs(charging_needs);
|
|
charge_needs.evseId = mapping.value().evse;
|
|
|
|
this->charge_point->on_ev_charging_needs(charge_needs);
|
|
} catch (const std::out_of_range& e) {
|
|
EVLOG_warning << "Could not convert iso15118 ChargingNeeds to OCPP NotifyEVChargingNeedsRequest: "
|
|
<< e.what();
|
|
}
|
|
} else {
|
|
EVLOG_warning << "ISO15118 Extension interface mapping not set! Not sending 'ChargingNeeds'!";
|
|
}
|
|
});
|
|
|
|
extension->subscribe_service_renegotiation_supported(
|
|
[this, extensions_id](bool service_renegotiation_supported) {
|
|
const auto& mapping = this->r_extensions_15118.at(extensions_id)->get_mapping();
|
|
if (mapping.has_value()) {
|
|
this->evse_service_renegotiation_supported[mapping->evse] = service_renegotiation_supported;
|
|
} else {
|
|
EVLOG_warning << "ISO15118 Extension interface mapping not set! Not retrieving 'Service "
|
|
"Renegotiation Supported'!";
|
|
}
|
|
});
|
|
|
|
extensions_id++;
|
|
}
|
|
}
|
|
|
|
void OCPP201::process_session_event(const int32_t evse_id, const types::evse_manager::SessionEvent& session_event) {
|
|
const auto connector_id = session_event.connector_id.value_or(1);
|
|
std::lock_guard<std::mutex> lg(this->session_event_mutex);
|
|
switch (session_event.event) {
|
|
case types::evse_manager::SessionEventEnum::SessionStarted: {
|
|
this->process_session_started(evse_id, connector_id, session_event);
|
|
break;
|
|
}
|
|
case types::evse_manager::SessionEventEnum::SessionFinished: {
|
|
this->process_session_finished(evse_id, connector_id, session_event);
|
|
break;
|
|
}
|
|
case types::evse_manager::SessionEventEnum::TransactionStarted: {
|
|
this->process_transaction_started(evse_id, connector_id, session_event);
|
|
break;
|
|
}
|
|
case types::evse_manager::SessionEventEnum::TransactionFinished: {
|
|
this->process_transaction_finished(evse_id, connector_id, session_event);
|
|
break;
|
|
}
|
|
case types::evse_manager::SessionEventEnum::SessionResumed:
|
|
this->process_session_resumed(evse_id, connector_id, session_event);
|
|
break;
|
|
case types::evse_manager::SessionEventEnum::ChargingStarted: {
|
|
this->process_charging_started(evse_id, connector_id, session_event);
|
|
break;
|
|
}
|
|
case types::evse_manager::SessionEventEnum::ChargingPausedEV: {
|
|
this->process_charging_paused_ev(evse_id, connector_id, session_event);
|
|
break;
|
|
}
|
|
case types::evse_manager::SessionEventEnum::ChargingPausedEVSE: {
|
|
this->process_charging_paused_evse(evse_id, connector_id, session_event);
|
|
break;
|
|
}
|
|
case types::evse_manager::SessionEventEnum::Disabled: {
|
|
this->process_disabled(evse_id, connector_id, session_event);
|
|
break;
|
|
}
|
|
case types::evse_manager::SessionEventEnum::Enabled: {
|
|
this->process_enabled(evse_id, connector_id, session_event);
|
|
break;
|
|
}
|
|
case types::evse_manager::SessionEventEnum::Authorized: {
|
|
this->process_authorized(evse_id, connector_id, session_event);
|
|
break;
|
|
}
|
|
case types::evse_manager::SessionEventEnum::Deauthorized: {
|
|
this->process_deauthorized(evse_id, connector_id, session_event);
|
|
break;
|
|
}
|
|
case types::evse_manager::SessionEventEnum::ReservationStart: {
|
|
this->process_reserved(evse_id, connector_id);
|
|
break;
|
|
}
|
|
case types::evse_manager::SessionEventEnum::ReservationEnd: {
|
|
this->process_reservation_end(evse_id, connector_id);
|
|
break;
|
|
}
|
|
// explicitly ignore the following session events for now
|
|
// TODO(kai): implement
|
|
case types::evse_manager::SessionEventEnum::AuthRequired:
|
|
case types::evse_manager::SessionEventEnum::PrepareCharging:
|
|
case types::evse_manager::SessionEventEnum::StoppingCharging:
|
|
case types::evse_manager::SessionEventEnum::ChargingFinished:
|
|
case types::evse_manager::SessionEventEnum::PluginTimeout:
|
|
case types::evse_manager::SessionEventEnum::SwitchingPhases:
|
|
break;
|
|
}
|
|
|
|
// process authorized event which will inititate a TransactionEvent(Updated) message in case the token has not
|
|
// yet been authorized by the CSMS
|
|
auto authorized_id_token = get_authorized_id_token(session_event);
|
|
if (authorized_id_token.has_value()) {
|
|
{
|
|
auto evse_evcc_id_handle = this->evse_evcc_id.handle();
|
|
if (!evse_evcc_id_handle->at(evse_id).empty()) {
|
|
update_evcc_id_token(authorized_id_token.value(), evse_evcc_id_handle->at(evse_id),
|
|
ocpp_protocol_version);
|
|
}
|
|
}
|
|
this->charge_point->on_authorized(evse_id, connector_id, authorized_id_token.value());
|
|
}
|
|
}
|
|
|
|
void OCPP201::process_tx_event_effect(const int32_t evse_id, const TxEventEffect tx_event_effect,
|
|
const types::evse_manager::SessionEvent& session_event) {
|
|
if (tx_event_effect == TxEventEffect::NONE) {
|
|
return;
|
|
}
|
|
|
|
const auto transaction_data = this->transaction_handler->get_transaction_data(evse_id);
|
|
if (transaction_data == nullptr) {
|
|
throw std::runtime_error("Could not start transaction because no tranasaction data is present");
|
|
}
|
|
transaction_data->timestamp = ocpp_conversions::to_ocpp_datetime_or_now(session_event.timestamp);
|
|
|
|
if (tx_event_effect == TxEventEffect::START_TRANSACTION) {
|
|
transaction_data->started = true;
|
|
transaction_data->meter_value = conversions::to_ocpp_meter_value(
|
|
get_meter_value(session_event), ocpp::v2::ReadingContextEnum::Transaction_Begin,
|
|
get_signed_meter_value(session_event));
|
|
this->charge_point->on_transaction_started(
|
|
evse_id, transaction_data->connector_id, transaction_data->session_id, transaction_data->timestamp,
|
|
transaction_data->trigger_reason, transaction_data->meter_value, transaction_data->id_token,
|
|
transaction_data->group_id_token, transaction_data->reservation_id, transaction_data->remote_start_id,
|
|
transaction_data->charging_state);
|
|
} else if (tx_event_effect == TxEventEffect::STOP_TRANSACTION) {
|
|
transaction_data->meter_value = conversions::to_ocpp_meter_value(get_meter_value(session_event),
|
|
ocpp::v2::ReadingContextEnum::Transaction_End,
|
|
get_signed_meter_value(session_event));
|
|
this->charge_point->on_transaction_finished(evse_id, transaction_data->timestamp, transaction_data->meter_value,
|
|
transaction_data->stop_reason, transaction_data->trigger_reason,
|
|
transaction_data->id_token, std::nullopt,
|
|
transaction_data->charging_state);
|
|
this->transaction_handler->reset_transaction_data(evse_id);
|
|
}
|
|
}
|
|
|
|
void OCPP201::process_session_started(const int32_t evse_id, const int32_t connector_id,
|
|
const types::evse_manager::SessionEvent& session_event) {
|
|
if (!session_event.session_started.has_value()) {
|
|
throw std::runtime_error("SessionEvent SessionStarted does not contain session_started context");
|
|
}
|
|
|
|
const auto session_started = session_event.session_started.value();
|
|
|
|
std::optional<ocpp::v2::IdToken> id_token = std::nullopt;
|
|
std::optional<ocpp::v2::IdToken> group_id_token = std::nullopt;
|
|
std::optional<int32_t> remote_start_id = std::nullopt;
|
|
auto charging_state = ocpp::v2::ChargingStateEnum::Idle;
|
|
auto trigger_reason = ocpp::v2::TriggerReasonEnum::Authorized;
|
|
auto tx_event = TxEvent::AUTHORIZED;
|
|
if (session_started.reason == types::evse_manager::StartSessionReason::EVConnected) {
|
|
tx_event = TxEvent::EV_CONNECTED;
|
|
trigger_reason = ocpp::v2::TriggerReasonEnum::CablePluggedIn;
|
|
charging_state = ocpp::v2::ChargingStateEnum::EVConnected;
|
|
} else if (!session_started.id_tag.has_value()) {
|
|
EVLOG_warning << "Session started with reason Authorized, but no id_tag provided as part of the "
|
|
"session event";
|
|
} else {
|
|
id_token = conversions::to_ocpp_id_token(session_started.id_tag.value().id_token);
|
|
auto evse_evcc_id_handle = this->evse_evcc_id.handle();
|
|
if (!evse_evcc_id_handle->at(evse_id).empty()) {
|
|
update_evcc_id_token(id_token.value(), evse_evcc_id_handle->at(evse_id), ocpp_protocol_version);
|
|
}
|
|
remote_start_id = session_started.id_tag.value().request_id;
|
|
if (session_started.id_tag.value().parent_id_token.has_value()) {
|
|
group_id_token = conversions::to_ocpp_id_token(session_started.id_tag.value().parent_id_token.value());
|
|
}
|
|
if (session_started.id_tag.value().authorization_type == types::authorization::AuthorizationType::OCPP) {
|
|
trigger_reason = ocpp::v2::TriggerReasonEnum::RemoteStart;
|
|
}
|
|
}
|
|
const auto timestamp = ocpp_conversions::to_ocpp_datetime_or_now(session_event.timestamp);
|
|
const auto reservation_id = session_started.reservation_id;
|
|
|
|
// this is always the first transaction related interaction, so we create TransactionData here
|
|
auto transaction_data =
|
|
std::make_shared<TransactionData>(connector_id, session_event.uuid, timestamp, trigger_reason, charging_state);
|
|
transaction_data->id_token = id_token;
|
|
transaction_data->group_id_token = group_id_token;
|
|
transaction_data->remote_start_id = remote_start_id;
|
|
transaction_data->reservation_id = reservation_id;
|
|
this->transaction_handler->add_transaction_data(evse_id, transaction_data);
|
|
|
|
const auto tx_event_effect = this->transaction_handler->submit_event(evse_id, tx_event);
|
|
this->process_tx_event_effect(evse_id, tx_event_effect, session_event);
|
|
if (session_started.reason == types::evse_manager::StartSessionReason::EVConnected) {
|
|
this->charge_point->on_session_started(evse_id, connector_id);
|
|
}
|
|
if (tx_event == TxEvent::EV_CONNECTED) {
|
|
this->everest_device_model_storage->update_connected_ev_available(evse_id, true);
|
|
}
|
|
}
|
|
|
|
void OCPP201::process_session_finished(const int32_t evse_id, const int32_t connector_id,
|
|
const types::evse_manager::SessionEvent& session_event) {
|
|
this->evse_soc_map.handle()->at(evse_id).reset();
|
|
auto transaction_data = this->transaction_handler->get_transaction_data(evse_id);
|
|
if (transaction_data != nullptr) {
|
|
transaction_data->charging_state = ocpp::v2::ChargingStateEnum::Idle;
|
|
transaction_data->stop_reason = ocpp::v2::ReasonEnum::EVDisconnected;
|
|
transaction_data->trigger_reason = ocpp::v2::TriggerReasonEnum::EVCommunicationLost;
|
|
}
|
|
const auto tx_event_effect = this->transaction_handler->submit_event(evse_id, TxEvent::EV_DISCONNECTED);
|
|
this->evse_evcc_id.handle()->at(evse_id) = "";
|
|
this->process_tx_event_effect(evse_id, tx_event_effect, session_event);
|
|
this->charge_point->on_session_finished(evse_id, connector_id);
|
|
this->everest_device_model_storage->update_connected_ev_available(evse_id, false);
|
|
}
|
|
|
|
void OCPP201::process_transaction_started(const int32_t evse_id, const int32_t connector_id,
|
|
const types::evse_manager::SessionEvent& session_event) {
|
|
if (!session_event.transaction_started.has_value()) {
|
|
throw std::runtime_error("SessionEvent TransactionStarted does not contain session_started context");
|
|
}
|
|
|
|
auto transaction_data = this->transaction_handler->get_transaction_data(evse_id);
|
|
if (transaction_data == nullptr) {
|
|
EVLOG_warning << "Could not update transaction data because no transaction data is present. This might happen "
|
|
"in case a TxStopPoint is already active when a TransactionStarted event occurs (e.g. "
|
|
"TxStopPoint is EnergyTransfer or ParkingBayOccupied)";
|
|
this->charge_point->on_session_started(evse_id, connector_id);
|
|
auto tx_event_effect = this->transaction_handler->submit_event(evse_id, TxEvent::AUTHORIZED);
|
|
this->process_tx_event_effect(evse_id, tx_event_effect, session_event);
|
|
tx_event_effect = this->transaction_handler->submit_event(evse_id, TxEvent::EV_CONNECTED);
|
|
this->process_tx_event_effect(evse_id, tx_event_effect, session_event);
|
|
this->everest_device_model_storage->update_connected_ev_available(evse_id, true);
|
|
return;
|
|
}
|
|
|
|
// at this point we dont know if the TransactionStarted event was triggered because of an Authorization or EV
|
|
// Plug in event. We assume cable has been plugged in first and then authorized and update if other order was
|
|
// applied
|
|
auto tx_event = TxEvent::AUTHORIZED;
|
|
auto trigger_reason = ocpp::v2::TriggerReasonEnum::Authorized;
|
|
const auto transaction_started = session_event.transaction_started.value();
|
|
if (transaction_started.reservation_id.has_value()) {
|
|
transaction_data->reservation_id = transaction_started.reservation_id;
|
|
}
|
|
transaction_data->remote_start_id = transaction_started.id_tag.request_id;
|
|
auto id_token = conversions::to_ocpp_id_token(transaction_started.id_tag.id_token);
|
|
auto evse_evcc_id_handle = this->evse_evcc_id.handle();
|
|
if (!evse_evcc_id_handle->at(evse_id).empty()) {
|
|
update_evcc_id_token(id_token, evse_evcc_id_handle->at(evse_id), ocpp_protocol_version);
|
|
}
|
|
transaction_data->id_token = id_token;
|
|
|
|
std::optional<ocpp::v2::IdToken> group_id_token = std::nullopt;
|
|
if (transaction_started.id_tag.parent_id_token.has_value()) {
|
|
transaction_data->group_id_token =
|
|
conversions::to_ocpp_id_token(transaction_started.id_tag.parent_id_token.value());
|
|
}
|
|
|
|
// if session started reason was Authorized, Transaction is started because of EV plug in event
|
|
if (transaction_data->trigger_reason == ocpp::v2::TriggerReasonEnum::Authorized or
|
|
transaction_data->trigger_reason == ocpp::v2::TriggerReasonEnum::RemoteStart) {
|
|
trigger_reason = ocpp::v2::TriggerReasonEnum::CablePluggedIn;
|
|
transaction_data->charging_state = ocpp::v2::ChargingStateEnum::EVConnected;
|
|
this->charge_point->on_session_started(evse_id, connector_id);
|
|
tx_event = TxEvent::EV_CONNECTED;
|
|
}
|
|
|
|
if (transaction_started.id_tag.authorization_type == types::authorization::AuthorizationType::OCPP) {
|
|
trigger_reason = ocpp::v2::TriggerReasonEnum::RemoteStart;
|
|
}
|
|
|
|
transaction_data->trigger_reason = trigger_reason;
|
|
const auto tx_event_effect = this->transaction_handler->submit_event(evse_id, tx_event);
|
|
this->process_tx_event_effect(evse_id, tx_event_effect, session_event);
|
|
if (tx_event == TxEvent::EV_CONNECTED) {
|
|
this->everest_device_model_storage->update_connected_ev_available(evse_id, true);
|
|
}
|
|
}
|
|
|
|
void OCPP201::process_transaction_finished(const int32_t evse_id, const int32_t connector_id,
|
|
const types::evse_manager::SessionEvent& session_event) {
|
|
if (!session_event.transaction_finished.has_value()) {
|
|
throw std::runtime_error("SessionEvent TransactionFinished does not contain transaction_finished context");
|
|
}
|
|
const auto transaction_finished = session_event.transaction_finished.value();
|
|
auto tx_event = TxEvent::NONE;
|
|
auto reason = ocpp::v2::ReasonEnum::Other;
|
|
if (transaction_finished.reason.has_value()) {
|
|
reason = conversions::to_ocpp_reason(transaction_finished.reason.value());
|
|
tx_event = get_tx_event(reason);
|
|
}
|
|
auto transaction_data = this->transaction_handler->get_transaction_data(evse_id);
|
|
if (transaction_data != nullptr) {
|
|
std::optional<ocpp::v2::IdToken> id_token = std::nullopt;
|
|
if (transaction_finished.id_tag.has_value()) {
|
|
id_token = conversions::to_ocpp_id_token(transaction_finished.id_tag.value().id_token);
|
|
auto evse_evcc_id_handle = this->evse_evcc_id.handle();
|
|
if (!evse_evcc_id_handle->at(evse_id).empty()) {
|
|
update_evcc_id_token(id_token.value(), evse_evcc_id_handle->at(evse_id), ocpp_protocol_version);
|
|
}
|
|
}
|
|
|
|
// this is required to report the correct charging_state within a TransactionEvent(Ended) message
|
|
auto charging_state = transaction_data->charging_state;
|
|
if (reason == ocpp::v2::ReasonEnum::EVDisconnected) {
|
|
charging_state = ocpp::v2::ChargingStateEnum::Idle;
|
|
} else if (reason == ocpp::v2::ReasonEnum::ImmediateReset &&
|
|
charging_state != ocpp::v2::ChargingStateEnum::Idle) {
|
|
charging_state = ocpp::v2::ChargingStateEnum::EVConnected;
|
|
} else if (tx_event == TxEvent::DEAUTHORIZED) {
|
|
charging_state = ocpp::v2::ChargingStateEnum::EVConnected;
|
|
}
|
|
transaction_data->trigger_reason = stop_reason_to_trigger_reason_enum(reason);
|
|
transaction_data->stop_reason = reason;
|
|
transaction_data->id_token = id_token;
|
|
transaction_data->charging_state = charging_state;
|
|
}
|
|
|
|
// tx_event could be DEAUTHORIZED or EV_DISCONNECTED
|
|
const auto tx_event_effect = this->transaction_handler->submit_event(evse_id, tx_event);
|
|
this->process_tx_event_effect(evse_id, tx_event_effect, session_event);
|
|
|
|
if (tx_event == TxEvent::DEAUTHORIZED) {
|
|
if (reason == ocpp::v2::ReasonEnum::Remote) {
|
|
this->charge_point->on_charging_state_changed(evse_id, ocpp::v2::ChargingStateEnum::EVConnected,
|
|
ocpp::v2::TriggerReasonEnum::RemoteStop);
|
|
} else {
|
|
this->charge_point->on_charging_state_changed(evse_id, ocpp::v2::ChargingStateEnum::EVConnected,
|
|
ocpp::v2::TriggerReasonEnum::StopAuthorized);
|
|
}
|
|
} else {
|
|
// TODO(piet): If StopTxOnEVSideDisconnect is false, authorization shall still be present. This cannot only
|
|
// be handled within this module, but probably also within EvseManager and Auth
|
|
|
|
// authorization is always withdrawn in case of TransactionFinished, so in case we haven't updated the
|
|
// transaction handler yet, we have to do it
|
|
// now
|
|
const auto tx_event_effect = this->transaction_handler->submit_event(evse_id, TxEvent::DEAUTHORIZED);
|
|
this->process_tx_event_effect(evse_id, tx_event_effect, session_event);
|
|
}
|
|
}
|
|
|
|
void OCPP201::process_session_resumed(const int32_t evse_id, const int32_t connector_id,
|
|
const types::evse_manager::SessionEvent& session_event) {
|
|
// resume transaction with data we get from the session event
|
|
// currently, the SessionResumed event only occurs after a power outage followed by
|
|
// a TransactionFinished event. We have to add the transaction data again to be able to
|
|
// properly process the transaction finished event.
|
|
// Currently, sending a TransactionEvent(Ended) after a power loss is only supported if
|
|
// the configuration variable InternalCtrlr::ResumeTransactionsOnBoot is set to true.
|
|
// If this is not the case, libocpp will not be able to process a TransactionFinished event
|
|
// after a power loss, because it does internally not restore transaction data on boot.
|
|
const auto timestamp = ocpp_conversions::to_ocpp_datetime_or_now(session_event.timestamp);
|
|
auto transaction_data =
|
|
std::make_shared<TransactionData>(connector_id, session_event.uuid, timestamp,
|
|
ocpp::v2::TriggerReasonEnum::TxResumed, ocpp::v2::ChargingStateEnum::Idle);
|
|
transaction_data->started = true;
|
|
this->transaction_handler->add_transaction_data(evse_id, transaction_data);
|
|
}
|
|
|
|
void OCPP201::process_charging_started(const int32_t evse_id, const int32_t connector_id,
|
|
const types::evse_manager::SessionEvent& session_event) {
|
|
auto transaction_data = this->transaction_handler->get_transaction_data(evse_id);
|
|
if (transaction_data != nullptr) {
|
|
transaction_data->trigger_reason = ocpp::v2::TriggerReasonEnum::ChargingStateChanged;
|
|
transaction_data->charging_state = ocpp::v2::ChargingStateEnum::Charging;
|
|
}
|
|
const auto tx_event_effect = this->transaction_handler->submit_event(evse_id, TxEvent::ENERGY_TRANSFER_STARTED);
|
|
this->process_tx_event_effect(evse_id, tx_event_effect, session_event);
|
|
this->charge_point->on_charging_state_changed(evse_id, ocpp::v2::ChargingStateEnum::Charging);
|
|
}
|
|
|
|
void OCPP201::process_charging_paused_ev(const int32_t evse_id, const int32_t connector_id,
|
|
const types::evse_manager::SessionEvent& session_event) {
|
|
auto transaction_data = this->transaction_handler->get_transaction_data(evse_id);
|
|
if (transaction_data != nullptr) {
|
|
transaction_data->charging_state = ocpp::v2::ChargingStateEnum::SuspendedEV;
|
|
transaction_data->trigger_reason = ocpp::v2::TriggerReasonEnum::ChargingStateChanged;
|
|
transaction_data->stop_reason = ocpp::v2::ReasonEnum::StoppedByEV;
|
|
}
|
|
const auto tx_event_effect = this->transaction_handler->submit_event(evse_id, TxEvent::ENERGY_TRANSFER_STOPPED);
|
|
this->process_tx_event_effect(evse_id, tx_event_effect, session_event);
|
|
this->charge_point->on_charging_state_changed(evse_id, ocpp::v2::ChargingStateEnum::SuspendedEV);
|
|
}
|
|
|
|
void OCPP201::process_charging_paused_evse(const int32_t evse_id, const int32_t connector_id,
|
|
const types::evse_manager::SessionEvent& session_event) {
|
|
auto transaction_data = this->transaction_handler->get_transaction_data(evse_id);
|
|
auto trigger_reason = ocpp::v2::TriggerReasonEnum::ChargingStateChanged;
|
|
if (transaction_data != nullptr) {
|
|
transaction_data->charging_state = ocpp::v2::ChargingStateEnum::SuspendedEVSE;
|
|
if (transaction_data->stop_reason == ocpp::v2::ReasonEnum::Remote) {
|
|
trigger_reason = ocpp::v2::TriggerReasonEnum::RemoteStop;
|
|
transaction_data->trigger_reason = ocpp::v2::TriggerReasonEnum::ChargingStateChanged;
|
|
}
|
|
}
|
|
const auto tx_event_effect = this->transaction_handler->submit_event(evse_id, TxEvent::ENERGY_TRANSFER_STOPPED);
|
|
this->process_tx_event_effect(evse_id, tx_event_effect, session_event);
|
|
this->charge_point->on_charging_state_changed(evse_id, ocpp::v2::ChargingStateEnum::SuspendedEVSE, trigger_reason);
|
|
}
|
|
|
|
void OCPP201::process_enabled(const int32_t evse_id, const int32_t connector_id,
|
|
const types::evse_manager::SessionEvent& session_event) {
|
|
this->charge_point->on_enabled(evse_id, connector_id);
|
|
}
|
|
|
|
void OCPP201::process_disabled(const int32_t evse_id, const int32_t connector_id,
|
|
const types::evse_manager::SessionEvent& session_event) {
|
|
this->charge_point->on_unavailable(evse_id, connector_id);
|
|
}
|
|
|
|
void OCPP201::process_authorized(const int32_t evse_id, const int32_t connector_id,
|
|
const types::evse_manager::SessionEvent& session_event) {
|
|
// currently handled as part of SessionStarted and TransactionStarted events
|
|
}
|
|
|
|
void OCPP201::process_deauthorized(const int32_t evse_id, const int32_t connector_id,
|
|
const types::evse_manager::SessionEvent& session_event) {
|
|
auto transaction_data = this->transaction_handler->get_transaction_data(evse_id);
|
|
if (transaction_data != nullptr) {
|
|
transaction_data->trigger_reason = ocpp::v2::TriggerReasonEnum::StopAuthorized;
|
|
}
|
|
const auto tx_event_effect = this->transaction_handler->submit_event(evse_id, TxEvent::DEAUTHORIZED);
|
|
this->process_tx_event_effect(evse_id, tx_event_effect, session_event);
|
|
}
|
|
|
|
void OCPP201::process_reserved(const int32_t evse_id, const int32_t connector_id) {
|
|
this->charge_point->on_reserved(evse_id, connector_id);
|
|
}
|
|
|
|
void OCPP201::process_reservation_end(const int32_t evse_id, const int32_t connector_id) {
|
|
this->charge_point->on_reservation_cleared(evse_id, connector_id);
|
|
}
|
|
|
|
void OCPP201::publish_charging_schedules(const std::vector<ocpp::v2::EnhancedCompositeSchedule>& 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<ocpp::v2::EnhancedCompositeSchedule>& composite_schedules) {
|
|
const auto start_time = ocpp::DateTime();
|
|
|
|
auto to_timestamp = [&](int seconds_offset) {
|
|
return ocpp::DateTime(start_time.to_time_point() + std::chrono::seconds(seconds_offset)).to_rfc3339();
|
|
};
|
|
|
|
int32_t setpoint_priority = LOWEST_SETPOINT_PRIORITY;
|
|
const auto resp = this->charge_point->request_value<std::string>(ocpp::v2::ControllerComponents::SmartChargingCtrlr,
|
|
ocpp::v2::Variable{SETPOINT_PRIORITY_VAR_NAME},
|
|
ocpp::v2::AttributeEnum::Actual);
|
|
|
|
if (resp.status == ocpp::v2::GetVariableStatusEnum::Accepted && resp.value.has_value()) {
|
|
setpoint_priority = resp.value.value() == "CSMS" ? HIGHEST_SETPOINT_PRIORITY : LOWEST_SETPOINT_PRIORITY;
|
|
}
|
|
|
|
auto create_setpoint_entry =
|
|
[&](const std::string& timestamp, const ocpp::v2::EnhancedChargingSchedulePeriod& period,
|
|
const ocpp::v2::ChargingRateUnitEnum& unit) -> std::optional<types::energy::ScheduleSetpointEntry> {
|
|
const bool has_basic_setpoint = period.setpoint.has_value();
|
|
const bool has_freq_table = period.v2xFreqWattCurve.has_value() && !period.v2xFreqWattCurve->empty();
|
|
|
|
if (!has_basic_setpoint && !has_freq_table) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
types::energy::ScheduleSetpointEntry entry;
|
|
types::energy::SetpointType setpoint;
|
|
setpoint.source = SETPOINT_SOURCE;
|
|
setpoint.priority = setpoint_priority;
|
|
entry.timestamp = timestamp;
|
|
|
|
if (has_basic_setpoint) {
|
|
if (unit == ocpp::v2::ChargingRateUnitEnum::A) {
|
|
setpoint.ac_current_A = period.setpoint.value();
|
|
} else {
|
|
setpoint.total_power_W = period.setpoint.value();
|
|
}
|
|
}
|
|
|
|
if (has_freq_table) {
|
|
std::vector<types::energy::FrequencyWattPoint> frequency_table;
|
|
for (const auto& point : period.v2xFreqWattCurve.value()) {
|
|
types::energy::FrequencyWattPoint freq_point;
|
|
freq_point.frequency_Hz = point.frequency;
|
|
freq_point.total_power_W = point.power;
|
|
frequency_table.push_back(freq_point);
|
|
}
|
|
setpoint.frequency_table = std::move(frequency_table);
|
|
}
|
|
|
|
entry.setpoint = std::move(setpoint);
|
|
return entry;
|
|
};
|
|
|
|
auto create_limits_entry =
|
|
[&](const std::string& timestamp, const ocpp::v2::EnhancedChargingSchedulePeriod& period,
|
|
const ocpp::v2::ChargingRateUnitEnum& unit) -> std::optional<types::energy::ScheduleReqEntry> {
|
|
if (!period.limit.has_value()) {
|
|
return std::nullopt;
|
|
}
|
|
|
|
types::energy::ScheduleReqEntry entry;
|
|
entry.timestamp = timestamp;
|
|
|
|
types::energy::LimitsReq limits_req;
|
|
if (unit == ocpp::v2::ChargingRateUnitEnum::A) {
|
|
limits_req.ac_max_current_A = {period.limit.value(), source_ext_limit};
|
|
if (period.numberPhases.has_value()) {
|
|
limits_req.ac_max_phase_count = {period.numberPhases.value(), source_ext_limit};
|
|
}
|
|
} else {
|
|
limits_req.total_power_W = {period.limit.value(), source_ext_limit};
|
|
}
|
|
|
|
entry.limits_to_leaves = limits_req;
|
|
return entry;
|
|
};
|
|
|
|
for (const auto& composite_schedule : composite_schedules) {
|
|
auto evse_id = composite_schedule.evseId;
|
|
if (not external_energy_limits::is_evse_sink_configured(this->r_evse_energy_sink, evse_id)) {
|
|
EVLOG_warning << "Can not apply external limits! No evse energy sink configured for evse_id: " << evse_id;
|
|
continue;
|
|
}
|
|
|
|
types::energy::ExternalLimits limits;
|
|
std::vector<types::energy::ScheduleReqEntry> schedule_import;
|
|
std::vector<types::energy::ScheduleSetpointEntry> schedule_setpoints;
|
|
|
|
const auto& unit = composite_schedule.chargingRateUnit;
|
|
|
|
for (const auto& period : composite_schedule.chargingSchedulePeriod) {
|
|
const auto timestamp = to_timestamp(period.startPeriod);
|
|
|
|
if (auto setpoint_entry = create_setpoint_entry(timestamp, period, unit)) {
|
|
schedule_setpoints.push_back(*setpoint_entry);
|
|
}
|
|
|
|
if (auto limits_entry = create_limits_entry(timestamp, period, unit)) {
|
|
schedule_import.push_back(*limits_entry);
|
|
}
|
|
}
|
|
|
|
limits.schedule_import = std::move(schedule_import);
|
|
limits.schedule_setpoints = std::move(schedule_setpoints);
|
|
|
|
auto& evse_sink = external_energy_limits::get_evse_sink_by_evse_id(this->r_evse_energy_sink, evse_id);
|
|
evse_sink.call_set_external_limits(limits);
|
|
}
|
|
}
|
|
|
|
} // namespace module
|