forked from fritob/Camper-Monitor
DeviceRole-Fälle für Victron- und Votronic-Solar eindeutig benennen
.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 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
e9b9c5bcd5
commit
83ea85f3b8
@@ -891,7 +891,7 @@ extension BluetoothManager: CBCentralManagerDelegate {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if device.role == .solar {
|
if device.role == .votronicSolar {
|
||||||
// Bewusst kein Draht zu `activityManager`: Der Solarertrag soll
|
// Bewusst kein Draht zu `activityManager`: Der Solarertrag soll
|
||||||
// nicht in der Live Activity/CarPlay auftauchen, die ist dem
|
// nicht in der Live Activity/CarPlay auftauchen, die ist dem
|
||||||
// Neigungsmesser vorbehalten.
|
// Neigungsmesser vorbehalten.
|
||||||
|
|||||||
@@ -34,9 +34,9 @@ enum DemoData {
|
|||||||
name: "Ladebooster", role: .chargeBooster, profileID: Profile.defaultID,
|
name: "Ladebooster", role: .chargeBooster, profileID: Profile.defaultID,
|
||||||
peripheralID: UUID(uuidString: "00000000-0000-0000-0000-0000000000B1")!)
|
peripheralID: UUID(uuidString: "00000000-0000-0000-0000-0000000000B1")!)
|
||||||
|
|
||||||
static let solar = ConfiguredDevice(
|
static let victronSolar = ConfiguredDevice(
|
||||||
id: UUID(uuidString: "00000000-0000-0000-0000-000000000050")!,
|
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")!)
|
peripheralID: UUID(uuidString: "00000000-0000-0000-0000-000000000051")!)
|
||||||
|
|
||||||
static let battery = ConfiguredDevice(
|
static let battery = ConfiguredDevice(
|
||||||
@@ -44,9 +44,9 @@ enum DemoData {
|
|||||||
name: "Bulltron 200 Ah", role: .bms, profileID: Profile.defaultID,
|
name: "Bulltron 200 Ah", role: .bms, profileID: Profile.defaultID,
|
||||||
peripheralID: UUID(uuidString: "00000000-0000-0000-0000-0000000000A1")!)
|
peripheralID: UUID(uuidString: "00000000-0000-0000-0000-0000000000A1")!)
|
||||||
|
|
||||||
static let caravanSolar = ConfiguredDevice(
|
static let caravanVictronSolar = ConfiguredDevice(
|
||||||
id: UUID(uuidString: "00000000-0000-0000-0000-0000000000C3")!,
|
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")!)
|
peripheralID: UUID(uuidString: "00000000-0000-0000-0000-0000000000C4")!)
|
||||||
|
|
||||||
static let fridge = ConfiguredDevice(
|
static let fridge = ConfiguredDevice(
|
||||||
@@ -61,11 +61,11 @@ enum DemoData {
|
|||||||
|
|
||||||
static let votronicSolar = ConfiguredDevice(
|
static let votronicSolar = ConfiguredDevice(
|
||||||
id: UUID(uuidString: "00000000-0000-0000-0000-0000000000D0")!,
|
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")!)
|
peripheralID: UUID(uuidString: "00000000-0000-0000-0000-0000000000D1")!)
|
||||||
|
|
||||||
static var devices: [ConfiguredDevice] {
|
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.
|
/// Leicht schräg stehend, damit die Libelle etwas zu zeigen hat.
|
||||||
@@ -103,9 +103,9 @@ enum DemoData {
|
|||||||
}
|
}
|
||||||
|
|
||||||
static func snapshots() -> [DeviceSnapshot] {
|
static func snapshots() -> [DeviceSnapshot] {
|
||||||
var solarSnapshot = DeviceSnapshot(deviceID: solar.id, timestamp: Date(), rssi: -58)
|
var victronSolarSnapshot = DeviceSnapshot(deviceID: victronSolar.id, timestamp: Date(), rssi: -58)
|
||||||
solarSnapshot.state = "Konstantspannung (Absorption)"
|
victronSolarSnapshot.state = "Konstantspannung (Absorption)"
|
||||||
solarSnapshot.metrics = [
|
victronSolarSnapshot.metrics = [
|
||||||
Metric("pv_power", "PV-Leistung", 284, unit: "W", precision: 0, primary: true),
|
Metric("pv_power", "PV-Leistung", 284, unit: "W", precision: 0, primary: true),
|
||||||
Metric("battery_power", "Ladeleistung", 262, unit: "W", precision: 0),
|
Metric("battery_power", "Ladeleistung", 262, unit: "W", precision: 0),
|
||||||
Metric("battery_voltage", "Batteriespannung", 14.12, unit: "V", precision: 2),
|
Metric("battery_voltage", "Batteriespannung", 14.12, unit: "V", precision: 2),
|
||||||
@@ -139,7 +139,7 @@ enum DemoData {
|
|||||||
Metric("cycles", "Ladezyklen", 143, unit: "", precision: 0),
|
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.state = "Erhaltung (Float)"
|
||||||
caravanSnapshot.metrics = [
|
caravanSnapshot.metrics = [
|
||||||
Metric("pv_power", "PV-Leistung", 62, unit: "W", precision: 0, primary: true),
|
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),
|
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),
|
fridgeState.snapshot(deviceID: fridge.id, rssi: -66),
|
||||||
levelState.snapshot(deviceID: level.id, rssi: -70),
|
levelState.snapshot(deviceID: level.id, rssi: -70),
|
||||||
votronicSolarESPState.snapshot(deviceID: votronicSolar.id, rssi: -58),
|
votronicSolarESPState.snapshot(deviceID: votronicSolar.id, rssi: -58),
|
||||||
|
|||||||
@@ -127,7 +127,7 @@ private struct ConfigureDeviceView: View {
|
|||||||
@Environment(\.dismiss) private var dismiss
|
@Environment(\.dismiss) private var dismiss
|
||||||
|
|
||||||
@State private var name: String = ""
|
@State private var name: String = ""
|
||||||
@State private var role: DeviceRole = .solarCharger
|
@State private var role: DeviceRole = .victronSolarCharger
|
||||||
@State private var key: String = ""
|
@State private var key: String = ""
|
||||||
|
|
||||||
private var needsKey: Bool { role.transport == .advertisement }
|
private var needsKey: Bool { role.transport == .advertisement }
|
||||||
@@ -188,15 +188,15 @@ private struct ConfigureDeviceView: View {
|
|||||||
private func prefill() {
|
private func prefill() {
|
||||||
if let recordType = discovery.victronRecordType {
|
if let recordType = discovery.victronRecordType {
|
||||||
switch VictronAdvertisement.RecordType(rawValue: recordType) {
|
switch VictronAdvertisement.RecordType(rawValue: recordType) {
|
||||||
case .solarCharger: role = .solarCharger
|
case .solarCharger: role = .victronSolarCharger
|
||||||
case .dcdcConverter, .orionXS: role = .chargeBooster
|
case .dcdcConverter, .orionXS: role = .chargeBooster
|
||||||
case .batteryMonitor: role = .batteryMonitor
|
case .batteryMonitor: role = .batteryMonitor
|
||||||
default: role = .solarCharger
|
default: role = .victronSolarCharger
|
||||||
}
|
}
|
||||||
} else if discovery.isLevelSensor {
|
} else if discovery.isLevelSensor {
|
||||||
role = .leveling
|
role = .leveling
|
||||||
} else if discovery.isVotronicSolarESPSensor {
|
} else if discovery.isVotronicSolarESPSensor {
|
||||||
role = .solar
|
role = .votronicSolar
|
||||||
} else if let name = discovery.name?.lowercased(),
|
} else if let name = discovery.name?.lowercased(),
|
||||||
["alpicool", "icecube", "ice cube", "fridge", "cool"].contains(where: name.contains) {
|
["alpicool", "icecube", "ice cube", "fridge", "cool"].contains(where: name.contains) {
|
||||||
role = .fridge
|
role = .fridge
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ enum WatchDemo {
|
|||||||
private static let profile = Profile(name: "Kastenwagen", symbol: "box.truck",
|
private static let profile = Profile(name: "Kastenwagen", symbol: "box.truck",
|
||||||
trackWidth: 1.85, wheelbase: 3.50)
|
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 batteryID = UUID(uuidString: "00000000-0000-0000-0000-0000000000A0")!
|
||||||
private static let fridgeID = UUID(uuidString: "00000000-0000-0000-0000-0000000000F0")!
|
private static let fridgeID = UUID(uuidString: "00000000-0000-0000-0000-0000000000F0")!
|
||||||
private static let levelID = UUID(uuidString: "00000000-0000-0000-0000-0000000000E0")!
|
private static let levelID = UUID(uuidString: "00000000-0000-0000-0000-0000000000E0")!
|
||||||
@@ -30,9 +30,9 @@ enum WatchDemo {
|
|||||||
level.pitchOffset = 0.4
|
level.pitchOffset = 0.4
|
||||||
level.rollOffset = -0.2
|
level.rollOffset = -0.2
|
||||||
|
|
||||||
var solar = DeviceSnapshot(deviceID: solarID, timestamp: now, rssi: -58)
|
var victronSolar = DeviceSnapshot(deviceID: victronSolarID, timestamp: now, rssi: -58)
|
||||||
solar.state = "Konstantspannung (Absorption)"
|
victronSolar.state = "Konstantspannung (Absorption)"
|
||||||
solar.metrics = [
|
victronSolar.metrics = [
|
||||||
Metric("pv_power", "PV-Leistung", 284 + 40 * swing, unit: "W", precision: 0, primary: true),
|
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_voltage", "Batteriespannung", 14.12, unit: "V", precision: 2),
|
||||||
Metric("battery_current", "Ladestrom", 18.6, unit: "A", precision: 1),
|
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,
|
WatchDevice(id: batteryID, name: "Bulltron 200 Ah", role: .bms,
|
||||||
link: .live, snapshot: battery, level: nil,
|
link: .live, snapshot: battery, level: nil,
|
||||||
orientation: nil, fridge: nil),
|
orientation: nil, fridge: nil),
|
||||||
WatchDevice(id: solarID, name: "Solar Dach", role: .solarCharger,
|
WatchDevice(id: victronSolarID, name: "Solar Dach", role: .victronSolarCharger,
|
||||||
link: .live, snapshot: solar, level: nil,
|
link: .live, snapshot: victronSolar, level: nil,
|
||||||
orientation: nil, fridge: nil),
|
orientation: nil, fridge: nil),
|
||||||
WatchDevice(id: fridgeID, name: "Kühlbox", role: .fridge,
|
WatchDevice(id: fridgeID, name: "Kühlbox", role: .fridge,
|
||||||
link: .live, snapshot: fridge, level: nil, orientation: nil,
|
link: .live, snapshot: fridge, level: nil, orientation: nil,
|
||||||
|
|||||||
@@ -4,36 +4,46 @@ import Foundation
|
|||||||
/// welche Kennzahl als "Hauptwert" auf der Kachel gross dargestellt wird.
|
/// welche Kennzahl als "Hauptwert" auf der Kachel gross dargestellt wird.
|
||||||
enum DeviceRole: String, Codable, CaseIterable, Identifiable, Sendable {
|
enum DeviceRole: String, Codable, CaseIterable, Identifiable, Sendable {
|
||||||
case chargeBooster
|
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 batteryMonitor
|
||||||
case bms
|
case bms
|
||||||
case fridge
|
case fridge
|
||||||
case leveling
|
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 id: String { rawValue }
|
||||||
|
|
||||||
var title: String {
|
var title: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .chargeBooster: return "Ladebooster"
|
case .chargeBooster: return "Ladebooster"
|
||||||
case .solarCharger: return "Solarladeregler"
|
case .victronSolarCharger: return "Solarladeregler"
|
||||||
case .batteryMonitor: return "Batteriemonitor"
|
case .batteryMonitor: return "Batteriemonitor"
|
||||||
case .bms: return "Batterie / BMS"
|
case .bms: return "Batterie / BMS"
|
||||||
case .fridge: return "Kühlbox"
|
case .fridge: return "Kühlbox"
|
||||||
case .leveling: return "Nivellierung"
|
case .leveling: return "Nivellierung"
|
||||||
case .solar: return "VotronicSolarESP"
|
case .votronicSolar: return "VotronicSolarESP"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var symbol: String {
|
var symbol: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .chargeBooster: return "bolt.car"
|
case .chargeBooster: return "bolt.car"
|
||||||
case .solarCharger: return "sun.max"
|
case .victronSolarCharger: return "sun.max"
|
||||||
case .batteryMonitor: return "gauge.with.dots.needle.bottom.50percent"
|
case .batteryMonitor: return "gauge.with.dots.needle.bottom.50percent"
|
||||||
case .bms: return "battery.100percent.bolt"
|
case .bms: return "battery.100percent.bolt"
|
||||||
case .fridge: return "refrigerator"
|
case .fridge: return "refrigerator"
|
||||||
case .leveling: return "level"
|
case .leveling: return "level"
|
||||||
case .solar: return "sun.max"
|
case .votronicSolar: return "sun.max"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,7 +63,7 @@ enum DeviceRole: String, Codable, CaseIterable, Identifiable, Sendable {
|
|||||||
/// braucht eine echte GATT-Verbindung.
|
/// braucht eine echte GATT-Verbindung.
|
||||||
var transport: DeviceTransport {
|
var transport: DeviceTransport {
|
||||||
switch self {
|
switch self {
|
||||||
case .bms, .fridge, .leveling, .solar: return .connect
|
case .bms, .fridge, .leveling, .votronicSolar: return .connect
|
||||||
default: return .advertisement
|
default: return .advertisement
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-10
@@ -76,22 +76,22 @@ var advertisement: [UInt8] = [0xE1, 0x02, 0x10, 0x00, 0x4C, 0xA0, 0x01,
|
|||||||
UInt8(nonce & 0xFF), UInt8(nonce >> 8), deviceKey[0]]
|
UInt8(nonce & 0xFF), UInt8(nonce >> 8), deviceKey[0]]
|
||||||
advertisement += encrypted
|
advertisement += encrypted
|
||||||
|
|
||||||
let solar = try? VictronAdvertisement.decode(manufacturerData: Data(advertisement),
|
let victronSolar = try? VictronAdvertisement.decode(manufacturerData: Data(advertisement),
|
||||||
key: deviceKey,
|
key: deviceKey,
|
||||||
deviceID: UUID(),
|
deviceID: UUID(),
|
||||||
rssi: -55)
|
rssi: -55)
|
||||||
func value(_ snapshot: DeviceSnapshot?, _ key: String) -> Double? {
|
func value(_ snapshot: DeviceSnapshot?, _ key: String) -> Double? {
|
||||||
snapshot?.metrics.first { $0.key == key }?.value
|
snapshot?.metrics.first { $0.key == key }?.value
|
||||||
}
|
}
|
||||||
check("Advertisement wird dekodiert", solar != nil)
|
check("Advertisement wird dekodiert", victronSolar != nil)
|
||||||
checkEqual("Zustand als Klartext", solar?.state, "Konstantstrom (Bulk)")
|
checkEqual("Zustand als Klartext", victronSolar?.state, "Konstantstrom (Bulk)")
|
||||||
checkEqual("kein Fehler gemeldet", solar?.fault, nil)
|
checkEqual("kein Fehler gemeldet", victronSolar?.fault, nil)
|
||||||
checkEqual("Batteriespannung", value(solar, "battery_voltage").map(round2), 13.45)
|
checkEqual("Batteriespannung", value(victronSolar, "battery_voltage").map(round2), 13.45)
|
||||||
checkEqual("Ladestrom", value(solar, "battery_current").map { ($0 * 10).rounded() / 10 }, 15.2)
|
checkEqual("Ladestrom", value(victronSolar, "battery_current").map { ($0 * 10).rounded() / 10 }, 15.2)
|
||||||
checkEqual("Tagesertrag", value(solar, "yield_today"), 2.34)
|
checkEqual("Tagesertrag", value(victronSolar, "yield_today"), 2.34)
|
||||||
checkEqual("PV-Leistung", value(solar, "pv_power"), 210)
|
checkEqual("PV-Leistung", value(victronSolar, "pv_power"), 210)
|
||||||
checkEqual("Laststrom bleibt leer (NA)", value(solar, "load_current"), nil)
|
checkEqual("Laststrom bleibt leer (NA)", value(victronSolar, "load_current"), nil)
|
||||||
checkEqual("PV-Leistung ist der Hauptwert", solar?.primaryMetric?.key, "pv_power")
|
checkEqual("PV-Leistung ist der Hauptwert", victronSolar?.primaryMetric?.key, "pv_power")
|
||||||
|
|
||||||
var wrongKey = deviceKey; wrongKey[0] = 0x00
|
var wrongKey = deviceKey; wrongKey[0] = 0x00
|
||||||
do {
|
do {
|
||||||
|
|||||||
@@ -17,6 +17,8 @@ swiftc -O -o "$OUT/tests" \
|
|||||||
Shared/Models/DeviceSnapshot.swift \
|
Shared/Models/DeviceSnapshot.swift \
|
||||||
Shared/Models/FridgeSettings.swift \
|
Shared/Models/FridgeSettings.swift \
|
||||||
Shared/Models/ConfiguredDevice.swift \
|
Shared/Models/ConfiguredDevice.swift \
|
||||||
|
Shared/Models/VehicleGraphicStyle.swift \
|
||||||
|
Shared/VehicleTilt.swift \
|
||||||
Shared/Models/LevelState.swift \
|
Shared/Models/LevelState.swift \
|
||||||
Shared/Models/Profile.swift \
|
Shared/Models/Profile.swift \
|
||||||
Shared/Models/AlignmentAssistant.swift \
|
Shared/Models/AlignmentAssistant.swift \
|
||||||
|
|||||||
Reference in New Issue
Block a user