mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-10 20:27:02 +01:00
The PC is no longer shut down at night, which broke the scheduling model outright. The wake time used to be decided once, at hibernate, and baked into the RTC by shutdown-wrapper.sh. With no shutdown there is no resume, so sleep-hook.sh never ran and nothing fired the alarm at all. Worse, wake-alarm.service was WantedBy=graphical-session.target and enabled by install.sh, while _should_run_alarm() gates on day / dismissed / workout-logged but has no *time* gate. On an always-on machine that target is reached on every login, so the alarm fired at login time on any alarm day — which is every day. Replace both with a systemd user timer ticking once a minute: - _sync.py reads the alarm the phone pushed to kuhyx/syncs, mirroring shutdown-wrapper.sh's jq parse so the two readers cannot drift. Caches each read, then degrades fresh cache -> fetch -> stale cache -> FALLBACK_ALARM_HOUR, so a network blip can never cost a wake-up. stdlib-only: the units run /usr/bin/python, so a dependency here is a production install-path hazard. - _trigger.py fires morning-routine.service (never wake-alarm.service — that skips the workout lock) when local time is inside [alarm, alarm + TRIGGER_CATCHUP_MINUTES). The catch-up window covers a suspended or just-booted machine; starting an already-active unit is a no-op, so an alarm on screen needs no extra dedupe state. - wake-alarm.service loses its [Install]; install.sh disables it. A timer rather than a resident scheduler: each tick is a fresh process, so there is no daemon to die silently and no wall-clock jump to handle across suspend/resume. Ticks are per-minute for exact-minute firing, but only hit the network when the cache is older than 5 minutes — ~288 GitHub reads/day, and phone changes land within ~5 minutes. Degraded reads log at INFO, not WARNING: at one tick a minute a persistent condition would be 1440 identical journal lines a day. The one WARNING kept is _trigger firing on a non-phone alarm time. Token resolution gains a fallback to the sibling apps' repo-scoped sync_token, in both readers, so one PAT covers every app on kuhyx/syncs. Note this surfaced that wake-alarm-sync/ does not exist in kuhyx/syncs at all: the phone has never completed a push since the crdt_sync migration, so the hibernate path had been silently using the +8h fallback. Fixing that is a phone-side action (reconnect GitHub with repo scope, then one "Set Alarm & Sync"). Alarms edited directly in the Clock app still do not reach the sync — that needs an ACTION_NEXT_ALARM_CLOCK_CHANGED receiver. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
125 lines
4.8 KiB
Bash
Executable File
125 lines
4.8 KiB
Bash
Executable File
#!/bin/bash
|
|
# Wrapper for /usr/bin/shutdown that redirects to rtcwake -m disk on alarm
|
|
# nights (every day of the week). This ensures that both the automated
|
|
# systemd timer AND manual "shutdown now" hibernate correctly so the PC
|
|
# wakes for the morning alarm.
|
|
#
|
|
# Install to /usr/local/bin/shutdown (takes priority over /usr/bin/shutdown
|
|
# because /usr/local/bin appears first in PATH).
|
|
#
|
|
# This is the FALLBACK path, for the rare night the PC is actually powered
|
|
# down. The primary path is wake-alarm-trigger.timer, which ticks once a
|
|
# minute on the always-on machine and fires at the phone's alarm time without
|
|
# any rtcwake involvement (see wake_alarm/_trigger.py).
|
|
#
|
|
# Wake time is taken from the alarm the phone app pushes to the private
|
|
# kuhyx/syncs repo via the shared crdt_sync transport (see phone_app/). Falls
|
|
# back to WAKE_AFTER_HOURS if sync is not configured or unreachable. Token:
|
|
# ~/.config/wake_alarm/sync.json key "token", else a sibling app's
|
|
# ~/.config/{diet_guard,screen_locker}/sync_token. Needs "repo" scope, since
|
|
# the Contents API must read a private repo.
|
|
|
|
set -euo pipefail
|
|
|
|
REAL_SHUTDOWN=/usr/bin/shutdown
|
|
RTCWAKE=/usr/sbin/rtcwake
|
|
# Fallback: how many hours after shutdown to wake when Gist is unavailable.
|
|
# Must match WAKE_AFTER_HOURS in wake_alarm/_constants.py.
|
|
WAKE_AFTER_HOURS=8
|
|
|
|
# Pass through reboots and cancel commands unchanged.
|
|
for arg in "$@"; do
|
|
case "$arg" in
|
|
-r|--reboot|-c|--cancel)
|
|
exec "$REAL_SHUTDOWN" "$@"
|
|
;;
|
|
esac
|
|
done
|
|
|
|
# Resolve the GitHub token, preferring wake_alarm's own JSON config and falling
|
|
# back to the sibling companion apps' raw single-line token files (one
|
|
# repo-scoped PAT covers every app syncing through kuhyx/syncs).
|
|
# Mirrors SYNC_CONFIG_FILE / SYNC_TOKEN_FALLBACK_FILES in
|
|
# wake_alarm/_constants.py — keep the two readers in step.
|
|
read_sync_token() {
|
|
local cfg="$HOME/.config/wake_alarm/sync.json"
|
|
local token fallback
|
|
|
|
if [[ -f "$cfg" ]]; then
|
|
token=$(jq -r '.token // empty' "$cfg" 2>/dev/null) || token=""
|
|
if [[ -n "$token" ]]; then
|
|
printf '%s' "$token"
|
|
return 0
|
|
fi
|
|
fi
|
|
|
|
for fallback in \
|
|
"$HOME/.config/diet_guard/sync_token" \
|
|
"$HOME/.config/screen_locker/sync_token"; do
|
|
[[ -f "$fallback" ]] || continue
|
|
token=$(tr -d '[:space:]' < "$fallback") || continue
|
|
if [[ -n "$token" ]]; then
|
|
printf '%s' "$token"
|
|
return 0
|
|
fi
|
|
done
|
|
|
|
return 1
|
|
}
|
|
|
|
# Read the alarm time the phone pushed to kuhyx/syncs via crdt_sync.
|
|
# Returns the Unix epoch (seconds) of the next occurrence of the stored H:M.
|
|
# Exits non-zero if sync is not configured, unreachable, or malformed.
|
|
#
|
|
# The phone is the sole writer, under the fixed device id "phone", so a single
|
|
# Contents-API read of its alarm.json is enough (no device discovery / HLC
|
|
# ordering needed here — that would only matter with multiple writers). The
|
|
# stored payload is a crdt_sync Log: {"alarm": {"fields": {"alarm_hour":
|
|
# [H, "<hlc>"], "alarm_minute": [M, "<hlc>"]}, ...}}. The raw media type makes
|
|
# the Contents API return that JSON directly, so no base64 decode is needed.
|
|
get_synced_alarm_epoch() {
|
|
local token log_json hour minute target_epoch now_epoch
|
|
token=$(read_sync_token) || return 1
|
|
|
|
local url="https://api.github.com/repos/kuhyx/syncs/contents"
|
|
url="$url/wake-alarm-sync/devices/phone/alarm.json"
|
|
log_json=$(curl -sf --max-time 10 \
|
|
-H "Authorization: token $token" \
|
|
-H "Accept: application/vnd.github.raw" \
|
|
"$url") || return 1
|
|
[[ -n "$log_json" ]] || return 1
|
|
|
|
hour=$(printf '%s' "$log_json" \
|
|
| jq -r '.alarm.fields.alarm_hour[0]' 2>/dev/null) || return 1
|
|
minute=$(printf '%s' "$log_json" \
|
|
| jq -r '.alarm.fields.alarm_minute[0]' 2>/dev/null) || return 1
|
|
[[ "$hour" =~ ^[0-9]+$ && "$minute" =~ ^[0-9]+$ ]] || return 1
|
|
|
|
printf -v now_epoch '%(%s)T' -1
|
|
target_epoch=$(date -d "today $hour:$minute" +%s)
|
|
if (( target_epoch <= now_epoch )); then
|
|
target_epoch=$(date -d "tomorrow $hour:$minute" +%s)
|
|
fi
|
|
echo "$target_epoch"
|
|
}
|
|
|
|
# Every day of the week is an alarm day (date +%u: Mon=1 … Sun=7).
|
|
tomorrow_dow=$(date -d "tomorrow" +%u)
|
|
case "$tomorrow_dow" in
|
|
1|2|3|4|5|6|7)
|
|
if wake_epoch=$(get_synced_alarm_epoch 2>/dev/null); then
|
|
logger -t shutdown-wrapper \
|
|
"crdt_sync: waking at $(date -d "@$wake_epoch") (epoch $wake_epoch)"
|
|
else
|
|
wake_epoch=$(( $(printf '%(%s)T' -1) + WAKE_AFTER_HOURS * 3600 ))
|
|
logger -t shutdown-wrapper \
|
|
"sync unavailable — fallback +${WAKE_AFTER_HOURS}h (epoch $wake_epoch)"
|
|
fi
|
|
sudo "$RTCWAKE" -m no -t "$wake_epoch"
|
|
exec /usr/bin/systemctl hibernate
|
|
;;
|
|
*)
|
|
exec "$REAL_SHUTDOWN" "$@"
|
|
;;
|
|
esac
|