wake-alarm/wake_alarm/_field_reader.py
Krzysztof kuhy Rudnicki c3c129b068 Make the morning session the carrot: earn 11:00 by finishing it in time
On 2026-09-19 the phone was powered off after the bathroom tag and nothing
on the PC noticed: every finished session was published and nothing read
it. Now wake-alarm distils the phone's morning into an HMAC-signed
~/.local/state/wake_alarm/morning_session.json (wake-alarm-session.timer,
07:00-11:00 every 2 min) that screen-locker and leetcode-guard read to
defer their locks -- provisionally until min(start + 90 min, 09:30) while
the session is live, until 11:00 once it completed inside that cutoff,
and not at all for a session finished later (the phone switched on at
noon completes the legs then; that must un-lock nothing).

The phone publishes the session at start as well as at finish
(current_session, cleared with the finish stamp), so the PC can tell
"started, not finished" from "never started". A required 1-5 energy tap
joins the weigh-in and ships as latest_energy with a PC reader mirroring
_weight (the shared half is _field_reader).

Also fixes the vmbox harness, red since the 2026-09-11 home-tidy: the
guest tree lives at ~/wake-alarm, and the INSTALL-OK grep matched vm's
echo of the command line. Takes Flutter 3.47.5.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Tp54ewdXRLaimUuT8LgYyn
2026-09-19 16:40:51 +02:00

74 lines
2.5 KiB
Python

"""Pick one field's newest value out of every device's synced log.
``_weight`` and ``_energy`` each read a single ``{date, value}`` pair the
phone publishes on ``alarm.json``; everything but the parse is the same, and
pylint rightly flagged the copy. This is the shared half: JSON in per device,
one winner out, ranked by the field's HLC with ``device_id`` breaking exact
ties so two installs never alternate between reads.
"""
from __future__ import annotations
import json
import logging
from typing import TYPE_CHECKING, Any
from wake_alarm._alarm_payload import extract_hlc
from wake_alarm._sync_transport import fetch_firebase_logs
if TYPE_CHECKING:
from collections.abc import Callable
_logger = logging.getLogger(__name__)
def field_value(log: dict[str, Any], field: str) -> object:
"""The value half of one ``(value, HLC)`` pair on the alarm record, or None."""
record = log.get("alarm")
fields = record.get("fields") if isinstance(record, dict) else None
pair = fields.get(field) if isinstance(fields, dict) else None
return pair[0] if isinstance(pair, list) and pair else None
def newest_field[T](
published: list[tuple[str, str]],
field: str,
parse: Callable[[object, str], T | None],
) -> T | None:
"""The most recently stamped ``field`` across every device's log.
Args:
published: ``(device_id, raw_json)`` per device, in any order.
field: The crdt_sync field name.
parse: Turns the raw value plus its device id into the result, or
None when the shape is not usable -- an unusable device never
outranks a usable one.
"""
ranked: list[tuple[str, str, T]] = []
for device_id, raw in published:
try:
log = json.loads(raw)
except json.JSONDecodeError:
continue
if not isinstance(log, dict):
continue
parsed = parse(field_value(log, field), device_id)
if parsed is not None:
ranked.append((extract_hlc(log, field), device_id, parsed))
if not ranked:
return None
return max(ranked, key=lambda entry: (entry[0], entry[1]))[2]
def latest_field[T](
field: str, parse: Callable[[object, str], T | None], *, what: str
) -> T | None:
"""``newest_field`` over a live Firebase read; None when unreadable."""
published = fetch_firebase_logs()
if published is None:
return None
value = newest_field(published, field, parse)
if value is None:
_logger.info("No %s published yet", what)
return value