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

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,412 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
import pytest
from everest.testing.core_utils.controller.test_controller_interface import (
TestController,
)
from ocpp.v201.enums import OperationalStatusEnumType, ChangeAvailabilityStatusEnumType
from ocpp.v201.datatypes import EVSEType
from ocpp.v201 import call_result as call_result
# fmt: off
from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility
from everest_test_utils import *
from everest.testing.ocpp_utils.fixtures import *
from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201
# fmt: on
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
@pytest.mark.everest_core_config(
get_everest_config_path_str("everest-config-ocpp201.yaml")
)
async def test_g03(
central_system_v201: CentralSystem,
charge_point_v201: ChargePoint201,
test_controller: TestController,
test_utility: TestUtility,
):
evse_1 = EVSEType(id=1)
evse_1_1 = EVSEType(id=1, connector_id=1)
evse_2 = EVSEType(id=2)
evse_2_1 = EVSEType(id=2, connector_id=1)
r: call_result.ChangeAvailability = (
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.inoperative, evse=evse_1_1
)
)
assert r.status == ChangeAvailabilityStatusEnumType.accepted
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"evseId": 1, "connectorId": 1, "connectorStatus": "Unavailable"},
)
r: call_result.ChangeAvailability = (
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.inoperative, evse=evse_2_1
)
)
assert r.status == ChangeAvailabilityStatusEnumType.accepted
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"evseId": 2, "connectorId": 1, "connectorStatus": "Unavailable"},
)
test_utility.forbidden_actions.append("StatusNotification")
test_utility.messages.clear()
r: call_result.ChangeAvailability = (
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.inoperative, evse=evse_1
)
)
assert r.status == ChangeAvailabilityStatusEnumType.accepted
test_utility.messages.clear()
r: call_result.ChangeAvailability = (
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.inoperative, evse=evse_2
)
)
assert r.status == ChangeAvailabilityStatusEnumType.accepted
test_utility.messages.clear()
r: call_result.ChangeAvailability = (
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.operative, evse=evse_1
)
)
assert r.status == ChangeAvailabilityStatusEnumType.accepted
test_utility.messages.clear()
r: call_result.ChangeAvailability = (
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.operative, evse=evse_2
)
)
assert r.status == ChangeAvailabilityStatusEnumType.accepted
test_utility.messages.clear()
test_utility.forbidden_actions.clear()
r: call_result.ChangeAvailability = (
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.operative, evse=evse_1_1
)
)
assert r.status == ChangeAvailabilityStatusEnumType.accepted
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"evseId": 1, "connectorId": 1, "connectorStatus": "Available"},
)
test_utility.messages.clear()
test_controller.stop()
await asyncio.sleep(1)
test_controller.start()
charge_point_v201 = await central_system_v201.wait_for_chargepoint(
wait_for_bootnotification=False
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"evseId": 1, "connectorId": 1, "connectorStatus": "Available"},
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"evseId": 2, "connectorId": 1, "connectorStatus": "Unavailable"},
)
test_utility.messages.clear()
r: call_result.ChangeAvailability = (
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.operative, evse=evse_2_1
)
)
assert r.status == ChangeAvailabilityStatusEnumType.accepted
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"evseId": 2, "connectorId": 1, "connectorStatus": "Available"},
)
test_controller.swipe("001", connectors=[1])
test_controller.plug_in()
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Started", "evse": {"id": 1}},
)
r: call_result.ChangeAvailability = (
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.inoperative, evse=evse_1_1
)
)
assert r.status == ChangeAvailabilityStatusEnumType.scheduled
test_utility.messages.clear()
r: call_result.ChangeAvailability = (
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.inoperative, evse=evse_2_1
)
)
assert r.status == ChangeAvailabilityStatusEnumType.accepted
test_utility.messages.clear()
r: call_result.ChangeAvailability = (
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.inoperative, evse=evse_1
)
)
assert r.status == ChangeAvailabilityStatusEnumType.scheduled
test_utility.messages.clear()
r: call_result.ChangeAvailability = (
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.inoperative, evse=evse_2
)
)
assert r.status == ChangeAvailabilityStatusEnumType.accepted
test_utility.messages.clear()
test_controller.plug_out()
assert await wait_for_and_validate(
test_utility, charge_point_v201, "TransactionEvent", {
"eventType": "Ended"}
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"evseId": 1, "connectorId": 1, "connectorStatus": "Unavailable"},
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"evseId": 2, "connectorId": 1, "connectorStatus": "Unavailable"},
)
test_utility.messages.clear()
# try state that EVSE is already in
r: call_result.ChangeAvailability = (
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.inoperative, evse=evse_1_1
)
)
assert r.status == ChangeAvailabilityStatusEnumType.accepted
test_utility.messages.clear()
r: call_result.ChangeAvailability = (
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.operative, evse=evse_1
)
)
assert r.status == ChangeAvailabilityStatusEnumType.accepted
r: call_result.ChangeAvailability = (
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.operative, evse=evse_1_1
)
)
assert r.status == ChangeAvailabilityStatusEnumType.accepted
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"evseId": 1, "connectorId": 1, "connectorStatus": "Available"},
)
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
@pytest.mark.everest_core_config(
get_everest_config_path_str("everest-config-ocpp201.yaml")
)
async def test_g04(
central_system_v201: CentralSystem,
charge_point_v201: ChargePoint201,
test_controller: TestController,
test_utility: TestUtility,
):
r: call_result.ChangeAvailability = (
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.operative
)
)
assert r.status == ChangeAvailabilityStatusEnumType.accepted
test_utility.messages.clear()
test_controller.swipe("001", connectors=[1])
test_controller.plug_in()
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Started", "evse": {"id": 1}},
)
r: call_result.ChangeAvailability = (
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.operative
)
)
assert r.status == ChangeAvailabilityStatusEnumType.accepted
r: call_result.ChangeAvailability = (
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.inoperative
)
)
assert r.status == ChangeAvailabilityStatusEnumType.scheduled
test_controller.plug_out()
test_utility.messages.clear()
assert await wait_for_and_validate(
test_utility, charge_point_v201, "TransactionEvent", {
"eventType": "Ended"}
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"evseId": 1, "connectorId": 1, "connectorStatus": "Unavailable"},
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"evseId": 2, "connectorId": 1, "connectorStatus": "Unavailable"},
)
test_utility.messages.clear()
test_controller.stop()
await asyncio.sleep(1)
test_controller.start()
charge_point_v201 = await central_system_v201.wait_for_chargepoint(
wait_for_bootnotification=False
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"evseId": 1, "connectorId": 1, "connectorStatus": "Unavailable"},
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"evseId": 2, "connectorId": 1, "connectorStatus": "Unavailable"},
)
r: call_result.ChangeAvailability = (
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.operative
)
)
assert r.status == ChangeAvailabilityStatusEnumType.accepted
test_utility.messages.clear()
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"evseId": 1, "connectorId": 1, "connectorStatus": "Available"},
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"evseId": 2, "connectorId": 1, "connectorStatus": "Available"},
)
await asyncio.sleep(2)
test_utility.messages.clear()
test_controller.swipe("001", connectors=[1])
await asyncio.sleep(2)
test_controller.plug_in()
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Started", "evse": {"id": 1}},
)
r: call_result.ChangeAvailability = (
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.inoperative
)
)
assert r.status == ChangeAvailabilityStatusEnumType.scheduled
test_utility.messages.clear()
r: call_result.ChangeAvailability = (
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.operative
)
)
assert r.status == ChangeAvailabilityStatusEnumType.accepted
await asyncio.sleep(2)
test_controller.plug_out()
test_utility.messages.clear()
assert await wait_for_and_validate(
test_utility, charge_point_v201, "TransactionEvent", {
"eventType": "Ended"}
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"evseId": 1, "connectorId": 1, "connectorStatus": "Available"},
)

View File

@@ -0,0 +1,155 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
import json
import pytest
from everest.testing.core_utils.controller.test_controller_interface import (
TestController,
)
from ocpp.v201.datatypes import SetVariableDataType, VariableType, ComponentType
from ocpp.v201 import call as call201
from ocpp.messages import Call, _DecimalEncoder
from ocpp.charge_point import asdict, remove_nones, snake_to_camel_case
# fmt: off
from everest.testing.ocpp_utils.charge_point_utils import TestUtility
from everest_test_utils import get_everest_config_path_str, send_message_without_validation
from everest.testing.ocpp_utils.central_system import CentralSystem
from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201
from validations import wait_for_callerror_and_validate
# fmt: on
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
@pytest.mark.everest_core_config(
get_everest_config_path_str("everest-config-ocpp201.yaml")
)
async def test_invalid_payloads(
central_system_v201: CentralSystem,
charge_point_v201: ChargePoint201,
test_controller: TestController,
test_utility: TestUtility,
):
# a payload field that is too long should trigger a FormationViolation CALLERROR
too_long_component_type_name_payload = call201.SetVariables(
set_variable_data=[SetVariableDataType(attribute_value="abc", component=ComponentType(
name="ThisIsMuchLongerThan50charactersThisIsMuchLongerThan50charactersThisIsMuchLongerThan50charactersThisIsMuchLongerThan50charactersThisIsMuchLongerThan50characters"), variable=VariableType(name="VariableName"))]
)
camel_case_payload = snake_to_camel_case(
asdict(too_long_component_type_name_payload))
call_msg = Call(
unique_id=str(charge_point_v201._unique_id_generator()),
action=too_long_component_type_name_payload.__class__.__name__,
payload=remove_nones(camel_case_payload),
)
await send_message_without_validation(charge_point_v201, call_msg)
assert await wait_for_callerror_and_validate(
test_utility, charge_point_v201, "FormationViolation"
)
# a unknown action should trigger a FormationViolation CALLERROR
call_msg = Call(
unique_id=str(charge_point_v201._unique_id_generator()),
action="ThisIsAnUnknownAction",
payload="Invalid",
)
await send_message_without_validation(charge_point_v201, call_msg)
assert await wait_for_callerror_and_validate(
test_utility, charge_point_v201, "FormationViolation"
)
# a malformed CALL should trigger a RpcFrameworkError CALLERROR
call_msg = "{Malformed"
async with charge_point_v201._call_lock:
await charge_point_v201._send(call_msg)
assert await wait_for_callerror_and_validate(
test_utility, charge_point_v201, "RpcFrameworkError"
)
# a malformed CALL should trigger a RpcFrameworkError CALLERROR
call_msg = b"\xd8\x00\x00\x00"
async with charge_point_v201._call_lock:
await charge_point_v201._send(call_msg)
assert await wait_for_callerror_and_validate(
test_utility, charge_point_v201, "RpcFrameworkError"
)
# a invalid payload should trigger a FormationViolation CALLERROR
call_msg = Call(
unique_id=str(charge_point_v201._unique_id_generator()),
action="SetVariables",
payload=None,
)
await send_message_without_validation(charge_point_v201, call_msg)
assert await wait_for_callerror_and_validate(
test_utility, charge_point_v201, "FormationViolation"
)
# a CALL message without UUID, action and payload should trigger a RpcFrameworkError CALLERROR
json_data = json.dumps(
[
2, # CALL
],
# By default json.dumps() adds a white space after every separator.
# By setting the separator manually that can be avoided.
separators=(",", ":"),
cls=_DecimalEncoder,
)
async with charge_point_v201._call_lock:
await charge_point_v201._send(json_data)
assert await wait_for_callerror_and_validate(
test_utility, charge_point_v201, "RpcFrameworkError"
)
# a completely empty message should trigger a RpcFrameworkError CALLERROR
json_data = json.dumps(
[
],
# By default json.dumps() adds a white space after every separator.
# By setting the separator manually that can be avoided.
separators=(",", ":"),
cls=_DecimalEncoder,
)
async with charge_point_v201._call_lock:
await charge_point_v201._send(json_data)
assert await wait_for_callerror_and_validate(
test_utility, charge_point_v201, "RpcFrameworkError"
)
# a unknown RPC message should trigger a MessageTypeNotSupported CALLERROR
json_data = json.dumps(
[
99, # should be unknown
"MessageId",
],
# By default json.dumps() adds a white space after every separator.
# By setting the separator manually that can be avoided.
separators=(",", ":"),
cls=_DecimalEncoder,
)
async with charge_point_v201._call_lock:
await charge_point_v201._send(json_data)
assert await wait_for_callerror_and_validate(
test_utility, charge_point_v201, "MessageTypeNotSupported"
)

View File

