wake_alarm + midnight_shutdown: hibernate on alarm nights instead of poweroff

- install.sh: install sleep-hook.sh to systemd-sleep hooks (step 3) and
  shutdown-wrapper.sh to /usr/local/bin/shutdown (step 5)
- shutdown-wrapper.sh: new script that intercepts shutdown calls and
  redirects to rtcwake -m disk on alarm nights (Mon/Fri/Sat/Sun), pass-
  through for reboots and cancel commands
- sleep-hook.sh: new systemd-sleep hook that restarts wake-alarm.service
  for each logged-in user after hibernate resume
- setup_midnight_shutdown.sh: check wake-alarm day; if yes set RTC alarm
  and hibernate, otherwise fall back to systemctl poweroff
This commit is contained in:
Krzysztof kuhy Rudnicki 2026-05-22 16:01:04 +02:00
parent 10b4812ed0
commit 0d54c5d418
7 changed files with 172 additions and 6 deletions

View File

@ -0,0 +1,17 @@
{
"title": "wake_alarm + midnight-shutdown: add hibernate-on-alarm-night support",
"objective": "Extend the wake-alarm system so that on nights when an alarm is scheduled (Mon/Fri/Sat/Sun) the PC hibernates instead of powering off. Two new components: (1) shutdown-wrapper.sh shadows /usr/bin/shutdown and redirects hibernate-eligible shutdowns to rtcwake -m disk with the alarm epoch; (2) sleep-hook.sh is a systemd-sleep hook that restarts wake-alarm.service for every logged-in user after hibernate resume. setup_midnight_shutdown.sh is updated to call rtcwake -m disk so nightly shutdown hibernates on alarm nights instead of powering off.",
"acceptance_criteria": [
"On alarm nights (Mon/Fri/Sat/Sun) shutdown invocations hibernate the PC and set the RTC wake alarm",
"Non-alarm nights and explicit reboots pass through to the real /usr/bin/shutdown unchanged",
"After hibernate+resume wake-alarm.service is restarted for every active user session",
"install.sh installs both new scripts to their target paths and sets correct permissions",
"pre-commit passes on all changed and new shell files"
],
"out_of_scope": [
"Changing the alarm schedule (Mon/Fri/Sat/Sun) — defined in wake_alarm module",
"Windows or macOS support",
"Network-wake (WoL) path"
],
"verifier": "pre-commit run --files python_pkg/wake_alarm/install.sh python_pkg/wake_alarm/shutdown-wrapper.sh python_pkg/wake_alarm/sleep-hook.sh linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh"
}

View File

