// // 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? var flag: ChargeCheckpointFlag { ChargeCheckpointFlag.fromStoredLabel(label) } } enum ChargeCheckpointFlag: String, CaseIterable { case initial case intermediate case final var title: String { switch self { case .initial: return "Initial" case .intermediate: return "Intermediate" case .final: return "Final" } } var anchorDescription: String { switch self { case .initial: return "initial checkpoint" case .intermediate: return "intermediate checkpoint" case .final: return "final checkpoint" } } static func fromStoredLabel(_ label: String?) -> ChargeCheckpointFlag { let normalized = label? .trimmingCharacters(in: .whitespacesAndNewlines) .lowercased() switch normalized { case "initial", "start": return .initial case "final", "end": return .final case "intermediate", nil, "": return .intermediate default: return .intermediate } } } 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 ChargerStandbyPowerSample: Identifiable, Hashable, Codable { let timestamp: Date let powerWatts: Double let currentAmps: Double let voltageVolts: Double var id: TimeInterval { timestamp.timeIntervalSince1970 } } struct ChargerStandbyPowerDistributionBin: Identifiable, Hashable { let index: Int let lowerBoundWatts: Double let upperBoundWatts: Double let count: Int let relativeFrequency: Double var id: Int { index } } struct ChargerStandbyPowerMeasurementStatistics: Hashable { let sampleCount: Int let observedDuration: TimeInterval let averagePowerWatts: Double let recentAveragePowerWatts: Double let medianPowerWatts: Double let minimumPowerWatts: Double let maximumPowerWatts: Double let standardDeviationPowerWatts: Double let coefficientOfVariation: Double let averageCurrentAmps: Double let averageVoltageVolts: Double let stabilityDeltaWatts: Double let stabilityToleranceWatts: Double let histogram: [ChargerStandbyPowerDistributionBin] var projectedDailyEnergyWh: Double { averagePowerWatts * 24 } var projectedWeeklyEnergyWh: Double { averagePowerWatts * 24 * 7 } var projectedMonthlyEnergyWh: Double { averagePowerWatts * 24 * 30 } var projectedYearlyEnergyWh: Double { averagePowerWatts * 24 * 365 } var stabilityDeltaMilliwatts: Double { stabilityDeltaWatts * 1000 } var isStable: Bool { sampleCount >= ChargerStandbyPowerMeasurementAnalyzer.minimumStableSampleCount && stabilityDeltaWatts <= stabilityToleranceWatts } } struct ChargerStandbyPowerMeasurementSummary: Identifiable, Hashable, Codable { let id: UUID let chargerID: UUID let meterMACAddress: String let meterName: String? let meterModel: String? let startedAt: Date let endedAt: Date let sampleCount: Int let stabilizedAt: Date? let averagePowerWatts: Double let recentAveragePowerWatts: Double let medianPowerWatts: Double let minimumPowerWatts: Double let maximumPowerWatts: Double let standardDeviationPowerWatts: Double let coefficientOfVariation: Double let averageCurrentAmps: Double let averageVoltageVolts: Double let stabilityDeltaWatts: Double let stabilityToleranceWatts: Double let powerSamplesWatts: [Double] var duration: TimeInterval { endedAt.timeIntervalSince(startedAt) } var projectedDailyEnergyWh: Double { averagePowerWatts * 24 } var projectedWeeklyEnergyWh: Double { averagePowerWatts * 24 * 7 } var projectedMonthlyEnergyWh: Double { averagePowerWatts * 24 * 30 } var projectedYearlyEnergyWh: Double { averagePowerWatts * 24 * 365 } var isStable: Bool { stabilizedAt != nil } var histogram: [ChargerStandbyPowerDistributionBin] { ChargerStandbyPowerMeasurementAnalyzer.histogram(for: powerSamplesWatts) } } enum ChargerStandbyPowerMeasurementAnalyzer { static let minimumStableSampleCount = 45 static let recentSampleWindow = 20 static let minimumStabilityToleranceWatts = 0.003 static let relativeStabilityTolerance = 0.01 static func statistics( from samples: [ChargerStandbyPowerSample], startedAt: Date, referenceDate: Date = Date() ) -> ChargerStandbyPowerMeasurementStatistics? { guard !samples.isEmpty else { return nil } let powerValues = samples.map(\.powerWatts).filter(\.isFinite) let currentValues = samples.map(\.currentAmps).filter(\.isFinite) let voltageValues = samples.map(\.voltageVolts).filter(\.isFinite) guard powerValues.isEmpty == false else { return nil } let averagePower = mean(powerValues) let recentWindow = min(recentSampleWindow, max(1, powerValues.count)) let recentAveragePower = mean(Array(powerValues.suffix(recentWindow))) let stabilityDelta = abs(averagePower - recentAveragePower) let stabilityTolerance = max( minimumStabilityToleranceWatts, abs(averagePower) * relativeStabilityTolerance ) return ChargerStandbyPowerMeasurementStatistics( sampleCount: powerValues.count, observedDuration: max(referenceDate.timeIntervalSince(startedAt), 0), averagePowerWatts: averagePower, recentAveragePowerWatts: recentAveragePower, medianPowerWatts: median(powerValues), minimumPowerWatts: powerValues.min() ?? 0, maximumPowerWatts: powerValues.max() ?? 0, standardDeviationPowerWatts: standardDeviation(powerValues, mean: averagePower), coefficientOfVariation: coefficientOfVariation(powerValues, mean: averagePower), averageCurrentAmps: mean(currentValues), averageVoltageVolts: mean(voltageValues), stabilityDeltaWatts: stabilityDelta, stabilityToleranceWatts: stabilityTolerance, histogram: histogram(for: powerValues) ) } static func measurementSummary( chargerID: UUID, meterMACAddress: String, meterName: String?, meterModel: String?, startedAt: Date, endedAt: Date, samples: [ChargerStandbyPowerSample], stabilizedAt: Date? ) -> ChargerStandbyPowerMeasurementSummary? { guard let statistics = statistics(from: samples, startedAt: startedAt, referenceDate: endedAt) else { return nil } return ChargerStandbyPowerMeasurementSummary( id: UUID(), chargerID: chargerID, meterMACAddress: meterMACAddress, meterName: meterName, meterModel: meterModel, startedAt: startedAt, endedAt: endedAt, sampleCount: statistics.sampleCount, stabilizedAt: stabilizedAt, averagePowerWatts: statistics.averagePowerWatts, recentAveragePowerWatts: statistics.recentAveragePowerWatts, medianPowerWatts: statistics.medianPowerWatts, minimumPowerWatts: statistics.minimumPowerWatts, maximumPowerWatts: statistics.maximumPowerWatts, standardDeviationPowerWatts: statistics.standardDeviationPowerWatts, coefficientOfVariation: statistics.coefficientOfVariation, averageCurrentAmps: statistics.averageCurrentAmps, averageVoltageVolts: statistics.averageVoltageVolts, stabilityDeltaWatts: statistics.stabilityDeltaWatts, stabilityToleranceWatts: statistics.stabilityToleranceWatts, powerSamplesWatts: samples.map(\.powerWatts) ) } static func histogram(for values: [Double], preferredBinCount: Int? = nil) -> [ChargerStandbyPowerDistributionBin] { let finiteValues = values.filter(\.isFinite) guard finiteValues.isEmpty == false else { return [] } let minimum = finiteValues.min() ?? 0 let maximum = finiteValues.max() ?? 0 let spread = maximum - minimum let binCount = preferredBinCount ?? min(18, max(8, Int(Double(finiteValues.count).squareRoot().rounded()))) guard spread > 0 else { return [ ChargerStandbyPowerDistributionBin( index: 0, lowerBoundWatts: minimum, upperBoundWatts: maximum, count: finiteValues.count, relativeFrequency: 1 ) ] } let safeBinCount = max(1, binCount) let binWidth = spread / Double(safeBinCount) var counts = Array(repeating: 0, count: safeBinCount) for value in finiteValues { let normalizedIndex = Int(((value - minimum) / binWidth).rounded(.down)) let safeIndex = min(max(normalizedIndex, 0), safeBinCount - 1) counts[safeIndex] += 1 } return counts.enumerated().map { index, count in let lowerBound = minimum + (Double(index) * binWidth) let upperBound = index == safeBinCount - 1 ? maximum : lowerBound + binWidth return ChargerStandbyPowerDistributionBin( index: index, lowerBoundWatts: lowerBound, upperBoundWatts: upperBound, count: count, relativeFrequency: Double(count) / Double(finiteValues.count) ) } } private static func mean(_ values: [Double]) -> Double { guard values.isEmpty == false else { return 0 } return values.reduce(0, +) / Double(values.count) } private static func median(_ values: [Double]) -> Double { guard values.isEmpty == false else { return 0 } let sorted = values.sorted() let middleIndex = sorted.count / 2 if sorted.count.isMultiple(of: 2) { return (sorted[middleIndex - 1] + sorted[middleIndex]) / 2 } return sorted[middleIndex] } private static func standardDeviation(_ values: [Double], mean: Double) -> Double { guard values.count > 1 else { return 0 } let variance = values.reduce(0) { partialResult, value in let delta = value - mean return partialResult + (delta * delta) } / Double(values.count) return variance.squareRoot() } private static func coefficientOfVariation(_ values: [Double], mean: Double) -> Double { guard abs(mean) > 0.000_001 else { return 0 } return standardDeviation(values, mean: mean) / abs(mean) } } 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] let standbyPowerMeasurements: [ChargerStandbyPowerMeasurementSummary] 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 latestStandbyPowerMeasurement: ChargerStandbyPowerMeasurementSummary? { standbyPowerMeasurements.first } 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 return Anchor( percent: checkpoint.batteryPercent, energyWh: checkpoint.measuredEnergyWh, timestamp: checkpoint.timestamp, description: checkpoint.flag.anchorDescription, 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 ) } func withStandbyPowerMeasurements(_ measurements: [ChargerStandbyPowerMeasurementSummary]) -> ChargedDeviceSummary { ChargedDeviceSummary( id: id, qrIdentifier: qrIdentifier, name: name, deviceClass: deviceClass, supportsChargingWhileOff: supportsChargingWhileOff, chargingStateAvailability: chargingStateAvailability, supportsWiredCharging: supportsWiredCharging, supportsWirelessCharging: supportsWirelessCharging, wirelessChargingProfile: wirelessChargingProfile, configuredCompletionCurrents: configuredCompletionCurrents, learnedCompletionCurrents: learnedCompletionCurrents, wirelessChargerEfficiencyFactor: wirelessChargerEfficiencyFactor, wiredChargeCompletionCurrentAmps: wiredChargeCompletionCurrentAmps, wirelessChargeCompletionCurrentAmps: wirelessChargeCompletionCurrentAmps, chargerObservedVoltageSelections: chargerObservedVoltageSelections, chargerIdleCurrentAmps: chargerIdleCurrentAmps, chargerEfficiencyFactor: chargerEfficiencyFactor, chargerMaximumPowerWatts: chargerMaximumPowerWatts, notes: notes, minimumCurrentAmps: minimumCurrentAmps, estimatedBatteryCapacityWh: estimatedBatteryCapacityWh, wiredMinimumCurrentAmps: wiredMinimumCurrentAmps, wirelessMinimumCurrentAmps: wirelessMinimumCurrentAmps, wiredEstimatedBatteryCapacityWh: wiredEstimatedBatteryCapacityWh, wirelessEstimatedBatteryCapacityWh: wirelessEstimatedBatteryCapacityWh, lastAssociatedMeterMAC: lastAssociatedMeterMAC, createdAt: createdAt, updatedAt: updatedAt, sessions: sessions, capacityHistory: capacityHistory, typicalCurve: typicalCurve, standbyPowerMeasurements: measurements ) } } 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 }