Add extracted tools: CitrineOS, OpenOCPP, ShapeShifter

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

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

View File

@@ -0,0 +1,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/*"
)

View File

@@ -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
}
}

View File

@@ -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": []
}

View File

@@ -0,0 +1,2 @@
[pytest]
pythonpath=../../../config/v2

View File

@@ -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"
]
}

View File

@@ -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"
]
}

View File

@@ -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"
]
}

View File

@@ -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"
]
}

View File

@@ -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"
]
}

View File

@@ -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"
]
}

View File

@@ -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"
]
}

View File

@@ -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"
]
}

View File

@@ -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"
]
}

View File

@@ -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"
]
}

View File

@@ -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"
]
}

View File

@@ -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"
]
}

View File

@@ -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"
]
}

View File

@@ -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"
]
}

View File

@@ -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"
]
}

View File

@@ -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"
]
}

View File

@@ -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"
]
}

View File

@@ -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})

View File

@@ -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;");
}
};

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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);
}

View File

@@ -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());
}
};

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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})

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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"
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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"
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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");
}

View File

@@ -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

View File

@@ -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

View File

@@ -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);
}

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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)

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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}
)

View File

@@ -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);
}

View File

@@ -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());
}

View File

@@ -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);
}

View File

@@ -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([&timestamp_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([&timestamp](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);
}

View File

@@ -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}});
}

View File

@@ -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
}

View File

@@ -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"
}

View File

@@ -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