From f21be3090ead022f08964bbc9cb5e7ec34871590 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Wed, 23 Sep 2026 10:25:40 +0200 Subject: [PATCH 1/4] removing meter-only consumers from load management priority --- .../control/counter_all/counter_all_data.py | 5 ++ .../counter_all/loadmanagement_prio.py | 22 +++++ .../counter_all/loadmanagement_prio_test.py | 26 ++++++ packages/helpermodules/subdata.py | 5 +- packages/helpermodules/subdata_test.py | 80 +++++++++++++++++++ packages/helpermodules/update_config.py | 30 ++++++- packages/helpermodules/update_config_test.py | 35 ++++++++ 7 files changed, 201 insertions(+), 2 deletions(-) create mode 100644 packages/helpermodules/subdata_test.py diff --git a/packages/control/counter_all/counter_all_data.py b/packages/control/counter_all/counter_all_data.py index 3806edb738..25c625d2d5 100644 --- a/packages/control/counter_all/counter_all_data.py +++ b/packages/control/counter_all/counter_all_data.py @@ -2,6 +2,7 @@ from typing import Callable, Dict, Generator, List, Protocol, Tuple, Union from control.chargepoint.chargepoint import Chargepoint +from control.consumer.usage import ConsumerUsage from dataclass_utils.factories import empty_list_factory from modules.common.component_type import ComponentType @@ -98,6 +99,10 @@ class LoadmanagementPrioProtocol(Protocol): @property def data(self) -> CounterAllData: ... def add_loadmanagement_prio_item(self, type: ComponentType, id: int) -> None: ... + def update_consumer_loadmanagement_prio(self, consumer_id: int, usage_type: ConsumerUsage) -> None: ... + def _has_loadmanagement_prio_item( + self, type: ComponentType, id: int, entries: List[Dict]) -> bool: ... + def remove_loadmanagement_prio_item(self, type: ComponentType, id: int) -> None: ... def _remove_loadmanagement_prio_item(self, type: ComponentType, id: int, entry: List[Dict]) -> bool: ... def sort_cps_by_loadmanagement_prios_nested(self, filtered_cps: List[Chargepoint]) -> List[List[Chargepoint]]: ... diff --git a/packages/control/counter_all/loadmanagement_prio.py b/packages/control/counter_all/loadmanagement_prio.py index 5bfe377d6d..0ccf9a7a8a 100644 --- a/packages/control/counter_all/loadmanagement_prio.py +++ b/packages/control/counter_all/loadmanagement_prio.py @@ -2,6 +2,7 @@ from typing import Dict, Generator, List, Tuple from control.chargepoint.chargepoint import Chargepoint +from control.consumer.usage import ConsumerUsage from control.counter_all.counter_all_data import LoadmanagementPrioProtocol from modules.common.component_type import ComponentType @@ -13,6 +14,27 @@ class LoadmanagementPrioMixin: def add_loadmanagement_prio_item(self: LoadmanagementPrioProtocol, type: ComponentType, id: int) -> None: self.data.get.loadmanagement_prios.append({"type": type.value, "id": id}) + def update_consumer_loadmanagement_prio( + self: LoadmanagementPrioProtocol, consumer_id: int, usage_type: ConsumerUsage) -> None: + consumer_in_prios = self._has_loadmanagement_prio_item( + ComponentType.CONSUMER, consumer_id, self.data.get.loadmanagement_prios) + if usage_type == ConsumerUsage.METER_ONLY: + if consumer_in_prios: + self.remove_loadmanagement_prio_item(ComponentType.CONSUMER, consumer_id) + elif consumer_in_prios is False: + self.add_loadmanagement_prio_item(ComponentType.CONSUMER, consumer_id) + + def _has_loadmanagement_prio_item(self: LoadmanagementPrioProtocol, + type: ComponentType, + id: int, + entries: List[Dict]) -> bool: + for entry in entries: + if entry["type"] == type.value and entry["id"] == id: + return True + if entry["type"] == "group" and self._has_loadmanagement_prio_item(type, id, entry["children"]): + return True + return False + def remove_loadmanagement_prio_item(self: LoadmanagementPrioProtocol, type: ComponentType, id: int) -> None: if self._remove_loadmanagement_prio_item(type, id, self.data.get.loadmanagement_prios) is False: # Kein Grund, die Löschung des restlichen Elements (Topics, Hierarchie) abzubrechen, wenn der diff --git a/packages/control/counter_all/loadmanagement_prio_test.py b/packages/control/counter_all/loadmanagement_prio_test.py index 5bb780ed92..bb0bf79987 100644 --- a/packages/control/counter_all/loadmanagement_prio_test.py +++ b/packages/control/counter_all/loadmanagement_prio_test.py @@ -5,6 +5,7 @@ import pytest from control.chargepoint.chargepoint import Chargepoint +from control.consumer.usage import ConsumerUsage from control.counter_all.counter_all import CounterAll from modules.common.component_type import ComponentType @@ -76,6 +77,31 @@ def test_add_item(loadmanagement_prios: List[Dict], assert c.data.get.loadmanagement_prios == expected_loadmanagement_prios +@pytest.mark.parametrize( + "loadmanagement_prios, usage_type, expected_loadmanagement_prios", + [ + pytest.param([], ConsumerUsage.CONTINUOUS, [{"type": "consumer", "id": 2}], id="add consumer"), + pytest.param([{"type": "consumer", "id": 2}], ConsumerUsage.CONTINUOUS, + [{"type": "consumer", "id": 2}], id="do not add duplicate"), + pytest.param([{ + "type": "group", + "label": "Verbraucher", + "children": [{"type": "consumer", "id": 2}], + }], ConsumerUsage.METER_ONLY, [], id="remove consumer from group"), + pytest.param([], ConsumerUsage.METER_ONLY, [], id="ignore absent meter"), + ], +) +def test_update_consumer_loadmanagement_prio(loadmanagement_prios: List[Dict], + usage_type: ConsumerUsage, + expected_loadmanagement_prios: List[Dict]): + c = CounterAll() + c.data.get.loadmanagement_prios = loadmanagement_prios + + c.update_consumer_loadmanagement_prio(2, usage_type) + + assert c.data.get.loadmanagement_prios == expected_loadmanagement_prios + + @pytest.mark.parametrize( "loadmanagement_prios, type, id, expected_loadmanagement_prios", [ diff --git a/packages/helpermodules/subdata.py b/packages/helpermodules/subdata.py index f0f76c693b..84f892972b 100644 --- a/packages/helpermodules/subdata.py +++ b/packages/helpermodules/subdata.py @@ -1279,7 +1279,10 @@ 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: self.set_json_payload_class(var[f"consumer{index}"].data, msg) elif re.search("openWB/consumer/[0-9]+/usage$", msg.topic) is not None: - var[f"consumer{index}"].data.usage = dataclass_from_dict(Usage, decode_payload(msg.payload)) + usage = dataclass_from_dict(Usage, decode_payload(msg.payload)) + var[f"consumer{index}"].data.usage = usage + if self.event_subdata_initialized.is_set(): + self.counter_all_data.update_consumer_loadmanagement_prio(int(index), usage.type) elif re.search("openWB/consumer/[0-9]+/control_parameter/", msg.topic) is not None: if re.search("openWB/consumer/[0-9]+/control_parameter/limit", msg.topic) is not None: payload = decode_payload(msg.payload) diff --git a/packages/helpermodules/subdata_test.py b/packages/helpermodules/subdata_test.py new file mode 100644 index 0000000000..994ea179bf --- /dev/null +++ b/packages/helpermodules/subdata_test.py @@ -0,0 +1,80 @@ +import json +from typing import Dict, List +from unittest.mock import Mock + +import pytest + +from control.consumer.consumer import Consumer +from control.consumer.usage import ConsumerUsage +from control.counter_all.counter_all import CounterAll +from helpermodules.subdata import SubData + + +@pytest.mark.parametrize( + "usage_type, loadmanagement_prios, expected_loadmanagement_prios", + [ + pytest.param( + ConsumerUsage.SUSPENDABLE_TUNABLE, + [], + [{"type": "consumer", "id": 2}], + id="add controllable consumer", + ), + pytest.param( + ConsumerUsage.CONTINUOUS, + [{"type": "group", "label": "Gruppe", "children": [{"type": "consumer", "id": 2}]}], + [{"type": "group", "label": "Gruppe", "children": [{"type": "consumer", "id": 2}]}], + id="do not duplicate grouped consumer", + ), + pytest.param( + ConsumerUsage.METER_ONLY, + [{"type": "group", "label": "Gruppe", "children": [{"type": "consumer", "id": 2}]}], + [], + id="remove meter-only consumer from group", + ), + pytest.param( + ConsumerUsage.METER_ONLY, + [], + [], + id="ignore absent meter-only consumer", + ), + ], +) +def test_process_consumer_usage_updates_loadmanagement_prios(usage_type: ConsumerUsage, + loadmanagement_prios: List[Dict], + expected_loadmanagement_prios: List[Dict], + monkeypatch: pytest.MonkeyPatch): + 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.loadmanagement_prios = loadmanagement_prios + monkeypatch.setattr(SubData, "counter_all_data", counter_all_data) + consumers = {"consumer2": Consumer(2)} + msg = Mock( + topic="openWB/consumer/2/usage", + payload=json.dumps({"type": usage_type.value}).encode("utf-8"), + ) + + subdata.process_consumer_topic(Mock(), consumers, msg) + + assert consumers["consumer2"].data.usage.type == usage_type + assert counter_all_data.data.get.loadmanagement_prios == expected_loadmanagement_prios + + +def test_process_consumer_usage_does_not_update_loadmanagement_prios_on_startup(monkeypatch: pytest.MonkeyPatch): + subdata = SubData.__new__(SubData) + subdata.event_subdata_initialized = Mock() + subdata.event_subdata_initialized.is_set.return_value = False + counter_all_data = CounterAll() + counter_all_data.data.get.loadmanagement_prios = [{"type": "consumer", "id": 2}] + monkeypatch.setattr(SubData, "counter_all_data", counter_all_data) + consumers = {"consumer2": Consumer(2)} + msg = Mock( + topic="openWB/consumer/2/usage", + payload=json.dumps({"type": ConsumerUsage.METER_ONLY.value}).encode("utf-8"), + ) + + subdata.process_consumer_topic(Mock(), consumers, msg) + + assert consumers["consumer2"].data.usage.type == ConsumerUsage.METER_ONLY + assert counter_all_data.data.get.loadmanagement_prios == [{"type": "consumer", "id": 2}] diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index bfcf34fc25..21a283f26f 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -58,7 +58,7 @@ class UpdateConfig: - DATASTORE_VERSION = 148 + DATASTORE_VERSION = 149 valid_topic = [ "^openWB/bat/config/bat_control_activated$", @@ -3864,3 +3864,31 @@ def upgrade(topic: str, payload) -> Optional[dict]: return {topic: NO_MODULE} self._loop_all_received_topics(upgrade) self._append_datastore_version(148) + + def upgrade_datastore_149(self) -> None: + """Verbraucher ohne Lastmanagement aus der Prioritätensteuerung entfernen.""" + meter_only_consumers = { + int(get_index(topic)) + for topic, payload in self.all_received_topics.items() + if re.search(r"^openWB/consumer/[0-9]+/usage$", topic) is not None + and decode_payload(payload).get("type") == "meter_only" + } + + def remove_meter_only_consumers(entries: list) -> None: + for entry in entries.copy(): + if entry.get("type") == "consumer" and entry.get("id") in meter_only_consumers: + entries.remove(entry) + elif entry.get("type") == "group": + children = entry.get("children", []) + remove_meter_only_consumers(children) + if not children: + entries.remove(entry) + + topic = "openWB/counter/get/loadmanagement_prios" + if topic in self.all_received_topics: + loadmanagement_prios = decode_payload(self.all_received_topics[topic]) + migrated_loadmanagement_prios = copy.deepcopy(loadmanagement_prios) + remove_meter_only_consumers(migrated_loadmanagement_prios) + if migrated_loadmanagement_prios != loadmanagement_prios: + self.__update_topic(topic, migrated_loadmanagement_prios) + self._append_datastore_version(149) diff --git a/packages/helpermodules/update_config_test.py b/packages/helpermodules/update_config_test.py index cad4b7c060..2195ac9bcd 100644 --- a/packages/helpermodules/update_config_test.py +++ b/packages/helpermodules/update_config_test.py @@ -363,3 +363,38 @@ def test_upgrade_datastore_142_ev_chargemode_conversion(ev0_prio: bool, assert uc.all_received_topics["openWB/counter/get/loadmanagement_prios"] == expected assert uc.all_received_topics["openWB/system/datastore_version"] == [131, 132, 142] assert mock_pub.pub.call_count == 2 # einmal publishen für Upgrade der Datastore-Version + + +def test_upgrade_datastore_149_removes_meter_only_consumers(mock_pub: Mock): + uc = UpdateConfig() + uc.all_received_topics = { + "openWB/consumer/1/usage": {"type": "continuous"}, + "openWB/consumer/2/usage": {"type": "meter_only"}, + "openWB/counter/get/loadmanagement_prios": [ + {"type": "vehicle", "id": 0}, + { + "type": "group", + "label": "Verbraucher", + "children": [ + {"type": "consumer", "id": 1}, + {"type": "consumer", "id": 2}, + ], + }, + {"type": "consumer", "id": 3}, + ], + "openWB/system/datastore_version": list(range(149)), + } + + uc.upgrade_datastore_149() + + assert uc.all_received_topics["openWB/counter/get/loadmanagement_prios"] == [ + {"type": "vehicle", "id": 0}, + { + "type": "group", + "label": "Verbraucher", + "children": [{"type": "consumer", "id": 1}], + }, + {"type": "consumer", "id": 3}, + ] + assert uc.all_received_topics["openWB/system/datastore_version"] == list(range(150)) + assert mock_pub.pub.call_count == 2 From a3b2401aa44860c1d82f829c32932bedf455186e Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Wed, 23 Sep 2026 11:02:09 +0200 Subject: [PATCH 2/4] review --- .../control/counter_all/counter_all_data.py | 2 +- .../counter_all/loadmanagement_prio.py | 5 ++++- .../counter_all/loadmanagement_prio_test.py | 16 +++++++++------- packages/helpermodules/subdata.py | 6 ++++-- packages/helpermodules/subdata_test.py | 19 ++++++++++++++++--- 5 files changed, 34 insertions(+), 14 deletions(-) diff --git a/packages/control/counter_all/counter_all_data.py b/packages/control/counter_all/counter_all_data.py index 25c625d2d5..93c86d0ce3 100644 --- a/packages/control/counter_all/counter_all_data.py +++ b/packages/control/counter_all/counter_all_data.py @@ -99,7 +99,7 @@ class LoadmanagementPrioProtocol(Protocol): @property def data(self) -> CounterAllData: ... def add_loadmanagement_prio_item(self, type: ComponentType, id: int) -> None: ... - def update_consumer_loadmanagement_prio(self, consumer_id: int, usage_type: ConsumerUsage) -> None: ... + def update_consumer_loadmanagement_prio(self, consumer_id: int, usage_type: ConsumerUsage) -> bool: ... def _has_loadmanagement_prio_item( self, type: ComponentType, id: int, entries: List[Dict]) -> bool: ... diff --git a/packages/control/counter_all/loadmanagement_prio.py b/packages/control/counter_all/loadmanagement_prio.py index 0ccf9a7a8a..664dbaab26 100644 --- a/packages/control/counter_all/loadmanagement_prio.py +++ b/packages/control/counter_all/loadmanagement_prio.py @@ -15,14 +15,17 @@ def add_loadmanagement_prio_item(self: LoadmanagementPrioProtocol, type: Compone self.data.get.loadmanagement_prios.append({"type": type.value, "id": id}) def update_consumer_loadmanagement_prio( - self: LoadmanagementPrioProtocol, consumer_id: int, usage_type: ConsumerUsage) -> None: + self: LoadmanagementPrioProtocol, consumer_id: int, usage_type: ConsumerUsage) -> bool: consumer_in_prios = self._has_loadmanagement_prio_item( ComponentType.CONSUMER, consumer_id, self.data.get.loadmanagement_prios) if usage_type == ConsumerUsage.METER_ONLY: if consumer_in_prios: self.remove_loadmanagement_prio_item(ComponentType.CONSUMER, consumer_id) + return True elif consumer_in_prios is False: self.add_loadmanagement_prio_item(ComponentType.CONSUMER, consumer_id) + return True + return False def _has_loadmanagement_prio_item(self: LoadmanagementPrioProtocol, type: ComponentType, diff --git a/packages/control/counter_all/loadmanagement_prio_test.py b/packages/control/counter_all/loadmanagement_prio_test.py index bb0bf79987..82fe91f801 100644 --- a/packages/control/counter_all/loadmanagement_prio_test.py +++ b/packages/control/counter_all/loadmanagement_prio_test.py @@ -78,28 +78,30 @@ def test_add_item(loadmanagement_prios: List[Dict], @pytest.mark.parametrize( - "loadmanagement_prios, usage_type, expected_loadmanagement_prios", + "loadmanagement_prios, usage_type, expected_loadmanagement_prios, expected_changed", [ - pytest.param([], ConsumerUsage.CONTINUOUS, [{"type": "consumer", "id": 2}], id="add consumer"), + pytest.param([], ConsumerUsage.CONTINUOUS, [{"type": "consumer", "id": 2}], True, id="add consumer"), pytest.param([{"type": "consumer", "id": 2}], ConsumerUsage.CONTINUOUS, - [{"type": "consumer", "id": 2}], id="do not add duplicate"), + [{"type": "consumer", "id": 2}], False, id="do not add duplicate"), pytest.param([{ "type": "group", "label": "Verbraucher", "children": [{"type": "consumer", "id": 2}], - }], ConsumerUsage.METER_ONLY, [], id="remove consumer from group"), - pytest.param([], ConsumerUsage.METER_ONLY, [], id="ignore absent meter"), + }], ConsumerUsage.METER_ONLY, [], True, id="remove consumer from group"), + pytest.param([], ConsumerUsage.METER_ONLY, [], False, id="ignore absent meter"), ], ) def test_update_consumer_loadmanagement_prio(loadmanagement_prios: List[Dict], usage_type: ConsumerUsage, - expected_loadmanagement_prios: List[Dict]): + expected_loadmanagement_prios: List[Dict], + expected_changed: bool): c = CounterAll() c.data.get.loadmanagement_prios = loadmanagement_prios - c.update_consumer_loadmanagement_prio(2, usage_type) + changed = c.update_consumer_loadmanagement_prio(2, usage_type) assert c.data.get.loadmanagement_prios == expected_loadmanagement_prios + assert changed is expected_changed @pytest.mark.parametrize( diff --git a/packages/helpermodules/subdata.py b/packages/helpermodules/subdata.py index 84f892972b..6bc721a323 100644 --- a/packages/helpermodules/subdata.py +++ b/packages/helpermodules/subdata.py @@ -1281,8 +1281,10 @@ def process_consumer_topic(self, client: mqtt.Client, var: Dict[str, Consumer], 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 - if self.event_subdata_initialized.is_set(): - self.counter_all_data.update_consumer_loadmanagement_prio(int(index), usage.type) + if (self.event_subdata_initialized.is_set() and + self.counter_all_data.update_consumer_loadmanagement_prio(int(index), usage.type)): + Pub().pub("openWB/set/counter/get/loadmanagement_prios", + self.counter_all_data.data.get.loadmanagement_prios) elif re.search("openWB/consumer/[0-9]+/control_parameter/", msg.topic) is not None: if re.search("openWB/consumer/[0-9]+/control_parameter/limit", msg.topic) is not None: payload = decode_payload(msg.payload) diff --git a/packages/helpermodules/subdata_test.py b/packages/helpermodules/subdata_test.py index 994ea179bf..0d093ab02f 100644 --- a/packages/helpermodules/subdata_test.py +++ b/packages/helpermodules/subdata_test.py @@ -11,30 +11,34 @@ @pytest.mark.parametrize( - "usage_type, loadmanagement_prios, expected_loadmanagement_prios", + "usage_type, loadmanagement_prios, expected_loadmanagement_prios, expected_changed", [ pytest.param( ConsumerUsage.SUSPENDABLE_TUNABLE, [], [{"type": "consumer", "id": 2}], + True, id="add controllable consumer", ), pytest.param( ConsumerUsage.CONTINUOUS, [{"type": "group", "label": "Gruppe", "children": [{"type": "consumer", "id": 2}]}], [{"type": "group", "label": "Gruppe", "children": [{"type": "consumer", "id": 2}]}], + False, id="do not duplicate grouped consumer", ), pytest.param( ConsumerUsage.METER_ONLY, [{"type": "group", "label": "Gruppe", "children": [{"type": "consumer", "id": 2}]}], [], + True, id="remove meter-only consumer from group", ), pytest.param( ConsumerUsage.METER_ONLY, [], [], + False, id="ignore absent meter-only consumer", ), ], @@ -42,7 +46,9 @@ def test_process_consumer_usage_updates_loadmanagement_prios(usage_type: ConsumerUsage, loadmanagement_prios: List[Dict], expected_loadmanagement_prios: List[Dict], - monkeypatch: pytest.MonkeyPatch): + expected_changed: bool, + monkeypatch: pytest.MonkeyPatch, + mock_pub: Mock): subdata = SubData.__new__(SubData) subdata.event_subdata_initialized = Mock() subdata.event_subdata_initialized.is_set.return_value = True @@ -59,9 +65,15 @@ def test_process_consumer_usage_updates_loadmanagement_prios(usage_type: Consume assert consumers["consumer2"].data.usage.type == usage_type assert counter_all_data.data.get.loadmanagement_prios == expected_loadmanagement_prios + if expected_changed: + mock_pub.pub.assert_called_once_with( + "openWB/set/counter/get/loadmanagement_prios", expected_loadmanagement_prios) + else: + mock_pub.pub.assert_not_called() -def test_process_consumer_usage_does_not_update_loadmanagement_prios_on_startup(monkeypatch: pytest.MonkeyPatch): +def test_process_consumer_usage_does_not_update_loadmanagement_prios_on_startup(monkeypatch: pytest.MonkeyPatch, + mock_pub: Mock): subdata = SubData.__new__(SubData) subdata.event_subdata_initialized = Mock() subdata.event_subdata_initialized.is_set.return_value = False @@ -78,3 +90,4 @@ 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() From de1e817eadb0f29c5e92df88c56698bf21c7724c Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Wed, 23 Sep 2026 11:17:59 +0200 Subject: [PATCH 3/4] extend for self_controlled --- .../control/counter_all/loadmanagement_prio.py | 4 ++-- .../counter_all/loadmanagement_prio_test.py | 3 +++ packages/helpermodules/subdata_test.py | 7 +++++++ packages/helpermodules/update_config.py | 14 ++++++++------ packages/helpermodules/update_config_test.py | 4 +++- 5 files changed, 23 insertions(+), 9 deletions(-) diff --git a/packages/control/counter_all/loadmanagement_prio.py b/packages/control/counter_all/loadmanagement_prio.py index 664dbaab26..571f189f2d 100644 --- a/packages/control/counter_all/loadmanagement_prio.py +++ b/packages/control/counter_all/loadmanagement_prio.py @@ -2,7 +2,7 @@ from typing import Dict, Generator, List, Tuple from control.chargepoint.chargepoint import Chargepoint -from control.consumer.usage import ConsumerUsage +from control.consumer.usage import ConsumerUsage, NOT_CONTROLLED from control.counter_all.counter_all_data import LoadmanagementPrioProtocol from modules.common.component_type import ComponentType @@ -18,7 +18,7 @@ def update_consumer_loadmanagement_prio( self: LoadmanagementPrioProtocol, consumer_id: int, usage_type: ConsumerUsage) -> bool: consumer_in_prios = self._has_loadmanagement_prio_item( ComponentType.CONSUMER, consumer_id, self.data.get.loadmanagement_prios) - if usage_type == ConsumerUsage.METER_ONLY: + if usage_type in NOT_CONTROLLED: if consumer_in_prios: self.remove_loadmanagement_prio_item(ComponentType.CONSUMER, consumer_id) return True diff --git a/packages/control/counter_all/loadmanagement_prio_test.py b/packages/control/counter_all/loadmanagement_prio_test.py index 82fe91f801..3dc1b7e640 100644 --- a/packages/control/counter_all/loadmanagement_prio_test.py +++ b/packages/control/counter_all/loadmanagement_prio_test.py @@ -89,6 +89,9 @@ def test_add_item(loadmanagement_prios: List[Dict], "children": [{"type": "consumer", "id": 2}], }], ConsumerUsage.METER_ONLY, [], True, id="remove consumer from group"), pytest.param([], ConsumerUsage.METER_ONLY, [], False, id="ignore absent meter"), + pytest.param([{"type": "consumer", "id": 2}], ConsumerUsage.SELF_CONTROLLED, [], True, + id="remove self-controlled consumer"), + pytest.param([], ConsumerUsage.SELF_CONTROLLED, [], False, id="ignore absent self-controlled consumer"), ], ) def test_update_consumer_loadmanagement_prio(loadmanagement_prios: List[Dict], diff --git a/packages/helpermodules/subdata_test.py b/packages/helpermodules/subdata_test.py index 0d093ab02f..548cdb9e50 100644 --- a/packages/helpermodules/subdata_test.py +++ b/packages/helpermodules/subdata_test.py @@ -41,6 +41,13 @@ False, id="ignore absent meter-only consumer", ), + pytest.param( + ConsumerUsage.SELF_CONTROLLED, + [{"type": "consumer", "id": 2}], + [], + True, + id="remove self-controlled consumer", + ), ], ) def test_process_consumer_usage_updates_loadmanagement_prios(usage_type: ConsumerUsage, diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 21a283f26f..95e47ab714 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -12,6 +12,7 @@ from paho.mqtt.client import Client as MqttClient, MQTTMessage from control.chargemode import Chargemode +from control.consumer.usage import NOT_CONTROLLED from control.limiting_value import LoadmanagementLimit from control.chargepoint.chargepoint_template import get_chargepoint_template_default @@ -3867,20 +3868,21 @@ def upgrade(topic: str, payload) -> Optional[dict]: def upgrade_datastore_149(self) -> None: """Verbraucher ohne Lastmanagement aus der Prioritätensteuerung entfernen.""" - meter_only_consumers = { + not_controlled_usage_types = {usage_type.value for usage_type in NOT_CONTROLLED} + not_controlled_consumers = { int(get_index(topic)) for topic, payload in self.all_received_topics.items() if re.search(r"^openWB/consumer/[0-9]+/usage$", topic) is not None - and decode_payload(payload).get("type") == "meter_only" + and decode_payload(payload).get("type") in not_controlled_usage_types } - def remove_meter_only_consumers(entries: list) -> None: + def remove_not_controlled_consumers(entries: list) -> None: for entry in entries.copy(): - if entry.get("type") == "consumer" and entry.get("id") in meter_only_consumers: + if entry.get("type") == "consumer" and entry.get("id") in not_controlled_consumers: entries.remove(entry) elif entry.get("type") == "group": children = entry.get("children", []) - remove_meter_only_consumers(children) + remove_not_controlled_consumers(children) if not children: entries.remove(entry) @@ -3888,7 +3890,7 @@ def remove_meter_only_consumers(entries: list) -> None: if topic in self.all_received_topics: loadmanagement_prios = decode_payload(self.all_received_topics[topic]) migrated_loadmanagement_prios = copy.deepcopy(loadmanagement_prios) - remove_meter_only_consumers(migrated_loadmanagement_prios) + remove_not_controlled_consumers(migrated_loadmanagement_prios) if migrated_loadmanagement_prios != loadmanagement_prios: self.__update_topic(topic, migrated_loadmanagement_prios) self._append_datastore_version(149) diff --git a/packages/helpermodules/update_config_test.py b/packages/helpermodules/update_config_test.py index 2195ac9bcd..09b7e8e87b 100644 --- a/packages/helpermodules/update_config_test.py +++ b/packages/helpermodules/update_config_test.py @@ -365,11 +365,12 @@ def test_upgrade_datastore_142_ev_chargemode_conversion(ev0_prio: bool, assert mock_pub.pub.call_count == 2 # einmal publishen für Upgrade der Datastore-Version -def test_upgrade_datastore_149_removes_meter_only_consumers(mock_pub: Mock): +def test_upgrade_datastore_149_removes_not_controlled_consumers(mock_pub: Mock): uc = UpdateConfig() uc.all_received_topics = { "openWB/consumer/1/usage": {"type": "continuous"}, "openWB/consumer/2/usage": {"type": "meter_only"}, + "openWB/consumer/4/usage": {"type": "self_controlled"}, "openWB/counter/get/loadmanagement_prios": [ {"type": "vehicle", "id": 0}, { @@ -378,6 +379,7 @@ def test_upgrade_datastore_149_removes_meter_only_consumers(mock_pub: Mock): "children": [ {"type": "consumer", "id": 1}, {"type": "consumer", "id": 2}, + {"type": "consumer", "id": 4}, ], }, {"type": "consumer", "id": 3}, From 68124096b9c904ab1b16f1fa75ffa6b4062af92f Mon Sep 17 00:00:00 2001 From: LKuemmel <76958050+LKuemmel@users.noreply.github.com> Date: Wed, 23 Sep 2026 11:23:48 +0200 Subject: [PATCH 4/4] Potential fix for pull request finding 'Preserve pre-existing empty groups during migration' Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- packages/helpermodules/update_config.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 95e47ab714..8baf996e62 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -3882,8 +3882,9 @@ def remove_not_controlled_consumers(entries: list) -> None: entries.remove(entry) elif entry.get("type") == "group": children = entry.get("children", []) + had_children = bool(children) remove_not_controlled_consumers(children) - if not children: + if had_children and not children: entries.remove(entry) topic = "openWB/counter/get/loadmanagement_prios"