From 50520ce17696a87397ff78b7cba793cbb0870cc9 Mon Sep 17 00:00:00 2001 From: Marc Emmers <35759328+marcemmers@users.noreply.github.com> Date: Mon, 17 Feb 2025 10:50:39 +0100 Subject: [PATCH] Refactoring of composite schedule calculation (#943) * Refactoring of GetCompositeSchedule --------- Signed-off-by: Marc Emmers --- .../v201/functional_blocks/smart_charging.hpp | 3 +- include/ocpp/v201/profile.hpp | 80 +- .../v201/functional_blocks/smart_charging.cpp | 191 +++-- lib/ocpp/v201/profile.cpp | 442 +++++------ tests/lib/ocpp/v201/CMakeLists.txt | 2 +- .../json/no_gap/TxDefaultProfile_401.json | 21 + .../json/no_gap/TxDefaultProfile_402.json | 21 + .../ChargingStationMaxProfile_24_Ampere.json | 38 + .../ChargingStationMaxProfile_401.json | 38 + .../json/singles/Recurring_Daily_301.json | 6 +- .../Recurring_Daily_302_phase_limit.json | 34 + .../singles/Relative_302_phase_limit.json | 32 + .../ocpp/v201/json/singles/Relative_303.json | 29 + .../singles/TXDefaultProfile_25_Watt.json | 36 + .../lib/ocpp/v201/smart_charging_matchers.hpp | 24 + .../lib/ocpp/v201/test_composite_schedule.cpp | 649 ++++++++++++++-- tests/lib/ocpp/v201/test_profile.cpp | 702 ------------------ 17 files changed, 1257 insertions(+), 1091 deletions(-) create mode 100644 tests/lib/ocpp/v201/json/no_gap/TxDefaultProfile_401.json create mode 100644 tests/lib/ocpp/v201/json/no_gap/TxDefaultProfile_402.json create mode 100644 tests/lib/ocpp/v201/json/singles/ChargingStationMaxProfile_24_Ampere.json create mode 100644 tests/lib/ocpp/v201/json/singles/ChargingStationMaxProfile_401.json create mode 100644 tests/lib/ocpp/v201/json/singles/Recurring_Daily_302_phase_limit.json create mode 100644 tests/lib/ocpp/v201/json/singles/Relative_302_phase_limit.json create mode 100644 tests/lib/ocpp/v201/json/singles/Relative_303.json create mode 100644 tests/lib/ocpp/v201/json/singles/TXDefaultProfile_25_Watt.json create mode 100644 tests/lib/ocpp/v201/smart_charging_matchers.hpp diff --git a/include/ocpp/v201/functional_blocks/smart_charging.hpp b/include/ocpp/v201/functional_blocks/smart_charging.hpp index 55e1fe9e2..a23daede3 100644 --- a/include/ocpp/v201/functional_blocks/smart_charging.hpp +++ b/include/ocpp/v201/functional_blocks/smart_charging.hpp @@ -158,8 +158,7 @@ class SmartCharging : public SmartChargingInterface { /// \brief Calculates the composite schedule for the given \p valid_profiles and the given \p connector_id /// CompositeSchedule calculate_composite_schedule(const ocpp::DateTime& start_time, const ocpp::DateTime& end_time, - const int32_t evse_id, - std::optional charging_rate_unit, + const int32_t evse_id, ChargingRateUnitEnum charging_rate_unit, bool is_offline, bool simulate_transaction_active); /// diff --git a/include/ocpp/v201/profile.hpp b/include/ocpp/v201/profile.hpp index e07f0bb26..57c5ffdda 100644 --- a/include/ocpp/v201/profile.hpp +++ b/include/ocpp/v201/profile.hpp @@ -6,6 +6,16 @@ namespace ocpp { namespace v201 { +struct IntermediatePeriod { + int32_t startPeriod; + float current_limit; + float power_limit; + std::optional numberPhases; + std::optional phaseToUse; +}; + +using IntermediateProfile = std::vector; + /// \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. @@ -35,6 +45,12 @@ struct period_entry_t { } }; +/// \brief Calculate the number of seconds elapsed between \param to and \param from +int32_t elapsed_seconds(const ocpp::DateTime& to, const ocpp::DateTime& from); + +/// \brief Rounds down the \param dt to the nearest second +ocpp::DateTime floor_seconds(const ocpp::DateTime& dt); + /// \brief calculate the start times for the profile /// \param now the current date and time /// \param end the end of the composite schedule @@ -67,33 +83,53 @@ std::vector calculate_profile(const DateTime& now, const DateTim 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 +/// \brief generate an ordered list of valid schedule periods for the profiles +/// \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 profiles A vector of charging profiles +/// \param purpose The purpose to generate the list for. Only profiles with this purpose are included. +/// \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_all_profiles(const DateTime& now, const DateTime& end, + const std::optional& session_start, + const std::vector& profiles, + ChargingProfilePurposeEnum purpose); + +/// \brief calculate the profile for the list of periods +/// \param periods the list of periods to build into the profile /// \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) /// \param default_number_phases default number of phases if no existing period limit applies /// \param supply_voltage Supply voltage of the grid. This value is only used in case a conversion between smart /// charging amp and watt limits is required \return the calculated composite schedule -CompositeSchedule calculate_composite_schedule(std::vector& combined_schedules, const DateTime& now, - const DateTime& end, - std::optional charging_rate_unit, - int32_t default_number_phases, int32_t supply_voltage); - -/// \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 -/// \param default_limits default limits if no existing period limit applies -/// \param supply_voltage Supply voltage of the grid. This value is only used in case a conversion between smart -/// charging amp and watt limits is required \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, - const CompositeScheduleDefaultLimits& default_limits, - int32_t supply_voltage); +IntermediateProfile generate_profile_from_periods(std::vector& periods, const DateTime& now, + const DateTime& end); + +/// \brief Generates a new profile by combining a \param tx_profile and a \param tx_default_profile. +/// The tx_profile will be preferred over the tx_default_profile whenever it has a value +/// \return A combined profile +IntermediateProfile merge_tx_profile_with_tx_default_profile(const IntermediateProfile& tx_profile, + const IntermediateProfile& tx_default_profile); + +/// \brief Generates a new profile by taking the lowest limit of all the provided \param profiles +IntermediateProfile merge_profiles_by_lowest_limit(const std::vector& profiles); + +/// \brief Generates a new profile by summing the limits of all the provided \param profiles, filling in defaults +/// wherever a profile has no limit +IntermediateProfile merge_profiles_by_summing_limits(const std::vector& profiles, + float current_default, float power_default); + +/// \brief Fills all the periods without a limit or a number of phases with the defaults provided +void fill_gaps_with_defaults(IntermediateProfile& schedule, float default_limit, int32_t default_number_phases); + +/// \brief Convert an intermediate profile into a final charging schedule. +/// This will fill in defaults and convert merge the current and power limits into the final \p charging_rate_unit based +/// limit +std::vector +convert_intermediate_into_schedule(const IntermediateProfile& profile, ChargingRateUnitEnum charging_rate_unit, + float default_limit, int32_t default_number_phases, float supply_voltage); } // namespace v201 -} // namespace ocpp +} // namespace ocpp \ No newline at end of file diff --git a/lib/ocpp/v201/functional_blocks/smart_charging.cpp b/lib/ocpp/v201/functional_blocks/smart_charging.cpp index b7dd250a1..3ac5724f1 100644 --- a/lib/ocpp/v201/functional_blocks/smart_charging.cpp +++ b/lib/ocpp/v201/functional_blocks/smart_charging.cpp @@ -274,89 +274,142 @@ ProfileValidationResultEnum SmartCharging::conform_and_validate_profile(Charging return result; } +namespace { +struct CompositeScheduleConfig { + std::vector purposes_to_ignore; + float current_limit{}; + float power_limit{}; + int32_t default_number_phases{}; + float supply_voltage{}; + + CompositeScheduleConfig(DeviceModel& device_model, bool is_offline) : + purposes_to_ignore{utils::get_purposes_to_ignore( + device_model.get_optional_value(ControllerComponentVariables::IgnoredProfilePurposesOffline) + .value_or(""), + is_offline)} { + + this->current_limit = + device_model.get_optional_value(ControllerComponentVariables::CompositeScheduleDefaultLimitAmps) + .value_or(DEFAULT_LIMIT_AMPS); + + this->power_limit = + device_model.get_optional_value(ControllerComponentVariables::CompositeScheduleDefaultLimitWatts) + .value_or(DEFAULT_LIMIT_WATTS); + + this->default_number_phases = + device_model.get_optional_value(ControllerComponentVariables::CompositeScheduleDefaultNumberPhases) + .value_or(DEFAULT_AND_MAX_NUMBER_PHASES); + + this->supply_voltage = + device_model.get_optional_value(ControllerComponentVariables::SupplyVoltage).value_or(LOW_VOLTAGE); + } +}; + +std::vector generate_evse_intermediates(std::vector&& evse_profiles, + const std::vector& station_wide_profiles, + const ocpp::DateTime& start_time, + const ocpp::DateTime& end_time, + std::optional session_start, + bool simulate_transaction_active + +) { + + // Combine the profiles with those from the station + evse_profiles.insert(evse_profiles.end(), station_wide_profiles.begin(), station_wide_profiles.end()); + + auto external_constraints_periods = + calculate_all_profiles(start_time, end_time, session_start, evse_profiles, + ChargingProfilePurposeEnum::ChargingStationExternalConstraints); + + std::vector output; + output.push_back(generate_profile_from_periods(external_constraints_periods, start_time, end_time)); + + // If there is a session active or we want to simulate, add the combined tx and tx_default to the output + if (session_start.has_value() || simulate_transaction_active) { + auto tx_default_periods = calculate_all_profiles(start_time, end_time, session_start, evse_profiles, + ChargingProfilePurposeEnum::TxDefaultProfile); + auto tx_periods = calculate_all_profiles(start_time, end_time, session_start, evse_profiles, + ChargingProfilePurposeEnum::TxProfile); + + auto tx_default = generate_profile_from_periods(tx_default_periods, start_time, end_time); + auto tx = generate_profile_from_periods(tx_periods, start_time, end_time); + + // Merges the TxProfile with the TxDefaultProfile, for every period preferring a tx period over a tx_default + // period + output.push_back(merge_tx_profile_with_tx_default_profile(tx, tx_default)); + } + + return output; +} +} // namespace + CompositeSchedule SmartCharging::calculate_composite_schedule(const ocpp::DateTime& start_time, const ocpp::DateTime& end_time, const int32_t evse_id, - std::optional charging_rate_unit, - bool is_offline, bool simulate_transaction_active) { - std::vector purposes_to_ignore = utils::get_purposes_to_ignore( - this->device_model.get_optional_value(ControllerComponentVariables::IgnoredProfilePurposesOffline) - .value_or(""), - is_offline); + ChargingRateUnitEnum charging_rate_unit, bool is_offline, + bool simulate_transaction_active) { - std::vector valid_profiles = get_valid_profiles(evse_id, purposes_to_ignore); + const CompositeScheduleConfig config{this->device_model, is_offline}; 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: - if (session_start.has_value() || simulate_transaction_active) { - tx_default_periods.insert(tx_default_periods.end(), periods.begin(), periods.end()); - } - break; - case ChargingProfilePurposeEnum::TxProfile: - if (session_start.has_value() || simulate_transaction_active) { - tx_periods.insert(tx_periods.end(), periods.begin(), periods.end()); - } - break; - default: - break; + const auto station_wide_profiles = get_valid_profiles_for_evse(STATION_WIDE_ID, config.purposes_to_ignore); + + std::vector combined_profiles{}; + + if (evse_id == STATION_WIDE_ID) { + auto nr_of_evses = this->evse_manager.get_number_of_evses(); + + // Get the ChargingStationExternalConstraints and Combined Tx(Default)Profiles per evse + std::vector evse_schedules{}; + for (int evse = 1; evse <= nr_of_evses; evse++) { + auto intermediates = generate_evse_intermediates( + get_valid_profiles_for_evse(evse, config.purposes_to_ignore), station_wide_profiles, start_time, + end_time, session_start, simulate_transaction_active); + + // Determine the lowest limits per evse + evse_schedules.push_back(merge_profiles_by_lowest_limit(intermediates)); } + + // Add all the limits of all the evse's together since that will be the max the whole charging station can + // consume at any point in time + combined_profiles.push_back( + merge_profiles_by_summing_limits(evse_schedules, config.current_limit, config.power_limit)); + + } else { + combined_profiles = generate_evse_intermediates(get_valid_profiles_for_evse(evse_id, config.purposes_to_ignore), + station_wide_profiles, start_time, end_time, session_start, + simulate_transaction_active); } - const auto default_amps_limit = - this->device_model.get_optional_value(ControllerComponentVariables::CompositeScheduleDefaultLimitAmps) - .value_or(DEFAULT_LIMIT_AMPS); - const auto default_watts_limit = - this->device_model.get_optional_value(ControllerComponentVariables::CompositeScheduleDefaultLimitWatts) - .value_or(DEFAULT_LIMIT_WATTS); - const auto default_number_phases = - this->device_model.get_optional_value(ControllerComponentVariables::CompositeScheduleDefaultNumberPhases) - .value_or(DEFAULT_AND_MAX_NUMBER_PHASES); - const auto supply_voltage = - this->device_model.get_optional_value(ControllerComponentVariables::SupplyVoltage).value_or(LOW_VOLTAGE); - - CompositeScheduleDefaultLimits default_limits = {default_amps_limit, default_watts_limit, default_number_phases}; - - auto charging_station_external_constraints = - ocpp::v201::calculate_composite_schedule(charging_station_external_constraints_periods, start_time, end_time, - charging_rate_unit, default_number_phases, supply_voltage); - auto composite_charge_point_max = ocpp::v201::calculate_composite_schedule( - charge_point_max_periods, start_time, end_time, charging_rate_unit, default_number_phases, supply_voltage); - auto composite_tx_default = ocpp::v201::calculate_composite_schedule( - tx_default_periods, start_time, end_time, charging_rate_unit, default_number_phases, supply_voltage); - auto composite_tx = ocpp::v201::calculate_composite_schedule(tx_periods, start_time, end_time, charging_rate_unit, - default_number_phases, supply_voltage); - - CompositeSchedule composite_schedule = - ocpp::v201::calculate_composite_schedule(charging_station_external_constraints, composite_charge_point_max, - composite_tx_default, composite_tx, default_limits, supply_voltage); - - // Set the EVSE ID for the resulting CompositeSchedule - composite_schedule.evseId = evse_id; - - return composite_schedule; + // ChargingStationMaxProfile is always station wide + auto charge_point_max_periods = calculate_all_profiles(start_time, end_time, session_start, station_wide_profiles, + ChargingProfilePurposeEnum::ChargingStationMaxProfile); + auto charge_point_max = generate_profile_from_periods(charge_point_max_periods, start_time, end_time); + + // Add the ChargingStationMaxProfile limits to the other profiles + combined_profiles.push_back(std::move(charge_point_max)); + + // Calculate the final limit of all the combined profiles + auto retval = merge_profiles_by_lowest_limit(combined_profiles); + + CompositeSchedule composite{}; + composite.evseId = evse_id; + composite.scheduleStart = floor_seconds(start_time); + composite.duration = elapsed_seconds(floor_seconds(end_time), floor_seconds(start_time)); + composite.chargingRateUnit = charging_rate_unit; + + // Convert the intermediate result into a proper schedule. Will fill in the periods with no limits with the default + // one + const auto limit = charging_rate_unit == ChargingRateUnitEnum::A ? config.current_limit : config.power_limit; + composite.chargingSchedulePeriod = convert_intermediate_into_schedule( + retval, charging_rate_unit, limit, config.default_number_phases, config.supply_voltage); + + return composite; } ProfileValidationResultEnum SmartCharging::validate_evse_exists(int32_t evse_id) const { diff --git a/lib/ocpp/v201/profile.cpp b/lib/ocpp/v201/profile.cpp index 9656d0692..9d8aa97d4 100644 --- a/lib/ocpp/v201/profile.cpp +++ b/lib/ocpp/v201/profile.cpp @@ -9,85 +9,14 @@ 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, - int32_t default_number_phases, int32_t supply_voltage) { - assert(entry != nullptr); - float limit = entry->limit; - std::int32_t number_phases = entry->number_phases.value_or(default_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 / (supply_voltage * number_phases); - } else { - limit = entry->limit * (supply_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) { +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) { +ocpp::DateTime floor_seconds(const ocpp::DateTime& dt) { return ocpp::DateTime(std::chrono::floor(dt.to_time_point())); } @@ -322,12 +251,14 @@ std::vector calculate_profile_entry(const DateTime& in_now, cons return entries; } -std::vector calculate_profile(const DateTime& now, const DateTime& end, - const std::optional& session_start, - const ChargingProfile& profile) { +namespace { +std::vector calculate_profile_unsorted(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 nr_of_entries = profile.chargingSchedule.front().chargingSchedulePeriod.size(); + for (uint8_t i = 0; i < nr_of_entries; i++) { const auto results = calculate_profile_entry(now, end, session_start, profile, i); for (const auto& entry : results) { if (entry.start <= end) { @@ -336,6 +267,10 @@ std::vector calculate_profile(const DateTime& now, const DateTim } } + return entries; +} + +void sort_periods_into_date_order(std::vector& periods) { // sort into date order struct { bool operator()(const period_entry_t& a, const period_entry_t& b) const { @@ -343,28 +278,44 @@ std::vector calculate_profile(const DateTime& now, const DateTim return a.start < b.start; } } less_than; - std::sort(entries.begin(), entries.end(), less_than); + std::sort(periods.begin(), periods.end(), less_than); +} +} // namespace + +std::vector calculate_profile(const DateTime& now, const DateTime& end, + const std::optional& session_start, + const ChargingProfile& profile) { + std::vector entries = calculate_profile_unsorted(now, end, session_start, profile); + + sort_periods_into_date_order(entries); return entries; } -CompositeSchedule calculate_composite_schedule(std::vector& in_combined_schedules, - const DateTime& in_now, const DateTime& in_end, - std::optional charging_rate_unit, - int32_t default_number_phases, int32_t supply_voltage) { +std::vector calculate_all_profiles(const DateTime& now, const DateTime& end, + const std::optional& session_start, + const std::vector& profiles, + ChargingProfilePurposeEnum purpose) { + std::vector output; + for (const auto& profile : profiles) { + if (profile.chargingProfilePurpose == purpose) { + std::vector periods = calculate_profile_unsorted(now, end, session_start, profile); + output.insert(output.end(), periods.begin(), periods.end()); + } + } - // Defaults to ChargingRateUnitEnum::A if not set. - const ChargingRateUnitEnum selected_unit = - (charging_rate_unit) ? charging_rate_unit.value() : ChargingRateUnitEnum::A; + sort_periods_into_date_order(output); + return output; +} + +IntermediateProfile generate_profile_from_periods(std::vector& periods, const DateTime& in_now, + const DateTime& in_end) { const auto now = floor_seconds(in_now); const auto end = floor_seconds(in_end); - CompositeSchedule composite; - composite.chargingSchedulePeriod = {}; - composite.evseId = EVSEID_NOT_SET; - composite.duration = elapsed_seconds(end, now); - composite.scheduleStart = now; - composite.chargingRateUnit = selected_unit; + if (periods.empty()) { + return {{0, NO_LIMIT_SPECIFIED, NO_LIMIT_SPECIFIED, std::nullopt, std::nullopt}}; + } // sort the combined_schedules in stack priority order struct { @@ -373,8 +324,9 @@ CompositeSchedule calculate_composite_schedule(std::vector& in_c return a.stack_level > b.stack_level; } } less_than; - std::sort(in_combined_schedules.begin(), in_combined_schedules.end(), less_than); + std::sort(periods.begin(), periods.end(), less_than); + IntermediateProfile combined{}; DateTime current = now; while (current < end) { @@ -383,7 +335,7 @@ CompositeSchedule calculate_composite_schedule(std::vector& in_c DateTime next_earliest = end; const period_entry_t* chosen{nullptr}; - for (const auto& schedule : in_combined_schedules) { + for (const auto& schedule : periods) { if (schedule.start <= earliest) { // ensure the earlier schedule is valid at the current time if (schedule.end > current) { @@ -399,16 +351,22 @@ CompositeSchedule calculate_composite_schedule(std::vector& in_c 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}); + combined.push_back( + {elapsed_seconds(current, now), NO_LIMIT_SPECIFIED, NO_LIMIT_SPECIFIED, std::nullopt, std::nullopt}); current = earliest; } else { // there is a schedule to use - const auto [limit, number_phases] = - convert_limit(chosen, selected_unit, default_number_phases, supply_voltage); + float current_limit = NO_LIMIT_SPECIFIED; + float power_limit = NO_LIMIT_SPECIFIED; + + if (chosen->charging_rate_unit == ChargingRateUnitEnum::A) { + current_limit = chosen->limit; + } else { + power_limit = chosen->limit; + } - ChargingSchedulePeriod charging_schedule_period{elapsed_seconds(current, now), limit, std::nullopt, - number_phases}; + IntermediatePeriod charging_schedule_period{elapsed_seconds(current, now), current_limit, power_limit, + chosen->number_phases, std::nullopt}; // If the new ChargingSchedulePeriod.phaseToUse field is set, pass it on // Profile validation has already ensured that the values have been properly set. @@ -416,7 +374,7 @@ CompositeSchedule calculate_composite_schedule(std::vector& in_c charging_schedule_period.phaseToUse = chosen->phase_to_use.value(); } - composite.chargingSchedulePeriod.push_back(charging_schedule_period); + combined.push_back(charging_schedule_period); if (chosen->end < next_earliest) { current = chosen->end; } else { @@ -425,160 +383,212 @@ CompositeSchedule calculate_composite_schedule(std::vector& in_c } } - return composite; + return combined; } -/// \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, const int32_t default_number_phases) { - - auto adjusted_period = prevailing_period; - - if (candidate_period.startPeriod == NO_START_PERIOD) { - return adjusted_period; - } +namespace { - 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_number_phases); +using period_iterator = IntermediateProfile::const_iterator; +using period_pair_vector = std::vector>; +using IntermediateProfileRef = std::reference_wrapper; - const auto period_max_phases = prevailing_period.numberPhases.value_or(default_number_phases); - adjusted_period.numberPhases = std::min(charge_point_max_phases, period_max_phases); +inline std::vector convert_to_ref_vector(const std::vector& profiles) { + std::vector references{}; + for (auto& profile : profiles) { + references.push_back(profile); + } + return references; +} - 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; +IntermediateProfile combine_list_of_profiles(const std::vector& profiles, + std::function combinator) { + if (profiles.empty()) { + // We should never get here as there are always profiles, otherwise there is a mistake in the calling function + // Return an empty profile to be safe + return {{0, NO_LIMIT_SPECIFIED, NO_LIMIT_SPECIFIED, std::nullopt, std::nullopt}}; + } + + IntermediateProfile combined{}; - adjusted_period.limit = std::floor(std::min(charge_point_limit_per_phase, period_limit_per_phase) * - adjusted_period.numberPhases.value()); + period_pair_vector profile_iterators{}; + for (const auto& wrapped_profile : profiles) { + auto& profile = wrapped_profile.get(); + if (!profile.empty()) { + profile_iterators.push_back(std::make_pair(profile.begin(), profile.end())); } } - return adjusted_period; -} + int32_t current_period = 0; + while (std::any_of(profile_iterators.begin(), profile_iterators.end(), + [](const std::pair& it) { return it.first != it.second; })) { -CompositeSchedule calculate_composite_schedule(const CompositeSchedule& charging_station_external_constraints, - const CompositeSchedule& charging_station_max, - const CompositeSchedule& tx_default, const CompositeSchedule& tx, - const CompositeScheduleDefaultLimits& default_limits, - int32_t supply_voltage) { + IntermediatePeriod period = combinator(profile_iterators); + period.startPeriod = current_period; - CompositeSchedule combined; - combined.chargingSchedulePeriod = {}; - combined.evseId = EVSEID_NOT_SET; - combined.duration = tx_default.duration; - combined.scheduleStart = tx_default.scheduleStart; - combined.chargingRateUnit = tx_default.chargingRateUnit; + if (combined.empty() || (period.current_limit != combined.back().current_limit) || + (period.power_limit != combined.back().power_limit) || + (period.numberPhases != combined.back().numberPhases)) { + combined.push_back(period); + } - const float default_limit = (tx_default.chargingRateUnit == ChargingRateUnitEnum::A) - ? static_cast(default_limits.amps) - : static_cast(default_limits.watts); + // Determine the next earliest period + int32_t next_lowest_period = std::numeric_limits::max(); - int current_period = 0; + for (const auto& [it, end] : profile_iterators) { + auto next = it + 1; + if (next != end && next->startPeriod > current_period && next->startPeriod < next_lowest_period) { + next_lowest_period = next->startPeriod; + } + } - const int end = std::max(charging_station_external_constraints.duration, - std::max(std::max(charging_station_max.duration, tx_default.duration), tx.duration)); + // If there is none, we are done + if (next_lowest_period == std::numeric_limits::max()) { + break; + } - 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(); + // Otherwise update to next earliest period + for (auto& [it, end] : profile_iterators) { + auto next = it + 1; + if (next != end && next->startPeriod == next_lowest_period) { + it++; + } + } + current_period = next_lowest_period; + } - 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()}; + if (combined.empty()) { + combined.push_back({0, NO_LIMIT_SPECIFIED, NO_LIMIT_SPECIFIED, std::nullopt, std::nullopt}); + } - 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}; + return combined; +} - update_itt(0, charging_station_external_constraints_itt, - charging_station_external_constraints.chargingSchedulePeriod.end(), - period_charging_station_external_constraints, duration_charging_station_external_constraints); +} // namespace - update_itt(0, charging_station_max_itt, charging_station_max.chargingSchedulePeriod.end(), - period_charging_station_max, duration_charging_station_max); +IntermediateProfile merge_tx_profile_with_tx_default_profile(const IntermediateProfile& tx_profile, + const IntermediateProfile& tx_default_profile) { - update_itt(0, tx_default_itt, tx_default.chargingSchedulePeriod.end(), period_tx_default, duration_tx_default); + auto combinator = [](const period_pair_vector& periods) { + IntermediatePeriod period{}; + period.current_limit = NO_LIMIT_SPECIFIED; + period.power_limit = NO_LIMIT_SPECIFIED; - update_itt(0, tx_itt, tx.chargingSchedulePeriod.end(), period_tx, duration_tx); + for (const auto& [it, end] : periods) { + if (it->current_limit != NO_LIMIT_SPECIFIED || it->power_limit != NO_LIMIT_SPECIFIED) { + period.current_limit = it->current_limit; + period.power_limit = it->power_limit; + period.numberPhases = it->numberPhases; + break; + } + } - ChargingSchedulePeriod last{1, NO_LIMIT_SPECIFIED, std::nullopt, default_limits.number_phases}; + return period; + }; - while (current_period < end) { + // This ordering together with the combinator will prefer the tx_profile above the default profile + std::vector profiles{tx_profile, tx_default_profile}; - // 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)); + return combine_list_of_profiles(profiles, combinator); +} - // create an unset period to override as needed. - ChargingSchedulePeriod period{NO_START_PERIOD, NO_LIMIT_SPECIFIED, std::nullopt, default_limits.number_phases}; +IntermediateProfile merge_profiles_by_lowest_limit(const std::vector& profiles) { + auto combinator = [](const period_pair_vector& periods) { + IntermediatePeriod period{}; + period.current_limit = std::numeric_limits::max(); + period.power_limit = std::numeric_limits::max(); - if (period_tx.startPeriod != NO_START_PERIOD) { - period = period_tx; - } + for (const auto& [it, end] : periods) { + if (it->current_limit >= 0.0F && it->current_limit < period.current_limit) { + period.current_limit = it->current_limit; + } + if (it->power_limit >= 0.0F && it->power_limit < period.power_limit) { + period.power_limit = it->power_limit; + } - 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; + // Copy number of phases if lower + if (!period.numberPhases.has_value()) { + // Don't care if this copies a nullopt, thats what it was already + period.numberPhases = it->numberPhases; + } else if (it->numberPhases.has_value() && it->numberPhases.value() < period.numberPhases.value()) { + period.numberPhases = it->numberPhases; } } - period = minimize_charging_schedule_period_by_limit(current_period, period, period_charging_station_max, - combined.chargingRateUnit, default_limits.number_phases); - period = minimize_charging_schedule_period_by_limit(current_period, period, - period_charging_station_external_constraints, - combined.chargingRateUnit, default_limits.number_phases); - - 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); - - if (period.startPeriod != NO_START_PERIOD) { + if (period.current_limit == std::numeric_limits::max()) { + period.current_limit = NO_LIMIT_SPECIFIED; + } + if (period.power_limit == std::numeric_limits::max()) { + period.power_limit = NO_LIMIT_SPECIFIED; + } + + return period; + }; + + return combine_list_of_profiles(convert_to_ref_vector(profiles), combinator); +} + +IntermediateProfile merge_profiles_by_summing_limits(const std::vector& profiles, + float current_default, float power_default) { + auto combinator = [current_default, power_default](const period_pair_vector& periods) { + IntermediatePeriod period{}; + for (const auto& [it, end] : periods) { + period.current_limit += it->current_limit >= 0.0F ? it->current_limit : current_default; + period.power_limit += it->power_limit >= 0.0F ? it->power_limit : power_default; + + // Copy number of phases if higher if (!period.numberPhases.has_value()) { - period.numberPhases = default_limits.number_phases; + // Don't care if this copies a nullopt, thats what it was already + period.numberPhases = it->numberPhases; + } else if (it->numberPhases.has_value() && it->numberPhases.value() > period.numberPhases.value()) { + period.numberPhases = it->numberPhases; } + } + return period; + }; - if (period.limit == NO_LIMIT_SPECIFIED) { - period.limit = default_limit; - } - period.startPeriod = current_period; + return combine_list_of_profiles(convert_to_ref_vector(profiles), combinator); +} - // 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; +std::vector +convert_intermediate_into_schedule(const IntermediateProfile& profile, ChargingRateUnitEnum charging_rate_unit, + float default_limit, int32_t default_number_phases, float supply_voltage) { + + std::vector output{}; + + for (const auto& period : profile) { + ChargingSchedulePeriod period_out{}; + period_out.startPeriod = period.startPeriod; + period_out.numberPhases = period.numberPhases; + + if (period.current_limit == NO_LIMIT_SPECIFIED && period.power_limit == NO_LIMIT_SPECIFIED) { + period_out.limit = default_limit; } else { - combined.chargingSchedulePeriod.push_back( - {current_period, default_limit, std::nullopt, default_limits.number_phases}); - current_period = end; + float transform_value = supply_voltage * period_out.numberPhases.value_or(default_number_phases); + period_out.limit = std::numeric_limits::max(); + if (charging_rate_unit == ChargingRateUnitEnum::A) { + if (period.current_limit != NO_LIMIT_SPECIFIED) { + period_out.limit = period.current_limit; + } + if (period.power_limit != NO_LIMIT_SPECIFIED) { + period_out.limit = std::min(period_out.limit, period.power_limit / transform_value); + } + } else { + if (period.power_limit != NO_LIMIT_SPECIFIED) { + period_out.limit = period.power_limit; + } + if (period.current_limit != NO_LIMIT_SPECIFIED) { + period_out.limit = std::min(period_out.limit, period.current_limit * transform_value); + } + } + } + + if (output.empty() || (period_out.limit != output.back().limit) || + (period_out.numberPhases != output.back().numberPhases)) { + output.push_back(period_out); } } - return combined; + return output; } } // namespace v201 diff --git a/tests/lib/ocpp/v201/CMakeLists.txt b/tests/lib/ocpp/v201/CMakeLists.txt index 3af4b6260..0721b8ab9 100644 --- a/tests/lib/ocpp/v201/CMakeLists.txt +++ b/tests/lib/ocpp/v201/CMakeLists.txt @@ -18,7 +18,7 @@ target_sources(libocpp_unit_tests PRIVATE 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/no_gap/TxDefaultProfile_401.json b/tests/lib/ocpp/v201/json/no_gap/TxDefaultProfile_401.json new file mode 100644 index 000000000..43b606225 --- /dev/null +++ b/tests/lib/ocpp/v201/json/no_gap/TxDefaultProfile_401.json @@ -0,0 +1,21 @@ +{ + "id": 401, + "chargingProfileKind": "Recurring", + "chargingProfilePurpose": "TxDefaultProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "A", + "chargingSchedulePeriod": [ + { + "limit": 16.0, + "startPeriod": 0 + } + ], + "duration": 300, + "startSchedule": "2024-01-01T08:00:00Z" + } + ], + "recurrencyKind": "Daily", + "stackLevel": 5 +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/no_gap/TxDefaultProfile_402.json b/tests/lib/ocpp/v201/json/no_gap/TxDefaultProfile_402.json new file mode 100644 index 000000000..842b5b2a1 --- /dev/null +++ b/tests/lib/ocpp/v201/json/no_gap/TxDefaultProfile_402.json @@ -0,0 +1,21 @@ +{ + "id": 402, + "chargingProfileKind": "Recurring", + "chargingProfilePurpose": "TxDefaultProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "A", + "chargingSchedulePeriod": [ + { + "limit": 12.0, + "startPeriod": 0 + } + ], + "duration": 300, + "startSchedule": "2024-01-01T08:05:00Z" + } + ], + "recurrencyKind": "Daily", + "stackLevel": 5 +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/singles/ChargingStationMaxProfile_24_Ampere.json b/tests/lib/ocpp/v201/json/singles/ChargingStationMaxProfile_24_Ampere.json new file mode 100644 index 000000000..173802a66 --- /dev/null +++ b/tests/lib/ocpp/v201/json/singles/ChargingStationMaxProfile_24_Ampere.json @@ -0,0 +1,38 @@ +{ + "id": 24, + "chargingProfileKind": "Recurring", + "chargingProfilePurpose": "ChargingStationMaxProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "A", + "chargingSchedulePeriod": [ + { + "limit": 16.0, + "numberPhases": 1, + "startPeriod": 0 + }, + { + "limit": 16.0, + "numberPhases": 3, + "startPeriod": 3600 + }, + { + "limit": 10.0, + "numberPhases": 1, + "startPeriod": 7200 + }, + { + "limit": 10.0, + "numberPhases": 3, + "startPeriod": 10800 + } + ], + "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/singles/ChargingStationMaxProfile_401.json b/tests/lib/ocpp/v201/json/singles/ChargingStationMaxProfile_401.json new file mode 100644 index 000000000..8089a2d6c --- /dev/null +++ b/tests/lib/ocpp/v201/json/singles/ChargingStationMaxProfile_401.json @@ -0,0 +1,38 @@ +{ + "id": 24, + "chargingProfileKind": "Recurring", + "chargingProfilePurpose": "ChargingStationMaxProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "A", + "chargingSchedulePeriod": [ + { + "limit": 24.0, + "numberPhases": 1, + "startPeriod": 0 + }, + { + "limit": 28.0, + "numberPhases": 1, + "startPeriod": 900 + }, + { + "limit": 30.0, + "numberPhases": 1, + "startPeriod": 1800 + }, + { + "limit": 32.0, + "numberPhases": 1, + "startPeriod": 2700 + } + ], + "duration": 86400, + "minChargingRate": 0.0, + "startSchedule": "2024-01-17T08:00:00.000Z" + } + ], + "recurrencyKind": "Daily", + "stackLevel": 0 +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/singles/Recurring_Daily_301.json b/tests/lib/ocpp/v201/json/singles/Recurring_Daily_301.json index 65e7b82bd..7f6b2bd98 100644 --- a/tests/lib/ocpp/v201/json/singles/Recurring_Daily_301.json +++ b/tests/lib/ocpp/v201/json/singles/Recurring_Daily_301.json @@ -8,15 +8,15 @@ "chargingRateUnit": "A", "chargingSchedulePeriod": [ { - "limit": 32.0, + "limit": 16.0, "startPeriod": 0 }, { - "limit": 31.0, + "limit": 15.0, "startPeriod": 1800 }, { - "limit": 30.0, + "limit": 14.0, "startPeriod": 2700 } ], diff --git a/tests/lib/ocpp/v201/json/singles/Recurring_Daily_302_phase_limit.json b/tests/lib/ocpp/v201/json/singles/Recurring_Daily_302_phase_limit.json new file mode 100644 index 000000000..054208620 --- /dev/null +++ b/tests/lib/ocpp/v201/json/singles/Recurring_Daily_302_phase_limit.json @@ -0,0 +1,34 @@ +{ + "id": 302, + "chargingProfileKind": "Recurring", + "chargingProfilePurpose": "TxDefaultProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "A", + "chargingSchedulePeriod": [ + { + "limit": 16.0, + "numberPhases": 1, + "startPeriod": 0 + }, + { + "limit": 15.0, + "numberPhases": 1, + "startPeriod": 1800 + }, + { + "limit": 14.0, + "numberPhases": 3, + "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/Relative_302_phase_limit.json b/tests/lib/ocpp/v201/json/singles/Relative_302_phase_limit.json new file mode 100644 index 000000000..52f1a9c12 --- /dev/null +++ b/tests/lib/ocpp/v201/json/singles/Relative_302_phase_limit.json @@ -0,0 +1,32 @@ +{ + "id": 302, + "chargingProfileKind": "Relative", + "chargingProfilePurpose": "TxDefaultProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "A", + "chargingSchedulePeriod": [ + { + "limit": 16.0, + "numberPhases": 3, + "startPeriod": 0 + }, + { + "limit": 15.0, + "numberPhases": 1, + "startPeriod": 1800 + }, + { + "limit": 14.0, + "numberPhases": 3, + "startPeriod": 2700 + } + ], + "duration": 3600 + } + ], + "stackLevel": 5, + "validFrom": "2024-01-01T12:00:00Z", + "validTo": "2025-01-01T14:00:00Z" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/singles/Relative_303.json b/tests/lib/ocpp/v201/json/singles/Relative_303.json new file mode 100644 index 000000000..6eef6253e --- /dev/null +++ b/tests/lib/ocpp/v201/json/singles/Relative_303.json @@ -0,0 +1,29 @@ +{ + "id": 301, + "chargingProfileKind": "Relative", + "chargingProfilePurpose": "TxDefaultProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "A", + "chargingSchedulePeriod": [ + { + "limit": 16.0, + "startPeriod": 0 + }, + { + "limit": 15.0, + "startPeriod": 1800 + }, + { + "limit": 14.0, + "startPeriod": 2700 + } + ], + "duration": 3600 + } + ], + "stackLevel": 5, + "validFrom": "2024-01-01T12:00:00Z", + "validTo": "2025-01-01T14:00:00Z" +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/json/singles/TXDefaultProfile_25_Watt.json b/tests/lib/ocpp/v201/json/singles/TXDefaultProfile_25_Watt.json new file mode 100644 index 000000000..d25bc378d --- /dev/null +++ b/tests/lib/ocpp/v201/json/singles/TXDefaultProfile_25_Watt.json @@ -0,0 +1,36 @@ +{ + "id": 25, + "chargingProfileKind": "Relative", + "chargingProfilePurpose": "TxDefaultProfile", + "chargingSchedule": [ + { + "id": 0, + "chargingRateUnit": "W", + "chargingSchedulePeriod": [ + { + "limit": 12420.0, + "startPeriod": 0 + }, + { + "limit": 8280.0, + "startPeriod": 300 + }, + { + "limit": 6210.0, + "startPeriod": 600 + }, + { + "limit": 4140.0, + "startPeriod": 900 + }, + { + "limit": 2070.0, + "startPeriod": 1200 + } + ], + "duration": 3600, + "minChargingRate": 0.0 + } + ], + "stackLevel": 0 +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/smart_charging_matchers.hpp b/tests/lib/ocpp/v201/smart_charging_matchers.hpp new file mode 100644 index 000000000..5c61b6bf6 --- /dev/null +++ b/tests/lib/ocpp/v201/smart_charging_matchers.hpp @@ -0,0 +1,24 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest + +#pragma once + +#include + +#include + +MATCHER_P2(PeriodEquals, start, limit, + "Period start " + testing::DescribeMatcher(start, negation) + " and limit " + + testing::DescribeMatcher(limit, negation)) { + return ExplainMatchResult(start, arg.startPeriod, result_listener) && + ExplainMatchResult(limit, arg.limit, result_listener); +} + +MATCHER_P3(PeriodEqualsWithPhases, start, limit, phases, + "Period start " + testing::DescribeMatcher(start, negation) + " and limit " + + testing::DescribeMatcher(limit, negation) + " and phases " + + testing::DescribeMatcher>(phases, negation)) { + return ExplainMatchResult(start, arg.startPeriod, result_listener) && + ExplainMatchResult(limit, arg.limit, result_listener) && + ExplainMatchResult(phases, arg.numberPhases, result_listener); +} \ No newline at end of file diff --git a/tests/lib/ocpp/v201/test_composite_schedule.cpp b/tests/lib/ocpp/v201/test_composite_schedule.cpp index 94e44a61c..c365d52f5 100644 --- a/tests/lib/ocpp/v201/test_composite_schedule.cpp +++ b/tests/lib/ocpp/v201/test_composite_schedule.cpp @@ -40,6 +40,7 @@ #include "mocks/database_handler_fake.hpp" +#include "smart_charging_matchers.hpp" #include "smart_charging_test_utils.hpp" #include @@ -51,6 +52,9 @@ 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; +constexpr int32_t DEFAULT_LIMIT_AMPERE = 57; +constexpr int32_t DEFAULT_LIMIT_WATT = 55612; +constexpr int32_t DEFAULT_NR_PHASES = 3; static const std::string DEFAULT_TX_ID = "f1522902-1170-416f-8e43-9e3bce28fde7"; static const std::string MIGRATION_FILES_PATH = "./resources/v201/device_model_migration_files"; static const std::string CONFIG_PATH = "./resources/example_config/v201/component_config"; @@ -151,7 +155,7 @@ class CompositeScheduleTestFixtureV201 : public DatabaseTestingUtils { } CompositeScheduleTestFixtureV201() : - evse_manager(NR_OF_EVSES), + evse_manager(std::make_unique(NR_OF_EVSES)), device_model_test_helper(), mock_dispatcher(), device_model(device_model_test_helper.get_device_model()), @@ -167,38 +171,101 @@ class CompositeScheduleTestFixtureV201 : public DatabaseTestingUtils { 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, "true", "test", true); + + const auto& default_limit_amps_cv = ControllerComponentVariables::CompositeScheduleDefaultLimitAmps; + device_model->set_value(default_limit_amps_cv.component, default_limit_amps_cv.variable.value(), + AttributeEnum::Actual, std::to_string(DEFAULT_LIMIT_AMPERE), "test", true); + + const auto& default_limit_watts_cv = ControllerComponentVariables::CompositeScheduleDefaultLimitWatts; + device_model->set_value(default_limit_watts_cv.component, default_limit_watts_cv.variable.value(), + AttributeEnum::Actual, std::to_string(DEFAULT_LIMIT_WATT), "test", true); + + const auto& default_limit_phases_cv = ControllerComponentVariables::CompositeScheduleDefaultNumberPhases; + device_model->set_value(default_limit_phases_cv.component, default_limit_phases_cv.variable.value(), + AttributeEnum::Actual, std::to_string(DEFAULT_NR_PHASES), "test", true); } - TestSmartCharging create_smart_charging_handler() { + std::unique_ptr create_smart_charging_handler() { std::unique_ptr database_connection = std::make_unique(fs::path("/tmp/ocpp201") / "cp.db"); this->database_handler = std::make_unique(std::move(database_connection), MIGRATION_FILES_LOCATION_V201); database_handler->open_connection(); - return TestSmartCharging(*device_model, this->evse_manager, connectivity_manager, mock_dispatcher, - *database_handler, set_charging_profiles_callback_mock.AsStdFunction()); + return std::make_unique(*device_model, *this->evse_manager, connectivity_manager, + mock_dispatcher, *database_handler, + set_charging_profiles_callback_mock.AsStdFunction()); } - // Default values used within the tests - EvseManagerFake evse_manager; + void reconfigure_for_nr_of_evses(int32_t nr_of_evses) { + this->evse_manager = std::make_unique(nr_of_evses); + this->handler = std::make_unique(*device_model, *this->evse_manager, connectivity_manager, + mock_dispatcher, *database_handler, + set_charging_profiles_callback_mock.AsStdFunction()); + } - sqlite3* db_handle; + // Default values used within the tests + std::unique_ptr evse_manager; - bool ignore_no_transaction = true; DeviceModelTestHelper device_model_test_helper; MockMessageDispatcher mock_dispatcher; DeviceModel* device_model; ::testing::NiceMock connectivity_manager; std::unique_ptr database_handler{}; MockFunction set_charging_profiles_callback_mock; - TestSmartCharging handler; + std::unique_ptr handler; boost::uuids::random_generator uuid_generator = boost::uuids::random_generator(); }; -TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_FoundationTest_Grid) { +TEST_F(CompositeScheduleTestFixtureV201, NoSchedulesPresent) { + const DateTime start_time = ocpp::DateTime("2024-01-02T00:00:00"); + const DateTime end_time = ocpp::DateTime("2024-01-02T01:00:00"); + + CompositeSchedule result = handler->calculate_composite_schedule(start_time, end_time, STATION_WIDE_ID, + ChargingRateUnitEnum::A, false, true); + + EXPECT_EQ(result.evseId, STATION_WIDE_ID); + EXPECT_EQ(result.scheduleStart, start_time); + EXPECT_EQ(result.duration, 3600); + EXPECT_EQ(result.chargingRateUnit, ChargingRateUnitEnum::A); + + // clang-format off + EXPECT_THAT(result.chargingSchedulePeriod, + testing::ElementsAre( + PeriodEquals(0, DEFAULT_LIMIT_AMPERE) + )); + // clang-format on +} + +TEST_F(CompositeScheduleTestFixtureV201, ExtraSeconds) { + this->load_charging_profiles_for_evse("singles/Absolute_301.json", STATION_WIDE_ID); + + const DateTime start_time = ocpp::DateTime("2024-01-01T12:01:59"); + const DateTime end_time = ocpp::DateTime("2024-01-01T13:02:01"); + + CompositeSchedule result = handler->calculate_composite_schedule(start_time, end_time, STATION_WIDE_ID, + ChargingRateUnitEnum::A, false, true); + + EXPECT_EQ(result.evseId, STATION_WIDE_ID); + EXPECT_EQ(result.scheduleStart, start_time); + EXPECT_EQ(result.duration, 3602); + EXPECT_EQ(result.chargingRateUnit, ChargingRateUnitEnum::A); + + // clang-format off + EXPECT_THAT(result.chargingSchedulePeriod, + testing::ElementsAre( + PeriodEquals( 0, DEFAULT_LIMIT_AMPERE), + PeriodEquals( 1, 32.0F), + PeriodEquals(1801, 31.0F), + PeriodEquals(2701, 30.0F), + PeriodEquals(3601, DEFAULT_LIMIT_AMPERE) + )); + // clang-format on +} + +TEST_F(CompositeScheduleTestFixtureV201, FoundationTest_Grid) { this->load_charging_profiles_for_evse(BASE_JSON_PATH + "/grid/", DEFAULT_EVSE_ID); - evse_manager.open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); + evse_manager->open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); const DateTime start_time = ocpp::DateTime("2024-01-17T00:00:00"); const DateTime end_time = ocpp::DateTime("2024-01-18T00:00:00"); @@ -307,16 +374,16 @@ TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_Foundati expected.scheduleStart = start_time; expected.chargingRateUnit = ChargingRateUnitEnum::W; - CompositeSchedule actual = handler.calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, - ChargingRateUnitEnum::W, false, false); + CompositeSchedule actual = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::W, false, false); ASSERT_EQ(actual, expected); } -TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_LayeredTest_SameStartTime) { +TEST_F(CompositeScheduleTestFixtureV201, LayeredTest_SameStartTime) { this->load_charging_profiles_for_evse(BASE_JSON_PATH + "/layered/", DEFAULT_EVSE_ID); - evse_manager.open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); + evse_manager->open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); // Time Window: START = Stack #1 start time || END = Stack #1 end time { @@ -334,8 +401,8 @@ TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_LayeredT expected.scheduleStart = start_time; expected.chargingRateUnit = ChargingRateUnitEnum::W; - CompositeSchedule actual = handler.calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, - ChargingRateUnitEnum::W, false, false); + CompositeSchedule actual = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::W, false, false); ASSERT_EQ(actual, expected); } @@ -360,8 +427,8 @@ TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_LayeredT expected.scheduleStart = start_time; expected.chargingRateUnit = ChargingRateUnitEnum::W; - CompositeSchedule actual = handler.calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, - ChargingRateUnitEnum::W, false, false); + CompositeSchedule actual = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::W, false, false); ASSERT_EQ(actual, expected); } @@ -391,17 +458,17 @@ TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_LayeredT expected.scheduleStart = start_time; expected.chargingRateUnit = ChargingRateUnitEnum::W; - CompositeSchedule actual = handler.calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, - ChargingRateUnitEnum::W, false, false); + CompositeSchedule actual = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::W, false, false); ASSERT_EQ(actual, expected); } } -TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_LayeredRecurringTest_FutureStartTime) { +TEST_F(CompositeScheduleTestFixtureV201, LayeredRecurringTest_FutureStartTime) { this->load_charging_profiles_for_evse(BASE_JSON_PATH + "/layered_recurring/", DEFAULT_EVSE_ID); - evse_manager.open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); + evse_manager->open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); const DateTime start_time = ocpp::DateTime("2024-02-17T18:04:00"); const DateTime end_time = ocpp::DateTime("2024-02-17T18:05:00"); @@ -417,23 +484,22 @@ TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_LayeredR expected.scheduleStart = start_time; expected.chargingRateUnit = ChargingRateUnitEnum::W; - CompositeSchedule actual = handler.calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, - ChargingRateUnitEnum::W, false, false); + CompositeSchedule actual = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::W, false, false); ASSERT_EQ(actual, expected); } -TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_LayeredTest_PreviousStartTime) { +TEST_F(CompositeScheduleTestFixtureV201, LayeredTest_PreviousStartTime) { this->load_charging_profiles_for_evse("singles/TXProfile_Absolute_Start18-04.json", DEFAULT_EVSE_ID); - evse_manager.open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); + evse_manager->open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); const DateTime start_time = ocpp::DateTime("2024-01-17T18:00:00"); const DateTime end_time = ocpp::DateTime("2024-01-17T18:05:00"); ChargingSchedulePeriod period1; period1.startPeriod = 0; - period1.limit = DEFAULT_LIMIT_WATTS; - period1.numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES; + period1.limit = DEFAULT_LIMIT_WATT; ChargingSchedulePeriod period2; period2.startPeriod = 240; period2.limit = 2000; @@ -445,16 +511,16 @@ TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_LayeredT expected.scheduleStart = start_time; expected.chargingRateUnit = ChargingRateUnitEnum::W; - CompositeSchedule actual = handler.calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, - ChargingRateUnitEnum::W, false, false); + CompositeSchedule actual = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::W, false, false); ASSERT_EQ(actual, expected); } -TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_LayeredRecurringTest_PreviousStartTime) { +TEST_F(CompositeScheduleTestFixtureV201, LayeredRecurringTest_PreviousStartTime) { this->load_charging_profiles_for_evse(BASE_JSON_PATH + "/layered_recurring/", DEFAULT_EVSE_ID); - evse_manager.open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); + evse_manager->open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); const DateTime start_time = ocpp::DateTime("2024-02-19T18:00:00"); const DateTime end_time = ocpp::DateTime("2024-02-19T19:04:00"); @@ -481,8 +547,8 @@ TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_LayeredR expected.scheduleStart = start_time; expected.chargingRateUnit = ChargingRateUnitEnum::W; - CompositeSchedule actual = handler.calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, - ChargingRateUnitEnum::W, false, false); + CompositeSchedule actual = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::W, false, false); ASSERT_EQ(actual, expected); } @@ -490,7 +556,7 @@ TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_LayeredR /** * Calculate Composite Schedule */ -TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_ValidateBaselineProfileVector) { +TEST_F(CompositeScheduleTestFixtureV201, 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(); @@ -498,7 +564,7 @@ TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_Validate ON_CALL(*database_handler, get_charging_profiles_for_evse(DEFAULT_EVSE_ID)) .WillByDefault(testing::Return(profiles)); - evse_manager.open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); + evse_manager->open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); ChargingSchedulePeriod period1; period1.startPeriod = 0; @@ -519,20 +585,21 @@ TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_Validate expected.scheduleStart = start_time; expected.chargingRateUnit = ChargingRateUnitEnum::W; - CompositeSchedule actual = handler.calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, - ChargingRateUnitEnum::W, false, false); + CompositeSchedule actual = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::W, false, false); ASSERT_EQ(actual, expected); } -TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_RelativeProfile_minutia) { +TEST_F(CompositeScheduleTestFixtureV201, RelativeProfile_minutia) { this->load_charging_profiles_for_evse(BASE_JSON_PATH + "/relative/", DEFAULT_EVSE_ID); const DateTime start_time = ocpp::DateTime("2024-05-17T05:00:00"); const DateTime end_time = ocpp::DateTime("2024-05-17T06:00:00"); - this->evse_manager.open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); - this->evse_manager.get_evse(DEFAULT_EVSE_ID).get_transaction()->start_time = start_time; + this->evse_manager->open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); + this->evse_manager->get_evse(DEFAULT_EVSE_ID).get_transaction()->start_time = start_time; + ChargingSchedulePeriod period; period.startPeriod = 0; period.limit = 2000.0; @@ -544,20 +611,20 @@ TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_Relative expected.scheduleStart = start_time; expected.chargingRateUnit = ChargingRateUnitEnum::W; - CompositeSchedule actual = handler.calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, - ChargingRateUnitEnum::W, false, false); + CompositeSchedule actual = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::W, false, false); ASSERT_EQ(actual, expected); } -TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_RelativeProfile_e2e) { +TEST_F(CompositeScheduleTestFixtureV201, RelativeProfile_e2e) { const DateTime start_time = ocpp::DateTime("2024-05-17T05:00:00"); const DateTime end_time = ocpp::DateTime("2024-05-17T06:01:00"); this->load_charging_profiles_for_evse(BASE_JSON_PATH + "/relative/", DEFAULT_EVSE_ID); - this->evse_manager.open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); - this->evse_manager.get_evse(DEFAULT_EVSE_ID).get_transaction()->start_time = start_time; + this->evse_manager->open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); + this->evse_manager->get_evse(DEFAULT_EVSE_ID).get_transaction()->start_time = start_time; ChargingSchedulePeriod period1; period1.startPeriod = 0; @@ -574,16 +641,16 @@ TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_Relative expected.scheduleStart = start_time; expected.chargingRateUnit = ChargingRateUnitEnum::W; - CompositeSchedule actual = handler.calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, - ChargingRateUnitEnum::W, false, false); + CompositeSchedule actual = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::W, false, false); ASSERT_EQ(actual, expected); } -TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_DemoCaseOne_17th) { +TEST_F(CompositeScheduleTestFixtureV201, DemoCaseOne_17th) { this->load_charging_profiles_for_evse(BASE_JSON_PATH + "/case_one/", DEFAULT_EVSE_ID); - evse_manager.open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); + evse_manager->open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); const DateTime start_time = ocpp::DateTime("2024-01-17T18:00:00"); const DateTime end_time = ocpp::DateTime("2024-01-18T06:00:00"); @@ -607,16 +674,16 @@ TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_DemoCase expected.scheduleStart = start_time; expected.chargingRateUnit = ChargingRateUnitEnum::W; - CompositeSchedule actual = handler.calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, - ChargingRateUnitEnum::W, false, false); + CompositeSchedule actual = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::W, false, false); ASSERT_EQ(actual, expected); } -TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_DemoCaseOne_19th) { +TEST_F(CompositeScheduleTestFixtureV201, DemoCaseOne_19th) { this->load_charging_profiles_for_evse(BASE_JSON_PATH + "/case_one/", DEFAULT_EVSE_ID); - evse_manager.open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); + evse_manager->open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); const DateTime start_time = ocpp::DateTime("2024-01-19T18:00:00"); const DateTime end_time = ocpp::DateTime("2024-01-20T06:00:00"); @@ -636,17 +703,17 @@ TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_DemoCase expected.scheduleStart = start_time; expected.chargingRateUnit = ChargingRateUnitEnum::W; - CompositeSchedule actual = handler.calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, - ChargingRateUnitEnum::W, false, false); + CompositeSchedule actual = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::W, false, false); ASSERT_EQ(actual, expected); } -TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_MaxOverridesHigherLimits) { +TEST_F(CompositeScheduleTestFixtureV201, MaxOverridesHigherLimits) { this->load_charging_profiles_for_evse(BASE_JSON_PATH + "/max/0/", STATION_WIDE_ID); this->load_charging_profiles_for_evse(BASE_JSON_PATH + "/max/1/", DEFAULT_EVSE_ID); - evse_manager.open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); + evse_manager->open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); const DateTime start_time = ocpp::DateTime("2024-01-17T00:00:00"); const DateTime end_time = ocpp::DateTime("2024-01-17T02:00:00"); @@ -666,16 +733,16 @@ TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_MaxOverr expected.scheduleStart = start_time; expected.chargingRateUnit = ChargingRateUnitEnum::W; - CompositeSchedule actual = handler.calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, - ChargingRateUnitEnum::W, false, false); + CompositeSchedule actual = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::W, false, false); ASSERT_EQ(actual, expected); } -TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_MaxOverridenByLowerLimits) { +TEST_F(CompositeScheduleTestFixtureV201, MaxOverridenByLowerLimits) { this->load_charging_profiles_for_evse(BASE_JSON_PATH + "/max/0/", STATION_WIDE_ID); this->load_charging_profiles_for_evse(BASE_JSON_PATH + "/max/1/", DEFAULT_EVSE_ID); - evse_manager.open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); + evse_manager->open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); const DateTime start_time = ocpp::DateTime("2024-01-17T22:00:00"); const DateTime end_time = ocpp::DateTime("2024-01-18T00:00:00"); @@ -695,16 +762,16 @@ TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_MaxOverr expected.scheduleStart = start_time; expected.chargingRateUnit = ChargingRateUnitEnum::W; - CompositeSchedule actual = handler.calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, - ChargingRateUnitEnum::W, false, false); + CompositeSchedule actual = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::W, false, false); ASSERT_EQ(actual, expected); } -TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_ExternalOverridesHigherLimits) { +TEST_F(CompositeScheduleTestFixtureV201, ExternalOverridesHigherLimits) { this->load_charging_profiles_for_evse(BASE_JSON_PATH + "/external/0/", STATION_WIDE_ID); this->load_charging_profiles_for_evse(BASE_JSON_PATH + "/external/1/", DEFAULT_EVSE_ID); - evse_manager.open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); + evse_manager->open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); const DateTime start_time = ocpp::DateTime("2024-01-17T00:00:00"); const DateTime end_time = ocpp::DateTime("2024-01-17T02:00:00"); @@ -724,16 +791,16 @@ TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_External expected.scheduleStart = start_time; expected.chargingRateUnit = ChargingRateUnitEnum::W; - CompositeSchedule actual = handler.calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, - ChargingRateUnitEnum::W, false, false); + CompositeSchedule actual = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::W, false, false); ASSERT_EQ(actual, expected); } -TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_ExternalOverridenByLowerLimits) { +TEST_F(CompositeScheduleTestFixtureV201, ExternalOverridenByLowerLimits) { this->load_charging_profiles_for_evse(BASE_JSON_PATH + "/external/0/", STATION_WIDE_ID); this->load_charging_profiles_for_evse(BASE_JSON_PATH + "/external/1/", DEFAULT_EVSE_ID); - evse_manager.open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); + evse_manager->open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); const DateTime start_time = ocpp::DateTime("2024-01-17T22:00:00"); const DateTime end_time = ocpp::DateTime("2024-01-18T00:00:00"); @@ -753,8 +820,8 @@ TEST_F(CompositeScheduleTestFixtureV201, K08_CalculateCompositeSchedule_External expected.scheduleStart = start_time; expected.chargingRateUnit = ChargingRateUnitEnum::W; - CompositeSchedule actual = handler.calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, - ChargingRateUnitEnum::W, false, false); + CompositeSchedule actual = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::W, false, false); ASSERT_EQ(actual, expected); } @@ -762,7 +829,7 @@ TEST_F(CompositeScheduleTestFixtureV201, OCTT_TC_K_41_CS) { this->load_charging_profiles_for_evse(BASE_JSON_PATH + "/OCCT_TC_K_41_CS/0/", STATION_WIDE_ID); this->load_charging_profiles_for_evse(BASE_JSON_PATH + "/OCCT_TC_K_41_CS/1/", DEFAULT_EVSE_ID); - evse_manager.open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); + evse_manager->open_transaction(DEFAULT_EVSE_ID, DEFAULT_TX_ID); const DateTime start_time = ocpp::DateTime("2024-08-21T12:24:40"); const DateTime end_time = ocpp::DateTime("2024-08-21T12:31:20"); @@ -798,10 +865,440 @@ TEST_F(CompositeScheduleTestFixtureV201, OCTT_TC_K_41_CS) { expected.scheduleStart = start_time; expected.chargingRateUnit = ChargingRateUnitEnum::A; - CompositeSchedule actual = handler.calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, - ChargingRateUnitEnum::A, false, false); + CompositeSchedule actual = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::A, false, false); ASSERT_EQ(actual, expected); } +TEST_F(CompositeScheduleTestFixtureV201, SingleStationMaxForEvse0) { + this->load_charging_profiles_for_evse("singles/ChargingStationMaxProfile_401.json", STATION_WIDE_ID); + + const DateTime start_time = ocpp::DateTime("2024-08-21T08:00:00"); + const DateTime end_time = ocpp::DateTime("2024-08-21T09:00:00"); + + CompositeSchedule result = handler->calculate_composite_schedule(start_time, end_time, STATION_WIDE_ID, + ChargingRateUnitEnum::A, false, false); + + EXPECT_EQ(result.evseId, STATION_WIDE_ID); + EXPECT_EQ(result.scheduleStart, start_time); + EXPECT_EQ(result.duration, 3600); + EXPECT_EQ(result.chargingRateUnit, ChargingRateUnitEnum::A); + + // clang-format off + EXPECT_THAT(result.chargingSchedulePeriod, + testing::ElementsAre( + PeriodEqualsWithPhases( 0, 24.0F, 1), + PeriodEqualsWithPhases( 900, 28.0F, 1), + PeriodEqualsWithPhases( 1800, 30.0F, 1), + PeriodEqualsWithPhases( 2700, 32.0F, 1) + )); + // clang-format on +} + +TEST_F(CompositeScheduleTestFixtureV201, SingleTxDefaultProfileForEvse0WithSingleEvse) { + this->load_charging_profiles_for_evse("singles/Relative_303.json", STATION_WIDE_ID); + + const DateTime start_time = ocpp::DateTime("2024-01-02T08:00:00"); + const DateTime end_time = ocpp::DateTime("2024-01-02T09:00:00"); + + CompositeSchedule result = handler->calculate_composite_schedule(start_time, end_time, STATION_WIDE_ID, + ChargingRateUnitEnum::A, false, true); + + EXPECT_EQ(result.evseId, STATION_WIDE_ID); + EXPECT_EQ(result.scheduleStart, start_time); + EXPECT_EQ(result.duration, 3600); + EXPECT_EQ(result.chargingRateUnit, ChargingRateUnitEnum::A); + + // clang-format off + EXPECT_THAT(result.chargingSchedulePeriod, + testing::ElementsAre( + PeriodEqualsWithPhases( 0, 16.0F, 3), + PeriodEqualsWithPhases( 1800, 15.0F, 3), + PeriodEqualsWithPhases( 2700, 14.0F, 3) + )); + // clang-format on +} + +TEST_F(CompositeScheduleTestFixtureV201, SingleTxDefaultProfileForEvse0WithMultipleEvses_Current) { + this->load_charging_profiles_for_evse("singles/Relative_303.json", STATION_WIDE_ID); + + const DateTime start_time = ocpp::DateTime("2024-01-02T08:00:00"); + const DateTime end_time = ocpp::DateTime("2024-01-02T09:00:00"); + + constexpr int32_t nr_of_evses = 2; + + this->reconfigure_for_nr_of_evses(nr_of_evses); + + CompositeSchedule result = handler->calculate_composite_schedule(start_time, end_time, STATION_WIDE_ID, + ChargingRateUnitEnum::A, false, true); + + EXPECT_EQ(result.evseId, STATION_WIDE_ID); + EXPECT_EQ(result.scheduleStart, start_time); + EXPECT_EQ(result.duration, 3600); + EXPECT_EQ(result.chargingRateUnit, ChargingRateUnitEnum::A); + + // clang-format off + EXPECT_THAT(result.chargingSchedulePeriod, + testing::ElementsAre( + PeriodEqualsWithPhases( 0, nr_of_evses * 16.0F, 3), + PeriodEqualsWithPhases( 1800, nr_of_evses * 15.0F, 3), + PeriodEqualsWithPhases( 2700, nr_of_evses * 14.0F, 3) + )); + // clang-format on +} + +TEST_F(CompositeScheduleTestFixtureV201, SingleTxDefaultProfileForEvse0WithMultipleEvses_Power) { + this->load_charging_profiles_for_evse("singles/TXDefaultProfile_25_Watt.json", STATION_WIDE_ID); + + constexpr int32_t nr_of_evses = 2; + + this->reconfigure_for_nr_of_evses(nr_of_evses); + + // Note the time is 1 minute off the whole hour + const DateTime start_time = ocpp::DateTime("2024-01-02T08:00:00"); + const DateTime end_time = ocpp::DateTime("2024-01-02T09:00:00"); + + CompositeSchedule result = handler->calculate_composite_schedule(start_time, end_time, STATION_WIDE_ID, + ChargingRateUnitEnum::A, false, true); + + EXPECT_EQ(result.evseId, STATION_WIDE_ID); + EXPECT_EQ(result.scheduleStart, start_time); + EXPECT_EQ(result.duration, 3600); + EXPECT_EQ(result.chargingRateUnit, ChargingRateUnitEnum::A); + + // clang-format off + EXPECT_THAT(result.chargingSchedulePeriod, + testing::ElementsAre( + PeriodEqualsWithPhases( 0, 36.0F, 3), + PeriodEqualsWithPhases( 300, 24.0F, 3), + PeriodEqualsWithPhases( 600, 18.0F, 3), + PeriodEqualsWithPhases( 900, 12.0F, 3), + PeriodEqualsWithPhases( 1200, 6.0F, 3) + )); + // clang-format on +} + +TEST_F(CompositeScheduleTestFixtureV201, SingleTxDefaultProfileWithStationMaxForEvse0WithSingleEvse) { + this->load_charging_profiles_for_evse("singles/ChargingStationMaxProfile_401.json", STATION_WIDE_ID); + this->load_charging_profiles_for_evse("singles/Relative_303.json", DEFAULT_EVSE_ID); + + // Note the time is 1 minute off the whole hour + const DateTime start_time = ocpp::DateTime("2024-01-02T08:01:00"); + const DateTime end_time = ocpp::DateTime("2024-01-02T09:01:00"); + + CompositeSchedule result = handler->calculate_composite_schedule(start_time, end_time, STATION_WIDE_ID, + ChargingRateUnitEnum::A, false, true); + + EXPECT_EQ(result.evseId, STATION_WIDE_ID); + EXPECT_EQ(result.scheduleStart, start_time); + EXPECT_EQ(result.duration, 3600); + EXPECT_EQ(result.chargingRateUnit, ChargingRateUnitEnum::A); + + // clang-format off + EXPECT_THAT(result.chargingSchedulePeriod, + testing::ElementsAre( + PeriodEqualsWithPhases( 0, 16.0F, 1), + PeriodEqualsWithPhases( 1800, 15.0F, 1), + PeriodEqualsWithPhases( 2700, 14.0F, 1) + )); + // clang-format on +} + +TEST_F(CompositeScheduleTestFixtureV201, SingleTxDefaultProfileWithStationMaxForEvse0WithMultipleEvses) { + this->load_charging_profiles_for_evse("singles/ChargingStationMaxProfile_401.json", STATION_WIDE_ID); + this->load_charging_profiles_for_evse("singles/Relative_303.json", DEFAULT_EVSE_ID); + this->load_charging_profiles_for_evse("singles/Relative_303.json", 2); + + constexpr int32_t nr_of_evses = 2; + + this->reconfigure_for_nr_of_evses(nr_of_evses); + + // Note the time is 1 minute off the whole hour + const DateTime start_time = ocpp::DateTime("2024-01-02T08:01:00"); + const DateTime end_time = ocpp::DateTime("2024-01-02T09:01:00"); + + CompositeSchedule result = handler->calculate_composite_schedule(start_time, end_time, STATION_WIDE_ID, + ChargingRateUnitEnum::A, false, true); + + EXPECT_EQ(result.evseId, STATION_WIDE_ID); + EXPECT_EQ(result.scheduleStart, start_time); + EXPECT_EQ(result.duration, 3600); + EXPECT_EQ(result.chargingRateUnit, ChargingRateUnitEnum::A); + + // clang-format off + EXPECT_THAT(result.chargingSchedulePeriod, + testing::ElementsAre( + PeriodEqualsWithPhases( 0, 24.0F, 1), + PeriodEqualsWithPhases( 840, 28.0F, 1), + PeriodEqualsWithPhases( 1740, 30.0F, 1), + PeriodEqualsWithPhases( 2700, 28.0F, 1) + )); + // clang-format on +} + +TEST_F(CompositeScheduleTestFixtureV201, TxProfilePerEvseWithMultipleEvses) { + this->load_charging_profiles_for_evse("singles/Recurring_Daily_301.json", DEFAULT_EVSE_ID); + this->load_charging_profiles_for_evse("singles/Relative_303.json", 2); + + constexpr int32_t nr_of_evses = 2; + + this->reconfigure_for_nr_of_evses(nr_of_evses); + + // Note the time is 1 minute off the whole hour + const DateTime start_time = ocpp::DateTime("2024-01-02T08:01:00"); + const DateTime end_time = ocpp::DateTime("2024-01-02T09:01:00"); + + CompositeSchedule result = handler->calculate_composite_schedule(start_time, end_time, STATION_WIDE_ID, + ChargingRateUnitEnum::A, false, true); + + EXPECT_EQ(result.evseId, STATION_WIDE_ID); + EXPECT_EQ(result.scheduleStart, start_time); + EXPECT_EQ(result.duration, 3600); + EXPECT_EQ(result.chargingRateUnit, ChargingRateUnitEnum::A); + + // clang-format off + EXPECT_THAT(result.chargingSchedulePeriod, + testing::ElementsAre( + PeriodEqualsWithPhases( 0, 32.0F, 3), + PeriodEqualsWithPhases( 1740, 31.0F, 3), + PeriodEqualsWithPhases( 1800, 30.0F, 3), + PeriodEqualsWithPhases( 2640, 29.0F, 3), + PeriodEqualsWithPhases( 2700, 28.0F, 3), + PeriodEqualsWithPhases( 3540, 14.0F + DEFAULT_LIMIT_AMPERE, 3) + )); + // clang-format on +} + +TEST_F(CompositeScheduleTestFixtureV201, MixingCurrentAndPower_16A_1P) { + this->load_charging_profiles_for_evse("singles/ChargingStationMaxProfile_24_Ampere.json", STATION_WIDE_ID); + this->load_charging_profiles_for_evse("singles/TXDefaultProfile_25_Watt.json", DEFAULT_EVSE_ID); + + const DateTime start_time = ocpp::DateTime("2024-01-02T00:00:00"); + const DateTime end_time = ocpp::DateTime("2024-01-02T01:00:00"); + + CompositeSchedule result = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::A, false, true); + + EXPECT_EQ(result.evseId, DEFAULT_EVSE_ID); + EXPECT_EQ(result.scheduleStart, start_time); + EXPECT_EQ(result.duration, 3600); + EXPECT_EQ(result.chargingRateUnit, ChargingRateUnitEnum::A); + + // clang-format off + EXPECT_THAT(result.chargingSchedulePeriod, + testing::ElementsAre( + PeriodEqualsWithPhases( 0, 16.0F, 1), + PeriodEqualsWithPhases( 1200, 9.0F, 1) + )); + // clang-format on +} + +TEST_F(CompositeScheduleTestFixtureV201, MixingCurrentAndPower_16A_3P) { + this->load_charging_profiles_for_evse("singles/ChargingStationMaxProfile_24_Ampere.json", STATION_WIDE_ID); + this->load_charging_profiles_for_evse("singles/TXDefaultProfile_25_Watt.json", DEFAULT_EVSE_ID); + + const DateTime start_time = ocpp::DateTime("2024-01-02T01:00:00"); + const DateTime end_time = ocpp::DateTime("2024-01-02T02:00:00"); + + CompositeSchedule result = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::A, false, true); + + EXPECT_EQ(result.evseId, DEFAULT_EVSE_ID); + EXPECT_EQ(result.scheduleStart, start_time); + EXPECT_EQ(result.duration, 3600); + EXPECT_EQ(result.chargingRateUnit, ChargingRateUnitEnum::A); + + // clang-format off + EXPECT_THAT(result.chargingSchedulePeriod, + testing::ElementsAre( + PeriodEqualsWithPhases( 0, 16.0F, 3), + PeriodEqualsWithPhases( 300, 12.0F, 3), + PeriodEqualsWithPhases( 600, 9.0F, 3), + PeriodEqualsWithPhases( 900, 6.0F, 3), + PeriodEqualsWithPhases( 1200, 3.0F, 3) + )); + // clang-format on +} + +TEST_F(CompositeScheduleTestFixtureV201, MixingCurrentAndPower_10A_1P) { + this->load_charging_profiles_for_evse("singles/ChargingStationMaxProfile_24_Ampere.json", STATION_WIDE_ID); + this->load_charging_profiles_for_evse("singles/TXDefaultProfile_25_Watt.json", DEFAULT_EVSE_ID); + + const DateTime start_time = ocpp::DateTime("2024-01-02T02:00:00"); + const DateTime end_time = ocpp::DateTime("2024-01-02T03:00:00"); + + CompositeSchedule result = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::A, false, true); + + EXPECT_EQ(result.evseId, DEFAULT_EVSE_ID); + EXPECT_EQ(result.scheduleStart, start_time); + EXPECT_EQ(result.duration, 3600); + EXPECT_EQ(result.chargingRateUnit, ChargingRateUnitEnum::A); + + // clang-format off + EXPECT_THAT(result.chargingSchedulePeriod, + testing::ElementsAre( + PeriodEqualsWithPhases( 0, 10.0F, 1), + PeriodEqualsWithPhases( 1200, 9.0F, 1) + )); + // clang-format on +} + +TEST_F(CompositeScheduleTestFixtureV201, MixingCurrentAndPower_10A_3P) { + this->load_charging_profiles_for_evse("singles/ChargingStationMaxProfile_24_Ampere.json", STATION_WIDE_ID); + this->load_charging_profiles_for_evse("singles/TXDefaultProfile_25_Watt.json", DEFAULT_EVSE_ID); + + const DateTime start_time = ocpp::DateTime("2024-01-02T03:00:00"); + const DateTime end_time = ocpp::DateTime("2024-01-02T04:00:00"); + + CompositeSchedule result = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::A, false, true); + + EXPECT_EQ(result.evseId, DEFAULT_EVSE_ID); + EXPECT_EQ(result.scheduleStart, start_time); + EXPECT_EQ(result.duration, 3600); + EXPECT_EQ(result.chargingRateUnit, ChargingRateUnitEnum::A); + + // clang-format off + EXPECT_THAT(result.chargingSchedulePeriod, + testing::ElementsAre( + PeriodEqualsWithPhases( 0, 10.0F, 3), + PeriodEqualsWithPhases( 600, 9.0F, 3), + PeriodEqualsWithPhases( 900, 6.0F, 3), + PeriodEqualsWithPhases( 1200, 3.0F, 3) + )); + // clang-format on +} + +TEST_F(CompositeScheduleTestFixtureV201, MixingCurrentAndPower_10A_1P_RequestPower) { + this->load_charging_profiles_for_evse("singles/ChargingStationMaxProfile_24_Ampere.json", STATION_WIDE_ID); + this->load_charging_profiles_for_evse("singles/TXDefaultProfile_25_Watt.json", DEFAULT_EVSE_ID); + + const DateTime start_time = ocpp::DateTime("2024-01-02T02:00:00"); + const DateTime end_time = ocpp::DateTime("2024-01-02T03:00:00"); + + CompositeSchedule result = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::W, false, true); + + EXPECT_EQ(result.evseId, DEFAULT_EVSE_ID); + EXPECT_EQ(result.scheduleStart, start_time); + EXPECT_EQ(result.duration, 3600); + EXPECT_EQ(result.chargingRateUnit, ChargingRateUnitEnum::W); + + // clang-format off + EXPECT_THAT(result.chargingSchedulePeriod, + testing::ElementsAre( + PeriodEqualsWithPhases( 0, 2300.0F, 1), + PeriodEqualsWithPhases( 1200, 2070.0F, 1) + )); + // clang-format on +} + +TEST_F(CompositeScheduleTestFixtureV201, MixingCurrentAndPower_10A_3P_RequestPower) { + this->load_charging_profiles_for_evse("singles/ChargingStationMaxProfile_24_Ampere.json", STATION_WIDE_ID); + this->load_charging_profiles_for_evse("singles/TXDefaultProfile_25_Watt.json", DEFAULT_EVSE_ID); + + const DateTime start_time = ocpp::DateTime("2024-01-02T03:00:00"); + const DateTime end_time = ocpp::DateTime("2024-01-02T04:00:00"); + + CompositeSchedule result = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::W, false, true); + + EXPECT_EQ(result.evseId, DEFAULT_EVSE_ID); + EXPECT_EQ(result.scheduleStart, start_time); + EXPECT_EQ(result.duration, 3600); + EXPECT_EQ(result.chargingRateUnit, ChargingRateUnitEnum::W); + + // clang-format off + EXPECT_THAT(result.chargingSchedulePeriod, + testing::ElementsAre( + PeriodEqualsWithPhases( 0, 6900.0F, 3), + PeriodEqualsWithPhases( 600, 6210.0F, 3), + PeriodEqualsWithPhases( 900, 4140.0F, 3), + PeriodEqualsWithPhases( 1200, 2070.0F, 3) + )); + // clang-format on +} + +TEST_F(CompositeScheduleTestFixtureV201, TxProfilePerEvseWithMultipleEvses_DifferentNrOfPhases) { + this->load_charging_profiles_for_evse("singles/Recurring_Daily_302_phase_limit.json", DEFAULT_EVSE_ID); + this->load_charging_profiles_for_evse("singles/Relative_302_phase_limit.json", 2); + + constexpr int32_t nr_of_evses = 2; + + this->reconfigure_for_nr_of_evses(nr_of_evses); + + // Note the time is 1 minute off the whole hour + const DateTime start_time = ocpp::DateTime("2024-01-02T08:00:00"); + const DateTime end_time = ocpp::DateTime("2024-01-02T09:00:00"); + + CompositeSchedule result = handler->calculate_composite_schedule(start_time, end_time, STATION_WIDE_ID, + ChargingRateUnitEnum::A, false, true); + + EXPECT_EQ(result.evseId, STATION_WIDE_ID); + EXPECT_EQ(result.scheduleStart, start_time); + EXPECT_EQ(result.duration, 3600); + EXPECT_EQ(result.chargingRateUnit, ChargingRateUnitEnum::A); + + // clang-format off + EXPECT_THAT(result.chargingSchedulePeriod, + testing::ElementsAre( + PeriodEqualsWithPhases( 0, 32.0F, 3), + PeriodEqualsWithPhases( 1800, 30.0F, 1), + PeriodEqualsWithPhases( 2700, 28.0F, 3) + )); + // clang-format on +} + +TEST_F(CompositeScheduleTestFixtureV201, MixingNumberOfPhasesOnSingleEvse) { + this->load_charging_profiles_for_evse("singles/ChargingStationMaxProfile_24_Ampere.json", STATION_WIDE_ID); + this->load_charging_profiles_for_evse("singles/Relative_302_phase_limit.json", DEFAULT_EVSE_ID); + + // Note the time is 40 minute off the whole hour + const DateTime start_time = ocpp::DateTime("2024-01-02T00:40:00"); + const DateTime end_time = ocpp::DateTime("2024-01-02T01:40:00"); + + CompositeSchedule result = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::A, false, true); + + EXPECT_EQ(result.evseId, DEFAULT_EVSE_ID); + EXPECT_EQ(result.scheduleStart, start_time); + EXPECT_EQ(result.duration, 3600); + EXPECT_EQ(result.chargingRateUnit, ChargingRateUnitEnum::A); + + // clang-format off + EXPECT_THAT(result.chargingSchedulePeriod, + testing::ElementsAre( + PeriodEqualsWithPhases( 0, 16.0F, 1), + PeriodEqualsWithPhases( 1200, 16.0F, 3), + PeriodEqualsWithPhases( 1800, 15.0F, 1), + PeriodEqualsWithPhases( 2700, 14.0F, 3) + )); + // clang-format on +} + +TEST_F(CompositeScheduleTestFixtureV201, NoGapsWithSequentialProfiles) { + this->load_charging_profiles_for_evse(BASE_JSON_PATH + "/no_gap/", STATION_WIDE_ID); + + const DateTime start_time = ocpp::DateTime("2024-01-02T08:00:00"); + const DateTime end_time = ocpp::DateTime("2024-01-02T08:20:00"); + + CompositeSchedule result = handler->calculate_composite_schedule(start_time, end_time, DEFAULT_EVSE_ID, + ChargingRateUnitEnum::A, false, true); + + EXPECT_EQ(result.evseId, DEFAULT_EVSE_ID); + EXPECT_EQ(result.scheduleStart, start_time); + EXPECT_EQ(result.duration, 1200); + EXPECT_EQ(result.chargingRateUnit, ChargingRateUnitEnum::A); + + // clang-format off + EXPECT_THAT(result.chargingSchedulePeriod, + testing::ElementsAre( + PeriodEquals( 0, 16.0F), + PeriodEquals( 300, 12.0F), + PeriodEquals( 600, DEFAULT_LIMIT_AMPERE) + )); + // clang-format on +} } // namespace ocpp::v201 diff --git a/tests/lib/ocpp/v201/test_profile.cpp b/tests/lib/ocpp/v201/test_profile.cpp index 6960a2398..7d885682c 100644 --- a/tests/lib/ocpp/v201/test_profile.cpp +++ b/tests/lib/ocpp/v201/test_profile.cpp @@ -677,706 +677,4 @@ TEST(OCPPTypesTest, ChargingSchedule_Equality) { ASSERT_EQ(schedule1, schedule2); } -TEST(OCPPTypesTest, CalculateChargingSchedule_Empty) { - std::vector combined_schedules{}; - ChargingSchedulePeriod period; - period.startPeriod = 0; - period.limit = NO_LIMIT_SPECIFIED; - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = std::chrono::duration_cast(minutes(10)).count(); - expected.scheduleStart = dt("12:00"); - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - CompositeSchedule actual = calculate_composite_schedule(combined_schedules, dt("12:00"), dt("12:10"), std::nullopt, - DEFAULT_AND_MAX_NUMBER_PHASES, LOW_VOLTAGE); - - 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}}; - ChargingSchedulePeriod period; - period.startPeriod = 0; - period.limit = 24.0; - period.numberPhases = 3; - - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = std::chrono::duration_cast(minutes(10)).count(); - expected.scheduleStart = now; - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, ChargingRateUnitEnum::A, - DEFAULT_AND_MAX_NUMBER_PHASES, LOW_VOLTAGE); - - 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}}; - - ChargingSchedulePeriod period1; - period1.startPeriod = 0; - period1.limit = 24.0; - period1.numberPhases = 3; - ChargingSchedulePeriod period2; - period2.startPeriod = 599; - period2.limit = NO_LIMIT_SPECIFIED; - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period1, period2}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = std::chrono::duration_cast(minutes(10)).count(); - expected.scheduleStart = now; - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, ChargingRateUnitEnum::A, - DEFAULT_AND_MAX_NUMBER_PHASES, LOW_VOLTAGE); - - 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}}; - - ChargingSchedulePeriod period; - period.startPeriod = 0; - period.limit = 24.0; - period.numberPhases = 3; - - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = std::chrono::duration_cast(minutes(10)).count(); - expected.scheduleStart = now; - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, ChargingRateUnitEnum::A, - DEFAULT_AND_MAX_NUMBER_PHASES, LOW_VOLTAGE); - - 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}}; - - ChargingSchedulePeriod period1; - period1.startPeriod = 0; - period1.limit = NO_LIMIT_SPECIFIED; - ChargingSchedulePeriod period2; - period2.startPeriod = 1; - period2.limit = 24.0; - period2.numberPhases = 3; - ChargingSchedulePeriod period3; - period3.startPeriod = 599; - period3.limit = NO_LIMIT_SPECIFIED; - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period1, period2, period3}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = std::chrono::duration_cast(minutes(10)).count(); - expected.scheduleStart = now; - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt, - DEFAULT_AND_MAX_NUMBER_PHASES, LOW_VOLTAGE); - - 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}}; - ChargingSchedulePeriod period; - period.startPeriod = 1; - period.limit = 24.0; - period.numberPhases = 3; - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = std::chrono::duration_cast(minutes(10)).count(); - expected.scheduleStart = now; - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt, - DEFAULT_AND_MAX_NUMBER_PHASES, LOW_VOLTAGE); - - 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}}; - ChargingSchedulePeriod period1; - period1.startPeriod = 0; - period1.limit = 24.0; - period1.numberPhases = 3; - ChargingSchedulePeriod period2; - period2.startPeriod = 300; - period2.limit = NO_LIMIT_SPECIFIED; - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period1, period2}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = std::chrono::duration_cast(minutes(10)).count(); - expected.scheduleStart = now; - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt, - DEFAULT_AND_MAX_NUMBER_PHASES, LOW_VOLTAGE); - - 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}}; - ChargingSchedulePeriod period1; - period1.startPeriod = 0; - period1.limit = NO_LIMIT_SPECIFIED; - ChargingSchedulePeriod period2; - period2.startPeriod = 120, period2.limit = 24.0, period2.numberPhases = 3; - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period1, period2}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = std::chrono::duration_cast(minutes(10)).count(); - expected.scheduleStart = now; - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt, - DEFAULT_AND_MAX_NUMBER_PHASES, LOW_VOLTAGE); - - 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}}; - ChargingSchedulePeriod period1; - period1.startPeriod = 0; - period1.limit = 24.0; - period1.numberPhases = 3; - ChargingSchedulePeriod period2; - period2.startPeriod = 300; - period2.limit = 32.0; - period2.numberPhases = 1; - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period1, period2}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = std::chrono::duration_cast(minutes(10)).count(); - expected.scheduleStart = now; - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt, - DEFAULT_AND_MAX_NUMBER_PHASES, LOW_VOLTAGE); - - 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}}; - ChargingSchedulePeriod period1; - period1.startPeriod = 0; - period1.limit = 32.0; - period1.numberPhases = 1; - ChargingSchedulePeriod period2; - period2.startPeriod = 300; - period2.limit = 24.0; - period2.numberPhases = 3; - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period1, period2}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = std::chrono::duration_cast(minutes(10)).count(); - expected.scheduleStart = now; - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt, - DEFAULT_AND_MAX_NUMBER_PHASES, LOW_VOLTAGE); - - 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}}; - ChargingSchedulePeriod period1; - period1.startPeriod = 0; - period1.limit = 24.0; - period1.numberPhases = 3; - ChargingSchedulePeriod period2; - period2.startPeriod = 120; - period2.limit = 32.0; - period2.numberPhases = 1; - ChargingSchedulePeriod period3; - period3.startPeriod = 300; - period3.limit = 24.0; - period3.numberPhases = 3; - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period1, period2, period3}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = std::chrono::duration_cast(minutes(10)).count(); - expected.scheduleStart = now; - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt, - DEFAULT_AND_MAX_NUMBER_PHASES, LOW_VOLTAGE); - - 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}}; - ChargingSchedulePeriod period; - period.startPeriod = 0; - period.limit = 24.0; - period.numberPhases = 3; - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = std::chrono::duration_cast(minutes(10)).count(); - expected.scheduleStart = now; - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt, - DEFAULT_AND_MAX_NUMBER_PHASES, LOW_VOLTAGE); - - 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}}; - ChargingSchedulePeriod period1; - period1.startPeriod = 0; - period1.limit = 32.0; - period1.numberPhases = 1; - ChargingSchedulePeriod period2; - period2.startPeriod = 300; - period2.limit = 24.0; - period2.numberPhases = 3; - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period1, period2}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = std::chrono::duration_cast(minutes(10)).count(); - expected.scheduleStart = now; - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt, - DEFAULT_AND_MAX_NUMBER_PHASES, LOW_VOLTAGE); - - 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}}; - ChargingSchedulePeriod period1; - period1.startPeriod = 0; - period1.limit = 24.0; - period1.numberPhases = 3; - ChargingSchedulePeriod period2; - period2.startPeriod = 300; - period2.limit = 32.0; - period2.numberPhases = 1; - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period1, period2}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = std::chrono::duration_cast(minutes(10)).count(); - expected.scheduleStart = now; - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt, - DEFAULT_AND_MAX_NUMBER_PHASES, LOW_VOLTAGE); - - 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}}; - ChargingSchedulePeriod period1; - period1.startPeriod = 0; - period1.limit = 32.0; - period1.numberPhases = 1; - ChargingSchedulePeriod period2; - period2.startPeriod = 300; - period2.limit = 24.0; - period2.numberPhases = 3; - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period1, period2}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = std::chrono::duration_cast(minutes(10)).count(); - expected.scheduleStart = now; - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt, - DEFAULT_AND_MAX_NUMBER_PHASES, LOW_VOLTAGE); - - 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}}; - ChargingSchedulePeriod period1; - period1.startPeriod = 0; - period1.limit = 24.0; - period1.numberPhases = 3; - ChargingSchedulePeriod period2; - period2.startPeriod = 300; - period2.limit = 32.0; - period2.numberPhases = 1; - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period1, period2}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = std::chrono::duration_cast(minutes(10)).count(); - expected.scheduleStart = now; - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt, - DEFAULT_AND_MAX_NUMBER_PHASES, LOW_VOLTAGE); - - 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}}; - ChargingSchedulePeriod period1; - period1.startPeriod = 0; - period1.limit = 32.0; - period1.numberPhases = 1; - ChargingSchedulePeriod period2; - period2.startPeriod = 299; - period2.limit = NO_LIMIT_SPECIFIED; - ChargingSchedulePeriod period3; - period3.startPeriod = 300; - period3.limit = 24.0; - period3.numberPhases = 3; - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period1, period2, period3}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = std::chrono::duration_cast(minutes(10)).count(); - expected.scheduleStart = now; - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt, - DEFAULT_AND_MAX_NUMBER_PHASES, LOW_VOLTAGE); - - 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}}; - ChargingSchedulePeriod period1; - period1.startPeriod = 0; - period1.limit = 32.0; - period1.numberPhases = 1; - period1.phaseToUse = 3; - ChargingSchedulePeriod period2; - period2.startPeriod = 299; - period2.limit = NO_LIMIT_SPECIFIED; - ChargingSchedulePeriod period3; - period3.startPeriod = 300; - period3.limit = 24.0; - period3.numberPhases = 3; - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period1, period2, period3}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = std::chrono::duration_cast(minutes(10)).count(); - expected.scheduleStart = now; - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - CompositeSchedule actual = calculate_composite_schedule(combined_schedules, now, end, std::nullopt, - DEFAULT_AND_MAX_NUMBER_PHASES, LOW_VOLTAGE); - - ASSERT_EQ(expected, actual); -} - -TEST(OCPPTypesTest, CalculateChargingScheduleCombined_Default) { - ChargingSchedulePeriod period; - period.startPeriod = 0; - period.limit = DEFAULT_LIMIT_AMPS; - period.numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES; - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = DEFAULT_SCHEDULE.duration; - expected.scheduleStart = DEFAULT_SCHEDULE.scheduleStart; - expected.chargingRateUnit = DEFAULT_SCHEDULE.chargingRateUnit; - - const CompositeSchedule actual = calculate_composite_schedule(DEFAULT_SCHEDULE, DEFAULT_SCHEDULE, DEFAULT_SCHEDULE, - DEFAULT_SCHEDULE, DEFAULT_LIMITS, LOW_VOLTAGE); - - ASSERT_EQ(expected, actual); -} - -TEST(OCPPTypesTest, CalculateChargingScheduleCombined_CombinedTxDefault) { - CompositeSchedule profile = DEFAULT_SCHEDULE; - ChargingSchedulePeriod period1; - period1.startPeriod = 0; - period1.limit = 10.0; - CompositeSchedule tx_default_schedule; - tx_default_schedule.chargingSchedulePeriod = {period1}; - tx_default_schedule.evseId = EVSEID_NOT_SET; - tx_default_schedule.duration = 600; - tx_default_schedule.scheduleStart = dt("12:00"); - tx_default_schedule.chargingRateUnit = ChargingRateUnitEnum::A; - - ChargingSchedulePeriod period2; - period2.startPeriod = 0; - period2.limit = 10; - period2.numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES; - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period2}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = 600; - expected.scheduleStart = dt("12:00"); - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - const CompositeSchedule actual = - calculate_composite_schedule(profile, profile, tx_default_schedule, profile, DEFAULT_LIMITS, LOW_VOLTAGE); - - ASSERT_EQ(expected, actual); -} - -TEST(OCPPTypesTest, CalculateChargingScheduleCombined_CombinedTxDefaultTx) { - CompositeSchedule charging_station_max = DEFAULT_SCHEDULE; - ChargingSchedulePeriod period1; - period1.startPeriod = 0; - period1.limit = 10.0; - CompositeSchedule tx_default_schedule; - tx_default_schedule.chargingSchedulePeriod = {period1}; - tx_default_schedule.evseId = EVSEID_NOT_SET; - tx_default_schedule.duration = 600; - tx_default_schedule.scheduleStart = dt("12:00"); - tx_default_schedule.chargingRateUnit = ChargingRateUnitEnum::A; - ChargingSchedulePeriod period2; - period2.startPeriod = 0; - period2.limit = 32.0; - CompositeSchedule tx_schedule; - tx_schedule.chargingSchedulePeriod = {period2}; - tx_schedule.evseId = EVSEID_NOT_SET; - tx_schedule.duration = 600; - tx_schedule.scheduleStart = dt("12:00"); - tx_schedule.chargingRateUnit = ChargingRateUnitEnum::A; - - ChargingSchedulePeriod period3; - period3.startPeriod = 0; - period3.limit = 32.0; - period3.numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES; - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period3}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = 600; - expected.scheduleStart = dt("12:00"); - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - const CompositeSchedule actual = calculate_composite_schedule( - DEFAULT_SCHEDULE, charging_station_max, tx_default_schedule, tx_schedule, DEFAULT_LIMITS, LOW_VOLTAGE); - - ASSERT_EQ(expected, actual); -} - -TEST(OCPPTypesTest, CalculateChargingScheduleCombined_CombinedOverlapTxAndTxDefault) { - ChargingSchedulePeriod period1; - period1.startPeriod = 0; - period1.limit = 10.0; - ChargingSchedulePeriod period2; - period2.startPeriod = 300; - period2.limit = 24.0; - CompositeSchedule tx_default_schedule; - tx_default_schedule.chargingSchedulePeriod = {period1, period2}; - tx_default_schedule.evseId = EVSEID_NOT_SET; - tx_default_schedule.duration = 600; - tx_default_schedule.scheduleStart = dt("12:00"); - tx_default_schedule.chargingRateUnit = ChargingRateUnitEnum::A; - - ChargingSchedulePeriod period3; - period3.startPeriod = 0; - period3.limit = NO_LIMIT_SPECIFIED; - ChargingSchedulePeriod period4; - period4.startPeriod = 150; - period4.limit = 32.0; - ChargingSchedulePeriod period5; - period5.startPeriod = 450; - period5.limit = NO_LIMIT_SPECIFIED; - CompositeSchedule tx_schedule; - tx_schedule.chargingSchedulePeriod = {period3, period4, period5}; - tx_schedule.evseId = EVSEID_NOT_SET; - tx_schedule.duration = 600; - tx_schedule.scheduleStart = dt("12:00"); - tx_schedule.chargingRateUnit = ChargingRateUnitEnum::A; - - ChargingSchedulePeriod period6; - period6.startPeriod = 0; - period6.limit = NO_LIMIT_SPECIFIED; - CompositeSchedule charging_station_max; - charging_station_max.chargingSchedulePeriod = {period6}; - charging_station_max.evseId = EVSEID_NOT_SET; - charging_station_max.duration = 600; - charging_station_max.scheduleStart = dt("12:00"); - charging_station_max.chargingRateUnit = ChargingRateUnitEnum::A; - - ChargingSchedulePeriod period7; - period7.startPeriod = 0; - period7.limit = 10.0; - period7.numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES; - ChargingSchedulePeriod period8; - period8.startPeriod = 150; - period8.limit = 32.0; - period8.numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES; - ChargingSchedulePeriod period9; - period9.startPeriod = 450; - period9.limit = 24.0; - period9.numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES; - - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period7, period8, period9}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = 600; - expected.scheduleStart = dt("12:00"); - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - const CompositeSchedule actual = calculate_composite_schedule( - DEFAULT_SCHEDULE, charging_station_max, tx_default_schedule, tx_schedule, DEFAULT_LIMITS, LOW_VOLTAGE); - - ASSERT_EQ(expected, actual); -} - -TEST(OCPPTypesTest, CalculateChargingScheduleCombined_CombinedOverlapTxTxDefaultAndChargingStationMax) { - ChargingSchedulePeriod period1; - period1.startPeriod = 0; - period1.limit = 10.0; - ChargingSchedulePeriod period2; - period2.startPeriod = 300; - period2.limit = 24.0; - CompositeSchedule tx_default_schedule; - tx_default_schedule.chargingSchedulePeriod = {period1, period2}; - tx_default_schedule.evseId = EVSEID_NOT_SET; - tx_default_schedule.duration = 600; - tx_default_schedule.scheduleStart = dt("12:00"); - tx_default_schedule.chargingRateUnit = ChargingRateUnitEnum::A; - - ChargingSchedulePeriod period3; - period3.startPeriod = 0; - period3.limit = NO_LIMIT_SPECIFIED; - ChargingSchedulePeriod period4; - period4.startPeriod = 150; - period4.limit = 32.0; - ChargingSchedulePeriod period5; - period5.startPeriod = 450; - period5.limit = NO_LIMIT_SPECIFIED; - - CompositeSchedule tx_schedule; - tx_schedule.chargingSchedulePeriod = {period3, period4, period5}; - tx_schedule.evseId = EVSEID_NOT_SET; - tx_schedule.duration = 600; - tx_schedule.scheduleStart = dt("12:00"); - tx_schedule.chargingRateUnit = ChargingRateUnitEnum::A; - - ChargingSchedulePeriod period6; - period6.startPeriod = 0; - period6.limit = NO_LIMIT_SPECIFIED; - ChargingSchedulePeriod period7; - period7.startPeriod = 500; - period7.limit = 15.0; - ChargingSchedulePeriod period8; - period8.startPeriod = 550; - period8.limit = NO_LIMIT_SPECIFIED; - CompositeSchedule charging_station_max; - charging_station_max.chargingSchedulePeriod = {period6, period7, period8}; - charging_station_max.evseId = EVSEID_NOT_SET; - charging_station_max.duration = 600; - charging_station_max.scheduleStart = dt("12:00"); - charging_station_max.chargingRateUnit = ChargingRateUnitEnum::A; - - ChargingSchedulePeriod period_expected1; - period_expected1.startPeriod = 0; - period_expected1.limit = 10.0; - period_expected1.numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES; - ChargingSchedulePeriod period_expected2; - period_expected2.startPeriod = 150; - period_expected2.limit = 32.0; - period_expected2.numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES; - ChargingSchedulePeriod period_expected3; - period_expected3.startPeriod = 450; - period_expected3.limit = 24.0; - period_expected3.numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES; - ChargingSchedulePeriod period_expected4; - period_expected4.startPeriod = 500; - period_expected4.limit = 15.0; - period_expected4.numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES; - ChargingSchedulePeriod period_expected5; - period_expected5.startPeriod = 550; - period_expected5.limit = 24.0; - period_expected5.numberPhases = DEFAULT_AND_MAX_NUMBER_PHASES; - - CompositeSchedule expected; - expected.chargingSchedulePeriod = {period_expected1, period_expected2, period_expected3, period_expected4, - period_expected5}; - expected.evseId = EVSEID_NOT_SET; - expected.duration = 600; - expected.scheduleStart = dt("12:00"); - expected.chargingRateUnit = ChargingRateUnitEnum::A; - - const CompositeSchedule actual = calculate_composite_schedule( - DEFAULT_SCHEDULE, charging_station_max, tx_default_schedule, tx_schedule, DEFAULT_LIMITS, LOW_VOLTAGE); - - ASSERT_EQ(expected, actual); -} - } // namespace \ No newline at end of file