diff --git a/iso15118/secc/controller/interface.py b/iso15118/secc/controller/interface.py index 7b5b0eb6..cba2cb39 100644 --- a/iso15118/secc/controller/interface.py +++ b/iso15118/secc/controller/interface.py @@ -33,6 +33,7 @@ CpState, EnergyTransferModeEnum, Protocol, + SessionStopAction, ) from iso15118.shared.messages.iso15118_2.datatypes import ( ACEVSEChargeParameter, @@ -699,3 +700,14 @@ async def get_15118_ev_certificate( - ISO 15118-20 and ISO 15118-2 """ raise NotImplementedError + + @abstractmethod + async def update_data_link(self, action: SessionStopAction) -> None: + """ + Called when EV requires termination or pausing of the charging session. + Args: + action : SessionStopAction + Relevant for: + - ISO 15118-20 and ISO 15118-2 + """ + raise NotImplementedError diff --git a/iso15118/secc/controller/simulator.py b/iso15118/secc/controller/simulator.py index dab128c0..371a8964 100644 --- a/iso15118/secc/controller/simulator.py +++ b/iso15118/secc/controller/simulator.py @@ -58,6 +58,7 @@ Namespace, PriceAlgorithm, Protocol, + SessionStopAction, UnitSymbol, ) from iso15118.shared.messages.iso15118_2.body import Body, CertificateInstallationRes @@ -910,3 +911,9 @@ async def get_15118_ev_certificate( ).decode("utf-8") return base64_encode_cert_install_res + + async def update_data_link(self, action: SessionStopAction) -> None: + """ + Overrides EVSEControllerInterface.update_data_link(). + """ + pass diff --git a/iso15118/secc/states/iso15118_2_states.py b/iso15118/secc/states/iso15118_2_states.py index c1edfb9b..abf7fe3e 100644 --- a/iso15118/secc/states/iso15118_2_states.py +++ b/iso15118/secc/states/iso15118_2_states.py @@ -84,6 +84,7 @@ CertificateChain, ChargeProgress, ChargeService, + ChargingSession, DHPublicKey, EncryptedPrivateKey, EnergyTransferModeList, @@ -123,7 +124,7 @@ verify_certs, verify_signature, ) -from iso15118.shared.states import Base64, State, Terminate +from iso15118.shared.states import Base64, Pause, State, Terminate logger = logging.getLogger(__name__) @@ -1869,9 +1870,12 @@ async def process_message( f"EV Requested to {session_status} the communication session", self.comm_session.writer.get_extra_info("peername"), ) - + if msg.body.session_stop_req.charging_session == ChargingSession.PAUSE: + next_state = Pause + else: + next_state = Terminate self.create_next_message( - Terminate, + next_state, SessionStopRes(response_code=ResponseCode.OK), Timeouts.V2G_SECC_SEQUENCE_TIMEOUT, Namespace.ISO_V2_MSG_DEF, diff --git a/iso15118/shared/comm_session.py b/iso15118/shared/comm_session.py index bdd81f5b..77dbd6c0 100644 --- a/iso15118/shared/comm_session.py +++ b/iso15118/shared/comm_session.py @@ -35,6 +35,7 @@ ISOV20PayloadTypes, Namespace, Protocol, + SessionStopAction, ) from iso15118.shared.messages.iso15118_2.datatypes import EnergyTransferModeEnum from iso15118.shared.messages.iso15118_2.msgdef import V2GMessage as V2GMessageV2 @@ -380,9 +381,9 @@ async def stop(self, reason: str): """ if self.current_state.next_state == Pause: self.save_session_info() - terminate_or_pause = "Pause" + terminate_or_pause = SessionStopAction.PAUSE else: - terminate_or_pause = "Terminate" + terminate_or_pause = SessionStopAction.TERMINATE logger.info( f"The data link will {terminate_or_pause} in 2 seconds and " @@ -391,8 +392,10 @@ async def stop(self, reason: str): logger.info(f"Reason: {reason}") await asyncio.sleep(2) - # TODO Signal data link layer to either terminate or pause the data - # link connection + # Signal data link layer to either terminate or pause the data + # link connection + if hasattr(self.comm_session, "evse_controller"): + await self.comm_session.evse_controller.update_data_link(terminate_or_pause) logger.info(f"{terminate_or_pause}d the data link") await asyncio.sleep(3) try: diff --git a/iso15118/shared/messages/enums.py b/iso15118/shared/messages/enums.py index c9b5ec90..7bc95028 100644 --- a/iso15118/shared/messages/enums.py +++ b/iso15118/shared/messages/enums.py @@ -436,3 +436,8 @@ class CpState(str, Enum): E = "E" F = "F" UNKNOWN = "UNKNOWN" + + +class SessionStopAction(str, Enum): + TERMINATE = "terminate" + PAUSE = "pause" diff --git a/tests/secc/states/test_iso15118_2_states.py b/tests/secc/states/test_iso15118_2_states.py index e762a341..2330eeae 100644 --- a/tests/secc/states/test_iso15118_2_states.py +++ b/tests/secc/states/test_iso15118_2_states.py @@ -12,6 +12,7 @@ PaymentDetails, PowerDelivery, ServiceDiscovery, + SessionStop, Terminate, WeldingDetection, ) @@ -25,8 +26,13 @@ EVSEProcessing, ) from iso15118.shared.messages.iso15118_2.body import ResponseCode -from iso15118.shared.messages.iso15118_2.datatypes import ACEVSEStatus, CertificateChain +from iso15118.shared.messages.iso15118_2.datatypes import ( + ACEVSEStatus, + CertificateChain, + ChargingSession, +) from iso15118.shared.security import get_random_bytes +from iso15118.shared.states import Pause from tests.secc.states.test_messages import ( get_charge_parameter_discovery_req_message_departure_time_one_hour, get_charge_parameter_discovery_req_message_no_departure_time, @@ -38,6 +44,7 @@ get_dummy_v2g_message_power_delivery_req_charge_stop, get_dummy_v2g_message_service_discovery_req, get_dummy_v2g_message_welding_detection_req, + get_dummy_v2g_session_stop_req, get_power_delivery_req_charging_profile_in_boundary_invalid, get_power_delivery_req_charging_profile_in_limits, get_power_delivery_req_charging_profile_not_in_limits_span_over_sa, @@ -55,6 +62,8 @@ def _comm_session(self, comm_secc_session_mock): self.comm_session = comm_secc_session_mock self.comm_session.config = Config() self.comm_session.is_tls = False + self.comm_session.writer = Mock() + self.comm_session.writer.get_extra_info = Mock() async def test_current_demand_to_power_delivery_when_power_delivery_received( self, @@ -104,8 +113,6 @@ async def test_payment_details_next_state_on_payment_details_req_auth( is_authorized_return_value: AuthorizationStatus, expected_next_state: StateSECC, ): - self.comm_session.writer = Mock() - self.comm_session.writer.get_extra_info = Mock() self.comm_session.selected_auth_option = AuthEnum.PNC_V2 mock_is_authorized = AsyncMock(return_value=is_authorized_return_value) @@ -183,8 +190,6 @@ async def test_authorization_next_state_on_authorization_request( expected_response_code: ResponseCode, expected_evse_processing: EVSEProcessing, ): - self.comm_session.writer = Mock() - self.comm_session.writer.get_extra_info = Mock() self.comm_session.selected_auth_option = auth_type mock_is_authorized = AsyncMock(return_value=is_authorized_return_value) @@ -211,8 +216,6 @@ async def test_authorization_next_state_on_authorization_request( ) async def test_authorization_req_gen_challenge_invalid(self): - self.comm_session.writer = Mock() - self.comm_session.writer.get_extra_info = Mock() self.comm_session.selected_auth_option = AuthEnum.PNC_V2 self.comm_session.contract_cert_chain = Mock() self.comm_session.gen_challenge = get_random_bytes(16) @@ -230,8 +233,6 @@ async def test_authorization_req_gen_challenge_invalid(self): ) async def test_authorization_req_gen_challenge_valid(self): - self.comm_session.writer = Mock() - self.comm_session.writer.get_extra_info = Mock() self.comm_session.selected_auth_option = AuthEnum.PNC_V2 self.comm_session.gen_challenge = get_random_bytes(16) id = "aReq" @@ -455,8 +456,6 @@ async def test_power_delivery_state_c( ): power_delivery = PowerDelivery(self.comm_session) - self.comm_session.writer = Mock() - self.comm_session.writer.get_extra_info = Mock() mock_get_cp_state = AsyncMock(return_value=get_state_return_value) self.comm_session.evse_controller.get_cp_state = mock_get_cp_state await power_delivery.process_message( @@ -467,8 +466,6 @@ async def test_power_delivery_state_c( async def test_service_discovery_req_unexpected_state(self): self.comm_session.selected_auth_option = AuthEnum.PNC_V2 self.comm_session.config.free_charging_service = False - self.comm_session.writer = Mock() - self.comm_session.writer.get_extra_info = Mock() service_discovery = ServiceDiscovery(self.comm_session) await service_discovery.process_message( message=get_dummy_v2g_message_service_discovery_req() @@ -484,8 +481,6 @@ async def test_service_discovery_req_unexpected_state(self): async def test_charging_status_evse_status(self): charging_status = ChargingStatus(self.comm_session) - self.comm_session.writer = Mock() - self.comm_session.writer.get_extra_info = Mock() self.comm_session.selected_schedule = 1 await charging_status.process_message(message=get_dummy_charging_status_req()) @@ -498,8 +493,6 @@ async def test_charging_status_evse_status(self): async def test_charging_status_evse_status_altered(self): charging_status = ChargingStatus(self.comm_session) - self.comm_session.writer = Mock() - self.comm_session.writer.get_extra_info = Mock() self.comm_session.selected_schedule = 1 async def get_ac_evse_status_patch(): @@ -513,3 +506,18 @@ async def get_ac_evse_status_patch(): await charging_status.process_message(message=get_dummy_charging_status_req()) charging_status_res = charging_status.message.body.charging_status_res assert charging_status_res.ac_evse_status == await get_ac_evse_status_patch() + + @pytest.mark.parametrize( + "charging_session, expected_next_state", + [ + (ChargingSession.PAUSE, Pause), + (ChargingSession.TERMINATE, Terminate), + ], + ) + async def test_session_stop_req(self, charging_session, expected_next_state): + # V2G2-718 + session_stop = SessionStop(self.comm_session) + await session_stop.process_message( + message=get_dummy_v2g_session_stop_req(charging_session) + ) + assert session_stop.next_state == expected_next_state diff --git a/tests/secc/states/test_messages.py b/tests/secc/states/test_messages.py index 34a10ebf..c1353f4b 100644 --- a/tests/secc/states/test_messages.py +++ b/tests/secc/states/test_messages.py @@ -508,3 +508,12 @@ def get_dummy_charging_status_req() -> V2GMessage: header=MessageHeader(session_id=MOCK_SESSION_ID), body=Body(charging_status_req=charging_status_req), ) + + +def get_dummy_v2g_session_stop_req(charging_session: ChargingSession) -> V2GMessage: + session_stop_req = SessionStopReq(charging_session=charging_session) + + return V2GMessage( + header=MessageHeader(session_id=MOCK_SESSION_ID), + body=Body(session_stop_req=session_stop_req), + )