Skip to content

Commit

Permalink
Schedule Validation Follow-Ups (#537)
Browse files Browse the repository at this point in the history
* evse: Add get_current_phase_type()

Reports whether the EVSE is of type AC, DC,
or unknown.

Signed-off-by: Christopher Davis <[email protected]>

* 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 <[email protected]>

* smart_charging: Implement K01.FR.44 for EVSEs

Signed-off-by: Christopher Davis <[email protected]>

* smart_charging: Handle K01.FR.45 for EVSEs

We reject profiles that provide invalid data here.

Signed-off-by: Christopher Davis <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

---------

Signed-off-by: Christopher Davis <[email protected]>
  • Loading branch information
christopher-davis-afs authored Apr 22, 2024
1 parent 76e2301 commit b693a78
Show file tree
Hide file tree
Showing 7 changed files with 196 additions and 20 deletions.
6 changes: 3 additions & 3 deletions doc/ocpp_201_status.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: | |
Expand Down
11 changes: 11 additions & 0 deletions include/ocpp/v201/evse.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,12 @@
namespace ocpp {
namespace v201 {

enum class CurrentPhaseType {
AC,
DC,
Unknown,
};

class EvseInterface {
public:
virtual ~EvseInterface();
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
19 changes: 17 additions & 2 deletions include/ocpp/v201/smart_charging.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@

namespace ocpp::v201 {

const int DEFAULT_AND_MAX_NUMBER_PHASES = 3;

enum class ProfileValidationResultEnum {
Valid,
EvseDoesNotExist,
Expand All @@ -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 {
Expand Down Expand Up @@ -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<EvseInterface*> evse_opt = std::nullopt) const;

///
/// \brief Adds a given \p profile and associated \p evse_id to our stored list of profiles
Expand Down
17 changes: 17 additions & 0 deletions lib/ocpp/v201/evse.cpp
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest

#include <optional>
#include <utility>

#include <everest/logging.hpp>
Expand Down Expand Up @@ -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<int32_t>(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
92 changes: 80 additions & 12 deletions lib/ocpp/v201/smart_charging.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<int32_t, std::unique_ptr<EvseInterface>>& evses) : evses(evses) {
Expand Down Expand Up @@ -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<EvseInterface*> 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;
Expand All @@ -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);
}
}
}
}
Expand Down
5 changes: 3 additions & 2 deletions tests/lib/ocpp/v201/mocks/evse_mock.hpp
Original file line number Diff line number Diff line change
Expand Up @@ -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<IdToken>& group_id_token,
const std::optional<int32_t> reservation_id,
const MeterValue& meter_start, const std::optional<IdToken>& id_token,
const std::optional<IdToken>& group_id_token, const std::optional<int32_t> 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,
Expand All @@ -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
66 changes: 65 additions & 1 deletion tests/lib/ocpp/v201/test_smart_charging_handler.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@

#include <component_state_manager_mock.hpp>
#include <device_model_storage_mock.hpp>
#include <evse_mock.hpp>
#include <evse_security_mock.hpp>
#include <ocpp/common/call_types.hpp>
#include <ocpp/v201/enums.hpp>
Expand Down Expand Up @@ -75,9 +76,13 @@ class ChargepointTestFixtureV201 : public testing::Test {
};
}

std::vector<ChargingSchedulePeriod> create_charging_schedule_periods(int32_t start_period) {
std::vector<ChargingSchedulePeriod>
create_charging_schedule_periods(int32_t start_period, std::optional<int32_t> number_phases = std::nullopt,
std::optional<int32_t> phase_to_use = std::nullopt) {
auto charging_schedule_period = ChargingSchedulePeriod{
.startPeriod = start_period,
.numberPhases = number_phases,
.phaseToUse = phase_to_use,
};

return {charging_schedule_period};
Expand Down Expand Up @@ -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<EvseMock>();
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<EvseMock>();
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<EvseMock>();
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<EvseMock>();
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

0 comments on commit b693a78

Please sign in to comment.