wake-alarm/wake_alarm/_weight.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

68 lines
2.1 KiB
Python

"""The phone's newest morning weigh-in, read from the synced alarm record.
The morning session (``phone_app``, ``DOCS-morning-session.md``) publishes
``latest_weight_kg`` -- ``{"date": "yyyy-mm-dd", "kg": 72.4}`` -- on the same
``alarm.json`` the alarm time rides on. This module is the PC-side reader,
built for diet-guard's protein target: it imports this lazily, so the two
packages stay independently installable.
Firebase only. The GitHub mirror has no per-field stamps and is on its way
out; a weigh-in that reached only the mirror is simply not seen yet.
python -m wake_alarm._weight # prints {"date": ..., "kg": ...} or nothing
"""
from __future__ import annotations
from dataclasses import dataclass
import json
import sys
from wake_alarm._field_reader import latest_field, newest_field
WEIGHT_FIELD = "latest_weight_kg"
@dataclass(frozen=True)
class Weight:
"""One weigh-in: the local date it was taken and the kilograms typed."""
date: str
kg: float
device_id: str
def _parse_weight(value: object, device_id: str) -> Weight | None:
"""The weigh-in out of one device's ``latest_weight_kg`` value, or None."""
if not isinstance(value, dict):
return None
date, kg = value.get("date"), value.get("kg")
if not isinstance(date, str) or isinstance(kg, bool):
return None
if not isinstance(kg, (int, float)):
return None
return Weight(date=date, kg=float(kg), device_id=device_id)
def newest_weight(published: list[tuple[str, str]]) -> Weight | None:
"""The most recently stamped weigh-in across every device's log."""
return newest_field(published, WEIGHT_FIELD, _parse_weight)
def latest_weight() -> Weight | None:
"""The phone's newest weigh-in from Firebase, or None when unreadable."""
return latest_field(WEIGHT_FIELD, _parse_weight, what="weigh-in")
def main() -> int:
"""Print the newest weigh-in as JSON; exit 1 when there is none."""
weight = latest_weight()
if weight is None:
return 1
sys.stdout.write(json.dumps({"date": weight.date, "kg": weight.kg}) + "\n")
return 0
if __name__ == "__main__":
sys.exit(main())