@@ -0,0 +1,916 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
from datetime import timezone
from unittest.mock import Mock, ANY
import logging
from copy import deepcopy
# Needs to be before the datatypes below since it overrides the v201 Action enum with the v16 one
from everest_test_utils import *
from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate
from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201
from everest.testing.core_utils.controller.test_controller_interface import TestController
from ocpp.v201 import call as call201
from ocpp.v201 import call_result as call_result201
from ocpp.v201.enums import (IdTokenEnumType as IdTokenTypeEnum, ConnectorStatusEnumType,
ClearCacheStatusEnumType, SetVariableStatusEnumType,
AttributeEnumType)
from ocpp.v201.datatypes import *
from everest.testing.ocpp_utils.fixtures import *
from everest_test_utils_probe_modules import (probe_module,
ProbeModuleCostAndPriceMetervaluesConfigurationAdjustment,
ProbeModuleCostAndPriceSessionCostConfigurationAdjustment)
from everest.testing.core_utils._configuration.libocpp_configuration_helper import (
GenericOCPP2XConfigAdjustment,
OCPP2XConfigVariableIdentifier,
)
from validations import validate_status_notification_201
log = logging.getLogger("ocpp201CaliforniaPricingTest")
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
@pytest.mark.inject_csms_mock
@pytest.mark.everest_core_config(get_everest_config_path_str('everest-config-ocpp201-costandprice.yaml'))
@pytest.mark.ocpp_config_adaptions(GenericOCPP2XConfigAdjustment([
(OCPP2XConfigVariableIdentifier("DisplayMessageCtrlr", "DisplayMessageCtrlrAvailable", "Actual"),
"true"),
(OCPP2XConfigVariableIdentifier("DisplayMessageCtrlr", "QRCodeDisplayCapable",
"Actual"), "true"),
(OCPP2XConfigVariableIdentifier("DisplayMessageCtrlr", "DisplayMessageLanguage", "Actual"),
"en"),
(OCPP2XConfigVariableIdentifier("TariffCostCtrlr", "TariffCostCtrlrAvailableTariff", "Actual"),
"true"),
(OCPP2XConfigVariableIdentifier("TariffCostCtrlr", "TariffCostCtrlrAvailableCost", "Actual"),
"true"),
(OCPP2XConfigVariableIdentifier("TariffCostCtrlr",
"TariffCostCtrlrEnabledTariff", "Actual"), "true"),
(OCPP2XConfigVariableIdentifier("TariffCostCtrlr",
"TariffCostCtrlrEnabledCost", "Actual"), "true"),
(OCPP2XConfigVariableIdentifier("TariffCostCtrlr", "NumberOfDecimalsForCostValues", "Actual"),
"5"),
(OCPP2XConfigVariableIdentifier("OCPPCommCtrlr", "MessageTimeout", "Actual"),
"1"),
(OCPP2XConfigVariableIdentifier("OCPPCommCtrlr", "MessageAttemptInterval",
"Actual"), "1"),
(OCPP2XConfigVariableIdentifier("OCPPCommCtrlr", "MessageAttempts", "Actual"),
"3"),
(OCPP2XConfigVariableIdentifier("AuthCacheCtrlr", "AuthCacheCtrlrEnabled", "Actual"),
"true"),
(OCPP2XConfigVariableIdentifier("AuthCtrlr", "LocalPreAuthorize",
"Actual"), "true"),
(OCPP2XConfigVariableIdentifier("AuthCacheCtrlr", "AuthCacheLifeTime", "Actual"),
"86400"),
(OCPP2XConfigVariableIdentifier("CustomizationCtrlr", "CustomImplementationCaliforniaPricingEnabled",
"Actual"), "true"),
(OCPP2XConfigVariableIdentifier("CustomizationCtrlr", "CustomImplementationMultiLanguageEnabled",
"Actual"), "true")
]))
class TestOcpp201CostAndPrice:
"""
Tests for OCPP 2.0.1 California Pricing Requirements
"""
cost_updated_custom_data = {
"vendorId": "org.openchargealliance.costmsg",
"timestamp": datetime.now(timezone.utc).isoformat(), "meterValue": 1234000,
"state": "Charging",
"chargingPrice": {"kWhPrice": 0.123, "hourPrice": 2.00, "flatFee": 42.42},
"idlePrice": {"graceMinutes": 30, "hourPrice": 1.00},
"nextPeriod": {
"atTime": (datetime.now(timezone.utc) + timedelta(hours=2)).isoformat(),
"chargingPrice": {"kWhPrice": 0.100, "hourPrice": 4.00, "flatFee": 84.84},
"idlePrice": {"hourPrice": 0.50}
},
"triggerMeterValue": {
"atTime": datetime.now(timezone.utc).isoformat(),
"atEnergykWh": 5.0,
"atPowerkW": 8.0
}
}
@staticmethod
async def start_transaction(test_controller: TestController, test_utility: TestUtility,
charge_point: ChargePoint201):
# prepare data for the test
evse_id1 = 1
connector_id = 1
# make an unknown IdToken
id_token = IdTokenType(
id_token="DEADBEEF",
type=IdTokenTypeEnum.iso14443
)
assert await wait_for_and_validate(test_utility, charge_point, "StatusNotification",
call201.StatusNotification(datetime.now().isoformat(),
ConnectorStatusEnumType.available,
evse_id=evse_id1,
connector_id=connector_id),
validate_status_notification_201)
# Charging station is now available, start charging session.
# swipe id tag to authorize
test_controller.swipe(id_token.id_token)
assert await wait_for_and_validate(test_utility, charge_point, "Authorize",
call201.Authorize(id_token
))
# start charging session
test_controller.plug_in()
# should send a Transaction event
transaction_event = await wait_for_and_validate(test_utility, charge_point, "TransactionEvent",
{"eventType": "Started"})
transaction_id = transaction_event['transaction_info']['transaction_id']
assert await wait_for_and_validate(test_utility, charge_point, "TransactionEvent",
{"eventType": "Updated"})
return transaction_id
@staticmethod
async def await_mock_called(mock):
while not mock.call_count:
await asyncio.sleep(0.1)
@staticmethod
async def await_mock_called_matching(mock, predicate):
"""Wait until `mock` has been called with args satisfying `predicate`."""
while not any(predicate(call) for call in mock.call_args_list):
await asyncio.sleep(0.1)
@pytest.mark.asyncio
@pytest.mark.probe_module
@pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceSessionCostConfigurationAdjustment())
async def test_set_running_cost(self, central_system: CentralSystem, test_controller: TestController,
test_utility: TestUtility, test_config: OcppTestConfiguration, probe_module):
"""
Test running and final cost, that is 'embedded' in the TransactionEventResponse.
"""
# prepare data for the test
transaction_event_response_started = call_result201.TransactionEvent()
transaction_event_response = call_result201.TransactionEvent()
# According to the OCPP spec this should be a floating point number but the test framework does not allow that.
transaction_event_response.total_cost = 3.13
transaction_event_response.updated_personal_message = {"format": "UTF8", "language": "en",
"content": "$2.81 @ $0.12/kWh, $0.50 @ $1/h, TOTAL KWH: 23.4 TIME: 03.50 COST: $3.31. Visit www.cpo.com/invoices/13546 for an invoice of your session."}
transaction_event_response.custom_data = {"vendorId": "org.openchargealliance.org.qrcode",
"qrCodeText": "https://www.cpo.com/invoices/13546"}
transaction_event_response_ended = deepcopy(transaction_event_response)
transaction_event_response_ended.total_cost = 55.1
received_data = {'cost_chunks': [{'cost': {'value': 313000}, 'timestamp_to': ANY}],
'currency': {'code': 'EUR', 'decimals': 5}, 'message': [{
'content': '$2.81 @ $0.12/kWh, $0.50 @ $1/h, TOTAL KWH: 23.4 TIME: 03.50 COST: $3.31. '
'Visit www.cpo.com/invoices/13546 for an invoice of your session.',
'format': 'UTF8', 'language': 'en'},
],
'qr_code': 'https://www.cpo.com/invoices/13546', 'session_id': ANY, 'status': 'Running'}
evse_id1 = 1
connector_id = 1
probe_module_mock_fn = Mock()
probe_module.subscribe_variable(
"session_cost", "session_cost", probe_module_mock_fn)
probe_module.start()
await probe_module.wait_to_be_ready()
chargepoint_with_pm = await central_system.wait_for_chargepoint()
# Clear cache
r: call_result201.ClearCache = await chargepoint_with_pm.clear_cache_req()
assert r.status == ClearCacheStatusEnumType.accepted
# make an unknown IdToken
id_token = IdTokenType(
id_token="DEADBEEF",
type=IdTokenTypeEnum.iso14443
)
# Three TransactionEvents will be sent: started, updated and ended. The last two have the pricing information.
central_system.mock.on_transaction_event.side_effect = [transaction_event_response_started, # Started
transaction_event_response, # Updated
transaction_event_response_ended] # Ended
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "StatusNotification",
call201.StatusNotification(datetime.now().isoformat(),
ConnectorStatusEnumType.available,
evse_id=evse_id1,
connector_id=connector_id),
validate_status_notification_201)
# Charging station is now available, start charging session.
# swipe id tag to authorize
test_controller.swipe(id_token.id_token)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "Authorize",
call201.Authorize(id_token
))
# start charging session
test_controller.plug_in()
# should send a Transaction event
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "TransactionEvent",
{"eventType": "Started"})
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "TransactionEvent",
{"eventType": "Updated"})
# A session cost message should have been received
await self.await_mock_called(probe_module_mock_fn)
probe_module_mock_fn.assert_called_once_with(received_data)
# Now stop the transaction, this should also send a TransactionEvent (Ended)
test_controller.plug_out()
# 'Final' costs are a bit different than the 'Running' costs.
received_data['cost_chunks'][0] = {
'cost': {'value': 5510000}, 'metervalue_to': 0, 'timestamp_to': ANY}
received_data['status'] = 'Finished'
probe_module_mock_fn.call_count = 0
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "TransactionEvent",
{"eventType": "Ended"})
await self.await_mock_called(probe_module_mock_fn)
probe_module_mock_fn.assert_called_once_with(received_data)
@pytest.mark.asyncio
@pytest.mark.probe_module
@pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceSessionCostConfigurationAdjustment())
async def test_cost_updated_request(self, central_system: CentralSystem,
test_controller: TestController, test_utility: TestUtility,
test_config: OcppTestConfiguration, probe_module):
"""
Test the 'cost updated request' with california pricing information.
"""
received_data = {
'charging_price': [
{'category': 'Time', 'price': {'currency': {
'code': 'EUR', 'decimals': 5}, 'value': {'value': 200000}}},
{'category': 'Energy',
'price': {'currency': {'code': 'EUR', 'decimals': 5}, 'value': {'value': 12300}}},
{'category': 'FlatFee',
'price': {'currency': {'code': 'EUR', 'decimals': 5}, 'value': {'value': 4242000}}}],
'cost_chunks': [
{'cost': {'value': 134500}, 'metervalue_to': 1234000, 'timestamp_to': ANY}],
'currency': {'code': 'EUR', 'decimals': 5},
'idle_price': {'grace_minutes': 30,
'hour_price': {'currency': {'code': 'EUR', 'decimals': 5}, 'value': {'value': 100000}}},
'next_period': {
'charging_price': [{'category': 'Time',
'price': {'currency': {'code': 'EUR', 'decimals': 5}, 'value': {'value': 400000}}},
{'category': 'Energy',
'price': {'currency': {'code': 'EUR', 'decimals': 5}, 'value': {'value': 10000}}},
{'category': 'FlatFee',
'price': {'currency': {'code': 'EUR', 'decimals': 5},
'value': {'value': 8484000}}}],
'idle_price': {'hour_price': {'currency': {'code': 'EUR', 'decimals': 5}, 'value': {'value': 50000}}},
'timestamp_from': ANY},
'session_id': ANY, 'status': 'Running'}
session_cost_mock = Mock()
probe_module.subscribe_variable(
"session_cost", "session_cost", session_cost_mock)
probe_module.start()
await probe_module.wait_to_be_ready()
chargepoint_with_pm = await central_system.wait_for_chargepoint()
# prepare data for the test
evse_id1 = 1
connector_id = 1
# make an unknown IdToken
id_token = IdTokenType(
id_token="DEADBEEF",
type=IdTokenTypeEnum.iso14443
)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "StatusNotification",
call201.StatusNotification(datetime.now().isoformat(),
ConnectorStatusEnumType.available,
evse_id=evse_id1,
connector_id=connector_id),
validate_status_notification_201)
# Send cost updated request while there is no transaction: This should just forward the request There is nothing
# in the spec that sais what to do here and you can't send a 'rejected'.
await chargepoint_with_pm.cost_update_req(total_cost=1.345, transaction_id="1",
custom_data=self.cost_updated_custom_data)
# A session cost message should have been received
await self.await_mock_called(session_cost_mock)
session_cost_mock.assert_called_once_with(received_data)
# swipe id tag to authorize
test_controller.swipe(id_token.id_token)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "Authorize",
call201.Authorize(id_token
))
# start charging session
test_controller.plug_in()
# should send a Transaction event
transaction_event = await wait_for_and_validate(test_utility, chargepoint_with_pm, "TransactionEvent",
{"eventType": "Started"})
transaction_id = transaction_event['transaction_info']['transaction_id']
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "TransactionEvent",
{"eventType": "Updated"})
# Clear cache
r: call_result201.ClearCache = await chargepoint_with_pm.clear_cache_req()
assert r.status == ClearCacheStatusEnumType.accepted
session_cost_mock.call_count = 0
await chargepoint_with_pm.cost_update_req(total_cost=1.345, transaction_id=transaction_id,
custom_data=self.cost_updated_custom_data)
# A session cost message should have been received
await self.await_mock_called(session_cost_mock)
session_cost_mock.assert_called_once_with(received_data)
# Clear cache
r: call_result201.ClearCache = await chargepoint_with_pm.clear_cache_req()
assert r.status == ClearCacheStatusEnumType.accepted
session_cost_mock.call_count = 0
# Set transaction id to a not existing transaction id.
await chargepoint_with_pm.cost_update_req(total_cost=1.345, transaction_id="12345",
custom_data=self.cost_updated_custom_data)
# A session cost message should still have been received
await self.await_mock_called(session_cost_mock)
session_cost_mock.assert_called_once_with(received_data)
@pytest.mark.asyncio
@pytest.mark.probe_module
@pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceMetervaluesConfigurationAdjustment(
evse_manager_ids=["connector_1", "connector_2"]))
async def test_running_cost_trigger_time(self, central_system: CentralSystem,
test_controller: TestController, test_utility: TestUtility,
test_config: OcppTestConfiguration, probe_module):
probe_module_mock_fn = Mock()
probe_module_mock_fn.return_value = {
"status": "OK"
}
probe_module.implement_command(
"ProbeModulePowerMeter", "start_transaction", probe_module_mock_fn)
probe_module.implement_command(
"ProbeModulePowerMeter", "stop_transaction", probe_module_mock_fn)
power_meter_value = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"energy_Wh_import": {
"total": 1.0
},
"power_W": {
"total": 1000.0
}
}
probe_module.start()
await probe_module.wait_to_be_ready()
# wait for libocpp to go online
chargepoint_with_pm = await central_system.wait_for_chargepoint()
# Start transaction
transaction_id = await self.start_transaction(test_controller, test_utility, chargepoint_with_pm)
timestamp = datetime.now(timezone.utc).isoformat()
power_meter_value["timestamp"] = timestamp
probe_module.publish_variable(
"ProbeModulePowerMeter", "powermeter", power_meter_value)
test_utility.messages.clear()
# Metervalues should be sent at below trigger time.
data = self.cost_updated_custom_data.copy()
data["triggerMeterValue"]["atTime"] = (datetime.now(
timezone.utc) + timedelta(seconds=3)).isoformat()
# Once the transaction is started, send a 'RunningCost' message.
await chargepoint_with_pm.cost_update_req(total_cost=1.345, transaction_id=transaction_id,
custom_data=data)
# At the given time, metervalues must have been sent.
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "MeterValues",
{"evseId": 1, "meterValue": [{"sampledValue": [
{"context": "Other", "location": "Outlet",
"measurand": "Energy.Active.Import.Register",
"unitOfMeasure": {"unit": "Wh"}, "value": 1.0}],
'timestamp': timestamp[:-9] + 'Z'}]}
)
@pytest.mark.asyncio
@pytest.mark.probe_module
@pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceMetervaluesConfigurationAdjustment(
evse_manager_ids=["connector_1", "connector_2"]
))
async def test_running_cost_trigger_energy(self, central_system: CentralSystem,
test_controller: TestController, test_utility: TestUtility,
test_config: OcppTestConfiguration, probe_module):
probe_module_mock_fn = Mock()
probe_module_mock_fn.return_value = {
"status": "OK"
}
probe_module.implement_command(
"ProbeModulePowerMeter", "start_transaction", probe_module_mock_fn)
probe_module.implement_command(
"ProbeModulePowerMeter", "stop_transaction", probe_module_mock_fn)
power_meter_value = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"energy_Wh_import": {
"total": 1.0
}
}
probe_module.start()
await probe_module.wait_to_be_ready()
# wait for libocpp to go online
chargepoint_with_pm = await central_system.wait_for_chargepoint()
# Start transaction
transaction_id = await self.start_transaction(test_controller, test_utility, chargepoint_with_pm)
timestamp = datetime.now(timezone.utc).isoformat()
power_meter_value["timestamp"] = timestamp
probe_module.publish_variable(
"ProbeModulePowerMeter", "powermeter", power_meter_value)
test_utility.messages.clear()
# Metervalues should be sent at below trigger time.
data = self.cost_updated_custom_data.copy()
# Send running cost, which has a trigger specified on atEnergykWh = 5.0
await chargepoint_with_pm.cost_update_req(total_cost=1.345, transaction_id=transaction_id,
custom_data=data)
# Now increase power meter value so it is above the specified trigger and publish the powermeter value
power_meter_value["energy_Wh_import"]["total"] = 6000.0
timestamp = datetime.now(timezone.utc).isoformat()
power_meter_value["timestamp"] = timestamp
probe_module.publish_variable(
"ProbeModulePowerMeter", "powermeter", power_meter_value)
# Powermeter value should be sent because of the trigger.
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "MeterValues",
{"evseId": 1, "meterValue": [{"sampledValue": [
{"context": "Other", "location": "Outlet",
"measurand": "Energy.Active.Import.Register",
"unitOfMeasure": {"unit": "Wh"}, "value": 6000.0}],
'timestamp': timestamp[:-9] + 'Z'}]}
)
@pytest.mark.asyncio
@pytest.mark.probe_module
@pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceMetervaluesConfigurationAdjustment(
evse_manager_ids=["connector_1", "connector_2"]
))
async def test_running_cost_trigger_power(self, central_system: CentralSystem,
test_controller: TestController, test_utility: TestUtility,
test_config: OcppTestConfiguration, probe_module):
probe_module_mock_fn = Mock()
probe_module_mock_fn.return_value = {
"status": "OK"
}
probe_module.implement_command(
"ProbeModulePowerMeter", "start_transaction", probe_module_mock_fn)
probe_module.implement_command(
"ProbeModulePowerMeter", "stop_transaction", probe_module_mock_fn)
power_meter_value = {
"timestamp": datetime.now(timezone.utc).isoformat(),
"energy_Wh_import": {
"total": 1.0
},
"power_W": {
"total": 1000.0
}
}
probe_module.start()
await probe_module.wait_to_be_ready()
# wait for libocpp to go online
chargepoint_with_pm = await central_system.wait_for_chargepoint()
# Start transaction
transaction_id = await self.start_transaction(test_controller, test_utility, chargepoint_with_pm)
timestamp = datetime.now(timezone.utc).isoformat()
power_meter_value["timestamp"] = timestamp
probe_module.publish_variable(
"ProbeModulePowerMeter", "powermeter", power_meter_value)
test_utility.messages.clear()
# Metervalues should be sent at below trigger time.
data = self.cost_updated_custom_data.copy()
# Send running cost, which has a trigger specified on atEnergykWh = 5.0
await chargepoint_with_pm.cost_update_req(total_cost=1.345, transaction_id=transaction_id,
custom_data=data)
# Set W above the trigger value and publish a new powermeter value.
power_meter_value["energy_Wh_import"]["total"] = 1.0
power_meter_value["power_W"]["total"] = 10000.0
timestamp = datetime.now(timezone.utc).isoformat()
power_meter_value["timestamp"] = timestamp
probe_module.publish_variable(
"ProbeModulePowerMeter", "powermeter", power_meter_value)
# Powermeter value should be sent because of the trigger.
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "MeterValues",
{"evseId": 1, "meterValue": [{"sampledValue": [
{"context": "Other", "location": "Outlet",
"measurand": "Energy.Active.Import.Register",
"unitOfMeasure": {"unit": "Wh"}, "value": 1.0},
{'context': 'Other', 'location': 'Outlet',
'measurand': 'Power.Active.Import', "unitOfMeasure": {"unit": "W"},
'value': 10000.00}
],
'timestamp': timestamp[:-9] + 'Z'}]}
)
# W value is below trigger, but hysteresis prevents sending the metervalue.
power_meter_value["power_W"]["total"] = 7990.0
timestamp = datetime.now(timezone.utc).isoformat()
power_meter_value["timestamp"] = timestamp
probe_module.publish_variable(
"ProbeModulePowerMeter", "powermeter", power_meter_value)
# So no metervalue is sent
assert not await wait_for_and_validate(test_utility, chargepoint_with_pm, "MeterValues",
{"evseId": 1, "meterValue": [{"sampledValue": [
{"context": "Other", "location": "Outlet",
"measurand": "Energy.Active.Import.Register",
"unitOfMeasure": {"unit": "Wh"}, "value": 1.0},
{'context': 'Other', 'location': 'Outlet',
'measurand': 'Power.Active.Import', "unitOfMeasure": {"unit": "W"},
'value': 7990.0}
],
'timestamp': timestamp[:-9] + 'Z'}]}
)
# Only when trigger is high ( / low) enough, metervalue will be sent.
power_meter_value["power_W"]["total"] = 7200.0
timestamp = datetime.now(timezone.utc).isoformat()
power_meter_value["timestamp"] = timestamp
probe_module.publish_variable(
"ProbeModulePowerMeter", "powermeter", power_meter_value)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "MeterValues",
{"evseId": 1, "meterValue": [{"sampledValue": [
{"context": "Other", "location": "Outlet",
"measurand": "Energy.Active.Import.Register",
"unitOfMeasure": {"unit": "Wh"}, "value": 1.0},
{'context': 'Other', 'location': 'Outlet',
'measurand': 'Power.Active.Import', "unitOfMeasure": {"unit": "W"},
'value': 7200.00}
],
'timestamp': timestamp[:-9] + 'Z'}]}
)
@pytest.mark.asyncio
@pytest.mark.probe_module
@pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceSessionCostConfigurationAdjustment())
async def test_tariff_fallback_message_on_authorize(self, central_system: CentralSystem,
test_controller: TestController,
test_utility: TestUtility,
test_config: OcppTestConfiguration,
probe_module):
"""
I04.FR.01 integration: When TariffFallbackMessage is configured and the CSMS returns no
personalMessage in AuthorizeResponse, test if personal message is still set to the configured fallback.
The injected personalMessage is then converted and published via session_cost.tariff_message
so the display can show the price to the EV Driver.
"""
tariff_message_mock = Mock()
probe_module.subscribe_variable("session_cost", "tariff_message", tariff_message_mock)
probe_module.start()
await probe_module.wait_to_be_ready()
chargepoint_with_pm = await central_system.wait_for_chargepoint()
# Clear auth cache so the CSMS is consulted for every authorization.
r: call_result201.ClearCache = await chargepoint_with_pm.clear_cache_req()
assert r.status == ClearCacheStatusEnumType.accepted
# Configure TariffFallbackMessage on the CS via OCPP SetVariables.
r = await chargepoint_with_pm.set_config_variables_req(
"TariffCostCtrlr", "TariffFallbackMessage", "Tariff: 0.30 EUR/kWh"
)
assert SetVariableResultType(**r.set_variable_result[0]).attribute_status == SetVariableStatusEnumType.accepted
evse_id1 = 1
connector_id = 1
id_token = IdTokenType(id_token="DEADBEEF", type=IdTokenTypeEnum.iso14443)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "StatusNotification",
call201.StatusNotification(datetime.now().isoformat(),
ConnectorStatusEnumType.available,
evse_id=evse_id1,
connector_id=connector_id),
validate_status_notification_201)
# Swipe to trigger authorization. The CSMS mock returns Accepted without a personalMessage
# (default behavior), so ensure_personal_message must inject the configured fallback.
test_controller.swipe(id_token.id_token)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "Authorize",
call201.Authorize(id_token))
# The fallback tariff message must arrive on session_cost.tariff_message.
await self.await_mock_called(tariff_message_mock)
call_data = tariff_message_mock.call_args[0][0]
assert call_data["identifier_id"] == "DEADBEEF"
assert call_data["identifier_type"] == "IdToken"
assert len(call_data["messages"]) == 1
assert call_data["messages"][0]["content"] == "Tariff: 0.30 EUR/kWh"
@pytest.mark.asyncio
@pytest.mark.probe_module
@pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceSessionCostConfigurationAdjustment())
async def test_tariff_fallback_message_multilanguage_on_authorize(self, central_system: CentralSystem,
test_controller: TestController,
test_utility: TestUtility,
test_config: OcppTestConfiguration,
probe_module):
"""
California Pricing 4.3.4 integration: When TariffFallbackMessage is configured for multiple languages, the
base (no-instance) message becomes personalMessage and language-specific instances go into
customData.personalMessageExtra. Both are forwarded as entries in session_cost.tariff_message
so multi-language displays can show the correct price text.
"""
tariff_message_mock = Mock()
probe_module.subscribe_variable("session_cost", "tariff_message", tariff_message_mock)
probe_module.start()
await probe_module.wait_to_be_ready()
chargepoint_with_pm = await central_system.wait_for_chargepoint()
# Clear auth cache so the CSMS is consulted for every authorization.
r: call_result201.ClearCache = await chargepoint_with_pm.clear_cache_req()
assert r.status == ClearCacheStatusEnumType.accepted
# Set the base TariffFallbackMessage (no language instance).
r = await chargepoint_with_pm.set_config_variables_req(
"TariffCostCtrlr", "TariffFallbackMessage", "Tariff: 0.30 EUR/kWh"
)
assert SetVariableResultType(**r.set_variable_result[0]).attribute_status == SetVariableStatusEnumType.accepted
# Set the German-language instance ("de" is in DisplayMessageCtrlr.Language.valuesList).
r = await chargepoint_with_pm.set_variables_req(set_variable_data=[
SetVariableDataType(
attribute_value="Tarif: 0,30 EUR/kWh",
attribute_type=AttributeEnumType.actual,
component=ComponentType(name="TariffCostCtrlr"),
variable=VariableType(name="TariffFallbackMessage", instance="de")
)
])
assert SetVariableResultType(**r.set_variable_result[0]).attribute_status == SetVariableStatusEnumType.accepted
evse_id1 = 1
connector_id = 1
id_token = IdTokenType(id_token="DEADBEEF", type=IdTokenTypeEnum.iso14443)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "StatusNotification",
call201.StatusNotification(datetime.now().isoformat(),
ConnectorStatusEnumType.available,
evse_id=evse_id1,
connector_id=connector_id),
validate_status_notification_201)
test_controller.swipe(id_token.id_token)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "Authorize",
call201.Authorize(id_token))
await self.await_mock_called(tariff_message_mock)
call_data = tariff_message_mock.call_args[0][0]
assert call_data["identifier_id"] == "DEADBEEF"
assert call_data["identifier_type"] == "IdToken"
# Base message + German entry from personalMessageExtra.
assert len(call_data["messages"]) == 2
assert call_data["messages"][0]["content"] == "Tariff: 0.30 EUR/kWh"
assert call_data["messages"][1]["content"] == "Tarif: 0,30 EUR/kWh"
assert call_data["messages"][1]["language"] == "de"
@pytest.mark.asyncio
@pytest.mark.probe_module
@pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceSessionCostConfigurationAdjustment())
async def test_total_cost_fallback_message_on_offline_transaction_end(self, central_system: CentralSystem,
test_controller: TestController,
test_utility: TestUtility,
test_config: OcppTestConfiguration,
probe_module):
"""
I05.FR.02 integration: When the CS is offline when a transaction ends and TotalCostFallbackMessage
is configured, a default total cost shall still be published via session_cost.tariff_message,
even if the CSMS response containing totalCost will never arrive.
"""
tariff_message_mock = Mock()
probe_module.subscribe_variable("session_cost", "tariff_message", tariff_message_mock)
probe_module.start()
await probe_module.wait_to_be_ready()
chargepoint_with_pm = await central_system.wait_for_chargepoint()
# Configure TotalCostFallbackMessage. TariffFallbackMessage is intentionally left empty so
# the auth step does not produce a tariff_message event.
r = await chargepoint_with_pm.set_config_variables_req(
"TariffCostCtrlr", "TotalCostFallbackMessage", "Total cost unavailable (offline)"
)
assert SetVariableResultType(**r.set_variable_result[0]).attribute_status == SetVariableStatusEnumType.accepted
evse_id1 = 1
connector_id = 1
id_token = IdTokenType(id_token="DEADBEEF", type=IdTokenTypeEnum.iso14443)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "StatusNotification",
call201.StatusNotification(datetime.now().isoformat(),
ConnectorStatusEnumType.available,
evse_id=evse_id1,
connector_id=connector_id),
validate_status_notification_201)
# Start a transaction while online.
test_controller.swipe(id_token.id_token)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "Authorize",
call201.Authorize(id_token))
test_controller.plug_in()
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "TransactionEvent",
{"eventType": "Started"})
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "TransactionEvent",
{"eventType": "Updated"})
# Go offline before ending the transaction so that the CS cannot retrieve totalCost.
test_controller.disconnect_websocket()
await asyncio.sleep(2)
# End the transaction while offline.
test_controller.plug_out()
# The TotalCostFallbackMessage must be published via session_cost.tariff_message.
await self.await_mock_called(tariff_message_mock)
call_data = tariff_message_mock.call_args[0][0]
assert call_data["identifier_type"] == "TransactionId"
assert len(call_data["messages"]) == 1
assert call_data["messages"][0]["content"] == "Total cost unavailable (offline)"
# Reconnect so queued TransactionEvent(Ended) can be sent and test teardown completes cleanly.
test_controller.connect_websocket()
chargepoint_with_pm = await central_system.wait_for_chargepoint(wait_for_bootnotification=False)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "TransactionEvent",
{"eventType": "Ended"})
@pytest.mark.asyncio
@pytest.mark.probe_module
@pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceSessionCostConfigurationAdjustment())
async def test_default_price_published_on_disconnect(self, central_system: CentralSystem,
test_controller: TestController,
test_utility: TestUtility,
test_config: OcppTestConfiguration,
probe_module):
"""
When TariffFallbackMessage and OfflineTariffFallbackMessage are configured and the CS goes
offline, default_price shall be published via session_cost.default_price with the configured
offline price text.
"""
default_price_mock = Mock()
probe_module.subscribe_variable("session_cost", "default_price", default_price_mock)
probe_module.start()
await probe_module.wait_to_be_ready()
chargepoint_with_pm = await central_system.wait_for_chargepoint()
r = await chargepoint_with_pm.set_config_variables_req(
"TariffCostCtrlr", "TariffFallbackMessage", "0.30 EUR/kWh"
)
assert SetVariableResultType(**r.set_variable_result[0]).attribute_status == SetVariableStatusEnumType.accepted
r = await chargepoint_with_pm.set_config_variables_req(
"TariffCostCtrlr", "OfflineTariffFallbackMessage", "Station is offline"
)
assert SetVariableResultType(**r.set_variable_result[0]).attribute_status == SetVariableStatusEnumType.accepted
# Reset any publications from the startup phase before triggering a controlled disconnect.
default_price_mock.reset_mock()
test_controller.disconnect_websocket()
await self.await_mock_called_matching(
default_price_mock,
lambda c: c.args[0]["messages"][0]["content"] == "Station is offline",
)
# Reconnect for clean teardown.
test_controller.connect_websocket()
await central_system.wait_for_chargepoint(wait_for_bootnotification=False)
@pytest.mark.asyncio
@pytest.mark.probe_module
@pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceSessionCostConfigurationAdjustment())
async def test_default_price_published_on_reconnect(self, central_system: CentralSystem,
test_controller: TestController,
test_utility: TestUtility,
test_config: OcppTestConfiguration,
probe_module):
"""
When TariffFallbackMessage is configured and the CS reconnects to the CSMS after being
offline, default_price shall be re-published with the configured online price text.
"""
default_price_mock = Mock()
probe_module.subscribe_variable("session_cost", "default_price", default_price_mock)
probe_module.start()
await probe_module.wait_to_be_ready()
chargepoint_with_pm = await central_system.wait_for_chargepoint()
r = await chargepoint_with_pm.set_config_variables_req(
"TariffCostCtrlr", "TariffFallbackMessage", "0.30 EUR/kWh"
)
assert SetVariableResultType(**r.set_variable_result[0]).attribute_status == SetVariableStatusEnumType.accepted
r = await chargepoint_with_pm.set_config_variables_req(
"TariffCostCtrlr", "OfflineTariffFallbackMessage", "Station is offline"
)
assert SetVariableResultType(**r.set_variable_result[0]).attribute_status == SetVariableStatusEnumType.accepted
# Go offline and wait for the offline publication, then clear the mock.
default_price_mock.reset_mock()
test_controller.disconnect_websocket()
await self.await_mock_called(default_price_mock)
default_price_mock.reset_mock()
# Reconnect and wait for the online publication.
test_controller.connect_websocket()
chargepoint_with_pm = await central_system.wait_for_chargepoint(wait_for_bootnotification=False)
await self.await_mock_called(default_price_mock)
call_data = default_price_mock.call_args[0][0]
assert len(call_data["messages"]) == 1
assert call_data["messages"][0]["content"] == "0.30 EUR/kWh"
@pytest.mark.asyncio
@pytest.mark.probe_module
@pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceSessionCostConfigurationAdjustment())
async def test_default_price_published_on_change(self, central_system: CentralSystem,
test_controller: TestController,
test_utility: TestUtility,
test_config: OcppTestConfiguration,
probe_module):
"""
When TariffFallbackMessage is changed via SetVariables while connected, default_price
shall be immediately re-published with the new price text.
"""
default_price_mock = Mock()
probe_module.subscribe_variable("session_cost", "default_price", default_price_mock)
probe_module.start()
await probe_module.wait_to_be_ready()
chargepoint_with_pm = await central_system.wait_for_chargepoint()
default_price_mock.reset_mock()
r = await chargepoint_with_pm.set_config_variables_req(
"TariffCostCtrlr", "TariffFallbackMessage", "0.30 EUR/kWh"
)
assert SetVariableResultType(**r.set_variable_result[0]).attribute_status == SetVariableStatusEnumType.accepted
await self.await_mock_called(default_price_mock)
call_data = default_price_mock.call_args[0][0]
assert len(call_data["messages"]) == 1
assert call_data["messages"][0]["content"] == "0.30 EUR/kWh"

View File

