forked from fritob/Camper-Monitor
Geräteansicht aufräumen: Diagnose runter, Schlüssel weg
Oben in der Geräteansicht stand, was zur Diagnose taugt, aber im Alltag nichts sagt. Der Empfangspegel ist raus - er hilft beim Aufspüren eines Geräts, sonst nie - und erscheint jetzt nur noch mit eingeblendeter Diagnose. Ebenso das Alter der Werte: solange sie frisch sind, sagt es nichts, was der Verbindungspunkt nicht schon zeigt. Bleiben sie stehen, ist es die eigentliche Nachricht und wird gezeigt. Der Victron-Schlüssel stand mit Eingabefeld und Anleitung ganz oben, obwohl er einmal eingetragen und danach nie wieder angefasst wird. Er hat jetzt eine eigene Seite, erreichbar aus den Einstellungen - dort, wo auch die Bluetooth-ID steht. In der Übersicht bleibt nur eine Zeile mit dem Stand: hinterlegt, fehlt oder passt nicht. Fehlt der Schlüssel wirklich oder gehört er zu einem anderen Gerät, steht weiter ganz oben ein Hinweis - dann ist er ja das Einzige, was zu tun ist. Kommen Werte an, passt er offensichtlich, und es wird nichts gemeldet. Der Vergleich der ersten Schlüsselbytes ist aus der Diagnose auf die Schlüsselseite gewandert. Dort wird er gebraucht. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -9,6 +9,8 @@ struct DeviceDetailView: View {
|
||||
@Environment(\.dismiss) private var dismiss
|
||||
|
||||
@State private var editedName = ""
|
||||
/// Nur zum Vergleich mit dem, was das Gerät sendet. Eingetragen wird der
|
||||
/// Schlüssel auf einer eigenen Seite.
|
||||
@State private var keyInput = ""
|
||||
@State private var showDeleteConfirmation = false
|
||||
@State private var didCopyReport = false
|
||||
@@ -40,6 +42,7 @@ struct DeviceDetailView: View {
|
||||
var body: some View {
|
||||
List {
|
||||
statusSection
|
||||
if needsKeyAttention { keyPrompt }
|
||||
|
||||
if currentDevice.role == .fridge, let fridge = bluetooth.fridgeStates[device.id], fridge.hasStatus {
|
||||
FridgeControls(device: currentDevice, state: fridge)
|
||||
@@ -102,7 +105,6 @@ struct DeviceDetailView: View {
|
||||
}
|
||||
|
||||
if currentDevice.role.transport == .advertisement {
|
||||
keySection
|
||||
if showsTechnicalDetails { diagnosticsSection }
|
||||
} else if showsTechnicalDetails {
|
||||
bmsDiagnosticsSection
|
||||
@@ -148,13 +150,18 @@ struct DeviceDetailView: View {
|
||||
Label(reason, systemImage: "pause.circle")
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
if let snapshot {
|
||||
// Solange die Werte frisch sind, sagt das Alter nichts, was der
|
||||
// Verbindungspunkt nicht schon zeigt. Erst wenn sie stehenbleiben,
|
||||
// ist es die eigentliche Nachricht.
|
||||
if let snapshot, snapshot.isStale || showsTechnicalDetails {
|
||||
LabeledContent("Aktualisiert vor") {
|
||||
Text(snapshot.timestamp, style: .relative)
|
||||
}
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
if let rssi = snapshot?.rssi {
|
||||
// Der Empfangspegel hilft beim Suchen eines Geräts, im Alltag
|
||||
// sagt er nichts – deshalb nur bei eingeblendeter Diagnose.
|
||||
if showsTechnicalDetails, let rssi = snapshot?.rssi {
|
||||
LabeledContent("Signal", value: "\(rssi) dBm")
|
||||
.foregroundStyle(.secondary)
|
||||
}
|
||||
@@ -201,21 +208,33 @@ struct DeviceDetailView: View {
|
||||
}
|
||||
}
|
||||
|
||||
private var keySection: some View {
|
||||
/// Ohne passenden Schlüssel bleibt das Gerät stumm – das ist dann keine
|
||||
/// Nebensache, sondern das Einzige, was zu tun ist.
|
||||
private var needsKeyAttention: Bool {
|
||||
guard currentDevice.role.transport == .advertisement else { return false }
|
||||
if keyBytesAgree == false { return true }
|
||||
// Kommen Werte an, passt der Schlüssel offensichtlich – dann ist hier
|
||||
// nichts zu tun und nichts zu melden.
|
||||
if snapshot != nil, linkState == .live { return false }
|
||||
return store.victronKeyText(for: device.id)?.hexBytes?.count != 16
|
||||
}
|
||||
|
||||
private var keyPrompt: some View {
|
||||
Section {
|
||||
TextField("32 Hex-Zeichen", text: $keyInput)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.onSubmit(saveKey)
|
||||
Button("Schlüssel speichern", action: saveKey)
|
||||
.disabled(keyInput.hexBytes?.count != 16)
|
||||
} header: {
|
||||
Text("Verschlüsselungsschlüssel")
|
||||
NavigationLink {
|
||||
VictronKeyView(device: currentDevice)
|
||||
} label: {
|
||||
Label(keyBytesAgree == false
|
||||
? "Schlüssel passt nicht zum Gerät"
|
||||
: "Verschlüsselungsschlüssel eintragen",
|
||||
systemImage: "key.horizontal.fill")
|
||||
.foregroundStyle(.orange)
|
||||
}
|
||||
} footer: {
|
||||
Text("In VictronConnect: Gerät öffnen → Zahnrad → ⋮ → Produkt-Info → "
|
||||
+ "„Instant Readout“ einschalten → Verschlüsselungsdaten anzeigen. "
|
||||
+ "Der Schlüssel ist 16 Byte lang (32 Hex-Zeichen).")
|
||||
Text(keyBytesAgree == false
|
||||
? "Der hinterlegte Schlüssel stammt von einem anderen Victron-Gerät."
|
||||
: "Victron-Geräte senden ihre Werte verschlüsselt. Ohne den "
|
||||
+ "Schlüssel aus VictronConnect bleibt die Anzeige leer.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -226,16 +245,6 @@ struct DeviceDetailView: View {
|
||||
private var diagnosticsSection: some View {
|
||||
if let info = bluetooth.diagnostics[device.id] {
|
||||
Section {
|
||||
LabeledContent("Gerät sendet als erstes Schlüsselbyte") {
|
||||
Text(info.expectedKeyText)
|
||||
.font(.body.monospaced())
|
||||
.foregroundStyle(keyBytesAgree == false ? .red : .primary)
|
||||
}
|
||||
LabeledContent("Eingetragener Schlüssel beginnt mit") {
|
||||
Text(enteredKeyText)
|
||||
.font(.body.monospaced())
|
||||
.foregroundStyle(keyBytesAgree == false ? .red : .secondary)
|
||||
}
|
||||
LabeledContent("Datensatz", value: info.recordName)
|
||||
LabeledContent("Produkt-ID") {
|
||||
Text(info.productIDText).font(.body.monospaced())
|
||||
@@ -250,16 +259,10 @@ struct DeviceDetailView: View {
|
||||
} header: {
|
||||
Text("Diagnose")
|
||||
} footer: {
|
||||
if keyBytesAgree == false {
|
||||
Text("Die beiden Bytes müssen übereinstimmen. Tun sie das nicht, "
|
||||
+ "stammt der Schlüssel von einem anderen Victron-Gerät – in "
|
||||
+ "VictronConnect prüfen, ob wirklich dieses Gerät geöffnet war.")
|
||||
} else {
|
||||
Text("Diese Werte sendet das Gerät unverschlüsselt mit.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// nil, solange kein vollständiger Schlüssel eingetragen ist.
|
||||
private var keyBytesAgree: Bool? {
|
||||
@@ -268,9 +271,9 @@ struct DeviceDetailView: View {
|
||||
return expected == entered
|
||||
}
|
||||
|
||||
private var enteredKeyText: String {
|
||||
guard let byte = keyInput.hexBytes?.first else { return "–" }
|
||||
return String(format: "0x%02X", byte)
|
||||
private var keyStatusText: String {
|
||||
if store.victronKeyText(for: device.id)?.hexBytes?.count != 16 { return "fehlt" }
|
||||
return keyBytesAgree == false ? "passt nicht" : "hinterlegt"
|
||||
}
|
||||
|
||||
/// Alles auf einmal, zum Weitergeben. Einzeln abzutippen ist zuviel
|
||||
@@ -452,6 +455,13 @@ struct DeviceDetailView: View {
|
||||
}
|
||||
}
|
||||
}
|
||||
if currentDevice.role.transport == .advertisement {
|
||||
NavigationLink {
|
||||
VictronKeyView(device: currentDevice)
|
||||
} label: {
|
||||
LabeledContent("Verschlüsselung", value: keyStatusText)
|
||||
}
|
||||
}
|
||||
LabeledContent("Bluetooth-ID") {
|
||||
Text(currentDevice.peripheralID.uuidString.prefix(8) + "…")
|
||||
.font(.caption.monospaced())
|
||||
@@ -465,11 +475,6 @@ struct DeviceDetailView: View {
|
||||
|
||||
// MARK: - Aktionen
|
||||
|
||||
private func saveKey() {
|
||||
store.setVictronKey(keyInput, for: device.id)
|
||||
bluetooth.refreshConfiguration()
|
||||
}
|
||||
|
||||
private func saveName() {
|
||||
let trimmed = editedName.trimmingCharacters(in: .whitespaces)
|
||||
// Ein leeres Feld beim Tippen darf den Namen nicht löschen.
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import SwiftUI
|
||||
|
||||
/// Eingabe des Victron-Verschlüsselungsschlüssels.
|
||||
///
|
||||
/// Der Schlüssel wird einmal eingetragen und danach nie wieder angefasst –
|
||||
/// deshalb steht er hier und nicht in der Geräteübersicht. Nur solange er
|
||||
/// fehlt oder nicht passt, weist die Übersicht darauf hin.
|
||||
struct VictronKeyView: View {
|
||||
let device: ConfiguredDevice
|
||||
|
||||
@Environment(DeviceStore.self) private var store
|
||||
@Environment(BluetoothManager.self) private var bluetooth
|
||||
|
||||
@State private var keyInput = ""
|
||||
|
||||
var body: some View {
|
||||
List {
|
||||
Section {
|
||||
TextField("32 Hex-Zeichen", text: $keyInput)
|
||||
.font(.system(.body, design: .monospaced))
|
||||
.textInputAutocapitalization(.never)
|
||||
.autocorrectionDisabled()
|
||||
.onSubmit(save)
|
||||
Button("Schlüssel speichern", action: save)
|
||||
.disabled(keyInput.hexBytes?.count != 16)
|
||||
} footer: {
|
||||
Text("In VictronConnect: Gerät öffnen → Zahnrad → ⋮ → Produkt-Info → "
|
||||
+ "„Instant Readout“ einschalten → Verschlüsselungsdaten anzeigen. "
|
||||
+ "Der Schlüssel ist 16 Byte lang (32 Hex-Zeichen).")
|
||||
}
|
||||
|
||||
// Das erste Byte sendet das Gerät unverschlüsselt mit. Stimmt es
|
||||
// nicht mit dem eingetragenen überein, gehört der Schlüssel zu
|
||||
// einem anderen Victron-Gerät – der häufigste Fehler überhaupt.
|
||||
if let expected = bluetooth.diagnostics[device.id]?.expectedKeyText {
|
||||
Section {
|
||||
LabeledContent("Gerät sendet") {
|
||||
Text(expected)
|
||||
.font(.body.monospaced())
|
||||
.foregroundStyle(bytesAgree == false ? .red : .primary)
|
||||
}
|
||||
LabeledContent("Eingetragen") {
|
||||
Text(enteredKeyText)
|
||||
.font(.body.monospaced())
|
||||
.foregroundStyle(bytesAgree == false ? .red : .secondary)
|
||||
}
|
||||
} header: {
|
||||
Text("Erstes Schlüsselbyte")
|
||||
} footer: {
|
||||
if bytesAgree == false {
|
||||
Text("Die beiden Bytes müssen übereinstimmen. Tun sie das "
|
||||
+ "nicht, stammt der Schlüssel von einem anderen "
|
||||
+ "Victron-Gerät – in VictronConnect prüfen, ob wirklich "
|
||||
+ "dieses Gerät geöffnet war.")
|
||||
} else {
|
||||
Text("Zum Vergleichen: dieses Byte sendet das Gerät "
|
||||
+ "unverschlüsselt mit.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.navigationTitle("Verschlüsselung")
|
||||
.navigationBarTitleDisplayMode(.inline)
|
||||
.onAppear { keyInput = store.victronKeyText(for: device.id) ?? "" }
|
||||
}
|
||||
|
||||
/// nil, solange kein vollständiger Schlüssel eingetragen ist.
|
||||
private var bytesAgree: Bool? {
|
||||
guard let expected = bluetooth.diagnostics[device.id]?.expectedKeyFirstByte,
|
||||
let entered = keyInput.hexBytes?.first else { return nil }
|
||||
return expected == entered
|
||||
}
|
||||
|
||||
private var enteredKeyText: String {
|
||||
guard let byte = keyInput.hexBytes?.first else { return "–" }
|
||||
return String(format: "0x%02X", byte)
|
||||
}
|
||||
|
||||
private func save() {
|
||||
store.setVictronKey(keyInput, for: device.id)
|
||||
bluetooth.refreshConfiguration()
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user