Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter

- CitrineOS core extracted (CSMS OCPP 2.0.1)
- OpenOCPP extracted (firmware OCPP 1.6J/2.0.1)
- ShapeShifter library installed (pip install -e)
- ShapeShifter specification extracted
- EVerest extracted

TODO updated with progress
This commit is contained in:
Eric F
2026-06-08 00:38:27 -04:00
parent 468cfeaa50
commit d398a6ced2
7326 changed files with 1177561 additions and 7 deletions

View File

@@ -0,0 +1,24 @@
add_compile_options(${ISO15118_COMPILE_OPTIONS_WARNING})
list(APPEND CMAKE_MODULE_PATH ${CPM_PACKAGE_catch2_SOURCE_DIR}/extras)
add_subdirectory(exi)
add_subdirectory(iso15118)
add_subdirectory(v2gtp)
#
# code coverage specifics
#
evc_include(CodeCoverage)
append_coverage_compiler_flags_to_target(iso15118)
setup_target_for_coverage_gcovr_html(
NAME ${PROJECT_NAME}_gcovr_coverage
EXECUTABLE ctest
)
setup_target_for_coverage_gcovr_xml(
NAME ${PROJECT_NAME}_gcovr_coverage_xml
EXECUTABLE ctest
)

View File

@@ -0,0 +1 @@
add_subdirectory(cb)

View File

@@ -0,0 +1,2 @@
add_subdirectory(app_hand)
add_subdirectory(iso20)

View File

@@ -0,0 +1,10 @@
add_executable(test_app_hand app_hand.cpp)
target_link_libraries(test_app_hand
PRIVATE
iso15118
Catch2::Catch2WithMain
)
include(Catch)
catch_discover_tests(test_app_hand)

View File

@@ -0,0 +1,43 @@
#include <catch2/catch_test_macros.hpp>
#include <iso15118/message/supported_app_protocol.hpp>
#include <iso15118/message/variant.hpp>
#include <string>
using namespace iso15118;
SCENARIO("App Protocol Ser/Des") {
GIVEN("A binary representation of an AppProtocolReq document") {
uint8_t doc_raw[] = "\x80"
"\x00\xf3\xab\x93\x71\xd3\x4b\x9b\x79\xd3\x9b\xa3\x21\xd3\x4b\x9b\x79"
"\xd1\x89\xa9\x89\x89\xc1\xd1\x69\x91\x81\xd2\x0a\x18\x01\x00\x00\x04"
"\x00\x40";
const io::StreamInputView stream_view{doc_raw, sizeof(doc_raw)};
message_20::Variant variant(io::v2gtp::PayloadType::SAP, stream_view);
THEN("It should be decoded succussfully") {
REQUIRE(variant.get_type() == message_20::Type::SupportedAppProtocolReq);
const auto& msg = variant.get<message_20::SupportedAppProtocolRequest>();
REQUIRE(msg.app_protocol.size() == 1);
const auto& ap = msg.app_protocol[0];
REQUIRE(ap.version_number_major == 1);
REQUIRE(ap.version_number_minor == 0);
REQUIRE(ap.schema_id == 1);
REQUIRE(ap.priority == 1);
REQUIRE(ap.protocol_namespace == "urn:iso:std:iso:15118:-20:AC");
}
}
// Todo(sl): Missing Decode message
// 80400040
// {"supportedAppProtocolRes": {"ResponseCode": "OK_SuccessfulNegotiation", "SchemaID": 1}}
}

View File

@@ -0,0 +1,29 @@
include(Catch)
function(create_exi_test_target NAME)
add_executable(test_exi_${NAME} ${NAME}.cpp)
target_link_libraries(test_exi_${NAME}
PRIVATE
iso15118
Catch2::Catch2WithMain
)
catch_discover_tests(test_exi_${NAME})
endfunction()
create_exi_test_target(session_setup)
create_exi_test_target(authorization_setup)
create_exi_test_target(authorization)
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(dc_cable_check)
create_exi_test_target(dc_pre_charge)
create_exi_test_target(power_delivery)
create_exi_test_target(dc_charge_loop)
create_exi_test_target(dc_welding_detection)
create_exi_test_target(session_stop)
create_exi_test_target(ac_charge_parameter_discovery)
create_exi_test_target(ac_charge_loop)

View File

@@ -0,0 +1,293 @@
#include <catch2/catch_test_macros.hpp>
#include <iso15118/message/ac_charge_loop.hpp>
#include <iso15118/message/variant.hpp>
#include "helper.hpp"
using namespace iso15118;
namespace dt = iso15118::message_20::datatypes;
SCENARIO("Se/Deserialize ac charge loop messages") {
GIVEN("Deserialize ac_charge_loop_req_scheduled mode") {
uint8_t doc_raw[] = {0x80, 0x08, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c, 0xdb, 0xfe, 0x1b,
0x60, 0x62, 0x88, 0x04, 0x00, 0x5c, 0xb0, 0x00, 0x40, 0x07, 0x02, 0xe8, 0x04, 0x00, 0x00,
0x80, 0x7e, 0x08, 0x01, 0x90, 0x10, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00, 0x3f, 0x06, 0x80,
0x78, 0x10, 0x00, 0x00, 0x04, 0x00, 0x00, 0x02, 0x00, 0x38, 0x17, 0x40, 0x40, 0x00, 0x00,
0x08, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00};
const io::StreamInputView stream_view{doc_raw, sizeof(doc_raw)};
message_20::Variant variant(io::v2gtp::PayloadType::Part20AC, stream_view);
THEN("It should be deserialized successfully") {
REQUIRE(variant.get_type() == message_20::Type::AC_ChargeLoopReq);
const auto& msg = variant.get<message_20::AC_ChargeLoopRequest>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456333);
REQUIRE(msg.meter_info_requested == false);
using ScheduledMode = message_20::datatypes::Scheduled_AC_CLReqControlMode;
REQUIRE(std::holds_alternative<ScheduledMode>(msg.control_mode));
const auto& control_mode = std::get<ScheduledMode>(msg.control_mode);
REQUIRE(control_mode.target_energy_request.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.target_energy_request) == 12345.0f);
REQUIRE(control_mode.max_energy_request.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.max_energy_request) == 12000.0f);
REQUIRE(control_mode.min_energy_request.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.min_energy_request) == 1.0f);
REQUIRE(dt::from_RationalNumber(control_mode.present_active_power) == 12000.0f);
REQUIRE(control_mode.present_active_power_L2.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.present_active_power_L2) == 0.0f);
REQUIRE(control_mode.present_active_power_L3.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.present_active_power_L3) == 0.0f);
REQUIRE(control_mode.present_reactive_power.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.present_reactive_power) == 0.0f);
REQUIRE(control_mode.present_reactive_power_L2.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.present_reactive_power_L2) == 0.0f);
REQUIRE(control_mode.present_reactive_power_L3.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.present_reactive_power_L3) == 0.0f);
REQUIRE(control_mode.max_charge_power.has_value() == true);
REQUIRE(message_20::datatypes::from_RationalNumber(*control_mode.max_charge_power) == 32);
REQUIRE(control_mode.max_charge_power_L2.has_value() == true);
REQUIRE(message_20::datatypes::from_RationalNumber(*control_mode.max_charge_power_L2) == 0.0f);
REQUIRE(control_mode.max_charge_power_L3.has_value() == true);
REQUIRE(message_20::datatypes::from_RationalNumber(*control_mode.max_charge_power_L3) == 0.0f);
REQUIRE(control_mode.min_charge_power.has_value() == true);
REQUIRE(message_20::datatypes::from_RationalNumber(*control_mode.min_charge_power) == 20);
REQUIRE(control_mode.min_charge_power_L2.has_value() == true);
REQUIRE(message_20::datatypes::from_RationalNumber(*control_mode.min_charge_power_L2) == 0.0f);
REQUIRE(control_mode.min_charge_power_L3.has_value() == true);
REQUIRE(message_20::datatypes::from_RationalNumber(*control_mode.min_charge_power_L3) == 0.0f);
}
}
GIVEN("Serialize ac_charge_loop request_scheduled mode") {
message_20::AC_ChargeLoopRequest req;
req.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456333};
req.meter_info_requested = false;
auto& control_mode = req.control_mode.emplace<message_20::datatypes::Scheduled_AC_CLReqControlMode>();
control_mode.target_energy_request = {12345, 0};
control_mode.max_energy_request = {12000, 0};
control_mode.min_energy_request = {1, 0};
control_mode.max_charge_power = {3200, -2};
control_mode.max_charge_power_L2 = {0, 0};
control_mode.max_charge_power_L3 = {0, 0};
control_mode.min_charge_power = {2000, -2};
control_mode.min_charge_power_L2 = {0, 0};
control_mode.min_charge_power_L3 = {0, 0};
control_mode.present_active_power = {12000, 0};
control_mode.present_active_power_L2 = {0, 0};
control_mode.present_active_power_L3 = {0, 0};
control_mode.present_reactive_power = {0, 0};
control_mode.present_reactive_power_L2 = {0, 0};
control_mode.present_reactive_power_L3 = {0, 0};
std::vector<uint8_t> expected = {0x80, 0x08, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c, 0xdb,
0xfe, 0x1b, 0x60, 0x62, 0x88, 0x04, 0x00, 0x5c, 0xb0, 0x00, 0x40, 0x07, 0x02,
0xe8, 0x04, 0x00, 0x00, 0x80, 0x7e, 0x08, 0x01, 0x90, 0x10, 0x00, 0x00, 0x02,
0x00, 0x00, 0x00, 0x3f, 0x06, 0x80, 0x78, 0x10, 0x00, 0x00, 0x04, 0x00, 0x00,
0x02, 0x00, 0x38, 0x17, 0x40, 0x40, 0x00, 0x00, 0x08, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x40, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(req) == expected);
}
}
GIVEN("Deserialize ac_charge_loop_req dynamic mode") {
uint8_t doc_raw[] = {0x80, 0x08, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c, 0xdb, 0xfe,
0x1b, 0x60, 0x62, 0x00, 0x51, 0x84, 0x0c, 0x78, 0x67, 0xed, 0x5b, 0x83, 0x04, 0x08,
0x78, 0x17, 0x02, 0x04, 0x3c, 0x0b, 0x81, 0x00, 0x00, 0x20, 0x82, 0x0d, 0xc0, 0xb0,
0x20, 0x00, 0x00, 0x08, 0x00, 0x00, 0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40,
0x00, 0x00, 0x20, 0x03, 0x81, 0x74, 0x08, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00,
0x00, 0x00, 0x40, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00};
const io::StreamInputView stream_view{doc_raw, sizeof(doc_raw)};
message_20::Variant variant(io::v2gtp::PayloadType::Part20AC, stream_view);
THEN("It should be deserialized successfully") {
REQUIRE(variant.get_type() == message_20::Type::AC_ChargeLoopReq);
const auto& msg = variant.get<message_20::AC_ChargeLoopRequest>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456333);
REQUIRE(msg.meter_info_requested == false);
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::datatypes::Dynamic_AC_CLReqControlMode;
REQUIRE(std::holds_alternative<DynamicMode>(msg.control_mode));
const auto& control_mode = std::get<DynamicMode>(msg.control_mode);
REQUIRE(control_mode.departure_time.has_value() == true);
REQUIRE(control_mode.departure_time.value() == 1727440880);
REQUIRE(dt::from_RationalNumber(control_mode.target_energy_request) == 60000.0f);
REQUIRE(dt::from_RationalNumber(control_mode.max_energy_request) == 60000.0f);
REQUIRE(dt::from_RationalNumber(control_mode.min_energy_request) == 1.0f);
REQUIRE(dt::from_RationalNumber(control_mode.max_charge_power) == 150000.0f);
REQUIRE(control_mode.max_charge_power_L2.has_value() == true);
REQUIRE(message_20::datatypes::from_RationalNumber(*control_mode.max_charge_power_L2) == 0.0f);
REQUIRE(control_mode.max_charge_power_L3.has_value() == true);
REQUIRE(message_20::datatypes::from_RationalNumber(*control_mode.max_charge_power_L3) == 0.0f);
REQUIRE(dt::from_RationalNumber(control_mode.min_charge_power) == 0.0f);
REQUIRE(control_mode.min_charge_power_L2.has_value() == true);
REQUIRE(message_20::datatypes::from_RationalNumber(*control_mode.min_charge_power_L2) == 0.0f);
REQUIRE(control_mode.min_charge_power_L3.has_value() == true);
REQUIRE(message_20::datatypes::from_RationalNumber(*control_mode.min_charge_power_L3) == 0.0f);
REQUIRE(dt::from_RationalNumber(control_mode.present_active_power) == 12000.0f);
REQUIRE(control_mode.present_active_power_L2.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.present_active_power_L2) == 0.0f);
REQUIRE(control_mode.present_active_power_L3.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.present_active_power_L3) == 0.0f);
REQUIRE(dt::from_RationalNumber(control_mode.present_reactive_power) == 0.0f);
REQUIRE(control_mode.present_reactive_power_L2.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.present_reactive_power_L2) == 0.0f);
REQUIRE(control_mode.present_reactive_power_L3.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.present_reactive_power_L3) == 0.0f);
}
}
GIVEN("Serialize ac_charge_loop request_dynamic mode") {
message_20::AC_ChargeLoopRequest req;
req.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456333};
req.meter_info_requested = false;
auto& disparam = req.display_parameters.emplace();
disparam.present_soc = 10;
disparam.charging_complete = false;
auto& control_mode = req.control_mode.emplace<message_20::datatypes::Dynamic_AC_CLReqControlMode>();
control_mode.departure_time = 1727440880;
control_mode.target_energy_request = {6000, 1};
control_mode.max_energy_request = {6000, 1};
control_mode.min_energy_request = {1, 0};
control_mode.max_charge_power = {1500, 2};
control_mode.max_charge_power_L2 = {0, 0};
control_mode.max_charge_power_L3 = {0, 0};
control_mode.min_charge_power = {0, 0};
control_mode.min_charge_power_L2 = {0, 0};
control_mode.min_charge_power_L3 = {0, 0};
control_mode.present_active_power = {12000, 0};
control_mode.present_active_power_L2 = {0, 0};
control_mode.present_active_power_L3 = {0, 0};
control_mode.present_reactive_power = {0, 0};
control_mode.present_reactive_power_L2 = {0, 0};
control_mode.present_reactive_power_L3 = {0, 0};
std::vector<uint8_t> expected = {
0x80, 0x08, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c, 0xdb, 0xfe, 0x1b, 0x60,
0x62, 0x00, 0x51, 0x84, 0x0c, 0x78, 0x67, 0xed, 0x5b, 0x83, 0x04, 0x08, 0x78, 0x17, 0x02, 0x04,
0x3c, 0x0b, 0x81, 0x00, 0x00, 0x20, 0x82, 0x0d, 0xc0, 0xb0, 0x20, 0x00, 0x00, 0x08, 0x00, 0x00,
0x04, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x03, 0x81, 0x74, 0x08, 0x00,
0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x10, 0x00, 0x00, 0x00};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(req) == expected);
}
}
GIVEN("Deserialize ac_charge_loop_res scheduled mode") {
uint8_t doc_raw[] = {0x80, 0x0c, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c, 0xeb,
0xfe, 0x1b, 0x60, 0x62, 0x00, 0x10, 0x0c, 0xc4, 0x69, 0x04, 0xb1, 0x20, 0x00,
0xc8, 0x80, 0x40, 0x07, 0x02, 0xe8, 0x04, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00,
0x08, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x04, 0x00, 0x70,
0x2e, 0x81, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00};
const io::StreamInputView stream_view{doc_raw, sizeof(doc_raw)};
message_20::Variant variant(io::v2gtp::PayloadType::Part20AC, stream_view);
THEN("It should be deserialized successfully") {
REQUIRE(variant.get_type() == message_20::Type::AC_ChargeLoopRes);
const auto& msg = variant.get<message_20::AC_ChargeLoopResponse>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456334);
REQUIRE(msg.meter_info.has_value() == true);
const auto& meter_info = msg.meter_info.value();
REQUIRE(meter_info.meter_id == "1");
REQUIRE(meter_info.charged_energy_reading_wh == 1234);
using ScheduledMode = message_20::datatypes::Scheduled_AC_CLResControlMode;
REQUIRE(std::holds_alternative<ScheduledMode>(msg.control_mode));
const auto& control_mode = std::get<ScheduledMode>(msg.control_mode);
REQUIRE(control_mode.target_active_power.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.target_active_power) == 12000.0f);
REQUIRE(control_mode.target_active_power_L2.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.target_active_power_L2) == 0.0f);
REQUIRE(control_mode.target_active_power_L3.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.target_active_power_L3) == 0.0f);
REQUIRE(control_mode.target_reactive_power.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.target_reactive_power) == 0.0f);
REQUIRE(control_mode.target_reactive_power_L2.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.target_reactive_power_L2) == 0.0f);
REQUIRE(control_mode.target_reactive_power_L3.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.target_reactive_power_L3) == 0.0f);
REQUIRE(control_mode.present_active_power.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.present_active_power) == 12000.0f);
REQUIRE(control_mode.present_active_power_L2.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.present_active_power_L2) == 0.0f);
REQUIRE(control_mode.present_active_power_L3.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.present_active_power_L3) == 0.0f);
}
}
GIVEN("Serialize ac_charge_loop response scheduled") {
message_20::AC_ChargeLoopResponse res;
res.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456334};
res.response_code = message_20::datatypes::ResponseCode::OK;
res.target_frequency = {50, 0};
auto& met_info = res.meter_info.emplace();
met_info.meter_id = "1";
met_info.charged_energy_reading_wh = 1234;
auto& res_ctrl_mode = res.control_mode.emplace<message_20::datatypes::Scheduled_AC_CLResControlMode>();
res_ctrl_mode.target_active_power = {12000, 0};
res_ctrl_mode.target_active_power_L2 = {0, 0};
res_ctrl_mode.target_active_power_L3 = {0, 0};
res_ctrl_mode.target_reactive_power = {0, 0};
res_ctrl_mode.target_reactive_power_L2 = {0, 0};
res_ctrl_mode.target_reactive_power_L3 = {0, 0};
res_ctrl_mode.present_active_power = {12000, 0};
res_ctrl_mode.present_active_power_L2 = {0, 0};
res_ctrl_mode.present_active_power_L3 = {0, 0};
std::vector<uint8_t> expected = {0x80, 0x0c, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c, 0xeb,
0xfe, 0x1b, 0x60, 0x62, 0x00, 0x10, 0x0c, 0xc4, 0x69, 0x04, 0xb1, 0x20, 0x00,
0xc8, 0x80, 0x40, 0x07, 0x02, 0xe8, 0x04, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00,
0x08, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x20, 0x00, 0x00, 0x04, 0x00, 0x70,
0x2e, 0x81, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
}

View File

@@ -0,0 +1,139 @@
#include <catch2/catch_test_macros.hpp>
#include <iso15118/message/ac_charge_parameter_discovery.hpp>
#include <iso15118/message/variant.hpp>
#include "helper.hpp"
using namespace iso15118;
SCENARIO("Se/Deserialize ac charge parameter discovery messages") {
GIVEN("Deserialize ac_charge_parameter_discovery_req") {
uint8_t doc_raw[] = {0x80, 0x10, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c, 0x3b,
0xfe, 0x1b, 0x60, 0x62, 0x07, 0xE0, 0x80, 0x19, 0x02, 0x00, 0x00, 0x00, 0x80,
0x00, 0x00, 0x3F, 0x06, 0x80, 0x78, 0x10, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00};
const io::StreamInputView stream_view{doc_raw, sizeof(doc_raw)};
message_20::Variant variant(io::v2gtp::PayloadType::Part20AC, stream_view);
THEN("It should be deserialized successfully") {
REQUIRE(variant.get_type() == message_20::Type::AC_ChargeParameterDiscoveryReq);
const auto& msg = variant.get<message_20::AC_ChargeParameterDiscoveryRequest>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456323);
using AC_ModeReq = message_20::datatypes::AC_CPDReqEnergyTransferMode;
REQUIRE(std::holds_alternative<AC_ModeReq>(msg.transfer_mode));
const auto& transfer_mode = std::get<AC_ModeReq>(msg.transfer_mode);
REQUIRE(message_20::datatypes::from_RationalNumber(transfer_mode.max_charge_power) == 32);
REQUIRE(transfer_mode.max_charge_power_L2.has_value() == true);
REQUIRE(message_20::datatypes::from_RationalNumber(*transfer_mode.max_charge_power_L2) == 0.0f);
REQUIRE(transfer_mode.max_charge_power_L3.has_value() == true);
REQUIRE(message_20::datatypes::from_RationalNumber(*transfer_mode.max_charge_power_L3) == 0.0f);
REQUIRE(message_20::datatypes::from_RationalNumber(transfer_mode.min_charge_power) == 20);
REQUIRE(transfer_mode.min_charge_power_L2.has_value() == true);
REQUIRE(message_20::datatypes::from_RationalNumber(*transfer_mode.min_charge_power_L2) == 0.0f);
REQUIRE(transfer_mode.min_charge_power_L3.has_value() == true);
REQUIRE(message_20::datatypes::from_RationalNumber(*transfer_mode.min_charge_power_L3) == 0.0f);
}
}
GIVEN("Serialize ac_charge_parameter_discovery_req") {
using AC_ModeReq = message_20::datatypes::AC_CPDReqEnergyTransferMode;
message_20::AC_ChargeParameterDiscoveryRequest req;
req.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456323};
auto& mode = req.transfer_mode.emplace<AC_ModeReq>();
mode.max_charge_power = {3200, -2};
mode.max_charge_power_L2 = {0, 0};
mode.max_charge_power_L3 = {0, 0};
mode.min_charge_power = {2000, -2};
mode.min_charge_power_L2 = {0, 0};
mode.min_charge_power_L3 = {0, 0};
std::vector<uint8_t> expected = {0x80, 0x10, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c, 0x3b,
0xfe, 0x1b, 0x60, 0x62, 0x07, 0xE0, 0x80, 0x19, 0x02, 0x00, 0x00, 0x00, 0x80,
0x00, 0x00, 0x3F, 0x06, 0x80, 0x78, 0x10, 0x00, 0x00, 0x04, 0x00, 0x00, 0x00};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(req) == expected);
}
}
// TODO(sl): Adding BPT_AC_CPDReqEnergyTransferMode tests
// TODO(rb): Adding BPT_AC_CPDResEnergyTransferMode tests
GIVEN("Deserialize ac_charge_parameter_discovery_res") {
uint8_t doc_raw[] = {0x80, 0x14, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c, 0x4b, 0xfe, 0x1b,
0x60, 0x62, 0x00, 0x04, 0x08, 0x50, 0x08, 0x81, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20,
0x43, 0x40, 0x3c, 0x08, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00};
const io::StreamInputView stream_view{doc_raw, sizeof(doc_raw)};
message_20::Variant variant(io::v2gtp::PayloadType::Part20AC, stream_view);
THEN("It should be deserialized successfully") {
REQUIRE(variant.get_type() == message_20::Type::AC_ChargeParameterDiscoveryRes);
const auto& msg = variant.get<message_20::AC_ChargeParameterDiscoveryResponse>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456324);
REQUIRE(msg.response_code == message_20::datatypes::ResponseCode::OK);
using AC_ModeRes = message_20::datatypes::AC_CPDResEnergyTransferMode;
REQUIRE(std::holds_alternative<AC_ModeRes>(msg.transfer_mode));
const auto& transfer_mode = std::get<AC_ModeRes>(msg.transfer_mode);
REQUIRE(message_20::datatypes::from_RationalNumber(transfer_mode.max_charge_power) == 22080);
REQUIRE(transfer_mode.max_charge_power_L2.has_value() == true);
REQUIRE(message_20::datatypes::from_RationalNumber(*transfer_mode.max_charge_power_L2) == 0.0f);
REQUIRE(transfer_mode.max_charge_power_L3.has_value() == true);
REQUIRE(message_20::datatypes::from_RationalNumber(*transfer_mode.max_charge_power_L3) == 0.0f);
REQUIRE(message_20::datatypes::from_RationalNumber(transfer_mode.min_charge_power) == 20000);
REQUIRE(transfer_mode.min_charge_power_L2.has_value() == true);
REQUIRE(message_20::datatypes::from_RationalNumber(*transfer_mode.min_charge_power_L2) == 0.0f);
REQUIRE(transfer_mode.min_charge_power_L3.has_value() == true);
REQUIRE(message_20::datatypes::from_RationalNumber(*transfer_mode.min_charge_power_L3) == 0.0f);
}
}
GIVEN("Serialize ac_charge_parameter_discovery_res") {
using AC_ModeRes = message_20::datatypes::AC_CPDResEnergyTransferMode;
message_20::AC_ChargeParameterDiscoveryResponse res;
res.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456324};
res.response_code = message_20::datatypes::ResponseCode::OK;
auto& mode = res.transfer_mode.emplace<AC_ModeRes>();
mode.max_charge_power = {2208, 1};
mode.max_charge_power_L2 = {0, 0};
mode.max_charge_power_L3 = {0, 0};
mode.min_charge_power = {2000, 1};
mode.min_charge_power_L2 = {0, 0};
mode.min_charge_power_L3 = {0, 0};
std::vector<uint8_t> expected = {0x80, 0x14, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d,
0x8c, 0x4b, 0xfe, 0x1b, 0x60, 0x62, 0x00, 0x04, 0x08, 0x50, 0x08,
0x81, 0x00, 0x00, 0x00, 0x40, 0x00, 0x00, 0x20, 0x43, 0x40, 0x3c,
0x08, 0x00, 0x00, 0x02, 0x00, 0x00, 0x01, 0x00, 0x00, 0x05, 0x00};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
// TODO(sl): Adding BPT_AC_CPDResEnergyTransferMode tests
// TODO(rb): Adding BPT_AC_CPDReqEnergyTransferMode tests
}

View File

@@ -0,0 +1,92 @@
#include <catch2/catch_test_macros.hpp>
#include <iso15118/message/authorization.hpp>
#include <iso15118/message/variant.hpp>
#include "helper.hpp"
using namespace iso15118;
SCENARIO("Se/Deserialize authorization messages") {
GIVEN("Deserialize authorization_req eim") {
uint8_t doc_raw[] = {0x80, 0x00, 0x04, 0x79, 0x0c, 0x8a, 0xdc, 0xee, 0xee,
0x09, 0x68, 0x8d, 0x6c, 0xac, 0x3a, 0x60, 0x62, 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 deserialized successfully") {
REQUIRE(variant.get_type() == message_20::Type::AuthorizationReq);
const auto& msg = variant.get<message_20::AuthorizationRequest>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0xF2, 0x19, 0x15, 0xB9, 0xDD, 0xDC, 0x12, 0xD1});
REQUIRE(header.timestamp == 1691411798);
REQUIRE(msg.selected_authorization_service == message_20::datatypes::Authorization::EIM);
REQUIRE(std::holds_alternative<message_20::datatypes::EIM_ASReqAuthorizationMode>(msg.authorization_mode));
}
}
// TODO(sl): Adding authorization_req pnc tests
GIVEN("Serialize authorization_res") {
message_20::AuthorizationResponse res;
res.header = message_20::Header{{0xF2, 0x19, 0x15, 0xB9, 0xDD, 0xDC, 0x12, 0xD1}, 1691411798};
res.response_code = message_20::datatypes::ResponseCode::OK;
res.evse_processing = message_20::datatypes::Processing::Finished;
std::vector<uint8_t> expected = {0x80, 0x04, 0x04, 0x79, 0x0c, 0x8a, 0xdc, 0xee, 0xee, 0x09,
0x68, 0x8d, 0x6c, 0xac, 0x3a, 0x60, 0x62, 0x00, 0x00};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
GIVEN("Deserialize authorization_res eim") {
uint8_t doc_raw[] = {0x80, 0x04, 0x04, 0x79, 0x0c, 0x8a, 0xdc, 0xee, 0xee, 0x09,
0x68, 0x8d, 0x6c, 0xac, 0x3a, 0x60, 0x62, 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 deserialized successfully") {
REQUIRE(variant.get_type() == message_20::Type::AuthorizationRes);
const auto& msg = variant.get<message_20::AuthorizationResponse>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0xF2, 0x19, 0x15, 0xB9, 0xDD, 0xDC, 0x12, 0xD1});
REQUIRE(header.timestamp == 1691411798);
REQUIRE(msg.evse_processing == message_20::datatypes::Processing::Finished);
REQUIRE(msg.response_code == message_20::datatypes::ResponseCode::OK);
}
}
GIVEN("Serialize authorization_req eim") {
message_20::AuthorizationRequest req;
req.header = message_20::Header{{0xF2, 0x19, 0x15, 0xB9, 0xDD, 0xDC, 0x12, 0xD1}, 1691411798};
req.selected_authorization_service = message_20::datatypes::Authorization::EIM;
req.authorization_mode = message_20::datatypes::EIM_ASReqAuthorizationMode{};
// Todo(sl): Adding certificate
std::vector<uint8_t> expected = {0x80, 0x00, 0x04, 0x79, 0x0c, 0x8a, 0xdc, 0xee, 0xee,
0x09, 0x68, 0x8d, 0x6c, 0xac, 0x3a, 0x60, 0x62, 0x00};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(req) == expected);
}
}
}

View File

@@ -0,0 +1,143 @@
#include <catch2/catch_test_macros.hpp>
#include <string>
#include <iso15118/message/authorization_setup.hpp>
#include <iso15118/message/variant.hpp>
#include "helper.hpp"
using namespace iso15118;
SCENARIO("Se/Deserialize authorization setup messages") {
GIVEN("Deserialize authorization_setup_req") {
uint8_t doc_raw[] = {0x80, 0x08, 0x04, 0x79, 0x0c, 0x8a, 0xdc, 0xee, 0xee,
0x09, 0x68, 0x8d, 0x6c, 0xac, 0x3a, 0x60, 0x62};
const io::StreamInputView stream_view{doc_raw, sizeof(doc_raw)};
message_20::Variant variant(io::v2gtp::PayloadType::Part20Main, stream_view);
THEN("It should be deserialized successfully") {
REQUIRE(variant.get_type() == message_20::Type::AuthorizationSetupReq);
const auto& msg = variant.get<message_20::AuthorizationSetupRequest>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0xF2, 0x19, 0x15, 0xB9, 0xDD, 0xDC, 0x12, 0xD1});
REQUIRE(header.timestamp == 1691411798);
}
}
GIVEN("Serialize authorization_setup_req") {
const auto header = message_20::Header{{0xF2, 0x19, 0x15, 0xB9, 0xDD, 0xDC, 0x12, 0xD1}, 1691411798};
const auto res = message_20::AuthorizationSetupRequest{header};
std::vector<uint8_t> expected = {0x80, 0x08, 0x04, 0x79, 0x0c, 0x8a, 0xdc, 0xee, 0xee,
0x09, 0x68, 0x8d, 0x6c, 0xac, 0x3a, 0x60, 0x62};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
GIVEN("Serialize authorization_setup_res") {
message_20::AuthorizationSetupResponse res;
res.header = message_20::Header{{0xF2, 0x19, 0x15, 0xB9, 0xDD, 0xDC, 0x12, 0xD1}, 1691411798};
res.response_code = message_20::datatypes::ResponseCode::OK;
res.authorization_services = {message_20::datatypes::Authorization::EIM};
res.certificate_installation_service = true;
res.authorization_mode = message_20::datatypes::EIM_ASResAuthorizationMode();
std::vector<uint8_t> expected = {0x80, 0x0c, 0x04, 0x79, 0x0c, 0x8a, 0xdc, 0xee, 0xee, 0x09,
0x68, 0x8d, 0x6c, 0xac, 0x3a, 0x60, 0x62, 0x00, 0x05, 0x00};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
GIVEN("Serialize authorization_setup_res pnc") {
message_20::AuthorizationSetupResponse res;
res.header = message_20::Header{{0xF2, 0x19, 0x15, 0xB9, 0xDD, 0xDC, 0x12, 0xD1}, 1691411798};
res.response_code = message_20::datatypes::ResponseCode::OK;
res.authorization_services = {message_20::datatypes::Authorization::EIM,
message_20::datatypes::Authorization::PnC};
res.certificate_installation_service = true;
auto& mode = res.authorization_mode.emplace<message_20::datatypes::PnC_ASResAuthorizationMode>();
mode.gen_challenge = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16};
// FIXME(SL): supported_providers missing
std::vector<uint8_t> expected = {0x80, 0x0c, 0x04, 0x79, 0x0c, 0x8a, 0xdc, 0xee, 0xee, 0x09, 0x68, 0x8d, 0x6c,
0xac, 0x3a, 0x60, 0x62, 0x00, 0x01, 0x12, 0x08, 0x00, 0x81, 0x01, 0x82, 0x02,
0x83, 0x03, 0x84, 0x04, 0x85, 0x05, 0x86, 0x06, 0x87, 0x07, 0x88, 0x10};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
GIVEN("Deserialize authorization_setup_res") {
uint8_t doc_raw[] = {0x80, 0x0c, 0x04, 0x79, 0x0c, 0x8a, 0xdc, 0xee, 0xee, 0x09,
0x68, 0x8d, 0x6c, 0xac, 0x3a, 0x60, 0x62, 0x00, 0x05, 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 deserialized successfully") {
REQUIRE(variant.get_type() == message_20::Type::AuthorizationSetupRes);
const auto& msg = variant.get<message_20::AuthorizationSetupResponse>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0xF2, 0x19, 0x15, 0xB9, 0xDD, 0xDC, 0x12, 0xD1});
REQUIRE(header.timestamp == 1691411798);
REQUIRE(msg.response_code == message_20::datatypes::ResponseCode::OK);
REQUIRE(msg.authorization_services.size() == 1);
REQUIRE(msg.authorization_services[0] == message_20::datatypes::Authorization::EIM);
REQUIRE(msg.certificate_installation_service == true);
REQUIRE(std::holds_alternative<message_20::datatypes::EIM_ASResAuthorizationMode>(msg.authorization_mode));
}
}
GIVEN("Deserialize authorization_setup_res_pnc") {
uint8_t doc_raw[] = {0x80, 0x0c, 0x04, 0x79, 0x0c, 0x8a, 0xdc, 0xee, 0xee, 0x09, 0x68, 0x8d, 0x6c,
0xac, 0x3a, 0x60, 0x62, 0x00, 0x01, 0x12, 0x08, 0x00, 0x81, 0x01, 0x82, 0x02,
0x83, 0x03, 0x84, 0x04, 0x85, 0x05, 0x86, 0x06, 0x87, 0x07, 0x88, 0x10};
const io::StreamInputView stream_view{doc_raw, sizeof(doc_raw)};
message_20::Variant variant(io::v2gtp::PayloadType::Part20Main, stream_view);
THEN("It should be deserialized successfully") {
REQUIRE(variant.get_type() == message_20::Type::AuthorizationSetupRes);
const auto& msg = variant.get<message_20::AuthorizationSetupResponse>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0xF2, 0x19, 0x15, 0xB9, 0xDD, 0xDC, 0x12, 0xD1});
REQUIRE(header.timestamp == 1691411798);
REQUIRE(msg.response_code == message_20::datatypes::ResponseCode::OK);
REQUIRE(msg.authorization_services.size() == 2);
REQUIRE(msg.authorization_services[0] == message_20::datatypes::Authorization::EIM);
REQUIRE(msg.authorization_services[1] == message_20::datatypes::Authorization::PnC);
REQUIRE(msg.certificate_installation_service == true);
const message_20::datatypes::GenChallenge exp_gen_challenge = {1, 2, 3, 4, 5, 6, 7, 8,
9, 10, 11, 12, 13, 14, 15, 16};
const auto& pnc_auth_mode =
std::get<message_20::datatypes::PnC_ASResAuthorizationMode>(msg.authorization_mode);
REQUIRE(pnc_auth_mode.gen_challenge == exp_gen_challenge);
}
}
}

View File

@@ -0,0 +1,121 @@
#include <catch2/catch_test_macros.hpp>
#include <iso15118/message/dc_cable_check.hpp>
#include <iso15118/message/variant.hpp>
#include "helper.hpp"
using namespace iso15118;
SCENARIO("Se/Deserialize dc cable check messages") {
GIVEN("Deserialize dc_cable_check_req") {
uint8_t doc_raw[] = {0x80, 0x2c, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7,
0x6c, 0x4d, 0x8c, 0x7b, 0xfe, 0x1b, 0x60, 0x62};
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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::DC_CableCheckReq);
const auto& msg = variant.get<message_20::DC_CableCheckRequest>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456327);
}
}
GIVEN("Serialize dc_cable_check_req") {
message_20::DC_CableCheckRequest req;
req.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456327};
std::vector<uint8_t> expected = {0x80, 0x2c, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7,
0x6c, 0x4d, 0x8c, 0x7b, 0xfe, 0x1b, 0x60, 0x62};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(req) == expected);
}
}
GIVEN("Serialize dc_cable_check_res ongoing") {
message_20::DC_CableCheckResponse res;
res.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456328};
res.response_code = message_20::datatypes::ResponseCode::OK;
res.processing = message_20::datatypes::Processing::Ongoing;
std::vector<uint8_t> expected = {0x80, 0x30, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c,
0x4d, 0x8c, 0x8b, 0xfe, 0x1b, 0x60, 0x62, 0x00, 0x10};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
GIVEN("Deserialize dc_cable_check_res_ongoing") {
uint8_t doc_raw[] = {0x80, 0x30, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c,
0x4d, 0x8c, 0x8b, 0xfe, 0x1b, 0x60, 0x62, 0x00, 0x10};
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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::DC_CableCheckRes);
const auto& msg = variant.get<message_20::DC_CableCheckResponse>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456328);
REQUIRE(msg.response_code == message_20::datatypes::ResponseCode::OK);
REQUIRE(msg.processing == message_20::datatypes::Processing::Ongoing);
}
}
GIVEN("Serialize dc_cable_check_res finished") {
message_20::DC_CableCheckResponse res;
res.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456331};
res.response_code = message_20::datatypes::ResponseCode::OK;
res.processing = message_20::datatypes::Processing::Finished;
std::vector<uint8_t> expected = {0x80, 0x30, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c,
0x4d, 0x8c, 0xbb, 0xfe, 0x1b, 0x60, 0x62, 0x00, 0x00};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
GIVEN("Deserialize dc_cable_check_res_finished") {
uint8_t doc_raw[] = {0x80, 0x30, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c,
0x4d, 0x8c, 0xbb, 0xfe, 0x1b, 0x60, 0x62, 0x00, 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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::DC_CableCheckRes);
const auto& msg = variant.get<message_20::DC_CableCheckResponse>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456331);
REQUIRE(msg.response_code == message_20::datatypes::ResponseCode::OK);
REQUIRE(msg.processing == message_20::datatypes::Processing::Finished);
}
}
}

