Newer Older
196 lines | 6.834kb
Bogdan Timofte authored 2 weeks ago
1
//
2
//  DataStore.swift
3
//  USB Meter
4
//
5
//  Created by Bogdan Timofte on 03/03/2020.
6
//  Copyright © 2020 Bogdan Timofte. All rights reserved.
7
//
8

            
9
import SwiftUI
10
import Combine
11
import CoreBluetooth
12

            
13
final class AppData : ObservableObject {
Bogdan Timofte authored a week ago
14
    struct MeterSummary: Identifiable {
Bogdan Timofte authored a week ago
15
        let macAddress: String
16
        let displayName: String
17
        let modelSummary: String
18
        let advertisedName: String?
19
        let lastSeen: Date?
20
        let lastConnected: Date?
21
        let meter: Meter?
22

            
23
        var id: String {
24
            macAddress
25
        }
26
    }
27

            
Bogdan Timofte authored 2 weeks ago
28
    private var bluetoothManagerNotification: AnyCancellable?
Bogdan Timofte authored a week ago
29
    private var meterStoreObserver: AnyCancellable?
30
    private var meterStoreCloudObserver: AnyCancellable?
31
    private let meterStore = MeterNameStore.shared
Bogdan Timofte authored 2 weeks ago
32

            
Bogdan Timofte authored 2 weeks ago
33
    init() {
Bogdan Timofte authored 2 weeks ago
34
        bluetoothManagerNotification = bluetoothManager.objectWillChange.sink { [weak self] _ in
Bogdan Timofte authored 2 weeks ago
35
            self?.scheduleObjectWillChange()
Bogdan Timofte authored 2 weeks ago
36
        }
Bogdan Timofte authored a week ago
37
        meterStoreObserver = NotificationCenter.default.publisher(for: .meterNameStoreDidChange)
38
            .receive(on: DispatchQueue.main)
39
            .sink { [weak self] _ in
40
                self?.refreshMeterMetadata()
41
            }
42
        meterStoreCloudObserver = NotificationCenter.default.publisher(for: .meterNameStoreCloudStatusDidChange)
43
            .receive(on: DispatchQueue.main)
44
            .sink { [weak self] _ in
45
                self?.scheduleObjectWillChange()
46
            }
Bogdan Timofte authored 2 weeks ago
47
    }
Bogdan Timofte authored a week ago
48

            
Bogdan Timofte authored 2 weeks ago
49
    let bluetoothManager = BluetoothManager()
Bogdan Timofte authored a week ago
50

            
Bogdan Timofte authored 2 weeks ago
51
    @Published var enableRecordFeature: Bool = true
Bogdan Timofte authored a week ago
52

            
Bogdan Timofte authored 2 weeks ago
53
    @Published var meters: [UUID:Meter] = [UUID:Meter]()
Bogdan Timofte authored a week ago
54

            
55
    var cloudAvailability: MeterNameStore.CloudAvailability {
56
        meterStore.currentCloudAvailability
57
    }
58

            
59
    func meterName(for macAddress: String) -> String? {
60
        meterStore.name(for: macAddress)
61
    }
62

            
63
    func setMeterName(_ name: String, for macAddress: String) {
64
        meterStore.upsert(macAddress: macAddress, name: name, temperatureUnitRawValue: nil)
65
    }
66

            
67
    func temperatureUnitPreference(for macAddress: String) -> TemperatureUnitPreference {
68
        let rawValue = meterStore.temperatureUnitRawValue(for: macAddress) ?? TemperatureUnitPreference.celsius.rawValue
69
        return TemperatureUnitPreference(rawValue: rawValue) ?? .celsius
70
    }
71

            
72
    func setTemperatureUnitPreference(_ preference: TemperatureUnitPreference, for macAddress: String) {
73
        meterStore.upsert(macAddress: macAddress, name: nil, temperatureUnitRawValue: preference.rawValue)
Bogdan Timofte authored 2 weeks ago
74
    }
Bogdan Timofte authored 2 weeks ago
75

            
Bogdan Timofte authored a week ago
76
    func registerMeter(macAddress: String, modelName: String?, advertisedName: String?) {
77
        meterStore.registerMeter(macAddress: macAddress, modelName: modelName, advertisedName: advertisedName)
Bogdan Timofte authored a week ago
78
    }
79

            
80
    func noteMeterSeen(at date: Date, macAddress: String) {
81
        meterStore.noteLastSeen(date, for: macAddress)
82
    }
83

            
84
    func noteMeterConnected(at date: Date, macAddress: String) {
85
        meterStore.noteLastConnected(date, for: macAddress)
86
    }
87

            
88
    func lastSeen(for macAddress: String) -> Date? {
89
        meterStore.lastSeen(for: macAddress)
90
    }
91

            
92
    func lastConnected(for macAddress: String) -> Date? {
93
        meterStore.lastConnected(for: macAddress)
94
    }
95

            
Bogdan Timofte authored a week ago
96
    var meterSummaries: [MeterSummary] {
Bogdan Timofte authored a week ago
97
        let liveMetersByMAC = Dictionary(uniqueKeysWithValues: meters.values.map { ($0.btSerial.macAddress.description, $0) })
98
        let recordsByMAC = Dictionary(uniqueKeysWithValues: meterStore.allRecords().map { ($0.macAddress, $0) })
99
        let macAddresses = Set(recordsByMAC.keys).union(liveMetersByMAC.keys)
100

            
101
        return macAddresses.map { macAddress in
102
            let liveMeter = liveMetersByMAC[macAddress]
103
            let record = recordsByMAC[macAddress]
104

            
Bogdan Timofte authored a week ago
105
            return MeterSummary(
Bogdan Timofte authored a week ago
106
                macAddress: macAddress,
Bogdan Timofte authored a week ago
107
                displayName: Self.friendlyDisplayName(liveMeter: liveMeter, record: record),
Bogdan Timofte authored a week ago
108
                modelSummary: liveMeter?.deviceModelSummary ?? record?.modelName ?? "Meter",
Bogdan Timofte authored a week ago
109
                advertisedName: liveMeter?.modelString ?? record?.advertisedName,
110
                lastSeen: liveMeter?.lastSeen ?? record?.lastSeen,
111
                lastConnected: liveMeter?.lastConnectedAt ?? record?.lastConnected,
112
                meter: liveMeter
113
            )
114
        }
115
        .sorted { lhs, rhs in
Bogdan Timofte authored a week ago
116
            if lhs.meter != nil && rhs.meter == nil {
117
                return true
118
            }
119
            if lhs.meter == nil && rhs.meter != nil {
120
                return false
121
            }
Bogdan Timofte authored a week ago
122
            let byName = lhs.displayName.localizedCaseInsensitiveCompare(rhs.displayName)
123
            if byName != .orderedSame {
124
                return byName == .orderedAscending
125
            }
126
            return lhs.macAddress < rhs.macAddress
127
        }
128
    }
129

            
Bogdan Timofte authored 2 weeks ago
130
    private func scheduleObjectWillChange() {
131
        DispatchQueue.main.async { [weak self] in
132
            self?.objectWillChange.send()
133
        }
134
    }
Bogdan Timofte authored a week ago
135

            
136
    private func refreshMeterMetadata() {
137
        DispatchQueue.main.async { [weak self] in
138
            guard let self else { return }
139
            var didUpdateAnyMeter = false
140
            for meter in self.meters.values {
141
                let mac = meter.btSerial.macAddress.description
142
                let displayName = self.meterName(for: mac) ?? mac
143
                if meter.name != displayName {
144
                    meter.updateNameFromStore(displayName)
145
                    didUpdateAnyMeter = true
146
                }
147

            
148
                let previousTemperaturePreference = meter.tc66TemperatureUnitPreference
149
                meter.reloadTemperatureUnitPreference()
150
                if meter.tc66TemperatureUnitPreference != previousTemperaturePreference {
151
                    didUpdateAnyMeter = true
152
                }
153
            }
154

            
155
            if didUpdateAnyMeter {
156
                self.scheduleObjectWillChange()
157
            }
158
        }
159
    }
Bogdan Timofte authored 2 weeks ago
160
}
Bogdan Timofte authored a week ago
161

            
162
extension AppData.MeterSummary {
163
    var tint: Color {
164
        switch modelSummary {
165
        case "UM25C":
166
            return .blue
167
        case "UM34C":
168
            return .yellow
169
        case "TC66C":
170
            return Model.TC66C.color
171
        default:
172
            return .secondary
173
        }
174
    }
175
}
Bogdan Timofte authored a week ago
176

            
177
private extension AppData {
178
    static func friendlyDisplayName(liveMeter: Meter?, record: MeterNameStore.Record?) -> String {
179
        if let liveName = liveMeter?.name.trimmingCharacters(in: .whitespacesAndNewlines), !liveName.isEmpty {
180
            return liveName
181
        }
182
        if let customName = record?.customName {
183
            return customName
184
        }
185
        if let advertisedName = record?.advertisedName {
186
            return advertisedName
187
        }
188
        if let recordModel = record?.modelName {
189
            return recordModel
190
        }
191
        if let liveModel = liveMeter?.deviceModelSummary {
192
            return liveModel
193
        }
194
        return "Meter"
195
    }
196
}