// // ChargeInsightsModel.swift // USB Meter // // Created by Codex on 10/04/2026. // import Foundation enum ChargedDeviceKind: String, Identifiable { case device case charger var id: String { rawValue } var title: String { switch self { case .device: return "Device" case .charger: return "Charger" } } var pluralTitle: String { switch self { case .device: return "Devices" case .charger: return "Chargers" } } var symbolName: String { switch self { case .device: return "iphone" case .charger: return "bolt.horizontal.circle" } } } enum ChargedDeviceClass: String, CaseIterable, Identifiable { case iphone case watch case powerbank case charger case other var id: String { rawValue } static var deviceCases: [ChargedDeviceClass] { allCases.filter { $0 != .charger } } var kind: ChargedDeviceKind { self == .charger ? .charger : .device } 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 paused case completed case abandoned var title: String { switch self { case .active: return "Active" case .paused: return "Paused" case .completed: return "Completed" case .abandoned: return "Abandoned" } } var isOpen: Bool { switch self { case .active, .paused: return true case .completed, .abandoned: return false } } } 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, Codable { 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 ChargingStateMode: String, CaseIterable, Identifiable, Codable { case on case off var id: String { rawValue } var title: String { switch self { case .on: return "On" case .off: return "Off" } } var description: String { switch self { case .on: return "Device stays powered on while charging." case .off: return "Device is powered off while charging." } } } enum ChargingStateAvailability: String, CaseIterable, Identifiable, Codable { case onOnly case onOrOff case offOnly var id: String { rawValue } var title: String { switch self { case .onOnly: return "On Only" case .onOrOff: return "On or Off" case .offOnly: return "Off Only" } } var description: String { switch self { case .onOnly: return "The device can be recorded only while it is powered on." case .onOrOff: return "The session must specify whether the device is on or off." case .offOnly: return "The device can be recorded only while it is powered off." } } var supportedModes: [ChargingStateMode] { switch self { case .onOnly: return [.on] case .onOrOff: return [.on, .off] case .offOnly: return [.off] } } var supportsMultipleModes: Bool { supportedModes.count > 1 } var supportsChargingWhileOff: Bool { self != .onOnly } static func fallback(for supportsChargingWhileOff: Bool) -> ChargingStateAvailability { supportsChargingWhileOff ? .onOrOff : .onOnly } } enum ChargeSessionKind: String, CaseIterable, Identifiable, Codable, Hashable { case wiredOn case wiredOff case wirelessOn case wirelessOff var id: String { rawValue } init(chargingTransportMode: ChargingTransportMode, chargingStateMode: ChargingStateMode) { switch (chargingTransportMode, chargingStateMode) { case (.wired, .on): self = .wiredOn case (.wired, .off): self = .wiredOff case (.wireless, .on): self = .wirelessOn case (.wireless, .off): self = .wirelessOff } } var chargingTransportMode: ChargingTransportMode { switch self { case .wiredOn, .wiredOff: return .wired case .wirelessOn, .wirelessOff: return .wireless } } var chargingStateMode: ChargingStateMode { switch self { case .wiredOn, .wirelessOn: return .on case .wiredOff, .wirelessOff: return .off } } var title: String { "\(chargingTransportMode.title) • \(chargingStateMode.title)" } var shortTitle: String { "\(chargingTransportMode.title) \(chargingStateMode.title)" } } 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 pausedAt: Date? let status: ChargeSessionStatus let sourceMode: ChargeSessionSourceMode let chargingTransportMode: ChargingTransportMode let chargingStateMode: ChargingStateMode let autoStopEnabled: Bool let measuredEnergyWh: Double let effectiveBatteryEnergyWh: Double? let measuredChargeAh: Double let meterEnergyBaselineWh: Double? let meterChargeBaselineAh: Double? let meterDurationBaselineSeconds: Double? let meterLastDurationSeconds: 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 sessionKind: ChargeSessionKind { ChargeSessionKind( chargingTransportMode: chargingTransportMode, chargingStateMode: chargingStateMode ) } var duration: TimeInterval { (endedAt ?? lastObservedAt).timeIntervalSince(startedAt) } var meterObservedDuration: TimeInterval? { guard let meterDurationBaselineSeconds, let meterLastDurationSeconds else { return nil } guard meterLastDurationSeconds >= meterDurationBaselineSeconds else { return nil } return meterLastDurationSeconds - meterDurationBaselineSeconds } var effectiveDuration: TimeInterval { meterObservedDuration ?? duration } var effectiveOrMeasuredEnergyWh: Double { effectiveBatteryEnergyWh ?? measuredEnergyWh } var batteryDeltaPercent: Double? { guard let startBatteryPercent, let endBatteryPercent, startBatteryPercent >= 0, endBatteryPercent >= 0 else { return nil } return endBatteryPercent - startBatteryPercent } var canAutoStop: Bool { autoStopEnabled && stopThresholdAmps > 0 } var isPaused: Bool { status == .paused } var isOpen: Bool { status.isOpen } } struct BatteryLevelPrediction: Hashable { let predictedPercent: Double let estimatedCapacityWh: Double let anchorPercent: Double let anchorEnergyWh: Double let anchorDescription: String } enum BatteryLevelPredictionTuning { static let checkpointSettleDuration: TimeInterval = 10 * 60 static func predictedPercent( anchorPercent: Double, anchorEnergyWh: Double, anchorTimestamp: Date, anchorIsCheckpoint: Bool, effectiveEnergyWh: Double, referenceTimestamp: Date, estimatedCapacityWh: Double ) -> Double { let energyDeltaWh = max(effectiveEnergyWh - anchorEnergyWh, 0) let rawGainPercent = (energyDeltaWh / estimatedCapacityWh) * 100 let stabilizedGainPercent: Double if anchorIsCheckpoint { let elapsedSinceAnchor = max(referenceTimestamp.timeIntervalSince(anchorTimestamp), 0) let settleProgress = min(max(elapsedSinceAnchor / checkpointSettleDuration, 0), 1) stabilizedGainPercent = rawGainPercent * settleProgress } else { stabilizedGainPercent = rawGainPercent } return min( 100, max( 0, anchorPercent + stabilizedGainPercent ) ) } } 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 chargingStateAvailability: ChargingStateAvailability let supportsWiredCharging: Bool let supportsWirelessCharging: Bool let wirelessChargingProfile: WirelessChargingProfile let configuredCompletionCurrents: [ChargeSessionKind: Double] let learnedCompletionCurrents: [ChargeSessionKind: Double] 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 kind: ChargedDeviceKind { deviceClass.kind } var identityTitle: String { isCharger ? kind.title : deviceClass.title } var identitySymbolName: String { isCharger ? kind.symbolName : deviceClass.symbolName } var activeSession: ChargeSessionSummary? { sessions.first(where: \.isOpen) } 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 } var supportedChargingStateModes: [ChargingStateMode] { chargingStateAvailability.supportedModes } func defaultChargingStateMode(for chargingTransportMode: ChargingTransportMode) -> ChargingStateMode { if let matchingSession = sessions.first(where: { $0.status.isOpen && $0.chargingTransportMode == chargingTransportMode }) { return matchingSession.chargingStateMode } return chargingStateAvailability.supportedModes.first ?? .on } func sessionKind( for chargingTransportMode: ChargingTransportMode, chargingStateMode: ChargingStateMode? = nil ) -> ChargeSessionKind { ChargeSessionKind( chargingTransportMode: chargingTransportMode, chargingStateMode: chargingStateMode ?? defaultChargingStateMode(for: chargingTransportMode) ) } 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 sessionKind: ChargeSessionKind) -> Double? { if let explicitCurrent = configuredCompletionCurrents[sessionKind] { return explicitCurrent } switch sessionKind.chargingTransportMode { case .wired: return wiredChargeCompletionCurrentAmps case .wireless: return wirelessChargeCompletionCurrentAmps } } func learnedCompletionCurrentAmps(for sessionKind: ChargeSessionKind) -> Double? { if let learnedCurrent = learnedCompletionCurrents[sessionKind] { return learnedCurrent } switch sessionKind.chargingTransportMode { case .wired: return wiredMinimumCurrentAmps ?? minimumCurrentAmps case .wireless: return wirelessMinimumCurrentAmps ?? minimumCurrentAmps } } func resolvedCompletionCurrentAmps( for chargingTransportMode: ChargingTransportMode, chargingStateMode: ChargingStateMode? = nil ) -> Double? { let sessionKind = sessionKind( for: chargingTransportMode, chargingStateMode: chargingStateMode ) return configuredCompletionCurrentAmps(for: sessionKind) ?? learnedCompletionCurrentAmps(for: sessionKind) ?? minimumCurrentAmps(for: chargingTransportMode) ?? minimumCurrentAmps } func batteryLevelPrediction( for session: ChargeSessionSummary, effectiveEnergyWhOverride: Double? = nil ) -> BatteryLevelPrediction? { let estimatedCapacityWh = session.capacityEstimateWh ?? estimatedBatteryCapacityWh(for: session.chargingTransportMode) ?? estimatedBatteryCapacityWh guard let estimatedCapacityWh, estimatedCapacityWh > 0 else { return nil } let effectiveEnergyWh = effectiveEnergyWhOverride ?? session.effectiveBatteryEnergyWh ?? session.measuredEnergyWh struct Anchor { let percent: Double let energyWh: Double let timestamp: Date let description: String let isCheckpoint: Bool } var anchors: [Anchor] = [] if let startBatteryPercent = session.startBatteryPercent, startBatteryPercent >= 0 { anchors.append( Anchor( percent: startBatteryPercent, energyWh: 0, timestamp: session.startedAt, description: "session start", isCheckpoint: false ) ) } anchors.append( contentsOf: session.checkpoints .sorted { lhs, rhs in if lhs.measuredEnergyWh != rhs.measuredEnergyWh { return lhs.measuredEnergyWh < rhs.measuredEnergyWh } return lhs.timestamp < rhs.timestamp } .filter { checkpoint in checkpoint.batteryPercent >= 0 } .map { checkpoint in let trimmedLabel = checkpoint.label?.trimmingCharacters(in: .whitespacesAndNewlines) return Anchor( percent: checkpoint.batteryPercent, energyWh: checkpoint.measuredEnergyWh, timestamp: checkpoint.timestamp, description: trimmedLabel.map { "checkpoint \($0)" } ?? "last checkpoint", isCheckpoint: true ) } ) guard !anchors.isEmpty else { return nil } let eligibleAnchors = anchors.filter { $0.energyWh <= effectiveEnergyWh + 0.05 } let anchor = eligibleAnchors.last ?? anchors.first! let predictedPercent = BatteryLevelPredictionTuning.predictedPercent( anchorPercent: anchor.percent, anchorEnergyWh: anchor.energyWh, anchorTimestamp: anchor.timestamp, anchorIsCheckpoint: anchor.isCheckpoint, effectiveEnergyWh: effectiveEnergyWh, referenceTimestamp: session.lastObservedAt, estimatedCapacityWh: estimatedCapacityWh ) 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 meterRecordingDurationSeconds: TimeInterval? let fallbackStopThresholdAmps: Double }