From 455aa9ebf27d02e97cda14699ed518b1fb0f4621 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Mon, 7 Sep 2026 12:18:57 +0200 Subject: [PATCH 1/4] proportionale Verteilung --- .../measurement_logging/process_log.py | 62 +++++- .../process_log_unit_test.py | 186 ++++++++++++++++++ 2 files changed, 247 insertions(+), 1 deletion(-) diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index 24eab8feff..b941baf8a4 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -415,6 +415,22 @@ def get_grid_counter(entry) -> Dict: else: raise KeyError(f"Kein Zähler für das Netz gefunden in Eintrag '{entry['timestamp']}'.") + def subtract_proportionally(direct: Dict, amount: float, sources: Dict) -> Dict: + amount = max(amount, 0) + available = {source: max(0, direct[source]) for source in sources} + + total_available = sum(available.values()) + if amount <= 0 or total_available <= 0: + return amount + amount_to_subtract = min(amount, total_available) + for source, value in available.items(): + share = value / total_available + direct[source] -= share * amount_to_subtract + + direct[source] = max(0, direct[source]) + + return amount - amount_to_subtract + try: message = "" grid_counter = get_grid_counter(entry) @@ -446,7 +462,50 @@ def get_grid_counter(entry) -> Dict: consumption = 0 try: - pv_direct = min(pv_exported, consumption) + """ + Neue Berechnung der Energiequellenanteile basierend auf Verbrauch und Einspeisung. + 1. Die Einspeisung aus PV, Batterie und CP wird proportional auf die Quellen verteilt. + -> Geneaue Aufteilung nicht bekannt -> deshalb proportional verteilt. + + 2. Der Batteriestrom import wird proportional auf die Quellen verteilt. + -> Nicht bekannt ob Pv/Grid/CP die Bat geladen hat -> deshalb proportional verteilt. + + 3. Der wirkliche Verbrauch (ohne Einspeisung und Bat laden) wird aufgeteilt auf energy_source. + """ + # """ + if consumption <= 0: + entry["energy_source"] = {"grid": 0, "pv": 0, "bat": 0, "cp": 0} + else: + + direct = {"grid": grid_imported, "pv": pv_exported, "bat": bat_exported, "cp": cp_exported} + + # Netzeinspeisung proportional auf die Quellen verteilen + remaining_grid_export = subtract_proportionally(direct, grid_exported, ("pv", "bat", "cp")) + + # Mehr eingespeister Strom als verfügbarer Verbrauch + if remaining_grid_export > 0: + subtract_proportionally(direct, remaining_grid_export, ("grid",)) + + # Batterie Import aufteilen + remaining_bat_import = subtract_proportionally(direct, bat_imported, ("pv", "cp", "grid")) + + # Mehr importierter Strom als verfügbarer Verbrauch + if remaining_bat_import > 0: + subtract_proportionally(direct, remaining_bat_import, ("bat",)) + # Reinen Verbrauch aufteilen + direct_total = sum(direct.values()) + + if direct_total <= 0: + entry["energy_source"] = {"grid": 0, "pv": 0, "bat": 0, "cp": 0} + else: + entry["energy_source"] = { + "grid": format(direct["grid"] / direct_total), + "pv": format(direct["pv"] / direct_total), + "bat": format(direct["bat"] / direct_total), + "cp": format(direct["cp"] / direct_total)} + # """ + """OLD + pv_direct = min(pv_exported - grid_exported - bat_imported, consumption) remaining = consumption - pv_direct bat_direct = min(bat_exported, remaining) @@ -462,6 +521,7 @@ def get_grid_counter(entry) -> Dict: "pv": format(pv_direct / consumption), "bat": format(bat_direct / consumption), "cp": format(cp_direct / consumption)} + # """ except ZeroDivisionError: entry["energy_source"] = {"grid": 0, "pv": 0, "bat": 0, "cp": 0} except Exception: diff --git a/packages/helpermodules/measurement_logging/process_log_unit_test.py b/packages/helpermodules/measurement_logging/process_log_unit_test.py index 30b56ec065..ab75633999 100644 --- a/packages/helpermodules/measurement_logging/process_log_unit_test.py +++ b/packages/helpermodules/measurement_logging/process_log_unit_test.py @@ -392,3 +392,189 @@ def test_collect_daily_log_data_json_decode_error(monkeypatch): # evaluation expected_result = {"entries": [], "names": {}} assert result == expected_result + + +def test_pv_export_and_bat_export_and_bat_import(): + entry = { + "timestamp": 1234567890, + "date": "00:00", + "bat": { + "all": { + "energy_imported": 1.0, + "energy_exported": 5.0, + "fault_state": 0, + } + }, + "cp": { + "all": { + "energy_imported": 0.0, + "energy_exported": 0.0, + "fault_state": 0, + } + }, + "pv": { + "all": { + "energy_exported": 10.0, + "fault_state": 0, + } + }, + "counter": { + "counter0": { + "grid": True, + "energy_imported": 2.0, + "energy_exported": 3.0, + "fault_state": 0, + } + } + } + # Pv 10 Exported + # Grid 2 Imported, 3 Exported + # Bat 1 Imported, 5 Exported + + # Reale Verbräuche + # 2-3+10+5-1+0 = 13 + + # Export aufteilen + # Lokal erzeugt 10+5 = 15 + # PV Anteil am Export: 2/3 ---> 2 kWh Einspeisung + # Bat Anteil am Export: 1/3 ---> 1 kWh Einspeisung + + # Pv = 8 + # Bat = 4 + # Grid = 2 + + # Batterie Import aufteilen + # Pv + Grid = 8 + 2 = 10 + # Anteil an Bat import + # PV 0,8 -> 0.8kWh + # Grid 0,2 -> 0.2kWh + + # Tatsächlicher Verbruach: + # Pv = 8 - 0.8 = 7.2 + # Grid = 2 - 0.2 = 1.8 + # Bat = 4 + # --------------------- + # Summe = 13 + + # Anteil der Energiequellen: + # Grid: 1,8/13 ≈ 0.1385 + # PV = 7,2/13 ≈ 0.5538 + # Bat = 4/13 ≈ 0.3077 + # CP = 0/13 ≈ 0.0 + + # Einspeisung + # 1 Pv + # 2 Bat + # CP + + # Speicherladen + # 1 Pv + # Grid + # CP + + result, message = analyse_percentage(entry) + + assert result["energy_source"] == { + "grid": 0.1385, + "pv": 0.5538, + "bat": 0.3077, + "cp": 0.0 + } + assert message == "" + + +@pytest.mark.parametrize( + "name, pv_exported, bat_exported, bat_imported, cp_exported, grid_imported, grid_exported, expected", + [ + ( + "grid import and export", + 10.0, # pv_exported + 0.0, # bat_exported + 0.0, # bat_imported + 0.0, # cp_exported + 2.0, # grid_imported + 3.0, # grid_exported + {"grid": 0.2222, "pv": 0.7778, "bat": 0.0, "cp": 0.0} + ), + ( + "grid export proportional pv and bat", + 10.0, # pv_exported + 5.0, # bat_exported + 0.0, # bat_imported + 0.0, # cp_exported + 0.0, # grid_imported + 3.0, # grid_exported + {"grid": 0.0, "pv": 0.6667, "bat": 0.3333, "cp": 0.0} + ), + ( + "grid export and bat import", + 10.0, # pv_exported + 5.0, # bat_exported + 1.0, # bat_imported + 0.0, # cp_exported + 2.0, # grid_imported + 3.0, # grid_exported + {"grid": 0.1385, "pv": 0.5538, "bat": 0.3077, "cp": 0.0} + ), + ( + "grid export proportional pv and cp ", + 6.0, # pv_exported + 0.0, # bat_exported + 0.0, # bat_imported + 3.0, # cp_exported + 0.0, # grid_imported + 3.0, # grid_exported + {"grid": 0.0, "pv": 0.6667, "bat": 0.0, "cp": 0.3333} + ), + ( + "bat import proportional pv and grid", + 8.0, # pv_exported + 0.0, # bat_exported + 2.0, # bat_imported + 0.0, # cp_exported + 2.0, # grid_imported + 0.0, # grid_exported + {"grid": 0.2, "pv": 0.8, "bat": 0.0, "cp": 0.0} + ), + ] +) +def test_analyse_percentage_proportional_distribution(name, pv_exported, bat_exported, bat_imported, cp_exported, grid_imported, grid_exported, expected): + entry = { + "timestamp": 1234567890, + "date": "00:00", + "bat": { + "all": { + "energy_imported": bat_imported, + "energy_exported": bat_exported, + "fault_state": 0, + } + }, + "cp": { + "all": { + "energy_exported": cp_exported, + "fault_state": 0, + } + }, + "pv": { + "all": { + "energy_exported": pv_exported, + "fault_state": 0, + } + }, + "counter": { + "counter0": { + "grid": True, + "energy_imported": grid_imported, + "energy_exported": grid_exported, + "fault_state": 0, + } + } + } + + result, message = analyse_percentage(entry) + + assert result["energy_source"] == expected + assert message == "" + + # Strommix muss zusammen 100% ergeben + assert sum(result["energy_source"].values()) == pytest.approx(1.0, abs=0.0002) From 17b5771860bc05f6a06790977ba037803c3ae692 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Mon, 7 Sep 2026 12:31:29 +0200 Subject: [PATCH 2/4] =?UTF-8?q?Priorit=C3=A4tsverteilung?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../measurement_logging/process_log.py | 76 ++++++++--------- .../process_log_unit_test.py | 84 ++++++++++--------- 2 files changed, 79 insertions(+), 81 deletions(-) diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index b941baf8a4..823ad09c33 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -463,36 +463,49 @@ def subtract_proportionally(direct: Dict, amount: float, sources: Dict) -> Dict: try: """ - Neue Berechnung der Energiequellenanteile basierend auf Verbrauch und Einspeisung. - 1. Die Einspeisung aus PV, Batterie und CP wird proportional auf die Quellen verteilt. - -> Geneaue Aufteilung nicht bekannt -> deshalb proportional verteilt. - - 2. Der Batteriestrom import wird proportional auf die Quellen verteilt. - -> Nicht bekannt ob Pv/Grid/CP die Bat geladen hat -> deshalb proportional verteilt. - - 3. Der wirkliche Verbrauch (ohne Einspeisung und Bat laden) wird aufgeteilt auf energy_source. + Berechnung der Energiequellenanteile: + Da die genaue Aufteilung der Energiequellen nicht bekannt ist, + wird die Einspeißung (grid_exported) entsprechend der folgenden Priorität aufgeteilt: + 1. PV + 2. Batterie + 3. CP + Sollte die Einspeißung nicht komplett von PV gedeckt werden, + wird der Rest von der Batterie übernommen, und falls nötig, vom CP. + + Entsprechend ähnlich wird der Batterieimport nach folgender Priorität aufgeteilt: + 1. PV + 2. Grid + 3. CP + + Anschließend wird der Verbrauch (ohne Einspeisung und Batterieimport) auf energy_source aufgeteilt. """ - # """ if consumption <= 0: entry["energy_source"] = {"grid": 0, "pv": 0, "bat": 0, "cp": 0} else: direct = {"grid": grid_imported, "pv": pv_exported, "bat": bat_exported, "cp": cp_exported} - # Netzeinspeisung proportional auf die Quellen verteilen - remaining_grid_export = subtract_proportionally(direct, grid_exported, ("pv", "bat", "cp")) - - # Mehr eingespeister Strom als verfügbarer Verbrauch - if remaining_grid_export > 0: - subtract_proportionally(direct, remaining_grid_export, ("grid",)) - - # Batterie Import aufteilen - remaining_bat_import = subtract_proportionally(direct, bat_imported, ("pv", "cp", "grid")) + # Einspeißung aufteilen + unassigned_export = grid_exported + for source in ("pv", "bat", "cp"): + if direct[source] > unassigned_export: + direct[source] -= unassigned_export + break + else: + unassigned_export -= direct[source] + direct[source] = 0 + + # Batterieimport aufteilen + unassigned_bat_import = bat_imported + for source in ("pv", "grid", "cp"): + if direct[source] > unassigned_bat_import: + direct[source] -= unassigned_bat_import + break + else: + unassigned_bat_import -= direct[source] + direct[source] = 0 - # Mehr importierter Strom als verfügbarer Verbrauch - if remaining_bat_import > 0: - subtract_proportionally(direct, remaining_bat_import, ("bat",)) - # Reinen Verbrauch aufteilen + # Anschließend Verbrauch aufteilen, wenn vorhanden direct_total = sum(direct.values()) if direct_total <= 0: @@ -503,25 +516,6 @@ def subtract_proportionally(direct: Dict, amount: float, sources: Dict) -> Dict: "pv": format(direct["pv"] / direct_total), "bat": format(direct["bat"] / direct_total), "cp": format(direct["cp"] / direct_total)} - # """ - """OLD - pv_direct = min(pv_exported - grid_exported - bat_imported, consumption) - remaining = consumption - pv_direct - - bat_direct = min(bat_exported, remaining) - remaining -= bat_direct - - cp_direct = min(cp_exported, remaining) - remaining -= cp_direct - - grid_direct = min(grid_imported, remaining) - - entry["energy_source"] = { - "grid": format(grid_direct / consumption), - "pv": format(pv_direct / consumption), - "bat": format(bat_direct / consumption), - "cp": format(cp_direct / consumption)} - # """ except ZeroDivisionError: entry["energy_source"] = {"grid": 0, "pv": 0, "bat": 0, "cp": 0} except Exception: diff --git a/packages/helpermodules/measurement_logging/process_log_unit_test.py b/packages/helpermodules/measurement_logging/process_log_unit_test.py index ab75633999..64f46bcf54 100644 --- a/packages/helpermodules/measurement_logging/process_log_unit_test.py +++ b/packages/helpermodules/measurement_logging/process_log_unit_test.py @@ -427,57 +427,50 @@ def test_pv_export_and_bat_export_and_bat_import(): } } } + + # Einspeisung - Priorität + # 1 Pv + # 2 Bat + # 3 CP + # -> erst komplette Einspeisung von PV berücksichtigen + # -> wenn dann noch weitere Einspeisung übrig ist -> Bat und dann CP + + # Speicherimport - Priorität + # 1 Pv + # 2 Grid + # 3 CP + # Pv 10 Exported # Grid 2 Imported, 3 Exported # Bat 1 Imported, 5 Exported - # Reale Verbräuche - # 2-3+10+5-1+0 = 13 + # Realer Verbrauch + # 2 - 3 + 10 + 5 - 1 + 0 = 13 # Export aufteilen - # Lokal erzeugt 10+5 = 15 - # PV Anteil am Export: 2/3 ---> 2 kWh Einspeisung - # Bat Anteil am Export: 1/3 ---> 1 kWh Einspeisung - - # Pv = 8 - # Bat = 4 - # Grid = 2 + # Pv - Export = 10 - 3 = 7 - # Batterie Import aufteilen - # Pv + Grid = 8 + 2 = 10 - # Anteil an Bat import - # PV 0,8 -> 0.8kWh - # Grid 0,2 -> 0.2kWh + # Bat import aufteilen + # Pv - Bat_imported = 7 - 1 = 6 - # Tatsächlicher Verbruach: - # Pv = 8 - 0.8 = 7.2 - # Grid = 2 - 0.2 = 1.8 - # Bat = 4 + # Tatsächlicher Verbrauch: + # Pv = 6 + # Grid = 2 + # Bat = 5 # --------------------- # Summe = 13 # Anteil der Energiequellen: - # Grid: 1,8/13 ≈ 0.1385 - # PV = 7,2/13 ≈ 0.5538 - # Bat = 4/13 ≈ 0.3077 + # Grid: 2/13 ≈ 0.1538 + # PV = 6/13 ≈ 0.4615 + # Bat = 5/13 ≈ 0.3846 # CP = 0/13 ≈ 0.0 - - # Einspeisung - # 1 Pv - # 2 Bat - # CP - - # Speicherladen - # 1 Pv - # Grid - # CP - result, message = analyse_percentage(entry) assert result["energy_source"] == { - "grid": 0.1385, - "pv": 0.5538, - "bat": 0.3077, + "grid": 0.1538, + "pv": 0.4615, + "bat": 0.3846, "cp": 0.0 } assert message == "" @@ -494,6 +487,7 @@ def test_pv_export_and_bat_export_and_bat_import(): 0.0, # cp_exported 2.0, # grid_imported 3.0, # grid_exported + # 2/9 7/9 {"grid": 0.2222, "pv": 0.7778, "bat": 0.0, "cp": 0.0} ), ( @@ -504,7 +498,8 @@ def test_pv_export_and_bat_export_and_bat_import(): 0.0, # cp_exported 0.0, # grid_imported 3.0, # grid_exported - {"grid": 0.0, "pv": 0.6667, "bat": 0.3333, "cp": 0.0} + # 0/12 7/12 5/12 + {"grid": 0.0, "pv": 0.5833, "bat": 0.4167, "cp": 0.0} ), ( "grid export and bat import", @@ -514,7 +509,8 @@ def test_pv_export_and_bat_export_and_bat_import(): 0.0, # cp_exported 2.0, # grid_imported 3.0, # grid_exported - {"grid": 0.1385, "pv": 0.5538, "bat": 0.3077, "cp": 0.0} + # 2/13 6/13 5/13 + {"grid": 0.1538, "pv": 0.4615, "bat": 0.3846, "cp": 0.0} ), ( "grid export proportional pv and cp ", @@ -524,7 +520,8 @@ def test_pv_export_and_bat_export_and_bat_import(): 3.0, # cp_exported 0.0, # grid_imported 3.0, # grid_exported - {"grid": 0.0, "pv": 0.6667, "bat": 0.0, "cp": 0.3333} + # 0/6 3/6 0/6 3/6 + {"grid": 0.0, "pv": 0.5, "bat": 0.0, "cp": 0.5} ), ( "bat import proportional pv and grid", @@ -534,11 +531,18 @@ def test_pv_export_and_bat_export_and_bat_import(): 0.0, # cp_exported 2.0, # grid_imported 0.0, # grid_exported - {"grid": 0.2, "pv": 0.8, "bat": 0.0, "cp": 0.0} + # 2/8 6/8 0/8 8/8 + {"grid": 0.25, "pv": 0.75, "bat": 0.0, "cp": 0.0} ), ] ) -def test_analyse_percentage_proportional_distribution(name, pv_exported, bat_exported, bat_imported, cp_exported, grid_imported, grid_exported, expected): +def test_analyse_percentage_proportional_distribution(name, + pv_exported, + bat_exported, + bat_imported, + cp_exported, + grid_imported, + grid_exported, expected): entry = { "timestamp": 1234567890, "date": "00:00", From 7a7bfa6f234cbd5a79e25d499104467de5d78e56 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Mon, 7 Sep 2026 12:40:31 +0200 Subject: [PATCH 3/4] Remove unused Funktion --- .../measurement_logging/process_log.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index 823ad09c33..16f2849b4c 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -414,23 +414,6 @@ def get_grid_counter(entry) -> Dict: return counter else: raise KeyError(f"Kein Zähler für das Netz gefunden in Eintrag '{entry['timestamp']}'.") - - def subtract_proportionally(direct: Dict, amount: float, sources: Dict) -> Dict: - amount = max(amount, 0) - available = {source: max(0, direct[source]) for source in sources} - - total_available = sum(available.values()) - if amount <= 0 or total_available <= 0: - return amount - amount_to_subtract = min(amount, total_available) - for source, value in available.items(): - share = value / total_available - direct[source] -= share * amount_to_subtract - - direct[source] = max(0, direct[source]) - - return amount - amount_to_subtract - try: message = "" grid_counter = get_grid_counter(entry) From e3b06fab6ff1fbae33f316b4408879140b8c2edf Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Mon, 7 Sep 2026 14:02:41 +0200 Subject: [PATCH 4/4] Add Copilot suggestions --- .../measurement_logging/process_log.py | 48 ++++++++++++------- 1 file changed, 31 insertions(+), 17 deletions(-) diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index 16f2849b4c..ef5e2b1a36 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -445,23 +445,21 @@ def get_grid_counter(entry) -> Dict: consumption = 0 try: - """ - Berechnung der Energiequellenanteile: - Da die genaue Aufteilung der Energiequellen nicht bekannt ist, - wird die Einspeißung (grid_exported) entsprechend der folgenden Priorität aufgeteilt: - 1. PV - 2. Batterie - 3. CP - Sollte die Einspeißung nicht komplett von PV gedeckt werden, - wird der Rest von der Batterie übernommen, und falls nötig, vom CP. - - Entsprechend ähnlich wird der Batterieimport nach folgender Priorität aufgeteilt: - 1. PV - 2. Grid - 3. CP - - Anschließend wird der Verbrauch (ohne Einspeisung und Batterieimport) auf energy_source aufgeteilt. - """ + # Berechnung der Energiequellenanteile: + # Da die genaue Aufteilung der Energiequellen nicht bekannt ist, + # wird die Einspeisung (grid_exported) entsprechend der folgenden Priorität aufgeteilt: + # 1. PV + # 2. Batterie + # 3. CP + # Sollte die Einspeisung nicht komplett von PV gedeckt werden, + # wird der Rest von der Batterie übernommen, und falls nötig, vom CP. + # + # Entsprechend ähnlich wird der Batterieimport nach folgender Priorität aufgeteilt: + # 1. PV + # 2. Grid + # 3. CP + # + # Anschließend wird der Verbrauch (ohne Einspeisung und Batterieimport) auf energy_source aufgeteilt. if consumption <= 0: entry["energy_source"] = {"grid": 0, "pv": 0, "bat": 0, "cp": 0} else: @@ -473,21 +471,37 @@ def get_grid_counter(entry) -> Dict: for source in ("pv", "bat", "cp"): if direct[source] > unassigned_export: direct[source] -= unassigned_export + unassigned_export = 0 break else: unassigned_export -= direct[source] direct[source] = 0 + if unassigned_export > 0: + # Fehler / inkonsistente Energiebilanz + log.warning( + f"grid_exported konnte nicht vollständig verteilt werden. " + f"Unverteilter Anteil: {unassigned_export}" + ) + # Batterieimport aufteilen unassigned_bat_import = bat_imported for source in ("pv", "grid", "cp"): if direct[source] > unassigned_bat_import: direct[source] -= unassigned_bat_import + unassigned_bat_import = 0 break else: unassigned_bat_import -= direct[source] direct[source] = 0 + if unassigned_bat_import > 0: + # Fehler / inkonsistente Energiebilanz + log.warning( + f"bat_imported konnte nicht vollständig verteilt werden. " + f"Unverteilter Anteil: {unassigned_bat_import}" + ) + # Anschließend Verbrauch aufteilen, wenn vorhanden direct_total = sum(direct.values())