#!/usr/bin/env bash # # setup-power.sh — keep a Mac mini awake 24/7 and make it come back on its own. # # What it configures (only the values that differ from what you ask for): # pmset -a sleep 0 never put the whole system to sleep # pmset -a disksleep 0 never spin down / power down disks # pmset -a displaysleep N the display may still sleep (default 10 min) # pmset -a womp 1 "Wake for network access" # pmset -a autorestart 1 "Start up automatically after a power failure" # pmset -a tcpkeepalive 1 keep network connections alive # systemsetup -setrestartfreeze on restart automatically if macOS freezes # pmset repeat wakeorpoweron ... optional daily safety power-on # # Safe by default: without --apply it only prints what it WOULD change. # Idempotent: running it again after --apply reports "already set" for everything. # # Usage: ./setup-power.sh [--apply] [options] (see --help) set -euo pipefail readonly SCRIPT_NAME="${0##*/}" readonly VERSION="1.0.0" APPLY=0 DISPLAY_SLEEP=10 WAKE_ON_NETWORK=1 RESTART_ON_FREEZE=1 DAILY_POWERON="" usage() { cat < Energy (or run 'sudo pmset restoredefaults'). EOF } die() { printf 'error: %s\n' "$*" >&2 exit 2 } log() { printf '%s\n' "$*"; } # Run a command with root privileges: directly when already root, otherwise via sudo. as_root() { if [[ ${EUID:-$(id -u)} -eq 0 ]]; then "$@" else sudo "$@" fi } # Reads that need admin rights (systemsetup needs root even to read a value). # Dry runs never call sudo: unless we're already root, only --apply may elevate. # Prints nothing when the value can't be read without sudo. read_as_admin() { if [[ ${EUID:-$(id -u)} -eq 0 ]]; then "$@" 2>/dev/null elif ((APPLY)); then sudo "$@" 2>/dev/null fi } parse_args() { while [[ $# -gt 0 ]]; do case "$1" in --apply) APPLY=1 ;; --dry-run) APPLY=0 ;; --display-sleep) [[ $# -ge 2 ]] || die "--display-sleep needs a number of minutes" [[ $2 =~ ^[0-9]+$ ]] || die "--display-sleep must be a whole number of minutes, got '$2'" DISPLAY_SLEEP=$2 shift ;; --no-wake-on-network) WAKE_ON_NETWORK=0 ;; --no-restart-on-freeze) RESTART_ON_FREEZE=0 ;; --daily-poweron) [[ $# -ge 2 ]] || die "--daily-poweron needs a time like 04:30" [[ $2 =~ ^([01][0-9]|2[0-3]):[0-5][0-9]$ ]] || die "--daily-poweron must be HH:MM (24h), got '$2'" DAILY_POWERON=$2 shift ;; -h | --help) usage exit 0 ;; -V | --version) printf '%s %s\n' "$SCRIPT_NAME" "$VERSION" exit 0 ;; *) die "unknown option '$1' (try --help)" ;; esac shift done } # Current value of a pmset key from `pmset -g`, e.g. " sleep 0 (sleep prevented by ...)". # Prints nothing if this Mac does not report the key. pmset_current() { pmset -g 2>/dev/null | awk -v key="$1" '$1 == key { print $2; exit }' } # "04:30" -> "4:30AM", matching how `pmset -g sched` prints repeating events. to_ampm() { local hh=${1%%:*} mm=${1##*:} suffix="AM" h h=$((10#$hh)) if ((h >= 12)); then suffix="PM"; fi if ((h == 0)); then h=12; elif ((h > 12)); then h=$((h - 12)); fi printf '%d:%s%s' "$h" "$mm" "$suffix" } main() { parse_args "$@" if [[ $(uname -s) != "Darwin" ]]; then die "this script only runs on macOS (uname reports '$(uname -s)')" fi command -v pmset >/dev/null 2>&1 || die "pmset not found; is this macOS?" if ((APPLY)); then log "Mode: APPLY — settings that differ will be changed." else log "Mode: DRY RUN — nothing will be changed. Re-run with --apply to make changes." fi log "" log "Power settings (pmset):" local -a desired=( "sleep 0" "disksleep 0" "displaysleep $DISPLAY_SLEEP" "autorestart 1" "tcpkeepalive 1" ) if ((WAKE_ON_NETWORK)); then desired+=("womp 1"); fi local -a changes=() local pair key want have for pair in "${desired[@]}"; do key=${pair%% *} want=${pair##* } have=$(pmset_current "$key") if [[ -z $have ]]; then log " [skip] $key: not reported by this Mac (unsupported here)" elif [[ $have == "$want" ]]; then log " [ok] $key = $have" else log " [change] $key: $have -> $want" changes+=("$key" "$want") fi done local pending=0 if ((${#changes[@]})); then pending=1 if ((APPLY)); then as_root pmset -a "${changes[@]}" log " applied: pmset -a ${changes[*]}" else log " would run: sudo pmset -a ${changes[*]}" fi fi if ((RESTART_ON_FREEZE)); then log "" log "Restart after freeze (systemsetup):" local freeze freeze=$(read_as_admin systemsetup -getrestartfreeze | awk -F': ' '{ print $2 }' || true) if [[ $freeze == "On" ]]; then log " [ok] restart after freeze = On" elif [[ -z $freeze ]] && ((!APPLY)); then pending=1 log " [unknown] restart after freeze: reading it needs admin rights, and dry runs never use sudo." log " Check it yourself: sudo systemsetup -getrestartfreeze" log " would run (if not already On): sudo systemsetup -setrestartfreeze on" else pending=1 log " [change] restart after freeze: ${freeze:-unknown} -> On" if ((APPLY)); then as_root systemsetup -setrestartfreeze on >/dev/null log " applied: systemsetup -setrestartfreeze on" else log " would run: sudo systemsetup -setrestartfreeze on" fi fi fi if [[ -n $DAILY_POWERON ]]; then log "" log "Daily safety power-on (pmset repeat):" local expected expected="wakepoweron at $(to_ampm "$DAILY_POWERON") every day" if pmset -g sched 2>/dev/null | grep -qiF "$expected"; then log " [ok] $expected" else pending=1 log " [change] schedule: $expected" log " (replaces any existing repeating power event)" if ((APPLY)); then as_root pmset repeat wakeorpoweron MTWRFSU "${DAILY_POWERON}:00" log " applied: pmset repeat wakeorpoweron MTWRFSU ${DAILY_POWERON}:00" else log " would run: sudo pmset repeat wakeorpoweron MTWRFSU ${DAILY_POWERON}:00" fi fi fi log "" if ((!pending)); then log "Everything is already set. Nothing to do." elif ((APPLY)); then log "Done. Verify with: pmset -g (and ./health-check.sh)" else log "Dry run complete. Re-run with --apply to make these changes." fi } main "$@"