From 1f8146f57d62b9eaef7e603749b29b095174792e Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 25 Sep 2026 16:16:50 +0200 Subject: [PATCH 1/2] =?UTF-8?q?Hierarchie:=20Entferne=20mit=20Verbraucher?= =?UTF-8?q?=20verkn=C3=BCpften=20Z=C3=A4hler?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../control/counter_all/counter_all_data.py | 6 +- packages/control/counter_all/hierarchy.py | 49 ++++++++++ packages/helpermodules/subdata.py | 8 ++ packages/helpermodules/subdata_test.py | 92 +++++++++++++++++++ packages/helpermodules/update_config.py | 42 ++++++++- packages/helpermodules/update_config_test.py | 31 +++++++ 6 files changed, 226 insertions(+), 2 deletions(-) diff --git a/packages/control/counter_all/counter_all_data.py b/packages/control/counter_all/counter_all_data.py index 93c86d0ce3..8295c06b9a 100644 --- a/packages/control/counter_all/counter_all_data.py +++ b/packages/control/counter_all/counter_all_data.py @@ -1,5 +1,5 @@ from dataclasses import dataclass, field -from typing import Callable, Dict, Generator, List, Protocol, Tuple, Union +from typing import Callable, Dict, Generator, List, Optional, Protocol, Tuple, Union from control.chargepoint.chargepoint import Chargepoint from control.consumer.usage import ConsumerUsage @@ -93,6 +93,10 @@ def get_list_of_elements_per_level(self) -> List[List[Dict[str, Union[int, str]] def hierarchy_add_item_below(self, new_id: int, new_type: ComponentType, id_to_find: int) -> None: ... def hierarchy_add_item_below_evu(self, new_id: int, new_type: ComponentType) -> None: ... def hierarchy_remove_item(self, id_to_find: int, keep_children: bool = True) -> None: ... + def update_linked_counter_hierarchy(self, + consumer_id: int, + old_counter_id: Optional[int], + new_counter_id: Optional[int]) -> bool: ... class LoadmanagementPrioProtocol(Protocol): diff --git a/packages/control/counter_all/hierarchy.py b/packages/control/counter_all/hierarchy.py index deb21187a1..cba550a874 100644 --- a/packages/control/counter_all/hierarchy.py +++ b/packages/control/counter_all/hierarchy.py @@ -11,6 +11,55 @@ class HierarchyMixin: + def _is_entry_of_type(self: HierarchyProtocol, id_to_find: int, type_to_find: ComponentType) -> bool: + entry = self.get_entry_of_element(id_to_find) + return bool(entry) and entry.get("type") == type_to_find.value + + def _get_parent_counter_id_of_consumer(self: HierarchyProtocol, consumer_id: int) -> Optional[int]: + parent = self.get_entry_of_parent(consumer_id) + if not parent or parent.get("type") != ComponentType.COUNTER.value: + return None + return parent["id"] + + def _add_unlinked_counter_below_consumer_parent_if_needed(self: HierarchyProtocol, + consumer_id: int, + counter_id: int) -> bool: + if self._is_entry_of_type(counter_id, ComponentType.COUNTER): + return False + parent_counter_id = self._get_parent_counter_id_of_consumer(consumer_id) + if parent_counter_id is None: + return False + self.hierarchy_add_item_below(counter_id, ComponentType.COUNTER, parent_counter_id) + return True + + def _remove_linked_counter_from_hierarchy_if_present(self: HierarchyProtocol, counter_id: int) -> bool: + counter_entry = self.get_entry_of_element(counter_id) + if not counter_entry or counter_entry.get("type") != ComponentType.COUNTER.value: + return False + parent_counter_id = self.get_entry_of_parent(counter_id).get("id") + if parent_counter_id is None: + return False + self.hierarchy_remove_item(counter_id) + return True + + def update_linked_counter_hierarchy(self: HierarchyProtocol, + consumer_id: int, + old_counter_id: Optional[int], + new_counter_id: Optional[int]) -> bool: + hierarchy_changed = False + + if not self._is_entry_of_type(consumer_id, ComponentType.CONSUMER): + return False + + if new_counter_id is not None: + hierarchy_changed = self._remove_linked_counter_from_hierarchy_if_present(new_counter_id) + + if old_counter_id is not None and old_counter_id != new_counter_id: + if self._add_unlinked_counter_below_consumer_parent_if_needed(consumer_id, old_counter_id): + hierarchy_changed = True + + return hierarchy_changed + def get_all_elements_without_children(self: HierarchyProtocol, id: int) -> List[Dict]: self.childless = [] self._get_all_elements_without_children_recursive(self.get_entry_of_element(id)) diff --git a/packages/helpermodules/subdata.py b/packages/helpermodules/subdata.py index 6bc721a323..b9cdc4bc7b 100644 --- a/packages/helpermodules/subdata.py +++ b/packages/helpermodules/subdata.py @@ -1277,7 +1277,15 @@ def process_consumer_topic(self, client: mqtt.Client, var: Dict[str, Consumer], elif re.search("openWB/consumer/[0-9]+/set", msg.topic) is not None: self.set_json_payload_class(var["consumer"+index].data.set, msg) elif re.search("openWB/consumer/[0-9]+/extra_meter", msg.topic) is not None: + old_extra_meter = var[f"consumer{index}"].data.extra_meter self.set_json_payload_class(var[f"consumer{index}"].data, msg) + if self.event_subdata_initialized.is_set() and old_extra_meter != var[f"consumer{index}"].data.extra_meter: + if self.counter_all_data.update_linked_counter_hierarchy( + int(index), + old_extra_meter, + var[f"consumer{index}"].data.extra_meter): + Pub().pub("openWB/set/counter/get/hierarchy", + self.counter_all_data.data.get.hierarchy) elif re.search("openWB/consumer/[0-9]+/usage$", msg.topic) is not None: usage = dataclass_from_dict(Usage, decode_payload(msg.payload)) var[f"consumer{index}"].data.usage = usage diff --git a/packages/helpermodules/subdata_test.py b/packages/helpermodules/subdata_test.py index 548cdb9e50..6d610bde64 100644 --- a/packages/helpermodules/subdata_test.py +++ b/packages/helpermodules/subdata_test.py @@ -98,3 +98,95 @@ def test_process_consumer_usage_does_not_update_loadmanagement_prios_on_startup( assert consumers["consumer2"].data.usage.type == ConsumerUsage.METER_ONLY assert counter_all_data.data.get.loadmanagement_prios == [{"type": "consumer", "id": 2}] mock_pub.pub.assert_not_called() + + +def test_process_consumer_extra_meter_removes_linked_counter_from_hierarchy(monkeypatch: pytest.MonkeyPatch, + mock_pub: Mock): + subdata = SubData.__new__(SubData) + subdata.event_subdata_initialized = Mock() + subdata.event_subdata_initialized.is_set.return_value = True + counter_all_data = CounterAll() + counter_all_data.data.get.hierarchy = [{ + "id": 0, + "type": "counter", + "children": [ + {"id": 2, "type": "consumer", "children": []}, + {"id": 5, "type": "counter", "children": []}, + ], + }] + monkeypatch.setattr(SubData, "counter_all_data", counter_all_data) + consumers = {"consumer2": Consumer(2)} + msg = Mock(topic="openWB/consumer/2/extra_meter", payload=b"5") + + subdata.process_consumer_topic(Mock(), consumers, msg) + + assert consumers["consumer2"].data.extra_meter == 5 + assert counter_all_data.data.get.hierarchy == [{ + "id": 0, + "type": "counter", + "children": [{"id": 2, "type": "consumer", "children": []}], + }] + mock_pub.pub.assert_called_once_with("openWB/set/counter/get/hierarchy", counter_all_data.data.get.hierarchy) + + +def test_process_consumer_extra_meter_adds_unlinked_counter_to_hierarchy(monkeypatch: pytest.MonkeyPatch, + mock_pub: Mock): + subdata = SubData.__new__(SubData) + subdata.event_subdata_initialized = Mock() + subdata.event_subdata_initialized.is_set.return_value = True + counter_all_data = CounterAll() + counter_all_data.data.get.hierarchy = [{ + "id": 0, + "type": "counter", + "children": [{"id": 2, "type": "consumer", "children": []}], + }] + monkeypatch.setattr(SubData, "counter_all_data", counter_all_data) + consumers = {"consumer2": Consumer(2)} + consumers["consumer2"].data.extra_meter = 5 + msg = Mock(topic="openWB/consumer/2/extra_meter", payload=b"null") + + subdata.process_consumer_topic(Mock(), consumers, msg) + + assert consumers["consumer2"].data.extra_meter is None + assert counter_all_data.data.get.hierarchy == [{ + "id": 0, + "type": "counter", + "children": [ + {"id": 2, "type": "consumer", "children": []}, + {"id": 5, "type": "counter", "children": []}, + ], + }] + mock_pub.pub.assert_called_once_with("openWB/set/counter/get/hierarchy", counter_all_data.data.get.hierarchy) + + +def test_process_consumer_extra_meter_switches_linked_counter_in_hierarchy(monkeypatch: pytest.MonkeyPatch, + mock_pub: Mock): + subdata = SubData.__new__(SubData) + subdata.event_subdata_initialized = Mock() + subdata.event_subdata_initialized.is_set.return_value = True + counter_all_data = CounterAll() + counter_all_data.data.get.hierarchy = [{ + "id": 0, + "type": "counter", + "children": [ + {"id": 2, "type": "consumer", "children": []}, + {"id": 6, "type": "counter", "children": []}, + ], + }] + monkeypatch.setattr(SubData, "counter_all_data", counter_all_data) + consumers = {"consumer2": Consumer(2)} + consumers["consumer2"].data.extra_meter = 5 + msg = Mock(topic="openWB/consumer/2/extra_meter", payload=b"6") + + subdata.process_consumer_topic(Mock(), consumers, msg) + + assert consumers["consumer2"].data.extra_meter == 6 + assert counter_all_data.data.get.hierarchy == [{ + "id": 0, + "type": "counter", + "children": [ + {"id": 2, "type": "consumer", "children": []}, + {"id": 5, "type": "counter", "children": []}, + ], + }] + mock_pub.pub.assert_called_once_with("openWB/set/counter/get/hierarchy", counter_all_data.data.get.hierarchy) diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 6ad4733248..0598c2e4fb 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -59,7 +59,7 @@ class UpdateConfig: - DATASTORE_VERSION = 150 + DATASTORE_VERSION = 151 valid_topic = [ "^openWB/bat/config/bat_control_activated$", @@ -3932,3 +3932,43 @@ def upgrade(topic: str, payload) -> Optional[dict]: self._loop_all_received_topics(upgrade) self._append_datastore_version(150) + + def upgrade_datastore_151(self): + """Bereits verknüpfte Verbraucher-Zähler aus der Hierarchie entfernen.""" + hierarchy_topic = "openWB/counter/get/hierarchy" + hierarchy = decode_payload(self.all_received_topics.get(hierarchy_topic)) + if not isinstance(hierarchy, list) or not hierarchy: + self._append_datastore_version(151) + return + + linked_counter_ids = { + int(extra_meter_id) + for topic, payload in self.all_received_topics.items() + if re.search(r"^openWB/consumer/[0-9]+/extra_meter$", topic) is not None + for extra_meter_id in [decode_payload(payload)] + if extra_meter_id is not None + } + + if not linked_counter_ids: + self._append_datastore_version(151) + return + + migrated_hierarchy = copy.deepcopy(hierarchy) + _counter_all = counter_all.CounterAll() + _counter_all.data.get.hierarchy = migrated_hierarchy + hierarchy_changed = False + + for linked_counter_id in linked_counter_ids: + counter_entry = _counter_all.get_entry_of_element(linked_counter_id) + if not counter_entry or counter_entry.get("type") != ComponentType.COUNTER.value: + continue + parent_entry = _counter_all.get_entry_of_parent(linked_counter_id) + if not parent_entry or parent_entry.get("type") != ComponentType.COUNTER.value: + continue + _counter_all.hierarchy_remove_item(linked_counter_id) + hierarchy_changed = True + + if hierarchy_changed: + self.__update_topic(hierarchy_topic, _counter_all.data.get.hierarchy) + + self._append_datastore_version(151) diff --git a/packages/helpermodules/update_config_test.py b/packages/helpermodules/update_config_test.py index 114ef64429..5667be977d 100644 --- a/packages/helpermodules/update_config_test.py +++ b/packages/helpermodules/update_config_test.py @@ -466,3 +466,34 @@ def test_upgrade_datastore_150_migrates_fixed_hours_weekdays(mock_pub): assert "openWB/optional/ep/flexible_tariff/provider" in updated_topics assert "openWB/optional/ep/grid_fee/provider" in updated_topics assert "openWB/system/datastore_version" in updated_topics + + +def test_upgrade_datastore_151_removes_linked_extra_meter_counters_from_hierarchy(mock_pub: Mock): + uc = UpdateConfig() + uc.all_received_topics = { + "openWB/consumer/2/extra_meter": 5, + "openWB/consumer/3/extra_meter": None, + "openWB/counter/get/hierarchy": [{ + "id": 0, + "type": "counter", + "children": [ + {"id": 2, "type": "consumer", "children": []}, + {"id": 5, "type": "counter", "children": []}, + {"id": 6, "type": "counter", "children": []}, + ], + }], + "openWB/system/datastore_version": list(range(151)), + } + + uc.upgrade_datastore_151() + + assert uc.all_received_topics["openWB/counter/get/hierarchy"] == [{ + "id": 0, + "type": "counter", + "children": [ + {"id": 2, "type": "consumer", "children": []}, + {"id": 6, "type": "counter", "children": []}, + ], + }] + assert uc.all_received_topics["openWB/system/datastore_version"] == list(range(152)) + assert mock_pub.pub.call_count == 2 From 66108e337b665f9332ee32e01369aefb6153b083 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 25 Sep 2026 16:21:24 +0200 Subject: [PATCH 2/2] flake8 --- packages/control/counter_all/counter_all_data.py | 1 + packages/control/counter_all/hierarchy.py | 4 ++-- packages/helpermodules/subdata.py | 3 ++- 3 files changed, 5 insertions(+), 3 deletions(-) diff --git a/packages/control/counter_all/counter_all_data.py b/packages/control/counter_all/counter_all_data.py index 8295c06b9a..a61ba69c3c 100644 --- a/packages/control/counter_all/counter_all_data.py +++ b/packages/control/counter_all/counter_all_data.py @@ -93,6 +93,7 @@ def get_list_of_elements_per_level(self) -> List[List[Dict[str, Union[int, str]] def hierarchy_add_item_below(self, new_id: int, new_type: ComponentType, id_to_find: int) -> None: ... def hierarchy_add_item_below_evu(self, new_id: int, new_type: ComponentType) -> None: ... def hierarchy_remove_item(self, id_to_find: int, keep_children: bool = True) -> None: ... + def update_linked_counter_hierarchy(self, consumer_id: int, old_counter_id: Optional[int], diff --git a/packages/control/counter_all/hierarchy.py b/packages/control/counter_all/hierarchy.py index cba550a874..25c0770b76 100644 --- a/packages/control/counter_all/hierarchy.py +++ b/packages/control/counter_all/hierarchy.py @@ -22,8 +22,8 @@ def _get_parent_counter_id_of_consumer(self: HierarchyProtocol, consumer_id: int return parent["id"] def _add_unlinked_counter_below_consumer_parent_if_needed(self: HierarchyProtocol, - consumer_id: int, - counter_id: int) -> bool: + consumer_id: int, + counter_id: int) -> bool: if self._is_entry_of_type(counter_id, ComponentType.COUNTER): return False parent_counter_id = self._get_parent_counter_id_of_consumer(consumer_id) diff --git a/packages/helpermodules/subdata.py b/packages/helpermodules/subdata.py index b9cdc4bc7b..6ed8592708 100644 --- a/packages/helpermodules/subdata.py +++ b/packages/helpermodules/subdata.py @@ -1279,7 +1279,8 @@ def process_consumer_topic(self, client: mqtt.Client, var: Dict[str, Consumer], elif re.search("openWB/consumer/[0-9]+/extra_meter", msg.topic) is not None: old_extra_meter = var[f"consumer{index}"].data.extra_meter self.set_json_payload_class(var[f"consumer{index}"].data, msg) - if self.event_subdata_initialized.is_set() and old_extra_meter != var[f"consumer{index}"].data.extra_meter: + if self.event_subdata_initialized.is_set() and old_extra_meter != var[ + f"consumer{index}"].data.extra_meter: if self.counter_all_data.update_linked_counter_hierarchy( int(index), old_extra_meter,