View File

@@ -0,0 +1,221 @@
#include <catch2/catch_test_macros.hpp>
#include <iso15118/message/dc_charge_loop.hpp>
#include <iso15118/message/variant.hpp>
#include "helper.hpp"
using namespace iso15118;
namespace dt = iso15118::message_20::datatypes;
SCENARIO("Se/Deserialize dc charge loop messages") {
GIVEN("Deserialize dc_charge_loop_req") {
uint8_t doc_raw[] = {0x80, 0x34, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c, 0xdb, 0xfe, 0x1b,
0x60, 0x62, 0x81, 0x00, 0x12, 0x00, 0x64, 0x64, 0x00, 0x0a, 0x02, 0x00, 0x24, 0x00, 0xca};
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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::DC_ChargeLoopReq);
const auto& msg = variant.get<message_20::DC_ChargeLoopRequest>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456333);
REQUIRE(msg.meter_info_requested == false);
REQUIRE(dt::from_RationalNumber(msg.present_voltage) == 400);
using ScheduledMode = message_20::datatypes::Scheduled_DC_CLReqControlMode;
REQUIRE(std::holds_alternative<ScheduledMode>(msg.control_mode));
const auto& control_mode = std::get<ScheduledMode>(msg.control_mode);
REQUIRE(dt::from_RationalNumber(control_mode.target_current) == 20);
REQUIRE(dt::from_RationalNumber(control_mode.target_voltage) == 400);
}
}
GIVEN("Serialize dc_charge_loop request") {
message_20::DC_ChargeLoopRequest req;
req.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456333};
req.meter_info_requested = false;
auto& control_mode = req.control_mode.emplace<message_20::datatypes::Scheduled_DC_CLReqControlMode>();
control_mode.target_current = {20, 0};
control_mode.target_voltage = {400, 0};
req.present_voltage = {400, 0};
std::vector<uint8_t> expected = {0x80, 0x34, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c,
0x4d, 0x8c, 0xdb, 0xfe, 0x1b, 0x60, 0x62, 0x81, 0x00, 0x12,
0x00, 0x64, 0x64, 0x00, 0x0a, 0x02, 0x00, 0x24, 0x00, 0xca};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(req) == expected);
}
}
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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::DC_ChargeLoopReq);
const auto& msg = variant.get<message_20::DC_ChargeLoopRequest>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x64, 0xEA, 0xED, 0x3D, 0x8E, 0x20, 0xC9, 0x59});
REQUIRE(header.timestamp == 1727440880);
REQUIRE(msg.meter_info_requested == false);
REQUIRE(dt::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::datatypes::Dynamic_DC_CLReqControlMode;
REQUIRE(std::holds_alternative<DynamicMode>(msg.control_mode));
const auto& control_mode = std::get<DynamicMode>(msg.control_mode);
REQUIRE(dt::from_RationalNumber(control_mode.target_energy_request) == 60000.0f);
REQUIRE(dt::from_RationalNumber(control_mode.max_energy_request) == 60000.0f);
REQUIRE(dt::from_RationalNumber(control_mode.min_energy_request) == 1.0f);
REQUIRE(dt::from_RationalNumber(control_mode.max_charge_power) == 150000.0f);
REQUIRE(dt::from_RationalNumber(control_mode.min_charge_power) == 0.0f);
REQUIRE(dt::from_RationalNumber(control_mode.max_charge_current) == 300.0f);
REQUIRE(dt::from_RationalNumber(control_mode.max_voltage) == 900.0f);
REQUIRE(dt::from_RationalNumber(control_mode.min_voltage) == 150.0f);
}
}
GIVEN("Serialize dc_charge_loop request dynamic mode") {
message_20::DC_ChargeLoopRequest req;
req.header = message_20::Header{{0x64, 0xEA, 0xED, 0x3D, 0x8E, 0x20, 0xC9, 0x59}, 1727440880};
req.meter_info_requested = false;
auto& control_mode = req.control_mode.emplace<message_20::datatypes::Dynamic_DC_CLReqControlMode>();
req.present_voltage = {400, 0};
control_mode.target_energy_request = {6000, 1};
control_mode.max_energy_request = {60, 3};
control_mode.min_energy_request = {1, 0};
control_mode.max_charge_power = {15000, 1};
control_mode.min_charge_power = {0, 0};
control_mode.max_charge_current = {300, 0};
control_mode.max_voltage = {900, 0};
control_mode.min_voltage = {150, 0};
req.display_parameters.emplace();
req.display_parameters->present_soc = 10;
req.display_parameters->charging_complete = false;
std::vector<uint8_t> expected = {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};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(req) == expected);
}
}
GIVEN("Serialize dc_charge_loop_res ongoing") {
message_20::DC_ChargeLoopResponse res;
res.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456334};
res.response_code = message_20::datatypes::ResponseCode::OK;
res.control_mode.emplace<message_20::datatypes::Scheduled_DC_CLResControlMode>();
res.current_limit_achieved = true;
res.power_limit_achieved = true;
res.voltage_limit_achieved = true;
res.present_current = {1000, -3};
res.present_voltage = {4000, -1};
std::vector<uint8_t> expected = {0x80, 0x38, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c,
0x4d, 0x8c, 0xeb, 0xfe, 0x1b, 0x60, 0x62, 0x00, 0x63, 0xe8,
0x74, 0x03, 0x81, 0xfc, 0x28, 0x07, 0xc2, 0x22, 0x90};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
GIVEN("Serialize dc_charge_loop_res dynamic dc_bpt") {
message_20::DC_ChargeLoopResponse res;
res.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456334};
res.response_code = message_20::datatypes::ResponseCode::OK;
auto& mode = res.control_mode.emplace<message_20::datatypes::Dynamic_DC_CLResControlMode>();
mode.max_charge_power = {150, 3};
mode.min_charge_power = {100, 0};
mode.max_charge_current = {60, 1};
mode.max_voltage = {9, 2};
res.current_limit_achieved = true;
res.power_limit_achieved = true;
res.voltage_limit_achieved = true;
res.present_current = {1000, -3};
res.present_voltage = {4000, -1};
std::vector<uint8_t> expected = {0x80, 0x38, 0x04, 0x1E, 0xA6, 0x5F, 0xC9, 0x9B, 0xA7, 0x6C, 0x4D,
0x8C, 0xEB, 0xFE, 0x1B, 0x60, 0x62, 0x00, 0x63, 0xE8, 0x74, 0x03,
0x81, 0xFC, 0x28, 0x07, 0xC2, 0x22, 0x70, 0x83, 0x09, 0x60, 0x10,
0x40, 0x03, 0x20, 0x20, 0x40, 0xF0, 0x10, 0x40, 0x12, 0x00};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
GIVEN("Deserialize dc_charge_loop res ongoing") {
uint8_t doc_raw[] = {0x80, 0x38, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c, 0xeb, 0xfe, 0x1b,
0x60, 0x62, 0x00, 0x63, 0xe8, 0x74, 0x03, 0x81, 0xfc, 0x28, 0x07, 0xc2, 0x22, 0x90};
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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::DC_ChargeLoopRes);
const auto& msg = variant.get<message_20::DC_ChargeLoopResponse>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456334);
REQUIRE(msg.response_code == message_20::datatypes::ResponseCode::OK);
REQUIRE(msg.current_limit_achieved == true);
REQUIRE(msg.power_limit_achieved == true);
REQUIRE(msg.voltage_limit_achieved == true);
REQUIRE(dt::from_RationalNumber(msg.present_current) == 1);
REQUIRE(dt::from_RationalNumber(msg.present_voltage) == 400);
REQUIRE(std::holds_alternative<message_20::datatypes::Scheduled_DC_CLResControlMode>(msg.control_mode));
const auto& control_mode = std::get<message_20::datatypes::Scheduled_DC_CLResControlMode>(msg.control_mode);
REQUIRE(control_mode.max_charge_current.has_value() == false);
REQUIRE(control_mode.max_charge_power.has_value() == false);
REQUIRE(control_mode.max_voltage.has_value() == false);
REQUIRE(control_mode.min_charge_power.has_value() == false);
}
}
}

View File

@@ -0,0 +1,133 @@
#include <catch2/catch_test_macros.hpp>
#include <iso15118/message/dc_charge_parameter_discovery.hpp>
#include <iso15118/message/variant.hpp>
#include "helper.hpp"
using namespace iso15118;
SCENARIO("Se/Deserialize dc charge parameter discovery messages") {
GIVEN("Deserialize dc_charge_parameter_discovery_req") {
uint8_t doc_raw[] = {0x80, 0x3c, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c, 0x3b, 0xfe,
0x1b, 0x60, 0x62, 0x88, 0x10, 0x98, 0x75, 0x04, 0x00, 0x32, 0x02, 0x00, 0x2b, 0x00,
0x81, 0x00, 0x01, 0x40, 0x80, 0x08, 0x40, 0x70, 0x40, 0x00, 0x50, 0x80};
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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::DC_ChargeParameterDiscoveryReq);
const auto& msg = variant.get<message_20::DC_ChargeParameterDiscoveryRequest>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456323);
using DC_ModeReq = message_20::datatypes::DC_CPDReqEnergyTransferMode;
REQUIRE(std::holds_alternative<DC_ModeReq>(msg.transfer_mode));
const auto& transfer_mode = std::get<DC_ModeReq>(msg.transfer_mode);
REQUIRE(message_20::datatypes::from_RationalNumber(transfer_mode.max_charge_current) == 300);
REQUIRE(message_20::datatypes::from_RationalNumber(transfer_mode.max_charge_power) == 150000);
REQUIRE(message_20::datatypes::from_RationalNumber(transfer_mode.max_voltage) == 900);
REQUIRE(message_20::datatypes::from_RationalNumber(transfer_mode.min_charge_current) == 10);
REQUIRE(message_20::datatypes::from_RationalNumber(transfer_mode.min_charge_power) == 100);
REQUIRE(message_20::datatypes::from_RationalNumber(transfer_mode.min_voltage) == 10);
}
}
GIVEN("Serialize dc_charge_parameter_discovery_req") {
using DC_ModeReq = message_20::datatypes::DC_CPDReqEnergyTransferMode;
message_20::DC_ChargeParameterDiscoveryRequest req;
req.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456323};
auto& mode = req.transfer_mode.emplace<DC_ModeReq>();
mode.max_charge_current = {300, 0};
mode.max_charge_power = {15000, 1};
mode.max_voltage = {900, 0};
mode.min_charge_current = {10, 0};
mode.min_charge_power = {100, 0};
mode.min_voltage = {10, 0};
std::vector<uint8_t> expected = {0x80, 0x3c, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c,
0x4d, 0x8c, 0x3b, 0xfe, 0x1b, 0x60, 0x62, 0x88, 0x10, 0x98,
0x75, 0x04, 0x00, 0x32, 0x02, 0x00, 0x2b, 0x00, 0x81, 0x00,
0x01, 0x40, 0x80, 0x08, 0x40, 0x70, 0x40, 0x00, 0x50, 0x80};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(req) == expected);
}
}
// TODO(sl): Adding BPT_DC_CPDReqEnergyTransferMode tests
// TODO(rb): Adding BPT_DC_CPDResEnergyTransferMode tests
GIVEN("Deserialize dc_charge_parameter_discovery_res") {
uint8_t doc_raw[] = {0x80, 0x40, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c, 0x4b, 0xfe, 0x1b,
0x60, 0x62, 0x00, 0x44, 0x08, 0x50, 0x08, 0x81, 0xfc, 0x34, 0x03, 0xc0, 0xfe, 0x1a, 0x01,
0xe0, 0x7d, 0x0e, 0x80, 0x70, 0x3f, 0x85, 0x42, 0x30, 0x1f, 0xc3, 0x40, 0x3c, 0x40};
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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::DC_ChargeParameterDiscoveryRes);
const auto& msg = variant.get<message_20::DC_ChargeParameterDiscoveryResponse>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456324);
REQUIRE(msg.response_code == message_20::datatypes::ResponseCode::OK);
using DC_ModeRes = message_20::datatypes::DC_CPDResEnergyTransferMode;
REQUIRE(std::holds_alternative<DC_ModeRes>(msg.transfer_mode));
const auto& transfer_mode = std::get<DC_ModeRes>(msg.transfer_mode);
REQUIRE(message_20::datatypes::from_RationalNumber(transfer_mode.max_charge_current) == 200);
REQUIRE(message_20::datatypes::from_RationalNumber(transfer_mode.max_charge_power) == 22080);
REQUIRE(message_20::datatypes::from_RationalNumber(transfer_mode.max_voltage) == 900);
REQUIRE(message_20::datatypes::from_RationalNumber(transfer_mode.min_charge_current) == 1);
REQUIRE(message_20::datatypes::from_RationalNumber(transfer_mode.min_charge_power) == 200);
REQUIRE(message_20::datatypes::from_RationalNumber(transfer_mode.min_voltage) == 200);
}
}
GIVEN("Serialize dc_charge_parameter_discovery_res") {
using DC_ModeRes = message_20::datatypes::DC_CPDResEnergyTransferMode;
message_20::DC_ChargeParameterDiscoveryResponse res;
res.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456324};
res.response_code = message_20::datatypes::ResponseCode::OK;
auto& mode = res.transfer_mode.emplace<DC_ModeRes>();
mode.max_charge_current = {2000, -1};
mode.max_charge_power = {2208, 1};
mode.max_voltage = {9000, -1};
mode.min_charge_current = {1000, -3};
mode.min_charge_power = {2000, -1};
mode.min_voltage = {2000, -1};
std::vector<uint8_t> expected = {0x80, 0x40, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d,
0x8c, 0x4b, 0xfe, 0x1b, 0x60, 0x62, 0x00, 0x44, 0x08, 0x50, 0x08,
0x81, 0xfc, 0x34, 0x03, 0xc0, 0xfe, 0x1a, 0x01, 0xe0, 0x7d, 0x0e,
0x80, 0x70, 0x3f, 0x85, 0x42, 0x30, 0x1f, 0xc3, 0x40, 0x3c, 0x40};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
// TODO(sl): Adding BPT_DC_CPDResEnergyTransferMode tests
// TODO(rb): Adding BPT_DC_CPDReqEnergyTransferMode tests
}

View File

@@ -0,0 +1,89 @@
#include <catch2/catch_test_macros.hpp>
#include <iso15118/message/dc_pre_charge.hpp>
#include <iso15118/message/variant.hpp>
#include "helper.hpp"
using namespace iso15118;
SCENARIO("Se/Deserialize dc pre charge messages") {
GIVEN("Deserialize dc_pre_charge_req") {
uint8_t doc_raw[] = {0x80, 0x44, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c, 0xbb,
0xfe, 0x1b, 0x60, 0x62, 0x21, 0x00, 0x12, 0x00, 0x60, 0x80, 0x09, 0x00, 0x30};
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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::DC_PreChargeReq);
const auto& msg = variant.get<message_20::DC_PreChargeRequest>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456331);
REQUIRE(msg.processing == message_20::datatypes::Processing::Ongoing);
REQUIRE(message_20::datatypes::from_RationalNumber(msg.present_voltage) == 400);
REQUIRE(message_20::datatypes::from_RationalNumber(msg.target_voltage) == 400);
}
}
GIVEN("Serialize dc_pre_charge_req") {
message_20::DC_PreChargeRequest req;
req.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456331};
req.processing = message_20::datatypes::Processing::Ongoing;
req.target_voltage = {400, 0};
req.present_voltage = {400, 0};
std::vector<uint8_t> expected = {0x80, 0x44, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c, 0xbb,
0xfe, 0x1b, 0x60, 0x62, 0x21, 0x00, 0x12, 0x00, 0x60, 0x80, 0x09, 0x00, 0x30};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(req) == expected);
}
}
GIVEN("Serialize dc_pre_charge_res") {
message_20::DC_PreChargeResponse res;
res.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456332};
res.response_code = message_20::datatypes::ResponseCode::OK;
res.present_voltage = {4000, -1};
std::vector<uint8_t> expected = {0x80, 0x48, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c,
0xcb, 0xfe, 0x1b, 0x60, 0x62, 0x00, 0x0f, 0xe1, 0x40, 0x3e, 0x00};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
GIVEN("Deserialize dc_pre_charge_res") {
uint8_t doc_raw[] = {0x80, 0x48, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c,
0xcb, 0xfe, 0x1b, 0x60, 0x62, 0x00, 0x0f, 0xe1, 0x40, 0x3e, 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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::DC_PreChargeRes);
const auto& msg = variant.get<message_20::DC_PreChargeResponse>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456332);
REQUIRE(msg.response_code == message_20::datatypes::ResponseCode::OK);
REQUIRE(message_20::datatypes::from_RationalNumber(msg.present_voltage) == 400);
}
}
}

View File

@@ -0,0 +1,88 @@
#include <catch2/catch_test_macros.hpp>
#include <iso15118/message/dc_welding_detection.hpp>
#include <iso15118/message/variant.hpp>
#include "helper.hpp"
using namespace iso15118;
SCENARIO("Se/Deserialize dc welding detection messages") {
GIVEN("Deserialize dc welding detection req") {
uint8_t doc_raw[] = {0x80, 0x4c, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7,
0x6c, 0x4d, 0x8d, 0x5b, 0xfe, 0x1b, 0x60, 0x62, 0x20};
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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::DC_WeldingDetectionReq);
const auto& msg = variant.get<message_20::DC_WeldingDetectionRequest>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456341);
REQUIRE(msg.processing == message_20::datatypes::Processing::Ongoing);
}
}
GIVEN("Serialize dc welding detection req") {
message_20::DC_WeldingDetectionRequest req;
req.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456341};
req.processing = message_20::datatypes::Processing::Ongoing;
std::vector<uint8_t> expected = {0x80, 0x4c, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7,
0x6c, 0x4d, 0x8d, 0x5b, 0xfe, 0x1b, 0x60, 0x62, 0x20};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(req) == expected);
}
}
GIVEN("Serialize dc_welding_detection res") {
message_20::DC_WeldingDetectionResponse res;
res.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456341};
res.response_code = message_20::datatypes::ResponseCode::OK;
res.present_voltage = {0, 0};
std::vector<uint8_t> expected = {0x80, 0x50, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d,
0x8d, 0x5b, 0xfe, 0x1b, 0x60, 0x62, 0x00, 0x10, 0x00, 0x00, 0x00};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
GIVEN("Deserialize dc welding detection res") {
uint8_t doc_raw[] = {0x80, 0x50, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d,
0x8d, 0x5b, 0xfe, 0x1b, 0x60, 0x62, 0x00, 0x10, 0x00, 0x00, 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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::DC_WeldingDetectionRes);
const auto& msg = variant.get<message_20::DC_WeldingDetectionResponse>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456341);
REQUIRE(msg.response_code == message_20::datatypes::ResponseCode::OK);
REQUIRE(msg.present_voltage.value == 0);
REQUIRE(msg.present_voltage.exponent == 0);
}
}
}

View File

@@ -0,0 +1,19 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Pionix GmbH and Contributors to EVerest
#pragma once
#include <cstdint>
#include <vector>
#include <iso15118/io/stream_view.hpp>
using namespace iso15118;
template <typename Message> std::vector<uint8_t> serialize_helper(const Message& message) {
uint8_t serialization_buffer[1024];
io::StreamOutputView out({serialization_buffer, sizeof(serialization_buffer)});
const auto size = message_20::serialize(message, out);
return std::vector<uint8_t>(serialization_buffer, serialization_buffer + size);
}

View File

@@ -0,0 +1,107 @@
#include <catch2/catch_test_macros.hpp>
#include <iso15118/message/power_delivery.hpp>
#include <iso15118/message/variant.hpp>
#include "helper.hpp"
using namespace iso15118;
SCENARIO("Se/Deserialize power delivery messages") {
GIVEN("Deserialize power_delivery_req") {
uint8_t doc_raw[] = {0x80, 0x54, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c, 0xdb, 0xfe,
0x1b, 0x60, 0x62, 0x00, 0x00, 0x01, 0x00, 0x42, 0x00, 0xb8, 0x41, 0x00, 0x51, 0x24};
const io::StreamInputView stream_view{doc_raw, sizeof(doc_raw)};
message_20::Variant variant(io::v2gtp::PayloadType::Part20Main, stream_view);
THEN("It should be deserialized successfully") {
REQUIRE(variant.get_type() == message_20::Type::PowerDeliveryReq);
const auto& msg = variant.get<message_20::PowerDeliveryRequest>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456333);
REQUIRE(msg.processing == message_20::datatypes::Processing::Finished);
REQUIRE(msg.charge_progress == message_20::datatypes::Progress::Start);
REQUIRE(msg.power_profile.has_value() == true);
auto& power_profile = msg.power_profile.value();
REQUIRE(power_profile.time_anchor == 0);
REQUIRE(power_profile.entries[0].duration == 23);
REQUIRE(message_20::datatypes::from_RationalNumber(power_profile.entries[0].power) == 1000);
REQUIRE(
std::holds_alternative<message_20::datatypes::Scheduled_EVPPTControlMode>(power_profile.control_mode));
const auto& mode = std::get<message_20::datatypes::Scheduled_EVPPTControlMode>(power_profile.control_mode);
REQUIRE(mode.power_tolerance_acceptance == message_20::datatypes::PowerToleranceAcceptance::Confirmed);
REQUIRE(mode.selected_schedule == 1);
}
}
GIVEN("Serialize power_delivery_req") {
message_20::PowerDeliveryRequest req;
req.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456333};
req.processing = message_20::datatypes::Processing::Finished;
req.charge_progress = message_20::datatypes::Progress::Start;
req.power_profile = message_20::datatypes::PowerProfile{};
req.power_profile->time_anchor = 0;
req.power_profile->entries.emplace_back();
req.power_profile->entries[0].duration = 23;
req.power_profile->entries[0].power = message_20::datatypes::RationalNumber{10, 2};
req.power_profile->control_mode.emplace<message_20::datatypes::Scheduled_EVPPTControlMode>();
auto& mode = std::get<message_20::datatypes::Scheduled_EVPPTControlMode>(req.power_profile->control_mode);
mode.power_tolerance_acceptance = message_20::datatypes::PowerToleranceAcceptance::Confirmed;
mode.selected_schedule = 1;
std::vector<uint8_t> expected = {0x80, 0x54, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c,
0x4d, 0x8c, 0xdb, 0xfe, 0x1b, 0x60, 0x62, 0x00, 0x00, 0x01,
0x00, 0x42, 0x00, 0xb8, 0x41, 0x00, 0x51, 0x24};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(req) == expected);
}
}
GIVEN("Serialize power_delivery_res") {
message_20::PowerDeliveryResponse res;
res.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456333};
res.response_code = message_20::datatypes::ResponseCode::OK;
std::vector<uint8_t> expected = {0x80, 0x58, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c,
0x4d, 0x8c, 0xdb, 0xfe, 0x1b, 0x60, 0x62, 0x00, 0x40};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
GIVEN("Deserialize power_delivery_res") {
uint8_t doc_raw[] = {0x80, 0x58, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c,
0x4d, 0x8c, 0xdb, 0xfe, 0x1b, 0x60, 0x62, 0x00, 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 deserialized successfully") {
REQUIRE(variant.get_type() == message_20::Type::PowerDeliveryRes);
const auto& msg = variant.get<message_20::PowerDeliveryResponse>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456333);
REQUIRE(msg.response_code == message_20::datatypes::ResponseCode::OK);
}
}
}

View File

@@ -0,0 +1,356 @@
#include <catch2/catch_test_macros.hpp>
#include <iso15118/message/schedule_exchange.hpp>
#include <iso15118/message/variant.hpp>
#include "helper.hpp"
using namespace iso15118;
namespace dt = iso15118::message_20::datatypes;
SCENARIO("Se/Deserialize schedule_exchange messages") {
GIVEN("Deserialize 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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::ScheduleExchangeReq);
const auto& msg = variant.get<message_20::ScheduleExchangeRequest>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x47, 0xFD, 0x3B, 0x4F, 0x13, 0x25, 0x57, 0xCA});
REQUIRE(header.timestamp == 1727082830);
REQUIRE(msg.max_supporting_points == 1024);
REQUIRE(std::holds_alternative<dt::Scheduled_SEReqControlMode>(msg.control_mode));
auto& control_mode = std::get<dt::Scheduled_SEReqControlMode>(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(dt::from_RationalNumber(*control_mode.target_energy) == 10000.0f);
REQUIRE(control_mode.max_energy.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.max_energy) == 20000.0f);
REQUIRE(control_mode.min_energy.has_value() == true);
REQUIRE(dt::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(dt::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(dt::from_RationalNumber(
ev_energy_offer.absolute_price_schedule.price_rule_stacks.at(0).price_rules.at(0).energy_fee) ==
0);
REQUIRE(dt::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 - scheduled mode") {
message_20::ScheduleExchangeRequest req;
req.header = message_20::Header{{0x47, 0xFD, 0x3B, 0x4F, 0x13, 0x25, 0x57, 0xCA}, 1727082830};
req.max_supporting_points = 1024;
auto& control_mode = req.control_mode.emplace<dt::Scheduled_SEReqControlMode>();
control_mode.departure_time = 7200;
control_mode.target_energy = dt::RationalNumber{10, 3};
control_mode.max_energy = dt::RationalNumber{20, 3};
control_mode.min_energy = dt::RationalNumber{5, -2};
control_mode.energy_offer.emplace();
control_mode.energy_offer->power_schedule.time_anchor = 0;
control_mode.energy_offer->power_schedule.entries.push_back({3600, {10, 3}});
control_mode.energy_offer->absolute_price_schedule.time_anchor = 0;
control_mode.energy_offer->absolute_price_schedule.currency = "EUR";
control_mode.energy_offer->absolute_price_schedule.price_algorithm =
"urn:iso:std:iso:15118:-20:PriceAlgorithm:1-Power";
dt::EVPriceRuleStack stack;
stack.duration = 0;
dt::EVPriceRule rule;
rule.energy_fee = {0, 0};
rule.power_range_start = {0, 0};
stack.price_rules.push_back(rule);
control_mode.energy_offer->absolute_price_schedule.price_rule_stacks.push_back(stack);
std::vector<uint8_t> expected = {
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};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(req) == expected);
}
}
GIVEN("DeSerialize 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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::ScheduleExchangeReq);
const auto& msg = variant.get<message_20::ScheduleExchangeRequest>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x39, 0x20, 0xB0, 0x04, 0x6E, 0x4A, 0xF9, 0x09});
REQUIRE(header.timestamp == 1727076438);
REQUIRE(msg.max_supporting_points == 1024);
REQUIRE(std::holds_alternative<dt::Dynamic_SEReqControlMode>(msg.control_mode));
auto& control_mode = std::get<dt::Dynamic_SEReqControlMode>(msg.control_mode);
REQUIRE(control_mode.departure_time == 7200);
REQUIRE(control_mode.minimum_soc == 30);
REQUIRE(control_mode.target_soc == 80);
REQUIRE(dt::from_RationalNumber(control_mode.target_energy) == 40000.0f);
REQUIRE(dt::from_RationalNumber(control_mode.max_energy) == 60000.0f);
REQUIRE(dt::from_RationalNumber(control_mode.min_energy) == -20000.0f);
REQUIRE(control_mode.max_v2x_energy.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.max_v2x_energy) == 5000.0f);
REQUIRE(control_mode.min_v2x_energy.has_value() == true);
REQUIRE(dt::from_RationalNumber(*control_mode.min_v2x_energy) == 0.0f);
}
}
GIVEN("Serialize schedule_exchange_req - dynamic mode") {
message_20::ScheduleExchangeRequest req;
req.header = message_20::Header{{0x39, 0x20, 0xB0, 0x04, 0x6E, 0x4A, 0xF9, 0x09}, 1727076438};
req.max_supporting_points = 1024;
auto& control_mode = req.control_mode.emplace<dt::Dynamic_SEReqControlMode>();
control_mode.departure_time = 7200;
control_mode.minimum_soc = 30;
control_mode.target_soc = 80;
control_mode.target_energy = dt::RationalNumber{40, 3};
control_mode.max_energy = dt::RationalNumber{60, 3};
control_mode.min_energy = dt::RationalNumber{-20, 3};
control_mode.max_v2x_energy = dt::RationalNumber{5, 3};
control_mode.min_v2x_energy = dt::RationalNumber{0, 0};
std::vector<uint8_t> expected = {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};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(req) == expected);
}
}
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 = dt::ResponseCode::OK;
res.processing = dt::Processing::Finished;
auto& control_mode = res.control_mode.emplace<dt::Scheduled_SEResControlMode>();
dt::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<uint8_t> 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 successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
GIVEN("Deserialize schedule_exchange_res - scheduled mode - no price") {
uint8_t doc_raw[] = {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};
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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::ScheduleExchangeRes);
const auto& msg = variant.get<message_20::ScheduleExchangeResponse>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x47, 0xFD, 0x3B, 0x4F, 0x13, 0x25, 0x57, 0xCA});
REQUIRE(header.timestamp == 1727082831);
REQUIRE(msg.response_code == dt::ResponseCode::OK);
REQUIRE(msg.processing == dt::Processing::Finished);
REQUIRE(msg.control_mode.index() == 1);
REQUIRE(std::holds_alternative<dt::Scheduled_SEResControlMode>(msg.control_mode));
auto& control_mode = std::get<dt::Scheduled_SEResControlMode>(msg.control_mode);
REQUIRE(control_mode.schedule_tuple.size() == 1);
REQUIRE(control_mode.schedule_tuple.at(0).schedule_tuple_id == 1);
REQUIRE(control_mode.schedule_tuple.at(0).charging_schedule.power_schedule.time_anchor == 1727082831);
REQUIRE(control_mode.schedule_tuple.at(0).charging_schedule.power_schedule.entries.size() == 1);
REQUIRE(control_mode.schedule_tuple.at(0).charging_schedule.power_schedule.entries.at(0).duration == 86400);
REQUIRE(dt::from_RationalNumber(
control_mode.schedule_tuple.at(0).charging_schedule.power_schedule.entries.at(0).power) ==
22080.0f);
}
}
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 = dt::ResponseCode::OK;
res.processing = dt::Processing::Finished;
auto& control_mode = res.control_mode.emplace<dt::Scheduled_SEResControlMode>();
dt::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<dt::PriceLevelSchedule>();
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<uint8_t> 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 successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
GIVEN("DeSerialize schedule_exchange_res - scheduled mode - price level") {
uint8_t doc_raw[] = {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};
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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::ScheduleExchangeRes);
const auto& msg = variant.get<message_20::ScheduleExchangeResponse>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x47, 0xFD, 0x3B, 0x4F, 0x13, 0x25, 0x57, 0xCA});
REQUIRE(header.timestamp == 1727082831);
REQUIRE(msg.response_code == dt::ResponseCode::OK);
REQUIRE(msg.processing == dt::Processing::Finished);
REQUIRE(msg.control_mode.index() == 1);
REQUIRE(std::holds_alternative<dt::Scheduled_SEResControlMode>(msg.control_mode));
auto& control_mode = std::get<dt::Scheduled_SEResControlMode>(msg.control_mode);
REQUIRE(control_mode.schedule_tuple.size() == 1);
REQUIRE(control_mode.schedule_tuple.at(0).schedule_tuple_id == 1);
REQUIRE(control_mode.schedule_tuple.at(0).charging_schedule.power_schedule.time_anchor == 1727082831);
REQUIRE(control_mode.schedule_tuple.at(0).charging_schedule.power_schedule.entries.size() == 1);
REQUIRE(control_mode.schedule_tuple.at(0).charging_schedule.power_schedule.entries.at(0).duration == 86400);
REQUIRE(dt::from_RationalNumber(
control_mode.schedule_tuple.at(0).charging_schedule.power_schedule.entries.at(0).power) ==
22080.0f);
REQUIRE(control_mode.schedule_tuple.at(0).charging_schedule.price_schedule.index() == 2);
auto& price_level =
std::get<dt::PriceLevelSchedule>(control_mode.schedule_tuple.at(0).charging_schedule.price_schedule);
REQUIRE(price_level.time_anchor == 1727082831);
REQUIRE(price_level.price_schedule_id == 1);
REQUIRE(price_level.number_of_price_levels == 0);
REQUIRE(price_level.price_level_schedule_entries.size() == 1);
REQUIRE(price_level.price_level_schedule_entries.at(0).duration == 23);
REQUIRE(price_level.price_level_schedule_entries.at(0).price_level == 8);
}
}
GIVEN("Serialize schedule_exchange_res - scheduled mode - absolute price") {
// TODO(sl): Add test + generate exi stream
}
GIVEN("DeSerialize schedule_exchange_res - scheduled mode - absolute price") {
// TODO(rb): 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 = dt::ResponseCode::OK;
res.processing = dt::Processing::Finished;
auto& control_mode = res.control_mode.emplace<dt::Dynamic_SEResControlMode>();
control_mode.departure_time = 2000;
std::vector<uint8_t> 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 successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
GIVEN("DeSerialize schedule_exchange_res - dynamic mode") {
uint8_t doc_raw[] = {0x80, 0x70, 0x04, 0x1c, 0x90, 0x58, 0x02, 0x37, 0x25, 0x7c, 0x84,
0x8d, 0x7b, 0x0c, 0x4b, 0x70, 0x62, 0x00, 0x02, 0x1a, 0x01, 0xe8};
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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::ScheduleExchangeRes);
const auto& msg = variant.get<message_20::ScheduleExchangeResponse>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x39, 0x20, 0xB0, 0x04, 0x6E, 0x4A, 0xF9, 0x09});
REQUIRE(header.timestamp == 1727076439);
REQUIRE(msg.response_code == dt::ResponseCode::OK);
REQUIRE(msg.processing == dt::Processing::Finished);
REQUIRE(msg.control_mode.index() == 0);
REQUIRE(std::holds_alternative<dt::Dynamic_SEResControlMode>(msg.control_mode));
auto& control_mode = std::get<dt::Dynamic_SEResControlMode>(msg.control_mode);
REQUIRE(control_mode.departure_time == 2000);
}
}
}

View File

@@ -0,0 +1,121 @@
#include <catch2/catch_test_macros.hpp>
#include <iso15118/message/service_detail.hpp>
#include <iso15118/message/variant.hpp>
#include "helper.hpp"
using namespace iso15118;
SCENARIO("Se/Deserialize service_detail messages") {
GIVEN("Deserialize service_detail_req") {
uint8_t doc_raw[] = {0x80, 0x74, 0x04, 0x02, 0x75, 0xff, 0x96, 0x4a, 0x2c, 0xed,
0xa1, 0x0e, 0x38, 0x7e, 0x8a, 0x60, 0x62, 0x02, 0x80};
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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::ServiceDetailReq);
const auto& msg = variant.get<message_20::ServiceDetailRequest>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x04, 0xEB, 0xFF, 0x2C, 0x94, 0x59, 0xDB, 0x42});
REQUIRE(header.timestamp == 1692009443);
REQUIRE(msg.service == message_20::to_underlying_value(message_20::datatypes::ServiceCategory::AC_BPT));
}
}
GIVEN("Serialize service_detail_req") {
message_20::ServiceDetailRequest req;
req.header = message_20::Header{{0x04, 0xEB, 0xFF, 0x2C, 0x94, 0x59, 0xDB, 0x42}, 1692009443};
req.service = message_20::to_underlying_value(message_20::datatypes::ServiceCategory::AC_BPT);
std::vector<uint8_t> expected = {0x80, 0x74, 0x04, 0x02, 0x75, 0xff, 0x96, 0x4a, 0x2c, 0xed,
0xa1, 0x0e, 0x38, 0x7e, 0x8a, 0x60, 0x62, 0x02, 0x80};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(req) == expected);
}
}
GIVEN("Serialize service_detail_res") {
message_20::ServiceDetailResponse res;
res.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456323};
res.response_code = message_20::datatypes::ResponseCode::OK;
res.service = message_20::to_underlying_value(message_20::datatypes::ServiceCategory::DC);
const auto list = message_20::datatypes::DcParameterList{
message_20::datatypes::DcConnector::Extended, message_20::datatypes::ControlMode::Scheduled,
message_20::datatypes::MobilityNeedsMode::ProvidedByEvcc, message_20::datatypes::Pricing::NoPricing};
res.service_parameter_list = {message_20::datatypes::ParameterSet(0, list)};
std::vector<uint8_t> expected = {
0x80, 0x78, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c, 0x3b, 0xfe, 0x1b, 0x60,
0x62, 0x00, 0x00, 0x80, 0x00, 0x02, 0xd0, 0xdb, 0xdb, 0x9b, 0x99, 0x58, 0xdd, 0x1b, 0xdc, 0x98,
0x04, 0x00, 0xd4, 0x36, 0xf6, 0xe7, 0x47, 0x26, 0xf6, 0xc4, 0xd6, 0xf6, 0x46, 0x56, 0x00, 0x80,
0x4d, 0x35, 0xbd, 0x89, 0xa5, 0xb1, 0xa5, 0xd1, 0xe5, 0x39, 0x95, 0x95, 0x91, 0xcd, 0x35, 0xbd,
0x91, 0x95, 0x80, 0x20, 0x09, 0x50, 0x72, 0x69, 0x63, 0x69, 0x6e, 0x67, 0x60, 0x00, 0xa0};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
GIVEN("Deserialize service_detail_res") {
uint8_t doc_raw[] = {0x80, 0x78, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c, 0x3b, 0xfe,
0x1b, 0x60, 0x62, 0x00, 0x00, 0x80, 0x00, 0x02, 0xd0, 0xdb, 0xdb, 0x9b, 0x99, 0x58,
0xdd, 0x1b, 0xdc, 0x98, 0x04, 0x00, 0xd4, 0x36, 0xf6, 0xe7, 0x47, 0x26, 0xf6, 0xc4,
0xd6, 0xf6, 0x46, 0x56, 0x00, 0x80, 0x4d, 0x35, 0xbd, 0x89, 0xa5, 0xb1, 0xa5, 0xd1,
0xe5, 0x39, 0x95, 0x95, 0x91, 0xcd, 0x35, 0xbd, 0x91, 0x95, 0x80, 0x20, 0x09, 0x50,
0x72, 0x69, 0x63, 0x69, 0x6e, 0x67, 0x60, 0x00, 0xa0};
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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::ServiceDetailRes);
const auto& msg = variant.get<message_20::ServiceDetailResponse>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456323);
REQUIRE(msg.service == message_20::to_underlying_value(message_20::datatypes::ServiceCategory::DC));
REQUIRE(msg.response_code == message_20::datatypes::ResponseCode::OK);
REQUIRE(msg.service_parameter_list.size() == 1);
REQUIRE(msg.service_parameter_list[0].id == 0);
REQUIRE(msg.service_parameter_list[0].parameter.size() == 4);
REQUIRE(msg.service_parameter_list[0].parameter[0].name == "Connector");
REQUIRE(std::holds_alternative<int32_t>(msg.service_parameter_list[0].parameter[0].value));
REQUIRE(std::get<int32_t>(msg.service_parameter_list[0].parameter[0].value) ==
static_cast<int32_t>(message_20::datatypes::DcConnector::Extended));
REQUIRE(msg.service_parameter_list[0].parameter[1].name == "ControlMode");
REQUIRE(std::holds_alternative<int32_t>(msg.service_parameter_list[0].parameter[1].value));
REQUIRE(std::get<int32_t>(msg.service_parameter_list[0].parameter[1].value) ==
static_cast<int32_t>(message_20::datatypes::ControlMode::Scheduled));
REQUIRE(msg.service_parameter_list[0].parameter[2].name == "MobilityNeedsMode");
REQUIRE(std::holds_alternative<int32_t>(msg.service_parameter_list[0].parameter[2].value));
REQUIRE(std::get<int32_t>(msg.service_parameter_list[0].parameter[2].value) ==
static_cast<int32_t>(message_20::datatypes::MobilityNeedsMode::ProvidedByEvcc));
REQUIRE(msg.service_parameter_list[0].parameter[3].name == "Pricing");
REQUIRE(std::holds_alternative<int32_t>(msg.service_parameter_list[0].parameter[3].value));
REQUIRE(std::get<int32_t>(msg.service_parameter_list[0].parameter[3].value) ==
static_cast<int32_t>(message_20::datatypes::Pricing::NoPricing));
}
}
}

