"""One look at the world for the fallback alarm, and its bookkeeping. Split from ``_fallback`` (tick, ring lifecycle, CLI): this is "what is true right now" -- today's alarm, the latch, the free-day pool and, only when the decision needs it, every device's ``alarm.json``. """ from __future__ import annotations from dataclasses import dataclass, replace from typing import TYPE_CHECKING import freedays from wake_alarm._enforcement_payload import EnforcementView, enforcement_view from wake_alarm._escapes import note_escapes from wake_alarm._events import emit_on_change, last_value, remember from wake_alarm._fallback_latch import Latch, read_latch from wake_alarm._fallback_policy import ( FINAL_REASONS, Facts, alarm_instant, needs_facts, ) from wake_alarm._session_payload import newest_session from wake_alarm._sync import get_alarm from wake_alarm._sync_transport import fetch_firebase_logs if TYPE_CHECKING: from datetime import datetime SETTLED_SLOT = "fallback_settled" @dataclass(frozen=True) class Observation: """One look at the world: the facts, and whether Firebase answered.""" facts: Facts reachable: bool = True view: EnforcementView | None = None def settled_reason(day: str, latch: Latch | None) -> str | None: """A final reason already seen today, from the latch or the event state.""" if latch is not None and latch.stop_reason in FINAL_REASONS: return latch.stop_reason remembered = (last_value(SETTLED_SLOT) or "").partition("|") return remembered[2] if remembered[0] == day and remembered[2] else None def observe(now: datetime) -> Observation: """Gather the facts; the network only when the decision needs it.""" day = now.date().isoformat() latch = read_latch(day) facts = Facts( now=now, alarm_at=alarm_instant(get_alarm(), now.date()), free_day=freedays.is_free_day(now.date()), latch=latch, settled=settled_reason(day, latch), ) if facts.free_day or not needs_facts(now, facts.alarm_at, latch): return Observation(facts) published = fetch_firebase_logs() if published is None: return Observation(facts, reachable=False) view = enforcement_view(published) session = newest_session(published) return Observation( replace( facts, escaped=view.escape_for(day) is not None, session_started=session is not None and session.date == day, ring_started=view.ring_started_on(day), ), view=view, ) def record(seen: Observation) -> None: """Log what this observation means for the PC's own state.""" day = seen.facts.now.date().isoformat() if seen.reachable: remember("firebase_unreachable", None) else: emit_on_change( "firebase_unreachable", day, "treating as no ping, no session", alarm=seen.facts.alarm_at.isoformat(), ) if seen.view is not None: note_escapes(seen.view) def remember_final(day: str, reason: str) -> None: """Make a final reason stick for the rest of the day.""" if reason in FINAL_REASONS: remember(SETTLED_SLOT, f"{day}|{reason}")