From 83ea85f3b8aea6a538cf10167b381955ef04b6d7 Mon Sep 17 00:00:00 2001 From: fototeddy Date: Sun, 6 Sep 2026 21:08:28 +0200 Subject: [PATCH] =?UTF-8?q?DeviceRole-F=C3=A4lle=20f=C3=BCr=20Victron-=20u?= =?UTF-8?q?nd=20Votronic-Solar=20eindeutig=20benennen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit .solarCharger und .solar sahen im Code zu ähnlich aus, um auf einen Blick zu erkennen, welches der Victron-Solarladeregler (passives BLE-Advertisement) und welches der Votronic-Solarladeregler (aktive GATT-Verbindung über die ESP32-Bridge) ist. Jetzt .victronSolarCharger und .votronicSolar, samt allen abgeleiteten Bezeichnern in DemoData, WatchDemo und Tests. Die rawValues bleiben "solarCharger"/"solar", damit bereits gespeicherte Geräte beim Decodieren nicht auf einen unbekannten Rollen-Wert treffen. VictronAdvertisement.RecordType.solarCharger (das rohe Protokollfeld) bleibt unverändert – schon durch den Typnamen eindeutig, und passend zur offiziellen Victron-Bezeichnung des Datensatztyps. run-tests.sh fehlten VehicleGraphicStyle.swift und VehicleTilt.swift, seit ConfiguredDevice sie referenziert – ergänzt, Testlauf grün. Co-Authored-By: Claude Sonnet 5 --- .../Bluetooth/BluetoothManager.swift | 2 +- CamperMonitor/Store/DemoData.swift | 22 +++++----- CamperMonitor/Views/AddDeviceView.swift | 8 ++-- CamperMonitorWatch/WatchDemo.swift | 12 ++--- Shared/Models/ConfiguredDevice.swift | 44 ++++++++++++------- Tests/main.swift | 20 ++++----- run-tests.sh | 2 + 7 files changed, 61 insertions(+), 49 deletions(-) diff --git a/CamperMonitor/Bluetooth/BluetoothManager.swift b/CamperMonitor/Bluetooth/BluetoothManager.swift index 7471b5e..6b4c592 100644 --- a/CamperMonitor/Bluetooth/BluetoothManager.swift +++ b/CamperMonitor/Bluetooth/BluetoothManager.swift @@ -891,7 +891,7 @@ extension BluetoothManager: CBCentralManagerDelegate { return } - if device.role == .solar { + if device.role == .votronicSolar { // Bewusst kein Draht zu `activityManager`: Der Solarertrag soll // nicht in der Live Activity/CarPlay auftauchen, die ist dem // Neigungsmesser vorbehalten. diff --git a/CamperMonitor/Store/DemoData.swift b/CamperMonitor/Store/DemoData.swift index e2ad63b..bf7657e 100644 --- a/CamperMonitor/Store/DemoData.swift +++ b/CamperMonitor/Store/DemoData.swift @@ -34,9 +34,9 @@ enum DemoData { name: "Ladebooster", role: .chargeBooster, profileID: Profile.defaultID, peripheralID: UUID(uuidString: "00000000-0000-0000-0000-0000000000B1")!) - static let solar = ConfiguredDevice( + static let victronSolar = ConfiguredDevice( id: UUID(uuidString: "00000000-0000-0000-0000-000000000050")!, - name: "Solar Dach", role: .solarCharger, profileID: Profile.defaultID, + name: "Solar Dach", role: .victronSolarCharger, profileID: Profile.defaultID, peripheralID: UUID(uuidString: "00000000-0000-0000-0000-000000000051")!) static let battery = ConfiguredDevice( @@ -44,9 +44,9 @@ enum DemoData { name: "Bulltron 200 Ah", role: .bms, profileID: Profile.defaultID, peripheralID: UUID(uuidString: "00000000-0000-0000-0000-0000000000A1")!) - static let caravanSolar = ConfiguredDevice( + static let caravanVictronSolar = ConfiguredDevice( id: UUID(uuidString: "00000000-0000-0000-0000-0000000000C3")!, - name: "Solar Wohnwagen", role: .solarCharger, profileID: secondProfile.id, + name: "Solar Wohnwagen", role: .victronSolarCharger, profileID: secondProfile.id, peripheralID: UUID(uuidString: "00000000-0000-0000-0000-0000000000C4")!) static let fridge = ConfiguredDevice( @@ -61,11 +61,11 @@ enum DemoData { static let votronicSolar = ConfiguredDevice( id: UUID(uuidString: "00000000-0000-0000-0000-0000000000D0")!, - name: "VotronicSolarESP", role: .solar, profileID: Profile.defaultID, + name: "VotronicSolarESP", role: .votronicSolar, profileID: Profile.defaultID, peripheralID: UUID(uuidString: "00000000-0000-0000-0000-0000000000D1")!) static var devices: [ConfiguredDevice] { - [solar, booster, battery, fridge, level, votronicSolar, caravanSolar] + [victronSolar, booster, battery, fridge, level, votronicSolar, caravanVictronSolar] } /// Leicht schräg stehend, damit die Libelle etwas zu zeigen hat. @@ -103,9 +103,9 @@ enum DemoData { } static func snapshots() -> [DeviceSnapshot] { - var solarSnapshot = DeviceSnapshot(deviceID: solar.id, timestamp: Date(), rssi: -58) - solarSnapshot.state = "Konstantspannung (Absorption)" - solarSnapshot.metrics = [ + var victronSolarSnapshot = DeviceSnapshot(deviceID: victronSolar.id, timestamp: Date(), rssi: -58) + victronSolarSnapshot.state = "Konstantspannung (Absorption)" + victronSolarSnapshot.metrics = [ Metric("pv_power", "PV-Leistung", 284, unit: "W", precision: 0, primary: true), Metric("battery_power", "Ladeleistung", 262, unit: "W", precision: 0), Metric("battery_voltage", "Batteriespannung", 14.12, unit: "V", precision: 2), @@ -139,7 +139,7 @@ enum DemoData { Metric("cycles", "Ladezyklen", 143, unit: "", precision: 0), ] - var caravanSnapshot = DeviceSnapshot(deviceID: caravanSolar.id, timestamp: Date(), rssi: -77) + var caravanSnapshot = DeviceSnapshot(deviceID: caravanVictronSolar.id, timestamp: Date(), rssi: -77) caravanSnapshot.state = "Erhaltung (Float)" caravanSnapshot.metrics = [ Metric("pv_power", "PV-Leistung", 62, unit: "W", precision: 0, primary: true), @@ -147,7 +147,7 @@ enum DemoData { Metric("battery_current", "Ladestrom", 4.4, unit: "A", precision: 1), ] - return [solarSnapshot, boosterSnapshot, batterySnapshot, + return [victronSolarSnapshot, boosterSnapshot, batterySnapshot, fridgeState.snapshot(deviceID: fridge.id, rssi: -66), levelState.snapshot(deviceID: level.id, rssi: -70), votronicSolarESPState.snapshot(deviceID: votronicSolar.id, rssi: -58), diff --git a/CamperMonitor/Views/AddDeviceView.swift b/CamperMonitor/Views/AddDeviceView.swift index 357f7fa..dcd27f5 100644 --- a/CamperMonitor/Views/AddDeviceView.swift +++ b/CamperMonitor/Views/AddDeviceView.swift @@ -127,7 +127,7 @@ private struct ConfigureDeviceView: View { @Environment(\.dismiss) private var dismiss @State private var name: String = "" - @State private var role: DeviceRole = .solarCharger + @State private var role: DeviceRole = .victronSolarCharger @State private var key: String = "" private var needsKey: Bool { role.transport == .advertisement } @@ -188,15 +188,15 @@ private struct ConfigureDeviceView: View { private func prefill() { if let recordType = discovery.victronRecordType { switch VictronAdvertisement.RecordType(rawValue: recordType) { - case .solarCharger: role = .solarCharger + case .solarCharger: role = .victronSolarCharger case .dcdcConverter, .orionXS: role = .chargeBooster case .batteryMonitor: role = .batteryMonitor - default: role = .solarCharger + default: role = .victronSolarCharger } } else if discovery.isLevelSensor { role = .leveling } else if discovery.isVotronicSolarESPSensor { - role = .solar + role = .votronicSolar } else if let name = discovery.name?.lowercased(), ["alpicool", "icecube", "ice cube", "fridge", "cool"].contains(where: name.contains) { role = .fridge diff --git a/CamperMonitorWatch/WatchDemo.swift b/CamperMonitorWatch/WatchDemo.swift index 4394dc3..54c2af1 100644 --- a/CamperMonitorWatch/WatchDemo.swift +++ b/CamperMonitorWatch/WatchDemo.swift @@ -13,7 +13,7 @@ enum WatchDemo { private static let profile = Profile(name: "Kastenwagen", symbol: "box.truck", trackWidth: 1.85, wheelbase: 3.50) - private static let solarID = UUID(uuidString: "00000000-0000-0000-0000-000000000050")! + private static let victronSolarID = UUID(uuidString: "00000000-0000-0000-0000-000000000050")! private static let batteryID = UUID(uuidString: "00000000-0000-0000-0000-0000000000A0")! private static let fridgeID = UUID(uuidString: "00000000-0000-0000-0000-0000000000F0")! private static let levelID = UUID(uuidString: "00000000-0000-0000-0000-0000000000E0")! @@ -30,9 +30,9 @@ enum WatchDemo { level.pitchOffset = 0.4 level.rollOffset = -0.2 - var solar = DeviceSnapshot(deviceID: solarID, timestamp: now, rssi: -58) - solar.state = "Konstantspannung (Absorption)" - solar.metrics = [ + var victronSolar = DeviceSnapshot(deviceID: victronSolarID, timestamp: now, rssi: -58) + victronSolar.state = "Konstantspannung (Absorption)" + victronSolar.metrics = [ Metric("pv_power", "PV-Leistung", 284 + 40 * swing, unit: "W", precision: 0, primary: true), Metric("battery_voltage", "Batteriespannung", 14.12, unit: "V", precision: 2), Metric("battery_current", "Ladestrom", 18.6, unit: "A", precision: 1), @@ -67,8 +67,8 @@ enum WatchDemo { WatchDevice(id: batteryID, name: "Bulltron 200 Ah", role: .bms, link: .live, snapshot: battery, level: nil, orientation: nil, fridge: nil), - WatchDevice(id: solarID, name: "Solar Dach", role: .solarCharger, - link: .live, snapshot: solar, level: nil, + WatchDevice(id: victronSolarID, name: "Solar Dach", role: .victronSolarCharger, + link: .live, snapshot: victronSolar, level: nil, orientation: nil, fridge: nil), WatchDevice(id: fridgeID, name: "Kühlbox", role: .fridge, link: .live, snapshot: fridge, level: nil, orientation: nil, diff --git a/Shared/Models/ConfiguredDevice.swift b/Shared/Models/ConfiguredDevice.swift index d43cc9a..3d3aac7 100644 --- a/Shared/Models/ConfiguredDevice.swift +++ b/Shared/Models/ConfiguredDevice.swift @@ -4,36 +4,46 @@ import Foundation /// welche Kennzahl als "Hauptwert" auf der Kachel gross dargestellt wird. enum DeviceRole: String, Codable, CaseIterable, Identifiable, Sendable { case chargeBooster - case solarCharger + /// Victron-Solarladeregler (SmartSolar/BlueSolar MPPT), passiv über das + /// verschlüsselte BLE-Advertisement gelesen. + /// + /// Der rawValue bleibt `"solarCharger"`, damit bereits gespeicherte + /// Geräte beim Decodieren nicht auf einen unbekannten Rollen-Wert + /// treffen – siehe `ConfiguredDevice.init(from:)`. + case victronSolarCharger = "solarCharger" case batteryMonitor case bms case fridge case leveling - case solar + /// Votronic-Solarladeregler, über eine ESP32-Bridge per GATT-Verbindung + /// angesprochen (siehe `VotronicSolarESPSession`). + /// + /// Der rawValue bleibt `"solar"`, siehe `victronSolarCharger` oben. + case votronicSolar = "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" + case .chargeBooster: return "Ladebooster" + case .victronSolarCharger: return "Solarladeregler" + case .batteryMonitor: return "Batteriemonitor" + case .bms: return "Batterie / BMS" + case .fridge: return "Kühlbox" + case .leveling: return "Nivellierung" + case .votronicSolar: 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" + case .chargeBooster: return "bolt.car" + case .victronSolarCharger: 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 .votronicSolar: return "sun.max" } } @@ -53,7 +63,7 @@ enum DeviceRole: String, Codable, CaseIterable, Identifiable, Sendable { /// braucht eine echte GATT-Verbindung. var transport: DeviceTransport { switch self { - case .bms, .fridge, .leveling, .solar: return .connect + case .bms, .fridge, .leveling, .votronicSolar: return .connect default: return .advertisement } } diff --git a/Tests/main.swift b/Tests/main.swift index ca211a9..63e3d04 100644 --- a/Tests/main.swift +++ b/Tests/main.swift @@ -76,22 +76,22 @@ var advertisement: [UInt8] = [0xE1, 0x02, 0x10, 0x00, 0x4C, 0xA0, 0x01, UInt8(nonce & 0xFF), UInt8(nonce >> 8), deviceKey[0]] advertisement += encrypted -let solar = try? VictronAdvertisement.decode(manufacturerData: Data(advertisement), +let victronSolar = try? VictronAdvertisement.decode(manufacturerData: Data(advertisement), key: deviceKey, deviceID: UUID(), rssi: -55) func value(_ snapshot: DeviceSnapshot?, _ key: String) -> Double? { snapshot?.metrics.first { $0.key == key }?.value } -check("Advertisement wird dekodiert", solar != nil) -checkEqual("Zustand als Klartext", solar?.state, "Konstantstrom (Bulk)") -checkEqual("kein Fehler gemeldet", solar?.fault, nil) -checkEqual("Batteriespannung", value(solar, "battery_voltage").map(round2), 13.45) -checkEqual("Ladestrom", value(solar, "battery_current").map { ($0 * 10).rounded() / 10 }, 15.2) -checkEqual("Tagesertrag", value(solar, "yield_today"), 2.34) -checkEqual("PV-Leistung", value(solar, "pv_power"), 210) -checkEqual("Laststrom bleibt leer (NA)", value(solar, "load_current"), nil) -checkEqual("PV-Leistung ist der Hauptwert", solar?.primaryMetric?.key, "pv_power") +check("Advertisement wird dekodiert", victronSolar != nil) +checkEqual("Zustand als Klartext", victronSolar?.state, "Konstantstrom (Bulk)") +checkEqual("kein Fehler gemeldet", victronSolar?.fault, nil) +checkEqual("Batteriespannung", value(victronSolar, "battery_voltage").map(round2), 13.45) +checkEqual("Ladestrom", value(victronSolar, "battery_current").map { ($0 * 10).rounded() / 10 }, 15.2) +checkEqual("Tagesertrag", value(victronSolar, "yield_today"), 2.34) +checkEqual("PV-Leistung", value(victronSolar, "pv_power"), 210) +checkEqual("Laststrom bleibt leer (NA)", value(victronSolar, "load_current"), nil) +checkEqual("PV-Leistung ist der Hauptwert", victronSolar?.primaryMetric?.key, "pv_power") var wrongKey = deviceKey; wrongKey[0] = 0x00 do { diff --git a/run-tests.sh b/run-tests.sh index 00611a5..628c02b 100755 --- a/run-tests.sh +++ b/run-tests.sh @@ -17,6 +17,8 @@ swiftc -O -o "$OUT/tests" \ Shared/Models/DeviceSnapshot.swift \ Shared/Models/FridgeSettings.swift \ Shared/Models/ConfiguredDevice.swift \ + Shared/Models/VehicleGraphicStyle.swift \ + Shared/VehicleTilt.swift \ Shared/Models/LevelState.swift \ Shared/Models/Profile.swift \ Shared/Models/AlignmentAssistant.swift \