Replace presence schedules with multiple presence patterns per child (schema 9)

A child now carries zero or more home/away presence patterns instead of
at most one schedule. Migration 9 creates presence_patterns, copies every
presence_schedules row across as a 'home' pattern named "Home schedule"
(cycle, anchor and pattern verbatim), and drops the old table in one
transaction. Migration 1 keeps creating the retired table from a frozen
copy of its DDL; schema.py no longer declares it.

PresenceEngine resolves override -> any covering away pattern (absent)
-> any covering home pattern (present) -> otherwise absent when the child
has a home pattern, else present. The home-pattern default keeps every
migrated child's presence unchanged.

The admin plane's one-per-child presence-schedule routes are replaced by
pattern CRUD: GET/POST /children/{child_id}/presence-patterns and
PATCH/DELETE /presence-patterns/{pattern_id}.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-09-24 19:31:34 -04:00
co-authored by Claude Opus 5.5
parent 5650a1f5a2
commit ef6940d937
22 changed files with 2974 additions and 936 deletions
+20 -5
View File
@@ -36,7 +36,7 @@ from custom_components.nestquest.core.snapshot import (
from custom_components.nestquest.core.dao_instances import QuestInstancesDao
from custom_components.nestquest.core.dao_presence import (
PresenceOverridesDao,
PresenceSchedulesDao,
PresencePatternsDao,
)
from custom_components.nestquest.core.db import NestQuestDatabase
from custom_components.nestquest.core.materialize import materialize
@@ -116,14 +116,16 @@ async def test_instance_payload_shape_and_missed_omission() -> None:
async def test_build_snapshot_explicit_values(hass, make_entry, tmp_path) -> None:
"""The builder produces the documented shape as literals, pinned in time.
Ada is AWAY today (a 2-week schedule whose absent week 0 covers today,
Ada is AWAY today (a 2-week home pattern whose empty week 0 covers today,
plus a presence override marking her present on today+2) so
``present`` is False and ``next_present`` is the override date. Her
open quest due at 10:00 is OVERDUE under the pinned 12:00 ``now``.
Bo's quest due at 17:00 is completed at 12:00 (on time) so its
payload ``state`` is ``completed`` and ``on_time`` is True. The first
scheduled active child is Ada, whose 2-week cycle anchored one day
before today puts ``cycle_day`` at 2.
patterned active child is Ada, whose FIRST (lowest id) pattern is a
2-week cycle anchored one day before today, putting ``cycle_day`` at
2; her second pattern (a 3-week one anchored today, covering no day)
must not drive the cycle day.
"""
# A standalone core database (the integration owns none): the
# builder and its seed run against it through the executor.
@@ -172,12 +174,25 @@ async def test_build_snapshot_explicit_values(hass, make_entry, tmp_path) -> Non
# covers today, so she is absent today; an override marks her present
# on today+2, so next_present resolves to that date and cycle_day is
# ((today - (today-1)) % 14) + 1 == 2.
await PresenceSchedulesDao(database).upsert_by_child(
patterns = PresencePatternsDao(database)
await patterns.create(
ada.id,
name="Home schedule",
kind="home",
cycle_length_weeks=2,
anchor_date=(today - datetime.timedelta(days=1)).isoformat(),
pattern="|0,1,2,3,4,5,6",
)
# A later pattern covering no day: it changes no presence answer and,
# not being the first pattern, must not set cycle_day (it would be 1).
await patterns.create(
ada.id,
name="Placeholder away",
kind="away",
cycle_length_weeks=3,
anchor_date=today_iso,
pattern="||",
)
returns_date = today + datetime.timedelta(days=2)
await PresenceOverridesDao(database).create(
ada.id,