diff --git a/packages/control/chargelog/chargelog.py b/packages/control/chargelog/chargelog.py index 2c3b42e372..e9a87b7339 100644 --- a/packages/control/chargelog/chargelog.py +++ b/packages/control/chargelog/chargelog.py @@ -14,6 +14,8 @@ from helpermodules.measurement_logging.process_log import ( FILE_ERRORS, CalculationType, _analyse_energy_source, _process_entries, get_totals) +from helpermodules.measurement_logging.write_log import LogType, LegacySmartHomeLogData, create_entry + # alte Daten: Startzeitpunkt der Ladung, Endzeitpunkt, Geladene Reichweite, Energie, Leistung, Ladedauer, LP-Nummer, # Lademodus, ID-Tag # json-Objekt: new_entry = { @@ -328,7 +330,7 @@ def write_new_entry(new_entry): def calc_energy_costs(cp, create_log_entry: bool = False): try: if cp.data.set.log.imported_since_plugged != 0 and cp.data.set.log.imported_since_mode_switch != 0: - processed_entries, reference_entries = _get_reference_entries(cp) + processed_entries, reference_entries = _get_reference_entries(cp, create_log_entry) charged_energy_by_source = calculate_charged_energy_by_source( cp, processed_entries, reference_entries, create_log_entry) _add_charged_energy_by_source(cp, charged_energy_by_source) @@ -361,15 +363,17 @@ def calculate_charged_energy_by_source(cp, processed_entries, reference_entries, reference_entries[0]["cp"][f"cp{cp.num}"]["imported"]) elif reference == ReferenceTime.END: if sum(cp.data.set.log.charged_energy_by_source.values()) == 0: + # Kurze Ladung, für die noch kein 5min-Intervall vergangen ist charged_energy = cp.data.set.log.imported_since_mode_switch else: - log.debug(f"cp.data.get.imported {cp.data.get.imported}") - charged_energy = cp.data.get.imported - \ - reference_entries[-1]["cp"][f"cp{cp.num}"]["imported"] + # Nur das noch nicht verbuchte Intervall + charged_energy = (reference_entries[-1]["cp"][f"cp{cp.num}"]["imported"] - + reference_entries[0]["cp"][f"cp{cp.num}"]["imported"]) else: raise TypeError(f"Unbekannter Referenz-Zeitpunkt {reference}") log.debug(f'power source {relative_energy_source}') log.debug(f"charged_energy {charged_energy}") + # Wenn keine Energie geladen wurde, keine Anteile berechnen if charged_energy <= 0: # wenn nur entladen wurde, keine Anteile berechnen return {source: 0 for source in ENERGY_SOURCES} @@ -402,14 +406,26 @@ def _get_reference_position(cp, create_log_entry: bool) -> ReferenceTime: return ReferenceTime.MIDDLE -def _get_reference_entries(cp) -> Tuple[List[Dict], List]: +def _get_reference_entries(cp, create_log_entry: bool = False) -> Tuple[List[Dict], List]: processed_entries = {} reference_entries = [] try: log_data = get_todays_daily_log() names = log_data["names"] entries = log_data["entries"] - if len(entries) >= 2: + if create_log_entry: + # beim beenden einer Ladung den aktuellen Messwert benutzen + if entries: + previous_entry = entries[-1] + else: + # Bei Tageswechsel + date_day_before = (datetime.datetime.now() + datetime.timedelta(days=-1)).strftime("%Y%m%d") + entries_day_before = get_daily_log(date_day_before)["entries"] + previous_entry = entries_day_before[-1] + + current_entry = create_entry(LogType.DAILY, LegacySmartHomeLogData(), previous_entry) + reference_entries = [previous_entry, current_entry] + elif len(entries) >= 2: reference_entries = [entries[-2], entries[-1]] else: date_day_before = (datetime.datetime.now() + datetime.timedelta(days=-1)).strftime("%Y%m%d") diff --git a/packages/control/chargelog/chargelog_test.py b/packages/control/chargelog/chargelog_test.py index 7ef150db23..fe3e535d43 100644 --- a/packages/control/chargelog/chargelog_test.py +++ b/packages/control/chargelog/chargelog_test.py @@ -92,6 +92,14 @@ def test_calc_charge_cost_reference_end(mock_data, monkeypatch): cp.data.get.imported = 4100 cp.data.set.log.charged_energy_by_source = {'grid': 1243, 'pv': 386, 'bat': 671, 'cp': 0.0} daily_log = mock_daily_log(monkeypatch) + current_entry = json.loads(json.dumps(daily_log["entries"][-1])) + current_entry["bat"]["all"]["exported"] = 4000 + current_entry["bat"]["bat2"]["exported"] = 4000 + current_entry["counter"]["counter0"]["imported"] = 4500 + current_entry["cp"]["cp4"]["imported"] = 4100 + current_entry["pv"]["all"]["exported"] = 3000 + current_entry["pv"]["pv1"]["exported"] = 3000 + monkeypatch.setattr(chargelog, "create_entry", Mock(return_value=current_entry)) with patch("builtins.open", mock_open(read_data=json.dumps(daily_log))): calc_energy_costs(cp, True) @@ -100,6 +108,77 @@ def test_calc_charge_cost_reference_end(mock_data, monkeypatch): assert round(cp.data.set.log.costs, 5) == 0.025 +def test_calc_charge_cost_short_charge_uses_current_entry(mock_data, monkeypatch): + cp = Chargepoint(4, None) + + # Kurzer Ladevorgang: insgesamt nur 80 Wh geladen. + # Es wurde vorher noch kein Energieanteil verbucht. + cp.data.set.log.imported_since_plugged = 80 + cp.data.set.log.imported_since_mode_switch = 80 + cp.data.set.log.timestamp_mode_switch = 1652683260 # 8:41 + cp.data.get.imported = 4080 + cp.data.set.log.charged_energy_by_source = { + 'bat': 0, + 'cp': 0, + 'grid': 0, + 'pv': 0 + } + + daily_log = mock_daily_log(monkeypatch) + + # Letzter gespeicherter Daily-Log ist 8:40: + # + # Grid imported = 2500 Wh + # PV exported = 2500 Wh + # CP imported = 4000 Wh + # + # Jetzt erzeugen wir den Live-Snapshot beim Ladeende um 8:42. + current_entry = json.loads(json.dumps(daily_log["entries"][-1])) + + current_entry["date"] = "8:42" + current_entry["timestamp"] = 1652683320 + + # Zwischen 8:40 und 8:42: + # + # +20 Wh Netz + # +60 Wh PV + # =80 Wh Verbrauch/Ladung + # + # Erwarteter Strommix: + # Netz = 20 / 80 = 25 % + # PV = 60 / 80 = 75 % + + current_entry["counter"]["counter0"]["imported"] = 2520 + + current_entry["pv"]["all"]["exported"] = 2560 + current_entry["pv"]["pv1"]["exported"] = 2560 + + current_entry["cp"]["all"]["imported"] = 4080 + current_entry["cp"]["cp4"]["imported"] = 4080 + + # _get_reference_entries() soll diesen aktuellen Snapshot verwenden. + monkeypatch.setattr( + chargelog, + "create_entry", + Mock(return_value=current_entry) + ) + + calc_energy_costs(cp, True) + + assert cp.data.set.log.charged_energy_by_source == pytest.approx({ + 'bat': 0, + 'cp': 0, + 'grid': 20, + 'pv': 60 + }) + + # Kosten: + # Netz: 20 Wh * 0.0003 €/Wh = 0.006 € + # PV: 60 Wh * 0.00015 €/Wh = 0.009 € + # Gesamt 0.015 € + assert cp.data.set.log.costs == pytest.approx(0.015) + + def test_calc_charge_cost_reference_end_unique_price(mock_data, monkeypatch): cp = Chargepoint(4, None) cp.data.set.log.imported_since_plugged = cp.data.set.log.imported_since_mode_switch = 3950 @@ -107,6 +186,14 @@ def test_calc_charge_cost_reference_end_unique_price(mock_data, monkeypatch): cp.data.get.imported = 4100 cp.data.set.log.charged_energy_by_source = {'grid': 1243, 'pv': 386, 'bat': 671, 'cp': 0.0} daily_log = mock_daily_log(monkeypatch) + current_entry = json.loads(json.dumps(daily_log["entries"][-1])) + current_entry["bat"]["all"]["exported"] = 4000 + current_entry["bat"]["bat2"]["exported"] = 4000 + current_entry["counter"]["counter0"]["imported"] = 4500 + current_entry["cp"]["cp4"]["imported"] = 4100 + current_entry["pv"]["all"]["exported"] = 3000 + current_entry["pv"]["pv1"]["exported"] = 3000 + monkeypatch.setattr(chargelog, "create_entry", Mock(return_value=current_entry)) data.data.general_data.data.prices = Prices(bat=0.0002, cp=0, grid=0.0002, pv=0.0002) with patch("builtins.open", mock_open(read_data=json.dumps(daily_log))): diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index b7415e7480..39bec7cf5d 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -409,6 +409,18 @@ def analyse_percentage(entry) -> Tuple[Dict, str]: def format(value): return round(value, 4) + def get_valid_energy(section: str, value: str) -> Union[int, float]: + """Liefert den Energiewert ohne fehlerhafte Einzelmodule.""" + modules = entry.get(section, {}) + if (len(modules) <= 1 or + safe_get_nested(entry, section, "all", "fault_state") != 2): + return safe_get_nested(entry, section, "all", value) + return sum( + module_data.get(value, 0) + for module_name, module_data in modules.items() + if module_name != "all" and module_data.get("fault_state", 0) != 2 + ) + def get_grid_counter(entry) -> Dict: # es gibt nur einen Zähler am EVU-Punkt for counter in entry["counter"].values(): @@ -419,27 +431,26 @@ def get_grid_counter(entry) -> Dict: try: message = "" grid_counter = get_grid_counter(entry) - # Wenn neben dem "all" Eintrag kein weiterer Eintrag existiert, dann gibt es keine Komponenten. - if ((safe_get_nested(entry, "bat", "all", "fault_state") == 2 and len(entry.get("bat", {})) > 1) or - (safe_get_nested(entry, "cp", "all", "fault_state") == 2 and len(entry.get("cp", {})) > 1) or - (safe_get_nested(entry, "pv", "all", "fault_state") == 2 and len(entry.get("pv", {})) > 1) or - grid_counter.get("fault_state", None) == 2): - + # nur wenn der Grid-Zähler im Feherlzustand ist + # ins Fallback gehen + if (grid_counter.get("fault_state", None) == 2): entry["energy_source"] = {"grid": 1, "pv": 0, "bat": 0, "cp": 0} + message += EOOR_STATE_MSG.format("der Zähler für das Netz") + else: if safe_get_nested(entry, "bat", "all", "fault_state") == 2 and len(entry.get("bat", {})) > 1: - message += EOOR_STATE_MSG.format("mind. einer der Speicher") + message += f"Der Strom-Mix um {entry['date'] } wird trotz Fehlerzustand " \ + "mindestens eines Speichers aus den vorhandenen Messwerten berechnet. \n" if safe_get_nested(entry, "cp", "all", "fault_state") == 2 and len(entry.get("cp", {})) > 1: - message += EOOR_STATE_MSG.format("mind. einer der Ladepunkte") + message += f"Der Strom-Mix um {entry['date'] } wird trotz Fehlerzustand " \ + "mindestens eines Ladepunkts aus den vorhandenen Messwerten berechnet. \n" if safe_get_nested(entry, "pv", "all", "fault_state") == 2 and len(entry.get("pv", {})) > 1: - message += EOOR_STATE_MSG.format("mind. einer der Wechselrichter") - if grid_counter.get("fault_state", None) == 2: - message += EOOR_STATE_MSG.format("der Zähler für das Netz") + message += f"Der Strom-Mix um {entry['date'] } wird trotz Fehlerzustand " \ + "mindestens eines Wechselrichters aus den vorhandenen Messwerten berechnet. \n" - else: - bat_imported = safe_get_nested(entry, "bat", "all", "energy_imported") - bat_exported = safe_get_nested(entry, "bat", "all", "energy_exported") - cp_exported = safe_get_nested(entry, "cp", "all", "energy_exported") - pv_exported = safe_get_nested(entry, "pv", "all", "energy_exported") + bat_imported = get_valid_energy("bat", "energy_imported") + bat_exported = get_valid_energy("bat", "energy_exported") + cp_exported = get_valid_energy("cp", "energy_exported") + pv_exported = get_valid_energy("pv", "energy_exported") grid_imported = grid_counter.get("energy_imported", 0) grid_exported = grid_counter.get("energy_exported", 0) consumption = grid_imported - grid_exported + pv_exported + bat_exported - bat_imported + cp_exported diff --git a/packages/helpermodules/measurement_logging/process_log_unit_test.py b/packages/helpermodules/measurement_logging/process_log_unit_test.py index 62a9e0fff2..b4da9869f1 100644 --- a/packages/helpermodules/measurement_logging/process_log_unit_test.py +++ b/packages/helpermodules/measurement_logging/process_log_unit_test.py @@ -55,6 +55,53 @@ def test_analyse_percentage(name: str, assert message == "" +def test_analyse_percentage_ignores_faulty_modules(): + entry = { + "timestamp": 1234567890, + "date": "00:31", + "bat": { + "all": {"energy_imported": 0, "energy_exported": 120, "fault_state": 2}, + "bat1": {"energy_imported": 0, "energy_exported": 20, "fault_state": 0}, + "bat2": {"energy_imported": 0, "energy_exported": 100, "fault_state": 2}, + }, + "cp": { + "all": {"energy_exported": 80, "fault_state": 2}, + "cp1": {"energy_exported": 30, "fault_state": 0}, + "cp2": {"energy_exported": 50, "fault_state": 2}, + }, + "pv": { + "all": {"energy_exported": 100, "fault_state": 2}, + "pv1": {"energy_exported": 40, "fault_state": 0}, + "pv2": {"energy_exported": 60, "fault_state": 2}, + }, + "counter": { + "counter0": { + "grid": True, + "energy_imported": 100, + "energy_exported": 0, + "fault_state": 0, + } + }, + } + + result, message = analyse_percentage(entry) + + assert result["energy_source"] == { + "grid": 0.5263, + "pv": 0.2105, + "bat": 0.1053, + "cp": 0.1579, + } + assert message == ( + "Der Strom-Mix um 00:31 wird trotz Fehlerzustand mindestens eines Speichers aus den vorhandenen Messwerten " + "berechnet. \n" + "Der Strom-Mix um 00:31 wird trotz Fehlerzustand mindestens eines Ladepunkts aus den vorhandenen Messwerten " + "berechnet. \n" + "Der Strom-Mix um 00:31 wird trotz Fehlerzustand mindestens eines Wechselrichters aus den vorhandenen " + "Messwerten berechnet. \n" + ) + + @pytest.mark.parametrize("test_case, entry_data, expected_energy_source, should_be_unchanged", [ ( "zero_consumption",