wake-reasons.sh 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164
  1. #!/usr/bin/env bash
  2. # mac-ops :: wake-reasons.sh
  3. # Why does this Mac wake up? Breakdown of pmset -g log wake events by cause.
  4. #
  5. # Common wake reason classes:
  6. # UserActivity / EHCx — user touched the keyboard / trackpad / a peripheral
  7. # BT.HID — Bluetooth keyboard/mouse activity
  8. # RTC / SMC — scheduled wake (Power Nap, Time Machine, calendar)
  9. # PWRB — power button pressed
  10. # USB.lid / Notifier — lid open or wake-via-USB device
  11. # Maintenance — system maintenance wake (dark wake)
  12. # Network — Wake-on-LAN / Bluetooth proximity
  13. set -u
  14. SINCE_DAYS=7
  15. TOP_N=15
  16. while [[ $# -gt 0 ]]; do
  17. case "$1" in
  18. --since)
  19. # Accept '7d' or '24h' or just N (days)
  20. v="$2"; shift 2
  21. case "$v" in
  22. *d) SINCE_DAYS="${v%d}" ;;
  23. *h) SINCE_DAYS=$(( ${v%h} / 24 )); [[ "$SINCE_DAYS" -lt 1 ]] && SINCE_DAYS=1 ;;
  24. *) SINCE_DAYS="$v" ;;
  25. esac
  26. ;;
  27. --top) TOP_N="$2"; shift 2 ;;
  28. --help|-h)
  29. cat <<EOF
  30. Usage: $0 [options]
  31. --since 7d|24h|N Lookback window (default: 7d)
  32. --top N Show top N wake reasons (default: 15)
  33. --json, --redact, --quiet, --verbose
  34. Wake reason quick reference:
  35. UserActivity Display/keyboard/trackpad — user-driven, expected
  36. BT.HID Bluetooth keyboard/mouse activity (often phantom at night)
  37. RTC Real-time clock — scheduled wake (Power Nap, calendar)
  38. PWRB Power button — manual wake
  39. USB.lid Lid open
  40. Maintenance Background maintenance (dark wake)
  41. Network WoL or Bluetooth proximity peer
  42. Heavy BT.HID wakes overnight usually mean a Bluetooth keyboard is "tapping" the
  43. display awake — easy fix is to disable "Wake for Bluetooth" or unpair the device.
  44. Heavy RTC wakes can mean Power Nap is enabled with too much background work.
  45. EOF
  46. exit 0 ;;
  47. *) shift ;;
  48. esac
  49. done
  50. source "$(dirname "$0")/_lib/common.sh"
  51. parse_common_flags "$@"
  52. maybe_filter_self "$@"
  53. # ----------------------------------------------------------------------------
  54. section "1. WAKE PATTERN OVERVIEW"
  55. # ----------------------------------------------------------------------------
  56. note " Lookback: ${SINCE_DAYS}d (pmset log retains roughly 7-14 days)"
  57. # pmset -g log format on modern macOS:
  58. # "2026-05-10 02:40:55 +1000 DarkWake DarkWake from Deep Idle [CDNPB] : due to NUB.SPMI0Sw3IRQ nub-spmi-a0.0x59 ... rtc/Maintenance ..."
  59. # Wake reasons appear after "due to" and end at "Using" or end-of-line.
  60. # Categories: rtc/Maintenance, rtc/SleepService, SMC.OutboxNotEmpty, NUB.SPMI*, etc.
  61. since_epoch=$(($(date +%s) - SINCE_DAYS * 86400))
  62. since_str=$(date -r "$since_epoch" "+%Y-%m-%d")
  63. raw=$(pmset -g log 2>/dev/null | awk -v since="$since_str" '
  64. $1 >= since && ($0 ~ /DarkWake/ || $0 ~ /[[:space:]]Wake[[:space:]]/) {print}
  65. ')
  66. wake_count=$(echo "$raw" | grep -c . || echo 0)
  67. if [[ "$wake_count" -eq 0 ]]; then
  68. log_info "Wakes (since $since_str)" "0 — Mac hasn't slept, or pmset log was cleared"
  69. emit_summary
  70. exit 0
  71. fi
  72. log_info "Total wake events (since $since_str)" "$wake_count"
  73. # ----------------------------------------------------------------------------
  74. section "2. WAKE REASONS BY CLASS"
  75. # ----------------------------------------------------------------------------
  76. note " Wake-cause class | count | pct"
  77. note " -----------------|-------|----"
  78. # Extract the bit after "due to" up to "Using" — these are the cause tokens.
  79. # Then classify by first significant token.
  80. reasons_raw=$(echo "$raw" | sed -nE 's/.*due to (.*) Using.*/\1/p; s/.*due to (.*)/\1/p' \
  81. | awk '
  82. {
  83. # Each line is a series of tokens. The most informative is usually the last
  84. # one before category-style "rtc/X" or "wifi/" or similar slash-form.
  85. for (i=1; i<=NF; i++) {
  86. if ($i ~ /\//) { print $i; next }
  87. }
  88. print $1 # fallback to first token
  89. }
  90. ')
  91. echo "$reasons_raw" | sort | uniq -c | sort -rn | head -"$TOP_N" | \
  92. while read -r count reason; do
  93. pct=$(( count * 100 / (wake_count > 0 ? wake_count : 1) ))
  94. class="?"
  95. case "$reason" in
  96. rtc/Maintenance*|rtc/Power*) class="rtc scheduled" ;;
  97. rtc/SleepService*) class="push-svc wake" ;;
  98. rtc/*) class="rtc" ;;
  99. SMC.OutboxNotEmpty*|smc/*) class="hardware (SMC)" ;;
  100. NUB.SPMI*|nub-spmi*) class="USB/peripheral" ;;
  101. wifibt/*|wlan/*) class="wifi/bluetooth" ;;
  102. EHC*|HID*|UserActivity) class="user input" ;;
  103. BT*) class="bluetooth peer" ;;
  104. PWRB*|PowerButton*) class="power button" ;;
  105. Maintenance*) class="maintenance" ;;
  106. Network*|WoL*) class="network" ;;
  107. *) class="other" ;;
  108. esac
  109. printf " %-18s | %5d | %3d%% (%s)\n" "$class" "$count" "$pct" "$reason"
  110. done
  111. # ----------------------------------------------------------------------------
  112. section "3. DARK WAKES (background maintenance)"
  113. # ----------------------------------------------------------------------------
  114. # pmset log line format: "DATE TIME TZ DarkWake \tDarkWake from ..."
  115. # The literal "DarkWake" appears in column 4 (after date/time/tz) AND in the message
  116. dark_wakes=$(echo "$raw" | awk '$4=="DarkWake"' | wc -l | tr -d ' ')
  117. log_info "Dark wakes" "$dark_wakes"
  118. if [[ "$dark_wakes" -gt 50 ]]; then
  119. log_warn "Dark wake count" "$dark_wakes — frequent background wakes drain battery"
  120. note " Common causes:"
  121. note " • Power Nap enabled (System Settings → Battery → Options)"
  122. note " • Backup destinations (Time Machine, Backblaze) running"
  123. note " • Calendar / Contacts / iCloud sync"
  124. fi
  125. # ----------------------------------------------------------------------------
  126. section "4. WAKE TIMING (last 24h)"
  127. # ----------------------------------------------------------------------------
  128. yesterday=$(date -v-1d "+%Y-%m-%d")
  129. note " Wakes since $yesterday:"
  130. echo "$raw" | awk -v y="$yesterday" '$1 >= y {print " "$1, $2, $0}' | grep -oE "[0-9]{4}-[0-9]{2}-[0-9]{2} [0-9:]+ .* Wake reason: [A-Za-z0-9_.]+" | tail -20
  131. # ----------------------------------------------------------------------------
  132. section "5. ASSERTIONS HOLDING SYSTEM AWAKE"
  133. # ----------------------------------------------------------------------------
  134. note " Current pmset assertions (who's preventing sleep right now):"
  135. pmset -g assertions 2>/dev/null | grep -E "(IDLE|PreventUserIdleSystemSleep|PreventSystemSleep|PreventDisplay)" | head -10 | sed 's/^/ /'
  136. # ----------------------------------------------------------------------------
  137. section "6. SLEEP/WAKE PREFERENCES"
  138. # ----------------------------------------------------------------------------
  139. note " pmset -g (custom settings):"
  140. pmset -g custom 2>/dev/null | head -25 | sed 's/^/ /'
  141. # ----------------------------------------------------------------------------
  142. emit_summary