From 9b2fba5ee52f8998686757fa11950644fb2179b5 Mon Sep 17 00:00:00 2001 From: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> Date: Tue, 27 Aug 2024 10:26:48 -0500 Subject: [PATCH] K01 - Small cleanups (#746) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Use dependency injection for ChargePoint. Signed-off-by: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> * Added check for SmartChargingCtrlrAvailable to ChargePoint::handle_set_charging_profile_req Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> Signed-off-by: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> * Added test DISABLED_K01FR29_SmartChargingCtrlrAvailableIsFalse_RespondsCallError Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> Signed-off-by: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> * Added update to status doc Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> Signed-off-by: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> * Updated Component to SmartChargingCtrlrAvailableEnabled as per PR Review. Updated config to true Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> Signed-off-by: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> * updated status doc to represent all completed and planned K01 function requirements updated some comment and test names after community discussion. Signed-off-by: Coury Richards <146002925+couryrr-afs@users.noreply.github.com> Signed-off-by: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> * Refactor so that ChargePoint constructors share logic and remove duplication. Signed-off-by: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> * Remove duplicated comments/documentation. Co-authored-by: Piet GΓΆmpel <37657534+Pietfried@users.noreply.github.com> Signed-off-by: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> * Move test comments/documentation above relevant tests. Signed-off-by: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> * Reenable test; added positive test case. Signed-off-by: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> * Correct comment. Signed-off-by: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> * updated status document per PR comments to better highlight the reasoning Signed-off-by: Coury Richards <146002925+couryrr-afs@users.noreply.github.com> Signed-off-by: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> * Rename SmartChargingCtrlrAvailableEnabled to SmartChargingCtrlrEnabled. Signed-off-by: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> * Update Smart Charging variables in new location. config.json was removed. Signed-off-by: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> * Add link to OCA question in status docs. Signed-off-by: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> * Use non-deprecated build kit Docker container. Signed-off-by: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> --------- Signed-off-by: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> Signed-off-by: Coury Richards <146002925+couryrr-afs@users.noreply.github.com> Co-authored-by: Christoph <367712+folkengine@users.noreply.github.com> Co-authored-by: Coury Richards <146002925+couryrr-afs@users.noreply.github.com> Co-authored-by: Piet GΓΆmpel <37657534+Pietfried@users.noreply.github.com> --- .github/workflows/build_and_test.yaml | 4 +- .../standardized/SmartChargingCtrlr.json | 7 +- doc/ocpp_201_status.md | 56 +++-- include/ocpp/v201/charge_point.hpp | 19 +- .../ocpp/v201/ctrlr_component_variables.hpp | 2 +- lib/ocpp/v201/charge_point.cpp | 196 ++++++++++++------ lib/ocpp/v201/ctrlr_component_variables.cpp | 2 +- lib/ocpp/v201/smart_charging.cpp | 4 +- tests/lib/ocpp/v201/test_charge_point.cpp | 143 ++++++++++++- .../ocpp/v201/test_smart_charging_handler.cpp | 10 +- 10 files changed, 330 insertions(+), 113 deletions(-) diff --git a/.github/workflows/build_and_test.yaml b/.github/workflows/build_and_test.yaml index 30e7e9fee..295061e17 100644 --- a/.github/workflows/build_and_test.yaml +++ b/.github/workflows/build_and_test.yaml @@ -40,8 +40,8 @@ jobs: rsync -a source/.ci/build-kit/ scripts - name: Pull docker container run: | - docker pull --platform=linux/x86_64 --quiet ghcr.io/everest/build-kit-alpine:latest - docker image tag ghcr.io/everest/build-kit-alpine:latest build-kit + docker pull --platform=linux/x86_64 --quiet ghcr.io/everest/everest-ci/build-kit-base:latest + docker image tag ghcr.io/everest/everest-ci/build-kit-base:latest build-kit - name: Run install with tests run: | docker run \ diff --git a/config/v201/component_config/standardized/SmartChargingCtrlr.json b/config/v201/component_config/standardized/SmartChargingCtrlr.json index 96cb71c77..3ca47b67e 100644 --- a/config/v201/component_config/standardized/SmartChargingCtrlr.json +++ b/config/v201/component_config/standardized/SmartChargingCtrlr.json @@ -28,13 +28,14 @@ "attributes": [ { "type": "Actual", - "mutability": "ReadOnly" + "mutability": "ReadOnly", + "value": true } ], "description": "Whether smart charging is supported.", "type": "boolean" }, - "SmartChargingCtrlrAvailableEnabled": { + "SmartChargingCtrlrEnabled": { "variable_name": "Enabled", "characteristics": { "supportsMonitoring": true, @@ -44,7 +45,7 @@ { "type": "Actual", "mutability": "ReadWrite", - "value": false + "value": true } ], "description": "Whether smart charging is enabled.", diff --git a/doc/ocpp_201_status.md b/doc/ocpp_201_status.md index 28d30a514..c99c62319 100644 --- a/doc/ocpp_201_status.md +++ b/doc/ocpp_201_status.md @@ -12,7 +12,6 @@ This document contains the status of which OCPP 2.0.1 numbered functional requir | ❓ | Actor responsible for or status of requirement is unknown | | πŸ€“ | Catch-all for FRs that are satisfied for other reasons (see the Remark column) | - ## General - General | ID | Status | Remark | @@ -1225,7 +1224,6 @@ This document contains the status of which OCPP 2.0.1 numbered functional requir |-----------|--------|--------| | J03.FR.04 | | | - ## SmartCharging - SetChargingProfile | ID | Status | Remark | @@ -1235,48 +1233,48 @@ This document contains the status of which OCPP 2.0.1 numbered functional requir | K01.FR.03 | 🌐 πŸ’‚ | `TxProfile`s without `transactionId`s are rejected. | | K01.FR.04 | βœ… | | | K01.FR.05 | βœ… | | -| K01.FR.06 | 🌐 | | -| K01.FR.07 | ⛽️ | Notified through the `signal_set_charging_profiles` callback. | +| K01.FR.06 | 🌐 πŸ’‚ | As part of validation any `ChargingProile` with a stackLevel - chargingProfilePurpose - evseId combination is rejected | +| K01.FR.07 | ⛽️ | K08 - Notified through the `signal_set_charging_profiles` callback. | | K01.FR.08 | 🌐 | `TxDefaultProfile`s are supported. | | K01.FR.09 | βœ… | | -| K01.FR.10 | βœ… | | -| K01.FR.11 | | | -| K01.FR.12 | | | -| K01.FR.13 | | | +| K01.FR.10 | ⛽️ | K08 - During validation `validFrom` and `validTo` are set if they are blank to support this | +| K01.FR.11 | ❎ | K08 - The application of `ChargingProfileSchedules` are done via the `CompositeSchedule` from `GetCompositeSchedule` | +| K01.FR.12 | ❎ | K08 - The application of `ChargingProfileSchedules` are done via the `CompositeSchedule` from `GetCompositeSchedule` | +| K01.FR.13 | ❎ | K08 - The application of `ChargingProfileSchedules` are done via the `CompositeSchedule` from `GetCompositeSchedule` | | K01.FR.14 | βœ… | | | K01.FR.15 | βœ… | | | K01.FR.16 | βœ… | | -| K01.FR.17 | | | -| K01.FR.19 | | | +| K01.FR.17 | ⛽️ | K08 - The application of `ChargingProfileSchedules` are done via the `CompositeSchedule` from `GetCompositeSchedule` | +| K01.FR.19 | βœ… | | | K01.FR.20 | βœ… | Suggests `ACPhaseSwitchingSupported` should be per EVSE, conflicting with the rest of the spec. | -| K01.FR.21 | | | +| K01.FR.21 | | There is an active community discussion on this topic. | | K01.FR.22 | | | | K01.FR.26 | βœ… | | | K01.FR.27 | βœ… | | | K01.FR.28 | βœ… | | -| K01.FR.29 | | | -| K01.FR.30 | | | -| K01.FR.31 | | | -| K01.FR.32 | βœ… | | +| K01.FR.29 | βœ… | | +| K01.FR.30 | ⛽️ | K08 - The application of `ChargingProfileSchedules` are done via the `CompositeSchedule` from `GetCompositeSchedule` | +| K01.FR.31 | βœ… | | +| K01.FR.32 | ⛽️ | K08 - The application of `ChargingProfileSchedules` are done via the `CompositeSchedule` from `GetCompositeSchedule` | | K01.FR.33 | βœ… | | -| K01.FR.34 | βœ… | | +| K01.FR.34 | | Defer to K15 - K17 work | | K01.FR.35 | βœ… | | -| K01.FR.36 | βœ… | | -| K01.FR.37 | | | -| K01.FR.38 | 🌐 πŸ’‚ | `ChargingStationMaxProfile`s with `Relative` for `chargingProfileKind` are rejected. | -| K01.FR.39 | 🌐 πŸ’‚ | New `TxProfile`s matching existing `(stackLevel, transactionId)` are rejected. | -| K01.FR.40 | 🌐 πŸ’‚ | `Absolute`/`Recurring` profiles without `startSchedule` fields are rejected. | -| K01.FR.41 | 🌐 πŸ’‚ | `Relative` profiles with `startSchedule` fields are rejected. | -| K01.FR.42 | | | -| K01.FR.43 | | | +| K01.FR.36 | ⛽️ | K08 | +| K01.FR.37 | ⛽️ | K08 | +| K01.FR.38 | βœ… | `ChargingStationMaxProfile`s with `Relative` for `chargingProfileKind` are rejected. | +| K01.FR.39 | βœ… | New `TxProfile`s matching existing `(stackLevel, transactionId)` are rejected. | +| K01.FR.40 | βœ… | `Absolute`/`Recurring` profiles without `startSchedule` fields are rejected. | +| K01.FR.41 | βœ… | `Relative` profiles with `startSchedule` fields are rejected. | +| K01.FR.42 | ⛽️ | | +| K01.FR.43 | | Open question to OCA - https://oca.causewaynow.com/wg/OCA-TWG/mail/thread/4254 | | K01.FR.44 | βœ… | We reject invalid profiles instead of modifying and accepting them. | | K01.FR.45 | βœ… | We reject invalid profiles instead of modifying and accepting them. | -| K01.FR.46 | | | -| K01.FR.47 | | | -| K01.FR.48 | | | +| K01.FR.46 | ⛽️ | K08 | +| K01.FR.47 | ⛽️ | K08 | +| K01.FR.48 | βœ… | | | K01.FR.49 | βœ… | | -| K01.FR.50 | | | -| K01.FR.51 | | | +| K01.FR.50 | ⛽️ | K08 | +| K01.FR.51 | ⛽️ | K08 | | K01.FR.52 | βœ… | | | K01.FR.53 | βœ… | | diff --git a/include/ocpp/v201/charge_point.hpp b/include/ocpp/v201/charge_point.hpp index dbbb29ad9..8077eb814 100644 --- a/include/ocpp/v201/charge_point.hpp +++ b/include/ocpp/v201/charge_point.hpp @@ -422,7 +422,7 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa std::unique_ptr evse_manager; // utility - std::unique_ptr> message_queue; + std::shared_ptr> message_queue; std::shared_ptr device_model; std::shared_ptr database_handler; @@ -507,6 +507,7 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa bool send(CallError call_error); // internal helper functions + void initialize(const std::map& evse_connector_structure, const std::string& message_log_path); void init_websocket(); WebsocketConnectionOptions get_ws_connection_options(const int32_t configuration_slot); void init_certificate_expiration_check_timers(); @@ -857,6 +858,22 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa const std::string& core_database_path, const std::string& sql_init_path, const std::string& message_log_path, const std::shared_ptr evse_security, const Callbacks& callbacks); + + /// \brief Construct a new ChargePoint object + /// \param evse_connector_structure Map that defines the structure of EVSE and connectors of the chargepoint. The + /// key represents the id of the EVSE and the value represents the number of connectors for this EVSE. The ids of + /// the EVSEs have to increment starting with 1. + /// \param device_model_storage device model storage instance + /// \param database_handler database handler instance + /// \param message_queue message queue instance + /// \param message_log_path Path to where logfiles are written to + /// \param evse_security Pointer to evse_security that manages security related operations + /// \param callbacks Callbacks that will be registered for ChargePoint + ChargePoint(const std::map& evse_connector_structure, std::shared_ptr device_model, + std::shared_ptr database_handler, + std::shared_ptr> message_queue, const std::string& message_log_path, + const std::shared_ptr evse_security, const Callbacks& callbacks); + ~ChargePoint(); void start(BootReasonEnum bootreason = BootReasonEnum::PowerUp) override; diff --git a/include/ocpp/v201/ctrlr_component_variables.hpp b/include/ocpp/v201/ctrlr_component_variables.hpp index 677b5c015..96ab3c7f1 100644 --- a/include/ocpp/v201/ctrlr_component_variables.hpp +++ b/include/ocpp/v201/ctrlr_component_variables.hpp @@ -194,7 +194,7 @@ extern const RequiredComponentVariable& OrganizationName; extern const RequiredComponentVariable& SecurityProfile; extern const ComponentVariable& ACPhaseSwitchingSupported; extern const ComponentVariable& SmartChargingCtrlrAvailable; -extern const ComponentVariable& SmartChargingCtrlrAvailableEnabled; +extern const ComponentVariable& SmartChargingCtrlrEnabled; extern const RequiredComponentVariable& EntriesChargingProfiles; extern const ComponentVariable& ExternalControlSignalsEnabled; extern const RequiredComponentVariable& LimitChangeSignificance; diff --git a/lib/ocpp/v201/charge_point.cpp b/lib/ocpp/v201/charge_point.cpp index 6383a3e6e..fec36c63d 100644 --- a/lib/ocpp/v201/charge_point.cpp +++ b/lib/ocpp/v201/charge_point.cpp @@ -62,6 +62,50 @@ bool Callbacks::all_callbacks_valid() const { this->transaction_event_response_callback.value() != nullptr); } +ChargePoint::ChargePoint(const std::map& evse_connector_structure, + std::shared_ptr device_model, std::shared_ptr database_handler, + std::shared_ptr> message_queue, + const std::string& message_log_path, const std::shared_ptr evse_security, + const Callbacks& callbacks) : + ocpp::ChargingStationBase(evse_security), + message_queue(message_queue), + device_model(device_model), + database_handler(database_handler), + registration_status(RegistrationStatusEnum::Rejected), + network_configuration_priority(0), + disable_automatic_websocket_reconnects(false), + skip_invalid_csms_certificate_notifications(false), + reset_scheduled(false), + reset_scheduled_evseids{}, + firmware_status(FirmwareStatusEnum::Idle), + upload_log_status(UploadLogStatusEnum::Idle), + bootreason(BootReasonEnum::PowerUp), + ocsp_updater(this->evse_security, this->send_callback( + MessageType::GetCertificateStatusResponse)), + monitoring_updater( + device_model, [this](const std::vector& events) { this->notify_event_req(events); }, + [this]() { return this->is_offline(); }), + csr_attempt(1), + client_certificate_expiration_check_timer([this]() { this->scheduled_check_client_certificate_expiration(); }), + v2g_certificate_expiration_check_timer([this]() { this->scheduled_check_v2g_certificate_expiration(); }), + callbacks(callbacks) { + + // Make sure the received callback struct is completely filled early before we actually start running + if (!this->callbacks.all_callbacks_valid()) { + EVLOG_AND_THROW(std::invalid_argument("All non-optional callbacks must be supplied")); + } + + if (!this->device_model) { + EVLOG_AND_THROW(std::invalid_argument("Device model should not be null")); + } + + if (!this->database_handler) { + EVLOG_AND_THROW(std::invalid_argument("Database handler should not be null")); + } + + initialize(evse_connector_structure, message_log_path); +} + ChargePoint::ChargePoint(const std::map& evse_connector_structure, const std::string& device_model_storage_address, const bool initialize_device_model, const std::string& device_model_migration_path, const std::string& device_model_config_path, @@ -114,76 +158,8 @@ ChargePoint::ChargePoint(const std::map& evse_connector_struct EVLOG_AND_THROW(std::invalid_argument("All non-optional callbacks must be supplied")); } - this->device_model->check_integrity(evse_connector_structure); - auto database_connection = std::make_unique(fs::path(core_database_path) / "cp.db"); this->database_handler = std::make_shared(std::move(database_connection), sql_init_path); - this->database_handler->open_connection(); - - // 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, bool initiated_by_trigger_message) { - 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; - } else { - this->status_notification_req(evse_id, connector_id, status, initiated_by_trigger_message); - return true; - } - }); - if (this->callbacks.cs_effective_operative_status_changed_callback.has_value()) { - this->component_state_manager->set_cs_effective_availability_changed_callback( - this->callbacks.cs_effective_operative_status_changed_callback.value()); - } - if (this->callbacks.evse_effective_operative_status_changed_callback.has_value()) { - this->component_state_manager->set_evse_effective_availability_changed_callback( - this->callbacks.evse_effective_operative_status_changed_callback.value()); - } - this->component_state_manager->set_connector_effective_availability_changed_callback( - this->callbacks.connector_effective_operative_status_changed_callback); - - auto transaction_meter_value_callback = [this](const MeterValue& _meter_value, EnhancedTransaction& transaction) { - if (_meter_value.sampledValue.empty() or !_meter_value.sampledValue.at(0).context.has_value()) { - EVLOG_info << "Not sending MeterValue due to no values"; - return; - } - - auto type = _meter_value.sampledValue.at(0).context.value(); - if (type != ReadingContextEnum::Sample_Clock and type != ReadingContextEnum::Sample_Periodic) { - EVLOG_info << "Not sending MeterValue due to wrong context"; - return; - } - - const auto filter_vec = utils::get_measurands_vec(this->device_model->get_value( - type == ReadingContextEnum::Sample_Clock ? ControllerComponentVariables::AlignedDataMeasurands - : ControllerComponentVariables::SampledDataTxUpdatedMeasurands)); - - const auto filtered_meter_value = utils::get_meter_value_with_measurands_applied(_meter_value, filter_vec); - - if (!filtered_meter_value.sampledValue.empty()) { - const auto trigger = type == ReadingContextEnum::Sample_Clock ? TriggerReasonEnum::MeterValueClock - : TriggerReasonEnum::MeterValuePeriodic; - this->transaction_event_req(TransactionEventEnum::Updated, DateTime(), transaction.get_transaction(), - trigger, transaction.get_seq_no(), std::nullopt, std::nullopt, std::nullopt, - std::vector(1, filtered_meter_value), std::nullopt, - this->is_offline(), std::nullopt); - } - }; - - this->evse_manager = std::make_unique( - evse_connector_structure, *this->device_model, this->database_handler, component_state_manager, - transaction_meter_value_callback, this->callbacks.pause_charging_callback); - - this->smart_charging_handler = - std::make_shared(*this->evse_manager, this->device_model, this->database_handler); - - // configure logging - this->configure_message_logging_format(message_log_path); - - // start monitoring - this->monitoring_updater.start_monitoring(); this->message_queue = std::make_unique>( [this](json message) -> bool { return this->websocket->send(message.dump()); }, @@ -197,7 +173,7 @@ ChargePoint::ChargePoint(const std::map& evse_connector_struct this->device_model->get_value(ControllerComponentVariables::MessageTimeout)}, this->database_handler); - this->auth_cache_cleanup_thread = std::thread(&ChargePoint::cache_cleanup_handler, this); + initialize(evse_connector_structure, message_log_path); } ChargePoint::~ChargePoint() { @@ -970,6 +946,73 @@ bool ChargePoint::send(CallError call_error) { return true; } +void ChargePoint::initialize(const std::map& evse_connector_structure, + const std::string& message_log_path) { + this->device_model->check_integrity(evse_connector_structure); + this->database_handler->open_connection(); + this->smart_charging_handler = + std::make_shared(*this->evse_manager, this->device_model, this->database_handler); + this->component_state_manager = std::make_shared( + evse_connector_structure, database_handler, + [this](auto evse_id, auto connector_id, auto status, bool initiated_by_trigger_message) { + 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; + } else { + this->status_notification_req(evse_id, connector_id, status, initiated_by_trigger_message); + return true; + } + }); + if (this->callbacks.cs_effective_operative_status_changed_callback.has_value()) { + this->component_state_manager->set_cs_effective_availability_changed_callback( + this->callbacks.cs_effective_operative_status_changed_callback.value()); + } + if (this->callbacks.evse_effective_operative_status_changed_callback.has_value()) { + this->component_state_manager->set_evse_effective_availability_changed_callback( + this->callbacks.evse_effective_operative_status_changed_callback.value()); + } + this->component_state_manager->set_connector_effective_availability_changed_callback( + this->callbacks.connector_effective_operative_status_changed_callback); + + auto transaction_meter_value_callback = [this](const MeterValue& _meter_value, EnhancedTransaction& transaction) { + if (_meter_value.sampledValue.empty() or !_meter_value.sampledValue.at(0).context.has_value()) { + EVLOG_info << "Not sending MeterValue due to no values"; + return; + } + + auto type = _meter_value.sampledValue.at(0).context.value(); + if (type != ReadingContextEnum::Sample_Clock and type != ReadingContextEnum::Sample_Periodic) { + EVLOG_info << "Not sending MeterValue due to wrong context"; + return; + } + + const auto filter_vec = utils::get_measurands_vec(this->device_model->get_value( + type == ReadingContextEnum::Sample_Clock ? ControllerComponentVariables::AlignedDataMeasurands + : ControllerComponentVariables::SampledDataTxUpdatedMeasurands)); + + const auto filtered_meter_value = utils::get_meter_value_with_measurands_applied(_meter_value, filter_vec); + + if (!filtered_meter_value.sampledValue.empty()) { + const auto trigger = type == ReadingContextEnum::Sample_Clock ? TriggerReasonEnum::MeterValueClock + : TriggerReasonEnum::MeterValuePeriodic; + this->transaction_event_req(TransactionEventEnum::Updated, DateTime(), transaction, trigger, + transaction.get_seq_no(), std::nullopt, std::nullopt, std::nullopt, + std::vector(1, filtered_meter_value), std::nullopt, + this->is_offline(), std::nullopt); + } + }; + + this->evse_manager = std::make_unique( + evse_connector_structure, *this->device_model, this->database_handler, component_state_manager, + transaction_meter_value_callback, this->callbacks.pause_charging_callback); + + this->configure_message_logging_format(message_log_path); + this->monitoring_updater.start_monitoring(); + + this->auth_cache_cleanup_thread = std::thread(&ChargePoint::cache_cleanup_handler, this); +} + void ChargePoint::init_websocket() { if (this->device_model->get_value(ControllerComponentVariables::ChargePointId).find(':') != @@ -3233,6 +3276,21 @@ void ChargePoint::handle_set_charging_profile_req(Calldevice_model->get_optional_value(ControllerComponentVariables::SmartChargingCtrlrAvailable) + .value_or(false); + + if (!is_smart_charging_available) { + EVLOG_warning << "SmartChargingCtrlrAvailable is not set for Charging Station. Returning NotSupported error"; + + const auto call_error = + CallError(call.uniqueId, "NotSupported", "Charging Station does not support smart charging", json({})); + this->send(call_error); + + return; + } + // K01.FR.22: Reject ChargingStationExternalConstraints profiles in SetChargingProfileRequest if (msg.chargingProfile.chargingProfilePurpose == ChargingProfilePurposeEnum::ChargingStationExternalConstraints) { response.statusInfo = StatusInfo(); diff --git a/lib/ocpp/v201/ctrlr_component_variables.cpp b/lib/ocpp/v201/ctrlr_component_variables.cpp index 074f11aaf..1607d6572 100644 --- a/lib/ocpp/v201/ctrlr_component_variables.cpp +++ b/lib/ocpp/v201/ctrlr_component_variables.cpp @@ -1081,7 +1081,7 @@ const ComponentVariable& SmartChargingCtrlrAvailable = { "Available", }), }; -const ComponentVariable& SmartChargingCtrlrAvailableEnabled = { +const ComponentVariable& SmartChargingCtrlrEnabled = { ControllerComponents::SmartChargingCtrlr, std::nullopt, std::optional({ diff --git a/lib/ocpp/v201/smart_charging.cpp b/lib/ocpp/v201/smart_charging.cpp index e05406741..6271ea120 100644 --- a/lib/ocpp/v201/smart_charging.cpp +++ b/lib/ocpp/v201/smart_charging.cpp @@ -391,12 +391,12 @@ SmartChargingHandler::validate_profile_schedules(ChargingProfile& profile, for (auto i = 0; i < schedule.chargingSchedulePeriod.size(); i++) { auto& charging_schedule_period = schedule.chargingSchedulePeriod[i]; - // K01.FR.19 + // K01.FR.48 and K01.FR.19 if (charging_schedule_period.numberPhases != 1 && charging_schedule_period.phaseToUse.has_value()) { return ProfileValidationResultEnum::ChargingSchedulePeriodInvalidPhaseToUse; } - // K01.FR.20 + // K01.FR.48 and K01.FR.20 if (charging_schedule_period.phaseToUse.has_value() && !device_model->get_optional_value(ControllerComponentVariables::ACPhaseSwitchingSupported) .value_or(false)) { diff --git a/tests/lib/ocpp/v201/test_charge_point.cpp b/tests/lib/ocpp/v201/test_charge_point.cpp index 5c82b6d5c..ebc2775e9 100644 --- a/tests/lib/ocpp/v201/test_charge_point.cpp +++ b/tests/lib/ocpp/v201/test_charge_point.cpp @@ -60,6 +60,13 @@ class ChargePointFixture : public DatabaseTestingUtils { charge_point->stop(); } + std::map create_evse_connector_structure() { + std::map evse_connector_structure; + evse_connector_structure.insert_or_assign(1, 1); + evse_connector_structure.insert_or_assign(2, 1); + return evse_connector_structure; + } + void create_device_model_db(const std::string& path) { InitDeviceModelDb db(path, MIGRATION_FILES_PATH); db.initialize_database(SCHEMAS_PATH, true); @@ -70,7 +77,6 @@ class ChargePointFixture : public DatabaseTestingUtils { create_device_model_db(DEVICE_MODEL_DB_IN_MEMORY_PATH); auto device_model_storage = std::make_unique(DEVICE_MODEL_DB_IN_MEMORY_PATH); auto device_model = std::make_shared(std::move(device_model_storage)); - // Defaults const auto& charging_rate_unit_cv = ControllerComponentVariables::ChargingScheduleChargingRateUnit; device_model->set_value(charging_rate_unit_cv.component, charging_rate_unit_cv.variable.value(), @@ -173,6 +179,27 @@ class ChargePointFixture : public DatabaseTestingUtils { std::unique_ptr charge_point = create_charge_point(); boost::uuids::random_generator uuid_generator = boost::uuids::random_generator(); + std::shared_ptr create_database_handler() { + auto database_connection = std::make_unique(fs::path("/tmp/ocpp201") / "cp.db"); + return std::make_shared(std::move(database_connection), MIGRATION_FILES_LOCATION_V201); + } + + std::shared_ptr> + create_message_queue(std::shared_ptr& database_handler) { + const auto DEFAULT_MESSAGE_QUEUE_SIZE_THRESHOLD = 2E5; + return std::make_shared>( + [this](json message) -> bool { return false; }, + MessageQueueConfig{ + this->device_model->get_value(ControllerComponentVariables::MessageAttempts), + this->device_model->get_value(ControllerComponentVariables::MessageAttemptInterval), + this->device_model->get_optional_value(ControllerComponentVariables::MessageQueueSizeThreshold) + .value_or(DEFAULT_MESSAGE_QUEUE_SIZE_THRESHOLD), + this->device_model->get_optional_value(ControllerComponentVariables::QueueAllMessages) + .value_or(false), + this->device_model->get_value(ControllerComponentVariables::MessageTimeout)}, + database_handler); + } + void configure_callbacks_with_mocks() { callbacks.is_reset_allowed_callback = is_reset_allowed_callback_mock.AsStdFunction(); callbacks.reset_callback = reset_callback_mock.AsStdFunction(); @@ -242,6 +269,68 @@ class ChargePointFixture : public DatabaseTestingUtils { ocpp::v201::Callbacks callbacks; }; +TEST_F(ChargePointFixture, CreateChargePoint) { + auto evse_connector_structure = create_evse_connector_structure(); + auto database_handler = create_database_handler(); + auto evse_security = std::make_shared(); + configure_callbacks_with_mocks(); + auto message_queue = create_message_queue(database_handler); + + EXPECT_NO_THROW(ocpp::v201::ChargePoint(evse_connector_structure, device_model, database_handler, message_queue, + "/tmp", evse_security, callbacks)); +} + +TEST_F(ChargePointFixture, CreateChargePoint_EVSEConnectorStructureDefinedBadly_ThrowsDeviceModelStorageError) { + auto database_handler = create_database_handler(); + auto evse_security = std::make_shared(); + configure_callbacks_with_mocks(); + auto message_queue = create_message_queue(database_handler); + + auto evse_connector_structure = std::map(); + + EXPECT_THROW(ocpp::v201::ChargePoint(evse_connector_structure, device_model, database_handler, message_queue, + "/tmp", evse_security, callbacks), + DeviceModelStorageError); +} + +TEST_F(ChargePointFixture, CreateChargePoint_MissingDeviceModel_ThrowsInvalidArgument) { + auto evse_connector_structure = create_evse_connector_structure(); + auto database_handler = create_database_handler(); + auto evse_security = std::make_shared(); + configure_callbacks_with_mocks(); + auto message_queue = std::make_shared>( + [this](json message) -> bool { return false; }, MessageQueueConfig{}, database_handler); + + EXPECT_THROW(ocpp::v201::ChargePoint(evse_connector_structure, nullptr, database_handler, message_queue, "/tmp", + evse_security, callbacks), + std::invalid_argument); +} + +TEST_F(ChargePointFixture, CreateChargePoint_MissingDatabaseHandler_ThrowsInvalidArgument) { + auto evse_connector_structure = create_evse_connector_structure(); + auto evse_security = std::make_shared(); + configure_callbacks_with_mocks(); + auto message_queue = std::make_shared>( + [this](json message) -> bool { return false; }, MessageQueueConfig{}, nullptr); + + auto database_handler = nullptr; + + EXPECT_THROW(ocpp::v201::ChargePoint(evse_connector_structure, device_model, database_handler, message_queue, + "/tmp", evse_security, callbacks), + std::invalid_argument); +} + +TEST_F(ChargePointFixture, CreateChargePoint_CallbacksNotValid_ThrowsInvalidArgument) { + auto evse_connector_structure = create_evse_connector_structure(); + auto database_handler = create_database_handler(); + auto evse_security = std::make_shared(); + auto message_queue = create_message_queue(database_handler); + + EXPECT_THROW(ocpp::v201::ChargePoint(evse_connector_structure, device_model, database_handler, message_queue, + "/tmp", evse_security, callbacks), + std::invalid_argument); +} + /* * K01.FR.02 states * @@ -620,4 +709,56 @@ TEST_F(ChargePointFixture, K01FR22_SetChargingProfileRequest_RejectsChargingStat charge_point->handle_message(set_charging_profile_req); } +TEST_F(ChargePointFixture, K01FR29_SmartChargingCtrlrAvailableIsFalse_RespondsCallError) { + auto evse_connector_structure = create_evse_connector_structure(); + auto database_handler = create_database_handler(); + auto evse_security = std::make_shared(); + configure_callbacks_with_mocks(); + + const auto cv = ControllerComponentVariables::SmartChargingCtrlrAvailable; + this->device_model->set_value(cv.component, cv.variable.value(), AttributeEnum::Actual, "false", "TEST", true); + + auto periods = create_charging_schedule_periods({0, 1, 2}); + auto profile = create_charging_profile( + DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), DEFAULT_TX_ID); + + SetChargingProfileRequest req; + req.evseId = DEFAULT_EVSE_ID; + req.chargingProfile = profile; + + auto set_charging_profile_req = + request_to_enhanced_message(req); + + EXPECT_CALL(*smart_charging_handler, validate_and_add_profile(testing::_, testing::_)).Times(0); + + charge_point->handle_message(set_charging_profile_req); +} + +TEST_F(ChargePointFixture, K01FR29_SmartChargingCtrlrAvailableIsTrue_CallsValidateAndAddProfile) { + auto evse_connector_structure = create_evse_connector_structure(); + auto database_handler = create_database_handler(); + auto evse_security = std::make_shared(); + configure_callbacks_with_mocks(); + + const auto cv = ControllerComponentVariables::SmartChargingCtrlrAvailable; + this->device_model->set_value(cv.component, cv.variable.value(), AttributeEnum::Actual, "true", "TEST", true); + + auto periods = create_charging_schedule_periods({0, 1, 2}); + auto profile = create_charging_profile( + DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), DEFAULT_TX_ID); + + SetChargingProfileRequest req; + req.evseId = DEFAULT_EVSE_ID; + req.chargingProfile = profile; + + auto set_charging_profile_req = + request_to_enhanced_message(req); + + EXPECT_CALL(*smart_charging_handler, validate_and_add_profile(profile, DEFAULT_EVSE_ID)); + + charge_point->handle_message(set_charging_profile_req); +} + } // namespace ocpp::v201 diff --git a/tests/lib/ocpp/v201/test_smart_charging_handler.cpp b/tests/lib/ocpp/v201/test_smart_charging_handler.cpp index 5dcf30ced..5bc280b9a 100644 --- a/tests/lib/ocpp/v201/test_smart_charging_handler.cpp +++ b/tests/lib/ocpp/v201/test_smart_charging_handler.cpp @@ -308,7 +308,7 @@ TEST_F(ChargepointTestFixtureV201, K01FR09_IfTxProfileEvseHasNoActiveTransaction EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::TxProfileEvseHasNoActiveTransaction)); } -TEST_F(ChargepointTestFixtureV201, K01FR19_NumberPhasesOtherThan1AndPhaseToUseSet_ThenProfileInvalid) { +TEST_F(ChargepointTestFixtureV201, K01FR48FR19_NumberPhasesOtherThan1AndPhaseToUseSet_ThenProfileInvalid) { auto periods = create_charging_schedule_periods_with_phases(0, 0, 1); auto profile = create_charging_profile( DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, @@ -320,7 +320,8 @@ TEST_F(ChargepointTestFixtureV201, K01FR19_NumberPhasesOtherThan1AndPhaseToUseSe EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingSchedulePeriodInvalidPhaseToUse)); } -TEST_F(ChargepointTestFixtureV201, K01FR20_IfPhaseToUseSetAndACPhaseSwitchingSupportedUndefined_ThenProfileIsInvalid) { +TEST_F(ChargepointTestFixtureV201, + K01FR48FR20_IfPhaseToUseSetAndACPhaseSwitchingSupportedUndefined_ThenProfileIsInvalid) { // As a device model with ac switching supported default set to 'true', we want to create a new database with the // ac switching support not set. But this is an in memory database, which is kept open until all handles to it are // closed. So we close all connections to the database. @@ -344,7 +345,7 @@ TEST_F(ChargepointTestFixtureV201, K01FR20_IfPhaseToUseSetAndACPhaseSwitchingSup testing::Eq(ProfileValidationResultEnum::ChargingSchedulePeriodPhaseToUseACPhaseSwitchingUnsupported)); } -TEST_F(ChargepointTestFixtureV201, K01FR20_IfPhaseToUseSetAndACPhaseSwitchingSupportedFalse_ThenProfileIsInvalid) { +TEST_F(ChargepointTestFixtureV201, K01FR48FR20_IfPhaseToUseSetAndACPhaseSwitchingSupportedFalse_ThenProfileIsInvalid) { // As a device model with ac switching supported default set to 'true', we want to create a new database with the // ac switching support not set. But this is an in memory database, which is kept open until all handles to it are // closed. So we close all connections to the database. @@ -368,7 +369,8 @@ TEST_F(ChargepointTestFixtureV201, K01FR20_IfPhaseToUseSetAndACPhaseSwitchingSup testing::Eq(ProfileValidationResultEnum::ChargingSchedulePeriodPhaseToUseACPhaseSwitchingUnsupported)); } -TEST_F(ChargepointTestFixtureV201, K01FR20_IfPhaseToUseSetAndACPhaseSwitchingSupportedTrue_ThenProfileIsNotInvalid) { +TEST_F(ChargepointTestFixtureV201, + K01FR48FR20_IfPhaseToUseSetAndACPhaseSwitchingSupportedTrue_ThenProfileIsNotInvalid) { auto periods = create_charging_schedule_periods_with_phases(0, 1, 1); auto profile = create_charging_profile( DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile,