A one-off `pip install "wake_alarm @ git+..."` from a scratch directory was used during the original cutover; that's a non-editable snapshot that a later git push silently never reaches. Document the correct deployment convention (install.sh from a durable ~/wake-alarm clone) so this doesn't drift again.
6.1 KiB
CLAUDE.md — wake_alarm
What this does
A weekend wake alarm: on alarm days (ALARM_DAYS in _constants.py — Mon,
Fri, Sat, Sun), the machine hibernates overnight and wakes itself via
rtcwake at the configured alarm time. wake-alarm.service then opens a
fullscreen Tk window that must be dismissed (with a typed challenge — see
_challenges.py), ramps fans to 100% via wake-alarm-fans.sh, plays audio
through whatever sink comes up after the monitor wakes, and optionally
toggles a TP-Link Tapo P110 smart plug via python-kasa.
Scheduling — hibernate-based, not a systemd timer
There is no systemd timer for this package. The wake mechanism is:
shutdown-wrapper.sh, installed to/usr/local/bin/shutdown(shadowing/usr/bin/shutdownvia PATH order), intercepts shutdown/poweroff calls on alarm nights and callsrtcwake -m diskinstead — hibernating with the RTC alarm set to wake the machine atWAKE_AFTER_HOURS(_constants.py) from now.sleep-hook.sh, installed to/usr/lib/systemd/system-sleep/, fires on resume ($1 == post) and startsmorning-routine.servicefor every logged-in session. That orchestrator (lives in testsAndMisc, not here — see below) runs the alarm first, then the workout screen lock, so the two never fight for the fullscreen.wake-alarm.serviceitself isType=simple, started either directly or by the orchestrator, and exits once the alarm is dismissed.
If you change WAKE_AFTER_HOURS in _constants.py, you must also update the
duplicate constant in shutdown-wrapper.sh (WAKE_AFTER_HOURS=8) — they are
not wired together, by design (the shell wrapper has no Python runtime
available at the point it intercepts shutdown).
Cross-repo coupling — not a bug
_constants.py's WORKOUT_LOG_FILE points at
~/screen-locker/screen_locker/workout_log.json — a file owned by the
separate, already-standalone screen-locker repo
(https://github.com/kuhyx/screen-locker). This is intentional: the alarm
reads whether today's workout was already logged by screen-locker to decide
whether the morning routine should also lock the workout screen. If this
path ever raises ModuleNotFoundError-style confusion, the bug is almost
certainly in the orchestrator (morning_routine in testsAndMisc), not
here — see the next section.
The morning_routine orchestrator lives elsewhere
morning_routine._orchestrator (in testsAndMisc/python_pkg/morning_routine/)
runs this package and screen_locker as two sequential subprocesses. When
either package is extracted to its own repo, the orchestrator's module
reference must be updated in the same change — this exact mistake
(orchestrator left pointing at python_pkg.screen_locker.screen_lock after
screen-locker's extraction on 2026-05-28) caused a month-long silent
production failure where the alarm fired and dismissed correctly but the
workout lock crashed with ModuleNotFoundError on every run. Once both
wake_alarm and screen_locker are pip-installed system-wide, the
orchestrator needs no PYTHONPATH/cwd plumbing — plain
subprocess.run([sys.executable, "-m", module, "--production"]) resolves
both.
Production dependency installation — read this before adding any dependency
wake-alarm.service runs /usr/bin/python directly — not a venv. Any
new non-stdlib dependency (this package itself, gatelock, python-kasa,
anything added later) must be installed into system Python's user
site-packages:
/usr/bin/python3 -m pip install --user --break-system-packages -e .
Always run install.sh (or pip install -e) from a durable clone, not a
scratch directory. install.sh does pip install -e "$REPO_DIR" —
editable, so a later git pull in that same clone updates the running
production code with no reinstall needed. The clone must live somewhere
permanent (this repo's convention: ~/wake-alarm, mirroring ~/screen-locker
for the screen-locker package) — if you pip install -e from /tmp/... or
run pip install "wake_alarm @ git+https://..." as a one-off, you get a
non-editable snapshot frozen at that commit, and the next git push here
silently does not reach the running service.
install.sh already does this. If you add a dependency and only install it
into a dev venv, the production service will silently fail with
ModuleNotFoundError on its next run — this exact gap caused a 3-day
diet_guard production outage (2026-06-19 to 2026-06-22) for the sibling
gatelock migration. Always verify against
/usr/bin/python3 -c "import <new_dep>", not just the dev venv, before
considering a dependency change done.
Operational gotchas
python-kasais optional at runtime (_smart_plug.pycatchesImportErrorand disables smart-plug control with a warning log), but it is a hard dependency for this repo's own tooling (mypy/pylint/tests need it importable) — seepyproject.toml/requirements.txt.- The
wavemodule needs special pylint handling._audio.pyopens WAV files in write mode; pylint's stdlib stub infers the read-mode overload and wrongly flagssetnchannels/setsampwidth/setframerate/writeframesas missing. See thegenerated-memberslist inpyproject.toml's[tool.pylint.typecheck]— don't remove it if pylint starts complaining aboutwave.Wave_write. wake_state.jsonis runtime state, not tracked. It holds the HMAC-signed dismissal record for the current alarm day. It used to be accidentally committed in the monorepo; it is gitignored here and must stay that way.
Commands
- Run tests:
python -m pytest wake_alarm/tests/ --cov=wake_alarm --cov-branch --cov-fail-under=100 - Lint:
pre-commit run --all-files - Test the lock manually (safe, closeable):
python -m wake_alarm._alarm --demo - Install for production:
bash install.sh
Do NOT
- Don't add a dependency without doing the production install-path check above.
- Don't forget the orchestrator when changing this package's module path or invocation — see "The morning_routine orchestrator lives elsewhere" above.
- Don't commit
wake_state.json.