Files
onyx/Packages/OnyxKit/Sources/MetricsProvider/ChargeActivity.swift
T
ScarriffleandClaude Opus 5 f289d02c37 Der Akku bekommt ein Gedächtnis
Verlauf über 24 Stunden, sieben Tage und dreißig Tage, aufgezeichnet alle fünf
Minuten. Als Säulen und nicht als Linie: eine Linie verbindet auch dort, wo
nichts gemessen wurde, und ein zugeklappter Rechner hatte keinen Ladestand —
eine Linie darüber behauptet einen.

Dazu Segelmodus und Hitzeschutz als reine Regeln im Helfer. Beide haben zwei
Schwellen, eine zum Anhalten und eine zum Weitermachen. Ohne diesen Abstand
hinge der Akku am Limit und würde bei jedem verlorenen Prozent nachgeladen —
hunderte winzige Ladevorgänge am Tag, und genau die altern ihn.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-05 21:06:06 +02:00

93 lines
3.6 KiB
Swift

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
}
}