Kühlbox nur verbinden, während man sie ansieht
Jede Verbindung meldet sich am Display der Box an – sie piept, und wer gerade davorsteht, wird gestört. Dauerhaft verbunden zu sein ist dort also nicht unsichtbar wie beim BMS, sondern lästig. Die Kühlbox wird deshalb nur noch verbunden, solange ihre Ansicht offen ist: am iPhone über die Detailansicht, an der Uhr über einen eigenen Befehl, den sie beim Öffnen und Schliessen schickt. Ein Stellbefehl geht weiterhin immer durch – ist die Box nicht verbunden, wird er gemerkt und löst den Verbindungsaufbau aus. Abgeriegelt sind alle drei Wege, über die bisher verbunden wurde: beim Start, im Takt des Wiederverbindens, und – das war das eigentliche Loch – sobald das Gerät in den Werbedaten auftaucht. Da die App dauerhaft scannt, hätte allein dieser Weg die Box weiter angefunkt. Dass sie in Reichweite ist, ist kein Grund, sie anzufassen. Auf der Übersicht steht dafür der zuletzt gestellte Stand statt der Messwerte: Sollwert, Ein/Aus, Eco oder Max, dazu wann er gestellt wurde. Das bleibt richtig, auch wenn es von gestern ist – ein Sollwert ändert sich nur, wenn jemand ihn ändert. Eine Innentemperatur von gestern sähe dagegen aus wie eine von jetzt, und man würde ihr glauben. Genau das sichern die neuen Prüfungen ab: Hauptwert ist der Sollwert, gemessene Temperatur und Bordspannung kommen nicht vor. Der Stand liegt in den Einstellungen (FridgeSettings) mit einem Zeitstempel, der „zuletzt geändert" bedeutet und nicht „zuletzt gesehen" – sonst behauptete die Kachel Frische, wo sich nichts getan hat. Die Uhr bekommt dasselbe Bild: WatchFridge trägt jetzt ein Kennzeichen isLive. BMS und Neigungsmesser bleiben dauerhaft verbunden. Sie stören nicht, und ihre Werte will man laufend sehen. Nicht nachgezogen ist die Android-App – dort verbindet der BluetoothManager weiterhin dauerhaft. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -232,6 +232,19 @@ struct AlpicoolState: Equatable {
|
||||
}
|
||||
}
|
||||
|
||||
/// Der Stand, der die Verbindung überdauert – ohne Messwerte.
|
||||
var settings: FridgeSettings? {
|
||||
guard hasStatus else { return nil }
|
||||
return FridgeSettings(isPoweredOn: isPoweredOn,
|
||||
isEco: isEco,
|
||||
isLocked: isLocked,
|
||||
isDualZone: isDualZone,
|
||||
usesFahrenheit: usesFahrenheit,
|
||||
leftTarget: leftTarget,
|
||||
rightTarget: isDualZone ? rightTarget : nil,
|
||||
updated: Date())
|
||||
}
|
||||
|
||||
/// Die Bytes, die ein Stellbefehl ändert.
|
||||
///
|
||||
/// Messwerte gehören nicht dazu: Temperatur und Spannung schwanken
|
||||
|
||||
@@ -178,6 +178,11 @@ final class BluetoothManager: NSObject {
|
||||
}
|
||||
private var managed: [UUID: ManagedDevice] = [:] // Schlüssel: peripheralID
|
||||
|
||||
/// Geräte, die nur verbunden werden, solange ihre Ansicht offen ist.
|
||||
/// Schlüssel ist die Gerätekennung, gezählt wird nicht – eine Ansicht ist
|
||||
/// zu einer Zeit offen.
|
||||
private var requestedSessions: Set<UUID> = []
|
||||
|
||||
private var bmsSessions: [UUID: BMSSession] = [:]
|
||||
private var levelSessions: [UUID: LevelSession] = [:]
|
||||
private var connectedPeripherals: [UUID: CBPeripheral] = [:]
|
||||
@@ -298,6 +303,14 @@ final class BluetoothManager: NSObject {
|
||||
fridgeStates = fridgeStates.filter { known.contains($0.key) }
|
||||
levelStates = levelStates.filter { known.contains($0.key) }
|
||||
|
||||
// Geräte, die nur auf Anforderung verbunden werden, zeigen bis dahin
|
||||
// ihren zuletzt gestellten Stand.
|
||||
for device in store.activeDevices where device.role.connectsOnDemand {
|
||||
guard linkStates[device.id] != .live else { continue }
|
||||
linkStates[device.id] = .idle
|
||||
showLastKnownFridgeSettings(for: device.id)
|
||||
}
|
||||
|
||||
queue.async { self.applyConfiguration(devices) }
|
||||
}
|
||||
|
||||
@@ -309,6 +322,57 @@ final class BluetoothManager: NSObject {
|
||||
}
|
||||
}
|
||||
|
||||
// MARK: - Verbindung auf Anforderung (vom Hauptthread aufgerufen)
|
||||
|
||||
/// Verbindet ein Gerät, das sonst unverbunden bleibt – solange seine
|
||||
/// Ansicht offen ist.
|
||||
///
|
||||
/// Für die Kühlbox: Jede Verbindung meldet sich an ihrem Display an, manche
|
||||
/// Modelle verlangen dabei sogar einen Tastendruck. Dauerhaft verbunden zu
|
||||
/// sein ist dort also nicht unsichtbar, sondern lästig.
|
||||
func beginSession(for device: ConfiguredDevice) {
|
||||
guard device.role.connectsOnDemand else { return }
|
||||
let deviceID = device.id
|
||||
queue.async {
|
||||
self.requestedSessions.insert(deviceID)
|
||||
guard let entry = self.managed.values.first(where: { $0.id == deviceID }) else { return }
|
||||
// Der Wunsch des Benutzers hebt die Wartesperre auf.
|
||||
self.clearBackOff(entry.peripheralID)
|
||||
self.connectIfNeeded(entry)
|
||||
}
|
||||
}
|
||||
|
||||
/// Gibt das Gerät wieder frei. Der zuletzt gemeldete Stand bleibt in der
|
||||
/// Übersicht stehen, damit dort nicht plötzlich eine Lücke klafft.
|
||||
func endSession(for device: ConfiguredDevice) {
|
||||
guard device.role.connectsOnDemand else { return }
|
||||
let deviceID = device.id
|
||||
queue.async {
|
||||
self.requestedSessions.remove(deviceID)
|
||||
guard let entry = self.managed.values.first(where: { $0.id == deviceID }) else { return }
|
||||
self.bmsSessions[entry.peripheralID]?.stop()
|
||||
self.bmsSessions[entry.peripheralID] = nil
|
||||
self.disconnect(entry.peripheralID)
|
||||
self.publish {
|
||||
self.linkStates[deviceID] = .idle
|
||||
self.showLastKnownFridgeSettings(for: deviceID)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Setzt an die Stelle der Live-Werte den zuletzt gestellten Stand.
|
||||
///
|
||||
/// Nur auf dem Hauptthread aufrufen: greift auf den Speicher zu.
|
||||
private func showLastKnownFridgeSettings(for deviceID: UUID) {
|
||||
guard let settings = store.lastFridgeSettings(for: deviceID) else {
|
||||
snapshots[deviceID] = nil
|
||||
return
|
||||
}
|
||||
snapshots[deviceID] = settings.snapshot(deviceID: deviceID)
|
||||
// Der Zustand bleibt stehen, damit sich die Box beim nächsten Öffnen
|
||||
// sofort bedienen lässt – die Schalter zeigen dann den letzten Stand.
|
||||
}
|
||||
|
||||
// MARK: - Kühlbox steuern (vom Hauptthread aufgerufen)
|
||||
|
||||
func updateFridgeZoneMode(for device: ConfiguredDevice) {
|
||||
@@ -431,8 +495,10 @@ final class BluetoothManager: NSObject {
|
||||
private func applyConfiguration(_ devices: [ManagedDevice]) {
|
||||
managed = Dictionary(uniqueKeysWithValues: devices.map { ($0.peripheralID, $0) })
|
||||
|
||||
// Verbindungen zu Geräten lösen, die nicht mehr dazugehören.
|
||||
let wanted = Set(devices.filter { $0.transport == .connect }.map(\.peripheralID))
|
||||
// Verbindungen zu Geräten lösen, die nicht mehr dazugehören – und zu
|
||||
// denen, die nur auf Anforderung verbunden werden und gerade niemand
|
||||
// anschaut.
|
||||
let wanted = Set(devices.filter { shouldStayConnected($0) }.map(\.peripheralID))
|
||||
for (peripheralID, session) in bmsSessions where !wanted.contains(peripheralID) {
|
||||
session.stop()
|
||||
bmsSessions[peripheralID] = nil
|
||||
@@ -509,11 +575,22 @@ final class BluetoothManager: NSObject {
|
||||
|
||||
private func connectManagedPeripherals() {
|
||||
guard central?.state == .poweredOn else { return }
|
||||
for device in managed.values where device.transport == .connect {
|
||||
for device in managed.values where shouldStayConnected(device) {
|
||||
connectIfNeeded(device)
|
||||
}
|
||||
}
|
||||
|
||||
/// Ob dieses Gerät von sich aus verbunden gehalten wird.
|
||||
///
|
||||
/// Geräte auf Anforderung – die Kühlbox – nur dann, wenn ihre Ansicht
|
||||
/// gerade offen ist oder ein Stellbefehl darauf wartet.
|
||||
private func shouldStayConnected(_ device: ManagedDevice) -> Bool {
|
||||
guard device.transport == .connect else { return false }
|
||||
guard device.role.connectsOnDemand else { return true }
|
||||
return requestedSessions.contains(device.id)
|
||||
|| pendingControls[device.peripheralID]?.isEmpty == false
|
||||
}
|
||||
|
||||
private func connectIfNeeded(_ device: ManagedDevice) {
|
||||
if let existing = connectedPeripherals[device.peripheralID],
|
||||
existing.state == .connected || existing.state == .connecting {
|
||||
@@ -736,8 +813,11 @@ extension BluetoothManager: CBCentralManagerDelegate {
|
||||
handleVictronAdvertisement(manufacturerData, device: device, rssi: rssi)
|
||||
|
||||
case .connect:
|
||||
// Das Gerät wurde gesehen – falls die Verbindung fehlt, jetzt aufbauen.
|
||||
if connectedPeripherals[identifier] == nil {
|
||||
// Das Gerät wurde gesehen – falls die Verbindung fehlt, jetzt
|
||||
// aufbauen. Ausser bei Geräten auf Anforderung: Die Kühlbox piept
|
||||
// bei jeder Verbindung, und dass sie in Reichweite ist, ist kein
|
||||
// Grund, sie anzufassen.
|
||||
if connectedPeripherals[identifier] == nil, shouldStayConnected(device) {
|
||||
connectIfNeeded(device)
|
||||
}
|
||||
}
|
||||
@@ -786,7 +866,14 @@ extension BluetoothManager: CBCentralManagerDelegate {
|
||||
)
|
||||
session.onFridgeState = { [weak self] state in
|
||||
self?.fridgeStateCache[peripheral.identifier] = state
|
||||
self?.publish { self?.fridgeStates[device.id] = state }
|
||||
self?.publish {
|
||||
self?.fridgeStates[device.id] = state
|
||||
// Für die Übersicht aufheben: Sie zeigt den Stand auch dann,
|
||||
// wenn längst niemand mehr verbunden ist.
|
||||
if let settings = state.settings {
|
||||
self?.store.setLastFridgeSettings(settings, for: device.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
session.fridgeZoneMode = device.fridgeZoneMode
|
||||
bmsSessions[peripheral.identifier] = session
|
||||
@@ -817,7 +904,15 @@ extension BluetoothManager: CBCentralManagerDelegate {
|
||||
let lifetime = connectedSince.removeValue(forKey: peripheral.identifier)
|
||||
.map { Date().timeIntervalSince($0) } ?? 0
|
||||
|
||||
if let device = managed[peripheral.identifier] {
|
||||
if let device = managed[peripheral.identifier], !shouldStayConnected(device) {
|
||||
// Auf Anforderung verbunden und niemand schaut mehr hin: nicht
|
||||
// wieder anklopfen, sondern den letzten Stand stehenlassen.
|
||||
let deviceID = device.id
|
||||
publish {
|
||||
self.linkStates[deviceID] = .idle
|
||||
self.showLastKnownFridgeSettings(for: deviceID)
|
||||
}
|
||||
} else if let device = managed[peripheral.identifier] {
|
||||
publish { self.linkStates[device.id] = .searching }
|
||||
if lifetime >= stableConnection {
|
||||
// Die Verbindung stand und ist weggefallen - im Fahrzeug der
|
||||
|
||||
Reference in New Issue
Block a user