1 contributor
//
// 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"
}
}