View File

@@ -0,0 +1,182 @@
#include <catch2/catch_test_macros.hpp>
#include <iso15118/message/service_discovery.hpp>
#include <iso15118/message/variant.hpp>
#include "helper.hpp"
using namespace iso15118;
SCENARIO("Se/Deserialize service_discovery messages") {
GIVEN("Deserialize service_discovery_req") {
uint8_t doc_raw[] = {0x80, 0x7c, 0x04, 0x02, 0x75, 0xff, 0x96, 0x4a, 0x2c,
0xed, 0xa1, 0x0e, 0x38, 0x7e, 0x8a, 0x60, 0x62, 0x80};
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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::ServiceDiscoveryReq);
const auto& msg = variant.get<message_20::ServiceDiscoveryRequest>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x04, 0xEB, 0xFF, 0x2C, 0x94, 0x59, 0xDB, 0x42});
REQUIRE(header.timestamp == 1692009443);
}
}
GIVEN("Deserialize service_discovery_req_with_supported_service_ids") {
uint8_t doc_raw[] = {0x80, 0x7c, 0x04, 0x02, 0x75, 0xff, 0x96, 0x4a, 0x2c, 0xed,
0xa1, 0x0e, 0x38, 0x7e, 0x8a, 0x60, 0x62, 0x00, 0x44};
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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::ServiceDiscoveryReq);
const auto& msg = variant.get<message_20::ServiceDiscoveryRequest>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x04, 0xEB, 0xFF, 0x2C, 0x94, 0x59, 0xDB, 0x42});
REQUIRE(header.timestamp == 1692009443);
REQUIRE(msg.supported_service_ids.has_value() == true);
REQUIRE(msg.supported_service_ids.value().size() == 1);
REQUIRE(msg.supported_service_ids.value()[0] == 2);
}
}
GIVEN("Serialize service_discovery_req") {
message_20::ServiceDiscoveryRequest req;
req.header = message_20::Header{{0x04, 0xEB, 0xFF, 0x2C, 0x94, 0x59, 0xDB, 0x42}, 1692009443};
std::vector<uint8_t> expected = {0x80, 0x7c, 0x04, 0x02, 0x75, 0xff, 0x96, 0x4a, 0x2c,
0xed, 0xa1, 0x0e, 0x38, 0x7e, 0x8a, 0x60, 0x62, 0x80};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(req) == expected);
}
}
GIVEN("Serialize service_discovery_req_with_supported_service_ids") {
message_20::ServiceDiscoveryRequest req;
req.header = message_20::Header{{0x04, 0xEB, 0xFF, 0x2C, 0x94, 0x59, 0xDB, 0x42}, 1692009443};
req.supported_service_ids = {2};
std::vector<uint8_t> expected = {0x80, 0x7c, 0x04, 0x02, 0x75, 0xff, 0x96, 0x4a, 0x2c, 0xed,
0xa1, 0x0e, 0x38, 0x7e, 0x8a, 0x60, 0x62, 0x00, 0x44};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(req) == expected);
}
}
GIVEN("Serialize service_discovery_res") {
message_20::ServiceDiscoveryResponse res;
res.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456322};
res.response_code = message_20::datatypes::ResponseCode::OK;
res.service_renegotiation_supported = false;
res.energy_transfer_service_list = {{message_20::datatypes::ServiceCategory::DC, false},
{message_20::datatypes::ServiceCategory::DC_BPT, false}};
std::vector<uint8_t> expected = {0x80, 0x80, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c,
0x2b, 0xfe, 0x1b, 0x60, 0x62, 0x00, 0x00, 0x02, 0x00, 0x01, 0x80, 0x50};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
GIVEN("Serialize service_discovery_res_with_vas_list") {
message_20::ServiceDiscoveryResponse res;
res.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456322};
res.response_code = message_20::datatypes::ResponseCode::OK;
res.service_renegotiation_supported = false;
res.energy_transfer_service_list = {{message_20::datatypes::ServiceCategory::DC, false},
{message_20::datatypes::ServiceCategory::DC_BPT, false}};
res.vas_list = {{message_20::to_underlying_value(message_20::datatypes::ServiceCategory::Internet), true}};
std::vector<uint8_t> expected = {0x80, 0x80, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c, 0x2b,
0xfe, 0x1b, 0x60, 0x62, 0x00, 0x00, 0x02, 0x00, 0x01, 0x80, 0x40, 0x82, 0x22};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
GIVEN("Deserialize service_discovery_res") {
uint8_t doc_raw[] = {0x80, 0x80, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c,
0x2b, 0xfe, 0x1b, 0x60, 0x62, 0x00, 0x00, 0x02, 0x00, 0x01, 0x80, 0x50};
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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::ServiceDiscoveryRes);
const auto& msg = variant.get<message_20::ServiceDiscoveryResponse>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456322);
REQUIRE(msg.response_code == message_20::datatypes::ResponseCode::OK);
REQUIRE(msg.service_renegotiation_supported == false);
REQUIRE(msg.energy_transfer_service_list.size() == 2);
REQUIRE(msg.energy_transfer_service_list[0].service_id == message_20::datatypes::ServiceCategory::DC);
REQUIRE(msg.energy_transfer_service_list[0].free_service == false);
REQUIRE(msg.energy_transfer_service_list[1].service_id == message_20::datatypes::ServiceCategory::DC_BPT);
REQUIRE(msg.energy_transfer_service_list[1].free_service == false);
}
}
GIVEN("Deserialize service_discovery_res_with_vas_list") {
uint8_t doc_raw[] = {0x80, 0x80, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c, 0x4d, 0x8c, 0x2b,
0xfe, 0x1b, 0x60, 0x62, 0x00, 0x00, 0x02, 0x00, 0x01, 0x80, 0x40, 0x82, 0x22};
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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::ServiceDiscoveryRes);
const auto& msg = variant.get<message_20::ServiceDiscoveryResponse>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456322);
REQUIRE(msg.response_code == message_20::datatypes::ResponseCode::OK);
REQUIRE(msg.service_renegotiation_supported == false);
REQUIRE(msg.energy_transfer_service_list.size() == 2);
REQUIRE(msg.energy_transfer_service_list[0].service_id == message_20::datatypes::ServiceCategory::DC);
REQUIRE(msg.energy_transfer_service_list[0].free_service == false);
REQUIRE(msg.energy_transfer_service_list[1].service_id == message_20::datatypes::ServiceCategory::DC_BPT);
REQUIRE(msg.energy_transfer_service_list[1].free_service == false);
REQUIRE(msg.vas_list.value().size() == 1);
REQUIRE(msg.vas_list.value()[0].service_id ==
message_20::to_underlying_value(message_20::datatypes::ServiceCategory::Internet));
REQUIRE(msg.vas_list.value()[0].free_service == true);
}
}
}

View File

@@ -0,0 +1,142 @@
#include <catch2/catch_test_macros.hpp>
#include <iso15118/message/service_selection.hpp>
#include <iso15118/message/variant.hpp>
#include "helper.hpp"
using namespace iso15118;
namespace dt = message_20::datatypes;
SCENARIO("Se/Deserialize service_selection messages") {
GIVEN("Deserialize service_selection_req") {
uint8_t doc_raw[] = {0x80, 0x84, 0x04, 0x02, 0x75, 0xff, 0x96, 0x4a, 0x2c, 0xed, 0xa1,
0x0e, 0x38, 0x7e, 0x8a, 0x60, 0x62, 0x01, 0x40, 0x08, 0x80};
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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::ServiceSelectionReq);
const auto& msg = variant.get<message_20::ServiceSelectionRequest>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x04, 0xEB, 0xFF, 0x2C, 0x94, 0x59, 0xDB, 0x42});
REQUIRE(header.timestamp == 1692009443);
REQUIRE(msg.selected_energy_transfer_service.service_id == dt::ServiceCategory::AC_BPT);
REQUIRE(msg.selected_energy_transfer_service.parameter_set_id == 1);
}
}
GIVEN("Deserialize service_selection_req vas") {
uint8_t doc_raw[] = {0x80, 0x84, 0x04, 0x02, 0x75, 0xFF, 0x96, 0x4A, 0x2C, 0xED, 0xA1, 0x0E,
0x38, 0x7E, 0x8A, 0x60, 0x62, 0x01, 0x40, 0x08, 0x04, 0x10, 0x04, 0x20};
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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::ServiceSelectionReq);
const auto& msg = variant.get<message_20::ServiceSelectionRequest>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x04, 0xEB, 0xFF, 0x2C, 0x94, 0x59, 0xDB, 0x42});
REQUIRE(header.timestamp == 1692009443);
REQUIRE(msg.selected_energy_transfer_service.service_id == dt::ServiceCategory::AC_BPT);
REQUIRE(msg.selected_energy_transfer_service.parameter_set_id == 1);
REQUIRE(msg.selected_vas_list.has_value() == true);
const auto& selected_vas_list = msg.selected_vas_list.value();
REQUIRE(selected_vas_list.size() == 1);
REQUIRE(selected_vas_list.at(0).service_id ==
message_20::to_underlying_value(dt::ServiceCategory::Internet));
REQUIRE(selected_vas_list.at(0).parameter_set_id == 2);
}
}
GIVEN("Serialize service_selection_req") {
message_20::ServiceSelectionRequest req;
req.header = message_20::Header{{0x04, 0xEB, 0xFF, 0x2C, 0x94, 0x59, 0xDB, 0x42}, 1692009443};
req.selected_energy_transfer_service.service_id = dt::ServiceCategory::AC_BPT;
req.selected_energy_transfer_service.parameter_set_id = 1;
std::vector<uint8_t> expected = {0x80, 0x84, 0x04, 0x02, 0x75, 0xff, 0x96, 0x4a, 0x2c, 0xed, 0xa1,
0x0e, 0x38, 0x7e, 0x8a, 0x60, 0x62, 0x01, 0x40, 0x08, 0x80};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(req) == expected);
}
}
GIVEN("Serialize service_selection_req vas") {
message_20::ServiceSelectionRequest req;
req.header = message_20::Header{{0x04, 0xEB, 0xFF, 0x2C, 0x94, 0x59, 0xDB, 0x42}, 1692009443};
req.selected_energy_transfer_service.service_id = dt::ServiceCategory::AC_BPT;
req.selected_energy_transfer_service.parameter_set_id = 1;
const auto vas = dt::VasSelectedService{message_20::to_underlying_value(dt::ServiceCategory::Internet), 2};
req.selected_vas_list.emplace({vas});
std::vector<uint8_t> expected = {0x80, 0x84, 0x04, 0x02, 0x75, 0xFF, 0x96, 0x4A, 0x2C, 0xED, 0xA1, 0x0E,
0x38, 0x7E, 0x8A, 0x60, 0x62, 0x01, 0x40, 0x08, 0x04, 0x10, 0x04, 0x20};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(req) == expected);
}
}
GIVEN("Serialize service_selection_res") {
message_20::ServiceSelectionResponse res;
res.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456323};
res.response_code = dt::ResponseCode::OK;
std::vector<uint8_t> expected = {0x80, 0x88, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c,
0x4d, 0x8c, 0x3b, 0xfe, 0x1b, 0x60, 0x62, 0x00, 0x00};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
GIVEN("Deserialize service_selection_res") {
uint8_t doc_raw[] = {0x80, 0x88, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c,
0x4d, 0x8c, 0x3b, 0xfe, 0x1b, 0x60, 0x62, 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 successfully") {
REQUIRE(variant.get_type() == message_20::Type::ServiceSelectionRes);
const auto& msg = variant.get<message_20::ServiceSelectionResponse>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456323);
REQUIRE(msg.response_code == dt::ResponseCode::OK);
}
}
}

View File

@@ -0,0 +1,87 @@
#include <catch2/catch_test_macros.hpp>
#include <iso15118/message/session_setup.hpp>
#include <iso15118/message/variant.hpp>
#include <string>
#include "helper.hpp"
using namespace iso15118;
SCENARIO("Se/Deserialize session setup messages") {
GIVEN("Deserialize session_setup_req") {
uint8_t doc_raw[] = {0x80, 0x8c, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc, 0x9f,
0x9c, 0x2b, 0xd0, 0x62, 0xb, 0x2b, 0xa6, 0xa4, 0xab, 0x18, 0x99, 0x19, 0x9a,
0x1a, 0x9b, 0x1b, 0x9c, 0x1c, 0x98, 0x20, 0xa1, 0x21, 0xa2, 0x22, 0xac, 0x0};
const io::StreamInputView stream_view{doc_raw, sizeof(doc_raw)};
message_20::Variant variant(io::v2gtp::PayloadType::Part20Main, stream_view);
THEN("It should be deserialized successfully") {
REQUIRE(variant.get_type() == message_20::Type::SessionSetupReq);
const auto& msg = variant.get<message_20::SessionSetupRequest>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0});
REQUIRE(header.timestamp == 1739635913);
REQUIRE(msg.evccid == "WMIV1234567890ABCDEX");
}
}
GIVEN("Serialize session_setup_res") {
const auto header = message_20::Header{{0x2E, 0xFA, 0x18, 0x94, 0xDC, 0x7B, 0x90, 0x11}, 1739635913};
const auto res = message_20::SessionSetupResponse{
header, message_20::datatypes::ResponseCode::OK_NewSessionEstablished, "DE*PNX*E12345*1"};
std::vector<uint8_t> expected = {0x80, 0x90, 0x4, 0x17, 0x7d, 0xc, 0x4a, 0x6e, 0x3d, 0xc8, 0x8, 0x8c,
0x9f, 0x9c, 0x2b, 0xd0, 0x62, 0x4, 0x4, 0x51, 0x11, 0x4a, 0x94, 0x13,
0x96, 0xa, 0x91, 0x4c, 0x4c, 0x8c, 0xcd, 0xd, 0x4a, 0x8c, 0x40};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
GIVEN("Deserialize session_setup_res") {
uint8_t doc_raw[] = {0x80, 0x90, 0x4, 0x17, 0x7d, 0xc, 0x4a, 0x6e, 0x3d, 0xc8, 0x8, 0x8c,
0x9f, 0x9c, 0x2b, 0xd0, 0x62, 0x4, 0x4, 0x51, 0x11, 0x4a, 0x94, 0x13,
0x96, 0xa, 0x91, 0x4c, 0x4c, 0x8c, 0xcd, 0xd, 0x4a, 0x8c, 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 deserialized successfully") {
REQUIRE(variant.get_type() == message_20::Type::SessionSetupRes);
const auto& msg = variant.get<message_20::SessionSetupResponse>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x2E, 0xFA, 0x18, 0x94, 0xDC, 0x7B, 0x90, 0x11});
REQUIRE(header.timestamp == 1739635913);
REQUIRE(msg.evseid == "DE*PNX*E12345*1");
}
}
GIVEN("Serialize session_setup_req") {
const auto header = message_20::Header{{0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, 1739635913};
const auto res = message_20::SessionSetupRequest{header, "WMIV1234567890ABCDEX"};
std::vector<uint8_t> expected = {0x80, 0x8c, 0x4, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0x0, 0xc, 0x9f,
0x9c, 0x2b, 0xd0, 0x62, 0xb, 0x2b, 0xa6, 0xa4, 0xab, 0x18, 0x99, 0x19, 0x9a,
0x1a, 0x9b, 0x1b, 0x9c, 0x1c, 0x98, 0x20, 0xa1, 0x21, 0xa2, 0x22, 0xac, 0x0};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
}

View File

@@ -0,0 +1,84 @@
#include <catch2/catch_test_macros.hpp>
#include <iso15118/message/session_stop.hpp>
#include <iso15118/message/variant.hpp>
#include "helper.hpp"
using namespace iso15118;
SCENARIO("Se/Deserialize session stop messages") {
GIVEN("Deserialize session_stop_req") {
uint8_t doc_raw[] = {0x80, 0x94, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7,
0x6c, 0x4d, 0x8d, 0x7b, 0xfe, 0x1b, 0x60, 0x62, 0x28};
const io::StreamInputView stream_view{doc_raw, sizeof(doc_raw)};
message_20::Variant variant(io::v2gtp::PayloadType::Part20Main, stream_view);
THEN("It should be deserialized successfully") {
REQUIRE(variant.get_type() == message_20::Type::SessionStopReq);
const auto& msg = variant.get<message_20::SessionStopRequest>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456343);
REQUIRE(msg.charging_session == message_20::datatypes::ChargingSession::Terminate);
}
}
GIVEN("Serialize session_stop_req") {
message_20::SessionStopRequest req;
req.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456343};
req.charging_session = message_20::datatypes::ChargingSession::Terminate;
std::vector<uint8_t> expected = {0x80, 0x94, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7,
0x6c, 0x4d, 0x8d, 0x7b, 0xfe, 0x1b, 0x60, 0x62, 0x28};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(req) == expected);
}
}
GIVEN("Serialize session_stop_res") {
message_20::SessionStopResponse res;
res.header = message_20::Header{{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B}, 1725456343};
res.response_code = message_20::datatypes::ResponseCode::OK;
std::vector<uint8_t> expected = {0x80, 0x98, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c,
0x4d, 0x8d, 0x7b, 0xfe, 0x1b, 0x60, 0x62, 0x00, 0x00};
THEN("It should be serialized successfully") {
REQUIRE(serialize_helper(res) == expected);
}
}
GIVEN("Deserialize session_stop_res") {
uint8_t doc_raw[] = {0x80, 0x98, 0x04, 0x1e, 0xa6, 0x5f, 0xc9, 0x9b, 0xa7, 0x6c,
0x4d, 0x8d, 0x7b, 0xfe, 0x1b, 0x60, 0x62, 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 deserialized successfully") {
REQUIRE(variant.get_type() == message_20::Type::SessionStopRes);
const auto& msg = variant.get<message_20::SessionStopResponse>();
const auto& header = msg.header;
REQUIRE(header.session_id == std::array<uint8_t, 8>{0x3D, 0x4C, 0xBF, 0x93, 0x37, 0x4E, 0xD8, 0x9B});
REQUIRE(header.timestamp == 1725456343);
REQUIRE(msg.response_code == message_20::datatypes::ResponseCode::OK);
}
}
}

View File

@@ -0,0 +1,17 @@
add_subdirectory(d20)
add_subdirectory(fsm)
add_subdirectory(io)
add_subdirectory(session)
add_subdirectory(states)
# add_executable(secc)
#
# target_sources(secc
# PRIVATE
# secc.cpp
# )
#
# target_link_libraries(secc
# PRIVATE
# iso15118::iso15118
# )

View File

@@ -0,0 +1,11 @@
include(Catch)
add_executable(test_timeouts timeouts.cpp)
target_link_libraries(test_timeouts
PRIVATE
iso15118
Catch2::Catch2WithMain
)
catch_discover_tests(test_timeouts)

View File

@@ -0,0 +1,77 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2025 Pionix GmbH and Contributors to EVerest
#include <catch2/catch_test_macros.hpp>
#include <thread>
#include <iso15118/d20/timeout.hpp>
SCENARIO("Timeouts Tests") {
GIVEN("Basic Timeout") {
auto timeout = iso15118::Timeout(20);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
REQUIRE(timeout.is_reached() == false);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
REQUIRE(timeout.is_reached() == true);
}
GIVEN("Start Timeout and reset timeout after reaching it") {
auto timeouts = iso15118::d20::Timeouts{};
timeouts.start_timeout(iso15118::d20::TimeoutType::CONTACTOR, 20);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
auto timeout_reached = timeouts.check();
REQUIRE(timeout_reached.has_value() == false);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
timeout_reached = timeouts.check();
REQUIRE(timeout_reached.has_value() == true);
auto& reached = timeout_reached.value();
REQUIRE(reached.size() == 1);
REQUIRE(reached.at(0) == iso15118::d20::TimeoutType::CONTACTOR);
timeouts.reset_timeout(iso15118::d20::TimeoutType::CONTACTOR);
timeout_reached = timeouts.check();
REQUIRE(timeout_reached.has_value() == false);
}
GIVEN("Start Timeout and stop timeout before reaching it") {
auto timeouts = iso15118::d20::Timeouts{};
timeouts.start_timeout(iso15118::d20::TimeoutType::CONTACTOR, 20);
std::this_thread::sleep_for(std::chrono::milliseconds(10));
auto timeout_reached = timeouts.check();
REQUIRE(timeout_reached.has_value() == false);
std::this_thread::sleep_for(std::chrono::milliseconds(5));
timeouts.stop_timeout(iso15118::d20::TimeoutType::CONTACTOR);
timeout_reached = timeouts.check();
REQUIRE(timeout_reached.has_value() == false);
}
GIVEN("Parallel Timeouts") {
auto timeouts = iso15118::d20::Timeouts{};
timeouts.start_timeout(iso15118::d20::TimeoutType::SEQUENCE, 30);
timeouts.start_timeout(iso15118::d20::TimeoutType::PERFORMANCE, 10);
timeouts.start_timeout(iso15118::d20::TimeoutType::CONTACTOR, 20);
std::this_thread::sleep_for(std::chrono::milliseconds(100));
const auto timeouts_reached = timeouts.check();
REQUIRE(timeouts_reached.has_value());
const auto& reached = timeouts_reached.value();
REQUIRE(reached.at(0) == iso15118::d20::TimeoutType::PERFORMANCE);
REQUIRE(reached.at(1) == iso15118::d20::TimeoutType::CONTACTOR);
REQUIRE(reached.at(2) == iso15118::d20::TimeoutType::SEQUENCE);
}
}

View File

@@ -0,0 +1,33 @@
include(Catch)
function(create_fsm_test_target NAME)
add_executable(test_fsm_${NAME} ${NAME}.cpp)
target_sources(test_fsm_${NAME}
PRIVATE
helper.cpp
)
target_link_libraries(test_fsm_${NAME}
PRIVATE
iso15118
Catch2::Catch2WithMain
)
catch_discover_tests(test_fsm_${NAME})
endfunction()
create_fsm_test_target(session_setup)
create_fsm_test_target(service_detail)
add_executable(test_d20_transitions d20_transitions.cpp)
target_sources(test_d20_transitions
PRIVATE
helper.cpp
)
target_link_libraries(test_d20_transitions
PRIVATE
iso15118
Catch2::Catch2WithMain
)
catch_discover_tests(test_d20_transitions)

View File

@@ -0,0 +1,186 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 Pionix GmbH and Contributors to EVerest
#include <catch2/catch_test_macros.hpp>
#include "helper.hpp"
#include <iso15118/d20/state/session_setup.hpp>
#include <iso15118/d20/state/supported_app_protocol.hpp>
#include <iso15118/message/supported_app_protocol.hpp>
using namespace iso15118;
SCENARIO("ISO15118-20 supported app protocol state transitions") {
// Move to helper function?
const auto evse_id = std::string("everest se");
const std::vector<message_20::datatypes::ServiceCategory> supported_energy_services = {
message_20::datatypes::ServiceCategory::DC};
const std::vector<uint16_t> vas_services{};
const auto cert_install{false};
const std::vector<message_20::datatypes::Authorization> auth_services = {message_20::datatypes::Authorization::EIM};
const d20::DcTransferLimits dc_limits;
const d20::AcTransferLimits ac_limits;
const d20::DcTransferLimits powersupply_limits;
const std::vector<d20::ControlMobilityNeedsModes> control_mobility_modes = {
{message_20::datatypes::ControlMode::Scheduled, message_20::datatypes::MobilityNeedsMode::ProvidedByEvcc}};
const std::string custom_namespace = "urn:iso:std:iso:15118:-20:AABB";
const d20::EvseSetupConfig evse_setup{
evse_id, supported_energy_services, auth_services, vas_services, cert_install, dc_limits,
ac_limits, control_mobility_modes, custom_namespace, std::nullopt, std::nullopt, powersupply_limits};
std::optional<d20::PauseContext> pause_ctx{std::nullopt};
const session::feedback::Callbacks callbacks{};
auto state_helper = FsmStateHelper(d20::SessionConfig(evse_setup), pause_ctx, callbacks);
auto ctx = state_helper.get_context();
GIVEN("Good case - DC") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::SupportedAppProtocol>()};
message_20::SupportedAppProtocolRequest req;
auto& ap = req.app_protocol.emplace_back();
ap.priority = 1;
ap.protocol_namespace = "urn:iso:std:iso:15118:-20:DC";
ap.schema_id = 1;
ap.version_number_major = 1;
ap.version_number_minor = 0;
state_helper.handle_request(req);
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::SessionSetup);
const auto response_message = ctx.get_response<message_20::SupportedAppProtocolResponse>();
REQUIRE(response_message.has_value());
const auto& supported_app_res = response_message.value();
REQUIRE(supported_app_res.response_code ==
message_20::SupportedAppProtocolResponse::ResponseCode::OK_SuccessfulNegotiation);
REQUIRE(supported_app_res.schema_id.value_or(0) == 1);
}
}
GIVEN("Good case - Custom") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::SupportedAppProtocol>()};
message_20::SupportedAppProtocolRequest req;
auto& ap = req.app_protocol.emplace_back();
ap.priority = 1;
ap.protocol_namespace = "urn:iso:std:iso:15118:-20:AABB";
ap.schema_id = 1;
ap.version_number_major = 1;
ap.version_number_minor = 0;
state_helper.handle_request(req);
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::SessionSetup);
const auto response_message = ctx.get_response<message_20::SupportedAppProtocolResponse>();
REQUIRE(response_message.has_value());
const auto& supported_app_res = response_message.value();
REQUIRE(supported_app_res.response_code ==
message_20::SupportedAppProtocolResponse::ResponseCode::OK_SuccessfulNegotiation);
REQUIRE(supported_app_res.schema_id.value_or(0) == 1);
}
}
GIVEN("Good case - Priority") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::SupportedAppProtocol>()};
message_20::SupportedAppProtocolRequest req;
auto& ap_dc = req.app_protocol.emplace_back();
ap_dc.priority = 2;
ap_dc.protocol_namespace = "urn:iso:std:iso:15118:-20:DC";
ap_dc.schema_id = 1;
ap_dc.version_number_major = 1;
ap_dc.version_number_minor = 0;
auto& ap_custom = req.app_protocol.emplace_back();
ap_custom.priority = 1;
ap_custom.protocol_namespace = "urn:iso:std:iso:15118:-20:AC";
ap_custom.schema_id = 3;
ap_custom.version_number_major = 1;
ap_custom.version_number_minor = 0;
state_helper.handle_request(req);
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::SessionSetup);
const auto response_message = ctx.get_response<message_20::SupportedAppProtocolResponse>();
REQUIRE(response_message.has_value());
const auto& supported_app_res = response_message.value();
REQUIRE(supported_app_res.response_code ==
message_20::SupportedAppProtocolResponse::ResponseCode::OK_SuccessfulNegotiation);
REQUIRE(supported_app_res.schema_id.value_or(0) == 3);
}
}
GIVEN("Bad case - Unknown protocol namespace") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::SupportedAppProtocol>()};
message_20::SupportedAppProtocolRequest req;
auto& ap = req.app_protocol.emplace_back();
ap.priority = 1;
ap.protocol_namespace = "Foobar";
ap.schema_id = 12;
ap.version_number_major = 2;
ap.version_number_minor = 11;
state_helper.handle_request(req);
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == false);
REQUIRE(fsm.get_current_state_id() == d20::StateID::SupportedAppProtocol);
const auto response_message = ctx.get_response<message_20::SupportedAppProtocolResponse>();
REQUIRE(response_message.has_value());
const auto& supported_app_res = response_message.value();
REQUIRE(supported_app_res.response_code ==
message_20::SupportedAppProtocolResponse::ResponseCode::Failed_NoNegotiation);
}
}
GIVEN("Bad case - empty app protocol") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::SupportedAppProtocol>()};
message_20::SupportedAppProtocolRequest req;
req.app_protocol.emplace_back();
state_helper.handle_request(req);
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == false);
REQUIRE(fsm.get_current_state_id() == d20::StateID::SupportedAppProtocol);
const auto response_message = ctx.get_response<message_20::SupportedAppProtocolResponse>();
REQUIRE(response_message.has_value());
const auto& supported_app_res = response_message.value();
REQUIRE(supported_app_res.response_code ==
message_20::SupportedAppProtocolResponse::ResponseCode::Failed_NoNegotiation);
}
}
}

View File

@@ -0,0 +1,7 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 Pionix GmbH and Contributors to EVerest
#include "helper.hpp"
iso15118::d20::Context& FsmStateHelper::get_context() {
return ctx;
}

View File

@@ -0,0 +1,61 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 Pionix GmbH and Contributors to EVerest
#pragma once
#include <array>
#include <iostream>
#include <optional>
#include <everest/util/fsm/fsm.hpp>
#include <iso15118/d20/config.hpp>
#include <iso15118/d20/context.hpp>
#include <iso15118/d20/control_event.hpp>
#include <iso15118/d20/states.hpp>
#include <iso15118/d20/timeout.hpp>
#include <iso15118/detail/cb_exi.hpp>
#include <iso15118/io/logging.hpp>
#include <iso15118/io/sdp.hpp>
#include <iso15118/io/stream_view.hpp>
#include <iso15118/message/variant.hpp>
#include <iso15118/session/feedback.hpp>
using namespace iso15118;
class FsmStateHelper {
public:
FsmStateHelper(const d20::SessionConfig& config, std::optional<d20::PauseContext>& pause_ctx_,
const session::feedback::Callbacks& callbacks) :
log(this), ctx(callbacks, log, config, pause_ctx_, active_control_event, msg_exch, timeouts) {
session::logging::set_session_log_callback([](std::size_t, const session::logging::Event& event) {
if (const auto* simple_event = std::get_if<session::logging::SimpleEvent>(&event)) {
printf("log(session: simple event): %s\n", simple_event->info.c_str());
} else {
printf("log(session): not decoded\n");
}
});
io::set_logging_callback([](LogLevel level, std::string message) {
printf("log(%d): %s\n", static_cast<int>(level), message.c_str());
});
};
d20::Context& get_context();
template <typename RequestType> void handle_request(const RequestType& request) {
msg_exch.set_request(std::make_unique<message_20::Variant>(request));
}
private:
std::array<uint8_t, 1024> output_buffer{};
io::StreamOutputView output_stream_view{output_buffer.data(), output_buffer.size()};
d20::MessageExchange msg_exch{output_stream_view};
std::optional<d20::ControlEvent> active_control_event;
session::SessionLogger log;
d20::Timeouts timeouts;
d20::Context ctx;
};

View File

