import Foundation import Observation /// Verwaltet die Fahrzeugprofile, die darin eingerichteten Geräte und deren /// Schlüssel. @Observable final class DeviceStore { private static let devicesKey = "configuredDevices" private static let profilesKey = "profiles" private static let activeProfileKey = "activeProfileID" /// Alle Geräte über alle Profile hinweg. private(set) var devices: [ConfiguredDevice] = [] private(set) var profiles: [Profile] = [] private(set) var activeProfileID: UUID = Profile.defaultID /// Zwischenspeicher, damit nicht bei jedem Advertisement die Keychain /// befragt wird – das passiert bis zu mehrmals pro Sekunde. @ObservationIgnored private var keyCache: [UUID: [UInt8]] = [:] init() { if DemoData.isEnabled { profiles = DemoData.profiles devices = DemoData.devices activeProfileID = DemoData.profiles[0].id return } load() } // MARK: - Profile var activeProfile: Profile? { profiles.first { $0.id == activeProfileID } } /// Die Geräte des gewählten Fahrzeugs. var activeDevices: [ConfiguredDevice] { devices.filter { $0.profileID == activeProfileID } } var hasMultipleProfiles: Bool { profiles.count > 1 } func selectProfile(_ profile: Profile) { guard profile.id != activeProfileID else { return } activeProfileID = profile.id save() } @discardableResult func addProfile(named name: String, symbol: String = "box.truck") -> Profile { let profile = Profile(name: name, symbol: symbol) profiles.append(profile) save() return profile } func update(_ profile: Profile) { guard let index = profiles.firstIndex(where: { $0.id == profile.id }) else { return } profiles[index] = profile save() } /// Entfernt ein Profil samt seiner Geräte und Schlüssel. Das letzte Profil /// bleibt bestehen – ohne Profil hätte die App keinen Ort für Geräte. func removeProfile(_ profile: Profile) { guard profiles.count > 1 else { return } for device in devices where device.profileID == profile.id { KeychainStore.setKey(nil, for: device.id) keyCache[device.id] = nil } devices.removeAll { $0.profileID == profile.id } profiles.removeAll { $0.id == profile.id } if activeProfileID == profile.id, let first = profiles.first { activeProfileID = first.id } save() } func deviceCount(in profile: Profile) -> Int { devices.count { $0.profileID == profile.id } } // MARK: - Geräte /// Legt ein Gerät im gerade gewählten Profil an. func add(_ device: ConfiguredDevice, victronKey: String? = nil) { devices.append(device) if let victronKey { setVictronKey(victronKey, for: device.id) } save() } func update(_ device: ConfiguredDevice) { guard let index = devices.firstIndex(where: { $0.id == device.id }) else { return } devices[index] = device save() } func remove(_ device: ConfiguredDevice) { devices.removeAll { $0.id == device.id } KeychainStore.setKey(nil, for: device.id) keyCache[device.id] = nil save() } /// Nur im aktiven Profil suchen: die Geräte des anderen Fahrzeugs sollen /// weder gelesen noch verbunden werden. func device(withPeripheralID id: UUID) -> ConfiguredDevice? { activeDevices.first { $0.peripheralID == id } } // MARK: - Victron-Schlüssel func victronKeyText(for deviceID: UUID) -> String? { KeychainStore.key(for: deviceID) } func setVictronKey(_ hex: String?, for deviceID: UUID) { let trimmed = hex?.trimmingCharacters(in: .whitespacesAndNewlines) let value = (trimmed?.isEmpty ?? true) ? nil : trimmed KeychainStore.setKey(value, for: deviceID) keyCache[deviceID] = value?.hexBytes } /// Der Schlüssel als Bytes, oder nil wenn keiner hinterlegt bzw. der /// hinterlegte keine 16 Byte lang ist. func victronKeyBytes(for deviceID: UUID) -> [UInt8]? { if let cached = keyCache[deviceID] { return cached } guard let bytes = KeychainStore.key(for: deviceID)?.hexBytes, bytes.count == 16 else { return nil } keyCache[deviceID] = bytes return bytes } // MARK: - Persistenz private func load() { let defaults = UserDefaults.standard if let data = defaults.data(forKey: Self.profilesKey), let decoded = try? JSONDecoder().decode([Profile].self, from: data), !decoded.isEmpty { profiles = decoded } else { // Erster Start oder Update von einer Version ohne Profile. profiles = [.initial] } if let data = defaults.data(forKey: Self.devicesKey), let decoded = try? JSONDecoder().decode([ConfiguredDevice].self, from: data) { devices = decoded } if let raw = defaults.string(forKey: Self.activeProfileKey), let id = UUID(uuidString: raw), profiles.contains(where: { $0.id == id }) { activeProfileID = id } else { activeProfileID = profiles[0].id } // Geräte, deren Profil nicht mehr existiert, wären sonst unerreichbar. let known = Set(profiles.map(\.id)) for index in devices.indices where !known.contains(devices[index].profileID) { devices[index].profileID = profiles[0].id } } private func save() { let defaults = UserDefaults.standard if let data = try? JSONEncoder().encode(devices) { defaults.set(data, forKey: Self.devicesKey) } if let data = try? JSONEncoder().encode(profiles) { defaults.set(data, forKey: Self.profilesKey) } defaults.set(activeProfileID.uuidString, forKey: Self.activeProfileKey) } }