mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-10 19: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>
160 lines
7.0 KiB
Bash
Executable File
160 lines
7.0 KiB
Bash
Executable File
#!/bin/bash
|
|
# Install the weekend wake alarm systemd user service and sudoers entry.
|
|
#
|
|
# Usage: bash install.sh
|
|
#
|
|
# What it does:
|
|
# 1. Installs wake_alarm + dependencies for /usr/bin/python
|
|
# 2. Installs system dependencies (alsa-utils, ddcutil)
|
|
# 3. Installs the systemd user units and enables the per-minute trigger timer
|
|
# (which fires the alarm at the time the phone synced)
|
|
# 4. Installs the systemd-sleep hook (restarts alarm after hibernate resume)
|
|
# 5. Adds a sudoers entry for passwordless rtcwake
|
|
# 6. Installs shutdown wrapper so "shutdown now" also hibernates on alarm nights
|
|
# 7. Installs fan-control script so alarm can max fans on wake
|
|
# 8. Installs python-kasa (AUR) so the alarm can toggle a Tapo P110 smart plug
|
|
# 9. Installs ddcutil and grants /dev/i2c-* access for DDC/CI monitor control
|
|
|
|
set -euo pipefail
|
|
|
|
# Split declare/assign so the command-substitution exit code is not masked (SC2155).
|
|
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
|
|
readonly SCRIPT_DIR
|
|
readonly REPO_DIR="$SCRIPT_DIR"
|
|
SERVICE_FILE="$SCRIPT_DIR/wake-alarm.service"
|
|
TRIGGER_SERVICE_SRC="$SCRIPT_DIR/wake-alarm-trigger.service"
|
|
TRIGGER_TIMER_SRC="$SCRIPT_DIR/wake-alarm-trigger.timer"
|
|
SLEEP_HOOK_SRC="$SCRIPT_DIR/sleep-hook.sh"
|
|
SHUTDOWN_WRAPPER_SRC="$SCRIPT_DIR/shutdown-wrapper.sh"
|
|
FANS_SCRIPT_SRC="$SCRIPT_DIR/wake-alarm-fans.sh"
|
|
FANS_SCRIPT_DST="/usr/local/bin/wake-alarm-fans.sh"
|
|
SYSTEMD_USER_DIR="$HOME/.config/systemd/user"
|
|
SLEEP_HOOK_DST="/usr/lib/systemd/system-sleep/wake-alarm.sh"
|
|
SHUTDOWN_WRAPPER_DST="/usr/local/bin/shutdown"
|
|
SUDOERS_FILE="/etc/sudoers.d/wake-alarm"
|
|
RTCWAKE_BIN="/usr/sbin/rtcwake"
|
|
|
|
echo "=== Weekend Wake Alarm Installer ==="
|
|
|
|
# 1. Install this package + its dependencies into system Python -------------
|
|
echo "[1/9] Installing wake_alarm + dependencies for /usr/bin/python..."
|
|
/usr/bin/python3 -m pip install --user --break-system-packages -e "$REPO_DIR"
|
|
echo " Installed. Verifying import..."
|
|
/usr/bin/python3 -c "import wake_alarm; import gatelock" \
|
|
&& echo " wake_alarm and gatelock import cleanly from the system interpreter."
|
|
|
|
# 2. Install system dependencies
|
|
echo "[2/9] Checking system dependencies..."
|
|
if ! command -v speaker-test &>/dev/null; then
|
|
echo " Installing alsa-utils (required for speaker-test)..."
|
|
sudo pacman -S --noconfirm alsa-utils
|
|
else
|
|
echo " alsa-utils already installed"
|
|
fi
|
|
|
|
# 3. Install systemd user units (alarm + the trigger timer that fires it)
|
|
echo "[3/9] Installing systemd user units..."
|
|
mkdir -p "$SYSTEMD_USER_DIR"
|
|
cp "$SERVICE_FILE" "$SYSTEMD_USER_DIR/wake-alarm.service"
|
|
cp "$TRIGGER_SERVICE_SRC" "$SYSTEMD_USER_DIR/wake-alarm-trigger.service"
|
|
cp "$TRIGGER_TIMER_SRC" "$SYSTEMD_USER_DIR/wake-alarm-trigger.timer"
|
|
systemctl --user daemon-reload
|
|
echo " Installed wake-alarm.service, wake-alarm-trigger.{service,timer}"
|
|
|
|
# wake-alarm.service must NOT auto-start: the PC is always on, so
|
|
# graphical-session.target is reached on every login and the alarm has no time
|
|
# gate — it would fire at login time. The timer owns firing (via
|
|
# morning-routine.service). Both guards matter under `set -euo pipefail`:
|
|
# a unit with no [Install] can't be enabled, and disabling a never-enabled unit
|
|
# can also return non-zero.
|
|
systemctl --user disable wake-alarm.service 2>/dev/null || true
|
|
echo " wake-alarm.service left disabled (started only by the trigger)"
|
|
|
|
systemctl --user enable --now wake-alarm-trigger.timer
|
|
echo " Trigger timer enabled and started (ticks once a minute)"
|
|
|
|
# 4. Install systemd-sleep hook (restarts alarm after hibernate resume)
|
|
echo "[4/9] Installing systemd-sleep hook..."
|
|
sudo cp "$SLEEP_HOOK_SRC" "$SLEEP_HOOK_DST"
|
|
sudo chmod 0755 "$SLEEP_HOOK_DST"
|
|
echo " Installed to $SLEEP_HOOK_DST"
|
|
|
|
# 5. Add sudoers entry for rtcwake (requires root)
|
|
echo "[5/9] Setting up sudoers for rtcwake..."
|
|
SUDOERS_LINE="$USER ALL=(root) NOPASSWD: $RTCWAKE_BIN"
|
|
if [[ -f "$SUDOERS_FILE" ]] && grep -qF "$SUDOERS_LINE" "$SUDOERS_FILE"; then
|
|
echo " Sudoers entry already exists"
|
|
else
|
|
echo " Adding sudoers entry (requires sudo)..."
|
|
echo "$SUDOERS_LINE" | sudo tee "$SUDOERS_FILE" > /dev/null
|
|
sudo chmod 0440 "$SUDOERS_FILE"
|
|
echo " Added: $SUDOERS_LINE"
|
|
fi
|
|
|
|
# 6. Install shutdown wrapper (/usr/local/bin/shutdown shadows /usr/bin/shutdown)
|
|
echo "[6/9] Installing shutdown wrapper..."
|
|
sudo cp "$SHUTDOWN_WRAPPER_SRC" "$SHUTDOWN_WRAPPER_DST"
|
|
sudo chmod 0755 "$SHUTDOWN_WRAPPER_DST"
|
|
echo " Installed to $SHUTDOWN_WRAPPER_DST"
|
|
echo " 'shutdown now' will now hibernate (not poweroff) on alarm nights."
|
|
|
|
# 7. Install fan-control script and its sudoers entry
|
|
echo "[7/9] Installing fan-control script..."
|
|
sudo cp "$FANS_SCRIPT_SRC" "$FANS_SCRIPT_DST"
|
|
sudo chmod 0755 "$FANS_SCRIPT_DST"
|
|
FANS_SUDOERS_LINE="$USER ALL=(root) NOPASSWD: $FANS_SCRIPT_DST"
|
|
if [[ -f "$SUDOERS_FILE" ]] && grep -qF "$FANS_SUDOERS_LINE" "$SUDOERS_FILE"; then
|
|
echo " Fan sudoers entry already exists"
|
|
else
|
|
# Append to existing file (or create)
|
|
echo "$FANS_SUDOERS_LINE" | sudo tee -a "$SUDOERS_FILE" > /dev/null
|
|
sudo chmod 0440 "$SUDOERS_FILE"
|
|
echo " Added fan sudoers entry"
|
|
fi
|
|
|
|
# 8. Install python-kasa (AUR) for TP-Link Tapo P110 smart-plug control
|
|
echo "[8/9] Installing python-kasa (AUR)..."
|
|
if python -c 'import kasa' 2>/dev/null; then
|
|
echo " python-kasa already installed"
|
|
elif command -v yay &>/dev/null; then
|
|
yay -S --noconfirm --needed python-kasa
|
|
else
|
|
echo " WARNING: yay not found; install python-kasa manually for smart-plug support" >&2
|
|
fi
|
|
if [[ ! -f "$HOME/.config/wake_alarm/tapo.json" ]]; then
|
|
echo " NOTE: ~/.config/wake_alarm/tapo.json not found — smart-plug control is disabled."
|
|
echo " Create it (mode 0600) with keys: host, email, password."
|
|
fi
|
|
|
|
# 9. Install ddcutil for DDC/CI monitor power control
|
|
# ddcutil lets the alarm force the G27Q on via DDC/CI even when the monitor
|
|
# was physically powered off (power button), bypassing DPMS limitations.
|
|
echo "[9/9] Installing ddcutil (DDC/CI monitor power control)..."
|
|
if command -v ddcutil &>/dev/null; then
|
|
echo " ddcutil already installed"
|
|
else
|
|
sudo pacman -S --noconfirm ddcutil
|
|
echo " ddcutil installed"
|
|
fi
|
|
# ddcutil needs access to /dev/i2c-* — add user to i2c group if it exists.
|
|
if getent group i2c &>/dev/null; then
|
|
if ! id -nG "$USER" | grep -qw i2c; then
|
|
sudo usermod -aG i2c "$USER"
|
|
echo " Added $USER to i2c group (re-login required for group to take effect)"
|
|
else
|
|
echo " $USER already in i2c group"
|
|
fi
|
|
else
|
|
echo " i2c group not found — ddcutil will run via sudo"
|
|
fi
|
|
|
|
echo "=== Installation complete ==="
|
|
echo "The trigger timer ticks once a minute and fires the alarm at the time"
|
|
echo "the phone last synced (needs ~/.config/wake_alarm/sync.json with a"
|
|
echo "repo-scoped GitHub token — see MIGRATION.md)."
|
|
echo "Check what it resolves right now: python -m wake_alarm._sync --show"
|
|
echo "If the PC is powered down instead, the shutdown wrapper hibernates with"
|
|
echo "rtcwake set to the same alarm, and the sleep hook runs it on resume."
|
|
echo "Fans will ramp to 100% while the alarm is active, then restore automatically."
|
|
echo "To test now: python -m wake_alarm._alarm --demo"
|