USB-Meter / USB Meter / Model / ChargeInsightsModel.swift
Newer Older
753 lines | 21.215kb
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 meterDurationBaselineSeconds: Double?
366
    let meterLastDurationSeconds: Double?
Bogdan Timofte authored a month ago
367
    let minimumObservedCurrentAmps: Double?
368
    let maximumObservedCurrentAmps: Double?
369
    let maximumObservedPowerWatts: Double?
370
    let maximumObservedVoltageVolts: Double?
371
    let selectedSourceVoltageVolts: Double?
372
    let completionCurrentAmps: Double?
373
    let stopThresholdAmps: Double
374
    let startBatteryPercent: Double?
375
    let endBatteryPercent: Double?
376
    let capacityEstimateWh: Double?
377
    let wirelessEfficiencyFactor: Double?
378
    let usesEstimatedWirelessEfficiency: Bool
379
    let shouldWarnAboutLowWirelessEfficiency: Bool
380
    let supportsChargingWhileOff: Bool
381
    let usedOfflineMeterCounters: Bool
382
    let targetBatteryPercent: Double?
383
    let targetBatteryAlertTriggeredAt: Date?
384
    let requiresCompletionConfirmation: Bool
385
    let completionConfirmationRequestedAt: Date?
386
    let completionContradictionPercent: Double?
387
    let selectedDataGroup: UInt8?
388
    let checkpoints: [ChargeCheckpointSummary]
389
    let aggregatedSamples: [ChargeSessionSampleSummary]
390

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

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

            
Bogdan Timofte authored a month ago
402
    var meterObservedDuration: TimeInterval? {
403
        guard let meterDurationBaselineSeconds, let meterLastDurationSeconds else {
404
            return nil
405
        }
406
        guard meterLastDurationSeconds >= meterDurationBaselineSeconds else {
407
            return nil
408
        }
409
        return meterLastDurationSeconds - meterDurationBaselineSeconds
410
    }
411

            
412
    var effectiveDuration: TimeInterval {
413
        meterObservedDuration ?? duration
414
    }
415

            
Bogdan Timofte authored a month ago
416
    var effectiveOrMeasuredEnergyWh: Double {
417
        effectiveBatteryEnergyWh ?? measuredEnergyWh
418
    }
419

            
420
    var batteryDeltaPercent: Double? {
Bogdan Timofte authored a month ago
421
        guard let startBatteryPercent, let endBatteryPercent,
422
              startBatteryPercent >= 0, endBatteryPercent >= 0 else { return nil }
Bogdan Timofte authored a month ago
423
        return endBatteryPercent - startBatteryPercent
424
    }
Bogdan Timofte authored a month ago
425

            
426
    var canAutoStop: Bool {
427
        autoStopEnabled && stopThresholdAmps > 0
428
    }
429

            
430
    var isPaused: Bool {
431
        status == .paused
432
    }
433

            
434
    var isOpen: Bool {
435
        status.isOpen
436
    }
