diff --git a/doc/ocpp_201_status.md b/doc/ocpp_201_status.md index 43e4afb7e..f3c3c8980 100644 --- a/doc/ocpp_201_status.md +++ b/doc/ocpp_201_status.md @@ -1266,7 +1266,7 @@ This document contains the status of which OCPP 2.0.1 numbered functional requir | K01.FR.40 | ✅ | `Absolute`/`Recurring` profiles without `startSchedule` fields are rejected. | | K01.FR.41 | ✅ | `Relative` profiles with `startSchedule` fields are rejected. | | K01.FR.42 | ⛽️ | | -| K01.FR.43 | | Open question to OCA - https://oca.causewaynow.com/wg/OCA-TWG/mail/thread/4254 | +| K01.FR.43 | | Open question to OCA - | | K01.FR.44 | ✅ | We reject invalid profiles instead of modifying and accepting them. | | K01.FR.45 | ✅ | We reject invalid profiles instead of modifying and accepting them. | | K01.FR.46 | ⛽️ | K08 | @@ -1381,7 +1381,7 @@ This document contains the status of which OCPP 2.0.1 numbered functional requir | K11.FR.01 | | | | K11.FR.02 | | | | K11.FR.03 | | | -| K11.FR.04 | | | +| K11.FR.04 | ✅ | | | K11.FR.05 | | | | K11.FR.06 | | | @@ -1390,9 +1390,9 @@ This document contains the status of which OCPP 2.0.1 numbered functional requir | ID | Status | Remark | |-----------|--------|--------| | K12.FR.01 | | | -| K12.FR.02 | | | -| K12.FR.03 | | | -| K12.FR.04 | | | +| K12.FR.02 | ✅ | | +| K12.FR.03 | ✅ | | +| K12.FR.04 | ✅ | The spec does not define what we should do when the source given is CSO. The system currently throws and exception. | | K12.FR.05 | | | ## SmartCharging - Reset / Release External Charging Limit @@ -1400,8 +1400,8 @@ This document contains the status of which OCPP 2.0.1 numbered functional requir | ID | Status | Remark | |-----------|--------|--------| | K13.FR.01 | | | -| K13.FR.02 | | | -| K13.FR.03 | | | +| K13.FR.02 | ✅ | | +| K13.FR.03 | ✅ | | ## SmartCharging - External Charging Limit with Local Controller diff --git a/include/ocpp/v201/charge_point.hpp b/include/ocpp/v201/charge_point.hpp index f2893498e..f76bdf4c2 100644 --- a/include/ocpp/v201/charge_point.hpp +++ b/include/ocpp/v201/charge_point.hpp @@ -249,6 +249,25 @@ class ChargePointInterface { /// virtual void on_variable_changed(const SetVariableData& set_variable_data) = 0; + /// \brief Notifies the ChargePoint that a new external limit has been set. This may send a + /// NotifyChargingLimitRequest if the \p percentage_delta is greater than our LimitChangeSignificance. + /// \param evse_id ID of the EVSE with the external limit + /// \param limit the new external limit + /// \param percentage_delta the percent changed from the existing limits + /// \param source the source of the external limit (NOTE: Should never be CSO) + virtual void on_external_limits_changed(const std::variant& limit, + double percentage_delta, ChargingLimitSourceEnum source, + std::optional evse_id) = 0; + + /// \brief Notifies the ChargePoint that an external limit has been cleared. + /// This shall send a ClearedChargingLimitRequest when called. + // This may send TransactionEventRequest if the \p percentage_delta is greater than our LimitChangeSignificance. + /// \param percentage_delta the percent changed from the existing limits + /// \param source the source of the external limit + /// \param evse_id if provided checks for transactions on the provided evse + virtual void on_external_limit_cleared(double percentage_delta, ChargingLimitSourceEnum source, + std::optional evse_id) = 0; + /// \brief Data transfer mechanism initiated by charger /// \param vendorId /// \param messageId @@ -819,6 +838,13 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa void on_variable_changed(const SetVariableData& set_variable_data) override; + void on_external_limits_changed(const std::variant& limit, + double percentage_delta, ChargingLimitSourceEnum source, + std::optional evse_id) override; + + void on_external_limit_cleared(double percentage_delta, ChargingLimitSourceEnum source, + std::optional evse_id) override; + std::optional data_transfer_req(const CiString<255>& vendorId, const std::optional>& messageId, const std::optional& data) override; diff --git a/include/ocpp/v201/smart_charging.hpp b/include/ocpp/v201/smart_charging.hpp index 2c1d9aa87..001d1c47d 100644 --- a/include/ocpp/v201/smart_charging.hpp +++ b/include/ocpp/v201/smart_charging.hpp @@ -4,14 +4,21 @@ #ifndef OCPP_V201_SMART_CHARGING_HPP #define OCPP_V201_SMART_CHARGING_HPP +#include "ocpp/v201/comparators.hpp" +#include "ocpp/v201/messages/ClearedChargingLimit.hpp" +#include "ocpp/v201/messages/TransactionEvent.hpp" #include #include +#include +#include +#include #include #include #include #include #include +#include #include #include #include @@ -56,6 +63,13 @@ enum class AddChargingProfileSource { RequestStartTransactionRequest }; +struct ConstantChargingLimit { + float limit; + ChargingRateUnitEnum charging_rate_unit; +}; + +bool operator==(const ConstantChargingLimit& a, const ConstantChargingLimit& b); + namespace conversions { /// \brief Converts the given ProfileValidationResultEnum \p e to human readable string /// \returns a string representation of the ProfileValidationResultEnum @@ -97,6 +111,15 @@ class SmartChargingHandlerInterface { const ocpp::DateTime& start_time, const ocpp::DateTime& end_time, const int32_t evse_id, std::optional charging_rate_unit) = 0; + + virtual std::optional>> + handle_external_limits_changed(const std::variant& limit, + double percentage_delta, ChargingLimitSourceEnum source, + std::optional evse_id) const = 0; + + virtual std::optional>> + handle_external_limit_cleared(double percentage_delta, ChargingLimitSourceEnum source, + std::optional evse_id) const = 0; }; /// \brief This class handles and maintains incoming ChargingProfiles and contains the logic @@ -165,6 +188,23 @@ class SmartChargingHandler : public SmartChargingHandlerInterface { const int32_t evse_id, std::optional charging_rate_unit) override; + /// + /// \brief Determines whether or not we should notify the CSMS of a changed external limit + /// based on \p percentage_delta and builds the notification. + /// + std::optional>> + handle_external_limits_changed(const std::variant& limit, + double percentage_delta, ChargingLimitSourceEnum source, + std::optional evse_id) const override; + + /// + /// \brief Determines whether or not we should notify the CSMS of a cleared external limit + /// based on \p percentage_delta and builds the notification. + /// + std::optional>> + handle_external_limit_cleared(double percentage_delta, ChargingLimitSourceEnum source, + std::optional evse_id) const override; + protected: /// /// \brief validates the existence of the given \p evse_id according to the specification @@ -218,6 +258,10 @@ class SmartChargingHandler : public SmartChargingHandlerInterface { std::vector get_valid_profiles_for_evse(int32_t evse_id); void conform_validity_periods(ChargingProfile& profile) const; CurrentPhaseType get_current_phase_type(const std::optional evse_opt) const; + TransactionEventRequest create_transaction_event_request(std::unique_ptr& tx) const; + bool process_evses_with_active_transactions(const bool limit_change_significance_exceeded, + std::vector& transaction_event_requests, + std::optional evse_id) const; }; } // namespace ocpp::v201 diff --git a/lib/ocpp/v201/charge_point.cpp b/lib/ocpp/v201/charge_point.cpp index 2b8a12d1a..f451aaba8 100644 --- a/lib/ocpp/v201/charge_point.cpp +++ b/lib/ocpp/v201/charge_point.cpp @@ -1,6 +1,7 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Pionix GmbH and Contributors to EVerest +#include #include #include #include @@ -1013,6 +1014,50 @@ void ChargePoint::on_variable_changed(const SetVariableData& set_variable_data) this->handle_variable_changed(set_variable_data); } +void ChargePoint::on_external_limits_changed(const std::variant& limit, + double percentage_delta, ChargingLimitSourceEnum source, + std::optional evse_id) { + auto request = + this->smart_charging_handler->handle_external_limits_changed(limit, percentage_delta, source, evse_id); + if (request.has_value()) { + auto [cleared_charging_limit_request, transaction_event_requests] = request.value(); + + ocpp::Call call(cleared_charging_limit_request, + this->message_queue->createMessageId()); + this->send(call); + + if (transaction_event_requests.size() > 0) { + for (auto transaction_event_request : transaction_event_requests) { + ocpp::Call call(transaction_event_request, + this->message_queue->createMessageId()); + this->send(call); + } + } + } +} + +void ChargePoint::on_external_limit_cleared(double percentage_delta, ChargingLimitSourceEnum source, + std::optional evse_id) { + auto request = this->smart_charging_handler->handle_external_limit_cleared(percentage_delta, source, evse_id); + + if (request.has_value()) { + + auto [cleared_charging_limit_request, transaction_event_requests] = request.value(); + + ocpp::Call call(cleared_charging_limit_request, + this->message_queue->createMessageId()); + this->send(call); + + if (transaction_event_requests.size() > 0) { + for (auto transaction_event_request : transaction_event_requests) { + ocpp::Call call(transaction_event_request, + this->message_queue->createMessageId()); + this->send(call); + } + } + } +} + bool ChargePoint::send(CallError call_error) { this->message_queue->push(call_error); return true; diff --git a/lib/ocpp/v201/smart_charging.cpp b/lib/ocpp/v201/smart_charging.cpp index 8dddb1f05..7ddd49414 100644 --- a/lib/ocpp/v201/smart_charging.cpp +++ b/lib/ocpp/v201/smart_charging.cpp @@ -9,10 +9,14 @@ #include "ocpp/v201/ctrlr_component_variables.hpp" #include "ocpp/v201/device_model.hpp" #include "ocpp/v201/evse.hpp" +#include "ocpp/v201/messages/ClearedChargingLimit.hpp" +#include "ocpp/v201/messages/NotifyChargingLimit.hpp" #include "ocpp/v201/messages/SetChargingProfile.hpp" +#include "ocpp/v201/messages/TransactionEvent.hpp" #include "ocpp/v201/ocpp_enums.hpp" #include "ocpp/v201/ocpp_types.hpp" #include "ocpp/v201/profile.hpp" +#include "ocpp/v201/transaction.hpp" #include "ocpp/v201/utils.hpp" #include #include @@ -20,11 +24,18 @@ #include #include #include +#include +#include using namespace std::chrono; namespace ocpp::v201 { +bool operator==(const ConstantChargingLimit& a, const ConstantChargingLimit& b) { + return std::fabs(a.limit - b.limit) <= std::numeric_limits::epsilon() && + a.charging_rate_unit == b.charging_rate_unit; +} + namespace conversions { std::string profile_validation_result_to_string(ProfileValidationResultEnum e) { switch (e) { @@ -614,4 +625,139 @@ CompositeSchedule SmartChargingHandler::calculate_composite_schedule( return composite_schedule; } +ChargingSchedule create_schedule_from_limit(const ConstantChargingLimit limit) { + return ChargingSchedule{ + .id = 0, + .chargingRateUnit = limit.charging_rate_unit, + .chargingSchedulePeriod = {ChargingSchedulePeriod{ + .startPeriod = 0, + .limit = limit.limit, + }}, + }; +} + +std::optional>> +SmartChargingHandler::handle_external_limits_changed(const std::variant& limit, + double percentage_delta, ChargingLimitSourceEnum source, + std::optional evse_id) const { + // K12.FR.04 + if (source == ChargingLimitSourceEnum::CSO) { + // The spec does not define what we should do when the source + // given is CSO. Here we just throw. + throw std::invalid_argument("The source of an external limit should not be CSO."); + } + + NotifyChargingLimitRequest notify_charging_limit_request = {}; + std::vector transaction_event_requests = {}; + + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + const float limit_change_significance = this->device_model->get_value(limit_change_cv); + + const auto& notify_charging_limit_cv = ControllerComponentVariables::NotifyChargingLimitWithSchedules; + const std::optional notify_with_schedules = + this->device_model->get_optional_value(notify_charging_limit_cv); + + std::optional>> request = {}; + std::pair> pair; + + const bool limit_change_significance_exceeded = percentage_delta > limit_change_significance; + + if (limit_change_significance_exceeded) { + notify_charging_limit_request = NotifyChargingLimitRequest{}; + notify_charging_limit_request.evseId = evse_id; + notify_charging_limit_request.chargingLimit = {.chargingLimitSource = source}; + if (notify_with_schedules.has_value() && notify_with_schedules.value()) { + if (const auto* limit_c = std::get_if(&limit)) { + notify_charging_limit_request.chargingSchedule = {{create_schedule_from_limit(*limit_c)}}; + } else if (const auto* limit_s = std::get_if(&limit)) { + notify_charging_limit_request.chargingSchedule = {{*limit_s}}; + } + } + pair.first = notify_charging_limit_request; + + // K11.FR.04 + this->process_evses_with_active_transactions(limit_change_significance_exceeded, transaction_event_requests, + evse_id); + + pair.second = transaction_event_requests; + request.emplace(pair); + } + return request; +} + +std::optional>> +SmartChargingHandler::handle_external_limit_cleared(double percentage_delta, ChargingLimitSourceEnum source, + std::optional evse_id) const { + + std::pair> pair; + std::vector transaction_event_requests = {}; + + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + const float limit_change_significance = this->device_model->get_value(limit_change_cv); + + const bool limit_change_significance_exceeded = percentage_delta > limit_change_significance; + + bool has_transaction = this->process_evses_with_active_transactions(limit_change_significance_exceeded, + transaction_event_requests, evse_id); + + std::optional>> request; + + if (has_transaction) { + // K13.FR.02 + ClearedChargingLimitRequest cleared_charging_limit_request = {}; + + if (evse_id.has_value()) { + cleared_charging_limit_request.evseId = evse_id.value(); + } + + // There is not restriction on this source in the spec. + // K12.FR04 requires it not to be CSO. Not enforced here. + cleared_charging_limit_request.chargingLimitSource = source; + pair.first = cleared_charging_limit_request; + + pair.second = transaction_event_requests; + request.emplace(pair); + } + + return request; +} + +TransactionEventRequest +SmartChargingHandler::create_transaction_event_request(std::unique_ptr& tx) const { + auto tmp = TransactionEventRequest{}; + tmp.eventType = TransactionEventEnum::Updated; + tmp.timestamp = ocpp::DateTime(); + tmp.triggerReason = TriggerReasonEnum::ChargingRateChanged; + + tmp.seqNo = tx->get_seq_no(); + tmp.transactionInfo = tx->get_transaction(); + + return tmp; +} + +bool SmartChargingHandler::process_evses_with_active_transactions( + const bool limit_change_significance_exceeded, std::vector& transaction_event_requests, + std::optional evse_id) const { + bool has_transaction = false; + if (evse_id.has_value()) { + auto evse = &this->evse_manager.get_evse(evse_id.value()); + if (evse->has_active_transaction()) { + has_transaction = true; + // K13.FR.03 + if (limit_change_significance_exceeded) { + auto& tx = evse->get_transaction(); + transaction_event_requests.push_back(this->create_transaction_event_request(tx)); + } + } + } else { + for (auto& evse : this->evse_manager) { + has_transaction = (process_evses_with_active_transactions(limit_change_significance_exceeded, + transaction_event_requests, evse.get_id()) || + has_transaction); + } + } + + return has_transaction; +} + } // namespace ocpp::v201 diff --git a/tests/lib/ocpp/v201/mocks/smart_charging_handler_mock.hpp b/tests/lib/ocpp/v201/mocks/smart_charging_handler_mock.hpp index 544d40777..b33cf1be6 100644 --- a/tests/lib/ocpp/v201/mocks/smart_charging_handler_mock.hpp +++ b/tests/lib/ocpp/v201/mocks/smart_charging_handler_mock.hpp @@ -3,12 +3,16 @@ #include "gmock/gmock.h" #include +#include #include +#include "ocpp/v201/messages/ClearedChargingLimit.hpp" #include "ocpp/v201/messages/SetChargingProfile.hpp" #include "ocpp/v201/ocpp_enums.hpp" #include "ocpp/v201/smart_charging.hpp" +typedef std::variant ChargingLimitVariant; + namespace ocpp::v201 { class SmartChargingHandlerMock : public SmartChargingHandlerInterface { public: @@ -28,5 +32,14 @@ class SmartChargingHandlerMock : public SmartChargingHandlerInterface { (std::vector & valid_profiles, const ocpp::DateTime& start_time, const ocpp::DateTime& end_time, const int32_t evse_id, std::optional charging_rate_unit)); + MOCK_METHOD((std::optional>>), + handle_external_limits_changed, + (const ChargingLimitVariant& limit, double percentage_delta, ChargingLimitSourceEnum source, + std::optional evse_id), + (const, override)); + MOCK_METHOD((std::optional>>), + handle_external_limit_cleared, + (double percentage_delta, ChargingLimitSourceEnum source, std::optional evse_id), + (const, override)); }; } // namespace ocpp::v201 diff --git a/tests/lib/ocpp/v201/test_charge_point.cpp b/tests/lib/ocpp/v201/test_charge_point.cpp index 1ce4c4851..018bf56d6 100644 --- a/tests/lib/ocpp/v201/test_charge_point.cpp +++ b/tests/lib/ocpp/v201/test_charge_point.cpp @@ -17,12 +17,16 @@ #include "ocpp/v201/smart_charging.hpp" #include "ocpp/v201/types.hpp" #include "smart_charging_handler_mock.hpp" +#include "smart_charging_test_utils.hpp" #include "gmock/gmock.h" #include #include +#include #include #include #include +#include +#include static const int DEFAULT_EVSE_ID = 1; static const int DEFAULT_PROFILE_ID = 1; @@ -941,4 +945,44 @@ TEST_F(ChargePointFunctionalityTestFixtureV201, K02FR05_TransactionEnds_WillDele TriggerReasonEnum::StopAuthorized, {}, {}, ChargingStateEnum::EVConnected); } +TEST_F(ChargePointFunctionalityTestFixtureV201, K12_OnExternalLimitsChanged_CallsHandler) { + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + device_model->set_value(limit_change_cv.component, limit_change_cv.variable.value(), AttributeEnum::Actual, "0.1", + "test"); + + ConstantChargingLimit limit = { + .limit = 100.0, + .charging_rate_unit = ChargingRateUnitEnum::A, + }; + double deltaChanged = 0.2; + auto source = ChargingLimitSourceEnum::Other; + + const std::variant new_limit(limit); + + const std::optional evse_id(DEFAULT_EVSE_ID); + + EXPECT_CALL(*smart_charging_handler, handle_external_limits_changed(new_limit, deltaChanged, source, evse_id)); + + charge_point->on_external_limits_changed(new_limit, deltaChanged, source, DEFAULT_EVSE_ID); +} + +TEST_F(ChargePointFunctionalityTestFixtureV201, K13_OnExternalLimitsCleared_CallsHandler) { + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + device_model->set_value(limit_change_cv.component, limit_change_cv.variable.value(), AttributeEnum::Actual, "0.1", + "test"); + + ConstantChargingLimit limit = { + .limit = 100.0, + .charging_rate_unit = ChargingRateUnitEnum::A, + }; + + std::optional evse_id; + double deltaChanged = 0.2; + auto source = ChargingLimitSourceEnum::Other; + + EXPECT_CALL(*smart_charging_handler, handle_external_limit_cleared(deltaChanged, source, evse_id)); + + charge_point->on_external_limit_cleared(deltaChanged, source, std::nullopt); +} + } // 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 4615edc96..afd04cf86 100644 --- a/tests/lib/ocpp/v201/test_smart_charging_handler.cpp +++ b/tests/lib/ocpp/v201/test_smart_charging_handler.cpp @@ -10,6 +10,8 @@ #include "ocpp/v201/device_model_storage_sqlite.hpp" #include "ocpp/v201/init_device_model_db.hpp" #include "ocpp/v201/ocpp_types.hpp" +#include "gmock/gmock.h" +#include "gtest/gtest.h" #include #include #include @@ -31,8 +33,11 @@ #include "comparators.hpp" #include +#include #include +#include "smart_charging_test_utils.hpp" + namespace ocpp::v201 { static const int NR_OF_EVSES = 2; @@ -1678,4 +1683,451 @@ TEST_F(SmartChargingHandlerTestFixtureV201, K05FR02_RequestStartTransactionReque ASSERT_THAT(sut, testing::Eq(ProfileValidationResultEnum::RequestStartTransactionNonTxProfile)); } +TEST_F(SmartChargingHandlerTestFixtureV201, K11FR04_HandleChangedChargingLimitRequest_NoTransactionExists) { + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + device_model->set_value(limit_change_cv.component, limit_change_cv.variable.value(), AttributeEnum::Actual, "0.1", + "test"); + + ConstantChargingLimit new_limit = { + .limit = 100.0, + .charging_rate_unit = ChargingRateUnitEnum::A, + }; + + double deltaChanged = 0.2; + auto source = ChargingLimitSourceEnum::Other; + + auto resp = handler.handle_external_limits_changed(new_limit, deltaChanged, source, std::nullopt); + ASSERT_THAT(resp.has_value(), testing::IsTrue()); + + auto [notify_charging_limit_request, transaction_event_requests] = resp.value(); + ASSERT_THAT(transaction_event_requests.size(), testing::Eq(0)); +} + +TEST_F(SmartChargingHandlerTestFixtureV201, K11FR04_HandleChangedChargingLimitRequest_OneTransactionExists) { + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + device_model->set_value(limit_change_cv.component, limit_change_cv.variable.value(), AttributeEnum::Actual, "0.1", + "test"); + + std::string transaction_id = uuid(); + this->evse_manager->open_transaction(DEFAULT_EVSE_ID, transaction_id); + + ConstantChargingLimit new_limit = { + .limit = 100.0, + .charging_rate_unit = ChargingRateUnitEnum::A, + }; + + double deltaChanged = 0.2; + auto source = ChargingLimitSourceEnum::Other; + + auto resp = handler.handle_external_limits_changed(new_limit, deltaChanged, source, std::nullopt); + ASSERT_THAT(resp.has_value(), testing::IsTrue()); + + auto [notify_charging_limit_request, transaction_event_requests] = resp.value(); + ASSERT_THAT(transaction_event_requests.size(), testing::Eq(1)); +} + +TEST_F(SmartChargingHandlerTestFixtureV201, + K12FR02_HandleExternalLimitsChanged_LimitChangeSignificanceNotMet_ReturnNone) { + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + device_model->set_value(limit_change_cv.component, limit_change_cv.variable.value(), AttributeEnum::Actual, "0.5", + "test"); + + ConstantChargingLimit new_limit = { + .limit = 100.0, + .charging_rate_unit = ChargingRateUnitEnum::A, + }; + double deltaChanged = 0.2; + auto source = ChargingLimitSourceEnum::Other; + + auto resp = handler.handle_external_limits_changed(new_limit, deltaChanged, source, DEFAULT_EVSE_ID); + + ASSERT_THAT(resp.has_value(), testing::IsFalse()); +} + +TEST_F(SmartChargingHandlerTestFixtureV201, + K12FR02_HandleExternalLimitsChanged_LimitChangeSignificanceEqual_ReturnNone) { + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + device_model->set_value(limit_change_cv.component, limit_change_cv.variable.value(), AttributeEnum::Actual, "0.2", + "test"); + + ConstantChargingLimit new_limit = { + .limit = 100.0, + .charging_rate_unit = ChargingRateUnitEnum::A, + }; + double deltaChanged = 0.2; + auto source = ChargingLimitSourceEnum::Other; + + auto resp = handler.handle_external_limits_changed(new_limit, deltaChanged, source, DEFAULT_EVSE_ID); + + ASSERT_THAT(resp.has_value(), testing::IsFalse()); +} + +TEST_F(SmartChargingHandlerTestFixtureV201, + K12FR02_HandleExternalLimitsChanged_LimitChangeSignificanceExceeded_ReturnNotifyChargingLimitRequest) { + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + device_model->set_value(limit_change_cv.component, limit_change_cv.variable.value(), AttributeEnum::Actual, "0.1", + "test"); + + ConstantChargingLimit new_limit = { + .limit = 100.0, + .charging_rate_unit = ChargingRateUnitEnum::A, + }; + double deltaChanged = 0.2; + auto source = ChargingLimitSourceEnum::Other; + + auto resp = handler.handle_external_limits_changed(new_limit, deltaChanged, source, DEFAULT_EVSE_ID); + + ASSERT_THAT(resp.has_value(), testing::IsTrue()); +} + +TEST_F( + SmartChargingHandlerTestFixtureV201, + K12FR03_HandleExternalLimitsChanged_LimitChangeSignificanceExceeded_EnableChargingLimitWithSchedulesTrue_IncludesScheduleFromConstantLimit) { + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + device_model->set_value(limit_change_cv.component, limit_change_cv.variable.value(), AttributeEnum::Actual, "0.1", + "test"); + const auto& notify_charging_limit_cv = ControllerComponentVariables::NotifyChargingLimitWithSchedules; + device_model->set_value(notify_charging_limit_cv.component, notify_charging_limit_cv.variable.value(), + AttributeEnum::Actual, "true", "test"); + + ConstantChargingLimit new_limit = { + .limit = 50.0, + .charging_rate_unit = ChargingRateUnitEnum::W, + }; + double deltaChanged = 0.2; + auto source = ChargingLimitSourceEnum::Other; + + auto charging_schedule = ChargingSchedule{.id = 0, + .chargingRateUnit = new_limit.charging_rate_unit, + .chargingSchedulePeriod = {ChargingSchedulePeriod{ + .startPeriod = 0, + .limit = new_limit.limit, + }}}; + + auto resp = handler.handle_external_limits_changed(new_limit, deltaChanged, source, DEFAULT_EVSE_ID); + + ASSERT_THAT(resp.has_value(), testing::IsTrue()); + auto [notify_charging_limit_request, s] = resp.value(); + + ASSERT_THAT(notify_charging_limit_request.chargingSchedule.has_value(), testing::IsTrue()); + ASSERT_THAT(notify_charging_limit_request.chargingSchedule.value(), testing::Contains(charging_schedule)); +} + +TEST_F( + SmartChargingHandlerTestFixtureV201, + K12FR03_HandleExternalLimitsChanged_LimitChangeSignificanceExceeded_EnableChargingLimitWithSchedulesTrue_IncludesGivenSchedule) { + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + device_model->set_value(limit_change_cv.component, limit_change_cv.variable.value(), AttributeEnum::Actual, "0.1", + "test"); + const auto& notify_charging_limit_cv = ControllerComponentVariables::NotifyChargingLimitWithSchedules; + device_model->set_value(notify_charging_limit_cv.component, notify_charging_limit_cv.variable.value(), + AttributeEnum::Actual, "true", "test"); + + double deltaChanged = 0.2; + auto source = ChargingLimitSourceEnum::Other; + + auto charging_schedule = ChargingSchedule{.id = 0, + .chargingRateUnit = ChargingRateUnitEnum::W, + .chargingSchedulePeriod = {ChargingSchedulePeriod{ + .startPeriod = 0, + .limit = 20, + }}}; + + auto resp = handler.handle_external_limits_changed(charging_schedule, deltaChanged, source, DEFAULT_EVSE_ID); + + ASSERT_THAT(resp.has_value(), testing::IsTrue()); + + auto [notify_charging_limit_request, s] = resp.value(); + + ASSERT_THAT(notify_charging_limit_request.chargingSchedule.has_value(), testing::IsTrue()); + ASSERT_THAT(notify_charging_limit_request.chargingSchedule.value(), testing::Contains(charging_schedule)); +} + +TEST_F( + SmartChargingHandlerTestFixtureV201, + K12FR03_HandleExternalLimitsChanged_LimitChangeSignificanceExceeded_EnableChargingLimitWithSchedulesUnset_NoSchedule) { + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + device_model->set_value(limit_change_cv.component, limit_change_cv.variable.value(), AttributeEnum::Actual, "0.1", + "test"); + + ConstantChargingLimit new_limit = { + .limit = 100.0, + .charging_rate_unit = ChargingRateUnitEnum::A, + }; + double deltaChanged = 0.2; + auto source = ChargingLimitSourceEnum::Other; + + auto resp = handler.handle_external_limits_changed(new_limit, deltaChanged, source, DEFAULT_EVSE_ID); + + ASSERT_THAT(resp.has_value(), testing::IsTrue()); + auto [notify_charging_limit_request, s] = resp.value(); + + ASSERT_THAT(notify_charging_limit_request.chargingSchedule.has_value(), testing::IsFalse()); +} + +TEST_F( + SmartChargingHandlerTestFixtureV201, + K12FR03_HandleExternalLimitsChanged_LimitChangeSignificanceExceeded_EnableChargingLimitWithSchedulesFalse_NoSchedule) { + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + device_model->set_value(limit_change_cv.component, limit_change_cv.variable.value(), AttributeEnum::Actual, "0.1", + "test"); + const auto& notify_charging_limit_cv = ControllerComponentVariables::NotifyChargingLimitWithSchedules; + device_model->set_value(notify_charging_limit_cv.component, notify_charging_limit_cv.variable.value(), + AttributeEnum::Actual, "false", "test"); + + ConstantChargingLimit new_limit = { + .limit = 100.0, + .charging_rate_unit = ChargingRateUnitEnum::A, + }; + double deltaChanged = 0.2; + auto source = ChargingLimitSourceEnum::Other; + + auto resp = handler.handle_external_limits_changed(new_limit, deltaChanged, source, DEFAULT_EVSE_ID); + + ASSERT_THAT(resp.has_value(), testing::IsTrue()); + auto [notify_charging_limit_request, s] = resp.value(); + + ASSERT_THAT(notify_charging_limit_request.chargingSchedule.has_value(), testing::IsFalse()); +} + +TEST_F(SmartChargingHandlerTestFixtureV201, K12FR04_HandleExternalLimitsChanged_NotificationIncludesSource) { + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + device_model->set_value(limit_change_cv.component, limit_change_cv.variable.value(), AttributeEnum::Actual, "0.1", + "test"); + + ConstantChargingLimit new_limit = { + .limit = 100.0, + .charging_rate_unit = ChargingRateUnitEnum::A, + }; + double deltaChanged = 0.2; + auto source = ChargingLimitSourceEnum::SO; + + auto resp = handler.handle_external_limits_changed(new_limit, deltaChanged, source, DEFAULT_EVSE_ID); + + ASSERT_THAT(resp.has_value(), testing::IsTrue()); + auto [notify_charging_limit_request, s] = resp.value(); + + ASSERT_THAT(notify_charging_limit_request.chargingLimit.chargingLimitSource, testing::Eq(source)); +} + +TEST_F(SmartChargingHandlerTestFixtureV201, K12FR04_HandleExternalLimitsChanged_ThrowsIfSourceIsCSO) { + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + device_model->set_value(limit_change_cv.component, limit_change_cv.variable.value(), AttributeEnum::Actual, "0.1", + "test"); + + ConstantChargingLimit new_limit = { + .limit = 100.0, + .charging_rate_unit = ChargingRateUnitEnum::A, + }; + double deltaChanged = 0.2; + auto source = ChargingLimitSourceEnum::CSO; + + EXPECT_THROW(handler.handle_external_limits_changed(new_limit, deltaChanged, source, DEFAULT_EVSE_ID), + std::invalid_argument); +} + +TEST_F(SmartChargingHandlerTestFixtureV201, K12_HandleExternalLimitsChanged_RequestHasGivenEvseId) { + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + device_model->set_value(limit_change_cv.component, limit_change_cv.variable.value(), AttributeEnum::Actual, "0.1", + "test"); + + auto evse_id = 2; + + ConstantChargingLimit new_limit = { + .limit = 100.0, + .charging_rate_unit = ChargingRateUnitEnum::A, + }; + double deltaChanged = 0.2; + auto source = ChargingLimitSourceEnum::SO; + + auto resp = handler.handle_external_limits_changed(new_limit, deltaChanged, source, evse_id); + + ASSERT_THAT(resp.has_value(), testing::IsTrue()); + + auto [notify_charging_limit_request, s] = resp.value(); + + ASSERT_THAT(notify_charging_limit_request.chargingLimit.chargingLimitSource, testing::Eq(source)); + ASSERT_THAT(notify_charging_limit_request.evseId.has_value(), testing::IsTrue()); + ASSERT_THAT(notify_charging_limit_request.evseId.value(), testing::Eq(evse_id)); +} + +TEST_F(SmartChargingHandlerTestFixtureV201, + K13FR01_HandleClearedChargingLimitRequest_NoTransactionForEvseId_NoRequestCreated) { + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + device_model->set_value(limit_change_cv.component, limit_change_cv.variable.value(), AttributeEnum::Actual, "0.5", + "test"); + + ConstantChargingLimit new_limit = { + .limit = 100.0, + .charging_rate_unit = ChargingRateUnitEnum::A, + }; + double deltaChanged = 0.2; + auto source = ChargingLimitSourceEnum::Other; + + auto resp = handler.handle_external_limit_cleared(deltaChanged, source, DEFAULT_EVSE_ID); + + ASSERT_THAT(resp.has_value(), testing::IsFalse()); +} + +TEST_F(SmartChargingHandlerTestFixtureV201, + K13FR01_HandleClearedChargingLimitRequest_NoTransactionExists_NoRequestCreated) { + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + device_model->set_value(limit_change_cv.component, limit_change_cv.variable.value(), AttributeEnum::Actual, "0.1", + "test"); + + ConstantChargingLimit new_limit = { + .limit = 100.0, + .charging_rate_unit = ChargingRateUnitEnum::A, + }; + double deltaChanged = 0.2; + auto source = ChargingLimitSourceEnum::Other; + + auto resp = handler.handle_external_limit_cleared(deltaChanged, source, std::nullopt); + ASSERT_THAT(resp.has_value(), testing::IsFalse()); +} + +TEST_F( + SmartChargingHandlerTestFixtureV201, + K13FR02_HandleClearedChargingLimitRequest_TransactionForEvseId_UnderLimitChangeSignificance_OnlyClearedChargingLimitRequest) { + + std::string transaction_id = uuid(); + this->evse_manager->open_transaction(DEFAULT_EVSE_ID, transaction_id); + + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + device_model->set_value(limit_change_cv.component, limit_change_cv.variable.value(), AttributeEnum::Actual, "0.5", + "test"); + + ConstantChargingLimit new_limit = { + .limit = 100.0, + .charging_rate_unit = ChargingRateUnitEnum::A, + }; + double deltaChanged = 0.2; + auto source = ChargingLimitSourceEnum::Other; + + auto resp = handler.handle_external_limit_cleared(deltaChanged, source, DEFAULT_EVSE_ID); + + ASSERT_THAT(resp.has_value(), testing::IsTrue()); + auto [f, s] = resp.value(); + ASSERT_THAT(f.evseId, testing::Eq(DEFAULT_EVSE_ID)); + ASSERT_THAT(f.chargingLimitSource, testing::Eq(source)); + + ASSERT_THAT(s.size(), testing::Eq(0)); +} + +TEST_F( + SmartChargingHandlerTestFixtureV201, + K13FR02_HandleClearedChargingLimitRequest_TransactionForEvseId_LimitChangeSignificanceEqual_OnlyClearedChargingLimitRequest) { + + std::string transaction_id = uuid(); + this->evse_manager->open_transaction(DEFAULT_EVSE_ID, transaction_id); + + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + device_model->set_value(limit_change_cv.component, limit_change_cv.variable.value(), AttributeEnum::Actual, "0.2", + "test"); + + ConstantChargingLimit new_limit = { + .limit = 100.0, + .charging_rate_unit = ChargingRateUnitEnum::A, + }; + double deltaChanged = 0.2; + auto source = ChargingLimitSourceEnum::Other; + + auto resp = handler.handle_external_limit_cleared(deltaChanged, source, DEFAULT_EVSE_ID); + ASSERT_THAT(resp.has_value(), testing::IsTrue()); + auto [f, s] = resp.value(); + ASSERT_THAT(f.evseId, testing::Eq(DEFAULT_EVSE_ID)); + ASSERT_THAT(f.chargingLimitSource, testing::Eq(source)); + + ASSERT_THAT(s.size(), testing::Eq(0)); +} + +TEST_F( + SmartChargingHandlerTestFixtureV201, + K13FR03_HandleClearedChargingLimitRequest_HasTransaction_LimitChangeSignificanceExceeded_BothClearedChargingLimitRequestAndTransactionEventRequest) { + + std::string transaction_id = uuid(); + this->evse_manager->open_transaction(DEFAULT_EVSE_ID, transaction_id); + + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + device_model->set_value(limit_change_cv.component, limit_change_cv.variable.value(), AttributeEnum::Actual, "0.1", + "test"); + + ConstantChargingLimit new_limit = { + .limit = 100.0, + .charging_rate_unit = ChargingRateUnitEnum::A, + }; + double deltaChanged = 0.2; + auto source = ChargingLimitSourceEnum::Other; + + auto resp = handler.handle_external_limit_cleared(deltaChanged, source, DEFAULT_EVSE_ID); + ASSERT_THAT(resp.has_value(), testing::IsTrue()); + auto [f, s] = resp.value(); + ASSERT_THAT(f.evseId, testing::Eq(DEFAULT_EVSE_ID)); + ASSERT_THAT(f.chargingLimitSource, testing::Eq(source)); + + ASSERT_THAT(s.size(), testing::Eq(1)); + ASSERT_THAT(s.at(0).eventType, testing::Eq(TransactionEventEnum::Updated)); + ASSERT_THAT(s.at(0).triggerReason, testing::Eq(TriggerReasonEnum::ChargingRateChanged)); + ASSERT_THAT(s.at(0).transactionInfo.transactionId, testing::Eq(CiString<36>(transaction_id))); +} + +TEST_F( + SmartChargingHandlerTestFixtureV201, + K13FR03_HandleClearedChargingLimitRequest_NoEvseId_TransactionExists_LimitChangeSignificanceExceeded_BothClearedChargingLimitRequestAndSingleTransactionEventRequest) { + + std::string transaction_id = uuid(); + this->evse_manager->open_transaction(DEFAULT_EVSE_ID, transaction_id); + + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + device_model->set_value(limit_change_cv.component, limit_change_cv.variable.value(), AttributeEnum::Actual, "0.1", + "test"); + + ConstantChargingLimit new_limit = { + .limit = 100.0, + .charging_rate_unit = ChargingRateUnitEnum::A, + }; + double deltaChanged = 0.2; + auto source = ChargingLimitSourceEnum::Other; + + auto resp = handler.handle_external_limit_cleared(deltaChanged, source, std::nullopt); + ASSERT_THAT(resp.has_value(), testing::IsTrue()); + auto [f, s] = resp.value(); + ASSERT_THAT(f.evseId.has_value(), testing::IsFalse()); + ASSERT_THAT(f.chargingLimitSource, testing::Eq(source)); + + ASSERT_THAT(s.size(), testing::Eq(1)); + ASSERT_THAT(s.at(0).eventType, testing::Eq(TransactionEventEnum::Updated)); + ASSERT_THAT(s.at(0).triggerReason, testing::Eq(TriggerReasonEnum::ChargingRateChanged)); + ASSERT_THAT(s.at(0).transactionInfo.transactionId, testing::Eq(CiString<36>(transaction_id))); +} + +TEST_F( + SmartChargingHandlerTestFixtureV201, + K13FR03_HandleClearedChargingLimitRequest_NoEvseId_TransactionsExist_LimitChangeSignificanceExceeded_BothClearedChargingLimitRequestAndMultipleTransactionEventRequest) { + + std::string transaction_id_a = uuid(); + std::string transaction_id_b = uuid(); + this->evse_manager->open_transaction(DEFAULT_EVSE_ID, transaction_id_a); + this->evse_manager->open_transaction(DEFAULT_EVSE_ID + 1, transaction_id_b); + + const auto& limit_change_cv = ControllerComponentVariables::LimitChangeSignificance; + device_model->set_value(limit_change_cv.component, limit_change_cv.variable.value(), AttributeEnum::Actual, "0.1", + "test"); + + ConstantChargingLimit new_limit = { + .limit = 100.0, + .charging_rate_unit = ChargingRateUnitEnum::A, + }; + double deltaChanged = 0.2; + auto source = ChargingLimitSourceEnum::Other; + + auto resp = handler.handle_external_limit_cleared(deltaChanged, source, std::nullopt); + ASSERT_THAT(resp.has_value(), testing::IsTrue()); + auto [f, s] = resp.value(); + ASSERT_THAT(f.evseId.has_value(), testing::IsFalse()); + ASSERT_THAT(f.chargingLimitSource, testing::Eq(source)); + + ASSERT_THAT(s.size(), testing::Eq(2)); +} + } // namespace ocpp::v201