diff --git a/doc/ocpp_201_status.md b/doc/ocpp_201_status.md index 1e510d50c8..2a5661c60b 100644 --- a/doc/ocpp_201_status.md +++ b/doc/ocpp_201_status.md @@ -1144,7 +1144,7 @@ This document contains the status of which OCPP 2.0.1 numbered requirements have | K01.FR.03 | | | | K01.FR.04 | :white_check_mark: | | | K01.FR.05 | | | -| K01.FR.06 | | | +| K01.FR.06 | :white_check_mark: | | | K01.FR.07 | | | | K01.FR.08 | | | | K01.FR.09 | | | diff --git a/include/ocpp/v201/smart_charging.hpp b/include/ocpp/v201/smart_charging.hpp index de4f278961..9c7402bfb7 100644 --- a/include/ocpp/v201/smart_charging.hpp +++ b/include/ocpp/v201/smart_charging.hpp @@ -32,7 +32,8 @@ enum class ProfileValidationResultEnum { ChargingSchedulePeriodInvalidPhaseToUse, ChargingSchedulePeriodUnsupportedNumberPhases, ChargingSchedulePeriodExtraneousPhaseValues, - DuplicateTxDefaultProfileFound + DuplicateTxDefaultProfileFound, + DuplicateProfileValidityPeriod }; namespace conversions { @@ -83,6 +84,11 @@ class SmartChargingHandler { /// void add_profile(int32_t evse_id, ChargingProfile& profile); + /// + /// \brief Checks a given \p profile and associated \p evse_id validFrom and validTo range + /// + bool is_overlapping_validity_period(int evse_id, ChargingProfile profile) const; + private: std::vector get_evse_specific_tx_default_profiles() const; std::vector get_station_wide_tx_default_profiles() const; diff --git a/lib/ocpp/v201/smart_charging.cpp b/lib/ocpp/v201/smart_charging.cpp index ca3e14c0ca..396d1ecc06 100644 --- a/lib/ocpp/v201/smart_charging.cpp +++ b/lib/ocpp/v201/smart_charging.cpp @@ -74,6 +74,11 @@ ProfileValidationResultEnum SmartChargingHandler::validate_evse_exists(int32_t e ProfileValidationResultEnum SmartChargingHandler::validate_tx_default_profile(const ChargingProfile& profile, int32_t evse_id) const { auto profiles = evse_id == 0 ? get_evse_specific_tx_default_profiles() : get_station_wide_tx_default_profiles(); + + if (is_overlapping_validity_period(evse_id, profile)) { + return ProfileValidationResultEnum::DuplicateProfileValidityPeriod; + } + for (auto candidate : profiles) { if (candidate.stackLevel == profile.stackLevel) { if (candidate.id != profile.id) { @@ -112,6 +117,7 @@ ProfileValidationResultEnum SmartChargingHandler::validate_tx_profile(const Char candidateProfile.stackLevel == profile.stackLevel; }); }; + if (std::any_of(charging_profiles.begin(), charging_profiles.end(), conflicts_with)) { return ProfileValidationResultEnum::TxProfileConflictingStackLevel; } @@ -125,10 +131,12 @@ ProfileValidationResultEnum SmartChargingHandler::validate_tx_profile(const Char * - K01.FR.43 * - K01.FR.48 */ + ProfileValidationResultEnum SmartChargingHandler::validate_profile_schedules(ChargingProfile& profile, std::optional evse_opt) const { - for (ChargingSchedule& schedule : profile.chargingSchedule) { + for (auto& schedule : profile.chargingSchedule) { + // A schedule must have at least one chargingSchedulePeriod if (schedule.chargingSchedulePeriod.empty()) { return ProfileValidationResultEnum::ChargingProfileNoChargingSchedulePeriods; @@ -224,4 +232,86 @@ std::vector SmartChargingHandler::get_station_wide_tx_default_p return station_wide_tx_default_profiles; } +bool SmartChargingHandler::is_overlapping_validity_period(int candidate_evse_id, + ChargingProfile candidate_profile) const { + if (candidate_profile.chargingProfilePurpose == ChargingProfilePurposeEnum::TxProfile) { + // This only applies to non TxProfile types. + return false; + } + + auto conflicts_with = [candidate_evse_id, &candidate_profile]( + const std::pair>& existing_profiles) { + auto existing_evse_id = existing_profiles.first; + if (existing_evse_id == candidate_evse_id) { + return std::any_of( + existing_profiles.second.begin(), existing_profiles.second.end(), + [&candidate_profile](const ChargingProfile& existing_profile) { + if (existing_profile.stackLevel == candidate_profile.stackLevel && + existing_profile.chargingProfileKind == candidate_profile.chargingProfileKind && + existing_profile.id != candidate_profile.id) { + + // Existing profile last forever should be updated to replace + if (!existing_profile.validFrom.has_value() && !existing_profile.validTo.has_value()) { + return true; // reject + } + + // Cannot accept new profile that last forever when there is an existing profile + if (!candidate_profile.validFrom.has_value() && !candidate_profile.validTo.has_value()) { + return true; // reject + } + + // Existing has validFrom only + if (existing_profile.validFrom.has_value() && !existing_profile.validTo.has_value()) { + if (candidate_profile.validFrom.has_value() && !candidate_profile.validTo.has_value()) { + return true; // reject + } + if (candidate_profile.validTo.has_value() && + candidate_profile.validTo <= existing_profile.validFrom) { + return true; // reject + } + } + + // Existing has validTo only + if (!existing_profile.validFrom.has_value() && existing_profile.validTo.has_value()) { + if (!candidate_profile.validFrom.has_value() && candidate_profile.validTo.has_value()) { + return true; // reject + } + if (candidate_profile.validFrom.has_value() && + candidate_profile.validFrom >= existing_profile.validTo) { + return true; // reject + } + } + + // Case Everything is set + if (existing_profile.validFrom.has_value() && existing_profile.validTo.has_value()) { + if (!candidate_profile.validFrom.has_value() && !candidate_profile.validTo.has_value()) { + return true; + } + + // Is not after + if (candidate_profile.validFrom.has_value() && + candidate_profile.validFrom <= existing_profile.validTo) { + return true; // reject + } else { + return false; + } + + // Is not before + if (candidate_profile.validTo.has_value() && + candidate_profile.validTo >= existing_profile.validFrom) { + return true; // reject + } else { + return false; + } + } + } + return false; + }); + } + return false; + }; + + return std::any_of(charging_profiles.begin(), charging_profiles.end(), conflicts_with); +} + } // 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 8f7ce43bc0..58c603f99a 100644 --- a/tests/lib/ocpp/v201/test_smart_charging_handler.cpp +++ b/tests/lib/ocpp/v201/test_smart_charging_handler.cpp @@ -1,4 +1,5 @@ #include "date/tz.h" +#include "ocpp/common/types.hpp" #include "ocpp/v201/ctrlr_component_variables.hpp" #include "ocpp/v201/device_model_storage_sqlite.hpp" #include "ocpp/v201/ocpp_types.hpp" @@ -112,7 +113,8 @@ class ChargepointTestFixtureV201 : public testing::Test { create_charging_profile(int32_t charging_profile_id, ChargingProfilePurposeEnum charging_profile_purpose, ChargingSchedule charging_schedule, std::string transaction_id, ChargingProfileKindEnum charging_profile_kind = ChargingProfileKindEnum::Absolute, - int stack_level = DEFAULT_STACK_LEVEL) { + int stack_level = DEFAULT_STACK_LEVEL, std::optional validFrom = {}, + std::optional validTo = {}) { auto recurrency_kind = RecurrencyKindEnum::Daily; std::vector charging_schedules = {charging_schedule}; return ChargingProfile{.id = charging_profile_id, @@ -122,8 +124,8 @@ class ChargepointTestFixtureV201 : public testing::Test { .chargingSchedule = charging_schedules, .customData = {}, .recurrencyKind = recurrency_kind, - .validFrom = {}, - .validTo = {}, + .validFrom = validFrom, + .validTo = validTo, .transactionId = transaction_id}; } @@ -186,12 +188,15 @@ class ChargepointTestFixtureV201 : public testing::Test { std::chrono::seconds(static_cast(1)), std::chrono::seconds(static_cast(1))); } - void install_profile_on_evse(int evse_id, int profile_id) { + void install_profile_on_evse(int evse_id, int profile_id, + std::optional validFrom = ocpp::DateTime("2024-01-01T17:00:00"), + std::optional validTo = ocpp::DateTime("2024-02-01T17:00:00")) { if (evse_id != STATION_WIDE_ID) { create_evse_with_id(evse_id); } - auto existing_profile = create_charging_profile(profile_id, ChargingProfilePurposeEnum::TxDefaultProfile, - create_charge_schedule(ChargingRateUnitEnum::A), uuid()); + auto existing_profile = create_charging_profile( + profile_id, ChargingProfilePurposeEnum::TxDefaultProfile, create_charge_schedule(ChargingRateUnitEnum::A), + uuid(), ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL, validFrom, validTo); handler.add_profile(evse_id, existing_profile); } @@ -289,53 +294,61 @@ TEST_F(ChargepointTestFixtureV201, K01FR35_IfChargingSchedulePeriodsAreNotInChon TEST_F(ChargepointTestFixtureV201, K01FR39_IfTxProfileHasSameTransactionAndStackLevelAsAnotherTxProfile_ThenProfileIsInvalid) { - create_evse_with_id(evse_id); + create_evse_with_id(DEFAULT_EVSE_ID); std::string transaction_id = uuid(); - open_evse_transaction(evse_id, transaction_id); + open_evse_transaction(DEFAULT_EVSE_ID, transaction_id); auto same_stack_level = 42; - auto profile_1 = - create_tx_profile(create_charge_schedule(ChargingRateUnitEnum::A), transaction_id, same_stack_level); - auto profile_2 = - create_tx_profile(create_charge_schedule(ChargingRateUnitEnum::A), transaction_id, same_stack_level); - handler.add_profile(profile_2); - auto sut = handler.validate_tx_profile(profile_1, *evses[evse_id]); + auto profile_1 = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A), transaction_id, + ChargingProfileKindEnum::Absolute, same_stack_level); + auto profile_2 = create_charging_profile(DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A), transaction_id, + ChargingProfileKindEnum::Absolute, same_stack_level); + handler.add_profile(DEFAULT_EVSE_ID, profile_2); + auto sut = handler.validate_tx_profile(profile_1, *evses[DEFAULT_EVSE_ID]); EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::TxProfileConflictingStackLevel)); } TEST_F(ChargepointTestFixtureV201, K01FR39_IfTxProfileHasDifferentTransactionButSameStackLevelAsAnotherTxProfile_ThenProfileIsValid) { - create_evse_with_id(evse_id); + create_evse_with_id(DEFAULT_EVSE_ID); std::string transaction_id = uuid(); std::string different_transaction_id = uuid(); - open_evse_transaction(evse_id, transaction_id); + open_evse_transaction(DEFAULT_EVSE_ID, transaction_id); auto same_stack_level = 42; - auto profile_1 = - create_tx_profile(create_charge_schedule(ChargingRateUnitEnum::A), transaction_id, same_stack_level); - auto profile_2 = - create_tx_profile(create_charge_schedule(ChargingRateUnitEnum::A), different_transaction_id, same_stack_level); - handler.add_profile(profile_2); - auto sut = handler.validate_tx_profile(profile_1, *evses[evse_id]); + auto profile_1 = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A), transaction_id, + ChargingProfileKindEnum::Absolute, same_stack_level); + auto profile_2 = create_charging_profile(DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A), different_transaction_id, + ChargingProfileKindEnum::Absolute, same_stack_level); + handler.add_profile(DEFAULT_EVSE_ID, profile_2); + auto sut = handler.validate_tx_profile(profile_1, *evses[DEFAULT_EVSE_ID]); EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::Valid)); } TEST_F(ChargepointTestFixtureV201, K01FR39_IfTxProfileHasSameTransactionButDifferentStackLevelAsAnotherTxProfile_ThenProfileIsValid) { - create_evse_with_id(evse_id); + create_evse_with_id(DEFAULT_EVSE_ID); std::string same_transaction_id = uuid(); - open_evse_transaction(evse_id, same_transaction_id); + open_evse_transaction(DEFAULT_EVSE_ID, same_transaction_id); auto stack_level_1 = 42; auto stack_level_2 = 43; - auto profile_1 = - create_tx_profile(create_charge_schedule(ChargingRateUnitEnum::A), same_transaction_id, stack_level_1); - auto profile_2 = - create_tx_profile(create_charge_schedule(ChargingRateUnitEnum::A), same_transaction_id, stack_level_2); - handler.add_profile(profile_2); - auto sut = handler.validate_tx_profile(profile_1, *evses[evse_id]); + + auto profile_1 = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A), same_transaction_id, + ChargingProfileKindEnum::Absolute, stack_level_1); + auto profile_2 = create_charging_profile(DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A), same_transaction_id, + ChargingProfileKindEnum::Absolute, stack_level_2); + + handler.add_profile(DEFAULT_EVSE_ID, profile_2); + auto sut = handler.validate_tx_profile(profile_1, *evses[DEFAULT_EVSE_ID]); EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::Valid)); } @@ -460,7 +473,8 @@ TEST_F(ChargepointTestFixtureV201, K01FR53_TxDefaultProfileValidIfAppliedToExist auto profile = create_charging_profile(DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxDefaultProfile, create_charge_schedule(ChargingRateUnitEnum::A), uuid(), - ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL); + ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL, + ocpp::DateTime("2024-02-02T17:00:00")); auto sut = handler.validate_tx_default_profile(profile, DEFAULT_EVSE_ID); EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::Valid)); @@ -478,63 +492,70 @@ TEST_F(ChargepointTestFixtureV201, K01FR53_TxDefaultProfileValidIfAppliedToDiffe EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::Valid)); } -TEST_F(ChargepointTestFixtureV201, K01FR44_IfNumberPhasesProvidedForDCEVSE_ThenProfileIsInvalid) { - auto mock_evse = testing::NiceMock(); - ON_CALL(mock_evse, get_current_phase_type).WillByDefault(testing::Return(CurrentPhaseType::DC)); +TEST_F(ChargepointTestFixtureV201, K01FR06_CandidateProfileLastForever_RejectIncoming) { + install_profile_on_evse(DEFAULT_EVSE_ID, DEFAULT_PROFILE_ID, {}, {}); - auto periods = create_charging_schedule_periods(0, 1); auto profile = create_charging_profile( - DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, - create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), uuid()); + DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxDefaultProfile, + create_charge_schedule(ChargingRateUnitEnum::A), uuid(), ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL, + ocpp::DateTime("2024-01-02T13:00:00"), ocpp::DateTime("2024-03-01T13:00:00")); - auto sut = handler.validate_profile_schedules(profile, &mock_evse); + auto sut = handler.validate_tx_default_profile(profile, DEFAULT_EVSE_ID); - EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingSchedulePeriodExtraneousPhaseValues)); + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::DuplicateProfileValidityPeriod)); } -TEST_F(ChargepointTestFixtureV201, K01FR44_IfPhaseToUseProvidedForDCEVSE_ThenProfileIsInvalid) { - auto mock_evse = testing::NiceMock(); - ON_CALL(mock_evse, get_current_phase_type).WillByDefault(testing::Return(CurrentPhaseType::DC)); +TEST_F(ChargepointTestFixtureV201, K01FR06_CandidateProfileHasValidFromIncomingValidToOverlaps_RejectIncoming) { + install_profile_on_evse(DEFAULT_EVSE_ID, DEFAULT_PROFILE_ID, ocpp::DateTime("2024-01-01T13:00:00"), {}); - auto periods = create_charging_schedule_periods(0, 1, 1); - auto profile = create_charging_profile( - DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, - create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), uuid()); + auto profile = create_charging_profile(DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxDefaultProfile, + create_charge_schedule(ChargingRateUnitEnum::A), uuid(), + ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL, {}, + ocpp::DateTime("2024-01-01T13:00:00")); - auto sut = handler.validate_profile_schedules(profile, &mock_evse); + auto sut = handler.validate_tx_default_profile(profile, DEFAULT_EVSE_ID); - EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingSchedulePeriodExtraneousPhaseValues)); + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::DuplicateProfileValidityPeriod)); } -TEST_F(ChargepointTestFixtureV201, K01FR45_IfNumberPhasesGreaterThanMaxNumberPhasesForACEVSE_ThenProfileIsInvalid) { - auto mock_evse = testing::NiceMock(); - ON_CALL(mock_evse, get_current_phase_type).WillByDefault(testing::Return(CurrentPhaseType::AC)); +TEST_F(ChargepointTestFixtureV201, K01FR06_CandidateProfileHasValidToIncomingValidFromOverlaps_RejectIncoming) { + install_profile_on_evse(DEFAULT_EVSE_ID, DEFAULT_PROFILE_ID, ocpp::DateTime("2024-02-01T13:00:00"), {}); - auto periods = create_charging_schedule_periods(0, 4); - auto profile = create_charging_profile( - DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, - create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), uuid()); + auto profile = create_charging_profile(DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxDefaultProfile, + create_charge_schedule(ChargingRateUnitEnum::A), uuid(), + ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL, + ocpp::DateTime("2024-01-31T13:00:00"), {}); - auto sut = handler.validate_profile_schedules(profile, &mock_evse); + auto sut = handler.validate_tx_default_profile(profile, DEFAULT_EVSE_ID); - EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingSchedulePeriodUnsupportedNumberPhases)); + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::DuplicateProfileValidityPeriod)); } -TEST_F(ChargepointTestFixtureV201, K01FR49_IfNumberPhasesMissingForACEVSE_ThenSetNumberPhasesToThree) { - auto mock_evse = testing::NiceMock(); - ON_CALL(mock_evse, get_current_phase_type).WillByDefault(testing::Return(CurrentPhaseType::AC)); +TEST_F(ChargepointTestFixtureV201, K01FR06_CandidateProfileHasValidPeriodIncomingIsForever_RejectIncoming) { + install_profile_on_evse(DEFAULT_EVSE_ID, DEFAULT_PROFILE_ID, ocpp::DateTime("2024-02-01T13:00:00"), + ocpp::DateTime("2024-02-01T13:00:00")); - auto periods = create_charging_schedule_periods(0); - auto profile = create_charging_profile( - DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, - create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), uuid()); + auto profile = create_charging_profile(DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxDefaultProfile, + create_charge_schedule(ChargingRateUnitEnum::A), uuid(), + ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL, {}, {}); - auto sut = handler.validate_profile_schedules(profile, &mock_evse); + auto sut = handler.validate_tx_default_profile(profile, DEFAULT_EVSE_ID); - auto numberPhases = profile.chargingSchedule[0].chargingSchedulePeriod[0].numberPhases; + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::DuplicateProfileValidityPeriod)); +} - EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::Valid)); - EXPECT_THAT(numberPhases, testing::Eq(3)); +TEST_F(ChargepointTestFixtureV201, K01FR06_CandidateProfileHasValidPeriodIncomingIsOverlaps_RejectIncoming) { + install_profile_on_evse(DEFAULT_EVSE_ID, DEFAULT_PROFILE_ID, ocpp::DateTime("2024-01-01T13:00:00"), + ocpp::DateTime("2024-02-01T13:00:00")); + + auto profile = create_charging_profile( + DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxDefaultProfile, + create_charge_schedule(ChargingRateUnitEnum::A, {}, {}), uuid(), ChargingProfileKindEnum::Absolute, + DEFAULT_STACK_LEVEL, ocpp::DateTime("2024-01-15T13:00:00"), ocpp::DateTime("2024-02-01T13:00:00")); + + auto sut = handler.validate_tx_default_profile(profile, DEFAULT_EVSE_ID); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::DuplicateProfileValidityPeriod)); } } // namespace ocpp::v201