diff --git a/config/v201/component_schemas/custom/EVSE_1.json b/config/v201/component_schemas/custom/EVSE_1.json index c8a167a50..2686ea3c9 100644 --- a/config/v201/component_schemas/custom/EVSE_1.json +++ b/config/v201/component_schemas/custom/EVSE_1.json @@ -79,6 +79,10 @@ { "type": "Actual", "mutability": "ReadOnly" + }, + { + "type": "MaxSet", + "mutability": "ReadOnly" } ], "description": " kW,The variableCharacteristic maxLimit, that holds the maximum power that this EVSE can provide, is required. The Actual value of the instantaneous (real) power is desired, but not required.", diff --git a/config/v201/component_schemas/custom/EVSE_2.json b/config/v201/component_schemas/custom/EVSE_2.json index 822713caa..58ba4a477 100644 --- a/config/v201/component_schemas/custom/EVSE_2.json +++ b/config/v201/component_schemas/custom/EVSE_2.json @@ -79,6 +79,10 @@ { "type": "Actual", "mutability": "ReadOnly" + }, + { + "type": "MaxSet", + "mutability": "ReadOnly" } ], "description": " kW,The variableCharacteristic maxLimit, that holds the maximum power that this EVSE can provide, is required. The Actual value of the instantaneous (real) power is desired, but not required.", diff --git a/config/v201/config.json b/config/v201/config.json index 9a62464ca..5fda8607a 100644 --- a/config/v201/config.json +++ b/config/v201/config.json @@ -46,12 +46,6 @@ "name": "EVSE", "evse_id": 1, "variables": { - "EVSEAvailabilityState": { - "variable_name": "AvailabilityState", - "attributes": { - "Actual": "" - } - }, "EVSEAvailable": { "variable_name": "Available", "attributes": { @@ -61,7 +55,7 @@ "EVSEPower": { "variable_name": "Power", "attributes": { - "Actual": 42 + "MaxSet": 22000 } }, "EVSESupplyPhases": { @@ -101,12 +95,6 @@ "name": "EVSE", "evse_id": 2, "variables": { - "EVSEAvailabilityState": { - "variable_name": "AvailabilityState", - "attributes": { - "Actual": "" - } - }, "EVSEAvailable": { "variable_name": "Available", "attributes": { @@ -116,7 +104,7 @@ "EVSEPower": { "variable_name": "Power", "attributes": { - "Actual": 42 + "MaxSet": 22000 } }, "EVSESupplyPhases": { diff --git a/include/ocpp/v201/charge_point.hpp b/include/ocpp/v201/charge_point.hpp index ef69ad9ae..2d8fd3858 100644 --- a/include/ocpp/v201/charge_point.hpp +++ b/include/ocpp/v201/charge_point.hpp @@ -264,6 +264,9 @@ class ChargePoint : ocpp::ChargingStationBase { void init_certificate_expiration_check_timers(); void scheduled_check_client_certificate_expiration(); void scheduled_check_v2g_certificate_expiration(); + void update_dm_availability_state(const int32_t evse_id, const int32_t connector_id, + const ConnectorStatusEnum status); + void update_dm_evse_power(const int32_t evse_id, const MeterValue& meter_value); /// \brief Gets the configured NetworkConnectionProfile based on the given \p configuration_slot . The /// central system uri ofthe connection options will not contain ws:// or wss:// because this method removes it if diff --git a/include/ocpp/v201/ctrlr_component_variables.hpp b/include/ocpp/v201/ctrlr_component_variables.hpp index 603e09431..869ec6a4d 100644 --- a/include/ocpp/v201/ctrlr_component_variables.hpp +++ b/include/ocpp/v201/ctrlr_component_variables.hpp @@ -106,9 +106,6 @@ extern const ComponentVariable& TimeOffset; extern const ComponentVariable& TimeOffsetNextTransition; extern const RequiredComponentVariable& TimeSource; extern const ComponentVariable& TimeZone; -extern const RequiredComponentVariable& ConnectorAvailable; -extern const RequiredComponentVariable& ConnectorType; -extern const RequiredComponentVariable& ConnectorSupplyPhases; extern const ComponentVariable& CustomImplementationEnabled; extern const RequiredComponentVariable& BytesPerMessageGetReport; extern const RequiredComponentVariable& BytesPerMessageGetVariables; @@ -122,10 +119,6 @@ extern const ComponentVariable& DisplayMessageCtrlrAvailable; extern const RequiredComponentVariable& NumberOfDisplayMessages; extern const RequiredComponentVariable& DisplayMessageSupportedFormats; extern const RequiredComponentVariable& DisplayMessageSupportedPriorities; -extern const ComponentVariable& EVSEAllowReset; -extern const RequiredComponentVariable& EVSEAvailable; -extern const RequiredComponentVariable& EVSEPower; -extern const RequiredComponentVariable& EVSESupplyPhases; extern const ComponentVariable& CentralContractValidationAllowed; extern const RequiredComponentVariable& ContractValidationOffline; extern const ComponentVariable& RequestMeteringReceipt; @@ -210,8 +203,25 @@ extern const RequiredComponentVariable& StopTxOnInvalidId; extern const ComponentVariable& TxBeforeAcceptedEnabled; extern const RequiredComponentVariable& TxStartPoint; extern const RequiredComponentVariable& TxStopPoint; - } // namespace ControllerComponentVariables + +namespace EvseComponentVariables { +extern const Variable& Available; +extern const Variable& AvailabilityState; +extern const Variable& SupplyPhases; +extern const Variable& AllowReset; +extern const Variable& Power; +ComponentVariable get_component_variable(const int32_t evse_id, const Variable& variable); +} // namespace EvseComponentVariables + +namespace ConnectorComponentVariables { +extern const Variable& Available; +extern const Variable& AvailabilityState; +extern const Variable& Type; +extern const Variable& SupplyPhases; +ComponentVariable get_component_variable(const int32_t evse_id, const int32_t connector_id, const Variable& variable); +} // namespace ConnectorComponentVariables + } // namespace v201 } // namespace ocpp diff --git a/include/ocpp/v201/device_model.hpp b/include/ocpp/v201/device_model.hpp index d0ef3aee4..421f7093b 100644 --- a/include/ocpp/v201/device_model.hpp +++ b/include/ocpp/v201/device_model.hpp @@ -207,7 +207,7 @@ class DeviceModel { /// \brief Check data integrity of the device model provided by the device model data storage: /// For "required" variables, assert values exist. Checks might be extended in the future. - void check_integrity(); + void check_integrity(const std::map& evse_connector_structure); }; } // namespace v201 diff --git a/include/ocpp/v201/utils.hpp b/include/ocpp/v201/utils.hpp index ba60442b9..eae2fac6c 100644 --- a/include/ocpp/v201/utils.hpp +++ b/include/ocpp/v201/utils.hpp @@ -61,6 +61,9 @@ std::string generate_token_hash(const IdToken& token); /// \return DateTime type timestamp ocpp::DateTime align_timestamp(const DateTime timestamp, std::chrono::seconds align_interval); +/// \brief Returns the total Power_Active_Import value from the \p meter_value or std::nullopt if it is not present +std::optional get_total_power_active_import(const MeterValue& meter_value); + } // namespace utils } // namespace v201 } // namespace ocpp diff --git a/lib/ocpp/v201/charge_point.cpp b/lib/ocpp/v201/charge_point.cpp index 6ebb455aa..d261905d1 100644 --- a/lib/ocpp/v201/charge_point.cpp +++ b/lib/ocpp/v201/charge_point.cpp @@ -79,7 +79,7 @@ ChargePoint::ChargePoint(const std::map& evse_connector_struct } this->device_model = std::make_unique(std::move(device_model_storage)); - this->device_model->check_integrity(); + this->device_model->check_integrity(evse_connector_structure); this->database_handler = std::make_shared(core_database_path, sql_init_path); this->database_handler->open_connection(); @@ -87,6 +87,7 @@ ChargePoint::ChargePoint(const std::map& evse_connector_struct // Set up the component state manager this->component_state_manager = std::make_shared( evse_connector_structure, database_handler, [this](auto evse_id, auto connector_id, auto status) { + this->update_dm_availability_state(evse_id, connector_id, status); if (this->websocket == nullptr || !this->websocket->is_connected() || this->registration_status != RegistrationStatusEnum::Accepted) { return false; @@ -441,6 +442,7 @@ void ChargePoint::on_meter_value(const int32_t evse_id, const MeterValue& meter_ this->aligned_data_evse0.set_values(meter_value); } else { this->evses.at(evse_id)->on_meter_value(meter_value); + this->update_dm_evse_power(evse_id, meter_value); } } @@ -3168,6 +3170,41 @@ void ChargePoint::scheduled_check_v2g_certificate_expiration() { .value_or(12 * 60 * 60))); } +void ChargePoint::update_dm_availability_state(const int32_t evse_id, const int32_t connector_id, + const ConnectorStatusEnum status) { + ComponentVariable evse_cv = + EvseComponentVariables::get_component_variable(evse_id, EvseComponentVariables::AvailabilityState); + ComponentVariable connector_cv = ConnectorComponentVariables::get_component_variable( + evse_id, connector_id, ConnectorComponentVariables::AvailabilityState); + if (evse_cv.variable.has_value()) { + this->device_model->set_read_only_value(evse_cv.component, evse_cv.variable.value(), + ocpp::v201::AttributeEnum::Actual, + conversions::connector_status_enum_to_string(status)); + } + if (connector_cv.variable.has_value()) { + this->device_model->set_read_only_value(connector_cv.component, connector_cv.variable.value(), + ocpp::v201::AttributeEnum::Actual, + conversions::connector_status_enum_to_string(status)); + } +} + +void ChargePoint::update_dm_evse_power(const int32_t evse_id, const MeterValue& meter_value) { + ComponentVariable evse_power_cv = + EvseComponentVariables::get_component_variable(evse_id, EvseComponentVariables::Power); + + if (!evse_power_cv.variable.has_value()) { + return; + } + + const auto power = utils::get_total_power_active_import(meter_value); + if (!power.has_value()) { + return; + } + + this->device_model->set_read_only_value(evse_power_cv.component, evse_power_cv.variable.value(), + AttributeEnum::Actual, std::to_string(power.value())); +} + void ChargePoint::set_cs_operative_status(OperationalStatusEnum new_status, bool persist) { this->component_state_manager->set_cs_individual_operational_status(new_status, persist); } diff --git a/lib/ocpp/v201/ctrlr_component_variables.cpp b/lib/ocpp/v201/ctrlr_component_variables.cpp index 1945b21a2..64666151b 100644 --- a/lib/ocpp/v201/ctrlr_component_variables.cpp +++ b/lib/ocpp/v201/ctrlr_component_variables.cpp @@ -13,11 +13,9 @@ const Component& AuthCacheCtrlr = {"AuthCacheCtrlr"}; const Component& AuthCtrlr = {"AuthCtrlr"}; const Component& ChargingStation = {"ChargingStation"}; const Component& ClockCtrlr = {"ClockCtrlr"}; -const Component& Connector = {"Connector"}; const Component& CustomizationCtrlr = {"CustomizationCtrlr"}; const Component& DeviceDataCtrlr = {"DeviceDataCtrlr"}; const Component& DisplayMessageCtrlr = {"DisplayMessageCtrlr"}; -const Component& EVSE = {"EVSE"}; const Component& ISO15118Ctrlr = {"ISO15118Ctrlr"}; const Component& LocalAuthListCtrlr = {"LocalAuthListCtrlr"}; const Component& MonitoringCtrlr = {"MonitoringCtrlr"}; @@ -525,27 +523,6 @@ const ComponentVariable& TimeZone = { "TimeZone", }), }; -const RequiredComponentVariable& ConnectorAvailable = { - ControllerComponents::Connector, - std::nullopt, - std::optional({ - "Available", - }), -}; -const RequiredComponentVariable& ConnectorType = { - ControllerComponents::Connector, - std::nullopt, - std::optional({ - "ConnectorType", - }), -}; -const RequiredComponentVariable& ConnectorSupplyPhases = { - ControllerComponents::Connector, - std::nullopt, - std::optional({ - "SupplyPhases", - }), -}; const ComponentVariable& CustomImplementationEnabled = { ControllerComponents::CustomizationCtrlr, std::nullopt, @@ -625,34 +602,6 @@ const RequiredComponentVariable& DisplayMessageSupportedPriorities = { "SupportedPriorities", }), }; -const ComponentVariable& EVSEAllowReset = { - ControllerComponents::EVSE, - std::nullopt, - std::optional({ - "AllowReset", - }), -}; -const RequiredComponentVariable& EVSEAvailable = { - ControllerComponents::EVSE, - std::nullopt, - std::optional({ - "Available", - }), -}; -const RequiredComponentVariable& EVSEPower = { - ControllerComponents::EVSE, - std::nullopt, - std::optional({ - "Power", - }), -}; -const RequiredComponentVariable& EVSESupplyPhases = { - ControllerComponents::EVSE, - std::nullopt, - std::optional({ - "SupplyPhases", - }), -}; const ComponentVariable& CentralContractValidationAllowed = { ControllerComponents::ISO15118Ctrlr, std::nullopt, @@ -1219,5 +1168,35 @@ const RequiredComponentVariable& TxStopPoint = { }; } // namespace ControllerComponentVariables + +namespace EvseComponentVariables { + +const Variable& Available = {"Available"}; +const Variable& AvailabilityState = {"AvailabilityState"}; +const Variable& SupplyPhases = {"SupplyPhases"}; +const Variable& AllowReset = {"AllowReset"}; +const Variable& Power = {"Power"}; + +ComponentVariable get_component_variable(const int32_t evse_id, const Variable& variable) { + EVSE evse = {evse_id}; + Component component = {"EVSE", std::nullopt, evse}; + return {component, std::nullopt, variable}; +} +} // namespace EvseComponentVariables + +namespace ConnectorComponentVariables { + +const Variable& Available = {"Available"}; +const Variable& AvailabilityState = {"AvailabilityState"}; +const Variable& Type = {"Type"}; +const Variable& SupplyPhases = {"SupplyPhases"}; + +ComponentVariable get_component_variable(const int32_t evse_id, const int32_t connector_id, const Variable& variable) { + EVSE evse = {evse_id, std::nullopt, connector_id}; + Component component = {"Connector", std::nullopt, evse}; + return {component, std::nullopt, variable}; +} +} // namespace ConnectorComponentVariables + } // namespace v201 } // namespace ocpp diff --git a/lib/ocpp/v201/device_model.cpp b/lib/ocpp/v201/device_model.cpp index d4151e80a..c90dedce4 100644 --- a/lib/ocpp/v201/device_model.cpp +++ b/lib/ocpp/v201/device_model.cpp @@ -18,6 +18,18 @@ static bool allow_zero(const Component& component, const Variable& variable) { component_variable == ControllerComponentVariables::SampledDataTxEndedInterval; } +bool allow_set_read_only_value(const Component& component, const Variable& variable, + const AttributeEnum attribute_enum) { + if (attribute_enum != AttributeEnum::Actual) { + return false; + } + + return component == ControllerComponents::AuthCacheCtrlr or component == ControllerComponents::LocalAuthListCtrlr or + component == ControllerComponents::OCPPCommCtrlr or component == ControllerComponents::SecurityCtrlr or + variable == EvseComponentVariables::AvailabilityState or variable == EvseComponentVariables::Power or + variable == ConnectorComponentVariables::AvailabilityState; +} + bool DeviceModel::component_criteria_match(const Component& component, const std::vector& component_criteria) { if (component_criteria.empty()) { @@ -214,11 +226,11 @@ DeviceModel::DeviceModel(std::unique_ptr device_model_storag SetVariableStatusEnum DeviceModel::set_read_only_value(const Component& component, const Variable& variable, const AttributeEnum& attribute_enum, const std::string& value) { - if (component == ControllerComponents::AuthCacheCtrlr or component == ControllerComponents::LocalAuthListCtrlr or - component == ControllerComponents::OCPPCommCtrlr or component == ControllerComponents::SecurityCtrlr) { + if (allow_set_read_only_value(component, variable, attribute_enum)) { return this->set_value(component, variable, attribute_enum, value, true); } - throw std::invalid_argument("Not allowed to set read only value for component " + component.name.get()); + throw std::invalid_argument("Not allowed to set read only value for component " + component.name.get() + + " and variable " + variable.name.get()); } std::optional DeviceModel::get_variable_meta_data(const Component& component, @@ -301,10 +313,54 @@ DeviceModel::get_custom_report_data(const std::optional& evse_connector_structure) { EVLOG_debug << "Checking integrity of device model in storage"; try { this->storage->check_integrity(); + + int32_t nr_evse_components; + std::map evse_id_nr_connector_components; + + for (const auto& [component, variable_map] : this->device_model) { + if (component.name == "EVSE") { + nr_evse_components++; + } else if (component.name == "Connector") { + if (evse_id_nr_connector_components.count(component.evse.value().id)) { + evse_id_nr_connector_components[component.evse.value().id] += 1; + } else { + evse_id_nr_connector_components[component.evse.value().id] = 1; + } + } + } + + // check if number of EVSE in the device model matches the configured number + if (nr_evse_components != evse_connector_structure.size()) { + throw DeviceModelStorageError("Number of EVSE configured in device model is incompatible with number of " + "configured EVSEs of the ChargePoint"); + } + + for (const auto [evse_id, nr_of_connectors] : evse_connector_structure) { + // check if number of Cpnnectors for this EVSE in the device model matches the configured number + if (evse_id_nr_connector_components[evse_id] != nr_of_connectors) { + throw DeviceModelStorageError( + "Number of Connectors configured in device model is incompatible with number " + "of configured Connectors of the ChargePoint"); + } + + // check if all relevant EVSE and Connector components can be found + EVSE evse = {evse_id}; + Component evse_component = {"EVSE", std::nullopt, evse}; + if (!this->device_model.count(evse_component)) { + throw DeviceModelStorageError("Could not find required EVSE component in device model"); + } + for (size_t connector_id = 1; connector_id <= nr_of_connectors; connector_id++) { + evse_component.name = "Connector"; + evse_component.evse.value().connectorId = connector_id; + if (!this->device_model.count(evse_component)) { + throw DeviceModelStorageError("Could not find required Connector component in device model"); + } + } + } } catch (const DeviceModelStorageError& e) { EVLOG_error << "Integrity check in Device Model storage failed:" << e.what(); throw e; diff --git a/lib/ocpp/v201/device_model_storage_sqlite.cpp b/lib/ocpp/v201/device_model_storage_sqlite.cpp index d1415794c..40d058c3f 100644 --- a/lib/ocpp/v201/device_model_storage_sqlite.cpp +++ b/lib/ocpp/v201/device_model_storage_sqlite.cpp @@ -19,7 +19,8 @@ DeviceModelStorageSqlite::DeviceModelStorageSqlite(const fs::path& db_path) { } int DeviceModelStorageSqlite::get_component_id(const Component& component_id) { - std::string select_query = "SELECT ID FROM COMPONENT WHERE NAME = ? AND INSTANCE IS ?"; + std::string select_query = + "SELECT ID FROM COMPONENT WHERE NAME = ? AND INSTANCE IS ? AND EVSE_ID IS ? AND CONNECTOR_ID IS ?"; SQLiteStatement select_stmt(this->db, select_query); select_stmt.bind_text(1, component_id.name.get(), SQLiteString::Transient); @@ -28,6 +29,16 @@ int DeviceModelStorageSqlite::get_component_id(const Component& component_id) { } else { select_stmt.bind_null(2); } + if (component_id.evse.has_value()) { + select_stmt.bind_int(3, component_id.evse.value().id); + if (component_id.evse.value().connectorId.has_value()) { + select_stmt.bind_int(4, component_id.evse.value().connectorId.value()); + } else { + select_stmt.bind_null(4); + } + } else { + select_stmt.bind_null(3); + } if (select_stmt.step() == SQLITE_ROW) { return select_stmt.column_int(0); diff --git a/lib/ocpp/v201/utils.cpp b/lib/ocpp/v201/utils.cpp index 161b22b6a..71d08f726 100644 --- a/lib/ocpp/v201/utils.cpp +++ b/lib/ocpp/v201/utils.cpp @@ -183,6 +183,15 @@ ocpp::DateTime align_timestamp(const DateTime timestamp, std::chrono::seconds al return rounded_time; } +std::optional get_total_power_active_import(const MeterValue& meter_value) { + for (const auto& sampled_value : meter_value.sampledValue) { + if (sampled_value.measurand == MeasurandEnum::Power_Active_Import and !sampled_value.phase.has_value()) { + return sampled_value.value; + } + } + return std::nullopt; +} + } // namespace utils } // namespace v201 } // namespace ocpp