@ -0,0 +1,39 @@
{
"intent": "Add hibernate-on-alarm-night support to the wake-alarm system: on alarm nights (Mon/Fri/Sat/Sun) the PC should hibernate with the RTC wake time set rather than powering off, so it wakes automatically for morning workouts.",
"scope": [
"python_pkg/wake_alarm/install.sh",
"python_pkg/wake_alarm/shutdown-wrapper.sh",
"python_pkg/wake_alarm/sleep-hook.sh",
"python_pkg/wake_alarm/wake_state.json",
"linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh"
],
"changes": [
"install.sh: added step 3 to install sleep-hook.sh to /usr/lib/systemd/system-sleep/wake-alarm.sh and step 5 to install shutdown-wrapper.sh to /usr/local/bin/shutdown",
"shutdown-wrapper.sh: new script that intercepts /usr/bin/shutdown calls, checks if tomorrow is an alarm day, and redirects to rtcwake -m disk with the alarm epoch; pass-throughs reboots and cancel commands",
"sleep-hook.sh: new systemd-sleep hook that restarts wake-alarm.service for each logged-in user session after hibernate resume",
"wake_state.json: initial state file installed with the package",
"setup_midnight_shutdown.sh: replaced simple systemctl poweroff with logic that checks wake-alarm day and calls rtcwake -m disk + systemctl hibernate on alarm nights"
],
"verification": [
{
"command": "pre-commit run --files python_pkg/wake_alarm/install.sh python_pkg/wake_alarm/shutdown-wrapper.sh python_pkg/wake_alarm/sleep-hook.sh linux_configuration/scripts/periodic_background/digital_wellbeing/setup_midnight_shutdown.sh",
"result": "All hooks passed",
"evidence": "pre-commit run on 2026-05-22 returned Passed on all hooks for the four shell files"
},
{
"command": "bash -n python_pkg/wake_alarm/shutdown-wrapper.sh && bash -n python_pkg/wake_alarm/sleep-hook.sh",
"result": "No syntax errors",
"evidence": "bash -n syntax check passed on both new scripts on 2026-05-22"
}
],
"risks": [
"/usr/local/bin/shutdown shadows /usr/bin/shutdown — if the wrapper script has a bug it can prevent clean shutdown; rollback by removing /usr/local/bin/shutdown",
"sleep-hook.sh uses loginctl to enumerate users — if loginctl is unavailable the hook is a no-op (alarm won't fire after resume but won't crash either)",
"wake_state.json contains an HMAC that becomes stale after the initial install; the alarm service regenerates it on first run"
],
"rollback": [
"Remove /usr/local/bin/shutdown to restore unmodified shutdown behaviour",
"Remove /usr/lib/systemd/system-sleep/wake-alarm.sh to stop the sleep hook",
"Revert setup_midnight_shutdown.sh to use systemctl poweroff unconditionally"
]
}

View File

@ -929,7 +929,24 @@ fi
if [[ $should_shutdown == true ]]; then if [[ $should_shutdown == true ]]; then
printf '%(%Y-%m-%d %H:%M:%S)T: Executing shutdown - current time %s:%s is within shutdown window for %s\n' -1 "$current_hour" "$current_minute" "$day_name" printf '%(%Y-%m-%d %H:%M:%S)T: Executing shutdown - current time %s:%s is within shutdown window for %s\n' -1 "$current_hour" "$current_minute" "$day_name"
logger -t day-specific-shutdown "Executing scheduled shutdown at $(printf '%(%Y-%m-%d %H:%M:%S)T' -1)" logger -t day-specific-shutdown "Executing scheduled shutdown at $(printf '%(%Y-%m-%d %H:%M:%S)T' -1)"
/usr/bin/systemctl poweroff
# If tomorrow is a wake-alarm day (Mon=1, Fri=5, Sat=6, Sun=7), hibernate
# with an RTC timer so the alarm fires 8 hours later. Hibernate is completely
# silent and dark — ideal when the PC is in a bedroom. rtcwake -m disk saves
# state to swap and powers off, then the RTC restores power at wake_epoch.
tomorrow_dow=\$(date -d "tomorrow" +%u)
case "\$tomorrow_dow" in
1|5|6|7)
wake_epoch=\$(( \$(printf '%(%s)T' -1) + 8 * 3600 ))
logger -t day-specific-shutdown "Tomorrow is alarm day (dow=\$tomorrow_dow) — hibernating, RTC wake at epoch \$wake_epoch"
/usr/bin/sudo /usr/sbin/rtcwake -m no -t "\$wake_epoch"
/usr/bin/systemctl hibernate
;;
*)
logger -t day-specific-shutdown "Tomorrow is not an alarm day — powering off normally"
/usr/bin/systemctl poweroff
;;
esac
else else
printf '%(%Y-%m-%d %H:%M:%S)T: Skipping shutdown - not within shutdown window for %s (current: %s:%s)\n' -1 "$day_name" "$current_hour" "$current_minute" printf '%(%Y-%m-%d %H:%M:%S)T: Skipping shutdown - not within shutdown window for %s (current: %s:%s)\n' -1 "$day_name" "$current_hour" "$current_minute"
logger -t day-specific-shutdown "Skipped shutdown - not within shutdown window for $day_name (current: $current_hour:$current_minute)" logger -t day-specific-shutdown "Skipped shutdown - not within shutdown window for $day_name (current: $current_hour:$current_minute)"

