forked from fritob/Camper-Monitor
Die IceCube-Boxen von Plug-in Festivals sind umgelabelte Alpicool-Boxen – der Hersteller verweist selbst auf die App "Alpicool T-Series". Bestätigt am Gerät: die Charakteristiken 00001235 und 00001236 tauchen im GATT-Baum auf. Dasselbe Protokoll sprechen BrassMonkey und Ocean Comfort. AlpicoolProtocol baut und liest die Rahmen (FE FE, Länge, Kommando, Daten, Bytesummen-Prüfsumme) und wertet die Statusantwort aus: Ist- und Solltemperatur je Zone, Betriebsart, Kompressorstatus, Bedienfeldsperre, Bordspannung und Batterieanzeige. Zweizonen-Boxen werden an der Nutzlastlänge erkannt. Auf Stellbefehle antwortet die Box mit Echo und Status in einer einzigen Benachrichtigung, weshalb der Rahmenleser weitersucht statt nach dem ersten Treffer abzubrechen; das Echo läuft mangels Statuslänge ins Leere. Als bisher einziges Gerät ist die Box auch stellbar: Ein/Aus, Eco/Max, Solltemperatur je Zone und Bedienfeldsperre. Nach jedem Stellbefehl wird der Zustand neu abgefragt, sodass die Schalter dem Gerät folgen und nicht der Eingabe. Ausschalten fragt einmal nach, weil dabei die Kühlung stoppt. Ein/Aus und Betriebsart brauchen den kompletten Einstellungsblock: er wird aus dem letzten Status neu aufgebaut und nur im betroffenen Byte geändert, sonst überschriebe die Box eigene Einstellungen mit Nullen. Ohne bekannten Status entsteht gar kein Stellbefehl. Die Namensprüfung in der Geräteliste hiess bisher looksLikeDaly und hätte Kühlboxen aus der Vorauswahl geworfen; sie deckt jetzt alle unterstützten Geräte ab und belegt die Art beim Einrichten vor. Protokoll und Feldbelegung nach Gruni22/alpicool_ha_ble. Die festen Anmelde- und Abfragepakete der Referenz sind byteweise abgesichert und belegen zugleich, dass die allgemeine Prüfsummenregel auch für sie gilt. 139 Prüfungen laufen durch. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
92 lines
3.2 KiB
Swift
92 lines
3.2 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
|
||
}
|
||
|
||
/// 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?
|
||
|
||
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)
|
||
}
|
||
}
|