diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index 24eab8feff..ef5e2b1a36 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -414,7 +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']}'.") - try: message = "" grid_counter = get_grid_counter(entry) @@ -446,22 +445,74 @@ def get_grid_counter(entry) -> Dict: consumption = 0 try: - pv_direct = min(pv_exported, consumption) - remaining = consumption - pv_direct - - bat_direct = min(bat_exported, remaining) - remaining -= bat_direct + # 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: + + direct = {"grid": grid_imported, "pv": pv_exported, "bat": bat_exported, "cp": cp_exported} + + # Einspeißung aufteilen + unassigned_export = grid_exported + 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 - cp_direct = min(cp_exported, remaining) - remaining -= cp_direct + 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}" + ) - grid_direct = min(grid_imported, remaining) + # Anschließend Verbrauch aufteilen, wenn vorhanden + direct_total = sum(direct.values()) - entry["energy_source"] = { - "grid": format(grid_direct / consumption), - "pv": format(pv_direct / consumption), - "bat": format(bat_direct / consumption), - "cp": format(cp_direct / consumption)} + 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)} 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..64f46bcf54 100644 --- a/packages/helpermodules/measurement_logging/process_log_unit_test.py +++ b/packages/helpermodules/measurement_logging/process_log_unit_test.py @@ -392,3 +392,193 @@ 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, + } + } + } + + # 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 + + # Realer Verbrauch + # 2 - 3 + 10 + 5 - 1 + 0 = 13 + + # Export aufteilen + # Pv - Export = 10 - 3 = 7 + + # Bat import aufteilen + # Pv - Bat_imported = 7 - 1 = 6 + + # Tatsächlicher Verbrauch: + # Pv = 6 + # Grid = 2 + # Bat = 5 + # --------------------- + # Summe = 13 + + # Anteil der Energiequellen: + # Grid: 2/13 ≈ 0.1538 + # PV = 6/13 ≈ 0.4615 + # Bat = 5/13 ≈ 0.3846 + # CP = 0/13 ≈ 0.0 + result, message = analyse_percentage(entry) + + assert result["energy_source"] == { + "grid": 0.1538, + "pv": 0.4615, + "bat": 0.3846, + "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 + # 2/9 7/9 + {"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 + # 0/12 7/12 5/12 + {"grid": 0.0, "pv": 0.5833, "bat": 0.4167, "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 + # 2/13 6/13 5/13 + {"grid": 0.1538, "pv": 0.4615, "bat": 0.3846, "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 + # 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", + 8.0, # pv_exported + 0.0, # bat_exported + 2.0, # bat_imported + 0.0, # cp_exported + 2.0, # grid_imported + 0.0, # grid_exported + # 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): + 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)