Files
Camper-Monitor/Shared/Models/ConfiguredDevice.swift
T
fototeddyandClaude Sonnet 5 e9b9c5bcd5 Solar-Integration und dev_watch-Geräte zusammenführen
solar-integration (aus dem separaten VanAligneiOS-Repo) und dev_watch
haben unabhängige Git-Historien, decken aber überlappende und sich
ergänzende Funktionen ab. Übernommen aus solar-integration: Votronic-
Solar-ESP-Anbindung samt Geräterolle, die Live-Activity/Widget-Extension
fürs Sperrbildschirm/Dynamic-Island/CarPlay, das Querformat-Layout für
Libelle/Fahrzeug-Ansicht und Ausrichtungs-Assistent, sowie die
mehreren Fahrzeuggrafik-Stile (Vanster/California). Beibehalten aus
dev_watch: alle zusätzlichen Geräteprotokolle (Daly-/JBD-BMS, Alpicool-
Kühlbox, WattCycle, Victron), die dort zwischenzeitlich entstanden.

Die Xcode-Projektdatei wurde von Hand um die neue Widget-Extension samt
SharedActivity-Gruppe erweitert (Datei-synchronisierte Gruppen, kein
App-Group-Entitlement nötig). Build für App, Watch und Widget-Extension
geprüft (Debug und Release).

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

140 lines
5.4 KiB
Swift
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import Foundation
/// Welche Rolle ein Gerät im Camper spielt. Bestimmt Icon, Sortierung und
/// welche Kennzahl als "Hauptwert" auf der Kachel gross dargestellt wird.
enum DeviceRole: String, Codable, CaseIterable, Identifiable, Sendable {
case chargeBooster
case solarCharger
case batteryMonitor
case bms
case fridge
case leveling
case solar
var id: String { rawValue }
var title: String {
switch self {
case .chargeBooster: return "Ladebooster"
case .solarCharger: return "Solarladeregler"
case .batteryMonitor: return "Batteriemonitor"
case .bms: return "Batterie / BMS"
case .fridge: return "Kühlbox"
case .leveling: return "Nivellierung"
case .solar: return "VotronicSolarESP"
}
}
var symbol: String {
switch self {
case .chargeBooster: return "bolt.car"
case .solarCharger: return "sun.max"
case .batteryMonitor: return "gauge.with.dots.needle.bottom.50percent"
case .bms: return "battery.100percent.bolt"
case .fridge: return "refrigerator"
case .leveling: return "level"
case .solar: return "sun.max"
}
}
/// Ob nur verbunden wird, während die Ansicht des Geräts offen ist.
///
/// Die Kühlbox meldet jede Verbindung an ihrem Display an und verlangt je
/// nach Modell sogar einen Tastendruck; dauerhaft verbunden zu sein ist
/// dort also nicht unsichtbar, sondern lästig. Ihre Werte ändern sich
/// ausserdem langsam, und was auf der Übersicht zählt der Sollwert
/// ändert sich überhaupt nur, wenn jemand ihn ändert.
///
/// BMS und Neigungsmesser bleiben dagegen verbunden: Sie stören nicht, und
/// ihre Werte will man laufend sehen.
var connectsOnDemand: Bool { self == .fridge }
/// Victron-Geräte werden passiv über das Advertisement gelesen, das Daly BMS
/// braucht eine echte GATT-Verbindung.
var transport: DeviceTransport {
switch self {
case .bms, .fridge, .leveling, .solar: return .connect
default: return .advertisement
}
}
}
enum DeviceTransport: Sendable {
/// Passives Mitlesen der BLE-Werbedaten (Victron Instant Readout).
case advertisement
/// Verbindungsaufbau, Kommando schreiben, Antwort per Notify lesen (Daly).
case connect
}
/// Wie viele Kühlzonen eine Box hat. Manche Einzonen-Boxen senden den langen
/// Datensatz einer Doppelzonen-Box mit, weshalb die Erkennung übersteuerbar ist.
enum FridgeZoneMode: String, Codable, CaseIterable, Identifiable, Sendable {
case automatic
case single
case dual
var id: String { rawValue }
var title: String {
switch self {
case .automatic: return "Automatisch"
case .single: return "Eine Zone"
case .dual: return "Zwei Zonen"
}
}
}
/// Ein vom Nutzer eingerichtetes Gerät. Der Victron-Schlüssel liegt nicht hier,
/// sondern in der Keychain (siehe `KeychainStore`).
struct ConfiguredDevice: Identifiable, Codable, Hashable, Sendable {
var id: UUID
var name: String
var role: DeviceRole
/// Zu welchem Fahrzeug das Gerät gehört.
var profileID: UUID
/// CoreBluetooth-Identifier des Peripherals. Pro iPhone stabil, aber nicht
/// geräteübergreifend deshalb wird beim Einrichten neu gescannt.
var peripheralID: UUID
/// Zuletzt gesehener Advertised Name, nur zur Wiedererkennung in der UI.
var advertisedName: String?
/// Nur für Kühlboxen.
var fridgeZoneMode: FridgeZoneMode = .automatic
/// Nur für den Neigungsmesser: wie er im Fahrzeug sitzt.
var sensorOrientation = SensorOrientation.identity
/// Nur für den Neigungsmesser: welche Fahrzeuggrafik die Fahrzeug-Ansicht zeigt.
var vehicleGraphicStyle = VehicleGraphicStyle.vanster
init(id: UUID = UUID(),
name: String,
role: DeviceRole,
profileID: UUID,
peripheralID: UUID,
advertisedName: String? = nil) {
self.id = id
self.name = name
self.role = role
self.profileID = profileID
self.peripheralID = peripheralID
self.advertisedName = advertisedName
}
/// Geräte, die vor der Profilverwaltung angelegt wurden, haben noch keine
/// Zuordnung die landen im Standardprofil.
init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: CodingKeys.self)
id = try container.decode(UUID.self, forKey: .id)
name = try container.decode(String.self, forKey: .name)
role = try container.decode(DeviceRole.self, forKey: .role)
profileID = try container.decodeIfPresent(UUID.self, forKey: .profileID)
?? Profile.defaultID
peripheralID = try container.decode(UUID.self, forKey: .peripheralID)
advertisedName = try container.decodeIfPresent(String.self, forKey: .advertisedName)
fridgeZoneMode = try container.decodeIfPresent(FridgeZoneMode.self, forKey: .fridgeZoneMode)
?? .automatic
sensorOrientation = try container.decodeIfPresent(SensorOrientation.self,
forKey: .sensorOrientation) ?? .identity
vehicleGraphicStyle = try container.decodeIfPresent(VehicleGraphicStyle.self,
forKey: .vehicleGraphicStyle) ?? .vanster
}
}