diff --git a/homeassistant/components/anglian_water/coordinator.py b/homeassistant/components/anglian_water/coordinator.py index 81c845420a6..7c2308148b6 100644 --- a/homeassistant/components/anglian_water/coordinator.py +++ b/homeassistant/components/anglian_water/coordinator.py @@ -92,6 +92,7 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]): _LOGGER.debug("Updating statistics for the first time") usage_sum = 0.0 last_stats_time = None + allow_update_last_stored_hour = False else: if not meter.readings or len(meter.readings) == 0: _LOGGER.debug("No recent usage statistics found, skipping update") @@ -107,6 +108,7 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]): continue start = dt_util.as_local(parsed_read_at) - timedelta(hours=1) _LOGGER.debug("Getting statistics at %s", start) + stats: dict[str, list[Any]] = {} for end in (start + timedelta(seconds=1), None): stats = await get_instance(self.hass).async_add_executor_job( statistics_during_period, @@ -127,15 +129,28 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]): "Not found, trying to find oldest statistic after %s", start, ) - assert stats - def _safe_get_sum(records: list[Any]) -> float: - if records and "sum" in records[0]: - return float(records[0]["sum"]) - return 0.0 + if not stats or not stats.get(usage_statistic_id): + _LOGGER.debug( + "Could not find existing statistics during period lookup for %s, " + "falling back to last stored statistic", + usage_statistic_id, + ) + allow_update_last_stored_hour = True + last_records = last_stat[usage_statistic_id] + usage_sum = float(last_records[0].get("sum") or 0.0) + last_stats_time = last_records[0]["start"] + else: + allow_update_last_stored_hour = False + records = stats[usage_statistic_id] - usage_sum = _safe_get_sum(stats.get(usage_statistic_id, [])) - last_stats_time = stats[usage_statistic_id][0]["start"] + def _safe_get_sum(records: list[Any]) -> float: + if records and "sum" in records[0]: + return float(records[0]["sum"]) + return 0.0 + + usage_sum = _safe_get_sum(records) + last_stats_time = records[0]["start"] usage_statistics = [] @@ -148,7 +163,13 @@ class AnglianWaterUpdateCoordinator(DataUpdateCoordinator[None]): ) continue start = dt_util.as_local(parsed_read_at) - timedelta(hours=1) - if last_stats_time is not None and start.timestamp() <= last_stats_time: + if last_stats_time is not None and ( + start.timestamp() < last_stats_time + or ( + start.timestamp() == last_stats_time + and not allow_update_last_stored_hour + ) + ): continue usage_state = max(0, read["consumption"] / 1000) usage_sum = max(0, read["read"]) diff --git a/tests/components/anglian_water/test_coordinator.py b/tests/components/anglian_water/test_coordinator.py index 1072b531218..45d03a11f31 100644 --- a/tests/components/anglian_water/test_coordinator.py +++ b/tests/components/anglian_water/test_coordinator.py @@ -1,6 +1,7 @@ """Tests for the Anglian Water coordinator.""" -from unittest.mock import AsyncMock +from datetime import timedelta +from unittest.mock import AsyncMock, patch from pyanglianwater.meter import SmartMeter import pytest @@ -162,3 +163,92 @@ async def test_coordinator_invalid_readings( "Could not parse read_at time also-invalid-date, skipping reading" in caplog.text ) + + +async def test_coordinator_subsequent_run_missing_period_statistics( + recorder_mock: Recorder, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_smart_meter: SmartMeter, + mock_anglian_water_client: AsyncMock, + caplog: pytest.LogCaptureFixture, +) -> None: + """Test the coordinator handles missing period lookup statistics.""" + coordinator = AnglianWaterUpdateCoordinator( + hass, mock_anglian_water_client, mock_config_entry + ) + await coordinator._async_update_data() + await async_wait_recording_done(hass) + + # Correct the latest already-stored reading. Fallback should still update + # this hour instead of skipping it. + mock_smart_meter.readings[-1] = { + "read_at": "2024-06-01T14:00:00", + "consumption": 35, + "read": 70, + } + + # Add a new later reading to ensure fallback also accepts newer entries. + mock_smart_meter.readings.append( + {"read_at": "2024-06-01T15:00:00", "consumption": 20, "read": 90} + ) + + with patch( + "homeassistant.components.anglian_water.coordinator.statistics_during_period", + return_value={}, + ): + await coordinator._async_update_data() + await async_wait_recording_done(hass) + + assert "Could not find existing statistics during period lookup" in caplog.text + + statistic_id = f"anglian_water:{ACCOUNT_NUMBER}_testsn_usage" + stats = await hass.async_add_executor_job( + get_last_statistics, hass, 1, statistic_id, True, {"sum"} + ) + assert stats[statistic_id][0]["sum"] >= 70 + + parsed_read_at = dt_util.parse_datetime("2024-06-01T14:00:00") + assert parsed_read_at is not None + corrected_start = dt_util.as_local(parsed_read_at) - timedelta(hours=1) + + corrected_stats = await hass.async_add_executor_job( + statistics_during_period, + hass, + corrected_start, + corrected_start + timedelta(seconds=1), + { + statistic_id, + }, + "hour", + None, + {"sum"}, + ) + assert corrected_stats[statistic_id][0]["sum"] == 70 + + +async def test_coordinator_period_statistics_without_sum( + recorder_mock: Recorder, + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_anglian_water_client: AsyncMock, +) -> None: + """Test period lookup records without sum are handled safely.""" + coordinator = AnglianWaterUpdateCoordinator( + hass, mock_anglian_water_client, mock_config_entry + ) + await coordinator._async_update_data() + await async_wait_recording_done(hass) + + statistic_id = f"anglian_water:{ACCOUNT_NUMBER}_testsn_usage" + with patch( + "homeassistant.components.anglian_water.coordinator.statistics_during_period", + return_value={statistic_id: [{"start": 0.0}]}, + ): + await coordinator._async_update_data() + await async_wait_recording_done(hass) + + stats = await hass.async_add_executor_job( + get_last_statistics, hass, 1, statistic_id, True, {"sum"} + ) + assert stats[statistic_id]