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/.gitmodules b/.gitmodules index 050687a..a88c767 100644 --- a/.gitmodules +++ b/.gitmodules @@ -1,4 +1,9 @@ [submodule "external_libs/HomePlugPWN"] path = external_libs/HomePlugPWN - url = git@github.com:JakeMG-INL/HomePlugPWN.git - branch = minimal-libs \ No newline at end of file + url = https://github.com/JakeMG-INL/HomePlugPWN.git + 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/comm_session_handler.py b/app/evcc/comm_session_handler.py index 735000d..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,12 +569,17 @@ 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) 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 +593,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/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/pev.py b/app/evcc/controller/pev.py index d960872..f0e0572 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 @@ -42,42 +40,60 @@ 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 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, - "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) @@ -107,13 +123,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/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/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/evcc/states/din_spec_states.py b/app/evcc/states/din_spec_states.py index 2bbf8d2..86b959d 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, ) @@ -744,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( @@ -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,13 +781,23 @@ 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, ) + 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/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/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/secc/controller/evse.py b/app/secc/controller/evse.py index 995e870..380a8ad 100644 --- a/app/secc/controller/evse.py +++ b/app/secc/controller/evse.py @@ -9,9 +9,6 @@ # need to do this to import the custom SECC and V2G scapy layer import time, logging -import netifaces - -from smbus import SMBus from app.shared.EmulatorEnum import RunMode @@ -53,22 +50,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 +92,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/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/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/app/secc/states/din_spec_states.py b/app/secc/states/din_spec_states.py index 7886f8a..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() @@ -847,15 +849,20 @@ 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() - ), - evse_max_power_limit=( - await self.comm_session.evse_controller.get_evse_max_power_limit() + await self.comm_session.evse_controller.get_evse_max_voltage_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, 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, 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__()) diff --git a/app/shared/utils.py b/app/shared/utils.py index 652e51d..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 @@ -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" 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 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 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"