import Foundation /// Eine einzelne Messgrösse in einer bereits formatierten Form. struct Metric: Identifiable, Hashable, Sendable { let key: String let label: String let value: Double? let unit: String /// Nachkommastellen für die Anzeige. let precision: Int /// Wird auf der Kachel gross dargestellt. let isPrimary: Bool var id: String { key } init(_ key: String, _ label: String, _ value: Double?, unit: String, precision: Int = 2, primary: Bool = false) { self.key = key self.label = label self.value = value self.unit = unit self.precision = precision self.isPrimary = primary } var formatted: String { guard let value else { return "–" } return String(format: "%.\(precision)f", value) } var formattedWithUnit: String { guard value != nil else { return "–" } return unit.isEmpty ? formatted : "\(formatted) \(unit)" } } /// Der komplette, zuletzt empfangene Zustand eines Geräts. struct DeviceSnapshot: Identifiable, Sendable { var id: UUID { deviceID } var deviceID: UUID var timestamp: Date var metrics: [Metric] = [] /// z.B. "Bulk", "Float", "Aus" var state: String? /// Klartext einer aktiven Störung, sonst nil. var fault: String? /// Grund, warum das Gerät gerade nicht lädt (Victron Off-Reason). var offReasons: [String] = [] var rssi: Int? /// Einzelzellspannungen in Volt (nur BMS). var cellVoltages: [Double] = [] /// Temperaturfühler in °C (nur BMS). var temperatures: [Double] = [] /// Feste Angaben des Geräts, etwa Modell oder Seriennummer. var info: [InfoItem] = [] struct InfoItem: Identifiable, Hashable, Sendable { let label: String let value: String var id: String { label } } var primaryMetric: Metric? { metrics.first(where: \.isPrimary) ?? metrics.first } var age: TimeInterval { Date().timeIntervalSince(timestamp) } /// Werte älter als eine Minute gelten als veraltet – Victron sendet etwa /// jede Sekunde, das Daly wird alle paar Sekunden gepollt. var isStale: Bool { age > 60 } } /// Verbindungszustand für die UI. enum DeviceLinkState: Equatable, Sendable { case idle case searching case connecting case live case needsKey case failed(String) var label: String { switch self { case .idle: return "Inaktiv" case .searching: return "Suche…" case .connecting: return "Verbinde…" case .live: return "Live" case .needsKey: return "Schlüssel fehlt" case .failed(let m): return m } } }