diff --git a/dependencies.yaml b/dependencies.yaml index 2f0b3b8e..c8124a8a 100644 --- a/dependencies.yaml +++ b/dependencies.yaml @@ -9,4 +9,4 @@ catch2: cmake_condition: "ISO15118_BUILD_TESTING" libcbv2g: git: https://github.com/EVerest/libcbv2g.git - git_tag: v0.2.0 + git_tag: v0.2.1 diff --git a/include/iso15118/d20/config.hpp b/include/iso15118/d20/config.hpp index 9e6a76e4..7a4d6a70 100644 --- a/include/iso15118/d20/config.hpp +++ b/include/iso15118/d20/config.hpp @@ -11,12 +11,18 @@ namespace iso15118::d20 { +struct ControlMobilityNeedsModes { + message_20::ControlMode control_mode; + message_20::MobilityNeedsMode mobility_mode; +}; + struct EvseSetupConfig { std::string evse_id; std::vector supported_energy_services; std::vector authorization_services; bool enable_certificate_install_service; d20::DcTransferLimits dc_limits; + std::vector control_mobility_modes; }; struct SessionConfig { @@ -37,6 +43,8 @@ struct SessionConfig { std::vector parking_parameter_list; DcTransferLimits dc_limits; + + std::vector supported_control_mobility_modes; }; } // namespace iso15118::d20 diff --git a/include/iso15118/d20/control_event.hpp b/include/iso15118/d20/control_event.hpp index 5eaa85c4..2ff49ac0 100644 --- a/include/iso15118/d20/control_event.hpp +++ b/include/iso15118/d20/control_event.hpp @@ -2,8 +2,11 @@ // Copyright 2023 Pionix GmbH and Contributors to EVerest #pragma once +#include +#include #include +#include #include namespace iso15118::d20 { @@ -51,7 +54,7 @@ class StopCharging { bool stop; }; -using ControlEvent = - std::variant; +using ControlEvent = std::variant; } // namespace iso15118::d20 diff --git a/include/iso15118/d20/dynamic_mode_parameters.hpp b/include/iso15118/d20/dynamic_mode_parameters.hpp new file mode 100644 index 00000000..d9c1b8ef --- /dev/null +++ b/include/iso15118/d20/dynamic_mode_parameters.hpp @@ -0,0 +1,17 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright 2024 Pionix GmbH and Contributors to EVerest +#pragma once + +#include +#include +#include + +namespace iso15118::d20 { + +struct UpdateDynamicModeParameters { + std::optional departure_time; + std::optional target_soc; + std::optional min_soc; +}; + +} // namespace iso15118::d20 \ No newline at end of file diff --git a/include/iso15118/d20/session.hpp b/include/iso15118/d20/session.hpp index 5f6a8026..339fc40a 100644 --- a/include/iso15118/d20/session.hpp +++ b/include/iso15118/d20/session.hpp @@ -91,16 +91,10 @@ class Session { void selected_service_parameters(const message_20::ServiceCategory service, const uint16_t id); - message_20::ServiceCategory get_selected_energy_service() const { - return selected_services.selected_energy_service; + auto get_selected_services() const& { + return selected_services; } - message_20::ControlMode get_selected_control_mode() const { - return selected_services.selected_control_mode; - } - - // TODO(sl): Define get_selected_*() if necessary - ~Session(); OfferedServices offered_services; diff --git a/include/iso15118/d20/state/dc_charge_loop.hpp b/include/iso15118/d20/state/dc_charge_loop.hpp index cd378a87..a015378b 100644 --- a/include/iso15118/d20/state/dc_charge_loop.hpp +++ b/include/iso15118/d20/state/dc_charge_loop.hpp @@ -4,6 +4,11 @@ #include "../fsm.hpp" +#include +#include + +#include + namespace iso15118::d20::state { struct DC_ChargeLoop : public FsmSimpleState { @@ -18,6 +23,8 @@ struct DC_ChargeLoop : public FsmSimpleState { float present_current{0}; bool stop{false}; + UpdateDynamicModeParameters dynamic_parameters; + bool first_entry_in_charge_loop{true}; }; diff --git a/include/iso15118/d20/state/schedule_exchange.hpp b/include/iso15118/d20/state/schedule_exchange.hpp index 88aaed22..67d80e5d 100644 --- a/include/iso15118/d20/state/schedule_exchange.hpp +++ b/include/iso15118/d20/state/schedule_exchange.hpp @@ -4,6 +4,11 @@ #include "../fsm.hpp" +#include +#include + +#include + namespace iso15118::d20::state { struct ScheduleExchange : public FsmSimpleState { @@ -12,6 +17,9 @@ struct ScheduleExchange : public FsmSimpleState { void enter() final; HandleEventReturnType handle_event(AllocatorType&, FsmEvent) final; + +private: + UpdateDynamicModeParameters dynamic_parameters; }; } // namespace iso15118::d20::state diff --git a/include/iso15118/detail/cb_exi.hpp b/include/iso15118/detail/cb_exi.hpp index 51c382b7..147968e3 100644 --- a/include/iso15118/detail/cb_exi.hpp +++ b/include/iso15118/detail/cb_exi.hpp @@ -48,6 +48,13 @@ \ out##_isUsed = static_cast(in); +#define CPP2CB_STRING_IF_USED(in, out) \ + if (in) { \ + CPP2CB_STRING(in.value(), out); \ + } \ + \ + out##_isUsed = static_cast(in); + template void cb_convert_enum(const T1& in, T2& out) { out = static_cast(in); } diff --git a/include/iso15118/detail/d20/state/dc_charge_loop.hpp b/include/iso15118/detail/d20/state/dc_charge_loop.hpp index d7afd3b6..50dcd900 100644 --- a/include/iso15118/detail/d20/state/dc_charge_loop.hpp +++ b/include/iso15118/detail/d20/state/dc_charge_loop.hpp @@ -2,9 +2,13 @@ // Copyright 2023 Pionix GmbH and Contributors to EVerest #pragma once +#include +#include #include #include +#include +#include #include #include #include @@ -13,8 +17,10 @@ namespace iso15118::d20::state { -std::tuple> -handle_request(const message_20::DC_ChargeLoopRequest& req, const d20::Session& session, const float present_voltage, - const float present_current, const bool stop, const DcTransferLimits& dc_limits); +message_20::DC_ChargeLoopResponse handle_request(const message_20::DC_ChargeLoopRequest& req, + const d20::Session& session, const float present_voltage, + const float present_current, const bool stop, + const DcTransferLimits& dc_limits, + const UpdateDynamicModeParameters& dynamic_parameters); } // namespace iso15118::d20::state diff --git a/include/iso15118/detail/d20/state/dc_pre_charge.hpp b/include/iso15118/detail/d20/state/dc_pre_charge.hpp index 5536451e..c3ac872f 100644 --- a/include/iso15118/detail/d20/state/dc_pre_charge.hpp +++ b/include/iso15118/detail/d20/state/dc_pre_charge.hpp @@ -2,6 +2,7 @@ // Copyright 2023 Pionix GmbH and Contributors to EVerest #pragma once +#include #include #include @@ -11,7 +12,7 @@ namespace iso15118::d20::state { -std::tuple -handle_request(const message_20::DC_PreChargeRequest& req, const d20::Session& session, const float present_voltage); +message_20::DC_PreChargeResponse handle_request(const message_20::DC_PreChargeRequest& req, const d20::Session& session, + const float present_voltage); } // namespace iso15118::d20::state diff --git a/include/iso15118/detail/d20/state/schedule_exchange.hpp b/include/iso15118/detail/d20/state/schedule_exchange.hpp index 7c275698..029afcf7 100644 --- a/include/iso15118/detail/d20/state/schedule_exchange.hpp +++ b/include/iso15118/detail/d20/state/schedule_exchange.hpp @@ -5,10 +5,17 @@ #include #include +#include +#include +#include + +#include + namespace iso15118::d20::state { message_20::ScheduleExchangeResponse handle_request(const message_20::ScheduleExchangeRequest& req, const d20::Session& session, - const message_20::RationalNumber& max_power); + const message_20::RationalNumber& max_power, + const UpdateDynamicModeParameters& dynamic_parameters); } // namespace iso15118::d20::state diff --git a/include/iso15118/message/common.hpp b/include/iso15118/message/common.hpp index 3ba776f8..9dfc2afc 100644 --- a/include/iso15118/message/common.hpp +++ b/include/iso15118/message/common.hpp @@ -286,4 +286,7 @@ RationalNumber from_float(float in); std::string from_Protocol(const Protocol& in); +std::string from_control_mode(const ControlMode& in); +std::string from_mobility_needs_mode(const MobilityNeedsMode& in); + } // namespace iso15118::message_20 diff --git a/include/iso15118/message/dc_charge_loop.hpp b/include/iso15118/message/dc_charge_loop.hpp index e5c01fc8..fc0f6c97 100644 --- a/include/iso15118/message/dc_charge_loop.hpp +++ b/include/iso15118/message/dc_charge_loop.hpp @@ -92,15 +92,17 @@ struct DC_ChargeLoopResponse { std::optional meter_info; std::optional receipt; - RationalNumber present_current; - RationalNumber present_voltage; + RationalNumber present_current{0, 0}; + RationalNumber present_voltage{0, 0}; bool power_limit_achieved{false}; bool current_limit_achieved{false}; bool voltage_limit_achieved{false}; std::variant - control_mode = Scheduled_DC_CLResControlMode(); + control_mode; + + DC_ChargeLoopResponse() : control_mode(std::in_place_type){}; }; } // namespace iso15118::message_20 diff --git a/include/iso15118/message/schedule_exchange.hpp b/include/iso15118/message/schedule_exchange.hpp index 2d385d14..53663e59 100644 --- a/include/iso15118/message/schedule_exchange.hpp +++ b/include/iso15118/message/schedule_exchange.hpp @@ -73,12 +73,6 @@ struct ScheduleExchangeRequest { struct ScheduleExchangeResponse { - static constexpr auto TAX_RULE_LENGTH = 10; - static constexpr auto PRICE_RULE_STACK_LENGTH = 1024; - static constexpr auto PRICE_RULE_LENGTH = 8; - static constexpr auto OVERSTAY_RULE_LENGTH = 5; - static constexpr auto ADDITIONAL_SERVICE_LENGTH = 5; - static constexpr auto PRICE_LEVEL_SCHEDULE_LENGTH = 1024; static constexpr auto SCHEDULED_POWER_DURATION_S = 86400; Header header; @@ -112,7 +106,7 @@ struct ScheduleExchangeResponse { struct PriceRuleStack { uint32_t duration; - std::array price_rule; + std::vector price_rule; }; struct OverstayRule { @@ -125,7 +119,7 @@ struct ScheduleExchangeResponse { struct OverstayRulesList { std::optional overstay_time_threshold; std::optional overstay_power_threshold; - std::array overstay_rule; + std::vector overstay_rule; }; struct AdditionalService { @@ -143,10 +137,10 @@ struct ScheduleExchangeResponse { std::string price_algorithm; std::optional minimum_cost; std::optional maximum_cost; - std::optional> tax_rules; - std::array price_rule_stacks; + std::optional> tax_rules; + std::vector price_rule_stacks; std::optional overstay_rules; - std::optional> additional_selected_services; + std::optional> additional_selected_services; }; struct PriceLevelScheduleEntry { @@ -160,7 +154,7 @@ struct ScheduleExchangeResponse { uint32_t price_schedule_id; std::optional price_schedule_description; uint8_t number_of_price_levels; - std::array price_level_schedule_entries; + std::vector price_level_schedule_entries; }; struct Dynamic_SEResControlMode { diff --git a/include/iso15118/session/feedback.hpp b/include/iso15118/session/feedback.hpp index aef73ee0..ad0396cb 100644 --- a/include/iso15118/session/feedback.hpp +++ b/include/iso15118/session/feedback.hpp @@ -2,10 +2,13 @@ // Copyright 2023 Pionix GmbH and Contributors to EVerest #pragma once +#include #include #include #include +#include +#include #include namespace iso15118::session { @@ -24,37 +27,30 @@ enum class Signal { DLINK_PAUSE, }; -struct DcChargeTarget { - float voltage{-1}; - float current{-1}; -}; - struct DcMaximumLimits { - float voltage{-1}; - float current{-1}; - float power{-1}; + float voltage{NAN}; + float current{NAN}; + float power{NAN}; }; -struct DisplayParameters { - std::optional present_soc; - std::optional minimum_soc; - std::optional target_soc; - std::optional maximum_soc; - std::optional remaining_time_to_minimum_soc; - std::optional remaining_time_to_target_soc; - std::optional remaining_time_to_maximum_soc; - std::optional battery_energy_capacity; - std::optional inlet_hot; -}; +using PresentVoltage = message_20::RationalNumber; +using MeterInfoRequested = bool; +using DcReqControlMode = std::variant; + +using DcChargeLoopReq = + std::variant; struct Callbacks { std::function signal; - std::function dc_charge_target; + std::function dc_pre_charge_target_voltage; + std::function dc_charge_loop_req; std::function dc_max_limits; std::function v2g_message; std::function evccid; std::function selected_protocol; - std::function display_parameters; }; } // namespace feedback @@ -64,12 +60,12 @@ class Feedback { Feedback(feedback::Callbacks); void signal(feedback::Signal) const; - void dc_charge_target(const feedback::DcChargeTarget&) const; + void dc_pre_charge_target_voltage(float) const; + void dc_charge_loop_req(const feedback::DcChargeLoopReq&) const; void dc_max_limits(const feedback::DcMaximumLimits&) const; void v2g_message(const message_20::Type&) const; void evcc_id(const std::string&) const; void selected_protocol(const std::string&) const; - void display_parameters(const feedback::DisplayParameters&) const; private: feedback::Callbacks callbacks; diff --git a/src/iso15118/d20/config.cpp b/src/iso15118/d20/config.cpp index 69bc5a52..1a99e346 100644 --- a/src/iso15118/d20/config.cpp +++ b/src/iso15118/d20/config.cpp @@ -8,33 +8,69 @@ namespace iso15118::d20 { -static std::vector get_default_dc_parameter_list() { - return {{ - message_20::DcConnector::Extended, - message_20::ControlMode::Scheduled, - message_20::MobilityNeedsMode::ProvidedByEvcc, - message_20::Pricing::NoPricing, - }}; +namespace { + +auto get_mobility_needs_mode(const ControlMobilityNeedsModes& mode) { + using namespace message_20; + + if (mode.control_mode == ControlMode::Scheduled and mode.mobility_mode == MobilityNeedsMode::ProvidedBySecc) { + logf_info("Setting the mobility needs mode to ProvidedByEvcc. In scheduled mode only ProvidedByEvcc is " + "supported."); + return MobilityNeedsMode::ProvidedByEvcc; + } + + return mode.mobility_mode; +} + +auto get_default_dc_parameter_list(const std::vector& control_mobility_modes) { + using namespace message_20; + + // TODO(sl): Add check if a control mode is more than one in that vector + + std::vector param_list; + + for (const auto& mode : control_mobility_modes) { + param_list.push_back({ + DcConnector::Extended, + mode.control_mode, + get_mobility_needs_mode(mode), + Pricing::NoPricing, + }); + } + + return param_list; } -static std::vector get_default_dc_bpt_parameter_list() { +auto get_default_dc_bpt_parameter_list(const std::vector& control_mobility_modes) { + using namespace message_20; + + // TODO(sl): Add check if a control mode is more than one in that vector - return {{{ - message_20::DcConnector::Extended, - message_20::ControlMode::Scheduled, - message_20::MobilityNeedsMode::ProvidedByEvcc, - message_20::Pricing::NoPricing, - }, - message_20::BptChannel::Unified, - message_20::GeneratorMode::GridFollowing}}; + std::vector param_list; + + for (const auto& mode : control_mobility_modes) { + param_list.push_back({{ + DcConnector::Extended, + mode.control_mode, + get_mobility_needs_mode(mode), + Pricing::NoPricing, + }, + BptChannel::Unified, + GeneratorMode::GridFollowing}); + } + + return param_list; } +} // namespace + SessionConfig::SessionConfig(EvseSetupConfig config) : evse_id(std::move(config.evse_id)), cert_install_service(config.enable_certificate_install_service), authorization_services(std::move(config.authorization_services)), supported_energy_transfer_services(std::move(config.supported_energy_services)), - dc_limits(std::move(config.dc_limits)) { + dc_limits(std::move(config.dc_limits)), + supported_control_mobility_modes(std::move(config.control_mobility_modes)) { const auto is_bpt_service = [](message_20::ServiceCategory service) { return service == message_20::ServiceCategory::DC_BPT; @@ -47,8 +83,14 @@ SessionConfig::SessionConfig(EvseSetupConfig config) : "limits. This can lead to session shutdowns."); } - dc_parameter_list = get_default_dc_parameter_list(); - dc_bpt_parameter_list = get_default_dc_bpt_parameter_list(); + if (supported_control_mobility_modes.empty()) { + logf_warning("No control modes were provided, set to scheduled mode"); + supported_control_mobility_modes = { + {message_20::ControlMode::Scheduled, message_20::MobilityNeedsMode::ProvidedByEvcc}}; + } + + dc_parameter_list = get_default_dc_parameter_list(supported_control_mobility_modes); + dc_bpt_parameter_list = get_default_dc_bpt_parameter_list(supported_control_mobility_modes); } } // namespace iso15118::d20 diff --git a/src/iso15118/d20/session.cpp b/src/iso15118/d20/session.cpp index 5eb8ea16..7410123a 100644 --- a/src/iso15118/d20/session.cpp +++ b/src/iso15118/d20/session.cpp @@ -4,6 +4,8 @@ #include +#include + namespace iso15118::d20 { Session::Session() { @@ -85,6 +87,10 @@ void Session::selected_service_parameters(const message_20::ServiceCategory serv this->selected_services = SelectedServiceParameters(message_20::ServiceCategory::DC, parameters.connector, parameters.control_mode, parameters.mobility_needs_mode, parameters.pricing); + + logf_info("Selected DC service parameters: control mode: %s, mobility needs mode: %s", + message_20::from_control_mode(parameters.control_mode).c_str(), + message_20::from_mobility_needs_mode(parameters.mobility_needs_mode).c_str()); } else { // Todo(sl): Should be not the case -> Raise Error? } @@ -97,6 +103,10 @@ void Session::selected_service_parameters(const message_20::ServiceCategory serv this->selected_services = SelectedServiceParameters( message_20::ServiceCategory::DC_BPT, parameters.connector, parameters.control_mode, parameters.mobility_needs_mode, parameters.pricing, parameters.bpt_channel, parameters.generator_mode); + + logf_info("Selected DC_BPT service parameters: control mode: %s, mobility needs mode: %s", + message_20::from_control_mode(parameters.control_mode).c_str(), + message_20::from_mobility_needs_mode(parameters.mobility_needs_mode).c_str()); } else { // Todo(sl): Should be not the case -> Raise Error? } diff --git a/src/iso15118/d20/state/dc_charge_loop.cpp b/src/iso15118/d20/state/dc_charge_loop.cpp index 41131905..1a2f99ab 100644 --- a/src/iso15118/d20/state/dc_charge_loop.cpp +++ b/src/iso15118/d20/state/dc_charge_loop.cpp @@ -17,6 +17,8 @@ using Dynamic_BPT_DC_Req = message_20::DC_ChargeLoopRequest::BPT_Dynamic_DC_CLRe using Scheduled_DC_Res = message_20::DC_ChargeLoopResponse::Scheduled_DC_CLResControlMode; using Scheduled_BPT_DC_Res = message_20::DC_ChargeLoopResponse::BPT_Scheduled_DC_CLResControlMode; +using Dynamic_DC_Res = message_20::DC_ChargeLoopResponse::Dynamic_DC_CLResControlMode; +using Dynamic_BPT_DC_Res = message_20::DC_ChargeLoopResponse::BPT_Dynamic_DC_CLResControlMode; template void convert(Out& out, const In& in); @@ -42,81 +44,138 @@ template <> void convert(Scheduled_BPT_DC_Res& out, const d20::DcTransferLimits& } } -static auto fill_parameters(const message_20::DisplayParameters& req_parameters) { - auto parameters = session::feedback::DisplayParameters{}; - - parameters.present_soc = req_parameters.present_soc; - parameters.minimum_soc = req_parameters.min_soc; - parameters.target_soc = req_parameters.target_soc; - parameters.maximum_soc = req_parameters.max_soc; - parameters.remaining_time_to_minimum_soc = req_parameters.remaining_time_to_min_soc; - parameters.remaining_time_to_target_soc = req_parameters.remaining_time_to_target_soc; - parameters.remaining_time_to_maximum_soc = req_parameters.remaining_time_to_max_soc; - if (req_parameters.battery_energy_capacity) { - parameters.battery_energy_capacity = message_20::from_RationalNumber(*req_parameters.battery_energy_capacity); +template <> void convert(Dynamic_DC_Res& out, const d20::DcTransferLimits& in) { + out.max_charge_power = in.charge_limits.power.max; + out.min_charge_power = in.charge_limits.power.min; + out.max_charge_current = in.charge_limits.current.max; + out.max_voltage = in.voltage.max; +} + +template <> void convert(Dynamic_BPT_DC_Res& out, const d20::DcTransferLimits& in) { + out.max_charge_power = in.charge_limits.power.max; + out.min_charge_power = in.charge_limits.power.min; + out.max_charge_current = in.charge_limits.current.max; + out.max_voltage = in.voltage.max; + out.min_voltage = in.voltage.min; + + if (in.discharge_limits.has_value()) { + auto& discharge_limits = in.discharge_limits.value(); + out.max_discharge_power = discharge_limits.power.max; + out.min_discharge_power = discharge_limits.power.min; + out.max_discharge_current = discharge_limits.current.max; } - parameters.inlet_hot = req_parameters.inlet_hot; +} - return parameters; +namespace { +template +void set_dynamic_parameters_in_res(T& res_mode, const UpdateDynamicModeParameters& parameters, + uint64_t header_timestamp) { + if (parameters.departure_time) { + const auto departure_time = static_cast(parameters.departure_time.value()); + if (departure_time > header_timestamp) { + res_mode.departure_time = static_cast(departure_time - header_timestamp); + } + } + res_mode.target_soc = parameters.target_soc; + res_mode.minimum_soc = parameters.min_soc; + res_mode.ack_max_delay = 30; // TODO(sl) what to send here and define 30 seconds as const } +} // namespace -std::tuple> -handle_request(const message_20::DC_ChargeLoopRequest& req, const d20::Session& session, const float present_voltage, - const float present_current, const bool stop, const DcTransferLimits& dc_limits) { +message_20::DC_ChargeLoopResponse handle_request(const message_20::DC_ChargeLoopRequest& req, + const d20::Session& session, const float present_voltage, + const float present_current, const bool stop, + const DcTransferLimits& dc_limits, + const UpdateDynamicModeParameters& dynamic_parameters) { message_20::DC_ChargeLoopResponse res; - std::optional charge_target{std::nullopt}; if (validate_and_setup_header(res.header, session, req.header.session_id) == false) { - return {response_with_code(res, message_20::ResponseCode::FAILED_UnknownSession), charge_target}; + return response_with_code(res, message_20::ResponseCode::FAILED_UnknownSession); } + const auto selected_services = session.get_selected_services(); + const auto selected_control_mode = selected_services.selected_control_mode; + const auto selected_energy_service = selected_services.selected_energy_service; + const auto selected_mobility_needs_mode = selected_services.selected_mobility_needs_mode; + if (std::holds_alternative(req.control_mode)) { - if (session.get_selected_energy_service() != message_20::ServiceCategory::DC) { - return {response_with_code(res, message_20::ResponseCode::FAILED), charge_target}; + // If the ev sends a false control mode or a false energy service other than the previous selected ones, then + // the charger should terminate the session + if (selected_control_mode != message_20::ControlMode::Scheduled or + selected_energy_service != message_20::ServiceCategory::DC) { + return response_with_code(res, message_20::ResponseCode::FAILED); } - const auto& req_mode = std::get(req.control_mode); - - charge_target = { - message_20::from_RationalNumber(req_mode.target_voltage), - message_20::from_RationalNumber(req_mode.target_current), - }; - - auto& mode = res.control_mode.emplace(); - convert(mode, dc_limits); + auto& res_mode = res.control_mode.emplace(); + convert(res_mode, dc_limits); } else if (std::holds_alternative(req.control_mode)) { - if (session.get_selected_energy_service() != message_20::ServiceCategory::DC_BPT) { - return {response_with_code(res, message_20::ResponseCode::FAILED), charge_target}; + // If the ev sends a false control mode or a false energy service other than the previous selected ones, then + // the charger should terminate the session + if (selected_control_mode != message_20::ControlMode::Scheduled or + selected_energy_service != message_20::ServiceCategory::DC_BPT) { + return response_with_code(res, message_20::ResponseCode::FAILED); } if (not dc_limits.discharge_limits.has_value()) { logf_error("Transfer mode is BPT, but only dc limits without discharge limits are provided!"); - return {response_with_code(res, message_20::ResponseCode::FAILED), charge_target}; + return response_with_code(res, message_20::ResponseCode::FAILED); } - auto& mode = res.control_mode.emplace(); - convert(mode, dc_limits); + auto& res_mode = res.control_mode.emplace(); + convert(res_mode, dc_limits); - const auto& req_mode = std::get(req.control_mode); + } else if (std::holds_alternative(req.control_mode)) { - charge_target = { - message_20::from_RationalNumber(req_mode.target_voltage), - message_20::from_RationalNumber(req_mode.target_current), - }; + // If the ev sends a false control mode or a false energy service other than the previous selected ones, then + // the charger should terminate the session + if (selected_control_mode != message_20::ControlMode::Dynamic or + selected_energy_service != message_20::ServiceCategory::DC) { + return response_with_code(res, message_20::ResponseCode::FAILED); + } + + auto& res_mode = res.control_mode.emplace(); + convert(res_mode, dc_limits); + + if (selected_mobility_needs_mode == message_20::MobilityNeedsMode::ProvidedBySecc) { + set_dynamic_parameters_in_res(res_mode, dynamic_parameters, res.header.timestamp); + } + + } else if (std::holds_alternative(req.control_mode)) { + + // If the ev sends a false control mode or a false energy service other than the previous selected ones, then + // the charger should terminate the session + if (selected_control_mode != message_20::ControlMode::Dynamic or + selected_energy_service != message_20::ServiceCategory::DC_BPT) { + return response_with_code(res, message_20::ResponseCode::FAILED); + } + + if (not dc_limits.discharge_limits.has_value()) { + logf_error("Transfer mode is BPT, but only dc limits without discharge limits are provided!"); + return response_with_code(res, message_20::ResponseCode::FAILED); + } + + auto& res_mode = res.control_mode.emplace(); + convert(res_mode, dc_limits); + + if (selected_mobility_needs_mode == message_20::MobilityNeedsMode::ProvidedBySecc) { + set_dynamic_parameters_in_res(res_mode, dynamic_parameters, res.header.timestamp); + } } res.present_voltage = iso15118::message_20::from_float(present_voltage); res.present_current = iso15118::message_20::from_float(present_current); + // TODO(sl): Setting EvseStatus, MeterInfo, Receipt, *_limit_achieved + if (stop) { res.status = {0, iso15118::message_20::EvseNotification::Terminate}; } - return {response_with_code(res, message_20::ResponseCode::OK), charge_target}; + return response_with_code(res, message_20::ResponseCode::OK); } void DC_ChargeLoop::enter() { @@ -127,11 +186,13 @@ FsmSimpleState::HandleEventReturnType DC_ChargeLoop::handle_event(AllocatorType& if (ev == FsmEvent::CONTROL_MESSAGE) { - if (const auto control_data = ctx.get_control_event()) { + if (const auto* control_data = ctx.get_control_event()) { present_voltage = control_data->voltage; present_current = control_data->current; - } else if (const auto control_data = ctx.get_control_event()) { + } else if (const auto* control_data = ctx.get_control_event()) { stop = *control_data; + } else if (const auto* control_data = ctx.get_control_event()) { + dynamic_parameters = *control_data; } // Ignore control message @@ -171,26 +232,23 @@ FsmSimpleState::HandleEventReturnType DC_ChargeLoop::handle_event(AllocatorType& first_entry_in_charge_loop = false; } - const auto [res, charge_target] = - handle_request(*req, ctx.session, present_voltage, present_current, stop, ctx.session_config.dc_limits); - - if (charge_target) { - ctx.feedback.dc_charge_target(charge_target.value()); - } + const auto res = handle_request(*req, ctx.session, present_voltage, present_current, stop, + ctx.session_config.dc_limits, dynamic_parameters); ctx.respond(res); - if (req->display_parameters.has_value()) { - const auto feedback_parameters = fill_parameters(*req->display_parameters); - - ctx.feedback.display_parameters(feedback_parameters); - } - if (res.response_code >= message_20::ResponseCode::FAILED) { ctx.session_stopped = true; return sa.PASS_ON; } + ctx.feedback.dc_charge_loop_req(req->control_mode); + ctx.feedback.dc_charge_loop_req(req->present_voltage); + ctx.feedback.dc_charge_loop_req(req->meter_info_requested); + if (req->display_parameters) { + ctx.feedback.dc_charge_loop_req(*req->display_parameters); + } + return sa.HANDLED_INTERNALLY; } else { ctx.log("Expected PowerDeliveryReq or DC_ChargeLoopReq! But code type id: %d", variant->get_type()); diff --git a/src/iso15118/d20/state/dc_charge_parameter_discovery.cpp b/src/iso15118/d20/state/dc_charge_parameter_discovery.cpp index bcd4f818..059d0397 100644 --- a/src/iso15118/d20/state/dc_charge_parameter_discovery.cpp +++ b/src/iso15118/d20/state/dc_charge_parameter_discovery.cpp @@ -56,8 +56,10 @@ handle_request(const message_20::DC_ChargeParameterDiscoveryRequest& req, const return response_with_code(res, message_20::ResponseCode::FAILED_UnknownSession); } + const auto selected_energy_service = session.get_selected_services().selected_energy_service; + if (std::holds_alternative(req.transfer_mode)) { - if (session.get_selected_energy_service() != message_20::ServiceCategory::DC) { + if (selected_energy_service != message_20::ServiceCategory::DC) { return response_with_code(res, message_20::ResponseCode::FAILED_WrongChargeParameter); } @@ -65,7 +67,7 @@ handle_request(const message_20::DC_ChargeParameterDiscoveryRequest& req, const convert(mode, dc_limits); } else if (std::holds_alternative(req.transfer_mode)) { - if (session.get_selected_energy_service() != message_20::ServiceCategory::DC_BPT) { + if (selected_energy_service != message_20::ServiceCategory::DC_BPT) { return response_with_code(res, message_20::ResponseCode::FAILED_WrongChargeParameter); } diff --git a/src/iso15118/d20/state/dc_pre_charge.cpp b/src/iso15118/d20/state/dc_pre_charge.cpp index 717b9bb4..c75391a9 100644 --- a/src/iso15118/d20/state/dc_pre_charge.cpp +++ b/src/iso15118/d20/state/dc_pre_charge.cpp @@ -10,22 +10,18 @@ namespace iso15118::d20::state { -std::tuple -handle_request(const message_20::DC_PreChargeRequest& req, const d20::Session& session, const float present_voltage) { +message_20::DC_PreChargeResponse handle_request(const message_20::DC_PreChargeRequest& req, const d20::Session& session, + const float present_voltage) { message_20::DC_PreChargeResponse res; - session::feedback::DcChargeTarget charge_target{}; if (validate_and_setup_header(res.header, session, req.header.session_id) == false) { - return {response_with_code(res, message_20::ResponseCode::FAILED_UnknownSession), charge_target}; + return response_with_code(res, message_20::ResponseCode::FAILED_UnknownSession); } - charge_target.voltage = message_20::from_RationalNumber(req.target_voltage); - charge_target.current = 0; - res.present_voltage = message_20::from_float(present_voltage); - return {response_with_code(res, message_20::ResponseCode::OK), charge_target}; + return response_with_code(res, message_20::ResponseCode::OK); } void DC_PreCharge::enter() { @@ -53,10 +49,9 @@ FsmSimpleState::HandleEventReturnType DC_PreCharge::handle_event(AllocatorType& const auto variant = ctx.pull_request(); if (const auto req = variant->get_if()) { - const auto [res, charge_target] = handle_request(*req, ctx.session, present_voltage); + const auto res = handle_request(*req, ctx.session, present_voltage); - // FIXME (aw): should we always send this charge_target, even if the res errored? - ctx.feedback.dc_charge_target(charge_target); + ctx.feedback.dc_pre_charge_target_voltage(message_20::from_RationalNumber(req->target_voltage)); ctx.respond(res); diff --git a/src/iso15118/d20/state/power_delivery.cpp b/src/iso15118/d20/state/power_delivery.cpp index 244bf3d1..6c6fb037 100644 --- a/src/iso15118/d20/state/power_delivery.cpp +++ b/src/iso15118/d20/state/power_delivery.cpp @@ -20,6 +20,8 @@ message_20::PowerDeliveryResponse handle_request(const message_20::PowerDelivery return response_with_code(res, message_20::ResponseCode::FAILED_UnknownSession); } + // TODO(sl): Check Req PowerProfile & ChannelSelection + // Todo(sl): Add standby feature and define as everest module config if (req.charge_progress == message_20::PowerDeliveryRequest::Progress::Standby) { return response_with_code(res, message_20::ResponseCode::WARNING_StandbyNotAllowed); @@ -53,10 +55,9 @@ FsmSimpleState::HandleEventReturnType PowerDelivery::handle_event(AllocatorType& const auto variant = ctx.pull_request(); if (const auto req = variant->get_if()) { - const auto [res, charge_target] = handle_request(*req, ctx.session, present_voltage); + const auto res = handle_request(*req, ctx.session, present_voltage); - // FIXME (aw): should we always send this charge_target, even if the res errored? - ctx.feedback.dc_charge_target(charge_target); + ctx.feedback.dc_pre_charge_target_voltage(message_20::from_RationalNumber(req->target_voltage)); ctx.respond(res); diff --git a/src/iso15118/d20/state/schedule_exchange.cpp b/src/iso15118/d20/state/schedule_exchange.cpp index 79a870d2..8e8065ed 100644 --- a/src/iso15118/d20/state/schedule_exchange.cpp +++ b/src/iso15118/d20/state/schedule_exchange.cpp @@ -12,9 +12,51 @@ namespace iso15118::d20::state { +using ScheduledReqControlMode = message_20::ScheduleExchangeRequest::Scheduled_SEReqControlMode; +using ScheduledResControlMode = message_20::ScheduleExchangeResponse::Scheduled_SEResControlMode; + +using DynamicReqControlMode = message_20::ScheduleExchangeRequest::Dynamic_SEReqControlMode; +using DynamicResControlMode = message_20::ScheduleExchangeResponse::Dynamic_SEResControlMode; + +namespace { +auto create_default_scheduled_control_mode(const message_20::RationalNumber& max_power) { + message_20::ScheduleExchangeResponse::ScheduleTuple schedule; + schedule.schedule_tuple_id = 1; + schedule.charging_schedule.power_schedule.time_anchor = + static_cast(std::time(nullptr)); // PowerSchedule is now active + + message_20::ScheduleExchangeResponse::PowerScheduleEntry power_schedule; + power_schedule.power = max_power; + power_schedule.duration = message_20::ScheduleExchangeResponse::SCHEDULED_POWER_DURATION_S; + schedule.charging_schedule.power_schedule.entries.push_back(power_schedule); + + ScheduledResControlMode scheduled_mode{}; + + // Providing no price schedule! + // NOTE: Agreement on iso15118.elaad.io: [V2G20-2176] is not required and should be ignored. + scheduled_mode.schedule_tuple = {schedule}; + return scheduled_mode; +} + +namespace { +void set_dynamic_parameters_in_res(DynamicResControlMode& res_mode, const UpdateDynamicModeParameters& parameters, + uint64_t header_timestamp) { + if (parameters.departure_time) { + const auto departure_time = static_cast(parameters.departure_time.value()); + if (departure_time > header_timestamp) { + res_mode.departure_time = static_cast(departure_time - header_timestamp); + } + } + res_mode.target_soc = parameters.target_soc; + res_mode.minimum_soc = parameters.min_soc; +} +} // namespace +} // namespace + message_20::ScheduleExchangeResponse handle_request(const message_20::ScheduleExchangeRequest& req, const d20::Session& session, - const message_20::RationalNumber& max_power) { + const message_20::RationalNumber& max_power, + const UpdateDynamicModeParameters& dynamic_parameters) { message_20::ScheduleExchangeResponse res; @@ -22,37 +64,29 @@ message_20::ScheduleExchangeResponse handle_request(const message_20::ScheduleEx return response_with_code(res, message_20::ResponseCode::FAILED_UnknownSession); } - // Todo(SL): Publish data from request? + const auto selected_services = session.get_selected_services(); + const auto selected_control_mode = selected_services.selected_control_mode; + const auto selected_mobility_needs_mode = selected_services.selected_mobility_needs_mode; - if (session.get_selected_control_mode() == message_20::ControlMode::Scheduled && - std::holds_alternative(req.control_mode)) { + // Todo(SL): Publish data from request? - auto& control_mode = - res.control_mode.emplace(); + if (selected_control_mode == message_20::ControlMode::Scheduled && + std::holds_alternative(req.control_mode)) { - // Define one minimal default schedule - // No price schedule, no discharging power schedule - // Todo(sl): Adding price schedule - // Todo(sl): Adding discharging schedule - message_20::ScheduleExchangeResponse::ScheduleTuple schedule; - schedule.schedule_tuple_id = 1; - schedule.charging_schedule.power_schedule.time_anchor = - static_cast(std::time(nullptr)); // PowerSchedule is now active + res.control_mode.emplace(create_default_scheduled_control_mode(max_power)); - message_20::ScheduleExchangeResponse::PowerScheduleEntry power_schedule; - power_schedule.power = max_power; - power_schedule.duration = message_20::ScheduleExchangeResponse::SCHEDULED_POWER_DURATION_S; + // TODO(sl): Adding price schedule + // TODO(sl): Adding discharging schedule - schedule.charging_schedule.power_schedule.entries.push_back(power_schedule); + } else if (selected_control_mode == message_20::ControlMode::Dynamic && + std::holds_alternative(req.control_mode)) { - control_mode.schedule_tuple.push_back(schedule); + // TODO(sl): Publish req dynamic mode parameters + auto& mode = res.control_mode.emplace(); - } else if (session.get_selected_control_mode() == message_20::ControlMode::Dynamic && - std::holds_alternative( - req.control_mode)) { - // TODO(sl): Adding Dynamic Mode - [[maybe_unused]] auto& control_mode = - res.control_mode.emplace(); + if (selected_mobility_needs_mode == message_20::MobilityNeedsMode::ProvidedBySecc) { + set_dynamic_parameters_in_res(mode, dynamic_parameters, res.header.timestamp); + } } else { logf_error("The control mode of the req message does not match the previously agreed contol mode."); @@ -69,6 +103,18 @@ void ScheduleExchange::enter() { } FsmSimpleState::HandleEventReturnType ScheduleExchange::handle_event(AllocatorType& sa, FsmEvent ev) { + + if (ev == FsmEvent::CONTROL_MESSAGE) { + + // TODO(sl): Not sure if the data comes here just in time? + if (const auto* control_data = ctx.get_control_event()) { + dynamic_parameters = *control_data; + } + + // Ignore control message + return sa.HANDLED_INTERNALLY; + } + if (ev != FsmEvent::V2GTP_MESSAGE) { return sa.PASS_ON; } @@ -79,14 +125,14 @@ FsmSimpleState::HandleEventReturnType ScheduleExchange::handle_event(AllocatorTy message_20::RationalNumber max_charge_power = {0, 0}; - const auto selected_energy_service = ctx.session.get_selected_energy_service(); + const auto selected_energy_service = ctx.session.get_selected_services().selected_energy_service; if (selected_energy_service == message_20::ServiceCategory::DC or selected_energy_service == message_20::ServiceCategory::DC_BPT) { max_charge_power = ctx.session_config.dc_limits.charge_limits.power.max; } - const auto res = handle_request(*req, ctx.session, max_charge_power); + const auto res = handle_request(*req, ctx.session, max_charge_power, dynamic_parameters); ctx.respond(res); diff --git a/src/iso15118/message/common.cpp b/src/iso15118/message/common.cpp index dc516c80..507b87b7 100644 --- a/src/iso15118/message/common.cpp +++ b/src/iso15118/message/common.cpp @@ -139,4 +139,24 @@ std::string from_Protocol(const Protocol& in) { return ""; } +std::string from_control_mode(const ControlMode& in) { + switch (in) { + case ControlMode::Scheduled: + return "Scheduled"; + case ControlMode::Dynamic: + return "Dynamic"; + } + return ""; +} + +std::string from_mobility_needs_mode(const MobilityNeedsMode& in) { + switch (in) { + case MobilityNeedsMode::ProvidedByEvcc: + return "ProvidedByEvcc"; + case MobilityNeedsMode::ProvidedBySecc: + return "ProvidedBySecc"; + } + return ""; +} + } // namespace iso15118::message_20 diff --git a/src/iso15118/message/dc_charge_loop.cpp b/src/iso15118/message/dc_charge_loop.cpp index e21da0b0..76000f08 100644 --- a/src/iso15118/message/dc_charge_loop.cpp +++ b/src/iso15118/message/dc_charge_loop.cpp @@ -73,7 +73,7 @@ template void convert(const InType& in, DC_ChargeLoopRequest:: convert(in.EVMaximumChargePower, out.max_charge_power); convert(in.EVMinimumChargePower, out.min_charge_power); - convert(in.EVMaximumChargeCurrent, out.max_charge_power); + convert(in.EVMaximumChargeCurrent, out.max_charge_current); convert(in.EVMaximumVoltage, out.max_voltage); convert(in.EVMinimumVoltage, out.min_voltage); } diff --git a/src/iso15118/message/schedule_exchange.cpp b/src/iso15118/message/schedule_exchange.cpp index 74f6b88d..250d2082 100644 --- a/src/iso15118/message/schedule_exchange.cpp +++ b/src/iso15118/message/schedule_exchange.cpp @@ -115,10 +115,10 @@ template <> void convert(const ScheduleExchangeResponse::PowerSchedule& in, stru if ((sizeof(out.PowerScheduleEntries.PowerScheduleEntry.array) / sizeof(out.PowerScheduleEntries.PowerScheduleEntry.array[0])) < in.entries.size()) { - throw std::runtime_error("array is too large"); + throw std::runtime_error("array is too large"); // FIXME(SL): Change error message } - for (auto i = 0; in.entries.size(); i++) { + for (std::size_t i = 0; i < in.entries.size(); i++) { auto& out_entry = out.PowerScheduleEntries.PowerScheduleEntry.array[i]; const auto& in_entry = in.entries[i]; @@ -130,12 +130,171 @@ template <> void convert(const ScheduleExchangeResponse::PowerSchedule& in, stru out.PowerScheduleEntries.PowerScheduleEntry.arrayLen = in.entries.size(); } +template <> void convert(const ScheduleExchangeResponse::TaxRule& in, struct iso20_TaxRuleType& out) { + out.TaxRuleID = in.tax_rule_id; + CPP2CB_STRING_IF_USED(in.tax_rule_name, out.TaxRuleName); + convert(in.tax_rate, out.TaxRate); + CPP2CB_ASSIGN_IF_USED(in.tax_included_in_price, out.TaxIncludedInPrice); + out.AppliesToEnergyFee = in.applies_to_energy_fee; + out.AppliesToParkingFee = in.applies_to_parking_fee; + out.AppliesToOverstayFee = in.applies_to_overstay_fee; + out.AppliesMinimumMaximumCost = in.applies_to_minimum_maximum_cost; +} + +template <> void convert(const ScheduleExchangeResponse::PriceRule& in, struct iso20_PriceRuleType& out) { + convert(in.energy_fee, out.EnergyFee); + CPP2CB_CONVERT_IF_USED(in.parking_fee, out.ParkingFee); + CPP2CB_ASSIGN_IF_USED(in.parking_fee_period, out.ParkingFeePeriod); + CPP2CB_ASSIGN_IF_USED(in.carbon_dioxide_emission, out.CarbonDioxideEmission); + CPP2CB_ASSIGN_IF_USED(in.renewable_generation_percentage, out.RenewableGenerationPercentage); + convert(in.power_range_start, out.PowerRangeStart); +} + +template <> void convert(const ScheduleExchangeResponse::PriceRuleStack& in, struct iso20_PriceRuleStackType& out) { + out.Duration = in.duration; + + if ((sizeof(out.PriceRule.array) / sizeof(out.PriceRule.array[0])) < in.price_rule.size()) { + throw std::runtime_error("array is too large"); // FIXME(SL): Change error message + } + for (std::size_t i = 0; i < in.price_rule.size(); i++) { + convert(in.price_rule.at(i), out.PriceRule.array[i]); + } + out.PriceRule.arrayLen = in.price_rule.size(); +} + +template <> void convert(const ScheduleExchangeResponse::OverstayRule& in, struct iso20_OverstayRuleType& out) { + CPP2CB_STRING_IF_USED(in.overstay_rule_description, out.OverstayRuleDescription); + out.StartTime = in.start_time; + convert(in.overstay_fee, out.OverstayFee); + out.OverstayFeePeriod = in.overstay_fee_period; +} + +template <> +void convert(const ScheduleExchangeResponse::AbsolutePriceSchedule& in, struct iso20_AbsolutePriceScheduleType& out) { + + CPP2CB_STRING_IF_USED(in.id, out.Id); + out.TimeAnchor = in.time_anchor; + out.PriceScheduleID = in.price_schedule_id; + CPP2CB_STRING_IF_USED(in.price_schedule_description, out.PriceScheduleDescription); + CPP2CB_STRING(in.currency, out.Currency); + CPP2CB_STRING(in.language, out.Language); + CPP2CB_STRING(in.price_algorithm, out.PriceAlgorithm); + CPP2CB_CONVERT_IF_USED(in.minimum_cost, out.MaximumCost); + CPP2CB_CONVERT_IF_USED(in.maximum_cost, out.MaximumCost); + + if (in.tax_rules.has_value()) { + out.TaxRules_isUsed = true; + const auto& in_tax_rules = in.tax_rules.value(); + + if ((sizeof(out.TaxRules.TaxRule.array) / sizeof(out.TaxRules.TaxRule.array[0])) < in_tax_rules.size()) { + throw std::runtime_error("array is too large"); // FIXME(SL): Change error message + } + + for (std::size_t i = 0; i < in_tax_rules.size(); i++) { + convert(in_tax_rules.at(i), out.TaxRules.TaxRule.array[i]); + } + out.TaxRules.TaxRule.arrayLen = in_tax_rules.size(); + } + + if ((sizeof(out.PriceRuleStacks.PriceRuleStack.array) / sizeof(out.PriceRuleStacks.PriceRuleStack.array[0])) < + in.price_rule_stacks.size()) { + throw std::runtime_error("array is too large"); // FIXME(SL): Change error message + } + for (std::size_t i = 0; i < in.price_rule_stacks.size(); i++) { + convert(in.price_rule_stacks.at(i), out.PriceRuleStacks.PriceRuleStack.array[i]); + } + out.PriceRuleStacks.PriceRuleStack.arrayLen = in.price_rule_stacks.size(); + + if (in.overstay_rules.has_value()) { + out.OverstayRules_isUsed = true; + const auto& in_overstay_rules = in.overstay_rules.value(); + + CPP2CB_ASSIGN_IF_USED(in_overstay_rules.overstay_time_threshold, out.OverstayRules.OverstayTimeThreshold); + CPP2CB_CONVERT_IF_USED(in_overstay_rules.overstay_power_threshold, out.OverstayRules.OverstayPowerThreshold); + + if ((sizeof(out.OverstayRules.OverstayRule.array) / sizeof(out.OverstayRules.OverstayRule.array[0])) < + in_overstay_rules.overstay_rule.size()) { + throw std::runtime_error("array is too large"); // FIXME(SL): Change error message + } + + for (std::size_t i = 0; i < in_overstay_rules.overstay_rule.size(); i++) { + convert(in_overstay_rules.overstay_rule.at(i), out.OverstayRules.OverstayRule.array[i]); + } + out.OverstayRules.OverstayRule.arrayLen = in_overstay_rules.overstay_rule.size(); + } + + if (in.additional_selected_services.has_value()) { + out.AdditionalSelectedServices_isUsed = true; + const auto& in_add_services = in.additional_selected_services.value(); + + if ((sizeof(out.AdditionalSelectedServices.AdditionalService.array) / + sizeof(out.AdditionalSelectedServices.AdditionalService.array[0])) < in_add_services.size()) { + throw std::runtime_error("array is too large"); // FIXME(SL): Change error message + } + + for (std::size_t i = 0; i < in_add_services.size(); i++) { + CPP2CB_STRING(in_add_services.at(i).service_name, + out.AdditionalSelectedServices.AdditionalService.array[i].ServiceName); + convert(in_add_services.at(i).service_fee, + out.AdditionalSelectedServices.AdditionalService.array[i].ServiceFee); + } + out.AdditionalSelectedServices.AdditionalService.arrayLen = in_add_services.size(); + } +} + +template <> +void convert(const ScheduleExchangeResponse::PriceLevelSchedule& in, struct iso20_PriceLevelScheduleType& out) { + + CPP2CB_STRING_IF_USED(in.id, out.Id); + out.TimeAnchor = in.time_anchor; + out.PriceScheduleID = in.price_schedule_id; + CPP2CB_STRING_IF_USED(in.price_schedule_description, out.PriceScheduleDescription); + out.NumberOfPriceLevels = in.number_of_price_levels; + + if ((sizeof(out.PriceLevelScheduleEntries.PriceLevelScheduleEntry.array) / + sizeof(out.PriceLevelScheduleEntries.PriceLevelScheduleEntry.array[0])) < + in.price_level_schedule_entries.size()) { + throw std::runtime_error("array is too large"); // FIXME(SL): Change error message + } + + for (std::size_t i = 0; i < in.price_level_schedule_entries.size(); i++) { + out.PriceLevelScheduleEntries.PriceLevelScheduleEntry.array[i].Duration = + in.price_level_schedule_entries.at(i).duration; + out.PriceLevelScheduleEntries.PriceLevelScheduleEntry.array[i].PriceLevel = + in.price_level_schedule_entries.at(i).price_level; + } + + out.PriceLevelScheduleEntries.PriceLevelScheduleEntry.arrayLen = in.price_level_schedule_entries.size(); +} + +using PriceSchedule = std::variant; +template void convert_price_schedule(const PriceSchedule& in, CbMessageType& out) { + + if (const auto* absolute_price = std::get_if(&in)) { + convert(*absolute_price, out.AbsolutePriceSchedule); + out.AbsolutePriceSchedule_isUsed = true; + } else if (const auto* price_level_schedule = std::get_if(&in)) { + convert(*price_level_schedule, out.PriceLevelSchedule); + out.PriceLevelSchedule_isUsed = true; + } else { + out.AbsolutePriceSchedule_isUsed = false; + out.PriceLevelSchedule_isUsed = false; + } +} + +template <> void convert(const PriceSchedule& in, struct iso20_ChargingScheduleType& out) { + convert_price_schedule(in, out); +} +template <> void convert(const PriceSchedule& in, struct iso20_Dynamic_SEResControlModeType& out) { + convert_price_schedule(in, out); +} + template <> void convert(const ScheduleExchangeResponse::ChargingSchedule& in, struct iso20_ChargingScheduleType& out) { init_iso20_ChargingScheduleType(&out); convert(in.power_schedule, out.PowerSchedule); - - // todo(sl): price_schedule + convert(in.price_schedule, out); } template <> void convert(const ScheduleExchangeResponse::ScheduleTuple& in, struct iso20_ScheduleTupleType& out) { @@ -158,17 +317,17 @@ struct ModeResponseVisitor { CPP2CB_ASSIGN_IF_USED(in.minimum_soc, out.MinimumSOC); CPP2CB_ASSIGN_IF_USED(in.target_soc, out.TargetSOC); - // Todo(sl): price_schedule + convert(in.price_schedule, out); } void operator()(const ScheduleExchangeResponse::Scheduled_SEResControlMode& in) { init_iso20_Scheduled_SEResControlModeType(&res.Scheduled_SEResControlMode); - CB_SET_USED(res.Dynamic_SEResControlMode); + CB_SET_USED(res.Scheduled_SEResControlMode); auto& out = res.Scheduled_SEResControlMode; if ((sizeof(out.ScheduleTuple.array) / sizeof(out.ScheduleTuple.array[0])) < in.schedule_tuple.size()) { - throw std::runtime_error("array is too large"); + throw std::runtime_error("array is too large"); // FIXME(SL): Change error message } for (std::size_t i = 0; i < in.schedule_tuple.size(); i++) { @@ -189,41 +348,9 @@ template <> void convert(const ScheduleExchangeResponse& in, struct iso20_Schedu cb_convert_enum(in.processing, out.EVSEProcessing); - // CPP2CB_ASSIGN_IF_USED(in.go_to_pause, out.GoToPause); - // std::visit(ModeResponseVisitor(out), in.control_mode); - - // ----------------------------------------------------------------------------------- - - out.GoToPause_isUsed = false; - - out.Dynamic_SEResControlMode_isUsed = false; - out.Scheduled_SEResControlMode_isUsed = true; - out.Scheduled_SEResControlMode.ScheduleTuple.arrayLen = 1; - auto& tuple = out.Scheduled_SEResControlMode.ScheduleTuple.array[0]; - tuple.DischargingSchedule_isUsed = false; - tuple.ChargingSchedule.AbsolutePriceSchedule_isUsed = false; - tuple.ChargingSchedule.PriceLevelSchedule_isUsed = true; - auto& price_schedule = tuple.ChargingSchedule.PriceLevelSchedule; - price_schedule.TimeAnchor = 234; - price_schedule.PriceScheduleID = 1; - price_schedule.PriceScheduleDescription_isUsed = false; - price_schedule.NumberOfPriceLevels = 0; - price_schedule.PriceLevelScheduleEntries.PriceLevelScheduleEntry.arrayLen = 1; - auto& price_entry = price_schedule.PriceLevelScheduleEntries.PriceLevelScheduleEntry.array[0]; - price_entry.Duration = 23; - price_entry.PriceLevel = 8; - tuple.ScheduleTupleID = 1; - auto& power_sched = tuple.ChargingSchedule.PowerSchedule; - power_sched.AvailableEnergy_isUsed = false; - power_sched.PowerTolerance_isUsed = false; - power_sched.TimeAnchor = 23423; - power_sched.PowerScheduleEntries.PowerScheduleEntry.arrayLen = 1; - auto& entry = power_sched.PowerScheduleEntries.PowerScheduleEntry.array[0]; - entry.Duration = 23; - entry.Power.Exponent = 2; - entry.Power.Value = 10; - entry.Power_L2_isUsed = 0; - entry.Power_L3_isUsed = 0; + CPP2CB_ASSIGN_IF_USED(in.go_to_pause, out.GoToPause); + + std::visit(ModeResponseVisitor(out), in.control_mode); } template <> void insert_type(VariantAccess& va, const struct iso20_ScheduleExchangeReqType& in) { diff --git a/src/iso15118/session/feedback.cpp b/src/iso15118/session/feedback.cpp index 5141889a..8e1fa918 100644 --- a/src/iso15118/session/feedback.cpp +++ b/src/iso15118/session/feedback.cpp @@ -13,8 +13,12 @@ void Feedback::signal(feedback::Signal signal) const { call_if_available(callbacks.signal, signal); } -void Feedback::dc_charge_target(const feedback::DcChargeTarget& charge_target) const { - call_if_available(callbacks.dc_charge_target, charge_target); +void Feedback::dc_pre_charge_target_voltage(float voltage) const { + call_if_available(callbacks.dc_pre_charge_target_voltage, voltage); +} + +void Feedback::dc_charge_loop_req(const feedback::DcChargeLoopReq& req_values) const { + call_if_available(callbacks.dc_charge_loop_req, req_values); } void Feedback::dc_max_limits(const feedback::DcMaximumLimits& max_limits) const { @@ -29,12 +33,8 @@ void Feedback::evcc_id(const std::string& evccid) const { call_if_available(callbacks.evccid, evccid); } -void Feedback::selected_protocol(const std::string& selected_protocol_) const { - call_if_available(callbacks.selected_protocol, selected_protocol_); -} - -void Feedback::display_parameters(const feedback::DisplayParameters& display_parameters_) const { - call_if_available(callbacks.display_parameters, display_parameters_); +void Feedback::selected_protocol(const std::string& selected_protocol) const { + call_if_available(callbacks.selected_protocol, selected_protocol); } } // namespace iso15118::session diff --git a/src/iso15118/session/iso.cpp b/src/iso15118/session/iso.cpp index 22c665a2..59cd7d97 100644 --- a/src/iso15118/session/iso.cpp +++ b/src/iso15118/session/iso.cpp @@ -159,6 +159,7 @@ TimePoint const& Session::poll() { // send all of our queued control events while ((active_control_event = control_event_queue.pop()) != std::nullopt) { + // TODO(sl): Save UpdateDynamicParameters as well for ScheduleExchange if (const auto control_data = ctx.get_control_event()) { ctx.session_config.dc_limits = *control_data; } diff --git a/test/exi/cb/iso20/CMakeLists.txt b/test/exi/cb/iso20/CMakeLists.txt index 84eb28ff..ac8ec83a 100644 --- a/test/exi/cb/iso20/CMakeLists.txt +++ b/test/exi/cb/iso20/CMakeLists.txt @@ -1,6 +1,6 @@ include(Catch) -macro(create_exi_test_target NAME) +function(create_exi_test_target NAME) add_executable(test_exi_${NAME} ${NAME}.cpp) target_link_libraries(test_exi_${NAME} PRIVATE @@ -8,7 +8,7 @@ macro(create_exi_test_target NAME) Catch2::Catch2WithMain ) catch_discover_tests(test_exi_${NAME}) -endmacro() +endfunction() create_exi_test_target(session_setup) create_exi_test_target(authorization_setup) @@ -17,7 +17,7 @@ create_exi_test_target(service_discovery) create_exi_test_target(service_detail) create_exi_test_target(service_selection) create_exi_test_target(dc_charge_parameter_discovery) -# create_exi_test_target(schedule_exchange) +create_exi_test_target(schedule_exchange) create_exi_test_target(dc_cable_check) create_exi_test_target(dc_pre_charge) create_exi_test_target(power_delivery) diff --git a/test/exi/cb/iso20/dc_charge_loop.cpp b/test/exi/cb/iso20/dc_charge_loop.cpp index 846ccae8..108c286e 100644 --- a/test/exi/cb/iso20/dc_charge_loop.cpp +++ b/test/exi/cb/iso20/dc_charge_loop.cpp @@ -38,6 +38,50 @@ SCENARIO("Se/Deserialize dc charge loop messages") { } } + GIVEN("Deserialize dc_charge_loop_req dynamic mode") { + + uint8_t doc_raw[] = {0x80, 0x34, 0x04, 0x32, 0x75, 0x76, 0x9e, 0xc7, 0x10, 0x64, 0xac, 0x8f, 0x0c, 0xfd, + 0xab, 0x70, 0x62, 0x00, 0x51, 0x84, 0x02, 0x00, 0x24, 0x00, 0xc6, 0x90, 0x21, 0xe0, + 0x5c, 0x08, 0x30, 0x3c, 0x04, 0x00, 0x00, 0x82, 0x04, 0x26, 0x1d, 0x41, 0x00, 0x00, + 0x00, 0x80, 0x0a, 0xc0, 0x20, 0x40, 0x04, 0x20, 0x38, 0x20, 0x02, 0x58, 0x04, 0x00}; + + const io::StreamInputView stream_view{doc_raw, sizeof(doc_raw)}; + + message_20::Variant variant(io::v2gtp::PayloadType::Part20DC, stream_view); + + THEN("It should be deserialized succussfully") { + REQUIRE(variant.get_type() == message_20::Type::DC_ChargeLoopReq); + + const auto& msg = variant.get(); + const auto& header = msg.header; + + REQUIRE(header.session_id == std::array{0x64, 0xEA, 0xED, 0x3D, 0x8E, 0x20, 0xC9, 0x59}); + REQUIRE(header.timestamp == 1727440880); + REQUIRE(msg.meter_info_requested == false); + REQUIRE(message_20::from_RationalNumber(msg.present_voltage) == 400.0f); + + REQUIRE(msg.display_parameters.has_value() == true); + const auto& display_parameters = msg.display_parameters.value(); + + REQUIRE(display_parameters.present_soc.value_or(0) == 10); + REQUIRE(display_parameters.charging_complete.value_or(true) == false); + + using DynamicMode = message_20::DC_ChargeLoopRequest::Dynamic_DC_CLReqControlMode; + + REQUIRE(std::holds_alternative(msg.control_mode)); + const auto& control_mode = std::get(msg.control_mode); + + REQUIRE(message_20::from_RationalNumber(control_mode.target_energy_request) == 60000.0f); + REQUIRE(message_20::from_RationalNumber(control_mode.max_energy_request) == 60000.0f); + REQUIRE(message_20::from_RationalNumber(control_mode.min_energy_request) == 1.0f); + REQUIRE(message_20::from_RationalNumber(control_mode.max_charge_power) == 150000.0f); + REQUIRE(message_20::from_RationalNumber(control_mode.min_charge_power) == 0.0f); + REQUIRE(message_20::from_RationalNumber(control_mode.max_charge_current) == 300.0f); + REQUIRE(message_20::from_RationalNumber(control_mode.max_voltage) == 900.0f); + REQUIRE(message_20::from_RationalNumber(control_mode.min_voltage) == 150.0f); + } + } + GIVEN("Serialize dc_charge_loop ongoing") { message_20::DC_ChargeLoopResponse res; diff --git a/test/exi/cb/iso20/schedule_exchange.cpp b/test/exi/cb/iso20/schedule_exchange.cpp new file mode 100644 index 00000000..677e36d9 --- /dev/null +++ b/test/exi/cb/iso20/schedule_exchange.cpp @@ -0,0 +1,192 @@ +#include + +#include +#include + +#include "helper.hpp" + +using namespace iso15118; + +SCENARIO("Se/Deserialize schedule_exchange messages") { + + GIVEN("Serialize schedule_exchange_req - scheduled mode") { + uint8_t doc_raw[] = {0x80, 0x6c, 0x04, 0x23, 0xfe, 0x9d, 0xa7, 0x89, 0x92, 0xab, 0xe5, 0x0c, 0xee, 0x2c, 0x4b, + 0x70, 0x62, 0x7e, 0x84, 0x28, 0x0e, 0x00, 0x83, 0x00, 0xa0, 0x10, 0x60, 0x28, 0x03, 0xf0, + 0x02, 0x80, 0x00, 0x04, 0x80, 0xe0, 0x41, 0x80, 0x50, 0x40, 0x00, 0x02, 0xa2, 0xaa, 0xa9, + 0x03, 0x27, 0x57, 0x26, 0xe3, 0xa6, 0x97, 0x36, 0xf3, 0xa7, 0x37, 0x46, 0x43, 0xa6, 0x97, + 0x36, 0xf3, 0xa3, 0x13, 0x53, 0x13, 0x13, 0x83, 0xa2, 0xd3, 0x23, 0x03, 0xa5, 0x07, 0x26, + 0x96, 0x36, 0x54, 0x16, 0xc6, 0x76, 0xf7, 0x26, 0x97, 0x46, 0x86, 0xd3, 0xa3, 0x12, 0xd5, + 0x06, 0xf7, 0x76, 0x57, 0x20, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x01, 0x40}; + + const io::StreamInputView stream_view{doc_raw, sizeof(doc_raw)}; + + message_20::Variant variant(io::v2gtp::PayloadType::Part20Main, stream_view); + + THEN("It should be decoded succussfully") { + + REQUIRE(variant.get_type() == message_20::Type::ScheduleExchangeReq); + + const auto& msg = variant.get(); + const auto& header = msg.header; + + REQUIRE(header.session_id == std::array{0x47, 0xFD, 0x3B, 0x4F, 0x13, 0x25, 0x57, 0xCA}); + REQUIRE(header.timestamp == 1727082830); + + REQUIRE(msg.max_supporting_points == 1024); + + REQUIRE(std::holds_alternative( + msg.control_mode)); + auto& control_mode = + std::get(msg.control_mode); + + REQUIRE(control_mode.departure_time.has_value() == true); + REQUIRE(control_mode.departure_time == 7200); + REQUIRE(control_mode.target_energy.has_value() == true); + REQUIRE(message_20::from_RationalNumber(*control_mode.target_energy) == 10000.0f); + REQUIRE(control_mode.max_energy.has_value() == true); + REQUIRE(message_20::from_RationalNumber(*control_mode.max_energy) == 20000.0f); + REQUIRE(control_mode.min_energy.has_value() == true); + REQUIRE(message_20::from_RationalNumber(*control_mode.min_energy) == 0.05f); + + REQUIRE(control_mode.energy_offer.has_value() == true); + auto& ev_energy_offer = control_mode.energy_offer.value(); + REQUIRE(ev_energy_offer.power_schedule.time_anchor == 0); + REQUIRE(ev_energy_offer.power_schedule.entries.size() == 1); + REQUIRE(ev_energy_offer.power_schedule.entries.at(0).duration == 3600); + REQUIRE(message_20::from_RationalNumber(ev_energy_offer.power_schedule.entries.at(0).power) == 10000.0f); + + REQUIRE(ev_energy_offer.absolute_price_schedule.time_anchor == 0); + REQUIRE(ev_energy_offer.absolute_price_schedule.currency == "EUR"); + REQUIRE(ev_energy_offer.absolute_price_schedule.price_algorithm == + "urn:iso:std:iso:15118:-20:PriceAlgorithm:1-Power"); + REQUIRE(ev_energy_offer.absolute_price_schedule.price_rule_stacks.size() == 1); + REQUIRE(ev_energy_offer.absolute_price_schedule.price_rule_stacks.at(0).duration == 0); + REQUIRE(ev_energy_offer.absolute_price_schedule.price_rule_stacks.at(0).price_rules.size() == 1); + REQUIRE(message_20::from_RationalNumber( + ev_energy_offer.absolute_price_schedule.price_rule_stacks.at(0).price_rules.at(0).energy_fee) == + 0); + REQUIRE(message_20::from_RationalNumber(ev_energy_offer.absolute_price_schedule.price_rule_stacks.at(0) + .price_rules.at(0) + .power_range_start) == 0); + } + } + + GIVEN("Serialize schedule_exchange_req - dynamic mode") { + uint8_t doc_raw[] = {0x80, 0x6c, 0x04, 0x1c, 0x90, 0x58, 0x02, 0x37, 0x25, 0x7c, 0x84, 0x8d, 0x6b, 0x0c, + 0x4b, 0x70, 0x62, 0x7e, 0x80, 0xa0, 0x38, 0x03, 0xc1, 0x40, 0x20, 0xc0, 0xa0, 0x10, + 0x60, 0x78, 0x08, 0x31, 0x13, 0x02, 0x0c, 0x01, 0x40, 0x80, 0x00, 0x00}; + + const io::StreamInputView stream_view{doc_raw, sizeof(doc_raw)}; + + message_20::Variant variant(io::v2gtp::PayloadType::Part20Main, stream_view); + + THEN("It should be decoded succussfully") { + + REQUIRE(variant.get_type() == message_20::Type::ScheduleExchangeReq); + + const auto& msg = variant.get(); + const auto& header = msg.header; + + REQUIRE(header.session_id == std::array{0x39, 0x20, 0xB0, 0x04, 0x6E, 0x4A, 0xF9, 0x09}); + REQUIRE(header.timestamp == 1727076438); + + REQUIRE(msg.max_supporting_points == 1024); + + REQUIRE(std::holds_alternative( + msg.control_mode)); + auto& control_mode = + std::get(msg.control_mode); + + REQUIRE(control_mode.departure_time == 7200); + REQUIRE(control_mode.minimum_soc == 30); + REQUIRE(control_mode.target_soc == 80); + REQUIRE(message_20::from_RationalNumber(control_mode.target_energy) == 40000.0f); + REQUIRE(message_20::from_RationalNumber(control_mode.max_energy) == 60000.0f); + REQUIRE(message_20::from_RationalNumber(control_mode.min_energy) == -20000.0f); + REQUIRE(control_mode.max_v2x_energy.has_value() == true); + REQUIRE(message_20::from_RationalNumber(*control_mode.max_v2x_energy) == 5000.0f); + REQUIRE(control_mode.min_v2x_energy.has_value() == true); + REQUIRE(message_20::from_RationalNumber(*control_mode.min_v2x_energy) == 0.0f); + } + } + + GIVEN("Serialize schedule_exchange_res - scheduled mode - no price") { + + message_20::ScheduleExchangeResponse res; + + res.header = message_20::Header{{0x47, 0xFD, 0x3B, 0x4F, 0x13, 0x25, 0x57, 0xCA}, 1727082831}; + res.response_code = message_20::ResponseCode::OK; + res.processing = message_20::Processing::Finished; + auto& control_mode = + res.control_mode.emplace(); + message_20::ScheduleExchangeResponse::ScheduleTuple tuple; + + tuple.schedule_tuple_id = 1; + tuple.charging_schedule.power_schedule.time_anchor = 1727082831; + tuple.charging_schedule.power_schedule.entries.push_back({86400, {2208, 1}, std::nullopt, std::nullopt}); + + control_mode.schedule_tuple.push_back(tuple); + + std::vector expected = {0x80, 0x70, 0x04, 0x23, 0xfe, 0x9d, 0xa7, 0x89, 0x92, 0xab, 0xe5, 0x0c, + 0xfe, 0x2c, 0x4b, 0x70, 0x62, 0x00, 0x04, 0x00, 0x41, 0x9f, 0xc5, 0x89, + 0x6e, 0x0c, 0x84, 0x05, 0x18, 0x28, 0x40, 0x85, 0x00, 0x89, 0x29, 0x40}; + + THEN("It should be serialized succussfully") { + REQUIRE(serialize_helper(res) == expected); + } + } + + GIVEN("Serialize schedule_exchange_res - scheduled mode - price level") { + message_20::ScheduleExchangeResponse res; + + res.header = message_20::Header{{0x47, 0xFD, 0x3B, 0x4F, 0x13, 0x25, 0x57, 0xCA}, 1727082831}; + res.response_code = message_20::ResponseCode::OK; + res.processing = message_20::Processing::Finished; + auto& control_mode = + res.control_mode.emplace(); + message_20::ScheduleExchangeResponse::ScheduleTuple tuple; + + tuple.schedule_tuple_id = 1; + tuple.charging_schedule.power_schedule.time_anchor = 1727082831; + tuple.charging_schedule.power_schedule.entries.push_back({86400, {2208, 1}, std::nullopt, std::nullopt}); + + auto& price_level = + tuple.charging_schedule.price_schedule.emplace(); + price_level.time_anchor = 1727082831; + price_level.price_schedule_id = 1; + price_level.number_of_price_levels = 0; + price_level.price_level_schedule_entries.push_back({23, 8}); + + control_mode.schedule_tuple.push_back(tuple); + + std::vector expected = {0x80, 0x70, 0x04, 0x23, 0xfe, 0x9d, 0xa7, 0x89, 0x92, 0xab, 0xe5, 0x0c, + 0xfe, 0x2c, 0x4b, 0x70, 0x62, 0x00, 0x04, 0x00, 0x41, 0x9f, 0xc5, 0x89, + 0x6e, 0x0c, 0x84, 0x05, 0x18, 0x28, 0x40, 0x85, 0x00, 0x89, 0x25, 0x67, + 0xf1, 0x62, 0x5b, 0x83, 0x00, 0x12, 0x00, 0x00, 0xb8, 0x08, 0x11, 0x40}; + + THEN("It should be serialized succussfully") { + REQUIRE(serialize_helper(res) == expected); + } + } + + GIVEN("Serialize schedule_exchange_res - scheduled mode - absolute price") { + // TODO(sl): Add test + generate exi stream + } + + GIVEN("Serialize schedule_exchange_res - dynamic mode") { + message_20::ScheduleExchangeResponse res; + + res.header = message_20::Header{{0x39, 0x20, 0xB0, 0x04, 0x6E, 0x4A, 0xF9, 0x09}, 1727076439}; + res.response_code = message_20::ResponseCode::OK; + res.processing = message_20::Processing::Finished; + auto& control_mode = res.control_mode.emplace(); + control_mode.departure_time = 2000; + + std::vector expected = {0x80, 0x70, 0x04, 0x1c, 0x90, 0x58, 0x02, 0x37, 0x25, 0x7c, 0x84, + 0x8d, 0x7b, 0x0c, 0x4b, 0x70, 0x62, 0x00, 0x02, 0x1a, 0x01, 0xe8}; + + THEN("It should be serialized succussfully") { + REQUIRE(serialize_helper(res) == expected); + } + } +} diff --git a/test/iso15118/fsm/d20_transitions.cpp b/test/iso15118/fsm/d20_transitions.cpp index 0bdc80b2..ec545903 100644 --- a/test/iso15118/fsm/d20_transitions.cpp +++ b/test/iso15118/fsm/d20_transitions.cpp @@ -18,8 +18,11 @@ SCENARIO("ISO15118-20 state transitions") { const auto cert_install{false}; const std::vector auth_services = {message_20::Authorization::EIM}; const d20::DcTransferLimits dc_limits; + const std::vector control_mobility_modes = { + {message_20::ControlMode::Scheduled, message_20::MobilityNeedsMode::ProvidedByEvcc}}; - const d20::EvseSetupConfig evse_setup{evse_id, supported_energy_services, auth_services, cert_install, dc_limits}; + const d20::EvseSetupConfig evse_setup{evse_id, supported_energy_services, auth_services, cert_install, + dc_limits, control_mobility_modes}; auto state_helper = FsmStateHelper(d20::SessionConfig(evse_setup)); diff --git a/test/iso15118/session/feedback.cpp b/test/iso15118/session/feedback.cpp index 3b1d81ba..c426985d 100644 --- a/test/iso15118/session/feedback.cpp +++ b/test/iso15118/session/feedback.cpp @@ -8,12 +8,12 @@ using namespace iso15118::session; struct FeedbackResults { feedback::Signal signal; - feedback::DcChargeTarget dc_charge_target; + float target_voltage; + feedback::DcChargeLoopReq dc_charge_loop_req; feedback::DcMaximumLimits dc_max_limits; iso15118::message_20::Type v2g_message; std::string evcc_id; std::string selected_protocol; - feedback::DisplayParameters display_parameters; }; SCENARIO("Feedback Tests") { @@ -22,8 +22,11 @@ SCENARIO("Feedback Tests") { feedback::Callbacks callbacks; callbacks.signal = [&feedback_results](feedback::Signal signal_) { feedback_results.signal = signal_; }; - callbacks.dc_charge_target = [&feedback_results](const feedback::DcChargeTarget& dc_charge_target_) { - feedback_results.dc_charge_target = dc_charge_target_; + callbacks.dc_pre_charge_target_voltage = [&feedback_results](float target_voltage_) { + feedback_results.target_voltage = target_voltage_; + }; + callbacks.dc_charge_loop_req = [&feedback_results](const feedback::DcChargeLoopReq& dc_charge_loop_req) { + feedback_results.dc_charge_loop_req = dc_charge_loop_req; }; callbacks.dc_max_limits = [&feedback_results](const feedback::DcMaximumLimits& dc_max_limits_) { feedback_results.dc_max_limits = dc_max_limits_; @@ -35,9 +38,6 @@ SCENARIO("Feedback Tests") { callbacks.selected_protocol = [&feedback_results](const std::string& protocol) { feedback_results.selected_protocol = protocol; }; - callbacks.display_parameters = [&feedback_results](const feedback::DisplayParameters& parameters) { - feedback_results.display_parameters = parameters; - }; const auto feedback = Feedback(callbacks); @@ -50,13 +50,102 @@ SCENARIO("Feedback Tests") { } } - GIVEN("Test dc_charge_target") { - const feedback::DcChargeTarget expected{421.4, 200.4}; - feedback.dc_charge_target({421.4, 200.4}); + GIVEN("Test dc_pre_charge_target_voltage") { + float expected{421.4}; + feedback.dc_pre_charge_target_voltage(421.4); + + THEN("dc_pre_charge_target_voltage should be like expected") { + REQUIRE(feedback_results.target_voltage == expected); + } + } + + GIVEN("Test dc_charge_loop_req - bpt scheduled") { + + using BPT_ScheduleReqControlMode = + iso15118::message_20::DC_ChargeLoopRequest::BPT_Scheduled_DC_CLReqControlMode; + + const BPT_ScheduleReqControlMode expected = {{ + {std::nullopt, std::nullopt, std::nullopt}, + {4402, -1}, + {30, 0}, + std::nullopt, + iso15118::message_20::RationalNumber{34, 0}, + std::nullopt, + std::nullopt, + std::nullopt, + }, + iso15118::message_20::RationalNumber{11, 3}, + iso15118::message_20::RationalNumber{32, 1}, + std::nullopt}; + + feedback.dc_charge_loop_req(expected); + + THEN("dc_charge_loop_req should be like expected") { + + const auto* dc_control_mode = std::get_if(&feedback_results.dc_charge_loop_req); + + const auto* bpt_scheduled_control_mode = std::get_if(dc_control_mode); + + REQUIRE(bpt_scheduled_control_mode->target_energy_request.has_value() == false); + REQUIRE(bpt_scheduled_control_mode->max_energy_request.has_value() == false); + REQUIRE(bpt_scheduled_control_mode->min_energy_request.has_value() == false); + + REQUIRE(from_RationalNumber(bpt_scheduled_control_mode->target_current) == + from_RationalNumber(expected.target_current)); + REQUIRE(from_RationalNumber(bpt_scheduled_control_mode->target_voltage) == + from_RationalNumber(expected.target_voltage)); + REQUIRE(bpt_scheduled_control_mode->max_charge_power.has_value() == false); + REQUIRE(bpt_scheduled_control_mode->min_charge_power.has_value() == true); + REQUIRE(from_RationalNumber(*bpt_scheduled_control_mode->min_charge_power) == + from_RationalNumber(expected.min_charge_power.value_or(iso15118::message_20::RationalNumber{}))); + REQUIRE(bpt_scheduled_control_mode->max_charge_current.has_value() == false); + REQUIRE(bpt_scheduled_control_mode->max_voltage.has_value() == false); + REQUIRE(bpt_scheduled_control_mode->min_voltage.has_value() == false); + + REQUIRE(bpt_scheduled_control_mode->max_discharge_power.has_value() == true); + REQUIRE(from_RationalNumber(*bpt_scheduled_control_mode->max_discharge_power) == + from_RationalNumber(expected.max_discharge_power.value_or(iso15118::message_20::RationalNumber{}))); + REQUIRE(bpt_scheduled_control_mode->min_discharge_power.has_value() == true); + REQUIRE(from_RationalNumber(*bpt_scheduled_control_mode->min_discharge_power) == + from_RationalNumber(expected.min_discharge_power.value_or(iso15118::message_20::RationalNumber{}))); + REQUIRE(bpt_scheduled_control_mode->max_discharge_current.has_value() == false); + } + } + + GIVEN("Test dc_charge_loop_req - dynamic") { + + using DynamicReqControlMode = iso15118::message_20::DC_ChargeLoopRequest::Dynamic_DC_CLReqControlMode; + + const DynamicReqControlMode expected = { + {std::nullopt, {2344, 1}, {30, 3}, {10, 3}}, {22, 3}, {0, 0}, {5, 2}, {9, 2}, {25, 1}}; + + feedback.dc_charge_loop_req(expected); + + THEN("dc_charge_loop_req should be like expected") { - THEN("dc_charge_target should be like expected") { - REQUIRE(feedback_results.dc_charge_target.voltage == expected.voltage); - REQUIRE(feedback_results.dc_charge_target.current == expected.current); + const auto* dc_control_mode = std::get_if(&feedback_results.dc_charge_loop_req); + + const auto* dynamic_control_mode = std::get_if(dc_control_mode); + + REQUIRE(dynamic_control_mode->departure_time.has_value() == false); + + REQUIRE(from_RationalNumber(dynamic_control_mode->target_energy_request) == + from_RationalNumber(expected.target_energy_request)); + REQUIRE(from_RationalNumber(dynamic_control_mode->max_energy_request) == + from_RationalNumber(expected.max_energy_request)); + REQUIRE(from_RationalNumber(dynamic_control_mode->min_energy_request) == + from_RationalNumber(expected.min_energy_request)); + + REQUIRE(from_RationalNumber(dynamic_control_mode->max_charge_power) == + from_RationalNumber(expected.max_charge_power)); + REQUIRE(from_RationalNumber(dynamic_control_mode->min_charge_power) == + from_RationalNumber(expected.min_charge_power)); + REQUIRE(from_RationalNumber(dynamic_control_mode->max_charge_current) == + from_RationalNumber(expected.max_charge_current)); + REQUIRE(from_RationalNumber(dynamic_control_mode->max_voltage) == + from_RationalNumber(expected.max_voltage)); + REQUIRE(from_RationalNumber(dynamic_control_mode->min_voltage) == + from_RationalNumber(expected.min_voltage)); } } @@ -100,17 +189,40 @@ SCENARIO("Feedback Tests") { } GIVEN("Test display_parameters") { - const feedback::DisplayParameters expected{40, std::nullopt, 95, std::nullopt, std::nullopt, - std::nullopt, std::nullopt, std::nullopt, std::nullopt}; - feedback.display_parameters( - {40, std::nullopt, 95, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt, std::nullopt}); + const iso15118::message_20::DisplayParameters expected{40, std::nullopt, 95, std::nullopt, + std::nullopt, std::nullopt, std::nullopt, std::nullopt, + std::nullopt, std::nullopt}; + + feedback.dc_charge_loop_req(expected); THEN("display_parameters should be like expected") { - REQUIRE(feedback_results.display_parameters.present_soc.has_value() == true); - REQUIRE(*feedback_results.display_parameters.present_soc == expected.present_soc.value_or(0)); - REQUIRE(feedback_results.display_parameters.minimum_soc.has_value() == false); - REQUIRE(feedback_results.display_parameters.target_soc.has_value() == true); - REQUIRE(*feedback_results.display_parameters.target_soc == expected.target_soc.value_or(0)); + const auto* display_parameters = + std::get_if(&feedback_results.dc_charge_loop_req); + REQUIRE(display_parameters->present_soc.has_value() == true); + REQUIRE(*display_parameters->present_soc == expected.present_soc.value_or(0)); + REQUIRE(display_parameters->min_soc.has_value() == false); + REQUIRE(display_parameters->target_soc.has_value() == true); + REQUIRE(*display_parameters->target_soc == expected.target_soc.value_or(0)); + } + } + + GIVEN("Test dc_present_voltage") { + feedback::PresentVoltage expected{7044, -1}; + feedback.dc_charge_loop_req(expected); + + THEN("dc_present_voltage should be like expected") { + const auto* present_voltage = std::get_if(&feedback_results.dc_charge_loop_req); + REQUIRE(iso15118::message_20::from_RationalNumber(*present_voltage) == + iso15118::message_20::from_RationalNumber(expected)); + } + } + + GIVEN("Test meter_info_requested") { + feedback::MeterInfoRequested expected{true}; + feedback.dc_charge_loop_req(true); + + THEN("meter_info_requested should be like expected") { + REQUIRE(*std::get_if(&feedback_results.dc_charge_loop_req) == expected); } } } diff --git a/test/iso15118/states/CMakeLists.txt b/test/iso15118/states/CMakeLists.txt index 75771738..3d781041 100644 --- a/test/iso15118/states/CMakeLists.txt +++ b/test/iso15118/states/CMakeLists.txt @@ -1,121 +1,25 @@ include(Catch) -add_executable(test_authorization_setup authorization_setup.cpp) - -target_link_libraries(test_authorization_setup - PRIVATE - iso15118 - Catch2::Catch2WithMain -) - -catch_discover_tests(test_authorization_setup) - -add_executable(test_authorization authorization.cpp) - -target_link_libraries(test_authorization - PRIVATE - iso15118 - Catch2::Catch2WithMain -) - -catch_discover_tests(test_authorization) - -add_executable(test_service_discovery service_discovery.cpp) - -target_link_libraries(test_service_discovery - PRIVATE - iso15118 - Catch2::Catch2WithMain -) - -catch_discover_tests(test_service_discovery) - -add_executable(test_service_detail service_detail.cpp) - -target_link_libraries(test_service_detail - PRIVATE - iso15118 - Catch2::Catch2WithMain -) - -catch_discover_tests(test_service_detail) - -add_executable(test_service_selection service_selection.cpp) - -target_link_libraries(test_service_selection - PRIVATE - iso15118 - Catch2::Catch2WithMain -) - -catch_discover_tests(test_service_selection) - -add_executable(test_dc_charge_parameter_discovery dc_charge_parameter_discovery.cpp) - -target_link_libraries(test_dc_charge_parameter_discovery - PRIVATE - iso15118 - Catch2::Catch2WithMain -) - -catch_discover_tests(test_dc_charge_parameter_discovery) - -add_executable(test_schedule_exchange schedule_exchange.cpp) - -target_link_libraries(test_schedule_exchange - PRIVATE - iso15118 - Catch2::Catch2WithMain -) - -catch_discover_tests(test_schedule_exchange) - -add_executable(test_dc_cable_check dc_cable_check.cpp) - -target_link_libraries(test_dc_cable_check - PRIVATE - iso15118 - Catch2::Catch2WithMain -) - -catch_discover_tests(test_dc_cable_check) - -add_executable(test_dc_pre_charge dc_pre_charge.cpp) - -target_link_libraries(test_dc_pre_charge - PRIVATE - iso15118 - Catch2::Catch2WithMain -) - -catch_discover_tests(test_dc_pre_charge) - -add_executable(test_power_delivery power_delivery.cpp) - -target_link_libraries(test_power_delivery - PRIVATE - iso15118 - Catch2::Catch2WithMain -) - -catch_discover_tests(test_power_delivery) - -add_executable(test_dc_welding_detection dc_welding_detection.cpp) - -target_link_libraries(test_dc_welding_detection - PRIVATE - iso15118 - Catch2::Catch2WithMain -) - -catch_discover_tests(test_dc_welding_detection) - -add_executable(test_session_stop session_stop.cpp) - -target_link_libraries(test_session_stop - PRIVATE - iso15118 - Catch2::Catch2WithMain -) - -catch_discover_tests(test_session_stop) +function(create_states_test_target NAME) + add_executable(test_${NAME} ${NAME}.cpp) + target_link_libraries(test_${NAME} + PRIVATE + iso15118 + Catch2::Catch2WithMain + ) + catch_discover_tests(test_${NAME}) +endfunction() + +create_states_test_target(authorization_setup) +create_states_test_target(authorization) +create_states_test_target(service_discovery) +create_states_test_target(service_detail) +create_states_test_target(service_selection) +create_states_test_target(dc_charge_parameter_discovery) +create_states_test_target(schedule_exchange) +create_states_test_target(dc_cable_check) +create_states_test_target(dc_pre_charge) +create_states_test_target(power_delivery) +create_states_test_target(dc_charge_loop) +create_states_test_target(dc_welding_detection) +create_states_test_target(session_stop) diff --git a/test/iso15118/states/dc_charge_loop.cpp b/test/iso15118/states/dc_charge_loop.cpp new file mode 100644 index 00000000..0c392d22 --- /dev/null +++ b/test/iso15118/states/dc_charge_loop.cpp @@ -0,0 +1,456 @@ +// SPDX-License-Identifier: Apache-2.0 +// Copyright Pionix GmbH and Contributors to EVerest +#include + +#include + +#include + +#include + +using namespace iso15118; + +using Scheduled_DC_Req = message_20::DC_ChargeLoopRequest::Scheduled_DC_CLReqControlMode; +using Scheduled_BPT_DC_Req = message_20::DC_ChargeLoopRequest::BPT_Scheduled_DC_CLReqControlMode; +using Dynamic_DC_Req = message_20::DC_ChargeLoopRequest::Dynamic_DC_CLReqControlMode; +using Dynamic_BPT_DC_Req = message_20::DC_ChargeLoopRequest::BPT_Dynamic_DC_CLReqControlMode; + +using Scheduled_DC_Res = message_20::DC_ChargeLoopResponse::Scheduled_DC_CLResControlMode; +using Scheduled_BPT_DC_Res = message_20::DC_ChargeLoopResponse::BPT_Scheduled_DC_CLResControlMode; +using Dynamic_DC_Res = message_20::DC_ChargeLoopResponse::Dynamic_DC_CLResControlMode; +using Dynamic_BPT_DC_Res = message_20::DC_ChargeLoopResponse::BPT_Dynamic_DC_CLResControlMode; + +SCENARIO("DC charge loop state handling") { + + const auto evse_id = std::string("everest se"); + const std::vector supported_energy_services = {message_20::ServiceCategory::DC, + message_20::ServiceCategory::DC_BPT}; + const auto cert_install{false}; + const std::vector auth_services = {message_20::Authorization::EIM}; + + d20::DcTransferLimits dc_limits; + dc_limits.charge_limits.power.max = {22, 3}; + dc_limits.charge_limits.power.min = {10, 0}; + dc_limits.charge_limits.current.max = {250, 0}; + dc_limits.voltage.max = {900, 0}; + auto& discharge_limits = dc_limits.discharge_limits.emplace<>(); + discharge_limits.power.max = {11, 3}; + discharge_limits.power.min = {10, 0}; + discharge_limits.current.max = {30, 0}; + + const std::vector control_mobility_modes = { + {message_20::ControlMode::Scheduled, message_20::MobilityNeedsMode::ProvidedByEvcc}, + {message_20::ControlMode::Dynamic, message_20::MobilityNeedsMode::ProvidedByEvcc}, + {message_20::ControlMode::Dynamic, message_20::MobilityNeedsMode::ProvidedBySecc}}; + + const d20::EvseSetupConfig evse_setup{evse_id, supported_energy_services, auth_services, cert_install, + dc_limits, control_mobility_modes}; + + GIVEN("Bad case - Unknown session") { + + d20::Session session = d20::Session(); + + message_20::DC_ChargeLoopRequest req; + req.header.session_id = session.get_id(); + req.header.timestamp = 1691411798; + + auto& req_control_mode = req.control_mode.emplace(); + req_control_mode.target_current = {40, 0}; + req_control_mode.target_voltage = {400, 0}; + + req.meter_info_requested = false; + req.present_voltage = {330, 0}; + + const auto res = d20::state::handle_request(req, d20::Session(), 330, 30, false, evse_setup.dc_limits, + d20::UpdateDynamicModeParameters()); + + THEN("ResponseCode: FAILED_UnknownSession, mandatory fields should be set") { + REQUIRE(res.response_code == message_20::ResponseCode::FAILED_UnknownSession); + REQUIRE(message_20::from_RationalNumber(res.present_current) == 0.0f); + REQUIRE(message_20::from_RationalNumber(res.present_voltage) == 0.0f); + REQUIRE(res.current_limit_achieved == false); + REQUIRE(res.power_limit_achieved == false); + REQUIRE(res.voltage_limit_achieved == false); + REQUIRE(std::holds_alternative(res.control_mode)); + } + } + + GIVEN("Bad case - false energy mode") { + + d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters( + message_20::ServiceCategory::DC_BPT, message_20::DcConnector::Extended, message_20::ControlMode::Scheduled, + message_20::MobilityNeedsMode::ProvidedByEvcc, message_20::Pricing::NoPricing, + message_20::BptChannel::Unified, message_20::GeneratorMode::GridFollowing); + + d20::Session session = d20::Session(service_parameters); + + message_20::DC_ChargeLoopRequest req; + req.header.session_id = session.get_id(); + req.header.timestamp = 1691411798; + + auto& req_control_mode = req.control_mode.emplace(); + req_control_mode.target_current = {40, 0}; + req_control_mode.target_voltage = {400, 0}; + + req.meter_info_requested = false; + req.present_voltage = {330, 0}; + + const auto res = d20::state::handle_request(req, session, 330, 30, false, evse_setup.dc_limits, + d20::UpdateDynamicModeParameters()); + + THEN("ResponseCode: FAILED, mandatory fields should be set") { + REQUIRE(res.response_code == message_20::ResponseCode::FAILED); + REQUIRE(message_20::from_RationalNumber(res.present_current) == 0.0f); + REQUIRE(message_20::from_RationalNumber(res.present_voltage) == 0.0f); + REQUIRE(res.current_limit_achieved == false); + REQUIRE(res.power_limit_achieved == false); + REQUIRE(res.voltage_limit_achieved == false); + REQUIRE(std::holds_alternative(res.control_mode)); + } + } + + GIVEN("Bad case - false control mode") { + + d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters( + message_20::ServiceCategory::DC, message_20::DcConnector::Extended, message_20::ControlMode::Dynamic, + message_20::MobilityNeedsMode::ProvidedByEvcc, message_20::Pricing::NoPricing); + + d20::Session session = d20::Session(service_parameters); + + message_20::DC_ChargeLoopRequest req; + req.header.session_id = session.get_id(); + req.header.timestamp = 1691411798; + + auto& req_control_mode = req.control_mode.emplace(); + req_control_mode.target_current = {40, 0}; + req_control_mode.target_voltage = {400, 0}; + + req.meter_info_requested = false; + req.present_voltage = {330, 0}; + + const auto res = d20::state::handle_request(req, session, 330, 30, false, evse_setup.dc_limits, + d20::UpdateDynamicModeParameters()); + + THEN("ResponseCode: FAILED, mandatory fields should be set") { + REQUIRE(res.response_code == message_20::ResponseCode::FAILED); + REQUIRE(message_20::from_RationalNumber(res.present_current) == 0.0f); + REQUIRE(message_20::from_RationalNumber(res.present_voltage) == 0.0f); + REQUIRE(res.current_limit_achieved == false); + REQUIRE(res.power_limit_achieved == false); + REQUIRE(res.voltage_limit_achieved == false); + REQUIRE(std::holds_alternative(res.control_mode)); + } + } + + GIVEN("Good case - DC scheduled mode") { + + d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters( + message_20::ServiceCategory::DC, message_20::DcConnector::Extended, message_20::ControlMode::Scheduled, + message_20::MobilityNeedsMode::ProvidedByEvcc, message_20::Pricing::NoPricing); + + d20::Session session = d20::Session(service_parameters); + + message_20::DC_ChargeLoopRequest req; + req.header.session_id = session.get_id(); + req.header.timestamp = 1691411798; + + auto& req_control_mode = req.control_mode.emplace(); + req_control_mode.target_current = {40, 0}; + req_control_mode.target_voltage = {400, 0}; + + req.meter_info_requested = false; + req.present_voltage = {330, 0}; + + const auto res = d20::state::handle_request(req, session, 330, 30, false, evse_setup.dc_limits, + d20::UpdateDynamicModeParameters()); + + THEN("ResponseCode: OK, mandatory fields should be set") { + REQUIRE(res.response_code == message_20::ResponseCode::OK); + REQUIRE(message_20::from_RationalNumber(res.present_current) == 30.0f); + REQUIRE(message_20::from_RationalNumber(res.present_voltage) == 330.0f); + REQUIRE(res.current_limit_achieved == false); + REQUIRE(res.power_limit_achieved == false); + REQUIRE(res.voltage_limit_achieved == false); + + REQUIRE(std::holds_alternative(res.control_mode)); + const auto& res_control_mode = std::get(res.control_mode); + REQUIRE(message_20::from_RationalNumber( + res_control_mode.max_charge_power.value_or(message_20::RationalNumber{0, 0})) == 22000.0f); + REQUIRE(message_20::from_RationalNumber( + res_control_mode.min_charge_power.value_or(message_20::RationalNumber{0, 0})) == 10.0f); + REQUIRE(message_20::from_RationalNumber( + res_control_mode.max_charge_current.value_or(message_20::RationalNumber{0, 0})) == 250.0f); + REQUIRE(message_20::from_RationalNumber( + res_control_mode.max_voltage.value_or(message_20::RationalNumber{0, 0})) == 900.0f); + } + } + + GIVEN("Good case - DC_BPT scheduled mode") { + + d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters( + message_20::ServiceCategory::DC_BPT, message_20::DcConnector::Extended, message_20::ControlMode::Scheduled, + message_20::MobilityNeedsMode::ProvidedByEvcc, message_20::Pricing::NoPricing); + + d20::Session session = d20::Session(service_parameters); + + message_20::DC_ChargeLoopRequest req; + req.header.session_id = session.get_id(); + req.header.timestamp = 1691411798; + + auto& req_control_mode = req.control_mode.emplace(); + req_control_mode.target_current = {40, 0}; + req_control_mode.target_voltage = {400, 0}; + req_control_mode.max_discharge_power.emplace({11, 3}); + + req.meter_info_requested = false; + req.present_voltage = {330, 0}; + + const auto res = d20::state::handle_request(req, session, 330, 30, false, evse_setup.dc_limits, + d20::UpdateDynamicModeParameters()); + + THEN("ResponseCode: OK, mandatory fields should be set") { + REQUIRE(res.response_code == message_20::ResponseCode::OK); + REQUIRE(message_20::from_RationalNumber(res.present_current) == 30.0f); + REQUIRE(message_20::from_RationalNumber(res.present_voltage) == 330.0f); + REQUIRE(res.current_limit_achieved == false); + REQUIRE(res.power_limit_achieved == false); + REQUIRE(res.voltage_limit_achieved == false); + + REQUIRE(std::holds_alternative(res.control_mode)); + const auto& res_control_mode = std::get(res.control_mode); + REQUIRE(message_20::from_RationalNumber( + res_control_mode.max_charge_power.value_or(message_20::RationalNumber{0, 0})) == 22000.0f); + REQUIRE(message_20::from_RationalNumber( + res_control_mode.min_charge_power.value_or(message_20::RationalNumber{0, 0})) == 10.0f); + REQUIRE(message_20::from_RationalNumber( + res_control_mode.max_charge_current.value_or(message_20::RationalNumber{0, 0})) == 250.0f); + REQUIRE(message_20::from_RationalNumber( + res_control_mode.max_voltage.value_or(message_20::RationalNumber{0, 0})) == 900.0f); + REQUIRE(message_20::from_RationalNumber( + res_control_mode.max_discharge_power.value_or(message_20::RationalNumber{0, 0})) == 11000.0f); + REQUIRE(message_20::from_RationalNumber( + res_control_mode.min_discharge_power.value_or(message_20::RationalNumber{0, 0})) == 10.0f); + REQUIRE(message_20::from_RationalNumber( + res_control_mode.max_discharge_current.value_or(message_20::RationalNumber{0, 0})) == 30.0f); + } + } + + GIVEN("Good case - DC dynamic mode") { + d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters( + message_20::ServiceCategory::DC, message_20::DcConnector::Extended, message_20::ControlMode::Dynamic, + message_20::MobilityNeedsMode::ProvidedByEvcc, message_20::Pricing::NoPricing); + + d20::Session session = d20::Session(service_parameters); + + message_20::DC_ChargeLoopRequest req; + req.header.session_id = session.get_id(); + req.header.timestamp = 1691411798; + + auto& req_control_mode = req.control_mode.emplace(); + req_control_mode.target_energy_request = {68, 3}; + req_control_mode.max_energy_request = {70, 3}; + req_control_mode.min_energy_request = {40, 3}; + req_control_mode.max_charge_power = {30, 3}; + req_control_mode.min_charge_power = {27, 2}; + req_control_mode.max_charge_current = {400, 0}; + req_control_mode.max_voltage = {950, 0}; + req_control_mode.min_voltage = {150, 0}; + + req.meter_info_requested = false; + req.present_voltage = {330, 0}; + + const auto res = d20::state::handle_request(req, session, 330, 30, false, evse_setup.dc_limits, + d20::UpdateDynamicModeParameters()); + + THEN("ResponseCode: OK, mandatory fields should be set") { + REQUIRE(res.response_code == message_20::ResponseCode::OK); + REQUIRE(message_20::from_RationalNumber(res.present_current) == 30.0f); + REQUIRE(message_20::from_RationalNumber(res.present_voltage) == 330.0f); + REQUIRE(res.current_limit_achieved == false); + REQUIRE(res.power_limit_achieved == false); + REQUIRE(res.voltage_limit_achieved == false); + + REQUIRE(std::holds_alternative(res.control_mode)); + const auto& res_control_mode = std::get(res.control_mode); + REQUIRE(message_20::from_RationalNumber(res_control_mode.max_charge_power) == 22000.0f); + REQUIRE(message_20::from_RationalNumber(res_control_mode.min_charge_power) == 10.0f); + REQUIRE(message_20::from_RationalNumber(res_control_mode.max_charge_current) == 250.0f); + REQUIRE(message_20::from_RationalNumber(res_control_mode.max_voltage) == 900.0f); + } + } + + GIVEN("Good case - DC_BPT dynamic mode") { + + d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters( + message_20::ServiceCategory::DC_BPT, message_20::DcConnector::Extended, message_20::ControlMode::Dynamic, + message_20::MobilityNeedsMode::ProvidedByEvcc, message_20::Pricing::NoPricing); + + d20::Session session = d20::Session(service_parameters); + + message_20::DC_ChargeLoopRequest req; + req.header.session_id = session.get_id(); + req.header.timestamp = 1691411798; + + auto& req_control_mode = req.control_mode.emplace(); + req_control_mode.target_energy_request = {68, 3}; + req_control_mode.max_energy_request = {70, 3}; + req_control_mode.min_energy_request = {40, 3}; + req_control_mode.max_charge_power = {30, 3}; + req_control_mode.min_charge_power = {27, 2}; + req_control_mode.max_charge_current = {400, 0}; + req_control_mode.max_voltage = {950, 0}; + req_control_mode.min_voltage = {150, 0}; + req_control_mode.max_discharge_power = {11, 3}; + req_control_mode.min_discharge_power = {10, 0}; + req_control_mode.max_discharge_current = {30, 0}; + + req.meter_info_requested = false; + req.present_voltage = {330, 0}; + + const auto res = d20::state::handle_request(req, session, 330, 30, false, evse_setup.dc_limits, + d20::UpdateDynamicModeParameters()); + + THEN("ResponseCode: OK, mandatory fields should be set") { + REQUIRE(res.response_code == message_20::ResponseCode::OK); + REQUIRE(message_20::from_RationalNumber(res.present_current) == 30.0f); + REQUIRE(message_20::from_RationalNumber(res.present_voltage) == 330.0f); + REQUIRE(res.current_limit_achieved == false); + REQUIRE(res.power_limit_achieved == false); + REQUIRE(res.voltage_limit_achieved == false); + + REQUIRE(std::holds_alternative(res.control_mode)); + const auto& res_control_mode = std::get(res.control_mode); + REQUIRE(message_20::from_RationalNumber(res_control_mode.max_charge_power) == 22000.0f); + REQUIRE(message_20::from_RationalNumber(res_control_mode.min_charge_power) == 10.0f); + REQUIRE(message_20::from_RationalNumber(res_control_mode.max_charge_current) == 250.0f); + REQUIRE(message_20::from_RationalNumber(res_control_mode.max_voltage) == 900.0f); + REQUIRE(message_20::from_RationalNumber(res_control_mode.max_discharge_power) == 11000.0f); + REQUIRE(message_20::from_RationalNumber(res_control_mode.min_discharge_power) == 10.0f); + REQUIRE(message_20::from_RationalNumber(res_control_mode.max_discharge_current) == 30.0f); + } + } + + GIVEN("Good case - DC dynamic mode, mobility_needs_mode = 2") { + d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters( + message_20::ServiceCategory::DC, message_20::DcConnector::Extended, message_20::ControlMode::Dynamic, + message_20::MobilityNeedsMode::ProvidedBySecc, message_20::Pricing::NoPricing); + + d20::Session session = d20::Session(service_parameters); + + message_20::DC_ChargeLoopRequest req; + req.header.session_id = session.get_id(); + req.header.timestamp = 1691411798; + + auto& req_control_mode = req.control_mode.emplace(); + req_control_mode.target_energy_request = {68, 3}; + req_control_mode.max_energy_request = {70, 3}; + req_control_mode.min_energy_request = {40, 3}; + req_control_mode.max_charge_power = {30, 3}; + req_control_mode.min_charge_power = {27, 2}; + req_control_mode.max_charge_current = {400, 0}; + req_control_mode.max_voltage = {950, 0}; + req_control_mode.min_voltage = {150, 0}; + + req.meter_info_requested = false; + req.present_voltage = {330, 0}; + + const d20::UpdateDynamicModeParameters dynamic_parameters = {std::time(nullptr) + 60, 95, std::nullopt}; + + const auto res = + d20::state::handle_request(req, session, 330, 30, false, evse_setup.dc_limits, dynamic_parameters); + + THEN("ResponseCode: OK, mandatory fields should be set") { + REQUIRE(res.response_code == message_20::ResponseCode::OK); + REQUIRE(message_20::from_RationalNumber(res.present_current) == 30.0f); + REQUIRE(message_20::from_RationalNumber(res.present_voltage) == 330.0f); + REQUIRE(res.current_limit_achieved == false); + REQUIRE(res.power_limit_achieved == false); + REQUIRE(res.voltage_limit_achieved == false); + + REQUIRE(std::holds_alternative(res.control_mode)); + const auto& res_control_mode = std::get(res.control_mode); + REQUIRE(message_20::from_RationalNumber(res_control_mode.max_charge_power) == 22000.0f); + REQUIRE(message_20::from_RationalNumber(res_control_mode.min_charge_power) == 10.0f); + REQUIRE(message_20::from_RationalNumber(res_control_mode.max_charge_current) == 250.0f); + REQUIRE(message_20::from_RationalNumber(res_control_mode.max_voltage) == 900.0f); + + REQUIRE(res_control_mode.departure_time.value_or(0) >= 59); + REQUIRE(res_control_mode.target_soc.value_or(0) == 95); + REQUIRE(res_control_mode.ack_max_delay.value_or(0) == 30); + } + } + + GIVEN("Good case - DC_BPT dynamic mode, mobility_needs_mode = 2") { + d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters( + message_20::ServiceCategory::DC_BPT, message_20::DcConnector::Extended, message_20::ControlMode::Dynamic, + message_20::MobilityNeedsMode::ProvidedBySecc, message_20::Pricing::NoPricing); + + d20::Session session = d20::Session(service_parameters); + + message_20::DC_ChargeLoopRequest req; + req.header.session_id = session.get_id(); + req.header.timestamp = 1691411798; + + auto& req_control_mode = req.control_mode.emplace(); + req_control_mode.target_energy_request = {68, 3}; + req_control_mode.max_energy_request = {70, 3}; + req_control_mode.min_energy_request = {40, 3}; + req_control_mode.max_charge_power = {30, 3}; + req_control_mode.min_charge_power = {27, 2}; + req_control_mode.max_charge_current = {400, 0}; + req_control_mode.max_voltage = {950, 0}; + req_control_mode.min_voltage = {150, 0}; + req_control_mode.max_discharge_power = {11, 3}; + req_control_mode.min_discharge_power = {10, 0}; + req_control_mode.max_discharge_current = {30, 0}; + + req.meter_info_requested = false; + req.present_voltage = {330, 0}; + + const d20::UpdateDynamicModeParameters dynamic_parameters = {std::time(nullptr) + 40, std::nullopt, 95}; + + const auto res = + d20::state::handle_request(req, session, 330, 30, false, evse_setup.dc_limits, dynamic_parameters); + + THEN("ResponseCode: OK, mandatory fields should be set") { + REQUIRE(res.response_code == message_20::ResponseCode::OK); + REQUIRE(message_20::from_RationalNumber(res.present_current) == 30.0f); + REQUIRE(message_20::from_RationalNumber(res.present_voltage) == 330.0f); + REQUIRE(res.current_limit_achieved == false); + REQUIRE(res.power_limit_achieved == false); + REQUIRE(res.voltage_limit_achieved == false); + + REQUIRE(std::holds_alternative(res.control_mode)); + const auto& res_control_mode = std::get(res.control_mode); + REQUIRE(message_20::from_RationalNumber(res_control_mode.max_charge_power) == 22000.0f); + REQUIRE(message_20::from_RationalNumber(res_control_mode.min_charge_power) == 10.0f); + REQUIRE(message_20::from_RationalNumber(res_control_mode.max_charge_current) == 250.0f); + REQUIRE(message_20::from_RationalNumber(res_control_mode.max_voltage) == 900.0f); + REQUIRE(message_20::from_RationalNumber(res_control_mode.max_discharge_power) == 11000.0f); + REQUIRE(message_20::from_RationalNumber(res_control_mode.min_discharge_power) == 10.0f); + REQUIRE(message_20::from_RationalNumber(res_control_mode.max_discharge_current) == 30.0f); + + REQUIRE(res_control_mode.departure_time.value_or(0) >= 39); + REQUIRE(res_control_mode.minimum_soc.value_or(0) == 95); + REQUIRE(res_control_mode.ack_max_delay.value_or(0) == 30); + } + } + + // Note(sl): Only in scheduled mode and if a powertolerance was sent from the secc + // TODO(sl): Adding test + // GIVEN("Warning case - Warning_EVPowerProfileViolation [V2G20-1864]") {} + // GIVEN("Bad case - Failed_EVPowerProfileViolation [V2G20-1864]") {} + + // TODO(sl): Adding test if ScheduleRenegotion is supported + // GIVEN("Warning case - Warning_ScheduleRenegotiationFailed") {} + // GIVEN("Bad case - Failed_ScheduleRenegotiationFailed") {} + + // Note(sl): Not yet + // GIVEN("Bad case - Failed_AssociationError (ACDP only)") {} + + // GIVEN("Bad Case - sequence error") {} // todo(sl): not here + + // GIVEN("Bad Case - Performance Timeout") {} // todo(sl): not here + + // GIVEN("Bad Case - Sequence Timeout") {} // todo(sl): not here +} \ No newline at end of file diff --git a/test/iso15118/states/dc_charge_parameter_discovery.cpp b/test/iso15118/states/dc_charge_parameter_discovery.cpp index 17043f7e..9fcf4782 100644 --- a/test/iso15118/states/dc_charge_parameter_discovery.cpp +++ b/test/iso15118/states/dc_charge_parameter_discovery.cpp @@ -19,8 +19,11 @@ SCENARIO("DC charge parameter discovery state handling") { const auto cert_install{false}; const std::vector auth_services = {message_20::Authorization::EIM}; const d20::DcTransferLimits dc_limits; + const std::vector control_mobility_modes = { + {message_20::ControlMode::Scheduled, message_20::MobilityNeedsMode::ProvidedByEvcc}}; - const d20::EvseSetupConfig evse_setup{evse_id, supported_energy_services, auth_services, cert_install, dc_limits}; + const d20::EvseSetupConfig evse_setup{evse_id, supported_energy_services, auth_services, cert_install, + dc_limits, control_mobility_modes}; GIVEN("Bad Case - Unknown session") { diff --git a/test/iso15118/states/dc_pre_charge.cpp b/test/iso15118/states/dc_pre_charge.cpp index 8ae5a71a..68e8c527 100644 --- a/test/iso15118/states/dc_pre_charge.cpp +++ b/test/iso15118/states/dc_pre_charge.cpp @@ -22,7 +22,7 @@ SCENARIO("DC Pre charge state handling") { const float present_voltage = 0.1; - const auto [res, charge_target] = d20::state::handle_request(req, d20::Session(), present_voltage); + const auto res = d20::state::handle_request(req, d20::Session(), present_voltage); THEN("ResponseCode: FAILED_UnknownSession, mandatory fields should be set") { REQUIRE(res.response_code == message_20::ResponseCode::FAILED_UnknownSession); @@ -44,7 +44,7 @@ SCENARIO("DC Pre charge state handling") { const float present_voltage = 400.1; - const auto [res, charge_target] = d20::state::handle_request(req, session, present_voltage); + const auto res = d20::state::handle_request(req, session, present_voltage); THEN("ResponseCode: OK, present_voltage should be 400.1V") { REQUIRE(res.response_code == message_20::ResponseCode::OK); diff --git a/test/iso15118/states/schedule_exchange.cpp b/test/iso15118/states/schedule_exchange.cpp index f75ac900..3cc42b24 100644 --- a/test/iso15118/states/schedule_exchange.cpp +++ b/test/iso15118/states/schedule_exchange.cpp @@ -25,7 +25,7 @@ SCENARIO("Schedule Exchange state handling") { message_20::RationalNumber max_power = {0, 0}; - const auto res = d20::state::handle_request(req, d20::Session(), max_power); + const auto res = d20::state::handle_request(req, d20::Session(), max_power, d20::UpdateDynamicModeParameters()); THEN("ResponseCode: FAILED_UnknownSession, mandatory fields should be set") { REQUIRE(res.response_code == message_20::ResponseCode::FAILED_UnknownSession); @@ -53,7 +53,7 @@ SCENARIO("Schedule Exchange state handling") { message_20::RationalNumber max_power = {0, 0}; - const auto res = d20::state::handle_request(req, session, max_power); + const auto res = d20::state::handle_request(req, session, max_power, d20::UpdateDynamicModeParameters()); THEN("ResponseCode: FAILED, mandatory fields should be set") { REQUIRE(res.response_code == message_20::ResponseCode::FAILED); @@ -81,7 +81,7 @@ SCENARIO("Schedule Exchange state handling") { message_20::RationalNumber max_power = {22, 3}; - const auto res = d20::state::handle_request(req, session, max_power); + const auto res = d20::state::handle_request(req, session, max_power, d20::UpdateDynamicModeParameters()); THEN("ResponseCode: OK") { REQUIRE(res.response_code == message_20::ResponseCode::OK); @@ -94,12 +94,37 @@ SCENARIO("Schedule Exchange state handling") { REQUIRE(res_control_mode.schedule_tuple.size() == 1); const auto& schedule_tuple = res_control_mode.schedule_tuple[0]; - REQUIRE(schedule_tuple.charging_schedule.power_schedule.entries[0].power.value == 22); - REQUIRE(schedule_tuple.charging_schedule.power_schedule.entries[0].power.exponent == 3); + REQUIRE(message_20::from_RationalNumber( + schedule_tuple.charging_schedule.power_schedule.entries.at(0).power) == 22000); } } - // GIVEN("Good case - Dynamic Mode") {} // todo(sl): dynamic mode is missing + GIVEN("Good case - Dynamic Mode") { + d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters( + message_20::ServiceCategory::DC, message_20::DcConnector::Extended, message_20::ControlMode::Dynamic, + message_20::MobilityNeedsMode::ProvidedByEvcc, message_20::Pricing::NoPricing); + + auto session = d20::Session(service_parameters); + + message_20::ScheduleExchangeRequest req; + req.header.session_id = session.get_id(); + req.header.timestamp = 1691411798; + + req.control_mode.emplace(); + + message_20::RationalNumber max_power = {22, 3}; + + const auto res = d20::state::handle_request(req, session, max_power, d20::UpdateDynamicModeParameters()); + + THEN("ResponseCode: OK") { + REQUIRE(res.response_code == message_20::ResponseCode::OK); + REQUIRE(res.processing == message_20::Processing::Finished); + + REQUIRE(std::holds_alternative(res.control_mode) == true); + } + } + + // GIVEN("Good case - setting new departure_time, target_soc & min_soc") // TODO(sl) // GIVEN("Bad Case - sequence error") {} // todo(sl): not here diff --git a/test/iso15118/states/service_detail.cpp b/test/iso15118/states/service_detail.cpp index c26a9ab3..3c22190c 100644 --- a/test/iso15118/states/service_detail.cpp +++ b/test/iso15118/states/service_detail.cpp @@ -13,8 +13,11 @@ SCENARIO("Service detail state handling") { const auto cert_install{false}; const std::vector auth_services = {message_20::Authorization::EIM}; const d20::DcTransferLimits dc_limits; + const std::vector control_mobility_modes = { + {message_20::ControlMode::Scheduled, message_20::MobilityNeedsMode::ProvidedByEvcc}}; - const d20::EvseSetupConfig evse_setup{evse_id, supported_energy_services, auth_services, cert_install, dc_limits}; + const d20::EvseSetupConfig evse_setup{evse_id, supported_energy_services, auth_services, cert_install, + dc_limits, control_mobility_modes}; GIVEN("Bad Case - Unknown session") { @@ -79,13 +82,7 @@ SCENARIO("Service detail state handling") { d20::Session session = d20::Session(); session.offered_services.energy_services = {message_20::ServiceCategory::DC}; - auto session_config = d20::SessionConfig(evse_setup); - session_config.dc_parameter_list = {{ - message_20::DcConnector::Extended, - message_20::ControlMode::Scheduled, - message_20::MobilityNeedsMode::ProvidedByEvcc, - message_20::Pricing::NoPricing, - }}; + const auto session_config = d20::SessionConfig(evse_setup); message_20::ServiceDetailRequest req; req.header.session_id = session.get_id(); @@ -125,17 +122,7 @@ SCENARIO("Service detail state handling") { d20::Session session = d20::Session(); session.offered_services.energy_services = {message_20::ServiceCategory::DC_BPT}; - auto session_config = d20::SessionConfig(evse_setup); - session_config.dc_bpt_parameter_list = {{ - { - message_20::DcConnector::Extended, - message_20::ControlMode::Scheduled, - message_20::MobilityNeedsMode::ProvidedByEvcc, - message_20::Pricing::NoPricing, - }, - message_20::BptChannel::Unified, - message_20::GeneratorMode::GridFollowing, - }}; + const auto session_config = d20::SessionConfig(evse_setup); message_20::ServiceDetailRequest req; req.header.session_id = session.get_id(); @@ -184,6 +171,7 @@ SCENARIO("Service detail state handling") { d20::Session session = d20::Session(); session.offered_services.energy_services = {message_20::ServiceCategory::DC}; + // FIXME(SL): Change evse_setup instead of session_config auto session_config = d20::SessionConfig(evse_setup); session_config.dc_parameter_list = {{ message_20::DcConnector::Extended, @@ -258,13 +246,7 @@ SCENARIO("Service detail state handling") { d20::Session session = d20::Session(); session.offered_services.energy_services = {message_20::ServiceCategory::DC}; - auto session_config = d20::SessionConfig(evse_setup); - session_config.dc_parameter_list = {{ - message_20::DcConnector::Extended, - message_20::ControlMode::Scheduled, - message_20::MobilityNeedsMode::ProvidedBySecc, - message_20::Pricing::NoPricing, - }}; + const auto session_config = d20::SessionConfig(evse_setup); message_20::ServiceDetailRequest req; req.header.session_id = session.get_id(); @@ -307,12 +289,6 @@ SCENARIO("Service detail state handling") { auto session_config = d20::SessionConfig(evse_setup); session_config.internet_parameter_list = {{message_20::Protocol::Http, message_20::Port::Port80}}; - session_config.dc_parameter_list = {{ - message_20::DcConnector::Extended, - message_20::ControlMode::Scheduled, - message_20::MobilityNeedsMode::ProvidedByEvcc, - message_20::Pricing::NoPricing, - }}; message_20::ServiceDetailRequest req; req.header.session_id = session.get_id(); @@ -348,12 +324,6 @@ SCENARIO("Service detail state handling") { auto session_config = d20::SessionConfig(evse_setup); session_config.parking_parameter_list = { {message_20::IntendedService::VehicleCheckIn, message_20::ParkingStatus::ManualExternal}}; - session_config.dc_parameter_list = {{ - message_20::DcConnector::Extended, - message_20::ControlMode::Scheduled, - message_20::MobilityNeedsMode::ProvidedByEvcc, - message_20::Pricing::NoPricing, - }}; message_20::ServiceDetailRequest req; req.header.session_id = session.get_id(); diff --git a/test/iso15118/states/service_selection.cpp b/test/iso15118/states/service_selection.cpp index 9b0f701a..69360e18 100644 --- a/test/iso15118/states/service_selection.cpp +++ b/test/iso15118/states/service_selection.cpp @@ -121,8 +121,10 @@ SCENARIO("Service selection state handling") { THEN("ResponseCode: OK") { REQUIRE(res.response_code == message_20::ResponseCode::OK); - REQUIRE(session.get_selected_energy_service() == message_20::ServiceCategory::DC); - REQUIRE(session.get_selected_control_mode() == message_20::ControlMode::Scheduled); + + const auto selected_services = session.get_selected_services(); + REQUIRE(selected_services.selected_energy_service == message_20::ServiceCategory::DC); + REQUIRE(selected_services.selected_control_mode == message_20::ControlMode::Scheduled); } } @@ -149,8 +151,10 @@ SCENARIO("Service selection state handling") { THEN("ResponseCode: OK") { REQUIRE(res.response_code == message_20::ResponseCode::OK); - REQUIRE(session.get_selected_energy_service() == message_20::ServiceCategory::DC_BPT); - REQUIRE(session.get_selected_control_mode() == message_20::ControlMode::Scheduled); + + const auto selected_services = session.get_selected_services(); + REQUIRE(selected_services.selected_energy_service == message_20::ServiceCategory::DC_BPT); + REQUIRE(selected_services.selected_control_mode == message_20::ControlMode::Scheduled); } }