feat(fans): add temperature-driven fan profile engine#3191
feat(fans): add temperature-driven fan profile engine#3191marxo126 wants to merge 3 commits intoexelban:masterfrom
Conversation
Adds opt-in fan profiles that drive setFanSpeed based on temperature sensor readings using a user-defined curve with hysteresis. Three built-in starter presets (Silent, Balanced, Performance) are editable through a new settings panel. When no profile is enabled the engine is inert and existing manual/automatic fan control is unchanged. Profiles are stored as JSON in ~/Library/Application Support/Stats/. The Sensors popup slider is disabled for fans currently driven by a profile, with a tooltip pointing to the profile name. Closes exelban#3162 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
so just me to understand, you vibe code and spam me with 3 PR and do not test even one of them? |
I tested these on my MacBook and they seem to have worked. I'd like to discuss with you if they would be a good fit for your app. for reason that I did not want to create another app when Stats with Fans already existed then improving it. |
Build fix after initial commit. Store, Constants, localizedString, PreferencesSection, and PreferencesRow are all defined in the Kit framework target.
Two issues found during runtime testing on M4 Max: 1. The CPU temperature group includes per-core sensors. Apple Silicon parks efficiency cores under low load and parked cores report very low values (~1.5°C). Averaging across all CPU sensors pulled the driving temperature 30-40°C below the actual hot-core temperature, so the curve evaluated at idle even when active P-cores were hot. Use max() instead — drives off the worst-case sensor, which is what real thermal control actually responds to. Also add a >5°C floor so any genuinely-stuck-at-zero sensor is ignored. 2. The Sensors module surfaces a 'Fastest fan' pseudo-fan with id < 0 (an aggregate, not a real SMC channel). The engine was writing curve targets to it. Filter to id >= 0 so only real fans receive writes. Verified end-to-end on Apple Silicon: at 67.86°C CPU, Silent curve correctly drives both real fans to 2324 RPM (linear interp between 60°C/1800 and 75°C/2800).
|
Update — runtime verified end-to-end on macOS 26.4.1, Apple Silicon (M4 Max). Test setup:
Result: Both fans ramped to within ±5 RPM of the curve target. Engine writes were correctly hysteresis-skipped on subsequent ticks once the target stabilised. Two real bugs surfaced during testing — both fixed in commit `c71ea3b9`:
Caveat: tested with the same unsigned-dev-build helper bypass as #3190 (necessary because SMAuthorizedClients cert match is required and dev builds are unsigned). Behaviour in a Developer ID-signed production build is identical — the engine code path is the same. Minor unfixed corner case: first tick has `bounds=nil` because `registerFans` runs at module init and the reader callback can fire before that completes. Engine still writes the unclamped target on first tick, then clamps correctly from second tick on. Worth noting but not a blocker — happy to swap `registerFans` to a synchronous call inside `init` if you'd prefer it bullet-proof on the first tick. |


What this adds
Opt-in fan profiles that drive
setFanSpeedbased on temperature sensor readings using a user-defined curve with hysteresis. When no profile is enabled the engine is inert and the existing manual/automatic fan UI is unchanged.~/Library/Application Support/Stats/fan-profiles.json; enable flags viaStore.sharedArchitecture
Modules/Sensors/fanProfile.swift—CurvePoint+FanProfileCodable, presets,FanProfileStoreJSON persistence (~141 LOC)Modules/Sensors/fanProfileEngine.swift—FanProfileEngine.sharedsingleton, hysteresis, RPM clamping (~131 LOC)Modules/Sensors/fanProfileSettings.swift— settings panel with preset picker + enable toggle (~155 LOC)Modules/Sensors/main.swift— registers fans + drivesprocessTickfrom existingusageCallbackModules/Sensors/popup.swift—setProfileControlled(_:)onFanViewto grey out manual slider when a profile is activeModules/Sensors/settings.swift— appends profile section to existing settings under#if arch(arm64)Scope decisions / known gaps
(All open to redirection from your review)
#if arch(arm64). Intel fan control uses a different SMC path; not touched.fanProfileEngine.swift).usageCallbackalready runs; the engine returns early before any SMC interaction.Testing
Logic-reviewed, not yet hardware-tested. Setting up an Apple Silicon validation pass once the approach has your sign-off.
Diff