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:
97
tools/EVerest-main/lib/everest/ocpp/tests/CMakeLists.txt
Normal file
97
tools/EVerest-main/lib/everest/ocpp/tests/CMakeLists.txt
Normal file
@@ -0,0 +1,97 @@
|
||||
include(${CMAKE_CURRENT_SOURCE_DIR}/libocpp-unittests.cmake)
|
||||
|
||||
# For libocpp tests, there is one big executable, which links against the ocpp lib and all other libs.
|
||||
# When it is useful to link only to the tested cpp files, a separate executable can be created for each file.
|
||||
# The source files can be added to this variable, which is a list. For example:
|
||||
# list(APPEND SEPARATE_UNIT_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/lib/ocpp/v2/functional_blocks/test_security.cpp)
|
||||
# CMake will then create a new test executable based on the filename and adds 'libocpp_' in front of it. In above
|
||||
# example, a test executable 'libocpp_test_security' will be created. In this example, in the CMakeLists of
|
||||
# `lib/ocpp/v2/functional_blocks`, files to link against can be added to this target / executable.
|
||||
#
|
||||
# For each test in this list, cmake will link agaist some 'default' cpp files (like utils and enums etc), set all
|
||||
# correct flags, add a test, set definitions, etc. See below.
|
||||
set(SEPARATE_UNIT_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/lib/ocpp/common/utils_tests.cpp)
|
||||
|
||||
# Add separate tests for V2 only.
|
||||
if(LIBOCPP_ENABLE_V2)
|
||||
# Add all v2 tests you don't want to include in the default test executable here.
|
||||
list(APPEND SEPARATE_UNIT_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/lib/ocpp/v2/functional_blocks/test_authorization.cpp)
|
||||
list(APPEND SEPARATE_UNIT_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/lib/ocpp/v2/functional_blocks/test_availability.cpp)
|
||||
list(APPEND SEPARATE_UNIT_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/lib/ocpp/v2/functional_blocks/test_security.cpp)
|
||||
list(APPEND SEPARATE_UNIT_TESTS ${CMAKE_CURRENT_SOURCE_DIR}/lib/ocpp/v2/functional_blocks/test_tariff_and_cost.cpp)
|
||||
endif()
|
||||
|
||||
|
||||
set(TEST_BINARY_DIR ${CMAKE_CURRENT_BINARY_DIR})
|
||||
set(GCOVR_DEPENDENCIES)
|
||||
|
||||
|
||||
add_libocpp_unittest(NAME libocpp_unit_tests PATH "")
|
||||
|
||||
target_link_libraries(libocpp_unit_tests PRIVATE
|
||||
ocpp
|
||||
${GTEST_LIBRARIES}
|
||||
)
|
||||
|
||||
|
||||
# Add executables, link libraries etc for the unit tests that are separate and do not link against the ocpp lib.
|
||||
# This loops over all tests added to `SEPARATE_UNIT_TESTS` and does the necessary things to make it build.
|
||||
# For now, everything is added that seems to be needed in every test. If later some sources and link libraries should
|
||||
# not be added here, they can always be removed from this loop and added to the test targets that actually need them.
|
||||
foreach(ITEM ${SEPARATE_UNIT_TESTS})
|
||||
set(TEST_ROOT_NAME)
|
||||
cmake_path(GET ITEM STEM TEST_ROOT_NAME)
|
||||
set(TEST_NAME "libocpp_${TEST_ROOT_NAME}")
|
||||
add_libocpp_unittest(NAME ${TEST_NAME} PATH ${ITEM})
|
||||
endforeach(ITEM)
|
||||
|
||||
# Subdirectories should be added only after adding the tests, because they have to exist for the CMakeLists.txt in the
|
||||
# child directories.
|
||||
add_subdirectory(lib/ocpp/common)
|
||||
|
||||
if(LIBOCPP_ENABLE_V16)
|
||||
add_subdirectory(lib/ocpp/v16)
|
||||
endif()
|
||||
|
||||
if(LIBOCPP_ENABLE_V2)
|
||||
add_subdirectory(lib/ocpp/v2)
|
||||
add_subdirectory(lib/ocpp/v21)
|
||||
endif()
|
||||
|
||||
add_custom_command(TARGET libocpp_unit_tests POST_BUILD
|
||||
COMMAND ${CMAKE_COMMAND} -E copy ${CONFIG_FILE_LOCATION_V16} ${CONFIG_FILE_RESOURCES_LOCATION_V16}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy ${USER_CONFIG_FILE_LOCATION_V16} ${USER_CONFIG_FILE_RESOURCES_LOCATION_V16}
|
||||
COMMAND ${CMAKE_COMMAND} -E remove_directory ${CONFIG_DIR_V16}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory ${OCPP1_6_CONFIG_DIR} ${CONFIG_DIR_V16}
|
||||
COMMAND ${CMAKE_COMMAND} -E remove_directory ${MIGRATION_FILES_LOCATION_V16}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory ${MIGRATION_FILES_SOURCE_DIR_V16} ${MIGRATION_FILES_LOCATION_V16}
|
||||
COMMAND ${CMAKE_COMMAND} -E remove_directory ${MIGRATION_FILES_LOCATION_V2}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory ${MIGRATION_FILES_SOURCE_DIR_V2} ${MIGRATION_FILES_LOCATION_V2}
|
||||
COMMAND ${CMAKE_COMMAND} -E remove_directory ${MIGRATION_FILES_DEVICE_MODEL_LOCATION_V2}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory ${MIGRATION_FILES_DEVICE_MODEL_SOURCE_DIR_V2} ${MIGRATION_FILES_DEVICE_MODEL_LOCATION_V2}
|
||||
COMMAND ${CMAKE_COMMAND} -E remove_directory ${DEVICE_MODEL_RESOURCES_LOCATION_V2}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory ${DEVICE_MODEL_CURRENT_RESOURCES_DIR} ${DEVICE_MODEL_RESOURCES_LOCATION_V2}
|
||||
COMMAND ${CMAKE_COMMAND} -E remove_directory ${DEVICE_MODEL_RESOURCES_CHANGED_LOCATION_V2}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory ${DEVICE_MODEL_CURRENT_CHANGED_RESOURCES_DIR} ${DEVICE_MODEL_RESOURCES_CHANGED_LOCATION_V2}
|
||||
COMMAND ${CMAKE_COMMAND} -E remove_directory ${DEVICE_MODEL_RESOURCES_WRONG_LOCATION_V2}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory ${DEVICE_MODEL_CURRENT_WRONG_RESOURCES_DIR} ${DEVICE_MODEL_RESOURCES_WRONG_LOCATION_V2}
|
||||
COMMAND ${CMAKE_COMMAND} -E remove_directory ${DEVICE_MODEL_EXAMPLE_CONFIG_LOCATION_V2}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy_directory ${DEVICE_MODEL_CURRENT_EXAMPLE_CONFIG_LOCATION_V2} ${DEVICE_MODEL_EXAMPLE_CONFIG_LOCATION_V2}
|
||||
COMMAND ${CMAKE_COMMAND} -E copy ${DEVICE_MODEL_TEST_DER_CONFIG_FILE} ${DEVICE_MODEL_EXAMPLE_CONFIG_LOCATION_V2}/custom/DCDERCtrlr_1.json
|
||||
)
|
||||
|
||||
set(GCOVR_ADDITIONAL_ARGS "--gcov-ignore-parse-errors=negative_hits.warn")
|
||||
|
||||
setup_target_for_coverage_gcovr_html(
|
||||
NAME ${PROJECT_NAME}_gcovr_coverage
|
||||
EXECUTABLE ctest
|
||||
DEPENDENCIES libocpp_unit_tests ${GCOVR_DEPENDENCIES}
|
||||
EXCLUDE "src/*" "tests/*"
|
||||
)
|
||||
|
||||
setup_target_for_coverage_gcovr_xml(
|
||||
NAME ${PROJECT_NAME}_gcovr_coverage_xml
|
||||
EXECUTABLE ctest
|
||||
DEPENDENCIES libocpp_unit_tests ${GCOVR_DEPENDENCIES}
|
||||
EXCLUDE "src/*" "tests/*"
|
||||
)
|
||||
@@ -0,0 +1,60 @@
|
||||
{
|
||||
"Internal": {
|
||||
"ChargePointId": "cp001",
|
||||
"CentralSystemURI": "127.0.0.1:8180/steve/websocket/CentralSystemService/",
|
||||
"ChargeBoxSerialNumber": "cp001",
|
||||
"ChargePointModel": "Yeti",
|
||||
"ChargePointVendor": "Pionix",
|
||||
"FirmwareVersion": "0.1",
|
||||
"LogMessagesFormat": [],
|
||||
"AllowChargingProfileWithoutStartSchedule": true
|
||||
},
|
||||
"Core": {
|
||||
"AuthorizeRemoteTxRequests": false,
|
||||
"ClockAlignedDataInterval": 900,
|
||||
"ConnectionTimeOut": 10,
|
||||
"ConnectorPhaseRotation": "0.RST,1.RST",
|
||||
"GetConfigurationMaxKeys": 100,
|
||||
"HeartbeatInterval": 86400,
|
||||
"LocalAuthorizeOffline": false,
|
||||
"LocalPreAuthorize": false,
|
||||
"MeterValuesAlignedData": "Energy.Active.Import.Register",
|
||||
"MeterValuesSampledData": "Energy.Active.Import.Register",
|
||||
"MeterValueSampleInterval": 0,
|
||||
"NumberOfConnectors": 1,
|
||||
"ResetRetries": 1,
|
||||
"StopTransactionOnEVSideDisconnect": true,
|
||||
"StopTransactionOnInvalidId": true,
|
||||
"StopTxnAlignedData": "Energy.Active.Import.Register",
|
||||
"StopTxnSampledData": "Energy.Active.Import.Register",
|
||||
"SupportedFeatureProfiles": "Core,FirmwareManagement,RemoteTrigger,Reservation,LocalAuthListManagement,SmartCharging",
|
||||
"TransactionMessageAttempts": 1,
|
||||
"TransactionMessageRetryInterval": 10,
|
||||
"UnlockConnectorOnEVSideDisconnect": true
|
||||
},
|
||||
"FirmwareManagement": {
|
||||
"SupportedFileTransferProtocols": "FTP"
|
||||
},
|
||||
"LocalAuthListManagement": {
|
||||
"LocalAuthListEnabled": true,
|
||||
"LocalAuthListMaxLength": 42,
|
||||
"SendLocalListMaxLength": 42
|
||||
},
|
||||
"SmartCharging": {
|
||||
"ChargeProfileMaxStackLevel": 42,
|
||||
"ChargingScheduleAllowedChargingRateUnit": "Current",
|
||||
"ChargingScheduleMaxPeriods": 42,
|
||||
"MaxChargingProfilesInstalled": 42
|
||||
},
|
||||
"Security": {
|
||||
"SecurityProfile": 0
|
||||
},
|
||||
"PnC": {
|
||||
"ISO15118CertificateManagementEnabled": true,
|
||||
"ISO15118PnCEnabled": true,
|
||||
"ContractValidationOffline": true
|
||||
},
|
||||
"CostAndPrice": {
|
||||
"CustomDisplayCostAndPrice": false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{}
|
||||
@@ -0,0 +1,278 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Schema for DCDERCtrlr EVSE 1",
|
||||
"name": "DCDERCtrlr",
|
||||
"evse_id": 1,
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"DCDERCtrlrAvailable": {
|
||||
"variable_name": "Available",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadWrite",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"description": "Whether DC DER control is supported by this EVSE. Default disabled; set to true to enable DER control on this EVSE.",
|
||||
"default": false,
|
||||
"type": "boolean"
|
||||
},
|
||||
"DCDERCtrlrInverterHwVersion": {
|
||||
"variable_name": "InverterHwVersion",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Hardware version of inverter.",
|
||||
"type": "string"
|
||||
},
|
||||
"DCDERCtrlrInverterManufacturer": {
|
||||
"variable_name": "InverterManufacturer",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Manufacturer of inverter.",
|
||||
"type": "string"
|
||||
},
|
||||
"DCDERCtrlrInverterModel": {
|
||||
"variable_name": "InverterModel",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Model of inverter.",
|
||||
"type": "string"
|
||||
},
|
||||
"DCDERCtrlrInverterSwVersion": {
|
||||
"variable_name": "InverterSwVersion",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Software version of inverter.",
|
||||
"type": "string"
|
||||
},
|
||||
"DCDERCtrlrMaxChargeRateVA": {
|
||||
"variable_name": "MaxChargeRateVA",
|
||||
"characteristics": {
|
||||
"unit": "VA",
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "decimal"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Maximum apparent power charge rating in voltamperes; may differ from the apparent power maximum rating.",
|
||||
"type": "number"
|
||||
},
|
||||
"DCDERCtrlrMaxChargeRateW": {
|
||||
"variable_name": "MaxChargeRateW",
|
||||
"characteristics": {
|
||||
"unit": "W",
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "decimal"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Maximum active power charge rating in watts.",
|
||||
"type": "number"
|
||||
},
|
||||
"DCDERCtrlrMaxVA": {
|
||||
"variable_name": "MaxVA",
|
||||
"characteristics": {
|
||||
"unit": "VA",
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "decimal"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Maximum apparent power rating in voltamperes.",
|
||||
"type": "number"
|
||||
},
|
||||
"DCDERCtrlrMaxVar": {
|
||||
"variable_name": "MaxVar",
|
||||
"characteristics": {
|
||||
"unit": "Var",
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "decimal"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Maximum injected reactive power rating in vars.",
|
||||
"type": "number"
|
||||
},
|
||||
"DCDERCtrlrMaxVarNeg": {
|
||||
"variable_name": "MaxVarNeg",
|
||||
"characteristics": {
|
||||
"unit": "Var",
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "decimal"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Maximum absorbed reactive power rating in vars.",
|
||||
"type": "number"
|
||||
},
|
||||
"DCDERCtrlrMaxW": {
|
||||
"variable_name": "MaxW",
|
||||
"characteristics": {
|
||||
"unit": "W",
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "decimal"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Active power rating in watts at unity power factor.",
|
||||
"type": "number"
|
||||
},
|
||||
"DCDERCtrlrModesSupported": {
|
||||
"variable_name": "ModesSupported",
|
||||
"characteristics": {
|
||||
"valuesList": "EnterService,FreqDroop,FreqWatt,FixedPFAbsorb,FixedPFInject,FixedVar,Gradients,HFMustTrip,HFMayTrip,HVMustTrip,HVMomCess,HVMayTrip,LimitMaxDischarge,LFMustTrip,LVMustTrip,LVMomCess,LVMayTrip,PowerMonitoringMustTrip,VoltVar,VoltWatt,WattPF,WattVar",
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "MemberList"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly",
|
||||
"value": "FreqDroop,FreqWatt,VoltWatt,LimitMaxDischarge,VoltVar,FixedVar,EnterService,Gradients"
|
||||
}
|
||||
],
|
||||
"description": "List of DER control modes supported by the DC inverter of this EVSE.",
|
||||
"type": "string"
|
||||
},
|
||||
"DCDERCtrlrOverExcitedPF": {
|
||||
"variable_name": "OverExcitedPF",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "decimal"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Over-excited power factor.",
|
||||
"type": "number"
|
||||
},
|
||||
"DCDERCtrlrOverExcitedW": {
|
||||
"variable_name": "OverExcitedW",
|
||||
"characteristics": {
|
||||
"unit": "W",
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "decimal"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Active power rating in watts at specified over-excited power factor.",
|
||||
"type": "number"
|
||||
},
|
||||
"DCDERCtrlrReactiveSusceptance": {
|
||||
"variable_name": "ReactiveSusceptance",
|
||||
"characteristics": {
|
||||
"unit": "s",
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "decimal"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Reactive susceptance that remains connected to the electrical power system in the cease to energize and trip state.",
|
||||
"type": "number"
|
||||
},
|
||||
"DCDERCtrlrUnderExcitedPF": {
|
||||
"variable_name": "UnderExcitedPF",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "decimal"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Under-excited power factor.",
|
||||
"type": "number"
|
||||
},
|
||||
"DCDERCtrlrUnderExcitedW": {
|
||||
"variable_name": "UnderExcitedW",
|
||||
"characteristics": {
|
||||
"unit": "W",
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "decimal"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Active power rating in watts at specified under-excited power factor.",
|
||||
"type": "number"
|
||||
}
|
||||
},
|
||||
"required": []
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
[pytest]
|
||||
pythonpath=../../../config/v2
|
||||
@@ -0,0 +1,93 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Schema for Connector",
|
||||
"type": "object",
|
||||
"name": "Connector",
|
||||
"evse_id": 1,
|
||||
"connector_id": 1,
|
||||
"properties": {
|
||||
"ConnectorAvailabilityState": {
|
||||
"variable_name": "AvailabilityState",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "OptionList",
|
||||
"valuesList": "Available,Occupied,Reserved,Unavailable,Faulted"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual"
|
||||
}
|
||||
],
|
||||
"description": "This variable reports current availability state for the Connector. Optional, because already reported in StatusNotification.",
|
||||
"type": "string"
|
||||
},
|
||||
"ConnectorAvailable": {
|
||||
"variable_name": "Available",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Component exists",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"ChargeProtocol": {
|
||||
"variable_name": "ChargeProtocol",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual"
|
||||
}
|
||||
],
|
||||
"description": "The Charging Control Protocol applicable to a Connector. CHAdeMO: CHAdeMO protocol, ISO15118: ISO15118 V2G protocol (wired or wireless) as used with CCS, CPPWM: IEC61851-1 / SAE J1772 protocol (ELV DC & PWM signalling via Control Pilot wire), Uncontrolled: No charging power management applies (e.g. Schuko socket), Undetermined: Yet to be determined (e.g. before plugged in), Unknown: Not determinable, NOTE: ChargeProtocol is distinct from and orthogonal to connectorType.",
|
||||
"type": "string"
|
||||
},
|
||||
"ConnectorType": {
|
||||
"variable_name": "ConnectorType",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly",
|
||||
"value": "cGBT"
|
||||
}
|
||||
],
|
||||
"description": "A value of ConnectorEnumType (See part 2) plus additionally: cGBT, cChaoJi, OppCharge. Specific type of connector, including sub-variant information. Note: Distinct and orthogonal to Charging Protocol, Power Type, Phases.",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"ConnectorSupplyPhases": {
|
||||
"variable_name": "SupplyPhases",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "integer"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Number of alternating current phases connected/available.",
|
||||
"type": "integer",
|
||||
"default": "0"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ConnectorAvailable",
|
||||
"ConnectorSupplyPhases",
|
||||
"ConnectorType"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,94 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Schema for Connector",
|
||||
"type": "object",
|
||||
"name": "Connector",
|
||||
"evse_id": 2,
|
||||
"connector_id": 1,
|
||||
"properties": {
|
||||
"ConnectorAvailabilityState": {
|
||||
"variable_name": "AvailabilityState",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "OptionList",
|
||||
"valuesList": "Available,Occupied,Reserved,Unavailable,Faulted"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual"
|
||||
}
|
||||
],
|
||||
"description": "This variable reports current availability state for the Connector. Optional, because already reported in StatusNotification.",
|
||||
"type": "string"
|
||||
},
|
||||
"ConnectorAvailable": {
|
||||
"variable_name": "Available",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly",
|
||||
"value": true
|
||||
}
|
||||
],
|
||||
"description": "Component exists",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"ChargeProtocol": {
|
||||
"variable_name": "ChargeProtocol",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual"
|
||||
}
|
||||
],
|
||||
"description": "The Charging Control Protocol applicable to a Connector. CHAdeMO: CHAdeMO protocol, ISO15118: ISO15118 V2G protocol (wired or wireless) as used with CCS, CPPWM: IEC61851-1 / SAE J1772 protocol (ELV DC & PWM signalling via Control Pilot wire), Uncontrolled: No charging power management applies (e.g. Schuko socket), Undetermined: Yet to be determined (e.g. before plugged in), Unknown: Not determinable, NOTE: ChargeProtocol is distinct from and orthogonal to connectorType.",
|
||||
"type": "string"
|
||||
},
|
||||
"ConnectorType": {
|
||||
"variable_name": "ConnectorType",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly",
|
||||
"value": "cChaoJi"
|
||||
}
|
||||
],
|
||||
"description": "A value of ConnectorEnumType (See part 2) plus additionally: cGBT, cChaoJi, OppCharge. Specific type of connector, including sub-variant information. Note: Distinct and orthogonal to Charging Protocol, Power Type, Phases.",
|
||||
"type": "string",
|
||||
"default": ""
|
||||
},
|
||||
"ConnectorSupplyPhases": {
|
||||
"variable_name": "SupplyPhases",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "integer"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Number of alternating current phases connected/available.",
|
||||
"type": "integer",
|
||||
"default": "0"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ConnectorAvailable",
|
||||
"ConnectorSupplyPhases",
|
||||
"ConnectorType"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Schema for EVSE",
|
||||
"type": "object",
|
||||
"name": "EVSE",
|
||||
"evse_id": 1,
|
||||
"properties": {
|
||||
"EVSEAllowReset": {
|
||||
"variable_name": "AllowReset",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Can be used to announce that an EVSE can be reset individually",
|
||||
"type": "boolean"
|
||||
},
|
||||
"EVSEAvailabilityState": {
|
||||
"variable_name": "AvailabilityState",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "OptionList",
|
||||
"valuesList": "Available,Occupied,Reserved,Unavailable,Faulted"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "This variable reports current availability state for the EVSE",
|
||||
"type": "string",
|
||||
"default": "Unavailable"
|
||||
},
|
||||
"EVSEAvailable": {
|
||||
"variable_name": "Available",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"description": "Component exists",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"EvseId": {
|
||||
"variable_name": "EvseId",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual"
|
||||
}
|
||||
],
|
||||
"description": "The name of the EVSE in the string format as required by ISO 15118 and IEC 63119-2.",
|
||||
"type": "string"
|
||||
},
|
||||
"EVSEPower": {
|
||||
"variable_name": "Power",
|
||||
"characteristics": {
|
||||
"unit": "W",
|
||||
"maxLimit": 22000,
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "decimal"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly",
|
||||
"value": 2000
|
||||
},
|
||||
{
|
||||
"type": "MaxSet",
|
||||
"mutability": "ReadOnly",
|
||||
"value": 44000
|
||||
}
|
||||
],
|
||||
"description": " kW,The variableCharacteristic maxLimit, that holds the maximum power that this EVSE can provide, is required. The Actual value of the instantaneous (real) power is desired, but not required.",
|
||||
"type": "number",
|
||||
"default": "0"
|
||||
},
|
||||
"EVSESupplyPhases": {
|
||||
"variable_name": "SupplyPhases",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "integer"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly",
|
||||
"value": 6
|
||||
}
|
||||
],
|
||||
"description": "Number of alternating current phases connected/available.",
|
||||
"type": "integer",
|
||||
"default": "0"
|
||||
},
|
||||
"ISO15118EvseId": {
|
||||
"variable_name": "ISO15118EvseId",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadWrite"
|
||||
}
|
||||
],
|
||||
"description": "The name of the EVSE in the string format as required by ISO 15118 and IEC 63119-2. Example: \"DE*ICE*E*1234567890*1\"",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"EVSEAvailabilityState",
|
||||
"EVSEAvailable",
|
||||
"EVSEPower",
|
||||
"EVSESupplyPhases"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Schema for EVSE",
|
||||
"type": "object",
|
||||
"name": "EVSE",
|
||||
"evse_id": 2,
|
||||
"properties": {
|
||||
"EVSEAllowReset": {
|
||||
"variable_name": "AllowReset",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Can be used to announce that an EVSE can be reset individually",
|
||||
"type": "boolean"
|
||||
},
|
||||
"EVSEAvailabilityState": {
|
||||
"variable_name": "AvailabilityState",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "OptionList",
|
||||
"valuesList": "Available,Occupied,Reserved,Unavailable,Faulted"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly",
|
||||
"value": "Faulted"
|
||||
}
|
||||
],
|
||||
"description": "This variable reports current availability state for the EVSE",
|
||||
"type": "string",
|
||||
"default": "Unavailable"
|
||||
},
|
||||
"EVSEAvailable": {
|
||||
"variable_name": "Available",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly",
|
||||
"value": true
|
||||
}
|
||||
],
|
||||
"description": "Component exists",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"EvseId": {
|
||||
"variable_name": "EvseId",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual"
|
||||
}
|
||||
],
|
||||
"description": "The name of the EVSE in the string format as required by ISO 15118 and IEC 63119-2.",
|
||||
"type": "string"
|
||||
},
|
||||
"EVSEPower": {
|
||||
"variable_name": "Power",
|
||||
"characteristics": {
|
||||
"unit": "W",
|
||||
"maxLimit": 22000,
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "decimal"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
},
|
||||
{
|
||||
"type": "MaxSet",
|
||||
"mutability": "ReadOnly",
|
||||
"value": 22000
|
||||
}
|
||||
],
|
||||
"description": " kW,The variableCharacteristic maxLimit, that holds the maximum power that this EVSE can provide, is required. The Actual value of the instantaneous (real) power is desired, but not required.",
|
||||
"type": "number",
|
||||
"default": "0"
|
||||
},
|
||||
"EVSESupplyPhases": {
|
||||
"variable_name": "SupplyPhases",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "integer"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Number of alternating current phases connected/available.",
|
||||
"type": "integer",
|
||||
"default": "0"
|
||||
},
|
||||
"ISO15118EvseId": {
|
||||
"variable_name": "ISO15118EvseId",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadWrite"
|
||||
}
|
||||
],
|
||||
"description": "The name of the EVSE in the string format as required by ISO 15118 and IEC 63119-2. Example: \"DE*ICE*E*1234567890*1\"",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"EVSEAvailabilityState",
|
||||
"EVSEAvailable",
|
||||
"EVSEPower",
|
||||
"EVSESupplyPhases"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,58 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Schema for UnitTestCtrlr",
|
||||
"name": "UnitTestCtrlr",
|
||||
"type": "object",
|
||||
"evse_id": 2,
|
||||
"connector_id": 3,
|
||||
"properties": {
|
||||
"UnitTestPropertyA": {
|
||||
"variable_name": "UnitTestPropertyAName",
|
||||
"source": "OCPP",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadWrite"
|
||||
}
|
||||
],
|
||||
"default": true,
|
||||
"type": "boolean"
|
||||
},
|
||||
"UnitTestPropertyB": {
|
||||
"variable_name": "UnitTestPropertyBName",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": false,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly",
|
||||
"value": "test_value"
|
||||
}
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"UnitTestPropertyC": {
|
||||
"variable_name": "UnitTestPropertyCName",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": false,
|
||||
"dataType": "integer"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"type": "integer"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"UnitTestPropertyA"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Schema for Connector",
|
||||
"type": "object",
|
||||
"name": "Connector",
|
||||
"evse_id": 1,
|
||||
"connector_id": 1,
|
||||
"properties": {
|
||||
"ConnectorAvailabilityState": {
|
||||
"variable_name": "AvailabilityState",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "OptionList",
|
||||
"valuesList": "Available,Occupied,Reserved,Unavailable,Faulted"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual"
|
||||
}
|
||||
],
|
||||
"description": "This variable reports current availability state for the Connector. Optional, because already reported in StatusNotification.",
|
||||
"type": "string"
|
||||
},
|
||||
"ConnectorAvailable": {
|
||||
"variable_name": "Available",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly",
|
||||
"value": true
|
||||
}
|
||||
],
|
||||
"description": "Component exists",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"ConnectorEnabled": {
|
||||
"variable_name": "Enabled",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": false,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadWrite"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ChargeProtocol": {
|
||||
"variable_name": "ChargeProtocol",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "WriteOnly"
|
||||
}
|
||||
],
|
||||
"description": "The Charging Control Protocol applicable to a Connector. CHAdeMO: CHAdeMO protocol, ISO15118: ISO15118 V2G protocol (wired or wireless) as used with CCS, CPPWM: IEC61851-1 / SAE J1772 protocol (ELV DC & PWM signalling via Control Pilot wire), Uncontrolled: No charging power management applies (e.g. Schuko socket), Undetermined: Yet to be determined (e.g. before plugged in), Unknown: Not determinable, NOTE: ChargeProtocol is distinct from and orthogonal to connectorType.",
|
||||
"type": "string"
|
||||
},
|
||||
"ConnectorType": {
|
||||
"variable_name": "ConnectorType",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "integer"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly",
|
||||
"value": "2"
|
||||
},
|
||||
{
|
||||
"type": "Target",
|
||||
"mutability": "ReadWrite"
|
||||
}
|
||||
],
|
||||
"description": "A value of ConnectorEnumType (See part 2) plus additionally: cGBT, cChaoJi, OppCharge. Specific type of connector, including sub-variant information. Note: Distinct and orthogonal to Charging Protocol, Power Type, Phases.",
|
||||
"type": "string"
|
||||
},
|
||||
"ConnectorSupplyPhases": {
|
||||
"variable_name": "SupplyPhases",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "integer"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Target",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Number of alternating current phases connected/available.",
|
||||
"type": "integer",
|
||||
"default": "0"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ConnectorAvailable",
|
||||
"ConnectorType"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Schema for Connector",
|
||||
"type": "object",
|
||||
"name": "Connector",
|
||||
"evse_id": 2,
|
||||
"connector_id": 2,
|
||||
"properties": {
|
||||
"ConnectorAvailabilityState": {
|
||||
"variable_name": "AvailabilityState",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "OptionList",
|
||||
"valuesList": "Available,Unavailable"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual"
|
||||
}
|
||||
],
|
||||
"description": "This variable reports current availability state for the Connector. Optional, because already reported in StatusNotification.",
|
||||
"type": "string"
|
||||
},
|
||||
"ConnectorAvailable": {
|
||||
"variable_name": "Available",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Component exists",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ConnectorAvailable"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Schema for EVSE",
|
||||
"type": "object",
|
||||
"name": "EVSE",
|
||||
"evse_id": 1,
|
||||
"properties": {
|
||||
"EVSEAllowReset": {
|
||||
"variable_name": "AllowReset",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Can be used to announce that an EVSE can be reset individually",
|
||||
"type": "boolean"
|
||||
},
|
||||
"EVSEAvailabilityState": {
|
||||
"variable_name": "AvailabilityState",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "OptionList",
|
||||
"valuesList": "Available,Occupied,Reserved,Unavailable,Faulted"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "This variable reports current availability state for the EVSE",
|
||||
"type": "string",
|
||||
"default": "Unavailable"
|
||||
},
|
||||
"EVSEAvailable": {
|
||||
"variable_name": "Available",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Component exists",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"EvseId": {
|
||||
"variable_name": "EvseId",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual"
|
||||
}
|
||||
],
|
||||
"description": "The name of the EVSE in the string format as required by ISO 15118 and IEC 63119-2.",
|
||||
"type": "string"
|
||||
},
|
||||
"EVSEPower": {
|
||||
"variable_name": "Power",
|
||||
"characteristics": {
|
||||
"unit": "kW",
|
||||
"maxLimit": 22000,
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "decimal"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
},
|
||||
{
|
||||
"type": "MaxSet",
|
||||
"mutability": "ReadOnly"
|
||||
},
|
||||
{
|
||||
"type": "Target",
|
||||
"mutability": "ReadWrite"
|
||||
}
|
||||
],
|
||||
"description": " kW,The variableCharacteristic maxLimit, that holds the maximum power that this EVSE can provide, is required. The Actual value of the instantaneous (real) power is desired, but not required.",
|
||||
"type": "number",
|
||||
"default": "0"
|
||||
},
|
||||
"EVSESupplyPhases": {
|
||||
"variable_name": "SupplyPhases",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "integer"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly",
|
||||
"value": 2
|
||||
}
|
||||
],
|
||||
"description": "Number of alternating current phases connected/available.",
|
||||
"type": "integer",
|
||||
"default": "0"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"EVSEAvailabilityState",
|
||||
"EVSEAvailable",
|
||||
"EVSEPower",
|
||||
"EVSESupplyPhases"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Schema for EVSE",
|
||||
"type": "object",
|
||||
"name": "EVSE",
|
||||
"evse_id": 2,
|
||||
"properties": {
|
||||
"EVSEAllowReset": {
|
||||
"variable_name": "AllowReset",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Can be used to announce that an EVSE can be reset individually",
|
||||
"type": "boolean"
|
||||
},
|
||||
"EVSEAvailabilityState": {
|
||||
"variable_name": "AvailabilityState",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": false,
|
||||
"dataType": "string",
|
||||
"valuesList": "Available,Occupied,Reserved,Unavailable,Faulted"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly",
|
||||
"value": "Faulted"
|
||||
}
|
||||
],
|
||||
"description": "This variable reports current availability state for the EVSE",
|
||||
"type": "string",
|
||||
"default": "Unavailable"
|
||||
},
|
||||
"EVSEAvailable": {
|
||||
"variable_name": "Available",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly",
|
||||
"value": false
|
||||
}
|
||||
],
|
||||
"description": "Component exists",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"EvseId": {
|
||||
"variable_name": "EvseId",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual"
|
||||
}
|
||||
],
|
||||
"description": "The name of the EVSE in the string format as required by ISO 15118 and IEC 63119-2.",
|
||||
"type": "string"
|
||||
},
|
||||
"EVSEPower": {
|
||||
"variable_name": "Power",
|
||||
"characteristics": {
|
||||
"unit": "W",
|
||||
"maxLimit": 22000,
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "decimal"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "MaxSet",
|
||||
"mutability": "ReadOnly",
|
||||
"value": 22000
|
||||
}
|
||||
],
|
||||
"description": " kW,The variableCharacteristic maxLimit, that holds the maximum power that this EVSE can provide, is required. The Actual value of the instantaneous (real) power is desired, but not required.",
|
||||
"type": "number",
|
||||
"default": "0"
|
||||
},
|
||||
"EVSESupplyPhases": {
|
||||
"variable_name": "SupplyPhases",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "integer"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Number of alternating current phases connected/available.",
|
||||
"type": "integer",
|
||||
"default": "0"
|
||||
},
|
||||
"ISO15118EvseId": {
|
||||
"variable_name": "ISO15118EvseId",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadWrite"
|
||||
}
|
||||
],
|
||||
"description": "The name of the EVSE in the string format as required by ISO 15118 and IEC 63119-2. Example: \"DE*ICE*E*1234567890*1\"",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"EVSEAvailabilityState",
|
||||
"EVSEAvailable",
|
||||
"EVSESupplyPhases"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,130 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Schema for EVSE",
|
||||
"type": "object",
|
||||
"name": "EVSE",
|
||||
"evse_id": 3,
|
||||
"properties": {
|
||||
"EVSEAllowReset": {
|
||||
"variable_name": "AllowReset",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Can be used to announce that an EVSE can be reset individually",
|
||||
"type": "boolean"
|
||||
},
|
||||
"EVSEAvailabilityState": {
|
||||
"variable_name": "AvailabilityState",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "OptionList",
|
||||
"valuesList": "Available,Occupied,Reserved,Unavailable,Faulted"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "This variable reports current availability state for the EVSE",
|
||||
"type": "string",
|
||||
"default": "Unavailable"
|
||||
},
|
||||
"EVSEAvailable": {
|
||||
"variable_name": "Available",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Component exists",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"EvseId": {
|
||||
"variable_name": "EvseId",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual"
|
||||
}
|
||||
],
|
||||
"description": "The name of the EVSE in the string format as required by ISO 15118 and IEC 63119-2.",
|
||||
"type": "string"
|
||||
},
|
||||
"EVSEPower": {
|
||||
"variable_name": "Power",
|
||||
"characteristics": {
|
||||
"unit": "W",
|
||||
"maxLimit": 22000,
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "decimal"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
},
|
||||
{
|
||||
"type": "MaxSet",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": " kW,The variableCharacteristic maxLimit, that holds the maximum power that this EVSE can provide, is required. The Actual value of the instantaneous (real) power is desired, but not required.",
|
||||
"type": "number",
|
||||
"default": "0"
|
||||
},
|
||||
"EVSESupplyPhases": {
|
||||
"variable_name": "SupplyPhases",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "integer"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Number of alternating current phases connected/available.",
|
||||
"type": "integer",
|
||||
"default": "0"
|
||||
},
|
||||
"ISO15118EvseId": {
|
||||
"variable_name": "ISO15118EvseId",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadWrite"
|
||||
}
|
||||
],
|
||||
"description": "The name of the EVSE in the string format as required by ISO 15118 and IEC 63119-2. Example: \"DE*ICE*E*1234567890*1\"",
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"EVSEAvailabilityState",
|
||||
"EVSEAvailable",
|
||||
"EVSEPower",
|
||||
"EVSESupplyPhases"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Schema for UnitTestCtrlr",
|
||||
"name": "UnitTestCtrlr",
|
||||
"type": "object",
|
||||
"evse_id": 2,
|
||||
"connector_id": 3,
|
||||
"properties": {
|
||||
"UnitTestPropertyA": {
|
||||
"variable_name": "UnitTestPropertyAName",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadWrite"
|
||||
}
|
||||
],
|
||||
"default": "1",
|
||||
"type": "string"
|
||||
},
|
||||
"UnitTestPropertyB": {
|
||||
"variable_name": "UnitTestPropertyBName",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": false,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"type": "string"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"UnitTestPropertyA"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Schema for Connector",
|
||||
"type": "object",
|
||||
"name": "Connector",
|
||||
"evse_id": 1,
|
||||
"connector_id": 1,
|
||||
"properties": {
|
||||
"ConnectorAvailabilityState": {
|
||||
"variable_name": "AvailabilityState",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "OptionList",
|
||||
"valuesList": "Available,Occupied,Reserved,Unavailable,Faulted"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual"
|
||||
}
|
||||
],
|
||||
"description": "This variable reports current availability state for the Connector. Optional, because already reported in StatusNotification.",
|
||||
"type": "string"
|
||||
},
|
||||
"ConnectorAvailable": {
|
||||
"variable_name": "Available",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Component exists",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"ConnectorEnabled": {
|
||||
"variable_name": "Enabled",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": false,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadWrite"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ChargeProtocol": {
|
||||
"variable_name": "ChargeProtocol",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "WriteOnly"
|
||||
}
|
||||
],
|
||||
"description": "The Charging Control Protocol applicable to a Connector. CHAdeMO: CHAdeMO protocol, ISO15118: ISO15118 V2G protocol (wired or wireless) as used with CCS, CPPWM: IEC61851-1 / SAE J1772 protocol (ELV DC & PWM signalling via Control Pilot wire), Uncontrolled: No charging power management applies (e.g. Schuko socket), Undetermined: Yet to be determined (e.g. before plugged in), Unknown: Not determinable, NOTE: ChargeProtocol is distinct from and orthogonal to connectorType.",
|
||||
"type": "string"
|
||||
},
|
||||
"ConnectorType": {
|
||||
"variable_name": "ConnectorType",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly",
|
||||
"value": "cCCS2"
|
||||
},
|
||||
{
|
||||
"type": "Target",
|
||||
"mutability": "ReadWrite"
|
||||
}
|
||||
],
|
||||
"description": "A value of ConnectorEnumType (See part 2) plus additionally: cGBT, cChaoJi, OppCharge. Specific type of connector, including sub-variant information. Note: Distinct and orthogonal to Charging Protocol, Power Type, Phases.",
|
||||
"type": "string",
|
||||
"default": "0"
|
||||
},
|
||||
"ConnectorSupplyPhases": {
|
||||
"variable_name": "SupplyPhases",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "integer"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Target",
|
||||
"mutability": "ReadOnly"
|
||||
},
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Number of alternating current phases connected/available.",
|
||||
"type": "integer",
|
||||
"default": "0"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ConnectorAvailable",
|
||||
"ConnectorSupplyPhases",
|
||||
"ConnectorType"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Schema for EVSE",
|
||||
"type": "object",
|
||||
"name": "EVSE",
|
||||
"evse_id": 1,
|
||||
"properties": {
|
||||
"EVSEAllowReset": {
|
||||
"variable_name": "AllowReset",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Can be used to announce that an EVSE can be reset individually",
|
||||
"type": "boolean"
|
||||
},
|
||||
"EVSEAvailabilityState": {
|
||||
"variable_name": "AvailabilityState",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "OptionList",
|
||||
"valuesList": "Available,Occupied,Reserved,Unavailable,Faulted"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "This variable reports current availability state for the EVSE",
|
||||
"type": "string",
|
||||
"default": "Unavailable"
|
||||
},
|
||||
"EVSEAvailable": {
|
||||
"variable_name": "Available",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Component exists",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"EvseId": {
|
||||
"variable_name": "EvseId",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual"
|
||||
}
|
||||
],
|
||||
"description": "The name of the EVSE in the string format as required by ISO 15118 and IEC 63119-2.",
|
||||
"type": "string"
|
||||
},
|
||||
"EVSEPower": {
|
||||
"variable_name": "Power",
|
||||
"characteristics": {
|
||||
"unit": "kW",
|
||||
"maxLimit": 22000,
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "decimal"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
},
|
||||
{
|
||||
"type": "MaxSet",
|
||||
"mutability": "ReadOnly"
|
||||
},
|
||||
{
|
||||
"type": "Target",
|
||||
"mutability": "ReadWrite"
|
||||
}
|
||||
],
|
||||
"description": " kW,The variableCharacteristic maxLimit, that holds the maximum power that this EVSE can provide, is required. The Actual value of the instantaneous (real) power is desired, but not required.",
|
||||
"type": "number",
|
||||
"default": "0"
|
||||
},
|
||||
"EVSESupplyPhases": {
|
||||
"variable_name": "SupplyPhases",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "integer"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly",
|
||||
"value": 2
|
||||
}
|
||||
],
|
||||
"description": "Number of alternating current phases connected/available.",
|
||||
"type": "integer",
|
||||
"default": "0"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"EVSEAvailabilityState",
|
||||
"EVSEAvailable",
|
||||
"EVSEPower",
|
||||
"EVSESupplyPhases"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Schema for UnitTestCtrlr",
|
||||
"name": "UnitTestCtrlr",
|
||||
"type": "object",
|
||||
"evse_id": 2,
|
||||
"connector_id": 3,
|
||||
"properties": {
|
||||
"UnitTestPropertyA": {
|
||||
"variable_name": "UnitTestPropertyAName",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadWrite"
|
||||
}
|
||||
],
|
||||
"type": "boolean"
|
||||
},
|
||||
"UnitTestPropertyB": {
|
||||
"variable_name": "UnitTestPropertyBName",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": false,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly",
|
||||
"value": "test_value"
|
||||
}
|
||||
],
|
||||
"type": "string"
|
||||
},
|
||||
"UnitTestPropertyC": {
|
||||
"variable_name": "UnitTestPropertyCName",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": false,
|
||||
"dataType": "integer"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Target",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"type": "integer",
|
||||
"default": 42
|
||||
},
|
||||
"UnitTestPropertyD": {
|
||||
"variable_name": "UnitTestPropertyDName",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": false,
|
||||
"dataType": "integer"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"type": "integer",
|
||||
"default": 42
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"UnitTestPropertyA",
|
||||
"UnitTestPropertyB",
|
||||
"UnitTestPropertyC",
|
||||
"UnitTestPropertyD"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Schema for Connector",
|
||||
"type": "object",
|
||||
"name": "Connector",
|
||||
"evse_id": 1,
|
||||
"connector_id": 1,
|
||||
"properties": {
|
||||
"ConnectorAvailabilityState": {
|
||||
"variable_name": "AvailabilityState",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "OptionList",
|
||||
"valuesList": "Available,Occupied,Reserved,Unavailable,Faulted"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual"
|
||||
}
|
||||
],
|
||||
"description": "This variable reports current availability state for the Connector. Optional, because already reported in StatusNotification.",
|
||||
"type": "string"
|
||||
},
|
||||
"ConnectorAvailable": {
|
||||
"variable_name": "Available",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Component exists",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"ConnectorEnabled": {
|
||||
"variable_name": "Enabled",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": false,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadWrite"
|
||||
}
|
||||
]
|
||||
},
|
||||
"ChargeProtocol": {
|
||||
"variable_name": "ChargeProtocol",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "WriteOnly"
|
||||
}
|
||||
],
|
||||
"description": "The Charging Control Protocol applicable to a Connector. CHAdeMO: CHAdeMO protocol, ISO15118: ISO15118 V2G protocol (wired or wireless) as used with CCS, CPPWM: IEC61851-1 / SAE J1772 protocol (ELV DC & PWM signalling via Control Pilot wire), Uncontrolled: No charging power management applies (e.g. Schuko socket), Undetermined: Yet to be determined (e.g. before plugged in), Unknown: Not determinable, NOTE: ChargeProtocol is distinct from and orthogonal to connectorType.",
|
||||
"type": "string"
|
||||
},
|
||||
"ConnectorType": {
|
||||
"variable_name": "ConnectorType",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly",
|
||||
"value": "OppCharge"
|
||||
},
|
||||
{
|
||||
"type": "Target",
|
||||
"mutability": "ReadWrite"
|
||||
}
|
||||
],
|
||||
"description": "A value of ConnectorEnumType (See part 2) plus additionally: cGBT, cChaoJi, OppCharge. Specific type of connector, including sub-variant information. Note: Distinct and orthogonal to Charging Protocol, Power Type, Phases.",
|
||||
"type": "string",
|
||||
"default": "sType2"
|
||||
},
|
||||
"ConnectorSupplyPhases": {
|
||||
"variable_name": "SupplyPhases",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "integer"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Target",
|
||||
"mutability": "ReadOnly"
|
||||
},
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Number of alternating current phases connected/available.",
|
||||
"type": "integer",
|
||||
"default": "0"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"ConnectorAvailable",
|
||||
"ConnectorSupplyPhases",
|
||||
"ConnectorType"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Schema for EVSE",
|
||||
"type": "object",
|
||||
"name": "EVSE",
|
||||
"evse_id": 1,
|
||||
"properties": {
|
||||
"EVSEAllowReset": {
|
||||
"variable_name": "AllowReset",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Can be used to announce that an EVSE can be reset individually",
|
||||
"type": "boolean"
|
||||
},
|
||||
"EVSEAvailabilityState": {
|
||||
"variable_name": "AvailabilityState",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "OptionList",
|
||||
"valuesList": "Available,Occupied,Reserved,Unavailable,Faulted"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "This variable reports current availability state for the EVSE",
|
||||
"type": "string",
|
||||
"default": "Unavailable"
|
||||
},
|
||||
"EVSEAvailable": {
|
||||
"variable_name": "Available",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"description": "Component exists",
|
||||
"type": "boolean",
|
||||
"default": false
|
||||
},
|
||||
"EvseId": {
|
||||
"variable_name": "EvseId",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "string"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual"
|
||||
}
|
||||
],
|
||||
"description": "The name of the EVSE in the string format as required by ISO 15118 and IEC 63119-2.",
|
||||
"type": "string"
|
||||
},
|
||||
"EVSEPower": {
|
||||
"variable_name": "Power",
|
||||
"characteristics": {
|
||||
"unit": "kW",
|
||||
"maxLimit": 22000,
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "decimal"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
},
|
||||
{
|
||||
"type": "MaxSet",
|
||||
"mutability": "ReadOnly"
|
||||
},
|
||||
{
|
||||
"type": "Target",
|
||||
"mutability": "ReadWrite"
|
||||
}
|
||||
],
|
||||
"description": " kW,The variableCharacteristic maxLimit, that holds the maximum power that this EVSE can provide, is required. The Actual value of the instantaneous (real) power is desired, but not required.",
|
||||
"type": "number",
|
||||
"default": "0"
|
||||
},
|
||||
"EVSESupplyPhases": {
|
||||
"variable_name": "SupplyPhases",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "integer"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly",
|
||||
"value": 2
|
||||
}
|
||||
],
|
||||
"description": "Number of alternating current phases connected/available.",
|
||||
"type": "integer",
|
||||
"default": "0"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"EVSEAvailabilityState",
|
||||
"EVSEAvailable",
|
||||
"EVSEPower",
|
||||
"EVSESupplyPhases"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Schema for UnitTestCtrlr",
|
||||
"name": "UnitTestCtrlr",
|
||||
"type": "object",
|
||||
"evse_id": 2,
|
||||
"connector_id": 3,
|
||||
"properties": {
|
||||
"UnitTestPropertyA": {
|
||||
"variable_name": "UnitTestPropertyAName",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": true,
|
||||
"dataType": "boolean"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadWrite",
|
||||
"value": 42
|
||||
}
|
||||
],
|
||||
"type": "boolean"
|
||||
},
|
||||
"UnitTestPropertyB": {
|
||||
"variable_name": "UnitTestPropertyBName",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": false,
|
||||
"dataType": "decimal"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly",
|
||||
"value": "test_value"
|
||||
}
|
||||
],
|
||||
"type": "number",
|
||||
"default": "test"
|
||||
},
|
||||
"UnitTestPropertyC": {
|
||||
"variable_name": "UnitTestPropertyCName",
|
||||
"characteristics": {
|
||||
"supportsMonitoring": false,
|
||||
"dataType": "integer"
|
||||
},
|
||||
"attributes": [
|
||||
{
|
||||
"type": "Actual",
|
||||
"mutability": "ReadOnly"
|
||||
}
|
||||
],
|
||||
"type": "integer",
|
||||
"default": "true"
|
||||
}
|
||||
},
|
||||
"required": [
|
||||
"UnitTestPropertyA"
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
target_sources(libocpp_unit_tests PRIVATE
|
||||
test_database_migration_files.cpp
|
||||
test_message_queue.cpp
|
||||
test_websocket_uri.cpp
|
||||
)
|
||||
|
||||
|
||||
set(TEST_UTILS_SOURCES ${LIBOCPP_LIB_PATH}/ocpp/common/utils.cpp)
|
||||
|
||||
target_sources(libocpp_utils_tests PRIVATE ${TEST_UTILS_SOURCES})
|
||||
@@ -0,0 +1,48 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <everest/database/sqlite/connection.hpp>
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
using namespace std::string_literals;
|
||||
|
||||
class DatabaseTestingUtils : public ::testing::Test {
|
||||
|
||||
protected:
|
||||
std::unique_ptr<everest::db::sqlite::ConnectionInterface> database;
|
||||
|
||||
public:
|
||||
DatabaseTestingUtils() : database(std::make_unique<everest::db::sqlite::Connection>("file::memory:?cache=shared")) {
|
||||
EXPECT_TRUE(this->database->open_connection());
|
||||
}
|
||||
|
||||
void ExpectUserVersion(std::uint32_t expected_version) {
|
||||
auto statement = this->database->new_statement("PRAGMA user_version");
|
||||
|
||||
EXPECT_EQ(statement->step(), SQLITE_ROW);
|
||||
EXPECT_EQ(statement->column_int(0), expected_version);
|
||||
}
|
||||
|
||||
void SetUserVersion(std::uint32_t user_version) {
|
||||
EXPECT_TRUE(this->database->execute_statement("PRAGMA user_version = "s + std::to_string(user_version)));
|
||||
}
|
||||
|
||||
bool DoesTableExist(std::string_view table) {
|
||||
const std::string statement = "SELECT name FROM sqlite_master WHERE type='table' AND name=@table_name";
|
||||
|
||||
std::unique_ptr<everest::db::sqlite::StatementInterface> table_exists_statement =
|
||||
this->database->new_statement(statement);
|
||||
table_exists_statement->bind_text("@table_name", std::string(table),
|
||||
everest::db::sqlite::SQLiteString::Transient);
|
||||
const int status = table_exists_statement->step();
|
||||
const int number_of_rows = table_exists_statement->get_number_of_rows();
|
||||
return status != SQLITE_ERROR && number_of_rows == 1;
|
||||
}
|
||||
|
||||
bool DoesColumnExist(std::string_view table, std::string_view column) {
|
||||
return this->database->execute_statement("SELECT "s + column.data() + " FROM " + table.data() + " LIMIT 1;");
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef OCPP_EVSE_SECURITY_MOCK_H
|
||||
#define OCPP_EVSE_SECURITY_MOCK_H
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <ocpp/common/evse_security.hpp>
|
||||
|
||||
namespace ocpp {
|
||||
|
||||
class EvseSecurityMock : public EvseSecurity {
|
||||
public:
|
||||
MOCK_METHOD(InstallCertificateResult, install_ca_certificate, (const std::string&, const CaCertificateType&),
|
||||
(override));
|
||||
MOCK_METHOD(DeleteCertificateResult, delete_certificate, (const ocpp::CertificateHashDataType&), (override));
|
||||
MOCK_METHOD(InstallCertificateResult, update_leaf_certificate,
|
||||
(const std::string&, const CertificateSigningUseEnum&), (override));
|
||||
MOCK_METHOD(CertificateValidationResult, verify_certificate, (const std::string&, const LeafCertificateType&),
|
||||
(override));
|
||||
MOCK_METHOD(CertificateValidationResult, verify_certificate,
|
||||
(const std::string&, const std::vector<LeafCertificateType>&), (override));
|
||||
MOCK_METHOD(std::vector<CertificateHashDataChain>, get_installed_certificates,
|
||||
(const std::vector<CertificateType>&), (override));
|
||||
MOCK_METHOD(std::vector<OCSPRequestData>, get_v2g_ocsp_request_data, (), (override));
|
||||
MOCK_METHOD(std::vector<OCSPRequestData>, get_mo_ocsp_request_data, (const std::string&), (override));
|
||||
MOCK_METHOD(void, update_ocsp_cache, (const CertificateHashDataType&, const std::string&), (override));
|
||||
MOCK_METHOD(bool, is_ca_certificate_installed, (const CaCertificateType&), (override));
|
||||
MOCK_METHOD(GetCertificateSignRequestResult, generate_certificate_signing_request,
|
||||
(const CertificateSigningUseEnum&, const std::string&, const std::string&, const std::string&, bool),
|
||||
(override));
|
||||
MOCK_METHOD(GetCertificateInfoResult, get_leaf_certificate_info, (const CertificateSigningUseEnum&, bool),
|
||||
(override));
|
||||
MOCK_METHOD(bool, update_certificate_links, (const CertificateSigningUseEnum&), (override));
|
||||
MOCK_METHOD(std::string, get_verify_file, (const CaCertificateType&), (override));
|
||||
MOCK_METHOD(std::string, get_verify_location, (const CaCertificateType&), (override));
|
||||
MOCK_METHOD(int, get_leaf_expiry_days_count, (const CertificateSigningUseEnum&), (override));
|
||||
};
|
||||
|
||||
} // namespace ocpp
|
||||
|
||||
#endif // OCPP_EVSE_SECURITY_MOCK_H
|
||||
@@ -0,0 +1,49 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "ocpp/v16/types.hpp"
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <memory>
|
||||
#include <ocpp/common/message_queue.hpp>
|
||||
|
||||
namespace {
|
||||
using namespace ocpp;
|
||||
|
||||
#if 0
|
||||
MessageQueue(
|
||||
const std::function<bool(json message)>& send_callback, const MessageQueueConfig<M>& config,
|
||||
const std::vector<M>& external_notify, std::shared_ptr<common::DatabaseHandlerCommon> database_handler,
|
||||
const std::function<void(const std::string& new_message_id, const std::string& old_message_id)>
|
||||
start_transaction_message_retry_callback =
|
||||
[](const std::string& new_message_id, const std::string& old_message_id) {})
|
||||
#endif
|
||||
|
||||
bool send_callback(json message) {
|
||||
return true;
|
||||
}
|
||||
|
||||
void start_transaction_message_retry_callback(const std::string& new_message_id, const std::string& old_message_id) {
|
||||
}
|
||||
|
||||
struct DatabaseHandlerCommonTest : public common::DatabaseHandlerCommon {
|
||||
DatabaseHandlerCommonTest() :
|
||||
common::DatabaseHandlerCommon(std::unique_ptr<common::DatabaseConnectionInterface>{}, "", 1) {
|
||||
}
|
||||
void init_sql() override {
|
||||
}
|
||||
};
|
||||
|
||||
TEST(MessageQueue, init) {
|
||||
MessageQueueConfig<v16::MessageType> config;
|
||||
std::vector<v16::MessageType> external_notify;
|
||||
std::shared_ptr<common::DatabaseHandlerCommon> database_handler = std::make_shared<DatabaseHandlerCommonTest>();
|
||||
|
||||
MessageQueue<v16::MessageType> queue(&send_callback, config, external_notify, database_handler,
|
||||
&start_transaction_message_retry_callback);
|
||||
|
||||
queue.start();
|
||||
queue.stop();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,22 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
|
||||
MATCHER_P2(PeriodEquals, start, limit,
|
||||
"Period start " + testing::DescribeMatcher<std::int32_t>(start, negation) + " and limit " +
|
||||
testing::DescribeMatcher<float>(limit, negation)) {
|
||||
return ExplainMatchResult(start, arg.startPeriod, result_listener) &&
|
||||
ExplainMatchResult(limit, arg.limit, result_listener);
|
||||
}
|
||||
|
||||
MATCHER_P3(PeriodEqualsWithPhases, start, limit, phases,
|
||||
"Period start " + testing::DescribeMatcher<std::int32_t>(start, negation) + " and limit " +
|
||||
testing::DescribeMatcher<float>(limit, negation) + " and phases " +
|
||||
testing::DescribeMatcher<std::optional<std::int32_t>>(phases, negation)) {
|
||||
return ExplainMatchResult(start, arg.startPeriod, result_listener) &&
|
||||
ExplainMatchResult(limit, arg.limit, result_listener) &&
|
||||
ExplainMatchResult(phases, arg.numberPhases, result_listener);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "test_database_migration_files.hpp"
|
||||
|
||||
TEST_P(DatabaseMigrationFilesTest, ApplyMigrationFilesStepByStep) {
|
||||
everest::db::sqlite::SchemaUpdater updater{this->database.get()};
|
||||
|
||||
for (std::uint32_t i = 1; i <= this->max_version; i++) {
|
||||
EXPECT_TRUE(updater.apply_migration_files(this->migration_files_path, i));
|
||||
this->ExpectUserVersion(i);
|
||||
}
|
||||
|
||||
for (std::uint32_t i = this->max_version; i > 0; i--) {
|
||||
EXPECT_TRUE(updater.apply_migration_files(this->migration_files_path, i));
|
||||
this->ExpectUserVersion(i);
|
||||
}
|
||||
}
|
||||
|
||||
TEST_P(DatabaseMigrationFilesTest, ApplyMigrationFilesAtOnce) {
|
||||
everest::db::sqlite::SchemaUpdater updater{this->database.get()};
|
||||
|
||||
EXPECT_TRUE(updater.apply_migration_files(this->migration_files_path, this->max_version));
|
||||
this->ExpectUserVersion(this->max_version);
|
||||
|
||||
EXPECT_TRUE(updater.apply_migration_files(this->migration_files_path, 1));
|
||||
this->ExpectUserVersion(1);
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "database_testing_utils.hpp"
|
||||
#include <everest/database/sqlite/schema_updater.hpp>
|
||||
|
||||
class DatabaseMigrationFilesTest
|
||||
: public DatabaseTestingUtils,
|
||||
public ::testing::WithParamInterface<std::tuple<std::filesystem::path, std::uint32_t>> {
|
||||
|
||||
protected:
|
||||
const std::filesystem::path migration_files_path;
|
||||
const std::uint32_t max_version;
|
||||
|
||||
public:
|
||||
DatabaseMigrationFilesTest() :
|
||||
DatabaseTestingUtils(),
|
||||
migration_files_path(std::get<std::filesystem::path>(GetParam())),
|
||||
max_version(std::get<std::uint32_t>(GetParam())) {
|
||||
EXPECT_TRUE(this->database->open_connection());
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,837 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <nlohmann/json.hpp>
|
||||
#include <ocpp/common/message_queue.hpp>
|
||||
|
||||
namespace ocpp {
|
||||
|
||||
using json = nlohmann::json;
|
||||
|
||||
/************************************************************************************************
|
||||
* Test Message Types
|
||||
*/
|
||||
|
||||
enum class TestMessageType {
|
||||
TRANSACTIONAL,
|
||||
TRANSACTIONAL_RESPONSE,
|
||||
TRANSACTIONAL_UPDATE,
|
||||
TRANSACTIONAL_UPDATE_RESPONSE,
|
||||
NON_TRANSACTIONAL,
|
||||
NON_TRANSACTIONAL_RESPONSE,
|
||||
InternalError,
|
||||
BootNotification,
|
||||
BootNotificationResponse
|
||||
};
|
||||
|
||||
static std::string to_string(TestMessageType m) {
|
||||
switch (m) {
|
||||
case TestMessageType::TRANSACTIONAL:
|
||||
return "transactional";
|
||||
case TestMessageType::TRANSACTIONAL_RESPONSE:
|
||||
return "transactionalResponse";
|
||||
case TestMessageType::TRANSACTIONAL_UPDATE:
|
||||
return "transactional_update";
|
||||
case TestMessageType::TRANSACTIONAL_UPDATE_RESPONSE:
|
||||
return "transactional_updateResponse";
|
||||
case TestMessageType::NON_TRANSACTIONAL:
|
||||
return "non_transactional";
|
||||
case TestMessageType::NON_TRANSACTIONAL_RESPONSE:
|
||||
return "non_transactionalResponse";
|
||||
case TestMessageType::InternalError:
|
||||
return "internal_error";
|
||||
case TestMessageType::BootNotification:
|
||||
return "boot_notification";
|
||||
case TestMessageType::BootNotificationResponse:
|
||||
return "boot_notificationResponse";
|
||||
}
|
||||
throw std::out_of_range("unknown TestMessageType");
|
||||
};
|
||||
|
||||
static TestMessageType to_test_message_type(const std::string& s) {
|
||||
if (s == "transactional") {
|
||||
return TestMessageType::TRANSACTIONAL;
|
||||
}
|
||||
if (s == "transactionalResponse") {
|
||||
return TestMessageType::TRANSACTIONAL_RESPONSE;
|
||||
}
|
||||
if (s == "transactional_update") {
|
||||
return TestMessageType::TRANSACTIONAL_UPDATE;
|
||||
}
|
||||
if (s == "transactional_updateResponse") {
|
||||
return TestMessageType::TRANSACTIONAL_UPDATE_RESPONSE;
|
||||
}
|
||||
if (s == "non_transactional") {
|
||||
return TestMessageType::NON_TRANSACTIONAL;
|
||||
}
|
||||
if (s == "non_transactionalResponse") {
|
||||
return TestMessageType::NON_TRANSACTIONAL_RESPONSE;
|
||||
}
|
||||
if (s == "internal_error") {
|
||||
return TestMessageType::InternalError;
|
||||
}
|
||||
if (s == "boot_notification") {
|
||||
return TestMessageType::BootNotification;
|
||||
}
|
||||
if (s == "boot_notificationResponse") {
|
||||
return TestMessageType::BootNotificationResponse;
|
||||
}
|
||||
throw std::out_of_range("unknown string for TestMessageType");
|
||||
};
|
||||
|
||||
struct TestRequest : Message {
|
||||
TestMessageType type = TestMessageType::NON_TRANSACTIONAL;
|
||||
std::optional<std::string> data;
|
||||
std::string get_type() const {
|
||||
return to_string(type);
|
||||
};
|
||||
};
|
||||
|
||||
void to_json(json& j, const TestRequest& k) {
|
||||
j = json{};
|
||||
if (k.data) {
|
||||
j["data"] = k.data.value();
|
||||
}
|
||||
}
|
||||
|
||||
void from_json(const json& j, TestRequest& k) {
|
||||
if (j.contains("data")) {
|
||||
k.data.emplace(j.at("data"));
|
||||
}
|
||||
}
|
||||
|
||||
template <> std::string MessageQueue<TestMessageType>::messagetype_to_string(TestMessageType m) {
|
||||
return to_string(m);
|
||||
}
|
||||
|
||||
template <> TestMessageType MessageQueue<TestMessageType>::string_to_messagetype(const std::string& s) {
|
||||
return to_test_message_type(s);
|
||||
}
|
||||
|
||||
template <> ControlMessage<TestMessageType>::ControlMessage(const json& message, bool stall_until_accepted) {
|
||||
this->message = message.get<json::array_t>();
|
||||
EVLOG_info << this->message;
|
||||
this->messageType = to_test_message_type(this->message[2]);
|
||||
this->message_attempts = 0;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const TestMessageType& message_type) {
|
||||
os << to_string(message_type);
|
||||
return os;
|
||||
};
|
||||
|
||||
bool is_transaction_message(const TestMessageType message_type) {
|
||||
return (message_type == TestMessageType::TRANSACTIONAL) || (message_type == TestMessageType::TRANSACTIONAL_UPDATE);
|
||||
}
|
||||
|
||||
bool is_start_transaction_message(const TestMessageType message_type) {
|
||||
return false;
|
||||
}
|
||||
|
||||
template <> bool ControlMessage<TestMessageType>::is_transaction_update_message() const {
|
||||
return this->messageType == TestMessageType::TRANSACTIONAL_UPDATE;
|
||||
}
|
||||
|
||||
bool is_boot_notification_message(const TestMessageType message_type) {
|
||||
return message_type == TestMessageType::BootNotification;
|
||||
}
|
||||
|
||||
/************************************************************************************************
|
||||
* MessageQueueTest
|
||||
*/
|
||||
|
||||
class DatabaseHandlerBaseMock : public common::DatabaseHandlerCommon {
|
||||
private:
|
||||
void init_sql() override {
|
||||
}
|
||||
|
||||
public:
|
||||
DatabaseHandlerBaseMock() : common::DatabaseHandlerCommon(nullptr, "", 1) {
|
||||
}
|
||||
|
||||
MOCK_METHOD(std::vector<common::DBTransactionMessage>, get_message_queue_messages, (const QueueType), (override));
|
||||
MOCK_METHOD(void, insert_message_queue_message, (const common::DBTransactionMessage&, const QueueType), (override));
|
||||
MOCK_METHOD(void, remove_message_queue_message, (const std::string&, const QueueType), (override));
|
||||
};
|
||||
|
||||
class MessageQueueTest : public ::testing::Test {
|
||||
int internal_message_count{0};
|
||||
int call_count{0};
|
||||
|
||||
protected:
|
||||
MessageQueueConfig<TestMessageType> config{};
|
||||
std::shared_ptr<DatabaseHandlerBaseMock> db;
|
||||
std::mutex call_marker_mutex;
|
||||
std::condition_variable call_marker_cond_var;
|
||||
testing::MockFunction<bool(json message)> send_callback_mock;
|
||||
Everest::SteadyTimer reception_timer;
|
||||
std::unique_ptr<MessageQueue<TestMessageType>> message_queue;
|
||||
|
||||
int get_call_count() {
|
||||
std::lock_guard<std::mutex> lock(call_marker_mutex);
|
||||
return call_count;
|
||||
}
|
||||
|
||||
/// \brief Increments call_count and notifies the condition variable.
|
||||
/// Use this from custom Invoke lambdas instead of accessing call_count directly.
|
||||
void mark_call_sent() {
|
||||
std::lock_guard<std::mutex> lock(call_marker_mutex);
|
||||
this->call_count++;
|
||||
this->call_marker_cond_var.notify_one();
|
||||
}
|
||||
|
||||
template <typename R> auto MarkAndReturn(R value, bool respond = false) {
|
||||
return testing::Invoke([this, value, respond](const json::array_t& s) -> R {
|
||||
if (respond) {
|
||||
reception_timer.timeout(
|
||||
[this, s]() {
|
||||
this->message_queue->receive(json{3, s[1], ""}.dump());
|
||||
},
|
||||
std::chrono::milliseconds(0));
|
||||
}
|
||||
std::lock_guard<std::mutex> lock(call_marker_mutex);
|
||||
this->call_count++;
|
||||
this->call_marker_cond_var.notify_one();
|
||||
return value;
|
||||
});
|
||||
}
|
||||
|
||||
void wait_for_calls(int expected_calls = 1, std::chrono::seconds timeout = std::chrono::seconds(3)) {
|
||||
std::unique_lock<std::mutex> lock(call_marker_mutex);
|
||||
EXPECT_TRUE(call_marker_cond_var.wait_for(
|
||||
lock, timeout, [this, expected_calls] { return this->call_count >= expected_calls; }));
|
||||
}
|
||||
|
||||
std::string push_message_call(const TestMessageType& message_type) {
|
||||
std::stringstream stream;
|
||||
stream << "test_call_" << internal_message_count;
|
||||
std::string unique_identifier = stream.str();
|
||||
internal_message_count++;
|
||||
return push_message_call(message_type, unique_identifier);
|
||||
}
|
||||
|
||||
std::string push_message_call(const TestMessageType& message_type, const std::string& identifier) {
|
||||
Call<TestRequest> call;
|
||||
call.msg.type = message_type;
|
||||
call.msg.data = identifier;
|
||||
call.uniqueId = identifier;
|
||||
message_queue->push_call(call);
|
||||
return identifier;
|
||||
}
|
||||
|
||||
void init_message_queue() {
|
||||
message_queue = std::make_unique<MessageQueue<TestMessageType>>(send_callback_mock.AsStdFunction(), config, db);
|
||||
message_queue->start();
|
||||
message_queue->set_registration_status_accepted();
|
||||
message_queue->resume(std::chrono::seconds(0));
|
||||
}
|
||||
|
||||
void restart_message_queue() {
|
||||
if (message_queue) {
|
||||
message_queue->stop();
|
||||
}
|
||||
message_queue = std::make_unique<MessageQueue<TestMessageType>>(send_callback_mock.AsStdFunction(), config, db);
|
||||
message_queue->start();
|
||||
message_queue->set_registration_status_accepted();
|
||||
message_queue->resume(std::chrono::seconds(0));
|
||||
}
|
||||
|
||||
void SetUp() override {
|
||||
call_count = 0;
|
||||
config = MessageQueueConfig<TestMessageType>{1, 1, 2, false};
|
||||
db = std::make_shared<DatabaseHandlerBaseMock>();
|
||||
init_message_queue();
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
message_queue->stop();
|
||||
};
|
||||
};
|
||||
|
||||
// \brief Test sending a transactional message
|
||||
TEST_F(MessageQueueTest, test_transactional_message_is_sent) {
|
||||
|
||||
EXPECT_CALL(send_callback_mock, Call(json{2, "0", "transactional", json{{"data", "test_data"}}}))
|
||||
.WillOnce(MarkAndReturn(true));
|
||||
EXPECT_CALL(*db, insert_message_queue_message(testing::_, testing::_));
|
||||
|
||||
Call<TestRequest> call;
|
||||
call.msg.type = TestMessageType::TRANSACTIONAL;
|
||||
call.msg.data = "test_data";
|
||||
call.uniqueId = "0";
|
||||
message_queue->push_call(call);
|
||||
|
||||
wait_for_calls();
|
||||
}
|
||||
|
||||
// \brief Test sending a non-transactional message
|
||||
TEST_F(MessageQueueTest, test_non_transactional_message_is_sent) {
|
||||
|
||||
EXPECT_CALL(send_callback_mock, Call(json{2, "0", "non_transactional", json{{"data", "test_data"}}}))
|
||||
.WillOnce(MarkAndReturn(true));
|
||||
|
||||
Call<TestRequest> call;
|
||||
call.msg.type = TestMessageType::NON_TRANSACTIONAL;
|
||||
call.msg.data = "test_data";
|
||||
call.uniqueId = "0";
|
||||
message_queue->push_call(call);
|
||||
|
||||
wait_for_calls();
|
||||
}
|
||||
|
||||
// \brief Test transactional messages that are sent while being offline are sent afterwards
|
||||
TEST_F(MessageQueueTest, test_queuing_up_of_transactional_messages) {
|
||||
|
||||
config.transaction_message_attempts = 2;
|
||||
restart_message_queue();
|
||||
|
||||
int message_count = config.queues_total_size_threshold + 3;
|
||||
testing::Sequence s;
|
||||
|
||||
// Setup: reject the first call ("offline"); after that, accept any call
|
||||
EXPECT_CALL(send_callback_mock, Call(testing::_)).InSequence(s).WillOnce(MarkAndReturn(false));
|
||||
EXPECT_CALL(send_callback_mock, Call(testing::_))
|
||||
.Times(message_count)
|
||||
.InSequence(s)
|
||||
.WillRepeatedly(MarkAndReturn(true, true));
|
||||
EXPECT_CALL(*db, insert_message_queue_message(testing::_, QueueType::Transaction)).Times(message_count);
|
||||
EXPECT_CALL(*db, remove_message_queue_message(testing::_, QueueType::Transaction)).Times(message_count);
|
||||
|
||||
// Act:
|
||||
// push first call and wait for callback; then push all other calls and resume queue
|
||||
push_message_call(TestMessageType::TRANSACTIONAL);
|
||||
wait_for_calls(1);
|
||||
|
||||
for (int i = 1; i < message_count; i++) {
|
||||
push_message_call(TestMessageType::TRANSACTIONAL);
|
||||
}
|
||||
|
||||
// expect one repeated and all other calls been made
|
||||
wait_for_calls(message_count + 1);
|
||||
}
|
||||
|
||||
// \brief Test that - with default setting - non-transactional messages that are not sent afterwards
|
||||
TEST_F(MessageQueueTest, test_non_queuing_up_of_non_transactional_messages) {
|
||||
|
||||
int message_count = config.queues_total_size_threshold + 3;
|
||||
testing::Sequence s;
|
||||
|
||||
// Setup: reject the first call ("offline"); after that, accept any call
|
||||
EXPECT_CALL(send_callback_mock, Call(testing::_)).InSequence(s).WillOnce(MarkAndReturn(false));
|
||||
EXPECT_CALL(send_callback_mock, Call(testing::_)).InSequence(s).WillRepeatedly(MarkAndReturn(true, true));
|
||||
|
||||
// Act:
|
||||
// push first call and wait for callback; then push all other calls and resume queue
|
||||
push_message_call(TestMessageType::NON_TRANSACTIONAL);
|
||||
wait_for_calls(1);
|
||||
|
||||
// go offline and push all other calls
|
||||
message_queue->pause();
|
||||
|
||||
for (int i = 1; i < message_count; i++) {
|
||||
push_message_call(TestMessageType::NON_TRANSACTIONAL);
|
||||
}
|
||||
|
||||
message_queue->resume(std::chrono::seconds(0));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
|
||||
// expect calls not repeated
|
||||
EXPECT_EQ(1, get_call_count());
|
||||
}
|
||||
|
||||
// \brief Test that if queue_all_messages is set to true, non-transactional messages that are sent when online again
|
||||
TEST_F(MessageQueueTest, test_queuing_up_of_non_transactional_messages) {
|
||||
config.queue_all_messages = true;
|
||||
config.transaction_message_attempts = 2;
|
||||
restart_message_queue();
|
||||
|
||||
int message_count = config.queues_total_size_threshold;
|
||||
testing::Sequence s;
|
||||
|
||||
// Setup: reject the first call ("offline"); after that, accept any call
|
||||
EXPECT_CALL(send_callback_mock, Call(testing::_)).InSequence(s).WillOnce(MarkAndReturn(false));
|
||||
EXPECT_CALL(send_callback_mock, Call(testing::_)).InSequence(s).WillRepeatedly(MarkAndReturn(true, true));
|
||||
|
||||
// Act:
|
||||
// push first call and wait for callback; then push all other calls and resume queue
|
||||
push_message_call(TestMessageType::NON_TRANSACTIONAL);
|
||||
wait_for_calls(1);
|
||||
|
||||
for (int i = 1; i < message_count; i++) {
|
||||
push_message_call(TestMessageType::NON_TRANSACTIONAL);
|
||||
}
|
||||
|
||||
// expect calls _are_ repeated
|
||||
wait_for_calls(message_count + 1);
|
||||
}
|
||||
|
||||
// \brief Test that if the max size threshold is exceeded, the non-transactional messages are dropped
|
||||
// Sends both non-transactions and transactional messages while on pause, expects a certain amount of non-transactional
|
||||
// to be dropped.
|
||||
TEST_F(MessageQueueTest, test_clean_up_non_transactional_queue) {
|
||||
|
||||
const int sent_transactional_messages = 10;
|
||||
const int sent_non_transactional_messages = 15;
|
||||
config.queues_total_size_threshold =
|
||||
20; // expect two messages to be dropped each round (3x), end up with 15-6=9 non-transactional remaining
|
||||
config.queue_all_messages = true;
|
||||
const int expected_skipped_transactional_messages = 6;
|
||||
restart_message_queue();
|
||||
|
||||
EXPECT_CALL(*db, insert_message_queue_message(testing::_, testing::_))
|
||||
.Times(sent_transactional_messages + sent_non_transactional_messages);
|
||||
EXPECT_CALL(*db, remove_message_queue_message(testing::_, testing::_))
|
||||
.Times(sent_transactional_messages + sent_non_transactional_messages)
|
||||
.WillRepeatedly(testing::Return());
|
||||
|
||||
// go offline
|
||||
message_queue->pause();
|
||||
|
||||
testing::Sequence s;
|
||||
for (int i = 0; i < sent_non_transactional_messages; i++) {
|
||||
auto msg_id = push_message_call(TestMessageType::NON_TRANSACTIONAL);
|
||||
|
||||
if (i >= expected_skipped_transactional_messages) {
|
||||
EXPECT_CALL(send_callback_mock,
|
||||
Call(json{2, msg_id, to_string(TestMessageType::NON_TRANSACTIONAL), json{{"data", msg_id}}}))
|
||||
.InSequence(s)
|
||||
.WillOnce(MarkAndReturn(true, true));
|
||||
}
|
||||
}
|
||||
for (int i = 0; i < sent_transactional_messages; i++) {
|
||||
auto msg_id = push_message_call(TestMessageType::TRANSACTIONAL);
|
||||
EXPECT_CALL(send_callback_mock,
|
||||
Call(json{2, msg_id, to_string(TestMessageType::TRANSACTIONAL), json{{"data", msg_id}}}))
|
||||
.InSequence(s)
|
||||
.WillOnce(MarkAndReturn(true, true));
|
||||
}
|
||||
|
||||
// go online again
|
||||
message_queue->resume(std::chrono::seconds(0));
|
||||
|
||||
// expect calls _are_ repeated
|
||||
wait_for_calls(sent_transactional_messages + sent_non_transactional_messages -
|
||||
expected_skipped_transactional_messages);
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
|
||||
// assert no further calls
|
||||
EXPECT_EQ(sent_transactional_messages + sent_non_transactional_messages - expected_skipped_transactional_messages,
|
||||
get_call_count());
|
||||
}
|
||||
|
||||
// \brief Test that if the max size threshold is exceeded, intermediate transactional (update) messages are dropped
|
||||
// Sends both non-transactions and transactional messages while on pause, expects all non-transactional, and any except
|
||||
// every forth transactional to be dropped
|
||||
TEST_F(MessageQueueTest, test_clean_up_transactional_queue) {
|
||||
|
||||
const int sent_non_transactional_messages = 10;
|
||||
const std::vector<int> transaction_update_messages{0, 4, 6,
|
||||
2}; // meaning there are 4 transactions, each with a "start" and
|
||||
// "stop" message and the provided number of updates;
|
||||
// in total 4*2 + 4+ 6 +2 = 20 messages
|
||||
config.queues_total_size_threshold = 13;
|
||||
/**
|
||||
* Message IDs:
|
||||
* non-transactional: 0 - 9
|
||||
* Transaction I: 10 - 11
|
||||
* Transaction II: 12 - 17
|
||||
* Transaction III: 18 - 25
|
||||
* Transaction IV: 26 - 29
|
||||
*
|
||||
* Expected dropping behavior
|
||||
* - adding msg 13-22 -> each drop 1 non-transactional (floored 10% of queue thresholds)
|
||||
* - adding msg 23 (update of third transaction) -> drop 4 messages with ids 13,15,19,21
|
||||
* - adding msg 27 (update of fourth transaction) -> drop 3 message with ids 14,20,23
|
||||
*/
|
||||
const std::set<std::string> expected_dropped_transaction_messages = {
|
||||
"test_call_13", "test_call_15", "test_call_19", "test_call_21", "test_call_14", "test_call_20", "test_call_23",
|
||||
};
|
||||
const int expected_sent_messages = 13;
|
||||
config.queue_all_messages = true;
|
||||
restart_message_queue();
|
||||
|
||||
EXPECT_CALL(*db, insert_message_queue_message(testing::_, testing::_)).Times(30);
|
||||
EXPECT_CALL(*db, remove_message_queue_message(testing::_, testing::_)).Times(30).WillRepeatedly(testing::Return());
|
||||
|
||||
// go offline
|
||||
message_queue->pause();
|
||||
|
||||
// Send messages / set up expected calls
|
||||
testing::Sequence s;
|
||||
for (int i = 0; i < sent_non_transactional_messages; i++) {
|
||||
push_message_call(TestMessageType::NON_TRANSACTIONAL);
|
||||
}
|
||||
|
||||
for (int update_messages : transaction_update_messages) {
|
||||
// transaction "start"
|
||||
auto start_msg_id = push_message_call(TestMessageType::TRANSACTIONAL);
|
||||
EXPECT_CALL(send_callback_mock, Call(json{2, start_msg_id, to_string(TestMessageType::TRANSACTIONAL),
|
||||
json{{"data", start_msg_id}}}))
|
||||
.InSequence(s)
|
||||
.WillOnce(MarkAndReturn(true, true));
|
||||
|
||||
for (int i = 0; i < update_messages; i++) {
|
||||
auto update_msg_id = push_message_call(TestMessageType::TRANSACTIONAL_UPDATE);
|
||||
|
||||
if (!expected_dropped_transaction_messages.count(update_msg_id)) {
|
||||
EXPECT_CALL(send_callback_mock,
|
||||
Call(json{2, update_msg_id, to_string(TestMessageType::TRANSACTIONAL_UPDATE),
|
||||
json{{"data", update_msg_id}}}))
|
||||
.InSequence(s)
|
||||
.WillOnce(MarkAndReturn(true, true));
|
||||
}
|
||||
}
|
||||
|
||||
auto stop_msg_id = push_message_call(TestMessageType::TRANSACTIONAL);
|
||||
// transaction "end"
|
||||
EXPECT_CALL(send_callback_mock,
|
||||
Call(json{2, stop_msg_id, to_string(TestMessageType::TRANSACTIONAL), json{{"data", stop_msg_id}}}))
|
||||
.InSequence(s)
|
||||
.WillOnce(MarkAndReturn(true, true));
|
||||
}
|
||||
|
||||
// Resume & verify
|
||||
message_queue->resume(std::chrono::seconds(0));
|
||||
|
||||
wait_for_calls(expected_sent_messages);
|
||||
}
|
||||
|
||||
// \brief Test that transactional retries and non-transactional messages are all delivered
|
||||
// when a transactional message continuously fails.
|
||||
//
|
||||
// OCPP does not require non-transactional messages to wait for transactional retries.
|
||||
// The RPC layer is synchronous (one in-flight at a time), but non-transactional messages
|
||||
// like Heartbeats or StatusNotifications are allowed to be sent between retry attempts.
|
||||
// This test verifies:
|
||||
// - The transactional message is retried the configured number of times
|
||||
// - The non-transactional message is still delivered (not starved by retries)
|
||||
// - Transactional messages maintain their own ordering
|
||||
TEST_F(MessageQueueTest, test_transactional_order_strictness) {
|
||||
config.queues_total_size_threshold = 20; // Ensure no drops
|
||||
config.transaction_message_attempts = 2;
|
||||
config.transaction_message_retry_interval = 0;
|
||||
restart_message_queue();
|
||||
|
||||
std::vector<std::string> call_order;
|
||||
std::mutex order_mutex;
|
||||
|
||||
EXPECT_CALL(send_callback_mock, Call(testing::_))
|
||||
.WillRepeatedly(testing::Invoke([&, this](const json::array_t& msg) -> bool {
|
||||
// Identify by payload data since UUID changes on retry
|
||||
std::string data = msg.at(3).at("data").get<std::string>();
|
||||
|
||||
{
|
||||
std::lock_guard<std::mutex> lock(order_mutex);
|
||||
call_order.push_back(data);
|
||||
}
|
||||
|
||||
this->mark_call_sent();
|
||||
|
||||
if (data == "TX1") {
|
||||
return false; // Trigger retry logic
|
||||
}
|
||||
|
||||
reception_timer.timeout(
|
||||
[this, msg]() {
|
||||
this->message_queue->receive(json{3, msg[1], ""}.dump());
|
||||
},
|
||||
std::chrono::milliseconds(0));
|
||||
return true;
|
||||
}));
|
||||
|
||||
// 1. Push TX1 (blocks TX2)
|
||||
// 2. Push TX2 (queued behind TX1)
|
||||
// 3. Push Heartbeat (Non-transactional)
|
||||
push_message_call(TestMessageType::TRANSACTIONAL, "TX1");
|
||||
push_message_call(TestMessageType::TRANSACTIONAL, "TX2");
|
||||
push_message_call(TestMessageType::NON_TRANSACTIONAL, "Heartbeat");
|
||||
|
||||
// Wait for: TX1 (fail) + TX1 (fail/drop) + TX2 (success) + Heartbeat (success) = 4 calls
|
||||
wait_for_calls(4);
|
||||
|
||||
std::lock_guard<std::mutex> lock(order_mutex);
|
||||
|
||||
// TX1 must have been attempted twice
|
||||
size_t tx1_count = std::count(call_order.begin(), call_order.end(), "TX1");
|
||||
EXPECT_EQ(tx1_count, 2);
|
||||
|
||||
// TX2 must ONLY appear after all TX1 attempts are done.
|
||||
// We find the first occurrence of TX2 and ensure no TX1 exists after it.
|
||||
auto first_tx2 = std::find(call_order.begin(), call_order.end(), "TX2");
|
||||
ASSERT_NE(first_tx2, call_order.end()) << "TX2 was never sent!";
|
||||
|
||||
auto tx1_after_tx2 = std::find(first_tx2, call_order.end(), "TX1");
|
||||
EXPECT_EQ(tx1_after_tx2, call_order.end()) << "TX1 was sent after TX2! Ordering violated.";
|
||||
|
||||
// Heartbeat was delivered
|
||||
EXPECT_TRUE(std::find(call_order.begin(), call_order.end(), "Heartbeat") != call_order.end());
|
||||
}
|
||||
|
||||
// \brief Test that once retry attempts are exceeded, the message is dropped and the next
|
||||
// message in the queue is sent successfully.
|
||||
TEST_F(MessageQueueTest, test_message_dropped_after_max_retries_then_next_message_sent) {
|
||||
|
||||
config.transaction_message_attempts = 2;
|
||||
config.transaction_message_retry_interval = 0;
|
||||
restart_message_queue();
|
||||
|
||||
std::vector<std::string> observed_actions;
|
||||
std::mutex actions_mutex;
|
||||
std::atomic<int> local_count{0};
|
||||
|
||||
EXPECT_CALL(send_callback_mock, Call(testing::_))
|
||||
.WillRepeatedly(testing::Invoke([&, this](const json::array_t& msg) -> bool {
|
||||
std::string action = msg.at(2).get<std::string>();
|
||||
int current = ++local_count;
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(actions_mutex);
|
||||
observed_actions.push_back(action);
|
||||
}
|
||||
this->mark_call_sent();
|
||||
if (action == "transactional" && current <= 2) {
|
||||
return false; // fail the first transactional message both times
|
||||
}
|
||||
// Succeed and respond to everything else
|
||||
reception_timer.timeout(
|
||||
[this, msg]() {
|
||||
this->message_queue->receive(json{3, msg[1], ""}.dump());
|
||||
},
|
||||
std::chrono::milliseconds(0));
|
||||
return true;
|
||||
}));
|
||||
|
||||
EXPECT_CALL(*db, insert_message_queue_message(testing::_, testing::_)).Times(2);
|
||||
EXPECT_CALL(*db, remove_message_queue_message(testing::_, testing::_)).Times(2).WillRepeatedly(testing::Return());
|
||||
|
||||
// Push a transactional message (will be retried and dropped) and a second transactional
|
||||
push_message_call(TestMessageType::TRANSACTIONAL);
|
||||
push_message_call(TestMessageType::TRANSACTIONAL);
|
||||
|
||||
// 2 failed attempts for first message + 1 successful for second
|
||||
wait_for_calls(3);
|
||||
|
||||
std::lock_guard<std::mutex> lk(actions_mutex);
|
||||
ASSERT_GE(observed_actions.size(), 3u);
|
||||
// First two are the failed transactional, third is the next transactional succeeding
|
||||
EXPECT_EQ(observed_actions[0], "transactional");
|
||||
EXPECT_EQ(observed_actions[1], "transactional");
|
||||
EXPECT_EQ(observed_actions[2], "transactional");
|
||||
}
|
||||
|
||||
// \brief Test handle_timeout_or_callerror when send_callback returns true but no response
|
||||
// arrives (timeout fires). Verifies that the timeout timer triggers retry.
|
||||
TEST_F(MessageQueueTest, test_timeout_triggers_retry_after_successful_send) {
|
||||
|
||||
config.transaction_message_attempts = 2;
|
||||
config.transaction_message_retry_interval = 0;
|
||||
config.message_timeout_seconds = 1; // short timeout so test completes quickly
|
||||
restart_message_queue();
|
||||
|
||||
std::mutex removal_mutex;
|
||||
std::condition_variable removal_cv;
|
||||
bool message_removed = false;
|
||||
|
||||
// send_callback returns true both times, but we never send a CALLRESULT,
|
||||
// so the timeout fires and triggers handle_timeout_or_callerror
|
||||
EXPECT_CALL(send_callback_mock, Call(testing::_)).Times(2).WillRepeatedly(MarkAndReturn(true));
|
||||
EXPECT_CALL(*db, insert_message_queue_message(testing::_, QueueType::Transaction)).Times(1);
|
||||
EXPECT_CALL(*db, remove_message_queue_message(testing::_, QueueType::Transaction))
|
||||
.Times(1)
|
||||
.WillOnce(testing::Invoke([&](const std::string&, const QueueType) {
|
||||
std::lock_guard<std::mutex> lk(removal_mutex);
|
||||
message_removed = true;
|
||||
removal_cv.notify_one();
|
||||
}));
|
||||
|
||||
push_message_call(TestMessageType::TRANSACTIONAL);
|
||||
|
||||
// Wait for both send attempts (timeouts fire at ~1s each)
|
||||
wait_for_calls(2, std::chrono::seconds(5));
|
||||
EXPECT_EQ(2, get_call_count());
|
||||
|
||||
// Wait for the DB removal that happens when the final timeout exhausts retries
|
||||
{
|
||||
std::unique_lock<std::mutex> lk(removal_mutex);
|
||||
EXPECT_TRUE(removal_cv.wait_for(lk, std::chrono::seconds(5), [&] { return message_removed; }));
|
||||
}
|
||||
}
|
||||
|
||||
// \brief Test handle_timeout_or_callerror when send_callback returns false.
|
||||
// Verifies that send failure followed by eventual success works correctly.
|
||||
TEST_F(MessageQueueTest, test_send_failure_then_success) {
|
||||
|
||||
config.transaction_message_attempts = 3;
|
||||
config.transaction_message_retry_interval = 0;
|
||||
restart_message_queue();
|
||||
|
||||
testing::Sequence s;
|
||||
|
||||
// Fail first two, succeed on third
|
||||
EXPECT_CALL(send_callback_mock, Call(testing::_)).InSequence(s).WillOnce(MarkAndReturn(false));
|
||||
EXPECT_CALL(send_callback_mock, Call(testing::_)).InSequence(s).WillOnce(MarkAndReturn(false));
|
||||
EXPECT_CALL(send_callback_mock, Call(testing::_)).InSequence(s).WillOnce(MarkAndReturn(true, true));
|
||||
|
||||
EXPECT_CALL(*db, insert_message_queue_message(testing::_, QueueType::Transaction)).Times(1);
|
||||
EXPECT_CALL(*db, remove_message_queue_message(testing::_, QueueType::Transaction)).Times(1);
|
||||
|
||||
push_message_call(TestMessageType::TRANSACTIONAL);
|
||||
wait_for_calls(3);
|
||||
|
||||
EXPECT_EQ(3, get_call_count());
|
||||
}
|
||||
|
||||
// \brief Test that non-transactional messages (without queue_all_messages) are dropped
|
||||
// immediately on send failure and are NOT retried, regardless of transaction_message_attempts.
|
||||
TEST_F(MessageQueueTest, test_non_transactional_not_retried_on_send_failure) {
|
||||
|
||||
config.queue_all_messages = false;
|
||||
config.transaction_message_attempts = 5; // high, to confirm it's irrelevant
|
||||
config.transaction_message_retry_interval = 0;
|
||||
restart_message_queue();
|
||||
|
||||
// Should only be called once - non-transactional messages without queue_all_messages are not retried
|
||||
EXPECT_CALL(send_callback_mock, Call(testing::_)).Times(1).WillOnce(MarkAndReturn(false));
|
||||
|
||||
push_message_call(TestMessageType::NON_TRANSACTIONAL);
|
||||
wait_for_calls(1);
|
||||
|
||||
// Wait to confirm no further retry
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
EXPECT_EQ(1, get_call_count());
|
||||
}
|
||||
|
||||
// \brief Test that non-transactional messages WITH queue_all_messages=true are retried
|
||||
// on send failure, just like transactional messages.
|
||||
TEST_F(MessageQueueTest, test_non_transactional_retried_with_queue_all_messages) {
|
||||
|
||||
config.queue_all_messages = true;
|
||||
config.transaction_message_attempts = 3;
|
||||
config.transaction_message_retry_interval = 0;
|
||||
restart_message_queue();
|
||||
|
||||
// All 3 attempts fail
|
||||
EXPECT_CALL(send_callback_mock, Call(testing::_)).Times(3).WillRepeatedly(MarkAndReturn(false));
|
||||
EXPECT_CALL(*db, insert_message_queue_message(testing::_, QueueType::Normal)).Times(1);
|
||||
EXPECT_CALL(*db, remove_message_queue_message(testing::_, QueueType::Normal)).Times(1);
|
||||
|
||||
push_message_call(TestMessageType::NON_TRANSACTIONAL);
|
||||
wait_for_calls(3);
|
||||
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
EXPECT_EQ(3, get_call_count());
|
||||
}
|
||||
|
||||
// \brief Test that BootNotification is not dropped when the normal message queue overflows.
|
||||
// Pushes a BootNotification plus enough messages to exceed the threshold, then verifies
|
||||
// BootNotification survives the drop and is still sent.
|
||||
TEST_F(MessageQueueTest, test_boot_notification_not_dropped_on_queue_overflow) {
|
||||
|
||||
config.queues_total_size_threshold = 10;
|
||||
config.queue_all_messages = true;
|
||||
restart_message_queue();
|
||||
|
||||
EXPECT_CALL(*db, insert_message_queue_message(testing::_, testing::_)).Times(testing::AnyNumber());
|
||||
EXPECT_CALL(*db, remove_message_queue_message(testing::_, testing::_))
|
||||
.Times(testing::AnyNumber())
|
||||
.WillRepeatedly(testing::Return());
|
||||
|
||||
// go offline
|
||||
message_queue->pause();
|
||||
|
||||
// Push BootNotification first (goes to front of normal queue)
|
||||
auto boot_id = push_message_call(TestMessageType::BootNotification);
|
||||
|
||||
// Push enough non-transactional messages to fill normal queue
|
||||
const int non_transactional_count = 8;
|
||||
for (int i = 0; i < non_transactional_count; i++) {
|
||||
push_message_call(TestMessageType::NON_TRANSACTIONAL);
|
||||
}
|
||||
|
||||
// Push transactional messages to exceed threshold and trigger drops
|
||||
const int transactional_count = 5;
|
||||
for (int i = 0; i < transactional_count; i++) {
|
||||
push_message_call(TestMessageType::TRANSACTIONAL);
|
||||
}
|
||||
|
||||
// Set up expectations: BootNotification must be sent
|
||||
std::atomic<bool> boot_sent{false};
|
||||
EXPECT_CALL(send_callback_mock, Call(testing::_))
|
||||
.WillRepeatedly(testing::Invoke([&, this](const json::array_t& msg) -> bool {
|
||||
std::string data = msg.at(3).at("data").get<std::string>();
|
||||
if (data == boot_id) {
|
||||
boot_sent = true;
|
||||
}
|
||||
this->mark_call_sent();
|
||||
reception_timer.timeout(
|
||||
[this, msg]() {
|
||||
this->message_queue->receive(json{3, msg[1], ""}.dump());
|
||||
},
|
||||
std::chrono::milliseconds(0));
|
||||
return true;
|
||||
}));
|
||||
|
||||
// go online again
|
||||
message_queue->resume(std::chrono::seconds(0));
|
||||
|
||||
// Wait for messages to be processed (BootNotification + surviving non-transactional + transactional)
|
||||
// Some non-transactional messages will have been dropped, but BootNotification must survive
|
||||
wait_for_calls(5, std::chrono::seconds(5));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
EXPECT_TRUE(boot_sent) << "BootNotification was dropped from the queue!";
|
||||
}
|
||||
|
||||
// \brief Test that when BootNotification is the only message in the normal queue,
|
||||
// check_queue_sizes() doesn't loop infinitely trying to drop it.
|
||||
TEST_F(MessageQueueTest, test_boot_notification_survives_when_only_message_in_normal_queue) {
|
||||
|
||||
config.queues_total_size_threshold = 10;
|
||||
config.queue_all_messages = true;
|
||||
restart_message_queue();
|
||||
|
||||
EXPECT_CALL(*db, insert_message_queue_message(testing::_, testing::_)).Times(testing::AnyNumber());
|
||||
EXPECT_CALL(*db, remove_message_queue_message(testing::_, testing::_))
|
||||
.Times(testing::AnyNumber())
|
||||
.WillRepeatedly(testing::Return());
|
||||
|
||||
// go offline
|
||||
message_queue->pause();
|
||||
|
||||
// Push BootNotification as the only normal message
|
||||
auto boot_id = push_message_call(TestMessageType::BootNotification);
|
||||
|
||||
// Fill transaction queue past threshold
|
||||
const int transactional_count = 15;
|
||||
for (int i = 0; i < transactional_count; i++) {
|
||||
push_message_call(TestMessageType::TRANSACTIONAL);
|
||||
}
|
||||
|
||||
// Set up expectations: BootNotification must be sent
|
||||
std::atomic<bool> boot_sent{false};
|
||||
EXPECT_CALL(send_callback_mock, Call(testing::_))
|
||||
.WillRepeatedly(testing::Invoke([&, this](const json::array_t& msg) -> bool {
|
||||
std::string data = msg.at(3).at("data").get<std::string>();
|
||||
if (data == boot_id) {
|
||||
boot_sent = true;
|
||||
}
|
||||
this->mark_call_sent();
|
||||
reception_timer.timeout(
|
||||
[this, msg]() {
|
||||
this->message_queue->receive(json{3, msg[1], ""}.dump());
|
||||
},
|
||||
std::chrono::milliseconds(0));
|
||||
return true;
|
||||
}));
|
||||
|
||||
// go online - this must not hang (the break guard prevents infinite loop)
|
||||
message_queue->resume(std::chrono::seconds(0));
|
||||
|
||||
// Wait for messages to be processed
|
||||
wait_for_calls(5, std::chrono::seconds(5));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
|
||||
EXPECT_TRUE(boot_sent) << "BootNotification was dropped from the queue!";
|
||||
}
|
||||
|
||||
} // namespace ocpp
|
||||
@@ -0,0 +1,47 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "ocpp/common/websocket/websocket_uri.hpp"
|
||||
|
||||
using namespace ocpp;
|
||||
|
||||
TEST(WebsocketUriTest, EmptyStrings) {
|
||||
EXPECT_THROW(Uri::parse_and_validate("", "cp0001", 1), std::invalid_argument);
|
||||
EXPECT_THROW(Uri::parse_and_validate("ws://test.uri.com", "", 1), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST(WebsocketUriTest, UriInvalid) {
|
||||
EXPECT_THROW(Uri::parse_and_validate("://invalid", "cp0001", 1), std::invalid_argument);
|
||||
EXPECT_THROW(Uri::parse_and_validate("ws:test.uri.com", "cp0001", 1), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST(WebsocketUriTest, InvalidSecurityLevel) {
|
||||
EXPECT_THROW(Uri::parse_and_validate("wss://test.uri.com", "cp0001", 4), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST(WebsocketUriTest, SecurityLevelMismatch) {
|
||||
EXPECT_THROW(Uri::parse_and_validate("wss://test.uri.com", "cp0001", 0), std::invalid_argument);
|
||||
EXPECT_THROW(Uri::parse_and_validate("wss://test.uri.com", "cp0001", 1), std::invalid_argument);
|
||||
EXPECT_THROW(Uri::parse_and_validate("ws://test.uri.com", "cp0001", 2), std::invalid_argument);
|
||||
EXPECT_THROW(Uri::parse_and_validate("ws://test.uri.com", "cp0001", 3), std::invalid_argument);
|
||||
}
|
||||
|
||||
TEST(WebsocketUriTest, AppendingIdentity) {
|
||||
EXPECT_EQ(Uri::parse_and_validate("ws://test.uri.com/path", "cp0001", 1).string(), "ws://test.uri.com/path/cp0001");
|
||||
EXPECT_EQ(Uri::parse_and_validate("ws://test.uri.com/path/", "cp0001", 1).string(),
|
||||
"ws://test.uri.com/path/cp0001");
|
||||
EXPECT_EQ(Uri::parse_and_validate("ws://test.uri.com/path/cp0001", "cp0001", 1).string(),
|
||||
"ws://test.uri.com/path/cp0001");
|
||||
}
|
||||
|
||||
TEST(WebsocketUriTest, SetsCorrectPortForUri) {
|
||||
auto uri_temp1 = Uri(Uri::parse_and_validate("test.uri.com/path/", "cp0001", 1));
|
||||
EXPECT_EQ(uri_temp1.string(), "ws://test.uri.com/path/cp0001");
|
||||
EXPECT_EQ(uri_temp1.get_port(), uri_default_port);
|
||||
|
||||
auto uri_temp2 = Uri(Uri::parse_and_validate("test.uri.com/path/", "cp0001", 2));
|
||||
EXPECT_EQ(uri_temp2.string(), "wss://test.uri.com/path/cp0001");
|
||||
EXPECT_EQ(uri_temp2.get_port(), uri_default_secure_port);
|
||||
}
|
||||
@@ -0,0 +1,102 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <ocpp/common/utils.hpp>
|
||||
|
||||
namespace ocpp {
|
||||
namespace common {
|
||||
|
||||
class UtilsTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(UtilsTest, test_is_integer) {
|
||||
ASSERT_TRUE(is_integer("+100"));
|
||||
ASSERT_TRUE(is_integer("-100"));
|
||||
ASSERT_TRUE(is_integer("100"));
|
||||
ASSERT_FALSE(is_integer("10x"));
|
||||
ASSERT_FALSE(is_integer("+10x"));
|
||||
ASSERT_FALSE(is_integer("-10x"));
|
||||
ASSERT_FALSE(is_integer("---"));
|
||||
ASSERT_FALSE(is_integer("+++"));
|
||||
}
|
||||
|
||||
TEST_F(UtilsTest, test_valid_datetime) {
|
||||
ASSERT_TRUE(is_rfc3339_datetime("2023-11-29T10:21:04Z"));
|
||||
ASSERT_TRUE(is_rfc3339_datetime("2019-04-12T23:20:50.5Z"));
|
||||
ASSERT_TRUE(is_rfc3339_datetime("2019-04-12T23:20:50.52Z"));
|
||||
ASSERT_TRUE(is_rfc3339_datetime("2019-04-12T23:20:50.523Z"));
|
||||
ASSERT_TRUE(is_rfc3339_datetime("2019-12-19T16:39:57+01:00"));
|
||||
ASSERT_TRUE(is_rfc3339_datetime("2019-12-19T16:39:57-01:00"));
|
||||
}
|
||||
|
||||
TEST_F(UtilsTest, test_invalid_datetime) {
|
||||
ASSERT_FALSE(is_rfc3339_datetime("1"));
|
||||
ASSERT_FALSE(is_rfc3339_datetime("1.1"));
|
||||
ASSERT_FALSE(is_rfc3339_datetime("true"));
|
||||
ASSERT_FALSE(is_rfc3339_datetime("abc"));
|
||||
|
||||
// more than 3 decimal digits are not allowed in OCPP
|
||||
ASSERT_FALSE(is_rfc3339_datetime("2023-11-29T10:21:04.0001Z"));
|
||||
}
|
||||
|
||||
TEST(Utils, test_split_string) {
|
||||
std::vector<std::string> result = split_string("This is a test", ' ');
|
||||
ASSERT_EQ(result.size(), 4);
|
||||
EXPECT_EQ(result.at(0), "This");
|
||||
EXPECT_EQ(result.at(1), "is");
|
||||
EXPECT_EQ(result.at(2), "a");
|
||||
EXPECT_EQ(result.at(3), "test");
|
||||
|
||||
result = split_string("This;is;a;test;", ' ');
|
||||
ASSERT_EQ(result.size(), 1);
|
||||
EXPECT_EQ(result.at(0), "This;is;a;test;");
|
||||
|
||||
result = split_string("Testing;with;google test;", ';');
|
||||
ASSERT_EQ(result.size(), 3);
|
||||
EXPECT_EQ(result.at(0), "Testing");
|
||||
EXPECT_EQ(result.at(1), "with");
|
||||
EXPECT_EQ(result.at(2), "google test");
|
||||
|
||||
result = split_string(",", ',');
|
||||
ASSERT_EQ(result.size(), 1);
|
||||
EXPECT_EQ(result.at(0), "");
|
||||
|
||||
result = split_string("", '.');
|
||||
EXPECT_EQ(result.size(), 0);
|
||||
|
||||
result = split_string("This is a test. It is performed using google test.", '.');
|
||||
ASSERT_EQ(result.size(), 2);
|
||||
EXPECT_EQ(result.at(0), "This is a test");
|
||||
EXPECT_EQ(result.at(1), " It is performed using google test");
|
||||
|
||||
result = split_string("Aa, Bb, Cc, Dd", ',', false);
|
||||
ASSERT_EQ(result.size(), 4);
|
||||
EXPECT_EQ(result.at(0), "Aa");
|
||||
EXPECT_EQ(result.at(1), " Bb");
|
||||
EXPECT_EQ(result.at(2), " Cc");
|
||||
EXPECT_EQ(result.at(3), " Dd");
|
||||
|
||||
result = split_string("Aa, Bb, Cc, Dd", ',', true);
|
||||
ASSERT_EQ(result.size(), 4);
|
||||
EXPECT_EQ(result.at(0), "Aa");
|
||||
EXPECT_EQ(result.at(1), "Bb");
|
||||
EXPECT_EQ(result.at(2), "Cc");
|
||||
EXPECT_EQ(result.at(3), "Dd");
|
||||
}
|
||||
|
||||
TEST(Utils, test_trim_string) {
|
||||
EXPECT_EQ(trim_string(""), "");
|
||||
EXPECT_EQ(trim_string(" trim this"), "trim this");
|
||||
EXPECT_EQ(trim_string(" trim this as well "), "trim this as well");
|
||||
EXPECT_EQ(trim_string("only space at end "), "only space at end");
|
||||
}
|
||||
|
||||
} // namespace common
|
||||
} // namespace ocpp
|
||||
@@ -0,0 +1,35 @@
|
||||
target_include_directories(libocpp_unit_tests PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}
|
||||
)
|
||||
|
||||
target_sources(libocpp_unit_tests PRIVATE
|
||||
v2config/memory_storage.cpp
|
||||
v2config/test_california_pricing.cpp
|
||||
v2config/test_config.cpp
|
||||
v2config/test_core.cpp
|
||||
v2config/test_custom.cpp
|
||||
v2config/test_firmware_management.cpp
|
||||
v2config/test_internal.cpp
|
||||
v2config/test_local_auth_list.cpp
|
||||
v2config/test_pnc.cpp
|
||||
v2config/test_security.cpp
|
||||
v2config/test_smart_charging.cpp
|
||||
v2config/test_user_config.cpp
|
||||
profile_tests_common.cpp
|
||||
profile_testsA.cpp
|
||||
profile_testsB.cpp
|
||||
profile_testsC.cpp
|
||||
test_database_migration_files.cpp
|
||||
test_smart_charging_handler.cpp
|
||||
database_tests.cpp
|
||||
test_message_queue.cpp
|
||||
test_charge_point_state_machine.cpp
|
||||
test_composite_schedule.cpp
|
||||
test_config_validation.cpp
|
||||
utils_tests.cpp
|
||||
test_configuration.cpp
|
||||
test_utils.cpp
|
||||
)
|
||||
|
||||
# Copy the json files used for testing to the destination directory
|
||||
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/json DESTINATION ${TEST_PROFILES_LOCATION_V16})
|
||||
@@ -0,0 +1,24 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
#ifndef OCPP_DATABASE_HANDLE_MOCK_H
|
||||
#define OCPP_DATABASE_HANDLE_MOCK_H
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <ocpp/v16/database_handler.hpp>
|
||||
|
||||
namespace ocpp {
|
||||
|
||||
class DatabaseHandlerMock : public v16::DatabaseHandler {
|
||||
public:
|
||||
DatabaseHandlerMock(std::unique_ptr<everest::db::sqlite::ConnectionInterface> database,
|
||||
const fs::path& init_script_path) :
|
||||
DatabaseHandler(std::move(database), init_script_path, 2){};
|
||||
MOCK_METHOD(void, insert_or_update_charging_profile, (const int, const v16::ChargingProfile&), (override));
|
||||
MOCK_METHOD(void, delete_charging_profile, (const int profile_id), (override));
|
||||
};
|
||||
|
||||
} // namespace ocpp
|
||||
|
||||
#endif // DATABASE_HANDLE_MOCK_H
|
||||
@@ -0,0 +1,177 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef DATABASE_STUB_HPP
|
||||
#define DATABASE_STUB_HPP
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <ocpp/v16/charge_point_configuration.hpp>
|
||||
#include <ocpp/v16/connector.hpp>
|
||||
#include <ocpp/v16/database_handler.hpp>
|
||||
#include <ocpp/v16/smart_charging.hpp>
|
||||
|
||||
namespace stubs {
|
||||
|
||||
using namespace ocpp::v16;
|
||||
using namespace ocpp;
|
||||
namespace fs = std::filesystem;
|
||||
using json = nlohmann::json;
|
||||
using namespace std::chrono;
|
||||
using namespace everest::db::sqlite;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// provide access to the SQLite database handle
|
||||
struct DatabaseHandlerTest : public DatabaseHandler {
|
||||
using DatabaseHandler::DatabaseHandler;
|
||||
};
|
||||
|
||||
struct SQLiteStatementTest : public StatementInterface {
|
||||
virtual int changes() {
|
||||
return 0;
|
||||
}
|
||||
virtual int step() {
|
||||
return SQLITE_DONE;
|
||||
}
|
||||
virtual int reset() {
|
||||
return 0;
|
||||
}
|
||||
virtual int bind_text(const int idx, const std::string& val, SQLiteString lifetime = SQLiteString::Static) {
|
||||
return 0;
|
||||
}
|
||||
virtual int bind_text(const std::string& param, const std::string& val,
|
||||
SQLiteString lifetime = SQLiteString::Static) {
|
||||
return 0;
|
||||
}
|
||||
virtual int bind_int(const int idx, const int val) {
|
||||
return 0;
|
||||
}
|
||||
virtual int bind_int(const std::string& param, const int val) {
|
||||
return 0;
|
||||
}
|
||||
virtual int bind_int64(const int idx, const std::int64_t val) {
|
||||
return 0;
|
||||
}
|
||||
virtual int bind_int64(const std::string& param, const std::int64_t val) {
|
||||
return 0;
|
||||
}
|
||||
virtual int bind_double(const int idx, const double val) {
|
||||
return 0;
|
||||
}
|
||||
virtual int bind_double(const std::string& param, const double val) {
|
||||
return 0;
|
||||
}
|
||||
virtual int bind_null(const int idx) {
|
||||
return 0;
|
||||
}
|
||||
virtual int bind_null(const std::string& param) {
|
||||
return 0;
|
||||
}
|
||||
virtual int get_number_of_rows() override {
|
||||
return 0;
|
||||
}
|
||||
virtual int column_type(const int idx) {
|
||||
return SQLITE_INTEGER;
|
||||
}
|
||||
virtual std::string column_text(const int idx) {
|
||||
return std::string();
|
||||
}
|
||||
virtual std::optional<std::string> column_text_nullable(const int idx) {
|
||||
return std::nullopt;
|
||||
}
|
||||
virtual int column_int(const int idx) {
|
||||
return 0;
|
||||
}
|
||||
virtual std::int64_t column_int64(const int idx) {
|
||||
return 0;
|
||||
}
|
||||
virtual double column_double(const int idx) {
|
||||
return 0.0;
|
||||
}
|
||||
virtual SqliteVariant column_variant(const std::string& name) {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
struct DatabaseConnectionTest : public ConnectionInterface {
|
||||
virtual bool open_connection() {
|
||||
return true;
|
||||
}
|
||||
virtual bool close_connection() {
|
||||
return true;
|
||||
}
|
||||
virtual std::unique_ptr<TransactionInterface> begin_transaction() {
|
||||
return std::unique_ptr<TransactionInterface>{};
|
||||
}
|
||||
virtual bool commit_transaction() {
|
||||
return true;
|
||||
}
|
||||
virtual bool rollback_transaction() {
|
||||
return true;
|
||||
}
|
||||
virtual bool execute_statement(const std::string& statement) {
|
||||
return true;
|
||||
}
|
||||
virtual std::unique_ptr<StatementInterface> new_statement(const std::string& sql) {
|
||||
return std::make_unique<SQLiteStatementTest>();
|
||||
}
|
||||
virtual const char* get_error_message() {
|
||||
return "";
|
||||
}
|
||||
virtual bool clear_table(const std::string& table) {
|
||||
return true;
|
||||
}
|
||||
virtual std::int64_t get_last_inserted_rowid() {
|
||||
return 1;
|
||||
}
|
||||
virtual void set_user_version(std::uint32_t version) override {
|
||||
}
|
||||
virtual std::uint32_t get_user_version() override {
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
class DbTestBase : public testing::Test {
|
||||
protected:
|
||||
const std::string chargepoint_id = "12345678";
|
||||
const fs::path database_path = "/tmp/";
|
||||
const fs::path init_script_path = "./core_migrations";
|
||||
const fs::path db_filename = database_path / (chargepoint_id + ".db");
|
||||
|
||||
std::map<std::int32_t, std::shared_ptr<Connector>> connectors;
|
||||
std::shared_ptr<stubs::DatabaseHandlerTest> database_handler;
|
||||
std::unique_ptr<ConnectionInterface> database_interface;
|
||||
std::unique_ptr<ChargePointConfiguration> configuration;
|
||||
|
||||
void add_connectors(unsigned int n) {
|
||||
for (unsigned int i = 0; i <= n; i++) {
|
||||
if (connectors[i] == nullptr) {
|
||||
// create connector
|
||||
connectors[i] = std::make_shared<Connector>(i);
|
||||
} else {
|
||||
// reset connector
|
||||
connectors[i] = nullptr;
|
||||
connectors[i] = std::make_shared<Connector>(i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void SetUp() override {
|
||||
database_interface = std::make_unique<stubs::DatabaseConnectionTest>();
|
||||
database_handler =
|
||||
std::make_shared<stubs::DatabaseHandlerTest>(std::move(database_interface), init_script_path, 1);
|
||||
std::ifstream ifs(CONFIG_FILE_LOCATION_V16);
|
||||
const std::string config_file((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>()));
|
||||
configuration =
|
||||
std::make_unique<ChargePointConfiguration>(config_file, CONFIG_DIR_V16, USER_CONFIG_FILE_LOCATION_V16);
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
std::filesystem::remove(db_filename);
|
||||
connectors.clear();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace stubs
|
||||
|
||||
#endif // DATABASE_STUB_HPP
|
||||
@@ -0,0 +1,449 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <filesystem>
|
||||
#include <gtest/gtest.h>
|
||||
#include <iostream>
|
||||
#include <ocpp/v16/database_handler.hpp>
|
||||
#include <optional>
|
||||
#include <thread>
|
||||
|
||||
using namespace everest::db;
|
||||
using namespace everest::db::sqlite;
|
||||
|
||||
namespace ocpp {
|
||||
namespace v16 {
|
||||
|
||||
ChargingProfile get_sample_charging_profile() {
|
||||
ChargingSchedulePeriod period1;
|
||||
period1.startPeriod = 0;
|
||||
period1.limit = 10;
|
||||
period1.numberPhases.emplace(3);
|
||||
|
||||
ChargingSchedulePeriod period2;
|
||||
period2.startPeriod = 30;
|
||||
period2.limit = 16;
|
||||
period2.numberPhases.emplace(3);
|
||||
|
||||
std::vector<ChargingSchedulePeriod> periods;
|
||||
periods.push_back(period1);
|
||||
periods.push_back(period2);
|
||||
|
||||
ChargingSchedule schedule;
|
||||
schedule.chargingRateUnit = ChargingRateUnit::A;
|
||||
schedule.chargingSchedulePeriod = periods;
|
||||
schedule.duration = 100;
|
||||
schedule.startSchedule.emplace(DateTime(date::utc_clock::now()));
|
||||
schedule.minChargingRate.emplace(6.4);
|
||||
|
||||
DateTime valid_from = DateTime(date::utc_clock::now());
|
||||
DateTime valid_to = DateTime(valid_from.to_time_point() + std::chrono::hours(3600));
|
||||
|
||||
ChargingProfile profile;
|
||||
profile.chargingProfileId = 1;
|
||||
profile.stackLevel = 2;
|
||||
profile.chargingProfilePurpose = ChargingProfilePurposeType::TxProfile;
|
||||
profile.chargingProfileKind = ChargingProfileKindType::Recurring;
|
||||
profile.recurrencyKind.emplace(RecurrencyKindType::Daily);
|
||||
profile.validFrom.emplace(valid_from);
|
||||
profile.validTo.emplace(valid_to);
|
||||
profile.chargingSchedule = schedule;
|
||||
|
||||
return profile;
|
||||
}
|
||||
|
||||
class DatabaseTest : public ::testing::Test {
|
||||
public:
|
||||
DatabaseTest() {
|
||||
auto database_connection = std::make_unique<Connection>("file::memory:?cache=shared");
|
||||
database_connection->open_connection(); // Open connection so memory stays shared
|
||||
this->db_handler = std::make_unique<DatabaseHandler>(std::move(database_connection),
|
||||
std::filesystem::path(MIGRATION_FILES_LOCATION_V16), 2);
|
||||
this->db_handler->open_connection();
|
||||
}
|
||||
|
||||
std::unique_ptr<DatabaseHandler> db_handler;
|
||||
};
|
||||
|
||||
TEST_F(DatabaseTest, test_init_connector_table) {
|
||||
auto availability_type = this->db_handler->get_connector_availability(1);
|
||||
ASSERT_EQ(AvailabilityType::Operative, availability_type);
|
||||
|
||||
auto availability_map = this->db_handler->get_connector_availability();
|
||||
ASSERT_EQ(AvailabilityType::Operative, availability_map.at(1));
|
||||
ASSERT_EQ(AvailabilityType::Operative, availability_map.at(2));
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, test_list_version) {
|
||||
|
||||
std::int32_t list_version = this->db_handler->get_local_list_version();
|
||||
ASSERT_EQ(0, list_version);
|
||||
|
||||
std::int32_t exp_list_version = 42;
|
||||
this->db_handler->insert_or_update_local_list_version(exp_list_version);
|
||||
list_version = this->db_handler->get_local_list_version();
|
||||
ASSERT_EQ(exp_list_version, list_version);
|
||||
|
||||
exp_list_version = 17;
|
||||
this->db_handler->insert_or_update_local_list_version(exp_list_version);
|
||||
list_version = this->db_handler->get_local_list_version();
|
||||
ASSERT_EQ(exp_list_version, list_version);
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, test_local_authorization_list_entry_1) {
|
||||
|
||||
const auto id_tag = CiString<20>("DEADBEEF");
|
||||
const auto unknown_id_tag = CiString<20>("BEEFBEEF");
|
||||
|
||||
IdTagInfo exp_id_tag_info;
|
||||
exp_id_tag_info.status = AuthorizationStatus::Accepted;
|
||||
|
||||
this->db_handler->insert_or_update_local_authorization_list_entry(id_tag, exp_id_tag_info);
|
||||
auto id_tag_info = this->db_handler->get_local_authorization_list_entry(id_tag);
|
||||
ASSERT_EQ(exp_id_tag_info.status, id_tag_info.value().status);
|
||||
|
||||
id_tag_info = this->db_handler->get_local_authorization_list_entry(unknown_id_tag);
|
||||
ASSERT_EQ(std::nullopt, id_tag_info);
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, test_local_authorization_list_entry_2) {
|
||||
|
||||
const auto id_tag = CiString<20>("DEADBEEF");
|
||||
const auto unknown_id_tag = CiString<20>("BEEFBEEF");
|
||||
const auto parent_id_tag = CiString<20>("PARENT");
|
||||
|
||||
IdTagInfo exp_id_tag_info;
|
||||
exp_id_tag_info.status = AuthorizationStatus::Accepted;
|
||||
exp_id_tag_info.expiryDate.emplace(DateTime());
|
||||
exp_id_tag_info.parentIdTag = parent_id_tag;
|
||||
|
||||
this->db_handler->insert_or_update_local_authorization_list_entry(id_tag, exp_id_tag_info);
|
||||
auto id_tag_info = this->db_handler->get_local_authorization_list_entry(id_tag);
|
||||
// expired because expiry date was set to now
|
||||
ASSERT_EQ(AuthorizationStatus::Expired, id_tag_info.value().status);
|
||||
ASSERT_EQ(exp_id_tag_info.parentIdTag.value().get(), parent_id_tag.get());
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, test_local_authorization_list) {
|
||||
|
||||
std::vector<LocalAuthorizationList> local_authorization_list;
|
||||
|
||||
const auto id_tag_1 = CiString<20>("DEADBEEF");
|
||||
const auto id_tag_2 = CiString<20>("BEEFBEEF");
|
||||
|
||||
IdTagInfo id_tag_info;
|
||||
id_tag_info.status = AuthorizationStatus::Accepted;
|
||||
id_tag_info.expiryDate = DateTime(DateTime().to_time_point() + std::chrono::hours(24));
|
||||
|
||||
// inserting id_tag_2 manually with id_tag_info
|
||||
this->db_handler->insert_or_update_local_authorization_list_entry(id_tag_2, id_tag_info);
|
||||
auto received_id_tag_info = this->db_handler->get_local_authorization_list_entry(id_tag_2);
|
||||
ASSERT_EQ(id_tag_info.status, received_id_tag_info.value().status);
|
||||
|
||||
LocalAuthorizationList entry_1;
|
||||
entry_1.idTag = id_tag_1;
|
||||
entry_1.idTagInfo = id_tag_info;
|
||||
|
||||
// idTagInfo of entry_2 is not set
|
||||
LocalAuthorizationList entry_2;
|
||||
entry_2.idTag = id_tag_2;
|
||||
|
||||
local_authorization_list.push_back(entry_1);
|
||||
local_authorization_list.push_back(entry_2);
|
||||
|
||||
this->db_handler->insert_or_update_local_authorization_list(local_authorization_list);
|
||||
|
||||
received_id_tag_info = this->db_handler->get_local_authorization_list_entry(id_tag_1);
|
||||
ASSERT_EQ(id_tag_info.status, received_id_tag_info.value().status);
|
||||
|
||||
// entry_2 had no idTagInfo so it is not set so it is deleted from the list
|
||||
received_id_tag_info = this->db_handler->get_local_authorization_list_entry(id_tag_2);
|
||||
ASSERT_EQ(std::nullopt, received_id_tag_info);
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, test_clear_authorization_list) {
|
||||
|
||||
const auto id_tag = CiString<20>("DEADBEEF");
|
||||
const auto parent_id_tag = CiString<20>("PARENT");
|
||||
|
||||
IdTagInfo exp_id_tag_info;
|
||||
exp_id_tag_info.status = AuthorizationStatus::Accepted;
|
||||
exp_id_tag_info.expiryDate.emplace(DateTime(DateTime().to_time_point() + std::chrono::hours(24)));
|
||||
exp_id_tag_info.parentIdTag = parent_id_tag;
|
||||
|
||||
this->db_handler->insert_or_update_local_authorization_list_entry(id_tag, exp_id_tag_info);
|
||||
auto id_tag_info = this->db_handler->get_local_authorization_list_entry(id_tag);
|
||||
|
||||
ASSERT_EQ(exp_id_tag_info.status, id_tag_info.value().status);
|
||||
ASSERT_EQ(exp_id_tag_info.parentIdTag.value().get(), parent_id_tag.get());
|
||||
|
||||
this->db_handler->clear_local_authorization_list();
|
||||
|
||||
id_tag_info = this->db_handler->get_local_authorization_list_entry(id_tag);
|
||||
ASSERT_EQ(std::nullopt, id_tag_info);
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, test_authorization_cache_entry) {
|
||||
|
||||
const auto id_tag = CiString<20>("DEADBEEF");
|
||||
const auto unknown_id_tag = CiString<20>("BEEFBEEF");
|
||||
|
||||
IdTagInfo exp_id_tag_info;
|
||||
exp_id_tag_info.status = AuthorizationStatus::Accepted;
|
||||
|
||||
this->db_handler->insert_or_update_authorization_cache_entry(id_tag, exp_id_tag_info);
|
||||
auto id_tag_info = this->db_handler->get_authorization_cache_entry(id_tag);
|
||||
ASSERT_EQ(exp_id_tag_info.status, id_tag_info.value().status);
|
||||
|
||||
id_tag_info = this->db_handler->get_authorization_cache_entry(unknown_id_tag);
|
||||
ASSERT_EQ(std::nullopt, id_tag_info);
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, test_authorization_cache_entry_2) {
|
||||
|
||||
const auto id_tag = CiString<20>("DEADBEEF");
|
||||
const auto unknown_id_tag = CiString<20>("BEEFBEEF");
|
||||
const auto parent_id_tag = CiString<20>("PARENT");
|
||||
|
||||
IdTagInfo exp_id_tag_info;
|
||||
exp_id_tag_info.status = AuthorizationStatus::Accepted;
|
||||
exp_id_tag_info.expiryDate.emplace(DateTime());
|
||||
exp_id_tag_info.parentIdTag = parent_id_tag;
|
||||
|
||||
this->db_handler->insert_or_update_authorization_cache_entry(id_tag, exp_id_tag_info);
|
||||
auto id_tag_info = this->db_handler->get_authorization_cache_entry(id_tag);
|
||||
// expired because expiry date was set to now
|
||||
ASSERT_EQ(AuthorizationStatus::Expired, id_tag_info.value().status);
|
||||
ASSERT_EQ(exp_id_tag_info.parentIdTag.value().get(), parent_id_tag.get());
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, test_clear_authorization_cache) {
|
||||
|
||||
const auto id_tag = CiString<20>("DEADBEEF");
|
||||
const auto parent_id_tag = CiString<20>("PARENT");
|
||||
|
||||
IdTagInfo exp_id_tag_info;
|
||||
exp_id_tag_info.status = AuthorizationStatus::Accepted;
|
||||
exp_id_tag_info.expiryDate.emplace(DateTime(DateTime().to_time_point() + std::chrono::hours(24)));
|
||||
exp_id_tag_info.parentIdTag = parent_id_tag;
|
||||
|
||||
this->db_handler->insert_or_update_authorization_cache_entry(id_tag, exp_id_tag_info);
|
||||
auto id_tag_info = this->db_handler->get_authorization_cache_entry(id_tag);
|
||||
|
||||
ASSERT_EQ(exp_id_tag_info.status, id_tag_info.value().status);
|
||||
ASSERT_EQ(exp_id_tag_info.parentIdTag.value().get(), parent_id_tag.get());
|
||||
|
||||
this->db_handler->clear_authorization_cache();
|
||||
|
||||
id_tag_info = this->db_handler->get_authorization_cache_entry(id_tag);
|
||||
ASSERT_EQ(std::nullopt, id_tag_info);
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, test_connector_availability) {
|
||||
|
||||
std::vector<std::int32_t> connectors;
|
||||
connectors.push_back(1);
|
||||
connectors.push_back(2);
|
||||
|
||||
this->db_handler->insert_or_update_connector_availability(connectors, AvailabilityType::Inoperative);
|
||||
|
||||
auto availability_type_1 = this->db_handler->get_connector_availability(1);
|
||||
auto availability_type_2 = this->db_handler->get_connector_availability(2);
|
||||
|
||||
ASSERT_EQ(AvailabilityType::Inoperative, availability_type_1);
|
||||
ASSERT_EQ(AvailabilityType::Inoperative, availability_type_2);
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, test_insert_and_get_transaction) {
|
||||
|
||||
std::optional<CiString<20>> id_tag;
|
||||
id_tag.emplace(CiString<20>("DEADBEEF"));
|
||||
|
||||
this->db_handler->insert_transaction("id-42", -1, 1, "DEADBEEF", "2022-08-18T09:42:41", 42, false, 42, "xyz");
|
||||
this->db_handler->update_transaction("id-42", 42);
|
||||
this->db_handler->update_transaction("id-42", 5000, "2022-08-18T10:42:41", id_tag, Reason::EVDisconnected, "xyz");
|
||||
this->db_handler->update_transaction_csms_ack(42);
|
||||
|
||||
this->db_handler->insert_transaction("id-43", -1, 1, "BEEFDEAD", "2022-08-18T09:42:41", 43, false, 43, "xyz");
|
||||
|
||||
auto incomplete_transactions = this->db_handler->get_transactions(true);
|
||||
|
||||
ASSERT_EQ(incomplete_transactions.size(), 1);
|
||||
auto transaction = incomplete_transactions.at(0);
|
||||
|
||||
ASSERT_EQ(transaction.session_id, "id-43");
|
||||
ASSERT_EQ(transaction.transaction_id, -1);
|
||||
|
||||
auto all_transactions = this->db_handler->get_transactions();
|
||||
ASSERT_EQ(all_transactions.size(), 2);
|
||||
transaction = all_transactions.at(0);
|
||||
ASSERT_EQ(transaction.id_tag_end.value(), "DEADBEEF");
|
||||
ASSERT_EQ(transaction.connector, 1);
|
||||
ASSERT_EQ(transaction.id_tag_start, "DEADBEEF");
|
||||
ASSERT_EQ(transaction.meter_start, 42);
|
||||
ASSERT_EQ(transaction.meter_stop.value(), 5000);
|
||||
ASSERT_EQ(transaction.stop_reason.value(), "EVDisconnected");
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, test_insert_and_get_transaction_without_id_tag) {
|
||||
|
||||
std::optional<CiString<20>> id_tag;
|
||||
this->db_handler->insert_transaction("id-42", -1, 1, "DEADBEEF", "2022-08-18T09:42:41", 42, false, 42, "xyz");
|
||||
this->db_handler->update_transaction("id-42", 42);
|
||||
this->db_handler->update_transaction("id-42", 5000, "2022-08-18T10:42:41", id_tag, Reason::EVDisconnected, "xyz");
|
||||
this->db_handler->update_transaction_csms_ack(42);
|
||||
|
||||
this->db_handler->insert_transaction("id-43", -1, 1, "BEEFDEAD", "2022-08-18T09:42:41", 43, false, 43, "xyz");
|
||||
|
||||
auto incomplete_transactions = this->db_handler->get_transactions(true);
|
||||
|
||||
ASSERT_EQ(incomplete_transactions.size(), 1);
|
||||
auto transaction = incomplete_transactions.at(0);
|
||||
|
||||
ASSERT_EQ(transaction.session_id, "id-43");
|
||||
ASSERT_EQ(transaction.transaction_id, -1);
|
||||
|
||||
auto all_transactions = this->db_handler->get_transactions();
|
||||
ASSERT_EQ(all_transactions.size(), 2);
|
||||
|
||||
transaction = all_transactions.at(0);
|
||||
ASSERT_FALSE(transaction.id_tag_end);
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, test_get_transaction_by_id_found) {
|
||||
this->db_handler->insert_transaction("session-abc", 42, 1, "RFID1", "2022-08-18T09:42:41", 0, false, std::nullopt,
|
||||
"msg-1");
|
||||
|
||||
const auto entry = this->db_handler->get_transaction(42);
|
||||
|
||||
ASSERT_TRUE(entry.has_value());
|
||||
ASSERT_EQ(entry->session_id, "session-abc");
|
||||
ASSERT_EQ(entry->transaction_id, 42);
|
||||
ASSERT_EQ(entry->connector, 1);
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, test_get_transaction_by_id_not_found) {
|
||||
// No transaction inserted — unknown transaction_id must return nullopt.
|
||||
const auto entry = this->db_handler->get_transaction(99999);
|
||||
|
||||
ASSERT_EQ(entry, std::nullopt);
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, test_insert_and_get_profiles) {
|
||||
// TODO enable again on fixing https://github.com/EVerest/libocpp/issues/384
|
||||
GTEST_SKIP() << "validFrom/validTo checks are failing. See https://github.com/EVerest/libocpp/issues/384";
|
||||
|
||||
const auto profile = get_sample_charging_profile();
|
||||
|
||||
this->db_handler->insert_or_update_charging_profile(1, profile);
|
||||
|
||||
const auto profiles = this->db_handler->get_charging_profiles();
|
||||
|
||||
ASSERT_EQ(profiles.size(), 1);
|
||||
const auto db_profile = profiles.at(0);
|
||||
|
||||
ASSERT_EQ(db_profile.chargingProfileId, profile.chargingProfileId);
|
||||
ASSERT_EQ(db_profile.stackLevel, profile.stackLevel);
|
||||
ASSERT_EQ(db_profile.chargingProfilePurpose, profile.chargingProfilePurpose);
|
||||
ASSERT_EQ(db_profile.chargingProfileKind, profile.chargingProfileKind);
|
||||
ASSERT_EQ(db_profile.recurrencyKind.value(), profile.recurrencyKind.value());
|
||||
ASSERT_EQ(db_profile.validFrom.value().to_rfc3339(), profile.validFrom.value().to_rfc3339());
|
||||
ASSERT_EQ(db_profile.validTo.value().to_rfc3339(), profile.validTo.value().to_rfc3339());
|
||||
|
||||
ASSERT_EQ(db_profile.chargingSchedule.chargingRateUnit, ChargingRateUnit::A);
|
||||
ASSERT_EQ(db_profile.chargingSchedule.duration, profile.chargingSchedule.duration);
|
||||
ASSERT_EQ(db_profile.chargingSchedule.startSchedule.value().to_rfc3339(),
|
||||
profile.chargingSchedule.startSchedule.value().to_rfc3339());
|
||||
ASSERT_EQ(db_profile.chargingSchedule.minChargingRate.value(), profile.chargingSchedule.minChargingRate.value());
|
||||
|
||||
for (size_t i = 0; i < profile.chargingSchedule.chargingSchedulePeriod.size(); i++) {
|
||||
ASSERT_EQ(db_profile.chargingSchedule.chargingSchedulePeriod.at(i).startPeriod,
|
||||
profile.chargingSchedule.chargingSchedulePeriod.at(i).startPeriod);
|
||||
ASSERT_EQ(db_profile.chargingSchedule.chargingSchedulePeriod.at(i).limit,
|
||||
profile.chargingSchedule.chargingSchedulePeriod.at(i).limit);
|
||||
ASSERT_EQ(db_profile.chargingSchedule.chargingSchedulePeriod.at(i).numberPhases.value(),
|
||||
profile.chargingSchedule.chargingSchedulePeriod.at(i).numberPhases.value());
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, test_update_profile_same_profile_id) {
|
||||
const auto profile1 = get_sample_charging_profile();
|
||||
const auto profile2 = get_sample_charging_profile();
|
||||
|
||||
this->db_handler->insert_or_update_charging_profile(1, profile1);
|
||||
this->db_handler->insert_or_update_charging_profile(2, profile2);
|
||||
|
||||
const auto profiles = this->db_handler->get_charging_profiles();
|
||||
|
||||
ASSERT_EQ(profiles.size(), 1);
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, test_update_profile_same_purpose_and_level_non_zero) {
|
||||
const auto profile1 = get_sample_charging_profile();
|
||||
auto profile2 = get_sample_charging_profile();
|
||||
|
||||
profile2.chargingProfileId++; // different profile ID
|
||||
|
||||
this->db_handler->insert_or_update_charging_profile(1, profile1);
|
||||
this->db_handler->insert_or_update_charging_profile(2, profile2);
|
||||
|
||||
const auto profiles = this->db_handler->get_charging_profiles();
|
||||
|
||||
ASSERT_EQ(profiles.size(), 1);
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, test_update_profile_same_purpose_and_level_connector_zero) {
|
||||
const auto profile1 = get_sample_charging_profile();
|
||||
auto profile2 = get_sample_charging_profile();
|
||||
|
||||
profile2.chargingProfileId++; // different profile ID
|
||||
|
||||
this->db_handler->insert_or_update_charging_profile(0, profile1);
|
||||
this->db_handler->insert_or_update_charging_profile(0, profile2);
|
||||
|
||||
const auto profiles = this->db_handler->get_charging_profiles();
|
||||
|
||||
ASSERT_EQ(profiles.size(), 1);
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, test_delete_profile) {
|
||||
const auto profile1 = get_sample_charging_profile();
|
||||
auto profile2 = get_sample_charging_profile();
|
||||
|
||||
profile2.chargingProfileId = 2;
|
||||
|
||||
// two profiles with same purpose and level are not allowed
|
||||
// see OCPP 1.6 3.13.2. Stacking charging profiles
|
||||
profile2.stackLevel++;
|
||||
|
||||
this->db_handler->insert_or_update_charging_profile(1, profile1);
|
||||
this->db_handler->insert_or_update_charging_profile(2, profile2);
|
||||
|
||||
auto profiles = this->db_handler->get_charging_profiles();
|
||||
ASSERT_EQ(profiles.size(), 2);
|
||||
|
||||
this->db_handler->delete_charging_profile(1);
|
||||
|
||||
profiles = this->db_handler->get_charging_profiles();
|
||||
ASSERT_EQ(profiles.size(), 1);
|
||||
|
||||
this->db_handler->delete_charging_profiles();
|
||||
|
||||
profiles = this->db_handler->get_charging_profiles();
|
||||
ASSERT_EQ(profiles.size(), 0);
|
||||
}
|
||||
|
||||
TEST_F(DatabaseTest, test_unknown_connector) {
|
||||
ASSERT_THROW(this->db_handler->get_connector_availability(5), RequiredEntryNotFoundException);
|
||||
ASSERT_THROW(this->db_handler->get_connector_id(5), RequiredEntryNotFoundException);
|
||||
|
||||
auto database_connection = std::make_unique<Connection>("file::memory:?cache=shared");
|
||||
database_connection->open_connection(); // Open connection so memory stays shared
|
||||
|
||||
database_connection->execute_statement("DROP TABLE CHARGING_PROFILES");
|
||||
|
||||
ASSERT_THROW(this->db_handler->get_charging_profiles(), QueryExecutionException);
|
||||
}
|
||||
|
||||
} // namespace v16
|
||||
} // namespace ocpp
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"chargingProfileId": 301,
|
||||
"chargingProfileKind": "Absolute",
|
||||
"chargingProfilePurpose": "TxDefaultProfile",
|
||||
"chargingSchedule": {
|
||||
"chargingRateUnit": "A",
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 32.0,
|
||||
"startPeriod": 0
|
||||
},
|
||||
{
|
||||
"limit": 31.0,
|
||||
"startPeriod": 1800
|
||||
},
|
||||
{
|
||||
"limit": 30.0,
|
||||
"startPeriod": 2700
|
||||
}
|
||||
],
|
||||
"duration": 3600,
|
||||
"startSchedule": "2024-01-01T12:02:00Z"
|
||||
},
|
||||
"stackLevel": 5,
|
||||
"validFrom": "2024-01-01T12:00:00Z",
|
||||
"validTo": "2024-01-01T14:00:00Z"
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"chargingProfileId": 24,
|
||||
"chargingProfileKind": "Recurring",
|
||||
"chargingProfilePurpose": "ChargePointMaxProfile",
|
||||
"chargingSchedule": {
|
||||
"id": 0,
|
||||
"chargingRateUnit": "A",
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 16.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 0
|
||||
},
|
||||
{
|
||||
"limit": 16.0,
|
||||
"numberPhases": 3,
|
||||
"startPeriod": 3600
|
||||
},
|
||||
{
|
||||
"limit": 10.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 7200
|
||||
},
|
||||
{
|
||||
"limit": 10.0,
|
||||
"numberPhases": 3,
|
||||
"startPeriod": 10800
|
||||
}
|
||||
],
|
||||
"duration": 86400,
|
||||
"minChargingRate": 0.0,
|
||||
"startSchedule": "2024-01-17T00:00:00.000Z"
|
||||
},
|
||||
"recurrencyKind": "Daily",
|
||||
"stackLevel": 0
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"chargingProfileId": 24,
|
||||
"chargingProfileKind": "Recurring",
|
||||
"chargingProfilePurpose": "ChargePointMaxProfile",
|
||||
"chargingSchedule": {
|
||||
"chargingRateUnit": "A",
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 24.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 0
|
||||
},
|
||||
{
|
||||
"limit": 28.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 900
|
||||
},
|
||||
{
|
||||
"limit": 30.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 1800
|
||||
},
|
||||
{
|
||||
"limit": 32.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 2700
|
||||
}
|
||||
],
|
||||
"duration": 86400,
|
||||
"minChargingRate": 0.0,
|
||||
"startSchedule": "2024-01-17T08:00:00.000Z"
|
||||
},
|
||||
"recurrencyKind": "Daily",
|
||||
"stackLevel": 0
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"chargingProfileId": 301,
|
||||
"chargingProfileKind": "Recurring",
|
||||
"chargingProfilePurpose": "TxDefaultProfile",
|
||||
"chargingSchedule": {
|
||||
"id": 0,
|
||||
"chargingRateUnit": "A",
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 16.0,
|
||||
"startPeriod": 0
|
||||
},
|
||||
{
|
||||
"limit": 15.0,
|
||||
"startPeriod": 1800
|
||||
},
|
||||
{
|
||||
"limit": 14.0,
|
||||
"startPeriod": 2700
|
||||
}
|
||||
],
|
||||
"duration": 3600,
|
||||
"startSchedule": "2024-01-01T08:00:00Z"
|
||||
},
|
||||
"recurrencyKind": "Daily",
|
||||
"stackLevel": 5,
|
||||
"validFrom": "2024-01-01T12:00:00Z",
|
||||
"validTo": "2024-02-01T12:00:00Z"
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"chargingProfileId": 302,
|
||||
"chargingProfileKind": "Recurring",
|
||||
"chargingProfilePurpose": "TxDefaultProfile",
|
||||
"chargingSchedule": {
|
||||
"id": 0,
|
||||
"chargingRateUnit": "A",
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 16.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 0
|
||||
},
|
||||
{
|
||||
"limit": 15.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 1800
|
||||
},
|
||||
{
|
||||
"limit": 14.0,
|
||||
"numberPhases": 3,
|
||||
"startPeriod": 2700
|
||||
}
|
||||
],
|
||||
"duration": 3600,
|
||||
"startSchedule": "2024-01-01T08:00:00Z"
|
||||
},
|
||||
"recurrencyKind": "Daily",
|
||||
"stackLevel": 5,
|
||||
"validFrom": "2024-01-01T12:00:00Z",
|
||||
"validTo": "2024-02-01T12:00:00Z"
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"chargingProfileId": 302,
|
||||
"chargingProfileKind": "Relative",
|
||||
"chargingProfilePurpose": "TxDefaultProfile",
|
||||
"chargingSchedule": {
|
||||
"id": 0,
|
||||
"chargingRateUnit": "A",
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 16.0,
|
||||
"numberPhases": 3,
|
||||
"startPeriod": 0
|
||||
},
|
||||
{
|
||||
"limit": 15.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 1800
|
||||
},
|
||||
{
|
||||
"limit": 14.0,
|
||||
"numberPhases": 3,
|
||||
"startPeriod": 2700
|
||||
}
|
||||
],
|
||||
"duration": 3600
|
||||
},
|
||||
"stackLevel": 5,
|
||||
"validFrom": "2024-01-01T12:00:00Z",
|
||||
"validTo": "2025-01-01T14:00:00Z"
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"chargingProfileId": 301,
|
||||
"chargingProfileKind": "Relative",
|
||||
"chargingProfilePurpose": "TxDefaultProfile",
|
||||
"chargingSchedule": {
|
||||
"chargingRateUnit": "A",
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 16.0,
|
||||
"startPeriod": 0
|
||||
},
|
||||
{
|
||||
"limit": 15.0,
|
||||
"startPeriod": 1800
|
||||
},
|
||||
{
|
||||
"limit": 14.0,
|
||||
"startPeriod": 2700
|
||||
}
|
||||
],
|
||||
"duration": 3600
|
||||
},
|
||||
"stackLevel": 5,
|
||||
"validFrom": "2024-01-01T12:00:00Z",
|
||||
"validTo": "2025-01-01T14:00:00Z"
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"chargingProfileId": 25,
|
||||
"chargingProfileKind": "Relative",
|
||||
"chargingProfilePurpose": "TxDefaultProfile",
|
||||
"chargingSchedule": {
|
||||
"id": 0,
|
||||
"chargingRateUnit": "W",
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 12420.0,
|
||||
"startPeriod": 0
|
||||
},
|
||||
{
|
||||
"limit": 8280.0,
|
||||
"startPeriod": 300
|
||||
},
|
||||
{
|
||||
"limit": 6210.0,
|
||||
"startPeriod": 600
|
||||
},
|
||||
{
|
||||
"limit": 4140.0,
|
||||
"startPeriod": 900
|
||||
},
|
||||
{
|
||||
"limit": 2070.0,
|
||||
"startPeriod": 1200
|
||||
}
|
||||
],
|
||||
"duration": 3600,
|
||||
"minChargingRate": 0.0
|
||||
},
|
||||
"stackLevel": 0
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"chargingProfileId": 1,
|
||||
"chargingProfileKind": "Absolute",
|
||||
"chargingProfilePurpose": "TxDefaultProfile",
|
||||
"chargingSchedule": {
|
||||
"chargingRateUnit": "W",
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 2000.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 0
|
||||
}
|
||||
],
|
||||
"duration": 1080,
|
||||
"minChargingRate": 0.0,
|
||||
"startSchedule": "2024-01-17T18:00:00.000Z"
|
||||
},
|
||||
"recurrencyKind": "Daily",
|
||||
"stackLevel": 1
|
||||
}
|
||||
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"chargingProfileId": 100,
|
||||
"chargingProfileKind": "Recurring",
|
||||
"chargingProfilePurpose": "TxDefaultProfile",
|
||||
"chargingSchedule": {
|
||||
"chargingRateUnit": "W",
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 11000.0,
|
||||
"numberPhases": 3,
|
||||
"startPeriod": 0
|
||||
},
|
||||
{
|
||||
"limit": 6000.0,
|
||||
"numberPhases": 3,
|
||||
"startPeriod": 28800
|
||||
},
|
||||
{
|
||||
"limit": 12000.0,
|
||||
"numberPhases": 3,
|
||||
"startPeriod": 72000
|
||||
}
|
||||
],
|
||||
"duration": 86400,
|
||||
"minChargingRate": 0.0,
|
||||
"startSchedule": "2023-01-17T17:00:00.000Z"
|
||||
},
|
||||
"recurrencyKind": "Daily",
|
||||
"stackLevel": 0
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"chargingProfileId": 10,
|
||||
"chargingProfileKind": "Absolute",
|
||||
"chargingProfilePurpose": "TxDefaultProfile",
|
||||
"chargingSchedule": {
|
||||
"chargingRateUnit": "W",
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 2000.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 0
|
||||
}
|
||||
],
|
||||
"duration": 1080,
|
||||
"minChargingRate": 0.0,
|
||||
"startSchedule": "2024-01-17T17:00:00.000Z"
|
||||
},
|
||||
"recurrencyKind": "Daily",
|
||||
"stackLevel": 1
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"chargingProfileId": 401,
|
||||
"chargingProfileKind": "Recurring",
|
||||
"chargingProfilePurpose": "TxDefaultProfile",
|
||||
"chargingSchedule": {
|
||||
"id": 0,
|
||||
"chargingRateUnit": "A",
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 16.0,
|
||||
"startPeriod": 0
|
||||
}
|
||||
],
|
||||
"duration": 300,
|
||||
"startSchedule": "2024-01-01T08:00:00Z"
|
||||
},
|
||||
"recurrencyKind": "Daily",
|
||||
"stackLevel": 5
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"chargingProfileId": 402,
|
||||
"chargingProfileKind": "Recurring",
|
||||
"chargingProfilePurpose": "TxDefaultProfile",
|
||||
"chargingSchedule": {
|
||||
"id": 0,
|
||||
"chargingRateUnit": "A",
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 12.0,
|
||||
"startPeriod": 0
|
||||
}
|
||||
],
|
||||
"duration": 300,
|
||||
"startSchedule": "2024-01-01T08:05:00Z"
|
||||
},
|
||||
"recurrencyKind": "Daily",
|
||||
"stackLevel": 5
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"chargingProfileId": 11,
|
||||
"chargingProfileKind": "Absolute",
|
||||
"chargingProfilePurpose": "TxDefaultProfile",
|
||||
"chargingSchedule": {
|
||||
"chargingRateUnit": "W",
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 2000.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 0
|
||||
}
|
||||
],
|
||||
"duration": 1080,
|
||||
"minChargingRate": 0.0,
|
||||
"startSchedule": "2024-01-17T17:00:00.000Z"
|
||||
},
|
||||
"recurrencyKind": "Daily",
|
||||
"stackLevel": 1
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"chargingProfileId": 1,
|
||||
"chargingProfileKind": "Absolute",
|
||||
"chargingProfilePurpose": "TxDefaultProfile",
|
||||
"chargingSchedule": {
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 2000.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 0
|
||||
}
|
||||
],
|
||||
"duration": 1080,
|
||||
"minChargingRate": 0.0,
|
||||
"startSchedule": "2024-01-17T17:00:00.000Z"
|
||||
},
|
||||
"recurrencyKind": "Daily",
|
||||
"stackLevel": 1
|
||||
}
|
||||
@@ -0,0 +1,36 @@
|
||||
{
|
||||
"chargingProfileId": 66,
|
||||
"chargingProfileKind": "Relative",
|
||||
"chargingProfilePurpose": "TxDefaultProfile",
|
||||
"chargingSchedule": {
|
||||
"chargingRateUnit": "W",
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 1000,
|
||||
"startPeriod": 0
|
||||
},
|
||||
{
|
||||
"limit": 7000,
|
||||
"startPeriod": 70
|
||||
},
|
||||
{
|
||||
"limit": 0,
|
||||
"startPeriod": 140
|
||||
},
|
||||
{
|
||||
"limit": 6000,
|
||||
"startPeriod": 210
|
||||
},
|
||||
{
|
||||
"limit": 1500,
|
||||
"startPeriod": 280
|
||||
},
|
||||
{
|
||||
"limit": 5000,
|
||||
"startPeriod": 350
|
||||
}
|
||||
],
|
||||
"minChargingRate": 500
|
||||
},
|
||||
"stackLevel": 1
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"chargingProfileId": 2,
|
||||
"chargingProfileKind": "Recurring",
|
||||
"chargingProfilePurpose": "TxProfile",
|
||||
"chargingSchedule": {
|
||||
"chargingRateUnit": "W",
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 2000.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 0
|
||||
}
|
||||
],
|
||||
"duration": 1080,
|
||||
"minChargingRate": 0.0,
|
||||
"startSchedule": "2024-01-17T18:04:00.000Z"
|
||||
},
|
||||
"recurrencyKind": "Daily",
|
||||
"stackLevel": 2
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
{
|
||||
"chargingProfileId": 3,
|
||||
"chargingProfileKind": "Absolute",
|
||||
"chargingProfilePurpose": "TxProfile",
|
||||
"chargingSchedule": {
|
||||
"chargingRateUnit": "W",
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 2000.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 0
|
||||
}
|
||||
],
|
||||
"duration": 1080,
|
||||
"minChargingRate": 0.0,
|
||||
"startSchedule": "2024-01-17T18:04:00.000Z"
|
||||
},
|
||||
"recurrencyKind": "Daily",
|
||||
"stackLevel": 2
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
{
|
||||
"chargingProfileId": 24,
|
||||
"chargingProfileKind": "Recurring",
|
||||
"chargingProfilePurpose": "TxProfile",
|
||||
"chargingSchedule": {
|
||||
"chargingRateUnit": "W",
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 1.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 0
|
||||
},
|
||||
{
|
||||
"limit": 2.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 3600
|
||||
},
|
||||
{
|
||||
"limit": 3.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 7200
|
||||
},
|
||||
{
|
||||
"limit": 4.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 10800
|
||||
},
|
||||
{
|
||||
"limit": 5.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 14400
|
||||
},
|
||||
{
|
||||
"limit": 6.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 18000
|
||||
},
|
||||
{
|
||||
"limit": 7.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 21600
|
||||
},
|
||||
{
|
||||
"limit": 8.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 25200
|
||||
},
|
||||
{
|
||||
"limit": 9.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 28800
|
||||
},
|
||||
{
|
||||
"limit": 10.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 32400
|
||||
},
|
||||
{
|
||||
"limit": 11.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 36000
|
||||
},
|
||||
{
|
||||
"limit": 12.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 39600
|
||||
},
|
||||
{
|
||||
"limit": 13.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 43200
|
||||
},
|
||||
{
|
||||
"limit": 14.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 46800
|
||||
},
|
||||
{
|
||||
"limit": 15.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 50400
|
||||
},
|
||||
{
|
||||
"limit": 16.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 54000
|
||||
},
|
||||
{
|
||||
"limit": 17.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 57600
|
||||
},
|
||||
{
|
||||
"limit": 18.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 61200
|
||||
},
|
||||
{
|
||||
"limit": 19.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 64800
|
||||
},
|
||||
{
|
||||
"limit": 20.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 68400
|
||||
},
|
||||
{
|
||||
"limit": 21.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 72000
|
||||
},
|
||||
{
|
||||
"limit": 22.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 75600
|
||||
},
|
||||
{
|
||||
"limit": 23.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 79200
|
||||
},
|
||||
{
|
||||
"limit": 24.0,
|
||||
"numberPhases": 1,
|
||||
"startPeriod": 82800
|
||||
}
|
||||
],
|
||||
"duration": 86400,
|
||||
"minChargingRate": 0.0,
|
||||
"startSchedule": "2023-01-17T00:00:00.000Z"
|
||||
},
|
||||
"recurrencyKind": "Daily",
|
||||
"stackLevel": 0
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,784 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
// execute: ./libocpp_unit_tests --gtest_filter=ProfileTests.*
|
||||
|
||||
#include "database_stub.hpp"
|
||||
#include "ocpp/v16/ocpp_types.hpp"
|
||||
#include "ocpp/v16/types.hpp"
|
||||
#include "profile_tests_common.hpp"
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <gtest/gtest.h>
|
||||
#include <memory>
|
||||
#include <ocpp/v16/connector.hpp>
|
||||
#include <ocpp/v16/database_handler.hpp>
|
||||
#include <ocpp/v16/smart_charging.hpp>
|
||||
#include <optional>
|
||||
#include <ostream>
|
||||
#include <string>
|
||||
|
||||
using namespace ocpp::v16;
|
||||
using namespace ocpp;
|
||||
namespace fs = std::filesystem;
|
||||
using json = nlohmann::json;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Test anonymous namespace
|
||||
namespace {
|
||||
using namespace std::chrono;
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Test charging profiles
|
||||
|
||||
const auto now = date::utc_clock::now();
|
||||
const ocpp::DateTime profileA_start_time(now - seconds(600));
|
||||
const ocpp::DateTime profileA_end_time(now + hours(2));
|
||||
const ChargingProfile profileA{
|
||||
301, // chargingProfileId
|
||||
5, // stackLevel
|
||||
ChargingProfilePurposeType::TxDefaultProfile, // chargingProfilePurpose
|
||||
ChargingProfileKindType::Absolute, // chargingProfileKind
|
||||
{
|
||||
// ChargingSchedule
|
||||
ChargingRateUnit::A, // chargingRateUnit
|
||||
{
|
||||
{
|
||||
// ChargingSchedulePeriod
|
||||
0, // startPeriod
|
||||
32.0, // limit
|
||||
std::nullopt, // optional - std::int32_t - numberPhases
|
||||
},
|
||||
{
|
||||
// ChargingSchedulePeriod
|
||||
6000, // startPeriod
|
||||
31.0, // limit
|
||||
std::nullopt, // optional - std::int32_t - numberPhases
|
||||
},
|
||||
{
|
||||
// ChargingSchedulePeriod
|
||||
12000, // startPeriod
|
||||
30.0, // limit
|
||||
std::nullopt, // optional - std::int32_t - numberPhases
|
||||
},
|
||||
},
|
||||
std::nullopt, // optional - std::int32_t duration
|
||||
profileA_start_time, // optional - ocpp::DateTime - startSchedule
|
||||
std::nullopt, // optional - float - minChargingRate
|
||||
}, // chargingSchedule
|
||||
std::nullopt, // transactionId
|
||||
std::nullopt, // recurrencyKind
|
||||
profileA_start_time, // validFrom
|
||||
profileA_end_time, // validTo
|
||||
};
|
||||
|
||||
ocpp::DateTime profileB_start_time(now);
|
||||
ocpp::DateTime profileB_end_time(now + hours(4));
|
||||
ChargingProfile profileB{
|
||||
302, // chargingProfileId
|
||||
5, // stackLevel
|
||||
ChargingProfilePurposeType::TxDefaultProfile, // chargingProfilePurpose
|
||||
ChargingProfileKindType::Absolute, // chargingProfileKind
|
||||
{
|
||||
// ChargingSchedule
|
||||
ChargingRateUnit::A, // chargingRateUnit
|
||||
{
|
||||
{
|
||||
// ChargingSchedulePeriod
|
||||
0, // startPeriod
|
||||
10.0, // limit
|
||||
std::nullopt, // optional - std::int32_t - numberPhases
|
||||
},
|
||||
{
|
||||
// ChargingSchedulePeriod
|
||||
7000, // startPeriod
|
||||
11.0, // limit
|
||||
std::nullopt, // optional - std::int32_t - numberPhases
|
||||
},
|
||||
},
|
||||
std::nullopt, // optional - std::int32_t duration
|
||||
profileB_start_time, // optional - ocpp::DateTime - startSchedule
|
||||
std::nullopt, // optional - float - minChargingRate
|
||||
}, // chargingSchedule
|
||||
std::nullopt, // transactionId
|
||||
std::nullopt, // recurrencyKind
|
||||
profileB_start_time, // validFrom
|
||||
profileB_end_time, // validTo
|
||||
};
|
||||
|
||||
ocpp::DateTime profileNoCharge_start_time(now - seconds(300));
|
||||
ocpp::DateTime profileNoCharge_end_time(now + hours(300));
|
||||
ChargingProfile profileNoCharge{
|
||||
302, // chargingProfileId
|
||||
5, // stackLevel
|
||||
ChargingProfilePurposeType::TxDefaultProfile, // chargingProfilePurpose
|
||||
ChargingProfileKindType::Relative, // chargingProfileKind
|
||||
{
|
||||
// ChargingSchedule
|
||||
ChargingRateUnit::A, // chargingRateUnit
|
||||
{
|
||||
{
|
||||
// ChargingSchedulePeriod
|
||||
0, // startPeriod
|
||||
0.0, // limit
|
||||
std::nullopt, // optional - std::int32_t - numberPhases
|
||||
},
|
||||
},
|
||||
std::nullopt, // optional - std::int32_t duration
|
||||
std::nullopt, // optional - ocpp::DateTime - startSchedule
|
||||
std::nullopt, // optional - float - minChargingRate
|
||||
}, // chargingSchedule
|
||||
std::nullopt, // transactionId
|
||||
std::nullopt, // recurrencyKind
|
||||
profileNoCharge_start_time, // validFrom
|
||||
profileNoCharge_end_time, // validTo
|
||||
};
|
||||
|
||||
ocpp::DateTime profileStack_start_time(now - minutes(5));
|
||||
ocpp::DateTime profileStack_end_time(now + hours(9));
|
||||
ChargingProfile profileStackA{
|
||||
303, // chargingProfileId
|
||||
10, // stackLevel
|
||||
ChargingProfilePurposeType::TxDefaultProfile, // chargingProfilePurpose
|
||||
ChargingProfileKindType::Absolute, // chargingProfileKind
|
||||
{
|
||||
// ChargingSchedule
|
||||
ChargingRateUnit::A, // chargingRateUnit
|
||||
{
|
||||
{
|
||||
// ChargingSchedulePeriod
|
||||
0, // startPeriod
|
||||
24.0, // limit
|
||||
std::nullopt, // optional - std::int32_t - numberPhases
|
||||
},
|
||||
},
|
||||
std::nullopt, // optional - std::int32_t duration
|
||||
profileStack_start_time, // optional - ocpp::DateTime - startSchedule
|
||||
std::nullopt, // optional - float - minChargingRate
|
||||
}, // chargingSchedule
|
||||
std::nullopt, // transactionId
|
||||
std::nullopt, // recurrencyKind
|
||||
profileStack_start_time, // validFrom
|
||||
profileStack_end_time, // validTo
|
||||
};
|
||||
|
||||
ChargingProfile profileStackB{
|
||||
304, // chargingProfileId
|
||||
20, // stackLevel
|
||||
ChargingProfilePurposeType::TxDefaultProfile, // chargingProfilePurpose
|
||||
ChargingProfileKindType::Absolute, // chargingProfileKind
|
||||
{
|
||||
// ChargingSchedule
|
||||
ChargingRateUnit::A, // chargingRateUnit
|
||||
{
|
||||
{
|
||||
// ChargingSchedulePeriod
|
||||
0, // startPeriod
|
||||
26.0, // limit
|
||||
std::nullopt, // optional - std::int32_t - numberPhases
|
||||
},
|
||||
},
|
||||
std::nullopt, // optional - std::int32_t duration
|
||||
profileStack_start_time, // optional - ocpp::DateTime - startSchedule
|
||||
std::nullopt, // optional - float - minChargingRate
|
||||
}, // chargingSchedule
|
||||
std::nullopt, // transactionId
|
||||
std::nullopt, // recurrencyKind
|
||||
profileStack_start_time, // validFrom
|
||||
profileStack_end_time, // validTo
|
||||
};
|
||||
|
||||
ocpp::DateTime profileStackC_start_time(now + minutes(30));
|
||||
ocpp::DateTime profileStackC_end_time(now + hours(9));
|
||||
ChargingProfile profileStackC{
|
||||
305, // chargingProfileId
|
||||
50, // stackLevel
|
||||
ChargingProfilePurposeType::TxDefaultProfile, // chargingProfilePurpose
|
||||
ChargingProfileKindType::Absolute, // chargingProfileKind
|
||||
{
|
||||
// ChargingSchedule
|
||||
ChargingRateUnit::A, // chargingRateUnit
|
||||
{
|
||||
{
|
||||
// ChargingSchedulePeriod
|
||||
0, // startPeriod
|
||||
28.0, // limit
|
||||
std::nullopt, // optional - std::int32_t - numberPhases
|
||||
},
|
||||
},
|
||||
std::nullopt, // optional - std::int32_t duration
|
||||
profileStackC_start_time, // optional - ocpp::DateTime - startSchedule
|
||||
std::nullopt, // optional - float - minChargingRate
|
||||
}, // chargingSchedule
|
||||
std::nullopt, // transactionId
|
||||
std::nullopt, // recurrencyKind
|
||||
profileStackC_start_time, // validFrom
|
||||
profileStackC_end_time, // validTo
|
||||
};
|
||||
|
||||
ocpp::DateTime profileTime_start_time(now + minutes(5));
|
||||
ocpp::DateTime profileTime_end_time(now + hours(9));
|
||||
ChargingProfile profileTime{
|
||||
401, // chargingProfileId
|
||||
90, // stackLevel
|
||||
ChargingProfilePurposeType::TxDefaultProfile, // chargingProfilePurpose
|
||||
ChargingProfileKindType::Absolute, // chargingProfileKind
|
||||
{
|
||||
// ChargingSchedule
|
||||
ChargingRateUnit::A, // chargingRateUnit
|
||||
{
|
||||
{
|
||||
// ChargingSchedulePeriod
|
||||
0, // startPeriod
|
||||
8.0, // limit
|
||||
std::nullopt, // optional - std::int32_t - numberPhases
|
||||
},
|
||||
},
|
||||
std::nullopt, // optional - std::int32_t duration
|
||||
profileTime_start_time, // optional - ocpp::DateTime - startSchedule
|
||||
std::nullopt, // optional - float - minChargingRate
|
||||
}, // chargingSchedule
|
||||
std::nullopt, // transactionId
|
||||
std::nullopt, // recurrencyKind
|
||||
profileTime_start_time, // validFrom
|
||||
profileTime_end_time, // validTo
|
||||
};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Test class
|
||||
class ProfileTestsB : public stubs::DbTestBase {};
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// Test cases
|
||||
|
||||
TEST(DateTime, init) {
|
||||
const ocpp::DateTime base(now);
|
||||
const ocpp::DateTime construct(base);
|
||||
const ocpp::DateTime construct_time(base.to_time_point());
|
||||
|
||||
EXPECT_EQ(base, construct);
|
||||
EXPECT_EQ(base, construct_time);
|
||||
EXPECT_TRUE(nearly_equal(base, construct));
|
||||
EXPECT_TRUE(nearly_equal(base, construct_time));
|
||||
|
||||
const ocpp::DateTime floor_construct_time(floor<seconds>(base.to_time_point()));
|
||||
EXPECT_TRUE(nearly_equal(base, floor_construct_time));
|
||||
|
||||
std::optional<ocpp::DateTime> opt_dt;
|
||||
opt_dt.emplace(ocpp::DateTime(floor<seconds>(base.to_time_point())));
|
||||
EXPECT_EQ(floor_construct_time, opt_dt.value());
|
||||
EXPECT_TRUE(nearly_equal(base, opt_dt.value()));
|
||||
}
|
||||
|
||||
TEST_F(ProfileTestsB, init) {
|
||||
add_connectors(2);
|
||||
// map doesn't include connector 0, database does
|
||||
SmartChargingHandler handler(connectors, database_handler, *configuration);
|
||||
ChargingProfile profile{
|
||||
101, // chargingProfileId
|
||||
20, // stackLevel
|
||||
ChargingProfilePurposeType::TxDefaultProfile, // chargingProfilePurpose
|
||||
ChargingProfileKindType::Relative, // chargingProfileKind
|
||||
{ChargingRateUnit::A, {}, std::nullopt, std::nullopt, std::nullopt}, // chargingSchedule
|
||||
std::nullopt, // transactionId
|
||||
std::nullopt, // recurrencyKind
|
||||
std::nullopt, // validFrom
|
||||
std::nullopt, // validTo
|
||||
};
|
||||
handler.add_tx_default_profile(profile, 1);
|
||||
// the following has a valgrind/memcheck reported leak via EVLOG_info and into the
|
||||
// boost libraries
|
||||
handler.clear_all_profiles();
|
||||
}
|
||||
|
||||
TEST_F(ProfileTestsB, validate_profileA) {
|
||||
// need to have a transaction for calculate_composite_schedule()
|
||||
// and calculate_enhanced_composite_schedule()
|
||||
int connector_id = 1;
|
||||
std::int32_t meter_start = 0;
|
||||
ocpp::DateTime timestamp(now);
|
||||
add_connectors(5);
|
||||
connectors[connector_id]->transaction =
|
||||
std::make_shared<Transaction>(-1, connector_id, "1234", "4567", meter_start, std::nullopt, timestamp, nullptr);
|
||||
// map doesn't include connector 0, database does
|
||||
SmartChargingHandler handler(connectors, database_handler, *configuration);
|
||||
auto tmp_profile = profileA;
|
||||
EXPECT_TRUE(
|
||||
handler.validate_profile(tmp_profile, 0, true, 100, 10, 10, {ChargingRateUnit::A, ChargingRateUnit::W}));
|
||||
// check profile not updated
|
||||
EXPECT_EQ(tmp_profile, profileA);
|
||||
handler.add_tx_default_profile(tmp_profile, connector_id);
|
||||
auto valid_profiles = handler.get_valid_profiles(profileA_start_time, profileA_end_time, connector_id);
|
||||
auto schedule = handler.calculate_composite_schedule(profileA_start_time, profileA_end_time, connector_id,
|
||||
ChargingRateUnit::A, false, true);
|
||||
// std::cout << "chargingSchedule:\n" << profileA << std::endl;
|
||||
// std::cout << "schedule:\n" << schedule << std::endl;
|
||||
EXPECT_EQ(profileA.chargingSchedule, schedule);
|
||||
auto enhanced_schedule = handler.calculate_enhanced_composite_schedule(
|
||||
profileA_start_time, profileA_end_time, connector_id, ChargingRateUnit::A, false, true);
|
||||
// std::cout << "enhanced schedule:\n" << enhanced_schedule << std::endl;
|
||||
EXPECT_EQ(profileA.chargingSchedule, enhanced_schedule);
|
||||
}
|
||||
|
||||
TEST_F(ProfileTestsB, validate_profileB) {
|
||||
// need to have a transaction for calculate_composite_schedule()
|
||||
// and calculate_enhanced_composite_schedule()
|
||||
int connector_id = 1;
|
||||
std::int32_t meter_start = 0;
|
||||
ocpp::DateTime timestamp(now + seconds(5));
|
||||
add_connectors(5);
|
||||
connectors[connector_id]->transaction =
|
||||
std::make_shared<Transaction>(-1, connector_id, "1234", "4567", meter_start, std::nullopt, timestamp, nullptr);
|
||||
// map doesn't include connector 0, database does
|
||||
SmartChargingHandler handler(connectors, database_handler, *configuration);
|
||||
auto tmp_profile = profileB;
|
||||
EXPECT_TRUE(
|
||||
handler.validate_profile(tmp_profile, 0, true, 100, 10, 10, {ChargingRateUnit::A, ChargingRateUnit::W}));
|
||||
// check profile not updated
|
||||
EXPECT_EQ(tmp_profile, profileB);
|
||||
handler.add_tx_default_profile(tmp_profile, connector_id);
|
||||
auto valid_profiles = handler.get_valid_profiles(profileB_start_time, profileB_end_time, connector_id);
|
||||
auto schedule = handler.calculate_composite_schedule(profileB_start_time, profileB_end_time, connector_id,
|
||||
ChargingRateUnit::A, false, true);
|
||||
EXPECT_EQ(profileB.chargingSchedule, schedule);
|
||||
auto enhanced_schedule = handler.calculate_enhanced_composite_schedule(
|
||||
profileB_start_time, profileB_end_time, connector_id, ChargingRateUnit::A, false, true);
|
||||
EXPECT_EQ(profileB.chargingSchedule, enhanced_schedule);
|
||||
}
|
||||
|
||||
TEST_F(ProfileTestsB, tx_default_0) {
|
||||
add_connectors(5);
|
||||
// map doesn't include connector 0, database does
|
||||
SmartChargingHandler handler(connectors, database_handler, *configuration);
|
||||
ChargingProfile profile{
|
||||
201, // chargingProfileId
|
||||
22, // stackLevel
|
||||
ChargingProfilePurposeType::TxDefaultProfile, // chargingProfilePurpose
|
||||
ChargingProfileKindType::Relative, // chargingProfileKind
|
||||
{ChargingRateUnit::A, {}, std::nullopt, std::nullopt, std::nullopt}, // chargingSchedule
|
||||
std::nullopt, // transactionId
|
||||
std::nullopt, // recurrencyKind
|
||||
std::nullopt, // validFrom
|
||||
std::nullopt, // validTo
|
||||
};
|
||||
handler.add_tx_default_profile(profile, 0);
|
||||
handler.clear_all_profiles();
|
||||
}
|
||||
|
||||
TEST_F(ProfileTestsB, single_profile) {
|
||||
std::int32_t connector = 1;
|
||||
std::int32_t meter_start = 0;
|
||||
ocpp::DateTime timestamp(now + seconds(5));
|
||||
add_connectors(1);
|
||||
|
||||
connectors[1]->transaction =
|
||||
std::make_shared<Transaction>(-1, connector, "1234", "4567", meter_start, std::nullopt, timestamp, nullptr);
|
||||
// map doesn't include connector 0, database does
|
||||
SmartChargingHandler handler(connectors, database_handler, *configuration);
|
||||
|
||||
handler.add_tx_default_profile(profileA, 1);
|
||||
|
||||
auto valid_profiles = handler.get_valid_profiles(profileA_start_time, profileA_end_time, 1);
|
||||
// std::cout << valid_profiles << std::endl;
|
||||
ASSERT_EQ(valid_profiles.size(), 1);
|
||||
EXPECT_EQ(profileA.chargingSchedule, valid_profiles[0].chargingSchedule);
|
||||
|
||||
auto schedule = handler.calculate_composite_schedule(profileA_start_time, profileA_end_time, 1, ChargingRateUnit::A,
|
||||
false, true);
|
||||
// std::cout << schedule << std::endl;
|
||||
EXPECT_EQ(profileA.chargingSchedule, schedule);
|
||||
}
|
||||
|
||||
TEST_F(ProfileTestsB, startup_no_charge) {
|
||||
std::int32_t connector = 1;
|
||||
std::int32_t meter_start = 0;
|
||||
ocpp::DateTime start_time(now);
|
||||
ocpp::DateTime end_time(now + hours(1));
|
||||
ocpp::DateTime timestamp(now);
|
||||
add_connectors(1);
|
||||
|
||||
// no active transaction
|
||||
// map doesn't include connector 0, database does
|
||||
SmartChargingHandler handler(connectors, database_handler, *configuration);
|
||||
|
||||
handler.add_tx_default_profile(profileNoCharge, 1);
|
||||
|
||||
auto valid_profiles = handler.get_valid_profiles(start_time, end_time, 1);
|
||||
ASSERT_EQ(valid_profiles.size(), 1);
|
||||
EXPECT_EQ(profileNoCharge.chargingSchedule, valid_profiles[0].chargingSchedule);
|
||||
|
||||
// std::cout << "profileNoCharge: no transaction" << std::endl;
|
||||
auto schedule = handler.calculate_enhanced_composite_schedule(start_time, profileNoCharge_end_time, 1,
|
||||
ChargingRateUnit::A, false, true);
|
||||
// std::cout << "chargingSchedule:" << profileNoCharge.chargingSchedule << std::endl;
|
||||
// std::cout << "schedule:" << schedule.chargingSchedulePeriod << std::endl;
|
||||
EXPECT_EQ(profileNoCharge.chargingSchedule, schedule);
|
||||
|
||||
// now with a transaction
|
||||
connectors[1]->transaction =
|
||||
std::make_shared<Transaction>(-1, connector, "1234", "4567", meter_start, std::nullopt, timestamp, nullptr);
|
||||
valid_profiles = handler.get_valid_profiles(start_time, profileNoCharge_end_time, 1);
|
||||
ASSERT_EQ(valid_profiles.size(), 1);
|
||||
EXPECT_EQ(profileNoCharge.chargingSchedule, valid_profiles[0].chargingSchedule);
|
||||
|
||||
// std::cout << "profileNoCharge: with transaction" << std::endl;
|
||||
schedule = handler.calculate_enhanced_composite_schedule(start_time, profileNoCharge_end_time, 1,
|
||||
ChargingRateUnit::A, false, true);
|
||||
// std::cout << "chargingSchedule:" << profileNoCharge.chargingSchedule << std::endl;
|
||||
// std::cout << "schedule:" << schedule.chargingSchedulePeriod << std::endl;
|
||||
EXPECT_EQ(profileNoCharge.chargingSchedule, schedule);
|
||||
|
||||
// transaction ended
|
||||
start_time = ocpp::DateTime(now + minutes(60));
|
||||
connectors[1]->transaction = nullptr;
|
||||
// std::cout << "profileNoCharge: with transaction finished" << std::endl;
|
||||
valid_profiles = handler.get_valid_profiles(start_time, profileNoCharge_end_time, 1);
|
||||
ASSERT_EQ(valid_profiles.size(), 1);
|
||||
EXPECT_EQ(profileNoCharge.chargingSchedule, valid_profiles[0].chargingSchedule);
|
||||
|
||||
schedule = handler.calculate_enhanced_composite_schedule(start_time, profileNoCharge_end_time, 1,
|
||||
ChargingRateUnit::A, false, true);
|
||||
// std::cout << "chargingSchedule:" << profileNoCharge.chargingSchedule << std::endl;
|
||||
// std::cout << "schedule:" << schedule.chargingSchedulePeriod << std::endl;
|
||||
EXPECT_EQ(profileNoCharge.chargingSchedule, schedule);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// get_valid_profiles tests
|
||||
|
||||
TEST_F(ProfileTestsB, get_valid_profiles_absolute) {
|
||||
std::int32_t connector = 1;
|
||||
add_connectors(1);
|
||||
ocpp::DateTime start_time(now);
|
||||
ocpp::DateTime end_time(now + minutes(10));
|
||||
connectors[1]->transaction =
|
||||
std::make_shared<Transaction>(-1, connector, "1234", "4567", 100, std::nullopt, start_time, nullptr);
|
||||
|
||||
SmartChargingHandler handler(connectors, database_handler, *configuration);
|
||||
|
||||
handler.add_tx_default_profile(profileStackA, 1);
|
||||
handler.add_tx_default_profile(profileStackB, 1);
|
||||
handler.add_tx_default_profile(profileStackC, 1);
|
||||
|
||||
auto valid_profiles = handler.get_valid_profiles(start_time, end_time, 1);
|
||||
auto nProfiles = valid_profiles.size();
|
||||
|
||||
// std::cout << "profiles:" << valid_profiles << std::endl;
|
||||
ASSERT_EQ(nProfiles, 3);
|
||||
EXPECT_EQ(profileStackA, valid_profiles[0]);
|
||||
EXPECT_EQ(profileStackB, valid_profiles[1]);
|
||||
EXPECT_EQ(profileStackC, valid_profiles[2]);
|
||||
}
|
||||
|
||||
TEST_F(ProfileTestsB, get_valid_profiles_absolute_delay) {
|
||||
std::int32_t connector = 1;
|
||||
add_connectors(1);
|
||||
ocpp::DateTime start_time(now);
|
||||
ocpp::DateTime end_time(now + minutes(10));
|
||||
connectors[1]->transaction =
|
||||
std::make_shared<Transaction>(-1, connector, "1234", "4567", 100, std::nullopt, start_time, nullptr);
|
||||
|
||||
auto absoluteB = profileStackB;
|
||||
absoluteB.validFrom = ocpp::DateTime(now + minutes(5));
|
||||
absoluteB.chargingSchedule.startSchedule = ocpp::DateTime(now + minutes(5));
|
||||
|
||||
SmartChargingHandler handler(connectors, database_handler, *configuration);
|
||||
|
||||
handler.add_tx_default_profile(profileStackA, 1);
|
||||
handler.add_tx_default_profile(absoluteB, 1);
|
||||
handler.add_tx_default_profile(profileStackC, 1);
|
||||
|
||||
auto valid_profiles = handler.get_valid_profiles(start_time, end_time, 1);
|
||||
auto nProfiles = valid_profiles.size();
|
||||
|
||||
// std::cout << "profiles:" << valid_profiles << std::endl;
|
||||
ASSERT_EQ(nProfiles, 3);
|
||||
EXPECT_EQ(profileStackA, valid_profiles[0]);
|
||||
EXPECT_EQ(absoluteB, valid_profiles[1]);
|
||||
EXPECT_EQ(profileStackC, valid_profiles[2]);
|
||||
}
|
||||
|
||||
TEST_F(ProfileTestsB, get_valid_profiles_relative) {
|
||||
std::int32_t connector = 1;
|
||||
add_connectors(1);
|
||||
ocpp::DateTime start_time(now);
|
||||
ocpp::DateTime end_time(now + minutes(10));
|
||||
connectors[1]->transaction =
|
||||
std::make_shared<Transaction>(-1, connector, "1234", "4567", 100, std::nullopt, start_time, nullptr);
|
||||
|
||||
auto relativeA = profileStackA;
|
||||
auto relativeB = profileStackB;
|
||||
relativeA.chargingProfileKind = ChargingProfileKindType::Relative;
|
||||
relativeA.chargingSchedule.startSchedule = std::nullopt;
|
||||
relativeB.chargingProfileKind = ChargingProfileKindType::Relative;
|
||||
relativeB.chargingSchedule.startSchedule = std::nullopt;
|
||||
|
||||
SmartChargingHandler handler(connectors, database_handler, *configuration);
|
||||
|
||||
handler.add_tx_default_profile(relativeA, 1);
|
||||
handler.add_tx_default_profile(relativeB, 1);
|
||||
handler.add_tx_default_profile(profileStackC, 1);
|
||||
|
||||
auto valid_profiles = handler.get_valid_profiles(start_time, end_time, 1);
|
||||
auto nProfiles = valid_profiles.size();
|
||||
|
||||
// std::cout << "profiles:" << valid_profiles << std::endl;
|
||||
ASSERT_EQ(nProfiles, 3);
|
||||
EXPECT_EQ(relativeA, valid_profiles[0]);
|
||||
EXPECT_EQ(relativeB, valid_profiles[1]);
|
||||
EXPECT_EQ(profileStackC, valid_profiles[2]);
|
||||
}
|
||||
|
||||
TEST_F(ProfileTestsB, get_valid_profiles_relative_delay) {
|
||||
std::int32_t connector = 1;
|
||||
add_connectors(1);
|
||||
ocpp::DateTime start_time(now);
|
||||
ocpp::DateTime end_time(now + minutes(10));
|
||||
connectors[1]->transaction =
|
||||
std::make_shared<Transaction>(-1, connector, "1234", "4567", 100, std::nullopt, start_time, nullptr);
|
||||
|
||||
auto relativeA = profileStackA;
|
||||
auto relativeB = profileStackB;
|
||||
relativeA.chargingProfileKind = ChargingProfileKindType::Relative;
|
||||
relativeA.chargingSchedule.startSchedule = std::nullopt;
|
||||
relativeB.chargingProfileKind = ChargingProfileKindType::Relative;
|
||||
relativeB.validFrom = ocpp::DateTime(now + minutes(5));
|
||||
relativeB.chargingSchedule.startSchedule = ocpp::DateTime(now + minutes(5));
|
||||
// relativeB.chargingSchedule.startSchedule = std::nullopt;
|
||||
|
||||
SmartChargingHandler handler(connectors, database_handler, *configuration);
|
||||
|
||||
handler.add_tx_default_profile(relativeA, 1);
|
||||
handler.add_tx_default_profile(relativeB, 1);
|
||||
handler.add_tx_default_profile(profileStackC, 1);
|
||||
|
||||
auto valid_profiles = handler.get_valid_profiles(start_time, end_time, 1);
|
||||
auto nProfiles = valid_profiles.size();
|
||||
|
||||
// std::cout << "profiles:" << valid_profiles << std::endl;
|
||||
ASSERT_EQ(nProfiles, 3);
|
||||
EXPECT_EQ(relativeA, valid_profiles[0]);
|
||||
EXPECT_EQ(relativeB, valid_profiles[1]);
|
||||
EXPECT_EQ(profileStackC, valid_profiles[2]);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// calculate_enhanced_composite_schedule tests
|
||||
|
||||
TEST_F(ProfileTestsB, single_absolute) {
|
||||
std::int32_t connector = 1;
|
||||
add_connectors(1);
|
||||
ocpp::DateTime start_time(now);
|
||||
ocpp::DateTime end_time(now + minutes(10));
|
||||
connectors[1]->transaction =
|
||||
std::make_shared<Transaction>(-1, connector, "1234", "4567", 100, std::nullopt, start_time, nullptr);
|
||||
|
||||
auto absoluteA = profileStackA;
|
||||
|
||||
SmartChargingHandler handler(connectors, database_handler, *configuration);
|
||||
|
||||
handler.add_tx_default_profile(absoluteA, 1);
|
||||
handler.add_tx_default_profile(profileStackC, 1);
|
||||
|
||||
auto valid_profiles = handler.get_valid_profiles(start_time, end_time, 1);
|
||||
auto nProfiles = valid_profiles.size();
|
||||
|
||||
// std::cout << "profiles:" << valid_profiles << std::endl;
|
||||
|
||||
auto enhanced_schedule =
|
||||
handler.calculate_enhanced_composite_schedule(start_time, end_time, 1, ChargingRateUnit::A, false, true);
|
||||
// std::cout << "schedule: " << enhanced_schedule << std::endl;
|
||||
// std::cout << "absoluteA.chargingSchedule:" << absoluteA.chargingSchedule << std::endl;
|
||||
EXPECT_EQ(enhanced_schedule.duration.value_or(-1), 600);
|
||||
EXPECT_EQ(enhanced_schedule.startSchedule, floor_seconds(start_time));
|
||||
ASSERT_EQ(enhanced_schedule.chargingSchedulePeriod.size(), 1);
|
||||
EXPECT_EQ(absoluteA.chargingSchedule.chargingSchedulePeriod[0].limit,
|
||||
enhanced_schedule.chargingSchedulePeriod[0].limit);
|
||||
EXPECT_EQ(absoluteA.chargingSchedule.chargingSchedulePeriod[0].startPeriod,
|
||||
enhanced_schedule.chargingSchedulePeriod[0].startPeriod);
|
||||
}
|
||||
|
||||
TEST_F(ProfileTestsB, stack_absolute) {
|
||||
// GTEST_SKIP() << "ignore for now";
|
||||
std::int32_t connector = 1;
|
||||
add_connectors(1);
|
||||
ocpp::DateTime start_time(now);
|
||||
ocpp::DateTime end_time(now + minutes(10));
|
||||
connectors[1]->transaction =
|
||||
std::make_shared<Transaction>(-1, connector, "1234", "4567", 100, std::nullopt, start_time, nullptr);
|
||||
|
||||
SmartChargingHandler handler(connectors, database_handler, *configuration);
|
||||
|
||||
handler.add_tx_default_profile(profileStackA, 1);
|
||||
handler.add_tx_default_profile(profileStackB, 1);
|
||||
handler.add_tx_default_profile(profileStackC, 1);
|
||||
|
||||
auto valid_profiles = handler.get_valid_profiles(start_time, end_time, 1);
|
||||
auto nProfiles = valid_profiles.size();
|
||||
|
||||
// std::cout << "profiles:" << valid_profiles << std::endl;
|
||||
ASSERT_EQ(nProfiles, 3);
|
||||
EXPECT_EQ(profileStackA, valid_profiles[0]);
|
||||
EXPECT_EQ(profileStackB, valid_profiles[1]);
|
||||
EXPECT_EQ(profileStackC, valid_profiles[2]);
|
||||
|
||||
auto schedule = handler.calculate_composite_schedule(start_time, end_time, 1, ChargingRateUnit::A, false, true);
|
||||
ASSERT_EQ(schedule.chargingSchedulePeriod.size(), 1);
|
||||
EXPECT_EQ(schedule.chargingSchedulePeriod[0].startPeriod, 0);
|
||||
EXPECT_EQ(schedule.chargingSchedulePeriod[0].limit, profileStackB.chargingSchedule.chargingSchedulePeriod[0].limit);
|
||||
|
||||
auto enhanced_schedule =
|
||||
handler.calculate_enhanced_composite_schedule(start_time, end_time, 1, ChargingRateUnit::A, false, true);
|
||||
// std::cout << "chargingSchedule:" << profileNoCharge.chargingSchedule << std::endl;
|
||||
// std::cout << "schedule:" << schedule.chargingSchedulePeriod << std::endl;
|
||||
ASSERT_EQ(enhanced_schedule.chargingSchedulePeriod.size(), 1);
|
||||
EXPECT_EQ(enhanced_schedule.startSchedule, floor_seconds(start_time));
|
||||
EXPECT_EQ(enhanced_schedule.chargingSchedulePeriod[0].startPeriod, 0);
|
||||
EXPECT_EQ(enhanced_schedule.chargingSchedulePeriod[0].limit,
|
||||
profileStackB.chargingSchedule.chargingSchedulePeriod[0].limit);
|
||||
}
|
||||
|
||||
TEST_F(ProfileTestsB, stack_absolute_delay) {
|
||||
// GTEST_SKIP() << "ignore for now";
|
||||
std::int32_t connector = 1;
|
||||
add_connectors(1);
|
||||
ocpp::DateTime start_time(now);
|
||||
ocpp::DateTime end_time(now + minutes(10));
|
||||
connectors[1]->transaction =
|
||||
std::make_shared<Transaction>(-1, connector, "1234", "4567", 100, std::nullopt, start_time, nullptr);
|
||||
|
||||
auto absoluteA = profileStackA;
|
||||
absoluteA.validTo = ocpp::DateTime(now + minutes(5));
|
||||
|
||||
auto absoluteB = profileStackB;
|
||||
absoluteB.validFrom = absoluteA.validTo;
|
||||
absoluteB.chargingSchedule.startSchedule = absoluteA.validTo;
|
||||
|
||||
SmartChargingHandler handler(connectors, database_handler, *configuration);
|
||||
|
||||
handler.add_tx_default_profile(absoluteA, 1);
|
||||
handler.add_tx_default_profile(absoluteB, 1);
|
||||
|
||||
auto valid_profiles = handler.get_valid_profiles(start_time, end_time, 1);
|
||||
auto nProfiles = valid_profiles.size();
|
||||
|
||||
// std::cout << "Time now: " << start_time << std::endl;
|
||||
// std::cout << "profiles:" << valid_profiles << std::endl;
|
||||
ASSERT_EQ(nProfiles, 2);
|
||||
EXPECT_EQ(absoluteA, valid_profiles[0]);
|
||||
EXPECT_EQ(absoluteB, valid_profiles[1]);
|
||||
|
||||
auto schedule = handler.calculate_composite_schedule(start_time, end_time, 1, ChargingRateUnit::A, false, true);
|
||||
// std::cout << "schedule:" << schedule << std::endl;
|
||||
// expecting two periods
|
||||
ASSERT_GE(schedule.chargingSchedulePeriod.size(), 2);
|
||||
EXPECT_EQ(schedule.startSchedule, floor_seconds(start_time));
|
||||
EXPECT_EQ(schedule.chargingSchedulePeriod[0].startPeriod, 0);
|
||||
EXPECT_EQ(schedule.chargingSchedulePeriod[0].limit, profileStackA.chargingSchedule.chargingSchedulePeriod[0].limit);
|
||||
EXPECT_EQ(schedule.chargingSchedulePeriod[1].startPeriod, 300);
|
||||
EXPECT_EQ(schedule.chargingSchedulePeriod[1].limit, absoluteB.chargingSchedule.chargingSchedulePeriod[0].limit);
|
||||
|
||||
auto enhanced_schedule =
|
||||
handler.calculate_enhanced_composite_schedule(start_time, end_time, 1, ChargingRateUnit::A, false, true);
|
||||
// std::cout << "schedule:" << enhanced_schedule << std::endl;
|
||||
// expecting two periods
|
||||
ASSERT_EQ(enhanced_schedule.chargingSchedulePeriod.size(), 2);
|
||||
EXPECT_EQ(enhanced_schedule.startSchedule, floor_seconds(start_time));
|
||||
EXPECT_EQ(enhanced_schedule.chargingSchedulePeriod[0].startPeriod, 0);
|
||||
EXPECT_EQ(enhanced_schedule.chargingSchedulePeriod[0].limit,
|
||||
profileStackA.chargingSchedule.chargingSchedulePeriod[0].limit);
|
||||
EXPECT_EQ(enhanced_schedule.chargingSchedulePeriod[1].startPeriod, 300);
|
||||
EXPECT_EQ(enhanced_schedule.chargingSchedulePeriod[1].limit,
|
||||
absoluteB.chargingSchedule.chargingSchedulePeriod[0].limit);
|
||||
}
|
||||
|
||||
TEST_F(ProfileTestsB, stack_absolute_delay_overlap) {
|
||||
// GTEST_SKIP() << "ignore for now";
|
||||
std::int32_t connector = 1;
|
||||
add_connectors(1);
|
||||
ocpp::DateTime start_time(now);
|
||||
ocpp::DateTime end_time(now + minutes(10));
|
||||
connectors[1]->transaction =
|
||||
std::make_shared<Transaction>(-1, connector, "1234", "4567", 100, std::nullopt, start_time, nullptr);
|
||||
|
||||
auto absoluteB = profileStackB;
|
||||
absoluteB.validFrom = ocpp::DateTime(now + minutes(5));
|
||||
absoluteB.chargingSchedule.startSchedule = ocpp::DateTime(now + minutes(5));
|
||||
|
||||
SmartChargingHandler handler(connectors, database_handler, *configuration);
|
||||
|
||||
handler.add_tx_default_profile(profileStackA, 1);
|
||||
handler.add_tx_default_profile(absoluteB, 1);
|
||||
|
||||
auto valid_profiles = handler.get_valid_profiles(start_time, end_time, 1);
|
||||
auto nProfiles = valid_profiles.size();
|
||||
|
||||
// std::cout << "Time now: " << start_time << std::endl;
|
||||
// std::cout << "profiles:" << valid_profiles << std::endl;
|
||||
ASSERT_EQ(nProfiles, 2);
|
||||
EXPECT_EQ(profileStackA, valid_profiles[0]);
|
||||
EXPECT_EQ(absoluteB, valid_profiles[1]);
|
||||
|
||||
auto schedule = handler.calculate_composite_schedule(start_time, end_time, 1, ChargingRateUnit::A, false, true);
|
||||
// std::cout << "schedule:" << schedule << std::endl;
|
||||
// expecting two periods
|
||||
ASSERT_EQ(schedule.chargingSchedulePeriod.size(), 2);
|
||||
EXPECT_EQ(schedule.startSchedule, floor_seconds(start_time));
|
||||
EXPECT_EQ(schedule.chargingSchedulePeriod[0].startPeriod, 0);
|
||||
EXPECT_EQ(schedule.chargingSchedulePeriod[0].limit, profileStackA.chargingSchedule.chargingSchedulePeriod[0].limit);
|
||||
EXPECT_EQ(schedule.chargingSchedulePeriod[1].startPeriod, 300);
|
||||
EXPECT_EQ(schedule.chargingSchedulePeriod[1].limit, absoluteB.chargingSchedule.chargingSchedulePeriod[0].limit);
|
||||
|
||||
auto enhanced_schedule =
|
||||
handler.calculate_enhanced_composite_schedule(start_time, end_time, 1, ChargingRateUnit::A, false, true);
|
||||
// std::cout << "schedule:" << enhanced_schedule << std::endl;
|
||||
// expecting two periods
|
||||
ASSERT_EQ(enhanced_schedule.chargingSchedulePeriod.size(), 2);
|
||||
EXPECT_EQ(enhanced_schedule.startSchedule, floor_seconds(start_time));
|
||||
EXPECT_EQ(enhanced_schedule.chargingSchedulePeriod[0].startPeriod, 0);
|
||||
EXPECT_EQ(enhanced_schedule.chargingSchedulePeriod[0].limit,
|
||||
profileStackA.chargingSchedule.chargingSchedulePeriod[0].limit);
|
||||
EXPECT_EQ(enhanced_schedule.chargingSchedulePeriod[1].startPeriod, 300);
|
||||
EXPECT_EQ(enhanced_schedule.chargingSchedulePeriod[1].limit,
|
||||
absoluteB.chargingSchedule.chargingSchedulePeriod[0].limit);
|
||||
}
|
||||
|
||||
TEST_F(ProfileTestsB, stack_relative) {
|
||||
std::int32_t connector = 1;
|
||||
add_connectors(1);
|
||||
ocpp::DateTime start_time(now);
|
||||
ocpp::DateTime end_time(now + minutes(10));
|
||||
connectors[1]->transaction =
|
||||
std::make_shared<Transaction>(-1, connector, "1234", "4567", 100, std::nullopt, start_time, nullptr);
|
||||
|
||||
auto relativeA = profileStackA;
|
||||
auto relativeB = profileStackB;
|
||||
relativeA.chargingProfileKind = ChargingProfileKindType::Relative;
|
||||
relativeA.chargingSchedule.startSchedule = std::nullopt;
|
||||
relativeB.chargingProfileKind = ChargingProfileKindType::Relative;
|
||||
relativeB.chargingSchedule.startSchedule = std::nullopt;
|
||||
|
||||
SmartChargingHandler handler(connectors, database_handler, *configuration);
|
||||
|
||||
handler.add_tx_default_profile(relativeA, 1);
|
||||
handler.add_tx_default_profile(relativeB, 1);
|
||||
|
||||
auto valid_profiles = handler.get_valid_profiles(start_time, end_time, 1);
|
||||
auto nShedules = valid_profiles.size();
|
||||
EXPECT_EQ(nShedules, 2);
|
||||
if (nShedules > 0) {
|
||||
EXPECT_EQ(relativeA.chargingSchedule, valid_profiles[0].chargingSchedule);
|
||||
}
|
||||
if (nShedules > 1) {
|
||||
EXPECT_EQ(relativeB.chargingSchedule, valid_profiles[1].chargingSchedule);
|
||||
}
|
||||
|
||||
auto schedule =
|
||||
handler.calculate_enhanced_composite_schedule(start_time, end_time, 1, ChargingRateUnit::A, false, true);
|
||||
// std::cout << "chargingSchedule:" << profileNoCharge.chargingSchedule << std::endl;
|
||||
// std::cout << "schedule:" << schedule.chargingSchedulePeriod << std::endl;
|
||||
EXPECT_EQ(relativeB.chargingSchedule, schedule);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,222 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "profile_tests_common.hpp"
|
||||
#include "ocpp/common/types.hpp"
|
||||
#include "ocpp/v16/ocpp_enums.hpp"
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// helper functions
|
||||
|
||||
namespace ocpp::v16 {
|
||||
using json = nlohmann::json;
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const std::vector<ChargingProfile>& profiles) {
|
||||
if (profiles.size() > 0) {
|
||||
std::uint32_t count = 0;
|
||||
for (const auto& i : profiles) {
|
||||
os << "[" << count++ << "] " << i;
|
||||
}
|
||||
} else {
|
||||
os << "<no profiles>";
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const std::vector<ChargingSchedulePeriod>& profiles) {
|
||||
if (profiles.size() > 0) {
|
||||
std::uint32_t count = 0;
|
||||
for (const auto& i : profiles) {
|
||||
json j;
|
||||
to_json(j, i);
|
||||
os << "[" << count++ << "] " << j << std::endl;
|
||||
}
|
||||
} else {
|
||||
os << "<no profiles>";
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const std::vector<EnhancedChargingSchedulePeriod>& profiles) {
|
||||
if (profiles.size() > 0) {
|
||||
std::uint32_t count = 0;
|
||||
for (const auto& i : profiles) {
|
||||
json j;
|
||||
to_json(j, i);
|
||||
os << "[" << count++ << "] " << j << std::endl;
|
||||
}
|
||||
} else {
|
||||
os << "<no profiles>";
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const EnhancedChargingSchedule& schedule) {
|
||||
json j;
|
||||
to_json(j, schedule);
|
||||
os << j;
|
||||
return os;
|
||||
}
|
||||
|
||||
bool operator==(const ChargingSchedulePeriod& a, const ChargingSchedulePeriod& b) {
|
||||
auto diff = std::abs(a.startPeriod - b.startPeriod);
|
||||
bool bRes = diff < 10; // allow for a small difference
|
||||
bRes = bRes && (a.limit == b.limit);
|
||||
bRes = bRes && optional_equal(a.numberPhases, b.numberPhases);
|
||||
return bRes;
|
||||
}
|
||||
|
||||
bool operator==(const ChargingSchedule& a, const ChargingSchedule& b) {
|
||||
bool bRes = true;
|
||||
auto min = std::min(a.chargingSchedulePeriod.size(), b.chargingSchedulePeriod.size());
|
||||
EXPECT_GT(min, 0);
|
||||
for (std::uint32_t i = 0; bRes && i < min; i++) {
|
||||
SCOPED_TRACE(std::string("i=") + std::to_string(i));
|
||||
bRes = bRes && a.chargingSchedulePeriod[i] == b.chargingSchedulePeriod[i];
|
||||
EXPECT_EQ(a.chargingSchedulePeriod[i], b.chargingSchedulePeriod[i]);
|
||||
}
|
||||
bRes = bRes && (a.chargingRateUnit == b.chargingRateUnit) && optional_equal(a.minChargingRate, b.minChargingRate);
|
||||
EXPECT_EQ(a.chargingRateUnit, b.chargingRateUnit);
|
||||
if (a.minChargingRate.has_value() && b.minChargingRate.has_value()) {
|
||||
EXPECT_EQ(a.minChargingRate.value(), b.minChargingRate.value());
|
||||
}
|
||||
bRes = bRes && optional_equal(a.startSchedule, b.startSchedule) && optional_equal(a.duration, b.duration);
|
||||
if (a.startSchedule.has_value() && b.startSchedule.has_value()) {
|
||||
EXPECT_EQ(floor_seconds(a.startSchedule.value()), floor_seconds(b.startSchedule.value()));
|
||||
}
|
||||
if (a.duration.has_value() && b.duration.has_value()) {
|
||||
EXPECT_EQ(a.duration.value(), b.duration.value());
|
||||
}
|
||||
return bRes;
|
||||
}
|
||||
|
||||
bool operator==(const ChargingSchedulePeriod& a, const EnhancedChargingSchedulePeriod& b) {
|
||||
auto diff = std::abs(a.startPeriod - b.startPeriod);
|
||||
bool bRes = diff < 10; // allow for a small difference
|
||||
bRes = bRes && (a.limit == b.limit);
|
||||
bRes = bRes && optional_equal(a.numberPhases, b.numberPhases);
|
||||
// b.stackLevel ignored
|
||||
return bRes;
|
||||
}
|
||||
|
||||
bool operator==(const EnhancedChargingSchedulePeriod& a, const EnhancedChargingSchedulePeriod& b) {
|
||||
bool bRes = a.startPeriod == b.startPeriod;
|
||||
bRes = bRes && (a.limit == b.limit);
|
||||
bRes = bRes && (a.stackLevel == b.stackLevel);
|
||||
bRes = bRes && (a.numberPhases.value_or(-1) == b.numberPhases.value_or(-1));
|
||||
return bRes;
|
||||
}
|
||||
|
||||
bool operator==(const ChargingSchedule& a, const EnhancedChargingSchedule& b) {
|
||||
bool bRes = true;
|
||||
auto min = std::min(a.chargingSchedulePeriod.size(), b.chargingSchedulePeriod.size());
|
||||
EXPECT_GT(min, 0);
|
||||
for (std::uint32_t i = 0; bRes && i < min; i++) {
|
||||
SCOPED_TRACE(std::string("i=") + std::to_string(i));
|
||||
bRes = bRes && a.chargingSchedulePeriod[i] == b.chargingSchedulePeriod[i];
|
||||
EXPECT_EQ(a.chargingSchedulePeriod[i], b.chargingSchedulePeriod[i]);
|
||||
}
|
||||
bRes = bRes && (a.chargingRateUnit == b.chargingRateUnit) && optional_equal(a.minChargingRate, b.minChargingRate);
|
||||
EXPECT_EQ(a.chargingRateUnit, b.chargingRateUnit);
|
||||
if (a.minChargingRate.has_value() && b.minChargingRate.has_value()) {
|
||||
EXPECT_EQ(a.minChargingRate.value(), b.minChargingRate.value());
|
||||
}
|
||||
bRes = bRes && optional_equal(a.startSchedule, b.startSchedule) && optional_equal(a.duration, b.duration);
|
||||
if (a.startSchedule.has_value() && b.startSchedule.has_value()) {
|
||||
EXPECT_EQ(floor_seconds(a.startSchedule.value()), floor_seconds(b.startSchedule.value()));
|
||||
}
|
||||
if (a.duration.has_value() && b.duration.has_value()) {
|
||||
EXPECT_EQ(a.duration.value(), b.duration.value());
|
||||
}
|
||||
return bRes;
|
||||
}
|
||||
|
||||
bool operator==(const EnhancedChargingSchedule& a, const EnhancedChargingSchedule& b) {
|
||||
const DateTime opt("1970-01-01T00:00:00Z");
|
||||
bool bRes = a.chargingSchedulePeriod.size() == b.chargingSchedulePeriod.size();
|
||||
bRes = bRes && (a.chargingRateUnit == b.chargingRateUnit);
|
||||
if (bRes) {
|
||||
for (std::uint8_t i = 0; i < a.chargingSchedulePeriod.size(); i++) {
|
||||
bRes = bRes && (a.chargingSchedulePeriod[i] == b.chargingSchedulePeriod[i]);
|
||||
}
|
||||
}
|
||||
bRes = bRes && (a.duration.value_or(-1) == b.duration.value_or(-1));
|
||||
bRes = bRes && (floor_seconds(a.startSchedule.value_or(opt)) == floor_seconds(b.startSchedule.value_or(opt)));
|
||||
bRes = bRes && (a.minChargingRate.value_or(-1.0) == b.minChargingRate.value_or(-1.0));
|
||||
return bRes;
|
||||
}
|
||||
|
||||
bool operator==(const ChargingProfile& a, const ChargingProfile& b) {
|
||||
bool bRes = (a.chargingProfileId == b.chargingProfileId) && (a.stackLevel == b.stackLevel) &&
|
||||
(a.chargingProfilePurpose == b.chargingProfilePurpose) &&
|
||||
(a.chargingProfileKind == b.chargingProfileKind) && (a.chargingSchedule == b.chargingSchedule);
|
||||
bRes = bRes && optional_equal(a.transactionId, b.transactionId) &&
|
||||
optional_equal(a.recurrencyKind, b.recurrencyKind) && optional_equal(a.validFrom, b.validFrom) &&
|
||||
optional_equal(a.validTo, b.validTo);
|
||||
return bRes;
|
||||
}
|
||||
|
||||
bool nearly_equal(const ocpp::DateTime& a, const ocpp::DateTime& b) {
|
||||
const auto difference = std::chrono::duration_cast<std::chrono::seconds>(a.to_time_point() - b.to_time_point());
|
||||
// allow +- 1 second to be considered equal
|
||||
const bool result = std::abs(difference.count()) <= 1;
|
||||
if (!result) {
|
||||
std::cerr << "nearly_equal (ocpp::DateTime)\n\tA: " << a << "\n\tB: " << b << std::endl;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
bool operator==(const period_entry_t& a, const period_entry_t& b) {
|
||||
bool bRes = (a.start == b.start) && (a.end == b.end) && (a.limit == b.limit) && (a.stack_level == b.stack_level) &&
|
||||
(a.charging_rate_unit == b.charging_rate_unit);
|
||||
if (a.number_phases && b.number_phases) {
|
||||
bRes = bRes && a.number_phases.value() == b.number_phases.value();
|
||||
}
|
||||
if (a.min_charging_rate && b.min_charging_rate) {
|
||||
bRes = bRes && a.min_charging_rate.value() == b.min_charging_rate.value();
|
||||
}
|
||||
return bRes;
|
||||
}
|
||||
|
||||
bool operator==(const std::vector<period_entry_t>& a, const std::vector<period_entry_t>& b) {
|
||||
bool bRes = a.size() == b.size();
|
||||
if (bRes) {
|
||||
for (std::uint8_t i = 0; i < a.size(); i++) {
|
||||
bRes = a[i] == b[i];
|
||||
if (!bRes) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
return bRes;
|
||||
}
|
||||
|
||||
bool validate_profile_result(const std::vector<period_entry_t>& result) {
|
||||
bool bRes{true};
|
||||
DateTime last{"1900-01-01T00:00:00Z"};
|
||||
for (const auto& i : result) {
|
||||
// ensure no overlaps
|
||||
bRes = i.start < i.end;
|
||||
bRes = bRes && i.start >= last;
|
||||
last = i.end;
|
||||
if (!bRes) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
return bRes;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const period_entry_t& entry) {
|
||||
os << entry.start << " " << entry.end << " S:" << entry.stack_level << " " << entry.limit
|
||||
<< ((entry.charging_rate_unit == ChargingRateUnit::A) ? "A" : "W");
|
||||
return os;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const std::vector<period_entry_t>& entries) {
|
||||
for (const auto& i : entries) {
|
||||
os << i << std::endl;
|
||||
}
|
||||
return os;
|
||||
}
|
||||
|
||||
} // namespace ocpp::v16
|
||||
@@ -0,0 +1,77 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef PROFILE_TESTS_COMMON_HPP
|
||||
#define PROFILE_TESTS_COMMON_HPP
|
||||
|
||||
#include <chrono>
|
||||
#include <iostream>
|
||||
#include <optional>
|
||||
#include <vector>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "ocpp/common/types.hpp"
|
||||
#include "ocpp/v16/ocpp_types.hpp"
|
||||
#include "ocpp/v16/profile.hpp"
|
||||
#include "ocpp/v16/types.hpp"
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
// helper functions
|
||||
namespace ocpp {
|
||||
inline bool operator==(const DateTime& a, const DateTime& b) {
|
||||
return a.to_time_point() == b.to_time_point();
|
||||
}
|
||||
|
||||
inline bool operator==(const DateTime& a, const std::string& b) {
|
||||
return a == DateTime(b);
|
||||
}
|
||||
|
||||
inline bool operator==(const DateTime& a, const char* b) {
|
||||
return a == DateTime(b);
|
||||
}
|
||||
|
||||
} // namespace ocpp
|
||||
|
||||
namespace ocpp::v16 {
|
||||
using json = nlohmann::json;
|
||||
|
||||
template <typename A> bool optional_equal(const std::optional<A>& a, const std::optional<A>& b) {
|
||||
bool bRes{true};
|
||||
if (a.has_value() && b.has_value()) {
|
||||
bRes = a.value() == b.value();
|
||||
}
|
||||
return bRes;
|
||||
}
|
||||
|
||||
inline bool optional_equal(const std::optional<DateTime>& a, const std::optional<DateTime>& b) {
|
||||
bool bRes{true};
|
||||
if (a.has_value() && b.has_value()) {
|
||||
bRes = floor_seconds(a.value()) == floor_seconds(b.value());
|
||||
}
|
||||
return bRes;
|
||||
}
|
||||
|
||||
std::ostream& operator<<(std::ostream& os, const std::vector<ChargingProfile>& profiles);
|
||||
std::ostream& operator<<(std::ostream& os, const std::vector<ChargingSchedulePeriod>& profiles);
|
||||
std::ostream& operator<<(std::ostream& os, const std::vector<EnhancedChargingSchedulePeriod>& profiles);
|
||||
std::ostream& operator<<(std::ostream& os, const EnhancedChargingSchedule& schedule);
|
||||
bool operator==(const ChargingSchedulePeriod& a, const ChargingSchedulePeriod& b);
|
||||
bool operator==(const ChargingSchedule& a, const ChargingSchedule& b);
|
||||
bool operator==(const ChargingSchedulePeriod& a, const EnhancedChargingSchedulePeriod& b);
|
||||
bool operator==(const EnhancedChargingSchedulePeriod& a, const EnhancedChargingSchedulePeriod& b);
|
||||
bool operator==(const ChargingSchedule& a, const EnhancedChargingSchedule& b);
|
||||
bool operator==(const EnhancedChargingSchedule& a, const EnhancedChargingSchedule& b);
|
||||
bool operator==(const ChargingProfile& a, const ChargingProfile& b);
|
||||
bool nearly_equal(const ocpp::DateTime& a, const ocpp::DateTime& b);
|
||||
|
||||
bool operator==(const period_entry_t& a, const period_entry_t& b);
|
||||
bool operator==(const std::vector<period_entry_t>& a, const std::vector<period_entry_t>& b);
|
||||
|
||||
bool validate_profile_result(const std::vector<period_entry_t>& result);
|
||||
std::ostream& operator<<(std::ostream& os, const period_entry_t& entry);
|
||||
std::ostream& operator<<(std::ostream& os, const std::vector<period_entry_t>& entries);
|
||||
|
||||
} // namespace ocpp::v16
|
||||
|
||||
#endif // PROFILE_TESTS_COMMON_HPP
|
||||
@@ -0,0 +1,138 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
#include <ocpp/v16/charge_point_state_machine.hpp>
|
||||
#include <ocpp/v16/ocpp_enums.hpp>
|
||||
#include <ocpp/v16/ocpp_types.hpp>
|
||||
|
||||
using namespace ocpp::v16;
|
||||
using ::testing::_;
|
||||
|
||||
class MockStatusNotificationCallback {
|
||||
public:
|
||||
MOCK_METHOD(void, Call,
|
||||
(FSMState state, ChargePointErrorCode error_code, ocpp::DateTime timestamp,
|
||||
std::optional<ocpp::CiString<50>> info, std::optional<ocpp::CiString<255>> vendor_id,
|
||||
std::optional<ocpp::CiString<50>> vendor_error_code));
|
||||
};
|
||||
|
||||
class ChargePointStateMachineTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
status_notification_callback = [&](FSMState state, ChargePointErrorCode error_code, ocpp::DateTime timestamp,
|
||||
std::optional<ocpp::CiString<50>> info,
|
||||
std::optional<ocpp::CiString<255>> vendor_id,
|
||||
std::optional<ocpp::CiString<50>> vendor_error_code) {
|
||||
mock_callback.Call(state, error_code, timestamp, info, vendor_id, vendor_error_code);
|
||||
};
|
||||
|
||||
state_machine = std::make_unique<ChargePointFSM>(status_notification_callback, FSMState::Available);
|
||||
}
|
||||
|
||||
std::unique_ptr<ChargePointFSM> state_machine;
|
||||
MockStatusNotificationCallback mock_callback;
|
||||
std::function<void(FSMState, ChargePointErrorCode, ocpp::DateTime, std::optional<ocpp::CiString<50>>,
|
||||
std::optional<ocpp::CiString<255>>, std::optional<ocpp::CiString<50>>)>
|
||||
status_notification_callback;
|
||||
};
|
||||
|
||||
TEST_F(ChargePointStateMachineTest, HandleError) {
|
||||
ErrorInfo error_info_1("uuid1", ChargePointErrorCode::ConnectorLockFailure, true);
|
||||
ErrorInfo error_info_2("uuid2", ChargePointErrorCode::GroundFailure, true);
|
||||
EXPECT_CALL(mock_callback, Call(FSMState::Faulted, ChargePointErrorCode::ConnectorLockFailure, _, _, _, _))
|
||||
.Times(1);
|
||||
EXPECT_CALL(mock_callback, Call(FSMState::Faulted, ChargePointErrorCode::GroundFailure, _, _, _, _)).Times(1);
|
||||
|
||||
state_machine->handle_error(error_info_1);
|
||||
state_machine->handle_error(error_info_2);
|
||||
}
|
||||
|
||||
TEST_F(ChargePointStateMachineTest, HandleError__ChangeState) {
|
||||
ErrorInfo error_info_1("uuid1", ChargePointErrorCode::GroundFailure, false, "InfoField", "vendor_id");
|
||||
EXPECT_CALL(mock_callback, Call(FSMState::Available, ChargePointErrorCode::GroundFailure, _, _, _, _)).Times(1);
|
||||
EXPECT_CALL(mock_callback, Call(FSMState::Preparing, ChargePointErrorCode::GroundFailure, _, _, _, _)).Times(1);
|
||||
|
||||
state_machine->handle_error(error_info_1);
|
||||
state_machine->handle_event(FSMEvent::UsageInitiated, ocpp::DateTime(), "AnotherInfoField");
|
||||
}
|
||||
|
||||
TEST_F(ChargePointStateMachineTest, HandleErrorCleared) {
|
||||
ErrorInfo error_info("uuid1", ChargePointErrorCode::ConnectorLockFailure, true);
|
||||
state_machine->handle_error(error_info);
|
||||
|
||||
EXPECT_CALL(mock_callback, Call(FSMState::Available, ChargePointErrorCode::NoError, _, _, _, _)).Times(1);
|
||||
|
||||
state_machine->handle_error_cleared("uuid1");
|
||||
}
|
||||
|
||||
TEST_F(ChargePointStateMachineTest, HandleErrorCleared__TwoErrors__OneCleared) {
|
||||
ErrorInfo error_info_1("uuid1", ChargePointErrorCode::ConnectorLockFailure, false);
|
||||
ErrorInfo error_info_2("uuid2", ChargePointErrorCode::GroundFailure, false);
|
||||
|
||||
EXPECT_CALL(mock_callback, Call(FSMState::Available, ChargePointErrorCode::ConnectorLockFailure, _, _, _, _))
|
||||
.Times(2);
|
||||
EXPECT_CALL(mock_callback, Call(FSMState::Available, ChargePointErrorCode::GroundFailure, _, _, _, _)).Times(1);
|
||||
EXPECT_CALL(mock_callback, Call(FSMState::Available, ChargePointErrorCode::NoError, _, _, _, _)).Times(0);
|
||||
|
||||
state_machine->handle_error(error_info_1);
|
||||
state_machine->handle_error(error_info_2);
|
||||
state_machine->handle_error_cleared("uuid2");
|
||||
|
||||
const auto latest_error = state_machine->get_latest_error();
|
||||
|
||||
EXPECT_TRUE(latest_error.has_value());
|
||||
EXPECT_EQ(latest_error.value().error_code, ChargePointErrorCode::ConnectorLockFailure);
|
||||
}
|
||||
|
||||
TEST_F(ChargePointStateMachineTest, HandleError__NonFault) {
|
||||
ErrorInfo error_info("uuid1", ChargePointErrorCode::ConnectorLockFailure, false);
|
||||
EXPECT_CALL(mock_callback, Call(FSMState::Available, ChargePointErrorCode::ConnectorLockFailure, _, _, _, _))
|
||||
.Times(1);
|
||||
|
||||
state_machine->handle_error(error_info);
|
||||
}
|
||||
|
||||
TEST_F(ChargePointStateMachineTest, HandleErrorCleared__NonFault) {
|
||||
ErrorInfo error_info("uuid1", ChargePointErrorCode::ConnectorLockFailure, false);
|
||||
|
||||
state_machine->handle_error(error_info);
|
||||
|
||||
EXPECT_CALL(mock_callback, Call(FSMState::Available, ChargePointErrorCode::NoError, _, _, _, _)).Times(1);
|
||||
|
||||
state_machine->handle_error_cleared("uuid1");
|
||||
}
|
||||
|
||||
TEST_F(ChargePointStateMachineTest, HandleErrorCleared__ClearUnknown) {
|
||||
ErrorInfo error_info("uuid1", ChargePointErrorCode::ConnectorLockFailure, false);
|
||||
|
||||
state_machine->handle_error(error_info);
|
||||
|
||||
EXPECT_CALL(mock_callback, Call(FSMState::Available, ChargePointErrorCode::NoError, _, _, _, _)).Times(0);
|
||||
|
||||
state_machine->handle_error_cleared("uuid2");
|
||||
state_machine->handle_error_cleared("uuid3");
|
||||
state_machine->handle_error_cleared("uuid4");
|
||||
}
|
||||
|
||||
TEST_F(ChargePointStateMachineTest, HandleErrorCleared__NonFault__StillActive) {
|
||||
ErrorInfo error_info_1("uuid1", ChargePointErrorCode::ConnectorLockFailure, false);
|
||||
ErrorInfo error_info_2("uuid2", ChargePointErrorCode::GroundFailure, true);
|
||||
|
||||
EXPECT_CALL(mock_callback, Call(FSMState::Available, ChargePointErrorCode::ConnectorLockFailure, _, _, _, _))
|
||||
.Times(2);
|
||||
EXPECT_CALL(mock_callback, Call(FSMState::Faulted, ChargePointErrorCode::GroundFailure, _, _, _, _)).Times(1);
|
||||
EXPECT_CALL(mock_callback, Call(FSMState::Available, ChargePointErrorCode::NoError, _, _, _, _)).Times(1);
|
||||
|
||||
state_machine->handle_error(error_info_1);
|
||||
state_machine->handle_error(error_info_2);
|
||||
state_machine->handle_error_cleared("uuid2");
|
||||
|
||||
const auto latest_error = state_machine->get_latest_error();
|
||||
|
||||
EXPECT_TRUE(latest_error.has_value());
|
||||
EXPECT_EQ(latest_error.value().error_code, ChargePointErrorCode::ConnectorLockFailure);
|
||||
|
||||
state_machine->handle_error_cleared("uuid1");
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,126 @@
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <everest/logging.hpp>
|
||||
#include <ocpp/common/schemas.hpp>
|
||||
|
||||
#include <memory>
|
||||
#include <nlohmann/json-schema.hpp>
|
||||
#include <nlohmann/json.hpp>
|
||||
|
||||
namespace {
|
||||
using nlohmann::basic_json;
|
||||
using nlohmann::json;
|
||||
using nlohmann::json_uri;
|
||||
using nlohmann::json_schema::basic_error_handler;
|
||||
using nlohmann::json_schema::json_validator;
|
||||
|
||||
constexpr const char* test_schema = R"({
|
||||
"$schema": "http://json-schema.org/draft-07/schema#",
|
||||
"description": "Json schema for Custom configuration keys",
|
||||
"$comment": "This is just an example schema and can be modified according to custom requirements",
|
||||
"type": "object",
|
||||
"required": [],
|
||||
"properties": {
|
||||
"ConnectorType": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"cType2",
|
||||
"sType2"
|
||||
],
|
||||
"default": "sType2",
|
||||
"description": "Used to indicate the type of connector used by the unit",
|
||||
"readOnly": true
|
||||
},
|
||||
"ConfigLastUpdatedBy": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "string",
|
||||
"enum": [
|
||||
"LOCAL",
|
||||
"CPMS"
|
||||
]
|
||||
},
|
||||
"description": "Variable used to indicate how the Charge Points configuration was last updated",
|
||||
"readOnly": true
|
||||
}
|
||||
}
|
||||
})";
|
||||
|
||||
class SchemaTest : public testing::Test {
|
||||
static void format_checker(const std::string& format, const std::string& value) {
|
||||
EVLOG_error << "format_checker: '" << format << "' '" << value << '\'';
|
||||
}
|
||||
|
||||
static void loader(const json_uri& uri, json& schema) {
|
||||
schema = nlohmann::json_schema::draft7_schema_builtin;
|
||||
}
|
||||
|
||||
class custom_error_handler : public basic_error_handler {
|
||||
private:
|
||||
void error(const json::json_pointer& pointer, const json& instance, const std::string& message) override {
|
||||
basic_error_handler::error(pointer, instance, message);
|
||||
EVLOG_error << "'" << pointer << "' - '" << instance << "': " << message;
|
||||
errors = true;
|
||||
}
|
||||
|
||||
public:
|
||||
bool errors{false};
|
||||
constexpr bool has_errors() const {
|
||||
return errors;
|
||||
}
|
||||
};
|
||||
|
||||
protected:
|
||||
std::unique_ptr<json_validator> validator;
|
||||
json schema;
|
||||
custom_error_handler err;
|
||||
|
||||
void SetUp() override {
|
||||
schema = json::parse(test_schema);
|
||||
validator = std::make_unique<json_validator>(&loader, &format_checker);
|
||||
validator->set_root_schema(schema);
|
||||
err.errors = false;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(SchemaTest, ValidationText) {
|
||||
json model = R"({"ConnectorType":"cType2"})"_json;
|
||||
validator->validate(model, err);
|
||||
EXPECT_FALSE(err.has_errors());
|
||||
}
|
||||
|
||||
TEST_F(SchemaTest, ValidationObj) {
|
||||
json model;
|
||||
model["ConnectorType"] = "cType2";
|
||||
validator->validate(model, err);
|
||||
EXPECT_FALSE(err.has_errors());
|
||||
}
|
||||
|
||||
TEST_F(SchemaTest, ValidationObjErr) {
|
||||
json model;
|
||||
model["ConnectorType"] = "cType3";
|
||||
validator->validate(model, err);
|
||||
EXPECT_TRUE(err.has_errors());
|
||||
}
|
||||
|
||||
TEST(SchemaObj, Success) {
|
||||
ocpp::Schemas schema(std::move(json::parse(test_schema)));
|
||||
auto validator = schema.get_validator();
|
||||
json model;
|
||||
model["ConnectorType"] = "cType2";
|
||||
EXPECT_NO_THROW(validator->validate(model));
|
||||
}
|
||||
|
||||
TEST(SchemaObj, Fail) {
|
||||
ocpp::Schemas schema(std::move(json::parse(test_schema)));
|
||||
auto validator = schema.get_validator();
|
||||
json model;
|
||||
model["ConnectorType"] = "cType3";
|
||||
EXPECT_ANY_THROW(validator->validate(model));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,184 @@
|
||||
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "ocpp/v16/charge_point_configuration_interface.hpp"
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <ocpp/common/schemas.hpp>
|
||||
#include <ocpp/v16/charge_point_configuration.hpp>
|
||||
|
||||
namespace {
|
||||
using namespace ocpp::v16;
|
||||
|
||||
struct ConfigurationTester : public testing::Test {
|
||||
std::unique_ptr<ChargePointConfigurationInterface> config;
|
||||
|
||||
void SetUp() override {
|
||||
fs::path cfg{CONFIG_DIR_V16};
|
||||
cfg /= "config-full.json";
|
||||
std::ifstream ifs(cfg);
|
||||
const std::string config_file((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>()));
|
||||
config = std::make_unique<ChargePointConfiguration>(config_file, CONFIG_DIR_V16, USER_CONFIG_FILE_LOCATION_V16);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(ConfigurationTester, SetUnknown) {
|
||||
auto get_result = config->get("HeartBeatInterval");
|
||||
EXPECT_TRUE(get_result.has_value());
|
||||
get_result = config->get("DoesNotExist");
|
||||
EXPECT_FALSE(get_result.has_value());
|
||||
|
||||
auto set_result = config->set("HeartBeatInterval", "352");
|
||||
EXPECT_EQ(set_result, ConfigurationStatus::Accepted);
|
||||
set_result = config->set("DoesNotExist", "never-set");
|
||||
EXPECT_FALSE(set_result.has_value()); // std::nullopt indicates key not known
|
||||
}
|
||||
|
||||
TEST_F(ConfigurationTester, BrokenChain) {
|
||||
// set() has a chain of if .. else if ..
|
||||
// test that there isn't a missing else
|
||||
// IgnoredProfilePurposesOffline is the fist key
|
||||
|
||||
// actually returns rejected rather than accepted
|
||||
// this is fine since the error case would be std::nullopt
|
||||
auto set_result = config->set("IgnoredProfilePurposesOffline", "TxProfile");
|
||||
EXPECT_TRUE(set_result.has_value());
|
||||
}
|
||||
|
||||
TEST(PartialSchemaValidator, CostAndPrice) {
|
||||
fs::path schema_file = CONFIG_DIR_V16;
|
||||
schema_file /= "profile_schemas/CostAndPrice.json";
|
||||
std::ifstream ifs(schema_file);
|
||||
auto schema_json = json::parse(ifs);
|
||||
ocpp::Schemas schema(schema_json);
|
||||
|
||||
const char* valid_str = R"({"priceText":"1"})";
|
||||
const char* invalid_str = R"({"priceText":null,"priceTextOffline":null,"chargingPrice":null})";
|
||||
|
||||
auto valid_json = json::parse(valid_str);
|
||||
auto invalid_json = json::parse(invalid_str);
|
||||
|
||||
auto validator = schema.get_validator();
|
||||
|
||||
json to_test;
|
||||
to_test["CustomDisplayCostAndPrice"] = false;
|
||||
to_test["DefaultPrice"] = valid_json;
|
||||
EXPECT_NO_THROW(validator->validate(to_test));
|
||||
to_test["DefaultPrice"] = invalid_json;
|
||||
EXPECT_ANY_THROW(validator->validate(to_test));
|
||||
}
|
||||
|
||||
TEST_F(ConfigurationTester, BadPriceText) {
|
||||
// PriceText value is JSON encoded - check that malformed and invalid
|
||||
// messages are correctly handled
|
||||
const char* valid = R"({"priceText":"default"})";
|
||||
const char* invalid = R"({"priceText":null,"priceTextOffline":null,"chargingPrice":null})";
|
||||
|
||||
auto set_result = config->set("DefaultPrice", valid);
|
||||
EXPECT_TRUE(set_result.has_value());
|
||||
EXPECT_EQ(set_result.value(), ConfigurationStatus::Accepted);
|
||||
|
||||
auto get_result = config->getDefaultPrice();
|
||||
ASSERT_TRUE(get_result.has_value());
|
||||
auto get_json = json::parse(get_result.value());
|
||||
EXPECT_EQ(get_json["priceText"], "default");
|
||||
|
||||
set_result = config->set("DefaultPrice", invalid);
|
||||
EXPECT_TRUE(set_result.has_value());
|
||||
EXPECT_EQ(set_result.value(), ConfigurationStatus::Rejected);
|
||||
|
||||
auto get_result2 = config->getDefaultPrice();
|
||||
ASSERT_TRUE(get_result2.has_value());
|
||||
EXPECT_EQ(get_result2, get_result);
|
||||
get_json = json::parse(get_result.value());
|
||||
EXPECT_EQ(get_json["priceText"], "default");
|
||||
|
||||
auto set_result2 = config->setDefaultPrice(invalid);
|
||||
EXPECT_EQ(set_result2, ConfigurationStatus::Rejected);
|
||||
|
||||
get_result2 = config->getDefaultPrice();
|
||||
ASSERT_TRUE(get_result2.has_value());
|
||||
EXPECT_EQ(get_result2, get_result);
|
||||
get_json = json::parse(get_result.value());
|
||||
EXPECT_EQ(get_json["priceText"], "default");
|
||||
}
|
||||
|
||||
TEST_F(ConfigurationTester, DefaultPriceTextEmptyArray) {
|
||||
const char* empty = R"([])";
|
||||
auto set_result = config->set("DefaultPriceText,en", empty);
|
||||
EXPECT_EQ(set_result, ConfigurationStatus::Rejected);
|
||||
set_result = config->setDefaultPriceText("DefaultPriceText,en", empty);
|
||||
EXPECT_EQ(set_result, ConfigurationStatus::Rejected);
|
||||
}
|
||||
|
||||
TEST_F(ConfigurationTester, DefaultPriceTextEmptyObject) {
|
||||
const char* empty = R"({})";
|
||||
auto set_result = config->set("DefaultPriceText,en", empty);
|
||||
EXPECT_EQ(set_result, ConfigurationStatus::Rejected);
|
||||
set_result = config->setDefaultPriceText("DefaultPriceText,en", empty);
|
||||
EXPECT_EQ(set_result, ConfigurationStatus::Rejected);
|
||||
}
|
||||
|
||||
TEST_F(ConfigurationTester, DefaultPriceInvalid) {
|
||||
const char* minimal = R"("priceText":[])";
|
||||
|
||||
auto set_result = config->set("DefaultPriceText,en", minimal);
|
||||
EXPECT_EQ(set_result, ConfigurationStatus::Rejected);
|
||||
set_result = config->setDefaultPriceText("DefaultPriceText,en", minimal);
|
||||
EXPECT_EQ(set_result, ConfigurationStatus::Rejected);
|
||||
}
|
||||
|
||||
TEST_F(ConfigurationTester, DefaultPriceTextMinimal) {
|
||||
const char* minimal = R"({"priceText":"Default"})";
|
||||
|
||||
auto set_result = config->set("DefaultPriceText,en", minimal);
|
||||
EXPECT_EQ(set_result, ConfigurationStatus::Accepted);
|
||||
set_result = config->setDefaultPriceText("DefaultPriceText,en", minimal);
|
||||
EXPECT_EQ(set_result, ConfigurationStatus::Accepted);
|
||||
}
|
||||
|
||||
TEST_F(ConfigurationTester, DefaultPriceText) {
|
||||
const char* minimal = R"({"priceText":"Default","priceTextOffline":"Offline"})";
|
||||
|
||||
auto set_result = config->set("DefaultPriceText,en", minimal);
|
||||
EXPECT_EQ(set_result, ConfigurationStatus::Accepted);
|
||||
set_result = config->setDefaultPriceText("DefaultPriceText,en", minimal);
|
||||
EXPECT_EQ(set_result, ConfigurationStatus::Accepted);
|
||||
}
|
||||
|
||||
// Boolean ChangeConfiguration values must be validated rather than silently
|
||||
// coerced to false. See EVerest/EVerest#2182.
|
||||
TEST_F(ConfigurationTester, BooleanKeyAcceptsValidLiterals) {
|
||||
const std::vector<std::string> accepted_values = {"true", "false", "True", "FALSE", "tRuE"};
|
||||
for (const auto& v : accepted_values) {
|
||||
auto result = config->set("AuthorizeRemoteTxRequests", v);
|
||||
ASSERT_TRUE(result.has_value()) << "value=" << v;
|
||||
EXPECT_EQ(result.value(), ConfigurationStatus::Accepted) << "value=" << v;
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(ConfigurationTester, BooleanKeyRejectsInvalidLiterals) {
|
||||
auto initial = config->get("AuthorizeRemoteTxRequests");
|
||||
ASSERT_TRUE(initial.has_value());
|
||||
ASSERT_TRUE(initial.value().value.has_value());
|
||||
const auto initial_value = initial.value().value.value();
|
||||
|
||||
const std::vector<std::string> rejected_values = {"maybe", "", "1", "0", "yes", "no", "tru", "falsey"};
|
||||
for (const auto& v : rejected_values) {
|
||||
auto result = config->set("AuthorizeRemoteTxRequests", v);
|
||||
ASSERT_TRUE(result.has_value()) << "value=" << v;
|
||||
EXPECT_EQ(result.value(), ConfigurationStatus::Rejected) << "value=" << v;
|
||||
|
||||
auto current = config->get("AuthorizeRemoteTxRequests");
|
||||
ASSERT_TRUE(current.has_value());
|
||||
ASSERT_TRUE(current.value().value.has_value()) << "value=" << v;
|
||||
EXPECT_EQ(current.value().value.value(), initial_value) << "value=" << v;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,116 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <lib/ocpp/common/test_database_migration_files.hpp>
|
||||
|
||||
// Apply generic test cases to v16 migrations
|
||||
INSTANTIATE_TEST_SUITE_P(V16, DatabaseMigrationFilesTest,
|
||||
::testing::Values(std::make_tuple(std::filesystem::path(MIGRATION_FILES_LOCATION_V16),
|
||||
MIGRATION_FILE_VERSION_V16)));
|
||||
|
||||
// Apply v16 specific test cases to migrations
|
||||
using DatabaseMigrationFilesTestV16 = DatabaseMigrationFilesTest;
|
||||
|
||||
INSTANTIATE_TEST_SUITE_P(V16, DatabaseMigrationFilesTestV16,
|
||||
::testing::Values(std::make_tuple(std::filesystem::path(MIGRATION_FILES_LOCATION_V16),
|
||||
MIGRATION_FILE_VERSION_V16)));
|
||||
|
||||
TEST_P(DatabaseMigrationFilesTestV16, V16_MigrationFile2) {
|
||||
everest::db::sqlite::SchemaUpdater updater{this->database.get()};
|
||||
|
||||
EXPECT_TRUE(updater.apply_migration_files(this->migration_files_path, 1));
|
||||
this->ExpectUserVersion(1);
|
||||
|
||||
// Transaction table should not contain these columns yet
|
||||
EXPECT_FALSE(this->DoesColumnExist("TRANSACTIONS", "START_TRANSACTION_MESSAGE_ID"));
|
||||
EXPECT_FALSE(this->DoesColumnExist("TRANSACTIONS", "STOP_TRANSACTION_MESSAGE_ID"));
|
||||
|
||||
// We expect to be able to insert into CONNECTORS and TRANSACTIONS table.
|
||||
EXPECT_TRUE(this->database->execute_statement("INSERT INTO CONNECTORS (ID, AVAILABILITY) VALUES (1, \"\")"));
|
||||
|
||||
std::string sql =
|
||||
"INSERT INTO TRANSACTIONS "
|
||||
"(ID, CONNECTOR, ID_TAG_START, TIME_START, METER_START, CSMS_ACK, METER_LAST, METER_LAST_TIME, LAST_UPDATE)"
|
||||
" VALUES "
|
||||
"(55, 1, \"\", \"\", 1, 0, 0, \"\", \"\")";
|
||||
EXPECT_TRUE(this->database->execute_statement(sql));
|
||||
|
||||
// We added a row with CSMS_ACK=0 so we should not find anything
|
||||
auto stmt = this->database->new_statement("SELECT ID FROM TRANSACTIONS WHERE CSMS_ACK=1;");
|
||||
EXPECT_EQ(stmt->step(), SQLITE_DONE);
|
||||
|
||||
// After applying the migration we expect to be at version 2
|
||||
EXPECT_TRUE(updater.apply_migration_files(this->migration_files_path, 2));
|
||||
this->ExpectUserVersion(2);
|
||||
|
||||
// We expect the added columns to exists
|
||||
EXPECT_TRUE(this->DoesColumnExist("TRANSACTIONS", "START_TRANSACTION_MESSAGE_ID"));
|
||||
EXPECT_TRUE(this->DoesColumnExist("TRANSACTIONS", "STOP_TRANSACTION_MESSAGE_ID"));
|
||||
|
||||
// We should be able to update the transaction we inserted earlier
|
||||
EXPECT_TRUE(this->database->execute_statement("UPDATE TRANSACTIONS SET METER_LAST=2 WHERE ID=55"));
|
||||
// We should be able to update the newly introduced field too
|
||||
EXPECT_TRUE(this->database->execute_statement(
|
||||
"UPDATE TRANSACTIONS SET START_TRANSACTION_MESSAGE_ID=\"test2\" WHERE ID=55"));
|
||||
|
||||
// The migration should have set all rows to CSMS_ACK=1 so we should find 1 row with ID=55 here
|
||||
stmt = this->database->new_statement("SELECT ID FROM TRANSACTIONS WHERE CSMS_ACK=1;");
|
||||
EXPECT_EQ(stmt->step(), SQLITE_ROW);
|
||||
EXPECT_EQ(stmt->column_int(0), 55);
|
||||
EXPECT_EQ(stmt->step(), SQLITE_DONE);
|
||||
|
||||
// After applying the down migration we expect to be at version 1
|
||||
EXPECT_TRUE(updater.apply_migration_files(this->migration_files_path, 1));
|
||||
this->ExpectUserVersion(1);
|
||||
|
||||
// We expect the added columns to no longer exists
|
||||
EXPECT_FALSE(this->DoesColumnExist("TRANSACTIONS", "START_TRANSACTION_MESSAGE_ID"));
|
||||
EXPECT_FALSE(this->DoesColumnExist("TRANSACTIONS", "STOP_TRANSACTION_MESSAGE_ID"));
|
||||
|
||||
// We should still be able to update the transaction we inserted earlier
|
||||
EXPECT_TRUE(this->database->execute_statement("UPDATE TRANSACTIONS SET METER_LAST=2 WHERE ID=55"));
|
||||
// We should not be able to update the field from version 2 any longer
|
||||
EXPECT_FALSE(this->database->execute_statement(
|
||||
"UPDATE TRANSACTIONS SET START_TRANSACTION_MESSAGE_ID=\"test2\" WHERE ID=55"));
|
||||
|
||||
// The down migration should not have touched CSMS_ACK=1 so we should still find 1 row with ID=55 here
|
||||
stmt = this->database->new_statement("SELECT ID FROM TRANSACTIONS WHERE CSMS_ACK=1;");
|
||||
EXPECT_EQ(stmt->step(), SQLITE_ROW);
|
||||
EXPECT_EQ(stmt->column_int(0), 55);
|
||||
EXPECT_EQ(stmt->step(), SQLITE_DONE);
|
||||
}
|
||||
|
||||
TEST_P(DatabaseMigrationFilesTestV16, V16_MigrationFile4_OCSPRequest) {
|
||||
everest::db::sqlite::SchemaUpdater updater{this->database.get()};
|
||||
|
||||
// Migrate up to version 3
|
||||
EXPECT_TRUE(updater.apply_migration_files(this->migration_files_path, 3));
|
||||
this->ExpectUserVersion(3);
|
||||
|
||||
// OCSP_REQUEST table should exist at this version
|
||||
EXPECT_TRUE(this->DoesTableExist("OCSP_REQUEST"));
|
||||
|
||||
// Migrate to version 4 (OCSP_REQUEST table should be dropped)
|
||||
EXPECT_TRUE(updater.apply_migration_files(this->migration_files_path, 4));
|
||||
this->ExpectUserVersion(4);
|
||||
|
||||
// The table should be gone
|
||||
EXPECT_FALSE(this->DoesTableExist("OCSP_REQUEST"));
|
||||
|
||||
// Now roll back to version 3
|
||||
EXPECT_TRUE(updater.apply_migration_files(this->migration_files_path, 3));
|
||||
this->ExpectUserVersion(3);
|
||||
|
||||
// OCSP_REQUEST table should be recreated
|
||||
EXPECT_TRUE(this->DoesTableExist("OCSP_REQUEST"));
|
||||
|
||||
// Optional: try to insert a row to verify it's functional
|
||||
EXPECT_TRUE(
|
||||
this->database->execute_statement("INSERT INTO OCSP_REQUEST (LAST_UPDATE) VALUES (\"2025-06-05T12:00:00Z\")"));
|
||||
|
||||
// Select to verify insert worked
|
||||
auto stmt = this->database->new_statement("SELECT LAST_UPDATE FROM OCSP_REQUEST");
|
||||
EXPECT_EQ(stmt->step(), SQLITE_ROW);
|
||||
EXPECT_EQ(stmt->column_text(0), "2025-06-05T12:00:00Z");
|
||||
EXPECT_EQ(stmt->step(), SQLITE_DONE);
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2024 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <ocpp/common/message_queue.hpp>
|
||||
#include <ocpp/v16/messages/Authorize.hpp>
|
||||
#include <ocpp/v16/messages/MeterValues.hpp>
|
||||
#include <ocpp/v16/messages/SecurityEventNotification.hpp>
|
||||
#include <ocpp/v16/messages/StartTransaction.hpp>
|
||||
|
||||
namespace ocpp {
|
||||
|
||||
namespace v16 {
|
||||
|
||||
/************************************************************************************************
|
||||
* ControlMessage
|
||||
*
|
||||
* Test implementations of ControlMessage template
|
||||
*/
|
||||
class ControlMessageV16Test : public ::testing::Test {
|
||||
|
||||
protected:
|
||||
};
|
||||
|
||||
TEST_F(ControlMessageV16Test, test_is_transactional) {
|
||||
|
||||
EXPECT_TRUE(is_transaction_message((ControlMessage<v16::MessageType>{
|
||||
Call<v16::StartTransactionRequest>{
|
||||
v16::StartTransactionRequest{}}}.messageType)));
|
||||
EXPECT_TRUE(is_transaction_message((ControlMessage<v16::MessageType>{
|
||||
Call<v16::StopTransactionRequest>{
|
||||
v16::StopTransactionRequest{}}}.messageType)));
|
||||
EXPECT_TRUE(is_transaction_message(ControlMessage<v16::MessageType>{
|
||||
Call<v16::SecurityEventNotificationRequest>{v16::SecurityEventNotificationRequest{}}}
|
||||
.messageType));
|
||||
EXPECT_TRUE(is_transaction_message(
|
||||
ControlMessage<v16::MessageType>{Call<v16::MeterValuesRequest>{v16::MeterValuesRequest{}}}.messageType));
|
||||
EXPECT_TRUE(!is_transaction_message(
|
||||
ControlMessage<v16::MessageType>{Call<v16::AuthorizeRequest>{v16::AuthorizeRequest{}}}.messageType));
|
||||
}
|
||||
|
||||
TEST_F(ControlMessageV16Test, test_is_transactional_update) {
|
||||
|
||||
EXPECT_TRUE(!(ControlMessage<v16::MessageType>{Call<v16::StartTransactionRequest>{v16::StartTransactionRequest{}}})
|
||||
.is_transaction_update_message());
|
||||
EXPECT_TRUE(!(ControlMessage<v16::MessageType>{Call<v16::StopTransactionRequest>{v16::StopTransactionRequest{}}})
|
||||
.is_transaction_update_message());
|
||||
EXPECT_TRUE(!(ControlMessage<v16::MessageType>{
|
||||
Call<v16::SecurityEventNotificationRequest>{v16::SecurityEventNotificationRequest{}}})
|
||||
.is_transaction_update_message());
|
||||
EXPECT_TRUE((ControlMessage<v16::MessageType>{Call<v16::MeterValuesRequest>{v16::MeterValuesRequest{}}})
|
||||
.is_transaction_update_message());
|
||||
|
||||
EXPECT_TRUE(!(ControlMessage<v16::MessageType>{Call<v16::AuthorizeRequest>{v16::AuthorizeRequest{}}})
|
||||
.is_transaction_update_message());
|
||||
}
|
||||
|
||||
} // namespace v16
|
||||
} // namespace ocpp
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,305 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <ocpp/common/utils.hpp>
|
||||
#include <ocpp/v16/utils.hpp>
|
||||
|
||||
namespace {
|
||||
using namespace ocpp::v16::utils;
|
||||
|
||||
// reverse the order of the arguments
|
||||
inline auto common_split_string(char separator, const std::string& csl) {
|
||||
return ocpp::split_string(csl, separator);
|
||||
}
|
||||
|
||||
TEST(CSL, ToCSL) {
|
||||
auto res = to_csl({});
|
||||
EXPECT_TRUE(res.empty());
|
||||
|
||||
res = to_csl({"One"});
|
||||
EXPECT_FALSE(res.empty());
|
||||
EXPECT_EQ(res, "One");
|
||||
|
||||
res = to_csl({"One", "Two"});
|
||||
EXPECT_FALSE(res.empty());
|
||||
EXPECT_EQ(res, "One,Two");
|
||||
|
||||
res = to_csl({"", "Two"});
|
||||
EXPECT_FALSE(res.empty());
|
||||
EXPECT_EQ(res, ",Two");
|
||||
|
||||
res = to_csl({"One", ""});
|
||||
EXPECT_FALSE(res.empty());
|
||||
EXPECT_EQ(res, "One,");
|
||||
|
||||
res = to_csl({"One", "Two", "Three"});
|
||||
EXPECT_FALSE(res.empty());
|
||||
EXPECT_EQ(res, "One,Two,Three");
|
||||
|
||||
res = to_csl({"", "", ""});
|
||||
EXPECT_FALSE(res.empty());
|
||||
EXPECT_EQ(res, ",,");
|
||||
|
||||
res = to_csl({"One", "", "Three"});
|
||||
EXPECT_FALSE(res.empty());
|
||||
EXPECT_EQ(res, "One,,Three");
|
||||
|
||||
res = to_csl({"", "Two", "Three"});
|
||||
EXPECT_FALSE(res.empty());
|
||||
EXPECT_EQ(res, ",Two,Three");
|
||||
|
||||
res = to_csl({"One", "Two", ""});
|
||||
EXPECT_FALSE(res.empty());
|
||||
EXPECT_EQ(res, "One,Two,");
|
||||
|
||||
res = to_csl({"", "", "Three"});
|
||||
EXPECT_FALSE(res.empty());
|
||||
EXPECT_EQ(res, ",,Three");
|
||||
|
||||
res = to_csl({"One", "", ""});
|
||||
EXPECT_FALSE(res.empty());
|
||||
EXPECT_EQ(res, "One,,");
|
||||
|
||||
// TODO(james-ctc): should this be detected?
|
||||
res = to_csl({"One", "Two", "Three,Four"});
|
||||
EXPECT_FALSE(res.empty());
|
||||
EXPECT_EQ(res, "One,Two,Three,Four");
|
||||
}
|
||||
|
||||
TEST(CSL, FromCSL) {
|
||||
auto res = from_csl("");
|
||||
EXPECT_TRUE(res.empty());
|
||||
|
||||
res = from_csl(",");
|
||||
EXPECT_TRUE(res.empty());
|
||||
|
||||
res = from_csl(",One");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 1);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
|
||||
res = from_csl(",One,");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 1);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
|
||||
res = from_csl(",One,Two");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 2);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
EXPECT_EQ(res[1], "Two");
|
||||
|
||||
res = from_csl(",One,Two,");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 2);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
EXPECT_EQ(res[1], "Two");
|
||||
|
||||
res = from_csl("One");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 1);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
|
||||
res = from_csl("One,");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 1);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
|
||||
res = from_csl("One,Two");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 2);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
EXPECT_EQ(res[1], "Two");
|
||||
|
||||
res = from_csl("One,Two,");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 2);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
EXPECT_EQ(res[1], "Two");
|
||||
|
||||
res = from_csl("One,,Two");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 2);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
EXPECT_EQ(res[1], "Two");
|
||||
|
||||
res = from_csl("One,,,Two");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 2);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
EXPECT_EQ(res[1], "Two");
|
||||
}
|
||||
|
||||
TEST(CSL, SplitString) {
|
||||
auto res = split_string(',', "");
|
||||
EXPECT_TRUE(res.empty());
|
||||
|
||||
res = split_string(',', ",");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 2);
|
||||
EXPECT_EQ(res[0], "");
|
||||
EXPECT_EQ(res[1], "");
|
||||
|
||||
res = split_string(',', ",One");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 2);
|
||||
EXPECT_EQ(res[0], "");
|
||||
EXPECT_EQ(res[1], "One");
|
||||
|
||||
res = split_string(',', ",One,");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 3);
|
||||
EXPECT_EQ(res[0], "");
|
||||
EXPECT_EQ(res[1], "One");
|
||||
EXPECT_EQ(res[2], "");
|
||||
|
||||
res = split_string(',', ",One,Two");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 3);
|
||||
EXPECT_EQ(res[0], "");
|
||||
EXPECT_EQ(res[1], "One");
|
||||
EXPECT_EQ(res[2], "Two");
|
||||
|
||||
res = split_string(',', ",One,Two,");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 4);
|
||||
EXPECT_EQ(res[0], "");
|
||||
EXPECT_EQ(res[1], "One");
|
||||
EXPECT_EQ(res[2], "Two");
|
||||
EXPECT_EQ(res[3], "");
|
||||
|
||||
res = split_string(',', "One");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 1);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
|
||||
res = split_string(',', "One,");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 2);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
EXPECT_EQ(res[1], "");
|
||||
|
||||
res = split_string(',', "One,Two");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 2);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
EXPECT_EQ(res[1], "Two");
|
||||
|
||||
res = split_string(',', "One,Two,");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 3);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
EXPECT_EQ(res[1], "Two");
|
||||
EXPECT_EQ(res[2], "");
|
||||
|
||||
res = split_string(',', "One,,Two");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 3);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
EXPECT_EQ(res[1], "");
|
||||
EXPECT_EQ(res[2], "Two");
|
||||
|
||||
res = split_string(',', "One,,,Two");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 4);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
EXPECT_EQ(res[1], "");
|
||||
EXPECT_EQ(res[2], "");
|
||||
EXPECT_EQ(res[3], "Two");
|
||||
}
|
||||
|
||||
TEST(CSL, SplitStringCommon) {
|
||||
// gives different results to v16::split_string
|
||||
|
||||
auto res = common_split_string(',', "");
|
||||
EXPECT_TRUE(res.empty());
|
||||
|
||||
res = common_split_string(',', ",");
|
||||
EXPECT_FALSE(res.empty());
|
||||
// ASSERT_EQ(res.size(), 2);
|
||||
ASSERT_EQ(res.size(), 1);
|
||||
EXPECT_EQ(res[0], "");
|
||||
|
||||
res = common_split_string(',', ",One");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 2);
|
||||
EXPECT_EQ(res[0], "");
|
||||
EXPECT_EQ(res[1], "One");
|
||||
|
||||
res = common_split_string(',', ",One,");
|
||||
EXPECT_FALSE(res.empty());
|
||||
// ASSERT_EQ(res.size(), 3);
|
||||
// EXPECT_EQ(res[0], "");
|
||||
// EXPECT_EQ(res[1], "One");
|
||||
// EXPECT_EQ(res[2], "");
|
||||
ASSERT_EQ(res.size(), 2);
|
||||
EXPECT_EQ(res[0], "");
|
||||
EXPECT_EQ(res[1], "One");
|
||||
|
||||
res = common_split_string(',', ",One,Two");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 3);
|
||||
EXPECT_EQ(res[0], "");
|
||||
EXPECT_EQ(res[1], "One");
|
||||
EXPECT_EQ(res[2], "Two");
|
||||
|
||||
res = common_split_string(',', ",One,Two,");
|
||||
EXPECT_FALSE(res.empty());
|
||||
// ASSERT_EQ(res.size(), 4);
|
||||
// EXPECT_EQ(res[0], "");
|
||||
// EXPECT_EQ(res[1], "One");
|
||||
// EXPECT_EQ(res[2], "Two");
|
||||
// EXPECT_EQ(res[3], "");
|
||||
ASSERT_EQ(res.size(), 3);
|
||||
EXPECT_EQ(res[0], "");
|
||||
EXPECT_EQ(res[1], "One");
|
||||
EXPECT_EQ(res[2], "Two");
|
||||
|
||||
res = common_split_string(',', "One");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 1);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
|
||||
res = common_split_string(',', "One,");
|
||||
EXPECT_FALSE(res.empty());
|
||||
// ASSERT_EQ(res.size(), 2);
|
||||
// EXPECT_EQ(res[0], "One");
|
||||
// EXPECT_EQ(res[1], "");
|
||||
ASSERT_EQ(res.size(), 1);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
|
||||
res = common_split_string(',', "One,Two");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 2);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
EXPECT_EQ(res[1], "Two");
|
||||
|
||||
res = common_split_string(',', "One,Two,");
|
||||
EXPECT_FALSE(res.empty());
|
||||
// ASSERT_EQ(res.size(), 3);
|
||||
// EXPECT_EQ(res[0], "One");
|
||||
// EXPECT_EQ(res[1], "Two");
|
||||
// EXPECT_EQ(res[2], "");
|
||||
ASSERT_EQ(res.size(), 2);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
EXPECT_EQ(res[1], "Two");
|
||||
|
||||
res = common_split_string(',', "One,,Two");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 3);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
EXPECT_EQ(res[1], "");
|
||||
EXPECT_EQ(res[2], "Two");
|
||||
|
||||
res = common_split_string(',', "One,,,Two");
|
||||
EXPECT_FALSE(res.empty());
|
||||
ASSERT_EQ(res.size(), 4);
|
||||
EXPECT_EQ(res[0], "One");
|
||||
EXPECT_EQ(res[1], "");
|
||||
EXPECT_EQ(res[2], "");
|
||||
EXPECT_EQ(res[3], "Two");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,116 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2025 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <ocpp/v16/utils.hpp>
|
||||
|
||||
namespace ocpp {
|
||||
namespace v16 {
|
||||
|
||||
class UtilsTest : public ::testing::Test {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
}
|
||||
|
||||
void TearDown() override {
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(UtilsTest, test_drop_transaction_data) {
|
||||
auto call = ocpp::Call<StopTransactionRequest>();
|
||||
ASSERT_FALSE(call.msg.transactionData.has_value());
|
||||
|
||||
std::vector<TransactionData> transaction_data = {
|
||||
{DateTime(), {{"1"}}},
|
||||
{DateTime(), {{"1"}, {"2"}}},
|
||||
{DateTime(), {{"1"}, {"2"}, {"3"}}},
|
||||
{DateTime(), {{"1"}, {"2"}, {"3"}, {"4"}}},
|
||||
{DateTime(), {{"1"}, {"2"}, {"3"}, {"4"}, {"5"}}},
|
||||
};
|
||||
|
||||
ASSERT_EQ(transaction_data.size(), 5);
|
||||
call.msg.transactionData = transaction_data;
|
||||
ASSERT_TRUE(call.msg.transactionData.has_value());
|
||||
ASSERT_EQ(call.msg.transactionData.value().at(0).sampledValue.size(), 1);
|
||||
ASSERT_EQ(call.msg.transactionData.value().at(1).sampledValue.size(), 2);
|
||||
ASSERT_EQ(call.msg.transactionData.value().at(2).sampledValue.size(), 3);
|
||||
ASSERT_EQ(call.msg.transactionData.value().at(3).sampledValue.size(), 4);
|
||||
ASSERT_EQ(call.msg.transactionData.value().at(4).sampledValue.size(), 5);
|
||||
utils::drop_transaction_data(500, call);
|
||||
ASSERT_EQ(call.msg.transactionData.value().size(), 3);
|
||||
ASSERT_EQ(call.msg.transactionData.value().at(0).sampledValue.size(), 1);
|
||||
ASSERT_EQ(call.msg.transactionData.value().at(1).sampledValue.size(), 3);
|
||||
ASSERT_EQ(call.msg.transactionData.value().at(2).sampledValue.size(), 5);
|
||||
}
|
||||
|
||||
TEST_F(UtilsTest, test_drop_transaction_data_six_elements) {
|
||||
auto call = ocpp::Call<StopTransactionRequest>();
|
||||
// Use padded values so the serialized message clearly exceeds the threshold
|
||||
std::string v(500, 'x');
|
||||
|
||||
std::vector<TransactionData> transaction_data = {
|
||||
{DateTime(), {{v}}},
|
||||
{DateTime(), {{v}, {v}}},
|
||||
{DateTime(), {{v}, {v}, {v}}},
|
||||
{DateTime(), {{v}, {v}, {v}, {v}}},
|
||||
{DateTime(), {{v}, {v}, {v}, {v}, {v}}},
|
||||
{DateTime(), {{v}, {v}, {v}, {v}, {v}, {v}}},
|
||||
};
|
||||
|
||||
call.msg.transactionData = transaction_data;
|
||||
ASSERT_EQ(call.msg.transactionData.value().size(), 6);
|
||||
utils::drop_transaction_data(9500, call);
|
||||
// Drop indices 1, 3 -> keep [0, 2, 4, 5]
|
||||
ASSERT_EQ(call.msg.transactionData.value().size(), 4);
|
||||
ASSERT_EQ(call.msg.transactionData.value().at(0).sampledValue.size(), 1);
|
||||
ASSERT_EQ(call.msg.transactionData.value().at(1).sampledValue.size(), 3);
|
||||
ASSERT_EQ(call.msg.transactionData.value().at(2).sampledValue.size(), 5);
|
||||
ASSERT_EQ(call.msg.transactionData.value().at(3).sampledValue.size(), 6);
|
||||
}
|
||||
|
||||
TEST_F(UtilsTest, test_drop_transaction_data_seven_elements) {
|
||||
auto call = ocpp::Call<StopTransactionRequest>();
|
||||
// Use padded values so the serialized message clearly exceeds the threshold
|
||||
std::string v(500, 'x');
|
||||
|
||||
std::vector<TransactionData> transaction_data = {
|
||||
{DateTime(), {{v}}},
|
||||
{DateTime(), {{v}, {v}}},
|
||||
{DateTime(), {{v}, {v}, {v}}},
|
||||
{DateTime(), {{v}, {v}, {v}, {v}}},
|
||||
{DateTime(), {{v}, {v}, {v}, {v}, {v}}},
|
||||
{DateTime(), {{v}, {v}, {v}, {v}, {v}, {v}}},
|
||||
{DateTime(), {{v}, {v}, {v}, {v}, {v}, {v}, {v}}},
|
||||
};
|
||||
|
||||
call.msg.transactionData = transaction_data;
|
||||
ASSERT_EQ(call.msg.transactionData.value().size(), 7);
|
||||
utils::drop_transaction_data(9500, call);
|
||||
// Drop indices 1, 3, 5 -> keep [0, 2, 4, 6]
|
||||
ASSERT_EQ(call.msg.transactionData.value().size(), 4);
|
||||
ASSERT_EQ(call.msg.transactionData.value().at(0).sampledValue.size(), 1);
|
||||
ASSERT_EQ(call.msg.transactionData.value().at(1).sampledValue.size(), 3);
|
||||
ASSERT_EQ(call.msg.transactionData.value().at(2).sampledValue.size(), 5);
|
||||
ASSERT_EQ(call.msg.transactionData.value().at(3).sampledValue.size(), 7);
|
||||
}
|
||||
|
||||
TEST_F(UtilsTest, test_drop_transaction_data_preserves_minimum) {
|
||||
auto call = ocpp::Call<StopTransactionRequest>();
|
||||
|
||||
std::vector<TransactionData> transaction_data = {
|
||||
{DateTime(), {{"1"}}},
|
||||
{DateTime(), {{"1"}, {"2"}}},
|
||||
{DateTime(), {{"1"}, {"2"}, {"3"}}},
|
||||
};
|
||||
|
||||
call.msg.transactionData = transaction_data;
|
||||
ASSERT_EQ(call.msg.transactionData.value().size(), 3);
|
||||
utils::drop_transaction_data(1, call);
|
||||
// With 3 elements, drop index 1 -> keep [0, 2], then size == 2 so loop stops
|
||||
ASSERT_EQ(call.msg.transactionData.value().size(), 2);
|
||||
ASSERT_EQ(call.msg.transactionData.value().at(0).sampledValue.size(), 1);
|
||||
ASSERT_EQ(call.msg.transactionData.value().at(1).sampledValue.size(), 3);
|
||||
}
|
||||
|
||||
} // namespace v16
|
||||
} // namespace ocpp
|
||||
@@ -0,0 +1,75 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <memory>
|
||||
|
||||
#include <ocpp/v16/charge_point_configuration.hpp>
|
||||
#include <ocpp/v16/charge_point_configuration_devicemodel.hpp>
|
||||
#include <ocpp/v16/utils.hpp>
|
||||
#include <string_view>
|
||||
|
||||
#include "memory_storage.hpp"
|
||||
|
||||
namespace ocpp::v16::stubs {
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
// create instances for v16 and v2 configuration
|
||||
class ConfigurationBase : public testing::Test {
|
||||
protected:
|
||||
std::unique_ptr<ocpp::v16::ChargePointConfigurationInterface> v16_config;
|
||||
std::unique_ptr<ocpp::v16::ChargePointConfigurationInterface> v2_config;
|
||||
std::unique_ptr<MemoryStorage> device_model;
|
||||
|
||||
void loadConfig(const std::string_view& file) {
|
||||
fs::path cfg{CONFIG_DIR_V16};
|
||||
cfg /= file;
|
||||
std::ifstream ifs(cfg);
|
||||
const std::string config_file((std::istreambuf_iterator<char>(ifs)), (std::istreambuf_iterator<char>()));
|
||||
v16_config = std::make_unique<ocpp::v16::ChargePointConfiguration>(config_file, CONFIG_DIR_V16,
|
||||
USER_CONFIG_FILE_LOCATION_V16);
|
||||
device_model = std::make_unique<MemoryStorage>();
|
||||
std::unique_ptr<ocpp::v2::DeviceModelInterface> proxy = std::make_unique<MemoryStorageProxy>(*device_model);
|
||||
v2_config = std::make_unique<ocpp::v16::ChargePointConfigurationDeviceModel>(CONFIG_DIR_V16, std::move(proxy));
|
||||
}
|
||||
|
||||
void SetUp() override {
|
||||
loadConfig("config.json");
|
||||
}
|
||||
|
||||
// void TearDown() override {
|
||||
// }
|
||||
};
|
||||
|
||||
// support parameterised tests so the same test can be run against:
|
||||
// - the v16 JSON configuration
|
||||
// - the v2 database interface (via an in-memory implementation)
|
||||
class Configuration : public ConfigurationBase, public testing::WithParamInterface<std::string_view> {
|
||||
public:
|
||||
ocpp::v16::ChargePointConfigurationInterface* get() {
|
||||
ocpp::v16::ChargePointConfigurationInterface* result{nullptr};
|
||||
if (GetParam() == "sql") {
|
||||
result = v2_config.get();
|
||||
} else if (GetParam() == "json") {
|
||||
result = v16_config.get();
|
||||
}
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
// create instances for v16 and v2 configuration
|
||||
class ConfigurationFull : public Configuration {
|
||||
protected:
|
||||
void SetUp() override {
|
||||
loadConfig("config-full.json");
|
||||
device_model->apply_full_config();
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ocpp::v16::stubs
|
||||
@@ -0,0 +1,77 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "ocpp/common/types.hpp"
|
||||
#include <ocpp/common/evse_security.hpp>
|
||||
|
||||
namespace ocpp::v16::stubs {
|
||||
|
||||
class EvseSecurityStub : public ocpp::EvseSecurity {
|
||||
|
||||
public:
|
||||
virtual ~EvseSecurityStub() = default;
|
||||
|
||||
InstallCertificateResult install_ca_certificate(const std::string& certificate,
|
||||
const CaCertificateType& certificate_type) override {
|
||||
return InstallCertificateResult::Accepted;
|
||||
}
|
||||
|
||||
DeleteCertificateResult delete_certificate(const CertificateHashDataType& certificate_hash_data) override {
|
||||
return DeleteCertificateResult::Accepted;
|
||||
}
|
||||
|
||||
InstallCertificateResult update_leaf_certificate(const std::string& certificate_chain,
|
||||
const CertificateSigningUseEnum& certificate_type) override {
|
||||
return InstallCertificateResult::Accepted;
|
||||
}
|
||||
CertificateValidationResult verify_certificate(const std::string& certificate_chain,
|
||||
const LeafCertificateType& certificate_type) override {
|
||||
return CertificateValidationResult::Valid;
|
||||
}
|
||||
CertificateValidationResult verify_certificate(const std::string& certificate_chain,
|
||||
const std::vector<LeafCertificateType>& certificate_types) override {
|
||||
return CertificateValidationResult::Valid;
|
||||
}
|
||||
std::vector<CertificateHashDataChain>
|
||||
get_installed_certificates(const std::vector<CertificateType>& certificate_types) override {
|
||||
return {};
|
||||
}
|
||||
std::vector<OCSPRequestData> get_v2g_ocsp_request_data() override {
|
||||
return {};
|
||||
}
|
||||
std::vector<OCSPRequestData> get_mo_ocsp_request_data(const std::string& certificate_chain) override {
|
||||
return {};
|
||||
}
|
||||
void update_ocsp_cache(const CertificateHashDataType& certificate_hash_data,
|
||||
const std::string& ocsp_response) override {
|
||||
}
|
||||
bool is_ca_certificate_installed(const CaCertificateType& certificate_type) override {
|
||||
return true;
|
||||
}
|
||||
GetCertificateSignRequestResult
|
||||
generate_certificate_signing_request(const CertificateSigningUseEnum& certificate_type, const std::string& country,
|
||||
const std::string& organization, const std::string& common,
|
||||
bool use_tpm) override {
|
||||
return {GetCertificateSignRequestStatus::KeyGenError, {}};
|
||||
}
|
||||
GetCertificateInfoResult get_leaf_certificate_info(const CertificateSigningUseEnum& certificate_type,
|
||||
bool include_ocsp = false) override {
|
||||
return {};
|
||||
}
|
||||
|
||||
bool update_certificate_links(const CertificateSigningUseEnum& certificate_type) override {
|
||||
return true;
|
||||
}
|
||||
std::string get_verify_file(const CaCertificateType& certificate_type) override {
|
||||
return {};
|
||||
}
|
||||
std::string get_verify_location(const CaCertificateType& certificate_type) override {
|
||||
return {};
|
||||
}
|
||||
int get_leaf_expiry_days_count(const CertificateSigningUseEnum& certificate_type) override {
|
||||
return 365;
|
||||
}
|
||||
};
|
||||
} // namespace ocpp::v16::stubs
|
||||
@@ -0,0 +1,771 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "memory_storage.hpp"
|
||||
|
||||
#include <exception>
|
||||
#include <ocpp/v16/charge_point_configuration_devicemodel.hpp>
|
||||
#include <ocpp/v16/utils.hpp>
|
||||
#include <ocpp/v2/ctrlr_component_variables.hpp>
|
||||
#include <ocpp/v2/ocpp_enums.hpp>
|
||||
#include <ocpp/v2/ocpp_types.hpp>
|
||||
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
namespace {
|
||||
|
||||
// initial values are from the JSON unit test config files
|
||||
// Do not add additional values
|
||||
const std::map<std::string, std::string> required_vars_internal = {
|
||||
{"CentralSystemURI", "127.0.0.1:8180/steve/websocket/CentralSystemService/"},
|
||||
{"ChargeBoxSerialNumber", "cp001"},
|
||||
{"ChargePointId", "cp001"},
|
||||
{"ChargePointVendor", "Pionix"},
|
||||
{"ChargePointModel", "Yeti"},
|
||||
{"FirmwareVersion", "0.1"},
|
||||
{"SupportedCiphers12",
|
||||
"ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384"},
|
||||
{"SupportedCiphers13", "TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256"},
|
||||
{"SupportedMeasurands", "Energy.Active.Import.Register,Energy.Active.Export.Register,Power.Active.Import,Voltage,"
|
||||
"Current.Import,Frequency,Current.Offered,Power.Offered,SoC,Temperature"},
|
||||
{"TLSKeylogFile", "/tmp/ocpp_tls_keylog.txt"},
|
||||
{"WebsocketPingPayload", "hello there"},
|
||||
{"AuthorizeConnectorZeroOnConnectorOne", "true"},
|
||||
{"LogMessages", "true"},
|
||||
{"UseSslDefaultVerifyPaths", "true"},
|
||||
{"VerifyCsmsCommonName", "true"},
|
||||
{"EnableTLSKeylog", "false"},
|
||||
{"LogMessagesRaw", "false"},
|
||||
{"LogRotation", "false"},
|
||||
{"LogRotationDateSuffix", "false"},
|
||||
{"StopTransactionIfUnlockNotSupported", "false"},
|
||||
{"UseTPM", "false"},
|
||||
{"UseTPMSeccLeafCertificate", "false"},
|
||||
{"VerifyCsmsAllowWildcards", "false"},
|
||||
{"MaxMessageSize", "65000"},
|
||||
{"MaxCompositeScheduleDuration", "31536000"},
|
||||
{"OcspRequestInterval", "604800"},
|
||||
{"RetryBackoffRandomRange", "10"},
|
||||
{"RetryBackoffRepeatTimes", "3"},
|
||||
{"RetryBackoffWaitMinimum", "3"},
|
||||
{"WaitForStopTransactionsOnResetTimeout", "60"},
|
||||
{"WebsocketPongTimeout", "5"},
|
||||
{"LogRotationMaximumFileCount", "0"},
|
||||
{"LogRotationMaximumFileSize", "0"},
|
||||
{"SupportedChargingProfilePurposeTypes", "ChargePointMaxProfile,TxDefaultProfile,TxProfile"},
|
||||
{"LogMessagesFormat", ""},
|
||||
{"CompositeScheduleDefaultLimitAmps", "48"},
|
||||
{"CompositeScheduleDefaultLimitWatts", "33120"},
|
||||
{"CompositeScheduleDefaultNumberPhases", "3"},
|
||||
{"SupplyVoltage", "230"},
|
||||
}; // namespace
|
||||
|
||||
// initial values are from the JSON unit test config files
|
||||
// Do not add additional values
|
||||
const std::map<std::string, std::string> required_vars_core = {
|
||||
{"NumberOfConnectors", "1"},
|
||||
{"SupportedFeatureProfiles",
|
||||
"Core,FirmwareManagement,RemoteTrigger,Reservation,LocalAuthListManagement,SmartCharging"},
|
||||
{"ConnectorPhaseRotation", "0.RST,1.RST"},
|
||||
{"MeterValuesAlignedData", "Energy.Active.Import.Register"},
|
||||
{"MeterValuesSampledData", "Energy.Active.Import.Register"},
|
||||
{"StopTxnAlignedData", "Energy.Active.Import.Register"},
|
||||
{"StopTxnSampledData", "Energy.Active.Import.Register"},
|
||||
{"AuthorizeRemoteTxRequests", "false"},
|
||||
{"LocalAuthorizeOffline", "false"},
|
||||
{"LocalPreAuthorize", "false"},
|
||||
{"StopTransactionOnInvalidId", "true"},
|
||||
{"UnlockConnectorOnEVSideDisconnect", "true"},
|
||||
{"ClockAlignedDataInterval", "900"},
|
||||
{"ConnectionTimeOut", "10"},
|
||||
{"GetConfigurationMaxKeys", "100"},
|
||||
{"HeartbeatInterval", "86400"},
|
||||
{"MeterValueSampleInterval", "0"},
|
||||
{"ResetRetries", "1"},
|
||||
{"TransactionMessageAttempts", "1"},
|
||||
{"TransactionMessageRetryInterval", "10"},
|
||||
{"StopTransactionOnEVSideDisconnect", "true"},
|
||||
};
|
||||
|
||||
// initial values are from the JSON unit test config files
|
||||
// Do not add additional values
|
||||
const std::map<std::string, std::string> required_vars_firmware_management = {
|
||||
{"SupportedFileTransferProtocols", "FTP"},
|
||||
};
|
||||
|
||||
// initial values are from the JSON unit test config files
|
||||
// Do not add additional values
|
||||
const std::map<std::string, std::string> required_vars_smart_charging = {
|
||||
{"ChargeProfileMaxStackLevel", "42"},
|
||||
{"ChargingScheduleAllowedChargingRateUnit", "Current"},
|
||||
{"ChargingScheduleMaxPeriods", "42"},
|
||||
{"MaxChargingProfilesInstalled", "42"},
|
||||
};
|
||||
|
||||
// initial values are from the JSON unit test config files
|
||||
// Do not add additional values
|
||||
const std::map<std::string, std::string> required_vars_security = {
|
||||
{"SecurityProfile", "0"},
|
||||
{"DisableSecurityEventNotifications", "false"},
|
||||
};
|
||||
|
||||
// initial values are from the JSON unit test config files
|
||||
// Do not add additional values
|
||||
const std::map<std::string, std::string> required_vars_local_auth_list = {
|
||||
{"LocalAuthListEnabled", "true"},
|
||||
{"LocalAuthListMaxLength", "42"},
|
||||
{"SendLocalListMaxLength", "42"},
|
||||
};
|
||||
|
||||
// initial values are from the JSON unit test config files
|
||||
// Do not add additional values
|
||||
const std::map<std::string, std::string> required_vars_pnc = {
|
||||
{"ISO15118CertificateManagementEnabled", "true"},
|
||||
{"ISO15118PnCEnabled", "true"},
|
||||
{"ContractValidationOffline", "true"},
|
||||
};
|
||||
|
||||
// initial values are from the JSON unit test config files
|
||||
// Do not add additional values
|
||||
const std::map<std::string, std::string> required_vars_california_pricing = {
|
||||
{"CustomDisplayCostAndPrice", "false"},
|
||||
};
|
||||
|
||||
// initial values are from the JSON unit test config files
|
||||
// Do not add additional values
|
||||
const std::map<std::string, std::string> required_vars_custom = {};
|
||||
|
||||
// additional values for full config
|
||||
const std::map<std::string, std::string> full_vars_california_pricing = {
|
||||
{"SupportedLanguages", "en, nl, de, nb_NO"},
|
||||
{"CustomMultiLanguageMessages", "true"},
|
||||
{"Language", "en"},
|
||||
};
|
||||
|
||||
using MemoryStorage = ocpp::v16::stubs::MemoryStorage;
|
||||
MemoryStorage::Storage vars_internal;
|
||||
MemoryStorage::Storage vars_core;
|
||||
MemoryStorage::Storage vars_firmware_management;
|
||||
MemoryStorage::Storage vars_smart_charging;
|
||||
MemoryStorage::Storage vars_security;
|
||||
MemoryStorage::Storage vars_local_auth_list;
|
||||
MemoryStorage::Storage vars_pnc;
|
||||
MemoryStorage::Storage vars_california_pricing;
|
||||
MemoryStorage::Storage vars_custom;
|
||||
MemoryStorage::Storage vars_additional;
|
||||
|
||||
const std::vector<MemoryStorage::Storage*> vars_list = {
|
||||
&vars_internal, &vars_core, &vars_firmware_management, &vars_smart_charging, &vars_security,
|
||||
&vars_local_auth_list, &vars_pnc, &vars_california_pricing, &vars_custom, &vars_additional,
|
||||
};
|
||||
|
||||
const ocpp::v2::VariableCharacteristics characteristics = {ocpp::v2::DataEnum::string, false, {}, {}, {}, {}, {}, {}};
|
||||
const ocpp::v2::VariableMetaData meta_data = {characteristics, {}, {}};
|
||||
|
||||
bool operator==(const ocpp::v2::Component& lhs, const ocpp::v2::Component& rhs) {
|
||||
return lhs.name == rhs.name;
|
||||
}
|
||||
|
||||
bool operator==(const std::optional<ocpp::v2::Variable>& lhs, const ocpp::v2::Variable& rhs) {
|
||||
if (lhs) {
|
||||
return lhs.value().name == rhs.name;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool is_same(const ocpp::v2::RequiredComponentVariable& var, const ocpp::v2::Component& component,
|
||||
const ocpp::v2::Variable& variable) {
|
||||
return ((var.component == component) && (var.variable == variable));
|
||||
}
|
||||
|
||||
std::string enhanced_convert(const ocpp::v2::Component& component, const ocpp::v2::Variable& variable,
|
||||
ocpp::v2::AttributeEnum attribute) {
|
||||
using namespace ocpp::v2::ControllerComponentVariables;
|
||||
auto result = ocpp::v16::keys::convert_v2(component, variable, attribute);
|
||||
if (!result.has_value()) {
|
||||
if (component.name == "EVSE" && variable.name == "ISO15118EvseId") {
|
||||
result = std::string{ocpp::v16::keys::convert(ocpp::v16::keys::valid_keys::ConnectorEvseIds)};
|
||||
}
|
||||
}
|
||||
return result.value_or(variable.name);
|
||||
}
|
||||
|
||||
std::string central_system_uri_to_json(const std::string& value) {
|
||||
// convert to JSON
|
||||
auto profile = json::parse(R"([{"ocppCsmsUrl":""}])");
|
||||
profile[0]["ocppCsmsUrl"] = value;
|
||||
return profile.dump(-1);
|
||||
}
|
||||
|
||||
void add_to_report(std::vector<ocpp::v2::ReportData>& report, const ocpp::v2::RequiredComponentVariable& rcv,
|
||||
ocpp::v2::MutabilityEnum mutability, const std::string& value) {
|
||||
if (rcv.variable) {
|
||||
ocpp::v2::ReportData data;
|
||||
data.component = rcv.component;
|
||||
data.variable = rcv.variable.value();
|
||||
ocpp::v2::VariableAttribute va;
|
||||
va.type = ocpp::v2::AttributeEnum::Actual;
|
||||
va.mutability = mutability;
|
||||
if (!value.empty()) {
|
||||
va.value = value;
|
||||
}
|
||||
data.variableAttribute.push_back(std::move(va));
|
||||
report.push_back(std::move(data));
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
namespace ocpp::v16::stubs {
|
||||
// ----------------------------------------------------------------------------
|
||||
// Static methods
|
||||
|
||||
std::optional<std::string> MemoryStorage::set_connector_id(std::int32_t id, const std::string& current,
|
||||
const std::string& value) {
|
||||
std::optional<std::string> result;
|
||||
if (id > 0) {
|
||||
const std::size_t index = id - 1;
|
||||
auto vec = ocpp::v16::utils::split_string(',', current);
|
||||
if (index >= vec.size()) {
|
||||
// add empty elements
|
||||
vec.insert(vec.end(), (index + 1) - vec.size(), {});
|
||||
}
|
||||
vec[index] = value;
|
||||
result = ocpp::v16::utils::to_csl(vec);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::optional<std::string> MemoryStorage::get_connector_id(std::int32_t id, const std::string& current) {
|
||||
std::optional<std::string> result;
|
||||
if (id > 0 && !current.empty()) {
|
||||
const std::size_t index = id - 1;
|
||||
auto vec = ocpp::v16::utils::split_string(',', current);
|
||||
if (index < vec.size()) {
|
||||
result = vec[index];
|
||||
} else {
|
||||
result = std::move(std::string{});
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
MemoryStorage::MemoryStorage() {
|
||||
vars_internal = required_vars_internal;
|
||||
vars_core = required_vars_core;
|
||||
vars_firmware_management = required_vars_firmware_management;
|
||||
vars_smart_charging = required_vars_smart_charging;
|
||||
vars_security = required_vars_security;
|
||||
vars_local_auth_list = required_vars_local_auth_list;
|
||||
vars_pnc = required_vars_pnc;
|
||||
vars_california_pricing = required_vars_california_pricing;
|
||||
vars_custom = required_vars_custom;
|
||||
|
||||
vars_additional.clear();
|
||||
read_only.clear();
|
||||
}
|
||||
|
||||
void MemoryStorage::apply_full_config() {
|
||||
vars_california_pricing.insert(full_vars_california_pricing.begin(), full_vars_california_pricing.end());
|
||||
}
|
||||
|
||||
std::optional<MemoryStorage::Storage::iterator> MemoryStorage::locate_v16(const std::string& name) const {
|
||||
// since V16 items are unique, just search through the storage items
|
||||
// to locate it
|
||||
for (auto& i : vars_list) {
|
||||
if (auto it = i->find(name); it != i->end()) {
|
||||
return it;
|
||||
}
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::string> MemoryStorage::get_v16(const std::string& name) const {
|
||||
// since V16 items are unique, just search through the storage items
|
||||
// to locate it
|
||||
auto it = locate_v16(name);
|
||||
if (it) {
|
||||
return it.value()->second;
|
||||
} else {
|
||||
std::cout << "get_v16: unable to locate '" << name << "'\n";
|
||||
}
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
std::optional<std::string> MemoryStorage::get_v16(ocpp::v16::keys::valid_keys key) const {
|
||||
return get_v16(std::string{ocpp::v16::keys::convert(key)});
|
||||
}
|
||||
|
||||
MemoryStorage::SetVariableStatusEnum MemoryStorage::set_v16(const std::string& name, const std::string& value) {
|
||||
// since V16 items are unique, just search through the storage items
|
||||
// to locate it
|
||||
SetVariableStatusEnum result{SetVariableStatusEnum::UnknownVariable};
|
||||
|
||||
auto it = locate_v16(name);
|
||||
if (it) {
|
||||
it.value()->second = value;
|
||||
result = SetVariableStatusEnum::Accepted;
|
||||
} else {
|
||||
auto found = keys::convert(name);
|
||||
bool create = found.has_value();
|
||||
|
||||
if (name.rfind("MeterPublicKey") == 0) {
|
||||
create = true;
|
||||
}
|
||||
|
||||
if (create) {
|
||||
std::cout << "set_v16: unable to locate '" << name << "' creating\n";
|
||||
vars_additional.insert({name, value});
|
||||
result = SetVariableStatusEnum::Accepted;
|
||||
|
||||
} else {
|
||||
std::cout << "set_v16: unable to locate '" << name << "' ignoring\n";
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
MemoryStorage::SetVariableStatusEnum MemoryStorage::set_v16_custom(const std::string& name, const std::string& value) {
|
||||
// since V16 items are unique, just search through the storage items
|
||||
// to locate it
|
||||
|
||||
auto it = locate_v16(name);
|
||||
if (it) {
|
||||
it.value()->second = value;
|
||||
} else {
|
||||
vars_additional.insert({name, value});
|
||||
}
|
||||
|
||||
return SetVariableStatusEnum::Accepted;
|
||||
}
|
||||
|
||||
void MemoryStorage::set_readonly(const std::string& key) {
|
||||
read_only.insert(key);
|
||||
}
|
||||
|
||||
std::optional<MemoryStorage::MutabilityEnum> MemoryStorage::get_mutability(const std::string& key_str) {
|
||||
std::optional<MutabilityEnum> result;
|
||||
|
||||
const auto sv_key_opt = keys::convert(key_str);
|
||||
if (sv_key_opt) {
|
||||
const auto sv_key = sv_key_opt.value();
|
||||
if (sv_key == keys::valid_keys::AuthorizationKey) {
|
||||
result = MemoryStorage::MutabilityEnum::WriteOnly;
|
||||
} else {
|
||||
result = (keys::is_readonly(sv_key)) ? MemoryStorage::MutabilityEnum::ReadOnly
|
||||
: MemoryStorage::MutabilityEnum::ReadWrite;
|
||||
}
|
||||
} else {
|
||||
if (const auto it = read_only.find(key_str); it == read_only.end()) {
|
||||
// check if key exists (not in the read only list)
|
||||
auto found = locate_v16(key_str);
|
||||
if (found) {
|
||||
result = MemoryStorage::MutabilityEnum::ReadWrite;
|
||||
}
|
||||
} else {
|
||||
result = MemoryStorage::MutabilityEnum::ReadOnly;
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
void MemoryStorage::add_supported_measureands_values_list(ocpp::v2::ReportData& data) {
|
||||
const auto supported = get_v16(ocpp::v16::keys::valid_keys::SupportedMeasurands);
|
||||
if (supported) {
|
||||
ocpp::v2::VariableCharacteristics vc;
|
||||
vc.valuesList = supported.value();
|
||||
data.variableCharacteristics = std::move(vc);
|
||||
}
|
||||
}
|
||||
|
||||
void MemoryStorage::add_to_report(std::vector<ocpp::v2::ReportData>& report, const std::string_view& name,
|
||||
const std::string_view& value) {
|
||||
using namespace ocpp::v2::ControllerComponentVariables;
|
||||
const std::string name_str{name};
|
||||
std::string value_str{value};
|
||||
|
||||
// Keys that map to VariableCharacteristics.maxLimit are handled separately via get_variable_meta_data;
|
||||
// they don't need a VariableAttribute entry in the report.
|
||||
const auto key = keys::convert(name);
|
||||
if (key) {
|
||||
const auto max_limit_cv = keys::convert_v2_max_limit(*key);
|
||||
if (max_limit_cv) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const auto cv = ocpp::v16::keys::convert_v2(name);
|
||||
if (cv) {
|
||||
auto component = std::get<ocpp::v2::Component>(*cv);
|
||||
auto variable = std::get<ocpp::v2::Variable>(*cv);
|
||||
auto attribute = std::get<ocpp::v2::AttributeEnum>(*cv);
|
||||
ocpp::v2::ReportData data;
|
||||
data.component = std::move(component);
|
||||
data.variable = std::move(variable);
|
||||
ocpp::v2::VariableAttribute va;
|
||||
va.type = attribute;
|
||||
va.mutability = get_mutability(name_str);
|
||||
if (!value_str.empty()) {
|
||||
va.value = std::move(value_str);
|
||||
}
|
||||
|
||||
if (key == keys::valid_keys::MeterValuesAlignedData) {
|
||||
add_supported_measureands_values_list(data);
|
||||
} else if (key == keys::valid_keys::StopTxnAlignedData) {
|
||||
add_supported_measureands_values_list(data);
|
||||
} else if (key == keys::valid_keys::StopTxnSampledData) {
|
||||
add_supported_measureands_values_list(data);
|
||||
} else if (key == keys::valid_keys::MeterValuesSampledData) {
|
||||
add_supported_measureands_values_list(data);
|
||||
}
|
||||
|
||||
data.variableAttribute.push_back(std::move(va));
|
||||
report.push_back(std::move(data));
|
||||
} else {
|
||||
if (name_str == "SupportedMeasurands") {
|
||||
// ignore
|
||||
} else {
|
||||
std::cerr << "add_to_report: missing '" << name_str << "'\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MemoryStorage::add_to_report(std::vector<ocpp::v2::ReportData>& report, const std::string_view& name,
|
||||
const std::map<std::string, std::string>& vars) {
|
||||
using namespace ocpp::v2::ControllerComponentVariables;
|
||||
for (const auto& i : vars) {
|
||||
add_to_report(report, i.first, i.second);
|
||||
}
|
||||
}
|
||||
|
||||
void MemoryStorage::generate_report(std::vector<ocpp::v2::ReportData>& report) {
|
||||
report.clear();
|
||||
add_to_report(report, "Internal", vars_internal);
|
||||
add_to_report(report, "Core", vars_core);
|
||||
add_to_report(report, "FirmwareManagement", vars_firmware_management);
|
||||
add_to_report(report, "SmartCharging", vars_smart_charging);
|
||||
add_to_report(report, "Security", vars_security);
|
||||
add_to_report(report, "LocalAuthListManagement", vars_local_auth_list);
|
||||
add_to_report(report, "PnC", vars_pnc);
|
||||
add_to_report(report, "CostAndPrice", vars_california_pricing);
|
||||
add_to_report(report, "Custom", vars_custom);
|
||||
}
|
||||
|
||||
void MemoryStorage::set(const std::string_view& component, const std::string_view& variable,
|
||||
const std::string_view& value) {
|
||||
const std::string variable_v{variable};
|
||||
std::cout << "set " << component << '[' << variable << "] = '" << value << "'\n";
|
||||
if (component == "Internal") {
|
||||
// std::cout << "Internal[" << variable_id.name << "]=" << value << '\n';
|
||||
vars_internal[variable_v] = value;
|
||||
} else if (component == "Core") {
|
||||
// std::cout << "Core[" << variable << "]=" << value << '\n';
|
||||
vars_core[variable_v] = value;
|
||||
} else if (component == "FirmwareManagement") {
|
||||
// std::cout << "FirmwareManagement[" << variable << "]=" << value << '\n';
|
||||
vars_firmware_management[variable_v] = value;
|
||||
} else if (component == "SmartCharging") {
|
||||
// std::cout << "SmartCharging[" << variable << "]=" << value << '\n';
|
||||
vars_smart_charging[variable_v] = value;
|
||||
} else if (component == "Security") {
|
||||
// std::cout << "Security[" << variable << "]=" << value << '\n';
|
||||
vars_security[variable_v] = value;
|
||||
} else if (component == "LocalAuthListManagement") {
|
||||
// std::cout << "LocalAuthListManagement[" << variable << "]=" << value << '\n';
|
||||
vars_local_auth_list[variable_v] = value;
|
||||
} else if (component == "PnC") {
|
||||
// std::cout << "PnC[" << variable << "]=" << value << '\n';
|
||||
vars_pnc[variable_v] = value;
|
||||
} else if (component == "CostAndPrice") {
|
||||
// std::cout << "CostAndPrice[" << variable << "]=" << value << '\n';
|
||||
vars_california_pricing[variable_v] = value;
|
||||
} else if (component == "Custom") {
|
||||
// std::cout << "Custom[" << variable << "]=" << value << '\n';
|
||||
vars_custom[variable_v] = value;
|
||||
} else {
|
||||
std::cerr << "set not implemented for: " << component << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
std::string MemoryStorage::get(const std::string_view& component, const std::string_view& variable) {
|
||||
Component component_id;
|
||||
Variable variable_id;
|
||||
std::string result;
|
||||
|
||||
component_id.name = std::string{component};
|
||||
variable_id.name = std::string{variable};
|
||||
(void)get_variable(component_id, variable_id, AttributeEnum::Actual, result, false);
|
||||
return result;
|
||||
}
|
||||
|
||||
void MemoryStorage::clear(const std::string_view& component, const std::string_view& variable) {
|
||||
const std::string var{variable};
|
||||
|
||||
if (component == "Internal") {
|
||||
vars_internal.erase(var);
|
||||
} else if (component == "Core") {
|
||||
vars_core.erase(var);
|
||||
} else if (component == "FirmwareManagement") {
|
||||
vars_firmware_management.erase(var);
|
||||
} else if (component == "SmartCharging") {
|
||||
vars_smart_charging.erase(var);
|
||||
} else if (component == "Security") {
|
||||
vars_security.erase(var);
|
||||
} else if (component == "LocalAuthListManagement") {
|
||||
vars_local_auth_list.erase(var);
|
||||
} else if (component == "PnC") {
|
||||
vars_pnc.erase(var);
|
||||
} else if (component == "CostAndPrice") {
|
||||
vars_california_pricing.erase(var);
|
||||
} else if (component == "Custom") {
|
||||
vars_custom.erase(var);
|
||||
} else {
|
||||
std::cerr << "clear not implemented for: " << component << '\n';
|
||||
}
|
||||
}
|
||||
|
||||
MemoryStorage::GetVariableStatusEnum MemoryStorage::get_variable(const Component& component_id,
|
||||
const Variable& variable_id,
|
||||
const AttributeEnum& attribute_enum,
|
||||
std::string& value, bool allow_write_only) const {
|
||||
auto result = GetVariableStatusEnum::UnknownVariable;
|
||||
const auto name = enhanced_convert(component_id, variable_id, attribute_enum);
|
||||
std::optional<std::string> retrieved;
|
||||
// std::cout << "--> " << component_id.name << '[' << variable_id.name << "]\n";
|
||||
if (name.empty()) {
|
||||
retrieved = get_v16(variable_id.name);
|
||||
} else {
|
||||
retrieved = get_v16(name);
|
||||
if (retrieved) {
|
||||
const auto key_opt = v16::keys::convert(name);
|
||||
if (key_opt) {
|
||||
if (key_opt.value() == v16::keys::valid_keys::ConnectorEvseIds) {
|
||||
if (component_id.evse) {
|
||||
const auto id = component_id.evse.value().id;
|
||||
auto fetched = get_connector_id(id, *retrieved);
|
||||
if (fetched) {
|
||||
retrieved = *fetched;
|
||||
} else {
|
||||
retrieved = "";
|
||||
}
|
||||
std::cout << component_id.name << '[' << variable_id.name << "]." << id << " has value: '"
|
||||
<< *retrieved << "' (" << name << ")\n";
|
||||
} else {
|
||||
std::cerr << "get_value with missing evse: " << component_id.name << '[' << variable_id.name
|
||||
<< "]\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
std::cout << component_id.name << '[' << variable_id.name << "] has no value (" << name << ")\n";
|
||||
}
|
||||
}
|
||||
if (retrieved) {
|
||||
value = *retrieved;
|
||||
result = GetVariableStatusEnum::Accepted;
|
||||
// std::cout << component_id.name << '[' << variable_id.name << "]." << id << " has value: '" << *retrieved
|
||||
// << "' (" << name << ")\n";
|
||||
} else {
|
||||
std::cerr << "get_variable not implemented for: " << component_id.name << ':' << variable_id.name << " ("
|
||||
<< name << ")\n";
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
MemoryStorage::SetVariableStatusEnum MemoryStorage::set_value(const Component& component_id,
|
||||
const Variable& variable_id,
|
||||
const AttributeEnum& attribute_enum,
|
||||
const std::string& value, const std::string& source,
|
||||
bool allow_read_only) {
|
||||
const auto key_str = enhanced_convert(component_id, variable_id, attribute_enum);
|
||||
MemoryStorage::SetVariableStatusEnum result;
|
||||
if (!key_str.empty()) {
|
||||
const auto key_opt = v16::keys::convert(key_str);
|
||||
std::string store_value = value;
|
||||
result = MemoryStorage::SetVariableStatusEnum::Accepted;
|
||||
if (key_opt) {
|
||||
if (key_opt.value() == v16::keys::valid_keys::ConnectorEvseIds) {
|
||||
result = MemoryStorage::SetVariableStatusEnum::Rejected;
|
||||
auto retrieved = get_v16(v16::keys::valid_keys::ConnectorEvseIds);
|
||||
store_value.clear();
|
||||
if (retrieved) {
|
||||
store_value = *retrieved;
|
||||
}
|
||||
if (component_id.evse) {
|
||||
const auto id = component_id.evse.value().id;
|
||||
auto updated = set_connector_id(id, store_value, value);
|
||||
if (updated) {
|
||||
store_value = *updated;
|
||||
result = MemoryStorage::SetVariableStatusEnum::Accepted;
|
||||
} else {
|
||||
std::cerr << "set_value with invalid evse: " << component_id.name << '[' << variable_id.name
|
||||
<< "] " << id << '\n';
|
||||
}
|
||||
} else {
|
||||
std::cerr << "set_value with missing evse: " << component_id.name << '[' << variable_id.name
|
||||
<< "]\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
if (result == MemoryStorage::SetVariableStatusEnum::Accepted) {
|
||||
result = set_v16(key_str, store_value);
|
||||
std::cout << component_id.name << '[' << variable_id.name << "] = '" << store_value << "' (" << key_str
|
||||
<< ") " << (int)result << '\n';
|
||||
}
|
||||
} else {
|
||||
result = set_v16_custom(variable_id.name, value);
|
||||
std::cout << component_id.name << '[' << variable_id.name << "] = '" << value << "' " << (int)result << '\n';
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
MemoryStorage::SetVariableStatusEnum MemoryStorage::set_read_only_value(const Component& component_id,
|
||||
const Variable& variable_id,
|
||||
const AttributeEnum& attribute_enum,
|
||||
const std::string& value,
|
||||
const std::string& source) {
|
||||
return set_value(component_id, variable_id, attribute_enum, value, source);
|
||||
}
|
||||
|
||||
MemoryStorage::SetVariableStatusEnum MemoryStorage::clear_value(const Component& component_id,
|
||||
const Variable& variable_id,
|
||||
const AttributeEnum& attribute_enum,
|
||||
const std::string& source) {
|
||||
// Stub: clearing is equivalent to setting "" without validate_value gating; the in-memory
|
||||
// storage doesn't enforce minLimit, so a plain set_value with allow_read_only=true matches.
|
||||
return set_value(component_id, variable_id, attribute_enum, "", source, true);
|
||||
}
|
||||
|
||||
std::optional<MemoryStorage::MutabilityEnum> MemoryStorage::get_mutability(const Component& component_id,
|
||||
const Variable& variable_id,
|
||||
const AttributeEnum& attribute_enum) {
|
||||
auto key_str_opt = keys::convert_v2(component_id, variable_id, attribute_enum);
|
||||
auto key_str = key_str_opt.value_or(variable_id.name);
|
||||
return get_mutability(key_str);
|
||||
}
|
||||
|
||||
std::optional<MemoryStorage::VariableMetaData> MemoryStorage::get_variable_meta_data(const Component& component_id,
|
||||
const Variable& variable_id) {
|
||||
std::optional<MemoryStorage::VariableMetaData> result;
|
||||
|
||||
// Check if this is a maxLimit key by iterating the known mappings
|
||||
auto fn = [&](keys::valid_keys key) {
|
||||
if (result) {
|
||||
return; // already found
|
||||
}
|
||||
const auto cv = keys::convert_v2_max_limit(key);
|
||||
if (cv && cv->first.name == component_id.name && cv->second.name == variable_id.name &&
|
||||
cv->second.instance == variable_id.instance) {
|
||||
const auto retrieved = get_v16(std::string{keys::convert(key)});
|
||||
if (retrieved) {
|
||||
MemoryStorage::VariableMetaData md;
|
||||
md.characteristics.dataType = v2::DataEnum::integer;
|
||||
md.characteristics.supportsMonitoring = false;
|
||||
try {
|
||||
md.characteristics.maxLimit = std::stof(*retrieved);
|
||||
} catch (...) {
|
||||
}
|
||||
result = std::move(md);
|
||||
}
|
||||
}
|
||||
};
|
||||
for (const auto& [key, cv] : keys::max_limit_entries) {
|
||||
fn(key);
|
||||
}
|
||||
|
||||
if (!result) {
|
||||
const auto key_str = keys::convert_v2(component_id, variable_id, ocpp::v2::AttributeEnum::Actual);
|
||||
const auto retrieved = get_v16(key_str.value_or(""));
|
||||
if (retrieved) {
|
||||
MemoryStorage::VariableMetaData md;
|
||||
md.characteristics.dataType = v2::DataEnum::string;
|
||||
md.characteristics.supportsMonitoring = false;
|
||||
result = std::move(md);
|
||||
} else {
|
||||
std::cerr << "get_variable_meta_data not implemented for: " << component_id.name << ':' << variable_id.name
|
||||
<< " (" << key_str.value_or("") << ")\n";
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<MemoryStorage::ReportData> MemoryStorage::get_base_report_data(const ReportBaseEnum& report_base) {
|
||||
if (report_base == v2::ReportBaseEnum::ConfigurationInventory) {
|
||||
std::vector<MemoryStorage::ReportData> result;
|
||||
generate_report(result);
|
||||
return result;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<MemoryStorage::ReportData>
|
||||
MemoryStorage::get_custom_report_data(const std::optional<std::vector<ComponentVariable>>& component_variables,
|
||||
const std::optional<std::vector<ComponentCriterionEnum>>& component_criteria) {
|
||||
std::vector<MemoryStorage::ReportData> result;
|
||||
if (component_variables) {
|
||||
for (const auto& component : component_variables.value()) {
|
||||
if (component.variable) {
|
||||
const auto name = ocpp::v16::keys::convert_v2(component.component, component.variable.value(),
|
||||
ocpp::v2::AttributeEnum::Actual);
|
||||
if (name) {
|
||||
const auto retrieved = get_v16(name.value());
|
||||
if (retrieved) {
|
||||
add_to_report(result, name.value(), *retrieved);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
std::vector<MemoryStorage::SetMonitoringResult>
|
||||
MemoryStorage::set_monitors(const std::vector<SetMonitoringData>& requests, const VariableMonitorType type) {
|
||||
return {};
|
||||
}
|
||||
|
||||
bool MemoryStorage::update_monitor_reference(std::int32_t monitor_id, const std::string& reference_value) {
|
||||
return false;
|
||||
}
|
||||
|
||||
std::vector<MemoryStorage::VariableMonitoringPeriodic> MemoryStorage::get_periodic_monitors() {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<MemoryStorage::MonitoringData>
|
||||
MemoryStorage::get_monitors(const std::vector<MonitoringCriterionEnum>& criteria,
|
||||
const std::vector<ComponentVariable>& component_variables) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::vector<MemoryStorage::ClearMonitoringResult> MemoryStorage::clear_monitors(const std::vector<int>& request_ids,
|
||||
bool allow_protected) {
|
||||
return {};
|
||||
}
|
||||
|
||||
std::int32_t MemoryStorage::clear_custom_monitors() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
void MemoryStorage::register_variable_listener(
|
||||
std::function<void(const std::unordered_map<std::int64_t, VariableMonitoringMeta>& monitors,
|
||||
const Component& component, const Variable& variable,
|
||||
const VariableCharacteristics& characteristics, const VariableAttribute& attribute,
|
||||
const std::string& value_previous, const std::string& value_current)>&& listener) {
|
||||
}
|
||||
|
||||
void MemoryStorage::register_monitor_listener(
|
||||
std::function<void(const VariableMonitoringMeta& updated_monitor, const Component& component,
|
||||
const Variable& variable, const VariableCharacteristics& characteristics,
|
||||
const VariableAttribute& attribute, const std::string& current_value)>&& listener) {
|
||||
}
|
||||
|
||||
void MemoryStorage::check_integrity(const std::map<std::int32_t, std::int32_t>& evse_connector_structure) {
|
||||
}
|
||||
|
||||
} // namespace ocpp::v16::stubs
|
||||
@@ -0,0 +1,255 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
// v2 storage provider that uses memory rather than a database
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <ocpp/v16/known_keys.hpp>
|
||||
#include <ocpp/v2/device_model_interface.hpp>
|
||||
#include <ocpp/v2/ocpp_enums.hpp>
|
||||
#include <ocpp/v2/ocpp_types.hpp>
|
||||
|
||||
#include <map>
|
||||
#include <set>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace ocpp::v16::stubs {
|
||||
|
||||
class MemoryStorage : public ocpp::v2::DeviceModelInterface {
|
||||
public:
|
||||
using Storage = std::map<std::string, std::string>;
|
||||
using VariableAttribute = ocpp::v2::VariableAttribute;
|
||||
using Component = ocpp::v2::Component;
|
||||
using ComponentVariable = ocpp::v2::ComponentVariable;
|
||||
using ComponentCriterionEnum = ocpp::v2::ComponentCriterionEnum;
|
||||
using Variable = ocpp::v2::Variable;
|
||||
using AttributeEnum = ocpp::v2::AttributeEnum;
|
||||
using GetVariableStatusEnum = ocpp::v2::GetVariableStatusEnum;
|
||||
using SetVariableStatusEnum = ocpp::v2::SetVariableStatusEnum;
|
||||
using VariableMonitoringMeta = ocpp::v2::VariableMonitoringMeta;
|
||||
using SetMonitoringData = ocpp::v2::SetMonitoringData;
|
||||
using SetMonitoringResult = ocpp::v2::SetMonitoringResult;
|
||||
using VariableMonitorType = ocpp::v2::VariableMonitorType;
|
||||
using VariableMonitoringPeriodic = ocpp::v2::VariableMonitoringPeriodic;
|
||||
using MonitoringCriterionEnum = ocpp::v2::MonitoringCriterionEnum;
|
||||
using MonitoringData = ocpp::v2::MonitoringData;
|
||||
using ClearMonitoringStatusEnum = ocpp::v2::ClearMonitoringStatusEnum;
|
||||
using ClearMonitoringResult = ocpp::v2::ClearMonitoringResult;
|
||||
using MutabilityEnum = ocpp::v2::MutabilityEnum;
|
||||
using VariableCharacteristics = ocpp::v2::VariableCharacteristics;
|
||||
using VariableMetaData = ocpp::v2::VariableMetaData;
|
||||
using ReportData = ocpp::v2::ReportData;
|
||||
using ReportBaseEnum = ocpp::v2::ReportBaseEnum;
|
||||
|
||||
static std::optional<std::string> set_connector_id(std::int32_t id, const std::string& current,
|
||||
const std::string& value);
|
||||
static std::optional<std::string> get_connector_id(std::int32_t id, const std::string& current);
|
||||
|
||||
private:
|
||||
std::set<std::string> read_only;
|
||||
|
||||
std::optional<MemoryStorage::Storage::iterator> locate_v16(const std::string& name) const;
|
||||
std::optional<std::string> get_v16(const std::string& name) const;
|
||||
std::optional<std::string> get_v16(ocpp::v16::keys::valid_keys key) const;
|
||||
SetVariableStatusEnum set_v16(const std::string& name, const std::string& value);
|
||||
SetVariableStatusEnum set_v16_custom(const std::string& name, const std::string& value);
|
||||
std::optional<MutabilityEnum> get_mutability(const std::string& key_str);
|
||||
void add_supported_measureands_values_list(ocpp::v2::ReportData& data);
|
||||
void add_to_report(std::vector<ocpp::v2::ReportData>& report, const std::string_view& name,
|
||||
const std::string_view& value);
|
||||
void add_to_report(std::vector<ocpp::v2::ReportData>& report, const std::string_view& name,
|
||||
const std::map<std::string, std::string>& vars);
|
||||
void generate_report(std::vector<ocpp::v2::ReportData>& report);
|
||||
|
||||
public:
|
||||
MemoryStorage();
|
||||
|
||||
void apply_full_config();
|
||||
|
||||
void set_readonly(const std::string& key);
|
||||
void set(const std::string_view& component, const std::string_view& variable, const std::string_view& value);
|
||||
std::string get(const std::string_view& component, const std::string_view& variable);
|
||||
void clear(const std::string_view& component, const std::string_view& variable);
|
||||
|
||||
GetVariableStatusEnum get_variable(const Component& component_id, const Variable& variable_id,
|
||||
const AttributeEnum& attribute_enum, std::string& value,
|
||||
bool allow_write_only = false) const override;
|
||||
|
||||
SetVariableStatusEnum set_value(const Component& component_id, const Variable& variable_id,
|
||||
const AttributeEnum& attribute_enum, const std::string& value,
|
||||
const std::string& source, bool allow_read_only = false) override;
|
||||
|
||||
SetVariableStatusEnum set_read_only_value(const Component& component_id, const Variable& variable_id,
|
||||
const AttributeEnum& attribute_enum, const std::string& value,
|
||||
const std::string& source) override;
|
||||
|
||||
SetVariableStatusEnum clear_value(const Component& component_id, const Variable& variable_id,
|
||||
const AttributeEnum& attribute_enum, const std::string& source) override;
|
||||
|
||||
std::optional<MutabilityEnum> get_mutability(const Component& component_id, const Variable& variable_id,
|
||||
const AttributeEnum& attribute_enum) override;
|
||||
|
||||
std::optional<VariableMetaData> get_variable_meta_data(const Component& component_id,
|
||||
const Variable& variable_id) override;
|
||||
|
||||
std::vector<ReportData> get_base_report_data(const ReportBaseEnum& report_base) override;
|
||||
|
||||
std::vector<ReportData> get_custom_report_data(
|
||||
const std::optional<std::vector<ComponentVariable>>& component_variables = std::nullopt,
|
||||
const std::optional<std::vector<ComponentCriterionEnum>>& component_criteria = std::nullopt) override;
|
||||
|
||||
std::vector<SetMonitoringResult>
|
||||
set_monitors(const std::vector<SetMonitoringData>& requests,
|
||||
const VariableMonitorType type = VariableMonitorType::CustomMonitor) override;
|
||||
|
||||
bool update_monitor_reference(std::int32_t monitor_id, const std::string& reference_value) override;
|
||||
|
||||
std::vector<VariableMonitoringPeriodic> get_periodic_monitors() override;
|
||||
|
||||
std::vector<MonitoringData> get_monitors(const std::vector<MonitoringCriterionEnum>& criteria,
|
||||
const std::vector<ComponentVariable>& component_variables) override;
|
||||
|
||||
std::vector<ClearMonitoringResult> clear_monitors(const std::vector<int>& request_ids,
|
||||
bool allow_protected = false) override;
|
||||
|
||||
std::int32_t clear_custom_monitors() override;
|
||||
|
||||
void register_variable_listener(
|
||||
std::function<void(const std::unordered_map<std::int64_t, VariableMonitoringMeta>& monitors,
|
||||
const Component& component, const Variable& variable,
|
||||
const VariableCharacteristics& characteristics, const VariableAttribute& attribute,
|
||||
const std::string& value_previous, const std::string& value_current)>&& listener) override;
|
||||
|
||||
void register_monitor_listener(
|
||||
std::function<void(const VariableMonitoringMeta& updated_monitor, const Component& component,
|
||||
const Variable& variable, const VariableCharacteristics& characteristics,
|
||||
const VariableAttribute& attribute, const std::string& current_value)>&& listener) override;
|
||||
|
||||
void check_integrity(const std::map<std::int32_t, std::int32_t>& evse_connector_structure) override;
|
||||
};
|
||||
|
||||
class MemoryStorageProxy : public ocpp::v2::DeviceModelInterface {
|
||||
private:
|
||||
MemoryStorage& storage;
|
||||
|
||||
public:
|
||||
using VariableAttribute = ocpp::v2::VariableAttribute;
|
||||
using Component = ocpp::v2::Component;
|
||||
using ComponentVariable = ocpp::v2::ComponentVariable;
|
||||
using ComponentCriterionEnum = ocpp::v2::ComponentCriterionEnum;
|
||||
using Variable = ocpp::v2::Variable;
|
||||
using AttributeEnum = ocpp::v2::AttributeEnum;
|
||||
using GetVariableStatusEnum = ocpp::v2::GetVariableStatusEnum;
|
||||
using SetVariableStatusEnum = ocpp::v2::SetVariableStatusEnum;
|
||||
using VariableMonitoringMeta = ocpp::v2::VariableMonitoringMeta;
|
||||
using SetMonitoringData = ocpp::v2::SetMonitoringData;
|
||||
using SetMonitoringResult = ocpp::v2::SetMonitoringResult;
|
||||
using VariableMonitorType = ocpp::v2::VariableMonitorType;
|
||||
using VariableMonitoringPeriodic = ocpp::v2::VariableMonitoringPeriodic;
|
||||
using MonitoringCriterionEnum = ocpp::v2::MonitoringCriterionEnum;
|
||||
using MonitoringData = ocpp::v2::MonitoringData;
|
||||
using ClearMonitoringStatusEnum = ocpp::v2::ClearMonitoringStatusEnum;
|
||||
using ClearMonitoringResult = ocpp::v2::ClearMonitoringResult;
|
||||
using MutabilityEnum = ocpp::v2::MutabilityEnum;
|
||||
using VariableCharacteristics = ocpp::v2::VariableCharacteristics;
|
||||
using VariableMetaData = ocpp::v2::VariableMetaData;
|
||||
using ReportData = ocpp::v2::ReportData;
|
||||
using ReportBaseEnum = ocpp::v2::ReportBaseEnum;
|
||||
|
||||
MemoryStorageProxy(MemoryStorage& obj) : storage(obj) {
|
||||
}
|
||||
|
||||
GetVariableStatusEnum get_variable(const Component& component_id, const Variable& variable_id,
|
||||
const AttributeEnum& attribute_enum, std::string& value,
|
||||
bool allow_write_only) const override {
|
||||
return storage.get_variable(component_id, variable_id, attribute_enum, value, allow_write_only);
|
||||
}
|
||||
|
||||
SetVariableStatusEnum set_value(const Component& component_id, const Variable& variable_id,
|
||||
const AttributeEnum& attribute_enum, const std::string& value,
|
||||
const std::string& source, bool allow_read_only) override {
|
||||
return storage.set_value(component_id, variable_id, attribute_enum, value, source, allow_read_only);
|
||||
}
|
||||
|
||||
SetVariableStatusEnum set_read_only_value(const Component& component_id, const Variable& variable_id,
|
||||
const AttributeEnum& attribute_enum, const std::string& value,
|
||||
const std::string& source) override {
|
||||
return storage.set_read_only_value(component_id, variable_id, attribute_enum, value, source);
|
||||
}
|
||||
|
||||
SetVariableStatusEnum clear_value(const Component& component_id, const Variable& variable_id,
|
||||
const AttributeEnum& attribute_enum, const std::string& source) override {
|
||||
return storage.clear_value(component_id, variable_id, attribute_enum, source);
|
||||
}
|
||||
|
||||
std::optional<MutabilityEnum> get_mutability(const Component& component_id, const Variable& variable_id,
|
||||
const AttributeEnum& attribute_enum) override {
|
||||
return storage.get_mutability(component_id, variable_id, attribute_enum);
|
||||
}
|
||||
|
||||
std::optional<VariableMetaData> get_variable_meta_data(const Component& component_id,
|
||||
const Variable& variable_id) override {
|
||||
return storage.get_variable_meta_data(component_id, variable_id);
|
||||
}
|
||||
|
||||
std::vector<ReportData> get_base_report_data(const ReportBaseEnum& report_base) override {
|
||||
return storage.get_base_report_data(report_base);
|
||||
}
|
||||
|
||||
std::vector<ReportData>
|
||||
get_custom_report_data(const std::optional<std::vector<ComponentVariable>>& component_variables,
|
||||
const std::optional<std::vector<ComponentCriterionEnum>>& component_criteria) override {
|
||||
return storage.get_custom_report_data(component_variables, component_criteria);
|
||||
}
|
||||
|
||||
std::vector<SetMonitoringResult>
|
||||
set_monitors(const std::vector<SetMonitoringData>& requests,
|
||||
const VariableMonitorType type = VariableMonitorType::CustomMonitor) override {
|
||||
return storage.set_monitors(requests);
|
||||
}
|
||||
|
||||
bool update_monitor_reference(std::int32_t monitor_id, const std::string& reference_value) override {
|
||||
return storage.update_monitor_reference(monitor_id, reference_value);
|
||||
}
|
||||
|
||||
std::vector<VariableMonitoringPeriodic> get_periodic_monitors() override {
|
||||
return storage.get_periodic_monitors();
|
||||
}
|
||||
|
||||
std::vector<MonitoringData> get_monitors(const std::vector<MonitoringCriterionEnum>& criteria,
|
||||
const std::vector<ComponentVariable>& component_variables) override {
|
||||
return storage.get_monitors(criteria, component_variables);
|
||||
}
|
||||
|
||||
std::vector<ClearMonitoringResult> clear_monitors(const std::vector<int>& request_ids,
|
||||
bool allow_protected) override {
|
||||
return storage.clear_monitors(request_ids, allow_protected);
|
||||
}
|
||||
|
||||
std::int32_t clear_custom_monitors() override {
|
||||
return storage.clear_custom_monitors();
|
||||
}
|
||||
|
||||
void register_variable_listener(
|
||||
std::function<void(const std::unordered_map<std::int64_t, VariableMonitoringMeta>& monitors,
|
||||
const Component& component, const Variable& variable,
|
||||
const VariableCharacteristics& characteristics, const VariableAttribute& attribute,
|
||||
const std::string& value_previous, const std::string& value_current)>&& listener) override {
|
||||
return storage.register_variable_listener(std::move(listener));
|
||||
}
|
||||
|
||||
void register_monitor_listener(
|
||||
std::function<void(const VariableMonitoringMeta& updated_monitor, const Component& component,
|
||||
const Variable& variable, const VariableCharacteristics& characteristics,
|
||||
const VariableAttribute& attribute, const std::string& current_value)>&& listener) override {
|
||||
return storage.register_monitor_listener(std::move(listener));
|
||||
}
|
||||
|
||||
void check_integrity(const std::map<std::int32_t, std::int32_t>& evse_connector_structure) override {
|
||||
return storage.check_integrity(evse_connector_structure);
|
||||
}
|
||||
};
|
||||
|
||||
} // namespace ocpp::v16::stubs
|
||||
@@ -0,0 +1,441 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "configuration_stub.hpp"
|
||||
|
||||
/*
|
||||
"DefaultPrice":
|
||||
{
|
||||
"priceText": "This is the price",
|
||||
"priceTextOffline": "Show this price text when offline!",
|
||||
"chargingPrice":
|
||||
{
|
||||
"kWhPrice": 3.14,
|
||||
"hourPrice": 0.42
|
||||
}
|
||||
},
|
||||
"DefaultPriceText":
|
||||
{
|
||||
"priceTexts":
|
||||
[
|
||||
{
|
||||
"priceText": "This is the price",
|
||||
"priceTextOffline": "Show this price text when offline!",
|
||||
"language": "en"
|
||||
},
|
||||
{
|
||||
"priceText": "Dit is de prijs",
|
||||
"priceTextOffline": "Laat dit zien wanneer de charging station offline is!",
|
||||
"language": "nl"
|
||||
},
|
||||
{
|
||||
"priceText": "Dette er prisen",
|
||||
"priceTextOffline": "Vis denne pristeksten når du er frakoblet",
|
||||
"language": "nb_NO"
|
||||
}
|
||||
]
|
||||
},
|
||||
*/
|
||||
|
||||
namespace {
|
||||
using namespace ocpp::v16::stubs;
|
||||
|
||||
TEST_P(Configuration, CustomDisplayCostAndPriceEnabled) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_FALSE(get()->getCustomDisplayCostAndPriceEnabled());
|
||||
auto kv = get()->getCustomDisplayCostAndPriceEnabledKeyValue();
|
||||
EXPECT_EQ(kv.key, "CustomDisplayCostAndPrice");
|
||||
EXPECT_EQ(kv.value, "false");
|
||||
EXPECT_TRUE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, DefaultTariffMessage) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
auto msg = get()->getDefaultTariffMessage(false);
|
||||
EXPECT_FALSE(msg.ocpp_transaction_id.has_value());
|
||||
EXPECT_FALSE(msg.identifier_id.has_value());
|
||||
EXPECT_FALSE(msg.identifier_type.has_value());
|
||||
EXPECT_TRUE(msg.message.empty());
|
||||
|
||||
msg = get()->getDefaultTariffMessage(true);
|
||||
EXPECT_FALSE(msg.ocpp_transaction_id.has_value());
|
||||
EXPECT_FALSE(msg.identifier_id.has_value());
|
||||
EXPECT_FALSE(msg.identifier_type.has_value());
|
||||
EXPECT_TRUE(msg.message.empty());
|
||||
}
|
||||
|
||||
TEST_P(Configuration, DefaultPrice) {
|
||||
using ConfigurationStatus = ocpp::v16::ConfigurationStatus;
|
||||
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_FALSE(get()->getDefaultPrice().has_value());
|
||||
auto kv = get()->getDefaultPriceKeyValue();
|
||||
ASSERT_FALSE(kv);
|
||||
|
||||
const char* minimal = R"({"priceText":"Default"})";
|
||||
|
||||
auto status = get()->setDefaultPrice(minimal);
|
||||
EXPECT_EQ(status, ConfigurationStatus::Accepted);
|
||||
auto json_result = json::parse(get()->getDefaultPrice().value_or(""));
|
||||
auto result = json_result.dump();
|
||||
EXPECT_EQ(result, minimal);
|
||||
|
||||
kv = get()->getDefaultPriceKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "DefaultPrice");
|
||||
json_result = json::parse(std::string{kv.value().value.value()});
|
||||
result = json_result.dump();
|
||||
EXPECT_EQ(result, minimal);
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, DefaultPriceText) {
|
||||
using ConfigurationStatus = ocpp::v16::ConfigurationStatus;
|
||||
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_FALSE(get()->getDefaultPriceText("en").has_value());
|
||||
auto kv = get()->getDefaultPriceTextKeyValue("en");
|
||||
EXPECT_EQ(kv.key, "DefaultPriceText,en");
|
||||
EXPECT_EQ(kv.value, "");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, DisplayTimeOffset) {
|
||||
using ConfigurationStatus = ocpp::v16::ConfigurationStatus;
|
||||
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_FALSE(get()->getDisplayTimeOffset().has_value());
|
||||
auto kv = get()->getDisplayTimeOffsetKeyValue();
|
||||
ASSERT_FALSE(kv);
|
||||
|
||||
auto status = get()->setDisplayTimeOffset("1:30");
|
||||
EXPECT_EQ(status, ConfigurationStatus::Accepted);
|
||||
EXPECT_EQ(get()->getDisplayTimeOffset(), "1:30");
|
||||
kv = get()->getDisplayTimeOffsetKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "TimeOffset");
|
||||
EXPECT_EQ(kv.value().value, "1:30");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, Language) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_FALSE(get()->getLanguage().has_value());
|
||||
auto kv = get()->getLanguageKeyValue();
|
||||
ASSERT_FALSE(kv);
|
||||
|
||||
get()->setLanguage("de");
|
||||
EXPECT_EQ(get()->getLanguage(), "de");
|
||||
kv = get()->getLanguageKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "Language");
|
||||
EXPECT_EQ(kv.value().value, "de");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, MultiLanguageSupportedLanguages) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_FALSE(get()->getMultiLanguageSupportedLanguages().has_value());
|
||||
auto kv = get()->getMultiLanguageSupportedLanguagesKeyValue();
|
||||
ASSERT_FALSE(kv);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, NextTimeOffsetTransitionDateTime) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_FALSE(get()->getNextTimeOffsetTransitionDateTime().has_value());
|
||||
auto kv = get()->getNextTimeOffsetTransitionDateTimeKeyValue();
|
||||
ASSERT_FALSE(kv);
|
||||
|
||||
get()->setNextTimeOffsetTransitionDateTime("2200-01-01T12:00:00");
|
||||
EXPECT_EQ(get()->getNextTimeOffsetTransitionDateTime(), "2200-01-01T12:00:00");
|
||||
kv = get()->getNextTimeOffsetTransitionDateTimeKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "NextTimeOffsetTransitionDateTime");
|
||||
EXPECT_EQ(kv.value().value, "2200-01-01T12:00:00");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, TimeOffsetNextTransition) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_FALSE(get()->getTimeOffsetNextTransition().has_value());
|
||||
auto kv = get()->getTimeOffsetNextTransitionKeyValue();
|
||||
ASSERT_FALSE(kv);
|
||||
|
||||
get()->setTimeOffsetNextTransition("-01:15");
|
||||
EXPECT_EQ(get()->getTimeOffsetNextTransition(), "-01:15");
|
||||
kv = get()->getTimeOffsetNextTransitionKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "TimeOffsetNextTransition");
|
||||
EXPECT_EQ(kv.value().value, "-01:15");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, CustomIdleFeeAfterStop) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_FALSE(get()->getCustomIdleFeeAfterStop().has_value());
|
||||
auto kv = get()->getCustomIdleFeeAfterStopKeyValue();
|
||||
ASSERT_FALSE(kv);
|
||||
|
||||
get()->setCustomIdleFeeAfterStop(false);
|
||||
EXPECT_TRUE(get()->getCustomIdleFeeAfterStop().has_value());
|
||||
EXPECT_FALSE(get()->getCustomIdleFeeAfterStop().value());
|
||||
kv = get()->getCustomIdleFeeAfterStopKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "CustomIdleFeeAfterStop");
|
||||
EXPECT_EQ(kv.value().value, "false");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, CustomMultiLanguageMessagesEnabled) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_FALSE(get()->getCustomMultiLanguageMessagesEnabled().has_value());
|
||||
auto kv = get()->getCustomMultiLanguageMessagesEnabledKeyValue();
|
||||
ASSERT_FALSE(kv);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, WaitForSetUserPriceTimeout) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_FALSE(get()->getWaitForSetUserPriceTimeout().has_value());
|
||||
auto kv = get()->getWaitForSetUserPriceTimeoutKeyValue();
|
||||
ASSERT_FALSE(kv);
|
||||
|
||||
get()->setWaitForSetUserPriceTimeout(3602);
|
||||
EXPECT_FALSE(get()->getWaitForSetUserPriceTimeout().has_value());
|
||||
kv = get()->getWaitForSetUserPriceTimeoutKeyValue();
|
||||
ASSERT_FALSE(kv);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, PriceNumberOfDecimalsForCostValues) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_FALSE(get()->getPriceNumberOfDecimalsForCostValues().has_value());
|
||||
auto kv = get()->getPriceNumberOfDecimalsForCostValuesKeyValue();
|
||||
ASSERT_FALSE(kv);
|
||||
}
|
||||
|
||||
TEST_P(ConfigurationFull, BadPriceText) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
using ConfigurationStatus = ocpp::v16::ConfigurationStatus;
|
||||
|
||||
// PriceText value is JSON encoded - check that malformed and invalid
|
||||
// messages are correctly handled
|
||||
const char* valid = R"({"priceText":"default"})";
|
||||
const char* invalid = R"({"priceText":null,"priceTextOffline":null,"chargingPrice":null})";
|
||||
|
||||
auto set_result = get()->set("DefaultPrice", valid);
|
||||
EXPECT_TRUE(set_result.has_value());
|
||||
EXPECT_EQ(set_result.value(), ConfigurationStatus::Accepted);
|
||||
|
||||
auto get_result = get()->getDefaultPrice();
|
||||
ASSERT_TRUE(get_result.has_value());
|
||||
auto get_json = json::parse(get_result.value());
|
||||
EXPECT_EQ(get_json["priceText"], "default");
|
||||
|
||||
set_result = get()->set("DefaultPrice", invalid);
|
||||
EXPECT_TRUE(set_result.has_value());
|
||||
EXPECT_EQ(set_result.value(), ConfigurationStatus::Rejected);
|
||||
|
||||
auto get_result2 = get()->getDefaultPrice();
|
||||
ASSERT_TRUE(get_result2.has_value());
|
||||
EXPECT_EQ(get_result2, get_result);
|
||||
get_json = json::parse(get_result.value());
|
||||
EXPECT_EQ(get_json["priceText"], "default");
|
||||
|
||||
auto set_result2 = get()->setDefaultPrice(invalid);
|
||||
EXPECT_EQ(set_result2, ConfigurationStatus::Rejected);
|
||||
|
||||
get_result2 = get()->getDefaultPrice();
|
||||
ASSERT_TRUE(get_result2.has_value());
|
||||
EXPECT_EQ(get_result2, get_result);
|
||||
get_json = json::parse(get_result.value());
|
||||
EXPECT_EQ(get_json["priceText"], "default");
|
||||
}
|
||||
|
||||
TEST_P(ConfigurationFull, DefaultPriceTextEmptyArray) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
using ConfigurationStatus = ocpp::v16::ConfigurationStatus;
|
||||
const char* empty = R"([])";
|
||||
auto set_result = get()->set("DefaultPriceText,en", empty);
|
||||
EXPECT_EQ(set_result, ConfigurationStatus::Rejected);
|
||||
set_result = get()->setDefaultPriceText("DefaultPriceText,en", empty);
|
||||
EXPECT_EQ(set_result, ConfigurationStatus::Rejected);
|
||||
}
|
||||
|
||||
TEST_P(ConfigurationFull, DefaultPriceTextEmptyObject) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
using ConfigurationStatus = ocpp::v16::ConfigurationStatus;
|
||||
const char* empty = R"({})";
|
||||
auto set_result = get()->set("DefaultPriceText,en", empty);
|
||||
EXPECT_EQ(set_result, ConfigurationStatus::Rejected);
|
||||
set_result = get()->setDefaultPriceText("DefaultPriceText,en", empty);
|
||||
EXPECT_EQ(set_result, ConfigurationStatus::Rejected);
|
||||
}
|
||||
|
||||
TEST_P(ConfigurationFull, DefaultPriceInvalid) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
using ConfigurationStatus = ocpp::v16::ConfigurationStatus;
|
||||
const char* minimal = R"("priceText":[])";
|
||||
|
||||
auto set_result = get()->set("DefaultPriceText,en", minimal);
|
||||
EXPECT_EQ(set_result, ConfigurationStatus::Rejected);
|
||||
set_result = get()->setDefaultPriceText("DefaultPriceText,en", minimal);
|
||||
EXPECT_EQ(set_result, ConfigurationStatus::Rejected);
|
||||
}
|
||||
|
||||
TEST_P(ConfigurationFull, DefaultPriceTextMinimal) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
using ConfigurationStatus = ocpp::v16::ConfigurationStatus;
|
||||
const char* minimal = R"({"priceText":"Default"})";
|
||||
|
||||
auto set_result = get()->set("DefaultPriceText,en", minimal);
|
||||
EXPECT_EQ(set_result, ConfigurationStatus::Accepted);
|
||||
set_result = get()->setDefaultPriceText("DefaultPriceText,en", minimal);
|
||||
EXPECT_EQ(set_result, ConfigurationStatus::Accepted);
|
||||
}
|
||||
|
||||
TEST_P(ConfigurationFull, DefaultPriceTextFull) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
using ConfigurationStatus = ocpp::v16::ConfigurationStatus;
|
||||
const char* minimal = R"({"priceText":"Default","priceTextOffline":"Offline"})";
|
||||
|
||||
auto set_result = get()->set("DefaultPriceText,en", minimal);
|
||||
EXPECT_EQ(set_result, ConfigurationStatus::Accepted);
|
||||
set_result = get()->setDefaultPriceText("DefaultPriceText,en", minimal);
|
||||
EXPECT_EQ(set_result, ConfigurationStatus::Accepted);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Oneoff tests where there are differences between implementations
|
||||
// Note: TEST_F and not TEST_P
|
||||
|
||||
TEST_F(Configuration, GetMultiLanguageSupportedLanguagesV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("CostAndPrice", "SupportedLanguages", "en,de");
|
||||
|
||||
EXPECT_TRUE(v2_config->getMultiLanguageSupportedLanguages().has_value());
|
||||
EXPECT_EQ(v2_config->getMultiLanguageSupportedLanguages().value(), "en,de");
|
||||
auto kv = v2_config->getMultiLanguageSupportedLanguagesKeyValue();
|
||||
ASSERT_TRUE(kv.has_value());
|
||||
EXPECT_EQ(kv.value().key, "SupportedLanguages");
|
||||
EXPECT_EQ(kv.value().value, "en,de");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, GetCustomMultiLanguageMessagesEnabledV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("CostAndPrice", "CustomMultiLanguageMessages", "false");
|
||||
|
||||
EXPECT_TRUE(v2_config->getCustomMultiLanguageMessagesEnabled().has_value());
|
||||
EXPECT_FALSE(v2_config->getCustomMultiLanguageMessagesEnabled().value());
|
||||
auto kv = v2_config->getCustomMultiLanguageMessagesEnabledKeyValue();
|
||||
ASSERT_TRUE(kv.has_value());
|
||||
EXPECT_EQ(kv.value().key, "CustomMultiLanguageMessages");
|
||||
EXPECT_EQ(kv.value().value, "false");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetWaitForSetUserPriceTimeoutV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("CostAndPrice", "WaitForSetUserPriceTimeout", "");
|
||||
|
||||
EXPECT_FALSE(v2_config->getWaitForSetUserPriceTimeout().has_value());
|
||||
auto kv = v2_config->getWaitForSetUserPriceTimeoutKeyValue();
|
||||
ASSERT_TRUE(kv.has_value());
|
||||
EXPECT_EQ(kv.value().key, "WaitForSetUserPriceTimeout");
|
||||
EXPECT_EQ(kv.value().value, "");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
|
||||
v2_config->setWaitForSetUserPriceTimeout(3001);
|
||||
EXPECT_TRUE(v2_config->getWaitForSetUserPriceTimeout().has_value());
|
||||
EXPECT_EQ(v2_config->getWaitForSetUserPriceTimeout().value(), 3001);
|
||||
kv = v2_config->getWaitForSetUserPriceTimeoutKeyValue();
|
||||
ASSERT_TRUE(kv.has_value());
|
||||
EXPECT_EQ(kv.value().key, "WaitForSetUserPriceTimeout");
|
||||
EXPECT_EQ(kv.value().value, "3001");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, GetPriceNumberOfDecimalsForCostValuesV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("CostAndPrice", "NumberOfDecimalsForCostValues", "3");
|
||||
|
||||
EXPECT_TRUE(v2_config->getPriceNumberOfDecimalsForCostValues().has_value());
|
||||
EXPECT_EQ(v2_config->getPriceNumberOfDecimalsForCostValues().value(), 3);
|
||||
auto kv = v2_config->getPriceNumberOfDecimalsForCostValuesKeyValue();
|
||||
ASSERT_TRUE(kv.has_value());
|
||||
EXPECT_EQ(kv.value().key, "NumberOfDecimalsForCostValues");
|
||||
EXPECT_EQ(kv.value().value, "3");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetDefaultPriceTextV2) {
|
||||
using ConfigurationStatus = ocpp::v16::ConfigurationStatus;
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("CostAndPrice", "NumberOfDecimalsForCostValues", "3");
|
||||
|
||||
EXPECT_FALSE(v2_config->getDefaultPriceText("en").has_value());
|
||||
auto kv = v2_config->getDefaultPriceTextKeyValue("en");
|
||||
EXPECT_EQ(kv.key, "DefaultPriceText,en");
|
||||
EXPECT_EQ(kv.value, "");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
// needs config item for multi language support
|
||||
// getMultiLanguageSupportedLanguages()
|
||||
device_model->set("CostAndPrice", "SupportedLanguages", "en,de");
|
||||
|
||||
const char* value_en = R"({
|
||||
"priceText": "This is the price",
|
||||
"priceTextOffline": "Show this price text when offline!"
|
||||
})";
|
||||
const char* value_de = R"({
|
||||
"priceText": "Das ist der Preis",
|
||||
"priceTextOffline": "Diesen Preistext anzeigen, wenn Sie offline sind!"
|
||||
})";
|
||||
|
||||
auto status = v2_config->setDefaultPriceText("DefaultPriceText,en", value_en);
|
||||
EXPECT_EQ(status, ConfigurationStatus::Accepted);
|
||||
EXPECT_EQ(v2_config->getDefaultPriceText("en"), value_en);
|
||||
kv = v2_config->getDefaultPriceTextKeyValue("en");
|
||||
EXPECT_EQ(kv.key, "DefaultPriceText,en");
|
||||
ASSERT_TRUE(kv.value.has_value());
|
||||
EXPECT_EQ(kv.value.value(), value_en);
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
status = v2_config->setDefaultPriceText("DefaultPriceText,de", value_de);
|
||||
EXPECT_EQ(status, ConfigurationStatus::Accepted);
|
||||
EXPECT_EQ(v2_config->getDefaultPriceText("de"), value_de);
|
||||
kv = v2_config->getDefaultPriceTextKeyValue("de");
|
||||
EXPECT_EQ(kv.key, "DefaultPriceText,de");
|
||||
ASSERT_TRUE(kv.value.has_value());
|
||||
EXPECT_EQ(kv.value.value(), value_de);
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
auto list = v2_config->getAllDefaultPriceTextKeyValues();
|
||||
ASSERT_TRUE(list);
|
||||
ASSERT_EQ(list.value().size(), 2);
|
||||
const auto& lv = list.value();
|
||||
|
||||
int en_index = 0;
|
||||
int de_index = 1;
|
||||
if (lv[0].key == "DefaultPriceText,de") {
|
||||
en_index = 1;
|
||||
de_index = 0;
|
||||
}
|
||||
|
||||
EXPECT_EQ(lv[en_index].key, "DefaultPriceText,en");
|
||||
ASSERT_TRUE(lv[en_index].value.has_value());
|
||||
EXPECT_EQ(lv[en_index].value.value(), value_en);
|
||||
EXPECT_FALSE(lv[en_index].readonly);
|
||||
|
||||
EXPECT_EQ(lv[de_index].key, "DefaultPriceText,de");
|
||||
ASSERT_TRUE(lv[de_index].value.has_value());
|
||||
EXPECT_EQ(lv[de_index].value.value(), value_de);
|
||||
EXPECT_FALSE(lv[de_index].readonly);
|
||||
}
|
||||
} // namespace
|
||||
@@ -0,0 +1,183 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "configuration_stub.hpp"
|
||||
#include "ocpp/v16/known_keys.hpp"
|
||||
#include "ocpp/v2/ocpp_enums.hpp"
|
||||
#include "ocpp/v2/ocpp_types.hpp"
|
||||
#include <ocpp/v16/charge_point_configuration_base.hpp>
|
||||
#include <optional>
|
||||
|
||||
namespace {
|
||||
using namespace ocpp::v16::stubs;
|
||||
|
||||
// run tests against V16 JSON and V2 database
|
||||
// gtest_filter: Config/Configuration.*
|
||||
INSTANTIATE_TEST_SUITE_P(Config, Configuration, testing::Values("sql", "json"));
|
||||
INSTANTIATE_TEST_SUITE_P(Config, ConfigurationFull, testing::Values("sql", "json"));
|
||||
|
||||
TEST(ConnectorID, Extract) {
|
||||
using CPCB = ocpp::v16::ChargePointConfigurationBase;
|
||||
|
||||
EXPECT_EQ(CPCB::extractConnectorIdFromMeterPublicKey(""), std::nullopt);
|
||||
EXPECT_EQ(CPCB::extractConnectorIdFromMeterPublicKey("1234"), std::nullopt);
|
||||
EXPECT_EQ(CPCB::extractConnectorIdFromMeterPublicKey("A"), std::nullopt);
|
||||
EXPECT_EQ(CPCB::extractConnectorIdFromMeterPublicKey("ABC"), std::nullopt);
|
||||
EXPECT_EQ(CPCB::extractConnectorIdFromMeterPublicKey("A1"), std::nullopt);
|
||||
EXPECT_EQ(CPCB::extractConnectorIdFromMeterPublicKey("A12"), std::nullopt);
|
||||
EXPECT_EQ(CPCB::extractConnectorIdFromMeterPublicKey("A123"), std::nullopt);
|
||||
EXPECT_EQ(CPCB::extractConnectorIdFromMeterPublicKey("MeterPublicKeyMeterPublicKey123"), std::nullopt);
|
||||
EXPECT_EQ(CPCB::extractConnectorIdFromMeterPublicKey("MeterPublicKey1"), 1);
|
||||
EXPECT_EQ(CPCB::extractConnectorIdFromMeterPublicKey("MeterPublicKey12"), 12);
|
||||
EXPECT_EQ(CPCB::extractConnectorIdFromMeterPublicKey("MeterPublicKey123"), 123);
|
||||
}
|
||||
|
||||
TEST(ConnectorID, Build) {
|
||||
using CPCB = ocpp::v16::ChargePointConfigurationBase;
|
||||
|
||||
EXPECT_EQ(CPCB::meterPublicKeyString(0), "MeterPublicKey0");
|
||||
EXPECT_EQ(CPCB::meterPublicKeyString(1), "MeterPublicKey1");
|
||||
EXPECT_EQ(CPCB::meterPublicKeyString(12), "MeterPublicKey12");
|
||||
EXPECT_EQ(CPCB::meterPublicKeyString(123), "MeterPublicKey123");
|
||||
}
|
||||
|
||||
TEST(ConnectorID, PhaseRotation) {
|
||||
using CPCB = ocpp::v16::ChargePointConfigurationBase;
|
||||
const char* no_phase_rotation = "0.NotApplicable,1.Unknown,2.NotApplicable,3.Unknown";
|
||||
const char* valid_phase_rotation = "0.RST,1.RTS,2.SRT,3.STR,4.TRS,5.TSR";
|
||||
const char* valid_phase_rotation_extended = "8.RST,9.RTS,10.SRT,11.STR,12.TRS,13.TSR";
|
||||
|
||||
EXPECT_TRUE(CPCB::isConnectorPhaseRotationValid(5, no_phase_rotation));
|
||||
EXPECT_FALSE(CPCB::isConnectorPhaseRotationValid(1, no_phase_rotation));
|
||||
|
||||
EXPECT_TRUE(CPCB::isConnectorPhaseRotationValid(5, valid_phase_rotation));
|
||||
EXPECT_FALSE(CPCB::isConnectorPhaseRotationValid(3, valid_phase_rotation));
|
||||
|
||||
EXPECT_TRUE(CPCB::isConnectorPhaseRotationValid(15, valid_phase_rotation_extended));
|
||||
EXPECT_FALSE(CPCB::isConnectorPhaseRotationValid(11, valid_phase_rotation_extended));
|
||||
|
||||
EXPECT_TRUE(CPCB::isConnectorPhaseRotationValid(5, ""));
|
||||
|
||||
// error cases
|
||||
EXPECT_FALSE(CPCB::isConnectorPhaseRotationValid(5, "123456"));
|
||||
EXPECT_FALSE(CPCB::isConnectorPhaseRotationValid(5, "abcdef"));
|
||||
EXPECT_FALSE(CPCB::isConnectorPhaseRotationValid(5, ".abcd"));
|
||||
EXPECT_FALSE(CPCB::isConnectorPhaseRotationValid(5, "abcd."));
|
||||
EXPECT_FALSE(CPCB::isConnectorPhaseRotationValid(5, "1."));
|
||||
EXPECT_FALSE(CPCB::isConnectorPhaseRotationValid(5, "11."));
|
||||
EXPECT_FALSE(CPCB::isConnectorPhaseRotationValid(5, "111."));
|
||||
EXPECT_FALSE(CPCB::isConnectorPhaseRotationValid(5, "1a.RST"));
|
||||
}
|
||||
|
||||
using namespace ocpp;
|
||||
|
||||
TEST(V2Mapping, V16ToV2) {
|
||||
using namespace ocpp::v16::keys;
|
||||
using namespace ocpp::v2;
|
||||
|
||||
auto res = convert_v2(valid_keys::CpoName);
|
||||
ASSERT_TRUE(res);
|
||||
auto component = std::get<Component>(res.value());
|
||||
auto variable = std::get<Variable>(res.value());
|
||||
EXPECT_EQ(component.name, "SecurityCtrlr");
|
||||
EXPECT_EQ(variable.name, "OrganizationName");
|
||||
|
||||
res = convert_v2("CpoName");
|
||||
ASSERT_TRUE(res);
|
||||
component = std::get<Component>(res.value());
|
||||
variable = std::get<Variable>(res.value());
|
||||
EXPECT_EQ(component.name, "SecurityCtrlr");
|
||||
EXPECT_EQ(variable.name, "OrganizationName");
|
||||
}
|
||||
|
||||
TEST(V2Mapping, V2ToV16) {
|
||||
using namespace ocpp::v16::keys;
|
||||
using namespace ocpp::v2;
|
||||
|
||||
Component comp;
|
||||
comp.name = "SecurityCtrlr";
|
||||
Variable var;
|
||||
var.name = "OrganizationName";
|
||||
auto res = convert_v2(comp, var, ocpp::v2::AttributeEnum::Actual);
|
||||
EXPECT_EQ(res, "CpoName");
|
||||
|
||||
// MaxChargingProfilesInstalled maps to VariableCharacteristics.maxLimit
|
||||
comp.name = "SmartChargingCtrlr";
|
||||
var.name = "Entries";
|
||||
var.instance = "ChargingProfiles";
|
||||
EXPECT_FALSE(convert_v2(comp, var, ocpp::v2::AttributeEnum::Actual));
|
||||
EXPECT_FALSE(convert_v2(comp, var, ocpp::v2::AttributeEnum::MaxSet));
|
||||
|
||||
// Instead it is reachable via convert_v2_max_limit.
|
||||
const auto max_limit = convert_v2_max_limit(valid_keys::MaxChargingProfilesInstalled);
|
||||
ASSERT_TRUE(max_limit);
|
||||
EXPECT_EQ(max_limit->first.name, "SmartChargingCtrlr");
|
||||
EXPECT_EQ(max_limit->second.name, "Entries");
|
||||
EXPECT_EQ(max_limit->second.instance, "ChargingProfiles");
|
||||
}
|
||||
|
||||
// Tests for get_all_key_value() with maxLimit keys.
|
||||
// These keys map to VariableCharacteristics.maxLimit rather than VariableAttribute,
|
||||
// so they are populated via a separate code path in get_all_key_value().
|
||||
class MaxLimitGetAll : public ConfigurationBase {};
|
||||
|
||||
TEST_F(MaxLimitGetAll, MaxLimitKeysIncludedWhenSet) {
|
||||
ASSERT_TRUE(device_model);
|
||||
device_model->set("Core", "MeterValuesAlignedDataMaxLength", "10");
|
||||
device_model->set("Core", "MeterValuesSampledDataMaxLength", "20");
|
||||
device_model->set("Core", "StopTxnAlignedDataMaxLength", "30");
|
||||
device_model->set("Core", "StopTxnSampledDataMaxLength", "40");
|
||||
device_model->set("LocalAuthListManagement", "LocalAuthListMaxLength", "50");
|
||||
device_model->set("LocalAuthListManagement", "SendLocalListMaxLength", "60");
|
||||
device_model->set("SmartCharging", "MaxChargingProfilesInstalled", "70");
|
||||
|
||||
const auto all = v2_config->get_all_key_value();
|
||||
const auto find = [&](const std::string& key) {
|
||||
return std::find_if(all.begin(), all.end(), [&](const auto& kv) { return kv.key == key.c_str(); });
|
||||
};
|
||||
|
||||
struct Expected {
|
||||
const char* key;
|
||||
const char* value;
|
||||
};
|
||||
const Expected expected[] = {
|
||||
{"MeterValuesAlignedDataMaxLength", "10"}, {"MeterValuesSampledDataMaxLength", "20"},
|
||||
{"StopTxnAlignedDataMaxLength", "30"}, {"StopTxnSampledDataMaxLength", "40"},
|
||||
{"LocalAuthListMaxLength", "50"}, {"SendLocalListMaxLength", "60"},
|
||||
{"MaxChargingProfilesInstalled", "70"},
|
||||
};
|
||||
|
||||
for (const auto& e : expected) {
|
||||
const auto it = find(e.key);
|
||||
ASSERT_NE(it, all.end()) << e.key << " missing from get_all_key_value()";
|
||||
EXPECT_EQ(it->value, e.value) << e.key;
|
||||
EXPECT_TRUE(it->readonly) << e.key << " should be readonly";
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(MaxLimitGetAll, MaxLimitKeysAbsentWhenNotSet) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// Do not configure any maxLimit keys — they should not appear in the result.
|
||||
const auto all = v2_config->get_all_key_value();
|
||||
const auto find = [&](const std::string& key) {
|
||||
return std::find_if(all.begin(), all.end(), [&](const auto& kv) { return kv.key == key.c_str(); });
|
||||
};
|
||||
|
||||
// LocalAuthListMaxLength, SendLocalListMaxLength, MaxChargingProfilesInstalled are present because they are part of
|
||||
// the example config
|
||||
|
||||
const char* keys[] = {
|
||||
"MeterValuesAlignedDataMaxLength",
|
||||
"MeterValuesSampledDataMaxLength",
|
||||
"StopTxnAlignedDataMaxLength",
|
||||
"StopTxnSampledDataMaxLength",
|
||||
};
|
||||
|
||||
for (const auto* key : keys) {
|
||||
EXPECT_EQ(find(key), all.end()) << key << " should be absent when maxLimit is not set";
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,788 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <optional>
|
||||
|
||||
#include "configuration_stub.hpp"
|
||||
#include "ocpp/v16/types.hpp"
|
||||
|
||||
namespace {
|
||||
using namespace ocpp::v16::stubs;
|
||||
|
||||
TEST_P(Configuration, ConnectorPhaseRotation) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getConnectorPhaseRotation(), "0.RST,1.RST");
|
||||
auto kv = get()->getConnectorPhaseRotationKeyValue();
|
||||
EXPECT_EQ(kv.key, "ConnectorPhaseRotation");
|
||||
EXPECT_EQ(kv.value, "0.RST,1.RST");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
get()->setConnectorPhaseRotation("0.TRS,1.TRS");
|
||||
EXPECT_EQ(get()->getConnectorPhaseRotation(), "0.TRS,1.TRS");
|
||||
kv = get()->getConnectorPhaseRotationKeyValue();
|
||||
EXPECT_EQ(kv.key, "ConnectorPhaseRotation");
|
||||
EXPECT_EQ(kv.value, "0.TRS,1.TRS");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, MeterValuesAlignedData) {
|
||||
using Measurand = ocpp::v16::Measurand;
|
||||
using Phase = ocpp::v16::Phase;
|
||||
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getMeterValuesAlignedData(), "Energy.Active.Import.Register");
|
||||
auto kv = get()->getMeterValuesAlignedDataKeyValue();
|
||||
EXPECT_EQ(kv.key, "MeterValuesAlignedData");
|
||||
EXPECT_EQ(kv.value, "Energy.Active.Import.Register");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
auto vec = get()->getMeterValuesAlignedDataVector();
|
||||
ASSERT_EQ(vec.size(), 4);
|
||||
EXPECT_EQ(vec[0].measurand, Measurand::Energy_Active_Import_Register);
|
||||
EXPECT_EQ(vec[0].phase, std::nullopt);
|
||||
EXPECT_EQ(vec[1].measurand, Measurand::Energy_Active_Import_Register);
|
||||
EXPECT_EQ(vec[1].phase, Phase::L1);
|
||||
EXPECT_EQ(vec[2].measurand, Measurand::Energy_Active_Import_Register);
|
||||
EXPECT_EQ(vec[2].phase, Phase::L2);
|
||||
EXPECT_EQ(vec[3].measurand, Measurand::Energy_Active_Import_Register);
|
||||
EXPECT_EQ(vec[3].phase, Phase::L3);
|
||||
|
||||
get()->setMeterValuesAlignedData("Current.Import");
|
||||
EXPECT_EQ(get()->getMeterValuesAlignedData(), "Current.Import");
|
||||
kv = get()->getMeterValuesAlignedDataKeyValue();
|
||||
EXPECT_EQ(kv.key, "MeterValuesAlignedData");
|
||||
EXPECT_EQ(kv.value, "Current.Import");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
vec = get()->getMeterValuesAlignedDataVector();
|
||||
ASSERT_EQ(vec.size(), 5);
|
||||
EXPECT_EQ(vec[0].measurand, Measurand::Current_Import);
|
||||
EXPECT_EQ(vec[0].phase, std::nullopt);
|
||||
EXPECT_EQ(vec[1].measurand, Measurand::Current_Import);
|
||||
EXPECT_EQ(vec[1].phase, Phase::L1);
|
||||
EXPECT_EQ(vec[2].measurand, Measurand::Current_Import);
|
||||
EXPECT_EQ(vec[2].phase, Phase::L2);
|
||||
EXPECT_EQ(vec[3].measurand, Measurand::Current_Import);
|
||||
EXPECT_EQ(vec[3].phase, Phase::L3);
|
||||
EXPECT_EQ(vec[4].measurand, Measurand::Current_Import);
|
||||
EXPECT_EQ(vec[4].phase, Phase::N);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, MeterValuesSampledData) {
|
||||
using Measurand = ocpp::v16::Measurand;
|
||||
using Phase = ocpp::v16::Phase;
|
||||
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getMeterValuesSampledData(), "Energy.Active.Import.Register");
|
||||
auto kv = get()->getMeterValuesSampledDataKeyValue();
|
||||
EXPECT_EQ(kv.key, "MeterValuesSampledData");
|
||||
EXPECT_EQ(kv.value, "Energy.Active.Import.Register");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
auto vec = get()->getMeterValuesSampledDataVector();
|
||||
ASSERT_EQ(vec.size(), 4);
|
||||
EXPECT_EQ(vec[0].measurand, Measurand::Energy_Active_Import_Register);
|
||||
EXPECT_EQ(vec[0].phase, std::nullopt);
|
||||
EXPECT_EQ(vec[1].measurand, Measurand::Energy_Active_Import_Register);
|
||||
EXPECT_EQ(vec[1].phase, Phase::L1);
|
||||
EXPECT_EQ(vec[2].measurand, Measurand::Energy_Active_Import_Register);
|
||||
EXPECT_EQ(vec[2].phase, Phase::L2);
|
||||
EXPECT_EQ(vec[3].measurand, Measurand::Energy_Active_Import_Register);
|
||||
EXPECT_EQ(vec[3].phase, Phase::L3);
|
||||
|
||||
get()->setMeterValuesSampledData("Energy.Active.Import.Register,Energy.Active.Export.Register");
|
||||
EXPECT_EQ(get()->getMeterValuesSampledData(), "Energy.Active.Import.Register,Energy.Active.Export.Register");
|
||||
kv = get()->getMeterValuesSampledDataKeyValue();
|
||||
EXPECT_EQ(kv.key, "MeterValuesSampledData");
|
||||
EXPECT_EQ(kv.value, "Energy.Active.Import.Register,Energy.Active.Export.Register");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
vec = get()->getMeterValuesSampledDataVector();
|
||||
ASSERT_EQ(vec.size(), 8);
|
||||
EXPECT_EQ(vec[0].measurand, Measurand::Energy_Active_Import_Register);
|
||||
EXPECT_EQ(vec[0].phase, std::nullopt);
|
||||
EXPECT_EQ(vec[1].measurand, Measurand::Energy_Active_Import_Register);
|
||||
EXPECT_EQ(vec[1].phase, Phase::L1);
|
||||
EXPECT_EQ(vec[2].measurand, Measurand::Energy_Active_Import_Register);
|
||||
EXPECT_EQ(vec[2].phase, Phase::L2);
|
||||
EXPECT_EQ(vec[3].measurand, Measurand::Energy_Active_Import_Register);
|
||||
EXPECT_EQ(vec[3].phase, Phase::L3);
|
||||
EXPECT_EQ(vec[4].measurand, Measurand::Energy_Active_Export_Register);
|
||||
EXPECT_EQ(vec[4].phase, std::nullopt);
|
||||
EXPECT_EQ(vec[5].measurand, Measurand::Energy_Active_Export_Register);
|
||||
EXPECT_EQ(vec[5].phase, Phase::L1);
|
||||
EXPECT_EQ(vec[6].measurand, Measurand::Energy_Active_Export_Register);
|
||||
EXPECT_EQ(vec[6].phase, Phase::L2);
|
||||
EXPECT_EQ(vec[7].measurand, Measurand::Energy_Active_Export_Register);
|
||||
EXPECT_EQ(vec[7].phase, Phase::L3);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, StopTxnAlignedData) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getStopTxnAlignedData(), "Energy.Active.Import.Register");
|
||||
auto kv = get()->getStopTxnAlignedDataKeyValue();
|
||||
EXPECT_EQ(kv.key, "StopTxnAlignedData");
|
||||
EXPECT_EQ(kv.value, "Energy.Active.Import.Register");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
get()->setStopTxnAlignedData("Voltage");
|
||||
EXPECT_EQ(get()->getStopTxnAlignedData(), "Voltage");
|
||||
kv = get()->getStopTxnAlignedDataKeyValue();
|
||||
EXPECT_EQ(kv.key, "StopTxnAlignedData");
|
||||
EXPECT_EQ(kv.value, "Voltage");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, StopTxnSampledData) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getStopTxnSampledData(), "Energy.Active.Import.Register");
|
||||
auto kv = get()->getStopTxnSampledDataKeyValue();
|
||||
EXPECT_EQ(kv.key, "StopTxnSampledData");
|
||||
EXPECT_EQ(kv.value, "Energy.Active.Import.Register");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
get()->setStopTxnSampledData("Frequency,Current.Offered");
|
||||
EXPECT_EQ(get()->getStopTxnSampledData(), "Frequency,Current.Offered");
|
||||
kv = get()->getStopTxnSampledDataKeyValue();
|
||||
EXPECT_EQ(kv.key, "StopTxnSampledData");
|
||||
EXPECT_EQ(kv.value, "Frequency,Current.Offered");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, SupportedFeatureProfiles) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getSupportedFeatureProfiles(),
|
||||
"Core,FirmwareManagement,RemoteTrigger,Reservation,LocalAuthListManagement,SmartCharging");
|
||||
auto kv = get()->getSupportedFeatureProfilesKeyValue();
|
||||
EXPECT_EQ(kv.key, "SupportedFeatureProfiles");
|
||||
EXPECT_EQ(kv.value, "Core,FirmwareManagement,RemoteTrigger,Reservation,LocalAuthListManagement,SmartCharging");
|
||||
EXPECT_TRUE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, AuthorizeRemoteTxRequests) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getAuthorizeRemoteTxRequests(), false);
|
||||
auto kv = get()->getAuthorizeRemoteTxRequestsKeyValue();
|
||||
EXPECT_EQ(kv.key, "AuthorizeRemoteTxRequests");
|
||||
EXPECT_EQ(kv.value, "false");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
get()->setAuthorizeRemoteTxRequests(true);
|
||||
EXPECT_EQ(get()->getAuthorizeRemoteTxRequests(), true);
|
||||
kv = get()->getAuthorizeRemoteTxRequestsKeyValue();
|
||||
EXPECT_EQ(kv.key, "AuthorizeRemoteTxRequests");
|
||||
EXPECT_EQ(kv.value, "true");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, LocalAuthorizeOffline) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getLocalAuthorizeOffline(), false);
|
||||
auto kv = get()->getLocalAuthorizeOfflineKeyValue();
|
||||
EXPECT_EQ(kv.key, "LocalAuthorizeOffline");
|
||||
EXPECT_EQ(kv.value, "false");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
get()->setLocalAuthorizeOffline(true);
|
||||
EXPECT_EQ(get()->getLocalAuthorizeOffline(), true);
|
||||
kv = get()->getLocalAuthorizeOfflineKeyValue();
|
||||
EXPECT_EQ(kv.key, "LocalAuthorizeOffline");
|
||||
EXPECT_EQ(kv.value, "true");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, LocalPreAuthorize) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getLocalPreAuthorize(), false);
|
||||
auto kv = get()->getLocalPreAuthorizeKeyValue();
|
||||
EXPECT_EQ(kv.key, "LocalPreAuthorize");
|
||||
EXPECT_EQ(kv.value, "false");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
get()->setLocalPreAuthorize(true);
|
||||
EXPECT_EQ(get()->getLocalPreAuthorize(), true);
|
||||
kv = get()->getLocalPreAuthorizeKeyValue();
|
||||
EXPECT_EQ(kv.key, "LocalPreAuthorize");
|
||||
EXPECT_EQ(kv.value, "true");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, StopTransactionOnInvalidId) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getStopTransactionOnInvalidId(), true);
|
||||
auto kv = get()->getStopTransactionOnInvalidIdKeyValue();
|
||||
EXPECT_EQ(kv.key, "StopTransactionOnInvalidId");
|
||||
EXPECT_EQ(kv.value, "true");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
get()->setStopTransactionOnInvalidId(false);
|
||||
EXPECT_EQ(get()->getStopTransactionOnInvalidId(), false);
|
||||
kv = get()->getStopTransactionOnInvalidIdKeyValue();
|
||||
EXPECT_EQ(kv.key, "StopTransactionOnInvalidId");
|
||||
EXPECT_EQ(kv.value, "false");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, UnlockConnectorOnEVSideDisconnect) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getUnlockConnectorOnEVSideDisconnect(), true);
|
||||
auto kv = get()->getUnlockConnectorOnEVSideDisconnectKeyValue();
|
||||
EXPECT_EQ(kv.key, "UnlockConnectorOnEVSideDisconnect");
|
||||
EXPECT_EQ(kv.value, "true");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
get()->setUnlockConnectorOnEVSideDisconnect(false);
|
||||
EXPECT_EQ(get()->getUnlockConnectorOnEVSideDisconnect(), false);
|
||||
kv = get()->getUnlockConnectorOnEVSideDisconnectKeyValue();
|
||||
EXPECT_EQ(kv.key, "UnlockConnectorOnEVSideDisconnect");
|
||||
EXPECT_EQ(kv.value, "false");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, ClockAlignedDataInterval) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getClockAlignedDataInterval(), 900);
|
||||
auto kv = get()->getClockAlignedDataIntervalKeyValue();
|
||||
EXPECT_EQ(kv.key, "ClockAlignedDataInterval");
|
||||
EXPECT_EQ(kv.value, "900");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
get()->setClockAlignedDataInterval(5200);
|
||||
EXPECT_EQ(get()->getClockAlignedDataInterval(), 5200);
|
||||
kv = get()->getClockAlignedDataIntervalKeyValue();
|
||||
EXPECT_EQ(kv.key, "ClockAlignedDataInterval");
|
||||
EXPECT_EQ(kv.value, "5200");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, ConnectionTimeOut) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getConnectionTimeOut(), 10);
|
||||
auto kv = get()->getConnectionTimeOutKeyValue();
|
||||
EXPECT_EQ(kv.key, "ConnectionTimeOut");
|
||||
EXPECT_EQ(kv.value, "10");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
get()->setConnectionTimeOut(60);
|
||||
EXPECT_EQ(get()->getConnectionTimeOut(), 60);
|
||||
kv = get()->getConnectionTimeOutKeyValue();
|
||||
EXPECT_EQ(kv.key, "ConnectionTimeOut");
|
||||
EXPECT_EQ(kv.value, "60");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, GetConfigurationMaxKeys) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getGetConfigurationMaxKeys(), 100);
|
||||
auto kv = get()->getGetConfigurationMaxKeysKeyValue();
|
||||
EXPECT_EQ(kv.key, "GetConfigurationMaxKeys");
|
||||
EXPECT_EQ(kv.value, "100");
|
||||
EXPECT_TRUE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, HeartbeatInterval) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getHeartbeatInterval(), 86400);
|
||||
auto kv = get()->getHeartbeatIntervalKeyValue();
|
||||
EXPECT_EQ(kv.key, "HeartbeatInterval");
|
||||
EXPECT_EQ(kv.value, "86400");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
get()->setHeartbeatInterval(70);
|
||||
EXPECT_EQ(get()->getHeartbeatInterval(), 70);
|
||||
kv = get()->getHeartbeatIntervalKeyValue();
|
||||
EXPECT_EQ(kv.key, "HeartbeatInterval");
|
||||
EXPECT_EQ(kv.value, "70");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, MeterValueSampleInterval) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getMeterValueSampleInterval(), 0);
|
||||
auto kv = get()->getMeterValueSampleIntervalKeyValue();
|
||||
EXPECT_EQ(kv.key, "MeterValueSampleInterval");
|
||||
EXPECT_EQ(kv.value, "0");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
get()->setMeterValueSampleInterval(125);
|
||||
EXPECT_EQ(get()->getMeterValueSampleInterval(), 125);
|
||||
kv = get()->getMeterValueSampleIntervalKeyValue();
|
||||
EXPECT_EQ(kv.key, "MeterValueSampleInterval");
|
||||
EXPECT_EQ(kv.value, "125");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, NumberOfConnectors) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getNumberOfConnectors(), 1);
|
||||
auto kv = get()->getNumberOfConnectorsKeyValue();
|
||||
EXPECT_EQ(kv.key, "NumberOfConnectors");
|
||||
EXPECT_EQ(kv.value, "1");
|
||||
EXPECT_TRUE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, ResetRetries) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getResetRetries(), 1);
|
||||
auto kv = get()->getResetRetriesKeyValue();
|
||||
EXPECT_EQ(kv.key, "ResetRetries");
|
||||
EXPECT_EQ(kv.value, "1");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
get()->setResetRetries(7);
|
||||
EXPECT_EQ(get()->getResetRetries(), 7);
|
||||
kv = get()->getResetRetriesKeyValue();
|
||||
EXPECT_EQ(kv.key, "ResetRetries");
|
||||
EXPECT_EQ(kv.value, "7");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, TransactionMessageAttempts) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getTransactionMessageAttempts(), 1);
|
||||
auto kv = get()->getTransactionMessageAttemptsKeyValue();
|
||||
EXPECT_EQ(kv.key, "TransactionMessageAttempts");
|
||||
EXPECT_EQ(kv.value, "1");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
get()->setTransactionMessageAttempts(4);
|
||||
EXPECT_EQ(get()->getTransactionMessageAttempts(), 4);
|
||||
kv = get()->getTransactionMessageAttemptsKeyValue();
|
||||
EXPECT_EQ(kv.key, "TransactionMessageAttempts");
|
||||
EXPECT_EQ(kv.value, "4");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, TransactionMessageRetryInterval) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getTransactionMessageRetryInterval(), 10);
|
||||
auto kv = get()->getTransactionMessageRetryIntervalKeyValue();
|
||||
EXPECT_EQ(kv.key, "TransactionMessageRetryInterval");
|
||||
EXPECT_EQ(kv.value, "10");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
get()->setTransactionMessageRetryInterval(1250);
|
||||
EXPECT_EQ(get()->getTransactionMessageRetryInterval(), 1250);
|
||||
kv = get()->getTransactionMessageRetryIntervalKeyValue();
|
||||
EXPECT_EQ(kv.key, "TransactionMessageRetryInterval");
|
||||
EXPECT_EQ(kv.value, "1250");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, AllowOfflineTxForUnknownId) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// No initial value set - hence set() doesn't work
|
||||
|
||||
EXPECT_FALSE(get()->getAllowOfflineTxForUnknownId().has_value());
|
||||
auto kv = get()->getAllowOfflineTxForUnknownIdKeyValue();
|
||||
ASSERT_FALSE(kv.has_value());
|
||||
|
||||
// set only works when there is a value configured
|
||||
get()->setAllowOfflineTxForUnknownId(true);
|
||||
EXPECT_FALSE(get()->getAllowOfflineTxForUnknownId().has_value());
|
||||
kv = get()->getAllowOfflineTxForUnknownIdKeyValue();
|
||||
ASSERT_FALSE(kv.has_value());
|
||||
}
|
||||
|
||||
TEST_P(Configuration, AuthorizationCacheEnabled) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// No initial value set - hence set() doesn't work
|
||||
|
||||
EXPECT_FALSE(get()->getAuthorizationCacheEnabled().has_value());
|
||||
auto kv = get()->getAuthorizationCacheEnabledKeyValue();
|
||||
ASSERT_FALSE(kv.has_value());
|
||||
|
||||
// set only works when there is a value configured
|
||||
get()->setAuthorizationCacheEnabled(true);
|
||||
EXPECT_FALSE(get()->getAuthorizationCacheEnabled().has_value());
|
||||
kv = get()->getAuthorizationCacheEnabledKeyValue();
|
||||
ASSERT_FALSE(kv.has_value());
|
||||
}
|
||||
|
||||
TEST_P(Configuration, ReserveConnectorZeroSupported) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_FALSE(get()->getReserveConnectorZeroSupported().has_value());
|
||||
auto kv = get()->getReserveConnectorZeroSupportedKeyValue();
|
||||
ASSERT_FALSE(kv.has_value());
|
||||
}
|
||||
|
||||
TEST_P(Configuration, StopTransactionOnEVSideDisconnect) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
|
||||
EXPECT_TRUE(get()->getStopTransactionOnEVSideDisconnect().has_value());
|
||||
EXPECT_TRUE(get()->getStopTransactionOnEVSideDisconnect().value());
|
||||
auto kv = get()->getStopTransactionOnEVSideDisconnectKeyValue();
|
||||
ASSERT_TRUE(kv.has_value());
|
||||
EXPECT_EQ(kv.value().key, "StopTransactionOnEVSideDisconnect");
|
||||
EXPECT_EQ(kv.value().value, "true");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, ConnectorPhaseRotationMaxLength) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_EQ(get()->getConnectorPhaseRotationMaxLength(), std::nullopt);
|
||||
auto kv = get()->getConnectorPhaseRotationMaxLengthKeyValue();
|
||||
EXPECT_EQ(kv, std::nullopt);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, LightIntensity) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_EQ(get()->getLightIntensity(), std::nullopt);
|
||||
auto kv = get()->getLightIntensityKeyValue();
|
||||
EXPECT_EQ(kv, std::nullopt);
|
||||
|
||||
// set only works when there is a value configured
|
||||
get()->setLightIntensity(777);
|
||||
EXPECT_EQ(get()->getLightIntensity(), std::nullopt);
|
||||
kv = get()->getLightIntensityKeyValue();
|
||||
EXPECT_EQ(kv, std::nullopt);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, MaxEnergyOnInvalidId) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_EQ(get()->getMaxEnergyOnInvalidId(), std::nullopt);
|
||||
auto kv = get()->getMaxEnergyOnInvalidIdKeyValue();
|
||||
EXPECT_EQ(kv, std::nullopt);
|
||||
|
||||
// set only works when there is a value configured
|
||||
get()->setMaxEnergyOnInvalidId(770);
|
||||
EXPECT_EQ(get()->getMaxEnergyOnInvalidId(), std::nullopt);
|
||||
kv = get()->getMaxEnergyOnInvalidIdKeyValue();
|
||||
EXPECT_EQ(kv, std::nullopt);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, MeterValuesAlignedDataMaxLength) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_EQ(get()->getMeterValuesAlignedDataMaxLength(), std::nullopt);
|
||||
auto kv = get()->getMeterValuesAlignedDataMaxLengthKeyValue();
|
||||
EXPECT_EQ(kv, std::nullopt);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, MeterValuesSampledDataMaxLength) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_EQ(get()->getMeterValuesSampledDataMaxLength(), std::nullopt);
|
||||
auto kv = get()->getMeterValuesSampledDataMaxLengthKeyValue();
|
||||
EXPECT_EQ(kv, std::nullopt);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, MinimumStatusDuration) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_EQ(get()->getMinimumStatusDuration(), std::nullopt);
|
||||
auto kv = get()->getMinimumStatusDurationKeyValue();
|
||||
EXPECT_EQ(kv, std::nullopt);
|
||||
|
||||
// set only works when there is a value configured
|
||||
get()->setMinimumStatusDuration(760);
|
||||
EXPECT_EQ(get()->getMinimumStatusDuration(), std::nullopt);
|
||||
kv = get()->getMinimumStatusDurationKeyValue();
|
||||
EXPECT_EQ(kv, std::nullopt);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, StopTxnAlignedDataMaxLength) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_EQ(get()->getStopTxnAlignedDataMaxLength(), std::nullopt);
|
||||
auto kv = get()->getStopTxnAlignedDataMaxLengthKeyValue();
|
||||
EXPECT_EQ(kv, std::nullopt);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, StopTxnSampledDataMaxLength) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_EQ(get()->getStopTxnSampledDataMaxLength(), std::nullopt);
|
||||
auto kv = get()->getStopTxnSampledDataMaxLengthKeyValue();
|
||||
EXPECT_EQ(kv, std::nullopt);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, SupportedFeatureProfilesMaxLength) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_EQ(get()->getSupportedFeatureProfilesMaxLength(), std::nullopt);
|
||||
auto kv = get()->getSupportedFeatureProfilesMaxLengthKeyValue();
|
||||
EXPECT_EQ(kv, std::nullopt);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, WebsocketPingInterval) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_EQ(get()->getWebsocketPingInterval(), std::nullopt);
|
||||
auto kv = get()->getWebsocketPingIntervalKeyValue();
|
||||
EXPECT_EQ(kv, std::nullopt);
|
||||
|
||||
// set only works when there is a value configured
|
||||
get()->setWebsocketPingInterval(707);
|
||||
EXPECT_EQ(get()->getWebsocketPingInterval(), std::nullopt);
|
||||
kv = get()->getWebsocketPingIntervalKeyValue();
|
||||
EXPECT_EQ(kv, std::nullopt);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, SupportedFeatureProfilesSet) {
|
||||
using SupportedFeatureProfiles = ocpp::v16::SupportedFeatureProfiles;
|
||||
|
||||
ASSERT_NE(get(), nullptr);
|
||||
const auto set = get()->getSupportedFeatureProfilesSet();
|
||||
|
||||
const std::set<SupportedFeatureProfiles> expected = {SupportedFeatureProfiles::Internal,
|
||||
SupportedFeatureProfiles::Core,
|
||||
SupportedFeatureProfiles::CostAndPrice,
|
||||
SupportedFeatureProfiles::FirmwareManagement,
|
||||
SupportedFeatureProfiles::LocalAuthListManagement,
|
||||
SupportedFeatureProfiles::Reservation,
|
||||
SupportedFeatureProfiles::SmartCharging,
|
||||
SupportedFeatureProfiles::RemoteTrigger,
|
||||
SupportedFeatureProfiles::Security,
|
||||
SupportedFeatureProfiles::PnC};
|
||||
|
||||
EXPECT_EQ(set.size(), expected.size());
|
||||
EXPECT_EQ(set, expected);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Oneoff tests where there are differences between implementations
|
||||
// Note: TEST_F and not TEST_P
|
||||
|
||||
TEST_F(Configuration, SetAllowOfflineTxForUnknownIdV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("Core", "AllowOfflineTxForUnknownId", "");
|
||||
|
||||
EXPECT_FALSE(v2_config->getAllowOfflineTxForUnknownId().has_value());
|
||||
auto kv = v2_config->getAllowOfflineTxForUnknownIdKeyValue();
|
||||
ASSERT_TRUE(kv.has_value());
|
||||
EXPECT_EQ(kv.value().key, "AllowOfflineTxForUnknownId");
|
||||
EXPECT_EQ(kv.value().value, "");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
|
||||
v2_config->setAllowOfflineTxForUnknownId(true);
|
||||
EXPECT_TRUE(v2_config->getAllowOfflineTxForUnknownId().has_value());
|
||||
EXPECT_EQ(v2_config->getAllowOfflineTxForUnknownId().value(), true);
|
||||
kv = v2_config->getAllowOfflineTxForUnknownIdKeyValue();
|
||||
ASSERT_TRUE(kv.has_value());
|
||||
EXPECT_EQ(kv.value().key, "AllowOfflineTxForUnknownId");
|
||||
EXPECT_EQ(kv.value().value, "true");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetAuthorizationCacheEnabledV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("Core", "AuthorizationCacheEnabled", "");
|
||||
|
||||
EXPECT_FALSE(v2_config->getAuthorizationCacheEnabled().has_value());
|
||||
auto kv = v2_config->getAuthorizationCacheEnabledKeyValue();
|
||||
ASSERT_TRUE(kv.has_value());
|
||||
EXPECT_EQ(kv.value().key, "AuthorizationCacheEnabled");
|
||||
EXPECT_EQ(kv.value().value, "");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
|
||||
v2_config->setAuthorizationCacheEnabled(true);
|
||||
EXPECT_TRUE(v2_config->getAuthorizationCacheEnabled().has_value());
|
||||
EXPECT_EQ(v2_config->getAuthorizationCacheEnabled().value(), true);
|
||||
kv = v2_config->getAuthorizationCacheEnabledKeyValue();
|
||||
ASSERT_TRUE(kv.has_value());
|
||||
EXPECT_EQ(kv.value().key, "AuthorizationCacheEnabled");
|
||||
EXPECT_EQ(kv.value().value, "true");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetBlinkRepeatV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("Core", "BlinkRepeat", "");
|
||||
|
||||
EXPECT_EQ(v2_config->getBlinkRepeat(), std::nullopt);
|
||||
auto kv = v2_config->getBlinkRepeatKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "BlinkRepeat");
|
||||
EXPECT_EQ(kv.value().value, "");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
|
||||
v2_config->setBlinkRepeat(31);
|
||||
EXPECT_EQ(v2_config->getBlinkRepeat(), 31);
|
||||
kv = v2_config->getBlinkRepeatKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "BlinkRepeat");
|
||||
EXPECT_EQ(kv.value().value, "31");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetConnectorPhaseRotationMaxLengthV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("Core", "ConnectorPhaseRotationMaxLength", "200");
|
||||
|
||||
EXPECT_EQ(v2_config->getConnectorPhaseRotationMaxLength(), 200);
|
||||
auto kv = v2_config->getConnectorPhaseRotationMaxLengthKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "ConnectorPhaseRotationMaxLength");
|
||||
EXPECT_EQ(kv.value().value, "200");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetLightIntensityV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("Core", "LightIntensity", "");
|
||||
|
||||
EXPECT_EQ(v2_config->getLightIntensity(), std::nullopt);
|
||||
auto kv = v2_config->getLightIntensityKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "LightIntensity");
|
||||
EXPECT_EQ(kv.value().value, "");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
|
||||
v2_config->setLightIntensity(776);
|
||||
EXPECT_EQ(v2_config->getLightIntensity(), 776);
|
||||
kv = v2_config->getLightIntensityKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "LightIntensity");
|
||||
EXPECT_EQ(kv.value().value, "776");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetMaxEnergyOnInvalidIdV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("Core", "MaxEnergyOnInvalidId", "");
|
||||
|
||||
EXPECT_EQ(v2_config->getMaxEnergyOnInvalidId(), std::nullopt);
|
||||
auto kv = v2_config->getMaxEnergyOnInvalidIdKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "MaxEnergyOnInvalidId");
|
||||
EXPECT_EQ(kv.value().value, "");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
|
||||
v2_config->setMaxEnergyOnInvalidId(770);
|
||||
EXPECT_EQ(v2_config->getMaxEnergyOnInvalidId(), 770);
|
||||
kv = v2_config->getMaxEnergyOnInvalidIdKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "MaxEnergyOnInvalidId");
|
||||
EXPECT_EQ(kv.value().value, "770");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetMeterValuesAlignedDataMaxLengthV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("Core", "MeterValuesAlignedDataMaxLength", "199");
|
||||
|
||||
EXPECT_EQ(v2_config->getMeterValuesAlignedDataMaxLength(), 199);
|
||||
auto kv = v2_config->getMeterValuesAlignedDataMaxLengthKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "MeterValuesAlignedDataMaxLength");
|
||||
EXPECT_EQ(kv.value().value, "199");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetMeterValuesSampledDataMaxLengthV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("Core", "MeterValuesSampledDataMaxLength", "198");
|
||||
|
||||
EXPECT_EQ(v2_config->getMeterValuesSampledDataMaxLength(), 198);
|
||||
auto kv = v2_config->getMeterValuesSampledDataMaxLengthKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "MeterValuesSampledDataMaxLength");
|
||||
EXPECT_EQ(kv.value().value, "198");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetMinimumStatusDurationV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("Core", "MinimumStatusDuration", "");
|
||||
|
||||
EXPECT_EQ(v2_config->getMinimumStatusDuration(), std::nullopt);
|
||||
auto kv = v2_config->getMinimumStatusDurationKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "MinimumStatusDuration");
|
||||
EXPECT_EQ(kv.value().value, "");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
|
||||
v2_config->setMinimumStatusDuration(760);
|
||||
EXPECT_EQ(v2_config->getMinimumStatusDuration(), 760);
|
||||
kv = v2_config->getMinimumStatusDurationKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "MinimumStatusDuration");
|
||||
EXPECT_EQ(kv.value().value, "760");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetStopTxnAlignedDataMaxLengthV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("Core", "StopTxnAlignedDataMaxLength", "83");
|
||||
|
||||
EXPECT_EQ(v2_config->getStopTxnAlignedDataMaxLength(), 83);
|
||||
auto kv = v2_config->getStopTxnAlignedDataMaxLengthKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "StopTxnAlignedDataMaxLength");
|
||||
EXPECT_EQ(kv.value().value, "83");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetStopTxnSampledDataMaxLengthV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("Core", "StopTxnSampledDataMaxLength", "84");
|
||||
|
||||
EXPECT_EQ(v2_config->getStopTxnSampledDataMaxLength(), 84);
|
||||
auto kv = v2_config->getStopTxnSampledDataMaxLengthKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "StopTxnSampledDataMaxLength");
|
||||
EXPECT_EQ(kv.value().value, "84");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetSupportedFeatureProfilesMaxLengthV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("Core", "SupportedFeatureProfilesMaxLength", "85");
|
||||
|
||||
EXPECT_EQ(v2_config->getSupportedFeatureProfilesMaxLength(), 85);
|
||||
auto kv = v2_config->getSupportedFeatureProfilesMaxLengthKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "SupportedFeatureProfilesMaxLength");
|
||||
EXPECT_EQ(kv.value().value, "85");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetWebsocketPingIntervalV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("Core", "WebSocketPingInterval", "");
|
||||
|
||||
EXPECT_EQ(v2_config->getWebsocketPingInterval(), std::nullopt);
|
||||
auto kv = v2_config->getWebsocketPingIntervalKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "WebsocketPingInterval");
|
||||
EXPECT_EQ(kv.value().value, "");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
|
||||
v2_config->setWebsocketPingInterval(707);
|
||||
EXPECT_EQ(v2_config->getWebsocketPingInterval(), 707);
|
||||
kv = v2_config->getWebsocketPingIntervalKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "WebsocketPingInterval");
|
||||
EXPECT_EQ(kv.value().value, "707");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,340 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <optional>
|
||||
|
||||
#include "configuration_stub.hpp"
|
||||
|
||||
namespace {
|
||||
using namespace ocpp::v16::stubs;
|
||||
|
||||
// expected values extracted from the JSON configuration files
|
||||
// also see memory_storage.cpp
|
||||
const std::map<std::string, std::string> expected_key_value = {
|
||||
{"AuthorizeConnectorZeroOnConnectorOne", "true"},
|
||||
{"CentralSystemURI", "127.0.0.1:8180/steve/websocket/CentralSystemService/"},
|
||||
{"ChargeBoxSerialNumber", "cp001"},
|
||||
{"ChargePointId", "cp001"},
|
||||
{"ChargePointModel", "Yeti"},
|
||||
{"ChargePointVendor", "Pionix"},
|
||||
{"CompositeScheduleDefaultLimitAmps", "48"},
|
||||
{"CompositeScheduleDefaultLimitWatts", "33120"},
|
||||
{"CompositeScheduleDefaultNumberPhases", "3"},
|
||||
{"FirmwareVersion", "0.1"},
|
||||
{"LogMessages", "true"},
|
||||
{"LogMessagesFormat", ""},
|
||||
{"LogMessagesRaw", "false"},
|
||||
{"MaxCompositeScheduleDuration", "31536000"},
|
||||
{"MaxMessageSize", "65000"},
|
||||
{"OcspRequestInterval", "604800"},
|
||||
{"RetryBackoffRandomRange", "10"},
|
||||
{"RetryBackoffRepeatTimes", "3"},
|
||||
{"RetryBackoffWaitMinimum", "3"},
|
||||
{"StopTransactionIfUnlockNotSupported", "false"},
|
||||
{"SupplyVoltage", "230"},
|
||||
{"SupportedChargingProfilePurposeTypes", "ChargePointMaxProfile,TxDefaultProfile,TxProfile"},
|
||||
{"SupportedCiphers12",
|
||||
"ECDHE-ECDSA-AES128-GCM-SHA256:ECDHE-ECDSA-AES256-GCM-SHA384:AES128-GCM-SHA256:AES256-GCM-SHA384"},
|
||||
{"SupportedCiphers13", "TLS_AES_256_GCM_SHA384:TLS_AES_128_GCM_SHA256"},
|
||||
{"SupportedMeasurands", "Energy.Active.Import.Register,Energy.Active.Export.Register,Power.Active.Import,Voltage,"
|
||||
"Current.Import,Frequency,Current.Offered,Power.Offered,SoC,Temperature"},
|
||||
{"UseSslDefaultVerifyPaths", "true"},
|
||||
{"VerifyCsmsAllowWildcards", "false"},
|
||||
{"VerifyCsmsCommonName", "true"},
|
||||
{"WaitForStopTransactionsOnResetTimeout", "60"},
|
||||
{"WebsocketPingPayload", "hello there"},
|
||||
{"WebsocketPongTimeout", "5"},
|
||||
{"AuthorizeRemoteTxRequests", "false"},
|
||||
{"ClockAlignedDataInterval", "900"},
|
||||
{"ConnectionTimeOut", "10"},
|
||||
{"ConnectorPhaseRotation", "0.RST,1.RST"},
|
||||
{"GetConfigurationMaxKeys", "100"},
|
||||
{"HeartbeatInterval", "86400"},
|
||||
{"LocalAuthorizeOffline", "false"},
|
||||
{"LocalPreAuthorize", "false"},
|
||||
{"MeterValueSampleInterval", "0"},
|
||||
{"MeterValuesAlignedData", "Energy.Active.Import.Register"},
|
||||
{"MeterValuesSampledData", "Energy.Active.Import.Register"},
|
||||
{"NumberOfConnectors", "1"},
|
||||
{"ResetRetries", "1"},
|
||||
{"StopTransactionOnEVSideDisconnect", "true"},
|
||||
{"StopTransactionOnInvalidId", "true"},
|
||||
{"StopTxnAlignedData", "Energy.Active.Import.Register"},
|
||||
{"StopTxnSampledData", "Energy.Active.Import.Register"},
|
||||
{"SupportedFeatureProfiles",
|
||||
"Core,FirmwareManagement,RemoteTrigger,Reservation,LocalAuthListManagement,SmartCharging"},
|
||||
{"TransactionMessageAttempts", "1"},
|
||||
{"TransactionMessageRetryInterval", "10"},
|
||||
{"UnlockConnectorOnEVSideDisconnect", "true"},
|
||||
{"CustomDisplayCostAndPrice", "false"},
|
||||
{"SupportedFileTransferProtocols", "FTP"},
|
||||
{"LocalAuthListEnabled", "true"},
|
||||
{"LocalAuthListMaxLength", "42"},
|
||||
{"SendLocalListMaxLength", "42"},
|
||||
{"ChargeProfileMaxStackLevel", "42"},
|
||||
{"ChargingScheduleAllowedChargingRateUnit", "Current"},
|
||||
{"ChargingScheduleMaxPeriods", "42"},
|
||||
{"MaxChargingProfilesInstalled", "42"},
|
||||
{"DisableSecurityEventNotifications", "false"},
|
||||
{"SecurityProfile", "0"},
|
||||
{"ContractValidationOffline", "true"},
|
||||
{"ISO15118CertificateManagementEnabled", "true"},
|
||||
{"ISO15118PnCEnabled", "true"},
|
||||
{"UseTPM", "false"},
|
||||
{"UseTPMSeccLeafCertificate", "false"},
|
||||
{"LogRotationMaximumFileCount", "0"},
|
||||
{"LogRotationMaximumFileSize", "0"},
|
||||
{"TLSKeylogFile", "/tmp/ocpp_tls_keylog.txt"},
|
||||
{"LogRotation", "false"},
|
||||
{"LogRotationDateSuffix", "false"},
|
||||
{"EnableTLSKeylog", "false"},
|
||||
};
|
||||
|
||||
TEST_P(Configuration, CustomKey) {
|
||||
using ConfigurationStatus = ocpp::v16::ConfigurationStatus;
|
||||
ASSERT_NE(get(), nullptr);
|
||||
|
||||
const std::string key{"GTCustom"};
|
||||
const std::string value{"GTCustomValue"};
|
||||
|
||||
EXPECT_FALSE(get()->getCustomKeyValue(key).has_value());
|
||||
EXPECT_EQ(get()->setCustomKey(key, value, false), ConfigurationStatus::Rejected);
|
||||
|
||||
// no point in testing setCustomKey(key, value, true)
|
||||
// since force==true still requires that the key exists
|
||||
// in only allows read-only keys to be changed
|
||||
}
|
||||
|
||||
TEST_P(Configuration, Get) {
|
||||
using ConfigurationStatus = ocpp::v16::ConfigurationStatus;
|
||||
ASSERT_NE(get(), nullptr);
|
||||
|
||||
// non-existent key
|
||||
EXPECT_FALSE(get()->get("DoesNotExist").has_value());
|
||||
|
||||
// read-only key
|
||||
auto kv = get()->get("ChargePointModel");
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "ChargePointModel");
|
||||
EXPECT_EQ(kv.value().value, "Yeti");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
|
||||
// read-write key
|
||||
|
||||
kv = get()->get("ClockAlignedDataInterval");
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "ClockAlignedDataInterval");
|
||||
EXPECT_EQ(kv.value().value, "900");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
|
||||
// check key exists and has a value
|
||||
EXPECT_EQ(get()->getTLSKeylogFile(), "/tmp/ocpp_tls_keylog.txt");
|
||||
|
||||
// check it is available via this call (read-only)
|
||||
kv = get()->get("TLSKeylogFile");
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "TLSKeylogFile");
|
||||
EXPECT_EQ(kv.value().value, "/tmp/ocpp_tls_keylog.txt");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
|
||||
// custom key (none defined)
|
||||
}
|
||||
|
||||
TEST_P(Configuration, Set) {
|
||||
using ConfigurationStatus = ocpp::v16::ConfigurationStatus;
|
||||
ASSERT_NE(get(), nullptr);
|
||||
|
||||
// non-existent key
|
||||
EXPECT_FALSE(get()->get("DoesNotExist").has_value());
|
||||
EXPECT_EQ(get()->set("DoesNotExist", "ToThisValue"), std::nullopt);
|
||||
|
||||
// read-only key
|
||||
auto kv = get()->get("ChargePointModel");
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "ChargePointModel");
|
||||
EXPECT_EQ(kv.value().value, "Yeti");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
|
||||
EXPECT_EQ(get()->set("ChargePointModel", "ToThisValue"), std::nullopt);
|
||||
kv = get()->get("ChargePointModel");
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "ChargePointModel");
|
||||
EXPECT_EQ(kv.value().value, "Yeti");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
|
||||
// some other examples
|
||||
EXPECT_EQ(get()->set("ChargePointSerialNumber", "<won't be set>"), std::nullopt);
|
||||
EXPECT_EQ(get()->set("ICCID", "<won't be set>"), std::nullopt);
|
||||
EXPECT_EQ(get()->set("ConnectorPhaseRotationMaxLength", "<won't be set>"), std::nullopt);
|
||||
EXPECT_EQ(get()->set("NumberOfConnectors", "<won't be set>"), std::nullopt);
|
||||
EXPECT_EQ(get()->set("MeterType", "<won't be set>"), std::nullopt);
|
||||
EXPECT_EQ(get()->set("UseSslDefaultVerifyPaths", "<won't be set>"), std::nullopt);
|
||||
EXPECT_EQ(get()->set("CertificateStoreMaxLength", "<won't be set>"), std::nullopt);
|
||||
|
||||
// read-write key
|
||||
kv = get()->get("ClockAlignedDataInterval");
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "ClockAlignedDataInterval");
|
||||
EXPECT_EQ(kv.value().value, "900");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
|
||||
EXPECT_EQ(get()->set("ClockAlignedDataInterval", "1201"), ConfigurationStatus::Accepted);
|
||||
kv = get()->get("ClockAlignedDataInterval");
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "ClockAlignedDataInterval");
|
||||
EXPECT_EQ(kv.value().value, "1201");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
|
||||
// check if read-only key exists and has a value
|
||||
EXPECT_EQ(get()->getTLSKeylogFile(), "/tmp/ocpp_tls_keylog.txt");
|
||||
kv = get()->get("TLSKeylogFile");
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "TLSKeylogFile");
|
||||
EXPECT_EQ(kv.value().value, "/tmp/ocpp_tls_keylog.txt");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
EXPECT_EQ(get()->set("TLSKeylogFile", "1201"), std::nullopt);
|
||||
EXPECT_EQ(get()->getTLSKeylogFile(), "/tmp/ocpp_tls_keylog.txt");
|
||||
|
||||
// custom key (none defined)
|
||||
}
|
||||
|
||||
TEST_P(Configuration, GetAllKeyValue) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
|
||||
const auto values = get()->get_all_key_value();
|
||||
EXPECT_EQ(values.size(), expected_key_value.size());
|
||||
|
||||
std::map<std::string, std::string> not_found;
|
||||
std::map<std::string, std::string> missing = expected_key_value;
|
||||
|
||||
for (const auto& i : values) {
|
||||
// std::cout << "Looking for: " << i << '\n';
|
||||
if (const auto& search = expected_key_value.find(i.key); search == expected_key_value.end()) {
|
||||
not_found.insert({i.key, i.value.value_or("")});
|
||||
} else {
|
||||
missing.erase(i.key);
|
||||
std::string actual{i.value.value_or("")};
|
||||
SCOPED_TRACE("Name: " + std::string{i.key});
|
||||
EXPECT_EQ(search->second, actual);
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(not_found.empty());
|
||||
if (!not_found.empty()) {
|
||||
std::cout << "Not found:\n";
|
||||
for (const auto& i : not_found) {
|
||||
std::cout << "{\"" << i.first << "\",\"" << i.second << "\"},\n";
|
||||
}
|
||||
}
|
||||
|
||||
EXPECT_TRUE(missing.empty());
|
||||
if (!missing.empty()) {
|
||||
std::cout << "Missing:\n";
|
||||
for (const auto& i : missing) {
|
||||
std::cout << "{\"" << i.first << "\",\"" << i.second << "\"},\n";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Oneoff tests where there are differences between implementations
|
||||
// Note: TEST_F and not TEST_P
|
||||
|
||||
TEST_F(Configuration, setCustomKeyV2) {
|
||||
using ConfigurationStatus = ocpp::v16::ConfigurationStatus;
|
||||
ASSERT_TRUE(device_model);
|
||||
|
||||
const std::string key{"GTCustom"};
|
||||
const std::string value{"GTCustomValue"};
|
||||
|
||||
// set an initial value
|
||||
device_model->set("Custom", key, "");
|
||||
|
||||
EXPECT_TRUE(v2_config->getCustomKeyValue(key).has_value());
|
||||
auto kv = v2_config->getCustomKeyValue(key);
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, key.c_str());
|
||||
EXPECT_EQ(kv.value().value, "");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
|
||||
EXPECT_EQ(v2_config->setCustomKey(key, value, false), ConfigurationStatus::Accepted);
|
||||
kv = v2_config->getCustomKeyValue(key);
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, key.c_str());
|
||||
EXPECT_EQ(kv.value().value, value.c_str());
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
|
||||
// TODO: test that force=true allows update of a read-only key
|
||||
// and force=false rejects update.
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetV2) {
|
||||
using ConfigurationStatus = ocpp::v16::ConfigurationStatus;
|
||||
ASSERT_TRUE(device_model);
|
||||
|
||||
// set an initial custom key value
|
||||
device_model->set("Custom", "ACustomKey", "");
|
||||
device_model->set("Custom", "ACustomRWKey", "");
|
||||
device_model->set_readonly("ACustomKey");
|
||||
|
||||
// non-existent key
|
||||
EXPECT_FALSE(v2_config->get("DoesNotExist").has_value());
|
||||
EXPECT_EQ(v2_config->set("DoesNotExist", "ToThisValue"), std::nullopt);
|
||||
|
||||
// read-only key
|
||||
auto kv = v2_config->get("ChargePointModel");
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "ChargePointModel");
|
||||
EXPECT_EQ(kv.value().value, "Yeti");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
|
||||
EXPECT_EQ(v2_config->set("ChargePointModel", "ToThisValue"), std::nullopt);
|
||||
kv = v2_config->get("ChargePointModel");
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "ChargePointModel");
|
||||
EXPECT_EQ(kv.value().value, "Yeti");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
|
||||
// read-write key
|
||||
kv = v2_config->get("ClockAlignedDataInterval");
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "ClockAlignedDataInterval");
|
||||
EXPECT_EQ(kv.value().value, "900");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
|
||||
EXPECT_EQ(v2_config->set("ClockAlignedDataInterval", "1201"), ConfigurationStatus::Accepted);
|
||||
kv = v2_config->get("ClockAlignedDataInterval");
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "ClockAlignedDataInterval");
|
||||
EXPECT_EQ(kv.value().value, "1201");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
|
||||
// custom key (read only)
|
||||
kv = v2_config->get("ACustomKey");
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "ACustomKey");
|
||||
EXPECT_EQ(kv.value().value, "");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
EXPECT_EQ(v2_config->set("ACustomKey", "ToThisValueToo"), std::nullopt);
|
||||
kv = v2_config->get("ACustomKey");
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "ACustomKey");
|
||||
EXPECT_EQ(kv.value().value, "");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
|
||||
// custom key (read write)
|
||||
kv = v2_config->get("ACustomRWKey");
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "ACustomRWKey");
|
||||
EXPECT_EQ(kv.value().value, "");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
EXPECT_EQ(v2_config->set("ACustomRWKey", "ToThisValueTooMore"), ConfigurationStatus::Accepted);
|
||||
kv = v2_config->get("ACustomRWKey");
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "ACustomRWKey");
|
||||
EXPECT_EQ(kv.value().value, "ToThisValueTooMore");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,41 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <optional>
|
||||
|
||||
#include "configuration_stub.hpp"
|
||||
#include "ocpp/v16/types.hpp"
|
||||
|
||||
namespace {
|
||||
using namespace ocpp::v16::stubs;
|
||||
|
||||
TEST_P(Configuration, SupportedFileTransferProtocols) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getSupportedFileTransferProtocols(), "FTP");
|
||||
auto kv = get()->getSupportedFileTransferProtocolsKeyValue();
|
||||
ASSERT_TRUE(kv.has_value());
|
||||
EXPECT_EQ(kv.value().key, "SupportedFileTransferProtocols");
|
||||
EXPECT_EQ(kv.value().value, "FTP");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Oneoff tests where there are differences between implementations
|
||||
// Note: TEST_F and not TEST_P
|
||||
|
||||
TEST_F(Configuration, SetSupportedFileTransferProtocolsV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("FirmwareManagement", "SupportedFileTransferProtocols", "HTTP,HTTPS");
|
||||
|
||||
EXPECT_EQ(v2_config->getSupportedFileTransferProtocols(), "HTTP,HTTPS");
|
||||
auto kv = v2_config->getSupportedFileTransferProtocolsKeyValue();
|
||||
ASSERT_TRUE(kv.has_value());
|
||||
EXPECT_EQ(kv.value().key, "SupportedFileTransferProtocols");
|
||||
EXPECT_EQ(kv.value().value, "HTTP,HTTPS");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,76 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "configuration_stub.hpp"
|
||||
|
||||
namespace {
|
||||
using namespace ocpp::v16::stubs;
|
||||
|
||||
TEST_P(Configuration, LocalAuthListEnabled) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_TRUE(get()->getLocalAuthListEnabled());
|
||||
auto kv = get()->getLocalAuthListEnabledKeyValue();
|
||||
EXPECT_EQ(kv.key, "LocalAuthListEnabled");
|
||||
EXPECT_EQ(kv.value, "true");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
get()->setLocalAuthListEnabled(false);
|
||||
EXPECT_FALSE(get()->getLocalAuthListEnabled());
|
||||
kv = get()->getLocalAuthListEnabledKeyValue();
|
||||
EXPECT_EQ(kv.key, "LocalAuthListEnabled");
|
||||
EXPECT_EQ(kv.value, "false");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, LocalAuthListMaxLength) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getLocalAuthListMaxLength(), 42);
|
||||
auto kv = get()->getLocalAuthListMaxLengthKeyValue();
|
||||
EXPECT_EQ(kv.key, "LocalAuthListMaxLength");
|
||||
EXPECT_EQ(kv.value, "42");
|
||||
EXPECT_TRUE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, SendLocalListMaxLength) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getSendLocalListMaxLength(), 42);
|
||||
auto kv = get()->getSendLocalListMaxLengthKeyValue();
|
||||
EXPECT_EQ(kv.key, "SendLocalListMaxLength");
|
||||
EXPECT_EQ(kv.value, "42");
|
||||
EXPECT_TRUE(kv.readonly);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Oneoff tests where there are differences between implementations
|
||||
// Note: TEST_F and not TEST_P
|
||||
|
||||
TEST_F(Configuration, GetLocalAuthListMaxLengthV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("LocalAuthListManagement", "LocalAuthListMaxLength", "101");
|
||||
|
||||
EXPECT_EQ(v2_config->getLocalAuthListMaxLength(), 101);
|
||||
auto kv = v2_config->getLocalAuthListMaxLengthKeyValue();
|
||||
EXPECT_EQ(kv.key, "LocalAuthListMaxLength");
|
||||
EXPECT_EQ(kv.value, "101");
|
||||
EXPECT_TRUE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, GetSendLocalListMaxLengthV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("LocalAuthListManagement", "SendLocalListMaxLength", "102");
|
||||
|
||||
EXPECT_EQ(v2_config->getSendLocalListMaxLength(), 102);
|
||||
auto kv = v2_config->getSendLocalListMaxLengthKeyValue();
|
||||
EXPECT_EQ(kv.key, "SendLocalListMaxLength");
|
||||
EXPECT_EQ(kv.value, "102");
|
||||
EXPECT_TRUE(kv.readonly);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,171 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "configuration_stub.hpp"
|
||||
|
||||
namespace {
|
||||
using namespace ocpp::v16::stubs;
|
||||
|
||||
TEST_P(Configuration, ContractValidationOffline) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_TRUE(get()->getContractValidationOffline());
|
||||
auto kv = get()->getContractValidationOfflineKeyValue();
|
||||
EXPECT_EQ(kv.key, "ContractValidationOffline");
|
||||
EXPECT_EQ(kv.value, "true");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
get()->setContractValidationOffline(false);
|
||||
EXPECT_FALSE(get()->getContractValidationOffline());
|
||||
kv = get()->getContractValidationOfflineKeyValue();
|
||||
EXPECT_EQ(kv.key, "ContractValidationOffline");
|
||||
EXPECT_EQ(kv.value, "false");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, ISO15118CertificateManagementEnabled) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_TRUE(get()->getISO15118CertificateManagementEnabled());
|
||||
auto kv = get()->getISO15118CertificateManagementEnabledKeyValue();
|
||||
EXPECT_EQ(kv.key, "ISO15118CertificateManagementEnabled");
|
||||
EXPECT_EQ(kv.value, "true");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
get()->setISO15118CertificateManagementEnabled(false);
|
||||
EXPECT_FALSE(get()->getISO15118CertificateManagementEnabled());
|
||||
kv = get()->getISO15118CertificateManagementEnabledKeyValue();
|
||||
EXPECT_EQ(kv.key, "ISO15118CertificateManagementEnabled");
|
||||
EXPECT_EQ(kv.value, "false");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, ISO15118PnCEnabled) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_TRUE(get()->getISO15118PnCEnabled());
|
||||
auto kv = get()->getISO15118PnCEnabledKeyValue();
|
||||
EXPECT_EQ(kv.key, "ISO15118PnCEnabled");
|
||||
EXPECT_EQ(kv.value, "true");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
get()->setISO15118PnCEnabled(false);
|
||||
EXPECT_FALSE(get()->getISO15118PnCEnabled());
|
||||
kv = get()->getISO15118PnCEnabledKeyValue();
|
||||
EXPECT_EQ(kv.key, "ISO15118PnCEnabled");
|
||||
EXPECT_EQ(kv.value, "false");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, CentralContractValidationAllowed) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_FALSE(get()->getCentralContractValidationAllowed().has_value());
|
||||
auto kv = get()->getCentralContractValidationAllowedKeyValue();
|
||||
ASSERT_FALSE(kv);
|
||||
|
||||
// needs existing value for set to work
|
||||
get()->setCentralContractValidationAllowed(false);
|
||||
EXPECT_FALSE(get()->getCentralContractValidationAllowed().has_value());
|
||||
kv = get()->getCentralContractValidationAllowedKeyValue();
|
||||
ASSERT_FALSE(kv);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, CertSigningRepeatTimes) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_FALSE(get()->getCertSigningRepeatTimes().has_value());
|
||||
auto kv = get()->getCertSigningRepeatTimesKeyValue();
|
||||
ASSERT_FALSE(kv);
|
||||
|
||||
// needs existing value for set to work
|
||||
get()->setCertSigningRepeatTimes(99);
|
||||
EXPECT_FALSE(get()->getCertSigningRepeatTimes().has_value());
|
||||
kv = get()->getCertSigningRepeatTimesKeyValue();
|
||||
ASSERT_FALSE(kv);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, CertSigningWaitMinimum) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_FALSE(get()->getCertSigningWaitMinimum().has_value());
|
||||
auto kv = get()->getCertSigningWaitMinimumKeyValue();
|
||||
ASSERT_FALSE(kv);
|
||||
|
||||
// needs existing value for set to work
|
||||
get()->setCertSigningWaitMinimum(55);
|
||||
EXPECT_FALSE(get()->getCertSigningWaitMinimum().has_value());
|
||||
kv = get()->getCertSigningWaitMinimumKeyValue();
|
||||
ASSERT_FALSE(kv);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Oneoff tests where there are differences between implementations
|
||||
// Note: TEST_F and not TEST_P
|
||||
|
||||
TEST_F(Configuration, SetCentralContractValidationAllowedV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("PnC", "CentralContractValidationAllowed", "");
|
||||
|
||||
EXPECT_FALSE(v2_config->getCentralContractValidationAllowed().has_value());
|
||||
auto kv = v2_config->getCentralContractValidationAllowedKeyValue();
|
||||
ASSERT_TRUE(kv.has_value());
|
||||
EXPECT_EQ(kv.value().key, "CentralContractValidationAllowed");
|
||||
EXPECT_EQ(kv.value().value, "");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
|
||||
v2_config->setCentralContractValidationAllowed(false);
|
||||
EXPECT_TRUE(v2_config->getCentralContractValidationAllowed().has_value());
|
||||
EXPECT_FALSE(v2_config->getCentralContractValidationAllowed().value());
|
||||
kv = v2_config->getCentralContractValidationAllowedKeyValue();
|
||||
ASSERT_TRUE(kv.has_value());
|
||||
EXPECT_EQ(kv.value().key, "CentralContractValidationAllowed");
|
||||
EXPECT_EQ(kv.value().value, "false");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetCertSigningRepeatTimesV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("PnC", "CertSigningRepeatTimes", "");
|
||||
|
||||
EXPECT_FALSE(v2_config->getCertSigningRepeatTimes().has_value());
|
||||
auto kv = v2_config->getCertSigningRepeatTimesKeyValue();
|
||||
ASSERT_TRUE(kv.has_value());
|
||||
EXPECT_EQ(kv.value().key, "CertSigningRepeatTimes");
|
||||
EXPECT_EQ(kv.value().value, "");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
|
||||
v2_config->setCertSigningRepeatTimes(55);
|
||||
EXPECT_TRUE(v2_config->getCertSigningRepeatTimes().has_value());
|
||||
EXPECT_EQ(v2_config->getCertSigningRepeatTimes(), 55);
|
||||
kv = v2_config->getCertSigningRepeatTimesKeyValue();
|
||||
ASSERT_TRUE(kv.has_value());
|
||||
EXPECT_EQ(kv.value().key, "CertSigningRepeatTimes");
|
||||
EXPECT_EQ(kv.value().value, "55");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetCertSigningWaitMinimumV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("PnC", "CertSigningWaitMinimum", "");
|
||||
|
||||
EXPECT_FALSE(v2_config->getCertSigningWaitMinimum().has_value());
|
||||
auto kv = v2_config->getCertSigningWaitMinimumKeyValue();
|
||||
ASSERT_TRUE(kv.has_value());
|
||||
EXPECT_EQ(kv.value().key, "CertSigningWaitMinimum");
|
||||
EXPECT_EQ(kv.value().value, "");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
|
||||
v2_config->setCertSigningWaitMinimum(54);
|
||||
EXPECT_TRUE(v2_config->getCertSigningWaitMinimum().has_value());
|
||||
EXPECT_EQ(v2_config->getCertSigningWaitMinimum(), 54);
|
||||
kv = v2_config->getCertSigningWaitMinimumKeyValue();
|
||||
ASSERT_TRUE(kv.has_value());
|
||||
EXPECT_EQ(kv.value().key, "CertSigningWaitMinimum");
|
||||
EXPECT_EQ(kv.value().value, "54");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,221 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "configuration_stub.hpp"
|
||||
|
||||
namespace {
|
||||
using namespace ocpp::v16::stubs;
|
||||
|
||||
TEST_P(Configuration, DisableSecurityEventNotifications) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
|
||||
// V16 gets a value from the schema file patches
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_FALSE(get()->getDisableSecurityEventNotifications());
|
||||
auto kv = get()->getDisableSecurityEventNotificationsKeyValue();
|
||||
EXPECT_EQ(kv.key, "DisableSecurityEventNotifications");
|
||||
EXPECT_EQ(kv.value, "false");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
get()->setDisableSecurityEventNotifications(true);
|
||||
EXPECT_TRUE(get()->getDisableSecurityEventNotifications());
|
||||
kv = get()->getDisableSecurityEventNotificationsKeyValue();
|
||||
EXPECT_EQ(kv.key, "DisableSecurityEventNotifications");
|
||||
EXPECT_EQ(kv.value, "true");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, SecurityProfile) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getSecurityProfile(), 0);
|
||||
auto kv = get()->getSecurityProfileKeyValue();
|
||||
EXPECT_EQ(kv.key, "SecurityProfile");
|
||||
EXPECT_EQ(kv.value, "0");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
get()->setSecurityProfile(3);
|
||||
EXPECT_EQ(get()->getSecurityProfile(), 3);
|
||||
kv = get()->getSecurityProfileKeyValue();
|
||||
EXPECT_EQ(kv.key, "SecurityProfile");
|
||||
EXPECT_EQ(kv.value, "3");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, AuthorizationKey) {
|
||||
// notes: this one has some special code behind it
|
||||
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
|
||||
// AuthorizationKey not set so we get the expected nullopt
|
||||
EXPECT_EQ(get()->getAuthorizationKey(), std::nullopt);
|
||||
|
||||
// AuthorizationKey not set but a KeyValue is returned
|
||||
// rather than nullopt. kv.value is nullopt though
|
||||
auto kv = get()->getAuthorizationKeyKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "AuthorizationKey");
|
||||
EXPECT_FALSE(kv.value().value.has_value());
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
|
||||
get()->setAuthorizationKey("01234567890123456789");
|
||||
// the correct key is returned
|
||||
EXPECT_EQ(get()->getAuthorizationKey(), "01234567890123456789");
|
||||
kv = get()->getAuthorizationKeyKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "AuthorizationKey");
|
||||
// a dummy value is set to avoid leaking the key to the CSMS
|
||||
EXPECT_EQ(kv.value().value, "DummyAuthorizationKey");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, CpoName) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getCpoName(), std::nullopt);
|
||||
|
||||
// CpoName not set but a KeyValue is returned
|
||||
// rather than nullopt. kv.value is nullopt though
|
||||
auto kv = get()->getCpoNameKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "CpoName");
|
||||
EXPECT_FALSE(kv.value().value.has_value());
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
|
||||
get()->setCpoName("setCpoName");
|
||||
EXPECT_EQ(get()->getCpoName(), "setCpoName");
|
||||
kv = get()->getCpoNameKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "CpoName");
|
||||
EXPECT_EQ(kv.value().value, "setCpoName");
|
||||
EXPECT_FALSE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, AdditionalRootCertificateCheck) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_EQ(get()->getAdditionalRootCertificateCheck(), std::nullopt);
|
||||
auto kv = get()->getAdditionalRootCertificateCheckKeyValue();
|
||||
ASSERT_FALSE(kv);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, CertificateSignedMaxChainSize) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_EQ(get()->getCertificateSignedMaxChainSize(), std::nullopt);
|
||||
auto kv = get()->getCertificateSignedMaxChainSizeKeyValue();
|
||||
ASSERT_FALSE(kv);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, CertificateStoreMaxLength) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_EQ(get()->getCertificateStoreMaxLength(), std::nullopt);
|
||||
auto kv = get()->getCertificateStoreMaxLengthKeyValue();
|
||||
ASSERT_FALSE(kv);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Oneoff tests where there are differences between implementations
|
||||
// Note: TEST_F and not TEST_P
|
||||
|
||||
TEST_F(Configuration, GetDisableSecurityEventNotificationsV16) {
|
||||
// this test should fail since DisableSecurityEventNotifications is not
|
||||
// in the JSON config file.
|
||||
// However there is a patch process that adds information from the
|
||||
// schema files
|
||||
// "Adding the following default values to the charge point configuration:"
|
||||
|
||||
// TODO(james-ctc): the V2 implementation will need to consider
|
||||
// those additions when migrating data
|
||||
|
||||
EXPECT_FALSE(v16_config->getDisableSecurityEventNotifications());
|
||||
auto kv = v16_config->getDisableSecurityEventNotificationsKeyValue();
|
||||
EXPECT_EQ(kv.key, "DisableSecurityEventNotifications");
|
||||
EXPECT_EQ(kv.value, "false");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
v16_config->setDisableSecurityEventNotifications(true);
|
||||
EXPECT_TRUE(v16_config->getDisableSecurityEventNotifications());
|
||||
kv = v16_config->getDisableSecurityEventNotificationsKeyValue();
|
||||
EXPECT_EQ(kv.key, "DisableSecurityEventNotifications");
|
||||
EXPECT_EQ(kv.value, "true");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetDisableSecurityEventNotificationsV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
device_model->clear("Security", "DisableSecurityEventNotifications");
|
||||
|
||||
// correctly fails since the key doesn't exits
|
||||
EXPECT_ANY_THROW(v2_config->getDisableSecurityEventNotifications(););
|
||||
EXPECT_ANY_THROW(v2_config->getDisableSecurityEventNotificationsKeyValue(););
|
||||
|
||||
// set an initial value
|
||||
device_model->set("Security", "DisableSecurityEventNotifications", "false");
|
||||
|
||||
EXPECT_FALSE(v2_config->getDisableSecurityEventNotifications());
|
||||
auto kv = v2_config->getDisableSecurityEventNotificationsKeyValue();
|
||||
EXPECT_EQ(kv.key, "DisableSecurityEventNotifications");
|
||||
EXPECT_EQ(kv.value, "false");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
|
||||
v2_config->setDisableSecurityEventNotifications(true);
|
||||
EXPECT_TRUE(v2_config->getDisableSecurityEventNotifications());
|
||||
kv = v2_config->getDisableSecurityEventNotificationsKeyValue();
|
||||
EXPECT_EQ(kv.key, "DisableSecurityEventNotifications");
|
||||
EXPECT_EQ(kv.value, "true");
|
||||
EXPECT_FALSE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetAdditionalRootCertificateCheckV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("Security", "AdditionalRootCertificateCheck", "");
|
||||
|
||||
EXPECT_FALSE(v2_config->getAdditionalRootCertificateCheck().has_value());
|
||||
auto kv = v2_config->getAdditionalRootCertificateCheckKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "AdditionalRootCertificateCheck");
|
||||
EXPECT_EQ(kv.value().value, "");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
|
||||
device_model->set("Security", "AdditionalRootCertificateCheck", "false");
|
||||
|
||||
EXPECT_TRUE(v2_config->getAdditionalRootCertificateCheck().has_value());
|
||||
EXPECT_FALSE(v2_config->getAdditionalRootCertificateCheck().value());
|
||||
|
||||
kv = v2_config->getAdditionalRootCertificateCheckKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "AdditionalRootCertificateCheck");
|
||||
EXPECT_EQ(kv.value().value, "false");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, GetCertificateSignedMaxChainSizeV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("Security", "CertificateSignedMaxChainSize", "5");
|
||||
|
||||
EXPECT_EQ(v2_config->getCertificateSignedMaxChainSize(), 5);
|
||||
auto kv = v2_config->getCertificateSignedMaxChainSizeKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "CertificateSignedMaxChainSize");
|
||||
EXPECT_EQ(kv.value().value, "5");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, GetCertificateStoreMaxLengthV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("Security", "CertificateStoreMaxLength", "512");
|
||||
|
||||
EXPECT_EQ(v2_config->getCertificateStoreMaxLength(), 512);
|
||||
auto kv = v2_config->getCertificateStoreMaxLengthKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "CertificateStoreMaxLength");
|
||||
EXPECT_EQ(kv.value().value, "512");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,136 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <optional>
|
||||
|
||||
#include "configuration_stub.hpp"
|
||||
#include "ocpp/v16/types.hpp"
|
||||
|
||||
namespace {
|
||||
using namespace ocpp::v16::stubs;
|
||||
|
||||
TEST_P(Configuration, ChargingScheduleAllowedChargingRateUnit) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getChargingScheduleAllowedChargingRateUnit(), "Current");
|
||||
auto kv = get()->getChargingScheduleAllowedChargingRateUnitKeyValue();
|
||||
EXPECT_EQ(kv.key, "ChargingScheduleAllowedChargingRateUnit");
|
||||
EXPECT_EQ(kv.value, "Current");
|
||||
EXPECT_TRUE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, ChargeProfileMaxStackLevel) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getChargeProfileMaxStackLevel(), 42);
|
||||
auto kv = get()->getChargeProfileMaxStackLevelKeyValue();
|
||||
EXPECT_EQ(kv.key, "ChargeProfileMaxStackLevel");
|
||||
EXPECT_EQ(kv.value, "42");
|
||||
EXPECT_TRUE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, ChargingScheduleMaxPeriods) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getChargingScheduleMaxPeriods(), 42);
|
||||
auto kv = get()->getChargingScheduleMaxPeriodsKeyValue();
|
||||
EXPECT_EQ(kv.key, "ChargingScheduleMaxPeriods");
|
||||
EXPECT_EQ(kv.value, "42");
|
||||
EXPECT_TRUE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, MaxChargingProfilesInstalled) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getMaxChargingProfilesInstalled(), 42);
|
||||
auto kv = get()->getMaxChargingProfilesInstalledKeyValue();
|
||||
EXPECT_EQ(kv.key, "MaxChargingProfilesInstalled");
|
||||
EXPECT_EQ(kv.value, "42");
|
||||
EXPECT_TRUE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, ConnectorSwitch3to1PhaseSupported) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
EXPECT_EQ(get()->getConnectorSwitch3to1PhaseSupported(), std::nullopt);
|
||||
auto kv = get()->getConnectorSwitch3to1PhaseSupportedKeyValue();
|
||||
ASSERT_FALSE(kv);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// Oneoff tests where there are differences between implementations
|
||||
// Note: TEST_F and not TEST_P
|
||||
|
||||
TEST_F(Configuration, SetChargingScheduleAllowedChargingRateUnitV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("SmartCharging", "ChargingScheduleAllowedChargingRateUnit", "Watts");
|
||||
|
||||
EXPECT_EQ(v2_config->getChargingScheduleAllowedChargingRateUnit(), "Watts");
|
||||
auto kv = v2_config->getChargingScheduleAllowedChargingRateUnitKeyValue();
|
||||
EXPECT_EQ(kv.key, "ChargingScheduleAllowedChargingRateUnit");
|
||||
EXPECT_EQ(kv.value, "Watts");
|
||||
EXPECT_TRUE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetChargeProfileMaxStackLevelV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("SmartCharging", "ChargeProfileMaxStackLevel", "11");
|
||||
|
||||
EXPECT_EQ(v2_config->getChargeProfileMaxStackLevel(), 11);
|
||||
auto kv = v2_config->getChargeProfileMaxStackLevelKeyValue();
|
||||
EXPECT_EQ(kv.key, "ChargeProfileMaxStackLevel");
|
||||
EXPECT_EQ(kv.value, "11");
|
||||
EXPECT_TRUE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetChargingScheduleMaxPeriodsV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("SmartCharging", "ChargingScheduleMaxPeriods", "12");
|
||||
|
||||
EXPECT_EQ(v2_config->getChargingScheduleMaxPeriods(), 12);
|
||||
auto kv = v2_config->getChargingScheduleMaxPeriodsKeyValue();
|
||||
EXPECT_EQ(kv.key, "ChargingScheduleMaxPeriods");
|
||||
EXPECT_EQ(kv.value, "12");
|
||||
EXPECT_TRUE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetMaxChargingProfilesInstalledV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("SmartCharging", "MaxChargingProfilesInstalled", "13");
|
||||
|
||||
EXPECT_EQ(v2_config->getMaxChargingProfilesInstalled(), 13);
|
||||
auto kv = v2_config->getMaxChargingProfilesInstalledKeyValue();
|
||||
EXPECT_EQ(kv.key, "MaxChargingProfilesInstalled");
|
||||
EXPECT_EQ(kv.value, "13");
|
||||
EXPECT_TRUE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_F(Configuration, SetConnectorSwitch3to1PhaseSupportedV2) {
|
||||
ASSERT_TRUE(device_model);
|
||||
// set an initial value
|
||||
device_model->set("SmartCharging", "ConnectorSwitch3to1PhaseSupported", "");
|
||||
|
||||
EXPECT_FALSE(v2_config->getConnectorSwitch3to1PhaseSupported().has_value());
|
||||
auto kv = v2_config->getConnectorSwitch3to1PhaseSupportedKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "ConnectorSwitch3to1PhaseSupported");
|
||||
EXPECT_EQ(kv.value().value, "");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
|
||||
device_model->set("SmartCharging", "ConnectorSwitch3to1PhaseSupported", "true");
|
||||
|
||||
EXPECT_TRUE(v2_config->getConnectorSwitch3to1PhaseSupported().has_value());
|
||||
EXPECT_TRUE(v2_config->getConnectorSwitch3to1PhaseSupported().value());
|
||||
|
||||
kv = v2_config->getConnectorSwitch3to1PhaseSupportedKeyValue();
|
||||
ASSERT_TRUE(kv);
|
||||
EXPECT_EQ(kv.value().key, "ConnectorSwitch3to1PhaseSupported");
|
||||
EXPECT_EQ(kv.value().value, "true");
|
||||
EXPECT_TRUE(kv.value().readonly);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,63 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <optional>
|
||||
|
||||
#include "configuration_stub.hpp"
|
||||
|
||||
namespace {
|
||||
using namespace ocpp::v16::stubs;
|
||||
|
||||
TEST_P(Configuration, setChargepointInformation) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getChargeBoxSerialNumber(), "cp001");
|
||||
EXPECT_EQ(get()->getChargePointSerialNumber(), std::nullopt);
|
||||
EXPECT_EQ(get()->getFirmwareVersion(), "0.1");
|
||||
|
||||
EXPECT_EQ(get()->getChargePointVendor(), "Pionix");
|
||||
EXPECT_EQ(get()->getChargePointModel(), "Yeti");
|
||||
|
||||
get()->setChargepointInformation("chargePointVendor", "chargePointModel", "chargePointSerialNumber",
|
||||
"chargeBoxSerialNumber", "firmwareVersion");
|
||||
EXPECT_EQ(get()->getChargePointVendor(), "chargePointVendor");
|
||||
EXPECT_EQ(get()->getChargePointModel(), "chargePointModel");
|
||||
EXPECT_EQ(get()->getChargePointSerialNumber(), "chargePointSerialNumber");
|
||||
EXPECT_EQ(get()->getChargeBoxSerialNumber(), "chargeBoxSerialNumber");
|
||||
EXPECT_EQ(get()->getFirmwareVersion(), "firmwareVersion");
|
||||
|
||||
auto kv = get()->getChargeBoxSerialNumberKeyValue();
|
||||
EXPECT_EQ(kv.key, "ChargeBoxSerialNumber");
|
||||
EXPECT_EQ(kv.value, "chargeBoxSerialNumber");
|
||||
EXPECT_TRUE(kv.readonly);
|
||||
|
||||
kv = get()->getFirmwareVersionKeyValue();
|
||||
EXPECT_EQ(kv.key, "FirmwareVersion");
|
||||
EXPECT_EQ(kv.value, "firmwareVersion");
|
||||
EXPECT_TRUE(kv.readonly);
|
||||
}
|
||||
|
||||
TEST_P(Configuration, setChargepointMeterInformation) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getMeterSerialNumber(), std::nullopt);
|
||||
EXPECT_EQ(get()->getMeterType(), std::nullopt);
|
||||
|
||||
get()->setChargepointMeterInformation("meterSerialNumber", "meterType");
|
||||
EXPECT_EQ(get()->getMeterSerialNumber(), "meterSerialNumber");
|
||||
EXPECT_EQ(get()->getMeterType(), "meterType");
|
||||
}
|
||||
|
||||
TEST_P(Configuration, setChargepointModemInformation) {
|
||||
ASSERT_NE(get(), nullptr);
|
||||
// initial values are from the JSON unit test config files
|
||||
EXPECT_EQ(get()->getICCID(), std::nullopt);
|
||||
EXPECT_EQ(get()->getIMSI(), std::nullopt);
|
||||
|
||||
get()->setChargepointModemInformation("ICCID", "IMSI");
|
||||
EXPECT_EQ(get()->getICCID(), "ICCID");
|
||||
EXPECT_EQ(get()->getIMSI(), "IMSI");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -0,0 +1,30 @@
|
||||
target_include_directories(libocpp_unit_tests PUBLIC
|
||||
mocks
|
||||
${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
target_sources(libocpp_unit_tests PRIVATE
|
||||
device_model_test_helper.cpp
|
||||
smart_charging_test_utils.cpp
|
||||
test_charge_point.cpp
|
||||
test_database_handler.cpp
|
||||
test_database_migration_files.cpp
|
||||
test_device_model_storage_sqlite.cpp
|
||||
test_notify_report_requests_splitter.cpp
|
||||
test_ocsp_updater.cpp
|
||||
test_component_state_manager.cpp
|
||||
test_database_handler.cpp
|
||||
test_device_model.cpp
|
||||
test_init_device_model_db.cpp
|
||||
test_network_config_sync.cpp
|
||||
comparators.cpp
|
||||
test_message_queue.cpp
|
||||
test_composite_schedule.cpp
|
||||
test_profile.cpp
|
||||
)
|
||||
|
||||
# Copy the json files used for testing to the destination directory
|
||||
file(COPY ${CMAKE_CURRENT_SOURCE_DIR}/json DESTINATION ${TEST_PROFILES_LOCATION_V2})
|
||||
|
||||
set(LIBOCPP_TESTS_V2_ROOT_DIR ${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
add_subdirectory(functional_blocks)
|
||||
@@ -0,0 +1,28 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <comparators.hpp>
|
||||
|
||||
namespace testing::internal {
|
||||
|
||||
bool operator==(const ::ocpp::CertificateHashDataType& a, const ::ocpp::CertificateHashDataType& b) {
|
||||
return a.serialNumber == b.serialNumber && a.issuerKeyHash == b.issuerKeyHash &&
|
||||
a.issuerNameHash == b.issuerNameHash && a.hashAlgorithm == b.hashAlgorithm;
|
||||
}
|
||||
bool operator==(const ::ocpp::v2::GetCertificateStatusRequest& a, const ::ocpp::v2::GetCertificateStatusRequest& b) {
|
||||
return a.ocspRequestData.serialNumber == b.ocspRequestData.serialNumber &&
|
||||
a.ocspRequestData.issuerKeyHash == b.ocspRequestData.issuerKeyHash &&
|
||||
a.ocspRequestData.issuerNameHash == b.ocspRequestData.issuerNameHash &&
|
||||
a.ocspRequestData.hashAlgorithm == b.ocspRequestData.hashAlgorithm &&
|
||||
a.ocspRequestData.responderURL == b.ocspRequestData.responderURL;
|
||||
}
|
||||
|
||||
} // namespace testing::internal
|
||||
|
||||
namespace ocpp::v2 {
|
||||
|
||||
bool operator==(const ChargingProfile& a, const ChargingProfile& b) {
|
||||
return a.chargingProfileKind == b.chargingProfileKind && a.chargingProfilePurpose == b.chargingProfilePurpose &&
|
||||
a.id == b.id && a.stackLevel == b.stackLevel;
|
||||
}
|
||||
} // namespace ocpp::v2
|
||||
@@ -0,0 +1,24 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright 2020 - 2023 Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#ifndef TESTS_OCPP_COMPARATORS_H
|
||||
#define TESTS_OCPP_COMPARATORS_H
|
||||
|
||||
#include <ocpp/common/types.hpp>
|
||||
#include <ocpp/v2/messages/GetCertificateStatus.hpp>
|
||||
#include <ocpp/v2/types.hpp>
|
||||
|
||||
namespace testing::internal {
|
||||
|
||||
bool operator==(const ::ocpp::CertificateHashDataType& a, const ::ocpp::CertificateHashDataType& b);
|
||||
bool operator==(const ::ocpp::v2::GetCertificateStatusRequest& a, const ::ocpp::v2::GetCertificateStatusRequest& b);
|
||||
|
||||
} // namespace testing::internal
|
||||
|
||||
namespace ocpp::v2 {
|
||||
|
||||
bool operator==(const ChargingProfile& a, const ChargingProfile& b);
|
||||
|
||||
} // namespace ocpp::v2
|
||||
|
||||
#endif // TESTS_OCPP_COMPARATORS_H
|
||||
@@ -0,0 +1,228 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include "device_model_test_helper.hpp"
|
||||
|
||||
#include <everest/database/sqlite/connection.hpp>
|
||||
#include <ocpp/v2/device_model.hpp>
|
||||
#include <ocpp/v2/device_model_storage_sqlite.hpp>
|
||||
|
||||
using namespace everest::db;
|
||||
using namespace everest::db::sqlite;
|
||||
|
||||
namespace ocpp::v2 {
|
||||
DeviceModelTestHelper::DeviceModelTestHelper(const std::string& database_path, const std::string& migration_files_path,
|
||||
const std::string& config_path) :
|
||||
database_path(database_path),
|
||||
migration_files_path(migration_files_path),
|
||||
config_path(config_path),
|
||||
database_connection(std::make_unique<everest::db::sqlite::Connection>(database_path)) {
|
||||
this->database_connection->open_connection();
|
||||
this->device_model = create_device_model();
|
||||
}
|
||||
|
||||
DeviceModel* DeviceModelTestHelper::get_device_model() {
|
||||
if (this->device_model == nullptr) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
return this->device_model.get();
|
||||
}
|
||||
|
||||
bool DeviceModelTestHelper::remove_variable_from_db(const std::string& component_name,
|
||||
const std::optional<std::string>& component_instance,
|
||||
const std::optional<std::uint32_t>& evse_id,
|
||||
const std::optional<std::uint32_t>& connector_id,
|
||||
const std::string& variable_name,
|
||||
const std::optional<std::string>& variable_instance) {
|
||||
const std::string delete_query = "DELETE FROM VARIABLE WHERE ID = "
|
||||
"(SELECT ID FROM VARIABLE WHERE COMPONENT_ID = "
|
||||
"(SELECT ID FROM COMPONENT WHERE NAME = ? AND INSTANCE IS ? AND "
|
||||
"EVSE_ID IS ? AND CONNECTOR_ID IS ?) "
|
||||
"AND NAME = ? AND INSTANCE IS ?)";
|
||||
|
||||
auto delete_stmt = this->database_connection->new_statement(delete_query);
|
||||
delete_stmt->bind_text(1, component_name, SQLiteString::Transient);
|
||||
if (component_instance.has_value()) {
|
||||
delete_stmt->bind_text(2, component_instance.value(), SQLiteString::Transient);
|
||||
} else {
|
||||
delete_stmt->bind_null(2);
|
||||
}
|
||||
if (evse_id.has_value()) {
|
||||
delete_stmt->bind_int(3, evse_id.value());
|
||||
if (connector_id.has_value()) {
|
||||
delete_stmt->bind_int(4, connector_id.value());
|
||||
} else {
|
||||
delete_stmt->bind_null(4);
|
||||
}
|
||||
} else {
|
||||
delete_stmt->bind_null(3);
|
||||
delete_stmt->bind_null(4);
|
||||
}
|
||||
|
||||
delete_stmt->bind_text(5, variable_name, SQLiteString::Transient);
|
||||
if (variable_instance.has_value()) {
|
||||
delete_stmt->bind_text(6, variable_instance.value(), SQLiteString::Transient);
|
||||
} else {
|
||||
delete_stmt->bind_null(6);
|
||||
}
|
||||
|
||||
if (delete_stmt->step() != SQLITE_DONE) {
|
||||
EVLOG_error << this->database_connection->get_error_message();
|
||||
return false;
|
||||
}
|
||||
|
||||
this->device_model = create_device_model(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DeviceModelTestHelper::update_variable_characteristics(const VariableCharacteristics& characteristics,
|
||||
const std::string& component_name,
|
||||
const std::optional<std::string>& component_instance,
|
||||
const std::optional<std::uint32_t>& evse_id,
|
||||
const std::optional<std::uint32_t>& connector_id,
|
||||
const std::string& variable_name,
|
||||
const std::optional<std::string>& variable_instance) {
|
||||
const std::string update_query =
|
||||
"UPDATE VARIABLE_CHARACTERISTICS SET DATATYPE_ID=@datatype_id, MAX_LIMIT=@max_limit, "
|
||||
"MIN_LIMIT=@min_limit, SUPPORTS_MONITORING=@supports_monitoring, UNIT=@unit, VALUES_LIST=@values_list WHERE "
|
||||
"VARIABLE_ID=(SELECT ID FROM VARIABLE WHERE COMPONENT_ID = "
|
||||
"(SELECT ID FROM COMPONENT WHERE NAME = @component_name AND INSTANCE IS @component_instance AND "
|
||||
"EVSE_ID IS @evse_id AND CONNECTOR_ID IS @connector_id) "
|
||||
"AND NAME = @variable_name AND INSTANCE IS @variable_instance)";
|
||||
|
||||
std::unique_ptr<StatementInterface> update_statement;
|
||||
try {
|
||||
update_statement = this->database_connection->new_statement(update_query);
|
||||
} catch (const QueryExecutionException&) {
|
||||
throw InitDeviceModelDbError("Could not create statement " + update_query);
|
||||
}
|
||||
|
||||
update_statement->bind_int("@datatype_id", static_cast<int>(characteristics.dataType));
|
||||
|
||||
const uint8_t supports_monitoring = (characteristics.supportsMonitoring ? 1 : 0);
|
||||
update_statement->bind_int("@supports_monitoring", supports_monitoring);
|
||||
|
||||
if (characteristics.unit.has_value()) {
|
||||
update_statement->bind_text("@unit", characteristics.unit.value(), SQLiteString::Transient);
|
||||
} else {
|
||||
update_statement->bind_null("@unit");
|
||||
}
|
||||
|
||||
if (characteristics.valuesList.has_value()) {
|
||||
update_statement->bind_text("@values_list", characteristics.valuesList.value(), SQLiteString::Transient);
|
||||
} else {
|
||||
update_statement->bind_null("@values_list");
|
||||
}
|
||||
|
||||
if (characteristics.maxLimit.has_value()) {
|
||||
update_statement->bind_double("@max_limit", static_cast<double>(characteristics.maxLimit.value()));
|
||||
} else {
|
||||
update_statement->bind_null("@max_limit");
|
||||
}
|
||||
|
||||
if (characteristics.minLimit.has_value()) {
|
||||
update_statement->bind_double("@min_limit", static_cast<double>(characteristics.minLimit.value()));
|
||||
} else {
|
||||
update_statement->bind_null("@min_limit");
|
||||
}
|
||||
|
||||
update_statement->bind_text("@component_name", component_name, SQLiteString::Transient);
|
||||
if (component_instance.has_value()) {
|
||||
update_statement->bind_text("@component_instance", component_instance.value(), SQLiteString::Transient);
|
||||
} else {
|
||||
update_statement->bind_null("@component_instance");
|
||||
}
|
||||
if (evse_id.has_value()) {
|
||||
update_statement->bind_int("@evse_id", evse_id.value());
|
||||
if (connector_id.has_value()) {
|
||||
update_statement->bind_int("@connector_id", connector_id.value());
|
||||
} else {
|
||||
update_statement->bind_null("@connector_id");
|
||||
}
|
||||
} else {
|
||||
update_statement->bind_null("@evse_id");
|
||||
update_statement->bind_null("@connector_id");
|
||||
}
|
||||
|
||||
update_statement->bind_text("@variable_name", variable_name, SQLiteString::Transient);
|
||||
if (variable_instance.has_value()) {
|
||||
update_statement->bind_text("@variable_instance", variable_instance.value(), SQLiteString::Transient);
|
||||
} else {
|
||||
update_statement->bind_null("@variable_instance");
|
||||
}
|
||||
|
||||
if (update_statement->step() != SQLITE_DONE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
this->device_model = create_device_model(false);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DeviceModelTestHelper::set_variable_attribute_value_null(const std::string& component_name,
|
||||
const std::optional<std::string>& component_instance,
|
||||
const std::optional<std::uint32_t>& evse_id,
|
||||
const std::optional<std::uint32_t>& connector_id,
|
||||
const std::string& variable_name,
|
||||
const std::optional<std::string>& variable_instance,
|
||||
const AttributeEnum& attribute_enum) {
|
||||
std::string update_query =
|
||||
"UPDATE VARIABLE_ATTRIBUTE SET VALUE=NULL WHERE VARIABLE_ID="
|
||||
"(SELECT ID FROM VARIABLE WHERE COMPONENT_ID = "
|
||||
"(SELECT ID FROM COMPONENT WHERE NAME = @component_name AND INSTANCE IS @component_instance AND "
|
||||
"EVSE_ID IS @evse_id AND CONNECTOR_ID IS @connector_id) "
|
||||
"AND NAME = @variable_name AND INSTANCE IS @variable_instance) "
|
||||
"AND TYPE_ID=@type_id";
|
||||
auto update_statement = this->database_connection->new_statement(update_query);
|
||||
|
||||
update_statement->bind_text("@component_name", component_name, SQLiteString::Transient);
|
||||
if (component_instance.has_value()) {
|
||||
update_statement->bind_text("@component_instance", component_instance.value(), SQLiteString::Transient);
|
||||
} else {
|
||||
update_statement->bind_null("@component_instance");
|
||||
}
|
||||
if (evse_id.has_value()) {
|
||||
update_statement->bind_int("@evse_id", evse_id.value());
|
||||
if (connector_id.has_value()) {
|
||||
update_statement->bind_int("@connector_id", connector_id.value());
|
||||
} else {
|
||||
update_statement->bind_null("@connector_id");
|
||||
}
|
||||
} else {
|
||||
update_statement->bind_null("@evse_id");
|
||||
update_statement->bind_null("@connector_id");
|
||||
}
|
||||
|
||||
update_statement->bind_text("@variable_name", variable_name, SQLiteString::Transient);
|
||||
if (variable_instance.has_value()) {
|
||||
update_statement->bind_text("@variable_instance", variable_instance.value(), SQLiteString::Transient);
|
||||
} else {
|
||||
update_statement->bind_null("@variable_instance");
|
||||
}
|
||||
|
||||
update_statement->bind_int("@type_id", static_cast<int>(attribute_enum));
|
||||
|
||||
if (update_statement->step() != SQLITE_DONE) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
void DeviceModelTestHelper::create_device_model_db() {
|
||||
InitDeviceModelDb db(this->database_path, this->migration_files_path);
|
||||
const auto component_configs = get_all_component_configs(this->config_path);
|
||||
db.initialize_database(component_configs, true);
|
||||
}
|
||||
|
||||
std::unique_ptr<DeviceModel> DeviceModelTestHelper::create_device_model(const bool init) {
|
||||
if (init) {
|
||||
create_device_model_db();
|
||||
}
|
||||
auto device_model_storage = std::make_unique<DeviceModelStorageSqlite>(this->database_path);
|
||||
auto dm = std::make_unique<DeviceModel>(std::move(device_model_storage));
|
||||
|
||||
return dm;
|
||||
}
|
||||
} // namespace ocpp::v2
|
||||
@@ -0,0 +1,128 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
/**
|
||||
* @file device_model_test_helper.hpp
|
||||
* @brief @copybrief ocpp::v2::DeviceModelTestHelper
|
||||
*
|
||||
* @class ocpp::v2::DeviceModelTestHelper
|
||||
* @brief Helper for tests where the device model is needed.
|
||||
*
|
||||
* If the device model is stored in memory, a database connection must be kept open at all times to prevent the
|
||||
* device model to be thrown away.
|
||||
*/
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <memory>
|
||||
|
||||
#include <ocpp/v2/device_model.hpp>
|
||||
#include <ocpp/v2/init_device_model_db.hpp>
|
||||
|
||||
const static std::string MIGRATION_FILES_PATH = "./resources/v2/device_model_migration_files";
|
||||
const static std::string CONFIG_PATH = "./resources/example_config/v2/component_config";
|
||||
const static std::string DEVICE_MODEL_DB_IN_MEMORY_PATH = "file::memory:?cache=shared";
|
||||
|
||||
namespace ocpp {
|
||||
namespace common {
|
||||
class Connection;
|
||||
}
|
||||
|
||||
namespace v2 {
|
||||
|
||||
class DeviceModelTestHelper {
|
||||
public:
|
||||
explicit DeviceModelTestHelper(const std::string& database_path = DEVICE_MODEL_DB_IN_MEMORY_PATH,
|
||||
const std::string& migration_files_path = MIGRATION_FILES_PATH,
|
||||
const std::string& config_path = CONFIG_PATH);
|
||||
DeviceModel* get_device_model();
|
||||
|
||||
///
|
||||
/// \brief Remove a variable from the database.
|
||||
/// \param component_name The component name.
|
||||
/// \param component_instance Component instance (optional).
|
||||
/// \param evse_id Evse id (optional).
|
||||
/// \param connector_id Connector id (optional).
|
||||
/// \param variable_name Variable name to remove.
|
||||
/// \param variable_instance Variable instance (optional).
|
||||
/// \return True on success.
|
||||
///
|
||||
/// \note After using this function, request a new device model with DeviceModelTestHelper::get_device_model(),
|
||||
/// because the device model has been changed in the database and the device model map has been read again.
|
||||
///
|
||||
bool remove_variable_from_db(const std::string& component_name,
|
||||
const std::optional<std::string>& component_instance,
|
||||
const std::optional<std::uint32_t>& evse_id,
|
||||
const std::optional<std::uint32_t>& connector_id, const std::string& variable_name,
|
||||
const std::optional<std::string>& variable_instance);
|
||||
|
||||
///
|
||||
/// \brief Update characteristics of a variable.
|
||||
/// \param characteristics The updated characteristics.
|
||||
/// \param component_name Component name.
|
||||
/// \param component_instance Component instance.
|
||||
/// \param evse_id The evse id.
|
||||
/// \param connector_id The connector id.
|
||||
/// \param variable_name The variable name.
|
||||
/// \param variable_instance The variable instance.
|
||||
/// \return True on success.
|
||||
///
|
||||
/// \note After using this function, request a new device model with DeviceModelTestHelper::get_device_model(),
|
||||
/// because the device model has been changed in the database and the device model map has been read again.
|
||||
/// \note We assume with this function that there each variable has only one characteristics entry.
|
||||
///
|
||||
bool update_variable_characteristics(const VariableCharacteristics& characteristics,
|
||||
const std::string& component_name,
|
||||
const std::optional<std::string>& component_instance,
|
||||
const std::optional<std::uint32_t>& evse_id,
|
||||
const std::optional<std::uint32_t>& connector_id,
|
||||
const std::string& variable_name,
|
||||
const std::optional<std::string>& variable_instance);
|
||||
|
||||
///
|
||||
/// \brief Set variable attribute to 'NULL'
|
||||
/// \param component_name Component name.
|
||||
/// \param component_instance Component instance.
|
||||
/// \param evse_id The evse id.
|
||||
/// \param connector_id The connector id.
|
||||
/// \param variable_name The variable name.
|
||||
/// \param variable_instance The variable instance.
|
||||
/// \param attribute_enum The variable attribute.
|
||||
/// \return True on success.
|
||||
///
|
||||
bool set_variable_attribute_value_null(const std::string& component_name,
|
||||
const std::optional<std::string>& component_instance,
|
||||
const std::optional<std::uint32_t>& evse_id,
|
||||
const std::optional<std::uint32_t>& connector_id,
|
||||
const std::string& variable_name,
|
||||
const std::optional<std::string>& variable_instance,
|
||||
const AttributeEnum& attribute_enum);
|
||||
|
||||
private:
|
||||
const std::string& database_path;
|
||||
const std::string& migration_files_path;
|
||||
const std::string& config_path;
|
||||
|
||||
// Connection as member so the database keeps open and is not destroyed (because this is an in memory
|
||||
// database).
|
||||
std::unique_ptr<everest::db::sqlite::Connection> database_connection;
|
||||
// Device model is a unique ptr here because of the database: it is stored in memory so as soon as the handle to
|
||||
// the database closes, the database is removed. So the handle should be opened before creating the devide model.
|
||||
// So the device model is initialized on nullptr, then the handle is opened, the devide model is created and the
|
||||
// handle stays open until the whole test is destructed.
|
||||
std::unique_ptr<DeviceModel> device_model;
|
||||
|
||||
///
|
||||
/// \brief Create the database for the device model and apply migrations.
|
||||
/// \param path Database path.
|
||||
///
|
||||
void create_device_model_db();
|
||||
|
||||
///
|
||||
/// \brief Create device model.
|
||||
/// \return The created device model.
|
||||
///
|
||||
std::unique_ptr<DeviceModel> create_device_model(const bool init = true);
|
||||
};
|
||||
} // namespace v2
|
||||
} // namespace ocpp
|
||||
@@ -0,0 +1,117 @@
|
||||
target_include_directories(libocpp_unit_tests PUBLIC
|
||||
../mocks
|
||||
${CMAKE_CURRENT_SOURCE_DIR})
|
||||
|
||||
target_sources(libocpp_unit_tests PRIVATE
|
||||
test_data_transfer.cpp
|
||||
test_reservation.cpp
|
||||
test_smart_charging.cpp)
|
||||
|
||||
|
||||
set(TEST_FUNCTIONAL_BLOCK_CONTEXT_SOURCES ${LIBOCPP_LIB_PATH}/ocpp/v2/average_meter_values.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/component_state_manager.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/connector.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/database_handler.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/evse.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/transaction.cpp)
|
||||
|
||||
|
||||
set(TEST_SECURITY_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/../device_model_test_helper.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../stubs/timer/timer_stub.cpp
|
||||
${TEST_FUNCTIONAL_BLOCK_CONTEXT_SOURCES}
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/functional_blocks/security.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/SecurityEventNotification.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/CertificateSigned.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/DeleteCertificate.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/GetInstalledCertificateIds.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/GetCompositeSchedule.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/InstallCertificate.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/SignCertificate.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/Get15118EVCertificate.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/Reset.cpp
|
||||
${LIBOCPP_TEST_INCLUDE_COMMON_SOURCES}
|
||||
${LIBOCPP_TEST_INCLUDE_V2_SOURCES}
|
||||
)
|
||||
|
||||
target_sources(libocpp_test_security PRIVATE
|
||||
${TEST_SECURITY_SOURCES})
|
||||
|
||||
target_include_directories(libocpp_test_security PUBLIC
|
||||
${LIBOCPP_INCLUDE_PATH}
|
||||
${LIBOCPP_3RDPARTY_PATH}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../stubs/timer
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../mocks
|
||||
${LIBOCPP_TESTS_V2_ROOT_DIR}
|
||||
)
|
||||
|
||||
|
||||
set(TEST_AUTHORIZATION_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/../device_model_test_helper.cpp
|
||||
${TEST_FUNCTIONAL_BLOCK_CONTEXT_SOURCES}
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/functional_blocks/authorization.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/ctrlr_component_variables.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/Authorize.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/ClearCache.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/GetCompositeSchedule.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/GetLocalListVersion.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/SendLocalList.cpp
|
||||
${LIBOCPP_TEST_INCLUDE_COMMON_SOURCES}
|
||||
${LIBOCPP_TEST_INCLUDE_V2_SOURCES}
|
||||
)
|
||||
|
||||
target_sources(libocpp_test_authorization PRIVATE
|
||||
${TEST_AUTHORIZATION_SOURCES})
|
||||
|
||||
target_include_directories(libocpp_test_authorization PUBLIC
|
||||
${LIBOCPP_INCLUDE_PATH}
|
||||
${LIBOCPP_3RDPARTY_PATH}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../mocks
|
||||
${LIBOCPP_TESTS_V2_ROOT_DIR}
|
||||
)
|
||||
|
||||
set(TEST_AVAILABILITY_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/../device_model_test_helper.cpp
|
||||
${TEST_FUNCTIONAL_BLOCK_CONTEXT_SOURCES}
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/functional_blocks/availability.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/ctrlr_component_variables.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/ChangeAvailability.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/GetCompositeSchedule.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/Heartbeat.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/StatusNotification.cpp
|
||||
${LIBOCPP_TEST_INCLUDE_COMMON_SOURCES}
|
||||
${LIBOCPP_TEST_INCLUDE_V2_SOURCES}
|
||||
)
|
||||
|
||||
target_sources(libocpp_test_availability PRIVATE
|
||||
${TEST_AVAILABILITY_SOURCES})
|
||||
|
||||
target_include_directories(libocpp_test_availability PUBLIC
|
||||
${LIBOCPP_INCLUDE_PATH}
|
||||
${LIBOCPP_3RDPARTY_PATH}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../mocks
|
||||
${LIBOCPP_TESTS_V2_ROOT_DIR}
|
||||
)
|
||||
|
||||
set(TEST_TARIFF_AND_COST_SOURCES ${CMAKE_CURRENT_SOURCE_DIR}/../device_model_test_helper.cpp
|
||||
${TEST_FUNCTIONAL_BLOCK_CONTEXT_SOURCES}
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/functional_blocks/tariff_and_cost.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/functional_blocks/display_message.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/ctrlr_component_variables.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/ClearDisplayMessage.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/GetCompositeSchedule.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/GetDisplayMessages.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/NotifyDisplayMessages.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/SetDisplayMessage.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/CostUpdated.cpp
|
||||
${LIBOCPP_LIB_PATH}/ocpp/v2/messages/TransactionEvent.cpp
|
||||
${LIBOCPP_TEST_INCLUDE_COMMON_SOURCES}
|
||||
${LIBOCPP_TEST_INCLUDE_V2_SOURCES}
|
||||
)
|
||||
|
||||
target_sources(libocpp_test_tariff_and_cost PRIVATE
|
||||
${TEST_TARIFF_AND_COST_SOURCES})
|
||||
|
||||
target_include_directories(libocpp_test_tariff_and_cost PUBLIC
|
||||
${LIBOCPP_INCLUDE_PATH}
|
||||
${LIBOCPP_3RDPARTY_PATH}
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/../mocks
|
||||
${LIBOCPP_TESTS_V2_ROOT_DIR}
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,537 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <ocpp/v2/functional_blocks/availability.hpp>
|
||||
|
||||
#include <ocpp/v2/ctrlr_component_variables.hpp>
|
||||
#include <ocpp/v2/device_model.hpp>
|
||||
#include <ocpp/v2/functional_blocks/functional_block_context.hpp>
|
||||
|
||||
#include <ocpp/v2/messages/Heartbeat.hpp>
|
||||
#include <ocpp/v2/messages/StatusNotification.hpp>
|
||||
|
||||
#include "component_state_manager_mock.hpp"
|
||||
#include "connectivity_manager_mock.hpp"
|
||||
#include "device_model_test_helper.hpp"
|
||||
#include "evse_manager_fake.hpp"
|
||||
// #include "evse_manager_mock.hpp"
|
||||
#include "evse_mock.hpp"
|
||||
#include "evse_security_mock.hpp"
|
||||
#include "message_dispatcher_mock.hpp"
|
||||
#include "mocks/database_handler_mock.hpp"
|
||||
|
||||
using namespace ocpp::v2;
|
||||
using testing::_;
|
||||
using testing::Invoke;
|
||||
using testing::MockFunction;
|
||||
using testing::Return;
|
||||
using testing::ReturnRef;
|
||||
|
||||
using EvseIteratorImpl = ocpp::VectorOfUniquePtrIterator<EvseInterface>;
|
||||
|
||||
class AvailabilityTest : public ::testing::Test {
|
||||
protected: // Members
|
||||
DeviceModelTestHelper device_model_test_helper;
|
||||
MockMessageDispatcher mock_dispatcher;
|
||||
DeviceModel* device_model;
|
||||
::testing::NiceMock<ConnectivityManagerMock> connectivity_manager;
|
||||
::testing::NiceMock<ocpp::v2::DatabaseHandlerMock> database_handler_mock;
|
||||
ocpp::EvseSecurityMock evse_security;
|
||||
EvseManagerFake evse_manager;
|
||||
ComponentStateManagerMock component_state_manager;
|
||||
std::atomic<ocpp::OcppProtocolVersion> ocpp_version;
|
||||
FunctionalBlockContext functional_block_context;
|
||||
MockFunction<void(const ocpp::DateTime& currentTime)> time_sync_callback;
|
||||
MockFunction<void()> all_connectors_unavailable_callback;
|
||||
EvseMock& evse_1;
|
||||
EvseMock& evse_2;
|
||||
std::unique_ptr<Availability> availability;
|
||||
|
||||
protected: // Functions
|
||||
AvailabilityTest() :
|
||||
device_model_test_helper(),
|
||||
mock_dispatcher(),
|
||||
device_model(device_model_test_helper.get_device_model()),
|
||||
connectivity_manager(),
|
||||
database_handler_mock(),
|
||||
evse_security(),
|
||||
evse_manager(2),
|
||||
component_state_manager(),
|
||||
ocpp_version(ocpp::OcppProtocolVersion::v201),
|
||||
functional_block_context{
|
||||
this->mock_dispatcher, *this->device_model, this->connectivity_manager, this->evse_manager,
|
||||
this->database_handler_mock, this->evse_security, this->component_state_manager, this->ocpp_version},
|
||||
evse_1(evse_manager.get_mock(1)),
|
||||
evse_2(evse_manager.get_mock(2)),
|
||||
availability(std::make_unique<Availability>(functional_block_context, time_sync_callback.AsStdFunction(),
|
||||
all_connectors_unavailable_callback.AsStdFunction())) {
|
||||
}
|
||||
|
||||
ocpp::EnhancedMessage<MessageType>
|
||||
create_example_change_availability_request(const OperationalStatusEnum operational_status,
|
||||
const std::optional<std::int32_t> evse_id,
|
||||
const std::optional<std::int32_t> connector_id) {
|
||||
ChangeAvailabilityRequest request;
|
||||
request.operationalStatus = operational_status;
|
||||
if (evse_id.has_value()) {
|
||||
EVSE evse;
|
||||
evse.id = evse_id.value();
|
||||
if (connector_id.has_value()) {
|
||||
evse.connectorId = connector_id.value();
|
||||
}
|
||||
request.evse = evse;
|
||||
}
|
||||
ocpp::Call<ChangeAvailabilityRequest> call(request);
|
||||
ocpp::EnhancedMessage<MessageType> enhanced_message;
|
||||
enhanced_message.messageType = MessageType::ChangeAvailability;
|
||||
enhanced_message.message = call;
|
||||
return enhanced_message;
|
||||
}
|
||||
|
||||
ocpp::EnhancedMessage<MessageType> create_example_heartbeat_response(const ocpp::DateTime& current_time) {
|
||||
HeartbeatResponse response;
|
||||
response.currentTime = current_time;
|
||||
ocpp::CallResult<HeartbeatResponse> call_result(response, "uniqueId");
|
||||
ocpp::EnhancedMessage<MessageType> enhanced_message;
|
||||
enhanced_message.messageType = MessageType::HeartbeatResponse;
|
||||
enhanced_message.message = call_result;
|
||||
return enhanced_message;
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(AvailabilityTest, heartbeat_req) {
|
||||
// When heartbeat request is called, a HeartBeatRequest should be sent to the message dispatcher.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).WillOnce(Invoke([](const json& call, bool /*triggered*/) {
|
||||
const auto message = call[ocpp::CALL_PAYLOAD].get<HeartbeatRequest>();
|
||||
EXPECT_EQ(message.get_type(), "Heartbeat");
|
||||
}));
|
||||
availability->heartbeat_req(false);
|
||||
}
|
||||
|
||||
TEST_F(AvailabilityTest, status_notification_req) {
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).WillOnce(Invoke([](const json& call, bool triggered) {
|
||||
const auto message = call[ocpp::CALL_PAYLOAD].get<StatusNotificationRequest>();
|
||||
EXPECT_EQ(message.connectorStatus, ConnectorStatusEnum::Unavailable);
|
||||
EXPECT_EQ(message.connectorId, 2);
|
||||
EXPECT_EQ(message.evseId, 1);
|
||||
EXPECT_LE(message.timestamp, ocpp::DateTime());
|
||||
EXPECT_FALSE(triggered);
|
||||
}));
|
||||
|
||||
availability->status_notification_req(1, 2, ConnectorStatusEnum::Unavailable, false);
|
||||
}
|
||||
|
||||
TEST_F(AvailabilityTest, handle_message_not_implemented) {
|
||||
// Only 'ChangeAvailability' and 'HeartbeatResponse' are implemented.
|
||||
ocpp::EnhancedMessage<MessageType> enhanced_message;
|
||||
enhanced_message.messageType = MessageType::ClearDisplayMessage;
|
||||
EXPECT_THROW(availability->handle_message(enhanced_message), MessageTypeNotImplementedException);
|
||||
}
|
||||
|
||||
TEST_F(AvailabilityTest, handle_message_heartbeat_response_timesource_not_heartbeat) {
|
||||
// When a heartbeat response is received and the time source is not 'Heartbeat', the time sync callback is not
|
||||
// called.
|
||||
const auto heartbeat_response = create_example_heartbeat_response(ocpp::DateTime());
|
||||
auto time_source_variable = ControllerComponentVariables::TimeSource;
|
||||
device_model->set_value(time_source_variable.component, time_source_variable.variable.value(),
|
||||
AttributeEnum::Actual, "NTP", "test", true);
|
||||
EXPECT_CALL(time_sync_callback, Call(_)).Times(0);
|
||||
availability->handle_message(heartbeat_response);
|
||||
}
|
||||
|
||||
TEST_F(AvailabilityTest, handle_message_heartbeat_response_timesource_heartbeat) {
|
||||
// When a heartbeat response is received and the time source is 'Heartbeat', the time sync callback should be
|
||||
// called.
|
||||
const auto heartbeat_response = create_example_heartbeat_response(ocpp::DateTime());
|
||||
auto time_source_variable = ControllerComponentVariables::TimeSource;
|
||||
device_model->set_value(time_source_variable.component, time_source_variable.variable.value(),
|
||||
AttributeEnum::Actual, "Heartbeat", "test", true);
|
||||
EXPECT_CALL(time_sync_callback, Call(_)).Times(1);
|
||||
availability->handle_message(heartbeat_response);
|
||||
}
|
||||
|
||||
TEST_F(AvailabilityTest, handle_scheduled_changed_availability_requests_nothing_scheduled) {
|
||||
// Call handle_scheduled_change_availability_requests without having any change availability requests scheduled.
|
||||
ON_CALL(evse_manager, any_transaction_active(_)).WillByDefault(Return(false));
|
||||
ON_CALL(evse_1, get_connector_effective_operational_status(_))
|
||||
.WillByDefault(Return(OperationalStatusEnum::Inoperative));
|
||||
ON_CALL(evse_2, get_connector_effective_operational_status(_))
|
||||
.WillByDefault(Return(OperationalStatusEnum::Inoperative));
|
||||
EXPECT_CALL(all_connectors_unavailable_callback, Call()).Times(0);
|
||||
this->availability->handle_scheduled_change_availability_requests(1);
|
||||
}
|
||||
|
||||
TEST_F(AvailabilityTest, handle_scheduled_changed_availability_requests_transaction_active) {
|
||||
// Call handle_scheduled_change_availability_requests with a current active transaction. This will not call the
|
||||
// all_connectors_unavailable callback.
|
||||
ON_CALL(evse_manager, any_transaction_active(_)).WillByDefault(Return(true));
|
||||
ON_CALL(evse_1, get_connector_effective_operational_status(_))
|
||||
.WillByDefault(Return(OperationalStatusEnum::Inoperative));
|
||||
ON_CALL(evse_2, get_connector_effective_operational_status(_))
|
||||
.WillByDefault(Return(OperationalStatusEnum::Inoperative));
|
||||
EVSE evse;
|
||||
evse.id = 1;
|
||||
AvailabilityChange change;
|
||||
change.persist = false;
|
||||
change.request.evse = evse;
|
||||
change.request.operationalStatus = OperationalStatusEnum::Inoperative;
|
||||
this->availability->set_scheduled_change_availability_requests(1, change);
|
||||
EXPECT_CALL(all_connectors_unavailable_callback, Call()).Times(0);
|
||||
this->availability->handle_scheduled_change_availability_requests(1);
|
||||
}
|
||||
|
||||
TEST_F(AvailabilityTest, handle_scheduled_changed_availability_requests_no_transaction_active_not_all_inoperative) {
|
||||
// Call handle_scheduled_change_availability_requests with no active transaction, but not all connectors are
|
||||
// inoperative. This will not call the all_connectors_unavailable callback.
|
||||
// Only an evse will be set to inoperative.
|
||||
ON_CALL(evse_manager, any_transaction_active(_)).WillByDefault(Return(false));
|
||||
ON_CALL(evse_1, get_connector_effective_operational_status(_))
|
||||
.WillByDefault(Return(OperationalStatusEnum::Operative));
|
||||
ON_CALL(evse_2, get_connector_effective_operational_status(_))
|
||||
.WillByDefault(Return(OperationalStatusEnum::Operative));
|
||||
ON_CALL(evse_1, get_number_of_connectors()).WillByDefault(Return(1));
|
||||
ON_CALL(evse_2, get_number_of_connectors()).WillByDefault(Return(1));
|
||||
ON_CALL(evse_1, has_active_transaction(_)).WillByDefault(Return(false));
|
||||
ON_CALL(evse_2, has_active_transaction(_)).WillByDefault(Return(false));
|
||||
EXPECT_CALL(evse_1, set_evse_operative_status(OperationalStatusEnum::Inoperative, false));
|
||||
EVSE evse;
|
||||
evse.id = 1;
|
||||
AvailabilityChange change;
|
||||
change.persist = false;
|
||||
change.request.evse = evse;
|
||||
change.request.operationalStatus = OperationalStatusEnum::Inoperative;
|
||||
this->availability->set_scheduled_change_availability_requests(1, change);
|
||||
EXPECT_CALL(all_connectors_unavailable_callback, Call()).Times(0);
|
||||
this->availability->handle_scheduled_change_availability_requests(1);
|
||||
}
|
||||
|
||||
TEST_F(AvailabilityTest, handle_scheduled_changed_availability_requests_no_transaction_active_all_inoperative) {
|
||||
// Call handle_scheduled_change_availability_requests with no active transaction, and all connectors are
|
||||
// inoperative. This will call the all_connectors_unavailable callback.
|
||||
// This time a connector is set to inoperative and persist is true.
|
||||
ON_CALL(evse_manager, any_transaction_active(_)).WillByDefault(Return(false));
|
||||
ON_CALL(evse_1, get_connector_effective_operational_status(_))
|
||||
.WillByDefault(Return(OperationalStatusEnum::Inoperative));
|
||||
ON_CALL(evse_2, get_connector_effective_operational_status(_))
|
||||
.WillByDefault(Return(OperationalStatusEnum::Inoperative));
|
||||
ON_CALL(evse_1, get_number_of_connectors()).WillByDefault(Return(1));
|
||||
ON_CALL(evse_2, get_number_of_connectors()).WillByDefault(Return(1));
|
||||
ON_CALL(evse_1, has_active_transaction(_)).WillByDefault(Return(false));
|
||||
ON_CALL(evse_2, has_active_transaction(_)).WillByDefault(Return(false));
|
||||
EXPECT_CALL(evse_1, set_connector_operative_status(2, OperationalStatusEnum::Inoperative, true));
|
||||
EVSE evse;
|
||||
evse.id = 1;
|
||||
evse.connectorId = 2;
|
||||
AvailabilityChange change;
|
||||
change.persist = true;
|
||||
change.request.evse = evse;
|
||||
change.request.operationalStatus = OperationalStatusEnum::Inoperative;
|
||||
this->availability->set_scheduled_change_availability_requests(1, change);
|
||||
EXPECT_CALL(all_connectors_unavailable_callback, Call()).Times(1);
|
||||
this->availability->handle_scheduled_change_availability_requests(1);
|
||||
}
|
||||
|
||||
TEST_F(AvailabilityTest, handle_scheduled_change_availability_requests_negative_evseid) {
|
||||
// Call handle_scheduled_change_availability_requests with a non existing (negative) evse id. This will not call any
|
||||
// callback.
|
||||
EXPECT_CALL(evse_manager, any_transaction_active(_)).Times(0);
|
||||
EVSE evse;
|
||||
evse.id = 1;
|
||||
evse.connectorId = 2;
|
||||
AvailabilityChange change;
|
||||
change.persist = true;
|
||||
change.request.evse = evse;
|
||||
change.request.operationalStatus = OperationalStatusEnum::Inoperative;
|
||||
this->availability->set_scheduled_change_availability_requests(1, change);
|
||||
EXPECT_CALL(all_connectors_unavailable_callback, Call()).Times(0);
|
||||
this->availability->handle_scheduled_change_availability_requests(-9999);
|
||||
}
|
||||
|
||||
TEST_F(AvailabilityTest, handle_message_change_availability_cs_operative) {
|
||||
ocpp::EnhancedMessage<MessageType> request =
|
||||
create_example_change_availability_request(OperationalStatusEnum::Operative, std::nullopt, std::nullopt);
|
||||
|
||||
// No EVSE, but if it requests if it's valid, return true. No transaction active. Operational status is Operative.
|
||||
ON_CALL(evse_manager, any_transaction_active(_)).WillByDefault(Return(false));
|
||||
ON_CALL(component_state_manager, get_cs_individual_operational_status())
|
||||
.WillByDefault(Return(OperationalStatusEnum::Operative));
|
||||
EXPECT_CALL(component_state_manager, set_cs_individual_operational_status(OperationalStatusEnum::Operative, true));
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
const auto message = call_result[ocpp::CALLRESULT_PAYLOAD].get<ChangeAvailabilityResponse>();
|
||||
EXPECT_EQ(message.status, ChangeAvailabilityStatusEnum::Accepted);
|
||||
}));
|
||||
|
||||
this->availability->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(AvailabilityTest, handle_message_change_availability_cs_inoperative_transaction_active) {
|
||||
ocpp::EnhancedMessage<MessageType> request =
|
||||
create_example_change_availability_request(OperationalStatusEnum::Inoperative, std::nullopt, std::nullopt);
|
||||
|
||||
// No EVSE, but if it requests if it's valid, return true. There is an active transaction. Operational status is
|
||||
// currently Operative.
|
||||
ON_CALL(evse_manager, any_transaction_active(_)).WillByDefault(Return(true));
|
||||
ON_CALL(component_state_manager, get_cs_individual_operational_status())
|
||||
.WillByDefault(Return(OperationalStatusEnum::Operative));
|
||||
EXPECT_CALL(component_state_manager, set_cs_individual_operational_status(OperationalStatusEnum::Operative, true))
|
||||
.Times(0);
|
||||
|
||||
// The CS will be scheduled to set to inoperative, but all evse's that do not have an active transaction will
|
||||
// already be set to inoperative.
|
||||
ON_CALL(evse_1, has_active_transaction()).WillByDefault(Return(false));
|
||||
ON_CALL(evse_2, has_active_transaction()).WillByDefault(Return(true));
|
||||
|
||||
// EVSE 1 has no active transaction, EVSE 2 has, so EVSE 1 will be set to inoperative.
|
||||
EXPECT_CALL(evse_1, set_evse_operative_status(OperationalStatusEnum::Inoperative, false));
|
||||
EXPECT_CALL(evse_2, set_evse_operative_status(OperationalStatusEnum::Inoperative, false)).Times(0);
|
||||
|
||||
// And since there is an active transaction, changing of the availability will be scheduled.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
const auto message = call_result[ocpp::CALLRESULT_PAYLOAD].get<ChangeAvailabilityResponse>();
|
||||
EXPECT_EQ(message.status, ChangeAvailabilityStatusEnum::Scheduled);
|
||||
}));
|
||||
|
||||
this->availability->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(AvailabilityTest, handle_message_change_availability_cs_inoperative_transaction_not_active) {
|
||||
ocpp::EnhancedMessage<MessageType> request =
|
||||
create_example_change_availability_request(OperationalStatusEnum::Inoperative, std::nullopt, std::nullopt);
|
||||
|
||||
// No EVSE, but if it requests if it's valid, return true. There is no active transaction. Operational status is
|
||||
// currently Operative.
|
||||
ON_CALL(evse_manager, any_transaction_active(_)).WillByDefault(Return(false));
|
||||
ON_CALL(component_state_manager, get_cs_individual_operational_status())
|
||||
.WillByDefault(Return(OperationalStatusEnum::Operative));
|
||||
|
||||
// The CS will be scheduled to set to inoperative, but all evse's that do not have an active transaction will
|
||||
// already be set to inoperative.
|
||||
ON_CALL(evse_1, has_active_transaction()).WillByDefault(Return(false));
|
||||
ON_CALL(evse_2, has_active_transaction()).WillByDefault(Return(false));
|
||||
|
||||
// CS has no active transaction, so it will be set to inoperative.
|
||||
EXPECT_CALL(component_state_manager,
|
||||
set_cs_individual_operational_status(OperationalStatusEnum::Inoperative, true));
|
||||
|
||||
// And since there is an active transaction, changing of the availability will be scheduled.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
const auto message = call_result[ocpp::CALLRESULT_PAYLOAD].get<ChangeAvailabilityResponse>();
|
||||
EXPECT_EQ(message.status, ChangeAvailabilityStatusEnum::Accepted);
|
||||
}));
|
||||
|
||||
this->availability->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(AvailabilityTest, handle_message_change_availability_cs_operative_transaction_not_active) {
|
||||
ocpp::EnhancedMessage<MessageType> request =
|
||||
create_example_change_availability_request(OperationalStatusEnum::Operative, std::nullopt, std::nullopt);
|
||||
|
||||
// No EVSE, but if it requests if it's valid, return true. There is no active transaction. Operational status is
|
||||
// currently Operative.
|
||||
ON_CALL(evse_manager, any_transaction_active(_)).WillByDefault(Return(false));
|
||||
ON_CALL(component_state_manager, get_cs_individual_operational_status())
|
||||
.WillByDefault(Return(OperationalStatusEnum::Operative));
|
||||
|
||||
// The CS will be scheduled to set to inoperative, but all evse's that do not have an active transaction will
|
||||
// already be set to inoperative.
|
||||
ON_CALL(evse_1, has_active_transaction()).WillByDefault(Return(false));
|
||||
ON_CALL(evse_2, has_active_transaction()).WillByDefault(Return(false));
|
||||
|
||||
// EVSE's have no active transaction, so they will be set to inoperative.
|
||||
EXPECT_CALL(component_state_manager, set_cs_individual_operational_status(OperationalStatusEnum::Operative, true));
|
||||
|
||||
// And since the CS is already in the Operative state, the status is 'accepted'.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
const auto message = call_result[ocpp::CALLRESULT_PAYLOAD].get<ChangeAvailabilityResponse>();
|
||||
EXPECT_EQ(message.status, ChangeAvailabilityStatusEnum::Accepted);
|
||||
}));
|
||||
|
||||
this->availability->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(AvailabilityTest, handle_message_change_availability_wrong_evse) {
|
||||
ocpp::EnhancedMessage<MessageType> request =
|
||||
create_example_change_availability_request(OperationalStatusEnum::Operative, 42, std::nullopt);
|
||||
|
||||
// Because the evse id does not exist, 'Rejected' is returned.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
const auto message = call_result[ocpp::CALLRESULT_PAYLOAD].get<ChangeAvailabilityResponse>();
|
||||
EXPECT_EQ(message.status, ChangeAvailabilityStatusEnum::Rejected);
|
||||
}));
|
||||
|
||||
this->availability->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(AvailabilityTest, handle_message_change_availability_evse_operative_transaction_active) {
|
||||
ocpp::EnhancedMessage<MessageType> request =
|
||||
create_example_change_availability_request(OperationalStatusEnum::Operative, 2, std::nullopt);
|
||||
|
||||
// Change availability for an evse when there is an active transaction
|
||||
ON_CALL(evse_manager, any_transaction_active(_)).WillByDefault(Return(true));
|
||||
ON_CALL(evse_1, has_active_transaction()).WillByDefault(Return(true));
|
||||
ON_CALL(evse_2, has_active_transaction()).WillByDefault(Return(true));
|
||||
ON_CALL(component_state_manager, get_evse_individual_operational_status(_))
|
||||
.WillByDefault(Return(OperationalStatusEnum::Operative));
|
||||
|
||||
// And since the EVSE is already in the Operative state, 'Accepted' is returned.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
const auto message = call_result[ocpp::CALLRESULT_PAYLOAD].get<ChangeAvailabilityResponse>();
|
||||
EXPECT_EQ(message.status, ChangeAvailabilityStatusEnum::Accepted);
|
||||
}));
|
||||
|
||||
EXPECT_CALL(component_state_manager, set_cs_individual_operational_status(OperationalStatusEnum::Operative, _))
|
||||
.Times(0);
|
||||
EXPECT_CALL(evse_1, set_evse_operative_status(_, _)).Times(0);
|
||||
EXPECT_CALL(evse_2, set_evse_operative_status(_, _)).Times(0);
|
||||
|
||||
this->availability->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(AvailabilityTest, handle_message_change_availability_connector_operative_transaction_inactive) {
|
||||
ocpp::EnhancedMessage<MessageType> request =
|
||||
create_example_change_availability_request(OperationalStatusEnum::Operative, 2, 1);
|
||||
|
||||
// Change availability for a connector when there is no active transaction and the connector is currently
|
||||
// inoperative
|
||||
ON_CALL(evse_manager, any_transaction_active(_)).WillByDefault(Return(false));
|
||||
ON_CALL(evse_1, has_active_transaction()).WillByDefault(Return(false));
|
||||
ON_CALL(evse_2, has_active_transaction()).WillByDefault(Return(false));
|
||||
ON_CALL(component_state_manager, get_evse_individual_operational_status(_))
|
||||
.WillByDefault(Return(OperationalStatusEnum::Inoperative));
|
||||
ON_CALL(component_state_manager, get_connector_individual_operational_status(_, _))
|
||||
.WillByDefault(Return(OperationalStatusEnum::Inoperative));
|
||||
|
||||
// And since the EVSE is already in the Operative state, 'Accepted' is returned.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
const auto message = call_result[ocpp::CALLRESULT_PAYLOAD].get<ChangeAvailabilityResponse>();
|
||||
EXPECT_EQ(message.status, ChangeAvailabilityStatusEnum::Accepted);
|
||||
}));
|
||||
|
||||
EXPECT_CALL(component_state_manager, set_cs_individual_operational_status(OperationalStatusEnum::Operative, _))
|
||||
.Times(0);
|
||||
EXPECT_CALL(evse_1, set_connector_operative_status(1, OperationalStatusEnum::Operative, _)).Times(0);
|
||||
EXPECT_CALL(evse_2, set_connector_operative_status(1, OperationalStatusEnum::Operative, _));
|
||||
|
||||
this->availability->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(AvailabilityTest, handle_message_change_availability_evse_inoperative_transaction_active) {
|
||||
ocpp::EnhancedMessage<MessageType> request =
|
||||
create_example_change_availability_request(OperationalStatusEnum::Inoperative, 1, std::nullopt);
|
||||
|
||||
// Change availability to inoperative for an evse when there is an active transaction and the connector is currently
|
||||
// operative
|
||||
ON_CALL(evse_manager, any_transaction_active(_)).WillByDefault(Return(true));
|
||||
ON_CALL(evse_1, has_active_transaction()).WillByDefault(Return(true));
|
||||
ON_CALL(evse_2, has_active_transaction()).WillByDefault(Return(true));
|
||||
ON_CALL(component_state_manager, get_evse_individual_operational_status(_))
|
||||
.WillByDefault(Return(OperationalStatusEnum::Operative));
|
||||
ON_CALL(component_state_manager, get_connector_individual_operational_status(_, _))
|
||||
.WillByDefault(Return(OperationalStatusEnum::Operative));
|
||||
|
||||
// And since the EVSE is already in the Operative state, 'Accepted' is returned.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
const auto message = call_result[ocpp::CALLRESULT_PAYLOAD].get<ChangeAvailabilityResponse>();
|
||||
EXPECT_EQ(message.status, ChangeAvailabilityStatusEnum::Scheduled);
|
||||
}));
|
||||
|
||||
EXPECT_CALL(component_state_manager, set_cs_individual_operational_status(OperationalStatusEnum::Inoperative, _))
|
||||
.Times(0);
|
||||
EXPECT_CALL(evse_1, set_evse_operative_status(OperationalStatusEnum::Inoperative, _)).Times(0);
|
||||
EXPECT_CALL(evse_2, set_evse_operative_status(OperationalStatusEnum::Inoperative, _)).Times(0);
|
||||
|
||||
this->availability->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(AvailabilityTest, handle_message_change_availability_evse_inoperative_transaction_active_2) {
|
||||
ocpp::EnhancedMessage<MessageType> request =
|
||||
create_example_change_availability_request(OperationalStatusEnum::Inoperative, 1, std::nullopt);
|
||||
|
||||
// Change availability to inoperative for an evse when there is an active transaction and the connector is currently
|
||||
// operative
|
||||
ON_CALL(evse_manager, any_transaction_active(_)).WillByDefault(Return(true));
|
||||
ON_CALL(evse_1, has_active_transaction()).WillByDefault(Return(false));
|
||||
ON_CALL(evse_2, has_active_transaction()).WillByDefault(Return(true));
|
||||
ON_CALL(evse_1, get_number_of_connectors()).WillByDefault(Return(2));
|
||||
ON_CALL(evse_2, get_number_of_connectors()).WillByDefault(Return(2));
|
||||
ON_CALL(component_state_manager, get_evse_individual_operational_status(_))
|
||||
.WillByDefault(Return(OperationalStatusEnum::Operative));
|
||||
ON_CALL(component_state_manager, get_connector_individual_operational_status(_, _))
|
||||
.WillByDefault(Return(OperationalStatusEnum::Operative));
|
||||
|
||||
// And since the EVSE is already in the Operative state, 'Accepted' is returned.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
const auto message = call_result[ocpp::CALLRESULT_PAYLOAD].get<ChangeAvailabilityResponse>();
|
||||
EXPECT_EQ(message.status, ChangeAvailabilityStatusEnum::Scheduled);
|
||||
}));
|
||||
|
||||
// All connectors of the evse are set to inoperative.
|
||||
EXPECT_CALL(component_state_manager, set_cs_individual_operational_status(OperationalStatusEnum::Inoperative, _))
|
||||
.Times(0);
|
||||
EXPECT_CALL(evse_1, set_connector_operative_status(1, OperationalStatusEnum::Inoperative, _));
|
||||
EXPECT_CALL(evse_1, set_connector_operative_status(2, OperationalStatusEnum::Inoperative, _));
|
||||
EXPECT_CALL(evse_2, set_connector_operative_status(1, OperationalStatusEnum::Inoperative, _)).Times(0);
|
||||
|
||||
this->availability->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(AvailabilityTest,
|
||||
handle_message_change_availability_evse_inoperative_transaction_active_connector_has_active_transaction) {
|
||||
ocpp::EnhancedMessage<MessageType> request =
|
||||
create_example_change_availability_request(OperationalStatusEnum::Inoperative, 2, std::nullopt);
|
||||
|
||||
// Change availability to inoperative for an evse when there is an active transaction and the connector is currently
|
||||
// operative
|
||||
ON_CALL(evse_manager, any_transaction_active(_)).WillByDefault(Return(true));
|
||||
ON_CALL(evse_1, has_active_transaction()).WillByDefault(Return(false));
|
||||
ON_CALL(evse_2, has_active_transaction()).WillByDefault(Return(true));
|
||||
ON_CALL(evse_2, has_active_transaction(1)).WillByDefault(Return(false));
|
||||
// Active transaction on connector 2, which will set connector 1 to inoperative and not touch connector 2.
|
||||
ON_CALL(evse_2, has_active_transaction(2)).WillByDefault(Return(true));
|
||||
ON_CALL(evse_1, get_number_of_connectors()).WillByDefault(Return(2));
|
||||
ON_CALL(evse_2, get_number_of_connectors()).WillByDefault(Return(2));
|
||||
ON_CALL(component_state_manager, get_evse_individual_operational_status(_))
|
||||
.WillByDefault(Return(OperationalStatusEnum::Operative));
|
||||
ON_CALL(component_state_manager, get_connector_individual_operational_status(_, _))
|
||||
.WillByDefault(Return(OperationalStatusEnum::Operative));
|
||||
|
||||
// And since the EVSE is already in the Operative state, 'Accepted' is returned.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
const auto message = call_result[ocpp::CALLRESULT_PAYLOAD].get<ChangeAvailabilityResponse>();
|
||||
EXPECT_EQ(message.status, ChangeAvailabilityStatusEnum::Scheduled);
|
||||
}));
|
||||
|
||||
// All connectors of the evse are set to inoperative.
|
||||
EXPECT_CALL(component_state_manager, set_cs_individual_operational_status(OperationalStatusEnum::Inoperative, _))
|
||||
.Times(0);
|
||||
EXPECT_CALL(evse_1, set_connector_operative_status(1, OperationalStatusEnum::Inoperative, _)).Times(0);
|
||||
EXPECT_CALL(evse_1, set_connector_operative_status(2, OperationalStatusEnum::Inoperative, _)).Times(0);
|
||||
// Connector 1 of evse 2 is set to inoperative because it has no active transaction, connector 2 has an active
|
||||
// transaction so the operational status is not changed.
|
||||
EXPECT_CALL(evse_2, set_connector_operative_status(1, OperationalStatusEnum::Inoperative, _));
|
||||
EXPECT_CALL(evse_2, set_connector_operative_status(2, OperationalStatusEnum::Inoperative, _)).Times(0);
|
||||
|
||||
this->availability->handle_message(request);
|
||||
|
||||
// There is now a scheduled change availability request. Let's stop the transaction and check if the evse will
|
||||
// be set to Inoperative now.
|
||||
ON_CALL(evse_manager, any_transaction_active(_)).WillByDefault(Return(false));
|
||||
EXPECT_CALL(evse_2, set_evse_operative_status(OperationalStatusEnum::Inoperative, _));
|
||||
this->availability->handle_scheduled_change_availability_requests(2);
|
||||
}
|
||||
|
||||
TEST_F(AvailabilityTest, set_heartbeat_timer_interval) {
|
||||
// When setting the heartbeat timer interval, a heartbeat request should be sent after the interval has ended.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).WillOnce(Invoke([](const json& call, bool /*triggered*/) {
|
||||
const auto message = call[ocpp::CALL_PAYLOAD].get<HeartbeatRequest>();
|
||||
EXPECT_EQ(message.get_type(), "Heartbeat");
|
||||
}));
|
||||
|
||||
// std::chrono::milliseconds ms(100);
|
||||
this->availability->set_heartbeat_timer_interval(std::chrono::seconds(1));
|
||||
usleep(1200000);
|
||||
}
|
||||
@@ -0,0 +1,219 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <ocpp/v2/functional_blocks/data_transfer.hpp>
|
||||
|
||||
#include "component_state_manager_mock.hpp"
|
||||
#include "connectivity_manager_mock.hpp"
|
||||
#include "evse_manager_fake.hpp"
|
||||
#include "evse_security_mock.hpp"
|
||||
#include "message_dispatcher_mock.hpp"
|
||||
#include "mocks/database_handler_mock.hpp"
|
||||
#include <ocpp/common/constants.hpp>
|
||||
#include <ocpp/v2/device_model.hpp>
|
||||
#include <ocpp/v2/functional_blocks/data_transfer.hpp>
|
||||
#include <ocpp/v2/functional_blocks/functional_block_context.hpp>
|
||||
#include <ocpp/v2/messages/DataTransfer.hpp>
|
||||
|
||||
using namespace ocpp::v2;
|
||||
using ::testing::_;
|
||||
using ::testing::Invoke;
|
||||
using ::testing::Return;
|
||||
|
||||
DataTransferRequest create_example_request() {
|
||||
DataTransferRequest request;
|
||||
request.vendorId = "TestVendor";
|
||||
request.messageId = "TestMessage";
|
||||
request.data = json{{"key", "value"}};
|
||||
return request;
|
||||
}
|
||||
|
||||
class DataTransferTest : public ::testing::Test {
|
||||
public:
|
||||
protected: // Members
|
||||
MockMessageDispatcher mock_dispatcher;
|
||||
DeviceModel* device_model;
|
||||
::testing::NiceMock<ConnectivityManagerMock> connectivity_manager;
|
||||
::testing::NiceMock<DatabaseHandlerMock> database_handler_mock;
|
||||
ocpp::EvseSecurityMock evse_security;
|
||||
EvseManagerFake evse_manager;
|
||||
ComponentStateManagerMock component_state_manager;
|
||||
std::atomic<ocpp::OcppProtocolVersion> ocpp_version;
|
||||
FunctionalBlockContext functional_block_context;
|
||||
|
||||
DataTransferTest() :
|
||||
mock_dispatcher(),
|
||||
device_model(nullptr),
|
||||
connectivity_manager(),
|
||||
database_handler_mock(),
|
||||
evse_security(),
|
||||
evse_manager(1),
|
||||
component_state_manager(),
|
||||
ocpp_version(ocpp::OcppProtocolVersion::v201),
|
||||
functional_block_context{
|
||||
this->mock_dispatcher, *this->device_model, this->connectivity_manager, this->evse_manager,
|
||||
this->database_handler_mock, this->evse_security, this->component_state_manager, this->ocpp_version} {
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(DataTransferTest, HandleDataTransferReq_NotImplemented) {
|
||||
DataTransfer data_transfer(functional_block_context, std::nullopt, ocpp::DEFAULT_WAIT_FOR_FUTURE_TIMEOUT);
|
||||
|
||||
DataTransferRequest request = create_example_request();
|
||||
ocpp::Call<DataTransferRequest> call(request);
|
||||
ocpp::EnhancedMessage<MessageType> enhanced_message;
|
||||
enhanced_message.messageType = MessageType::Authorize; // this cant be handled by DataTransfer functional block
|
||||
enhanced_message.message = call;
|
||||
|
||||
EXPECT_THROW(data_transfer.handle_message(enhanced_message), MessageTypeNotImplementedException);
|
||||
}
|
||||
|
||||
TEST_F(DataTransferTest, HandleDataTransferReq_NoCallback) {
|
||||
DataTransfer data_transfer(functional_block_context, std::nullopt, ocpp::DEFAULT_WAIT_FOR_FUTURE_TIMEOUT);
|
||||
|
||||
DataTransferRequest request = create_example_request();
|
||||
ocpp::Call<DataTransferRequest> call(request);
|
||||
ocpp::EnhancedMessage<MessageType> enhanced_message;
|
||||
enhanced_message.messageType = MessageType::DataTransfer;
|
||||
enhanced_message.message = call;
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<DataTransferResponse>();
|
||||
EXPECT_EQ(response.status, DataTransferStatusEnum::UnknownVendorId);
|
||||
}));
|
||||
|
||||
data_transfer.handle_message(enhanced_message);
|
||||
}
|
||||
|
||||
TEST_F(DataTransferTest, HandleDataTransferReq_WithCallback) {
|
||||
auto callback = [](const DataTransferRequest&) {
|
||||
DataTransferResponse response;
|
||||
response.status = DataTransferStatusEnum::Accepted;
|
||||
return response;
|
||||
};
|
||||
|
||||
DataTransfer data_transfer(functional_block_context, callback, ocpp::DEFAULT_WAIT_FOR_FUTURE_TIMEOUT);
|
||||
|
||||
DataTransferRequest request = create_example_request();
|
||||
ocpp::Call<DataTransferRequest> call(request);
|
||||
ocpp::EnhancedMessage<MessageType> enhanced_message;
|
||||
enhanced_message.messageType = MessageType::DataTransfer;
|
||||
enhanced_message.message = call;
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<DataTransferResponse>();
|
||||
EXPECT_EQ(response.status, DataTransferStatusEnum::Accepted);
|
||||
}));
|
||||
|
||||
data_transfer.handle_message(enhanced_message);
|
||||
}
|
||||
|
||||
TEST_F(DataTransferTest, DataTransferReq_Offline) {
|
||||
DataTransfer data_transfer(functional_block_context, std::nullopt, ocpp::DEFAULT_WAIT_FOR_FUTURE_TIMEOUT);
|
||||
|
||||
DataTransferRequest request = create_example_request();
|
||||
|
||||
ocpp::EnhancedMessage<MessageType> offline_message;
|
||||
offline_message.offline = true;
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_async(_, _))
|
||||
.WillOnce(Return(std::async(std::launch::deferred, [offline_message]() { return offline_message; })));
|
||||
|
||||
auto response = data_transfer.data_transfer_req(request);
|
||||
|
||||
EXPECT_FALSE(response.has_value());
|
||||
}
|
||||
|
||||
TEST_F(DataTransferTest, DataTransferReq_Timeout) {
|
||||
DataTransfer data_transfer(functional_block_context, std::nullopt, std::chrono::seconds(1));
|
||||
|
||||
DataTransferRequest request = create_example_request();
|
||||
|
||||
auto timeout_future = std::async(std::launch::async, []() -> ocpp::EnhancedMessage<MessageType> {
|
||||
std::this_thread::sleep_for(std::chrono::seconds(2));
|
||||
return {};
|
||||
});
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_async(_, _)).WillOnce(Return(std::move(timeout_future)));
|
||||
|
||||
auto response = data_transfer.data_transfer_req(request);
|
||||
|
||||
EXPECT_FALSE(response.has_value());
|
||||
}
|
||||
|
||||
TEST_F(DataTransferTest, DataTransferReq_Accepted) {
|
||||
DataTransfer data_transfer(functional_block_context, std::nullopt, ocpp::DEFAULT_WAIT_FOR_FUTURE_TIMEOUT);
|
||||
|
||||
DataTransferRequest request = create_example_request();
|
||||
|
||||
DataTransferResponse expected_response;
|
||||
expected_response.status = DataTransferStatusEnum::Accepted;
|
||||
|
||||
ocpp::CallResult<DataTransferResponse> call_result(expected_response, "uniqueId");
|
||||
|
||||
ocpp::EnhancedMessage<MessageType> enhanced_message;
|
||||
enhanced_message.messageType = MessageType::DataTransferResponse;
|
||||
enhanced_message.message = call_result;
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_async(_, _))
|
||||
.WillOnce(Return(std::async(std::launch::deferred, [enhanced_message]() { return enhanced_message; })));
|
||||
|
||||
auto response = data_transfer.data_transfer_req(request.vendorId, request.messageId, request.data);
|
||||
|
||||
ASSERT_TRUE(response.has_value());
|
||||
EXPECT_EQ(response->status, DataTransferStatusEnum::Accepted);
|
||||
}
|
||||
|
||||
TEST_F(DataTransferTest, DataTransferReq_EnumConversionException) {
|
||||
DataTransfer data_transfer(functional_block_context, std::nullopt, ocpp::DEFAULT_WAIT_FOR_FUTURE_TIMEOUT);
|
||||
|
||||
DataTransferRequest request = create_example_request();
|
||||
|
||||
ocpp::EnhancedMessage<MessageType> enhanced_message;
|
||||
enhanced_message.offline = false;
|
||||
enhanced_message.messageType = MessageType::DataTransferResponse;
|
||||
enhanced_message.uniqueId = "unique-id-123";
|
||||
enhanced_message.message =
|
||||
json::parse("[3, \"unique-id-123\", {\"status\": \"Wrong\"}]"); // will cause a throw of EnumConversionException
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_async(_, _))
|
||||
.WillOnce(Return(std::async(std::launch::deferred, [enhanced_message]() -> ocpp::EnhancedMessage<MessageType> {
|
||||
return enhanced_message;
|
||||
})));
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_error(_)).WillOnce([](const ocpp::CallError& call_error) {
|
||||
EXPECT_EQ(call_error.errorCode, "FormationViolation");
|
||||
});
|
||||
|
||||
auto result = data_transfer.data_transfer_req(request);
|
||||
|
||||
EXPECT_FALSE(result.has_value());
|
||||
}
|
||||
|
||||
TEST_F(DataTransferTest, DataTransferReq_JsonException) {
|
||||
DataTransfer data_transfer(functional_block_context, std::nullopt, ocpp::DEFAULT_WAIT_FOR_FUTURE_TIMEOUT);
|
||||
|
||||
DataTransferRequest request = create_example_request();
|
||||
|
||||
ocpp::EnhancedMessage<MessageType> enhanced_message;
|
||||
enhanced_message.offline = false;
|
||||
enhanced_message.messageType = MessageType::DataTransferResponse;
|
||||
enhanced_message.uniqueId = "unique-id-123";
|
||||
enhanced_message.message = "{NoValidJson"; // will cause a throw of json exception
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_async(_, _))
|
||||
.WillOnce(Return(std::async(std::launch::deferred, [enhanced_message]() -> ocpp::EnhancedMessage<MessageType> {
|
||||
return enhanced_message;
|
||||
})));
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_error(_)).WillOnce([](const ocpp::CallError& call_error) {
|
||||
EXPECT_EQ(call_error.errorCode, "FormationViolation");
|
||||
});
|
||||
|
||||
auto result = data_transfer.data_transfer_req(request);
|
||||
|
||||
EXPECT_FALSE(result.has_value());
|
||||
}
|
||||
@@ -0,0 +1,617 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include "component_state_manager_mock.hpp"
|
||||
#include "connectivity_manager_mock.hpp"
|
||||
#include "evse_security_mock.hpp"
|
||||
#include "mocks/database_handler_mock.hpp"
|
||||
#include <evse_manager_fake.hpp>
|
||||
#include <message_dispatcher_mock.hpp>
|
||||
|
||||
#include <device_model_test_helper.hpp>
|
||||
#include <ocpp/v2/functional_blocks/reservation.hpp>
|
||||
|
||||
#include <ocpp/v2/ctrlr_component_variables.hpp>
|
||||
#include <ocpp/v2/device_model.hpp>
|
||||
#include <ocpp/v2/device_model_storage_sqlite.hpp>
|
||||
#include <ocpp/v2/functional_blocks/functional_block_context.hpp>
|
||||
#include <ocpp/v2/init_device_model_db.hpp>
|
||||
|
||||
#include <ocpp/v2/messages/CancelReservation.hpp>
|
||||
#include <ocpp/v2/messages/ReservationStatusUpdate.hpp>
|
||||
#include <ocpp/v2/messages/ReserveNow.hpp>
|
||||
#include <ocpp/v2/messages/Reset.hpp>
|
||||
const static std::uint32_t NR_OF_EVSES = 2;
|
||||
|
||||
using namespace ocpp::v2;
|
||||
using ::testing::_;
|
||||
using ::testing::Invoke;
|
||||
using ::testing::MockFunction;
|
||||
using ::testing::Return;
|
||||
|
||||
class ReservationTest : public ::testing::Test {
|
||||
public:
|
||||
protected: // Functions
|
||||
ReservationTest() :
|
||||
database_connection(std::make_unique<everest::db::sqlite::Connection>(DEVICE_MODEL_DB_IN_MEMORY_PATH)),
|
||||
ocpp_version(ocpp::OcppProtocolVersion::v201) {
|
||||
database_connection->open_connection();
|
||||
this->device_model = create_device_model();
|
||||
this->functional_block_context = std::make_unique<FunctionalBlockContext>(
|
||||
this->mock_dispatcher, *this->device_model, this->connectivity_manager, this->evse_manager,
|
||||
this->database_handler, this->evse_security, this->component_state_manager, this->ocpp_version);
|
||||
this->reservation = std::make_unique<Reservation>(
|
||||
*functional_block_context, reserve_now_callback_mock.AsStdFunction(),
|
||||
cancel_reservation_callback_mock.AsStdFunction(), is_reservation_for_token_callback_mock.AsStdFunction());
|
||||
default_test_token.idToken = "SOME_TOKEN";
|
||||
default_test_token.type = IdTokenEnumStringType::ISO14443;
|
||||
}
|
||||
|
||||
///
|
||||
/// \brief Create the database for the device model and apply migrations.
|
||||
/// \param path Database path.
|
||||
///
|
||||
void create_device_model_db(const std::string& path) {
|
||||
InitDeviceModelDb db(path, MIGRATION_FILES_PATH);
|
||||
const auto component_configs = get_all_component_configs(CONFIG_PATH);
|
||||
db.initialize_database(component_configs, true);
|
||||
}
|
||||
|
||||
///
|
||||
/// \brief Create device model.
|
||||
/// \param is_reservation_available Value of ReservationCtrlr variable 'Available' in the device model.
|
||||
/// \param is_reservation_enabled Value of ReservationCtrlr variable 'enabled' in the device model.
|
||||
/// \param non_evse_specific_enabled Enable/disable non evse specific reservations in the device model.
|
||||
/// \return The created device model.
|
||||
///
|
||||
std::unique_ptr<DeviceModel> create_device_model(const bool is_reservation_available = true,
|
||||
const bool is_reservation_enabled = true,
|
||||
const bool non_evse_specific_enabled = true) {
|
||||
create_device_model_db(DEVICE_MODEL_DB_IN_MEMORY_PATH);
|
||||
auto device_model_storage = std::make_unique<DeviceModelStorageSqlite>(DEVICE_MODEL_DB_IN_MEMORY_PATH);
|
||||
auto dm = std::make_unique<DeviceModel>(std::move(device_model_storage));
|
||||
// Defaults
|
||||
set_reservation_available(dm.get(), is_reservation_available);
|
||||
set_reservation_enabled(dm.get(), is_reservation_enabled);
|
||||
set_non_evse_specific(dm.get(), non_evse_specific_enabled);
|
||||
|
||||
// Check values
|
||||
const bool reservation_available_in_device_model =
|
||||
dm->get_optional_value<bool>(ControllerComponentVariables::ReservationCtrlrAvailable).value_or(false);
|
||||
EXPECT_EQ(reservation_available_in_device_model, is_reservation_available);
|
||||
|
||||
const bool reservation_enabled_in_device_model =
|
||||
dm->get_optional_value<bool>(ControllerComponentVariables::ReservationCtrlrEnabled).value_or(false);
|
||||
EXPECT_EQ(reservation_enabled_in_device_model, is_reservation_enabled);
|
||||
|
||||
const bool non_evse_specific_enabled_device_model =
|
||||
dm->get_optional_value<bool>(ControllerComponentVariables::ReservationCtrlrNonEvseSpecific).value_or(false);
|
||||
EXPECT_EQ(non_evse_specific_enabled_device_model, non_evse_specific_enabled);
|
||||
|
||||
return dm;
|
||||
}
|
||||
|
||||
///
|
||||
/// \brief Set value of ReservationCtrlr variable 'Enabled' in the device model.
|
||||
/// \param device_model The device model to set the value in.
|
||||
/// \param enabled True to set to enabled.
|
||||
///
|
||||
void set_reservation_enabled(DeviceModel* device_model, const bool enabled) {
|
||||
const auto& reservation_enabled = ControllerComponentVariables::ReservationCtrlrEnabled;
|
||||
EXPECT_EQ(device_model->set_value(reservation_enabled.component, reservation_enabled.variable.value(),
|
||||
AttributeEnum::Actual, enabled ? "true" : "false", "default", true),
|
||||
SetVariableStatusEnum::Accepted);
|
||||
}
|
||||
|
||||
///
|
||||
/// \brief Set value of ReservationCtrlr variable 'Available' in the device model.
|
||||
/// \param device_model The device model to set the value in.
|
||||
/// \param available True to set to available.
|
||||
///
|
||||
void set_reservation_available(DeviceModel* device_model, const bool available) {
|
||||
const auto& reservation_available = ControllerComponentVariables::ReservationCtrlrAvailable;
|
||||
EXPECT_EQ(device_model->set_value(reservation_available.component, reservation_available.variable.value(),
|
||||
AttributeEnum::Actual, (available ? "true" : "false"), "default", true),
|
||||
SetVariableStatusEnum::Accepted);
|
||||
}
|
||||
|
||||
///
|
||||
/// \brief Enable or disable non evse specific reservations in the device model.
|
||||
/// \param device_model The device model to set the value in.
|
||||
/// \param non_evse_specific_enabled True to enable non evse specific reservations.
|
||||
///
|
||||
void set_non_evse_specific(DeviceModel* device_model, const bool non_evse_specific_enabled) {
|
||||
const auto& non_evse_specific = ControllerComponentVariables::ReservationCtrlrNonEvseSpecific;
|
||||
EXPECT_EQ(device_model->set_value(non_evse_specific.component, non_evse_specific.variable.value(),
|
||||
AttributeEnum::Actual, (non_evse_specific_enabled ? "true" : "false"),
|
||||
"default", true),
|
||||
SetVariableStatusEnum::Accepted);
|
||||
}
|
||||
|
||||
///
|
||||
/// \brief Create example ReserveNow request to use in tests.
|
||||
/// \param evse_id Optional evse id.
|
||||
/// \param connector_type Optional connector type.
|
||||
/// \return The request message.
|
||||
///
|
||||
ocpp::EnhancedMessage<MessageType>
|
||||
create_example_reserve_now_request(const std::optional<std::int32_t> evse_id = std::nullopt,
|
||||
const std::optional<ocpp::CiString<20>> connector_type = std::nullopt) {
|
||||
ReserveNowRequest request;
|
||||
request.connectorType = connector_type;
|
||||
request.evseId = evse_id;
|
||||
request.id = 1;
|
||||
request.idToken = default_test_token;
|
||||
ocpp::Call<ReserveNowRequest> call(request);
|
||||
ocpp::EnhancedMessage<MessageType> enhanced_message;
|
||||
enhanced_message.messageType = MessageType::ReserveNow;
|
||||
enhanced_message.message = call;
|
||||
return enhanced_message;
|
||||
}
|
||||
|
||||
///
|
||||
/// \brief Create example CancelReservation request to use in tests.
|
||||
/// \param reservation_id The reservation id.
|
||||
/// \return The request message.
|
||||
///
|
||||
ocpp::EnhancedMessage<MessageType> create_example_cancel_reservation_request(const std::int32_t reservation_id) {
|
||||
CancelReservationRequest request;
|
||||
request.reservationId = reservation_id;
|
||||
ocpp::Call<CancelReservationRequest> call(request);
|
||||
ocpp::EnhancedMessage<MessageType> enhanced_message;
|
||||
enhanced_message.messageType = MessageType::CancelReservation;
|
||||
enhanced_message.message = call;
|
||||
return enhanced_message;
|
||||
}
|
||||
|
||||
protected: // Members
|
||||
ConnectivityManagerMock connectivity_manager;
|
||||
ocpp::v2::DatabaseHandlerMock database_handler;
|
||||
ocpp::EvseSecurityMock evse_security;
|
||||
ComponentStateManagerMock component_state_manager;
|
||||
// Connection as member so the database keeps open and is not destroyed (because this is an in memory
|
||||
// database).
|
||||
std::unique_ptr<everest::db::sqlite::Connection> database_connection;
|
||||
MockMessageDispatcher mock_dispatcher;
|
||||
EvseManagerFake evse_manager{NR_OF_EVSES};
|
||||
// Device model is a unique ptr here because of the database: it is stored in memory so as soon as the handle to
|
||||
// the database closes, the database is removed. So the handle should be opened before creating the devide model.
|
||||
// So the device model is initialized on nullptr, then the handle is opened, the devide model is created and the
|
||||
// handle stays open until the whole test is destructed.
|
||||
std::unique_ptr<DeviceModel> device_model;
|
||||
MockFunction<ReserveNowStatusEnum(const ReserveNowRequest& request)> reserve_now_callback_mock;
|
||||
MockFunction<bool(const std::int32_t reservationId)> cancel_reservation_callback_mock;
|
||||
MockFunction<ocpp::ReservationCheckStatus(const std::int32_t evse_id, const ocpp::CiString<255> idToken,
|
||||
const std::optional<ocpp::CiString<255>> groupIdToken)>
|
||||
is_reservation_for_token_callback_mock;
|
||||
std::atomic<ocpp::OcppProtocolVersion> ocpp_version;
|
||||
std::unique_ptr<FunctionalBlockContext> functional_block_context;
|
||||
// Make reservation a unique ptr so we can create it after creating the device model.
|
||||
std::unique_ptr<Reservation> reservation;
|
||||
|
||||
IdToken default_test_token;
|
||||
};
|
||||
|
||||
TEST_F(ReservationTest, handle_reserve_now_reservation_not_available) {
|
||||
// In the device model, reservation is set to not available. This should reject the request.
|
||||
set_reservation_available(this->device_model.get(), false);
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<ReserveNowResponse>();
|
||||
EXPECT_EQ(response.status, ReserveNowStatusEnum::Rejected);
|
||||
ASSERT_TRUE(response.statusInfo.has_value());
|
||||
ASSERT_TRUE(response.statusInfo.value().additionalInfo.has_value());
|
||||
EXPECT_EQ(response.statusInfo.value().additionalInfo.value(), "Reservation is not available");
|
||||
}));
|
||||
|
||||
EvseMock& m1 = evse_manager.get_mock(1);
|
||||
EvseMock& m2 = evse_manager.get_mock(2);
|
||||
|
||||
EXPECT_CALL(m1, get_connector_status(_)).Times(0);
|
||||
EXPECT_CALL(m2, get_connector_status(_)).Times(0);
|
||||
|
||||
const ocpp::EnhancedMessage<MessageType> request = create_example_reserve_now_request();
|
||||
this->reservation->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, handle_reserve_now_callback_nullptr) {
|
||||
// The callback to make the reservation is a nullptr. This should reject the request.
|
||||
Reservation r{*this->functional_block_context, nullptr, cancel_reservation_callback_mock.AsStdFunction(),
|
||||
is_reservation_for_token_callback_mock.AsStdFunction()};
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<ReserveNowResponse>();
|
||||
EXPECT_EQ(response.status, ReserveNowStatusEnum::Rejected);
|
||||
ASSERT_TRUE(response.statusInfo.has_value());
|
||||
ASSERT_TRUE(response.statusInfo.value().additionalInfo.has_value());
|
||||
EXPECT_EQ(response.statusInfo.value().additionalInfo.value(), "Reservation is not implemented");
|
||||
}));
|
||||
|
||||
EvseMock& m1 = evse_manager.get_mock(1);
|
||||
EvseMock& m2 = evse_manager.get_mock(2);
|
||||
|
||||
EXPECT_CALL(m1, get_connector_status(_)).Times(0);
|
||||
EXPECT_CALL(m2, get_connector_status(_)).Times(0);
|
||||
|
||||
const ocpp::EnhancedMessage<MessageType> request = create_example_reserve_now_request();
|
||||
r.handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, handle_reserve_now_reservation_disabled) {
|
||||
// In the device model, reservation is set to not enabled. This should reject the request.
|
||||
set_reservation_enabled(this->device_model.get(), false);
|
||||
|
||||
const bool reservation_enabled_in_device_model =
|
||||
this->device_model->get_optional_value<bool>(ControllerComponentVariables::ReservationCtrlrEnabled)
|
||||
.value_or(false);
|
||||
EXPECT_EQ(reservation_enabled_in_device_model, false);
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<ReserveNowResponse>();
|
||||
EXPECT_EQ(response.status, ReserveNowStatusEnum::Rejected);
|
||||
ASSERT_TRUE(response.statusInfo.has_value());
|
||||
ASSERT_TRUE(response.statusInfo.value().additionalInfo.has_value());
|
||||
EXPECT_EQ(response.statusInfo.value().additionalInfo.value(), "Reservation is not enabled");
|
||||
}));
|
||||
|
||||
EvseMock& m1 = evse_manager.get_mock(1);
|
||||
EvseMock& m2 = evse_manager.get_mock(2);
|
||||
|
||||
EXPECT_CALL(m1, get_connector_status(_)).Times(0);
|
||||
EXPECT_CALL(m2, get_connector_status(_)).Times(0);
|
||||
|
||||
const ocpp::EnhancedMessage<MessageType> request = create_example_reserve_now_request();
|
||||
this->reservation->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, handle_reserve_now_non_evse_specific_disabled) {
|
||||
// In the device model, non evse specific reservations are disabled. So when we try to make a reservation for
|
||||
// a non specific evse (no evse id given), the request should be rejected.
|
||||
set_non_evse_specific(this->device_model.get(), false);
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<ReserveNowResponse>();
|
||||
EXPECT_EQ(response.status, ReserveNowStatusEnum::Rejected);
|
||||
ASSERT_TRUE(response.statusInfo.has_value());
|
||||
ASSERT_TRUE(response.statusInfo.value().additionalInfo.has_value());
|
||||
EXPECT_EQ(response.statusInfo.value().additionalInfo.value(),
|
||||
"No evse id was given while it should be sent in the request when NonEvseSpecific is disabled");
|
||||
}));
|
||||
|
||||
EvseMock& m1 = evse_manager.get_mock(1);
|
||||
EvseMock& m2 = evse_manager.get_mock(2);
|
||||
|
||||
EXPECT_CALL(m1, get_connector_status(_)).Times(0);
|
||||
EXPECT_CALL(m2, get_connector_status(_)).Times(0);
|
||||
|
||||
const ocpp::EnhancedMessage<MessageType> request = create_example_reserve_now_request();
|
||||
this->reservation->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, handle_reserve_now_evse_not_existing) {
|
||||
// Try to make a reservation with a not existing evse id. This should reject the request.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<ReserveNowResponse>();
|
||||
EXPECT_EQ(response.status, ReserveNowStatusEnum::Rejected);
|
||||
ASSERT_TRUE(response.statusInfo.has_value());
|
||||
ASSERT_TRUE(response.statusInfo.value().additionalInfo.has_value());
|
||||
EXPECT_EQ(response.statusInfo.value().additionalInfo.value(), "Evse id does not exist");
|
||||
}));
|
||||
|
||||
const ocpp::EnhancedMessage<MessageType> request = create_example_reserve_now_request(5);
|
||||
this->reservation->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, handle_reserve_now_connector_not_existing) {
|
||||
// Try to make a reservation for a connector type that does not exist. This should reject the request.
|
||||
EvseMock& m1 = evse_manager.get_mock(1);
|
||||
EXPECT_CALL(m1, does_connector_exist(ConnectorEnumStringType::Pan)).WillOnce(Return(false));
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<ReserveNowResponse>();
|
||||
EXPECT_EQ(response.status, ReserveNowStatusEnum::Rejected);
|
||||
ASSERT_TRUE(response.statusInfo.has_value());
|
||||
ASSERT_TRUE(response.statusInfo.value().additionalInfo.has_value());
|
||||
EXPECT_EQ(response.statusInfo.value().additionalInfo.value(), "Connector type does not exist");
|
||||
}));
|
||||
|
||||
const ocpp::EnhancedMessage<MessageType> request =
|
||||
create_example_reserve_now_request(1, ConnectorEnumStringType::Pan);
|
||||
this->reservation->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, handle_reserve_now_connectors_not_existing) {
|
||||
// Try to make a non evse specific reservation for a connector type that does not exist. This should reject the
|
||||
// request.
|
||||
EvseMock& m1 = evse_manager.get_mock(1);
|
||||
ON_CALL(m1, does_connector_exist(ConnectorEnumStringType::cG105)).WillByDefault(Return(false));
|
||||
|
||||
EvseMock& m2 = evse_manager.get_mock(2);
|
||||
ON_CALL(m2, does_connector_exist(ConnectorEnumStringType::cG105)).WillByDefault(Return(false));
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<ReserveNowResponse>();
|
||||
EXPECT_EQ(response.status, ReserveNowStatusEnum::Rejected);
|
||||
ASSERT_TRUE(response.statusInfo.has_value());
|
||||
ASSERT_TRUE(response.statusInfo.value().additionalInfo.has_value());
|
||||
EXPECT_EQ(response.statusInfo.value().additionalInfo.value(), "Could not get status info from connector");
|
||||
}));
|
||||
|
||||
const ocpp::EnhancedMessage<MessageType> request =
|
||||
create_example_reserve_now_request(std::nullopt, ConnectorEnumStringType::cG105);
|
||||
this->reservation->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, handle_reserve_now_one_connector_not_existing) {
|
||||
// Try to make a non evse specific reservation. One connector does not have the given connector, but the other does,
|
||||
// so the reservation request should be accepted.
|
||||
const ocpp::EnhancedMessage<MessageType> request =
|
||||
create_example_reserve_now_request(std::nullopt, ConnectorEnumStringType::cTesla);
|
||||
|
||||
EvseMock& m1 = evse_manager.get_mock(1);
|
||||
EXPECT_CALL(m1, does_connector_exist(ConnectorEnumStringType::cTesla)).WillOnce(Return(false));
|
||||
|
||||
EvseMock& m2 = evse_manager.get_mock(2);
|
||||
EXPECT_CALL(m2, does_connector_exist(ConnectorEnumStringType::cTesla)).WillOnce(Return(true));
|
||||
|
||||
EXPECT_CALL(reserve_now_callback_mock, Call(_)).WillOnce(Return(ReserveNowStatusEnum::Accepted));
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<ReserveNowResponse>();
|
||||
EXPECT_EQ(response.status, ReserveNowStatusEnum::Accepted);
|
||||
}));
|
||||
|
||||
this->reservation->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, handle_reserve_now_all_connectors_not_available) {
|
||||
// Try to make a reservation with all connectors unavailable. Since the evse manager has the last word in this,
|
||||
// we try to do the request and it can be accepted anyway (or at least the correct reason for not accepting the
|
||||
// reservation can be returned, if this is a real scenario).
|
||||
const ocpp::EnhancedMessage<MessageType> request =
|
||||
create_example_reserve_now_request(std::nullopt, ConnectorEnumStringType::cTesla);
|
||||
|
||||
EvseMock& m1 = evse_manager.get_mock(1);
|
||||
EXPECT_CALL(m1, does_connector_exist(ConnectorEnumStringType::cTesla)).WillOnce(Return(true));
|
||||
|
||||
EvseMock& m2 = evse_manager.get_mock(2);
|
||||
EXPECT_CALL(m2, does_connector_exist(ConnectorEnumStringType::cTesla)).Times(0);
|
||||
|
||||
EXPECT_CALL(reserve_now_callback_mock, Call(_)).WillOnce(Return(ReserveNowStatusEnum::Accepted));
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<ReserveNowResponse>();
|
||||
EXPECT_EQ(response.status, ReserveNowStatusEnum::Accepted);
|
||||
}));
|
||||
|
||||
this->reservation->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, handle_reserve_now_non_specific_evse_successful) {
|
||||
// Try to make a non evse specific reservation which is accepted.
|
||||
const ocpp::EnhancedMessage<MessageType> request =
|
||||
create_example_reserve_now_request(std::nullopt, ConnectorEnumStringType::cTesla);
|
||||
|
||||
EvseMock& m1 = evse_manager.get_mock(1);
|
||||
EXPECT_CALL(m1, does_connector_exist(ConnectorEnumStringType::cTesla)).WillOnce(Return(true));
|
||||
|
||||
ON_CALL(reserve_now_callback_mock, Call(_)).WillByDefault(Invoke([](const ReserveNowRequest reserve_now_request) {
|
||||
EXPECT_FALSE(reserve_now_request.evseId.has_value());
|
||||
return ReserveNowStatusEnum::Accepted;
|
||||
}));
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<ReserveNowResponse>();
|
||||
EXPECT_EQ(response.status, ReserveNowStatusEnum::Accepted);
|
||||
}));
|
||||
|
||||
this->reservation->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, handle_reserve_now_specific_evse_successful) {
|
||||
// Try to make a reservation for an existing evse, which is accepted.
|
||||
std::optional<ocpp::CiString<20>> tesla_connector_type = ConnectorEnumStringType::cTesla;
|
||||
|
||||
const ocpp::EnhancedMessage<MessageType> request = create_example_reserve_now_request(2, tesla_connector_type);
|
||||
|
||||
EvseMock& m2 = evse_manager.get_mock(2);
|
||||
ON_CALL(m2, does_connector_exist(ConnectorEnumStringType::cTesla)).WillByDefault(Return(true));
|
||||
|
||||
ON_CALL(reserve_now_callback_mock, Call(_)).WillByDefault(Invoke([](const ReserveNowRequest reserve_now_request) {
|
||||
EXPECT_TRUE(reserve_now_request.evseId.has_value());
|
||||
EXPECT_EQ(reserve_now_request.evseId.value(), 2);
|
||||
return ReserveNowStatusEnum::Accepted;
|
||||
}));
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<ReserveNowResponse>();
|
||||
EXPECT_EQ(response.status, ReserveNowStatusEnum::Accepted);
|
||||
}));
|
||||
|
||||
this->reservation->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, handle_reserve_now_specific_evse_occupied) {
|
||||
// Try to make a reservation for a non specific evse, but all evse's are occupied.
|
||||
std::optional<ocpp::CiString<20>> tesla_connector_type = ConnectorEnumStringType::cTesla;
|
||||
|
||||
const ocpp::EnhancedMessage<MessageType> request = create_example_reserve_now_request(2, tesla_connector_type);
|
||||
|
||||
EvseMock& m2 = evse_manager.get_mock(2);
|
||||
ON_CALL(m2, does_connector_exist(ConnectorEnumStringType::cTesla)).WillByDefault(Return(true));
|
||||
|
||||
EXPECT_CALL(reserve_now_callback_mock, Call(_)).WillOnce(Return(ReserveNowStatusEnum::Occupied));
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<ReserveNowResponse>();
|
||||
EXPECT_EQ(response.status, ReserveNowStatusEnum::Occupied);
|
||||
}));
|
||||
|
||||
this->reservation->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, handle_cancel_reservation_reservation_not_available) {
|
||||
// Try to cancel a reservation, while Reservations is not available in the device model. This will reject the
|
||||
// request.
|
||||
set_reservation_available(this->device_model.get(), false);
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<CancelReservationResponse>();
|
||||
EXPECT_EQ(response.status, CancelReservationStatusEnum::Rejected);
|
||||
}));
|
||||
|
||||
const ocpp::EnhancedMessage<MessageType> request = create_example_cancel_reservation_request(2);
|
||||
|
||||
this->reservation->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, handle_cancel_reservation_reservation_not_enabled) {
|
||||
// Try to cancel a reservation, while Reservations is not enabled in the device model. This will reject the request.
|
||||
set_reservation_enabled(this->device_model.get(), false);
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<CancelReservationResponse>();
|
||||
EXPECT_EQ(response.status, CancelReservationStatusEnum::Rejected);
|
||||
}));
|
||||
|
||||
const ocpp::EnhancedMessage<MessageType> request = create_example_cancel_reservation_request(2);
|
||||
|
||||
this->reservation->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, handle_cancel_reservation_callback_nullptr) {
|
||||
// Try to cancel a reservation, while the cancel reservation callback is a nullptr. This will reject the request.
|
||||
Reservation r{*this->functional_block_context, reserve_now_callback_mock.AsStdFunction(), nullptr,
|
||||
is_reservation_for_token_callback_mock.AsStdFunction()};
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<CancelReservationResponse>();
|
||||
EXPECT_EQ(response.status, CancelReservationStatusEnum::Rejected);
|
||||
}));
|
||||
|
||||
const ocpp::EnhancedMessage<MessageType> request = create_example_cancel_reservation_request(2);
|
||||
|
||||
r.handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, handle_cancel_reservation_accepted) {
|
||||
// Try to cancel a reservation, which is accepted.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<CancelReservationResponse>();
|
||||
EXPECT_EQ(response.status, CancelReservationStatusEnum::Accepted);
|
||||
}));
|
||||
|
||||
const ocpp::EnhancedMessage<MessageType> request = create_example_cancel_reservation_request(2);
|
||||
|
||||
EXPECT_CALL(cancel_reservation_callback_mock, Call(_)).WillOnce(Return(true));
|
||||
|
||||
this->reservation->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, handle_cancel_reservation_rejected) {
|
||||
// Try to cancel a reservation, which is rejected.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<CancelReservationResponse>();
|
||||
EXPECT_EQ(response.status, CancelReservationStatusEnum::Rejected);
|
||||
}));
|
||||
|
||||
const ocpp::EnhancedMessage<MessageType> request = create_example_cancel_reservation_request(2);
|
||||
|
||||
EXPECT_CALL(cancel_reservation_callback_mock, Call(_)).WillOnce(Return(false));
|
||||
|
||||
this->reservation->handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, handle_message_wrong_type) {
|
||||
// Try to handle a message with the wrong type, should throw an exception.
|
||||
ResetRequest request;
|
||||
request.type = ResetEnum::Immediate;
|
||||
ocpp::Call<ResetRequest> call(request);
|
||||
ocpp::EnhancedMessage<MessageType> enhanced_message;
|
||||
enhanced_message.messageType = MessageType::Reset;
|
||||
enhanced_message.message = call;
|
||||
|
||||
EXPECT_THROW(reservation->handle_message(enhanced_message), MessageTypeNotImplementedException);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, handle_reserve_now_no_evses) {
|
||||
// Try to make a 'global' reservation, but there are no evse's in the evse manager.
|
||||
EvseManagerFake evse_manager_no_evses{0};
|
||||
|
||||
const FunctionalBlockContext b{this->mock_dispatcher, *this->device_model, this->connectivity_manager,
|
||||
evse_manager_no_evses, this->database_handler, this->evse_security,
|
||||
this->component_state_manager, this->ocpp_version};
|
||||
this->functional_block_context = std::make_unique<FunctionalBlockContext>(b);
|
||||
|
||||
Reservation r{*this->functional_block_context, reserve_now_callback_mock.AsStdFunction(),
|
||||
cancel_reservation_callback_mock.AsStdFunction(),
|
||||
is_reservation_for_token_callback_mock.AsStdFunction()};
|
||||
|
||||
const ocpp::EnhancedMessage<MessageType> request =
|
||||
create_example_reserve_now_request(std::nullopt, ConnectorEnumStringType::cTesla);
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<ReserveNowResponse>();
|
||||
EXPECT_EQ(response.status, ReserveNowStatusEnum::Rejected);
|
||||
ASSERT_TRUE(response.statusInfo.has_value());
|
||||
ASSERT_TRUE(response.statusInfo.value().additionalInfo.has_value());
|
||||
EXPECT_EQ(response.statusInfo.value().additionalInfo.value(), "No evse's found in charging station");
|
||||
}));
|
||||
|
||||
r.handle_message(request);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, on_reservation_status) {
|
||||
// Call 'on_reservation_status' and check if the request is sent (to the dispatcher)
|
||||
ReservationStatusUpdateRequest request;
|
||||
request.reservationId = 3;
|
||||
request.reservationUpdateStatus = ReservationUpdateStatusEnum::Removed;
|
||||
ocpp::Call<ReservationStatusUpdateRequest> call(request);
|
||||
ocpp::EnhancedMessage<MessageType> enhanced_message;
|
||||
enhanced_message.messageType = MessageType::CancelReservation;
|
||||
enhanced_message.message = call;
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).WillOnce(Invoke([](const json& call, bool triggered) {
|
||||
auto response = call[ocpp::CALL_PAYLOAD].get<ReservationStatusUpdateRequest>();
|
||||
EXPECT_EQ(response.reservationUpdateStatus, ReservationUpdateStatusEnum::Removed);
|
||||
EXPECT_EQ(response.reservationId, 3);
|
||||
EXPECT_FALSE(triggered);
|
||||
}));
|
||||
|
||||
reservation->on_reservation_status(3, ReservationUpdateStatusEnum::Removed);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, is_evse_reserved_for_other) {
|
||||
// Call 'is_evse_reserved_for_other' and check if callback is called and the correct value is returned. In this
|
||||
// case: NotReserved.
|
||||
EXPECT_CALL(is_reservation_for_token_callback_mock, Call(42, _, _))
|
||||
.WillOnce(Return(ocpp::ReservationCheckStatus::NotReserved));
|
||||
|
||||
EvseMock& m1 = evse_manager.get_mock(1);
|
||||
IdToken id_token;
|
||||
id_token.idToken = "ID_TOKEN_THINGIE";
|
||||
|
||||
EXPECT_CALL(m1, get_id).WillOnce(Return(42));
|
||||
|
||||
EXPECT_EQ(reservation->is_evse_reserved_for_other(m1, id_token, std::nullopt),
|
||||
ocpp::ReservationCheckStatus::NotReserved);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, on_reserved) {
|
||||
// Call 'on_reserved' and check if the event is submitted to the evse.
|
||||
EvseMock& m1 = evse_manager.get_mock(2);
|
||||
EXPECT_CALL(m1, submit_event(1, ConnectorEvent::Reserve)).Times(1);
|
||||
|
||||
reservation->on_reserved(2, 1);
|
||||
}
|
||||
|
||||
TEST_F(ReservationTest, on_reservation_cleared) {
|
||||
// Cann 'on_reservation_cleared' and check if the event is submitted to the evse.
|
||||
EvseMock& m1 = evse_manager.get_mock(1);
|
||||
EXPECT_CALL(m1, submit_event(1, ConnectorEvent::ReservationCleared)).Times(1);
|
||||
|
||||
reservation->on_reservation_cleared(1, 1);
|
||||
}
|
||||
@@ -0,0 +1,986 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <ocpp/v2/ctrlr_component_variables.hpp>
|
||||
#include <ocpp/v2/device_model.hpp>
|
||||
#include <ocpp/v2/functional_blocks/functional_block_context.hpp>
|
||||
|
||||
#define private public // Make everything in security.hpp public so we can trigger the timer.
|
||||
#include <ocpp/v2/functional_blocks/security.hpp>
|
||||
#undef private
|
||||
#include <ocpp/v2/messages/CertificateSigned.hpp>
|
||||
#include <ocpp/v2/messages/Reset.hpp>
|
||||
#include <ocpp/v2/messages/SecurityEventNotification.hpp>
|
||||
#include <ocpp/v2/messages/SignCertificate.hpp>
|
||||
|
||||
#include "component_state_manager_mock.hpp"
|
||||
#include "connectivity_manager_mock.hpp"
|
||||
#include "device_model_test_helper.hpp"
|
||||
#include "evse_manager_fake.hpp"
|
||||
#include "evse_security_mock.hpp"
|
||||
#include "message_dispatcher_mock.hpp"
|
||||
#include "mocks/database_handler_mock.hpp"
|
||||
#include "ocsp_updater_mock.hpp"
|
||||
#include "timer_stub.hpp"
|
||||
|
||||
using namespace ocpp;
|
||||
using namespace ocpp::v2;
|
||||
using ::testing::_;
|
||||
using ::testing::Invoke;
|
||||
using ::testing::MockFunction;
|
||||
using ::testing::Return;
|
||||
|
||||
class SecurityTest : public ::testing::Test {
|
||||
public:
|
||||
protected: // Members
|
||||
DeviceModelTestHelper device_model_test_helper;
|
||||
DeviceModel* device_model;
|
||||
MockMessageDispatcher mock_dispatcher;
|
||||
ocpp::MessageLogging logging;
|
||||
ocpp::EvseSecurityMock evse_security;
|
||||
ConnectivityManagerMock connectivity_manager;
|
||||
EvseManagerFake evse_manager;
|
||||
ComponentStateManagerMock component_state_manager;
|
||||
::testing::NiceMock<ocpp::v2::DatabaseHandlerMock> database_handler_mock;
|
||||
OcspUpdaterMock ocsp_updater;
|
||||
MockFunction<void(const ocpp::CiString<50>& event_type, const std::optional<ocpp::CiString<255>>& tech_info)>
|
||||
security_event_callback_mock;
|
||||
std::atomic<ocpp::OcppProtocolVersion> ocpp_version;
|
||||
FunctionalBlockContext functional_block_context;
|
||||
Security security;
|
||||
|
||||
protected: // Functions
|
||||
SecurityTest() :
|
||||
device_model_test_helper(),
|
||||
device_model(device_model_test_helper.get_device_model()),
|
||||
logging(false, "", "", false, false, false, false, false, false, false, nullptr),
|
||||
evse_security(),
|
||||
connectivity_manager(),
|
||||
evse_manager(2),
|
||||
component_state_manager(),
|
||||
ocpp_version(ocpp::OcppProtocolVersion::v201),
|
||||
functional_block_context{
|
||||
this->mock_dispatcher, *this->device_model, this->connectivity_manager, this->evse_manager,
|
||||
this->database_handler_mock, this->evse_security, this->component_state_manager, this->ocpp_version},
|
||||
security(functional_block_context, logging, ocsp_updater, security_event_callback_mock.AsStdFunction()) {
|
||||
}
|
||||
|
||||
ocpp::EnhancedMessage<MessageType> create_example_certificate_signed_request(
|
||||
const std::string& certificate_chain = "",
|
||||
const std::optional<ocpp::v2::CertificateSigningUseEnum> certificate_type = std::nullopt) {
|
||||
CertificateSignedRequest request;
|
||||
request.certificateChain = certificate_chain;
|
||||
request.certificateType = certificate_type;
|
||||
ocpp::Call<CertificateSignedRequest> call(request);
|
||||
ocpp::EnhancedMessage<MessageType> enhanced_message;
|
||||
enhanced_message.messageType = MessageType::CertificateSigned;
|
||||
enhanced_message.message = call;
|
||||
return enhanced_message;
|
||||
}
|
||||
|
||||
ocpp::EnhancedMessage<MessageType>
|
||||
create_example_sign_certificate_response(const GenericStatusEnum status = GenericStatusEnum::Accepted) {
|
||||
SignCertificateResponse response;
|
||||
response.status = status;
|
||||
ocpp::CallResult<SignCertificateResponse> call_result;
|
||||
call_result.msg = response;
|
||||
ocpp::EnhancedMessage<MessageType> enhanced_message;
|
||||
enhanced_message.messageType = MessageType::SignCertificateResponse;
|
||||
enhanced_message.message = call_result;
|
||||
return enhanced_message;
|
||||
}
|
||||
|
||||
void set_update_certificate_symlinks_enabled(DeviceModel* device_model, const bool enabled) {
|
||||
const auto& update_certificate_symlinks = ControllerComponentVariables::UpdateCertificateSymlinks;
|
||||
EXPECT_EQ(device_model->set_value(update_certificate_symlinks.component,
|
||||
update_certificate_symlinks.variable.value(), AttributeEnum::Actual,
|
||||
enabled ? "true" : "false", "default", true),
|
||||
SetVariableStatusEnum::Accepted);
|
||||
}
|
||||
|
||||
void set_security_profile(DeviceModel* device_model, const int profile) {
|
||||
const auto& security_profile = ControllerComponentVariables::SecurityProfile;
|
||||
EXPECT_EQ(device_model->set_value(security_profile.component, security_profile.variable.value(),
|
||||
AttributeEnum::Actual, std::to_string(profile), "default", true),
|
||||
SetVariableStatusEnum::Accepted);
|
||||
}
|
||||
};
|
||||
|
||||
TEST_F(SecurityTest, handle_message_not_implemented) {
|
||||
// Try to handle a message with the wrong type, should throw an exception.
|
||||
ResetRequest request;
|
||||
request.type = ResetEnum::Immediate;
|
||||
ocpp::Call<ResetRequest> call(request);
|
||||
ocpp::EnhancedMessage<MessageType> enhanced_message;
|
||||
enhanced_message.messageType = MessageType::Reset;
|
||||
enhanced_message.message = call;
|
||||
|
||||
EXPECT_THROW(security.handle_message(enhanced_message), MessageTypeNotImplementedException);
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, handle_message_certificate_signed_v2gcertificate) {
|
||||
set_update_certificate_symlinks_enabled(this->device_model, true);
|
||||
|
||||
// Leaf certificate should be updated.
|
||||
EXPECT_CALL(evse_security, update_leaf_certificate("", ocpp::CertificateSigningUseEnum::V2GCertificate))
|
||||
.WillOnce(Return(ocpp::InstallCertificateResult::Accepted));
|
||||
// For V2G certificates, OCSP cache update should be triggered.
|
||||
EXPECT_CALL(ocsp_updater, trigger_ocsp_cache_update()).Times(1);
|
||||
// For V2G certificates, a symlink update should be triggered when that is set in the device model
|
||||
EXPECT_CALL(evse_security, update_certificate_links(ocpp::CertificateSigningUseEnum::V2GCertificate)).Times(1);
|
||||
// As updating the leaf certificate is accepted, the call result will be 'Accepted'.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<CertificateSignedResponse>();
|
||||
EXPECT_EQ(response.status, CertificateSignedStatusEnum::Accepted);
|
||||
}));
|
||||
|
||||
security.handle_message(
|
||||
create_example_certificate_signed_request("", ocpp::v2::CertificateSigningUseEnum::V2GCertificate));
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, handle_message_certificate_signed_v2gcertificate_symlinks_disabled) {
|
||||
set_update_certificate_symlinks_enabled(this->device_model, false);
|
||||
|
||||
// Leaf certificate should be updated.
|
||||
EXPECT_CALL(evse_security, update_leaf_certificate("", ocpp::CertificateSigningUseEnum::V2GCertificate))
|
||||
.WillOnce(Return(ocpp::InstallCertificateResult::Accepted));
|
||||
// For V2G certificates, OCSP cache update should be triggered.
|
||||
EXPECT_CALL(ocsp_updater, trigger_ocsp_cache_update()).Times(1);
|
||||
// For V2G certificates, a symlink update should not be triggered when it is not set in the device model
|
||||
EXPECT_CALL(evse_security, update_certificate_links(ocpp::CertificateSigningUseEnum::V2GCertificate)).Times(0);
|
||||
// As updating the leaf certificate is accepted, the call result will be 'Accepted'.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<CertificateSignedResponse>();
|
||||
EXPECT_EQ(response.status, CertificateSignedStatusEnum::Accepted);
|
||||
}));
|
||||
|
||||
security.handle_message(
|
||||
create_example_certificate_signed_request("", ocpp::v2::CertificateSigningUseEnum::V2GCertificate));
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, handle_message_certificate_signed_v2gcertificate_update_leaf_not_accepted) {
|
||||
set_update_certificate_symlinks_enabled(this->device_model, true);
|
||||
|
||||
// Leaf certificate should be updated, returns 'Expired'.
|
||||
EXPECT_CALL(evse_security, update_leaf_certificate("", ocpp::CertificateSigningUseEnum::V2GCertificate))
|
||||
.WillOnce(Return(ocpp::InstallCertificateResult::Expired));
|
||||
// For V2G certificates, OCSP cache update should be triggered, but only when updating leaf certificate is accepted.
|
||||
EXPECT_CALL(ocsp_updater, trigger_ocsp_cache_update()).Times(0);
|
||||
// For V2G certificates, a symlink update should be triggered when that is set in the device model
|
||||
EXPECT_CALL(evse_security, update_certificate_links(ocpp::CertificateSigningUseEnum::V2GCertificate)).Times(1);
|
||||
// As updating the leaf certificate is accepted, the call result will be 'Accepted'.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<CertificateSignedResponse>();
|
||||
EXPECT_EQ(response.status, CertificateSignedStatusEnum::Rejected);
|
||||
}));
|
||||
// Install certificate is not accepted, this should trigger a security event notification.
|
||||
EXPECT_CALL(security_event_callback_mock,
|
||||
Call(CiString<50>("InvalidChargingStationCertificate"),
|
||||
std::optional<CiString<255>>(ocpp::conversions::install_certificate_result_to_string(
|
||||
ocpp::InstallCertificateResult::Expired))));
|
||||
|
||||
security.handle_message(
|
||||
create_example_certificate_signed_request("", ocpp::v2::CertificateSigningUseEnum::V2GCertificate));
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, handle_message_certificate_signed_chargingstationcertificate_accepted_securityprofile_3) {
|
||||
set_update_certificate_symlinks_enabled(this->device_model, true);
|
||||
set_security_profile(this->device_model, 3);
|
||||
|
||||
// Leaf certificate should be updated.
|
||||
EXPECT_CALL(evse_security, update_leaf_certificate("", ocpp::CertificateSigningUseEnum::ChargingStationCertificate))
|
||||
.WillOnce(Return(ocpp::InstallCertificateResult::Accepted));
|
||||
// For Charging Station certificates, OCSP cache update should NOT be triggered.
|
||||
EXPECT_CALL(ocsp_updater, trigger_ocsp_cache_update()).Times(0);
|
||||
// For V2G certificates, a symlink update should NOT be triggered, also not when it is set in the device model.
|
||||
EXPECT_CALL(evse_security, update_certificate_links(ocpp::CertificateSigningUseEnum::V2GCertificate)).Times(0);
|
||||
// As updating the leaf certificate is accepted, the call result will be 'Accepted'.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<CertificateSignedResponse>();
|
||||
EXPECT_EQ(response.status, CertificateSignedStatusEnum::Accepted);
|
||||
}));
|
||||
// The connectivity manager should be informed of the changed certificate (because of security profile 3)
|
||||
EXPECT_CALL(connectivity_manager, on_charging_station_certificate_changed()).Times(1);
|
||||
// A security event notification should be sent (because of security profile 3)
|
||||
EXPECT_CALL(security_event_callback_mock,
|
||||
Call(CiString<50>("ReconfigurationOfSecurityParameters"),
|
||||
std::optional<CiString<255>>("Changed charging station certificate")));
|
||||
|
||||
security.handle_message(
|
||||
create_example_certificate_signed_request("", ocpp::v2::CertificateSigningUseEnum::ChargingStationCertificate));
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, handle_message_certificate_signed_chargingstationcertificate_accepted_securityprofile_1) {
|
||||
set_update_certificate_symlinks_enabled(this->device_model, true);
|
||||
set_security_profile(this->device_model, 1);
|
||||
|
||||
// Leaf certificate should be updated.
|
||||
EXPECT_CALL(evse_security, update_leaf_certificate("", ocpp::CertificateSigningUseEnum::ChargingStationCertificate))
|
||||
.WillOnce(Return(ocpp::InstallCertificateResult::Accepted));
|
||||
// For Charging Station certificates, OCSP cache update should NOT be triggered.
|
||||
EXPECT_CALL(ocsp_updater, trigger_ocsp_cache_update()).Times(0);
|
||||
// For V2G certificates, a symlink update should NOT be triggered, also not when it is set in the device model.
|
||||
EXPECT_CALL(evse_security, update_certificate_links(ocpp::CertificateSigningUseEnum::V2GCertificate)).Times(0);
|
||||
// As updating the leaf certificate is accepted, the call result will be 'Accepted'.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).WillOnce(Invoke([](const json& call_result) {
|
||||
auto response = call_result[ocpp::CALLRESULT_PAYLOAD].get<CertificateSignedResponse>();
|
||||
EXPECT_EQ(response.status, CertificateSignedStatusEnum::Accepted);
|
||||
}));
|
||||
// The connectivity manager should NOT be informed of the changed certificate (because of security profile < 3)
|
||||
EXPECT_CALL(connectivity_manager, on_charging_station_certificate_changed()).Times(0);
|
||||
// A security event notification should NOT be sent (because of security profile < 3)
|
||||
EXPECT_CALL(security_event_callback_mock, Call(_, _)).Times(0);
|
||||
|
||||
// When no certificate type is given, charging station certificate type is used.
|
||||
security.handle_message(create_example_certificate_signed_request("", std::nullopt));
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, sign_certificate_request_accepted) {
|
||||
this->device_model->set_value(ControllerComponentVariables::ChargeBoxSerialNumber.component,
|
||||
ControllerComponentVariables::ChargeBoxSerialNumber.variable.value(),
|
||||
AttributeEnum::Actual, "testserialnumber", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::OrganizationName.component,
|
||||
ControllerComponentVariables::OrganizationName.variable.value(),
|
||||
AttributeEnum::Actual, "testOrganization", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrCountryName.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrCountryName.variable.value(),
|
||||
AttributeEnum::Actual, "testCountry", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::UseTPM.component,
|
||||
ControllerComponentVariables::UseTPM.variable.value(), AttributeEnum::Actual, "false",
|
||||
"test", true);
|
||||
|
||||
// Let the request be accepted, with a CSR in the result.
|
||||
ocpp::GetCertificateSignRequestResult sign_request_result;
|
||||
sign_request_result.status = GetCertificateSignRequestStatus::Accepted;
|
||||
sign_request_result.csr = "csr";
|
||||
|
||||
EXPECT_CALL(this->evse_security,
|
||||
generate_certificate_signing_request(ocpp::CertificateSigningUseEnum::ChargingStationCertificate,
|
||||
"testCountry", "testOrganization", "testserialnumber", false))
|
||||
.WillOnce(Return(sign_request_result));
|
||||
|
||||
// This will send a 'sign certificate request' to the CSMS.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).WillOnce(Invoke([](const json& call, bool triggered) {
|
||||
auto request = call[ocpp::CALL_PAYLOAD].get<SignCertificateRequest>();
|
||||
ASSERT_TRUE(request.certificateType.has_value());
|
||||
EXPECT_EQ(request.certificateType.value(), ocpp::v2::CertificateSigningUseEnum::ChargingStationCertificate);
|
||||
EXPECT_EQ(request.csr, "csr");
|
||||
EXPECT_FALSE(triggered);
|
||||
}));
|
||||
|
||||
security.sign_certificate_req(ocpp::CertificateSigningUseEnum::ChargingStationCertificate, false);
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, sign_certificate_request_twice) {
|
||||
// When a sign certificate request is done twice and the first does not have a response yet, the second request is
|
||||
// not handled.
|
||||
this->device_model->set_value(ControllerComponentVariables::ChargeBoxSerialNumber.component,
|
||||
ControllerComponentVariables::ChargeBoxSerialNumber.variable.value(),
|
||||
AttributeEnum::Actual, "testserialnumber", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::OrganizationName.component,
|
||||
ControllerComponentVariables::OrganizationName.variable.value(),
|
||||
AttributeEnum::Actual, "testOrganization", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrCountryName.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrCountryName.variable.value(),
|
||||
AttributeEnum::Actual, "testCountry", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::UseTPM.component,
|
||||
ControllerComponentVariables::UseTPM.variable.value(), AttributeEnum::Actual, "false",
|
||||
"test", true);
|
||||
|
||||
ocpp::GetCertificateSignRequestResult sign_request_result;
|
||||
sign_request_result.status = GetCertificateSignRequestStatus::Accepted;
|
||||
sign_request_result.csr = "csr";
|
||||
|
||||
EXPECT_CALL(this->evse_security,
|
||||
generate_certificate_signing_request(ocpp::CertificateSigningUseEnum::ChargingStationCertificate,
|
||||
"testCountry", "testOrganization", "testserialnumber", false))
|
||||
.WillOnce(Return(sign_request_result));
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).WillOnce(Invoke([](const json& call, bool triggered) {
|
||||
auto request = call[ocpp::CALL_PAYLOAD].get<SignCertificateRequest>();
|
||||
ASSERT_TRUE(request.certificateType.has_value());
|
||||
EXPECT_EQ(request.certificateType.value(), ocpp::v2::CertificateSigningUseEnum::ChargingStationCertificate);
|
||||
EXPECT_EQ(request.csr, "csr");
|
||||
EXPECT_FALSE(triggered);
|
||||
}));
|
||||
|
||||
security.sign_certificate_req(ocpp::CertificateSigningUseEnum::ChargingStationCertificate, false);
|
||||
|
||||
// Now try a second time, which should fail because there is no answer yet.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).Times(0);
|
||||
|
||||
security.sign_certificate_req(ocpp::CertificateSigningUseEnum::ChargingStationCertificate, false);
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, sign_certificate_request_accepted_no_csr) {
|
||||
// Try to sign a certificate request, but the 'evse security' does not return a CSR.
|
||||
this->device_model->set_value(ControllerComponentVariables::ChargeBoxSerialNumber.component,
|
||||
ControllerComponentVariables::ChargeBoxSerialNumber.variable.value(),
|
||||
AttributeEnum::Actual, "testserialnumber", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::OrganizationName.component,
|
||||
ControllerComponentVariables::OrganizationName.variable.value(),
|
||||
AttributeEnum::Actual, "testOrganization", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrCountryName.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrCountryName.variable.value(),
|
||||
AttributeEnum::Actual, "testCountry", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::UseTPM.component,
|
||||
ControllerComponentVariables::UseTPM.variable.value(), AttributeEnum::Actual, "false",
|
||||
"test", true);
|
||||
|
||||
// An empty CSR is returned, although the status is 'Accepted'.
|
||||
ocpp::GetCertificateSignRequestResult sign_request_result;
|
||||
sign_request_result.status = GetCertificateSignRequestStatus::Accepted;
|
||||
sign_request_result.csr = std::nullopt;
|
||||
|
||||
EXPECT_CALL(this->evse_security,
|
||||
generate_certificate_signing_request(ocpp::CertificateSigningUseEnum::ChargingStationCertificate,
|
||||
"testCountry", "testOrganization", "testserialnumber", false))
|
||||
.WillOnce(Return(sign_request_result));
|
||||
|
||||
// A security event notification will be sent to inform the CSMS that the generation of the CSR has failed.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).WillOnce(Invoke([](const json& call, bool triggered) {
|
||||
auto request = call[ocpp::CALL_PAYLOAD].get<SecurityEventNotificationRequest>();
|
||||
EXPECT_EQ(request.get_type(), "SecurityEventNotification");
|
||||
EXPECT_EQ(request.type.get(), ocpp::security_events::CSRGENERATIONFAILED);
|
||||
EXPECT_FALSE(triggered);
|
||||
}));
|
||||
|
||||
// And a security event is sent as well.
|
||||
EXPECT_CALL(security_event_callback_mock, Call(CiString<50>(ocpp::security_events::CSRGENERATIONFAILED), _));
|
||||
|
||||
security.sign_certificate_req(ocpp::CertificateSigningUseEnum::ChargingStationCertificate, false);
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, sign_certificate_request_not_accepted) {
|
||||
this->device_model->set_value(ControllerComponentVariables::ChargeBoxSerialNumber.component,
|
||||
ControllerComponentVariables::ChargeBoxSerialNumber.variable.value(),
|
||||
AttributeEnum::Actual, "testserialnumber", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::OrganizationName.component,
|
||||
ControllerComponentVariables::OrganizationName.variable.value(),
|
||||
AttributeEnum::Actual, "testOrganization", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrCountryName.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrCountryName.variable.value(),
|
||||
AttributeEnum::Actual, "testCountry", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::UseTPM.component,
|
||||
ControllerComponentVariables::UseTPM.variable.value(), AttributeEnum::Actual, "false",
|
||||
"test", true);
|
||||
|
||||
// Try to send a certificate request, but the generation of the CSR fails.
|
||||
ocpp::GetCertificateSignRequestResult sign_request_result;
|
||||
sign_request_result.status = GetCertificateSignRequestStatus::GenerationError;
|
||||
sign_request_result.csr = "csr";
|
||||
|
||||
EXPECT_CALL(this->evse_security,
|
||||
generate_certificate_signing_request(ocpp::CertificateSigningUseEnum::ChargingStationCertificate,
|
||||
"testCountry", "testOrganization", "testserialnumber", false))
|
||||
.WillOnce(Return(sign_request_result));
|
||||
|
||||
// A security event notification is sent to the CSMS to let it know that the CSR generation has failed.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).WillOnce(Invoke([](const json& call, bool triggered) {
|
||||
auto request = call[ocpp::CALL_PAYLOAD].get<SecurityEventNotificationRequest>();
|
||||
EXPECT_EQ(request.get_type(), "SecurityEventNotification");
|
||||
EXPECT_EQ(request.type.get(), ocpp::security_events::CSRGENERATIONFAILED);
|
||||
EXPECT_FALSE(triggered);
|
||||
}));
|
||||
|
||||
// And a security event is sent.
|
||||
EXPECT_CALL(security_event_callback_mock, Call(CiString<50>(ocpp::security_events::CSRGENERATIONFAILED), _));
|
||||
|
||||
security.sign_certificate_req(ocpp::CertificateSigningUseEnum::ChargingStationCertificate, false);
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, sign_certificate_request_no_organization_name) {
|
||||
// Try to sign certificate request, but the organization name is not given.
|
||||
device_model_test_helper.remove_variable_from_db(
|
||||
ControllerComponentVariables::OrganizationName.component.name.get(), std::nullopt, std::nullopt, std::nullopt,
|
||||
ControllerComponentVariables::OrganizationName.variable->name.get(), std::nullopt);
|
||||
|
||||
device_model = device_model_test_helper.get_device_model();
|
||||
const FunctionalBlockContext b{
|
||||
this->mock_dispatcher, *this->device_model, this->connectivity_manager, this->evse_manager,
|
||||
this->database_handler_mock, this->evse_security, this->component_state_manager, this->ocpp_version};
|
||||
Security s(b, logging, ocsp_updater, security_event_callback_mock.AsStdFunction());
|
||||
|
||||
this->device_model->set_value(ControllerComponentVariables::ChargeBoxSerialNumber.component,
|
||||
ControllerComponentVariables::ChargeBoxSerialNumber.variable.value(),
|
||||
AttributeEnum::Actual, "testserialnumber", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrCountryName.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrCountryName.variable.value(),
|
||||
AttributeEnum::Actual, "testCountry", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::UseTPM.component,
|
||||
ControllerComponentVariables::UseTPM.variable.value(), AttributeEnum::Actual, "false",
|
||||
"test", true);
|
||||
|
||||
// So the request will not be sent at all.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).Times(0);
|
||||
|
||||
s.sign_certificate_req(ocpp::CertificateSigningUseEnum::ChargingStationCertificate, false);
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, sign_certificate_request_no_serial_number) {
|
||||
// Try to sign certificate request, but the serial number is not given.
|
||||
device_model_test_helper.remove_variable_from_db(
|
||||
ControllerComponentVariables::OrganizationName.component.name.get(), std::nullopt, std::nullopt, std::nullopt,
|
||||
ControllerComponentVariables::OrganizationName.variable->name.get(), std::nullopt);
|
||||
|
||||
device_model = device_model_test_helper.get_device_model();
|
||||
|
||||
const FunctionalBlockContext b{
|
||||
this->mock_dispatcher, *this->device_model, this->connectivity_manager, this->evse_manager,
|
||||
this->database_handler_mock, this->evse_security, this->component_state_manager, this->ocpp_version};
|
||||
Security s(b, logging, ocsp_updater, security_event_callback_mock.AsStdFunction());
|
||||
|
||||
this->device_model->set_value(ControllerComponentVariables::OrganizationName.component,
|
||||
ControllerComponentVariables::OrganizationName.variable.value(),
|
||||
AttributeEnum::Actual, "testOrganization", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrCountryName.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrCountryName.variable.value(),
|
||||
AttributeEnum::Actual, "testCountry", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::UseTPM.component,
|
||||
ControllerComponentVariables::UseTPM.variable.value(), AttributeEnum::Actual, "false",
|
||||
"test", true);
|
||||
|
||||
// So the request will not be sent at all.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).Times(0);
|
||||
|
||||
s.sign_certificate_req(ocpp::CertificateSigningUseEnum::ChargingStationCertificate, false);
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, sign_certificate_request_no_country) {
|
||||
// Try to sign certificate request, but the country is not given.
|
||||
device_model_test_helper.remove_variable_from_db(
|
||||
ControllerComponentVariables::OrganizationName.component.name.get(), std::nullopt, std::nullopt, std::nullopt,
|
||||
ControllerComponentVariables::OrganizationName.variable->name.get(), std::nullopt);
|
||||
|
||||
device_model = device_model_test_helper.get_device_model();
|
||||
const FunctionalBlockContext b{
|
||||
this->mock_dispatcher, *this->device_model, this->connectivity_manager, this->evse_manager,
|
||||
this->database_handler_mock, this->evse_security, this->component_state_manager, this->ocpp_version};
|
||||
Security s(b, logging, ocsp_updater, security_event_callback_mock.AsStdFunction());
|
||||
|
||||
this->device_model->set_value(ControllerComponentVariables::OrganizationName.component,
|
||||
ControllerComponentVariables::OrganizationName.variable.value(),
|
||||
AttributeEnum::Actual, "testOrganization", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::ChargeBoxSerialNumber.component,
|
||||
ControllerComponentVariables::ChargeBoxSerialNumber.variable.value(),
|
||||
AttributeEnum::Actual, "testserialnumber", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::UseTPM.component,
|
||||
ControllerComponentVariables::UseTPM.variable.value(), AttributeEnum::Actual, "false",
|
||||
"test", true);
|
||||
|
||||
// So the request will not be sent at all.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).Times(0);
|
||||
|
||||
s.sign_certificate_req(ocpp::CertificateSigningUseEnum::ChargingStationCertificate, false);
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, sign_certificate_request_v2g_accepted) {
|
||||
// Try to sign v2g certificate request.
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrSeccId.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrSeccId.variable.value(),
|
||||
AttributeEnum::Actual, "iso_testcommonname", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrOrganizationName.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrOrganizationName.variable.value(),
|
||||
AttributeEnum::Actual, "iso_testOrganization", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrCountryName.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrCountryName.variable.value(),
|
||||
AttributeEnum::Actual, "iso_testCountry", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::UseTPMSeccLeafCertificate.component,
|
||||
ControllerComponentVariables::UseTPMSeccLeafCertificate.variable.value(),
|
||||
AttributeEnum::Actual, "true", "test", true);
|
||||
|
||||
// Which is accepted by 'evse security'.
|
||||
ocpp::GetCertificateSignRequestResult sign_request_result;
|
||||
sign_request_result.status = GetCertificateSignRequestStatus::Accepted;
|
||||
sign_request_result.csr = "csr";
|
||||
|
||||
EXPECT_CALL(this->evse_security,
|
||||
generate_certificate_signing_request(ocpp::CertificateSigningUseEnum::V2GCertificate, "iso_testCountry",
|
||||
"iso_testOrganization", "iso_testcommonname", true))
|
||||
.WillOnce(Return(sign_request_result));
|
||||
|
||||
// This will send a 'SignCertificateRequest' to the CSMS.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).WillOnce(Invoke([](const json& call, bool triggered) {
|
||||
auto request = call[ocpp::CALL_PAYLOAD].get<SignCertificateRequest>();
|
||||
ASSERT_TRUE(request.certificateType.has_value());
|
||||
EXPECT_EQ(request.certificateType.value(), ocpp::v2::CertificateSigningUseEnum::V2GCertificate);
|
||||
EXPECT_EQ(request.csr, "csr");
|
||||
EXPECT_TRUE(triggered);
|
||||
}));
|
||||
|
||||
security.sign_certificate_req(ocpp::CertificateSigningUseEnum::V2GCertificate, true);
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, sign_certificate_request_manufacturer_cert_accepted) {
|
||||
// Try to sign manufacturer certificate request.
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrSeccId.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrSeccId.variable.value(),
|
||||
AttributeEnum::Actual, "iso_testcommonname", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrOrganizationName.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrOrganizationName.variable.value(),
|
||||
AttributeEnum::Actual, "iso_testOrganization", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrCountryName.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrCountryName.variable.value(),
|
||||
AttributeEnum::Actual, "iso_testCountry", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::UseTPMSeccLeafCertificate.component,
|
||||
ControllerComponentVariables::UseTPMSeccLeafCertificate.variable.value(),
|
||||
AttributeEnum::Actual, "true", "test", true);
|
||||
|
||||
// Which is accepted by 'evse security'.
|
||||
ocpp::GetCertificateSignRequestResult sign_request_result;
|
||||
sign_request_result.status = GetCertificateSignRequestStatus::Accepted;
|
||||
sign_request_result.csr = "csr";
|
||||
|
||||
EXPECT_CALL(this->evse_security, generate_certificate_signing_request(
|
||||
ocpp::CertificateSigningUseEnum::ManufacturerCertificate, "iso_testCountry",
|
||||
"iso_testOrganization", "iso_testcommonname", true))
|
||||
.WillOnce(Return(sign_request_result));
|
||||
|
||||
// This will send a 'SignCertificateRequest' to the CSMS.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).WillOnce(Invoke([](const json& call, bool triggered) {
|
||||
auto request = call[ocpp::CALL_PAYLOAD].get<SignCertificateRequest>();
|
||||
ASSERT_TRUE(request.certificateType.has_value());
|
||||
EXPECT_EQ(request.certificateType.value(), ocpp::v2::CertificateSigningUseEnum::V2GCertificate);
|
||||
EXPECT_EQ(request.csr, "csr");
|
||||
EXPECT_TRUE(triggered);
|
||||
}));
|
||||
|
||||
security.sign_certificate_req(ocpp::CertificateSigningUseEnum::ManufacturerCertificate, true);
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, sign_certificate_request_v2g_no_common_name) {
|
||||
// Try to sign v2g certificate request, but the common name is not given.
|
||||
device_model_test_helper.remove_variable_from_db(
|
||||
ControllerComponentVariables::ISO15118CtrlrSeccId.component.name.get(), std::nullopt, std::nullopt,
|
||||
std::nullopt, ControllerComponentVariables::ISO15118CtrlrSeccId.variable->name.get(), std::nullopt);
|
||||
|
||||
device_model = device_model_test_helper.get_device_model();
|
||||
const FunctionalBlockContext b{
|
||||
this->mock_dispatcher, *this->device_model, this->connectivity_manager, this->evse_manager,
|
||||
this->database_handler_mock, this->evse_security, this->component_state_manager, this->ocpp_version};
|
||||
Security s(b, logging, ocsp_updater, security_event_callback_mock.AsStdFunction());
|
||||
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrOrganizationName.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrOrganizationName.variable.value(),
|
||||
AttributeEnum::Actual, "iso_testOrganization", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrCountryName.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrCountryName.variable.value(),
|
||||
AttributeEnum::Actual, "iso_testCountry", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::UseTPM.component,
|
||||
ControllerComponentVariables::UseTPM.variable.value(), AttributeEnum::Actual, "false",
|
||||
"test", true);
|
||||
|
||||
// So the request will not be sent at all.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).Times(0);
|
||||
|
||||
s.sign_certificate_req(ocpp::CertificateSigningUseEnum::V2GCertificate, false);
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, sign_certificate_request_v2g_no_organization) {
|
||||
// Try to sign v2g certificate request, but the organization is not given.
|
||||
device_model_test_helper.remove_variable_from_db(
|
||||
ControllerComponentVariables::ISO15118CtrlrOrganizationName.component.name.get(), std::nullopt, std::nullopt,
|
||||
std::nullopt, ControllerComponentVariables::ISO15118CtrlrOrganizationName.variable->name.get(), std::nullopt);
|
||||
|
||||
device_model = device_model_test_helper.get_device_model();
|
||||
const FunctionalBlockContext b{
|
||||
this->mock_dispatcher, *this->device_model, this->connectivity_manager, this->evse_manager,
|
||||
this->database_handler_mock, this->evse_security, this->component_state_manager, this->ocpp_version};
|
||||
Security s(b, logging, ocsp_updater, security_event_callback_mock.AsStdFunction());
|
||||
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrSeccId.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrSeccId.variable.value(),
|
||||
AttributeEnum::Actual, "iso_testcommonname", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrCountryName.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrCountryName.variable.value(),
|
||||
AttributeEnum::Actual, "iso_testCountry", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::UseTPM.component,
|
||||
ControllerComponentVariables::UseTPM.variable.value(), AttributeEnum::Actual, "false",
|
||||
"test", true);
|
||||
|
||||
// So the request will not be sent at all.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).Times(0);
|
||||
|
||||
s.sign_certificate_req(ocpp::CertificateSigningUseEnum::ManufacturerCertificate, false);
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, sign_certificate_request_v2g_no_country) {
|
||||
// Try to sign v2g certificate request, but the country is not given.
|
||||
device_model_test_helper.remove_variable_from_db(
|
||||
ControllerComponentVariables::ISO15118CtrlrCountryName.component.name.get(), std::nullopt, std::nullopt,
|
||||
std::nullopt, ControllerComponentVariables::ISO15118CtrlrCountryName.variable->name.get(), std::nullopt);
|
||||
|
||||
device_model = device_model_test_helper.get_device_model();
|
||||
const FunctionalBlockContext b{
|
||||
this->mock_dispatcher, *this->device_model, this->connectivity_manager, this->evse_manager,
|
||||
this->database_handler_mock, this->evse_security, this->component_state_manager, this->ocpp_version};
|
||||
Security s(b, logging, ocsp_updater, security_event_callback_mock.AsStdFunction());
|
||||
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrSeccId.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrSeccId.variable.value(),
|
||||
AttributeEnum::Actual, "iso_testcommonname", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrOrganizationName.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrOrganizationName.variable.value(),
|
||||
AttributeEnum::Actual, "iso_testOrganization", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::UseTPM.component,
|
||||
ControllerComponentVariables::UseTPM.variable.value(), AttributeEnum::Actual, "false",
|
||||
"test", true);
|
||||
|
||||
// So the request will not be sent at all.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).Times(0);
|
||||
|
||||
s.sign_certificate_req(ocpp::CertificateSigningUseEnum::V2GCertificate, false);
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, security_event_notification_no_timestamp) {
|
||||
// Send security event notification, without giving a timestamp.
|
||||
// For testing purposes, store the current time.
|
||||
const DateTime timestamp_test_start = DateTime();
|
||||
|
||||
// The security event notification request will be sent to the CSMS.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _))
|
||||
.WillOnce(Invoke([×tamp_test_start](const json& call, bool triggered) {
|
||||
auto request = call[ocpp::CALL_PAYLOAD].get<SecurityEventNotificationRequest>();
|
||||
EXPECT_EQ(request.get_type(), "SecurityEventNotification");
|
||||
EXPECT_EQ(request.type.get(), "test");
|
||||
EXPECT_EQ(request.techInfo->get(), "tech info!!");
|
||||
// Datetime must be somewhere between the start of the test and 'now'.
|
||||
DateTime now;
|
||||
// With the conversion from and to rfc3339 (for conversion to / from json), precision is lost. So we do the
|
||||
// same here, otherwise the test might fail.
|
||||
now.from_rfc3339(DateTime().to_rfc3339());
|
||||
EXPECT_LE(request.timestamp.to_time_point(), now.to_time_point());
|
||||
// With the conversion from and to rfc3339 (for conversion to / from json), precision is lost. So we do the
|
||||
// same here, otherwise the test might fail.
|
||||
DateTime start_time;
|
||||
start_time.from_rfc3339(timestamp_test_start.to_rfc3339());
|
||||
EXPECT_GE(request.timestamp.to_time_point(), start_time.to_time_point());
|
||||
EXPECT_FALSE(triggered);
|
||||
}));
|
||||
|
||||
// And the security event callback is called.
|
||||
EXPECT_CALL(security_event_callback_mock, Call(CiString<50>("test"), _));
|
||||
|
||||
security.security_event_notification_req("test", "tech info!!", true, true, std::nullopt);
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, security_event_notification_with_timestamp) {
|
||||
// Send security event notification, with a given timestamp.
|
||||
const auto timestamp = DateTime(date::utc_clock::now() - std::chrono::minutes(15));
|
||||
|
||||
// The security event notification request will be sent to the CSMS.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).WillOnce(Invoke([×tamp](const json& call, bool triggered) {
|
||||
auto request = call[ocpp::CALL_PAYLOAD].get<SecurityEventNotificationRequest>();
|
||||
EXPECT_EQ(request.get_type(), "SecurityEventNotification");
|
||||
EXPECT_EQ(request.type.get(), "test_with_given_timestamp");
|
||||
// Timestamp must be the exact timestamp we have given.
|
||||
EXPECT_EQ(request.timestamp.to_rfc3339(), timestamp.to_rfc3339());
|
||||
EXPECT_FALSE(triggered);
|
||||
}));
|
||||
|
||||
// And the security event callback is called.
|
||||
EXPECT_CALL(security_event_callback_mock, Call(CiString<50>("test_with_given_timestamp"), _));
|
||||
|
||||
security.security_event_notification_req("test_with_given_timestamp", std::nullopt, true, true, timestamp);
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, security_event_notification_not_critical) {
|
||||
// Trigger a not critical security event notification
|
||||
// Which will not send the security event to the CSMS.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).Times(0);
|
||||
|
||||
// But it will call the security event callback.
|
||||
EXPECT_CALL(security_event_callback_mock, Call(CiString<50>("test"), _));
|
||||
|
||||
security.security_event_notification_req("test", std::nullopt, true, false, std::nullopt);
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, security_event_notification_not_critital_not_triggered_internally) {
|
||||
// Trigger a not critical security event that is not triggered internally.
|
||||
// Which will not be sent to the CSMS.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).Times(0);
|
||||
|
||||
// And the callback is also not called.
|
||||
EXPECT_CALL(security_event_callback_mock, Call(CiString<50>("test"), _)).Times(0);
|
||||
|
||||
security.security_event_notification_req("test", std::nullopt, false, false, std::nullopt);
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, security_event_notification_not_triggered_internally) {
|
||||
// Trigger a critical security event.
|
||||
// Which will send a security event notification to the CSMS.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).WillOnce(Invoke([&](const json& call, bool triggered) {
|
||||
auto request = call[ocpp::CALL_PAYLOAD].get<SecurityEventNotificationRequest>();
|
||||
EXPECT_EQ(request.get_type(), "SecurityEventNotification");
|
||||
EXPECT_EQ(request.type.get(), "not_triggered_internally");
|
||||
EXPECT_FALSE(triggered);
|
||||
}));
|
||||
|
||||
// But when not triggered internally, the callback is not called.
|
||||
EXPECT_CALL(security_event_callback_mock, Call(_, _)).Times(0);
|
||||
|
||||
security.security_event_notification_req("not_triggered_internally", std::nullopt, false, true, std::nullopt);
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, security_event_notification_no_callback) {
|
||||
// Trigger a critical security event, but there is no callback to call.
|
||||
const FunctionalBlockContext b{
|
||||
this->mock_dispatcher, *this->device_model, this->connectivity_manager, this->evse_manager,
|
||||
this->database_handler_mock, this->evse_security, this->component_state_manager, this->ocpp_version};
|
||||
Security s(b, logging, ocsp_updater, nullptr);
|
||||
|
||||
// This will send a security event notification to the CSMS.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).WillOnce(Invoke([&](const json& call, bool triggered) {
|
||||
auto request = call[ocpp::CALL_PAYLOAD].get<SecurityEventNotificationRequest>();
|
||||
EXPECT_EQ(request.get_type(), "SecurityEventNotification");
|
||||
EXPECT_EQ(request.type.get(), "no_callback");
|
||||
EXPECT_FALSE(triggered);
|
||||
}));
|
||||
|
||||
// But the mock is not called, since it is not given in the constructor.
|
||||
EXPECT_CALL(security_event_callback_mock, Call(_, _)).Times(0);
|
||||
|
||||
s.security_event_notification_req("no_callback", std::nullopt, true, true, std::nullopt);
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, handle_sign_certificate_response_successful) {
|
||||
// Sign certificate and wait for certificate signed.
|
||||
timer_stub_reset_timeout_called_count();
|
||||
set_update_certificate_symlinks_enabled(this->device_model, true);
|
||||
set_security_profile(this->device_model, 1);
|
||||
this->device_model->set_value(ControllerComponentVariables::ChargeBoxSerialNumber.component,
|
||||
ControllerComponentVariables::ChargeBoxSerialNumber.variable.value(),
|
||||
AttributeEnum::Actual, "testserialnumber", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::OrganizationName.component,
|
||||
ControllerComponentVariables::OrganizationName.variable.value(),
|
||||
AttributeEnum::Actual, "testOrganization", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrCountryName.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrCountryName.variable.value(),
|
||||
AttributeEnum::Actual, "testCountry", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::UseTPM.component,
|
||||
ControllerComponentVariables::UseTPM.variable.value(), AttributeEnum::Actual, "false",
|
||||
"test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::CertSigningWaitMinimum.component,
|
||||
ControllerComponentVariables::CertSigningWaitMinimum.variable.value(),
|
||||
AttributeEnum::Actual, "1", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::CertSigningRepeatTimes.component,
|
||||
ControllerComponentVariables::CertSigningRepeatTimes.variable.value(),
|
||||
AttributeEnum::Actual, "2", "test", true);
|
||||
|
||||
// The 'sign_certificate_req' will send a 'sign certificate request' to the CSMS.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).WillOnce(Invoke([](const json& call, bool triggered) {
|
||||
auto request = call[ocpp::CALL_PAYLOAD].get<SignCertificateRequest>();
|
||||
ASSERT_TRUE(request.certificateType.has_value());
|
||||
EXPECT_EQ(request.certificateType.value(), ocpp::v2::CertificateSigningUseEnum::ChargingStationCertificate);
|
||||
EXPECT_EQ(request.csr, "csr");
|
||||
EXPECT_FALSE(triggered);
|
||||
}));
|
||||
|
||||
// First do a request.
|
||||
ocpp::GetCertificateSignRequestResult sign_request_result;
|
||||
sign_request_result.status = GetCertificateSignRequestStatus::Accepted;
|
||||
sign_request_result.csr = "csr";
|
||||
|
||||
EXPECT_CALL(this->evse_security,
|
||||
generate_certificate_signing_request(ocpp::CertificateSigningUseEnum::ChargingStationCertificate,
|
||||
"testCountry", "testOrganization", "testserialnumber", false))
|
||||
.WillOnce(Return(sign_request_result));
|
||||
|
||||
security.sign_certificate_req(ocpp::CertificateSigningUseEnum::ChargingStationCertificate, false);
|
||||
|
||||
// Then the response is processed.
|
||||
const ocpp::EnhancedMessage<MessageType> response =
|
||||
create_example_sign_certificate_response(GenericStatusEnum::Accepted);
|
||||
|
||||
security.handle_message(response);
|
||||
EXPECT_GE(timer_stub_get_timeout_called_count(), 1);
|
||||
|
||||
// Leaf certificate should be updated.
|
||||
EXPECT_CALL(evse_security, update_leaf_certificate("", ocpp::CertificateSigningUseEnum::ChargingStationCertificate))
|
||||
.WillOnce(Return(ocpp::InstallCertificateResult::Accepted));
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_));
|
||||
|
||||
security.handle_message(create_example_certificate_signed_request("", std::nullopt));
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, handle_sign_certificate_response_no_response) {
|
||||
// Sign certificate and wait for certificate signed, but call callback of timer instead (simulating that
|
||||
// certificate signed request is never called).
|
||||
timer_stub_reset_timeout_called_count();
|
||||
timer_stub_reset_callback();
|
||||
set_update_certificate_symlinks_enabled(this->device_model, true);
|
||||
set_security_profile(this->device_model, 1);
|
||||
this->device_model->set_value(ControllerComponentVariables::ChargeBoxSerialNumber.component,
|
||||
ControllerComponentVariables::ChargeBoxSerialNumber.variable.value(),
|
||||
AttributeEnum::Actual, "testserialnumber", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::OrganizationName.component,
|
||||
ControllerComponentVariables::OrganizationName.variable.value(),
|
||||
AttributeEnum::Actual, "testOrganization", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrCountryName.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrCountryName.variable.value(),
|
||||
AttributeEnum::Actual, "testCountry", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::UseTPM.component,
|
||||
ControllerComponentVariables::UseTPM.variable.value(), AttributeEnum::Actual, "false",
|
||||
"test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::CertSigningWaitMinimum.component,
|
||||
ControllerComponentVariables::CertSigningWaitMinimum.variable.value(),
|
||||
AttributeEnum::Actual, "1", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::CertSigningRepeatTimes.component,
|
||||
ControllerComponentVariables::CertSigningRepeatTimes.variable.value(),
|
||||
AttributeEnum::Actual, "2", "test", true);
|
||||
|
||||
// The 'sign_certificate_req' will send a 'sign certificate request' to the CSMS.
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).WillRepeatedly(Invoke([](const json& call, bool triggered) {
|
||||
auto request = call[ocpp::CALL_PAYLOAD].get<SignCertificateRequest>();
|
||||
ASSERT_TRUE(request.certificateType.has_value());
|
||||
EXPECT_EQ(request.certificateType.value(), ocpp::v2::CertificateSigningUseEnum::ChargingStationCertificate);
|
||||
EXPECT_EQ(request.csr, "csr");
|
||||
EXPECT_FALSE(triggered);
|
||||
}));
|
||||
|
||||
// First do a request.
|
||||
ocpp::GetCertificateSignRequestResult sign_request_result;
|
||||
sign_request_result.status = GetCertificateSignRequestStatus::Accepted;
|
||||
sign_request_result.csr = "csr";
|
||||
|
||||
EXPECT_CALL(this->evse_security,
|
||||
generate_certificate_signing_request(ocpp::CertificateSigningUseEnum::ChargingStationCertificate,
|
||||
"testCountry", "testOrganization", "testserialnumber", false))
|
||||
.WillOnce(Return(sign_request_result));
|
||||
|
||||
security.sign_certificate_req(ocpp::CertificateSigningUseEnum::ChargingStationCertificate, false);
|
||||
|
||||
// Then the response is processed.
|
||||
const ocpp::EnhancedMessage<MessageType> response =
|
||||
create_example_sign_certificate_response(GenericStatusEnum::Accepted);
|
||||
|
||||
security.handle_message(response);
|
||||
EXPECT_GE(timer_stub_get_timeout_called_count(), 1);
|
||||
|
||||
// Leaf certificate should be updated.
|
||||
ON_CALL(evse_security, update_leaf_certificate("", ocpp::CertificateSigningUseEnum::ChargingStationCertificate))
|
||||
.WillByDefault(Return(ocpp::InstallCertificateResult::Accepted));
|
||||
|
||||
EXPECT_CALL(this->evse_security,
|
||||
generate_certificate_signing_request(ocpp::CertificateSigningUseEnum::ChargingStationCertificate,
|
||||
"testCountry", "testOrganization", "testserialnumber", false))
|
||||
.WillOnce(Return(sign_request_result));
|
||||
|
||||
// Timeout is over, callback is called.
|
||||
timer_stub_get_callback()();
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, handle_sign_certificate_response_backoff_values) {
|
||||
// Verify the exponential backoff sequence: with CertSigningWaitMinimum=30 and CertSigningRepeatTimes=2,
|
||||
// first wait should be 30s, second wait 60s, then stop.
|
||||
timer_stub_reset_timeout_called_count();
|
||||
timer_stub_reset_callback();
|
||||
timer_stub_reset_timeout_interval();
|
||||
set_update_certificate_symlinks_enabled(this->device_model, true);
|
||||
set_security_profile(this->device_model, 1);
|
||||
this->device_model->set_value(ControllerComponentVariables::ChargeBoxSerialNumber.component,
|
||||
ControllerComponentVariables::ChargeBoxSerialNumber.variable.value(),
|
||||
AttributeEnum::Actual, "testserialnumber", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::OrganizationName.component,
|
||||
ControllerComponentVariables::OrganizationName.variable.value(),
|
||||
AttributeEnum::Actual, "testOrganization", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrCountryName.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrCountryName.variable.value(),
|
||||
AttributeEnum::Actual, "testCountry", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::UseTPM.component,
|
||||
ControllerComponentVariables::UseTPM.variable.value(), AttributeEnum::Actual, "false",
|
||||
"test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::CertSigningWaitMinimum.component,
|
||||
ControllerComponentVariables::CertSigningWaitMinimum.variable.value(),
|
||||
AttributeEnum::Actual, "30", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::CertSigningRepeatTimes.component,
|
||||
ControllerComponentVariables::CertSigningRepeatTimes.variable.value(),
|
||||
AttributeEnum::Actual, "2", "test", true);
|
||||
|
||||
ocpp::GetCertificateSignRequestResult sign_request_result;
|
||||
sign_request_result.status = GetCertificateSignRequestStatus::Accepted;
|
||||
sign_request_result.csr = "csr";
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).WillRepeatedly(Invoke([](const json& call, bool triggered) {
|
||||
// Accept all SignCertificate.req dispatches
|
||||
}));
|
||||
EXPECT_CALL(this->evse_security,
|
||||
generate_certificate_signing_request(ocpp::CertificateSigningUseEnum::ChargingStationCertificate,
|
||||
"testCountry", "testOrganization", "testserialnumber", false))
|
||||
.WillRepeatedly(Return(sign_request_result));
|
||||
|
||||
// Initial sign_certificate_req + Accepted response
|
||||
security.sign_certificate_req(ocpp::CertificateSigningUseEnum::ChargingStationCertificate, false);
|
||||
security.handle_message(create_example_sign_certificate_response(GenericStatusEnum::Accepted));
|
||||
|
||||
// First timeout should be 30s (CertSigningWaitMinimum * 2^0)
|
||||
EXPECT_EQ(timer_stub_get_timeout_interval_ms(), 30000);
|
||||
|
||||
// Fire timeout callback → triggers retry → feed new Accepted response → new timer
|
||||
timer_stub_reset_timeout_interval();
|
||||
timer_stub_get_callback()(); // increments csr_attempt to 2
|
||||
security.handle_message(create_example_sign_certificate_response(GenericStatusEnum::Accepted));
|
||||
|
||||
// Second timeout should be 60s (CertSigningWaitMinimum * 2^1)
|
||||
EXPECT_EQ(timer_stub_get_timeout_interval_ms(), 60000);
|
||||
|
||||
// Fire timeout callback again → csr_attempt becomes 3 > RepeatTimes(2) → should stop
|
||||
timer_stub_reset_timeout_called_count();
|
||||
timer_stub_reset_timeout_interval();
|
||||
timer_stub_get_callback()(); // increments csr_attempt to 3
|
||||
security.handle_message(create_example_sign_certificate_response(GenericStatusEnum::Accepted));
|
||||
|
||||
// Timer should not have been restarted (csr_attempt > CertSigningRepeatTimes)
|
||||
EXPECT_EQ(timer_stub_get_timeout_called_count(), 0);
|
||||
EXPECT_EQ(timer_stub_get_timeout_interval_ms(), 0);
|
||||
}
|
||||
|
||||
TEST_F(SecurityTest, handle_sign_certificate_response_backoff_floor) {
|
||||
// Verify the 10s safety floor: with CertSigningWaitMinimum=0, the floor of 10s should apply.
|
||||
timer_stub_reset_timeout_called_count();
|
||||
timer_stub_reset_callback();
|
||||
timer_stub_reset_timeout_interval();
|
||||
set_update_certificate_symlinks_enabled(this->device_model, true);
|
||||
set_security_profile(this->device_model, 1);
|
||||
this->device_model->set_value(ControllerComponentVariables::ChargeBoxSerialNumber.component,
|
||||
ControllerComponentVariables::ChargeBoxSerialNumber.variable.value(),
|
||||
AttributeEnum::Actual, "testserialnumber", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::OrganizationName.component,
|
||||
ControllerComponentVariables::OrganizationName.variable.value(),
|
||||
AttributeEnum::Actual, "testOrganization", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::ISO15118CtrlrCountryName.component,
|
||||
ControllerComponentVariables::ISO15118CtrlrCountryName.variable.value(),
|
||||
AttributeEnum::Actual, "testCountry", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::UseTPM.component,
|
||||
ControllerComponentVariables::UseTPM.variable.value(), AttributeEnum::Actual, "false",
|
||||
"test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::CertSigningWaitMinimum.component,
|
||||
ControllerComponentVariables::CertSigningWaitMinimum.variable.value(),
|
||||
AttributeEnum::Actual, "0", "test", true);
|
||||
this->device_model->set_value(ControllerComponentVariables::CertSigningRepeatTimes.component,
|
||||
ControllerComponentVariables::CertSigningRepeatTimes.variable.value(),
|
||||
AttributeEnum::Actual, "1", "test", true);
|
||||
|
||||
ocpp::GetCertificateSignRequestResult sign_request_result;
|
||||
sign_request_result.status = GetCertificateSignRequestStatus::Accepted;
|
||||
sign_request_result.csr = "csr";
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call(_, _)).WillRepeatedly(Invoke([](const json& call, bool triggered) {
|
||||
// Accept all SignCertificate.req dispatches
|
||||
}));
|
||||
EXPECT_CALL(this->evse_security,
|
||||
generate_certificate_signing_request(ocpp::CertificateSigningUseEnum::ChargingStationCertificate,
|
||||
"testCountry", "testOrganization", "testserialnumber", false))
|
||||
.WillRepeatedly(Return(sign_request_result));
|
||||
|
||||
// Initial sign_certificate_req + Accepted response
|
||||
security.sign_certificate_req(ocpp::CertificateSigningUseEnum::ChargingStationCertificate, false);
|
||||
security.handle_message(create_example_sign_certificate_response(GenericStatusEnum::Accepted));
|
||||
|
||||
// First timeout should be 10s (max(10, 0) * 2^0 = 10s floor)
|
||||
EXPECT_EQ(timer_stub_get_timeout_interval_ms(), 10000);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,589 @@
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
// Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
#include <gmock/gmock.h>
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <boost/asio/io_context.hpp>
|
||||
|
||||
#include <ocpp/v2/ctrlr_component_variables.hpp>
|
||||
#include <ocpp/v2/device_model.hpp>
|
||||
#include <ocpp/v2/functional_blocks/functional_block_context.hpp>
|
||||
#include <ocpp/v2/functional_blocks/tariff_and_cost.hpp>
|
||||
#include <ocpp/v2/messages/CostUpdated.hpp>
|
||||
|
||||
#include "component_state_manager_mock.hpp"
|
||||
#include "connectivity_manager_mock.hpp"
|
||||
#include "device_model_test_helper.hpp"
|
||||
#include "evse_manager_fake.hpp"
|
||||
#include "evse_security_mock.hpp"
|
||||
#include "message_dispatcher_mock.hpp"
|
||||
#include "meter_values_mock.hpp"
|
||||
#include "mocks/database_handler_mock.hpp"
|
||||
|
||||
using namespace ocpp::v2;
|
||||
using ocpp::IdentifierType;
|
||||
using ocpp::RunningCost;
|
||||
using ocpp::RunningCostState;
|
||||
using ocpp::TariffMessage;
|
||||
using ::testing::_;
|
||||
using ::testing::Invoke;
|
||||
using ::testing::MockFunction;
|
||||
using ::testing::NiceMock;
|
||||
using ::testing::Return;
|
||||
|
||||
static const std::string TARIFF_FALLBACK_MESSAGE = "Tariff: 0.30 EUR/kWh";
|
||||
static const std::string OFFLINE_TARIFF_FALLBACK_MESSAGE = "Offline: 0.35 EUR/kWh";
|
||||
static const std::string TOTAL_COST_FALLBACK_MESSAGE = "Total cost unavailable (offline)";
|
||||
static const std::string TRANSACTION_ID = "txn-001";
|
||||
|
||||
class TariffAndCostTest : public ::testing::Test {
|
||||
protected:
|
||||
DeviceModelTestHelper device_model_test_helper;
|
||||
MockMessageDispatcher mock_dispatcher;
|
||||
DeviceModel* device_model;
|
||||
NiceMock<ConnectivityManagerMock> connectivity_manager;
|
||||
NiceMock<DatabaseHandlerMock> database_handler_mock;
|
||||
ocpp::EvseSecurityMock evse_security;
|
||||
EvseManagerFake evse_manager;
|
||||
ComponentStateManagerMock component_state_manager;
|
||||
std::atomic<ocpp::OcppProtocolVersion> ocpp_version;
|
||||
FunctionalBlockContext functional_block_context;
|
||||
NiceMock<MeterValuesMock> meter_values_mock;
|
||||
boost::asio::io_context io_context;
|
||||
|
||||
std::optional<TariffMessageCallback> tariff_message_callback_opt;
|
||||
std::optional<SetRunningCostCallback> set_running_cost_callback_opt;
|
||||
std::optional<DefaultPriceCallback> default_price_callback_opt;
|
||||
|
||||
MockFunction<void(const TariffMessage&)> tariff_message_mock;
|
||||
MockFunction<void(const RunningCost&, std::uint32_t, std::optional<std::string>)> running_cost_mock;
|
||||
|
||||
TariffAndCostTest() :
|
||||
device_model(device_model_test_helper.get_device_model()),
|
||||
evse_manager(1),
|
||||
ocpp_version(ocpp::OcppProtocolVersion::v201),
|
||||
functional_block_context{mock_dispatcher, *device_model, connectivity_manager, evse_manager,
|
||||
database_handler_mock, evse_security, component_state_manager, ocpp_version} {
|
||||
}
|
||||
|
||||
std::unique_ptr<TariffAndCost> make_tariff_and_cost() {
|
||||
return std::make_unique<TariffAndCost>(functional_block_context, meter_values_mock, tariff_message_callback_opt,
|
||||
set_running_cost_callback_opt, default_price_callback_opt, io_context);
|
||||
}
|
||||
|
||||
void set_tariff_enabled(bool available = true, bool enabled = true) {
|
||||
const auto& avail = ControllerComponentVariables::TariffCostCtrlrAvailableTariff;
|
||||
const auto& en = ControllerComponentVariables::TariffCostCtrlrEnabledTariff;
|
||||
ASSERT_EQ(device_model->set_value(avail.component, avail.variable.value(), AttributeEnum::Actual,
|
||||
available ? "true" : "false", "default", true),
|
||||
SetVariableStatusEnum::Accepted);
|
||||
ASSERT_EQ(device_model->set_value(en.component, en.variable.value(), AttributeEnum::Actual,
|
||||
enabled ? "true" : "false", "default", true),
|
||||
SetVariableStatusEnum::Accepted);
|
||||
}
|
||||
|
||||
void set_cost_enabled(bool available = true, bool enabled = true) {
|
||||
const auto& avail = ControllerComponentVariables::TariffCostCtrlrAvailableCost;
|
||||
const auto& en = ControllerComponentVariables::TariffCostCtrlrEnabledCost;
|
||||
ASSERT_EQ(device_model->set_value(avail.component, avail.variable.value(), AttributeEnum::Actual,
|
||||
available ? "true" : "false", "default", true),
|
||||
SetVariableStatusEnum::Accepted);
|
||||
ASSERT_EQ(device_model->set_value(en.component, en.variable.value(), AttributeEnum::Actual,
|
||||
enabled ? "true" : "false", "default", true),
|
||||
SetVariableStatusEnum::Accepted);
|
||||
}
|
||||
|
||||
void set_tariff_fallback_message(const std::string& msg) {
|
||||
Variable var;
|
||||
var.name = "TariffFallbackMessage";
|
||||
ASSERT_EQ(device_model->set_value(ControllerComponents::TariffCostCtrlr, var, AttributeEnum::Actual, msg,
|
||||
"default", true),
|
||||
SetVariableStatusEnum::Accepted);
|
||||
}
|
||||
|
||||
void set_total_cost_fallback_message(const std::string& msg) {
|
||||
Variable var;
|
||||
var.name = "TotalCostFallbackMessage";
|
||||
ASSERT_EQ(device_model->set_value(ControllerComponents::TariffCostCtrlr, var, AttributeEnum::Actual, msg,
|
||||
"default", true),
|
||||
SetVariableStatusEnum::Accepted);
|
||||
}
|
||||
|
||||
void set_offline_tariff_fallback_message(const std::string& msg) {
|
||||
Variable var;
|
||||
var.name = "OfflineTariffFallbackMessage";
|
||||
ASSERT_EQ(device_model->set_value(ControllerComponents::TariffCostCtrlr, var, AttributeEnum::Actual, msg,
|
||||
"default", true),
|
||||
SetVariableStatusEnum::Accepted);
|
||||
}
|
||||
|
||||
void set_tariff_fallback_message_instance(const std::string& instance, const std::string& msg) {
|
||||
Variable var;
|
||||
var.name = "TariffFallbackMessage";
|
||||
var.instance = instance;
|
||||
ASSERT_EQ(device_model->set_value(ControllerComponents::TariffCostCtrlr, var, AttributeEnum::Actual, msg,
|
||||
"default", true),
|
||||
SetVariableStatusEnum::Accepted);
|
||||
}
|
||||
|
||||
// Helpers for building enhanced messages
|
||||
static ocpp::EnhancedMessage<MessageType> make_costupdated_message(const CostUpdatedRequest& req) {
|
||||
ocpp::Call<CostUpdatedRequest> call(req);
|
||||
ocpp::EnhancedMessage<MessageType> msg;
|
||||
msg.messageType = MessageType::CostUpdated;
|
||||
msg.message = call;
|
||||
return msg;
|
||||
}
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// handle_message
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_F(TariffAndCostTest, HandleMessage_WrongType_Throws) {
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
CostUpdatedRequest req;
|
||||
req.totalCost = 1.0f;
|
||||
req.transactionId = TRANSACTION_ID;
|
||||
ocpp::Call<CostUpdatedRequest> call(req);
|
||||
ocpp::EnhancedMessage<MessageType> msg;
|
||||
msg.messageType = MessageType::Authorize; // wrong type
|
||||
msg.message = call;
|
||||
|
||||
EXPECT_THROW(tc->handle_message(msg), MessageTypeNotImplementedException);
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, HandleMessage_CostUpdated_CostDisabled_DispatchesResult) {
|
||||
// Cost is disabled: handler should dispatch a CostUpdatedResponse but not call the callback.
|
||||
set_running_cost_callback_opt = running_cost_mock.AsStdFunction();
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
EXPECT_CALL(running_cost_mock, Call(_, _, _)).Times(0);
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).Times(1);
|
||||
|
||||
CostUpdatedRequest req;
|
||||
req.totalCost = 5.0f;
|
||||
req.transactionId = TRANSACTION_ID;
|
||||
tc->handle_message(make_costupdated_message(req));
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, HandleMessage_CostUpdated_CostEnabled_CallsRunningCostCallback) {
|
||||
set_cost_enabled();
|
||||
set_running_cost_callback_opt = running_cost_mock.AsStdFunction();
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
EXPECT_CALL(running_cost_mock, Call(_, _, _)).Times(1);
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).Times(1);
|
||||
|
||||
CostUpdatedRequest req;
|
||||
req.totalCost = 5.0f;
|
||||
req.transactionId = TRANSACTION_ID;
|
||||
tc->handle_message(make_costupdated_message(req));
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, HandleMessage_CostUpdated_NoRunningCostCallback_DispatchesResult) {
|
||||
set_cost_enabled();
|
||||
// No running cost callback set.
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
EXPECT_CALL(mock_dispatcher, dispatch_call_result(_)).Times(1);
|
||||
|
||||
CostUpdatedRequest req;
|
||||
req.totalCost = 5.0f;
|
||||
req.transactionId = TRANSACTION_ID;
|
||||
tc->handle_message(make_costupdated_message(req));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// send_total_cost_fallback_message
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_F(TariffAndCostTest, SendTotalCostFallbackMessage_CostDisabled_NoCallback) {
|
||||
tariff_message_callback_opt = tariff_message_mock.AsStdFunction();
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
EXPECT_CALL(tariff_message_mock, Call(_)).Times(0);
|
||||
tc->send_total_cost_fallback_message(TRANSACTION_ID);
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, SendTotalCostFallbackMessage_CostEnabled_NoCallback_DoesNotCrash) {
|
||||
set_cost_enabled();
|
||||
// tariff_message_callback_opt remains nullopt
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
EXPECT_NO_FATAL_FAILURE(tc->send_total_cost_fallback_message(TRANSACTION_ID));
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, SendTotalCostFallbackMessage_CostEnabled_NoMessageConfigured_NoCallback) {
|
||||
set_cost_enabled();
|
||||
tariff_message_callback_opt = tariff_message_mock.AsStdFunction();
|
||||
// TotalCostFallbackMessage is empty by default.
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
EXPECT_CALL(tariff_message_mock, Call(_)).Times(0);
|
||||
tc->send_total_cost_fallback_message(TRANSACTION_ID);
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, SendTotalCostFallbackMessage_CostEnabled_MessageConfigured_CallsCallback) {
|
||||
set_cost_enabled();
|
||||
set_total_cost_fallback_message(TOTAL_COST_FALLBACK_MESSAGE);
|
||||
tariff_message_callback_opt = tariff_message_mock.AsStdFunction();
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
EXPECT_CALL(tariff_message_mock, Call(_)).WillOnce(Invoke([](const TariffMessage& msg) {
|
||||
ASSERT_EQ(msg.message.size(), 1u);
|
||||
EXPECT_EQ(msg.message[0].message, TOTAL_COST_FALLBACK_MESSAGE);
|
||||
}));
|
||||
tc->send_total_cost_fallback_message(TRANSACTION_ID);
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, SendTotalCostFallbackMessage_SetsTransactionId) {
|
||||
set_cost_enabled();
|
||||
set_total_cost_fallback_message(TOTAL_COST_FALLBACK_MESSAGE);
|
||||
tariff_message_callback_opt = tariff_message_mock.AsStdFunction();
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
EXPECT_CALL(tariff_message_mock, Call(_)).WillOnce(Invoke([](const TariffMessage& msg) {
|
||||
EXPECT_EQ(msg.identifier_id, TRANSACTION_ID);
|
||||
EXPECT_EQ(msg.identifier_type, IdentifierType::TransactionId);
|
||||
}));
|
||||
tc->send_total_cost_fallback_message(TRANSACTION_ID);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// ensure_personal_message
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
TEST_F(TariffAndCostTest, EnsureTariffMessage_TariffDisabled_NoOp) {
|
||||
set_tariff_fallback_message(TARIFF_FALLBACK_MESSAGE);
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
IdTokenInfo info;
|
||||
info.status = AuthorizationStatusEnum::Accepted;
|
||||
tc->ensure_personal_message(info, false);
|
||||
|
||||
EXPECT_FALSE(info.personalMessage.has_value());
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, EnsureTariffMessage_PersonalMessageAlreadySet_NoOp) {
|
||||
set_tariff_enabled();
|
||||
set_tariff_fallback_message(TARIFF_FALLBACK_MESSAGE);
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
IdTokenInfo info;
|
||||
info.status = AuthorizationStatusEnum::Accepted;
|
||||
MessageContent existing;
|
||||
existing.content = "CSMS provided message";
|
||||
existing.format = MessageFormatEnum::UTF8;
|
||||
info.personalMessage = existing;
|
||||
|
||||
tc->ensure_personal_message(info, false);
|
||||
|
||||
EXPECT_EQ(info.personalMessage.value().content, "CSMS provided message");
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, EnsureTariffMessage_NoFallbackConfigured_NoOp) {
|
||||
set_tariff_enabled();
|
||||
// TariffFallbackMessage is empty by default.
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
IdTokenInfo info;
|
||||
info.status = AuthorizationStatusEnum::Accepted;
|
||||
tc->ensure_personal_message(info, false);
|
||||
|
||||
EXPECT_FALSE(info.personalMessage.has_value());
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, EnsureTariffMessage_FallbackConfigured_SetsPersonalMessage) {
|
||||
set_tariff_enabled();
|
||||
set_tariff_fallback_message(TARIFF_FALLBACK_MESSAGE);
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
IdTokenInfo info;
|
||||
info.status = AuthorizationStatusEnum::Accepted;
|
||||
tc->ensure_personal_message(info, false);
|
||||
|
||||
ASSERT_TRUE(info.personalMessage.has_value());
|
||||
EXPECT_EQ(std::string(info.personalMessage->content), TARIFF_FALLBACK_MESSAGE);
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, EnsureTariffMessage_Offline_UsesFallback) {
|
||||
set_tariff_enabled();
|
||||
set_tariff_fallback_message(TARIFF_FALLBACK_MESSAGE);
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
IdTokenInfo info;
|
||||
info.status = AuthorizationStatusEnum::Accepted;
|
||||
tc->ensure_personal_message(info, true); // offline=true, no offline-specific message configured
|
||||
|
||||
// Should fall back to TariffFallbackMessage
|
||||
ASSERT_TRUE(info.personalMessage.has_value());
|
||||
EXPECT_EQ(std::string(info.personalMessage->content), TARIFF_FALLBACK_MESSAGE);
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, EnsureTariffMessage_Offline_OfflineMessageConfigured_UsesOfflineMessage) {
|
||||
set_tariff_enabled();
|
||||
set_tariff_fallback_message(TARIFF_FALLBACK_MESSAGE);
|
||||
set_offline_tariff_fallback_message(OFFLINE_TARIFF_FALLBACK_MESSAGE);
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
IdTokenInfo info;
|
||||
info.status = AuthorizationStatusEnum::Accepted;
|
||||
tc->ensure_personal_message(info, true);
|
||||
|
||||
ASSERT_TRUE(info.personalMessage.has_value());
|
||||
EXPECT_EQ(std::string(info.personalMessage->content), OFFLINE_TARIFF_FALLBACK_MESSAGE);
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, EnsureTariffMessage_DefaultLanguageEntryBecomesPersonalMessage) {
|
||||
set_tariff_enabled();
|
||||
// Set the base (no-instance) fallback message.
|
||||
set_tariff_fallback_message(TARIFF_FALLBACK_MESSAGE);
|
||||
// Also set the language-specific "en-US" instance that is present in the example config.
|
||||
Variable en_var;
|
||||
en_var.name = "TariffFallbackMessage";
|
||||
en_var.instance = "en-US";
|
||||
ASSERT_EQ(device_model->set_value(ControllerComponents::TariffCostCtrlr, en_var, AttributeEnum::Actual,
|
||||
"Tariff: 0.30 EUR/kWh (en-US)", "default", true),
|
||||
SetVariableStatusEnum::Accepted);
|
||||
// Set DisplayMessageCtrlr.Language to "en-US" so it matches the instance above.
|
||||
// Note: "en-US" must be in the current valuesList. The example config has "en_US,de,nl" (underscore);
|
||||
// since "en-US" ≠ "en_US" the language-specific entry won't be found via the valuesList scan.
|
||||
// This test therefore verifies the fallback: when no language match, index 0 (the base message)
|
||||
// is used for personalMessage with no extra entries.
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
IdTokenInfo info;
|
||||
info.status = AuthorizationStatusEnum::Accepted;
|
||||
tc->ensure_personal_message(info, false);
|
||||
|
||||
ASSERT_TRUE(info.personalMessage.has_value());
|
||||
// Base message (index 0) is selected since no language match for the default "en_US" entries.
|
||||
EXPECT_EQ(std::string(info.personalMessage->content), TARIFF_FALLBACK_MESSAGE);
|
||||
// No extra entries since there are no matching per-language instances found via the valuesList.
|
||||
if (info.customData.has_value()) {
|
||||
const json& cd = info.customData.value();
|
||||
if (cd.contains("personalMessageExtra")) {
|
||||
EXPECT_TRUE(cd.at("personalMessageExtra").empty());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, EnsureTariffMessage_MultipleLanguages_PopulatesPersonalMessageExtra) {
|
||||
// With both a base message and a language-specific "de" instance, the base message should
|
||||
// become personalMessage (default_language is empty → index 0 is primary), and the "de"
|
||||
// message should go into customData.personalMessageExtra per California Pricing spec 4.3.4.
|
||||
set_tariff_enabled();
|
||||
set_tariff_fallback_message(TARIFF_FALLBACK_MESSAGE);
|
||||
// "de" is in DisplayMessageCtrlr.Language.valuesList ("en_US,de,nl") so it will be found.
|
||||
set_tariff_fallback_message_instance("de", "Tarif: 0,30 EUR/kWh");
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
IdTokenInfo info;
|
||||
info.status = AuthorizationStatusEnum::Accepted;
|
||||
tc->ensure_personal_message(info, false);
|
||||
|
||||
// Base message is primary (index 0 — no default language configured).
|
||||
ASSERT_TRUE(info.personalMessage.has_value());
|
||||
EXPECT_EQ(std::string(info.personalMessage->content), TARIFF_FALLBACK_MESSAGE);
|
||||
|
||||
// The "de" entry must appear in customData.personalMessageExtra.
|
||||
ASSERT_TRUE(info.customData.has_value());
|
||||
const json& cd = info.customData.value();
|
||||
EXPECT_EQ(cd.at("vendorId"), "org.openchargealliance.multilanguage");
|
||||
ASSERT_TRUE(cd.contains("personalMessageExtra"));
|
||||
ASSERT_EQ(cd.at("personalMessageExtra").size(), 1u);
|
||||
EXPECT_EQ(cd.at("personalMessageExtra").at(0).at("content"), "Tarif: 0,30 EUR/kWh");
|
||||
EXPECT_EQ(cd.at("personalMessageExtra").at(0).at("language"), "de");
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, EnsureTariffMessage_MaxFourExtraLanguages) {
|
||||
// personalMessageExtra is capped at 4 entries (spec 4.3.4).
|
||||
// We use the base message + "de" (only "de" and "nl" are in valuesList besides "en_US"),
|
||||
// so in practice the cap is not hit here — this test verifies no crash with max entries.
|
||||
set_tariff_enabled();
|
||||
set_tariff_fallback_message(TARIFF_FALLBACK_MESSAGE);
|
||||
set_tariff_fallback_message_instance("de", "Tarif: 0,30 EUR/kWh");
|
||||
set_tariff_fallback_message_instance("nl", "Tarief: 0,30 EUR/kWh");
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
IdTokenInfo info;
|
||||
info.status = AuthorizationStatusEnum::Accepted;
|
||||
tc->ensure_personal_message(info, false);
|
||||
|
||||
ASSERT_TRUE(info.personalMessage.has_value());
|
||||
EXPECT_EQ(std::string(info.personalMessage->content), TARIFF_FALLBACK_MESSAGE);
|
||||
|
||||
ASSERT_TRUE(info.customData.has_value());
|
||||
const json& cd = info.customData.value();
|
||||
EXPECT_EQ(cd.at("vendorId"), "org.openchargealliance.multilanguage");
|
||||
// Two extra languages — both must be present.
|
||||
ASSERT_EQ(cd.at("personalMessageExtra").size(), 2u);
|
||||
// Extra entries must not exceed the max of 4.
|
||||
EXPECT_LE(cd.at("personalMessageExtra").size(), 4u);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// handle_cost_and_tariff
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
static TransactionEventRequest make_transaction_event_request(const std::string& transaction_id,
|
||||
TransactionEventEnum event_type) {
|
||||
TransactionEventRequest req;
|
||||
req.eventType = event_type;
|
||||
req.timestamp = ocpp::DateTime();
|
||||
req.triggerReason = TriggerReasonEnum::Authorized;
|
||||
req.seqNo = 0;
|
||||
req.transactionInfo.transactionId = transaction_id;
|
||||
return req;
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, HandleCostAndTariff_NeitherEnabled_NoCallbacks) {
|
||||
tariff_message_callback_opt = tariff_message_mock.AsStdFunction();
|
||||
set_running_cost_callback_opt = running_cost_mock.AsStdFunction();
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
EXPECT_CALL(tariff_message_mock, Call(_)).Times(0);
|
||||
EXPECT_CALL(running_cost_mock, Call(_, _, _)).Times(0);
|
||||
|
||||
TransactionEventResponse response;
|
||||
MessageContent personal_msg;
|
||||
personal_msg.content = "Some tariff info";
|
||||
personal_msg.format = MessageFormatEnum::UTF8;
|
||||
response.updatedPersonalMessage = personal_msg;
|
||||
response.totalCost = 5.0f;
|
||||
|
||||
const auto req = make_transaction_event_request(TRANSACTION_ID, TransactionEventEnum::Ended);
|
||||
tc->handle_cost_and_tariff(response, req, json{{"totalCost", 5.0}});
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, HandleCostAndTariff_TariffEnabled_WithUpdatedPersonalMessage_CallsTariffCallback) {
|
||||
set_tariff_enabled();
|
||||
tariff_message_callback_opt = tariff_message_mock.AsStdFunction();
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
EXPECT_CALL(tariff_message_mock, Call(_)).WillOnce(Invoke([](const TariffMessage& msg) {
|
||||
ASSERT_EQ(msg.message.size(), 1u);
|
||||
EXPECT_EQ(msg.message[0].message, "Tariff: 0.25 EUR/kWh");
|
||||
EXPECT_EQ(msg.identifier_type, IdentifierType::TransactionId);
|
||||
}));
|
||||
|
||||
TransactionEventResponse response;
|
||||
MessageContent personal_msg;
|
||||
personal_msg.content = "Tariff: 0.25 EUR/kWh";
|
||||
personal_msg.format = MessageFormatEnum::UTF8;
|
||||
response.updatedPersonalMessage = personal_msg;
|
||||
|
||||
const auto req = make_transaction_event_request(TRANSACTION_ID, TransactionEventEnum::Updated);
|
||||
tc->handle_cost_and_tariff(response, req, json{});
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, HandleCostAndTariff_TariffEnabled_NoUpdatedPersonalMessage_NoTariffCallback) {
|
||||
set_tariff_enabled();
|
||||
tariff_message_callback_opt = tariff_message_mock.AsStdFunction();
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
EXPECT_CALL(tariff_message_mock, Call(_)).Times(0);
|
||||
|
||||
TransactionEventResponse response;
|
||||
// No updatedPersonalMessage set.
|
||||
|
||||
const auto req = make_transaction_event_request(TRANSACTION_ID, TransactionEventEnum::Updated);
|
||||
tc->handle_cost_and_tariff(response, req, json{});
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, HandleCostAndTariff_CostEnabled_WithTotalCost_CallsRunningCostCallback) {
|
||||
set_cost_enabled();
|
||||
set_running_cost_callback_opt = running_cost_mock.AsStdFunction();
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
EXPECT_CALL(running_cost_mock, Call(_, _, _))
|
||||
.WillOnce(Invoke([](const RunningCost& cost, std::uint32_t decimals, std::optional<std::string> currency) {
|
||||
EXPECT_DOUBLE_EQ(cost.cost, 5.0);
|
||||
EXPECT_EQ(cost.state, RunningCostState::Finished);
|
||||
}));
|
||||
|
||||
TransactionEventResponse response;
|
||||
response.totalCost = 5.0f;
|
||||
|
||||
const auto req = make_transaction_event_request(TRANSACTION_ID, TransactionEventEnum::Ended);
|
||||
tc->handle_cost_and_tariff(response, req, json{{"totalCost", 5.0}});
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, HandleCostAndTariff_CostEnabled_NoTotalCost_NoRunningCostCallback) {
|
||||
set_cost_enabled();
|
||||
set_running_cost_callback_opt = running_cost_mock.AsStdFunction();
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
EXPECT_CALL(running_cost_mock, Call(_, _, _)).Times(0);
|
||||
|
||||
TransactionEventResponse response;
|
||||
// No totalCost.
|
||||
|
||||
const auto req = make_transaction_event_request(TRANSACTION_ID, TransactionEventEnum::Ended);
|
||||
tc->handle_cost_and_tariff(response, req, json{});
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, HandleCostAndTariff_BothEnabled_CallsBothCallbacks) {
|
||||
set_tariff_enabled();
|
||||
set_cost_enabled();
|
||||
tariff_message_callback_opt = tariff_message_mock.AsStdFunction();
|
||||
set_running_cost_callback_opt = running_cost_mock.AsStdFunction();
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
EXPECT_CALL(tariff_message_mock, Call(_)).Times(1);
|
||||
EXPECT_CALL(running_cost_mock, Call(_, _, _)).Times(1);
|
||||
|
||||
TransactionEventResponse response;
|
||||
MessageContent personal_msg;
|
||||
personal_msg.content = "Tariff info";
|
||||
personal_msg.format = MessageFormatEnum::UTF8;
|
||||
response.updatedPersonalMessage = personal_msg;
|
||||
response.totalCost = 3.5f;
|
||||
|
||||
const auto req = make_transaction_event_request(TRANSACTION_ID, TransactionEventEnum::Ended);
|
||||
tc->handle_cost_and_tariff(response, req, json{{"totalCost", 3.5}});
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, HandleCostAndTariff_CostEnabled_RunningCostIncludesTariffMessages) {
|
||||
set_tariff_enabled();
|
||||
set_cost_enabled();
|
||||
set_running_cost_callback_opt = running_cost_mock.AsStdFunction();
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
EXPECT_CALL(running_cost_mock, Call(_, _, _))
|
||||
.WillOnce(Invoke([](const RunningCost& cost, std::uint32_t, std::optional<std::string>) {
|
||||
ASSERT_TRUE(cost.cost_messages.has_value());
|
||||
ASSERT_EQ(cost.cost_messages->size(), 1u);
|
||||
EXPECT_EQ(cost.cost_messages->at(0).message, "Tariff info");
|
||||
}));
|
||||
|
||||
TransactionEventResponse response;
|
||||
MessageContent personal_msg;
|
||||
personal_msg.content = "Tariff info";
|
||||
personal_msg.format = MessageFormatEnum::UTF8;
|
||||
response.updatedPersonalMessage = personal_msg;
|
||||
response.totalCost = 3.5f;
|
||||
|
||||
const auto req = make_transaction_event_request(TRANSACTION_ID, TransactionEventEnum::Ended);
|
||||
tc->handle_cost_and_tariff(response, req, json{{"totalCost", 3.5}});
|
||||
}
|
||||
|
||||
TEST_F(TariffAndCostTest, HandleCostAndTariff_ChargingState_RunningCostIsCharging) {
|
||||
set_cost_enabled();
|
||||
set_running_cost_callback_opt = running_cost_mock.AsStdFunction();
|
||||
auto tc = make_tariff_and_cost();
|
||||
|
||||
EXPECT_CALL(running_cost_mock, Call(_, _, _))
|
||||
.WillOnce(Invoke([](const RunningCost& cost, std::uint32_t, std::optional<std::string>) {
|
||||
EXPECT_EQ(cost.state, RunningCostState::Charging);
|
||||
}));
|
||||
|
||||
TransactionEventResponse response;
|
||||
response.totalCost = 1.0f;
|
||||
|
||||
const auto req = make_transaction_event_request(TRANSACTION_ID, TransactionEventEnum::Updated);
|
||||
tc->handle_cost_and_tariff(response, req, json{{"totalCost", 1.0}});
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
{
|
||||
"id": 1,
|
||||
"chargingProfileKind": "Absolute",
|
||||
"chargingProfilePurpose": "ChargingStationMaxProfile",
|
||||
"chargingSchedule": [
|
||||
{
|
||||
"chargingRateUnit": "A",
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 10.000000,
|
||||
"numberPhases": 3,
|
||||
"startPeriod": 0
|
||||
}
|
||||
],
|
||||
"duration": 86404,
|
||||
"id": 1,
|
||||
"startSchedule": "2024-08-21T12:24:36Z"
|
||||
}
|
||||
],
|
||||
"stackLevel": 0
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"id": 2,
|
||||
"chargingProfileKind": "Absolute",
|
||||
"chargingProfilePurpose": "TxDefaultProfile",
|
||||
"chargingSchedule": [
|
||||
{
|
||||
"chargingRateUnit": "A",
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 6.000000,
|
||||
"numberPhases": 3,
|
||||
"startPeriod": 0
|
||||
},
|
||||
{
|
||||
"limit": 10.000000,
|
||||
"numberPhases": 3,
|
||||
"startPeriod": 60
|
||||
},
|
||||
{
|
||||
"limit": 8.000000,
|
||||
"numberPhases": 3,
|
||||
"startPeriod": 120
|
||||
},
|
||||
{
|
||||
"limit": 15.000000,
|
||||
"numberPhases": 3,
|
||||
"startPeriod": 180
|
||||
},
|
||||
{
|
||||
"limit": 8.000000,
|
||||
"numberPhases": 3,
|
||||
"startPeriod": 260
|
||||
}
|
||||
],
|
||||
"duration": 304,
|
||||
"id": 1,
|
||||
"startSchedule": "2024-08-21T12:24:36Z"
|
||||
}
|
||||
],
|
||||
"stackLevel": 0,
|
||||
"validFrom": "2024-08-21T12:24:36Z",
|
||||
"validTo": "2024-08-21T12:31:25Z"
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"chargingProfileKind": "Absolute",
|
||||
"chargingProfilePurpose": "TxProfile",
|
||||
"chargingSchedule": [
|
||||
{
|
||||
"chargingRateUnit": "A",
|
||||
"chargingSchedulePeriod": [
|
||||
{
|
||||
"limit": 8.000000,
|
||||
"numberPhases": 3,
|
||||
"startPeriod": 0
|
||||
},
|
||||
{
|
||||
"limit": 11.000000,
|
||||
"numberPhases": 3,
|
||||
"startPeriod": 50
|
||||
},
|
||||
{
|
||||
"limit": 16.000000,
|
||||
"numberPhases": 3,
|
||||
"startPeriod": 140
|
||||
},
|
||||
{
|
||||
"limit": 6.000000,
|
||||
"numberPhases": 3,
|
||||
"startPeriod": 200
|
||||
},
|
||||
{
|
||||
"limit": 12.000000,
|
||||
"numberPhases": 3,
|
||||
"startPeriod": 240
|
||||
}
|
||||
],
|
||||
"duration": 264,
|
||||
"id": 1,
|
||||
"startSchedule": "2024-08-21T12:24:36Z"
|
||||
}
|
||||
],
|
||||
"id": 2,
|
||||
"stackLevel": 0,
|
||||
"transactionId": "f1522902-1170-416f-8e43-9e3bce28fde7"
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user