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:
@@ -0,0 +1,486 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import traceback
|
||||
# fmt: off
|
||||
import logging
|
||||
|
||||
from everest.testing.core_utils.controller.test_controller_interface import TestController
|
||||
|
||||
from ocpp.v21 import call as call21
|
||||
from ocpp.v21 import call_result as call_result21
|
||||
from ocpp.v21.enums import *
|
||||
from ocpp.v21.datatypes import *
|
||||
from ocpp.routing import on, create_route_map
|
||||
from everest.testing.ocpp_utils.fixtures import *
|
||||
from everest_test_utils import * # Needs to be before the datatypes below since it overrides the v21 Action enum with the v16 one
|
||||
from ocpp.v21.enums import (Action, ConnectorStatusEnumType, AuthorizationStatusEnumType, EnergyTransferModeEnumType, AttributeEnumType, GetVariableStatusEnumType, NotifyEVChargingNeedsStatusEnumType, NotifyAllowedEnergyTransferStatusEnumType)
|
||||
from validations import validate_status_notification_201
|
||||
from everest.testing.core_utils._configuration.libocpp_configuration_helper import GenericOCPP2XConfigAdjustment
|
||||
from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility
|
||||
# fmt: on
|
||||
|
||||
log = logging.getLogger("bidirectionalTest")
|
||||
|
||||
|
||||
def validate_notify_ev_charging_needs(meta_data, msg, expected):
|
||||
# Q01.FR.03
|
||||
return (
|
||||
msg.payload["evseId"] == expected["evseId"]
|
||||
and msg.payload["chargingNeeds"]["requestedEnergyTransfer"] == expected["requestedEnergyTransfer"]
|
||||
and msg.payload["chargingNeeds"]["v2xChargingParameters"]
|
||||
and msg.payload["chargingNeeds"]["controlMode"] == expected["controlMode"]
|
||||
)
|
||||
|
||||
|
||||
def validate_tx_event_with_evccid(meta_data, msg, expected):
|
||||
return (
|
||||
msg.payload["eventType"] == expected["eventType"]
|
||||
and msg.payload["idToken"]["additionalInfo"][0]["type"] == "EVCCID"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.xdist_group(name="ISO15118")
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config("everest-config-ocpp201-sil-dc-d20-eim.yaml")
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_q01(
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
test_utility: TestUtility,
|
||||
):
|
||||
"""
|
||||
Q01
|
||||
...
|
||||
"""
|
||||
|
||||
log.info(
|
||||
"##################### Q01: V2X Authorization #################"
|
||||
)
|
||||
id_token = IdTokenType(id_token="8BADF00D",
|
||||
type="ISO14443")
|
||||
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint(wait_for_bootnotification=True)
|
||||
|
||||
assert await wait_for_and_validate(
|
||||
test_utility,
|
||||
charge_point_v21,
|
||||
"StatusNotification",
|
||||
call21.StatusNotification(
|
||||
1, ConnectorStatusEnumType.available, 1, datetime.now().isoformat()
|
||||
),
|
||||
validate_status_notification_201,
|
||||
)
|
||||
|
||||
iso_enabled = GetVariableDataType(component=ComponentType(name="ISO15118Ctrlr", evse=EVSEType(id=1)),
|
||||
variable=VariableType(name="Enabled"),
|
||||
attribute_type=AttributeEnumType.actual)
|
||||
v2x_enabled = GetVariableDataType(component=ComponentType(name="V2XChargingCtrlr", evse=EVSEType(id=1)),
|
||||
variable=VariableType(name="Enabled"),
|
||||
attribute_type=AttributeEnumType.actual)
|
||||
v2x_supported_op_modes = GetVariableDataType(component=ComponentType(name="V2XChargingCtrlr", evse=EVSEType(id=1)),
|
||||
variable=VariableType(
|
||||
name="SupportedOperationModes"),
|
||||
attribute_type=AttributeEnumType.actual)
|
||||
v2x_supported_energy_transfers = GetVariableDataType(component=ComponentType(name="V2XChargingCtrlr", evse=EVSEType(id=1)),
|
||||
variable=VariableType(
|
||||
name="SupportedEnergyTransferModes"),
|
||||
attribute_type=AttributeEnumType.actual)
|
||||
list_of_vars = [iso_enabled, v2x_enabled,
|
||||
v2x_supported_op_modes, v2x_supported_energy_transfers]
|
||||
r: call_result21.GetVariables = await charge_point_v21.get_variables_req(get_variable_data=list_of_vars)
|
||||
|
||||
for result in r.get_variable_result:
|
||||
assert result['attribute_status'] == GetVariableStatusEnumType.accepted
|
||||
if result['variable']['name'] == 'Enabled':
|
||||
# Q01.FR.01
|
||||
assert result['attribute_value'] == 'true'
|
||||
elif result['variable']['name'] == 'SupportedOperationModes':
|
||||
# Q01.FR.31
|
||||
# TODO(mlitre) update to check for the min requirements once they are supported
|
||||
# Notably we need: ChargingOnly, CentralSetpoint and CentralFrequency
|
||||
assert result['attribute_value']
|
||||
elif result['variable']['name'] == 'SupportedEnergyTransferModes':
|
||||
# Q01.FR.32, we just check that it is not empty
|
||||
assert result['attribute_value']
|
||||
|
||||
test_controller.plug_in_dc_iso()
|
||||
assert await wait_for_and_validate(
|
||||
test_utility,
|
||||
charge_point_v21,
|
||||
"StatusNotification",
|
||||
call21.StatusNotification(
|
||||
1, ConnectorStatusEnumType.occupied, 1, datetime.now().isoformat()
|
||||
),
|
||||
validate_status_notification_201,
|
||||
)
|
||||
|
||||
@on(Action.authorize)
|
||||
def on_authorize(**kwargs):
|
||||
return call_result21.Authorize(
|
||||
id_token_info=IdTokenInfoType(
|
||||
status=AuthorizationStatusEnumType.accepted,
|
||||
group_id_token=IdTokenType(
|
||||
id_token="12345", type="Central"
|
||||
),
|
||||
),
|
||||
allowed_energy_transfer=[EnergyTransferModeEnumType.dc_bpt]
|
||||
)
|
||||
|
||||
setattr(charge_point_v21, "on_authorize", on_authorize)
|
||||
central_system_v21.chargepoint.route_map = create_route_map(
|
||||
central_system_v21.chargepoint
|
||||
)
|
||||
test_controller.swipe(id_token.id_token)
|
||||
|
||||
# Question: should we check other flows aka auth first then plug
|
||||
# Checking here Q01.FR.02 for idToken is not reliable, as we start tx before we get evcc id
|
||||
assert await wait_for_and_validate(
|
||||
test_utility,
|
||||
charge_point_v21,
|
||||
"TransactionEvent",
|
||||
{"eventType": "Started", "offline": False},
|
||||
)
|
||||
|
||||
# Q01.FR.03: Check all the fields of NotifyEVChargingNeeds
|
||||
assert await wait_for_and_validate(
|
||||
test_utility,
|
||||
charge_point_v21,
|
||||
"NotifyEVChargingNeeds",
|
||||
{"evseId": 1, "requestedEnergyTransfer": "DC",
|
||||
"controlMode": "DynamicControl", "mobilityNeedsMode": "EVCC"},
|
||||
validate_notify_ev_charging_needs
|
||||
)
|
||||
|
||||
# Check variables after NotifyEVChargingNeeds, so that we don't have to guess when the variables have been updated
|
||||
ev_available = GetVariableDataType(component=ComponentType(name="ConnectedEV", evse=EVSEType(id=1)),
|
||||
variable=VariableType(name="Available"),
|
||||
attribute_type=AttributeEnumType.actual)
|
||||
ev_vehicle_id = GetVariableDataType(component=ComponentType(name="ConnectedEV", evse=EVSEType(id=1)),
|
||||
variable=VariableType(
|
||||
name="VehicleId"),
|
||||
attribute_type=AttributeEnumType.actual)
|
||||
ev_protocol_agreed = GetVariableDataType(component=ComponentType(name="ConnectedEV", evse=EVSEType(id=1)),
|
||||
variable=VariableType(
|
||||
name="ProtocolAgreed"),
|
||||
attribute_type=AttributeEnumType.actual)
|
||||
ev_protocol_supported_by_ev1 = GetVariableDataType(component=ComponentType(name="ConnectedEV", evse=EVSEType(id=1)),
|
||||
variable=VariableType(
|
||||
name="ProtocolSupportedByEV", instance="1"),
|
||||
attribute_type=AttributeEnumType.actual)
|
||||
list_of_vars = [ev_available, ev_vehicle_id,
|
||||
ev_protocol_agreed, ev_protocol_supported_by_ev1]
|
||||
r: call_result21.GetVariables = await charge_point_v21.get_variables_req(get_variable_data=list_of_vars)
|
||||
# TODO(mlitre): Add check on VehicleCertificate when it is supported
|
||||
|
||||
# Q01.FR.36: Validate ConnectedEV variables
|
||||
for result in r.get_variable_result:
|
||||
assert result['attribute_status'] == GetVariableStatusEnumType.accepted
|
||||
if result['variable']['name'] == 'Available':
|
||||
assert result['attribute_value'] == 'true'
|
||||
elif result['variable']['name'] == 'VehicleId':
|
||||
assert result['attribute_value']
|
||||
elif result['variable']['name'] == 'ProtocolAgreed':
|
||||
assert result['attribute_value'] == 'urn:iso:std:iso:15118:-20:DC,1,0'
|
||||
elif result['variable']['name'] == 'ProtocolSupportedByEV':
|
||||
# TODO(mlitre): How many should we check?
|
||||
assert result['attribute_value']
|
||||
|
||||
assert await wait_for_and_validate(
|
||||
test_utility,
|
||||
charge_point_v21,
|
||||
"TransactionEvent",
|
||||
{"eventType": "Updated", "transactionInfo": {"chargingState": "Charging"}},
|
||||
)
|
||||
|
||||
test_controller.swipe(id_token.id_token)
|
||||
|
||||
assert await wait_for_and_validate(
|
||||
test_utility,
|
||||
charge_point_v21,
|
||||
"TransactionEvent",
|
||||
{"eventType": "Ended"},
|
||||
validate_tx_event_with_evccid
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.xdist_group(name="ISO15118")
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config("everest-config-ocpp201-sil-dc-d20-eim.yaml")
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_rejected_q01(
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
test_utility: TestUtility,
|
||||
):
|
||||
"""
|
||||
Q01
|
||||
...
|
||||
"""
|
||||
|
||||
log.info(
|
||||
"##################### Q01: V2X Authorization #################"
|
||||
)
|
||||
id_token = IdTokenType(id_token="8BADF00D",
|
||||
type="ISO14443")
|
||||
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint(wait_for_bootnotification=True)
|
||||
|
||||
assert await wait_for_and_validate(
|
||||
test_utility,
|
||||
charge_point_v21,
|
||||
"StatusNotification",
|
||||
call21.StatusNotification(
|
||||
1, ConnectorStatusEnumType.available, 1, datetime.now().isoformat()
|
||||
),
|
||||
validate_status_notification_201,
|
||||
)
|
||||
|
||||
iso_enabled = GetVariableDataType(component=ComponentType(name="ISO15118Ctrlr", evse=EVSEType(id=1)),
|
||||
variable=VariableType(name="Enabled"),
|
||||
attribute_type=AttributeEnumType.actual)
|
||||
v2x_enabled = GetVariableDataType(component=ComponentType(name="V2XChargingCtrlr", evse=EVSEType(id=1)),
|
||||
variable=VariableType(name="Enabled"),
|
||||
attribute_type=AttributeEnumType.actual)
|
||||
v2x_supported_op_modes = GetVariableDataType(component=ComponentType(name="V2XChargingCtrlr", evse=EVSEType(id=1)),
|
||||
variable=VariableType(
|
||||
name="SupportedOperationModes"),
|
||||
attribute_type=AttributeEnumType.actual)
|
||||
v2x_supported_energy_transfers = GetVariableDataType(component=ComponentType(name="V2XChargingCtrlr", evse=EVSEType(id=1)),
|
||||
variable=VariableType(
|
||||
name="SupportedEnergyTransferModes"),
|
||||
attribute_type=AttributeEnumType.actual)
|
||||
list_of_vars = [iso_enabled, v2x_enabled,
|
||||
v2x_supported_op_modes, v2x_supported_energy_transfers]
|
||||
r: call_result21.GetVariables = await charge_point_v21.get_variables_req(get_variable_data=list_of_vars)
|
||||
|
||||
for result in r.get_variable_result:
|
||||
assert result['attribute_status'] == GetVariableStatusEnumType.accepted
|
||||
if result['variable']['name'] == 'Enabled':
|
||||
# Q01.FR.01
|
||||
assert result['attribute_value'] == 'true'
|
||||
elif result['variable']['name'] == 'SupportedOperationModes':
|
||||
# Q01.FR.31
|
||||
# TODO(mlitre) update to check for the min requirements once they are supported: ChargingOnly, CentralSetpoint, CentralFrequency
|
||||
assert result['attribute_value']
|
||||
elif result['variable']['name'] == 'SupportedEnergyTransferModes':
|
||||
# Q01.FR.32, we just check that it is not empty
|
||||
assert result['attribute_value']
|
||||
|
||||
test_controller.plug_in_dc_iso()
|
||||
assert await wait_for_and_validate(
|
||||
test_utility,
|
||||
charge_point_v21,
|
||||
"StatusNotification",
|
||||
call21.StatusNotification(
|
||||
1, ConnectorStatusEnumType.occupied, 1, datetime.now().isoformat()
|
||||
),
|
||||
validate_status_notification_201,
|
||||
)
|
||||
|
||||
@on(Action.authorize)
|
||||
def on_authorize(**kwargs):
|
||||
return call_result21.Authorize(
|
||||
id_token_info=IdTokenInfoType(
|
||||
status=AuthorizationStatusEnumType.accepted,
|
||||
group_id_token=IdTokenType(
|
||||
id_token="12345", type="Central"
|
||||
),
|
||||
),
|
||||
allowed_energy_transfer=[EnergyTransferModeEnumType.dc_bpt]
|
||||
)
|
||||
|
||||
setattr(charge_point_v21, "on_authorize", on_authorize)
|
||||
central_system_v21.chargepoint.route_map = create_route_map(
|
||||
central_system_v21.chargepoint
|
||||
)
|
||||
test_controller.swipe(id_token.id_token)
|
||||
|
||||
assert await wait_for_and_validate(
|
||||
test_utility,
|
||||
charge_point_v21,
|
||||
"TransactionEvent",
|
||||
{"eventType": "Started", "offline": False},
|
||||
)
|
||||
|
||||
@on(Action.notify_ev_charging_needs)
|
||||
def on_notify_ev_charging_needs(**kwargs):
|
||||
return call_result21.NotifyEVChargingNeeds(
|
||||
status=NotifyEVChargingNeedsStatusEnumType.rejected
|
||||
)
|
||||
|
||||
setattr(charge_point_v21, "on_notify_ev_charging_needs",
|
||||
on_notify_ev_charging_needs)
|
||||
central_system_v21.chargepoint.route_map = create_route_map(
|
||||
central_system_v21.chargepoint
|
||||
)
|
||||
|
||||
# Q01.FR.03: Check all the fields of NotifyEVChargingNeeds
|
||||
assert await wait_for_and_validate(
|
||||
test_utility,
|
||||
charge_point_v21,
|
||||
"NotifyEVChargingNeeds",
|
||||
{"evseId": 1, "requestedEnergyTransfer": "DC",
|
||||
"controlMode": "DynamicControl", "mobilityNeedsMode": "EVCC"},
|
||||
validate_notify_ev_charging_needs
|
||||
)
|
||||
# Check variables after NotifyEVChargingNeeds, so that we don't have to guess when the variables have been updated
|
||||
ev_available = GetVariableDataType(component=ComponentType(name="ConnectedEV", evse=EVSEType(id=1)),
|
||||
variable=VariableType(name="Available"),
|
||||
attribute_type=AttributeEnumType.actual)
|
||||
ev_vehicle_id = GetVariableDataType(component=ComponentType(name="ConnectedEV", evse=EVSEType(id=1)),
|
||||
variable=VariableType(
|
||||
name="VehicleId"),
|
||||
attribute_type=AttributeEnumType.actual)
|
||||
ev_protocol_agreed = GetVariableDataType(component=ComponentType(name="ConnectedEV", evse=EVSEType(id=1)),
|
||||
variable=VariableType(
|
||||
name="ProtocolAgreed"),
|
||||
attribute_type=AttributeEnumType.actual)
|
||||
ev_protocol_supported_by_ev = GetVariableDataType(component=ComponentType(name="ConnectedEV", evse=EVSEType(id=1)),
|
||||
variable=VariableType(
|
||||
name="ProtocolSupportedByEV", instance="1"),
|
||||
attribute_type=AttributeEnumType.actual)
|
||||
list_of_vars = [ev_available, ev_vehicle_id,
|
||||
ev_protocol_agreed, ev_protocol_supported_by_ev]
|
||||
r: call_result21.GetVariables = await charge_point_v21.get_variables_req(get_variable_data=list_of_vars)
|
||||
# TODO(mlitre): Add check on VehicleCertificate when it is supported
|
||||
|
||||
# Q01.FR.36: Validate ConnectedEV variables
|
||||
for result in r.get_variable_result:
|
||||
assert result['attribute_status'] == GetVariableStatusEnumType.accepted
|
||||
if result['variable']['name'] == 'Available':
|
||||
assert result['attribute_value'] == 'true'
|
||||
elif result['variable']['name'] == 'VehicleId':
|
||||
# TODO(mlitre): Do we know the value before hand to check?
|
||||
assert result['attribute_value']
|
||||
elif result['variable']['name'] == 'ProtocolAgreed':
|
||||
assert result['attribute_value'] == 'urn:iso:std:iso:15118:-20:DC,1,0'
|
||||
elif result['variable']['name'] == 'ProtocolSupportedByEV':
|
||||
# TODO(mlitre): How many should we check?
|
||||
assert result['attribute_value']
|
||||
|
||||
# Q01.FR.06
|
||||
assert await wait_for_and_validate(
|
||||
test_utility,
|
||||
charge_point_v21,
|
||||
"TransactionEvent",
|
||||
{"eventType": "Ended", "transactionInfo": {
|
||||
"stoppedReason": "ReqEnergyTransferRejected"}, "triggerReason": "AbnormalCondition"},
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.xdist_group(name="ISO15118")
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config("everest-config-ocpp201-sil-dc-d20-eim.yaml")
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_q02_no_service_renegotiation(
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
test_utility: TestUtility,
|
||||
):
|
||||
"""
|
||||
Q02
|
||||
...
|
||||
"""
|
||||
|
||||
log.info(
|
||||
"##################### Q02: Starting in operationMode ChargingOnly before enabling V2X #################"
|
||||
)
|
||||
id_token = IdTokenType(id_token="8BADF00D",
|
||||
type="ISO14443")
|
||||
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint(wait_for_bootnotification=True)
|
||||
|
||||
assert await wait_for_and_validate(
|
||||
test_utility,
|
||||
charge_point_v21,
|
||||
"StatusNotification",
|
||||
call21.StatusNotification(
|
||||
1, ConnectorStatusEnumType.available, 1, datetime.now().isoformat()
|
||||
),
|
||||
validate_status_notification_201,
|
||||
)
|
||||
test_controller.plug_in_dc_iso()
|
||||
|
||||
# Make sure we don't start BPT
|
||||
@on(Action.authorize)
|
||||
def on_authorize(**kwargs):
|
||||
return call_result21.Authorize(
|
||||
id_token_info=IdTokenInfoType(
|
||||
status=AuthorizationStatusEnumType.accepted,
|
||||
group_id_token=IdTokenType(
|
||||
id_token="12345", type="Central"
|
||||
),
|
||||
),
|
||||
allowed_energy_transfer=[EnergyTransferModeEnumType.dc]
|
||||
)
|
||||
|
||||
setattr(charge_point_v21, "on_authorize", on_authorize)
|
||||
central_system_v21.chargepoint.route_map = create_route_map(
|
||||
central_system_v21.chargepoint
|
||||
)
|
||||
test_controller.swipe(id_token.id_token)
|
||||
r: call21.TransactionEvent = call21.TransactionEvent(
|
||||
**await wait_for_and_validate(
|
||||
test_utility,
|
||||
charge_point_v21,
|
||||
"TransactionEvent",
|
||||
{"eventType": "Started"},
|
||||
)
|
||||
)
|
||||
|
||||
transaction = TransactionType(**r.transaction_info)
|
||||
|
||||
assert await wait_for_and_validate(
|
||||
test_utility,
|
||||
charge_point_v21,
|
||||
"NotifyEVChargingNeeds",
|
||||
{"evseId": 1, "requestedEnergyTransfer": "DC",
|
||||
"controlMode": "DynamicControl", "mobilityNeedsMode": "EVCC"},
|
||||
validate_notify_ev_charging_needs
|
||||
)
|
||||
r: call_result21.NotifyAllowedEnergyTransfer = await charge_point_v21.notify_allowed_energy_transfer_request(allowed_energy_transfer=[EnergyTransferModeEnumType.dc_bpt], transaction_id=transaction.transaction_id)
|
||||
# TODO(mlitre): Once service renegotiation is supported expect Accepted instead of rejected
|
||||
assert r.status == NotifyAllowedEnergyTransferStatusEnumType.rejected
|
||||
587
tools/EVerest-main/tests/ocpp_tests/test_sets/ocpp21/der.py
Normal file
587
tools/EVerest-main/tests/ocpp_tests/test_sets/ocpp21/der.py
Normal file
@@ -0,0 +1,587 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
"""
|
||||
Integration smoke tests for OCPP 2.1 R04 - Configure DER control settings at Charging Station.
|
||||
|
||||
Covers the routing/availability behaviour end-to-end over a real websocket connection:
|
||||
* CS returns NotImplemented (CALLERROR) when DCDERCtrlr/ACDERCtrlr is not available
|
||||
* CS returns Accepted when DCDERCtrlr.Available=true and controlType is in ModesSupported
|
||||
* CS returns NotSupported when the controlType is not in ModesSupported
|
||||
* GetDERControl with no stored controls returns NotFound
|
||||
* ClearDERControl with no controls returns NotFound
|
||||
|
||||
These tests exercise the wiring done in libocpp: the DERControl functional block
|
||||
is conditionally instantiated based on the DERCtrlr.Available variable, and
|
||||
SetDERControl/GetDERControl/ClearDERControl messages are routed to it.
|
||||
"""
|
||||
|
||||
import pytest
|
||||
import logging
|
||||
|
||||
# fmt: off
|
||||
from everest.testing.core_utils.controller.test_controller_interface import TestController
|
||||
|
||||
from ocpp.v21 import call as call21
|
||||
from ocpp.v21 import call_result as call_result21
|
||||
from ocpp.v21.enums import *
|
||||
from ocpp.v21.datatypes import *
|
||||
from ocpp.routing import on, create_route_map
|
||||
|
||||
from everest.testing.ocpp_utils.fixtures import *
|
||||
from everest_test_utils import * # must come before v21 datatype re-imports
|
||||
from ocpp.v21.enums import Action, DERControlEnumType, DERControlStatusEnumType, DERUnitEnumType
|
||||
|
||||
from everest.testing.core_utils._configuration.libocpp_configuration_helper import GenericOCPP2XConfigAdjustment
|
||||
from everest.testing.ocpp_utils.charge_point_utils import TestUtility, OcppTestConfiguration, wait_for_and_validate
|
||||
# fmt: on
|
||||
|
||||
log = logging.getLogger("derControlTest")
|
||||
|
||||
# The DER controller component configs were removed from the shipped library and
|
||||
# e2e config sets pending a proper interface to provide them dynamically. Without
|
||||
# DCDERCtrlr_1 present in the device model these end-to-end tests cannot configure
|
||||
# DER, so skip the whole module for now. The DER behaviour stays covered by the
|
||||
# libocpp unit tests (DERControlTest), which carry their own DCDERCtrlr_1 fixture.
|
||||
pytestmark = pytest.mark.skip(
|
||||
reason="DER controller config removed pending a dynamic DER interface; "
|
||||
"covered by libocpp DERControlTest unit tests for now"
|
||||
)
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# R04 - DER not available: SetDERControl is rejected as NotImplemented
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config(
|
||||
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
||||
)
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_set_der_control_der_not_available_not_implemented(
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
test_utility: TestUtility,
|
||||
):
|
||||
"""R04: Without DCDERCtrlr/ACDERCtrlr Available, SetDERControl returns CALLERROR NotImplemented.
|
||||
|
||||
The Python ocpp library suppresses CallErrors by default (returning None)
|
||||
rather than raising, so we assert that the response is None. The underlying
|
||||
CALLError is logged as a warning by the library.
|
||||
"""
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint()
|
||||
|
||||
freq_droop = FreqDroopType(
|
||||
priority=0,
|
||||
over_freq=61.0,
|
||||
under_freq=59.0,
|
||||
over_droop=5.0,
|
||||
under_droop=5.0,
|
||||
response_time=3.0,
|
||||
)
|
||||
|
||||
r = await charge_point_v21.set_der_control_req(
|
||||
is_default=True,
|
||||
control_id="ctrl-1",
|
||||
control_type=DERControlEnumType.freq_droop,
|
||||
freq_droop=freq_droop,
|
||||
)
|
||||
# CALLError NotImplemented is suppressed to None by the library
|
||||
assert r is None
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# R04.FR.02 - DER available + supported controlType: Accepted
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config(
|
||||
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
||||
)
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
),
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"DCDERCtrlr_1", "DCDERCtrlrAvailable", "Actual"
|
||||
),
|
||||
"true",
|
||||
),
|
||||
# ModesSupported is ReadOnly; test relies on the default list in
|
||||
# DCDERCtrlr_1.json: FreqDroop,VoltWatt,LimitMaxDischarge,VoltVar,
|
||||
# FixedVar,EnterService,Gradients
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_set_der_control_supported_type_accepted(
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
test_utility: TestUtility,
|
||||
):
|
||||
"""R04.FR.02: With DCDERCtrlr Available and FreqDroop in ModesSupported, set default FreqDroop returns Accepted."""
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint()
|
||||
|
||||
freq_droop = FreqDroopType(
|
||||
priority=0,
|
||||
over_freq=61.0,
|
||||
under_freq=59.0,
|
||||
over_droop=5.0,
|
||||
under_droop=5.0,
|
||||
response_time=3.0,
|
||||
)
|
||||
|
||||
r: call_result21.SetDERControl = await charge_point_v21.set_der_control_req(
|
||||
is_default=True,
|
||||
control_id="ctrl-default-fd",
|
||||
control_type=DERControlEnumType.freq_droop,
|
||||
freq_droop=freq_droop,
|
||||
)
|
||||
assert r.status == DERControlStatusEnumType.accepted
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# R04.FR.01 - DER available but controlType not in ModesSupported: NotSupported
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config(
|
||||
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
||||
)
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
),
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"DCDERCtrlr_1", "DCDERCtrlrAvailable", "Actual"
|
||||
),
|
||||
"true",
|
||||
),
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"DCDERCtrlr_1", "DCDERCtrlrModesSupported", "Actual"
|
||||
),
|
||||
# HFMustTrip is intentionally NOT in this list
|
||||
"FreqDroop,VoltWatt",
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_set_der_control_unsupported_type_not_supported(
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
test_utility: TestUtility,
|
||||
):
|
||||
"""R04.FR.01: With DCDERCtrlr Available but controlType not in ModesSupported, returns NotSupported.
|
||||
|
||||
HFMustTrip is intentionally NOT in the default ModesSupported list
|
||||
(DCDERCtrlr_1.json), so this should return NotSupported.
|
||||
"""
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint()
|
||||
|
||||
# HFMustTrip curve with yUnit=Not_Applicable (valid shape, unsupported type).
|
||||
# NOTE: StrEnum auto-converts "Not_Applicable" -> Python attr `not__applicable` (double underscore).
|
||||
curve = DERCurveType(
|
||||
curve_data=[DERCurvePointsType(x=62.0, y=1.0)],
|
||||
priority=0,
|
||||
y_unit=DERUnitEnumType.not__applicable,
|
||||
)
|
||||
|
||||
r: call_result21.SetDERControl = await charge_point_v21.set_der_control_req(
|
||||
is_default=True,
|
||||
control_id="ctrl-hf-trip",
|
||||
control_type=DERControlEnumType.hf_must_trip,
|
||||
curve=curve,
|
||||
)
|
||||
assert r.status == DERControlStatusEnumType.not_supported
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# R04.FR.30 - GetDERControl with no stored controls: NotFound
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config(
|
||||
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
||||
)
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
),
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"DCDERCtrlr_1", "DCDERCtrlrAvailable", "Actual"
|
||||
),
|
||||
"true",
|
||||
),
|
||||
# ModesSupported is ReadOnly; test relies on the default list in
|
||||
# DCDERCtrlr_1.json: FreqDroop,VoltWatt,LimitMaxDischarge,VoltVar,
|
||||
# FixedVar,EnterService,Gradients
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_get_der_control_no_controls_not_found(
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
test_utility: TestUtility,
|
||||
):
|
||||
"""R04.FR.30: GetDERControl when no matching controls exist returns NotFound."""
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint()
|
||||
|
||||
r: call_result21.GetDERControl = await charge_point_v21.get_der_control_req(
|
||||
request_id=42,
|
||||
control_type=DERControlEnumType.freq_droop,
|
||||
)
|
||||
assert r.status == DERControlStatusEnumType.not_found
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# R04.FR.41 - ClearDERControl with no matching controls: NotFound
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config(
|
||||
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
||||
)
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
),
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"DCDERCtrlr_1", "DCDERCtrlrAvailable", "Actual"
|
||||
),
|
||||
"true",
|
||||
),
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"DCDERCtrlr_1", "DCDERCtrlrModesSupported", "Actual"
|
||||
),
|
||||
"FreqDroop,VoltWatt",
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_clear_der_control_no_match_not_found(
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
test_utility: TestUtility,
|
||||
):
|
||||
"""R04.FR.41: ClearDERControl when no matching controls exist returns NotFound."""
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint()
|
||||
|
||||
r: call_result21.ClearDERControl = await charge_point_v21.clear_der_control_req(
|
||||
is_default=True,
|
||||
control_type=DERControlEnumType.freq_droop,
|
||||
)
|
||||
assert r.status == DERControlStatusEnumType.not_found
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# TC_R_102 end-to-end - Clearing controlTypes
|
||||
# (R04.FR.02, FR.30, FR.33, FR.41, FR.44, FR.45)
|
||||
#
|
||||
# Exercises the cross-message state evolution that the unit tests can't:
|
||||
# Set defaults of two distinct types, Clear-by-type one of them, verify the
|
||||
# other survives, then Clear-all and verify the table is empty. Multi-row
|
||||
# Report payload ordering itself is unit-tested in
|
||||
# DERControlTest.GetDERControl_ReportOrdersMultiRowByIsSuperseded; this test
|
||||
# locks in that the persisted state evolves correctly across messages.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config(
|
||||
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
||||
)
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
),
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"DCDERCtrlr_1", "DCDERCtrlrAvailable", "Actual"
|
||||
),
|
||||
"true",
|
||||
),
|
||||
# ModesSupported is ReadOnly; relies on the default list in
|
||||
# DCDERCtrlr_1.json which includes both FreqDroop and VoltWatt.
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_tc_r_102_clear_by_type_then_clear_all(
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
test_utility: TestUtility,
|
||||
):
|
||||
"""TC_R_102: Set two defaults of distinct types, clear one by type, verify
|
||||
the other survives, then clear all and verify the table is empty.
|
||||
"""
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint()
|
||||
|
||||
# Step 1: SetDERControl default FreqDroop (R04.FR.02)
|
||||
freq_droop = FreqDroopType(
|
||||
priority=0,
|
||||
over_freq=61.0,
|
||||
under_freq=59.0,
|
||||
over_droop=5.0,
|
||||
under_droop=5.0,
|
||||
response_time=3.0,
|
||||
)
|
||||
r: call_result21.SetDERControl = await charge_point_v21.set_der_control_req(
|
||||
is_default=True,
|
||||
control_id="ctrl-default-fd",
|
||||
control_type=DERControlEnumType.freq_droop,
|
||||
freq_droop=freq_droop,
|
||||
)
|
||||
assert r.status == DERControlStatusEnumType.accepted
|
||||
|
||||
# Step 2: SetDERControl default VoltWatt
|
||||
volt_watt_curve = DERCurveType(
|
||||
curve_data=[DERCurvePointsType(x=240.0, y=100.0)],
|
||||
priority=1,
|
||||
y_unit=DERUnitEnumType.pct_maxw,
|
||||
)
|
||||
r = await charge_point_v21.set_der_control_req(
|
||||
is_default=True,
|
||||
control_id="ctrl-default-vw",
|
||||
control_type=DERControlEnumType.volt_watt,
|
||||
curve=volt_watt_curve,
|
||||
)
|
||||
assert r.status == DERControlStatusEnumType.accepted
|
||||
|
||||
# Step 3: GetDERControl FreqDroop -> Accepted (the row exists)
|
||||
r_get: call_result21.GetDERControl = await charge_point_v21.get_der_control_req(
|
||||
request_id=1,
|
||||
is_default=True,
|
||||
control_type=DERControlEnumType.freq_droop,
|
||||
)
|
||||
assert r_get.status == DERControlStatusEnumType.accepted
|
||||
|
||||
# Step 4: GetDERControl VoltWatt -> Accepted (the row exists)
|
||||
r_get = await charge_point_v21.get_der_control_req(
|
||||
request_id=2,
|
||||
is_default=True,
|
||||
control_type=DERControlEnumType.volt_watt,
|
||||
)
|
||||
assert r_get.status == DERControlStatusEnumType.accepted
|
||||
|
||||
# Step 5: ClearDERControl by type=FreqDroop (R04.FR.45)
|
||||
r_clear: call_result21.ClearDERControl = (
|
||||
await charge_point_v21.clear_der_control_req(
|
||||
is_default=True,
|
||||
control_type=DERControlEnumType.freq_droop,
|
||||
)
|
||||
)
|
||||
assert r_clear.status == DERControlStatusEnumType.accepted
|
||||
|
||||
# Step 6: GetDERControl FreqDroop -> NotFound (the row was cleared, R04.FR.30)
|
||||
r_get = await charge_point_v21.get_der_control_req(
|
||||
request_id=3,
|
||||
is_default=True,
|
||||
control_type=DERControlEnumType.freq_droop,
|
||||
)
|
||||
assert r_get.status == DERControlStatusEnumType.not_found
|
||||
|
||||
# Step 7: GetDERControl VoltWatt -> Accepted (untouched by the targeted clear)
|
||||
r_get = await charge_point_v21.get_der_control_req(
|
||||
request_id=4,
|
||||
is_default=True,
|
||||
control_type=DERControlEnumType.volt_watt,
|
||||
)
|
||||
assert r_get.status == DERControlStatusEnumType.accepted
|
||||
|
||||
# Step 8: ClearDERControl with no controlType / controlId,
|
||||
# isDefault=true -> clear ALL matching defaults (R04.FR.44)
|
||||
r_clear = await charge_point_v21.clear_der_control_req(is_default=True)
|
||||
assert r_clear.status == DERControlStatusEnumType.accepted
|
||||
|
||||
# Step 9: GetDERControl any default -> NotFound (table is empty)
|
||||
r_get = await charge_point_v21.get_der_control_req(
|
||||
request_id=5,
|
||||
is_default=True,
|
||||
control_type=DERControlEnumType.volt_watt,
|
||||
)
|
||||
assert r_get.status == DERControlStatusEnumType.not_found
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# TC_R_103 - SetDERControl with unsupported controlType returns NotSupported.
|
||||
# (R04.FR.01) Locks in the spec contract that prior log analysis showed the
|
||||
# certification tool can violate by picking a supported type for this step.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config(
|
||||
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
||||
)
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
),
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"DCDERCtrlr_1", "DCDERCtrlrAvailable", "Actual"
|
||||
),
|
||||
"true",
|
||||
),
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"DCDERCtrlr_1", "DCDERCtrlrModesSupported", "Actual"
|
||||
),
|
||||
# WattVar deliberately omitted.
|
||||
"FreqDroop,VoltWatt,VoltVar,FixedVar,LimitMaxDischarge,EnterService,Gradients",
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_tc_r_103_unsupported_control_type_returns_not_supported(
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
test_utility: TestUtility,
|
||||
):
|
||||
"""TC_R_103 step 2: SetDERControl with controlType not in ModesSupported -> NotSupported."""
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint()
|
||||
|
||||
watt_var_curve = DERCurveType(
|
||||
curve_data=[DERCurvePointsType(x=100.0, y=50.0)],
|
||||
priority=0,
|
||||
y_unit=DERUnitEnumType.pct_max_var,
|
||||
)
|
||||
|
||||
r: call_result21.SetDERControl = await charge_point_v21.set_der_control_req(
|
||||
is_default=True,
|
||||
control_id="ctrl-wv",
|
||||
control_type=DERControlEnumType.watt_var,
|
||||
curve=watt_var_curve,
|
||||
)
|
||||
assert r.status == DERControlStatusEnumType.not_supported
|
||||
|
||||
|
||||
# -----------------------------------------------------------------------------
|
||||
# TC_R_104 - GetDERControl Accepted MUST be followed by an outbound
|
||||
# ReportDERControl message. (R04.FR.32-33). Regression guard for a libocpp bug
|
||||
# where MessageType::ReportDERControl was missing from the enum, causing the
|
||||
# outbound Call to throw and emit a FormationViolation CallError instead.
|
||||
# -----------------------------------------------------------------------------
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config(
|
||||
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
||||
)
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
),
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"DCDERCtrlr_1", "DCDERCtrlrAvailable", "Actual"
|
||||
),
|
||||
"true",
|
||||
),
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_tc_r_104_get_emits_report_der_control(
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
test_utility: TestUtility,
|
||||
):
|
||||
"""TC_R_104: SetDERControl(default FreqDroop), then GetDERControl. CS must
|
||||
respond with GetDERControlResponse(status=Accepted) AND then emit a separate
|
||||
ReportDERControl message carrying the stored control.
|
||||
"""
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint()
|
||||
|
||||
freq_droop = FreqDroopType(
|
||||
priority=0,
|
||||
over_freq=61.0,
|
||||
under_freq=59.0,
|
||||
over_droop=5.0,
|
||||
under_droop=5.0,
|
||||
response_time=3.0,
|
||||
)
|
||||
r_set: call_result21.SetDERControl = await charge_point_v21.set_der_control_req(
|
||||
is_default=True,
|
||||
control_id="ctrl-r104",
|
||||
control_type=DERControlEnumType.freq_droop,
|
||||
freq_droop=freq_droop,
|
||||
)
|
||||
assert r_set.status == DERControlStatusEnumType.accepted
|
||||
|
||||
r_get: call_result21.GetDERControl = await charge_point_v21.get_der_control_req(
|
||||
request_id=104,
|
||||
is_default=True,
|
||||
control_type=DERControlEnumType.freq_droop,
|
||||
)
|
||||
assert r_get.status == DERControlStatusEnumType.accepted
|
||||
|
||||
# The wire-level message that the libocpp bug used to suppress.
|
||||
assert await wait_for_and_validate(
|
||||
test_utility,
|
||||
charge_point_v21,
|
||||
"ReportDERControl",
|
||||
{"requestId": 104},
|
||||
)
|
||||
@@ -0,0 +1,971 @@
|
||||
# SPDX-License-Identifier: Apache-2.0
|
||||
# Copyright Pionix GmbH and Contributors to EVerest
|
||||
|
||||
import pytest
|
||||
from datetime import datetime, timezone
|
||||
|
||||
import traceback
|
||||
# fmt: off
|
||||
import logging
|
||||
|
||||
from everest.testing.core_utils.controller.test_controller_interface import TestController
|
||||
|
||||
from ocpp.v21 import call as call21
|
||||
from ocpp.v21 import call_result as call_result21
|
||||
from ocpp.v21.enums import *
|
||||
from ocpp.v21.datatypes import *
|
||||
from ocpp.routing import on, create_route_map
|
||||
from everest.testing.ocpp_utils.fixtures import *
|
||||
from everest_test_utils import * # Needs to be before the datatypes below since it overrides the v21 Action enum with the v16 one
|
||||
from ocpp.v21.enums import (
|
||||
Action,
|
||||
ConnectorStatusEnumType,
|
||||
AttributeEnumType,
|
||||
)
|
||||
from validations import validate_status_notification_201
|
||||
from everest.testing.core_utils._configuration.libocpp_configuration_helper import GenericOCPP2XConfigAdjustment
|
||||
from everest.testing.ocpp_utils.charge_point_utils import wait_for_and_validate, TestUtility, OcppTestConfiguration
|
||||
# fmt: on
|
||||
|
||||
log = logging.getLogger("provisioningTest")
|
||||
|
||||
|
||||
def validate_set_variables_success(response, expected_count=1):
|
||||
"""Validate SetVariables response indicates success.
|
||||
response is a call_result.SetVariables with set_variable_result as list of dicts.
|
||||
"""
|
||||
if not response or not response.set_variable_result:
|
||||
return False
|
||||
|
||||
success_count = sum(
|
||||
1
|
||||
for r in response.set_variable_result
|
||||
if r.get("attribute_status") == "Accepted"
|
||||
)
|
||||
return success_count >= expected_count
|
||||
|
||||
|
||||
def validate_set_variables_rejected(response, expected_reason=None):
|
||||
"""Validate SetVariables response indicates rejection.
|
||||
response is a call_result.SetVariables with set_variable_result as list of dicts.
|
||||
"""
|
||||
if not response or not response.set_variable_result:
|
||||
return False
|
||||
|
||||
for result in response.set_variable_result:
|
||||
status = result.get("attribute_status")
|
||||
if status != "Accepted":
|
||||
if expected_reason:
|
||||
status_info = result.get("attribute_status_info") or {}
|
||||
reason = status_info.get("reason_code", "").upper()
|
||||
return expected_reason.upper() in reason
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config(
|
||||
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
||||
)
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_cold_boot_01(
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
test_utility: TestUtility,
|
||||
):
|
||||
"""
|
||||
B01.FR.01
|
||||
...
|
||||
"""
|
||||
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint()
|
||||
|
||||
try:
|
||||
# expect StatusNotification with status available
|
||||
assert await wait_for_and_validate(
|
||||
test_utility,
|
||||
charge_point_v21,
|
||||
"StatusNotification",
|
||||
call21.StatusNotification(
|
||||
1, ConnectorStatusEnumType.available, 1, datetime.now().isoformat()
|
||||
),
|
||||
validate_status_notification_201,
|
||||
)
|
||||
except Exception as e:
|
||||
traceback.print_exc()
|
||||
logging.critical(e)
|
||||
|
||||
# TOOD(piet): Check configured HeartbeatInterval of BootNotificationResponse
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config(
|
||||
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
||||
)
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_cold_boot_pending_01(
|
||||
test_config: OcppTestConfiguration,
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
test_utility: TestUtility,
|
||||
):
|
||||
|
||||
@on(Action.boot_notification)
|
||||
def on_boot_notification_pending(**kwargs):
|
||||
return call_result21.BootNotification(
|
||||
current_time=datetime.now().isoformat(),
|
||||
interval=5,
|
||||
status=RegistrationStatusEnumType.pending,
|
||||
)
|
||||
|
||||
@on(Action.boot_notification)
|
||||
def on_boot_notification_accepted(**kwargs):
|
||||
return call_result21.BootNotification(
|
||||
current_time=datetime.now(timezone.utc).isoformat(),
|
||||
interval=5,
|
||||
status=RegistrationStatusEnumType.accepted,
|
||||
)
|
||||
|
||||
test_utility.forbidden_actions.append("SecurityEventNotification")
|
||||
|
||||
central_system_v21.function_overrides.append(
|
||||
("on_boot_notification", on_boot_notification_pending)
|
||||
)
|
||||
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint()
|
||||
|
||||
setattr(charge_point_v21, "on_boot_notification",
|
||||
on_boot_notification_accepted)
|
||||
central_system_v21.chargepoint.route_map = create_route_map(
|
||||
central_system_v21.chargepoint
|
||||
)
|
||||
|
||||
assert await wait_for_and_validate(
|
||||
test_utility, charge_point_v21, "BootNotification", {}
|
||||
)
|
||||
|
||||
test_utility.forbidden_actions.clear()
|
||||
|
||||
test_controller.plug_in()
|
||||
|
||||
assert await wait_for_and_validate(
|
||||
test_utility, charge_point_v21, "SecurityEventNotification", {}
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config(
|
||||
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
||||
)
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_cold_boot_rejected_01(
|
||||
test_config: OcppTestConfiguration,
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
test_utility: TestUtility,
|
||||
):
|
||||
|
||||
@on(Action.boot_notification)
|
||||
def on_boot_notification_pending(**kwargs):
|
||||
return call_result21.BootNotification(
|
||||
current_time=datetime.now().isoformat(),
|
||||
interval=5,
|
||||
status=RegistrationStatusEnumType.rejected,
|
||||
)
|
||||
|
||||
@on(Action.boot_notification)
|
||||
def on_boot_notification_accepted(**kwargs):
|
||||
return call_result21.BootNotification(
|
||||
current_time=datetime.now(timezone.utc).isoformat(),
|
||||
interval=5,
|
||||
status=RegistrationStatusEnumType.accepted,
|
||||
)
|
||||
|
||||
central_system_v21.function_overrides.append(
|
||||
("on_boot_notification", on_boot_notification_pending)
|
||||
)
|
||||
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint()
|
||||
|
||||
setattr(charge_point_v21, "on_boot_notification",
|
||||
on_boot_notification_accepted)
|
||||
central_system_v21.chargepoint.route_map = create_route_map(
|
||||
central_system_v21.chargepoint
|
||||
)
|
||||
|
||||
assert await wait_for_and_validate(
|
||||
test_utility, charge_point_v21, "BootNotification", {}
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config(
|
||||
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
||||
)
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_get_network_configuration_slot_1(
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
):
|
||||
"""
|
||||
B09.FR.01 - Get NetworkConfiguration for slot 1 (primary configuration)
|
||||
Verify that GetVariables can retrieve NetworkConfiguration[1] variables
|
||||
"""
|
||||
log.info(
|
||||
"##################### B09.FR.01: Get NetworkConfiguration Slot 1 #################"
|
||||
)
|
||||
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint(
|
||||
wait_for_bootnotification=True
|
||||
)
|
||||
|
||||
# Request OcppCsmsUrl for NetworkConfiguration slot 1
|
||||
get_var = GetVariableDataType(
|
||||
component=ComponentType(
|
||||
name="NetworkConfiguration", instance="1"
|
||||
),
|
||||
variable=VariableType(name="OcppCsmsUrl"),
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
)
|
||||
|
||||
response = await charge_point_v21.get_variables_req(
|
||||
get_variable_data=[get_var]
|
||||
)
|
||||
|
||||
assert response and response.get_variable_result, "No get variable result"
|
||||
results = response.get_variable_result
|
||||
assert len(results) > 0, "Empty get variable result"
|
||||
assert (
|
||||
results[0].get("attribute_status") == "Accepted"
|
||||
), f"Failed to get OcppCsmsUrl: {results[0]}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config(
|
||||
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
||||
)
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_set_network_configuration_slot_2(
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
):
|
||||
"""
|
||||
B09.FR.09 - Set NetworkConfiguration for slot 2 (backup configuration)
|
||||
Verify that SetVariables can update NetworkConfiguration[2] variables
|
||||
which are not in the priority list
|
||||
"""
|
||||
log.info(
|
||||
"##################### B09.FR.09: Set NetworkConfiguration Slot 2 #################"
|
||||
)
|
||||
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint(
|
||||
wait_for_bootnotification=True
|
||||
)
|
||||
|
||||
# Set OcppCsmsUrl for NetworkConfiguration slot 2
|
||||
set_var = SetVariableDataType(
|
||||
component=ComponentType(
|
||||
name="NetworkConfiguration", instance="2"
|
||||
),
|
||||
variable=VariableType(name="OcppCsmsUrl"),
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
attribute_value="wss://backup-csms.example.com/ocpp",
|
||||
)
|
||||
|
||||
response = await charge_point_v21.set_variables_req(
|
||||
set_variable_data=[set_var]
|
||||
)
|
||||
|
||||
assert validate_set_variables_success(response, 1), \
|
||||
f"Failed to set OcppCsmsUrl on slot 2: {response}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config(
|
||||
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
||||
)
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_get_all_network_configuration_variables_slot_1(
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
):
|
||||
"""
|
||||
Verify GetVariables can retrieve all NetworkConfiguration[1] variables
|
||||
"""
|
||||
log.info(
|
||||
"##################### Get All NetworkConfiguration[1] Variables #################"
|
||||
)
|
||||
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint(
|
||||
wait_for_bootnotification=True
|
||||
)
|
||||
|
||||
# Request all main configuration variables for slot 1
|
||||
variable_names = [
|
||||
"OcppCsmsUrl",
|
||||
"SecurityProfile",
|
||||
"OcppInterface",
|
||||
"OcppTransport",
|
||||
"MessageTimeout",
|
||||
]
|
||||
|
||||
get_vars = [
|
||||
GetVariableDataType(
|
||||
component=ComponentType(
|
||||
name="NetworkConfiguration", instance="1"
|
||||
),
|
||||
variable=VariableType(name=var_name),
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
)
|
||||
for var_name in variable_names
|
||||
]
|
||||
|
||||
response = await charge_point_v21.get_variables_req(
|
||||
get_variable_data=get_vars
|
||||
)
|
||||
|
||||
assert response and response.get_variable_result, "No get variable result"
|
||||
results = response.get_variable_result
|
||||
assert len(results) == len(variable_names), "Not all variables returned"
|
||||
|
||||
# Count successful retrievals
|
||||
success_count = sum(
|
||||
1
|
||||
for r in results
|
||||
if r.get("attribute_status") == "Accepted"
|
||||
)
|
||||
assert (
|
||||
success_count == len(variable_names)
|
||||
), f"Some variables failed: {results}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config(
|
||||
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
||||
)
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_network_configuration_priority_list_management(
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
):
|
||||
"""
|
||||
TC_B_107_CS: Add and remove slots from NetworkConfigurationPriority list
|
||||
B09.FR.21, B09.FR.22, B09.FR.23 - Verify that CSMS can manage which configuration slots
|
||||
are in the priority list, and verify that configuration values are consistent when
|
||||
slots are added/removed from the priority list.
|
||||
"""
|
||||
log.info(
|
||||
"##################### TC_B_107_CS: NetworkConfigurationPriority List Management #################"
|
||||
)
|
||||
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint(
|
||||
wait_for_bootnotification=True
|
||||
)
|
||||
|
||||
# First, set a configuration on slot 2 (which is not in priority by default)
|
||||
set_vars = [
|
||||
SetVariableDataType(
|
||||
component=ComponentType(name="NetworkConfiguration", instance="2"),
|
||||
variable=VariableType(name="OcppCsmsUrl"),
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
attribute_value="wss://backup-csms.example.com/ocpp",
|
||||
),
|
||||
SetVariableDataType(
|
||||
component=ComponentType(name="NetworkConfiguration", instance="2"),
|
||||
variable=VariableType(name="SecurityProfile"),
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
attribute_value="1",
|
||||
),
|
||||
]
|
||||
|
||||
response = await charge_point_v21.set_variables_req(
|
||||
set_variable_data=set_vars
|
||||
)
|
||||
|
||||
assert validate_set_variables_success(
|
||||
response, len(set_vars)
|
||||
), f"Failed to set initial configuration on slot 2: {response}"
|
||||
|
||||
# Verify the configuration was set
|
||||
get_var = GetVariableDataType(
|
||||
component=ComponentType(name="NetworkConfiguration", instance="2"),
|
||||
variable=VariableType(name="OcppCsmsUrl"),
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
)
|
||||
|
||||
response = await charge_point_v21.get_variables_req(
|
||||
get_variable_data=[get_var]
|
||||
)
|
||||
|
||||
assert response and response.get_variable_result, "No get variable result"
|
||||
results = response.get_variable_result
|
||||
assert len(results) > 0, "Empty get variable result"
|
||||
assert (
|
||||
results[0].get("attribute_status") == "Accepted"
|
||||
), f"Failed to get OcppCsmsUrl from slot 2: {results[0]}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config(
|
||||
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
||||
)
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_network_configuration_reject_priority_slot_changes(
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
):
|
||||
"""
|
||||
TC_B_108_CS: Reject SetVariables on the currently active NetworkConfiguration slot
|
||||
B09.FR.22 - Verify that attempts to change configuration on the active slot
|
||||
are rejected with "PriorityNetworkConf" reason code.
|
||||
"""
|
||||
log.info(
|
||||
"##################### TC_B_108_CS: Reject Changes to Priority Slots #################"
|
||||
)
|
||||
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint(
|
||||
wait_for_bootnotification=True
|
||||
)
|
||||
|
||||
# Attempt to change NetworkConfiguration slot 1 (which is in priority by default)
|
||||
# This should be rejected with "PriorityNetworkConf" reason
|
||||
set_var = SetVariableDataType(
|
||||
component=ComponentType(name="NetworkConfiguration", instance="1"),
|
||||
variable=VariableType(name="OcppCsmsUrl"),
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
attribute_value="wss://new-csms.example.com/ocpp",
|
||||
)
|
||||
|
||||
response = await charge_point_v21.set_variables_req(
|
||||
set_variable_data=[set_var]
|
||||
)
|
||||
|
||||
# Validate that the change was rejected with PriorityNetworkConf reason
|
||||
assert validate_set_variables_rejected(
|
||||
response, "PriorityNetworkConf"
|
||||
), f"Expected SetVariables to be rejected with PriorityNetworkConf, but got: {response}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config(
|
||||
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
||||
)
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_network_configuration_security_downgrade_rejection_set_network_profile(
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
):
|
||||
"""
|
||||
TC_B_110_CS: Reject security profile downgrade via SetNetworkProfileRequest
|
||||
B09.FR.31 - Verify that SetNetworkProfileRequest is rejected with "NoSecurityDowngrade"
|
||||
reason when attempting to downgrade the active security profile.
|
||||
"""
|
||||
log.info(
|
||||
"##################### TC_B_110_CS: Reject Security Downgrade via SetNetworkProfileRequest #################"
|
||||
)
|
||||
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint(
|
||||
wait_for_bootnotification=True
|
||||
)
|
||||
|
||||
# First, get the current active security profile from slot 1
|
||||
get_var = GetVariableDataType(
|
||||
component=ComponentType(name="NetworkConfiguration", instance="1"),
|
||||
variable=VariableType(name="SecurityProfile"),
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
)
|
||||
|
||||
response = await charge_point_v21.get_variables_req(
|
||||
get_variable_data=[get_var]
|
||||
)
|
||||
|
||||
assert response and response.get_variable_result, "No get variable result"
|
||||
results = response.get_variable_result
|
||||
assert len(results) > 0, "Empty get variable result"
|
||||
|
||||
# Try to set a lower security profile on slot 2
|
||||
# Assuming the current active profile is 1, try to set slot 2 to 0 (which would be lower)
|
||||
set_vars = [
|
||||
SetVariableDataType(
|
||||
component=ComponentType(name="NetworkConfiguration", instance="2"),
|
||||
variable=VariableType(name="SecurityProfile"),
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
attribute_value="0", # Downgrade attempt
|
||||
),
|
||||
]
|
||||
|
||||
response = await charge_point_v21.set_variables_req(
|
||||
set_variable_data=set_vars
|
||||
)
|
||||
|
||||
# B09.FR.31: Downgrade attempt should be rejected with NoSecurityDowngrade
|
||||
assert validate_set_variables_rejected(
|
||||
response, "NoSecurityDowngrade"
|
||||
), f"Expected security downgrade to be rejected with NoSecurityDowngrade, but got: {response}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config(
|
||||
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
||||
)
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_network_configuration_security_downgrade_rejection_set_variables(
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
):
|
||||
"""
|
||||
TC_B_111_CS: Reject security profile downgrade via SetVariables on NetworkConfiguration
|
||||
B09.FR.32 - Verify that SetVariables is rejected with "NoSecurityDowngrade" reason
|
||||
when attempting to downgrade the SecurityProfile variable on an active configuration slot.
|
||||
"""
|
||||
log.info(
|
||||
"##################### TC_B_111_CS: Reject Security Downgrade via SetVariables #################"
|
||||
)
|
||||
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint(
|
||||
wait_for_bootnotification=True
|
||||
)
|
||||
|
||||
# First, set a higher security profile on slot 2
|
||||
set_var = SetVariableDataType(
|
||||
component=ComponentType(name="NetworkConfiguration", instance="2"),
|
||||
variable=VariableType(name="SecurityProfile"),
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
attribute_value="2", # Set to profile 2
|
||||
)
|
||||
|
||||
response = await charge_point_v21.set_variables_req(
|
||||
set_variable_data=[set_var]
|
||||
)
|
||||
|
||||
# This should succeed if slot 2 is not active
|
||||
assert validate_set_variables_success(
|
||||
response, 1
|
||||
), f"Failed to set initial security profile: {response}"
|
||||
|
||||
# Now try to downgrade the security profile on slot 2
|
||||
set_var = SetVariableDataType(
|
||||
component=ComponentType(name="NetworkConfiguration", instance="2"),
|
||||
variable=VariableType(name="SecurityProfile"),
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
attribute_value="0", # Attempt downgrade
|
||||
)
|
||||
|
||||
response = await charge_point_v21.set_variables_req(
|
||||
set_variable_data=[set_var]
|
||||
)
|
||||
|
||||
# B09.FR.32: Downgrade attempt on slot 2 after setting profile 2 should be rejected
|
||||
assert validate_set_variables_rejected(
|
||||
response, "NoSecurityDowngrade"
|
||||
), f"Expected security downgrade to be rejected with NoSecurityDowngrade, but got: {response}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config(
|
||||
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
||||
)
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_network_configuration_allow_security_downgrade_flag(
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
):
|
||||
"""
|
||||
TC_B_112_CS: AllowSecurityDowngrade configuration flag behavior
|
||||
B09.FR.04, B09.FR.31 - Verify that when AllowSecurityDowngrade is set to false,
|
||||
all security profile downgrades are rejected regardless of whether they occur
|
||||
via SetNetworkProfileRequest or SetVariables on NetworkConfiguration.
|
||||
"""
|
||||
log.info(
|
||||
"##################### TC_B_112_CS: AllowSecurityDowngrade Flag Enforcement #################"
|
||||
)
|
||||
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint(
|
||||
wait_for_bootnotification=True
|
||||
)
|
||||
|
||||
# Check the current AllowSecurityDowngrade setting
|
||||
get_var = GetVariableDataType(
|
||||
component=ComponentType(name="SecurityCtrlr"),
|
||||
variable=VariableType(name="AllowSecurityDowngrade"),
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
)
|
||||
|
||||
response = await charge_point_v21.get_variables_req(
|
||||
get_variable_data=[get_var]
|
||||
)
|
||||
|
||||
# Verify we can read the AllowSecurityDowngrade variable
|
||||
assert response and response.get_variable_result, "No get variable result"
|
||||
results = response.get_variable_result
|
||||
assert len(results) > 0, "Empty get variable result"
|
||||
|
||||
# When AllowSecurityDowngrade is false, any downgrade should be rejected
|
||||
# Try to set a lower security profile
|
||||
set_var = SetVariableDataType(
|
||||
component=ComponentType(name="NetworkConfiguration", instance="2"),
|
||||
variable=VariableType(name="SecurityProfile"),
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
attribute_value="0",
|
||||
)
|
||||
|
||||
response = await charge_point_v21.set_variables_req(
|
||||
set_variable_data=[set_var]
|
||||
)
|
||||
|
||||
# B09.FR.04, B09.FR.31: When AllowSecurityDowngrade is false, any downgrade should be rejected
|
||||
assert validate_set_variables_rejected(
|
||||
response, "NoSecurityDowngrade"
|
||||
), f"Expected security downgrade to be rejected with NoSecurityDowngrade, but got: {response}"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
@pytest.mark.ocpp_version("ocpp2.1")
|
||||
@pytest.mark.everest_core_config(
|
||||
get_everest_config_path_str("everest-config-ocpp201.yaml")
|
||||
)
|
||||
@pytest.mark.ocpp_config_adaptions(
|
||||
GenericOCPP2XConfigAdjustment(
|
||||
[
|
||||
(
|
||||
OCPP2XConfigVariableIdentifier(
|
||||
"InternalCtrlr", "SupportedOcppVersions", "Actual"
|
||||
),
|
||||
"ocpp2.1",
|
||||
)
|
||||
]
|
||||
)
|
||||
)
|
||||
async def test_network_configuration_dm_end_to_end(
|
||||
central_system_v21: CentralSystem,
|
||||
test_controller: TestController,
|
||||
):
|
||||
"""
|
||||
US-006: End-to-end integration test for NetworkConfiguration device model.
|
||||
|
||||
Verifies the full lifecycle:
|
||||
1. Migration from legacy NetworkConnectionProfiles blob to DM variables on boot
|
||||
2. GetVariables reads correct values from NetworkConfiguration[1]
|
||||
3. SetVariables updates a non-active slot and the change persists (read-back)
|
||||
4. SetVariables on the active slot is rejected with PriorityNetworkConf
|
||||
"""
|
||||
log.info(
|
||||
"##################### US-006: NetworkConfiguration DM End-to-End #################"
|
||||
)
|
||||
|
||||
test_controller.start()
|
||||
charge_point_v21 = await central_system_v21.wait_for_chargepoint(
|
||||
wait_for_bootnotification=True
|
||||
)
|
||||
|
||||
# ── Step 1 & 2: Verify migration happened by reading slot 1 DM variables ──
|
||||
# The charge point booted with a legacy NetworkConnectionProfiles blob.
|
||||
# Migration should have populated NetworkConfiguration[1] with those values.
|
||||
variable_names = ["OcppCsmsUrl", "SecurityProfile", "OcppInterface", "OcppTransport"]
|
||||
|
||||
get_vars = [
|
||||
GetVariableDataType(
|
||||
component=ComponentType(
|
||||
name="NetworkConfiguration", instance="1"
|
||||
),
|
||||
variable=VariableType(name=var_name),
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
)
|
||||
for var_name in variable_names
|
||||
]
|
||||
|
||||
response = await charge_point_v21.get_variables_req(
|
||||
get_variable_data=get_vars
|
||||
)
|
||||
|
||||
assert response and response.get_variable_result, "No get variable result for slot 1"
|
||||
results = response.get_variable_result
|
||||
assert len(results) == len(variable_names), (
|
||||
f"Expected {len(variable_names)} results, got {len(results)}"
|
||||
)
|
||||
|
||||
# Build a map of variable name → value for easier assertions
|
||||
slot1_values = {}
|
||||
for r in results:
|
||||
var_name = r.get("variable", {}).get("name")
|
||||
status = r.get("attribute_status")
|
||||
assert status == "Accepted", (
|
||||
f"GetVariables failed for {var_name}: status={status}"
|
||||
)
|
||||
slot1_values[var_name] = r.get("attribute_value")
|
||||
|
||||
# Verify migration produced correct values from the legacy blob
|
||||
# The legacy blob has: securityProfile=1, ocppInterface=Wired0, ocppTransport=JSON
|
||||
# OcppCsmsUrl is injected by the test framework to point to the test CSMS
|
||||
assert slot1_values["OcppCsmsUrl"], "OcppCsmsUrl should not be empty after migration"
|
||||
assert "ws" in slot1_values["OcppCsmsUrl"].lower(), (
|
||||
f"OcppCsmsUrl should be a websocket URL, got: {slot1_values['OcppCsmsUrl']}"
|
||||
)
|
||||
assert slot1_values["SecurityProfile"] == "1", (
|
||||
f"SecurityProfile should be 1, got: {slot1_values['SecurityProfile']}"
|
||||
)
|
||||
assert slot1_values["OcppInterface"] == "Wired0", (
|
||||
f"OcppInterface should be Wired0, got: {slot1_values['OcppInterface']}"
|
||||
)
|
||||
assert slot1_values["OcppTransport"] == "JSON", (
|
||||
f"OcppTransport should be JSON, got: {slot1_values['OcppTransport']}"
|
||||
)
|
||||
|
||||
log.info("Step 1-2 PASSED: Migration verified — slot 1 DM variables match legacy blob")
|
||||
|
||||
# ── Step 3: SetVariables on non-active slot 2, then read back to verify persistence ──
|
||||
new_url = "wss://updated-backup.example.com/ocpp"
|
||||
new_security_profile = "2"
|
||||
new_interface = "Wired0"
|
||||
new_transport = "JSON"
|
||||
|
||||
set_vars = [
|
||||
SetVariableDataType(
|
||||
component=ComponentType(
|
||||
name="NetworkConfiguration", instance="2"
|
||||
),
|
||||
variable=VariableType(name="OcppCsmsUrl"),
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
attribute_value=new_url,
|
||||
),
|
||||
SetVariableDataType(
|
||||
component=ComponentType(
|
||||
name="NetworkConfiguration", instance="2"
|
||||
),
|
||||
variable=VariableType(name="SecurityProfile"),
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
attribute_value=new_security_profile,
|
||||
),
|
||||
SetVariableDataType(
|
||||
component=ComponentType(
|
||||
name="NetworkConfiguration", instance="2"
|
||||
),
|
||||
variable=VariableType(name="OcppInterface"),
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
attribute_value=new_interface,
|
||||
),
|
||||
SetVariableDataType(
|
||||
component=ComponentType(
|
||||
name="NetworkConfiguration", instance="2"
|
||||
),
|
||||
variable=VariableType(name="OcppTransport"),
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
attribute_value=new_transport,
|
||||
),
|
||||
]
|
||||
|
||||
response = await charge_point_v21.set_variables_req(
|
||||
set_variable_data=set_vars
|
||||
)
|
||||
|
||||
assert validate_set_variables_success(response, len(set_vars)), (
|
||||
f"SetVariables on slot 2 should succeed: {response}"
|
||||
)
|
||||
|
||||
# Read back slot 2 to verify persistence
|
||||
get_vars_slot2 = [
|
||||
GetVariableDataType(
|
||||
component=ComponentType(
|
||||
name="NetworkConfiguration", instance="2"
|
||||
),
|
||||
variable=VariableType(name=var_name),
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
)
|
||||
for var_name in variable_names
|
||||
]
|
||||
|
||||
response = await charge_point_v21.get_variables_req(
|
||||
get_variable_data=get_vars_slot2
|
||||
)
|
||||
|
||||
assert response and response.get_variable_result, "No get variable result for slot 2"
|
||||
results = response.get_variable_result
|
||||
|
||||
slot2_values = {}
|
||||
for r in results:
|
||||
var_name = r.get("variable", {}).get("name")
|
||||
status = r.get("attribute_status")
|
||||
assert status == "Accepted", (
|
||||
f"GetVariables failed for slot 2 {var_name}: status={status}"
|
||||
)
|
||||
slot2_values[var_name] = r.get("attribute_value")
|
||||
|
||||
assert slot2_values["OcppCsmsUrl"] == new_url, (
|
||||
f"Slot 2 OcppCsmsUrl should be {new_url}, got: {slot2_values['OcppCsmsUrl']}"
|
||||
)
|
||||
assert slot2_values["SecurityProfile"] == new_security_profile, (
|
||||
f"Slot 2 SecurityProfile should be {new_security_profile}, got: {slot2_values['SecurityProfile']}"
|
||||
)
|
||||
assert slot2_values["OcppInterface"] == new_interface, (
|
||||
f"Slot 2 OcppInterface should be {new_interface}, got: {slot2_values['OcppInterface']}"
|
||||
)
|
||||
assert slot2_values["OcppTransport"] == new_transport, (
|
||||
f"Slot 2 OcppTransport should be {new_transport}, got: {slot2_values['OcppTransport']}"
|
||||
)
|
||||
|
||||
log.info("Step 3 PASSED: SetVariables on slot 2 persisted and verified via GetVariables")
|
||||
|
||||
# ── Step 4: SetVariables on the active slot (1) should be rejected ──
|
||||
set_var_active = SetVariableDataType(
|
||||
component=ComponentType(
|
||||
name="NetworkConfiguration", instance="1"
|
||||
),
|
||||
variable=VariableType(name="OcppCsmsUrl"),
|
||||
attribute_type=AttributeEnumType.actual,
|
||||
attribute_value="wss://should-be-rejected.example.com/ocpp",
|
||||
)
|
||||
|
||||
response = await charge_point_v21.set_variables_req(
|
||||
set_variable_data=[set_var_active]
|
||||
)
|
||||
|
||||
assert validate_set_variables_rejected(
|
||||
response, "PriorityNetworkConf"
|
||||
), (
|
||||
f"SetVariables on active slot 1 should be rejected with "
|
||||
f"PriorityNetworkConf, but got: {response}"
|
||||
)
|
||||
|
||||
log.info("Step 4 PASSED: SetVariables on active slot 1 rejected with PriorityNetworkConf")
|
||||
Reference in New Issue
Block a user