# 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")