@@ -0,0 +1,317 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
import pytest_asyncio
# fmt: off
import logging
from copy import deepcopy
from typing import Dict
from unittest.mock import Mock, call as mock_call
import json
import time
import pytest
from everest.testing.core_utils.common import Requirement
from everest.testing.ocpp_utils.central_system import CentralSystem
from test_sets.everest_test_utils import * # Needs to be before the datatypes below since it overrides the v201 Action enum with the v16 one
from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201
from everest.testing.core_utils.probe_module import ProbeModule
from everest.testing.core_utils import EverestConfigAdjustmentStrategy
log = logging.getLogger("ocpp201DataTransferTest")
async def await_mock_called(mock):
while not mock.call_count:
await asyncio.sleep(0.1)
# FIXME: redefine probe_module and chargepoint_with_pm here until the ones in conftest.py are fixed
@pytest.fixture
def probe_module(started_test_controller, everest_core) -> ProbeModule:
# initiate the probe module, connecting to the same runtime session the test controller started
module = ProbeModule(everest_core.get_runtime_session())
return module
@pytest_asyncio.fixture
async def chargepoint_with_pm(central_system: CentralSystem, probe_module: ProbeModule):
"""Fixture for ChargePoint16. Requires central_system_v201 and test_controller. Starts test_controller immediately
"""
probe_module.start()
await probe_module.wait_to_be_ready()
# wait for libocpp to go online
cp = await central_system.wait_for_chargepoint()
yield cp
await cp.stop()
class ProbeModuleDataTransferConfigurationAdjustment(EverestConfigAdjustmentStrategy):
def adjust_everest_configuration(self, everest_config: Dict):
adjusted_config = deepcopy(everest_config)
adjusted_config["active_modules"]["ocpp"]["connections"]["data_transfer"] = [{"module_id": "probe", "implementation_id": "ProbeModuleDataTransfer"}]
return adjusted_config
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
@pytest.mark.everest_core_config("everest-config-ocpp201-data-transfer.yaml")
@pytest.mark.inject_csms_mock
class TestOcpp201DataTransferIntegration:
"""
Integration tests for the OCPP201 Module's implementation of the P-test cases (data transfer)
Uses the probe module and a mock CSMS.
"""
@pytest.mark.parametrize("response_status",
["Accepted", "Rejected", "UnknownMessageId", "UnknownVendorId"],
ids=["successful", "failed", "unknown_message_id", "unknown_vendor_id"])
@pytest.mark.parametrize("message_id",
["message123", None],
ids=["with_msg_id", "no_msg_id"])
@pytest.mark.parametrize("data",
["string_data", 42, 1.2345, False, None],
ids=["string_data", "int_data", "float_data", "bool_data", "no_data"])
@pytest.mark.probe_module
@pytest.mark.everest_config_adaptions(ProbeModuleDataTransferConfigurationAdjustment())
@pytest.mark.asyncio
async def test_p1(self, response_status, message_id, data, central_system: CentralSystem, probe_module):
"""
Use case P01: Data transfer to the Charging Station
"""
probe_module_mock_fn = Mock()
probe_module_mock_fn.side_effect = [{
"status": response_status,
"data": json.dumps("response123")
}]
probe_module.implement_command("ProbeModuleDataTransfer", "data_transfer", probe_module_mock_fn)
probe_module.start()
await probe_module.wait_to_be_ready()
# wait for libocpp to go online
chargepoint_with_pm = await central_system.wait_for_chargepoint()
data_transfer_result: call_result201.DataTransfer = await chargepoint_with_pm.data_transfer_req(
message_id=message_id,
data=data,
vendor_id="vendor123"
)
assert data_transfer_result == call_result201.DataTransfer(
data="response123",
status=response_status
)
if message_id is None:
if data is None:
assert probe_module_mock_fn.mock_calls == [mock_call({
"request": {
"vendor_id": "vendor123"
}
})]
else:
assert probe_module_mock_fn.mock_calls == [mock_call({
"request": {
"vendor_id": "vendor123",
"data": json.dumps(data)
}
})]
else:
if data is None:
assert probe_module_mock_fn.mock_calls == [mock_call({
"request": {
"message_id": message_id,
"vendor_id": "vendor123"
}
})]
else:
assert probe_module_mock_fn.mock_calls == [mock_call({
"request": {
"message_id": message_id,
"vendor_id": "vendor123",
"data": json.dumps(data)
}
})]
@pytest.mark.parametrize("response_status",
["Accepted", "Rejected", "UnknownMessageId", "UnknownVendorId"],
ids=["successful", "failed", "unknown_message_id", "unknown_vendor_id"])
@pytest.mark.parametrize("message_id",
["message987", None],
ids=["with_msg_id", "no_msg_id"])
@pytest.mark.probe_module
@pytest.mark.everest_config_adaptions(ProbeModuleDataTransferConfigurationAdjustment())
@pytest.mark.asyncio
async def test_p1_json(self, response_status, message_id, central_system: CentralSystem, probe_module):
"""
Use case P01: Data transfer to the Charging Station
"""
probe_module_mock_fn = Mock()
probe_module_mock_fn.side_effect = [{
"status": response_status,
"data": "{\"response987\":\"hello\"}"
}]
probe_module.implement_command("ProbeModuleDataTransfer", "data_transfer", probe_module_mock_fn)
probe_module.start()
await probe_module.wait_to_be_ready()
# wait for libocpp to go online
chargepoint_with_pm = await central_system.wait_for_chargepoint()
data_transfer_result: call_result201.DataTransfer = await chargepoint_with_pm.data_transfer_req(
message_id=message_id,
data={"request987":"hi"},
vendor_id="vendor123"
)
assert data_transfer_result == call_result201.DataTransfer(
data={'response987':'hello'},
status=response_status
)
if message_id is None:
assert probe_module_mock_fn.mock_calls == [mock_call({
"request": {
"vendor_id": "vendor123",
"data": "{\"request987\":\"hi\"}"
}
})]
else:
assert probe_module_mock_fn.mock_calls == [mock_call({
"request": {
"message_id": message_id,
"vendor_id": "vendor123",
"data": "{\"request987\":\"hi\"}"
}
})]
@pytest.mark.parametrize("response_status",
["Accepted", "Rejected", "UnknownMessageId", "UnknownVendorId"],
ids=["successful", "failed", "unknown_message_id", "unknown_vendor_id"])
@pytest.mark.parametrize("message_id",
["message123", None],
ids=["with_msg_id", "no_msg_id"])
@pytest.mark.parametrize("data",
["string_data", 42, 1.2345, False, None],
ids=["string_data", "int_data", "float_data", "bool_data", "no_data"])
@pytest.mark.probe_module(
connections={
"ocpp_data_transfer": [Requirement(module_id="ocpp", implementation_id="data_transfer")]
}
)
@pytest.mark.asyncio
async def test_p2(self, response_status, message_id, data, central_system: CentralSystem,
chargepoint_with_pm: ChargePoint201, probe_module):
"""
Use case P02: Data transfer to the CSMS
"""
central_system.mock.on_data_transfer.side_effect = [
call_result201.DataTransfer(status=response_status, data="response123")
]
response = json.dumps("response123")
if message_id is None:
if data is None:
assert await probe_module.call_command("ocpp_data_transfer", "data_transfer", {
"request": {"vendor_id": "vendor123"}
}) == {"status": response_status, "data": response}
else:
assert await probe_module.call_command("ocpp_data_transfer", "data_transfer", {
"request": {"vendor_id": "vendor123", "data": json.dumps(data)}
}) == {"status": response_status, "data": response}
else:
if data is None:
assert await probe_module.call_command("ocpp_data_transfer", "data_transfer", {
"request": {"vendor_id": "vendor123", "message_id": message_id}
}) == {"status": response_status, "data": response}
else:
assert await probe_module.call_command("ocpp_data_transfer", "data_transfer", {
"request": {"vendor_id": "vendor123", "message_id": message_id, "data": json.dumps(data)}
}) == {"status": response_status, "data": response}
@pytest.mark.parametrize("response_status",
["Accepted", "Rejected", "UnknownMessageId", "UnknownVendorId"],
ids=["successful", "failed", "unknown_message_id", "unknown_vendor_id"])
@pytest.mark.parametrize("message_id",
["message987", None],
ids=["with_msg_id", "no_msg_id"])
@pytest.mark.probe_module(
connections={
"ocpp_data_transfer": [Requirement(module_id="ocpp", implementation_id="data_transfer")]
}
)
@pytest.mark.asyncio
async def test_p2_json(self, response_status, message_id, central_system: CentralSystem,
chargepoint_with_pm: ChargePoint201, probe_module):
"""
Use case P02: Data transfer to the CSMS
"""
central_system.mock.on_data_transfer.side_effect = [
call_result201.DataTransfer(status=response_status, data={'response987':'hello'})
]
if message_id is None:
assert await probe_module.call_command("ocpp_data_transfer", "data_transfer", {
"request": {"vendor_id": "vendor123", "data": "{\"request987\":\"hi\"}"}
}) == {"status": response_status, "data": "{\"response987\":\"hello\"}"}
else:
assert await probe_module.call_command("ocpp_data_transfer", "data_transfer", {
"request": {"vendor_id": "vendor123", "message_id": message_id, "data": "{\"request987\":\"hi\"}"}
}) == {"status": response_status, "data": "{\"response987\":\"hello\"}"}
@pytest.mark.asyncio
async def test_p1_no_callback(self, charge_point: ChargePoint201):
"""
Use case P01: Data transfer to the Charging Station
"""
data_transfer_result: call_result201.DataTransfer = await charge_point.data_transfer_req(
message_id="message123",
data="request123",
vendor_id="vendor123"
)
assert data_transfer_result == call_result201.DataTransfer(
data=None,
status="UnknownVendorId"
)
@pytest.mark.probe_module(
connections={
"ocpp_data_transfer": [Requirement(module_id="ocpp", implementation_id="data_transfer")]
}
)
@pytest.mark.probe_module
@pytest.mark.everest_config_adaptions(ProbeModuleDataTransferConfigurationAdjustment())
@pytest.mark.asyncio
@pytest.mark.skip("Fails because callback sleeps for 400s and test case expects response. Check expected behavior")
async def test_p1_no_response(self, central_system: CentralSystem, probe_module):
"""
Use case P01: Data transfer to the Charging Station but Charging Station does not respond
"""
def data_transfer_side_effect(*args, **kwargs):
time.sleep(400)
return call_result201.DataTransfer(status="Accepted", data={'response987':'hello'})
probe_module_mock_fn = Mock()
probe_module_mock_fn.side_effect = data_transfer_side_effect
probe_module.implement_command("ProbeModuleDataTransfer", "data_transfer", probe_module_mock_fn)
probe_module.start()
await probe_module.wait_to_be_ready()
# wait for libocpp to go online
chargepoint_with_pm = await central_system.wait_for_chargepoint()
data_transfer_result: call_result201.DataTransfer = await chargepoint_with_pm.data_transfer_req(
message_id="message123",
data="data",
vendor_id="vendor123"
)
assert data_transfer_result == call_result201.DataTransfer(
status="Rejected"
)

View File

@@ -0,0 +1,412 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
from datetime import timezone
from unittest.mock import Mock
import pytest
import logging
from everest.testing.ocpp_utils.central_system import CentralSystem
# Needs to be before the datatypes below since it overrides the v201 Action enum with the v16 one
from everest_test_utils import *
from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility
from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201
from everest.testing.core_utils.controller.test_controller_interface import TestController
from everest_test_utils_probe_modules import (probe_module,
ProbeModuleCostAndPriceDisplayMessageConfigurationAdjustment)
from ocpp.v201 import call as call201
from ocpp.v201 import call_result as call_result201
from ocpp.v201.enums import (
IdTokenEnumType as IdTokenTypeEnum, ConnectorStatusEnumType)
from ocpp.v201.datatypes import *
from everest.testing.core_utils._configuration.libocpp_configuration_helper import (
GenericOCPP2XConfigAdjustment,
OCPP2XConfigVariableIdentifier,
)
from validations import validate_status_notification_201
log = logging.getLogger("ocpp201DisplayMessageTest")
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
@pytest.mark.inject_csms_mock
@pytest.mark.everest_core_config(get_everest_config_path_str('everest-config-ocpp201-costandprice.yaml'))
@pytest.mark.ocpp_config_adaptions(GenericOCPP2XConfigAdjustment([
(OCPP2XConfigVariableIdentifier("DisplayMessageCtrlr", "DisplayMessageCtrlrAvailable", "Actual"),
"true"),
(OCPP2XConfigVariableIdentifier("DisplayMessageCtrlr", "QRCodeDisplayCapable",
"Actual"), "true"),
(OCPP2XConfigVariableIdentifier("DisplayMessageCtrlr", "DisplayMessageLanguage", "Actual"),
"en"),
(OCPP2XConfigVariableIdentifier("TariffCostCtrlr", "TariffCostCtrlrAvailableTariff", "Actual"),
"true"),
(OCPP2XConfigVariableIdentifier("TariffCostCtrlr", "TariffCostCtrlrAvailableCost", "Actual"),
"true"),
(OCPP2XConfigVariableIdentifier("TariffCostCtrlr",
"TariffCostCtrlrEnabledTariff", "Actual"), "true"),
(OCPP2XConfigVariableIdentifier("TariffCostCtrlr",
"TariffCostCtrlrEnabledCost", "Actual"), "true"),
(OCPP2XConfigVariableIdentifier("TariffCostCtrlr", "NumberOfDecimalsForCostValues", "Actual"),
"5"),
(OCPP2XConfigVariableIdentifier("OCPPCommCtrlr", "MessageTimeout", "Actual"),
"1"),
(OCPP2XConfigVariableIdentifier("OCPPCommCtrlr", "MessageAttemptInterval",
"Actual"), "1"),
(OCPP2XConfigVariableIdentifier("OCPPCommCtrlr", "MessageAttempts", "Actual"),
"3"),
(OCPP2XConfigVariableIdentifier("AuthCacheCtrlr", "AuthCacheCtrlrEnabled", "Actual"),
"true"),
(OCPP2XConfigVariableIdentifier("AuthCtrlr", "LocalPreAuthorize",
"Actual"), "true"),
(OCPP2XConfigVariableIdentifier("AuthCacheCtrlr", "AuthCacheLifeTime", "Actual"),
"86400"),
(OCPP2XConfigVariableIdentifier("DisplayMessageCtrlr", "DisplayMessageSupportedPriorities",
"Actual"), "AlwaysFront,NormalCycle"),
(OCPP2XConfigVariableIdentifier("DisplayMessageCtrlr", "DisplayMessageSupportedFormats",
"Actual"), "ASCII,URI,UTF8"),
(OCPP2XConfigVariableIdentifier("DisplayMessageCtrlr", "DisplayMessageSupportedStates", "Actual"),
"Charging,Faulted,Unavailable")
]))
class TestOcpp201DisplayMessage:
"""
Tests for OCPP 2.0.1 Display Message
"""
@staticmethod
async def start_transaction(test_controller: TestController, test_utility: TestUtility,
charge_point: ChargePoint201):
# prepare data for the test
evse_id1 = 1
connector_id = 1
# make an unknown IdToken
id_token = IdTokenType(
id_token="DEADBEEF",
type=IdTokenTypeEnum.iso14443
)
assert await wait_for_and_validate(test_utility, charge_point, "StatusNotification",
call201.StatusNotification(datetime.now().isoformat(),
ConnectorStatusEnumType.available,
evse_id=evse_id1,
connector_id=connector_id),
validate_status_notification_201)
# Charging station is now available, start charging session.
# swipe id tag to authorize
test_controller.swipe(id_token.id_token)
assert await wait_for_and_validate(test_utility, charge_point, "Authorize",
call201.Authorize(id_token
))
# start charging session
test_controller.plug_in()
# should send a Transaction event
transaction_event = await wait_for_and_validate(test_utility, charge_point, "TransactionEvent",
{"eventType": "Started"})
transaction_id = transaction_event['transaction_info']['transaction_id']
assert await wait_for_and_validate(test_utility, charge_point, "TransactionEvent",
{"eventType": "Updated"})
return transaction_id
@staticmethod
async def await_mock_called(mock):
while not mock.call_count:
await asyncio.sleep(0.1)
@pytest.mark.asyncio
@pytest.mark.probe_module
@pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceDisplayMessageConfigurationAdjustment())
async def test_set_display_message(self, central_system: CentralSystem, test_controller: TestController,
test_utility: TestUtility, test_config: OcppTestConfiguration, probe_module):
probe_module_mock_fn = Mock()
probe_module_mock_fn.return_value = {
"status": "Accepted"
}
probe_module.implement_command("ProbeModuleDisplayMessage", "set_display_message",
probe_module_mock_fn)
probe_module.implement_command("ProbeModuleDisplayMessage", "get_display_messages",
probe_module_mock_fn)
probe_module.implement_command("ProbeModuleDisplayMessage", "clear_display_message",
probe_module_mock_fn)
probe_module.start()
await probe_module.wait_to_be_ready()
chargepoint_with_pm = await central_system.wait_for_chargepoint()
start_time = datetime.now(timezone.utc).isoformat()
end_time = (datetime.now(timezone.utc) +
timedelta(minutes=1)).isoformat()
message = {'id': 1, 'priority': 'NormalCycle',
'message': {'format': 'UTF8', 'language': 'en',
'content': 'This is a display message'},
'startDateTime': start_time,
'endDateTime': end_time}
await chargepoint_with_pm.set_display_message_req(message=message, custom_data=None)
# Display message should have received a message with the current price information
data_received = {
'request': [{'id': 1, 'identifier_type': 'TransactionId',
'message': {'content': 'This is a display message', 'format': 'UTF8', 'language': 'en'},
'priority': 'NormalCycle', 'timestamp_from': start_time[:-9] + 'Z',
'timestamp_to': end_time[:-9] + 'Z'}]
}
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "SetDisplayMessage",
call_result201.SetDisplayMessage(
status='Accepted'),
timeout=5)
probe_module_mock_fn.assert_called_once_with(data_received)
# Test rejected return value
probe_module_mock_fn.return_value = {
"status": "Rejected"
}
await chargepoint_with_pm.set_display_message_req(message=message, custom_data=None)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "SetDisplayMessage",
call_result201.SetDisplayMessage(
status='Rejected'),
timeout=5)
probe_module_mock_fn.return_value = {
"status": "Accepted"
}
# Test unsupported priority
message['priority'] = 'InFront'
await chargepoint_with_pm.set_display_message_req(message=message, custom_data=None)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "SetDisplayMessage",
call_result201.SetDisplayMessage(
status='NotSupportedPriority'),
timeout=5)
message['priority'] = 'NormalCycle'
# Test unsupported message format
message['message']['format'] = 'HTML'
await chargepoint_with_pm.set_display_message_req(message=message, custom_data=None)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "SetDisplayMessage",
call_result201.SetDisplayMessage(
status='NotSupportedMessageFormat'),
timeout=5)
message['message']['format'] = 'UTF8'
# Test unsupported state
message['state'] = 'Idle'
await chargepoint_with_pm.set_display_message_req(message=message, custom_data=None)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "SetDisplayMessage",
call_result201.SetDisplayMessage(
status='NotSupportedState'),
timeout=5)
message['state'] = 'Charging'
# Test unknown transaction
message['transactionId'] = '12345'
await chargepoint_with_pm.set_display_message_req(message=message, custom_data=None)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "SetDisplayMessage",
call_result201.SetDisplayMessage(
status='UnknownTransaction'),
timeout=5)
@pytest.mark.asyncio
@pytest.mark.probe_module
@pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceDisplayMessageConfigurationAdjustment())
async def test_set_display_message_with_transaction(self, central_system: CentralSystem,
test_controller: TestController,
test_utility: TestUtility, test_config: OcppTestConfiguration,
probe_module):
probe_module_mock_fn = Mock()
probe_module_mock_fn.return_value = {
"status": "Accepted"
}
probe_module.implement_command("ProbeModuleDisplayMessage", "set_display_message",
probe_module_mock_fn)
probe_module.implement_command("ProbeModuleDisplayMessage", "get_display_messages",
probe_module_mock_fn)
probe_module.implement_command("ProbeModuleDisplayMessage", "clear_display_message",
probe_module_mock_fn)
probe_module.start()
await probe_module.wait_to_be_ready()
chargepoint_with_pm = await central_system.wait_for_chargepoint()
transaction_id = await self.start_transaction(test_controller, test_utility, chargepoint_with_pm)
message = {'transactionId': transaction_id, 'id': 1, 'priority': 'NormalCycle',
'message': {'format': 'UTF8', 'language': 'en',
'content': 'This is a display message'}}
await chargepoint_with_pm.set_display_message_req(message=message, custom_data=None)
# Display message should have received a message with the current price information
data_received = {
'request': [{'id': 1, 'identifier_id': transaction_id, 'identifier_type': 'TransactionId',
'message': {'content': 'This is a display message', 'format': 'UTF8', 'language': 'en'},
'priority': 'NormalCycle'}]
}
probe_module_mock_fn.assert_called_once_with(data_received)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "SetDisplayMessage",
call_result201.SetDisplayMessage(
status='Accepted'),
timeout=5)
# Test unknown transaction
message['transactionId'] = '12345'
await chargepoint_with_pm.set_display_message_req(message=message, custom_data=None)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "SetDisplayMessage",
call_result201.SetDisplayMessage(
status='UnknownTransaction'),
timeout=5)
@pytest.mark.asyncio
@pytest.mark.probe_module
@pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceDisplayMessageConfigurationAdjustment())
async def test_get_display_messages(self, central_system: CentralSystem, test_controller: TestController,
test_utility: TestUtility, test_config: OcppTestConfiguration, probe_module):
probe_module_mock_fn = Mock()
probe_module.implement_command("ProbeModuleDisplayMessage", "set_display_message",
probe_module_mock_fn)
probe_module.implement_command("ProbeModuleDisplayMessage", "get_display_messages",
probe_module_mock_fn)
probe_module.implement_command("ProbeModuleDisplayMessage", "clear_display_message",
probe_module_mock_fn)
probe_module.start()
await probe_module.wait_to_be_ready()
chargepoint_with_pm = await central_system.wait_for_chargepoint()
# No messages should return 'unknown'
probe_module_mock_fn.return_value = {
"messages": []
}
await chargepoint_with_pm.get_display_nessages_req(request_id=1)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "GetDisplayMessage",
call_result201.GetDisplayMessages(
status='Unknown'),
timeout=5)
# At least one message should return 'accepted'
probe_module_mock_fn.return_value = {
"messages": [
{'id': 1, 'message': {'content': 'This is a display message', 'format': 'UTF8', 'language': 'en'},
'priority': 'InFront'}
]
}
await chargepoint_with_pm.get_display_nessages_req(id=[1], request_id=1)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "GetDisplayMessage",
call_result201.GetDisplayMessages(
status='Accepted'),
timeout=5)
assert await \
wait_for_and_validate(test_utility, chargepoint_with_pm, "NotifyDisplayMessages",
call201.NotifyDisplayMessages(request_id=1,
message_info=[{"id": 1,
"message": {
"content": "This is a "
"display message",
"format": "UTF8",
"language": "en"},
"priority": "InFront"}]))
# Return multiple messages
probe_module_mock_fn.return_value = {
"messages": [
{'id': 1, 'message': {'content': 'This is a display message', 'format': 'UTF8', 'language': 'en'},
'priority': 'InFront'},
{'id': 2, 'message': {'content': 'This is a display message 2', 'format': 'UTF8', 'language': 'en'},
'priority': 'NormalCycle'}
]
}
await chargepoint_with_pm.get_display_nessages_req(request_id=1)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "GetDisplayMessage",
call_result201.GetDisplayMessages(
status='Accepted'),
timeout=5)
assert await \
wait_for_and_validate(test_utility, chargepoint_with_pm, "NotifyDisplayMessages",
call201.NotifyDisplayMessages(request_id=1,
message_info=[{"id": 1,
"message": {
"content": "This is a "
"display message",
"format": "UTF8",
"language": "en"},
"priority": "InFront"}, {"id": 2,
"message": {
"content": "This is a "
"display message 2",
"format": "UTF8",
"language": "en"},
"priority": "NormalCycle"}
]))
@pytest.mark.asyncio
@pytest.mark.probe_module
@pytest.mark.everest_config_adaptions(ProbeModuleCostAndPriceDisplayMessageConfigurationAdjustment())
async def test_clear_display_messages(self, central_system: CentralSystem, test_controller: TestController,
test_utility: TestUtility, test_config: OcppTestConfiguration, probe_module):
probe_module_mock_fn = Mock()
probe_module.implement_command("ProbeModuleDisplayMessage", "set_display_message",
probe_module_mock_fn)
probe_module.implement_command("ProbeModuleDisplayMessage", "get_display_messages",
probe_module_mock_fn)
probe_module.implement_command("ProbeModuleDisplayMessage", "clear_display_message",
probe_module_mock_fn)
probe_module.start()
await probe_module.wait_to_be_ready()
chargepoint_with_pm = await central_system.wait_for_chargepoint()
# Clear display message is accepted
probe_module_mock_fn.return_value = {
"status": "Accepted"
}
await chargepoint_with_pm.clear_display_message_req(id=1)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "ClearDisplayMessage",
call_result201.ClearDisplayMessage(
status='Accepted'),
timeout=5)
# Clear display message returns unknown
probe_module_mock_fn.return_value = {
"status": "Unknown"
}
await chargepoint_with_pm.clear_display_message_req(id=1)
assert await wait_for_and_validate(test_utility, chargepoint_with_pm, "ClearDisplayMessage",
call_result201.ClearDisplayMessage(
status='Unknown'),
timeout=5)