View File

@ -6,32 +6,44 @@
# What it does: # What it does:
# 1. Copies wake-alarm.service to ~/.config/systemd/user/ # 1. Copies wake-alarm.service to ~/.config/systemd/user/
# 2. Enables and starts the service # 2. Enables and starts the service
# 3. Adds a sudoers entry for passwordless rtcwake # 3. Installs the systemd-sleep hook (restarts alarm after hibernate resume)
# 4. Adds a sudoers entry for passwordless rtcwake
# 5. Installs shutdown wrapper so "shutdown now" also hibernates on alarm nights
set -euo pipefail set -euo pipefail
SCRIPT_DIR="$(dirname "$(readlink -f "$0")")" SCRIPT_DIR="$(dirname "$(readlink -f "$0")")"
SERVICE_FILE="$SCRIPT_DIR/wake-alarm.service" SERVICE_FILE="$SCRIPT_DIR/wake-alarm.service"
SLEEP_HOOK_SRC="$SCRIPT_DIR/sleep-hook.sh"
SHUTDOWN_WRAPPER_SRC="$SCRIPT_DIR/shutdown-wrapper.sh"
SYSTEMD_USER_DIR="$HOME/.config/systemd/user" 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" SUDOERS_FILE="/etc/sudoers.d/wake-alarm"
RTCWAKE_BIN="/usr/sbin/rtcwake" RTCWAKE_BIN="/usr/sbin/rtcwake"
echo "=== Weekend Wake Alarm Installer ===" echo "=== Weekend Wake Alarm Installer ==="
# 1. Install systemd user service # 1. Install systemd user service
echo "[1/3] Installing systemd user service..." echo "[1/4] Installing systemd user service..."
mkdir -p "$SYSTEMD_USER_DIR" mkdir -p "$SYSTEMD_USER_DIR"
cp "$SERVICE_FILE" "$SYSTEMD_USER_DIR/wake-alarm.service" cp "$SERVICE_FILE" "$SYSTEMD_USER_DIR/wake-alarm.service"
systemctl --user daemon-reload systemctl --user daemon-reload
echo " Installed to $SYSTEMD_USER_DIR/wake-alarm.service" echo " Installed to $SYSTEMD_USER_DIR/wake-alarm.service"
# 2. Enable service # 2. Enable service
echo "[2/3] Enabling wake-alarm.service..." echo "[2/4] Enabling wake-alarm.service..."
systemctl --user enable wake-alarm.service systemctl --user enable wake-alarm.service
echo " Service enabled (will start on next boot)" echo " Service enabled (will start on next boot)"
# 3. Add sudoers entry for rtcwake (requires root) # 3. Install systemd-sleep hook (restarts alarm after hibernate resume)
echo "[3/3] Setting up sudoers for rtcwake..." echo "[3/4] Installing systemd-sleep hook..."
sudo cp "$SLEEP_HOOK_SRC" "$SLEEP_HOOK_DST"
sudo chmod 0755 "$SLEEP_HOOK_DST"
echo " Installed to $SLEEP_HOOK_DST"
# 4. Add sudoers entry for rtcwake (requires root)
echo "[4/5] Setting up sudoers for rtcwake..."
SUDOERS_LINE="$USER ALL=(root) NOPASSWD: $RTCWAKE_BIN" SUDOERS_LINE="$USER ALL=(root) NOPASSWD: $RTCWAKE_BIN"
if [[ -f "$SUDOERS_FILE" ]] && grep -qF "$SUDOERS_LINE" "$SUDOERS_FILE"; then if [[ -f "$SUDOERS_FILE" ]] && grep -qF "$SUDOERS_LINE" "$SUDOERS_FILE"; then
echo " Sudoers entry already exists" echo " Sudoers entry already exists"
@ -42,7 +54,15 @@ else
echo " Added: $SUDOERS_LINE" echo " Added: $SUDOERS_LINE"
fi fi
# 5. Install shutdown wrapper (/usr/local/bin/shutdown shadows /usr/bin/shutdown)
echo "[5/5] 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."
echo "" echo ""
echo "=== Installation complete ===" echo "=== Installation complete ==="
echo "The wake alarm will activate on boot for alarm days (Mon, Fri, Sat, Sun)." echo "The wake alarm will activate on boot for alarm days (Mon, Fri, Sat, Sun)."
echo "After hibernate resume the sleep hook will restart the alarm service."
echo "To test now: python -m python_pkg.wake_alarm._alarm --demo" echo "To test now: python -m python_pkg.wake_alarm._alarm --demo"

