From b693a782eea6f0aacce7f98b53f18d3c146a2679 Mon Sep 17 00:00:00 2001 From: christopher-davis-afs <150722105+christopher-davis-afs@users.noreply.github.com> Date: Mon, 22 Apr 2024 02:53:53 -0400 Subject: [PATCH] Schedule Validation Follow-Ups (#537) * evse: Add get_current_phase_type() Reports whether the EVSE is of type AC, DC, or unknown. Signed-off-by: Christopher Davis <150722105+christopher-davis-afs@users.noreply.github.com> * smart_charging: Implement ostream operator for ProfileValidationResultEnum Makes it a little bit easier to reason about test logs dealing with this enum. Signed-off-by: Christopher Davis <150722105+christopher-davis-afs@users.noreply.github.com> * smart_charging: Implement K01.FR.44 for EVSEs Signed-off-by: Christopher Davis <150722105+christopher-davis-afs@users.noreply.github.com> * smart_charging: Handle K01.FR.45 for EVSEs We reject profiles that provide invalid data here. Signed-off-by: Christopher Davis <150722105+christopher-davis-afs@users.noreply.github.com> * smart_charging: Handle K01.FR.49 for EVSEs `SmartChargingHandler::validate_profile_schedules()` no longer takes a constant reference to the profile, as we make modifications to the profile. Also adjusts the code to ensure we aren't copying instead of using references. Signed-off-by: Christopher Davis <150722105+christopher-davis-afs@users.noreply.github.com> * smart_charging: Fix K01.FR.35 implementation Refactoring this code when implementing FR.49 allowed a bug to be spotted. Previously we weren't checking that values were increasing after period 0. Signed-off-by: Christopher Davis <150722105+christopher-davis-afs@users.noreply.github.com> --------- Signed-off-by: Christopher Davis <150722105+christopher-davis-afs@users.noreply.github.com> --- doc/ocpp_201_status.md | 6 +- include/ocpp/v201/evse.hpp | 11 +++ include/ocpp/v201/smart_charging.hpp | 19 +++- lib/ocpp/v201/evse.cpp | 17 ++++ lib/ocpp/v201/smart_charging.cpp | 92 ++++++++++++++++--- tests/lib/ocpp/v201/mocks/evse_mock.hpp | 5 +- .../ocpp/v201/test_smart_charging_handler.cpp | 66 ++++++++++++- 7 files changed, 196 insertions(+), 20 deletions(-) diff --git a/doc/ocpp_201_status.md b/doc/ocpp_201_status.md index c40fc2c80..1e510d50c 100644 --- a/doc/ocpp_201_status.md +++ b/doc/ocpp_201_status.md @@ -1178,12 +1178,12 @@ This document contains the status of which OCPP 2.0.1 numbered requirements have | K01.FR.41 | :white_check_mark: | | | K01.FR.42 | | | | K01.FR.43 | | | -| K01.FR.44 | | | -| K01.FR.45 | | | +| K01.FR.44 | :white_check_mark: | We reject invalid profiles instead of modifying and accepting them. | +| K01.FR.45 | :white_check_mark: | We reject invalid profiles instead of modifying and accepting them. | | K01.FR.46 | | | | K01.FR.47 | | | | K01.FR.48 | | | -| K01.FR.49 | | | +| K01.FR.49 | :white_check_mark: | | | K01.FR.50 | | | | K01.FR.51 | | | | K01.FR.52 | :white_check_mark: | | diff --git a/include/ocpp/v201/evse.hpp b/include/ocpp/v201/evse.hpp index 4e668d248..f2d4548a3 100644 --- a/include/ocpp/v201/evse.hpp +++ b/include/ocpp/v201/evse.hpp @@ -18,6 +18,12 @@ namespace ocpp { namespace v201 { +enum class CurrentPhaseType { + AC, + DC, + Unknown, +}; + class EvseInterface { public: virtual ~EvseInterface(); @@ -118,6 +124,9 @@ class EvseInterface { /// \brief Restores the operative status of a connector within this EVSE to the persisted status and recomputes its /// effective status \param connector_id The ID of the connector virtual void restore_connector_operative_status(int32_t connector_id) = 0; + + /// \brief Returns the phase type for the EVSE based on its SupplyPhases. It can be AC, DC, or Unknown. + virtual CurrentPhaseType get_current_phase_type() = 0; }; /// \brief Represents an EVSE. An EVSE can contain multiple Connector objects, but can only supply energy to one of @@ -199,6 +208,8 @@ class Evse : public EvseInterface { void set_evse_operative_status(OperationalStatusEnum new_status, bool persist); void set_connector_operative_status(int32_t connector_id, OperationalStatusEnum new_status, bool persist); void restore_connector_operative_status(int32_t connector_id); + + CurrentPhaseType get_current_phase_type(); }; } // namespace v201 diff --git a/include/ocpp/v201/smart_charging.hpp b/include/ocpp/v201/smart_charging.hpp index 12fc38bd8..de4f27896 100644 --- a/include/ocpp/v201/smart_charging.hpp +++ b/include/ocpp/v201/smart_charging.hpp @@ -14,6 +14,8 @@ namespace ocpp::v201 { +const int DEFAULT_AND_MAX_NUMBER_PHASES = 3; + enum class ProfileValidationResultEnum { Valid, EvseDoesNotExist, @@ -28,9 +30,19 @@ enum class ProfileValidationResultEnum { ChargingProfileExtraneousStartSchedule, ChargingSchedulePeriodsOutOfOrder, ChargingSchedulePeriodInvalidPhaseToUse, + ChargingSchedulePeriodUnsupportedNumberPhases, + ChargingSchedulePeriodExtraneousPhaseValues, DuplicateTxDefaultProfileFound }; +namespace conversions { +/// \brief Converts the given ProfileValidationResultEnum \p e to human readable string +/// \returns a string representation of the ProfileValidationResultEnum +std::string profile_validation_result_to_string(ProfileValidationResultEnum e); +} // namespace conversions + +std::ostream& operator<<(std::ostream& os, const ProfileValidationResultEnum validation_result); + /// \brief This class handles and maintains incoming ChargingProfiles and contains the logic /// to calculate the composite schedules class SmartChargingHandler { @@ -60,8 +72,11 @@ class SmartChargingHandler { /// ProfileValidationResultEnum validate_tx_profile(const ChargingProfile& profile, EvseInterface& evse) const; - /// \brief validates that the given \p profile has valid charging schedules - ProfileValidationResultEnum validate_profile_schedules(const ChargingProfile& profile) const; + /// \brief validates that the given \p profile has valid charging schedules. + /// If a profiles charging schedule period does not have a valid numberPhases, + /// we set it to the default value (3). + ProfileValidationResultEnum validate_profile_schedules(ChargingProfile& profile, + std::optional evse_opt = std::nullopt) const; /// /// \brief Adds a given \p profile and associated \p evse_id to our stored list of profiles diff --git a/lib/ocpp/v201/evse.cpp b/lib/ocpp/v201/evse.cpp index d677710c3..5d6c12581 100644 --- a/lib/ocpp/v201/evse.cpp +++ b/lib/ocpp/v201/evse.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest +#include #include #include @@ -309,5 +310,21 @@ Connector* Evse::get_connector(int32_t connector_id) { return this->id_connector_map.at(connector_id).get(); } +CurrentPhaseType Evse::get_current_phase_type() { + ComponentVariable evse_variable = + EvseComponentVariables::get_component_variable(this->evse_id, EvseComponentVariables::SupplyPhases); + auto supply_phases = this->device_model.get_optional_value(evse_variable); + if (supply_phases == std::nullopt) { + return CurrentPhaseType::Unknown; + } else if (*supply_phases == 1 || *supply_phases == 3) { + return CurrentPhaseType::AC; + } else if (*supply_phases == 0) { + return CurrentPhaseType::DC; + } + + // NOTE: SupplyPhases should never be a value that isn't NULL, 1, 3, or 0. + return CurrentPhaseType::Unknown; +} + } // namespace v201 } // namespace ocpp diff --git a/lib/ocpp/v201/smart_charging.cpp b/lib/ocpp/v201/smart_charging.cpp index 0a1c831e2..ca3e14c0c 100644 --- a/lib/ocpp/v201/smart_charging.cpp +++ b/lib/ocpp/v201/smart_charging.cpp @@ -15,6 +15,52 @@ using namespace std::chrono; namespace ocpp::v201 { +namespace conversions { +std::string profile_validation_result_to_string(ProfileValidationResultEnum e) { + switch (e) { + case ProfileValidationResultEnum::Valid: + return "Valid"; + case ProfileValidationResultEnum::EvseDoesNotExist: + return "EvseDoesNotExist"; + case ProfileValidationResultEnum::TxProfileMissingTransactionId: + return "TxProfileMissingTransactionId"; + case ProfileValidationResultEnum::TxProfileEvseIdNotGreaterThanZero: + return "TxProfileEvseIdNotGreaterThanZero"; + case ProfileValidationResultEnum::TxProfileTransactionNotOnEvse: + return "TxProfileTransactionNotOnEvse"; + case ProfileValidationResultEnum::TxProfileEvseHasNoActiveTransaction: + return "TxProfileEvseHasNoActiveTransaction"; + case ProfileValidationResultEnum::TxProfileConflictingStackLevel: + return "TxProfileConflictingStackLevel"; + case ProfileValidationResultEnum::ChargingProfileNoChargingSchedulePeriods: + return "ChargingProfileNoChargingSchedulePeriods"; + case ProfileValidationResultEnum::ChargingProfileFirstStartScheduleIsNotZero: + return "ChargingProfileFirstStartScheduleIsNotZero"; + case ProfileValidationResultEnum::ChargingProfileMissingRequiredStartSchedule: + return "ChargingProfileMissingRequiredStartSchedule"; + case ProfileValidationResultEnum::ChargingProfileExtraneousStartSchedule: + return "ChargingProfileExtraneousStartSchedule"; + case ProfileValidationResultEnum::ChargingSchedulePeriodsOutOfOrder: + return "ChargingSchedulePeriodsOutOfOrder"; + case ProfileValidationResultEnum::ChargingSchedulePeriodInvalidPhaseToUse: + return "ChargingSchedulePeriodInvalidPhaseToUse"; + case ProfileValidationResultEnum::ChargingSchedulePeriodUnsupportedNumberPhases: + return "ChargingSchedulePeriodUnsupportedNumberPhases"; + case ProfileValidationResultEnum::ChargingSchedulePeriodExtraneousPhaseValues: + return "ChargingSchedulePeriodExtraneousPhaseValues"; + case ProfileValidationResultEnum::DuplicateTxDefaultProfileFound: + return "DuplicateTxDefaultProfileFound"; + } + + throw std::out_of_range("No known string conversion for provided enum of type ProfileValidationResultEnum"); +} +} // namespace conversions + +std::ostream& operator<<(std::ostream& os, const ProfileValidationResultEnum validation_result) { + os << conversions::profile_validation_result_to_string(validation_result); + return os; +} + const int32_t STATION_WIDE_ID = 0; SmartChargingHandler::SmartChargingHandler(std::map>& evses) : evses(evses) { @@ -77,21 +123,19 @@ ProfileValidationResultEnum SmartChargingHandler::validate_tx_profile(const Char * - 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) { +ProfileValidationResultEnum +SmartChargingHandler::validate_profile_schedules(ChargingProfile& profile, + std::optional evse_opt) const { + for (ChargingSchedule& schedule : profile.chargingSchedule) { // 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++) { + auto& charging_schedule_period = schedule.chargingSchedulePeriod[i]; // K01.FR.19 if (charging_schedule_period.numberPhases != 1 && charging_schedule_period.phaseToUse.has_value()) { return ProfileValidationResultEnum::ChargingSchedulePeriodInvalidPhaseToUse; @@ -100,13 +144,37 @@ ProfileValidationResultEnum SmartChargingHandler::validate_profile_schedules(con // 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]; + } + + // K01.FR.35 + if (i + 1 < schedule.chargingSchedulePeriod.size()) { + auto next_charging_schedule_period = schedule.chargingSchedulePeriod[i + 1]; if (next_charging_schedule_period.startPeriod <= charging_schedule_period.startPeriod) { return ProfileValidationResultEnum::ChargingSchedulePeriodsOutOfOrder; - } else { - charging_schedule_period = next_charging_schedule_period; + } + } + + 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; + } + + 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); + } } } } diff --git a/tests/lib/ocpp/v201/mocks/evse_mock.hpp b/tests/lib/ocpp/v201/mocks/evse_mock.hpp index 2ed636283..973f373d4 100644 --- a/tests/lib/ocpp/v201/mocks/evse_mock.hpp +++ b/tests/lib/ocpp/v201/mocks/evse_mock.hpp @@ -9,8 +9,8 @@ class EvseMock : public EvseInterface { MOCK_METHOD(uint32_t, get_number_of_connectors, ()); MOCK_METHOD(void, open_transaction, (const std::string& transaction_id, const int32_t connector_id, const DateTime& timestamp, - const MeterValue& meter_start, const IdToken& id_token, const std::optional& group_id_token, - const std::optional reservation_id, + const MeterValue& meter_start, const std::optional& id_token, + const std::optional& group_id_token, const std::optional reservation_id, const std::chrono::seconds sampled_data_tx_updated_interval, const std::chrono::seconds sampled_data_tx_ended_interval, const std::chrono::seconds aligned_data_tx_updated_interval, @@ -33,5 +33,6 @@ class EvseMock : public EvseInterface { MOCK_METHOD(void, set_connector_operative_status, (int32_t connector_id, OperationalStatusEnum new_status, bool persist)); MOCK_METHOD(void, restore_connector_operative_status, (int32_t connector_id)); + MOCK_METHOD(CurrentPhaseType, get_current_phase_type, ()); }; } // 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 67f093adb..088ccfe67 100644 --- a/tests/lib/ocpp/v201/test_smart_charging_handler.cpp +++ b/tests/lib/ocpp/v201/test_smart_charging_handler.cpp @@ -11,6 +11,7 @@ #include #include +#include #include #include #include @@ -75,9 +76,13 @@ class ChargepointTestFixtureV201 : public testing::Test { }; } - std::vector create_charging_schedule_periods(int32_t start_period) { + std::vector + create_charging_schedule_periods(int32_t start_period, std::optional number_phases = std::nullopt, + std::optional phase_to_use = std::nullopt) { auto charging_schedule_period = ChargingSchedulePeriod{ .startPeriod = start_period, + .numberPhases = number_phases, + .phaseToUse = phase_to_use, }; return {charging_schedule_period}; @@ -481,4 +486,63 @@ 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)); + + 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()); + + auto sut = handler.validate_profile_schedules(profile, &mock_evse); + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::ChargingSchedulePeriodExtraneousPhaseValues)); +} + +TEST_F(ChargepointTestFixtureV201, K01FR44_IfPhaseToUseProvidedForDCEVSE_ThenProfileIsInvalid) { + auto mock_evse = testing::NiceMock(); + ON_CALL(mock_evse, get_current_phase_type).WillByDefault(testing::Return(CurrentPhaseType::DC)); + + 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 sut = handler.validate_profile_schedules(profile, &mock_evse); + + 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)); + + 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 sut = handler.validate_profile_schedules(profile, &mock_evse); + + 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)); + + 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 sut = handler.validate_profile_schedules(profile, &mock_evse); + + auto numberPhases = profile.chargingSchedule[0].chargingSchedulePeriod[0].numberPhases; + + EXPECT_THAT(sut, testing::Eq(ProfileValidationResultEnum::Valid)); + EXPECT_THAT(numberPhases, testing::Eq(3)); +} + } // namespace ocpp::v201