USB-Meter / USB Meter / Model / ChargeInsightsModel.swift
Newer Older
1381 lines | 42.085kb
Bogdan Timofte authored a month ago
1
//
2
//  ChargeInsightsModel.swift
3
//  USB Meter
4
//
5
//  Created by Codex on 10/04/2026.
6
//
7

            
8
import Foundation
9

            
Bogdan Timofte authored a month ago
10
enum ChargedDeviceKind: String, Identifiable, Codable {
Bogdan Timofte authored a month ago
11
    case device
12
    case charger
13

            
14
    var id: String { rawValue }
15

            
16
    var title: String {
17
        switch self {
18
        case .device:
19
            return "Device"
20
        case .charger:
21
            return "Charger"
22
        }
23
    }
24

            
25
    var pluralTitle: String {
26
        switch self {
27
        case .device:
28
            return "Devices"
29
        case .charger:
30
            return "Chargers"
31
        }
32
    }
33

            
34
    var symbolName: String {
35
        switch self {
36
        case .device:
37
            return "iphone"
38
        case .charger:
39
            return "bolt.horizontal.circle"
40
        }
41
    }
42
}
43

            
Bogdan Timofte authored a month ago
44
enum ChargedDeviceClass: String, CaseIterable, Identifiable, Codable {
Bogdan Timofte authored a month ago
45
    case iphone
46
    case watch
47
    case powerbank
48
    case charger
49
    case other
50

            
51
    var id: String { rawValue }
52

            
Bogdan Timofte authored a month ago
53
    static var deviceCases: [ChargedDeviceClass] {
54
        allCases.filter { $0 != .charger }
55
    }
56

            
57
    var kind: ChargedDeviceKind {
58
        self == .charger ? .charger : .device
59
    }
60

            
Bogdan Timofte authored a month ago
61
    var title: String {
62
        switch self {
63
        case .iphone:
64
            return "iPhone"
65
        case .watch:
66
            return "Watch"
67
        case .powerbank:
68
            return "Powerbank"
69
        case .charger:
70
            return "Charger"
71
        case .other:
72
            return "Other"
73
        }
74
    }
75

            
76
    var symbolName: String {
77
        switch self {
78
        case .iphone:
79
            return "iphone"
80
        case .watch:
81
            return "applewatch"
82
        case .powerbank:
83
            return "battery.100.bolt"
84
        case .charger:
85
            return "bolt.badge.clock"
86
        case .other:
87
            return "shippingbox"
88
        }
89
    }
Bogdan Timofte authored a month ago
90

            
91
    var enforcedChargingSupport: (wired: Bool, wireless: Bool)? {
92
        switch self {
93
        case .watch:
94
            return (wired: false, wireless: true)
95
        case .powerbank:
96
            return (wired: true, wireless: false)
97
        case .charger:
98
            return (wired: false, wireless: true)
99
        case .iphone, .other:
100
            return nil
101
        }
102
    }
103

            
104
    var enforcedChargingStateAvailability: ChargingStateAvailability? {
105
        switch self {
106
        case .watch:
107
            return .onOnly
108
        case .powerbank:
109
            return .offOnly
110
        case .charger:
111
            return .onOnly
112
        case .iphone, .other:
113
            return nil
114
        }
115
    }
116

            
117
    func normalizedChargingSupport(
118
        supportsWiredCharging: Bool,
119
        supportsWirelessCharging: Bool
120
    ) -> (wired: Bool, wireless: Bool) {
121
        enforcedChargingSupport ?? (wired: supportsWiredCharging, wireless: supportsWirelessCharging)
122
    }
123

            
124
    func normalizedChargingStateAvailability(
125
        _ chargingStateAvailability: ChargingStateAvailability
126
    ) -> ChargingStateAvailability {
127
        enforcedChargingStateAvailability ?? chargingStateAvailability
128
    }
Bogdan Timofte authored a month ago
129
}
130

            
131
enum ChargeSessionStatus: String {
132
    case active
Bogdan Timofte authored a month ago
133
    case paused
Bogdan Timofte authored a month ago
134
    case completed
135
    case abandoned
136

            
137
    var title: String {
Bogdan Timofte authored a month ago
138
        switch self {
139
        case .active:
140
            return "Active"
141
        case .paused:
142
            return "Paused"
143
        case .completed:
144
            return "Completed"
145
        case .abandoned:
146
            return "Abandoned"
147
        }
148
    }
149

            
150
    var isOpen: Bool {
151
        switch self {
152
        case .active, .paused:
153
            return true
154
        case .completed, .abandoned:
155
            return false
156
        }
Bogdan Timofte authored a month ago
157
    }
158
}
159

            
160
enum ChargeSessionSourceMode: String {
161
    case live
162
    case offline
163
    case blended
164

            
165
    var title: String {
166
        switch self {
167
        case .live:
168
            return "Live"
169
        case .offline:
170
            return "Offline Counters"
171
        case .blended:
172
            return "Blended"
173
        }
174
    }
175
}
176

            
Bogdan Timofte authored a month ago
177
enum ChargingTransportMode: String, CaseIterable, Identifiable, Codable {
Bogdan Timofte authored a month ago
178
    case wired
179
    case wireless
180

            
181
    var id: String { rawValue }
182

            
183
    var title: String {
184
        switch self {
185
        case .wired:
186
            return "Wired"
187
        case .wireless:
188
            return "Wireless"
189
        }
190
    }
191

            
192
    var symbolName: String {
193
        switch self {
194
        case .wired:
195
            return "cable.connector"
196
        case .wireless:
197
            return "dot.radiowaves.left.and.right"
198
        }
199
    }
200
}
201

            
Bogdan Timofte authored a month ago
202
enum ChargingStateMode: String, CaseIterable, Identifiable, Codable {
203
    case on
204
    case off
205

            
206
    var id: String { rawValue }
207

            
208
    var title: String {
209
        switch self {
210
        case .on:
211
            return "On"
212
        case .off:
213
            return "Off"
214
        }
215
    }
216

            
217
    var description: String {
218
        switch self {
219
        case .on:
220
            return "Device stays powered on while charging."
221
        case .off:
222
            return "Device is powered off while charging."
223
        }
224
    }
225
}
226

            
227
enum ChargingStateAvailability: String, CaseIterable, Identifiable, Codable {
228
    case onOnly
229
    case onOrOff
230
    case offOnly
231

            
232
    var id: String { rawValue }
233

            
234
    var title: String {
235
        switch self {
236
        case .onOnly:
237
            return "On Only"
238
        case .onOrOff:
239
            return "On or Off"
240
        case .offOnly:
241
            return "Off Only"
242
        }
243
    }
244

            
245
    var description: String {
246
        switch self {
247
        case .onOnly:
248
            return "The device can be recorded only while it is powered on."
249
        case .onOrOff:
250
            return "The session must specify whether the device is on or off."
251
        case .offOnly:
252
            return "The device can be recorded only while it is powered off."
253
        }
254
    }
255

            
256
    var supportedModes: [ChargingStateMode] {
257
        switch self {
258
        case .onOnly:
259
            return [.on]
260
        case .onOrOff:
261
            return [.on, .off]
262
        case .offOnly:
263
            return [.off]
264
        }
265
    }
266

            
267
    var supportsMultipleModes: Bool {
268
        supportedModes.count > 1
269
    }
270

            
271
    var supportsChargingWhileOff: Bool {
272
        self != .onOnly
273
    }
274

            
275
    static func fallback(for supportsChargingWhileOff: Bool) -> ChargingStateAvailability {
276
        supportsChargingWhileOff ? .onOrOff : .onOnly
277
    }
278
}
279

            
280
enum ChargeSessionKind: String, CaseIterable, Identifiable, Codable, Hashable {
281
    case wiredOn
282
    case wiredOff
283
    case wirelessOn
284
    case wirelessOff
285

            
286
    var id: String { rawValue }
287

            
288
    init(chargingTransportMode: ChargingTransportMode, chargingStateMode: ChargingStateMode) {
289
        switch (chargingTransportMode, chargingStateMode) {
290
        case (.wired, .on):
291
            self = .wiredOn
292
        case (.wired, .off):
293
            self = .wiredOff
294
        case (.wireless, .on):
295
            self = .wirelessOn
296
        case (.wireless, .off):
297
            self = .wirelessOff
298
        }
299
    }
300

            
301
    var chargingTransportMode: ChargingTransportMode {
302
        switch self {
303
        case .wiredOn, .wiredOff:
304
            return .wired
305
        case .wirelessOn, .wirelessOff:
306
            return .wireless
307
        }
308
    }
309

            
310
    var chargingStateMode: ChargingStateMode {
311
        switch self {
312
        case .wiredOn, .wirelessOn:
313
            return .on
314
        case .wiredOff, .wirelessOff:
315
            return .off
316
        }
317
    }
318

            
319
    var title: String {
320
        "\(chargingTransportMode.title) • \(chargingStateMode.title)"
321
    }
322

            
323
    var shortTitle: String {
324
        "\(chargingTransportMode.title) \(chargingStateMode.title)"
325
    }
326
}
327

            
Bogdan Timofte authored a month ago
328
enum WirelessChargingProfile: String, CaseIterable, Identifiable, Codable {
Bogdan Timofte authored a month ago
329
    case magsafe
330
    case genericQi
331

            
332
    var id: String { rawValue }
333

            
334
    var title: String {
335
        switch self {
336
        case .magsafe:
337
            return "MagSafe"
338
        case .genericQi:
339
            return "Generic Qi"
340
        }
341
    }
342

            
343
    var description: String {
344
        switch self {
345
        case .magsafe:
346
            return "Use separate wireless-efficiency calibration from devices that also have reliable wired capacity."
347
        case .genericQi:
348
            return "Use only automatic efficiency estimates and show a low-efficiency warning when needed."
349
        }
350
    }
351
}
352

            
Bogdan Timofte authored a month ago
353
enum ChargerType: String, CaseIterable, Identifiable, Codable {
354
    case appleMagSafe
355
    case appleWatch
356
    case genericMagSafe
357
    case genericQi
358

            
359
    var id: String { rawValue }
360

            
361
    var title: String {
362
        switch self {
363
        case .appleMagSafe: return "Apple MagSafe Charger"
364
        case .appleWatch: return "Apple Watch Charger"
365
        case .genericMagSafe: return "Generic MagSafe"
366
        case .genericQi: return "Generic Qi"
367
        }
368
    }
369

            
370
    var symbolName: String {
371
        switch self {
372
        case .appleMagSafe: return "magsafe.batterypack"
373
        case .appleWatch: return "applewatch.radiowaves.left.and.right"
374
        case .genericMagSafe: return "bolt.circle"
375
        case .genericQi: return "bolt.horizontal.circle"
376
        }
377
    }
378

            
379
    /// Whether this charger type uses magnetic alignment, enabling more accurate efficiency calibration.
380
    var supportsAlignment: Bool {
381
        switch self {
382
        case .appleMagSafe, .appleWatch, .genericMagSafe: return true
383
        case .genericQi: return false
384
        }
385
    }
386

            
387
    var wirelessChargingProfile: WirelessChargingProfile {
388
        supportsAlignment ? .magsafe : .genericQi
389
    }
390
}
391

            
Bogdan Timofte authored a month ago
392
enum ChargedDeviceTemplateIconSource: String, Codable {
393
    case systemSymbol
394
    case asset
395
}
396

            
397
struct ChargedDeviceTemplateIcon: Hashable, Codable {
398
    let type: ChargedDeviceTemplateIconSource
399
    let name: String
400
    let fallbackSystemName: String?
401

            
402
    static func systemSymbol(
403
        _ name: String,
404
        fallbackSystemName: String? = nil
405
    ) -> ChargedDeviceTemplateIcon {
406
        ChargedDeviceTemplateIcon(
407
            type: .systemSymbol,
408
            name: name,
409
            fallbackSystemName: fallbackSystemName
410
        )
411
    }
412

            
413
    func resolvedSystemSymbolName(fallbackSystemName: String) -> String {
414
        switch type {
415
        case .systemSymbol:
416
            return name
417
        case .asset:
418
            return self.fallbackSystemName ?? fallbackSystemName
419
        }
420
    }
421
}
422

            
423
struct ChargedDeviceTemplateDefinition: Identifiable, Hashable, Codable {
424
    let id: String
425
    let name: String
426
    let group: String
427
    let kind: ChargedDeviceKind
428
    let deviceClass: ChargedDeviceClass
429
    let icon: ChargedDeviceTemplateIcon
430
    let chargingStateAvailability: ChargingStateAvailability
431
    let supportsWiredCharging: Bool
432
    let supportsWirelessCharging: Bool
433
    let wirelessChargingProfile: WirelessChargingProfile
434
    let sortOrder: Int
435

            
436
    var chargingSupportSummary: String {
437
        switch (supportsWiredCharging, supportsWirelessCharging) {
438
        case (true, true):
439
            return "Wired + Wireless"
440
        case (true, false):
441
            return "Wired only"
442
        case (false, true):
443
            return "Wireless only"
444
        case (false, false):
445
            return "No charging transport"
446
        }
447
    }
448

            
449
    var capabilitySummary: String {
Bogdan Timofte authored a month ago
450
        if kind == .charger {
451
            return wirelessChargingProfile.title
452
        }
Bogdan Timofte authored a month ago
453
        var components = [chargingStateAvailability.title, chargingSupportSummary]
454
        if supportsWirelessCharging {
455
            components.append(wirelessChargingProfile.title)
456
        }
457
        return components.joined(separator: " • ")
458
    }
459
}
460

            
461
private struct ChargedDeviceTemplateDocument: Codable {
462
    let templates: [ChargedDeviceTemplateDefinition]
463
}
464

            
465
struct ChargedDeviceTemplateCatalog {
466
    static let shared = ChargedDeviceTemplateCatalog()
467

            
468
    let templates: [ChargedDeviceTemplateDefinition]
469
    private let templatesByID: [String: ChargedDeviceTemplateDefinition]
470

            
471
    private init(bundle: Bundle = .main) {
472
        let loadedTemplates: [ChargedDeviceTemplateDefinition]
473

            
474
        if let resourceURL = bundle.url(forResource: "ChargedDeviceTemplates", withExtension: "json"),
475
           let data = try? Data(contentsOf: resourceURL),
476
           let document = try? JSONDecoder().decode(ChargedDeviceTemplateDocument.self, from: data) {
477
            loadedTemplates = document.templates
478
        } else {
479
            loadedTemplates = []
480
        }
481

            
482
        self.templates = loadedTemplates.sorted { lhs, rhs in
483
            if lhs.group != rhs.group {
484
                return lhs.group < rhs.group
485
            }
486
            if lhs.sortOrder != rhs.sortOrder {
487
                return lhs.sortOrder < rhs.sortOrder
488
            }
489
            return lhs.name < rhs.name
490
        }
491
        self.templatesByID = Dictionary(uniqueKeysWithValues: self.templates.map { ($0.id, $0) })
492
    }
493

            
494
    func template(id: String?) -> ChargedDeviceTemplateDefinition? {
495
        guard let id else {
496
            return nil
497
        }
498
        return templatesByID[id]
499
    }
500

            
501
    func templates(for kind: ChargedDeviceKind) -> [ChargedDeviceTemplateDefinition] {
502
        templates.filter { $0.kind == kind }
503
    }
504
}
505

            
Bogdan Timofte authored a month ago
506
struct ChargeCheckpointSummary: Identifiable, Hashable {
507
    let id: UUID
508
    let sessionID: UUID
509
    let chargedDeviceID: UUID
510
    let timestamp: Date
511
    let batteryPercent: Double
512
    let measuredEnergyWh: Double
513
    let measuredChargeAh: Double
514
    let currentAmps: Double
515
    let voltageVolts: Double?
516
    let label: String?
Bogdan Timofte authored a month ago
517

            
518
    var flag: ChargeCheckpointFlag {
519
        ChargeCheckpointFlag.fromStoredLabel(label)
520
    }
521
}
522

            
523
enum ChargeCheckpointFlag: String, CaseIterable {
524
    case initial
525
    case intermediate
526
    case final
527

            
528
    var title: String {
529
        switch self {
530
        case .initial:
531
            return "Initial"
532
        case .intermediate:
533
            return "Intermediate"
534
        case .final:
535
            return "Final"
536
        }
537
    }
538

            
539
    var anchorDescription: String {
540
        switch self {
541
        case .initial:
542
            return "initial checkpoint"
543
        case .intermediate:
544
            return "intermediate checkpoint"
545
        case .final:
546
            return "final checkpoint"
547
        }
548
    }
549

            
550
    static func fromStoredLabel(_ label: String?) -> ChargeCheckpointFlag {
551
        let normalized = label?
552
            .trimmingCharacters(in: .whitespacesAndNewlines)
553
            .lowercased()
554

            
555
        switch normalized {
556
        case "initial", "start":
557
            return .initial
558
        case "final", "end":
559
            return .final
560
        case "intermediate", nil, "":
561
            return .intermediate
562
        default:
563
            return .intermediate
564
        }
565
    }
Bogdan Timofte authored a month ago
566
}
567

            
568
struct ChargeSessionSampleSummary: Identifiable, Hashable {
569
    let sessionID: UUID
570
    let chargedDeviceID: UUID
571
    let bucketIndex: Int
572
    let timestamp: Date
573
    let averageCurrentAmps: Double
574
    let averageVoltageVolts: Double?
575
    let averagePowerWatts: Double
576
    let measuredEnergyWh: Double
577
    let measuredChargeAh: Double
578
    let sampleCount: Int
579

            
580
    var id: String {
581
        "\(sessionID.uuidString)-\(bucketIndex)"
582
    }
583
}
584

            
585
struct ChargeSessionSummary: Identifiable, Hashable {
586
    let id: UUID
587
    let chargedDeviceID: UUID
588
    let chargerID: UUID?
589
    let meterMACAddress: String?
590
    let meterName: String?
591
    let meterModel: String?
592
    let startedAt: Date
593
    let endedAt: Date?
594
    let lastObservedAt: Date
Bogdan Timofte authored a month ago
595
    let pausedAt: Date?
Bogdan Timofte authored a month ago
596
    let status: ChargeSessionStatus
597
    let sourceMode: ChargeSessionSourceMode
598
    let chargingTransportMode: ChargingTransportMode
Bogdan Timofte authored a month ago
599
    let chargingStateMode: ChargingStateMode
600
    let autoStopEnabled: Bool
Bogdan Timofte authored a month ago
601
    let measuredEnergyWh: Double
602
    let effectiveBatteryEnergyWh: Double?
603
    let measuredChargeAh: Double
Bogdan Timofte authored a month ago
604
    let meterEnergyBaselineWh: Double?
605
    let meterChargeBaselineAh: Double?
Bogdan Timofte authored a month ago
606
    let meterDurationBaselineSeconds: Double?
607
    let meterLastDurationSeconds: Double?
Bogdan Timofte authored a month ago
608
    let minimumObservedCurrentAmps: Double?
609
    let maximumObservedCurrentAmps: Double?
610
    let maximumObservedPowerWatts: Double?
611
    let maximumObservedVoltageVolts: Double?
612
    let selectedSourceVoltageVolts: Double?
613
    let completionCurrentAmps: Double?
614
    let stopThresholdAmps: Double
615
    let startBatteryPercent: Double?
616
    let endBatteryPercent: Double?
617
    let capacityEstimateWh: Double?
618
    let wirelessEfficiencyFactor: Double?
619
    let usesEstimatedWirelessEfficiency: Bool
620
    let shouldWarnAboutLowWirelessEfficiency: Bool
621
    let supportsChargingWhileOff: Bool
622
    let usedOfflineMeterCounters: Bool
623
    let targetBatteryPercent: Double?
624
    let targetBatteryAlertTriggeredAt: Date?
625
    let requiresCompletionConfirmation: Bool
626
    let completionConfirmationRequestedAt: Date?
627
    let completionContradictionPercent: Double?
628
    let selectedDataGroup: UInt8?
629
    let checkpoints: [ChargeCheckpointSummary]
630
    let aggregatedSamples: [ChargeSessionSampleSummary]
631

            
Bogdan Timofte authored a month ago
632
    var sessionKind: ChargeSessionKind {
633
        ChargeSessionKind(
634
            chargingTransportMode: chargingTransportMode,
635
            chargingStateMode: chargingStateMode
636
        )
637
    }
638

            
Bogdan Timofte authored a month ago
639
    var duration: TimeInterval {
640
        (endedAt ?? lastObservedAt).timeIntervalSince(startedAt)
641
    }
642

            
Bogdan Timofte authored a month ago
643
    var meterObservedDuration: TimeInterval? {
644
        guard let meterDurationBaselineSeconds, let meterLastDurationSeconds else {
645
            return nil
646
        }
647
        guard meterLastDurationSeconds >= meterDurationBaselineSeconds else {
648
            return nil
649
        }
650
        return meterLastDurationSeconds - meterDurationBaselineSeconds
651
    }
652

            
653
    var effectiveDuration: TimeInterval {
654
        meterObservedDuration ?? duration
655
    }
656

            
Bogdan Timofte authored a month ago
657
    var effectiveOrMeasuredEnergyWh: Double {
658
        effectiveBatteryEnergyWh ?? measuredEnergyWh
659
    }
660

            
661
    var batteryDeltaPercent: Double? {
Bogdan Timofte authored a month ago
662
        guard let startBatteryPercent, let endBatteryPercent,
663
              startBatteryPercent >= 0, endBatteryPercent >= 0 else { return nil }
Bogdan Timofte authored a month ago
664
        return endBatteryPercent - startBatteryPercent
665
    }
Bogdan Timofte authored a month ago
666

            
667
    var canAutoStop: Bool {
668
        autoStopEnabled && stopThresholdAmps > 0
669
    }
670

            
671
    var isPaused: Bool {
672
        status == .paused
673
    }
674

            
675
    var isOpen: Bool {
676
        status.isOpen
677
    }
Bogdan Timofte authored a month ago
678
}
679

            
680
struct BatteryLevelPrediction: Hashable {
681
    let predictedPercent: Double
682
    let estimatedCapacityWh: Double
683
    let anchorPercent: Double
684
    let anchorEnergyWh: Double
685
    let anchorDescription: String
686
}
687

            
Bogdan Timofte authored a month ago
688
enum BatteryLevelPredictionTuning {
689
    static let checkpointSettleDuration: TimeInterval = 10 * 60
690

            
691
    static func predictedPercent(
692
        anchorPercent: Double,
693
        anchorEnergyWh: Double,
694
        anchorTimestamp: Date,
695
        anchorIsCheckpoint: Bool,
696
        effectiveEnergyWh: Double,
697
        referenceTimestamp: Date,
698
        estimatedCapacityWh: Double
699
    ) -> Double {
700
        let energyDeltaWh = max(effectiveEnergyWh - anchorEnergyWh, 0)
701
        let rawGainPercent = (energyDeltaWh / estimatedCapacityWh) * 100
702
        let stabilizedGainPercent: Double
703

            
704
        if anchorIsCheckpoint {
705
            let elapsedSinceAnchor = max(referenceTimestamp.timeIntervalSince(anchorTimestamp), 0)
706
            let settleProgress = min(max(elapsedSinceAnchor / checkpointSettleDuration, 0), 1)
707
            stabilizedGainPercent = rawGainPercent * settleProgress
708
        } else {
709
            stabilizedGainPercent = rawGainPercent
710
        }
711

            
712
        return min(
713
            100,
714
            max(
715
                0,
716
                anchorPercent + stabilizedGainPercent
717
            )
718
        )
719
    }
720
}
721

            
Bogdan Timofte authored a month ago
722
struct CapacityTrendPoint: Identifiable, Hashable {
723
    let sessionID: UUID
724
    let timestamp: Date
725
    let capacityWh: Double
726
    let chargingTransportMode: ChargingTransportMode
727

            
728
    var id: UUID { sessionID }
729
}
730

            
731
struct TypicalChargeCurvePoint: Identifiable, Hashable {
732
    let percentBin: Int
733
    let averageEnergyWh: Double
734
    let averageChargeAh: Double
735
    let sampleCount: Int
736

            
737
    var id: Int { percentBin }
738
}
739

            
Bogdan Timofte authored a month ago
740
struct ChargerStandbyPowerSample: Identifiable, Hashable, Codable {
741
    let timestamp: Date
742
    let powerWatts: Double
743
    let currentAmps: Double
744
    let voltageVolts: Double
745

            
746
    var id: TimeInterval {
747
        timestamp.timeIntervalSince1970
748
    }
749
}
750

            
751
struct ChargerStandbyPowerDistributionBin: Identifiable, Hashable {
752
    let index: Int
753
    let lowerBoundWatts: Double
754
    let upperBoundWatts: Double
755
    let count: Int
756
    let relativeFrequency: Double
757

            
758
    var id: Int { index }
759
}
760

            
761
struct ChargerStandbyPowerMeasurementStatistics: Hashable {
762
    let sampleCount: Int
763
    let observedDuration: TimeInterval
764
    let averagePowerWatts: Double
765
    let recentAveragePowerWatts: Double
766
    let medianPowerWatts: Double
767
    let minimumPowerWatts: Double
768
    let maximumPowerWatts: Double
769
    let standardDeviationPowerWatts: Double
770
    let coefficientOfVariation: Double
771
    let averageCurrentAmps: Double
772
    let averageVoltageVolts: Double
773
    let stabilityDeltaWatts: Double
774
    let stabilityToleranceWatts: Double
775
    let histogram: [ChargerStandbyPowerDistributionBin]
776

            
777
    var projectedDailyEnergyWh: Double {
778
        averagePowerWatts * 24
779
    }
780

            
781
    var projectedWeeklyEnergyWh: Double {
782
        averagePowerWatts * 24 * 7
783
    }
784

            
785
    var projectedMonthlyEnergyWh: Double {
786
        averagePowerWatts * 24 * 30
787
    }
788

            
789
    var projectedYearlyEnergyWh: Double {
790
        averagePowerWatts * 24 * 365
791
    }
792

            
793
    var stabilityDeltaMilliwatts: Double {
794
        stabilityDeltaWatts * 1000
795
    }
796

            
797
    var isStable: Bool {
798
        sampleCount >= ChargerStandbyPowerMeasurementAnalyzer.minimumStableSampleCount
799
        && stabilityDeltaWatts <= stabilityToleranceWatts
800
    }
801
}
802

            
803
struct ChargerStandbyPowerMeasurementSummary: Identifiable, Hashable, Codable {
804
    let id: UUID
805
    let chargerID: UUID
806
    let meterMACAddress: String
807
    let meterName: String?
808
    let meterModel: String?
809
    let startedAt: Date
810
    let endedAt: Date
811
    let sampleCount: Int
812
    let stabilizedAt: Date?
813
    let averagePowerWatts: Double
814
    let recentAveragePowerWatts: Double
815
    let medianPowerWatts: Double
816
    let minimumPowerWatts: Double
817
    let maximumPowerWatts: Double
818
    let standardDeviationPowerWatts: Double
819
    let coefficientOfVariation: Double
820
    let averageCurrentAmps: Double
821
    let averageVoltageVolts: Double
822
    let stabilityDeltaWatts: Double
823
    let stabilityToleranceWatts: Double
824
    let powerSamplesWatts: [Double]
825

            
826
    var duration: TimeInterval {
827
        endedAt.timeIntervalSince(startedAt)
828
    }
829

            
830
    var projectedDailyEnergyWh: Double {
831
        averagePowerWatts * 24
832
    }
833

            
834
    var projectedWeeklyEnergyWh: Double {
835
        averagePowerWatts * 24 * 7
836
    }
837

            
838
    var projectedMonthlyEnergyWh: Double {
839
        averagePowerWatts * 24 * 30
840
    }
841

            
842
    var projectedYearlyEnergyWh: Double {
843
        averagePowerWatts * 24 * 365
844
    }
845

            
846
    var isStable: Bool {
847
        stabilizedAt != nil
848
    }
849

            
850
    var histogram: [ChargerStandbyPowerDistributionBin] {
851
        ChargerStandbyPowerMeasurementAnalyzer.histogram(for: powerSamplesWatts)
852
    }
853
}
854

            
855
enum ChargerStandbyPowerMeasurementAnalyzer {
856
    static let minimumStableSampleCount = 45
857
    static let recentSampleWindow = 20
858
    static let minimumStabilityToleranceWatts = 0.003
859
    static let relativeStabilityTolerance = 0.01
860

            
861
    static func statistics(
862
        from samples: [ChargerStandbyPowerSample],
863
        startedAt: Date,
864
        referenceDate: Date = Date()
865
    ) -> ChargerStandbyPowerMeasurementStatistics? {
866
        guard !samples.isEmpty else {
867
            return nil
868
        }
869

            
870
        let powerValues = samples.map(\.powerWatts).filter(\.isFinite)
871
        let currentValues = samples.map(\.currentAmps).filter(\.isFinite)
872
        let voltageValues = samples.map(\.voltageVolts).filter(\.isFinite)
873

            
874
        guard powerValues.isEmpty == false else {
875
            return nil
876
        }
877

            
878
        let averagePower = mean(powerValues)
879
        let recentWindow = min(recentSampleWindow, max(1, powerValues.count))
880
        let recentAveragePower = mean(Array(powerValues.suffix(recentWindow)))
881
        let stabilityDelta = abs(averagePower - recentAveragePower)
882
        let stabilityTolerance = max(
883
            minimumStabilityToleranceWatts,
884
            abs(averagePower) * relativeStabilityTolerance
885
        )
886

            
887
        return ChargerStandbyPowerMeasurementStatistics(
888
            sampleCount: powerValues.count,
889
            observedDuration: max(referenceDate.timeIntervalSince(startedAt), 0),
890
            averagePowerWatts: averagePower,
891
            recentAveragePowerWatts: recentAveragePower,
892
            medianPowerWatts: median(powerValues),
893
            minimumPowerWatts: powerValues.min() ?? 0,
894
            maximumPowerWatts: powerValues.max() ?? 0,
895
            standardDeviationPowerWatts: standardDeviation(powerValues, mean: averagePower),
896
            coefficientOfVariation: coefficientOfVariation(powerValues, mean: averagePower),
897
            averageCurrentAmps: mean(currentValues),
898
            averageVoltageVolts: mean(voltageValues),
899
            stabilityDeltaWatts: stabilityDelta,
900
            stabilityToleranceWatts: stabilityTolerance,
901
            histogram: histogram(for: powerValues)
902
        )
903
    }
904

            
905
    static func measurementSummary(
906
        chargerID: UUID,
907
        meterMACAddress: String,
908
        meterName: String?,
909
        meterModel: String?,
910
        startedAt: Date,
911
        endedAt: Date,
912
        samples: [ChargerStandbyPowerSample],
913
        stabilizedAt: Date?
914
    ) -> ChargerStandbyPowerMeasurementSummary? {
915
        guard let statistics = statistics(from: samples, startedAt: startedAt, referenceDate: endedAt) else {
916
            return nil
917
        }
918

            
919
        return ChargerStandbyPowerMeasurementSummary(
920
            id: UUID(),
921
            chargerID: chargerID,
922
            meterMACAddress: meterMACAddress,
923
            meterName: meterName,
924
            meterModel: meterModel,
925
            startedAt: startedAt,
926
            endedAt: endedAt,
927
            sampleCount: statistics.sampleCount,
928
            stabilizedAt: stabilizedAt,
929
            averagePowerWatts: statistics.averagePowerWatts,
930
            recentAveragePowerWatts: statistics.recentAveragePowerWatts,
931
            medianPowerWatts: statistics.medianPowerWatts,
932
            minimumPowerWatts: statistics.minimumPowerWatts,
933
            maximumPowerWatts: statistics.maximumPowerWatts,
934
            standardDeviationPowerWatts: statistics.standardDeviationPowerWatts,
935
            coefficientOfVariation: statistics.coefficientOfVariation,
936
            averageCurrentAmps: statistics.averageCurrentAmps,
937
            averageVoltageVolts: statistics.averageVoltageVolts,
938
            stabilityDeltaWatts: statistics.stabilityDeltaWatts,
939
            stabilityToleranceWatts: statistics.stabilityToleranceWatts,
940
            powerSamplesWatts: samples.map(\.powerWatts)
941
        )
942
    }
943

            
944
    static func histogram(for values: [Double], preferredBinCount: Int? = nil) -> [ChargerStandbyPowerDistributionBin] {
945
        let finiteValues = values.filter(\.isFinite)
946
        guard finiteValues.isEmpty == false else {
947
            return []
948
        }
949

            
950
        let minimum = finiteValues.min() ?? 0
951
        let maximum = finiteValues.max() ?? 0
952
        let spread = maximum - minimum
953
        let binCount = preferredBinCount ?? min(18, max(8, Int(Double(finiteValues.count).squareRoot().rounded())))
954

            
955
        guard spread > 0 else {
956
            return [
957
                ChargerStandbyPowerDistributionBin(
958
                    index: 0,
959
                    lowerBoundWatts: minimum,
960
                    upperBoundWatts: maximum,
961
                    count: finiteValues.count,
962
                    relativeFrequency: 1
963
                )
964
            ]
965
        }
966

            
967
        let safeBinCount = max(1, binCount)
968
        let binWidth = spread / Double(safeBinCount)
969
        var counts = Array(repeating: 0, count: safeBinCount)
970

            
971
        for value in finiteValues {
972
            let normalizedIndex = Int(((value - minimum) / binWidth).rounded(.down))
973
            let safeIndex = min(max(normalizedIndex, 0), safeBinCount - 1)
974
            counts[safeIndex] += 1
975
        }
976

            
977
        return counts.enumerated().map { index, count in
978
            let lowerBound = minimum + (Double(index) * binWidth)
979
            let upperBound = index == safeBinCount - 1 ? maximum : lowerBound + binWidth
980

            
981
            return ChargerStandbyPowerDistributionBin(
982
                index: index,
983
                lowerBoundWatts: lowerBound,
984
                upperBoundWatts: upperBound,
985
                count: count,
986
                relativeFrequency: Double(count) / Double(finiteValues.count)
987
            )
988
        }
989
    }
990

            
991
    private static func mean(_ values: [Double]) -> Double {
992
        guard values.isEmpty == false else {
993
            return 0
994
        }
995
        return values.reduce(0, +) / Double(values.count)
996
    }
997

            
998
    private static func median(_ values: [Double]) -> Double {
999
        guard values.isEmpty == false else {
1000
            return 0
1001
        }
1002

            
1003
        let sorted = values.sorted()
1004
        let middleIndex = sorted.count / 2
1005

            
1006
        if sorted.count.isMultiple(of: 2) {
1007
            return (sorted[middleIndex - 1] + sorted[middleIndex]) / 2
1008
        }
1009

            
1010
        return sorted[middleIndex]
1011
    }
1012

            
1013
    private static func standardDeviation(_ values: [Double], mean: Double) -> Double {
1014
        guard values.count > 1 else {
1015
            return 0
1016
        }
1017

            
1018
        let variance = values.reduce(0) { partialResult, value in
1019
            let delta = value - mean
1020
            return partialResult + (delta * delta)
1021
        } / Double(values.count)
1022

            
1023
        return variance.squareRoot()
1024
    }
1025

            
1026
    private static func coefficientOfVariation(_ values: [Double], mean: Double) -> Double {
1027
        guard abs(mean) > 0.000_001 else {
1028
            return 0
1029
        }
1030

            
1031
        return standardDeviation(values, mean: mean) / abs(mean)
1032
    }
1033
}
1034

            
Bogdan Timofte authored a month ago
1035
struct ChargedDeviceSummary: Identifiable, Hashable {
1036
    let id: UUID
1037
    let qrIdentifier: String
1038
    let name: String
1039
    let deviceClass: ChargedDeviceClass
Bogdan Timofte authored a month ago
1040
    let deviceTemplateID: String?
1041
    let templateDefinition: ChargedDeviceTemplateDefinition?
Bogdan Timofte authored a month ago
1042
    let supportsChargingWhileOff: Bool
Bogdan Timofte authored a month ago
1043
    let chargingStateAvailability: ChargingStateAvailability
Bogdan Timofte authored a month ago
1044
    let supportsWiredCharging: Bool
1045
    let supportsWirelessCharging: Bool
Bogdan Timofte authored a month ago
1046
    let chargerType: ChargerType?
Bogdan Timofte authored a month ago
1047
    let wirelessChargingProfile: WirelessChargingProfile
Bogdan Timofte authored a month ago
1048
    let configuredCompletionCurrents: [ChargeSessionKind: Double]
1049
    let learnedCompletionCurrents: [ChargeSessionKind: Double]
Bogdan Timofte authored a month ago
1050
    let wirelessChargerEfficiencyFactor: Double?
1051
    let wiredChargeCompletionCurrentAmps: Double?
1052
    let wirelessChargeCompletionCurrentAmps: Double?
1053
    let chargerObservedVoltageSelections: [Double]
1054
    let chargerIdleCurrentAmps: Double?
1055
    let chargerEfficiencyFactor: Double?
1056
    let chargerMaximumPowerWatts: Double?
1057
    let notes: String?
1058
    let minimumCurrentAmps: Double?
1059
    let estimatedBatteryCapacityWh: Double?
1060
    let wiredMinimumCurrentAmps: Double?
1061
    let wirelessMinimumCurrentAmps: Double?
1062
    let wiredEstimatedBatteryCapacityWh: Double?
1063
    let wirelessEstimatedBatteryCapacityWh: Double?
1064
    let lastAssociatedMeterMAC: String?
1065
    let createdAt: Date
1066
    let updatedAt: Date
1067
    let sessions: [ChargeSessionSummary]
1068
    let capacityHistory: [CapacityTrendPoint]
1069
    let typicalCurve: [TypicalChargeCurvePoint]
Bogdan Timofte authored a month ago
1070
    let standbyPowerMeasurements: [ChargerStandbyPowerMeasurementSummary]
Bogdan Timofte authored a month ago
1071

            
1072
    var isCharger: Bool {
1073
        deviceClass == .charger
1074
    }
1075

            
Bogdan Timofte authored a month ago
1076
    var kind: ChargedDeviceKind {
1077
        deviceClass.kind
1078
    }
1079

            
1080
    var identityTitle: String {
Bogdan Timofte authored a month ago
1081
        templateDefinition?.name ?? (isCharger ? kind.title : deviceClass.title)
Bogdan Timofte authored a month ago
1082
    }
1083

            
Bogdan Timofte authored a month ago
1084
    var fallbackIdentitySymbolName: String {
Bogdan Timofte authored a month ago
1085
        isCharger ? kind.symbolName : deviceClass.symbolName
1086
    }
1087

            
Bogdan Timofte authored a month ago
1088
    var identityIcon: ChargedDeviceTemplateIcon {
1089
        templateDefinition?.icon ?? .systemSymbol(fallbackIdentitySymbolName)
1090
    }
1091

            
1092
    var identitySymbolName: String {
1093
        identityIcon.resolvedSystemSymbolName(fallbackSystemName: fallbackIdentitySymbolName)
1094
    }
1095

            
Bogdan Timofte authored a month ago
1096
    var activeSession: ChargeSessionSummary? {
Bogdan Timofte authored a month ago
1097
        sessions.first(where: \.isOpen)
Bogdan Timofte authored a month ago
1098
    }
1099

            
1100
    var recentCompletedSessions: [ChargeSessionSummary] {
1101
        sessions.filter { $0.status == .completed }
1102
    }
1103

            
1104
    var sessionCount: Int {
1105
        sessions.count
1106
    }
1107

            
Bogdan Timofte authored a month ago
1108
    var latestStandbyPowerMeasurement: ChargerStandbyPowerMeasurementSummary? {
1109
        standbyPowerMeasurements.first
1110
    }
1111

            
Bogdan Timofte authored a month ago
1112
    var supportedChargingModes: [ChargingTransportMode] {
1113
        var modes: [ChargingTransportMode] = []
1114
        if supportsWiredCharging {
1115
            modes.append(.wired)
1116
        }
1117
        if supportsWirelessCharging {
1118
            modes.append(.wireless)
1119
        }
Bogdan Timofte authored a month ago
1120
        return modes
Bogdan Timofte authored a month ago
1121
    }
1122

            
Bogdan Timofte authored a month ago
1123
    var supportedChargingStateModes: [ChargingStateMode] {
1124
        chargingStateAvailability.supportedModes
1125
    }
1126

            
Bogdan Timofte authored a month ago
1127
    var hasMultipleChargingTransports: Bool {
1128
        supportedChargingModes.count > 1
1129
    }
1130

            
1131
    var hasMultipleChargingStateModes: Bool {
1132
        supportedChargingStateModes.count > 1
1133
    }
1134

            
1135
    var showsWirelessProfileDetails: Bool {
1136
        supportsWirelessCharging
1137
            && hasMultipleChargingTransports
1138
            && deviceClass != .watch
1139
    }
1140

            
1141
    var chargingSupportSummary: String {
1142
        switch (supportsWiredCharging, supportsWirelessCharging) {
1143
        case (true, true):
1144
            return "Supports wired and wireless charging."
1145
        case (true, false):
1146
            return "Supports wired charging only."
1147
        case (false, true):
1148
            return "Supports wireless charging only."
1149
        case (false, false):
1150
            return "No charging method configured."
1151
        }
1152
    }
1153

            
Bogdan Timofte authored a month ago
1154
    func defaultChargingStateMode(for chargingTransportMode: ChargingTransportMode) -> ChargingStateMode {
1155
        if let matchingSession = sessions.first(where: {
1156
            $0.status.isOpen && $0.chargingTransportMode == chargingTransportMode
1157
        }) {
1158
            return matchingSession.chargingStateMode
1159
        }
1160
        return chargingStateAvailability.supportedModes.first ?? .on
1161
    }
1162

            
1163
    func sessionKind(
1164
        for chargingTransportMode: ChargingTransportMode,
1165
        chargingStateMode: ChargingStateMode? = nil
1166
    ) -> ChargeSessionKind {
1167
        ChargeSessionKind(
1168
            chargingTransportMode: chargingTransportMode,
1169
            chargingStateMode: chargingStateMode ?? defaultChargingStateMode(for: chargingTransportMode)
1170
        )
1171
    }
1172

            
Bogdan Timofte authored a month ago
1173
    func estimatedBatteryCapacityWh(for chargingTransportMode: ChargingTransportMode) -> Double? {
1174
        switch chargingTransportMode {
1175
        case .wired:
1176
            return wiredEstimatedBatteryCapacityWh
1177
        case .wireless:
1178
            return wirelessEstimatedBatteryCapacityWh
1179
        }
1180
    }
1181

            
1182
    func minimumCurrentAmps(for chargingTransportMode: ChargingTransportMode) -> Double? {
1183
        switch chargingTransportMode {
1184
        case .wired:
1185
            return wiredMinimumCurrentAmps
1186
        case .wireless:
1187
            return wirelessMinimumCurrentAmps
1188
        }
1189
    }
1190

            
Bogdan Timofte authored a month ago
1191
    func shouldShowChargingTransport(_ chargingTransportMode: ChargingTransportMode) -> Bool {
1192
        hasMultipleChargingTransports
1193
            || supportedChargingModes.contains(chargingTransportMode) == false
1194
    }
1195

            
1196
    func shouldShowChargingStateMode(_ chargingStateMode: ChargingStateMode) -> Bool {
1197
        hasMultipleChargingStateModes
1198
            || supportedChargingStateModes.contains(chargingStateMode) == false
1199
    }
1200

            
Bogdan Timofte authored a month ago
1201
    func configuredCompletionCurrentAmps(for sessionKind: ChargeSessionKind) -> Double? {
1202
        if let explicitCurrent = configuredCompletionCurrents[sessionKind] {
1203
            return explicitCurrent
1204
        }
1205

            
1206
        switch sessionKind.chargingTransportMode {
Bogdan Timofte authored a month ago
1207
        case .wired:
1208
            return wiredChargeCompletionCurrentAmps
1209
        case .wireless:
1210
            return wirelessChargeCompletionCurrentAmps
1211
        }
1212
    }
1213

            
Bogdan Timofte authored a month ago
1214
    func learnedCompletionCurrentAmps(for sessionKind: ChargeSessionKind) -> Double? {
1215
        if let learnedCurrent = learnedCompletionCurrents[sessionKind] {
1216
            return learnedCurrent
1217
        }
1218

            
1219
        switch sessionKind.chargingTransportMode {
1220
        case .wired:
1221
            return wiredMinimumCurrentAmps ?? minimumCurrentAmps
1222
        case .wireless:
1223
            return wirelessMinimumCurrentAmps ?? minimumCurrentAmps
1224
        }
1225
    }
1226

            
1227
    func resolvedCompletionCurrentAmps(
1228
        for chargingTransportMode: ChargingTransportMode,
1229
        chargingStateMode: ChargingStateMode? = nil
1230
    ) -> Double? {
1231
        let sessionKind = sessionKind(
1232
            for: chargingTransportMode,
1233
            chargingStateMode: chargingStateMode
1234
        )
1235

            
1236
        return configuredCompletionCurrentAmps(for: sessionKind)
1237
            ?? learnedCompletionCurrentAmps(for: sessionKind)
Bogdan Timofte authored a month ago
1238
            ?? minimumCurrentAmps(for: chargingTransportMode)
1239
            ?? minimumCurrentAmps
1240
    }
1241

            
Bogdan Timofte authored a month ago
1242
    func batteryLevelPrediction(
1243
        for session: ChargeSessionSummary,
1244
        effectiveEnergyWhOverride: Double? = nil
1245
    ) -> BatteryLevelPrediction? {
Bogdan Timofte authored a month ago
1246
        let estimatedCapacityWh = session.capacityEstimateWh
1247
            ?? estimatedBatteryCapacityWh(for: session.chargingTransportMode)
1248
            ?? estimatedBatteryCapacityWh
1249

            
1250
        guard let estimatedCapacityWh, estimatedCapacityWh > 0 else {
1251
            return nil
1252
        }
1253

            
Bogdan Timofte authored a month ago
1254
        let effectiveEnergyWh = effectiveEnergyWhOverride
1255
            ?? session.effectiveBatteryEnergyWh
1256
            ?? session.measuredEnergyWh
Bogdan Timofte authored a month ago
1257

            
1258
        struct Anchor {
1259
            let percent: Double
1260
            let energyWh: Double
Bogdan Timofte authored a month ago
1261
            let timestamp: Date
Bogdan Timofte authored a month ago
1262
            let description: String
Bogdan Timofte authored a month ago
1263
            let isCheckpoint: Bool
Bogdan Timofte authored a month ago
1264
        }
1265

            
1266
        var anchors: [Anchor] = []
1267

            
Bogdan Timofte authored a month ago
1268
        if let startBatteryPercent = session.startBatteryPercent, startBatteryPercent >= 0 {
Bogdan Timofte authored a month ago
1269
            anchors.append(
1270
                Anchor(
1271
                    percent: startBatteryPercent,
1272
                    energyWh: 0,
Bogdan Timofte authored a month ago
1273
                    timestamp: session.startedAt,
1274
                    description: "session start",
1275
                    isCheckpoint: false
Bogdan Timofte authored a month ago
1276
                )
1277
            )
1278
        }
1279

            
1280
        anchors.append(
1281
            contentsOf: session.checkpoints
1282
                .sorted { lhs, rhs in
1283
                    if lhs.measuredEnergyWh != rhs.measuredEnergyWh {
1284
                        return lhs.measuredEnergyWh < rhs.measuredEnergyWh
1285
                    }
1286
                    return lhs.timestamp < rhs.timestamp
1287
                }
Bogdan Timofte authored a month ago
1288
                .filter { checkpoint in
1289
                    checkpoint.batteryPercent >= 0
1290
                }
Bogdan Timofte authored a month ago
1291
                .map { checkpoint in
1292
                    return Anchor(
1293
                        percent: checkpoint.batteryPercent,
1294
                        energyWh: checkpoint.measuredEnergyWh,
Bogdan Timofte authored a month ago
1295
                        timestamp: checkpoint.timestamp,
Bogdan Timofte authored a month ago
1296
                        description: checkpoint.flag.anchorDescription,
Bogdan Timofte authored a month ago
1297
                        isCheckpoint: true
Bogdan Timofte authored a month ago
1298
                    )
1299
                }
1300
        )
1301

            
1302
        guard !anchors.isEmpty else {
1303
            return nil
1304
        }
1305

            
1306
        let eligibleAnchors = anchors.filter { $0.energyWh <= effectiveEnergyWh + 0.05 }
1307
        let anchor = eligibleAnchors.last ?? anchors.first!
Bogdan Timofte authored a month ago
1308
        let predictedPercent = BatteryLevelPredictionTuning.predictedPercent(
1309
            anchorPercent: anchor.percent,
1310
            anchorEnergyWh: anchor.energyWh,
1311
            anchorTimestamp: anchor.timestamp,
1312
            anchorIsCheckpoint: anchor.isCheckpoint,
1313
            effectiveEnergyWh: effectiveEnergyWh,
1314
            referenceTimestamp: session.lastObservedAt,
1315
            estimatedCapacityWh: estimatedCapacityWh
Bogdan Timofte authored a month ago
1316
        )
1317

            
1318
        return BatteryLevelPrediction(
1319
            predictedPercent: predictedPercent,
1320
            estimatedCapacityWh: estimatedCapacityWh,
1321
            anchorPercent: anchor.percent,
1322
            anchorEnergyWh: anchor.energyWh,
1323
            anchorDescription: anchor.description
1324
        )
1325
    }
Bogdan Timofte authored a month ago
1326

            
1327
    func withStandbyPowerMeasurements(_ measurements: [ChargerStandbyPowerMeasurementSummary]) -> ChargedDeviceSummary {
1328
        ChargedDeviceSummary(
1329
            id: id,
1330
            qrIdentifier: qrIdentifier,
1331
            name: name,
1332
            deviceClass: deviceClass,
Bogdan Timofte authored a month ago
1333
            deviceTemplateID: deviceTemplateID,
1334
            templateDefinition: templateDefinition,
Bogdan Timofte authored a month ago
1335
            supportsChargingWhileOff: supportsChargingWhileOff,
1336
            chargingStateAvailability: chargingStateAvailability,
1337
            supportsWiredCharging: supportsWiredCharging,
1338
            supportsWirelessCharging: supportsWirelessCharging,
Bogdan Timofte authored a month ago
1339
            chargerType: chargerType,
Bogdan Timofte authored a month ago
1340
            wirelessChargingProfile: wirelessChargingProfile,
1341
            configuredCompletionCurrents: configuredCompletionCurrents,
1342
            learnedCompletionCurrents: learnedCompletionCurrents,
1343
            wirelessChargerEfficiencyFactor: wirelessChargerEfficiencyFactor,
1344
            wiredChargeCompletionCurrentAmps: wiredChargeCompletionCurrentAmps,
1345
            wirelessChargeCompletionCurrentAmps: wirelessChargeCompletionCurrentAmps,
1346
            chargerObservedVoltageSelections: chargerObservedVoltageSelections,
1347
            chargerIdleCurrentAmps: chargerIdleCurrentAmps,
1348
            chargerEfficiencyFactor: chargerEfficiencyFactor,
1349
            chargerMaximumPowerWatts: chargerMaximumPowerWatts,
1350
            notes: notes,
1351
            minimumCurrentAmps: minimumCurrentAmps,
1352
            estimatedBatteryCapacityWh: estimatedBatteryCapacityWh,
1353
            wiredMinimumCurrentAmps: wiredMinimumCurrentAmps,
1354
            wirelessMinimumCurrentAmps: wirelessMinimumCurrentAmps,
1355
            wiredEstimatedBatteryCapacityWh: wiredEstimatedBatteryCapacityWh,
1356
            wirelessEstimatedBatteryCapacityWh: wirelessEstimatedBatteryCapacityWh,
1357
            lastAssociatedMeterMAC: lastAssociatedMeterMAC,
1358
            createdAt: createdAt,
1359
            updatedAt: updatedAt,
1360
            sessions: sessions,
1361
            capacityHistory: capacityHistory,
1362
            typicalCurve: typicalCurve,
1363
            standbyPowerMeasurements: measurements
1364
        )
1365
    }
Bogdan Timofte authored a month ago
1366
}
1367

            
1368
struct ChargingMonitorSnapshot {
1369
    let meterMACAddress: String
1370
    let meterName: String
1371
    let meterModel: String
1372
    let observedAt: Date
1373
    let voltageVolts: Double
1374
    let currentAmps: Double
1375
    let powerWatts: Double
1376
    let selectedDataGroup: UInt8?
1377
    let meterChargeCounterAh: Double?
1378
    let meterEnergyCounterWh: Double?
Bogdan Timofte authored a month ago
1379
    let meterRecordingDurationSeconds: TimeInterval?
Bogdan Timofte authored a month ago
1380
    let fallbackStopThresholdAmps: Double
1381
}