From 43a791333f50ee554d9b73069f9acbcc80a0a87d Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Wed, 22 Oct 2025 07:34:23 +0200 Subject: [PATCH 1/2] calc charge cost by used energy source in 5 min intervall --- packages/conftest.py | 1 + packages/control/chargelog/chargelog.py | 307 ++++++------------ packages/control/chargelog/chargelog_test.py | 238 +++++++------- .../control/chargelog/process_chargelog.py | 125 +++++++ .../control/chargepoint/chargepoint_data.py | 5 + packages/helpermodules/command.py | 4 +- .../measurement_logging/write_log.py | 3 +- packages/main.py | 6 +- 8 files changed, 357 insertions(+), 332 deletions(-) create mode 100644 packages/control/chargelog/process_chargelog.py diff --git a/packages/conftest.py b/packages/conftest.py index abfa8dc5ea..959c842090 100644 --- a/packages/conftest.py +++ b/packages/conftest.py @@ -33,6 +33,7 @@ def mock_today(monkeypatch) -> None: datetime_mock = MagicMock(wraps=datetime.datetime) # Montag 16.05.2022, 8:40:52 "05/16/2022, 08:40:52" Unix: 1652683252 datetime_mock.today.return_value = datetime.datetime(2022, 5, 16, 8, 40, 52) + datetime_mock.now.return_value = datetime.datetime(2022, 5, 16, 8, 40, 52) monkeypatch.setattr(datetime, "datetime", datetime_mock) mock_today_timestamp = Mock(return_value=1652683252) monkeypatch.setattr(timecheck, "create_timestamp", mock_today_timestamp) diff --git a/packages/control/chargelog/chargelog.py b/packages/control/chargelog/chargelog.py index b974b3ea27..c09a95a2f3 100644 --- a/packages/control/chargelog/chargelog.py +++ b/packages/control/chargelog/chargelog.py @@ -1,16 +1,17 @@ +import copy import datetime from enum import Enum import json import logging import os import pathlib -from typing import Any, Dict, List, Optional +from typing import Any, Dict, List, Optional, Tuple from control import data from dataclass_utils import asdict -from helpermodules.measurement_logging.process_log import (CalculationType, analyse_percentage, - get_log_from_date_until_now, process_entry) -from helpermodules.measurement_logging.write_log import LegacySmartHomeLogData, LogType, create_entry +from helpermodules.measurement_logging.process_log import ( + FILE_ERRORS, CalculationType, _analyse_energy_source, + _process_entries, analyse_percentage, get_log_from_date_until_now, get_totals) from helpermodules.pub import Pub from helpermodules import timecheck from helpermodules.utils.json_file_handler import write_and_check @@ -218,7 +219,7 @@ def _create_entry(chargepoint, charging_ev, immediately: bool = True): # power calculation needs to be fixed if useful: # log_data.imported_since_mode_switch / (duration / 3600) power = get_value_or_default(lambda: round(log_data.imported_since_mode_switch / (time_charged / 3600), 2)) - calculate_charge_cost(chargepoint, True) + calc_energy_costs(chargepoint, True) energy_source = get_value_or_default(lambda: analyse_percentage(get_log_from_date_until_now( log_data.timestamp_mode_switch)["totals"])["energy_source"]) costs = round(log_data.costs, 2) @@ -235,7 +236,7 @@ def _create_entry(chargepoint, charging_ev, immediately: bool = True): "vehicle": { "id": get_value_or_default(lambda: log_data.ev), - "name": get_value_or_default(lambda: _get_ev_name(log_data.ev)), + "name": get_value_or_default(lambda: data.data.ev_data[f"ev{log_data.ev}"].data.name, ""), "chargemode": get_value_or_default(lambda: log_data.chargemode_log_entry), "prio": get_value_or_default(lambda: log_data.prio), "rfid": get_value_or_default(lambda: log_data.rfid), @@ -289,161 +290,47 @@ def write_new_entry(new_entry): log.debug(f"Neuer Ladelog-Eintrag: {new_entry}") -def _get_ev_name(ev: int) -> str: - try: - return data.data.ev_data[f"ev{ev}"].data.name - except Exception: - return "" - - -def get_log_data(request: Dict): - """ json-Objekt mit gefilterten Logdaten erstellen - - Parameter - --------- - request: dict - Infos zum Request: Monat, Jahr, Filter - """ - log_data = {"entries": [], "totals": {}} - try: - # Datei einlesen - filepath = str(_get_parent_file() / "data" / "charge_log" / - (str(request["year"]) + str(request["month"]) + ".json")) - try: - with open(filepath, "r", encoding="utf-8") as json_file: - charge_log = json.load(json_file) - except FileNotFoundError: - log.debug("Kein Ladelog für %s gefunden!" % (str(request))) - return log_data - # Liste mit gefilterten Einträgen erstellen - for entry in charge_log: - if len(entry) > 0: - if ( - "id" in request["filter"]["chargepoint"] and - len(request["filter"]["chargepoint"]["id"]) > 0 and - entry["chargepoint"]["id"] not in request["filter"]["chargepoint"]["id"] - ): - log.debug( - "Verwerfe Eintrag wegen Ladepunkt ID: %s != %s" % - (str(entry["chargepoint"]["id"]), str(request["filter"]["chargepoint"]["id"])) - ) - continue - if ( - "id" in request["filter"]["vehicle"] and - len(request["filter"]["vehicle"]["id"]) > 0 and - entry["vehicle"]["id"] not in request["filter"]["vehicle"]["id"] - ): - log.debug( - "Verwerfe Eintrag wegen Fahrzeug ID: %s != %s" % - (str(entry["vehicle"]["id"]), str(request["filter"]["vehicle"]["id"])) - ) - continue - if ( - "tag" in request["filter"]["vehicle"] and - len(request["filter"]["vehicle"]["tag"]) > 0 and - entry["vehicle"]["rfid"] not in request["filter"]["vehicle"]["tag"] - ): - log.debug( - "Verwerfe Eintrag wegen ID Tag: %s != %s" % - (str(entry["vehicle"]["rfid"]), str(request["filter"]["vehicle"]["tag"])) - ) - continue - if ( - "chargemode" in request["filter"]["vehicle"] and - len(request["filter"]["vehicle"]["chargemode"]) > 0 and - entry["vehicle"]["chargemode"] not in request["filter"]["vehicle"]["chargemode"] - ): - log.debug( - "Verwerfe Eintrag wegen Lademodus: %s != %s" % - (str(entry["vehicle"]["chargemode"]), str(request["filter"]["vehicle"]["chargemode"])) - ) - continue - if ( - "prio" in request["filter"]["vehicle"] and - request["filter"]["vehicle"]["prio"] is not entry["vehicle"]["prio"] - ): - log.debug( - "Verwerfe Eintrag wegen Priorität: %s != %s" % - (str(entry["vehicle"]["prio"]), str(request["filter"]["vehicle"]["prio"])) - ) - continue - - # wenn wir hier ankommen, passt der Eintrag zum Filter - log_data["entries"].append(entry) - log_data["totals"] = get_totals_of_filtered_log_data(log_data) - except Exception: - log.exception("Fehler im Ladelog-Modul") - return log_data - - -def get_totals_of_filtered_log_data(log_data: Dict) -> Dict: - def get_sum(entry_name: str) -> float: - sum = 0 - try: - for entry in log_data["entries"]: - sum += entry["data"][entry_name] - return sum - except Exception: - return None - if len(log_data["entries"]) > 0: - # Summen bilden - duration_sum = "00:00" - try: - for entry in log_data["entries"]: - duration_sum = timecheck.duration_sum( - duration_sum, entry["time"]["time_charged"]) - except Exception: - duration_sum = None - range_charged_sum = get_sum("range_charged") - mode_sum = get_sum("imported_since_mode_switch") - power_sum = get_sum("power") - costs_sum = get_sum("costs") - power_sum = power_sum / len(log_data["entries"]) - return { - "time_charged": duration_sum, - "range_charged": range_charged_sum, - "imported_since_mode_switch": mode_sum, - "power": power_sum, - "costs": costs_sum, - } +def calc_energy_costs(cp, create_log_entry: bool = False): + 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() + 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) + log.debug(f"charged_energy_by_source {charged_energy_by_source} " + f"total charged_energy_by_source {cp.data.set.log.charged_energy_by_source}") + costs = _calc_costs(charged_energy_by_source, reference_entries[-1]["prices"]) + cp.data.set.log.costs += costs + Pub().pub(f"openWB/set/chargepoint/{cp.num}/set/log", asdict(cp.data.set.log)) -def calculate_charge_cost(cp, create_log_entry: bool = False): - content = get_todays_daily_log() +def calculate_charged_energy_by_source(cp, processed_entries, reference_entries, 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: - reference = _get_reference_position(cp, create_log_entry) - reference_time = get_reference_time(cp, reference) - reference_entry = _get_reference_entry(content["entries"], reference_time) - energy_entry = process_entry(reference_entry, - create_entry(LogType.DAILY, LegacySmartHomeLogData(), reference_entry), - CalculationType.ENERGY) - energy_source_entry = analyse_percentage(energy_entry) - log.debug(f"reference {reference}, reference_time {reference_time}, " - f"cp.data.set.log.imported_since_mode_switch {cp.data.set.log.imported_since_mode_switch}, " - f"cp.data.set.log.timestamp_start_charging {cp.data.set.log.timestamp_start_charging}") - log.debug(f"energy_source_entry {energy_source_entry}") - if reference == ReferenceTime.START: + reference = _get_reference_position(cp, create_log_entry) + absolut_energy_source = processed_entries["totals"]["cp"][f"cp{cp.num}"] + relative_energy_source = get_relative_energy_source(absolut_energy_source) + log.debug(f"reference {reference}, " + f"cp.data.set.log.imported_since_mode_switch {cp.data.set.log.imported_since_mode_switch}, " + f"cp.data.set.log.timestamp_start_charging {cp.data.set.log.timestamp_start_charging}") + log.debug(f"energy_source_entry {relative_energy_source}") + if reference == ReferenceTime.START: + charged_energy = cp.data.set.log.imported_since_mode_switch + elif reference == ReferenceTime.MIDDLE: + charged_energy = (reference_entries[-1]["cp"][f"cp{cp.num}"]["imported"] - + reference_entries[0]["cp"][f"cp{cp.num}"]["imported"]) + elif reference == ReferenceTime.END: + if (timecheck.create_timestamp() - cp.data.set.log.timestamp_start_charging) < 300: charged_energy = cp.data.set.log.imported_since_mode_switch - elif reference == ReferenceTime.MIDDLE: - charged_energy = (content["entries"][-1]["cp"][f"cp{cp.num}"]["imported"] - - energy_source_entry["cp"][f"cp{cp.num}"]["imported"]) - elif reference == ReferenceTime.END: - # timestamp_before_full_hour, dann gibt es schon ein Zwischenergebnis - if timecheck.create_unix_timestamp_current_full_hour() <= cp.data.set.log.timestamp_start_charging: - 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 - \ - energy_entry["cp"][f"cp{cp.num}"]["imported"] else: - raise TypeError(f"Unbekannter Referenz-Zeitpunkt {reference}") - log.debug(f'power source {energy_source_entry["energy_source"]}') - log.debug(f"charged_energy {charged_energy}") - costs = _calc(energy_source_entry["energy_source"], charged_energy) - cp.data.set.log.costs += costs - log.debug(f"current costs {costs}, total costs {cp.data.set.log.costs}") - Pub().pub(f"openWB/set/chargepoint/{cp.num}/set/log", asdict(cp.data.set.log)) + 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"] + else: + raise TypeError(f"Unbekannter Referenz-Zeitpunkt {reference}") + log.debug(f'power source {relative_energy_source}') + log.debug(f"charged_energy {charged_energy}") + return _get_charged_energy_by_source( + relative_energy_source, charged_energy) + except Exception: log.exception(f"Fehler beim Berechnen der Ladekosten für Ladepunkt {cp.num}") @@ -454,53 +341,41 @@ class ReferenceTime(Enum): END = 2 +ENERGY_SOURCES = ("bat", "cp", "grid", "pv") + + def _get_reference_position(cp, create_log_entry: bool) -> ReferenceTime: - # Referenz-Zeitpunkt ermitteln (angesteckt oder letzte volle Stunde) - # Wurde innerhalb der letzten Stunde angesteckt? if create_log_entry: - # Ladekosten für angefangene Stunde ermitteln + # Ladekosten in einem angebrochenen 5 Min Intervall ermitteln return ReferenceTime.END else: - # Wenn der Ladevorgang erst innerhalb der letzten Stunde gestartet wurde, ist das das erste Zwischenergebnis. - one_hour_back = timecheck.create_timestamp() - 3600 - if (one_hour_back - cp.data.set.log.timestamp_start_charging) < 0: + # Wenn der Ladevorgang erst innerhalb des letzten 5 Min Intervalls gestartet wurde, + # ist das das erste Zwischenergebnis. + if (timecheck.create_timestamp() - cp.data.set.log.timestamp_start_charging) < 300: return ReferenceTime.START else: return ReferenceTime.MIDDLE -def get_reference_time(cp, reference_position): - if reference_position == ReferenceTime.START: - return cp.data.set.log.timestamp_start_charging - elif reference_position == ReferenceTime.MIDDLE: - return timecheck.create_timestamp() - 3540 - elif reference_position == ReferenceTime.END: - # Wenn der Ladevorgang erst innerhalb der letzten Stunde gestartet wurde. - if timecheck.create_unix_timestamp_current_full_hour() <= cp.data.set.log.timestamp_start_charging: - return cp.data.set.log.timestamp_start_charging - else: - return timecheck.create_unix_timestamp_current_full_hour() + 60 - else: - raise TypeError(f"Unbekannter Referenz-Zeitpunkt {reference_position}") - - -def _get_reference_entry(entries: List[Dict], reference_time: float) -> Dict: - for entry in reversed(entries): - if entry["timestamp"] <= reference_time: - return entry - else: - # Tagesumbruch - content = _get_yesterdays_daily_log() - if content: - for entry in reversed(content["entries"]): - if entry["timestamp"] < reference_time: - return entry +def _get_reference_entries() -> Tuple[List[Dict], List]: + processed_entries = {} + reference_entries = [] + try: + entries = get_todays_daily_log()["entries"] + if len(entries) >= 2: + reference_entries = [entries[-2], entries[-1]] else: - return {} - - -def _get_yesterdays_daily_log(): - return get_daily_log((datetime.datetime.today()-datetime.timedelta(days=1)).strftime("%Y%m%d")) + date_day_before = (datetime.datetime.now() + datetime.timedelta(days=-1)).strftime("%Y%m%d") + entries_day_before = get_daily_log(date_day_before)["entries"] + reference_entries = [entries_day_before[-1], entries[0]] + processed_entries["entries"] = copy.deepcopy(reference_entries) + processed_entries["entries"] = _process_entries(processed_entries["entries"], CalculationType.ENERGY) + processed_entries["totals"] = get_totals(processed_entries["entries"], False) + processed_entries = _analyse_energy_source(processed_entries) + except Exception: + log.exception("Fehler beim Zusammenstellen der zwei letzten Logeinträge") + finally: + return processed_entries, reference_entries def get_todays_daily_log(): @@ -512,26 +387,48 @@ def get_daily_log(day): try: with open(filepath, "r", encoding="utf-8") as json_file: return json.load(json_file) - except FileNotFoundError: + except FILE_ERRORS: return [] -def _calc(energy_source: Dict[str, float], charged_energy_last_hour: float) -> float: - prices = data.data.general_data.data.prices +def _calc_costs(charged_energy_by_source: Dict[str, float], costs: Dict[str, float]) -> float: - bat_costs = prices.bat * charged_energy_last_hour * energy_source["bat"] - cp_costs = prices.cp * charged_energy_last_hour * energy_source["cp"] - try: - grid_costs = data.data.optional_data.et_get_current_price() * charged_energy_last_hour * energy_source["grid"] - except Exception: - grid_costs = prices.grid * charged_energy_last_hour * energy_source["grid"] - pv_costs = prices.pv * charged_energy_last_hour * energy_source["pv"] + bat_costs = costs["bat"] * charged_energy_by_source["bat"] + cp_costs = costs["cp"] * charged_energy_by_source["cp"] + grid_costs = costs["grid"] * charged_energy_by_source["grid"] + pv_costs = costs["pv"] * charged_energy_by_source["pv"] log.debug( - f'Ladepreis für die letzte Stunde: {bat_costs}€ Speicher ({energy_source["bat"]}%), {grid_costs}€ Netz ' - f'({energy_source["grid"]}%), {pv_costs}€ Pv ({energy_source["pv"]}%)') + f'Ladepreis nach Energiequelle: {bat_costs}€ Speicher ({charged_energy_by_source["bat"]/1000}kWh), ' + f'{grid_costs}€ Netz ({charged_energy_by_source["grid"]/1000}kWh),' + f' {pv_costs}€ Pv ({charged_energy_by_source["pv"]/1000}kWh), ' + f'{cp_costs}€ Ladepunkte ({charged_energy_by_source["cp"]/1000}kWh)') return round(bat_costs + cp_costs + grid_costs + pv_costs, 4) +def _get_charged_energy_by_source(energy_source, charged_energy) -> Dict[str, float]: + charged_energy_by_source = {} + for source in ENERGY_SOURCES: + charged_energy_by_source[source] = energy_source[source] * charged_energy + return charged_energy_by_source + + +def _add_charged_energy_by_source(cp, charged_energy_by_source): + for source in ENERGY_SOURCES: + cp.data.set.log.charged_energy_by_source[source] += charged_energy_by_source[source] + + +def get_relative_energy_source(absolut_energy_source: Dict[str, float]) -> Dict[str, float]: + if absolut_energy_source["energy_imported"] == 0: + return {source: 0 for source in ENERGY_SOURCES} + else: + relative_energy_source = {} + for source in ENERGY_SOURCES: + for absolute_source, value in absolut_energy_source.items(): + if source in absolute_source: + relative_energy_source[source] = value / absolut_energy_source["energy_imported"] + return relative_energy_source + + def _get_parent_file() -> pathlib.Path: return pathlib.Path(__file__).resolve().parents[3] diff --git a/packages/control/chargelog/chargelog_test.py b/packages/control/chargelog/chargelog_test.py index 1ab700af43..e6cf39d715 100644 --- a/packages/control/chargelog/chargelog_test.py +++ b/packages/control/chargelog/chargelog_test.py @@ -1,46 +1,13 @@ -import datetime -from unittest.mock import Mock +import json +from unittest.mock import Mock, mock_open, patch import pytest from control import data from control.chargelog import chargelog -from control.chargelog.chargelog import calculate_charge_cost +from control.chargelog.chargelog import calc_energy_costs from control.chargepoint.chargepoint import Chargepoint -from helpermodules import timecheck -from test_utils.test_environment import running_on_github - - -def mock_daily_log_with_charging(date: str, num_of_intervalls, monkeypatch): - """erzeugt ein daily_log, im ersten Eintrag gibt es keine Änderung, danach wird bis inklusive dem letzten Beitrag - geladen""" - bat_exported = pv_exported = cp_imported = counter_imported = 2000 - date = datetime.datetime.strptime(date, "%m/%d/%Y, %H:%M") - daily_log = {"entries": []} - for i in range(0, num_of_intervalls): - if i != 0: - bat_exported += 1000 - pv_exported += 500 - cp_imported += 2000 - counter_imported += 500 - daily_log["entries"].append({'bat': {'all': {'exported': bat_exported, 'imported': 2000, 'soc': 100}, - 'bat2': {'exported': bat_exported, 'imported': 2000, 'soc': 100}}, - 'counter': {'counter0': {'exported': 2000, - 'grid': True, - 'imported': counter_imported}}, - 'cp': {'all': {'exported': 0, 'imported': cp_imported}, - 'cp4': {'exported': 0, 'imported': cp_imported}}, - 'date': date.strftime("%H:%M"), - 'ev': {'ev0': {'soc': None}}, - 'hc': {'all': {'imported': 0}}, - 'pv': {'all': {'exported': pv_exported}, 'pv1': {'exported': pv_exported}}, - 'sh': {}, - 'timestamp': date.timestamp()}) - date += datetime.timedelta(minutes=5) - mock_todays_daily_log = Mock(return_value=daily_log) - monkeypatch.setattr(chargelog, "get_todays_daily_log", mock_todays_daily_log) - return daily_log @pytest.fixture() @@ -49,106 +16,133 @@ def mock_data() -> None: data.data.optional_data.et_module = None -def mock_create_entry_reference_end(clock, daily_log, monkeypatch): - current_log = daily_log["entries"][-1] - current_log["cp"]["all"]["imported"] += 500 - current_log["cp"]["cp4"]["imported"] += 500 - current_log["counter"]["counter0"]["imported"] += 500 - current_log["date"] = clock - current_log["timestamp"] = datetime.datetime.strptime(f"05/16/2022, {clock}", "%m/%d/%Y, %H:%M").timestamp() - mock_create_entry = Mock(return_value=current_log) - monkeypatch.setattr(chargelog, "create_entry", mock_create_entry) +def mock_daily_log(monkeypatch): + daily_log = {"entries": [{'bat': {'all': {'exported': 2000, 'imported': 2000, 'soc': 100}, + 'bat2': {'exported': 2000, 'imported': 2000, 'soc': 100}}, + 'counter': {'counter0': {'exported': 2000, + 'grid': True, + 'imported': 500}}, + 'cp': {'all': {'exported': 0, 'imported': 2000}, + 'cp4': {'exported': 0, 'imported': 2000}}, + 'date': "8:35", + 'ev': {'ev0': {'soc': None}}, + 'hc': {'all': {'imported': 0}}, + 'pv': {'all': {'exported': 2000}, 'pv1': {'exported': 2000}}, + 'sh': {}, + 'timestamp': 1652682900, + 'prices': {'grid': 0.0003, 'pv': 0.00015, 'bat': 0.0002, 'cp': 0}}, + {'bat': {'all': {'exported': 3000, 'imported': 2000, 'soc': 100}, + 'bat2': {'exported': 3000, 'imported': 2000, 'soc': 100}}, + 'counter': {'counter0': {'exported': 2000, + 'grid': True, + 'imported': 2500}}, + 'cp': {'all': {'exported': 0, 'imported': 4000}, + 'cp4': {'exported': 0, 'imported': 4000}}, + 'date': "8:40", + 'ev': {'ev0': {'soc': None}}, + 'hc': {'all': {'imported': 0}}, + 'pv': {'all': {'exported': 2500}, 'pv1': {'exported': 2500}}, + 'sh': {}, + 'timestamp': 1652683200, + 'prices': {'grid': 0.0003, 'pv': 0.00015, 'bat': 0.0002, 'cp': 0}}]} + mock_todays_daily_log = Mock(return_value=daily_log) + monkeypatch.setattr(chargelog, "get_todays_daily_log", mock_todays_daily_log) + return daily_log -def init_cp(charged_energy, costs, start_hour, start_minute=47): +def test_calc_charge_cost_reference_middle(mock_data, monkeypatch): cp = Chargepoint(4, None) - cp.data.set.log.imported_since_plugged = cp.data.set.log.imported_since_mode_switch = charged_energy - cp.data.set.log.timestamp_start_charging = datetime.datetime(2022, 5, 16, start_hour, start_minute).timestamp() - cp.data.get.imported = charged_energy + 2000 - cp.data.set.log.costs = costs - return cp - - -def test_calc_charge_cost_no_hour_change_reference_end(mock_data, monkeypatch): - cp = init_cp(6500, 0, 10, start_minute=27) - daily_log = mock_daily_log_with_charging("05/16/2022, 10:25", 4, monkeypatch) - mock_create_entry_reference_end("10:42", daily_log, monkeypatch) - - calculate_charge_cost(cp, True) - - assert cp.data.set.log.costs == 1.425 - + cp.data.set.log.imported_since_plugged = cp.data.set.log.imported_since_mode_switch = 3950 + cp.data.set.log.timestamp_start_charging = 1652682600 # 8:30 + cp.data.get.imported = 4050 + cp.data.set.log.charged_energy_by_source = {'bat': 100, 'cp': 0, 'grid': 100, 'pv': 100} + daily_log = mock_daily_log(monkeypatch) -def test_calc_charge_cost_first_hour_change_reference_begin(mock_data, monkeypatch): - cp = init_cp(6000, 0, 7) - daily_log = mock_daily_log_with_charging("05/16/2022, 07:45", 4, monkeypatch) - current_log = daily_log["entries"][-1] - current_log["date"] = "08:00" - current_log["timestamp"] = datetime.datetime.strptime("05/16/2022, 08:00", "%m/%d/%Y, %H:%M").timestamp() - mock_create_entry = Mock(return_value=current_log) - monkeypatch.setattr(chargelog, "create_entry", mock_create_entry) + with patch("builtins.open", mock_open(read_data=json.dumps(daily_log))): + calc_energy_costs(cp) - calculate_charge_cost(cp, False) + assert cp.data.set.log.charged_energy_by_source == { + 'grid': 1243, 'pv': 386, 'bat': 671, 'cp': 0.0} + assert round(cp.data.set.log.costs, 5) == 0.5 - assert cp.data.set.log.costs == 1.275 +def test_calc_charge_cost_reference_start(mock_data, monkeypatch): + cp = Chargepoint(4, None) + cp.data.set.log.imported_since_plugged = cp.data.set.log.imported_since_mode_switch = 100 + cp.data.set.log.timestamp_start_charging = 1652683230 # 8:40:30 + cp.data.get.imported = 4100 + cp.data.set.log.charged_energy_by_source = {'bat': 0, 'cp': 0, 'grid': 0, 'pv': 0} + daily_log = mock_daily_log(monkeypatch) -def test_calc_charge_cost_first_hour_change_reference_begin_day_change(mock_data, monkeypatch): - cp = init_cp(6000, 0, 23) - daily_log = mock_daily_log_with_charging("05/16/2022, 23:45", 4, monkeypatch) - current_log = daily_log["entries"][-1] - current_log["date"] = "00:00" - current_log["timestamp"] = datetime.datetime.strptime("05/17/2022, 00:00", "%m/%d/%Y, %H:%M").timestamp() - mock_create_entry = Mock(return_value=current_log) - monkeypatch.setattr(chargelog, "create_entry", mock_create_entry) - mock_today_timestamp = Mock(return_value=1652738421) - monkeypatch.setattr(timecheck, "create_timestamp", mock_today_timestamp) - - calculate_charge_cost(cp, False) - - assert cp.data.set.log.costs == 1.275 - - -def test_calc_charge_cost_one_hour_change_reference_end(mock_data, monkeypatch): - if running_on_github(): - # ToDo Zeitzonen berücksichtigen, damit Tests auf Github laufen - return - cp = init_cp(22500, 1.275, 7) - daily_log = mock_daily_log_with_charging("05/16/2022, 07:45", 12, monkeypatch) - mock_create_entry_reference_end("08:40", daily_log, monkeypatch) - - calculate_charge_cost(cp, True) + with patch("builtins.open", mock_open(read_data=json.dumps(daily_log))): + calc_energy_costs(cp) - assert cp.data.set.log.costs == 4.8248999999999995 + assert cp.data.set.log.charged_energy_by_source == { + 'bat': 28.549999999999997, 'cp': 0.0, 'grid': 57.15, 'pv': 14.299999999999999} + assert round(cp.data.set.log.costs, 5) == 0.025 -def test_calc_charge_cost_two_hour_change_reference_middle(mock_data, monkeypatch): - if running_on_github(): - # ToDo Zeitzonen berücksichtigen, damit Tests auf Github laufen - return - cp = init_cp(22500, 1.275, 6) - daily_log = mock_daily_log_with_charging("05/16/2022, 06:45", 16, monkeypatch) - current_log = daily_log["entries"][-1] - current_log["date"] = "08:00" - current_log["timestamp"] = datetime.datetime(2022, 5, 16, 8).timestamp() - mock_create_entry = Mock(return_value=current_log) - monkeypatch.setattr(chargelog, "create_entry", mock_create_entry) - mock_today_timestamp = Mock(return_value=1652680801) - monkeypatch.setattr(timecheck, "create_timestamp", mock_today_timestamp) +def test_calc_charge_cost_reference_end(mock_data, monkeypatch): + cp = Chargepoint(4, None) + cp.data.set.log.imported_since_plugged = cp.data.set.log.imported_since_mode_switch = 3950 + cp.data.set.log.timestamp_start_charging = 1652682600 # 8:30 + 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) - calculate_charge_cost(cp, False) + with patch("builtins.open", mock_open(read_data=json.dumps(daily_log))): + calc_energy_costs(cp, True) - assert cp.data.set.log.costs == 6.375 + assert cp.data.set.log.charged_energy_by_source == {'bat': 699.55, 'cp': 0.0, 'grid': 1300.15, 'pv': 400.3} + assert round(cp.data.set.log.costs, 5) == 0.025 -def test_calc_charge_cost_two_hour_change_reference_end(mock_data, monkeypatch): - if running_on_github(): - # ToDo Zeitzonen berücksichtigen, damit Tests auf Github laufen - return - cp = init_cp(46500, 6.375, 6) - daily_log = mock_daily_log_with_charging("05/16/2022, 06:45", 24, monkeypatch) - mock_create_entry_reference_end("08:40", daily_log, monkeypatch) +def test_calc_charge_cost_reference_middle_day_change(mock_data, monkeypatch): + cp = Chargepoint(4, None) + cp.data.set.log.imported_since_plugged = cp.data.set.log.imported_since_mode_switch = 3950 + cp.data.set.log.timestamp_start_charging = 1652682600 # 8:30 + cp.data.get.imported = 4050 + cp.data.set.log.charged_energy_by_source = {'bat': 100, 'cp': 0, 'grid': 100, 'pv': 100} + yesterday_daily_log = {"entries": [{'bat': {'all': {'exported': 2000, 'imported': 2000, 'soc': 100}, + 'bat2': {'exported': 2000, 'imported': 2000, 'soc': 100}}, + 'counter': {'counter0': {'exported': 2000, + 'grid': True, + 'imported': 500}}, + 'cp': {'all': {'exported': 0, 'imported': 2000}, + 'cp4': {'exported': 0, 'imported': 2000}}, + 'date': "8:35", + 'ev': {'ev0': {'soc': None}}, + 'hc': {'all': {'imported': 0}}, + 'pv': {'all': {'exported': 2000}, 'pv1': {'exported': 2000}}, + 'sh': {}, + 'timestamp': 1652682900, + 'prices': {'grid': 0.0003, 'pv': 0.00015, 'bat': 0.0002, 'cp': 0}}]} + mock_yesterdays_daily_log = Mock(return_value=yesterday_daily_log) + monkeypatch.setattr(chargelog, "get_daily_log", mock_yesterdays_daily_log) + + daily_log = {"entries": [{'bat': {'all': {'exported': 3000, 'imported': 2000, 'soc': 100}, + 'bat2': {'exported': 3000, 'imported': 2000, 'soc': 100}}, + 'counter': {'counter0': {'exported': 2000, + 'grid': True, + 'imported': 2500}}, + 'cp': {'all': {'exported': 0, 'imported': 4000}, + 'cp4': {'exported': 0, 'imported': 4000}}, + 'date': "8:40", + 'ev': {'ev0': {'soc': None}}, + 'hc': {'all': {'imported': 0}}, + 'pv': {'all': {'exported': 2500}, 'pv1': {'exported': 2500}}, + 'sh': {}, + 'timestamp': 1652683200, + 'prices': {'grid': 0.0003, 'pv': 0.00015, 'bat': 0.0002, 'cp': 0}}]} + mock_todays_daily_log = Mock(return_value=daily_log) + monkeypatch.setattr(chargelog, "get_todays_daily_log", mock_todays_daily_log) - calculate_charge_cost(cp, True) + with patch("builtins.open", side_effect=[ + mock_open(read_data=json.dumps(daily_log)), + mock_open(read_data=json.dumps(yesterday_daily_log)) + ]): + calc_energy_costs(cp) - assert cp.data.set.log.costs == 9.924900000000001 + assert cp.data.set.log.charged_energy_by_source == { + 'grid': 1243, 'pv': 386, 'bat': 671, 'cp': 0.0} + assert round(cp.data.set.log.costs, 5) == 0.5 diff --git a/packages/control/chargelog/process_chargelog.py b/packages/control/chargelog/process_chargelog.py new file mode 100644 index 0000000000..9349db3e26 --- /dev/null +++ b/packages/control/chargelog/process_chargelog.py @@ -0,0 +1,125 @@ +import json +import logging +import pathlib +from typing import Dict + +from helpermodules import timecheck + + +log = logging.getLogger("chargelog") + + +def get_log_data(request: Dict): + """ json-Objekt mit gefilterten Logdaten erstellen + + Parameter + --------- + request: dict + Infos zum Request: Monat, Jahr, Filter + """ + log_data = {"entries": [], "totals": {}} + try: + # Datei einlesen + filepath = str(_get_parent_file() / "data" / "charge_log" / + (str(request["year"]) + str(request["month"]) + ".json")) + try: + with open(filepath, "r", encoding="utf-8") as json_file: + charge_log = json.load(json_file) + except FileNotFoundError: + log.debug("Kein Ladelog für %s gefunden!" % (str(request))) + return log_data + # Liste mit gefilterten Einträgen erstellen + for entry in charge_log: + if len(entry) > 0: + if ( + "id" in request["filter"]["chargepoint"] and + len(request["filter"]["chargepoint"]["id"]) > 0 and + entry["chargepoint"]["id"] not in request["filter"]["chargepoint"]["id"] + ): + log.debug( + "Verwerfe Eintrag wegen Ladepunkt ID: %s != %s" % + (str(entry["chargepoint"]["id"]), str(request["filter"]["chargepoint"]["id"])) + ) + continue + if ( + "id" in request["filter"]["vehicle"] and + len(request["filter"]["vehicle"]["id"]) > 0 and + entry["vehicle"]["id"] not in request["filter"]["vehicle"]["id"] + ): + log.debug( + "Verwerfe Eintrag wegen Fahrzeug ID: %s != %s" % + (str(entry["vehicle"]["id"]), str(request["filter"]["vehicle"]["id"])) + ) + continue + if ( + "tag" in request["filter"]["vehicle"] and + len(request["filter"]["vehicle"]["tag"]) > 0 and + entry["vehicle"]["rfid"] not in request["filter"]["vehicle"]["tag"] + ): + log.debug( + "Verwerfe Eintrag wegen ID Tag: %s != %s" % + (str(entry["vehicle"]["rfid"]), str(request["filter"]["vehicle"]["tag"])) + ) + continue + if ( + "chargemode" in request["filter"]["vehicle"] and + len(request["filter"]["vehicle"]["chargemode"]) > 0 and + entry["vehicle"]["chargemode"] not in request["filter"]["vehicle"]["chargemode"] + ): + log.debug( + "Verwerfe Eintrag wegen Lademodus: %s != %s" % + (str(entry["vehicle"]["chargemode"]), str(request["filter"]["vehicle"]["chargemode"])) + ) + continue + if ( + "prio" in request["filter"]["vehicle"] and + request["filter"]["vehicle"]["prio"] is not entry["vehicle"]["prio"] + ): + log.debug( + "Verwerfe Eintrag wegen Priorität: %s != %s" % + (str(entry["vehicle"]["prio"]), str(request["filter"]["vehicle"]["prio"])) + ) + continue + + # wenn wir hier ankommen, passt der Eintrag zum Filter + log_data["entries"].append(entry) + log_data["totals"] = get_totals_of_filtered_log_data(log_data) + except Exception: + log.exception("Fehler im Ladelog-Modul") + return log_data + + +def get_totals_of_filtered_log_data(log_data: Dict) -> Dict: + def get_sum(entry_name: str) -> float: + sum = 0 + try: + for entry in log_data["entries"]: + sum += entry["data"][entry_name] + return sum + except Exception: + return None + if len(log_data["entries"]) > 0: + # Summen bilden + duration_sum = "00:00" + try: + for entry in log_data["entries"]: + duration_sum = timecheck.duration_sum( + duration_sum, entry["time"]["time_charged"]) + except Exception: + duration_sum = None + range_charged_sum = get_sum("range_charged") + mode_sum = get_sum("imported_since_mode_switch") + power_sum = get_sum("power") + costs_sum = get_sum("costs") + power_sum = power_sum / len(log_data["entries"]) + return { + "time_charged": duration_sum, + "range_charged": range_charged_sum, + "imported_since_mode_switch": mode_sum, + "power": power_sum, + "costs": costs_sum, + } + + +def _get_parent_file() -> pathlib.Path: + return pathlib.Path(__file__).resolve().parents[3] diff --git a/packages/control/chargepoint/chargepoint_data.py b/packages/control/chargepoint/chargepoint_data.py index 1e9bcff377..cb4358f7d4 100644 --- a/packages/control/chargepoint/chargepoint_data.py +++ b/packages/control/chargepoint/chargepoint_data.py @@ -64,10 +64,15 @@ class ConnectedVehicle: soc: ConnectedSoc = field(default_factory=connected_soc_factory) +def empty_enery_source_dict_factory(): + return {'bat': 0, 'cp': 0, 'grid': 0, 'pv': 0} + + @dataclass class Log: begin: Optional[float] = None chargemode_log_entry: str = "_" + charged_energy_by_source: Dict[str, float] = field(default_factory=empty_enery_source_dict_factory) costs: float = 0 end: Optional[float] = None imported_at_mode_switch: float = 0 diff --git a/packages/helpermodules/command.py b/packages/helpermodules/command.py index 4f4eafd2f7..2d1b79d96c 100644 --- a/packages/helpermodules/command.py +++ b/packages/helpermodules/command.py @@ -12,7 +12,7 @@ from pathlib import Path import paho.mqtt.client as mqtt -from control.chargelog import chargelog +from control.chargelog.process_chargelog import get_log_data from control.chargepoint import chargepoint from control.chargepoint.chargepoint_template import get_chargepoint_template_default @@ -719,7 +719,7 @@ def sendDebug(self, connection_id: str, payload: dict) -> None: pub_user_message(payload, connection_id, "Systembericht wurde versandt.", MessageType.SUCCESS) def getChargeLog(self, connection_id: str, payload: dict) -> None: - Pub().pub(f'openWB/set/log/{connection_id}/data', chargelog.get_log_data(payload["data"])) + Pub().pub(f'openWB/set/log/{connection_id}/data', get_log_data(payload["data"])) def getDailyLog(self, connection_id: str, payload: dict) -> None: Pub().pub(f'openWB/set/log/daily/{payload["data"]["date"]}', diff --git a/packages/helpermodules/measurement_logging/write_log.py b/packages/helpermodules/measurement_logging/write_log.py index ef01486b26..36ccc2fc44 100644 --- a/packages/helpermodules/measurement_logging/write_log.py +++ b/packages/helpermodules/measurement_logging/write_log.py @@ -204,7 +204,8 @@ def create_entry(log_type: LogType, sh_log_data: LegacySmartHomeLogData, previou grid_price = prices.grid prices_dict = {"grid": grid_price, "pv": prices.pv, - "bat": prices.bat} + "bat": prices.bat, + "cp": prices.cp} except Exception: log.exception("Fehler im Werte-Logging-Modul für Preise") prices_dict = {} diff --git a/packages/main.py b/packages/main.py index 2fb8b4038b..0617b6894f 100755 --- a/packages/main.py +++ b/packages/main.py @@ -19,7 +19,7 @@ import time from threading import Event, Thread, enumerate import traceback -from control.chargelog.chargelog import calculate_charge_cost +from control.chargelog.chargelog import calc_energy_costs, calculate_charged_energy_by_source from control import data, prepare, process from control.algorithm import algorithm @@ -182,6 +182,8 @@ def handler5MinAlgorithm(self): totals = save_log(LogType.DAILY) update_daily_yields(totals) update_pv_monthly_yearly_yields() + for cp in data.data.cp_data.values(): + calc_energy_costs(cp) data.data.general_data.grid_protection() data.data.optional_data.ocpp_transfer_meter_values() data.data.counter_all_data.validate_hierarchy() @@ -250,7 +252,7 @@ def handler_hour(self): try: with ChangedValuesContext(loadvars_.event_module_update_completed): for cp in data.data.cp_data.values(): - calculate_charge_cost(cp) + calculate_charged_energy_by_source(cp) data.data.optional_data.et_get_prices() logger.clear_in_memory_log_handler(None) except Exception: From 86432ed56d95852bfed8584938b3fed1734fd541 Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Wed, 29 Oct 2025 10:19:59 +0100 Subject: [PATCH 2/2] review --- packages/control/chargelog/chargelog.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/packages/control/chargelog/chargelog.py b/packages/control/chargelog/chargelog.py index c09a95a2f3..83b2d49445 100644 --- a/packages/control/chargelog/chargelog.py +++ b/packages/control/chargelog/chargelog.py @@ -61,6 +61,8 @@ # } log = logging.getLogger("chargelog") +MEASUREMENT_LOGGING_INTERVAL = 300 # in Sekunden + def collect_data(chargepoint): """ @@ -318,7 +320,7 @@ def calculate_charged_energy_by_source(cp, processed_entries, reference_entries, charged_energy = (reference_entries[-1]["cp"][f"cp{cp.num}"]["imported"] - reference_entries[0]["cp"][f"cp{cp.num}"]["imported"]) elif reference == ReferenceTime.END: - if (timecheck.create_timestamp() - cp.data.set.log.timestamp_start_charging) < 300: + if (timecheck.create_timestamp()-cp.data.set.log.timestamp_start_charging) < MEASUREMENT_LOGGING_INTERVAL: charged_energy = cp.data.set.log.imported_since_mode_switch else: log.debug(f"cp.data.get.imported {cp.data.get.imported}") @@ -351,7 +353,7 @@ def _get_reference_position(cp, create_log_entry: bool) -> ReferenceTime: else: # Wenn der Ladevorgang erst innerhalb des letzten 5 Min Intervalls gestartet wurde, # ist das das erste Zwischenergebnis. - if (timecheck.create_timestamp() - cp.data.set.log.timestamp_start_charging) < 300: + if (timecheck.create_timestamp() - cp.data.set.log.timestamp_start_charging) < MEASUREMENT_LOGGING_INTERVAL: return ReferenceTime.START else: return ReferenceTime.MIDDLE