From b8a69c1a003e75a719763a48fc83dbb157c01554 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Wed, 29 Jul 2026 15:09:07 +0200 Subject: [PATCH 01/11] Add Hausverbrauch Counter --- .../public/default-dynamic-security.json | 12 + packages/control/counter.py | 2 + packages/control/counter_all.py | 64 ++++- .../control/counter_home_consumption_test.py | 256 +++++++++++++++++- packages/helpermodules/setdata.py | 2 + packages/helpermodules/update_config.py | 15 +- 6 files changed, 343 insertions(+), 8 deletions(-) diff --git a/data/config/mosquitto/public/default-dynamic-security.json b/data/config/mosquitto/public/default-dynamic-security.json index 10ed378e2f..54737cd054 100644 --- a/data/config/mosquitto/public/default-dynamic-security.json +++ b/data/config/mosquitto/public/default-dynamic-security.json @@ -1526,6 +1526,12 @@ "priority": 0, "allow": true }, + { + "acltype": "publishClientSend", + "topic": "openWB/set/counter/+/config/is_home_consumption_counter", + "priority": 0, + "allow": true + }, { "acltype": "publishClientSend", "topic": "openWB/set/counter/+/config/max_total_power", @@ -1586,6 +1592,12 @@ "priority": 0, "allow": true }, + { + "acltype": "publishClientReceive", + "topic": "openWB/counter/+/config/is_home_consumption_counter", + "priority": 0, + "allow": true + }, { "acltype": "publishClientReceive", "topic": "openWB/counter/+/config/max_total_power", diff --git a/packages/control/counter.py b/packages/control/counter.py index e7351b3cef..5d067036cd 100644 --- a/packages/control/counter.py +++ b/packages/control/counter.py @@ -27,6 +27,7 @@ def get_counter_default_config(): return {"max_power_errorcase": 7000, "max_currents": [35]*3, "max_total_power": 24000, + "is_home_consumption_counter": False } @@ -42,6 +43,7 @@ class Config: max_currents: List[float] = field(default_factory=currents_list_factory, metadata={ "topic": "get/max_currents"}) max_total_power: float = field(default=0, metadata={"topic": "get/max_total_power"}) + is_home_consumption_counter: bool = field(default=0, metadata={"topic": "get/is_home_consumption_counter"}) def config_factory() -> Config: diff --git a/packages/control/counter_all.py b/packages/control/counter_all.py index 89f214b773..2f7492bab2 100644 --- a/packages/control/counter_all.py +++ b/packages/control/counter_all.py @@ -151,20 +151,29 @@ def _validate_home_consumption_counter(self): def _calc_home_consumption(self) -> Tuple[float, List]: power = 0 + home_consumption = 0 if self.data.config.home_consumption_source_id is None: id_source = self.get_id_evu_counter() else: id_source = self.data.config.home_consumption_source_id + elements_to_sum_up = self.get_elements_for_downstream_calculation(id_source) for element in elements_to_sum_up: if element["type"] == ComponentType.CHARGEPOINT.value: component = data.data.cp_data[f"cp{element['id']}"] elif element["type"] == ComponentType.BAT.value: component = data.data.bat_data[f"bat{element['id']}"] - elif element["type"] == ComponentType.COUNTER.value: - component = data.data.counter_data[f"counter{element['id']}"] elif element["type"] == ComponentType.INVERTER.value: component = data.data.pv_data[f"pv{element['id']}"] + elif element["type"] == ComponentType.COUNTER.value: + component = data.data.counter_data[f"counter{element['id']}"] + + home, not_home = self._calc_home_consumption_child(element) + if component.data.config.is_home_consumption_counter: + # zähl die differenze mit zum Hausverbrauch, ansonsten nicht + home_consumption += float(component.data.get.power) - not_home + else: + home_consumption += home if component.data.get.fault_state < 2: power += component.data.get.power @@ -172,7 +181,56 @@ def _calc_home_consumption(self) -> Tuple[float, List]: log.warning( f"Komponente {element['type']}{component.num} ist im Fehlerzustand und wird nicht berücksichtigt.") evu = data.data.counter_data[f"counter{id_source}"].data.get.power - return evu - power - self.data.set.smarthome_power_excluded_from_home_consumption, elements_to_sum_up + return (evu - power + home_consumption - self.data.set.smarthome_power_excluded_from_home_consumption, + elements_to_sum_up) + + def _calc_home_consumption_child(self, element): + home_consumption = 0.0 + not_home_consumption = 0.0 + for child in element["children"]: + if child["type"] == ComponentType.COUNTER.value: + component = data.data.counter_data[f"counter{child['id']}"] + # Wenn der unterCounter im Fehlerzustand ist, wird er nicht berücksichtigt. + if component.data.get.fault_state >= 1: + log.warning( + f"Komponente {child['type']}{component.num} ist im Fehlerzustand und " + "wird nicht berücksichtigt bei der Berechnung des Hausverbrauchs.") + continue + + # Hat der unterCounter Kinder? Dann werden diese ebenfalls berücksichtigt. + if child["children"]: + home, not_home = self._calc_home_consumption_child(child) + if component.data.config.is_home_consumption_counter: + # Beim Hausverbrauchszähler wird nur der Anteil ohne + # bereits bekannte Nicht-Hausverbraucher addiert. + home_consumption += float(component.data.get.power) - not_home + not_home_consumption += not_home + else: + home_consumption += home + not_home_consumption += not_home + else: + # Blatt-Unterzähler muss direkt berücksichtigt werden, sonst fehlt sein Beitrag komplett. + if component.data.config.is_home_consumption_counter: + home_consumption += float(component.data.get.power) + else: + not_home_consumption += component.data.get.power + + else: + if child["type"] == ComponentType.CHARGEPOINT.value: + component = data.data.cp_data[f"cp{child['id']}"] + elif child["type"] == ComponentType.BAT.value: + component = data.data.bat_data[f"bat{child['id']}"] + elif child["type"] == ComponentType.INVERTER.value: + component = data.data.pv_data[f"pv{child['id']}"] + + if component.data.get.fault_state < 2: + not_home_consumption += component.data.get.power + else: + log.warning( + f"Komponente {child['type']}{component.num} ist im Fehlerzustand und " + "wird nicht berücksichtigt bei der Berechnung des Hausverbrauchs.") + + return home_consumption, not_home_consumption def _add_hybrid_bat(self, id: int) -> List: elements = [] diff --git a/packages/control/counter_home_consumption_test.py b/packages/control/counter_home_consumption_test.py index 6303dd63e9..5ff2138139 100644 --- a/packages/control/counter_home_consumption_test.py +++ b/packages/control/counter_home_consumption_test.py @@ -3,21 +3,65 @@ import pytest from control import data -from packages.conftest import hierarchy_hc_counter, hierarchy_standard, hierarchy_hybrid, hierarchy_nested + + +from control.bat import Bat, BatData +from control.bat import Get as BatGet +from control.bat import Set as BatSet +from control.chargepoint.chargepoint import Chargepoint, ChargepointData +from control.chargepoint.chargepoint_data import Config, Get, Set +from control.counter import Counter, CounterData +from control.counter import Config as CounterConfig +from control.counter import Get as CounterGet +from control.counter import Set as CounterSet from control.counter_all import CounterAll +from control.pv import Pv, PvData +from control.pv import Config as PvConfig +from control.pv import Get as PvGet + +from modules.chargepoints.mqtt.chargepoint_module import ChargepointModule +from modules.common.component_state import ChargepointState +from modules.common.store._api import LoggingValueStore + +from packages.conftest import ( + hierarchy_hc_counter, + hierarchy_standard, + hierarchy_hybrid, + hierarchy_nested) from modules.common.fault_state import FaultStateLevel @pytest.mark.parametrize("counter_all", [pytest.param(hierarchy_standard, id="standard"), pytest.param(hierarchy_hybrid, id="hybrid"), - pytest.param(hierarchy_nested, id="nested")]) + pytest.param(hierarchy_nested, id="nested") + ]) def test_calc_home_consumption(counter_all: Callable[[], CounterAll], data_): c = counter_all() home_consumption = c._calc_home_consumption()[0] assert home_consumption == 500 +@pytest.mark.parametrize( + ["counter_all", "expected_home_consumption"], + [ + pytest.param("hierarchy_hybrid_with_home_consumption", 500, id="hierarchy_hybrid_with_home_consumption"), + pytest.param("hierarchy_nested_with_home_consumption", 500, id="hierarchy_nested_with_home_consumption"), + pytest.param("hierarchy_standard_with_home_consumption", 500, id="hierarchy_standard_with_home_consumption"), + pytest.param("hierarchy_nested_two_level_with_home_consumption", + 500, id="hierarchy_nested_two_level_with_home_consumption"), + ], +) +def test_calc_home_consumption_with_configured_home_consumption_counter( + counter_all: str, + expected_home_consumption: int, + data_home_consumption, +): + c = globals()[counter_all]() + home_consumption = c._calc_home_consumption()[0] + assert home_consumption == expected_home_consumption + + def test_calc_home_consumption_hc_counter(data_hc_counter_): c = hierarchy_hc_counter() c.data.config.home_consumption_source_id = 6 @@ -31,7 +75,7 @@ def test_calc_home_consumption_hc_counter(data_hc_counter_): "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")]) + pytest.param(-100, 3, 0, 3, id="invalid home consumption, reset home consumption")]) def test_set_home_consumption(home_consumption: int, invalid_home_consumption: int, expected_home_consumption: int, @@ -62,5 +106,209 @@ def test_validate_home_consumption_counter(monkeypatch): with pytest.raises(Exception) as e: c._validate_home_consumption_counter() - assert str(e.value) == CounterAll.EVU_IS_HC_COUNTER_ERROR + + +def hierarchy_standard_with_home_consumption() -> CounterAll: + # counter0 + # | + # - cp4 + # - cp5 + # - cp3 + # - inverter1 + # - bat2 + # counter_8 <-- home consumption counter + # + + # counter8 = 500 <- home consumption + # Final Home Consumption = 0 + c = CounterAll() + c.data.get.hierarchy = [{"id": 0, "type": "counter", + "children": [ + {"id": 4, "type": "cp", "children": []}, + {"id": 5, "type": "cp", "children": []}, + {"id": 3, "type": "cp", "children": []}, + {"id": 1, "type": "inverter", "children": []}, + {"id": 2, "type": "bat", "children": []}]}, + {"id": 8, "type": "counter", + "children": []}] + return c + + +def hierarchy_hybrid_with_home_consumption() -> CounterAll: + # counter0 + # | + # - cp3 + # - cp4 + # - counter8 <-- home consumption counter + # | + # - cp5 + # - inverter1 + # - bat2 + # counter8 = 500 <- home consumption + # Final Home Consumption = 500 + c = CounterAll() + c.data.get.hierarchy = [{"id": 0, "type": "counter", + "children": [ + {"id": 3, "type": "cp", "children": []}, + {"id": 4, "type": "cp", "children": []}, + {"id": 8, "type": "counter", + "children": [ + {"id": 5, "type": "cp", "children": []}]}, + {"id": 1, "type": "inverter", "children": []}, + {"id": 2, "type": "bat", "children": []}]}] + return c + + +def hierarchy_nested_with_home_consumption() -> CounterAll: + # counter0 + # | + # - cp3 + # - counter6 + # | + # - cp4 + # - counter_8 <-- home consumption counter + # | + # - cp5 + # - inverter1 + # - bat2 + + # counter8 = 500 <- home consumption + # Final Home Consumption = 500 + c = CounterAll() + c.data.get.hierarchy = [{"id": 0, "type": "counter", + "children": [ + {"id": 3, "type": "cp", "children": []}, + {"id": 6, "type": "counter", + "children": [ + {"id": 4, "type": "cp", "children": []}, + {"id": 8, "type": "counter", + "children": [ + {"id": 5, "type": "cp", "children": []}]}]}, + {"id": 1, "type": "inverter", "children": []}, + {"id": 2, "type": "bat", "children": []}]}] + return c + + +def hierarchy_nested_two_level_with_home_consumption() -> CounterAll: + # counter0 + # | + # - cp3 + # - counter9 <-- home consumption counter + # | + # - cp4 + # - counter_10 <-- home consumption counter + # | + # - cp5 + # + # - inverter1 + # - bat2 + + # coutner9 = 250 <- home consumption + # counter10 = 250 <- home consumption + # Final Home Consumption = 500 + c = CounterAll() + c.data.get.hierarchy = [{"id": 0, "type": "counter", + "children": [ + {"id": 3, "type": "cp", "children": []}, + {"id": 9, "type": "counter", + "children": [ + {"id": 4, "type": "cp", "children": []}, + {"id": 10, "type": "counter", + "children": [ + {"id": 5, "type": "cp", "children": []}]}, + ]}, + {"id": 1, "type": "inverter", "children": []}, + {"id": 2, "type": "bat", "children": []}]}] + return c + + +@pytest.fixture() +def data_home_consumption() -> None: + data.data_init(Mock()) + data.data.cp_data = { + "cp3": Mock(spec=Chargepoint, data=Mock(spec=ChargepointData, + config=Mock(spec=Config, phase_1=1), + get=Mock(spec=Get, currents=[30, 0, 0], power=6900, + daily_imported=10000, daily_exported=0, imported=56000, + fault_state=0), + set=Mock(spec=Set, loadmanagement_available=True)), + chargepoint_module=Mock(spec=ChargepointModule, + store=Mock(spec=LoggingValueStore, + delegate=Mock(spec=LoggingValueStore, + state=ChargepointState(currents=[30, 0, 0], + power=6900, + plug_state=False, + charge_state=False, + imported=None, + exported=None, + phases_in_use=0))))), + "cp4": Mock(spec=Chargepoint, data=Mock(spec=ChargepointData, + config=Mock(spec=Config, phase_1=2), + get=Mock(spec=Get, currents=[0, 15, 15], power=6900, + daily_imported=10000, daily_exported=0, imported=60000, + fault_state=0), + set=Mock(spec=Set, loadmanagement_available=True)), + chargepoint_module=Mock(spec=ChargepointModule, + store=Mock(spec=LoggingValueStore, + delegate=Mock(spec=LoggingValueStore, + state=ChargepointState(currents=[0, 15, 15], + power=6900, + plug_state=False, + charge_state=False, + imported=None, + exported=None, + phases_in_use=0))))), + "cp5": Mock(spec=Chargepoint, data=Mock(spec=ChargepointData, + config=Mock(spec=Config, phase_1=3), + get=Mock(spec=Get, currents=[10]*3, power=6900, + daily_imported=10000, daily_exported=0, imported=62000, + fault_state=0), + set=Mock(spec=Set, loadmanagement_available=True)), + chargepoint_module=Mock(spec=ChargepointModule, + store=Mock(spec=LoggingValueStore, + delegate=Mock(spec=LoggingValueStore, + state=ChargepointState(currents=[10]*3, + power=6900, + plug_state=False, + charge_state=False, + imported=None, + exported=None, + phases_in_use=0)))))} + data.data.bat_data.update({"bat2": Mock(spec=Bat, num=2, data=Mock(spec=BatData, get=Mock( + spec=BatGet, power=-5000, daily_imported=6200, daily_exported=3000, imported=12000, exported=10000, + currents=None, fault_state=0), + set=Mock(spec=BatSet, power_limit=None)))}) + data.data.pv_data.update({"pv1": Mock(spec=Pv, data=Mock( + spec=PvData, get=Mock(spec=PvGet, power=-10000, daily_exported=6000, exported=27000, currents=None, + fault_state=0), config=Mock(spec=PvConfig, max_ac_out=10000)))}) + data.data.counter_data.update({ + "counter0": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( + spec=CounterGet, currents=[40]*3, power=6200, daily_imported=45000, daily_exported=3000, fault_state=0), + config=Mock(spec=CounterConfig, is_home_consumption_counter=False))), + "counter6": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( + spec=CounterGet, currents=[25, 10, 25], power=14300, daily_imported=20000, daily_exported=0, + imported=14000, exported=18000, fault_state=0), + config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=False), + set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), + "counter7": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( + spec=CounterGet, currents=[25, 10, 25], power=20700, daily_imported=20000, daily_exported=0, + imported=14000, exported=18000, fault_state=0), + config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=False), + set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), + + "counter8": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( + spec=CounterGet, currents=[25, 10, 25], power=7400, daily_imported=20000, daily_exported=0, + imported=14000, exported=18000, fault_state=0), + config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=True), + set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), + "counter9": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( + spec=CounterGet, currents=[25, 10, 25], power=14300, daily_imported=20000, daily_exported=0, + imported=14000, exported=18000, fault_state=0), + config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=True), + set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), + "counter10": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( + spec=CounterGet, currents=[25, 10, 25], power=7150, daily_imported=20000, daily_exported=0, + imported=14000, exported=18000, fault_state=0), + config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=True), + set=Mock(spec=CounterSet, raw_currents_left=[31]*3)))}) diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 7973936181..d0400aa576 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -946,6 +946,8 @@ def process_counter_topic(self, msg: mqtt.MQTTMessage): elif ("/config/max_total_power" in msg.topic or "/config/max_power_errorcase" in msg.topic): self._validate_value(msg, int, [(0, float("inf"))]) + elif ("/config/is_home_consumption_counter" in msg.topic): + self._validate_value(msg, bool) elif subdata.SubData.counter_data.get(f"counter{get_index(msg.topic)}"): if ("/get/powers" in msg.topic or "/get/currents" in msg.topic): diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 48d2f72b1e..0a40a77b71 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -58,7 +58,7 @@ class UpdateConfig: - DATASTORE_VERSION = 137 + DATASTORE_VERSION = 138 valid_topic = [ "^openWB/bat/config/bat_control_activated$", @@ -213,6 +213,7 @@ class UpdateConfig: "^openWB/counter/[0-9]+/config/max_power_errorcase$", "^openWB/counter/[0-9]+/config/max_currents$", "^openWB/counter/[0-9]+/config/max_total_power$", + "^openWB/counter/[0-9]+/config/is_home_consumption_counter$", "^openWB/general/allow_unencrypted_access$", "^openWB/general/extern$", @@ -3481,3 +3482,15 @@ def upgrade(topic: str, payload) -> Optional[dict]: return {topic: payload} self._loop_all_received_topics(upgrade) self._append_datastore_version(137) + + + def upgrade_datastore_138(self) -> None: + def upgrade(topic: str, payload) -> Optional[dict]: + + if re.search("openWB/counter/[0-9]+/config", topic) is not None: + index = get_index(topic) + if f"openWB/counter/{index}/config/is_home_consumption_counter" not in self.all_received_topics: + is_home_consumption_counter = get_counter_default_config()["is_home_consumption_counter"] + return {f"openWB/counter/{index}/config/is_home_consumption_counter": is_home_consumption_counter} + self._loop_all_received_topics(upgrade) + self._append_datastore_version(138) From e46aa39578a43ba438a8190edad01e194e56a27b Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Fri, 31 Jul 2026 07:32:47 +0200 Subject: [PATCH 02/11] Refactor and Fix Hausverbrauch Counter --- .../public/default-dynamic-security.json | 12 -- packages/conftest.py | 5 +- packages/control/counter_all.py | 114 +++++++++--------- .../control/counter_home_consumption_test.py | 26 ++-- packages/helpermodules/create_debug.py | 15 ++- .../measurement_logging/write_log.py | 3 +- packages/helpermodules/setdata.py | 2 - packages/helpermodules/update_config.py | 15 ++- 8 files changed, 87 insertions(+), 105 deletions(-) diff --git a/data/config/mosquitto/public/default-dynamic-security.json b/data/config/mosquitto/public/default-dynamic-security.json index 54737cd054..e9ace61b90 100644 --- a/data/config/mosquitto/public/default-dynamic-security.json +++ b/data/config/mosquitto/public/default-dynamic-security.json @@ -1496,12 +1496,6 @@ "priority": 0, "allow": true }, - { - "acltype": "publishClientSend", - "topic": "openWB/set/counter/config/home_consumption_source_id", - "priority": 0, - "allow": true - }, { "acltype": "publishClientSend", "topic": "openWB/set/counter/config/consider_less_charging", @@ -1556,12 +1550,6 @@ "priority": 0, "allow": true }, - { - "acltype": "publishClientReceive", - "topic": "openWB/counter/config/home_consumption_source_id", - "priority": 0, - "allow": true - }, { "acltype": "publishClientReceive", "topic": "openWB/counter/config/consider_less_charging", diff --git a/packages/conftest.py b/packages/conftest.py index b58a69d591..b99bb5a9a5 100644 --- a/packages/conftest.py +++ b/packages/conftest.py @@ -189,11 +189,12 @@ def data_() -> None: fault_state=0), config=Mock(spec=PvConfig, max_ac_out=10000)))}) data.data.counter_data.update({ "counter0": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( - spec=CounterGet, currents=[40]*3, power=6200, daily_imported=45000, daily_exported=3000, fault_state=0))), + spec=CounterGet, currents=[40]*3, power=6200, daily_imported=45000, daily_exported=3000, fault_state=0), + config=Mock(spec=CounterConfig, is_home_consumption_counter=True))), "counter6": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( spec=CounterGet, currents=[25, 10, 25], power=13800, daily_imported=20000, daily_exported=0, imported=14000, exported=18000, fault_state=0), - config=Mock(spec=CounterConfig, max_currents=[32]*3), + config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=False), set=Mock(spec=CounterSet, raw_currents_left=[31]*3)))}) diff --git a/packages/control/counter_all.py b/packages/control/counter_all.py index 2f7492bab2..36ee85e5ec 100644 --- a/packages/control/counter_all.py +++ b/packages/control/counter_all.py @@ -20,8 +20,6 @@ @dataclass class Config: - home_consumption_source_id: Optional[str] = field( - default=None, metadata={"topic": "config/home_consumption_source_id"}) consider_less_charging: bool = field( default=False, metadata={"topic": "config/consider_less_charging"}) @@ -101,15 +99,12 @@ def get_id_evu_counter(self) -> int: def set_home_consumption(self) -> None: try: - self._validate_home_consumption_counter() + # self._validate_home_consumption_counter() home_consumption, elements = self._calc_home_consumption() if home_consumption < 0: log.error( f"Ungültiger Hausverbrauch: {home_consumption}W, Berücksichtigte Komponenten neben EVU {elements}") - if self.data.config.home_consumption_source_id is None: - hc_counter_source = self.get_evu_counter_str() - else: - hc_counter_source = f"counter{self.data.config.home_consumption_source_id}" + hc_counter_source = self.get_evu_counter_str() hc_counter_data = data.data.counter_data[hc_counter_source].data if hc_counter_data.get.fault_state == FaultStateLevel.NO_ERROR: hc_counter_data.get.fault_state = FaultStateLevel.WARNING.value @@ -130,32 +125,15 @@ def set_home_consumption(self) -> None: except Exception: log.exception("Fehler in der allgemeinen Zähler-Klasse") - EVU_IS_HC_COUNTER_ERROR = ("Der EVU-Zähler kann nicht als Quelle für den Hausverbrauch verwendet werden. Meist ist " - "der Zähler am EVU-Punkt installiert, dann muss im Lastmanagement unter Hausverbrauch" - " 'von openWB berechnen' ausgewählt werden. Wenn der Zähler im Hausverbrauchszweig " - "installiert ist, einen virtuellen Zähler anlegen und im Lastmanagement ganz links " - "anordnen.") - - def _validate_home_consumption_counter(self): - if self.data.config.home_consumption_source_id is not None: - if self.data.config.home_consumption_source_id == self.get_id_evu_counter(): - hc_counter_data = data.data.counter_data[self.get_evu_counter_str()].data - hc_counter_data.get.fault_state = FaultStateLevel.ERROR.value - hc_counter_data.get.fault_str = self.EVU_IS_HC_COUNTER_ERROR - evu_counter = self.get_id_evu_counter() - Pub().pub(f"openWB/set/counter/{evu_counter}/get/fault_state", - hc_counter_data.get.fault_state) - Pub().pub(f"openWB/set/counter/{evu_counter}/get/fault_str", - hc_counter_data.get.fault_str) - raise Exception(self.EVU_IS_HC_COUNTER_ERROR) - def _calc_home_consumption(self) -> Tuple[float, List]: power = 0 home_consumption = 0 - if self.data.config.home_consumption_source_id is None: - id_source = self.get_id_evu_counter() - else: - id_source = self.data.config.home_consumption_source_id + not_home_consumption = 0 + not_home_consumption_evu = 0 + id_source = self.get_id_evu_counter() + evu_is_home = data.data.counter_data[f"counter{id_source}"].data.config.is_home_consumption_counter + evu = data.data.counter_data[f"counter{id_source}"].data.get.power + home_child = False elements_to_sum_up = self.get_elements_for_downstream_calculation(id_source) for element in elements_to_sum_up: @@ -168,52 +146,78 @@ def _calc_home_consumption(self) -> Tuple[float, List]: elif element["type"] == ComponentType.COUNTER.value: component = data.data.counter_data[f"counter{element['id']}"] - home, not_home = self._calc_home_consumption_child(element) - if component.data.config.is_home_consumption_counter: - # zähl die differenze mit zum Hausverbrauch, ansonsten nicht - home_consumption += float(component.data.get.power) - not_home - else: + is_home_branch = evu_is_home or component.data.config.is_home_consumption_counter + + # Alles was unter dem Counter hängt + home, not_home, home_child = self._calc_home_consumption_child(element, is_home_branch) + + # Wurde in den Kindern ein Hausverbrauchszähler gefunden, dann wird der Hausverbrauch aus den Kindern übernommen. + if home_child: home_consumption += home + not_home_consumption += not_home + + else: + home_consumption += 0.0 + not_home_consumption += home + not_home + + # Der aktuelle Counter selber + if is_home_branch: + home_consumption += float(component.data.get.power) - home - not_home + else: + not_home_consumption += float(component.data.get.power) - home - not_home if component.data.get.fault_state < 2: + # Power über alles power += component.data.get.power + + # Power von allen Komponenten aus der ersten ebene des EVU + if element["type"] != ComponentType.COUNTER.value: + not_home_consumption_evu += component.data.get.power else: log.warning( f"Komponente {element['type']}{component.num} ist im Fehlerzustand und wird nicht berücksichtigt.") - evu = data.data.counter_data[f"counter{id_source}"].data.get.power - return (evu - power + home_consumption - self.data.set.smarthome_power_excluded_from_home_consumption, - elements_to_sum_up) - def _calc_home_consumption_child(self, element): + home_consumption_evu = evu - power + + if evu_is_home: + return evu - not_home_consumption - not_home_consumption_evu, elements_to_sum_up + else: + return evu - not_home_consumption - not_home_consumption_evu - home_consumption_evu, elements_to_sum_up + + def _calc_home_consumption_child(self, element, is_home) -> Tuple[float, float, bool]: + is_home_new = is_home home_consumption = 0.0 not_home_consumption = 0.0 for child in element["children"]: if child["type"] == ComponentType.COUNTER.value: component = data.data.counter_data[f"counter{child['id']}"] - # Wenn der unterCounter im Fehlerzustand ist, wird er nicht berücksichtigt. - if component.data.get.fault_state >= 1: + # Wenn der unter Counter im Fehlerzustand ist, wird er nicht berücksichtigt. + if component.data.get.fault_state >= 2: log.warning( f"Komponente {child['type']}{component.num} ist im Fehlerzustand und " "wird nicht berücksichtigt bei der Berechnung des Hausverbrauchs.") continue + child_is_home = is_home or component.data.config.is_home_consumption_counter - # Hat der unterCounter Kinder? Dann werden diese ebenfalls berücksichtigt. if child["children"]: - home, not_home = self._calc_home_consumption_child(child) - if component.data.config.is_home_consumption_counter: - # Beim Hausverbrauchszähler wird nur der Anteil ohne - # bereits bekannte Nicht-Hausverbraucher addiert. - home_consumption += float(component.data.get.power) - not_home - not_home_consumption += not_home - else: + # Alles was unter dem Counter hängt + home, not_home, child_branch_is_home = self._calc_home_consumption_child(child, child_is_home) + + is_home_new = is_home_new or child_branch_is_home + + # Wurde in den Kindern ein Hausverbrauchszähler gefunden, dann wird der Hausverbrauch aus den Kindern übernommen. + if child_branch_is_home: home_consumption += home not_home_consumption += not_home - else: - # Blatt-Unterzähler muss direkt berücksichtigt werden, sonst fehlt sein Beitrag komplett. - if component.data.config.is_home_consumption_counter: - home_consumption += float(component.data.get.power) else: - not_home_consumption += component.data.get.power + home_consumption += 0.0 + not_home_consumption += home+not_home + + # Der aktuelle Counter selber + if child_is_home: + home_consumption += float(component.data.get.power) - home - not_home + else: + not_home_consumption += float(component.data.get.power) - home - not_home else: if child["type"] == ComponentType.CHARGEPOINT.value: @@ -230,7 +234,7 @@ def _calc_home_consumption_child(self, element): f"Komponente {child['type']}{component.num} ist im Fehlerzustand und " "wird nicht berücksichtigt bei der Berechnung des Hausverbrauchs.") - return home_consumption, not_home_consumption + return home_consumption, not_home_consumption, is_home_new def _add_hybrid_bat(self, id: int) -> List: elements = [] diff --git a/packages/control/counter_home_consumption_test.py b/packages/control/counter_home_consumption_test.py index 5ff2138139..7015500001 100644 --- a/packages/control/counter_home_consumption_test.py +++ b/packages/control/counter_home_consumption_test.py @@ -47,7 +47,7 @@ def test_calc_home_consumption(counter_all: Callable[[], CounterAll], data_): [ pytest.param("hierarchy_hybrid_with_home_consumption", 500, id="hierarchy_hybrid_with_home_consumption"), pytest.param("hierarchy_nested_with_home_consumption", 500, id="hierarchy_nested_with_home_consumption"), - pytest.param("hierarchy_standard_with_home_consumption", 500, id="hierarchy_standard_with_home_consumption"), + pytest.param("hierarchy_standard_with_home_consumption", 0, id="hierarchy_standard_with_home_consumption"), pytest.param("hierarchy_nested_two_level_with_home_consumption", 500, id="hierarchy_nested_two_level_with_home_consumption"), ], @@ -62,13 +62,6 @@ def test_calc_home_consumption_with_configured_home_consumption_counter( assert home_consumption == expected_home_consumption -def test_calc_home_consumption_hc_counter(data_hc_counter_): - c = hierarchy_hc_counter() - c.data.config.home_consumption_source_id = 6 - home_consumption = c._calc_home_consumption()[0] - assert home_consumption == 1100 - - @pytest.mark.parametrize(["home_consumption", "invalid_home_consumption", "expected_home_consumption", @@ -98,17 +91,6 @@ def test_set_home_consumption(home_consumption: int, assert c.data.set.home_consumption == expected_home_consumption -def test_validate_home_consumption_counter(monkeypatch): - c = CounterAll() - c.data.config.home_consumption_source_id = 0 - monkeypatch.setattr(c, "get_id_evu_counter", lambda: 0) - monkeypatch.setattr(c, "get_evu_counter_str", lambda: "counter0") - - with pytest.raises(Exception) as e: - c._validate_home_consumption_counter() - assert str(e.value) == CounterAll.EVU_IS_HC_COUNTER_ERROR - - def hierarchy_standard_with_home_consumption() -> CounterAll: # counter0 # | @@ -297,6 +279,12 @@ def data_home_consumption() -> None: config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=False), set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), + "counter11": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( + spec=CounterGet, currents=[25, 10, 25], power=6200, daily_imported=20000, daily_exported=0, + imported=14000, exported=18000, fault_state=0), + config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=True), + set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), + "counter8": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( spec=CounterGet, currents=[25, 10, 25], power=7400, daily_imported=20000, daily_exported=0, imported=14000, exported=18000, fault_state=0), diff --git a/packages/helpermodules/create_debug.py b/packages/helpermodules/create_debug.py index cb032927a5..e2aa81f002 100644 --- a/packages/helpermodules/create_debug.py +++ b/packages/helpermodules/create_debug.py @@ -35,8 +35,8 @@ def get_common_data(): ip_address = None try: updateAvailable = subdata.SubData.system_data["system"].data["current_branch_commit"] and \ - subdata.SubData.system_data["system"].data["current_branch_commit"] != \ - subdata.SubData.system_data["system"].data["current_commit"] + subdata.SubData.system_data["system"].data["current_branch_commit"] != \ + subdata.SubData.system_data["system"].data["current_commit"] except Exception: updateAvailable = False @@ -177,7 +177,7 @@ def config_and_state(): f"{component_data.data.config.max_currents} A\n" "--| Counter_Max_Power_Errorcase: " f"{component_data.data.config.max_power_errorcase} W\n") - elif counter_all_data.data.config.home_consumption_source_id == component_data.num: + elif component_data.data.config.is_home_consumption_counter: parsed_data += ("--| Counter_Type: Hausverbrauchszähler\n" "--| Counter_Max_Power: " f"{component_data.data.config.max_total_power} W\n" @@ -251,8 +251,7 @@ def get_hierarchy(hierarchy, level=0): counter_all_data = data.data.counter_all_data if counter_all_data.get_evu_counter_str() == f"counter{component_data.num}": counter_type = ("EVU-Zähler") - elif (counter_all_data.data.config.home_consumption_source_id == - component_data.num): + elif (component_data.data.config.is_home_consumption_counter): counter_type = ("Hausverbrauchszähler") else: counter_type = "Sonstiger Zähler" @@ -461,9 +460,9 @@ def write_to_file(file_handler, func, default: Optional[Any] = None): json_rsp = req.get_http_session().put("https://debughandler.wb-solution.de", data=data, params={ - 'debugemail': debug_email, - 'ticketnumber': ticketnumber, - 'subject': subject + 'debugemail': debug_email, + 'ticketnumber': ticketnumber, + 'subject': subject }, timeout=10).json() diff --git a/packages/helpermodules/measurement_logging/write_log.py b/packages/helpermodules/measurement_logging/write_log.py index fd8d51aa4b..8b1db45e88 100644 --- a/packages/helpermodules/measurement_logging/write_log.py +++ b/packages/helpermodules/measurement_logging/write_log.py @@ -248,8 +248,7 @@ def create_entry(log_type: LogType, sh_log_data: LegacySmartHomeLogData, previou counter_dict = {} for counter in data.data.counter_data.values(): try: - home_consumption_source_id = data.data.counter_all_data.data.config.home_consumption_source_id - if (home_consumption_source_id is None or counter.num != home_consumption_source_id): + if not counter.data.config.is_home_consumption_counter: counter_dict.update( {f"counter{counter.num}": { "imported": counter.data.get.imported, diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index d0400aa576..e02e36b151 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -933,8 +933,6 @@ def process_counter_topic(self, msg: mqtt.MQTTMessage): self._validate_value(msg, float, [(0, float("inf"))]) elif "openWB/set/counter/get/hierarchy" in msg.topic: self._validate_value(msg, None) - elif "openWB/set/counter/config/home_consumption_source_id" in msg.topic: - self._validate_value(msg, int) elif "openWB/set/counter/set/simulation" in msg.topic: self._validate_value(msg, "json") elif "/set/consumption_left" in msg.topic: diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 0a40a77b71..db9764795d 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -58,7 +58,7 @@ class UpdateConfig: - DATASTORE_VERSION = 138 + DATASTORE_VERSION = 139 valid_topic = [ "^openWB/bat/config/bat_control_activated$", @@ -186,7 +186,6 @@ class UpdateConfig: "^openWB/command/todo$", "^openWB/counter/config/consider_less_charging$", - "^openWB/counter/config/home_consumption_source_id$", "^openWB/counter/get/hierarchy$", "^openWB/counter/set/disengageable_smarthome_power$", "^openWB/counter/set/imported_home_consumption$", @@ -589,7 +588,6 @@ class UpdateConfig: ("openWB/chargepoint/template/0", get_chargepoint_template_default()), ("openWB/counter/get/hierarchy", []), ("openWB/counter/config/consider_less_charging", counter_all.Config().consider_less_charging), - ("openWB/counter/config/home_consumption_source_id", counter_all.Config().home_consumption_source_id), ("openWB/vehicle/0/name", "Standard-Fahrzeug"), ("openWB/vehicle/0/color", DEFAULT_COLORS.VEHICLE.value), ("openWB/vehicle/0/info", {"manufacturer": None, "model": None}), @@ -3483,10 +3481,8 @@ def upgrade(topic: str, payload) -> Optional[dict]: self._loop_all_received_topics(upgrade) self._append_datastore_version(137) - def upgrade_datastore_138(self) -> None: def upgrade(topic: str, payload) -> Optional[dict]: - if re.search("openWB/counter/[0-9]+/config", topic) is not None: index = get_index(topic) if f"openWB/counter/{index}/config/is_home_consumption_counter" not in self.all_received_topics: @@ -3494,3 +3490,12 @@ def upgrade(topic: str, payload) -> Optional[dict]: return {f"openWB/counter/{index}/config/is_home_consumption_counter": is_home_consumption_counter} self._loop_all_received_topics(upgrade) self._append_datastore_version(138) + + def upgrade_datastore_139(self) -> None: + old_topic = "openWB/counter/config/home_consumption_source_id" + if old_topic in self.all_received_topics: + source_id = decode_payload(self.all_received_topics[old_topic]) + if source_id is not None: + source_id = int(source_id) + self.__update_topic(f"openWB/counter/{source_id}/config/is_home_consumption_counter", True) + self._append_datastore_version(139) From 65504889444c52d1123e62e263e6810d0dd36fd1 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Fri, 31 Jul 2026 09:08:37 +0200 Subject: [PATCH 03/11] Add more test cases --- packages/control/counter_all.py | 6 +- .../control/counter_home_consumption_test.py | 189 +++++++++++++++--- 2 files changed, 166 insertions(+), 29 deletions(-) diff --git a/packages/control/counter_all.py b/packages/control/counter_all.py index 36ee85e5ec..57f0c634f5 100644 --- a/packages/control/counter_all.py +++ b/packages/control/counter_all.py @@ -185,7 +185,7 @@ def _calc_home_consumption(self) -> Tuple[float, List]: return evu - not_home_consumption - not_home_consumption_evu - home_consumption_evu, elements_to_sum_up def _calc_home_consumption_child(self, element, is_home) -> Tuple[float, float, bool]: - is_home_new = is_home + is_home_local = is_home home_consumption = 0.0 not_home_consumption = 0.0 for child in element["children"]: @@ -203,7 +203,7 @@ def _calc_home_consumption_child(self, element, is_home) -> Tuple[float, float, # Alles was unter dem Counter hängt home, not_home, child_branch_is_home = self._calc_home_consumption_child(child, child_is_home) - is_home_new = is_home_new or child_branch_is_home + is_home_local = is_home_local or child_branch_is_home # Wurde in den Kindern ein Hausverbrauchszähler gefunden, dann wird der Hausverbrauch aus den Kindern übernommen. if child_branch_is_home: @@ -234,7 +234,7 @@ def _calc_home_consumption_child(self, element, is_home) -> Tuple[float, float, f"Komponente {child['type']}{component.num} ist im Fehlerzustand und " "wird nicht berücksichtigt bei der Berechnung des Hausverbrauchs.") - return home_consumption, not_home_consumption, is_home_new + return home_consumption, not_home_consumption, is_home_local def _add_hybrid_bat(self, id: int) -> List: elements = [] diff --git a/packages/control/counter_home_consumption_test.py b/packages/control/counter_home_consumption_test.py index 7015500001..5ed91e7e78 100644 --- a/packages/control/counter_home_consumption_test.py +++ b/packages/control/counter_home_consumption_test.py @@ -45,11 +45,17 @@ def test_calc_home_consumption(counter_all: Callable[[], CounterAll], data_): @pytest.mark.parametrize( ["counter_all", "expected_home_consumption"], [ - pytest.param("hierarchy_hybrid_with_home_consumption", 500, id="hierarchy_hybrid_with_home_consumption"), - pytest.param("hierarchy_nested_with_home_consumption", 500, id="hierarchy_nested_with_home_consumption"), - pytest.param("hierarchy_standard_with_home_consumption", 0, id="hierarchy_standard_with_home_consumption"), - pytest.param("hierarchy_nested_two_level_with_home_consumption", - 500, id="hierarchy_nested_two_level_with_home_consumption"), + pytest.param("hierarchy_home_consumption_standard", 0, id="hierarchy_home_consumption_standard"), + pytest.param("hierarchy_home_consumption_hybrid", 500, id="hierarchy_home_consumption_hybrid"), + pytest.param("hierarchy_nested_home_consumption_level_3", 500, id="hierarchy_nested_home_consumption_level_3"), + pytest.param("hierarchy_nested_home_consumption_level_2", + 500, id="hierarchy_nested_home_consumption_level_2"), + pytest.param("hierarchy_home_consumption_only_root", + 1000, id="hierarchy_home_consumption_only_root"), + pytest.param("hierarchy_home_consumption_all", + 1000, id="hierarchy_home_consumption_all"), + pytest.param("hierarchy_nested_home_consumption_multi_level_2", 250, + id="hierarchy_nested_home_consumption_multi_level_2"), ], ) def test_calc_home_consumption_with_configured_home_consumption_counter( @@ -91,7 +97,7 @@ def test_set_home_consumption(home_consumption: int, assert c.data.set.home_consumption == expected_home_consumption -def hierarchy_standard_with_home_consumption() -> CounterAll: +def hierarchy_home_consumption_standard() -> CounterAll: # counter0 # | # - cp4 @@ -102,7 +108,9 @@ def hierarchy_standard_with_home_consumption() -> CounterAll: # counter_8 <-- home consumption counter # - # counter8 = 500 <- home consumption + # UnbekannterVerbraucher/Hausverbrauch am Countern + # counter0 = 750 + # counter8 = 500 # Final Home Consumption = 0 c = CounterAll() c.data.get.hierarchy = [{"id": 0, "type": "counter", @@ -117,7 +125,7 @@ def hierarchy_standard_with_home_consumption() -> CounterAll: return c -def hierarchy_hybrid_with_home_consumption() -> CounterAll: +def hierarchy_home_consumption_hybrid() -> CounterAll: # counter0 # | # - cp3 @@ -127,7 +135,10 @@ def hierarchy_hybrid_with_home_consumption() -> CounterAll: # - cp5 # - inverter1 # - bat2 - # counter8 = 500 <- home consumption + + # UnbekannterVerbraucher/Hausverbrauch am Countern + # counter0 = 250 + # counter8 = 500 # Final Home Consumption = 500 c = CounterAll() c.data.get.hierarchy = [{"id": 0, "type": "counter", @@ -142,20 +153,23 @@ def hierarchy_hybrid_with_home_consumption() -> CounterAll: return c -def hierarchy_nested_with_home_consumption() -> CounterAll: +def hierarchy_nested_home_consumption_level_3() -> CounterAll: # counter0 # | # - cp3 # - counter6 # | # - cp4 - # - counter_8 <-- home consumption counter + # - counter8 <-- home consumption counter # | # - cp5 - # - inverter1 + # - inverter1 # - bat2 - # counter8 = 500 <- home consumption + # UnbekannterVerbraucher/Hausverbrauch am Countern + # counter0 = 250 + # counter6 = 0 + # counter8 = 500 # Final Home Consumption = 500 c = CounterAll() c.data.get.hierarchy = [{"id": 0, "type": "counter", @@ -166,28 +180,31 @@ def hierarchy_nested_with_home_consumption() -> CounterAll: {"id": 4, "type": "cp", "children": []}, {"id": 8, "type": "counter", "children": [ - {"id": 5, "type": "cp", "children": []}]}]}, - {"id": 1, "type": "inverter", "children": []}, + {"id": 5, "type": "cp", "children": []}]}, + {"id": 1, "type": "inverter", "children": []} + ]}, {"id": 2, "type": "bat", "children": []}]}] return c -def hierarchy_nested_two_level_with_home_consumption() -> CounterAll: +def hierarchy_nested_home_consumption_level_2() -> CounterAll: # counter0 # | # - cp3 # - counter9 <-- home consumption counter # | # - cp4 - # - counter_10 <-- home consumption counter + # - counter10 <-- home consumption counter # | # - cp5 # - # - inverter1 + # - inverter1 # - bat2 - # coutner9 = 250 <- home consumption - # counter10 = 250 <- home consumption + # UnbekannterVerbraucher/Hausverbrauch am Countern + # counter0 = 250 + # coutner9 = 250 + # counter10 = 250 # Final Home Consumption = 500 c = CounterAll() c.data.get.hierarchy = [{"id": 0, "type": "counter", @@ -199,8 +216,117 @@ def hierarchy_nested_two_level_with_home_consumption() -> CounterAll: {"id": 10, "type": "counter", "children": [ {"id": 5, "type": "cp", "children": []}]}, + {"id": 1, "type": "inverter", "children": []}, + ]}, + {"id": 2, "type": "bat", "children": []}]}] + return c + + +def hierarchy_home_consumption_only_root() -> CounterAll: + # counter11 <-- home consumption counter + # | + # - cp3 + # - counter6 + # | + # - cp4 + # - counter13 + # | + # - cp5 + # + # - inverter1 + # - bat2 + + # UnbekannterVerbraucher/Hausverbrauch am Countern + # counter11 = 500 + # coutner6 = 250 + # counter13 = 250 + # Final Home Consumption = 1000 + c = CounterAll() + c.data.get.hierarchy = [{"id": 11, "type": "counter", + "children": [ + {"id": 3, "type": "cp", "children": []}, + {"id": 6, "type": "counter", + "children": [ + {"id": 4, "type": "cp", "children": []}, + {"id": 13, "type": "counter", + "children": [ + {"id": 5, "type": "cp", "children": []}]}, + {"id": 1, "type": "inverter", "children": []}, + ]}, + {"id": 2, "type": "bat", "children": []}]}] + return c + + +def hierarchy_home_consumption_all() -> CounterAll: + # counter11 <-- home consumption counter + # | + # - cp3 + # - counter9 <-- home consumption counter + # | + # - cp4 + # - counter10 <-- home consumption counter + # | + # - cp5 + # + # - inverter1 + # - bat2 + + # UnbekannterVerbraucher/Hausverbrauch am Countern + # counter11 = 500 + # coutner9 = 250 + # counter10 = 250 + # Final Home Consumption = 1000 + c = CounterAll() + c.data.get.hierarchy = [{"id": 11, "type": "counter", + "children": [ + {"id": 3, "type": "cp", "children": []}, + {"id": 9, "type": "counter", + "children": [ + {"id": 4, "type": "cp", "children": []}, + {"id": 10, "type": "counter", + "children": [ + {"id": 5, "type": "cp", "children": []}]}, + {"id": 1, "type": "inverter", "children": []}, + ]}, + {"id": 2, "type": "bat", "children": []}]}] + return c + + +def hierarchy_nested_home_consumption_multi_level_2() -> CounterAll: + # counter0 + # | + # - cp3 + # - counter6 + # | + # - cp4 + # - counter10 <-- home consumption counter + # | + # - cp5 + # - counter14 + # | + # - inverter1 + # - bat2 + + # UnbekannterVerbraucher/Hausverbrauch am Countern + # counter0 = 250 + # coutner6 = 0 + # counter10 = 250 + # counter14 = 250 + # Final Home Consumption = 250 + c = CounterAll() + c.data.get.hierarchy = [{"id": 0, "type": "counter", + "children": [ + {"id": 3, "type": "cp", "children": []}, + {"id": 6, "type": "counter", + "children": [ + {"id": 4, "type": "cp", "children": []}, + {"id": 10, "type": "counter", + "children": [ + {"id": 5, "type": "cp", "children": []}]}, + {"id": 14, "type": "counter", + "children": [ + {"id": 1, "type": "inverter", "children": []}]}, ]}, - {"id": 1, "type": "inverter", "children": []}, {"id": 2, "type": "bat", "children": []}]}] return c @@ -266,10 +392,10 @@ def data_home_consumption() -> None: fault_state=0), config=Mock(spec=PvConfig, max_ac_out=10000)))}) data.data.counter_data.update({ "counter0": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( - spec=CounterGet, currents=[40]*3, power=6200, daily_imported=45000, daily_exported=3000, fault_state=0), + spec=CounterGet, currents=[40]*3, power=6450, daily_imported=45000, daily_exported=3000, fault_state=0), config=Mock(spec=CounterConfig, is_home_consumption_counter=False))), "counter6": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( - spec=CounterGet, currents=[25, 10, 25], power=14300, daily_imported=20000, daily_exported=0, + spec=CounterGet, currents=[25, 10, 25], power=4300, daily_imported=20000, daily_exported=0, imported=14000, exported=18000, fault_state=0), config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=False), set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), @@ -278,9 +404,19 @@ def data_home_consumption() -> None: imported=14000, exported=18000, fault_state=0), config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=False), set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), + "counter13": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( + spec=CounterGet, currents=[25, 10, 25], power=7150, daily_imported=20000, daily_exported=0, + imported=14000, exported=18000, fault_state=0), + config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=False), + set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), + "counter14": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( + spec=CounterGet, currents=[25, 10, 25], power=-9750, daily_imported=20000, daily_exported=0, + imported=14000, exported=18000, fault_state=0), + config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=False), + set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), "counter11": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( - spec=CounterGet, currents=[25, 10, 25], power=6200, daily_imported=20000, daily_exported=0, + spec=CounterGet, currents=[25, 10, 25], power=6700, daily_imported=20000, daily_exported=0, imported=14000, exported=18000, fault_state=0), config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=True), set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), @@ -291,7 +427,7 @@ def data_home_consumption() -> None: config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=True), set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), "counter9": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( - spec=CounterGet, currents=[25, 10, 25], power=14300, daily_imported=20000, daily_exported=0, + spec=CounterGet, currents=[25, 10, 25], power=4300, daily_imported=20000, daily_exported=0, imported=14000, exported=18000, fault_state=0), config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=True), set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), @@ -299,4 +435,5 @@ def data_home_consumption() -> None: spec=CounterGet, currents=[25, 10, 25], power=7150, daily_imported=20000, daily_exported=0, imported=14000, exported=18000, fault_state=0), config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=True), - set=Mock(spec=CounterSet, raw_currents_left=[31]*3)))}) + set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), + }) From 302ea6a4096b0bcad552cb02945dc6d7392af6c7 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Fri, 31 Jul 2026 09:33:11 +0200 Subject: [PATCH 04/11] Fix Flake8 error --- packages/control/counter_all.py | 9 +++++---- packages/control/counter_home_consumption_test.py | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/control/counter_all.py b/packages/control/counter_all.py index 57f0c634f5..d2e4f5c370 100644 --- a/packages/control/counter_all.py +++ b/packages/control/counter_all.py @@ -4,13 +4,12 @@ from dataclasses import dataclass, field import logging import re -from typing import Callable, Dict, List, Optional, Tuple, Union +from typing import Callable, Dict, List, Tuple, Union from control import data from control.counter import Counter from dataclass_utils.factories import empty_list_factory from helpermodules.messaging import MessageType, pub_system_message -from helpermodules.pub import Pub from modules.common.component_type import ComponentType, component_type_to_readable_text from modules.common.fault_state import FaultStateLevel from modules.common.simcount import SimCounter @@ -151,7 +150,8 @@ def _calc_home_consumption(self) -> Tuple[float, List]: # Alles was unter dem Counter hängt home, not_home, home_child = self._calc_home_consumption_child(element, is_home_branch) - # Wurde in den Kindern ein Hausverbrauchszähler gefunden, dann wird der Hausverbrauch aus den Kindern übernommen. + # Wurde in den Kindern ein Hausverbrauchszähler gefunden, + # dann wird der Hausverbrauch aus den Kindern übernommen. if home_child: home_consumption += home not_home_consumption += not_home @@ -205,7 +205,8 @@ def _calc_home_consumption_child(self, element, is_home) -> Tuple[float, float, is_home_local = is_home_local or child_branch_is_home - # Wurde in den Kindern ein Hausverbrauchszähler gefunden, dann wird der Hausverbrauch aus den Kindern übernommen. + # Wurde in den Kindern ein Hausverbrauchszähler gefunden, + # dann wird der Hausverbrauch aus den Kindern übernommen. if child_branch_is_home: home_consumption += home not_home_consumption += not_home diff --git a/packages/control/counter_home_consumption_test.py b/packages/control/counter_home_consumption_test.py index 5ed91e7e78..28c9598c40 100644 --- a/packages/control/counter_home_consumption_test.py +++ b/packages/control/counter_home_consumption_test.py @@ -24,7 +24,6 @@ from modules.common.store._api import LoggingValueStore from packages.conftest import ( - hierarchy_hc_counter, hierarchy_standard, hierarchy_hybrid, hierarchy_nested) From 1ee4542fbb9a5a7538e3c3b256f33d66ccfb577d Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Fri, 31 Jul 2026 09:59:39 +0200 Subject: [PATCH 05/11] Add Copilot suggestions --- packages/control/counter.py | 2 +- packages/control/counter_all.py | 13 +++++++++++-- packages/helpermodules/update_config.py | 8 ++++++-- 3 files changed, 18 insertions(+), 5 deletions(-) diff --git a/packages/control/counter.py b/packages/control/counter.py index 5d067036cd..2edb7b50af 100644 --- a/packages/control/counter.py +++ b/packages/control/counter.py @@ -43,7 +43,7 @@ class Config: max_currents: List[float] = field(default_factory=currents_list_factory, metadata={ "topic": "get/max_currents"}) max_total_power: float = field(default=0, metadata={"topic": "get/max_total_power"}) - is_home_consumption_counter: bool = field(default=0, metadata={"topic": "get/is_home_consumption_counter"}) + is_home_consumption_counter: bool = field(default=False, metadata={"topic": "get/is_home_consumption_counter"}) def config_factory() -> Config: diff --git a/packages/control/counter_all.py b/packages/control/counter_all.py index d2e4f5c370..6a226a6268 100644 --- a/packages/control/counter_all.py +++ b/packages/control/counter_all.py @@ -180,9 +180,11 @@ def _calc_home_consumption(self) -> Tuple[float, List]: home_consumption_evu = evu - power if evu_is_home: - return evu - not_home_consumption - not_home_consumption_evu, elements_to_sum_up + return (evu - not_home_consumption - not_home_consumption_evu - + self.data.set.smarthome_power_excluded_from_home_consumption), elements_to_sum_up else: - return evu - not_home_consumption - not_home_consumption_evu - home_consumption_evu, elements_to_sum_up + return (evu - not_home_consumption - not_home_consumption_evu - home_consumption_evu - + self.data.set.smarthome_power_excluded_from_home_consumption), elements_to_sum_up def _calc_home_consumption_child(self, element, is_home) -> Tuple[float, float, bool]: is_home_local = is_home @@ -219,6 +221,13 @@ def _calc_home_consumption_child(self, element, is_home) -> Tuple[float, float, home_consumption += float(component.data.get.power) - home - not_home else: not_home_consumption += float(component.data.get.power) - home - not_home + else: + # Leaf counter: account its own power and propagate the home-branch flag. + is_home_local = is_home_local or child_is_home + if child_is_home: + home_consumption += float(component.data.get.power) + else: + not_home_consumption += float(component.data.get.power) else: if child["type"] == ComponentType.CHARGEPOINT.value: diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index db9764795d..421935c05f 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -3496,6 +3496,10 @@ def upgrade_datastore_139(self) -> None: if old_topic in self.all_received_topics: source_id = decode_payload(self.all_received_topics[old_topic]) if source_id is not None: - source_id = int(source_id) - self.__update_topic(f"openWB/counter/{source_id}/config/is_home_consumption_counter", True) + try: + source_id = int(source_id) + except (TypeError, ValueError): + log.warning(f"Invalid '{old_topic}' value: {source_id!r}; skipping migration") + else: + self.__update_topic(f"openWB/counter/{source_id}/config/is_home_consumption_counter", True) self._append_datastore_version(139) From 1e4608105bc00cece78e987248fc87f22609b442 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Mon, 3 Aug 2026 15:02:42 +0200 Subject: [PATCH 06/11] Refactor HC-Berechnung --- packages/control/counter.py | 8 +- packages/control/counter_all.py | 173 ++++++++---------- .../control/counter_home_consumption_test.py | 101 ++++++---- packages/helpermodules/update_config.py | 9 +- 4 files changed, 143 insertions(+), 148 deletions(-) diff --git a/packages/control/counter.py b/packages/control/counter.py index 2edb7b50af..d10d72cbd3 100644 --- a/packages/control/counter.py +++ b/packages/control/counter.py @@ -39,11 +39,11 @@ class ControlRangeState(Enum): @dataclass class Config: - max_power_errorcase: float = field(default=7000, metadata={"topic": "get/max_power_errorcase"}) + max_power_errorcase: float = field(default=7000, metadata={"topic": "config/max_power_errorcase"}) max_currents: List[float] = field(default_factory=currents_list_factory, metadata={ - "topic": "get/max_currents"}) - max_total_power: float = field(default=0, metadata={"topic": "get/max_total_power"}) - is_home_consumption_counter: bool = field(default=False, metadata={"topic": "get/is_home_consumption_counter"}) + "topic": "config/max_currents"}) + max_total_power: float = field(default=0, metadata={"topic": "config/max_total_power"}) + is_home_consumption_counter: bool = field(default=False, metadata={"topic": "config/is_home_consumption_counter"}) def config_factory() -> Config: diff --git a/packages/control/counter_all.py b/packages/control/counter_all.py index 6a226a6268..80efd51164 100644 --- a/packages/control/counter_all.py +++ b/packages/control/counter_all.py @@ -98,7 +98,6 @@ def get_id_evu_counter(self) -> int: def set_home_consumption(self) -> None: try: - # self._validate_home_consumption_counter() home_consumption, elements = self._calc_home_consumption() if home_consumption < 0: log.error( @@ -125,126 +124,98 @@ def set_home_consumption(self) -> None: log.exception("Fehler in der allgemeinen Zähler-Klasse") def _calc_home_consumption(self) -> Tuple[float, List]: - power = 0 - home_consumption = 0 - not_home_consumption = 0 - not_home_consumption_evu = 0 - id_source = self.get_id_evu_counter() - evu_is_home = data.data.counter_data[f"counter{id_source}"].data.config.is_home_consumption_counter - evu = data.data.counter_data[f"counter{id_source}"].data.get.power - home_child = False + hc_all_power = 0 + no_hc_all_power = 0 + no_hc_evu = 0 + evu_id = self.get_id_evu_counter() + elements_to_sum_up = self.get_elements_for_downstream_calculation(evu_id) + + evu_is_HC = data.data.counter_data[f"counter{evu_id}"].data.config.is_home_consumption_counter - elements_to_sum_up = self.get_elements_for_downstream_calculation(id_source) for element in elements_to_sum_up: - if element["type"] == ComponentType.CHARGEPOINT.value: - component = data.data.cp_data[f"cp{element['id']}"] - elif element["type"] == ComponentType.BAT.value: - component = data.data.bat_data[f"bat{element['id']}"] - elif element["type"] == ComponentType.INVERTER.value: - component = data.data.pv_data[f"pv{element['id']}"] - elif element["type"] == ComponentType.COUNTER.value: + if element["type"] == ComponentType.COUNTER.value: component = data.data.counter_data[f"counter{element['id']}"] - is_home_branch = evu_is_home or component.data.config.is_home_consumption_counter + _hc_all_power, hc_counter = self._get_home_consumption_counter(element, evu_is_HC=evu_is_HC) + hc_all_power += _hc_all_power - # Alles was unter dem Counter hängt - home, not_home, home_child = self._calc_home_consumption_child(element, is_home_branch) + for counter in hc_counter: + no_hc_all_power += self._get_no_home_consumption(counter) + continue - # Wurde in den Kindern ein Hausverbrauchszähler gefunden, - # dann wird der Hausverbrauch aus den Kindern übernommen. - if home_child: - home_consumption += home - not_home_consumption += not_home - - else: - home_consumption += 0.0 - not_home_consumption += home + not_home - - # Der aktuelle Counter selber - if is_home_branch: - home_consumption += float(component.data.get.power) - home - not_home - else: - not_home_consumption += float(component.data.get.power) - home - not_home + elif element["type"] == ComponentType.BAT.value: + component = data.data.bat_data[f"bat{element['id']}"] + elif element["type"] == ComponentType.CHARGEPOINT.value: + component = data.data.cp_data[f"cp{element['id']}"] + elif element["type"] == ComponentType.INVERTER.value: + component = data.data.pv_data[f"pv{element['id']}"] if component.data.get.fault_state < 2: - # Power über alles - power += component.data.get.power - - # Power von allen Komponenten aus der ersten ebene des EVU - if element["type"] != ComponentType.COUNTER.value: - not_home_consumption_evu += component.data.get.power + no_hc_evu += component.data.get.power else: log.warning( f"Komponente {element['type']}{component.num} ist im Fehlerzustand und wird nicht berücksichtigt.") - home_consumption_evu = evu - power + evu_power = data.data.counter_data[f"counter{evu_id}"].data.get.power - if evu_is_home: - return (evu - not_home_consumption - not_home_consumption_evu - + if data.data.counter_data[f"counter{evu_id}"].data.config.is_home_consumption_counter: + return (evu_power - no_hc_all_power - no_hc_evu - self.data.set.smarthome_power_excluded_from_home_consumption), elements_to_sum_up else: - return (evu - not_home_consumption - not_home_consumption_evu - home_consumption_evu - + return (hc_all_power - no_hc_all_power - self.data.set.smarthome_power_excluded_from_home_consumption), elements_to_sum_up - def _calc_home_consumption_child(self, element, is_home) -> Tuple[float, float, bool]: - is_home_local = is_home - home_consumption = 0.0 - not_home_consumption = 0.0 - for child in element["children"]: - if child["type"] == ComponentType.COUNTER.value: - component = data.data.counter_data[f"counter{child['id']}"] - # Wenn der unter Counter im Fehlerzustand ist, wird er nicht berücksichtigt. - if component.data.get.fault_state >= 2: - log.warning( - f"Komponente {child['type']}{component.num} ist im Fehlerzustand und " - "wird nicht berücksichtigt bei der Berechnung des Hausverbrauchs.") - continue - child_is_home = is_home or component.data.config.is_home_consumption_counter - - if child["children"]: - # Alles was unter dem Counter hängt - home, not_home, child_branch_is_home = self._calc_home_consumption_child(child, child_is_home) - - is_home_local = is_home_local or child_branch_is_home - - # Wurde in den Kindern ein Hausverbrauchszähler gefunden, - # dann wird der Hausverbrauch aus den Kindern übernommen. - if child_branch_is_home: - home_consumption += home - not_home_consumption += not_home - else: - home_consumption += 0.0 - not_home_consumption += home+not_home + def _get_no_home_consumption(self, element) -> float: + # Summiert die Leistung aller Komponenten, die nicht als Hausverbrauch gezählt werden, + # unterhalb des angegebenen Elements. + not_home_consumption = 0 + if element["type"] != ComponentType.COUNTER.value: + # Wenn kein Counter, dann get power davon -> not_home_consumption + if element["type"] == ComponentType.CHARGEPOINT.value: + component = data.data.cp_data[f"cp{element['id']}"] + elif element["type"] == ComponentType.BAT.value: + component = data.data.bat_data[f"bat{element['id']}"] + elif element["type"] == ComponentType.INVERTER.value: + component = data.data.pv_data[f"pv{element['id']}"] - # Der aktuelle Counter selber - if child_is_home: - home_consumption += float(component.data.get.power) - home - not_home - else: - not_home_consumption += float(component.data.get.power) - home - not_home - else: - # Leaf counter: account its own power and propagate the home-branch flag. - is_home_local = is_home_local or child_is_home - if child_is_home: - home_consumption += float(component.data.get.power) - else: - not_home_consumption += float(component.data.get.power) + if component.data.get.fault_state < 2: + not_home_consumption += component.data.get.power - else: - if child["type"] == ComponentType.CHARGEPOINT.value: - component = data.data.cp_data[f"cp{child['id']}"] - elif child["type"] == ComponentType.BAT.value: - component = data.data.bat_data[f"bat{child['id']}"] - elif child["type"] == ComponentType.INVERTER.value: - component = data.data.pv_data[f"pv{child['id']}"] - - if component.data.get.fault_state < 2: - not_home_consumption += component.data.get.power - else: - log.warning( - f"Komponente {child['type']}{component.num} ist im Fehlerzustand und " - "wird nicht berücksichtigt bei der Berechnung des Hausverbrauchs.") + return not_home_consumption - return home_consumption, not_home_consumption, is_home_local + # Wenn Counter, dann get not_home_consumption von allen Children + for child in element["children"]: + not_home_consumption += self._get_no_home_consumption(child) + + return not_home_consumption + + def _get_home_consumption_counter(self, elements, evu_is_HC=False) -> Tuple[float, List]: + # Sucht rekursiv HC-Zähler im Teilbaum, summiert deren Leistung + # und gibt eine Liste der gefundenen HC-Zähler zurück. + # -> wenn mehrer HC-Zähler auf der selben Ebene sind + + total_power = 0 + hc_counters = [] + + if elements["type"] == ComponentType.COUNTER.value: + component = data.data.counter_data[f"counter{elements['id']}"] + if component.data.config.is_home_consumption_counter or evu_is_HC: + hc_counters.append(elements) + total_power += component.data.get.power + # Kinder nicht weiter durchsuchen, + # da deren Leistung bereits enthalten ist. + return total_power, hc_counters + else: + # Zähler kein HC -> check Kinder + # Alle durchgehen, falls ein Zähler mehrer Zähler als Kinder hat + for element in elements["children"]: + if element["type"] == ComponentType.COUNTER.value: + power, counters = self._get_home_consumption_counter(element, evu_is_HC=evu_is_HC) + total_power += power + hc_counters.extend(counters) + else: + continue + return total_power, hc_counters def _add_hybrid_bat(self, id: int) -> List: elements = [] diff --git a/packages/control/counter_home_consumption_test.py b/packages/control/counter_home_consumption_test.py index 28c9598c40..793455715c 100644 --- a/packages/control/counter_home_consumption_test.py +++ b/packages/control/counter_home_consumption_test.py @@ -13,7 +13,6 @@ from control.counter import Counter, CounterData from control.counter import Config as CounterConfig from control.counter import Get as CounterGet -from control.counter import Set as CounterSet from control.counter_all import CounterAll from control.pv import Pv, PvData from control.pv import Config as PvConfig @@ -55,6 +54,8 @@ def test_calc_home_consumption(counter_all: Callable[[], CounterAll], data_): 1000, id="hierarchy_home_consumption_all"), pytest.param("hierarchy_nested_home_consumption_multi_level_2", 250, id="hierarchy_nested_home_consumption_multi_level_2"), + pytest.param("hierarchy_nested_home_consumption_2_hc_childs", 500, + id="hierarchy_nested_home_consumption_2_hc_childs") ], ) def test_calc_home_consumption_with_configured_home_consumption_counter( @@ -330,6 +331,45 @@ def hierarchy_nested_home_consumption_multi_level_2() -> CounterAll: return c +def hierarchy_nested_home_consumption_2_hc_childs() -> CounterAll: + # counter0 + # | + # - cp3 + # - counter6 + # | + # - cp4 + # - counter10 <-- home consumption counter + # | + # - cp5 + # - counter15 <-- home consumption counter + # | + # - inverter1 + # - bat2 + + # UnbekannterVerbraucher/Hausverbrauch am Countern + # counter0 = 250 + # coutner6 = 0 + # counter10 = 250 + # counter15 = 250 + # Final Home Consumption = 500 + c = CounterAll() + c.data.get.hierarchy = [{"id": 0, "type": "counter", + "children": [ + {"id": 3, "type": "cp", "children": []}, + {"id": 6, "type": "counter", + "children": [ + {"id": 4, "type": "cp", "children": []}, + {"id": 10, "type": "counter", + "children": [ + {"id": 5, "type": "cp", "children": []}]}, + {"id": 15, "type": "counter", + "children": [ + {"id": 1, "type": "inverter", "children": []}]}, + ]}, + {"id": 2, "type": "bat", "children": []}]}] + return c + + @pytest.fixture() def data_home_consumption() -> None: data.data_init(Mock()) @@ -383,56 +423,41 @@ def data_home_consumption() -> None: exported=None, phases_in_use=0)))))} data.data.bat_data.update({"bat2": Mock(spec=Bat, num=2, data=Mock(spec=BatData, get=Mock( - spec=BatGet, power=-5000, daily_imported=6200, daily_exported=3000, imported=12000, exported=10000, - currents=None, fault_state=0), + spec=BatGet, power=-5000, fault_state=0), set=Mock(spec=BatSet, power_limit=None)))}) data.data.pv_data.update({"pv1": Mock(spec=Pv, data=Mock( - spec=PvData, get=Mock(spec=PvGet, power=-10000, daily_exported=6000, exported=27000, currents=None, - fault_state=0), config=Mock(spec=PvConfig, max_ac_out=10000)))}) + spec=PvData, get=Mock(spec=PvGet, power=-10000, fault_state=0), config=Mock(spec=PvConfig, max_ac_out=10000)))}) data.data.counter_data.update({ "counter0": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( - spec=CounterGet, currents=[40]*3, power=6450, daily_imported=45000, daily_exported=3000, fault_state=0), + spec=CounterGet, power=6450, fault_state=0), config=Mock(spec=CounterConfig, is_home_consumption_counter=False))), "counter6": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( - spec=CounterGet, currents=[25, 10, 25], power=4300, daily_imported=20000, daily_exported=0, - imported=14000, exported=18000, fault_state=0), - config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=False), - set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), + spec=CounterGet, power=4300, fault_state=0), + config=Mock(spec=CounterConfig, is_home_consumption_counter=False))), "counter7": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( - spec=CounterGet, currents=[25, 10, 25], power=20700, daily_imported=20000, daily_exported=0, - imported=14000, exported=18000, fault_state=0), - config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=False), - set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), + spec=CounterGet, power=20700, fault_state=0), + config=Mock(spec=CounterConfig, is_home_consumption_counter=False))), "counter13": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( - spec=CounterGet, currents=[25, 10, 25], power=7150, daily_imported=20000, daily_exported=0, - imported=14000, exported=18000, fault_state=0), - config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=False), - set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), + spec=CounterGet, power=7150, fault_state=0), + config=Mock(spec=CounterConfig, is_home_consumption_counter=False))), "counter14": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( - spec=CounterGet, currents=[25, 10, 25], power=-9750, daily_imported=20000, daily_exported=0, - imported=14000, exported=18000, fault_state=0), - config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=False), - set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), + spec=CounterGet, power=-9750, fault_state=0), + config=Mock(spec=CounterConfig, is_home_consumption_counter=False))), "counter11": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( - spec=CounterGet, currents=[25, 10, 25], power=6700, daily_imported=20000, daily_exported=0, - imported=14000, exported=18000, fault_state=0), - config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=True), - set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), + spec=CounterGet, power=6700, fault_state=0), + config=Mock(spec=CounterConfig, is_home_consumption_counter=True))), "counter8": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( - spec=CounterGet, currents=[25, 10, 25], power=7400, daily_imported=20000, daily_exported=0, - imported=14000, exported=18000, fault_state=0), - config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=True), - set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), + spec=CounterGet, power=7400, fault_state=0), + config=Mock(spec=CounterConfig, is_home_consumption_counter=True))), "counter9": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( - spec=CounterGet, currents=[25, 10, 25], power=4300, daily_imported=20000, daily_exported=0, - imported=14000, exported=18000, fault_state=0), - config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=True), - set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), + spec=CounterGet, power=4300, fault_state=0), + config=Mock(spec=CounterConfig, is_home_consumption_counter=True))), "counter10": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( - spec=CounterGet, currents=[25, 10, 25], power=7150, daily_imported=20000, daily_exported=0, - imported=14000, exported=18000, fault_state=0), - config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=True), - set=Mock(spec=CounterSet, raw_currents_left=[31]*3))), + spec=CounterGet, power=7150, fault_state=0), + config=Mock(spec=CounterConfig, is_home_consumption_counter=True))), + "counter15": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( + spec=CounterGet, power=-9750, fault_state=0), + config=Mock(spec=CounterConfig, is_home_consumption_counter=True))), }) diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 421935c05f..90d2f560f8 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -58,7 +58,7 @@ class UpdateConfig: - DATASTORE_VERSION = 139 + DATASTORE_VERSION = 138 valid_topic = [ "^openWB/bat/config/bat_control_activated$", @@ -3471,6 +3471,7 @@ def upgrade(topic: str, payload) -> Optional[dict]: self._append_datastore_version(136) def upgrade_datastore_137(self) -> None: + # Update all counters with new Parameter and default wert def upgrade(topic: str, payload) -> Optional[dict]: if re.search("openWB/vehicle/template/ev_template/[0-9]+$", topic) is not None: payload = decode_payload(payload) @@ -3489,9 +3490,7 @@ def upgrade(topic: str, payload) -> Optional[dict]: is_home_consumption_counter = get_counter_default_config()["is_home_consumption_counter"] return {f"openWB/counter/{index}/config/is_home_consumption_counter": is_home_consumption_counter} self._loop_all_received_topics(upgrade) - self._append_datastore_version(138) - - def upgrade_datastore_139(self) -> None: + # Remove old Topic old_topic = "openWB/counter/config/home_consumption_source_id" if old_topic in self.all_received_topics: source_id = decode_payload(self.all_received_topics[old_topic]) @@ -3502,4 +3501,4 @@ def upgrade_datastore_139(self) -> None: log.warning(f"Invalid '{old_topic}' value: {source_id!r}; skipping migration") else: self.__update_topic(f"openWB/counter/{source_id}/config/is_home_consumption_counter", True) - self._append_datastore_version(139) + self._append_datastore_version(138) From 3ab24fb986f47e68c6a8a6aab0d5c3ec7a9446e6 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Thu, 13 Aug 2026 14:17:31 +0200 Subject: [PATCH 07/11] Add auto-mode for HC-Counter --- .../public/default-dynamic-security.json | 12 ++ packages/conftest.py | 5 +- packages/control/counter.py | 5 +- packages/control/counter_all.py | 183 ++++++++++-------- .../control/counter_home_consumption_test.py | 51 +++-- packages/helpermodules/create_debug.py | 4 +- .../measurement_logging/write_log.py | 19 +- packages/helpermodules/setdata.py | 3 +- packages/helpermodules/update_config.py | 13 +- 9 files changed, 186 insertions(+), 109 deletions(-) diff --git a/data/config/mosquitto/public/default-dynamic-security.json b/data/config/mosquitto/public/default-dynamic-security.json index e9ace61b90..965b348b9e 100644 --- a/data/config/mosquitto/public/default-dynamic-security.json +++ b/data/config/mosquitto/public/default-dynamic-security.json @@ -1526,6 +1526,12 @@ "priority": 0, "allow": true }, + { + "acltype": "publishClientSend", + "topic": "openWB/set/counter/+/config/is_home_consumption_counter_auto", + "priority": 0, + "allow": true + }, { "acltype": "publishClientSend", "topic": "openWB/set/counter/+/config/max_total_power", @@ -1586,6 +1592,12 @@ "priority": 0, "allow": true }, + { + "acltype": "publishClientReceive", + "topic": "openWB/counter/+/config/is_home_consumption_counter_auto", + "priority": 0, + "allow": true + }, { "acltype": "publishClientReceive", "topic": "openWB/counter/+/config/max_total_power", diff --git a/packages/conftest.py b/packages/conftest.py index b99bb5a9a5..15959bfbe7 100644 --- a/packages/conftest.py +++ b/packages/conftest.py @@ -190,11 +190,12 @@ def data_() -> None: data.data.counter_data.update({ "counter0": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( spec=CounterGet, currents=[40]*3, power=6200, daily_imported=45000, daily_exported=3000, fault_state=0), - config=Mock(spec=CounterConfig, is_home_consumption_counter=True))), + config=Mock(spec=CounterConfig, is_home_consumption_counter=True, is_home_consumption_counter_auto=False))), "counter6": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( spec=CounterGet, currents=[25, 10, 25], power=13800, daily_imported=20000, daily_exported=0, imported=14000, exported=18000, fault_state=0), - config=Mock(spec=CounterConfig, max_currents=[32]*3, is_home_consumption_counter=False), + config=Mock(spec=CounterConfig, max_currents=[32]*3, + is_home_consumption_counter=False, is_home_consumption_counter_auto=False), set=Mock(spec=CounterSet, raw_currents_left=[31]*3)))}) diff --git a/packages/control/counter.py b/packages/control/counter.py index d10d72cbd3..c16124e61e 100644 --- a/packages/control/counter.py +++ b/packages/control/counter.py @@ -27,7 +27,8 @@ def get_counter_default_config(): return {"max_power_errorcase": 7000, "max_currents": [35]*3, "max_total_power": 24000, - "is_home_consumption_counter": False + "is_home_consumption_counter": False, + "is_home_consumption_counter_auto": True } @@ -44,6 +45,8 @@ class Config: "topic": "config/max_currents"}) max_total_power: float = field(default=0, metadata={"topic": "config/max_total_power"}) is_home_consumption_counter: bool = field(default=False, metadata={"topic": "config/is_home_consumption_counter"}) + is_home_consumption_counter_auto: bool = field( + default=True, metadata={"topic": "config/is_home_consumption_counter_auto"}) def config_factory() -> Config: diff --git a/packages/control/counter_all.py b/packages/control/counter_all.py index 80efd51164..4b63661d60 100644 --- a/packages/control/counter_all.py +++ b/packages/control/counter_all.py @@ -123,99 +123,90 @@ def set_home_consumption(self) -> None: except Exception: log.exception("Fehler in der allgemeinen Zähler-Klasse") - def _calc_home_consumption(self) -> Tuple[float, List]: - hc_all_power = 0 - no_hc_all_power = 0 - no_hc_evu = 0 - evu_id = self.get_id_evu_counter() - elements_to_sum_up = self.get_elements_for_downstream_calculation(evu_id) - - evu_is_HC = data.data.counter_data[f"counter{evu_id}"].data.config.is_home_consumption_counter - - for element in elements_to_sum_up: - if element["type"] == ComponentType.COUNTER.value: - component = data.data.counter_data[f"counter{element['id']}"] + def _get_component(self, element: Dict): + if element["type"] == ComponentType.COUNTER.value: + return data.data.counter_data[f"counter{element['id']}"] + elif element["type"] == ComponentType.CHARGEPOINT.value: + return data.data.cp_data[f"cp{element['id']}"] + elif element["type"] == ComponentType.BAT.value: + return data.data.bat_data[f"bat{element['id']}"] + elif element["type"] == ComponentType.INVERTER.value: + return data.data.pv_data[f"pv{element['id']}"] + else: + raise ValueError(f"Unbekannter Komponententyp: {element['type']}") - _hc_all_power, hc_counter = self._get_home_consumption_counter(element, evu_is_HC=evu_is_HC) - hc_all_power += _hc_all_power + def _get_is_home_consumption(self, counter: Counter, parent_home_consumption: bool) -> bool: + # Wenn auto ausgeählt ist, wird die einstellung vom Parent übernommen + # Wenn nicht, wird die Einstellung vom Zähler selbst genommen + if counter.data.config.is_home_consumption_counter_auto: + return parent_home_consumption + else: + return counter.data.config.is_home_consumption_counter - for counter in hc_counter: - no_hc_all_power += self._get_no_home_consumption(counter) - continue + def _get_local_power_from_counter(self, element) -> float: + # Wird nur von Countern aufgerufen + # Gib den lokalen Verbrauch des Zählers zurück + # Bewertet noch nicht, ob Hausverbrauch oder nicht + local_power = data.data.counter_data[f"counter{element['id']}"].data.get.power - elif element["type"] == ComponentType.BAT.value: - component = data.data.bat_data[f"bat{element['id']}"] - elif element["type"] == ComponentType.CHARGEPOINT.value: - component = data.data.cp_data[f"cp{element['id']}"] - elif element["type"] == ComponentType.INVERTER.value: - component = data.data.pv_data[f"pv{element['id']}"] + for child in element["children"]: + comp = self._get_component(child) - if component.data.get.fault_state < 2: - no_hc_evu += component.data.get.power + if comp.data.get.fault_state < 2: + local_power -= comp.data.get.power else: log.warning( - f"Komponente {element['type']}{component.num} ist im Fehlerzustand und wird nicht berücksichtigt.") + f"Komponente {element['type']}{comp.num} ist im Fehlerzustand und wird nicht berücksichtigt.") - evu_power = data.data.counter_data[f"counter{evu_id}"].data.get.power + return local_power + + def _calc_home_consumption_from_counter(self, element, parent_home_consumption): + # Wird nur von Countern aufgerufen + # Bewertet, ob Hausverbrauch oder nicht + # Gibt den Hausverbrauch des Zählers zurück + + home_consumption = 0.0 + local_power = self._get_local_power_from_counter(element) + + counter = self._get_component(element) + child_home_consumption = self._get_is_home_consumption(counter, parent_home_consumption) + + if child_home_consumption: + home_consumption += local_power - if data.data.counter_data[f"counter{evu_id}"].data.config.is_home_consumption_counter: - return (evu_power - no_hc_all_power - no_hc_evu - - self.data.set.smarthome_power_excluded_from_home_consumption), elements_to_sum_up - else: - return (hc_all_power - no_hc_all_power - - self.data.set.smarthome_power_excluded_from_home_consumption), elements_to_sum_up - - def _get_no_home_consumption(self, element) -> float: - # Summiert die Leistung aller Komponenten, die nicht als Hausverbrauch gezählt werden, - # unterhalb des angegebenen Elements. - not_home_consumption = 0 - if element["type"] != ComponentType.COUNTER.value: - # Wenn kein Counter, dann get power davon -> not_home_consumption - if element["type"] == ComponentType.CHARGEPOINT.value: - component = data.data.cp_data[f"cp{element['id']}"] - elif element["type"] == ComponentType.BAT.value: - component = data.data.bat_data[f"bat{element['id']}"] - elif element["type"] == ComponentType.INVERTER.value: - component = data.data.pv_data[f"pv{element['id']}"] - - if component.data.get.fault_state < 2: - not_home_consumption += component.data.get.power - - return not_home_consumption - - # Wenn Counter, dann get not_home_consumption von allen Children for child in element["children"]: - not_home_consumption += self._get_no_home_consumption(child) - - return not_home_consumption - - def _get_home_consumption_counter(self, elements, evu_is_HC=False) -> Tuple[float, List]: - # Sucht rekursiv HC-Zähler im Teilbaum, summiert deren Leistung - # und gibt eine Liste der gefundenen HC-Zähler zurück. - # -> wenn mehrer HC-Zähler auf der selben Ebene sind - - total_power = 0 - hc_counters = [] - - if elements["type"] == ComponentType.COUNTER.value: - component = data.data.counter_data[f"counter{elements['id']}"] - if component.data.config.is_home_consumption_counter or evu_is_HC: - hc_counters.append(elements) - total_power += component.data.get.power - # Kinder nicht weiter durchsuchen, - # da deren Leistung bereits enthalten ist. - return total_power, hc_counters + comp = self._get_component(child) + + if comp.data.get.fault_state < 2: + if isinstance(comp, Counter): + home_consumption += self._calc_home_consumption_from_counter(child, child_home_consumption) else: - # Zähler kein HC -> check Kinder - # Alle durchgehen, falls ein Zähler mehrer Zähler als Kinder hat - for element in elements["children"]: - if element["type"] == ComponentType.COUNTER.value: - power, counters = self._get_home_consumption_counter(element, evu_is_HC=evu_is_HC) - total_power += power - hc_counters.extend(counters) - else: - continue - return total_power, hc_counters + log.warning( + f"Komponente {element['type']}{comp.num} ist im Fehlerzustand und wird nicht berücksichtigt.") + + return home_consumption + + def _calc_home_consumption(self) -> Tuple[float, List]: + evu_id = self.get_id_evu_counter() + + # get_elements_for_downstream_calculation berücksichtigt Hybrid-Batterien + # wo die Bat im Wechselrichter ist (als child) und nicht direkt unter einem Zähler hängt + # + # get_elements_for_downstream_calculation liefert nur die Elemente unterhalb des EVU. + # Für die Rekursion bauen wir daher ein virtuelles Root-Element für den EVU-Zähler, + # ohne die echte Hierarchie zu verändern! + + elements = self.get_elements_for_downstream_calculation(evu_id) + evu_element = {"id": evu_id, "type": ComponentType.COUNTER.value, "children": elements} + + home_consumption = 0.0 + + # Rekursion startet immer beim EVU-Zähler. + home_consumption = self._calc_home_consumption_from_counter(evu_element, False) + + home_consumption -= self.data.set.smarthome_power_excluded_from_home_consumption + + return home_consumption, evu_element def _add_hybrid_bat(self, id: int) -> List: elements = [] @@ -524,6 +515,32 @@ def check_and_add(type_name: ComponentType, data_structure): "Lastmanagements gesetzt werden kann. Bitte zuerst einen EVU-Zähler hinzufügen."), MessageType.ERROR) + def _is_home_consumption_counter_by_id(self, counter_id: int) -> bool: + counter_entry = self.get_entry_of_element(counter_id) + if not counter_entry: + raise IndexError(f"Element {counter_id} konnte nicht in der Hierarchie gefunden werden.") + if counter_entry["type"] != ComponentType.COUNTER.value: + raise ValueError(f"Element {counter_id} ist kein Zähler.") + + counter_obj = data.data.counter_data[f"counter{counter_id}"] + + # Explizite Einstellung hat Vorrang, nur Auto wird vom Parent geerbt. + if not counter_obj.data.config.is_home_consumption_counter_auto: + return counter_obj.data.config.is_home_consumption_counter + + parent = self.get_entry_of_parent(counter_id) + if not parent or parent["type"] != ComponentType.COUNTER.value: + # Auto am Wurzel-Zähler entspricht dem bisherigen Startwert False. + return False + + return self._is_home_consumption_counter_by_id(parent["id"]) + + def is_home_consumption_counter(self, counter_id: int) -> bool: + """Ermittelt den effektiven Home-Consumption-Status eines Zählers. + Berücksichtigt den Auto-Parameter entlang aller übergeordneten Zähler. + """ + return self._is_home_consumption_counter_by_id(counter_id) + def get_max_id_in_hierarchy(current_entry: List, max_id: int) -> int: for item in current_entry: diff --git a/packages/control/counter_home_consumption_test.py b/packages/control/counter_home_consumption_test.py index 793455715c..3ad22809d6 100644 --- a/packages/control/counter_home_consumption_test.py +++ b/packages/control/counter_home_consumption_test.py @@ -226,10 +226,10 @@ def hierarchy_home_consumption_only_root() -> CounterAll: # counter11 <-- home consumption counter # | # - cp3 - # - counter6 + # - counter16 # | # - cp4 - # - counter13 + # - counter17 # | # - cp5 # @@ -238,17 +238,17 @@ def hierarchy_home_consumption_only_root() -> CounterAll: # UnbekannterVerbraucher/Hausverbrauch am Countern # counter11 = 500 - # coutner6 = 250 - # counter13 = 250 + # coutner16 = 250 + # counter17 = 250 # Final Home Consumption = 1000 c = CounterAll() c.data.get.hierarchy = [{"id": 11, "type": "counter", "children": [ {"id": 3, "type": "cp", "children": []}, - {"id": 6, "type": "counter", + {"id": 16, "type": "counter", "children": [ {"id": 4, "type": "cp", "children": []}, - {"id": 13, "type": "counter", + {"id": 17, "type": "counter", "children": [ {"id": 5, "type": "cp", "children": []}]}, {"id": 1, "type": "inverter", "children": []}, @@ -430,34 +430,53 @@ def data_home_consumption() -> None: data.data.counter_data.update({ "counter0": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( spec=CounterGet, power=6450, fault_state=0), - config=Mock(spec=CounterConfig, is_home_consumption_counter=False))), + config=Mock(spec=CounterConfig, is_home_consumption_counter=False, + is_home_consumption_counter_auto=False))), "counter6": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( spec=CounterGet, power=4300, fault_state=0), - config=Mock(spec=CounterConfig, is_home_consumption_counter=False))), + config=Mock(spec=CounterConfig, is_home_consumption_counter=False, + is_home_consumption_counter_auto=False))), "counter7": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( spec=CounterGet, power=20700, fault_state=0), - config=Mock(spec=CounterConfig, is_home_consumption_counter=False))), + config=Mock(spec=CounterConfig, is_home_consumption_counter=False, + is_home_consumption_counter_auto=False))), "counter13": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( spec=CounterGet, power=7150, fault_state=0), - config=Mock(spec=CounterConfig, is_home_consumption_counter=False))), + config=Mock(spec=CounterConfig, is_home_consumption_counter=False, + is_home_consumption_counter_auto=False))), "counter14": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( spec=CounterGet, power=-9750, fault_state=0), - config=Mock(spec=CounterConfig, is_home_consumption_counter=False))), + config=Mock(spec=CounterConfig, is_home_consumption_counter=False, + is_home_consumption_counter_auto=False))), "counter11": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( spec=CounterGet, power=6700, fault_state=0), - config=Mock(spec=CounterConfig, is_home_consumption_counter=True))), + config=Mock(spec=CounterConfig, is_home_consumption_counter=True, + is_home_consumption_counter_auto=False))), "counter8": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( spec=CounterGet, power=7400, fault_state=0), - config=Mock(spec=CounterConfig, is_home_consumption_counter=True))), + config=Mock(spec=CounterConfig, is_home_consumption_counter=True, + is_home_consumption_counter_auto=False))), "counter9": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( spec=CounterGet, power=4300, fault_state=0), - config=Mock(spec=CounterConfig, is_home_consumption_counter=True))), + config=Mock(spec=CounterConfig, is_home_consumption_counter=True, + is_home_consumption_counter_auto=False))), "counter10": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( spec=CounterGet, power=7150, fault_state=0), - config=Mock(spec=CounterConfig, is_home_consumption_counter=True))), + config=Mock(spec=CounterConfig, is_home_consumption_counter=True, + is_home_consumption_counter_auto=False))), "counter15": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( spec=CounterGet, power=-9750, fault_state=0), - config=Mock(spec=CounterConfig, is_home_consumption_counter=True))), + config=Mock(spec=CounterConfig, is_home_consumption_counter=True, + is_home_consumption_counter_auto=False))), + + "counter16": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( + spec=CounterGet, power=4300, fault_state=0), + config=Mock(spec=CounterConfig, is_home_consumption_counter=False, + is_home_consumption_counter_auto=True))), + "counter17": Mock(spec=Counter, data=Mock(spec=CounterData, get=Mock( + spec=CounterGet, power=7150, fault_state=0), + config=Mock(spec=CounterConfig, is_home_consumption_counter=False, + is_home_consumption_counter_auto=True))), }) diff --git a/packages/helpermodules/create_debug.py b/packages/helpermodules/create_debug.py index e2aa81f002..40b447fc4e 100644 --- a/packages/helpermodules/create_debug.py +++ b/packages/helpermodules/create_debug.py @@ -177,7 +177,7 @@ def config_and_state(): f"{component_data.data.config.max_currents} A\n" "--| Counter_Max_Power_Errorcase: " f"{component_data.data.config.max_power_errorcase} W\n") - elif component_data.data.config.is_home_consumption_counter: + elif counter_all_data.is_home_consumption_counter(component_data.num): parsed_data += ("--| Counter_Type: Hausverbrauchszähler\n" "--| Counter_Max_Power: " f"{component_data.data.config.max_total_power} W\n" @@ -251,7 +251,7 @@ def get_hierarchy(hierarchy, level=0): counter_all_data = data.data.counter_all_data if counter_all_data.get_evu_counter_str() == f"counter{component_data.num}": counter_type = ("EVU-Zähler") - elif (component_data.data.config.is_home_consumption_counter): + elif counter_all_data.is_home_consumption_counter(component_data.num): counter_type = ("Hausverbrauchszähler") else: counter_type = "Sonstiger Zähler" diff --git a/packages/helpermodules/measurement_logging/write_log.py b/packages/helpermodules/measurement_logging/write_log.py index 8b1db45e88..7d5270428e 100644 --- a/packages/helpermodules/measurement_logging/write_log.py +++ b/packages/helpermodules/measurement_logging/write_log.py @@ -246,14 +246,29 @@ def create_entry(log_type: LogType, sh_log_data: LegacySmartHomeLogData, previou log.exception("Fehler im Werte-Logging-Modul für EV "+str(ev)) counter_dict = {} + counter_all_data = data.data.counter_all_data + # Zählt alle effektiven Hausverbrauchs-Zähler, auch bei Auto-Vererbung über den Parent. + is_home_consumption_by_counter = {} + for current_counter in data.data.counter_data.values(): + try: + is_home_consumption_by_counter[current_counter.num] = counter_all_data.is_home_consumption_counter( + current_counter.num) + except Exception: + log.exception("Fehler beim Ermitteln der Hausverbrauchszähler.") + is_home_consumption_by_counter[current_counter.num] = False + + home_consumption_counter_count = sum(1 for is_hc in is_home_consumption_by_counter.values() if is_hc) + for counter in data.data.counter_data.values(): try: - if not counter.data.config.is_home_consumption_counter: + is_home_consumption_counter = is_home_consumption_by_counter.get(counter.num, False) + # Nur bei genau einem HV-Zähler ausblenden, bei mehreren einzeln anzeigen. + if not is_home_consumption_counter or home_consumption_counter_count > 1: counter_dict.update( {f"counter{counter.num}": { "imported": counter.data.get.imported, "exported": counter.data.get.exported, - "grid": True if data.data.counter_all_data.get_id_evu_counter() == counter.num else False, + "grid": True if counter_all_data.get_id_evu_counter() == counter.num else False, "fault_state": counter.data.get.fault_state}}) except Exception: log.exception("Fehler im Werte-Logging-Modul für Zähler "+str(counter)) diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index e02e36b151..fbd8f89a4c 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -944,7 +944,8 @@ def process_counter_topic(self, msg: mqtt.MQTTMessage): elif ("/config/max_total_power" in msg.topic or "/config/max_power_errorcase" in msg.topic): self._validate_value(msg, int, [(0, float("inf"))]) - elif ("/config/is_home_consumption_counter" in msg.topic): + elif ("/config/is_home_consumption_counter" in msg.topic + or "/config/is_home_consumption_counter_auto" in msg.topic): self._validate_value(msg, bool) elif subdata.SubData.counter_data.get(f"counter{get_index(msg.topic)}"): if ("/get/powers" in msg.topic or diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 90d2f560f8..5b320a1f03 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -213,6 +213,7 @@ class UpdateConfig: "^openWB/counter/[0-9]+/config/max_currents$", "^openWB/counter/[0-9]+/config/max_total_power$", "^openWB/counter/[0-9]+/config/is_home_consumption_counter$", + "^openWB/counter/[0-9]+/config/is_home_consumption_counter_auto$", "^openWB/general/allow_unencrypted_access$", "^openWB/general/extern$", @@ -3486,9 +3487,16 @@ def upgrade_datastore_138(self) -> None: def upgrade(topic: str, payload) -> Optional[dict]: if re.search("openWB/counter/[0-9]+/config", topic) is not None: index = get_index(topic) + new_topics = {} if f"openWB/counter/{index}/config/is_home_consumption_counter" not in self.all_received_topics: - is_home_consumption_counter = get_counter_default_config()["is_home_consumption_counter"] - return {f"openWB/counter/{index}/config/is_home_consumption_counter": is_home_consumption_counter} + new_topics[f"openWB/counter/{index}/config/is_home_consumption_counter"] = ( + get_counter_default_config()["is_home_consumption_counter"] + ) + if f"openWB/counter/{index}/config/is_home_consumption_counter_auto" not in self.all_received_topics: + new_topics[f"openWB/counter/{index}/config/is_home_consumption_counter_auto"] = ( + get_counter_default_config()["is_home_consumption_counter_auto"] + ) + return new_topics if new_topics else None self._loop_all_received_topics(upgrade) # Remove old Topic old_topic = "openWB/counter/config/home_consumption_source_id" @@ -3501,4 +3509,5 @@ def upgrade(topic: str, payload) -> Optional[dict]: log.warning(f"Invalid '{old_topic}' value: {source_id!r}; skipping migration") else: self.__update_topic(f"openWB/counter/{source_id}/config/is_home_consumption_counter", True) + self.__update_topic(f"openWB/counter/{source_id}/config/is_home_consumption_counter_auto", False) self._append_datastore_version(138) From 2f70d171fa8930f2e02d7a5e873bed297b6b68f4 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Thu, 13 Aug 2026 15:38:30 +0200 Subject: [PATCH 08/11] Updage update_config --- packages/helpermodules/update_config.py | 36 +++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 5b320a1f03..e1e69f95b3 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -3484,6 +3484,25 @@ def upgrade(topic: str, payload) -> Optional[dict]: self._append_datastore_version(137) def upgrade_datastore_138(self) -> None: + def get_direct_child_counter_ids(hierarchy, parent_counter_id: int) -> List[int]: + def find_counter_entry(elements) -> Optional[dict]: + for element in elements: + if element.get("type") == "counter" and element.get("id") == parent_counter_id: + return element + found = find_counter_entry(element.get("children", [])) + if found is not None: + return found + return None + + parent_entry = find_counter_entry(hierarchy) + if parent_entry is None: + return [] + return [ + child["id"] + for child in parent_entry.get("children", []) + if child.get("type") == "counter" + ] + def upgrade(topic: str, payload) -> Optional[dict]: if re.search("openWB/counter/[0-9]+/config", topic) is not None: index = get_index(topic) @@ -3508,6 +3527,23 @@ def upgrade(topic: str, payload) -> Optional[dict]: except (TypeError, ValueError): log.warning(f"Invalid '{old_topic}' value: {source_id!r}; skipping migration") else: + # Bisherigen Source-Counter explizit als Hausverbrauchs-Zähler setzen. self.__update_topic(f"openWB/counter/{source_id}/config/is_home_consumption_counter", True) self.__update_topic(f"openWB/counter/{source_id}/config/is_home_consumption_counter_auto", False) + + # Direkte Kind-Zähler explizit deaktivieren, damit Auto-Vererbung hier endet. + hierarchy_topic = "openWB/counter/get/hierarchy" + hierarchy = decode_payload(self.all_received_topics.get(hierarchy_topic, [])) + if isinstance(hierarchy, list): + for child_counter_id in get_direct_child_counter_ids(hierarchy, source_id): + self.__update_topic( + f"openWB/counter/{child_counter_id}/config/is_home_consumption_counter", False) + self.__update_topic( + f"openWB/counter/{child_counter_id}/config/is_home_consumption_counter_auto", False) + else: + log.warning( + "Migration der Hausverbrauchs-Zaehler (upgrade_datastore_138) fehlgeschlagen: " + f"ungueltige Hierarchie in '{hierarchy_topic}'. " + "Direkte Kind-Zaehler des bisherigen Hausverbrauchs-Zaehlers wurden nicht angepasst." + ) self._append_datastore_version(138) From 9e7534b56a0c10a21a02cb91d78fb2306b2ae226 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Fri, 14 Aug 2026 08:55:22 +0200 Subject: [PATCH 09/11] Add Type-Hints --- packages/control/counter_all.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/packages/control/counter_all.py b/packages/control/counter_all.py index 4b63661d60..ab7e7ce7ff 100644 --- a/packages/control/counter_all.py +++ b/packages/control/counter_all.py @@ -4,7 +4,7 @@ from dataclasses import dataclass, field import logging import re -from typing import Callable, Dict, List, Tuple, Union +from typing import Any, Callable, Dict, List, Tuple, Union from control import data from control.counter import Counter @@ -123,7 +123,7 @@ def set_home_consumption(self) -> None: except Exception: log.exception("Fehler in der allgemeinen Zähler-Klasse") - def _get_component(self, element: Dict): + def _get_component(self, element: Dict) -> Any: if element["type"] == ComponentType.COUNTER.value: return data.data.counter_data[f"counter{element['id']}"] elif element["type"] == ComponentType.CHARGEPOINT.value: @@ -143,7 +143,7 @@ def _get_is_home_consumption(self, counter: Counter, parent_home_consumption: bo else: return counter.data.config.is_home_consumption_counter - def _get_local_power_from_counter(self, element) -> float: + def _get_local_power_from_counter(self, element: Dict) -> float: # Wird nur von Countern aufgerufen # Gib den lokalen Verbrauch des Zählers zurück # Bewertet noch nicht, ob Hausverbrauch oder nicht @@ -160,7 +160,8 @@ def _get_local_power_from_counter(self, element) -> float: return local_power - def _calc_home_consumption_from_counter(self, element, parent_home_consumption): + def _calc_home_consumption_from_counter( + self, element: Dict, parent_home_consumption: bool) -> float: # Wird nur von Countern aufgerufen # Bewertet, ob Hausverbrauch oder nicht # Gibt den Hausverbrauch des Zählers zurück From cd7053e950df7f2ed93f92d7ddd25388793f385c Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Fri, 14 Aug 2026 08:59:56 +0200 Subject: [PATCH 10/11] fix Flake8 error --- packages/control/counter_all.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/control/counter_all.py b/packages/control/counter_all.py index ab7e7ce7ff..359d0c250b 100644 --- a/packages/control/counter_all.py +++ b/packages/control/counter_all.py @@ -4,7 +4,7 @@ from dataclasses import dataclass, field import logging import re -from typing import Any, Callable, Dict, List, Tuple, Union +from typing import Any, Callable, Dict, List, Optional, Tuple, Union from control import data from control.counter import Counter From 59c99ae1aaf608650428f75a2a9611c61045161b Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Fri, 14 Aug 2026 09:08:45 +0200 Subject: [PATCH 11/11] retrigger checks --- packages/control/counter_all.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/control/counter_all.py b/packages/control/counter_all.py index 359d0c250b..3c4677edf7 100644 --- a/packages/control/counter_all.py +++ b/packages/control/counter_all.py @@ -195,7 +195,7 @@ def _calc_home_consumption(self) -> Tuple[float, List]: # # get_elements_for_downstream_calculation liefert nur die Elemente unterhalb des EVU. # Für die Rekursion bauen wir daher ein virtuelles Root-Element für den EVU-Zähler, - # ohne die echte Hierarchie zu verändern! + # ohne die echte Hierarchie zu verändern. elements = self.get_elements_for_downstream_calculation(evu_id) evu_element = {"id": evu_id, "type": ComponentType.COUNTER.value, "children": elements}