Skip to content

fix(fans): use IOKit power notifications for sleep/wake fan state#3189

Draft
marxo126 wants to merge 2 commits intoexelban:masterfrom
marxo126:fix/sleep-wake-iokit
Draft

fix(fans): use IOKit power notifications for sleep/wake fan state#3189
marxo126 wants to merge 2 commits intoexelban:masterfrom
marxo126:fix/sleep-wake-iokit

Conversation

@marxo126
Copy link
Copy Markdown

@marxo126 marxo126 commented May 7, 2026

Problem

Fan sleep/wake handling currently lives in Modules/Sensors/popup.swift and uses NSWorkspace.didWakeNotification / NSWorkspace.willSleepNotification. This has three structural issues:

  1. NSWorkspace.didWakeNotification arrives after SMC is ready only intermittently — IORegisterForSystemPower (IOKit) fires earlier and more reliably.
  2. The sleep/wake observers are added on the popup view (FanView). If the user closes the menu bar popup between sleep and wake, the observers are torn down with the view and the user's manual fan mode is never restored.
  3. willSleepMode / willSleepSpeed snapshot lives on the popup view, so it is only valid while the popup is alive.

This causes the regressions reported in #3091 ("manual mode resets to auto after sleep/wake") and #2977 ("fan goes to 100% after sleep" — same root cause path: snapshot lost, restoration falls back to default).

Change

  • New Modules/Sensors/fanPower.swiftFanPowerManager.shared singleton subscribing to IORegisterForSystemPower. Holds the per-fan (mode, speed) snapshot dictionary at the app level, independent of any view lifecycle.
  • On kIOMessageSystemWillSleep: snapshot every registered fan with non-automatic mode, then set automatic before sleep. Acknowledge with IOAllowPowerChange.
  • On kIOMessageSystemHasPoweredOn: 2-second delay (let SMC settle), then restore mode + speed via SMCHelper.shared. Clear snapshot.
  • Modules/Sensors/popup.swift — removed the NSWorkspace observers, wakeListener, sleepListener, willSleepMode, willSleepSpeed, and the now-dead resetModeAfterSleep re-sync block. The reader's normal tick picks up the restored mode on next read.
  • Modules/Sensors/main.swift — registers each discovered fan with FanPowerManager.shared at module init time.

Net diff

4 files changed, 121 insertions(+), 55 deletions(-)
 Modules/Sensors/fanPower.swift  | 114 ++++++++++++++++ (new)
 Modules/Sensors/main.swift      |   3 +
 Modules/Sensors/popup.swift     | -55
 Stats.xcodeproj/project.pbxproj |   4 +

Testing

Logic-reviewed against the existing fan code paths but not yet tested on real hardware — flagging this as Draft so you can take a look at the approach before I run it through sleep/wake on Apple Silicon.

If the approach lands well I'm happy to:

  • Run a sleep/wake matrix on Apple Silicon (M-series) and report
  • Add an integration shim if you want this gated behind a settings toggle for safety during rollout

Marking Draft. Happy to iterate or split further if you'd prefer the new singleton lives elsewhere (e.g. Kit/).

marxo126 added 2 commits May 7, 2026 10:59
NSWorkspace.didWakeNotification fires too late and unreliably for SMC
restoration. Move sleep/wake handling to IORegisterForSystemPower in a
new app-level FanPowerManager singleton, decoupled from the popup view.

Fixes the case where the popup is closed at sleep time — previously the
observers were torn down with the popup and the user's manual fan mode
was lost on wake.

Closes exelban#3091
Closes exelban#2977
…onstants

Build fixes after initial commit:
- IOPowerSourceCallbackType does not exist; correct type is IOServiceInterestCallback
- kIOMessageSystemWillSleep/kIOMessageSystemHasPoweredOn are not bridged
  through IOKit.pwr_mgt; declare locally with their canonical hex values
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant