#!/bin/bash ################################################################################ # menus/power_schedule.sh - "Power / Display / Quiet Hours" menu. # # Sixth menu migrated, and the biggest and riskiest so far: it writes # systemd timers/services, a cron entry, and shell scripts that can power # off the physical machine, blank the display, mute audio, and (via RTC) # wake the machine back up on a schedule. Every write goes through # $SYSTEMD_DIR / $CRON_D_DIR / $BIN_DIR (lib/config.sh) rather than # hardcoded /etc/systemd/system, /etc/cron.d, /usr/local/bin, so tests can # point them at a scratch directory - this file must never assume it's # safe to actually mutate the real system just because it's running. # # Deliberately out of scope: the legacy dispatcher's "6. Test schedules & # system" led into a shared diagnostics submenu (audio test, network # test, keyboard test, ...) that isn't specific to scheduling and belongs # with a future Advanced/Diagnostics migration instead. What *is* in # scope - testing the schedule you just configured - stays here as the # same inline "test now?" prompts the legacy menu already had. # # Bug fixed vs. the legacy configure_power_display_quiet: it refused to # even open "Configure power schedule" when no RTC wake was detected, # even though shutdown-only scheduling (configure_power_schedule's own # fallback) never needed RTC in the first place. Also: none of shutdown # time/wake time/display off/on/quiet start/end/custom Electron reload # time were validated as HH:MM in the legacy menu (plain `read`, no # format check) - a typo would silently produce a broken OnCalendar= # value. All of those now go through ask_time. # # Depends on: lib/menu.sh, lib/config.sh being sourced first. ################################################################################ ################################################################################ # Shared helpers ################################################################################ rtc_wake_available() { [[ -w /sys/class/rtc/rtc0/wakealarm ]] || sudo test -w /sys/class/rtc/rtc0/wakealarm 2>/dev/null } timer_exists() { [[ -f "$SYSTEMD_DIR/$1" ]] } timer_oncalendar() { grep "^OnCalendar=" "$SYSTEMD_DIR/$1" 2>/dev/null | cut -d'=' -f2 | sed 's/\*-\*-\* //' | sed 's/:00$//' } ################################################################################ # Top-level menu ################################################################################ power_schedule_status() { local any=false if timer_exists kiosk-shutdown.timer; then any=true local t; t=$(timer_oncalendar kiosk-shutdown.timer) || true echo "Power: shutdown daily at ${t:-an unknown time}" fi if timer_exists kiosk-display-off.timer; then any=true echo "Display: off at $(timer_oncalendar kiosk-display-off.timer), on at $(timer_oncalendar kiosk-display-on.timer)" fi if timer_exists kiosk-quiet-start.timer; then any=true echo "Quiet: $(timer_oncalendar kiosk-quiet-start.timer) to $(timer_oncalendar kiosk-quiet-end.timer)" fi if timer_exists kiosk-electron-reload.timer; then any=true echo "Reload: enabled ($(timer_oncalendar kiosk-electron-reload.timer))" fi $any || echo "No schedules configured" echo if rtc_wake_available; then echo "RTC wake: available (can schedule power on/off)" else echo "RTC wake: not available (display/quiet/reload scheduling still works)" fi } power_schedule_menu_builder() { MENU_LABELS=( "Configure power schedule$(rtc_wake_available || echo ' (shutdown only - no RTC wake)')" "Configure display schedule" "Configure quiet hours" "Configure Electron reload schedule" "Remove all schedules" ) MENU_HANDLERS=( action_configure_power_schedule action_configure_display_schedule action_configure_quiet_hours electron_reload_menu action_remove_all_schedules ) } power_schedule_menu() { run_menu "POWER / DISPLAY / QUIET HOURS" power_schedule_menu_builder power_schedule_status } ################################################################################ # Power schedule ################################################################################ action_configure_power_schedule() { echo local rtc_ok=false rtc_wake_available && rtc_ok=true if $rtc_ok; then echo "RTC wake capability detected - can schedule shutdown and wake." else echo "RTC wake not available - shutdown only, no auto-wake." fi echo local shutdown_time wake_time="" shutdown_time=$(ask_time "Shutdown time (24-hour HH:MM)" "22:00") $rtc_ok && wake_time=$(ask_time "Wake time (24-hour HH:MM)" "06:00") sudo systemctl stop kiosk-shutdown.timer 2>/dev/null || true sudo systemctl disable kiosk-shutdown.timer 2>/dev/null || true sudo rm -f "$SYSTEMD_DIR/kiosk-shutdown.service" "$SYSTEMD_DIR/kiosk-shutdown.timer" sudo rm -f "$BIN_DIR/kiosk-power-off.sh" "$BIN_DIR/rtc-wake.sh" sudo rm -f "$CRON_D_DIR/kiosk-rtc-wake" sudo tee "$BIN_DIR/kiosk-power-off.sh" > /dev/null <<'EOF' #!/bin/bash logger "KIOSK: Scheduled shutdown initiated" systemctl poweroff EOF sudo chmod +x "$BIN_DIR/kiosk-power-off.sh" sudo tee "$SYSTEMD_DIR/kiosk-shutdown.service" > /dev/null < /dev/null < /dev/null <<'RTCSCRIPT' #!/bin/bash WAKE_TIME="$1" CURRENT=$(date +%s) WAKE=$(date -d "$WAKE_TIME" +%s) # If wake time is earlier than current time, schedule for tomorrow [[ $WAKE -le $CURRENT ]] && WAKE=$(date -d "tomorrow $WAKE_TIME" +%s) # Clear existing alarm echo 0 > /sys/class/rtc/rtc0/wakealarm 2>/dev/null || true # Set new alarm if echo $WAKE > /sys/class/rtc/rtc0/wakealarm 2>/dev/null; then logger "KIOSK: RTC wake set for $(date -d @$WAKE '+%Y-%m-%d %H:%M:%S')" echo "RTC wake set for $(date -d @$WAKE '+%Y-%m-%d %H:%M:%S')" else logger "KIOSK: ERROR - Failed to set RTC wake" echo "ERROR: Failed to set RTC wake" exit 1 fi RTCSCRIPT sudo chmod +x "$BIN_DIR/rtc-wake.sh" local shutdown_hour="${shutdown_time%%:*}" local shutdown_min="${shutdown_time##*:}" local wake_min=$((10#$shutdown_min - 5)) local wake_hour=$((10#$shutdown_hour)) [[ $wake_min -lt 0 ]] && { wake_min=$((wake_min + 60)); wake_hour=$((wake_hour - 1)); } [[ $wake_hour -lt 0 ]] && wake_hour=$((wake_hour + 24)) sudo tee "$CRON_D_DIR/kiosk-rtc-wake" > /dev/null <> /var/log/kiosk-rtc.log 2>&1 EOF log_info "RTC wake cron job created" fi if enable_and_start_units kiosk-shutdown.timer; then log_success "Power schedule configured: shutdown at ${shutdown_time}$( [[ -n "$wake_time" ]] && echo ", wake at ${wake_time}")" else log_warning "Schedule files written, but systemctl enable/start failed - check 'systemctl status kiosk-shutdown.timer'" fi } ################################################################################ # Display schedule ################################################################################ action_configure_display_schedule() { echo if timer_exists kiosk-shutdown.timer; then log_warning "Power shutdown configured at $(timer_oncalendar kiosk-shutdown.timer) - display will already be off by then" echo fi local doff don doff=$(ask_time "Display OFF time (24-hour HH:MM)" "22:00") don=$(ask_time "Display ON time (24-hour HH:MM)" "06:00") sudo systemctl stop kiosk-display-off.timer kiosk-display-on.timer 2>/dev/null || true sudo systemctl disable kiosk-display-off.timer kiosk-display-on.timer 2>/dev/null || true sudo rm -f "$SYSTEMD_DIR"/kiosk-display-off.{service,timer} "$SYSTEMD_DIR"/kiosk-display-on.{service,timer} sudo rm -f "$BIN_DIR/kiosk-display-off.sh" "$BIN_DIR/kiosk-display-on.sh" sudo tee "$BIN_DIR/kiosk-display-off.sh" > /dev/null </dev/null && logger "KIOSK: xset dpms off success" || logger "KIOSK: xset dpms off failed" # Method 2: vbetool (if available) if command -v vbetool &>/dev/null; then vbetool dpms off 2>/dev/null && echo "✓ vbetool off" || echo "✗ vbetool failed" fi # Method 3: Backlight control (laptops) if [[ -d /sys/class/backlight ]]; then for bl in /sys/class/backlight/*/brightness; do if [[ -w "\$bl" ]]; then echo 0 > "\$bl" 2>/dev/null && echo "✓ backlight off: \$bl" || echo "✗ backlight failed" fi done fi logger "KIOSK: Display turned OFF (scheduled)" EOF sudo chmod +x "$BIN_DIR/kiosk-display-off.sh" sudo tee "$BIN_DIR/kiosk-display-on.sh" > /dev/null </dev/null && logger "KIOSK: xset dpms on success" || logger "KIOSK: xset dpms on failed" # Method 2: vbetool (if available) if command -v vbetool &>/dev/null; then vbetool dpms on 2>/dev/null && echo "✓ vbetool on" || echo "✗ vbetool failed" fi # Method 3: Backlight control (laptops) if [[ -d /sys/class/backlight ]]; then for bl in /sys/class/backlight/*/brightness; do if [[ -w "\$bl" ]]; then cat "\${bl%/*}/max_brightness" > "\$bl" 2>/dev/null && echo "✓ backlight on: \$bl" || echo "✗ backlight failed" fi done fi # Method 4: Wake up input (move mouse) sudo -u ${KIOSK_USER} DISPLAY=:0 XAUTHORITY=${KIOSK_HOME}/.Xauthority xdotool mousemove 1 1 2>/dev/null && logger "KIOSK: mouse wiggle success" || logger "KIOSK: mouse wiggle failed" # Method 5: Signal Electron app to require password if enabled sudo -u ${KIOSK_USER} touch ${KIOSK_DIR}/.display-wake 2>/dev/null && logger "KIOSK: password flag set" || logger "KIOSK: password flag failed" logger "KIOSK: Display turned ON (scheduled)" EOF sudo chmod +x "$BIN_DIR/kiosk-display-on.sh" sudo tee "$SYSTEMD_DIR/kiosk-display-off.service" > /dev/null < /dev/null < /dev/null < /dev/null </dev/null || true sudo systemctl disable kiosk-quiet-start.timer kiosk-quiet-end.timer 2>/dev/null || true sudo rm -f "$SYSTEMD_DIR"/kiosk-quiet-start.{service,timer} "$SYSTEMD_DIR"/kiosk-quiet-end.{service,timer} sudo rm -f "$BIN_DIR/kiosk-quiet-start.sh" "$BIN_DIR/kiosk-quiet-end.sh" case "$qmode" in 2) sudo tee "$BIN_DIR/kiosk-quiet-start.sh" > /dev/null <<'EOF' #!/bin/bash systemctl stop squeezelite 2>/dev/null logger "KIOSK: Quiet hours started - Squeezelite stopped" echo "✓ Quiet hours: Squeezelite stopped" EOF sudo tee "$BIN_DIR/kiosk-quiet-end.sh" > /dev/null <<'EOF' #!/bin/bash systemctl start squeezelite 2>/dev/null logger "KIOSK: Quiet hours ended - Squeezelite started" echo "✓ Quiet hours ended: Squeezelite started" EOF ;; *) qmode=1 sudo tee "$BIN_DIR/kiosk-quiet-start.sh" > /dev/null <<'EOF' #!/bin/bash # Save current volume before muting pactl get-sink-volume @DEFAULT_SINK@ | grep -oE '[0-9]+%' | head -1 | tr -d '%' > /tmp/kiosk-vol-backup 2>/dev/null || echo "100" > /tmp/kiosk-vol-backup pactl set-sink-mute @DEFAULT_SINK@ 1 2>/dev/null logger "KIOSK: Quiet hours started - all audio muted" echo "✓ Quiet hours: All audio muted" EOF sudo tee "$BIN_DIR/kiosk-quiet-end.sh" > /dev/null <<'EOF' #!/bin/bash # Restore previous volume VOL=$(cat /tmp/kiosk-vol-backup 2>/dev/null || echo "100") pactl set-sink-mute @DEFAULT_SINK@ 0 2>/dev/null pactl set-sink-volume @DEFAULT_SINK@ ${VOL}% 2>/dev/null logger "KIOSK: Quiet hours ended - audio restored to ${VOL}%" echo "✓ Quiet hours ended: Audio restored to ${VOL}%" EOF ;; esac sudo chmod +x "$BIN_DIR/kiosk-quiet-start.sh" "$BIN_DIR/kiosk-quiet-end.sh" sudo tee "$SYSTEMD_DIR/kiosk-quiet-start.service" > /dev/null < /dev/null < /dev/null < /dev/null </dev/null || true sudo systemctl disable kiosk-electron-reload.timer 2>/dev/null || true sudo rm -f "$SYSTEMD_DIR"/kiosk-electron-reload.{service,timer} sudo rm -f "$BIN_DIR/kiosk-reload-electron" sudo tee "$BIN_DIR/kiosk-reload-electron" > /dev/null <<'RELOADSCRIPT' #!/bin/bash logger "KIOSK: Scheduled Electron reload" systemctl restart lightdm RELOADSCRIPT sudo chmod +x "$BIN_DIR/kiosk-reload-electron" sudo tee "$SYSTEMD_DIR/kiosk-electron-reload.service" > /dev/null < /dev/null </dev/null || true sudo systemctl disable kiosk-electron-reload.timer 2>/dev/null || true sudo rm -f "$SYSTEMD_DIR"/kiosk-electron-reload.{service,timer} sudo rm -f "$BIN_DIR/kiosk-reload-electron" sudo systemctl daemon-reload 2>/dev/null || true log_success "Automatic Electron reload disabled" fi } ################################################################################ # Remove all ################################################################################ action_remove_all_schedules() { echo ask_yes_no "Remove ALL power/display/quiet/reload schedules?" "n" || { echo "Cancelled"; return; } power_schedule_do_remove_all } # The actual removal, no prompt - shared with Complete Uninstall so that # operation doesn't need to re-implement schedule teardown a second time. power_schedule_do_remove_all() { for timer in kiosk-shutdown kiosk-display-off kiosk-display-on kiosk-quiet-start kiosk-quiet-end kiosk-electron-reload; do sudo systemctl stop "${timer}.timer" 2>/dev/null || true sudo systemctl disable "${timer}.timer" 2>/dev/null || true done sudo rm -f "$SYSTEMD_DIR"/kiosk-shutdown.{service,timer} sudo rm -f "$SYSTEMD_DIR"/kiosk-display-off.{service,timer} "$SYSTEMD_DIR"/kiosk-display-on.{service,timer} sudo rm -f "$SYSTEMD_DIR"/kiosk-quiet-start.{service,timer} "$SYSTEMD_DIR"/kiosk-quiet-end.{service,timer} sudo rm -f "$SYSTEMD_DIR"/kiosk-electron-reload.{service,timer} sudo rm -f "$BIN_DIR/kiosk-power-off.sh" "$BIN_DIR/kiosk-display-off.sh" "$BIN_DIR/kiosk-display-on.sh" sudo rm -f "$BIN_DIR/kiosk-quiet-start.sh" "$BIN_DIR/kiosk-quiet-end.sh" sudo rm -f "$BIN_DIR/rtc-wake.sh" "$BIN_DIR/kiosk-reload-electron" sudo rm -f "$CRON_D_DIR/kiosk-rtc-wake" sudo systemctl daemon-reload 2>/dev/null || true log_success "All schedules removed" }