// // DataStore.swift // USB Meter // // Created by Bogdan Timofte on 03/03/2020. // Copyright © 2020 Bogdan Timofte. All rights reserved. // import SwiftUI import Combine import CoreBluetooth final class AppData : ObservableObject { struct MeterSummary: Identifiable { let macAddress: String let displayName: String let modelSummary: String let advertisedName: String? let lastSeen: Date? let lastConnected: Date? let meter: Meter? var id: String { macAddress } } private var bluetoothManagerNotification: AnyCancellable? private var meterStoreObserver: AnyCancellable? private var meterStoreCloudObserver: AnyCancellable? private let meterStore = MeterNameStore.shared init() { bluetoothManagerNotification = bluetoothManager.objectWillChange.sink { [weak self] _ in self?.scheduleObjectWillChange() } meterStoreObserver = NotificationCenter.default.publisher(for: .meterNameStoreDidChange) .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.refreshMeterMetadata() } meterStoreCloudObserver = NotificationCenter.default.publisher(for: .meterNameStoreCloudStatusDidChange) .receive(on: DispatchQueue.main) .sink { [weak self] _ in self?.scheduleObjectWillChange() } } let bluetoothManager = BluetoothManager() @Published var enableRecordFeature: Bool = true @Published var meters: [UUID:Meter] = [UUID:Meter]() var cloudAvailability: MeterNameStore.CloudAvailability { meterStore.currentCloudAvailability } func meterName(for macAddress: String) -> String? { meterStore.name(for: macAddress) } func setMeterName(_ name: String, for macAddress: String) { meterStore.upsert(macAddress: macAddress, name: name, temperatureUnitRawValue: nil) } func temperatureUnitPreference(for macAddress: String) -> TemperatureUnitPreference { let rawValue = meterStore.temperatureUnitRawValue(for: macAddress) ?? TemperatureUnitPreference.celsius.rawValue return TemperatureUnitPreference(rawValue: rawValue) ?? .celsius } func setTemperatureUnitPreference(_ preference: TemperatureUnitPreference, for macAddress: String) { meterStore.upsert(macAddress: macAddress, name: nil, temperatureUnitRawValue: preference.rawValue) } func registerMeter(macAddress: String, modelName: String?, advertisedName: String?) { meterStore.registerMeter(macAddress: macAddress, modelName: modelName, advertisedName: advertisedName) } func noteMeterSeen(at date: Date, macAddress: String) { meterStore.noteLastSeen(date, for: macAddress) } func noteMeterConnected(at date: Date, macAddress: String) { meterStore.noteLastConnected(date, for: macAddress) } func lastSeen(for macAddress: String) -> Date? { meterStore.lastSeen(for: macAddress) } func lastConnected(for macAddress: String) -> Date? { meterStore.lastConnected(for: macAddress) } var meterSummaries: [MeterSummary] { let liveMetersByMAC = Dictionary(uniqueKeysWithValues: meters.values.map { ($0.btSerial.macAddress.description, $0) }) let recordsByMAC = Dictionary(uniqueKeysWithValues: meterStore.allRecords().map { ($0.macAddress, $0) }) let macAddresses = Set(recordsByMAC.keys).union(liveMetersByMAC.keys) return macAddresses.map { macAddress in let liveMeter = liveMetersByMAC[macAddress] let record = recordsByMAC[macAddress] return MeterSummary( macAddress: macAddress, displayName: Self.friendlyDisplayName(liveMeter: liveMeter, record: record), modelSummary: liveMeter?.deviceModelSummary ?? record?.modelName ?? "Meter", advertisedName: liveMeter?.modelString ?? record?.advertisedName, lastSeen: liveMeter?.lastSeen ?? record?.lastSeen, lastConnected: liveMeter?.lastConnectedAt ?? record?.lastConnected, meter: liveMeter ) } .sorted { lhs, rhs in if lhs.meter != nil && rhs.meter == nil { return true } if lhs.meter == nil && rhs.meter != nil { return false } let byName = lhs.displayName.localizedCaseInsensitiveCompare(rhs.displayName) if byName != .orderedSame { return byName == .orderedAscending } return lhs.macAddress < rhs.macAddress } } private func scheduleObjectWillChange() { DispatchQueue.main.async { [weak self] in self?.objectWillChange.send() } } private func refreshMeterMetadata() { DispatchQueue.main.async { [weak self] in guard let self else { return } var didUpdateAnyMeter = false for meter in self.meters.values { let mac = meter.btSerial.macAddress.description let displayName = self.meterName(for: mac) ?? mac if meter.name != displayName { meter.updateNameFromStore(displayName) didUpdateAnyMeter = true } let previousTemperaturePreference = meter.tc66TemperatureUnitPreference meter.reloadTemperatureUnitPreference() if meter.tc66TemperatureUnitPreference != previousTemperaturePreference { didUpdateAnyMeter = true } } if didUpdateAnyMeter { self.scheduleObjectWillChange() } } } } extension AppData.MeterSummary { var tint: Color { switch modelSummary { case "UM25C": return .blue case "UM34C": return .yellow case "TC66C": return Model.TC66C.color default: return .secondary } } } private extension AppData { static func friendlyDisplayName(liveMeter: Meter?, record: MeterNameStore.Record?) -> String { if let liveName = liveMeter?.name.trimmingCharacters(in: .whitespacesAndNewlines), !liveName.isEmpty { return liveName } if let customName = record?.customName { return customName } if let advertisedName = record?.advertisedName { return advertisedName } if let recordModel = record?.modelName { return recordModel } if let liveModel = liveMeter?.deviceModelSummary { return liveModel } return "Meter" } }