From bdc754d12fd08e0da18d3156642c695f4bf67bd2 Mon Sep 17 00:00:00 2001 From: Andreas Date: Wed, 16 Sep 2026 13:19:18 +0200 Subject: [PATCH] fix(measurement): restore JSON records into the existing singleton --- src/akkudoktoreos/measurement/measurement.py | 18 +++-- tests/test_measurement_file_restore.py | 80 ++++++++++++++++++++ 2 files changed, 91 insertions(+), 7 deletions(-) create mode 100644 tests/test_measurement_file_restore.py diff --git a/src/akkudoktoreos/measurement/measurement.py b/src/akkudoktoreos/measurement/measurement.py index 6cd25415..688d6480 100644 --- a/src/akkudoktoreos/measurement/measurement.py +++ b/src/akkudoktoreos/measurement/measurement.py @@ -6,6 +6,7 @@ data records for measurements. The measurements can be added programmatically or imported from a file or JSON string. """ +import json from pathlib import Path from typing import Any, Optional @@ -313,14 +314,17 @@ class Measurement(SingletonMixin, DataImportMixin, DataSequence[MeasurementDataR if not measurement_file_path.exists(): return False try: - # Validate into a temporary instance - loaded = self.__class__.model_validate_json( - measurement_file_path.read_text(encoding="utf-8") - ) - - # Explicitly add data records to the existing singleton - for record in loaded.records: + # Measurement is a singleton; validating another Measurement + # returns this instance instead of restoring serialized records. + payload = json.loads(measurement_file_path.read_text(encoding="utf-8")) + # Validate the complete file before modifying the live records. + records = [ + MeasurementDataRecord.model_validate(data) + for data in payload.get("records", []) + ] + for record in records: await self.insert_by_datetime(record) except Exception as e: logger.exception("Cannot load measurements") + return False return True diff --git a/tests/test_measurement_file_restore.py b/tests/test_measurement_file_restore.py new file mode 100644 index 00000000..bcca16f2 --- /dev/null +++ b/tests/test_measurement_file_restore.py @@ -0,0 +1,80 @@ +"""Regression coverage for measurement persistence through the JSON fallback.""" + +import json +from unittest.mock import AsyncMock + +import pytest + +from akkudoktoreos.core.coreabc import get_measurement +from akkudoktoreos.core.dataabc import DataSequence + + +@pytest.fixture +def file_measurements(config_eos, tmp_path, monkeypatch): + measurement = get_measurement() + config_eos.measurement.load_emr_keys = ["meter"] + config_eos.general.data_folder_path = tmp_path + config_eos.database.provider = None + measurement._db_reset_state() + monkeypatch.setattr(DataSequence, "save", AsyncMock(return_value=False)) + monkeypatch.setattr(DataSequence, "load", AsyncMock(return_value=False)) + try: + yield measurement + finally: + measurement._db_reset_state() + + +@pytest.mark.asyncio +async def test_json_roundtrip_restores_records_into_existing_singleton(file_measurements): + measurement = file_measurements + await measurement.update_value("2026-09-16T08:00:00Z", "meter", 123.5) + await measurement.update_value("2026-09-16T09:00:00Z", "meter", 124.0) + assert await measurement.save() + measurement._db_reset_state() + + assert await measurement.load() + assert get_measurement() is measurement + assert [record["meter"] for record in measurement.records] == [123.5, 124.0] + assert [record.date_time.in_timezone("UTC").hour for record in measurement.records] == [8, 9] + # Reloading must update matching timestamps, without duplicating them. + assert await measurement.load() + assert len(measurement.records) == 2 + + +@pytest.mark.asyncio +async def test_json_restore_preserves_other_in_memory_timestamps(file_measurements): + measurement = file_measurements + await measurement.update_value("2026-09-16T08:00:00Z", "meter", 123.5) + assert await measurement.save() + measurement._db_reset_state() + await measurement.update_value("2026-09-16T09:00:00Z", "meter", 124.0) + assert await measurement.load() + assert [record["meter"] for record in measurement.records] == [123.5, 124.0] + + +@pytest.mark.asyncio +@pytest.mark.parametrize("payload", [ + "invalid json", + json.dumps({"records": [ + {"date_time": "2026-09-16T08:00:00Z", "meter": 123.5}, + {"date_time": "not a date", "meter": 124.0}, + ]}), +]) +async def test_invalid_json_does_not_partially_restore(file_measurements, payload): + measurement = file_measurements + measurement._measurement_file_path().write_text(payload, encoding="utf-8") + assert await measurement.load() is False + assert measurement.records == [] + + +@pytest.mark.asyncio +async def test_database_success_does_not_read_json(file_measurements, monkeypatch): + measurement = file_measurements + measurement._measurement_file_path().write_text("invalid json", encoding="utf-8") + monkeypatch.setattr(DataSequence, "load", AsyncMock(return_value=True)) + assert await measurement.load() + + +@pytest.mark.asyncio +async def test_missing_json_reports_not_loaded(file_measurements): + assert await file_measurements.load() is False