import SwiftUI /// Einrichten eines neuen Geräts: scannen, auswählen, benennen. struct AddDeviceView: View { @Environment(DeviceStore.self) private var store @Environment(BluetoothManager.self) private var bluetooth @Environment(\.dismiss) private var dismiss @State private var selected: Discovery? /// Auf einem Stellplatz sind dutzende fremde Geräte in Reichweite. @State private var showsAllDevices = false /// Nur Geräte anzeigen, die in den letzten Sekunden zu hören waren – /// sonst füllt sich die Liste in Wohnmobilparks endlos. private var visibleDiscoveries: [Discovery] { // Bewusst nicht nach Signalstärke sortieren: die schwankt im // Sekundentakt und die Liste würde unter dem Finger springen. bluetooth.discoveries.values .filter { showsAllDevices || $0.looksLikeSupported } .sorted { lhs, rhs in //if lhs.isVictron != rhs.isVictron { return lhs.isVictron } if lhs.looksLikeSupported != rhs.looksLikeSupported { return lhs.looksLikeSupported } return lhs.firstSeen < rhs.firstSeen } } private var alreadyAdded: Set { Set(store.activeDevices.map(\.peripheralID)) } var body: some View { NavigationStack { List { Picker("Anzeige", selection: $showsAllDevices) { Text("Passende Geräte").tag(false) Text("Alle").tag(true) } .pickerStyle(.segmented) .listRowBackground(Color.clear) Section { if visibleDiscoveries.isEmpty { Label(showsAllDevices ? "Suche…" : "Noch nichts Passendes gefunden", systemImage: "antenna.radiowaves.left.and.right") .foregroundStyle(.secondary) } ForEach(visibleDiscoveries) { discovery in Button { selected = discovery } label: { row(for: discovery) } .disabled(alreadyAdded.contains(discovery.id)) } } header: { HStack { Text("Gefundene Geräte") Spacer() ProgressView().controlSize(.small) } } footer: { Text("Victron-Geräte werden automatisch erkannt. Damit sie hier " + "auftauchen, muss „Instant Readout“ in VictronConnect aktiv sein. " + "Das BMS meldet sich meist als „DL-…“. Kühlboxen tragen oft " + "einen kryptischen Namen – findest du dein Gerät nicht, auf " + "„Alle“ umschalten.") } } .navigationTitle("Gerät für \(store.activeProfile?.name ?? "Camper")") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Fertig") { dismiss() } } } .sheet(item: $selected) { discovery in ConfigureDeviceView(discovery: discovery) { dismiss() } } .onAppear { bluetooth.clearDiscoveries() bluetooth.isDiscovering = true } .onDisappear { bluetooth.isDiscovering = false } } } private func row(for discovery: Discovery) -> some View { HStack(spacing: 12) { Image(systemName/*: discovery.isVictron ? "bolt.circle.fill"*/ : discovery.looksLikeSupported ? "battery.100percent" : "dot.radiowaves.left.and.right") .font(.title3) .foregroundStyle(/*discovery.isVictron || */discovery.looksLikeSupported ? Color.accentColor : Color.secondary) .frame(width: 28) VStack(alignment: .leading, spacing: 2) { Text(discovery.displayName) .foregroundStyle(.primary) Text(discovery.subtitle) .font(.caption) .foregroundStyle(.secondary) } Spacer() if alreadyAdded.contains(discovery.id) { Text("Hinzugefügt") .font(.caption) .foregroundStyle(.secondary) } else { SignalBars(rssi: discovery.rssi) } } } } /// Formular für ein neu gewähltes Gerät. private struct ConfigureDeviceView: View { let discovery: Discovery let onSaved: () -> Void @Environment(DeviceStore.self) private var store @Environment(BluetoothManager.self) private var bluetooth @Environment(\.dismiss) private var dismiss @State private var name: String = "" @State private var role: DeviceRole = .leveling @State private var key: String = "" //private var needsKey: Bool { role.transport == .advertisement } //private var keyIsValid: Bool { key.hexBytes?.count == 16 } private var canSave: Bool { !name.trimmingCharacters(in: .whitespaces).isEmpty /*&& (!needsKey || key.isEmpty || keyIsValid)*/ } var body: some View { NavigationStack { Form { Section("Gerät") { LabeledContent("Gefunden als", value: discovery.displayName) TextField("Name", text: $name) Picker("Art", selection: $role) { ForEach(DeviceRole.allCases) { role in Text(role.title).tag(role) } } } /*if needsKey { Section { TextField("32 Hex-Zeichen", text: $key) .font(.system(.body, design: .monospaced)) .textInputAutocapitalization(.never) .autocorrectionDisabled() if !key.isEmpty && !keyIsValid { Label("Der Schlüssel muss 16 Byte (32 Hex-Zeichen) haben.", systemImage: "exclamationmark.circle") .font(.caption) .foregroundStyle(.orange) } } header: { Text("Verschlüsselungsschlüssel") } footer: { Text("VictronConnect → Gerät → Zahnrad → ⋮ → Produkt-Info → " + "„Instant Readout“ aktivieren → Verschlüsselungsdaten anzeigen. " + "Kann auch später nachgetragen werden.") } }*/ } .navigationTitle("Einrichten") .navigationBarTitleDisplayMode(.inline) .toolbar { ToolbarItem(placement: .cancellationAction) { Button("Abbrechen") { dismiss() } } ToolbarItem(placement: .confirmationAction) { Button("Sichern", action: save).disabled(!canSave) } } .onAppear(perform: prefill) } } /// Aus dem Advertisement lässt sich Art und Name oft schon erraten. private func prefill() { /*if let recordType = discovery.victronRecordType { switch VictronAdvertisement.RecordType(rawValue: recordType) { case .solarCharger: role = .solarCharger case .dcdcConverter, .orionXS: role = .chargeBooster case .batteryMonitor: role = .batteryMonitor default: role = .solarCharger } } else*/ if discovery.isLevelSensor { role = .leveling } else if discovery.isVotronicSolarESPSensor { role = .solar } /*else if let name = discovery.name?.lowercased(), ["alpicool", "icecube", "ice cube", "fridge", "cool"].contains(where: name.contains) { role = .fridge } else if discovery.looksLikeSupported { role = .bms }*/ // Funknamen wie "WTaEaAA25342229" taugen nicht als Anzeigename. Die // Art des Geräts ist der bessere Vorschlag; der Funkname steht // ohnehin darüber unter "Gefunden als". if discovery.isLevelSensor { name = "Nivellierung" } else if discovery.isVotronicSolarESPSensor { name = role.title } else if let advertised = discovery.name, advertised.count <= 20, advertised.contains(" ") || advertised.rangeOfCharacter(from: .decimalDigits) == nil { name = advertised } else { name = role.title } } private func save() { let device = ConfiguredDevice( name: name.trimmingCharacters(in: .whitespaces), role: role, profileID: store.activeProfileID, peripheralID: discovery.id, advertisedName: discovery.name ) store.add(device/*, victronKey: needsKey && keyIsValid ? key : nil*/) bluetooth.refreshConfiguration() dismiss() onSaved() } } /// Signalstärke als drei Balken. struct SignalBars: View { let rssi: Int private var level: Int { switch rssi { case (-60)...: return 3 case (-75)..<(-60): return 2 case (-90)..<(-75): return 1 default: return 0 } } var body: some View { HStack(alignment: .bottom, spacing: 2) { ForEach(1...3, id: \.self) { bar in RoundedRectangle(cornerRadius: 1) .fill(bar <= level ? Color.accentColor : Color.secondary.opacity(0.25)) .frame(width: 3, height: CGFloat(bar) * 4 + 2) } } .accessibilityLabel("Signalstärke \(level) von 3") } }