@@ -0,0 +1,953 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2025 Pionix GmbH and Contributors to EVerest
#include <catch2/catch_test_macros.hpp>
#include "helper.hpp"
#include <iso15118/d20/state/service_detail.hpp>
#include <iso15118/d20/state/service_selection.hpp>
#include <iso15118/message/service_detail.hpp>
#include <iso15118/detail/helper.hpp>
using namespace iso15118;
SCENARIO("ISO15118-20 service detail state transitions") {
namespace dt = message_20::datatypes;
const auto evse_id = std::string("everest se");
const std::vector<dt::ServiceCategory> supported_energy_services = {dt::ServiceCategory::DC};
const auto cert_install{false};
const std::vector<uint16_t> vas_services{}; // TODO(SL): Add Custom service
const std::vector<dt::Authorization> auth_services = {dt::Authorization::EIM};
const d20::DcTransferLimits dc_limits;
const d20::AcTransferLimits ac_limits;
const d20::DcTransferLimits powersupply_limits;
const std::vector<d20::ControlMobilityNeedsModes> control_mobility_modes = {
{dt::ControlMode::Scheduled, dt::MobilityNeedsMode::ProvidedByEvcc}};
const d20::EvseSetupConfig evse_setup{
evse_id, supported_energy_services, auth_services, vas_services, cert_install, dc_limits,
ac_limits, control_mobility_modes, std::nullopt, std::nullopt, std::nullopt, powersupply_limits};
std::optional<d20::PauseContext> pause_ctx{std::nullopt};
session::feedback::Callbacks callbacks{};
callbacks.get_vas_parameters = [](uint16_t id) {
auto service_parameter_list = dt::ServiceParameterList{};
if (id == 4599) {
auto& parameter_set = service_parameter_list.emplace_back();
parameter_set.id = 0;
parameter_set.parameter.push_back({"Service1", 40});
parameter_set.parameter.push_back({"Service2", "house"});
} else if (id == message_20::to_underlying_value(dt::ServiceCategory::ParkingStatus)) {
auto& parameter_set = service_parameter_list.emplace_back();
parameter_set.id = 0;
parameter_set.parameter.push_back({"IntendedService", 1});
parameter_set.parameter.push_back({"ParkingStatusType", 4});
} else if (id == message_20::to_underlying_value(dt::ServiceCategory::Internet)) {
auto& parameter_set = service_parameter_list.emplace_back();
parameter_set.id = 3;
parameter_set.parameter.push_back({"Protocol", "http"});
parameter_set.parameter.push_back({"Port", 80});
}
return std::make_optional(service_parameter_list);
};
auto state_helper = FsmStateHelper(d20::SessionConfig(evse_setup), pause_ctx, callbacks);
auto ctx = state_helper.get_context();
ctx.session = d20::Session();
GIVEN("Bad Case - Unknown session") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::ServiceDetail>()};
const auto header_req = message_20::Header{d20::Session().get_id(), 1691411798};
const auto req =
message_20::ServiceDetailRequest{header_req, message_20::to_underlying_value(dt::ServiceCategory::DC)};
state_helper.handle_request(req);
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == false);
REQUIRE(fsm.get_current_state_id() == d20::StateID::ServiceDetail);
const auto response_message = ctx.get_response<message_20::ServiceDetailResponse>();
REQUIRE(response_message.has_value());
const auto& res = response_message.value();
REQUIRE(res.response_code == dt::ResponseCode::FAILED_UnknownSession);
REQUIRE(res.service == message_20::to_underlying_value(dt::ServiceCategory::DC));
REQUIRE(res.service_parameter_list.size() == 1);
}
}
GIVEN("Bad Case - FAILED_ServiceIDInvalid") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::ServiceDetail>()};
const auto header_req = message_20::Header{ctx.session.get_id(), 1691411798};
const auto req =
message_20::ServiceDetailRequest{header_req, message_20::to_underlying_value(dt::ServiceCategory::DC_BPT)};
state_helper.handle_request(req);
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == false);
REQUIRE(fsm.get_current_state_id() == d20::StateID::ServiceDetail);
const auto response_message = ctx.get_response<message_20::ServiceDetailResponse>();
REQUIRE(response_message.has_value());
const auto& res = response_message.value();
REQUIRE(res.response_code == dt::ResponseCode::FAILED_ServiceIDInvalid);
REQUIRE(res.service == message_20::to_underlying_value(dt::ServiceCategory::DC));
REQUIRE(res.service_parameter_list.size() == 1);
}
}
GIVEN("Good Case - DC Service") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::ServiceDetail>()};
const auto header_req = message_20::Header{ctx.session.get_id(), 1691411798};
const auto req =
message_20::ServiceDetailRequest{header_req, message_20::to_underlying_value(dt::ServiceCategory::DC)};
state_helper.handle_request(req);
ctx.session.offered_services.energy_services = {dt::ServiceCategory::DC};
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::ServiceSelection);
const auto response_message = ctx.get_response<message_20::ServiceDetailResponse>();
REQUIRE(response_message.has_value());
const auto& res = response_message.value();
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.service == message_20::to_underlying_value(dt::ServiceCategory::DC));
REQUIRE(res.service_parameter_list.size() == 1);
auto& parameters = res.service_parameter_list[0];
REQUIRE(parameters.id == 0);
REQUIRE(parameters.parameter.size() == 4);
// Connector == Extended
REQUIRE(parameters.parameter[0].name == "Connector");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[0].value));
REQUIRE(std::get<int32_t>(parameters.parameter[0].value) == 2);
// ControlMode == Scheduled
REQUIRE(parameters.parameter[1].name == "ControlMode");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[1].value));
REQUIRE(std::get<int32_t>(parameters.parameter[1].value) == 1);
// MobilityNeedsMode == ProvidedbyEvcc
REQUIRE(parameters.parameter[2].name == "MobilityNeedsMode");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[2].value));
REQUIRE(std::get<int32_t>(parameters.parameter[2].value) == 1);
// Pricing == No Pricing
REQUIRE(parameters.parameter[3].name == "Pricing");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[3].value));
REQUIRE(std::get<int32_t>(parameters.parameter[3].value) == 0);
}
}
GIVEN("Good Case - DC_BPT Service") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::ServiceDetail>()};
const auto header_req = message_20::Header{ctx.session.get_id(), 1691411798};
const auto req =
message_20::ServiceDetailRequest{header_req, message_20::to_underlying_value(dt::ServiceCategory::DC_BPT)};
state_helper.handle_request(req);
ctx.session.offered_services.energy_services = {dt::ServiceCategory::DC_BPT};
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::ServiceSelection);
const auto response_message = ctx.get_response<message_20::ServiceDetailResponse>();
REQUIRE(response_message.has_value());
const auto& res = response_message.value();
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.service == message_20::to_underlying_value(dt::ServiceCategory::DC_BPT));
REQUIRE(res.service_parameter_list.size() == 1);
auto& parameters = res.service_parameter_list[0];
REQUIRE(parameters.id == 0);
REQUIRE(parameters.parameter.size() == 6);
// Connector == Extended
REQUIRE(parameters.parameter[0].name == "Connector");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[0].value));
REQUIRE(std::get<int32_t>(parameters.parameter[0].value) == 2);
// ControlMode == Scheduled
REQUIRE(parameters.parameter[1].name == "ControlMode");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[1].value));
REQUIRE(std::get<int32_t>(parameters.parameter[1].value) == 1);
// MobilityNeedsMode == ProvidedbyEvcc
REQUIRE(parameters.parameter[2].name == "MobilityNeedsMode");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[2].value));
REQUIRE(std::get<int32_t>(parameters.parameter[2].value) == 1);
// Pricing == No Pricing
REQUIRE(parameters.parameter[3].name == "Pricing");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[3].value));
REQUIRE(std::get<int32_t>(parameters.parameter[3].value) == 0);
// BPTChannel == Unified
REQUIRE(parameters.parameter[4].name == "BPTChannel");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[4].value));
REQUIRE(std::get<int32_t>(parameters.parameter[4].value) == 1);
// GeneratorMode == GridFollowing
REQUIRE(parameters.parameter[5].name == "GeneratorMode");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[5].value));
REQUIRE(std::get<int32_t>(parameters.parameter[5].value) == 1);
}
}
GIVEN("Good Case - 2x DC Service") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::ServiceDetail>()};
const auto header_req = message_20::Header{ctx.session.get_id(), 1691411798};
const auto req =
message_20::ServiceDetailRequest{header_req, message_20::to_underlying_value(dt::ServiceCategory::DC)};
state_helper.handle_request(req);
ctx.session.offered_services.energy_services = {dt::ServiceCategory::DC};
const auto temp = ctx.session_config.dc_parameter_list;
ctx.session_config.dc_parameter_list = {{
dt::DcConnector::Extended,
dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc,
dt::Pricing::NoPricing,
},
{
dt::DcConnector::Extended,
dt::ControlMode::Dynamic,
dt::MobilityNeedsMode::ProvidedBySecc,
dt::Pricing::NoPricing,
}};
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::ServiceSelection);
const auto response_message = ctx.get_response<message_20::ServiceDetailResponse>();
REQUIRE(response_message.has_value());
const auto& res = response_message.value();
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.service == message_20::to_underlying_value(dt::ServiceCategory::DC));
REQUIRE(res.service_parameter_list.size() == 2);
auto& parameters_0 = res.service_parameter_list[0];
REQUIRE(parameters_0.id == 0);
REQUIRE(parameters_0.parameter.size() == 4);
// Connector == Extended
REQUIRE(parameters_0.parameter[0].name == "Connector");
REQUIRE(std::holds_alternative<int32_t>(parameters_0.parameter[0].value));
REQUIRE(std::get<int32_t>(parameters_0.parameter[0].value) == 2);
// ControlMode == Scheduled
REQUIRE(parameters_0.parameter[1].name == "ControlMode");
REQUIRE(std::holds_alternative<int32_t>(parameters_0.parameter[1].value));
REQUIRE(std::get<int32_t>(parameters_0.parameter[1].value) == 1);
// MobilityNeedsMode == ProvidedbyEvcc
REQUIRE(parameters_0.parameter[2].name == "MobilityNeedsMode");
REQUIRE(std::holds_alternative<int32_t>(parameters_0.parameter[2].value));
REQUIRE(std::get<int32_t>(parameters_0.parameter[2].value) == 1);
// Pricing == No Pricing
REQUIRE(parameters_0.parameter[3].name == "Pricing");
REQUIRE(std::holds_alternative<int32_t>(parameters_0.parameter[3].value));
REQUIRE(std::get<int32_t>(parameters_0.parameter[3].value) == 0);
auto& parameters_1 = res.service_parameter_list[1];
REQUIRE(parameters_1.id == 1);
REQUIRE(parameters_1.parameter.size() == 4);
// Connector == Extended
REQUIRE(parameters_1.parameter[0].name == "Connector");
REQUIRE(std::holds_alternative<int32_t>(parameters_1.parameter[0].value));
REQUIRE(std::get<int32_t>(parameters_1.parameter[0].value) == 2);
// ControlMode == Scheduled
REQUIRE(parameters_1.parameter[1].name == "ControlMode");
REQUIRE(std::holds_alternative<int32_t>(parameters_1.parameter[1].value));
REQUIRE(std::get<int32_t>(parameters_1.parameter[1].value) == 2);
// MobilityNeedsMode == ProvidedbyEvcc
REQUIRE(parameters_1.parameter[2].name == "MobilityNeedsMode");
REQUIRE(std::holds_alternative<int32_t>(parameters_1.parameter[2].value));
REQUIRE(std::get<int32_t>(parameters_1.parameter[2].value) == 2);
// Pricing == No Pricing
REQUIRE(parameters_1.parameter[3].name == "Pricing");
REQUIRE(std::holds_alternative<int32_t>(parameters_1.parameter[3].value));
REQUIRE(std::get<int32_t>(parameters_1.parameter[3].value) == 0);
}
ctx.session_config.dc_parameter_list = temp;
}
GIVEN("Good Case - DC Service: Scheduled Mode: 1, MobilityNeedsMode: 2 change to 1") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::ServiceDetail>()};
const auto header_req = message_20::Header{ctx.session.get_id(), 1691411798};
const auto req =
message_20::ServiceDetailRequest{header_req, message_20::to_underlying_value(dt::ServiceCategory::DC)};
state_helper.handle_request(req);
ctx.session.offered_services.energy_services = {dt::ServiceCategory::DC};
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::ServiceSelection);
const auto response_message = ctx.get_response<message_20::ServiceDetailResponse>();
REQUIRE(response_message.has_value());
const auto& res = response_message.value();
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.service == message_20::to_underlying_value(dt::ServiceCategory::DC));
REQUIRE(res.service_parameter_list.size() == 1);
auto& parameters = res.service_parameter_list[0];
REQUIRE(parameters.id == 0);
REQUIRE(parameters.parameter.size() == 4);
// Connector == Extended
REQUIRE(parameters.parameter[0].name == "Connector");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[0].value));
REQUIRE(std::get<int32_t>(parameters.parameter[0].value) == 2);
// ControlMode == Scheduled
REQUIRE(parameters.parameter[1].name == "ControlMode");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[1].value));
REQUIRE(std::get<int32_t>(parameters.parameter[1].value) == 1);
// MobilityNeedsMode == ProvidedbyEvcc
REQUIRE(parameters.parameter[2].name == "MobilityNeedsMode");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[2].value));
REQUIRE(std::get<int32_t>(parameters.parameter[2].value) == 1);
// Pricing == No Pricing
REQUIRE(parameters.parameter[3].name == "Pricing");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[3].value));
REQUIRE(std::get<int32_t>(parameters.parameter[3].value) == 0);
}
}
GIVEN("Good Case - Internet Service") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::ServiceDetail>()};
const auto header_req = message_20::Header{ctx.session.get_id(), 1691411798};
const auto req = message_20::ServiceDetailRequest{
header_req, message_20::to_underlying_value(dt::ServiceCategory::Internet)};
state_helper.handle_request(req);
ctx.session_config.internet_parameter_list = {
{dt::Protocol::Http, dt::Port::Port80}}; // TODO(SL): Reset to start
ctx.session.offered_services.energy_services = {dt::ServiceCategory::DC};
ctx.session.offered_services.vas_services = {message_20::to_underlying_value(dt::ServiceCategory::Internet)};
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::ServiceSelection);
const auto response_message = ctx.get_response<message_20::ServiceDetailResponse>();
REQUIRE(response_message.has_value());
const auto& res = response_message.value();
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.service == message_20::to_underlying_value(dt::ServiceCategory::Internet));
REQUIRE(res.service_parameter_list.size() == 1);
auto& parameters = res.service_parameter_list[0];
REQUIRE(parameters.id == 3);
REQUIRE(parameters.parameter.size() == 2);
// Protocol == HTTP
REQUIRE(parameters.parameter[0].name == "Protocol");
REQUIRE(std::holds_alternative<std::string>(parameters.parameter[0].value));
REQUIRE(std::get<std::string>(parameters.parameter[0].value) == "http");
// Port == 80
REQUIRE(parameters.parameter[1].name == "Port");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[1].value));
REQUIRE(std::get<int32_t>(parameters.parameter[1].value) == 80);
}
}
GIVEN("Good Case - Parking status service") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::ServiceDetail>()};
const auto header_req = message_20::Header{ctx.session.get_id(), 1691411798};
const auto req = message_20::ServiceDetailRequest{
header_req, message_20::to_underlying_value(dt::ServiceCategory::ParkingStatus)};
state_helper.handle_request(req);
ctx.session_config.parking_parameter_list = {
{dt::IntendedService::VehicleCheckIn, dt::ParkingStatus::ManualExternal}}; // TODO(SL): Reset to start value
ctx.session.offered_services.energy_services = {dt::ServiceCategory::DC};
ctx.session.offered_services.vas_services = {
message_20::to_underlying_value(dt::ServiceCategory::ParkingStatus)};
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::ServiceSelection);
const auto response_message = ctx.get_response<message_20::ServiceDetailResponse>();
REQUIRE(response_message.has_value());
const auto& res = response_message.value();
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.service == message_20::to_underlying_value(dt::ServiceCategory::ParkingStatus));
REQUIRE(res.service_parameter_list.size() == 1);
auto& parameters = res.service_parameter_list[0];
REQUIRE(parameters.id == 0);
REQUIRE(parameters.parameter.size() == 2);
// IntendedService == VehicleCheckIn
REQUIRE(parameters.parameter[0].name == "IntendedService");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[0].value));
REQUIRE(std::get<int32_t>(parameters.parameter[0].value) == 1);
// ParkingStatusType == Manual/External
REQUIRE(parameters.parameter[1].name == "ParkingStatusType");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[1].value));
REQUIRE(std::get<int32_t>(parameters.parameter[1].value) == 4);
}
}
GIVEN("Good Case - AC Service") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::ServiceDetail>()};
const auto header_req = message_20::Header{ctx.session.get_id(), 1691411798};
const auto req =
message_20::ServiceDetailRequest{header_req, message_20::to_underlying_value(dt::ServiceCategory::AC)};
state_helper.handle_request(req);
ctx.session_config.ac_parameter_list = {{
dt::AcConnector::ThreePhase,
dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc,
230,
dt::Pricing::NoPricing,
}};
ctx.session.offered_services.energy_services = {dt::ServiceCategory::AC};
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::ServiceSelection);
const auto response_message = ctx.get_response<message_20::ServiceDetailResponse>();
REQUIRE(response_message.has_value());
const auto& res = response_message.value();
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.service == message_20::to_underlying_value(dt::ServiceCategory::AC));
REQUIRE(res.service_parameter_list.size() == 1);
auto& parameters = res.service_parameter_list[0];
REQUIRE(parameters.id == 0);
REQUIRE(parameters.parameter.size() == 5);
// Connector == ThreePhases
REQUIRE(parameters.parameter[0].name == "Connector");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[0].value));
REQUIRE(std::get<int32_t>(parameters.parameter[0].value) == 3);
// ControlMode == Scheduled
REQUIRE(parameters.parameter[1].name == "ControlMode");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[1].value));
REQUIRE(std::get<int32_t>(parameters.parameter[1].value) == 1);
// EVSENominalVoltage == 230
REQUIRE(parameters.parameter[2].name == "EVSENominalVoltage");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[2].value));
REQUIRE(std::get<int32_t>(parameters.parameter[2].value) == 230);
// MobilityNeedsMode == ProvidedbyEvcc
REQUIRE(parameters.parameter[3].name == "MobilityNeedsMode");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[3].value));
REQUIRE(std::get<int32_t>(parameters.parameter[3].value) == 1);
// Pricing == No Pricing
REQUIRE(parameters.parameter[4].name == "Pricing");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[4].value));
REQUIRE(std::get<int32_t>(parameters.parameter[4].value) == 0);
}
}
GIVEN("Good Case - AC_BPT Service") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::ServiceDetail>()};
const auto header_req = message_20::Header{ctx.session.get_id(), 1691411798};
const auto req =
message_20::ServiceDetailRequest{header_req, message_20::to_underlying_value(dt::ServiceCategory::AC_BPT)};
state_helper.handle_request(req);
ctx.session.offered_services.energy_services = {dt::ServiceCategory::AC_BPT};
ctx.session_config.ac_bpt_parameter_list = {{
{
dt::AcConnector::ThreePhase,
dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc,
230,
dt::Pricing::NoPricing,
},
dt::BptChannel::Unified,
dt::GeneratorMode::GridFollowing,
dt::GridCodeIslandingDetectionMethod::Passive,
}};
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::ServiceSelection);
const auto response_message = ctx.get_response<message_20::ServiceDetailResponse>();
REQUIRE(response_message.has_value());
const auto& res = response_message.value();
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.service == message_20::to_underlying_value(dt::ServiceCategory::AC_BPT));
REQUIRE(res.service_parameter_list.size() == 1);
auto& parameters = res.service_parameter_list[0];
REQUIRE(parameters.id == 0);
REQUIRE(parameters.parameter.size() == 8);
// Connector == ThreePhases
REQUIRE(parameters.parameter[0].name == "Connector");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[0].value));
REQUIRE(std::get<int32_t>(parameters.parameter[0].value) == 3);
// ControlMode == Scheduled
REQUIRE(parameters.parameter[1].name == "ControlMode");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[1].value));
REQUIRE(std::get<int32_t>(parameters.parameter[1].value) == 1);
// EVSENominalVoltage == 230
REQUIRE(parameters.parameter[2].name == "EVSENominalVoltage");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[2].value));
REQUIRE(std::get<int32_t>(parameters.parameter[2].value) == 230);
// MobilityNeedsMode == ProvidedbyEvcc
REQUIRE(parameters.parameter[3].name == "MobilityNeedsMode");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[3].value));
REQUIRE(std::get<int32_t>(parameters.parameter[3].value) == 1);
// Pricing == No Pricing
REQUIRE(parameters.parameter[4].name == "Pricing");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[4].value));
REQUIRE(std::get<int32_t>(parameters.parameter[4].value) == 0);
// BPTChannel == Unified
REQUIRE(parameters.parameter[5].name == "BPTChannel");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[5].value));
REQUIRE(std::get<int32_t>(parameters.parameter[5].value) == 1);
// GeneratorMode == GridFollowing
REQUIRE(parameters.parameter[6].name == "GeneratorMode");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[6].value));
REQUIRE(std::get<int32_t>(parameters.parameter[6].value) == 1);
// DetectionMethodGridCodeIslanding == Passive
REQUIRE(parameters.parameter[7].name == "DetectionMethodGridCodeIslanding");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[7].value));
REQUIRE(std::get<int32_t>(parameters.parameter[7].value) == 2);
}
}
GIVEN("Good Case - 2x AC Services") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::ServiceDetail>()};
const auto header_req = message_20::Header{ctx.session.get_id(), 1691411798};
const auto req =
message_20::ServiceDetailRequest{header_req, message_20::to_underlying_value(dt::ServiceCategory::AC)};
state_helper.handle_request(req);
ctx.session.offered_services.energy_services = {dt::ServiceCategory::AC};
ctx.session_config.ac_parameter_list = {{
dt::AcConnector::ThreePhase,
dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc,
230,
dt::Pricing::NoPricing,
},
{
dt::AcConnector::ThreePhase,
dt::ControlMode::Dynamic,
dt::MobilityNeedsMode::ProvidedBySecc,
230,
dt::Pricing::NoPricing,
}};
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::ServiceSelection);
const auto response_message = ctx.get_response<message_20::ServiceDetailResponse>();
REQUIRE(response_message.has_value());
const auto& res = response_message.value();
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.service == message_20::to_underlying_value(dt::ServiceCategory::AC));
REQUIRE(res.service_parameter_list.size() == 2);
auto& parameters_0 = res.service_parameter_list[0];
REQUIRE(parameters_0.id == 0);
REQUIRE(parameters_0.parameter.size() == 5);
// Connector == ThreePhases
REQUIRE(parameters_0.parameter[0].name == "Connector");
REQUIRE(std::holds_alternative<int32_t>(parameters_0.parameter[0].value));
REQUIRE(std::get<int32_t>(parameters_0.parameter[0].value) == 3);
// ControlMode == Scheduled
REQUIRE(parameters_0.parameter[1].name == "ControlMode");
REQUIRE(std::holds_alternative<int32_t>(parameters_0.parameter[1].value));
REQUIRE(std::get<int32_t>(parameters_0.parameter[1].value) == 1);
// EVSENominalVoltage == 230
REQUIRE(parameters_0.parameter[2].name == "EVSENominalVoltage");
REQUIRE(std::holds_alternative<int32_t>(parameters_0.parameter[2].value));
REQUIRE(std::get<int32_t>(parameters_0.parameter[2].value) == 230);
// MobilityNeedsMode == ProvidedbyEvcc
REQUIRE(parameters_0.parameter[3].name == "MobilityNeedsMode");
REQUIRE(std::holds_alternative<int32_t>(parameters_0.parameter[3].value));
REQUIRE(std::get<int32_t>(parameters_0.parameter[3].value) == 1);
// Pricing == No Pricing
REQUIRE(parameters_0.parameter[4].name == "Pricing");
REQUIRE(std::holds_alternative<int32_t>(parameters_0.parameter[4].value));
REQUIRE(std::get<int32_t>(parameters_0.parameter[4].value) == 0);
auto& parameters_1 = res.service_parameter_list[1];
REQUIRE(parameters_1.id == 1);
REQUIRE(parameters_1.parameter.size() == 5);
// Connector == ThreePhases
REQUIRE(parameters_1.parameter[0].name == "Connector");
REQUIRE(std::holds_alternative<int32_t>(parameters_1.parameter[0].value));
REQUIRE(std::get<int32_t>(parameters_1.parameter[0].value) == 3);
// ControlMode == Dynamic
REQUIRE(parameters_1.parameter[1].name == "ControlMode");
REQUIRE(std::holds_alternative<int32_t>(parameters_1.parameter[1].value));
REQUIRE(std::get<int32_t>(parameters_1.parameter[1].value) == 2);
// EVSENominalVoltage == 230
REQUIRE(parameters_1.parameter[2].name == "EVSENominalVoltage");
REQUIRE(std::holds_alternative<int32_t>(parameters_1.parameter[2].value));
REQUIRE(std::get<int32_t>(parameters_1.parameter[2].value) == 230);
// MobilityNeedsMode == ProvidedbySecc
REQUIRE(parameters_1.parameter[3].name == "MobilityNeedsMode");
REQUIRE(std::holds_alternative<int32_t>(parameters_1.parameter[3].value));
REQUIRE(std::get<int32_t>(parameters_1.parameter[3].value) == 2);
// Pricing == No Pricing
REQUIRE(parameters_1.parameter[4].name == "Pricing");
REQUIRE(std::holds_alternative<int32_t>(parameters_1.parameter[4].value));
REQUIRE(std::get<int32_t>(parameters_1.parameter[4].value) == 0);
}
}
GIVEN("Good Case - AC Service: Scheduled Mode: 1, MobilityNeedsMode: 2 change to 1") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::ServiceDetail>()};
const auto header_req = message_20::Header{ctx.session.get_id(), 1691411798};
const auto req =
message_20::ServiceDetailRequest{header_req, message_20::to_underlying_value(dt::ServiceCategory::AC)};
state_helper.handle_request(req);
ctx.session.offered_services.energy_services = {dt::ServiceCategory::AC};
ctx.session_config.ac_parameter_list = {{
dt::AcConnector::ThreePhase,
dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedBySecc,
230,
dt::Pricing::NoPricing,
}};
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::ServiceSelection);
const auto response_message = ctx.get_response<message_20::ServiceDetailResponse>();
REQUIRE(response_message.has_value());
const auto& res = response_message.value();
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.service == message_20::to_underlying_value(dt::ServiceCategory::AC));
REQUIRE(res.service_parameter_list.size() == 1);
auto& parameters = res.service_parameter_list[0];
REQUIRE(parameters.id == 0);
REQUIRE(parameters.parameter.size() == 5);
// Connector == ThreePhases
REQUIRE(parameters.parameter[0].name == "Connector");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[0].value));
REQUIRE(std::get<int32_t>(parameters.parameter[0].value) == 3);
// ControlMode == Scheduled
REQUIRE(parameters.parameter[1].name == "ControlMode");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[1].value));
REQUIRE(std::get<int32_t>(parameters.parameter[1].value) == 1);
// EVSENominalVoltage == 230
REQUIRE(parameters.parameter[2].name == "EVSENominalVoltage");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[2].value));
REQUIRE(std::get<int32_t>(parameters.parameter[2].value) == 230);
// MobilityNeedsMode == ProvidedbyEvcc
REQUIRE(parameters.parameter[3].name == "MobilityNeedsMode");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[3].value));
REQUIRE(std::get<int32_t>(parameters.parameter[3].value) == 1);
// Pricing == No Pricing
REQUIRE(parameters.parameter[4].name == "Pricing");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[4].value));
REQUIRE(std::get<int32_t>(parameters.parameter[4].value) == 0);
}
}
GIVEN("Good Case - MCS Service") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::ServiceDetail>()};
const auto header_req = message_20::Header{ctx.session.get_id(), 1691411798};
const auto req =
message_20::ServiceDetailRequest{header_req, message_20::to_underlying_value(dt::ServiceCategory::MCS)};
state_helper.handle_request(req);
ctx.session.offered_services.energy_services = {dt::ServiceCategory::MCS};
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::ServiceSelection);
const auto response_message = ctx.get_response<message_20::ServiceDetailResponse>();
REQUIRE(response_message.has_value());
const auto& res = response_message.value();
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.service == message_20::to_underlying_value(dt::ServiceCategory::MCS));
REQUIRE(res.service_parameter_list.size() == 1);
auto& parameters = res.service_parameter_list[0];
REQUIRE(parameters.id == 0);
REQUIRE(parameters.parameter.size() == 4);
// Connector == MCS
REQUIRE(parameters.parameter[0].name == "Connector");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[0].value));
REQUIRE(std::get<int32_t>(parameters.parameter[0].value) == 1);
// ControlMode == Scheduled
REQUIRE(parameters.parameter[1].name == "ControlMode");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[1].value));
REQUIRE(std::get<int32_t>(parameters.parameter[1].value) == 1);
// MobilityNeedsMode == ProvidedbyEvcc
REQUIRE(parameters.parameter[2].name == "MobilityNeedsMode");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[2].value));
REQUIRE(std::get<int32_t>(parameters.parameter[2].value) == 1);
// Pricing == No Pricing
REQUIRE(parameters.parameter[3].name == "Pricing");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[3].value));
REQUIRE(std::get<int32_t>(parameters.parameter[3].value) == 0);
}
}
GIVEN("Good Case - MCS_BPT Service") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::ServiceDetail>()};
const auto header_req = message_20::Header{ctx.session.get_id(), 1691411798};
const auto req =
message_20::ServiceDetailRequest{header_req, message_20::to_underlying_value(dt::ServiceCategory::MCS_BPT)};
state_helper.handle_request(req);
ctx.session.offered_services.energy_services = {dt::ServiceCategory::MCS_BPT};
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::ServiceSelection);
const auto response_message = ctx.get_response<message_20::ServiceDetailResponse>();
REQUIRE(response_message.has_value());
const auto& res = response_message.value();
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.service == message_20::to_underlying_value(dt::ServiceCategory::MCS_BPT));
REQUIRE(res.service_parameter_list.size() == 1);
auto& parameters = res.service_parameter_list[0];
REQUIRE(parameters.id == 0);
REQUIRE(parameters.parameter.size() == 6);
// Connector == MCS
REQUIRE(parameters.parameter[0].name == "Connector");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[0].value));
REQUIRE(std::get<int32_t>(parameters.parameter[0].value) == 1);
// ControlMode == Scheduled
REQUIRE(parameters.parameter[1].name == "ControlMode");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[1].value));
REQUIRE(std::get<int32_t>(parameters.parameter[1].value) == 1);
// MobilityNeedsMode == ProvidedbyEvcc
REQUIRE(parameters.parameter[2].name == "MobilityNeedsMode");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[2].value));
REQUIRE(std::get<int32_t>(parameters.parameter[2].value) == 1);
// Pricing == No Pricing
REQUIRE(parameters.parameter[3].name == "Pricing");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[3].value));
REQUIRE(std::get<int32_t>(parameters.parameter[3].value) == 0);
// BPTChannel == Unified
REQUIRE(parameters.parameter[4].name == "BPTChannel");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[4].value));
REQUIRE(std::get<int32_t>(parameters.parameter[4].value) == 1);
// GeneratorMode == GridFollowing
REQUIRE(parameters.parameter[5].name == "GeneratorMode");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[5].value));
REQUIRE(std::get<int32_t>(parameters.parameter[5].value) == 1);
}
}
GIVEN("Good case - Ev requests parameter from custom vas") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::ServiceDetail>()};
const auto header_req = message_20::Header{ctx.session.get_id(), 1691411798};
const auto req = message_20::ServiceDetailRequest{header_req, 4599};
state_helper.handle_request(req);
ctx.session.offered_services.vas_services = {4599};
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::ServiceSelection);
const auto response_message = ctx.get_response<message_20::ServiceDetailResponse>();
REQUIRE(response_message.has_value());
const auto& service_detail_res = response_message.value();
REQUIRE(service_detail_res.response_code == dt::ResponseCode::OK);
REQUIRE(service_detail_res.service == 4599);
REQUIRE(service_detail_res.service_parameter_list.size() == 1);
auto& parameters = service_detail_res.service_parameter_list[0];
REQUIRE(parameters.id == 0);
REQUIRE(parameters.parameter.size() == 2);
REQUIRE(parameters.parameter[0].name == "Service1");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[0].value));
REQUIRE(std::get<int32_t>(parameters.parameter[0].value) == 40);
REQUIRE(parameters.parameter[1].name == "Service2");
REQUIRE(std::holds_alternative<std::string>(parameters.parameter[1].value));
REQUIRE(std::get<std::string>(parameters.parameter[1].value) == "house");
}
}
GIVEN("Good case - Ev requests parameter from parking vas") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::ServiceDetail>()};
const auto header_req = message_20::Header{ctx.session.get_id(), 1691411798};
const auto req = message_20::ServiceDetailRequest{
header_req, message_20::to_underlying_value(dt::ServiceCategory::ParkingStatus)};
state_helper.handle_request(req);
ctx.session.offered_services.vas_services = {
message_20::to_underlying_value(dt::ServiceCategory::ParkingStatus)};
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::ServiceSelection);
const auto response_message = ctx.get_response<message_20::ServiceDetailResponse>();
REQUIRE(response_message.has_value());
const auto& service_detail_res = response_message.value();
REQUIRE(service_detail_res.response_code == dt::ResponseCode::OK);
REQUIRE(service_detail_res.service == message_20::to_underlying_value(dt::ServiceCategory::ParkingStatus));
REQUIRE(service_detail_res.service_parameter_list.size() == 1);
auto& parameters = service_detail_res.service_parameter_list[0];
REQUIRE(parameters.id == 0);
REQUIRE(parameters.parameter.size() == 2);
REQUIRE(parameters.parameter[0].name == "IntendedService");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[0].value));
REQUIRE(std::get<int32_t>(parameters.parameter[0].value) == 1);
REQUIRE(parameters.parameter[1].name == "ParkingStatusType");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[1].value));
REQUIRE(std::get<int32_t>(parameters.parameter[1].value) == 4);
}
}
GIVEN("Good case - Ev requests parameter from internet vas") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::ServiceDetail>()};
const auto header_req = message_20::Header{ctx.session.get_id(), 1691411798};
const auto req = message_20::ServiceDetailRequest{
header_req, message_20::to_underlying_value(dt::ServiceCategory::Internet)};
state_helper.handle_request(req);
ctx.session.offered_services.vas_services = {message_20::to_underlying_value(dt::ServiceCategory::Internet)};
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::ServiceSelection);
const auto response_message = ctx.get_response<message_20::ServiceDetailResponse>();
REQUIRE(response_message.has_value());
const auto& service_detail_res = response_message.value();
REQUIRE(service_detail_res.response_code == dt::ResponseCode::OK);
REQUIRE(service_detail_res.service == message_20::to_underlying_value(dt::ServiceCategory::Internet));
REQUIRE(service_detail_res.service_parameter_list.size() == 1);
auto& parameters = service_detail_res.service_parameter_list[0];
REQUIRE(parameters.id == 3);
REQUIRE(parameters.parameter.size() == 2);
REQUIRE(parameters.parameter[0].name == "Protocol");
REQUIRE(std::holds_alternative<std::string>(parameters.parameter[0].value));
REQUIRE(std::get<std::string>(parameters.parameter[0].value) == "http");
REQUIRE(parameters.parameter[1].name == "Port");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[1].value));
REQUIRE(std::get<int32_t>(parameters.parameter[1].value) == 80);
}
}
}

View File

@@ -0,0 +1,224 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2025 Pionix GmbH and Contributors to EVerest
#include <catch2/catch_test_macros.hpp>
#include "helper.hpp"
#include <iso15118/d20/state/authorization_setup.hpp>
#include <iso15118/d20/state/session_setup.hpp>
#include <iso15118/message/service_detail.hpp>
#include <iso15118/message/session_setup.hpp>
using namespace iso15118;
SCENARIO("ISO15118-20 session setup state transitions") {
namespace dt = message_20::datatypes;
// Move to helper function?
const auto evse_id = std::string("everest se");
const std::vector<dt::ServiceCategory> supported_energy_services = {dt::ServiceCategory::DC};
const auto cert_install{false};
const std::vector<uint16_t> vas_services{};
const std::vector<dt::Authorization> auth_services = {dt::Authorization::EIM};
const d20::DcTransferLimits dc_limits;
const d20::AcTransferLimits ac_limits;
const d20::DcTransferLimits powersupply_limits;
const std::vector<d20::ControlMobilityNeedsModes> control_mobility_modes = {
{dt::ControlMode::Scheduled, dt::MobilityNeedsMode::ProvidedByEvcc}};
const d20::EvseSetupConfig evse_setup{
evse_id, supported_energy_services, auth_services, vas_services, cert_install, dc_limits,
ac_limits, control_mobility_modes, std::nullopt, std::nullopt, std::nullopt, powersupply_limits};
std::optional<d20::PauseContext> pause_ctx{std::nullopt};
const session::feedback::Callbacks callbacks{};
auto state_helper = FsmStateHelper(d20::SessionConfig(evse_setup), pause_ctx, callbacks);
auto ctx = state_helper.get_context();
const auto session_id = std::array<uint8_t, 8>{0x10, 0x34, 0xAB, 0x7A, 0x01, 0xF3, 0x95, 0x02};
auto& pause = pause_ctx.emplace();
pause.selected_service_parameters =
d20::SelectedServiceParameters(dt::ServiceCategory::DC, dt::DcConnector::Extended, dt::ControlMode::Dynamic,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::Pricing::NoPricing);
pause.vehicle_cert_session_id_hash = {0x58, 0xD6, 0x9A, 0x86, 0xF5, 0xCF, 0x86, 0xC0, 0x2F, 0x06, 0x1A, 0xAC, 0x9B,
0x26, 0x83, 0x0F, 0xB1, 0x66, 0xCC, 0x4E, 0xC8, 0x75, 0x68, 0xA2, 0xF2, 0x3D,
0x11, 0xC7, 0x61, 0x64, 0x18, 0x34, 0x56, 0x7A, 0x34, 0x30, 0x0C, 0x7E, 0x9A,
0xF4, 0x00, 0xF5, 0xBC, 0x61, 0x46, 0xC5, 0xA2, 0x74, 0x52, 0xFE, 0x15, 0x7D,
0x28, 0x77, 0xC8, 0x52, 0x06, 0x59, 0x22, 0x45, 0x3A, 0x9F, 0xA6, 0x54};
pause.old_session_id = session_id;
GIVEN("Good case - New session") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::SessionSetup>()};
const auto header_req = message_20::Header{{0, 0, 0, 0, 0, 0, 0, 0}, 1691411798};
const auto req = message_20::SessionSetupRequest{header_req, "WMIV1234567890ABCDEX"};
state_helper.handle_request(req);
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::AuthorizationSetup);
REQUIRE(ctx.session.get_id() != std::array<uint8_t, 8>{0});
const auto response_message = ctx.get_response<message_20::SessionSetupResponse>();
REQUIRE(response_message.has_value());
const auto& session_setup_res = response_message.value();
REQUIRE(session_setup_res.response_code == dt::ResponseCode::OK_NewSessionEstablished);
REQUIRE(session_setup_res.evseid == evse_id);
}
}
GIVEN("Good case - resume old session") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::SessionSetup>()};
ctx.set_new_vehicle_cert_hash(io::sha512_hash_t{
0x3F, 0x66, 0xE4, 0x5F, 0x3A, 0x30, 0x3B, 0x8F, 0x47, 0xCD, 0xD6, 0x86, 0xAD, 0x75, 0x13, 0x6F,
0xCE, 0x44, 0xE6, 0xAD, 0xDC, 0x52, 0x8A, 0x6A, 0x3D, 0xAC, 0x5F, 0x8D, 0xCB, 0x5A, 0x67, 0xF3,
0xE5, 0xA5, 0xF2, 0x56, 0x74, 0x5A, 0xFA, 0xF2, 0x28, 0x31, 0xCE, 0xAB, 0xE8, 0x3C, 0xD7, 0x3C,
0xF2, 0x83, 0x81, 0xAA, 0x5D, 0x87, 0x13, 0xA5, 0x78, 0xA8, 0xB4, 0xAB, 0x0D, 0x62, 0x1F, 0x83});
const auto header_req = message_20::Header{session_id, 1691411798};
const auto req = message_20::SessionSetupRequest{header_req, "WMIV1234567890ABCDEX"};
state_helper.handle_request(req);
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::DC_ChargeParameterDiscovery);
REQUIRE(ctx.session.get_id() == session_id);
const auto response_message = ctx.get_response<message_20::SessionSetupResponse>();
REQUIRE(response_message.has_value());
const auto& session_setup_res = response_message.value();
REQUIRE(session_setup_res.response_code == dt::ResponseCode::OK_OldSessionJoined);
REQUIRE(session_setup_res.evseid == evse_id);
}
pause_ctx.reset();
}
GIVEN("Try to resume old session with another session id") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::SessionSetup>()};
ctx.set_new_vehicle_cert_hash(io::sha512_hash_t{
0x3F, 0x66, 0xE4, 0x5F, 0x3A, 0x30, 0x3B, 0x8F, 0x47, 0xCD, 0xD6, 0x86, 0xAD, 0x75, 0x13, 0x6F,
0xCE, 0x44, 0xE6, 0xAD, 0xDC, 0x52, 0x8A, 0x6A, 0x3D, 0xAC, 0x5F, 0x8D, 0xCB, 0x5A, 0x67, 0xF3,
0xE5, 0xA5, 0xF2, 0x56, 0x74, 0x5A, 0xFA, 0xF2, 0x28, 0x31, 0xCE, 0xAB, 0xE8, 0x3C, 0xD7, 0x3C,
0xF2, 0x83, 0x81, 0xAA, 0x5D, 0x87, 0x13, 0xA5, 0x78, 0xA8, 0xB4, 0xAB, 0x0D, 0x62, 0x1F, 0x83});
const auto header_req = message_20::Header{{0x10, 0x34, 0xAB, 0x7B, 0x01, 0xF3, 0x95, 0x02}, 1691411798};
const auto req = message_20::SessionSetupRequest{header_req, "WMIV1234567890ABCDEX"};
state_helper.handle_request(req);
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::AuthorizationSetup);
REQUIRE(ctx.session.get_id() != session_id);
REQUIRE(ctx.session.get_id() != std::array<uint8_t, 8>{0});
const auto response_message = ctx.get_response<message_20::SessionSetupResponse>();
REQUIRE(response_message.has_value());
const auto& session_setup_res = response_message.value();
REQUIRE(session_setup_res.response_code == dt::ResponseCode::OK_NewSessionEstablished);
REQUIRE(session_setup_res.evseid == evse_id);
}
pause_ctx.reset();
}
GIVEN("Try to resume old session with no vehicle cert hash") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::SessionSetup>()};
const auto header_req = message_20::Header{session_id, 1691411798};
const auto req = message_20::SessionSetupRequest{header_req, "WMIV1234567890ABCDEX"};
state_helper.handle_request(req);
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::AuthorizationSetup);
REQUIRE(ctx.session.get_id() != session_id);
REQUIRE(ctx.session.get_id() != std::array<uint8_t, 8>{0});
const auto response_message = ctx.get_response<message_20::SessionSetupResponse>();
REQUIRE(response_message.has_value());
const auto& session_setup_res = response_message.value();
REQUIRE(session_setup_res.response_code == dt::ResponseCode::OK_NewSessionEstablished);
REQUIRE(session_setup_res.evseid == evse_id);
}
pause_ctx.reset();
}
GIVEN("Try to resume old session with different vehicle cert hash") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::SessionSetup>()};
ctx.set_new_vehicle_cert_hash(io::sha512_hash_t{
0x3F, 0x66, 0xE4, 0x5F, 0x3A, 0x30, 0x3B, 0x8F, 0x47, 0xCD, 0xD6, 0x86, 0xAD, 0x75, 0x13, 0x6F,
0xDF, 0x44, 0xE6, 0xAD, 0xDC, 0x52, 0x8A, 0x6A, 0x3D, 0xAC, 0x5F, 0x8D, 0xCB, 0x5A, 0x67, 0xF3,
0xE5, 0xA5, 0xF2, 0x56, 0x74, 0x5A, 0xFA, 0xF2, 0x28, 0x31, 0xCE, 0xAB, 0xE8, 0x3C, 0xD7, 0x3C,
0xF2, 0x83, 0x81, 0xAA, 0x5D, 0x87, 0x13, 0xA5, 0x78, 0xA8, 0xB4, 0xAB, 0x0D, 0x62, 0x1F, 0x83});
const auto header_req = message_20::Header{session_id, 1691411798};
const auto req = message_20::SessionSetupRequest{header_req, "WMIV1234567890ABCDEX"};
state_helper.handle_request(req);
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == true);
REQUIRE(fsm.get_current_state_id() == d20::StateID::AuthorizationSetup);
REQUIRE(ctx.session.get_id() != session_id);
REQUIRE(ctx.session.get_id() != std::array<uint8_t, 8>{0});
const auto response_message = ctx.get_response<message_20::SessionSetupResponse>();
REQUIRE(response_message.has_value());
const auto& session_setup_res = response_message.value();
REQUIRE(session_setup_res.response_code == dt::ResponseCode::OK_NewSessionEstablished);
REQUIRE(session_setup_res.evseid == evse_id);
}
pause_ctx.reset();
}
GIVEN("Sequence Error") {
fsm::v2::FSM<d20::StateBase> fsm{ctx.create_state<d20::state::SessionSetup>()};
const auto header_req = message_20::Header{d20::Session().get_id(), 1691411798};
const auto req =
message_20::ServiceDetailRequest{header_req, message_20::to_underlying_value(dt::ServiceCategory::DC)};
state_helper.handle_request(req);
const auto result = fsm.feed(d20::Event::V2GTP_MESSAGE);
THEN("Check state transition") {
REQUIRE(result.transitioned() == false);
REQUIRE(fsm.get_current_state_id() == d20::StateID::SessionSetup);
const auto response_message = ctx.get_response<message_20::ServiceDetailResponse>();
REQUIRE(response_message.has_value());
const auto& res = response_message.value();
REQUIRE(res.response_code == dt::ResponseCode::FAILED_SequenceError);
REQUIRE(res.service == message_20::to_underlying_value(dt::ServiceCategory::DC));
REQUIRE(res.service_parameter_list.size() == 1);
}
}
}

