From 88dd617638cdf19ed33cbe96aca8c62821a9adc1 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 23 Oct 2026 16:04:39 +0200 Subject: [PATCH 01/16] internal_socket --- data/config/mosquitto/local/openwb_local.conf | 1 + packages/control/consumer/rfid.py | 5 +- packages/control/consumer/rfid_test.py | 7 +- packages/helpermodules/command.py | 16 ++ packages/helpermodules/command_test.py | 87 +++++++++ .../missing_role_topics_test.py | 14 ++ packages/helpermodules/setdata.py | 22 +++ packages/helpermodules/subdata.py | 14 ++ packages/helpermodules/update_config.py | 15 ++ packages/main.py | 1 + packages/modules/common/component_type.py | 3 + packages/modules/common/fault_state.py | 3 +- packages/modules/common/hardware_check.py | 113 +---------- .../modules/common/hardware_check_test.py | 12 +- packages/modules/common/store/__init__.py | 1 + .../modules/common/store/_socket_internal.py | 23 +++ packages/modules/configuration.py | 25 +-- packages/modules/consumers/openwb/__init__.py | 0 .../openwb/internal_socket/__init__.py | 0 .../openwb/internal_socket/config.py | 24 +++ .../openwb/internal_socket/consumer.py | 79 ++++++++ .../openwb/internal_socket/consumer_test.py | 59 ++++++ .../openwb/internal_socket/selection.py | 30 +++ packages/modules/consumers/openwb/vendor.py | 14 ++ packages/modules/devices/vendors.py | 7 + .../internal_chargepoint_handler/clients.py | 30 +-- .../hardware_definition.py | 23 +++ .../internal_chargepoint_handler.py | 137 +++++++++++--- .../internal_hardware_check.py | 132 +++++++++++++ .../internal_socket.py | 71 +++++++ .../internal_socket_config.py | 33 ++++ .../internal_socket_module_test.py | 38 ++++ .../internal_socket_test.py | 175 ++++++++++++++++++ .../internal_chargepoint_handler/rfid.py | 1 + 34 files changed, 1029 insertions(+), 186 deletions(-) create mode 100644 packages/modules/common/store/_socket_internal.py create mode 100644 packages/modules/consumers/openwb/__init__.py create mode 100644 packages/modules/consumers/openwb/internal_socket/__init__.py create mode 100644 packages/modules/consumers/openwb/internal_socket/config.py create mode 100644 packages/modules/consumers/openwb/internal_socket/consumer.py create mode 100644 packages/modules/consumers/openwb/internal_socket/consumer_test.py create mode 100644 packages/modules/consumers/openwb/internal_socket/selection.py create mode 100644 packages/modules/consumers/openwb/vendor.py create mode 100644 packages/modules/internal_chargepoint_handler/hardware_definition.py create mode 100644 packages/modules/internal_chargepoint_handler/internal_hardware_check.py create mode 100644 packages/modules/internal_chargepoint_handler/internal_socket.py create mode 100644 packages/modules/internal_chargepoint_handler/internal_socket_config.py create mode 100644 packages/modules/internal_chargepoint_handler/internal_socket_module_test.py create mode 100644 packages/modules/internal_chargepoint_handler/internal_socket_test.py diff --git a/data/config/mosquitto/local/openwb_local.conf b/data/config/mosquitto/local/openwb_local.conf index e96e2071d2..b30e2c81d7 100644 --- a/data/config/mosquitto/local/openwb_local.conf +++ b/data/config/mosquitto/local/openwb_local.conf @@ -38,6 +38,7 @@ topic openWB/consumer/+/get/# out 2 topic openWB/consumer/+/set/# out 2 topic openWB/internal_chargepoint/# out 2 +topic openWB/internal_socket/# out 2 topic openWB/io/# out 2 topic openWB/internal_io/# out 2 diff --git a/packages/control/consumer/rfid.py b/packages/control/consumer/rfid.py index dafe50de8c..c58e5d6b4c 100644 --- a/packages/control/consumer/rfid.py +++ b/packages/control/consumer/rfid.py @@ -6,8 +6,11 @@ from helpermodules import timecheck +RFID_SUPPORTED_MODULE_TYPES = ("mqtt", "internal_socket") + + def rfid_supported(module) -> bool: - return getattr(module, "type", None) == "mqtt" + return getattr(module, "type", None) in RFID_SUPPORTED_MODULE_TYPES class ConsumerRfidMixin: diff --git a/packages/control/consumer/rfid_test.py b/packages/control/consumer/rfid_test.py index 338d5fc15f..df8c511c1b 100644 --- a/packages/control/consumer/rfid_test.py +++ b/packages/control/consumer/rfid_test.py @@ -8,20 +8,21 @@ from control.chargemode import Chargemode from control.consumer.consumer import Consumer from control.consumer.consumer_data import ConsumerConfig, RfidReset +from control.consumer.rfid import RFID_SUPPORTED_MODULE_TYPES from control.consumer.usage import ConsumerUsage from dataclass_utils import asdict, dataclass_from_dict from helpermodules.subdata import SubData from helpermodules.changed_values_handler import ChangedValuesHandler -@pytest.fixture -def consumer(monkeypatch): +@pytest.fixture(params=RFID_SUPPORTED_MODULE_TYPES) +def consumer(monkeypatch, request): data.data_init(Mock()) data.data.optional_data = optional.Optional() data.data.optional_data.data.rfid.active = True consumer = Consumer(1) consumer.module = Mock() - consumer.data.module = Mock(type="mqtt") + consumer.data.module = Mock(type=request.param) consumer.data.config.rfid_enabled = True consumer.data.config.valid_tags = ["AB??", "1234"] consumer.data.usage.type = ConsumerUsage.SUSPENDABLE_ONOFF diff --git a/packages/helpermodules/command.py b/packages/helpermodules/command.py index d6526dc066..0bd83749c5 100644 --- a/packages/helpermodules/command.py +++ b/packages/helpermodules/command.py @@ -50,6 +50,8 @@ from modules.chargepoints.internal_openwb.config import InternalChargepointMode from modules.common.component_type import ComponentType, special_to_general_type_mapping, type_to_topic_mapping from modules.common.configurable_vehicle import GeneralVehicleConfig +from modules.consumers.openwb.internal_socket.config import InternalSocketConfiguration +from modules.consumers.openwb.internal_socket.selection import validate_selection log = logging.getLogger(__name__) @@ -1200,11 +1202,22 @@ def resetUserPassword(self, connection_id: str, payload: dict) -> None: def addConsumer(self, connection_id: str, payload: dict) -> None: """ sendet das Topic, zu dem ein neues Device erstellt werden soll. """ + socket_configuration = None + if payload["data"]["vendor"] == "openwb" and payload["data"]["type"] == "internal_socket": + socket_configuration = InternalSocketConfiguration( + chargepoint_id=payload["data"].get("configuration", {}).get("chargepoint_id")) + try: + validate_selection(socket_configuration) + except ValueError as e: + pub_user_message(payload, connection_id, str(e), MessageType.ERROR) + return new_id = self.max_id_hierarchy + 1 dev = importlib.import_module(f'.consumers.{payload["data"]["vendor"]}' f'.{payload["data"]["type"]}.consumer', "modules") consumer_default = asdict(dev.device_descriptor.configuration_factory()) + if socket_configuration is not None: + consumer_default["configuration"] = asdict(socket_configuration) consumer_default["id"] = new_id try: evu_counter = SubData.counter_all_data.get_id_evu_counter() @@ -1251,6 +1264,9 @@ def removeConsumer(self, connection_id: str, payload: dict) -> None: payload, connection_id, f'Die ID \'{payload["data"]["consumer_id"]}\' ist größer als die maximal vergebene ' f'ID \'{self.max_id_hierarchy}\'.', MessageType.ERROR) + consumer = SubData.consumer_data.get(f'consumer{payload["data"]["consumer_id"]}') + if consumer is not None and getattr(consumer.data.module, "type", None) == "internal_socket": + consumer.module.switch_off() ProcessBrokerBranch(f'consumer/{payload["data"]["consumer_id"]}/').remove_topics() SubData.counter_all_data.hierarchy_remove_item(payload["data"]["consumer_id"]) Pub().pub("openWB/set/counter/get/hierarchy", SubData.counter_all_data.data.get.hierarchy) diff --git a/packages/helpermodules/command_test.py b/packages/helpermodules/command_test.py index 8a20864c0d..e203bbef6e 100644 --- a/packages/helpermodules/command_test.py +++ b/packages/helpermodules/command_test.py @@ -167,3 +167,90 @@ def test_get_max_id_hierarchy_by_topics(mock_pub, monkeypatch): assert command_instance.max_id_hierarchy == 9 mock_pub.pub.assert_any_call("openWB/set/command/max_id/hierarchy", 9) + + +@pytest.mark.parametrize("consumer_type", ["internal_socket", "mqtt"]) +def test_remove_internal_socket_switches_off_before_removing_topics(consumer_type, mock_pub, monkeypatch): + order = [] + consumer = SimpleNamespace(data=SimpleNamespace(module=SimpleNamespace(type=consumer_type)), module=Mock()) + consumer.module.switch_off.side_effect = lambda: order.append("off") + branch = Mock() + branch.remove_topics.side_effect = lambda: order.append("remove") + monkeypatch.setattr(command, "ProcessBrokerBranch", Mock(return_value=branch)) + monkeypatch.setattr(command, "pub_user_message", Mock()) + monkeypatch.setattr(SubData, "consumer_data", {"consumer2": consumer}) + monkeypatch.setattr(SubData, "counter_all_data", Mock()) + monkeypatch.setattr(SubData, "system_data", { + "system": SimpleNamespace(data={"security": {"user_management_active": False}})}) + instance = Command.__new__(Command) + instance.max_id_hierarchy = 2 + instance.removeConsumer("test", {"data": {"consumer_id": 2}}) + assert order == (["off", "remove"] if consumer_type == "internal_socket" else ["remove"]) + + +@pytest.mark.parametrize("existing_type, requested_type, existing_cp, requested_cp, blocked", [ + ("internal_socket", "internal_socket", None, None, True), + ("mqtt", "internal_socket", None, None, False), + (None, "internal_socket", None, None, False), + ("internal_socket", "mqtt", None, None, False), + ("internal_socket", "internal_socket", None, 4, False), + ("internal_socket", "internal_socket", 4, None, False), + ("internal_socket", "internal_socket", 4, 5, False), + ("internal_socket", "internal_socket", 4, 4, True), + ("internal_socket", "internal_socket", 4, 6, True), + ("internal_socket", "internal_socket", None, 0, True), + ("internal_socket", "internal_socket", 0, None, True), +]) +def test_add_consumer_rejects_second_internal_socket_before_side_effects( + existing_type, requested_type, existing_cp, requested_cp, blocked, mock_pub, monkeypatch): + existing_module = SimpleNamespace(type=existing_type, configuration=SimpleNamespace( + chargepoint_id=existing_cp)) if existing_type else None + monkeypatch.setattr(SubData, "consumer_data", { + "consumer2": SimpleNamespace(num=2, data=SimpleNamespace(module=existing_module))}) + monkeypatch.setattr(SubData, "cp_data", { + f"cp{num}": SimpleNamespace(chargepoint=SimpleNamespace(chargepoint_module=SimpleNamespace( + config=SimpleNamespace(type="external_openwb", configuration=SimpleNamespace(ip_address=host))))) + for num, host in [(4, "192.168.1.40"), (5, "192.168.1.50"), (6, "192.168.1.40")]}) + SubData.cp_data["cp0"] = SimpleNamespace(chargepoint=SimpleNamespace(chargepoint_module=SimpleNamespace( + config=SimpleNamespace(type="internal_openwb")))) + counter = Mock() + counter.get_id_evu_counter.return_value = 0 + counter.data.get.hierarchy = [] + counter.data.get.loadmanagement_prios = [] + monkeypatch.setattr(SubData, "counter_all_data", counter) + monkeypatch.setattr(SubData, "system_data", { + "system": SimpleNamespace(data={"security": {"user_management_active": False}})}) + message = Mock() + monkeypatch.setattr(command, "pub_user_message", message) + vendor = "openwb" if requested_type == "internal_socket" else "generic" + setup = SimpleNamespace(id=0, type=requested_type, name="Test-Verbraucher", info={}, + configuration=SimpleNamespace(), vendor=vendor, + usage=(ConsumerUsage.SUSPENDABLE_ONOFF,)) + module_import = Mock(return_value=SimpleNamespace(device_descriptor=SimpleNamespace( + configuration_factory=Mock(return_value=setup)))) + monkeypatch.setattr(command.importlib, "import_module", module_import) + instance = Command.__new__(Command) + instance.max_id_hierarchy = 2 + payload = {"data": {"vendor": vendor, "type": requested_type, + "configuration": {"chargepoint_id": requested_cp}}} + + instance.addConsumer("test", payload) + + if blocked: + message.assert_called_once_with( + payload, "test", "Die Steckdose dieser openWB kann nur einem Verbraucher zugeordnet werden.", + command.MessageType.ERROR) + module_import.assert_not_called() + counter.get_id_evu_counter.assert_not_called() + counter.hierarchy_add_item_below.assert_not_called() + counter.add_loadmanagement_prio_item.assert_not_called() + mock_pub.pub.assert_not_called() + assert instance.max_id_hierarchy == 2 + else: + assert instance.max_id_hierarchy == 3 + counter.hierarchy_add_item_below.assert_called_once() + assert any(call.args[0] == "openWB/consumer/3/module" for call in mock_pub.pub.call_args_list) + if requested_type == "internal_socket": + module_payload = next(call.args[1] for call in mock_pub.pub.call_args_list + if call.args[0] == "openWB/consumer/3/module") + assert module_payload["configuration"] == {"chargepoint_id": requested_cp} diff --git a/packages/helpermodules/mosquitto_dynsec/missing_role_topics_test.py b/packages/helpermodules/mosquitto_dynsec/missing_role_topics_test.py index e5a1a35a27..5d5088415e 100644 --- a/packages/helpermodules/mosquitto_dynsec/missing_role_topics_test.py +++ b/packages/helpermodules/mosquitto_dynsec/missing_role_topics_test.py @@ -79,6 +79,20 @@ def _get_data_path() -> Path: 'openWB/internal_chargepoint/+/get/state_str', 'openWB/internal_chargepoint/+/get/rfid', 'openWB/internal_chargepoint/+/get/rfid_timestamp', + 'openWB/internal_socket/data/set_state', + 'openWB/internal_socket/data/detected', + 'openWB/internal_socket/data/last_tag', + 'openWB/internal_socket/get/state', + 'openWB/internal_socket/get/rfid', + 'openWB/internal_socket/get/power', + 'openWB/internal_socket/get/imported', + 'openWB/internal_socket/get/exported', + 'openWB/internal_socket/get/currents', + 'openWB/internal_socket/get/voltages', + 'openWB/internal_socket/get/powers', + 'openWB/internal_socket/get/fault_state', + 'openWB/internal_socket/get/fault_str', + 'openWB/internal_socket/get/error_timestamp', # abgesehen vom Fahrzeug können mqtt get-Topics bei aktiver BV nur gepublished werden 'openWB/mqtt/bat/+/get/power', 'openWB/mqtt/bat/+/get/soc', diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 64cf820e86..9114b7fb0f 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -102,6 +102,8 @@ def on_message(self, client: mqtt.Client, userdata, msg: mqtt.MQTTMessage): self.process_command_topic(msg) elif "openWB/set/internal_chargepoint/" in msg.topic: self.process_internal_chargepoint_topic(msg) + elif "openWB/set/internal_socket/" in msg.topic: + self.process_internal_socket_topic(msg) elif "openWB/set/LegacySmartHome/" in msg.topic: self.process_legacy_smart_home_topic(msg) elif "openWB/set/consumer/" in msg.topic: @@ -1235,6 +1237,26 @@ def process_internal_chargepoint_topic(self, msg): except Exception: log.exception(f"Fehler im setdata-Modul: Topic {msg.topic}, Value: {msg.payload}") + def process_internal_socket_topic(self, msg): + try: + suffix = msg.topic.removeprefix("openWB/set/internal_socket/") + if suffix in ("data/set_state", "data/detected", "get/state"): + self._validate_value(msg, bool) + elif suffix == "get/error_timestamp": + self._validate_value(msg, float, [(0, TIMESTAMP_2100)]) + elif suffix == "get/fault_state": + self._validate_value(msg, int, [(0, 2)]) + elif suffix in ("data/last_tag", "get/fault_str", "get/rfid"): + self._validate_value(msg, str) + elif suffix in ("get/power", "get/imported", "get/exported"): + self._validate_value(msg, float) + elif suffix in ("get/currents", "get/voltages", "get/powers"): + self._validate_value(msg, "json") + else: + self.__unknown_topic(msg) + except Exception: + log.exception(f"Fehler im setdata-Modul: Topic {msg.topic}, Value: {msg.payload}") + def process_legacy_smart_home_topic(self, msg): """Handler für die alten SmartHome-Topics diff --git a/packages/helpermodules/subdata.py b/packages/helpermodules/subdata.py index 26f20853d3..6fce0710ca 100644 --- a/packages/helpermodules/subdata.py +++ b/packages/helpermodules/subdata.py @@ -46,6 +46,7 @@ from modules.internal_chargepoint_handler.internal_chargepoint_handler_config import ( GlobalHandlerData, InternalChargepoint, RfidData) from modules.vehicles.manual.config import ManualSoc +from modules.internal_chargepoint_handler.internal_socket_config import InternalSocket log = logging.getLogger(__name__) mqtt_log = logging.getLogger("mqtt") @@ -76,6 +77,7 @@ class SubData: "global_data": GlobalHandlerData(), "rfid_data": RfidData()} io_actions = io_device.IoActions() + internal_socket_data = InternalSocket() io_states: Dict[str, io_device.IoStates] = {} optional_data = optional.Optional() system_data = {"system": system.System()} @@ -157,6 +159,7 @@ def on_connect(self, client: mqtt.Client, userdata, flags: dict, rc: int): ("openWB/consumer/+/control_parameter/#", 2), ("openWB/command/command_completed", 2), ("openWB/internal_chargepoint/#", 2), + ("openWB/internal_socket/#", 2), # MQTT Bridge Topics vor "openWB/system/+" abonnieren, damit sie auch vor # "openWB/system/subdata_initialized" empfangen werden! ("openWB/system/mqtt/bridge/+", 2), @@ -205,6 +208,8 @@ def on_message(self, client: mqtt.Client, userdata, msg: mqtt.MQTTMessage): self.process_io_topic(self.io_states, msg) elif "openWB/internal_chargepoint/" in msg.topic: self.process_internal_chargepoint_topic(client, self.internal_chargepoint_data, msg) + elif "openWB/internal_socket/" in msg.topic: + self.process_internal_socket_topic(msg) elif "openWB/optional/" in msg.topic: self.process_optional_topic(self.optional_data, msg) elif "openWB/counter/" in msg.topic: @@ -1250,6 +1255,15 @@ def set_internal_chargepoint_configured(self): internal_configured = False self.internal_chargepoint_data["global_data"].configured = internal_configured + def process_internal_socket_topic(self, msg): + try: + if msg.topic.startswith("openWB/internal_socket/data/"): + self.set_json_payload_class(self.internal_socket_data.data, msg) + elif msg.topic.startswith("openWB/internal_socket/get/"): + self.set_json_payload_class(self.internal_socket_data.get, msg) + except Exception: + log.exception("Fehler im subdata-Modul für die interne Steckdose") + def process_legacy_smarthome_topic(self, client: mqtt.Client, var: counter_all.CounterAll, msg: mqtt.MQTTMessage): """ Handler für die SmartHome-Topics des alten diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index e74a8f2dfe..2de067e918 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -365,6 +365,21 @@ class UpdateConfig: "^openWB/internal_chargepoint/[0-1]/get/rfid$", "^openWB/internal_chargepoint/[0-1]/get/rfid_timestamp$", + "^openWB/internal_socket/data/set_state$", + "^openWB/internal_socket/data/detected$", + "^openWB/internal_socket/data/last_tag$", + "^openWB/internal_socket/get/state$", + "^openWB/internal_socket/get/rfid$", + "^openWB/internal_socket/get/power$", + "^openWB/internal_socket/get/imported$", + "^openWB/internal_socket/get/exported$", + "^openWB/internal_socket/get/currents$", + "^openWB/internal_socket/get/voltages$", + "^openWB/internal_socket/get/powers$", + "^openWB/internal_socket/get/fault_state$", + "^openWB/internal_socket/get/fault_str$", + "^openWB/internal_socket/get/error_timestamp$", + "^openWB/io/action/[0-9]+/config$", "^openWB/io/action/[0-9]+/timestamp$", "^openWB/io/states/[0-9]+/get/analog_input$", diff --git a/packages/main.py b/packages/main.py index d8986737e6..84bc939775 100755 --- a/packages/main.py +++ b/packages/main.py @@ -308,6 +308,7 @@ def schedule_jobs(): handler = HandlerAlgorithm() prep = prepare.Prepare() general_internal_chargepoint_handler = GeneralInternalChargepointHandler() + general_internal_chargepoint_handler.auto_detect_internal_socket() rfid = RfidReader() event_ev_template = Event() event_ev_template.set() diff --git a/packages/modules/common/component_type.py b/packages/modules/common/component_type.py index 2f8cb58a65..d4820bba31 100644 --- a/packages/modules/common/component_type.py +++ b/packages/modules/common/component_type.py @@ -10,6 +10,7 @@ class ComponentType(Enum): COUNTER = "counter" FLEXIBLE_TARIFF = "dynamic_tariff" GRID_FEE = "grid_tariff" + INTERNAL_SOCKET = "internal_socket" INVERTER = "inverter" IO = "io" VEHICLE = "vehicle" @@ -37,6 +38,8 @@ def type_to_topic_mapping(component_type: str) -> str: return "consumer" elif "counter" in component_type: return "counter" + elif "internal_socket" in component_type: + return ComponentType.INTERNAL_SOCKET.value elif "inverter" in component_type: return "pv" elif ComponentType.FLEXIBLE_TARIFF.value in component_type: diff --git a/packages/modules/common/fault_state.py b/packages/modules/common/fault_state.py index 499725ac52..2d3e23247d 100644 --- a/packages/modules/common/fault_state.py +++ b/packages/modules/common/fault_state.py @@ -50,7 +50,8 @@ def store_error(self) -> None: traceback.format_exc()) topic = component_type.type_to_topic_mapping(self.component_info.type) if (self.component_info.type == component_type.ComponentType.FLEXIBLE_TARIFF.value or - self.component_info.type == component_type.ComponentType.GRID_FEE.value): + self.component_info.type == component_type.ComponentType.GRID_FEE.value or + self.component_info.type == component_type.ComponentType.INTERNAL_SOCKET.value): topic_prefix = f"openWB/set/{topic}" else: topic_prefix = f"openWB/set/{topic}/{self.component_info.id}" diff --git a/packages/modules/common/hardware_check.py b/packages/modules/common/hardware_check.py index 59c0a8c616..0d0ff31c15 100644 --- a/packages/modules/common/hardware_check.py +++ b/packages/modules/common/hardware_check.py @@ -1,38 +1,19 @@ -import logging -import time -import pymodbus -from typing import Any, Optional, Protocol, Tuple, Union +from typing import Optional -from modules.common.component_state import CounterState, EvseState -from modules.common.evse import Evse +from modules.common.component_state import CounterState from modules.common.fault_state import FaultState -from modules.common.modbus import ModbusSerialClient_, ModbusTcpClient_ -log = logging.getLogger(__name__) - -EVSE_MIN_FIRMWARE = 7 -MAX_ATTEMPTS = 3 -RETRY_DELAY_SECONDS = 0.3 - -READ_RS485_ADAPTER_AGAIN = "Erneutes Auslesen des {}" -READ_USB_ADAPTER_AGAIN = READ_RS485_ADAPTER_AGAIN.format('USB-Adapters') -READ_LAN_ADAPTER_AGAIN = READ_RS485_ADAPTER_AGAIN.format('LAN-Konverters') -METER_PROBLEM = "Erneutes Auslesen des Zählers" READ_METER_VOLTAGES_AGAIN = "Erneutes Auslesen der Spannungen am Zähler: {}V" -METER_NO_SERIAL_NUMBER = ("Die Seriennummer des Zählers für das Ladelog kann nicht ausgelesen werden. Wenn Du die " - "Seriennummer für Abrechnungszwecke benötigst, wende Dich bitte an unseren Support. Die " - "Funktionalität wird dadurch nicht beeinträchtigt!") -READ_EVSE_AGAIN = "Erneutes Auslesen der EVSE" def check_meter_values(counter_state: CounterState, fault_state: Optional[FaultState] = None) -> None: - meter_msg = _check_meter_values(counter_state) + meter_msg = check_meter(counter_state) if fault_state and meter_msg: fault_state.warning(meter_msg) -def _check_meter_values(counter_state: CounterState) -> Optional[str]: +def check_meter(counter_state: CounterState) -> Optional[str]: # Nur prüfen, dass keine Phase ausgefallen ist # Es gibt einige Fälle, in denen die Normtoleranzen der Netzspannung nicht eingehalten werden, aber kein Defekt # vorliegt und der Kunde nicht eingreifen muss. Dann soll keine Warnung angezeigt werden. @@ -42,89 +23,3 @@ def _check_meter_values(counter_state: CounterState) -> Optional[str]: if (voltages[1] == 0 and voltages[2] > 30) or voltages[0] == 0: return READ_METER_VOLTAGES_AGAIN.format(voltages) return None - - -class ClientHandlerProtocol(Protocol): - @property - def client(self) -> Union[ModbusSerialClient_, ModbusTcpClient_]: ... - @property - def local_charge_point_num(self) -> int: ... - @property - def fault_state(self) -> FaultState: ... - @property - def evse_client(self) -> Evse: ... - @property - def meter_client(self) -> Any: ... - @property - def read_error(self) -> int: ... - @property - def handle_exception(self, exception: Exception) -> bool: ... - @property - def request_and_check_hardware(self, fault_state: FaultState) -> Tuple[EvseState, CounterState]: ... - @property - def check_meter(self) -> Tuple[bool, Optional[str], CounterState]: ... - - -class SeriesHardwareCheckMixin: - def __init__(self) -> None: - pass - - def handle_exception(self: ClientHandlerProtocol, exception: Exception): - # separated for test purposes - if (isinstance(self.client, ModbusTcpClient_) and - isinstance(exception, pymodbus.exceptions.ConnectionException)): - raise exception - else: - return False - - def request_and_check_hardware(self: ClientHandlerProtocol, - fault_state: FaultState) -> Tuple[EvseState, CounterState]: - evse_check_passed = False - evse_state: EvseState - # 2x Retry bei EVSE-Auslesen vor dem Absetzen einer Fehlermeldung - try: - with self.client: - for attempt in range(MAX_ATTEMPTS): - try: - evse_state = self.evse_client.get_evse_state() - evse_check_passed = True - break - except Exception as e: - evse_check_passed = self.handle_exception(e) - # nur warten, wenn danach noch ein Versuch folgt - if attempt < MAX_ATTEMPTS - 1 and evse_check_passed is False: - time.sleep(RETRY_DELAY_SECONDS) - except Exception as e: - evse_check_passed = self.handle_exception(e) - meter_check_passed, meter_error_msg, counter_state = self.check_meter() - if meter_check_passed is False and evse_check_passed is False: - if isinstance(self.client, ModbusTcpClient_): - raise Exception(READ_LAN_ADAPTER_AGAIN) - else: - raise Exception(READ_USB_ADAPTER_AGAIN) - if meter_check_passed is False: - if evse_check_passed is False: - if isinstance(self.client, ModbusTcpClient_): - raise Exception(READ_LAN_ADAPTER_AGAIN) - else: - raise Exception(READ_USB_ADAPTER_AGAIN) - else: - raise Exception(meter_error_msg) - elif evse_check_passed and meter_check_passed and meter_error_msg is not None: - fault_state.warning(meter_error_msg) - if evse_check_passed is False: - if meter_error_msg is not None: - raise Exception(f"{READ_EVSE_AGAIN} {meter_error_msg}") - else: - raise Exception(READ_EVSE_AGAIN) - return evse_state, counter_state - - def check_meter(self: ClientHandlerProtocol) -> Tuple[bool, Optional[str], CounterState]: - try: - with self.client: - counter_state = self.meter_client.get_counter_state() - if counter_state.serial_number == "0" or counter_state.serial_number is None: - log.warning(METER_NO_SERIAL_NUMBER) - return True, _check_meter_values(counter_state), counter_state - except Exception: - return False, METER_PROBLEM, None diff --git a/packages/modules/common/hardware_check_test.py b/packages/modules/common/hardware_check_test.py index 6da1ae58b1..c567e6b633 100644 --- a/packages/modules/common/hardware_check_test.py +++ b/packages/modules/common/hardware_check_test.py @@ -7,12 +7,12 @@ from modules.common import hardware_check from modules.common.component_state import CounterState, EvseState from modules.common.evse import Evse -from modules.common.hardware_check import ( - READ_EVSE_AGAIN, READ_LAN_ADAPTER_AGAIN, READ_METER_VOLTAGES_AGAIN, METER_PROBLEM, READ_USB_ADAPTER_AGAIN, - SeriesHardwareCheckMixin, _check_meter_values) +from modules.common.hardware_check import READ_METER_VOLTAGES_AGAIN, check_meter from modules.common.modbus import NO_CONNECTION, ModbusSerialClient_, ModbusTcpClient_ from modules.conftest import SAMPLE_IP, SAMPLE_PORT from modules.internal_chargepoint_handler.clients import ClientHandler +from modules.internal_chargepoint_handler.internal_hardware_check import ( + METER_PROBLEM, READ_EVSE_AGAIN, READ_LAN_ADAPTER_AGAIN, READ_USB_ADAPTER_AGAIN, SeriesHardwareCheckMixin) @pytest.mark.parametrize( @@ -129,13 +129,13 @@ def test_check_meter_values_voltages(voltages, power, expected_msg, monkeypatch) # setup counter_state = Mock(voltages=voltages, currents=[0, 0, 0], powers=[0, 0, 0], power=power) # execution - msg = _check_meter_values(counter_state) + msg = check_meter(counter_state) # assert assert msg == expected_msg if expected_msg is None else expected_msg.format(voltages) -@patch('modules.common.hardware_check.ClientHandlerProtocol') +@patch('modules.internal_chargepoint_handler.internal_hardware_check.ClientHandlerProtocol') @pytest.mark.parametrize("serial_number, voltages, expected", [(12345, [230]*3, (True, None, CounterState)), (Exception(), [230]*3, (False, METER_PROBLEM, None))]) @@ -161,7 +161,7 @@ def test_check_meter( monkeypatch.setattr(hardware_check, "check_meter_values", mock_check_meter_values) # Act - result = mixin.check_meter(MockClientHandlerProtocol) + result = mixin.check_meter(MockClientHandlerProtocol, mock_meter_client) # Assert assert result[0] == expected[0] diff --git a/packages/modules/common/store/__init__.py b/packages/modules/common/store/__init__.py index 7ce3c7f905..f337f2c722 100644 --- a/packages/modules/common/store/__init__.py +++ b/packages/modules/common/store/__init__.py @@ -4,5 +4,6 @@ from modules.common.store._chargepoint_internal import get_internal_chargepoint_value_store from modules.common.store._factory import get_component_value_store from modules.common.store._io import get_io_value_store +from modules.common.store._socket_internal import get_internal_socket_value_store from modules.common.store._tariff import get_flexible_tariff_value_store, get_grid_fee_value_store from modules.common.store._forecast import get_forecast_value_store diff --git a/packages/modules/common/store/_socket_internal.py b/packages/modules/common/store/_socket_internal.py new file mode 100644 index 0000000000..33a6f6c506 --- /dev/null +++ b/packages/modules/common/store/_socket_internal.py @@ -0,0 +1,23 @@ +from modules.common.component_state import ConsumerState +from modules.common.store._api import LoggingValueStore, ValueStore +from modules.common.store._broker import pub_to_broker + + +class InternalSocketValueStore(ValueStore[ConsumerState]): + def set(self, state: ConsumerState) -> None: + self.state = state + + def update(self): + topic_prefix = "openWB/set/internal_socket/get" + pub_to_broker(f"{topic_prefix}/voltages", self.state.voltages, 2) + pub_to_broker(f"{topic_prefix}/currents", self.state.currents, 2) + pub_to_broker(f"{topic_prefix}/imported", self.state.imported, 2) + pub_to_broker(f"{topic_prefix}/exported", self.state.exported, 2) + pub_to_broker(f"{topic_prefix}/power", self.state.power, 2) + pub_to_broker(f"{topic_prefix}/powers", self.state.powers, 2) + pub_to_broker(f"{topic_prefix}/state", self.state.state) + pub_to_broker(f"{topic_prefix}/rfid", self.state.rfid) + + +def get_internal_socket_value_store() -> ValueStore[ConsumerState]: + return LoggingValueStore(InternalSocketValueStore()) diff --git a/packages/modules/configuration.py b/packages/modules/configuration.py index 0219453ac8..51ba0984bf 100644 --- a/packages/modules/configuration.py +++ b/packages/modules/configuration.py @@ -5,6 +5,7 @@ import dataclass_utils from helpermodules.pub import Pub +from modules.devices.vendors import READABLE_GROUP_NAME as READABLE_VENDOR_GROUP_NAME, VendorGroup from modules.io_actions.groups import READABLE_GROUP_NAME, ActionGroup import sys log = logging.getLogger(__name__) @@ -237,16 +238,6 @@ def update_nested_dict(dictionary: Dict, update: Dict) -> Dict: return dictionary def get_vendor_groups() -> Dict: - def get_vendor_group_name(group: str) -> str: - # ToDo: find a better way to lookup the group names in "devices/vendors.py" - if group == "generic": - return "herstellerunabhängig" - if group == "openwb": - return "openWB" - if group == "vendors": - return "andere Hersteller" - return group - vendor_groups = {} path_list = Path(_get_packages_path()/"modules"/"devices").glob('**/vendor.py') for path in path_list: @@ -259,7 +250,7 @@ def get_vendor_group_name(group: str) -> str: f".devices.{vendor_path}.vendor", "modules").vendor_descriptor.configuration_factory() entry = { vendor_info.group: { - "group_name": get_vendor_group_name(vendor_info.group), + "group_name": READABLE_VENDOR_GROUP_NAME[VendorGroup(vendor_info.group)], "vendors": { vendor_path: { "vendor_name": vendor_info.vendor, @@ -484,16 +475,6 @@ def get_vendor_consumers(vendor: str) -> Dict: log.exception(f"Fehler im configuration-Modul: devices: {path}") return consumers - def get_vendor_group_name(group: str) -> str: - # ToDo: find a better way to lookup the group names in "devices/vendors.py" - if group == "generic": - return "herstellerunabhängig" - if group == "openwb": - return "openWB" - if group == "vendors": - return "andere Hersteller" - return group - vendor_groups = {} path_list = Path(_get_packages_path()/"modules"/"consumers").glob('**/vendor.py') for path in path_list: @@ -506,7 +487,7 @@ def get_vendor_group_name(group: str) -> str: f".consumers.{vendor_path}.vendor", "modules").vendor_descriptor.configuration_factory() vendor_groups = update_nested_dict(vendor_groups, { vendor_info.group: { - "group_name": get_vendor_group_name(vendor_info.group), + "group_name": READABLE_VENDOR_GROUP_NAME[VendorGroup(vendor_info.group)], "vendors": { vendor_path: { "vendor_name": vendor_info.vendor, diff --git a/packages/modules/consumers/openwb/__init__.py b/packages/modules/consumers/openwb/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/openwb/internal_socket/__init__.py b/packages/modules/consumers/openwb/internal_socket/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/packages/modules/consumers/openwb/internal_socket/config.py b/packages/modules/consumers/openwb/internal_socket/config.py new file mode 100644 index 0000000000..1161bfba43 --- /dev/null +++ b/packages/modules/consumers/openwb/internal_socket/config.py @@ -0,0 +1,24 @@ +from typing import Optional, Tuple + +from control.consumer.usage import ConsumerUsage +from helpermodules.auto_str import auto_str +from modules.common.consumer_setup import ConsumerSetup +from ..vendor import vendor_descriptor + + +@auto_str +class InternalSocketConfiguration: + def __init__(self, chargepoint_id: Optional[int] = None): + self.chargepoint_id = chargepoint_id + + +@auto_str +class InternalSocket(ConsumerSetup[InternalSocketConfiguration]): + def __init__(self, name: str = "Lokale Steckdose (SDM120)", + type: str = "internal_socket", + id: int = 0, + configuration: Optional[InternalSocketConfiguration] = None, + usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_ONOFF, + ConsumerUsage.METER_ONLY), **kwargs): + super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory().type, + configuration=configuration or InternalSocketConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/openwb/internal_socket/consumer.py b/packages/modules/consumers/openwb/internal_socket/consumer.py new file mode 100644 index 0000000000..43edf03794 --- /dev/null +++ b/packages/modules/consumers/openwb/internal_socket/consumer.py @@ -0,0 +1,79 @@ +import logging + +from control import data +from helpermodules.broker import BrokerClient +from helpermodules.pub import pub_single +from helpermodules.utils._get_default import get_default +from helpermodules.utils.topic_parser import decode_payload +from modules.common.abstract_device import DeviceDescriptor +from modules.common.component_state import ConsumerState +from modules.common.configurable_consumer import ConfigurableConsumer +from modules.consumers.openwb.internal_socket.config import InternalSocket + +log = logging.getLogger(__name__) + + +def create_consumer(config: InternalSocket): + def clear_rfid(): + pub_single("openWB/set/internal_socket/data/last_tag", None, hostname=get_host()) + + def get_host(): + if config.configuration.chargepoint_id is None: + return "localhost" + try: + cp_module = data.data.cp_data[f"cp{config.configuration.chargepoint_id}"].chargepoint_module + except KeyError: + raise KeyError(("Bitte einen vorhandenen openWB-Ladepunkt auswählen.")) + if cp_module.config.type == "internal_openwb": + return "localhost" + return cp_module.config.configuration.ip_address + + def update(): + received_topics = {} + + def parse_received_topics(value: str): + return received_topics.get(f"{prefix}{value}", get_default(ConsumerState, value)) + + def on_connect(client, userdata, flags, rc): + client.subscribe("openWB/internal_socket/get/#") + + def on_message(client, userdata, message): + received_topics[message.topic] = decode_payload(message.payload) + + BrokerClient(f"subscribeSocketConsumer{config.id}", on_connect, on_message, + host=get_host(), port=1886 if get_host() == "localhost" else 1883).start_finite_loop(timeout=2) + prefix = "openWB/internal_socket/get/" + log.debug(f"Empfange MQTT Daten für Verbraucher {config.id}: {received_topics}") + try: + fault_state = received_topics.get(f"{prefix}fault_state") + if fault_state == 2: + consumer.fault_state.error(received_topics.get( + f"{prefix}fault_str", "Fehlerstatus vom internen Ladepunkt")) + elif fault_state == 1: + consumer.fault_state.warning(received_topics.get( + f"{prefix}fault_str", "Warnung vom internen Ladepunkt")) + return ConsumerState( + power=parse_received_topics("power"), + imported=parse_received_topics("imported"), + exported=parse_received_topics("exported"), + powers=parse_received_topics("powers"), + voltages=parse_received_topics("voltages"), + currents=parse_received_topics("currents"), + temperatures=parse_received_topics("temperatures"), + rfid=parse_received_topics("rfid"), + ) + except KeyError: + raise KeyError("Es wurden nicht alle notwendigen Daten empfangen.") + + def switch(state): + pub_single("openWB/set/internal_socket/data/set_state", state, hostname=get_host()) + + consumer = ConfigurableConsumer(consumer_config=config, + update=update, + switch_on=lambda: switch(True), + switch_off=lambda: switch(False), + clear_rfid=clear_rfid) + return consumer + + +device_descriptor = DeviceDescriptor(configuration_factory=InternalSocket) diff --git a/packages/modules/consumers/openwb/internal_socket/consumer_test.py b/packages/modules/consumers/openwb/internal_socket/consumer_test.py new file mode 100644 index 0000000000..7cc99cd899 --- /dev/null +++ b/packages/modules/consumers/openwb/internal_socket/consumer_test.py @@ -0,0 +1,59 @@ +import json +from types import SimpleNamespace +from unittest.mock import Mock + +import pytest + +from control import data +from modules.consumers.openwb.internal_socket import consumer as socket_consumer +from modules.consumers.openwb.internal_socket.config import InternalSocket, InternalSocketConfiguration + + +@pytest.mark.parametrize("tag", ["0012345678", None]) +@pytest.mark.parametrize("cp_type, host, duo_num", [ + ("internal_openwb", "localhost", 0), + ("external_openwb", "192.168.1.40", 1), +]) +def test_socket_rfid_uses_selected_reader_and_clears_only_on_request( + tag, cp_type, host, duo_num, monkeypatch, mock_pub): + cp_module = Mock(config=SimpleNamespace( + type=cp_type, configuration=SimpleNamespace(ip_address=host, duo_num=duo_num))) + data.data_init(Mock()) + monkeypatch.setattr(data.data, "cp_data", {"cp4": SimpleNamespace(chargepoint_module=cp_module)}) + rfid_topic = "openWB/internal_socket/get/rfid" + + def broker_factory(name, on_connect, on_message, **kwargs): + assert kwargs == {"host": host, "port": 1886 if host == "localhost" else 1883} + + def receive(timeout): + client = Mock() + on_connect(client, None, None, 0) + assert [call.args[0] for call in client.subscribe.call_args_list] == [ + "openWB/internal_socket/get/#"] + topics = {"openWB/internal_socket/get/power": 100, rfid_topic: tag, + "openWB/internal_chargepoint/9/get/rfid": "wrong-reader"} + for topic, value in topics.items(): + on_message(None, None, Mock(topic=topic, payload=json.dumps(value).encode())) + return Mock(start_finite_loop=receive) + + configurable = Mock() + monkeypatch.setattr(socket_consumer, "BrokerClient", broker_factory) + monkeypatch.setattr(socket_consumer, "ConfigurableConsumer", configurable) + publisher = Mock() + monkeypatch.setattr(socket_consumer, "pub_single", publisher) + socket_consumer.create_consumer(InternalSocket(id=1, configuration=InternalSocketConfiguration(chargepoint_id=4))) + + mock_pub.reset_mock() + state = configurable.call_args.kwargs["update"]() + + assert state.power == 100 + assert state.rfid == tag + publisher.assert_not_called() + cp_module.clear_rfid.assert_not_called() + + publisher.reset_mock() + configurable.call_args.kwargs["clear_rfid"]() + + assert [call.args for call in publisher.call_args_list] == [ + ("openWB/set/internal_socket/data/last_tag", None)] + assert all(call.kwargs == {"hostname": host} for call in publisher.call_args_list) diff --git a/packages/modules/consumers/openwb/internal_socket/selection.py b/packages/modules/consumers/openwb/internal_socket/selection.py new file mode 100644 index 0000000000..b3f24069e2 --- /dev/null +++ b/packages/modules/consumers/openwb/internal_socket/selection.py @@ -0,0 +1,30 @@ +"""Validate that each physical openWB socket belongs to one consumer.""" +from helpermodules.subdata import SubData + + +def socket_host(configuration): + chargepoint_id = getattr(configuration, "chargepoint_id", None) + if chargepoint_id is None: + return "localhost" + cp = SubData.cp_data.get(f"cp{chargepoint_id}") + if cp is None: + raise ValueError("Bitte einen vorhandenen openWB-Ladepunkt auswählen.") + setup = cp.chargepoint.chargepoint_module.config + if setup.type == "internal_openwb": + return "localhost" + if setup.type == "external_openwb": + host = setup.configuration.ip_address + if host and host not in ("localhost", "127.0.0.1", "::1"): + return host + raise ValueError("Bitte einen internen oder Secondary-openWB-Ladepunkt mit gültiger IP auswählen.") + + +def validate_selection(configuration, consumer_id=None): + host = socket_host(configuration) + for consumer in SubData.consumer_data.values(): + module = consumer.data.module + if consumer.num == consumer_id or getattr(module, "type", None) != "internal_socket": + continue + if socket_host(module.configuration) == host: + raise ValueError("Die Steckdose dieser openWB kann nur einem Verbraucher zugeordnet werden.") + return host diff --git a/packages/modules/consumers/openwb/vendor.py b/packages/modules/consumers/openwb/vendor.py new file mode 100644 index 0000000000..f07445ac46 --- /dev/null +++ b/packages/modules/consumers/openwb/vendor.py @@ -0,0 +1,14 @@ +from pathlib import Path + +from modules.common.abstract_device import DeviceDescriptor +from modules.devices.vendors import VendorGroup + + +class Vendor: + def __init__(self): + self.type = Path(__file__).parent.name + self.vendor = "openWB" + self.group = VendorGroup.OPENWB.value + + +vendor_descriptor = DeviceDescriptor(configuration_factory=Vendor) diff --git a/packages/modules/devices/vendors.py b/packages/modules/devices/vendors.py index 7dabd03ad1..0fe4497444 100644 --- a/packages/modules/devices/vendors.py +++ b/packages/modules/devices/vendors.py @@ -5,3 +5,10 @@ class VendorGroup(Enum): GENERIC = "generic" OPENWB = "openwb" VENDORS = "vendors" + + +READABLE_GROUP_NAME = { + VendorGroup.GENERIC: "herstellerunabhängig", + VendorGroup.OPENWB: "openWB", + VendorGroup.VENDORS: "andere Hersteller", +} diff --git a/packages/modules/internal_chargepoint_handler/clients.py b/packages/modules/internal_chargepoint_handler/clients.py index a66175c0c2..05633253a9 100644 --- a/packages/modules/internal_chargepoint_handler/clients.py +++ b/packages/modules/internal_chargepoint_handler/clients.py @@ -1,35 +1,21 @@ import logging -from typing import List, NamedTuple, Optional, Tuple, Union +from typing import List, Optional, Tuple, Union from helpermodules.logger import ModifyLoglevelContext from modules.chargepoints.internal_openwb.config import InternalChargepointMode from modules.common.fault_state import FaultState -from modules.common.hardware_check import SeriesHardwareCheckMixin from modules.common.modbus import ModbusSerialClient_, ModbusTcpClient_ -from modules.common import mpm3pm, sdm +from modules.common import sdm from modules.common import evse -from modules.common import b23 from modules.common.serial_modbus_devices import get_serial_modbus_devices, BUS_SOURCES +from modules.internal_chargepoint_handler.hardware_definition import ( + CP0_METERS, CP1_METERS, EVSE_ID_CP0, EVSE_ID_ONE_BUS_CP1, EVSE_ID_ONE_BUS_SE_CP1, + EVSE_ID_SE_CP0, EVSE_ID_TWO_BUSSES_CP1, EVSE_MIN_FIRMWARE, SOCKET_METERS, MeterClient, meter_config) +from modules.internal_chargepoint_handler.internal_hardware_check import SeriesHardwareCheckMixin log = logging.getLogger(__name__) -MeterClient = Union[mpm3pm.Mpm3pm, sdm.Sdm630_72, b23.B23] -MeterType = Union[type[mpm3pm.Mpm3pm], type[sdm.Sdm630_72], type[b23.B23]] -meter_config = NamedTuple("meter_config", [('type', MeterType), ('modbus_id', int)]) -CP0_METERS = [meter_config(mpm3pm.Mpm3pm, modbus_id=5), - meter_config(sdm.Sdm630_72, modbus_id=105), - meter_config(b23.B23, modbus_id=201)] - -CP1_METERS = [meter_config(mpm3pm.Mpm3pm, modbus_id=6), meter_config(sdm.Sdm630_72, modbus_id=106)] - -EVSE_ID_CP0 = [1] -EVSE_ID_SE_CP0 = [11] -EVSE_ID_TWO_BUSSES_CP1 = [1, 2] -EVSE_ID_ONE_BUS_CP1 = [2] -EVSE_ID_ONE_BUS_SE_CP1 = [12] -EVSE_MIN_FIRMWARE = 7 - class ClientHandler(SeriesHardwareCheckMixin): def __init__(self, @@ -42,6 +28,7 @@ def __init__(self, self.evse_client = self._evse_factory(client, evse_ids) self.meter_client = self.find_meter_client(CP0_METERS if self.local_charge_point_num == 0 else CP1_METERS, client, fault_state) + self.socket_meter_client: Optional[sdm.Sdm120] = self.find_meter_client(SOCKET_METERS, client, fault_state) self.request_and_check_hardware(fault_state) self.read_error = 0 @@ -98,6 +85,9 @@ def get_pins_cp_interruption(self) -> int: else: return 15 + def get_pins_socket(self) -> Tuple[int, int]: + return 23, 26 # BOARD numbering: direction BCM 11, enable BCM 7 + def client_factory(mode: InternalChargepointMode, local_charge_point_num: int, diff --git a/packages/modules/internal_chargepoint_handler/hardware_definition.py b/packages/modules/internal_chargepoint_handler/hardware_definition.py new file mode 100644 index 0000000000..5581b6f7ce --- /dev/null +++ b/packages/modules/internal_chargepoint_handler/hardware_definition.py @@ -0,0 +1,23 @@ +from typing import NamedTuple, Union + +from modules.common import mpm3pm, sdm +from modules.common import b23 + + +MeterClient = Union[mpm3pm.Mpm3pm, sdm.Sdm120, sdm.Sdm630_72, b23.B23] +MeterType = Union[type[mpm3pm.Mpm3pm], type[sdm.Sdm630_72], type[b23.B23], type[sdm.Sdm120]] +meter_config = NamedTuple("meter_config", [('type', MeterType), ('modbus_id', int)]) +CP0_METERS = [meter_config(mpm3pm.Mpm3pm, modbus_id=5), + meter_config(sdm.Sdm630_72, modbus_id=105), + meter_config(b23.B23, modbus_id=201)] + +CP1_METERS = [meter_config(mpm3pm.Mpm3pm, modbus_id=6), meter_config(sdm.Sdm630_72, modbus_id=106)] + +SOCKET_METERS = [meter_config(sdm.Sdm120, modbus_id=54)] + +EVSE_ID_CP0 = [1] +EVSE_ID_SE_CP0 = [11] +EVSE_ID_TWO_BUSSES_CP1 = [1, 2] +EVSE_ID_ONE_BUS_CP1 = [2] +EVSE_ID_ONE_BUS_SE_CP1 = [12] +EVSE_MIN_FIRMWARE = 7 diff --git a/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py b/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py index ba5eaef0df..8fbad2fb0d 100644 --- a/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py +++ b/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py @@ -7,16 +7,22 @@ from typing import Optional from helpermodules import timecheck -from helpermodules.logger import clear_in_memory_log_handler +from helpermodules import pub +from helpermodules.logger import ModifyLoglevelContext, clear_in_memory_log_handler from helpermodules.pub import Pub from helpermodules.subdata import SubData from modules.chargepoints.internal_openwb.config import InternalChargepointMode from modules.common.component_context import SingleComponentUpdateContext from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.modbus import ModbusSerialClient_ +from modules.common.serial_modbus_devices import BUS_SOURCES, get_serial_modbus_devices from modules.internal_chargepoint_handler import chargepoint_module from modules.internal_chargepoint_handler.clients import ClientHandler, client_factory +from modules.internal_chargepoint_handler.hardware_definition import SOCKET_METERS from modules.internal_chargepoint_handler.pro_plus import ProPlus from modules.internal_chargepoint_handler.socket import Socket +from modules.internal_chargepoint_handler.internal_socket import InternalSocketModule +from modules.internal_chargepoint_handler.internal_socket_config import InternalSocket, InternalSocketData from modules.internal_chargepoint_handler.internal_chargepoint_handler_config import ( GlobalHandlerData, InternalChargepoint, InternalChargepointData, RfidData) log = logging.getLogger(__name__) @@ -83,14 +89,26 @@ def __thread_cp_interruption(self, duration: int) -> None: f"openWB/internal_chargepoint/{self.cp_module.local_charge_point_num}/data/cp_interruption_duration", 0) +class UpdateInternalSocketState: + def __init__(self, socket_module: InternalSocketModule) -> None: + self.module = socket_module + + def update_state(self, data: InternalSocketData, heartbeat_expired: bool) -> None: + if not data.set_state or heartbeat_expired: + self.module.set_state(False) + else: + self.module.set_state(True) + + class InternalChargepointHandler: def __init__(self, mode: InternalChargepointMode, global_data: GlobalHandlerData, - parent_cp0: str, + parent_cp0: InternalChargepoint, hierarchy_id_cp0: int, - parent_cp1: Optional[str], + parent_cp1: Optional[InternalChargepoint], hierarchy_id_cp1: Optional[int], + parent_socket: InternalSocket, event_start: Event, event_stop: Event) -> None: log.debug(f"Init internal chargepoint as {mode}") @@ -99,25 +117,43 @@ def __init__(self, self.heartbeat = False self.fault_state_info_cp0 = FaultState( ComponentInfo(0, "Interner Ladepunkt 0", "internal_chargepoint", hierarchy_id=hierarchy_id_cp0)) - fault_state_info_cp1 = FaultState( - ComponentInfo(1, "Interner Ladepunkt 1", "internal_chargepoint", hierarchy_id=hierarchy_id_cp1)) + self.fault_state_info_socket = FaultState(ComponentInfo(None, "Interne Steckdose", "internal_socket")) + with SingleComponentUpdateContext(self.fault_state_info_cp0, reraise=True): - self.init_gpio() - try: - with SingleComponentUpdateContext(self.fault_state_info_cp0, reraise=True): - # Allgemeine Fehlermeldungen an LP 1: - if mode == InternalChargepointMode.PRO_PLUS: - self.cp0_client_handler = None - else: - self.cp0_client_handler = client_factory(mode, 0, self.fault_state_info_cp0) - self.cp0 = HandlerChargepoint(self.cp0_client_handler, 0, mode, - global_data, parent_cp0, hierarchy_id_cp0) - except Exception: - self.cp0_client_handler = None - self.cp0 = None + self.init_gpio(mode) + if parent_socket.data.detected: + try: + with SingleComponentUpdateContext(self.fault_state_info_cp0, reraise=True): + with SingleComponentUpdateContext(self.fault_state_info_socket, reraise=True): + if mode == InternalChargepointMode.PRO_PLUS: + self.cp0_client_handler = None + else: + self.cp0_client_handler = client_factory(mode, 0, self.fault_state_info_cp0) + self.socket = HandlerInternalSocket(self.cp0_client_handler, global_data, parent_socket) + self.cp0 = HandlerChargepoint(self.cp0_client_handler, 0, mode, + global_data, parent_cp0, hierarchy_id_cp0) + except Exception: + self.cp0_client_handler = None + self.cp0 = None + self.socket = None + else: + self.socket = None + try: + with SingleComponentUpdateContext(self.fault_state_info_cp0, reraise=True): + if mode == InternalChargepointMode.PRO_PLUS: + self.cp0_client_handler = None + else: + self.cp0_client_handler = client_factory(mode, 0, self.fault_state_info_cp0) + self.cp0 = HandlerChargepoint(self.cp0_client_handler, 0, mode, + global_data, parent_cp0, hierarchy_id_cp0) + except Exception: + self.cp0_client_handler = None + self.cp0 = None try: if ((mode == InternalChargepointMode.DUO or mode == InternalChargepointMode.SE) and - hierarchy_id_cp0 is not None): + hierarchy_id_cp1 is not None): + fault_state_info_cp1 = FaultState( + ComponentInfo(1, "Interner Ladepunkt 1", "internal_chargepoint", hierarchy_id=hierarchy_id_cp1)) with SingleComponentUpdateContext(fault_state_info_cp1, reraise=True): log.debug("Zweiter Ladepunkt für Duo konfiguriert.") self.cp1_client_handler = client_factory(mode, 1, fault_state_info_cp1, self.cp0_client_handler) @@ -130,7 +166,7 @@ def __init__(self, self.cp1 = None self.cp1_client_handler = None - def init_gpio(self) -> None: + def init_gpio(self, mode: InternalChargepointMode) -> None: GPIO.setwarnings(False) GPIO.setmode(GPIO.BOARD) GPIO.setup(37, GPIO.OUT) @@ -139,9 +175,14 @@ def init_gpio(self) -> None: GPIO.setup(29, GPIO.OUT) GPIO.setup(11, GPIO.OUT) GPIO.setup(15, GPIO.OUT) - # GPIOs for socket - GPIO.setup(23, GPIO.OUT) - GPIO.setup(26, GPIO.OUT) + + if mode in (InternalChargepointMode.SERIES, InternalChargepointMode.SE): + GPIO.setup(26, GPIO.OUT, initial=GPIO.LOW) + GPIO.setup(23, GPIO.OUT, initial=GPIO.LOW) + else: + # GPIOs for socket + GPIO.setup(23, GPIO.OUT) + GPIO.setup(26, GPIO.OUT) GPIO.setup(19, GPIO.IN, pull_up_down=GPIO.PUD_UP) def loop(self) -> None: @@ -159,7 +200,11 @@ def _loop(): heartbeat_cp0 = self.cp0.update(data["global_data"], data["cp0"].data, data["rfid_data"]) if self.cp1: heartbeat_cp1 = self.cp1.update(data["global_data"], data["cp1"].data, data["rfid_data"]) - self.heartbeat = True if heartbeat_cp0 and heartbeat_cp1 else False + if self.socket: + socket_data = copy.deepcopy(SubData.internal_socket_data.data) + heartbeat_socket = self.socket.update(data["global_data"], socket_data) + self.heartbeat = True if heartbeat_cp0 and heartbeat_cp1 and ( + self.socket is None or heartbeat_socket) else False time.sleep(1.1) # Allgemeine Fehlermeldungen an LP 1 with SingleComponentUpdateContext(self.fault_state_info_cp0, update_always=False): @@ -234,11 +279,54 @@ def _check_heartbeat_expired(self, heartbeat) -> bool: return False +class HandlerInternalSocket: + def __init__(self, + client_handler: Optional[ClientHandler], + global_data: GlobalHandlerData, + internal_socket: InternalSocket) -> None: + self.module = InternalSocketModule(client_handler, internal_socket) + self.global_data = global_data + with SingleComponentUpdateContext(self.module.fault_state): + self.update_state = UpdateInternalSocketState(self.module) + + def update(self, global_data: GlobalHandlerData, data: InternalSocketData) -> bool: + with SingleComponentUpdateContext(self.module.fault_state): + self.module.get_values(data.last_tag) + heartbeat_expired = self._check_heartbeat_expired(global_data.heartbeat) + self.update_state.update_state(data, heartbeat_expired) + return True + return False + + def _check_heartbeat_expired(self, heartbeat) -> bool: + if heartbeat+80 < timecheck.create_timestamp(): + log.error(f"Heartbeat Fehler seit {timecheck.create_timestamp()-heartbeat}" + "s keine Verbindung. Stoppe Ladung.") + return True + else: + return False + + class GeneralInternalChargepointHandler: def __init__(self) -> None: self.event_stop = Event() self.event_start = Event() + def auto_detect_internal_socket(self): + try: + device, count = get_serial_modbus_devices() + if count == 1 and device[0] in BUS_SOURCES: + port = device[0] + client = ModbusSerialClient_(port) + fault_state = FaultState(ComponentInfo(None, "Interne Steckdose", "internal_socket")) + if ClientHandler.find_meter_client(SOCKET_METERS, client, fault_state) is not None: + pub.pub_single("openWB/internal_socket/data/detected", True) + log.debug(f"Verbrauchszähler mit lokaler Auslesung nutzt Port {port}") + return + with ModifyLoglevelContext(log, logging.DEBUG): + log.debug("Es konnte keine interne Steckdose erkannt werden.") + except Exception: + log.exception("Fehler bei automatischen Erkennung einer internen Steckdose") + def handler(self): while True: try: @@ -266,6 +354,7 @@ def handler(self): hierarchy_id_cp0, data["cp1"], hierarchy_id_cp1, + copy.deepcopy(SubData.internal_socket_data), self.event_start, self.event_stop) self.internal_chargepoint_handler.loop() diff --git a/packages/modules/internal_chargepoint_handler/internal_hardware_check.py b/packages/modules/internal_chargepoint_handler/internal_hardware_check.py new file mode 100644 index 0000000000..7ab11fb69b --- /dev/null +++ b/packages/modules/internal_chargepoint_handler/internal_hardware_check.py @@ -0,0 +1,132 @@ +import logging +import time +import pymodbus +from typing import Any, Optional, Protocol, Tuple, Union + +from modules.common.component_state import CounterState, EvseState +from modules.common.evse import Evse +from modules.common.fault_state import FaultState +from modules.common.hardware_check import check_meter +from modules.common.modbus import ModbusSerialClient_, ModbusTcpClient_ +from modules.internal_chargepoint_handler.clients import MeterClient + +log = logging.getLogger(__name__) + + +EVSE_MIN_FIRMWARE = 7 +MAX_ATTEMPTS = 3 +RETRY_DELAY_SECONDS = 0.3 + +READ_RS485_ADAPTER_AGAIN = "Erneutes Auslesen des {}" +READ_USB_ADAPTER_AGAIN = READ_RS485_ADAPTER_AGAIN.format('USB-Adapters') +READ_LAN_ADAPTER_AGAIN = READ_RS485_ADAPTER_AGAIN.format('LAN-Konverters') +METER_PROBLEM = "Erneutes Auslesen des Zählers" +METER_NO_SERIAL_NUMBER = ("Die Seriennummer des Zählers für das Ladelog kann nicht ausgelesen werden. Wenn Du die " + "Seriennummer für Abrechnungszwecke benötigst, wende Dich bitte an unseren Support. Die " + "Funktionalität wird dadurch nicht beeinträchtigt!") +READ_EVSE_AGAIN = "Erneutes Auslesen der EVSE" + + +class ClientHandlerProtocol(Protocol): + @property + def client(self) -> Union[ModbusSerialClient_, ModbusTcpClient_]: ... + @property + def local_charge_point_num(self) -> int: ... + @property + def fault_state(self) -> FaultState: ... + @property + def evse_client(self) -> Evse: ... + @property + def meter_client(self) -> Any: ... + @property + def socket_meter_client(self) -> Any: ... + @property + def read_error(self) -> int: ... + @property + def handle_exception(self, exception: Exception) -> bool: ... + @property + def request_and_check_hardware(self, fault_state: FaultState) -> Tuple[EvseState, CounterState]: ... + @property + def request_and_check_socket(self, fault_state: FaultState) -> CounterState: ... + @property + def check_meter(self) -> Tuple[bool, Optional[str], CounterState]: ... + + +class SeriesHardwareCheckMixin: + def __init__(self) -> None: + pass + + def handle_exception(self: ClientHandlerProtocol, exception: Exception): + # separated for test purposes + if (isinstance(self.client, ModbusTcpClient_) and + isinstance(exception, pymodbus.exceptions.ConnectionException)): + raise exception + else: + return False + + def request_and_check_hardware(self: ClientHandlerProtocol, + fault_state: FaultState) -> Tuple[EvseState, CounterState]: + evse_check_passed = False + evse_state: EvseState + # 2x Retry bei EVSE-Auslesen vor dem Absetzen einer Fehlermeldung + try: + with self.client: + for attempt in range(MAX_ATTEMPTS): + try: + evse_state = self.evse_client.get_evse_state() + evse_check_passed = True + break + except Exception as e: + evse_check_passed = self.handle_exception(e) + # nur warten, wenn danach noch ein Versuch folgt + if attempt < MAX_ATTEMPTS - 1 and evse_check_passed is False: + time.sleep(RETRY_DELAY_SECONDS) + except Exception as e: + evse_check_passed = self.handle_exception(e) + meter_check_passed, meter_error_msg, counter_state = self.check_meter(self.meter_client) + if meter_check_passed is False and evse_check_passed is False: + if isinstance(self.client, ModbusTcpClient_): + raise Exception(READ_LAN_ADAPTER_AGAIN) + else: + raise Exception(READ_USB_ADAPTER_AGAIN) + if meter_check_passed is False: + if evse_check_passed is False: + if isinstance(self.client, ModbusTcpClient_): + raise Exception(READ_LAN_ADAPTER_AGAIN) + else: + raise Exception(READ_USB_ADAPTER_AGAIN) + else: + raise Exception(meter_error_msg) + elif evse_check_passed and meter_check_passed and meter_error_msg is not None: + fault_state.warning(meter_error_msg) + if evse_check_passed is False: + if meter_error_msg is not None: + raise Exception(f"{READ_EVSE_AGAIN} {meter_error_msg}") + else: + raise Exception(READ_EVSE_AGAIN) + return evse_state, counter_state + + def check_meter(self: ClientHandlerProtocol, meter_client: MeterClient) -> Tuple[bool, Optional[str], CounterState]: + try: + with self.client: + counter_state = meter_client.get_counter_state() + if counter_state.serial_number == "0" or counter_state.serial_number is None: + log.warning(METER_NO_SERIAL_NUMBER) + return True, check_meter(counter_state), counter_state + except Exception: + return False, METER_PROBLEM, None + + def request_and_check_socket(self: ClientHandlerProtocol, fault_state: FaultState) -> CounterState: + socket_meter_check_passed, socket_meter_error_msg, counter_state = self.check_meter(self.socket_meter_client) + if socket_meter_check_passed is False: + meter_check_passed = self.check_meter(self.meter_client)[0] + if meter_check_passed is False: + if isinstance(self.client, ModbusTcpClient_): + raise Exception(READ_LAN_ADAPTER_AGAIN) + else: + raise Exception(READ_USB_ADAPTER_AGAIN) + else: + raise Exception(socket_meter_error_msg) + if socket_meter_check_passed and socket_meter_error_msg is not None: + fault_state.warning(socket_meter_error_msg) + return counter_state diff --git a/packages/modules/internal_chargepoint_handler/internal_socket.py b/packages/modules/internal_chargepoint_handler/internal_socket.py new file mode 100644 index 0000000000..dc0fc04b78 --- /dev/null +++ b/packages/modules/internal_chargepoint_handler/internal_socket.py @@ -0,0 +1,71 @@ +import logging +import time +from typing import Optional + +from helpermodules.utils.error_handling import ErrorTimerContext +from modules.common.component_context import SingleComponentUpdateContext +from modules.common.component_state import ConsumerState +from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.store import get_internal_socket_value_store +from modules.internal_chargepoint_handler.clients import ClientHandler +from modules.internal_chargepoint_handler.internal_socket_config import InternalSocket + +log = logging.getLogger(__name__) + +try: + import RPi.GPIO as GPIO +except ImportError: + log.info("failed to import RPi.GPIO! maybe we are not running on a pi") + + +class InternalSocketModule: + def __init__(self, client_handler: ClientHandler, internal_socket: InternalSocket) -> None: + self.store_internal = get_internal_socket_value_store() + self.fault_state = FaultState(ComponentInfo(None, "Interne Steckdose", "internal_socket")) + self.client_error_context = ErrorTimerContext( + "openWB/set/internal_socket/get/error_timestamp", "Steckdose bleibt ausgeschaltet.", + hide_exception=True) + self.client_error_context.error_timestamp = internal_socket.get.error_timestamp + self._client = client_handler + self.current_state = False + self.old_socket_state = ConsumerState(power=0, currents=[0]*3, powers=[0]*3, state=False) + + def get_values(self, last_tag: Optional[str] = None) -> ConsumerState: + chargepoint_state = self.old_socket_state + with self.client_error_context: + counter_state = self._client.request_and_check_socket(self.fault_state) + + time.sleep(0.1) + self.client_error_context.reset_error_counter() + + chargepoint_state = ConsumerState( + power=counter_state.power, + currents=counter_state.currents, + imported=counter_state.imported, + exported=counter_state.exported, + powers=counter_state.powers, + voltages=counter_state.voltages, + state=self.current_state, + rfid=last_tag, + ) + + if self.client_error_context.error_counter_exceeded(): + chargepoint_state = ConsumerState( + power=0, currents=[0]*3, powers=[0]*3, state=False, + imported=self.old_socket_state.imported, exported=self.old_socket_state.exported) + + self.store_internal.set(chargepoint_state) + self.store_internal.update() + self.old_socket_state = chargepoint_state + return self.old_socket_state + + def set_state(self, state: bool) -> None: + with SingleComponentUpdateContext(self.fault_state, update_always=False): + direction_pin, enable_pin = self._client.get_pins_socket() + if state: + GPIO.output(direction_pin, GPIO.HIGH) + GPIO.output(enable_pin, GPIO.HIGH) + else: + GPIO.output(enable_pin, GPIO.LOW) + GPIO.output(direction_pin, GPIO.LOW) + self.current_state = state diff --git a/packages/modules/internal_chargepoint_handler/internal_socket_config.py b/packages/modules/internal_chargepoint_handler/internal_socket_config.py new file mode 100644 index 0000000000..5aaf567d93 --- /dev/null +++ b/packages/modules/internal_chargepoint_handler/internal_socket_config.py @@ -0,0 +1,33 @@ +from dataclasses import dataclass, field +from typing import List, Optional + +from dataclass_utils.factories import currents_list_factory, voltages_list_factory +from helpermodules.constants import NO_ERROR + + +@dataclass +class InternalSocketData: + detected: bool = False + set_state: bool = False + last_tag: Optional[str] = None + + +@dataclass +class InternalSocketGet: + rfid: Optional[str] = None + state: bool = False + power: float = 0 + imported: float = 0 + exported: float = 0 + currents: List[float] = field(default_factory=currents_list_factory) + voltages: List[float] = field(default_factory=voltages_list_factory) + powers: List[float] = field(default_factory=currents_list_factory) + fault_state: int = 0 + fault_str: str = NO_ERROR + error_timestamp: Optional[float] = None + + +@dataclass +class InternalSocket: + data: InternalSocketData = field(default_factory=InternalSocketData) + get: InternalSocketGet = field(default_factory=InternalSocketGet) diff --git a/packages/modules/internal_chargepoint_handler/internal_socket_module_test.py b/packages/modules/internal_chargepoint_handler/internal_socket_module_test.py new file mode 100644 index 0000000000..54f34ce163 --- /dev/null +++ b/packages/modules/internal_chargepoint_handler/internal_socket_module_test.py @@ -0,0 +1,38 @@ +from unittest.mock import MagicMock, Mock + +import pytest + +from modules.common.component_state import CounterState, ConsumerState +from modules.common.store import _socket_internal +from modules.internal_chargepoint_handler import internal_socket + + +@pytest.mark.parametrize("tag", ["0012345678", None]) +def test_internal_socket_measurement_publishes_current_rfid_and_clears_previous_tag(tag, monkeypatch): + publisher = Mock() + monkeypatch.setattr(_socket_internal, "pub_to_broker", publisher) + monkeypatch.setattr(internal_socket.time, "sleep", Mock()) + module = internal_socket.InternalSocketModule.__new__(internal_socket.InternalSocketModule) + module._client = Mock() + module._client.request_and_check_socket.return_value = CounterState( + power=100, imported=20, exported=0, currents=[1, 0, 0], voltages=[230, 0, 0]) + module.client_error_context = MagicMock() + module.client_error_context.error_counter_exceeded.return_value = False + module.fault_state = Mock() + module.current_state = False + module.measurement_valid = False + module.old_socket_state = ConsumerState(power=0) + module.store_internal = _socket_internal.InternalSocketValueStore() + + state = module.get_values(tag) + + assert isinstance(state, ConsumerState) + assert state.rfid == tag + assert state.power == 100 + publisher.assert_any_call("openWB/set/internal_socket/get/rfid", tag) + publisher.reset_mock() + + state = module.get_values(None) + + assert state.rfid is None + publisher.assert_any_call("openWB/set/internal_socket/get/rfid", None) diff --git a/packages/modules/internal_chargepoint_handler/internal_socket_test.py b/packages/modules/internal_chargepoint_handler/internal_socket_test.py new file mode 100644 index 0000000000..0802e217a1 --- /dev/null +++ b/packages/modules/internal_chargepoint_handler/internal_socket_test.py @@ -0,0 +1,175 @@ +import json +from types import SimpleNamespace +from unittest.mock import Mock + +import pytest + +from helpermodules import setdata +from helpermodules.subdata import SubData +from modules.internal_chargepoint_handler.internal_socket_config import InternalSocket + + +@pytest.mark.parametrize("suffix, value", [ + ("data/set_state", True), ("data/detected", True), + ("data/last_tag", "0012345678"), ("data/last_tag", None), + ("get/state", False), ("get/fault_state", 2), ("get/fault_str", "Messfehler"), + ("get/power", 500.0), ("get/imported", 1000.0), ("get/exported", 0.0), + ("get/currents", [2.2, 0, 0]), ("get/voltages", [230, 0, 0]), + ("get/powers", [500, 0, 0]), + ("get/error_timestamp", None), + ("get/rfid", "0012345678"), ("get/rfid", None), +]) +def test_internal_socket_set_topics_are_validated_and_stored(suffix, value, monkeypatch): + publisher = Mock() + monkeypatch.setattr(setdata, "Pub", Mock(return_value=publisher)) + monkeypatch.setattr(SubData, "internal_socket_data", InternalSocket()) + setter = setdata.SetData.__new__(setdata.SetData) + setter.process_internal_socket_topic(SimpleNamespace( + topic=f"openWB/set/internal_socket/{suffix}", payload=json.dumps(value).encode())) + publisher.pub.assert_any_call(f"openWB/internal_socket/{suffix}", value, retain=True) + subscriber = SubData.__new__(SubData) + subscriber.process_internal_socket_topic(SimpleNamespace( + topic=f"openWB/internal_socket/{suffix}", payload=json.dumps(value).encode())) + branch, key = suffix.split("/") + assert getattr(getattr(SubData.internal_socket_data, branch), key) == value + + +def test_handler_loop_uses_socket_data_updated_by_mqtt(monkeypatch): + from unittest.mock import MagicMock + from modules.chargepoints.internal_openwb.config import InternalChargepointMode + from modules.internal_chargepoint_handler import internal_chargepoint_handler as handler_module + from modules.internal_chargepoint_handler.internal_chargepoint_handler_config import ( + GlobalHandlerData, InternalChargepoint, RfidData) + + monkeypatch.setattr(SubData, "internal_socket_data", InternalSocket()) + subscriber = SubData.__new__(SubData) + subscriber.process_internal_socket_topic(SimpleNamespace( + topic="openWB/internal_socket/data/set_state", payload=json.dumps(True).encode())) + monkeypatch.setattr(SubData, "internal_chargepoint_data", { + "global_data": GlobalHandlerData(heartbeat=1000), "cp0": InternalChargepoint(), + "rfid_data": RfidData(last_tag="0012345678")}) + monkeypatch.setattr(SubData, "system_data", { + "system": SimpleNamespace(data={"debug_level": 10})}) + monkeypatch.setattr(handler_module.time, "sleep", Mock()) + monkeypatch.setattr(handler_module, "clear_in_memory_log_handler", Mock()) + handler = handler_module.InternalChargepointHandler.__new__(handler_module.InternalChargepointHandler) + handler.cp0 = Mock(mode=InternalChargepointMode.SERIES) + handler.cp1 = None + handler.socket = Mock() + handler.cp0_client_handler = SimpleNamespace(client=MagicMock()) + handler.cp1_client_handler = None + from modules.common.fault_state import ComponentInfo, FaultState + handler.fault_state_info_cp0 = FaultState(ComponentInfo(0, "Test", "internal_chargepoint")) + handler.event_stop = Mock(is_set=Mock(side_effect=[False, True])) + + handler.loop() + + handler.socket.update.assert_called_once() + global_data, socket_data = handler.socket.update.call_args.args + assert global_data.heartbeat == 1000 + assert socket_data.set_state is True + assert socket_data.last_tag is None + + +@pytest.mark.parametrize("heartbeat, expected_on", [(1000, True), (900, False)]) +def test_socket_handler_uses_shared_chargepoint_heartbeat(heartbeat, expected_on, monkeypatch): + from modules.common.fault_state import ComponentInfo, FaultState + from modules.internal_chargepoint_handler import internal_chargepoint_handler as handler_module + from modules.internal_chargepoint_handler.internal_chargepoint_handler_config import GlobalHandlerData + from modules.internal_chargepoint_handler.internal_socket_config import InternalSocketData + + monkeypatch.setattr(handler_module.timecheck, "create_timestamp", Mock(return_value=1000)) + handler = handler_module.HandlerInternalSocket.__new__(handler_module.HandlerInternalSocket) + handler.module = Mock(fault_state=FaultState(ComponentInfo(None, "Test", "internal_socket"))) + handler.update_state = handler_module.UpdateInternalSocketState(handler.module) + + assert handler.update(GlobalHandlerData(heartbeat=heartbeat), InternalSocketData(set_state=True)) + + handler.module.set_state.assert_called_once_with(expected_on) + chargepoint = handler_module.HandlerChargepoint.__new__(handler_module.HandlerChargepoint) + assert chargepoint._check_heartbeat_expired(heartbeat) is not expected_on + + +@pytest.mark.parametrize("first_ack", ["chargepoint", "consumer"]) +def test_rfid_scan_is_independent_of_acknowledgement_order(first_ack, monkeypatch): + import asyncio + from modules.internal_chargepoint_handler import rfid + from modules.internal_chargepoint_handler.internal_chargepoint_handler_config import RfidData + from modules.consumers.openwb.internal_socket import consumer as socket_consumer + from modules.consumers.openwb.internal_socket.config import InternalSocket as SocketConsumer + + monkeypatch.setattr(SubData, "internal_socket_data", InternalSocket()) + source = {"rfid_data": RfidData()} + monkeypatch.setattr(SubData, "internal_chargepoint_data", source) + setter = setdata.SetData.__new__(setdata.SetData) + subscriber = SubData.__new__(SubData) + + def message(topic, value): + return SimpleNamespace(topic=topic, payload=json.dumps(value).encode()) + + def publish(topic, value, **kwargs): + if value == "": + return + if topic.startswith("openWB/set/internal_socket/"): + setter.process_internal_socket_topic(message(topic, value)) + elif topic == "openWB/set/internal_chargepoint/last_tag": + setter.process_internal_chargepoint_topic(message(topic, value)) + elif topic.startswith("openWB/internal_socket/"): + subscriber.process_internal_socket_topic(message(topic, value)) + elif topic == "openWB/internal_chargepoint/last_tag": + subscriber.process_internal_chargepoint_topic(None, source, message(topic, value)) + + monkeypatch.setattr(setdata, "Pub", Mock(return_value=Mock(pub=publish))) + monkeypatch.setattr(rfid, "Pub", Mock(return_value=Mock(pub=publish))) + monkeypatch.setattr(rfid, "categorize", lambda event: event) + monkeypatch.setattr(socket_consumer, "pub_single", publish) + configurable = Mock() + monkeypatch.setattr(socket_consumer, "ConfigurableConsumer", configurable) + socket_consumer.create_consumer(SocketConsumer(id=1)) + + async def keyboard_events(): + for digit in "0012345678": + yield SimpleNamespace(type=rfid.ecodes.EV_KEY, keystate=1, + scancode=getattr(rfid.ecodes, f"KEY_{digit}")) + yield SimpleNamespace(type=rfid.ecodes.EV_KEY, keystate=1, scancode=rfid.ecodes.KEY_ENTER) + + def scan(): + reader = rfid.RfidReader.__new__(rfid.RfidReader) + device = SimpleNamespace(path="/test/rfid", async_read_loop=keyboard_events) + asyncio.run(reader._read_events(device)) + + def acknowledge_chargepoint(): + setter.process_internal_chargepoint_topic(message("openWB/set/internal_chargepoint/last_tag", None)) + + acknowledge_consumer = configurable.call_args.kwargs["clear_rfid"] + scan() + assert source["rfid_data"].last_tag == "0012345678" + assert SubData.internal_socket_data.data.last_tag == "0012345678" + + if first_ack == "chargepoint": + acknowledge_chargepoint() + assert SubData.internal_socket_data.data.last_tag == "0012345678" + acknowledge_consumer() + else: + acknowledge_consumer() + assert source["rfid_data"].last_tag == "0012345678" + acknowledge_chargepoint() + + assert source["rfid_data"].last_tag is None + assert SubData.internal_socket_data.data.last_tag is None + scan() + assert source["rfid_data"].last_tag == "0012345678" + assert SubData.internal_socket_data.data.last_tag == "0012345678" + + +@pytest.mark.parametrize("tag", ["0012345678", None]) +def test_setdata_does_not_distribute_chargepoint_tags(tag, monkeypatch): + publisher = Mock() + monkeypatch.setattr(setdata, "Pub", Mock(return_value=publisher)) + setter = setdata.SetData.__new__(setdata.SetData) + + setter.process_internal_chargepoint_topic(SimpleNamespace( + topic="openWB/set/internal_chargepoint/last_tag", payload=json.dumps(tag).encode())) + + publisher.pub.assert_any_call("openWB/internal_chargepoint/last_tag", tag, retain=True) + assert all("internal_socket" not in call.args[0] for call in publisher.pub.call_args_list) diff --git a/packages/modules/internal_chargepoint_handler/rfid.py b/packages/modules/internal_chargepoint_handler/rfid.py index 46878c99e7..72fca9db54 100644 --- a/packages/modules/internal_chargepoint_handler/rfid.py +++ b/packages/modules/internal_chargepoint_handler/rfid.py @@ -129,6 +129,7 @@ async def _read_events(self, device: InputDevice): if len(key_string) > 0: log.debug(f"RFID-String: {key_string}") Pub().pub("openWB/set/internal_chargepoint/last_tag", key_string) + Pub().pub("openWB/set/internal_socket/data/last_tag", key_string) key_string = "" else: log.debug(f"new key: {data.scancode} - {ecodes.KEY[data.scancode]}") From a587c52618c0cad9832d52114a9913fb2d48a792 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 23 Oct 2026 16:28:50 +0200 Subject: [PATCH 02/16] fix --- packages/helpermodules/command.py | 13 -- packages/helpermodules/command_test.py | 68 ------- .../openwb/internal_socket/consumer.py | 2 - .../openwb/internal_socket/selection.py | 30 --- .../internal_socket_test.py | 175 ------------------ 5 files changed, 288 deletions(-) delete mode 100644 packages/modules/consumers/openwb/internal_socket/selection.py delete mode 100644 packages/modules/internal_chargepoint_handler/internal_socket_test.py diff --git a/packages/helpermodules/command.py b/packages/helpermodules/command.py index 0bd83749c5..71ca16809d 100644 --- a/packages/helpermodules/command.py +++ b/packages/helpermodules/command.py @@ -50,8 +50,6 @@ from modules.chargepoints.internal_openwb.config import InternalChargepointMode from modules.common.component_type import ComponentType, special_to_general_type_mapping, type_to_topic_mapping from modules.common.configurable_vehicle import GeneralVehicleConfig -from modules.consumers.openwb.internal_socket.config import InternalSocketConfiguration -from modules.consumers.openwb.internal_socket.selection import validate_selection log = logging.getLogger(__name__) @@ -1202,22 +1200,11 @@ def resetUserPassword(self, connection_id: str, payload: dict) -> None: def addConsumer(self, connection_id: str, payload: dict) -> None: """ sendet das Topic, zu dem ein neues Device erstellt werden soll. """ - socket_configuration = None - if payload["data"]["vendor"] == "openwb" and payload["data"]["type"] == "internal_socket": - socket_configuration = InternalSocketConfiguration( - chargepoint_id=payload["data"].get("configuration", {}).get("chargepoint_id")) - try: - validate_selection(socket_configuration) - except ValueError as e: - pub_user_message(payload, connection_id, str(e), MessageType.ERROR) - return new_id = self.max_id_hierarchy + 1 dev = importlib.import_module(f'.consumers.{payload["data"]["vendor"]}' f'.{payload["data"]["type"]}.consumer', "modules") consumer_default = asdict(dev.device_descriptor.configuration_factory()) - if socket_configuration is not None: - consumer_default["configuration"] = asdict(socket_configuration) consumer_default["id"] = new_id try: evu_counter = SubData.counter_all_data.get_id_evu_counter() diff --git a/packages/helpermodules/command_test.py b/packages/helpermodules/command_test.py index e203bbef6e..e5e49dc9fd 100644 --- a/packages/helpermodules/command_test.py +++ b/packages/helpermodules/command_test.py @@ -186,71 +186,3 @@ def test_remove_internal_socket_switches_off_before_removing_topics(consumer_typ instance.max_id_hierarchy = 2 instance.removeConsumer("test", {"data": {"consumer_id": 2}}) assert order == (["off", "remove"] if consumer_type == "internal_socket" else ["remove"]) - - -@pytest.mark.parametrize("existing_type, requested_type, existing_cp, requested_cp, blocked", [ - ("internal_socket", "internal_socket", None, None, True), - ("mqtt", "internal_socket", None, None, False), - (None, "internal_socket", None, None, False), - ("internal_socket", "mqtt", None, None, False), - ("internal_socket", "internal_socket", None, 4, False), - ("internal_socket", "internal_socket", 4, None, False), - ("internal_socket", "internal_socket", 4, 5, False), - ("internal_socket", "internal_socket", 4, 4, True), - ("internal_socket", "internal_socket", 4, 6, True), - ("internal_socket", "internal_socket", None, 0, True), - ("internal_socket", "internal_socket", 0, None, True), -]) -def test_add_consumer_rejects_second_internal_socket_before_side_effects( - existing_type, requested_type, existing_cp, requested_cp, blocked, mock_pub, monkeypatch): - existing_module = SimpleNamespace(type=existing_type, configuration=SimpleNamespace( - chargepoint_id=existing_cp)) if existing_type else None - monkeypatch.setattr(SubData, "consumer_data", { - "consumer2": SimpleNamespace(num=2, data=SimpleNamespace(module=existing_module))}) - monkeypatch.setattr(SubData, "cp_data", { - f"cp{num}": SimpleNamespace(chargepoint=SimpleNamespace(chargepoint_module=SimpleNamespace( - config=SimpleNamespace(type="external_openwb", configuration=SimpleNamespace(ip_address=host))))) - for num, host in [(4, "192.168.1.40"), (5, "192.168.1.50"), (6, "192.168.1.40")]}) - SubData.cp_data["cp0"] = SimpleNamespace(chargepoint=SimpleNamespace(chargepoint_module=SimpleNamespace( - config=SimpleNamespace(type="internal_openwb")))) - counter = Mock() - counter.get_id_evu_counter.return_value = 0 - counter.data.get.hierarchy = [] - counter.data.get.loadmanagement_prios = [] - monkeypatch.setattr(SubData, "counter_all_data", counter) - monkeypatch.setattr(SubData, "system_data", { - "system": SimpleNamespace(data={"security": {"user_management_active": False}})}) - message = Mock() - monkeypatch.setattr(command, "pub_user_message", message) - vendor = "openwb" if requested_type == "internal_socket" else "generic" - setup = SimpleNamespace(id=0, type=requested_type, name="Test-Verbraucher", info={}, - configuration=SimpleNamespace(), vendor=vendor, - usage=(ConsumerUsage.SUSPENDABLE_ONOFF,)) - module_import = Mock(return_value=SimpleNamespace(device_descriptor=SimpleNamespace( - configuration_factory=Mock(return_value=setup)))) - monkeypatch.setattr(command.importlib, "import_module", module_import) - instance = Command.__new__(Command) - instance.max_id_hierarchy = 2 - payload = {"data": {"vendor": vendor, "type": requested_type, - "configuration": {"chargepoint_id": requested_cp}}} - - instance.addConsumer("test", payload) - - if blocked: - message.assert_called_once_with( - payload, "test", "Die Steckdose dieser openWB kann nur einem Verbraucher zugeordnet werden.", - command.MessageType.ERROR) - module_import.assert_not_called() - counter.get_id_evu_counter.assert_not_called() - counter.hierarchy_add_item_below.assert_not_called() - counter.add_loadmanagement_prio_item.assert_not_called() - mock_pub.pub.assert_not_called() - assert instance.max_id_hierarchy == 2 - else: - assert instance.max_id_hierarchy == 3 - counter.hierarchy_add_item_below.assert_called_once() - assert any(call.args[0] == "openWB/consumer/3/module" for call in mock_pub.pub.call_args_list) - if requested_type == "internal_socket": - module_payload = next(call.args[1] for call in mock_pub.pub.call_args_list - if call.args[0] == "openWB/consumer/3/module") - assert module_payload["configuration"] == {"chargepoint_id": requested_cp} diff --git a/packages/modules/consumers/openwb/internal_socket/consumer.py b/packages/modules/consumers/openwb/internal_socket/consumer.py index 43edf03794..5e9700f224 100644 --- a/packages/modules/consumers/openwb/internal_socket/consumer.py +++ b/packages/modules/consumers/openwb/internal_socket/consumer.py @@ -24,8 +24,6 @@ def get_host(): cp_module = data.data.cp_data[f"cp{config.configuration.chargepoint_id}"].chargepoint_module except KeyError: raise KeyError(("Bitte einen vorhandenen openWB-Ladepunkt auswählen.")) - if cp_module.config.type == "internal_openwb": - return "localhost" return cp_module.config.configuration.ip_address def update(): diff --git a/packages/modules/consumers/openwb/internal_socket/selection.py b/packages/modules/consumers/openwb/internal_socket/selection.py deleted file mode 100644 index b3f24069e2..0000000000 --- a/packages/modules/consumers/openwb/internal_socket/selection.py +++ /dev/null @@ -1,30 +0,0 @@ -"""Validate that each physical openWB socket belongs to one consumer.""" -from helpermodules.subdata import SubData - - -def socket_host(configuration): - chargepoint_id = getattr(configuration, "chargepoint_id", None) - if chargepoint_id is None: - return "localhost" - cp = SubData.cp_data.get(f"cp{chargepoint_id}") - if cp is None: - raise ValueError("Bitte einen vorhandenen openWB-Ladepunkt auswählen.") - setup = cp.chargepoint.chargepoint_module.config - if setup.type == "internal_openwb": - return "localhost" - if setup.type == "external_openwb": - host = setup.configuration.ip_address - if host and host not in ("localhost", "127.0.0.1", "::1"): - return host - raise ValueError("Bitte einen internen oder Secondary-openWB-Ladepunkt mit gültiger IP auswählen.") - - -def validate_selection(configuration, consumer_id=None): - host = socket_host(configuration) - for consumer in SubData.consumer_data.values(): - module = consumer.data.module - if consumer.num == consumer_id or getattr(module, "type", None) != "internal_socket": - continue - if socket_host(module.configuration) == host: - raise ValueError("Die Steckdose dieser openWB kann nur einem Verbraucher zugeordnet werden.") - return host diff --git a/packages/modules/internal_chargepoint_handler/internal_socket_test.py b/packages/modules/internal_chargepoint_handler/internal_socket_test.py deleted file mode 100644 index 0802e217a1..0000000000 --- a/packages/modules/internal_chargepoint_handler/internal_socket_test.py +++ /dev/null @@ -1,175 +0,0 @@ -import json -from types import SimpleNamespace -from unittest.mock import Mock - -import pytest - -from helpermodules import setdata -from helpermodules.subdata import SubData -from modules.internal_chargepoint_handler.internal_socket_config import InternalSocket - - -@pytest.mark.parametrize("suffix, value", [ - ("data/set_state", True), ("data/detected", True), - ("data/last_tag", "0012345678"), ("data/last_tag", None), - ("get/state", False), ("get/fault_state", 2), ("get/fault_str", "Messfehler"), - ("get/power", 500.0), ("get/imported", 1000.0), ("get/exported", 0.0), - ("get/currents", [2.2, 0, 0]), ("get/voltages", [230, 0, 0]), - ("get/powers", [500, 0, 0]), - ("get/error_timestamp", None), - ("get/rfid", "0012345678"), ("get/rfid", None), -]) -def test_internal_socket_set_topics_are_validated_and_stored(suffix, value, monkeypatch): - publisher = Mock() - monkeypatch.setattr(setdata, "Pub", Mock(return_value=publisher)) - monkeypatch.setattr(SubData, "internal_socket_data", InternalSocket()) - setter = setdata.SetData.__new__(setdata.SetData) - setter.process_internal_socket_topic(SimpleNamespace( - topic=f"openWB/set/internal_socket/{suffix}", payload=json.dumps(value).encode())) - publisher.pub.assert_any_call(f"openWB/internal_socket/{suffix}", value, retain=True) - subscriber = SubData.__new__(SubData) - subscriber.process_internal_socket_topic(SimpleNamespace( - topic=f"openWB/internal_socket/{suffix}", payload=json.dumps(value).encode())) - branch, key = suffix.split("/") - assert getattr(getattr(SubData.internal_socket_data, branch), key) == value - - -def test_handler_loop_uses_socket_data_updated_by_mqtt(monkeypatch): - from unittest.mock import MagicMock - from modules.chargepoints.internal_openwb.config import InternalChargepointMode - from modules.internal_chargepoint_handler import internal_chargepoint_handler as handler_module - from modules.internal_chargepoint_handler.internal_chargepoint_handler_config import ( - GlobalHandlerData, InternalChargepoint, RfidData) - - monkeypatch.setattr(SubData, "internal_socket_data", InternalSocket()) - subscriber = SubData.__new__(SubData) - subscriber.process_internal_socket_topic(SimpleNamespace( - topic="openWB/internal_socket/data/set_state", payload=json.dumps(True).encode())) - monkeypatch.setattr(SubData, "internal_chargepoint_data", { - "global_data": GlobalHandlerData(heartbeat=1000), "cp0": InternalChargepoint(), - "rfid_data": RfidData(last_tag="0012345678")}) - monkeypatch.setattr(SubData, "system_data", { - "system": SimpleNamespace(data={"debug_level": 10})}) - monkeypatch.setattr(handler_module.time, "sleep", Mock()) - monkeypatch.setattr(handler_module, "clear_in_memory_log_handler", Mock()) - handler = handler_module.InternalChargepointHandler.__new__(handler_module.InternalChargepointHandler) - handler.cp0 = Mock(mode=InternalChargepointMode.SERIES) - handler.cp1 = None - handler.socket = Mock() - handler.cp0_client_handler = SimpleNamespace(client=MagicMock()) - handler.cp1_client_handler = None - from modules.common.fault_state import ComponentInfo, FaultState - handler.fault_state_info_cp0 = FaultState(ComponentInfo(0, "Test", "internal_chargepoint")) - handler.event_stop = Mock(is_set=Mock(side_effect=[False, True])) - - handler.loop() - - handler.socket.update.assert_called_once() - global_data, socket_data = handler.socket.update.call_args.args - assert global_data.heartbeat == 1000 - assert socket_data.set_state is True - assert socket_data.last_tag is None - - -@pytest.mark.parametrize("heartbeat, expected_on", [(1000, True), (900, False)]) -def test_socket_handler_uses_shared_chargepoint_heartbeat(heartbeat, expected_on, monkeypatch): - from modules.common.fault_state import ComponentInfo, FaultState - from modules.internal_chargepoint_handler import internal_chargepoint_handler as handler_module - from modules.internal_chargepoint_handler.internal_chargepoint_handler_config import GlobalHandlerData - from modules.internal_chargepoint_handler.internal_socket_config import InternalSocketData - - monkeypatch.setattr(handler_module.timecheck, "create_timestamp", Mock(return_value=1000)) - handler = handler_module.HandlerInternalSocket.__new__(handler_module.HandlerInternalSocket) - handler.module = Mock(fault_state=FaultState(ComponentInfo(None, "Test", "internal_socket"))) - handler.update_state = handler_module.UpdateInternalSocketState(handler.module) - - assert handler.update(GlobalHandlerData(heartbeat=heartbeat), InternalSocketData(set_state=True)) - - handler.module.set_state.assert_called_once_with(expected_on) - chargepoint = handler_module.HandlerChargepoint.__new__(handler_module.HandlerChargepoint) - assert chargepoint._check_heartbeat_expired(heartbeat) is not expected_on - - -@pytest.mark.parametrize("first_ack", ["chargepoint", "consumer"]) -def test_rfid_scan_is_independent_of_acknowledgement_order(first_ack, monkeypatch): - import asyncio - from modules.internal_chargepoint_handler import rfid - from modules.internal_chargepoint_handler.internal_chargepoint_handler_config import RfidData - from modules.consumers.openwb.internal_socket import consumer as socket_consumer - from modules.consumers.openwb.internal_socket.config import InternalSocket as SocketConsumer - - monkeypatch.setattr(SubData, "internal_socket_data", InternalSocket()) - source = {"rfid_data": RfidData()} - monkeypatch.setattr(SubData, "internal_chargepoint_data", source) - setter = setdata.SetData.__new__(setdata.SetData) - subscriber = SubData.__new__(SubData) - - def message(topic, value): - return SimpleNamespace(topic=topic, payload=json.dumps(value).encode()) - - def publish(topic, value, **kwargs): - if value == "": - return - if topic.startswith("openWB/set/internal_socket/"): - setter.process_internal_socket_topic(message(topic, value)) - elif topic == "openWB/set/internal_chargepoint/last_tag": - setter.process_internal_chargepoint_topic(message(topic, value)) - elif topic.startswith("openWB/internal_socket/"): - subscriber.process_internal_socket_topic(message(topic, value)) - elif topic == "openWB/internal_chargepoint/last_tag": - subscriber.process_internal_chargepoint_topic(None, source, message(topic, value)) - - monkeypatch.setattr(setdata, "Pub", Mock(return_value=Mock(pub=publish))) - monkeypatch.setattr(rfid, "Pub", Mock(return_value=Mock(pub=publish))) - monkeypatch.setattr(rfid, "categorize", lambda event: event) - monkeypatch.setattr(socket_consumer, "pub_single", publish) - configurable = Mock() - monkeypatch.setattr(socket_consumer, "ConfigurableConsumer", configurable) - socket_consumer.create_consumer(SocketConsumer(id=1)) - - async def keyboard_events(): - for digit in "0012345678": - yield SimpleNamespace(type=rfid.ecodes.EV_KEY, keystate=1, - scancode=getattr(rfid.ecodes, f"KEY_{digit}")) - yield SimpleNamespace(type=rfid.ecodes.EV_KEY, keystate=1, scancode=rfid.ecodes.KEY_ENTER) - - def scan(): - reader = rfid.RfidReader.__new__(rfid.RfidReader) - device = SimpleNamespace(path="/test/rfid", async_read_loop=keyboard_events) - asyncio.run(reader._read_events(device)) - - def acknowledge_chargepoint(): - setter.process_internal_chargepoint_topic(message("openWB/set/internal_chargepoint/last_tag", None)) - - acknowledge_consumer = configurable.call_args.kwargs["clear_rfid"] - scan() - assert source["rfid_data"].last_tag == "0012345678" - assert SubData.internal_socket_data.data.last_tag == "0012345678" - - if first_ack == "chargepoint": - acknowledge_chargepoint() - assert SubData.internal_socket_data.data.last_tag == "0012345678" - acknowledge_consumer() - else: - acknowledge_consumer() - assert source["rfid_data"].last_tag == "0012345678" - acknowledge_chargepoint() - - assert source["rfid_data"].last_tag is None - assert SubData.internal_socket_data.data.last_tag is None - scan() - assert source["rfid_data"].last_tag == "0012345678" - assert SubData.internal_socket_data.data.last_tag == "0012345678" - - -@pytest.mark.parametrize("tag", ["0012345678", None]) -def test_setdata_does_not_distribute_chargepoint_tags(tag, monkeypatch): - publisher = Mock() - monkeypatch.setattr(setdata, "Pub", Mock(return_value=publisher)) - setter = setdata.SetData.__new__(setdata.SetData) - - setter.process_internal_chargepoint_topic(SimpleNamespace( - topic="openWB/set/internal_chargepoint/last_tag", payload=json.dumps(tag).encode())) - - publisher.pub.assert_any_call("openWB/internal_chargepoint/last_tag", tag, retain=True) - assert all("internal_socket" not in call.args[0] for call in publisher.pub.call_args_list) From b3f44d059af4e61d7bca091564b05d6506772de6 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 23 Oct 2026 16:31:04 +0200 Subject: [PATCH 03/16] Review --- .../internal_chargepoint_handler.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py b/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py index 8fbad2fb0d..0ac51de2ae 100644 --- a/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py +++ b/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py @@ -127,9 +127,10 @@ def __init__(self, with SingleComponentUpdateContext(self.fault_state_info_socket, reraise=True): if mode == InternalChargepointMode.PRO_PLUS: self.cp0_client_handler = None + self.socket = None else: self.cp0_client_handler = client_factory(mode, 0, self.fault_state_info_cp0) - self.socket = HandlerInternalSocket(self.cp0_client_handler, global_data, parent_socket) + self.socket = HandlerInternalSocket(self.cp0_client_handler, global_data, parent_socket) self.cp0 = HandlerChargepoint(self.cp0_client_handler, 0, mode, global_data, parent_cp0, hierarchy_id_cp0) except Exception: From 5ee058404670e124d760925b276608e03ca1f6c1 Mon Sep 17 00:00:00 2001 From: LKuemmel <76958050+LKuemmel@users.noreply.github.com> Date: Fri, 23 Oct 2026 16:30:25 +0200 Subject: [PATCH 04/16] Potential fix for pull request finding 'Break the circular import between hardware checks and clients' Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- .../internal_chargepoint_handler/internal_hardware_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/modules/internal_chargepoint_handler/internal_hardware_check.py b/packages/modules/internal_chargepoint_handler/internal_hardware_check.py index 7ab11fb69b..5af2e99311 100644 --- a/packages/modules/internal_chargepoint_handler/internal_hardware_check.py +++ b/packages/modules/internal_chargepoint_handler/internal_hardware_check.py @@ -8,7 +8,7 @@ from modules.common.fault_state import FaultState from modules.common.hardware_check import check_meter from modules.common.modbus import ModbusSerialClient_, ModbusTcpClient_ -from modules.internal_chargepoint_handler.clients import MeterClient +from modules.internal_chargepoint_handler.hardware_definition import MeterClient log = logging.getLogger(__name__) From 9eff7d1b3589bd2d265827596b7252630bc4d4c4 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Tue, 27 Oct 2026 11:04:29 +0100 Subject: [PATCH 05/16] improve --- data/config/mosquitto/public/default-dynamic-security.json | 6 ++++++ packages/modules/consumers/openwb/internal_socket/config.py | 3 ++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/data/config/mosquitto/public/default-dynamic-security.json b/data/config/mosquitto/public/default-dynamic-security.json index b6d2bac6ec..bb7c876850 100644 --- a/data/config/mosquitto/public/default-dynamic-security.json +++ b/data/config/mosquitto/public/default-dynamic-security.json @@ -972,6 +972,12 @@ "priority": 0, "allow": true }, + { + "acltype": "publishClientReceive", + "topic": "openWB/chargepoint/+/config", + "priority": 0, + "allow": true + }, { "acltype": "publishClientReceive", "topic": "openWB/consumer/+/config", diff --git a/packages/modules/consumers/openwb/internal_socket/config.py b/packages/modules/consumers/openwb/internal_socket/config.py index 1161bfba43..714c3095ea 100644 --- a/packages/modules/consumers/openwb/internal_socket/config.py +++ b/packages/modules/consumers/openwb/internal_socket/config.py @@ -14,11 +14,12 @@ def __init__(self, chargepoint_id: Optional[int] = None): @auto_str class InternalSocket(ConsumerSetup[InternalSocketConfiguration]): - def __init__(self, name: str = "Lokale Steckdose (SDM120)", + def __init__(self, name: str = "Steckdose in openWB-Ladepunkt", type: str = "internal_socket", id: int = 0, configuration: Optional[InternalSocketConfiguration] = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_ONOFF, + ConsumerUsage.CONTINUOUS, ConsumerUsage.METER_ONLY), **kwargs): super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory().type, configuration=configuration or InternalSocketConfiguration(), usage=usage, **kwargs) From d05ab842c59d21ac28401dd751a627a0b3f09b79 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Tue, 27 Oct 2026 11:26:05 +0100 Subject: [PATCH 06/16] rename wallbox socket --- packages/control/consumer/rfid.py | 2 +- packages/helpermodules/command.py | 2 +- packages/helpermodules/command_test.py | 6 +- packages/helpermodules/setdata.py | 4 +- packages/helpermodules/subdata.py | 14 ++--- packages/main.py | 2 +- packages/modules/common/component_type.py | 6 +- packages/modules/common/fault_state.py | 2 +- packages/modules/common/store/__init__.py | 2 +- ..._socket_internal.py => _wallbox_socket.py} | 6 +- .../__init__.py | 0 .../config.py | 12 ++-- .../consumer.py | 8 +-- .../consumer_test.py | 15 ++--- .../internal_chargepoint_handler/clients.py | 7 ++- .../hardware_definition.py | 2 +- .../internal_chargepoint_handler.py | 61 ++++++++++--------- .../internal_hardware_check.py | 17 +++--- .../{internal_socket.py => wallbox_socket.py} | 38 ++++++------ ...ket_config.py => wallbox_socket_config.py} | 10 +-- ..._test.py => wallbox_socket_module_test.py} | 18 +++--- 21 files changed, 119 insertions(+), 115 deletions(-) rename packages/modules/common/store/{_socket_internal.py => _wallbox_socket.py} (83%) rename packages/modules/consumers/openwb/{internal_socket => wallbox_socket}/__init__.py (100%) rename packages/modules/consumers/openwb/{internal_socket => wallbox_socket}/config.py (64%) rename packages/modules/consumers/openwb/{internal_socket => wallbox_socket}/consumer.py (91%) rename packages/modules/consumers/openwb/{internal_socket => wallbox_socket}/consumer_test.py (75%) rename packages/modules/internal_chargepoint_handler/{internal_socket.py => wallbox_socket.py} (56%) rename packages/modules/internal_chargepoint_handler/{internal_socket_config.py => wallbox_socket_config.py} (78%) rename packages/modules/internal_chargepoint_handler/{internal_socket_module_test.py => wallbox_socket_module_test.py} (58%) diff --git a/packages/control/consumer/rfid.py b/packages/control/consumer/rfid.py index c58e5d6b4c..b4e56813fb 100644 --- a/packages/control/consumer/rfid.py +++ b/packages/control/consumer/rfid.py @@ -6,7 +6,7 @@ from helpermodules import timecheck -RFID_SUPPORTED_MODULE_TYPES = ("mqtt", "internal_socket") +RFID_SUPPORTED_MODULE_TYPES = ("mqtt", "wallbox_socket") def rfid_supported(module) -> bool: diff --git a/packages/helpermodules/command.py b/packages/helpermodules/command.py index 71ca16809d..2ad940749a 100644 --- a/packages/helpermodules/command.py +++ b/packages/helpermodules/command.py @@ -1252,7 +1252,7 @@ def removeConsumer(self, connection_id: str, payload: dict) -> None: f'Die ID \'{payload["data"]["consumer_id"]}\' ist größer als die maximal vergebene ' f'ID \'{self.max_id_hierarchy}\'.', MessageType.ERROR) consumer = SubData.consumer_data.get(f'consumer{payload["data"]["consumer_id"]}') - if consumer is not None and getattr(consumer.data.module, "type", None) == "internal_socket": + if consumer is not None and getattr(consumer.data.module, "type", None) == "wallbox_socket": consumer.module.switch_off() ProcessBrokerBranch(f'consumer/{payload["data"]["consumer_id"]}/').remove_topics() SubData.counter_all_data.hierarchy_remove_item(payload["data"]["consumer_id"]) diff --git a/packages/helpermodules/command_test.py b/packages/helpermodules/command_test.py index e5e49dc9fd..d861db0720 100644 --- a/packages/helpermodules/command_test.py +++ b/packages/helpermodules/command_test.py @@ -169,8 +169,8 @@ def test_get_max_id_hierarchy_by_topics(mock_pub, monkeypatch): mock_pub.pub.assert_any_call("openWB/set/command/max_id/hierarchy", 9) -@pytest.mark.parametrize("consumer_type", ["internal_socket", "mqtt"]) -def test_remove_internal_socket_switches_off_before_removing_topics(consumer_type, mock_pub, monkeypatch): +@pytest.mark.parametrize("consumer_type", ["wallbox_socket", "mqtt"]) +def test_remove_wallbox_socket_switches_off_before_removing_topics(consumer_type, mock_pub, monkeypatch): order = [] consumer = SimpleNamespace(data=SimpleNamespace(module=SimpleNamespace(type=consumer_type)), module=Mock()) consumer.module.switch_off.side_effect = lambda: order.append("off") @@ -185,4 +185,4 @@ def test_remove_internal_socket_switches_off_before_removing_topics(consumer_typ instance = Command.__new__(Command) instance.max_id_hierarchy = 2 instance.removeConsumer("test", {"data": {"consumer_id": 2}}) - assert order == (["off", "remove"] if consumer_type == "internal_socket" else ["remove"]) + assert order == (["off", "remove"] if consumer_type == "wallbox_socket" else ["remove"]) diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 9114b7fb0f..89179529ec 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -103,7 +103,7 @@ def on_message(self, client: mqtt.Client, userdata, msg: mqtt.MQTTMessage): elif "openWB/set/internal_chargepoint/" in msg.topic: self.process_internal_chargepoint_topic(msg) elif "openWB/set/internal_socket/" in msg.topic: - self.process_internal_socket_topic(msg) + self.process_wallbox_socket_topic(msg) elif "openWB/set/LegacySmartHome/" in msg.topic: self.process_legacy_smart_home_topic(msg) elif "openWB/set/consumer/" in msg.topic: @@ -1237,7 +1237,7 @@ def process_internal_chargepoint_topic(self, msg): except Exception: log.exception(f"Fehler im setdata-Modul: Topic {msg.topic}, Value: {msg.payload}") - def process_internal_socket_topic(self, msg): + def process_wallbox_socket_topic(self, msg): try: suffix = msg.topic.removeprefix("openWB/set/internal_socket/") if suffix in ("data/set_state", "data/detected", "get/state"): diff --git a/packages/helpermodules/subdata.py b/packages/helpermodules/subdata.py index 6fce0710ca..8635e217f8 100644 --- a/packages/helpermodules/subdata.py +++ b/packages/helpermodules/subdata.py @@ -46,7 +46,7 @@ from modules.internal_chargepoint_handler.internal_chargepoint_handler_config import ( GlobalHandlerData, InternalChargepoint, RfidData) from modules.vehicles.manual.config import ManualSoc -from modules.internal_chargepoint_handler.internal_socket_config import InternalSocket +from modules.internal_chargepoint_handler.wallbox_socket_config import WallboxSocket log = logging.getLogger(__name__) mqtt_log = logging.getLogger("mqtt") @@ -77,7 +77,7 @@ class SubData: "global_data": GlobalHandlerData(), "rfid_data": RfidData()} io_actions = io_device.IoActions() - internal_socket_data = InternalSocket() + wallbox_socket_data = WallboxSocket() io_states: Dict[str, io_device.IoStates] = {} optional_data = optional.Optional() system_data = {"system": system.System()} @@ -209,7 +209,7 @@ def on_message(self, client: mqtt.Client, userdata, msg: mqtt.MQTTMessage): elif "openWB/internal_chargepoint/" in msg.topic: self.process_internal_chargepoint_topic(client, self.internal_chargepoint_data, msg) elif "openWB/internal_socket/" in msg.topic: - self.process_internal_socket_topic(msg) + self.process_wallbox_socket_topic(msg) elif "openWB/optional/" in msg.topic: self.process_optional_topic(self.optional_data, msg) elif "openWB/counter/" in msg.topic: @@ -1255,14 +1255,14 @@ def set_internal_chargepoint_configured(self): internal_configured = False self.internal_chargepoint_data["global_data"].configured = internal_configured - def process_internal_socket_topic(self, msg): + def process_wallbox_socket_topic(self, msg): try: if msg.topic.startswith("openWB/internal_socket/data/"): - self.set_json_payload_class(self.internal_socket_data.data, msg) + self.set_json_payload_class(self.wallbox_socket_data.data, msg) elif msg.topic.startswith("openWB/internal_socket/get/"): - self.set_json_payload_class(self.internal_socket_data.get, msg) + self.set_json_payload_class(self.wallbox_socket_data.get, msg) except Exception: - log.exception("Fehler im subdata-Modul für die interne Steckdose") + log.exception("Fehler im subdata-Modul für die Wallbox-Steckdose") def process_legacy_smarthome_topic(self, client: mqtt.Client, var: counter_all.CounterAll, msg: mqtt.MQTTMessage): """ Handler für die SmartHome-Topics des alten diff --git a/packages/main.py b/packages/main.py index 84bc939775..68b4273cf5 100755 --- a/packages/main.py +++ b/packages/main.py @@ -308,7 +308,7 @@ def schedule_jobs(): handler = HandlerAlgorithm() prep = prepare.Prepare() general_internal_chargepoint_handler = GeneralInternalChargepointHandler() - general_internal_chargepoint_handler.auto_detect_internal_socket() + general_internal_chargepoint_handler.auto_detect_wallbox_socket() rfid = RfidReader() event_ev_template = Event() event_ev_template.set() diff --git a/packages/modules/common/component_type.py b/packages/modules/common/component_type.py index d4820bba31..48b89ba9fb 100644 --- a/packages/modules/common/component_type.py +++ b/packages/modules/common/component_type.py @@ -10,7 +10,7 @@ class ComponentType(Enum): COUNTER = "counter" FLEXIBLE_TARIFF = "dynamic_tariff" GRID_FEE = "grid_tariff" - INTERNAL_SOCKET = "internal_socket" + WALLBOX_SOCKET = "wallbox_socket" INVERTER = "inverter" IO = "io" VEHICLE = "vehicle" @@ -38,8 +38,8 @@ def type_to_topic_mapping(component_type: str) -> str: return "consumer" elif "counter" in component_type: return "counter" - elif "internal_socket" in component_type: - return ComponentType.INTERNAL_SOCKET.value + elif "wallbox_socket" in component_type: + return "internal_socket" elif "inverter" in component_type: return "pv" elif ComponentType.FLEXIBLE_TARIFF.value in component_type: diff --git a/packages/modules/common/fault_state.py b/packages/modules/common/fault_state.py index 2d3e23247d..cdf6b2695d 100644 --- a/packages/modules/common/fault_state.py +++ b/packages/modules/common/fault_state.py @@ -51,7 +51,7 @@ def store_error(self) -> None: topic = component_type.type_to_topic_mapping(self.component_info.type) if (self.component_info.type == component_type.ComponentType.FLEXIBLE_TARIFF.value or self.component_info.type == component_type.ComponentType.GRID_FEE.value or - self.component_info.type == component_type.ComponentType.INTERNAL_SOCKET.value): + self.component_info.type == component_type.ComponentType.WALLBOX_SOCKET.value): topic_prefix = f"openWB/set/{topic}" else: topic_prefix = f"openWB/set/{topic}/{self.component_info.id}" diff --git a/packages/modules/common/store/__init__.py b/packages/modules/common/store/__init__.py index f337f2c722..16b66fb06a 100644 --- a/packages/modules/common/store/__init__.py +++ b/packages/modules/common/store/__init__.py @@ -4,6 +4,6 @@ from modules.common.store._chargepoint_internal import get_internal_chargepoint_value_store from modules.common.store._factory import get_component_value_store from modules.common.store._io import get_io_value_store -from modules.common.store._socket_internal import get_internal_socket_value_store +from modules.common.store._wallbox_socket import get_wallbox_socket_value_store from modules.common.store._tariff import get_flexible_tariff_value_store, get_grid_fee_value_store from modules.common.store._forecast import get_forecast_value_store diff --git a/packages/modules/common/store/_socket_internal.py b/packages/modules/common/store/_wallbox_socket.py similarity index 83% rename from packages/modules/common/store/_socket_internal.py rename to packages/modules/common/store/_wallbox_socket.py index 33a6f6c506..2f33001257 100644 --- a/packages/modules/common/store/_socket_internal.py +++ b/packages/modules/common/store/_wallbox_socket.py @@ -3,7 +3,7 @@ from modules.common.store._broker import pub_to_broker -class InternalSocketValueStore(ValueStore[ConsumerState]): +class WallboxSocketValueStore(ValueStore[ConsumerState]): def set(self, state: ConsumerState) -> None: self.state = state @@ -19,5 +19,5 @@ def update(self): pub_to_broker(f"{topic_prefix}/rfid", self.state.rfid) -def get_internal_socket_value_store() -> ValueStore[ConsumerState]: - return LoggingValueStore(InternalSocketValueStore()) +def get_wallbox_socket_value_store() -> ValueStore[ConsumerState]: + return LoggingValueStore(WallboxSocketValueStore()) diff --git a/packages/modules/consumers/openwb/internal_socket/__init__.py b/packages/modules/consumers/openwb/wallbox_socket/__init__.py similarity index 100% rename from packages/modules/consumers/openwb/internal_socket/__init__.py rename to packages/modules/consumers/openwb/wallbox_socket/__init__.py diff --git a/packages/modules/consumers/openwb/internal_socket/config.py b/packages/modules/consumers/openwb/wallbox_socket/config.py similarity index 64% rename from packages/modules/consumers/openwb/internal_socket/config.py rename to packages/modules/consumers/openwb/wallbox_socket/config.py index 714c3095ea..a0dba5bc1b 100644 --- a/packages/modules/consumers/openwb/internal_socket/config.py +++ b/packages/modules/consumers/openwb/wallbox_socket/config.py @@ -7,19 +7,19 @@ @auto_str -class InternalSocketConfiguration: +class WallboxSocketConfiguration: def __init__(self, chargepoint_id: Optional[int] = None): self.chargepoint_id = chargepoint_id @auto_str -class InternalSocket(ConsumerSetup[InternalSocketConfiguration]): - def __init__(self, name: str = "Steckdose in openWB-Ladepunkt", - type: str = "internal_socket", +class WallboxSocket(ConsumerSetup[WallboxSocketConfiguration]): + def __init__(self, name: str = "Wallbox-Steckdose", + type: str = "wallbox_socket", id: int = 0, - configuration: Optional[InternalSocketConfiguration] = None, + configuration: Optional[WallboxSocketConfiguration] = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_ONOFF, ConsumerUsage.CONTINUOUS, ConsumerUsage.METER_ONLY), **kwargs): super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory().type, - configuration=configuration or InternalSocketConfiguration(), usage=usage, **kwargs) + configuration=configuration or WallboxSocketConfiguration(), usage=usage, **kwargs) diff --git a/packages/modules/consumers/openwb/internal_socket/consumer.py b/packages/modules/consumers/openwb/wallbox_socket/consumer.py similarity index 91% rename from packages/modules/consumers/openwb/internal_socket/consumer.py rename to packages/modules/consumers/openwb/wallbox_socket/consumer.py index 5e9700f224..f5f3a3a5fb 100644 --- a/packages/modules/consumers/openwb/internal_socket/consumer.py +++ b/packages/modules/consumers/openwb/wallbox_socket/consumer.py @@ -8,12 +8,12 @@ from modules.common.abstract_device import DeviceDescriptor from modules.common.component_state import ConsumerState from modules.common.configurable_consumer import ConfigurableConsumer -from modules.consumers.openwb.internal_socket.config import InternalSocket +from modules.consumers.openwb.wallbox_socket.config import WallboxSocket log = logging.getLogger(__name__) -def create_consumer(config: InternalSocket): +def create_consumer(config: WallboxSocket): def clear_rfid(): pub_single("openWB/set/internal_socket/data/last_tag", None, hostname=get_host()) @@ -38,7 +38,7 @@ def on_connect(client, userdata, flags, rc): def on_message(client, userdata, message): received_topics[message.topic] = decode_payload(message.payload) - BrokerClient(f"subscribeSocketConsumer{config.id}", on_connect, on_message, + BrokerClient(f"subscribeWallboxSocketConsumer{config.id}", on_connect, on_message, host=get_host(), port=1886 if get_host() == "localhost" else 1883).start_finite_loop(timeout=2) prefix = "openWB/internal_socket/get/" log.debug(f"Empfange MQTT Daten für Verbraucher {config.id}: {received_topics}") @@ -74,4 +74,4 @@ def switch(state): return consumer -device_descriptor = DeviceDescriptor(configuration_factory=InternalSocket) +device_descriptor = DeviceDescriptor(configuration_factory=WallboxSocket) diff --git a/packages/modules/consumers/openwb/internal_socket/consumer_test.py b/packages/modules/consumers/openwb/wallbox_socket/consumer_test.py similarity index 75% rename from packages/modules/consumers/openwb/internal_socket/consumer_test.py rename to packages/modules/consumers/openwb/wallbox_socket/consumer_test.py index 7cc99cd899..38e1280578 100644 --- a/packages/modules/consumers/openwb/internal_socket/consumer_test.py +++ b/packages/modules/consumers/openwb/wallbox_socket/consumer_test.py @@ -5,8 +5,8 @@ import pytest from control import data -from modules.consumers.openwb.internal_socket import consumer as socket_consumer -from modules.consumers.openwb.internal_socket.config import InternalSocket, InternalSocketConfiguration +from modules.consumers.openwb.wallbox_socket import consumer as wallbox_socket_consumer +from modules.consumers.openwb.wallbox_socket.config import WallboxSocket, WallboxSocketConfiguration @pytest.mark.parametrize("tag", ["0012345678", None]) @@ -14,7 +14,7 @@ ("internal_openwb", "localhost", 0), ("external_openwb", "192.168.1.40", 1), ]) -def test_socket_rfid_uses_selected_reader_and_clears_only_on_request( +def test_wallbox_socket_rfid_uses_selected_reader_and_clears_only_on_request( tag, cp_type, host, duo_num, monkeypatch, mock_pub): cp_module = Mock(config=SimpleNamespace( type=cp_type, configuration=SimpleNamespace(ip_address=host, duo_num=duo_num))) @@ -37,11 +37,12 @@ def receive(timeout): return Mock(start_finite_loop=receive) configurable = Mock() - monkeypatch.setattr(socket_consumer, "BrokerClient", broker_factory) - monkeypatch.setattr(socket_consumer, "ConfigurableConsumer", configurable) + monkeypatch.setattr(wallbox_socket_consumer, "BrokerClient", broker_factory) + monkeypatch.setattr(wallbox_socket_consumer, "ConfigurableConsumer", configurable) publisher = Mock() - monkeypatch.setattr(socket_consumer, "pub_single", publisher) - socket_consumer.create_consumer(InternalSocket(id=1, configuration=InternalSocketConfiguration(chargepoint_id=4))) + monkeypatch.setattr(wallbox_socket_consumer, "pub_single", publisher) + wallbox_socket_consumer.create_consumer( + WallboxSocket(id=1, configuration=WallboxSocketConfiguration(chargepoint_id=4))) mock_pub.reset_mock() state = configurable.call_args.kwargs["update"]() diff --git a/packages/modules/internal_chargepoint_handler/clients.py b/packages/modules/internal_chargepoint_handler/clients.py index 05633253a9..7d4b3ac245 100644 --- a/packages/modules/internal_chargepoint_handler/clients.py +++ b/packages/modules/internal_chargepoint_handler/clients.py @@ -11,7 +11,7 @@ from modules.common.serial_modbus_devices import get_serial_modbus_devices, BUS_SOURCES from modules.internal_chargepoint_handler.hardware_definition import ( CP0_METERS, CP1_METERS, EVSE_ID_CP0, EVSE_ID_ONE_BUS_CP1, EVSE_ID_ONE_BUS_SE_CP1, - EVSE_ID_SE_CP0, EVSE_ID_TWO_BUSSES_CP1, EVSE_MIN_FIRMWARE, SOCKET_METERS, MeterClient, meter_config) + EVSE_ID_SE_CP0, EVSE_ID_TWO_BUSSES_CP1, EVSE_MIN_FIRMWARE, WALLBOX_SOCKET_METERS, MeterClient, meter_config) from modules.internal_chargepoint_handler.internal_hardware_check import SeriesHardwareCheckMixin log = logging.getLogger(__name__) @@ -28,7 +28,8 @@ def __init__(self, self.evse_client = self._evse_factory(client, evse_ids) self.meter_client = self.find_meter_client(CP0_METERS if self.local_charge_point_num == 0 else CP1_METERS, client, fault_state) - self.socket_meter_client: Optional[sdm.Sdm120] = self.find_meter_client(SOCKET_METERS, client, fault_state) + self.wallbox_socket_meter_client: Optional[sdm.Sdm120] = self.find_meter_client( + WALLBOX_SOCKET_METERS, client, fault_state) self.request_and_check_hardware(fault_state) self.read_error = 0 @@ -85,7 +86,7 @@ def get_pins_cp_interruption(self) -> int: else: return 15 - def get_pins_socket(self) -> Tuple[int, int]: + def get_pins_wallbox_socket(self) -> Tuple[int, int]: return 23, 26 # BOARD numbering: direction BCM 11, enable BCM 7 diff --git a/packages/modules/internal_chargepoint_handler/hardware_definition.py b/packages/modules/internal_chargepoint_handler/hardware_definition.py index 5581b6f7ce..9ef120b2d0 100644 --- a/packages/modules/internal_chargepoint_handler/hardware_definition.py +++ b/packages/modules/internal_chargepoint_handler/hardware_definition.py @@ -13,7 +13,7 @@ CP1_METERS = [meter_config(mpm3pm.Mpm3pm, modbus_id=6), meter_config(sdm.Sdm630_72, modbus_id=106)] -SOCKET_METERS = [meter_config(sdm.Sdm120, modbus_id=54)] +WALLBOX_SOCKET_METERS = [meter_config(sdm.Sdm120, modbus_id=54)] EVSE_ID_CP0 = [1] EVSE_ID_SE_CP0 = [11] diff --git a/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py b/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py index 0ac51de2ae..45eb566444 100644 --- a/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py +++ b/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py @@ -18,11 +18,11 @@ from modules.common.serial_modbus_devices import BUS_SOURCES, get_serial_modbus_devices from modules.internal_chargepoint_handler import chargepoint_module from modules.internal_chargepoint_handler.clients import ClientHandler, client_factory -from modules.internal_chargepoint_handler.hardware_definition import SOCKET_METERS +from modules.internal_chargepoint_handler.hardware_definition import WALLBOX_SOCKET_METERS from modules.internal_chargepoint_handler.pro_plus import ProPlus from modules.internal_chargepoint_handler.socket import Socket -from modules.internal_chargepoint_handler.internal_socket import InternalSocketModule -from modules.internal_chargepoint_handler.internal_socket_config import InternalSocket, InternalSocketData +from modules.internal_chargepoint_handler.wallbox_socket import WallboxSocketModule +from modules.internal_chargepoint_handler.wallbox_socket_config import WallboxSocket, WallboxSocketData from modules.internal_chargepoint_handler.internal_chargepoint_handler_config import ( GlobalHandlerData, InternalChargepoint, InternalChargepointData, RfidData) log = logging.getLogger(__name__) @@ -89,11 +89,11 @@ def __thread_cp_interruption(self, duration: int) -> None: f"openWB/internal_chargepoint/{self.cp_module.local_charge_point_num}/data/cp_interruption_duration", 0) -class UpdateInternalSocketState: - def __init__(self, socket_module: InternalSocketModule) -> None: - self.module = socket_module +class UpdateWallboxSocketState: + def __init__(self, wallbox_socket_module: WallboxSocketModule) -> None: + self.module = wallbox_socket_module - def update_state(self, data: InternalSocketData, heartbeat_expired: bool) -> None: + def update_state(self, data: WallboxSocketData, heartbeat_expired: bool) -> None: if not data.set_state or heartbeat_expired: self.module.set_state(False) else: @@ -108,7 +108,7 @@ def __init__(self, hierarchy_id_cp0: int, parent_cp1: Optional[InternalChargepoint], hierarchy_id_cp1: Optional[int], - parent_socket: InternalSocket, + parent_wallbox_socket: WallboxSocket, event_start: Event, event_stop: Event) -> None: log.debug(f"Init internal chargepoint as {mode}") @@ -117,28 +117,29 @@ def __init__(self, self.heartbeat = False self.fault_state_info_cp0 = FaultState( ComponentInfo(0, "Interner Ladepunkt 0", "internal_chargepoint", hierarchy_id=hierarchy_id_cp0)) - self.fault_state_info_socket = FaultState(ComponentInfo(None, "Interne Steckdose", "internal_socket")) + self.fault_state_info_wallbox_socket = FaultState(ComponentInfo(None, "Wallbox-Steckdose", "wallbox_socket")) with SingleComponentUpdateContext(self.fault_state_info_cp0, reraise=True): self.init_gpio(mode) - if parent_socket.data.detected: + if parent_wallbox_socket.data.detected: try: with SingleComponentUpdateContext(self.fault_state_info_cp0, reraise=True): - with SingleComponentUpdateContext(self.fault_state_info_socket, reraise=True): + with SingleComponentUpdateContext(self.fault_state_info_wallbox_socket, reraise=True): if mode == InternalChargepointMode.PRO_PLUS: self.cp0_client_handler = None - self.socket = None + self.wallbox_socket = None else: self.cp0_client_handler = client_factory(mode, 0, self.fault_state_info_cp0) - self.socket = HandlerInternalSocket(self.cp0_client_handler, global_data, parent_socket) + self.wallbox_socket = HandlerWallboxSocket( + self.cp0_client_handler, global_data, parent_wallbox_socket) self.cp0 = HandlerChargepoint(self.cp0_client_handler, 0, mode, global_data, parent_cp0, hierarchy_id_cp0) except Exception: self.cp0_client_handler = None self.cp0 = None - self.socket = None + self.wallbox_socket = None else: - self.socket = None + self.wallbox_socket = None try: with SingleComponentUpdateContext(self.fault_state_info_cp0, reraise=True): if mode == InternalChargepointMode.PRO_PLUS: @@ -201,11 +202,11 @@ def _loop(): heartbeat_cp0 = self.cp0.update(data["global_data"], data["cp0"].data, data["rfid_data"]) if self.cp1: heartbeat_cp1 = self.cp1.update(data["global_data"], data["cp1"].data, data["rfid_data"]) - if self.socket: - socket_data = copy.deepcopy(SubData.internal_socket_data.data) - heartbeat_socket = self.socket.update(data["global_data"], socket_data) + if self.wallbox_socket: + wallbox_socket_data = copy.deepcopy(SubData.wallbox_socket_data.data) + heartbeat_wallbox_socket = self.wallbox_socket.update(data["global_data"], wallbox_socket_data) self.heartbeat = True if heartbeat_cp0 and heartbeat_cp1 and ( - self.socket is None or heartbeat_socket) else False + self.wallbox_socket is None or heartbeat_wallbox_socket) else False time.sleep(1.1) # Allgemeine Fehlermeldungen an LP 1 with SingleComponentUpdateContext(self.fault_state_info_cp0, update_always=False): @@ -280,17 +281,17 @@ def _check_heartbeat_expired(self, heartbeat) -> bool: return False -class HandlerInternalSocket: +class HandlerWallboxSocket: def __init__(self, client_handler: Optional[ClientHandler], global_data: GlobalHandlerData, - internal_socket: InternalSocket) -> None: - self.module = InternalSocketModule(client_handler, internal_socket) + wallbox_socket: WallboxSocket) -> None: + self.module = WallboxSocketModule(client_handler, wallbox_socket) self.global_data = global_data with SingleComponentUpdateContext(self.module.fault_state): - self.update_state = UpdateInternalSocketState(self.module) + self.update_state = UpdateWallboxSocketState(self.module) - def update(self, global_data: GlobalHandlerData, data: InternalSocketData) -> bool: + def update(self, global_data: GlobalHandlerData, data: WallboxSocketData) -> bool: with SingleComponentUpdateContext(self.module.fault_state): self.module.get_values(data.last_tag) heartbeat_expired = self._check_heartbeat_expired(global_data.heartbeat) @@ -312,21 +313,21 @@ def __init__(self) -> None: self.event_stop = Event() self.event_start = Event() - def auto_detect_internal_socket(self): + def auto_detect_wallbox_socket(self): try: device, count = get_serial_modbus_devices() if count == 1 and device[0] in BUS_SOURCES: port = device[0] client = ModbusSerialClient_(port) - fault_state = FaultState(ComponentInfo(None, "Interne Steckdose", "internal_socket")) - if ClientHandler.find_meter_client(SOCKET_METERS, client, fault_state) is not None: + fault_state = FaultState(ComponentInfo(None, "Wallbox-Steckdose", "wallbox_socket")) + if ClientHandler.find_meter_client(WALLBOX_SOCKET_METERS, client, fault_state) is not None: pub.pub_single("openWB/internal_socket/data/detected", True) log.debug(f"Verbrauchszähler mit lokaler Auslesung nutzt Port {port}") return with ModifyLoglevelContext(log, logging.DEBUG): - log.debug("Es konnte keine interne Steckdose erkannt werden.") + log.debug("Es konnte keine Wallbox-Steckdose erkannt werden.") except Exception: - log.exception("Fehler bei automatischen Erkennung einer internen Steckdose") + log.exception("Fehler bei der automatischen Erkennung einer Wallbox-Steckdose") def handler(self): while True: @@ -355,7 +356,7 @@ def handler(self): hierarchy_id_cp0, data["cp1"], hierarchy_id_cp1, - copy.deepcopy(SubData.internal_socket_data), + copy.deepcopy(SubData.wallbox_socket_data), self.event_start, self.event_stop) self.internal_chargepoint_handler.loop() diff --git a/packages/modules/internal_chargepoint_handler/internal_hardware_check.py b/packages/modules/internal_chargepoint_handler/internal_hardware_check.py index 5af2e99311..73502d3fc7 100644 --- a/packages/modules/internal_chargepoint_handler/internal_hardware_check.py +++ b/packages/modules/internal_chargepoint_handler/internal_hardware_check.py @@ -39,7 +39,7 @@ def evse_client(self) -> Evse: ... @property def meter_client(self) -> Any: ... @property - def socket_meter_client(self) -> Any: ... + def wallbox_socket_meter_client(self) -> Any: ... @property def read_error(self) -> int: ... @property @@ -47,7 +47,7 @@ def handle_exception(self, exception: Exception) -> bool: ... @property def request_and_check_hardware(self, fault_state: FaultState) -> Tuple[EvseState, CounterState]: ... @property - def request_and_check_socket(self, fault_state: FaultState) -> CounterState: ... + def request_and_check_wallbox_socket(self, fault_state: FaultState) -> CounterState: ... @property def check_meter(self) -> Tuple[bool, Optional[str], CounterState]: ... @@ -116,9 +116,10 @@ def check_meter(self: ClientHandlerProtocol, meter_client: MeterClient) -> Tuple except Exception: return False, METER_PROBLEM, None - def request_and_check_socket(self: ClientHandlerProtocol, fault_state: FaultState) -> CounterState: - socket_meter_check_passed, socket_meter_error_msg, counter_state = self.check_meter(self.socket_meter_client) - if socket_meter_check_passed is False: + def request_and_check_wallbox_socket(self: ClientHandlerProtocol, fault_state: FaultState) -> CounterState: + wallbox_socket_meter_check_passed, wallbox_socket_meter_error_msg, counter_state = self.check_meter( + self.wallbox_socket_meter_client) + if wallbox_socket_meter_check_passed is False: meter_check_passed = self.check_meter(self.meter_client)[0] if meter_check_passed is False: if isinstance(self.client, ModbusTcpClient_): @@ -126,7 +127,7 @@ def request_and_check_socket(self: ClientHandlerProtocol, fault_state: FaultStat else: raise Exception(READ_USB_ADAPTER_AGAIN) else: - raise Exception(socket_meter_error_msg) - if socket_meter_check_passed and socket_meter_error_msg is not None: - fault_state.warning(socket_meter_error_msg) + raise Exception(wallbox_socket_meter_error_msg) + if wallbox_socket_meter_check_passed and wallbox_socket_meter_error_msg is not None: + fault_state.warning(wallbox_socket_meter_error_msg) return counter_state diff --git a/packages/modules/internal_chargepoint_handler/internal_socket.py b/packages/modules/internal_chargepoint_handler/wallbox_socket.py similarity index 56% rename from packages/modules/internal_chargepoint_handler/internal_socket.py rename to packages/modules/internal_chargepoint_handler/wallbox_socket.py index dc0fc04b78..fd4807b6c8 100644 --- a/packages/modules/internal_chargepoint_handler/internal_socket.py +++ b/packages/modules/internal_chargepoint_handler/wallbox_socket.py @@ -6,9 +6,9 @@ from modules.common.component_context import SingleComponentUpdateContext from modules.common.component_state import ConsumerState from modules.common.fault_state import ComponentInfo, FaultState -from modules.common.store import get_internal_socket_value_store +from modules.common.store import get_wallbox_socket_value_store from modules.internal_chargepoint_handler.clients import ClientHandler -from modules.internal_chargepoint_handler.internal_socket_config import InternalSocket +from modules.internal_chargepoint_handler.wallbox_socket_config import WallboxSocket log = logging.getLogger(__name__) @@ -18,27 +18,27 @@ log.info("failed to import RPi.GPIO! maybe we are not running on a pi") -class InternalSocketModule: - def __init__(self, client_handler: ClientHandler, internal_socket: InternalSocket) -> None: - self.store_internal = get_internal_socket_value_store() - self.fault_state = FaultState(ComponentInfo(None, "Interne Steckdose", "internal_socket")) +class WallboxSocketModule: + def __init__(self, client_handler: ClientHandler, wallbox_socket: WallboxSocket) -> None: + self.store_wallbox_socket = get_wallbox_socket_value_store() + self.fault_state = FaultState(ComponentInfo(None, "Wallbox-Steckdose", "wallbox_socket")) self.client_error_context = ErrorTimerContext( - "openWB/set/internal_socket/get/error_timestamp", "Steckdose bleibt ausgeschaltet.", + "openWB/set/internal_socket/get/error_timestamp", "Wallbox-Steckdose bleibt ausgeschaltet.", hide_exception=True) - self.client_error_context.error_timestamp = internal_socket.get.error_timestamp + self.client_error_context.error_timestamp = wallbox_socket.get.error_timestamp self._client = client_handler self.current_state = False - self.old_socket_state = ConsumerState(power=0, currents=[0]*3, powers=[0]*3, state=False) + self.old_wallbox_socket_state = ConsumerState(power=0, currents=[0]*3, powers=[0]*3, state=False) def get_values(self, last_tag: Optional[str] = None) -> ConsumerState: - chargepoint_state = self.old_socket_state + wallbox_socket_state = self.old_wallbox_socket_state with self.client_error_context: - counter_state = self._client.request_and_check_socket(self.fault_state) + counter_state = self._client.request_and_check_wallbox_socket(self.fault_state) time.sleep(0.1) self.client_error_context.reset_error_counter() - chargepoint_state = ConsumerState( + wallbox_socket_state = ConsumerState( power=counter_state.power, currents=counter_state.currents, imported=counter_state.imported, @@ -50,18 +50,18 @@ def get_values(self, last_tag: Optional[str] = None) -> ConsumerState: ) if self.client_error_context.error_counter_exceeded(): - chargepoint_state = ConsumerState( + wallbox_socket_state = ConsumerState( power=0, currents=[0]*3, powers=[0]*3, state=False, - imported=self.old_socket_state.imported, exported=self.old_socket_state.exported) + imported=self.old_wallbox_socket_state.imported, exported=self.old_wallbox_socket_state.exported) - self.store_internal.set(chargepoint_state) - self.store_internal.update() - self.old_socket_state = chargepoint_state - return self.old_socket_state + self.store_wallbox_socket.set(wallbox_socket_state) + self.store_wallbox_socket.update() + self.old_wallbox_socket_state = wallbox_socket_state + return self.old_wallbox_socket_state def set_state(self, state: bool) -> None: with SingleComponentUpdateContext(self.fault_state, update_always=False): - direction_pin, enable_pin = self._client.get_pins_socket() + direction_pin, enable_pin = self._client.get_pins_wallbox_socket() if state: GPIO.output(direction_pin, GPIO.HIGH) GPIO.output(enable_pin, GPIO.HIGH) diff --git a/packages/modules/internal_chargepoint_handler/internal_socket_config.py b/packages/modules/internal_chargepoint_handler/wallbox_socket_config.py similarity index 78% rename from packages/modules/internal_chargepoint_handler/internal_socket_config.py rename to packages/modules/internal_chargepoint_handler/wallbox_socket_config.py index 5aaf567d93..be1a2859d4 100644 --- a/packages/modules/internal_chargepoint_handler/internal_socket_config.py +++ b/packages/modules/internal_chargepoint_handler/wallbox_socket_config.py @@ -6,14 +6,14 @@ @dataclass -class InternalSocketData: +class WallboxSocketData: detected: bool = False set_state: bool = False last_tag: Optional[str] = None @dataclass -class InternalSocketGet: +class WallboxSocketGet: rfid: Optional[str] = None state: bool = False power: float = 0 @@ -28,6 +28,6 @@ class InternalSocketGet: @dataclass -class InternalSocket: - data: InternalSocketData = field(default_factory=InternalSocketData) - get: InternalSocketGet = field(default_factory=InternalSocketGet) +class WallboxSocket: + data: WallboxSocketData = field(default_factory=WallboxSocketData) + get: WallboxSocketGet = field(default_factory=WallboxSocketGet) diff --git a/packages/modules/internal_chargepoint_handler/internal_socket_module_test.py b/packages/modules/internal_chargepoint_handler/wallbox_socket_module_test.py similarity index 58% rename from packages/modules/internal_chargepoint_handler/internal_socket_module_test.py rename to packages/modules/internal_chargepoint_handler/wallbox_socket_module_test.py index 54f34ce163..9c1ceb7afc 100644 --- a/packages/modules/internal_chargepoint_handler/internal_socket_module_test.py +++ b/packages/modules/internal_chargepoint_handler/wallbox_socket_module_test.py @@ -3,26 +3,26 @@ import pytest from modules.common.component_state import CounterState, ConsumerState -from modules.common.store import _socket_internal -from modules.internal_chargepoint_handler import internal_socket +from modules.common.store import _wallbox_socket +from modules.internal_chargepoint_handler import wallbox_socket @pytest.mark.parametrize("tag", ["0012345678", None]) -def test_internal_socket_measurement_publishes_current_rfid_and_clears_previous_tag(tag, monkeypatch): +def test_wallbox_socket_measurement_publishes_current_rfid_and_clears_previous_tag(tag, monkeypatch): publisher = Mock() - monkeypatch.setattr(_socket_internal, "pub_to_broker", publisher) - monkeypatch.setattr(internal_socket.time, "sleep", Mock()) - module = internal_socket.InternalSocketModule.__new__(internal_socket.InternalSocketModule) + monkeypatch.setattr(_wallbox_socket, "pub_to_broker", publisher) + monkeypatch.setattr(wallbox_socket.time, "sleep", Mock()) + module = wallbox_socket.WallboxSocketModule.__new__(wallbox_socket.WallboxSocketModule) module._client = Mock() - module._client.request_and_check_socket.return_value = CounterState( + module._client.request_and_check_wallbox_socket.return_value = CounterState( power=100, imported=20, exported=0, currents=[1, 0, 0], voltages=[230, 0, 0]) module.client_error_context = MagicMock() module.client_error_context.error_counter_exceeded.return_value = False module.fault_state = Mock() module.current_state = False module.measurement_valid = False - module.old_socket_state = ConsumerState(power=0) - module.store_internal = _socket_internal.InternalSocketValueStore() + module.old_wallbox_socket_state = ConsumerState(power=0) + module.store_wallbox_socket = _wallbox_socket.WallboxSocketValueStore() state = module.get_values(tag) From 24a6d1f63e156fd3bb188fdd0abdef984718edb2 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Tue, 27 Oct 2026 13:43:10 +0100 Subject: [PATCH 07/16] fixes --- packages/modules/consumers/openwb/wallbox_socket/consumer.py | 5 ++++- .../internal_chargepoint_handler.py | 3 ++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/packages/modules/consumers/openwb/wallbox_socket/consumer.py b/packages/modules/consumers/openwb/wallbox_socket/consumer.py index f5f3a3a5fb..47e655fb75 100644 --- a/packages/modules/consumers/openwb/wallbox_socket/consumer.py +++ b/packages/modules/consumers/openwb/wallbox_socket/consumer.py @@ -74,4 +74,7 @@ def switch(state): return consumer -device_descriptor = DeviceDescriptor(configuration_factory=WallboxSocket) +device_descriptor = DeviceDescriptor( + configuration_factory=WallboxSocket, + compatibility_device_note=("Eine gleichzeitige Verwendung von Freischaltung per RFID und separater Leistunsmessung " + "ist nicht möglich.")) diff --git a/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py b/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py index 45eb566444..fcd2d5bdd0 100644 --- a/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py +++ b/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py @@ -322,7 +322,8 @@ def auto_detect_wallbox_socket(self): fault_state = FaultState(ComponentInfo(None, "Wallbox-Steckdose", "wallbox_socket")) if ClientHandler.find_meter_client(WALLBOX_SOCKET_METERS, client, fault_state) is not None: pub.pub_single("openWB/internal_socket/data/detected", True) - log.debug(f"Verbrauchszähler mit lokaler Auslesung nutzt Port {port}") + with ModifyLoglevelContext(log, logging.DEBUG): + log.debug(f"Wallbox-Steckdose verbaut.") return with ModifyLoglevelContext(log, logging.DEBUG): log.debug("Es konnte keine Wallbox-Steckdose erkannt werden.") From 1cd21253d76e3b78594cc17524d0f14e761ff664 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Tue, 27 Oct 2026 14:37:58 +0100 Subject: [PATCH 08/16] fix log message --- packages/main.py | 1 - .../internal_chargepoint_handler.py | 3 ++- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/main.py b/packages/main.py index 68b4273cf5..d8986737e6 100755 --- a/packages/main.py +++ b/packages/main.py @@ -308,7 +308,6 @@ def schedule_jobs(): handler = HandlerAlgorithm() prep = prepare.Prepare() general_internal_chargepoint_handler = GeneralInternalChargepointHandler() - general_internal_chargepoint_handler.auto_detect_wallbox_socket() rfid = RfidReader() event_ev_template = Event() event_ev_template.set() diff --git a/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py b/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py index fcd2d5bdd0..5fc03c7326 100644 --- a/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py +++ b/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py @@ -323,7 +323,7 @@ def auto_detect_wallbox_socket(self): if ClientHandler.find_meter_client(WALLBOX_SOCKET_METERS, client, fault_state) is not None: pub.pub_single("openWB/internal_socket/data/detected", True) with ModifyLoglevelContext(log, logging.DEBUG): - log.debug(f"Wallbox-Steckdose verbaut.") + log.debug("Wallbox-Steckdose verbaut.") return with ModifyLoglevelContext(log, logging.DEBUG): log.debug("Es konnte keine Wallbox-Steckdose erkannt werden.") @@ -331,6 +331,7 @@ def auto_detect_wallbox_socket(self): log.exception("Fehler bei der automatischen Erkennung einer Wallbox-Steckdose") def handler(self): + self.auto_detect_wallbox_socket() while True: try: self.event_start.wait() From 9b539a05be4167bd648bf6657e80a8fcfffa0b09 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Tue, 27 Oct 2026 14:40:48 +0100 Subject: [PATCH 09/16] prevent removal of connected chargepoint --- packages/helpermodules/command.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/packages/helpermodules/command.py b/packages/helpermodules/command.py index 2ad940749a..9270b61fa0 100644 --- a/packages/helpermodules/command.py +++ b/packages/helpermodules/command.py @@ -413,6 +413,16 @@ def removeChargepoint(self, connection_id: str, payload: dict) -> None: payload, connection_id, f'Ladepunkt mit ID \'{cp_id}\' existiert nicht.', MessageType.ERROR) return + for consumer in SubData.consumer_data.values(): + module = consumer.data.module + if (getattr(module, "type", None) == "wallbox_socket" and + module.configuration.chargepoint_id == cp_id): + pub_user_message( + payload, connection_id, + f'Ladepunkt mit ID \'{cp_id}\' kann nicht gelöscht werden, da er der Wallbox-Steckdose ' + f'\'{module.name}\' zugeordnet ist. Bitte zuerst die Zuordnung bei der Wallbox-Steckdose ändern.', + MessageType.ERROR) + return cp_type = cp.chargepoint.data.config.type if cp_type == "external_openwb": cp_ip = cp.chargepoint.data.config.configuration.get("ip_address") From 130538a954da093aebec35b28edbdf85c257b83b Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Tue, 27 Oct 2026 15:02:47 +0100 Subject: [PATCH 10/16] update openwb local conf version, error handling --- data/config/mosquitto/local/openwb_local.conf | 2 +- .../modules/common/configurable_consumer.py | 10 +++--- .../openwb/wallbox_socket/consumer.py | 35 ++++++++++--------- 3 files changed, 24 insertions(+), 23 deletions(-) diff --git a/data/config/mosquitto/local/openwb_local.conf b/data/config/mosquitto/local/openwb_local.conf index b30e2c81d7..02060b6c81 100644 --- a/data/config/mosquitto/local/openwb_local.conf +++ b/data/config/mosquitto/local/openwb_local.conf @@ -1,4 +1,4 @@ -# openwb-version:21 +# openwb-version:22 listener 1886 localhost allow_anonymous true diff --git a/packages/modules/common/configurable_consumer.py b/packages/modules/common/configurable_consumer.py index e6902724c9..2e0c34e69f 100644 --- a/packages/modules/common/configurable_consumer.py +++ b/packages/modules/common/configurable_consumer.py @@ -92,21 +92,21 @@ def update(self): self.store.set(consumer_state) def set_power_limit(self, power_limit: float, data: SetLimitData) -> None: - with SingleComponentUpdateContext(self.fault_state): + with SingleComponentUpdateContext(self.fault_state, update_always=False): self.module_set_power_limit(power_limit, data) def switch_on(self) -> None: - with SingleComponentUpdateContext(self.fault_state): + with SingleComponentUpdateContext(self.fault_state, update_always=False): self.module_switch_on() def switch_off(self) -> None: - with SingleComponentUpdateContext(self.fault_state): + with SingleComponentUpdateContext(self.fault_state, update_always=False): self.module_switch_off() def send_values(self, values: CurrentValues) -> None: - with SingleComponentUpdateContext(self.fault_state): + with SingleComponentUpdateContext(self.fault_state, update_always=False): self.module_send_values(values) def clear_rfid(self) -> None: - with SingleComponentUpdateContext(self.fault_state): + with SingleComponentUpdateContext(self.fault_state, update_always=False): self.module_clear_rfid() diff --git a/packages/modules/consumers/openwb/wallbox_socket/consumer.py b/packages/modules/consumers/openwb/wallbox_socket/consumer.py index 47e655fb75..cbd595618a 100644 --- a/packages/modules/consumers/openwb/wallbox_socket/consumer.py +++ b/packages/modules/consumers/openwb/wallbox_socket/consumer.py @@ -42,26 +42,27 @@ def on_message(client, userdata, message): host=get_host(), port=1886 if get_host() == "localhost" else 1883).start_finite_loop(timeout=2) prefix = "openWB/internal_socket/get/" log.debug(f"Empfange MQTT Daten für Verbraucher {config.id}: {received_topics}") + fault_state = received_topics.get(f"{prefix}fault_state") + if fault_state == 2: + consumer.fault_state.error(received_topics.get( + f"{prefix}fault_str", "Fehlerstatus vom internen Ladepunkt")) + elif fault_state == 1: + consumer.fault_state.warning(received_topics.get( + f"{prefix}fault_str", "Warnung vom internen Ladepunkt")) try: - fault_state = received_topics.get(f"{prefix}fault_state") - if fault_state == 2: - consumer.fault_state.error(received_topics.get( - f"{prefix}fault_str", "Fehlerstatus vom internen Ladepunkt")) - elif fault_state == 1: - consumer.fault_state.warning(received_topics.get( - f"{prefix}fault_str", "Warnung vom internen Ladepunkt")) - return ConsumerState( - power=parse_received_topics("power"), - imported=parse_received_topics("imported"), - exported=parse_received_topics("exported"), - powers=parse_received_topics("powers"), - voltages=parse_received_topics("voltages"), - currents=parse_received_topics("currents"), - temperatures=parse_received_topics("temperatures"), - rfid=parse_received_topics("rfid"), - ) + power = received_topics[f"{prefix}power"] except KeyError: raise KeyError("Es wurden nicht alle notwendigen Daten empfangen.") + return ConsumerState( + power=parse_received_topics("power"), + imported=parse_received_topics("imported"), + exported=parse_received_topics("exported"), + powers=parse_received_topics("powers"), + voltages=parse_received_topics("voltages"), + currents=parse_received_topics("currents"), + temperatures=parse_received_topics("temperatures"), + rfid=parse_received_topics("rfid"), + ) def switch(state): pub_single("openWB/set/internal_socket/data/set_state", state, hostname=get_host()) From c92731473a1e6c5b79cb5333373c788ae1ac005c Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Wed, 28 Oct 2026 08:16:24 +0100 Subject: [PATCH 11/16] default value --- packages/control/consumer/consumer_data.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/control/consumer/consumer_data.py b/packages/control/consumer/consumer_data.py index 6cecb7733a..6d2f3a7910 100644 --- a/packages/control/consumer/consumer_data.py +++ b/packages/control/consumer/consumer_data.py @@ -66,7 +66,7 @@ class ConsumerConfig: rfid_enabled: bool = False valid_tags: List[str] = field(default_factory=empty_list_factory) rfid_reset: RfidReset = RfidReset.AFTER_TIME - rfid_timeout: int = 300 + rfid_timeout: int = 3600 @dataclass From dc9b99f45f7bc818acc012ef6934389290693c83 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Wed, 28 Oct 2026 09:02:47 +0100 Subject: [PATCH 12/16] update usage types --- packages/modules/consumers/openwb/wallbox_socket/config.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/packages/modules/consumers/openwb/wallbox_socket/config.py b/packages/modules/consumers/openwb/wallbox_socket/config.py index a0dba5bc1b..bfe8a4e1c6 100644 --- a/packages/modules/consumers/openwb/wallbox_socket/config.py +++ b/packages/modules/consumers/openwb/wallbox_socket/config.py @@ -19,7 +19,6 @@ def __init__(self, name: str = "Wallbox-Steckdose", id: int = 0, configuration: Optional[WallboxSocketConfiguration] = None, usage: Tuple[ConsumerUsage, ...] = (ConsumerUsage.SUSPENDABLE_ONOFF, - ConsumerUsage.CONTINUOUS, - ConsumerUsage.METER_ONLY), **kwargs): + ConsumerUsage.CONTINUOUS), **kwargs): super().__init__(name, type, id, vendor=vendor_descriptor.configuration_factory().type, configuration=configuration or WallboxSocketConfiguration(), usage=usage, **kwargs) From 4b44be2f07c8fd5a7f49e2088bfb7716320a2d10 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Wed, 28 Oct 2026 14:33:16 +0100 Subject: [PATCH 13/16] split client handlers for cp and wallbox socket --- .../internal_chargepoint_handler/clients.py | 23 +++---- .../internal_chargepoint_handler.py | 63 ++++++++----------- .../internal_hardware_check.py | 20 ------ .../wallbox_socket.py | 43 ++++++++++--- .../wallbox_socket_module_test.py | 4 +- 5 files changed, 76 insertions(+), 77 deletions(-) diff --git a/packages/modules/internal_chargepoint_handler/clients.py b/packages/modules/internal_chargepoint_handler/clients.py index 7d4b3ac245..0b488c10b6 100644 --- a/packages/modules/internal_chargepoint_handler/clients.py +++ b/packages/modules/internal_chargepoint_handler/clients.py @@ -5,13 +5,12 @@ from modules.common.fault_state import FaultState from modules.common.modbus import ModbusSerialClient_, ModbusTcpClient_ -from modules.common import sdm from modules.common import evse from modules.common.serial_modbus_devices import get_serial_modbus_devices, BUS_SOURCES from modules.internal_chargepoint_handler.hardware_definition import ( CP0_METERS, CP1_METERS, EVSE_ID_CP0, EVSE_ID_ONE_BUS_CP1, EVSE_ID_ONE_BUS_SE_CP1, - EVSE_ID_SE_CP0, EVSE_ID_TWO_BUSSES_CP1, EVSE_MIN_FIRMWARE, WALLBOX_SOCKET_METERS, MeterClient, meter_config) + EVSE_ID_SE_CP0, EVSE_ID_TWO_BUSSES_CP1, EVSE_MIN_FIRMWARE, MeterClient, meter_config) from modules.internal_chargepoint_handler.internal_hardware_check import SeriesHardwareCheckMixin log = logging.getLogger(__name__) @@ -28,8 +27,6 @@ def __init__(self, self.evse_client = self._evse_factory(client, evse_ids) self.meter_client = self.find_meter_client(CP0_METERS if self.local_charge_point_num == 0 else CP1_METERS, client, fault_state) - self.wallbox_socket_meter_client: Optional[sdm.Sdm120] = self.find_meter_client( - WALLBOX_SOCKET_METERS, client, fault_state) self.request_and_check_hardware(fault_state) self.read_error = 0 @@ -86,29 +83,29 @@ def get_pins_cp_interruption(self) -> int: else: return 15 - def get_pins_wallbox_socket(self) -> Tuple[int, int]: - return 23, 26 # BOARD numbering: direction BCM 11, enable BCM 7 - def client_factory(mode: InternalChargepointMode, local_charge_point_num: int, fault_state: FaultState, - created_client_handler: Optional[ClientHandler] = None) -> ClientHandler: + created_client: Optional[Union[ModbusSerialClient_, ModbusTcpClient_]] = None) -> ClientHandler: serial_client, evse_ids = get_modbus_client( - mode, local_charge_point_num, created_client_handler, fault_state) + mode, local_charge_point_num, created_client, fault_state) return ClientHandler(local_charge_point_num, serial_client, evse_ids, fault_state) def get_modbus_client(mode: InternalChargepointMode, local_charge_point_num: int, - created_client_handler: Optional[ClientHandler] = None, + created_client: Optional[Union[ModbusSerialClient_, ModbusTcpClient_]] = None, fault_state: Optional[FaultState] = None) -> Tuple[Union[ModbusSerialClient_, ModbusTcpClient_], List[int]]: resolved_devices, counter = get_serial_modbus_devices() if counter == 0: # Wenn kein USB-Gerät gefunden wird, wird der Modbus-Anschluss der AddOn-Platine genutzt (/dev/serial0) - serial_client = ModbusSerialClient_("/dev/serial0") + if local_charge_point_num == 1 and created_client is not None: + serial_client = created_client + else: + serial_client = ModbusSerialClient_("/dev/serial0") if local_charge_point_num == 0: evse_ids = EVSE_ID_CP0 else: @@ -126,8 +123,8 @@ def get_modbus_client(mode: InternalChargepointMode, # Don't create two clients for one source! with ModifyLoglevelContext(log, logging.DEBUG): log.debug("LP1 gleiches Device wie LP0") - if created_client_handler: - serial_client = created_client_handler.client + if created_client is not None: + serial_client = created_client else: serial_client = ModbusSerialClient_(resolved_devices[0]) if mode == InternalChargepointMode.SE: diff --git a/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py b/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py index 5fc03c7326..f21ec869f4 100644 --- a/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py +++ b/packages/modules/internal_chargepoint_handler/internal_chargepoint_handler.py @@ -17,7 +17,7 @@ from modules.common.modbus import ModbusSerialClient_ from modules.common.serial_modbus_devices import BUS_SOURCES, get_serial_modbus_devices from modules.internal_chargepoint_handler import chargepoint_module -from modules.internal_chargepoint_handler.clients import ClientHandler, client_factory +from modules.internal_chargepoint_handler.clients import ClientHandler, client_factory, get_modbus_client from modules.internal_chargepoint_handler.hardware_definition import WALLBOX_SOCKET_METERS from modules.internal_chargepoint_handler.pro_plus import ProPlus from modules.internal_chargepoint_handler.socket import Socket @@ -121,36 +121,25 @@ def __init__(self, with SingleComponentUpdateContext(self.fault_state_info_cp0, reraise=True): self.init_gpio(mode) - if parent_wallbox_socket.data.detected: - try: - with SingleComponentUpdateContext(self.fault_state_info_cp0, reraise=True): - with SingleComponentUpdateContext(self.fault_state_info_wallbox_socket, reraise=True): - if mode == InternalChargepointMode.PRO_PLUS: - self.cp0_client_handler = None - self.wallbox_socket = None - else: - self.cp0_client_handler = client_factory(mode, 0, self.fault_state_info_cp0) - self.wallbox_socket = HandlerWallboxSocket( - self.cp0_client_handler, global_data, parent_wallbox_socket) - self.cp0 = HandlerChargepoint(self.cp0_client_handler, 0, mode, - global_data, parent_cp0, hierarchy_id_cp0) - except Exception: - self.cp0_client_handler = None - self.cp0 = None - self.wallbox_socket = None - else: - self.wallbox_socket = None - try: - with SingleComponentUpdateContext(self.fault_state_info_cp0, reraise=True): - if mode == InternalChargepointMode.PRO_PLUS: - self.cp0_client_handler = None - else: - self.cp0_client_handler = client_factory(mode, 0, self.fault_state_info_cp0) - self.cp0 = HandlerChargepoint(self.cp0_client_handler, 0, mode, - global_data, parent_cp0, hierarchy_id_cp0) - except Exception: - self.cp0_client_handler = None - self.cp0 = None + self.cp0_client_handler = None + self.cp0_modbus_client = None + self.cp0 = None + self.wallbox_socket = None + if mode != InternalChargepointMode.PRO_PLUS: + with SingleComponentUpdateContext(self.fault_state_info_cp0): + self.cp0_modbus_client, evse_ids = get_modbus_client( + mode, 0, fault_state=self.fault_state_info_cp0) + if parent_wallbox_socket.data.detected and self.cp0_modbus_client is not None: + with SingleComponentUpdateContext(self.fault_state_info_wallbox_socket): + self.wallbox_socket = HandlerWallboxSocket( + self.cp0_modbus_client, global_data, parent_wallbox_socket) + if self.cp0_modbus_client is not None or mode == InternalChargepointMode.PRO_PLUS: + with SingleComponentUpdateContext(self.fault_state_info_cp0): + if self.cp0_modbus_client is not None: + self.cp0_client_handler = ClientHandler( + 0, self.cp0_modbus_client, evse_ids, self.fault_state_info_cp0) + self.cp0 = HandlerChargepoint(self.cp0_client_handler, 0, mode, + global_data, parent_cp0, hierarchy_id_cp0) try: if ((mode == InternalChargepointMode.DUO or mode == InternalChargepointMode.SE) and hierarchy_id_cp1 is not None): @@ -158,7 +147,7 @@ def __init__(self, ComponentInfo(1, "Interner Ladepunkt 1", "internal_chargepoint", hierarchy_id=hierarchy_id_cp1)) with SingleComponentUpdateContext(fault_state_info_cp1, reraise=True): log.debug("Zweiter Ladepunkt für Duo konfiguriert.") - self.cp1_client_handler = client_factory(mode, 1, fault_state_info_cp1, self.cp0_client_handler) + self.cp1_client_handler = client_factory(mode, 1, fault_state_info_cp1, self.cp0_modbus_client) self.cp1 = HandlerChargepoint(self.cp1_client_handler, 1, mode, global_data, parent_cp1, hierarchy_id_cp1) else: @@ -197,7 +186,7 @@ def _loop(): data = copy.deepcopy(SubData.internal_chargepoint_data) log.debug(data) log.setLevel(SubData.system_data["system"].data["debug_level"]) - heartbeat_cp0, heartbeat_cp1 = True, True + heartbeat_cp0, heartbeat_cp1 = self.cp0 is not None, True if self.cp0: heartbeat_cp0 = self.cp0.update(data["global_data"], data["cp0"].data, data["rfid_data"]) if self.cp1: @@ -213,11 +202,13 @@ def _loop(): clients = [] is_pro_plus = self.cp0 is not None and self.cp0.mode == InternalChargepointMode.PRO_PLUS if not is_pro_plus: + if self.cp0_modbus_client is not None: + clients.append(self.cp0_modbus_client) for handler in (self.cp0_client_handler, self.cp1_client_handler): if handler is not None and handler.client not in clients: clients.append(handler.client) if not clients: - log.error("Kein ClientHandler vorhanden. Beende.") + log.error("Kein Modbus-Client vorhanden. Beende.") return with ExitStack() as stack: @@ -283,10 +274,10 @@ def _check_heartbeat_expired(self, heartbeat) -> bool: class HandlerWallboxSocket: def __init__(self, - client_handler: Optional[ClientHandler], + client: ModbusSerialClient_, global_data: GlobalHandlerData, wallbox_socket: WallboxSocket) -> None: - self.module = WallboxSocketModule(client_handler, wallbox_socket) + self.module = WallboxSocketModule(client, wallbox_socket) self.global_data = global_data with SingleComponentUpdateContext(self.module.fault_state): self.update_state = UpdateWallboxSocketState(self.module) diff --git a/packages/modules/internal_chargepoint_handler/internal_hardware_check.py b/packages/modules/internal_chargepoint_handler/internal_hardware_check.py index 73502d3fc7..1bc9cd8ec1 100644 --- a/packages/modules/internal_chargepoint_handler/internal_hardware_check.py +++ b/packages/modules/internal_chargepoint_handler/internal_hardware_check.py @@ -39,16 +39,12 @@ def evse_client(self) -> Evse: ... @property def meter_client(self) -> Any: ... @property - def wallbox_socket_meter_client(self) -> Any: ... - @property def read_error(self) -> int: ... @property def handle_exception(self, exception: Exception) -> bool: ... @property def request_and_check_hardware(self, fault_state: FaultState) -> Tuple[EvseState, CounterState]: ... @property - def request_and_check_wallbox_socket(self, fault_state: FaultState) -> CounterState: ... - @property def check_meter(self) -> Tuple[bool, Optional[str], CounterState]: ... @@ -115,19 +111,3 @@ def check_meter(self: ClientHandlerProtocol, meter_client: MeterClient) -> Tuple return True, check_meter(counter_state), counter_state except Exception: return False, METER_PROBLEM, None - - def request_and_check_wallbox_socket(self: ClientHandlerProtocol, fault_state: FaultState) -> CounterState: - wallbox_socket_meter_check_passed, wallbox_socket_meter_error_msg, counter_state = self.check_meter( - self.wallbox_socket_meter_client) - if wallbox_socket_meter_check_passed is False: - meter_check_passed = self.check_meter(self.meter_client)[0] - if meter_check_passed is False: - if isinstance(self.client, ModbusTcpClient_): - raise Exception(READ_LAN_ADAPTER_AGAIN) - else: - raise Exception(READ_USB_ADAPTER_AGAIN) - else: - raise Exception(wallbox_socket_meter_error_msg) - if wallbox_socket_meter_check_passed and wallbox_socket_meter_error_msg is not None: - fault_state.warning(wallbox_socket_meter_error_msg) - return counter_state diff --git a/packages/modules/internal_chargepoint_handler/wallbox_socket.py b/packages/modules/internal_chargepoint_handler/wallbox_socket.py index fd4807b6c8..7a5ce7b2df 100644 --- a/packages/modules/internal_chargepoint_handler/wallbox_socket.py +++ b/packages/modules/internal_chargepoint_handler/wallbox_socket.py @@ -1,13 +1,17 @@ import logging import time -from typing import Optional +from typing import Optional, Tuple, Union from helpermodules.utils.error_handling import ErrorTimerContext from modules.common.component_context import SingleComponentUpdateContext -from modules.common.component_state import ConsumerState +from modules.common.component_state import ConsumerState, CounterState from modules.common.fault_state import ComponentInfo, FaultState +from modules.common.hardware_check import check_meter +from modules.common.modbus import ModbusSerialClient_, ModbusTcpClient_ from modules.common.store import get_wallbox_socket_value_store -from modules.internal_chargepoint_handler.clients import ClientHandler +from modules.internal_chargepoint_handler.hardware_definition import CP0_METERS, WALLBOX_SOCKET_METERS, MeterClient +from modules.internal_chargepoint_handler.internal_hardware_check import ( + METER_NO_SERIAL_NUMBER, METER_PROBLEM, READ_LAN_ADAPTER_AGAIN, READ_USB_ADAPTER_AGAIN) from modules.internal_chargepoint_handler.wallbox_socket_config import WallboxSocket log = logging.getLogger(__name__) @@ -19,21 +23,46 @@ class WallboxSocketModule: - def __init__(self, client_handler: ClientHandler, wallbox_socket: WallboxSocket) -> None: + def __init__(self, client: Union[ModbusSerialClient_, ModbusTcpClient_], wallbox_socket: WallboxSocket) -> None: self.store_wallbox_socket = get_wallbox_socket_value_store() self.fault_state = FaultState(ComponentInfo(None, "Wallbox-Steckdose", "wallbox_socket")) self.client_error_context = ErrorTimerContext( "openWB/set/internal_socket/get/error_timestamp", "Wallbox-Steckdose bleibt ausgeschaltet.", hide_exception=True) self.client_error_context.error_timestamp = wallbox_socket.get.error_timestamp - self._client = client_handler + self._client = client + meter_type, modbus_id = WALLBOX_SOCKET_METERS[0] + self.meter_client = meter_type(modbus_id, client, self.fault_state) self.current_state = False self.old_wallbox_socket_state = ConsumerState(power=0, currents=[0]*3, powers=[0]*3, state=False) + def check_meter(self, meter_client: MeterClient) -> Tuple[bool, Optional[str], Optional[CounterState]]: + try: + with self._client: + counter_state = meter_client.get_counter_state() + if counter_state.serial_number == "0" or counter_state.serial_number is None: + log.warning(METER_NO_SERIAL_NUMBER) + return True, check_meter(counter_state), counter_state + except Exception: + return False, METER_PROBLEM, None + + def request_and_check_wallbox_socket(self) -> CounterState: + meter_check_passed, meter_error_msg, counter_state = self.check_meter(self.meter_client) + if meter_check_passed is False: + # Probe the chargepoint meter only to distinguish a meter failure from a bus failure. + for meter_type, modbus_id in CP0_METERS: + diagnostic_meter = meter_type(modbus_id, self._client, self.fault_state) + if self.check_meter(diagnostic_meter)[0]: + raise Exception(meter_error_msg) + raise Exception(READ_USB_ADAPTER_AGAIN) + if meter_error_msg is not None: + self.fault_state.warning(meter_error_msg) + return counter_state + def get_values(self, last_tag: Optional[str] = None) -> ConsumerState: wallbox_socket_state = self.old_wallbox_socket_state with self.client_error_context: - counter_state = self._client.request_and_check_wallbox_socket(self.fault_state) + counter_state = self.request_and_check_wallbox_socket() time.sleep(0.1) self.client_error_context.reset_error_counter() @@ -61,7 +90,7 @@ def get_values(self, last_tag: Optional[str] = None) -> ConsumerState: def set_state(self, state: bool) -> None: with SingleComponentUpdateContext(self.fault_state, update_always=False): - direction_pin, enable_pin = self._client.get_pins_wallbox_socket() + direction_pin, enable_pin = 23, 26 # BOARD numbering: BCM 11, BCM 7 if state: GPIO.output(direction_pin, GPIO.HIGH) GPIO.output(enable_pin, GPIO.HIGH) diff --git a/packages/modules/internal_chargepoint_handler/wallbox_socket_module_test.py b/packages/modules/internal_chargepoint_handler/wallbox_socket_module_test.py index 9c1ceb7afc..c667e39bd1 100644 --- a/packages/modules/internal_chargepoint_handler/wallbox_socket_module_test.py +++ b/packages/modules/internal_chargepoint_handler/wallbox_socket_module_test.py @@ -14,7 +14,9 @@ def test_wallbox_socket_measurement_publishes_current_rfid_and_clears_previous_t monkeypatch.setattr(wallbox_socket.time, "sleep", Mock()) module = wallbox_socket.WallboxSocketModule.__new__(wallbox_socket.WallboxSocketModule) module._client = Mock() - module._client.request_and_check_wallbox_socket.return_value = CounterState( + module._client = MagicMock() + module.meter_client = Mock() + module.meter_client.get_counter_state.return_value = CounterState( power=100, imported=20, exported=0, currents=[1, 0, 0], voltages=[230, 0, 0]) module.client_error_context = MagicMock() module.client_error_context.error_counter_exceeded.return_value = False From ff6d01cc6f5026a99c749edd78e6c92f925ea009 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Wed, 28 Oct 2026 15:12:36 +0100 Subject: [PATCH 14/16] error handling --- .../wallbox_socket.py | 19 ++++++++++++++----- 1 file changed, 14 insertions(+), 5 deletions(-) diff --git a/packages/modules/internal_chargepoint_handler/wallbox_socket.py b/packages/modules/internal_chargepoint_handler/wallbox_socket.py index 7a5ce7b2df..5902756032 100644 --- a/packages/modules/internal_chargepoint_handler/wallbox_socket.py +++ b/packages/modules/internal_chargepoint_handler/wallbox_socket.py @@ -11,7 +11,7 @@ from modules.common.store import get_wallbox_socket_value_store from modules.internal_chargepoint_handler.hardware_definition import CP0_METERS, WALLBOX_SOCKET_METERS, MeterClient from modules.internal_chargepoint_handler.internal_hardware_check import ( - METER_NO_SERIAL_NUMBER, METER_PROBLEM, READ_LAN_ADAPTER_AGAIN, READ_USB_ADAPTER_AGAIN) + METER_NO_SERIAL_NUMBER, METER_PROBLEM, READ_USB_ADAPTER_AGAIN) from modules.internal_chargepoint_handler.wallbox_socket_config import WallboxSocket log = logging.getLogger(__name__) @@ -31,8 +31,8 @@ def __init__(self, client: Union[ModbusSerialClient_, ModbusTcpClient_], wallbox hide_exception=True) self.client_error_context.error_timestamp = wallbox_socket.get.error_timestamp self._client = client - meter_type, modbus_id = WALLBOX_SOCKET_METERS[0] - self.meter_client = meter_type(modbus_id, client, self.fault_state) + self.meter_client: Optional[MeterClient] = None + self.request_and_check_wallbox_socket() self.current_state = False self.old_wallbox_socket_state = ConsumerState(power=0, currents=[0]*3, powers=[0]*3, state=False) @@ -47,11 +47,20 @@ def check_meter(self, meter_client: MeterClient) -> Tuple[bool, Optional[str], O return False, METER_PROBLEM, None def request_and_check_wallbox_socket(self) -> CounterState: - meter_check_passed, meter_error_msg, counter_state = self.check_meter(self.meter_client) + try: + if self.meter_client is None: + meter_type, modbus_id = WALLBOX_SOCKET_METERS[0] + self.meter_client = meter_type(modbus_id, self._client, self.fault_state) + meter_check_passed, meter_error_msg, counter_state = self.check_meter(self.meter_client) + except Exception: + meter_check_passed, meter_error_msg, counter_state = False, METER_PROBLEM, None if meter_check_passed is False: # Probe the chargepoint meter only to distinguish a meter failure from a bus failure. for meter_type, modbus_id in CP0_METERS: - diagnostic_meter = meter_type(modbus_id, self._client, self.fault_state) + try: + diagnostic_meter = meter_type(modbus_id, self._client, self.fault_state) + except Exception: + continue if self.check_meter(diagnostic_meter)[0]: raise Exception(meter_error_msg) raise Exception(READ_USB_ADAPTER_AGAIN) From 77bb23856d0fb272ca0eb948b062b0ff36f715b4 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Thu, 29 Oct 2026 09:34:51 +0100 Subject: [PATCH 15/16] flake8 --- .../modules/consumers/openwb/wallbox_socket/consumer.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/packages/modules/consumers/openwb/wallbox_socket/consumer.py b/packages/modules/consumers/openwb/wallbox_socket/consumer.py index cbd595618a..746a4d627f 100644 --- a/packages/modules/consumers/openwb/wallbox_socket/consumer.py +++ b/packages/modules/consumers/openwb/wallbox_socket/consumer.py @@ -49,12 +49,8 @@ def on_message(client, userdata, message): elif fault_state == 1: consumer.fault_state.warning(received_topics.get( f"{prefix}fault_str", "Warnung vom internen Ladepunkt")) - try: - power = received_topics[f"{prefix}power"] - except KeyError: - raise KeyError("Es wurden nicht alle notwendigen Daten empfangen.") return ConsumerState( - power=parse_received_topics("power"), + power=received_topics[f"{prefix}power"], imported=parse_received_topics("imported"), exported=parse_received_topics("exported"), powers=parse_received_topics("powers"), From 67068af851422736547493e9bb8e22acf52be102 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Thu, 29 Oct 2026 09:42:13 +0100 Subject: [PATCH 16/16] adapt pytests to new default --- packages/control/consumer/rfid_test.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/control/consumer/rfid_test.py b/packages/control/consumer/rfid_test.py index df8c511c1b..edf4c04623 100644 --- a/packages/control/consumer/rfid_test.py +++ b/packages/control/consumer/rfid_test.py @@ -24,6 +24,7 @@ def consumer(monkeypatch, request): consumer.module = Mock() consumer.data.module = Mock(type=request.param) consumer.data.config.rfid_enabled = True + consumer.data.config.rfid_timeout = 300 consumer.data.config.valid_tags = ["AB??", "1234"] consumer.data.usage.type = ConsumerUsage.SUSPENDABLE_ONOFF consumer.data.get.voltages = [230] * 3