Demo-Modus: Persistenz und Kühlbox-Bedienung reparieren, Live Activity aktivieren

Drei vom Nutzer im Testbuild gefundene Fehler:

* DeviceStore.save() schrieb auch im Demo-Modus in UserDefaults. Jede
  Änderung während aktivem Demo-Modus (z.B. Fahrzeuggrafik oder
  Einbaulage ändern) persistierte damit die erfundenen Fahrzeuge/Geräte
  dauerhaft – sie blieben nach dem Deaktivieren des Demo-Modus stehen.
  save() ist jetzt im Demo-Modus ein no-op.

* Die Kühlbox-Schalter (Ein/Aus, Eco/Max, Solltemperatur, Sperre) liefen
  über sendFridgeSettings(), das eine echte Bluetooth-Verbindung
  voraussetzt (`managed`-Eintrag) – im Demo-Modus gibt es die nie, die
  Schalter taten also nichts. Im Demo-Modus wird der angezeigte
  AlpicoolState jetzt direkt geändert, ohne den Umweg über einen
  Stellbefehl.

* Der Live-Activity-Schalter liess sich nicht einschalten, weil dem
  Haupt-App-Target der Info.plist-Schlüssel NSSupportsLiveActivities
  fehlte – Activity.request() wirft ohne ihn, wird aber vom Aufrufer
  stillschweigend abgefangen, sodass der Schalter beim Antippen einfach
  auf Aus zurückspringt.

Build (App + Watch) und Tests grün.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
fototeddy
2026-09-06 22:05:37 +02:00
co-authored by Claude Sonnet 5
parent b956324234
commit eed8f33e30
3 changed files with 41 additions and 0 deletions
+2
View File
@@ -507,6 +507,7 @@
INFOPLIST_FILE = "Config/VanControl-Info.plist"; INFOPLIST_FILE = "Config/VanControl-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "VanControl Pro"; INFOPLIST_KEY_CFBundleDisplayName = "VanControl Pro";
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Zum Auslesen von Victron-Geraeten und dem Daly BMS per Bluetooth."; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Zum Auslesen von Victron-Geraeten und dem Daly BMS per Bluetooth.";
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
@@ -538,6 +539,7 @@
INFOPLIST_FILE = "Config/VanControl-Info.plist"; INFOPLIST_FILE = "Config/VanControl-Info.plist";
INFOPLIST_KEY_CFBundleDisplayName = "VanControl Pro"; INFOPLIST_KEY_CFBundleDisplayName = "VanControl Pro";
INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Zum Auslesen von Victron-Geraeten und dem Daly BMS per Bluetooth."; INFOPLIST_KEY_NSBluetoothAlwaysUsageDescription = "Zum Auslesen von Victron-Geraeten und dem Daly BMS per Bluetooth.";
INFOPLIST_KEY_NSSupportsLiveActivities = YES;
INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES;
INFOPLIST_KEY_UILaunchScreen_Generation = YES; INFOPLIST_KEY_UILaunchScreen_Generation = YES;
INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight";
@@ -394,6 +394,10 @@ final class BluetoothManager: NSObject {
// MARK: - Kühlbox steuern (vom Hauptthread aufgerufen) // MARK: - Kühlbox steuern (vom Hauptthread aufgerufen)
func updateFridgeZoneMode(for device: ConfiguredDevice) { func updateFridgeZoneMode(for device: ConfiguredDevice) {
guard !isDemo else {
applyDemoFridgeChange(for: device.id) { $0.zoneMode = device.fridgeZoneMode }
return
}
let deviceID = device.id, mode = device.fridgeZoneMode let deviceID = device.id, mode = device.fridgeZoneMode
queue.async { queue.async {
guard let session = self.session(for: deviceID) else { return } guard let session = self.session(for: deviceID) else { return }
@@ -404,21 +408,51 @@ final class BluetoothManager: NSObject {
} }
func setFridgeTarget(_ celsius: Int, zone: AlpicoolState.Zone, for deviceID: UUID) { func setFridgeTarget(_ celsius: Int, zone: AlpicoolState.Zone, for deviceID: UUID) {
guard !isDemo else {
applyDemoFridgeChange(for: deviceID) { state in
switch zone {
case .left: state.leftTarget = celsius
case .right: state.rightTarget = celsius
}
}
return
}
sendFridgeSettings(for: deviceID) { _ in AlpicoolState.setTarget(zone: zone, to: celsius) } sendFridgeSettings(for: deviceID) { _ in AlpicoolState.setTarget(zone: zone, to: celsius) }
} }
func setFridgePower(_ on: Bool, for deviceID: UUID) { func setFridgePower(_ on: Bool, for deviceID: UUID) {
guard !isDemo else {
applyDemoFridgeChange(for: deviceID) { $0.isPoweredOn = on }
return
}
sendFridgeSettings(for: deviceID) { $0.settingsCommand(poweredOn: on) } sendFridgeSettings(for: deviceID) { $0.settingsCommand(poweredOn: on) }
} }
func setFridgeEco(_ eco: Bool, for deviceID: UUID) { func setFridgeEco(_ eco: Bool, for deviceID: UUID) {
guard !isDemo else {
applyDemoFridgeChange(for: deviceID) { $0.runMode = eco ? 1 : 0 }
return
}
sendFridgeSettings(for: deviceID) { $0.settingsCommand(eco: eco) } sendFridgeSettings(for: deviceID) { $0.settingsCommand(eco: eco) }
} }
func setFridgeLock(_ locked: Bool, for deviceID: UUID) { func setFridgeLock(_ locked: Bool, for deviceID: UUID) {
guard !isDemo else {
applyDemoFridgeChange(for: deviceID) { $0.isLocked = locked }
return
}
sendFridgeSettings(for: deviceID) { $0.settingsCommand(locked: locked) } sendFridgeSettings(for: deviceID) { $0.settingsCommand(locked: locked) }
} }
/// Im Demo-Modus gibt es keine Box, die einen Stellbefehl bestätigt die
/// Änderung wird direkt im angezeigten Zustand nachgezogen, auf dem
/// Hauptthread, wo `fridgeStates` lebt.
private func applyDemoFridgeChange(for deviceID: UUID, _ change: (inout AlpicoolState) -> Void) {
var state = fridgeStates[deviceID] ?? DemoData.fridgeState
change(&state)
fridgeStates[deviceID] = state
}
/// Der Einstellungsblock wird aus dem zuletzt empfangenen Zustand gebaut /// Der Einstellungsblock wird aus dem zuletzt empfangenen Zustand gebaut
/// und zwar auf der Funk-Queue, wo dieser Zustand lebt. /// und zwar auf der Funk-Queue, wo dieser Zustand lebt.
/// ///
+5
View File
@@ -244,7 +244,12 @@ final class DeviceStore {
} }
} }
/// Im Demo-Modus wird nichts geschrieben sonst überlebten die erfundenen
/// Fahrzeuge und Geräte das Ausschalten des Demo-Modus, weil z.B. das
/// Ändern der Fahrzeuggrafik oder der Einbaulage ganz normal `save()`
/// aufruft.
private func save() { private func save() {
guard !DemoData.isEnabled else { return }
let defaults = UserDefaults.standard let defaults = UserDefaults.standard
if let data = try? JSONEncoder().encode(devices) { if let data = try? JSONEncoder().encode(devices) {
defaults.set(data, forKey: Self.devicesKey) defaults.set(data, forKey: Self.devicesKey)