View File

@@ -0,0 +1,31 @@
include(Catch)
add_executable(test_logging logging.cpp)
target_link_libraries(test_logging
PRIVATE
iso15118
Catch2::Catch2WithMain
)
catch_discover_tests(test_logging)
add_executable(test_sdp_server sdp_server_test.cpp)
target_link_libraries(test_sdp_server PRIVATE iso15118 Catch2::Catch2WithMain)
catch_discover_tests(test_sdp_server)
add_executable(connection_openssl_test)
add_custom_command(
TARGET connection_openssl_test POST_BUILD
COMMAND mkdir -p ${CMAKE_CURRENT_BINARY_DIR}/pki
COMMAND cd pki && cp pki.sh ${CMAKE_CURRENT_BINARY_DIR}/pki && cp -r configs ${CMAKE_CURRENT_BINARY_DIR}/pki
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
target_sources(connection_openssl_test
PRIVATE
connection_openssl.cpp
)
target_link_libraries(connection_openssl_test
PRIVATE
iso15118::iso15118
)

View File

@@ -0,0 +1,33 @@
# Connection Test
## Standalone server
- Run `pki.sh` to build the test certificates and keys
- use `openssl s_client` to make test connections
- Run from the directory containing the test executable
### Standalone TLS server
Tests the TLS Server in isolation.
- `./connection_openssl -i <interface name>`
- gracefully terminates after 30 seconds
- requires client certificate
- Supports TCP, TLS1.2 and TLS1.3
### openssl s_client commands
TLS 1.2 and 1.3:
```sh
openssl s_client -connect [ipv6%iface]:port -verify 2 -debug -msg -verifyCAfile ./pki/certs/ca/v2g/V2G_ROOT_CA.pem -verify_return_error -cert ./pki/certs/client/vehicle/VEHICLE_LEAF.pem -cert_chain ./pki/certs/ca/vehicle/VEHICLE_CERT_CHAIN.pem -certform PEM -key ./pki/certs/client/vehicle/VEHICLE_LEAF.key -keyform PEM -pass file:./pki/certs/client/vehicle/VEHICLE_LEAF_PASSWORD.txt -ciphersuites "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256" -cipher "ECDHE-ECDSA-AES128-SHA256" -requestCAfile ./pki/certs/ca/v2g/V2G_ROOT_CA.pem
```
TLS 1.2 only:
```sh
openssl s_client -connect [ipv6%iface]:port -verify 2 -debug -msg -verifyCAfile ./pki/certs/ca/v2g/V2G_ROOT_CA.pem -verify_return_error -tls1_2 -cipher "ECDHE-ECDSA-AES128-SHA256"
```
TLS 1.3 only:
```sh
openssl s_client -connect [ipv6%iface]:port -verify 2 -debug -msg -verifyCAfile ./pki/certs/ca/v2g/V2G_ROOT_CA.pem -verify_return_error -tls1_3 -cert ./pki/certs/client/vehicle/VEHICLE_LEAF.pem -cert_chain ./pki/certs/ca/vehicle/VEHICLE_CERT_CHAIN.pem -certform PEM -key ./pki/certs/client/vehicle/VEHICLE_LEAF.key -keyform PEM -pass file:./pki/certs/client/vehicle/VEHICLE_LEAF_PASSWORD.txt -ciphersuites "TLS_AES_256_GCM_SHA384:TLS_CHACHA20_POLY1305_SHA256" -requestCAfile ./pki/certs/ca/v2g/V2G_ROOT_CA.pem
```

View File

@@ -0,0 +1,117 @@
#include <filesystem>
#include <iostream>
#include <string>
#include <unistd.h>
#include <iso15118/config.hpp>
#include <iso15118/io/connection_ssl.hpp>
#include <iso15118/io/logging.hpp>
#include <iso15118/io/poll_manager.hpp>
#include <iso15118/io/time.hpp>
namespace {
static constexpr auto DEFAULT_PW{"123456"};
static constexpr auto POLL_MANAGER_TIMEOUT_MS = 50;
static constexpr auto STOP_TIME = 30;
const char* short_opts = "hi:";
std::string interface {};
void parse_options(int argc, char** argv) {
int c{0};
while ((c = getopt(argc, argv, short_opts)) != -1) {
switch (c) {
case 'i':
interface = std::string(optarg);
break;
case 'h':
case '?':
std::cout << "Usage: " << argv[0] << " [-i]" << std::endl;
std::cout << " -i <interface name>" << std::endl;
exit(1);
default:
exit(2);
}
}
if (interface.empty()) {
std::cerr << "Error: " << argv[0] << " requires -i <interface name>" << std::endl;
exit(3);
}
}
void handle_connection_event(iso15118::io::ConnectionEvent event) {
using namespace iso15118;
using Event = io::ConnectionEvent;
switch (event) {
case Event::ACCEPTED:
std::cout << "Accepted connection" << std::endl;
return;
case Event::NEW_DATA:
std::cout << "New Data" << std::endl;
return;
case Event::OPEN:
std::cout << "Connection open" << std::endl;
return;
case Event::CLOSED:
std::cout << "Connection is closed" << std::endl;
return;
}
}
} // namespace
int main(int argc, char** argv) {
using namespace iso15118;
parse_options(argc, argv);
io::set_logging_callback([](LogLevel level, const std::string& message) {
std::cout << "log(" << static_cast<int>(level) << "): " << message << std::endl;
});
auto poll_manager = io::PollManager();
const auto interface_name = interface;
const config::SSLConfig ssl{iso15118::config::CertificateBackend::EVEREST_LAYOUT,
{},
"pki/certs/client/cso/CPO_CERT_CHAIN.pem",
"pki/certs/client/cso/SECC_LEAF.key",
DEFAULT_PW,
"pki/certs/ca/v2g/V2G_ROOT_CA.pem",
"pki/certs/ca/oem/OEM_ROOT_CA.pem",
false, // enable_ssl_logging
true, // enable_tls_key_logging
false, // enforce_tls_1_3
"/tmp"}; // tls_key_log_file_path
auto connection = io::ConnectionSSL(poll_manager, interface_name, ssl);
connection.set_event_callback([](io::ConnectionEvent event) { handle_connection_event(event); });
auto next_event = get_current_time_point();
const auto start_time_point = next_event;
while (true) {
const auto duration =
std::chrono::duration_cast<std::chrono::duration<double>>(get_current_time_point() - start_time_point);
if (duration.count() >= STOP_TIME) {
break;
}
const auto poll_timeout_ms = get_timeout_ms_until(next_event, POLL_MANAGER_TIMEOUT_MS);
poll_manager.poll(poll_timeout_ms);
next_event = offset_time_point_by_ms(get_current_time_point(), POLL_MANAGER_TIMEOUT_MS);
}
connection.close();
return 0;
}

View File

@@ -0,0 +1,188 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Pionix GmbH and Contributors to EVerest
#include <catch2/catch_test_macros.hpp>
#include <iso15118/detail/helper.hpp>
#include <iso15118/io/logging.hpp>
using namespace iso15118;
SCENARIO("Logging Tests") {
LogLevel log_level;
std::string log_msg;
io::set_logging_callback([&log_level, &log_msg](const iso15118::LogLevel& level, const std::string& msg) {
log_level = level;
log_msg = msg;
});
GIVEN("Test logf without LogLevel") {
const LogLevel expected_log_level{LogLevel::Info};
const std::string expected_msg = "TEST!";
logf("TEST!");
THEN("log_level & log_msg should be like expected") {
REQUIRE(log_level == expected_log_level);
REQUIRE(log_msg == expected_msg);
}
}
GIVEN("Test logf with LogLevel") {
const LogLevel expected_log_level{LogLevel::Warning};
const std::string expected_msg = "logf with warning";
logf(LogLevel::Warning, "logf with warning");
THEN("log_level & log_msg should be like expected") {
REQUIRE(log_level == expected_log_level);
REQUIRE(log_msg == expected_msg);
}
}
GIVEN("Test logf with arguments") {
const LogLevel expected_log_level{LogLevel::Info};
const std::string expected_msg = "logf: 5";
logf("logf: %d", 5);
THEN("log_level & log_msg should be like expected") {
REQUIRE(log_level == expected_log_level);
REQUIRE(log_msg == expected_msg);
}
}
GIVEN("Test logf_error") {
const LogLevel expected_log_level{LogLevel::Error};
const std::string expected_msg = "Test logf_error";
logf_error("Test logf_error");
THEN("log_level & log_msg should be like expected") {
REQUIRE(log_level == expected_log_level);
REQUIRE(log_msg == expected_msg);
}
}
GIVEN("Test logf_error with arguments") {
const LogLevel expected_log_level{LogLevel::Error};
const std::string expected_msg = "Test logf_error: 8";
logf_error("Test logf_error: %d", 8);
THEN("log_level & log_msg should be like expected") {
REQUIRE(log_level == expected_log_level);
REQUIRE(log_msg == expected_msg);
}
}
GIVEN("Test logf_warning") {
const LogLevel expected_log_level{LogLevel::Warning};
const std::string expected_msg = "Test logf_warning";
logf_warning("Test logf_warning");
THEN("log_level & log_msg should be like expected") {
REQUIRE(log_level == expected_log_level);
REQUIRE(log_msg == expected_msg);
}
}
GIVEN("Test logf_warning with arguments") {
const LogLevel expected_log_level{LogLevel::Warning};
const std::string expected_msg = "Test logf_warning: 4";
logf_warning("Test logf_warning: %d", 4);
THEN("log_level & log_msg should be like expected") {
REQUIRE(log_level == expected_log_level);
REQUIRE(log_msg == expected_msg);
}
}
GIVEN("Test logf_info") {
const LogLevel expected_log_level{LogLevel::Info};
const std::string expected_msg = "Test logf_info";
logf_info("Test logf_info");
THEN("log_level & log_msg should be like expected") {
REQUIRE(log_level == expected_log_level);
REQUIRE(log_msg == expected_msg);
}
}
GIVEN("Test logf_info with arguments") {
const LogLevel expected_log_level{LogLevel::Info};
const std::string expected_msg = "Test logf_info: d";
logf_info("Test logf_info: %c", 'd');
THEN("log_level & log_msg should be like expected") {
REQUIRE(log_level == expected_log_level);
REQUIRE(log_msg == expected_msg);
}
}
GIVEN("Test logf_debug") {
const LogLevel expected_log_level{LogLevel::Debug};
const std::string expected_msg = "Test logf_debug";
logf_debug("Test logf_debug");
THEN("log_level & log_msg should be like expected") {
REQUIRE(log_level == expected_log_level);
REQUIRE(log_msg == expected_msg);
}
}
GIVEN("Test logf_debug with arguments") {
const LogLevel expected_log_level{LogLevel::Debug};
const std::string expected_msg = "Test logf_debug: 23";
logf_debug("Test logf_debug: %d", 23);
THEN("log_level & log_msg should be like expected") {
REQUIRE(log_level == expected_log_level);
REQUIRE(log_msg == expected_msg);
}
}
GIVEN("Test logf_trace") {
const LogLevel expected_log_level{LogLevel::Trace};
const std::string expected_msg = "Test logf_trace";
logf_trace("Test logf_trace");
THEN("log_level & log_msg should be like expected") {
REQUIRE(log_level == expected_log_level);
REQUIRE(log_msg == expected_msg);
}
}
GIVEN("Test logf_trace with arguments") {
const LogLevel expected_log_level{LogLevel::Trace};
const std::string expected_msg = "Test logf_trace: 20000";
logf_trace("Test logf_trace: %d", 20000);
THEN("log_level & log_msg should be like expected") {
REQUIRE(log_level == expected_log_level);
REQUIRE(log_msg == expected_msg);
}
}
}

View File

@@ -0,0 +1,6 @@
*.pem
*.der
*.key
certs/
csrs/

View File

@@ -0,0 +1,15 @@
[req]
prompt = no
distinguished_name = ca_dn
[ca_dn]
commonName = UKSWI123456791A
organizationName = EVerest
countryName = DE
domainComponent = MO
[ext]
basicConstraints = critical,CA:false
keyUsage = critical,digitalSignature,nonRepudiation,keyEncipherment,keyAgreement
subjectKeyIdentifier = hash
authorityInfoAccess = OCSP;URI:https://www.example.com/, caIssuers;URI:https://www.example.com/Intermediate-CA.cer

View File

@@ -0,0 +1,15 @@
[req]
prompt = no
distinguished_name = ca_dn
[ca_dn]
commonName = CPOSubCA1
organizationName = EVerest
countryName = DE
domainComponent = V2G
[ext]
basicConstraints = critical,CA:true,pathlen:1
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash
authorityInfoAccess = OCSP;URI:https://www.example.com/, caIssuers;URI:https://www.example.com/Intermediate-CA.cer

View File

@@ -0,0 +1,15 @@
[req]
prompt = no
distinguished_name = ca_dn
[ca_dn]
commonName = CPOSubCA2
organizationName = EVerest
countryName = DE
domainComponent = V2G
[ext]
basicConstraints = critical,CA:true,pathlen:0
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash
authorityInfoAccess = OCSP;URI:https://www.example.com/, caIssuers;URI:https://www.example.com/Intermediate-CA.cer

View File

@@ -0,0 +1,15 @@
[req]
prompt = no
distinguished_name = ca_dn
[ca_dn]
commonName = CPS Leaf
organizationName = EVerest
countryName = DE
domainComponent = CPS
[ext]
basicConstraints = critical,CA:false
keyUsage = critical,digitalSignature
subjectKeyIdentifier = hash

View File

@@ -0,0 +1,15 @@
[req]
prompt = no
distinguished_name = ca_dn
[ca_dn]
commonName = ProvSubCA1
organizationName = EVerest
countryName = DE
domainComponent = CPS
[ext]
basicConstraints = critical,CA:true,pathlen:1
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash

View File

@@ -0,0 +1,15 @@
[req]
prompt = no
distinguished_name = ca_dn
[ca_dn]
commonName = ProvSubCA2
organizationName = EVerest
countryName = DE
domainComponent = CPS
[ext]
basicConstraints = critical,CA:true,pathlen:0
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash

View File

@@ -0,0 +1,16 @@
[req]
prompt = no
distinguished_name = ca_dn
[ca_dn]
commonName = MORootCA
organizationName = EVerest
countryName = DE
domainComponent = MO
[ext]
basicConstraints = critical,CA:true
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash
authorityInfoAccess = OCSP;URI:https://www.example.com/, caIssuers;URI:https://www.example.com/Intermediate-CA.cer

View File

@@ -0,0 +1,16 @@
[req]
prompt = no
distinguished_name = ca_dn
[ca_dn]
commonName = PKI-Ext_CRT_MO_SUB1_VALID
organizationName = EVerest
countryName = DE
domainComponent = MO
[ext]
basicConstraints = critical,CA:true,pathlen:1
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash
authorityInfoAccess = OCSP;URI:https://www.example.com/, caIssuers;URI:https://www.example.com/Intermediate-CA.cer

View File

@@ -0,0 +1,16 @@
[req]
prompt = no
distinguished_name = ca_dn
[ca_dn]
commonName = PKI-Ext_CRT_MO_SUB2_VALID
organizationName = EVerest
countryName = DE
domainComponent = MO
[ext]
basicConstraints = critical,CA:true,pathlen:0
keyUsage = critical,digitalSignature,nonRepudiation,keyCertSign,cRLSign
subjectKeyIdentifier = hash
authorityInfoAccess = OCSP;URI:https://www.example.com/, caIssuers;URI:https://www.example.com/Intermediate-CA.cer

View File

@@ -0,0 +1,15 @@
[req]
prompt = no
distinguished_name = ca_dn
[ca_dn]
commonName = OEMProvCert
organizationName = EVerest
countryName = DE
domainComponent = OEM
[ext]
basicConstraints = critical,CA:false
keyUsage = critical,digitalSignature,keyAgreement
subjectKeyIdentifier = hash

View File

@@ -0,0 +1,15 @@
[req]
prompt = no
distinguished_name = ca_dn
[ca_dn]
commonName = OEMRootCA
organizationName = EVerest
countryName = DE
domainComponent = OEM
[ext]
basicConstraints = critical,CA:true
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash

View File

@@ -0,0 +1,15 @@
[req]
prompt = no
distinguished_name = ca_dn
[ca_dn]
commonName = OEMSubCA1
organizationName = EVerest
countryName = DE
domainComponent = OEM
[ext]
basicConstraints = critical,CA:true,pathlen:1
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash

View File

@@ -0,0 +1,15 @@
[req]
prompt = no
distinguished_name = ca_dn
[ca_dn]
commonName = OEMSubCA2
organizationName = EVerest
countryName = DE
domainComponent = OEM
[ext]
basicConstraints = critical,CA:true,pathlen:0
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash

View File

@@ -0,0 +1,15 @@
[req]
prompt = no
distinguished_name = ca_dn
[ca_dn]
commonName = SECCCert
organizationName = EVerest
countryName = DE
domainComponent = CPO
[ext]
basicConstraints = critical,CA:false
keyUsage = critical,digitalSignature,keyAgreement
subjectKeyIdentifier = hash

View File

@@ -0,0 +1,15 @@
[req]
prompt = no
distinguished_name = ca_dn
[ca_dn]
commonName = V2GRootCA
organizationName = EVerest
countryName = DE
domainComponent = V2G
[ext]
basicConstraints = critical,CA:true
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash

View File

@@ -0,0 +1,15 @@
[req]
prompt = no
distinguished_name = ca_dn
[ca_dn]
commonName = WMIV1234567890ABCDEX
organizationName = EVerest
countryName = DE
domainComponent = OEM
[ext]
basicConstraints = critical,CA:false
keyUsage = critical,digitalSignature,keyAgreement
subjectKeyIdentifier = hash

View File

@@ -0,0 +1,15 @@
[req]
prompt = no
distinguished_name = ca_dn
[ca_dn]
commonName = VehicleSubCA1
organizationName = EVerest
countryName = DE
domainComponent = OEM
[ext]
basicConstraints = critical,CA:true,pathlen:1
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash

View File

@@ -0,0 +1,15 @@
[req]
prompt = no
distinguished_name = ca_dn
[ca_dn]
commonName = VehicleSubCA2
organizationName = EVerest
countryName = DE
domainComponent = OEM
[ext]
basicConstraints = critical,CA:true,pathlen:0
keyUsage = critical,keyCertSign,cRLSign
subjectKeyIdentifier = hash

View File

@@ -0,0 +1,128 @@
#!/bin/sh
VALIDITY_OEM_LEAF_CERT=1460
VALIDITY_OEM_SUBCA2_CERT=1460
VALIDITY_OEM_SUBCA1_CERT=1460
VALIDITY_OEM_ROOT_CERT=3650
VALIDITY_SECC_LEAF_CERT=60
VALIDITY_CPO_SUBCA1_CERT=1460
VALIDITY_CPO_SUBCA2_CERT=365
VALIDITY_V2G_ROOT_CERT=3650
VALIDITY_VEHICLE_LEAF_CERT=1460
VALIDITY_VEHICLE_SUBCA1_CERT=1460
VALIDITY_VEHICLE_SUBCA2_CERT=3650
SYMMETRIC_CIPHER=-aes-128-cbc # TODO Check correct version for ISO 15118-20
SYMMETRIC_CIPHER_PKCS12=-aes128 # TODO Check correct version for ISO 15118-20
SHA=-sha256 # TODO Check correct version for ISO 15118-20
EC_CURVE=prime256v1 # TODO Check correct version for ISO 15118-20
password=123456
echo "Password used is: '$password'"
# 0) Create directories if not yet existing
CERT_PATH=certs
CSR_PATH=csrs
CA_CSO_PATH=$CERT_PATH/ca/cso
CA_OEM_PATH=$CERT_PATH/ca/oem
CA_V2G_PATH=$CERT_PATH/ca/v2g
CA_VEHICLE_PATH=$CERT_PATH/ca/vehicle
CLIENT_CSO_PATH=$CERT_PATH/client/cso
CLIENT_OEM_PATH=$CERT_PATH/client/oem
CLIENT_V2G_PATH=$CERT_PATH/client/v2g
CLIENT_VEHICLE_PATH=$CERT_PATH/client/vehicle
mkdir -p $CERT_PATH
mkdir -p $CSR_PATH
mkdir -p $CA_CSO_PATH
mkdir -p $CA_OEM_PATH
mkdir -p $CA_V2G_PATH
mkdir -p $CA_VEHICLE_PATH
mkdir -p $CLIENT_CSO_PATH
mkdir -p $CLIENT_OEM_PATH
mkdir -p $CLIENT_V2G_PATH
mkdir -p $CLIENT_VEHICLE_PATH
# 1) Create a self-signed V2G_ROOT_CA certificate
openssl ecparam -genkey -name $EC_CURVE | openssl ec $SYMMETRIC_CIPHER -passout pass:$password -out $CLIENT_V2G_PATH/V2G_ROOT_CA.key
openssl req -new -key $CLIENT_V2G_PATH/V2G_ROOT_CA.key -passin pass:$password -config configs/v2gRootCACert.cnf -out $CSR_PATH/V2G_ROOT_CA.csr
openssl x509 -req -in $CSR_PATH/V2G_ROOT_CA.csr -extfile configs/v2gRootCACert.cnf -extensions ext -signkey $CLIENT_V2G_PATH/V2G_ROOT_CA.key -passin pass:$password $SHA -set_serial 12345 -out $CA_V2G_PATH/V2G_ROOT_CA.pem -days $VALIDITY_V2G_ROOT_CERT
# 2) Create an intermediate CPO sub-CA 1 certificate which is directly signed
# by the V2G_ROOT_CA certificate
openssl ecparam -genkey -name $EC_CURVE | openssl ec $SYMMETRIC_CIPHER -passout pass:$password -out $CLIENT_CSO_PATH/CPO_SUB_CA1.key
openssl req -new -key $CLIENT_CSO_PATH/CPO_SUB_CA1.key -passin pass:$password -config configs/cpoSubCA1Cert.cnf -out $CSR_PATH/CPO_SUB_CA1.csr
openssl x509 -req -in $CSR_PATH/CPO_SUB_CA1.csr -extfile configs/cpoSubCA1Cert.cnf -extensions ext -CA $CA_V2G_PATH/V2G_ROOT_CA.pem -CAkey $CLIENT_V2G_PATH/V2G_ROOT_CA.key -passin pass:$password -set_serial 12346 -out $CA_CSO_PATH/CPO_SUB_CA1.pem -days $VALIDITY_CPO_SUBCA1_CERT
# 3) Create a second intermediate CPO sub-CA certificate (sub-CA 2) just the way
# the previous intermedia certificate was created, which is directly signed
# by the CPO_SUB_CA1
openssl ecparam -genkey -name $EC_CURVE | openssl ec $SYMMETRIC_CIPHER -passout pass:$password -out $CLIENT_CSO_PATH/CPO_SUB_CA2.key
openssl req -new -key $CLIENT_CSO_PATH/CPO_SUB_CA2.key -passin pass:$password -config configs/cpoSubCA2Cert.cnf -out $CSR_PATH/CPO_SUB_CA2.csr
openssl x509 -req -in $CSR_PATH/CPO_SUB_CA2.csr -extfile configs/cpoSubCA2Cert.cnf -extensions ext -CA $CA_CSO_PATH/CPO_SUB_CA1.pem -CAkey $CLIENT_CSO_PATH/CPO_SUB_CA1.key -passin pass:$password -set_serial 12347 -days $VALIDITY_CPO_SUBCA2_CERT -out $CA_CSO_PATH/CPO_SUB_CA2.pem
# 4) Create an SECC certificate, which is the leaf certificate belonging to
# the charging station that authenticates itself to the EVCC during a TLS
# handshake, signed by CPO_SUB_CA2
openssl ecparam -genkey -name $EC_CURVE | openssl ec $SYMMETRIC_CIPHER -passout pass:$password -out $CLIENT_CSO_PATH/SECC_LEAF.key
openssl req -new -key $CLIENT_CSO_PATH/SECC_LEAF.key -passin pass:$password -config configs/seccLeafCert.cnf -out $CSR_PATH/SECC_LEAF.csr
openssl x509 -req -in $CSR_PATH/SECC_LEAF.csr -extfile configs/seccLeafCert.cnf -extensions ext -CA $CA_CSO_PATH/CPO_SUB_CA2.pem -CAkey $CLIENT_CSO_PATH/CPO_SUB_CA2.key -passin pass:$password -set_serial 12348 -days $VALIDITY_SECC_LEAF_CERT -out $CLIENT_CSO_PATH/SECC_LEAF.pem
cat $CLIENT_CSO_PATH/SECC_LEAF.pem $CA_CSO_PATH/CPO_SUB_CA2.pem $CA_CSO_PATH/CPO_SUB_CA1.pem > $CLIENT_CSO_PATH/CPO_CERT_CHAIN.pem
# 5) Create a self-signed OEM_ROOT_CA certificate (validity is up to the OEM)
openssl ecparam -genkey -name $EC_CURVE | openssl ec $SYMMETRIC_CIPHER -passout pass:$password -out $CLIENT_OEM_PATH/OEM_ROOT_CA.key
openssl req -new -key $CLIENT_OEM_PATH/OEM_ROOT_CA.key -passin pass:$password -config configs/oemRootCACert.cnf -out $CSR_PATH/OEM_ROOT_CA.csr
openssl x509 -req -in $CSR_PATH/OEM_ROOT_CA.csr -extfile configs/oemRootCACert.cnf -extensions ext -signkey $CLIENT_OEM_PATH/OEM_ROOT_CA.key -passin pass:$password $SHA -set_serial 12349 -out $CA_OEM_PATH/OEM_ROOT_CA.pem -days $VALIDITY_OEM_ROOT_CERT
# 6) Create an intermediate OEM sub-CA certificate, which is directly signed by
# the OEM_ROOT_CA certificate (validity is up to the OEM)
openssl ecparam -genkey -name $EC_CURVE | openssl ec $SYMMETRIC_CIPHER -passout pass:$password -out $CLIENT_OEM_PATH/OEM_SUB_CA1.key
openssl req -new -key $CLIENT_OEM_PATH/OEM_SUB_CA1.key -passin pass:$password -config configs/oemSubCA1Cert.cnf -out $CSR_PATH/OEM_SUB_CA1.csr
openssl x509 -req -in $CSR_PATH/OEM_SUB_CA1.csr -extfile configs/oemSubCA1Cert.cnf -extensions ext -CA $CA_OEM_PATH/OEM_ROOT_CA.pem -CAkey $CLIENT_OEM_PATH/OEM_ROOT_CA.key -passin pass:$password -set_serial 12350 -days $VALIDITY_OEM_SUBCA1_CERT -out $CA_OEM_PATH/OEM_SUB_CA1.pem
# 7) Create a second intermediate OEM sub-CA certificate, which is directly
# signed by the OEM_SUB_CA1 certificate (validity is up to the OEM)
openssl ecparam -genkey -name $EC_CURVE | openssl ec $SYMMETRIC_CIPHER -passout pass:$password -out $CLIENT_OEM_PATH/OEM_SUB_CA2.key
openssl req -new -key $CLIENT_OEM_PATH/OEM_SUB_CA2.key -passin pass:$password -config configs/oemSubCA2Cert.cnf -out $CSR_PATH/OEM_SUB_CA2.csr
openssl x509 -req -in $CSR_PATH/OEM_SUB_CA2.csr -extfile configs/oemSubCA2Cert.cnf -extensions ext -CA $CA_OEM_PATH/OEM_SUB_CA1.pem -CAkey $CLIENT_OEM_PATH/OEM_SUB_CA1.key -passin pass:$password -set_serial 12351 -days $VALIDITY_OEM_SUBCA2_CERT -out $CA_OEM_PATH/OEM_SUB_CA2.pem
# 8) Create an OEM provisioning certificate, which is the leaf certificate
# belonging to the OEM certificate chain (used for contract certificate
# installation)
openssl ecparam -genkey -name $EC_CURVE | openssl ec $SYMMETRIC_CIPHER -passout pass:$password -out $CLIENT_OEM_PATH/OEM_LEAF.key
openssl req -new -key $CLIENT_OEM_PATH/OEM_LEAF.key -passin pass:$password -config configs/oemLeafCert.cnf -out $CSR_PATH/OEM_LEAF.csr
openssl x509 -req -in $CSR_PATH/OEM_LEAF.csr -extfile configs/oemLeafCert.cnf -extensions ext -CA $CA_OEM_PATH/OEM_SUB_CA2.pem -CAkey $CLIENT_OEM_PATH/OEM_SUB_CA2.key -passin pass:$password -set_serial 12352 -days $VALIDITY_OEM_LEAF_CERT -out $CLIENT_OEM_PATH/OEM_LEAF.pem
cat $CLIENT_OEM_PATH/OEM_LEAF.pem $CA_OEM_PATH/OEM_SUB_CA2.pem $CA_OEM_PATH/OEM_SUB_CA1.pem > $CA_OEM_PATH/OEM_CERT_CHAIN.pem
# 16) Create an intermediate vehicle sub-CA 1 certificate which is directly signed
# by the V2GRootCA certificate
# ---------------------------------------------------------------------------
openssl ecparam -genkey -name $EC_CURVE | openssl ec $SYMMETRIC_CIPHER -passout pass:$password -out $CLIENT_VEHICLE_PATH/VEHICLE_SUB_CA1.key
openssl req -new -key $CLIENT_VEHICLE_PATH/VEHICLE_SUB_CA1.key -passin pass:$password -config configs/vehicleSubCA1Cert.cnf -out $CSR_PATH/VEHICLE_SUB_CA1.csr
openssl x509 -req -in $CSR_PATH/VEHICLE_SUB_CA1.csr -extfile configs/vehicleSubCA1Cert.cnf -extensions ext -CA $CA_V2G_PATH/V2G_ROOT_CA.pem -CAkey $CLIENT_V2G_PATH/V2G_ROOT_CA.key -passin pass:$password -set_serial 12360 -out $CA_VEHICLE_PATH/VEHICLE_SUB_CA1.pem -days $VALIDITY_VEHICLE_SUBCA1_CERT
# 17) Create a second intermediate vehicle sub-CA certificate (sub-CA 2) just the way
# the previous intermedia certificate was created, which is directly signed
# by the VEHICLE_SUB_CA1
openssl ecparam -genkey -name $EC_CURVE | openssl ec $SYMMETRIC_CIPHER -passout pass:$password -out $CLIENT_VEHICLE_PATH/VEHICLE_SUB_CA2.key
openssl req -new -key $CLIENT_VEHICLE_PATH/VEHICLE_SUB_CA2.key -passin pass:$password -config configs/vehicleSubCA2Cert.cnf -out $CSR_PATH/VEHICLE_SUB_CA2.csr
openssl x509 -req -in $CSR_PATH/VEHICLE_SUB_CA2.csr -extfile configs/vehicleSubCA2Cert.cnf -extensions ext -CA $CA_VEHICLE_PATH/VEHICLE_SUB_CA1.pem -CAkey $CLIENT_VEHICLE_PATH/VEHICLE_SUB_CA1.key -passin pass:$password -set_serial 12361 -days $VALIDITY_VEHICLE_SUBCA2_CERT -out $CA_VEHICLE_PATH/VEHICLE_SUB_CA2.pem
# 18) Create an vehicle certificate, which is the leaf certificate belonging to
# the electric vehicle that authenticates itself to the SECC during a TLS
# handshake, signed by vehicleSubCA2
openssl ecparam -genkey -name $EC_CURVE | openssl ec $SYMMETRIC_CIPHER -passout pass:$password -out $CLIENT_VEHICLE_PATH/VEHICLE_LEAF.key
openssl req -new -key $CLIENT_VEHICLE_PATH/VEHICLE_LEAF.key -passin pass:$password -config configs/vehicleLeafCert.cnf -out $CSR_PATH/VEHICLE_LEAF.csr
openssl x509 -req -in $CSR_PATH/VEHICLE_LEAF.csr -extfile configs/vehicleLeafCert.cnf -extensions ext -CA $CA_VEHICLE_PATH/VEHICLE_SUB_CA2.pem -CAkey $CLIENT_VEHICLE_PATH/VEHICLE_SUB_CA2.key -passin pass:$password -set_serial 12362 -days $VALIDITY_VEHICLE_LEAF_CERT -out $CLIENT_VEHICLE_PATH/VEHICLE_LEAF.pem
cat $CLIENT_VEHICLE_PATH/VEHICLE_LEAF.pem $CA_VEHICLE_PATH/VEHICLE_SUB_CA2.pem $CA_VEHICLE_PATH/VEHICLE_SUB_CA1.pem > $CA_VEHICLE_PATH/VEHICLE_CERT_CHAIN.pem
# 19) Place all passwords to generated private keys in separate text files.
# In this script, even though we use a single password for all certificates,
# certificates from a different source could have been generated with a different
# passphrase/passkey/password altogether. Leave them empty if no password is required.
echo $password > $CLIENT_CSO_PATH/SECC_LEAF_PASSWORD.txt
echo $password > $CLIENT_OEM_PATH/OEM_LEAF_PASSWORD.txt
echo $password > $CLIENT_V2G_PATH/V2G_ROOT_CA_PASSWORD.txt
echo $password > $CLIENT_VEHICLE_PATH/VEHICLE_LEAF_PASSWORD.txt

View File

@@ -0,0 +1,47 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2025 Pionix GmbH and Contributors to EVerest
#include <catch2/catch_test_macros.hpp>
#include <optional>
#include <thread>
#include <iso15118/io/time.hpp>
SCENARIO("V2G Communication Setup Timeout Pattern") {
GIVEN("A timeout starts when dlink becomes ready") {
std::optional<iso15118::Timeout> timeout;
timeout.emplace(100);
REQUIRE(timeout.has_value());
REQUIRE(timeout->is_reached() == false);
}
GIVEN("A timeout fires after duration expires") {
std::optional<iso15118::Timeout> timeout;
timeout.emplace(15);
std::this_thread::sleep_for(std::chrono::milliseconds(20));
REQUIRE(timeout->is_reached() == true);
}
GIVEN("A timeout is cancelled when session is established") {
std::optional<iso15118::Timeout> timeout;
timeout.emplace(100);
REQUIRE(timeout.has_value());
// Session established -> cancel timeout
timeout.reset();
REQUIRE_FALSE(timeout.has_value());
}
GIVEN("A timeout is cancelled when dlink becomes not ready") {
std::optional<iso15118::Timeout> timeout;
timeout.emplace(100);
REQUIRE(timeout.has_value());
// dlink not ready -> cancel timeout
timeout.reset();
REQUIRE_FALSE(timeout.has_value());
}
}

View File

@@ -0,0 +1,10 @@
#include <cstdio>
#include <iso15118/tbd_controller.hpp>
int main(int argc, char* argv[]) {
iso15118::TbdController controller;
controller.loop();
return 0;
}

View File

@@ -0,0 +1,11 @@
include(Catch)
add_executable(test_feedback feedback.cpp)
target_link_libraries(test_feedback
PRIVATE
iso15118
Catch2::Catch2WithMain
)
catch_discover_tests(test_feedback)

View File

@@ -0,0 +1,321 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2024 Pionix GmbH and Contributors to EVerest
#include <algorithm>
#include <catch2/catch_test_macros.hpp>
#include <iso15118/session/feedback.hpp>
using namespace iso15118::session;
namespace dt = iso15118::message_20::datatypes;
struct FeedbackResults {
feedback::Signal signal;
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;
iso15118::d20::EVInformation ev_information;
uint16_t id;
dt::VasSelectedServiceList selected_vas;
};
SCENARIO("Feedback Tests") {
FeedbackResults feedback_results;
feedback::Callbacks callbacks;
callbacks.signal = [&feedback_results](feedback::Signal signal_) { feedback_results.signal = signal_; };
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_;
};
callbacks.v2g_message = [&feedback_results](const iso15118::message_20::Type& type) {
feedback_results.v2g_message = type;
};
callbacks.evccid = [&feedback_results](const std::string& evcc_id_) { feedback_results.evcc_id = evcc_id_; };
callbacks.selected_protocol = [&feedback_results](const std::string& protocol) {
feedback_results.selected_protocol = protocol;
};
callbacks.ev_information = [&feedback_results](const iso15118::d20::EVInformation& ev_information) {
feedback_results.ev_information = ev_information;
};
callbacks.get_vas_parameters = [&feedback_results](uint16_t id) {
feedback_results.id = id;
auto service_parameter_list = dt::ServiceParameterList{};
auto& parameter_set = service_parameter_list.emplace_back();
parameter_set.id = 0;
parameter_set.parameter.push_back({"Service1", 40});
parameter_set.parameter.push_back({"Service2", "house"});
return std::make_optional(service_parameter_list);
};
callbacks.selected_vas_services = [&feedback_results](dt::VasSelectedServiceList selected_vas_) {
feedback_results.selected_vas = selected_vas_;
};
const auto feedback = Feedback(callbacks);
GIVEN("Test signal") {
const feedback::Signal expected = feedback::Signal::REQUIRE_AUTH_EIM;
feedback.signal(feedback::Signal::REQUIRE_AUTH_EIM);
THEN("signal should be like expected") {
REQUIRE(feedback_results.signal == expected);
}
}
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 = dt::BPT_Scheduled_DC_CLReqControlMode;
const BPT_ScheduleReqControlMode expected = {{
{std::nullopt, std::nullopt, std::nullopt},
{4402, -1},
{30, 0},
std::nullopt,
dt::RationalNumber{34, 0},
std::nullopt,
std::nullopt,
std::nullopt,
},
dt::RationalNumber{11, 3},
dt::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::DcReqControlMode>(&feedback_results.dc_charge_loop_req);
const auto* bpt_scheduled_control_mode = std::get_if<BPT_ScheduleReqControlMode>(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(dt::from_RationalNumber(bpt_scheduled_control_mode->target_current) ==
dt::from_RationalNumber(expected.target_current));
REQUIRE(dt::from_RationalNumber(bpt_scheduled_control_mode->target_voltage) ==
dt::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(dt::from_RationalNumber(*bpt_scheduled_control_mode->min_charge_power) ==
dt::from_RationalNumber(expected.min_charge_power.value_or(dt::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(dt::from_RationalNumber(*bpt_scheduled_control_mode->max_discharge_power) ==
dt::from_RationalNumber(expected.max_discharge_power.value_or(dt::RationalNumber{})));
REQUIRE(bpt_scheduled_control_mode->min_discharge_power.has_value() == true);
REQUIRE(dt::from_RationalNumber(*bpt_scheduled_control_mode->min_discharge_power) ==
dt::from_RationalNumber(expected.min_discharge_power.value_or(dt::RationalNumber{})));
REQUIRE(bpt_scheduled_control_mode->max_discharge_current.has_value() == false);
}
}
GIVEN("Test dc_charge_loop_req - dynamic") {
using DynamicReqControlMode = dt::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") {
const auto* dc_control_mode = std::get_if<feedback::DcReqControlMode>(&feedback_results.dc_charge_loop_req);
const auto* dynamic_control_mode = std::get_if<DynamicReqControlMode>(dc_control_mode);
REQUIRE(dynamic_control_mode->departure_time.has_value() == false);
REQUIRE(dt::from_RationalNumber(dynamic_control_mode->target_energy_request) ==
dt::from_RationalNumber(expected.target_energy_request));
REQUIRE(dt::from_RationalNumber(dynamic_control_mode->max_energy_request) ==
dt::from_RationalNumber(expected.max_energy_request));
REQUIRE(dt::from_RationalNumber(dynamic_control_mode->min_energy_request) ==
dt::from_RationalNumber(expected.min_energy_request));
REQUIRE(dt::from_RationalNumber(dynamic_control_mode->max_charge_power) ==
dt::from_RationalNumber(expected.max_charge_power));
REQUIRE(dt::from_RationalNumber(dynamic_control_mode->min_charge_power) ==
dt::from_RationalNumber(expected.min_charge_power));
REQUIRE(dt::from_RationalNumber(dynamic_control_mode->max_charge_current) ==
dt::from_RationalNumber(expected.max_charge_current));
REQUIRE(dt::from_RationalNumber(dynamic_control_mode->max_voltage) ==
dt::from_RationalNumber(expected.max_voltage));
REQUIRE(dt::from_RationalNumber(dynamic_control_mode->min_voltage) ==
dt::from_RationalNumber(expected.min_voltage));
}
}
GIVEN("Test dc_max_limits") {
const feedback::DcMaximumLimits expected{803.1, 10.0, 8031};
feedback.dc_max_limits({803.1, 10.0, 8031});
THEN("dc_max_limits should be like expected") {
REQUIRE(feedback_results.dc_max_limits.voltage == expected.voltage);
REQUIRE(feedback_results.dc_max_limits.current == expected.current);
REQUIRE(feedback_results.dc_max_limits.power == expected.power);
}
}
GIVEN("Test v2g_message") {
using Type = iso15118::message_20::Type;
const Type expected = Type::DC_CableCheckReq;
feedback.v2g_message(Type::DC_CableCheckReq);
THEN("v2g_message should be like expected") {
REQUIRE(feedback_results.v2g_message == expected);
}
}
GIVEN("Test evcc_id") {
const std::string expected = "54EA7E40B356";
feedback.evcc_id("54EA7E40B356");
THEN("evcc_id should be like expected") {
REQUIRE(feedback_results.evcc_id == expected);
}
}
GIVEN("Test selected_protocol") {
const std::string expected = "ISO15118-20:DC";
feedback.selected_protocol("ISO15118-20:DC");
THEN("selected_protocol should be like expected") {
REQUIRE(feedback_results.selected_protocol == expected);
}
}
GIVEN("Test display_parameters") {
const dt::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") {
const auto* display_parameters = std::get_if<dt::DisplayParameters>(&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::PresentVoltage>(&feedback_results.dc_charge_loop_req);
REQUIRE(dt::from_RationalNumber(*present_voltage) == dt::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::MeterInfoRequested>(&feedback_results.dc_charge_loop_req) == expected);
}
}
struct SupportedAppProtocol {
std::string protocol_namespace;
uint32_t version_number_major;
uint32_t version_number_minor;
uint8_t schema_id;
uint8_t priority;
};
// TODO(SL): Missing tests for notify_ev_charging_needs, selected_service_parameters
GIVEN("Test ev_information") {
iso15118::d20::EVInformation expected{
iso15118::d20::EVSupportedAppProtocols{{"urn:iso:std:iso:15118:-20:DC", 1, 1, 1, 1}},
{"urn:iso:std:iso:15118:-20:DC", 1, 1, 1, 1},
"54EA7E40B356",
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt};
feedback.ev_information({iso15118::d20::EVSupportedAppProtocols{{"urn:iso:std:iso:15118:-20:DC", 1, 1, 1, 1}},
{"urn:iso:std:iso:15118:-20:DC", 1, 1, 1, 1},
"54EA7E40B356",
std::nullopt,
std::nullopt,
std::nullopt,
std::nullopt});
THEN("ev_information should be like expected") {
REQUIRE(feedback_results.ev_information.ev_supported_app_protocols == expected.ev_supported_app_protocols);
REQUIRE(feedback_results.ev_information.selected_app_protocol == expected.selected_app_protocol);
REQUIRE(feedback_results.ev_information.evcc_id == expected.evcc_id);
REQUIRE(feedback_results.ev_information.ev_tls_leaf_cert == expected.ev_tls_leaf_cert);
REQUIRE(feedback_results.ev_information.ev_tls_sub_ca_1_cert == expected.ev_tls_sub_ca_1_cert);
REQUIRE(feedback_results.ev_information.ev_tls_sub_ca_2_cert == expected.ev_tls_sub_ca_2_cert);
REQUIRE(feedback_results.ev_information.ev_tls_root_cert == expected.ev_tls_root_cert);
}
}
GIVEN("Test get_vas_parameters") {
uint16_t expected{3};
const auto result = feedback.get_vas_parameters(3);
THEN("get_vas_parameters should be like expected") {
REQUIRE(result.has_value());
const auto& vas = result.value();
REQUIRE(vas.size() == 1);
auto& parameters = vas[0];
REQUIRE(parameters.id == 0);
REQUIRE(parameters.parameter.size() == 2);
REQUIRE(parameters.parameter[0].name == "Service1");
REQUIRE(std::holds_alternative<int32_t>(parameters.parameter[0].value));
REQUIRE(std::get<int32_t>(parameters.parameter[0].value) == 40);
REQUIRE(parameters.parameter[1].name == "Service2");
REQUIRE(std::holds_alternative<std::string>(parameters.parameter[1].value));
REQUIRE(std::get<std::string>(parameters.parameter[1].value) == "house");
REQUIRE(feedback_results.id == expected);
}
}
GIVEN("Test selected_vas_services") {
const dt::VasSelectedServiceList expected{{34000, 0}, {5462, 5}};
feedback.selected_vas_services(dt::VasSelectedServiceList{{34000, 0}, {5462, 5}});
THEN("meter_info_requested should be like expected") {
REQUIRE(feedback_results.selected_vas.at(0).service_id == expected.at(0).service_id);
REQUIRE(feedback_results.selected_vas.at(0).parameter_set_id == expected.at(0).parameter_set_id);
REQUIRE(feedback_results.selected_vas.at(1).service_id == expected.at(1).service_id);
REQUIRE(feedback_results.selected_vas.at(1).parameter_set_id == expected.at(1).parameter_set_id);
}
}
}

View File

@@ -0,0 +1,26 @@
include(Catch)
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(ac_charge_loop)
create_states_test_target(ac_charge_parameter_discovery)
create_states_test_target(authorization_setup)
create_states_test_target(authorization)
create_states_test_target(service_discovery)
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)

View File

@@ -0,0 +1,433 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <catch2/catch_test_macros.hpp>
#include <iso15118/detail/d20/state/ac_charge_loop.hpp>
#include <iso15118/d20/config.hpp>
using namespace iso15118;
namespace dt = message_20::datatypes;
using Scheduled_AC_Req = dt::Scheduled_AC_CLReqControlMode;
using Scheduled_BPT_AC_Req = dt::BPT_Scheduled_AC_CLReqControlMode;
using Dynamic_AC_Req = dt::Dynamic_AC_CLReqControlMode;
using Dynamic_BPT_AC_Req = dt::BPT_Dynamic_AC_CLReqControlMode;
using Scheduled_AC_Res = dt::Scheduled_AC_CLResControlMode;
using Scheduled_BPT_AC_Res = dt::BPT_Scheduled_AC_CLResControlMode;
using Dynamic_AC_Res = dt::Dynamic_AC_CLResControlMode;
using Dynamic_BPT_AC_Res = dt::BPT_Dynamic_AC_CLResControlMode;
SCENARIO("AC charge loop state handling") {
const auto evse_id = std::string("everest se");
const std::vector<dt::ServiceCategory> supported_energy_services = {dt::ServiceCategory::AC,
dt::ServiceCategory::AC_BPT};
const auto cert_install{false};
const std::vector<dt::Authorization> auth_services = {dt::Authorization::EIM};
const std::vector<uint16_t> vas_services{};
d20::DcTransferLimits dc_limits;
d20::DcTransferLimits powersupply_limits;
d20::AcTransferLimits ac_limits;
ac_limits.charge_power = {{22, 3}, {10, 0}};
ac_limits.nominal_frequency = {50, 0};
auto& discharge_limits = ac_limits.discharge_power.emplace();
discharge_limits.max = {11, 3};
discharge_limits.min = {10, 0};
const std::vector<d20::ControlMobilityNeedsModes> control_mobility_modes = {
{dt::ControlMode::Scheduled, dt::MobilityNeedsMode::ProvidedByEvcc},
{dt::ControlMode::Dynamic, dt::MobilityNeedsMode::ProvidedByEvcc},
{dt::ControlMode::Dynamic, dt::MobilityNeedsMode::ProvidedBySecc}};
const d20::EvseSetupConfig evse_setup{
evse_id, supported_energy_services, auth_services, vas_services, cert_install, dc_limits,
ac_limits, control_mobility_modes, std::nullopt, std::nullopt, std::nullopt, powersupply_limits};
GIVEN("Bad case - Unknown session") {
d20::Session session = d20::Session();
message_20::AC_ChargeLoopRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_control_mode = req.control_mode.emplace<Scheduled_AC_Req>();
req_control_mode.present_active_power = {0, 0};
req.meter_info_requested = false;
const auto res = d20::state::handle_request(req, d20::Session(), false, false, 50, d20::AcTargetPower(),
d20::AcPresentPower(), d20::UpdateDynamicModeParameters());
THEN("ResponseCode: FAILED_UnknownSession, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_UnknownSession);
REQUIRE(res.status.has_value() == false);
REQUIRE(res.meter_info.has_value() == false);
REQUIRE(res.receipt.has_value() == false);
REQUIRE(res.target_frequency.has_value() == false);
REQUIRE(std::holds_alternative<Scheduled_AC_Res>(res.control_mode));
}
}
GIVEN("Bad case - false energy mode") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::AC_BPT, dt::AcConnector::ThreePhase, dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::Pricing::NoPricing, dt::BptChannel::Unified,
dt::GeneratorMode::GridFollowing, 230, dt::GridCodeIslandingDetectionMethod::Passive);
d20::Session session = d20::Session(service_parameters);
message_20::AC_ChargeLoopRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_control_mode = req.control_mode.emplace<Scheduled_AC_Req>();
req_control_mode.present_active_power = {0, 0};
req.meter_info_requested = false;
const auto res = d20::state::handle_request(req, d20::Session(), false, false, 50, d20::AcTargetPower(),
d20::AcPresentPower(), d20::UpdateDynamicModeParameters());
THEN("ResponseCode: FAILED_UnknownSession, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_UnknownSession);
REQUIRE(res.status.has_value() == false);
REQUIRE(res.meter_info.has_value() == false);
REQUIRE(res.receipt.has_value() == false);
REQUIRE(res.target_frequency.has_value() == false);
REQUIRE(std::holds_alternative<Scheduled_AC_Res>(res.control_mode));
}
}
GIVEN("Bad case - false control mode") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::AC, dt::AcConnector::ThreePhase, dt::ControlMode::Dynamic,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::Pricing::NoPricing, 230);
d20::Session session = d20::Session(service_parameters);
message_20::AC_ChargeLoopRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_control_mode = req.control_mode.emplace<Scheduled_AC_Req>();
req_control_mode.present_active_power = {0, 0};
req.meter_info_requested = false;
const auto res = d20::state::handle_request(req, d20::Session(), false, false, 50, d20::AcTargetPower(),
d20::AcPresentPower(), d20::UpdateDynamicModeParameters());
THEN("ResponseCode: FAILED_UnknownSession, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_UnknownSession);
REQUIRE(res.status.has_value() == false);
REQUIRE(res.meter_info.has_value() == false);
REQUIRE(res.receipt.has_value() == false);
REQUIRE(res.target_frequency.has_value() == false);
REQUIRE(std::holds_alternative<Scheduled_AC_Res>(res.control_mode));
}
}
GIVEN("Good case - AC scheduled mode") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::AC, dt::AcConnector::ThreePhase, dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::Pricing::NoPricing, 230);
d20::Session session = d20::Session(service_parameters);
message_20::AC_ChargeLoopRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_control_mode = req.control_mode.emplace<Scheduled_AC_Req>();
req_control_mode.present_active_power = {11, 3};
req.meter_info_requested = false;
const auto ac_target_power = d20::AcTargetPower{};
auto ac_present_power = d20::AcPresentPower{};
ac_present_power.present_active_power = {11, 3};
const auto res = d20::state::handle_request(req, session, false, false, 50, ac_target_power, ac_present_power,
d20::UpdateDynamicModeParameters());
THEN("ResponseCode: OK, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.status.has_value() == false);
REQUIRE(res.meter_info.has_value() == false);
REQUIRE(res.receipt.has_value() == false);
REQUIRE(dt::from_RationalNumber(res.target_frequency.value_or(dt::RationalNumber{0, 0})) == 50.0f);
REQUIRE(std::holds_alternative<Scheduled_AC_Res>(res.control_mode));
const auto& res_control_mode = std::get<Scheduled_AC_Res>(res.control_mode);
REQUIRE(dt::from_RationalNumber(res_control_mode.present_active_power.value_or(dt::RationalNumber{0, 0})) ==
11000.0f);
}
}
GIVEN("Good case - AC_BPT scheduled mode") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::AC_BPT, dt::AcConnector::ThreePhase, dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::Pricing::NoPricing, dt::BptChannel::Unified,
dt::GeneratorMode::GridFollowing, 230, dt::GridCodeIslandingDetectionMethod::Passive);
d20::Session session = d20::Session(service_parameters);
message_20::AC_ChargeLoopRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_control_mode = req.control_mode.emplace<Scheduled_BPT_AC_Req>();
req_control_mode.present_active_power = {0, 0};
req.meter_info_requested = false;
const auto ac_target_power = d20::AcTargetPower{};
auto ac_present_power = d20::AcPresentPower{};
ac_present_power.present_active_power = {11, 3};
const auto res = d20::state::handle_request(req, session, false, false, 50, ac_target_power, ac_present_power,
d20::UpdateDynamicModeParameters());
THEN("ResponseCode: OK, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.status.has_value() == false);
REQUIRE(res.meter_info.has_value() == false);
REQUIRE(res.receipt.has_value() == false);
REQUIRE(dt::from_RationalNumber(res.target_frequency.value_or(dt::RationalNumber{0, 0})) == 50.0f);
REQUIRE(std::holds_alternative<Scheduled_BPT_AC_Res>(res.control_mode));
const auto& res_control_mode = std::get<Scheduled_BPT_AC_Res>(res.control_mode);
REQUIRE(dt::from_RationalNumber(res_control_mode.present_active_power.value_or(dt::RationalNumber{0, 0})) ==
11000.0f);
}
}
GIVEN("Good case - AC dynamic mode") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::AC, dt::AcConnector::ThreePhase, dt::ControlMode::Dynamic,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::Pricing::NoPricing, 230);
d20::Session session = d20::Session(service_parameters);
message_20::AC_ChargeLoopRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_control_mode = req.control_mode.emplace<Dynamic_AC_Req>();
req_control_mode.present_active_power = {11, 3};
req_control_mode.max_charge_power = {11, 3};
req_control_mode.min_charge_power = {4, 0};
req_control_mode.present_reactive_power = {10, 0};
req.meter_info_requested = false;
auto ac_target_power = d20::AcTargetPower{};
ac_target_power.target_active_power = {11, 3};
auto ac_present_power = d20::AcPresentPower{};
ac_present_power.present_active_power = {11, 3};
const auto res = d20::state::handle_request(req, session, false, false, 50, ac_target_power, ac_present_power,
d20::UpdateDynamicModeParameters());
THEN("ResponseCode: OK, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.status.has_value() == false);
REQUIRE(res.meter_info.has_value() == false);
REQUIRE(res.receipt.has_value() == false);
REQUIRE(dt::from_RationalNumber(res.target_frequency.value_or(dt::RationalNumber{0, 0})) == 50.0f);
REQUIRE(std::holds_alternative<Dynamic_AC_Res>(res.control_mode));
const auto& res_control_mode = std::get<Dynamic_AC_Res>(res.control_mode);
REQUIRE(dt::from_RationalNumber(res_control_mode.present_active_power.value_or(dt::RationalNumber{0, 0})) ==
11000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.target_active_power) == 11000.0f);
}
}
GIVEN("Good case - AC_BPT dynamic mode") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::AC_BPT, dt::AcConnector::ThreePhase, dt::ControlMode::Dynamic,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::Pricing::NoPricing, dt::BptChannel::Unified,
dt::GeneratorMode::GridFollowing, 230, dt::GridCodeIslandingDetectionMethod::Passive);
d20::Session session = d20::Session(service_parameters);
message_20::AC_ChargeLoopRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_control_mode = req.control_mode.emplace<Dynamic_BPT_AC_Req>();
req_control_mode.present_active_power = {11, 3};
req_control_mode.max_charge_power = {11, 3};
req_control_mode.min_charge_power = {4, 0};
req_control_mode.present_reactive_power = {10, 0};
req.meter_info_requested = false;
auto ac_target_power = d20::AcTargetPower{};
ac_target_power.target_active_power = {11, 3};
auto ac_present_power = d20::AcPresentPower{};
ac_present_power.present_active_power = {11, 3};
const auto res = d20::state::handle_request(req, session, false, false, 50, ac_target_power, ac_present_power,
d20::UpdateDynamicModeParameters());
THEN("ResponseCode: OK, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.status.has_value() == false);
REQUIRE(res.meter_info.has_value() == false);
REQUIRE(res.receipt.has_value() == false);
REQUIRE(dt::from_RationalNumber(res.target_frequency.value_or(dt::RationalNumber{0, 0})) == 50.0f);
REQUIRE(std::holds_alternative<Dynamic_BPT_AC_Res>(res.control_mode));
const auto& res_control_mode = std::get<Dynamic_BPT_AC_Res>(res.control_mode);
REQUIRE(dt::from_RationalNumber(res_control_mode.present_active_power.value_or(dt::RationalNumber{0, 0})) ==
11000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.target_active_power) == 11000.0f);
}
}
GIVEN("Good case - AC dynamic mode, mobility_needs_mode = 2") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::AC, dt::AcConnector::ThreePhase, dt::ControlMode::Dynamic,
dt::MobilityNeedsMode::ProvidedBySecc, dt::Pricing::NoPricing, 230);
d20::Session session = d20::Session(service_parameters);
message_20::AC_ChargeLoopRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_control_mode = req.control_mode.emplace<Dynamic_AC_Req>();
req_control_mode.present_active_power = {11, 3};
req_control_mode.max_charge_power = {11, 3};
req_control_mode.min_charge_power = {4, 0};
req_control_mode.present_reactive_power = {10, 0};
req.meter_info_requested = false;
auto ac_target_power = d20::AcTargetPower{};
ac_target_power.target_active_power = {11, 3};
auto ac_present_power = d20::AcPresentPower{};
ac_present_power.present_active_power = {11, 3};
const d20::UpdateDynamicModeParameters dynamic_parameters = {std::time(nullptr) + 40, std::nullopt, 95};
const auto res = d20::state::handle_request(req, session, false, false, 50, ac_target_power, ac_present_power,
dynamic_parameters);
THEN("ResponseCode: OK, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.status.has_value() == false);
REQUIRE(res.meter_info.has_value() == false);
REQUIRE(res.receipt.has_value() == false);
REQUIRE(dt::from_RationalNumber(res.target_frequency.value_or(dt::RationalNumber{0, 0})) == 50.0f);
REQUIRE(std::holds_alternative<Dynamic_AC_Res>(res.control_mode));
const auto& res_control_mode = std::get<Dynamic_AC_Res>(res.control_mode);
REQUIRE(dt::from_RationalNumber(res_control_mode.present_active_power.value_or(dt::RationalNumber{0, 0})) ==
11000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.target_active_power) == 11000.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);
}
}
GIVEN("Good case - AC_BPT dynamic mode, mobility_needs_mode = 2") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::AC_BPT, dt::AcConnector::ThreePhase, dt::ControlMode::Dynamic,
dt::MobilityNeedsMode::ProvidedBySecc, dt::Pricing::NoPricing, dt::BptChannel::Unified,
dt::GeneratorMode::GridFollowing, 230, dt::GridCodeIslandingDetectionMethod::Passive);
d20::Session session = d20::Session(service_parameters);
message_20::AC_ChargeLoopRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_control_mode = req.control_mode.emplace<Dynamic_BPT_AC_Req>();
req_control_mode.present_active_power = {11, 3};
req_control_mode.max_charge_power = {11, 3};
req_control_mode.min_charge_power = {4, 0};
req_control_mode.present_reactive_power = {10, 0};
req.meter_info_requested = false;
auto ac_target_power = d20::AcTargetPower{};
ac_target_power.target_active_power = {11, 3};
auto ac_present_power = d20::AcPresentPower{};
ac_present_power.present_active_power = {11, 3};
const d20::UpdateDynamicModeParameters dynamic_parameters = {std::time(nullptr) + 40, std::nullopt, 95};
const auto res = d20::state::handle_request(req, session, false, false, 50, ac_target_power, ac_present_power,
dynamic_parameters);
THEN("ResponseCode: OK, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.status.has_value() == false);
REQUIRE(res.meter_info.has_value() == false);
REQUIRE(res.receipt.has_value() == false);
REQUIRE(dt::from_RationalNumber(res.target_frequency.value_or(dt::RationalNumber{0, 0})) == 50.0f);
REQUIRE(std::holds_alternative<Dynamic_BPT_AC_Res>(res.control_mode));
const auto& res_control_mode = std::get<Dynamic_BPT_AC_Res>(res.control_mode);
REQUIRE(dt::from_RationalNumber(res_control_mode.present_active_power.value_or(dt::RationalNumber{0, 0})) ==
11000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.target_active_power) == 11000.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);
}
}
GIVEN("Good case - AC dynamic mode & pause from charger") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::AC, dt::AcConnector::ThreePhase, dt::ControlMode::Dynamic,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::Pricing::NoPricing, 230);
d20::Session session = d20::Session(service_parameters);
message_20::AC_ChargeLoopRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_control_mode = req.control_mode.emplace<Dynamic_AC_Req>();
req_control_mode.present_active_power = {11, 3};
req_control_mode.max_charge_power = {11, 3};
req_control_mode.min_charge_power = {4, 0};
req_control_mode.present_reactive_power = {10, 0};
req.meter_info_requested = false;
auto ac_target_power = d20::AcTargetPower{};
ac_target_power.target_active_power = {11, 3};
auto ac_present_power = d20::AcPresentPower{};
ac_present_power.present_active_power = {11, 3};
const auto res = d20::state::handle_request(req, session, false, true, 50, ac_target_power, ac_present_power,
d20::UpdateDynamicModeParameters());
THEN("ResponseCode: OK, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.meter_info.has_value() == false);
REQUIRE(res.receipt.has_value() == false);
REQUIRE(dt::from_RationalNumber(res.target_frequency.value_or(dt::RationalNumber{0, 0})) == 50.0f);
REQUIRE(std::holds_alternative<Dynamic_AC_Res>(res.control_mode));
const auto& res_control_mode = std::get<Dynamic_AC_Res>(res.control_mode);
REQUIRE(dt::from_RationalNumber(res_control_mode.present_active_power.value_or(dt::RationalNumber{0, 0})) ==
11000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.target_active_power) == 11000.0f);
REQUIRE(res.status.has_value() == true);
REQUIRE(res.status.value().notification == dt::EvseNotification::Pause);
REQUIRE(res.status.value().notification_max_delay == 60);
}
}
}

