From 57886d40a2f889c431322d57d4cd1d79725c0410 Mon Sep 17 00:00:00 2001 From: tropxy Date: Tue, 21 Dec 2021 18:06:14 +0000 Subject: [PATCH 1/6] improved secc configuration handling and updated readme --- .env.dev.docker | 1 + .env.dev.local | 1 + Makefile | 11 +- README.md | 30 ++--- iso15118/secc/comm_session_handler.py | 22 ++-- iso15118/secc/secc_settings.py | 138 +++++++++++++-------- iso15118/secc/start_secc.py | 10 +- iso15118/secc/states/iso15118_20_states.py | 11 +- iso15118/secc/states/iso15118_2_states.py | 18 +-- iso15118/secc/states/sap_states.py | 4 +- iso15118/secc/transport/tcp_server.py | 8 +- iso15118/secc/transport/udp_server.py | 16 ++- poetry.lock | 15 +-- 13 files changed, 155 insertions(+), 130 deletions(-) diff --git a/.env.dev.docker b/.env.dev.docker index ce7d5641..d814c347 100644 --- a/.env.dev.docker +++ b/.env.dev.docker @@ -6,6 +6,7 @@ REDIS_HOST=redis # SECC Settings +SECC_CONTROLLER_SIM=True FREE_CHARGING_SERVICE=False FREE_CERT_INSTALL_SERVICE=True ALLOW_CERT_INSTALL_SERVICE=True diff --git a/.env.dev.local b/.env.dev.local index a4a078ba..62bb0f70 100644 --- a/.env.dev.local +++ b/.env.dev.local @@ -6,6 +6,7 @@ REDIS_HOST=localhost # SECC Settings +SECC_CONTROLLER_SIM=True FREE_CHARGING_SERVICE=False FREE_CERT_INSTALL_SERVICE=True ALLOW_CERT_INSTALL_SERVICE=True diff --git a/Makefile b/Makefile index 50f607d2..1a1d3616 100644 --- a/Makefile +++ b/Makefile @@ -66,17 +66,14 @@ run: poetry-update: poetry update -poetry-install: - poetry update - poetry install - -install-local: poetry-install +install-local: + pip install . run-evcc: - python iso15118/evcc/start_evcc.py + $(shell which python) iso15118/evcc/start_evcc.py run-secc: - python iso15118/secc/start_secc.py + $(shell which python) iso15118/secc/start_secc.py mypy: diff --git a/README.md b/README.md index 0982aa58..d1513ffe 100644 --- a/README.md +++ b/README.md @@ -11,7 +11,6 @@ The primary dependencies to install the project are the following: > - Poetry [^3] > - Python >= 3.7 - There are two recommended ways of running the project: 1. Building and running the docker file: @@ -59,7 +58,6 @@ is fired up automatically, including certificates generation, tests and linting. Also both SECC and EVCC are spawned, automatically. - For option number `2`, the certificates need to be provided. The project includes a script to help on the generation of -2 and -20 certificates. This script is located under `iso15118/shared/pki/` directory and is called `create_certs.sh`. @@ -70,12 +68,12 @@ $ ./create_certs.sh -h ``` --- + **IPv6 WARNING** For the system to work locally, the network interface to be used needs to have an IPv6 local-link address assigned. - For Docker, the `docker-compose.yml` was configured to create an `IPv6` network called `ipv6_net`, which enables the containers to acquire a local-link address, which is required to establish an ISO 15118 communication. This configuration is @@ -98,31 +96,29 @@ Since the Switch team relies mostly on MacOS and this project is on a developmen file `docker-compose-host-mode.yml` are copied to the main compose file, `docker-compose.yml`. In that case, it is advised to back up the compose file. - --- - ## Environment Settings Finally, the project includes a few configuration variables, whose default values can be modified by setting them as environmental variables. The following table provides a few of the available variables: -| ENV | Default Value | Description | -| -------------------------- | ---------------- | ------------------------------------------------------------------------------------------------------ | -| NETWORK_INTERFACE | `eth0` | HomePlug Green PHY Network Interface from which the high-level communication (HLC) will be established | -| REDIS_HOST | `localhost` | Redis Host URL | -| REDIS_PORT | `10001` | Redis Port | -| LOG_LEVEL | `INFO` | Level of the Python log service | +| ENV | Default Value | Description | +| ------------------- | ------------- | ------------------------------------------------------------------------------------------------------ | +| NETWORK_INTERFACE | `eth0` | HomePlug Green PHY Network Interface from which the high-level communication (HLC) will be established | +| SECC_CONTROLLER_SIM | `False` | Whether or not to simulate the SECC Controller Interface | +| SECC_ENFORCE_TLS | `False` | Whether or not the SECC will enforce a TLS connection | +| REDIS_HOST | `localhost` | Redis Host URL | +| REDIS_PORT | `10001` | Redis Port | +| LOG_LEVEL | `INFO` | Level of the Python log service | - -The project includes a few environmental files, in the root directory, for +The project includes a few environmental files, in the root directory, for different purposes: -* `.env.dev.docker` - ENV file with development settings, tailored to be used with docker -* `.env.dev.local` - ENV file with development settings, tailored to be used with -the local host - +- `.env.dev.docker` - ENV file with development settings, tailored to be used with docker +- `.env.dev.local` - ENV file with development settings, tailored to be used with + the local host If the user runs the project locally, e.g. using `$ make build && make run-secc`, it is required to create a `.env` file, containing the required settings. diff --git a/iso15118/secc/comm_session_handler.py b/iso15118/secc/comm_session_handler.py index a45d772a..9e91aeb2 100644 --- a/iso15118/secc/comm_session_handler.py +++ b/iso15118/secc/comm_session_handler.py @@ -21,7 +21,7 @@ init_failed_responses_iso_v2, init_failed_responses_iso_v20, ) -from iso15118.secc.secc_settings import ENFORCE_TLS, EVSE_CONTROLLER +from iso15118.secc.secc_settings import Config from iso15118.secc.transport.tcp_server import TCPServer from iso15118.secc.transport.udp_server import UDPServer from iso15118.shared.comm_session import V2GCommunicationSession @@ -62,14 +62,17 @@ def __init__( self, transport: Tuple[StreamReader, StreamWriter], session_handler_queue: asyncio.Queue, + config: Config ): # Need to import here to avoid a circular import error # pylint: disable=import-outside-toplevel from iso15118.secc.states.sap_states import SupportedAppProtocol super().__init__(transport, SupportedAppProtocol, session_handler_queue, self) + + self.config = config # The EVSE controller that implements the interface EVSEControllerInterface - self.evse_controller: EVSEControllerInterface = EVSE_CONTROLLER() + self.evse_controller: EVSEControllerInterface = config.evse_controller() # The authorization option(s) offered with ServiceDiscoveryRes in # ISO 15118-2 and with AuthorizationSetupRes in ISO 15118-20 self.offered_auth_options: Optional[List[AuthEnum]] = [] @@ -133,11 +136,12 @@ class CommunicationSessionHandler: # pylint: disable=too-many-instance-attributes - def __init__(self): + def __init__(self, config: Config): self.list_of_tasks = [] self.udp_server = None self.tcp_server = None + self.config = config # Receiving queue for UDP or TCP packets and session # triggers (e.g. pause/terminate) @@ -156,8 +160,8 @@ async def __init__. constructor. """ - self.udp_server = UDPServer(self._rcv_queue) - self.tcp_server = TCPServer(self._rcv_queue) + self.udp_server = UDPServer(self._rcv_queue, self.config.iface) + self.tcp_server = TCPServer(self._rcv_queue, self.config.iface) self.list_of_tasks = [ self.get_from_rcv_queue(self._rcv_queue), @@ -165,7 +169,7 @@ async def __init__. self.tcp_server.start_tls(), ] - if not ENFORCE_TLS: + if not self.config.enforce_tls: self.list_of_tasks.append(self.tcp_server.start_no_tls()) logger.debug("Communication session handler started") @@ -203,7 +207,7 @@ async def get_from_rcv_queue(self, queue: asyncio.Queue): comm_session.resume() except KeyError: comm_session = SECCCommunicationSession( - notification.transport, self._rcv_queue + notification.transport, self._rcv_queue, self.config ) task = asyncio.create_task( @@ -251,7 +255,7 @@ async def process_incoming_udp_packet(self, message: UDPPacketNotification): sdp_request = SDPRequest.from_payload(v2gtp_msg.payload) logger.debug(f"SDPRequest received: {sdp_request}") - if ENFORCE_TLS or sdp_request.security == Security.TLS: + if self.config.enforce_tls or sdp_request.security == Security.TLS: port = self.tcp_server.port_tls else: port = self.tcp_server.port_no_tls @@ -262,7 +266,7 @@ async def process_incoming_udp_packet(self, message: UDPPacketNotification): ) sdp_response = create_sdp_response( - sdp_request, ipv6_bytes, port, ENFORCE_TLS + sdp_request, ipv6_bytes, port, self.config.enforce_tls ) except InvalidSDPRequestError as exc: logger.exception( diff --git a/iso15118/secc/secc_settings.py b/iso15118/secc/secc_settings.py index 22778756..f614fbab 100644 --- a/iso15118/secc/secc_settings.py +++ b/iso15118/secc/secc_settings.py @@ -1,53 +1,91 @@ -import environs - +import logging +import os +from dataclasses import dataclass +from typing import Optional, Type, List from iso15118.secc.controller.simulator import SimEVSEController +from iso15118.secc.controller.interface import EVSEControllerInterface from iso15118.shared.messages.enums import AuthEnum, Protocol +from iso15118.shared.network import validate_nic + +import environs + + +logger = logging.getLogger(__name__) + + +@dataclass +class Config: + iface: Optional[str] = None + redis_host: Optional[str] = None + redis_port: Optional[int] = None + log_level: Optional[int] = None + evse_controller: Type[EVSEControllerInterface] = None + enforce_tls: bool = False + free_charging_service: bool = False + free_cert_install_service: bool = True + allow_cert_install_service: bool = True + supported_protocols: Optional[List[Protocol]] = None + supported_auth_options: Optional[List[AuthEnum]] = None + + def load_envs(self, env_path: Optional[str] = None) -> None: + """ + Tries to load the .env file containing all the project settings. + If `env_path` is not specified, it will get the .env on the current + working directory of the project + Args: + env_path (str): Absolute path to the location of the .env file + """ + env = environs.Env(eager=False) + if not env_path: + env_path = os.getcwd() + "/.env" + env.read_env(path=env_path) # read .env file, if it exists + self.iface = env.str("NETWORK_INTERFACE", default="eth0") + # validate the NIC selected + validate_nic(self.iface) + + # Redis Configuration + self.redis_host = env.str("REDIS_HOST", default='localhost') + self.redis_port = env.int("REDIS_PORT", default=6379) + + self.log_level = env.str("LOG_LEVEL", default="INFO") + + self.evse_controller = EVSEControllerInterface + if env.bool("SECC_CONTROLLER_SIM", default=False): + self.evse_controller = SimEVSEController + + # Indicates whether or not the SECC should always enforce a + # TLS-secured communication + # session. If True, the SECC will only fire up a TCP server with an + # SSL session context + # and ignore the Security byte value from the SDP request. + self.enforce_tls = env.bool("SECC_ENFORCE_TLS", default=False) + + # Indicates whether or not the ChargeService (energy transfer) is free. + # Should be configurable via OCPP messages. + # Must be one of the bool values True or False + self.free_charging_service = env.bool("FREE_CHARGING_SERVICE", default=False) + + # Indicates whether or not the installation of a contract certificate is free. + # Should be configurable via OCPP messages. + # Must be one of the bool values True or False + self.free_cert_install_service = env.bool("FREE_CERT_INSTALL_SERVICE", default=True) + + # Indicates whether or not the installation/update of a contract certificate + # shall be offered to the EV. Should be configurable via OCPP messages. + # Must be one of the bool values True or False + self.allow_cert_install_service = env.bool("ALLOW_CERT_INSTALL_SERVICE", default=True) + + # Supported protocols, used for SupportedAppProtocol (SAP). The order in which + # the protocols are listed here determines the priority (i.e. first list entry + # has higher priority than second list entry). A list entry must be a member + # of the Protocol enum + self.supported_protocols = [Protocol.ISO_15118_2, + Protocol.ISO_15118_20_AC] + + # Supported authentication options (named payment options in ISO 15118-2). + # Note: SECC will not offer 'pnc' if chosen transport protocol is not TLS + # Must be a list containing either AuthEnum members EIM (for External + # Identification Means), PNC (for Plug & Charge) or both + self.supported_auth_options = [AuthEnum.EIM, AuthEnum.PNC] -env = environs.Env(eager=False) - -env.read_env() # read .env - -# Choose the EVController implementation. Must be the class name of the controller -# that implements the EVControllerInterface -EVSE_CONTROLLER = SimEVSEController - -# Supported protocols, used for SupportedAppProtocol (SAP). The order in which -# the protocols are listed here determines the priority (i.e. first list entry -# has higher priority than second list entry). A list entry must be a member -# of the Protocol enum -SUPPORTED_PROTOCOLS = [ - Protocol.ISO_15118_2, - Protocol.ISO_15118_20_AC, -] - -# This timer is set in docker-compose.dev.yml, for merely debugging and dev -# reasons -NETWORK_INTERFACE = env.str("NETWORK_INTERFACE", default="eth0") - -# Supported authentication options (named payment options in ISO 15118-2). -# Note: SECC will not offer 'pnc' if chosen transport protocol is not TLS -# Must be a list containing either AuthEnum members EIM (for External -# Identification Means), PNC (for Plug & Charge) or both -SUPPORTED_AUTH_OPTIONS = [AuthEnum.EIM, AuthEnum.PNC] - -# Indicates whether or not the ChargeService (energy transfer) is free. -# Should be configurable via OCPP messages. -# Must be one of the bool values True or False -FREE_CHARGING_SERVICE = env.bool("FREE_CHARGING_SERVICE", default=False) - -# Indicates whether or not the installation of a contract certificate is free. -# Should be configurable via OCPP messages. -# Must be one of the bool values True or False -FREE_CERT_INSTALL_SERVICE = env.bool("FREE_CERT_INSTALL_SERVICE", default=True) - -# Indicates whether or not the installation/update of a contract certificate -# shall be offered to the EV. Should be configurable via OCPP messages. -# Must be one of the bool values True or False -ALLOW_CERT_INSTALL_SERVICE = env.bool("ALLOW_CERT_INSTALL_SERVICE", default=True) - -# Indicates whether or not the SECC should always enforce a TLS-secured communication -# session. If True, the SECC will only fire up a TCP server with an SSL session context -# and ignore the Security byte value from the SDP request. -ENFORCE_TLS = env.bool("SECC_ENFORCE_TLS", default=False) - -env.seal() # raise all errors at once, if any + env.seal() # raise all errors at once, if any diff --git a/iso15118/secc/start_secc.py b/iso15118/secc/start_secc.py index 80686e78..1b9a55a4 100644 --- a/iso15118/secc/start_secc.py +++ b/iso15118/secc/start_secc.py @@ -1,19 +1,23 @@ import asyncio import logging - +from typing import Optional +from iso15118.secc.secc_settings import Config from iso15118.secc.comm_session_handler import CommunicationSessionHandler logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) -async def main(): +async def main(env_path: Optional[str] = None): """ Entrypoint function that starts the ISO 15118 code running on the SECC (Supply Equipment Communication Controller) """ - session_handler = CommunicationSessionHandler() try: + # get configuration + config = Config() + config.load_envs(env_path) + session_handler = CommunicationSessionHandler(config) await session_handler.start_session_handler() except Exception as exc: logger.error(f"SECC terminated: {exc}") diff --git a/iso15118/secc/states/iso15118_20_states.py b/iso15118/secc/states/iso15118_20_states.py index 023a918e..c11ca4ae 100644 --- a/iso15118/secc/states/iso15118_20_states.py +++ b/iso15118/secc/states/iso15118_20_states.py @@ -9,10 +9,6 @@ from typing import List, Union from iso15118.secc.comm_session_handler import SECCCommunicationSession -from iso15118.secc.secc_settings import ( - ALLOW_CERT_INSTALL_SERVICE, - SUPPORTED_AUTH_OPTIONS, -) from iso15118.secc.states.secc_state import StateSECC from iso15118.shared.exi_codec import to_exi from iso15118.shared.messages.app_protocol import ( @@ -178,10 +174,11 @@ def process_message( auth_options: List[AuthEnum] = [] eim_as_res, pnc_as_res = None, None - if AuthEnum.EIM in SUPPORTED_AUTH_OPTIONS: + supported_auth_options = self.comm_session.config.supported_auth_options + if AuthEnum.EIM in supported_auth_options: auth_options.append(AuthEnum.EIM) eim_as_res = EIMAuthSetupResParams() - if AuthEnum.PNC in SUPPORTED_AUTH_OPTIONS: + if AuthEnum.PNC in supported_auth_options: auth_options.append(AuthEnum.PNC) pnc_as_res = PnCAuthSetupResParams( gen_challenge=get_random_bytes(16), @@ -197,7 +194,7 @@ def process_message( ), response_code=ResponseCode.OK, auth_services=auth_options, - cert_install_service=ALLOW_CERT_INSTALL_SERVICE, + cert_install_service=self.comm_session.config.allow_cert_install_service, eim_as_res=eim_as_res, pnc_as_res=pnc_as_res, ) diff --git a/iso15118/secc/states/iso15118_2_states.py b/iso15118/secc/states/iso15118_2_states.py index 5f290b89..91b5147d 100644 --- a/iso15118/secc/states/iso15118_2_states.py +++ b/iso15118/secc/states/iso15118_2_states.py @@ -9,12 +9,6 @@ from typing import List, Optional, Type, Union from iso15118.secc.comm_session_handler import SECCCommunicationSession -from iso15118.secc.secc_settings import ( - ALLOW_CERT_INSTALL_SERVICE, - FREE_CERT_INSTALL_SERVICE, - FREE_CHARGING_SERVICE, - SUPPORTED_AUTH_OPTIONS, -) from iso15118.secc.states.secc_state import StateSECC from iso15118.shared.exceptions import ( CertAttributeError, @@ -92,7 +86,6 @@ from iso15118.shared.messages.iso15118_20.common_types import ( V2GMessage as V2GMessageV20, ) -from iso15118.shared.messages.sdp import Security from iso15118.shared.messages.timeouts import Timeouts from iso15118.shared.notifications import StopNotification from iso15118.shared.security import ( @@ -267,10 +260,11 @@ def get_services(self, category_filter: ServiceCategory) -> ServiceDiscoveryRes: auth_options.append(AuthOptions(value=AuthEnum.PNC_V2)) self.comm_session.offered_auth_options.append(AuthEnum.PNC_V2) else: - if AuthEnum.EIM in SUPPORTED_AUTH_OPTIONS: + supported_auth_options = self.comm_session.config.supported_auth_options + if AuthEnum.EIM in supported_auth_options: auth_options.append(AuthOptions(value=AuthEnum.EIM_V2)) self.comm_session.offered_auth_options.append(AuthEnum.EIM_V2) - if AuthEnum.PNC in SUPPORTED_AUTH_OPTIONS and self.comm_session.is_tls: + if AuthEnum.PNC in supported_auth_options and self.comm_session.is_tls: auth_options.append(AuthOptions(value=AuthEnum.PNC_V2)) self.comm_session.offered_auth_options.append(AuthEnum.PNC_V2) @@ -278,7 +272,7 @@ def get_services(self, category_filter: ServiceCategory) -> ServiceDiscoveryRes: service_id=ServiceID.CHARGING, service_name=ServiceName.CHARGING, service_category=ServiceCategory.CHARGING, - free_service=FREE_CHARGING_SERVICE, + free_service=self.comm_session.config.free_charging_service, supported_energy_transfer_mode=self.comm_session.evse_controller.get_supported_energy_transfer_modes(), ) @@ -286,7 +280,7 @@ def get_services(self, category_filter: ServiceCategory) -> ServiceDiscoveryRes: # Value-added services (VAS), like installation of contract certificates # and the Internet service, are only allowed with TLS-secured comm. if self.comm_session.is_tls: - if ALLOW_CERT_INSTALL_SERVICE and ( + if self.comm_session.config.allow_cert_install_service and ( category_filter is None or category_filter == ServiceCategory.CERTIFICATE ): @@ -294,7 +288,7 @@ def get_services(self, category_filter: ServiceCategory) -> ServiceDiscoveryRes: service_id=2, service_name=ServiceName.CERTIFICATE, service_category=ServiceCategory.CERTIFICATE, - free_service=FREE_CERT_INSTALL_SERVICE, + free_service=self.comm_session.config.free_cert_install_service, ) service_list.append(Service(service_details=cert_install_service)) diff --git a/iso15118/secc/states/sap_states.py b/iso15118/secc/states/sap_states.py index 057ef49c..b162a66d 100644 --- a/iso15118/secc/states/sap_states.py +++ b/iso15118/secc/states/sap_states.py @@ -8,13 +8,11 @@ import logging.config from typing import Type, Union -from iso15118.secc import secc_settings from iso15118.secc.comm_session_handler import SECCCommunicationSession from iso15118.secc.states.iso15118_2_states import SessionSetup as SessionSetupV2 from iso15118.secc.states.iso15118_20_states import SessionSetup as SessionSetupV20 from iso15118.secc.states.secc_state import StateSECC from iso15118.shared import settings -from iso15118.shared.exceptions import FaultyStateImplementationError from iso15118.shared.messages.app_protocol import ( ResponseCodeSAP, SupportedAppProtocolReq, @@ -64,7 +62,7 @@ def process_message( sap_req.app_protocol.sort(key=lambda proto: proto.priority) sap_res: Union[SupportedAppProtocolRes, None] = None supported_ns_list = [ - protocol.ns.value for protocol in secc_settings.SUPPORTED_PROTOCOLS + protocol.ns.value for protocol in self.comm_session.config.supported_protocols ] next_state: Type[State] = Terminate # some default that is not None diff --git a/iso15118/secc/transport/tcp_server.py b/iso15118/secc/transport/tcp_server.py index fac08b29..8c016cdc 100644 --- a/iso15118/secc/transport/tcp_server.py +++ b/iso15118/secc/transport/tcp_server.py @@ -3,7 +3,6 @@ import socket from typing import Tuple -from iso15118.secc.secc_settings import NETWORK_INTERFACE from iso15118.shared import settings from iso15118.shared.network import get_link_local_full_addr, get_tcp_port from iso15118.shared.notifications import TCPClientNotification @@ -26,11 +25,12 @@ class TCPServer(asyncio.Protocol): # (host, port, flowinfo, scope_id) ipv6_address_host: str - def __init__(self, session_handler_queue: asyncio.Queue) -> None: + def __init__(self, session_handler_queue: asyncio.Queue, iface: str) -> None: self._session_handler_queue = session_handler_queue # The dynamic TCP port number in the range of (49152-65535) self.port_no_tls = get_tcp_port() self.port_tls = get_tcp_port() + self.iface = iface # Making sure the TCP and TLS port are definitely different while self.port_no_tls == self.port_tls: @@ -93,7 +93,7 @@ async def server_factory(self, tls: bool) -> None: # Allows address to be reused sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - self.full_ipv6_address = await get_link_local_full_addr(port, NETWORK_INTERFACE) + self.full_ipv6_address = await get_link_local_full_addr(port, self.iface) self.ipv6_address_host = self.full_ipv6_address[0] # Bind the socket to the IP address and port for receiving @@ -112,7 +112,7 @@ async def server_factory(self, tls: bool) -> None: logger.debug( f"{server_type} server started at " - f"address {self.ipv6_address_host}%{NETWORK_INTERFACE} and " + f"address {self.ipv6_address_host}%{self.iface} and " f"port {port}" ) diff --git a/iso15118/secc/transport/udp_server.py b/iso15118/secc/transport/udp_server.py index 1233e420..8f05b93a 100644 --- a/iso15118/secc/transport/udp_server.py +++ b/iso15118/secc/transport/udp_server.py @@ -5,10 +5,9 @@ from asyncio import DatagramTransport from typing import Tuple, Optional -from iso15118.secc.secc_settings import NETWORK_INTERFACE from iso15118.shared import settings from iso15118.shared.messages.v2gtp import V2GTPMessage -from iso15118.shared.network import SDP_MULTICAST_GROUP, SDP_SERVER_PORT, validate_nic +from iso15118.shared.network import SDP_MULTICAST_GROUP, SDP_SERVER_PORT from iso15118.shared.notifications import ( ReceiveTimeoutNotification, UDPPacketNotification, @@ -41,14 +40,15 @@ class UDPServer(asyncio.DatagramProtocol): https://docs.python.org/3/library/asyncio-protocol.html """ - def __init__(self, session_handler_queue: asyncio.Queue): + def __init__(self, session_handler_queue: asyncio.Queue, iface: str): self.started: bool = False + self.iface = iface self._session_handler_queue: asyncio.Queue = session_handler_queue self._rcv_queue: asyncio.Queue = asyncio.Queue() self._transport: Optional[DatagramTransport] = None @staticmethod - def _create_socket() -> 'socket': + def _create_socket(iface: str) -> 'socket': """ This method is necessary because Python does not allow async def __init__. @@ -79,9 +79,7 @@ async def __init__. # aton stands for "Ascii TO Numeric" multicast_group_bin = socket.inet_pton(socket.AF_INET6, SDP_MULTICAST_GROUP) - validate_nic(NETWORK_INTERFACE) - - interface_idx = socket.if_nametoindex(NETWORK_INTERFACE) + interface_idx = socket.if_nametoindex(iface) join_multicast_group_req = ( multicast_group_bin + struct.pack("@I", interface_idx) # address + interface @@ -100,13 +98,13 @@ async def start(self): # One protocol instance will be created to serve all client requests self._transport, _ = await loop.create_datagram_endpoint( lambda: self, - sock=self._create_socket(), + sock=self._create_socket(self.iface), reuse_address=True, ) logger.debug( "UDP server started at address " - f"{SDP_MULTICAST_GROUP}%{NETWORK_INTERFACE} " + f"{SDP_MULTICAST_GROUP}%{self.iface} " f"and port {SDP_SERVER_PORT}" ) tasks = [self.rcv_task()] diff --git a/poetry.lock b/poetry.lock index 58285796..5a276713 100644 --- a/poetry.lock +++ b/poetry.lock @@ -23,15 +23,12 @@ python-versions = "*" [[package]] name = "async-timeout" -version = "4.0.1" +version = "4.0.2" description = "Timeout context manager for asyncio programs" category = "main" optional = false python-versions = ">=3.6" -[package.dependencies] -typing-extensions = ">=3.6.5" - [[package]] name = "asynctest" version = "0.13.0" @@ -684,7 +681,7 @@ python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" [[package]] name = "tomli" -version = "1.2.2" +version = "1.2.3" description = "A lil' TOML parser" category = "dev" optional = false @@ -726,8 +723,8 @@ alabaster = [ {file = "alabaster-0.7.12.tar.gz", hash = "sha256:a661d72d58e6ea8a57f7a86e37d86716863ee5e92788398526d58b26a4e4dc02"}, ] async-timeout = [ - {file = "async-timeout-4.0.1.tar.gz", hash = "sha256:b930cb161a39042f9222f6efb7301399c87eeab394727ec5437924a36d6eef51"}, - {file = "async_timeout-4.0.1-py3-none-any.whl", hash = "sha256:a22c0b311af23337eb05fcf05a8b51c3ea53729d46fb5460af62bee033cec690"}, + {file = "async-timeout-4.0.2.tar.gz", hash = "sha256:2163e1640ddb52b7a8c80d0a67a08587e5d245cc9c553a74a847056bc2976b15"}, + {file = "async_timeout-4.0.2-py3-none-any.whl", hash = "sha256:8ca1e4fcf50d07413d66d1a5e416e42cfdf5851c981d679a09851a6853383b3c"}, ] asynctest = [ {file = "asynctest-0.13.0-py3-none-any.whl", hash = "sha256:5da6118a7e6d6b54d83a8f7197769d046922a44d2a99c21382f0a6e4fadae676"}, @@ -1224,8 +1221,8 @@ toml = [ {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, ] tomli = [ - {file = "tomli-1.2.2-py3-none-any.whl", hash = "sha256:f04066f68f5554911363063a30b108d2b5a5b1a010aa8b6132af78489fe3aade"}, - {file = "tomli-1.2.2.tar.gz", hash = "sha256:c6ce0015eb38820eaf32b5db832dbc26deb3dd427bd5f6556cf0acac2c214fee"}, + {file = "tomli-1.2.3-py3-none-any.whl", hash = "sha256:e3069e4be3ead9668e21cb9b074cd948f7b3113fd9c8bba083f48247aab8b11c"}, + {file = "tomli-1.2.3.tar.gz", hash = "sha256:05b6166bff487dc068d322585c7ea4ef78deed501cc124060e0f238e89a9231f"}, ] typing-extensions = [ {file = "typing_extensions-4.0.1-py3-none-any.whl", hash = "sha256:7f001e5ac290a0c0401508864c7ec868be4e701886d5b573a9528ed3973d9d3b"}, From a84f1b25493669cfb918af7a63569e3479a0263e Mon Sep 17 00:00:00 2001 From: tropxy Date: Wed, 22 Dec 2021 13:50:43 +0000 Subject: [PATCH 2/6] Added configuration handling for the evcc --- .env.dev.docker | 3 +- .env.dev.local | 3 +- README.md | 5 +- iso15118/evcc/comm_session_handler.py | 68 +++++----- iso15118/evcc/controller/interface.py | 3 +- iso15118/evcc/controller/simulator.py | 5 +- iso15118/evcc/evcc_settings.py | 147 +++++++++++++-------- iso15118/evcc/start_evcc.py | 11 +- iso15118/evcc/states/iso15118_20_states.py | 3 +- iso15118/evcc/states/sap_states.py | 12 +- iso15118/evcc/transport/tcp_client.py | 6 +- iso15118/evcc/transport/udp_client.py | 15 +-- iso15118/secc/secc_settings.py | 1 + 13 files changed, 163 insertions(+), 119 deletions(-) diff --git a/.env.dev.docker b/.env.dev.docker index d814c347..90aecd0c 100644 --- a/.env.dev.docker +++ b/.env.dev.docker @@ -13,7 +13,8 @@ ALLOW_CERT_INSTALL_SERVICE=True SECC_ENFORCE_TLS=False # EVCC Settings -USE_TLS=False +EVCC_CONTROLLER_SIM=True +EVCC_USE_TLS=False EVCC_ENFORCE_TLS=False # LOG Settings diff --git a/.env.dev.local b/.env.dev.local index 62bb0f70..cacac1d6 100644 --- a/.env.dev.local +++ b/.env.dev.local @@ -13,7 +13,8 @@ ALLOW_CERT_INSTALL_SERVICE=True SECC_ENFORCE_TLS=False # EVCC Settings -USE_TLS=False +EVCC_CONTROLLER_SIM=True +EVCC_USE_TLS=False EVCC_ENFORCE_TLS=False # LOG Settings diff --git a/README.md b/README.md index d1513ffe..c33286d4 100644 --- a/README.md +++ b/README.md @@ -109,8 +109,11 @@ The following table provides a few of the available variables: | NETWORK_INTERFACE | `eth0` | HomePlug Green PHY Network Interface from which the high-level communication (HLC) will be established | | SECC_CONTROLLER_SIM | `False` | Whether or not to simulate the SECC Controller Interface | | SECC_ENFORCE_TLS | `False` | Whether or not the SECC will enforce a TLS connection | +| EVCC_CONTROLLER_SIM | `False` | Whether or not to simulate the EVCC Controller Interface | +| EVCC_USE_TLS | `True` | Whether or not the EVCC signals the preference to communicate with a TLS connection | +| EVCC_ENFORCE_TLS | `False` | Whether or not the EVCC will only accept TLS connections | | REDIS_HOST | `localhost` | Redis Host URL | -| REDIS_PORT | `10001` | Redis Port | +| REDIS_PORT | `6379` | Redis Port | | LOG_LEVEL | `INFO` | Level of the Python log service | The project includes a few environmental files, in the root directory, for diff --git a/iso15118/evcc/comm_session_handler.py b/iso15118/evcc/comm_session_handler.py index dd9f5bb1..c2d3f461 100644 --- a/iso15118/evcc/comm_session_handler.py +++ b/iso15118/evcc/comm_session_handler.py @@ -15,15 +15,8 @@ from pydantic.error_wrappers import ValidationError -from iso15118.evcc import evcc_settings +from iso15118.evcc.evcc_settings import Config from iso15118.evcc.controller.interface import EVControllerInterface -from iso15118.evcc.evcc_settings import ( - ENFORCE_TLS, - EV_CONTROLLER, - SDP_RETRY_CYCLES, - SUPPORTED_PROTOCOLS, - USE_TLS, -) from iso15118.evcc.transport.tcp_client import TCPClient from iso15118.evcc.transport.udp_client import UDPClient from iso15118.shared.comm_session import V2GCommunicationSession @@ -71,6 +64,7 @@ def __init__( self, transport: Tuple[StreamReader, StreamWriter], session_handler_queue: asyncio.Queue, + config: Config ): # Need to import here to avoid a circular import error # pylint: disable=import-outside-toplevel @@ -83,8 +77,10 @@ def __init__( # Session, so we dont need to do this self injection, since self # is already injected by default on a child super().__init__(transport, SupportedAppProtocol, session_handler_queue, self) + + self.config = config # The EV controller that implements the interface EVControllerInterface - self.ev_controller: EVControllerInterface = EV_CONTROLLER() + self.ev_controller: EVControllerInterface = self.config.ev_controller() # The authorization option (called PaymentOption in ISO 15118-2) the # EVCC selected from the authorization options offered by the SECC self.selected_auth_option: Optional[AuthEnum] = None @@ -108,7 +104,7 @@ def __init__( self.renegotiation_requested = False # The ID of the EVSE that controls the power flow to the EV self.evse_id: str = "" - self.is_tls = USE_TLS + self.is_tls = self.config.use_tls def create_sap(self) -> Union[SupportedAppProtocolReq, None]: """ @@ -124,7 +120,7 @@ def create_sap(self) -> Union[SupportedAppProtocolReq, None]: app_protocols = [] schema_id = 0 priority = 0 - for protocol in SUPPORTED_PROTOCOLS: + for protocol in self.config.supported_protocols: # A SchemaID (schema_id) is simply a running counter, enabling the # SECC to refer to a specific entry. It can, in principle, be # randomly chosen by the EVCC as long as it's in the value range of @@ -188,9 +184,21 @@ def save_session_info(self): "Writing session variables to settings for use when " "resuming the communication session later" ) - RESUME_SESSION_ID = self.session_id - evcc_settings.RESUME_SELECTED_AUTH_OPTION = self.selected_auth_option - evcc_settings.RESUME_REQUESTED_ENERGY_MODE = self.selected_energy_mode + + # === PAUSING RELATED INFORMATION === + # If a charging session needs to be paused, the EVCC needs to persist certain + # information that must be provided again once the communication session + # resumes. This information includes: + # - Session ID: int or None + # - Selected authorization option: must be a member of AuthEnum enum or None + # - Requested energy transfer mode: must be a member of EnergyTransferModeEnum + # or None + # TODO Check what ISO 15118-20 demands for pausing + + # TODO: save the settings into redis + # RESUME_SESSION_ID = self.session_id + # RESUME_SELECTED_AUTH_OPTION = self.selected_auth_option + # RESUME_REQUESTED_ENERGY_MODE = self.selected_energy_mode class CommunicationSessionHandler: @@ -201,13 +209,14 @@ class CommunicationSessionHandler: # pylint: disable=too-many-instance-attributes - def __init__(self): + def __init__(self, config: Config): self.list_of_tasks = [] self.udp_client = None self.tcp_client = None self.tls_client = None + self.config = config self.sdp_retries_number = SDP_MAX_REQUEST_COUNTER - self._sdp_retry_cycles = SDP_RETRY_CYCLES + self._sdp_retry_cycles = self.config.sdp_retry_cycles # Receiving queue for UDP client to notify about incoming datagrams self._rcv_queue = asyncio.Queue(0) @@ -224,15 +233,7 @@ async def start_session_handler(self): async def __init__. Therefore, we need to create a separate async method to be our constructor. """ - if not SDP_RETRY_CYCLES or SDP_RETRY_CYCLES <= 0: - logger.error( - "The EVCC setting SDP_RETRY_CYCLES must set to a " - "value greater than or equal to 1, otherwise the " - "communication session handler cannot start" - ) - return - - self.udp_client = UDPClient(self._rcv_queue) + self.udp_client = UDPClient(self._rcv_queue, self.config.iface) self.list_of_tasks = [ self.udp_client.start(), self.get_from_rcv_queue(self._rcv_queue), @@ -255,7 +256,7 @@ async def send_sdp(self): while not self.udp_client.started: await asyncio.sleep(0.1) security = Security.NO_TLS - if USE_TLS: + if self.config.use_tls: security = Security.TLS sdp_request = SDPRequest(security=security, transport_protocol=Transport.TCP) v2gtp_msg = V2GTPMessage( @@ -311,7 +312,7 @@ async def restart_sdp(self, new_sdp_cycle: bool): if new_sdp_cycle: if self._sdp_retry_cycles == 0: raise SDPFailedError( - f"EVCC tried {SDP_RETRY_CYCLES} times to initiate a " + f"EVCC tried {self.config.sdp_retry_cycles} times to initiate a " "V2GCommunicationSession, but maximum number of SDP retry " f"cycles is now reached. {shutdown_msg}" ) @@ -347,7 +348,7 @@ async def start_comm_session(self, host: IPv6Address, port: int, is_tls: bool): f"{host.compressed} at port {port} ..." ) self.tcp_client = await TCPClient.create( - host, port, self._rcv_queue, is_tls + host, port, self._rcv_queue, is_tls, self.config.iface ) logger.debug("TCP client connected") except Exception as exc: @@ -358,7 +359,8 @@ async def start_comm_session(self, host: IPv6Address, port: int, is_tls: bool): return comm_session = EVCCCommunicationSession( - (self.tcp_client.reader, self.tcp_client.writer), self._rcv_queue + (self.tcp_client.reader, self.tcp_client.writer), self._rcv_queue, + self.config ) try: @@ -419,13 +421,13 @@ 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 ENFORCE_TLS) or ( - secc_signals_tls and not USE_TLS + if (not secc_signals_tls and self.config.enforce_tls) or ( + secc_signals_tls and not self.config.use_tls ): logger.error( "Security mismatch, can't initiate communication session." - f"\nEVCC setting USE_TLS: {USE_TLS}" - f"\nEVCC setting ENFORCE_TLS: {ENFORCE_TLS}" + f"\nEVCC setting USE_TLS: {self.config.use_tls}" + f"\nEVCC setting ENFORCE_TLS: {self.config.enforce_tls}" f"\nSDP response signals TLS: {secc_signals_tls}" ) return diff --git a/iso15118/evcc/controller/interface.py b/iso15118/evcc/controller/interface.py index 059ff1b6..48e58217 100644 --- a/iso15118/evcc/controller/interface.py +++ b/iso15118/evcc/controller/interface.py @@ -32,7 +32,7 @@ class ChargeParamsV2: class EVControllerInterface(ABC): @abstractmethod - def get_evcc_id(self, protocol: Protocol) -> str: + def get_evcc_id(self, protocol: Protocol, iface: str) -> str: """ Retrieves the EVCCID, which is a field of the SessionSetupReq. The structure of the EVCCID depends on the protocol version. In DIN SPEC 70121 and ISO 15118-2, @@ -41,6 +41,7 @@ def get_evcc_id(self, protocol: Protocol) -> str: Args: protocol: The communication protocol, a member of the Protocol enum + iface (str): The network interface selected Raises: InvalidProtocolError diff --git a/iso15118/evcc/controller/simulator.py b/iso15118/evcc/controller/simulator.py index dbee31bc..d3864033 100644 --- a/iso15118/evcc/controller/simulator.py +++ b/iso15118/evcc/controller/simulator.py @@ -46,13 +46,12 @@ class SimEVController(EVControllerInterface): def __init__(self): self.charging_loop_cycles: int = 0 - def get_evcc_id(self, protocol: Protocol) -> str: + def get_evcc_id(self, protocol: Protocol, iface: str) -> str: """Overrides EVControllerInterface.get_evcc_id().""" - from iso15118.evcc.evcc_settings import NETWORK_INTERFACE if protocol in (Protocol.ISO_15118_2, Protocol.DIN_SPEC_70121): try: - hex_str = get_nic_mac_address(NETWORK_INTERFACE) + hex_str = get_nic_mac_address(iface) return hex_str.replace(":", "").upper() except MACAddressNotFound as exc: logger.warning( diff --git a/iso15118/evcc/evcc_settings.py b/iso15118/evcc/evcc_settings.py index e4e4eb29..bd86f09b 100644 --- a/iso15118/evcc/evcc_settings.py +++ b/iso15118/evcc/evcc_settings.py @@ -1,63 +1,94 @@ -import environs - +import logging +import os +from dataclasses import dataclass +from typing import Optional, Type, List from iso15118.evcc.controller.simulator import SimEVController +from iso15118.evcc.controller.interface import EVControllerInterface from iso15118.shared.messages.enums import Protocol +from iso15118.shared.network import validate_nic + +import environs +from marshmallow.validate import Range + + +logger = logging.getLogger(__name__) + + +@dataclass +class Config: + iface: Optional[str] = None + redis_host: Optional[str] = None + redis_port: Optional[int] = None + log_level: Optional[int] = None + ev_controller: Type[EVControllerInterface] = None + sdp_retry_cycles: Optional[int] = None + max_contract_certs: Optional[int] = None + use_tls: bool = True + enforce_tls: bool = False + supported_protocols: Optional[List[Protocol]] = None + + def load_envs(self, env_path: Optional[str] = None) -> None: + """ + Tries to load the .env file containing all the project settings. + If `env_path` is not specified, it will get the .env on the current + working directory of the project + Args: + env_path (str): Absolute path to the location of the .env file + """ + env = environs.Env(eager=False) + if not env_path: + env_path = os.getcwd() + "/.env" + env.read_env(path=env_path) # read .env file, if it exists + + self.iface = env.str("NETWORK_INTERFACE", default="eth0") + # validate the NIC selected + validate_nic(self.iface) + + # Redis Configuration + self.redis_host = env.str("REDIS_HOST", default='localhost') + self.redis_port = env.int("REDIS_PORT", default=6379) + + self.log_level = env.str("LOG_LEVEL", default="INFO") + + # Choose the EVController implementation. Must be the class name of the controller + # that implements the EVControllerInterface + self.ev_controller = EVControllerInterface + if env.bool("EVCC_CONTROLLER_SIM", default=False): + self.ev_controller = SimEVController + + # How often shall SDP (SECC Discovery Protocol) retries happen before reverting + # to using nominal duty cycle PWM-based charging? + self.sdp_retry_cycles = env.int("SDP_RETRY_CYCLES", default=1, + validate=lambda n: n > 0) + + # For ISO 15118-20 only + # Maximum amount of contract certificates (and associated certificate chains) + # the EV can store. That value is used in the CertificateInstallationReq. + # Must be an integer between 0 and 65535, should be bigger than 0. + self.max_contract_certs = env.int("MAX_CONTRACT_CERTS", default=3, + validate=Range(min=1, max=65535)) + + # Indicates the security level (either TCP (unencrypted) or TLS (encrypted)) the EVCC + # shall send in the SDP request + self.use_tls = env.bool("EVCC_USE_TLS", default=True) + + # Indicates whether or not the EVCC should always enforce a TLS-secured communication + # session. If True, the EVCC will only continue setting up a communication session if + # the SECC's SDP response has the Security field set to the enum value Security.TLS. + # If the USE_TLS setting is set to False and ENFORCE_TLS is set to True, then + # ENFORCE_TLS overrules USE_TLS. + self.enforce_tls = env.bool("EVCC_ENFORCE_TLS", default=False) + + # Supported protocols, used for SupportedAppProtocol (SAP). The order in which + # the protocols are listed here determines the priority (i.e. first list entry + # has higher priority than second list entry). A list entry must be a member + # of the Protocol enum + self.supported_protocols = [Protocol.ISO_15118_2, + Protocol.ISO_15118_20_AC] + + env.seal() # raise all errors at once, if any + -# Choose the EVController implementation. Must be the class name of the controller -# that implements the EVControllerInterface -EV_CONTROLLER = SimEVController - -env = environs.Env(eager=False) - -env.read_env() # read .env - -# Supported protocols, used for SupportedAppProtocol (SAP). The order in which -# the protocols are listed here determines the priority (i.e. first list entry -# has higher priority than second list entry). A list entry must be a member -# of the Protocol enum -SUPPORTED_PROTOCOLS = [ - Protocol.ISO_15118_2, - Protocol.ISO_15118_20_AC, -] - - -# Provide the name of a specific network interface card (NIC, like 'en0') here. -# If no NIC is provided, the list of NICs is scanned and the first one that has -# an IPv6 address with a local-link address is chosen. -NETWORK_INTERFACE = env.str("NETWORK_INTERFACE", default="eth0") - -# How often shall SDP (SECC Discovery Protocol) retries happen before reverting -# to using nominal duty cycle PWM-based charging? -SDP_RETRY_CYCLES = env.int("SDP_RETRY_CYCLES", default=1) - -# === PAUSING RELATED INFORMATION === -# If a charging session needs to be paused, the EVCC needs to persist certain -# information that must be provided again once the communication session -# resumes. This information includes: -# - Session ID: int or None -# - Selected authorization option: must be a member of AuthEnum enum or None -# - Requested energy transfer mode: must be a member of EnergyTransferModeEnum -# or None -# TODO Check what ISO 15118-20 demands for pausing RESUME_SELECTED_AUTH_OPTION = None RESUME_SESSION_ID = None -RESUME_REQUESTED_ENERGY_MODE = None - -# For ISO 15118-20 only -# Maximum amount of contract certificates (and associated certificate chains) -# the EV can store. That value is used in the CertificateInstallationReq. -# Must be an integer between 0 and 65535, should be bigger than 0. -MAX_CONTRACT_CERTS = env.int("MAX_CONTRACT_CERTS", default=3) - -# Indicates the security level (either TCP (unencrypted) or TLS (encrypted)) the EVCC -# shall send in the SDP request -USE_TLS = env.bool("USE_TLS", default=True) - -# Indicates whether or not the EVCC should always enforce a TLS-secured communication -# session. If True, the EVCC will only continue setting up a communication session if -# the SECC's SDP response has the Security field set to the enum value Security.TLS. -# If the USE_TLS setting is set to False and ENFORCE_TLS is set to True, then -# ENFORCE_TLS overrules USE_TLS. -ENFORCE_TLS = env.bool("EVCC_ENFORCE_TLS", default=False) - -env.seal() # raise all errors at once, if any +RESUME_REQUESTED_ENERGY_MODE = None \ No newline at end of file diff --git a/iso15118/evcc/start_evcc.py b/iso15118/evcc/start_evcc.py index 46eecab4..aee4fd78 100644 --- a/iso15118/evcc/start_evcc.py +++ b/iso15118/evcc/start_evcc.py @@ -1,21 +1,26 @@ import asyncio import logging - +from typing import Optional +from iso15118.evcc.evcc_settings import Config from iso15118.evcc.comm_session_handler import CommunicationSessionHandler logging.basicConfig(level=logging.DEBUG) logger = logging.getLogger(__name__) -async def main(): +async def main(env_path: Optional[str] = None): """ Entrypoint function that starts the ISO 15118 code running on the EVCC (EV Communication Controller) """ # TODO: we need to read the ISO 15118 version and the Security value # from some settings file - session_handler = CommunicationSessionHandler() + try: + # get configuration + config = Config() + config.load_envs(env_path) + session_handler = CommunicationSessionHandler(config) await session_handler.start_session_handler() except Exception as exc: logger.error(f"EVCC terminated: {exc}") diff --git a/iso15118/evcc/states/iso15118_20_states.py b/iso15118/evcc/states/iso15118_20_states.py index c8550f7f..25bb1317 100644 --- a/iso15118/evcc/states/iso15118_20_states.py +++ b/iso15118/evcc/states/iso15118_20_states.py @@ -9,7 +9,6 @@ from typing import Union from iso15118.evcc.comm_session_handler import EVCCCommunicationSession -from iso15118.evcc.evcc_settings import MAX_CONTRACT_CERTS from iso15118.evcc.states.evcc_state import StateEVCC from iso15118.shared import settings from iso15118.shared.exceptions import PrivateKeyReadError @@ -170,7 +169,7 @@ def process_message( ) ) ], - max_contract_cert_chains=MAX_CONTRACT_CERTS, + max_contract_cert_chains=self.comm_session.config.max_contract_certs, prioritized_emaids=self.comm_session.ev_controller.get_prioritised_emaids(), ) diff --git a/iso15118/evcc/states/sap_states.py b/iso15118/evcc/states/sap_states.py index 0fdbfa0d..12a2ddf1 100644 --- a/iso15118/evcc/states/sap_states.py +++ b/iso15118/evcc/states/sap_states.py @@ -11,7 +11,6 @@ from iso15118.evcc import evcc_settings from iso15118.evcc.comm_session_handler import EVCCCommunicationSession -from iso15118.evcc.evcc_settings import SUPPORTED_PROTOCOLS from iso15118.evcc.states.evcc_state import StateEVCC from iso15118.evcc.states.iso15118_2_states import SessionSetup as SessionSetupV2 from iso15118.evcc.states.iso15118_20_states import SessionSetup as SessionSetupV20 @@ -76,7 +75,10 @@ def process_message( next_msg: Union[ SupportedAppProtocolReq, SupportedAppProtocolRes, BodyBase, V2GRequest ] = SessionSetupReqV2( - evcc_id=self.comm_session.ev_controller.get_evcc_id(Protocol.ISO_15118_2) + evcc_id=self.comm_session.ev_controller.get_evcc_id( + Protocol.ISO_15118_2, + self.comm_session.config.iface + ) ) next_ns: Namespace = Namespace.ISO_V2_MSG_DEF next_state: Type[State] = Terminate # some default that is not None @@ -100,7 +102,8 @@ def process_message( next_msg = SessionSetupReqV20( header=header, evcc_id=self.comm_session.ev_controller.get_evcc_id( - self.comm_session.protocol + self.comm_session.protocol, + self.comm_session.config.iface ), ) next_ns = Namespace.ISO_V20_COMMON_MSG @@ -112,7 +115,7 @@ def process_message( "EVCC sent an invalid protocol namespace in " f"its previous SupportedAppProtocolReq: " f"{protocol.protocol_ns}. Allowed " - f"namespaces are: {SUPPORTED_PROTOCOLS}" + f"namespaces are: {self.comm_session.config.supported_protocols}" ) raise MessageProcessingError("SupportedAppProtocolReq") break @@ -137,6 +140,7 @@ def get_session_id(self) -> str: If there's no stored session ID, we'll set the session ID equal to zero. The session ID is also stored as a comm session variable. """ + # TODO: get the session id from Redis if evcc_settings.RESUME_SESSION_ID: self.comm_session.session_id = evcc_settings.RESUME_SESSION_ID evcc_settings.RESUME_SESSION_ID = None diff --git a/iso15118/evcc/transport/tcp_client.py b/iso15118/evcc/transport/tcp_client.py index f77a92b0..b0184226 100644 --- a/iso15118/evcc/transport/tcp_client.py +++ b/iso15118/evcc/transport/tcp_client.py @@ -3,7 +3,6 @@ import socket from ipaddress import IPv6Address -from iso15118.evcc.evcc_settings import NETWORK_INTERFACE from iso15118.shared import settings from iso15118.shared.security import get_ssl_context @@ -34,7 +33,8 @@ async def create( host: IPv6Address, port: int, session_handler_queue: asyncio.Queue, - is_tls: bool + is_tls: bool, + iface: str ) -> "TCPClient": """ TCPClient setup @@ -46,7 +46,7 @@ async def create( # which includes the scope id. This is why, in the next line, # we concatenate the host IP with the NIC defined with the # NETWORK_INTERFACE env - full_host_address = host.compressed + f"%{NETWORK_INTERFACE}" + full_host_address = host.compressed + f"%{iface}" try: self.reader, self.writer = await asyncio.open_connection( diff --git a/iso15118/evcc/transport/udp_client.py b/iso15118/evcc/transport/udp_client.py index cbb23d53..d8d71be5 100644 --- a/iso15118/evcc/transport/udp_client.py +++ b/iso15118/evcc/transport/udp_client.py @@ -5,11 +5,10 @@ from asyncio import DatagramProtocol, DatagramTransport from typing import Tuple, Optional -from iso15118.evcc.evcc_settings import NETWORK_INTERFACE from iso15118.shared import settings from iso15118.shared.messages.timeouts import Timeouts from iso15118.shared.messages.v2gtp import V2GTPMessage -from iso15118.shared.network import SDP_MULTICAST_GROUP, SDP_SERVER_PORT, validate_nic +from iso15118.shared.network import SDP_MULTICAST_GROUP, SDP_SERVER_PORT from iso15118.shared.notifications import ( ReceiveTimeoutNotification, UDPPacketNotification, @@ -37,22 +36,20 @@ class UDPClient(DatagramProtocol): https://docs.python.org/3/library/asyncio-protocol.html """ - def __init__(self, session_handler_queue: asyncio.Queue): + def __init__(self, session_handler_queue: asyncio.Queue, iface: str): self._session_handler_queue: asyncio.Queue = session_handler_queue # Indication whether or not the UDP client connection is open or closed self.started: bool = False self._rcv_queue: asyncio.Queue = asyncio.Queue() self._transport: Optional[DatagramTransport] = None + self.iface = iface @staticmethod - def _create_socket() -> 'socket': + def _create_socket(iface: str) -> 'socket': """ This method creates an IPv6 socket configured to send multicast datagrams """ - # raises an exception if the interface chosen is invalid - validate_nic(NETWORK_INTERFACE) - # Initialise the socket for IPv6 datagrams # Address family (determines network layer protocol, here IPv6) # Socket type (datagram, determines transport layer protocol UDP) @@ -71,7 +68,7 @@ def _create_socket() -> 'socket': # which interface it shall send its multicast packets. It can be seen # as the dual of bind(), in the server side, since bind() controls which # interface(s) the socket receives multicast packets from. - interface_index = socket.if_nametoindex(NETWORK_INTERFACE) + interface_index = socket.if_nametoindex(iface) sock.setsockopt(socket.IPPROTO_IPV6, socket.IPV6_MULTICAST_IF, interface_index) return sock @@ -86,7 +83,7 @@ async def start(self): loop = asyncio.get_running_loop() self._transport, _ = await loop.create_datagram_endpoint( protocol_factory=lambda: self, - sock=self._create_socket(), + sock=self._create_socket(self.iface), ) def connection_made(self, transport): diff --git a/iso15118/secc/secc_settings.py b/iso15118/secc/secc_settings.py index f614fbab..c69076da 100644 --- a/iso15118/secc/secc_settings.py +++ b/iso15118/secc/secc_settings.py @@ -39,6 +39,7 @@ def load_envs(self, env_path: Optional[str] = None) -> None: if not env_path: env_path = os.getcwd() + "/.env" env.read_env(path=env_path) # read .env file, if it exists + self.iface = env.str("NETWORK_INTERFACE", default="eth0") # validate the NIC selected validate_nic(self.iface) From 7ddef8ca7e89b9f499fd40f6c243c4bf8d105932 Mon Sep 17 00:00:00 2001 From: tropxy Date: Wed, 22 Dec 2021 18:42:36 +0000 Subject: [PATCH 3/6] Refactored the collection of shared settings and log configuration. Also provided a way to select the location of the PKI certificate structure directory --- README.md | 23 +++++++++--------- iso15118/evcc/comm_session_handler.py | 4 +-- iso15118/evcc/controller/simulator.py | 6 +---- iso15118/evcc/start_evcc.py | 4 +-- iso15118/evcc/states/iso15118_20_states.py | 6 +---- iso15118/evcc/states/iso15118_2_states.py | 6 +---- iso15118/evcc/states/sap_states.py | 6 +---- iso15118/evcc/transport/tcp_client.py | 6 +---- iso15118/evcc/transport/udp_client.py | 6 +---- iso15118/secc/comm_session_handler.py | 4 +-- iso15118/secc/controller/simulator.py | 6 +---- iso15118/secc/start_secc.py | 3 ++- iso15118/secc/states/iso15118_20_states.py | 4 +-- iso15118/secc/states/iso15118_2_states.py | 4 +-- iso15118/secc/states/sap_states.py | 6 +---- iso15118/secc/states/secc_state.py | 7 +----- iso15118/secc/transport/tcp_server.py | 6 +---- iso15118/secc/transport/udp_server.py | 9 +------ iso15118/shared/comm_session.py | 6 +---- iso15118/shared/exi_codec.py | 17 ++++++------- iso15118/shared/exificient_wrapper.py | 12 +++------ iso15118/shared/logging.conf | 1 - iso15118/shared/logging/__init__.py | 12 +++++++++ iso15118/shared/logging/logging.conf | 27 +++++++++++++++++++++ iso15118/shared/messages/enums.py | 9 ++----- iso15118/shared/messages/iso15118_2/body.py | 6 +---- iso15118/shared/messages/sdp.py | 6 +---- iso15118/shared/messages/v2gtp.py | 19 +++------------ iso15118/shared/network.py | 6 +---- iso15118/shared/security.py | 6 +---- iso15118/shared/settings.py | 23 +++++++++++++----- iso15118/shared/states.py | 6 +---- iso15118/shared/utils.py | 6 +---- template.Dockerfile | 3 +-- 34 files changed, 110 insertions(+), 171 deletions(-) delete mode 120000 iso15118/shared/logging.conf create mode 100644 iso15118/shared/logging/__init__.py create mode 100644 iso15118/shared/logging/logging.conf diff --git a/README.md b/README.md index c33286d4..7eff1fc7 100644 --- a/README.md +++ b/README.md @@ -104,17 +104,18 @@ Finally, the project includes a few configuration variables, whose default values can be modified by setting them as environmental variables. The following table provides a few of the available variables: -| ENV | Default Value | Description | -| ------------------- | ------------- | ------------------------------------------------------------------------------------------------------ | -| NETWORK_INTERFACE | `eth0` | HomePlug Green PHY Network Interface from which the high-level communication (HLC) will be established | -| SECC_CONTROLLER_SIM | `False` | Whether or not to simulate the SECC Controller Interface | -| SECC_ENFORCE_TLS | `False` | Whether or not the SECC will enforce a TLS connection | -| EVCC_CONTROLLER_SIM | `False` | Whether or not to simulate the EVCC Controller Interface | -| EVCC_USE_TLS | `True` | Whether or not the EVCC signals the preference to communicate with a TLS connection | -| EVCC_ENFORCE_TLS | `False` | Whether or not the EVCC will only accept TLS connections | -| REDIS_HOST | `localhost` | Redis Host URL | -| REDIS_PORT | `6379` | Redis Port | -| LOG_LEVEL | `INFO` | Level of the Python log service | +| ENV | Default Value | Description | +| ------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------ | +| NETWORK_INTERFACE | `eth0` | HomePlug Green PHY Network Interface from which the high-level communication (HLC) will be established | +| SECC_CONTROLLER_SIM | `False` | Whether or not to simulate the SECC Controller Interface | +| SECC_ENFORCE_TLS | `False` | Whether or not the SECC will enforce a TLS connection | +| EVCC_CONTROLLER_SIM | `False` | Whether or not to simulate the EVCC Controller Interface | +| EVCC_USE_TLS | `True` | Whether or not the EVCC signals the preference to communicate with a TLS connection | +| EVCC_ENFORCE_TLS | `False` | Whether or not the EVCC will only accept TLS connections | +| PKI_PATH | `/iso15118/shared/pki/` | Path for the location of the PKI where the certificates are located | +| REDIS_HOST | `localhost` | Redis Host URL | +| REDIS_PORT | `6379` | Redis Port | +| LOG_LEVEL | `INFO` | Level of the Python log service | The project includes a few environmental files, in the root directory, for different purposes: diff --git a/iso15118/evcc/comm_session_handler.py b/iso15118/evcc/comm_session_handler.py index c2d3f461..d0823643 100644 --- a/iso15118/evcc/comm_session_handler.py +++ b/iso15118/evcc/comm_session_handler.py @@ -8,7 +8,7 @@ """ import asyncio -import logging.config +import logging from asyncio.streams import StreamReader, StreamWriter from ipaddress import IPv6Address from typing import List, Optional, Tuple, Union @@ -45,10 +45,8 @@ StopNotification, UDPPacketNotification, ) -from iso15118.shared.settings import LOGGER_CONF_PATH from iso15118.shared.utils import cancel_task, wait_till_finished -logging.config.fileConfig(fname=LOGGER_CONF_PATH, disable_existing_loggers=False) logger = logging.getLogger(__name__) SDP_MAX_REQUEST_COUNTER = 50 diff --git a/iso15118/evcc/controller/simulator.py b/iso15118/evcc/controller/simulator.py index d3864033..95c76ece 100644 --- a/iso15118/evcc/controller/simulator.py +++ b/iso15118/evcc/controller/simulator.py @@ -3,11 +3,10 @@ retrieve data from the EV. The DummyEVController overrides all abstract methods from EVControllerInterface. """ -import logging.config +import logging from typing import List, Optional, Tuple from iso15118.evcc.controller.interface import ChargeParamsV2, EVControllerInterface -from iso15118.shared import settings from iso15118.shared.exceptions import InvalidProtocolError, MACAddressNotFound from iso15118.shared.messages.enums import Namespace, Protocol from iso15118.shared.messages.iso15118_2.datatypes import ( @@ -32,9 +31,6 @@ from iso15118.shared.messages.iso15118_20.common_types import RationalNumber from iso15118.shared.network import get_nic_mac_address -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) diff --git a/iso15118/evcc/start_evcc.py b/iso15118/evcc/start_evcc.py index aee4fd78..3f0881b0 100644 --- a/iso15118/evcc/start_evcc.py +++ b/iso15118/evcc/start_evcc.py @@ -1,10 +1,11 @@ import asyncio import logging from typing import Optional +from iso15118.shared.logging import _init_logger from iso15118.evcc.evcc_settings import Config from iso15118.evcc.comm_session_handler import CommunicationSessionHandler -logging.basicConfig(level=logging.DEBUG) +_init_logger() logger = logging.getLogger(__name__) @@ -15,7 +16,6 @@ async def main(env_path: Optional[str] = None): """ # TODO: we need to read the ISO 15118 version and the Security value # from some settings file - try: # get configuration config = Config() diff --git a/iso15118/evcc/states/iso15118_20_states.py b/iso15118/evcc/states/iso15118_20_states.py index 25bb1317..2d74c0c5 100644 --- a/iso15118/evcc/states/iso15118_20_states.py +++ b/iso15118/evcc/states/iso15118_20_states.py @@ -4,13 +4,12 @@ SessionStopRes. """ -import logging.config +import logging import time from typing import Union from iso15118.evcc.comm_session_handler import EVCCCommunicationSession from iso15118.evcc.states.evcc_state import StateEVCC -from iso15118.shared import settings from iso15118.shared.exceptions import PrivateKeyReadError from iso15118.shared.exi_codec import to_exi from iso15118.shared.messages.app_protocol import ( @@ -47,9 +46,6 @@ load_priv_key, ) -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) diff --git a/iso15118/evcc/states/iso15118_2_states.py b/iso15118/evcc/states/iso15118_2_states.py index ff481801..a0aff9bc 100644 --- a/iso15118/evcc/states/iso15118_2_states.py +++ b/iso15118/evcc/states/iso15118_2_states.py @@ -4,14 +4,13 @@ SessionStopRes. """ -import logging.config +import logging from time import time from typing import List, Union from iso15118.evcc import evcc_settings from iso15118.evcc.comm_session_handler import EVCCCommunicationSession from iso15118.evcc.states.evcc_state import StateEVCC -from iso15118.shared import settings from iso15118.shared.exceptions import DecryptionError, PrivateKeyReadError from iso15118.shared.exi_codec import to_exi from iso15118.shared.messages.app_protocol import ( @@ -84,9 +83,6 @@ ) from iso15118.shared.states import Terminate -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) diff --git a/iso15118/evcc/states/sap_states.py b/iso15118/evcc/states/sap_states.py index 12a2ddf1..6fb9f5b9 100644 --- a/iso15118/evcc/states/sap_states.py +++ b/iso15118/evcc/states/sap_states.py @@ -5,7 +5,7 @@ SupportedAppProtocolReq and -Res message pair to mutually agree upon a protocol. """ -import logging.config +import logging import time from typing import Type, Union @@ -14,7 +14,6 @@ from iso15118.evcc.states.evcc_state import StateEVCC from iso15118.evcc.states.iso15118_2_states import SessionSetup as SessionSetupV2 from iso15118.evcc.states.iso15118_20_states import SessionSetup as SessionSetupV20 -from iso15118.shared import settings from iso15118.shared.exceptions import MessageProcessingError from iso15118.shared.messages.app_protocol import ( SupportedAppProtocolReq, @@ -40,9 +39,6 @@ from iso15118.shared.messages.timeouts import Timeouts as TimeoutsShared from iso15118.shared.states import State, Terminate -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) diff --git a/iso15118/evcc/transport/tcp_client.py b/iso15118/evcc/transport/tcp_client.py index b0184226..e46a8fc1 100644 --- a/iso15118/evcc/transport/tcp_client.py +++ b/iso15118/evcc/transport/tcp_client.py @@ -1,14 +1,10 @@ import asyncio -import logging.config +import logging import socket from ipaddress import IPv6Address -from iso15118.shared import settings from iso15118.shared.security import get_ssl_context -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) SLEEP = 10 diff --git a/iso15118/evcc/transport/udp_client.py b/iso15118/evcc/transport/udp_client.py index d8d71be5..ce530afe 100644 --- a/iso15118/evcc/transport/udp_client.py +++ b/iso15118/evcc/transport/udp_client.py @@ -1,11 +1,10 @@ import asyncio -import logging.config +import logging import socket import struct from asyncio import DatagramProtocol, DatagramTransport from typing import Tuple, Optional -from iso15118.shared import settings from iso15118.shared.messages.timeouts import Timeouts from iso15118.shared.messages.v2gtp import V2GTPMessage from iso15118.shared.network import SDP_MULTICAST_GROUP, SDP_SERVER_PORT @@ -14,9 +13,6 @@ UDPPacketNotification, ) -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) diff --git a/iso15118/secc/comm_session_handler.py b/iso15118/secc/comm_session_handler.py index 9e91aeb2..5db7b764 100644 --- a/iso15118/secc/comm_session_handler.py +++ b/iso15118/secc/comm_session_handler.py @@ -11,7 +11,7 @@ """ import asyncio -import logging.config +import logging import socket from asyncio.streams import StreamReader, StreamWriter from typing import Dict, List, Optional, Tuple, Union @@ -45,10 +45,8 @@ TCPClientNotification, UDPPacketNotification, ) -from iso15118.shared.settings import LOGGER_CONF_PATH from iso15118.shared.utils import cancel_task, wait_till_finished -logging.config.fileConfig(fname=LOGGER_CONF_PATH, disable_existing_loggers=False) logger = logging.getLogger(__name__) diff --git a/iso15118/secc/controller/simulator.py b/iso15118/secc/controller/simulator.py index 20a3b703..f148ad17 100644 --- a/iso15118/secc/controller/simulator.py +++ b/iso15118/secc/controller/simulator.py @@ -2,12 +2,11 @@ This module contains the code to retrieve (hardware-related) data from the EVSE (Electric Vehicle Supply Equipment). """ -import logging.config +import logging import time from typing import List, Optional, Union from iso15118.secc.controller.interface import EVSEControllerInterface -from iso15118.shared import settings from iso15118.shared.exceptions import InvalidProtocolError from iso15118.shared.messages.enums import Namespace, Protocol from iso15118.shared.messages.iso15118_2.datatypes import ( @@ -40,9 +39,6 @@ from iso15118.shared.messages.iso15118_20.common_messages import ProviderID from iso15118.shared.messages.iso15118_20.common_types import MeterInfo as MeterInfoV20 -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) diff --git a/iso15118/secc/start_secc.py b/iso15118/secc/start_secc.py index 1b9a55a4..abaaf3f7 100644 --- a/iso15118/secc/start_secc.py +++ b/iso15118/secc/start_secc.py @@ -1,10 +1,11 @@ import asyncio import logging from typing import Optional +from iso15118.shared.logging import _init_logger from iso15118.secc.secc_settings import Config from iso15118.secc.comm_session_handler import CommunicationSessionHandler -logging.basicConfig(level=logging.DEBUG) +_init_logger() logger = logging.getLogger(__name__) diff --git a/iso15118/secc/states/iso15118_20_states.py b/iso15118/secc/states/iso15118_20_states.py index c11ca4ae..3db375ef 100644 --- a/iso15118/secc/states/iso15118_20_states.py +++ b/iso15118/secc/states/iso15118_20_states.py @@ -4,7 +4,7 @@ SessionStopReq. """ -import logging.config +import logging import time from typing import List, Union @@ -40,9 +40,7 @@ ) from iso15118.shared.messages.iso15118_20.timeouts import Timeouts from iso15118.shared.security import get_random_bytes, verify_signature -from iso15118.shared.settings import LOGGER_CONF_PATH -logging.config.fileConfig(fname=LOGGER_CONF_PATH, disable_existing_loggers=False) logger = logging.getLogger(__name__) diff --git a/iso15118/secc/states/iso15118_2_states.py b/iso15118/secc/states/iso15118_2_states.py index 91b5147d..ee3c4b2b 100644 --- a/iso15118/secc/states/iso15118_2_states.py +++ b/iso15118/secc/states/iso15118_2_states.py @@ -4,7 +4,7 @@ SessionStopReq. """ -import logging.config +import logging import time from typing import List, Optional, Type, Union @@ -101,10 +101,8 @@ verify_certs, verify_signature, ) -from iso15118.shared.settings import LOGGER_CONF_PATH from iso15118.shared.states import State, Terminate -logging.config.fileConfig(fname=LOGGER_CONF_PATH, disable_existing_loggers=False) logger = logging.getLogger(__name__) diff --git a/iso15118/secc/states/sap_states.py b/iso15118/secc/states/sap_states.py index b162a66d..2d2fcc23 100644 --- a/iso15118/secc/states/sap_states.py +++ b/iso15118/secc/states/sap_states.py @@ -5,14 +5,13 @@ SupportedAppProtocolReq and -Res message pair to mutually agree upon a protocol. """ -import logging.config +import logging from typing import Type, Union from iso15118.secc.comm_session_handler import SECCCommunicationSession from iso15118.secc.states.iso15118_2_states import SessionSetup as SessionSetupV2 from iso15118.secc.states.iso15118_20_states import SessionSetup as SessionSetupV20 from iso15118.secc.states.secc_state import StateSECC -from iso15118.shared import settings from iso15118.shared.messages.app_protocol import ( ResponseCodeSAP, SupportedAppProtocolReq, @@ -26,9 +25,6 @@ from iso15118.shared.messages.timeouts import Timeouts from iso15118.shared.states import State, Terminate -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) diff --git a/iso15118/secc/states/secc_state.py b/iso15118/secc/states/secc_state.py index 2eba53cb..94fbb06a 100644 --- a/iso15118/secc/states/secc_state.py +++ b/iso15118/secc/states/secc_state.py @@ -3,13 +3,11 @@ which extends the state shared between the EVCC and SECC. """ -import logging.config +import logging from abc import ABC from typing import List, Optional, Type, TypeVar, Union from iso15118.secc.comm_session_handler import SECCCommunicationSession -from iso15118.shared import settings -from iso15118.shared.exceptions import FaultyStateImplementationError from iso15118.shared.messages.app_protocol import ( ResponseCodeSAP, SupportedAppProtocolReq, @@ -42,9 +40,6 @@ from iso15118.shared.notifications import StopNotification from iso15118.shared.states import State, Terminate -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) diff --git a/iso15118/secc/transport/tcp_server.py b/iso15118/secc/transport/tcp_server.py index 8c016cdc..3d16dde0 100644 --- a/iso15118/secc/transport/tcp_server.py +++ b/iso15118/secc/transport/tcp_server.py @@ -1,16 +1,12 @@ import asyncio -import logging.config +import logging import socket from typing import Tuple -from iso15118.shared import settings from iso15118.shared.network import get_link_local_full_addr, get_tcp_port from iso15118.shared.notifications import TCPClientNotification from iso15118.shared.security import get_ssl_context -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) diff --git a/iso15118/secc/transport/udp_server.py b/iso15118/secc/transport/udp_server.py index 8f05b93a..df714d81 100644 --- a/iso15118/secc/transport/udp_server.py +++ b/iso15118/secc/transport/udp_server.py @@ -1,11 +1,10 @@ import asyncio -import logging.config +import logging import socket import struct from asyncio import DatagramTransport from typing import Tuple, Optional -from iso15118.shared import settings from iso15118.shared.messages.v2gtp import V2GTPMessage from iso15118.shared.network import SDP_MULTICAST_GROUP, SDP_SERVER_PORT from iso15118.shared.notifications import ( @@ -14,14 +13,8 @@ ) from iso15118.shared.utils import wait_till_finished -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) -# TODO should be coming from SLAC -IFACE = "en0" - class UDPServer(asyncio.DatagramProtocol): """ diff --git a/iso15118/shared/comm_session.py b/iso15118/shared/comm_session.py index ca0c377e..6e1639c9 100644 --- a/iso15118/shared/comm_session.py +++ b/iso15118/shared/comm_session.py @@ -6,7 +6,7 @@ """ import asyncio -import logging.config +import logging from abc import ABC, abstractmethod from asyncio.streams import StreamReader, StreamWriter from typing import List, Optional, Tuple, Type, Union @@ -14,7 +14,6 @@ from pydantic import ValidationError from typing_extensions import TYPE_CHECKING -from iso15118.shared import settings from iso15118.shared.exceptions import ( EXIDecodingError, FaultyStateImplementationError, @@ -41,9 +40,6 @@ from iso15118.shared.states import Pause, State, Terminate from iso15118.shared.utils import wait_till_finished -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) if TYPE_CHECKING: diff --git a/iso15118/shared/exi_codec.py b/iso15118/shared/exi_codec.py index d8d98fc4..81a26b1c 100644 --- a/iso15118/shared/exi_codec.py +++ b/iso15118/shared/exi_codec.py @@ -1,12 +1,12 @@ import base64 import json -import logging.config +import logging from base64 import b64decode, b64encode -from typing import Optional, Union +from typing import Union from pydantic import ValidationError -from iso15118.shared import settings +from iso15118.shared.settings import MESSAGE_LOG_JSON, MESSAGE_LOG_EXI from iso15118.shared.exceptions import EXIDecodingError, EXIEncodingError from iso15118.shared.exificient_wrapper import ExiCodec from iso15118.shared.messages import BaseModel @@ -35,9 +35,6 @@ ) from iso15118.shared.messages.xmldsig import SignedInfo -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) exi_codec = ExiCodec() @@ -162,7 +159,7 @@ def to_exi(msg_element: BaseModel, protocol_ns: str) -> bytes: {exc}" ) from exc - if settings.MESSAGE_LOG_JSON: + if MESSAGE_LOG_JSON: logger.debug( f"Message to encode: \n{msg_content} " f"\nXSD namespace: {protocol_ns}" ) @@ -178,7 +175,7 @@ def to_exi(msg_element: BaseModel, protocol_ns: str) -> bytes: f"EXIEncodingError for {str(msg_element)}: " f"{exc}" ) from exc - if settings.MESSAGE_LOG_EXI: + if MESSAGE_LOG_EXI: logger.debug(f"EXI-encoded message: \n{exi_stream.hex()}") logger.debug( "EXI-encoded message (Base64):" f"\n{base64.b64encode(exi_stream).hex()}" @@ -204,7 +201,7 @@ def from_exi( Raises: EXIDecodingError """ - if settings.MESSAGE_LOG_EXI: + if MESSAGE_LOG_EXI: logger.debug( f"EXI-encoded message: \n{exi_message.hex()}" f"\n XSD namespace: {namespace}" @@ -220,7 +217,7 @@ def from_exi( except Exception as exc: raise EXIDecodingError(f"EXIDecodingError: {exc}") from exc - if settings.MESSAGE_LOG_JSON: + if MESSAGE_LOG_JSON: logger.debug( f"Decoded message: \n{decoded_dict}" f"\nXSD namespace: {namespace}" ) diff --git a/iso15118/shared/exificient_wrapper.py b/iso15118/shared/exificient_wrapper.py index fe28d987..cdc50ff7 100644 --- a/iso15118/shared/exificient_wrapper.py +++ b/iso15118/shared/exificient_wrapper.py @@ -1,17 +1,12 @@ -import binascii import json -import logging.config -import os +import logging from builtins import Exception from py4j.java_gateway import JavaGateway -from iso15118.shared import settings +from iso15118.shared.settings import JAR_FILE_PATH from iso15118.shared.messages.enums import Protocol -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) @@ -24,9 +19,8 @@ def compare_messages(json_to_encode, decoded_json): class ExiCodec: def __init__(self): logging.getLogger("py4j").setLevel(logging.CRITICAL) - path_to_jar_file = os.path.join(settings.ROOT_DIR, "../shared/EXICodec.jar") self.gateway = JavaGateway.launch_gateway( - classpath=path_to_jar_file, + classpath=JAR_FILE_PATH, die_on_exit=False, javaopts=["--add-opens", "java.base/java.lang=ALL-UNNAMED"], ) diff --git a/iso15118/shared/logging.conf b/iso15118/shared/logging.conf deleted file mode 120000 index 2496b6c0..00000000 --- a/iso15118/shared/logging.conf +++ /dev/null @@ -1 +0,0 @@ -../../logging.conf \ No newline at end of file diff --git a/iso15118/shared/logging/__init__.py b/iso15118/shared/logging/__init__.py new file mode 100644 index 00000000..4b02c515 --- /dev/null +++ b/iso15118/shared/logging/__init__.py @@ -0,0 +1,12 @@ +import os +import logging.config + +LOGGING_DIR = os.path.dirname(os.path.abspath(__file__)) +LOGGER_CONF_PATH = os.path.join(LOGGING_DIR, "logging.conf") + + +def _init_logger(): + logging.config.fileConfig( + fname=LOGGER_CONF_PATH, disable_existing_loggers=False + ) + diff --git a/iso15118/shared/logging/logging.conf b/iso15118/shared/logging/logging.conf new file mode 100644 index 00000000..efbd1a34 --- /dev/null +++ b/iso15118/shared/logging/logging.conf @@ -0,0 +1,27 @@ +[loggers] +keys=root,sampleLogger + +[handlers] +keys=consoleHandler + +[formatters] +keys=sampleFormatter + +[logger_root] +level=DEBUG +handlers=consoleHandler + +[logger_sampleLogger] +level=DEBUG +handlers=consoleHandler +qualname=sampleLogger +propagate=0 + +[handler_consoleHandler] +class=StreamHandler +level=DEBUG +formatter=sampleFormatter +args=(sys.stdout,) + +[formatter_sampleFormatter] +format=%(levelname)s %(asctime)s - %(name)s (%(lineno)d): %(message)s \ No newline at end of file diff --git a/iso15118/shared/messages/enums.py b/iso15118/shared/messages/enums.py index 5ea98e3e..3edb9543 100644 --- a/iso15118/shared/messages/enums.py +++ b/iso15118/shared/messages/enums.py @@ -1,12 +1,7 @@ -import logging.config -from enum import Enum, IntEnum, auto +import logging +from enum import Enum, IntEnum from typing import List, Union -from iso15118.shared import settings - -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) INT_32_MAX = 4294967295 diff --git a/iso15118/shared/messages/iso15118_2/body.py b/iso15118/shared/messages/iso15118_2/body.py index 9840afe0..2c802897 100644 --- a/iso15118/shared/messages/iso15118_2/body.py +++ b/iso15118/shared/messages/iso15118_2/body.py @@ -9,13 +9,12 @@ (or class) that matches the definitions in the XSD schema, including the XSD element names by using the 'alias' attribute. """ -import logging.config +import logging from abc import ABC from typing import List, Optional, Tuple, Type from pydantic import Field, root_validator, validator -from iso15118.shared import settings from iso15118.shared.messages import BaseModel from iso15118.shared.messages.enums import AuthEnum from iso15118.shared.messages.iso15118_2.datatypes import ( @@ -62,9 +61,6 @@ ) from iso15118.shared.validators import one_field_must_be_set -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) diff --git a/iso15118/shared/messages/sdp.py b/iso15118/shared/messages/sdp.py index 5592a062..c22b6346 100644 --- a/iso15118/shared/messages/sdp.py +++ b/iso15118/shared/messages/sdp.py @@ -1,14 +1,10 @@ -import logging.config +import logging from enum import IntEnum from ipaddress import IPv6Address from typing import Union -from iso15118.shared import settings from iso15118.shared.exceptions import InvalidSDPRequestError, InvalidSDPResponseError -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) MIN_TCP_PORT = 49152 diff --git a/iso15118/shared/messages/v2gtp.py b/iso15118/shared/messages/v2gtp.py index 9bff251d..8e6515fd 100644 --- a/iso15118/shared/messages/v2gtp.py +++ b/iso15118/shared/messages/v2gtp.py @@ -1,17 +1,12 @@ -import logging.config -from dataclasses import dataclass -from typing import Optional, Union +import logging +from typing import Union -from iso15118.shared import settings from iso15118.shared.exceptions import ( InvalidPayloadTypeError, InvalidProtocolError, InvalidV2GTPMessageError, ) -from iso15118.shared.messages.app_protocol import ( - SupportedAppProtocolReq, - SupportedAppProtocolRes, -) + from iso15118.shared.messages.enums import ( INT_32_MAX, DINPayloadTypes, @@ -20,15 +15,7 @@ Protocol, V2GTPVersion, ) -from iso15118.shared.messages.iso15118_2.msgdef import V2GMessage as V2GMessageV2 -from iso15118.shared.messages.iso15118_20.common_types import ( - V2GMessage as V2GMessageV20, -) -from iso15118.shared.messages.iso15118_20.common_types import V2GRequest, V2GResponse -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) diff --git a/iso15118/shared/network.py b/iso15118/shared/network.py index 4de213c8..0aca6d8a 100644 --- a/iso15118/shared/network.py +++ b/iso15118/shared/network.py @@ -1,5 +1,5 @@ import asyncio -import logging.config +import logging import socket from ipaddress import IPv6Address from random import randint @@ -7,14 +7,10 @@ import psutil -from iso15118.shared import settings from iso15118.shared.exceptions import (MACAddressNotFound, NoLinkLocalAddressError, InvalidInterfaceError) -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) SDP_MULTICAST_GROUP = "FF02::1" diff --git a/iso15118/shared/security.py b/iso15118/shared/security.py index 864423cf..af204d48 100644 --- a/iso15118/shared/security.py +++ b/iso15118/shared/security.py @@ -1,4 +1,4 @@ -import logging.config +import logging import secrets from datetime import datetime from enum import Enum, auto @@ -27,7 +27,6 @@ load_der_x509_certificate, ) -from iso15118.shared import settings from iso15118.shared.exceptions import ( CertAttributeError, CertChainLengthError, @@ -67,9 +66,6 @@ ) from iso15118.shared.settings import PKI_PATH -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) diff --git a/iso15118/shared/settings.py b/iso15118/shared/settings.py index 293cc17a..e52ebcd9 100644 --- a/iso15118/shared/settings.py +++ b/iso15118/shared/settings.py @@ -1,10 +1,21 @@ import os +import environs ROOT_DIR = os.path.dirname(os.path.abspath(__file__)) -LOGGER_CONF_PATH = os.path.join(ROOT_DIR, "logging.conf") -PKI_PATH = os.path.join(ROOT_DIR, "pki/") +JAR_FILE_PATH = ROOT_DIR + "/EXICodec.jar" + +WORK_DIR = os.getcwd() +SHARED_CWD = WORK_DIR + "/iso15118/shared/" + +ENV_PATH = WORK_DIR + "/.env" + +env = environs.Env(eager=False) +env.read_env(path=ENV_PATH) # read .env file, if it exists + +PKI_PATH = env.str("PKI_PATH", default=SHARED_CWD + "pki/") +MESSAGE_LOG_JSON = env.bool("MESSAGE_LOG_JSON", default=True) +MESSAGE_LOG_EXI = env.bool("MESSAGE_LOG_EXI", default=False) + +env.seal() # raise all errors at once, if any + -# Log the messages that are EXI encoded and decoded as JSON for debugging -# purposes. Must be True or False -MESSAGE_LOG_JSON = True -MESSAGE_LOG_EXI = False diff --git a/iso15118/shared/states.py b/iso15118/shared/states.py index 820315b0..c2789402 100644 --- a/iso15118/shared/states.py +++ b/iso15118/shared/states.py @@ -1,10 +1,9 @@ -import logging.config +import logging from abc import ABC, abstractmethod from typing import TYPE_CHECKING, Optional, Type, Union from pydantic import ValidationError -from iso15118.shared import settings from iso15118.shared.exceptions import ( EXIEncodingError, InvalidPayloadTypeError, @@ -31,9 +30,6 @@ from iso15118.shared.messages.v2gtp import V2GTPMessage from iso15118.shared.messages.xmldsig import Signature -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) if TYPE_CHECKING: diff --git a/iso15118/shared/utils.py b/iso15118/shared/utils.py index 93a86cc8..67baec47 100644 --- a/iso15118/shared/utils.py +++ b/iso15118/shared/utils.py @@ -5,16 +5,12 @@ import asyncio import json -import logging.config +import logging import os from contextlib import suppress from typing import Any, Awaitable, List -from iso15118.shared import settings -logging.config.fileConfig( - fname=settings.LOGGER_CONF_PATH, disable_existing_loggers=False -) logger = logging.getLogger(__name__) diff --git a/template.Dockerfile b/template.Dockerfile index 08c65935..d74094d8 100644 --- a/template.Dockerfile +++ b/template.Dockerfile @@ -33,7 +33,6 @@ RUN poetry update && poetry install --no-interaction --no-ansi # Copy the project to the system COPY iso15118/ iso15118/ -COPY logging.conf iso15118/shared/ # Run the tests and linting #RUN poetry run pytest -vv --cov-config .coveragerc --cov-report term-missing --durations=3 --cov=. @@ -65,7 +64,7 @@ RUN /venv/bin/pip install dist/*.whl # RUN cd /venv/lib/python3.10/site-packages/iso15118/shared/pki && ./create_certs.sh -v iso-2 # This is not the ideal way to provide the certificate chain to the container, but for now it works -COPY --from=build /usr/src/app/iso15118/shared/pki/ /venv/lib/python3.10/site-packages/iso15118/shared/pki/ +COPY --from=build /usr/src/app/iso15118/shared/pki/ /usr/src/app/iso15118/shared/pki/ # This will run the entrypoint script defined in the pyproject.toml From 282ccfd416bb57bc829b53fcecef868c5c3f4503 Mon Sep 17 00:00:00 2001 From: tropxy Date: Thu, 23 Dec 2021 14:23:39 +0000 Subject: [PATCH 4/6] Added new line at the end --- iso15118/evcc/evcc_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iso15118/evcc/evcc_settings.py b/iso15118/evcc/evcc_settings.py index bd86f09b..dc9a7b9e 100644 --- a/iso15118/evcc/evcc_settings.py +++ b/iso15118/evcc/evcc_settings.py @@ -91,4 +91,4 @@ def load_envs(self, env_path: Optional[str] = None) -> None: RESUME_SELECTED_AUTH_OPTION = None RESUME_SESSION_ID = None -RESUME_REQUESTED_ENERGY_MODE = None \ No newline at end of file +RESUME_REQUESTED_ENERGY_MODE = None From 4f64bc201ac07f47c847b3dddec7b885391f3735 Mon Sep 17 00:00:00 2001 From: tropxy Date: Tue, 28 Dec 2021 13:09:32 +0000 Subject: [PATCH 5/6] improved README --- README.md | 24 ++++++++++++------------ iso15118/shared/logging/logging.conf | 2 +- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/README.md b/README.md index 7eff1fc7..919835aa 100644 --- a/README.md +++ b/README.md @@ -104,18 +104,18 @@ Finally, the project includes a few configuration variables, whose default values can be modified by setting them as environmental variables. The following table provides a few of the available variables: -| ENV | Default Value | Description | -| ------------------- | ---------------------------- | ------------------------------------------------------------------------------------------------------ | -| NETWORK_INTERFACE | `eth0` | HomePlug Green PHY Network Interface from which the high-level communication (HLC) will be established | -| SECC_CONTROLLER_SIM | `False` | Whether or not to simulate the SECC Controller Interface | -| SECC_ENFORCE_TLS | `False` | Whether or not the SECC will enforce a TLS connection | -| EVCC_CONTROLLER_SIM | `False` | Whether or not to simulate the EVCC Controller Interface | -| EVCC_USE_TLS | `True` | Whether or not the EVCC signals the preference to communicate with a TLS connection | -| EVCC_ENFORCE_TLS | `False` | Whether or not the EVCC will only accept TLS connections | -| PKI_PATH | `/iso15118/shared/pki/` | Path for the location of the PKI where the certificates are located | -| REDIS_HOST | `localhost` | Redis Host URL | -| REDIS_PORT | `6379` | Redis Port | -| LOG_LEVEL | `INFO` | Level of the Python log service | +| ENV | Default Value | Description | +| ------------------- | ---------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| NETWORK_INTERFACE | `eth0` | HomePlug Green PHY Network Interface from which the high-level communication (HLC) will be established | +| SECC_CONTROLLER_SIM | `False` | Whether or not to simulate the SECC Controller Interface | +| SECC_ENFORCE_TLS | `False` | Whether or not the SECC will enforce a TLS connection | +| EVCC_CONTROLLER_SIM | `False` | Whether or not to simulate the EVCC Controller Interface | +| EVCC_USE_TLS | `True` | Whether or not the EVCC signals the preference to communicate with a TLS connection | +| EVCC_ENFORCE_TLS | `False` | Whether or not the EVCC will only accept TLS connections | +| PKI_PATH | `/iso15118/shared/pki/` | Path for the location of the PKI where the certificates are located. By default, the system will look for the PKI directory under the current working directory | +| REDIS_HOST | `localhost` | Redis Host URL | +| REDIS_PORT | `6379` | Redis Port | +| LOG_LEVEL | `INFO` | Level of the Python log service | The project includes a few environmental files, in the root directory, for different purposes: diff --git a/iso15118/shared/logging/logging.conf b/iso15118/shared/logging/logging.conf index efbd1a34..81f094d4 100644 --- a/iso15118/shared/logging/logging.conf +++ b/iso15118/shared/logging/logging.conf @@ -24,4 +24,4 @@ formatter=sampleFormatter args=(sys.stdout,) [formatter_sampleFormatter] -format=%(levelname)s %(asctime)s - %(name)s (%(lineno)d): %(message)s \ No newline at end of file +format=%(levelname)s %(asctime)s - %(name)s (%(lineno)d): %(message)s From 19d84c0bd74360670ab4ae9bb6eef08d234adaa6 Mon Sep 17 00:00:00 2001 From: tropxy Date: Tue, 28 Dec 2021 13:11:41 +0000 Subject: [PATCH 6/6] minor editorial change --- iso15118/secc/secc_settings.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/iso15118/secc/secc_settings.py b/iso15118/secc/secc_settings.py index c69076da..70c3f119 100644 --- a/iso15118/secc/secc_settings.py +++ b/iso15118/secc/secc_settings.py @@ -54,11 +54,10 @@ def load_envs(self, env_path: Optional[str] = None) -> None: if env.bool("SECC_CONTROLLER_SIM", default=False): self.evse_controller = SimEVSEController - # Indicates whether or not the SECC should always enforce a - # TLS-secured communication - # session. If True, the SECC will only fire up a TCP server with an - # SSL session context - # and ignore the Security byte value from the SDP request. + # Indicates whether or not the SECC should always enforce a TLS-secured + # communication session. If True, the SECC will only fire up a TCP server + # with an SSL session context and ignore the Security byte value from the + # SDP request. self.enforce_tls = env.bool("SECC_ENFORCE_TLS", default=False) # Indicates whether or not the ChargeService (energy transfer) is free.