From 0ae035f84998eec58221e642b197bf83d73d7c23 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 17 May 2026 09:34:11 +0000 Subject: [PATCH 1/3] Fix utility meter next_reset shifting forward on entity rename Co-Authored-By: Claude Opus 4.6 (1M context) --- .../components/utility_meter/sensor.py | 12 +++-- tests/components/utility_meter/test_sensor.py | 54 +++++++++++++++++++ 2 files changed, 62 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 683a069fc8b0d..84fb50bd37e48 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -406,11 +406,12 @@ def __init__( self._current_tz = None self._config_scheduler() - def _config_scheduler(self): + def _config_scheduler(self, start_time=None): self.scheduler = ( CronSim( self._cron_pattern, - dt_util.now( + start_time + or dt_util.now( dt_util.get_default_time_zone() ), # we need timezone for DST purposes (see issue #102984) ) @@ -608,8 +609,6 @@ async def async_added_to_hass(self) -> None: # and we need to reconfigure the scheduler self._current_tz = self.hass.config.time_zone - await self._program_reset() - self.async_on_remove( async_dispatcher_connect( self.hass, SIGNAL_RESET_METER, self.async_reset_meter @@ -628,6 +627,11 @@ async def async_added_to_hass(self) -> None: if last_sensor_data.status == COLLECTING: # Null lambda to allow cancelling the collection on tariff change self._collecting = lambda: None + # Reconfigure the scheduler from the restored last_reset so that + # next_reset is not shifted forward on entity restore/rename. + self._config_scheduler(self._last_reset) + + await self._program_reset() @callback def async_source_tracking(event): diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index f684cdb16a0bb..148f57104f832 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -1799,6 +1799,60 @@ async def test_tz_changes(hass: HomeAssistant) -> None: assert state.attributes.get("next_reset") != "2024-10-28T00:00:00+01:00" +async def test_next_reset_not_shifted_on_restore(hass: HomeAssistant) -> None: + """Test that a missed reset fires on restore after entity rename. + + When an entity is restored (e.g. after rename) and a reset was missed, + the scheduler should catch up from last_reset rather than starting from + now(), which would skip the missed reset entirely. + """ + last_reset = "2024-10-27T00:00:00+00:00" + + mock_restore_cache_with_extra_data( + hass, + [ + ( + State( + "sensor.energy_bill", + "3", + attributes={ + ATTR_STATUS: COLLECTING, + ATTR_LAST_RESET: last_reset, + ATTR_UNIT_OF_MEASUREMENT: UnitOfEnergy.KILO_WATT_HOUR, + }, + ), + { + "native_value": { + "__type": "", + "decimal_str": "3", + }, + "native_unit_of_measurement": "kWh", + "last_reset": last_reset, + "last_period": "0", + "last_valid_state": "3", + "status": "collecting", + "input_device_class": "energy", + }, + ), + ], + ) + + # Restore at noon on Oct 28 - the Oct 28 midnight reset was missed + now = dt_util.parse_datetime("2024-10-28T12:00:00+00:00") + with freeze_time(now): + assert await async_setup_component(hass, DOMAIN, gen_config("daily")) + await hass.async_block_till_done() + hass.bus.async_fire(EVENT_HOMEASSISTANT_STARTED) + await hass.async_block_till_done() + + state = hass.states.get("sensor.energy_bill") + assert state is not None + # The missed reset should have fired as a catch-up, updating last_reset. + # Without the fix, last_reset stays at the original value because + # the scheduler starts from now() and skips the missed period. + assert state.attributes.get("last_reset") != last_reset + + async def test_self_reset_daily(hass: HomeAssistant) -> None: """Test daily reset of meter.""" await _test_self_reset( From b192f76d98298ce58991f1aafd789175a576560a Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 17 May 2026 10:08:27 +0000 Subject: [PATCH 2/3] Convert restored last_reset to local time for scheduler DST handling Co-Authored-By: Claude Opus 4.6 (1M context) --- homeassistant/components/utility_meter/sensor.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 84fb50bd37e48..d917953064a87 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -629,7 +629,9 @@ async def async_added_to_hass(self) -> None: self._collecting = lambda: None # Reconfigure the scheduler from the restored last_reset so that # next_reset is not shifted forward on entity restore/rename. - self._config_scheduler(self._last_reset) + self._config_scheduler( + dt_util.as_local(self._last_reset) if self._last_reset else None + ) await self._program_reset() From 18c1ac4d211e6ff58cf4f66731bb1c8ac0ee3b57 Mon Sep 17 00:00:00 2001 From: Franck Nijhof Date: Sun, 17 May 2026 10:22:17 +0000 Subject: [PATCH 3/3] Add type annotation to _config_scheduler and strengthen test assertions Co-Authored-By: Claude Opus 4.6 (1M context) --- homeassistant/components/utility_meter/sensor.py | 2 +- tests/components/utility_meter/test_sensor.py | 9 ++++++--- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index d917953064a87..dac8bdc0791d0 100644 --- a/homeassistant/components/utility_meter/sensor.py +++ b/homeassistant/components/utility_meter/sensor.py @@ -406,7 +406,7 @@ def __init__( self._current_tz = None self._config_scheduler() - def _config_scheduler(self, start_time=None): + def _config_scheduler(self, start_time: datetime | None = None) -> None: self.scheduler = ( CronSim( self._cron_pattern, diff --git a/tests/components/utility_meter/test_sensor.py b/tests/components/utility_meter/test_sensor.py index 148f57104f832..7bcb2f5f8b412 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -1847,10 +1847,13 @@ async def test_next_reset_not_shifted_on_restore(hass: HomeAssistant) -> None: state = hass.states.get("sensor.energy_bill") assert state is not None - # The missed reset should have fired as a catch-up, updating last_reset. - # Without the fix, last_reset stays at the original value because - # the scheduler starts from now() and skips the missed period. + # The missed reset should have fired as a catch-up, updating last_reset + # and recording the previous period value. Without the fix, last_reset + # stays at the original value because the scheduler starts from now() + # and skips the missed period entirely. assert state.attributes.get("last_reset") != last_reset + assert state.attributes.get("last_period") == "3" + assert state.state == "0" async def test_self_reset_daily(hass: HomeAssistant) -> None: