fix(time): futurize() must compare in HA-local wall time, not OS timezone - #335
fix(time): futurize() must compare in HA-local wall time, not OS timezone#335pluskal wants to merge 1 commit into
Conversation
…zone futurize() built 'today'/'now' from date.today()/datetime.now() (OS timezone) while parse_time()-derived times are naive in the HA-configured timezone. When the host TZ != HA TZ (e.g. host UTC, HA Europe/Prague), a just-fired time callback rescheduled itself to an instant that is 'future' in OS terms but already past in HA-local terms; async_track_point_in_time then re-fired it immediately in a tight loop. Observed as sunrise end_time callbacks re-firing for exactly the UTC-offset window and flooding MQTT with repeated turn_off commands. Anchor both sides of the comparison to HA-local wall time via homeassistant.util.dt (already imported).
There was a problem hiding this comment.
Pull request overview
This PR fixes a timezone mismatch in Model.futurize() by anchoring “now/today” to Home Assistant’s configured local wall time instead of the host/process timezone, preventing immediate re-fire loops when scheduling time-based callbacks.
Changes:
- Switch reference “now”/“today” to HA-local wall time via
homeassistant.util.dtto avoid OS-tz vs HA-tz drift. - Normalize timezone-aware inputs to HA-local naive datetimes before comparing/scheduling.
Comments suppressed due to low confidence (1)
custom_components/entity_controller/init.py:1709
- For consistency with the rest of the module (e.g., parse_time/make_naive), consider using make_naive() for the aware->HA-local-naive conversion instead of open-coding dt.as_local(...).replace(tzinfo=None).
if t.tzinfo is not None:
t = dt.as_local(t).replace(tzinfo=None)
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| # again immediately, in an endless loop (2026-07-09 incident: sunrise | ||
| # end_time callbacks re-fired for exactly the UTC-offset window, | ||
| # flooding MQTT with turn_off commands). | ||
| now_local = dt.as_local(dt.now()).replace(tzinfo=None) |
|
Thanks for this, could you review the Copilot suggestion please |
|
I haven't found any issues with this running on my home assistant. Please just review the copilot suggestion and use the make_native function if it's applicable in this context. Once I hear back from you, happy to merge it. |
Problem
Model.futurize()builds the reference "today"/"now" fromdate.today()/datetime.now(), which follow the OS/process timezone. The times it compares against come fromparse_time()and are naive values in the HA-configured timezone. When the two differ (e.g. host on UTC, Home Assistant onEurope/Prague), a time-based callback that has just fired reschedules itself to an instant that is still "in the future" in OS-local terms but already in the past in HA-local terms.async_track_point_in_timethen fires it again immediately, and the callback re-arms itself in a tight loop.In practice this showed up as
end_time/sunrise callbacks re-firing continuously for exactly the UTC-offset-sized window, flooding MQTT with repeatedturn_offcommands until the offset window elapsed.This bites any deployment where the container/host timezone is not the same as the HA timezone (common on NixOS/containerized installs that keep the host on UTC).
Fix
Anchor both sides of the comparison to HA-local wall time using
homeassistant.util.dt(already imported at module top):No behavior change when host TZ == HA TZ.
Testing
Deployed on an installation with host=UTC / HA=Europe/Prague; the callback re-fire loops / MQTT command floods around sunrise are gone, and normal time-restriction scheduling is unaffected.