Task 9047a97b — Extract the panel snapshot builder into core
Done-condition: core exposes a pure, HA-free snapshot builder (core/snapshot.py) that, given the database, settings, a date and a timezone, returns the household snapshot in the panel shape (children in sort order, each child's instances in the documented payload shape with missed instances omitted, per-child counts, presence, cycle day). The HA coordinator's _async_update_data is refactored to call it, behaviour unchanged. A test asserts the builder's output for a seeded household matches the coordinator's snapshot.
Changes
custom_components/nestquest/core/snapshot.py (NEW): the pure pieces — QuestInstanceView, ChildDaySnapshot, NestQuestSnapshot, _cycle_day, and async build_snapshot(database, settings, today, time_zone) -> NestQuestSnapshot. No homeassistant import. The assembly logic that lived inline in the coordinator's _async_update_data now lives here.
custom_components/nestquest/coordinator.py: now a thin DataUpdateCoordinator wrapper. _async_update_data resolves the local timezone + settings and delegates to build_snapshot. The dataclasses and _cycle_day are re-exported so existing sensor.py / binary_sensor.py imports (from .coordinator import ChildDaySnapshot, NestQuestCoordinator, QuestInstanceView) keep working unchanged — entity_ids and per-child instance attribute payload shape unchanged.
custom_components/nestquest/__init__.py: passes the entry's settings into the coordinator constructor (the builder's forward-compatible settings param).
tests/test_sensors.py, tests/test_entity_matrix.py: the two tests that monkeypatched derive_state on the coordinator module now patch custom_components.nestquest.core.snapshot.derive_state (the module that now owns the call). Behaviour identical.
tests/test_core_snapshot.py (NEW): seeds a household (3 active + 1 inactive child, a shared daily quest, today's instances, one completed instance, a 1-week presence schedule) and asserts build_snapshot's output is byte-for-byte equal to the coordinator's snapshot for the same database, date and timezone — plus shape invariants (sort order, inactive omitted, missed omitted, counts, presence, cycle day).
Why
The API's panel snapshot route (next task) needs this snapshot, but the assembly lived only in coordinator.py, which is HA-coupled (DataUpdateCoordinator). Both consumers must share ONE implementation.
Verification
uv run pytest -q → 1588 passed (baseline 1587 + 1 new test).
test_core_isolation.py green: no homeassistant import, no climb-out, no bare core import under core/.
No repo-root core/; no top-level bare core import under the integration.
No new re-export shim added (core/snapshot.py is a new module; the coordinator re-export is internal to the integration).
No merge requested. Do not mark the Maestro task done.
## Task 9047a97b — Extract the panel snapshot builder into core
**Done-condition:** core exposes a pure, HA-free snapshot builder (`core/snapshot.py`) that, given the database, settings, a date and a timezone, returns the household snapshot in the panel shape (children in sort order, each child's instances in the documented payload shape with missed instances omitted, per-child counts, presence, cycle day). The HA coordinator's `_async_update_data` is refactored to call it, behaviour unchanged. A test asserts the builder's output for a seeded household matches the coordinator's snapshot.
## Changes
- **`custom_components/nestquest/core/snapshot.py`** (NEW): the pure pieces — `QuestInstanceView`, `ChildDaySnapshot`, `NestQuestSnapshot`, `_cycle_day`, and async `build_snapshot(database, settings, today, time_zone) -> NestQuestSnapshot`. No `homeassistant` import. The assembly logic that lived inline in the coordinator's `_async_update_data` now lives here.
- **`custom_components/nestquest/coordinator.py`**: now a thin `DataUpdateCoordinator` wrapper. `_async_update_data` resolves the local timezone + settings and delegates to `build_snapshot`. The dataclasses and `_cycle_day` are re-exported so existing `sensor.py` / `binary_sensor.py` imports (`from .coordinator import ChildDaySnapshot, NestQuestCoordinator, QuestInstanceView`) keep working unchanged — entity_ids and per-child instance attribute payload shape unchanged.
- **`custom_components/nestquest/__init__.py`**: passes the entry's `settings` into the coordinator constructor (the builder's forward-compatible settings param).
- **`tests/test_sensors.py`, `tests/test_entity_matrix.py`**: the two tests that monkeypatched `derive_state` on the coordinator module now patch `custom_components.nestquest.core.snapshot.derive_state` (the module that now owns the call). Behaviour identical.
- **`tests/test_core_snapshot.py`** (NEW): seeds a household (3 active + 1 inactive child, a shared daily quest, today's instances, one completed instance, a 1-week presence schedule) and asserts `build_snapshot`'s output is byte-for-byte equal to the coordinator's snapshot for the same database, date and timezone — plus shape invariants (sort order, inactive omitted, missed omitted, counts, presence, cycle day).
## Why
The API's panel snapshot route (next task) needs this snapshot, but the assembly lived only in `coordinator.py`, which is HA-coupled (`DataUpdateCoordinator`). Both consumers must share ONE implementation.
## Verification
- `uv run pytest -q` → **1588 passed** (baseline 1587 + 1 new test).
- `test_core_isolation.py` green: no `homeassistant` import, no climb-out, no bare `core` import under `core/`.
- No repo-root `core/`; no top-level bare `core` import under the integration.
- No new re-export shim added (`core/snapshot.py` is a new module; the coordinator re-export is internal to the integration).
No merge requested. Do not mark the Maestro task done.
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.
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.
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).
dev_agent
merged commit ca477125d6 into feature/cd03735b-2498-4b34-b84c-c1cb2b271ba7-nestquest-api-service2026-09-21 17:43:52 -04:00
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Task 9047a97b — Extract the panel snapshot builder into core
Done-condition: core exposes a pure, HA-free snapshot builder (
core/snapshot.py) that, given the database, settings, a date and a timezone, returns the household snapshot in the panel shape (children in sort order, each child's instances in the documented payload shape with missed instances omitted, per-child counts, presence, cycle day). The HA coordinator's_async_update_datais refactored to call it, behaviour unchanged. A test asserts the builder's output for a seeded household matches the coordinator's snapshot.Changes
custom_components/nestquest/core/snapshot.py(NEW): the pure pieces —QuestInstanceView,ChildDaySnapshot,NestQuestSnapshot,_cycle_day, and asyncbuild_snapshot(database, settings, today, time_zone) -> NestQuestSnapshot. Nohomeassistantimport. The assembly logic that lived inline in the coordinator's_async_update_datanow lives here.custom_components/nestquest/coordinator.py: now a thinDataUpdateCoordinatorwrapper._async_update_dataresolves the local timezone + settings and delegates tobuild_snapshot. The dataclasses and_cycle_dayare re-exported so existingsensor.py/binary_sensor.pyimports (from .coordinator import ChildDaySnapshot, NestQuestCoordinator, QuestInstanceView) keep working unchanged — entity_ids and per-child instance attribute payload shape unchanged.custom_components/nestquest/__init__.py: passes the entry'ssettingsinto the coordinator constructor (the builder's forward-compatible settings param).tests/test_sensors.py,tests/test_entity_matrix.py: the two tests that monkeypatchedderive_stateon the coordinator module now patchcustom_components.nestquest.core.snapshot.derive_state(the module that now owns the call). Behaviour identical.tests/test_core_snapshot.py(NEW): seeds a household (3 active + 1 inactive child, a shared daily quest, today's instances, one completed instance, a 1-week presence schedule) and assertsbuild_snapshot's output is byte-for-byte equal to the coordinator's snapshot for the same database, date and timezone — plus shape invariants (sort order, inactive omitted, missed omitted, counts, presence, cycle day).Why
The API's panel snapshot route (next task) needs this snapshot, but the assembly lived only in
coordinator.py, which is HA-coupled (DataUpdateCoordinator). Both consumers must share ONE implementation.Verification
uv run pytest -q→ 1588 passed (baseline 1587 + 1 new test).test_core_isolation.pygreen: nohomeassistantimport, no climb-out, no barecoreimport undercore/.core/; no top-level barecoreimport under the integration.core/snapshot.pyis a new module; the coordinator re-export is internal to the integration).No merge requested. Do not mark the Maestro task done.