SAFETY-123: the day plan's hour loop steps in UTC and always ends
checks / unit (pull_request) Successful in 48m40s
infra-pin / fresh (pull_request) Successful in 2s

dayplan._hour_slices() added an hour to the local wall clock and read it
back. Without tzdata (tariff._EUTimezone) 02:00 on the spring change reads
as 00:00 UTC again, so the loop never advanced and the list grew until the
process died. Each step now adds the time left in the local hour to the UTC
instant. Ordinary days give the old slices; the spring day gives 23 slices
and the autumn day 25. No option, no text change.

test_safety123.py: every quarter of 27-29 March and 30 Oct-1 Nov 2027 (and
the US change days) for the slices, dayplan.plan() and
Controller._mode_roles(), each call under a hard time limit.

Co-Authored-By: Claude <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Bdqr7PiaHcBnzPituu2yTC
This commit is contained in:
glenn schrooyen
2026-10-27 08:58:54 +01:00
co-authored by Claude
parent a1d07030f8
commit d2820e8b9b
3 changed files with 564 additions and 4 deletions
@@ -0,0 +1,35 @@
# SAFETY-123 - the day plan's hour loop steps in UTC
**Status: for review.** Tests: `test_safety123.py`. No option, no text change.
`dayplan._hour_slices()` stepped to the next local hour by adding an hour to
the local wall clock and converting back. Where the zone has no tzdata
(`tariff._EUTimezone`: the CI image, a dev host, an image that loses tzdata)
the missing 02:00 of the spring change reads as summer time, which is
00:00 UTC again. At 28 March 2027 00:00 UTC `t` did not move and the list
grew until the process died. Control and the web page share one event loop,
so the whole add-on froze. `dayplan.plan()` calls it on every plan, before it
looks at a battery's mode, so a site with all batteries on Solar first reaches
it as well, if the prices give a plan.
The shipped image has tzdata (`amd64-base:3.20`), where this zone gives the
right answer, so a running add-on is not known to be hit. The fix is for any
host without it. (AUDIT-01 F4, card SAFETY-120, is the same defect.)
**Decision.** Each step adds the time left in the local hour to the UTC
instant. It is at least a microsecond, so `t` always advances and the loop
ends, in any zone, with or without tzdata. A day of 23 hours gives 23 slices
and a day of 25 hours gives 25, each at most an hour, none across a local
hour. On ordinary days the slices are the old ones.
**One change on the autumn day.** The old function (with tzdata) made one
two-hour slice for the doubled hour 02:00-03:00; it now makes two of an hour,
both labelled 2. `plan()` sums per slice, so only a forecast with sun in that
hour could tell the difference. There is none.
**Other loops.** The other code that walks local hours or days steps in UTC
or on the calendar date (`static_tariff`, `price_forecast`, `price_history`,
`price`, `maintenance`, `homeview.local_day`, `solarfc._bounds`), or has a
fixed bound. None can stall. One known inexactness stays, outside this fix:
`solarfc` spreads a state-only forecast over hours since local midnight, so on
the two change days its sun curve is an hour off the clock.