From 233984aa58b2fa16cd9f91537ef62d0fb69dd326 Mon Sep 17 00:00:00 2001 From: Christopher Davis <150722105+christopher-davis-afs@users.noreply.github.com> Date: Tue, 27 Feb 2024 15:44:46 +0000 Subject: [PATCH] smart_charging: Add validation for profile schedules Implements: * K01.FR.19 * K01.FR.31 * K01.FR.35 * K01.FR.40 * K01.FR.41 Does not implement: * K01.FR.20 * K01.FR.34 * K01.FR.43 * K01.FR.45 * K01.FR.48 These functional requirements rely on functionality in DeviceModel, the EVSE, or message handling that is not currently implemented in Everest. Co-authored-by: Coury Richards <146002925+couryrr-afs@users.noreply.github.com> Signed-off-by: Christopher Davis <150722105+christopher-davis-afs@users.noreply.github.com> --- include/ocpp/v201/smart_charging.hpp | 11 +- lib/ocpp/v201/smart_charging.cpp | 52 +++++++ .../ocpp/v201/test_smart_charging_handler.cpp | 128 +++++++++++++++++- 3 files changed, 187 insertions(+), 4 deletions(-) diff --git a/include/ocpp/v201/smart_charging.hpp b/include/ocpp/v201/smart_charging.hpp index 2d31cba457..fadf6016a0 100644 --- a/include/ocpp/v201/smart_charging.hpp +++ b/include/ocpp/v201/smart_charging.hpp @@ -20,7 +20,13 @@ enum class ProfileValidationResultEnum { TxProfileEvseIdNotGreaterThanZero, TxProfileTransactionNotOnEvse, TxProfileEvseHasNoActiveTransaction, - TxProfileConflictingStackLevel + TxProfileConflictingStackLevel, + ChargingProfileNoChargingSchedulePeriods, + ChargingProfileFirstStartScheduleIsNotZero, + ChargingProfileMissingRequiredStartSchedule, + ChargingProfileExtraneousStartSchedule, + ChargingSchedulePeriodsOutOfOrder, + ChargingSchedulePeriodInvalidPhaseToUse, }; /// \brief This class handles and maintains incoming ChargingProfiles and contains the logic @@ -39,6 +45,9 @@ class SmartChargingHandler { /// ProfileValidationResultEnum validate_tx_profile(const ChargingProfile& profile, Evse& evse) const; + /// \brief validates that the given \p profile has valid charging schedules + ProfileValidationResultEnum validate_profile_schedules(const ChargingProfile& profile) const; + /// \brief Adds a given \p profile to our stored list of profiles void add_profile(const ChargingProfile& profile); }; diff --git a/lib/ocpp/v201/smart_charging.cpp b/lib/ocpp/v201/smart_charging.cpp index bfedad4407..d28e60d726 100644 --- a/lib/ocpp/v201/smart_charging.cpp +++ b/lib/ocpp/v201/smart_charging.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest +#include "everest/logging.hpp" #include "ocpp/common/types.hpp" #include "ocpp/v201/enums.hpp" #include "ocpp/v201/ocpp_types.hpp" @@ -45,6 +46,57 @@ ProfileValidationResultEnum SmartChargingHandler::validate_tx_profile(const Char return ProfileValidationResultEnum::Valid; } +/* TODO: Implement the following functional requirements: + * - K01.FR.20 + * - K01.FR.34 + * - K01.FR.43 + * - K01.FR.45 + * - K01.FR.48 + */ +ProfileValidationResultEnum SmartChargingHandler::validate_profile_schedules(const ChargingProfile& profile) const { + auto schedules = profile.chargingSchedule; + + for (auto schedule : schedules) { + // A schedule must have at least one chargingSchedulePeriod + if (schedule.chargingSchedulePeriod.empty()) { + return ProfileValidationResultEnum::ChargingProfileNoChargingSchedulePeriods; + } + + auto charging_schedule_period = schedule.chargingSchedulePeriod[0]; + + for (auto i = 0; i < schedule.chargingSchedulePeriod.size(); i++) { + // K01.FR.19 + if (charging_schedule_period.numberPhases != 1 && charging_schedule_period.phaseToUse.has_value()) { + return ProfileValidationResultEnum::ChargingSchedulePeriodInvalidPhaseToUse; + } + + // K01.FR.31 + if (i == 0 && charging_schedule_period.startPeriod != 0) { + return ProfileValidationResultEnum::ChargingProfileFirstStartScheduleIsNotZero; + // K01.FR.35 + } else if (i != 0) { + auto next_charging_schedule_period = schedule.chargingSchedulePeriod[i]; + if (next_charging_schedule_period.startPeriod <= charging_schedule_period.startPeriod) { + return ProfileValidationResultEnum::ChargingSchedulePeriodsOutOfOrder; + } else { + charging_schedule_period = next_charging_schedule_period; + } + } + } + + // K01.FR.40 + if (profile.chargingProfileKind != ChargingProfileKindEnum::Relative && !schedule.startSchedule.has_value()) { + return ProfileValidationResultEnum::ChargingProfileMissingRequiredStartSchedule; + // K01.FR.41 + } else if (profile.chargingProfileKind == ChargingProfileKindEnum::Relative && + schedule.startSchedule.has_value()) { + return ProfileValidationResultEnum::ChargingProfileExtraneousStartSchedule; + } + } + + return ProfileValidationResultEnum::Valid; +} + void SmartChargingHandler::add_profile(const ChargingProfile& profile) { charging_profiles.push_back(profile); } diff --git a/tests/lib/ocpp/v201/test_smart_charging_handler.cpp b/tests/lib/ocpp/v201/test_smart_charging_handler.cpp index 9d4e0a834f..f804083d91 100644 --- a/tests/lib/ocpp/v201/test_smart_charging_handler.cpp +++ b/tests/lib/ocpp/v201/test_smart_charging_handler.cpp @@ -1,6 +1,7 @@ #include "date/tz.h" #include "ocpp/v201/ctrlr_component_variables.hpp" #include "ocpp/v201/device_model_storage_sqlite.hpp" +#include "ocpp/v201/ocpp_types.hpp" #include #include #include @@ -18,6 +19,7 @@ #include #include +#include namespace ocpp::v201 { @@ -47,11 +49,60 @@ class ChargepointTestFixtureV201 : public testing::Test { }; } - ChargingProfile create_tx_profile(ChargingSchedule charging_schedule, std::string transaction_id, - int stack_level = 1) { + ChargingSchedule create_charge_schedule(ChargingRateUnitEnum charging_rate_unit, + std::vector charging_schedule_period, + std::optional start_schedule = std::nullopt) { + int32_t id; + std::optional custom_data; + std::optional duration; + std::optional min_charging_rate; + std::optional sales_tariff; + + return ChargingSchedule{ + id, + charging_rate_unit, + charging_schedule_period, + custom_data, + start_schedule, + duration, + min_charging_rate, + sales_tariff, + }; + } + + std::vector create_charging_schedule_periods(int32_t start_period) { + auto charging_schedule_period = ChargingSchedulePeriod{ + .startPeriod = start_period, + }; + + return {charging_schedule_period}; + } + + std::vector create_charging_schedule_periods(std::vector start_periods) { + auto charging_schedule_periods = std::vector(); + for (auto start_period : start_periods) { + auto charging_schedule_period = ChargingSchedulePeriod{ + .startPeriod = start_period, + }; + charging_schedule_periods.push_back(charging_schedule_period); + } + + return charging_schedule_periods; + } + + std::vector + create_charging_schedule_periods_with_phases(int32_t start_period, int32_t numberPhases, int32_t phaseToUse) { + auto charging_schedule_period = + ChargingSchedulePeriod{.startPeriod = start_period, .numberPhases = numberPhases, .phaseToUse = phaseToUse}; + + return {charging_schedule_period}; + } + + ChargingProfile + create_tx_profile(ChargingSchedule charging_schedule, std::string transaction_id, int stack_level = 1, + ChargingProfileKindEnum charging_profile_kind = ChargingProfileKindEnum::Absolute) { auto charging_profile_id = 1; auto charging_profile_purpose = ChargingProfilePurposeEnum::TxProfile; - auto charging_profile_kind = ChargingProfileKindEnum::Absolute; auto recurrency_kind = RecurrencyKindEnum::Daily; std::vector charging_schedules = {charging_schedule}; return ChargingProfile{.id = charging_profile_id, @@ -230,4 +281,75 @@ TEST_F(ChargepointTestFixtureV201, EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::Valid)); } +TEST_F(ChargepointTestFixtureV201, K01FR19_NumberPhasesOtherThan1AndPhaseToUseSet_ThenProfileInvalid) { + auto periods = create_charging_schedule_periods_with_phases(0, 0, 1); + auto profile = create_tx_profile( + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), uuid(), 1, + ChargingProfileKindEnum::Relative); + + auto sut = handler.validate_profile_schedules(profile); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingSchedulePeriodInvalidPhaseToUse)); +} + +TEST_F(ChargepointTestFixtureV201, K01_IfChargingSchedulePeriodsAreMissing_ThenProfileIsInvalid) { + auto profile = create_tx_profile(create_charge_schedule(ChargingRateUnitEnum::A), uuid()); + + auto sut = handler.validate_profile_schedules(profile); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingProfileNoChargingSchedulePeriods)); +} + +TEST_F(ChargepointTestFixtureV201, K01FR31_IfStartPeriodOfFirstChargingSchedulePeriodIsNotZero_ThenProfileIsInvalid) { + auto periods = create_charging_schedule_periods(1); + auto profile = create_tx_profile(create_charge_schedule(ChargingRateUnitEnum::A, periods), uuid()); + + auto sut = handler.validate_profile_schedules(profile); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingProfileFirstStartScheduleIsNotZero)); +} + +TEST_F(ChargepointTestFixtureV201, K01FR35_IfChargingSchedulePeriodsAreNotInChonologicalOrder_ThenProfileIsInvalid) { + auto periods = create_charging_schedule_periods({0, 2, 1}); + auto profile = create_tx_profile(create_charge_schedule(ChargingRateUnitEnum::A, periods), uuid()); + + auto sut = handler.validate_profile_schedules(profile); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingSchedulePeriodsOutOfOrder)); +} + +TEST_F(ChargepointTestFixtureV201, + K01FR40_IfChargingProfileKindIsAbsoluteAndStartScheduleDoesNotExist_ThenProfileIsInvalid) { + auto periods = create_charging_schedule_periods(0); + auto profile = create_tx_profile(create_charge_schedule(ChargingRateUnitEnum::A, periods), uuid(), 1, + ChargingProfileKindEnum::Absolute); + + auto sut = handler.validate_profile_schedules(profile); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingProfileMissingRequiredStartSchedule)); +} + +TEST_F(ChargepointTestFixtureV201, + K01FR40_IfChargingProfileKindIsRecurringAndStartScheduleDoesNotExist_ThenProfileIsInvalid) { + auto periods = create_charging_schedule_periods(0); + auto profile = create_tx_profile(create_charge_schedule(ChargingRateUnitEnum::A, periods), uuid(), 1, + ChargingProfileKindEnum::Recurring); + + auto sut = handler.validate_profile_schedules(profile); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingProfileMissingRequiredStartSchedule)); +} + +TEST_F(ChargepointTestFixtureV201, + K01FR41_IfChargingProfileKindIsRelativeAndStartScheduleDoesExist_ThenProfileIsInvalid) { + auto periods = create_charging_schedule_periods(0); + auto profile = create_tx_profile( + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), uuid(), 1, + ChargingProfileKindEnum::Relative); + + auto sut = handler.validate_profile_schedules(profile); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingProfileExtraneousStartSchedule)); +} + } // namespace ocpp::v201