build_snapshot now takes now ONLY and derives today = now.date()
inside (P3-1): a single source of truth so the calendar date and
wall-clock time can never disagree across two reads. Coordinator
call site updated.
The expected completed_at in tests/test_core_snapshot.py is now built
from pinned_now.astimezone(datetime.timezone.utc).isoformat() instead
of a literal '+00:00' UTC string (P3-2), so the assertion is
timezone-independent rather than relying on the hass fixture's UTC
timezone.
Pin the coordinator's clock to pinned_now, run async_refresh, and
assert coordinator.data == built (the frozen dataclasses make == a
full deep compare) — the REQUIRED tie-in that proves the coordinator
wrapper actually delegates to build_snapshot, which the literal
assertions alone could not (P2).
PR #148 review (CHANGES_REQUIRED) follow-up:
- Move the panel payload dict shaper (_instance_payload) from the
HA-importing sensor.py into core/snapshot.py as instance_payload, and
have sensor.py import it from core. The API panel route now shares ONE
shaping implementation with the HA sensor layer (done -> completed,
on_time passthrough, missed omitted unless include_missed).
- build_snapshot takes the HA-local now explicitly instead of
reading datetime.now(time_zone) internally, so date and time come from
a single clock read and the overdue check is pinned in tests. The
coordinator passes the same now it already resolved; time_zone is
dropped (now is timezone-aware).
- Strengthen tests/test_core_snapshot.py with explicit literal expected
values instead of self-equality: an AWAY child (2-week absent-week
schedule + a presence override => next_present), a due-timed quest
exercised as overdue under a pinned 12:00 now, a completed quest
(done -> completed, on_time True), cycle_day == 2, and a constructed
missed view proving omission from the panel payload and retention in
the admin payload.
- Docstring cleanups: state plainly that settings is currently
unused (no 'wired' implication); correct the coordinator docstring
(settings are passed in, not resolved off the entry); trim the
coordinator __all__ to what other modules actually import.
- Derive the seed/materialize date from coordinator._local_now() so the
seed date and snapshot date cannot diverge near midnight; drop the
redundant bo/ghost/list_children leftovers from the prior seed.
The household snapshot assembly that lived inline in the coordinator's
_async_update_data is extracted into a pure, Home-Assistant-free
core/snapshot.py: the QuestInstanceView / ChildDaySnapshot /
NestQuestSnapshot dataclasses, the _cycle_day helper, and a new async
build_snapshot(database, settings, today, time_zone) builder.
The coordinator is now a thin DataUpdateCoordinator wrapper that
resolves the local timezone and settings and delegates to
build_snapshot; the dataclasses and _cycle_day are re-exported from the
coordinator module so existing sensor.py / binary_sensor.py imports keep
working unchanged (entity_ids and per-child instance attribute payload
shape unchanged). The API service's panel snapshot route (next task)
shares this one implementation.
Tests that monkeypatched derive_state on the coordinator module now
patch core.snapshot.derive_state (the module that now owns the call).
A new test seeds a household and asserts build_snapshot's output equals
the coordinator's snapshot for the same database and date. Suite green
at 1588.