import Foundation /// Was mit dem Akku gerade geschieht — in Worten statt in Symbolen. /// /// „Am Netz" allein beantwortet die Frage nicht, die man hat, wenn der /// Ladestand stehen bleibt: lädt es gerade nicht, weil das Limit erreicht ist, /// oder stimmt etwas nicht? public enum ChargeActivity: Equatable, Sendable { case charging /// Angehalten, weil das eingestellte Ladelimit erreicht ist. case pausedAtLimit(Int) /// Am Netz, lädt aber nicht — Grund unbekannt. case pluggedNotCharging case onBattery /// Wie weit unter dem Limit noch als „am Limit" gilt. /// /// Der Ladestand pendelt um die Grenze. Auf das Prozent genau zu /// vergleichen hieße, den Grund die halbe Zeit zu verschweigen. public static let limitTolerance = 2 public static func resolve(isCharging: Bool, isPluggedIn: Bool, charge: Double, limit: Int?) -> ChargeActivity { if isCharging { return .charging } guard isPluggedIn else { return .onBattery } let percent = Int((min(max(charge, 0), 1) * 100).rounded()) // Ein Limit von hundert ist keines. if let limit, limit < 100, percent >= limit - limitTolerance { return .pausedAtLimit(limit) } // Steht das Laden aus einem anderen Grund — etwa weil macOS den Akku // schont —, ist das der ehrliche Befund. Einen falschen Grund zu // nennen wäre schlimmer als keinen. return .pluggedNotCharging } } /// Was am Strom geschehen ist — und ob es überhaupt etwas war. /// /// ``ChargeActivity`` sagt, welcher Zustand *gilt*. Das ist nicht dasselbe wie /// „es ist etwas passiert": der Zustand gilt die ganze Zeit, gemeldet werden /// soll er nur beim Wechsel. public enum PowerEvent: Equatable, Sendable { case charging case plugged case unplugged case atLimit(Int) /// Am Akku und fast leer. Schlägt `unplugged`: beides trifft zu, aber nur /// eines davon muss man sehen. case low } /// Merkt sich den letzten Zustand und meldet nur Wechsel. /// /// **Der erste gelesene Zustand meldet nichts.** Sonst begrüßte Onyx jeden /// Start mit „am Netz" — einer Nachricht über nichts, das gerade geschehen ist. /// /// Eigener Typ und nicht zwei Zeilen im Programmstart, weil hier drei Regeln /// zusammenkommen, die man einzeln prüfen können muss: die Schwelle für „fast /// leer", der Vorrang vor „am Akku" und das Schweigen beim ersten Mal. public struct PowerWatcher: Equatable, Sendable { /// Ab hier heißt „am Akku" nicht mehr nur „am Akku". public static let lowThreshold = 0.2 private var last: PowerEvent? public init() {} /// Der zuletzt gesehene Zustand. `nil`, solange nichts gelesen wurde. public var current: PowerEvent? { last } /// Nimmt eine Messung entgegen und gibt zurück, was zu melden ist — oder /// `nil`, wenn sich nichts geändert hat. public mutating func observe(isCharging: Bool, isPluggedIn: Bool, charge: Double, limit: Int?) -> PowerEvent? { let state = ChargeActivity.resolve(isCharging: isCharging, isPluggedIn: isPluggedIn, charge: charge, limit: limit) let event: PowerEvent = switch state { case .charging: .charging case .pausedAtLimit(let value): .atLimit(value) case .pluggedNotCharging: .plugged case .onBattery: charge <= Self.lowThreshold ? .low : .unplugged } defer { last = event } guard let previous = last else { return nil } return event == previous ? nil : event } }