From b0b1a4bd3a9129c1e818e89bc7b722a1a69c1071 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Tue, 29 Sep 2026 08:20:48 +0200 Subject: [PATCH 1/3] =?UTF-8?q?Leistungssumme=20f=C3=BCr=20Verbraucher&Z?= =?UTF-8?q?=C3=A4hler=20au=C3=9Ferhalb=20des=20HV?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/control/counter_all/counter_all.py | 38 +++-- .../control/counter_all/counter_all_data.py | 1 + .../counter_home_consumption_test.py | 157 +++++++++++++++++- packages/helpermodules/setdata.py | 1 + 4 files changed, 175 insertions(+), 22 deletions(-) diff --git a/packages/control/counter_all/counter_all.py b/packages/control/counter_all/counter_all.py index 2e8b05f724..197e776752 100644 --- a/packages/control/counter_all/counter_all.py +++ b/packages/control/counter_all/counter_all.py @@ -5,6 +5,7 @@ from typing import Any, Dict, List, Tuple from control import data +from control.consumer.consumer import Consumer from control.counter_all.counter_all_data import CounterAllData from control.counter_all.hierarchy import HierarchyMixin from control.counter_all.loadmanagement_prio import LoadmanagementPrioMixin @@ -48,8 +49,9 @@ def get_id_evu_counter(self) -> int: def set_home_consumption(self) -> None: try: - home_consumption, elements = self._calc_home_consumption() + home_consumption, not_in_home_consumption, elements = self._calc_home_consumption() home_consumption = round(home_consumption, 2) + not_in_home_consumption = round(not_in_home_consumption, 2) if home_consumption < 0: log.error( f"Ungültiger Hausverbrauch: {home_consumption}W, Berücksichtigte Komponenten neben EVU {elements}") @@ -68,6 +70,7 @@ def set_home_consumption(self) -> None: home_consumption = 0 else: self.data.set.invalid_home_consumption = 0 + self.data.set.not_in_home_consumption = not_in_home_consumption self.data.set.home_consumption = home_consumption imported, _ = self.sim_counter.sim_count(self.data.set.home_consumption) self.data.set.imported_home_consumption = round(imported, 2) @@ -96,12 +99,10 @@ def _get_is_home_consumption(self, counter: Counter, parent_home_consumption: st return counter.data.config.is_home_consumption_counter - def _get_is_home_consumption_consumer(self, consumer: Any, parent_home_consumption: str) -> str: - consumer_mode = getattr(consumer.data.config, "is_home_consumption_consumer", - CounterMode.AUTO_HOME_CONSUMPTION.value) - if consumer_mode == CounterMode.AUTO_HOME_CONSUMPTION.value: + def _get_is_home_consumption_consumer(self, consumer: Consumer, parent_home_consumption: str) -> str: + if consumer.data.config.is_home_consumption_consumer == CounterMode.AUTO_HOME_CONSUMPTION.value: return parent_home_consumption - return consumer_mode + return consumer.data.config.is_home_consumption_consumer def _get_local_power_from_counter(self, element: Dict) -> float: # Wird nur von Countern aufgerufen @@ -121,12 +122,13 @@ def _get_local_power_from_counter(self, element: Dict) -> float: return local_power def _calc_home_consumption_from_counter( - self, element: Dict, parent_home_consumption: str) -> float: + self, element: Dict, parent_home_consumption: str) -> Tuple[float, float]: # Wird nur von Countern aufgerufen # Bewertet, ob Hausverbrauch oder nicht # Gibt den Hausverbrauch des Zählers zurück home_consumption = 0.0 + not_home_consumption = 0.0 local_power = self._get_local_power_from_counter(element) counter = self._get_component(element) @@ -134,24 +136,31 @@ def _calc_home_consumption_from_counter( if child_home_consumption == CounterMode.HOME_CONSUMPTION.value: home_consumption += local_power + elif child_home_consumption == CounterMode.NOT_HOME_CONSUMPTION.value: + not_home_consumption += local_power for child in element["children"]: comp = self._get_component(child) if comp.data.get.fault_state < 2: if child["type"] == ComponentType.COUNTER.value: - home_consumption += self._calc_home_consumption_from_counter(child, child_home_consumption) + add_home_consumption, add_not_home_consumption = self._calc_home_consumption_from_counter( + child, child_home_consumption) + home_consumption += add_home_consumption + not_home_consumption += add_not_home_consumption elif child["type"] == ComponentType.CONSUMER.value: - if self._get_is_home_consumption_consumer( - comp, child_home_consumption) == CounterMode.HOME_CONSUMPTION.value: + mode = self._get_is_home_consumption_consumer(comp, child_home_consumption) + if mode == CounterMode.HOME_CONSUMPTION.value: home_consumption += comp.data.get.power + elif mode == CounterMode.NOT_HOME_CONSUMPTION.value: + not_home_consumption += comp.data.get.power else: log.warning( f"Komponente {element['type']}{comp.num} ist im Fehlerzustand und wird nicht berücksichtigt.") - return home_consumption + return home_consumption, not_home_consumption - def _calc_home_consumption(self) -> Tuple[float, Dict]: + def _calc_home_consumption(self) -> Tuple[float, float, Dict]: evu_id = self.get_id_evu_counter() # get_elements_for_downstream_calculation berücksichtigt Hybrid-Batterien @@ -167,11 +176,12 @@ def _calc_home_consumption(self) -> Tuple[float, Dict]: home_consumption = 0.0 # Rekursion startet immer beim EVU-Zähler. - home_consumption = self._calc_home_consumption_from_counter(evu_element, CounterMode.HOME_CONSUMPTION.value) + home_consumption, not_in_home_consumption = self._calc_home_consumption_from_counter( + evu_element, CounterMode.HOME_CONSUMPTION.value) home_consumption -= self.data.set.smarthome_power_excluded_from_home_consumption - return home_consumption, evu_element + return home_consumption, not_in_home_consumption, evu_element def _add_hybrid_bat(self, id: int) -> List: elements = [] diff --git a/packages/control/counter_all/counter_all_data.py b/packages/control/counter_all/counter_all_data.py index a61ba69c3c..af1fb6246a 100644 --- a/packages/control/counter_all/counter_all_data.py +++ b/packages/control/counter_all/counter_all_data.py @@ -22,6 +22,7 @@ class Set: loadmanagement_active: bool = field( default=False, metadata={"topic": "set/loadmanagement_active"}) home_consumption: float = field(default=0, metadata={"topic": "set/home_consumption"}) + not_in_home_consumption: float = field(default=0, metadata={"topic": "set/not_in_home_consumption"}) smarthome_power_excluded_from_home_consumption: float = field( default=0, metadata={"topic": "set/smarthome_power_excluded_from_home_consumption"}) diff --git a/packages/control/counter_all/counter_home_consumption_test.py b/packages/control/counter_all/counter_home_consumption_test.py index c6b01c52f1..1714d67fe2 100644 --- a/packages/control/counter_all/counter_home_consumption_test.py +++ b/packages/control/counter_all/counter_home_consumption_test.py @@ -67,16 +67,23 @@ def test_calc_home_consumption_with_configured_home_consumption_counter( @pytest.mark.parametrize(["home_consumption", + "not_in_home_consumption", "invalid_home_consumption", + "initial_not_in_home_consumption", "expected_home_consumption", - "expected_invalid_home_consumption"], - [pytest.param(500, 0, 500, 0, id="valid home consumption"), - pytest.param(-100, 0, 200, 1, id="first invalid home consumption"), - pytest.param(-100, 3, 0, 3, id="invalid home consumption, reset home consumption")]) + "expected_invalid_home_consumption", + "expected_not_in_home_consumption"], + [pytest.param(500, 150, 0, 42, 500, 0, 150, id="valid home consumption"), + pytest.param(-100, 150, 0, 42, 200, 1, 42, id="first invalid home consumption"), + pytest.param(-100, 150, 3, 42, 0, 3, 150, + id="invalid home consumption, reset home consumption")]) def test_set_home_consumption(home_consumption: int, + not_in_home_consumption: int, invalid_home_consumption: int, + initial_not_in_home_consumption: int, expected_home_consumption: int, expected_invalid_home_consumption: int, + expected_not_in_home_consumption: int, monkeypatch, data_): # setup @@ -84,7 +91,8 @@ def test_set_home_consumption(home_consumption: int, data.data.counter_data["counter0"].data.get.fault_state = FaultStateLevel.NO_ERROR c.data.set.invalid_home_consumption = invalid_home_consumption c.data.set.home_consumption = 200 - calc_home_consumption_mock = Mock(return_value=[home_consumption, []]) + c.data.set.not_in_home_consumption = initial_not_in_home_consumption + calc_home_consumption_mock = Mock(return_value=[home_consumption, not_in_home_consumption, []]) monkeypatch.setattr(CounterAll, "_calc_home_consumption", calc_home_consumption_mock) # execution @@ -93,16 +101,17 @@ def test_set_home_consumption(home_consumption: int, # evaluation assert c.data.set.invalid_home_consumption == expected_invalid_home_consumption assert c.data.set.home_consumption == expected_home_consumption + assert c.data.set.not_in_home_consumption == expected_not_in_home_consumption @pytest.mark.parametrize( ["counter_mode", "consumer_mode", "expected_home_consumption"], [ - pytest.param(CounterMode.HOME_CONSUMPTION.value, "auto_home_consumption", 1000, + pytest.param(CounterMode.HOME_CONSUMPTION.value, CounterMode.AUTO_HOME_CONSUMPTION.value, 1000, id="consumer_auto_inherits_home"), - pytest.param(CounterMode.HOME_CONSUMPTION.value, "no_home_consumption", 700, + pytest.param(CounterMode.HOME_CONSUMPTION.value, CounterMode.NOT_HOME_CONSUMPTION.value, 700, id="consumer_explicit_no_home"), - pytest.param(CounterMode.NOT_HOME_CONSUMPTION.value, "home_consumption", 300, + pytest.param(CounterMode.NOT_HOME_CONSUMPTION.value, CounterMode.HOME_CONSUMPTION.value, 300, id="consumer_explicit_home_overrides_parent"), ], ) @@ -142,6 +151,138 @@ def test_calc_home_consumption_with_consumer_modes(counter_mode: str, assert home_consumption == expected_home_consumption +@pytest.mark.parametrize( + ["consumer_mode", "parent_mode", "expected_mode"], + [ + pytest.param(CounterMode.AUTO_HOME_CONSUMPTION.value, CounterMode.HOME_CONSUMPTION.value, + CounterMode.HOME_CONSUMPTION.value, id="auto_inherits_parent"), + pytest.param(CounterMode.HOME_CONSUMPTION.value, CounterMode.NOT_HOME_CONSUMPTION.value, + CounterMode.HOME_CONSUMPTION.value, id="explicit_home_overrides_parent"), + pytest.param(CounterMode.NOT_HOME_CONSUMPTION.value, CounterMode.HOME_CONSUMPTION.value, + CounterMode.NOT_HOME_CONSUMPTION.value, id="explicit_no_home_overrides_parent"), + ], +) +def test_get_is_home_consumption_consumer(consumer_mode: str, + parent_mode: str, + expected_mode: str): + c = CounterAll() + consumer = Mock(data=Mock(config=Mock(is_home_consumption_consumer=consumer_mode))) + + mode = c._get_is_home_consumption_consumer(consumer, parent_mode) + + assert mode == expected_mode + + +def test_get_local_power_from_counter_ignores_faulty_child(): + data.data_init(Mock()) + c = CounterAll() + + data.data.counter_data = { + "counter0": Mock( + spec=Counter, + num=0, + data=Mock( + spec=CounterData, + get=Mock(spec=CounterGet, power=1000, fault_state=0), + config=Mock(spec=CounterConfig, is_home_consumption_counter=CounterMode.HOME_CONSUMPTION.value), + ), + ), + "counter1": Mock( + spec=Counter, + num=1, + data=Mock( + spec=CounterData, + get=Mock(spec=CounterGet, power=300, fault_state=0), + config=Mock(spec=CounterConfig, is_home_consumption_counter=CounterMode.HOME_CONSUMPTION.value), + ), + ), + "counter2": Mock( + spec=Counter, + num=2, + data=Mock( + spec=CounterData, + get=Mock(spec=CounterGet, power=200, fault_state=2), + config=Mock(spec=CounterConfig, is_home_consumption_counter=CounterMode.HOME_CONSUMPTION.value), + ), + ), + } + + element = { + "id": 0, + "type": "counter", + "children": [ + {"id": 1, "type": "counter", "children": []}, + {"id": 2, "type": "counter", "children": []}, + ], + } + + local_power = c._get_local_power_from_counter(element) + + assert local_power == 700 + + +def test_calc_home_consumption_from_counter_splits_home_and_not_home(): + data.data_init(Mock()) + c = CounterAll() + c.data.get.hierarchy = [{ + "id": 0, + "type": "counter", + "children": [ + {"id": 7, "type": "consumer", "children": []}, + { + "id": 8, + "type": "counter", + "children": [{"id": 9, "type": "consumer", "children": []}], + }, + ], + }] + + data.data.counter_data = { + "counter0": Mock( + spec=Counter, + num=0, + data=Mock( + spec=CounterData, + get=Mock(spec=CounterGet, power=1000, fault_state=0), + config=Mock(spec=CounterConfig, + is_home_consumption_counter=CounterMode.AUTO_HOME_CONSUMPTION.value), + ), + ), + "counter8": Mock( + spec=Counter, + num=8, + data=Mock( + spec=CounterData, + get=Mock(spec=CounterGet, power=100, fault_state=0), + config=Mock(spec=CounterConfig, + is_home_consumption_counter=CounterMode.HOME_CONSUMPTION.value), + ), + ), + } + data.data.consumer_data = { + "consumer7": Mock( + num=7, + data=Mock( + get=Mock(power=400, fault_state=0), + config=Mock(is_home_consumption_consumer=CounterMode.HOME_CONSUMPTION.value), + ), + ), + "consumer9": Mock( + num=9, + data=Mock( + get=Mock(power=50, fault_state=0), + config=Mock(is_home_consumption_consumer=CounterMode.NOT_HOME_CONSUMPTION.value), + ), + ), + } + + home_consumption, not_home_consumption = c._calc_home_consumption_from_counter( + c.data.get.hierarchy[0], CounterMode.HOME_CONSUMPTION.value) + + assert home_consumption == 950 + assert not_home_consumption == 50 + + def hierarchy_home_consumption_standard() -> CounterAll: # counter0 # | diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 665ff72fb9..d67736b248 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -952,6 +952,7 @@ def process_counter_topic(self, msg: mqtt.MQTTMessage): elif "openWB/set/counter/set/invalid_home_consumption" in msg.topic: self._validate_value(msg, int, [(0, 3)]) elif ("openWB/set/counter/set/home_consumption" in msg.topic or + "openWB/set/counter/set/not_in_home_consumption" in msg.topic or "openWB/set/counter/set/imported_home_consumption" in msg.topic or "openWB/set/counter/set/daily_yield_home_consumption" in msg.topic or "openWB/set/counter/set/disengageable_smarthome_power" in msg.topic): From 9050d767684911d3cf034542fa5af4d7d142c01d Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Tue, 29 Sep 2026 11:49:39 +0200 Subject: [PATCH 2/3] Review --- data/config/mosquitto/public/default-dynamic-security.json | 6 ++++++ packages/control/process.py | 3 ++- packages/helpermodules/create_debug.py | 5 +++++ packages/helpermodules/update_config.py | 1 + 4 files changed, 14 insertions(+), 1 deletion(-) diff --git a/data/config/mosquitto/public/default-dynamic-security.json b/data/config/mosquitto/public/default-dynamic-security.json index 8e0cbba91a..adae24d06a 100644 --- a/data/config/mosquitto/public/default-dynamic-security.json +++ b/data/config/mosquitto/public/default-dynamic-security.json @@ -2994,6 +2994,12 @@ "priority": 0, "allow": true }, + { + "acltype": "publishClientReceive", + "topic": "openWB/counter/set/not_in_home_consumption", + "priority": 0, + "allow": true + }, { "acltype": "publishClientReceive", "topic": "openWB/counter/set/imported_home_consumption", diff --git a/packages/control/process.py b/packages/control/process.py index 607d227c4c..81c472eb93 100644 --- a/packages/control/process.py +++ b/packages/control/process.py @@ -217,7 +217,8 @@ def _start_consumer(self, consumer: Consumer) -> Optional[Thread]: bat_soc=data.data.bat_all_data.data.get.soc, cp_power=data.data.cp_all_data.data.get.power, evu_power=data.data.counter_all_data.get_evu_counter().data.get.power, - home_consumption=data.data.counter_all_data.data.set.home_consumption, + home_consumption=(data.data.counter_all_data.data.set.home_consumption + + data.data.counter_all_data.data.set.not_in_home_consumption), pv_power=data.data.pv_all_data.data.get.power ) return Thread(target=consumer.module.send_values, diff --git a/packages/helpermodules/create_debug.py b/packages/helpermodules/create_debug.py index 6c2db31f0c..eae3d69e55 100644 --- a/packages/helpermodules/create_debug.py +++ b/packages/helpermodules/create_debug.py @@ -221,6 +221,11 @@ def config_and_state(): except Exception: home_consumption = "Keine Daten" parsed_data += f"Home_Consumption:\n {home_consumption}\n" + try: + not_in_home_consumption = filter_log_file('mqtt', 'openWB/counter/set/not_in_home_consumption', 5) + except Exception: + not_in_home_consumption = "Keine Daten" + parsed_data += f"Not_In_Home_Consumption:\n {not_in_home_consumption}\n" with ErrorHandlingContext(): parsed_data += "\n## Charge Points ##\n" parsed_data += f"CP_All_Power: {data.data.cp_all_data.data.get.power / 1000} kW\n\n" diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 862f29bb68..40871f51ee 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -225,6 +225,7 @@ class UpdateConfig: "^openWB/counter/set/imported_home_consumption$", "^openWB/counter/set/invalid_home_consumption$", "^openWB/counter/set/home_consumption$", + "^openWB/counter/set/not_in_home_consumption$", "^openWB/counter/set/daily_yield_home_consumption$", "^openWB/counter/set/simulation$", "^openWB/counter/[0-9]+/get/voltages$", From 59ddb140a94f5f6bbf45df4fec9437e4d4103b8b Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Tue, 29 Sep 2026 12:04:36 +0200 Subject: [PATCH 3/3] review --- packages/control/counter_all/counter_all.py | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/control/counter_all/counter_all.py b/packages/control/counter_all/counter_all.py index 197e776752..5ef6cb5972 100644 --- a/packages/control/counter_all/counter_all.py +++ b/packages/control/counter_all/counter_all.py @@ -180,6 +180,7 @@ def _calc_home_consumption(self) -> Tuple[float, float, Dict]: evu_element, CounterMode.HOME_CONSUMPTION.value) home_consumption -= self.data.set.smarthome_power_excluded_from_home_consumption + not_in_home_consumption += self.data.set.smarthome_power_excluded_from_home_consumption return home_consumption, not_in_home_consumption, evu_element