Compare commits

..
Author SHA1 Message Date
overseerandClaude Opus 5.5 bce2792d8c Compute the missed-sweep delay as real elapsed time across DST
_delay_seconds subtracted two aware datetimes sharing the household
ZoneInfo, which Python does as wall-clock arithmetic, so the sweep fired
an hour late at spring-forward and an hour early at fall-back. Both
instants are now compared and subtracted in UTC.

A nonexistent rollover time resolves to the first valid wall minute
at/after it (the gap's end); an ambiguous one resolves to its first
occurrence (fold=0). Adds America/New_York spring-forward, fall-back,
nonexistent and ambiguous cases alongside the ordinary EDT case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-26 09:54:36 -04:00
3 changed files with 23 additions and 30 deletions
+1 -1
View File
@@ -370,7 +370,7 @@ describe("SettingsScreen — preferences", () => {
fireEvent.click(within(form).getByRole("button", { name: "Save preferences" }));
}
it("renders the current value of the ten settings form fields (timezone is API-only)", async () => {
it("renders the current value of all ten fields", async () => {
const { form } = await renderPreferences();
expect(field(form, "Planning horizon (days)").value).toBe("14");
expect(field(form, "Day rollover").value).toBe("03:00");
+19 -25
View File
@@ -165,12 +165,10 @@ integration's Home-Assistant config-entry options remain a temporary,
SEPARATE source until Feature 18 wires the client to the API — the two
are deliberately not unified here:
- ``GET /settings`` returns the current effective settings: all eleven
- ``GET /settings`` returns the current effective settings: all ten
documented fields (the rolling horizon, the day rollover time, the
notify target, the three notification times, the four enable
toggles and the ``timezone`` — the IANA household zone the
household-local dates derive from, empty meaning host local) — the
defaults on a fresh database.
notify target, the three notification times and the four enable
toggles) — the defaults on a fresh database.
- ``PATCH /settings`` updates ONLY the supplied fields: the body is a
JSON object of settings field names to new values, passed STRAIGHT
to :func:`nestquest_core.settings_store.update_settings` — the
@@ -186,19 +184,18 @@ The missed-sweep trigger (task 058c7b69) follows the same pattern over
:mod:`nestquest_core.sweep` — the HA-free sweep policy the
integration's rollover listener shares:
- ``POST /missed-sweep`` runs the sweep for the household-local
``today`` (ONE :func:`_local_now` clock read re-expressed in the
configured household ``timezone`` — empty falls back to host local —
threaded into the core call) and publishes each returned
``(event_type, payload)`` pair DIRECTLY on the app's ONE transition
publisher — the sweep-built payload, never re-fetched (the core
commits its watermark before returning, so a failed re-fetch after it
would lose the missed event forever), the same
publish-what-the-core-built shape api/scheduler.py uses. The
watermark rule (a same-day rerun announces nothing, a post-downtime
run sweeps the accumulated window once), the no-completion-event rule
and the read-only-over-the-domain-tables guarantee all live in the
core; the response reports how many transitions were published.
- ``POST /missed-sweep`` runs the sweep for the API host's local
``today`` (ONE :func:`_local_now` clock read threaded into the core
call) and publishes each returned ``(event_type, payload)`` pair
DIRECTLY on the app's ONE transition publisher — the sweep-built
payload, never re-fetched (the core commits its watermark before
returning, so a failed re-fetch after it would lose the missed event
forever), the same publish-what-the-core-built shape
api/scheduler.py uses. The watermark rule
(a same-day rerun announces nothing, a post-downtime run sweeps the
accumulated window once), the no-completion-event rule and the
read-only-over-the-domain-tables guarantee all live in the core; the
response reports how many transitions were published.
The occurrence-preview route (task 5f843564) lets the admin PWA's
Definitions edit sheet show the next dates a rule fires on WITHOUT
@@ -208,10 +205,9 @@ reimplementing the recurrence rules client-side:
``rule`` object create/edit take (:class:`AdminRuleRequest`, built
through the SAME :func:`_rule_from_request` / core ``from_dict``
validation, so a rejected rule is 422 exactly as on create), an
optional ``start_date`` (default: the household-local ``today`` —
ONE :func:`_local_now` clock read re-expressed in the configured
household ``timezone``, empty falling back to host local) and an
optional ``count`` (1..50, default 10). The dates come from ONE
optional ``start_date`` (default: the API host's local ``today``,
ONE :func:`_local_now` clock read) and an optional ``count`` (1..50,
default 10). The dates come from ONE
:func:`nestquest_core.recurrence.occurrences_between` call — the
same engine the materializer walks — so the preview can only agree
with what would be generated. The scan is BOUNDED server-side: the
@@ -2285,9 +2281,7 @@ async def admin_run_missed_sweep(
handler performs NO auth of its own). Thin adapter over the SAME
HA-free sweep policy the integration's rollover listener runs
(:mod:`nestquest_core.sweep`): ONE timezone-aware clock read
(:func:`_local_now`, re-expressed in the configured household
``timezone``; empty falls back to host local) pins the
household-local ``today``, and ONE
(:func:`_local_now`) pins the API host's local ``today``, and ONE
:func:`nestquest_core.sweep.run_missed_sweep` call does everything
else — the past-due open-instance query, the documented payload
build and the watermark update (a same-day rerun is an empty
+3 -4
View File
@@ -40,8 +40,8 @@ announces nothing.
The caller supplies ``today`` as a plain :class:`datetime.date`: the
household-local calendar date, read from ``hass.config.time_zone`` by
the integration or from the configured household timezone by the API.
This module never reads a timezone database or config of its own.
the integration or from the API host's local clock by the API. This
module never reads a timezone database or config of its own.
"""
from __future__ import annotations
@@ -98,8 +98,7 @@ async def run_missed_sweep(
``today`` is the household-local calendar date the sweep runs for
(the caller owns the clock: the integration reads it from
``hass.config.time_zone``, the API from the configured household
timezone, falling back to its host's local time when unset).
``hass.config.time_zone``, the API from its host's local time).
Returns the ``(event_type, payload)`` tuples to announce, EMPTY
when the watermark already covers ``today`` (a same-night rerun) —
the caller decides how to publish them and announces nothing on