diff --git a/doc/ocpp_201_status.md b/doc/ocpp_201_status.md index a5b8764d1..5dac4419f 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 | :no_entry_sign: | | | K01.FR.04 | :white_check_mark: | | | K01.FR.05 | | | -| K01.FR.06 | | | +| K01.FR.06 | :white_check_mark: | | | K01.FR.07 | | | | K01.FR.08 | :no_entry_sign: | | | K01.FR.09 | | | @@ -1157,10 +1157,10 @@ This document contains the status of which OCPP 2.0.1 numbered requirements have | K01.FR.16 | | | | K01.FR.17 | | | | K01.FR.19 | | | -| K01.FR.20 | :white_check_mark: | | +| K01.FR.20 | :white_check_mark: | This FR hints that `ACPhaseSwitchingSupported` should be per EVSE, but this makes no sense with the rest of the spec. | | K01.FR.21 | | | | K01.FR.22 | | | -| K01.FR.26 | | | +| K01.FR.26 | :white_check_mark: | | | K01.FR.27 | | | | K01.FR.28 | | | | K01.FR.29 | | | @@ -1172,7 +1172,7 @@ This document contains the status of which OCPP 2.0.1 numbered requirements have | K01.FR.35 | | | | K01.FR.36 | :white_check_mark: | | | K01.FR.37 | | | -| K01.FR.38 | | | +| K01.FR.38 | :white_check_mark: | | | K01.FR.39 | :white_check_mark: | | | K01.FR.40 | :white_check_mark: | | | K01.FR.41 | :white_check_mark: | | @@ -1218,7 +1218,7 @@ This document contains the status of which OCPP 2.0.1 numbered requirements have | --- | --- | --- | | K04.FR.01 | | | | K04.FR.02 | | | -| K04.FR.03 | | | +| K04.FR.03 | :white_check_mark: | | | K04.FR.04 | | The same as K01.FR.21 | | K04.FR.05 | | This should be handled by the user of libocpp | ## SmartCharging - Remote Start Transaction with Charging Profile diff --git a/include/ocpp/v201/charge_point.hpp b/include/ocpp/v201/charge_point.hpp index 7a318277e..130bd3a3b 100644 --- a/include/ocpp/v201/charge_point.hpp +++ b/include/ocpp/v201/charge_point.hpp @@ -4,6 +4,7 @@ #pragma once #include +#include #include #include @@ -393,7 +394,7 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa // utility std::unique_ptr> message_queue; - std::unique_ptr device_model; + std::shared_ptr device_model; std::shared_ptr database_handler; std::map scheduled_change_availability_requests; diff --git a/include/ocpp/v201/smart_charging.hpp b/include/ocpp/v201/smart_charging.hpp index de4f27896..248cb215a 100644 --- a/include/ocpp/v201/smart_charging.hpp +++ b/include/ocpp/v201/smart_charging.hpp @@ -4,9 +4,11 @@ #ifndef OCPP_V201_SMART_CHARGING_HPP #define OCPP_V201_SMART_CHARGING_HPP +#include "ocpp/v201/device_model.hpp" #include "ocpp/v201/enums.hpp" #include +#include #include #include #include @@ -19,6 +21,7 @@ const int DEFAULT_AND_MAX_NUMBER_PHASES = 3; enum class ProfileValidationResultEnum { Valid, EvseDoesNotExist, + InvalidProfileType, TxProfileMissingTransactionId, TxProfileEvseIdNotGreaterThanZero, TxProfileTransactionNotOnEvse, @@ -28,11 +31,16 @@ enum class ProfileValidationResultEnum { ChargingProfileFirstStartScheduleIsNotZero, ChargingProfileMissingRequiredStartSchedule, ChargingProfileExtraneousStartSchedule, + ChargingScheduleChargingRateUnitUnsupported, ChargingSchedulePeriodsOutOfOrder, ChargingSchedulePeriodInvalidPhaseToUse, ChargingSchedulePeriodUnsupportedNumberPhases, ChargingSchedulePeriodExtraneousPhaseValues, - DuplicateTxDefaultProfileFound + ChargingSchedulePeriodPhaseToUseACPhaseSwitchingUnsupported, + ChargingStationMaxProfileCannotBeRelative, + ChargingStationMaxProfileEvseIdGreaterThanZero, + DuplicateTxDefaultProfileFound, + DuplicateProfileValidityPeriod }; namespace conversions { @@ -48,6 +56,7 @@ std::ostream& operator<<(std::ostream& os, const ProfileValidationResultEnum val class SmartChargingHandler { private: std::map>& evses; + std::shared_ptr& device_model; std::shared_ptr database_handler; // cppcheck-suppress unusedStructMember @@ -55,22 +64,43 @@ class SmartChargingHandler { std::vector station_wide_charging_profiles; public: - explicit SmartChargingHandler(std::map>& evses); + explicit SmartChargingHandler(std::map>& evses, + std::shared_ptr& device_model); + /// + /// \brief validates the given \p profile according to the specification. + /// If a profile does not have validFrom or validTo set, we conform the values + /// to a representation that fits the spec. + /// + ProfileValidationResultEnum validate_profile(ChargingProfile& profile, int32_t evse_id); + + /// + /// \brief Adds a given \p profile and associated \p evse_id to our stored list of profiles + /// + void add_profile(int32_t evse_id, ChargingProfile& profile); + +protected: /// /// \brief validates the existence of the given \p evse_id according to the specification /// ProfileValidationResultEnum validate_evse_exists(int32_t evse_id) const; + /// + /// \brief validates requirements that apply only to the ChargingStationMaxProfile \p profile + /// according to the specification + /// + ProfileValidationResultEnum validate_charging_station_max_profile(const ChargingProfile& profile, + int32_t evse_id) const; + /// /// \brief validates the given \p profile and associated \p evse_id according to the specification /// - ProfileValidationResultEnum validate_tx_default_profile(const ChargingProfile& profile, int32_t evse_id) const; + ProfileValidationResultEnum validate_tx_default_profile(ChargingProfile& profile, int32_t evse_id) const; /// /// \brief validates the given \p profile according to the specification /// - ProfileValidationResultEnum validate_tx_profile(const ChargingProfile& profile, EvseInterface& evse) const; + ProfileValidationResultEnum validate_tx_profile(const ChargingProfile& profile, int32_t evse_id) const; /// \brief validates that the given \p profile has valid charging schedules. /// If a profiles charging schedule period does not have a valid numberPhases, @@ -79,13 +109,16 @@ class SmartChargingHandler { std::optional evse_opt = std::nullopt) const; /// - /// \brief Adds a given \p profile and associated \p evse_id to our stored list of profiles + /// \brief Checks a given \p profile and associated \p evse_id validFrom and validTo range + /// This method assumes that the existing profile will have dates set for validFrom and validTo /// - void add_profile(int32_t evse_id, ChargingProfile& profile); + bool is_overlapping_validity_period(int evse_id, const ChargingProfile& profile) const; private: std::vector get_evse_specific_tx_default_profiles() const; std::vector get_station_wide_tx_default_profiles() const; + void conform_validity_periods(ChargingProfile& profile) const; + CurrentPhaseType get_current_phase_type(const std::optional evse_opt) const; }; } // namespace ocpp::v201 diff --git a/lib/ocpp/v201/charge_point.cpp b/lib/ocpp/v201/charge_point.cpp index c57afcc2d..edcfb369a 100644 --- a/lib/ocpp/v201/charge_point.cpp +++ b/lib/ocpp/v201/charge_point.cpp @@ -80,7 +80,7 @@ ChargePoint::ChargePoint(const std::map& evse_connector_struct EVLOG_AND_THROW(std::invalid_argument("All non-optional callbacks must be supplied")); } - this->device_model = std::make_unique(std::move(device_model_storage)); + this->device_model = std::make_shared(std::move(device_model_storage)); this->device_model->check_integrity(evse_connector_structure); auto database_connection = std::make_unique(fs::path(core_database_path) / "cp.db"); diff --git a/lib/ocpp/v201/smart_charging.cpp b/lib/ocpp/v201/smart_charging.cpp index ca3e14c0c..0f54573f6 100644 --- a/lib/ocpp/v201/smart_charging.cpp +++ b/lib/ocpp/v201/smart_charging.cpp @@ -1,15 +1,20 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest +#include "date/tz.h" #include "everest/logging.hpp" #include "ocpp/common/types.hpp" +#include "ocpp/v201/ctrlr_component_variables.hpp" +#include "ocpp/v201/device_model.hpp" #include "ocpp/v201/enums.hpp" #include "ocpp/v201/evse.hpp" #include "ocpp/v201/ocpp_types.hpp" #include "ocpp/v201/transaction.hpp" +#include #include #include #include +#include using namespace std::chrono; @@ -22,6 +27,8 @@ std::string profile_validation_result_to_string(ProfileValidationResultEnum e) { return "Valid"; case ProfileValidationResultEnum::EvseDoesNotExist: return "EvseDoesNotExist"; + case ProfileValidationResultEnum::InvalidProfileType: + return "InvalidProfileType"; case ProfileValidationResultEnum::TxProfileMissingTransactionId: return "TxProfileMissingTransactionId"; case ProfileValidationResultEnum::TxProfileEvseIdNotGreaterThanZero: @@ -40,6 +47,8 @@ std::string profile_validation_result_to_string(ProfileValidationResultEnum e) { return "ChargingProfileMissingRequiredStartSchedule"; case ProfileValidationResultEnum::ChargingProfileExtraneousStartSchedule: return "ChargingProfileExtraneousStartSchedule"; + case ProfileValidationResultEnum::ChargingScheduleChargingRateUnitUnsupported: + return "ChargingScheduleChargingRateUnitUnsupported"; case ProfileValidationResultEnum::ChargingSchedulePeriodsOutOfOrder: return "ChargingSchedulePeriodsOutOfOrder"; case ProfileValidationResultEnum::ChargingSchedulePeriodInvalidPhaseToUse: @@ -48,8 +57,16 @@ std::string profile_validation_result_to_string(ProfileValidationResultEnum e) { return "ChargingSchedulePeriodUnsupportedNumberPhases"; case ProfileValidationResultEnum::ChargingSchedulePeriodExtraneousPhaseValues: return "ChargingSchedulePeriodExtraneousPhaseValues"; + case ProfileValidationResultEnum::ChargingSchedulePeriodPhaseToUseACPhaseSwitchingUnsupported: + return "ChargingSchedulePeriodPhaseToUseACPhaseSwitchingUnsupported"; + case ProfileValidationResultEnum::ChargingStationMaxProfileCannotBeRelative: + return "ChargingStationMaxProfileCannotBeRelative"; + case ProfileValidationResultEnum::ChargingStationMaxProfileEvseIdGreaterThanZero: + return "ChargingStationMaxProfileEvseIdGreaterThanZero"; case ProfileValidationResultEnum::DuplicateTxDefaultProfileFound: return "DuplicateTxDefaultProfileFound"; + case ProfileValidationResultEnum::DuplicateProfileValidityPeriod: + return "DuplicateProfileValidityPeriod"; } throw std::out_of_range("No known string conversion for provided enum of type ProfileValidationResultEnum"); @@ -63,7 +80,67 @@ std::ostream& operator<<(std::ostream& os, const ProfileValidationResultEnum val const int32_t STATION_WIDE_ID = 0; -SmartChargingHandler::SmartChargingHandler(std::map>& evses) : evses(evses) { +CurrentPhaseType SmartChargingHandler::get_current_phase_type(const std::optional evse_opt) const { + if (evse_opt.has_value()) { + return evse_opt.value()->get_current_phase_type(); + } + + auto supply_phases = + this->device_model->get_value(ControllerComponentVariables::ChargingStationSupplyPhases); + if (supply_phases == 1 || supply_phases == 3) { + return CurrentPhaseType::AC; + } else if (supply_phases == 0) { + return CurrentPhaseType::DC; + } + + return CurrentPhaseType::Unknown; +} + +SmartChargingHandler::SmartChargingHandler(std::map>& evses, + std::shared_ptr& device_model) : + evses(evses), device_model(device_model) { +} + +ProfileValidationResultEnum SmartChargingHandler::validate_profile(ChargingProfile& profile, int32_t evse_id) { + conform_validity_periods(profile); + + auto result = ProfileValidationResultEnum::Valid; + if (evse_id != STATION_WIDE_ID) { + result = this->validate_evse_exists(evse_id); + if (result != ProfileValidationResultEnum::Valid) { + return result; + } + } + + if (evse_id != STATION_WIDE_ID) { + auto& evse = *evses[evse_id]; + result = this->validate_profile_schedules(profile, &evse); + } else { + result = this->validate_profile_schedules(profile); + } + if (result != ProfileValidationResultEnum::Valid) { + return result; + } + + switch (profile.chargingProfilePurpose) { + case ChargingProfilePurposeEnum::ChargingStationMaxProfile: + result = this->validate_charging_station_max_profile(profile, evse_id); + break; + case ChargingProfilePurposeEnum::TxDefaultProfile: + result = this->validate_tx_default_profile(profile, evse_id); + break; + case ChargingProfilePurposeEnum::TxProfile: + result = this->validate_tx_profile(profile, evse_id); + break; + case ChargingProfilePurposeEnum::ChargingStationExternalConstraints: + // TODO: How do we check this? We shouldn't set it in + // `SetChargingProfileRequest`, but that doesn't mean they're always + // invalid. K01.FR.05 is the only thing that seems relevant. + result = ProfileValidationResultEnum::Valid; + break; + } + + return result; } ProfileValidationResultEnum SmartChargingHandler::validate_evse_exists(int32_t evse_id) const { @@ -71,9 +148,35 @@ ProfileValidationResultEnum SmartChargingHandler::validate_evse_exists(int32_t e : ProfileValidationResultEnum::Valid; } -ProfileValidationResultEnum SmartChargingHandler::validate_tx_default_profile(const ChargingProfile& profile, +ProfileValidationResultEnum SmartChargingHandler::validate_charging_station_max_profile(const ChargingProfile& profile, + int32_t evse_id) const { + if (profile.chargingProfilePurpose != ChargingProfilePurposeEnum::ChargingStationMaxProfile) { + return ProfileValidationResultEnum::InvalidProfileType; + } + + if (is_overlapping_validity_period(evse_id, profile)) { + return ProfileValidationResultEnum::DuplicateProfileValidityPeriod; + } + + if (evse_id > 0) { + return ProfileValidationResultEnum::ChargingStationMaxProfileEvseIdGreaterThanZero; + } + + if (profile.chargingProfileKind == ChargingProfileKindEnum::Relative) { + return ProfileValidationResultEnum::ChargingStationMaxProfileCannotBeRelative; + } + + return ProfileValidationResultEnum::Valid; +} + +ProfileValidationResultEnum SmartChargingHandler::validate_tx_default_profile(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) { @@ -86,16 +189,22 @@ ProfileValidationResultEnum SmartChargingHandler::validate_tx_default_profile(co } ProfileValidationResultEnum SmartChargingHandler::validate_tx_profile(const ChargingProfile& profile, - EvseInterface& evse) const { + int32_t evse_id) const { if (!profile.transactionId.has_value()) { return ProfileValidationResultEnum::TxProfileMissingTransactionId; } - int32_t evseId = evse.get_evse_info().id; - if (evseId <= 0) { + if (evse_id <= 0) { return ProfileValidationResultEnum::TxProfileEvseIdNotGreaterThanZero; } + // We don't want to retrieve an EVSE that doesn't exist below this point. + auto result = this->validate_evse_exists(evse_id); + if (result != ProfileValidationResultEnum::Valid) { + return result; + } + + auto& evse = *evses[evse_id]; if (!evse.has_active_transaction()) { return ProfileValidationResultEnum::TxProfileEvseHasNoActiveTransaction; } @@ -112,6 +221,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; } @@ -120,15 +230,24 @@ ProfileValidationResultEnum SmartChargingHandler::validate_tx_profile(const Char } /* TODO: Implement the following functional requirements: - * - K01.FR.20 * - K01.FR.34 * - 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) { + // K01.FR.26; We currently need to do string conversions for this manually because our DeviceModel class does + // not let us get a vector of ChargingScheduleChargingRateUnits. + auto supported_charging_rate_units = + this->device_model->get_value(ControllerComponentVariables::ChargingScheduleChargingRateUnit); + if (supported_charging_rate_units.find(conversions::charging_rate_unit_enum_to_string( + schedule.chargingRateUnit)) == supported_charging_rate_units.npos) { + return ProfileValidationResultEnum::ChargingScheduleChargingRateUnitUnsupported; + } + // A schedule must have at least one chargingSchedulePeriod if (schedule.chargingSchedulePeriod.empty()) { return ProfileValidationResultEnum::ChargingProfileNoChargingSchedulePeriods; @@ -141,6 +260,13 @@ SmartChargingHandler::validate_profile_schedules(ChargingProfile& profile, return ProfileValidationResultEnum::ChargingSchedulePeriodInvalidPhaseToUse; } + // K01.FR.20 + if (charging_schedule_period.phaseToUse.has_value() && + !device_model->get_optional_value(ControllerComponentVariables::ACPhaseSwitchingSupported) + .value_or(false)) { + return ProfileValidationResultEnum::ChargingSchedulePeriodPhaseToUseACPhaseSwitchingUnsupported; + } + // K01.FR.31 if (i == 0 && charging_schedule_period.startPeriod != 0) { return ProfileValidationResultEnum::ChargingProfileFirstStartScheduleIsNotZero; @@ -154,27 +280,24 @@ SmartChargingHandler::validate_profile_schedules(ChargingProfile& profile, } } - if (evse_opt.has_value()) { - auto evse = evse_opt.value(); - // K01.FR.44 for EVSEs; We reject profiles that provide invalid numberPhases/phaseToUse instead - // of silently acccepting them. - if (evse->get_current_phase_type() == CurrentPhaseType::DC && - (charging_schedule_period.numberPhases.has_value() || - charging_schedule_period.phaseToUse.has_value())) { - return ProfileValidationResultEnum::ChargingSchedulePeriodExtraneousPhaseValues; + auto phase_type = this->get_current_phase_type(evse_opt); + // K01.FR.44; We reject profiles that provide invalid numberPhases/phaseToUse instead + // of silently acccepting them. + if (phase_type == CurrentPhaseType::DC && (charging_schedule_period.numberPhases.has_value() || + charging_schedule_period.phaseToUse.has_value())) { + return ProfileValidationResultEnum::ChargingSchedulePeriodExtraneousPhaseValues; + } + + if (phase_type == CurrentPhaseType::AC) { + // K01.FR.45; Once again rejecting invalid values + if (charging_schedule_period.numberPhases.has_value() && + charging_schedule_period.numberPhases > DEFAULT_AND_MAX_NUMBER_PHASES) { + return ProfileValidationResultEnum::ChargingSchedulePeriodUnsupportedNumberPhases; } - if (evse->get_current_phase_type() == CurrentPhaseType::AC) { - // K01.FR.45; Once again rejecting invalid values - if (charging_schedule_period.numberPhases.has_value() && - charging_schedule_period.numberPhases > DEFAULT_AND_MAX_NUMBER_PHASES) { - return ProfileValidationResultEnum::ChargingSchedulePeriodUnsupportedNumberPhases; - } - - // K01.FR.49 - if (!charging_schedule_period.numberPhases.has_value()) { - charging_schedule_period.numberPhases.emplace(DEFAULT_AND_MAX_NUMBER_PHASES); - } + // K01.FR.49 + if (!charging_schedule_period.numberPhases.has_value()) { + charging_schedule_period.numberPhases.emplace(DEFAULT_AND_MAX_NUMBER_PHASES); } } } @@ -224,4 +347,39 @@ 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, + const 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) { + + return candidate_profile.validFrom <= existing_profile.validTo && + candidate_profile.validTo >= existing_profile.validFrom; // reject + } + return false; + }); + } + return false; + }; + + return std::any_of(charging_profiles.begin(), charging_profiles.end(), conflicts_with); +} + +void SmartChargingHandler::conform_validity_periods(ChargingProfile& profile) const { + profile.validFrom = profile.validFrom.value_or(ocpp::DateTime()); + profile.validTo = profile.validTo.value_or(ocpp::DateTime(date::utc_clock::time_point::max())); +} + } // namespace ocpp::v201 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 0d4f261e5..5b8ca4ad6 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -1,5 +1,6 @@ set(MIGRATION_FILES_LOCATION_V16 "${CMAKE_CURRENT_BINARY_DIR}/resources/v16/migration_files") set(MIGRATION_FILES_LOCATION_V201 "${CMAKE_CURRENT_BINARY_DIR}/resources/v201/migration_files") +set(DEVICE_MODEL_DB_LOCATION_V201 "${CMAKE_INSTALL_PREFIX}/${CMAKE_INSTALL_DATADIR}/everest/modules/OCPP201/device_model_storage.db") add_executable(database_tests database_tests.cpp) @@ -31,6 +32,7 @@ target_compile_definitions(libocpp_unit_tests MIGRATION_FILES_LOCATION_V201="${MIGRATION_FILES_LOCATION_V201}" MIGRATION_FILE_VERSION_V16=${MIGRATION_FILE_VERSION_V16} MIGRATION_FILE_VERSION_V201=${MIGRATION_FILE_VERSION_V201} + DEVICE_MODEL_DB_LOCATION_V201="${DEVICE_MODEL_DB_LOCATION_V201}" ) add_custom_command(TARGET libocpp_unit_tests POST_BUILD diff --git a/tests/lib/ocpp/v201/test_smart_charging_handler.cpp b/tests/lib/ocpp/v201/test_smart_charging_handler.cpp index 7c12c9abc..3a1f8683e 100644 --- a/tests/lib/ocpp/v201/test_smart_charging_handler.cpp +++ b/tests/lib/ocpp/v201/test_smart_charging_handler.cpp @@ -2,7 +2,9 @@ // Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest #include "date/tz.h" +#include "ocpp/common/types.hpp" #include "ocpp/v201/ctrlr_component_variables.hpp" +#include "ocpp/v201/device_model.hpp" #include "ocpp/v201/device_model_storage_sqlite.hpp" #include "ocpp/v201/ocpp_types.hpp" #include @@ -11,6 +13,7 @@ #include #include #include +#include #include #include @@ -31,12 +34,31 @@ static const int STATION_WIDE_ID = 0; static const int DEFAULT_EVSE_ID = 1; static const int DEFAULT_PROFILE_ID = 1; static const int DEFAULT_STACK_LEVEL = 1; +static const std::string DEFAULT_TX_ID = "10c75ff7-74f5-44f5-9d01-f649f3ac7b78"; + +class TestSmartChargingHandler : public SmartChargingHandler { +public: + using SmartChargingHandler::validate_charging_station_max_profile; + using SmartChargingHandler::validate_evse_exists; + using SmartChargingHandler::validate_profile_schedules; + using SmartChargingHandler::validate_tx_default_profile; + using SmartChargingHandler::validate_tx_profile; + + TestSmartChargingHandler(std::map>& evses, + std::shared_ptr& device_model) : + SmartChargingHandler(evses, device_model) { + } +}; class ChargepointTestFixtureV201 : public testing::Test { protected: void SetUp() override { } + void TearDown() override { + sqlite3_close(this->db_handle); + } + ChargingSchedule create_charge_schedule(ChargingRateUnitEnum charging_rate_unit) { int32_t id; std::vector charging_schedule_period; @@ -113,9 +135,10 @@ class ChargepointTestFixtureV201 : public testing::Test { ChargingProfile create_charging_profile(int32_t charging_profile_id, ChargingProfilePurposeEnum charging_profile_purpose, - ChargingSchedule charging_schedule, std::string transaction_id, + ChargingSchedule charging_schedule, std::optional 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, @@ -125,36 +148,40 @@ class ChargepointTestFixtureV201 : public testing::Test { .chargingSchedule = charging_schedules, .customData = {}, .recurrencyKind = recurrency_kind, - .validFrom = {}, - .validTo = {}, + .validFrom = validFrom, + .validTo = validTo, .transactionId = transaction_id}; } - ChargingProfile create_tx_profile_with_missing_transaction_id(ChargingSchedule charging_schedule) { - auto charging_profile_id = DEFAULT_PROFILE_ID; - auto stack_level = DEFAULT_STACK_LEVEL; - 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{ - charging_profile_id, - stack_level, - charging_profile_purpose, - charging_profile_kind, - charging_schedules, - {}, // transactionId - recurrency_kind, - {}, // validFrom - {} // validTo - }; + void create_device_model_db(const std::string& path) { + sqlite3* source_handle; + sqlite3_open(DEVICE_MODEL_DB_LOCATION_V201, &source_handle); + sqlite3_open(path.c_str(), &db_handle); + + auto* backup = sqlite3_backup_init(db_handle, "main", source_handle, "main"); + sqlite3_backup_step(backup, -1); + sqlite3_backup_finish(backup); + + sqlite3_close(source_handle); } - DeviceModel create_device_model() { - 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)); + std::shared_ptr + create_device_model(const std::string& path = "file:device_model?mode=memory&cache=shared", + const std::optional ac_phase_switching_supported = "true") { + create_device_model_db(path); + auto device_model_storage = std::make_unique(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(), + AttributeEnum::Actual, "A,W", true); + + const auto& ac_phase_switching_cv = ControllerComponentVariables::ACPhaseSwitchingSupported; + device_model->set_value(ac_phase_switching_cv.component, ac_phase_switching_cv.variable.value(), + AttributeEnum::Actual, ac_phase_switching_supported.value_or(""), true); + + return device_model; } void create_evse_with_id(int id) { @@ -163,13 +190,13 @@ class ChargepointTestFixtureV201 : public testing::Test { 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(), + 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 create_smart_charging_handler() { - return SmartChargingHandler(evses); + TestSmartChargingHandler create_smart_charging_handler() { + return TestSmartChargingHandler(evses, device_model); } std::string uuid() { @@ -189,12 +216,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), + {}, ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL, validFrom, validTo); handler.add_profile(evse_id, existing_profile); } @@ -202,16 +232,19 @@ class ChargepointTestFixtureV201 : public testing::Test { std::map> evses; std::shared_ptr database_handler; + sqlite3* db_handle; + bool ignore_no_transaction = true; - DeviceModel device_model = create_device_model(); - SmartChargingHandler handler = create_smart_charging_handler(); + std::shared_ptr device_model = create_device_model(); + TestSmartChargingHandler handler = create_smart_charging_handler(); boost::uuids::random_generator uuid_generator = boost::uuids::random_generator(); }; TEST_F(ChargepointTestFixtureV201, K01FR03_IfTxProfileIsMissingTransactionId_ThenProfileIsInvalid) { create_evse_with_id(DEFAULT_EVSE_ID); - auto profile = create_tx_profile_with_missing_transaction_id(create_charge_schedule(ChargingRateUnitEnum::A)); - auto sut = handler.validate_tx_profile(profile, *evses[DEFAULT_EVSE_ID]); + auto profile = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A), {}); + auto sut = handler.validate_tx_profile(profile, DEFAULT_EVSE_ID); EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::TxProfileMissingTransactionId)); } @@ -220,8 +253,8 @@ TEST_F(ChargepointTestFixtureV201, K01FR16_IfTxProfileHasEvseIdNotGreaterThanZer auto wrong_evse_id = STATION_WIDE_ID; create_evse_with_id(wrong_evse_id); auto profile = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, - create_charge_schedule(ChargingRateUnitEnum::A), uuid()); - auto sut = handler.validate_tx_profile(profile, *evses[wrong_evse_id]); + create_charge_schedule(ChargingRateUnitEnum::A), DEFAULT_TX_ID); + auto sut = handler.validate_tx_profile(profile, wrong_evse_id); EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::TxProfileEvseIdNotGreaterThanZero)); } @@ -230,8 +263,8 @@ TEST_F(ChargepointTestFixtureV201, K01FR33_IfTxProfileTransactionIsNotOnEvse_The create_evse_with_id(DEFAULT_EVSE_ID); open_evse_transaction(DEFAULT_EVSE_ID, "wrong transaction id"); auto profile = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, - create_charge_schedule(ChargingRateUnitEnum::A), uuid()); - auto sut = handler.validate_tx_profile(profile, *evses[DEFAULT_EVSE_ID]); + create_charge_schedule(ChargingRateUnitEnum::A), DEFAULT_TX_ID); + auto sut = handler.validate_tx_profile(profile, DEFAULT_EVSE_ID); EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::TxProfileTransactionNotOnEvse)); } @@ -243,88 +276,90 @@ TEST_F(ChargepointTestFixtureV201, K01FR09_IfTxProfileEvseHasNoActiveTransaction auto date_time = ocpp::DateTime("2024-01-17T17:00:00"); create_evse_with_id(DEFAULT_EVSE_ID); auto profile = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, - create_charge_schedule(ChargingRateUnitEnum::A), uuid()); - auto sut = handler.validate_tx_profile(profile, *evses[DEFAULT_EVSE_ID]); + create_charge_schedule(ChargingRateUnitEnum::A), DEFAULT_TX_ID); + auto sut = handler.validate_tx_profile(profile, DEFAULT_EVSE_ID); EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::TxProfileEvseHasNoActiveTransaction)); } -TEST_F(ChargepointTestFixtureV201, - K01FR06_IfTxProfileHasSameTransactionAndStackLevelAsAnotherTxProfile_ThenProfileIsInvalid) { - create_evse_with_id(DEFAULT_EVSE_ID); - std::string transaction_id = uuid(); - open_evse_transaction(DEFAULT_EVSE_ID, transaction_id); +TEST_F(ChargepointTestFixtureV201, K01FR19_NumberPhasesOtherThan1AndPhaseToUseSet_ThenProfileInvalid) { + auto periods = create_charging_schedule_periods_with_phases(0, 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")), DEFAULT_TX_ID, + ChargingProfileKindEnum::Absolute, 1); - auto same_stack_level = 42; - 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]); + auto sut = handler.validate_profile_schedules(profile); - EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::TxProfileConflictingStackLevel)); + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingSchedulePeriodInvalidPhaseToUse)); } -TEST_F(ChargepointTestFixtureV201, - K01FR06_IfTxProfileHasDifferentTransactionButSameStackLevelAsAnotherTxProfile_ThenProfileIsValid) { - create_evse_with_id(DEFAULT_EVSE_ID); - std::string transaction_id = uuid(); - std::string different_transaction_id = uuid(); - open_evse_transaction(DEFAULT_EVSE_ID, transaction_id); +TEST_F(ChargepointTestFixtureV201, K01FR20_IfPhaseToUseSetAndACPhaseSwitchingSupportedUndefined_ThenProfileIsInvalid) { + auto device_model_without_ac_phase_switching = + create_device_model("file:device_model2?mode=memory&cache=shared", {}); + device_model = std::move(device_model_without_ac_phase_switching); - auto same_stack_level = 42; - 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]); + auto periods = create_charging_schedule_periods_with_phases(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")), DEFAULT_TX_ID, + ChargingProfileKindEnum::Absolute, 1); - EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::Valid)); + auto sut = handler.validate_profile_schedules(profile); + + EXPECT_THAT(sut, + testing::Eq(ProfileValidationResultEnum::ChargingSchedulePeriodPhaseToUseACPhaseSwitchingUnsupported)); } -TEST_F(ChargepointTestFixtureV201, - K01FR06_IfTxProfileHasSameTransactionButDifferentStackLevelAsAnotherTxProfile_ThenProfileIsValid) { - create_evse_with_id(DEFAULT_EVSE_ID); - std::string same_transaction_id = uuid(); - open_evse_transaction(DEFAULT_EVSE_ID, same_transaction_id); +TEST_F(ChargepointTestFixtureV201, K01FR20_IfPhaseToUseSetAndACPhaseSwitchingSupportedFalse_ThenProfileIsInvalid) { + auto device_model_with_false_ac_phase_switching = + create_device_model("file:device_model2?mode=memory&cache=shared", "false"); + device_model = std::move(device_model_with_false_ac_phase_switching); - auto stack_level_1 = 42; - auto stack_level_2 = 43; + auto periods = create_charging_schedule_periods_with_phases(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")), DEFAULT_TX_ID, + ChargingProfileKindEnum::Absolute, 1); - 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); + auto sut = handler.validate_profile_schedules(profile); - 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::ChargingSchedulePeriodPhaseToUseACPhaseSwitchingUnsupported)); +} - EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::Valid)); +TEST_F(ChargepointTestFixtureV201, K01FR20_IfPhaseToUseSetAndACPhaseSwitchingSupportedTrue_ThenProfileIsNotInvalid) { + auto periods = create_charging_schedule_periods_with_phases(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")), DEFAULT_TX_ID, + ChargingProfileKindEnum::Absolute, 1); + + auto sut = handler.validate_profile_schedules(profile); + + EXPECT_THAT(sut, testing::Not(ProfileValidationResultEnum::ChargingSchedulePeriodExtraneousPhaseValues)); } -TEST_F(ChargepointTestFixtureV201, K01FR19_NumberPhasesOtherThan1AndPhaseToUseSet_ThenProfileInvalid) { - auto periods = create_charging_schedule_periods_with_phases(0, 0, 1); +TEST_F(ChargepointTestFixtureV201, + K01FR26_IfChargingRateUnitIsNotInChargingScheduleChargingRateUnits_ThenProfileIsInvalid) { + const auto& charging_rate_unit_cv = ControllerComponentVariables::ChargingScheduleChargingRateUnit; + device_model->set_value(charging_rate_unit_cv.component, charging_rate_unit_cv.variable.value(), + AttributeEnum::Actual, "W", true); + + 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(), + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), DEFAULT_TX_ID, ChargingProfileKindEnum::Absolute, 1); auto sut = handler.validate_profile_schedules(profile); - EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingSchedulePeriodInvalidPhaseToUse)); + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingScheduleChargingRateUnitUnsupported)); } TEST_F(ChargepointTestFixtureV201, K01_IfChargingSchedulePeriodsAreMissing_ThenProfileIsInvalid) { auto profile = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, - create_charge_schedule(ChargingRateUnitEnum::A), uuid()); + create_charge_schedule(ChargingRateUnitEnum::A), DEFAULT_TX_ID); auto sut = handler.validate_profile_schedules(profile); @@ -334,7 +369,7 @@ TEST_F(ChargepointTestFixtureV201, K01_IfChargingSchedulePeriodsAreMissing_ThenP TEST_F(ChargepointTestFixtureV201, K01FR31_IfStartPeriodOfFirstChargingSchedulePeriodIsNotZero_ThenProfileIsInvalid) { auto periods = create_charging_schedule_periods(1); auto profile = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, - create_charge_schedule(ChargingRateUnitEnum::A, periods), uuid()); + create_charge_schedule(ChargingRateUnitEnum::A, periods), DEFAULT_TX_ID); auto sut = handler.validate_profile_schedules(profile); @@ -344,18 +379,127 @@ TEST_F(ChargepointTestFixtureV201, K01FR31_IfStartPeriodOfFirstChargingScheduleP TEST_F(ChargepointTestFixtureV201, K01FR35_IfChargingSchedulePeriodsAreNotInChonologicalOrder_ThenProfileIsInvalid) { auto periods = create_charging_schedule_periods({0, 2, 1}); auto profile = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, - create_charge_schedule(ChargingRateUnitEnum::A, periods), uuid()); + create_charge_schedule(ChargingRateUnitEnum::A, periods), DEFAULT_TX_ID); auto sut = handler.validate_profile_schedules(profile); EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingSchedulePeriodsOutOfOrder)); } +TEST_F(ChargepointTestFixtureV201, K01_ValidateChargingStationMaxProfile_NotChargingStationMaxProfile_Invalid) { + create_evse_with_id(STATION_WIDE_ID); + auto periods = create_charging_schedule_periods({0, 1, 2}); + auto profile = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxDefaultProfile, + create_charge_schedule(ChargingRateUnitEnum::A)); + + auto sut = handler.validate_charging_station_max_profile(profile, STATION_WIDE_ID); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::InvalidProfileType)); +} + +TEST_F(ChargepointTestFixtureV201, K04FR03_ValidateChargingStationMaxProfile_EvseIDgt0_Invalid) { + const int EVSE_ID_1 = DEFAULT_EVSE_ID; + auto periods = create_charging_schedule_periods({0, 1, 2}); + auto profile = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::ChargingStationMaxProfile, + create_charge_schedule(ChargingRateUnitEnum::A, periods)); + + auto sut = handler.validate_charging_station_max_profile(profile, EVSE_ID_1); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingStationMaxProfileEvseIdGreaterThanZero)); +} + +TEST_F(ChargepointTestFixtureV201, K01FR38_ChargingProfilePurposeIsChargingStationMaxProfile_KindIsAbsolute_Valid) { + auto profile = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::ChargingStationMaxProfile, + create_charge_schedule(ChargingRateUnitEnum::A)); + + auto sut = handler.validate_charging_station_max_profile(profile, STATION_WIDE_ID); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::Valid)); +} + +TEST_F(ChargepointTestFixtureV201, K01FR38_ChargingProfilePurposeIsChargingStationMaxProfile_KindIsRecurring_Valid) { + auto profile = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::ChargingStationMaxProfile, + create_charge_schedule(ChargingRateUnitEnum::A), {}, + ChargingProfileKindEnum::Recurring); + + auto sut = handler.validate_charging_station_max_profile(profile, STATION_WIDE_ID); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::Valid)); +} + +TEST_F(ChargepointTestFixtureV201, K01FR38_ChargingProfilePurposeIsChargingStationMaxProfile_KindIsRelative_Invalid) { + auto profile = + create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::ChargingStationMaxProfile, + create_charge_schedule(ChargingRateUnitEnum::A), {}, ChargingProfileKindEnum::Relative); + + auto sut = handler.validate_charging_station_max_profile(profile, STATION_WIDE_ID); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingStationMaxProfileCannotBeRelative)); +} + +TEST_F(ChargepointTestFixtureV201, + K01FR39_IfTxProfileHasSameTransactionAndStackLevelAsAnotherTxProfile_ThenProfileIsInvalid) { + create_evse_with_id(DEFAULT_EVSE_ID); + open_evse_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); + + auto same_stack_level = 42; + auto profile_1 = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A), DEFAULT_TX_ID, + ChargingProfileKindEnum::Absolute, same_stack_level); + auto profile_2 = create_charging_profile(DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A), DEFAULT_TX_ID, + ChargingProfileKindEnum::Absolute, same_stack_level); + handler.add_profile(DEFAULT_EVSE_ID, profile_2); + auto sut = handler.validate_tx_profile(profile_1, DEFAULT_EVSE_ID); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::TxProfileConflictingStackLevel)); +} + +TEST_F(ChargepointTestFixtureV201, + K01FR39_IfTxProfileHasDifferentTransactionButSameStackLevelAsAnotherTxProfile_ThenProfileIsValid) { + create_evse_with_id(DEFAULT_EVSE_ID); + std::string different_transaction_id = uuid(); + open_evse_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); + + auto same_stack_level = 42; + auto profile_1 = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A), DEFAULT_TX_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, DEFAULT_EVSE_ID); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::Valid)); +} + +TEST_F(ChargepointTestFixtureV201, + K01FR39_IfTxProfileHasSameTransactionButDifferentStackLevelAsAnotherTxProfile_ThenProfileIsValid) { + create_evse_with_id(DEFAULT_EVSE_ID); + open_evse_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); + + auto stack_level_1 = 42; + auto stack_level_2 = 43; + + auto profile_1 = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A), DEFAULT_TX_ID, + ChargingProfileKindEnum::Absolute, stack_level_1); + auto profile_2 = create_charging_profile(DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A), DEFAULT_TX_ID, + ChargingProfileKindEnum::Absolute, stack_level_2); + + handler.add_profile(DEFAULT_EVSE_ID, profile_2); + auto sut = handler.validate_tx_profile(profile_1, DEFAULT_EVSE_ID); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::Valid)); +} + TEST_F(ChargepointTestFixtureV201, K01FR40_IfChargingProfileKindIsAbsoluteAndStartScheduleDoesNotExist_ThenProfileIsInvalid) { auto periods = create_charging_schedule_periods(0); auto profile = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, - create_charge_schedule(ChargingRateUnitEnum::A, periods), uuid(), + create_charge_schedule(ChargingRateUnitEnum::A, periods), DEFAULT_TX_ID, ChargingProfileKindEnum::Absolute, 1); auto sut = handler.validate_profile_schedules(profile); @@ -367,7 +511,7 @@ TEST_F(ChargepointTestFixtureV201, K01FR40_IfChargingProfileKindIsRecurringAndStartScheduleDoesNotExist_ThenProfileIsInvalid) { auto periods = create_charging_schedule_periods(0); auto profile = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, - create_charge_schedule(ChargingRateUnitEnum::A, periods), uuid(), + create_charge_schedule(ChargingRateUnitEnum::A, periods), DEFAULT_TX_ID, ChargingProfileKindEnum::Recurring, 1); auto sut = handler.validate_profile_schedules(profile); @@ -380,7 +524,7 @@ TEST_F(ChargepointTestFixtureV201, 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(), + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), DEFAULT_TX_ID, ChargingProfileKindEnum::Relative, 1); auto sut = handler.validate_profile_schedules(profile); @@ -423,7 +567,7 @@ TEST_P(ChargepointTestFixtureV201_FR52, K01FR52_TxDefaultProfileValidationV201Te install_profile_on_evse(DEFAULT_EVSE_ID, DEFAULT_PROFILE_ID); auto profile = create_charging_profile(added_profile_id, ChargingProfilePurposeEnum::TxDefaultProfile, - create_charge_schedule(ChargingRateUnitEnum::A), uuid(), + create_charge_schedule(ChargingRateUnitEnum::A), {}, ChargingProfileKindEnum::Absolute, added_stack_level); auto sut = handler.validate_tx_default_profile(profile, STATION_WIDE_ID); @@ -448,7 +592,7 @@ TEST_P(ChargepointTestFixtureV201_FR53, K01FR53_TxDefaultProfileValidationV201Te create_evse_with_id(DEFAULT_EVSE_ID); auto profile = create_charging_profile(added_profile_id, ChargingProfilePurposeEnum::TxDefaultProfile, - create_charge_schedule(ChargingRateUnitEnum::A), uuid(), + create_charge_schedule(ChargingRateUnitEnum::A), {}, ChargingProfileKindEnum::Absolute, added_stack_level); auto sut = handler.validate_tx_default_profile(profile, DEFAULT_EVSE_ID); @@ -459,7 +603,7 @@ TEST_F(ChargepointTestFixtureV201, K01FR52_TxDefaultProfileValidIfAppliedToWhole install_profile_on_evse(STATION_WIDE_ID, DEFAULT_PROFILE_ID); auto profile = create_charging_profile(DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxDefaultProfile, - create_charge_schedule(ChargingRateUnitEnum::A), uuid(), + create_charge_schedule(ChargingRateUnitEnum::A), {}, ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL); auto sut = handler.validate_tx_default_profile(profile, STATION_WIDE_ID); @@ -470,7 +614,7 @@ TEST_F(ChargepointTestFixtureV201, K01FR53_TxDefaultProfileValidIfAppliedToExist install_profile_on_evse(DEFAULT_EVSE_ID, DEFAULT_PROFILE_ID); auto profile = create_charging_profile(DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxDefaultProfile, - create_charge_schedule(ChargingRateUnitEnum::A), uuid(), + create_charge_schedule(ChargingRateUnitEnum::A), {}, ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL); auto sut = handler.validate_tx_default_profile(profile, DEFAULT_EVSE_ID); @@ -482,7 +626,7 @@ TEST_F(ChargepointTestFixtureV201, K01FR53_TxDefaultProfileValidIfAppliedToDiffe create_evse_with_id(DEFAULT_EVSE_ID + 1); auto profile = create_charging_profile(DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxDefaultProfile, - create_charge_schedule(ChargingRateUnitEnum::A), uuid(), + create_charge_schedule(ChargingRateUnitEnum::A), {}, ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL); auto sut = handler.validate_tx_default_profile(profile, DEFAULT_EVSE_ID + 1); @@ -496,7 +640,7 @@ TEST_F(ChargepointTestFixtureV201, K01FR44_IfNumberPhasesProvidedForDCEVSE_ThenP 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()); + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), DEFAULT_TX_ID); auto sut = handler.validate_profile_schedules(profile, &mock_evse); @@ -510,13 +654,43 @@ TEST_F(ChargepointTestFixtureV201, K01FR44_IfPhaseToUseProvidedForDCEVSE_ThenPro 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()); + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), DEFAULT_TX_ID); auto sut = handler.validate_profile_schedules(profile, &mock_evse); EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingSchedulePeriodExtraneousPhaseValues)); } +TEST_F(ChargepointTestFixtureV201, K01FR44_IfNumberPhasesProvidedForDCChargingStation_ThenProfileIsInvalid) { + device_model->set_value(ControllerComponentVariables::ChargingStationSupplyPhases.component, + ControllerComponentVariables::ChargingStationSupplyPhases.variable.value(), + AttributeEnum::Actual, std::to_string(0), true); + + 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")), DEFAULT_TX_ID); + + auto sut = handler.validate_profile_schedules(profile, std::nullopt); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingSchedulePeriodExtraneousPhaseValues)); +} + +TEST_F(ChargepointTestFixtureV201, K01FR44_IfPhaseToUseProvidedForDCChargingStation_ThenProfileIsInvalid) { + device_model->set_value(ControllerComponentVariables::ChargingStationSupplyPhases.component, + ControllerComponentVariables::ChargingStationSupplyPhases.variable.value(), + AttributeEnum::Actual, std::to_string(0), true); + + 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")), DEFAULT_TX_ID); + + auto sut = handler.validate_profile_schedules(profile, std::nullopt); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingSchedulePeriodExtraneousPhaseValues)); +} + TEST_F(ChargepointTestFixtureV201, K01FR45_IfNumberPhasesGreaterThanMaxNumberPhasesForACEVSE_ThenProfileIsInvalid) { auto mock_evse = testing::NiceMock(); ON_CALL(mock_evse, get_current_phase_type).WillByDefault(testing::Return(CurrentPhaseType::AC)); @@ -524,13 +698,29 @@ TEST_F(ChargepointTestFixtureV201, K01FR45_IfNumberPhasesGreaterThanMaxNumberPha 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()); + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), DEFAULT_TX_ID); auto sut = handler.validate_profile_schedules(profile, &mock_evse); EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingSchedulePeriodUnsupportedNumberPhases)); } +TEST_F(ChargepointTestFixtureV201, + K01FR45_IfNumberPhasesGreaterThanMaxNumberPhasesForACChargingStation_ThenProfileIsInvalid) { + device_model->set_value(ControllerComponentVariables::ChargingStationSupplyPhases.component, + ControllerComponentVariables::ChargingStationSupplyPhases.variable.value(), + AttributeEnum::Actual, std::to_string(1), true); + + 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")), DEFAULT_TX_ID); + + auto sut = handler.validate_profile_schedules(profile, std::nullopt); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingSchedulePeriodUnsupportedNumberPhases)); +} + TEST_F(ChargepointTestFixtureV201, K01FR49_IfNumberPhasesMissingForACEVSE_ThenSetNumberPhasesToThree) { auto mock_evse = testing::NiceMock(); ON_CALL(mock_evse, get_current_phase_type).WillByDefault(testing::Return(CurrentPhaseType::AC)); @@ -538,7 +728,7 @@ TEST_F(ChargepointTestFixtureV201, K01FR49_IfNumberPhasesMissingForACEVSE_ThenSe 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()); + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), DEFAULT_TX_ID); auto sut = handler.validate_profile_schedules(profile, &mock_evse); @@ -548,4 +738,216 @@ TEST_F(ChargepointTestFixtureV201, K01FR49_IfNumberPhasesMissingForACEVSE_ThenSe EXPECT_THAT(numberPhases, testing::Eq(3)); } +TEST_F(ChargepointTestFixtureV201, K01FR49_IfNumberPhasesMissingForACChargingStation_ThenSetNumberPhasesToThree) { + device_model->set_value(ControllerComponentVariables::ChargingStationSupplyPhases.component, + ControllerComponentVariables::ChargingStationSupplyPhases.variable.value(), + AttributeEnum::Actual, std::to_string(3), true); + + 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")), DEFAULT_TX_ID); + + auto sut = handler.validate_profile_schedules(profile, std::nullopt); + + auto numberPhases = profile.chargingSchedule[0].chargingSchedulePeriod[0].numberPhases; + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::Valid)); + EXPECT_THAT(numberPhases, testing::Eq(3)); +} + +TEST_F(ChargepointTestFixtureV201, K01FR06_ExistingProfileLastsForever_RejectIncoming) { + install_profile_on_evse(DEFAULT_EVSE_ID, DEFAULT_PROFILE_ID, ocpp::DateTime(date::utc_clock::time_point::min()), + ocpp::DateTime(date::utc_clock::time_point::max())); + + auto periods = create_charging_schedule_periods(0); + auto profile = create_charging_profile( + DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxDefaultProfile, + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), {}, + ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL, ocpp::DateTime("2024-01-02T13:00:00"), + ocpp::DateTime("2024-03-01T13:00:00")); + + auto sut = handler.validate_profile(profile, DEFAULT_EVSE_ID); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::DuplicateProfileValidityPeriod)); +} + +TEST_F(ChargepointTestFixtureV201, K01FR06_ExisitingProfileHasValidFromIncomingValidToOverlaps_RejectIncoming) { + install_profile_on_evse(DEFAULT_EVSE_ID, DEFAULT_PROFILE_ID, ocpp::DateTime("2024-01-01T13:00:00"), + ocpp::DateTime(date::utc_clock::time_point::max())); + + auto periods = create_charging_schedule_periods(0); + auto profile = create_charging_profile( + DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxDefaultProfile, + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), {}, + ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL, {}, ocpp::DateTime("2024-01-01T13:00:00")); + + auto sut = handler.validate_profile(profile, DEFAULT_EVSE_ID); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::DuplicateProfileValidityPeriod)); +} + +TEST_F(ChargepointTestFixtureV201, K01FR06_ExisitingProfileHasValidToIncomingValidFromOverlaps_RejectIncoming) { + install_profile_on_evse(DEFAULT_EVSE_ID, DEFAULT_PROFILE_ID, ocpp::DateTime("2024-02-01T13:00:00"), + ocpp::DateTime(date::utc_clock::time_point::max())); + + auto periods = create_charging_schedule_periods(0); + auto profile = create_charging_profile( + DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxDefaultProfile, + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), {}, + ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL, ocpp::DateTime("2024-01-31T13:00:00"), {}); + + auto sut = handler.validate_profile(profile, DEFAULT_EVSE_ID); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::DuplicateProfileValidityPeriod)); +} + +TEST_F(ChargepointTestFixtureV201, K01FR06_ExisitingProfileHasValidPeriodIncomingIsNowToMax_RejectIncoming) { + install_profile_on_evse(DEFAULT_EVSE_ID, DEFAULT_PROFILE_ID, + ocpp::DateTime(date::utc_clock::now() - std::chrono::hours(5 * 24)), + ocpp::DateTime(date::utc_clock::now() + std::chrono::hours(5 * 24))); + + auto periods = create_charging_schedule_periods(0); + auto profile = create_charging_profile( + DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxDefaultProfile, + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), {}, + ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL, {}, {}); + + auto sut = handler.validate_profile(profile, DEFAULT_EVSE_ID); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::DuplicateProfileValidityPeriod)); +} + +TEST_F(ChargepointTestFixtureV201, K01FR06_ExisitingProfileHasValidPeriodIncomingOverlaps_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 periods = create_charging_schedule_periods(0); + auto profile = create_charging_profile( + DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxDefaultProfile, + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00")), {}, + ChargingProfileKindEnum::Absolute, DEFAULT_STACK_LEVEL, ocpp::DateTime("2024-01-15T13:00:00"), + ocpp::DateTime("2024-02-01T13:00:00")); + + auto sut = handler.validate_profile(profile, DEFAULT_EVSE_ID); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::DuplicateProfileValidityPeriod)); +} + +TEST_F(ChargepointTestFixtureV201, K01_ValidateProfile_IfEvseDoesNotExist_ThenProfileIsInvalid) { + auto profile = create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A), DEFAULT_TX_ID); + + auto sut = handler.validate_profile(profile, DEFAULT_EVSE_ID + 1); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::EvseDoesNotExist)); +} + +TEST_F(ChargepointTestFixtureV201, K01_ValidateProfile_IfScheduleIsInvalid_ThenProfileIsInvalid) { + create_evse_with_id(DEFAULT_EVSE_ID); + + auto extraneous_start_schedule = ocpp::DateTime("2024-01-17T17: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, extraneous_start_schedule), + DEFAULT_TX_ID, ChargingProfileKindEnum::Relative, 1); + + auto sut = handler.validate_profile(profile, DEFAULT_EVSE_ID); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingProfileExtraneousStartSchedule)); +} + +TEST_F(ChargepointTestFixtureV201, K01_ValidateProfile_IfChargeStationMaxProfileIsInvalid_ThenProfileIsInvalid) { + create_evse_with_id(DEFAULT_EVSE_ID); + auto periods = create_charging_schedule_periods({0, 1, 2}); + auto profile = create_charging_profile( + DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::ChargingStationMaxProfile, + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00"))); + + auto sut = handler.validate_profile(profile, DEFAULT_EVSE_ID); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingStationMaxProfileEvseIdGreaterThanZero)); +} + +TEST_F(ChargepointTestFixtureV201, + K01_ValidateProfile_IfDuplicateTxDefaultProfileFoundOnEVSE_IsInvalid_ThenProfileIsInvalid) { + install_profile_on_evse(DEFAULT_EVSE_ID, DEFAULT_PROFILE_ID); + + auto periods = create_charging_schedule_periods(0); + + auto profile = create_charging_profile(DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxDefaultProfile, + create_charge_schedule(ChargingRateUnitEnum::A, periods), {}, + ChargingProfileKindEnum::Relative, DEFAULT_STACK_LEVEL); + + auto sut = handler.validate_profile(profile, STATION_WIDE_ID); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::DuplicateTxDefaultProfileFound)); +} + +TEST_F(ChargepointTestFixtureV201, + K01_ValidateProfile_IfDuplicateTxDefaultProfileFoundOnChargingStation_IsInvalid_ThenProfileIsInvalid) { + install_profile_on_evse(STATION_WIDE_ID, DEFAULT_PROFILE_ID); + create_evse_with_id(DEFAULT_EVSE_ID); + + auto periods = create_charging_schedule_periods(0); + + auto profile = create_charging_profile(DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxDefaultProfile, + create_charge_schedule(ChargingRateUnitEnum::A, periods), {}, + ChargingProfileKindEnum::Relative, DEFAULT_STACK_LEVEL); + + auto sut = handler.validate_profile(profile, DEFAULT_EVSE_ID); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::DuplicateTxDefaultProfileFound)); +} + +TEST_F(ChargepointTestFixtureV201, K01_ValidateProfile_IfTxProfileIsInvalid_ThenProfileIsInvalid) { + create_evse_with_id(DEFAULT_EVSE_ID); + 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"))); + auto sut = handler.validate_profile(profile, DEFAULT_EVSE_ID); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::TxProfileMissingTransactionId)); +} + +TEST_F(ChargepointTestFixtureV201, K01_ValidateProfile_IfTxProfileIsValid_ThenProfileIsValid) { + auto periods = create_charging_schedule_periods({0, 1, 2}); + + create_evse_with_id(DEFAULT_EVSE_ID); + open_evse_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); + + 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); + auto sut = handler.validate_profile(profile, DEFAULT_EVSE_ID); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::Valid)); +} + +TEST_F(ChargepointTestFixtureV201, K01_ValidateProfile_IfTxDefaultProfileIsValid_ThenProfileIsValid) { + auto periods = create_charging_schedule_periods({0, 1, 2}); + + create_evse_with_id(DEFAULT_EVSE_ID); + + auto profile = create_charging_profile( + DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxDefaultProfile, + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00"))); + auto sut = handler.validate_profile(profile, DEFAULT_EVSE_ID); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::Valid)); +} + +TEST_F(ChargepointTestFixtureV201, K01_ValidateProfile_IfChargeStationMaxProfileIsValid_ThenProfileIsValid) { + auto periods = create_charging_schedule_periods({0, 1, 2}); + auto profile = create_charging_profile( + DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::ChargingStationMaxProfile, + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00"))); + + auto sut = handler.validate_profile(profile, STATION_WIDE_ID); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::Valid)); +} + } // namespace ocpp::v201