Fix utility meter next_reset shifting forward on entity rename (#170957)

Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
Franck Nijhof
2026-05-17 10:31:56 -04:00
committed by GitHub
co-authored by Claude Opus 4.6
parent 9a1fd913bf
commit ff7964bcfc
2 changed files with 67 additions and 4 deletions
@@ -406,11 +406,12 @@ class UtilityMeterSensor(RestoreSensor):
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 @@ class UtilityMeterSensor(RestoreSensor):
# 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 @@ class UtilityMeterSensor(RestoreSensor):
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):
@@ -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": "<class 'decimal.Decimal'>",
"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(