View File

@@ -0,0 +1,290 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 Pionix GmbH and Contributors to EVerest
#include <catch2/catch_test_macros.hpp>
#include <iso15118/detail/d20/state/ac_charge_parameter_discovery.hpp>
using namespace iso15118;
namespace dt = message_20::datatypes;
using AC_ModeReq = dt::AC_CPDReqEnergyTransferMode;
using BPT_AC_ModeReq = dt::BPT_AC_CPDReqEnergyTransferMode;
using AC_ModeRes = dt::AC_CPDResEnergyTransferMode;
using BPT_AC_ModeRes = dt::BPT_AC_CPDResEnergyTransferMode;
SCENARIO("AC charge parameter discovery state handling") {
GIVEN("Bad Case - Unknown session") {
auto session = d20::Session();
const auto evse_id = std::string("everest se");
const std::vector<dt::ServiceCategory> supported_energy_services = {dt::ServiceCategory::AC};
const auto cert_install{false};
const std::vector<dt::Authorization> auth_services = {dt::Authorization::EIM};
const std::vector<uint16_t> vas_services{};
const d20::DcTransferLimits dc_limits;
const d20::AcTransferLimits ac_limits;
const d20::DcTransferLimits powersupply_limits;
const std::vector<d20::ControlMobilityNeedsModes> control_mobility_modes = {
{dt::ControlMode::Scheduled, dt::MobilityNeedsMode::ProvidedByEvcc}};
const d20::EvseSetupConfig evse_setup{
evse_id, supported_energy_services, auth_services, vas_services, cert_install, dc_limits,
ac_limits, control_mobility_modes, std::nullopt, std::nullopt, std::nullopt, powersupply_limits};
message_20::AC_ChargeParameterDiscoveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_out = req.transfer_mode.emplace<AC_ModeReq>();
req_out.max_charge_power = {11, 3};
req_out.min_charge_power = {23, 2};
const auto res = d20::state::handle_request(req, d20::Session(), d20::SessionConfig(evse_setup).ac_limits,
iso15118::d20::AcPresentPower{});
THEN("ResponseCode: FAILED_UnknownSession, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_UnknownSession);
REQUIRE(std::holds_alternative<AC_ModeRes>(res.transfer_mode));
const auto& transfer_mode = std::get<AC_ModeRes>(res.transfer_mode);
REQUIRE(dt::from_RationalNumber(transfer_mode.max_charge_power) == 0.0f);
REQUIRE(dt::from_RationalNumber(transfer_mode.min_charge_power) == 0.0f);
REQUIRE(dt::from_RationalNumber(transfer_mode.nominal_frequency) == 0.0f);
}
}
GIVEN("Bad Case: e.g. ac transfer mod instead of ac_bpt transfer mod - FAILED_WrongChargeParameter") {
const auto service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::AC_BPT, dt::AcConnector::ThreePhase, dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::Pricing::NoPricing, dt::BptChannel::Unified,
dt::GeneratorMode::GridFollowing, 230, dt::GridCodeIslandingDetectionMethod::Passive);
const auto evse_id = std::string("everest se");
const std::vector<dt::ServiceCategory> supported_energy_services = {dt::ServiceCategory::AC};
const auto cert_install{false};
const std::vector<dt::Authorization> auth_services = {dt::Authorization::EIM};
const std::vector<uint16_t> vas_services{};
const d20::DcTransferLimits dc_limits;
const d20::AcTransferLimits ac_limits;
const d20::DcTransferLimits powersupply_limits;
const std::vector<d20::ControlMobilityNeedsModes> control_mobility_modes = {
{dt::ControlMode::Scheduled, dt::MobilityNeedsMode::ProvidedByEvcc}};
const d20::EvseSetupConfig evse_setup{
evse_id, supported_energy_services, auth_services, vas_services, cert_install, dc_limits,
ac_limits, control_mobility_modes, std::nullopt, std::nullopt, std::nullopt, powersupply_limits};
auto session = d20::Session(service_parameters);
message_20::AC_ChargeParameterDiscoveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_out = req.transfer_mode.emplace<AC_ModeReq>();
req_out.max_charge_power = {11, 3};
req_out.min_charge_power = {23, 2};
const auto res = d20::state::handle_request(req, session, d20::SessionConfig(evse_setup).ac_limits,
iso15118::d20::AcPresentPower{});
THEN("ResponseCode: FAILED_WrongChargeParameter, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_WrongChargeParameter);
REQUIRE(std::holds_alternative<AC_ModeRes>(res.transfer_mode));
const auto& transfer_mode = std::get<AC_ModeRes>(res.transfer_mode);
REQUIRE(dt::from_RationalNumber(transfer_mode.max_charge_power) == 0.0f);
REQUIRE(dt::from_RationalNumber(transfer_mode.min_charge_power) == 0.0f);
REQUIRE(dt::from_RationalNumber(transfer_mode.nominal_frequency) == 0.0f);
}
}
GIVEN("Bad Case: e.g. AC_BPT transfer mod instead of ac transfer mod - FAILED_WrongChargeParameter") {
const auto service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::AC, dt::AcConnector::ThreePhase, dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::Pricing::NoPricing, 230);
const auto evse_id = std::string("everest se");
const std::vector<dt::ServiceCategory> supported_energy_services = {dt::ServiceCategory::AC};
const auto cert_install{false};
const std::vector<dt::Authorization> auth_services = {dt::Authorization::EIM};
const std::vector<uint16_t> vas_services{};
const d20::DcTransferLimits dc_limits;
const d20::AcTransferLimits ac_limits;
const d20::DcTransferLimits powersupply_limits;
const std::vector<d20::ControlMobilityNeedsModes> control_mobility_modes = {
{dt::ControlMode::Scheduled, dt::MobilityNeedsMode::ProvidedByEvcc}};
const d20::EvseSetupConfig evse_setup{
evse_id, supported_energy_services, auth_services, vas_services, cert_install, dc_limits,
ac_limits, control_mobility_modes, std::nullopt, std::nullopt, std::nullopt, powersupply_limits};
auto session = d20::Session(service_parameters);
message_20::AC_ChargeParameterDiscoveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_out = req.transfer_mode.emplace<BPT_AC_ModeReq>();
req_out.max_charge_power = {11, 3};
req_out.min_charge_power = {23, 2};
req_out.max_discharge_power = {11, 3};
req_out.min_discharge_power = {23, 2};
const auto res = d20::state::handle_request(req, session, d20::SessionConfig(evse_setup).ac_limits,
iso15118::d20::AcPresentPower{});
THEN("ResponseCode: FAILED_WrongChargeParameter, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_WrongChargeParameter);
REQUIRE(std::holds_alternative<AC_ModeRes>(res.transfer_mode));
const auto& transfer_mode = std::get<AC_ModeRes>(res.transfer_mode);
REQUIRE(dt::from_RationalNumber(transfer_mode.max_charge_power) == 0.0f);
REQUIRE(dt::from_RationalNumber(transfer_mode.min_charge_power) == 0.0f);
REQUIRE(dt::from_RationalNumber(transfer_mode.nominal_frequency) == 0.0f);
}
}
GIVEN("Good Case: AC") {
const auto service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::AC, dt::AcConnector::ThreePhase, dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::Pricing::NoPricing, 230);
auto session = d20::Session(service_parameters);
const auto evse_id = std::string("everest se");
const std::vector<dt::ServiceCategory> supported_energy_services = {dt::ServiceCategory::AC};
const auto cert_install{false};
const std::vector<dt::Authorization> auth_services = {dt::Authorization::EIM};
const std::vector<uint16_t> vas_services{};
const d20::DcTransferLimits dc_limits;
const d20::AcTransferLimits ac_limits;
const d20::DcTransferLimits powersupply_limits;
const std::vector<d20::ControlMobilityNeedsModes> control_mobility_modes = {
{dt::ControlMode::Scheduled, dt::MobilityNeedsMode::ProvidedByEvcc}};
const d20::EvseSetupConfig evse_setup{
evse_id, supported_energy_services, auth_services, vas_services, cert_install, dc_limits,
ac_limits, control_mobility_modes, std::nullopt, std::nullopt, std::nullopt, powersupply_limits};
d20::SessionConfig config = d20::SessionConfig(evse_setup);
float max_charge_power = 11000.0f;
float min_charge_power = 11000.0f;
float nominal_frequency = 50.0f;
float power_ramp_limitation = 2.0f;
config.ac_limits.charge_power.max = dt::from_float(max_charge_power);
config.ac_limits.charge_power.min = dt::from_float(min_charge_power);
config.ac_limits.nominal_frequency = dt::from_float(nominal_frequency);
config.ac_limits.power_ramp_limitation = dt::from_float(power_ramp_limitation);
message_20::AC_ChargeParameterDiscoveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_out = req.transfer_mode.emplace<AC_ModeReq>();
req_out.max_charge_power = {11, 3};
req_out.min_charge_power = {23, 2};
const auto res = d20::state::handle_request(req, session, config.ac_limits, iso15118::d20::AcPresentPower{});
THEN("ResponseCode: OK") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(std::holds_alternative<AC_ModeRes>(res.transfer_mode));
const auto& transfer_mode = std::get<AC_ModeRes>(res.transfer_mode);
REQUIRE(dt::from_RationalNumber(transfer_mode.max_charge_power) == max_charge_power);
REQUIRE(dt::from_RationalNumber(transfer_mode.min_charge_power) == min_charge_power);
REQUIRE(dt::from_RationalNumber(transfer_mode.nominal_frequency) == nominal_frequency);
REQUIRE(transfer_mode.power_ramp_limitation.has_value() == true);
REQUIRE(dt::from_RationalNumber(transfer_mode.power_ramp_limitation.value()) == power_ramp_limitation);
}
}
GIVEN("Good Case: AC_BPT") {
const auto service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::AC_BPT, dt::AcConnector::ThreePhase, dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::Pricing::NoPricing, dt::BptChannel::Unified,
dt::GeneratorMode::GridFollowing, 230, dt::GridCodeIslandingDetectionMethod::Passive);
auto session = d20::Session(service_parameters);
const auto evse_id = std::string("everest se");
const std::vector<dt::ServiceCategory> supported_energy_services = {dt::ServiceCategory::AC};
const auto cert_install{false};
const std::vector<dt::Authorization> auth_services = {dt::Authorization::EIM};
const std::vector<uint16_t> vas_services{};
const d20::DcTransferLimits dc_limits;
const d20::AcTransferLimits ac_limits;
const d20::DcTransferLimits powersupply_limits;
const std::vector<d20::ControlMobilityNeedsModes> control_mobility_modes = {
{dt::ControlMode::Scheduled, dt::MobilityNeedsMode::ProvidedByEvcc}};
const d20::EvseSetupConfig evse_setup{
evse_id, supported_energy_services, auth_services, vas_services, cert_install, dc_limits,
ac_limits, control_mobility_modes, std::nullopt, std::nullopt, std::nullopt, powersupply_limits};
auto config = d20::SessionConfig(evse_setup);
float max_charge_power = 11000.0f;
float min_charge_power = 11000.0f;
float nominal_frequency = 50.0f;
float power_ramp_limitation = 2.0f;
float max_discharge_power = 6000.0f;
float min_discharge_power = 3000.0f;
config.ac_limits.charge_power.max = dt::from_float(max_charge_power);
config.ac_limits.charge_power.min = dt::from_float(min_charge_power);
config.ac_limits.nominal_frequency = dt::from_float(nominal_frequency);
config.ac_limits.power_ramp_limitation = dt::from_float(power_ramp_limitation);
d20::Limit<dt::RationalNumber> discharge_power;
discharge_power.max = dt::from_float(max_discharge_power);
discharge_power.min = dt::from_float(min_discharge_power);
config.ac_limits.discharge_power = discharge_power;
message_20::AC_ChargeParameterDiscoveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_out = req.transfer_mode.emplace<BPT_AC_ModeReq>();
req_out.max_charge_power = {11, 3};
req_out.min_charge_power = {23, 2};
req_out.max_discharge_power = {11, 3};
req_out.min_discharge_power = {23, 2};
const auto res = d20::state::handle_request(req, session, config.ac_limits, iso15118::d20::AcPresentPower{});
THEN("ResponseCode: OK") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(std::holds_alternative<BPT_AC_ModeRes>(res.transfer_mode));
const auto& transfer_mode = std::get<BPT_AC_ModeRes>(res.transfer_mode);
REQUIRE(dt::from_RationalNumber(transfer_mode.max_charge_power) == max_charge_power);
REQUIRE(dt::from_RationalNumber(transfer_mode.min_charge_power) == min_charge_power);
REQUIRE(dt::from_RationalNumber(transfer_mode.nominal_frequency) == nominal_frequency);
REQUIRE(transfer_mode.power_ramp_limitation.has_value() == true);
REQUIRE(dt::from_RationalNumber(transfer_mode.power_ramp_limitation.value()) == power_ramp_limitation);
REQUIRE(dt::from_RationalNumber(transfer_mode.max_discharge_power) == max_discharge_power);
REQUIRE(dt::from_RationalNumber(transfer_mode.min_discharge_power) == min_discharge_power);
}
}
GIVEN("Bad Case: EV Parameter does not fit in evse parameters - FAILED_WrongChargeParameter") {
}
// 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
}

View File

