From 26746ac3eedcf3c7937521533359347411bc0043 Mon Sep 17 00:00:00 2001 From: Christopher Davis <150722105+christopher-davis-afs@users.noreply.github.com> Date: Tue, 6 Feb 2024 19:50:47 +0000 Subject: [PATCH] SmartChargingHandler: Add validation for TxProfiles Creates a skeleton for SmartChargingHandler and adds a method to validate TxProfiles and TxProfiles only. This method ensures that a given profile (assumed to be TxProfile) fits all of the functional requirements specific to TxProfiles per K01. The function, `validate_tx_profile ()`, takes a profile and an EVSE, and returns a variant of `ProfileValidationResultEnum`. This enum allows us to have detailed responses for the different types of invalid profiles, and eventually will let us log *why* a profile is invalid if the general validation fails. Part of the prerequisites for https://github.com/EVerest/libocpp/issues/361 Co-authored-by: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> Signed-off-by: Christopher Davis <150722105+christopher-davis-afs@users.noreply.github.com> --- include/ocpp/v201/smart_charging.hpp | 51 +++ lib/CMakeLists.txt | 1 + lib/ocpp/v201/smart_charging.cpp | 54 +++ tests/lib/ocpp/v201/CMakeLists.txt | 3 +- .../ocpp/v201/test_smart_charging_handler.cpp | 375 ++++++++++++++++++ 5 files changed, 483 insertions(+), 1 deletion(-) create mode 100644 include/ocpp/v201/smart_charging.hpp create mode 100644 lib/ocpp/v201/smart_charging.cpp create mode 100644 tests/lib/ocpp/v201/test_smart_charging_handler.cpp diff --git a/include/ocpp/v201/smart_charging.hpp b/include/ocpp/v201/smart_charging.hpp new file mode 100644 index 0000000000..5ff165932d --- /dev/null +++ b/include/ocpp/v201/smart_charging.hpp @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#ifndef OCPP_V201_SMART_CHARGING_HPP +#define OCPP_V201_SMART_CHARGING_HPP + +#include "ocpp/v201/enums.hpp" +#include + +#include +#include +#include +#include + +namespace ocpp { +namespace v201 { + +enum class ProfileValidationResultEnum { + Valid, + TxProfileMissingTransactionId, + TxProfileEvseIdNotGreaterThanZero, + TxProfileTransactionNotOnEvse, + TxProfileEvseHasNoActiveTransaction, + TxProfileConflictingStackLevel +}; + +/// \brief This class handles and maintains incoming ChargingProfiles and contains the logic +/// to calculate the composite schedules +class SmartChargingHandler { +private: + std::shared_ptr database_handler; + std::vector charging_profiles; + +public: + explicit SmartChargingHandler(std::shared_ptr database_handler); + + /// + /// \brief validates the given \p profile according to the specification + /// + ProfileValidationResultEnum validate_tx_profile(ChargingProfile& profile, Evse& evse) const; + + void add_profile(ChargingProfile& profile); +}; + +bool validate_schedule(const ChargingSchedule& schedule, const int charging_schedule_max_periods, + const std::vector& charging_schedule_allowed_charging_rate_units); + +} // namespace v201 +} // namespace ocpp + +#endif // OCPP_V201_SMART_CHARGING_HPP diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 8acf5b77a9..b50a3c9e00 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -32,6 +32,7 @@ target_sources(ocpp ocpp/v16/types.cpp ocpp/v201/average_meter_values.cpp ocpp/v201/charge_point.cpp + ocpp/v201/smart_charging.cpp ocpp/v201/connector.cpp ocpp/v201/ctrlr_component_variables.cpp ocpp/v201/database_handler.cpp diff --git a/lib/ocpp/v201/smart_charging.cpp b/lib/ocpp/v201/smart_charging.cpp new file mode 100644 index 0000000000..5e0341f999 --- /dev/null +++ b/lib/ocpp/v201/smart_charging.cpp @@ -0,0 +1,54 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest + +#include "ocpp/common/types.hpp" +#include "ocpp/v201/enums.hpp" +#include "ocpp/v201/ocpp_types.hpp" +#include "ocpp/v201/transaction.hpp" +#include +#include + +using namespace std::chrono; + +namespace ocpp { +namespace v201 { + +SmartChargingHandler::SmartChargingHandler(std::shared_ptr database_handler) : + database_handler(database_handler) { +} + +ProfileValidationResultEnum SmartChargingHandler::validate_tx_profile(ChargingProfile& profile, Evse& evse) const { + if (!profile.transactionId.has_value()) { + return ProfileValidationResultEnum::TxProfileMissingTransactionId; + } + + int32_t evseId = evse.get_evse_info().id; + if (evseId <= 0) { + return ProfileValidationResultEnum::TxProfileEvseIdNotGreaterThanZero; + } + + if (!evse.has_active_transaction()) { + return ProfileValidationResultEnum::TxProfileEvseHasNoActiveTransaction; + } + + auto& transaction = evse.get_transaction(); + if (transaction->transactionId != profile.transactionId.value()) { + return ProfileValidationResultEnum::TxProfileTransactionNotOnEvse; + } + + auto conflicts_with = [&profile](const ChargingProfile& candidate) { + return candidate.transactionId == profile.transactionId && candidate.stackLevel == profile.stackLevel; + }; + if (std::any_of(charging_profiles.begin(), charging_profiles.end(), conflicts_with)) { + return ProfileValidationResultEnum::TxProfileConflictingStackLevel; + } + + return ProfileValidationResultEnum::Valid; +} + +void SmartChargingHandler::add_profile(ChargingProfile& profile) { + charging_profiles.push_back(profile); +} + +} // namespace v201 +} // namespace ocpp diff --git a/tests/lib/ocpp/v201/CMakeLists.txt b/tests/lib/ocpp/v201/CMakeLists.txt index 55e18f592c..005db65d15 100644 --- a/tests/lib/ocpp/v201/CMakeLists.txt +++ b/tests/lib/ocpp/v201/CMakeLists.txt @@ -3,4 +3,5 @@ target_sources(libocpp_unit_tests PRIVATE test_device_model_storage_sqlite.cpp test_notify_report_requests_splitter.cpp test_ocsp_updater.cpp - test_component_state_manager.cpp) + test_component_state_manager.cpp + test_smart_charging_handler.cpp) diff --git a/tests/lib/ocpp/v201/test_smart_charging_handler.cpp b/tests/lib/ocpp/v201/test_smart_charging_handler.cpp new file mode 100644 index 0000000000..083c486cdb --- /dev/null +++ b/tests/lib/ocpp/v201/test_smart_charging_handler.cpp @@ -0,0 +1,375 @@ +#include "date/tz.h" +#include "ocpp/v201/ctrlr_component_variables.hpp" +#include "ocpp/v201/device_model_storage_sqlite.hpp" +#include +#include +#include +#include +#include +#include +namespace fs = std::filesystem; + +#include +#include +#include +#include +#include +#include + +#include + +namespace ocpp { +namespace v201 { + +class DeviceModelStorageMock : public DeviceModelStorage { +public: + MOCK_METHOD(DeviceModelMap, get_device_model, ()); + MOCK_METHOD(std::optional, get_variable_attribute, + (const Component&, const Variable&, const AttributeEnum&)); + MOCK_METHOD(std::vector, get_variable_attributes, + (const Component&, const Variable&, const std::optional&)); + MOCK_METHOD(bool, set_variable_attribute_value, + (const Component&, const Variable&, const AttributeEnum&, const std::string&)); + MOCK_METHOD(void, check_integrity, ()); +}; + +class ComponentStateManagerMock : public ComponentStateManagerInterface { + + MOCK_METHOD(void, set_cs_effective_availability_changed_callback, + (const std::function& callback)); + + MOCK_METHOD(void, set_evse_effective_availability_changed_callback, + (const std::function& callback)); + + MOCK_METHOD(void, set_connector_effective_availability_changed_callback, + (const std::function& callback)); + + MOCK_METHOD(OperationalStatusEnum, get_cs_individual_operational_status, ()); + MOCK_METHOD(OperationalStatusEnum, get_evse_individual_operational_status, (int32_t evse_id)); + MOCK_METHOD(OperationalStatusEnum, get_connector_individual_operational_status, + (int32_t evse_id, int32_t connector_id)); + MOCK_METHOD(OperationalStatusEnum, get_cs_persisted_operational_status, ()); + MOCK_METHOD(OperationalStatusEnum, get_evse_persisted_operational_status, (int32_t evse_id)); + MOCK_METHOD(OperationalStatusEnum, get_connector_persisted_operational_status, + (int32_t evse_id, int32_t connector_id)); + MOCK_METHOD(void, set_cs_individual_operational_status, (OperationalStatusEnum new_status, bool persist)); + MOCK_METHOD(void, set_evse_individual_operational_status, + (int32_t evse_id, OperationalStatusEnum new_status, bool persist)); + MOCK_METHOD(void, set_connector_individual_operational_status, + (int32_t evse_id, int32_t connector_id, OperationalStatusEnum new_status, bool persist)); + MOCK_METHOD(OperationalStatusEnum, get_evse_effective_operational_status, (int32_t evse_id)); + MOCK_METHOD(OperationalStatusEnum, get_connector_effective_operational_status, + (int32_t evse_id, int32_t connector_id)); + MOCK_METHOD(ConnectorStatusEnum, get_connector_effective_status, (int32_t evse_id, int32_t connector_id)); + MOCK_METHOD(void, set_connector_occupied, (int32_t evse_id, int32_t connector_id, bool is_occupied)); + MOCK_METHOD(void, set_connector_reserved, (int32_t evse_id, int32_t connector_id, bool is_reserved)); + MOCK_METHOD(void, set_connector_faulted, (int32_t evse_id, int32_t connector_id, bool is_faulted)); + MOCK_METHOD(void, set_connector_unavailable, (int32_t evse_id, int32_t connector_id, bool is_unavailable)); + MOCK_METHOD(void, trigger_all_effective_availability_changed_callbacks, ()); + MOCK_METHOD(void, send_status_notification_all_connectors, ()); + MOCK_METHOD(void, send_status_notification_changed_connectors, ()); + MOCK_METHOD(void, send_status_notification_single_connector, (int32_t evse_id, int32_t connector_id)); +}; + +/** + * Chargepoint Test Fixture + * + * Test Matrix: + * + * Positive Boundary Conditions: + * - PB01 Valid Profile + * - PB02 Valid Profile No startSchedule & handler allows no startSchedule & profile.chargingProfileKind == Absolute + * - PB03 Valid Profile No startSchedule & handler allows no startSchedule & profile.chargingProfileKind == Relative + * - PB04 Absolute ChargePointMaxProfile Profile with connector id 0 + * - PB05 Absolute TxDefaultProfile + * - PB06 Absolute TxProfile ignore_no_transaction == true + * - PB07 Absolute TxProfile && connector transaction != nullptr && transaction_id matches SKIPPED: was not able to test + * + * Negative Boundary Conditions: + * - NB01 Valid Profile, ConnectorID gt this->connectors.size() + * - NB02 Valid Profile, ConnectorID lt 0 + * - NB03 profile.stackLevel lt 0 + * - NB04 profile.stackLevel gt profile_max_stack_level + * - NB05 profile.chargingProfileKind == Absolute && !profile.chargingSchedule.startSchedule + * - NB06 Number of installed Profiles is > max_charging_profiles_installed + * - NB07 Invalid ChargingSchedule + * - NB08 profile.chargingProfileKind == Recurring && !profile.recurrencyKind + * - NB09 profile.chargingProfileKind == Recurring && !startSchedule + * - NB10 profile.chargingProfileKind == Recurring && !startSchedule && !allow_charging_profile_without_start_schedule + * - NB11 Absolute ChargePointMaxProfile Profile with connector id not 0 + * - NB12 Absolute TxProfile connector_id == 0 + */ +class ChargepointTestFixtureV201 : public testing::Test { +protected: + void SetUp() override { + } + + ChargingSchedule createChargeSchedule() { + return ChargingSchedule{{}}; + } + + ChargingSchedule createChargeSchedule(ChargingRateUnitEnum chargingRateUnit) { + int32_t id; + std::vector chargingSchedulePeriod; + std::optional customData; + std::optional startSchedule; + std::optional duration; + std::optional minChargingRate; + std::optional salesTariff; + + return ChargingSchedule{ + id, chargingRateUnit, chargingSchedulePeriod, customData, startSchedule, duration, minChargingRate, + salesTariff, + }; + } + + ChargingProfile createTxProfile(ChargingSchedule chargingSchedule, std::string transactionId, int stackLevel = 1) { + auto chargingProfileId = 1; + auto chargingProfilePurpose = ChargingProfilePurposeEnum::TxProfile; + auto chargingProfileKind = ChargingProfileKindEnum::Absolute; + auto recurrencyKind = RecurrencyKindEnum::Daily; + std::vector chargingSchedules = {chargingSchedule}; + return ChargingProfile{.id = chargingProfileId, + .stackLevel = stackLevel, + .chargingProfilePurpose = chargingProfilePurpose, + .chargingProfileKind = chargingProfileKind, + .chargingSchedule = chargingSchedules, + .customData = {}, + .recurrencyKind = recurrencyKind, + .validFrom = {}, + .validTo = {}, + .transactionId = transactionId}; + } + + ChargingProfile createTxProfileWithMissingTransactionId(ChargingSchedule chargingSchedule) { + auto chargingProfileId = 1; + auto stackLevel = 1; + auto chargingProfilePurpose = ChargingProfilePurposeEnum::TxProfile; + auto chargingProfileKind = ChargingProfileKindEnum::Absolute; + auto recurrencyKind = RecurrencyKindEnum::Daily; + std::vector chargingSchedules = {chargingSchedule}; + return ChargingProfile{ + chargingProfileId, + stackLevel, + chargingProfilePurpose, + chargingProfileKind, + chargingSchedules, + {}, // transactionId + recurrencyKind, + {}, // validFrom + {} // validTo + }; + } + + /** + * TxDefaultProfile, stack #1: time-of-day limitation to 2 kW, recurring every day from 17:00h to 20:00h. + * + * This profile is Example #1 taken from the OCPP 2.0.1 Spec Part 2, page 241. + */ + ChargingProfile createChargingProfile_Example1() { + auto chargingRateUnit = ChargingRateUnitEnum::W; + auto chargingSchedulePeriod = std::vector{ + ChargingSchedulePeriod{.startPeriod = 0, .limit = 2000, .numberPhases = 1}}; + auto duration = 1080; + auto startSchedule = ocpp::DateTime("2024-01-17T17:00:00"); + float minChargingRate = 0; + auto chargingSchedule = ChargingSchedule{.chargingRateUnit = chargingRateUnit, + .chargingSchedulePeriod = chargingSchedulePeriod, + .startSchedule = startSchedule, + .duration = duration, + .minChargingRate = minChargingRate}; + std::vector chargingSchedules = {chargingSchedule}; + + auto chargingProfileId = 1; + auto stackLevel = 1; + auto chargingProfilePurpose = ChargingProfilePurposeEnum::TxDefaultProfile; + auto chargingProfileKind = ChargingProfileKindEnum::Absolute; + auto recurrencyKind = RecurrencyKindEnum::Daily; + return ChargingProfile{ + chargingProfileId, + stackLevel, + chargingProfilePurpose, + chargingProfileKind, + chargingSchedules, + {}, // transactionId + recurrencyKind, + {}, // validFrom + {} // validTo + }; + } + + /** + * TxDefaultProfile, stack #2: overruling Sundays to no limit, recurring every week starting 2020-01-05. + * + * This profile is Example #2 taken from the OCPP 2.0.1 Spec Part 2, page 241. + */ + ChargingProfile createChargingProfile_Example2() { + auto chargingRateUnit = ChargingRateUnitEnum::W; + auto chargingSchedulePeriod = std::vector{ + ChargingSchedulePeriod{.startPeriod = 0, .limit = 999999, .numberPhases = 1}}; + auto duration = 0; + auto startSchedule = ocpp::DateTime("2020-01-19T00:00:00"); + float minChargingRate = 0; + auto chargingSchedule = ChargingSchedule{.chargingRateUnit = chargingRateUnit, + .chargingSchedulePeriod = chargingSchedulePeriod, + .startSchedule = startSchedule, + .duration = duration, + .minChargingRate = minChargingRate}; + std::vector chargingSchedules = {chargingSchedule}; + + auto chargingProfileId = 11; + auto stackLevel = 2; + auto chargingProfilePurpose = ChargingProfilePurposeEnum::TxDefaultProfile; + auto chargingProfileKind = ChargingProfileKindEnum::Recurring; + auto recurrencyKind = RecurrencyKindEnum::Weekly; + return ChargingProfile{chargingProfileId, + stackLevel, + chargingProfilePurpose, + chargingProfileKind, + chargingSchedules, + {}, // transactionId + recurrencyKind, + {}, + {}}; + } + + DeviceModel createDeviceModel() { + std::unique_ptr storage_mock = + std::make_unique>(); + ON_CALL(*storage_mock, get_device_model).WillByDefault(testing::Return(DeviceModelMap())); + return DeviceModel(std::move(storage_mock)); + } + + void createEvseWithId(int id) { + testing::MockFunction reservation_id)> + transaction_meter_value_req_mock; + testing::MockFunction pause_charging_callback_mock; + auto e1 = std::make_unique( + id, 1, device_model, database_handler, std::make_shared(), + transaction_meter_value_req_mock.AsStdFunction(), pause_charging_callback_mock.AsStdFunction()); + evses[id] = std::move(e1); + } + + SmartChargingHandler createSmartChargingHandler() { + return SmartChargingHandler(database_handler); + } + + std::string uuid() { + std::stringstream s; + s << uuid_generator(); + return s.str(); + } + + void open_evse_transaction(int evseId, std::string transactionId) { + auto connector_id = 1; + auto meter_start = MeterValue(); + auto id_token = IdToken(); + auto date_time = ocpp::DateTime("2024-01-17T17:00:00"); + evses[evseId]->open_transaction( + transactionId, connector_id, date_time, meter_start, id_token, {}, {}, + std::chrono::seconds(static_cast(1)), std::chrono::seconds(static_cast(1)), + std::chrono::seconds(static_cast(1)), std::chrono::seconds(static_cast(1))); + } + // Default values used within the tests + std::map> evses; + std::shared_ptr database_handler; + + const int evseId = 1; + bool ignore_no_transaction = true; + const int profile_max_stack_level = 1; + const int max_charging_profiles_installed = 1; + const int charging_schedule_max_periods = 1; + DeviceModel device_model = createDeviceModel(); + SmartChargingHandler handler = createSmartChargingHandler(); + boost::uuids::random_generator uuid_generator = boost::uuids::random_generator(); +}; + +TEST_F(ChargepointTestFixtureV201, IfTxProfileIsMissingTransactionId_ThenProfileIsInvalid) { + createEvseWithId(evseId); + auto profile = createTxProfileWithMissingTransactionId(createChargeSchedule(ChargingRateUnitEnum::A)); + auto sut = handler.validate_tx_profile(profile, *evses[evseId]); + + ASSERT_THAT(sut, testing::Eq(ProfileValidationResultEnum::TxProfileMissingTransactionId)); +} + +TEST_F(ChargepointTestFixtureV201, IfTxProfileHasEvseIdNotGreaterThanZero_ThenProfileIsInvalid) { + auto wrongEvseId = 0; + createEvseWithId(0); + auto profile = createTxProfile(createChargeSchedule(ChargingRateUnitEnum::A), uuid()); + auto sut = handler.validate_tx_profile(profile, *evses[wrongEvseId]); + + ASSERT_THAT(sut, testing::Eq(ProfileValidationResultEnum::TxProfileEvseIdNotGreaterThanZero)); +} + +TEST_F(ChargepointTestFixtureV201, IfTxProfileTransactionIsNotOnEvse_ThenProfileIsInvalid) { + createEvseWithId(evseId); + open_evse_transaction(evseId, "wrong transaction id"); + auto profile = createTxProfile(createChargeSchedule(ChargingRateUnitEnum::A), uuid()); + auto sut = handler.validate_tx_profile(profile, *evses[evseId]); + + ASSERT_THAT(sut, testing::Eq(ProfileValidationResultEnum::TxProfileTransactionNotOnEvse)); +} + +TEST_F(ChargepointTestFixtureV201, IfTxProfileEvseHasNoActiveTransaction_ThenProfileIsInvalid) { + auto connector_id = 1; + auto meter_start = MeterValue(); + auto id_token = IdToken(); + auto date_time = ocpp::DateTime("2024-01-17T17:00:00"); + createEvseWithId(evseId); + auto profile = createTxProfile(createChargeSchedule(ChargingRateUnitEnum::A), uuid()); + auto sut = handler.validate_tx_profile(profile, *evses[evseId]); + + ASSERT_THAT(sut, testing::Eq(ProfileValidationResultEnum::TxProfileEvseHasNoActiveTransaction)); +} + +TEST_F(ChargepointTestFixtureV201, IfTxProfileHasSameTransactionAndStackLevelAsAnotherTxProfile_ThenProfileIsInvalid) { + createEvseWithId(evseId); + std::string transactionId = uuid(); + open_evse_transaction(evseId, transactionId); + + auto sameStackLevel = 42; + auto profile1 = createTxProfile(createChargeSchedule(ChargingRateUnitEnum::A), transactionId, sameStackLevel); + auto profile2 = createTxProfile(createChargeSchedule(ChargingRateUnitEnum::A), transactionId, sameStackLevel); + handler.add_profile(profile2); + auto sut = handler.validate_tx_profile(profile1, *evses[evseId]); + + ASSERT_THAT(sut, testing::Eq(ProfileValidationResultEnum::TxProfileConflictingStackLevel)); +} + +TEST_F(ChargepointTestFixtureV201, + IfTxProfileHasDifferentTransactionButSameStackLevelAsAnotherTxProfile_ThenProfileIsValid) { + createEvseWithId(evseId); + std::string transactionId = uuid(); + std::string differentTransactionId = uuid(); + open_evse_transaction(evseId, transactionId); + + auto sameStackLevel = 42; + auto profile1 = createTxProfile(createChargeSchedule(ChargingRateUnitEnum::A), transactionId, sameStackLevel); + auto profile2 = + createTxProfile(createChargeSchedule(ChargingRateUnitEnum::A), differentTransactionId, sameStackLevel); + handler.add_profile(profile2); + auto sut = handler.validate_tx_profile(profile1, *evses[evseId]); + + ASSERT_THAT(sut, testing::Eq(ProfileValidationResultEnum::Valid)); +} + +TEST_F(ChargepointTestFixtureV201, + IfTxProfileHasSameTransactionButDifferentStackLevelAsAnotherTxProfile_ThenProfileIsValid) { + createEvseWithId(evseId); + std::string sameTransactionId = uuid(); + open_evse_transaction(evseId, sameTransactionId); + + auto stackLevel1 = 42; + auto stackLevel2 = 43; + auto profile1 = createTxProfile(createChargeSchedule(ChargingRateUnitEnum::A), sameTransactionId, stackLevel1); + auto profile2 = createTxProfile(createChargeSchedule(ChargingRateUnitEnum::A), sameTransactionId, stackLevel2); + handler.add_profile(profile2); + auto sut = handler.validate_tx_profile(profile1, *evses[evseId]); + + ASSERT_THAT(sut, testing::Eq(ProfileValidationResultEnum::Valid)); +} + +} // namespace v201 +} // namespace ocpp \ No newline at end of file