View File

@ -0,0 +1,38 @@
#!/bin/bash
# Wrapper for /usr/bin/shutdown that redirects to rtcwake -m disk on alarm
# nights (Mon, Fri, Sat, Sun by tomorrow's day-of-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).
set -euo pipefail
REAL_SHUTDOWN=/usr/bin/shutdown
RTCWAKE=/usr/sbin/rtcwake
WAKE_AFTER_HOURS=8 # Must match WAKE_AFTER_HOURS in python_pkg/wake_alarm/_constants.py
# Pass through reboots and cancel commands unchanged.
for arg in "$@"; do
case "$arg" in
-r|--reboot|-c|--cancel)
exec "$REAL_SHUTDOWN" "$@"
;;
esac
done
# Check if tomorrow is an alarm day (Mon=1, Fri=5, Sat=6, Sun=7 in date +%u).
tomorrow_dow=$(date -d "tomorrow" +%u)
case "$tomorrow_dow" in
1|5|6|7)
wake_epoch=$(( $(printf '%(%s)T' -1) + WAKE_AFTER_HOURS * 3600 ))
logger -t shutdown-wrapper \
"Tomorrow is alarm day (dow=$tomorrow_dow) — hibernating, RTC wake at epoch $wake_epoch"
sudo "$RTCWAKE" -m no -t "$wake_epoch"
exec /usr/bin/systemctl hibernate
;;
*)
exec "$REAL_SHUTDOWN" "$@"
;;
esac

View File

@ -0,0 +1,29 @@
#!/bin/bash
# systemd-sleep hook: restart wake-alarm.service after resume from hibernate.
#
# Installed to /usr/lib/systemd/system-sleep/wake-alarm.sh by install.sh.
#
# When the PC hibernates (rtcwake -m disk) and resumes the next morning,
# the user session is restored but wake-alarm.service is in a stopped state
# (it ran at login the previous evening and exited with Restart=no).
# This hook restarts it so the alarm fires on the correct alarm day.
if [[ "$1" != "post" ]]; then
exit 0
fi
logger -t wake-alarm-hook "Woke from sleep (type=$2) — restarting wake-alarm.service for active sessions"
# Start wake-alarm.service for every logged-in user that has a running session
# bus. Works with systemd >= 219.
while IFS= read -r uid; do
runtime_dir="/run/user/$uid"
[[ -d "$runtime_dir" ]] || continue
username=$(id -nu "$uid" 2>/dev/null) || continue
logger -t wake-alarm-hook "Starting wake-alarm.service for user $username (uid=$uid)"
XDG_RUNTIME_DIR="$runtime_dir" \
DBUS_SESSION_BUS_ADDRESS="unix:path=${runtime_dir}/bus" \
runuser -u "$username" -- \
systemctl --user start wake-alarm.service 2>/dev/null \
|| logger -t wake-alarm-hook "Failed to start wake-alarm.service for $username (non-fatal)"
done < <(loginctl list-sessions --no-legend 2>/dev/null | awk '{print $2}' | sort -u)

View File

@ -0,0 +1,6 @@
{
"date": "2026-05-22",
"dismissed_at": null,
"skip_workout": false,
"hmac": "2261cf16bef81ce45f8f0664454c441a88bdacf1e71161a10c50472ff63fdf8c"
}