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:
File diff suppressed because it is too large
Load Diff
@@ -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"},
|
||||
)
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
)
|
||||
@@ -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)
|
||||
@@ -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,
|
||||
)
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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},
|
||||
)
|
||||
@@ -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
|
||||
@@ -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"}
|
||||
)
|
||||
@@ -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
@@ -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
@@ -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)
|
||||
@@ -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"
|
||||
}
|
||||
},
|
||||
)
|
||||
Reference in New Issue
Block a user