@@ -0,0 +1,124 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 Pionix GmbH and Contributors to EVerest
#include <catch2/catch_test_macros.hpp>
#include <iso15118/detail/d20/state/authorization.hpp>
using namespace iso15118;
namespace dt = message_20::datatypes;
using AuthStatus = message_20::datatypes::AuthStatus;
SCENARIO("Authorization state handling") {
GIVEN("Bad Case - Unknown session") {
d20::Session session = d20::Session();
message_20::AuthorizationRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.selected_authorization_service = dt::Authorization::EIM;
req.authorization_mode.emplace<dt::EIM_ASReqAuthorizationMode>();
const auto res = d20::state::handle_request(req, d20::Session(), AuthStatus::Pending, false);
THEN("ResponseCode: FAILED_UnknownSession, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_UnknownSession);
REQUIRE(res.evse_processing == dt::Processing::Finished);
}
}
GIVEN("Warning - Authorization selection is invalid") {
d20::Session session = d20::Session();
session.offered_services.auth_services = {dt::Authorization::PnC};
message_20::AuthorizationRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.selected_authorization_service = dt::Authorization::EIM;
req.authorization_mode.emplace<dt::EIM_ASReqAuthorizationMode>();
const auto res = d20::state::handle_request(req, session, AuthStatus::Pending, false);
THEN("ResponseCode: FAILED_UnknownSession, EvseProcessing: Finished") {
REQUIRE(res.response_code == dt::ResponseCode::WARNING_AuthorizationSelectionInvalid);
REQUIRE(res.evse_processing == dt::Processing::Finished);
}
}
// EIM test cases
GIVEN("Warning - EIM Authorization Failure") { // [V2G20-2219]
d20::Session session = d20::Session();
session.offered_services.auth_services = {dt::Authorization::EIM, dt::Authorization::PnC};
message_20::AuthorizationRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.selected_authorization_service = dt::Authorization::EIM;
req.authorization_mode.emplace<dt::EIM_ASReqAuthorizationMode>();
const auto res = d20::state::handle_request(req, session, AuthStatus::Rejected, false);
THEN("ResponseCode: WARNING_EIMAuthorizationFailure, EvseProcessing: Finished") {
REQUIRE(res.response_code == dt::ResponseCode::WARNING_EIMAuthorizationFailure);
REQUIRE(res.evse_processing == dt::Processing::Finished);
}
}
GIVEN("Good case - EIM waiting for authorization") {
d20::Session session = d20::Session();
session.offered_services.auth_services = {dt::Authorization::EIM, dt::Authorization::PnC};
message_20::AuthorizationRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.selected_authorization_service = dt::Authorization::EIM;
req.authorization_mode.emplace<dt::EIM_ASReqAuthorizationMode>();
const auto res = d20::state::handle_request(req, session, AuthStatus::Pending, false);
THEN("ResponseCode: Ok, EvseProcessing: Ongoing") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.evse_processing == dt::Processing::Ongoing);
}
}
GIVEN("Good case - EIM authorized") {
d20::Session session = d20::Session();
session.offered_services.auth_services = {dt::Authorization::EIM, dt::Authorization::PnC};
message_20::AuthorizationRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.selected_authorization_service = dt::Authorization::EIM;
req.authorization_mode.emplace<dt::EIM_ASReqAuthorizationMode>();
const auto res = d20::state::handle_request(req, session, AuthStatus::Accepted, false);
THEN("ResponseCode: Ok, EvseProcessing: Finished") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.evse_processing == dt::Processing::Finished);
}
}
// GIVEN("Bad Case - Ongoing timeout reached") {}
// PnC test cases
// GIVEN("Bad Case - sequence error") {} // TODO(sl): not here
// GIVEN("Performance Timeout") {} // TODO(sl): not here
// GIVEN("Sequence Timeout") {} // TODO(sl): not here
}

View File

@@ -0,0 +1,121 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 Pionix GmbH and Contributors to EVerest
#include <catch2/catch_test_macros.hpp>
#include <iso15118/detail/d20/state/authorization_setup.hpp>
using namespace iso15118;
namespace dt = message_20::datatypes;
SCENARIO("Authorization setup state handling") {
GIVEN("Bad Case - Unknown session") {
auto session = d20::Session();
message_20::AuthorizationSetupRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
session = d20::Session();
const auto cert_install_service = false;
const std::vector<dt::Authorization> authorization_services = {dt::Authorization::EIM};
const auto res = d20::state::handle_request(req, session, cert_install_service, authorization_services);
THEN("ResponseCode: FAILED_UnknownSession, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_UnknownSession);
REQUIRE(res.certificate_installation_service == false);
REQUIRE(res.authorization_services.size() == 1);
REQUIRE(res.authorization_services[0] == dt::Authorization::EIM);
REQUIRE(std::holds_alternative<dt::EIM_ASResAuthorizationMode>(res.authorization_mode));
}
}
GIVEN("Good Case - EIM only , cert_install_service not provided") {
auto session = d20::Session();
message_20::AuthorizationSetupRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
const auto cert_install_service = false;
const std::vector<dt::Authorization> authorization_services = {dt::Authorization::EIM};
const auto res = d20::state::handle_request(req, session, cert_install_service, authorization_services);
THEN("ResponseCode: Ok, cert_install = false, authorization_servie = EIM") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.certificate_installation_service == false);
REQUIRE(res.authorization_services.size() == 1);
REQUIRE(res.authorization_services[0] == dt::Authorization::EIM);
REQUIRE(std::holds_alternative<dt::EIM_ASResAuthorizationMode>(res.authorization_mode));
REQUIRE(session.offered_services.auth_services[0] == dt::Authorization::EIM);
}
}
GIVEN("Good Case - PnC only, cert_install_service not provided") {
auto session = d20::Session();
message_20::AuthorizationSetupRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
const auto cert_install_service = false;
const std::vector<dt::Authorization> authorization_services = {dt::Authorization::PnC};
const auto res = d20::state::handle_request(req, session, cert_install_service, authorization_services);
THEN("ResponseCode: Ok, cert_install = false, authorization_servie = PnC, authorization_mode = PnC_Mode") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.certificate_installation_service == false);
REQUIRE(res.authorization_services.size() == 1);
REQUIRE(res.authorization_services[0] == dt::Authorization::PnC);
REQUIRE(std::holds_alternative<dt::PnC_ASResAuthorizationMode>(res.authorization_mode));
const auto& auth_mode = std::get<dt::PnC_ASResAuthorizationMode>(res.authorization_mode);
REQUIRE(auth_mode.gen_challenge.empty() == false);
REQUIRE(session.offered_services.auth_services[0] == dt::Authorization::PnC);
}
}
GIVEN("Good Case - EIM + PnC, cert_install_service provided") {
auto session = d20::Session();
message_20::AuthorizationSetupRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
const auto cert_install_service = true;
const std::vector<dt::Authorization> authorization_services = {dt::Authorization::PnC, dt::Authorization::EIM};
const auto res = d20::state::handle_request(req, session, cert_install_service, authorization_services);
THEN("ResponseCode: Ok, cert_install = true, authorization_servie = EIM & PnC, authorization_mode = PnC_Mode") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.certificate_installation_service == true);
REQUIRE(res.authorization_services.size() == 2);
REQUIRE((res.authorization_services[0] == dt::Authorization::EIM ||
res.authorization_services[0] == dt::Authorization::PnC));
REQUIRE((res.authorization_services[1] == dt::Authorization::EIM ||
res.authorization_services[1] == dt::Authorization::PnC));
REQUIRE(std::holds_alternative<dt::PnC_ASResAuthorizationMode>(res.authorization_mode));
const auto& auth_mode = std::get<dt::PnC_ASResAuthorizationMode>(res.authorization_mode);
REQUIRE(auth_mode.gen_challenge.empty() == false);
REQUIRE((session.offered_services.auth_services[0] == dt::Authorization::EIM ||
session.offered_services.auth_services[0] == dt::Authorization::PnC));
REQUIRE((session.offered_services.auth_services[1] == dt::Authorization::EIM ||
session.offered_services.auth_services[1] == dt::Authorization::PnC));
}
}
// GIVEN("Bad Case - sequence error") {} // TODO(sl): not here
// GIVEN("Performance Timeout") {} // TODO(sl): not here
// GIVEN("Sequence Timeout") {} // TODO(sl): not here
}

View File

@@ -0,0 +1,64 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 Pionix GmbH and Contributors to EVerest
#include <catch2/catch_test_macros.hpp>
#include <iso15118/detail/d20/state/dc_cable_check.hpp>
using namespace iso15118;
namespace dt = message_20::datatypes;
SCENARIO("DC cable check state handling") {
GIVEN("Bad case - Unknown session") {
d20::Session session = d20::Session();
message_20::DC_CableCheckRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
const auto res = d20::state::handle_request(req, d20::Session(), false);
THEN("ResponseCode: FAILED_UnknownSession, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_UnknownSession);
REQUIRE(res.processing == dt::Processing::Ongoing);
}
}
GIVEN("Good case - ongoing ") {
d20::Session session = d20::Session();
message_20::DC_CableCheckRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
const auto res = d20::state::handle_request(req, session, false);
THEN("ResponseCode: OK, processing: Ongoing") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.processing == dt::Processing::Ongoing);
}
}
GIVEN("Good case - finished ") {
d20::Session session = d20::Session();
message_20::DC_CableCheckRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
const auto res = d20::state::handle_request(req, session, true);
THEN("ResponseCode: OK, processing: Finished") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.processing == dt::Processing::Finished);
}
}
// 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
}

View File

@@ -0,0 +1,798 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright Pionix GmbH and Contributors to EVerest
#include <catch2/catch_test_macros.hpp>
#include <iso15118/detail/d20/state/dc_charge_loop.hpp>
#include <iso15118/d20/config.hpp>
using namespace iso15118;
namespace dt = message_20::datatypes;
using Scheduled_DC_Req = message_20::datatypes::Scheduled_DC_CLReqControlMode;
using Scheduled_BPT_DC_Req = message_20::datatypes::BPT_Scheduled_DC_CLReqControlMode;
using Dynamic_DC_Req = message_20::datatypes::Dynamic_DC_CLReqControlMode;
using Dynamic_BPT_DC_Req = message_20::datatypes::BPT_Dynamic_DC_CLReqControlMode;
using Scheduled_DC_Res = message_20::datatypes::Scheduled_DC_CLResControlMode;
using Scheduled_BPT_DC_Res = message_20::datatypes::BPT_Scheduled_DC_CLResControlMode;
using Dynamic_DC_Res = message_20::datatypes::Dynamic_DC_CLResControlMode;
using Dynamic_BPT_DC_Res = message_20::datatypes::BPT_Dynamic_DC_CLResControlMode;
SCENARIO("DC charge loop state handling") {
const auto evse_id = std::string("everest se");
const std::vector<dt::ServiceCategory> supported_energy_services = {dt::ServiceCategory::DC,
dt::ServiceCategory::DC_BPT};
const auto cert_install{false};
const std::vector<dt::Authorization> auth_services = {dt::Authorization::EIM};
const std::vector<uint16_t> vas_services{};
d20::DcTransferLimits dc_limits;
d20::AcTransferLimits ac_limits;
d20::DcTransferLimits powersupply_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<d20::ControlMobilityNeedsModes> control_mobility_modes = {
{dt::ControlMode::Scheduled, dt::MobilityNeedsMode::ProvidedByEvcc},
{dt::ControlMode::Dynamic, dt::MobilityNeedsMode::ProvidedByEvcc},
{dt::ControlMode::Dynamic, dt::MobilityNeedsMode::ProvidedBySecc}};
const d20::EvseSetupConfig evse_setup{
evse_id, supported_energy_services, auth_services, vas_services, cert_install, dc_limits,
ac_limits, control_mobility_modes, std::nullopt, std::nullopt, std::nullopt, powersupply_limits};
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<Scheduled_DC_Req>();
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, false, evse_setup.dc_limits,
d20::UpdateDynamicModeParameters());
THEN("ResponseCode: FAILED_UnknownSession, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_UnknownSession);
REQUIRE(dt::from_RationalNumber(res.present_current) == 0.0f);
REQUIRE(dt::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<Scheduled_DC_Res>(res.control_mode));
}
}
GIVEN("Bad case - false energy mode") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::DC_BPT, dt::DcConnector::Extended, dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::Pricing::NoPricing, dt::BptChannel::Unified,
dt::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<Scheduled_DC_Req>();
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, false, evse_setup.dc_limits,
d20::UpdateDynamicModeParameters());
THEN("ResponseCode: FAILED, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED);
REQUIRE(dt::from_RationalNumber(res.present_current) == 0.0f);
REQUIRE(dt::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<Scheduled_DC_Res>(res.control_mode));
}
}
GIVEN("Bad case - false control mode") {
d20::SelectedServiceParameters service_parameters =
d20::SelectedServiceParameters(dt::ServiceCategory::DC, dt::DcConnector::Extended, dt::ControlMode::Dynamic,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::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<Scheduled_DC_Req>();
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, false, evse_setup.dc_limits,
d20::UpdateDynamicModeParameters());
THEN("ResponseCode: FAILED, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED);
REQUIRE(dt::from_RationalNumber(res.present_current) == 0.0f);
REQUIRE(dt::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<Scheduled_DC_Res>(res.control_mode));
}
}
GIVEN("Good case - DC scheduled mode") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::DC, dt::DcConnector::Extended, dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::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<Scheduled_DC_Req>();
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, false, evse_setup.dc_limits,
d20::UpdateDynamicModeParameters());
THEN("ResponseCode: OK, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(dt::from_RationalNumber(res.present_current) == 30.0f);
REQUIRE(dt::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<Scheduled_DC_Res>(res.control_mode));
const auto& res_control_mode = std::get<Scheduled_DC_Res>(res.control_mode);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_power.value_or(dt::RationalNumber{0, 0})) ==
22000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.min_charge_power.value_or(dt::RationalNumber{0, 0})) ==
10.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_current.value_or(dt::RationalNumber{0, 0})) ==
250.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_voltage.value_or(dt::RationalNumber{0, 0})) == 900.0f);
}
}
GIVEN("Good case - DC_BPT scheduled mode") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::DC_BPT, dt::DcConnector::Extended, dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::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<Scheduled_BPT_DC_Req>();
req_control_mode.target_current = {40, 0};
req_control_mode.target_voltage = {400, 0};
req_control_mode.max_discharge_power.emplace<dt::RationalNumber>({11, 3});
req.meter_info_requested = false;
req.present_voltage = {330, 0};
const auto res = d20::state::handle_request(req, session, 330, 30, false, false, evse_setup.dc_limits,
d20::UpdateDynamicModeParameters());
THEN("ResponseCode: OK, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(dt::from_RationalNumber(res.present_current) == 30.0f);
REQUIRE(dt::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<Scheduled_BPT_DC_Res>(res.control_mode));
const auto& res_control_mode = std::get<Scheduled_BPT_DC_Res>(res.control_mode);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_power.value_or(dt::RationalNumber{0, 0})) ==
22000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.min_charge_power.value_or(dt::RationalNumber{0, 0})) ==
10.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_current.value_or(dt::RationalNumber{0, 0})) ==
250.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_voltage.value_or(dt::RationalNumber{0, 0})) == 900.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_discharge_power.value_or(dt::RationalNumber{0, 0})) ==
11000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.min_discharge_power.value_or(dt::RationalNumber{0, 0})) ==
10.0f);
REQUIRE(dt::from_RationalNumber(
res_control_mode.max_discharge_current.value_or(dt::RationalNumber{0, 0})) == 30.0f);
}
}
GIVEN("Good case - DC dynamic mode") {
d20::SelectedServiceParameters service_parameters =
d20::SelectedServiceParameters(dt::ServiceCategory::DC, dt::DcConnector::Extended, dt::ControlMode::Dynamic,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::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<Dynamic_DC_Req>();
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, false, evse_setup.dc_limits,
d20::UpdateDynamicModeParameters());
THEN("ResponseCode: OK, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(dt::from_RationalNumber(res.present_current) == 30.0f);
REQUIRE(dt::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<Dynamic_DC_Res>(res.control_mode));
const auto& res_control_mode = std::get<Dynamic_DC_Res>(res.control_mode);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_power) == 22000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.min_charge_power) == 10.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_current) == 250.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_voltage) == 900.0f);
}
}
GIVEN("Good case - DC_BPT dynamic mode") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::DC_BPT, dt::DcConnector::Extended, dt::ControlMode::Dynamic,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::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<Dynamic_BPT_DC_Req>();
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, false, evse_setup.dc_limits,
d20::UpdateDynamicModeParameters());
THEN("ResponseCode: OK, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(dt::from_RationalNumber(res.present_current) == 30.0f);
REQUIRE(dt::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<Dynamic_BPT_DC_Res>(res.control_mode));
const auto& res_control_mode = std::get<Dynamic_BPT_DC_Res>(res.control_mode);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_power) == 22000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.min_charge_power) == 10.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_current) == 250.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_voltage) == 900.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_discharge_power) == 11000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.min_discharge_power) == 10.0f);
REQUIRE(dt::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(dt::ServiceCategory::DC, dt::DcConnector::Extended, dt::ControlMode::Dynamic,
dt::MobilityNeedsMode::ProvidedBySecc, dt::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<Dynamic_DC_Req>();
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, false, evse_setup.dc_limits, dynamic_parameters);
THEN("ResponseCode: OK, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(dt::from_RationalNumber(res.present_current) == 30.0f);
REQUIRE(dt::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<Dynamic_DC_Res>(res.control_mode));
const auto& res_control_mode = std::get<Dynamic_DC_Res>(res.control_mode);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_power) == 22000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.min_charge_power) == 10.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_current) == 250.0f);
REQUIRE(dt::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(
dt::ServiceCategory::DC_BPT, dt::DcConnector::Extended, dt::ControlMode::Dynamic,
dt::MobilityNeedsMode::ProvidedBySecc, dt::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<Dynamic_BPT_DC_Req>();
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, false, evse_setup.dc_limits, dynamic_parameters);
THEN("ResponseCode: OK, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(dt::from_RationalNumber(res.present_current) == 30.0f);
REQUIRE(dt::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<Dynamic_BPT_DC_Res>(res.control_mode));
const auto& res_control_mode = std::get<Dynamic_BPT_DC_Res>(res.control_mode);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_power) == 22000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.min_charge_power) == 10.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_current) == 250.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_voltage) == 900.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_discharge_power) == 11000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.min_discharge_power) == 10.0f);
REQUIRE(dt::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);
}
}
GIVEN("Good case - DC dynamic mode & pause from charger") {
d20::SelectedServiceParameters service_parameters =
d20::SelectedServiceParameters(dt::ServiceCategory::DC, dt::DcConnector::Extended, dt::ControlMode::Dynamic,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::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<Dynamic_DC_Req>();
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, true, evse_setup.dc_limits,
d20::UpdateDynamicModeParameters());
THEN("ResponseCode: OK, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(dt::from_RationalNumber(res.present_current) == 30.0f);
REQUIRE(dt::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<Dynamic_DC_Res>(res.control_mode));
const auto& res_control_mode = std::get<Dynamic_DC_Res>(res.control_mode);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_power) == 22000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.min_charge_power) == 10.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_current) == 250.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_voltage) == 900.0f);
REQUIRE(res.status.has_value() == true);
REQUIRE(res.status.value().notification == dt::EvseNotification::Pause);
REQUIRE(res.status.value().notification_max_delay == 60);
}
}
GIVEN("Good case - MCS scheduled mode") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::MCS, dt::DcConnector::Extended, dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::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<Scheduled_DC_Req>();
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, false, evse_setup.dc_limits,
d20::UpdateDynamicModeParameters());
THEN("ResponseCode: OK, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(dt::from_RationalNumber(res.present_current) == 30.0f);
REQUIRE(dt::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<Scheduled_DC_Res>(res.control_mode));
const auto& res_control_mode = std::get<Scheduled_DC_Res>(res.control_mode);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_power.value_or(dt::RationalNumber{0, 0})) ==
22000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.min_charge_power.value_or(dt::RationalNumber{0, 0})) ==
10.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_current.value_or(dt::RationalNumber{0, 0})) ==
250.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_voltage.value_or(dt::RationalNumber{0, 0})) == 900.0f);
}
}
GIVEN("Good case - MCS_BPT scheduled mode") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::MCS_BPT, dt::DcConnector::Extended, dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::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<Scheduled_BPT_DC_Req>();
req_control_mode.target_current = {40, 0};
req_control_mode.target_voltage = {400, 0};
req_control_mode.max_discharge_power.emplace<dt::RationalNumber>({11, 3});
req.meter_info_requested = false;
req.present_voltage = {330, 0};
const auto res = d20::state::handle_request(req, session, 330, 30, false, false, evse_setup.dc_limits,
d20::UpdateDynamicModeParameters());
THEN("ResponseCode: OK, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(dt::from_RationalNumber(res.present_current) == 30.0f);
REQUIRE(dt::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<Scheduled_BPT_DC_Res>(res.control_mode));
const auto& res_control_mode = std::get<Scheduled_BPT_DC_Res>(res.control_mode);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_power.value_or(dt::RationalNumber{0, 0})) ==
22000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.min_charge_power.value_or(dt::RationalNumber{0, 0})) ==
10.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_current.value_or(dt::RationalNumber{0, 0})) ==
250.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_voltage.value_or(dt::RationalNumber{0, 0})) == 900.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_discharge_power.value_or(dt::RationalNumber{0, 0})) ==
11000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.min_discharge_power.value_or(dt::RationalNumber{0, 0})) ==
10.0f);
REQUIRE(dt::from_RationalNumber(
res_control_mode.max_discharge_current.value_or(dt::RationalNumber{0, 0})) == 30.0f);
}
}
GIVEN("Good case - MCS dynamic mode") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::MCS, dt::DcConnector::Extended, dt::ControlMode::Dynamic,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::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<Dynamic_DC_Req>();
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, false, evse_setup.dc_limits,
d20::UpdateDynamicModeParameters());
THEN("ResponseCode: OK, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(dt::from_RationalNumber(res.present_current) == 30.0f);
REQUIRE(dt::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<Dynamic_DC_Res>(res.control_mode));
const auto& res_control_mode = std::get<Dynamic_DC_Res>(res.control_mode);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_power) == 22000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.min_charge_power) == 10.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_current) == 250.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_voltage) == 900.0f);
}
}
GIVEN("Good case - MCS_BPT dynamic mode") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::MCS_BPT, dt::DcConnector::Extended, dt::ControlMode::Dynamic,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::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<Dynamic_BPT_DC_Req>();
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, false, evse_setup.dc_limits,
d20::UpdateDynamicModeParameters());
THEN("ResponseCode: OK, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(dt::from_RationalNumber(res.present_current) == 30.0f);
REQUIRE(dt::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<Dynamic_BPT_DC_Res>(res.control_mode));
const auto& res_control_mode = std::get<Dynamic_BPT_DC_Res>(res.control_mode);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_power) == 22000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.min_charge_power) == 10.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_current) == 250.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_voltage) == 900.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_discharge_power) == 11000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.min_discharge_power) == 10.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_discharge_current) == 30.0f);
}
}
GIVEN("Good case - MCS dynamic mode, mobility_needs_mode = 2") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::MCS, dt::DcConnector::Extended, dt::ControlMode::Dynamic,
dt::MobilityNeedsMode::ProvidedBySecc, dt::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<Dynamic_DC_Req>();
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, false, evse_setup.dc_limits, dynamic_parameters);
THEN("ResponseCode: OK, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(dt::from_RationalNumber(res.present_current) == 30.0f);
REQUIRE(dt::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<Dynamic_DC_Res>(res.control_mode));
const auto& res_control_mode = std::get<Dynamic_DC_Res>(res.control_mode);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_power) == 22000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.min_charge_power) == 10.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_current) == 250.0f);
REQUIRE(dt::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 - MCS_BPT dynamic mode, mobility_needs_mode = 2") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::MCS_BPT, dt::DcConnector::Extended, dt::ControlMode::Dynamic,
dt::MobilityNeedsMode::ProvidedBySecc, dt::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<Dynamic_BPT_DC_Req>();
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, false, evse_setup.dc_limits, dynamic_parameters);
THEN("ResponseCode: OK, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(dt::from_RationalNumber(res.present_current) == 30.0f);
REQUIRE(dt::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<Dynamic_BPT_DC_Res>(res.control_mode));
const auto& res_control_mode = std::get<Dynamic_BPT_DC_Res>(res.control_mode);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_power) == 22000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.min_charge_power) == 10.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_charge_current) == 250.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_voltage) == 900.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.max_discharge_power) == 11000.0f);
REQUIRE(dt::from_RationalNumber(res_control_mode.min_discharge_power) == 10.0f);
REQUIRE(dt::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
}

View File

@@ -0,0 +1,458 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 Pionix GmbH and Contributors to EVerest
#include <catch2/catch_test_macros.hpp>
#include <iso15118/detail/d20/state/dc_charge_parameter_discovery.hpp>
using namespace iso15118;
namespace dt = message_20::datatypes;
using DC_ModeReq = message_20::datatypes::DC_CPDReqEnergyTransferMode;
using BPT_DC_ModeReq = message_20::datatypes::BPT_DC_CPDReqEnergyTransferMode;
using DC_ModeRes = message_20::datatypes::DC_CPDResEnergyTransferMode;
using BPT_DC_ModeRes = message_20::datatypes::BPT_DC_CPDResEnergyTransferMode;
SCENARIO("DC charge parameter discovery state handling") {
const auto evse_id = std::string("everest se");
const std::vector<dt::ServiceCategory> supported_energy_services = {dt::ServiceCategory::DC};
const auto cert_install{false};
const std::vector<dt::Authorization> auth_services = {dt::Authorization::EIM};
const std::vector<uint16_t> vas_services{};
const d20::DcTransferLimits dc_limits;
const d20::AcTransferLimits ac_limits;
const d20::DcTransferLimits powersupply_limits;
const std::vector<d20::ControlMobilityNeedsModes> control_mobility_modes = {
{dt::ControlMode::Scheduled, dt::MobilityNeedsMode::ProvidedByEvcc}};
const d20::EvseSetupConfig evse_setup{
evse_id, supported_energy_services, auth_services, vas_services, cert_install, dc_limits,
ac_limits, control_mobility_modes, std::nullopt, std::nullopt, std::nullopt, powersupply_limits};
GIVEN("Bad Case - Unknown session") {
d20::Session session = d20::Session();
message_20::DC_ChargeParameterDiscoveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_out = req.transfer_mode.emplace<DC_ModeReq>();
req_out.max_charge_power = {50, 3};
req_out.min_charge_power = {0, 0};
req_out.max_charge_current = {125, 0};
req_out.min_charge_current = {0, 0};
req_out.max_voltage = {400, 0};
req_out.min_voltage = {0, 0};
const auto res = d20::state::handle_request(req, d20::Session(), evse_setup.powersupply_limits);
THEN("ResponseCode: FAILED_UnknownSession, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_UnknownSession);
REQUIRE(std::holds_alternative<DC_ModeRes>(res.transfer_mode));
const auto& transfer_mode = std::get<DC_ModeRes>(res.transfer_mode);
REQUIRE(transfer_mode.max_charge_power.value == 0);
REQUIRE(transfer_mode.max_charge_power.exponent == 0);
REQUIRE(transfer_mode.min_charge_power.value == 0);
REQUIRE(transfer_mode.min_charge_power.exponent == 0);
REQUIRE(transfer_mode.max_charge_current.value == 0);
REQUIRE(transfer_mode.max_charge_current.exponent == 0);
REQUIRE(transfer_mode.min_charge_current.value == 0);
REQUIRE(transfer_mode.min_charge_current.exponent == 0);
REQUIRE(transfer_mode.max_voltage.value == 0);
REQUIRE(transfer_mode.max_voltage.exponent == 0);
REQUIRE(transfer_mode.min_voltage.value == 0);
REQUIRE(transfer_mode.min_voltage.exponent == 0);
REQUIRE(transfer_mode.power_ramp_limit.has_value() == false);
}
}
GIVEN("Bad Case: e.g. dc transfer mod instead of dc_bpt transfer mod - FAILED_WrongChargeParameter") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::DC_BPT, dt::DcConnector::Extended, dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::Pricing::NoPricing, dt::BptChannel::Unified,
dt::GeneratorMode::GridFollowing);
d20::Session session = d20::Session(service_parameters);
message_20::DC_ChargeParameterDiscoveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_out = req.transfer_mode.emplace<DC_ModeReq>();
req_out.max_charge_power = {50, 3};
req_out.min_charge_power = {0, 0};
req_out.max_charge_current = {125, 0};
req_out.min_charge_current = {0, 0};
req_out.max_voltage = {400, 0};
req_out.min_voltage = {0, 0};
const auto res = d20::state::handle_request(req, session, evse_setup.powersupply_limits);
THEN("ResponseCode: FAILED_WrongChargeParameter, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_WrongChargeParameter);
REQUIRE(std::holds_alternative<DC_ModeRes>(res.transfer_mode));
const auto& transfer_mode = std::get<DC_ModeRes>(res.transfer_mode);
REQUIRE(transfer_mode.max_charge_power.value == 0);
REQUIRE(transfer_mode.max_charge_power.exponent == 0);
REQUIRE(transfer_mode.min_charge_power.value == 0);
REQUIRE(transfer_mode.min_charge_power.exponent == 0);
REQUIRE(transfer_mode.max_charge_current.value == 0);
REQUIRE(transfer_mode.max_charge_current.exponent == 0);
REQUIRE(transfer_mode.min_charge_current.value == 0);
REQUIRE(transfer_mode.min_charge_current.exponent == 0);
REQUIRE(transfer_mode.max_voltage.value == 0);
REQUIRE(transfer_mode.max_voltage.exponent == 0);
REQUIRE(transfer_mode.min_voltage.value == 0);
REQUIRE(transfer_mode.min_voltage.exponent == 0);
REQUIRE(transfer_mode.power_ramp_limit.has_value() == false);
}
}
GIVEN("Bad Case: e.g. DC_BPT transfer mod instead of dc transfer mod - FAILED_WrongChargeParameter") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::DC, dt::DcConnector::Extended, dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::Pricing::NoPricing);
d20::Session session = d20::Session(service_parameters);
message_20::DC_ChargeParameterDiscoveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_out = req.transfer_mode.emplace<BPT_DC_ModeReq>();
req_out.max_charge_power = {50, 3};
req_out.min_charge_power = {0, 0};
req_out.max_charge_current = {125, 0};
req_out.min_charge_current = {0, 0};
req_out.max_voltage = {400, 0};
req_out.min_voltage = {0, 0};
req_out.max_discharge_power = {11, 3};
req_out.min_discharge_power = {0, 0};
req_out.max_discharge_current = {25, 0};
req_out.min_discharge_current = {0, 0};
const auto res = d20::state::handle_request(req, session, evse_setup.powersupply_limits);
THEN("ResponseCode: FAILED_WrongChargeParameter, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_WrongChargeParameter);
REQUIRE(std::holds_alternative<DC_ModeRes>(res.transfer_mode));
const auto& transfer_mode = std::get<DC_ModeRes>(res.transfer_mode);
REQUIRE(transfer_mode.max_charge_power.value == 0);
REQUIRE(transfer_mode.max_charge_power.exponent == 0);
REQUIRE(transfer_mode.min_charge_power.value == 0);
REQUIRE(transfer_mode.min_charge_power.exponent == 0);
REQUIRE(transfer_mode.max_charge_current.value == 0);
REQUIRE(transfer_mode.max_charge_current.exponent == 0);
REQUIRE(transfer_mode.min_charge_current.value == 0);
REQUIRE(transfer_mode.min_charge_current.exponent == 0);
REQUIRE(transfer_mode.max_voltage.value == 0);
REQUIRE(transfer_mode.max_voltage.exponent == 0);
REQUIRE(transfer_mode.min_voltage.value == 0);
REQUIRE(transfer_mode.min_voltage.exponent == 0);
REQUIRE(transfer_mode.power_ramp_limit.has_value() == false);
}
}
GIVEN("Good Case: DC") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::DC, dt::DcConnector::Extended, dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::Pricing::NoPricing);
d20::Session session = d20::Session(service_parameters);
d20::DcTransferLimits powersupply_limits;
powersupply_limits.charge_limits.power.max = {22, 3};
powersupply_limits.charge_limits.current.max = {25, 0};
powersupply_limits.voltage.max = {900, 0};
dt::RationalNumber power_ramp_limit = {20, 0};
powersupply_limits.power_ramp_limit.emplace<>(power_ramp_limit);
message_20::DC_ChargeParameterDiscoveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_out = req.transfer_mode.emplace<DC_ModeReq>();
req_out.max_charge_power = {50, 3};
req_out.min_charge_power = {0, 0};
req_out.max_charge_current = {125, 0};
req_out.min_charge_current = {0, 0};
req_out.max_voltage = {400, 0};
req_out.min_voltage = {0, 0};
const auto res = d20::state::handle_request(req, session, powersupply_limits);
THEN("ResponseCode: OK") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(std::holds_alternative<DC_ModeRes>(res.transfer_mode));
const auto& transfer_mode = std::get<DC_ModeRes>(res.transfer_mode);
REQUIRE(transfer_mode.max_charge_power.value == 22);
REQUIRE(transfer_mode.max_charge_power.exponent == 3);
REQUIRE(transfer_mode.min_charge_power.value == 0);
REQUIRE(transfer_mode.min_charge_power.exponent == 0);
REQUIRE(transfer_mode.max_charge_current.value == 25);
REQUIRE(transfer_mode.max_charge_current.exponent == 0);
REQUIRE(transfer_mode.min_charge_current.value == 0);
REQUIRE(transfer_mode.min_charge_current.exponent == 0);
REQUIRE(transfer_mode.max_voltage.value == 900);
REQUIRE(transfer_mode.max_voltage.exponent == 0);
REQUIRE(transfer_mode.min_voltage.value == 0);
REQUIRE(transfer_mode.min_voltage.exponent == 0);
REQUIRE(transfer_mode.power_ramp_limit.has_value() == true);
REQUIRE(transfer_mode.power_ramp_limit.value().value == 20);
REQUIRE(transfer_mode.power_ramp_limit.value().exponent == 0);
}
}
GIVEN("Good Case: DC_BPT") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::DC_BPT, dt::DcConnector::Extended, dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::Pricing::NoPricing, dt::BptChannel::Unified,
dt::GeneratorMode::GridFollowing);
d20::Session session = d20::Session(service_parameters);
d20::DcTransferLimits powersupply_limits;
powersupply_limits.charge_limits.power.max = {22, 3};
powersupply_limits.charge_limits.current.max = {25, 0};
powersupply_limits.voltage.max = {900, 0};
auto& discharge_limits = powersupply_limits.discharge_limits.emplace();
discharge_limits.power.max = {11, 3};
discharge_limits.current.max = {25, 0};
message_20::DC_ChargeParameterDiscoveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_out = req.transfer_mode.emplace<BPT_DC_ModeReq>();
req_out.max_charge_power = {50, 3};
req_out.min_charge_power = {0, 0};
req_out.max_charge_current = {125, 0};
req_out.min_charge_current = {0, 0};
req_out.max_voltage = {400, 0};
req_out.min_voltage = {0, 0};
req_out.max_discharge_power = {11, 3};
req_out.min_discharge_power = {0, 0};
req_out.max_discharge_current = {25, 0};
req_out.min_discharge_current = {0, 0};
const auto res = d20::state::handle_request(req, session, powersupply_limits);
THEN("ResponseCode: OK") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(std::holds_alternative<BPT_DC_ModeRes>(res.transfer_mode));
const auto& transfer_mode = std::get<BPT_DC_ModeRes>(res.transfer_mode);
REQUIRE(transfer_mode.max_charge_power.value == 22);
REQUIRE(transfer_mode.max_charge_power.exponent == 3);
REQUIRE(transfer_mode.min_charge_power.value == 0);
REQUIRE(transfer_mode.min_charge_power.exponent == 0);
REQUIRE(transfer_mode.max_charge_current.value == 25);
REQUIRE(transfer_mode.max_charge_current.exponent == 0);
REQUIRE(transfer_mode.min_charge_current.value == 0);
REQUIRE(transfer_mode.min_charge_current.exponent == 0);
REQUIRE(transfer_mode.max_voltage.value == 900);
REQUIRE(transfer_mode.max_voltage.exponent == 0);
REQUIRE(transfer_mode.min_voltage.value == 0);
REQUIRE(transfer_mode.min_voltage.exponent == 0);
REQUIRE(transfer_mode.power_ramp_limit.has_value() == false);
REQUIRE(transfer_mode.max_discharge_power.value == 11);
REQUIRE(transfer_mode.max_discharge_power.exponent == 3);
REQUIRE(transfer_mode.min_discharge_power.value == 0);
REQUIRE(transfer_mode.min_discharge_power.exponent == 0);
REQUIRE(transfer_mode.max_discharge_current.value == 25);
REQUIRE(transfer_mode.max_discharge_current.exponent == 0);
REQUIRE(transfer_mode.min_discharge_current.value == 0);
REQUIRE(transfer_mode.min_discharge_current.exponent == 0);
}
}
GIVEN("Bad Case: Provided DC charge limits but the ev wants bpt charge parameter - FAILED") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::DC_BPT, dt::DcConnector::Extended, dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::Pricing::NoPricing, dt::BptChannel::Unified,
dt::GeneratorMode::GridFollowing);
d20::Session session = d20::Session(service_parameters);
d20::DcTransferLimits powersupply_limits;
powersupply_limits.charge_limits.power.max = {22, 3};
powersupply_limits.charge_limits.current.max = {25, 0};
powersupply_limits.voltage.max = {900, 0};
message_20::DC_ChargeParameterDiscoveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_out = req.transfer_mode.emplace<BPT_DC_ModeReq>();
req_out.max_charge_power = {50, 3};
req_out.min_charge_power = {0, 0};
req_out.max_charge_current = {125, 0};
req_out.min_charge_current = {0, 0};
req_out.max_voltage = {400, 0};
req_out.min_voltage = {0, 0};
req_out.max_discharge_power = {11, 3};
req_out.min_discharge_power = {0, 0};
req_out.max_discharge_current = {25, 0};
req_out.min_discharge_current = {0, 0};
const auto res = d20::state::handle_request(req, session, powersupply_limits);
THEN("ResponseCode: FAILED, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED);
REQUIRE(std::holds_alternative<DC_ModeRes>(res.transfer_mode));
const auto& transfer_mode = std::get<DC_ModeRes>(res.transfer_mode);
REQUIRE(transfer_mode.max_charge_power.value == 0);
REQUIRE(transfer_mode.max_charge_power.exponent == 0);
REQUIRE(transfer_mode.min_charge_power.value == 0);
REQUIRE(transfer_mode.min_charge_power.exponent == 0);
REQUIRE(transfer_mode.max_charge_current.value == 0);
REQUIRE(transfer_mode.max_charge_current.exponent == 0);
REQUIRE(transfer_mode.min_charge_current.value == 0);
REQUIRE(transfer_mode.min_charge_current.exponent == 0);
REQUIRE(transfer_mode.max_voltage.value == 0);
REQUIRE(transfer_mode.max_voltage.exponent == 0);
REQUIRE(transfer_mode.min_voltage.value == 0);
REQUIRE(transfer_mode.min_voltage.exponent == 0);
REQUIRE(transfer_mode.power_ramp_limit.has_value() == false);
}
}
GIVEN("Bad Case: EV Parameter does not fit in evse parameters - FAILED_WrongChargeParameter") {
}
GIVEN("Good Case: MCS") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::MCS, dt::DcConnector::Extended, dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::Pricing::NoPricing);
d20::Session session = d20::Session(service_parameters);
d20::DcTransferLimits powersupply_limits;
powersupply_limits.charge_limits.power.max = {22, 3};
powersupply_limits.charge_limits.current.max = {25, 0};
powersupply_limits.voltage.max = {900, 0};
dt::RationalNumber power_ramp_limit = {20, 0};
powersupply_limits.power_ramp_limit.emplace<>(power_ramp_limit);
message_20::DC_ChargeParameterDiscoveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_out = req.transfer_mode.emplace<DC_ModeReq>();
req_out.max_charge_power = {50, 3};
req_out.min_charge_power = {0, 0};
req_out.max_charge_current = {125, 0};
req_out.min_charge_current = {0, 0};
req_out.max_voltage = {400, 0};
req_out.min_voltage = {0, 0};
const auto res = d20::state::handle_request(req, session, powersupply_limits);
THEN("ResponseCode: OK") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(std::holds_alternative<DC_ModeRes>(res.transfer_mode));
const auto& transfer_mode = std::get<DC_ModeRes>(res.transfer_mode);
REQUIRE(transfer_mode.max_charge_power.value == 22);
REQUIRE(transfer_mode.max_charge_power.exponent == 3);
REQUIRE(transfer_mode.min_charge_power.value == 0);
REQUIRE(transfer_mode.min_charge_power.exponent == 0);
REQUIRE(transfer_mode.max_charge_current.value == 25);
REQUIRE(transfer_mode.max_charge_current.exponent == 0);
REQUIRE(transfer_mode.min_charge_current.value == 0);
REQUIRE(transfer_mode.min_charge_current.exponent == 0);
REQUIRE(transfer_mode.max_voltage.value == 900);
REQUIRE(transfer_mode.max_voltage.exponent == 0);
REQUIRE(transfer_mode.min_voltage.value == 0);
REQUIRE(transfer_mode.min_voltage.exponent == 0);
REQUIRE(transfer_mode.power_ramp_limit.has_value() == true);
REQUIRE(transfer_mode.power_ramp_limit.value().value == 20);
REQUIRE(transfer_mode.power_ramp_limit.value().exponent == 0);
}
}
GIVEN("Good Case: MCS_BPT") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::MCS_BPT, dt::DcConnector::Extended, dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::Pricing::NoPricing, dt::BptChannel::Unified,
dt::GeneratorMode::GridFollowing);
d20::Session session = d20::Session(service_parameters);
d20::DcTransferLimits powersupply_limits;
powersupply_limits.charge_limits.power.max = {22, 3};
powersupply_limits.charge_limits.current.max = {25, 0};
powersupply_limits.voltage.max = {900, 0};
auto& discharge_limits = powersupply_limits.discharge_limits.emplace();
discharge_limits.power.max = {11, 3};
discharge_limits.current.max = {25, 0};
message_20::DC_ChargeParameterDiscoveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& req_out = req.transfer_mode.emplace<BPT_DC_ModeReq>();
req_out.max_charge_power = {50, 3};
req_out.min_charge_power = {0, 0};
req_out.max_charge_current = {125, 0};
req_out.min_charge_current = {0, 0};
req_out.max_voltage = {400, 0};
req_out.min_voltage = {0, 0};
req_out.max_discharge_power = {11, 3};
req_out.min_discharge_power = {0, 0};
req_out.max_discharge_current = {25, 0};
req_out.min_discharge_current = {0, 0};
const auto res = d20::state::handle_request(req, session, powersupply_limits);
THEN("ResponseCode: OK") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(std::holds_alternative<BPT_DC_ModeRes>(res.transfer_mode));
const auto& transfer_mode = std::get<BPT_DC_ModeRes>(res.transfer_mode);
REQUIRE(transfer_mode.max_charge_power.value == 22);
REQUIRE(transfer_mode.max_charge_power.exponent == 3);
REQUIRE(transfer_mode.min_charge_power.value == 0);
REQUIRE(transfer_mode.min_charge_power.exponent == 0);
REQUIRE(transfer_mode.max_charge_current.value == 25);
REQUIRE(transfer_mode.max_charge_current.exponent == 0);
REQUIRE(transfer_mode.min_charge_current.value == 0);
REQUIRE(transfer_mode.min_charge_current.exponent == 0);
REQUIRE(transfer_mode.max_voltage.value == 900);
REQUIRE(transfer_mode.max_voltage.exponent == 0);
REQUIRE(transfer_mode.min_voltage.value == 0);
REQUIRE(transfer_mode.min_voltage.exponent == 0);
REQUIRE(transfer_mode.power_ramp_limit.has_value() == false);
REQUIRE(transfer_mode.max_discharge_power.value == 11);
REQUIRE(transfer_mode.max_discharge_power.exponent == 3);
REQUIRE(transfer_mode.min_discharge_power.value == 0);
REQUIRE(transfer_mode.min_discharge_power.exponent == 0);
REQUIRE(transfer_mode.max_discharge_current.value == 25);
REQUIRE(transfer_mode.max_discharge_current.exponent == 0);
REQUIRE(transfer_mode.min_discharge_current.value == 0);
REQUIRE(transfer_mode.min_discharge_current.exponent == 0);
}
}
// 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
}