View File

@@ -0,0 +1,285 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
import pytest
import asyncio
from copy import deepcopy
from typing import Dict
from datetime import datetime
from everest.testing.ocpp_utils.charge_point_utils import (
wait_for_and_validate,
TestUtility,
)
from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201
from everest.testing.ocpp_utils.fixtures import central_system_v201, CentralSystem
from everest.testing.core_utils import EverestConfigAdjustmentStrategy
from everest.testing.core_utils.controller.test_controller_interface import (
TestController,
)
from validations import validate_notify_report_data_201
from ocpp.v201 import call
from ocpp.v201 import call_result as call_result
from ocpp.v201.datatypes import *
from ocpp.v201.enums import (
GetVariableStatusEnumType,
AttributeEnumType,
SetVariableStatusEnumType,
ReportBaseEnumType,
MutabilityEnumType,
DataEnumType,
GenericDeviceModelStatusEnumType,
)
class OCPP201ModuleAccessConfigurationAdjustment(EverestConfigAdjustmentStrategy):
def adjust_everest_configuration(self, everest_config: Dict):
adjusted_config = deepcopy(everest_config)
adjusted_config["active_modules"]["ocpp"]["access"] = {
"config": {
"allow_global_read": True,
"allow_global_write": True,
"allow_set_read_only": True,
}
}
return adjusted_config
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
@pytest.mark.everest_config_adaptions(OCPP201ModuleAccessConfigurationAdjustment())
async def test_set_get_variable_01(
charge_point_v201: ChargePoint201,
test_utility: TestUtility,
test_controller: TestController,
central_system_v201: CentralSystem,
):
"""Test setting and getting variables in the Everest Device Model.
This test gets the initial values of the `session_logging` and `session_logging_path` variables,
sets new values for them, and then verifies that the values are correctly set after a restart .
"""
await charge_point_v201.get_variables_req(
get_variable_data=[
GetVariableDataType(
component=ComponentType(name="EvseManager", instance="connector_1"),
variable=VariableType(name="session_logging"),
attribute_type=AttributeEnumType.actual,
),
GetVariableDataType(
component=ComponentType(name="EvseManager", instance="connector_1"),
variable=VariableType(name="session_logging_path"),
attribute_type=AttributeEnumType.actual,
),
]
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"GetVariables",
call_result.GetVariables(
get_variable_result=[
GetVariableResultType(
component=ComponentType(name="EvseManager", instance="connector_1"),
variable=VariableType(name="session_logging"),
attribute_status=GetVariableStatusEnumType.accepted,
attribute_type=AttributeEnumType.actual,
attribute_value="true",
),
GetVariableResultType(
component=ComponentType(name="EvseManager", instance="connector_1"),
variable=VariableType(name="session_logging_path"),
attribute_status=GetVariableStatusEnumType.accepted,
attribute_type=AttributeEnumType.actual,
attribute_value="/tmp",
),
]
),
)
await charge_point_v201.set_variables_req(
set_variable_data=[
SetVariableDataType(
attribute_value="false",
attribute_type=AttributeEnumType.actual,
component=ComponentType(name="EvseManager", instance="connector_1"),
variable=VariableType(name="session_logging"),
),
SetVariableDataType(
attribute_value="/another/path",
attribute_type=AttributeEnumType.actual,
component=ComponentType(name="EvseManager", instance="connector_1"),
variable=VariableType(name="session_logging_path"),
),
]
)
await wait_for_and_validate(
test_utility,
charge_point_v201,
"SetVariables",
call_result.SetVariables(
set_variable_result=[
SetVariableResultType(
attribute_status=SetVariableStatusEnumType.reboot_required,
component=ComponentType(name="EvseManager", instance="connector_1"),
variable=VariableType(name="session_logging"),
attribute_type=AttributeEnumType.actual,
),
SetVariableResultType(
attribute_status=SetVariableStatusEnumType.reboot_required,
component=ComponentType(name="EvseManager", instance="connector_1"),
variable=VariableType(name="session_logging_path"),
attribute_type=AttributeEnumType.actual,
),
]
),
)
# Restart the charge point to apply the changes
test_controller.stop()
await asyncio.sleep(1)
test_controller.start()
charge_point_v201 = await central_system_v201.wait_for_chargepoint(
wait_for_bootnotification=True
)
# After restart, the values should be set to the new values
await charge_point_v201.get_variables_req(
get_variable_data=[
GetVariableDataType(
component=ComponentType(name="EvseManager", instance="connector_1"),
variable=VariableType(name="session_logging"),
attribute_type=AttributeEnumType.actual,
),
GetVariableDataType(
component=ComponentType(name="EvseManager", instance="connector_1"),
variable=VariableType(name="session_logging_path"),
attribute_type=AttributeEnumType.actual,
),
]
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"GetVariables",
call_result.GetVariables(
get_variable_result=[
GetVariableResultType(
component=ComponentType(name="EvseManager", instance="connector_1"),
variable=VariableType(name="session_logging"),
attribute_status=GetVariableStatusEnumType.accepted,
attribute_type=AttributeEnumType.actual,
attribute_value="false",
),
GetVariableResultType(
component=ComponentType(name="EvseManager", instance="connector_1"),
variable=VariableType(name="session_logging_path"),
attribute_status=GetVariableStatusEnumType.accepted,
attribute_type=AttributeEnumType.actual,
attribute_value="/another/path",
),
]
),
)
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
async def test_set_get_variable_02(
charge_point_v201: ChargePoint201,
test_utility: TestUtility,
):
"""Test getting variables when access to the Everest Device Model is not allowed (pytest marker is not used here).
This test attempts to get the `session_logging` and `session_logging_path` variables,
which should return an unknown component status since access is not allowed.
"""
await charge_point_v201.get_variables_req(
get_variable_data=[
GetVariableDataType(
component=ComponentType(name="EvseManager", instance="connector_1"),
variable=VariableType(name="session_logging"),
attribute_type=AttributeEnumType.actual,
),
GetVariableDataType(
component=ComponentType(name="EvseManager", instance="connector_1"),
variable=VariableType(name="session_logging_path"),
attribute_type=AttributeEnumType.actual,
),
]
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"GetVariables",
call_result.GetVariables(
get_variable_result=[
GetVariableResultType(
component=ComponentType(name="EvseManager", instance="connector_1"),
variable=VariableType(name="session_logging"),
attribute_status=GetVariableStatusEnumType.unknown_component,
attribute_type=AttributeEnumType.actual,
),
GetVariableResultType(
component=ComponentType(name="EvseManager", instance="connector_1"),
variable=VariableType(name="session_logging_path"),
attribute_status=GetVariableStatusEnumType.unknown_component,
attribute_type=AttributeEnumType.actual,
),
]
),
)
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
@pytest.mark.everest_config_adaptions(OCPP201ModuleAccessConfigurationAdjustment())
async def test_get_base_report(
charge_point_v201: ChargePoint201, test_utility: TestUtility
):
"""Test getting a base report from the Everest Device Model.
This test requests a full inventory report and verifies that at least one expected report data is returned.
It checks for the `csms_ca_bundle` variable in the `EvseSecurity` component.
"""
await charge_point_v201.get_base_report_req(
request_id=1, report_base=ReportBaseEnumType.full_inventory
)
await wait_for_and_validate(
test_utility,
charge_point_v201,
"GetBaseReport",
call_result.GetBaseReport(status=GenericDeviceModelStatusEnumType.accepted),
)
exp_single_report_data = ReportDataType(
component=ComponentType(name="EvseSecurity", instance="evse_security"),
variable=VariableType(name="csms_ca_bundle"),
variable_attribute=VariableAttributeType(
type=AttributeEnumType.actual,
value="ca/csms/CSMS_ROOT_CA.pem",
mutability=MutabilityEnumType.read_only,
),
variable_characteristics=VariableCharacteristicsType(
data_type=DataEnumType.string, supports_monitoring=False
),
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"NotifyReport",
call.NotifyReport(
request_id=1,
generated_at=datetime.now().isoformat(),
seq_no=0,
report_data=[exp_single_report_data],
),
validate_notify_report_data_201,
)

View File

@@ -0,0 +1,127 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
import logging
import pytest
from everest.testing.ocpp_utils.central_system import CentralSystem
from everest.testing.core_utils.controller.test_controller_interface import TestController
from ocpp.v201 import call as call201
# Needs to be before the datatypes below since it overrides the v201 Action enum with the v16 one
from test_sets.everest_test_utils import *
from everest.testing.ocpp_utils.charge_point_utils import (
wait_for_and_validate,
TestUtility,
)
from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201
from everest_test_utils import *
from everest.testing.ocpp_utils.fixtures import *
log = logging.getLogger("iso15118ExtensionsTest")
def validate_notify_ev_charging_needs(meta_data, msg, exp_payload):
return True
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
@pytest.mark.skip(
"Extension tests currently still have an issue with the evMaxCurrent property which was" \
" defined as an integer in OCPP2.0.1 and decimal in OCPP2.1")
@pytest.mark.xdist_group(name="ISO15118")
class TestIso15118ExtenstionsOcppIntegration:
@pytest.mark.everest_core_config("everest-config-ocpp201-sil-dc-d20.yaml")
async def test_charge_params_sent_dc_d20(
self,
request,
central_system: CentralSystem,
charge_point: ChargePoint201,
test_controller: TestController,
test_config,
test_utility: TestUtility,
):
test_controller.plug_in_dc_iso()
assert await wait_for_and_validate(
test_utility,
charge_point,
"NotifyEVChargingNeeds",
call201.NotifyEVChargingNeeds(
evse_id="1",
charging_needs=None,
custom_data=None
),
validate_notify_ev_charging_needs,
)
test_utility.messages.clear()
assert await wait_for_and_validate(
test_utility,
charge_point,
"TransactionEvent",
{
"eventType": "Updated",
"triggerReason": "ChargingStateChanged",
"transactionInfo": {"chargingState": "Charging"},
},
)
test_controller.plug_out_iso()
assert await wait_for_and_validate(
test_utility,
charge_point,
"StatusNotification",
{"connectorStatus": "Available", "evseId": 1},
)
@pytest.mark.everest_core_config("everest-config-ocpp201-sil-dc-d2.yaml")
async def test_charge_params_sent_dc_evsev2g_d2(
self,
request,
central_system: CentralSystem,
charge_point: ChargePoint201,
test_controller: TestController,
test_config,
test_utility: TestUtility,
):
test_controller.plug_in_dc_iso()
assert await wait_for_and_validate(
test_utility,
charge_point,
"NotifyEVChargingNeeds",
call201.NotifyEVChargingNeeds(
evse_id="1",
charging_needs=None,
custom_data=None
),
validate_notify_ev_charging_needs,
)
assert await wait_for_and_validate(
test_utility,
charge_point,
"TransactionEvent",
{
"eventType": "Updated",
"triggerReason": "ChargingStateChanged",
"transactionInfo": {"chargingState": "Charging"},
},
)
test_utility.messages.clear()
test_controller.plug_out_iso()
assert await wait_for_and_validate(
test_utility,
charge_point,
"StatusNotification",
{"connectorStatus": "Available", "evseId": 1},
)

View File

@@ -0,0 +1,860 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
# fmt: off
import pytest
import logging
from everest.testing.core_utils.controller.test_controller_interface import TestController
from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility, ValidationMode
from everest.testing.ocpp_utils.fixtures import *
from everest_test_utils import *
from ocpp.v201.enums import (IdTokenEnumType as IdTokenTypeEnum)
from ocpp.v201.enums import *
from ocpp.v201.datatypes import *
from ocpp.v201 import call as call201
from ocpp.v201 import call_result as call_result201
from ocpp.routing import on, create_route_map
from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201
# fmt: on
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
async def test_D01_D02(
charge_point_v201: ChargePoint201,
central_system_v201: CentralSystem,
test_controller: TestController,
test_utility: TestUtility,
):
id_token_123 = IdTokenType(id_token="123", type=IdTokenTypeEnum.iso14443)
id_token_124 = IdTokenType(id_token="124", type=IdTokenTypeEnum.iso14443)
id_token_125 = IdTokenType(id_token="125", type=IdTokenTypeEnum.iso14443)
id_token_accepted = IdTokenInfoType(
status=AuthorizationStatusEnumType.accepted)
id_token_blocked = IdTokenInfoType(
status=AuthorizationStatusEnumType.blocked)
# D02.FR.01
async def check_list_version(expected_version: int):
r: call_result201.GetLocalListVersion = (
await charge_point_v201.get_local_list_version()
)
assert r.version_number == expected_version
# D01.FR.12
async def check_list_size(expected_size: int):
r: call_result201.GetVariables = (
await charge_point_v201.get_config_variables_req(
"LocalAuthListCtrlr", "Entries"
)
)
get_variables_result: GetVariableResultType = GetVariableResultType(
**r.get_variable_result[0]
)
assert get_variables_result.attribute_status == GetVariableStatusEnumType.accepted
assert get_variables_result.attribute_value == str(expected_size)
# LocalAuthListCtrlr needs to be avaialable
r: call_result201.GetVariables = (
await charge_point_v201.get_config_variables_req(
"LocalAuthListCtrlr", "Available"
)
)
get_variables_result: GetVariableResultType = GetVariableResultType(
**r.get_variable_result[0]
)
assert get_variables_result.attribute_status == GetVariableStatusEnumType.accepted
assert get_variables_result.attribute_value == "true"
# Enable local list
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"LocalAuthListCtrlr", "Enabled", "true"
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
# D02.FR.02 LocalAuthListEnabled is true amd CSMS has not sent any update
await check_list_version(0)
await check_list_size(0)
# D01.FR.18 VersionNumber shall be greater than 0 (we fail otherwise)
r: call_result201.SendLocalList = (
await charge_point_v201.send_local_list_req(
version_number=0, update_type=UpdateEnumType.full
)
)
assert r.status == SendLocalListStatusEnumType.failed
r: call_result201.SendLocalList = (
await charge_point_v201.send_local_list_req(
version_number=0, update_type=UpdateEnumType.differential
)
)
assert r.status == SendLocalListStatusEnumType.failed
await check_list_version(0)
await check_list_size(0)
# D01.FR.01
# D01.FR.02
# Add first list version
r: call_result201.SendLocalList = (
await charge_point_v201.send_local_list_req(
version_number=10,
update_type=UpdateEnumType.full,
local_authorization_list=[
AuthorizationData(
id_token=id_token_123, id_token_info=id_token_accepted
),
AuthorizationData(
id_token=id_token_124, id_token_info=id_token_accepted
),
],
)
)
assert r.status == SendLocalListStatusEnumType.accepted
await check_list_version(10)
await check_list_size(2)
# D01.FR.04
r: call_result201.SendLocalList = (
await charge_point_v201.send_local_list_req(
version_number=20, update_type=UpdateEnumType.full
)
)
assert r.status == SendLocalListStatusEnumType.accepted
await check_list_version(20)
await check_list_size(0)
r: call_result201.SendLocalList = (
await charge_point_v201.send_local_list_req(
version_number=12,
update_type=UpdateEnumType.full,
local_authorization_list=[
AuthorizationData(
id_token=id_token_123, id_token_info=id_token_accepted
),
AuthorizationData(
id_token=id_token_124, id_token_info=id_token_accepted
),
AuthorizationData(
id_token=id_token_125, id_token_info=id_token_blocked
),
],
)
)
assert r.status == SendLocalListStatusEnumType.accepted
await check_list_version(12)
await check_list_size(3)
# D01.FR.05
r: call_result201.SendLocalList = (
await charge_point_v201.send_local_list_req(
version_number=15, update_type=UpdateEnumType.differential
)
)
assert r.status == SendLocalListStatusEnumType.accepted
await check_list_version(15)
await check_list_size(3)
# D01.FR.06
r: call_result201.SendLocalList = (
await charge_point_v201.send_local_list_req(
version_number=25,
update_type=UpdateEnumType.full,
local_authorization_list=[
AuthorizationData(
id_token=id_token_123, id_token_info=id_token_accepted
),
AuthorizationData(
id_token=id_token_124, id_token_info=id_token_accepted
),
AuthorizationData(
id_token=id_token_124, id_token_info=id_token_accepted
),
],
)
)
assert r.status == SendLocalListStatusEnumType.failed
await check_list_version(15)
await check_list_size(3)
# idTokenInfo is required when UpdateEnumType is full
r: call_result201.SendLocalList = (
await charge_point_v201.send_local_list_req(
version_number=3,
update_type=UpdateEnumType.full,
local_authorization_list=[
AuthorizationData(
id_token=id_token_123, id_token_info=id_token_accepted
),
AuthorizationData(id_token=id_token_124),
],
)
)
assert r.status == SendLocalListStatusEnumType.failed
await check_list_version(15)
await check_list_size(3)
# D01.FR.15
r: call_result201.SendLocalList = (
await charge_point_v201.send_local_list_req(
version_number=25,
update_type=UpdateEnumType.full,
local_authorization_list=[
AuthorizationData(
id_token=id_token_123, id_token_info=id_token_accepted
),
AuthorizationData(
id_token=id_token_124, id_token_info=id_token_accepted
),
],
)
)
assert r.status == SendLocalListStatusEnumType.accepted
await check_list_version(25)
await check_list_size(2)
# D01.FR.16 Update
r: call_result201.SendLocalList = (
await charge_point_v201.send_local_list_req(
version_number=26,
update_type=UpdateEnumType.differential,
local_authorization_list=[
AuthorizationData(id_token=id_token_123,
id_token_info=id_token_blocked)
],
)
)
assert r.status == SendLocalListStatusEnumType.accepted
await check_list_version(26)
await check_list_size(2)
# D01.FR.16 Add
r: call_result201.SendLocalList = (
await charge_point_v201.send_local_list_req(
version_number=27,
update_type=UpdateEnumType.differential,
local_authorization_list=[
AuthorizationData(
id_token=id_token_125, id_token_info=id_token_accepted
)
],
)
)
assert r.status == SendLocalListStatusEnumType.accepted
await check_list_version(27)
await check_list_size(3)
# D01.FR.17 Remove if empty idTokenInfo
r: call_result201.SendLocalList = (
await charge_point_v201.send_local_list_req(
version_number=28,
update_type=UpdateEnumType.differential,
local_authorization_list=[
AuthorizationData(id_token=id_token_123)],
)
)
assert r.status == SendLocalListStatusEnumType.accepted
await check_list_version(28)
await check_list_size(2)
# D01.FR.19 Smaller or equal version_number should be ignored with status set to VersionMismatch
r: call_result201.SendLocalList = (
await charge_point_v201.send_local_list_req(
version_number=27,
update_type=UpdateEnumType.differential,
local_authorization_list=[
AuthorizationData(id_token=id_token_125)],
)
)
assert r.status == SendLocalListStatusEnumType.version_mismatch
r: call_result201.SendLocalList = (
await charge_point_v201.send_local_list_req(
version_number=28,
update_type=UpdateEnumType.differential,
local_authorization_list=[
AuthorizationData(id_token=id_token_125)],
)
)
assert r.status == SendLocalListStatusEnumType.version_mismatch
await check_list_version(28)
await check_list_size(2)
# D01.FR.13
# Disable auth list again to check if version returns to 0
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"LocalAuthListCtrlr", "Enabled", "false"
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
# D02.FR.03: Always return 0 when LocalAuthListEnabled is false
await check_list_version(0)
# Disabled so should not be able to send list
r: call_result201.SendLocalList = (
await charge_point_v201.send_local_list_req(
version_number=1,
update_type=UpdateEnumType.full,
local_authorization_list=[
AuthorizationData(
id_token=id_token_123, id_token_info=id_token_accepted
),
AuthorizationData(
id_token=id_token_124, id_token_info=id_token_accepted
),
],
)
)
assert r.status == SendLocalListStatusEnumType.failed
async def prepare_auth_cache(
charge_point_v201: ChargePoint201,
central_system_v201: CentralSystem,
test_controller: TestController,
test_utility: TestUtility,
accepted_tags: [],
rejected_tags: [],
):
# Prepare the cache with valid and invalid tags
def get_token_info(token: str):
if token in accepted_tags:
return IdTokenInfoType(status=AuthorizationStatusEnumType.accepted)
else:
return IdTokenInfoType(status=AuthorizationStatusEnumType.blocked)
@on(Action.authorize)
def on_authorize(**kwargs):
msg = call201.Authorize(**kwargs)
msg_token = IdTokenType(**msg.id_token)
return call_result201.Authorize(
id_token_info=get_token_info(msg_token.id_token)
)
setattr(charge_point_v201, "on_authorize", on_authorize)
central_system_v201.chargepoint.route_map = create_route_map(
central_system_v201.chargepoint
)
test_utility.validation_mode = ValidationMode.STRICT
for tag in accepted_tags:
test_controller.swipe(tag)
test_controller.plug_in()
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Started"},
)
test_controller.swipe(tag)
test_controller.plug_out()
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"connectorStatus": "Available", "evseId": 1},
)
for tag in rejected_tags:
test_controller.swipe(tag)
assert await wait_for_and_validate(
test_utility, charge_point_v201, "Authorize", {
"idToken": {"idToken": tag}}
)
test_utility.validation_mode = ValidationMode.EASY
test_utility.messages.clear()
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
async def test_C13(
charge_point_v201: ChargePoint201,
central_system_v201: CentralSystem,
test_controller: TestController,
test_utility: TestUtility,
):
# LocalAuthListCtrlr needs to be avaialable
r: call_result201.GetVariables = (
await charge_point_v201.get_config_variables_req(
"LocalAuthListCtrlr", "Available"
)
)
get_variables_result: GetVariableResultType = GetVariableResultType(
**r.get_variable_result[0]
)
assert get_variables_result.attribute_status == GetVariableStatusEnumType.accepted
assert get_variables_result.attribute_value == "true"
# Enable local list
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"LocalAuthListCtrlr", "Enabled", "true"
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
# Set OfflineThreshold
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"OCPPCommCtrlr", "OfflineThreshold", "2"
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
# Disable offline tx for unknown id
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"AuthCtrlr", "OfflineTxForUnknownIdEnabled", "false"
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
id_token_123 = IdTokenType(id_token="123", type=IdTokenTypeEnum.iso14443)
id_token_124 = IdTokenType(id_token="124", type=IdTokenTypeEnum.iso14443)
id_token_125 = IdTokenType(id_token="125", type=IdTokenTypeEnum.iso14443)
id_token_accepted = IdTokenInfoType(
status=AuthorizationStatusEnumType.accepted)
id_token_blocked = IdTokenInfoType(
status=AuthorizationStatusEnumType.blocked)
async def check_list_version(expected_version: int):
r: call_result201.GetLocalListVersion = (
await charge_point_v201.get_local_list_version()
)
assert r.version_number == expected_version
async def check_list_size(expected_size: int):
r: call_result201.GetVariables = (
await charge_point_v201.get_config_variables_req(
"LocalAuthListCtrlr", "Entries"
)
)
get_variables_result: GetVariableResultType = GetVariableResultType(
**r.get_variable_result[0]
)
assert get_variables_result.attribute_status == GetVariableStatusEnumType.accepted
assert get_variables_result.attribute_value == str(expected_size)
await prepare_auth_cache(
charge_point_v201=charge_point_v201,
central_system_v201=central_system_v201,
test_controller=test_controller,
test_utility=test_utility,
accepted_tags=[id_token_123.id_token, id_token_125.id_token],
rejected_tags=[id_token_124.id_token],
)
# Add first list version
r: call_result201.SendLocalList = (
await charge_point_v201.send_local_list_req(
version_number=10,
update_type=UpdateEnumType.full,
local_authorization_list=[
AuthorizationData(
id_token=id_token_123, id_token_info=id_token_accepted
),
AuthorizationData(
id_token=id_token_124, id_token_info=id_token_accepted
),
AuthorizationData(
id_token=id_token_125, id_token_info=id_token_blocked
),
],
)
)
assert r.status == SendLocalListStatusEnumType.accepted
await check_list_version(10)
await check_list_size(3)
test_utility.forbidden_actions.append("Authorize")
# C13.FR.02
# C13.FR.03
# Valid token in the local list may be authorized offline
# Check AuthList: Valid, Cache: Invalid
# Expected result: Start session
logging.info("disconnect the ws connection...")
test_controller.disconnect_websocket()
await asyncio.sleep(2)
test_controller.swipe(id_token_123.id_token)
test_controller.plug_in()
await asyncio.sleep(2)
logging.info("connecting the ws connection")
test_controller.connect_websocket()
# wait for reconnect
charge_point_v201 = await central_system_v201.wait_for_chargepoint(
wait_for_bootnotification=False
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{
"eventType": "Started",
"idToken": {"idToken": id_token_123.id_token, "type": "ISO14443"},
},
)
test_utility.messages.clear()
test_controller.swipe(id_token_123.id_token)
test_controller.plug_out()
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{
"eventType": "Ended"
},
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"connectorStatus": "Available", "evseId": 1},
)
test_utility.messages.clear()
# C13.FR.01
# Invalid token in local list may not be authorized
# Check AuthList: Invalid, Cache: Valid
# Expected result: No session started
test_utility.forbidden_actions.append("TransactionEvent")
await asyncio.sleep(2)
logging.info("disconnect the ws connection...")
test_controller.disconnect_websocket()
await asyncio.sleep(2)
test_controller.swipe(id_token_125.id_token)
test_controller.plug_in()
await asyncio.sleep(5)
logging.info("connecting the ws connection")
test_controller.connect_websocket()
# wait for reconnect
charge_point_v201 = await central_system_v201.wait_for_chargepoint(
wait_for_bootnotification=False
)
test_controller.plug_out()
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"connectorStatus": "Available", "evseId": 1},
)
test_utility.forbidden_actions.remove("TransactionEvent")
# C13.FR.04
# With OfflineTxForUnknownIdEnabled == true
# Invalid token in local list may not be authorized
# Unkown token may be authorized
# See errata for case C13.FR.04
# Enable offline tx for unknown id
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"AuthCtrlr", "OfflineTxForUnknownIdEnabled", "true"
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
logging.info("disconnect the ws connection...")
test_controller.disconnect_websocket()
await asyncio.sleep(2)
test_controller.swipe(id_token_125.id_token)
test_controller.swipe("unknown")
test_controller.plug_in()
await asyncio.sleep(5)
logging.info("connecting the ws connection")
test_controller.connect_websocket()
# wait for reconnect
charge_point_v201 = await central_system_v201.wait_for_chargepoint(
wait_for_bootnotification=False
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Started", "idToken": {
"idToken": "unknown", "type": "ISO14443"}},
)
test_controller.plug_out()
test_controller.swipe("unknown")
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"connectorStatus": "Available", "evseId": 1},
)
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
async def test_C14(
charge_point_v201: ChargePoint201,
central_system_v201: CentralSystem,
test_controller: TestController,
test_utility: TestUtility,
):
# LocalAuthListCtrlr needs to be avaialable
r: call_result201.GetVariables = (
await charge_point_v201.get_config_variables_req(
"LocalAuthListCtrlr", "Available"
)
)
get_variables_result: GetVariableResultType = GetVariableResultType(
**r.get_variable_result[0]
)
assert get_variables_result.attribute_status == GetVariableStatusEnumType.accepted
assert get_variables_result.attribute_value == "true"
# AuthCacheCtrlr needs to be avaialable
r: call_result201.GetVariables = (
await charge_point_v201.get_config_variables_req("AuthCacheCtrlr", "Available")
)
get_variables_result: GetVariableResultType = GetVariableResultType(
**r.get_variable_result[0]
)
assert get_variables_result.attribute_status == GetVariableStatusEnumType.accepted
assert get_variables_result.attribute_value == "true"
# Enable local list
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"LocalAuthListCtrlr", "Enabled", "true"
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
# Enable AuthCacheCtrlr
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"AuthCacheCtrlr", "Enabled", "true"
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
# Disable offline tx for unknown id
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"AuthCtrlr", "OfflineTxForUnknownIdEnabled", "false"
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
id_token_123 = IdTokenType(id_token="123", type=IdTokenTypeEnum.iso14443)
id_token_124 = IdTokenType(id_token="124", type=IdTokenTypeEnum.iso14443)
id_token_accepted = IdTokenInfoType(
status=AuthorizationStatusEnumType.accepted)
id_token_blocked = IdTokenInfoType(
status=AuthorizationStatusEnumType.blocked)
async def check_list_version(expected_version: int):
r: call_result201.GetLocalListVersion = (
await charge_point_v201.get_local_list_version()
)
assert r.version_number == expected_version
async def check_list_size(expected_size: int):
r: call_result201.GetVariables = (
await charge_point_v201.get_config_variables_req(
"LocalAuthListCtrlr", "Entries"
)
)
get_variables_result: GetVariableResultType = GetVariableResultType(
**r.get_variable_result[0]
)
assert get_variables_result.attribute_status == GetVariableStatusEnumType.accepted
assert get_variables_result.attribute_value == str(expected_size)
await prepare_auth_cache(
charge_point_v201=charge_point_v201,
central_system_v201=central_system_v201,
test_controller=test_controller,
test_utility=test_utility,
accepted_tags=[id_token_123.id_token],
rejected_tags=[id_token_124.id_token],
)
# Add first list version
r: call_result201.SendLocalList = (
await charge_point_v201.send_local_list_req(
version_number=10,
update_type=UpdateEnumType.full,
local_authorization_list=[
AuthorizationData(
id_token=id_token_124, id_token_info=id_token_accepted
),
AuthorizationData(
id_token=id_token_123, id_token_info=id_token_blocked
),
],
)
)
assert r.status == SendLocalListStatusEnumType.accepted
await check_list_version(10)
await check_list_size(2)
await asyncio.sleep(1)
# C14.FR.02
# Check AuthList: Valid, Cache: Invalid
# Expected result: Start session without authorizeReq
test_utility.messages.clear()
test_utility.forbidden_actions.append("Authorize")
test_controller.swipe(id_token_124.id_token)
test_controller.plug_in()
test_utility.validation_mode = ValidationMode.STRICT
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{
"eventType": "Started",
"idToken": {"idToken": id_token_124.id_token, "type": "ISO14443"},
},
)
test_utility.validation_mode = ValidationMode.EASY
await asyncio.sleep(1)
test_controller.swipe(id_token_124.id_token)
test_controller.plug_out()
test_utility.validation_mode = ValidationMode.STRICT
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"connectorStatus": "Available", "evseId": 1},
)
test_utility.validation_mode = ValidationMode.EASY
test_utility.forbidden_actions.remove("Authorize")
# C14.FR.01
# C14.FR.03
# Check AuthList: Invalid, Cache: Valid
# Expected result: Send autorize request
await asyncio.sleep(1)
test_utility.messages.clear()
test_controller.swipe(id_token_123.id_token)
test_controller.plug_in()
test_utility.validation_mode = ValidationMode.STRICT
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"Authorize",
call201.Authorize(id_token=id_token_123),
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{
"eventType": "Started",
"idToken": {"idToken": id_token_123.id_token, "type": "ISO14443"},
},
)
test_utility.validation_mode = ValidationMode.EASY
test_controller.swipe(id_token_123.id_token)
test_controller.plug_out()
test_utility.validation_mode = ValidationMode.STRICT
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"connectorStatus": "Available", "evseId": 1},
)
test_utility.validation_mode = ValidationMode.EASY