Bogdan Timofte authored a month ago
437
}
438

            
439
struct BatteryLevelPrediction: Hashable {
440
    let predictedPercent: Double
441
    let estimatedCapacityWh: Double
442
    let anchorPercent: Double
443
    let anchorEnergyWh: Double
444
    let anchorDescription: String
445
}
446

            
Bogdan Timofte authored a month ago
447
enum BatteryLevelPredictionTuning {
448
    static let checkpointSettleDuration: TimeInterval = 10 * 60
449

            
450
    static func predictedPercent(
451
        anchorPercent: Double,
452
        anchorEnergyWh: Double,
453
        anchorTimestamp: Date,
454
        anchorIsCheckpoint: Bool,
455
        effectiveEnergyWh: Double,
456
        referenceTimestamp: Date,
457
        estimatedCapacityWh: Double
458
    ) -> Double {
459
        let energyDeltaWh = max(effectiveEnergyWh - anchorEnergyWh, 0)
460
        let rawGainPercent = (energyDeltaWh / estimatedCapacityWh) * 100
461
        let stabilizedGainPercent: Double
462

            
463
        if anchorIsCheckpoint {
464
            let elapsedSinceAnchor = max(referenceTimestamp.timeIntervalSince(anchorTimestamp), 0)
465
            let settleProgress = min(max(elapsedSinceAnchor / checkpointSettleDuration, 0), 1)
466
            stabilizedGainPercent = rawGainPercent * settleProgress
467
        } else {
468
            stabilizedGainPercent = rawGainPercent
469
        }
470

            
471
        return min(
472
            100,
473
            max(
474
                0,
475
                anchorPercent + stabilizedGainPercent
476
            )
477
        )
478
    }
479
}
480

            
Bogdan Timofte authored a month ago
481
struct CapacityTrendPoint: Identifiable, Hashable {
482
    let sessionID: UUID
483
    let timestamp: Date
484
    let capacityWh: Double
485
    let chargingTransportMode: ChargingTransportMode
486

            
487
    var id: UUID { sessionID }
488
}
489

            
490
struct TypicalChargeCurvePoint: Identifiable, Hashable {
491
    let percentBin: Int
492
    let averageEnergyWh: Double
493
    let averageChargeAh: Double
494
    let sampleCount: Int
495

            
496
    var id: Int { percentBin }
497
}
498

            
499
struct ChargedDeviceSummary: Identifiable, Hashable {
500
    let id: UUID
501
    let qrIdentifier: String
502
    let name: String
503
    let deviceClass: ChargedDeviceClass
504
    let supportsChargingWhileOff: Bool
Bogdan Timofte authored a month ago
505
    let chargingStateAvailability: ChargingStateAvailability
Bogdan Timofte authored a month ago
506
    let supportsWiredCharging: Bool
507
    let supportsWirelessCharging: Bool
508
    let wirelessChargingProfile: WirelessChargingProfile
Bogdan Timofte authored a month ago
509
    let configuredCompletionCurrents: [ChargeSessionKind: Double]
510
    let learnedCompletionCurrents: [ChargeSessionKind: Double]
Bogdan Timofte authored a month ago
511
    let wirelessChargerEfficiencyFactor: Double?
512
    let wiredChargeCompletionCurrentAmps: Double?
513
    let wirelessChargeCompletionCurrentAmps: Double?
514
    let chargerObservedVoltageSelections: [Double]
515
    let chargerIdleCurrentAmps: Double?
516
    let chargerEfficiencyFactor: Double?
517
    let chargerMaximumPowerWatts: Double?
518
    let notes: String?
519
    let minimumCurrentAmps: Double?
520
    let estimatedBatteryCapacityWh: Double?
521
    let wiredMinimumCurrentAmps: Double?
522
    let wirelessMinimumCurrentAmps: Double?
523
    let wiredEstimatedBatteryCapacityWh: Double?
524
    let wirelessEstimatedBatteryCapacityWh: Double?
525
    let lastAssociatedMeterMAC: String?
526
    let createdAt: Date
527
    let updatedAt: Date
528
    let sessions: [ChargeSessionSummary]
529
    let capacityHistory: [CapacityTrendPoint]
530
    let typicalCurve: [TypicalChargeCurvePoint]
531

            
532
    var isCharger: Bool {
533
        deviceClass == .charger
534
    }
535

            
Bogdan Timofte authored a month ago
536
    var kind: ChargedDeviceKind {
537
        deviceClass.kind
538
    }
539

            
540
    var identityTitle: String {
541
        isCharger ? kind.title : deviceClass.title
542
    }
543

            
544
    var identitySymbolName: String {
545
        isCharger ? kind.symbolName : deviceClass.symbolName
546
    }
547

            
Bogdan Timofte authored a month ago
548
    var activeSession: ChargeSessionSummary? {
Bogdan Timofte authored a month ago
549
        sessions.first(where: \.isOpen)
Bogdan Timofte authored a month ago
550
    }
551

            
552
    var recentCompletedSessions: [ChargeSessionSummary] {
553
        sessions.filter { $0.status == .completed }
554
    }
555

            
556
    var sessionCount: Int {
557
        sessions.count
558
    }
559

            
560
    var supportedChargingModes: [ChargingTransportMode] {
561
        var modes: [ChargingTransportMode] = []
562
        if supportsWiredCharging {
563
            modes.append(.wired)
564
        }
565
        if supportsWirelessCharging {
566
            modes.append(.wireless)
567
        }
Bogdan Timofte authored a month ago
568
        return modes
Bogdan Timofte authored a month ago
569
    }
570

            
Bogdan Timofte authored a month ago
571
    var supportedChargingStateModes: [ChargingStateMode] {
572
        chargingStateAvailability.supportedModes
573
    }
574

            
575
    func defaultChargingStateMode(for chargingTransportMode: ChargingTransportMode) -> ChargingStateMode {
576
        if let matchingSession = sessions.first(where: {
577
            $0.status.isOpen && $0.chargingTransportMode == chargingTransportMode
578
        }) {
579
            return matchingSession.chargingStateMode
580
        }
581
        return chargingStateAvailability.supportedModes.first ?? .on
582
    }
583

            
584
    func sessionKind(
585
        for chargingTransportMode: ChargingTransportMode,
586
        chargingStateMode: ChargingStateMode? = nil
587
    ) -> ChargeSessionKind {
588
        ChargeSessionKind(
589
            chargingTransportMode: chargingTransportMode,
590
            chargingStateMode: chargingStateMode ?? defaultChargingStateMode(for: chargingTransportMode)
591
        )
592
    }
593

            
Bogdan Timofte authored a month ago
594
    func estimatedBatteryCapacityWh(for chargingTransportMode: ChargingTransportMode) -> Double? {
595
        switch chargingTransportMode {
596
        case .wired:
597
            return wiredEstimatedBatteryCapacityWh
598
        case .wireless:
599
            return wirelessEstimatedBatteryCapacityWh
600
        }
601
    }
602

            
603
    func minimumCurrentAmps(for chargingTransportMode: ChargingTransportMode) -> Double? {
604
        switch chargingTransportMode {
605
        case .wired:
606
            return wiredMinimumCurrentAmps
607
        case .wireless:
608
            return wirelessMinimumCurrentAmps
609
        }
610
    }
611

            
Bogdan Timofte authored a month ago
612
    func configuredCompletionCurrentAmps(for sessionKind: ChargeSessionKind) -> Double? {
613
        if let explicitCurrent = configuredCompletionCurrents[sessionKind] {
614
            return explicitCurrent
615
        }
616

            
617
        switch sessionKind.chargingTransportMode {
Bogdan Timofte authored a month ago
618
        case .wired:
619
            return wiredChargeCompletionCurrentAmps
620
        case .wireless:
621
            return wirelessChargeCompletionCurrentAmps
622
        }
623
    }
624

            
Bogdan Timofte authored a month ago
625
    func learnedCompletionCurrentAmps(for sessionKind: ChargeSessionKind) -> Double? {
626
        if let learnedCurrent = learnedCompletionCurrents[sessionKind] {
627
            return learnedCurrent
628
        }
629

            
630
        switch sessionKind.chargingTransportMode {
631
        case .wired:
632
            return wiredMinimumCurrentAmps ?? minimumCurrentAmps
633
        case .wireless:
634
            return wirelessMinimumCurrentAmps ?? minimumCurrentAmps
635
        }
636
    }
637

            
638
    func resolvedCompletionCurrentAmps(
639
        for chargingTransportMode: ChargingTransportMode,
640
        chargingStateMode: ChargingStateMode? = nil
641
    ) -> Double? {
642
        let sessionKind = sessionKind(
643
            for: chargingTransportMode,
644
            chargingStateMode: chargingStateMode
645
        )
646

            
647
        return configuredCompletionCurrentAmps(for: sessionKind)
648
            ?? learnedCompletionCurrentAmps(for: sessionKind)
Bogdan Timofte authored a month ago
649
            ?? minimumCurrentAmps(for: chargingTransportMode)
650
            ?? minimumCurrentAmps
651
    }
652

            
Bogdan Timofte authored a month ago
653
    func batteryLevelPrediction(
654
        for session: ChargeSessionSummary,
655
        effectiveEnergyWhOverride: Double? = nil
656
    ) -> BatteryLevelPrediction? {
Bogdan Timofte authored a month ago
657
        let estimatedCapacityWh = session.capacityEstimateWh
658
            ?? estimatedBatteryCapacityWh(for: session.chargingTransportMode)
659
            ?? estimatedBatteryCapacityWh
660

            
661
        guard let estimatedCapacityWh, estimatedCapacityWh > 0 else {
662
            return nil
663
        }
664

            
Bogdan Timofte authored a month ago
665
        let effectiveEnergyWh = effectiveEnergyWhOverride
666
            ?? session.effectiveBatteryEnergyWh
667
            ?? session.measuredEnergyWh
Bogdan Timofte authored a month ago
668

            
669
        struct Anchor {
670
            let percent: Double
671
            let energyWh: Double
Bogdan Timofte authored a month ago
672
            let timestamp: Date
Bogdan Timofte authored a month ago
673
            let description: String
Bogdan Timofte authored a month ago
674
            let isCheckpoint: Bool
Bogdan Timofte authored a month ago
675
        }
676

            
677
        var anchors: [Anchor] = []
678

            
Bogdan Timofte authored a month ago
679
        if let startBatteryPercent = session.startBatteryPercent, startBatteryPercent >= 0 {
Bogdan Timofte authored a month ago
680
            anchors.append(
681
                Anchor(
682
                    percent: startBatteryPercent,
683
                    energyWh: 0,
Bogdan Timofte authored a month ago
684
                    timestamp: session.startedAt,
685
                    description: "session start",
686
                    isCheckpoint: false
Bogdan Timofte authored a month ago
687
                )
688
            )
689
        }
690

            
691
        anchors.append(
692
            contentsOf: session.checkpoints
693
                .sorted { lhs, rhs in
694
                    if lhs.measuredEnergyWh != rhs.measuredEnergyWh {
695
                        return lhs.measuredEnergyWh < rhs.measuredEnergyWh
696
                    }
697
                    return lhs.timestamp < rhs.timestamp
698
                }
Bogdan Timofte authored a month ago
699
                .filter { checkpoint in
700
                    checkpoint.batteryPercent >= 0
701
                }
Bogdan Timofte authored a month ago
702
                .map { checkpoint in
703
                    let trimmedLabel = checkpoint.label?.trimmingCharacters(in: .whitespacesAndNewlines)
704
                    return Anchor(
705
                        percent: checkpoint.batteryPercent,
706
                        energyWh: checkpoint.measuredEnergyWh,
Bogdan Timofte authored a month ago
707
                        timestamp: checkpoint.timestamp,
708
                        description: trimmedLabel.map { "checkpoint \($0)" } ?? "last checkpoint",
709
                        isCheckpoint: true
Bogdan Timofte authored a month ago
710
                    )
711
                }
712
        )
713

            
714
        guard !anchors.isEmpty else {
715
            return nil
716
        }
717

            
718
        let eligibleAnchors = anchors.filter { $0.energyWh <= effectiveEnergyWh + 0.05 }
719
        let anchor = eligibleAnchors.last ?? anchors.first!
Bogdan Timofte authored a month ago
720
        let predictedPercent = BatteryLevelPredictionTuning.predictedPercent(
721
            anchorPercent: anchor.percent,
722
            anchorEnergyWh: anchor.energyWh,
723
            anchorTimestamp: anchor.timestamp,
724
            anchorIsCheckpoint: anchor.isCheckpoint,
725
            effectiveEnergyWh: effectiveEnergyWh,
726
            referenceTimestamp: session.lastObservedAt,
727
            estimatedCapacityWh: estimatedCapacityWh
Bogdan Timofte authored a month ago
728
        )
729

            
730
        return BatteryLevelPrediction(
731
            predictedPercent: predictedPercent,
732
            estimatedCapacityWh: estimatedCapacityWh,
733
            anchorPercent: anchor.percent,
734
            anchorEnergyWh: anchor.energyWh,
735
            anchorDescription: anchor.description
736
        )
737
    }
738
}
739

            
740
struct ChargingMonitorSnapshot {
741
    let meterMACAddress: String
742
    let meterName: String
743
    let meterModel: String
744
    let observedAt: Date
745
    let voltageVolts: Double
746
    let currentAmps: Double
747
    let powerWatts: Double
748
    let selectedDataGroup: UInt8?
749
    let meterChargeCounterAh: Double?
750
    let meterEnergyCounterWh: Double?
Bogdan Timofte authored a month ago
751
    let meterRecordingDurationSeconds: TimeInterval?
Bogdan Timofte authored a month ago
752
    let fallbackStopThresholdAmps: Double
753
}