1 contributor
//
// ChargeInsightsModel.swift
// USB Meter
//
// Created by Codex on 10/04/2026.
//
import Foundation
enum ChargedDeviceClass: String, CaseIterable, Identifiable {
case iphone
case watch
case powerbank
case charger
case other
var id: String { rawValue }
var title: String {
switch self {
case .iphone:
return "iPhone"
case .watch:
return "Watch"
case .powerbank:
return "Powerbank"
case .charger:
return "Charger"
case .other:
return "Other"
}
}
var symbolName: String {
switch self {
case .iphone:
return "iphone"
case .watch:
return "applewatch"
case .powerbank:
return "battery.100.bolt"
case .charger:
return "bolt.badge.clock"
case .other:
return "shippingbox"
}
}
}
enum ChargeSessionStatus: String {
case active
case completed
case abandoned
var title: String {
rawValue.capitalized
}
}
enum ChargeSessionSourceMode: String {
case live
case offline
case blended
var title: String {
switch self {
case .live:
return "Live"
case .offline:
return "Offline Counters"
case .blended:
return "Blended"
}
}
}
enum ChargingTransportMode: String, CaseIterable, Identifiable {
case wired
case wireless
var id: String { rawValue }
var title: String {
switch self {
case .wired:
return "Wired"
case .wireless:
return "Wireless"
}
}
var symbolName: String {
switch self {
case .wired:
return "cable.connector"
case .wireless:
return "dot.radiowaves.left.and.right"
}
}
}
enum WirelessChargingProfile: String, CaseIterable, Identifiable {
case magsafe
case genericQi
var id: String { rawValue }
var title: String {
switch self {
case .magsafe:
return "MagSafe"
case .genericQi:
return "Generic Qi"
}
}
var description: String {
switch self {
case .magsafe:
return "Use separate wireless-efficiency calibration from devices that also have reliable wired capacity."
case .genericQi:
return "Use only automatic efficiency estimates and show a low-efficiency warning when needed."
}
}
}
struct ChargeCheckpointSummary: Identifiable, Hashable {
let id: UUID
let sessionID: UUID
let chargedDeviceID: UUID
let timestamp: Date
let batteryPercent: Double
let measuredEnergyWh: Double
let measuredChargeAh: Double
let currentAmps: Double
let voltageVolts: Double?
let label: String?
}
struct ChargeSessionSampleSummary: Identifiable, Hashable {
let sessionID: UUID
let chargedDeviceID: UUID
let bucketIndex: Int
let timestamp: Date
let averageCurrentAmps: Double
let averageVoltageVolts: Double?
let averagePowerWatts: Double
let measuredEnergyWh: Double
let measuredChargeAh: Double
let sampleCount: Int
var id: String {
"\(sessionID.uuidString)-\(bucketIndex)"
}
}
struct ChargeSessionSummary: Identifiable, Hashable {
let id: UUID
let chargedDeviceID: UUID
let chargerID: UUID?
let meterMACAddress: String?
let meterName: String?
let meterModel: String?
let startedAt: Date
let endedAt: Date?
let lastObservedAt: Date
let status: ChargeSessionStatus
let sourceMode: ChargeSessionSourceMode
let chargingTransportMode: ChargingTransportMode
let measuredEnergyWh: Double
let effectiveBatteryEnergyWh: Double?
let measuredChargeAh: Double
let minimumObservedCurrentAmps: Double?
let maximumObservedCurrentAmps: Double?
let maximumObservedPowerWatts: Double?
let maximumObservedVoltageVolts: Double?
let selectedSourceVoltageVolts: Double?
let completionCurrentAmps: Double?
let stopThresholdAmps: Double
let startBatteryPercent: Double?
let endBatteryPercent: Double?
let capacityEstimateWh: Double?
let wirelessEfficiencyFactor: Double?
let usesEstimatedWirelessEfficiency: Bool
let shouldWarnAboutLowWirelessEfficiency: Bool
let supportsChargingWhileOff: Bool
let usedOfflineMeterCounters: Bool
let targetBatteryPercent: Double?
let targetBatteryAlertTriggeredAt: Date?
let requiresCompletionConfirmation: Bool
let completionConfirmationRequestedAt: Date?
let completionContradictionPercent: Double?
let selectedDataGroup: UInt8?
let checkpoints: [ChargeCheckpointSummary]
let aggregatedSamples: [ChargeSessionSampleSummary]
var duration: TimeInterval {
(endedAt ?? lastObservedAt).timeIntervalSince(startedAt)
}
var effectiveOrMeasuredEnergyWh: Double {
effectiveBatteryEnergyWh ?? measuredEnergyWh
}
var batteryDeltaPercent: Double? {
guard let startBatteryPercent, let endBatteryPercent else { return nil }
return endBatteryPercent - startBatteryPercent
}
}
struct BatteryLevelPrediction: Hashable {
let predictedPercent: Double
let estimatedCapacityWh: Double
let anchorPercent: Double
let anchorEnergyWh: Double
let anchorDescription: String
}
struct CapacityTrendPoint: Identifiable, Hashable {
let sessionID: UUID
let timestamp: Date
let capacityWh: Double
let chargingTransportMode: ChargingTransportMode
var id: UUID { sessionID }
}
struct TypicalChargeCurvePoint: Identifiable, Hashable {
let percentBin: Int
let averageEnergyWh: Double
let averageChargeAh: Double
let sampleCount: Int
var id: Int { percentBin }
}
struct ChargedDeviceSummary: Identifiable, Hashable {
let id: UUID
let qrIdentifier: String
let name: String
let deviceClass: ChargedDeviceClass
let supportsChargingWhileOff: Bool
let supportsWiredCharging: Bool
let supportsWirelessCharging: Bool
let preferredChargingTransportMode: ChargingTransportMode
let wirelessChargingProfile: WirelessChargingProfile
let wirelessChargerEfficiencyFactor: Double?
let wiredChargeCompletionCurrentAmps: Double?
let wirelessChargeCompletionCurrentAmps: Double?
let chargerObservedVoltageSelections: [Double]
let chargerIdleCurrentAmps: Double?
let chargerEfficiencyFactor: Double?
let chargerMaximumPowerWatts: Double?
let notes: String?
let minimumCurrentAmps: Double?
let estimatedBatteryCapacityWh: Double?
let wiredMinimumCurrentAmps: Double?
let wirelessMinimumCurrentAmps: Double?
let wiredEstimatedBatteryCapacityWh: Double?
let wirelessEstimatedBatteryCapacityWh: Double?
let lastAssociatedMeterMAC: String?
let createdAt: Date
let updatedAt: Date
let sessions: [ChargeSessionSummary]
let capacityHistory: [CapacityTrendPoint]
let typicalCurve: [TypicalChargeCurvePoint]
var isCharger: Bool {
deviceClass == .charger
}
var activeSession: ChargeSessionSummary? {
sessions.first(where: { $0.status == .active })
}
var recentCompletedSessions: [ChargeSessionSummary] {
sessions.filter { $0.status == .completed }
}
var sessionCount: Int {
sessions.count
}
var supportedChargingModes: [ChargingTransportMode] {
var modes: [ChargingTransportMode] = []
if supportsWiredCharging {
modes.append(.wired)
}
if supportsWirelessCharging {
modes.append(.wireless)
}
return modes.isEmpty ? [preferredChargingTransportMode] : modes
}
func estimatedBatteryCapacityWh(for chargingTransportMode: ChargingTransportMode) -> Double? {
switch chargingTransportMode {
case .wired:
return wiredEstimatedBatteryCapacityWh
case .wireless:
return wirelessEstimatedBatteryCapacityWh
}
}
func minimumCurrentAmps(for chargingTransportMode: ChargingTransportMode) -> Double? {
switch chargingTransportMode {
case .wired:
return wiredMinimumCurrentAmps
case .wireless:
return wirelessMinimumCurrentAmps
}
}
func configuredCompletionCurrentAmps(for chargingTransportMode: ChargingTransportMode) -> Double? {
switch chargingTransportMode {
case .wired:
return wiredChargeCompletionCurrentAmps
case .wireless:
return wirelessChargeCompletionCurrentAmps
}
}
func resolvedCompletionCurrentAmps(for chargingTransportMode: ChargingTransportMode) -> Double? {
configuredCompletionCurrentAmps(for: chargingTransportMode)
?? minimumCurrentAmps(for: chargingTransportMode)
?? minimumCurrentAmps
}
func batteryLevelPrediction(for session: ChargeSessionSummary) -> BatteryLevelPrediction? {
let estimatedCapacityWh = session.capacityEstimateWh
?? estimatedBatteryCapacityWh(for: session.chargingTransportMode)
?? estimatedBatteryCapacityWh
guard let estimatedCapacityWh, estimatedCapacityWh > 0 else {
return nil
}
let effectiveEnergyWh = session.effectiveBatteryEnergyWh ?? session.measuredEnergyWh
struct Anchor {
let percent: Double
let energyWh: Double
let description: String
}
var anchors: [Anchor] = []
if let startBatteryPercent = session.startBatteryPercent {
anchors.append(
Anchor(
percent: startBatteryPercent,
energyWh: 0,
description: "session start"
)
)
}
anchors.append(
contentsOf: session.checkpoints
.sorted { lhs, rhs in
if lhs.measuredEnergyWh != rhs.measuredEnergyWh {
return lhs.measuredEnergyWh < rhs.measuredEnergyWh
}
return lhs.timestamp < rhs.timestamp
}
.map { checkpoint in
let trimmedLabel = checkpoint.label?.trimmingCharacters(in: .whitespacesAndNewlines)
return Anchor(
percent: checkpoint.batteryPercent,
energyWh: checkpoint.measuredEnergyWh,
description: trimmedLabel.map { "checkpoint \($0)" } ?? "last checkpoint"
)
}
)
guard !anchors.isEmpty else {
return nil
}
let eligibleAnchors = anchors.filter { $0.energyWh <= effectiveEnergyWh + 0.05 }
let anchor = eligibleAnchors.last ?? anchors.first!
let energyDeltaWh = max(effectiveEnergyWh - anchor.energyWh, 0)
let predictedPercent = min(
100,
max(
0,
anchor.percent + ((energyDeltaWh / estimatedCapacityWh) * 100)
)
)
return BatteryLevelPrediction(
predictedPercent: predictedPercent,
estimatedCapacityWh: estimatedCapacityWh,
anchorPercent: anchor.percent,
anchorEnergyWh: anchor.energyWh,
anchorDescription: anchor.description
)
}
}
struct ChargingMonitorSnapshot {
let meterMACAddress: String
let meterName: String
let meterModel: String
let observedAt: Date
let voltageVolts: Double
let currentAmps: Double
let powerWatts: Double
let selectedDataGroup: UInt8?
let meterChargeCounterAh: Double?
let meterEnergyCounterWh: Double?
let fallbackStopThresholdAmps: Double
}