Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

improved secc and evcc configuration handling and updated readme #6

Merged
merged 6 commits into from
Dec 28, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion .env.dev.docker
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ REDIS_HOST=redis


# SECC Settings
SECC_CONTROLLER_SIM=True
FREE_CHARGING_SERVICE=False
FREE_CERT_INSTALL_SERVICE=True
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
Expand Down
4 changes: 3 additions & 1 deletion .env.dev.local
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ REDIS_HOST=localhost


# SECC Settings
SECC_CONTROLLER_SIM=True
FREE_CHARGING_SERVICE=False
FREE_CERT_INSTALL_SERVICE=True
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
Expand Down
11 changes: 4 additions & 7 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down
36 changes: 18 additions & 18 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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`.
Expand All @@ -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
Expand All @@ -98,31 +96,33 @@ 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 |


The project includes a few environmental files, in the root directory, for
| 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 | `<CWD>/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:

* `.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.
Expand Down
72 changes: 36 additions & 36 deletions iso15118/evcc/comm_session_handler.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,22 +8,15 @@
"""

import asyncio
import logging.config
import logging
from asyncio.streams import StreamReader, StreamWriter
from ipaddress import IPv6Address
from typing import List, Optional, Tuple, Union

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
Expand Down Expand Up @@ -52,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
Expand All @@ -71,6 +62,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
Expand All @@ -83,8 +75,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
Expand All @@ -108,7 +102,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]:
"""
Expand All @@ -124,7 +118,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
Expand Down Expand Up @@ -188,9 +182,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:
Expand All @@ -201,13 +207,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)
Expand All @@ -224,15 +231,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),
Expand All @@ -255,7 +254,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(
Expand Down Expand Up @@ -311,7 +310,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}"
)
Expand Down Expand Up @@ -347,7 +346,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:
Expand All @@ -358,7 +357,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:
Expand Down Expand Up @@ -419,13 +419,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
Expand Down
3 changes: 2 additions & 1 deletion iso15118/evcc/controller/interface.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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
Expand Down
11 changes: 3 additions & 8 deletions iso15118/evcc/controller/simulator.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand All @@ -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__)


Expand All @@ -46,13 +42,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(
Expand Down
Loading