From 6c5a7032ba0f46d9e2ff424d1ad9a4d786effeaa Mon Sep 17 00:00:00 2001 From: Jake Guidry Date: Mon, 16 Feb 2026 13:16:15 -0700 Subject: [PATCH 01/10] updated remote for submodule --- .gitmodules | 2 +- external_libs/HomePlugPWN | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitmodules b/.gitmodules index 050687a..d154cfc 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,4 @@ [submodule "external_libs/HomePlugPWN"] path = external_libs/HomePlugPWN - url = git@github.com:JakeMG-INL/HomePlugPWN.git + url = https://github.com/JakeMG-INL/HomePlugPWN.git branch = minimal-libs \ No newline at end of file diff --git a/external_libs/HomePlugPWN b/external_libs/HomePlugPWN index eae353c..ff840e7 160000 --- a/external_libs/HomePlugPWN +++ b/external_libs/HomePlugPWN @@ -1 +1 @@ -Subproject commit eae353cb5146933e6f4e69587074733e813d05f6 +Subproject commit ff840e707b0c54e06b1c836ed47112daffefd200 From 395ccfbf7a2fec9a29973cfa548c51197480a406 Mon Sep 17 00:00:00 2001 From: Jake Guidry Date: Tue, 17 Feb 2026 14:40:33 -0700 Subject: [PATCH 02/10] added "virtual" option for running without SMBus --- .env.evcc | 7 ++++-- .env.secc | 7 ++++-- app/evcc/controller/pev.py | 47 +++++++++++++++++++++++-------------- app/evcc/evcc_settings.py | 4 ++++ app/secc/controller/evse.py | 39 +++++++++++++++++------------- app/secc/secc_settings.py | 4 ++++ external_libs/EXPy | 1 + 7 files changed, 72 insertions(+), 37 deletions(-) create mode 160000 external_libs/EXPy diff --git a/.env.evcc b/.env.evcc index 59d0731..333b905 100644 --- a/.env.evcc +++ b/.env.evcc @@ -1,5 +1,5 @@ # General Settings -NETWORK_INTERFACE=eth1 +NETWORK_INTERFACE=acccs_evcc ENABLE_TLS_1_3=True ENABLE_NMAP=False @@ -15,4 +15,7 @@ EVCCID=1FMVAA45B63C47DD58Y6 # NMAP Settings NMAP_ARGS="-sS -sU -6" -NMAP_PORTS="-" \ No newline at end of file +NMAP_PORTS="-" + +# Virtual Testing +VIRTUAL=True \ No newline at end of file diff --git a/.env.secc b/.env.secc index a293598..045cdfc 100644 --- a/.env.secc +++ b/.env.secc @@ -1,5 +1,5 @@ # General Settings -NETWORK_INTERFACE=eth2 +NETWORK_INTERFACE=acccs_secc ENABLE_TLS_1_3=True ENABLE_NMAP=False @@ -20,4 +20,7 @@ EVSEID=USFRDE8326 # NMAP Settings NMAP_ARGS="-sS -sU -6" -NMAP_PORTS="-" \ No newline at end of file +NMAP_PORTS="-" + +# Virtual Testing +VIRTUAL=True \ No newline at end of file diff --git a/app/evcc/controller/pev.py b/app/evcc/controller/pev.py index d960872..72005b3 100644 --- a/app/evcc/controller/pev.py +++ b/app/evcc/controller/pev.py @@ -12,8 +12,6 @@ import json import logging -from smbus import SMBus - from app.shared.EmulatorEnum import RunMode, PEVState from app.evcc.transport.slac import SLACHandler @@ -52,22 +50,28 @@ def __init__(self, args): self.destinationIP = None self.destinationPort = None self.slac = None + + self.virtual = self.config.virtual + + if not self.virtual: + from smbus import SMBus - # I2C bus for relays - self.bus = SMBus(1) + # I2C bus for relays + self.bus = SMBus(1) - # Constants for i2c controlled relays - self.I2C_ADDR = 0x20 - self.CONTROL_REG = 0x9 - self.PEV_CP1 = 0b10 - self.PEV_CP2 = 0b100 - self.PEV_PP = 0b10000 - self.ALL_OFF = 0b0 + # Constants for i2c controlled relays + self.I2C_ADDR = 0x20 + self.CONTROL_REG = 0x9 + self.PEV_CP1 = 0b10 + self.PEV_CP2 = 0b100 + self.PEV_PP = 0b10000 + self.ALL_OFF = 0b0 async def start(self): - # Initialize the smbus for I2C commands - self.bus.write_byte_data(self.I2C_ADDR, 0x00, 0x00) - self.toggleProximity() + if not self.virtual: + # Initialize the smbus for I2C commands + self.bus.write_byte_data(self.I2C_ADDR, 0x00, 0x00) + self.toggleProximity() evcc_config = { "supportedProtocols": self.protocols, @@ -107,13 +111,22 @@ def openProximity(self): def setState(self, state: PEVState): if state == PEVState.A: logger.info("Going to state A") - self.bus.write_byte_data(self.I2C_ADDR, self.CONTROL_REG, self.ALL_OFF) + if self.virtual: + return + else: + self.bus.write_byte_data(self.I2C_ADDR, self.CONTROL_REG, self.ALL_OFF) elif state == PEVState.B: logger.info("Going to state B") - self.bus.write_byte_data(self.I2C_ADDR, self.CONTROL_REG, self.PEV_PP | self.PEV_CP1) + if self.virtual: + return + else: + self.bus.write_byte_data(self.I2C_ADDR, self.CONTROL_REG, self.PEV_PP | self.PEV_CP1) elif state == PEVState.C: logger.info("Going to state C") - self.bus.write_byte_data(self.I2C_ADDR, self.CONTROL_REG, self.PEV_PP | self.PEV_CP1 | self.PEV_CP2) + if self.virtual: + return + else: + self.bus.write_byte_data(self.I2C_ADDR, self.CONTROL_REG, self.PEV_PP | self.PEV_CP1 | self.PEV_CP2) def toggleProximity(self, t: int = 5): self.openProximity() diff --git a/app/evcc/evcc_settings.py b/app/evcc/evcc_settings.py index 80eaf02..c304748 100644 --- a/app/evcc/evcc_settings.py +++ b/app/evcc/evcc_settings.py @@ -22,6 +22,7 @@ class Config: console_log_level: Optional[str] = None file_log_level: Optional[str] = None ev_config_file_path: str = None + virtual: bool = False def load_envs(self, env_path: Optional[str] = None) -> None: """ @@ -50,6 +51,9 @@ def load_envs(self, env_path: Optional[str] = None) -> None: default="app/shared/examples/evcc/iso15118_2/evcc_config_eim_ac.json", # noqa ) ) + + self.virtual = env.bool("VIRTUAL", default=False) + env.seal() # raise all errors at once, if any load_shared_settings() logger.info("EVCC environment settings:") diff --git a/app/secc/controller/evse.py b/app/secc/controller/evse.py index 995e870..289273c 100644 --- a/app/secc/controller/evse.py +++ b/app/secc/controller/evse.py @@ -11,8 +11,6 @@ import time, logging import netifaces -from smbus import SMBus - from app.shared.EmulatorEnum import RunMode from app.secc.transport.slac import SLACHandler @@ -53,22 +51,28 @@ def __init__(self, args): self.destinationIP = None self.destinationPort = None self.slac = None + + self.virtual = self.config.virtual - # I2C bus for relays - self.bus = SMBus(1) + if not self.virtual: + from smbus import SMBus + + # I2C bus for relays + self.bus = SMBus(1) - # Constants for i2c controlled relays - self.I2C_ADDR = 0x20 - self.CONTROL_REG = 0x9 - self.EVSE_CP = 0b1 - self.EVSE_PP = 0b1000 - self.ALL_OFF = 0b0 + # Constants for i2c controlled relays + self.I2C_ADDR = 0x20 + self.CONTROL_REG = 0x9 + self.EVSE_CP = 0b1 + self.EVSE_PP = 0b1000 + self.ALL_OFF = 0b0 # Start the emulator async def start(self): - # Initialize the I2C bus for wwrite - self.bus.write_byte_data(self.I2C_ADDR, 0x00, 0x00) - self.toggleProximity() + if not self.virtual: + # Initialize the smbus for I2C commands + self.bus.write_byte_data(self.I2C_ADDR, 0x00, 0x00) + self.toggleProximity() self.iface = self.config.iface self.slac = SLACHandler(self) @@ -89,15 +93,18 @@ async def start(self): def closeProximity(self): if self.modified_cordset: logger.info("Closing CP/PP relay connections") - self.bus.write_byte_data(self.I2C_ADDR, self.CONTROL_REG, self.EVSE_PP | self.EVSE_CP) + if not self.virtual: + self.bus.write_byte_data(self.I2C_ADDR, self.CONTROL_REG, self.EVSE_PP | self.EVSE_CP) else: logger.info("Closing CP relay connection") - self.bus.write_byte_data(self.I2C_ADDR, self.CONTROL_REG, self.EVSE_CP) + if not self.virtual: + self.bus.write_byte_data(self.I2C_ADDR, self.CONTROL_REG, self.EVSE_CP) # Close the circuit for the proximity pins def openProximity(self): logger.info("Opening CP/PP relay connections") - self.bus.write_byte_data(self.I2C_ADDR, self.CONTROL_REG, self.ALL_OFF) + if not self.virtual: + self.bus.write_byte_data(self.I2C_ADDR, self.CONTROL_REG, self.ALL_OFF) # Opens and closes proximity circuit with a delay def toggleProximity(self, t: int = 5): diff --git a/app/secc/secc_settings.py b/app/secc/secc_settings.py index f6264d9..09243d2 100644 --- a/app/secc/secc_settings.py +++ b/app/secc/secc_settings.py @@ -32,6 +32,7 @@ class Config: supported_protocols: Optional[List[Protocol]] = None supported_auth_options: Optional[List[AuthEnum]] = None standby_allowed: bool = False + virtual: bool = False default_protocols = [ "DIN_SPEC_70121", "ISO_15118_2", @@ -110,6 +111,9 @@ def load_envs(self, env_path: Optional[str] = None) -> None: # enum values in PowerDeliveryReq's ChargeProgress field). In Standby, the # EV can still use value-added services while not consuming any power. self.standby_allowed = env.bool("STANDBY_ALLOWED", default=False) + + self.virtual = env.bool("VIRTUAL", default=False) + load_shared_settings(env_path) env.seal() # raise all errors at once, if any self.env_dump = dict(env.dump()) diff --git a/external_libs/EXPy b/external_libs/EXPy new file mode 160000 index 0000000..fbef6d4 --- /dev/null +++ b/external_libs/EXPy @@ -0,0 +1 @@ +Subproject commit fbef6d486f473f8745c0d63d6e0accba8986371b From e07fd1f1f301046d832ce990339cb341a3e50214 Mon Sep 17 00:00:00 2001 From: Jake Guidry Date: Tue, 17 Feb 2026 14:40:53 -0700 Subject: [PATCH 03/10] added scripts to add and remove virtual ethernets for testing --- remove_veth.sh | 22 ++++++++++++++++++++++ setup_veth.sh | 31 +++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+) create mode 100755 remove_veth.sh create mode 100755 setup_veth.sh diff --git a/remove_veth.sh b/remove_veth.sh new file mode 100755 index 0000000..be0624f --- /dev/null +++ b/remove_veth.sh @@ -0,0 +1,22 @@ +#!/bin/bash + +set -e + +IFACE_A="acccs_secc" +IFACE_B="acccs_evcc" + +REMOVED=0 + +for IFACE in "$IFACE_A" "$IFACE_B"; do + if ip link show "$IFACE" &>/dev/null; then + echo "Removing interface $IFACE..." + sudo ip link delete "$IFACE" + REMOVED=1 + fi +done + +if [ "$REMOVED" -eq 0 ]; then + echo "No interfaces found to remove." +else + echo "Done! Teardown complete." +fi diff --git a/setup_veth.sh b/setup_veth.sh new file mode 100755 index 0000000..1e2c669 --- /dev/null +++ b/setup_veth.sh @@ -0,0 +1,31 @@ +#!/bin/bash + +set -e + +IFACE_A="acccs_secc" +IFACE_B="acccs_evcc" +ADDR_A="fe80::1" +ADDR_B="fe80::2" + +# Teardown existing interfaces if they exist +for IFACE in "$IFACE_A" "$IFACE_B"; do + if ip link show "$IFACE" &>/dev/null; then + echo "Existing interface $IFACE found, removing..." + sudo ip link delete "$IFACE" + fi +done + +echo "Creating veth pair: $IFACE_A <-> $IFACE_B" +sudo ip link add "$IFACE_A" type veth peer name "$IFACE_B" + +echo "Bringing interfaces up..." +sudo ip link set "$IFACE_A" up +sudo ip link set "$IFACE_B" up + +echo "Assigning link-local addresses..." +sudo ip addr add "${ADDR_A}/64" dev "$IFACE_A" +sudo ip addr add "${ADDR_B}/64" dev "$IFACE_B" + +echo "Done. Interface summary:" +ip addr show "$IFACE_A" +ip addr show "$IFACE_B" From 6bb36b8b8593a5cef75d893b704e402e9bb6397c Mon Sep 17 00:00:00 2001 From: MD Sahabul Hossain Date: Tue, 17 Mar 2026 14:02:08 -0600 Subject: [PATCH 04/10] Fixed EV config prioritization bug In EV config JSON files, options for various fields were supposed to be prioritized based on order. But the original code used sets which doesn't preserve order. Signed-off-by: MD Sahabul Hossain --- app/evcc/controller/pev.py | 34 +++++++++++++++++++++++----------- app/shared/utils.py | 6 +++--- 2 files changed, 26 insertions(+), 14 deletions(-) diff --git a/app/evcc/controller/pev.py b/app/evcc/controller/pev.py index 72005b3..f0e0572 100644 --- a/app/evcc/controller/pev.py +++ b/app/evcc/controller/pev.py @@ -40,10 +40,10 @@ def __init__(self, args): self.sourceMAC = get_nic_mac_address(self.iface) self.sourceIP = str(get_link_local_addr(self.iface)) self.sourcePort = args.source_port[0] if args.source_port else get_tcp_port() - self.protocols = args.protocols.split(",") if args.protocols else ["ISO_15118_2", "DIN_SPEC_70121"] - self.authModes = args.authmodes.split(",") if args.authmodes else ["PNC", "EIM"] - self.energyMode = args.energymode if args.energymode else "DC" - self.useTLS = args.useTLS if args.useTLS else "True" + self.protocols = args.protocols.split(",") if args.protocols else None + self.authModes = args.authmodes.split(",") if args.authmodes else None + self.energyMode = args.energymode if args.energymode else None + self.useTLS = args.useTLS if args.useTLS else None self.slacSoundTimeout = args.slacSoundTimeout if args.slacSoundTimeout else 1000 self.destinationMAC = None @@ -73,15 +73,27 @@ async def start(self): self.bus.write_byte_data(self.I2C_ADDR, 0x00, 0x00) self.toggleProximity() - evcc_config = { - "supportedProtocols": self.protocols, - "supportedAuthModes": self.authModes, - "supportedEnergyServices": [self.energyMode], - "useTls": self.useTLS, - } + with open(self.config.ev_config_file_path, "r") as f: + evcc_config_data = json.load(f) + + if self.protocols: + evcc_config_data["supportedProtocols"] = self.protocols + if self.authModes: + evcc_config_data["supportedAuthModes"] = self.authModes + if self.energyMode: + evcc_config_data["supportedEnergyServices"] = [self.energyMode] + if self.useTLS: + if self.useTLS.lower() == "true": + evcc_config_data["useTls"] = True + elif self.useTLS.lower() == "false": + evcc_config_data["useTls"] = False + else: + logger.warning(f"Invalid value for useTLS: {self.useTLS}. " + f"Should be 'true' or 'false'. Defaulting to the .env value.") + self.config.ev_config_file_path = "app/shared/examples/evcc/evcc_settings.json" with open(self.config.ev_config_file_path, "w") as f: - json.dump(evcc_config, f, indent=4) + json.dump(evcc_config_data, f, indent=4) evcc_config = await load_from_file(self.config.ev_config_file_path) self.slac = SLACHandler(self) diff --git a/app/shared/utils.py b/app/shared/utils.py index 652e51d..b574ab6 100644 --- a/app/shared/utils.py +++ b/app/shared/utils.py @@ -28,7 +28,7 @@ def load_requested_protocols(read_protocols: Optional[List[str]]) -> List[Protoc ] protocols = _format_list(read_protocols) - valid_protocols = list(set(protocols).intersection(supported_protocols)) + valid_protocols = [protocol for protocol in protocols if protocol in supported_protocols] if not valid_protocols: raise NoSupportedProtocols( f"No supported protocols configured. Supported protocols are " @@ -53,7 +53,7 @@ def load_requested_energy_services( ] services = _format_list(read_services) - valid_services = list(set(services).intersection(supported_services)) + valid_services = [service for service in services if service in supported_services] if not valid_services: raise NoSupportedEnergyServices( f"No supported energy services configured. Supported energy services are " @@ -70,7 +70,7 @@ def load_requested_auth_modes(read_auth_modes: Optional[List[str]]) -> List[Auth "PNC", ] auth_modes = _format_list(read_auth_modes) - valid_auth_options = list(set(auth_modes).intersection(default_auth_modes)) + valid_auth_options = [mode for mode in auth_modes if mode in default_auth_modes] if not valid_auth_options: raise NoSupportedAuthenticationModes( f"No supported authentication modes configured. Supported auth modes" From fae4543fd1056b57d984aeb2235280d40ec74207 Mon Sep 17 00:00:00 2001 From: MD Sahabul Hossain Date: Tue, 17 Mar 2026 14:41:32 -0600 Subject: [PATCH 05/10] Replaced deprecated datetime items Signed-off-by: MD Sahabul Hossain --- app/shared/security.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/app/shared/security.py b/app/shared/security.py index 1c885b4..d28e796 100644 --- a/app/shared/security.py +++ b/app/shared/security.py @@ -9,7 +9,7 @@ import ssl import sslkeylog from base64 import urlsafe_b64encode -from datetime import datetime +from datetime import datetime, timezone from enum import Enum, auto from ssl import DER_cert_to_PEM_cert, SSLContext, SSLError, VerifyMode from typing import Dict, List, Optional, Tuple, Union, cast @@ -478,7 +478,7 @@ def log_certs_details(certs: List[bytes]): logger.debug(f"Issuer: {der_cert.issuer}") logger.debug(f"Serial number: {der_cert.serial_number}") logger.debug( - f"Validity: {der_cert.not_valid_before} - {der_cert.not_valid_after}" + f"Validity: {der_cert.not_valid_before_utc} - {der_cert.not_valid_after_utc}" ) logger.debug( f"Fingerprint: {der_cert.fingerprint(der_cert.signature_hash_algorithm).hex(':')}" # noqa @@ -732,11 +732,11 @@ def check_validity(certs: List[Certificate]): Raises: CertNotYetValidError, CertExpiredError """ - now = datetime.utcnow() + now = datetime.now(timezone.utc) for cert in certs: - if cert.not_valid_before > now: + if cert.not_valid_before_utc > now: raise CertNotYetValidError(cert.subject.__str__()) - if cert.not_valid_after < now: + if cert.not_valid_after_utc < now: raise CertExpiredError(cert.subject.__str__()) From 6401f856d15c1a733ed269f0f11f5b3a1ec8f854 Mon Sep 17 00:00:00 2001 From: MD Sahabul Hossain Date: Tue, 17 Mar 2026 14:54:01 -0600 Subject: [PATCH 06/10] Fixed some DIN specific bugs which mixed up DIN with ISO 15118-2 Signed-off-by: MD Sahabul Hossain --- app/evcc/controller/interface.py | 8 ++- app/evcc/controller/simulator.py | 57 +++++++++++++++++-- app/evcc/states/din_spec_states.py | 39 +++++++++---- app/evcc/states/iso15118_2_states.py | 44 +++++++++++---- app/secc/controller/interface.py | 82 ++++++++++++++++++++-------- app/secc/controller/simulator.py | 25 ++++++++- app/secc/states/din_spec_states.py | 12 +++- app/secc/states/iso15118_2_states.py | 12 +++- 8 files changed, 218 insertions(+), 61 deletions(-) diff --git a/app/evcc/controller/interface.py b/app/evcc/controller/interface.py index ba78436..3f597dd 100644 --- a/app/evcc/controller/interface.py +++ b/app/evcc/controller/interface.py @@ -503,7 +503,8 @@ async def is_bulk_charging_complete(self) -> bool: raise NotImplementedError @abstractmethod - async def get_remaining_time_to_full_soc(self) -> PVRemainingTimeToFullSOC: + async def get_remaining_time_to_full_soc( + self, protocol: Protocol) -> PVRemainingTimeToFullSOC: """ Gets the remaining time until full soc is reached. @@ -611,7 +612,8 @@ async def get_target_voltage(self) -> RationalNumber: """ raise NotImplementedError - async def get_remaining_time_to_bulk_soc(self) -> PVRemainingTimeToBulkSOC: + async def get_remaining_time_to_bulk_soc( + self, protocol: Protocol) -> PVRemainingTimeToBulkSOC: """ Gets the remaining time until bulk soc is reached. @@ -666,7 +668,7 @@ async def get_dc_ev_power_delivery_parameter(self) -> DCEVPowerDeliveryParameter raise NotImplementedError @abstractmethod - async def get_dc_charge_params(self) -> DCEVChargeParams: + async def get_dc_charge_params(self, protocol: Protocol) -> DCEVChargeParams: """ This would return an encapsulation of the following parameters: DC Max Current Limit diff --git a/app/evcc/controller/simulator.py b/app/evcc/controller/simulator.py index ea59080..9eb212d 100644 --- a/app/evcc/controller/simulator.py +++ b/app/evcc/controller/simulator.py @@ -33,6 +33,14 @@ PVPMax, PVRemainingTimeToBulkSOC, PVRemainingTimeToFullSOC, + PVEVMaxCurrentLimitDin, + PVEVMaxPowerLimitDin, + PVEVMaxVoltageLimitDin, + PVEVEnergyCapacityDin, + PVEVTargetCurrentDin, + PVEVTargetVoltageDin, + PVRemainingTimeToBulkSOCDin, + PVRemainingTimeToFullSOCDin, ) from app.shared.messages.din_spec.datatypes import ( DCEVPowerDeliveryParameter as DCEVPowerDeliveryParameterDINSPEC, @@ -593,7 +601,11 @@ async def is_precharged( async def get_dc_ev_power_delivery_parameter_dinspec( self, ) -> DCEVPowerDeliveryParameterDINSPEC: - pass + return DCEVPowerDeliveryParameterDINSPEC( + dc_ev_status=await self.get_dc_ev_status_dinspec(), + bulk_charging_complete=False, + charging_complete=await self.continue_charging(), + ) async def get_dc_ev_power_delivery_parameter(self) -> DCEVPowerDeliveryParameter: return DCEVPowerDeliveryParameter( @@ -611,11 +623,19 @@ async def is_charging_complete(self) -> bool: else: return False - async def get_remaining_time_to_full_soc(self) -> PVRemainingTimeToFullSOC: - return PVRemainingTimeToFullSOC(multiplier=0, value=100, unit="s") + async def get_remaining_time_to_full_soc( + self, protocol: Protocol) -> PVRemainingTimeToFullSOC: + if protocol == Protocol.DIN_SPEC_70121: + return PVRemainingTimeToFullSOCDin(multiplier=0, value=100, unit="s") + else: + return PVRemainingTimeToFullSOC(multiplier=0, value=100, unit="s") - async def get_remaining_time_to_bulk_soc(self) -> PVRemainingTimeToBulkSOC: - return PVRemainingTimeToBulkSOC(multiplier=0, value=80, unit="s") + async def get_remaining_time_to_bulk_soc( + self, protocol: Protocol) -> PVRemainingTimeToBulkSOC: + if protocol == Protocol.DIN_SPEC_70121: + return PVRemainingTimeToBulkSOCDin(multiplier=0, value=80, unit="s") + else: + return PVRemainingTimeToBulkSOC(multiplier=0, value=80, unit="s") async def welding_detection_has_finished(self): if self.welding_detection_cycles == 3: @@ -674,8 +694,33 @@ async def get_ac_charge_loop_params_v20( # | DC-SPECIFIC FUNCTIONS | # ============================================================================ - async def get_dc_charge_params(self) -> DCEVChargeParams: + async def get_dc_charge_params(self, protocol: Protocol) -> DCEVChargeParams: """Applies to both DIN SPEC and 15118-2""" + if protocol not in (Protocol.ISO_15118_2, Protocol.DIN_SPEC_70121): + logger.error( + f"Invalid protocol '{protocol}' for DC charge params, " + "expected ISO 15118-2 or DIN SPEC 70121" + ) + raise InvalidProtocolError + elif protocol == Protocol.DIN_SPEC_70121: + self.dc_ev_charge_params.dc_max_current_limit = PVEVMaxCurrentLimitDin( + multiplier=-3, value=32000, unit=UnitSymbol.AMPERE + ) + self.dc_ev_charge_params.dc_max_power_limit = PVEVMaxPowerLimitDin( + multiplier=1, value=8000, unit=UnitSymbol.WATT + ) + self.dc_ev_charge_params.dc_max_voltage_limit = PVEVMaxVoltageLimitDin( + multiplier=1, value=50, unit=UnitSymbol.VOLTAGE + ) + self.dc_ev_charge_params.dc_energy_capacity = PVEVEnergyCapacityDin( + multiplier=1, value=7000, unit=UnitSymbol.WATT_HOURS + ) + self.dc_ev_charge_params.dc_target_current = PVEVTargetCurrentDin( + multiplier=0, value=1, unit=UnitSymbol.AMPERE + ) + self.dc_ev_charge_params.dc_target_voltage = PVEVTargetVoltageDin( + multiplier=1, value=50, unit=UnitSymbol.VOLTAGE + ) return self.dc_ev_charge_params async def get_dc_ev_status_dinspec(self) -> DCEVStatusDINSPEC: diff --git a/app/evcc/states/din_spec_states.py b/app/evcc/states/din_spec_states.py index 2bbf8d2..b21942a 100644 --- a/app/evcc/states/din_spec_states.py +++ b/app/evcc/states/din_spec_states.py @@ -334,7 +334,8 @@ async def build_charge_parameter_discovery_req(self) -> ChargeParameterDiscovery await self.comm_session.ev_controller.get_dc_ev_status_dinspec() ) dc_charge_params: DCEVChargeParams = ( - await self.comm_session.ev_controller.get_dc_charge_params() + await self.comm_session.ev_controller.get_dc_charge_params( + protocol=Protocol.DIN_SPEC_70121) ) max_current_limit = dc_charge_params.dc_max_current_limit max_voltage_limit = dc_charge_params.dc_max_voltage_limit @@ -431,7 +432,9 @@ async def process_message( async def build_charge_parameter_discovery_req(self) -> ChargeParameterDiscoveryReq: ev_controller = self.comm_session.ev_controller - dc_charge_params: DCEVChargeParams = await ev_controller.get_dc_charge_params() + dc_charge_params: DCEVChargeParams = await ev_controller.get_dc_charge_params( + protocol=Protocol.DIN_SPEC_70121 + ) max_current_limit = dc_charge_params.dc_max_current_limit max_voltage_limit = dc_charge_params.dc_max_voltage_limit dc_charge_parameter: DCEVChargeParameter = DCEVChargeParameter( @@ -519,7 +522,9 @@ async def process_message( async def build_pre_charge_req(self) -> PreChargeReq: ev_controller = self.comm_session.ev_controller - dc_charge_params = await ev_controller.get_dc_charge_params() + dc_charge_params = await ev_controller.get_dc_charge_params( + protocol=Protocol.DIN_SPEC_70121 + ) pre_charge_req = PreChargeReq( dc_ev_status=await ev_controller.get_dc_ev_status_dinspec(), ev_target_voltage=dc_charge_params.dc_target_voltage, @@ -598,7 +603,9 @@ async def process_message( async def build_pre_charge_req(self) -> PreChargeReq: ev_controller = self.comm_session.ev_controller - dc_charge_params = await ev_controller.get_dc_charge_params() + dc_charge_params = await ev_controller.get_dc_charge_params( + protocol=Protocol.DIN_SPEC_70121 + ) pre_charge_req = PreChargeReq( dc_ev_status=await ev_controller.get_dc_ev_status_dinspec(), ev_target_voltage=dc_charge_params.dc_target_voltage, @@ -612,7 +619,7 @@ async def build_power_delivery_req(self) -> PowerDeliveryReq: ready_to_charge=True, charging_profile=None, dc_ev_power_delivery_parameter=( - await ev_controller.get_dc_ev_power_delivery_parameter() + await ev_controller.get_dc_ev_power_delivery_parameter_dinspec() ), ) return power_delivery_req @@ -660,7 +667,9 @@ async def process_message( async def build_current_demand_req(self) -> CurrentDemandReq: ev_controller = self.comm_session.ev_controller - dc_charge_params = await ev_controller.get_dc_charge_params() + dc_charge_params = await ev_controller.get_dc_charge_params( + protocol=Protocol.DIN_SPEC_70121 + ) current_demand_req: CurrentDemandReq = CurrentDemandReq( dc_ev_status=await ev_controller.get_dc_ev_status_dinspec(), ev_target_current=dc_charge_params.dc_target_current, @@ -670,10 +679,12 @@ async def build_current_demand_req(self) -> CurrentDemandReq: bulk_charging_complete=(await ev_controller.is_bulk_charging_complete()), charging_complete=await ev_controller.is_charging_complete(), remaining_time_to_full_soc=( - await ev_controller.get_remaining_time_to_full_soc() + await ev_controller.get_remaining_time_to_full_soc( + protocol=Protocol.DIN_SPEC_70121) ), remaining_time_to_bulk_soc=( - await ev_controller.get_remaining_time_to_bulk_soc() + await ev_controller.get_remaining_time_to_bulk_soc( + protocol=Protocol.DIN_SPEC_70121) ), ev_target_voltage=dc_charge_params.dc_target_voltage, ) @@ -758,7 +769,9 @@ async def stop_charging(self): async def build_current_demand_req(self): ev_controller = self.comm_session.ev_controller - dc_charge_params: DCEVChargeParams = await ev_controller.get_dc_charge_params() + dc_charge_params: DCEVChargeParams = await ev_controller.get_dc_charge_params( + protocol=Protocol.DIN_SPEC_70121 + ) current_demand_req: CurrentDemandReq = CurrentDemandReq( dc_ev_status=await ev_controller.get_dc_ev_status_dinspec(), ev_target_current=dc_charge_params.dc_target_current, @@ -768,10 +781,14 @@ async def build_current_demand_req(self): bulk_charging_complete=(await ev_controller.is_bulk_charging_complete()), charging_complete=await ev_controller.is_charging_complete(), remaining_time_to_full_soc=( - await ev_controller.get_remaining_time_to_full_soc() + await ev_controller.get_remaining_time_to_full_soc( + protocol=Protocol.DIN_SPEC_70121 + ) ), remaining_time_to_bulk_soc=( - await ev_controller.get_remaining_time_to_bulk_soc() + await ev_controller.get_remaining_time_to_bulk_soc( + protocol=Protocol.DIN_SPEC_70121 + ) ), ev_target_voltage=dc_charge_params.dc_target_voltage, ) diff --git a/app/evcc/states/iso15118_2_states.py b/app/evcc/states/iso15118_2_states.py index 0022eaa..3e9567b 100644 --- a/app/evcc/states/iso15118_2_states.py +++ b/app/evcc/states/iso15118_2_states.py @@ -941,7 +941,9 @@ async def process_message( async def build_current_demand_data(self) -> CurrentDemandReq: dc_ev_charge_params = ( - await self.comm_session.ev_controller.get_dc_charge_params() + await self.comm_session.ev_controller.get_dc_charge_params( + protocol=Protocol.ISO_15118_2 + ) ) current_demand_req = CurrentDemandReq( dc_ev_status=await self.comm_session.ev_controller.get_dc_ev_status(), @@ -955,10 +957,14 @@ async def build_current_demand_data(self) -> CurrentDemandReq: await self.comm_session.ev_controller.is_charging_complete() ), remaining_time_to_full_soc=( - await self.comm_session.ev_controller.get_remaining_time_to_full_soc() + await self.comm_session.ev_controller.get_remaining_time_to_full_soc( + protocol=Protocol.ISO_15118_2 + ) ), remaining_time_to_bulk_soc=( - await self.comm_session.ev_controller.get_remaining_time_to_bulk_soc() + await self.comm_session.ev_controller.get_remaining_time_to_bulk_soc( + protocol=Protocol.ISO_15118_2 + ) ), ev_target_voltage=dc_ev_charge_params.dc_target_voltage, ) @@ -1039,7 +1045,9 @@ async def process_message( ) async def build_current_demand_req(self) -> CurrentDemandReq: - dc_charge_params = await self.comm_session.ev_controller.get_dc_charge_params() + dc_charge_params = await self.comm_session.ev_controller.get_dc_charge_params( + protocol=Protocol.ISO_15118_2 + ) current_demand_req: CurrentDemandReq = CurrentDemandReq( dc_ev_status=await self.comm_session.ev_controller.get_dc_ev_status(), ev_target_current=dc_charge_params.dc_target_current, @@ -1054,10 +1062,14 @@ async def build_current_demand_req(self) -> CurrentDemandReq: await self.comm_session.ev_controller.is_charging_complete() ), remaining_time_to_full_soc=( - await self.comm_session.ev_controller.get_remaining_time_to_full_soc() + await self.comm_session.ev_controller.get_remaining_time_to_full_soc( + protocol=Protocol.ISO_15118_2 + ) ), remaining_time_to_bulk_soc=( - await self.comm_session.ev_controller.get_remaining_time_to_bulk_soc() + await self.comm_session.ev_controller.get_remaining_time_to_bulk_soc( + protocol=Protocol.ISO_15118_2 + ) ), ) return current_demand_req @@ -1293,7 +1305,9 @@ async def process_message( async def build_pre_charge_message(self): charge_params: DCEVChargeParams = ( - await self.comm_session.ev_controller.get_dc_charge_params() + await self.comm_session.ev_controller.get_dc_charge_params( + protocol=Protocol.ISO_15118_2 + ) ) pre_charge_req = PreChargeReq( dc_ev_status=await self.comm_session.ev_controller.get_dc_ev_status(), @@ -1367,7 +1381,9 @@ async def process_message( async def build_pre_charge_message(self): charge_params: DCEVChargeParams = ( - await self.comm_session.ev_controller.get_dc_charge_params() + await self.comm_session.ev_controller.get_dc_charge_params( + protocol=Protocol.ISO_15118_2 + ) ) pre_charge_req = PreChargeReq( dc_ev_status=await self.comm_session.ev_controller.get_dc_ev_status(), @@ -1430,7 +1446,9 @@ async def process_message( async def build_current_demand_data(self) -> CurrentDemandReq: ev_controller = self.comm_session.ev_controller - dc_ev_charge_params = await ev_controller.get_dc_charge_params() + dc_ev_charge_params = await ev_controller.get_dc_charge_params( + protocol=Protocol.ISO_15118_2 + ) current_demand_req = CurrentDemandReq( dc_ev_status=await ev_controller.get_dc_ev_status(), ev_target_current=dc_ev_charge_params.dc_target_current, @@ -1439,10 +1457,14 @@ async def build_current_demand_data(self) -> CurrentDemandReq: bulk_charging_complete=(await ev_controller.is_bulk_charging_complete()), charging_complete=await ev_controller.is_charging_complete(), remaining_time_to_full_soc=( - await ev_controller.get_remaining_time_to_full_soc() + await ev_controller.get_remaining_time_to_full_soc( + protocol=Protocol.ISO_15118_2 + ) ), remaining_time_to_bulk_soc=( - await ev_controller.get_remaining_time_to_bulk_soc() + await ev_controller.get_remaining_time_to_bulk_soc( + protocol=Protocol.ISO_15118_2 + ) ), ev_target_voltage=dc_ev_charge_params.dc_target_voltage, ) diff --git a/app/secc/controller/interface.py b/app/secc/controller/interface.py index 3266dd3..bec15cd 100644 --- a/app/secc/controller/interface.py +++ b/app/secc/controller/interface.py @@ -22,8 +22,13 @@ PVEVSEMaxCurrentLimit, PVEVSEMaxPowerLimit, PVEVSEMaxVoltageLimit, + PVEVSEMaxCurrentLimitDin, + PVEVSEMaxPowerLimitDin, + PVEVSEMaxVoltageLimitDin, PVEVSEPresentCurrent, + PVEVSEPresentCurrentDin, PVEVSEPresentVoltage, + PVEVSEPresentVoltageDin ) from app.shared.messages.din_spec.datatypes import ( ResponseCode as ResponseCodeDINSPEC, @@ -762,7 +767,7 @@ async def get_dc_charge_parameters_v2(self) -> DCEVSEChargeParameter: async def get_evse_present_voltage( self, protocol: Protocol - ) -> Union[PVEVSEPresentVoltage, RationalNumber]: + ) -> Union[PVEVSEPresentVoltage, PVEVSEPresentVoltageDin, RationalNumber]: """ Gets the presently available voltage at the EVSE @@ -775,7 +780,10 @@ async def get_evse_present_voltage( exponent, value = PhysicalValue.get_exponent_value_repr( cast(int, self.evse_data_context.present_voltage) ) - return PVEVSEPresentVoltage(multiplier=exponent, value=value, unit="V") + if protocol == Protocol.DIN_SPEC_70121: + return PVEVSEPresentVoltageDin(multiplier=exponent, value=value, unit="V") + else: + return PVEVSEPresentVoltage(multiplier=exponent, value=value, unit="V") else: return RationalNumber.get_rational_repr( self.evse_data_context.present_voltage @@ -783,7 +791,7 @@ async def get_evse_present_voltage( async def get_evse_present_current( self, protocol: Protocol - ) -> Union[PVEVSEPresentCurrent, RationalNumber]: + ) -> Union[PVEVSEPresentCurrent, PVEVSEPresentCurrentDin, RationalNumber]: """ Gets the presently available current at the EVSE @@ -796,7 +804,10 @@ async def get_evse_present_current( exponent, value = PhysicalValue.get_exponent_value_repr( cast(int, self.evse_data_context.present_current) ) - return PVEVSEPresentCurrent(multiplier=exponent, value=value, unit="A") + if protocol == Protocol.DIN_SPEC_70121: + return PVEVSEPresentCurrentDin(multiplier=exponent, value=value, unit="A") + else: + return PVEVSEPresentCurrent(multiplier=exponent, value=value, unit="A") else: return RationalNumber.get_rational_repr( self.evse_data_context.present_current @@ -881,12 +892,14 @@ async def is_evse_power_limit_achieved(self) -> bool: # TODO retrieve from evse data context return False - async def get_evse_max_voltage_limit(self) -> PVEVSEMaxVoltageLimit: + async def get_evse_max_voltage_limit( + self, protocol: Protocol) -> PVEVSEMaxVoltageLimit: """ Gets the max voltage that can be provided by the charger Relevant for: - ISO 15118-2 + - DIN SPEC 70121 """ session_limits = self.evse_data_context.session_limits if self.evse_data_context.current_type == CurrentType.AC: @@ -894,20 +907,29 @@ async def get_evse_max_voltage_limit(self) -> PVEVSEMaxVoltageLimit: else: voltage_limit = session_limits.dc_limits.max_voltage exponent, value = PhysicalValue.get_exponent_value_repr(voltage_limit) - return PVEVSEMaxVoltageLimit( - multiplier=exponent, - value=value, - unit=UnitSymbol.VOLTAGE, - ) + if protocol == Protocol.DIN_SPEC_70121: + return PVEVSEMaxVoltageLimitDin( + multiplier=exponent, + value=value, + unit=UnitSymbol.VOLTAGE, + ) + else: + return PVEVSEMaxVoltageLimit( + multiplier=exponent, + value=value, + unit=UnitSymbol.VOLTAGE, + ) async def get_evse_max_current_limit( self, - ) -> Union[PVEVSEMaxCurrentLimit, PVEVSEMaxCurrent]: + protocol: Protocol, + ) -> Union[PVEVSEMaxCurrentLimit, PVEVSEMaxCurrentLimitDin, PVEVSEMaxCurrent]: """ Gets the max current that can be provided by the charger Relevant for: - ISO 15118-2 + - DIN SPEC 70121 """ # This is currently being used by -2 only. logger.info( @@ -967,11 +989,18 @@ async def get_evse_max_current_limit( current_limit = session_limits.dc_limits.max_charge_current logger.debug(f"Active EVSEMaxCurrentLimit: {current_limit}") exponent, value = PhysicalValue.get_exponent_value_repr(current_limit) - return PVEVSEMaxCurrentLimit( - multiplier=exponent, - value=value, - unit=UnitSymbol.AMPERE, - ) + if protocol == Protocol.DIN_SPEC_70121: + return PVEVSEMaxCurrentLimitDin( + multiplier=exponent, + value=value, + unit=UnitSymbol.AMPERE, + ) + else: + return PVEVSEMaxCurrentLimit( + multiplier=exponent, + value=value, + unit=UnitSymbol.AMPERE, + ) @abstractmethod async def get_dc_charge_params_v20( @@ -987,12 +1016,14 @@ async def get_dc_charge_params_v20( """ raise NotImplementedError - async def get_evse_max_power_limit(self) -> PVEVSEMaxPowerLimit: + async def get_evse_max_power_limit( + self, protocol: Protocol) -> PVEVSEMaxPowerLimit: """ Gets the max power that can be provided by the charger Relevant for: - ISO 15118-2 + - DIN SPEC 70121 """ session_limits = self.evse_data_context.session_limits max_discharge_power = 0.0 @@ -1007,11 +1038,18 @@ async def get_evse_max_power_limit(self) -> PVEVSEMaxPowerLimit: else: power_limit = max_charge_power exponent, value = PhysicalValue.get_exponent_value_repr(power_limit) - return PVEVSEMaxPowerLimit( - multiplier=exponent, - value=value, - unit=UnitSymbol.WATT, - ) + if protocol == Protocol.DIN_SPEC_70121: + return PVEVSEMaxPowerLimitDin( + multiplier=exponent, + value=value, + unit=UnitSymbol.WATT, + ) + else: + return PVEVSEMaxPowerLimit( + multiplier=exponent, + value=value, + unit=UnitSymbol.WATT, + ) async def get_dc_charge_loop_params_v20( self, control_mode: ControlMode, selected_service: ServiceV20 diff --git a/app/secc/controller/simulator.py b/app/secc/controller/simulator.py index 2b5f674..10a734a 100644 --- a/app/secc/controller/simulator.py +++ b/app/secc/controller/simulator.py @@ -44,6 +44,11 @@ PVEVSEMinCurrentLimit, PVEVSEMinVoltageLimit, PVEVSEPeakCurrentRipple, + PVEVSEMaxCurrentLimitDin, + PVEVSEMaxPowerLimitDin, + PVEVSEMaxVoltageLimitDin, + PVEVSEMinCurrentLimitDin, + PVEVSEMinVoltageLimitDin, ) from app.shared.messages.din_spec.datatypes import ( PMaxScheduleEntry as PMaxScheduleEntryDINSPEC, @@ -271,6 +276,15 @@ def reset_ev_data_context(self): async def set_status(self, status: ServiceStatus) -> None: logger.debug(f"New Status: {status}") + async def is_valid_evse_id(self, evse_id: str) -> bool: + # A DIN 70121 EVSE ID is a string of hexadecimal + # format (each byte represented by two hexadecimal digits). + try: + bytes.fromhex(evse_id) + return True + except ValueError: + return False + async def get_evse_id(self, protocol: Protocol) -> str: # To transform a string-based DIN SPEC 91286 EVSE ID to hexBinary # representation and vice versa, the following conversion rules shall @@ -289,6 +303,10 @@ async def get_evse_id(self, protocol: Protocol) -> str: evse_id = env.str("EVSEID", default="ZZ00000") else: evse_id = env.str("EVSEID", default="49A89A6360") + if not await self.is_valid_evse_id(evse_id): + logger.warning(f"Invalid EVSE ID {evse_id} provided for " + f"protocol {protocol}. Using default EVSE ID.") + evse_id = "49A89A6360" env.seal() # raise all errors at once, if any return evse_id @@ -915,8 +933,11 @@ async def is_evse_power_limit_achieved(self) -> bool: # async def get_evse_max_current_limit(self) -> PVEVSEMaxCurrentLimit: # return PVEVSEMaxCurrentLimit(multiplier=0, value=300, unit="A") - async def get_evse_max_power_limit(self) -> PVEVSEMaxPowerLimit: - return PVEVSEMaxPowerLimit(multiplier=1, value=1000, unit="W") + async def get_evse_max_power_limit(self, protocol: Protocol) -> PVEVSEMaxPowerLimit: + if protocol == Protocol.DIN_SPEC_70121: + return PVEVSEMaxPowerLimitDin(multiplier=1, value=1000, unit="W") + else: + return PVEVSEMaxPowerLimit(multiplier=1, value=1000, unit="W") async def get_dc_charge_params_v20( self, energy_service: ServiceV20 diff --git a/app/secc/states/din_spec_states.py b/app/secc/states/din_spec_states.py index 7886f8a..740d02e 100644 --- a/app/secc/states/din_spec_states.py +++ b/app/secc/states/din_spec_states.py @@ -847,13 +847,19 @@ async def process_message( await self.comm_session.evse_controller.is_evse_power_limit_achieved() ), evse_max_current_limit=( - await self.comm_session.evse_controller.get_evse_max_current_limit() + await self.comm_session.evse_controller.get_evse_max_current_limit( + Protocol.DIN_SPEC_70121 + ) ), evse_max_voltage_limit=( - await self.comm_session.evse_controller.get_evse_max_voltage_limit() + await self.comm_session.evse_controller.get_evse_max_voltage_limit( + Protocol.DIN_SPEC_70121 + ) ), evse_max_power_limit=( - await self.comm_session.evse_controller.get_evse_max_power_limit() + await self.comm_session.evse_controller.get_evse_max_power_limit( + Protocol.DIN_SPEC_70121 + ) ), ) await self.comm_session.evse_controller.send_display_params() diff --git a/app/secc/states/iso15118_2_states.py b/app/secc/states/iso15118_2_states.py index b5a7da6..6bb463b 100644 --- a/app/secc/states/iso15118_2_states.py +++ b/app/secc/states/iso15118_2_states.py @@ -2472,7 +2472,9 @@ async def process_message( current = ( await evse_controller.get_evse_present_current(Protocol.ISO_15118_2) ) - max_power = await evse_controller.get_evse_max_power_limit() + max_power = await evse_controller.get_evse_max_power_limit( + protocol=Protocol.ISO_15118_2 + ) current_demand_res = CurrentDemandRes( response_code=ResponseCode.OK, dc_evse_status=await evse_controller.get_dc_evse_status(), @@ -2485,8 +2487,12 @@ async def process_message( await evse_controller.is_evse_voltage_limit_achieved() ), evse_power_limit_achieved=await evse_controller.is_evse_power_limit_achieved(), # noqa - evse_max_voltage_limit=await evse_controller.get_evse_max_voltage_limit(), - evse_max_current_limit=await evse_controller.get_evse_max_current_limit(), + evse_max_voltage_limit=await evse_controller.get_evse_max_voltage_limit( + protocol=Protocol.ISO_15118_2 + ), + evse_max_current_limit=await evse_controller.get_evse_max_current_limit( + protocol=Protocol.ISO_15118_2 + ), evse_max_power_limit=max_power, evse_id=await evse_controller.get_evse_id(Protocol.ISO_15118_2), sa_schedule_tuple_id=self.comm_session.selected_schedule, From a704f52917f7b4b203bffdf771d9bd3f506caeeb Mon Sep 17 00:00:00 2001 From: MD Sahabul Hossain Date: Mon, 23 Mar 2026 12:17:26 -0600 Subject: [PATCH 07/10] Increase default session ID length; preserve list order; add EXPy submodule Signed-off-by: MD Sahabul Hossain --- .gitmodules | 7 ++++++- app/evcc/states/sap_states.py | 2 +- app/shared/utils.py | 2 +- 3 files changed, 8 insertions(+), 3 deletions(-) diff --git a/.gitmodules b/.gitmodules index d154cfc..a88c767 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,9 @@ [submodule "external_libs/HomePlugPWN"] path = external_libs/HomePlugPWN url = https://github.com/JakeMG-INL/HomePlugPWN.git - branch = minimal-libs \ No newline at end of file + branch = minimal-libs + +[submodule "external_libs/EXPy"] + path = external_libs/EXPy + url = https://github.com/IdahoLabResearch/EXPy.git + branch = main \ No newline at end of file diff --git a/app/evcc/states/sap_states.py b/app/evcc/states/sap_states.py index ba5d981..25cb055 100644 --- a/app/evcc/states/sap_states.py +++ b/app/evcc/states/sap_states.py @@ -187,7 +187,7 @@ async def process_message( f"ID '{sap_res.schema_id}'" ) - def get_session_id(self, length=1) -> str: + def get_session_id(self, length=6) -> str: """ Check if there's a saved session ID from a previously paused charging session and applies that for the now resumed charging session. diff --git a/app/shared/utils.py b/app/shared/utils.py index b574ab6..101232e 100644 --- a/app/shared/utils.py +++ b/app/shared/utils.py @@ -15,7 +15,7 @@ def _format_list(read_settings: List[str]) -> List[str]: read_settings = list(filter(None, read_settings)) read_settings = [setting.strip().upper() for setting in read_settings] - read_settings = list(set(read_settings)) + read_settings = list(dict.fromkeys(read_settings)) return read_settings From 058e9e7455fb1dc2adc0aad8d584c24b48852578 Mon Sep 17 00:00:00 2001 From: MD Sahabul Hossain Date: Mon, 23 Mar 2026 13:57:03 -0600 Subject: [PATCH 08/10] Fixed DIN SPEC 70121 specific data type error Also, added power delivery related information for DIN Signed-off-by: MD Sahabul Hossain --- app/evcc/states/din_spec_states.py | 8 +++++++- app/secc/states/din_spec_states.py | 33 +++++++++++++++--------------- 2 files changed, 24 insertions(+), 17 deletions(-) diff --git a/app/evcc/states/din_spec_states.py b/app/evcc/states/din_spec_states.py index b21942a..86b959d 100644 --- a/app/evcc/states/din_spec_states.py +++ b/app/evcc/states/din_spec_states.py @@ -755,7 +755,7 @@ async def stop_charging(self): power_delivery_req = PowerDeliveryReq( ready_to_charge=False, dc_ev_power_delivery_parameter=( - await ev_controller.get_dc_ev_power_delivery_parameter() + await ev_controller.get_dc_ev_power_delivery_parameter_dinspec() ), ) self.create_next_message( @@ -792,6 +792,12 @@ async def build_current_demand_req(self): ), ev_target_voltage=dc_charge_params.dc_target_voltage, ) + current = dc_charge_params.dc_target_current + voltage = dc_charge_params.dc_target_voltage + max_power = dc_charge_params.dc_max_power_limit + logger.info(f"EV Target Voltage: {voltage.value * (10 ** voltage.multiplier)} {voltage.unit.value}") + logger.info(f"EV Target Current: {current.value * (10 ** current.multiplier)} {current.unit.value}") + logger.info(f"EV Max Power Limit: {max_power.value * (10 ** max_power.multiplier)} {max_power.unit.value}") return current_demand_req diff --git a/app/secc/states/din_spec_states.py b/app/secc/states/din_spec_states.py index 740d02e..36c38f4 100644 --- a/app/secc/states/din_spec_states.py +++ b/app/secc/states/din_spec_states.py @@ -825,20 +825,22 @@ async def process_message( ResponseCode.FAILED, ) return - + + evse_controller = self.comm_session.evse_controller + voltage = ( + await evse_controller.get_evse_present_voltage(Protocol.DIN_SPEC_70121) + ) + current = ( + await evse_controller.get_evse_present_current(Protocol.DIN_SPEC_70121) + ) + max_power = await evse_controller.get_evse_max_power_limit( + protocol=Protocol.DIN_SPEC_70121 + ) current_demand_res: CurrentDemandRes = CurrentDemandRes( response_code=ResponseCode.OK, dc_evse_status=await self.comm_session.evse_controller.get_dc_evse_status(), - evse_present_voltage=( - await self.comm_session.evse_controller.get_evse_present_voltage( - Protocol.DIN_SPEC_70121 - ) - ), - evse_present_current=( - await self.comm_session.evse_controller.get_evse_present_current( - Protocol.DIN_SPEC_70121 - ) - ), + evse_present_voltage=voltage, + evse_present_current=current, evse_current_limit_achieved=current_demand_req.charging_complete, evse_voltage_limit_achieved=( await self.comm_session.evse_controller.is_evse_voltage_limit_achieved() @@ -856,12 +858,11 @@ async def process_message( Protocol.DIN_SPEC_70121 ) ), - evse_max_power_limit=( - await self.comm_session.evse_controller.get_evse_max_power_limit( - Protocol.DIN_SPEC_70121 - ) - ), + evse_max_power_limit=max_power, ) + logger.info(f"EVSE Present Voltage: {voltage.value * (10 ** voltage.multiplier)} {voltage.unit.value}") + logger.info(f"EVSE Present Current: {current.value * (10 ** current.multiplier)} {current.unit.value}") + logger.info(f"EVSE Max Power Limit: {max_power.value * (10 ** max_power.multiplier)} {max_power.unit.value}") await self.comm_session.evse_controller.send_display_params() self.create_next_message( None, From e9de169ee559173b2bc4d181b67fed63c664d92c Mon Sep 17 00:00:00 2001 From: MD Sahabul Hossain Date: Mon, 23 Mar 2026 22:42:06 -0600 Subject: [PATCH 09/10] Gracefully terminate the EVCC script after failure Also, removed unnecessary package import Signed-off-by: MD Sahabul Hossain --- app/evcc/comm_session_handler.py | 2 ++ app/secc/controller/evse.py | 1 - 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/app/evcc/comm_session_handler.py b/app/evcc/comm_session_handler.py index 735000d..372d2b1 100644 --- a/app/evcc/comm_session_handler.py +++ b/app/evcc/comm_session_handler.py @@ -583,6 +583,7 @@ async def get_from_rcv_queue(self, queue: asyncio.Queue): await self.restart_sdp(False) except SDPFailedError as exc: logger.exception(exc) + break # TODO not sure what else to do here elif isinstance(notification, StopNotification): await cancel_task(self.comm_session[1]) @@ -596,6 +597,7 @@ async def get_from_rcv_queue(self, queue: asyncio.Queue): await self.restart_sdp(True) except SDPFailedError as exc: logger.exception(exc) + break # TODO not sure what else to do here else: logger.warning( diff --git a/app/secc/controller/evse.py b/app/secc/controller/evse.py index 289273c..380a8ad 100644 --- a/app/secc/controller/evse.py +++ b/app/secc/controller/evse.py @@ -9,7 +9,6 @@ # need to do this to import the custom SECC and V2G scapy layer import time, logging -import netifaces from app.shared.EmulatorEnum import RunMode From 9a8cbe3a4a5232089fa873f05f419537f425a2e5 Mon Sep 17 00:00:00 2001 From: MD Sahabul Hossain Date: Tue, 24 Mar 2026 14:11:44 -0600 Subject: [PATCH 10/10] Additional fixes for graceful EVCC termination Signed-off-by: MD Sahabul Hossain --- app/evcc/comm_session_handler.py | 32 ++++++++++++++------------------ 1 file changed, 14 insertions(+), 18 deletions(-) diff --git a/app/evcc/comm_session_handler.py b/app/evcc/comm_session_handler.py index 372d2b1..f9f258c 100644 --- a/app/evcc/comm_session_handler.py +++ b/app/evcc/comm_session_handler.py @@ -428,11 +428,11 @@ async def start_comm_session(self, host: IPv6Address, port: int, is_tls: bool): ) logger.info("TCP client connected") except Exception as exc: - logger.exception( + logger.error( f"{exc.__class__.__name__} when trying to connect " f"to host {host} and port {port}" ) - return + raise exc if shared_settings[SettingKey.ENABLE_NMAP]: self.nmap_scanner = threading.Thread(target=self.port_scan, args=(host,), name='nmap_scanner') @@ -456,12 +456,12 @@ async def start_comm_session(self, host: IPv6Address, port: int, is_tls: bool): comm_session.start(Timeouts.SUPPORTED_APP_PROTOCOL_REQ) ) except MessageProcessingError as exc: - logger.exception( + logger.error( f"{exc.__class__.__name__} occurred while trying to " - f"create create an SDPRequest" + f"create an SupportedAppProtocolReq message" ) - return - + raise exc + def port_scan(self, host): if not os.path.isdir("scan_results"): @@ -502,12 +502,8 @@ async def process_incoming_udp_packet(self, message: UDPPacketNotification): sdp_response = SDPResponse.from_payload(v2gtp_msg.payload) except InvalidSDPResponseError as exc: logger.error(exc) - try: - await self.restart_sdp(True) - return - except SDPFailedError as exc: - logger.exception(exc) - return # TODO check if this is correct here + await self.restart_sdp(True) + return logger.info(f"SDPResponse received: {sdp_response}") @@ -553,11 +549,7 @@ async def process_incoming_udp_packet(self, message: UDPPacketNotification): f"Incoming datagram of {len(message)} bytes is no " f"valid SDPResponse message" ) - try: - await self.restart_sdp(True) - except SDPFailedError as exc: - logger.exception(exc) - return # TODO check if this is correct here + await self.restart_sdp(True) return await self.start_comm_session(host, port, secc_signals_tls) @@ -577,7 +569,11 @@ async def get_from_rcv_queue(self, queue: asyncio.Queue): try: if isinstance(notification, UDPPacketNotification): - await self.process_incoming_udp_packet(notification) + try: + await self.process_incoming_udp_packet(notification) + except Exception as exc: + logger.exception(exc) + break elif isinstance(notification, ReceiveTimeoutNotification): try: await self.restart_sdp(False)