From a76460e35c1491026afd48faf2a6b76c302a6531 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Wed, 19 Aug 2026 09:54:54 +0200 Subject: [PATCH 01/17] Improve Monatsauswertung mit Tages-Summen --- .../measurement_logging/process_log.py | 121 +++++++++++++++++- packages/main.py | 3 + 2 files changed, 122 insertions(+), 2 deletions(-) diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index 24eab8feff..2806b2a8b0 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -264,6 +264,55 @@ def _collect_daily_log_data(date: str): def get_monthly_log(date: str): data = _collect_monthly_log_data(date) + entries_with_daily_totals = [] + + for entry in data["entries"]: + day = entry["date"] + if day == timecheck.create_timestamp_YYYYMMDD(): + # Überspringe den aktuellen Tag + daily_totals = None + else: + daily_totals = load_daily_source_totals(day) + + if daily_totals is not None: + _apply_daily_source_totals(entry, daily_totals) + entries_with_daily_totals.append(entry) + else: + # Keine Datei vorhanden -> erzeuge neue Datei + if day == timecheck.create_timestamp_YYYYMMDD(): + # beim aktuellen Tag generiere die Tages-Summen, aber speichere diese nicht ab + content = save_daily_source_totals(day, saveing=False) + new_gendaily_totals = content.get("totals", None) + log.debug(f"Totals-Werte vom aktuellen Tag {new_gendaily_totals}") + else: + # bei älteren Tagen generiere die Tages-Summen und speichere diese ab + # Fallback für ältere Tage, falls die Tages-Summen nicht vorhanden sind + content = save_daily_source_totals(day, saveing=True) + new_gendaily_totals = content.get("totals", None) + + if new_gendaily_totals is not None: + _apply_daily_source_totals(entry, new_gendaily_totals) + entries_with_daily_totals.append(entry) + else: + log.debug(f"Keine Tages-Summen fuer {day} gefunden und konnten auch nicht neu erzeugt werden.") + # Abbruch und Fallback auf alte Berechnung + entries_with_daily_totals = [] + break + + if len(entries_with_daily_totals) > 0: + + data["entries"] = entries_with_daily_totals + + if len(data["entries"]) > 0: + # Wurde im alten System in _process_entries gemacht... + # Entfernt den Eintrag des Folgemonats + data["entries"].pop() + pass + data["totals"] = get_totals(data["entries"], False) + data["totals"] = analyse_percentage_totals(data["entries"], data["totals"]) + return data + + # Fallback: alter Rechenweg, falls keine Tages-Summen verfuegbar sind. data["entries"] = _process_entries(data["entries"], CalculationType.ENERGY) data["totals"] = get_totals(data["entries"], False) data = _analyse_energy_source(data) @@ -282,10 +331,13 @@ def _collect_monthly_log_data(date: str): with open(f"{_get_data_folder_path()}/daily_log/{today}.json", "r") as todayJsonFile: today_log_data = json.load(todayJsonFile) - if len(today_log_data["entries"]) > 0: - log_data["entries"].append(today_log_data["entries"][-1]) + if len(today_log_data.get("entries", [])) > 0: + today_entry = today_log_data["entries"][-1] + today_entry["date"] = today + log_data["entries"].append(today_entry) except FILE_ERRORS: pass + else: # add first entry of next month try: @@ -665,3 +717,68 @@ def _calculate_average_power(time_diff: float, current_imported: float = 0, next def _get_data_folder_path() -> str: return str(Path(__file__).resolve().parents[3] / "data") + + +def calculate_daily_source_totals(date: str): + data = _collect_daily_log_data(date) + data["entries"] = _process_entries(data["entries"], calculation=CalculationType.ENERGY) + data["totals"] = get_totals(data["entries"], process_entries=False) + data = _analyse_energy_source(data) + + return data["totals"] + + +# Das wird im midnight-handler aufgrufen +# -> erzeugt für jede Tag eine Totals datei +def save_daily_source_totals(date: str, saveing: bool = True): + try: + totals = calculate_daily_source_totals(date) + # Erzeugt Ordner daily_totals, falls nicht vorhanden + totals_dir = Path(_get_data_folder_path()) / "daily_totals" + filepath = totals_dir / f"{date}_totals.json" + + content = {"date": date, "totals": totals} + + if saveing: + totals_dir.mkdir(parents=True, exist_ok=True) + with open(str(filepath), "w") as jsonFile: + json.dump(content, jsonFile, ensure_ascii=False, indent=2) + + log.debug(f"Tages-Summen für {date} gespeichert in {filepath}") + return content + + except FILE_ERRORS: + log.exception(f"Fehler beim Speichern der Tages-Summen für {date}") + + +def load_daily_source_totals(date: str): + try: + filepath = f"{_get_data_folder_path()}/daily_totals/{date}_totals.json" + if not Path(filepath).is_file(): + log.debug(f"Keine Tages-Summen-Datei gefunden: {filepath}") + return None + + with open(str(filepath), "r") as jsonFile: + content = json.load(jsonFile) + + log.debug(f"Tages-Summen für {date} geladen aus {filepath}") + return content["totals"] + + except FILE_ERRORS: + log.exception(f"Fehler beim Laden der Tages-Summen für {date}") + + +def _apply_daily_source_totals(entry: Dict, daily_totals: Dict): + for section, section_totals in daily_totals.items(): + section_data = entry.get(section) + if not isinstance(section_data, dict) or not isinstance(section_totals, dict): + continue + + for module, module_totals in section_totals.items(): + module_data = section_data.get(module) + if not isinstance(module_data, dict) or not isinstance(module_totals, dict): + continue + + # Alle vorhandenen Summenfelder des Moduls mit den Tages-Summen ueberschreiben. + module_data.update(module_totals) + return entry diff --git a/packages/main.py b/packages/main.py index 3385139ba9..dbe409275f 100755 --- a/packages/main.py +++ b/packages/main.py @@ -37,6 +37,7 @@ from modules.utils import wait_for_module_update_completed from smarthome.smarthome import readmq, smarthome_handler +from helpermodules.measurement_logging.process_log import save_daily_source_totals class HandlerAlgorithm: def __init__(self): @@ -231,6 +232,8 @@ def handler5Min(self): def handler_midnight(self): try: save_log(LogType.MONTHLY) + previous_day = timecheck.get_relative_date_string(timecheck.create_timestamp_YYYYMMDD(), day_offset=-1) + save_daily_source_totals(previous_day) thread_errors_path = Path(Path(__file__).resolve().parents[1]/"ramdisk"/"thread_errors.log") with thread_errors_path.open("w") as f: f.write("") From aef9cc58da14670e4bbc5c554978f4164ee5910a Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Wed, 19 Aug 2026 10:39:07 +0200 Subject: [PATCH 02/17] fix error --- .../measurement_logging/process_log.py | 21 +++++++++++++++---- 1 file changed, 17 insertions(+), 4 deletions(-) diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index 2806b2a8b0..c0ee91f722 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -649,13 +649,26 @@ def process_entry(entry: dict, next_entry: dict, calculation: CalculationType): new_data = {} if "imported" in entry[type][module].keys() or "exported" in entry[type][module].keys(): def get_current_and_next(value_key: str) -> Tuple[float, float]: - def get_single_value(source: dict, default: int = 0) -> float: + def get_single_value(source: dict) -> Optional[float]: try: - return source[type][module][value_key] + value = source[type][module][value_key] + if isinstance(value, (int, float)): + return float(value) except KeyError: - return default + pass + return None + current_value = get_single_value(entry) - return current_value, get_single_value(next_entry, current_value) + next_value = get_single_value(next_entry) + + # Keep meter deltas neutral if one side is invalid/missing. + if current_value is None and next_value is None: + return 0.0, 0.0 + if current_value is None: + return next_value, next_value + if next_value is None: + return current_value, current_value + return current_value, next_value value_imported, next_value_imported = get_current_and_next("imported") value_exported, next_value_exported = get_current_and_next("exported") if calculation in [CalculationType.POWER, CalculationType.ALL]: From 8e60fffe6a976d0e6cc565b2d6c6418f824cd94a Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Thu, 20 Aug 2026 10:51:16 +0200 Subject: [PATCH 03/17] Improve Jahresauswertung mit Monats-Summen --- .../measurement_logging/process_log.py | 369 ++++++++++-------- packages/main.py | 11 +- 2 files changed, 212 insertions(+), 168 deletions(-) diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index c0ee91f722..b4fb1ca1da 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -1,4 +1,5 @@ from enum import Enum +from copy import deepcopy import json import logging from pathlib import Path @@ -263,174 +264,123 @@ def _collect_daily_log_data(date: str): def get_monthly_log(date: str): - data = _collect_monthly_log_data(date) - entries_with_daily_totals = [] + # Nur Logs ab dem ältesten Tageslog auswerten + # Sonst werden unötige totals Werte gespeichert + oldest_log_day = _oldest_log_day() + if (oldest_log_day is None + or date < oldest_log_day[:6]): # Jahr und Monat + return {"entries": [], "names": {}, "colors": {}, "totals": {}} - for entry in data["entries"]: - day = entry["date"] - if day == timecheck.create_timestamp_YYYYMMDD(): - # Überspringe den aktuellen Tag - daily_totals = None - else: - daily_totals = load_daily_source_totals(day) + monthly_entries = [] + monthly_names = {} + monthly_colors = {} - if daily_totals is not None: - _apply_daily_source_totals(entry, daily_totals) - entries_with_daily_totals.append(entry) - else: - # Keine Datei vorhanden -> erzeuge neue Datei - if day == timecheck.create_timestamp_YYYYMMDD(): - # beim aktuellen Tag generiere die Tages-Summen, aber speichere diese nicht ab - content = save_daily_source_totals(day, saveing=False) - new_gendaily_totals = content.get("totals", None) - log.debug(f"Totals-Werte vom aktuellen Tag {new_gendaily_totals}") - else: - # bei älteren Tagen generiere die Tages-Summen und speichere diese ab - # Fallback für ältere Tage, falls die Tages-Summen nicht vorhanden sind - content = save_daily_source_totals(day, saveing=True) - new_gendaily_totals = content.get("totals", None) - - if new_gendaily_totals is not None: - _apply_daily_source_totals(entry, new_gendaily_totals) - entries_with_daily_totals.append(entry) - else: - log.debug(f"Keine Tages-Summen fuer {day} gefunden und konnten auch nicht neu erzeugt werden.") - # Abbruch und Fallback auf alte Berechnung - entries_with_daily_totals = [] - break + this_month = timecheck.create_timestamp_YYYYMM() + today = timecheck.create_timestamp_YYYYMMDD() + day = f"{date}01" + + while day.startswith(date): + if date == this_month and day > today: + break + if day < oldest_log_day: + day = timecheck.get_relative_date_string(day, day_offset=1) + continue + + content = load_daily_source_totals_content(day) + if content is None: + # aktuelle Tageswerte nur berechnen, historische Tage zusaetzlich speichern + content = save_daily_source_totals(day, saveing=(day != today)) + + if isinstance(content, dict): + daily_totals = content.get("totals") + daily_entry = content.get("entry") - if len(entries_with_daily_totals) > 0: + if isinstance(daily_totals, dict) and isinstance(daily_entry, dict) and len(daily_entry) > 0: + daily_entry = deepcopy(daily_entry) + daily_entry["date"] = day + _apply_source_totals(daily_entry, daily_totals) - data["entries"] = entries_with_daily_totals + monthly_entries.append(daily_entry) + if isinstance(content.get("names"), dict): + monthly_names.update(content["names"]) + if isinstance(content.get("colors"), dict): + monthly_colors.update(content["colors"]) - if len(data["entries"]) > 0: - # Wurde im alten System in _process_entries gemacht... - # Entfernt den Eintrag des Folgemonats - data["entries"].pop() - pass + day = timecheck.get_relative_date_string(day, day_offset=1) + + if len(monthly_entries) > 0: + data = {"entries": monthly_entries, "names": monthly_names, "colors": monthly_colors} data["totals"] = get_totals(data["entries"], False) data["totals"] = analyse_percentage_totals(data["entries"], data["totals"]) - return data - # Fallback: alter Rechenweg, falls keine Tages-Summen verfuegbar sind. - data["entries"] = _process_entries(data["entries"], CalculationType.ENERGY) - data["totals"] = get_totals(data["entries"], False) - data = _analyse_energy_source(data) - return data + # Fallback für ältere Monate + # da wir den Monat jetzt schon berechnet haben, können wir ihn auch direkt speichern + # falls er noch nicht existiert. + filepath = Path(_get_data_folder_path()) / "monthly_totals" / f"{date}_totals.json" + if not filepath.is_file() and date != this_month and date >= oldest_log_day[:6]: + save_monthly_source_totals(date, data, saveing=True) + return data -def _collect_monthly_log_data(date: str): - try: - with open(f"{_get_data_folder_path()}/monthly_log/{date}.json", "r") as jsonFile: - log_data = json.load(jsonFile) - this_month = timecheck.create_timestamp_YYYYMM() - if date == this_month: - # add last entry of current day, if current month is requested - try: - today = timecheck.create_timestamp_YYYYMMDD() - with open(f"{_get_data_folder_path()}/daily_log/{today}.json", - "r") as todayJsonFile: - today_log_data = json.load(todayJsonFile) - if len(today_log_data.get("entries", [])) > 0: - today_entry = today_log_data["entries"][-1] - today_entry["date"] = today - log_data["entries"].append(today_entry) - except FILE_ERRORS: - pass - - else: - # add first entry of next month - try: - next_date = timecheck.get_relative_date_string(date, month_offset=1) - with open(f"{_get_data_folder_path()}/monthly_log/{next_date}.json", - "r") as nextJsonFile: - next_log_data = json.load(nextJsonFile) - log_data["entries"].append(next_log_data["entries"][0]) - except FILE_ERRORS: - pass - except FILE_ERRORS: - log_data = {"entries": [], "names": {}} - return log_data + # Fallback, wenn keine Daten vorhanden sind + return {"entries": [], "names": {}, "colors": {}, "totals": {}} def get_yearly_log(year: str): - data = _collect_yearly_log_data(year) - data["entries"] = _process_entries(data["entries"], CalculationType.ENERGY) - data["totals"] = get_totals(data["entries"], False) - data = _analyse_energy_source(data) - return data + # Nur Logs ab dem ältesten Tageslog auswerten + # Sonst werden unötige totals Werte gespeichert + oldest_log_day = _oldest_log_day() + if oldest_log_day is None or year < oldest_log_day[:4]: + return {"entries": [], "names": {}, "colors": {}, "totals": {}} + monthly_entries = [] + monthly_names = {} + monthly_colors = {} -def _collect_yearly_log_data(year: str): - def add_monthly_log(month: str, check_next_month: bool = False) -> None: - monthly_log_path = Path(__file__).resolve().parents[3]/"data"/"monthly_log" - try: - with open(monthly_log_path / f"{month}.json", "r") as jsonFile: - content = json.load(jsonFile) - entries.append(content["entries"][0]) - # add last entry of current file if next file is missing - if check_next_month: - next_month = timecheck.get_relative_date_string(month, month_offset=1) - if not (monthly_log_path / (next_month+".json")).is_file(): - entries.append(content["entries"][-1]) - log.debug(f"Keine Logdatei für Monat {next_month} gefunden, " - f"füge letzten Datensatz von {month} ein: {entries[-1]['date']}") - names.update(content["names"]) - except FILE_ERRORS: - log.debug(f"Kein Log für Monat {month} gefunden.") - - def add_daily_log(day: str) -> None: - try: - with open(f"{_get_data_folder_path()}/daily_log/{day}.json", "r") as dayJsonFile: - day_log_data = json.load(dayJsonFile) - if len(day_log_data["entries"]) > 0: - entries.append(day_log_data["entries"][-1]) - except FILE_ERRORS: - pass - - entries = [] - names = {} - dates = [] - - # we have to find a valid data range - this_year = timecheck.create_timestamp_YYYY() this_month = timecheck.create_timestamp_YYYYMM() - if year < this_year: - # if the requested year is in the past, just add all possible months - for month in range(1, 13): - dates.append(f"{year}{month:02}") - else: - # add all months until current month - for month in range(1, int(this_month[-2:])+1): - dates.append(f"{year}{month:02}") - # add data for month range - for date in dates: - try: - log.debug(f"add regular month: {date}") - add_monthly_log(date, date != this_month) - except Exception: - log.exception(f"Fehler beim Zusammenstellen der Jahresdaten für Monat {date}") + month = f"{year}01" - # now we have to find a valid "next" entry for proper calculation - if year == this_year: # current year - # add todays last entry - this_day = timecheck.create_timestamp_YYYYMMDD() - try: - log.debug(f"add today: {this_day}") - add_daily_log(this_day) - except Exception: - log.exception(f"Fehler beim Zusammenstellen der Jahresdaten für den aktuellen Tag {this_day}") - else: - # no special handling here, just add first entry of next month - next_date = f"{int(year)+1}01" - try: - log.debug(f"add next month: {next_date}") - add_monthly_log(next_date) - except Exception: - log.exception(f"Fehler beim Zusammenstellen der Jahresdaten für Monat {next_date}") + while month.startswith(year): + if month > this_month: + break + + if month < oldest_log_day[:6]: + month = timecheck.get_relative_date_string(month, month_offset=1) + continue + + content = load_monthly_source_totals_content(month) + if content is None: + # aktuelle Monatswerte nur berechnen, historische Monate zusaetzlich speichern + # Fallback, wenn bei der Jahresauswertung ein Monat fehlt, + # dann wird dieser Monat berechnet und gespeichert + content = save_monthly_source_totals(month, None, saveing=(month != this_month)) + + if isinstance(content, dict): + monthly_totals = content.get("totals") + monthly_entry = content.get("entry") + + if isinstance(monthly_totals, dict) and isinstance(monthly_entry, dict) and len(monthly_entry) > 0: + monthly_entry = deepcopy(monthly_entry) + monthly_entry["date"] = month + _apply_source_totals(monthly_entry, monthly_totals) + + monthly_entries.append(monthly_entry) + if isinstance(content.get("names"), dict): + monthly_names.update(content["names"]) + if isinstance(content.get("colors"), dict): + monthly_colors.update(content["colors"]) + + month = timecheck.get_relative_date_string(month, month_offset=1) + + if len(monthly_entries) > 0: + data = {"entries": monthly_entries, "names": monthly_names, "colors": monthly_colors} + data["totals"] = get_totals(data["entries"], False) + data["totals"] = analyse_percentage_totals(data["entries"], data["totals"]) + + return data - # return our data - return {"entries": entries, "names": names} + # Fallback, wenn keine Daten vorhanden sind + return {"entries": [], "names": {}, "colors": {}, "totals": {}} def _analyse_energy_source(data, calc_cp: Optional[str] = None) -> Dict: @@ -732,25 +682,37 @@ def _get_data_folder_path() -> str: return str(Path(__file__).resolve().parents[3] / "data") -def calculate_daily_source_totals(date: str): - data = _collect_daily_log_data(date) - data["entries"] = _process_entries(data["entries"], calculation=CalculationType.ENERGY) - data["totals"] = get_totals(data["entries"], process_entries=False) - data = _analyse_energy_source(data) - - return data["totals"] - - -# Das wird im midnight-handler aufgrufen -# -> erzeugt für jede Tag eine Totals datei def save_daily_source_totals(date: str, saveing: bool = True): try: - totals = calculate_daily_source_totals(date) + data = _collect_daily_log_data(date) + processed_entries = _process_entries(data.get("entries", []), calculation=CalculationType.ENERGY) + totals = get_totals(processed_entries, process_entries=False) + analysed_data = _analyse_energy_source({ + "entries": processed_entries, + "totals": totals, + "names": data.get("names", {}) + }) + totals = analysed_data["totals"] + + source_entries = data.get("entries", []) + daily_entry = {} + if len(source_entries) > 0: + # Nur den letzten Eintrag des Tages nehmen + daily_entry = deepcopy(source_entries[-1]) + daily_entry["date"] = date + _apply_source_totals(daily_entry, totals) + # Erzeugt Ordner daily_totals, falls nicht vorhanden totals_dir = Path(_get_data_folder_path()) / "daily_totals" filepath = totals_dir / f"{date}_totals.json" - content = {"date": date, "totals": totals} + content = { + "date": date, + "totals": totals, + "entry": daily_entry, + "names": data.get("names", {}), + "colors": data.get("colors", {}) + } if saveing: totals_dir.mkdir(parents=True, exist_ok=True) @@ -764,7 +726,7 @@ def save_daily_source_totals(date: str, saveing: bool = True): log.exception(f"Fehler beim Speichern der Tages-Summen für {date}") -def load_daily_source_totals(date: str): +def load_daily_source_totals_content(date: str): try: filepath = f"{_get_data_folder_path()}/daily_totals/{date}_totals.json" if not Path(filepath).is_file(): @@ -775,13 +737,68 @@ def load_daily_source_totals(date: str): content = json.load(jsonFile) log.debug(f"Tages-Summen für {date} geladen aus {filepath}") - return content["totals"] + return content except FILE_ERRORS: log.exception(f"Fehler beim Laden der Tages-Summen für {date}") -def _apply_daily_source_totals(entry: Dict, daily_totals: Dict): +def save_monthly_source_totals(date: str, data: Dict, saveing: bool = True): + try: + # Hauptsächlich für Midnight-Handler + # Wenn keine Daten übergeben werden, dann die Monatswerte berechnen + if data is None: + data = get_monthly_log(date) + + totals = data["totals"] + source_entries = data.get("entries", []) + monthly_entry = {} + if len(source_entries) > 0: + # Nur den letzten Eintrag des Monats nehmen + monthly_entry = deepcopy(source_entries[-1]) + monthly_entry["date"] = date + + # Erzeugt Ordner monthly_totals, falls nicht vorhanden + totals_dir = Path(_get_data_folder_path()) / "monthly_totals" + filepath = totals_dir / f"{date}_totals.json" + content = { + "date": date, + "totals": totals, + "entry": monthly_entry, + "names": data.get("names", {}), + "colors": data.get("colors", {}) + } + + if saveing: + totals_dir.mkdir(parents=True, exist_ok=True) + with open(str(filepath), "w") as jsonFile: + json.dump(content, jsonFile, ensure_ascii=False, indent=2) + + log.debug(f"Monats-Summen für {date} gespeichert in {filepath}") + return content + + except FILE_ERRORS: + log.exception(f"Fehler beim Speichern der Monats-Summen für {date}") + + +def load_monthly_source_totals_content(date: str): + try: + filepath = f"{_get_data_folder_path()}/monthly_totals/{date}_totals.json" + if not Path(filepath).is_file(): + log.debug(f"Keine Monats-Summen-Datei gefunden: {filepath}") + return None + + with open(str(filepath), "r") as jsonFile: + content = json.load(jsonFile) + + log.debug(f"Monats-Summen für {date} geladen aus {filepath}") + return content + + except FILE_ERRORS: + log.exception(f"Fehler beim Laden der Monats-Summen für {date}") + + +def _apply_source_totals(entry: Dict, daily_totals: Dict): for section, section_totals in daily_totals.items(): section_data = entry.get(section) if not isinstance(section_data, dict) or not isinstance(section_totals, dict): @@ -795,3 +812,23 @@ def _apply_daily_source_totals(entry: Dict, daily_totals: Dict): # Alle vorhandenen Summenfelder des Moduls mit den Tages-Summen ueberschreiben. module_data.update(module_totals) return entry + + +def _oldest_log_day() -> Optional[str]: + try: + daily_log_dir = Path(_get_data_folder_path()) / "daily_log" + if not daily_log_dir.is_dir(): + return None + + daily_log_files = list(daily_log_dir.glob("*.json")) + if not daily_log_files: + return None + + # st_ctime = creation time of the file + # st_mtime = last modification time of the file + oldest_file = min(daily_log_files, key=lambda f: f.stat().st_ctime) + oldest_date = oldest_file.stem # Get the filename without extension + return oldest_date + except Exception: + log.exception("Fehler beim Ermitteln des ältesten Tageslogs") + return None diff --git a/packages/main.py b/packages/main.py index dbe409275f..4d9109d5c3 100755 --- a/packages/main.py +++ b/packages/main.py @@ -37,7 +37,7 @@ from modules.utils import wait_for_module_update_completed from smarthome.smarthome import readmq, smarthome_handler -from helpermodules.measurement_logging.process_log import save_daily_source_totals +from helpermodules.measurement_logging.process_log import save_daily_source_totals, save_monthly_source_totals class HandlerAlgorithm: def __init__(self): @@ -232,8 +232,15 @@ def handler5Min(self): def handler_midnight(self): try: save_log(LogType.MONTHLY) - previous_day = timecheck.get_relative_date_string(timecheck.create_timestamp_YYYYMMDD(), day_offset=-1) + today = timecheck.create_timestamp_YYYYMMDD() + previous_day = timecheck.get_relative_date_string(today, day_offset=-1) save_daily_source_totals(previous_day) + + prev_month = timecheck.get_relative_date_string(today, month_offset=-1)[:6] + # Neuer Monat hat angefangen, daher Monats Totals speichern + if today[6:8] == "01": + save_monthly_source_totals(prev_month ,None, saveing=True) + thread_errors_path = Path(Path(__file__).resolve().parents[1]/"ramdisk"/"thread_errors.log") with thread_errors_path.open("w") as f: f.write("") From 446976394707ce0e5a8ffff4fc864eb06888fa46 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Thu, 20 Aug 2026 11:17:35 +0200 Subject: [PATCH 04/17] Remove LogType --- .../measurement_logging/process_log.py | 6 ++-- .../measurement_logging/write_log.py | 29 ++++--------------- packages/main.py | 7 ++--- 3 files changed, 12 insertions(+), 30 deletions(-) diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index b4fb1ca1da..a6122f6e64 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -6,7 +6,7 @@ from typing import Dict, List, Optional, Tuple, Union from helpermodules import timecheck -from helpermodules.measurement_logging.write_log import (LegacySmartHomeLogData, LogType, create_entry, +from helpermodules.measurement_logging.write_log import (LegacySmartHomeLogData, create_entry, get_previous_entry) from helpermodules.messaging import MessageType, pub_system_message from helpermodules.utils.precision_math import decimal_add, decimal_divide, decimal_multiply, decimal_subtract @@ -246,8 +246,8 @@ def _collect_daily_log_data(date: str): log_data = json.load(json_file) if date == timecheck.create_timestamp_YYYYMMDD(): # beim aktuellen Tag den aktuellen Datensatz ergänzen - log_data["entries"].append(create_entry( - LogType.DAILY, LegacySmartHomeLogData(), get_previous_entry(parent_file, log_data))) + log_data["entries"].append(create_entry(LegacySmartHomeLogData(), + get_previous_entry(parent_file, log_data))) else: # bei älteren als letzten Datensatz den des nächsten Tags try: diff --git a/packages/helpermodules/measurement_logging/write_log.py b/packages/helpermodules/measurement_logging/write_log.py index fd8d51aa4b..fc1a2dacbd 100644 --- a/packages/helpermodules/measurement_logging/write_log.py +++ b/packages/helpermodules/measurement_logging/write_log.py @@ -97,11 +97,6 @@ # } -class LogType(Enum): - DAILY = "daily" - MONTHLY = "monthly" - - class LegacySmartHomeLogData: def __init__(self) -> None: self.all_received_topics: Dict = {} @@ -133,20 +128,11 @@ def on_message(self, client: MqttClient, userdata, msg: MQTTMessage): self.all_received_topics.update({msg.topic: msg.payload}) -def save_log(log_type: LogType): - """ Parameter - --------- - folder: str - gibt an, ob ein Tages-oder Monats-Log-Eintrag erstellt werden soll. - """ +def save_log(): try: - parent_file = Path(__file__).resolve().parents[3] / "data" / \ - ("daily_log" if log_type == LogType.DAILY else "monthly_log") + parent_file = Path(__file__).resolve().parents[3] / "data" / "daily_log" parent_file.mkdir(mode=0o755, parents=True, exist_ok=True) - if log_type == LogType.DAILY: - file_name = timecheck.create_timestamp_YYYYMMDD() - else: - file_name = timecheck.create_timestamp_YYYYMM() + file_name = timecheck.create_timestamp_YYYYMMDD() filepath = str(parent_file / f"{file_name}.json") try: @@ -162,7 +148,7 @@ def save_log(log_type: LogType): previous_entry = get_previous_entry(parent_file, content) sh_log_data = LegacySmartHomeLogData() - new_entry = create_entry(log_type, sh_log_data, previous_entry) + new_entry = create_entry(sh_log_data, previous_entry) # json-Objekt in Datei einfügen @@ -194,11 +180,8 @@ def get_previous_entry(parent_file: Path, content: Dict) -> Optional[Dict]: return previous_entry -def create_entry(log_type: LogType, sh_log_data: LegacySmartHomeLogData, previous_entry: Optional[Dict]) -> Dict: - if log_type == LogType.DAILY: - date = timecheck.create_timestamp_HH_MM() - else: - date = timecheck.create_timestamp_YYYYMMDD() +def create_entry(sh_log_data: LegacySmartHomeLogData, previous_entry: Optional[Dict]) -> Dict: + date = timecheck.create_timestamp_HH_MM() current_timestamp = int(timecheck.create_timestamp()) try: diff --git a/packages/main.py b/packages/main.py index 4d9109d5c3..6c2827e526 100755 --- a/packages/main.py +++ b/packages/main.py @@ -27,7 +27,7 @@ from helpermodules.changed_values_handler import ChangedValuesContext from helpermodules.mosquitto_dynsec.mosquitto_dynsec import check_roles_at_start from helpermodules.measurement_logging.update_yields import update_daily_yields, update_pv_monthly_yearly_yields -from helpermodules.measurement_logging.write_log import LogType, save_log +from helpermodules.measurement_logging.write_log import save_log from helpermodules.modbusserver import start_modbus_server from helpermodules.pub import Pub from modules import configuration, loadvars, update_soc @@ -181,7 +181,7 @@ def handler5MinAlgorithm(self): """ try: with ChangedValuesContext(loadvars_.event_module_update_completed): - totals = save_log(LogType.DAILY) + totals = save_log() update_daily_yields(totals) update_pv_monthly_yearly_yields() for cp in data.data.cp_data.values(): @@ -231,13 +231,12 @@ def handler5Min(self): @__with_handler_lock(error_threshold=60) def handler_midnight(self): try: - save_log(LogType.MONTHLY) today = timecheck.create_timestamp_YYYYMMDD() previous_day = timecheck.get_relative_date_string(today, day_offset=-1) save_daily_source_totals(previous_day) prev_month = timecheck.get_relative_date_string(today, month_offset=-1)[:6] - # Neuer Monat hat angefangen, daher Monats Totals speichern + # Neuer Monat hat angefangen, daher Monats Totals speichern if today[6:8] == "01": save_monthly_source_totals(prev_month ,None, saveing=True) From 28217f83d652fbea814baaa8a558bdfdc70e96e1 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Thu, 20 Aug 2026 11:26:38 +0200 Subject: [PATCH 05/17] Remove unused import --- packages/helpermodules/measurement_logging/write_log.py | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/helpermodules/measurement_logging/write_log.py b/packages/helpermodules/measurement_logging/write_log.py index fc1a2dacbd..ea86577707 100644 --- a/packages/helpermodules/measurement_logging/write_log.py +++ b/packages/helpermodules/measurement_logging/write_log.py @@ -1,4 +1,3 @@ -from enum import Enum import os import json import logging From 36c27c05cd269ebeba8a43307c211e46f10dc0b0 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Thu, 20 Aug 2026 13:23:49 +0200 Subject: [PATCH 06/17] Add Copilot Suggestions --- .../measurement_logging/process_log.py | 54 ++++++++++++------- packages/main.py | 2 +- 2 files changed, 35 insertions(+), 21 deletions(-) diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index a6122f6e64..b0b1e467c3 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -1,5 +1,6 @@ from enum import Enum from copy import deepcopy +import datetime import json import logging from pathlib import Path @@ -289,7 +290,7 @@ def get_monthly_log(date: str): content = load_daily_source_totals_content(day) if content is None: # aktuelle Tageswerte nur berechnen, historische Tage zusaetzlich speichern - content = save_daily_source_totals(day, saveing=(day != today)) + content = save_daily_source_totals(day, saving=(day != today)) if isinstance(content, dict): daily_totals = content.get("totals") @@ -318,7 +319,7 @@ def get_monthly_log(date: str): # falls er noch nicht existiert. filepath = Path(_get_data_folder_path()) / "monthly_totals" / f"{date}_totals.json" if not filepath.is_file() and date != this_month and date >= oldest_log_day[:6]: - save_monthly_source_totals(date, data, saveing=True) + save_monthly_source_totals(date, data, saving=True) return data @@ -353,7 +354,7 @@ def get_yearly_log(year: str): # aktuelle Monatswerte nur berechnen, historische Monate zusaetzlich speichern # Fallback, wenn bei der Jahresauswertung ein Monat fehlt, # dann wird dieser Monat berechnet und gespeichert - content = save_monthly_source_totals(month, None, saveing=(month != this_month)) + content = save_monthly_source_totals(month, None, saving=(month != this_month)) if isinstance(content, dict): monthly_totals = content.get("totals") @@ -682,10 +683,11 @@ def _get_data_folder_path() -> str: return str(Path(__file__).resolve().parents[3] / "data") -def save_daily_source_totals(date: str, saveing: bool = True): +def save_daily_source_totals(date: str, saving: bool = True): try: data = _collect_daily_log_data(date) - processed_entries = _process_entries(data.get("entries", []), calculation=CalculationType.ENERGY) + source_entries = data.get("entries", []) + processed_entries = _process_entries(deepcopy(source_entries), calculation=CalculationType.ENERGY) totals = get_totals(processed_entries, process_entries=False) analysed_data = _analyse_energy_source({ "entries": processed_entries, @@ -694,11 +696,10 @@ def save_daily_source_totals(date: str, saveing: bool = True): }) totals = analysed_data["totals"] - source_entries = data.get("entries", []) daily_entry = {} - if len(source_entries) > 0: - # Nur den letzten Eintrag des Tages nehmen - daily_entry = deepcopy(source_entries[-1]) + source_daily_entry = _get_last_entry_for_period(source_entries, date, "%Y%m%d") + if source_daily_entry is not None: + daily_entry = deepcopy(source_daily_entry) daily_entry["date"] = date _apply_source_totals(daily_entry, totals) @@ -714,7 +715,7 @@ def save_daily_source_totals(date: str, saveing: bool = True): "colors": data.get("colors", {}) } - if saveing: + if saving: totals_dir.mkdir(parents=True, exist_ok=True) with open(str(filepath), "w") as jsonFile: json.dump(content, jsonFile, ensure_ascii=False, indent=2) @@ -743,7 +744,7 @@ def load_daily_source_totals_content(date: str): log.exception(f"Fehler beim Laden der Tages-Summen für {date}") -def save_monthly_source_totals(date: str, data: Dict, saveing: bool = True): +def save_monthly_source_totals(date: str, data: Optional[Dict], saving: bool = True): try: # Hauptsächlich für Midnight-Handler # Wenn keine Daten übergeben werden, dann die Monatswerte berechnen @@ -753,9 +754,10 @@ def save_monthly_source_totals(date: str, data: Dict, saveing: bool = True): totals = data["totals"] source_entries = data.get("entries", []) monthly_entry = {} - if len(source_entries) > 0: + source_monthly_entry = _get_last_entry_for_period(source_entries, date, "%Y%m") + if source_monthly_entry is not None: # Nur den letzten Eintrag des Monats nehmen - monthly_entry = deepcopy(source_entries[-1]) + monthly_entry = deepcopy(source_monthly_entry) monthly_entry["date"] = date # Erzeugt Ordner monthly_totals, falls nicht vorhanden @@ -769,7 +771,7 @@ def save_monthly_source_totals(date: str, data: Dict, saveing: bool = True): "colors": data.get("colors", {}) } - if saveing: + if saving: totals_dir.mkdir(parents=True, exist_ok=True) with open(str(filepath), "w") as jsonFile: json.dump(content, jsonFile, ensure_ascii=False, indent=2) @@ -798,6 +800,21 @@ def load_monthly_source_totals_content(date: str): log.exception(f"Fehler beim Laden der Monats-Summen für {date}") +def _get_last_entry_for_period(entries: List, period: str, period_format: str) -> Optional[Dict]: + # Suche den letzten Eintrag in der Liste, der dem angegebenen Zeitraum entspricht. + for entry in reversed(entries): + if isinstance(entry, dict) and isinstance(entry.get("timestamp"), (int, float)): + entry_period = datetime.datetime.fromtimestamp(entry["timestamp"]).strftime(period_format) + if entry_period == period: + return entry + + # Fallback: Falls kein passender Zeitstempel gefunden wird, letzten gueltigen Eintrag verwenden. + for entry in reversed(entries): + if isinstance(entry, dict): + return entry + return None + + def _apply_source_totals(entry: Dict, daily_totals: Dict): for section, section_totals in daily_totals.items(): section_data = entry.get(section) @@ -820,15 +837,12 @@ def _oldest_log_day() -> Optional[str]: if not daily_log_dir.is_dir(): return None - daily_log_files = list(daily_log_dir.glob("*.json")) + daily_log_files = [p for p in daily_log_dir.glob("*.json") if p.stem.isdigit()] if not daily_log_files: return None - # st_ctime = creation time of the file - # st_mtime = last modification time of the file - oldest_file = min(daily_log_files, key=lambda f: f.stat().st_ctime) - oldest_date = oldest_file.stem # Get the filename without extension - return oldest_date + oldest_file = min(daily_log_files, key=lambda f: f.stem) + return oldest_file.stem except Exception: log.exception("Fehler beim Ermitteln des ältesten Tageslogs") return None diff --git a/packages/main.py b/packages/main.py index 6c2827e526..2f0e680ea5 100755 --- a/packages/main.py +++ b/packages/main.py @@ -238,7 +238,7 @@ def handler_midnight(self): prev_month = timecheck.get_relative_date_string(today, month_offset=-1)[:6] # Neuer Monat hat angefangen, daher Monats Totals speichern if today[6:8] == "01": - save_monthly_source_totals(prev_month ,None, saveing=True) + save_monthly_source_totals(prev_month ,None, saving=True) thread_errors_path = Path(Path(__file__).resolve().parents[1]/"ramdisk"/"thread_errors.log") with thread_errors_path.open("w") as f: From b8eff8ce9969609f08795db4d04c01f854e16c7a Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Fri, 21 Aug 2026 07:56:05 +0200 Subject: [PATCH 07/17] Update pv-yield Berechnung --- .../measurement_logging/update_yields.py | 155 +++++++++++------- packages/main.py | 7 +- 2 files changed, 100 insertions(+), 62 deletions(-) diff --git a/packages/helpermodules/measurement_logging/update_yields.py b/packages/helpermodules/measurement_logging/update_yields.py index 23b1a8a251..816963e132 100644 --- a/packages/helpermodules/measurement_logging/update_yields.py +++ b/packages/helpermodules/measurement_logging/update_yields.py @@ -5,7 +5,7 @@ from control import data from helpermodules import timecheck -from helpermodules.measurement_logging.process_log import get_totals +from helpermodules.measurement_logging.process_log import get_totals, load_daily_source_totals_content, load_monthly_source_totals_content log = logging.getLogger(__name__) @@ -17,6 +17,7 @@ def update_daily_yields(entries): totals = get_totals(entries) [update_module_yields(type, totals) for type in ("bat", "counter", "cp", "pv")] data.data.counter_all_data.data.set.daily_yield_home_consumption = totals["hc"]["all"]["energy_imported"] + return totals except Exception: log.exception("Fehler beim Veröffentlichen der Tageserträge.") @@ -40,72 +41,108 @@ def update_module_yields(module: str, totals: Dict) -> None: log.exception(f"Fehler beim Aktualisieren der Tageserträge für Modul {m} vom Typ {module}.") -def update_pv_monthly_yearly_yields(): - """ veröffentlicht die monatlichen und jährlichen Erträge für PV +def update_pv_monthly_yearly_yields(daily_totals: Dict) -> None: + """ + veröffentlicht die monatlichen und jährlichen Erträge für PV """ - _update_pv_monthly_yields() - _update_pv_yearly_yields() + monthly_totals = _get_pv_monthly_yields(daily_totals) + yearly_totals = _get_pv_yearly_yields(monthly_totals) -def _update_pv_monthly_yields(): - """ veröffentlicht die monatlichen Erträge für PV - für pv_all nicht die Differenz aus den Logs nehmen, sondern die Summe der Module. Wenn im laufenden Monat ein Modul - gelöscht wurde und keins oder eines mit niedrigerem Zählerstand hinzugefügt wird, wird sonst ein negativer Wert - ermittelt. + pv_all_monthly_yield = 0 + pv_all_yearly_yield = 0 + + for pv_module in data.data.pv_data.values(): + + # Was wurde im Monat/Jahr exportiert + monthly_yield = monthly_totals.get(f"pv{pv_module.num}", {}).get("energy_exported", 0) + yearly_yield = yearly_totals.get(f"pv{pv_module.num}", {}).get("energy_exported", 0) + + data.data.pv_data[f"pv{pv_module.num}"].data.get.monthly_exported = monthly_yield + data.data.pv_data[f"pv{pv_module.num}"].data.get.yearly_exported = yearly_yield + + # Summe über alle Module für pv_all + pv_all_monthly_yield += monthly_yield + pv_all_yearly_yield += yearly_yield + + data.data.pv_all_data.data.get.monthly_exported = pv_all_monthly_yield + data.data.pv_all_data.data.get.yearly_exported = pv_all_yearly_yield + + +def _get_pv_monthly_yields(daily_totals: Dict) -> Dict: """ - try: - pv_all_monthly_yield = 0 - with open(f"data/monthly_log/{timecheck.create_timestamp_YYYYMM()}.json", "r") as f: - monthly_log = json.load(f) - for pv_module in data.data.pv_data.values(): - for entry in monthly_log["entries"]: - if entry["pv"].get(f"pv{pv_module.num}"): - monthly_yield = data.data.pv_data[f"pv{pv_module.num}"].data.get.exported - \ - entry["pv"][f"pv{pv_module.num}"]["exported"] - pv_all_monthly_yield += monthly_yield - data.data.pv_data[f"pv{pv_module.num}"].data.get.monthly_exported = monthly_yield - break - data.data.pv_all_data.data.get.monthly_exported = pv_all_monthly_yield - except FileNotFoundError: - # am Tag der Ersteinrichtung gibt es noch kein Monatslog-File, das wird erst um Mitternacht erstellt. - log.debug("No monthly logfile found for calculation of monthly yield") - except Exception: - log.exception("Fehler beim Veröffentlichen der monatlichen Erträge für PV") + Berechnet den Unterschied zwischen dem Zählerstand vom ersten Tag des aktuellen Monats bis zum aktuellen Tag. + """ + + this_month = timecheck.create_timestamp_YYYYMM() + today = timecheck.create_timestamp_YYYYMMDD() + + pv_totals = {} + + daily_log_path = _get_parent_path()/"data"/"daily_log" + + for logfile in sorted(daily_log_path.glob(f"{this_month}*.json")): + day = logfile.stem + totals = {} + if day == today: + continue # Der aktuelle Tag wird später behandelt + else: + # Lade alle vergangenen Tage des Monats aus dem daily_totals-Logfile, um die Tageserträge zu ermitteln + content = load_daily_source_totals_content(day) + if content is not None: + totals = content.get("totals", {}) + + # Totals aufsummieren + _add_pv_totals(pv_totals, totals.get("pv", {})) + + # aktueller Tag ergänzen + _add_pv_totals(pv_totals, daily_totals.get("pv", {})) + return pv_totals -def _update_pv_yearly_yields(): - """ veröffentlicht die jährlichen Erträge für PV - für pv_all nicht die Differenz aus den Logs nehmen, sondern die Summe der Module. Wenn unterjährig ein Modul - gelöscht wurde und keins oder eines mit niedrigerem Zählerstand hinzugefügt wird, wird sonst ein negativer Wert - ermittelt. + +def _get_pv_yearly_yields(current_monthly_totals: Dict) -> Dict: """ - try: - pv_all_yearly_yield = 0 - path_list = list(Path(_get_parent_path()/"data"/"monthly_log").glob(f"{timecheck.create_timestamp_YYYY()}*")) - sorted_path_list = sorted([str(p) for p in path_list]) - for pv_module in data.data.pv_data.values(): - found_pv = False - for path in sorted_path_list: - with open(path, "r") as f: - monthly_log = json.load(f) - for entry in monthly_log["entries"]: - # erster Eintrag mit PV im Jahr, falls WR erst im laufenden Jahr hinzugefügt wurden - if entry["pv"].get(f"pv{pv_module.num}"): - yearly_yield = data.data.pv_data[f"pv{pv_module.num}"].data.get.exported - \ - entry["pv"][f"pv{pv_module.num}"]["exported"] - pv_all_yearly_yield += yearly_yield - data.data.pv_data[f"pv{pv_module.num}"].data.get.yearly_exported = yearly_yield - found_pv = True - break - if found_pv: - break - else: - # am Tag der Ersteinrichtung gibt es noch kein Monatslog-File, das wird erst um Mitternacht erstellt. - log.debug("No monthly logfile found for calculation of yearly yield") - data.data.pv_all_data.data.get.yearly_exported = pv_all_yearly_yield - except Exception: - log.exception("Fehler beim Veröffentlichen der jährlichen Erträge für PV") + Berechnet den Unterschied zwischen dem Zählerstand vom ersten Monat des aktuellen Jahres bis zum aktuellen Monat. + """ + this_year = timecheck.create_timestamp_YYYY() + this_month = timecheck.create_timestamp_YYYYMM() + + pv_totals = {} + + monthly_log_path = _get_parent_path()/"data"/"monthly_totals" + + # Wenn es noch keinen Montas Totals gibt + if monthly_log_path.is_dir(): + for logfile in sorted(monthly_log_path.glob(f"{this_year}*_totals.json")): + month = logfile.stem[:6] + totals = {} + if month == this_month: + continue # Der aktuelle Monat wird später behandelt + content = load_monthly_source_totals_content(month) + if content is not None: + totals = content.get("totals", {}) + + # Totals aufsummieren + _add_pv_totals(pv_totals, totals.get("pv", {})) + + # aktueller Monat ergänzen + _add_pv_totals(pv_totals, current_monthly_totals) + + return pv_totals def _get_parent_path() -> Path: return Path(__file__).resolve().parents[3] + + +def _add_pv_totals(target: Dict, source: Dict) -> None: + for pv_key, values in source.items(): + energy_exported = values.get("energy_exported", 0) + + # Wenn das PV-Modul noch nicht im target ist, initialisiere es mit 0 + if pv_key not in target: + target[pv_key] = {"energy_exported": 0} + + # Addiere die energy_exported Werte für das PV-Modul + target[pv_key]["energy_exported"] += energy_exported diff --git a/packages/main.py b/packages/main.py index 2f0e680ea5..ad5848a64d 100755 --- a/packages/main.py +++ b/packages/main.py @@ -181,9 +181,10 @@ def handler5MinAlgorithm(self): """ try: with ChangedValuesContext(loadvars_.event_module_update_completed): - totals = save_log() - update_daily_yields(totals) - update_pv_monthly_yearly_yields() + entries = save_log() + daily_totals = update_daily_yields(entries) + update_pv_monthly_yearly_yields(daily_totals) + for cp in data.data.cp_data.values(): calc_energy_costs(cp) data.data.general_data.grid_protection() From ca42cd1b6032f21a94236dea075e1ca09dbb5d0d Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Fri, 21 Aug 2026 08:03:41 +0200 Subject: [PATCH 08/17] Fix Flake8 errors --- .../helpermodules/measurement_logging/update_yields.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/helpermodules/measurement_logging/update_yields.py b/packages/helpermodules/measurement_logging/update_yields.py index 816963e132..67a88d2748 100644 --- a/packages/helpermodules/measurement_logging/update_yields.py +++ b/packages/helpermodules/measurement_logging/update_yields.py @@ -1,11 +1,12 @@ -import json import logging from pathlib import Path from typing import Dict from control import data from helpermodules import timecheck -from helpermodules.measurement_logging.process_log import get_totals, load_daily_source_totals_content, load_monthly_source_totals_content +from helpermodules.measurement_logging.process_log import (get_totals, + load_daily_source_totals_content, + load_monthly_source_totals_content) log = logging.getLogger(__name__) @@ -42,7 +43,7 @@ def update_module_yields(module: str, totals: Dict) -> None: def update_pv_monthly_yearly_yields(daily_totals: Dict) -> None: - """ + """ veröffentlicht die monatlichen und jährlichen Erträge für PV """ From b5f479fb62fa584b8a22c5180086cd729a240ee2 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Fri, 21 Aug 2026 09:06:17 +0200 Subject: [PATCH 09/17] Add Copolit Suggestions --- .../measurement_logging/process_log.py | 19 +++++++--- .../measurement_logging/update_yields.py | 35 ++++++++++--------- packages/main.py | 5 +-- 3 files changed, 37 insertions(+), 22 deletions(-) diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index b0b1e467c3..f46d81f6b5 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -281,7 +281,8 @@ def get_monthly_log(date: str): day = f"{date}01" while day.startswith(date): - if date == this_month and day > today: + # Zukunftstage/-monate nicht verarbeiten, um keine leeren daily_totals zu erzeugen. + if day > today: break if day < oldest_log_day: day = timecheck.get_relative_date_string(day, day_offset=1) @@ -313,6 +314,7 @@ def get_monthly_log(date: str): data = {"entries": monthly_entries, "names": monthly_names, "colors": monthly_colors} data["totals"] = get_totals(data["entries"], False) data["totals"] = analyse_percentage_totals(data["entries"], data["totals"]) + data = _analyse_energy_source(data) # Fallback für ältere Monate # da wir den Monat jetzt schon berechnet haben, können wir ihn auch direkt speichern @@ -329,7 +331,7 @@ def get_monthly_log(date: str): def get_yearly_log(year: str): # Nur Logs ab dem ältesten Tageslog auswerten - # Sonst werden unötige totals Werte gespeichert + # Sonst werden unnötige totals Werte gespeichert oldest_log_day = _oldest_log_day() if oldest_log_day is None or year < oldest_log_day[:4]: return {"entries": [], "names": {}, "colors": {}, "totals": {}} @@ -377,6 +379,7 @@ def get_yearly_log(year: str): data = {"entries": monthly_entries, "names": monthly_names, "colors": monthly_colors} data["totals"] = get_totals(data["entries"], False) data["totals"] = analyse_percentage_totals(data["entries"], data["totals"]) + data = _analyse_energy_source(data) return data @@ -818,14 +821,22 @@ def _get_last_entry_for_period(entries: List, period: str, period_format: str) - def _apply_source_totals(entry: Dict, daily_totals: Dict): for section, section_totals in daily_totals.items(): section_data = entry.get(section) - if not isinstance(section_data, dict) or not isinstance(section_totals, dict): + if not isinstance(section_totals, dict): continue + if not isinstance(section_data, dict): + section_data = {} + entry[section] = section_data + for module, module_totals in section_totals.items(): module_data = section_data.get(module) - if not isinstance(module_data, dict) or not isinstance(module_totals, dict): + if not isinstance(module_totals, dict): continue + if not isinstance(module_data, dict): + module_data = {} + section_data[module] = module_data + # Alle vorhandenen Summenfelder des Moduls mit den Tages-Summen ueberschreiben. module_data.update(module_totals) return entry diff --git a/packages/helpermodules/measurement_logging/update_yields.py b/packages/helpermodules/measurement_logging/update_yields.py index 67a88d2748..5c579da0aa 100644 --- a/packages/helpermodules/measurement_logging/update_yields.py +++ b/packages/helpermodules/measurement_logging/update_yields.py @@ -6,7 +6,9 @@ from helpermodules import timecheck from helpermodules.measurement_logging.process_log import (get_totals, load_daily_source_totals_content, - load_monthly_source_totals_content) + load_monthly_source_totals_content, + save_daily_source_totals, + get_monthly_log) log = logging.getLogger(__name__) @@ -90,7 +92,10 @@ def _get_pv_monthly_yields(daily_totals: Dict) -> Dict: else: # Lade alle vergangenen Tage des Monats aus dem daily_totals-Logfile, um die Tageserträge zu ermitteln content = load_daily_source_totals_content(day) - if content is not None: + if content is None: + # Fallback/Migration: fehlende Tages-Totals aus den Tageslogs berechnen und speichern. + content = save_daily_source_totals(day, saving=True) + if isinstance(content, dict): totals = content.get("totals", {}) # Totals aufsummieren @@ -111,21 +116,19 @@ def _get_pv_yearly_yields(current_monthly_totals: Dict) -> Dict: pv_totals = {} - monthly_log_path = _get_parent_path()/"data"/"monthly_totals" - - # Wenn es noch keinen Montas Totals gibt - if monthly_log_path.is_dir(): - for logfile in sorted(monthly_log_path.glob(f"{this_year}*_totals.json")): - month = logfile.stem[:6] - totals = {} - if month == this_month: - continue # Der aktuelle Monat wird später behandelt - content = load_monthly_source_totals_content(month) - if content is not None: - totals = content.get("totals", {}) + month = f"{this_year}01" + while month < this_month: + totals = {} + content = load_monthly_source_totals_content(month) + if content is None: + # Fallback/Migration: fehlende Monats-Totals berechnen und speichern. + content = get_monthly_log(month) + if isinstance(content, dict): + totals = content.get("totals", {}) - # Totals aufsummieren - _add_pv_totals(pv_totals, totals.get("pv", {})) + # Totals aufsummieren + _add_pv_totals(pv_totals, totals.get("pv", {})) + month = timecheck.get_relative_date_string(month, month_offset=1) # aktueller Monat ergänzen _add_pv_totals(pv_totals, current_monthly_totals) diff --git a/packages/main.py b/packages/main.py index ad5848a64d..390189f925 100755 --- a/packages/main.py +++ b/packages/main.py @@ -183,8 +183,9 @@ def handler5MinAlgorithm(self): with ChangedValuesContext(loadvars_.event_module_update_completed): entries = save_log() daily_totals = update_daily_yields(entries) - update_pv_monthly_yearly_yields(daily_totals) - + if daily_totals is not None: + update_pv_monthly_yearly_yields(daily_totals) + for cp in data.data.cp_data.values(): calc_energy_costs(cp) data.data.general_data.grid_protection() From cc6c637fb9da3526fa145e4dc2372490d5df68cf Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Mon, 24 Aug 2026 15:00:39 +0200 Subject: [PATCH 10/17] Improve pv-yield Fallback --- .../measurement_logging/process_log.py | 28 +++++++++++++++++++ .../measurement_logging/update_yields.py | 8 +++++- 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index f46d81f6b5..a8c1d81976 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -5,6 +5,7 @@ import logging from pathlib import Path from typing import Dict, List, Optional, Tuple, Union +from concurrent.futures import ProcessPoolExecutor from helpermodules import timecheck from helpermodules.measurement_logging.write_log import (LegacySmartHomeLogData, create_entry, @@ -265,6 +266,10 @@ def _collect_daily_log_data(date: str): def get_monthly_log(date: str): + + if not (len(date) == 6 and date.isdigit()): + log.debug(f"Ungültiges Datum für Monats-Summen: {date}") + return {"entries": [], "names": {}, "colors": {}, "totals": {}} # Nur Logs ab dem ältesten Tageslog auswerten # Sonst werden unötige totals Werte gespeichert oldest_log_day = _oldest_log_day() @@ -688,6 +693,9 @@ def _get_data_folder_path() -> str: def save_daily_source_totals(date: str, saving: bool = True): try: + if not (len(date) == 8 and date.isdigit()): + log.debug(f"Ungültiges Datum für Tages-Summen: {date}") + return None data = _collect_daily_log_data(date) source_entries = data.get("entries", []) processed_entries = _process_entries(deepcopy(source_entries), calculation=CalculationType.ENERGY) @@ -732,6 +740,10 @@ def save_daily_source_totals(date: str, saving: bool = True): def load_daily_source_totals_content(date: str): try: + if not (len(date) == 8 and date.isdigit()): + log.debug(f"Ungültiges Datum für Tages-Summen: {date}") + return None + filepath = f"{_get_data_folder_path()}/daily_totals/{date}_totals.json" if not Path(filepath).is_file(): log.debug(f"Keine Tages-Summen-Datei gefunden: {filepath}") @@ -857,3 +869,19 @@ def _oldest_log_day() -> Optional[str]: except Exception: log.exception("Fehler beim Ermitteln des ältesten Tageslogs") return None + + +def generate_daily_totals_for_current_year(): + months_list = [] + + current_year = timecheck.create_timestamp_YYYY() + current_month = timecheck.create_timestamp_YYYYMM()[4:6] + + for month in range(1, int(current_month)): + month_str = f"{current_year}{month:02d}" + months_list.append(month_str) + + with ProcessPoolExecutor() as executor: + executor.map(get_monthly_log, months_list) + + log.debug("Tages-Summen für das aktuelle Jahr wurden berechnet und gespeichert.") diff --git a/packages/helpermodules/measurement_logging/update_yields.py b/packages/helpermodules/measurement_logging/update_yields.py index 5c579da0aa..69f3ce1926 100644 --- a/packages/helpermodules/measurement_logging/update_yields.py +++ b/packages/helpermodules/measurement_logging/update_yields.py @@ -8,7 +8,8 @@ load_daily_source_totals_content, load_monthly_source_totals_content, save_daily_source_totals, - get_monthly_log) + get_monthly_log, + generate_daily_totals_for_current_year) log = logging.getLogger(__name__) @@ -49,6 +50,11 @@ def update_pv_monthly_yearly_yields(daily_totals: Dict) -> None: veröffentlicht die monatlichen und jährlichen Erträge für PV """ + folder = _get_parent_path()/"data"/"daily_totals" + if not folder.exists(): + # Nur wenn es noch keine Tages-Totals gibt, werden diese berechnet und gespeichert. + generate_daily_totals_for_current_year() + monthly_totals = _get_pv_monthly_yields(daily_totals) yearly_totals = _get_pv_yearly_yields(monthly_totals) From eeabcb4c6007f297b71b9252de4a647973579c6b Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Mon, 24 Aug 2026 15:01:48 +0200 Subject: [PATCH 11/17] Add Tests --- .../process_log_unit_test.py | 155 ++++++++++++++++++ 1 file changed, 155 insertions(+) diff --git a/packages/helpermodules/measurement_logging/process_log_unit_test.py b/packages/helpermodules/measurement_logging/process_log_unit_test.py index 30b56ec065..a800ec91de 100644 --- a/packages/helpermodules/measurement_logging/process_log_unit_test.py +++ b/packages/helpermodules/measurement_logging/process_log_unit_test.py @@ -4,6 +4,7 @@ from typing import Dict from unittest.mock import Mock, mock_open import pytest +import datetime from helpermodules.measurement_logging.process_log import ( analyse_percentage, @@ -11,6 +12,9 @@ process_entry, get_totals, _collect_daily_log_data, + _get_last_entry_for_period, + _apply_source_totals, + get_monthly_log, calc_energy_imported_by_source, analyse_percentage_totals, CalculationType) @@ -392,3 +396,154 @@ def test_collect_daily_log_data_json_decode_error(monkeypatch): # evaluation expected_result = {"entries": [], "names": {}} assert result == expected_result + + +def test_get_monthly_log_aggregates_days_and_saves_missing_month_totals(monkeypatch): + # setup + month = "202404" + today = "20240402" + + def relative_date_string(date_value, day_offset=0, month_offset=0): + base = json.loads(json.dumps(date_value)) + if month_offset: + dt = datetime.datetime.strptime(base, "%Y%m") + year = dt.year + ((dt.month - 1 + month_offset) // 12) + month_value = ((dt.month - 1 + month_offset) % 12) + 1 + return f"{year:04d}{month_value:02d}" + dt = datetime.datetime.strptime(base, "%Y%m%d") + return (dt + datetime.timedelta(days=day_offset)).strftime("%Y%m%d") + + # daily_totals Mockdaten + day1_content = { + "totals": {"cp": {"all": {"energy_imported": 10}}}, + "entry": {"timestamp": 1, "cp": {"all": {}}}, + "names": {"cp1": "Ladepunkt 1"}, + "colors": {"cp1": "#123456"} + } + day2_content = { + "totals": {"cp": {"all": {"energy_imported": 20}}}, + "entry": {"timestamp": 2, "cp": {"all": {}}}, + "names": {"cp2": "Ladepunkt 2"}, + "colors": {"cp2": "#654321"} + } + + save_daily_mock = Mock(return_value=day2_content) + save_monthly_mock = Mock() + get_totals_mock = Mock(return_value={"mocked": "totals"}) + + monkeypatch.setattr("helpermodules.measurement_logging.process_log._oldest_log_day", Mock(return_value="20240401")) + monkeypatch.setattr("helpermodules.measurement_logging.process_log._get_data_folder_path", + Mock(return_value="/tmp")) + monkeypatch.setattr("helpermodules.measurement_logging.process_log.timecheck.create_timestamp_YYYYMM", + Mock(return_value="202406")) + monkeypatch.setattr("helpermodules.measurement_logging.process_log.timecheck.create_timestamp_YYYYMMDD", + Mock(return_value=today)) + monkeypatch.setattr("helpermodules.measurement_logging.process_log.timecheck.get_relative_date_string", + relative_date_string) + + # Nur für den ersten Tag gibt es bereits eine daily_totals-Datei + monkeypatch.setattr( + "helpermodules.measurement_logging.process_log.load_daily_source_totals_content", + lambda day: day1_content if day == "20240401" else None) + monkeypatch.setattr("helpermodules.measurement_logging.process_log.save_daily_source_totals", save_daily_mock) + monkeypatch.setattr("helpermodules.measurement_logging.process_log.get_totals", get_totals_mock) + monkeypatch.setattr( + "helpermodules.measurement_logging.process_log.analyse_percentage_totals", + lambda entries, totals: {"mocked": "analysed_totals", "count": len(entries), "totals": totals}) + monkeypatch.setattr("helpermodules.measurement_logging.process_log._analyse_energy_source", lambda data: data) + monkeypatch.setattr("helpermodules.measurement_logging.process_log.save_monthly_source_totals", save_monthly_mock) + + # execution + result = get_monthly_log(month) + + # evaluation + assert len(result["entries"]) == 2 + assert result["entries"][0]["date"] == "20240401" + assert result["entries"][0]["cp"]["all"]["energy_imported"] == 10 + assert result["entries"][1]["date"] == "20240402" + assert result["entries"][1]["cp"]["all"]["energy_imported"] == 20 + assert result["names"] == {"cp1": "Ladepunkt 1", "cp2": "Ladepunkt 2"} + assert result["colors"] == {"cp1": "#123456", "cp2": "#654321"} + assert result["totals"] == {"mocked": "analysed_totals", "count": 2, "totals": {"mocked": "totals"}} + + # Für den 2. Tag gibt es noch keine daily_totals-Datei + # -> soll auch nicht gespeichert werden, da der 2. Tag der aktuelle Tag ist + save_daily_mock.assert_called_once_with("20240402", saving=False) + + # monthly_totals sollen gespeichert werden + save_monthly_mock.assert_called_once_with(month, result, saving=True) + + +def test_apply_source_totals_updates_existing_and_creates_missing_sections(): + # setup + entry = { + "cp": { + "all": {"energy_imported": 5, "keep": "x"}, + "cp9": {"keep_cp": True} + }, + "meta": {"unchanged": True} + } + daily_totals = { + "cp": { + "all": {"energy_imported": 12, "energy_exported": 3}, + "cp1": {"energy_imported": 7}, + "cp_invalid": 99 + }, + "hc": { + "all": {"energy_imported": 2} + }, + "invalid_section": "ignore_me" + } + + # execution + result = _apply_source_totals(entry, daily_totals) + + # evaluation + # in-place behavior + assert result is entry + + # existing module gets overwritten/extended, unrelated fields stay + assert result["cp"]["all"]["energy_imported"] == 12 + assert result["cp"]["all"]["energy_exported"] == 3 + assert result["cp"]["all"]["keep"] == "x" + + # missing module and section are created + assert result["cp"]["cp1"]["energy_imported"] == 7 + assert result["hc"]["all"]["energy_imported"] == 2 + + # invalid totals are ignored + assert "cp_invalid" not in result["cp"] + assert "invalid_section" not in result + + # unrelated data stays unchanged + assert result["cp"]["cp9"]["keep_cp"] is True + assert result["meta"]["unchanged"] is True + + +@pytest.mark.parametrize( + "entries, period, period_format, expected", + [ + ( + [ + {"timestamp": 1711929600, "value": "april_1"}, + {"timestamp": 1712016000, "value": "april_2"}, + {"timestamp": 1714608000, "value": "may_2"}, + ], + "202404", + "%Y%m", + {"timestamp": 1712016000, "value": "april_2"}, + ), + ( + [], + "202404", + "%Y%m", + None, + ), + ], +) +def test_get_last_entry_for_period(entries, period, period_format, expected): + # execution + result = _get_last_entry_for_period(entries, period, period_format) + + # evaluation + assert result == expected From bfd0344bd04d48bce147d8725aeefeff1e216082 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Tue, 25 Aug 2026 14:47:42 +0200 Subject: [PATCH 12/17] Refactor yearly-totals-calculaiton and improve performance --- .../measurement_logging/process_log.py | 107 +++++++++++------- .../measurement_logging/update_yields.py | 7 +- 2 files changed, 69 insertions(+), 45 deletions(-) diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index a8c1d81976..04a44b8924 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -1,11 +1,12 @@ from enum import Enum from copy import deepcopy -import datetime import json import logging from pathlib import Path from typing import Dict, List, Optional, Tuple, Union +from datetime import datetime from concurrent.futures import ProcessPoolExecutor +from concurrent.futures.process import BrokenProcessPool from helpermodules import timecheck from helpermodules.measurement_logging.write_log import (LegacySmartHomeLogData, create_entry, @@ -341,44 +342,25 @@ def get_yearly_log(year: str): if oldest_log_day is None or year < oldest_log_day[:4]: return {"entries": [], "names": {}, "colors": {}, "totals": {}} + results = generate_daily_totals_for_year(year) monthly_entries = [] monthly_names = {} monthly_colors = {} - this_month = timecheck.create_timestamp_YYYYMM() - month = f"{year}01" - - while month.startswith(year): - if month > this_month: - break - - if month < oldest_log_day[:6]: - month = timecheck.get_relative_date_string(month, month_offset=1) + for monthly_data in results: + if not isinstance(monthly_data, dict): continue - content = load_monthly_source_totals_content(month) - if content is None: - # aktuelle Monatswerte nur berechnen, historische Monate zusaetzlich speichern - # Fallback, wenn bei der Jahresauswertung ein Monat fehlt, - # dann wird dieser Monat berechnet und gespeichert - content = save_monthly_source_totals(month, None, saving=(month != this_month)) - - if isinstance(content, dict): - monthly_totals = content.get("totals") - monthly_entry = content.get("entry") - - if isinstance(monthly_totals, dict) and isinstance(monthly_entry, dict) and len(monthly_entry) > 0: - monthly_entry = deepcopy(monthly_entry) - monthly_entry["date"] = month - _apply_source_totals(monthly_entry, monthly_totals) - - monthly_entries.append(monthly_entry) - if isinstance(content.get("names"), dict): - monthly_names.update(content["names"]) - if isinstance(content.get("colors"), dict): - monthly_colors.update(content["colors"]) + result_entries = monthly_data.get("entries") + result_names = monthly_data.get("names") + result_colors = monthly_data.get("colors") - month = timecheck.get_relative_date_string(month, month_offset=1) + if isinstance(result_entries, list) and len(result_entries) > 0: + monthly_entries.extend(result_entries) + if isinstance(result_names, dict): + monthly_names.update(result_names) + if isinstance(result_colors, dict): + monthly_colors.update(result_colors) if len(monthly_entries) > 0: data = {"entries": monthly_entries, "names": monthly_names, "colors": monthly_colors} @@ -819,7 +801,7 @@ def _get_last_entry_for_period(entries: List, period: str, period_format: str) - # Suche den letzten Eintrag in der Liste, der dem angegebenen Zeitraum entspricht. for entry in reversed(entries): if isinstance(entry, dict) and isinstance(entry.get("timestamp"), (int, float)): - entry_period = datetime.datetime.fromtimestamp(entry["timestamp"]).strftime(period_format) + entry_period = datetime.fromtimestamp(entry["timestamp"]).strftime(period_format) if entry_period == period: return entry @@ -871,17 +853,58 @@ def _oldest_log_day() -> Optional[str]: return None -def generate_daily_totals_for_current_year(): +def generate_daily_totals_for_year(year: str): + if not (len(year) == 4 and year.isdigit()): + log.debug(f"Ungültiges Jahr für Jahres-Summen: {year}") + return [] months_list = [] - current_year = timecheck.create_timestamp_YYYY() - current_month = timecheck.create_timestamp_YYYYMM()[4:6] + if current_year == year: + # aktuelles Jahr + current_month = timecheck.create_timestamp_YYYYMM()[4:6] + for month in range(1, int(current_month)): + month_str = f"{current_year}{month:02d}" + months_list.append(month_str) + else: + # historisches Jahr + for month in range(1, 13): + month_str = f"{year}{month:02d}" + months_list.append(month_str) + try: + with ProcessPoolExecutor() as executor: + results = list(executor.map(get_monthly_parallel, months_list)) - for month in range(1, int(current_month)): - month_str = f"{current_year}{month:02d}" - months_list.append(month_str) + except BrokenProcessPool: + # Fängt Out of Memory: Killed process ab + print(f"Beim vorgenerieren der daily totals fürs Jahr {year} " + f"ist ein Worker-Prozess unerwartet gestorben!") - with ProcessPoolExecutor() as executor: - executor.map(get_monthly_log, months_list) + log.debug(f"Tages-Summen für das Jahr {year} wurden berechnet und gespeichert.") + return results + + +def get_monthly_parallel(month: str): + this_month = timecheck.create_timestamp_YYYYMM()[4:6] + monthly_entries = [] + monthly_names = {} + monthly_colors = {} - log.debug("Tages-Summen für das aktuelle Jahr wurden berechnet und gespeichert.") + content = load_monthly_source_totals_content(month) + if content is None: + content = save_monthly_source_totals(month, None, saving=(month != this_month)) + + if isinstance(content, dict): + monthly_totals = content.get("totals") + monthly_entry = content.get("entry") + + if isinstance(monthly_totals, dict) and isinstance(monthly_entry, dict) and len(monthly_entry) > 0: + monthly_entry = deepcopy(monthly_entry) + monthly_entry["date"] = month + _apply_source_totals(monthly_entry, monthly_totals) + + monthly_entries.append(monthly_entry) + if isinstance(content.get("names"), dict): + monthly_names.update(content["names"]) + if isinstance(content.get("colors"), dict): + monthly_colors.update(content["colors"]) + return {"entries": monthly_entries, "names": monthly_names, "colors": monthly_colors} diff --git a/packages/helpermodules/measurement_logging/update_yields.py b/packages/helpermodules/measurement_logging/update_yields.py index 69f3ce1926..675c5a4e11 100644 --- a/packages/helpermodules/measurement_logging/update_yields.py +++ b/packages/helpermodules/measurement_logging/update_yields.py @@ -9,7 +9,7 @@ load_monthly_source_totals_content, save_daily_source_totals, get_monthly_log, - generate_daily_totals_for_current_year) + generate_daily_totals_for_year) log = logging.getLogger(__name__) @@ -52,8 +52,9 @@ def update_pv_monthly_yearly_yields(daily_totals: Dict) -> None: folder = _get_parent_path()/"data"/"daily_totals" if not folder.exists(): - # Nur wenn es noch keine Tages-Totals gibt, werden diese berechnet und gespeichert. - generate_daily_totals_for_current_year() + # Nur wenn es noch keine Tages-Totals-Folder gibt, + # werden die totals fürs aktuelle Jahr berechnet und gespeichert. + generate_daily_totals_for_year(timecheck.create_timestamp_YYYY()) monthly_totals = _get_pv_monthly_yields(daily_totals) yearly_totals = _get_pv_yearly_yields(monthly_totals) From a86ab4d9daf660884a1ebb98f750cfa4ae687f21 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Wed, 26 Aug 2026 07:39:40 +0200 Subject: [PATCH 13/17] fixes --- .../helpermodules/measurement_logging/process_log.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index 04a44b8924..e79d22c360 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -857,8 +857,12 @@ def generate_daily_totals_for_year(year: str): if not (len(year) == 4 and year.isdigit()): log.debug(f"Ungültiges Jahr für Jahres-Summen: {year}") return [] - months_list = [] + current_year = timecheck.create_timestamp_YYYY() + if year > current_year: + return [] + + months_list = [] if current_year == year: # aktuelles Jahr current_month = timecheck.create_timestamp_YYYYMM()[4:6] @@ -871,20 +875,20 @@ def generate_daily_totals_for_year(year: str): month_str = f"{year}{month:02d}" months_list.append(month_str) try: - with ProcessPoolExecutor() as executor: + with ProcessPoolExecutor(max_workers=2) as executor: results = list(executor.map(get_monthly_parallel, months_list)) except BrokenProcessPool: - # Fängt Out of Memory: Killed process ab print(f"Beim vorgenerieren der daily totals fürs Jahr {year} " f"ist ein Worker-Prozess unerwartet gestorben!") + results = [] log.debug(f"Tages-Summen für das Jahr {year} wurden berechnet und gespeichert.") return results def get_monthly_parallel(month: str): - this_month = timecheck.create_timestamp_YYYYMM()[4:6] + this_month = timecheck.create_timestamp_YYYYMM() monthly_entries = [] monthly_names = {} monthly_colors = {} From ed39a270737bea3011d03ff161341671d3931169 Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Wed, 26 Aug 2026 08:09:24 +0200 Subject: [PATCH 14/17] fix --- packages/helpermodules/measurement_logging/process_log.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index e79d22c360..31706c47f5 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -866,7 +866,7 @@ def generate_daily_totals_for_year(year: str): if current_year == year: # aktuelles Jahr current_month = timecheck.create_timestamp_YYYYMM()[4:6] - for month in range(1, int(current_month)): + for month in range(1, int(current_month) + 1): month_str = f"{current_year}{month:02d}" months_list.append(month_str) else: From 7355e941470605aeb9da4579d450e624b410cf1a Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Wed, 26 Aug 2026 08:46:00 +0200 Subject: [PATCH 15/17] Add Copolit Suggestions --- packages/helpermodules/measurement_logging/process_log.py | 6 +++--- .../measurement_logging/process_log_unit_test.py | 4 +++- packages/main.py | 2 +- 3 files changed, 7 insertions(+), 5 deletions(-) diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index 31706c47f5..fed38e7e98 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -272,7 +272,7 @@ def get_monthly_log(date: str): log.debug(f"Ungültiges Datum für Monats-Summen: {date}") return {"entries": [], "names": {}, "colors": {}, "totals": {}} # Nur Logs ab dem ältesten Tageslog auswerten - # Sonst werden unötige totals Werte gespeichert + # Sonst werden unnötige Totals-Werte gespeichert oldest_log_day = _oldest_log_day() if (oldest_log_day is None or date < oldest_log_day[:6]): # Jahr und Monat @@ -879,8 +879,8 @@ def generate_daily_totals_for_year(year: str): results = list(executor.map(get_monthly_parallel, months_list)) except BrokenProcessPool: - print(f"Beim vorgenerieren der daily totals fürs Jahr {year} " - f"ist ein Worker-Prozess unerwartet gestorben!") + log.exception(f"Beim vorgenerieren der daily totals fürs Jahr {year} " + f"ist ein Worker-Prozess unerwartet gestorben!") results = [] log.debug(f"Tages-Summen für das Jahr {year} wurden berechnet und gespeichert.") diff --git a/packages/helpermodules/measurement_logging/process_log_unit_test.py b/packages/helpermodules/measurement_logging/process_log_unit_test.py index a800ec91de..58034dbf98 100644 --- a/packages/helpermodules/measurement_logging/process_log_unit_test.py +++ b/packages/helpermodules/measurement_logging/process_log_unit_test.py @@ -5,6 +5,7 @@ from unittest.mock import Mock, mock_open import pytest import datetime +import tempfile from helpermodules.measurement_logging.process_log import ( analyse_percentage, @@ -432,8 +433,9 @@ def relative_date_string(date_value, day_offset=0, month_offset=0): get_totals_mock = Mock(return_value={"mocked": "totals"}) monkeypatch.setattr("helpermodules.measurement_logging.process_log._oldest_log_day", Mock(return_value="20240401")) + monkeypatch.setattr("helpermodules.measurement_logging.process_log._get_data_folder_path", - Mock(return_value="/tmp")) + Mock(return_value=tempfile.mkdtemp(prefix="process_log_test_"))) monkeypatch.setattr("helpermodules.measurement_logging.process_log.timecheck.create_timestamp_YYYYMM", Mock(return_value="202406")) monkeypatch.setattr("helpermodules.measurement_logging.process_log.timecheck.create_timestamp_YYYYMMDD", diff --git a/packages/main.py b/packages/main.py index 390189f925..e2a2d6d066 100755 --- a/packages/main.py +++ b/packages/main.py @@ -240,7 +240,7 @@ def handler_midnight(self): prev_month = timecheck.get_relative_date_string(today, month_offset=-1)[:6] # Neuer Monat hat angefangen, daher Monats Totals speichern if today[6:8] == "01": - save_monthly_source_totals(prev_month ,None, saving=True) + save_monthly_source_totals(prev_month, None, saving=True) thread_errors_path = Path(Path(__file__).resolve().parents[1]/"ramdisk"/"thread_errors.log") with thread_errors_path.open("w") as f: From f58e679a471a27ba403192e28bc368b6fbf7411a Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Thu, 27 Aug 2026 07:38:59 +0200 Subject: [PATCH 16/17] Zwischenstand --- .../measurement_logging/process_log.py | 140 +++++++++++++++++- packages/main.py | 6 +- 2 files changed, 135 insertions(+), 11 deletions(-) diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index fed38e7e98..04bf90bfb9 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -2,11 +2,14 @@ from copy import deepcopy import json import logging +import gc +import time from pathlib import Path from typing import Dict, List, Optional, Tuple, Union -from datetime import datetime from concurrent.futures import ProcessPoolExecutor from concurrent.futures.process import BrokenProcessPool +from multiprocessing import Pool, TimeoutError as MultiprocessingTimeoutError +from datetime import datetime, timedelta, date from helpermodules import timecheck from helpermodules.measurement_logging.write_log import (LegacySmartHomeLogData, create_entry, @@ -235,6 +238,8 @@ def get_totals(entries: List, process_entries: bool = True) -> Dict: def get_daily_log(date: str): + # generate_all_totals_files() + # return None data = _collect_daily_log_data(date) data["entries"] = _process_entries(data["entries"], CalculationType.ALL) data["totals"] = get_totals(data["entries"], False) @@ -680,7 +685,11 @@ def save_daily_source_totals(date: str, saving: bool = True): return None data = _collect_daily_log_data(date) source_entries = data.get("entries", []) - processed_entries = _process_entries(deepcopy(source_entries), calculation=CalculationType.ENERGY) + + # Keep one source snapshot for the daily output entry and process entries in place to avoid full-copy peaks. + source_daily_entry = _get_last_entry_for_period(source_entries, date, "%Y%m%d") + processed_entries = _process_entries(source_entries, calculation=CalculationType.ENERGY) + totals = get_totals(processed_entries, process_entries=False) analysed_data = _analyse_energy_source({ "entries": processed_entries, @@ -690,7 +699,6 @@ def save_daily_source_totals(date: str, saving: bool = True): totals = analysed_data["totals"] daily_entry = {} - source_daily_entry = _get_last_entry_for_period(source_entries, date, "%Y%m%d") if source_daily_entry is not None: daily_entry = deepcopy(source_daily_entry) daily_entry["date"] = date @@ -710,10 +718,17 @@ def save_daily_source_totals(date: str, saving: bool = True): if saving: totals_dir.mkdir(parents=True, exist_ok=True) - with open(str(filepath), "w") as jsonFile: - json.dump(content, jsonFile, ensure_ascii=False, indent=2) + if not filepath.is_file(): + with open(str(filepath), "w") as jsonFile: + json.dump(content, jsonFile, ensure_ascii=False, indent=2) - log.debug(f"Tages-Summen für {date} gespeichert in {filepath}") + log.debug(f"Tages-Summen für {date} gespeichert in {filepath}") + else: + log.debug(f"Tages-Summen existieren bereits: {filepath}") + + del data + del source_entries + gc.collect() return content except FILE_ERRORS: @@ -741,7 +756,7 @@ def load_daily_source_totals_content(date: str): log.exception(f"Fehler beim Laden der Tages-Summen für {date}") -def save_monthly_source_totals(date: str, data: Optional[Dict], saving: bool = True): +def save_monthly_source_totals(date: str, data: Optional[Dict] = None, saving: bool = True): try: # Hauptsächlich für Midnight-Handler # Wenn keine Daten übergeben werden, dann die Monatswerte berechnen @@ -875,7 +890,7 @@ def generate_daily_totals_for_year(year: str): month_str = f"{year}{month:02d}" months_list.append(month_str) try: - with ProcessPoolExecutor(max_workers=2) as executor: + with ProcessPoolExecutor() as executor: results = list(executor.map(get_monthly_parallel, months_list)) except BrokenProcessPool: @@ -912,3 +927,112 @@ def get_monthly_parallel(month: str): if isinstance(content.get("colors"), dict): monthly_colors.update(content["colors"]) return {"entries": monthly_entries, "names": monthly_names, "colors": monthly_colors} + + +def _save_daily_source_totals_worker(day: str) -> int: + """Worker-Wrapper: große Inhalte lokal schreiben, nur kleines Ack zurückgeben.""" + save_daily_source_totals(day, saving=True) + return 1 + + +def _save_monthly_source_totals_worker(month: str) -> int: + """Worker-Wrapper: große Inhalte lokal schreiben, nur kleines Ack zurückgeben.""" + save_monthly_source_totals(month, None, saving=True) + return 1 + + +def _drain_pool_results(iterator, total: int, label: str, timeout_seconds: int = 120, max_timeouts: int = 3): + """Konsumiert Pool-Ergebnisse mit Timeout, um stille Hänger sichtbar zu machen.""" + completed = 0 + timeout_count = 0 + log.info(f"{label}: starte drain ({total} Tasks, timeout={timeout_seconds}s).") + + while completed < total: + try: + iterator.next(timeout=timeout_seconds) + completed += 1 + timeout_count = 0 + if completed % 50 == 0 or completed == total: + log.info(f"{label}: Fortschritt {completed}/{total}.") + except MultiprocessingTimeoutError: + timeout_count += 1 + log.warning( + f"{label}: seit {timeout_seconds}s kein Ergebnis (" + f"{completed}/{total}). Timeout {timeout_count}/{max_timeouts}.") + if timeout_count >= max_timeouts: + raise TimeoutError( + f"{label}: keine neuen Ergebnisse nach {max_timeouts * timeout_seconds}s " + f"({completed}/{total} erledigt)." + ) + + log.info(f"{label}: drain abgeschlossen ({completed}/{total}).") + + +def generate_all_totals_files(): + + oldest_log_day = "20260101" # _oldest_log_day() + if oldest_log_day is None: + return + + start = datetime.strptime(oldest_log_day, "%Y%m%d").date() + today = date.today() + + tage = [ + (start + timedelta(days=i)).strftime("%Y%m%d") + for i in range((today - start).days + 1) + ] + + monate = sorted(set(tag[:6] for tag in tage)) + + totals_dir = Path(_get_data_folder_path()) / "daily_totals" + if not totals_dir.is_dir(): + print("daily_totals existiert noch nicht") + + start = time.perf_counter() + try: + print("_with_TIMOUT___POOL_TAGE_2_!!!!!") + with Pool(processes=2, maxtasksperchild=30) as pool: + # Ergebnisse streamen und mit Timeout überwachen, damit Hänger nicht still bleiben. + iterator = pool.imap_unordered(_save_daily_source_totals_worker, tage, chunksize=1) + log.info(f"daily_totals: iterator erstellt, starte drain für {len(tage)} Tage.") + _drain_pool_results(iterator, len(tage), "daily_totals") + + print("Erfolgreich beendet, ohne den RAM zu sprengen!") + + except Exception: + log.exception( + f"Beim Vorgenerieren der daily totals für Tage ist ein Fehler aufgetreten " + f"({len(tage)} Tage).") + ende = time.perf_counter() + dauer_t = ende - start + print(f"Dauer TAGE : {dauer_t:.6f} Sekunden") + # print(tage) + + totals_dir = Path(_get_data_folder_path()) / "monthly_totals" + if not totals_dir.is_dir(): + print("monthly_totals existiert noch nicht") + + start = time.perf_counter() + try: + + # Alle 5 Task wird der speicher komplett geleert, damit kein Prozess unbemerkt abstürzt + # und der RAM nicht überläuft. + # chunksize=1 -> pro Task nur ein Monat + # maxtasksperchild=5 -> nach 5 Tasks wird der Prozess beendet und ein neuer gestartet + print("POOL_Monate_2_!!!!!") + with Pool(processes=2, maxtasksperchild=5) as pool: + # Ergebnisse streamen und mit Timeout überwachen, damit Hänger nicht still bleiben. + iterator = pool.imap_unordered(_save_monthly_source_totals_worker, monate, chunksize=1) + log.info(f"monthly_totals: iterator erstellt, starte drain für {len(monate)} Monate.") + _drain_pool_results(iterator, len(monate), "monthly_totals") + + print("Erfolgreich beendet, ohne den RAM zu sprengen!") + + except Exception: + log.exception( + f"Beim Vorgenerieren der monthly totals für Monate ist ein Fehler aufgetreten " + f"({len(monate)} Monate).") + ende = time.perf_counter() + dauer_m = ende - start + print(f"Dauer MONATE : {dauer_m:.6f} Sekunden") + # print(monate) diff --git a/packages/main.py b/packages/main.py index e2a2d6d066..b525856bd1 100755 --- a/packages/main.py +++ b/packages/main.py @@ -182,9 +182,9 @@ def handler5MinAlgorithm(self): try: with ChangedValuesContext(loadvars_.event_module_update_completed): entries = save_log() - daily_totals = update_daily_yields(entries) - if daily_totals is not None: - update_pv_monthly_yearly_yields(daily_totals) + #daily_totals = update_daily_yields(entries) + #if daily_totals is not None: + # update_pv_monthly_yearly_yields(daily_totals) for cp in data.data.cp_data.values(): calc_energy_costs(cp) From fdbff05dfa5bb0649d34d6c261f1df707c00eb5a Mon Sep 17 00:00:00 2001 From: Alexander Hartung Date: Fri, 28 Aug 2026 09:47:12 +0200 Subject: [PATCH 17/17] Generate all totals in subprocess and add file operation versioning --- .../public/default-dynamic-security.json | 6 + .../generate_totals_subprocess.py | 85 +++++ .../measurement_logging/process_log.py | 322 +++++------------- .../process_log_unit_test.py | 80 ----- .../measurement_logging/update_yields.py | 13 +- .../missing_role_topics_test.py | 1 + packages/helpermodules/setdata.py | 3 + packages/helpermodules/update_config.py | 61 ++++ packages/main.py | 6 +- 9 files changed, 249 insertions(+), 328 deletions(-) create mode 100644 packages/helpermodules/measurement_logging/generate_totals_subprocess.py diff --git a/data/config/mosquitto/public/default-dynamic-security.json b/data/config/mosquitto/public/default-dynamic-security.json index 10ed378e2f..308341f675 100644 --- a/data/config/mosquitto/public/default-dynamic-security.json +++ b/data/config/mosquitto/public/default-dynamic-security.json @@ -183,6 +183,12 @@ "priority": 0, "allow": true }, + { + "acltype": "publishClientReceive", + "topic": "openWB/system/log_totals_generation_finished", + "priority": 0, + "allow": true + }, { "acltype": "publishClientReceive", "topic": "openWB/system/usage_terms_acknowledged", diff --git a/packages/helpermodules/measurement_logging/generate_totals_subprocess.py b/packages/helpermodules/measurement_logging/generate_totals_subprocess.py new file mode 100644 index 0000000000..0071e050da --- /dev/null +++ b/packages/helpermodules/measurement_logging/generate_totals_subprocess.py @@ -0,0 +1,85 @@ +from pathlib import Path +from datetime import date + +from typing import List +from control import data +from helpermodules import pub +from helpermodules.measurement_logging.process_log import (save_daily_source_totals, + save_monthly_source_totals) + +import logging +from logging.handlers import RotatingFileHandler + +handler = RotatingFileHandler( + filename="/var/www/html/openWB/ramdisk/generate_totals.log", + maxBytes=5 * 1024 * 1024, # 5 MB, + backupCount=1, +) + +logging.basicConfig( + handlers=[handler], + level=logging.DEBUG, + format="%(asctime)s [%(levelname)s] %(message)s", +) + +log = logging.getLogger(__name__) + + +def get_all_days_to_calc(): + try: + daily_log_dir = Path(__file__).resolve().parents[3] / "data" / "daily_log" + if not daily_log_dir.is_dir(): + return None + + today_stem = date.today().strftime("%Y%m%d") + daily_log_files = sorted( + [p for p in daily_log_dir.glob("*.json") + if p.stem.isdigit() and p.stem < today_stem], + key=lambda p: p.stem, + ) + + log.debug(f"Anzahl zu berechnender Tageslogs: {len(daily_log_files)}") + return daily_log_files + except Exception: + log.debug("Fehler beim Abrufen der Tageslogs. Es werden keine Tageslogs berechnet.") + return None + + +def get_all_months_to_calc(days: List[str] = None): + if days is None: + return None + current_month = date.today().strftime("%Y%m") + months = sorted({p.stem[:6] for p in days if len(p.stem) == 8 and p.stem[:6] < current_month}) + log.debug(f"Anzahl zu berechnender Monatslogs: {len(months)}") + return months + + +def generate_totals(): + errors = 0 + try: + days_to_calc = get_all_days_to_calc() or [] + months_to_calc = get_all_months_to_calc(days_to_calc) or [] + + for day in days_to_calc: + try: + save_daily_source_totals(day.stem, saving=True) + except Exception: + log.exception(f"Fehler beim Generieren der Tageserträge für {day.stem}, Tag wird übersprungen.") + errors += 1 + continue + + for month in months_to_calc: + try: + save_monthly_source_totals(month, saving=True) + except Exception: + log.exception(f"Fehler beim Generieren der Monatswerte für {month}, Monat wird übersprungen.") + errors += 1 + continue + + finally: + log.debug(f"Totals-Migration abgeschlossen. Fehlerhafte Logs: {errors}.") + pub.Pub().pub("openWB/set/system/log_totals_generation_finished", True) + + +if __name__ == "__main__": + generate_totals() diff --git a/packages/helpermodules/measurement_logging/process_log.py b/packages/helpermodules/measurement_logging/process_log.py index 04bf90bfb9..d9fb170822 100644 --- a/packages/helpermodules/measurement_logging/process_log.py +++ b/packages/helpermodules/measurement_logging/process_log.py @@ -2,15 +2,9 @@ from copy import deepcopy import json import logging -import gc -import time from pathlib import Path from typing import Dict, List, Optional, Tuple, Union -from concurrent.futures import ProcessPoolExecutor -from concurrent.futures.process import BrokenProcessPool -from multiprocessing import Pool, TimeoutError as MultiprocessingTimeoutError -from datetime import datetime, timedelta, date - +from datetime import datetime from helpermodules import timecheck from helpermodules.measurement_logging.write_log import (LegacySmartHomeLogData, create_entry, get_previous_entry) @@ -238,8 +232,6 @@ def get_totals(entries: List, process_entries: bool = True) -> Dict: def get_daily_log(date: str): - # generate_all_totals_files() - # return None data = _collect_daily_log_data(date) data["entries"] = _process_entries(data["entries"], CalculationType.ALL) data["totals"] = get_totals(data["entries"], False) @@ -287,7 +279,6 @@ def get_monthly_log(date: str): monthly_names = {} monthly_colors = {} - this_month = timecheck.create_timestamp_YYYYMM() today = timecheck.create_timestamp_YYYYMMDD() day = f"{date}01" @@ -301,7 +292,7 @@ def get_monthly_log(date: str): content = load_daily_source_totals_content(day) if content is None: - # aktuelle Tageswerte nur berechnen, historische Tage zusaetzlich speichern + # Dürfte eigentlich nie passieren... content = save_daily_source_totals(day, saving=(day != today)) if isinstance(content, dict): @@ -326,14 +317,6 @@ def get_monthly_log(date: str): data["totals"] = get_totals(data["entries"], False) data["totals"] = analyse_percentage_totals(data["entries"], data["totals"]) data = _analyse_energy_source(data) - - # Fallback für ältere Monate - # da wir den Monat jetzt schon berechnet haben, können wir ihn auch direkt speichern - # falls er noch nicht existiert. - filepath = Path(_get_data_folder_path()) / "monthly_totals" / f"{date}_totals.json" - if not filepath.is_file() and date != this_month and date >= oldest_log_day[:6]: - save_monthly_source_totals(date, data, saving=True) - return data # Fallback, wenn keine Daten vorhanden sind @@ -347,32 +330,47 @@ def get_yearly_log(year: str): if oldest_log_day is None or year < oldest_log_day[:4]: return {"entries": [], "names": {}, "colors": {}, "totals": {}} - results = generate_daily_totals_for_year(year) monthly_entries = [] monthly_names = {} monthly_colors = {} + this_month = timecheck.create_timestamp_YYYYMM() + month = f"{year}01" + + while month.startswith(year): + if month > this_month: + break - for monthly_data in results: - if not isinstance(monthly_data, dict): + if month < oldest_log_day[:6]: + month = timecheck.get_relative_date_string(month, month_offset=1) continue - result_entries = monthly_data.get("entries") - result_names = monthly_data.get("names") - result_colors = monthly_data.get("colors") + content = load_monthly_source_totals_content(month) + if content is None: + # Dürfte eigentlich nie passieren... + content = save_monthly_source_totals(month, None, saving=(month != this_month)) - if isinstance(result_entries, list) and len(result_entries) > 0: - monthly_entries.extend(result_entries) - if isinstance(result_names, dict): - monthly_names.update(result_names) - if isinstance(result_colors, dict): - monthly_colors.update(result_colors) + if isinstance(content, dict): + monthly_totals = content.get("totals") + monthly_entry = content.get("entry") + + if isinstance(monthly_totals, dict) and isinstance(monthly_entry, dict) and len(monthly_entry) > 0: + monthly_entry = deepcopy(monthly_entry) + monthly_entry["date"] = month + _apply_source_totals(monthly_entry, monthly_totals) + + monthly_entries.append(monthly_entry) + if isinstance(content.get("names"), dict): + monthly_names.update(content["names"]) + if isinstance(content.get("colors"), dict): + monthly_colors.update(content["colors"]) + + month = timecheck.get_relative_date_string(month, month_offset=1) if len(monthly_entries) > 0: data = {"entries": monthly_entries, "names": monthly_names, "colors": monthly_colors} data["totals"] = get_totals(data["entries"], False) data["totals"] = analyse_percentage_totals(data["entries"], data["totals"]) data = _analyse_energy_source(data) - return data # Fallback, wenn keine Daten vorhanden sind @@ -528,33 +526,59 @@ def calc_energy_imported_by_source(entry, names, message_key_filter: Optional[st def analyse_percentage_totals(entries, totals): - for section in ("hc", "cp"): - if "all" not in totals[section].keys(): - totals[section]["all"] = {} - for source in ("grid", "pv", "bat", "cp"): - totals["hc"]["all"].update({f"energy_imported_{source}": 0}) - for entry in entries: - if "hc" in entry.keys() and "all" in entry["hc"].keys(): - current_value = totals["hc"]["all"][f"energy_imported_{source}"] - add_value = entry["hc"]["all"].get(f"energy_imported_{source}", 0) - totals["hc"]["all"][f"energy_imported_{source}"] = decimal_add( - current_value, add_value) - for key in entry["cp"].keys(): - if f"energy_imported_{source}" in entry["cp"][key].keys(): - if totals["cp"][key].get(f"energy_imported_{source}") is None: - totals["cp"][key].update({f"energy_imported_{source}": 0}) - current_value = totals["cp"][key][f"energy_imported_{source}"] - add_value = entry["cp"][key][f"energy_imported_{source}"] - totals["cp"][key][f"energy_imported_{source}"] = decimal_add( - current_value, add_value) - for key, counter in entry["counter"].items(): - if counter["grid"] is False: - if totals["counter"][key].get(f"energy_imported_{source}") is None: - totals["counter"][key].update({f"energy_imported_{source}": 0}) - current_value = totals["counter"][key][f"energy_imported_{source}"] - add_value = counter[f"energy_imported_{source}"] - totals["counter"][key][f"energy_imported_{source}"] = decimal_add( + sources = ("grid", "pv", "bat", "cp") + + def ensure_zero_source_keys(module_totals: Dict): + if isinstance(module_totals, dict): + for source in sources: + module_totals[f"energy_imported_{source}"] = 0 + + try: + for section in ("hc", "cp"): + if "all" not in totals[section].keys(): + totals[section]["all"] = {} + for source in sources: + totals["hc"]["all"].update({f"energy_imported_{source}": 0}) + for entry in entries: + if "hc" in entry.keys() and "all" in entry["hc"].keys(): + current_value = totals["hc"]["all"][f"energy_imported_{source}"] + add_value = entry["hc"]["all"].get(f"energy_imported_{source}", 0) + totals["hc"]["all"][f"energy_imported_{source}"] = decimal_add( current_value, add_value) + for key in entry["cp"].keys(): + if f"energy_imported_{source}" in entry["cp"][key].keys(): + if totals["cp"][key].get(f"energy_imported_{source}") is None: + totals["cp"][key].update({f"energy_imported_{source}": 0}) + current_value = totals["cp"][key][f"energy_imported_{source}"] + add_value = entry["cp"][key][f"energy_imported_{source}"] + totals["cp"][key][f"energy_imported_{source}"] = decimal_add( + current_value, add_value) + for key, counter in entry["counter"].items(): + if counter["grid"] is False: + if totals["counter"][key].get(f"energy_imported_{source}") is None: + totals["counter"][key].update({f"energy_imported_{source}": 0}) + current_value = totals["counter"][key][f"energy_imported_{source}"] + add_value = counter[f"energy_imported_{source}"] + totals["counter"][key][f"energy_imported_{source}"] = decimal_add( + current_value, add_value) + except Exception: + log.exception("Fehler beim Berechnen der Summen der Energiequellen") + # Im Fehlerfall werden die Totals auf 0 gesetzt + # -> dann wird nur die Ladung/Entladung-Leistung angezeigt. + # -> sprich keine Aufteilung der Energiequellen, sondern nur die Gesamtwerte. + totals.setdefault("hc", {}) + totals["hc"].setdefault("all", {}) + ensure_zero_source_keys(totals["hc"]["all"]) + + totals.setdefault("cp", {}) + for key, module_totals in totals["cp"].items(): + ensure_zero_source_keys(module_totals) + + totals.setdefault("counter", {}) + for key, module_totals in totals["counter"].items(): + ensure_zero_source_keys(module_totals) + + return totals return totals @@ -718,17 +742,11 @@ def save_daily_source_totals(date: str, saving: bool = True): if saving: totals_dir.mkdir(parents=True, exist_ok=True) - if not filepath.is_file(): - with open(str(filepath), "w") as jsonFile: - json.dump(content, jsonFile, ensure_ascii=False, indent=2) + with open(str(filepath), "w") as jsonFile: + json.dump(content, jsonFile, ensure_ascii=False, indent=2) - log.debug(f"Tages-Summen für {date} gespeichert in {filepath}") - else: - log.debug(f"Tages-Summen existieren bereits: {filepath}") + log.debug(f"Tages-Summen für {date} gespeichert in {filepath}") - del data - del source_entries - gc.collect() return content except FILE_ERRORS: @@ -866,173 +884,3 @@ def _oldest_log_day() -> Optional[str]: except Exception: log.exception("Fehler beim Ermitteln des ältesten Tageslogs") return None - - -def generate_daily_totals_for_year(year: str): - if not (len(year) == 4 and year.isdigit()): - log.debug(f"Ungültiges Jahr für Jahres-Summen: {year}") - return [] - - current_year = timecheck.create_timestamp_YYYY() - if year > current_year: - return [] - - months_list = [] - if current_year == year: - # aktuelles Jahr - current_month = timecheck.create_timestamp_YYYYMM()[4:6] - for month in range(1, int(current_month) + 1): - month_str = f"{current_year}{month:02d}" - months_list.append(month_str) - else: - # historisches Jahr - for month in range(1, 13): - month_str = f"{year}{month:02d}" - months_list.append(month_str) - try: - with ProcessPoolExecutor() as executor: - results = list(executor.map(get_monthly_parallel, months_list)) - - except BrokenProcessPool: - log.exception(f"Beim vorgenerieren der daily totals fürs Jahr {year} " - f"ist ein Worker-Prozess unerwartet gestorben!") - results = [] - - log.debug(f"Tages-Summen für das Jahr {year} wurden berechnet und gespeichert.") - return results - - -def get_monthly_parallel(month: str): - this_month = timecheck.create_timestamp_YYYYMM() - monthly_entries = [] - monthly_names = {} - monthly_colors = {} - - content = load_monthly_source_totals_content(month) - if content is None: - content = save_monthly_source_totals(month, None, saving=(month != this_month)) - - if isinstance(content, dict): - monthly_totals = content.get("totals") - monthly_entry = content.get("entry") - - if isinstance(monthly_totals, dict) and isinstance(monthly_entry, dict) and len(monthly_entry) > 0: - monthly_entry = deepcopy(monthly_entry) - monthly_entry["date"] = month - _apply_source_totals(monthly_entry, monthly_totals) - - monthly_entries.append(monthly_entry) - if isinstance(content.get("names"), dict): - monthly_names.update(content["names"]) - if isinstance(content.get("colors"), dict): - monthly_colors.update(content["colors"]) - return {"entries": monthly_entries, "names": monthly_names, "colors": monthly_colors} - - -def _save_daily_source_totals_worker(day: str) -> int: - """Worker-Wrapper: große Inhalte lokal schreiben, nur kleines Ack zurückgeben.""" - save_daily_source_totals(day, saving=True) - return 1 - - -def _save_monthly_source_totals_worker(month: str) -> int: - """Worker-Wrapper: große Inhalte lokal schreiben, nur kleines Ack zurückgeben.""" - save_monthly_source_totals(month, None, saving=True) - return 1 - - -def _drain_pool_results(iterator, total: int, label: str, timeout_seconds: int = 120, max_timeouts: int = 3): - """Konsumiert Pool-Ergebnisse mit Timeout, um stille Hänger sichtbar zu machen.""" - completed = 0 - timeout_count = 0 - log.info(f"{label}: starte drain ({total} Tasks, timeout={timeout_seconds}s).") - - while completed < total: - try: - iterator.next(timeout=timeout_seconds) - completed += 1 - timeout_count = 0 - if completed % 50 == 0 or completed == total: - log.info(f"{label}: Fortschritt {completed}/{total}.") - except MultiprocessingTimeoutError: - timeout_count += 1 - log.warning( - f"{label}: seit {timeout_seconds}s kein Ergebnis (" - f"{completed}/{total}). Timeout {timeout_count}/{max_timeouts}.") - if timeout_count >= max_timeouts: - raise TimeoutError( - f"{label}: keine neuen Ergebnisse nach {max_timeouts * timeout_seconds}s " - f"({completed}/{total} erledigt)." - ) - - log.info(f"{label}: drain abgeschlossen ({completed}/{total}).") - - -def generate_all_totals_files(): - - oldest_log_day = "20260101" # _oldest_log_day() - if oldest_log_day is None: - return - - start = datetime.strptime(oldest_log_day, "%Y%m%d").date() - today = date.today() - - tage = [ - (start + timedelta(days=i)).strftime("%Y%m%d") - for i in range((today - start).days + 1) - ] - - monate = sorted(set(tag[:6] for tag in tage)) - - totals_dir = Path(_get_data_folder_path()) / "daily_totals" - if not totals_dir.is_dir(): - print("daily_totals existiert noch nicht") - - start = time.perf_counter() - try: - print("_with_TIMOUT___POOL_TAGE_2_!!!!!") - with Pool(processes=2, maxtasksperchild=30) as pool: - # Ergebnisse streamen und mit Timeout überwachen, damit Hänger nicht still bleiben. - iterator = pool.imap_unordered(_save_daily_source_totals_worker, tage, chunksize=1) - log.info(f"daily_totals: iterator erstellt, starte drain für {len(tage)} Tage.") - _drain_pool_results(iterator, len(tage), "daily_totals") - - print("Erfolgreich beendet, ohne den RAM zu sprengen!") - - except Exception: - log.exception( - f"Beim Vorgenerieren der daily totals für Tage ist ein Fehler aufgetreten " - f"({len(tage)} Tage).") - ende = time.perf_counter() - dauer_t = ende - start - print(f"Dauer TAGE : {dauer_t:.6f} Sekunden") - # print(tage) - - totals_dir = Path(_get_data_folder_path()) / "monthly_totals" - if not totals_dir.is_dir(): - print("monthly_totals existiert noch nicht") - - start = time.perf_counter() - try: - - # Alle 5 Task wird der speicher komplett geleert, damit kein Prozess unbemerkt abstürzt - # und der RAM nicht überläuft. - # chunksize=1 -> pro Task nur ein Monat - # maxtasksperchild=5 -> nach 5 Tasks wird der Prozess beendet und ein neuer gestartet - print("POOL_Monate_2_!!!!!") - with Pool(processes=2, maxtasksperchild=5) as pool: - # Ergebnisse streamen und mit Timeout überwachen, damit Hänger nicht still bleiben. - iterator = pool.imap_unordered(_save_monthly_source_totals_worker, monate, chunksize=1) - log.info(f"monthly_totals: iterator erstellt, starte drain für {len(monate)} Monate.") - _drain_pool_results(iterator, len(monate), "monthly_totals") - - print("Erfolgreich beendet, ohne den RAM zu sprengen!") - - except Exception: - log.exception( - f"Beim Vorgenerieren der monthly totals für Monate ist ein Fehler aufgetreten " - f"({len(monate)} Monate).") - ende = time.perf_counter() - dauer_m = ende - start - print(f"Dauer MONATE : {dauer_m:.6f} Sekunden") - # print(monate) diff --git a/packages/helpermodules/measurement_logging/process_log_unit_test.py b/packages/helpermodules/measurement_logging/process_log_unit_test.py index 58034dbf98..79dcaef1ab 100644 --- a/packages/helpermodules/measurement_logging/process_log_unit_test.py +++ b/packages/helpermodules/measurement_logging/process_log_unit_test.py @@ -4,8 +4,6 @@ from typing import Dict from unittest.mock import Mock, mock_open import pytest -import datetime -import tempfile from helpermodules.measurement_logging.process_log import ( analyse_percentage, @@ -15,7 +13,6 @@ _collect_daily_log_data, _get_last_entry_for_period, _apply_source_totals, - get_monthly_log, calc_energy_imported_by_source, analyse_percentage_totals, CalculationType) @@ -399,83 +396,6 @@ def test_collect_daily_log_data_json_decode_error(monkeypatch): assert result == expected_result -def test_get_monthly_log_aggregates_days_and_saves_missing_month_totals(monkeypatch): - # setup - month = "202404" - today = "20240402" - - def relative_date_string(date_value, day_offset=0, month_offset=0): - base = json.loads(json.dumps(date_value)) - if month_offset: - dt = datetime.datetime.strptime(base, "%Y%m") - year = dt.year + ((dt.month - 1 + month_offset) // 12) - month_value = ((dt.month - 1 + month_offset) % 12) + 1 - return f"{year:04d}{month_value:02d}" - dt = datetime.datetime.strptime(base, "%Y%m%d") - return (dt + datetime.timedelta(days=day_offset)).strftime("%Y%m%d") - - # daily_totals Mockdaten - day1_content = { - "totals": {"cp": {"all": {"energy_imported": 10}}}, - "entry": {"timestamp": 1, "cp": {"all": {}}}, - "names": {"cp1": "Ladepunkt 1"}, - "colors": {"cp1": "#123456"} - } - day2_content = { - "totals": {"cp": {"all": {"energy_imported": 20}}}, - "entry": {"timestamp": 2, "cp": {"all": {}}}, - "names": {"cp2": "Ladepunkt 2"}, - "colors": {"cp2": "#654321"} - } - - save_daily_mock = Mock(return_value=day2_content) - save_monthly_mock = Mock() - get_totals_mock = Mock(return_value={"mocked": "totals"}) - - monkeypatch.setattr("helpermodules.measurement_logging.process_log._oldest_log_day", Mock(return_value="20240401")) - - monkeypatch.setattr("helpermodules.measurement_logging.process_log._get_data_folder_path", - Mock(return_value=tempfile.mkdtemp(prefix="process_log_test_"))) - monkeypatch.setattr("helpermodules.measurement_logging.process_log.timecheck.create_timestamp_YYYYMM", - Mock(return_value="202406")) - monkeypatch.setattr("helpermodules.measurement_logging.process_log.timecheck.create_timestamp_YYYYMMDD", - Mock(return_value=today)) - monkeypatch.setattr("helpermodules.measurement_logging.process_log.timecheck.get_relative_date_string", - relative_date_string) - - # Nur für den ersten Tag gibt es bereits eine daily_totals-Datei - monkeypatch.setattr( - "helpermodules.measurement_logging.process_log.load_daily_source_totals_content", - lambda day: day1_content if day == "20240401" else None) - monkeypatch.setattr("helpermodules.measurement_logging.process_log.save_daily_source_totals", save_daily_mock) - monkeypatch.setattr("helpermodules.measurement_logging.process_log.get_totals", get_totals_mock) - monkeypatch.setattr( - "helpermodules.measurement_logging.process_log.analyse_percentage_totals", - lambda entries, totals: {"mocked": "analysed_totals", "count": len(entries), "totals": totals}) - monkeypatch.setattr("helpermodules.measurement_logging.process_log._analyse_energy_source", lambda data: data) - monkeypatch.setattr("helpermodules.measurement_logging.process_log.save_monthly_source_totals", save_monthly_mock) - - # execution - result = get_monthly_log(month) - - # evaluation - assert len(result["entries"]) == 2 - assert result["entries"][0]["date"] == "20240401" - assert result["entries"][0]["cp"]["all"]["energy_imported"] == 10 - assert result["entries"][1]["date"] == "20240402" - assert result["entries"][1]["cp"]["all"]["energy_imported"] == 20 - assert result["names"] == {"cp1": "Ladepunkt 1", "cp2": "Ladepunkt 2"} - assert result["colors"] == {"cp1": "#123456", "cp2": "#654321"} - assert result["totals"] == {"mocked": "analysed_totals", "count": 2, "totals": {"mocked": "totals"}} - - # Für den 2. Tag gibt es noch keine daily_totals-Datei - # -> soll auch nicht gespeichert werden, da der 2. Tag der aktuelle Tag ist - save_daily_mock.assert_called_once_with("20240402", saving=False) - - # monthly_totals sollen gespeichert werden - save_monthly_mock.assert_called_once_with(month, result, saving=True) - - def test_apply_source_totals_updates_existing_and_creates_missing_sections(): # setup entry = { diff --git a/packages/helpermodules/measurement_logging/update_yields.py b/packages/helpermodules/measurement_logging/update_yields.py index 675c5a4e11..438574c251 100644 --- a/packages/helpermodules/measurement_logging/update_yields.py +++ b/packages/helpermodules/measurement_logging/update_yields.py @@ -8,8 +8,7 @@ load_daily_source_totals_content, load_monthly_source_totals_content, save_daily_source_totals, - get_monthly_log, - generate_daily_totals_for_year) + get_monthly_log) log = logging.getLogger(__name__) @@ -49,12 +48,10 @@ def update_pv_monthly_yearly_yields(daily_totals: Dict) -> None: """ veröffentlicht die monatlichen und jährlichen Erträge für PV """ - - folder = _get_parent_path()/"data"/"daily_totals" - if not folder.exists(): - # Nur wenn es noch keine Tages-Totals-Folder gibt, - # werden die totals fürs aktuelle Jahr berechnet und gespeichert. - generate_daily_totals_for_year(timecheck.create_timestamp_YYYY()) + # pv_data und pv_all_data werden nur aktualisiert, wenn das Update der Logfiles abgeschlossen ist. + if not data.data.system_data["system"].data["log_totals_generation_finished"]: + log.debug("Update der Logfiles läuft noch. Monatliche und jährliche PV-Erträge werden nicht aktualisiert.") + return monthly_totals = _get_pv_monthly_yields(daily_totals) yearly_totals = _get_pv_yearly_yields(monthly_totals) diff --git a/packages/helpermodules/mosquitto_dynsec/missing_role_topics_test.py b/packages/helpermodules/mosquitto_dynsec/missing_role_topics_test.py index b06822ab14..cd6f53ab21 100644 --- a/packages/helpermodules/mosquitto_dynsec/missing_role_topics_test.py +++ b/packages/helpermodules/mosquitto_dynsec/missing_role_topics_test.py @@ -99,6 +99,7 @@ def _get_data_path() -> Path: 'openWB/optional/int_display/pin_active', 'openWB/optional/int_display/pin_code', 'openWB/system/datastore_version', + 'openWB/system/file_operation_version', 'openWB/system/device/+/component/+/simulation', 'openWB/system/device/+/component/+/simulation/power_present', 'openWB/system/device/+/component/+/simulation/present_exported', diff --git a/packages/helpermodules/setdata.py b/packages/helpermodules/setdata.py index 7c03072896..61e600c560 100644 --- a/packages/helpermodules/setdata.py +++ b/packages/helpermodules/setdata.py @@ -1045,6 +1045,7 @@ def process_system_topic(self, msg: mqtt.MQTTMessage): "openWB/set/system/perform_update" in msg.topic or "openWB/set/system/wizard_done" in msg.topic or "openWB/set/system/update_in_progress" in msg.topic or + "openWB/set/system/log_totals_generation_finished" in msg.topic or "openWB/set/system/backup_cloud/backup_before_update" in msg.topic or "openWB/set/system/installAssistantDone" in msg.topic or "openWB/set/system/dataprotection_acknowledged" in msg.topic or @@ -1059,6 +1060,8 @@ def process_system_topic(self, msg: mqtt.MQTTMessage): self._validate_value(msg, float) elif "openWB/set/system/datastore_version" in msg.topic: self._validate_value(msg, int, [(0, UpdateConfig.DATASTORE_VERSION)], collection=list) + elif "openWB/set/system/file_operation_version" in msg.topic: + self._validate_value(msg, int, [(0, UpdateConfig.FILE_OPERATION_VERSION)], collection=list) elif "openWB/set/system/GetRemoteSupport" in msg.topic: # Server-Topic enthält kein json-Payload. payload = msg.payload.decode("utf-8") diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 48d2f72b1e..58605ca4ff 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -9,6 +9,8 @@ from pathlib import Path import re import time +import sys +import subprocess from typing import List, Optional from paho.mqtt.client import Client as MqttClient, MQTTMessage @@ -60,6 +62,8 @@ class UpdateConfig: DATASTORE_VERSION = 137 + FILE_OPERATION_VERSION = 0 + valid_topic = [ "^openWB/bat/config/bat_control_activated$", "^openWB/bat/config/power_limit_mode$", @@ -526,10 +530,12 @@ class UpdateConfig: "^openWB/system/device/[0-9]+/component/[0-9]+/simulation/timestamp_present$", "^openWB/system/device/[0-9]+/config$", "^openWB/system/device/module_update_completed$", + "^openWB/system/file_operation_version$", "^openWB/system/hostname$", "^openWB/system/io/[0-9]+/config$", "^openWB/system/ip_address$", "^openWB/system/lastlivevaluesJson$", + "^openWB/system/log_totals_generation_finished$", "^openWB/system/mac_address$", "^openWB/system/mqtt/bridge/[0-9]+$", "^openWB/system/mqtt/valid_partner_ids$", @@ -727,6 +733,7 @@ def update(self): try: # erst breaking changes auflösen, sonst sind alte Topics schon gelöscht self.__solve_breaking_changes() + self.__solve_breaking_changes_filesystem() self.__remove_outdated_topics() self._remove_invalid_topics() self.__pub_missing_defaults() @@ -827,6 +834,54 @@ def __solve_breaking_changes(self) -> None: pub_system_message( {}, "Fehler bei der Aktualisierung der Konfiguration des Brokers.", MessageType.ERROR) + def __solve_breaking_changes_filesystem(self) -> None: + """ """ + file_operation_version = decode_payload(self.all_received_topics.get("openWB/system/file_operation_version")) + if file_operation_version is None or isinstance(file_operation_version, int): + file_operation_version = list(range(file_operation_version or self.FILE_OPERATION_VERSION+1)) + self.__update_topic("openWB/system/file_operation_version", file_operation_version) + log.debug(f"current file operation version: {file_operation_version}") + log.debug(f"target file operation version: {self.FILE_OPERATION_VERSION}") + for version in list(range(self.FILE_OPERATION_VERSION+1)): + try: + if version not in file_operation_version: + log.debug(f"upgrading File Operation version '{version}'") + getattr(self, f"upgrade_file_operation_{version}")() + except AttributeError: + log.error(f"missing upgrade function! '{version}'") + except Exception: + log.exception("Fehler bei der Aktualisierung des Brokers.") + pub_system_message( + {}, "Fehler bei der Aktualisierung der Konfiguration des Brokers.", MessageType.ERROR) + + def upgrade_file_operation_0(self) -> None: + """ + Generiere die Totals-Summen für Tage und Monate + """ + + self.__update_topic("openWB/system/log_totals_generation_finished", False) + try: + _generate_totals_subprocess = subprocess.Popen( + [ + sys.executable, + "-m", + "helpermodules.measurement_logging.generate_totals_subprocess" + ], + cwd="/var/www/html/openWB/packages", + start_new_session=True, + stdin=subprocess.DEVNULL, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + close_fds=True + ) + + log.debug("generate_totals_subprocess gestartet, PID: %s", _generate_totals_subprocess.pid) + print(f"generate_totals_subprocess gestartet, PID: {_generate_totals_subprocess.pid}") + except Exception: + _generate_totals_subprocess = None + log.exception("Fehler beim Starten des generate_totals_subprocess.") + self._append_file_operation_version(0) + def _loop_all_received_topics(self, callback) -> None: modified_topics = {} for topic, payload in self.all_received_topics.items(): @@ -845,6 +900,12 @@ def _append_datastore_version(self, version: int) -> None: datastore_versions.append(version) self.__update_topic("openWB/system/datastore_version", datastore_versions) + def _append_file_operation_version(self, version: int) -> None: + file_operation_versions = decode_payload(self.all_received_topics.get("openWB/system/file_operation_version")) + if version not in file_operation_versions: + file_operation_versions.append(version) + self.__update_topic("openWB/system/file_operation_version", file_operation_versions) + def upgrade_datastore_0(self) -> None: def upgrade(topic: str, payload) -> Optional[dict]: modified_topics = {} diff --git a/packages/main.py b/packages/main.py index b525856bd1..e2a2d6d066 100755 --- a/packages/main.py +++ b/packages/main.py @@ -182,9 +182,9 @@ def handler5MinAlgorithm(self): try: with ChangedValuesContext(loadvars_.event_module_update_completed): entries = save_log() - #daily_totals = update_daily_yields(entries) - #if daily_totals is not None: - # update_pv_monthly_yearly_yields(daily_totals) + daily_totals = update_daily_yields(entries) + if daily_totals is not None: + update_pv_monthly_yearly_yields(daily_totals) for cp in data.data.cp_data.values(): calc_energy_costs(cp)