Files
OpenStats/Packages/OpenStatsKit/Sources/OpenStatsUI/State/FanController.swift
T
gentpan 4741d40e1b OpenStats:macOS 菜单栏系统监控初始版本
- 菜单栏:CPU/GPU/内存多样式、网速两行、温度、风扇
- 面板:概览仪表盘、进程、散热、防休眠,液态玻璃与浅色/深色主题
- 采集层:CPU/内存/网络/GPU/磁盘/电池/进程/SMC 温度与风扇,按界面分级采样
- root 辅助工具:风扇调速、合盖不睡眠,断开时自动恢复
- XcodeGen 工程、单元测试、截图模式
2026-09-13 02:44:05 +05:00

118 lines
3.6 KiB
Swift

import Foundation
import Metrics
import Observation
import SMC
@MainActor
@Observable
public final class FanController {
public enum Mode: String, CaseIterable, Identifiable {
case automatic, cooling, maximum, custom
public var id: String { rawValue }
var title: String {
switch self {
case .automatic: "自动"
case .cooling: "降温"
case .maximum: "强冷"
case .custom: "自定义"
}
}
/// “降温”档位于最低与最高转速之间的 60%
static let coolingLevel = 0.6
}
public private(set) var mode: Mode = .automatic
/// 自定义档位:0 为最低转速,1 为最高转速
public var customLevel: Double = 0.5
public private(set) var isApplying = false
public private(set) var notice: String?
public private(set) var noticeIsError = false
@ObservationIgnored private let helper: HelperClient
@ObservationIgnored private let store: MetricsStore
@ObservationIgnored private let settings: AppSettings
init(helper: HelperClient, store: MetricsStore, settings: AppSettings) {
self.helper = helper
self.store = store
self.settings = settings
}
var hasFans: Bool { !(store.sensors?.fans.isEmpty ?? true) }
func select(_ newMode: Mode) async {
guard newMode != mode || newMode == .custom else { return }
let previous = mode
mode = newMode
if let error = await apply() {
mode = previous
show(error, isError: true)
} else {
clearNotice()
}
}
func commitCustomLevel() async {
guard mode == .custom else { return }
if let error = await apply() { show(error, isError: true) }
}
/// 连接恢复或唤醒后重新下发
func reapply() async {
guard mode != .automatic else { return }
if let error = await apply() { show(error, isError: true) }
}
/// 每次收到新指标时检查温度,自定义档位过低且 CPU 过热时交还系统控制
func evaluateSafety() {
guard mode == .custom, customLevel < 1,
let hottest = store.sensors?.temperature(.cpu)?.maximum,
hottest >= Double(settings.fanSafetyTemperature) else { return }
Task {
mode = .automatic
_ = await apply()
show("CPU 温度达到 \(Format.temperature(hottest, fahrenheit: settings.useFahrenheit)),已恢复系统自动控制", isError: false)
}
}
func targetRPM(for fan: FanState) -> Double {
switch mode {
case .automatic: fan.target
case .cooling: fan.minimum + (fan.maximum - fan.minimum) * Mode.coolingLevel
case .maximum: fan.maximum
case .custom: fan.minimum + (fan.maximum - fan.minimum) * customLevel
}
}
private func apply() async -> String? {
let fans = store.sensors?.fans ?? []
guard !fans.isEmpty else { return "未检测到可调节的风扇" }
isApplying = true
defer { isApplying = false }
if mode == .automatic {
return await helper.resetAllFans()
}
for fan in fans {
if let error = await helper.setFanTarget(fan: fan.id, rpm: targetRPM(for: fan)) {
_ = await helper.resetAllFans()
return error
}
}
return nil
}
private func show(_ message: String, isError: Bool) {
notice = message
noticeIsError = isError
}
private func clearNotice() {
notice = nil
noticeIsError = false
}
}