Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 10 additions & 4 deletions homeassistant/components/utility_meter/sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
)
Expand Down Expand Up @@ -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
Expand All @@ -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
)

Comment thread
frenck marked this conversation as resolved.
await self._program_reset()

@callback
def async_source_tracking(event):
Expand Down
57 changes: 57 additions & 0 deletions tests/components/utility_meter/test_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down