USB-Meter / USB Meter / Model / ChargeInsightsModel.swift
Newer Older
736 lines | 20.63kb
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 {
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 {
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
    }
90
}
91

            
92
enum ChargeSessionStatus: String {
93
    case active
Bogdan Timofte authored a month ago
94
    case paused
Bogdan Timofte authored a month ago
95
    case completed
96
    case abandoned
97

            
98
    var title: String {
Bogdan Timofte authored a month ago
99
        switch self {
100
        case .active:
101
            return "Active"
102
        case .paused:
103
            return "Paused"
104
        case .completed:
105
            return "Completed"
106
        case .abandoned:
107
            return "Abandoned"
108
        }
109
    }
110

            
111
    var isOpen: Bool {
112
        switch self {
113
        case .active, .paused:
114
            return true
115
        case .completed, .abandoned:
116
            return false
117
        }
Bogdan Timofte authored a month ago
118
    }
119
}
120

            
121
enum ChargeSessionSourceMode: String {
122
    case live
123
    case offline
124
    case blended
125

            
126
    var title: String {
127
        switch self {
128
        case .live:
129
            return "Live"
130
        case .offline:
131
            return "Offline Counters"
132
        case .blended:
133
            return "Blended"
134
        }
135
    }
136
}
137

            
Bogdan Timofte authored a month ago
138
enum ChargingTransportMode: String, CaseIterable, Identifiable, Codable {
Bogdan Timofte authored a month ago
139
    case wired
140
    case wireless
141

            
142
    var id: String { rawValue }
143

            
144
    var title: String {
145
        switch self {
146
        case .wired:
147
            return "Wired"
148
        case .wireless:
149
            return "Wireless"
150
        }
151
    }
152

            
153
    var symbolName: String {
154
        switch self {
155
        case .wired:
156
            return "cable.connector"
157
        case .wireless:
158
            return "dot.radiowaves.left.and.right"
159
        }
160
    }
161
}
162

            
Bogdan Timofte authored a month ago
163
enum ChargingStateMode: String, CaseIterable, Identifiable, Codable {
164
    case on
165
    case off
166

            
167
    var id: String { rawValue }
168

            
169
    var title: String {
170
        switch self {
171
        case .on:
172
            return "On"
173
        case .off:
174
            return "Off"
175
        }
176
    }
177

            
178
    var description: String {
179
        switch self {
180
        case .on:
181
            return "Device stays powered on while charging."
182
        case .off:
183
            return "Device is powered off while charging."
184
        }
185
    }
186
}
187

            
188
enum ChargingStateAvailability: String, CaseIterable, Identifiable, Codable {
189
    case onOnly
190
    case onOrOff
191
    case offOnly
192

            
193
    var id: String { rawValue }
194

            
195
    var title: String {
196
        switch self {
197
        case .onOnly:
198
            return "On Only"
199
        case .onOrOff:
200
            return "On or Off"
201
        case .offOnly:
202
            return "Off Only"
203
        }
204
    }
205

            
206
    var description: String {
207
        switch self {
208
        case .onOnly:
209
            return "The device can be recorded only while it is powered on."
210
        case .onOrOff:
211
            return "The session must specify whether the device is on or off."
212
        case .offOnly:
213
            return "The device can be recorded only while it is powered off."
214
        }
215
    }
216

            
217
    var supportedModes: [ChargingStateMode] {
218
        switch self {
219
        case .onOnly:
220
            return [.on]
221
        case .onOrOff:
222
            return [.on, .off]
223
        case .offOnly:
224
            return [.off]
225
        }
226
    }
227

            
228
    var supportsMultipleModes: Bool {
229
        supportedModes.count > 1
230
    }
231

            
232
    var supportsChargingWhileOff: Bool {
233
        self != .onOnly
234
    }
235

            
236
    static func fallback(for supportsChargingWhileOff: Bool) -> ChargingStateAvailability {
237
        supportsChargingWhileOff ? .onOrOff : .onOnly
238
    }
239
}
240

            
241
enum ChargeSessionKind: String, CaseIterable, Identifiable, Codable, Hashable {
242
    case wiredOn
243
    case wiredOff
244
    case wirelessOn
245
    case wirelessOff
246

            
247
    var id: String { rawValue }
248

            
249
    init(chargingTransportMode: ChargingTransportMode, chargingStateMode: ChargingStateMode) {
250
        switch (chargingTransportMode, chargingStateMode) {
251
        case (.wired, .on):
252
            self = .wiredOn
253
        case (.wired, .off):
254
            self = .wiredOff
255
        case (.wireless, .on):
256
            self = .wirelessOn
257
        case (.wireless, .off):
258
            self = .wirelessOff
259
        }
260
    }
261

            
262
    var chargingTransportMode: ChargingTransportMode {
263
        switch self {
264
        case .wiredOn, .wiredOff:
265
            return .wired
266
        case .wirelessOn, .wirelessOff:
267
            return .wireless
268
        }
269
    }
270

            
271
    var chargingStateMode: ChargingStateMode {
272
        switch self {
273
        case .wiredOn, .wirelessOn:
274
            return .on
275
        case .wiredOff, .wirelessOff:
276
            return .off
277
        }
278
    }
279

            
280
    var title: String {
281
        "\(chargingTransportMode.title) • \(chargingStateMode.title)"
282
    }
283

            
284
    var shortTitle: String {
285
        "\(chargingTransportMode.title) \(chargingStateMode.title)"
286
    }
287
}
288

            
Bogdan Timofte authored a month ago
289
enum WirelessChargingProfile: String, CaseIterable, Identifiable {
290
    case magsafe
291
    case genericQi
292

            
293
    var id: String { rawValue }
294

            
295
    var title: String {
296
        switch self {
297
        case .magsafe:
298
            return "MagSafe"
299
        case .genericQi:
300
            return "Generic Qi"
301
        }
302
    }
303

            
304
    var description: String {
305
        switch self {
306
        case .magsafe:
307
            return "Use separate wireless-efficiency calibration from devices that also have reliable wired capacity."
308
        case .genericQi:
309
            return "Use only automatic efficiency estimates and show a low-efficiency warning when needed."
310
        }
311
    }
312
}
313

            
314
struct ChargeCheckpointSummary: Identifiable, Hashable {
315
    let id: UUID
316
    let sessionID: UUID
317
    let chargedDeviceID: UUID
318
    let timestamp: Date
319
    let batteryPercent: Double
320
    let measuredEnergyWh: Double
321
    let measuredChargeAh: Double
322
    let currentAmps: Double
323
    let voltageVolts: Double?
324
    let label: String?
325
}
326

            
327
struct ChargeSessionSampleSummary: Identifiable, Hashable {
328
    let sessionID: UUID
329
    let chargedDeviceID: UUID
330
    let bucketIndex: Int
331
    let timestamp: Date
332
    let averageCurrentAmps: Double
333
    let averageVoltageVolts: Double?
334
    let averagePowerWatts: Double
335
    let measuredEnergyWh: Double
336
    let measuredChargeAh: Double
337
    let sampleCount: Int
338

            
339
    var id: String {
340
        "\(sessionID.uuidString)-\(bucketIndex)"
341
    }
342
}
343

            
344
struct ChargeSessionSummary: Identifiable, Hashable {
345
    let id: UUID
346
    let chargedDeviceID: UUID
347
    let chargerID: UUID?
348
    let meterMACAddress: String?
349
    let meterName: String?
350
    let meterModel: String?
351
    let startedAt: Date
352
    let endedAt: Date?
353
    let lastObservedAt: Date
Bogdan Timofte authored a month ago
354
    let pausedAt: Date?
Bogdan Timofte authored a month ago
355
    let status: ChargeSessionStatus
356
    let sourceMode: ChargeSessionSourceMode
357
    let chargingTransportMode: ChargingTransportMode
Bogdan Timofte authored a month ago
358
    let chargingStateMode: ChargingStateMode
359
    let autoStopEnabled: Bool
Bogdan Timofte authored a month ago
360
    let measuredEnergyWh: Double
361
    let effectiveBatteryEnergyWh: Double?
362
    let measuredChargeAh: Double
Bogdan Timofte authored a month ago
363
    let meterEnergyBaselineWh: Double?
364
    let meterChargeBaselineAh: Double?
Bogdan Timofte authored a month ago
365
    let minimumObservedCurrentAmps: Double?
366
    let maximumObservedCurrentAmps: Double?
367
    let maximumObservedPowerWatts: Double?
368
    let maximumObservedVoltageVolts: Double?
369
    let selectedSourceVoltageVolts: Double?
370
    let completionCurrentAmps: Double?
371
    let stopThresholdAmps: Double
372
    let startBatteryPercent: Double?
373
    let endBatteryPercent: Double?
374
    let capacityEstimateWh: Double?
375
    let wirelessEfficiencyFactor: Double?
376
    let usesEstimatedWirelessEfficiency: Bool
377
    let shouldWarnAboutLowWirelessEfficiency: Bool
378
    let supportsChargingWhileOff: Bool
379
    let usedOfflineMeterCounters: Bool
380
    let targetBatteryPercent: Double?
381
    let targetBatteryAlertTriggeredAt: Date?
382
    let requiresCompletionConfirmation: Bool
383
    let completionConfirmationRequestedAt: Date?
384
    let completionContradictionPercent: Double?
385
    let selectedDataGroup: UInt8?
386
    let checkpoints: [ChargeCheckpointSummary]
387
    let aggregatedSamples: [ChargeSessionSampleSummary]
388

            
Bogdan Timofte authored a month ago
389
    var sessionKind: ChargeSessionKind {
390
        ChargeSessionKind(
391
            chargingTransportMode: chargingTransportMode,
392
            chargingStateMode: chargingStateMode
393
        )
394
    }
395

            
Bogdan Timofte authored a month ago
396
    var duration: TimeInterval {
397
        (endedAt ?? lastObservedAt).timeIntervalSince(startedAt)
398
    }
399

            
400
    var effectiveOrMeasuredEnergyWh: Double {
401
        effectiveBatteryEnergyWh ?? measuredEnergyWh
402
    }
403

            
404
    var batteryDeltaPercent: Double? {
Bogdan Timofte authored a month ago
405
        guard let startBatteryPercent, let endBatteryPercent,
406
              startBatteryPercent >= 0, endBatteryPercent >= 0 else { return nil }
Bogdan Timofte authored a month ago
407
        return endBatteryPercent - startBatteryPercent
408
    }
Bogdan Timofte authored a month ago
409

            
410
    var canAutoStop: Bool {
411
        autoStopEnabled && stopThresholdAmps > 0
412
    }
413

            
414
    var isPaused: Bool {
415
        status == .paused
416
    }
417

            
418
    var isOpen: Bool {
419
        status.isOpen
420
    }
Bogdan Timofte authored a month ago
421
}
422

            
423
struct BatteryLevelPrediction: Hashable {
424
    let predictedPercent: Double
425
    let estimatedCapacityWh: Double
426
    let anchorPercent: Double
427
    let anchorEnergyWh: Double
428
    let anchorDescription: String
429
}
430

            
Bogdan Timofte authored a month ago
431
enum BatteryLevelPredictionTuning {
432
    static let checkpointSettleDuration: TimeInterval = 10 * 60
433

            
434
    static func predictedPercent(
435
        anchorPercent: Double,
436
        anchorEnergyWh: Double,
437
        anchorTimestamp: Date,
438
        anchorIsCheckpoint: Bool,
439
        effectiveEnergyWh: Double,
440
        referenceTimestamp: Date,
441
        estimatedCapacityWh: Double
442
    ) -> Double {
443
        let energyDeltaWh = max(effectiveEnergyWh - anchorEnergyWh, 0)
444
        let rawGainPercent = (energyDeltaWh / estimatedCapacityWh) * 100
445
        let stabilizedGainPercent: Double
446

            
447
        if anchorIsCheckpoint {
448
            let elapsedSinceAnchor = max(referenceTimestamp.timeIntervalSince(anchorTimestamp), 0)
449
            let settleProgress = min(max(elapsedSinceAnchor / checkpointSettleDuration, 0), 1)
450
            stabilizedGainPercent = rawGainPercent * settleProgress
451
        } else {
452
            stabilizedGainPercent = rawGainPercent
453
        }
454

            
455
        return min(
456
            100,
457
            max(
458
                0,
459
                anchorPercent + stabilizedGainPercent
460
            )
461
        )
462
    }
463
}
464

            
Bogdan Timofte authored a month ago
465
struct CapacityTrendPoint: Identifiable, Hashable {
466
    let sessionID: UUID
467
    let timestamp: Date
468
    let capacityWh: Double
469
    let chargingTransportMode: ChargingTransportMode
470

            
471
    var id: UUID { sessionID }
472
}
473

            
474
struct TypicalChargeCurvePoint: Identifiable, Hashable {
475
    let percentBin: Int
476
    let averageEnergyWh: Double
477
    let averageChargeAh: Double
478
    let sampleCount: Int
479

            
480
    var id: Int { percentBin }
481
}
482

            
483
struct ChargedDeviceSummary: Identifiable, Hashable {
484
    let id: UUID
485
    let qrIdentifier: String
486
    let name: String
487
    let deviceClass: ChargedDeviceClass
488
    let supportsChargingWhileOff: Bool
Bogdan Timofte authored a month ago
489
    let chargingStateAvailability: ChargingStateAvailability
Bogdan Timofte authored a month ago
490
    let supportsWiredCharging: Bool
491
    let supportsWirelessCharging: Bool
492
    let wirelessChargingProfile: WirelessChargingProfile
Bogdan Timofte authored a month ago
493
    let configuredCompletionCurrents: [ChargeSessionKind: Double]
494
    let learnedCompletionCurrents: [ChargeSessionKind: Double]
Bogdan Timofte authored a month ago
495
    let wirelessChargerEfficiencyFactor: Double?
496
    let wiredChargeCompletionCurrentAmps: Double?
497
    let wirelessChargeCompletionCurrentAmps: Double?
498
    let chargerObservedVoltageSelections: [Double]
499
    let chargerIdleCurrentAmps: Double?
500
    let chargerEfficiencyFactor: Double?
501
    let chargerMaximumPowerWatts: Double?
502
    let notes: String?
503
    let minimumCurrentAmps: Double?
504
    let estimatedBatteryCapacityWh: Double?
505
    let wiredMinimumCurrentAmps: Double?
506
    let wirelessMinimumCurrentAmps: Double?
507
    let wiredEstimatedBatteryCapacityWh: Double?
508
    let wirelessEstimatedBatteryCapacityWh: Double?
509
    let lastAssociatedMeterMAC: String?
510
    let createdAt: Date
511
    let updatedAt: Date
512
    let sessions: [ChargeSessionSummary]
513
    let capacityHistory: [CapacityTrendPoint]
514
    let typicalCurve: [TypicalChargeCurvePoint]
515

            
516
    var isCharger: Bool {
517
        deviceClass == .charger
518
    }
519

            
Bogdan Timofte authored a month ago
520
    var kind: ChargedDeviceKind {
521
        deviceClass.kind
522
    }
523

            
524
    var identityTitle: String {
525
        isCharger ? kind.title : deviceClass.title
526
    }
527

            
528
    var identitySymbolName: String {
529
        isCharger ? kind.symbolName : deviceClass.symbolName
530
    }
531

            
Bogdan Timofte authored a month ago
532
    var activeSession: ChargeSessionSummary? {
Bogdan Timofte authored a month ago
533
        sessions.first(where: \.isOpen)
Bogdan Timofte authored a month ago
534
    }
535

            
536
    var recentCompletedSessions: [ChargeSessionSummary] {
537
        sessions.filter { $0.status == .completed }
538
    }
539

            
540
    var sessionCount: Int {
541
        sessions.count
542
    }
543

            
544
    var supportedChargingModes: [ChargingTransportMode] {
545
        var modes: [ChargingTransportMode] = []
546
        if supportsWiredCharging {
547
            modes.append(.wired)
548
        }
549
        if supportsWirelessCharging {
550
            modes.append(.wireless)
551
        }
Bogdan Timofte authored a month ago
552
        return modes
Bogdan Timofte authored a month ago
553
    }
554

            
Bogdan Timofte authored a month ago
555
    var supportedChargingStateModes: [ChargingStateMode] {
556
        chargingStateAvailability.supportedModes
557
    }
558

            
559
    func defaultChargingStateMode(for chargingTransportMode: ChargingTransportMode) -> ChargingStateMode {
560
        if let matchingSession = sessions.first(where: {
561
            $0.status.isOpen && $0.chargingTransportMode == chargingTransportMode
562
        }) {
563
            return matchingSession.chargingStateMode
564
        }
565
        return chargingStateAvailability.supportedModes.first ?? .on
566
    }
567

            
568
    func sessionKind(
569
        for chargingTransportMode: ChargingTransportMode,
570
        chargingStateMode: ChargingStateMode? = nil
571
    ) -> ChargeSessionKind {
572
        ChargeSessionKind(
573
            chargingTransportMode: chargingTransportMode,
574
            chargingStateMode: chargingStateMode ?? defaultChargingStateMode(for: chargingTransportMode)
575
        )
576
    }
577

            
Bogdan Timofte authored a month ago
578
    func estimatedBatteryCapacityWh(for chargingTransportMode: ChargingTransportMode) -> Double? {
579
        switch chargingTransportMode {
580
        case .wired:
581
            return wiredEstimatedBatteryCapacityWh
582
        case .wireless:
583
            return wirelessEstimatedBatteryCapacityWh
584
        }
585
    }
586

            
587
    func minimumCurrentAmps(for chargingTransportMode: ChargingTransportMode) -> Double? {
588
        switch chargingTransportMode {
589
        case .wired:
590
            return wiredMinimumCurrentAmps
591
        case .wireless:
592
            return wirelessMinimumCurrentAmps
593
        }
594
    }
595

            
Bogdan Timofte authored a month ago
596
    func configuredCompletionCurrentAmps(for sessionKind: ChargeSessionKind) -> Double? {
597
        if let explicitCurrent = configuredCompletionCurrents[sessionKind] {
598
            return explicitCurrent
599
        }
600

            
601
        switch sessionKind.chargingTransportMode {
Bogdan Timofte authored a month ago
602
        case .wired:
603
            return wiredChargeCompletionCurrentAmps
604
        case .wireless:
605
            return wirelessChargeCompletionCurrentAmps
606
        }
607
    }
608

            
Bogdan Timofte authored a month ago
609
    func learnedCompletionCurrentAmps(for sessionKind: ChargeSessionKind) -> Double? {
610
        if let learnedCurrent = learnedCompletionCurrents[sessionKind] {
611
            return learnedCurrent
612
        }
613

            
614
        switch sessionKind.chargingTransportMode {
615
        case .wired:
616
            return wiredMinimumCurrentAmps ?? minimumCurrentAmps
617
        case .wireless:
618
            return wirelessMinimumCurrentAmps ?? minimumCurrentAmps
619
        }
620
    }
621

            
622
    func resolvedCompletionCurrentAmps(
623
        for chargingTransportMode: ChargingTransportMode,
624
        chargingStateMode: ChargingStateMode? = nil
625
    ) -> Double? {
626
        let sessionKind = sessionKind(
627
            for: chargingTransportMode,
628
            chargingStateMode: chargingStateMode
629
        )
630

            
631
        return configuredCompletionCurrentAmps(for: sessionKind)
632
            ?? learnedCompletionCurrentAmps(for: sessionKind)
Bogdan Timofte authored a month ago
633
            ?? minimumCurrentAmps(for: chargingTransportMode)
634
            ?? minimumCurrentAmps
635
    }
636

            
Bogdan Timofte authored a month ago
637
    func batteryLevelPrediction(
638
        for session: ChargeSessionSummary,
639
        effectiveEnergyWhOverride: Double? = nil
640
    ) -> BatteryLevelPrediction? {
Bogdan Timofte authored a month ago
641
        let estimatedCapacityWh = session.capacityEstimateWh
642
            ?? estimatedBatteryCapacityWh(for: session.chargingTransportMode)
643
            ?? estimatedBatteryCapacityWh
644

            
645
        guard let estimatedCapacityWh, estimatedCapacityWh > 0 else {
646
            return nil
647
        }
648

            
Bogdan Timofte authored a month ago
649
        let effectiveEnergyWh = effectiveEnergyWhOverride
650
            ?? session.effectiveBatteryEnergyWh
651
            ?? session.measuredEnergyWh
Bogdan Timofte authored a month ago
652

            
653
        struct Anchor {
654
            let percent: Double
655
            let energyWh: Double
Bogdan Timofte authored a month ago
656
            let timestamp: Date
Bogdan Timofte authored a month ago
657
            let description: String
Bogdan Timofte authored a month ago
658
            let isCheckpoint: Bool
Bogdan Timofte authored a month ago
659
        }
660

            
661
        var anchors: [Anchor] = []
662

            
Bogdan Timofte authored a month ago
663
        if let startBatteryPercent = session.startBatteryPercent, startBatteryPercent >= 0 {
Bogdan Timofte authored a month ago
664
            anchors.append(
665
                Anchor(
666
                    percent: startBatteryPercent,
667
                    energyWh: 0,
Bogdan Timofte authored a month ago
668
                    timestamp: session.startedAt,
669
                    description: "session start",
670
                    isCheckpoint: false
Bogdan Timofte authored a month ago
671
                )
672
            )
673
        }
674

            
675
        anchors.append(
676
            contentsOf: session.checkpoints
677
                .sorted { lhs, rhs in
678
                    if lhs.measuredEnergyWh != rhs.measuredEnergyWh {
679
                        return lhs.measuredEnergyWh < rhs.measuredEnergyWh
680
                    }
681
                    return lhs.timestamp < rhs.timestamp
682
                }
Bogdan Timofte authored a month ago
683
                .filter { checkpoint in
684
                    checkpoint.batteryPercent >= 0
685
                }
Bogdan Timofte authored a month ago
686
                .map { checkpoint in
687
                    let trimmedLabel = checkpoint.label?.trimmingCharacters(in: .whitespacesAndNewlines)
688
                    return Anchor(
689
                        percent: checkpoint.batteryPercent,
690
                        energyWh: checkpoint.measuredEnergyWh,
Bogdan Timofte authored a month ago
691
                        timestamp: checkpoint.timestamp,
692
                        description: trimmedLabel.map { "checkpoint \($0)" } ?? "last checkpoint",
693
                        isCheckpoint: true
Bogdan Timofte authored a month ago
694
                    )
695
                }
696
        )
697

            
698
        guard !anchors.isEmpty else {
699
            return nil
700
        }
701

            
702
        let eligibleAnchors = anchors.filter { $0.energyWh <= effectiveEnergyWh + 0.05 }
703
        let anchor = eligibleAnchors.last ?? anchors.first!
Bogdan Timofte authored a month ago
704
        let predictedPercent = BatteryLevelPredictionTuning.predictedPercent(
705
            anchorPercent: anchor.percent,
706
            anchorEnergyWh: anchor.energyWh,
707
            anchorTimestamp: anchor.timestamp,
708
            anchorIsCheckpoint: anchor.isCheckpoint,
709
            effectiveEnergyWh: effectiveEnergyWh,
710
            referenceTimestamp: session.lastObservedAt,
711
            estimatedCapacityWh: estimatedCapacityWh
Bogdan Timofte authored a month ago
712
        )
713

            
714
        return BatteryLevelPrediction(
715
            predictedPercent: predictedPercent,
716
            estimatedCapacityWh: estimatedCapacityWh,
717
            anchorPercent: anchor.percent,
718
            anchorEnergyWh: anchor.energyWh,
719
            anchorDescription: anchor.description
720
        )
721
    }
722
}
723

            
724
struct ChargingMonitorSnapshot {
725
    let meterMACAddress: String
726
    let meterName: String
727
    let meterModel: String
728
    let observedAt: Date
729
    let voltageVolts: Double
730
    let currentAmps: Double
731
    let powerWatts: Double
732
    let selectedDataGroup: UInt8?
733
    let meterChargeCounterAh: Double?
734
    let meterEnergyCounterWh: Double?
735
    let fallbackStopThresholdAmps: Double
736
}