diff --git a/modules/OCPP201/CMakeLists.txt b/modules/OCPP201/CMakeLists.txt index f0bda799b1..4a0548d1e5 100644 --- a/modules/OCPP201/CMakeLists.txt +++ b/modules/OCPP201/CMakeLists.txt @@ -46,6 +46,19 @@ target_sources(${MODULE_NAME} "device_model/everest_device_model_storage.cpp" "device_model/composed_device_model_storage.cpp" "device_model/definitions.cpp" + "device_model/mapping/variable_mapping.cpp" +) + +# Install mapping.yaml +install( + FILES device_model/mapping/mapping.yaml + DESTINATION ${CMAKE_INSTALL_DATADIR}/everest/modules/${MODULE_NAME} +) + +# Install schema +install( + FILES device_model/mapping/mapping_schema.json + DESTINATION ${CMAKE_INSTALL_DATADIR}/everest/modules/${MODULE_NAME} ) if(EVEREST_CORE_BUILD_TESTING) diff --git a/modules/OCPP201/OCPP201.cpp b/modules/OCPP201/OCPP201.cpp index 03ec8ebdb5..952c36ecd6 100644 --- a/modules/OCPP201/OCPP201.cpp +++ b/modules/OCPP201/OCPP201.cpp @@ -9,6 +9,7 @@ #include #include +#include #include #include #include @@ -420,6 +421,17 @@ void OCPP201::ready() { } }(); + const auto mapping_file_path = [&]() { + const auto config_mapping_file_path = fs::path(this->config.MappingFilePath); + if (config_mapping_file_path.is_relative()) { + return this->ocpp_share_path / config_mapping_file_path; + } else { + return config_mapping_file_path; + } + }(); + + const auto mapping_schema_path = this->ocpp_share_path / "mapping_schema.yaml"; + if (!fs::exists(this->config.MessageLogPath)) { try { fs::create_directory(this->config.MessageLogPath); @@ -838,6 +850,8 @@ void OCPP201::ready() { std::map evse_connector_structure = this->get_connector_structure(); + auto variable_mapping = std::make_unique(mapping_file_path, mapping_schema_path); + // initialize libocpp device model auto libocpp_device_model_storage = std::make_shared( device_model_database_path, device_model_database_migration_path, device_model_config_path); @@ -845,7 +859,7 @@ void OCPP201::ready() { // initialize everest device model this->everest_device_model_storage = std::make_shared( r_evse_manager, this->evse_hardware_capabilities_map, everest_device_model_database_path, - device_model_database_migration_path, get_config_service_client()); + device_model_database_migration_path, std::move(variable_mapping), get_config_service_client()); // initialize composed device model, this will be provided to the ChargePoint constructor auto composed_device_model_storage = std::make_unique(); diff --git a/modules/OCPP201/OCPP201.hpp b/modules/OCPP201/OCPP201.hpp index 750ca8f045..d1b04516bb 100644 --- a/modules/OCPP201/OCPP201.hpp +++ b/modules/OCPP201/OCPP201.hpp @@ -53,6 +53,7 @@ struct Conf { int RequestCompositeScheduleDurationS; std::string RequestCompositeScheduleUnit; int DelayOcppStart; + std::string MappingFilePath; }; class OCPP201 : public Everest::ModuleBase { diff --git a/modules/OCPP201/device_model/composed_device_model_storage.cpp b/modules/OCPP201/device_model/composed_device_model_storage.cpp index abf04d1067..eaa51445f1 100644 --- a/modules/OCPP201/device_model/composed_device_model_storage.cpp +++ b/modules/OCPP201/device_model/composed_device_model_storage.cpp @@ -40,7 +40,20 @@ bool ComposedDeviceModelStorage::register_device_model_storage( ocpp::v2::DeviceModelMap ComposedDeviceModelStorage::get_device_model() { ocpp::v2::DeviceModelMap device_model_map; for (const auto& [name, device_model_storage] : this->device_model_storages) { - device_model_map.merge(device_model_storage->get_device_model()); + const auto& partial_device_model = device_model_storage->get_device_model(); + for (const auto& [component, variable_map] : partial_device_model) { + auto& existing_variable_map = device_model_map[component]; // Inserts if not present + // Merge variable_map into existing_variable_map + for (const auto& [variable, variable_meta_data] : variable_map) { + if (existing_variable_map.find(variable) != existing_variable_map.end()) { + EVLOG_warning << "Variable " << variable.name << " already exists in component " << component.name + << " but is also defined in device model storage: " << name; + EVLOG_AND_THROW(std::runtime_error( + "Variable already exists in component. Fix your device model configuration.")); + } + existing_variable_map[variable] = variable_meta_data; // Overwrite or insert + } + } } return device_model_map; } diff --git a/modules/OCPP201/device_model/everest_device_model_storage.cpp b/modules/OCPP201/device_model/everest_device_model_storage.cpp index e780d99e51..dd64dd1a69 100644 --- a/modules/OCPP201/device_model/everest_device_model_storage.cpp +++ b/modules/OCPP201/device_model/everest_device_model_storage.cpp @@ -83,6 +83,30 @@ ComponentKey get_connector_component_key(const int32_t evse_id, const int32_t co return component; } +ocpp::v2::Component get_component(const ComponentKey& component_key) { + ocpp::v2::Component component; + component.name = ocpp::CiString<50>(component_key.name, ocpp::StringTooLarge::Truncate); + if (component_key.instance.has_value()) { + component.instance = ocpp::CiString<50>(component_key.instance.value(), ocpp::StringTooLarge::Truncate); + } + if (component_key.evse_id.has_value()) { + ocpp::v2::EVSE evse; + evse.id = component_key.evse_id.value(); + evse.connectorId = component_key.connector_id; + component.evse = evse; + } + return component; +} + +ocpp::v2::Variable get_variable(const DeviceModelVariable& dm_variable) { + ocpp::v2::Variable variable; + variable.name = ocpp::CiString<50>(dm_variable.name, ocpp::StringTooLarge::Truncate); + if (dm_variable.instance.has_value()) { + variable.instance = ocpp::CiString<50>(dm_variable.instance.value(), ocpp::StringTooLarge::Truncate); + } + return variable; +} + // Helper function to construct DeviceModelVariable with common structure DeviceModelVariable make_variable(const std::string& name, const ocpp::v2::VariableCharacteristics& characteristics, const std::string& value = "", @@ -147,45 +171,18 @@ std::string get_everest_config_value(const everest::config::ModuleConfigurationP throw std::out_of_range("Could not find requested config key: " + config_key); } -// Populate EVerest module config variables -std::vector -build_everest_config_variables(const everest::config::ModuleConfigurationParameters& module_config) { - std::vector component_config; - for (const auto& [impl, config_params] : module_config) { - std::string prefix; - if (impl != Everest::config::MODULE_IMPLEMENTATION_ID) { - // prefix variable name with impl + . - prefix = impl + "."; - } - for (const auto& config_param : config_params) { - try { - const auto variable_name = prefix + config_param.name; - ocpp::v2::VariableCharacteristics characteristics; - characteristics.dataType = to_ocpp_data_enum(config_param.characteristics.datatype); - characteristics.supportsMonitoring = false; // TODO: can we enable monitoring support? - // TODO: add unit if/once available? - - auto device_model_variable = make_variable( - variable_name, characteristics, get_everest_config_value(module_config, impl, config_param.name), - to_ocpp_mutability_enum(config_param.characteristics.mutability)); - component_config.push_back(device_model_variable); - } catch (const std::exception& e) { - EVLOG_error << "Could not add EVerest config entry to OCPP device model: " << e.what(); - } - } - } - - return component_config; -} - } // anonymous namespace EverestDeviceModelStorage::EverestDeviceModelStorage( const std::vector>& r_evse_manager, const std::map& evse_hardware_capabilities_map, const std::filesystem::path& db_path, const std::filesystem::path& migration_files_path, + std::unique_ptr variable_mapping, std::shared_ptr config_service_client) : - r_evse_manager(r_evse_manager), config_service_client(config_service_client) { + r_evse_manager(r_evse_manager), + config_service_client(config_service_client), + variable_mapping(std::move(variable_mapping)) { + this->module_configs = config_service_client->get_module_configs(); this->mappings = config_service_client->get_mappings(); std::map> component_configs; @@ -228,7 +225,9 @@ EverestDeviceModelStorage::EverestDeviceModelStorage( } } - component_configs[component_key] = build_everest_config_variables(module_config); + Component component = get_component(component_key); + component_configs[component_key] = + build_everest_config_variables(module_config, module_id_type.module_id, component); } ocpp::v2::InitDeviceModelDb init_device_model_db(db_path, migration_files_path); @@ -237,7 +236,86 @@ EverestDeviceModelStorage::EverestDeviceModelStorage( this->device_model_storage = std::make_unique(db_path); this->init_hw_capabilities(evse_hardware_capabilities_map); - this->init_everest_config(); +} + +std::vector EverestDeviceModelStorage::build_everest_config_variables( + const everest::config::ModuleConfigurationParameters& module_config, const std::string& module_id, + const ocpp::v2::Component& component) { + std::vector component_config; + for (const auto& [impl, config_params] : module_config) { + std::string prefix; + if (impl != Everest::config::MODULE_IMPLEMENTATION_ID) { + // prefix variable name with impl + . + prefix = impl + "."; + } + for (const auto& config_param : config_params) { + try { + const auto variable_name = prefix + config_param.name; + ocpp::v2::VariableCharacteristics characteristics; + characteristics.dataType = to_ocpp_data_enum(config_param.characteristics.datatype); + characteristics.supportsMonitoring = false; // TODO: can we enable monitoring support? + // TODO: add unit if/once available? + + auto device_model_variable = make_variable( + variable_name, characteristics, get_everest_config_value(module_config, impl, config_param.name), + to_ocpp_mutability_enum(config_param.characteristics.mutability)); + component_config.push_back(device_model_variable); + + everest::config::ConfigurationParameterIdentifier config_param_id; + config_param_id.module_id = module_id; + config_param_id.configuration_parameter_name = config_param.name; + config_param_id.module_implementation_id = impl; + + const auto ocpp_cv_opt = this->variable_mapping->get_ocpp_cv(config_param_id); + + if (ocpp_cv_opt.has_value()) { + const auto& ocpp_cv = ocpp_cv_opt.value(); + ocpp::v2::ComponentVariable everest_cv = {component, get_variable(device_model_variable)}; + // store mapping from EVerest OCPP ComponentVariable and the OCPP representation of it + this->variable_mapping->add_cv_mapping(everest_cv, ocpp_cv); + this->stored_in_everest_config_service.insert(ocpp_cv); + } + + ocpp::v2::ComponentVariable component_variable{component, get_variable(device_model_variable)}; + this->stored_in_everest_config_service.insert(component_variable); + } catch (const std::exception& e) { + EVLOG_error << "Could not add EVerest config entry to OCPP device model: " << e.what(); + } + } + } + + return component_config; +} + +ocpp::v2::DeviceModelMap EverestDeviceModelStorage::apply_mappings(const ocpp::v2::DeviceModelMap& device_model_map) { + ocpp::v2::DeviceModelMap mapped_device_model; + + for (const auto& [component, variable_map] : device_model_map) { + for (const auto& [variable, variable_meta_data] : variable_map) { + ocpp::v2::ComponentVariable everest_component_variable{component, variable}; + const auto& ocpp_component_variable_opt = this->variable_mapping->get_ocpp_cv(everest_component_variable); + if (ocpp_component_variable_opt.has_value()) { + const auto& ocpp_component_variable = ocpp_component_variable_opt.value(); + EVLOG_info << "Mapping identified for component variable: " << ocpp_component_variable; + + if (!ocpp_component_variable.variable.has_value()) { + EVLOG_warning << "No variable defined for component variable: " << ocpp_component_variable; + continue; + } + + ocpp::v2::VariableMetaData ocpp_variable_meta_data; + ocpp_variable_meta_data.characteristics = variable_meta_data.characteristics; + ocpp_variable_meta_data.monitors = variable_meta_data.monitors; + ocpp_variable_meta_data.source = variable_meta_data.source; + + mapped_device_model[ocpp_component_variable.component][ocpp_component_variable.variable.value()] = + ocpp_variable_meta_data; + } + mapped_device_model[component][variable] = variable_meta_data; + } + } + + return mapped_device_model; } void EverestDeviceModelStorage::init_hw_capabilities( @@ -270,41 +348,6 @@ void EverestDeviceModelStorage::init_hw_capabilities( } } -void EverestDeviceModelStorage::init_everest_config() { - for (const auto& [module_id_type, module_config] : this->module_configs) { - for (const auto& [impl, config_params] : module_config) { - std::string prefix; - if (impl != Everest::config::MODULE_IMPLEMENTATION_ID) { - // prefix variable name with impl + . - prefix = impl + "."; - } - for (const auto& config_param : config_params) { - try { - const auto variable_name = prefix + config_param.name; - - Component component; - component.name = module_id_type.module_type; - component.instance = module_id_type.module_id; - Variable variable; - variable.name = variable_name; - ocpp::v2::ComponentVariable component_variable; - component_variable.component = component; - component_variable.variable = variable; - // allows to differentiate variables backed by the EVerest config from other device model variables - this->stored_in_everest_config_service.insert(component_variable); - - std::lock_guard lock(device_model_mutex); - this->device_model_storage->set_variable_attribute_value( - component, variable, ocpp::v2::AttributeEnum::Actual, - get_everest_config_value(module_config, impl, config_param.name), VARIABLE_SOURCE_EVEREST); - } catch (const std::exception& e) { - EVLOG_error << "Could not initialize EVerest config entry in OCPP device model: " << e.what(); - } - } - } - } -} - void EverestDeviceModelStorage::update_hw_capabilities( const Component& evse_component, const types::evse_board_support::HardwareCapabilities& hw_capabilities) { std::lock_guard lock(device_model_mutex); @@ -324,7 +367,10 @@ void EverestDeviceModelStorage::update_power(const int32_t evse_id, const float ocpp::v2::DeviceModelMap EverestDeviceModelStorage::get_device_model() { std::lock_guard lock(device_model_mutex); - return this->device_model_storage->get_device_model(); + // Get the device model from the storage and apply variable mappings + auto device_model = this->device_model_storage->get_device_model(); + // The device model in the storage does not contain the mappings, so we apply the defined mappings to it + return this->apply_mappings(device_model); } std::optional @@ -332,7 +378,23 @@ EverestDeviceModelStorage::get_variable_attribute(const ocpp::v2::Component& com const ocpp::v2::Variable& variable_id, const ocpp::v2::AttributeEnum& attribute_enum) { std::lock_guard lock(device_model_mutex); - return this->device_model_storage->get_variable_attribute(component_id, variable_id, attribute_enum); + + // check if a mapping exists + ocpp::v2::ComponentVariable component_variable{component_id, variable_id}; + const auto everest_component_variable_opt = this->variable_mapping->get_everest_cv(component_variable); + if (everest_component_variable_opt.has_value()) { + const auto& everest_component_variable = everest_component_variable_opt.value(); + + if (!everest_component_variable.variable.has_value()) { + EVLOG_warning << "No variable defined for mapped component variable: " << everest_component_variable; + return std::nullopt; + } + + return this->device_model_storage->get_variable_attribute( + everest_component_variable.component, everest_component_variable.variable.value(), attribute_enum); + } else { + return this->device_model_storage->get_variable_attribute(component_id, variable_id, attribute_enum); + } } std::vector @@ -340,7 +402,22 @@ EverestDeviceModelStorage::get_variable_attributes(const ocpp::v2::Component& co const ocpp::v2::Variable& variable_id, const std::optional& attribute_enum) { std::lock_guard lock(device_model_mutex); - return this->device_model_storage->get_variable_attributes(component_id, variable_id, attribute_enum); + // check if a mapping exists + ocpp::v2::ComponentVariable component_variable{component_id, variable_id}; + const auto everest_component_variable_opt = this->variable_mapping->get_everest_cv(component_variable); + if (everest_component_variable_opt.has_value()) { + const auto& everest_component_variable = everest_component_variable_opt.value(); + + if (!everest_component_variable.variable.has_value()) { + EVLOG_warning << "No variable defined for mapped component variable: " << everest_component_variable; + return {}; + } + + return this->device_model_storage->get_variable_attributes( + everest_component_variable.component, everest_component_variable.variable.value(), attribute_enum); + } else { + return this->device_model_storage->get_variable_attributes(component_id, variable_id, attribute_enum); + } } ocpp::v2::SetVariableStatusEnum EverestDeviceModelStorage::set_variable_attribute_value( @@ -349,26 +426,29 @@ ocpp::v2::SetVariableStatusEnum EverestDeviceModelStorage::set_variable_attribut std::lock_guard lock(device_model_mutex); - int evse_id = 0; - if (component_id.evse.has_value()) { - evse_id = component_id.evse.value().id; - } - ocpp::v2::ComponentVariable component_variable; component_variable.component = component_id; component_variable.variable = variable_id; + + const auto everest_component_variable_opt = this->variable_mapping->get_everest_cv(component_variable); + if (everest_component_variable_opt.has_value()) { + // there is a mapping, so the incoming component variable is not present in the storage, we need to map it to + // the EVerest representation of it and set this + component_variable = everest_component_variable_opt.value(); + } + auto stored_in_everest_config_service_it = this->stored_in_everest_config_service.find(component_variable); if (stored_in_everest_config_service_it != this->stored_in_everest_config_service.end()) { if (attribute_enum != ocpp::v2::AttributeEnum::Actual) { return ocpp::v2::SetVariableStatusEnum::Rejected; } - if (not component_id.instance.has_value()) { + if (not component_variable.component.instance.has_value()) { return ocpp::v2::SetVariableStatusEnum::Rejected; } - const auto module_id = component_id.instance.value(); + const auto module_id = component_variable.component.instance.value(); everest::config::ConfigurationParameterIdentifier identifier; identifier.module_id = module_id; - const std::string variable_name = variable_id.name; + const std::string variable_name = component_variable.variable.value().name; const auto strpos = variable_name.find("."); if (strpos != std::string::npos) { identifier.module_implementation_id = variable_name.substr(0, strpos); @@ -382,12 +462,12 @@ ocpp::v2::SetVariableStatusEnum EverestDeviceModelStorage::set_variable_attribut if (result.set_status == everest::config::SetConfigStatus::Accepted) { // immediately set it in the libocpp device model as well - const auto libocpp_result = this->device_model_storage->set_variable_attribute_value( - component_id, variable_id, attribute_enum, value, source); - if (libocpp_result != ocpp::v2::SetVariableStatusEnum::Accepted) { + const auto everest_dm_result = this->device_model_storage->set_variable_attribute_value( + component_variable.component, component_variable.variable.value(), attribute_enum, value, source); + if (everest_dm_result != ocpp::v2::SetVariableStatusEnum::Accepted) { EVLOG_error << "Device model set variable results disagree"; } - return libocpp_result; // FIXME: what to return, libocpp or EVerest result? + return everest_dm_result; // FIXME: what to return, libocpp or EVerest result? } else if (result.set_status == everest::config::SetConfigStatus::Rejected) { return ocpp::v2::SetVariableStatusEnum::Rejected; } else if (result.set_status == everest::config::SetConfigStatus::RebootRequired) { diff --git a/modules/OCPP201/device_model/everest_device_model_storage.hpp b/modules/OCPP201/device_model/everest_device_model_storage.hpp index e1cc539e83..5f5e92024f 100644 --- a/modules/OCPP201/device_model/everest_device_model_storage.hpp +++ b/modules/OCPP201/device_model/everest_device_model_storage.hpp @@ -9,8 +9,10 @@ #include #include +#include #include #include +#include #include namespace module::device_model { @@ -20,6 +22,7 @@ class EverestDeviceModelStorage : public ocpp::v2::DeviceModelStorageInterface { const std::vector>& r_evse_manager, const std::map& evse_hardware_capabilities_map, const std::filesystem::path& db_path, const std::filesystem::path& migration_files_path, + std::unique_ptr variable_mapping, std::shared_ptr config_service_client); virtual ~EverestDeviceModelStorage() override = default; virtual ocpp::v2::DeviceModelMap get_device_model() override; @@ -55,11 +58,15 @@ class EverestDeviceModelStorage : public ocpp::v2::DeviceModelStorageInterface { std::shared_ptr config_service_client; std::map module_configs; std::map mappings; + std::unique_ptr variable_mapping; void init_hw_capabilities( const std::map& evse_hardware_capabilities_map); void update_hw_capabilities(const ocpp::v2::Component& evse_component, const types::evse_board_support::HardwareCapabilities& hw_capabilities); - void init_everest_config(); + std::vector + build_everest_config_variables(const everest::config::ModuleConfigurationParameters& module_config, + const std::string& module_id, const ocpp::v2::Component& component); + ocpp::v2::DeviceModelMap apply_mappings(const ocpp::v2::DeviceModelMap& device_model_map); }; } // namespace module::device_model diff --git a/modules/OCPP201/device_model/mapping/mapping.yaml b/modules/OCPP201/device_model/mapping/mapping.yaml new file mode 100644 index 0000000000..d9f16eed14 --- /dev/null +++ b/modules/OCPP201/device_model/mapping/mapping.yaml @@ -0,0 +1,19 @@ +mappings: + - ocpp: + component: + name: "TxCtrlr" + variable: + name: "EVConnectionTimeOut" + everest: + module_id: "auth" + configuration_parameter_name: "connection_timeout" + module_implementation_id: "!module" + + - ocpp: + component: + name: "AuthCtrlr" + variable: + name: "MasterPassGroupId" + everest: + module_id: "auth" + configuration_parameter_name: "master_pass_group_id" diff --git a/modules/OCPP201/device_model/mapping/mapping_schema.json b/modules/OCPP201/device_model/mapping/mapping_schema.json new file mode 100644 index 0000000000..bb9302aaa4 --- /dev/null +++ b/modules/OCPP201/device_model/mapping/mapping_schema.json @@ -0,0 +1,63 @@ +{ + "$schema": "http://json-schema.org/draft-07/schema#", + "title": "ComponentVariable to ConfigurationParameterIdentifier Mapping", + "type": "object", + "properties": { + "mappings": { + "type": "array", + "items": { + "type": "object", + "properties": { + "ocpp": { + "type": "object", + "properties": { + "component": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "instance": { "type": "string" }, + "evse": { + "type": "object", + "properties": { + "id": { "type": "integer" }, + "connectorId": { "type": "integer" } + }, + "required": ["id"], + "additionalProperties": false + } + }, + "required": ["name"], + "additionalProperties": false + }, + "variable": { + "type": "object", + "properties": { + "name": { "type": "string" }, + "instance": { "type": "string" } + }, + "required": ["name"], + "additionalProperties": false + } + }, + "required": ["component", "variable"], + "additionalProperties": false + }, + "everest": { + "type": "object", + "properties": { + "module_id": { "type": "string" }, + "configuration_parameter_name": { "type": "string" }, + "module_implementation_id": { "type": "string" } + }, + "required": ["module_id", "configuration_parameter_name"], + "additionalProperties": false + } + }, + "required": ["ocpp", "everest"], + "additionalProperties": false + } + } + }, + "required": ["mappings"], + "additionalProperties": false +} diff --git a/modules/OCPP201/device_model/mapping/variable_mapping.cpp b/modules/OCPP201/device_model/mapping/variable_mapping.cpp new file mode 100644 index 0000000000..a9dc3877dd --- /dev/null +++ b/modules/OCPP201/device_model/mapping/variable_mapping.cpp @@ -0,0 +1,62 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#include +#include +#include +#include + +using namespace ocpp::v2; +using namespace everest::config; + +VariableMapping::VariableMapping(const fs::path& mapping_file, const fs::path& schema_file) { + if (!fs::exists(mapping_file)) { + EVLOG_warning << "Mapping file does not exist: " << mapping_file; + return; + } + + const auto& mapping = Everest::load_yaml(mapping_file); + const auto& schema = Everest::load_yaml(schema_file); + + auto validator = nlohmann::json_schema::json_validator{}; + validator.set_root_schema(schema); + validator.validate(mapping); + + for (const auto& entry : mapping["mappings"]) { + ComponentVariable cv = entry["ocpp"]; + ConfigurationParameterIdentifier cpi = entry["everest"]; + user_mapping[cpi] = cv; + } +}; + +void VariableMapping::add_cv_mapping(const ComponentVariable& everest_component_variable, + const ComponentVariable& ocpp_component_variable) { + this->everest_cv_to_ocpp_cv_mapping[everest_component_variable] = ocpp_component_variable; + this->ocpp_cv_to_everest_cv_mapping[ocpp_component_variable] = everest_component_variable; +} + +std::optional VariableMapping::get_ocpp_cv(const ConfigurationParameterIdentifier& identifier) { + auto it = user_mapping.find(identifier); + if (it != user_mapping.end()) { + return it->second; + } + return std::nullopt; +} + +std::optional +VariableMapping::get_ocpp_cv(const ocpp::v2::ComponentVariable& everest_component_variable) { + auto it = everest_cv_to_ocpp_cv_mapping.find(everest_component_variable); + if (it != everest_cv_to_ocpp_cv_mapping.end()) { + return it->second; + } + return std::nullopt; +} + +std::optional +VariableMapping::get_everest_cv(const ocpp::v2::ComponentVariable& ocpp_component_variable) { + auto it = ocpp_cv_to_everest_cv_mapping.find(ocpp_component_variable); + if (it != ocpp_cv_to_everest_cv_mapping.end()) { + return it->second; + } + return std::nullopt; +} diff --git a/modules/OCPP201/device_model/mapping/variable_mapping.hpp b/modules/OCPP201/device_model/mapping/variable_mapping.hpp new file mode 100644 index 0000000000..9668a7fca3 --- /dev/null +++ b/modules/OCPP201/device_model/mapping/variable_mapping.hpp @@ -0,0 +1,47 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest + +#pragma once + +#include +#include +#include + +/// \brief This class is used to map EVerest module configuration parameters to OCPP component variables +class VariableMapping { + +public: + /// \brief Constructor that loads the mapping from the given \p mapping_file and validates it against the schema in + /// \p schema_file + VariableMapping(const fs::path& mapping_file, const fs::path& schema_file); + + /// \brief EVerest modules are represented as OCPP component variables in the OCPP device model. The following + /// functions adds a bi-directional mapping for the EVerest component variables and (mostly standardized) OCPP + /// component variables. + void add_cv_mapping(const ocpp::v2::ComponentVariable& everest_component_variable, + const ocpp::v2::ComponentVariable& ocpp_component_variable); + + /// \brief Gets a component variable from the given ȨVerest configuration parameter \p identifier + /// \return An optional OCPP component variable if the mapping exists, otherwise std::nullopt + std::optional + get_ocpp_cv(const everest::config::ConfigurationParameterIdentifier& identifier); + + /// \brief Gets a component variable from the given \p everest_component_variable + /// \return An optional OCPP component variable if the mapping exists, otherwise std::nullopt + std::optional + get_ocpp_cv(const ocpp::v2::ComponentVariable& everest_component_variable); + + /// \brief Gets the EVerest component variable for the given \p ocpp_component_variable + std::optional + get_everest_cv(const ocpp::v2::ComponentVariable& ocpp_component_variable); + +private: + std::map + user_mapping; // Maps EVerest configuration parameters to OCPP component variables + + // EVerest modules are represented as OCPP component variables in the OCPP device model. The following maps are + // bi-directional mappings to map between EVerest component variables and (mostly standardized) OCPP component + // variables. + std::map everest_cv_to_ocpp_cv_mapping; + std::map ocpp_cv_to_everest_cv_mapping; +}; \ No newline at end of file diff --git a/modules/OCPP201/doc.rst b/modules/OCPP201/doc.rst index 79bf61e832..a603fb71e3 100644 --- a/modules/OCPP201/doc.rst +++ b/modules/OCPP201/doc.rst @@ -364,14 +364,22 @@ than the value configured for `CompositeScheduleIntervalS` because otherwise tim Device model implementation details ----------------------------------- -For managing configuration and telemetry data of a charging station, the OCPP2 specification introduces -a device model that is very different to the design of OCPP1.6. -The specified device model comes with these high-level requirements: +This module provides a complete implementation of the OCPP 2.x Device Model by combining multiple variable sources into a unified view that can be exposed to the Central System (CSMS). +The design supports both standardized and vendor-specific variables from libocpp and EVerest modules through a flexible, pluggable architecture. -* 3-tier model: Break charging station down into 3 main tiers: ChargingStation, EVSE and Connector -* Components and Variables: Break down charging station into components and variables for configuration and telemetry -* Complex data structure for reporting and configuration of variables -* Device model contains variables of the whole charging station, beyond OCPP business logic +Overview of OCPP Device Model +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +The Device Model (introduced in OCPP 2.0.1 and extended in 2.1) is a hierarchical, structured data model to manage the configuration and telemetry of a charging station. It defines: + +* **Components** (e.g., ChargingStation, EVSE, TxCtrlr) +* **Variables** (e.g., HeartbeatInterval, AllowReset) + +This enables the CSMS to: + +* Dynamically discover station capabilities +* Read/write configuration values +* Monitor status and telemetry The device model of OCPP2 can contain various physical or logical components and variables. While in OCPP1.6 almost all of the standardized configuration keys are used to influence the control flow of @@ -379,51 +387,146 @@ libocpp, in OCPP2 the configuration and telemetry variables that can be part of control or reporting capabilities of only libocpp. Still there is a large share of standardized variables in OCPP2 that do influence the control flow of libocpp. -Internally and externally managed variables -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -EVerest has multiple different data sources that control the values variables that OCPP requires to report to the CSMS. -It is therefore required to make a distinction between **internally** and **externally** managed variables of the device model. - -We define **internally** and **externally** managed variables as follows: - -* Internally Managed: Owned, stored and accessed in libocpp in device model storage - Examples: HeartbeatInterval, AuthorizeRemoteStart, SampledDataTxEndedMeasurands, AuthCacheStorage -* Externally Managed: Owned, stored and accessed via EVerest config service (not yet supported) - Examples: ConnectionTimeout, MasterPassGroupId -* For externally managed variables a mapping to the EVerest configuration parameter is defined (not yet supported) - -Note that the EVerest config service is not yet implemented. Currently all components and variables are controlled -by the libocpp device model storage implementation. - -Device Model Implementation of this module -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Challenges in EVerest +^^^^^^^^^^^^^^^^^^^^^ + +EVerest's modular architecture presents specific challenges: + +* **Fixed hierarchy vs. loosely coupled modules**: OCPP enforces a strict component/variable structure, while EVerest modules are independently configured and instantiated. +* **Existing configurations differ**: EVerest module configs use their own names, types, and metadata and do not always simply map to OCPP variables. +* **High metadata overhead**: Each variable in the Device Model requires detailed attributes (e.g., characteristics, mutability, monitoring). +* **No distinction between config and telemetry**: OCPP treats all variables uniformly, while EVerest separates static config (for modules) and dynamic telemetry (mostly vars of EVerest interfaces). + +Solution: Composed Device Model +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +To address these issues, a **ComposedDeviceModel** abstraction is introduced as part of this module. It combines two sub device models: + +* **Libocpp Device Model**: Manages standardized OCPP variables used directly by libocpp. Defined via JSON component configs. Stored in SQLite database using the DeviceModelStorageSqlite. +* **EVerest Device Model**: Maps EVerest module configurations to OCPP Device Model components and variables. Provides standardized EVSE and Connector +components and respective variables. Is using EVerest configuration service to read and write EVerest module configuration parameters. Allows mapping of EVerest +module parameters to OCPP variables. The OCPP representation is also stored in SQLite database using the DeviceModelStorageSqlite. + +Mapping Strategy for EVerest Device Model +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +| **OCPP Element** | **EVerest Source** | **Description** | +| ------------------------------------------ | ----------------------------------------------------- | ---------------------------------------------------- | +| Component.name | `ModuleIdType.module_type` | EVerest module type (e.g., `EvseManager`, `EvseV2G`) | +| Component.instance | `ModuleIdTypemodule_id` | ID from EVerest config | +| Component.evse.id | `ModuleTierMappings.module.evse` | EVSE ID (if mapped) | +| Component.evse.connector\_id | `ModuleTierMappings.module.connector` | Connector ID (if mapped) | +| Variables | `ModuleConfigurationParameters` | Each parameter becomes an OCPP variable | +| Variable.name | `impl + "." + config_param.name` (if impl != !module) | Prefix with implementation name if needed | +| VariableCharacteristics.dataType | `config_param.characteristics.datatype` | Translated to OCPP type | +| VariableCharacteristics.unit | *(TODO)* | Not yet implemented | +| VariableCharacteristics.supportsMonitoring | `false` *(TODO)* | Monitoring is currently not supported | +| VariableAttribute.mutability | `config_param.characteristics.mutability` | Translated to OCPP mutability enum | +| VariableAttribute.value | `config_param.value` | Actual value from config | + +The `ComposedDeviceModel` is passed to libocpp and routes OCPP operations to the correct source, avoiding duplication and supporting clear separation of ownership. + +User Mapping of EVerest Configuration Parameters to OCPP Variables +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +EVerest modules expose their configuration parameters in a format that does not always directly match the standardized OCPP 2.x Device Model. To bridge this gap, +the **Variable Mapping** mechanism allows users a flexible and explicit mapping between: + +* **EVerest Configuration Parameters** (identified by `module_id`, `configuration_parameter_name`, and optionally an `implementation_id`) +* **Standardized OCPP Component Variables** (identified by `component` and `variable` names in the Device Model) + +This mapping is defined in a YAML file (`mapping.yaml`) and validated against a JSON schema (`mapping_schema.json`) at runtime. The mapping supports bidirectional translation: + +* **When exposing the Device Model**: EVerest configuration parameters are mapped to the appropriate standardized OCPP components and variables before being sent to the CSMS. +* **When processing incoming OCPP requests**: Operations such as `SetVariables.req` are resolved to the corresponding EVerest configuration parameter, ensuring the correct module is updated. + +Example for `mapping.yaml`: + +```yaml +mappings: + - ocpp: + component: + name: "TxCtrlr" + variable: + name: "EVConnectionTimeOut" + everest: + module_id: "auth" + configuration_parameter_name: "connection_timeout" + module_implementation_id: "!module" +``` + +This example maps the OCPP `TxCtrlr.EVConnectionTimeout` variable to the `auth.connection_timeout` configuration parameter of the EVerest module. + +Note that when the CSMS requests the Device Model (e.g. via GetBaseReport.req), both representations will be shown to the CSMS. +For the example mapping defined that would mean an extract of the NotifyReport.req would contain: + +```json +... +{ + "component": { + "instance": "auth", + "name": "Auth" + }, + "variable": { + "name": "connection_timeout" + }, + "variableAttribute": [ + { + "constant": false, + "mutability": "ReadWrite", + "persistent": true, + "type": "Actual", + "value": "15" + } + ], + "variableCharacteristics": { + "dataType": "integer", + "supportsMonitoring": false + } +}, +{ + "component": { + "name": "TxCtrlr" + }, + "variable": { + "name": "EVConnectionTimeOut" + }, + "variableAttribute": [ + { + "constant": false, + "mutability": "ReadWrite", + "persistent": true, + "type": "Actual", + "value": "15" + } + ], + "variableCharacteristics": { + "dataType": "integer", + "supportsMonitoring": false + } +}, +... +``` + +EVerest Configuration Service +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -This module provides an implementation of device model API provided as part of libocpp (it implements -`device_model_storage_interface.hpp`). -The implementation is designed to fullfill the requirements of the device model API even if the components and variables are -controlled by different sources (Internally, Externally). +The **EVerest Configuration Service** is a central interface for reading and writing module configuration parameters within the EVerest runtime. It enables: +When used with the Device Model, the Configuration Service acts as the **backend for EVerest configuration parameters**. For example, if the CSMS sets an +OCPP configuration variable that maps to an EVerest parameter, the request is transparently translated into a call to the Configuration Service, +which applies the change to the correct module. -Device Model Sources -^^^^^^^^^^^^^^^^^^^^ +Device Model Implementation Architecture +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -Device Model variables are defined in JSON component configs. For each variable a property `source` can be used to define -the source that controls it. This design allows for a single source of truth for each variable and it -allows the device model implementation of this module to address the correct source for the requested operation. -Today `OCPP` is the only supported source for internally managed variables. +Class diagram for device model: -Sources for externally managed configuration variables like the EVerest config service are under development. +.. image:: doc/device_model_class_diagram.png -Sequence of variable access for internally and externally managed variables -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ +Sequence of variable access for internally and externally managed variables: .. image:: doc/sequence_config_service_and_ocpp.png -Class diagram for device model -^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ - -.. image:: doc/device_model_class_diagram.png - Clarification of the device model classes of this diagram: * DeviceModel: @@ -440,15 +543,15 @@ Clarification of the device model classes of this diagram: * DeviceModelStorageSqlite * Implements DeviceModelStorageInterface as part of libocpp - * This storage holds internally managed variables + * Is used in libocpp as well as in EVerest device model as storage backend * EverestDeviceModelStorage * Implements DeviceModelStorageInterface as part of everest-core (OCPP201 module) * Uses EVerest config service to retrieve configuration variables of EVerest modules + * Uses DeviceModelStorageSqlite to store OCPP representation of EVerest configuration parameters and EVSE and Connector components * ComposedDeviceModelStorage * (Final) implementation of DeviceModelStorageInterface as part of everest-core (OCPP201 module) * A reference of this class will be passed to libocpp's ChargePoint constructor - * Differentiates between externally and internally managed variables diff --git a/modules/OCPP201/doc/device_model_class_diagram.png b/modules/OCPP201/doc/device_model_class_diagram.png index 1b989dab31..a9a0e67e69 100644 Binary files a/modules/OCPP201/doc/device_model_class_diagram.png and b/modules/OCPP201/doc/device_model_class_diagram.png differ diff --git a/modules/OCPP201/doc/device_model_class_diagram.puml b/modules/OCPP201/doc/device_model_class_diagram.puml index bca34cbf73..a0643b9b6d 100644 --- a/modules/OCPP201/doc/device_model_class_diagram.puml +++ b/modules/OCPP201/doc/device_model_class_diagram.puml @@ -1,49 +1,37 @@ @startuml package libocpp { - -class ChargePoint { - - device_model: DeviceModel -} - -class DeviceModel { - - device_model: DeviceModelStorageInterface - + get_device_model(): DeviceModelRepresentation - + get_value(...): T - + set_value(...): SetVariableStatusEnum -} - -interface DeviceModelStorageInterface { - + get_device_model(): DeviceModelRepresentation - + get_variable_attribute(...): std::optional - + set_variable_attribute_value(...): bool -} - -class DeviceModelStorageSqlite implements DeviceModelStorageInterface - + class ChargePoint { + - device_model: DeviceModel + } + + class DeviceModel { + - device_model: DeviceModelStorageInterface + + get_device_model(): DeviceModelRepresentation + + get_value(...): T + + set_value(...): SetVariableStatusEnum + } + + interface DeviceModelStorageInterface { + + get_device_model(): DeviceModelRepresentation + + get_variable_attribute(...): std::optional + + set_variable_attribute_value(...): bool + } + + class DeviceModelStorageSqlite implements DeviceModelStorageInterface } package everest-core { - -class EverestDeviceModelStorage implements libocpp.DeviceModelStorageInterface -class ComposedDeviceModelStorage implements libocpp.DeviceModelStorageInterface { - - everest_storage: EverestDeviceModelStorage - - libocpp_storage: DeviceModelStorageSqlite + class EverestDeviceModelStorage implements libocpp.DeviceModelStorageInterface + class ComposedDeviceModelStorage implements libocpp.DeviceModelStorageInterface { + - everest_storage: EverestDeviceModelStorage + - libocpp_storage: DeviceModelStorageSqlite + } } -} - -note left of ChargePoint - ChargePoint and DeviceModel are - implemented within the library. -end note - -note right of ComposedDeviceModelStorage - This implementation will be passed to libocpp's constructor -end note ChargePoint *-- DeviceModel DeviceModel *-- DeviceModelStorageInterface ComposedDeviceModelStorage *-- EverestDeviceModelStorage -ComposedDeviceModelStorage *-- DeviceModelStorageSqlite +EverestDeviceModelStorage *-- DeviceModelStorageSqlite -@enduml +@enduml \ No newline at end of file diff --git a/modules/OCPP201/doc/sequence_config_service_and_ocpp.png b/modules/OCPP201/doc/sequence_config_service_and_ocpp.png index 7e0b87995e..c0ca93b4c3 100644 Binary files a/modules/OCPP201/doc/sequence_config_service_and_ocpp.png and b/modules/OCPP201/doc/sequence_config_service_and_ocpp.png differ diff --git a/modules/OCPP201/doc/sequence_config_service_and_ocpp.puml b/modules/OCPP201/doc/sequence_config_service_and_ocpp.puml index b32528b25d..0cb86833bf 100644 --- a/modules/OCPP201/doc/sequence_config_service_and_ocpp.puml +++ b/modules/OCPP201/doc/sequence_config_service_and_ocpp.puml @@ -1,5 +1,5 @@ @startuml -'https://plantuml.com/sequence-diagram + !pragma teoz true participant CSMS order 10 participant libocpp order 20 @@ -7,40 +7,27 @@ participant ComposedDeviceModel order 30 database DeviceModelStorageSqlite order 40 database EverestDeviceModelStorage order 50 -autonumber "" -skinparam sequenceArrowThickness 2 - -== Get Device Model at startup == +autonumber -ComposedDeviceModel->ComposedDeviceModel: Initialize device model based on component config -libocpp->ComposedDeviceModel: get_device_model -loop For each variable defined in component config - alt internally managed variable - ComposedDeviceModel->InternalStorage: get_value - InternalStorage->ComposedDeviceModel: get_value response - else externally managed variable - ComposedDeviceModel->ExternalStorage: get_value - ExternalStorage->ComposedDeviceModel: get_value response +== Get Device Model at startup == +libocpp -> ComposedDeviceModel: get_device_model() +loop for each variable + alt libocpp source + ComposedDeviceModel -> DeviceModelStorageSqlite: get_value() + else everest source + ComposedDeviceModel -> EverestDeviceModelStorage: get_value() end end -ComposedDeviceModel->libocpp: get_device_model response +ComposedDeviceModel --> libocpp: return model -== SetVariables.req by CSMS == -CSMS->libocpp: SetVariables.req -loop For each SetVariable request - libocpp->libocpp: Logical internal validation - libocpp->libocpp: Device Model validation - alt request is valid - libocpp->ComposedDeviceModel: set_value - alt internally managed variable - ComposedDeviceModel->InternalStorage: set_value - InternalStorage->ComposedDeviceModel: set_value response - else externally managed variable - ComposedDeviceModel->ExternalStorage: set_value - ExternalStorage->ComposedDeviceModel: set_value response - end - ComposedDeviceModel->libocpp: set_value response - end +== SetVariable.req == +CSMS -> libocpp: SetVariable.req +libocpp -> ComposedDeviceModel: set_value() +alt libocpp source + ComposedDeviceModel -> DeviceModelStorageSqlite: set_value() +else everest source + ComposedDeviceModel -> EverestDeviceModelStorage: set_value() end +ComposedDeviceModel --> libocpp: return status -@enduml +@enduml \ No newline at end of file diff --git a/modules/OCPP201/manifest.yaml b/modules/OCPP201/manifest.yaml index dc2a6927c0..8547c6c8e3 100644 --- a/modules/OCPP201/manifest.yaml +++ b/modules/OCPP201/manifest.yaml @@ -65,6 +65,11 @@ config: This is only used to prevent issues from passing by availlable before preparing on a restart. type: integer default: 0 + MappingFilePath: + description: >- + Path to the mapping file that contains the mapping between OCPP variables and Everest configuration parameters. + type: string + default: mapping.yaml provides: auth_validator: description: Validates the provided token using CSMS, AuthorizationList or AuthorizationCache