Jede Verbindung meldet sich am Display der Box an – sie piept, und wer gerade davorsteht, wird gestört. Dauerhaft verbunden zu sein ist dort also nicht unsichtbar wie beim BMS, sondern lästig. Die Kühlbox wird deshalb nur noch verbunden, solange ihre Ansicht offen ist: am iPhone über die Detailansicht, an der Uhr über einen eigenen Befehl, den sie beim Öffnen und Schliessen schickt. Ein Stellbefehl geht weiterhin immer durch – ist die Box nicht verbunden, wird er gemerkt und löst den Verbindungsaufbau aus. Abgeriegelt sind alle drei Wege, über die bisher verbunden wurde: beim Start, im Takt des Wiederverbindens, und – das war das eigentliche Loch – sobald das Gerät in den Werbedaten auftaucht. Da die App dauerhaft scannt, hätte allein dieser Weg die Box weiter angefunkt. Dass sie in Reichweite ist, ist kein Grund, sie anzufassen. Auf der Übersicht steht dafür der zuletzt gestellte Stand statt der Messwerte: Sollwert, Ein/Aus, Eco oder Max, dazu wann er gestellt wurde. Das bleibt richtig, auch wenn es von gestern ist – ein Sollwert ändert sich nur, wenn jemand ihn ändert. Eine Innentemperatur von gestern sähe dagegen aus wie eine von jetzt, und man würde ihr glauben. Genau das sichern die neuen Prüfungen ab: Hauptwert ist der Sollwert, gemessene Temperatur und Bordspannung kommen nicht vor. Der Stand liegt in den Einstellungen (FridgeSettings) mit einem Zeitstempel, der „zuletzt geändert" bedeutet und nicht „zuletzt gesehen" – sonst behauptete die Kachel Frische, wo sich nichts getan hat. Die Uhr bekommt dasselbe Bild: WatchFridge trägt jetzt ein Kennzeichen isLive. BMS und Neigungsmesser bleiben dauerhaft verbunden. Sie stören nicht, und ihre Werte will man laufend sehen. Nicht nachgezogen ist die Android-App – dort verbindet der BluetoothManager weiterhin dauerhaft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
133 lines
5.0 KiB
Swift
133 lines
5.0 KiB
Swift
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
|
||
|
||
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"
|
||
}
|
||
}
|
||
|
||
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"
|
||
}
|
||
}
|
||
|
||
/// 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: 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
|
||
|
||
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
|
||
}
|
||
}
|