View File

@@ -0,0 +1,183 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
# fmt: off
import pytest
from datetime import datetime
import logging
from everest.testing.core_utils.controller.test_controller_interface import TestController
from validations import *
from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility
from everest.testing.ocpp_utils.fixtures import *
from everest_test_utils import * # Needs to be before the datatypes below since it overrides the v201 Action enum with the v16 one
from ocpp.v201.enums import (IdTokenEnumType as IdTokenTypeEnum, SetVariableStatusEnumType, ConnectorStatusEnumType,GetVariableStatusEnumType)
from ocpp.v201.datatypes import *
from ocpp.v201 import call as call201
from ocpp.v201 import call_result as call_result201
# fmt: on
log = logging.getLogger("meterValues")
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
async def test_J01_19(
central_system_v201: CentralSystem,
test_controller: TestController,
test_utility: TestUtility,
):
"""
J01.FR.19
...
"""
# prepare data for the test
evse_id1 = 1
connector_id = 1
evse_id2 = 2
# make an unknown IdToken
id_tokenJ01 = IdTokenType(
id_token="8BADF00D", type=IdTokenTypeEnum.iso14443)
log.info(
"##################### J01.FR.19: Sending Meter Values not related to a transaction #################"
)
test_utility.messages.clear()
test_controller.start()
charge_point_v201 = await central_system_v201.wait_for_chargepoint(
wait_for_bootnotification=True
)
# expect StatusNotification with status available
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
call201.StatusNotification(
datetime.now().isoformat(),
ConnectorStatusEnumType.available,
evse_id=evse_id1,
connector_id=connector_id,
),
validate_status_notification_201,
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
call201.StatusNotification(
datetime.now().isoformat(),
ConnectorStatusEnumType.available,
evse_id=evse_id2,
connector_id=connector_id,
),
validate_status_notification_201,
)
# Configure AlignedDataInterval
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"AlignedDataCtrlr", "Interval", "3"
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
# Configure SampledDataInterval
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"SampledDataCtrlr", "TxUpdatedInterval", "3"
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
# Configure AlignedDataInterval
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"AlignedDataCtrlr", "SendDuringIdle", "true"
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
# Configure PhaseRotation
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"ChargingStation", "PhaseRotation", "TRS"
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
# Get the value of PhaseRotation
r: call_result201.GetVariables = (
await charge_point_v201.get_config_variables_req(
"ChargingStation", "PhaseRotation"
)
)
get_variables_result: GetVariableResultType = GetVariableResultType(
**r.get_variable_result[0]
)
if get_variables_result.attribute_status == GetVariableStatusEnumType.accepted:
log.info("Phase Rotation %s " % get_variables_result.attribute_value)
# send meter values periodically when not charging
logging.debug("Collecting meter values...")
for _ in range(3):
# send MeterValues
assert await wait_for_and_validate(
test_utility, charge_point_v201, "MeterValues", {"evseId": 1}
)
assert await wait_for_and_validate(
test_utility, charge_point_v201, "MeterValues", {"evseId": 2}
)
# swipe id tag to authorize
test_controller.swipe(id_tokenJ01.id_token)
# start charging session
test_controller.plug_in()
test_utility.messages.clear()
# when in a middle of a transaction do not send meter values
test_utility.forbidden_actions.append("MeterValues")
assert await wait_for_and_validate(
test_utility, charge_point_v201, "TransactionEvent", {
"eventType": "Started"}
)
for _ in range(3):
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Updated"},
)
# swipe id tag to de-authorize
test_controller.swipe(id_tokenJ01.id_token)
# stop charging session
test_controller.plug_out()
assert await wait_for_and_validate(
test_utility, charge_point_v201, "TransactionEvent", {
"eventType": "Ended"}
)

View File

