From 079e12b1424f6e1bab948be80d770c36d0321f5b Mon Sep 17 00:00:00 2001 From: Christoph <367712+folkengine@users.noreply.github.com> Date: Thu, 5 Sep 2024 10:58:30 -0700 Subject: [PATCH] Feature/K08 Get Composite Schedule (#745) * * Added OCPP 2.0.1 Calculate Composite Schedule functionality and tests * Added Generic optional type equality function for testing option types * Create v201/profile.hpp to match file structure used in 1.6 CompositeSchedule work * Added to_string functions to v201/utils to facilitate easier testing, logging and debugging * Added OCPP 1.6 test_composite_schedule.cpp suite. * Added ability to use serialized JSON Profiles scenarios for testing in v1.6 and 2.0.1. * Added JSON Profiles based on specific testing scenarios for v1.6 and 2.0.1. * Enabled now working 1.6 Composit Schedule tests * Added v201/test_profile.cpp suite mirroring but refactoring tests done for v1.6 version * Removed excessive logging Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Added sorting of vectors in SmartChargingTestUtils::get_charging_profiles_from_directory() to fix order issue in tests on CI Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Updated READMEs to fix linting issues Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Updated READMEs to fix more linting issues Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Removed unused LimitStackLevelPair struct caught by linter Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Added const to functions as per linter Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Added pass by const reference to functions as per linter Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Updated status doc Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Add get_valid_profiles() to Smart Charging Handler. When retrieving the valid profiles for calculating composite schedules, this function returns all of the profiles for a given EVSE ID, as well as the station-wide profiles. It also ensures that the profiles are valid. The calculations for composite schedule handle the start and end time, so this function does not account for it at this stage. Signed-off-by: Christopher Davis <150722105+christopher-davis-afs@users.noreply.github.com> Signed-off-by: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> * added calculation for charging station external constraints. added tests to cover external constraints. refactored calculation for both max and external to a method. Signed-off-by: Coury Richards <146002925+couryrr-afs@users.noreply.github.com> * smart_charging: Make more functions mockable Make `calculate_composite_schedule()` and `get_valid_profiles()` part of the `SmartChargingHandlerInterface` so they may be overriden for mocking. Signed-off-by: Christopher Davis <150722105+christopher-davis-afs@users.noreply.github.com> * smart_charging: Take chargingRateUnit as optional in `calculate_composite_schedule()` The internal functions (and the v1.6 impl) take this as optional, but we were requiring a value. Signed-off-by: Christopher Davis <150722105+christopher-davis-afs@users.noreply.github.com> * charge_point: Handle GetCompositeScheduleRequest Adds a message handler for GetCompositeScheduleRequest and associated tests within `test_charge_point.cpp`. Due to the core functionality being tested within `test_composite_schedule.cpp`, we primarily test that `get_valid_profiles()` and `calculate_composite_schedule()` are being called within the right contexts: * We calculate a composite schedule from a list of valid profiles * We do not calculate a composite schedule when the EVSE ID is unknown * We do not calculate a composite schedule when the `chargingRateUnit` sent in the request is not configured. Signed-off-by: Christopher Davis <150722105+christopher-davis-afs@users.noreply.github.com> * doc: Update OCPP v2.0.1 status doc We now cover: - K08.FR.03 - K08.FR.04 - K08.FR.05 - K08.FR.07 Signed-off-by: Christopher Davis <150722105+christopher-davis-afs@users.noreply.github.com> * Resolving PR comments from @Pietfried Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Moving location of json profiles as per PR comment Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Moving static functions only used in file into anonymous namespace as per PR comments Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Moving static functions only used in file into anonymous namespace as per PR comments Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Added documentation to test utility function as per PR review Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Moving equality operators to smart_charging_test_utils as per PR review Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Corrected mistake in Status doc as per PR review Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Updated constants as per PR review Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Updated profile comments as per PR review Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Moved functions only used in tests to tests as per PR review Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Added link to issue resolved in Case One composite schedule scenario test as per PR review Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Removed unused struct as per PR review Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Moved functions only used in tests to tests as per PR review Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Rolled back accidental changes to 1.6 code Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Remove profile scenario not used in PR Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Added more detailed Grid foundation test as per PR review. Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Fixed CalculateProfileEntryType_Param_Test as per PR review. Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Updated CalculateChargingSchedule_Overlap test names as per PR review. Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Updated CalculateChargingScheduleCombined_CombinedOverlapT names as per PR review. Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Moved functions only used in tests to tests as per PR review Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Added in grabbing transaction session start for relative profiles as per PR review Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Added check for evse_id == 0 as per PR review Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Touch to rerun GitHub Actions Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Touch to rerun GitHub Actions Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> * Touch to rerun GitHub Actions Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> --------- Signed-off-by: Christoph <367712+folkengine@users.noreply.github.com> Signed-off-by: Christopher Davis <150722105+christopher-davis-afs@users.noreply.github.com> Signed-off-by: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> Signed-off-by: Coury Richards <146002925+couryrr-afs@users.noreply.github.com> Co-authored-by: Gianfranco Berardi <54074967+gberardi-pillar@users.noreply.github.com> Co-authored-by: Coury Richards <146002925+couryrr-afs@users.noreply.github.com> Co-authored-by: Christopher Davis <150722105+christopher-davis-afs@users.noreply.github.com> --- doc/ocpp_201_status.md | 14 +- include/ocpp/common/constants.hpp | 23 + include/ocpp/common/types.hpp | 1 + include/ocpp/v201/charge_point.hpp | 2 + include/ocpp/v201/profile.hpp | 92 ++ include/ocpp/v201/smart_charging.hpp | 19 + lib/CMakeLists.txt | 1 + lib/ocpp/v201/charge_point.cpp | 39 + lib/ocpp/v201/profile.cpp | 592 ++++++++ lib/ocpp/v201/smart_charging.cpp | 87 +- tests/CMakeLists.txt | 5 + tests/lib/ocpp/v16/CMakeLists.txt | 4 + .../ocpp/v16/json/TxDefaultProfile_01.json | 20 + .../ocpp/v16/json/TxDefaultProfile_100.json | 30 + .../v16/json/TxDefaultProfile_2kw_17-20.json | 20 + ...efaultProfile_Absolute_Daily_2kw_1080.json | 20 + .../TxDefaultProfile_no_chargingRateUnit.json | 19 + tests/lib/ocpp/v16/json/TxProfile_02.json | 20 + .../ocpp/v16/json/TxProfile_03_Absolute.json | 20 + tests/lib/ocpp/v16/json/TxProfile_grid.json | 135 ++ .../lib/ocpp/v16/test_composite_schedule.cpp | 324 +++++ tests/lib/ocpp/v201/CMakeLists.txt | 8 +- tests/lib/ocpp/v201/json/README.md | 5 + tests/lib/ocpp/v201/json/baseline/README.md | 19 + .../ocpp/v201/json/baseline/TxProfile_1.json | 24 + .../v201/json/baseline/TxProfile_100.json | 34 + tests/lib/ocpp/v201/json/case_one/README.md | 14 + .../ocpp/v201/json/case_one/TxProfile_1.json | 23 + .../v201/json/case_one/TxProfile_100.json | 34 + ...ExternalConstraintProfile_grid_hourly.json | 138 ++ .../ChargingStationMaxProfile_2000.json | 22 + tests/lib/ocpp/v201/json/external/README.md | 8 + .../v201/json/external/TXProfile_2001.json | 23 + tests/lib/ocpp/v201/json/grid/README.md | 7 + .../v201/json/grid/TxProfile_grid_hourly.json | 139 ++ tests/lib/ocpp/v201/json/layered/README.md | 9 + .../v201/json/layered/TXProfile_single.json | 23 + .../json/layered/TxProfile_grid_hourly.json | 139 ++ .../v201/json/layered_recurring/README.md | 8 + .../layered_recurring/TXProfile_single.json | 24 + .../TxProfile_grid_hourly.json | 139 ++ ...ChargingStationMaxProfile_grid_hourly.json | 138 ++ tests/lib/ocpp/v201/json/max/README.md | 9 + .../ocpp/v201/json/max/TXProfile_2000.json | 23 + .../ocpp/v201/json/max/TXProfile_2001.json | 23 + tests/lib/ocpp/v201/json/relative/README.md | 8 + .../json/relative/TxProfile_grid_hourly.json | 139 ++ .../json/relative/TxProfile_relative.json | 22 + .../ocpp/v201/json/singles/Absolute_301.json | 30 + .../json/singles/Absolute_NoDuration_301.json | 29 + .../lib/ocpp/v201/json/singles/ProfileA.json | 31 + tests/lib/ocpp/v201/json/singles/README.md | 31 + .../json/singles/Recurring_Daily_301.json | 31 + .../Recurring_Daily_NoDuration_301.json | 30 + .../json/singles/Recurring_Weekly_301.json | 31 + .../Recurring_Weekly_NoDuration_301.json | 30 + .../ocpp/v201/json/singles/Relative_301.json | 29 + .../Relative_MultipleChargingSchedules.json | 58 + .../json/singles/Relative_NoDuration_301.json | 28 + .../TXProfile_Absolute_Start18-04.json | 23 + ...rofile_CONCERNING_overlapping_periods.json | 44 + .../mocks/smart_charging_handler_mock.hpp | 5 + .../ocpp/v201/smart_charging_test_utils.hpp | 238 ++++ tests/lib/ocpp/v201/test_charge_point.cpp | 77 ++ .../v201/test_component_state_manager.cpp | 1 + .../lib/ocpp/v201/test_composite_schedule.cpp | 816 +++++++++++ tests/lib/ocpp/v201/test_profile.cpp | 1211 +++++++++++++++++ .../ocpp/v201/test_smart_charging_handler.cpp | 76 +- 68 files changed, 5528 insertions(+), 10 deletions(-) create mode 100644 include/ocpp/common/constants.hpp create mode 100644 include/ocpp/v201/profile.hpp create mode 100644 lib/ocpp/v201/profile.cpp create mode 100644 tests/lib/ocpp/v16/json/TxDefaultProfile_01.json create mode 100644 tests/lib/ocpp/v16/json/TxDefaultProfile_100.json create mode 100644 tests/lib/ocpp/v16/json/TxDefaultProfile_2kw_17-20.json create mode 100644 tests/lib/ocpp/v16/json/TxDefaultProfile_Absolute_Daily_2kw_1080.json create mode 100644 tests/lib/ocpp/v16/json/TxDefaultProfile_no_chargingRateUnit.json create mode 100644 tests/lib/ocpp/v16/json/TxProfile_02.json create mode 100644 tests/lib/ocpp/v16/json/TxProfile_03_Absolute.json create mode 100644 tests/lib/ocpp/v16/json/TxProfile_grid.json create mode 100644 tests/lib/ocpp/v16/test_composite_schedule.cpp create mode 100644 tests/lib/ocpp/v201/json/README.md create mode 100644 tests/lib/ocpp/v201/json/baseline/README.md create mode 100644 tests/lib/ocpp/v201/json/baseline/TxProfile_1.json create mode 100644 tests/lib/ocpp/v201/json/baseline/TxProfile_100.json create mode 100644 tests/lib/ocpp/v201/json/case_one/README.md create mode 100644 tests/lib/ocpp/v201/json/case_one/TxProfile_1.json create mode 100644 tests/lib/ocpp/v201/json/case_one/TxProfile_100.json create mode 100644 tests/lib/ocpp/v201/json/external/ChargingStationExternalConstraintProfile_grid_hourly.json create mode 100644 tests/lib/ocpp/v201/json/external/ChargingStationMaxProfile_2000.json create mode 100644 tests/lib/ocpp/v201/json/external/README.md create mode 100644 tests/lib/ocpp/v201/json/external/TXProfile_2001.json create mode 100644 tests/lib/ocpp/v201/json/grid/README.md create mode 100644 tests/lib/ocpp/v201/json/grid/TxProfile_grid_hourly.json create mode 100644 tests/lib/ocpp/v201/json/layered/README.md create mode 100644 tests/lib/ocpp/v201/json/layered/TXProfile_single.json create mode 100644 tests/lib/ocpp/v201/json/layered/TxProfile_grid_hourly.json create mode 100644 tests/lib/ocpp/v201/json/layered_recurring/README.md create mode 100644 tests/lib/ocpp/v201/json/layered_recurring/TXProfile_single.json create mode 100644 tests/lib/ocpp/v201/json/layered_recurring/TxProfile_grid_hourly.json create mode 100644 tests/lib/ocpp/v201/json/max/ChargingStationMaxProfile_grid_hourly.json create mode 100644 tests/lib/ocpp/v201/json/max/README.md create mode 100644 tests/lib/ocpp/v201/json/max/TXProfile_2000.json create mode 100644 tests/lib/ocpp/v201/json/max/TXProfile_2001.json create mode 100644 tests/lib/ocpp/v201/json/relative/README.md create mode 100644 tests/lib/ocpp/v201/json/relative/TxProfile_grid_hourly.json create mode 100644 tests/lib/ocpp/v201/json/relative/TxProfile_relative.json create mode 100644 tests/lib/ocpp/v201/json/singles/Absolute_301.json create mode 100644 tests/lib/ocpp/v201/json/singles/Absolute_NoDuration_301.json create mode 100644 tests/lib/ocpp/v201/json/singles/ProfileA.json create mode 100644 tests/lib/ocpp/v201/json/singles/README.md create mode 100644 tests/lib/ocpp/v201/json/singles/Recurring_Daily_301.json create mode 100644 tests/lib/ocpp/v201/json/singles/Recurring_Daily_NoDuration_301.json create mode 100644 tests/lib/ocpp/v201/json/singles/Recurring_Weekly_301.json create mode 100644 tests/lib/ocpp/v201/json/singles/Recurring_Weekly_NoDuration_301.json create mode 100644 tests/lib/ocpp/v201/json/singles/Relative_301.json create mode 100644 tests/lib/ocpp/v201/json/singles/Relative_MultipleChargingSchedules.json create mode 100644 tests/lib/ocpp/v201/json/singles/Relative_NoDuration_301.json create mode 100644 tests/lib/ocpp/v201/json/singles/TXProfile_Absolute_Start18-04.json create mode 100644 tests/lib/ocpp/v201/json/singles/TxProfile_CONCERNING_overlapping_periods.json create mode 100644 tests/lib/ocpp/v201/smart_charging_test_utils.hpp create mode 100644 tests/lib/ocpp/v201/test_composite_schedule.cpp create mode 100644 tests/lib/ocpp/v201/test_profile.cpp diff --git a/doc/ocpp_201_status.md b/doc/ocpp_201_status.md index c99c62319..1ac81a08e 100644 --- a/doc/ocpp_201_status.md +++ b/doc/ocpp_201_status.md @@ -1341,13 +1341,13 @@ This document contains the status of which OCPP 2.0.1 numbered functional requir | ID | Status | Remark | |-----------|--------|--------| -| K08.FR.01 | ❎ | | -| K08.FR.02 | | | -| K08.FR.03 | | | -| K08.FR.04 | | | -| K08.FR.05 | | | -| K08.FR.06 | | | -| K08.FR.07 | | | +| K08.FR.01 | ✅ | | +| K08.FR.02 | ✅ | | +| K08.FR.03 | ✅ | | +| K08.FR.04 | ✅ | | +| K08.FR.05 | ✅ | | +| K08.FR.06 | ✅ | | +| K08.FR.07 | ✅ | | ## SmartCharging - Get Charging Profiles diff --git a/include/ocpp/common/constants.hpp b/include/ocpp/common/constants.hpp new file mode 100644 index 000000000..02b9ec700 --- /dev/null +++ b/include/ocpp/common/constants.hpp @@ -0,0 +1,23 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest + +#include + +namespace ocpp { + +// Time +constexpr std::int32_t DAYS_PER_WEEK = 7; +constexpr std::int32_t HOURS_PER_DAY = 24; +constexpr std::int32_t SECONDS_PER_HOUR = 3600; +constexpr std::int32_t SECONDS_PER_DAY = 86400; + +constexpr float DEFAULT_LIMIT_AMPS = 48.0; +constexpr float DEFAULT_LIMIT_WATTS = 33120.0; +constexpr std::int32_t DEFAULT_AND_MAX_NUMBER_PHASES = 3; +constexpr float LOW_VOLTAGE = 230; + +constexpr float NO_LIMIT_SPECIFIED = -1.0; +constexpr std::int32_t NO_START_PERIOD = -1; +constexpr std::int32_t EVSEID_NOT_SET = -1; + +} // namespace ocpp \ No newline at end of file diff --git a/include/ocpp/common/types.hpp b/include/ocpp/common/types.hpp index 3a08879d8..f1f0b1d74 100644 --- a/include/ocpp/common/types.hpp +++ b/include/ocpp/common/types.hpp @@ -1,5 +1,6 @@ // SPDX-License-Identifier: Apache-2.0 // Copyright Pionix GmbH and Contributors to EVerest + #ifndef OCPP_COMMON_TYPES_HPP #define OCPP_COMMON_TYPES_HPP diff --git a/include/ocpp/v201/charge_point.hpp b/include/ocpp/v201/charge_point.hpp index 09b758973..2c4b35d18 100644 --- a/include/ocpp/v201/charge_point.hpp +++ b/include/ocpp/v201/charge_point.hpp @@ -37,6 +37,7 @@ #include #include #include +#include #include #include #include @@ -729,6 +730,7 @@ class ChargePoint : public ChargePointInterface, private ocpp::ChargingStationBa void handle_set_charging_profile_req(Call call); void handle_clear_charging_profile_req(Call call); void handle_get_charging_profiles_req(Call call); + void handle_get_composite_schedule_req(Call call); // Functional Block L: Firmware management void handle_firmware_update_req(Call call); diff --git a/include/ocpp/v201/profile.hpp b/include/ocpp/v201/profile.hpp new file mode 100644 index 000000000..cfbe2e1e9 --- /dev/null +++ b/include/ocpp/v201/profile.hpp @@ -0,0 +1,92 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest + +#include + +namespace ocpp { +namespace v201 { + +/// \brief Returns elements from a specific ChargingProfile and ChargingSchedulePeriod +/// for use in the calculation of the CompositeSchedule within a specific slice +/// of time. These are aggregated by Profile. +/// \param in_start The starting time +/// \param in_duration The duration for the specific slice of time +/// \param in_period the details of this period +/// \param in_profile the charging profile +/// \return an entry with smart charging information for a specific period in time +struct period_entry_t { + void init(const ocpp::DateTime& in_start, int in_duration, const ChargingSchedulePeriod& in_period, + const ChargingProfile& in_profile); + bool validate(const ChargingProfile& profile, const ocpp::DateTime& now); + + ocpp::DateTime start; + ocpp::DateTime end; + float limit; + std::optional number_phases; + std::optional phase_to_use; + std::int32_t stack_level; + ChargingRateUnitEnum charging_rate_unit; + std::optional min_charging_rate; + + bool equals(const period_entry_t& other) const { + return (start == other.end) && (end == other.end) && (limit == other.limit) && + (number_phases == other.number_phases) && (stack_level == other.stack_level) && + (charging_rate_unit == other.charging_rate_unit) && (min_charging_rate == other.min_charging_rate); + } +}; + +/// \brief calculate the start times for the profile +/// \param now the current date and time +/// \param end the end of the composite schedule +/// \param session_start optional when the charging session started +/// \param profile the charging profile +/// \return a list of the start times of the profile +std::vector calculate_start(const DateTime& now, const DateTime& end, + const std::optional& session_start, const ChargingProfile& profile); + +/// \brief Calculates the period entries based upon the indicated time window for every profile passed in. +/// \param now the current date and time +/// \param end the end of the composite schedule +/// \param session_start optional when the charging session started +/// \param profile the charging profile +/// \param period_index the schedule period index +/// \note used by calculate_profile +/// \return the list of start times +std::vector calculate_profile_entry(const DateTime& now, const DateTime& end, + const std::optional& session_start, + const ChargingProfile& profile, std::uint8_t period_index); + +/// \brief generate an ordered list of valid schedule periods for the profile +/// \param now the current date and time +/// \param end ignore entries beyond this date and time (i.e. that start after end) +/// \param session_start optional when the charging session started +/// \param profile the charging profile +/// \return a list of profile periods with calculated date & time start and end times +/// \note it is valid for there to be gaps (for recurring profiles) +std::vector calculate_profile(const DateTime& now, const DateTime& end, + const std::optional& session_start, + const ChargingProfile& profile); + +/// \brief calculate the composite schedule for the list of periods +/// \param combined_schedules the list of periods to build into the schedule +/// \param now the start of the composite schedule +/// \param end the end (i.e. duration) of the composite schedule +/// \param charging_rate_unit the units to use (defaults to Amps) +/// \return the calculated composite schedule +CompositeSchedule calculate_composite_schedule(std::vector& combined_schedules, const DateTime& now, + const DateTime& end, + std::optional charging_rate_unit); + +/// \brief calculate the combined composite schedule from all of the different types of +/// CompositeSchedules +/// \param charge_point_max the composite schedule for ChargePointMax profiles +/// \param tx_default the composite schedule for TxDefault profiles +/// \param tx the composite schedule for Tx profiles +/// \return the calculated combined composite schedule +/// \note all composite schedules must have the same units configured +CompositeSchedule calculate_composite_schedule(const CompositeSchedule& charging_station_external_constraints, + const CompositeSchedule& charging_station_max, + const CompositeSchedule& tx_default, const CompositeSchedule& tx); + +} // namespace v201 +} // namespace ocpp \ No newline at end of file diff --git a/include/ocpp/v201/smart_charging.hpp b/include/ocpp/v201/smart_charging.hpp index 99f8f1598..ffc97e3bd 100644 --- a/include/ocpp/v201/smart_charging.hpp +++ b/include/ocpp/v201/smart_charging.hpp @@ -86,6 +86,12 @@ class SmartChargingHandlerInterface { virtual std::vector get_reported_profiles(const GetChargingProfilesRequest& request) const = 0; + virtual std::vector get_valid_profiles(int32_t evse_id) = 0; + + virtual CompositeSchedule calculate_composite_schedule(std::vector& valid_profiles, + const ocpp::DateTime& start_time, + const ocpp::DateTime& end_time, const int32_t evse_id, + std::optional charging_rate_unit) = 0; }; /// \brief This class handles and maintains incoming ChargingProfiles and contains the logic @@ -137,6 +143,18 @@ class SmartChargingHandler : public SmartChargingHandlerInterface { std::vector get_reported_profiles(const GetChargingProfilesRequest& request) const override; + /// \brief Retrieves all profiles that should be considered for calculating the composite schedule. + /// + std::vector get_valid_profiles(int32_t evse_id) override; + + /// + /// \brief Calculates the composite schedule for the given \p valid_profiles and the given \p connector_id + /// + CompositeSchedule calculate_composite_schedule(std::vector& valid_profiles, + const ocpp::DateTime& start_time, const ocpp::DateTime& end_time, + const int32_t evse_id, + std::optional charging_rate_unit) override; + protected: /// /// \brief validates the existence of the given \p evse_id according to the specification @@ -182,6 +200,7 @@ class SmartChargingHandler : public SmartChargingHandlerInterface { std::vector get_station_wide_profiles() const; std::vector get_evse_specific_tx_default_profiles() const; std::vector get_station_wide_tx_default_profiles() const; + 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; }; diff --git a/lib/CMakeLists.txt b/lib/CMakeLists.txt index 9a32e18af..74368b859 100644 --- a/lib/CMakeLists.txt +++ b/lib/CMakeLists.txt @@ -68,6 +68,7 @@ if(LIBOCPP_ENABLE_V201) ocpp/v201/notify_report_requests_splitter.cpp ocpp/v201/message_queue.cpp ocpp/v201/ocpp_enums.cpp + ocpp/v201/profile.cpp ocpp/v201/ocpp_types.cpp ocpp/v201/ocsp_updater.cpp ocpp/v201/monitoring_updater.cpp diff --git a/lib/ocpp/v201/charge_point.cpp b/lib/ocpp/v201/charge_point.cpp index aed79595e..67b488ceb 100644 --- a/lib/ocpp/v201/charge_point.cpp +++ b/lib/ocpp/v201/charge_point.cpp @@ -1178,6 +1178,9 @@ void ChargePoint::handle_message(const EnhancedMessage& messa case MessageType::GetChargingProfiles: this->handle_get_charging_profiles_req(json_message); break; + case MessageType::GetCompositeSchedule: + this->handle_get_composite_schedule_req(json_message); + break; case MessageType::SetMonitoringBase: this->handle_set_monitoring_base_req(json_message); break; @@ -3181,6 +3184,42 @@ void ChargePoint::handle_get_charging_profiles_req(Call call) { + EVLOG_debug << "Received GetCompositeScheduleRequest: " << call.msg << "\nwith messageId: " << call.uniqueId; + auto msg = call.msg; + GetCompositeScheduleResponse response; + response.status = GenericStatusEnum::Rejected; + + auto supported_charging_rate_units = + this->device_model->get_value(ControllerComponentVariables::ChargingScheduleChargingRateUnit); + auto unit_supported = supported_charging_rate_units.find(conversions::charging_rate_unit_enum_to_string( + msg.chargingRateUnit.value())) != supported_charging_rate_units.npos; + + // K01.FR.05 & K01.FR.07 + if (this->evse_manager->does_evse_exist(msg.evseId) && unit_supported) { + auto start_time = ocpp::DateTime(); + auto end_time = ocpp::DateTime(start_time.to_time_point() + std::chrono::seconds(msg.duration)); + + std::vector valid_profiles = this->smart_charging_handler->get_valid_profiles(msg.evseId); + auto schedule = this->smart_charging_handler->calculate_composite_schedule(valid_profiles, start_time, end_time, + msg.evseId, msg.chargingRateUnit); + + response.status = GenericStatusEnum::Accepted; + response.schedule = schedule; + } else { + auto reason = unit_supported ? ProfileValidationResultEnum::EvseDoesNotExist + : ProfileValidationResultEnum::ChargingScheduleChargingRateUnitUnsupported; + response.statusInfo = StatusInfo(); + response.statusInfo->reasonCode = conversions::profile_validation_result_to_reason_code(reason); + response.statusInfo->additionalInfo = conversions::profile_validation_result_to_string(reason); + EVLOG_debug << "Rejecting SetChargingProfileRequest:\n reasonCode: " << response.statusInfo->reasonCode.get() + << "\nadditionalInfo: " << response.statusInfo->additionalInfo->get(); + } + + ocpp::CallResult call_result(response, call.uniqueId); + this->send(call_result); +} + void ChargePoint::handle_firmware_update_req(Call call) { EVLOG_debug << "Received UpdateFirmwareRequest: " << call.msg << "\nwith messageId: " << call.uniqueId; if (call.msg.firmware.signingCertificate.has_value() or call.msg.firmware.signature.has_value()) { diff --git a/lib/ocpp/v201/profile.cpp b/lib/ocpp/v201/profile.cpp new file mode 100644 index 000000000..f28f5dd8c --- /dev/null +++ b/lib/ocpp/v201/profile.cpp @@ -0,0 +1,592 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest + +#include "ocpp/v201/profile.hpp" +#include "everest/logging.hpp" +#include +#include + +using std::chrono::duration_cast; +using std::chrono::seconds; + +namespace { + +/// \brief update the iterator when the current period has elapsed +/// \param[in] schedule_duration the time in seconds from the start of the composite schedule +/// \param[inout] itt the iterator for the periods in the schedule +/// \param[in] end the item beyond the last period in the schedule +/// \param[out] period the details of the current period in the schedule +/// \param[out] period_duration how long this period lasts +/// +/// \note period_duration is defined by the startPeriod of the next period or forever when +/// there is no next period. +void update_itt(const int schedule_duration, std::vector::const_iterator& itt, + const std::vector::const_iterator& end, + ocpp::v201::ChargingSchedulePeriod& period, int& period_duration) { + if (itt != end) { + // default is to remain in the current period + period = *itt; + + /* + * calculate the duration of this period: + * - the startPeriod of the next period in the vector, or + * - forever where there is no next period + */ + auto next = std::next(itt); + period_duration = (next != end) ? next->startPeriod : std::numeric_limits::max(); + + if (schedule_duration >= period_duration) { + /* + * when the current duration is beyond the duration of this period + * move to the next period in the vector and recalculate the period duration + * (the handling for being at the last element is below) + */ + itt++; + if (itt != end) { + period = *itt; + next = std::next(itt); + period_duration = (next != end) ? next->startPeriod : std::numeric_limits::max(); + } + } + } + + /* + * all periods in the schedule have been used + * i.e. there are no future periods to consider in this schedule + */ + if (itt == end) { + period.startPeriod = -1; + period_duration = std::numeric_limits::max(); + } +} + +std::pair convert_limit(const ocpp::v201::period_entry_t* const entry, + const ocpp::v201::ChargingRateUnitEnum selected_unit) { + assert(entry != nullptr); + float limit = entry->limit; + std::int32_t number_phases = entry->number_phases.value_or(ocpp::DEFAULT_AND_MAX_NUMBER_PHASES); + + // if the units are the same - don't change the values + if (selected_unit != entry->charging_rate_unit) { + if (selected_unit == ocpp::v201::ChargingRateUnitEnum::A) { + limit = entry->limit / (ocpp::LOW_VOLTAGE * number_phases); + } else { + limit = entry->limit * (ocpp::LOW_VOLTAGE * number_phases); + } + } + + return {limit, number_phases}; +} +} // namespace + +namespace ocpp { +namespace v201 { + +inline std::int32_t elapsed_seconds(const ocpp::DateTime& to, const ocpp::DateTime& from) { + return duration_cast(to.to_time_point() - from.to_time_point()).count(); +} + +inline ocpp::DateTime floor_seconds(const ocpp::DateTime& dt) { + return ocpp::DateTime(std::chrono::floor(dt.to_time_point())); +} + +/// \brief populate a schedule period +/// \param in_start the start time of the profile +/// \param in_duration the time in seconds from the start of the profile to the end of this period +/// \param in_period the details of this period +void period_entry_t::init(const DateTime& in_start, int in_duration, const ChargingSchedulePeriod& in_period, + const ChargingProfile& in_profile) { + // note duration can be negative and hence end time is before start time + // see period_entry_t::validate() + const auto start_tp = std::chrono::floor(in_start.to_time_point()); + start = std::move(DateTime(start_tp + seconds(in_period.startPeriod))); + end = std::move(DateTime(start_tp + seconds(in_duration))); + limit = in_period.limit; + number_phases = in_period.numberPhases; + stack_level = in_profile.stackLevel; + charging_rate_unit = in_profile.chargingSchedule.front().chargingRateUnit; + min_charging_rate = in_profile.chargingSchedule.front().minChargingRate; +} + +bool period_entry_t::validate(const ChargingProfile& profile, const ocpp::DateTime& now) { + bool b_valid{true}; + + if (profile.validFrom) { + const auto valid_from = floor_seconds(profile.validFrom.value()); + if (valid_from > start) { + // the calculated start is before the profile is valid + if (valid_from >= end) { + // the whole period isn't valid + b_valid = false; + } else { + // adjust start to match validFrom + start = valid_from; + } + } + } + + b_valid = b_valid && end > start; // check end time is after the start time + b_valid = b_valid && end > now; // ignore expired periods + return b_valid; +} + +/// \brief calculate the start times for the profile +/// \param in_now the current date and time +/// \param in_end the end of the composite schedule +/// \param in_session_start optional when the charging session started +/// \param in_profile the charging profile +/// \return a list of the start times of the profile +std::vector calculate_start(const DateTime& in_now, const DateTime& in_end, + const std::optional& in_session_start, + const ChargingProfile& in_profile) { + /* + * Absolute schedules start at the defined startSchedule + * Relative schedules start at session start + * Recurring schedules start based on startSchedule and the current date/time + * start can be affected by the profile validFrom. See period_entry_t::validate() + */ + std::vector start_times; + DateTime start = floor_seconds(in_now); // fallback when a better value can't be found + + switch (in_profile.chargingProfileKind) { + case ChargingProfileKindEnum::Absolute: + // TODO how to deal with multible ChargingSchedules? Currently only handling one. + if (in_profile.chargingSchedule.front().startSchedule) { + start = in_profile.chargingSchedule.front().startSchedule.value(); + } else { + // Absolute should have a startSchedule + EVLOG_warning << "Absolute charging profile (" << in_profile.id << ") without startSchedule"; + + // use validFrom where available + if (in_profile.validFrom) { + start = in_profile.validFrom.value(); + } + } + start_times.push_back(floor_seconds(start)); + break; + case ChargingProfileKindEnum::Recurring: + // TODO how to deal with multible ChargingSchedules? + if (in_profile.recurrencyKind && in_profile.chargingSchedule.front().startSchedule) { + const auto start_schedule = floor_seconds(in_profile.chargingSchedule.front().startSchedule.value()); + const auto end = floor_seconds(in_end); + const auto now_tp = start.to_time_point(); + int seconds_to_go_back{0}; + int seconds_to_go_forward{0}; + + /* + example problem case: + - allow daily charging 08:00 to 18:00 + at 07:00 and 19:00 what should the start time be? + a) profile could have 1 period (32A) at 0s with a duration of 36000s (10 hours) + relying on a lower stack level to deny charging + b) profile could have 2 periods (32A) at 0s and (0A) at 36000s (10 hours) + i.e. the profile covers the full 24 hours + at 07:00 is the start time in 1 hour, or 23 hours ago? + 23 hours ago is the chosen result - however the profile code needs to consider that + a new daily profile is about to start hence the next start time is provided. + Weekly has a similar problem + */ + + switch (in_profile.recurrencyKind.value()) { + case RecurrencyKindEnum::Daily: + seconds_to_go_back = duration_cast(now_tp - start_schedule.to_time_point()).count() % + (HOURS_PER_DAY * SECONDS_PER_HOUR); + if (seconds_to_go_back < 0) { + seconds_to_go_back += HOURS_PER_DAY * SECONDS_PER_HOUR; + } + seconds_to_go_forward = HOURS_PER_DAY * SECONDS_PER_HOUR; + break; + case RecurrencyKindEnum::Weekly: + seconds_to_go_back = duration_cast(now_tp - start_schedule.to_time_point()).count() % + (SECONDS_PER_DAY * DAYS_PER_WEEK); + if (seconds_to_go_back < 0) { + seconds_to_go_back += SECONDS_PER_DAY * DAYS_PER_WEEK; + } + seconds_to_go_forward = SECONDS_PER_DAY * DAYS_PER_WEEK; + break; + default: + EVLOG_error << "Invalid RecurrencyKindType: " << static_cast(in_profile.recurrencyKind.value()); + break; + } + + start = std::move(DateTime(now_tp - seconds(seconds_to_go_back))); + + while (start <= end) { + start_times.push_back(start); + start = DateTime(start.to_time_point() + seconds(seconds_to_go_forward)); + } + } + break; + case ChargingProfileKindEnum::Relative: + // if there isn't a session start then assume the session starts now + if (in_session_start) { + start = floor_seconds(in_session_start.value()); + } + start_times.push_back(start); + break; + default: + EVLOG_error << "Invalid ChargingProfileKindEnum: " << static_cast(in_profile.chargingProfileKind); + break; + } + return start_times; +} + +std::vector calculate_profile_entry(const DateTime& in_now, const DateTime& in_end, + const std::optional& in_session_start, + const ChargingProfile& in_profile, std::uint8_t in_period_index) { + std::vector entries; + + if (in_period_index >= in_profile.chargingSchedule.front().chargingSchedulePeriod.size()) { + EVLOG_error << "Invalid schedule period index [" << static_cast(in_period_index) + << "] (too large) for profile " << in_profile.id; + } else { + const auto& this_period = in_profile.chargingSchedule.front().chargingSchedulePeriod[in_period_index]; + + if ((in_period_index == 0) && (this_period.startPeriod != 0)) { + // invalid profile - first period must be 0 + EVLOG_error << "Invalid schedule period index [0] startPeriod " << this_period.startPeriod + << " for profile " << in_profile.id; + } else if ((in_period_index > 0) && + (in_profile.chargingSchedule.front().chargingSchedulePeriod[in_period_index - 1].startPeriod >= + this_period.startPeriod)) { + // invalid profile - periods must be in order and with increasing startPeriod values + EVLOG_error << "Invalid schedule period index [" << static_cast(in_period_index) << "] startPeriod " + << this_period.startPeriod << " for profile " << in_profile.id; + } else { + const bool has_next_period = + (in_period_index + 1) < in_profile.chargingSchedule.front().chargingSchedulePeriod.size(); + + // start time(s) of the schedule + // the start time of this period is calculated in period_entry_t::init() + const auto schedule_start = calculate_start(in_now, in_end, in_session_start, in_profile); + + for (std::uint8_t i = 0; i < schedule_start.size(); i++) { + const bool has_next_occurrance = (i + 1) < schedule_start.size(); + const auto& entry_start = schedule_start[i]; + + /* + * The duration of this period (from the start of the schedule) is the sooner of + * - forever + * - next period start time + * - optional duration + * - the start of the next recurrence + * - optional validTo + */ + + int duration = std::numeric_limits::max(); // forever + + if (has_next_period) { + duration = + in_profile.chargingSchedule.front().chargingSchedulePeriod[in_period_index + 1].startPeriod; + } + + // check optional chargingSchedule duration field + if (in_profile.chargingSchedule.front().duration && + (in_profile.chargingSchedule.front().duration.value() < duration)) { + duration = in_profile.chargingSchedule.front().duration.value(); + } + + // check duration doesn't extend into the next recurrence + if (has_next_occurrance) { + const auto next_start = + duration_cast(schedule_start[i + 1].to_time_point() - entry_start.to_time_point()) + .count(); + if (next_start < duration) { + duration = next_start; + } + } + + // check duration doesn't extend beyond profile validity + if (in_profile.validTo) { + // note can be negative + const auto valid_to = floor_seconds(in_profile.validTo.value()); + const auto valid_to_seconds = + duration_cast(valid_to.to_time_point() - entry_start.to_time_point()).count(); + if (valid_to_seconds < duration) { + duration = valid_to_seconds; + } + } + + period_entry_t entry; + entry.init(entry_start, duration, this_period, in_profile); + const auto now = floor_seconds(in_now); + + if (entry.validate(in_profile, now)) { + entries.push_back(std::move(entry)); + } + } + } + } + + return entries; +} + +std::vector calculate_profile(const DateTime& now, const DateTime& end, + const std::optional& session_start, + const ChargingProfile& profile) { + std::vector entries; + + for (std::uint8_t i = 0; i < profile.chargingSchedule.front().chargingSchedulePeriod.size(); i++) { + const auto results = calculate_profile_entry(now, end, session_start, profile, i); + for (const auto& entry : results) { + if (entry.start <= end) { + entries.push_back(entry); + } + } + } + + // sort into date order + struct { + bool operator()(const period_entry_t& a, const period_entry_t& b) const { + // earliest first + return a.start < b.start; + } + } less_than; + std::sort(entries.begin(), entries.end(), less_than); + return entries; +} + +CompositeSchedule calculate_composite_schedule(std::vector& in_combined_schedules, + const DateTime& in_now, const DateTime& in_end, + std::optional charging_rate_unit) { + + // Defaults to ChargingRateUnitEnum::A if not set. + const ChargingRateUnitEnum selected_unit = + (charging_rate_unit) ? charging_rate_unit.value() : ChargingRateUnitEnum::A; + + const auto now = floor_seconds(in_now); + const auto end = floor_seconds(in_end); + + CompositeSchedule composite{ + .chargingSchedulePeriod = {}, + .evseId = EVSEID_NOT_SET, + .duration = elapsed_seconds(end, now), + .scheduleStart = now, + .chargingRateUnit = selected_unit, + }; + + // sort the combined_schedules in stack priority order + struct { + bool operator()(const period_entry_t& a, const period_entry_t& b) const { + // highest stack level first + return a.stack_level > b.stack_level; + } + } less_than; + std::sort(in_combined_schedules.begin(), in_combined_schedules.end(), less_than); + + DateTime current = now; + + while (current < end) { + // find schedule to use for time: current + DateTime earliest = end; + DateTime next_earliest = end; + const period_entry_t* chosen{nullptr}; + + for (const auto& schedule : in_combined_schedules) { + if (schedule.start <= earliest) { + // ensure the earlier schedule is valid at the current time + if (schedule.end > current) { + next_earliest = earliest; + earliest = schedule.start; + chosen = &schedule; + if (earliest <= current) { + break; + } + } + } + } + + if (earliest > current) { + // there is a gap to fill + composite.chargingSchedulePeriod.push_back( + {elapsed_seconds(current, now), NO_LIMIT_SPECIFIED, std::nullopt, std::nullopt, std::nullopt}); + current = earliest; + } else { + // there is a schedule to use + const auto [limit, number_phases] = convert_limit(chosen, selected_unit); + + ChargingSchedulePeriod charging_schedule_period{elapsed_seconds(current, now), limit, std::nullopt, + number_phases}; + + // If the new ChargingSchedulePeriod.phaseToUse field is set, pass it on + // Profile validation has already ensured that the values have been properly set. + if (chosen->phase_to_use.has_value()) { + charging_schedule_period.phaseToUse = chosen->phase_to_use.value(); + } + + composite.chargingSchedulePeriod.push_back(charging_schedule_period); + if (chosen->end < next_earliest) { + current = chosen->end; + } else { + current = next_earliest; + } + } + } + + return composite; +} + +/// \brief update the period based on the power limit +/// \param[in] current the current startPeriod based on duration +/// \param[inout] prevailing_period the details of the current period in the schedule. +/// \param[in] candidate_period the period that is being compared to period. +/// \param[in] current_charging_rate_unit_enum details of the current composite schedule charging_rate_unit for +/// conversion. +/// +ChargingSchedulePeriod +minimize_charging_schedule_period_by_limit(const int current, const ChargingSchedulePeriod& prevailing_period, + const ChargingSchedulePeriod& candidate_period, + const ChargingRateUnitEnum current_charging_rate_unit_enum) { + + auto adjusted_period = prevailing_period; + + if (candidate_period.startPeriod == NO_START_PERIOD) { + return adjusted_period; + } + + if (prevailing_period.limit == NO_LIMIT_SPECIFIED && candidate_period.limit != NO_LIMIT_SPECIFIED) { + adjusted_period = candidate_period; + } else if (candidate_period.limit != NO_LIMIT_SPECIFIED) { + const auto charge_point_max_phases = candidate_period.numberPhases.value_or(DEFAULT_AND_MAX_NUMBER_PHASES); + + const auto period_max_phases = prevailing_period.numberPhases.value_or(DEFAULT_AND_MAX_NUMBER_PHASES); + adjusted_period.numberPhases = std::min(charge_point_max_phases, period_max_phases); + + if (current_charging_rate_unit_enum == ChargingRateUnitEnum::A) { + if (candidate_period.limit < prevailing_period.limit) { + adjusted_period.limit = candidate_period.limit; + } + } else { + const auto charge_point_limit_per_phase = candidate_period.limit / charge_point_max_phases; + const auto period_limit_per_phase = prevailing_period.limit / period_max_phases; + + adjusted_period.limit = std::floor(std::min(charge_point_limit_per_phase, period_limit_per_phase) * + adjusted_period.numberPhases.value()); + } + } + + return adjusted_period; +} + +CompositeSchedule calculate_composite_schedule(const CompositeSchedule& charging_station_external_constraints, + const CompositeSchedule& charging_station_max, + const CompositeSchedule& tx_default, const CompositeSchedule& tx) { + + CompositeSchedule combined{ + .chargingSchedulePeriod = {}, + .evseId = EVSEID_NOT_SET, + .duration = tx_default.duration, + .scheduleStart = tx_default.scheduleStart, + .chargingRateUnit = tx_default.chargingRateUnit, + + }; + + const float default_limit = + (tx_default.chargingRateUnit == ChargingRateUnitEnum::A) ? DEFAULT_LIMIT_AMPS : DEFAULT_LIMIT_WATTS; + + int current_period = 0; + + const int end = std::max(charging_station_external_constraints.duration, + std::max(std::max(charging_station_max.duration, tx_default.duration), tx.duration)); + + auto charging_station_external_constraints_itt = + charging_station_external_constraints.chargingSchedulePeriod.begin(); + auto charging_station_max_itt = charging_station_max.chargingSchedulePeriod.begin(); + auto tx_default_itt = tx_default.chargingSchedulePeriod.begin(); + auto tx_itt = tx.chargingSchedulePeriod.begin(); + + int duration_charging_station_external_constraints{std::numeric_limits::max()}; + int duration_charging_station_max{std::numeric_limits::max()}; + int duration_tx_default{std::numeric_limits::max()}; + int duration_tx{std::numeric_limits::max()}; + + ChargingSchedulePeriod period_charging_station_external_constraints{NO_START_PERIOD, NO_LIMIT_SPECIFIED, + std::nullopt, std::nullopt}; + ChargingSchedulePeriod period_charging_station_max{NO_START_PERIOD, NO_LIMIT_SPECIFIED, std::nullopt, std::nullopt}; + ChargingSchedulePeriod period_tx_default{NO_START_PERIOD, NO_LIMIT_SPECIFIED, std::nullopt, std::nullopt}; + ChargingSchedulePeriod period_tx{NO_START_PERIOD, NO_LIMIT_SPECIFIED, std::nullopt, std::nullopt}; + + update_itt(0, charging_station_external_constraints_itt, + charging_station_external_constraints.chargingSchedulePeriod.end(), + period_charging_station_external_constraints, duration_charging_station_external_constraints); + + update_itt(0, charging_station_max_itt, charging_station_max.chargingSchedulePeriod.end(), + period_charging_station_max, duration_charging_station_max); + + update_itt(0, tx_default_itt, tx_default.chargingSchedulePeriod.end(), period_tx_default, duration_tx_default); + + update_itt(0, tx_itt, tx.chargingSchedulePeriod.end(), period_tx, duration_tx); + + ChargingSchedulePeriod last{ + .startPeriod = 1, .limit = NO_LIMIT_SPECIFIED, .numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES}; + + while (current_period < end) { + + // get the duration that covers the smallest amount of time. + int duration = std::min(duration_charging_station_external_constraints, + std::min(std::min(duration_charging_station_max, duration_tx_default), duration_tx)); + + // create an unset period to override as needed. + ChargingSchedulePeriod period{ + .startPeriod = NO_START_PERIOD, .limit = NO_LIMIT_SPECIFIED, .numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES}; + + if (period_tx.startPeriod != NO_START_PERIOD) { + period = period_tx; + } + + if (period_tx_default.startPeriod != NO_START_PERIOD) { + if ((period.limit == NO_LIMIT_SPECIFIED) && (period_tx_default.limit != NO_LIMIT_SPECIFIED)) { + period = period_tx_default; + } + } + + period = minimize_charging_schedule_period_by_limit(current_period, period, period_charging_station_max, + combined.chargingRateUnit); + period = minimize_charging_schedule_period_by_limit( + current_period, period, period_charging_station_external_constraints, combined.chargingRateUnit); + + update_itt(duration, charging_station_external_constraints_itt, + charging_station_external_constraints.chargingSchedulePeriod.end(), + period_charging_station_external_constraints, duration_charging_station_external_constraints); + update_itt(duration, charging_station_max_itt, charging_station_max.chargingSchedulePeriod.end(), + period_charging_station_max, duration_charging_station_max); + update_itt(duration, tx_default_itt, tx_default.chargingSchedulePeriod.end(), period_tx_default, + duration_tx_default); + update_itt(duration, tx_itt, tx.chargingSchedulePeriod.end(), period_tx, duration_tx); + + /* + * defaults for numberPhases and limit need to consider the capabilities of the charger and grid + * connection. These are currently hard-coded in default_number_phases and default_limit but + * should be set from configuration. (see top of this file) + * + * the defaults should allow charging at the maximum allowed rate + */ + + if (period.startPeriod != NO_START_PERIOD) { + if (!period.numberPhases.has_value()) { + period.numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES; + } + + if (period.limit == NO_LIMIT_SPECIFIED) { + period.limit = default_limit; + } + period.startPeriod = current_period; + + // check this new period is a change from the previous one + if ((period.limit != last.limit) || (period.numberPhases.value() != last.numberPhases.value())) { + combined.chargingSchedulePeriod.push_back(period); + } + current_period = duration; + last = period; + } else { + combined.chargingSchedulePeriod.push_back( + {current_period, default_limit, std::nullopt, DEFAULT_AND_MAX_NUMBER_PHASES}); + current_period = end; + } + } + + return combined; +} + +} // namespace v201 +} // namespace ocpp \ No newline at end of file diff --git a/lib/ocpp/v201/smart_charging.cpp b/lib/ocpp/v201/smart_charging.cpp index 6271ea120..197a5c4bc 100644 --- a/lib/ocpp/v201/smart_charging.cpp +++ b/lib/ocpp/v201/smart_charging.cpp @@ -11,9 +11,11 @@ #include "ocpp/v201/messages/SetChargingProfile.hpp" #include "ocpp/v201/ocpp_enums.hpp" #include "ocpp/v201/ocpp_types.hpp" -#include "ocpp/v201/transaction.hpp" +#include "ocpp/v201/profile.hpp" +#include "ocpp/v201/utils.hpp" #include #include +#include #include #include @@ -546,6 +548,32 @@ SmartChargingHandler::get_reported_profiles(const GetChargingProfilesRequest& re return profiles; } +std::vector SmartChargingHandler::get_valid_profiles_for_evse(int32_t evse_id) { + std::vector valid_profiles; + + if (charging_profiles.count(evse_id) > 0) { + auto& evse_profiles = this->charging_profiles.at(evse_id); + for (auto profile : evse_profiles) { + if (this->validate_profile(profile, evse_id) == ProfileValidationResultEnum::Valid) { + valid_profiles.push_back(profile); + } + } + } + + return valid_profiles; +} + +std::vector SmartChargingHandler::get_valid_profiles(int32_t evse_id) { + std::vector valid_profiles = get_valid_profiles_for_evse(evse_id); + + if (evse_id != STATION_WIDE_ID) { + auto station_wide_profiles = get_valid_profiles_for_evse(STATION_WIDE_ID); + valid_profiles.insert(valid_profiles.end(), station_wide_profiles.begin(), station_wide_profiles.end()); + } + + return valid_profiles; +} + std::vector SmartChargingHandler::get_evse_specific_tx_default_profiles() const { std::vector evse_specific_tx_default_profiles; @@ -621,4 +649,61 @@ SmartChargingHandler::verify_no_conflicting_external_constraints_id(const Chargi return result; } +CompositeSchedule SmartChargingHandler::calculate_composite_schedule( + std::vector& valid_profiles, const ocpp::DateTime& start_time, const ocpp::DateTime& end_time, + const int32_t evse_id, std::optional charging_rate_unit) { + + std::optional session_start{}; + + if (this->evse_manager.does_evse_exist(evse_id) and evse_id != 0 and + this->evse_manager.get_evse(evse_id).get_transaction() != nullptr) { + const auto& transaction = this->evse_manager.get_evse(evse_id).get_transaction(); + session_start = transaction->start_time; + } + + std::vector charging_station_external_constraints_periods{}; + std::vector charge_point_max_periods{}; + std::vector tx_default_periods{}; + std::vector tx_periods{}; + + for (const auto& profile : valid_profiles) { + std::vector periods{}; + periods = ocpp::v201::calculate_profile(start_time, end_time, session_start, profile); + + switch (profile.chargingProfilePurpose) { + case ChargingProfilePurposeEnum::ChargingStationExternalConstraints: + charging_station_external_constraints_periods.insert(charging_station_external_constraints_periods.end(), + periods.begin(), periods.end()); + break; + case ChargingProfilePurposeEnum::ChargingStationMaxProfile: + charge_point_max_periods.insert(charge_point_max_periods.end(), periods.begin(), periods.end()); + break; + case ChargingProfilePurposeEnum::TxDefaultProfile: + tx_default_periods.insert(tx_default_periods.end(), periods.begin(), periods.end()); + break; + case ChargingProfilePurposeEnum::TxProfile: + tx_periods.insert(tx_periods.end(), periods.begin(), periods.end()); + break; + default: + break; + } + } + + auto charging_station_external_constraints = ocpp::v201::calculate_composite_schedule( + charging_station_external_constraints_periods, start_time, end_time, charging_rate_unit); + auto composite_charge_point_max = + ocpp::v201::calculate_composite_schedule(charge_point_max_periods, start_time, end_time, charging_rate_unit); + auto composite_tx_default = + ocpp::v201::calculate_composite_schedule(tx_default_periods, start_time, end_time, charging_rate_unit); + auto composite_tx = ocpp::v201::calculate_composite_schedule(tx_periods, start_time, end_time, charging_rate_unit); + + CompositeSchedule composite_schedule = ocpp::v201::calculate_composite_schedule( + charging_station_external_constraints, composite_charge_point_max, composite_tx_default, composite_tx); + + // Set the EVSE ID for the resulting CompositeSchedule + composite_schedule.evseId = evse_id; + + return composite_schedule; +} + } // namespace ocpp::v201 diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index fbd59ad9c..3af471e5a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -10,6 +10,9 @@ set(DEVICE_MODEL_CURRENT_CHANGED_RESOURCES_DIR ${CMAKE_CURRENT_SOURCE_DIR}/confi set(DEVICE_MODEL_CURRENT_WRONG_RESOURCES_DIR ${CMAKE_CURRENT_SOURCE_DIR}/config/v201/resources_wrong) set(DEVICE_MODEL_EXAMPLE_SCHEMAS_LOCATION_V201 "${CMAKE_CURRENT_BINARY_DIR}/resources/example_config/v201/component_config") set(DEVICE_MODEL_CURRENT_EXAMPLE_SCHEMAS_LOCATION_V201 "${PROJECT_SOURCE_DIR}/config/v201/component_config") +set(TEST_PROFILES_LOCATION_V16 "${CMAKE_CURRENT_BINARY_DIR}/resources/profiles/v16") +set(TEST_PROFILES_LOCATION_V201 "${CMAKE_CURRENT_BINARY_DIR}/resources/profiles/v201") + add_executable(libocpp_unit_tests config/v201/resources_wrong/component_config_required_no_value/standardized/UnitTestCtrlr.json) @@ -21,6 +24,8 @@ target_compile_definitions(libocpp_unit_tests 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}" + TEST_PROFILES_LOCATION_V16="${TEST_PROFILES_LOCATION_V16}" + TEST_PROFILES_LOCATION_V201="${TEST_PROFILES_LOCATION_V201}" ) add_custom_command(TARGET libocpp_unit_tests POST_BUILD diff --git a/tests/lib/ocpp/v16/CMakeLists.txt b/tests/lib/ocpp/v16/CMakeLists.txt index e3762a0dd..ec7864a10 100644 --- a/tests/lib/ocpp/v16/CMakeLists.txt +++ b/tests/lib/ocpp/v16/CMakeLists.txt @@ -12,4 +12,8 @@ target_sources(libocpp_unit_tests PRIVATE database_tests.cpp test_message_queue.cpp test_charge_point_state_machine.cpp + test_composite_schedule.cpp ) + +# Copy the json files used for testing to the destination directory +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/json DESTINATION ${TEST_PROFILES_LOCATION_V16}) diff --git a/tests/lib/ocpp/v16/json/TxDefaultProfile_01.json b/tests/lib/ocpp/v16/json/TxDefaultProfile_01.json new file mode 100644 index 000000000..e34451e82 --- /dev/null +++ b/tests/lib/ocpp/v16/json/TxDefaultProfile_01.json @@ -0,0 +1,20 @@ +{ + "chargingProfileId": 1, + "chargingProfileKind": "Absolute", + "chargingProfilePurpose": "TxDefaultProfile", + "chargingSchedule": { + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 2000.0, + "numberPhases": 1, + "startPeriod": 0 + } + ], + "duration": 1080, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T18:00:00.000Z" + }, + "recurrencyKind": "Daily", + "stackLevel": 1 +} \ No newline at end of file diff --git a/tests/lib/ocpp/v16/json/TxDefaultProfile_100.json b/tests/lib/ocpp/v16/json/TxDefaultProfile_100.json new file mode 100644 index 000000000..fd2196089 --- /dev/null +++ b/tests/lib/ocpp/v16/json/TxDefaultProfile_100.json @@ -0,0 +1,30 @@ +{ + "chargingProfileId": 100, + "chargingProfileKind": "Recurring", + "chargingProfilePurpose": "TxDefaultProfile", + "chargingSchedule": { + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 11000.0, + "numberPhases": 3, + "startPeriod": 0 + }, + { + "limit": 6000.0, + "numberPhases": 3, + "startPeriod": 28800 + }, + { + "limit": 12000.0, + "numberPhases": 3, + "startPeriod": 72000 + } + ], + "duration": 86400, + "minChargingRate": 0.0, + "startSchedule": "2023-01-17T17:00:00.000Z" + }, + "recurrencyKind": "Daily", + "stackLevel": 0 +} \ No newline at end of file diff --git a/tests/lib/ocpp/v16/json/TxDefaultProfile_2kw_17-20.json b/tests/lib/ocpp/v16/json/TxDefaultProfile_2kw_17-20.json new file mode 100644 index 000000000..516945257 --- /dev/null +++ b/tests/lib/ocpp/v16/json/TxDefaultProfile_2kw_17-20.json @@ -0,0 +1,20 @@ +{ + "chargingProfileId": 10, + "chargingProfileKind": "Absolute", + "chargingProfilePurpose": "TxDefaultProfile", + "chargingSchedule": { + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 2000.0, + "numberPhases": 1, + "startPeriod": 0 + } + ], + "duration": 1080, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T17:00:00.000Z" + }, + "recurrencyKind": "Daily", + "stackLevel": 1 +} \ No newline at end of file diff --git a/tests/lib/ocpp/v16/json/TxDefaultProfile_Absolute_Daily_2kw_1080.json b/tests/lib/ocpp/v16/json/TxDefaultProfile_Absolute_Daily_2kw_1080.json new file mode 100644 index 000000000..f95fb1175 --- /dev/null +++ b/tests/lib/ocpp/v16/json/TxDefaultProfile_Absolute_Daily_2kw_1080.json @@ -0,0 +1,20 @@ +{ + "chargingProfileId": 11, + "chargingProfileKind": "Absolute", + "chargingProfilePurpose": "TxDefaultProfile", + "chargingSchedule": { + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 2000.0, + "numberPhases": 1, + "startPeriod": 0 + } + ], + "duration": 1080, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T17:00:00.000Z" + }, + "recurrencyKind": "Daily", + "stackLevel": 1 +} \ No newline at end of file diff --git a/tests/lib/ocpp/v16/json/TxDefaultProfile_no_chargingRateUnit.json b/tests/lib/ocpp/v16/json/TxDefaultProfile_no_chargingRateUnit.json new file mode 100644 index 000000000..9148abea7 --- /dev/null +++ b/tests/lib/ocpp/v16/json/TxDefaultProfile_no_chargingRateUnit.json @@ -0,0 +1,19 @@ +{ + "chargingProfileId": 1, + "chargingProfileKind": "Absolute", + "chargingProfilePurpose": "TxDefaultProfile", + "chargingSchedule": { + "chargingSchedulePeriod": [ + { + "limit": 2000.0, + "numberPhases": 1, + "startPeriod": 0 + } + ], + "duration": 1080, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T17:00:00.000Z" + }, + "recurrencyKind": "Daily", + "stackLevel": 1 +} \ No newline at end of file diff --git a/tests/lib/ocpp/v16/json/TxProfile_02.json b/tests/lib/ocpp/v16/json/TxProfile_02.json new file mode 100644 index 000000000..74187239a --- /dev/null +++ b/tests/lib/ocpp/v16/json/TxProfile_02.json @@ -0,0 +1,20 @@ +{ + "chargingProfileId": 2, + "chargingProfileKind": "Recurring", + "chargingProfilePurpose": "TxProfile", + "chargingSchedule": { + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 2000.0, + "numberPhases": 1, + "startPeriod": 0 + } + ], + "duration": 1080, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T18:04:00.000Z" + }, + "recurrencyKind": "Daily", + "stackLevel": 2 +} \ No newline at end of file diff --git a/tests/lib/ocpp/v16/json/TxProfile_03_Absolute.json b/tests/lib/ocpp/v16/json/TxProfile_03_Absolute.json new file mode 100644 index 000000000..146d5b0f0 --- /dev/null +++ b/tests/lib/ocpp/v16/json/TxProfile_03_Absolute.json @@ -0,0 +1,20 @@ +{ + "chargingProfileId": 3, + "chargingProfileKind": "Absolute", + "chargingProfilePurpose": "TxProfile", + "chargingSchedule": { + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 2000.0, + "numberPhases": 1, + "startPeriod": 0 + } + ], + "duration": 1080, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T18:04:00.000Z" + }, + "recurrencyKind": "Daily", + "stackLevel": 2 +} \ No newline at end of file diff --git a/tests/lib/ocpp/v16/json/TxProfile_grid.json b/tests/lib/ocpp/v16/json/TxProfile_grid.json new file mode 100644 index 000000000..5c4250500 --- /dev/null +++ b/tests/lib/ocpp/v16/json/TxProfile_grid.json @@ -0,0 +1,135 @@ +{ + "chargingProfileId": 24, + "chargingProfileKind": "Recurring", + "chargingProfilePurpose": "TxProfile", + "chargingSchedule": { + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 1.0, + "numberPhases": 1, + "startPeriod": 0 + }, + { + "limit": 2.0, + "numberPhases": 1, + "startPeriod": 3600 + }, + { + "limit": 3.0, + "numberPhases": 1, + "startPeriod": 7200 + }, + { + "limit": 4.0, + "numberPhases": 1, + "startPeriod": 10800 + }, + { + "limit": 5.0, + "numberPhases": 1, + "startPeriod": 14400 + }, + { + "limit": 6.0, + "numberPhases": 1, + "startPeriod": 18000 + }, + { + "limit": 7.0, + "numberPhases": 1, + "startPeriod": 21600 + }, + { + "limit": 8.0, + "numberPhases": 1, + "startPeriod": 25200 + }, + { + "limit": 9.0, + "numberPhases": 1, + "startPeriod": 28800 + }, + { + "limit": 10.0, + "numberPhases": 1, + "startPeriod": 32400 + }, + { + "limit": 11.0, + "numberPhases": 1, + "startPeriod": 36000 + }, + { + "limit": 12.0, + "numberPhases": 1, + "startPeriod": 39600 + }, + { + "limit": 13.0, + "numberPhases": 1, + "startPeriod": 43200 + }, + { + "limit": 14.0, + "numberPhases": 1, + "startPeriod": 46800 + }, + { + "limit": 15.0, + "numberPhases": 1, + "startPeriod": 50400 + }, + { + "limit": 16.0, + "numberPhases": 1, + "startPeriod": 54000 + }, + { + "limit": 17.0, + "numberPhases": 1, + "startPeriod": 57600 + }, + { + "limit": 18.0, + "numberPhases": 1, + "startPeriod": 61200 + }, + { + "limit": 19.0, + "numberPhases": 1, + "startPeriod": 64800 + }, + { + "limit": 20.0, + "numberPhases": 1, + "startPeriod": 68400 + }, + { + "limit": 21.0, + "numberPhases": 1, + "startPeriod": 72000 + }, + { + "limit": 22.0, + "numberPhases": 1, + "startPeriod": 75600 + }, + { + "limit": 23.0, + "numberPhases": 1, + "startPeriod": 79200 + }, + { + "limit": 24.0, + "numberPhases": 1, + "startPeriod": 82800 + } + ], + "duration": 86400, + "minChargingRate": 0.0, + "startSchedule": "2023-01-17T00:00:00.000Z" + }, + "recurrencyKind": "Daily", + "stackLevel": 0 +} \ No newline at end of file diff --git a/tests/lib/ocpp/v16/test_composite_schedule.cpp b/tests/lib/ocpp/v16/test_composite_schedule.cpp new file mode 100644 index 000000000..15ec70f2d --- /dev/null +++ b/tests/lib/ocpp/v16/test_composite_schedule.cpp @@ -0,0 +1,324 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest + +#include +#include +#include +#include +#include +#include +#include +namespace fs = std::filesystem; + +#include "everest/logging.hpp" +#include +#include +#include +#include +// #include +#include +#include +#include + +namespace ocpp { +namespace v16 { + +/** + * CompositeSchedule Test Fixture + */ +class CompositeScheduleTestFixture : public testing::Test { +protected: + void SetUp() override { + this->evse_security = std::make_shared(); + } + + void add_connector(int id) { + auto connector = std::make_shared(id); + + auto timer = std::unique_ptr(); + + connector->transaction = + std::make_shared(-1, id, "test", "test", 1, std::nullopt, ocpp::DateTime(), std::move(timer)); + connectors[id] = connector; + } + + SmartChargingHandler* create_smart_charging_handler(const int number_of_connectors) { + for (int i = 0; i <= number_of_connectors; i++) { + add_connector(i); + } + + const std::string chargepoint_id = "1"; + const fs::path database_path = "na"; + const fs::path init_script_path = "na"; + + auto database = std::make_unique(database_path / (chargepoint_id + ".db")); + std::shared_ptr> database_handler = + std::make_shared>(std::move(database), init_script_path); + + auto handler = new SmartChargingHandler(connectors, database_handler, true); + + return handler; + } + + ChargingProfile get_charging_profile_from_file(const std::string& filename) { + const std::string base_path = std::string(TEST_PROFILES_LOCATION_V16) + "/json/"; + const std::string full_path = base_path + filename; + + std::ifstream f(full_path.c_str()); + json data = json::parse(f); + + ChargingProfile cp; + from_json(data, cp); + return cp; + } + + std::string get_log_duration_string(int32_t duration) { + if (duration < 1) { + return "0 Seconds "; + } + + int32_t remaining = duration; + + std::string log_str = ""; + + if (remaining >= 86400) { + int32_t days = remaining / 86400; + remaining = remaining % 86400; + if (days > 1) { + log_str += std::to_string(days) + " Days "; + } else { + log_str += std::to_string(days) + " Day "; + } + } + if (remaining >= 3600) { + int32_t hours = remaining / 3600; + remaining = remaining % 3600; + log_str += std::to_string(hours) + " Hours "; + } + if (remaining >= 60) { + int32_t minutes = remaining / 60; + remaining = remaining % 60; + log_str += std::to_string(minutes) + " Minutes "; + } + if (remaining > 0) { + log_str += std::to_string(remaining) + " Seconds "; + } + return log_str; + } + + void log_duration(int32_t duration) { + EVLOG_info << get_log_duration_string(duration); + } + + void log_me(ChargingProfile& cp) { + json cp_json; + to_json(cp_json, cp); + + EVLOG_info << " ChargingProfile> " << cp_json.dump(4); + log_duration(cp.chargingSchedule.duration.value_or(0)); + } + + void log_me(std::vector profiles) { + EVLOG_info << "["; + for (auto& profile : profiles) { + log_me(profile); + } + EVLOG_info << "]"; + } + + void log_me(EnhancedChargingSchedule& ecs) { + json ecs_json; + to_json(ecs_json, ecs); + + EVLOG_info << "EnhancedChargingSchedule> " << ecs_json.dump(4); + } + + void log_me(EnhancedChargingSchedule& ecs, const DateTime start_time) { + log_me(ecs); + EVLOG_info << "Start Time> " << start_time.to_rfc3339(); + + int32_t i = 0; + for (auto& period : ecs.chargingSchedulePeriod) { + i++; + int32_t numberPhases = 0; + if (period.numberPhases.has_value()) { + numberPhases = period.numberPhases.value(); + } + EVLOG_info << " period #" << i << " {limit: " << period.limit << " numberPhases:" << numberPhases + << " stackLevel:" << period.stackLevel << "} starts " + << get_log_duration_string(period.startPeriod) << "in"; + } + if (ecs.duration.has_value()) { + EVLOG_info << " period #" << i << " ends after " << get_log_duration_string(ecs.duration.value()); + } else { + EVLOG_info << " period #" << i << " ends in 0 Seconds"; + } + } + + /// \brief Returns a vector of ChargingProfiles to be used as a baseline for testing core functionality + /// of generating an EnhancedChargingSchedule. + std::vector get_baseline_profile_vector() { + auto profile_01 = get_charging_profile_from_file("TxDefaultProfile_01.json"); + // auto profile_02 = getChargingProfileFromFile("TxDefaultProfile_02.json"); + auto profile_100 = get_charging_profile_from_file("TxDefaultProfile_100.json"); + // return {profile_01, profile_02, profile_100}; + return {profile_01, profile_100}; + } + + // Default values used within the tests + std::map> connectors; + std::shared_ptr database_handler; + std::shared_ptr evse_security; +}; + +TEST_F(CompositeScheduleTestFixture, CalculateEnhancedCompositeSchedule_ValidatedBaseline) { + // GTEST_SKIP(); + auto handler = create_smart_charging_handler(1); + std::vector profiles = get_baseline_profile_vector(); + log_me(profiles); + + const DateTime my_date_start_range = ocpp::DateTime("2024-01-17T18:01:00"); + const DateTime my_date_end_range = ocpp::DateTime("2024-01-18T06:00:00"); + + EVLOG_info << " Start> " << my_date_start_range.to_rfc3339(); + EVLOG_info << " End> " << my_date_end_range.to_rfc3339(); + + auto composite_schedule = handler->calculate_enhanced_composite_schedule( + profiles, my_date_start_range, my_date_end_range, 1, profiles.at(0).chargingSchedule.chargingRateUnit); + + log_me(composite_schedule, my_date_start_range); + + ASSERT_EQ(composite_schedule.chargingRateUnit, ChargingRateUnit::W); + ASSERT_EQ(composite_schedule.duration, 43140); + ASSERT_EQ(profiles.size(), 2); + ASSERT_EQ(composite_schedule.chargingSchedulePeriod.size(), 3); + auto& period_01 = composite_schedule.chargingSchedulePeriod.at(0); + ASSERT_EQ(period_01.limit, 2000); + ASSERT_EQ(period_01.numberPhases, 1); + ASSERT_EQ(period_01.stackLevel, 1); + ASSERT_EQ(period_01.startPeriod, 0); + auto& period_02 = composite_schedule.chargingSchedulePeriod.at(1); + ASSERT_EQ(period_02.limit, 11000); + ASSERT_EQ(period_02.numberPhases, 3); + ASSERT_EQ(period_02.stackLevel, 0); + ASSERT_EQ(period_02.startPeriod, 1020); + auto& period_03 = composite_schedule.chargingSchedulePeriod.at(2); + ASSERT_EQ(period_03.limit, 6000.0); + ASSERT_EQ(period_03.numberPhases, 3); + ASSERT_EQ(period_03.stackLevel, 0); + ASSERT_EQ(period_03.startPeriod, 25140); +} + +/// +/// This was a defect in the earier 1.6 code that has now been fixed. Basically a tight test with a higher +/// stack Profile that is Absolute but marked with a recurrencyKind of Daily. +/// +TEST_F(CompositeScheduleTestFixture, CalculateEnhancedCompositeSchedule_TightLayeredTestWithAbsoluteProfile) { + auto handler = create_smart_charging_handler(1); + + ChargingProfile profile_grid = get_charging_profile_from_file("TxProfile_grid.json"); + ChargingProfile txprofile_03 = get_charging_profile_from_file("TxProfile_03_Absolute.json"); + std::vector profiles = {profile_grid, txprofile_03}; + + const DateTime my_date_start_range = ocpp::DateTime("2024-01-18T18:04:00"); + const DateTime my_date_end_range = ocpp::DateTime("2024-01-18T18:22:00"); + + EVLOG_info << " Start> " << my_date_start_range.to_rfc3339(); + EVLOG_info << " End> " << my_date_end_range.to_rfc3339(); + + auto composite_schedule = handler->calculate_enhanced_composite_schedule( + profiles, my_date_start_range, my_date_end_range, 1, profiles.at(0).chargingSchedule.chargingRateUnit); + + log_me(composite_schedule, my_date_start_range); + + ASSERT_EQ(composite_schedule.chargingSchedulePeriod.size(), 1); + ASSERT_EQ(composite_schedule.duration, 1080); + ASSERT_EQ(composite_schedule.chargingSchedulePeriod.at(0).limit, 19.0); +} + +/// +/// A tight CompositeSchedude test is one where the start and end times exactly match the time window of the +/// highest stack level. +/// +TEST_F(CompositeScheduleTestFixture, CalculateEnhancedCompositeSchedule_TightLayeredTest) { + auto handler = create_smart_charging_handler(1); + + ChargingProfile profile_grid = get_charging_profile_from_file("TxProfile_grid.json"); + ChargingProfile txprofile_02 = get_charging_profile_from_file("TxProfile_02.json"); + std::vector profiles = {profile_grid, txprofile_02}; + + const DateTime my_date_start_range = ocpp::DateTime("2024-01-18T18:04:00"); + const DateTime my_date_end_range = ocpp::DateTime("2024-01-18T18:22:00"); + + EVLOG_info << " Start> " << my_date_start_range.to_rfc3339(); + EVLOG_info << " End> " << my_date_end_range.to_rfc3339(); + + auto composite_schedule = handler->calculate_enhanced_composite_schedule( + profiles, my_date_start_range, my_date_end_range, 1, profiles.at(0).chargingSchedule.chargingRateUnit); + + log_me(composite_schedule, my_date_start_range); + + ASSERT_EQ(composite_schedule.chargingSchedulePeriod.size(), 1); + ASSERT_EQ(composite_schedule.duration, 1080); + ASSERT_EQ(composite_schedule.chargingSchedulePeriod.at(0).limit, 2000.0); +} + +/// +/// A fat CompositeSchedude test is one where the start time begins before the time window of the highest stack level +/// profile, and end time is afterwards. +/// +TEST_F(CompositeScheduleTestFixture, CalculateEnhancedCompositeSchedule_FatLayeredTest) { + auto handler = create_smart_charging_handler(1); + + ChargingProfile profile_grid = get_charging_profile_from_file("TxProfile_grid.json"); + ChargingProfile txprofile_02 = get_charging_profile_from_file("TxProfile_02.json"); + std::vector profiles = {profile_grid, txprofile_02}; + + const DateTime my_date_start_range = ocpp::DateTime("2024-01-18T18:02:00"); + const DateTime my_date_end_range = ocpp::DateTime("2024-01-18T18:24:00"); + + EVLOG_info << " Start> " << my_date_start_range.to_rfc3339(); + EVLOG_info << " End> " << my_date_end_range.to_rfc3339(); + + auto composite_schedule = handler->calculate_enhanced_composite_schedule( + profiles, my_date_start_range, my_date_end_range, 1, profiles.at(0).chargingSchedule.chargingRateUnit); + + log_me(composite_schedule, my_date_start_range); + + ASSERT_EQ(composite_schedule.chargingSchedulePeriod.size(), 3); + ASSERT_EQ(composite_schedule.duration, 1320); + ASSERT_EQ(composite_schedule.chargingSchedulePeriod.at(0).limit, 19.0); + ASSERT_EQ(composite_schedule.chargingSchedulePeriod.at(1).limit, 2000.0); + ASSERT_EQ(composite_schedule.chargingSchedulePeriod.at(1).startPeriod, 120.0); + ASSERT_EQ(composite_schedule.chargingSchedulePeriod.at(2).limit, 19.0); + ASSERT_EQ(composite_schedule.chargingSchedulePeriod.at(2).startPeriod, 1200.0); +} + +/// +/// A simple test to verify that the generated CompositeSchedule where the start and end times +/// are thin, aka they fall inside a specific ChargingSchedulePeriod's time window +/// +TEST_F(CompositeScheduleTestFixture, CalculateEnhancedCompositeSchedule_ThinTest) { + auto handler = create_smart_charging_handler(1); + + ChargingProfile profile_grid = get_charging_profile_from_file("TxProfile_grid.json"); + std::vector profiles = {profile_grid}; + + const DateTime my_date_start_range = ocpp::DateTime("2024-01-18T18:04:00"); + const DateTime my_date_end_range = ocpp::DateTime("2024-01-18T18:22:00"); + + EVLOG_info << " Start> " << my_date_start_range.to_rfc3339(); + EVLOG_info << " End> " << my_date_end_range.to_rfc3339(); + + auto composite_schedule = handler->calculate_enhanced_composite_schedule( + profiles, my_date_start_range, my_date_end_range, 1, profiles.at(0).chargingSchedule.chargingRateUnit); + + log_me(composite_schedule, my_date_start_range); + + ASSERT_EQ(composite_schedule.chargingSchedulePeriod.size(), 1); + ASSERT_EQ(composite_schedule.duration, 1080); + ASSERT_EQ(composite_schedule.chargingSchedulePeriod.at(0).limit, 19.0); +} + +} // namespace v16 +} // namespace ocpp diff --git a/tests/lib/ocpp/v201/CMakeLists.txt b/tests/lib/ocpp/v201/CMakeLists.txt index 97c2714f4..723c3b43f 100644 --- a/tests/lib/ocpp/v201/CMakeLists.txt +++ b/tests/lib/ocpp/v201/CMakeLists.txt @@ -16,4 +16,10 @@ target_sources(libocpp_unit_tests PRIVATE test_smart_charging_handler.cpp utils_tests.cpp comparators.cpp - test_message_queue.cpp) + test_message_queue.cpp + test_composite_schedule.cpp + test_profile.cpp + smart_charging_test_utils.hpp) + +# Copy the json files used for testing to the destination directory +file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/json DESTINATION ${TEST_PROFILES_LOCATION_V201}) diff --git a/tests/lib/ocpp/v201/json/README.md b/tests/lib/ocpp/v201/json/README.md new file mode 100644 index 000000000..3a988a749 --- /dev/null +++ b/tests/lib/ocpp/v201/json/README.md @@ -0,0 +1,5 @@ + +# Sample Smart Charging Profiles for Testing + +A collection of JSON Profile files used for testing different Smart Charging +scenarios. diff --git a/tests/lib/ocpp/v201/json/baseline/README.md b/tests/lib/ocpp/v201/json/baseline/README.md new file mode 100644 index 000000000..7307d8b80 --- /dev/null +++ b/tests/lib/ocpp/v201/json/baseline/README.md @@ -0,0 +1,19 @@ +# Baseline Profiles + +Profiles created to use as a baseline for testing Composite Schedule scenarios. +The goal was to use actual Profiles from the standard as a baseline for Unit +and Integration Testing via tools such as +[Appenzell](https://github.com/US-JOET/appenzell). + +## Profiles + +`TxProfile_100.json` is based on the example profile on page 240 of the +`OCPP-2.0.1_part2_specification_edition2` document. The only substantial change +has been to change the 3rd `ChargingSchedulePeriod` limit to 12,000 so that it +can be more easily differentiated from the first period with a limit of 11,000. +Since it has a Stack Level of 0, any profile with a higher one will take +precedence. + +`TxProfile_1.json` is based on the example profile on page 241 of the +`OCPP-2.0.1_part2_specification_edition2` document. With a Stack Level of 1, +any overlapping `ChargingSchedulePeriods` will take precedence. diff --git a/tests/lib/ocpp/v201/json/baseline/TxProfile_1.json b/tests/lib/ocpp/v201/json/baseline/TxProfile_1.json new file mode 100644 index 000000000..e9d3a8fea --- /dev/null +++ b/tests/lib/ocpp/v201/json/baseline/TxProfile_1.json @@ -0,0 +1,24 @@ +{ + "id": 1, + "chargingProfilePurpose": "TxProfile", + "chargingProfileKind": "Recurring", + "recurrencyKind": "Daily", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 2000.0, + "numberPhases": 1, + "startPeriod": 0 + } + ], + "duration": 1080, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T18:00:00.000Z" + } + ], + "stackLevel": 1, + "transactionId": "f1522902-1170-416f-8e43-9e3bce28fab7" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/baseline/TxProfile_100.json b/tests/lib/ocpp/v201/json/baseline/TxProfile_100.json new file mode 100644 index 000000000..b98848a1e --- /dev/null +++ b/tests/lib/ocpp/v201/json/baseline/TxProfile_100.json @@ -0,0 +1,34 @@ +{ + "id": 100, + "stackLevel": 0, + "chargingProfilePurpose": "TxProfile", + "chargingProfileKind": "Recurring", + "recurrencyKind": "Daily", + "chargingSchedule": [ + { + "id": 1, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 11000.0, + "numberPhases": 3, + "startPeriod": 0 + }, + { + "limit": 6000.0, + "numberPhases": 3, + "startPeriod": 28800 + }, + { + "limit": 12000.0, + "numberPhases": 3, + "startPeriod": 72000 + } + ], + "duration": 86400, + "minChargingRate": 0.0, + "startSchedule": "2023-01-17T17:00:00.000Z" + } + ], + "transactionId": "f1522902-1170-416f-8e43-9e3bce28fab8" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/case_one/README.md b/tests/lib/ocpp/v201/json/case_one/README.md new file mode 100644 index 000000000..e12d9eb3c --- /dev/null +++ b/tests/lib/ocpp/v201/json/case_one/README.md @@ -0,0 +1,14 @@ +# Case One + +Case one is a variation of the baseline scenario but with `TxProfile_1.json` +being `Absolute` instead of `Recurring`. It was designed to track down a +potential defect that was being tracked in the earlier version of the code. +See [Issue #609](https://github.com/EVerest/libocpp/issues/609). + +The following tests are designed to isolate the issue: + +* K08_CalculateCompositeSchedule_DemoCaseOne_17th +* K08_CalculateCompositeSchedule_DemoCaseOne_19th + +In the first the time window matches both profiles, while in the second it +only matches the `Recurring` profile. diff --git a/tests/lib/ocpp/v201/json/case_one/TxProfile_1.json b/tests/lib/ocpp/v201/json/case_one/TxProfile_1.json new file mode 100644 index 000000000..66584cacd --- /dev/null +++ b/tests/lib/ocpp/v201/json/case_one/TxProfile_1.json @@ -0,0 +1,23 @@ +{ + "id": 1, + "chargingProfileKind": "Absolute", + "chargingProfilePurpose": "TxProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 2000.0, + "numberPhases": 1, + "startPeriod": 0 + } + ], + "duration": 1080, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T18:00:00.000Z" + } + ], + "stackLevel": 1, + "transactionId": "f1522902-1170-416f-8e43-9e3bce28fab7" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/case_one/TxProfile_100.json b/tests/lib/ocpp/v201/json/case_one/TxProfile_100.json new file mode 100644 index 000000000..a6376f1bf --- /dev/null +++ b/tests/lib/ocpp/v201/json/case_one/TxProfile_100.json @@ -0,0 +1,34 @@ +{ + "id": 100, + "stackLevel": 0, + "chargingProfilePurpose": "TxProfile", + "chargingProfileKind": "Recurring", + "recurrencyKind": "Daily", + "chargingSchedule": [ + { + "id": 1, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 11000.0, + "numberPhases": 1, + "startPeriod": 0 + }, + { + "limit": 6000.0, + "numberPhases": 1, + "startPeriod": 28800 + }, + { + "limit": 12000.0, + "numberPhases": 1, + "startPeriod": 72000 + } + ], + "duration": 86400, + "minChargingRate": 0.0, + "startSchedule": "2023-01-17T17:00:00.000Z" + } + ], + "transactionId": "f1522902-1170-416f-8e43-9e3bce28fab7" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/external/ChargingStationExternalConstraintProfile_grid_hourly.json b/tests/lib/ocpp/v201/json/external/ChargingStationExternalConstraintProfile_grid_hourly.json new file mode 100644 index 000000000..84e11590b --- /dev/null +++ b/tests/lib/ocpp/v201/json/external/ChargingStationExternalConstraintProfile_grid_hourly.json @@ -0,0 +1,138 @@ +{ + "id": 24, + "chargingProfileKind": "Recurring", + "chargingProfilePurpose": "ChargingStationExternalConstraints", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 10.0, + "numberPhases": 1, + "startPeriod": 0 + }, + { + "limit": 20.0, + "numberPhases": 1, + "startPeriod": 3600 + }, + { + "limit": 30.0, + "numberPhases": 1, + "startPeriod": 7200 + }, + { + "limit": 40.0, + "numberPhases": 1, + "startPeriod": 10800 + }, + { + "limit": 50.0, + "numberPhases": 1, + "startPeriod": 14400 + }, + { + "limit": 60.0, + "numberPhases": 1, + "startPeriod": 18000 + }, + { + "limit": 70.0, + "numberPhases": 1, + "startPeriod": 21600 + }, + { + "limit": 80.0, + "numberPhases": 1, + "startPeriod": 25200 + }, + { + "limit": 90.0, + "numberPhases": 1, + "startPeriod": 28800 + }, + { + "limit": 100.0, + "numberPhases": 1, + "startPeriod": 32400 + }, + { + "limit": 110.0, + "numberPhases": 1, + "startPeriod": 36000 + }, + { + "limit": 120.0, + "numberPhases": 1, + "startPeriod": 39600 + }, + { + "limit": 130.0, + "numberPhases": 1, + "startPeriod": 43200 + }, + { + "limit": 140.0, + "numberPhases": 1, + "startPeriod": 46800 + }, + { + "limit": 150.0, + "numberPhases": 1, + "startPeriod": 50400 + }, + { + "limit": 160.0, + "numberPhases": 1, + "startPeriod": 54000 + }, + { + "limit": 170.0, + "numberPhases": 1, + "startPeriod": 57600 + }, + { + "limit": 180.0, + "numberPhases": 1, + "startPeriod": 61200 + }, + { + "limit": 190.0, + "numberPhases": 1, + "startPeriod": 64800 + }, + { + "limit": 200.0, + "numberPhases": 1, + "startPeriod": 68400 + }, + { + "limit": 210.0, + "numberPhases": 1, + "startPeriod": 72000 + }, + { + "limit": 220.0, + "numberPhases": 1, + "startPeriod": 75600 + }, + { + "limit": 230.0, + "numberPhases": 1, + "startPeriod": 79200 + }, + { + "limit": 240.0, + "numberPhases": 1, + "startPeriod": 82800 + } + ], + "duration": 86400, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T00:00:00.000Z" + } + ], + "recurrencyKind": "Daily", + "stackLevel": 0 +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/external/ChargingStationMaxProfile_2000.json b/tests/lib/ocpp/v201/json/external/ChargingStationMaxProfile_2000.json new file mode 100644 index 000000000..f1e988b01 --- /dev/null +++ b/tests/lib/ocpp/v201/json/external/ChargingStationMaxProfile_2000.json @@ -0,0 +1,22 @@ +{ + "id": 2000, + "chargingProfileKind": "Absolute", + "chargingProfilePurpose": "ChargingStationMaxProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 2000.0, + "numberPhases": 1, + "startPeriod": 0 + } + ], + "duration": 3600, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T00:00:00.000Z" + } + ], + "stackLevel": 0 +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/external/README.md b/tests/lib/ocpp/v201/json/external/README.md new file mode 100644 index 000000000..70a03abe3 --- /dev/null +++ b/tests/lib/ocpp/v201/json/external/README.md @@ -0,0 +1,8 @@ +# External + +This scenario layers TxProfiles on top of a ChargingStationExternalConstraints that has limits for all 24 hours. + +Used by: + +* `K08_CalculateCompositeSchedule_ExternalOverridesHigherLimits` +* `K08_CalculateCompositeSchedule_ExternalOverridenByLowerLimits` diff --git a/tests/lib/ocpp/v201/json/external/TXProfile_2001.json b/tests/lib/ocpp/v201/json/external/TXProfile_2001.json new file mode 100644 index 000000000..80a2a278c --- /dev/null +++ b/tests/lib/ocpp/v201/json/external/TXProfile_2001.json @@ -0,0 +1,23 @@ +{ + "id": 2001, + "chargingProfileKind": "Absolute", + "chargingProfilePurpose": "TxProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 10.0, + "numberPhases": 1, + "startPeriod": 0 + } + ], + "duration": 3600, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T23:00:00.000Z" + } + ], + "stackLevel": 1, + "transactionId": "fe380033-249d-4690-8dc0-f0a0d7842769" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/grid/README.md b/tests/lib/ocpp/v201/json/grid/README.md new file mode 100644 index 000000000..6d626e463 --- /dev/null +++ b/tests/lib/ocpp/v201/json/grid/README.md @@ -0,0 +1,7 @@ +# Grid Profile + +This is a foundational Profile designed to act as a sort of temporal grid paper +for testing out different Profile combinations. Each charging schedule period +is exactly 1 hour apart, with the limit increasing by 1 every hour. Since it +has a Stack Level of 0, any Profile with a higher Stack Level will take +precidence over it. diff --git a/tests/lib/ocpp/v201/json/grid/TxProfile_grid_hourly.json b/tests/lib/ocpp/v201/json/grid/TxProfile_grid_hourly.json new file mode 100644 index 000000000..eede45790 --- /dev/null +++ b/tests/lib/ocpp/v201/json/grid/TxProfile_grid_hourly.json @@ -0,0 +1,139 @@ +{ + "id": 24, + "chargingProfileKind": "Recurring", + "chargingProfilePurpose": "TxProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 1.0, + "numberPhases": 1, + "startPeriod": 0 + }, + { + "limit": 2.0, + "numberPhases": 1, + "startPeriod": 3600 + }, + { + "limit": 3.0, + "numberPhases": 1, + "startPeriod": 7200 + }, + { + "limit": 4.0, + "numberPhases": 1, + "startPeriod": 10800 + }, + { + "limit": 5.0, + "numberPhases": 1, + "startPeriod": 14400 + }, + { + "limit": 6.0, + "numberPhases": 1, + "startPeriod": 18000 + }, + { + "limit": 7.0, + "numberPhases": 1, + "startPeriod": 21600 + }, + { + "limit": 8.0, + "numberPhases": 1, + "startPeriod": 25200 + }, + { + "limit": 9.0, + "numberPhases": 1, + "startPeriod": 28800 + }, + { + "limit": 10.0, + "numberPhases": 1, + "startPeriod": 32400 + }, + { + "limit": 11.0, + "numberPhases": 1, + "startPeriod": 36000 + }, + { + "limit": 12.0, + "numberPhases": 1, + "startPeriod": 39600 + }, + { + "limit": 13.0, + "numberPhases": 1, + "startPeriod": 43200 + }, + { + "limit": 14.0, + "numberPhases": 1, + "startPeriod": 46800 + }, + { + "limit": 15.0, + "numberPhases": 1, + "startPeriod": 50400 + }, + { + "limit": 16.0, + "numberPhases": 1, + "startPeriod": 54000 + }, + { + "limit": 17.0, + "numberPhases": 1, + "startPeriod": 57600 + }, + { + "limit": 18.0, + "numberPhases": 1, + "startPeriod": 61200 + }, + { + "limit": 19.0, + "numberPhases": 1, + "startPeriod": 64800 + }, + { + "limit": 20.0, + "numberPhases": 1, + "startPeriod": 68400 + }, + { + "limit": 21.0, + "numberPhases": 1, + "startPeriod": 72000 + }, + { + "limit": 22.0, + "numberPhases": 1, + "startPeriod": 75600 + }, + { + "limit": 23.0, + "numberPhases": 1, + "startPeriod": 79200 + }, + { + "limit": 24.0, + "numberPhases": 1, + "startPeriod": 82800 + } + ], + "duration": 86400, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T00:00:00.000Z" + } + ], + "recurrencyKind": "Daily", + "stackLevel": 0, + "transactionId": "f1522902-1170-416f-8e43-9e3bce28fde7" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/layered/README.md b/tests/lib/ocpp/v201/json/layered/README.md new file mode 100644 index 000000000..df5021a31 --- /dev/null +++ b/tests/lib/ocpp/v201/json/layered/README.md @@ -0,0 +1,9 @@ +# Layered + +This scenario layers an Absolute Profile on top of the Grid Profile in order to +provide opportunities to test how absolute Profiles handle different time +windows. + +Used by: + +* `K08_CalculateCompositeSchedule_LayeredTest_SameStartTime` diff --git a/tests/lib/ocpp/v201/json/layered/TXProfile_single.json b/tests/lib/ocpp/v201/json/layered/TXProfile_single.json new file mode 100644 index 000000000..fc876b7cd --- /dev/null +++ b/tests/lib/ocpp/v201/json/layered/TXProfile_single.json @@ -0,0 +1,23 @@ +{ + "id": 2000, + "chargingProfileKind": "Absolute", + "chargingProfilePurpose": "TxProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 2000.0, + "numberPhases": 1, + "startPeriod": 0 + } + ], + "duration": 1080, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T18:04:00.000Z" + } + ], + "stackLevel": 1, + "transactionId": "f1522902-1170-416f-8e43-9e3bce28fab7" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/layered/TxProfile_grid_hourly.json b/tests/lib/ocpp/v201/json/layered/TxProfile_grid_hourly.json new file mode 100644 index 000000000..eede45790 --- /dev/null +++ b/tests/lib/ocpp/v201/json/layered/TxProfile_grid_hourly.json @@ -0,0 +1,139 @@ +{ + "id": 24, + "chargingProfileKind": "Recurring", + "chargingProfilePurpose": "TxProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 1.0, + "numberPhases": 1, + "startPeriod": 0 + }, + { + "limit": 2.0, + "numberPhases": 1, + "startPeriod": 3600 + }, + { + "limit": 3.0, + "numberPhases": 1, + "startPeriod": 7200 + }, + { + "limit": 4.0, + "numberPhases": 1, + "startPeriod": 10800 + }, + { + "limit": 5.0, + "numberPhases": 1, + "startPeriod": 14400 + }, + { + "limit": 6.0, + "numberPhases": 1, + "startPeriod": 18000 + }, + { + "limit": 7.0, + "numberPhases": 1, + "startPeriod": 21600 + }, + { + "limit": 8.0, + "numberPhases": 1, + "startPeriod": 25200 + }, + { + "limit": 9.0, + "numberPhases": 1, + "startPeriod": 28800 + }, + { + "limit": 10.0, + "numberPhases": 1, + "startPeriod": 32400 + }, + { + "limit": 11.0, + "numberPhases": 1, + "startPeriod": 36000 + }, + { + "limit": 12.0, + "numberPhases": 1, + "startPeriod": 39600 + }, + { + "limit": 13.0, + "numberPhases": 1, + "startPeriod": 43200 + }, + { + "limit": 14.0, + "numberPhases": 1, + "startPeriod": 46800 + }, + { + "limit": 15.0, + "numberPhases": 1, + "startPeriod": 50400 + }, + { + "limit": 16.0, + "numberPhases": 1, + "startPeriod": 54000 + }, + { + "limit": 17.0, + "numberPhases": 1, + "startPeriod": 57600 + }, + { + "limit": 18.0, + "numberPhases": 1, + "startPeriod": 61200 + }, + { + "limit": 19.0, + "numberPhases": 1, + "startPeriod": 64800 + }, + { + "limit": 20.0, + "numberPhases": 1, + "startPeriod": 68400 + }, + { + "limit": 21.0, + "numberPhases": 1, + "startPeriod": 72000 + }, + { + "limit": 22.0, + "numberPhases": 1, + "startPeriod": 75600 + }, + { + "limit": 23.0, + "numberPhases": 1, + "startPeriod": 79200 + }, + { + "limit": 24.0, + "numberPhases": 1, + "startPeriod": 82800 + } + ], + "duration": 86400, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T00:00:00.000Z" + } + ], + "recurrencyKind": "Daily", + "stackLevel": 0, + "transactionId": "f1522902-1170-416f-8e43-9e3bce28fde7" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/layered_recurring/README.md b/tests/lib/ocpp/v201/json/layered_recurring/README.md new file mode 100644 index 000000000..a1c0a4096 --- /dev/null +++ b/tests/lib/ocpp/v201/json/layered_recurring/README.md @@ -0,0 +1,8 @@ +# Layered Recurring + +This scenario matches Layered except now the higher Profile is recurring. + +Used by: + +* `K08_CalculateCompositeSchedule_LayeredRecurringTest_PreviousStartTime` +* `K08_CalculateCompositeSchedule_LayeredRecurringTest_FutureStartTime` diff --git a/tests/lib/ocpp/v201/json/layered_recurring/TXProfile_single.json b/tests/lib/ocpp/v201/json/layered_recurring/TXProfile_single.json new file mode 100644 index 000000000..abab1f235 --- /dev/null +++ b/tests/lib/ocpp/v201/json/layered_recurring/TXProfile_single.json @@ -0,0 +1,24 @@ +{ + "id": 2000, + "chargingProfileKind": "Recurring", + "chargingProfilePurpose": "TxProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 2000.0, + "numberPhases": 1, + "startPeriod": 0 + } + ], + "duration": 1080, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T18:04:00.000Z" + } + ], + "recurrencyKind": "Daily", + "stackLevel": 1, + "transactionId": "f1522902-1170-416f-8e43-9e3bce28fab7" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/layered_recurring/TxProfile_grid_hourly.json b/tests/lib/ocpp/v201/json/layered_recurring/TxProfile_grid_hourly.json new file mode 100644 index 000000000..eede45790 --- /dev/null +++ b/tests/lib/ocpp/v201/json/layered_recurring/TxProfile_grid_hourly.json @@ -0,0 +1,139 @@ +{ + "id": 24, + "chargingProfileKind": "Recurring", + "chargingProfilePurpose": "TxProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 1.0, + "numberPhases": 1, + "startPeriod": 0 + }, + { + "limit": 2.0, + "numberPhases": 1, + "startPeriod": 3600 + }, + { + "limit": 3.0, + "numberPhases": 1, + "startPeriod": 7200 + }, + { + "limit": 4.0, + "numberPhases": 1, + "startPeriod": 10800 + }, + { + "limit": 5.0, + "numberPhases": 1, + "startPeriod": 14400 + }, + { + "limit": 6.0, + "numberPhases": 1, + "startPeriod": 18000 + }, + { + "limit": 7.0, + "numberPhases": 1, + "startPeriod": 21600 + }, + { + "limit": 8.0, + "numberPhases": 1, + "startPeriod": 25200 + }, + { + "limit": 9.0, + "numberPhases": 1, + "startPeriod": 28800 + }, + { + "limit": 10.0, + "numberPhases": 1, + "startPeriod": 32400 + }, + { + "limit": 11.0, + "numberPhases": 1, + "startPeriod": 36000 + }, + { + "limit": 12.0, + "numberPhases": 1, + "startPeriod": 39600 + }, + { + "limit": 13.0, + "numberPhases": 1, + "startPeriod": 43200 + }, + { + "limit": 14.0, + "numberPhases": 1, + "startPeriod": 46800 + }, + { + "limit": 15.0, + "numberPhases": 1, + "startPeriod": 50400 + }, + { + "limit": 16.0, + "numberPhases": 1, + "startPeriod": 54000 + }, + { + "limit": 17.0, + "numberPhases": 1, + "startPeriod": 57600 + }, + { + "limit": 18.0, + "numberPhases": 1, + "startPeriod": 61200 + }, + { + "limit": 19.0, + "numberPhases": 1, + "startPeriod": 64800 + }, + { + "limit": 20.0, + "numberPhases": 1, + "startPeriod": 68400 + }, + { + "limit": 21.0, + "numberPhases": 1, + "startPeriod": 72000 + }, + { + "limit": 22.0, + "numberPhases": 1, + "startPeriod": 75600 + }, + { + "limit": 23.0, + "numberPhases": 1, + "startPeriod": 79200 + }, + { + "limit": 24.0, + "numberPhases": 1, + "startPeriod": 82800 + } + ], + "duration": 86400, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T00:00:00.000Z" + } + ], + "recurrencyKind": "Daily", + "stackLevel": 0, + "transactionId": "f1522902-1170-416f-8e43-9e3bce28fde7" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/max/ChargingStationMaxProfile_grid_hourly.json b/tests/lib/ocpp/v201/json/max/ChargingStationMaxProfile_grid_hourly.json new file mode 100644 index 000000000..d68146e22 --- /dev/null +++ b/tests/lib/ocpp/v201/json/max/ChargingStationMaxProfile_grid_hourly.json @@ -0,0 +1,138 @@ +{ + "id": 24, + "chargingProfileKind": "Recurring", + "chargingProfilePurpose": "ChargingStationMaxProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 10.0, + "numberPhases": 1, + "startPeriod": 0 + }, + { + "limit": 20.0, + "numberPhases": 1, + "startPeriod": 3600 + }, + { + "limit": 30.0, + "numberPhases": 1, + "startPeriod": 7200 + }, + { + "limit": 40.0, + "numberPhases": 1, + "startPeriod": 10800 + }, + { + "limit": 50.0, + "numberPhases": 1, + "startPeriod": 14400 + }, + { + "limit": 60.0, + "numberPhases": 1, + "startPeriod": 18000 + }, + { + "limit": 70.0, + "numberPhases": 1, + "startPeriod": 21600 + }, + { + "limit": 80.0, + "numberPhases": 1, + "startPeriod": 25200 + }, + { + "limit": 90.0, + "numberPhases": 1, + "startPeriod": 28800 + }, + { + "limit": 100.0, + "numberPhases": 1, + "startPeriod": 32400 + }, + { + "limit": 110.0, + "numberPhases": 1, + "startPeriod": 36000 + }, + { + "limit": 120.0, + "numberPhases": 1, + "startPeriod": 39600 + }, + { + "limit": 130.0, + "numberPhases": 1, + "startPeriod": 43200 + }, + { + "limit": 140.0, + "numberPhases": 1, + "startPeriod": 46800 + }, + { + "limit": 150.0, + "numberPhases": 1, + "startPeriod": 50400 + }, + { + "limit": 160.0, + "numberPhases": 1, + "startPeriod": 54000 + }, + { + "limit": 170.0, + "numberPhases": 1, + "startPeriod": 57600 + }, + { + "limit": 180.0, + "numberPhases": 1, + "startPeriod": 61200 + }, + { + "limit": 190.0, + "numberPhases": 1, + "startPeriod": 64800 + }, + { + "limit": 200.0, + "numberPhases": 1, + "startPeriod": 68400 + }, + { + "limit": 210.0, + "numberPhases": 1, + "startPeriod": 72000 + }, + { + "limit": 220.0, + "numberPhases": 1, + "startPeriod": 75600 + }, + { + "limit": 230.0, + "numberPhases": 1, + "startPeriod": 79200 + }, + { + "limit": 240.0, + "numberPhases": 1, + "startPeriod": 82800 + } + ], + "duration": 86400, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T00:00:00.000Z" + } + ], + "recurrencyKind": "Daily", + "stackLevel": 0 +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/max/README.md b/tests/lib/ocpp/v201/json/max/README.md new file mode 100644 index 000000000..add1dc302 --- /dev/null +++ b/tests/lib/ocpp/v201/json/max/README.md @@ -0,0 +1,9 @@ +# Max + +This scenario layers TxProfiles on top of a ChargingStationMaxProfile that has +limits for all 24 hours. + +Used by: + +* `K08_CalculateCompositesSchedule_MaxOverridesHigherLimits` +* `K08_CalculateCompositeSchedule_MaxOverridenByLowerLimits` diff --git a/tests/lib/ocpp/v201/json/max/TXProfile_2000.json b/tests/lib/ocpp/v201/json/max/TXProfile_2000.json new file mode 100644 index 000000000..fcbe22a3a --- /dev/null +++ b/tests/lib/ocpp/v201/json/max/TXProfile_2000.json @@ -0,0 +1,23 @@ +{ + "id": 2000, + "chargingProfileKind": "Absolute", + "chargingProfilePurpose": "TxProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 2000.0, + "numberPhases": 1, + "startPeriod": 0 + } + ], + "duration": 3600, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T00:00:00.000Z" + } + ], + "stackLevel": 0, + "transactionId": "fe380033-249d-4690-8dc0-f0a0d7842769" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/max/TXProfile_2001.json b/tests/lib/ocpp/v201/json/max/TXProfile_2001.json new file mode 100644 index 000000000..80a2a278c --- /dev/null +++ b/tests/lib/ocpp/v201/json/max/TXProfile_2001.json @@ -0,0 +1,23 @@ +{ + "id": 2001, + "chargingProfileKind": "Absolute", + "chargingProfilePurpose": "TxProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 10.0, + "numberPhases": 1, + "startPeriod": 0 + } + ], + "duration": 3600, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T23:00:00.000Z" + } + ], + "stackLevel": 1, + "transactionId": "fe380033-249d-4690-8dc0-f0a0d7842769" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/relative/README.md b/tests/lib/ocpp/v201/json/relative/README.md new file mode 100644 index 000000000..bcb7bebad --- /dev/null +++ b/tests/lib/ocpp/v201/json/relative/README.md @@ -0,0 +1,8 @@ +# Layered Relative + +This scenario matches Layered except now the higher Profile is Relative. + +Used by: + +* `K08_CalculateCompositeSchedule_RelativeProfile_minutia` +* `K08_CalculateCompositeSchedule_RelativeProfile_e2e` diff --git a/tests/lib/ocpp/v201/json/relative/TxProfile_grid_hourly.json b/tests/lib/ocpp/v201/json/relative/TxProfile_grid_hourly.json new file mode 100644 index 000000000..eede45790 --- /dev/null +++ b/tests/lib/ocpp/v201/json/relative/TxProfile_grid_hourly.json @@ -0,0 +1,139 @@ +{ + "id": 24, + "chargingProfileKind": "Recurring", + "chargingProfilePurpose": "TxProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 1.0, + "numberPhases": 1, + "startPeriod": 0 + }, + { + "limit": 2.0, + "numberPhases": 1, + "startPeriod": 3600 + }, + { + "limit": 3.0, + "numberPhases": 1, + "startPeriod": 7200 + }, + { + "limit": 4.0, + "numberPhases": 1, + "startPeriod": 10800 + }, + { + "limit": 5.0, + "numberPhases": 1, + "startPeriod": 14400 + }, + { + "limit": 6.0, + "numberPhases": 1, + "startPeriod": 18000 + }, + { + "limit": 7.0, + "numberPhases": 1, + "startPeriod": 21600 + }, + { + "limit": 8.0, + "numberPhases": 1, + "startPeriod": 25200 + }, + { + "limit": 9.0, + "numberPhases": 1, + "startPeriod": 28800 + }, + { + "limit": 10.0, + "numberPhases": 1, + "startPeriod": 32400 + }, + { + "limit": 11.0, + "numberPhases": 1, + "startPeriod": 36000 + }, + { + "limit": 12.0, + "numberPhases": 1, + "startPeriod": 39600 + }, + { + "limit": 13.0, + "numberPhases": 1, + "startPeriod": 43200 + }, + { + "limit": 14.0, + "numberPhases": 1, + "startPeriod": 46800 + }, + { + "limit": 15.0, + "numberPhases": 1, + "startPeriod": 50400 + }, + { + "limit": 16.0, + "numberPhases": 1, + "startPeriod": 54000 + }, + { + "limit": 17.0, + "numberPhases": 1, + "startPeriod": 57600 + }, + { + "limit": 18.0, + "numberPhases": 1, + "startPeriod": 61200 + }, + { + "limit": 19.0, + "numberPhases": 1, + "startPeriod": 64800 + }, + { + "limit": 20.0, + "numberPhases": 1, + "startPeriod": 68400 + }, + { + "limit": 21.0, + "numberPhases": 1, + "startPeriod": 72000 + }, + { + "limit": 22.0, + "numberPhases": 1, + "startPeriod": 75600 + }, + { + "limit": 23.0, + "numberPhases": 1, + "startPeriod": 79200 + }, + { + "limit": 24.0, + "numberPhases": 1, + "startPeriod": 82800 + } + ], + "duration": 86400, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T00:00:00.000Z" + } + ], + "recurrencyKind": "Daily", + "stackLevel": 0, + "transactionId": "f1522902-1170-416f-8e43-9e3bce28fde7" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/relative/TxProfile_relative.json b/tests/lib/ocpp/v201/json/relative/TxProfile_relative.json new file mode 100644 index 000000000..6e4c36ca2 --- /dev/null +++ b/tests/lib/ocpp/v201/json/relative/TxProfile_relative.json @@ -0,0 +1,22 @@ +{ + "id": 66, + "chargingProfileKind": "Relative", + "chargingProfilePurpose": "TxProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 2000.0, + "numberPhases": 1, + "startPeriod": 0 + } + ], + "duration": 3601, + "minChargingRate": 0.0 + } + ], + "stackLevel": 1, + "transactionId": "f1522902-1170-416f-8e43-9e3bce28fde7" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/singles/Absolute_301.json b/tests/lib/ocpp/v201/json/singles/Absolute_301.json new file mode 100644 index 000000000..cc00b3c50 --- /dev/null +++ b/tests/lib/ocpp/v201/json/singles/Absolute_301.json @@ -0,0 +1,30 @@ +{ + "id": 301, + "chargingProfileKind": "Absolute", + "chargingProfilePurpose": "TxDefaultProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "A", + "chargingSchedulePeriod": [ + { + "limit": 32.0, + "startPeriod": 0 + }, + { + "limit": 31.0, + "startPeriod": 1800 + }, + { + "limit": 30.0, + "startPeriod": 2700 + } + ], + "duration": 3600, + "startSchedule": "2024-01-01T12:02:00Z" + } + ], + "stackLevel": 5, + "validFrom": "2024-01-01T12:00:00Z", + "validTo": "2024-01-01T14:00:00Z" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/singles/Absolute_NoDuration_301.json b/tests/lib/ocpp/v201/json/singles/Absolute_NoDuration_301.json new file mode 100644 index 000000000..97de7347c --- /dev/null +++ b/tests/lib/ocpp/v201/json/singles/Absolute_NoDuration_301.json @@ -0,0 +1,29 @@ +{ + "id": 301, + "chargingProfileKind": "Absolute", + "chargingProfilePurpose": "TxDefaultProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "A", + "chargingSchedulePeriod": [ + { + "limit": 32.0, + "startPeriod": 0 + }, + { + "limit": 31.0, + "startPeriod": 1800 + }, + { + "limit": 30.0, + "startPeriod": 2700 + } + ], + "startSchedule": "2024-01-01T12:02:00Z" + } + ], + "stackLevel": 5, + "validFrom": "2024-01-01T12:00:00Z", + "validTo": "2024-01-01T14:00:00Z" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/singles/ProfileA.json b/tests/lib/ocpp/v201/json/singles/ProfileA.json new file mode 100644 index 000000000..5d4d1bce3 --- /dev/null +++ b/tests/lib/ocpp/v201/json/singles/ProfileA.json @@ -0,0 +1,31 @@ +{ + "id": 301, + "chargingProfileKind": "Absolute", + "chargingProfilePurpose": "TxDefaultProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "A", + "chargingSchedulePeriod": [ + { + "limit": 32.0, + "startPeriod": 0 + }, + { + "limit": 31.0, + "startPeriod": 6000 + }, + { + "limit": 30.0, + "startPeriod": 12000 + } + ], + "duration": 3600, + "startSchedule": "2024-01-01T12:02:00Z" + } + ], + "stackLevel": 5, + "validFrom": "2024-01-01T12:00:00Z", + "validTo": "2024-01-01T14:00:00Z", + "transactionId": "g1522902-1170-416f-8e43-9e3bce28fab7" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/singles/README.md b/tests/lib/ocpp/v201/json/singles/README.md new file mode 100644 index 000000000..8c6c4110b --- /dev/null +++ b/tests/lib/ocpp/v201/json/singles/README.md @@ -0,0 +1,31 @@ +# Profiles for tests only requiring a single one + +The following Profiles are all serialized JSON versions of ones instantiated +directly in the original `tests/lib/ocpp/v16/profile_testsA.cpp` OCPP 1.6 +versions of the tests. + +* singles/Absolute_301.json +* singles/Absolute_NoDuration_301.json +* singles/Relative_301.json +* singles/Relative_NoDuration_301.json +* singles/Recurring_Daily_301.json +* singles/Recurring_Daily_NoDuration_301.json +* singles/Recurring_Weekly_301.json +* singles/Recurring_Weekly_NoDuration_301.json + +The goal is to clearly isolate out each profile, and simplify as much as +possible the writing of the tests by leveraging +[GoogleTests's parameter based testing feature](https://google.github.io/googletest/reference/testing.html#TEST_P), +greatly reducing the amount of boiler plate needed for the tests. + +* singles/TXProfile_Absolute_Start18-04.json + +This profile is used for a specific test scenario where any actual Profile +ChargingSchedulePeriod happens after the time window of the request. + +* singles/TxProfile_CONCERNING_overlapping_periods.json + +This is a Profile created with a vector of `ChargingSchedulePeriods` that is +longer in duraction for a single day, but is recurring daily so that they +will start to overlap after 24 hours. The idea is to create a Profile to test +this sort of edge case. Right now there aren't any tests using it. diff --git a/tests/lib/ocpp/v201/json/singles/Recurring_Daily_301.json b/tests/lib/ocpp/v201/json/singles/Recurring_Daily_301.json new file mode 100644 index 000000000..65e7b82bd --- /dev/null +++ b/tests/lib/ocpp/v201/json/singles/Recurring_Daily_301.json @@ -0,0 +1,31 @@ +{ + "id": 301, + "chargingProfileKind": "Recurring", + "chargingProfilePurpose": "TxDefaultProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "A", + "chargingSchedulePeriod": [ + { + "limit": 32.0, + "startPeriod": 0 + }, + { + "limit": 31.0, + "startPeriod": 1800 + }, + { + "limit": 30.0, + "startPeriod": 2700 + } + ], + "duration": 3600, + "startSchedule": "2024-01-01T08:00:00Z" + } + ], + "recurrencyKind": "Daily", + "stackLevel": 5, + "validFrom": "2024-01-01T12:00:00Z", + "validTo": "2024-02-01T12:00:00Z" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/singles/Recurring_Daily_NoDuration_301.json b/tests/lib/ocpp/v201/json/singles/Recurring_Daily_NoDuration_301.json new file mode 100644 index 000000000..cb032651e --- /dev/null +++ b/tests/lib/ocpp/v201/json/singles/Recurring_Daily_NoDuration_301.json @@ -0,0 +1,30 @@ +{ + "id": 301, + "chargingProfileKind": "Recurring", + "chargingProfilePurpose": "TxDefaultProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "A", + "chargingSchedulePeriod": [ + { + "limit": 32.0, + "startPeriod": 0 + }, + { + "limit": 31.0, + "startPeriod": 1800 + }, + { + "limit": 30.0, + "startPeriod": 2700 + } + ], + "startSchedule": "2024-01-01T08:00:00Z" + } + ], + "recurrencyKind": "Daily", + "stackLevel": 5, + "validFrom": "2024-01-01T12:00:00Z", + "validTo": "2024-02-01T12:00:00Z" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/singles/Recurring_Weekly_301.json b/tests/lib/ocpp/v201/json/singles/Recurring_Weekly_301.json new file mode 100644 index 000000000..582d0068c --- /dev/null +++ b/tests/lib/ocpp/v201/json/singles/Recurring_Weekly_301.json @@ -0,0 +1,31 @@ +{ + "id": 301, + "chargingProfileKind": "Recurring", + "chargingProfilePurpose": "TxDefaultProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "A", + "chargingSchedulePeriod": [ + { + "limit": 32.0, + "startPeriod": 0 + }, + { + "limit": 31.0, + "startPeriod": 1800 + }, + { + "limit": 30.0, + "startPeriod": 2700 + } + ], + "duration": 3600, + "startSchedule": "2024-01-03T16:00:00Z" + } + ], + "recurrencyKind": "Weekly", + "stackLevel": 5, + "validFrom": "2024-01-01T12:00:00Z", + "validTo": "2024-02-01T12:00:00Z" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/singles/Recurring_Weekly_NoDuration_301.json b/tests/lib/ocpp/v201/json/singles/Recurring_Weekly_NoDuration_301.json new file mode 100644 index 000000000..26a2954e0 --- /dev/null +++ b/tests/lib/ocpp/v201/json/singles/Recurring_Weekly_NoDuration_301.json @@ -0,0 +1,30 @@ +{ + "id": 301, + "chargingProfileKind": "Recurring", + "chargingProfilePurpose": "TxDefaultProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "A", + "chargingSchedulePeriod": [ + { + "limit": 32.0, + "startPeriod": 0 + }, + { + "limit": 31.0, + "startPeriod": 1800 + }, + { + "limit": 30.0, + "startPeriod": 2700 + } + ], + "startSchedule": "2024-01-03T16:00:00Z" + } + ], + "recurrencyKind": "Weekly", + "stackLevel": 5, + "validFrom": "2024-01-01T12:00:00Z", + "validTo": "2024-02-01T12:00:00Z" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/singles/Relative_301.json b/tests/lib/ocpp/v201/json/singles/Relative_301.json new file mode 100644 index 000000000..e60b2837d --- /dev/null +++ b/tests/lib/ocpp/v201/json/singles/Relative_301.json @@ -0,0 +1,29 @@ +{ + "id": 301, + "chargingProfileKind": "Relative", + "chargingProfilePurpose": "TxDefaultProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "A", + "chargingSchedulePeriod": [ + { + "limit": 32.0, + "startPeriod": 0 + }, + { + "limit": 31.0, + "startPeriod": 1800 + }, + { + "limit": 30.0, + "startPeriod": 2700 + } + ], + "duration": 3600 + } + ], + "stackLevel": 5, + "validFrom": "2024-01-01T12:00:00Z", + "validTo": "2024-01-01T14:00:00Z" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/singles/Relative_MultipleChargingSchedules.json b/tests/lib/ocpp/v201/json/singles/Relative_MultipleChargingSchedules.json new file mode 100644 index 000000000..f53534271 --- /dev/null +++ b/tests/lib/ocpp/v201/json/singles/Relative_MultipleChargingSchedules.json @@ -0,0 +1,58 @@ +{ + "id": 66, + "chargingProfileKind": "Relative", + "chargingProfilePurpose": "TxProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 11.0, + "numberPhases": 1, + "startPeriod": 0 + }, + { + "limit": 12.0, + "numberPhases": 1, + "startPeriod": 10 + } + ], + "duration": 19, + "minChargingRate": 0.0 + }, + { + "id": 1, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 31.0, + "numberPhases": 1, + "startPeriod": 0 + }, + { + "limit": 31.0, + "numberPhases": 1, + "startPeriod": 8 + } + ], + "duration": 37, + "minChargingRate": 0.0 + }, + { + "id": 2, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 71.0, + "numberPhases": 1, + "startPeriod": 0 + } + ], + "duration": 79, + "minChargingRate": 0.0 + } + ], + "stackLevel": 1, + "transactionId": "g1522902-1170-416f-8e43-9e3bce28fab7" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/singles/Relative_NoDuration_301.json b/tests/lib/ocpp/v201/json/singles/Relative_NoDuration_301.json new file mode 100644 index 000000000..be5914da4 --- /dev/null +++ b/tests/lib/ocpp/v201/json/singles/Relative_NoDuration_301.json @@ -0,0 +1,28 @@ +{ + "id": 301, + "chargingProfileKind": "Relative", + "chargingProfilePurpose": "TxDefaultProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "A", + "chargingSchedulePeriod": [ + { + "limit": 32.0, + "startPeriod": 0 + }, + { + "limit": 31.0, + "startPeriod": 1800 + }, + { + "limit": 30.0, + "startPeriod": 2700 + } + ] + } + ], + "stackLevel": 5, + "validFrom": "2024-01-01T12:00:00Z", + "validTo": "2024-01-01T14:00:00Z" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/singles/TXProfile_Absolute_Start18-04.json b/tests/lib/ocpp/v201/json/singles/TXProfile_Absolute_Start18-04.json new file mode 100644 index 000000000..fc876b7cd --- /dev/null +++ b/tests/lib/ocpp/v201/json/singles/TXProfile_Absolute_Start18-04.json @@ -0,0 +1,23 @@ +{ + "id": 2000, + "chargingProfileKind": "Absolute", + "chargingProfilePurpose": "TxProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 2000.0, + "numberPhases": 1, + "startPeriod": 0 + } + ], + "duration": 1080, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T18:04:00.000Z" + } + ], + "stackLevel": 1, + "transactionId": "f1522902-1170-416f-8e43-9e3bce28fab7" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/singles/TxProfile_CONCERNING_overlapping_periods.json b/tests/lib/ocpp/v201/json/singles/TxProfile_CONCERNING_overlapping_periods.json new file mode 100644 index 000000000..4dfb504b4 --- /dev/null +++ b/tests/lib/ocpp/v201/json/singles/TxProfile_CONCERNING_overlapping_periods.json @@ -0,0 +1,44 @@ +{ + "id": 100, + "stackLevel": 0, + "chargingProfilePurpose": "TxProfile", + "chargingProfileKind": "Recurring", + "recurrencyKind": "Daily", + "chargingSchedule": [ + { + "id": 1, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 11000.0, + "numberPhases": 3, + "startPeriod": 0 + }, + { + "limit": 6000.0, + "numberPhases": 3, + "startPeriod": 28800 + }, + { + "limit": 12000.0, + "numberPhases": 3, + "startPeriod": 72100 + }, + { + "limit": 12005.0, + "numberPhases": 3, + "startPeriod": 73010 + }, + { + "limit": 12010.0, + "numberPhases": 3, + "startPeriod": 74020 + } + ], + "duration": 86400, + "minChargingRate": 0.0, + "startSchedule": "2023-01-17T17:00:00.000Z" + } + ], + "transactionId": "f1522902-1170-416f-8e43-9e3bce28fab8" +} \ No newline at end of file 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 7a2a13a0c..807c57f11 100644 --- a/tests/lib/ocpp/v201/mocks/smart_charging_handler_mock.hpp +++ b/tests/lib/ocpp/v201/mocks/smart_charging_handler_mock.hpp @@ -17,5 +17,10 @@ class SmartChargingHandlerMock : public SmartChargingHandlerInterface { MOCK_METHOD(ClearChargingProfileResponse, clear_profiles, (const ClearChargingProfileRequest& request), (override)); MOCK_METHOD(std::vector, get_reported_profiles, (const GetChargingProfilesRequest& request), (const, override)); + MOCK_METHOD(std::vector, get_valid_profiles, (int32_t evse_id)); + MOCK_METHOD(CompositeSchedule, calculate_composite_schedule, + (std::vector & valid_profiles, const ocpp::DateTime& start_time, + const ocpp::DateTime& end_time, const int32_t evse_id, + std::optional charging_rate_unit)); }; } // namespace ocpp::v201 diff --git a/tests/lib/ocpp/v201/smart_charging_test_utils.hpp b/tests/lib/ocpp/v201/smart_charging_test_utils.hpp new file mode 100644 index 000000000..5403b86c3 --- /dev/null +++ b/tests/lib/ocpp/v201/smart_charging_test_utils.hpp @@ -0,0 +1,238 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest + +#include "everest/logging.hpp" +#include "ocpp/v201/ocpp_types.hpp" +#include "ocpp/v201/profile.hpp" +#include "ocpp/v201/utils.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace ocpp::v201 { + +static const std::string BASE_JSON_PATH = std::string(TEST_PROFILES_LOCATION_V201) + "/json"; + +inline bool operator==(const ChargingSchedulePeriod& a, const ChargingSchedulePeriod& b) { + auto diff = std::abs(a.startPeriod - b.startPeriod); + bool bRes = diff < 10; // allow for a small difference + bRes = bRes && (a.limit == b.limit); + bRes = bRes && (a.numberPhases == b.numberPhases); + bRes = bRes && (a.phaseToUse == b.phaseToUse); + return bRes; +} + +inline bool operator!=(const ChargingSchedulePeriod& a, const ChargingSchedulePeriod& b) { + return (!(a == b)); +} + +inline bool operator==(const CompositeSchedule& a, const CompositeSchedule& b) { + bool bRes = true; + + if (a.chargingSchedulePeriod.size() != b.chargingSchedulePeriod.size()) { + return false; + } + + for (std::uint32_t i = 0; bRes && i < a.chargingSchedulePeriod.size(); i++) { + bRes = a.chargingSchedulePeriod[i] == b.chargingSchedulePeriod[i]; + } + + bRes = bRes && (a.evseId == b.evseId); + bRes = bRes && (a.duration == b.duration); + bRes = bRes && (a.scheduleStart == b.scheduleStart); + bRes = bRes && (a.chargingRateUnit == b.chargingRateUnit); + + return bRes; +} + +inline bool operator!=(const CompositeSchedule& a, const CompositeSchedule& b) { + return (!(a == b)); +} + +inline bool operator==(const ChargingSchedule& a, const ChargingSchedule& b) { + bool bRes = true; + + if (a.chargingSchedulePeriod.size() != b.chargingSchedulePeriod.size()) { + return false; + } + + for (std::uint32_t i = 0; bRes && i < a.chargingSchedulePeriod.size(); i++) { + bRes = a.chargingSchedulePeriod[i] == b.chargingSchedulePeriod[i]; + } + + bRes = bRes && (a.chargingRateUnit == b.chargingRateUnit); + bRes = bRes && (a.startSchedule == b.startSchedule); + bRes = bRes && (a.duration == b.duration); + bRes = bRes && (a.minChargingRate == b.minChargingRate); + + return bRes; +} + +inline bool operator!=(const ChargingSchedule& a, const ChargingSchedule& b) { + return !(a == b); +} + +inline bool operator==(const period_entry_t& a, const period_entry_t& b) { + bool bRes = (a.start == b.start) && (a.end == b.end) && (a.limit == b.limit) && (a.stack_level == b.stack_level) && + (a.charging_rate_unit == b.charging_rate_unit); + if (a.number_phases && b.number_phases) { + bRes = bRes && a.number_phases.value() == b.number_phases.value(); + } + if (a.min_charging_rate && b.min_charging_rate) { + bRes = bRes && a.min_charging_rate.value() == b.min_charging_rate.value(); + } + return bRes; +} + +inline bool operator!=(const period_entry_t& a, const period_entry_t& b) { + return !(a == b); +} + +inline bool operator==(const std::vector& a, const std::vector& b) { + bool bRes = a.size() == b.size(); + if (bRes) { + for (std::uint8_t i = 0; i < a.size(); i++) { + bRes = a[i] == b[i]; + if (!bRes) { + break; + } + } + } + return bRes; +} + +inline std::string to_string(const period_entry_t& entry) { + std::string result = "Period Entry: {"; + result += "Start: " + entry.start.to_rfc3339() + ", "; + result += "End: " + entry.end.to_rfc3339() + ", "; + result += "Limit: " + std::to_string(entry.limit) + ", "; + if (entry.number_phases.has_value()) { + result += "Number of Phases: " + std::to_string(entry.number_phases.value()) + ", "; + } + result += "Stack Level: " + std::to_string(entry.stack_level) + ", "; + result += "ChargingRateUnit:" + conversions::charging_rate_unit_enum_to_string(entry.charging_rate_unit); + + if (entry.min_charging_rate.has_value()) { + result += ", Min Charging Rate: " + std::to_string(entry.min_charging_rate.value()); + } + + result += "}"; + return result; +} + +inline std::ostream& operator<<(std::ostream& os, const period_entry_t& entry) { + os << to_string(entry); + return os; +} + +static ocpp::DateTime dt(const std::string& dt_string) { + ocpp::DateTime dt; + + if (dt_string.length() == 4) { + dt = ocpp::DateTime("2024-01-01T0" + dt_string + ":00Z"); + } else if (dt_string.length() == 5) { + dt = ocpp::DateTime("2024-01-01T" + dt_string + ":00Z"); + } else if (dt_string.length() == 7) { + dt = ocpp::DateTime("2024-01-0" + dt_string + ":00Z"); + } else if (dt_string.length() == 8) { + dt = ocpp::DateTime("2024-01-" + dt_string + ":00Z"); + } else if (dt_string.length() == 11) { + dt = ocpp::DateTime("2024-" + dt_string + ":00Z"); + } else if (dt_string.length() == 16) { + dt = ocpp::DateTime(dt_string + ":00Z"); + } + + return dt; +} + +class SmartChargingTestUtils { +public: + static std::vector get_charging_profiles_from_directory(const std::string& path) { + EVLOG_debug << "get_charging_profiles_from_directory: " << path; + std::vector profiles; + for (const auto& entry : fs::directory_iterator(path)) { + if (!entry.is_directory()) { + fs::path path = entry.path(); + if (path.extension() == ".json") { + ChargingProfile profile = get_charging_profile_from_path(path); + std::cout << path << std::endl; + profiles.push_back(profile); + } + } + } + + // Sort profiles by id in ascending order + std::sort(profiles.begin(), profiles.end(), + [](const ChargingProfile& a, const ChargingProfile& b) { return a.id < b.id; }); + + EVLOG_debug << "get_charging_profiles_from_directory END"; + return profiles; + } + + static ChargingProfile get_charging_profile_from_path(const std::string& path) { + EVLOG_debug << "get_charging_profile_from_path: " << path; + std::ifstream f(path.c_str()); + json data = json::parse(f); + + ChargingProfile cp; + from_json(data, cp); + return cp; + } + + static ChargingProfile get_charging_profile_from_file(const std::string& filename) { + const std::string full_path = BASE_JSON_PATH + "/" + filename; + + return get_charging_profile_from_path(full_path); + } + + static std::vector get_charging_profiles_from_file(const std::string& filename) { + std::vector profiles; + profiles.push_back(get_charging_profile_from_file(filename)); + return profiles; + } + + /// \brief Returns a vector of ChargingProfiles to be used as a baseline for testing core functionality + /// of generating an EnhancedChargingSchedule. + static std::vector get_baseline_profile_vector() { + return get_charging_profiles_from_directory(BASE_JSON_PATH + "/" + "baseline/"); + } + + static std::string to_string(std::vector& profiles) { + std::string s; + json cp_json; + for (auto& profile : profiles) { + if (!s.empty()) + s += ", "; + to_json(cp_json, profile); + s += cp_json.dump(4); + } + + return "[" + s + "]"; + } + + /// \brief Validates that there is no overlap in the submitted period_entry_t collection + /// \param period_entry_t collection + /// \note If there are any overlapping period_entry_t entries the function returns false + static bool validate_profile_result(const std::vector& result) { + bool bRes{true}; + DateTime last{"1900-01-01T00:00:00Z"}; + for (const auto& i : result) { + // ensure no overlaps + bRes = i.start < i.end; + bRes = bRes && i.start >= last; + last = i.end; + if (!bRes) { + break; + } + } + return bRes; + } +}; + +} // namespace ocpp::v201 \ No newline at end of file diff --git a/tests/lib/ocpp/v201/test_charge_point.cpp b/tests/lib/ocpp/v201/test_charge_point.cpp index 6172041e3..ba5faab65 100644 --- a/tests/lib/ocpp/v201/test_charge_point.cpp +++ b/tests/lib/ocpp/v201/test_charge_point.cpp @@ -1,3 +1,6 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest + #include "comparators.hpp" #include "everest/logging.hpp" #include "evse_security_mock.hpp" @@ -7,6 +10,7 @@ #include "ocpp/v201/charge_point.hpp" #include "ocpp/v201/device_model_storage_sqlite.hpp" #include "ocpp/v201/init_device_model_db.hpp" +#include "ocpp/v201/messages/GetCompositeSchedule.hpp" #include "ocpp/v201/messages/SetChargingProfile.hpp" #include "ocpp/v201/ocpp_enums.hpp" #include "ocpp/v201/smart_charging.hpp" @@ -788,4 +792,77 @@ TEST_F(ChargePointFixture, K01FR29_SmartChargingCtrlrAvailableIsTrue_CallsValida charge_point->handle_message(set_charging_profile_req); } +TEST_F(ChargePointFixture, K08_GetCompositeSchedule_CallsCalculateGetCompositeSchedule) { + GetCompositeScheduleRequest req; + req.evseId = DEFAULT_EVSE_ID; + req.chargingRateUnit = ChargingRateUnitEnum::W; + + auto get_composite_schedule_req = + request_to_enhanced_message(req); + + EXPECT_CALL(*smart_charging_handler, calculate_composite_schedule(testing::_, testing::_, testing::_, + DEFAULT_EVSE_ID, req.chargingRateUnit)); + + charge_point->handle_message(get_composite_schedule_req); +} + +TEST_F(ChargePointFixture, K08_GetCompositeSchedule_CallsCalculateGetCompositeScheduleWithValidProfiles) { + GetCompositeScheduleRequest req; + req.evseId = DEFAULT_EVSE_ID; + req.chargingRateUnit = ChargingRateUnitEnum::W; + + auto get_composite_schedule_req = + request_to_enhanced_message(req); + + std::vector profiles = { + create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A, + create_charging_schedule_periods({0, 1, 2}), + ocpp::DateTime("2024-01-17T17:00:00")), + DEFAULT_TX_ID), + }; + + ON_CALL(*smart_charging_handler, get_valid_profiles(DEFAULT_EVSE_ID)).WillByDefault(testing::Return(profiles)); + EXPECT_CALL(*smart_charging_handler, + calculate_composite_schedule(profiles, testing::_, testing::_, DEFAULT_EVSE_ID, req.chargingRateUnit)); + + charge_point->handle_message(get_composite_schedule_req); +} + +TEST_F(ChargePointFixture, K08FR05_GetCompositeSchedule_DoesNotCalculateCompositeScheduleForNonexistentEVSE) { + GetCompositeScheduleRequest req; + req.evseId = DEFAULT_EVSE_ID + 3; + req.chargingRateUnit = ChargingRateUnitEnum::W; + + auto get_composite_schedule_req = + request_to_enhanced_message(req); + + EXPECT_CALL(*smart_charging_handler, get_valid_profiles(testing::_)).Times(0); + EXPECT_CALL(*smart_charging_handler, + calculate_composite_schedule(testing::_, testing::_, testing::_, testing::_, testing::_)) + .Times(0); + + charge_point->handle_message(get_composite_schedule_req); +} + +TEST_F(ChargePointFixture, K08FR07_GetCompositeSchedule_DoesNotCalculateCompositeScheduleForIncorrectChargingRateUnit) { + GetCompositeScheduleRequest req; + req.evseId = DEFAULT_EVSE_ID; + req.chargingRateUnit = ChargingRateUnitEnum::W; + + auto get_composite_schedule_req = + request_to_enhanced_message(req); + + 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", "test", true); + + EXPECT_CALL(*smart_charging_handler, get_valid_profiles(testing::_)).Times(0); + EXPECT_CALL(*smart_charging_handler, + calculate_composite_schedule(testing::_, testing::_, testing::_, testing::_, testing::_)) + .Times(0); + + charge_point->handle_message(get_composite_schedule_req); +} + } // namespace ocpp::v201 diff --git a/tests/lib/ocpp/v201/test_component_state_manager.cpp b/tests/lib/ocpp/v201/test_component_state_manager.cpp index d4a17f4d0..f8b4cda51 100644 --- a/tests/lib/ocpp/v201/test_component_state_manager.cpp +++ b/tests/lib/ocpp/v201/test_component_state_manager.cpp @@ -67,6 +67,7 @@ class MockCallbacks { class ComponentStateManagerTest : public ::testing::Test { protected: void SetUp() override { + testing::FLAGS_gmock_verbose = "error"; } ComponentStateManager component_state_manager(std::shared_ptr database, diff --git a/tests/lib/ocpp/v201/test_composite_schedule.cpp b/tests/lib/ocpp/v201/test_composite_schedule.cpp new file mode 100644 index 000000000..d6a0cf8c8 --- /dev/null +++ b/tests/lib/ocpp/v201/test_composite_schedule.cpp @@ -0,0 +1,816 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest + +#include "date/tz.h" +#include "everest/logging.hpp" +#include "lib/ocpp/common/database_testing_utils.hpp" +#include "ocpp/common/constants.hpp" +#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/init_device_model_db.hpp" +#include "ocpp/v201/ocpp_types.hpp" +#include "ocpp/v201/utils.hpp" +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "smart_charging_test_utils.hpp" + +#include +#include + +namespace ocpp::v201 { +static const int NR_OF_EVSES = 1; +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"; +const static std::string MIGRATION_FILES_PATH = "./resources/v201/device_model_migration_files"; +const static std::string SCHEMAS_PATH = "./resources/example_config/v201/component_schemas"; +const static std::string CONFIG_PATH = "./resources/example_config/v201/config.json"; +const static std::string DEVICE_MODEL_DB_IN_MEMORY_PATH = "file::memory:?cache=shared"; + +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; + + using SmartChargingHandler::SmartChargingHandler; +}; + +class ChargepointTestFixtureV201 : public DatabaseTestingUtils { +protected: + void SetUp() override { + } + + void TearDown() override { + } + + ChargingSchedule create_charge_schedule(ChargingRateUnitEnum charging_rate_unit) { + int32_t id; + std::vector charging_schedule_period; + std::optional custom_data; + std::optional start_schedule; + std::optional duration; + std::optional min_charging_rate; + std::optional sales_tariff; + + return ChargingSchedule{ + id, + charging_rate_unit, + charging_schedule_period, + custom_data, + start_schedule, + duration, + min_charging_rate, + sales_tariff, + }; + } + + ChargingSchedule create_charge_schedule(ChargingRateUnitEnum charging_rate_unit, + std::vector charging_schedule_period, + std::optional start_schedule = std::nullopt) { + int32_t id; + std::optional custom_data; + std::optional duration; + std::optional min_charging_rate; + std::optional sales_tariff; + + return ChargingSchedule{ + id, + charging_rate_unit, + charging_schedule_period, + custom_data, + start_schedule, + duration, + min_charging_rate, + sales_tariff, + }; + } + + 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}; + } + + std::vector create_charging_schedule_periods(std::vector start_periods) { + auto charging_schedule_periods = std::vector(); + for (auto start_period : start_periods) { + auto charging_schedule_period = ChargingSchedulePeriod{ + .startPeriod = start_period, + }; + charging_schedule_periods.push_back(charging_schedule_period); + } + + return charging_schedule_periods; + } + + std::vector + create_charging_schedule_periods_with_phases(int32_t start_period, int32_t numberPhases, int32_t phaseToUse) { + auto charging_schedule_period = + ChargingSchedulePeriod{.startPeriod = start_period, .numberPhases = numberPhases, .phaseToUse = phaseToUse}; + + return {charging_schedule_period}; + } + + ChargingProfile + create_charging_profile(int32_t charging_profile_id, ChargingProfilePurposeEnum charging_profile_purpose, + ChargingSchedule charging_schedule, std::optional transaction_id = {}, + ChargingProfileKindEnum charging_profile_kind = ChargingProfileKindEnum::Absolute, + 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, + .stackLevel = stack_level, + .chargingProfilePurpose = charging_profile_purpose, + .chargingProfileKind = charging_profile_kind, + .chargingSchedule = charging_schedules, + .customData = {}, + .recurrencyKind = recurrency_kind, + .validFrom = validFrom, + .validTo = validTo, + .transactionId = transaction_id}; + } + + void create_device_model_db(const std::string& path) { + InitDeviceModelDb db(path, MIGRATION_FILES_PATH); + db.initialize_database(SCHEMAS_PATH, true); + // db.initialize_database(CONFIG_PATH, false); + } + + std::shared_ptr + create_device_model(const std::optional ac_phase_switching_supported = "true") { + create_device_model_db(DEVICE_MODEL_DB_IN_MEMORY_PATH); + auto device_model_storage = std::make_unique(DEVICE_MODEL_DB_IN_MEMORY_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", "test", 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(""), "test", true); + + return device_model; + } + + TestSmartChargingHandler create_smart_charging_handler() { + std::unique_ptr database_connection = + std::make_unique(fs::path("/tmp/ocpp201") / "cp.db"); + std::shared_ptr database_handler = + std::make_shared(std::move(database_connection), MIGRATION_FILES_LOCATION_V201); + database_handler->open_connection(); + return TestSmartChargingHandler(*this->evse_manager, device_model, database_handler); + } + + // Default values used within the tests + std::unique_ptr evse_manager = std::make_unique(NR_OF_EVSES); + + sqlite3* db_handle; + + bool ignore_no_transaction = true; + 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, K08_CalculateCompositeSchedule_FoundationTest_Grid) { + std::vector profiles = + SmartChargingTestUtils::get_charging_profiles_from_directory(BASE_JSON_PATH + "/grid/"); + const DateTime start_time = ocpp::DateTime("2024-01-17T00:00:00"); + const DateTime end_time = ocpp::DateTime("2024-01-18T00:00:00"); + CompositeSchedule expected = { + .chargingSchedulePeriod = {{ + .startPeriod = 0, + .limit = 1.0, + .numberPhases = 1, + }, + { + .startPeriod = 3600, + .limit = 2.0, + .numberPhases = 1, + }, + { + .startPeriod = 7200, + .limit = 3.0, + .numberPhases = 1, + }, + { + .startPeriod = 10800, + .limit = 4.0, + .numberPhases = 1, + }, + { + .startPeriod = 14400, + .limit = 5.0, + .numberPhases = 1, + }, + { + .startPeriod = 18000, + .limit = 6.0, + .numberPhases = 1, + }, + { + .startPeriod = 21600, + .limit = 7.0, + .numberPhases = 1, + }, + { + .startPeriod = 25200, + .limit = 8.0, + .numberPhases = 1, + }, + { + .startPeriod = 28800, + .limit = 9.0, + .numberPhases = 1, + }, + { + .startPeriod = 32400, + .limit = 10.0, + .numberPhases = 1, + }, + { + .startPeriod = 36000, + .limit = 11.0, + .numberPhases = 1, + }, + { + .startPeriod = 39600, + .limit = 12.0, + .numberPhases = 1, + }, + { + .startPeriod = 43200, + .limit = 13.0, + .numberPhases = 1, + }, + { + .startPeriod = 46800, + .limit = 14.0, + .numberPhases = 1, + }, + { + .startPeriod = 50400, + .limit = 15.0, + .numberPhases = 1, + }, + { + .startPeriod = 54000, + .limit = 16.0, + .numberPhases = 1, + }, + { + .startPeriod = 57600, + .limit = 17.0, + .numberPhases = 1, + }, + { + .startPeriod = 61200, + .limit = 18.0, + .numberPhases = 1, + }, + { + .startPeriod = 64800, + .limit = 19.0, + .numberPhases = 1, + }, + { + .startPeriod = 68400, + .limit = 20.0, + .numberPhases = 1, + }, + { + .startPeriod = 72000, + .limit = 21.0, + .numberPhases = 1, + }, + { + .startPeriod = 75600, + .limit = 22.0, + .numberPhases = 1, + }, + { + .startPeriod = 79200, + .limit = 23.0, + .numberPhases = 1, + }, + { + .startPeriod = 82800, + .limit = 24.0, + .numberPhases = 1, + }}, + .evseId = DEFAULT_EVSE_ID, + .duration = 86400, + .scheduleStart = start_time, + .chargingRateUnit = ChargingRateUnitEnum::W, + }; + + CompositeSchedule actual = + handler.calculate_composite_schedule(profiles, start_time, end_time, DEFAULT_EVSE_ID, ChargingRateUnitEnum::W); + + ASSERT_EQ(actual, expected); +} + +TEST_F(ChargepointTestFixtureV201, K08_CalculateCompositeSchedule_LayeredTest_SameStartTime) { + std::vector profiles = + SmartChargingTestUtils::get_charging_profiles_from_directory(BASE_JSON_PATH + "/layered/"); + + // Time Window: START = Stack #1 start time || END = Stack #1 end time + { + const DateTime start_time = ocpp::DateTime("2024-01-18T18:04:00"); + const DateTime end_time = ocpp::DateTime("2024-01-18T18:22:00"); + + CompositeSchedule expected = { + .chargingSchedulePeriod = {{ + .startPeriod = 0, + .limit = 19.0, + .numberPhases = 1, + }}, + .evseId = DEFAULT_EVSE_ID, + .duration = 1080, + .scheduleStart = start_time, + .chargingRateUnit = ChargingRateUnitEnum::W, + }; + + CompositeSchedule actual = handler.calculate_composite_schedule(profiles, start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::W); + + ASSERT_EQ(actual, expected); + } + + // Time Window: START = Stack #1 start time || END = After Stack #1 end time Before next Start #0 start time + { + const DateTime start_time = ocpp::DateTime("2024-01-17T18:04:00"); + const DateTime end_time = ocpp::DateTime("2024-01-17T18:33:00"); + + CompositeSchedule expected = { + .chargingSchedulePeriod = {{ + .startPeriod = 0, + .limit = 2000.0, + .numberPhases = 1, + }, + { + .startPeriod = 1080, + .limit = 19.0, + .numberPhases = 1, + }}, + .evseId = DEFAULT_EVSE_ID, + .duration = 1740, + .scheduleStart = start_time, + .chargingRateUnit = ChargingRateUnitEnum::W, + }; + + CompositeSchedule actual = handler.calculate_composite_schedule(profiles, start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::W); + + ASSERT_EQ(actual, expected); + } + + // Time Window: START = Stack #1 start time || END = After next Start #0 start time + { + const DateTime start_time = ocpp::DateTime("2024-01-17T18:04:00"); + const DateTime end_time = ocpp::DateTime("2024-01-17T19:04:00"); + + CompositeSchedule expected = { + .chargingSchedulePeriod = {{ + .startPeriod = 0, + .limit = 2000.0, + .numberPhases = 1, + }, + { + .startPeriod = 1080, + .limit = 19.0, + .numberPhases = 1, + }, + { + .startPeriod = 3360, + .limit = 20.0, + .numberPhases = 1, + }}, + .evseId = DEFAULT_EVSE_ID, + .duration = 3600, + .scheduleStart = start_time, + .chargingRateUnit = ChargingRateUnitEnum::W, + }; + + CompositeSchedule actual = handler.calculate_composite_schedule(profiles, start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::W); + + ASSERT_EQ(actual, expected); + } +} + +TEST_F(ChargepointTestFixtureV201, K08_CalculateCompositeSchedule_LayeredRecurringTest_FutureStartTime) { + std::vector profiles = + SmartChargingTestUtils::get_charging_profiles_from_directory(BASE_JSON_PATH + "/layered_recurring/"); + + const DateTime start_time = ocpp::DateTime("2024-02-17T18:04:00"); + const DateTime end_time = ocpp::DateTime("2024-02-17T18:05:00"); + + CompositeSchedule expected = { + .chargingSchedulePeriod = {{ + .startPeriod = 0, + .limit = 2000.0, + .numberPhases = 1, + }}, + .evseId = DEFAULT_EVSE_ID, + .duration = 60, + .scheduleStart = start_time, + .chargingRateUnit = ChargingRateUnitEnum::W, + }; + + CompositeSchedule actual = + handler.calculate_composite_schedule(profiles, start_time, end_time, DEFAULT_EVSE_ID, ChargingRateUnitEnum::W); + + ASSERT_EQ(actual, expected); +} + +TEST_F(ChargepointTestFixtureV201, K08_CalculateCompositeSchedule_LayeredTest_PreviousStartTime) { + std::vector profiles = + SmartChargingTestUtils::get_charging_profiles_from_file("singles/TXProfile_Absolute_Start18-04.json"); + const DateTime start_time = ocpp::DateTime("2024-01-17T18:00:00"); + const DateTime end_time = ocpp::DateTime("2024-01-17T18:05:00"); + CompositeSchedule expected = { + .chargingSchedulePeriod = {{ + .startPeriod = 0, + .limit = DEFAULT_LIMIT_WATTS, + .numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES, + }, + { + .startPeriod = 240, + .limit = + profiles.at(0).chargingSchedule.front().chargingSchedulePeriod.front().limit, + .numberPhases = 1, + }}, + .evseId = DEFAULT_EVSE_ID, + .duration = 300, + .scheduleStart = start_time, + .chargingRateUnit = ChargingRateUnitEnum::W, + }; + + CompositeSchedule actual = + handler.calculate_composite_schedule(profiles, start_time, end_time, DEFAULT_EVSE_ID, ChargingRateUnitEnum::W); + + ASSERT_EQ(actual, expected); +} + +TEST_F(ChargepointTestFixtureV201, K08_CalculateCompositeSchedule_LayeredRecurringTest_PreviousStartTime) { + std::vector profiles = + SmartChargingTestUtils::get_charging_profiles_from_directory(BASE_JSON_PATH + "/layered_recurring/"); + const DateTime start_time = ocpp::DateTime("2024-02-19T18:00:00"); + const DateTime end_time = ocpp::DateTime("2024-02-19T19:04:00"); + CompositeSchedule expected = { + .chargingSchedulePeriod = + {{ + .startPeriod = 0, + .limit = 19.0, + .numberPhases = 1, + }, + { + .startPeriod = 240, + .limit = profiles.back().chargingSchedule.front().chargingSchedulePeriod.front().limit, + .numberPhases = 1, + }, + { + .startPeriod = 1320, + .limit = 19.0, + .numberPhases = 1, + }, + { + .startPeriod = 3600, + .limit = 20.0, + .numberPhases = 1, + }}, + .evseId = DEFAULT_EVSE_ID, + .duration = 3840, + .scheduleStart = start_time, + .chargingRateUnit = ChargingRateUnitEnum::W, + }; + + CompositeSchedule actual = + handler.calculate_composite_schedule(profiles, start_time, end_time, DEFAULT_EVSE_ID, ChargingRateUnitEnum::W); + + ASSERT_EQ(actual, expected); +} + +/** + * Calculate Composite Schedule + */ +TEST_F(ChargepointTestFixtureV201, K08_CalculateCompositeSchedule_ValidateBaselineProfileVector) { + const DateTime start_time = ocpp::DateTime("2024-01-17T18:01:00"); + const DateTime end_time = ocpp::DateTime("2024-01-18T06:00:00"); + std::vector profiles = SmartChargingTestUtils::get_baseline_profile_vector(); + CompositeSchedule expected = { + .chargingSchedulePeriod = {{ + .startPeriod = 0, + .limit = 2000.0, + .numberPhases = 1, + }, + { + .startPeriod = 1020, + .limit = 11000.0, + .numberPhases = 3, + }, + { + .startPeriod = 25140, + .limit = 6000.0, + .numberPhases = 3, + }}, + .evseId = DEFAULT_EVSE_ID, + .duration = 43140, + .scheduleStart = start_time, + .chargingRateUnit = ChargingRateUnitEnum::W, + }; + + CompositeSchedule actual = + handler.calculate_composite_schedule(profiles, start_time, end_time, DEFAULT_EVSE_ID, ChargingRateUnitEnum::W); + + ASSERT_EQ(actual, expected); +} + +TEST_F(ChargepointTestFixtureV201, K08_CalculateCompositeSchedule_RelativeProfile_minutia) { + const DateTime start_time = ocpp::DateTime("2024-05-17T05:00:00"); + const DateTime end_time = ocpp::DateTime("2024-05-17T06:00:00"); + + std::vector profiles = + SmartChargingTestUtils::get_charging_profiles_from_directory(BASE_JSON_PATH + "/relative/"); + this->evse_manager->open_transaction(DEFAULT_EVSE_ID, profiles.at(0).transactionId.value()); + + // Doing this in order to avoid mocking system_clock::now() + auto transaction = std::move(this->evse_manager->get_evse(DEFAULT_EVSE_ID).get_transaction()); + + CompositeSchedule expected = { + .chargingSchedulePeriod = {{ + .startPeriod = 0, + .limit = 2000.0, + .numberPhases = 1, + }}, + .evseId = DEFAULT_EVSE_ID, + .duration = 3600, + .scheduleStart = start_time, + .chargingRateUnit = ChargingRateUnitEnum::W, + }; + + CompositeSchedule actual = + handler.calculate_composite_schedule(profiles, start_time, end_time, DEFAULT_EVSE_ID, ChargingRateUnitEnum::W); + + ASSERT_EQ(actual, expected); +} + +TEST_F(ChargepointTestFixtureV201, K08_CalculateCompositeSchedule_RelativeProfile_e2e) { + std::vector profiles = + SmartChargingTestUtils::get_charging_profiles_from_directory(BASE_JSON_PATH + "/relative/"); + this->evse_manager->open_transaction(DEFAULT_EVSE_ID, profiles.at(0).transactionId.value()); + + // Doing this in order to avoid mocking system_clock::now() + auto transaction = std::move(this->evse_manager->get_evse(DEFAULT_EVSE_ID).get_transaction()); + + const DateTime start_time = ocpp::DateTime("2024-05-17T05:00:00"); + const DateTime end_time = ocpp::DateTime("2024-05-17T06:01:00"); + + CompositeSchedule expected = { + .chargingSchedulePeriod = {{ + .startPeriod = 0, + .limit = 2000.0, + .numberPhases = 1, + }, + { + .startPeriod = 3601, + .limit = 7.0, + .numberPhases = 1, + }}, + .evseId = DEFAULT_EVSE_ID, + .duration = 3660, + .scheduleStart = start_time, + .chargingRateUnit = ChargingRateUnitEnum::W, + }; + + CompositeSchedule actual = + handler.calculate_composite_schedule(profiles, start_time, end_time, DEFAULT_EVSE_ID, ChargingRateUnitEnum::W); + + ASSERT_EQ(actual, expected); +} + +TEST_F(ChargepointTestFixtureV201, K08_CalculateCompositeSchedule_DemoCaseOne_17th) { + std::vector profiles = + SmartChargingTestUtils::get_charging_profiles_from_directory(BASE_JSON_PATH + "/case_one/"); + ChargingProfile relative_profile = profiles.front(); + auto transaction_id = relative_profile.transactionId.value(); + this->evse_manager->open_transaction(DEFAULT_EVSE_ID, transaction_id); + const DateTime start_time = ocpp::DateTime("2024-01-17T18:00:00"); + const DateTime end_time = ocpp::DateTime("2024-01-18T06:00:00"); + + CompositeSchedule expected = { + .chargingSchedulePeriod = {{ + .startPeriod = 0, + .limit = 2000.0, + .numberPhases = 1, + }, + { + .startPeriod = 1080, + .limit = 11000.0, + .numberPhases = 1, + }, + { + .startPeriod = 25200, + .limit = 6000.0, + .numberPhases = 1, + }}, + .evseId = DEFAULT_EVSE_ID, + .duration = 43200, + .scheduleStart = start_time, + .chargingRateUnit = ChargingRateUnitEnum::W, + }; + + CompositeSchedule actual = + handler.calculate_composite_schedule(profiles, start_time, end_time, DEFAULT_EVSE_ID, ChargingRateUnitEnum::W); + + ASSERT_EQ(actual, expected); +} + +TEST_F(ChargepointTestFixtureV201, K08_CalculateCompositeSchedule_DemoCaseOne_19th) { + std::vector profiles = + SmartChargingTestUtils::get_charging_profiles_from_directory(BASE_JSON_PATH + "/case_one/"); + ChargingProfile first_profile = profiles.front(); + auto transaction_id = first_profile.transactionId.value(); + this->evse_manager->open_transaction(DEFAULT_EVSE_ID, transaction_id); + const DateTime start_time = ocpp::DateTime("2024-01-19T18:00:00"); + const DateTime end_time = ocpp::DateTime("2024-01-20T06:00:00"); + + CompositeSchedule expected = { + .chargingSchedulePeriod = {{ + .startPeriod = 0, + .limit = 11000.0, + .numberPhases = 1, + }, + { + .startPeriod = 25200, + .limit = 6000.0, + .numberPhases = 1, + }}, + .evseId = DEFAULT_EVSE_ID, + .duration = 43200, + .scheduleStart = start_time, + .chargingRateUnit = ChargingRateUnitEnum::W, + }; + + CompositeSchedule actual = + handler.calculate_composite_schedule(profiles, start_time, end_time, DEFAULT_EVSE_ID, ChargingRateUnitEnum::W); + + ASSERT_EQ(ProfileValidationResultEnum::Valid, handler.validate_profile(profiles.at(0), DEFAULT_EVSE_ID)); + ASSERT_EQ(ProfileValidationResultEnum::Valid, handler.validate_profile(profiles.at(1), DEFAULT_EVSE_ID)); + ASSERT_EQ(actual, expected); +} + +TEST_F(ChargepointTestFixtureV201, K08_CalculateCompositeSchedule_MaxOverridesHigherLimits) { + std::vector profiles = + SmartChargingTestUtils::get_charging_profiles_from_directory(BASE_JSON_PATH + "/max/"); + + const DateTime start_time = ocpp::DateTime("2024-01-17T00:00:00"); + const DateTime end_time = ocpp::DateTime("2024-01-17T02:00:00"); + + CompositeSchedule expected = { + .chargingSchedulePeriod = {{ + .startPeriod = 0, + .limit = 10.0, + .numberPhases = 1, + }, + { + .startPeriod = 3600, + .limit = 20.0, + .numberPhases = 1, + }}, + .evseId = DEFAULT_EVSE_ID, + .duration = 7200, + .scheduleStart = start_time, + .chargingRateUnit = ChargingRateUnitEnum::W, + }; + + CompositeSchedule actual = + handler.calculate_composite_schedule(profiles, start_time, end_time, DEFAULT_EVSE_ID, ChargingRateUnitEnum::W); + ASSERT_EQ(actual, expected); +} + +TEST_F(ChargepointTestFixtureV201, K08_CalculateCompositeSchedule_MaxOverridenByLowerLimits) { + std::vector profiles = + SmartChargingTestUtils::get_charging_profiles_from_directory(BASE_JSON_PATH + "/max/"); + + const DateTime start_time = ocpp::DateTime("2024-01-17T22:00:00"); + const DateTime end_time = ocpp::DateTime("2024-01-18T00:00:00"); + + CompositeSchedule expected = { + .chargingSchedulePeriod = {{ + .startPeriod = 0, + .limit = 230.0, + .numberPhases = 1, + }, + { + .startPeriod = 3600, + .limit = 10.0, + .numberPhases = 1, + }}, + .evseId = DEFAULT_EVSE_ID, + .duration = 7200, + .scheduleStart = start_time, + .chargingRateUnit = ChargingRateUnitEnum::W, + }; + + CompositeSchedule actual = + handler.calculate_composite_schedule(profiles, start_time, end_time, DEFAULT_EVSE_ID, ChargingRateUnitEnum::W); + ASSERT_EQ(actual, expected); +} + +TEST_F(ChargepointTestFixtureV201, K08_CalculateCompositeSchedule_ExternalOverridesHigherLimits) { + std::vector profiles = + SmartChargingTestUtils::get_charging_profiles_from_directory(BASE_JSON_PATH + "/external/"); + + const DateTime start_time = ocpp::DateTime("2024-01-17T00:00:00"); + const DateTime end_time = ocpp::DateTime("2024-01-17T02:00:00"); + + CompositeSchedule expected = { + .chargingSchedulePeriod = {{ + .startPeriod = 0, + .limit = 10.0, + .numberPhases = 1, + }, + { + .startPeriod = 3600, + .limit = 20.0, + .numberPhases = 1, + }}, + .evseId = DEFAULT_EVSE_ID, + .duration = 7200, + .scheduleStart = start_time, + .chargingRateUnit = ChargingRateUnitEnum::W, + }; + + CompositeSchedule actual = + handler.calculate_composite_schedule(profiles, start_time, end_time, DEFAULT_EVSE_ID, ChargingRateUnitEnum::W); + ASSERT_EQ(actual, expected); +} + +TEST_F(ChargepointTestFixtureV201, K08_CalculateCompositeSchedule_ExternalOverridenByLowerLimits) { + std::vector profiles = + SmartChargingTestUtils::get_charging_profiles_from_directory(BASE_JSON_PATH + "/external/"); + + const DateTime start_time = ocpp::DateTime("2024-01-17T22:00:00"); + const DateTime end_time = ocpp::DateTime("2024-01-18T00:00:00"); + + CompositeSchedule expected = { + .chargingSchedulePeriod = {{ + .startPeriod = 0, + .limit = 230.0, + .numberPhases = 1, + }, + { + .startPeriod = 3600, + .limit = 10.0, + .numberPhases = 1, + }}, + .evseId = DEFAULT_EVSE_ID, + .duration = 7200, + .scheduleStart = start_time, + .chargingRateUnit = ChargingRateUnitEnum::W, + }; + + CompositeSchedule actual = + handler.calculate_composite_schedule(profiles, start_time, end_time, DEFAULT_EVSE_ID, ChargingRateUnitEnum::W); + ASSERT_EQ(actual, expected); +} + +} // namespace ocpp::v201 \ No newline at end of file diff --git a/tests/lib/ocpp/v201/test_profile.cpp b/tests/lib/ocpp/v201/test_profile.cpp new file mode 100644 index 000000000..ef83f33a8 --- /dev/null +++ b/tests/lib/ocpp/v201/test_profile.cpp @@ -0,0 +1,1211 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest + +#include +#include +#include +#include +#include +#include + +#include "everest/logging.hpp" +#include "ocpp/common/constants.hpp" +#include "ocpp/common/types.hpp" +#include "ocpp/v201/ocpp_types.hpp" +#include "ocpp/v201/utils.hpp" + +#include "smart_charging_test_utils.hpp" + +namespace { +using namespace ocpp::v201; +using namespace ocpp; +using ocpp::v201::dt; +using std::nullopt; +using std::chrono::minutes; +using std::chrono::seconds; + +period_entry_t gen_pe(ocpp::DateTime start, ocpp::DateTime end, ChargingProfile profile, int period_at) { + return {.start = start, + .end = end, + .limit = profile.chargingSchedule.front().chargingSchedulePeriod[period_at].limit, + .stack_level = profile.stackLevel, + .charging_rate_unit = profile.chargingSchedule.front().chargingRateUnit}; +} + +const ChargingProfile absolute_profile = + SmartChargingTestUtils::get_charging_profile_from_file("singles/Absolute_301.json"); +const ChargingProfile absolute_profile_no_duration = + SmartChargingTestUtils::get_charging_profile_from_file("singles/Absolute_NoDuration_301.json"); +const ChargingProfile relative_profile = + SmartChargingTestUtils::get_charging_profile_from_file("singles/Relative_301.json"); +const ChargingProfile relative_profile_no_duration = + SmartChargingTestUtils::get_charging_profile_from_file("singles/Relative_NoDuration_301.json"); +const ChargingProfile daily_profile = + SmartChargingTestUtils::get_charging_profile_from_file("singles/Recurring_Daily_301.json"); +const ChargingProfile daily_profile_no_duration = + SmartChargingTestUtils::get_charging_profile_from_file("singles/Recurring_Daily_NoDuration_301.json"); +const ChargingProfile weekly_profile = + SmartChargingTestUtils::get_charging_profile_from_file("singles/Recurring_Weekly_301.json"); +const ChargingProfile weekly_profile_no_duration = + SmartChargingTestUtils::get_charging_profile_from_file("singles/Recurring_Weekly_NoDuration_301.json"); + +CompositeSchedule DEFAULT_SCHEDULE = { + .chargingSchedulePeriod = {}, + .evseId = EVSEID_NOT_SET, + .duration = 600, + .scheduleStart = dt("12:00"), + .chargingRateUnit = ChargingRateUnitEnum::A, +}; + +class ChargingProfileType_Param_Test + : public ::testing::TestWithParam, + ChargingProfile, ocpp::DateTime, std::optional>> {}; + +INSTANTIATE_TEST_SUITE_P( + ChargingProfileType_Param_Test_Instantiate, ChargingProfileType_Param_Test, + testing::Values( + // Absolute Profiles + // not started, started, finished, session started + std::make_tuple(dt("11:50"), dt("20:50"), nullopt, absolute_profile, dt("12:02"), nullopt), + std::make_tuple(dt("12:10"), dt("20:50"), nullopt, absolute_profile, dt("12:02"), nullopt), + std::make_tuple(dt("14:10"), dt("20:50"), nullopt, absolute_profile, dt("12:02"), nullopt), + std::make_tuple(dt("12:10"), dt("20:50"), dt("12:05"), absolute_profile, dt("12:02"), nullopt), + + // Relative Profiles + // not started, started, finished; session started: before, during & after profile + std::make_tuple(dt("11:50"), dt("20:50"), nullopt, relative_profile, dt("11:50"), nullopt), + std::make_tuple(dt("12:10"), dt("20:50"), nullopt, relative_profile, dt("12:10"), nullopt), + std::make_tuple(dt("14:10"), dt("20:50"), nullopt, relative_profile, dt("14:10"), nullopt), + std::make_tuple(dt("12:10"), dt("20:50"), dt("11:50"), relative_profile, dt("11:50"), nullopt), + std::make_tuple(dt("12:55"), dt("20:50"), dt("12:50"), relative_profile, dt("12:50"), nullopt), + std::make_tuple(dt("14:15"), dt("20:50"), dt("12:10"), relative_profile, dt("12:10"), nullopt), + + // Recurring Daily Profiles + // profile not started yet - start time is before profile is valid + std::make_tuple(dt("11:50"), dt("2T20:50"), nullopt, daily_profile, dt("8:00"), dt("2T08:00")), + // profile started - start time is before profile is valid + std::make_tuple(dt("12:10"), dt("2T20:50"), nullopt, daily_profile, dt("8:00"), dt("2T08:00")), + // start time is before profile is valid (and the previous day) + std::make_tuple(dt("2T07:10"), dt("2T20:50"), nullopt, daily_profile, dt("8:00"), dt("2T08:00")), + std::make_tuple(dt("2T08:10"), dt("3T20:50"), nullopt, daily_profile, dt("2T08:00"), dt("3T08:00")), + std::make_tuple(dt("2T23:10"), dt("3T20:50"), nullopt, daily_profile, dt("2T08:00"), dt("3T08:00")), + std::make_tuple(dt("3T07:10"), dt("3T20:50"), nullopt, daily_profile, dt("2T08:00"), dt("3T08:00")), + // profile finished + std::make_tuple(dt("02-03T14:10"), dt("02-04T20:50"), nullopt, daily_profile, dt("02-03T08:00"), + dt("02-04T08:00")), + // session started + std::make_tuple(dt("5T12:10"), dt("6T20:50"), dt("6T08:00"), daily_profile, dt("5T08:00"), dt("6T08:00")), + + // Recurring Weekly Profiles + // profile not started yet - start time is before profile is valid + std::make_tuple(dt("11:50"), dt("7T20:50"), nullopt, weekly_profile, dt("2023-12-27T16:00"), dt("3T16:00")), + // profile started + std::make_tuple(dt("12:10"), dt("7T20:50"), nullopt, weekly_profile, dt("2023-12-27T16:00"), dt("3T16:00")), + std::make_tuple(dt("3T07:10"), dt("7T20:50"), nullopt, weekly_profile, dt("2023-12-27T16:00"), dt("3T16:00")), + std::make_tuple(dt("3T23:10"), dt("10T20:50"), nullopt, weekly_profile, dt("3T16:00"), dt("10T16:00")), + std::make_tuple(dt("4T23:10"), dt("10T20:50"), nullopt, weekly_profile, dt("3T16:00"), dt("10T16:00")), + std::make_tuple(dt("10T07:10"), dt("10T20:50"), nullopt, weekly_profile, dt("3T16:00"), dt("10T16:00")), + std::make_tuple(dt("10T20:10"), dt("17T20:50"), nullopt, weekly_profile, dt("10T16:00"), dt("17T16:00")), + // profile finished + std::make_tuple(dt("02-03T14:10"), dt("02-10T20:50"), nullopt, weekly_profile, dt("31T16:00"), + dt("02-07T16:00")), + // session started + std::make_tuple(dt("4T23:10"), dt("12T20:50"), dt("5T11:50"), weekly_profile, dt("3T16:00"), dt("10T16:00")))); + +TEST_P(ChargingProfileType_Param_Test, CalculateSessionStart) { + ocpp::DateTime now = std::get<0>(GetParam()); + ocpp::DateTime end = std::get<1>(GetParam()); + std::optional session_start = std::get<2>(GetParam()); + ChargingProfile profile = std::get<3>(GetParam()); + DateTime expected_start_time = std::get<4>(GetParam()); + std::optional second_session_start = std::get<5>(GetParam()); + + std::vector start_time = calculate_start(now, end, session_start, profile); + + for (DateTime t : start_time) { + EVLOG_debug << "Start time: " << t.to_rfc3339(); + } + + if (!second_session_start.has_value()) { + ASSERT_EQ(start_time.size(), 1); + EXPECT_EQ(start_time[0], expected_start_time); + } else { + ASSERT_EQ(start_time.size(), 2); + EXPECT_EQ(start_time[0], expected_start_time); + EXPECT_EQ(start_time[1], second_session_start); + } +} + +TEST(ChargingProfileTypeTest, CalculateStartSingle) { + // profile not started yet + DateTime now("2024-01-01T11:50:00Z"); + DateTime end("2024-01-07T20:50:00Z"); + auto start_time = calculate_start(now, end, nullopt, weekly_profile); + // start time is before profile is valid + ASSERT_EQ(start_time.size(), 2); + EXPECT_EQ(start_time[0].to_rfc3339(), "2023-12-27T16:00:00.000Z"); + EXPECT_EQ(start_time[1].to_rfc3339(), "2024-01-03T16:00:00.000Z"); +} + +class CalculateProfileEntryType_Param_Test + : public ::testing::TestWithParam< + std::tuple, ChargingProfile, ocpp::DateTime, + ocpp::DateTime, int, std::optional, std::optional>> {}; + +INSTANTIATE_TEST_SUITE_P(CalculateProfileEntryType_Param_Test_Instantiate, CalculateProfileEntryType_Param_Test, + testing::Values( + // Absolute Profiles + std::make_tuple(dt("12:10"), dt("20:50"), nullopt, absolute_profile, dt("12:02"), + dt("12:32"), 0, nullopt, nullopt), + std::make_tuple(dt("12:10"), dt("20:50"), nullopt, absolute_profile, dt("12:32"), + dt("12:47"), 1, nullopt, nullopt), + std::make_tuple(dt("12:10"), dt("20:50"), nullopt, absolute_profile, dt("12:47"), + dt("13:02"), 2, nullopt, nullopt), + std::make_tuple(dt("12:10"), dt("20:50"), nullopt, absolute_profile_no_duration, + dt("12:47"), dt("14:00"), 2, nullopt, nullopt), + + // Relative Profiles + // Matches 1.6 ProfileTestsA/calculateProfileEntryRelative0 + std::make_tuple(dt("12:20"), dt("20:50"), nullopt, relative_profile, dt("12:20"), + dt("12:50"), 0, nullopt, nullopt), + std::make_tuple(dt("12:20"), dt("20:50"), dt("12:15"), relative_profile, dt("12:15"), + dt("12:45"), 0, nullopt, nullopt), + + // Matches 1.6 ProfileTestsA/calculateProfileEntryRelative1 + std::make_tuple(dt("12:20"), dt("20:50"), nullopt, relative_profile, dt("12:50"), + dt("13:05"), 1, nullopt, nullopt), + std::make_tuple(dt("12:20"), dt("20:50"), dt("12:15"), relative_profile, dt("12:45"), + dt("13:00"), 1, nullopt, nullopt), + + // Matches 1.6 ProfileTestsA/calculateProfileEntryRelative2 + std::make_tuple(dt("12:20"), dt("20:50"), nullopt, relative_profile, dt("13:05"), + dt("13:20"), 2, nullopt, nullopt), + std::make_tuple(dt("12:20"), dt("20:50"), dt("12:15"), relative_profile, dt("13:00"), + dt("13:15"), 2, nullopt, nullopt), + + // Matches 1.6 ProfileTestsA/calculateProfileEntryRelativeNoDuration + std::make_tuple(dt("12:20"), dt("20:50"), nullopt, relative_profile_no_duration, + dt("13:05"), dt("14:00"), 2, nullopt, nullopt), + std::make_tuple(dt("12:20"), dt("20:50"), dt("12:15"), relative_profile_no_duration, + dt("13:00"), dt("14:00"), 2, nullopt, nullopt), + + // Matches 1.6 ProfileTestsA/calculateProfileEntryRecurringDaily0 + std::make_tuple(dt("2T08:10"), dt("3T20:50"), nullopt, daily_profile, dt("2T08:00"), + dt("2T08:30"), 0, dt("3T08:00"), dt("3T08:30")), + + // Matches 1.6 ProfileTestsA/calculateProfileEntryRecurringDaily1 + std::make_tuple(dt("2T08:10"), dt("3T20:50"), nullopt, daily_profile, dt("2T08:30"), + dt("2T08:45"), 1, dt("3T08:30"), dt("3T08:45")), + + // Matches 1.6 ProfileTestsA/calculateProfileEntryRecurringDaily2 + std::make_tuple(dt("2T08:10"), dt("3T20:50"), nullopt, daily_profile, dt("2T08:45"), + dt("2T09:00"), 2, dt("3T08:45"), dt("3T09:00")), + + // Matches 1.6 ProfileTestsA/calculateProfileEntryRecurringDailyNoDuration + std::make_tuple(dt("2T08:10"), dt("4T08:00"), nullopt, daily_profile_no_duration, + dt("2T08:45"), dt("3T08:00"), 2, dt("3T08:45"), dt("4T08:00")), + + // Matches 1.6 ProfileTestsA/calculateProfileEntryRecurringDailyBeforeValid + std::make_tuple(dt("8:10"), dt("2T20:50"), nullopt, daily_profile, dt("2T08:45"), + dt("2T09:00"), 2, nullopt, nullopt), + std::make_tuple(dt("8:10"), dt("3T20:50"), nullopt, daily_profile_no_duration, dt("12:00"), + dt("2T08:00"), 2, dt("2T08:45"), dt("3T08:00")), + std::make_tuple(dt("3T16:10"), dt("10T20:50"), nullopt, weekly_profile, dt("3T16:00"), + dt("3T16:30"), 0, dt("10T16:00"), dt("10T16:30")), + std::make_tuple(dt("3T16:10"), dt("10T20:50"), nullopt, weekly_profile, dt("3T16:30"), + dt("3T16:45"), 1, dt("10T16:30"), dt("10T16:45")), + + std::make_tuple(dt("2023-12-30T08:10"), dt("3T20:50"), nullopt, weekly_profile, + dt("3T16:45"), dt("3T17:00"), 2, nullopt, nullopt), + std::make_tuple(dt("2023-12-30T08:10"), dt("10T20:50"), nullopt, + weekly_profile_no_duration, dt("12:00"), dt("3T16:00"), 2, dt("3T16:45"), + dt("10T16:00")) + + )); + +TEST_P(CalculateProfileEntryType_Param_Test, CalculateProfileEntry_Positive) { + + DateTime now = std::get<0>(GetParam()); + DateTime end = std::get<1>(GetParam()); + std::optional session_start = std::get<2>(GetParam()); + ChargingProfile profile = std::get<3>(GetParam()); + DateTime expected_start = std::get<4>(GetParam()); + DateTime expected_end = std::get<5>(GetParam()); + int period_index = std::get<6>(GetParam()); + std::optional expected_2nd_entry_start = std::get<7>(GetParam()); + std::optional expected_2nd_entry_end = std::get<8>(GetParam()); + + std::vector period_entries = + calculate_profile_entry(now, end, session_start, profile, period_index); + + period_entry_t expected_entry{.start = expected_start, + .end = expected_end, + .limit = profile.chargingSchedule.front().chargingSchedulePeriod[period_index].limit, + .stack_level = profile.stackLevel, + .charging_rate_unit = profile.chargingSchedule.front().chargingRateUnit}; + + for (period_entry_t pet : period_entries) { + EVLOG_debug << ">>> " << pet; + } + + EXPECT_EQ(period_entries.front(), expected_entry); + + if (!expected_2nd_entry_start.has_value()) { + EXPECT_EQ(1, period_entries.size()); + } else { + period_entry_t second_entry = period_entries.at(1); + + period_entry_t expected_second_entry{ + .start = expected_2nd_entry_start.value(), + .end = expected_2nd_entry_end.value(), + .limit = profile.chargingSchedule.front().chargingSchedulePeriod[period_index].limit, + .stack_level = profile.stackLevel, + .charging_rate_unit = profile.chargingSchedule.front().chargingRateUnit}; + + EVLOG_debug << " second_entry> " << second_entry; + EVLOG_debug << "expected_second_entry> " << expected_second_entry; + + EXPECT_EQ(second_entry, expected_second_entry); + } +} + +/// This specific test needs to be run in isolation. +/// Matches 1.6 ProfileTestsA/calculateProfileEntryRecurringWeeklyNoDuration +TEST(ChargingProfileTypeTest, CalculateProfileEntry_RecurringWeeklyNoDuration) { + DateTime now("2024-01-03T16:10:00Z"); + DateTime end("2024-01-17T20:50:00Z"); + + std::vector period_entries = + calculate_profile_entry(now, end, nullopt, weekly_profile_no_duration, 2); + + ASSERT_GE(period_entries.size(), 2); + + const auto* entry = &period_entries[0]; + + EXPECT_EQ(entry->start, DateTime("2024-01-03T16:45:00Z")); + EXPECT_EQ(entry->end, DateTime("2024-01-10T16:00:00Z")); + EXPECT_EQ(entry->limit, weekly_profile_no_duration.chargingSchedule.front().chargingSchedulePeriod[2].limit); + EXPECT_FALSE(entry->number_phases); + EXPECT_EQ(entry->stack_level, weekly_profile_no_duration.stackLevel); + + entry = &period_entries[1]; + + EXPECT_EQ(entry->start, DateTime("2024-01-10T16:45:00Z")); + EXPECT_EQ(entry->end, DateTime("2024-01-17T16:00:00Z")); + EXPECT_EQ(entry->limit, weekly_profile_no_duration.chargingSchedule.front().chargingSchedulePeriod[2].limit); + EXPECT_FALSE(entry->number_phases); + EXPECT_EQ(entry->stack_level, weekly_profile_no_duration.stackLevel); +} + +class CalculateProfileEntryType_NegativeBoundary_Param_Test + : public ::testing::TestWithParam< + std::tuple, ChargingProfile, int>> {}; + +INSTANTIATE_TEST_SUITE_P(CalculateProfileEntryType_NegativeBoundary_Param_Test_Instantiate, + CalculateProfileEntryType_NegativeBoundary_Param_Test, + testing::Values( + // Absolute Profiles + // not started, started, finished, session started + std::make_tuple(dt("12:10"), dt("20:50"), nullopt, absolute_profile, 3), + std::make_tuple(dt("18:00"), dt("20:50"), nullopt, absolute_profile, 1), + std::make_tuple(dt("12:20"), dt("20:50"), nullopt, relative_profile, 3), + std::make_tuple(dt("12:20"), dt("20:50"), dt("12:15"), relative_profile, 3), + std::make_tuple(dt("18:00"), dt("20:50"), nullopt, relative_profile_no_duration, 1), + std::make_tuple(dt("18:00"), dt("20:50"), dt("12:15"), relative_profile_no_duration, 1), + std::make_tuple(dt("8:10"), dt("20:50"), nullopt, daily_profile, 3), + std::make_tuple(dt("03-01T08:10"), dt("20:50"), nullopt, daily_profile_no_duration, 1), + std::make_tuple(dt("3T16:10"), dt("20:50"), nullopt, weekly_profile_no_duration, 3), + std::make_tuple(dt("03-01T08:10"), dt("03-10T20:50"), nullopt, weekly_profile, 1), + std::make_tuple(dt("2023-12-27T08:10"), dt("20:50"), nullopt, weekly_profile, 2))); + +TEST_P(CalculateProfileEntryType_NegativeBoundary_Param_Test, CalculateProfileEntry_Negative) { + ocpp::DateTime now = std::get<0>(GetParam()); + ocpp::DateTime end = std::get<1>(GetParam()); + std::optional session_start = std::get<2>(GetParam()); + ChargingProfile profile = std::get<3>(GetParam()); + int period_index = std::get<4>(GetParam()); + + std::vector period_entries = + calculate_profile_entry(now, end, session_start, profile, period_index); + + ASSERT_EQ(period_entries.size(), 0); +} + +TEST(OCPPTypesTest, PeriodEntry_Equality) { + period_entry_t actual_entry{.start = dt("2T08:45"), + .end = dt("3T08:00"), + .limit = absolute_profile.chargingSchedule.front().chargingSchedulePeriod[0].limit, + .stack_level = absolute_profile.stackLevel, + .charging_rate_unit = absolute_profile.chargingSchedule.front().chargingRateUnit}; + period_entry_t same_entry = actual_entry; + + period_entry_t different_entry{.start = dt("3T08:00"), + .end = dt("3T08:00"), + .limit = absolute_profile.chargingSchedule.front().chargingSchedulePeriod[0].limit, + .stack_level = absolute_profile.stackLevel, + .charging_rate_unit = absolute_profile.chargingSchedule.front().chargingRateUnit}; + + ASSERT_EQ(actual_entry, same_entry); + ASSERT_NE(actual_entry, different_entry); +} + +class CalculateProfileType_Param_Test + : public ::testing::TestWithParam< + std::tuple>> {}; + +INSTANTIATE_TEST_SUITE_P( + CalculateProfileType_Param_Test_Instantiate, CalculateProfileType_Param_Test, + testing::Values( + // Absolute Profiles + // not started, started, finished, session started + std::make_tuple(dt("8:10"), dt("20:50"), dt("2023-12-27T08:05"), absolute_profile, nullopt), + std::make_tuple(dt("12:01"), dt("20:50"), dt("2023-12-27T08:05"), absolute_profile, nullopt), + std::make_tuple(dt("12:40"), dt("20:50"), dt("2023-12-27T08:05"), absolute_profile, 2), + std::make_tuple(dt("14:01"), dt("20:50"), dt("2023-12-27T08:05"), absolute_profile, 0))); + +TEST_P(CalculateProfileType_Param_Test, CalculateProfileDirect) { + ocpp::DateTime now = std::get<0>(GetParam()); + ocpp::DateTime end = std::get<1>(GetParam()); + ocpp::DateTime session_start = std::get<2>(GetParam()); + ChargingProfile profile = std::get<3>(GetParam()); + std::optional size = std::get<4>(GetParam()); + + std::vector period_entries_no_session = calculate_profile(now, end, nullopt, profile); + std::vector period_entries = calculate_profile(now, end, session_start, absolute_profile); + + // If no size is passed than it's the size of the Profile's + ASSERT_EQ(size.value_or(profile.chargingSchedule.front().chargingSchedulePeriod.size()), + period_entries_no_session.size()); + EXPECT_EQ(period_entries_no_session, period_entries); + EXPECT_TRUE(SmartChargingTestUtils::validate_profile_result(period_entries_no_session)); + + // ASSERT_EQ(period_entries.size(), 0); +} + +TEST(OCPPTypesTest, CalculateProfile_Absolute) { + // before start expecting all periods to be included + std::vector period_entries_no_session = + calculate_profile(dt("8:10"), dt("20:50"), nullopt, absolute_profile); + std::vector period_entries_before = + calculate_profile(dt("8:10"), dt("20:50"), dt("2023-12-27T08:05"), absolute_profile); + + ASSERT_EQ(absolute_profile.chargingSchedule.front().chargingSchedulePeriod.size(), + period_entries_no_session.size()); + ASSERT_EQ(absolute_profile.chargingSchedule.front().chargingSchedulePeriod.size(), period_entries_before.size()); + EXPECT_EQ(period_entries_no_session, period_entries_before); + EXPECT_TRUE(SmartChargingTestUtils::validate_profile_result(period_entries_no_session)); + + // just before start + period_entries_no_session = calculate_profile(dt("12:01"), dt("20:50"), nullopt, absolute_profile); + period_entries_before = calculate_profile(dt("12:01"), dt("20:50"), dt("2023-12-27T08:05"), absolute_profile); + + ASSERT_EQ(absolute_profile.chargingSchedule.front().chargingSchedulePeriod.size(), + period_entries_no_session.size()); + ASSERT_EQ(absolute_profile.chargingSchedule.front().chargingSchedulePeriod.size(), period_entries_before.size()); + EXPECT_EQ(period_entries_no_session, period_entries_before); + EXPECT_TRUE(SmartChargingTestUtils::validate_profile_result(period_entries_no_session)); + + // during + period_entries_no_session = calculate_profile(dt("12:40"), dt("20:50"), nullopt, absolute_profile); + period_entries_before = calculate_profile(dt("12:40"), dt("20:50"), dt("2023-12-27T08:05"), absolute_profile); + + ASSERT_EQ(2, period_entries_no_session.size()); + ASSERT_EQ(2, period_entries_before.size()); + EXPECT_EQ(period_entries_no_session, period_entries_before); + EXPECT_TRUE(SmartChargingTestUtils::validate_profile_result(period_entries_no_session)); + + // after + period_entries_no_session = calculate_profile(dt("14:01"), dt("20:50"), nullopt, absolute_profile); + period_entries_before = calculate_profile(dt("14:01"), dt("20:50"), dt("2023-12-27T08:05"), absolute_profile); + + ASSERT_EQ(0, period_entries_no_session.size()); + ASSERT_EQ(0, period_entries_before.size()); + EXPECT_EQ(period_entries_no_session, period_entries_before); + EXPECT_TRUE(SmartChargingTestUtils::validate_profile_result(period_entries_no_session)); +} + +TEST(OCPPTypesTest, CalculateProfile_AbsoluteLimited) { + // Before start expecting no periods + ASSERT_EQ(0, calculate_profile(dt("8:10"), dt("8:30"), nullopt, absolute_profile).size()); + + // Just before start expecting a single period + std::vector period_entries_just_before_start = + calculate_profile(dt("12:01"), dt("12:21"), nullopt, absolute_profile); + + ASSERT_EQ(1, period_entries_just_before_start.size()); + EXPECT_TRUE(SmartChargingTestUtils::validate_profile_result(period_entries_just_before_start)); + ASSERT_EQ(gen_pe(dt("12:02"), dt("12:32"), absolute_profile, 0), period_entries_just_before_start.front()); + + // During start expecting 2 periods + std::vector period_entries_during_start = + calculate_profile(dt("12:40"), dt("13:00"), nullopt, absolute_profile); + + ASSERT_EQ(2, period_entries_during_start.size()); + ASSERT_EQ(gen_pe(dt("12:32"), dt("12:47"), absolute_profile, 1), period_entries_during_start.front()); + ASSERT_EQ(gen_pe(dt("12:47"), dt("13:02"), absolute_profile, 2), period_entries_during_start.back()); + EXPECT_TRUE(SmartChargingTestUtils::validate_profile_result(period_entries_during_start)); + + // After expecting no periods + ASSERT_EQ(0, calculate_profile(dt("14:01"), dt("14:21"), nullopt, absolute_profile).size()); +} + +TEST(OCPPTypesTest, CalculateProfile_Relative) { + // Before start expecting no periods + ASSERT_EQ(0, calculate_profile(dt("8:10"), dt("20:50"), nullopt, relative_profile).size()); + ASSERT_EQ(0, calculate_profile(dt("8:10"), dt("20:50"), dt("2023-12-27T08:05"), relative_profile).size()); + + // JUST BEFORE START + // Expecting all periods + std::vector pe_before_no_session = + calculate_profile(dt("11:58"), dt("20:50"), nullopt, relative_profile); + std::vector pe_before = calculate_profile(dt("11:58"), dt("20:50"), dt("11:55"), relative_profile); + + // While the period entries should have the same length, adding a session start should change the result + ASSERT_EQ(pe_before_no_session.size(), relative_profile.chargingSchedule.front().chargingSchedulePeriod.size()); + ASSERT_EQ(pe_before.size(), relative_profile.chargingSchedule.front().chargingSchedulePeriod.size()); + EXPECT_NE(pe_before_no_session, pe_before); + + // Validate period entries with no session + ASSERT_EQ(gen_pe(dt("12:00"), dt("12:28"), relative_profile, 0), pe_before_no_session.front()); + ASSERT_EQ(gen_pe(dt("12:28"), dt("12:43"), relative_profile, 1), pe_before_no_session.at(1)); + ASSERT_EQ(gen_pe(dt("12:43"), dt("12:58"), relative_profile, 2), pe_before_no_session.back()); + + // Validate period entries with session + ASSERT_EQ(gen_pe(dt("12:00"), dt("12:25"), relative_profile, 0), pe_before.front()); + ASSERT_EQ(gen_pe(dt("12:25"), dt("12:40"), relative_profile, 1), pe_before.at(1)); + ASSERT_EQ(gen_pe(dt("12:40"), dt("12:55"), relative_profile, 2), pe_before.back()); + + // During START + // Expecting all periods for no session and 2 periods when there is an existing session + std::vector pe_during_no_session = + calculate_profile(dt("12:40"), dt("20:50"), nullopt, relative_profile); + std::vector pe_during = calculate_profile(dt("12:40"), dt("20:50"), dt("12:38"), relative_profile); + + ASSERT_EQ(3, pe_during_no_session.size()); + ASSERT_EQ(3, pe_during.size()); + // the session start should change the result + EXPECT_NE(pe_during_no_session, pe_during); + EXPECT_TRUE(SmartChargingTestUtils::validate_profile_result(pe_during_no_session)); + EXPECT_TRUE(SmartChargingTestUtils::validate_profile_result(pe_during)); + + ASSERT_EQ(gen_pe(dt("12:38"), dt("13:08"), relative_profile, 0), pe_during.front()); + ASSERT_EQ(gen_pe(dt("13:08"), dt("13:23"), relative_profile, 1), pe_during.at(1)); + ASSERT_EQ(gen_pe(dt("13:23"), dt("13:38"), relative_profile, 2), pe_during.back()); + + // During, but a bit later now only creates 2 periods with an existing sesion + std::vector pe_during_later_no_session = + calculate_profile(dt("13:10"), dt("20:50"), nullopt, relative_profile); + std::vector pe_during_later = + calculate_profile(dt("13:10"), dt("20:50"), dt("12:38"), relative_profile); + + ASSERT_EQ(3, pe_during_later_no_session.size()); + ASSERT_EQ(2, pe_during_later.size()); + EXPECT_NE(pe_during_later_no_session, pe_during_later); + EXPECT_TRUE(SmartChargingTestUtils::validate_profile_result(pe_during_later_no_session)); + EXPECT_TRUE(SmartChargingTestUtils::validate_profile_result(pe_during_later)); + + ASSERT_EQ(gen_pe(dt("13:08"), dt("13:23"), relative_profile, 1), pe_during_later.front()); + ASSERT_EQ(gen_pe(dt("13:23"), dt("13:38"), relative_profile, 2), pe_during_later.back()); + + // After + ASSERT_EQ(0, calculate_profile(dt("14:02"), dt("14:01"), nullopt, relative_profile).size()); + ASSERT_EQ(0, calculate_profile(dt("14:02"), dt("14:01"), dt("14:01"), relative_profile).size()); +} + +TEST(OCPPTypesTest, CalculateProfile_RelativeLimited) { + // Before start expecting no periods + // Time window: starts 2" into session ends 22" into session + ASSERT_EQ(0, calculate_profile(dt("8:12"), dt("8:32"), dt("8:10"), relative_profile).size()); + + // Just before start, same time window expecting 1 + std::vector periods = calculate_profile(dt("11:57"), dt("12:17"), dt("11:55"), relative_profile); + ASSERT_EQ(1, periods.size()); + EXPECT_TRUE(SmartChargingTestUtils::validate_profile_result(periods)); + ASSERT_EQ(gen_pe(dt("12:00"), dt("12:25"), relative_profile, 0), periods.front()); + + // During A - time window: starts 25" into session ends 45" into session + periods = calculate_profile(dt("12:20"), dt("12:40"), dt("11:55"), relative_profile); + ASSERT_EQ(3, periods.size()); + EXPECT_TRUE(SmartChargingTestUtils::validate_profile_result(periods)); + ASSERT_EQ(gen_pe(dt("12:00"), dt("12:25"), relative_profile, 0), periods.front()); + ASSERT_EQ(gen_pe(dt("12:25"), dt("12:40"), relative_profile, 1), periods.at(1)); + ASSERT_EQ(gen_pe(dt("12:40"), dt("12:55"), relative_profile, 2), periods.back()); + + // During B - time window: starts 35" into session ends 55" into session + periods = calculate_profile(dt("12:30"), dt("12:50"), dt("11:55"), relative_profile); + ASSERT_EQ(2, periods.size()); + EXPECT_TRUE(SmartChargingTestUtils::validate_profile_result(periods)); + ASSERT_EQ(gen_pe(dt("12:25"), dt("12:40"), relative_profile, 1), periods.front()); + ASSERT_EQ(gen_pe(dt("12:40"), dt("12:55"), relative_profile, 2), periods.back()); + + // TODO Delete me + for (period_entry_t pet : periods) { + EVLOG_debug << ">>> " << pet; + } + + // During C - session starts towards end of profiles duration. time window: starts 35" into session ends 55" + // into + periods = calculate_profile(dt("13:55"), dt("14:15"), dt("13:20"), relative_profile); + ASSERT_EQ(1, periods.size()); + EXPECT_TRUE(SmartChargingTestUtils::validate_profile_result(periods)); + ASSERT_EQ(gen_pe(dt("13:50"), dt("14:00"), relative_profile, 1), periods.front()); + + // After + periods = calculate_profile(dt("14:03"), dt("14:23"), dt("14:01"), relative_profile); + ASSERT_EQ(0, periods.size()); +} + +TEST(OCPPTypesTest, ChargingSchedulePeriod_Equality) { + ChargingSchedulePeriod period1 = ChargingSchedulePeriod{ + .startPeriod = 0, + .limit = NO_LIMIT_SPECIFIED, + }; + ChargingSchedulePeriod period2 = ChargingSchedulePeriod{ + .startPeriod = 0, + .limit = NO_LIMIT_SPECIFIED, + }; + ASSERT_EQ(period1, period1); + ASSERT_EQ(period1, period2); + + // startPeriod not equal if a diff greater than 9 + period2.startPeriod = 10; + ASSERT_NE(period1, period2); + + // startPeriod equal if a diff within 9 + period2.startPeriod = 9; + ASSERT_EQ(period1, period2); + + period1.limit = 1; + ASSERT_NE(period1, period2); + + period2 = ChargingSchedulePeriod{ + .startPeriod = 0, + .limit = 1, + }; + ASSERT_EQ(period1, period2); + + // Optional phases + period1.numberPhases = 3; + ASSERT_NE(period1, period2); + + period2.numberPhases = 3; + ASSERT_EQ(period1, period2); + + // Optional phaseToUse + period1.phaseToUse = 1; + ASSERT_NE(period1, period2); + + period2.phaseToUse = 1; + ASSERT_EQ(period1, period2); +} + +TEST(OCPPTypesTest, ChargingSchedule_Equality) { + std::vector periods = {ChargingSchedulePeriod{ + .startPeriod = 0, + .limit = 10, + }, + ChargingSchedulePeriod{ + .startPeriod = 100, + .limit = 20, + }}; + ChargingSchedule schedule1 = ChargingSchedule{ + .id = 0, + .chargingSchedulePeriod = periods, + .duration = std::chrono::duration_cast(minutes(10)).count(), + }; + ChargingSchedule schedule2 = ChargingSchedule{ + .id = 0, + .chargingSchedulePeriod = {periods.at(0)}, + .duration = std::chrono::duration_cast(minutes(10)).count(), + }; + ASSERT_NE(schedule1, schedule2); + + // Perios must match + schedule2.chargingSchedulePeriod = {periods.at(1), {periods.at(0)}}; + ASSERT_NE(schedule1, schedule2); + + schedule2.chargingSchedulePeriod = periods; + ASSERT_EQ(schedule1, schedule2); + + // chargingRateUnit must match (defaults to W) + schedule1.chargingRateUnit = ChargingRateUnitEnum::A; + ASSERT_NE(schedule1, schedule2); + + schedule1.chargingRateUnit = ChargingRateUnitEnum::W; + ASSERT_EQ(schedule1, schedule2); + + // startSchedule must match + schedule1.startSchedule = dt("12:30"); + ASSERT_NE(schedule1, schedule2); + + schedule2.startSchedule = dt("12:31"); + ASSERT_NE(schedule1, schedule2); + + schedule2.startSchedule = dt("12:30"); + ASSERT_EQ(schedule1, schedule2); + + // duration must match + schedule1.duration = 3200; + ASSERT_NE(schedule1, schedule2); + + schedule2.duration = 1600; + ASSERT_NE(schedule1, schedule2); + + schedule2.duration = 3200; + ASSERT_EQ(schedule1, schedule2); + + // minChargingRate must match + schedule1.minChargingRate = 1000.0; + ASSERT_NE(schedule1, schedule2); + + schedule2.minChargingRate = 199.0; + ASSERT_NE(schedule1, schedule2); + + schedule2.minChargingRate = 1000.0; + ASSERT_EQ(schedule1, schedule2); +} + +TEST(OCPPTypesTest, CalculateChargingSchedule_Empty) { + std::vector combined_schedules{}; + CompositeSchedule expected = CompositeSchedule{ + .chargingSchedulePeriod = {ChargingSchedulePeriod{.startPeriod = 0, .limit = NO_LIMIT_SPECIFIED}}, + .evseId = EVSEID_NOT_SET, + .duration = std::chrono::duration_cast(minutes(10)).count(), + .scheduleStart = dt("12:00"), + .chargingRateUnit = ChargingRateUnitEnum::A}; + + CompositeSchedule actual = calculate_composite_schedule(combined_schedules, dt("12:00"), dt("12:10"), std::nullopt); + + ASSERT_EQ(expected, actual); +} + +TEST(OCPPTypesTest, CalculateChargingSchedule_Exact) { + DateTime now = dt("12:00"); + DateTime end = dt("12:10"); + std::vector combined_schedules{ + {now, end, 24.0, 3, std::nullopt, 1, ChargingRateUnitEnum::A, std::nullopt}}; + CompositeSchedule expected = CompositeSchedule{ + .chargingSchedulePeriod = {ChargingSchedulePeriod{ + .startPeriod = 0, + .limit = 24.0, + .numberPhases = 3, + }}, + .evseId = EVSEID_NOT_SET, + .duration = std::chrono::duration_cast(minutes(10)).count(), + .scheduleStart = now, + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, ChargingRateUnitEnum::A); + + ASSERT_EQ(expected, actual); +} + +TEST(OCPPTypesTest, CalculateChargingSchedule_ShortExact) { + DateTime now = dt("12:00"); + DateTime end = dt("12:10"); + std::vector combined_schedules{{now, DateTime(end.to_time_point() - seconds(1)), 24.0, 3, + std::nullopt, 1, ChargingRateUnitEnum::A, std::nullopt}}; + + CompositeSchedule expected = CompositeSchedule{ + .chargingSchedulePeriod = {ChargingSchedulePeriod{ + .startPeriod = 0, + .limit = 24.0, + .numberPhases = 3, + }, + ChargingSchedulePeriod{.startPeriod = 599, .limit = NO_LIMIT_SPECIFIED}}, + .evseId = EVSEID_NOT_SET, + .duration = std::chrono::duration_cast(minutes(10)).count(), + .scheduleStart = now, + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, ChargingRateUnitEnum::A); + + ASSERT_EQ(expected, actual); +} + +TEST(OCPPTypesTest, CalculateChargingSchedule_LongExact) { + DateTime now = dt("12:00"); + DateTime end = dt("12:10"); + std::vector combined_schedules{{DateTime(now.to_time_point() - seconds(1)), end, 24.0, 3, + std::nullopt, 1, ChargingRateUnitEnum::A, std::nullopt}}; + + CompositeSchedule expected = CompositeSchedule{ + .chargingSchedulePeriod = {ChargingSchedulePeriod{ + .startPeriod = 0, + .limit = 24.0, + .numberPhases = 3, + }}, + .evseId = EVSEID_NOT_SET, + .duration = std::chrono::duration_cast(minutes(10)).count(), + .scheduleStart = now, + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, ChargingRateUnitEnum::A); + + ASSERT_EQ(expected, actual); +} + +TEST(OCPPTypesTest, CalculateChargingSchedule_AlmostExact) { + DateTime now = dt("12:00"); + DateTime end = dt("12:10"); + std::vector combined_schedules{{DateTime(now.to_time_point() + seconds(1)), + DateTime(end.to_time_point() - seconds(1)), 24.0, 3, std::nullopt, + 1, ChargingRateUnitEnum::A, std::nullopt}}; + CompositeSchedule expected = CompositeSchedule{ + .chargingSchedulePeriod = {ChargingSchedulePeriod{.startPeriod = 0, .limit = NO_LIMIT_SPECIFIED}, + ChargingSchedulePeriod{ + .startPeriod = 1, + .limit = 24.0, + .numberPhases = 3, + }, + ChargingSchedulePeriod{.startPeriod = 599, .limit = NO_LIMIT_SPECIFIED}}, + .evseId = EVSEID_NOT_SET, + .duration = std::chrono::duration_cast(minutes(10)).count(), + .scheduleStart = now, + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt); + + ASSERT_EQ(expected, actual); +} + +TEST(OCPPTypesTest, CalculateChargingSchedule_SingleLong) { + DateTime now = dt("12:00"); + DateTime end = dt("12:10"); + std::vector combined_schedules{ + {dt("11:00"), dt("12:30"), 24.0, 3, std::nullopt, 1, ChargingRateUnitEnum::A, std::nullopt}}; + CompositeSchedule expected = CompositeSchedule{ + .chargingSchedulePeriod = {ChargingSchedulePeriod{ + .startPeriod = 1, + .limit = 24.0, + .numberPhases = 3, + }}, + .evseId = EVSEID_NOT_SET, + .duration = std::chrono::duration_cast(minutes(10)).count(), + .scheduleStart = now, + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt); + + ASSERT_EQ(expected, actual); +} + +TEST(OCPPTypesTest, CalculateChargingSchedule_SingleShort) { + DateTime now = dt("12:00"); + DateTime end = dt("12:10"); + std::vector combined_schedules{ + {dt("11:00"), dt("12:05"), 24.0, 3, std::nullopt, 1, ChargingRateUnitEnum::A, std::nullopt}}; + CompositeSchedule expected = CompositeSchedule{ + .chargingSchedulePeriod = {ChargingSchedulePeriod{ + .startPeriod = 0, + .limit = 24.0, + .numberPhases = 3, + }, + ChargingSchedulePeriod{.startPeriod = 300, .limit = NO_LIMIT_SPECIFIED}}, + .evseId = EVSEID_NOT_SET, + .duration = std::chrono::duration_cast(minutes(10)).count(), + .scheduleStart = now, + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt); + + ASSERT_EQ(expected, actual); +} + +TEST(OCPPTypesTest, CalculateChargingSchedule_SingleDelayedStartLong) { + DateTime now = dt("12:00"); + DateTime end = dt("12:10"); + std::vector combined_schedules{ + {dt("12:02"), dt("12:30"), 24.0, 3, std::nullopt, 1, ChargingRateUnitEnum::A, std::nullopt}}; + CompositeSchedule expected = CompositeSchedule{ + .chargingSchedulePeriod = {ChargingSchedulePeriod{ + .startPeriod = 0, + .limit = NO_LIMIT_SPECIFIED, + }, + ChargingSchedulePeriod{.startPeriod = 120, .limit = 24.0, .numberPhases = 3}}, + .evseId = EVSEID_NOT_SET, + .duration = std::chrono::duration_cast(minutes(10)).count(), + .scheduleStart = now, + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt); + + ASSERT_EQ(expected, actual); +} + +TEST(OCPPTypesTest, CalculateChargingSchedule_OverlapStart) { + DateTime now = dt("12:00"); + DateTime end = dt("12:10"); + std::vector combined_schedules{ + {dt("12:05"), dt("13:00"), 32.0, 1, std::nullopt, 21, ChargingRateUnitEnum::A, std::nullopt}, + {dt("11:30"), dt("12:30"), 24.0, 3, std::nullopt, 1, ChargingRateUnitEnum::A, std::nullopt}}; + CompositeSchedule expected = CompositeSchedule{ + .chargingSchedulePeriod = {ChargingSchedulePeriod{.startPeriod = 0, .limit = 24.0, .numberPhases = 3}, + ChargingSchedulePeriod{.startPeriod = 300, .limit = 32.0, .numberPhases = 1}}, + .evseId = EVSEID_NOT_SET, + .duration = std::chrono::duration_cast(minutes(10)).count(), + .scheduleStart = now, + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt); + + ASSERT_EQ(expected, actual); +} + +TEST(OCPPTypesTest, CalculateChargingSchedule_OverlapEnd) { + DateTime now = dt("12:00"); + DateTime end = dt("12:10"); + std::vector combined_schedules{ + {dt("11:30"), dt("12:05"), 32.0, 1, std::nullopt, 21, ChargingRateUnitEnum::A, std::nullopt}, + {dt("11:30"), dt("12:30"), 24.0, 3, std::nullopt, 1, ChargingRateUnitEnum::A, std::nullopt}}; + CompositeSchedule expected = CompositeSchedule{ + .chargingSchedulePeriod = {ChargingSchedulePeriod{.startPeriod = 0, .limit = 32.0, .numberPhases = 1}, + ChargingSchedulePeriod{.startPeriod = 300, .limit = 24.0, .numberPhases = 3}}, + .evseId = EVSEID_NOT_SET, + .duration = std::chrono::duration_cast(minutes(10)).count(), + .scheduleStart = now, + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt); + + ASSERT_EQ(expected, actual); +} + +TEST(OCPPTypesTest, CalculateChargingSchedule_OverlapMiddle) { + DateTime now = dt("12:00"); + DateTime end = dt("12:10"); + std::vector combined_schedules{ + {dt("12:02"), dt("12:05"), 32.0, 1, std::nullopt, 21, ChargingRateUnitEnum::A, std::nullopt}, + {dt("11:30"), dt("12:30"), 24.0, 3, std::nullopt, 1, ChargingRateUnitEnum::A, std::nullopt}}; + CompositeSchedule expected = CompositeSchedule{ + .chargingSchedulePeriod = {ChargingSchedulePeriod{.startPeriod = 0, .limit = 24.0, .numberPhases = 3}, + ChargingSchedulePeriod{.startPeriod = 120, .limit = 32.0, .numberPhases = 1}, + ChargingSchedulePeriod{.startPeriod = 300, .limit = 24.0, .numberPhases = 3}}, + .evseId = EVSEID_NOT_SET, + .duration = std::chrono::duration_cast(minutes(10)).count(), + .scheduleStart = now, + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt); + + ASSERT_EQ(expected, actual); +} + +TEST(OCPPTypesTest, CalculateChargingSchedule_OverlapIgnore) { + DateTime now = dt("12:00"); + DateTime end = dt("12:10"); + std::vector combined_schedules{ + {dt("12:05"), dt("13:00"), 32.0, 1, std::nullopt, 21, ChargingRateUnitEnum::A, std::nullopt}, + {dt("11:30"), dt("12:30"), 24.0, 3, std::nullopt, 31, ChargingRateUnitEnum::A, std::nullopt}}; + CompositeSchedule expected = CompositeSchedule{ + .chargingSchedulePeriod = {ChargingSchedulePeriod{.startPeriod = 0, .limit = 24.0, .numberPhases = 3}}, + .evseId = EVSEID_NOT_SET, + .duration = std::chrono::duration_cast(minutes(10)).count(), + .scheduleStart = now, + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt); + + ASSERT_EQ(expected, actual); +} + +TEST(OCPPTypesTest, CalculateChargingSchedule_NoGapA) { + DateTime now = dt("12:00"); + DateTime end = dt("12:10"); + std::vector combined_schedules{ + {dt("11:50"), dt("12:05"), 32.0, 1, std::nullopt, 21, ChargingRateUnitEnum::A, std::nullopt}, + {dt("12:05"), dt("12:30"), 24.0, 3, std::nullopt, 31, ChargingRateUnitEnum::A, std::nullopt}}; + CompositeSchedule expected = CompositeSchedule{ + .chargingSchedulePeriod = {ChargingSchedulePeriod{.startPeriod = 0, .limit = 32.0, .numberPhases = 1}, + ChargingSchedulePeriod{.startPeriod = 300, .limit = 24.0, .numberPhases = 3}}, + .evseId = EVSEID_NOT_SET, + .duration = std::chrono::duration_cast(minutes(10)).count(), + .scheduleStart = now, + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt); + + ASSERT_EQ(expected, actual); +} + +TEST(OCPPTypesTest, CalculateChargingSchedule_NoGapB) { + DateTime now = dt("12:00"); + DateTime end = dt("12:10"); + std::vector combined_schedules{ + {dt("12:05"), dt("12:30"), 32.0, 1, std::nullopt, 21, ChargingRateUnitEnum::A, std::nullopt}, + {dt("11:50"), dt("12:05"), 24.0, 3, std::nullopt, 31, ChargingRateUnitEnum::A, std::nullopt}}; + CompositeSchedule expected = CompositeSchedule{ + .chargingSchedulePeriod = {ChargingSchedulePeriod{.startPeriod = 0, .limit = 24.0, .numberPhases = 3}, + ChargingSchedulePeriod{.startPeriod = 300, .limit = 32.0, .numberPhases = 1}}, + .evseId = EVSEID_NOT_SET, + .duration = std::chrono::duration_cast(minutes(10)).count(), + .scheduleStart = now, + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt); + + ASSERT_EQ(expected, actual); +} + +TEST(OCPPTypesTest, CalculateChargingSchedule_Overlap) { + DateTime now = dt("12:00"); + DateTime end = dt("12:10"); + std::vector combined_schedules{ + {dt("11:50"), dt("12:05"), 32.0, 1, std::nullopt, 21, ChargingRateUnitEnum::A, std::nullopt}, + {dt("12:05"), dt("12:30"), 24.0, 3, std::nullopt, 31, ChargingRateUnitEnum::A, std::nullopt}}; + CompositeSchedule expected = CompositeSchedule{ + .chargingSchedulePeriod = {ChargingSchedulePeriod{.startPeriod = 0, .limit = 32.0, .numberPhases = 1}, + ChargingSchedulePeriod{.startPeriod = 300, .limit = 24.0, .numberPhases = 3}}, + .evseId = EVSEID_NOT_SET, + .duration = std::chrono::duration_cast(minutes(10)).count(), + .scheduleStart = now, + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt); + + ASSERT_EQ(expected, actual); +} + +/// Inverts the start and end times for the combined_schedules. +TEST(OCPPTypesTest, CalculateChargingSchedule_OverlapInverted) { + DateTime now = dt("12:00"); + DateTime end = dt("12:10"); + std::vector combined_schedules{ + {dt("12:05"), dt("12:30"), 32.0, 1, std::nullopt, 21, ChargingRateUnitEnum::A, std::nullopt}, + {dt("11:50"), dt("12:05"), 24.0, 3, std::nullopt, 31, ChargingRateUnitEnum::A, std::nullopt}}; + CompositeSchedule expected = CompositeSchedule{ + .chargingSchedulePeriod = {ChargingSchedulePeriod{.startPeriod = 0, .limit = 24.0, .numberPhases = 3}, + ChargingSchedulePeriod{.startPeriod = 300, .limit = 32.0, .numberPhases = 1}}, + .evseId = EVSEID_NOT_SET, + .duration = std::chrono::duration_cast(minutes(10)).count(), + .scheduleStart = now, + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt); + + ASSERT_EQ(expected, actual); +} + +TEST(OCPPTypesTest, CalculateChargingSchedule_1SecondGap) { + DateTime now = dt("12:00"); + DateTime end = dt("12:10"); + std::vector combined_schedules{ + {dt("11:50"), DateTime{"2024-01-01T12:04:59Z"}, 32.0, 1, nullopt, 21, ChargingRateUnitEnum::A, std::nullopt}, + {dt("12:05"), dt("12:30"), 24.0, 3, nullopt, 31, ChargingRateUnitEnum::A, std::nullopt}}; + CompositeSchedule expected = CompositeSchedule{ + .chargingSchedulePeriod = {ChargingSchedulePeriod{.startPeriod = 0, .limit = 32.0, .numberPhases = 1}, + ChargingSchedulePeriod{.startPeriod = 299, .limit = NO_LIMIT_SPECIFIED}, + ChargingSchedulePeriod{.startPeriod = 300, .limit = 24.0, .numberPhases = 3}}, + .evseId = EVSEID_NOT_SET, + .duration = std::chrono::duration_cast(minutes(10)).count(), + .scheduleStart = now, + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt); + + ASSERT_EQ(expected, actual); +} + +TEST(OCPPTypesTest, CalculateChargingSchedule_WithPhaseToUse) { + DateTime now = dt("12:00"); + DateTime end = dt("12:10"); + std::vector combined_schedules{ + {dt("11:50"), DateTime{"2024-01-01T12:04:59Z"}, 32.0, 1, 3, 21, ChargingRateUnitEnum::A, std::nullopt}, + {dt("12:05"), dt("12:30"), 24.0, 3, std::nullopt, 31, ChargingRateUnitEnum::A, std::nullopt}}; + CompositeSchedule expected = CompositeSchedule{ + .chargingSchedulePeriod = {ChargingSchedulePeriod{ + .startPeriod = 0, .limit = 32.0, .numberPhases = 1, .phaseToUse = 3}, + ChargingSchedulePeriod{.startPeriod = 299, .limit = NO_LIMIT_SPECIFIED}, + ChargingSchedulePeriod{.startPeriod = 300, .limit = 24.0, .numberPhases = 3}}, + .evseId = EVSEID_NOT_SET, + .duration = std::chrono::duration_cast(minutes(10)).count(), + .scheduleStart = now, + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt); + + ASSERT_EQ(expected, actual); +} + +TEST(OCPPTypesTest, CalculateChargingScheduleCombined_Default) { + CompositeSchedule expected = { + .chargingSchedulePeriod = {ChargingSchedulePeriod{ + .startPeriod = 0, .limit = DEFAULT_LIMIT_AMPS, .numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES}}, + .evseId = EVSEID_NOT_SET, + .duration = DEFAULT_SCHEDULE.duration, + .scheduleStart = DEFAULT_SCHEDULE.scheduleStart, + .chargingRateUnit = DEFAULT_SCHEDULE.chargingRateUnit, + + }; + + const CompositeSchedule actual = + calculate_composite_schedule(DEFAULT_SCHEDULE, DEFAULT_SCHEDULE, DEFAULT_SCHEDULE, DEFAULT_SCHEDULE); + + ASSERT_EQ(expected, actual); +} + +TEST(OCPPTypesTest, CalculateChargingScheduleCombined_CombinedTxDefault) { + CompositeSchedule profile = DEFAULT_SCHEDULE; + CompositeSchedule tx_default_schedule = { + .chargingSchedulePeriod = {{0, 10.0, nullopt}}, + .evseId = EVSEID_NOT_SET, + .duration = 600, + .scheduleStart = dt("12:00"), + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + CompositeSchedule expected = CompositeSchedule{ + .chargingSchedulePeriod = {ChargingSchedulePeriod{ + .startPeriod = 0, .limit = 10, .numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES}}, + .evseId = EVSEID_NOT_SET, + .duration = 600, + .scheduleStart = dt("12:00"), + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + const CompositeSchedule actual = calculate_composite_schedule(profile, profile, tx_default_schedule, profile); + + ASSERT_EQ(expected, actual); +} + +TEST(OCPPTypesTest, CalculateChargingScheduleCombined_CombinedTxDefaultTx) { + CompositeSchedule charging_station_max = DEFAULT_SCHEDULE; + CompositeSchedule tx_default_schedule = { + .chargingSchedulePeriod = {{0, 10.0, nullopt}}, + .evseId = EVSEID_NOT_SET, + .duration = 600, + .scheduleStart = dt("12:00"), + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + CompositeSchedule tx_schedule = { + .chargingSchedulePeriod = {{0, 32.0, nullopt}}, + .evseId = EVSEID_NOT_SET, + .duration = 600, + .scheduleStart = dt("12:00"), + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule expected = { + .chargingSchedulePeriod = {ChargingSchedulePeriod{ + .startPeriod = 0, .limit = 32.0, .numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES}}, + .evseId = EVSEID_NOT_SET, + .duration = 600, + .scheduleStart = dt("12:00"), + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + const CompositeSchedule actual = + calculate_composite_schedule(DEFAULT_SCHEDULE, charging_station_max, tx_default_schedule, tx_schedule); + + ASSERT_EQ(expected, actual); +} + +TEST(OCPPTypesTest, CalculateChargingScheduleCombined_CombinedOverlapTxAndTxDefault) { + CompositeSchedule tx_default_schedule = { + .chargingSchedulePeriod = {{0, 10.0, std::nullopt}, {300, 24.0, nullopt}}, + .evseId = EVSEID_NOT_SET, + .duration = 600, + .scheduleStart = dt("12:00"), + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule tx_schedule = { + .chargingSchedulePeriod = {{0, NO_LIMIT_SPECIFIED, nullopt}, + {150, 32.0, std::nullopt}, + {450, NO_LIMIT_SPECIFIED, nullopt}}, + .evseId = EVSEID_NOT_SET, + .duration = 600, + .scheduleStart = dt("12:00"), + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule charging_station_max = { + .chargingSchedulePeriod = {{0, NO_LIMIT_SPECIFIED, nullopt}}, + .evseId = EVSEID_NOT_SET, + .duration = 600, + .scheduleStart = dt("12:00"), + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule expected = { + .chargingSchedulePeriod = + {ChargingSchedulePeriod{.startPeriod = 0, .limit = 10.0, .numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES}, + ChargingSchedulePeriod{.startPeriod = 150, .limit = 32.0, .numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES}, + ChargingSchedulePeriod{.startPeriod = 450, .limit = 24.0, .numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES}}, + .evseId = EVSEID_NOT_SET, + .duration = 600, + .scheduleStart = dt("12:00"), + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + const CompositeSchedule actual = + calculate_composite_schedule(DEFAULT_SCHEDULE, charging_station_max, tx_default_schedule, tx_schedule); + + ASSERT_EQ(expected, actual); +} + +TEST(OCPPTypesTest, CalculateChargingScheduleCombined_CombinedOverlapTxTxDefaultAndChargingStationMax) { + CompositeSchedule tx_default_schedule = { + .chargingSchedulePeriod = {{0, 10.0, std::nullopt}, {300, 24.0, std::nullopt}}, + .evseId = EVSEID_NOT_SET, + .duration = 600, + .scheduleStart = dt("12:00"), + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule tx_schedule = { + .chargingSchedulePeriod = {{0, NO_LIMIT_SPECIFIED, std::nullopt}, + {150, 32.0, std::nullopt}, + {450, NO_LIMIT_SPECIFIED, std::nullopt}}, + .evseId = EVSEID_NOT_SET, + .duration = 600, + .scheduleStart = dt("12:00"), + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule charging_station_max = { + .chargingSchedulePeriod = {{0, NO_LIMIT_SPECIFIED, std::nullopt}, + {500, 15.0, std::nullopt}, + {550, NO_LIMIT_SPECIFIED, std::nullopt}}, + .evseId = EVSEID_NOT_SET, + .duration = 600, + .scheduleStart = dt("12:00"), + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + CompositeSchedule expected = { + .chargingSchedulePeriod = + {ChargingSchedulePeriod{.startPeriod = 0, .limit = 10.0, .numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES}, + ChargingSchedulePeriod{.startPeriod = 150, .limit = 32.0, .numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES}, + ChargingSchedulePeriod{.startPeriod = 450, .limit = 24.0, .numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES}, + ChargingSchedulePeriod{.startPeriod = 500, .limit = 15.0, .numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES}, + ChargingSchedulePeriod{.startPeriod = 550, .limit = 24.0, .numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES}}, + .evseId = EVSEID_NOT_SET, + .duration = 600, + .scheduleStart = dt("12:00"), + .chargingRateUnit = ChargingRateUnitEnum::A, + }; + + const CompositeSchedule actual = + calculate_composite_schedule(DEFAULT_SCHEDULE, charging_station_max, tx_default_schedule, tx_schedule); + + ASSERT_EQ(expected, actual); +} + +} // namespace \ No newline at end of file diff --git a/tests/lib/ocpp/v201/test_smart_charging_handler.cpp b/tests/lib/ocpp/v201/test_smart_charging_handler.cpp index 5bc280b9a..46e41541a 100644 --- a/tests/lib/ocpp/v201/test_smart_charging_handler.cpp +++ b/tests/lib/ocpp/v201/test_smart_charging_handler.cpp @@ -34,7 +34,7 @@ namespace ocpp::v201 { -static const int NR_OF_EVSES = 1; +static const int NR_OF_EVSES = 2; static const int STATION_WIDE_ID = 0; static const int DEFAULT_EVSE_ID = 1; static const int DEFAULT_PROFILE_ID = 1; @@ -245,6 +245,19 @@ class ChargepointTestFixtureV201 : public DatabaseTestingUtils { handler.add_profile(existing_profile, evse_id); } + std::optional add_valid_profile_to(int evse_id, int profile_id) { + auto periods = create_charging_schedule_periods({0, 1, 2}); + auto profile = create_charging_profile( + profile_id, ChargingProfilePurposeEnum::TxDefaultProfile, + create_charge_schedule(ChargingRateUnitEnum::A, periods, ocpp::DateTime("2024-01-17T17:00:00"))); + auto response = handler.validate_and_add_profile(profile, evse_id); + if (response.status == ChargingProfileStatusEnum::Accepted) { + return profile; + } else { + return {}; + } + } + // Default values used within the tests std::unique_ptr evse_manager = std::make_unique(NR_OF_EVSES); @@ -1437,4 +1450,65 @@ TEST_F(ChargepointTestFixtureV201, K10_ClearChargingProfile_UnknownId) { EXPECT_THAT(profiles, testing::Contains(profile)); } +TEST_F(ChargepointTestFixtureV201, K08_GetValidProfiles_IfNoProfiles_ThenNoValidProfilesReturned) { + auto profiles = handler.get_valid_profiles(DEFAULT_EVSE_ID); + EXPECT_THAT(profiles, testing::IsEmpty()); +} + +TEST_F(ChargepointTestFixtureV201, K08_GetValidProfiles_IfEvseHasProfiles_ThenThoseProfilesReturned) { + auto profile = add_valid_profile_to(DEFAULT_EVSE_ID, DEFAULT_PROFILE_ID); + ASSERT_TRUE(profile.has_value()); + + auto profiles = handler.get_valid_profiles(DEFAULT_EVSE_ID); + EXPECT_THAT(profiles, testing::Contains(profile)); +} + +TEST_F(ChargepointTestFixtureV201, K08_GetValidProfiles_IfOtherEvseHasProfiles_ThenThoseProfilesAreNotReturned) { + auto profile1 = add_valid_profile_to(DEFAULT_EVSE_ID, DEFAULT_PROFILE_ID); + ASSERT_TRUE(profile1.has_value()); + auto profile2 = add_valid_profile_to(DEFAULT_EVSE_ID + 1, DEFAULT_PROFILE_ID + 1); + ASSERT_TRUE(profile2.has_value()); + + auto profiles = handler.get_valid_profiles(DEFAULT_EVSE_ID); + EXPECT_THAT(profiles, testing::Contains(profile1)); + EXPECT_THAT(profiles, testing::Not(testing::Contains(profile2))); +} + +TEST_F(ChargepointTestFixtureV201, K08_GetValidProfiles_IfStationWideProfilesExist_ThenThoseProfilesAreReturned) { + auto profile = add_valid_profile_to(STATION_WIDE_ID, DEFAULT_PROFILE_ID); + ASSERT_TRUE(profile.has_value()); + + auto profiles = handler.get_valid_profiles(DEFAULT_EVSE_ID); + EXPECT_THAT(profiles, testing::Contains(profile)); +} + +TEST_F(ChargepointTestFixtureV201, K08_GetValidProfiles_IfStationWideProfilesExist_ThenThoseProfilesAreReturnedOnce) { + auto profile = add_valid_profile_to(STATION_WIDE_ID, DEFAULT_PROFILE_ID); + ASSERT_TRUE(profile.has_value()); + + auto profiles = handler.get_valid_profiles(STATION_WIDE_ID); + EXPECT_THAT(profiles, testing::Contains(profile)); + EXPECT_THAT(profiles.size(), testing::Eq(1)); +} + +TEST_F(ChargepointTestFixtureV201, K08_GetValidProfiles_IfInvalidProfileExists_ThenThatProfileIsNotReturned) { + auto extraneous_start_schedule = ocpp::DateTime("2024-01-17T17:00:00"); + auto periods = create_charging_schedule_periods(0); + auto invalid_profile = + create_charging_profile(DEFAULT_PROFILE_ID, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A, periods, extraneous_start_schedule), + DEFAULT_TX_ID, ChargingProfileKindEnum::Relative, 1); + handler.add_profile(invalid_profile, DEFAULT_EVSE_ID); + + auto invalid_station_wide_profile = + create_charging_profile(DEFAULT_PROFILE_ID + 1, ChargingProfilePurposeEnum::TxProfile, + create_charge_schedule(ChargingRateUnitEnum::A, periods, extraneous_start_schedule), + DEFAULT_TX_ID, ChargingProfileKindEnum::Relative, 1); + handler.add_profile(invalid_station_wide_profile, STATION_WIDE_ID); + + auto profiles = handler.get_valid_profiles(DEFAULT_EVSE_ID); + EXPECT_THAT(profiles, testing::Not(testing::Contains(invalid_profile))); + EXPECT_THAT(profiles, testing::Not(testing::Contains(invalid_station_wide_profile))); +} + } // namespace ocpp::v201