View File

@@ -0,0 +1,63 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 Pionix GmbH and Contributors to EVerest
#include <catch2/catch_test_macros.hpp>
#include <iso15118/detail/d20/state/dc_pre_charge.hpp>
using namespace iso15118;
namespace dt = message_20::datatypes;
SCENARIO("DC Pre charge state handling") {
GIVEN("Bad case - Unknown session") {
auto session = d20::Session();
message_20::DC_PreChargeRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.processing = dt::Processing::Ongoing;
req.present_voltage = {0, 0};
req.target_voltage = {400, 0};
const float present_voltage = 0.1;
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 == dt::ResponseCode::FAILED_UnknownSession);
REQUIRE(res.present_voltage.value == 0);
REQUIRE(res.present_voltage.exponent == 0);
}
}
GIVEN("Good Case") {
auto session = d20::Session();
message_20::DC_PreChargeRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.processing = dt::Processing::Ongoing;
req.present_voltage = {0, 0};
req.target_voltage = {400, 0};
const float present_voltage = 400.1;
const auto res = d20::state::handle_request(req, session, present_voltage);
THEN("ResponseCode: OK, present_voltage should be 400.1V") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.present_voltage.value == 4001);
REQUIRE(res.present_voltage.exponent == -1);
}
}
// 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
}

View File

@@ -0,0 +1,60 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 Pionix GmbH and Contributors to EVerest
#include <catch2/catch_test_macros.hpp>
#include <iso15118/detail/d20/state/dc_welding_detection.hpp>
using namespace iso15118;
namespace dt = message_20::datatypes;
SCENARIO("DC Welding Detection state handling") {
GIVEN("Bad case - Unknown session") {
auto session = d20::Session();
message_20::DC_WeldingDetectionRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.processing = dt::Processing::Ongoing;
const float present_voltage = 0.1;
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 == dt::ResponseCode::FAILED_UnknownSession);
REQUIRE(res.present_voltage.value == 0);
REQUIRE(res.present_voltage.exponent == 0);
}
}
GIVEN("Good Case") {
auto session = d20::Session();
message_20::DC_WeldingDetectionRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.processing = dt::Processing::Ongoing;
const float present_voltage = 200;
const auto res = d20::state::handle_request(req, session, present_voltage);
THEN("ResponseCode: OK, present_voltage should be 200V") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.present_voltage.value == 2000);
REQUIRE(res.present_voltage.exponent == -1);
}
}
// GIVEN("Bad case - State B was not measuered -> FAILED") {} // TODO(sl): check evse_manager?
// 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
}

View File

@@ -0,0 +1,86 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 Pionix GmbH and Contributors to EVerest
#include <catch2/catch_test_macros.hpp>
#include <iso15118/detail/d20/state/power_delivery.hpp>
using namespace iso15118;
namespace dt = message_20::datatypes;
SCENARIO("Power delivery state handling") {
GIVEN("Bad case - Unknown session") {
d20::Session session = d20::Session();
message_20::PowerDeliveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.processing = dt::Processing::Ongoing;
req.charge_progress = dt::Progress::Start;
const auto res = d20::state::handle_request(req, d20::Session(), false);
THEN("ResponseCode: FAILED_UnknownSession, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_UnknownSession);
REQUIRE(res.status.has_value() == false);
}
}
GIVEN("Not so bad case - WARNING_StandbyNotAllowed") {
d20::Session session = d20::Session();
message_20::PowerDeliveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.processing = dt::Processing::Ongoing;
req.charge_progress = dt::Progress::Standby;
const auto res = d20::state::handle_request(req, session, false);
// Right now standby ist not supported
THEN("ResponseCode: WARNING_StandbyNotAllowed, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::WARNING_StandbyNotAllowed);
REQUIRE(res.status.has_value() == false);
}
}
GIVEN("Good case") {
}
GIVEN("Bad case - EVPowerProfileInvalid") {
}
GIVEN("Bad case - ScheduleSelectionInvalid") {
}
GIVEN("Bad case - PowerDeliveryNotApplied") {
} // TODO(sl): evse is not able to deliver energy
GIVEN("Bad case - PowerToleranceNotConfirmed") {
} // TODO(sl): Scheduled Mode + Provided PowerTolerance in ScheduleExchangeRes
GIVEN("Not so bad case - WARNING_PowerToleranceNotConfirmed") {
} // TODO(sl): Scheduled Mode + Provided PowerTolerance in ScheduleExchangeRes
GIVEN("Good case - OK_PowerToleranceConfirmed") {
} // TODO(sl): Scheduled Mode + Provided PowerTolerance in ScheduleExchangeRes
GIVEN("Bad case - AC ContactorError") {
d20::Session session = d20::Session();
message_20::PowerDeliveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.processing = dt::Processing::Ongoing;
req.charge_progress = dt::Progress::Start;
const auto res = d20::state::handle_request(req, session, true);
THEN("ResponseCode: FAILED_ContactorError, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_ContactorError);
REQUIRE(res.status.has_value() == false);
}
} // TODO(sl): AC stuff
// 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
}

View File

@@ -0,0 +1,162 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 Pionix GmbH and Contributors to EVerest
#include <catch2/catch_test_macros.hpp>
#include <iso15118/detail/d20/state/schedule_exchange.hpp>
using namespace iso15118;
namespace dt = message_20::datatypes;
SCENARIO("Schedule Exchange state handling") {
using Scheduled_ModeReq = message_20::datatypes::Scheduled_SEReqControlMode;
using Scheduled_ModeRes = message_20::datatypes::Scheduled_SEResControlMode;
using Dynamic_ModeReq = message_20::datatypes::Dynamic_SEReqControlMode;
using Dynamic_ModeRes = message_20::datatypes::Dynamic_SEResControlMode;
GIVEN("Bad case - Unknown session") {
d20::Session session = d20::Session();
message_20::ScheduleExchangeRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.control_mode.emplace<Scheduled_ModeReq>();
dt::RationalNumber max_power = {0, 0};
const auto res =
d20::state::handle_request(req, d20::Session(), max_power, d20::UpdateDynamicModeParameters(), false);
THEN("ResponseCode: FAILED_UnknownSession, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_UnknownSession);
REQUIRE(res.processing == dt::Processing::Finished);
REQUIRE(std::holds_alternative<Dynamic_ModeRes>(res.control_mode) == true);
const auto& res_control_mode = std::get<Dynamic_ModeRes>(res.control_mode);
REQUIRE(std::holds_alternative<std::monostate>(res_control_mode.price_schedule) == true);
}
}
GIVEN("Bad case - False control mode") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::DC, dt::DcConnector::Extended, dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::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<Dynamic_ModeReq>();
dt::RationalNumber max_power = {0, 0};
const auto res = d20::state::handle_request(req, session, max_power, d20::UpdateDynamicModeParameters(), false);
THEN("ResponseCode: FAILED, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED);
REQUIRE(res.processing == dt::Processing::Finished);
REQUIRE(std::holds_alternative<Dynamic_ModeRes>(res.control_mode) == true);
const auto& res_control_mode = std::get<Dynamic_ModeRes>(res.control_mode);
REQUIRE(std::holds_alternative<std::monostate>(res_control_mode.price_schedule) == true);
}
}
GIVEN("Good case - Scheduled Mode") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::DC, dt::DcConnector::Extended, dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::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<Scheduled_ModeReq>();
dt::RationalNumber max_power = {22, 3};
const auto res = d20::state::handle_request(req, session, max_power, d20::UpdateDynamicModeParameters(), false);
THEN("ResponseCode: OK") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.processing == dt::Processing::Finished);
REQUIRE(std::holds_alternative<Scheduled_ModeRes>(res.control_mode) == true);
const auto& res_control_mode = std::get<Scheduled_ModeRes>(res.control_mode);
REQUIRE(res_control_mode.schedule_tuple.size() == 1);
const auto& schedule_tuple = res_control_mode.schedule_tuple[0];
REQUIRE(dt::from_RationalNumber(schedule_tuple.charging_schedule.power_schedule.entries.at(0).power) ==
22000);
}
}
GIVEN("Good case - Dynamic Mode") {
d20::SelectedServiceParameters service_parameters =
d20::SelectedServiceParameters(dt::ServiceCategory::DC, dt::DcConnector::Extended, dt::ControlMode::Dynamic,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::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<Dynamic_ModeReq>();
dt::RationalNumber max_power = {22, 3};
const auto res = d20::state::handle_request(req, session, max_power, d20::UpdateDynamicModeParameters(), false);
THEN("ResponseCode: OK") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.processing == dt::Processing::Finished);
REQUIRE(std::holds_alternative<Dynamic_ModeRes>(res.control_mode) == true);
}
}
GIVEN("Good case - MCS Dynamic Mode") {
d20::SelectedServiceParameters service_parameters = d20::SelectedServiceParameters(
dt::ServiceCategory::MCS, dt::DcConnector::Extended, dt::ControlMode::Dynamic,
dt::MobilityNeedsMode::ProvidedByEvcc, dt::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<Dynamic_ModeReq>();
dt::RationalNumber max_power = {22, 3};
const auto res = d20::state::handle_request(req, session, max_power, d20::UpdateDynamicModeParameters(), false);
THEN("ResponseCode: OK") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.processing == dt::Processing::Finished);
REQUIRE(std::holds_alternative<Dynamic_ModeRes>(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
// GIVEN("Performance Timeout") {} // TODO(sl): not here
// GIVEN("Sequence Timeout") {} // TODO(sl): not here
}

View File

@@ -0,0 +1,202 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 Pionix GmbH and Contributors to EVerest
#include <catch2/catch_test_macros.hpp>
#include <iso15118/detail/d20/state/service_discovery.hpp>
using namespace iso15118;
namespace dt = message_20::datatypes;
SCENARIO("Service discovery state handling") {
GIVEN("Bad Case - Unknown session") {
auto session = d20::Session();
message_20::ServiceDiscoveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
session = d20::Session();
std::vector<dt::ServiceCategory> energy_services = {dt::ServiceCategory::DC};
std::vector<dt::ServiceCategory> ev_energy_services{};
const auto res = d20::state::handle_request(req, session, energy_services, {}, ev_energy_services);
THEN("ResponseCode: FAILED_UnknownSession, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_UnknownSession);
REQUIRE(res.service_renegotiation_supported == false);
REQUIRE(res.energy_transfer_service_list.size() == 1);
REQUIRE(res.energy_transfer_service_list[0].free_service == false);
REQUIRE(res.energy_transfer_service_list[0].service_id == dt::ServiceCategory::AC);
REQUIRE(res.vas_list.has_value() == false);
}
}
GIVEN("Good Case - Setting ac services") {
d20::Session session = d20::Session();
message_20::ServiceDiscoveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
std::vector<dt::ServiceCategory> supported_energy_transfer_services = {dt::ServiceCategory::AC,
dt::ServiceCategory::AC_BPT};
std::vector<dt::ServiceCategory> ev_energy_services{};
const auto res =
d20::state::handle_request(req, session, supported_energy_transfer_services, {}, ev_energy_services);
THEN("ResponseCode: OK, energy_transfer_service_list: AC & AC_WPT, vaslist: empty") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.service_renegotiation_supported == false);
REQUIRE(res.energy_transfer_service_list.size() == 2);
REQUIRE(res.energy_transfer_service_list[0].free_service == false);
REQUIRE((res.energy_transfer_service_list[0].service_id == dt::ServiceCategory::AC ||
res.energy_transfer_service_list[0].service_id == dt::ServiceCategory::AC_BPT));
REQUIRE(res.energy_transfer_service_list[1].free_service == false);
REQUIRE((res.energy_transfer_service_list[1].service_id == dt::ServiceCategory::AC ||
res.energy_transfer_service_list[1].service_id == dt::ServiceCategory::AC_BPT));
REQUIRE(res.vas_list.has_value() == false);
}
}
GIVEN("Good Case - Setting dc services") {
d20::Session session = d20::Session();
message_20::ServiceDiscoveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
std::vector<dt::ServiceCategory> supported_energy_transfer_services = {dt::ServiceCategory::DC,
dt::ServiceCategory::DC_BPT};
std::vector<dt::ServiceCategory> ev_energy_services{};
const auto res =
d20::state::handle_request(req, session, supported_energy_transfer_services, {}, ev_energy_services);
THEN("ResponseCode: OK, energy_transfer_service_list: DC & DC_WPT, vaslist: empty") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.service_renegotiation_supported == false);
REQUIRE(res.energy_transfer_service_list.size() == 2);
REQUIRE(res.energy_transfer_service_list[0].free_service == false);
REQUIRE((res.energy_transfer_service_list[0].service_id == dt::ServiceCategory::DC ||
res.energy_transfer_service_list[0].service_id == dt::ServiceCategory::DC_BPT));
REQUIRE(res.energy_transfer_service_list[1].free_service == false);
REQUIRE((res.energy_transfer_service_list[1].service_id == dt::ServiceCategory::DC ||
res.energy_transfer_service_list[1].service_id == dt::ServiceCategory::DC_BPT));
REQUIRE(res.vas_list.has_value() == false);
}
}
GIVEN("Good Case - Setting MCS services") {
d20::Session session = d20::Session();
message_20::ServiceDiscoveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
std::vector<dt::ServiceCategory> supported_energy_transfer_services = {dt::ServiceCategory::MCS,
dt::ServiceCategory::MCS_BPT};
std::vector<dt::ServiceCategory> ev_energy_services{};
const auto res =
d20::state::handle_request(req, session, supported_energy_transfer_services, {}, ev_energy_services);
THEN("ResponseCode: OK, energy_transfer_service_list: MCS & MCS_BPT, vaslist: empty") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.service_renegotiation_supported == false);
REQUIRE(res.energy_transfer_service_list.size() == 2);
REQUIRE(res.energy_transfer_service_list[0].free_service == false);
REQUIRE((res.energy_transfer_service_list[0].service_id == dt::ServiceCategory::MCS or
res.energy_transfer_service_list[0].service_id == dt::ServiceCategory::MCS_BPT));
REQUIRE(res.energy_transfer_service_list[1].free_service == false);
REQUIRE((res.energy_transfer_service_list[1].service_id == dt::ServiceCategory::MCS or
res.energy_transfer_service_list[1].service_id == dt::ServiceCategory::MCS_BPT));
REQUIRE(res.vas_list.has_value() == false);
}
}
GIVEN("Good Case - Setting services + vas list") {
d20::Session session = d20::Session();
message_20::ServiceDiscoveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
std::vector<dt::ServiceCategory> supported_energy_transfer_services = {dt::ServiceCategory::DC,
dt::ServiceCategory::DC_BPT};
std::vector<uint16_t> supported_vas_services = {
message_20::to_underlying_value(dt::ServiceCategory::ParkingStatus)};
std::vector<dt::ServiceCategory> ev_energy_services{};
const auto res = d20::state::handle_request(req, session, supported_energy_transfer_services,
supported_vas_services, ev_energy_services);
THEN("ResponseCode: OK, energy_transfer_service_list: DC & DC_BPT, vaslist: ParkingStatus") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.service_renegotiation_supported == false);
REQUIRE(res.energy_transfer_service_list.size() == 2);
REQUIRE(res.energy_transfer_service_list[0].free_service == false);
REQUIRE((res.energy_transfer_service_list[0].service_id == dt::ServiceCategory::DC ||
res.energy_transfer_service_list[0].service_id == dt::ServiceCategory::DC_BPT));
REQUIRE(res.energy_transfer_service_list[1].free_service == false);
REQUIRE((res.energy_transfer_service_list[1].service_id == dt::ServiceCategory::DC ||
res.energy_transfer_service_list[1].service_id == dt::ServiceCategory::DC_BPT));
REQUIRE(res.vas_list.has_value() == true);
REQUIRE(res.vas_list.value()[0].free_service == false);
REQUIRE(res.vas_list.value()[0].service_id ==
message_20::to_underlying_value(dt::ServiceCategory::ParkingStatus));
}
}
GIVEN("Good Case - Filter supported_service_providers") {
d20::Session session = d20::Session();
message_20::ServiceDiscoveryRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
auto& supported_service_ids = req.supported_service_ids.emplace();
supported_service_ids.push_back(2);
supported_service_ids.push_back(65);
std::vector<dt::ServiceCategory> supported_energy_transfer_services = {dt::ServiceCategory::DC,
dt::ServiceCategory::DC_BPT};
std::vector<uint16_t> supported_vas_services = {
message_20::to_underlying_value(dt::ServiceCategory::ParkingStatus)};
std::vector<dt::ServiceCategory> ev_energy_services{};
const auto res = d20::state::handle_request(req, session, supported_energy_transfer_services,
supported_vas_services, ev_energy_services);
THEN("ResponseCode: OK, energy_transfer_service_list: DC, vaslist: empty") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(res.service_renegotiation_supported == false);
REQUIRE(res.energy_transfer_service_list.size() == 1);
REQUIRE(res.energy_transfer_service_list[0].free_service == false);
REQUIRE(res.energy_transfer_service_list[0].service_id == dt::ServiceCategory::DC);
REQUIRE(res.vas_list.has_value() == false);
}
}
// [V2G20-1644]
GIVEN("Good case - Resuming secc shall provide the same service ids and parameter set ids "
"(ServiceRenegotiationSupported: false)") {
} // Todo(sl): Fill out
GIVEN("Bad case - EV supported_service_ids do not match with evse supported services") {
} // Todo(sl): Fill out
// GIVEN("Bad Case - sequence error") {} // TODO(sl): not here
// GIVEN("Performance Timeout") {} // TODO(sl): not here
// GIVEN("Sequence Timeout") {} // TODO(sl): not here
}

View File

@@ -0,0 +1,421 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 Pionix GmbH and Contributors to EVerest
#include <catch2/catch_test_macros.hpp>
#include <iso15118/detail/d20/state/service_selection.hpp>
using namespace iso15118;
namespace dt = message_20::datatypes;
SCENARIO("Service selection state handling") {
GIVEN("Bad case - Unknown session") {
d20::Session session = d20::Session();
message_20::ServiceSelectionRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.selected_energy_transfer_service.service_id = dt::ServiceCategory::DC_BPT;
req.selected_energy_transfer_service.parameter_set_id = 0;
session = d20::Session();
const auto res = d20::state::handle_request(req, session);
THEN("ResponseCode: FAILED_UnknownSession, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_UnknownSession);
}
}
GIVEN("Bad case: selected_energy_transfer_service false parameter set id - FAILED_ServiceSelectionInvalid") {
d20::Session session = d20::Session();
session.offered_services.energy_services = {dt::ServiceCategory::DC};
session.offered_services.dc_parameter_list[0] = {
dt::DcConnector::Extended,
dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc,
dt::Pricing::NoPricing,
};
message_20::ServiceSelectionRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.selected_energy_transfer_service.service_id = dt::ServiceCategory::DC;
req.selected_energy_transfer_service.parameter_set_id = 1;
const auto res = d20::state::handle_request(req, session);
THEN("ResponseCode: FAILED_ServiceSelectionInvalid, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_ServiceSelectionInvalid);
}
}
GIVEN("Bad case: selected_energy_transfer service is not correct - FAILED_NoEnergyTransferServiceSelected") {
d20::Session session = d20::Session();
session.offered_services.energy_services = {dt::ServiceCategory::DC};
session.offered_services.dc_parameter_list[0] = {
dt::DcConnector::Extended,
dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc,
dt::Pricing::NoPricing,
};
message_20::ServiceSelectionRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.selected_energy_transfer_service.service_id = dt::ServiceCategory::AC;
req.selected_energy_transfer_service.parameter_set_id = 0;
const auto res = d20::state::handle_request(req, session);
THEN("ResponseCode: FAILED_NoEnergyTransferServiceSelected, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_NoEnergyTransferServiceSelected);
}
}
GIVEN("Good case") {
d20::Session session = d20::Session();
session.offered_services.energy_services = {dt::ServiceCategory::DC};
session.offered_services.dc_parameter_list[0] = {
dt::DcConnector::Extended,
dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc,
dt::Pricing::NoPricing,
};
message_20::ServiceSelectionRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.selected_energy_transfer_service.service_id = dt::ServiceCategory::DC;
req.selected_energy_transfer_service.parameter_set_id = 0;
const auto res = d20::state::handle_request(req, session);
THEN("ResponseCode: OK") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
}
}
GIVEN("Good case - Check if session variables is set") {
d20::Session session = d20::Session();
session.offered_services.energy_services = {dt::ServiceCategory::DC};
session.offered_services.dc_parameter_list[0] = {
dt::DcConnector::Extended,
dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc,
dt::Pricing::NoPricing,
};
message_20::ServiceSelectionRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.selected_energy_transfer_service.service_id = dt::ServiceCategory::DC;
req.selected_energy_transfer_service.parameter_set_id = 0;
const auto res = d20::state::handle_request(req, session);
THEN("ResponseCode: OK") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
const auto selected_services = session.get_selected_services();
REQUIRE(selected_services.selected_energy_service == dt::ServiceCategory::DC);
REQUIRE(selected_services.selected_control_mode == dt::ControlMode::Scheduled);
}
}
GIVEN("Good case - DC_BPT") {
d20::Session session = d20::Session();
session.offered_services.energy_services = {dt::ServiceCategory::DC_BPT};
session.offered_services.dc_bpt_parameter_list[0] = {{
dt::DcConnector::Extended,
dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc,
dt::Pricing::NoPricing,
},
dt::BptChannel::Unified,
dt::GeneratorMode::GridFollowing};
message_20::ServiceSelectionRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.selected_energy_transfer_service.service_id = dt::ServiceCategory::DC_BPT;
req.selected_energy_transfer_service.parameter_set_id = 0;
const auto res = d20::state::handle_request(req, session);
THEN("ResponseCode: OK") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
const auto selected_services = session.get_selected_services();
REQUIRE(selected_services.selected_energy_service == dt::ServiceCategory::DC_BPT);
REQUIRE(selected_services.selected_control_mode == dt::ControlMode::Scheduled);
}
}
GIVEN("Bad case: selected_vas_list false service id - FAILED_ServiceSelectionInvalid") {
d20::Session session = d20::Session();
session.offered_services.energy_services = {dt::ServiceCategory::DC};
session.offered_services.dc_parameter_list[0] = {
dt::DcConnector::Extended,
dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc,
dt::Pricing::NoPricing,
};
session.offered_services.vas_services = {message_20::to_underlying_value(dt::ServiceCategory::Internet)};
session.offered_services.internet_parameter_list[0] = {
dt::Protocol::Http,
dt::Port::Port80,
};
message_20::ServiceSelectionRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.selected_energy_transfer_service.service_id = dt::ServiceCategory::DC;
req.selected_energy_transfer_service.parameter_set_id = 0;
req.selected_vas_list = {{message_20::to_underlying_value(dt::ServiceCategory::ParkingStatus), 0}};
const auto res = d20::state::handle_request(req, session);
THEN("ResponseCode: FAILED_ServiceSelectionInvalid, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_ServiceSelectionInvalid);
}
}
GIVEN("Bad case: selected_vas_list false parameter set id - FAILED_ServiceSelectionInvalid") {
d20::Session session = d20::Session();
session.offered_services.energy_services = {dt::ServiceCategory::DC};
session.offered_services.dc_parameter_list[0] = {
dt::DcConnector::Extended,
dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc,
dt::Pricing::NoPricing,
};
session.offered_services.vas_services = {message_20::to_underlying_value(dt::ServiceCategory::Internet)};
session.offered_services.internet_parameter_list[0] = {
dt::Protocol::Http,
dt::Port::Port80,
};
message_20::ServiceSelectionRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.selected_energy_transfer_service.service_id = dt::ServiceCategory::DC;
req.selected_energy_transfer_service.parameter_set_id = 0;
req.selected_vas_list = {{message_20::to_underlying_value(dt::ServiceCategory::Internet), 1}};
const auto res = d20::state::handle_request(req, session);
THEN("ResponseCode: FAILED_ServiceSelectionInvalid, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_ServiceSelectionInvalid);
}
}
GIVEN("Good case - DC & Internet & Parking") {
d20::Session session = d20::Session();
session.offered_services.energy_services = {dt::ServiceCategory::DC};
session.offered_services.dc_parameter_list[0] = {
dt::DcConnector::Extended,
dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc,
dt::Pricing::NoPricing,
};
session.offered_services.vas_services = {message_20::to_underlying_value(dt::ServiceCategory::Internet),
message_20::to_underlying_value(dt::ServiceCategory::ParkingStatus)};
session.offered_services.internet_parameter_list[0] = {
dt::Protocol::Http,
dt::Port::Port80,
};
session.offered_services.parking_parameter_list[0] = {
dt::IntendedService::VehicleCheckIn,
dt::ParkingStatus::ManualExternal,
};
message_20::ServiceSelectionRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.selected_energy_transfer_service.service_id = dt::ServiceCategory::DC;
req.selected_energy_transfer_service.parameter_set_id = 0;
req.selected_vas_list = {{message_20::to_underlying_value(dt::ServiceCategory::Internet), 0},
{message_20::to_underlying_value(dt::ServiceCategory::ParkingStatus), 0}};
const auto res = d20::state::handle_request(req, session);
THEN("ResponseCode: OK") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
}
}
GIVEN("Good case - AC") {
d20::Session session = d20::Session();
session.offered_services.energy_services = {dt::ServiceCategory::AC};
session.offered_services.ac_parameter_list[0] = {
dt::AcConnector::ThreePhase, dt::ControlMode::Scheduled, dt::MobilityNeedsMode::ProvidedByEvcc, 230,
dt::Pricing::NoPricing,
};
message_20::ServiceSelectionRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.selected_energy_transfer_service.service_id = dt::ServiceCategory::AC;
req.selected_energy_transfer_service.parameter_set_id = 0;
const auto res = d20::state::handle_request(req, session);
THEN("ResponseCode: OK") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
}
}
GIVEN("Good case - AC_BPT") {
d20::Session session = d20::Session();
session.offered_services.energy_services = {dt::ServiceCategory::AC_BPT};
session.offered_services.ac_bpt_parameter_list[0] = {{
dt::AcConnector::ThreePhase,
dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc,
230,
dt::Pricing::NoPricing,
},
dt::BptChannel::Unified,
dt::GeneratorMode::GridFollowing,
dt::GridCodeIslandingDetectionMethod::Passive};
message_20::ServiceSelectionRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.selected_energy_transfer_service.service_id = dt::ServiceCategory::AC_BPT;
req.selected_energy_transfer_service.parameter_set_id = 0;
const auto res = d20::state::handle_request(req, session);
THEN("ResponseCode: OK") {
const auto selected_services = session.get_selected_services();
REQUIRE(res.response_code == dt::ResponseCode::OK);
REQUIRE(selected_services.selected_energy_service == dt::ServiceCategory::AC_BPT);
REQUIRE(selected_services.selected_control_mode == dt::ControlMode::Scheduled);
}
}
GIVEN("Good case - MCS") {
d20::Session session = d20::Session();
session.offered_services.energy_services = {dt::ServiceCategory::MCS};
session.offered_services.mcs_parameter_list[0] = {
dt::McsConnector::Mcs,
dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc,
dt::Pricing::NoPricing,
};
message_20::ServiceSelectionRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.selected_energy_transfer_service.service_id = dt::ServiceCategory::MCS;
req.selected_energy_transfer_service.parameter_set_id = 0;
const auto res = d20::state::handle_request(req, session);
THEN("ResponseCode: OK") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
const auto selected_services = session.get_selected_services();
REQUIRE(selected_services.selected_energy_service == dt::ServiceCategory::MCS);
REQUIRE(selected_services.selected_control_mode == dt::ControlMode::Scheduled);
REQUIRE(*std::get_if<dt::McsConnector>(&selected_services.selected_connector) == dt::McsConnector::Mcs);
}
}
GIVEN("Good case - MCS_BPT") {
d20::Session session = d20::Session();
session.offered_services.energy_services = {dt::ServiceCategory::MCS_BPT};
session.offered_services.mcs_bpt_parameter_list[0] = {{
dt::McsConnector::Mcs,
dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc,
dt::Pricing::NoPricing,
},
dt::BptChannel::Unified,
dt::GeneratorMode::GridFollowing};
message_20::ServiceSelectionRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.selected_energy_transfer_service.service_id = dt::ServiceCategory::MCS_BPT;
req.selected_energy_transfer_service.parameter_set_id = 0;
const auto res = d20::state::handle_request(req, session);
THEN("ResponseCode: OK") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
const auto selected_services = session.get_selected_services();
REQUIRE(selected_services.selected_energy_service == dt::ServiceCategory::MCS_BPT);
REQUIRE(selected_services.selected_control_mode == dt::ControlMode::Scheduled);
REQUIRE(*std::get_if<dt::McsConnector>(&selected_services.selected_connector) == dt::McsConnector::Mcs);
const auto bpt_channel = selected_services.selected_bpt_channel.has_value() and
selected_services.selected_bpt_channel.value() == dt::BptChannel::Unified;
REQUIRE(bpt_channel == true);
}
}
GIVEN("Good case - DC & Custom VAS") {
d20::Session session = d20::Session();
session.offered_services.energy_services = {dt::ServiceCategory::DC};
session.offered_services.dc_parameter_list[0] = {
dt::DcConnector::Extended,
dt::ControlMode::Scheduled,
dt::MobilityNeedsMode::ProvidedByEvcc,
dt::Pricing::NoPricing,
};
session.offered_services.vas_services = {4599};
session.offered_services.custom_vas_list[4599] = {0, 2};
message_20::ServiceSelectionRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.selected_energy_transfer_service.service_id = dt::ServiceCategory::DC;
req.selected_energy_transfer_service.parameter_set_id = 0;
req.selected_vas_list = {
{4599, 0},
};
const auto res = d20::state::handle_request(req, session);
THEN("ResponseCode: OK") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
}
}
// GIVEN("Bad case - FAILED_NoServiceRenegotiationSupported") {} // todo(sl): pause/resume not supported yet
// 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
}

View File

@@ -0,0 +1,75 @@
// SPDX-License-Identifier: Apache-2.0
// Copyright 2023 Pionix GmbH and Contributors to EVerest
#include <catch2/catch_test_macros.hpp>
#include <iso15118/detail/d20/state/session_stop.hpp>
using namespace iso15118;
namespace dt = message_20::datatypes;
SCENARIO("Session Stop state handling") {
GIVEN("Bad case - Unknown session") {
auto session = d20::Session();
message_20::SessionStopRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.charging_session = dt::ChargingSession::Terminate;
const auto res = d20::state::handle_request(req, d20::Session());
THEN("ResponseCode: FAILED_UnknownSession, mandatory fields should be set") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_UnknownSession);
}
}
GIVEN("Good Case") {
auto session = d20::Session();
message_20::SessionStopRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.charging_session = dt::ChargingSession::Terminate;
const auto res = d20::state::handle_request(req, session);
THEN("ResponseCode: OK") {
REQUIRE(res.response_code == dt::ResponseCode::OK);
}
}
GIVEN("Bad case - FAILED_NoServiceRenegotiationSupported") {
auto session = d20::Session();
session.service_renegotiation_supported = false;
message_20::SessionStopRequest req;
req.header.session_id = session.get_id();
req.header.timestamp = 1691411798;
req.charging_session = dt::ChargingSession::ServiceRenegotiation;
const auto res = d20::state::handle_request(req, session);
THEN("ResponseCode: OK") {
REQUIRE(res.response_code == dt::ResponseCode::FAILED_NoServiceRenegotiationSupported);
}
}
// GIVEN("Bad case - Dynamic mode, FAILED_PauseNotAllowed") {} // TODO(sl): EVCC requests Pause, but Secc didnt
// initiated
// GIVEN("Bad case - Scheduled mode , FAILED_PauseNotAllowed") {} // TODO(sl): Check current EVPowerProfileEntry for
// 0kW
// GIVEN("Bad case - State B was not measuered -> FAILED") {} // TODO(sl): check evse_manager?
// 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
}

View File

@@ -0,0 +1,9 @@
add_executable(test_v2gtp v2gtp.cpp)
target_link_libraries(test_v2gtp
PRIVATE
cbv2g::tp
Catch2::Catch2WithMain
)
include(Catch)
catch_discover_tests(test_v2gtp)

View File

@@ -0,0 +1,17 @@
#include <catch2/catch_test_macros.hpp>
#include <cbv2g/exi_v2gtp.h>
SCENARIO("V2GTP header test") {
GIVEN("A binary representation of a valid (pre -20) V2GTP header") {
uint8_t header[] = {0x01, 0xfe, 0x80, 0x01, 0x00, 0x00, 0x00, 0x24};
THEN("It should be decoded succussfully") {
uint32_t payload_len;
auto ec = V2GTP_ReadHeader(header, &payload_len);
REQUIRE(ec == V2GTP_ERROR__NO_ERROR);
REQUIRE(payload_len == 0x24);
}
}
}