From a29e7ebbb65aee969b7a7e530b9cfd4a9d1f66bc Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 23 Oct 2026 14:26:05 +0200 Subject: [PATCH 1/2] chargelog: migrate old chargelogs to int priority --- .../chargelog_priority_migration_test.py | 104 ++++++++++++++++++ packages/helpermodules/update_config.py | 31 +++++- 2 files changed, 134 insertions(+), 1 deletion(-) create mode 100644 packages/helpermodules/chargelog_priority_migration_test.py diff --git a/packages/helpermodules/chargelog_priority_migration_test.py b/packages/helpermodules/chargelog_priority_migration_test.py new file mode 100644 index 0000000000..8900f861ec --- /dev/null +++ b/packages/helpermodules/chargelog_priority_migration_test.py @@ -0,0 +1,104 @@ +import json +from unittest.mock import Mock + +import pytest + +from helpermodules import update_config +from helpermodules.update_config import UpdateConfig + + +def make_update_config(tmp_path): + config = UpdateConfig() + config.base_path = tmp_path + config.all_received_topics = { + "openWB/system/file_operation_version": [0], + } + return config + + +def test_charge_log_priority_migration_preserves_numbers_and_is_idempotent(tmp_path, monkeypatch): + config = make_update_config(tmp_path) + directory = tmp_path / "data" / "charge_log" + directory.mkdir(parents=True) + entries = [ + {"vehicle": {"prio": value}, "data": {"costs": 1.23}} + for value in (True, False, 1, 2, 7, None, "1") + ] + [{"vehicle": {}}, {}, {"vehicle": None}] + path = directory / "202610.json" + path.write_text(json.dumps(entries)) + untouched = directory / "202609.json" + untouched.write_text('[{"vehicle": {"prio": 3}}]') + original_untouched = untouched.read_bytes() + + config.upgrade_file_operation_1() + + migrated = json.loads(path.read_text()) + assert [entry["vehicle"]["prio"] for entry in migrated[:7]] == [1, 2, 1, 2, 7, None, "1"] + assert all(type(entry["vehicle"]["prio"]) is int for entry in migrated[:5]) + assert all(entry["data"] == {"costs": 1.23} for entry in migrated[:7]) + assert migrated[7:] == entries[7:] + assert untouched.read_bytes() == original_untouched + assert config.all_received_topics["openWB/system/file_operation_version"] == [0, 1] + + writer = Mock(side_effect=AssertionError("Bereits migrierte Dateien dürfen nicht erneut geschrieben werden.")) + monkeypatch.setattr(update_config, "write_and_check", writer) + config.upgrade_file_operation_1() + writer.assert_not_called() + + +def test_charge_log_priority_migration_without_files(tmp_path): + config = make_update_config(tmp_path) + config.upgrade_file_operation_1() + assert config.all_received_topics["openWB/system/file_operation_version"] == [0, 1] + + +def test_charge_log_priority_migration_runs_once_via_regular_file_upgrade(tmp_path, monkeypatch): + config = make_update_config(tmp_path) + config.all_received_topics["openWB/system/log_data_ready"] = True + directory = tmp_path / "data" / "charge_log" + directory.mkdir(parents=True) + path = directory / "202610.json" + path.write_text('[{"vehicle": {"prio": true}}]') + migration = Mock(wraps=config.upgrade_file_operation_1) + monkeypatch.setattr(config, "upgrade_file_operation_1", migration) + + config._UpdateConfig__solve_breaking_changes_filesystem() + config._UpdateConfig__solve_breaking_changes_filesystem() + + migration.assert_called_once_with() + assert type(json.loads(path.read_text())[0]["vehicle"]["prio"]) is int + assert config.all_received_topics["openWB/system/file_operation_version"] == [0, 1] + + +@pytest.mark.parametrize("content", ["{broken", '{"entries": []}']) +def test_charge_log_priority_migration_retries_failed_files(tmp_path, content): + config = make_update_config(tmp_path) + directory = tmp_path / "data" / "charge_log" + directory.mkdir(parents=True) + path = directory / "202610.json" + path.write_text(content) + + with pytest.raises(RuntimeError): + config.upgrade_file_operation_1() + + assert path.read_text() == content + assert config.all_received_topics["openWB/system/file_operation_version"] == [0] + path.write_text('[{"vehicle": {"prio": false}}]') + config.upgrade_file_operation_1() + assert json.loads(path.read_text())[0]["vehicle"]["prio"] == 2 + assert config.all_received_topics["openWB/system/file_operation_version"] == [0, 1] + + +def test_charge_log_priority_migration_detects_failed_write_of_true_to_one(tmp_path, monkeypatch): + config = make_update_config(tmp_path) + directory = tmp_path / "data" / "charge_log" + directory.mkdir(parents=True) + path = directory / "202610.json" + path.write_text('[{"vehicle": {"prio": true}}]') + monkeypatch.setattr(update_config, "write_and_check", Mock()) + + with pytest.raises(RuntimeError): + config.upgrade_file_operation_1() + + assert json.loads(path.read_text())[0]["vehicle"]["prio"] is True + assert config.all_received_topics["openWB/system/file_operation_version"] == [0] diff --git a/packages/helpermodules/update_config.py b/packages/helpermodules/update_config.py index 3394623b07..3940866485 100644 --- a/packages/helpermodules/update_config.py +++ b/packages/helpermodules/update_config.py @@ -65,7 +65,7 @@ class UpdateConfig: DATASTORE_VERSION = 154 - FILE_OPERATION_VERSION = 0 + FILE_OPERATION_VERSION = 1 valid_topic = [ "^openWB/bat/config/bat_control_activated$", @@ -996,6 +996,35 @@ def upgrade_file_operation_0(self) -> None: _generate_totals_subprocess = None log.exception("Fehler beim Starten des generate_totals_subprocess.") + def upgrade_file_operation_1(self) -> None: + """Boolesche Fahrzeug-Priorität in historischen Ladeprotokollen in Rang 1/2 umwandeln.""" + failed_files = [] + for file_path in sorted((self.base_path / "data" / "charge_log").glob("*.json")): + try: + with file_path.open(encoding="utf-8") as file: + entries = json.load(file) + if not isinstance(entries, list): + raise ValueError("Das Ladeprotokoll enthält keine Liste.") + changed = False + for entry in entries: + vehicle = entry.get("vehicle") if isinstance(entry, dict) else None + if isinstance(vehicle, dict) and isinstance(vehicle.get("prio"), bool): + vehicle["prio"] = 1 if vehicle["prio"] else 2 + changed = True + if changed: + write_and_check(str(file_path), entries) + with file_path.open(encoding="utf-8") as file: + written_entries = json.load(file) + # bool und int müssen unterschieden werden (True == 1 in Python). + if json.dumps(written_entries, sort_keys=True) != json.dumps(entries, sort_keys=True): + raise ValueError("Die Prioritäts-Migration wurde nicht vollständig gespeichert.") + except (OSError, ValueError): + log.exception("Fehler bei der Prioritäts-Migration des Ladeprotokolls %s", file_path) + failed_files.append(str(file_path)) + if failed_files: + raise RuntimeError(f"Ladeprotokoll-Prioritäten konnten nicht migriert werden: {failed_files}") + self._append_file_operation_version(1) + def _loop_all_received_topics(self, callback) -> None: modified_topics = {} for topic, payload in self.all_received_topics.items(): From fbec283739565b66d3cf82d4af8266cf9e9cd48e Mon Sep 17 00:00:00 2001 From: LKuemmel Date: Fri, 23 Oct 2026 14:24:10 +0200 Subject: [PATCH 2/2] chargelog: log priority --- packages/control/chargelog/chargelog.py | 6 ++-- packages/control/chargelog/priority_test.py | 36 +++++++++++++++++++ .../control/chargelog/process_chargelog.py | 10 ------ .../control/chargepoint/chargepoint_data.py | 2 +- .../control/counter_all/counter_all_data.py | 2 ++ .../counter_all/loadmanagement_prio.py | 9 ++++- .../data_migration/data_migration.py | 2 +- 7 files changed, 52 insertions(+), 15 deletions(-) create mode 100644 packages/control/chargelog/priority_test.py diff --git a/packages/control/chargelog/chargelog.py b/packages/control/chargelog/chargelog.py index 674159cdc9..9c5f9cb86f 100644 --- a/packages/control/chargelog/chargelog.py +++ b/packages/control/chargelog/chargelog.py @@ -1,4 +1,5 @@ from control.chargemode import Chargemode +from modules.common.component_type import ComponentType from helpermodules.utils.json_file_handler import write_and_check from helpermodules import timecheck import copy @@ -32,7 +33,7 @@ # "id": 1, # "name": "Auto", # "chargemode": instant_charging, -# "prio": False, +# "prio": 2, # "rfid": "123" # "soc_at_start": 50, # "soc_at_end": 60, @@ -121,7 +122,8 @@ def collect_data(chargepoint): log_data.soc_at_start = get_value_or_default(lambda: charging_ev.data.get.soc) log_data.ev = get_value_or_default(lambda: chargepoint.data.set.charging_ev_data.num, 0) - log_data.prio = get_value_or_default(lambda: chargepoint.data.control_parameter.prio, False) + log_data.prio = get_value_or_default( + lambda: data.data.counter_all_data.get_loadmanagement_priority(ComponentType.VEHICLE, log_data.ev)) log_data.rfid = get_value_or_default(lambda: chargepoint.data.set.rfid) log_data.odometer = get_value_or_default(lambda: charging_ev.data.get.odometer) log_data.imported_since_mode_switch = get_value_or_default( diff --git a/packages/control/chargelog/priority_test.py b/packages/control/chargelog/priority_test.py new file mode 100644 index 0000000000..9842b7f767 --- /dev/null +++ b/packages/control/chargelog/priority_test.py @@ -0,0 +1,36 @@ +from unittest.mock import Mock + +import pytest + +from control import data +from control.chargelog import chargelog +from control.chargepoint.chargepoint import Chargepoint +from control.ev.ev import Ev + + +@pytest.mark.parametrize("entries, expected", [ + ([{"type": "vehicle", "id": 9}], 1), + ([{"type": "consumer", "id": 9}, {"type": "vehicle", "id": 9}], 2), + ([{"type": "consumer", "id": 0}, {"type": "group", "children": [ + {"type": "vehicle", "id": 1}, {"type": "vehicle", "id": 9}]}], 2), + ([{"type": "vehicle", "id": 1}], None), +]) +def test_collect_data_logs_shared_priority_rank(entries, expected, monkeypatch): + data.data_init(Mock()) + data.data.counter_all_data.data.get.loadmanagement_prios = entries + cp = Chargepoint(4, None) + vehicle = Ev(9) + cp.data.set.charging_ev_data = vehicle + cp.data.get.plug_state = True + cp.data.get.charge_state = True + cp.data.get.imported = 1000 + cp.data.get.exported = 0 + + chargelog.collect_data(cp) + + assert cp.data.set.log.ev == 9 + assert cp.data.set.log.prio == expected + assert cp.data.set.log.prio is None or type(cp.data.set.log.prio) is int + monkeypatch.setattr(chargelog, "calc_energy_costs", Mock()) + entry = chargelog._create_entry(cp, vehicle) + assert entry["vehicle"]["prio"] == expected diff --git a/packages/control/chargelog/process_chargelog.py b/packages/control/chargelog/process_chargelog.py index 9349db3e26..31ee4f2733 100644 --- a/packages/control/chargelog/process_chargelog.py +++ b/packages/control/chargelog/process_chargelog.py @@ -71,16 +71,6 @@ def get_log_data(request: Dict): (str(entry["vehicle"]["chargemode"]), str(request["filter"]["vehicle"]["chargemode"])) ) continue - if ( - "prio" in request["filter"]["vehicle"] and - request["filter"]["vehicle"]["prio"] is not entry["vehicle"]["prio"] - ): - log.debug( - "Verwerfe Eintrag wegen Priorität: %s != %s" % - (str(entry["vehicle"]["prio"]), str(request["filter"]["vehicle"]["prio"])) - ) - continue - # wenn wir hier ankommen, passt der Eintrag zum Filter log_data["entries"].append(entry) log_data["totals"] = get_totals_of_filtered_log_data(log_data) diff --git a/packages/control/chargepoint/chargepoint_data.py b/packages/control/chargepoint/chargepoint_data.py index 2b17b6e853..2899fef19d 100644 --- a/packages/control/chargepoint/chargepoint_data.py +++ b/packages/control/chargepoint/chargepoint_data.py @@ -91,7 +91,7 @@ class Log: timestamp_start_charging: Optional[float] = None timestamp_mode_switch: Optional[float] = None ev: int = -1 - prio: bool = False + prio: Optional[int] = None rfid: Optional[str] = None serial_number: Optional[str] = None soc_at_start: Optional[int] = None diff --git a/packages/control/counter_all/counter_all_data.py b/packages/control/counter_all/counter_all_data.py index e97ce145df..0ce4d877d6 100644 --- a/packages/control/counter_all/counter_all_data.py +++ b/packages/control/counter_all/counter_all_data.py @@ -104,6 +104,8 @@ def update_linked_counter_hierarchy(self, class LoadmanagementPrioProtocol(Protocol): + def get_loadmanagement_priority(self, type: ComponentType, id: int) -> Optional[int]: ... + @property def data(self) -> CounterAllData: ... def add_loadmanagement_prio_item(self, type: ComponentType, id: int) -> None: ... diff --git a/packages/control/counter_all/loadmanagement_prio.py b/packages/control/counter_all/loadmanagement_prio.py index 571f189f2d..1353bdaf81 100644 --- a/packages/control/counter_all/loadmanagement_prio.py +++ b/packages/control/counter_all/loadmanagement_prio.py @@ -1,5 +1,5 @@ import logging -from typing import Dict, Generator, List, Tuple +from typing import Dict, Generator, List, Optional, Tuple from control.chargepoint.chargepoint import Chargepoint from control.consumer.usage import ConsumerUsage, NOT_CONTROLLED @@ -11,6 +11,13 @@ class LoadmanagementPrioMixin: + def get_loadmanagement_priority(self: LoadmanagementPrioProtocol, + type: ComponentType, id: int) -> Optional[int]: + for rank, entry in enumerate(self.data.get.loadmanagement_prios, start=1): + if self._has_loadmanagement_prio_item(type, id, [entry]): + return rank + return None + def add_loadmanagement_prio_item(self: LoadmanagementPrioProtocol, type: ComponentType, id: int) -> None: self.data.get.loadmanagement_prios.append({"type": type.value, "id": id}) diff --git a/packages/helpermodules/data_migration/data_migration.py b/packages/helpermodules/data_migration/data_migration.py index 510448599d..b50cbb0f06 100644 --- a/packages/helpermodules/data_migration/data_migration.py +++ b/packages/helpermodules/data_migration/data_migration.py @@ -244,7 +244,7 @@ def conv_1_9_datetimes(datetime_str): "id": vehicle_id, "name": vehicle_name, "chargemode": chargemode, - "prio": False, + "prio": 2, "rfid": rfid }, "time":