From 79d6f30d348eef68e4e556668292761f4a24dbd7 Mon Sep 17 00:00:00 2001 From: ikaratass <89934937+ikaratass@users.noreply.github.com> Date: Fri, 16 Dec 2022 09:18:32 +0000 Subject: [PATCH] supported energy services are read from json file --- iso15118/evcc/comm_session_handler.py | 32 +++++----- iso15118/evcc/controller/simulator.py | 49 +++++++-------- iso15118/evcc/evcc_config.py | 35 +++++------ iso15118/evcc/evcc_settings.py | 3 - iso15118/evcc/states/sap_states.py | 6 +- .../shared/examples/evcc/evcc_config.json | 5 +- iso15118/shared/exceptions.py | 4 ++ iso15118/shared/utils.py | 59 ++++++++++++++++++- 8 files changed, 125 insertions(+), 68 deletions(-) diff --git a/iso15118/evcc/comm_session_handler.py b/iso15118/evcc/comm_session_handler.py index 44d2bd178..8b330eb39 100644 --- a/iso15118/evcc/comm_session_handler.py +++ b/iso15118/evcc/comm_session_handler.py @@ -57,7 +57,7 @@ StopNotification, UDPPacketNotification, ) -from iso15118.shared.utils import cancel_task, wait_for_tasks +from iso15118.shared.utils import cancel_task, load_requested_protocols, wait_for_tasks logger = logging.getLogger(__name__) @@ -71,12 +71,12 @@ class EVCCCommunicationSession(V2GCommunicationSession): """ def __init__( - self, - transport: Tuple[StreamReader, StreamWriter], - session_handler_queue: asyncio.Queue, - evcc_config: EVCCConfig, - iface: str, - ev_controller: EVControllerInterface, + self, + transport: Tuple[StreamReader, StreamWriter], + session_handler_queue: asyncio.Queue, + evcc_config: EVCCConfig, + iface: str, + ev_controller: EVControllerInterface, ): # Need to import here to avoid a circular import error # pylint: disable=import-outside-toplevel @@ -149,10 +149,7 @@ def create_sap(self) -> Union[SupportedAppProtocolReq, None]: app_protocols = [] schema_id = 0 priority = 0 - supported_protocols = [] - for protocols in self.config.supported_protocols: - if protocols.name in list(map(lambda p: p.name, Protocol)): - supported_protocols.append(Protocol[protocols.name]) + supported_protocols = load_requested_protocols(self.config.supported_protocols) # [V2G-DC-618] For DC charging according to DIN SPEC 70121, # an SDP server shall send an SECC Discovery Response message with Transport @@ -261,11 +258,11 @@ class CommunicationSessionHandler: # pylint: disable=too-many-instance-attributes def __init__( - self, - config: EVCCConfig, - iface: str, - codec: IEXICodec, - ev_controller: EVControllerInterface, + self, + config: EVCCConfig, + iface: str, + codec: IEXICodec, + ev_controller: EVControllerInterface, ): self.list_of_tasks = [] self.udp_client = None @@ -424,6 +421,7 @@ async def start_comm_session(self, host: IPv6Address, port: int, is_tls: bool): (self.tcp_client.reader, self.tcp_client.writer), self._rcv_queue, self.config, + self.iface, self.ev_controller, ) @@ -489,7 +487,7 @@ async def process_incoming_udp_packet(self, message: UDPPacketNotification): # The rationale behind this might be that the EV OEM trades convenience # (the EV driver can always charge) over security. if (not secc_signals_tls and self.config.enforce_tls) or ( - secc_signals_tls and not self.config.use_tls + secc_signals_tls and not self.config.use_tls ): logger.error( "Security mismatch, can't initiate communication session." diff --git a/iso15118/evcc/controller/simulator.py b/iso15118/evcc/controller/simulator.py index 53c3ba8bb..9ad3adde0 100644 --- a/iso15118/evcc/controller/simulator.py +++ b/iso15118/evcc/controller/simulator.py @@ -98,6 +98,7 @@ DCChargeParameterDiscoveryReqParams, ) from iso15118.shared.network import get_nic_mac_address +from iso15118.shared.utils import load_requested_energy_services logger = logging.getLogger(__name__) @@ -113,7 +114,9 @@ def __init__(self, evcc_config: Optional[EVCCConfig] = None): self.precharge_loop_cycles: int = 0 self._charging_is_completed = False self._soc = 10 - + self.supported_energy_services = load_requested_energy_services( + self.config.supported_energy_services + ) self.dc_ev_charge_params: DCEVChargeParams = DCEVChargeParams( dc_max_current_limit=PVEVMaxCurrentLimit( multiplier=-3, value=32000, unit=UnitSymbol.AMPERE @@ -161,7 +164,7 @@ async def get_evcc_id(self, protocol: Protocol, iface: str) -> str: raise InvalidProtocolError async def get_energy_transfer_mode( - self, protocol: Protocol + self, protocol: Protocol ) -> EnergyTransferModeEnum: """Overrides EVControllerInterface.get_energy_transfer_mode().""" if protocol == Protocol.DIN_SPEC_70121: @@ -170,10 +173,10 @@ async def get_energy_transfer_mode( async def get_supported_energy_services(self) -> List[ServiceV20]: """Overrides EVControllerInterface.get_energy_transfer_service().""" - return self.config.supported_energy_services + return self.supported_energy_services async def select_energy_service_v20( - self, services: List[MatchedService] + self, services: List[MatchedService] ) -> SelectedEnergyService: """Overrides EVControllerInterface.select_energy_service_v20().""" matched_energy_services = [ @@ -194,7 +197,7 @@ async def select_energy_service_v20( return None async def select_vas_services_v20( - self, services: List[MatchedService] + self, services: List[MatchedService] ) -> Optional[List[SelectedVAS]]: """Overrides EVControllerInterface.select_vas_services_v20().""" matched_vas_services = [ @@ -256,7 +259,7 @@ async def get_charge_params_v2(self, protocol: Protocol) -> ChargeParamsV2: ) async def get_charge_params_v20( - self, selected_service: SelectedEnergyService + self, selected_service: SelectedEnergyService ) -> Union[ ACChargeParameterDiscoveryReqParams, BPTACChargeParameterDiscoveryReqParams, @@ -305,7 +308,7 @@ async def get_charge_params_v20( ) async def get_scheduled_se_params( - self, selected_energy_service: SelectedEnergyService + self, selected_energy_service: SelectedEnergyService ) -> ScheduledScheduleExchangeReqParams: """Overrides EVControllerInterface.get_scheduled_se_params().""" ev_price_rule = EVPriceRule( @@ -356,7 +359,7 @@ async def get_scheduled_se_params( return scheduled_params async def get_dynamic_se_params( - self, selected_energy_service: SelectedEnergyService + self, selected_energy_service: SelectedEnergyService ) -> DynamicScheduleExchangeReqParams: """Overrides EVControllerInterface.get_dynamic_se_params().""" dynamic_params = DynamicScheduleExchangeReqParams( @@ -373,7 +376,7 @@ async def get_dynamic_se_params( return dynamic_params async def process_scheduled_se_params( - self, scheduled_params: ScheduledScheduleExchangeResParams, pause: bool + self, scheduled_params: ScheduledScheduleExchangeResParams, pause: bool ) -> Tuple[Optional[EVPowerProfile], ChargeProgressV20]: """Overrides EVControllerInterface.process_scheduled_se_params().""" is_ready = bool(random.getrandbits(1)) @@ -420,7 +423,7 @@ async def process_scheduled_se_params( return ev_power_profile, charge_progress async def process_dynamic_se_params( - self, dynamic_params: DynamicScheduleExchangeResParams, pause: bool + self, dynamic_params: DynamicScheduleExchangeResParams, pause: bool ) -> Tuple[Optional[EVPowerProfile], ChargeProgressV20]: """Overrides EVControllerInterface.process_dynamic_se_params().""" is_ready = bool(random.getrandbits(1)) @@ -457,7 +460,7 @@ async def is_cert_install_needed(self) -> bool: return self.config.is_cert_install_needed async def process_sa_schedules_dinspec( - self, sa_schedules: List[SAScheduleTupleEntryDINSPEC] + self, sa_schedules: List[SAScheduleTupleEntryDINSPEC] ) -> int: """Overrides EVControllerInterface.process_sa_schedules_dinspec().""" schedule = sa_schedules.pop() @@ -482,8 +485,8 @@ async def process_sa_schedules_dinspec( zero_power = 1 last_profile_entry_details = ProfileEntryDetailsDINSPEC( start=( - schedule_entry_details.time_interval.start - + schedule_entry_details.time_interval.duration + schedule_entry_details.time_interval.start + + schedule_entry_details.time_interval.duration ), max_power=zero_power, ) @@ -492,7 +495,7 @@ async def process_sa_schedules_dinspec( return schedule.sa_schedule_tuple_id async def process_sa_schedules_v2( - self, sa_schedules: List[SAScheduleTuple] + self, sa_schedules: List[SAScheduleTuple] ) -> Tuple[ChargeProgressV2, int, ChargingProfile]: """Overrides EVControllerInterface.process_sa_schedules().""" secc_schedule = sa_schedules.pop() @@ -517,8 +520,8 @@ async def process_sa_schedules_v2( zero_power = PVPMax(multiplier=0, value=0, unit=UnitSymbol.WATT) last_profile_entry_details = ProfileEntryDetails( start=( - schedule_entry_details.time_interval.start - + schedule_entry_details.time_interval.duration + schedule_entry_details.time_interval.start + + schedule_entry_details.time_interval.duration ), max_power=zero_power, ) @@ -546,7 +549,7 @@ async def continue_charging(self) -> bool: return True async def store_contract_cert_and_priv_key( - self, contract_cert: bytes, priv_key: bytes + self, contract_cert: bytes, priv_key: bytes ): """Overrides EVControllerInterface.store_contract_cert_and_priv_key().""" # TODO Need to store the contract cert and private key @@ -562,7 +565,7 @@ async def is_precharged(self, present_voltage_evse: PVEVSEPresentVoltage) -> boo return True async def get_dc_ev_power_delivery_parameter_dinspec( - self, + self, ) -> DCEVPowerDeliveryParameterDINSPEC: return DCEVPowerDeliveryParameterDINSPEC( dc_ev_status=await self.get_dc_ev_status_dinspec(), @@ -631,7 +634,7 @@ async def get_ac_charge_params_v20(self) -> ACChargeParameterDiscoveryReqParams: ) async def get_ac_bpt_charge_params_v20( - self, + self, ) -> BPTACChargeParameterDiscoveryReqParams: """Overrides EVControllerInterface.get_bpt_ac_charge_params_v20().""" ac_charge_params_v20 = (await self.get_ac_charge_params_v20()).dict() @@ -646,7 +649,7 @@ async def get_ac_bpt_charge_params_v20( ) async def get_scheduled_ac_charge_loop_params( - self, + self, ) -> ScheduledACChargeLoopReqParams: """Overrides EVControllerInterface.get_scheduled_ac_charge_loop_params().""" return ScheduledACChargeLoopReqParams( @@ -655,7 +658,7 @@ async def get_scheduled_ac_charge_loop_params( ) async def get_bpt_scheduled_ac_charge_loop_params( - self, + self, ) -> BPTScheduledACChargeLoopReqParams: """Overrides EVControllerInterface.get_bpt_scheduled_ac_charge_loop_params().""" return BPTScheduledACChargeLoopReqParams( @@ -677,7 +680,7 @@ async def get_dynamic_ac_charge_loop_params(self) -> DynamicACChargeLoopReqParam ) async def get_bpt_dynamic_ac_charge_loop_params( - self, + self, ) -> BPTDynamicACChargeLoopReqParams: """Overrides EVControllerInterface.get_bpt_dynamic_ac_charge_loop_params().""" return BPTDynamicACChargeLoopReqParams( @@ -727,7 +730,7 @@ async def get_dc_charge_params_v20(self) -> DCChargeParameterDiscoveryReqParams: ) async def get_dc_bpt_charge_params_v20( - self, + self, ) -> BPTDCChargeParameterDiscoveryReqParams: """Overrides EVControllerInterface.get_bpt_dc_charge_params_v20().""" dc_charge_params_v20 = (await self.get_dc_charge_params_v20()).dict() diff --git a/iso15118/evcc/evcc_config.py b/iso15118/evcc/evcc_config.py index 7062da7d0..ac4db44b8 100644 --- a/iso15118/evcc/evcc_config.py +++ b/iso15118/evcc/evcc_config.py @@ -1,14 +1,13 @@ import json import logging -from dataclasses import dataclass, field, asdict, fields +from dataclasses import dataclass, fields from enum import Enum from typing import List, Optional import dacite from aiofile import async_open -from dacite import from_dict, Config -from iso15118.shared.messages.enums import Protocol, UINT_16_MAX, ServiceV20 +from iso15118.shared.messages.enums import UINT_16_MAX logger = logging.getLogger(__name__) @@ -24,8 +23,7 @@ class SupportedProtocolOption(Enum): @dataclass class EVCCConfig: - supported_energy_services: List[ServiceV20] = None - supports_eim: bool = True + supported_energy_services: List[str] = None is_cert_install_needed: bool = True # Indicates the security level (either TCP (unencrypted) or TLS (encrypted)) # the EVCC shall send in the SDP request @@ -35,8 +33,6 @@ class EVCCConfig: enforce_tls: bool = False supported_protocols: Optional[List[str]] = None max_supporting_points: Optional[int] = None - is_cert_install_needed: bool = True - supported_energy_services: Optional[List[str]] = None def __post_init__(self): # Supported protocols, used for SupportedAppProtocol (SAP). The order in which @@ -50,12 +46,8 @@ def __post_init__(self): "ISO_15118_20_AC", "DIN_SPEC_70121", ] - for protocol in self.supported_protocols: - if protocol not in list(map(lambda p: p.name, Protocol)): - raise Exception("Wrong attribute for supported protocol in config file." - f"Should be in list " - f"{list(map(lambda p: p.name, Protocol))}") - + if self.supported_energy_services is None: + self.supported_energy_services = ["AC"] # Indicates the maximum number of entries the EVCC supports within the # sub-elements of a ScheduleTuple (e.g. PowerScheduleType and PriceRuleType in # ISO 15118-20 as well as PMaxSchedule and SalesTariff in ISO 15118-2). @@ -64,15 +56,18 @@ def __post_init__(self): self.max_supporting_points = 1024 if not 0 <= self.max_supporting_points <= 1024: - raise Exception("Wrong range for max_supporting_points in config file. " - "Should be in [0..1024]") + raise Exception( + "Wrong range for max_supporting_points in config file. " + "Should be in [0..1024]" + ) # How often shall SDP (SECC Discovery Protocol) retries happen before reverting # to using nominal duty cycle PWM-based charging? if self.sdp_retry_cycles is None: self.sdp_retry_cycles = 1 if self.sdp_retry_cycles < 0: - raise Exception("Wrong range for sdp_retry_cycles in config file. " - "Should be in [0..]") + raise Exception( + "Wrong range for sdp_retry_cycles in config file. " "Should be in [0..]" + ) # Indicates the security level (either TCP (unencrypted) or TLS (encrypted)) # the EVCC shall send in the SDP request if self.use_tls is None: @@ -95,8 +90,10 @@ def __post_init__(self): if self.max_contract_certs is None: self.max_contract_certs = 3 if not 1 < self.max_contract_certs < UINT_16_MAX: - raise Exception("Wrong range for max_contract_certs in config file. " - "Should be in [1..UINT_16_MAX]") + raise Exception( + "Wrong range for max_contract_certs in config file. " + "Should be in [1..UINT_16_MAX]" + ) async def load_from_file(file_name: str) -> EVCCConfig: diff --git a/iso15118/evcc/evcc_settings.py b/iso15118/evcc/evcc_settings.py index 624936a2b..35d27ed2b 100644 --- a/iso15118/evcc/evcc_settings.py +++ b/iso15118/evcc/evcc_settings.py @@ -4,9 +4,6 @@ from typing import Optional import environs -from marshmallow.validate import Range - -from iso15118.shared.messages.enums import UINT_16_MAX, Protocol from iso15118.shared.network import validate_nic from iso15118.shared.settings import shared_settings diff --git a/iso15118/evcc/states/sap_states.py b/iso15118/evcc/states/sap_states.py index 6d3de0c48..daac7176c 100644 --- a/iso15118/evcc/states/sap_states.py +++ b/iso15118/evcc/states/sap_states.py @@ -84,7 +84,7 @@ async def process_message( BodyBaseDINSPEC, ] = SessionSetupReqV2( evcc_id=await self.comm_session.ev_controller.get_evcc_id( - Protocol.ISO_15118_2, self.comm_session.config.iface + Protocol.ISO_15118_2, self.comm_session.iface ) ) next_ns: Namespace = Namespace.ISO_V2_MSG_DEF @@ -105,7 +105,7 @@ async def process_message( next_msg = SessionSetupReqDINSPEC( evcc_id=await self.comm_session.ev_controller.get_evcc_id( - Protocol.DIN_SPEC_70121, self.comm_session.config.iface + Protocol.DIN_SPEC_70121, self.comm_session.iface ) ) @@ -121,7 +121,7 @@ async def process_message( next_msg = SessionSetupReqV20( header=header, evcc_id=await self.comm_session.ev_controller.get_evcc_id( - self.comm_session.protocol, self.comm_session.config.iface + self.comm_session.protocol, self.comm_session.iface ), ) next_ns = Namespace.ISO_V20_COMMON_MSG diff --git a/iso15118/shared/examples/evcc/evcc_config.json b/iso15118/shared/examples/evcc/evcc_config.json index 1d95fd410..f51d7b381 100644 --- a/iso15118/shared/examples/evcc/evcc_config.json +++ b/iso15118/shared/examples/evcc/evcc_config.json @@ -1,10 +1,11 @@ { "supported_protocols": [ "ISO_15118_2", - "DIN_SPEC_70121" + "DIN_SPEC_70121", + "ISO_15118_20_AC" ], "supported_energy_services": [ - "ServiceV20.AC_BPT" + "AC_BPT" ], "supports_eim": false, "is_cert_install_needed": false, diff --git a/iso15118/shared/exceptions.py b/iso15118/shared/exceptions.py index 7631967b7..326b87976 100644 --- a/iso15118/shared/exceptions.py +++ b/iso15118/shared/exceptions.py @@ -221,6 +221,10 @@ class NoSupportedProtocols(Exception): """Is thrown when no supported protocols are configured""" +class NoSupportedEnergyServices(Exception): + """Is thrown when no supported energy services are configured""" + + class NoSupportedAuthenticationModes(Exception): """Is thrown when no supported authentication modes are configured""" diff --git a/iso15118/shared/utils.py b/iso15118/shared/utils.py index 3737a99ce..7befb302e 100644 --- a/iso15118/shared/utils.py +++ b/iso15118/shared/utils.py @@ -1,10 +1,67 @@ import asyncio import logging -from typing import Coroutine, List +from typing import Coroutine, List, Optional + +from iso15118.shared.exceptions import NoSupportedEnergyServices, NoSupportedProtocols +from iso15118.shared.messages.enums import Protocol, ServiceV20 logger = logging.getLogger(__name__) +def _format_list(read_settings: List[str]) -> List[str]: + read_settings = list(filter(None, read_settings)) + read_settings = [setting.strip().upper() for setting in read_settings] + read_settings = list(set(read_settings)) + return read_settings + + +def load_requested_protocols(read_protocols: Optional[List[str]]) -> List[Protocol]: + supported_protocols = [ + "ISO_15118_2", + "ISO_15118_20_AC", + "DIN_SPEC_70121", + ] + + protocols = _format_list(read_protocols) + valid_protocols = list(set(protocols).intersection(supported_protocols)) + if not valid_protocols: + raise NoSupportedProtocols( + f"No supported protocols configured. Supported protocols are " + f"{supported_protocols} and could be configured in evcc_config.json" + ) + supported_protocols = [Protocol[x] for x in valid_protocols] + return supported_protocols + + +def load_requested_energy_services( + read_services: Optional[List[str]], +) -> List[ServiceV20]: + supported_services = [ + "AC", + "DC", + "WPT", + "DC_ACDP", + "AC_BPT", + "DC_BPT", + "DC_ACDP_BPT", + "INTERNET", + "PARKING_STATUS", + ] + + services = _format_list(read_services) + print(services) + print(supported_services) + valid_services = list(set(services).intersection(supported_services)) + print(valid_services) + if not valid_services: + raise NoSupportedEnergyServices( + f"No supported energy services configured. Supported energy services are " + f"{supported_services} and could be configured in evcc_config.json" + ) + supported_services = [ServiceV20[x] for x in valid_services] + return supported_services + + async def cancel_task(task): """Cancel the task safely""" task.cancel()