diff --git a/homeassistant/components/utility_meter/sensor.py b/homeassistant/components/utility_meter/sensor.py index 683a069fc8b0d..dac8bdc0791d0 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: datetime | None = None) -> 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,13 @@ 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( + dt_util.as_local(self._last_reset) if self._last_reset else None + ) + + 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..7bcb2f5f8b412 100644 --- a/tests/components/utility_meter/test_sensor.py +++ b/tests/components/utility_meter/test_sensor.py @@ -1799,6 +1799,63 @@ 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 + # 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: """Test daily reset of meter.""" await _test_self_reset(