diff --git a/Makefile b/Makefile
index 1d3253ec..c7d94f8e 100644
--- a/Makefile
+++ b/Makefile
@@ -79,7 +79,7 @@ run-secc:
mypy:
- mypy --config-file ../mypy.ini iso15118 tests
+ mypy --config-file mypy.ini iso15118 tests
reformat:
isort iso15118 tests && black --line-length=88 iso15118 tests
@@ -88,7 +88,7 @@ black:
black --check --diff --line-length=88 iso15118 tests
flake8:
- flake8 --config ../.flake8 iso15118 tests
+ flake8 --config .flake8 iso15118 tests
code-quality: reformat mypy black flake8
diff --git a/iso15118/evcc/controller/interface.py b/iso15118/evcc/controller/interface.py
index 48e58217..a7121d44 100644
--- a/iso15118/evcc/controller/interface.py
+++ b/iso15118/evcc/controller/interface.py
@@ -4,23 +4,22 @@
from abc import ABC, abstractmethod
from dataclasses import dataclass
-from typing import List, Optional, Tuple, Union
+from typing import List, Optional, Tuple
from iso15118.shared.messages.enums import Protocol
from iso15118.shared.messages.iso15118_2.datatypes import (
ACEVChargeParameter,
ChargeProgress,
+ ChargingProfile,
DCEVChargeParameter,
- EnergyTransferMode,
EnergyTransferModeEnum,
- ProfileEntry,
- SAScheduleTuple,
+ SAScheduleTupleEntry,
)
from iso15118.shared.messages.iso15118_20.ac import (
ACChargeParameterDiscoveryReqParams,
BPTACChargeParameterDiscoveryReqParams,
)
-from iso15118.shared.messages.iso15118_20.common_messages import EMAID
+from iso15118.shared.messages.iso15118_20.common_messages import EMAIDList
@dataclass
@@ -127,8 +126,8 @@ def is_cert_install_needed(self) -> bool:
@abstractmethod
def process_sa_schedules(
- self, sa_schedules: List[SAScheduleTuple]
- ) -> Tuple[ChargeProgress, int, List[ProfileEntry]]:
+ self, sa_schedules: List[SAScheduleTupleEntry]
+ ) -> Tuple[ChargeProgress, int, ChargingProfile]:
"""
Processes the SAScheduleList provided with the ChargeParameterDiscoveryRes
to decide which of the offered schedules to choose and whether or not to
@@ -184,7 +183,7 @@ def store_contract_cert_and_priv_key(self, contract_cert: bytes, priv_key: bytes
raise NotImplementedError
@abstractmethod
- def get_prioritised_emaids(self) -> Optional[List[EMAID]]:
+ def get_prioritised_emaids(self) -> Optional[EMAIDList]:
"""
Indicates the list of EMAIDs (E-Mobility Account IDs) referencing contract
certificates that shall be installed into the EV. The EMAIDs are given in
diff --git a/iso15118/evcc/controller/simulator.py b/iso15118/evcc/controller/simulator.py
index 1a6cbdea..5bf54345 100644
--- a/iso15118/evcc/controller/simulator.py
+++ b/iso15118/evcc/controller/simulator.py
@@ -12,22 +12,22 @@
from iso15118.shared.messages.iso15118_2.datatypes import (
ACEVChargeParameter,
ChargeProgress,
+ ChargingProfile,
EnergyTransferModeEnum,
- ProfileEntry,
ProfileEntryDetails,
PVEAmount,
PVEVMaxCurrent,
PVEVMaxVoltage,
PVEVMinCurrent,
PVPMax,
- SAScheduleTuple,
+ SAScheduleTupleEntry,
UnitSymbol,
)
from iso15118.shared.messages.iso15118_20.ac import (
ACChargeParameterDiscoveryReqParams,
BPTACChargeParameterDiscoveryReqParams,
)
-from iso15118.shared.messages.iso15118_20.common_messages import EMAID
+from iso15118.shared.messages.iso15118_20.common_messages import EMAIDList
from iso15118.shared.messages.iso15118_20.common_types import RationalNumber
from iso15118.shared.network import get_nic_mac_address
@@ -114,23 +114,22 @@ def is_cert_install_needed(self) -> bool:
return True
def process_sa_schedules(
- self, sa_schedules: List[SAScheduleTuple]
- ) -> Tuple[ChargeProgress, int, List[ProfileEntry]]:
+ self, sa_schedules: List[SAScheduleTupleEntry]
+ ) -> Tuple[ChargeProgress, int, ChargingProfile]:
"""Overrides EVControllerInterface.process_sa_schedules()."""
schedule = sa_schedules.pop()
- profile_entry_list: List[ProfileEntry] = []
+ profile_entry_list: List[ProfileEntryDetails] = []
# The charging schedule coming from the SECC is called 'schedule', the
# pendant coming from the EVCC (after having processed the offered
# schedule(s)) is called 'profile'. Therefore, we use the prefix
# 'schedule_' for data from the SECC, and 'profile_' for data from the EVCC.
- for p_max_schedule_entry in schedule.tuple.p_max_schedule:
- schedule_entry_details = p_max_schedule_entry.entry_details
+ for schedule_entry_details in schedule.p_max_schedule.entry_details:
profile_entry_details = ProfileEntryDetails(
start=schedule_entry_details.time_interval.start,
max_power=schedule_entry_details.p_max,
)
- profile_entry_list.append(ProfileEntry(entry_details=profile_entry_details))
+ profile_entry_list.append(profile_entry_details)
# The last PMaxSchedule element has an optional 'duration' field. if
# 'duration' is present, then there'll be no more PMaxSchedule element
@@ -145,17 +144,15 @@ def process_sa_schedules(
),
max_power=zero_power,
)
- profile_entry_list.append(
- ProfileEntry(entry_details=last_profile_entry_details)
- )
+ profile_entry_list.append(last_profile_entry_details)
# TODO If a SalesTariff is present and digitally signed (and TLS is used),
# verify each sales tariff with the mobility operator sub 2 certificate
return (
ChargeProgress.START,
- schedule.tuple.sa_schedule_tuple_id,
- profile_entry_list,
+ schedule.sa_schedule_tuple_id,
+ ChargingProfile(profile_entries=profile_entry_list),
)
def continue_charging(self) -> bool:
@@ -175,5 +172,5 @@ def store_contract_cert_and_priv_key(self, contract_cert: bytes, priv_key: bytes
# TODO Need to store the contract cert and private key
pass
- def get_prioritised_emaids(self) -> Optional[List[EMAID]]:
+ def get_prioritised_emaids(self) -> Optional[EMAIDList]:
return None
diff --git a/iso15118/evcc/evcc_settings.py b/iso15118/evcc/evcc_settings.py
index 94c5cbee..25578214 100644
--- a/iso15118/evcc/evcc_settings.py
+++ b/iso15118/evcc/evcc_settings.py
@@ -50,8 +50,8 @@ def load_envs(self, env_path: Optional[str] = None) -> None:
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
+ # 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
@@ -70,13 +70,15 @@ def load_envs(self, env_path: Optional[str] = None) -> None:
"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
+ # 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.
+ # 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)
diff --git a/iso15118/evcc/states/evcc_state.py b/iso15118/evcc/states/evcc_state.py
index a777879b..9b3b20d5 100644
--- a/iso15118/evcc/states/evcc_state.py
+++ b/iso15118/evcc/states/evcc_state.py
@@ -6,22 +6,15 @@
from typing import Optional, Type, TypeVar, Union
from iso15118.evcc.comm_session_handler import EVCCCommunicationSession
-from iso15118.shared.exceptions import FaultyStateImplementationError
from iso15118.shared.messages.app_protocol import (
SupportedAppProtocolReq,
SupportedAppProtocolRes,
)
from iso15118.shared.messages.iso15118_2.body import BodyBase, Response
-from iso15118.shared.messages.iso15118_2.body import (
- SessionSetupReq as SessionSetupReqV2,
-)
from iso15118.shared.messages.iso15118_2.body import (
SessionSetupRes as SessionSetupResV2,
)
from iso15118.shared.messages.iso15118_2.msgdef import V2GMessage as V2GMessageV2
-from iso15118.shared.messages.iso15118_20.common_messages import (
- SessionSetupReq as SessionSetupReqV20,
-)
from iso15118.shared.messages.iso15118_20.common_messages import (
SessionSetupRes as SessionSetupResV20,
)
diff --git a/iso15118/evcc/states/iso15118_20_states.py b/iso15118/evcc/states/iso15118_20_states.py
index 2d74c0c5..111444e8 100644
--- a/iso15118/evcc/states/iso15118_20_states.py
+++ b/iso15118/evcc/states/iso15118_20_states.py
@@ -165,8 +165,8 @@ def process_message(
)
)
],
- max_contract_cert_chains=self.comm_session.config.max_contract_certs,
- prioritized_emaids=self.comm_session.ev_controller.get_prioritised_emaids(),
+ max_contract_cert_chains=self.comm_session.config.max_contract_certs, # noqa: E501
+ prioritized_emaids=self.comm_session.ev_controller.get_prioritised_emaids(), # noqa: E501
)
self.create_next_message(
@@ -178,7 +178,7 @@ def process_message(
except PrivateKeyReadError as exc:
self.stop_state_machine(
"Can't read private key necessary to sign "
- f"CertificateInstallationReq: {exc.error}"
+ f"CertificateInstallationReq: {exc}"
)
return
@@ -213,7 +213,7 @@ def process_message(
except PrivateKeyReadError as exc:
self.stop_state_machine(
"Can't read private key necessary to sign "
- f"AuthorizationReq: {exc.error}"
+ f"AuthorizationReq: {exc}"
)
return
else:
diff --git a/iso15118/evcc/states/iso15118_2_states.py b/iso15118/evcc/states/iso15118_2_states.py
index a0aff9bc..8617a147 100644
--- a/iso15118/evcc/states/iso15118_2_states.py
+++ b/iso15118/evcc/states/iso15118_2_states.py
@@ -47,13 +47,13 @@
)
from iso15118.shared.messages.iso15118_2.datatypes import (
ACEVSEStatus,
- AuthOptions,
ChargeProgress,
+ ChargeService,
ChargingSession,
- EnergyTransferMode,
+ EnergyTransferModeEnum,
EVSENotification,
EVSEProcessing,
- RootCertificateID,
+ RootCertificateIDList,
SelectedService,
SelectedServiceList,
ServiceCategory,
@@ -158,17 +158,16 @@ def process_message(
self.stop_state_machine("ChargeService not offered")
return
- self.select_auth_mode(service_discovery_res.auth_option_list)
+ self.select_auth_mode(service_discovery_res.auth_option_list.auth_options)
self.select_services(service_discovery_res)
self.select_energy_transfer_mode()
- offered_energy_modes = (
- service_discovery_res.charge_service.supported_energy_transfer_mode
- )
- if (
- EnergyTransferMode(value=self.comm_session.selected_energy_mode)
- not in offered_energy_modes
- ):
+ charge_service: ChargeService = service_discovery_res.charge_service
+ offered_energy_modes: List[
+ EnergyTransferModeEnum
+ ] = charge_service.supported_energy_transfer_mode.energy_modes
+
+ if self.comm_session.selected_energy_mode not in offered_energy_modes:
self.stop_state_machine(
f"Offered energy transfer modes "
f"{offered_energy_modes} not compatible with "
@@ -223,7 +222,7 @@ def select_energy_transfer_mode(self):
self.comm_session.ev_controller.get_energy_transfer_mode()
)
- def select_auth_mode(self, auth_option_list: List[AuthOptions]):
+ def select_auth_mode(self, auth_option_list: List[AuthEnum]):
"""
Check if an authorization mode (aka payment option in ISO 15118-2) was
saved from a previously paused communication session and reuse for
@@ -240,15 +239,12 @@ def select_auth_mode(self, auth_option_list: List[AuthOptions]):
)
evcc_settings.RESUME_SELECTED_AUTH_OPTION = None
else:
- # Chose Plug & Charge (pnc) or External Identification Means (eim)
+ # Choose Plug & Charge (pnc) or External Identification Means (eim)
# as the selected authorization option. The car manufacturer might
# have a mechanism to determine a user-defined or default
# authorization option. This implementation favors pnc, but
# feel free to change if need be.
- if (
- AuthOptions(value=AuthEnum.PNC_V2) in auth_option_list
- and self.comm_session.is_tls
- ):
+ if AuthEnum.PNC_V2 in auth_option_list and self.comm_session.is_tls:
self.comm_session.selected_auth_option = AuthEnum.PNC_V2
else:
self.comm_session.selected_auth_option = AuthEnum.EIM_V2
@@ -272,24 +268,22 @@ def select_services(self, service_discovery_res: ServiceDiscoveryRes):
offered_services: str = ""
- for service in service_discovery_res.service_list:
+ for service in service_discovery_res.service_list.services:
offered_services += (
"\nService ID: "
- f"{service.service_details.service_id}, "
+ f"{service.service_id}, "
"Service name: "
- f"{service.service_details.service_name}"
+ f"{service.service_name}"
)
if (
- service.service_details.service_category == ServiceCategory.CERTIFICATE
+ service.service_category == ServiceCategory.CERTIFICATE
and self.comm_session.selected_auth_option
and self.comm_session.selected_auth_option == AuthEnum.PNC_V2
and self.comm_session.ev_controller.is_cert_install_needed()
):
# Make sure to send a ServiceDetailReq for the
# Certificate service
- self.comm_session.service_details_to_request.append(
- service.service_details.service_id
- )
+ self.comm_session.service_details_to_request.append(service.service_id)
# TODO We should actually first ask for the ServiceDetails and
# based on the service parameter list make absolutely sure
@@ -394,13 +388,13 @@ def process_message(
cert_install_req = CertificateInstallationReq(
id="id1",
oem_provisioning_cert=load_cert(CertPath.OEM_LEAF_DER),
- root_cert_ids=[
- RootCertificateID(
- x509_issuer_serial=X509IssuerSerial(
+ list_of_root_cert_ids=RootCertificateIDList(
+ x509_issuer_serials=[
+ X509IssuerSerial(
x509_issuer_name=issuer, x509_serial_number=serial
)
- )
- ],
+ ]
+ ),
)
try:
@@ -424,7 +418,7 @@ def process_message(
except PrivateKeyReadError as exc:
self.stop_state_machine(
"Can't read private key necessary to sign "
- f"CertificateInstallationReq: {exc.error}"
+ f"CertificateInstallationReq: {exc}"
)
return
else:
@@ -510,10 +504,7 @@ def process_message(
),
],
leaf_cert=cert_install_res.cps_cert_chain.certificate,
- sub_ca_certs=[
- cert.certificate
- for cert in cert_install_res.cps_cert_chain.sub_certificates
- ],
+ sub_ca_certs=cert_install_res.cps_cert_chain.sub_certificates.certificates,
root_ca_cert_path=CertPath.V2G_ROOT_DER,
):
self.stop_state_machine(
@@ -540,12 +531,12 @@ def process_message(
self.stop_state_machine(
"Can't read private key needed to decrypt "
"encrypted private key contained in "
- f"CertificateInstallationRes. {exc.error}"
+ f"CertificateInstallationRes. {exc}"
)
return
payment_details_req = PaymentDetailsReq(
- emaid=EMAID(value=get_cert_cn(load_cert(CertPath.CONTRACT_LEAF_DER))),
+ emaid=get_cert_cn(load_cert(CertPath.CONTRACT_LEAF_DER)),
cert_chain=load_cert_chain(
protocol=Protocol.ISO_15118_2,
leaf_path=CertPath.CONTRACT_LEAF_DER,
@@ -610,7 +601,7 @@ def process_message(
)
except PrivateKeyReadError as exc:
self.stop_state_machine(
- f"Can't read private key to sign AuthorizationReq: " f"{exc.error}"
+ f"Can't read private key to sign AuthorizationReq: " f"{exc}"
)
return
@@ -720,7 +711,7 @@ def process_message(
schedule_id,
charging_profile,
) = self.comm_session.ev_controller.process_sa_schedules(
- charge_params_res.sa_schedule_list
+ charge_params_res.sa_schedule_list.values
)
power_delivery_req = PowerDeliveryReq(
@@ -899,7 +890,7 @@ def process_message(
except PrivateKeyReadError as exc:
self.stop_state_machine(
"Can't read private key necessary to sign "
- f"MeteringReceiptReq: {exc.error}"
+ f"MeteringReceiptReq: {exc}"
)
return
elif ac_evse_status.evse_notification == EVSENotification.RE_NEGOTIATION:
diff --git a/iso15118/evcc/states/sap_states.py b/iso15118/evcc/states/sap_states.py
index 1da74647..154c8d8b 100644
--- a/iso15118/evcc/states/sap_states.py
+++ b/iso15118/evcc/states/sap_states.py
@@ -108,8 +108,8 @@ def process_message(
logger.error(
"EVCC sent an invalid protocol namespace in "
f"its previous SupportedAppProtocolReq: "
- f"{protocol.protocol_ns}. Allowed "
- f"namespaces are: {self.comm_session.config.supported_protocols}"
+ f"{protocol.protocol_ns}. Allowed namespaces are:"
+ f" {self.comm_session.config.supported_protocols}"
)
raise MessageProcessingError("SupportedAppProtocolReq")
break
diff --git a/iso15118/secc/comm_session_handler.py b/iso15118/secc/comm_session_handler.py
index 6029d541..5e35adf0 100644
--- a/iso15118/secc/comm_session_handler.py
+++ b/iso15118/secc/comm_session_handler.py
@@ -36,7 +36,10 @@
CertificateChain as CertificateChainV2,
)
from iso15118.shared.messages.iso15118_2.datatypes import MeterInfo as MeterInfoV2
-from iso15118.shared.messages.iso15118_2.datatypes import SAScheduleTuple, Service
+from iso15118.shared.messages.iso15118_2.datatypes import (
+ SAScheduleTupleEntry,
+ ServiceDetails,
+)
from iso15118.shared.messages.sdp import SDPRequest, Security, create_sdp_response
from iso15118.shared.messages.timeouts import Timeouts
from iso15118.shared.messages.v2gtp import V2GTPMessage
@@ -75,7 +78,7 @@ def __init__(
# ISO 15118-2 and with AuthorizationSetupRes in ISO 15118-20
self.offered_auth_options: Optional[List[AuthEnum]] = []
# The value-added services offered with ServiceDiscoveryRes
- self.offered_services: List[Service] = []
+ self.offered_services: List[ServiceDetails] = []
# The authorization option (called PaymentOption in ISO 15118-2) the
# EVCC selected with the PaymentServiceSelectionReq
self.selected_auth_option: Optional[AuthEnum] = None
@@ -84,7 +87,7 @@ def __init__(
self.evcc_id: Union[bytes, str, None] = None
# The list of offered charging schedules, sent to the EVCC via the
# ChargeParameterDiscoveryRes message
- self.offered_schedules: List[SAScheduleTuple] = []
+ self.offered_schedules: List[SAScheduleTupleEntry] = []
# Whether or not the SECC received a PowerDeliveryReq with
# ChargeProgress set to 'Start'
self.charge_progress_started: bool = False
diff --git a/iso15118/secc/controller/interface.py b/iso15118/secc/controller/interface.py
index 4380038d..bd4d1967 100644
--- a/iso15118/secc/controller/interface.py
+++ b/iso15118/secc/controller/interface.py
@@ -12,14 +12,13 @@
ACEVSEStatus,
DCEVSEChargeParameter,
DCEVSEStatus,
- EnergyTransferMode,
EnergyTransferModeEnum,
)
from iso15118.shared.messages.iso15118_2.datatypes import MeterInfo as MeterInfoV2
from iso15118.shared.messages.iso15118_2.datatypes import (
PVEVSEPresentCurrent,
PVEVSEPresentVoltage,
- SAScheduleTuple,
+ SAScheduleTupleEntry,
)
from iso15118.shared.messages.iso15118_20.common_messages import ProviderID
from iso15118.shared.messages.iso15118_20.common_types import MeterInfo as MeterInfoV20
@@ -45,17 +44,11 @@ def get_evse_id(self) -> str:
raise NotImplementedError
@abstractmethod
- def get_supported_energy_transfer_modes(
- self, as_enums: bool = False
- ) -> Union[List[EnergyTransferMode], List[EnergyTransferModeEnum]]:
+ def get_supported_energy_transfer_modes(self) -> List[EnergyTransferModeEnum]:
"""
The MQTT interface needs to provide the information on the available energy
transfer modes, which depends on the socket the EV is connected to
- Args:
- as_enums: Whether or not you want a list of EnergyTransferMode elements
- or of the actual enum values (of type EnergyTransferModeEnum)
-
Relevant for:
- ISO 15118-2
"""
@@ -79,7 +72,7 @@ def is_authorised(self) -> bool:
@abstractmethod
def get_sa_schedule_list(
self, max_schedule_entries: Optional[int], departure_time: int = 0
- ) -> Optional[List[SAScheduleTuple]]:
+ ) -> Optional[List[SAScheduleTupleEntry]]:
"""
Requests the charging schedule from a secondary actor (SA) like a
charge point operator, if available. If no backend information is given
@@ -96,7 +89,7 @@ def get_sa_schedule_list(
implies the need to start charging immediately.
Returns:
- A list of SAScheduleTuple entries to influence the EV's charging profile
+ A list of SAScheduleTupleEntry values to influence the EV's charging profile
if the backend/charger can provide the information already, or None if
the calculation is still ongoing.
diff --git a/iso15118/secc/controller/simulator.py b/iso15118/secc/controller/simulator.py
index f148ad17..b01c6510 100644
--- a/iso15118/secc/controller/simulator.py
+++ b/iso15118/secc/controller/simulator.py
@@ -15,7 +15,6 @@
DCEVSEChargeParameter,
DCEVSEStatus,
DCEVSEStatusCode,
- EnergyTransferMode,
EnergyTransferModeEnum,
EVSENotification,
IsolationLevel,
@@ -32,7 +31,6 @@
RelativeTimeInterval,
SalesTariff,
SalesTariffEntry,
- SAScheduleTuple,
SAScheduleTupleEntry,
UnitSymbol,
)
@@ -55,20 +53,10 @@ def get_evse_id(self) -> str:
"""Overrides EVSEControllerInterface.get_evse_id()."""
return "UK123E1234"
- def get_supported_energy_transfer_modes(
- self, as_enums: bool = False
- ) -> Union[List[EnergyTransferMode], List[EnergyTransferModeEnum]]:
+ def get_supported_energy_transfer_modes(self) -> List[EnergyTransferModeEnum]:
"""Overrides EVSEControllerInterface.get_supported_energy_transfer_modes()."""
- ac_single_phase = EnergyTransferMode(
- value=EnergyTransferModeEnum.AC_SINGLE_PHASE_CORE
- )
- ac_three_phase = EnergyTransferMode(
- value=EnergyTransferModeEnum.AC_THREE_PHASE_CORE
- )
-
- if as_enums:
- return [ac_single_phase.value, ac_three_phase.value]
-
+ ac_single_phase = EnergyTransferModeEnum.AC_SINGLE_PHASE_CORE
+ ac_three_phase = EnergyTransferModeEnum.AC_THREE_PHASE_CORE
return [ac_single_phase, ac_three_phase]
def is_authorised(self) -> bool:
@@ -77,17 +65,17 @@ def is_authorised(self) -> bool:
def get_sa_schedule_list(
self, max_schedule_entries: Optional[int], departure_time: int = 0
- ) -> Optional[List[SAScheduleTuple]]:
+ ) -> Optional[List[SAScheduleTupleEntry]]:
"""Overrides EVSEControllerInterface.get_sa_schedule_list()."""
- sa_schedule_list: List[SAScheduleTuple] = []
- p_max_schedule_entries: List[PMaxScheduleEntry] = []
+ sa_schedule_list: List[SAScheduleTupleEntry] = []
# PMaxSchedule
p_max = PVPMax(multiplier=0, value=11000, unit=UnitSymbol.WATT)
entry_details = PMaxScheduleEntryDetails(
p_max=p_max, time_interval=RelativeTimeInterval(start=0, duration=3600)
)
- p_max_schedule_entries.append(PMaxScheduleEntry(entry_details=entry_details))
+ p_max_schedule_entries = [entry_details]
+ p_max_schedule_entry = PMaxScheduleEntry(entry_details=p_max_schedule_entries)
# SalesTariff
sales_tariff_entries: List[SalesTariffEntry] = []
@@ -110,15 +98,15 @@ def get_sa_schedule_list(
# Putting the list of SAScheduleTuple entries together
sa_schedule_tuple_entry = SAScheduleTupleEntry(
sa_schedule_tuple_id=1,
- p_max_schedule=p_max_schedule_entries,
+ p_max_schedule=p_max_schedule_entry,
sales_tariff=sales_tariff,
)
- sa_schedule_tuple = SAScheduleTuple(tuple=sa_schedule_tuple_entry)
+
# TODO We could also implement an optional SalesTariff, but for the sake of
# time we'll do that later (after the basics are implemented).
# When implementing the SalesTariff, we also need to apply a digital
# signature to it.
- sa_schedule_list.append(sa_schedule_tuple)
+ sa_schedule_list.append(sa_schedule_tuple_entry)
# TODO We need to take care of [V2G2-741], which says that the SECC needs to
# resend a previously agreed SAScheduleTuple and the "period of time
diff --git a/iso15118/secc/failed_responses.py b/iso15118/secc/failed_responses.py
index f17f218b..0d6d72b0 100644
--- a/iso15118/secc/failed_responses.py
+++ b/iso15118/secc/failed_responses.py
@@ -62,7 +62,7 @@
from iso15118.shared.messages.iso15118_2.body import (
WeldingDetectionRes as WeldingDetectionResV2,
)
-from iso15118.shared.messages.iso15118_2.datatypes import ACEVSEStatus, AuthOptions
+from iso15118.shared.messages.iso15118_2.datatypes import ACEVSEStatus, AuthOptionList
from iso15118.shared.messages.iso15118_2.datatypes import (
CertificateChain as CertificateChainV2,
)
@@ -72,8 +72,8 @@
DCEVSEStatusCode,
DHPublicKey,
EncryptedPrivateKey,
- EnergyTransferMode,
EnergyTransferModeEnum,
+ EnergyTransferModeList,
EVSENotification,
EVSEProcessing,
IsolationLevel,
@@ -99,7 +99,6 @@
from iso15118.shared.messages.iso15118_20.common_messages import (
AuthorizationSetupReq,
AuthorizationSetupRes,
- Certificate,
)
from iso15118.shared.messages.iso15118_20.common_messages import (
CertificateChain as CertificateChainV20,
@@ -162,6 +161,9 @@
SessionStopRes as SessionStopResV20,
)
from iso15118.shared.messages.iso15118_20.common_messages import SignedInstallationData
+from iso15118.shared.messages.iso15118_20.common_messages import (
+ SubCertificates as SubCertificatesV20,
+)
from iso15118.shared.messages.iso15118_20.common_types import (
MessageHeader as MessageHeaderV20,
)
@@ -189,14 +191,14 @@ def init_failed_responses_iso_v2() -> dict:
),
ServiceDiscoveryReqV2: ServiceDiscoveryResV2(
response_code=ResponseCodeV2.FAILED,
- auth_option_list=[AuthOptions(value=AuthEnum.EIM_V2)],
+ auth_option_list=AuthOptionList(auth_options=[AuthEnum.EIM_V2]),
charge_service=ChargeService(
service_id=ServiceID.CHARGING,
service_category=ServiceCategory.CHARGING,
free_service=False,
- supported_energy_transfer_mode=[
- EnergyTransferMode(value=EnergyTransferModeEnum.DC_CORE)
- ],
+ supported_energy_transfer_mode=EnergyTransferModeList(
+ energy_modes=[EnergyTransferModeEnum.DC_CORE]
+ ),
),
),
ServiceDetailReqV2: ServiceDetailResV2(
@@ -354,7 +356,7 @@ def init_failed_responses_iso_v20() -> dict:
signed_installation_data=SignedInstallationData(
contract_cert_chain=CertificateChainV20(
certificate=bytes(0),
- sub_certificates=[Certificate(certificate=bytes(0))],
+ sub_certificates=SubCertificatesV20(certificates=[bytes(0)]),
),
ecdh_curve=ECDHCurve.x448,
dh_public_key=bytes(0),
diff --git a/iso15118/secc/states/iso15118_20_states.py b/iso15118/secc/states/iso15118_20_states.py
index 3db375ef..b6864427 100644
--- a/iso15118/secc/states/iso15118_20_states.py
+++ b/iso15118/secc/states/iso15118_20_states.py
@@ -180,7 +180,7 @@ def process_message(
auth_options.append(AuthEnum.PNC)
pnc_as_res = PnCAuthSetupResParams(
gen_challenge=get_random_bytes(16),
- supported_providers=self.comm_session.evse_controller.get_supported_providers(),
+ supported_providers=self.comm_session.evse_controller.get_supported_providers(), # noqa: E501
)
# TODO [V2G20-2096], [V2G20-2570]
diff --git a/iso15118/secc/states/iso15118_2_states.py b/iso15118/secc/states/iso15118_2_states.py
index ee3c4b2b..a45a0a9e 100644
--- a/iso15118/secc/states/iso15118_2_states.py
+++ b/iso15118/secc/states/iso15118_2_states.py
@@ -62,8 +62,7 @@
from iso15118.shared.messages.iso15118_2.datatypes import (
ACEVSEChargeParameter,
ACEVSEStatus,
- AuthOptions,
- Certificate,
+ AuthOptionList,
CertificateChain,
ChargeProgress,
ChargeService,
@@ -71,16 +70,19 @@
DCEVSEChargeParameter,
DHPublicKey,
EncryptedPrivateKey,
+ EnergyTransferModeList,
EVSENotification,
EVSEProcessing,
Parameter,
ParameterSet,
- Service,
+ SAScheduleList,
ServiceCategory,
ServiceDetails,
ServiceID,
+ ServiceList,
ServiceName,
ServiceParameterList,
+ SubCertificates,
)
from iso15118.shared.messages.iso15118_2.msgdef import V2GMessage as V2GMessageV2
from iso15118.shared.messages.iso15118_20.common_types import (
@@ -245,36 +247,38 @@ def get_services(self, category_filter: ServiceCategory) -> ServiceDiscoveryRes:
Currently no filter based on service scope is applied since its string
value is not standardized in any way
"""
- # The datatype in ISO 15118-2 is called "PaymentOption", but it's rather
- # about the authorization method than about payment, thus 'auth_options'
- auth_options: List[AuthOptions] = []
+ auth_options: List[AuthEnum] = []
if self.comm_session.selected_auth_option:
# In case the EVCC resumes a paused charging session, the SECC
# must only offer the auth option the EVCC selected previously
if self.comm_session.selected_auth_option == AuthEnum.EIM_V2:
- auth_options.append(AuthOptions(value=AuthEnum.EIM_V2))
- self.comm_session.offered_auth_options.append(AuthEnum.EIM_V2)
+ auth_options.append(AuthEnum.EIM_V2)
else:
- auth_options.append(AuthOptions(value=AuthEnum.PNC_V2))
- self.comm_session.offered_auth_options.append(AuthEnum.PNC_V2)
+ auth_options.append(AuthEnum.PNC_V2)
else:
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)
+ auth_options.append(AuthEnum.EIM_V2)
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)
+ auth_options.append(AuthEnum.PNC_V2)
+
+ self.comm_session.offered_auth_options = auth_options
+
+ energy_modes = (
+ self.comm_session.evse_controller.get_supported_energy_transfer_modes()
+ )
charge_service = ChargeService(
service_id=ServiceID.CHARGING,
service_name=ServiceName.CHARGING,
service_category=ServiceCategory.CHARGING,
free_service=self.comm_session.config.free_charging_service,
- supported_energy_transfer_mode=self.comm_session.evse_controller.get_supported_energy_transfer_modes(),
+ supported_energy_transfer_mode=EnergyTransferModeList(
+ energy_modes=energy_modes
+ ),
)
- service_list: List[Service] = []
+ service_list: List[ServiceDetails] = []
# 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:
@@ -289,7 +293,7 @@ def get_services(self, category_filter: ServiceCategory) -> ServiceDiscoveryRes:
free_service=self.comm_session.config.free_cert_install_service,
)
- service_list.append(Service(service_details=cert_install_service))
+ service_list.append(cert_install_service)
# Add more value-added services (VAS) here if need be
@@ -298,14 +302,13 @@ def get_services(self, category_filter: ServiceCategory) -> ServiceDiscoveryRes:
# an EXI decoding error. The XSD definition does not allow an empty list
# (otherwise it would also say: minOccurs="0"):
#
+ offered_services = None
if len(service_list) > 0:
- offered_services = service_list
- else:
- offered_services = None
+ offered_services = ServiceList(services=service_list)
service_discovery_res = ServiceDiscoveryRes(
response_code=ResponseCode.OK,
- auth_option_list=auth_options,
+ auth_option_list=AuthOptionList(auth_options=auth_options),
charge_service=charge_service,
service_list=offered_services,
)
@@ -472,7 +475,7 @@ def process_message(
charge_service_selected = True
continue
if service.service_id not in [
- offered_service.service_details.service_id
+ offered_service.service_id
for offered_service in self.comm_session.offered_services
]:
self.stop_state_machine(
@@ -593,7 +596,7 @@ def process_message(
except PrivateKeyReadError as exc:
self.stop_state_machine(
"Can't read private key to encrypt for "
- f"CertificateInstallationRes: {exc.error}",
+ f"CertificateInstallationRes: {exc}",
message,
ResponseCode.FAILED,
)
@@ -603,10 +606,12 @@ def process_message(
contract_cert_chain = CertificateChain(
id="id1",
certificate=load_cert(CertPath.CONTRACT_LEAF_DER),
- sub_certificates=[
- Certificate(certificate=load_cert(CertPath.MO_SUB_CA2_DER)),
- Certificate(certificate=load_cert(CertPath.MO_SUB_CA1_DER)),
- ],
+ sub_certificates=SubCertificates(
+ certificates=[
+ load_cert(CertPath.MO_SUB_CA2_DER),
+ load_cert(CertPath.MO_SUB_CA1_DER),
+ ]
+ ),
)
encrypted_priv_key = EncryptedPrivateKey(
id="id2", value=encrypted_priv_key_bytes
@@ -615,16 +620,19 @@ def process_message(
emaid = EMAID(
id="id4", value=get_cert_cn(load_cert(CertPath.CONTRACT_LEAF_DER))
)
+ cps_certificate_chain = CertificateChain(
+ certificate=load_cert(CertPath.CPS_LEAF_DER),
+ sub_certificates=SubCertificates(
+ certificates=[
+ load_cert(CertPath.CPS_SUB_CA2_DER),
+ load_cert(CertPath.CPS_SUB_CA1_DER),
+ ]
+ ),
+ )
cert_install_res = CertificateInstallationRes(
response_code=ResponseCode.OK,
- cps_cert_chain=CertificateChain(
- certificate=load_cert(CertPath.CPS_LEAF_DER),
- sub_certificates=[
- Certificate(certificate=load_cert(CertPath.CPS_SUB_CA2_DER)),
- Certificate(certificate=load_cert(CertPath.CPS_SUB_CA1_DER)),
- ],
- ),
+ cps_cert_chain=cps_certificate_chain,
contract_cert_chain=contract_cert_chain,
encrypted_private_key=encrypted_priv_key,
dh_public_key=dh_public_key,
@@ -632,21 +640,31 @@ def process_message(
)
try:
- signature = create_signature(
- [
- (
- contract_cert_chain.id,
- to_exi(contract_cert_chain, Namespace.ISO_V2_MSG_DEF),
- ),
- (
- encrypted_priv_key.id,
- to_exi(encrypted_priv_key, Namespace.ISO_V2_MSG_DEF),
- ),
- (dh_public_key.id, to_exi(dh_public_key, Namespace.ISO_V2_MSG_DEF)),
- (emaid.id, to_exi(emaid, Namespace.ISO_V2_MSG_DEF)),
- ],
- load_priv_key(KeyPath.CPS_LEAF_PEM, KeyEncoding.PEM),
+ # Elements to sign, containing its id and the exi encoded stream
+ contract_cert_tuple = (
+ contract_cert_chain.id,
+ to_exi(contract_cert_chain, Namespace.ISO_V2_MSG_DEF),
)
+ encrypted_priv_key_tuple = (
+ encrypted_priv_key.id,
+ to_exi(encrypted_priv_key, Namespace.ISO_V2_MSG_DEF),
+ )
+ dh_public_key_tuple = (
+ dh_public_key.id,
+ to_exi(dh_public_key, Namespace.ISO_V2_MSG_DEF),
+ )
+ emaid_tuple = (emaid.id, to_exi(emaid, Namespace.ISO_V2_MSG_DEF))
+
+ elements_to_sign = [
+ contract_cert_tuple,
+ encrypted_priv_key_tuple,
+ dh_public_key_tuple,
+ emaid_tuple,
+ ]
+ # The private key to be used for the signature
+ signature_key = load_priv_key(KeyPath.CPS_LEAF_PEM, KeyEncoding.PEM)
+
+ signature = create_signature(elements_to_sign, signature_key)
self.create_next_message(
PaymentDetails,
@@ -658,7 +676,7 @@ def process_message(
except PrivateKeyReadError as exc:
self.stop_state_machine(
"Can't read private key needed to create signature "
- f"for CertificateInstallationRes: {exc.error}",
+ f"for CertificateInstallationRes: {exc}",
message,
ResponseCode.FAILED,
)
@@ -708,10 +726,7 @@ def process_message(
try:
leaf_cert = payment_details_req.cert_chain.certificate
- sub_ca_certs = [
- sub_ca_cert.certificate
- for sub_ca_cert in payment_details_req.cert_chain.sub_certificates
- ]
+ sub_ca_certs = payment_details_req.cert_chain.sub_certificates.certificates
# TODO There should be an OCPP setting that determines whether
# or not the charging station should verify (is in
# possession of MO or V2G Root certificates) or if it
@@ -796,7 +811,7 @@ class Authorization(StateSECC):
In case of rejected and according to table 112 from ISO 15118-2, the
errors allowed to be used are: FAILED, FAILED_Challenge_Invalid or
FAILED_Certificate_Revoked.
- Please check: https://dev.azure.com/switch-ev/Josev/_backlogs/backlog/Josev%20Team/Stories/?workitem=1049
+ Please check: https://dev.azure.com/switch-ev/Josev/_backlogs/backlog/Josev%20Team/Stories/?workitem=1049 # noqa: E501
"""
@@ -919,9 +934,7 @@ def process_message(
if (
charge_params_req.requested_energy_mode
- not in self.comm_session.evse_controller.get_supported_energy_transfer_modes(
- True
- )
+ not in self.comm_session.evse_controller.get_supported_energy_transfer_modes() # noqa: E501
):
self.stop_state_machine(
f"{charge_params_req.requested_energy_mode} not "
@@ -961,7 +974,7 @@ def process_message(
evse_processing=EVSEProcessing.FINISHED
if sa_schedule_list
else EVSEProcessing.ONGOING,
- sa_schedule_list=sa_schedule_list,
+ sa_schedule_list=SAScheduleList(values=sa_schedule_list),
ac_charge_parameter=ac_evse_charge_params,
dc_charge_parameter=dc_evse_charge_params,
)
@@ -977,24 +990,20 @@ def process_message(
# operator (MO), but for testing purposes you can set it here
# TODO We should probably have a test mode setting
for schedule in sa_schedule_list:
- if schedule.tuple.sales_tariff:
+ if schedule.sales_tariff:
try:
- signature = create_signature(
- [
- (
- schedule.tuple.sales_tariff.id,
- to_exi(
- schedule.tuple.sales_tariff,
- Namespace.ISO_V2_MSG_DEF,
- ),
- )
- ],
- load_priv_key(KeyPath.MO_SUB_CA2_PEM, KeyEncoding.PEM),
+ element_to_sign = (
+ schedule.sales_tariff.id,
+ to_exi(schedule.sales_tariff, Namespace.ISO_V2_MSG_DEF),
+ )
+ signature_key = load_priv_key(
+ KeyPath.MO_SUB_CA2_PEM, KeyEncoding.PEM
)
+ signature = create_signature([element_to_sign], signature_key)
except PrivateKeyReadError as exc:
logger.warning(
"Can't read private key to needed to create "
- f"signature for SalesTariff: {exc.error}"
+ f"signature for SalesTariff: {exc}"
)
# If a SalesTariff isn't signed, that's not the end of the
# world, no reason to stop the charging process here
@@ -1081,7 +1090,7 @@ def process_message(
power_delivery_req: PowerDeliveryReq = msg.body.power_delivery_req
if power_delivery_req.sa_schedule_tuple_id not in [
- schedule.tuple.sa_schedule_tuple_id
+ schedule.sa_schedule_tuple_id
for schedule in self.comm_session.offered_schedules
]:
self.stop_state_machine(
diff --git a/iso15118/secc/states/secc_state.py b/iso15118/secc/states/secc_state.py
index 94fbb06a..5d31e2ea 100644
--- a/iso15118/secc/states/secc_state.py
+++ b/iso15118/secc/states/secc_state.py
@@ -18,18 +18,12 @@
from iso15118.shared.messages.iso15118_2.body import (
SessionSetupReq as SessionSetupReqV2,
)
-from iso15118.shared.messages.iso15118_2.body import (
- SessionSetupRes as SessionSetupResV2,
-)
from iso15118.shared.messages.iso15118_2.body import get_msg_type
from iso15118.shared.messages.iso15118_2.datatypes import ResponseCode as ResponseCodeV2
from iso15118.shared.messages.iso15118_2.msgdef import V2GMessage as V2GMessageV2
from iso15118.shared.messages.iso15118_20.common_messages import (
SessionSetupReq as SessionSetupReqV20,
)
-from iso15118.shared.messages.iso15118_20.common_messages import (
- SessionSetupRes as SessionSetupResV20,
-)
from iso15118.shared.messages.iso15118_20.common_types import (
ResponseCode as ResponseCodeV20,
)
@@ -158,7 +152,7 @@ def check_msg(
or a PaymentDetailsReq, or an AuthorizationReq (according to
the state machine outlined in ISO 15118-2). But when first
entering the state, the EVCC must send a
- PaymentServiceSelectionReq - that's what expect_first is about.
+ PaymentServiceSelectionReq - that's what expect_first is about. # noqa: E501
Set to True by default, as some states expect only one request
message type.
diff --git a/iso15118/shared/EXICodec.bkp.jar b/iso15118/shared/EXICodec.bkp.jar
new file mode 100644
index 00000000..7dc9902e
Binary files /dev/null and b/iso15118/shared/EXICodec.bkp.jar differ
diff --git a/iso15118/shared/EXICodec.jar b/iso15118/shared/EXICodec.jar
index 7dc9902e..d782dc94 100644
Binary files a/iso15118/shared/EXICodec.jar and b/iso15118/shared/EXICodec.jar differ
diff --git a/iso15118/shared/comm_session.py b/iso15118/shared/comm_session.py
index 6e1639c9..4e8a829a 100644
--- a/iso15118/shared/comm_session.py
+++ b/iso15118/shared/comm_session.py
@@ -34,7 +34,6 @@
from iso15118.shared.messages.iso15118_20.common_types import (
V2GMessage as V2GMessageV20,
)
-from iso15118.shared.messages.sdp import Security
from iso15118.shared.messages.v2gtp import V2GTPMessage
from iso15118.shared.notifications import StopNotification
from iso15118.shared.states import Pause, State, Terminate
@@ -179,8 +178,7 @@ def process_message(self, message: bytes):
try:
decoded_message = from_exi(v2gtp_msg.payload, self.get_exi_ns())
except EXIDecodingError as exc:
- logger.error(f"{exc.error}")
- self.current_state.next_state = Terminate
+ logger.exception(f"{exc}")
raise exc
# Shouldn't happen, but just to be sure (otherwise mypy would complain)
@@ -201,7 +199,7 @@ def process_message(self, message: bytes):
)
raise exc
except FaultyStateImplementationError as exc:
- logger.exception(f"{exc.__class__.__name__}: {exc.error}")
+ logger.exception(f"{exc.__class__.__name__}: {exc}")
raise exc
except ValidationError as exc:
logger.exception(f"{exc.__class__.__name__}: {exc.raw_errors}")
@@ -313,7 +311,7 @@ async def start(self, timeout: float):
def save_session_info(self):
raise NotImplementedError
- async def stop(self):
+ async def stop(self, reason: str):
"""
Closes the TCP connection after 5 seconds and terminates or pauses the
data link for this V2GCommunicationSession object after 2 seconds to
@@ -333,18 +331,18 @@ async def stop(self):
self.save_session_info()
terminate_or_pause = "Pause"
else:
- terminate_or_pause = "Terminat"
+ terminate_or_pause = "Terminate"
logger.debug(
- f"{terminate_or_pause}ing the data link in 2 seconds and "
- "closing the TCP connection in 5 seconds. "
- f"Reason: {self.stop_reason.reason}"
+ f"The data link will {terminate_or_pause} in 2 seconds and "
+ "the TCP connection will close in 5 seconds. "
+ f"Reason: {reason}"
)
await asyncio.sleep(2)
# TODO Signal data link layer to either terminate or pause the data
# link connection
- logger.debug(f"{terminate_or_pause}ed the data link")
+ logger.debug(f"{terminate_or_pause}d the data link")
await asyncio.sleep(3)
self.writer.close()
await self.writer.wait_closed()
@@ -389,11 +387,12 @@ async def rcv_loop(self, timeout: float):
message = await asyncio.wait_for(self.reader.read(7000), timeout)
if message == b"" and self.reader.at_eof():
- await self.stop()
+ stop_reason: str = "TCP peer closed connection"
+ await self.stop(reason=stop_reason)
self.session_handler_queue.put_nowait(
StopNotification(
False,
- "TCP peer closed connection",
+ stop_reason,
self.writer.get_extra_info("peername"),
)
)
@@ -417,7 +416,7 @@ async def rcv_loop(self, timeout: float):
False, error_msg, self.writer.get_extra_info("peername")
)
- await self.stop()
+ await self.stop(reason=error_msg)
self.session_handler_queue.put_nowait(self.stop_reason)
return
@@ -432,7 +431,7 @@ async def rcv_loop(self, timeout: float):
await self.send(self.current_state.next_v2gtp_msg)
if self.current_state.next_state in (Terminate, Pause):
- await self.stop()
+ await self.stop(reason="")
self.comm_session.session_handler_queue.put_nowait(
self.comm_session.stop_reason
)
@@ -450,28 +449,36 @@ async def rcv_loop(self, timeout: float):
if isinstance(exc, MessageProcessingError):
message_name = exc.message_name
if isinstance(exc, FaultyStateImplementationError):
- additional_info = ": " + exc.error
+ additional_info = f": {exc}"
if isinstance(exc, EXIDecodingError):
- additional_info = ": " + exc.__str__()
+ additional_info = f": {exc}"
+
+ stop_reason: str = (
+ f"{exc.__class__.__name__} occurred while processing message "
+ f"{message_name} in state {str(self.current_state)}"
+ f":{additional_info}"
+ )
self.stop_reason = StopNotification(
False,
- f"{exc.__class__.__name__} occurred while processing message "
- f"{message_name} in state {str(self.current_state)}{additional_info}",
+ stop_reason,
self.writer.get_extra_info("peername"),
)
- await self.stop()
+ await self.stop(stop_reason)
self.session_handler_queue.put_nowait(self.stop_reason)
return
except (AttributeError, ValueError) as exc:
+ stop_reason: str = (
+ f"{exc.__class__.__name__} occurred while processing message in "
+ f"state {str(self.current_state)}: {exc}"
+ )
self.stop_reason = StopNotification(
False,
- f"{exc.__class__.__name__} occurred while processing message in "
- f"state {str(self.current_state)}: {exc}",
+ stop_reason,
self.writer.get_extra_info("peername"),
)
- await self.stop()
+ await self.stop(stop_reason)
self.session_handler_queue.put_nowait(self.stop_reason)
return
diff --git a/iso15118/shared/exceptions.py b/iso15118/shared/exceptions.py
index 05f867fe..69123c8d 100644
--- a/iso15118/shared/exceptions.py
+++ b/iso15118/shared/exceptions.py
@@ -70,10 +70,6 @@ class FaultyStateImplementationError(Exception):
information as to what specifically is wrong.
"""
- def __init__(self, error: str):
- Exception.__init__(self)
- self.error = error
-
class InvalidPayloadTypeError(Exception):
"""Is thrown when trying to instantiate a V2GTPMessage object with a
@@ -92,18 +88,10 @@ class InvalidProtocolError(Exception):
class EXIEncodingError(Exception):
"""Is thrown when trying to serialise anobject into an EXI byte stream"""
- def __init__(self, error: str):
- Exception.__init__(self)
- self.error = error
-
class EXIDecodingError(Exception):
"""Is thrown when trying to EXI decode an incoming byte stream"""
- def __init__(self, error: str):
- Exception.__init__(self)
- self.error = error
-
class InvalidSettingsValueError(Exception):
"""
@@ -214,28 +202,14 @@ def __init__(self, allowed_num_sub_cas: int, num_sub_cas: int):
class EncryptionError(Exception):
"""Is thrown when an error occurs while trying to encrypt a private key"""
- def __init__(self):
- Exception.__init__(self)
-
class DecryptionError(Exception):
"""Is thrown when an error occurs while trying to decrypt a private key"""
- def __init__(self):
- Exception.__init__(self)
-
class KeyTypeError(Exception):
"""Is thrown when loading a private key whose type is not recognised"""
- def __init__(self, error: str):
- Exception.__init__(self)
- self.error = error
-
class PrivateKeyReadError(Exception):
"""Is thrown when an error occurs while trying to load a private key"""
-
- def __init__(self, error: str):
- Exception.__init__(self)
- self.error = error
diff --git a/iso15118/shared/exi_codec.py b/iso15118/shared/exi_codec.py
index ba01cff9..b59a98d6 100644
--- a/iso15118/shared/exi_codec.py
+++ b/iso15118/shared/exi_codec.py
@@ -105,6 +105,18 @@ def object_hook(self, dct) -> dict:
if len(dct[field]) <= 15:
continue
+ if field == "Certificate" and isinstance(dct[field], list):
+ # The types CertificateChain and SubCertificates both have fields
+ # with the name `Certificate`. However, in `CertificateChain`
+ # the field is of the type bytes, whilst in `SubCertificates` is
+ # of the type list[bytes].
+ # This difference needs to be taken into account; so here we look
+ # for the list type, decode its elements and substitute the entry
+ # in the dict with the new list.
+ certificate_list = [b64decode(value) for value in dct[field]]
+ dct[field] = certificate_list
+ continue
+
dct[field] = b64decode(dct[field])
return dct
@@ -130,6 +142,8 @@ def to_exi(msg_element: BaseModel, protocol_ns: str) -> bytes:
str(msg_element) == "CertificateChain"
and protocol_ns == Namespace.ISO_V2_MSG_DEF
):
+ # TODO: If we add `ContractSignatureCertChain` as the return of __str__
+ # for the CertificateChain class, do we still need this if clause?
# In case of CertificateInstallationRes and CertificateUpdateRes,
# str(message) would not be 'ContractSignatureCertChain' but
# 'CertificateChain' (the type of ContractSignatureCertChain)
@@ -137,11 +151,16 @@ def to_exi(msg_element: BaseModel, protocol_ns: str) -> bytes:
elif str(msg_element) == "CertificateChain" and protocol_ns.startswith(
Namespace.ISO_V20_BASE
):
+ # TODO: If we add `CPSCertificateChain` as the return of __str__
+ # for a unique class for V20 or even call it CPSCertificateChain
+ # do we still need this if clause?
# In case of CertificateInstallationRes,
# str(message) would not be 'CPSCertificateChain' but
# 'CertificateChain' (the type of CPSCertificateChain)
message_dict = {"CPSCertificateChain": msg_to_dct}
elif str(msg_element) == "SignedCertificateChain":
+ # TODO: If we add `OEMProvisioningCertificateChain` as the return of __str__
+ # for the SignedCertificateChain class, do we still need this if clause?
# In case of CertificateInstallationReq,
# str(message) would not be 'OEMProvisioningCertificateChain' but
# 'SignedCertificateChain' (the type of OEMProvisioningCertificateChain)
@@ -211,11 +230,18 @@ def from_exi(
)
try:
- decoded_dict = json.loads(
- exi_codec.decode(exi_message, namespace), cls=CustomJSONDecoder
- )
+ exi_decoded = exi_codec.decode(exi_message, namespace)
except Exception as exc:
- raise EXIDecodingError(f"EXIDecodingError: {exc}") from exc
+ raise EXIDecodingError(
+ f"EXIDecodingError ({exc.__class__.__name__}): " f"{exc}"
+ ) from exc
+ try:
+ decoded_dict = json.loads(exi_decoded, cls=CustomJSONDecoder)
+ except json.JSONDecodeError as exc:
+ raise EXIDecodingError(
+ f"JSON decoding error ({exc.__class__.__name__}) while "
+ f"processing decoded EXI: {exc}"
+ ) from exc
if MESSAGE_LOG_JSON:
logger.debug(
@@ -236,7 +262,7 @@ def from_exi(
if namespace == Namespace.ISO_V2_MSG_DEF:
return V2GMessageV2.parse_obj(decoded_dict["V2G_Message"])
- if namespace.startswith("urn:iso:std:iso:15118:-20"):
+ if namespace.startswith(Namespace.ISO_V20_BASE):
# The message name is the first key of the dict
msg_name = next(iter(decoded_dict))
# When parsing the dict, we need to remove the first key, which is
@@ -267,10 +293,13 @@ def from_exi(
# TODO Add support for DIN SPEC 70121
- raise EXIDecodingError(
- "EXI decoding error: can't identify protocol to " "use for decoding"
- )
+ raise EXIDecodingError("Can't identify protocol to use for decoding")
except ValidationError as exc:
+ raise EXIDecodingError(
+ f"Error parsing the decoded EXI into a Pydantic class: {exc}. "
+ f"\n\nDecoded dict: {decoded_dict}"
+ ) from exc
+ except EXIDecodingError as exc:
raise EXIDecodingError(
f"EXI decoding error: {exc}. \n\nDecoded dict: " f"{decoded_dict}"
) from exc
diff --git a/iso15118/shared/exificient_wrapper.py b/iso15118/shared/exificient_wrapper.py
index 7438958c..558c08a9 100644
--- a/iso15118/shared/exificient_wrapper.py
+++ b/iso15118/shared/exificient_wrapper.py
@@ -26,16 +26,16 @@ def __init__(self):
)
self.exi_codec = self.gateway.jvm.com.siemens.ct.exi.main.cmd.EXICodec()
-
+ schemas = self.gateway.jvm.com.siemens.ct.exi.main.cmd.BuiltInSchema
self.protocol_schema_mapping = {
- "": self.gateway.jvm.com.siemens.ct.exi.main.cmd.BuiltInSchema.AppProtocol,
- "urn:iso:15118:2:2013:MsgDef": self.gateway.jvm.com.siemens.ct.exi.main.cmd.BuiltInSchema.ISO15118_2,
- "urn:iso:std:iso:15118:-20:CommonMessages": self.gateway.jvm.com.siemens.ct.exi.main.cmd.BuiltInSchema.ISO15118_20_V2G_CI_CommonMessages,
- "urn:iso:std:iso:15118:-20:AC": self.gateway.jvm.com.siemens.ct.exi.main.cmd.BuiltInSchema.ISO15118_20_V2G_CI_AC,
- "urn:iso:std:iso:15118:-20:DC": self.gateway.jvm.com.siemens.ct.exi.main.cmd.BuiltInSchema.ISO15118_20_V2G_CI_DC,
- "urn:iso:std:iso:15118:-20:WPT": self.gateway.jvm.com.siemens.ct.exi.main.cmd.BuiltInSchema.ISO15118_20_V2G_CI_WPT,
- "urn:iso:std:iso:15118:-20:ACDP": self.gateway.jvm.com.siemens.ct.exi.main.cmd.BuiltInSchema.ISO15118_20_V2G_CI_ACDP,
- "http://www.w3.org/2000/09/xmldsig#": self.gateway.jvm.com.siemens.ct.exi.main.cmd.BuiltInSchema.XSDCore,
+ "": schemas.AppProtocol,
+ "urn:iso:15118:2:2013:MsgDef": schemas.ISO15118_2,
+ "urn:iso:std:iso:15118:-20:CommonMessages": schemas.ISO15118_20_V2G_CI_CommonMessages, # noqa: E501
+ "urn:iso:std:iso:15118:-20:AC": schemas.ISO15118_20_V2G_CI_AC,
+ "urn:iso:std:iso:15118:-20:DC": schemas.ISO15118_20_V2G_CI_DC,
+ "urn:iso:std:iso:15118:-20:WPT": schemas.ISO15118_20_V2G_CI_WPT,
+ "urn:iso:std:iso:15118:-20:ACDP": schemas.ISO15118_20_V2G_CI_ACDP,
+ "http://www.w3.org/2000/09/xmldsig#": schemas.XSDCore,
}
logger.debug(f"EXICodec version: {self.exi_codec.get_version()}")
diff --git a/iso15118/shared/messages/enums.py b/iso15118/shared/messages/enums.py
index f74ae9ef..506bbda5 100644
--- a/iso15118/shared/messages/enums.py
+++ b/iso15118/shared/messages/enums.py
@@ -122,12 +122,12 @@ class Namespace(str, Enum):
ISO_V2_MSG_BODY = "urn:iso:15118:2:2013:MsgBody"
ISO_V2_MSG_DT = "urn:iso:15118:2:2013:MsgDataTypes"
ISO_V20_BASE = "urn:iso:std:iso:15118:-20"
- ISO_V20_COMMON_MSG = "urn:iso:std:iso:15118:-20:CommonMessages"
- ISO_V20_COMMON_TYPES = "urn:iso:std:iso:15118:-20:CommonTypes"
- ISO_V20_AC = "urn:iso:std:iso:15118:-20:AC"
- ISO_V20_DC = "urn:iso:std:iso:15118:-20:DC"
- ISO_V20_WPT = "urn:iso:std:iso:15118:-20:WPT"
- ISO_V20_ACDP = "urn:iso:std:iso:15118:-20:ACDP"
+ ISO_V20_COMMON_MSG = ISO_V20_BASE + ":CommonMessages"
+ ISO_V20_COMMON_TYPES = ISO_V20_BASE + ":CommonTypes"
+ ISO_V20_AC = ISO_V20_BASE + ":AC"
+ ISO_V20_DC = ISO_V20_BASE + ":DC"
+ ISO_V20_WPT = ISO_V20_BASE + ":WPT"
+ ISO_V20_ACDP = ISO_V20_BASE + ":ACDP"
XML_DSIG = "http://www.w3.org/2000/09/xmldsig#"
SAP = "urn:iso:15118:2:2010:AppProtocol"
diff --git a/iso15118/shared/messages/iso15118_2/body.py b/iso15118/shared/messages/iso15118_2/body.py
index 2c802897..43d77371 100644
--- a/iso15118/shared/messages/iso15118_2/body.py
+++ b/iso15118/shared/messages/iso15118_2/body.py
@@ -11,7 +11,7 @@
"""
import logging
from abc import ABC
-from typing import List, Optional, Tuple, Type
+from typing import Optional, Tuple, Type
from pydantic import Field, root_validator, validator
@@ -22,10 +22,11 @@
ACEVChargeParameter,
ACEVSEChargeParameter,
ACEVSEStatus,
- AuthOptions,
+ AuthOptionList,
CertificateChain,
ChargeProgress,
ChargeService,
+ ChargingProfile,
ChargingSession,
DCEVChargeParameter,
DCEVPowerDeliveryParameter,
@@ -37,7 +38,6 @@
EnergyTransferModeEnum,
EVSEProcessing,
MeterInfo,
- ProfileEntry,
PVEVMaxCurrentLimit,
PVEVMaxPowerLimit,
PVEVMaxVoltageLimit,
@@ -52,12 +52,13 @@
PVRemainingTimeToBulkSOC,
PVRemainingTimeToFullSOC,
ResponseCode,
- RootCertificateID,
- SAScheduleTuple,
+ RootCertificateIDList,
+ SAScheduleList,
SelectedServiceList,
- Service,
ServiceCategory,
+ ServiceList,
ServiceParameterList,
+ eMAID,
)
from iso15118.shared.validators import one_field_must_be_set
@@ -144,8 +145,8 @@ class CertificateInstallationReq(BodyBase):
oem_provisioning_cert: bytes = Field(
..., max_length=800, alias="OEMProvisioningCert"
)
- root_cert_ids: List[RootCertificateID] = Field(
- ..., max_items=20, alias="ListOfRootCertificateIDs"
+ list_of_root_cert_ids: RootCertificateIDList = Field(
+ ..., alias="ListOfRootCertificateIDs"
)
@@ -173,7 +174,7 @@ class CertificateUpdateReq(BodyBase):
..., alias="ContractSignatureCertChain"
)
emaid: EMAID = Field(..., alias="eMAID")
- list_of_root_cert_ids: List[RootCertificateID] = Field(
+ list_of_root_cert_ids: RootCertificateIDList = Field(
..., alias="ListOfRootCertificateIDs"
)
@@ -274,9 +275,7 @@ class ChargeParameterDiscoveryRes(Response):
"""See section 8.4.3.8.3 in ISO 15118-2"""
evse_processing: EVSEProcessing = Field(..., alias="EVSEProcessing")
- sa_schedule_list: List[SAScheduleTuple] = Field(
- None, max_items=3, alias="SAScheduleList"
- )
+ sa_schedule_list: SAScheduleList = Field(None, alias="SAScheduleList")
ac_charge_parameter: ACEVSEChargeParameter = Field(
None, alias="AC_EVSEChargeParameter"
)
@@ -416,7 +415,8 @@ def check_sessionid_is_hexbinary(cls, value):
# pylint: disable=no-self-argument
# pylint: disable=no-self-use
try:
- test = int(value, 16)
+ # convert value to int, assuming base 16
+ int(value, 16)
return value
except ValueError as exc:
raise ValueError(
@@ -456,7 +456,7 @@ class MeteringReceiptRes(Response):
class PaymentDetailsReq(BodyBase):
"""See section 8.4.3.6.2 in ISO 15118-2"""
- emaid: EMAID = Field(..., alias="eMAID")
+ emaid: eMAID = Field(..., alias="eMAID")
cert_chain: CertificateChain = Field(..., alias="ContractSignatureCertChain")
@@ -492,9 +492,7 @@ class PowerDeliveryReq(BodyBase):
charge_progress: ChargeProgress = Field(..., alias="ChargeProgress")
# XSD type unsignedByte with value range [1..255]
sa_schedule_tuple_id: int = Field(..., ge=1, le=255, alias="SAScheduleTupleID")
- charging_profile: List[ProfileEntry] = Field(
- None, max_items=24, alias="ChargingProfile"
- )
+ charging_profile: ChargingProfile = Field(None, alias="ChargingProfile")
dc_ev_power_delivery_parameter: DCEVPowerDeliveryParameter = Field(
None, alias="DC_EVPowerDeliveryParameter"
)
@@ -570,11 +568,9 @@ class ServiceDiscoveryReq(BodyBase):
class ServiceDiscoveryRes(Response):
"""See section 8.4.3.3.3 in ISO 15118-2"""
- auth_option_list: List[AuthOptions] = Field(
- ..., min_items=1, max_items=2, alias="PaymentOptionList"
- )
+ auth_option_list: AuthOptionList = Field(..., alias="PaymentOptionList")
charge_service: ChargeService = Field(..., alias="ChargeService")
- service_list: List[Service] = Field(None, max_items=8, alias="ServiceList")
+ service_list: ServiceList = Field(None, alias="ServiceList")
class SessionSetupReq(BodyBase):
@@ -595,7 +591,8 @@ def check_sessionid_is_hexbinary(cls, value):
# pylint: disable=no-self-argument
# pylint: disable=no-self-use
try:
- test = int(value, 16)
+ # convert value to int, assuming base 16
+ int(value, 16)
return value
except ValueError as exc:
raise ValueError(
diff --git a/iso15118/shared/messages/iso15118_2/datatypes.py b/iso15118/shared/messages/iso15118_2/datatypes.py
index 82ba8161..f0e3ead8 100644
--- a/iso15118/shared/messages/iso15118_2/datatypes.py
+++ b/iso15118/shared/messages/iso15118_2/datatypes.py
@@ -14,7 +14,7 @@
from enum import Enum, IntEnum
from typing import List, Literal
-from pydantic import Field, root_validator, validator
+from pydantic import Field, conbytes, constr, root_validator, validator
from iso15118.shared.messages import BaseModel
from iso15118.shared.messages.enums import (
@@ -28,6 +28,13 @@
from iso15118.shared.messages.xmldsig import X509IssuerSerial
from iso15118.shared.validators import one_field_must_be_set
+# https://pydantic-docs.helpmanual.io/usage/types/#constrained-types
+# constrained types
+# Check Annex C.6 or the certificateType in V2G_CI_MsgDataTypes.xsd
+Certificate = conbytes(max_length=800)
+# Check Annex C.6 or the eMAIDType in V2G_CI_MsgDataTypes.xsd
+eMAID = constr(min_length=14, max_length=15)
+
class UnitSymbol(str, Enum):
"""
@@ -404,20 +411,24 @@ class ACEVSEChargeParameter(BaseModel):
evse_max_current: PVEVSEMaxCurrent = Field(..., alias="EVSEMaxCurrent")
-class Certificate(BaseModel):
- """See sections 8.5.2.5 and 8.5.2.26 in ISO 15118-2"""
+class SubCertificates(BaseModel):
+ """See sections 8.5.2.5 and 8.5.2.26 in ISO 15118-2
+
+ According to the schemas, SubCertificates can contain up to 4 certificates.
+ However, according to requirement [V2G2-656]:
+ `The number of Certificates in the SubCertificates shall not exceed 2`
+ So, we set it here to 2, the max number of certificates allowed.
+ """
- certificate: bytes = Field(..., max_length=800, alias="Certificate")
+ certificates: List[Certificate] = Field(..., max_items=2, alias="Certificate")
class CertificateChain(BaseModel):
"""See section 8.5.2.5 in ISO 15118-2"""
id: str = Field(None, alias="Id")
- # Note that the type here must be bytes and not Certificate, otherwise we
- # end up with a json structure that does not match the XSD schema
- certificate: bytes = Field(..., max_length=800, alias="Certificate")
- sub_certificates: List[Certificate] = Field(None, alias="SubCertificates")
+ certificate: Certificate = Field(..., alias="Certificate")
+ sub_certificates: SubCertificates = Field(None, alias="SubCertificates")
def __str__(self):
return type(self).__name__
@@ -442,10 +453,12 @@ class EnergyTransferModeEnum(str, Enum):
DC_UNIQUE = "DC_unique"
-class EnergyTransferMode(BaseModel):
+class EnergyTransferModeList(BaseModel):
"""See section 8.5.2.4 in ISO 15118-2"""
- value: EnergyTransferModeEnum = Field(..., alias="EnergyTransferMode")
+ energy_modes: List[EnergyTransferModeEnum] = Field(
+ ..., max_items=6, alias="EnergyTransferMode"
+ )
class ServiceID(IntEnum):
@@ -493,8 +506,8 @@ class ServiceDetails(BaseModel):
class ChargeService(ServiceDetails):
"""See section 8.5.2.3 in ISO 15118-2"""
- supported_energy_transfer_mode: List[EnergyTransferMode] = Field(
- ..., max_items=6, alias="SupportedEnergyTransferMode"
+ supported_energy_transfer_mode: EnergyTransferModeList = Field(
+ ..., alias="SupportedEnergyTransferMode"
)
@@ -509,10 +522,12 @@ class ProfileEntryDetails(BaseModel):
)
-class ProfileEntry(BaseModel):
+class ChargingProfile(BaseModel):
"""See section 8.5.2.10 in ISO 15118-2"""
- entry_details: ProfileEntryDetails = Field(..., alias="ProfileEntry")
+ profile_entries: List[ProfileEntryDetails] = Field(
+ ..., max_items=24, alias="ProfileEntry"
+ )
class ChargingSession(str, Enum):
@@ -682,7 +697,16 @@ class DCEVSEChargeParameter(BaseModel):
class DHPublicKey(BaseModel):
- """See section 8.5.2.29 in ISO 15118-2"""
+ """See section 8.5.2.29 in ISO 15118-2
+
+ 'Id' is actually an XML attribute, but JSON (our serialisation method)
+ doesn't have attributes. The EXI codec has to en-/decode accordingly.
+ id: str = Field(..., alias="Id")
+ The XSD doesn't explicitly state a Value element for
+ DiffieHellmanPublickeyType but its base XSD type named
+ dHpublickeyType has an XSD element . That's why
+ we add this 'value' field
+ """
id: str = Field(..., alias="Id")
value: bytes = Field(..., max_length=65, alias="value")
@@ -709,10 +733,12 @@ class FaultCode(str, Enum):
UNKNOWN_ERROR = "UnknownError"
-class RootCertificateID(BaseModel):
+class RootCertificateIDList(BaseModel):
"""See section 8.5.2.27 in ISO 15118-2"""
- x509_issuer_serial: X509IssuerSerial = Field(..., alias="RootCertificateID")
+ x509_issuer_serials: List[X509IssuerSerial] = Field(
+ ..., max_items=20, alias="RootCertificateID"
+ )
class MeterInfo(BaseModel):
@@ -798,7 +824,7 @@ class ParameterSet(BaseModel):
parameters: List[Parameter] = Field(..., max_items=16, alias="Parameter")
-class AuthOptions(BaseModel):
+class AuthOptionList(BaseModel):
"""
See section 8.5.2.9 in ISO 15118-2
@@ -806,7 +832,9 @@ class AuthOptions(BaseModel):
about the authorization method than about payment, thus the name AuthOption
"""
- value: AuthEnum = Field(..., alias="PaymentOption")
+ auth_options: List[AuthEnum] = Field(
+ ..., min_items=1, max_items=2, alias="PaymentOption"
+ )
class RelativeTimeInterval(BaseModel):
@@ -826,7 +854,9 @@ class PMaxScheduleEntryDetails(BaseModel):
class PMaxScheduleEntry(BaseModel):
"""See section 8.5.2.14 in ISO 15118-2"""
- entry_details: PMaxScheduleEntryDetails = Field(..., alias="PMaxScheduleEntry")
+ entry_details: List[PMaxScheduleEntryDetails] = Field(
+ ..., max_items=1024, alias="PMaxScheduleEntry"
+ )
class ResponseCode(str, Enum):
@@ -881,10 +911,10 @@ class SelectedServiceList(BaseModel):
)
-class Service(BaseModel):
+class ServiceList(BaseModel):
"""See section 8.5.2.2 in ISO 15118-2"""
- service_details: ServiceDetails = Field(..., alias="Service")
+ services: List[ServiceDetails] = Field(..., max_items=8, alias="Service")
class ServiceParameterList(BaseModel):
@@ -995,7 +1025,7 @@ def sales_tariff_id_value_range(cls, value):
def __str__(self):
# The XSD conform element name
- return "SalesTariff"
+ return type(self).__name__
class SAScheduleTupleEntry(BaseModel):
@@ -1003,23 +1033,29 @@ class SAScheduleTupleEntry(BaseModel):
# XSD type unsignedByte with value range [1..255]
sa_schedule_tuple_id: int = Field(..., ge=1, le=255, alias="SAScheduleTupleID")
- p_max_schedule: List[PMaxScheduleEntry] = Field(
- ..., max_items=1024, alias="PMaxSchedule"
- )
+ p_max_schedule: PMaxScheduleEntry = Field(..., alias="PMaxSchedule")
sales_tariff: SalesTariff = Field(None, alias="SalesTariff")
-class SAScheduleTuple(BaseModel):
- """See section 8.5.2.13 in ISO 15118-2"""
-
- tuple: SAScheduleTupleEntry = Field(..., alias="SAScheduleTuple")
+class SAScheduleList(BaseModel):
+ values: List[SAScheduleTupleEntry] = Field(
+ ..., max_items=3, alias="SAScheduleTuple"
+ )
class EMAID(BaseModel):
- # 'Id' is actually an XML attribute, but JSON (our serialisation method)
- # doesn't have attributes. The EXI codec has to en-/decode accordingly.
+ """
+ This is the complex datatype defined in the XML schemas as EMAIDType, containing
+ an id attribute; not to be confused with the simple type, that EMAID is
+ derived from, called eMAIDType, which is of string type, with a min length of
+ 14 and a max length of 15 characters.
+
+ 'Id' is actually an XML attribute, but JSON (our serialisation method)
+ doesn't have attributes. The EXI codec has to en-/decode accordingly.
+ """
+
id: str = Field(None, alias="Id")
- value: str = Field(..., min_length=14, max_length=15, alias="value")
+ value: eMAID = Field(..., alias="value")
def __str__(self):
# The XSD conform element name
diff --git a/iso15118/shared/messages/iso15118_2/header.py b/iso15118/shared/messages/iso15118_2/header.py
index 01afda7c..86029298 100644
--- a/iso15118/shared/messages/iso15118_2/header.py
+++ b/iso15118/shared/messages/iso15118_2/header.py
@@ -39,7 +39,8 @@ def check_sessionid_is_hexbinary(cls, value):
# pylint: disable=no-self-argument
# pylint: disable=no-self-use
try:
- test = int(value, 16)
+ # convert value to int, assuming base 16
+ int(value, 16)
return value
except ValueError as exc:
raise ValueError(
diff --git a/iso15118/shared/messages/iso15118_20/common_messages.py b/iso15118/shared/messages/iso15118_20/common_messages.py
index 168038ba..7a61b7b6 100644
--- a/iso15118/shared/messages/iso15118_20/common_messages.py
+++ b/iso15118/shared/messages/iso15118_20/common_messages.py
@@ -20,7 +20,9 @@
from iso15118.shared.messages.enums import AuthEnum
from iso15118.shared.messages.iso15118_20.common_types import (
UINT_32_MAX,
+ Certificate,
EVSEStatus,
+ Identifier,
MeterInfo,
Processing,
RationalNumber,
@@ -42,16 +44,16 @@ class ECDHCurve(str, Enum):
x448 = "X448"
-class EMAID(BaseModel):
+class EMAIDList(BaseModel):
"""See Annex C.1 in ISO 15118-20"""
- emaid: str = Field(..., max_length=255, alias="EMAID")
+ emaids: List[Identifier] = Field(..., max_items=8, alias="EMAID")
-class Certificate(BaseModel):
- """A DER encoded X.509 certificate"""
+class SubCertificates(BaseModel):
+ """See Annex C.1 or V2G_CI_CommonTypes.xsd in ISO 15118-20"""
- certificate: bytes = Field(..., max_length=800, alias="Certificate")
+ certificates: List[Certificate] = Field(..., max_items=3, alias="Certificate")
class CertificateChain(BaseModel):
@@ -59,8 +61,8 @@ class CertificateChain(BaseModel):
# Note that the type here must be bytes and not Certificate, otherwise we
# end up with a json structure that does not match the XSD schema
- certificate: bytes = Field(..., max_length=800, alias="Certificate")
- sub_certificates: List[Certificate] = Field(None, alias="SubCertificates")
+ certificate: Certificate = Field(..., alias="Certificate")
+ sub_certificates: SubCertificates = Field(None, alias="SubCertificates")
class SignedCertificateChain(BaseModel):
@@ -71,8 +73,8 @@ class SignedCertificateChain(BaseModel):
id: str = Field(..., max_length=255, alias="Id")
# Note that the type here must be bytes and not Certificate, otherwise we
# end up with a json structure that does not match the XSD schema
- certificate: bytes = Field(..., max_length=800, alias="Certificate")
- sub_certificates: List[Certificate] = Field(None, alias="SubCertificates")
+ certificate: Certificate = Field(..., alias="Certificate")
+ sub_certificates: SubCertificates = Field(None, alias="SubCertificates")
def __str__(self):
return type(self).__name__
@@ -83,8 +85,8 @@ class ContractCertificateChain(BaseModel):
# Note that the type here must be bytes and not Certificate, otherwise we
# end up with a json structure that does not match the XSD schema
- certificate: bytes = Field(..., max_length=800, alias="Certificate")
- sub_certificates: List[Certificate] = Field(..., alias="SubCertificates")
+ certificate: Certificate = Field(..., alias="Certificate")
+ sub_certificates: SubCertificates = Field(..., alias="SubCertificates")
class SessionSetupReq(V2GRequest):
@@ -957,7 +959,8 @@ def check_sessionid_is_hexbinary(cls, value):
# pylint: disable=no-self-argument
# pylint: disable=no-self-use
try:
- test = int(value, 16)
+ # convert value to int, assuming base 16
+ int(value, 16)
return value
except ValueError as exc:
raise ValueError(
@@ -1004,16 +1007,14 @@ class CertificateInstallationReq(V2GRequest):
oem_prov_cert_chain: SignedCertificateChain = Field(
..., alias="OEMProvisioningCertificateChain"
)
- list_of_root_cert_ids: List[RootCertificateID] = Field(
- ..., max_items=20, alias="ListOfRootCertificateIDs"
+ list_of_root_cert_ids: RootCertificateID = Field(
+ ..., alias="ListOfRootCertificateIDs"
)
# XSD type unsignedShort (16 bit integer) with value range [0..65535]
max_contract_cert_chains: int = Field(
..., ge=0, le=65535, alias="MaximumContractCertificateChains"
)
- prioritized_emaids: List[EMAID] = Field(
- None, max_items=8, alias="PrioritizedEMAIDs"
- )
+ prioritized_emaids: EMAIDList = Field(None, alias="PrioritizedEMAIDs")
class SignedInstallationData(BaseModel):
diff --git a/iso15118/shared/messages/iso15118_20/common_types.py b/iso15118/shared/messages/iso15118_20/common_types.py
index 9f84520c..509ac7f3 100644
--- a/iso15118/shared/messages/iso15118_20/common_types.py
+++ b/iso15118/shared/messages/iso15118_20/common_types.py
@@ -14,12 +14,18 @@
from enum import Enum
from typing import List
-from pydantic import Field, validator
+from pydantic import Field, conbytes, constr, validator
from iso15118.shared.messages import BaseModel
from iso15118.shared.messages.enums import UINT_32_MAX
from iso15118.shared.messages.xmldsig import Signature, X509IssuerSerial
+# https://pydantic-docs.helpmanual.io/usage/types/#constrained-types
+# constrained types
+# Check Annex C.1 or V2G_CI_CommonTypes.xsd
+Certificate = conbytes(max_length=1600)
+Identifier = constr(max_length=255)
+
class MessageHeader(BaseModel):
"""See section 8.3.3 in ISO 15118-20"""
@@ -41,7 +47,8 @@ def check_sessionid_is_hexbinary(cls, value):
# pylint: disable=no-self-argument
# pylint: disable=no-self-use
try:
- test = int(value, 16)
+ # convert value to int, assuming base 16
+ int(value, 16)
return value
except ValueError as exc:
raise ValueError(
@@ -319,4 +326,6 @@ class Processing(str, Enum):
class RootCertificateID(BaseModel):
"""See section 8.3.5.3.27 in ISO 15118-20"""
- x509_issuer_serial: X509IssuerSerial = Field(..., alias="RootCertificateID")
+ x509_issuer_serial: List[X509IssuerSerial] = Field(
+ ..., max_items=20, alias="RootCertificateID"
+ )
diff --git a/iso15118/shared/messages/sdp.py b/iso15118/shared/messages/sdp.py
index c22b6346..84eff1e4 100644
--- a/iso15118/shared/messages/sdp.py
+++ b/iso15118/shared/messages/sdp.py
@@ -150,9 +150,9 @@ def __init__(
"""
TODO: Docstrings
- # TODO: We may want to use here the related package or something like pydantic
- # which adds some better validations (but also depends if it makes sense given
- # the criteria of having a small image)
+ TODO: We may want to use here the related package or something like pydantic
+ which adds some better validations (but also depends if it makes sense
+ given the criteria of having a small image)
Also raise Exceptions
"""
@@ -222,13 +222,12 @@ def __len__(self):
return 20
def __repr__(self):
+ ip_address: str = IPv6Address(int.from_bytes(self.ip_address, "big")).compressed
return (
- "["
- + f"IP address: {IPv6Address(int.from_bytes(self.ip_address, 'big')).compressed}"
+ f"[ IP address: {ip_address}"
f", Port: {str(self.port)} "
f", Security: {self.security.name} "
- f", Transport: {self.transport_protocol.name} "
- "]"
+ f", Transport: {self.transport_protocol.name} ]"
)
diff --git a/iso15118/shared/messages/xmldsig.py b/iso15118/shared/messages/xmldsig.py
index 43d257cd..c182a87c 100644
--- a/iso15118/shared/messages/xmldsig.py
+++ b/iso15118/shared/messages/xmldsig.py
@@ -1,32 +1,86 @@
+"""
+DataTypes for the construction of the XML signature syntax
+Please check:
+Section 7.9.2.4.2 XML Signature mechanism from ISO 15118-2
+https://en.wikipedia.org/wiki/XML_Signature
+https://www.w3.org/TR/xmldsig-core1/
+"""
+
from typing import List
-from pydantic import Field
+from pydantic import Field, HttpUrl
from iso15118.shared.messages import BaseModel
-class TransformDetails(BaseModel):
- algorithm: str = Field(..., alias="Algorithm")
+class Transform(BaseModel):
+ algorithm: HttpUrl = Field(..., alias="Algorithm")
-class Transform(BaseModel):
- details: TransformDetails = Field(..., alias="Transform")
+class Transforms(BaseModel):
+ """
+ According to requirement [V2G2-767], the maximum number of transforms
+ is limited to one (1), i.e. just one single Transform algorithm can be
+ indicated.
+ """
+
+ transform: List[Transform] = Field(..., max_items=1, alias="Transform")
class DigestMethod(BaseModel):
- algorithm: str = Field(..., alias="Algorithm")
+ algorithm: HttpUrl = Field(..., alias="Algorithm")
class SignatureMethod(BaseModel):
- algorithm: str = Field(..., alias="Algorithm")
+ algorithm: HttpUrl = Field(..., alias="Algorithm")
class CanonicalizationMethod(BaseModel):
- algorithm: str = Field(..., alias="Algorithm")
+ algorithm: HttpUrl = Field(..., alias="Algorithm")
class Reference(BaseModel):
- transforms: List[Transform] = Field(..., alias="Transforms")
+ """
+ Reference is an object that represents the Reference XML element of a Signature.
+ This element is a reference to the element of a V2G body message that will
+ be signed.
+
+ According to xmldisg-core-schema, "Id", "URI" and "Type" all belong to the
+ Reference complex type, however, according to requirement [V2G2-771], Type
+ shall not be used. Also, the URI is enough to reference the Id attribute of
+ the element in the message body.
+
+ In order to understand how Reference is used and the connection to the V2G
+ body message, the user is invited to check the example in annex J, section
+ J.2 of the ISO 15118-2, which is partially transcribed here:
+
+ V2G body element contains Id="ID1"
+
+
+ U29tZSBSYW5kb20gRGF0YQ==
+
+
+ The Signature contains the Reference element with a URI which refers to the
+ V2G body element (ID1)
+
+
+
+
+ # noqa: E501
+
+
+
+
+
+ 0bXgPQBlvuVrMXmERTBR61TKGPwOCRYXT4s8d6mPSqk= # noqa: E501
+
+
+
+
+
+ """
+
+ transforms: Transforms = Field(..., alias="Transforms")
digest_method: DigestMethod = Field(..., alias="DigestMethod")
digest_value: bytes = Field(..., alias="DigestValue")
# 'URI' is actually an XML attribute, but JSON (our serialisation method)
@@ -35,6 +89,16 @@ class Reference(BaseModel):
class SignedInfo(BaseModel):
+ """
+ SignedInfo is an object that belongs to the Signature element as exemplified
+ in annex J, section J.2 of the ISO 15118-2.
+
+ According to the schema, the Reference attribute is unbounded, however, according
+ to requirement [V2G2-909]:
+ "The signature shall not reference more than 4 signed elements"
+ Therefore, a limit of 4 to the number of items of the `Reference` is enforced.
+ """
+
canonicalization_method: CanonicalizationMethod = Field(
..., alias="CanonicalizationMethod"
)
@@ -62,7 +126,7 @@ class X509IssuerSerial(BaseModel):
class SignedElement(BaseModel):
"""
The element of a BodyBase (the message inside the Body element of a
- V2GMessage) that needs to be digitally signed and referernced in the
+ V2GMessage) that needs to be digitally signed and referenced in the
SignedInfo field of the header.
Example:
diff --git a/iso15118/shared/network.py b/iso15118/shared/network.py
index db7c31d5..6630fb86 100644
--- a/iso15118/shared/network.py
+++ b/iso15118/shared/network.py
@@ -27,7 +27,8 @@ def _get_link_local_addr(nic: str) -> Union[IPv6Address, None]:
Args:
nic_addr_list A list of tuples per network interface card (NIC),
each containing e.g. address family and IP address.
- More info: https://psutil.readthedocs.io/en/latest/#psutil.net_if_addrs
+ More info:
+ https://psutil.readthedocs.io/en/latest/#psutil.net_if_addrs
Returns:
The IPv6 link-local address from the given list of NIC
@@ -64,7 +65,7 @@ async def _get_full_ipv6_address(host: str, port: int) -> Tuple[str, int, int, i
In this case we will get one entry that will look like
[ (, , 6, '',
('fe80::4fd:9dc8:b138:3bcc', 65334, 0, 5)) ]
- Check https://docs.python.org/3/library/asyncio-eventloop.html?highlight=getaddrinfo#asyncio.loop.getaddrinfo
+ Check https://docs.python.org/3/library/asyncio-eventloop.html?highlight=getaddrinfo#asyncio.loop.getaddrinfo # noqa: E501
loop.getaddrinfo is the async version of socket.getaddrinfo
Check https://docs.python.org/3/library/socket.html#socket.getaddrinfo
diff --git a/iso15118/shared/security.py b/iso15118/shared/security.py
index af204d48..d3c279a3 100644
--- a/iso15118/shared/security.py
+++ b/iso15118/shared/security.py
@@ -7,7 +7,6 @@
from cryptography.exceptions import InvalidSignature, UnsupportedAlgorithm
from cryptography.hazmat.backends.openssl.backend import Backend
-from cryptography.hazmat.primitives import hashes
from cryptography.hazmat.primitives.asymmetric import ec
from cryptography.hazmat.primitives.asymmetric.ec import (
SECP256R1,
@@ -15,6 +14,7 @@
EllipticCurvePublicKey,
)
from cryptography.hazmat.primitives.ciphers import Cipher, algorithms, modes
+from cryptography.hazmat.primitives.hashes import SHA256, Hash, HashAlgorithm
from cryptography.hazmat.primitives.kdf.concatkdf import ConcatKDFHash
from cryptography.hazmat.primitives.serialization import (
load_der_private_key,
@@ -42,17 +42,19 @@
)
from iso15118.shared.exi_codec import to_exi
from iso15118.shared.messages.enums import Namespace, Protocol
-from iso15118.shared.messages.iso15118_2.datatypes import Certificate as CertificateV2
from iso15118.shared.messages.iso15118_2.datatypes import (
CertificateChain as CertificateChainV2,
)
-from iso15118.shared.messages.iso15118_20.common_messages import (
- Certificate as CertificateV20,
+from iso15118.shared.messages.iso15118_2.datatypes import (
+ SubCertificates as SubCertificatesV2,
)
from iso15118.shared.messages.iso15118_20.common_messages import (
CertificateChain as CertificateChainV20,
)
from iso15118.shared.messages.iso15118_20.common_messages import SignedCertificateChain
+from iso15118.shared.messages.iso15118_20.common_messages import (
+ SubCertificates as SubCertificatesV20,
+)
from iso15118.shared.messages.xmldsig import (
CanonicalizationMethod,
DigestMethod,
@@ -62,7 +64,7 @@
SignatureValue,
SignedInfo,
Transform,
- TransformDetails,
+ Transforms,
)
from iso15118.shared.settings import PKI_PATH
@@ -351,19 +353,21 @@ def load_cert_chain(
sub_ca1_cert = load_cert(sub_ca1_path) if sub_ca1_path else None
if protocol == Protocol.ISO_15118_2:
- sub_ca_certs_v2: List[CertificateV2] = [CertificateV2(certificate=sub_ca2_cert)]
+ sub_ca_certs_v2: SubCertificatesV2 = SubCertificatesV2(
+ certificates=[sub_ca2_cert]
+ )
if sub_ca1_cert:
- sub_ca_certs_v2.append(CertificateV2(certificate=sub_ca1_cert))
+ sub_ca_certs_v2.certificates.append(sub_ca1_cert)
return CertificateChainV2(
certificate=leaf_cert, sub_certificates=sub_ca_certs_v2
)
if protocol.ns.startswith(Namespace.ISO_V20_BASE):
- sub_ca_certs_v20: List[CertificateV20] = [
- CertificateV20(certificate=sub_ca2_cert)
- ]
+ sub_ca_certs_v20: SubCertificatesV20 = SubCertificatesV20(
+ certificates=[sub_ca2_cert]
+ )
if sub_ca1_cert:
- sub_ca_certs_v20.append(CertificateV20(certificate=sub_ca1_cert))
+ sub_ca_certs_v20.certificates.append(sub_ca1_cert)
if id:
# In ISO 15118-20, there's a distinction between a CertificateChain
@@ -483,7 +487,7 @@ def verify_certs(
pub_key.verify(
leaf_cert.signature,
leaf_cert.tbs_certificate_bytes,
- ec.ECDSA(hashes.SHA256()),
+ ec.ECDSA(SHA256()),
)
else:
# TODO Add support for ISO 15118-20 public key types
@@ -499,7 +503,7 @@ def verify_certs(
leaf_cert.signature,
leaf_cert.tbs_certificate_bytes,
# TODO Find a way to read id dynamically from the certificate
- ec.ECDSA(hashes.SHA256()),
+ ec.ECDSA(SHA256()),
)
else:
# TODO Add support for ISO 15118-20 public key types
@@ -516,7 +520,7 @@ def verify_certs(
pub_key.verify(
sub_ca2_cert.signature,
sub_ca2_cert.tbs_certificate_bytes,
- ec.ECDSA(hashes.SHA256()),
+ ec.ECDSA(SHA256()),
)
else:
# TODO Add support for ISO 15118-20 public key types
@@ -533,7 +537,7 @@ def verify_certs(
pub_key.verify(
sub_ca1_cert.signature,
sub_ca1_cert.tbs_certificate_bytes,
- ec.ECDSA(hashes.SHA256()),
+ ec.ECDSA(SHA256()),
)
else:
# TODO Add support for ISO 15118-20 public key types
@@ -550,7 +554,7 @@ def verify_certs(
pub_key.verify(
sub_ca2_cert.signature,
sub_ca2_cert.tbs_certificate_bytes,
- ec.ECDSA(hashes.SHA256()),
+ ec.ECDSA(SHA256()),
)
else:
# TODO Add support for ISO 15118-20 public key types
@@ -564,13 +568,14 @@ def verify_certs(
issuer=cert_to_check.issuer.__str__(),
) from exc
except UnsupportedAlgorithm as exc:
+ cert_hash_algorithm: HashAlgorithm = cert_to_check.signature_hash_algorithm
raise CertSignatureError(
subject=cert_to_check.subject.__str__(),
issuer=cert_to_check.issuer.__str__(),
extra_info=f"UnsupportedAlgorithm for certificate "
f"{cert_to_check.subject.__str__()}. "
f"\nSignature hash algorithm: "
- f"{cert_to_check.signature_hash_algorithm.name if cert_to_check.signature_hash_algorithm else 'None'}"
+ f"{cert_hash_algorithm.name if cert_hash_algorithm else 'None'}"
f"\nSignature algorithm: "
f"{cert_to_check.signature_algorithm_oid}"
# TODO This OpenSSL version may not be the complied one
@@ -666,8 +671,11 @@ def create_signature(
and then applying ECDSA (Elliptic Curve Digital Signature Algorithm) to
it, encrypting with the private key provided.
+ (Check Annex J, section J.2 in ISO 15118-2, for a reference of how to
+ generate a signature)
+
Args:
- elements_to_sign: A list of tuples [int, bytes], where the first entry
+ elements_to_sign: A list of tuples [str, bytes], where the first entry
of each tuple is the Id field (XML attribute) and the
second entry is the EXI encoded bytes representation
of the element for which a Reference element in the
@@ -688,17 +696,12 @@ def create_signature(
# 1. Step: Reference generation
reference_list: List[Reference] = []
- for body_element in elements_to_sign:
- id_attr, exi_encoded = body_element
+ for id_attr, exi_encoded in elements_to_sign:
reference = Reference(
uri="#" + id_attr,
- transforms=[
- Transform(
- details=TransformDetails(
- algorithm="http://www.w3.org/TR/canonical-exi/"
- )
- )
- ],
+ transforms=Transforms(
+ transform=[Transform(algorithm="http://www.w3.org/TR/canonical-exi/")]
+ ),
digest_method=DigestMethod(
algorithm="http://www.w3.org/2001/04/xmlenc#sha256"
),
@@ -719,9 +722,7 @@ def create_signature(
# 2. Step: Signature generation
exi_encoded_signed_info = to_exi(signed_info, Namespace.XML_DSIG)
- signature_value = signature_key.sign(
- exi_encoded_signed_info, ec.ECDSA(hashes.SHA256())
- )
+ signature_value = signature_key.sign(exi_encoded_signed_info, ec.ECDSA(SHA256()))
signature = Signature(
signed_info=signed_info, signature_value=SignatureValue(value=signature_value)
)
@@ -780,8 +781,7 @@ def verify_signature(
True, if the signature can be successfully verified, False otherwise.
"""
# 1. Step: Digest value check for each reference element
- for body_element in elements_to_sign:
- id_attr, exi_encoded = body_element
+ for id_attr, exi_encoded in elements_to_sign:
logger.debug(f"Verifying digest for element with ID '{id_attr}'")
calculated_digest = create_digest(exi_encoded)
message_digests_equal = False
@@ -817,7 +817,7 @@ def verify_signature(
pub_key.verify(
signature.signature_value.value,
exi_encoded_signed_info,
- ec.ECDSA(hashes.SHA256()),
+ ec.ECDSA(SHA256()),
)
else:
# TODO Add support for ISO 15118-20 public key types
@@ -857,7 +857,7 @@ def verify_signature(
def create_digest(exi_encoded_element) -> bytes:
- digest = hashes.Hash(hashes.SHA256())
+ digest = Hash(SHA256())
digest.update(exi_encoded_element)
return digest.finalize()
@@ -962,7 +962,7 @@ def encrypt_priv_key(
)
concat_kdf = ConcatKDFHash(
- algorithm=hashes.SHA256(),
+ algorithm=SHA256(),
length=symmetric_key_length_in_bytes,
otherinfo=other_info,
)
@@ -1045,7 +1045,7 @@ def decrypt_priv_key(
)
concat_kdf = ConcatKDFHash(
- algorithm=hashes.SHA256(),
+ algorithm=SHA256(),
length=symmetric_key_length_in_bytes,
otherinfo=other_info,
)
@@ -1067,6 +1067,8 @@ class CertPath(str, Enum):
Provides the path to certificates used for Plug & Charge. The encoding
format is indicated by the latter part of the enum name (_DER or _PEM)
+ TODO: Make filepath flexible, so we can choose between -2 and -20 certificates
+
NOTE: For a productive environment, the access to certificate should be
managed in a secure way (e.g. through a hardware security module).
"""
diff --git a/iso15118/shared/states.py b/iso15118/shared/states.py
index c2789402..9bb19776 100644
--- a/iso15118/shared/states.py
+++ b/iso15118/shared/states.py
@@ -260,7 +260,7 @@ def create_next_message(
try:
exi_payload = to_exi(to_be_exi_encoded, namespace)
except EXIEncodingError as exc:
- logger.error(f"{exc.error}")
+ logger.error(f"{exc}")
self.next_state = Terminate
raise
diff --git a/iso15118/shared/utils.py b/iso15118/shared/utils.py
index 1762f8b1..78d4c2b1 100644
--- a/iso15118/shared/utils.py
+++ b/iso15118/shared/utils.py
@@ -59,7 +59,7 @@ async def wait_till_finished(
# As of Python 3.8 `asyncio.wait()` should be called only with
# `asyncio.Task`s.
- # See: https://docs.python.org/3/library/asyncio-task.html#asyncio-example-wait-coroutine
+ # See: https://docs.python.org/3/library/asyncio-task.html#asyncio-example-wait-coroutine # noqa: E501
for awaitable in awaitables:
if not isinstance(awaitable, asyncio.Task):
awaitable = asyncio.create_task(awaitable)
diff --git a/pyproject.toml b/pyproject.toml
index dac82cfa..37b15ede 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -26,6 +26,9 @@ isort = "^5.10.1"
flake8 = "^4.0.1"
pytest-cov = "^3.0.0"
+[tool.isort]
+profile = "black"
+
[build-system]
requires = ["poetry-core>=1.0.0"]
diff --git a/template.Dockerfile b/template.Dockerfile
index b4907064..f779bd0d 100644
--- a/template.Dockerfile
+++ b/template.Dockerfile
@@ -11,7 +11,7 @@ ENV PYTHONFAULTHANDLER=1 \
PIP_DEFAULT_TIMEOUT=100 \
POETRY_VERSION=1.1.11 \
VIRTUALENV_PIP=21.2.1 \
- MYPY_VERSION=0.910
+ MYPY_VERSION=0.930
RUN pip install "poetry==$POETRY_VERSION" "mypy==$MYPY_VERSION"
@@ -38,8 +38,9 @@ COPY iso15118/ iso15118/
COPY tests/ tests/
RUN poetry run pytest -vv --cov-config .coveragerc --cov-report term-missing --durations=3 --cov=.
RUN poetry run black --check --diff --line-length=88 iso15118 tests
-#RUN flake8 --config .flake8 iso15118 tests
-#RUN poetry run mypy --config-file mypy.ini cs run.py tests
+RUN poetry run flake8 --config .flake8 iso15118 tests
+# RUN poetry run mypy --config-file mypy.ini iso15118 tests
+
# Generate the wheel to be used by next stage
RUN poetry build