Zwei Fehler am Gerät gefunden. Einzonen-Boxen als Doppelzone erkannt: Manche senden den langen Datensatz einer Zweizonen-Box mit, füllen den zweiten Block aber mit Nullen. Die Nutzlastlänge allein taugt deshalb nicht als Kriterium. Als zweizonig gilt jetzt nur, wo dieser Block überhaupt Inhalt hat, und in den Geräteeinstellungen lässt sich die Erkennung auf "Eine Zone" oder "Zwei Zonen" festnageln. Einschalten reagierte nicht: Ein/Aus und Betriebsart schicken den kompletten Einstellungsblock, der mit dem langen Datensatz auf 31 Byte kommt. Ohne ausgehandelte MTU nimmt BLE aber nur 20 Nutzbytes je Schreibvorgang an, der Rest wurde stillschweigend verworfen. Die Solltemperatur ging als kurzes Paket durch, alles Längere nicht. Lange Pakete werden jetzt anhand von maximumWriteValueLength aufgeteilt und versetzt geschrieben; die Statusabfrage danach wartet entsprechend länger. Aus dem kurzen Datensatz ist der Befehl übrigens genau 20 Byte groß und hätte gerade noch gepasst - deshalb trat es nur an dieser Box auf. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
114 lines
4.0 KiB
Swift
114 lines
4.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
|
||
|
||
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"
|
||
}
|
||
}
|
||
|
||
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"
|
||
}
|
||
}
|
||
|
||
/// 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: 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
|
||
|
||
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
|
||
}
|
||
}
|