// // 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 }