@@ -0,0 +1,661 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
# fmt: off
import os
import sys
from everest.testing.core_utils.controller.test_controller_interface import TestController
sys.path.append(os.path.abspath(
os.path.join(os.path.dirname(__file__), "../..")))
from everest.testing.ocpp_utils.fixtures import test_utility, central_system, CentralSystem
from ocpp.v201.enums import DeleteCertificateStatusEnumType, AuthorizeCertificateStatusEnumType
from ocpp.v201 import call as call201
from ocpp.routing import create_route_map
import asyncio
import pytest
from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility
from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201
from everest.testing.core_utils._configuration.libocpp_configuration_helper import GenericOCPP2XConfigAdjustment
from everest_test_utils import *
# fmt: on
def validate_authorize_req(
authorize_req: call201.Authorize, contains_contract, contains_ocsp
):
return (authorize_req.certificate != None) == contains_contract and (
authorize_req.iso15118_certificate_hash_data != None
) == contains_ocsp
@pytest.mark.ocpp_version("ocpp2.0.1")
@pytest.mark.everest_core_config(
get_everest_config_path_str("everest-config-ocpp201-sil-dc-d2.yaml")
)
@pytest.mark.ocpp_config_adaptions(
GenericOCPP2XConfigAdjustment(
[
(
OCPP2XConfigVariableIdentifier(
"ISO15118Ctrlr",
"ContractCertificateInstallationEnabled",
"Actual",
),
True,
)
]
)
)
@pytest.mark.xdist_group(name="ISO15118")
class TestPlugAndCharge:
@pytest.mark.asyncio
@pytest.mark.source_certs_dir(Path(__file__).parent.parent / "everest-aux/certs")
async def test_contract_installation_and_authorization_01(
self,
request,
exi_generator,
central_system: CentralSystem,
charge_point: ChargePoint201,
test_controller: TestController,
test_config,
test_utility: TestUtility,
):
"""
Test for contract installation on the vehicle and succeeding authorization and charging process
"""
setattr(
charge_point, "on_get_15118_ev_certificate", make_on_get_15118_ev_certificate(exi_generator)
)
central_system.chargepoint.route_map = create_route_map(
central_system.chargepoint
)
test_controller.plug_in_dc_iso()
assert await wait_for_and_validate(
test_utility, charge_point, "Get15118EVCertificate", {
"action": "Install"}
)
# expect authorize.req
authorize_req: call201.Authorize = call201.Authorize(
**await wait_for_and_validate(test_utility, charge_point, "Authorize", {})
)
assert validate_authorize_req(authorize_req, False, True)
# expect StartTransaction.req
assert await wait_for_and_validate(
test_utility,
charge_point,
"TransactionEvent",
{
"eventType": "Started",
"id_token": {
"idToken": test_config.authorization_info.emaid,
"type": "eMAID",
},
},
)
test_utility.messages.clear()
test_controller.plug_out_iso()
# expect StatusNotification with status available
assert await wait_for_and_validate(
test_utility,
charge_point,
"StatusNotification",
{"connectorStatus": "Available"},
)
@pytest.mark.asyncio
async def test_contract_installation_and_authorization_02(
self,
request,
exi_generator,
central_system: CentralSystem,
charge_point: ChargePoint201,
test_controller: TestController,
test_utility: TestUtility,
):
"""
Test for contract installation on the vehicle and succeeding authorization request that is rejected by CSMS
"""
@on(Action.authorize)
def on_authorize(**kwargs):
return call_result201.Authorize(
id_token_info=IdTokenInfoType(
status=AuthorizationStatusEnumType.blocked,
)
)
setattr(
charge_point, "on_get_15118_ev_certificate", make_on_get_15118_ev_certificate(exi_generator)
)
setattr(charge_point, "on_authorize", on_authorize)
central_system.chargepoint.route_map = create_route_map(
central_system.chargepoint
)
await asyncio.sleep(3)
test_controller.plug_in_dc_iso()
assert await wait_for_and_validate(
test_utility, charge_point, "Get15118EVCertificate", {
"action": "Install"}
)
# expect authorize.req
authorize_req: call201.Authorize = call201.Authorize(
**await wait_for_and_validate(test_utility, charge_point, "Authorize", {})
)
assert validate_authorize_req(authorize_req, False, True)
test_utility.messages.clear()
test_utility.forbidden_actions.append("StartTransaction")
test_utility.messages.clear()
test_controller.plug_out_iso()
# expect StatusNotification with status available
assert await wait_for_and_validate(
test_utility,
charge_point,
"StatusNotification",
{"connectorStatus": "Available"},
)
@pytest.mark.asyncio
@pytest.mark.source_certs_dir(Path(__file__).parent.parent / "everest-aux/certs")
@pytest.mark.ocpp_config_adaptions(
GenericOCPP2XConfigAdjustment(
[
(
OCPP2XConfigVariableIdentifier(
"ISO15118Ctrlr",
"CentralContractValidationAllowed",
"Actual",
),
True,
),
(
OCPP2XConfigVariableIdentifier(
"ISO15118Ctrlr",
"ContractCertificateInstallationEnabled",
"Actual",
),
True,
),
]
)
)
async def test_contract_installation_and_authorization_03(
self,
request,
exi_generator,
central_system: CentralSystem,
charge_point: ChargePoint201,
test_controller: TestController,
test_config,
test_utility: TestUtility,
):
"""
Test for contract installation on the vehicle and succeeding authorization and charging process
"""
setattr(
charge_point, "on_get_15118_ev_certificate", make_on_get_15118_ev_certificate(exi_generator)
)
central_system.chargepoint.route_map = create_route_map(
central_system.chargepoint
)
certificate_hash_data = {
"hashAlgorithm": "SHA256",
"issuerKeyHash": "66fce9295edc049f4a183458948ecaa8e3558e4aa3041f13a2363d1d953d33e5",
"issuerNameHash": "3a1ad85a129bd5db30c2f099a541f76e562b8a30e9f49f3f47077eeae3750a2a",
"serialNumber": "3041",
}
delete_certificate_req = {
"certificate_hash_data": certificate_hash_data}
# delete MO root
delete_certificate_response: call_result201.DeleteCertificate = (
await charge_point.delete_certificate_req(
**delete_certificate_req,
)
)
assert (
delete_certificate_response.status == DeleteCertificateStatusEnumType.accepted
)
test_controller.plug_in_dc_iso()
assert await wait_for_and_validate(
test_utility, charge_point, "Get15118EVCertificate", {
"action": "Install"}
)
test_utility.messages.clear()
# expect authorize.req
authorize_req: call201.Authorize = call201.Authorize(
**await wait_for_and_validate(test_utility, charge_point, "Authorize", {})
)
assert validate_authorize_req(authorize_req, True, False)
# expect StartTransaction.req
assert await wait_for_and_validate(
test_utility,
charge_point,
"TransactionEvent",
{
"eventType": "Started",
"id_token": {
"idToken": test_config.authorization_info.emaid,
"type": "eMAID",
},
},
)
test_utility.messages.clear()
test_controller.plug_out_iso()
# expect StatusNotification with status available
assert await wait_for_and_validate(
test_utility,
charge_point,
"StatusNotification",
{"connectorStatus": "Available"},
)
@pytest.mark.asyncio
@pytest.mark.source_certs_dir(Path(__file__).parent.parent / "everest-aux/certs")
@pytest.mark.ocpp_config_adaptions(
GenericOCPP2XConfigAdjustment(
[
(
OCPP2XConfigVariableIdentifier(
"ISO15118Ctrlr",
"CentralContractValidationAllowed",
"Actual",
),
False,
),
(
OCPP2XConfigVariableIdentifier(
"ISO15118Ctrlr",
"ContractCertificateInstallationEnabled",
"Actual",
),
True,
),
]
)
)
async def test_contract_installation_and_authorization_04(
self,
request,
exi_generator,
central_system: CentralSystem,
charge_point: ChargePoint201,
test_controller: TestController,
test_config,
test_utility: TestUtility,
):
"""
Test for contract installation on the vehicle and not succeeding authorization because CentralContractValidationAllowed is false
"""
setattr(
charge_point, "on_get_15118_ev_certificate", make_on_get_15118_ev_certificate(exi_generator)
)
central_system.chargepoint.route_map = create_route_map(
central_system.chargepoint
)
certificate_hash_data = {
"hashAlgorithm": "SHA256",
"issuerKeyHash": "66fce9295edc049f4a183458948ecaa8e3558e4aa3041f13a2363d1d953d33e5",
"issuerNameHash": "3a1ad85a129bd5db30c2f099a541f76e562b8a30e9f49f3f47077eeae3750a2a",
"serialNumber": "3041",
}
delete_certificate_req = {
"certificate_hash_data": certificate_hash_data}
# delete MO root
delete_certificate_response: call_result201.DeleteCertificate = (
await charge_point.delete_certificate_req(
**delete_certificate_req,
)
)
assert (
delete_certificate_response.status == DeleteCertificateStatusEnumType.accepted
)
test_controller.plug_in_dc_iso()
# expect StatusNotification with status available
assert await wait_for_and_validate(
test_utility,
charge_point,
"StatusNotification",
{"connectorStatus": "Occupied"},
)
assert await wait_for_and_validate(
test_utility, charge_point, "Get15118EVCertificate", {
"action": "Install"}
)
test_utility.messages.clear()
test_utility.forbidden_actions.append("Authorize")
test_utility.forbidden_actions.append("TransactionEvent")
test_utility.messages.clear()
test_controller.plug_out_iso()
# expect StatusNotification with status available
assert await wait_for_and_validate(
test_utility,
charge_point,
"StatusNotification",
{"connectorStatus": "Available"},
)
@pytest.mark.asyncio
@pytest.mark.source_certs_dir(Path(__file__).parent.parent / "everest-aux/certs")
@pytest.mark.ocpp_config_adaptions(
GenericOCPP2XConfigAdjustment(
[
(
OCPP2XConfigVariableIdentifier(
"ISO15118Ctrlr",
"CentralContractValidationAllowed",
"Actual",
),
True,
),
(
OCPP2XConfigVariableIdentifier(
"ISO15118Ctrlr",
"ContractCertificateInstallationEnabled",
"Actual",
),
True,
),
]
)
)
@pytest.mark.asyncio
async def test_contract_revoked(
self,
request,
exi_generator,
central_system: CentralSystem,
charge_point: ChargePoint201,
test_controller: TestController,
test_utility: TestUtility,
):
"""
Test for contract installation on the vehicle and succeeding authorization request that is rejected by CSMS
"""
@on(Action.authorize)
def on_authorize(**kwargs):
return call_result201.Authorize(
id_token_info=IdTokenInfoType(
status=AuthorizationStatusEnumType.blocked),
certificate_status=AuthorizeCertificateStatusEnumType.certificate_revoked,
)
setattr(
charge_point, "on_get_15118_ev_certificate", make_on_get_15118_ev_certificate(exi_generator)
)
setattr(charge_point, "on_authorize", on_authorize)
central_system.chargepoint.route_map = create_route_map(
central_system.chargepoint
)
certificate_hash_data = {
"hashAlgorithm": "SHA256",
"issuerKeyHash": "66fce9295edc049f4a183458948ecaa8e3558e4aa3041f13a2363d1d953d33e5",
"issuerNameHash": "3a1ad85a129bd5db30c2f099a541f76e562b8a30e9f49f3f47077eeae3750a2a",
"serialNumber": "3041",
}
delete_certificate_req = {
"certificate_hash_data": certificate_hash_data}
# delete MO root
delete_certificate_response: call_result201.DeleteCertificate = (
await charge_point.delete_certificate_req(
**delete_certificate_req,
)
)
assert (
delete_certificate_response.status == DeleteCertificateStatusEnumType.accepted
)
await asyncio.sleep(3)
test_controller.plug_in_dc_iso()
assert await wait_for_and_validate(
test_utility, charge_point, "Get15118EVCertificate", {
"action": "Install"}
)
# expect authorize.req
authorize_req: call201.Authorize = call201.Authorize(
**await wait_for_and_validate(
test_utility,
charge_point,
"Authorize",
{"idToken": {"idToken": "UKSWI123456789A", "type": "eMAID"}},
)
)
assert validate_authorize_req(authorize_req, True, False)
test_utility.messages.clear()
# verify that transaction does not start
test_utility.forbidden_actions.append("TransactionEvent")
test_utility.messages.clear()
test_controller.plug_out_iso()
# expect StatusNotification with status available
assert await wait_for_and_validate(
test_utility,
charge_point,
"StatusNotification",
{"connectorStatus": "Available"},
)
@pytest.mark.asyncio
@pytest.mark.source_certs_dir(Path(__file__).parent.parent / "everest-aux/certs")
@pytest.mark.ocpp_config_adaptions(
GenericOCPP2XConfigAdjustment(
[
(
OCPP2XConfigVariableIdentifier(
"ISO15118Ctrlr",
"V2GCertificateInstallationEnabled",
"Actual",
),
True,
),
(
OCPP2XConfigVariableIdentifier(
"ISO15118Ctrlr",
"ContractCertificateInstallationEnabled",
"Actual",
),
True,
),
(
OCPP2XConfigVariableIdentifier(
"ISO15118Ctrlr",
"SeccId",
"Actual",
),
"cp001",
),
(
OCPP2XConfigVariableIdentifier(
"ISO15118Ctrlr",
"ISO15118CtrlrOrganizationName",
"Actual",
),
"EVerest",
),
(
OCPP2XConfigVariableIdentifier(
"ISO15118Ctrlr",
"ISO15118CtrlrCountryName",
"Actual",
),
"DE",
),
]
)
)
async def test_no_tls_after_secc_leaf_deleted(
self,
exi_generator,
central_system: CentralSystem,
charge_point: ChargePoint201,
test_controller: TestController,
test_config,
test_utility: TestUtility,
):
"""
Test for contract installation on the vehicle and succeeding authorization and charging process
"""
setattr(
charge_point, "on_get_15118_ev_certificate", make_on_get_15118_ev_certificate(exi_generator)
)
central_system.chargepoint.route_map = create_route_map(
central_system.chargepoint
)
test_controller.plug_in_dc_iso()
assert await wait_for_and_validate(
test_utility, charge_point, "Get15118EVCertificate", {
"action": "Install"}
)
# expect authorize.req
authorize_req: call201.Authorize = call201.Authorize(
**await wait_for_and_validate(test_utility, charge_point, "Authorize", {})
)
assert validate_authorize_req(authorize_req, False, True)
# expect StartTransaction.req
assert await wait_for_and_validate(
test_utility,
charge_point,
"TransactionEvent",
{
"eventType": "Started",
"id_token": {
"idToken": test_config.authorization_info.emaid,
"type": "eMAID",
},
},
)
test_utility.messages.clear()
test_controller.plug_out_iso()
# expect StatusNotification with status available
assert await wait_for_and_validate(
test_utility,
charge_point,
"StatusNotification",
{"connectorStatus": "Available"},
)
certificate_hash_data = {
"hashAlgorithm": "SHA256",
"issuerKeyHash": "cd081d59ed5020a04c4dd028c92ee8758653464a69ebee8f90f4129ba4ef6d66",
"issuerNameHash": "e3883400da76434e273f52534ee5f9bb021bb436d1efef0d793213ad3b25d766",
"serialNumber": "303c"
}
delete_certificate_req = {
"certificate_hash_data": certificate_hash_data}
# delete MO root
delete_certificate_response: call_result201.DeleteCertificate = (
await charge_point.delete_certificate_req(
**delete_certificate_req,
)
)
assert (
delete_certificate_response.status == DeleteCertificateStatusEnumType.accepted
)
test_controller.plug_in_dc_iso()
assert await wait_for_and_validate(
test_utility,
charge_point,
"StatusNotification",
{"connectorStatus": "Occupied"},
)
await asyncio.sleep(10) # wait for ISO process to start
test_utility.messages.clear()
test_controller.swipe("DEADBEEF")
# expect authorize.req
authorize_req: call201.Authorize = call201.Authorize(
**await wait_for_and_validate(test_utility, charge_point, "Authorize", {"idToken": {"type": "ISO14443"}})
)
assert validate_authorize_req(authorize_req, False, False)
# expect StartTransaction.req
assert await wait_for_and_validate(
test_utility,
charge_point,
"TransactionEvent",
{
"eventType": "Started",
"id_token": {
"type": "ISO14443",
},
},
)
test_controller.plug_out_iso()
assert await wait_for_and_validate(
test_utility,
charge_point,
"TransactionEvent",
{
"eventType": "Ended",
},
)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,807 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
from datetime import datetime, timezone
import pytest
# fmt: off
import sys
import os
from everest.testing.core_utils.controller.test_controller_interface import TestController
sys.path.append(os.path.abspath(
os.path.join(os.path.dirname(__file__), "../..")))
from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility, ValidationMode
from everest.testing.ocpp_utils.fixtures import *
from ocpp.routing import on, after, create_route_map
from ocpp.v201.enums import (IdTokenEnumType as IdTokenTypeEnum, TriggerMessageStatusEnumType)
from ocpp.v201.enums import *
from ocpp.v201.datatypes import *
from ocpp.v201 import call as call201
from validations import validate_status_notification_201, validate_measurands_match
from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201
from everest_test_utils import *
from validations import wait_for_callerror_and_validate
# fmt: on
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
async def test_F01_F02_F03(
charge_point_v201: ChargePoint201,
test_controller: TestController,
test_utility: TestUtility,
):
"""
F01.FR.01
F01.FR.02
F01.FR.03
F01.FR.05
F01.FR.07
F01.FR.14
F01.FR.19
F01.FR.23
"""
# prepare data for the test
evse_id = 1
connector_id = 1
remote_start_id = 1
id_token = IdTokenType(id_token="DEADBEEF", type=IdTokenTypeEnum.iso14443)
evse = EVSEType(id=evse_id, connector_id=connector_id)
# Disable AuthCacheCtrlr
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"AuthCacheCtrlr", "Enabled", "false"
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
# get configured measurands in order to compare them to the measurands used in TransactionEvent(eventType=Started) for testing F01.FR.14
r: call_result201.GetVariables = (
await charge_point_v201.get_config_variables_req(
"SampledDataCtrlr", "TxStartedMeasurands"
)
)
get_variables_result: GetVariableResultType = GetVariableResultType(
**r.get_variable_result[0]
)
assert get_variables_result.attribute_status == GetVariableStatusEnumType.accepted
expected_started_measurands = get_variables_result.attribute_value.split(
",")
# get configured measurands in order to compare them to the measurands used in TransactionEvent(eventType=Started) for testing F01.FR.14
r: call_result201.GetVariables = (
await charge_point_v201.get_config_variables_req(
"SampledDataCtrlr", "TxUpdatedMeasurands"
)
)
get_variables_result: GetVariableResultType = GetVariableResultType(
**r.get_variable_result[0]
)
assert get_variables_result.attribute_status == GetVariableStatusEnumType.accepted
# get configured measurands in order to compare them to the measurands used in TransactionEvent(eventType=Started) for testing F01.FR.14
r: call_result201.GetVariables = (
await charge_point_v201.get_config_variables_req(
"SampledDataCtrlr", "TxEndedMeasurands"
)
)
get_variables_result: GetVariableResultType = GetVariableResultType(
**r.get_variable_result[0]
)
assert get_variables_result.attribute_status == GetVariableStatusEnumType.accepted
expected_ended_measurands = get_variables_result.attribute_value.split(",")
# set AuthorizeRemoteStart to true
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"AuthCtrlr", "AuthorizeRemoteStart", "true"
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
# put EVSE to unavailable
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.inoperative, evse=evse
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
call201.StatusNotification(
datetime.now().isoformat(),
ConnectorStatusEnumType.unavailable,
evse_id,
connector_id,
),
validate_status_notification_201,
)
# send RequestStartTransaction while EVSE in unavailable and expect rejected
await charge_point_v201.request_start_transaction_req(
id_token=id_token, remote_start_id=remote_start_id
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"RequestStartTransaction",
call_result201.RequestStartTransaction(
status=RequestStartStopStatusEnumType.rejected
),
)
# put EVSE to available
await charge_point_v201.change_availablility_req(
operational_status=OperationalStatusEnumType.operative, evse=evse
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
call201.StatusNotification(
datetime.now().isoformat(),
ConnectorStatusEnumType.available,
evse_id,
connector_id,
),
validate_status_notification_201,
)
await asyncio.sleep(2)
# send RequestStartTransaction without evse_id and expect Rejected
await charge_point_v201.request_start_transaction_req(
id_token=id_token, remote_start_id=remote_start_id
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"RequestStartTransaction",
call_result201.RequestStartTransaction(
status=RequestStartStopStatusEnumType.rejected
),
)
# send RequestStartTransaction and expect Accepted
await charge_point_v201.request_start_transaction_req(
id_token=id_token, remote_start_id=remote_start_id, evse_id=evse_id
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"RequestStartTransaction",
call_result201.RequestStartTransaction(
status=RequestStartStopStatusEnumType.accepted
),
)
# because AuthorizeRemoteStart is true we expect an Authorize here
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"Authorize",
call201.Authorize(id_token=id_token),
)
test_controller.plug_in()
# eventType=Started
assert await wait_for_and_validate(
test_utility, charge_point_v201, "TransactionEvent", {
"eventType": "Started"}
)
test_utility.messages.clear()
test_controller.plug_out()
# eventType=Ended
assert await wait_for_and_validate(
test_utility, charge_point_v201, "TransactionEvent", {
"eventType": "Ended"}
)
test_utility.messages.clear()
# set AuthorizeRemoteStart to false
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"AuthCtrlr", "AuthorizeRemoteStart", "false"
)
)
test_utility.forbidden_actions.append("Authorize")
test_controller.plug_in()
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
call201.StatusNotification(
datetime.now().isoformat(),
ConnectorStatusEnumType.occupied,
evse_id,
connector_id,
),
validate_status_notification_201,
)
# send RequestStartTransaction and expect Accepted
await charge_point_v201.request_start_transaction_req(
id_token=id_token, remote_start_id=remote_start_id, evse_id=evse_id
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"RequestStartTransaction",
call_result201.RequestStartTransaction(
status=RequestStartStopStatusEnumType.accepted
),
)
# because AuthorizeRemoteStart is false we directly expect a TransactionEvent(eventType=Started)
r: call201.TransactionEvent = call201.TransactionEvent(
**await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Started"},
)
)
transaction = TransactionType(**r.transaction_info)
# do some basic checks on TransactionEvent
assert r.trigger_reason == TriggerReasonEnumType.remote_start
assert r.event_type == TransactionEventEnumType.started
assert EVSEType(**r.evse) == evse
assert IdTokenType(**r.id_token) == id_token
# check if the configured measurands are part of the MeterValue of the TransactionEvent
assert validate_measurands_match(
MeterValueType(**r.meter_value[0]), expected_started_measurands
)
await asyncio.sleep(2)
# send RequestStartTransaction and expect Rejected because transaction_id is wrong
await charge_point_v201.request_stop_transaction_req(
transaction_id="wrong_transaction_id"
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"RequestStopTransaction",
call_result201.RequestStartTransaction(
status=RequestStartStopStatusEnumType.rejected
),
)
# send RequestStartTransaction and expect Accepted
await charge_point_v201.request_stop_transaction_req(
transaction_id=transaction.transaction_id
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"RequestStopTransaction",
call_result201.RequestStartTransaction(
status=RequestStartStopStatusEnumType.accepted
),
)
r: call201.TransactionEvent = call201.TransactionEvent(
**await wait_for_and_validate(
test_utility, charge_point_v201, "TransactionEvent", {
"eventType": "Ended"}
)
)
transaction = TransactionType(**r.transaction_info)
assert r.trigger_reason == TriggerReasonEnumType.remote_stop
assert transaction.stopped_reason == ReasonEnumType.remote
assert transaction.remote_start_id == remote_start_id
assert validate_measurands_match(
MeterValueType(**r.meter_value[0]), expected_ended_measurands
)
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
async def test_F06(
central_system_v201: CentralSystem,
test_controller: TestController,
test_utility: TestUtility,
):
"""
F06.FR.03
F06.FR.04
F06.FR.05
F06.FR.05
F06.FR.06
F06.FR.07
F06.FR.08
F06.FR.09
F06.FR.10
F06.FR.11
F06.FR.12
F06.FR.17
"""
# Skipped for now (Do test NotImplemented):
# LogStatusNotification
# FirmwareStatusNotification
# PublishFirmwareStatusNotification
# SignChargingStationCertificate
# SignV2GCertificate
# SignCombinedCertificate
# Test BootNotification
@on(Action.boot_notification)
def on_boot_notification_pending(**kwargs):
return call_result201.BootNotification(
current_time=datetime.now().isoformat(),
interval=5,
status=RegistrationStatusEnumType.rejected,
)
@on(Action.boot_notification)
def on_boot_notification_accepted(**kwargs):
return call_result201.BootNotification(
current_time=datetime.now(timezone.utc).isoformat(),
interval=300,
status=RegistrationStatusEnumType.accepted,
)
@after(Action.boot_notification)
async def after_boot_notification(reason, charging_station, **kwargs):
# F06.FR.17: Reject trigger messages when boot_notification_state is Accepted
r: call_result201.TriggerMessage = await charge_point_v201.trigger_message_req(
requested_message=MessageTriggerEnumType.boot_notification
)
assert TriggerMessageStatusEnumType(
r.status) == TriggerMessageStatusEnumType.rejected
central_system_v201.function_overrides.append(
("on_boot_notification", on_boot_notification_pending)
)
test_controller.start()
charge_point_v201: ChargePoint201 = await central_system_v201.wait_for_chargepoint()
r: call_result201.TriggerMessage = (
await charge_point_v201.trigger_message_req(
requested_message=MessageTriggerEnumType.boot_notification
)
)
assert TriggerMessageStatusEnumType(
r.status) == TriggerMessageStatusEnumType.accepted
test_utility.validation_mode = ValidationMode.STRICT
assert await wait_for_and_validate(
test_utility, charge_point_v201, "BootNotification", {
"reason": "Triggered"}
)
central_system_v201.function_overrides.append(
("on_boot_notification", on_boot_notification_accepted)
)
setattr(charge_point_v201, "after_boot_notification",
on_boot_notification_accepted)
central_system_v201.chargepoint.route_map = create_route_map(
central_system_v201.chargepoint
)
# Trigger again so we respond with accepted
r: call_result201.TriggerMessage = (
await charge_point_v201.trigger_message_req(
requested_message=MessageTriggerEnumType.boot_notification
)
)
assert TriggerMessageStatusEnumType(
r.status) == TriggerMessageStatusEnumType.accepted
assert await wait_for_and_validate(
test_utility, charge_point_v201, "BootNotification", {
"reason": "Triggered"}
)
test_utility.validation_mode = ValidationMode.EASY
# Limit the amount of data in metervalues and transactions
# add a sleep to ensure that the csms sends boot notification response before triggering SetVariables
await asyncio.sleep(2)
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"AlignedDataCtrlr", "Measurands", MeasurandEnumType.current_import
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"SampledDataCtrlr", "TxStartedMeasurands", MeasurandEnumType.power_active_import
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"SampledDataCtrlr", "TxUpdatedMeasurands", MeasurandEnumType.power_active_import
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"SampledDataCtrlr", "TxEndedMeasurands", MeasurandEnumType.power_active_import
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
# Test Heartbeat
r: call_result201.TriggerMessage = (
await charge_point_v201.trigger_message_req(
requested_message=MessageTriggerEnumType.heartbeat
)
)
assert TriggerMessageStatusEnumType(
r.status) == TriggerMessageStatusEnumType.accepted
test_utility.validation_mode = ValidationMode.STRICT
assert await wait_for_and_validate(
test_utility, charge_point_v201, "Heartbeat", {}, timeout=2
)
test_utility.validation_mode = ValidationMode.EASY
# Test Metervalues
r: call_result201.TriggerMessage = (
await charge_point_v201.trigger_message_req(
requested_message=MessageTriggerEnumType.meter_values, evse=EVSEType(
id=1)
)
)
assert TriggerMessageStatusEnumType(
r.status) == TriggerMessageStatusEnumType.accepted
def check_meter_value(response):
for meter_value in response.meter_value:
value = MeterValueType(**meter_value)
for sampled_value in value.sampled_value:
value = SampledValueType(**sampled_value)
assert value.measurand == MeasurandEnumType.current_import
assert value.context == ReadingContextEnumType.trigger
r: call201.MeterValues = call201.MeterValues(
**await wait_for_and_validate(
test_utility, charge_point_v201, "MeterValues", {"evseId": 1}
)
)
check_meter_value(r)
r: call_result201.TriggerMessage = (
await charge_point_v201.trigger_message_req(
requested_message=MessageTriggerEnumType.meter_values
)
)
assert TriggerMessageStatusEnumType(
r.status) == TriggerMessageStatusEnumType.accepted
r: call201.MeterValues = call201.MeterValues(
**await wait_for_and_validate(
test_utility, charge_point_v201, "MeterValues", {"evseId": 1}
)
)
check_meter_value(r)
r: call201.MeterValues = call201.MeterValues(
**await wait_for_and_validate(
test_utility, charge_point_v201, "MeterValues", {"evseId": 2}
)
)
check_meter_value(r)
r = await charge_point_v201.trigger_message_req(
requested_message=MessageTriggerEnumType.meter_values, evse=EVSEType(
id=3)
)
assert await wait_for_callerror_and_validate(
test_utility, charge_point_v201, "OccurrenceConstraintViolation"
)
# Test StatusNotification
r: call_result201.TriggerMessage = (
await charge_point_v201.trigger_message_req(
requested_message=MessageTriggerEnumType.status_notification,
evse=EVSEType(id=1, connector_id=1),
)
)
assert TriggerMessageStatusEnumType(
r.status) == TriggerMessageStatusEnumType.accepted
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"evseId": 1, "connectorId": 1, "connectorStatus": "Available"},
)
r: call_result201.TriggerMessage = (
await charge_point_v201.trigger_message_req(
requested_message=MessageTriggerEnumType.status_notification,
evse=EVSEType(id=1, connector_id=2),
)
)
assert TriggerMessageStatusEnumType(
r.status) == TriggerMessageStatusEnumType.rejected
r: call_result201.TriggerMessage = (
await charge_point_v201.trigger_message_req(
requested_message=MessageTriggerEnumType.status_notification,
evse=EVSEType(id=3, connector_id=1),
)
)
assert await wait_for_callerror_and_validate(
test_utility, charge_point_v201, "OccurrenceConstraintViolation"
)
# F06.FR.12
r: call_result201.TriggerMessage = (
await charge_point_v201.trigger_message_req(
requested_message=MessageTriggerEnumType.status_notification
)
)
assert TriggerMessageStatusEnumType(
r.status) == TriggerMessageStatusEnumType.rejected
r: call_result201.TriggerMessage = (
await charge_point_v201.trigger_message_req(
requested_message=MessageTriggerEnumType.status_notification,
evse=EVSEType(id=1),
)
)
assert TriggerMessageStatusEnumType(
r.status) == TriggerMessageStatusEnumType.rejected
# Test TransactionEvent
r: call_result201.TriggerMessage = (
await charge_point_v201.trigger_message_req(
requested_message=MessageTriggerEnumType.transaction_event, evse=EVSEType(
id=1)
)
)
assert TriggerMessageStatusEnumType(
r.status) == TriggerMessageStatusEnumType.rejected
test_controller.swipe("001", connectors=[1, 2])
test_controller.plug_in()
r: call201.TransactionEvent = call201.TransactionEvent(
**await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Started", "evse": {"id": 1}},
)
)
transaction_1: TransactionType = TransactionType(**r.transaction_info)
test_utility.validation_mode = ValidationMode.STRICT
r: call_result201.TriggerMessage = (
await charge_point_v201.trigger_message_req(
requested_message=MessageTriggerEnumType.transaction_event, evse=EVSEType(
id=1)
)
)
assert TriggerMessageStatusEnumType(
r.status) == TriggerMessageStatusEnumType.accepted
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{
"eventType": "Updated",
"triggerReason": "Trigger",
"transactionInfo": {"transactionId": transaction_1.transaction_id},
},
)
r: call_result201.TriggerMessage = (
await charge_point_v201.trigger_message_req(
requested_message=MessageTriggerEnumType.transaction_event
)
)
assert TriggerMessageStatusEnumType(
r.status) == TriggerMessageStatusEnumType.accepted
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{
"eventType": "Updated",
"triggerReason": "Trigger",
"transactionInfo": {"transactionId": transaction_1.transaction_id},
},
)
test_utility.validation_mode = ValidationMode.EASY
test_controller.swipe("002", connectors=[1, 2])
test_controller.plug_in(connector_id=2)
r: call201.TransactionEvent = call201.TransactionEvent(
**await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Started", "evse": {"id": 2}},
)
)
transaction_2: TransactionType = TransactionType(**r.transaction_info)
r: call201.TransactionEvent = call201.TransactionEvent(
**await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{
"eventType": "Updated",
"triggerReason": "ChargingStateChanged",
"transactionInfo": {"transactionId": transaction_2.transaction_id},
},
)
)
test_utility.validation_mode = ValidationMode.STRICT
r: call_result201.TriggerMessage = (
await charge_point_v201.trigger_message_req(
requested_message=MessageTriggerEnumType.transaction_event, evse=EVSEType(
id=2)
)
)
assert TriggerMessageStatusEnumType(
r.status) == TriggerMessageStatusEnumType.accepted
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{
"eventType": "Updated",
"triggerReason": "Trigger",
"transactionInfo": {"transactionId": transaction_2.transaction_id},
},
)
r: call_result201.TriggerMessage = (
await charge_point_v201.trigger_message_req(
requested_message=MessageTriggerEnumType.transaction_event
)
)
assert TriggerMessageStatusEnumType(
r.status) == TriggerMessageStatusEnumType.accepted
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{
"eventType": "Updated",
"triggerReason": "Trigger",
"transactionInfo": {"transactionId": transaction_1.transaction_id},
},
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{
"eventType": "Updated",
"triggerReason": "Trigger",
"transactionInfo": {"transactionId": transaction_2.transaction_id},
},
)
test_utility.validation_mode = ValidationMode.EASY
test_controller.swipe("001", connectors=[1, 2])
test_controller.swipe("002", connectors=[1, 2])
test_controller.plug_out()
test_controller.plug_out(connector_id=2)
# Test LogStatusNotificaiton
r: call_result201.TriggerMessage = (
await charge_point_v201.trigger_message_req(
requested_message=MessageTriggerEnumType.log_status_notification
)
)
assert TriggerMessageStatusEnumType(
r.status) == TriggerMessageStatusEnumType.accepted
assert await wait_for_and_validate(
test_utility, charge_point_v201, "LogStatusNotification", {
"status": "Idle"}
)
# Waiting for the log callback to be implemented in the everest core
# log_param = LogParametersType(
# remote_location="ftp://user:12345@localhost:2121",
# oldest_timestamp=(datetime.now(timezone.utc) - timedelta(days=1)).isoformat(),
# latest_timestamp=datetime.now(timezone.utc).isoformat()
# )
# r: call_result201.TriggerMessage = await charge_point_v201.get_log_req(log=log_param, log_type=LogType.diagnostics_log, request_id=10)
# assert await wait_for_and_validate(test_utility, charge_point_v201, "LogStatusNotification", {"status": "Uploading"})
# assert await wait_for_and_validate(test_utility, charge_point_v201, "LogStatusNotification", {"status": "UploadFailed"})
# r: call_result201.TriggerMessage = await charge_point_v201.trigger_message_req(requested_message=MessageTriggerEnumType.log_status_notification)
# assert TriggerMessageStatusEnumType(r.status) == TriggerMessageStatusEnumType.accepted
# test_utility.validation_mode = ValidationMode.STRICT
# assert await wait_for_and_validate(test_utility, charge_point_v201, "LogStatusNotification", {"status": "Idle"})
# test_utility.validation_mode = ValidationMode.EASY
# Test FirmwareStatusNotification
r: call_result201.TriggerMessage = (
await charge_point_v201.trigger_message_req(
requested_message=MessageTriggerEnumType.firmware_status_notification
)
)
assert TriggerMessageStatusEnumType(
r.status) == TriggerMessageStatusEnumType.accepted
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"FirmwareStatusNotification",
{"status": "Idle"},
)
# Waiting for the update firmware callback to be implemented in the everest core
# firmware_type = FirmwareType(
# location="ftp://user:12345@localhost:2121",
# retrieve_date_time=(datetime.now(timezone.utc) + timedelta(seconds=10)).isoformat()
# )
# test_utility.validation_mode = ValidationMode.STRICT
# r: call_result201.UpdateFirmware = await charge_point_v201.update_firmware(firmware=firmware_type, request_id=10)
# assert await wait_for_and_validate(test_utility, charge_point_v201, "FirmwareStatusNotification", {"status": "DownloadScheduled", "requestId": 10})
# r: call_result201.TriggerMessage = await charge_point_v201.trigger_message_req(requested_message=MessageTriggerEnumType.firmware_status_notification)
# assert TriggerMessageStatusEnumType(r.status) == TriggerMessageStatusEnumType.accepted
# assert await wait_for_and_validate(test_utility, charge_point_v201, "FirmwareStatusNotification", {"status": "DownloadScheduled", "requestId": 10})
# assert await wait_for_and_validate(test_utility, charge_point_v201, "FirmwareStatusNotification", {"status": "Downloading", "requestId": 10})
# assert await wait_for_and_validate(test_utility, charge_point_v201, "FirmwareStatusNotification", {"status": "Downloading", "requestId": 10})
# assert await wait_for_and_validate(test_utility, charge_point_v201, "FirmwareStatusNotification", {"status": "DownloadFailed", "requestId": 10})
# r: call_result201.TriggerMessage = await charge_point_v201.trigger_message_req(requested_message=MessageTriggerEnumType.firmware_status_notification)
# assert TriggerMessageStatusEnumType(r.status) == TriggerMessageStatusEnumType.accepted
# assert await wait_for_and_validate(test_utility, charge_point_v201, "FirmwareStatusNotification", {"status": "Idle"})
# test_utility.validation_mode = ValidationMode.EASY

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,959 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
import asyncio
import logging
from dataclasses import dataclass
from datetime import datetime
from pathlib import Path
from typing import Any
import pytest
from OpenSSL import crypto
from cryptography import x509
from cryptography.hazmat.backends import default_backend
from cryptography.hazmat.primitives._serialization import PublicFormat, Encoding
from everest.testing.core_utils.controller.test_controller_interface import (
TestController,
)
from everest.testing.core_utils.everest_core import EverestCore
from everest.testing.core_utils.probe_module import ProbeModule
from everest_test_utils import OCPPConfigReader, CertificateHelper
from unittest.mock import call as mock_call, Mock, ANY
from ocpp.v201 import call_result
from ocpp.v201.datatypes import SetVariableResultType
from ocpp.v201.enums import SetVariableStatusEnumType
from everest.testing.ocpp_utils.charge_point_v201 import ChargePoint201
from everest.testing.ocpp_utils.central_system import CentralSystem
from everest.testing.core_utils._configuration.libocpp_configuration_helper import (
GenericOCPP2XConfigAdjustment,
OCPP2XConfigVariableIdentifier,
)
from everest.testing.core_utils._configuration.libocpp_configuration_helper import (
_OCPP2XNetworkConnectionProfileAdjustment,
)
log = logging.getLogger("OCPP201Security")
@dataclass
class _CertificateSigningTestData:
signed_certificate: str | None = None
csr: str | None = None
signed_certificate_valid: bool = True
csr_accepted: bool = True
class _BaseTest:
@staticmethod
async def _wait_for_mock_called(mock, call=None, timeout=2):
async def _await_called():
while not mock.call_count or (call and call not in mock.mock_calls):
await asyncio.sleep(0.1)
await asyncio.wait_for(_await_called(), timeout=timeout)
def _setup_csms_mock(self, csms_mock: Mock, test_data: _CertificateSigningTestData):
status = "Accepted" if test_data.csr_accepted else "Rejected"
csms_mock.on_sign_certificate.side_effect = (
lambda csr: call_result.SignCertificate(status=status)
)
def _get_expected_csr_data(
self, certificate_type: str, ocpp_config_reader: OCPPConfigReader
):
if certificate_type == "ChargingStationCertificate":
return {
"certificate_type": "CSMS",
"common": ocpp_config_reader.get_variable(
"InternalCtrlr", "ChargeBoxSerialNumber"
),
"country": ocpp_config_reader.get_variable(
"ISO15118Ctrlr", "ISO15118CtrlrCountryName"
),
"organization": ocpp_config_reader.get_variable(
"SecurityCtrlr", "OrganizationName"
),
"use_tpm": False,
}
else:
return {
"certificate_type": "V2G",
"common": ocpp_config_reader.get_variable(
"InternalCtrlr", "ChargeBoxSerialNumber"
),
"country": ocpp_config_reader.get_variable(
"ISO15118Ctrlr", "ISO15118CtrlrCountryName"
),
"organization": ocpp_config_reader.get_variable(
"ISO15118Ctrlr", "ISO15118CtrlrOrganizationName"
),
"use_tpm": False,
}
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
@pytest.mark.everest_core_config("everest-config-ocpp201-probe-module.yaml")
@pytest.mark.inject_csms_mock
@pytest.mark.probe_module
@pytest.mark.parametrize(
"skip_implementation",
[
{
"ProbeModuleSecurity": [
"generate_certificate_signing_request",
"update_leaf_certificate",
]
}
],
)
@pytest.mark.skip("The certificate chains are not properly set up to run these tests")
class TestSecurityOCPPIntegration(_BaseTest):
@dataclass
class _SecurityModuleMocks:
generate_certificate_signing_request: Mock
update_leaf_certificate: Mock
def _setup_security_module_mocks(
self, probe_module: ProbeModule, test_data: _CertificateSigningTestData
) -> _SecurityModuleMocks:
security_generate_certificate_signing_request_mock = Mock()
security_generate_certificate_signing_request_mock.side_effect = (
lambda arg: test_data.csr
)
probe_module.implement_command(
"ProbeModuleSecurity",
"generate_certificate_signing_request",
security_generate_certificate_signing_request_mock,
)
security_update_leaf_certificate_mock = Mock()
security_update_leaf_certificate_mock.side_effect = lambda arg: (
"Accepted"
if test_data.signed_certificate_valid
else "InvalidCertificateChain"
)
probe_module.implement_command(
"ProbeModuleSecurity",
"update_leaf_certificate", # installs and verifies
security_update_leaf_certificate_mock,
)
return self._SecurityModuleMocks(
generate_certificate_signing_request=security_generate_certificate_signing_request_mock,
update_leaf_certificate=security_update_leaf_certificate_mock,
)
@pytest.mark.parametrize(
"certificate_type", ["ChargingStationCertificate", "V2GCertificate"]
)
async def test_A02_update_charging_station_certificate_by_csms_request(
self,
certificate_type,
probe_module,
ocpp_config_reader,
central_system: CentralSystem,
):
"""A02 use case success behavior."""
# Setup Test Data & mocks
csms_mock = central_system.mock
test_data = _CertificateSigningTestData(
csr="mock certificate request", signed_certificate="mock signed certificate"
)
security_mocks = self._setup_security_module_mocks(
probe_module, test_data)
self._setup_csms_mock(csms_mock, test_data)
# start and ready probe module EvseManagers and wait for libocpp to connect
probe_module.start()
await probe_module.wait_to_be_ready()
probe_module.publish_variable("ProbeModuleConnectorA", "ready", True)
probe_module.publish_variable("ProbeModuleConnectorB", "ready", True)
chargepoint_with_pm = await central_system.wait_for_chargepoint()
# # Act CSMS triggers SignChargingStationCertificate
trigger_result: call_result.TriggerMessage = (
await chargepoint_with_pm.trigger_message_req(
# todo: SignV2GCertificate
requested_message=f"Sign{certificate_type}"
)
)
# Verify:
# - OCPP accepts trigger and
# - calls security module "generate_certificate_signing_request"
# - sends CSR to CSMS
assert trigger_result.status == "Accepted"
await self._wait_for_mock_called(
security_mocks.generate_certificate_signing_request
)
assert security_mocks.generate_certificate_signing_request.mock_calls == [
mock_call(self._get_expected_csr_data(
certificate_type, ocpp_config_reader))
]
#
await self._wait_for_mock_called(csms_mock.on_sign_certificate)
assert csms_mock.on_sign_certificate.mock_calls == [
mock_call(csr=test_data.csr)
]
# Act II: CSMS sends signed result to ChargePoint
signed_result: call_result.CertificateSigned = (
await chargepoint_with_pm.certificate_signed_req(
certificate_chain=test_data.signed_certificate,
certificate_type=certificate_type,
)
)
# Verify II
# - Chargepoint accepts signed certificate
# - OCPP module verifies and installs certificate in Security Module
assert signed_result == call_result.CertificateSigned(
status="Accepted")
await self._wait_for_mock_called(security_mocks.update_leaf_certificate)
assert security_mocks.update_leaf_certificate.mock_calls == [
mock_call(
{
"certificate_chain": test_data.signed_certificate,
"certificate_type": (
"CSMS"
if certificate_type == "ChargingStationCertificate"
else "V2G"
),
}
)
]
async def test_A02_update_charging_station_certificate_by_csms_request_retry(
self, probe_module, ocpp_config_reader, central_system: CentralSystem
):
"""Test the retry behavior on failed attempts.
In particular tests requirements A02.FR.17, A02.FR.18, A02.FR.19"""
# Setup Test Data & mocks
csms_mock = central_system.mock
test_data = _CertificateSigningTestData(
csr="mock certificate request", signed_certificate="mock signed certificate"
)
certificate_type = "ChargingStationCertificate"
security_mocks = self._setup_security_module_mocks(
probe_module, test_data)
self._setup_csms_mock(csms_mock, test_data)
# start and ready probe module EvseManagers and wait for libocpp to connect
probe_module.start()
await probe_module.wait_to_be_ready()
probe_module.publish_variable("ProbeModuleConnectorA", "ready", True)
probe_module.publish_variable("ProbeModuleConnectorB", "ready", True)
chargepoint_with_pm = await central_system.wait_for_chargepoint()
# Act CSMS triggers SignChargingStationCertificate
trigger_result: call_result.TriggerMessage = (
await chargepoint_with_pm.trigger_message_req(
requested_message="SignChargingStationCertificate"
)
)
# Verify:
# - OCPP accepts trigger and
# - calls security module "generate_certificate_signing_request"
# - sends CSR to CSMS
assert trigger_result.status == "Accepted"
await self._wait_for_mock_called(
security_mocks.generate_certificate_signing_request
)
assert security_mocks.generate_certificate_signing_request.mock_calls == [
mock_call(self._get_expected_csr_data(
certificate_type, ocpp_config_reader))
]
await self._wait_for_mock_called(csms_mock.on_sign_certificate)
assert csms_mock.on_sign_certificate.mock_calls == [
mock_call(csr=test_data.csr)
]
# Verify: The CSMS does not send the certificate, thus request is repeated as many times as configured
repeat_times = ocpp_config_reader.get_variable(
"SecurityCtrlr", "CertSigningRepeatTimes"
)
async def _await_called():
while not len(csms_mock.on_sign_certificate.mock_calls) == repeat_times:
await asyncio.sleep(0.1)
await asyncio.wait_for(_await_called(), 10)
await asyncio.sleep(0.1) # await unexpected further call
assert csms_mock.on_sign_certificate.mock_calls == repeat_times * [
mock_call(csr=test_data.csr)
]
security_mocks.update_leaf_certificate.assert_not_called()
async def test_A04_rejected_security_event_notification(
self, probe_module, ocpp_config_reader, central_system: CentralSystem
):
"""A02 & A04: OCPP module sends security event if certificate is rejected
Also tests A02.FR.20 (no repetition)
"""
# Setup Test Data & mocks
csms_mock = central_system.mock
test_data = _CertificateSigningTestData(
csr="mock certificate request",
signed_certificate="mock signed certificate",
signed_certificate_valid=False,
)
certificate_type = "ChargingStationCertificate"
security_mocks = self._setup_security_module_mocks(
probe_module, test_data)
self._setup_csms_mock(csms_mock, test_data)
# start and ready probe module EvseManagers and wait for libocpp to connect
probe_module.start()
await probe_module.wait_to_be_ready()
probe_module.publish_variable("ProbeModuleConnectorA", "ready", True)
probe_module.publish_variable("ProbeModuleConnectorB", "ready", True)
chargepoint_with_pm = await central_system.wait_for_chargepoint()
# Act CSMS triggers SignChargingStationCertificate
trigger_result: call_result.TriggerMessage = (
await chargepoint_with_pm.trigger_message_req(
requested_message="SignChargingStationCertificate" # todo: SignV2GCertificate
)
)
# Verify:
# - OCPP accepts trigger and
# - calls security module "generate_certificate_signing_request"
# - sends CSR to CSMS
assert trigger_result.status == "Accepted"
await self._wait_for_mock_called(
security_mocks.generate_certificate_signing_request
)
assert security_mocks.generate_certificate_signing_request.mock_calls == [
mock_call(self._get_expected_csr_data(
certificate_type, ocpp_config_reader))
]
await self._wait_for_mock_called(csms_mock.on_sign_certificate)
assert csms_mock.on_sign_certificate.mock_calls == [
mock_call(csr=test_data.csr)
]
# Act II: CSMS sends signed result to ChargePoint
signed_result: call_result.CertificateSigned = (
await chargepoint_with_pm.certificate_signed_req(
certificate_chain=test_data.signed_certificate,
certificate_type=certificate_type,
)
)
# Verify II
# - Chargepoint accepts signed certificate
# - OCPP module rejects certificate and sends security event notification
assert signed_result == call_result.CertificateSigned(
status="Rejected")
await self._wait_for_mock_called(
csms_mock.on_security_event_notification,
call=mock_call(
timestamp=ANY,
tech_info="InvalidCertificateChain",
type="InvalidChargingStationCertificate",
),
)
# test A02.FR.20 - no repeated request
await asyncio.sleep(
0.3
) # wait the minimum time between two retries and a little more
assert csms_mock.on_sign_certificate.call_count == 1
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
@pytest.mark.everest_core_config("everest-config-ocpp201.yaml")
@pytest.mark.source_certs_dir(Path(__file__).parent.parent / "everest-aux/certs")
@pytest.mark.inject_csms_mock
@pytest.mark.skip("The certificate chains are not properly set up to run these tests")
class TestSecurityOCPPE2E(_BaseTest):
@dataclass
class _ParsedCSR:
csr: str
common: str
organization: str
country: str
email_address: str | None
public_key: str
@classmethod
def _parse_certificate_request(cls, csr: str) -> _ParsedCSR:
request = x509.load_pem_x509_csr(
csr.encode("utf-8"), default_backend())
email_address = None
if request.subject.get_attributes_for_oid(x509.NameOID.EMAIL_ADDRESS):
email_address = request.subject.get_attributes_for_oid(
x509.NameOID.EMAIL_ADDRESS
)[0].value
return cls._ParsedCSR(
csr,
request.subject.get_attributes_for_oid(
x509.NameOID.COMMON_NAME)[0].value,
request.subject.get_attributes_for_oid(x509.NameOID.ORGANIZATION_NAME)[
0
].value,
request.subject.get_attributes_for_oid(
x509.NameOID.COUNTRY_NAME)[0].value,
email_address,
request.public_key()
.public_bytes(
encoding=Encoding.PEM, format=PublicFormat.SubjectPublicKeyInfo
)
.decode("utf-8"),
)
@pytest.mark.parametrize(
"certificate_type", ["ChargingStationCertificate", "V2GCertificate"]
)
async def test_A02_update_charging_station_certificate_by_csms_request(
self,
certificate_type,
ocpp_config_reader,
central_system: CentralSystem,
charge_point: ChargePoint201,
):
"""A02 use case success behavior (installation of new certificate)
Tested Requirements: A02.FR.02, A02.FR.05 (as far as possible in this context), A02.FR.06
"""
# Setup Test Data & mocks
csms_mock = central_system.mock
test_data = _CertificateSigningTestData(
signed_certificate="mock signed certificate"
)
self._setup_csms_mock(csms_mock, test_data)
# # Act CSMS triggers SignChargingStationCertificate
trigger_result: call_result.TriggerMessage = (
await charge_point.trigger_message_req(
requested_message=f"Sign{certificate_type}"
)
)
# Verify:
# - OCPP accepts trigger and
# - calls security module "generate_certificate_signing_request"
# - sends CSR to CSMS
assert trigger_result.status == "Accepted"
await self._wait_for_mock_called(csms_mock.on_sign_certificate)
assert csms_mock.on_sign_certificate.mock_calls == [
mock_call(csr=ANY, certificate_type=certificate_type)
]
received_csr_data = self._parse_certificate_request(
csms_mock.on_sign_certificate.mock_calls[0].kwargs["csr"]
)
expected_csr_data = self._get_expected_csr_data(
certificate_type, ocpp_config_reader
)
assert received_csr_data.common == expected_csr_data["common"]
assert received_csr_data.organization == expected_csr_data["organization"]
assert received_csr_data.country == expected_csr_data["country"]
if certificate_type == "ChargingStationCertificate":
signed_certificate = CertificateHelper.sign_certificate_request(
received_csr_data.csr,
issuer_certificate_path=Path(__file__).parent
/ "../everest-aux/certs/ca/csms/CSMS_ROOT_CA.pem",
issuer_private_key_path=Path(__file__).parent
/ "../everest-aux/certs/ca/csms/CSMS_ROOT_CA.key",
issuer_private_key_passphrase="123456",
)
else:
# build certificate chain starting with leaf up to CPO SUB CA1
signed_certificate = CertificateHelper.sign_certificate_request(
received_csr_data.csr,
issuer_certificate_path=Path(__file__).parent
/ "../everest-aux/certs/ca/cso/CPO_SUB_CA2.pem",
issuer_private_key_path=Path(__file__).parent
/ "../everest-aux/certs/client/csms/CPO_SUB_CA2.key",
issuer_private_key_passphrase="123456",
)
signed_certificate += (
Path(__file__).parent /
"../everest-aux/certs/ca/cso/CPO_SUB_CA2.pem"
).read_text() + "\n"
signed_certificate += (
Path(__file__).parent /
"../everest-aux/certs/ca/cso/CPO_SUB_CA1.pem"
).read_text() + "\n"
# Act II: CSMS sends signed result to ChargePoint
signed_result: call_result.CertificateSigned = (
await charge_point.certificate_signed_req(
certificate_chain=signed_certificate, certificate_type=certificate_type
)
)
# Verify II
# - Chargepoint accepts signed certificate
# - OCPP module verifies and installs certificate in Security Module
assert signed_result == call_result.CertificateSigned(
status="Accepted")
async def test_A02_update_charging_station_certificate_by_csms_request_invalid_as_expired(
self,
ocpp_config_reader,
central_system: CentralSystem,
charge_point: ChargePoint201,
):
"""Test the charging station rejects an expired certificate after a signing request."""
csms_mock = central_system.mock
test_data = _CertificateSigningTestData(
signed_certificate="mock signed certificate"
)
self._setup_csms_mock(csms_mock, test_data)
# # Act CSMS triggers SignChargingStationCertificate
trigger_result: call_result.TriggerMessage = (
await charge_point.trigger_message_req(
requested_message=f"SignChargingStationCertificate"
)
)
# Verify:
# - OCPP accepts trigger and
# - calls security module "generate_certificate_signing_request"
# - sends CSR to CSMS
assert trigger_result.status == "Accepted"
await self._wait_for_mock_called(csms_mock.on_sign_certificate)
received_csr_data = self._parse_certificate_request(
csms_mock.on_sign_certificate.mock_calls[0].kwargs["csr"]
)
signed_certificate = CertificateHelper.sign_certificate_request(
received_csr_data.csr,
issuer_certificate_path=Path(__file__).parent
/ "../everest-aux/certs/ca/csms/CSMS_ROOT_CA.pem",
issuer_private_key_path=Path(__file__).parent
/ "../everest-aux/certs/ca/csms/CSMS_ROOT_CA.key",
issuer_private_key_passphrase="123456",
relative_expiration_time=-60, # expired a minute ago
)
# Act II: CSMS sends signed result to ChargePoint
signed_result: call_result.CertificateSigned = (
await charge_point.certificate_signed_req(
certificate_chain=signed_certificate,
certificate_type="ChargingStationCertificate",
)
)
# Verify II
# - Chargepoint accepts signed certificate
# - OCPP module rejects wrongly signed certificate
assert signed_result == call_result.CertificateSigned(
status="Rejected")
# Assert an InvalidChargingStationCertificate event is triggered
await self._wait_for_mock_called(
csms_mock.on_security_event_notification,
call=mock_call(
timestamp=ANY,
tech_info="Expired",
type="InvalidChargingStationCertificate",
),
)
async def test_A02_update_charging_station_certificate_by_csms_request_invalid_due_to_wrong_ca(
self,
ocpp_config_reader,
central_system: CentralSystem,
charge_point: ChargePoint201,
):
csms_mock = central_system.mock
test_data = _CertificateSigningTestData(
signed_certificate="mock signed certificate"
)
self._setup_csms_mock(csms_mock, test_data)
# # Act CSMS triggers SignChargingStationCertificate
trigger_result: call_result.TriggerMessage = (
await charge_point.trigger_message_req(
requested_message=f"SignChargingStationCertificate"
)
)
# Verify:
# - OCPP accepts trigger and
# - calls security module "generate_certificate_signing_request"
# - sends CSR to CSMS
assert trigger_result.status == "Accepted"
await self._wait_for_mock_called(csms_mock.on_sign_certificate)
received_csr_data = self._parse_certificate_request(
csms_mock.on_sign_certificate.mock_calls[0].kwargs["csr"]
)
# the MO root ca is invalid for the CSMS certificate!
signed_certificate = CertificateHelper.sign_certificate_request(
received_csr_data.csr,
issuer_certificate_path=Path(__file__).parent
/ "../everest-aux/certs/ca/mo/MO_ROOT_CA.pem",
issuer_private_key_path=Path(__file__).parent
/ "../everest-aux/certs/client/mo/MO_ROOT_CA.key",
issuer_private_key_passphrase="123456",
)
# Act II: CSMS sends signed result to ChargePoint
signed_result: call_result.CertificateSigned = (
await charge_point.certificate_signed_req(
certificate_chain=signed_certificate,
certificate_type="ChargingStationCertificate",
)
)
# Verify II
# - Chargepoint accepts signed certificate
# - OCPP module rejects wrongly signed certificate
assert signed_result == call_result.CertificateSigned(
status="Rejected")
# Assert an InvalidChargingStationCertificate event is triggered
await self._wait_for_mock_called(
csms_mock.on_security_event_notification,
call=mock_call(
timestamp=ANY,
tech_info="InvalidCertificateChain",
type="InvalidChargingStationCertificate",
),
)
def assert_websocket_client_sslproto_certificate_equals_certificate(
self, websocket_client_cert: dict[str, Any], certificate: str
):
x509_cert = crypto.load_certificate(
crypto.FILETYPE_PEM, certificate.encode("utf-8")
)
def _compare_websocket_and_cert_components(
websocket_components, cert_components
):
websocket_cert_subject_dict = {
k: v for ((k, v),) in websocket_components}
cert_subject_dict = {k: v for (k, v) in cert_components}
for websocket_key, cert_key in [
("countryName", b"C"),
("commonName", b"CN"),
("organizationName", b"O"),
("domainComponent", b"DC"),
]:
assert websocket_cert_subject_dict.get(
websocket_key, ""
) == cert_subject_dict.get(cert_key, b"").decode("utf-8")
_compare_websocket_and_cert_components(
websocket_client_cert["subject"], x509_cert.get_subject(
).get_components()
)
_compare_websocket_and_cert_components(
websocket_client_cert["issuer"], x509_cert.get_issuer(
).get_components()
)
assert (
int(websocket_client_cert["serialNumber"], 16)
== x509_cert.get_serial_number()
)
assert datetime.strptime(
websocket_client_cert["notBefore"], "%b %d %H:%M:%S %Y GMT"
) == datetime.strptime(
x509_cert.get_notBefore().decode("utf-8"), "%Y%m%d%H%M%SZ"
)
@pytest.mark.csms_tls(verify_client_certificate=True)
@pytest.mark.ocpp_config_adaptions(
_OCPP2XNetworkConnectionProfileAdjustment(None, None, 3)
)
async def test_A02_use_newest_certificate_after_installation(
self, central_system: CentralSystem, charge_point: ChargePoint201
):
"""Test station uses new certificate after installation
Tests requirement A02.FR.08
"""
# Check originally used certificate
assert len(central_system.ws_server.websockets) == 1
old_connection = next(iter(central_system.ws_server.websockets))
original_certificate = old_connection.transport.get_extra_info(
"peercert")
expected_original_certificate = (
Path(__file__).parent /
"../everest-aux/certs/client/csms/CSMS_RSA.pem"
).read_text()
self.assert_websocket_client_sslproto_certificate_equals_certificate(
original_certificate, expected_original_certificate
)
# Install new certificate by CSMS request
csms_mock = central_system.mock
self._setup_csms_mock(csms_mock, _CertificateSigningTestData())
await charge_point.trigger_message_req(
requested_message=f"SignChargingStationCertificate"
)
await self._wait_for_mock_called(csms_mock.on_sign_certificate)
received_csr_data = self._parse_certificate_request(
csms_mock.on_sign_certificate.mock_calls[0].kwargs["csr"]
)
signed_certificate = CertificateHelper.sign_certificate_request(
received_csr_data.csr,
issuer_certificate_path=Path(__file__).parent
/ "../everest-aux/certs/ca/csms/CSMS_ROOT_CA.pem",
issuer_private_key_path=Path(__file__).parent
/ "../everest-aux/certs/ca/csms/CSMS_ROOT_CA.key",
issuer_private_key_passphrase="123456",
)
signed_result: call_result.CertificateSigned = (
await charge_point.certificate_signed_req(
certificate_chain=signed_certificate,
certificate_type="ChargingStationCertificate",
)
)
assert signed_result == call_result.CertificateSigned(
status="Accepted")
# Verify: wait for new connection to be established; validate certificate
async def wait_for_reconnect():
while len(
central_system.ws_server.websockets
) < 1 or central_system.ws_server.websockets == {old_connection}:
await asyncio.sleep(0.1)
await asyncio.wait_for(wait_for_reconnect(), 4)
assert len(central_system.ws_server.websockets) == 1
new_connection = next(iter(central_system.ws_server.websockets))
new_certificate = new_connection.transport.get_extra_info("peercert")
self.assert_websocket_client_sslproto_certificate_equals_certificate(
new_certificate, signed_certificate
)
@pytest.mark.ocpp_config_adaptions(
_OCPP2XNetworkConnectionProfileAdjustment(None, None, 3)
)
@pytest.mark.csms_tls(verify_client_certificate=True)
async def test_A02_use_newest_certificate_according_to_validity(
self,
everest_core: EverestCore,
test_controller: TestController,
central_system: CentralSystem,
charge_point: ChargePoint201,
):
"""Verifies condition A02.FR.09: The Charging Station SHALL use the newest certificate, as measured by the start of the validity period."""
assert len(central_system.ws_server.websockets) == 1
old_connection = next(iter(central_system.ws_server.websockets))
# Setup: install new certificates
cert_directory = Path(
everest_core.everest_config["active_modules"]["evse_security"][
"config_module"
]["csms_leaf_cert_directory"]
)
key_directory = Path(
everest_core.everest_config["active_modules"]["evse_security"][
"config_module"
]["csms_leaf_key_directory"]
)
ca_certificate = (
Path(__file__).parent /
"../everest-aux/certs/ca/csms/CSMS_ROOT_CA.pem"
)
ca_key = Path(__file__).parent / \
"../everest-aux/certs/ca/csms/CSMS_ROOT_CA.key"
ca_passphrase = "123456" # nosec bandit B105
# Install 3 certificates; the second is newest w.r.t. validity (shortest relative shift from now to the past)
certificates = {}
for cert_index, (cert_name, relative_valid_time) in enumerate(
[("Cert1-notNewest", -50), ("Cert2-newest", -10),
("Cert3-notNewest", -100)]
):
cert_req, cert_key = CertificateHelper.generate_certificate_request(
cert_name
)
cert = CertificateHelper.sign_certificate_request(
cert_req,
issuer_certificate_path=ca_certificate,
issuer_private_key_path=ca_key,
issuer_private_key_passphrase=ca_passphrase,
serial=42 + cert_index,
relative_valid_time=relative_valid_time,
)
(cert_directory / f"{cert_name}.pem").write_text(cert)
(key_directory / f"{cert_name}.key").write_text(cert_key)
certificates[cert_name] = cert
test_controller.stop()
test_controller.start()
async def wait_for_reconnect():
while len(
central_system.ws_server.websockets
) < 1 or central_system.ws_server.websockets == {old_connection}:
await asyncio.sleep(0.1)
await asyncio.wait_for(wait_for_reconnect(), 5)
assert len(central_system.ws_server.websockets) == 1
new_connection = next(iter(central_system.ws_server.websockets))
new_certificate = new_connection.transport.get_extra_info("peercert")
self.assert_websocket_client_sslproto_certificate_equals_certificate(
new_certificate, certificates["Cert2-newest"]
)
@pytest.mark.parametrize("certificate_type", ["ChargingStation", "V2G"])
@pytest.mark.ocpp_config_adaptions(
GenericOCPP2XConfigAdjustment(
[
(
OCPP2XConfigVariableIdentifier(
"InternalCtrlr",
"V2GCertificateExpireCheckInitialDelaySeconds",
"Actual",
),
0,
)
]
)
)
@pytest.mark.ocpp_config_adaptions(
GenericOCPP2XConfigAdjustment(
[
(
OCPP2XConfigVariableIdentifier(
"InternalCtrlr",
"ClientCertificateExpireCheckInitialDelaySeconds",
"Actual",
),
0,
)
]
)
)
@pytest.mark.ocpp_config_adaptions(
_OCPP2XNetworkConnectionProfileAdjustment(None, None, 3)
)
async def test_A03_install_new_if_expired(
self,
certificate_type,
everest_core: EverestCore,
test_controller: TestController,
central_system: CentralSystem,
charge_point: ChargePoint201,
):
"""Verifies condition A03.FR.02: Expiring certificates shall be renewed."""
csms_mock = central_system.mock
self._setup_csms_mock(csms_mock, _CertificateSigningTestData())
# Setup: install new certificates that shortly expire
if certificate_type == "ChargingStation":
cert_directory = Path(
everest_core.everest_config["active_modules"]["evse_security"][
"config_module"
]["csms_leaf_cert_directory"]
)
key_directory = Path(
everest_core.everest_config["active_modules"]["evse_security"][
"config_module"
]["csms_leaf_key_directory"]
)
ca_certificate = (
Path(__file__).parent /
"../everest-aux/certs/ca/csms/CSMS_ROOT_CA.pem"
)
ca_key = (
Path(__file__).parent /
"../everest-aux/certs/ca/csms/CSMS_ROOT_CA.key"
)
ca_passphrase = "123456" # nosec bandit B10
else:
cert_directory = Path(
everest_core.everest_config["active_modules"]["evse_security"][
"config_module"
]["secc_leaf_cert_directory"]
)
key_directory = Path(
everest_core.everest_config["active_modules"]["evse_security"][
"config_module"
]["secc_leaf_cert_directory"]
)
ca_certificate = (
Path(__file__).parent /
"../everest-aux/certs/ca/cso/CPO_SUB_CA2.pem"
)
ca_key = (
Path(__file__).parent
/ "../everest-aux/certs/client/csms/CPO_SUB_CA2.key"
)
ca_passphrase = "123456" # nosec bandit B105
# Remove old certificates
for f in cert_directory.glob("*.pem"):
f.unlink()
for f in cert_directory.glob("*.key"):
f.unlink()
# Install new one almost expired
cert_req, cert_key = CertificateHelper.generate_certificate_request(
"almost expired"
)
cert = CertificateHelper.sign_certificate_request(
cert_req,
issuer_certificate_path=ca_certificate,
issuer_private_key_path=ca_key,
issuer_private_key_passphrase=ca_passphrase,
relative_expiration_time=300, # expires in 5 minutes
)
(cert_directory / "almost_expired.pem").write_text(cert)
(key_directory / "almost_expired.key").write_text(cert_key)
test_controller.stop()
test_controller.start()
await self._wait_for_mock_called(csms_mock.on_sign_certificate, timeout=10)
async def test_A01(
self, central_system: CentralSystem, charge_point_v201: ChargePoint201
):
# Disable AuthCacheCtrlr
r: call_result.SetVariables = (
await charge_point_v201.set_config_variables_req(
"SecurityCtrlr", "BasicAuthPassword", "BEEFDEADBEEFDEAD"
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
# wait for reconnect
await central_system.wait_for_chargepoint(wait_for_bootnotification=False)

View File

@@ -0,0 +1,699 @@
# SPDX-License-Identifier: Apache-2.0
# Copyright Pionix GmbH and Contributors to EVerest
import pytest
import asyncio
from datetime import datetime
# fmt: off
import logging
from everest.testing.core_utils.controller.test_controller_interface import TestController
from ocpp.routing import on, create_route_map
from ocpp.v201 import call as call201
from ocpp.v201 import call_result as call_result201
from ocpp.v201.enums import *
from ocpp.v201.datatypes import *
from everest.testing.ocpp_utils.fixtures import *
from everest_test_utils import * # Needs to be before the datatypes below since it overrides the v201 Action enum with the v16 one
from ocpp.v201.enums import (Action, IdTokenEnumType as IdTokenTypeEnum, SetVariableStatusEnumType, ClearCacheStatusEnumType, ConnectorStatusEnumType)
from validations import validate_status_notification_201
from everest.testing.core_utils._configuration.libocpp_configuration_helper import GenericOCPP2XConfigAdjustment, OCPP2XConfigVariableIdentifier
from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility, ValidationMode, validate_incoming_messages
# fmt: on
log = logging.getLogger("transactionsTest")
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
async def test_E04(
central_system_v201: CentralSystem,
test_controller: TestController,
test_utility: TestUtility,
):
"""
E04.FR.01
...
"""
# prepare data for the test
evse_id1 = 1
connector_id = 1
evse_id2 = 2
# make an unknown IdToken
id_token = IdTokenType(id_token="8BADF00D", type=IdTokenTypeEnum.iso14443)
log.info(
"##################### E04: Transaction started while charging station is offline #################"
)
test_controller.start()
charge_point_v201 = await central_system_v201.wait_for_chargepoint(
wait_for_bootnotification=True
)
# expect StatusNotification with status available
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
call201.StatusNotification(
datetime.now().isoformat(),
ConnectorStatusEnumType.available,
evse_id=evse_id1,
connector_id=connector_id,
),
validate_status_notification_201,
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
call201.StatusNotification(
datetime.now().isoformat(),
ConnectorStatusEnumType.available,
evse_id=evse_id2,
connector_id=connector_id,
),
validate_status_notification_201,
)
# Enable AuthCacheCtrlr
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"AuthCacheCtrlr", "Enabled", "true"
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
# Enable LocalPreAuthorize
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"AuthCtrlr", "LocalPreAuthorize", "true"
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
# Set AuthCacheLifeTime
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"AuthCacheCtrlr", "LifeTime", "86400"
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
# Clear cache
r: call_result201.ClearCache = await charge_point_v201.clear_cache_req()
assert r.status == ClearCacheStatusEnumType.accepted
# E04.FR.03 the queued transaction messages must contain the flag 'offline' as TRUE
# Enable offline authorization for unknown ID
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"AuthCtrlr", "OfflineTxForUnknownIdEnabled", "true"
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
# Enable AlignedDataSignReadings (Not implemented yet)
r: call_result201.SetVariables = (
await charge_point_v201.set_config_variables_req(
"AlignedDataCtrlr", "SignReadings", "true"
)
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
test_utility.messages.clear()
# Disconnect CS
log.debug(" Disconnect the CS from the CSMS")
test_controller.disconnect_websocket()
await asyncio.sleep(2)
# swipe id tag to authorize
test_controller.swipe(id_token.id_token)
# start charging session
test_controller.plug_in()
# charge for 30 seconds
await asyncio.sleep(30)
# swipe id tag to de-authorize
test_controller.swipe(id_token.id_token)
# stop charging session
test_controller.plug_out()
await asyncio.sleep(10)
# Connect CS
log.debug(" Connect the CS to the CSMS")
test_controller.connect_websocket()
# wait for reconnect
charge_point_v201 = await central_system_v201.wait_for_chargepoint(
wait_for_bootnotification=False
)
# All offline generated transaction messaages must be marked offline = True
# should send a Transaction event C15.FR.02
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Started", "offline": True},
)
# should send a Transaction event C15.FR.02
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Updated", "offline": True},
)
# should send a Transaction event C15.FR.02
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Ended", "offline": True},
)
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
@pytest.mark.inject_csms_mock
@pytest.mark.ocpp_config_adaptions(
GenericOCPP2XConfigAdjustment(
[
(
OCPP2XConfigVariableIdentifier(
"OCPPCommCtrlr", "MessageTimeout", "Actual"
),
"1",
),
(
OCPP2XConfigVariableIdentifier(
"OCPPCommCtrlr", "MessageAttemptInterval", "Actual"
),
"1",
),
(
OCPP2XConfigVariableIdentifier(
"OCPPCommCtrlr", "MessageAttempts", "Actual"
),
"3",
),
]
)
)
@pytest.mark.flaky(reruns=1)
async def test_cleanup_transaction_events_after_max_attempts_exhausted(
central_system: CentralSystem,
test_controller: TestController,
test_utility: TestUtility,
):
"""
Test if transaction events are properly cleaned up after the max message attempts
...
"""
# prepare data for the test
evse_id1 = 1
connector_id = 1
evse_id2 = 2
connector_id2 = 1
# make an unknown IdToken
id_token = IdTokenType(id_token="8BADF00D", type=IdTokenTypeEnum.iso14443)
test_controller.start()
charge_point_v201 = await central_system.wait_for_chargepoint(
wait_for_bootnotification=True
)
tx_event_attempt = SetVariableDataType(attribute_value="3", attribute_type=AttributeEnumType.actual,
component=ComponentType(name="OCPPCommCtrlr"), variable=VariableType(name="MessageAttempts", instance="TransactionEvent"))
tx_event_interval = SetVariableDataType(attribute_value="1", attribute_type=AttributeEnumType.actual,
component=ComponentType(name="OCPPCommCtrlr"), variable=VariableType(name="MessageAttemptInterval", instance="TransactionEvent"))
r: call_result201.SetVariables = (
await charge_point_v201.set_variables_req(set_variable_data=[tx_event_attempt, tx_event_interval])
)
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[0]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
set_variable_result: SetVariableResultType = SetVariableResultType(
**r.set_variable_result[1]
)
assert set_variable_result.attribute_status == SetVariableStatusEnumType.accepted
# expect StatusNotification with status available
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
call201.StatusNotification(
datetime.now().isoformat(),
ConnectorStatusEnumType.available,
evse_id=evse_id1,
connector_id=connector_id,
),
validate_status_notification_201,
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
call201.StatusNotification(
datetime.now().isoformat(),
ConnectorStatusEnumType.available,
evse_id=evse_id2,
connector_id=connector_id2,
),
validate_status_notification_201,
)
# return a CALLERROR for the transaction event
central_system.mock.on_transaction_event.side_effect = [
NotImplementedError()]
# swipe id tag to authorize
test_controller.swipe(id_token.id_token)
# start charging session
test_controller.plug_in()
# should send a Transaction event
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Started", "offline": False},
)
test_utility.validation_mode = ValidationMode.STRICT
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Started", "offline": False},
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Started", "offline": False},
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Updated", "offline": False},
)
test_utility.validation_mode = ValidationMode.EASY
central_system.mock.on_transaction_event.reset()
# respond properly to transaction events again
central_system.mock.on_transaction_event.side_effect = [
call_result201.TransactionEvent()
]
# stop charging session
test_controller.plug_out()
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Ended", "offline": False},
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
{"evseId": 1, "connectorId": 1, "connectorStatus": "Available"},
)
test_controller.stop()
# add a sleep to allow time for reboot
await asyncio.sleep(2)
test_controller.start()
# no attempts on delivering the transaction message should be made
assert await validate_incoming_messages(test_utility, charge_point_v201, "TransactionEvent", {}, timeout=10) is False
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
@pytest.mark.inject_csms_mock
@pytest.mark.ocpp_config_adaptions(
GenericOCPP2XConfigAdjustment(
[
(
OCPP2XConfigVariableIdentifier(
"AlignedDataCtrlr", "AlignedDataTxEndedInterval", "Actual"
),
"5",
)
]
)
)
async def test_two_parallel_transactions(
central_system: CentralSystem,
test_controller: TestController,
test_utility: TestUtility,
):
"""
Test if two parallel transactions work
...
"""
# prepare data for the test
evse_id1 = 1
connector_id = 1
evse_id2 = 2
connector_id2 = 1
# make an unknown IdToken
id_token = IdTokenType(id_token="8BADF00D", type=IdTokenTypeEnum.iso14443)
id_token2 = IdTokenType(id_token="ABAD1DEA", type=IdTokenTypeEnum.iso14443)
test_controller.start()
charge_point_v201 = await central_system.wait_for_chargepoint(
wait_for_bootnotification=True
)
# expect StatusNotification with status available
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
call201.StatusNotification(
datetime.now().isoformat(),
ConnectorStatusEnumType.available,
evse_id=evse_id1,
connector_id=connector_id,
),
validate_status_notification_201,
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
call201.StatusNotification(
datetime.now().isoformat(),
ConnectorStatusEnumType.available,
evse_id=evse_id2,
connector_id=connector_id2,
),
validate_status_notification_201,
)
# swipe id tag to authorize
test_controller.swipe(id_token.id_token, connectors=[1])
# start charging session
test_controller.plug_in(evse_id1)
# should send a Transaction event
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Started", "offline": False},
)
# swipe id tag to authorize
test_controller.swipe(id_token2.id_token, connectors=[2])
# start charging session
test_controller.plug_in(evse_id2)
# should send a Transaction event
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Started", "offline": False},
)
# let transactions run for a bit
await asyncio.sleep(10)
# # stop charging session
test_controller.plug_out(evse_id1)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Ended", "offline": False},
)
test_controller.plug_out(evse_id2)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Ended", "offline": False},
)
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
async def test_id_token_info_updated_in_tx_event(
central_system_v201: CentralSystem,
test_controller: TestController,
test_utility: TestUtility,
):
# prepare data for the test
evse_id1 = 1
connector_id = 1
evse_id2 = 2
# make an unknown IdToken
id_token = IdTokenType(id_token="8BADF00D", type=IdTokenTypeEnum.iso14443)
log.info(
"##################### Transaction with token info updated in transaction event #################"
)
test_controller.start()
charge_point_v201 = await central_system_v201.wait_for_chargepoint(
wait_for_bootnotification=True
)
# expect StatusNotification with status available
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
call201.StatusNotification(
datetime.now().isoformat(),
ConnectorStatusEnumType.available,
evse_id=evse_id1,
connector_id=connector_id,
),
validate_status_notification_201,
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"StatusNotification",
call201.StatusNotification(
datetime.now().isoformat(),
ConnectorStatusEnumType.available,
evse_id=evse_id2,
connector_id=connector_id,
),
validate_status_notification_201,
)
@on(Action.transaction_event)
def on_transaction_event(**kwargs):
msg = call201.TransactionEvent(**kwargs)
if msg.id_token != None:
msg_token = IdTokenType(**msg.id_token)
return call_result201.TransactionEvent(
id_token_info=IdTokenInfoType(
status=AuthorizationStatusEnumType.accepted,
group_id_token=IdTokenType(
id_token="123", type=IdTokenTypeEnum.central
),
)
)
else:
return call_result201.TransactionEvent()
setattr(charge_point_v201, "on_transaction_event", on_transaction_event)
central_system_v201.chargepoint.route_map = create_route_map(
central_system_v201.chargepoint
)
# swipe id tag to authorize
test_controller.swipe(id_token.id_token)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"Authorize",
{},
)
# start charging session
test_controller.plug_in()
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Started"},
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Updated"},
)
# charge for 10 seconds
await asyncio.sleep(10)
@on(Action.authorize)
def on_authorize(**kwargs):
msg = call201.Authorize(**kwargs)
msg_token = IdTokenType(**msg.id_token)
return call_result201.Authorize(
id_token_info=IdTokenInfoType(
status=AuthorizationStatusEnumType.accepted,
group_id_token=IdTokenType(
id_token="123", type=IdTokenTypeEnum.central
),
)
)
setattr(charge_point_v201, "on_authorize", on_authorize)
central_system_v201.chargepoint.route_map = create_route_map(
central_system_v201.chargepoint
)
# swipe id tag to de-authorize
id_token = IdTokenType(id_token="8BADF00A", type=IdTokenTypeEnum.iso14443)
test_controller.swipe(id_token.id_token)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"Authorize",
{},
)
# should send a Transaction event C15.FR.02
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Ended"},
)
# stop charging session
test_controller.plug_out()
@pytest.mark.asyncio
@pytest.mark.ocpp_version("ocpp2.0.1")
@pytest.mark.everest_core_config('everest-config-ocpp201-ps.yaml')
@pytest.mark.use_temporary_persistent_store
@pytest.mark.ocpp_config_adaptions(
GenericOCPP2XConfigAdjustment(
[
(
OCPP2XConfigVariableIdentifier(
"InternalCtrlr",
"ResumeTransactionsOnBoot",
"Actual",
),
"true",
),
]
)
)
async def test_stop_pending_transactions(
central_system_v201: CentralSystem,
test_controller: TestController,
test_utility: TestUtility,
):
logging.info("######### test_stop_pending_transactions #########")
# prepare data for the test
evse_id = 1
connector_id = 1
remote_start_id = 1
id_token = IdTokenType(id_token="DEADBEEF", type=IdTokenTypeEnum.iso14443)
test_controller.start()
charge_point_v201 = await central_system_v201.wait_for_chargepoint(
wait_for_bootnotification=True
)
await charge_point_v201.request_start_transaction_req(
id_token=id_token, remote_start_id=remote_start_id, evse_id=evse_id
)
test_controller.plug_in()
assert await wait_for_and_validate(
test_utility, charge_point_v201, "TransactionEvent", {
"eventType": "Started"}
)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{"eventType": "Updated"},
)
# charge for some time...
logging.debug("Charging for a while...")
await asyncio.sleep(2)
test_controller.stop()
await asyncio.sleep(2)
test_controller.start()
charge_point_v201 = await central_system_v201.wait_for_chargepoint(
wait_for_bootnotification=False
)
await asyncio.sleep(2)
assert await wait_for_and_validate(
test_utility,
charge_point_v201,
"TransactionEvent",
{
"eventType": "Ended",
"transactionInfo": {
"stoppedReason": "PowerLoss"
}
},
)