Files
wehub-resource-sync 1d1286fadb
Build / Build and test (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 13:18:38 +08:00

163 lines
4.7 KiB
Swift

import Foundation
import MacToolsPluginKit
// MARK: - Fan Preset
struct FanPreset: Identifiable, Codable, Equatable {
let id: String
var name: String
var strategy: FanControlStrategy
/// Built-in presets (auto / full-speed) cannot be deleted or renamed.
let isBuiltIn: Bool
init(id: String, name: String, strategy: FanControlStrategy, isBuiltIn: Bool = false) {
self.id = id
self.name = name
self.strategy = strategy
self.isBuiltIn = isBuiltIn
}
func displayName(localization: PluginLocalization = PluginLocalization(bundle: .main)) -> String {
switch id {
case FanPresetBuiltInID.auto:
return localization.string("preset.auto.name", defaultValue: "自动")
case FanPresetBuiltInID.fullSpeed:
return localization.string("preset.fullSpeed.name", defaultValue: "全速")
default:
return name
}
}
}
enum FanControlStrategy: Codable, Equatable {
/// Restore macOS automatic fan management.
case auto
/// Set all fans to their hardware-reported maximum speed.
case fullSpeed
/// Set all fans to a fixed RPM (clamped to hardware limits at write time).
case fixed(rpm: Int)
}
// MARK: - Built-in Preset IDs
enum FanPresetBuiltInID {
static let auto = "builtin-auto"
static let fullSpeed = "builtin-full-speed"
}
// MARK: - Fan Snapshot
struct FanSnapshot: Equatable {
var fanCount: Int
var fanSpeeds: [Int]
var fanMinSpeeds: [Int]
var fanMaxSpeeds: [Int]
var cpuTemperature: Double?
var averageSpeed: Int? {
guard !fanSpeeds.isEmpty else { return nil }
return fanSpeeds.reduce(0, +) / fanSpeeds.count
}
var globalMaxSpeed: Int {
fanMaxSpeeds.max() ?? FanRPMLimits.fallbackMax
}
static let empty = FanSnapshot(
fanCount: 0,
fanSpeeds: [],
fanMinSpeeds: [],
fanMaxSpeeds: [],
cpuTemperature: nil
)
func isMeaningfullyEquivalent(
to other: FanSnapshot,
rpmTolerance: Int = 10,
temperatureTolerance: Double = 0.5
) -> Bool {
guard
fanCount == other.fanCount,
fanMinSpeeds == other.fanMinSpeeds,
fanMaxSpeeds == other.fanMaxSpeeds,
fanSpeeds.count == other.fanSpeeds.count
else {
return false
}
for (lhs, rhs) in zip(fanSpeeds, other.fanSpeeds) {
if abs(lhs - rhs) > rpmTolerance {
return false
}
}
switch (cpuTemperature, other.cpuTemperature) {
case (.none, .none):
return true
case let (.some(lhs), .some(rhs)):
return abs(lhs - rhs) <= temperatureTolerance
case (.some, .none), (.none, .some):
return false
}
}
}
// MARK: - RPM Limits
enum FanRPMLimits {
static let absoluteMin = 500
static let absoluteMax = 8000
static let fallbackMin = 1000
static let fallbackMax = 5200
static let defaultCustomRPM = 3000
}
// MARK: - Write Error
enum FanWriteError: Error, LocalizedError {
case helperNotFound
case helperInstallFailed(String)
case helperVerificationFailed
case writeFailed(String)
var errorDescription: String? {
localizedDescription()
}
func localizedDescription(localization: PluginLocalization = PluginLocalization(bundle: .main)) -> String {
switch self {
case .helperNotFound:
return localization.string(
"writeError.helperNotFound",
defaultValue: "未找到内置风扇控制组件。请重新安装风扇控制插件。"
)
case .helperInstallFailed(let msg):
return localization.format(
"writeError.helperInstallFailed",
defaultValue: "安装风扇控制组件失败:%@",
msg
)
case .helperVerificationFailed:
return localization.string(
"writeError.helperVerificationFailed",
defaultValue: "风扇控制组件校验失败。请重新安装风扇控制插件。"
)
case .writeFailed(let msg):
return localization.format("writeError.writeFailed", defaultValue: "写入风扇速度失败:%@", msg)
}
}
}
// MARK: - SMC Protocols
protocol FanControlSMCReading: AnyObject {
@MainActor func readSnapshot() -> FanSnapshot
}
protocol FanControlSMCWriting: AnyObject {
@MainActor var isHelperAvailable: Bool { get }
@MainActor var isInstalledHelperAvailable: Bool { get }
@MainActor @discardableResult
func apply(strategy: FanControlStrategy, snapshot: FanSnapshot) -> FanWriteError?
}