import Foundation /// Der von der Firmware erlaubte Drehzahlbereich eines Lüfters. public struct FanLimits: Equatable, Sendable, Codable { public let minimum: Double public let maximum: Double public init(minimum: Double, maximum: Double) { self.minimum = minimum self.maximum = maximum } /// Ob die Werte überhaupt brauchbar sind. Nicht lesbare Keys ergeben Nullen. public var isUsable: Bool { minimum > 0 && maximum > minimum } } public struct FanRequest: Equatable, Sendable, Codable { public let targetRPM: Double public init(targetRPM: Double) { self.targetRPM = targetRPM } } public enum FanDecision: Equatable, Sendable { case manual(rpm: Double) case automatic(reason: AutomaticReason) public enum AutomaticReason: String, Equatable, Sendable { /// Niemand will etwas anderes. case noRequest /// Die App hat sich zu lange nicht gemeldet. case watchdog /// Es ist zu heiß für eine feste Drehzahl. case temperature /// Ohne Temperaturmesswert lässt sich Sicherheit nicht beurteilen. case noTemperature /// Der erlaubte Bereich ist unbekannt. case unknownLimits } } /// Entscheidet, ob eine angeforderte Drehzahl gesetzt werden darf. /// /// Diese Regeln sind **nicht abschaltbar** und liegen im Helfer, nicht in der /// Oberfläche. Die Oberfläche kann abstürzen, eingefroren oder beendet sein, /// während die Lüfter auf einem festen Wert stehen — genau dafür gibt es das /// Netz. Als reine Funktion ist es vollständig prüfbar, ohne Hardware. public enum FanSafety { /// Meldet sich die App länger nicht, übernimmt die Automatik. public static let watchdogTimeout: TimeInterval = 5 /// Ab hier zählt nur noch Kühlung. Apple Silicon drosselt in dieser Gegend /// von selbst; eine festgenagelte Drehzahl verschlimmert die Lage, weil sie /// nicht nachregelt. public static let criticalCelsius: Double = 95 public static func decide(request: FanRequest?, lastHeartbeat: Date, now: Date, hottestCelsius: Double?, limits: FanLimits) -> FanDecision { // Rangfolge: erst die Gründe, die Schaden verhindern, dann der Wunsch. guard let request else { return .automatic(reason: .noRequest) } guard let hottest = hottestCelsius else { // Ohne Temperatur lässt sich nicht beurteilen, ob eine feste // Drehzahl gerade gefährlich ist. Blind weiterdrehen wäre falsch. return .automatic(reason: .noTemperature) } guard hottest < criticalCelsius else { return .automatic(reason: .temperature) } guard now.timeIntervalSince(lastHeartbeat) <= watchdogTimeout else { return .automatic(reason: .watchdog) } guard limits.isUsable else { return .automatic(reason: .unknownLimits) } // Nie unter das Firmware-Minimum: ein Lüfter, der langsamer läuft als // vorgesehen, kann den Rechner überhitzen, ohne dass die Automatik // eingreift — die ist ja gerade abgeschaltet. return .manual(rpm: min(max(request.targetRPM, limits.minimum), limits.maximum)) } }