USB-Meter / USB Meter / Model / ChargeInsightsModel.swift
1 contributor
1688 lines | 56.135kb
//
//  ChargeInsightsModel.swift
//  USB Meter
//
//  Created by Codex on 10/04/2026.
//

import Foundation

enum ChargedDeviceKind: String, Identifiable, Codable {
    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, Codable {
    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"
        }
    }

    var enforcedChargingSupport: (wired: Bool, wireless: Bool)? {
        switch self {
        case .watch:
            return (wired: false, wireless: true)
        case .powerbank:
            return (wired: true, wireless: false)
        case .charger:
            return (wired: false, wireless: true)
        case .iphone, .other:
            return nil
        }
    }

    var enforcedChargingStateAvailability: ChargingStateAvailability? {
        switch self {
        case .watch:
            return .onOnly
        case .powerbank:
            return .offOnly
        case .charger:
            return .onOnly
        case .iphone, .other:
            return nil
        }
    }

    var defaultChargingSupport: (wired: Bool, wireless: Bool) {
        if let enforcedChargingSupport {
            return enforcedChargingSupport
        }

        switch self {
        case .iphone:
            return (wired: true, wireless: true)
        case .watch:
            return (wired: false, wireless: true)
        case .powerbank:
            return (wired: true, wireless: false)
        case .charger:
            return (wired: false, wireless: true)
        case .other:
            return (wired: true, wireless: false)
        }
    }

    var defaultChargingStateAvailability: ChargingStateAvailability {
        enforcedChargingStateAvailability ?? {
            switch self {
            case .iphone:
                return .onOrOff
            case .watch:
                return .onOnly
            case .powerbank:
                return .offOnly
            case .charger, .other:
                return .onOrOff
            }
        }()
    }

    func normalizedChargingSupport(
        supportsWiredCharging: Bool,
        supportsWirelessCharging: Bool
    ) -> (wired: Bool, wireless: Bool) {
        enforcedChargingSupport ?? (wired: supportsWiredCharging, wireless: supportsWirelessCharging)
    }

    func normalizedChargingStateAvailability(
        _ chargingStateAvailability: ChargingStateAvailability
    ) -> ChargingStateAvailability {
        enforcedChargingStateAvailability ?? chargingStateAvailability
    }
}

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, Codable {
    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."
        }
    }
}

enum ChargerType: String, CaseIterable, Identifiable, Codable {
    case appleMagSafe
    case appleWatch
    case genericMagSafe
    case genericQi

    var id: String { rawValue }

    var title: String {
        switch self {
        case .appleMagSafe: return "Apple MagSafe Charger"
        case .appleWatch: return "Apple Watch Charger"
        case .genericMagSafe: return "Generic MagSafe"
        case .genericQi: return "Generic Qi"
        }
    }

    var symbolName: String {
        switch self {
        case .appleMagSafe: return "magsafe.batterypack"
        case .appleWatch: return "applewatch.radiowaves.left.and.right"
        case .genericMagSafe: return "bolt.circle"
        case .genericQi: return "bolt.horizontal.circle"
        }
    }

    /// Whether this charger type uses magnetic alignment, enabling more accurate efficiency calibration.
    var supportsAlignment: Bool {
        switch self {
        case .appleMagSafe, .appleWatch, .genericMagSafe: return true
        case .genericQi: return false
        }
    }

    var wirelessChargingProfile: WirelessChargingProfile {
        supportsAlignment ? .magsafe : .genericQi
    }
}

enum ChargedDeviceTemplateIconSource: String, Codable {
    case systemSymbol
    case asset
}

struct ChargedDeviceTemplateIcon: Hashable, Codable {
    let type: ChargedDeviceTemplateIconSource
    let name: String
    let fallbackSystemName: String?

    static func systemSymbol(
        _ name: String,
        fallbackSystemName: String? = nil
    ) -> ChargedDeviceTemplateIcon {
        ChargedDeviceTemplateIcon(
            type: .systemSymbol,
            name: name,
            fallbackSystemName: fallbackSystemName
        )
    }

    func resolvedSystemSymbolName(fallbackSystemName: String) -> String {
        switch type {
        case .systemSymbol:
            return name
        case .asset:
            return self.fallbackSystemName ?? fallbackSystemName
        }
    }
}

struct ChargedDeviceTemplateDefinition: Identifiable, Hashable, Codable {
    let id: String
    let name: String
    let group: String
    let kind: ChargedDeviceKind
    let deviceClass: ChargedDeviceClass
    let icon: ChargedDeviceTemplateIcon
    let chargingStateAvailability: ChargingStateAvailability
    let supportsWiredCharging: Bool
    let supportsWirelessCharging: Bool
    let wirelessChargingProfile: WirelessChargingProfile
    let sortOrder: Int

    var chargingSupportSummary: String {
        switch (supportsWiredCharging, supportsWirelessCharging) {
        case (true, true):
            return "Wired + Wireless"
        case (true, false):
            return "Wired only"
        case (false, true):
            return "Wireless only"
        case (false, false):
            return "No charging transport"
        }
    }

    var capabilitySummary: String {
        if kind == .charger {
            return wirelessChargingProfile.title
        }
        var components = [chargingStateAvailability.title, chargingSupportSummary]
        if supportsWirelessCharging {
            components.append(wirelessChargingProfile.title)
        }
        return components.joined(separator: " • ")
    }
}

private struct ChargedDeviceTemplateDocument: Codable {
    let templates: [ChargedDeviceTemplateDefinition]
}

struct ChargedDeviceTemplateCatalog {
    static let shared = ChargedDeviceTemplateCatalog()

    let templates: [ChargedDeviceTemplateDefinition]
    private let templatesByID: [String: ChargedDeviceTemplateDefinition]

    private init(bundle: Bundle = .main) {
        let loadedTemplates: [ChargedDeviceTemplateDefinition]

        if let resourceURL = bundle.url(forResource: "ChargedDeviceTemplates", withExtension: "json"),
           let data = try? Data(contentsOf: resourceURL),
           let document = try? JSONDecoder().decode(ChargedDeviceTemplateDocument.self, from: data) {
            loadedTemplates = document.templates
        } else {
            loadedTemplates = []
        }

        self.templates = loadedTemplates.sorted { lhs, rhs in
            if lhs.group != rhs.group {
                return lhs.group < rhs.group
            }
            if lhs.sortOrder != rhs.sortOrder {
                return lhs.sortOrder < rhs.sortOrder
            }
            return lhs.name < rhs.name
        }
        self.templatesByID = Dictionary(uniqueKeysWithValues: self.templates.map { ($0.id, $0) })
    }

    func template(id: String?) -> ChargedDeviceTemplateDefinition? {
        guard let id else {
            return nil
        }
        return templatesByID[id]
    }

    func templates(for kind: ChargedDeviceKind) -> [ChargedDeviceTemplateDefinition] {
        templates.filter { $0.kind == kind }
    }
}

struct ChargeCheckpointSummary: Identifiable, Hashable {
    let id: UUID
    let sessionID: UUID
    let chargedDeviceID: UUID
    let timestamp: Date
    let batteryPercent: Double
    let measuredEnergyWh: 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 estimatedBatteryPercent: 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 meterEnergyBaselineWh: Double?
    let meterDurationBaselineSeconds: Double?
    let meterLastDurationSeconds: Double?
    let minimumObservedCurrentAmps: Double?
    let maximumObservedCurrentAmps: Double?
    let maximumObservedPowerWatts: Double?
    let maximumObservedVoltageVolts: Double?
    let hasObservedChargeFlow: Bool
    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 trimStart: Date?
    let trimEnd: Date?
    let wasConflictHealed: Bool
    let checkpoints: [ChargeCheckpointSummary]
    let aggregatedSamples: [ChargeSessionSampleSummary]

    var effectiveTrimStart: Date { trimStart ?? startedAt }
    var effectiveTrimEnd: Date { trimEnd ?? (endedAt ?? lastObservedAt) }
    var isTrimmed: Bool { trimStart != nil || trimEnd != nil }
    var effectiveTimeRange: ClosedRange<Date> {
        let start = effectiveTrimStart
        let end = max(effectiveTrimEnd, start)
        return start...end
    }
    var displayedAggregatedSamples: [ChargeSessionSampleSummary] {
        guard isTrimmed else { return aggregatedSamples }
        let range = effectiveTimeRange
        return aggregatedSamples.filter { range.contains($0.timestamp) }
    }

    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 {
        if isTrimmed {
            return max(effectiveTrimEnd.timeIntervalSince(effectiveTrimStart), 0)
        }

        // Use timestamp-based duration as primary source; only use meter counter if it's consistent
        let timestampDuration = duration

        if let meterDuration = meterObservedDuration {
            // Allow 5% tolerance for meter counter vs timestamp calculation
            let tolerance = timestampDuration * 0.05
            let lower = timestampDuration - tolerance
            let upper = timestampDuration + tolerance

            // If meter duration is within tolerance range, use it (more precise)
            // Otherwise fall back to timestamp-based duration
            if meterDuration >= lower && meterDuration <= upper {
                return meterDuration
            }
        }

        return timestampDuration
    }

    var effectiveOrMeasuredEnergyWh: Double {
        effectiveBatteryEnergyWh ?? measuredEnergyWh
    }

    var hasSavableChargeData: Bool {
        hasObservedChargeFlow
            || measuredEnergyWh > 0
            || (maximumObservedCurrentAmps ?? 0) > 0
            || (maximumObservedPowerWatts ?? 0) > 0
            || !aggregatedSamples.isEmpty
    }

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

enum BatteryLevelPredictionBasis: Hashable {
    case capacityEstimate
    case checkpointEnergyMap

    var metricLabel: String {
        switch self {
        case .capacityEstimate:
            return "est. capacity"
        case .checkpointEnergyMap:
            return "energy map"
        }
    }

    var explanatoryLabel: String {
        switch self {
        case .capacityEstimate:
            return "estimated capacity"
        case .checkpointEnergyMap:
            return "checkpoint energy map"
        }
    }
}

struct BatteryLevelPrediction: Hashable {
    let predictedPercent: Double
    let estimatedCapacityWh: Double?
    let basis: BatteryLevelPredictionBasis
    let anchorPercent: Double
    let anchorEnergyWh: Double
    let anchorDescription: String

    func energyWh(forPercent percent: Double) -> Double? {
        guard let estimatedCapacityWh, estimatedCapacityWh > 0 else {
            return nil
        }

        return anchorEnergyWh + ((percent - anchorPercent) / 100) * estimatedCapacityWh
    }
}

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 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, Codable {
    let index: Int
    let lowerBoundWatts: Double
    let upperBoundWatts: Double
    let count: Int
    let relativeFrequency: Double

    var id: Int { index }
}

enum HistogramResolution: Int, CaseIterable, Identifiable {
    case x1 = 1
    case x2 = 2
    case x4 = 4

    var id: Int { rawValue }

    var label: String {
        switch self {
        case .x1: return "1×"
        case .x2: return "2×"
        case .x4: return "4×"
        }
    }
}

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
    /// Histogram stored at 4× resolution. Use `histogram(resolution:)` to downsample for display.
    let storedHistogram: [ChargerStandbyPowerDistributionBin]

    // MARK: - Codable (with migration from legacy powerSamplesWatts)

    private enum CodingKeys: String, CodingKey {
        case id, chargerID, meterMACAddress, meterName, meterModel
        case startedAt, endedAt, sampleCount, stabilizedAt
        case averagePowerWatts, recentAveragePowerWatts, medianPowerWatts
        case minimumPowerWatts, maximumPowerWatts, standardDeviationPowerWatts
        case coefficientOfVariation, averageCurrentAmps, averageVoltageVolts
        case stabilityDeltaWatts, stabilityToleranceWatts
        case storedHistogram
        case powerSamplesWatts // legacy – decode only
    }

    init(
        id: UUID, chargerID: UUID, meterMACAddress: String, meterName: String?, meterModel: String?,
        startedAt: Date, endedAt: Date, sampleCount: Int, stabilizedAt: Date?,
        averagePowerWatts: Double, recentAveragePowerWatts: Double, medianPowerWatts: Double,
        minimumPowerWatts: Double, maximumPowerWatts: Double, standardDeviationPowerWatts: Double,
        coefficientOfVariation: Double, averageCurrentAmps: Double, averageVoltageVolts: Double,
        stabilityDeltaWatts: Double, stabilityToleranceWatts: Double,
        storedHistogram: [ChargerStandbyPowerDistributionBin]
    ) {
        self.id = id; self.chargerID = chargerID; self.meterMACAddress = meterMACAddress
        self.meterName = meterName; self.meterModel = meterModel
        self.startedAt = startedAt; self.endedAt = endedAt; self.sampleCount = sampleCount
        self.stabilizedAt = stabilizedAt
        self.averagePowerWatts = averagePowerWatts; self.recentAveragePowerWatts = recentAveragePowerWatts
        self.medianPowerWatts = medianPowerWatts; self.minimumPowerWatts = minimumPowerWatts
        self.maximumPowerWatts = maximumPowerWatts
        self.standardDeviationPowerWatts = standardDeviationPowerWatts
        self.coefficientOfVariation = coefficientOfVariation
        self.averageCurrentAmps = averageCurrentAmps; self.averageVoltageVolts = averageVoltageVolts
        self.stabilityDeltaWatts = stabilityDeltaWatts; self.stabilityToleranceWatts = stabilityToleranceWatts
        self.storedHistogram = storedHistogram
    }

    init(from decoder: Decoder) throws {
        let c = try decoder.container(keyedBy: CodingKeys.self)
        id = try c.decode(UUID.self, forKey: .id)
        chargerID = try c.decode(UUID.self, forKey: .chargerID)
        meterMACAddress = try c.decode(String.self, forKey: .meterMACAddress)
        meterName = try c.decodeIfPresent(String.self, forKey: .meterName)
        meterModel = try c.decodeIfPresent(String.self, forKey: .meterModel)
        startedAt = try c.decode(Date.self, forKey: .startedAt)
        endedAt = try c.decode(Date.self, forKey: .endedAt)
        sampleCount = try c.decode(Int.self, forKey: .sampleCount)
        stabilizedAt = try c.decodeIfPresent(Date.self, forKey: .stabilizedAt)
        averagePowerWatts = try c.decode(Double.self, forKey: .averagePowerWatts)
        recentAveragePowerWatts = try c.decode(Double.self, forKey: .recentAveragePowerWatts)
        medianPowerWatts = try c.decode(Double.self, forKey: .medianPowerWatts)
        minimumPowerWatts = try c.decode(Double.self, forKey: .minimumPowerWatts)
        maximumPowerWatts = try c.decode(Double.self, forKey: .maximumPowerWatts)
        standardDeviationPowerWatts = try c.decode(Double.self, forKey: .standardDeviationPowerWatts)
        coefficientOfVariation = try c.decode(Double.self, forKey: .coefficientOfVariation)
        averageCurrentAmps = try c.decode(Double.self, forKey: .averageCurrentAmps)
        averageVoltageVolts = try c.decode(Double.self, forKey: .averageVoltageVolts)
        stabilityDeltaWatts = try c.decode(Double.self, forKey: .stabilityDeltaWatts)
        stabilityToleranceWatts = try c.decode(Double.self, forKey: .stabilityToleranceWatts)

        let decodedBins = try? c.decodeIfPresent([ChargerStandbyPowerDistributionBin].self, forKey: .storedHistogram)
        if let decodedBins, !decodedBins.isEmpty {
            storedHistogram = decodedBins
        } else {
            // Migrate from legacy raw samples format
            let samples = (try? c.decodeIfPresent([Double].self, forKey: .powerSamplesWatts)) ?? []
            let base = min(18, max(8, Int(Double(samples.count).squareRoot().rounded())))
            storedHistogram = ChargerStandbyPowerMeasurementAnalyzer.histogram(
                for: samples,
                preferredBinCount: base * HistogramResolution.x4.rawValue
            )
        }
    }

    func encode(to encoder: Encoder) throws {
        var c = encoder.container(keyedBy: CodingKeys.self)
        try c.encode(id, forKey: .id)
        try c.encode(chargerID, forKey: .chargerID)
        try c.encode(meterMACAddress, forKey: .meterMACAddress)
        try c.encodeIfPresent(meterName, forKey: .meterName)
        try c.encodeIfPresent(meterModel, forKey: .meterModel)
        try c.encode(startedAt, forKey: .startedAt)
        try c.encode(endedAt, forKey: .endedAt)
        try c.encode(sampleCount, forKey: .sampleCount)
        try c.encodeIfPresent(stabilizedAt, forKey: .stabilizedAt)
        try c.encode(averagePowerWatts, forKey: .averagePowerWatts)
        try c.encode(recentAveragePowerWatts, forKey: .recentAveragePowerWatts)
        try c.encode(medianPowerWatts, forKey: .medianPowerWatts)
        try c.encode(minimumPowerWatts, forKey: .minimumPowerWatts)
        try c.encode(maximumPowerWatts, forKey: .maximumPowerWatts)
        try c.encode(standardDeviationPowerWatts, forKey: .standardDeviationPowerWatts)
        try c.encode(coefficientOfVariation, forKey: .coefficientOfVariation)
        try c.encode(averageCurrentAmps, forKey: .averageCurrentAmps)
        try c.encode(averageVoltageVolts, forKey: .averageVoltageVolts)
        try c.encode(stabilityDeltaWatts, forKey: .stabilityDeltaWatts)
        try c.encode(stabilityToleranceWatts, forKey: .stabilityToleranceWatts)
        try c.encode(storedHistogram, forKey: .storedHistogram)
    }

    // MARK: - Computed

    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 }

    /// Returns the histogram downsampled to the requested resolution.
    /// The stored histogram is always at 4× so factor = 4 / resolution.rawValue.
    func histogram(resolution: HistogramResolution = .x4) -> [ChargerStandbyPowerDistributionBin] {
        let factor = HistogramResolution.x4.rawValue / resolution.rawValue
        return ChargerStandbyPowerMeasurementAnalyzer.downsample(storedHistogram, factor: factor)
    }
}

enum ChargerStandbyPowerMeasurementAnalyzer {
    static let minimumStableSampleCount = 45
    static let recentSampleWindow = 40
    static let minimumStabilityToleranceWatts = 0.010
    static let relativeStabilityTolerance = 0.05

    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
        )

        let baseBinCount = min(18, max(8, Int(Double(powerValues.count).squareRoot().rounded())))
        let liveHistogram = histogram(for: powerValues, preferredBinCount: baseBinCount * HistogramResolution.x4.rawValue)

        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: liveHistogram
        )
    }

    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,
            storedHistogram: statistics.histogram
        )
    }

    /// Merges consecutive bins in groups of `factor`. Returns the input unchanged when factor ≤ 1.
    static func downsample(
        _ bins: [ChargerStandbyPowerDistributionBin],
        factor: Int
    ) -> [ChargerStandbyPowerDistributionBin] {
        guard factor > 1, !bins.isEmpty else { return bins }
        let totalCount = bins.reduce(0) { $0 + $1.count }
        var result: [ChargerStandbyPowerDistributionBin] = []
        var inputIndex = 0
        var outputIndex = 0
        while inputIndex < bins.count {
            let group = Array(bins[inputIndex..<min(inputIndex + factor, bins.count)])
            let mergedCount = group.reduce(0) { $0 + $1.count }
            let relFreq = totalCount > 0 ? Double(mergedCount) / Double(totalCount) : 0
            result.append(ChargerStandbyPowerDistributionBin(
                index: outputIndex,
                lowerBoundWatts: group.first!.lowerBoundWatts,
                upperBoundWatts: group.last!.upperBoundWatts,
                count: mergedCount,
                relativeFrequency: relFreq
            ))
            inputIndex += factor
            outputIndex += 1
        }
        return result
    }

    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 deviceTemplateID: String?
    let templateDefinition: ChargedDeviceTemplateDefinition?
    let supportsChargingWhileOff: Bool
    let chargingStateAvailability: ChargingStateAvailability
    let supportsWiredCharging: Bool
    let supportsWirelessCharging: Bool
    let chargerType: ChargerType?
    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 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 {
        templateDefinition?.name ?? (isCharger ? kind.title : deviceClass.title)
    }

    var fallbackIdentitySymbolName: String {
        isCharger ? kind.symbolName : deviceClass.symbolName
    }

    var identityIcon: ChargedDeviceTemplateIcon {
        templateDefinition?.icon ?? .systemSymbol(fallbackIdentitySymbolName)
    }

    var identitySymbolName: String {
        identityIcon.resolvedSystemSymbolName(fallbackSystemName: fallbackIdentitySymbolName)
    }

    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
    }

    var hasMultipleChargingTransports: Bool {
        supportedChargingModes.count > 1
    }

    var hasMultipleChargingStateModes: Bool {
        supportedChargingStateModes.count > 1
    }

    var showsWirelessProfileDetails: Bool {
        supportsWirelessCharging
            && hasMultipleChargingTransports
            && deviceClass != .watch
    }

    var chargingSupportSummary: String {
        switch (supportsWiredCharging, supportsWirelessCharging) {
        case (true, true):
            return "Supports wired and wireless charging."
        case (true, false):
            return "Supports wired charging only."
        case (false, true):
            return "Supports wireless charging only."
        case (false, false):
            return "No charging method configured."
        }
    }

    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 shouldShowChargingTransport(_ chargingTransportMode: ChargingTransportMode) -> Bool {
        hasMultipleChargingTransports
            || supportedChargingModes.contains(chargingTransportMode) == false
    }

    func shouldShowChargingStateMode(_ chargingStateMode: ChargingStateMode) -> Bool {
        hasMultipleChargingStateModes
            || supportedChargingStateModes.contains(chargingStateMode) == false
    }

    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,
        referenceTimestamp: Date? = nil
    ) -> BatteryLevelPrediction? {
        let estimatedCapacityWh = session.capacityEstimateWh
            ?? estimatedBatteryCapacityWh(for: session.chargingTransportMode)
            ?? estimatedBatteryCapacityWh

        let effectiveEnergyWh = effectiveEnergyWhOverride
            ?? session.effectiveBatteryEnergyWh
            ?? session.measuredEnergyWh

        struct Anchor {
            let percent: Double
            let energyWh: Double
            let timestamp: Date
            let description: String
            let isCheckpoint: Bool
        }

        func anchorCapacityCandidates(from anchors: [Anchor]) -> [Double] {
            var candidates: [Double] = []

            for lowerIndex in anchors.indices {
                for upperIndex in anchors.indices where upperIndex > lowerIndex {
                    let lower = anchors[lowerIndex]
                    let upper = anchors[upperIndex]
                    let percentDelta = upper.percent - lower.percent
                    let energyDelta = upper.energyWh - lower.energyWh

                    guard percentDelta >= 3, energyDelta > 0.01 else {
                        continue
                    }

                    let capacityWh = energyDelta / (percentDelta / 100)
                    guard capacityWh.isFinite, capacityWh > 0, capacityWh < 1_000 else {
                        continue
                    }

                    candidates.append(capacityWh)
                }
            }

            return candidates
        }

        func inferredCheckpointEnergyMapCapacityWh(from anchors: [Anchor]) -> Double? {
            let candidates = anchorCapacityCandidates(from: anchors)
            guard !candidates.isEmpty else {
                return nil
            }

            let sortedCandidates = candidates.sorted()
            return sortedCandidates[sortedCandidates.count / 2]
        }

        var anchors: [Anchor] = []

        if let startBatteryPercent = session.startBatteryPercent, startBatteryPercent >= 0 {
            anchors.append(
                Anchor(
                    percent: startBatteryPercent,
                    energyWh: 0,
                    timestamp: session.effectiveTrimStart,
                    description: "session start",
                    isCheckpoint: false
                )
            )
        }

        anchors.append(
            contentsOf: session.checkpoints
                .filter { $0.batteryPercent >= 0 }
                .map { checkpoint in
                    Anchor(
                        percent: checkpoint.batteryPercent,
                        energyWh: checkpoint.measuredEnergyWh,
                        timestamp: checkpoint.timestamp,
                        description: checkpoint.flag.anchorDescription,
                        isCheckpoint: true
                    )
                }
        )

        let sortedAnchors = anchors.sorted { lhs, rhs in
            if lhs.energyWh != rhs.energyWh {
                return lhs.energyWh < rhs.energyWh
            }
            return lhs.timestamp < rhs.timestamp
        }

        guard !sortedAnchors.isEmpty else {
            return nil
        }

        let canUseCheckpointEnergyMap = deviceClass == .watch || deviceClass == .iphone
        let inferredCapacityWh = estimatedCapacityWh
            ?? (canUseCheckpointEnergyMap ? inferredCheckpointEnergyMapCapacityWh(from: sortedAnchors) : nil)
        let basis: BatteryLevelPredictionBasis = estimatedCapacityWh == nil
            ? .checkpointEnergyMap
            : .capacityEstimate

        let lowerAnchor = sortedAnchors.last { $0.energyWh <= effectiveEnergyWh + 0.05 }
        let upperAnchor = sortedAnchors.first { $0.energyWh > effectiveEnergyWh + 0.05 }
        let anchor = lowerAnchor ?? upperAnchor ?? sortedAnchors.first!

        let predictedPercent: Double
        if let lowerAnchor,
           let upperAnchor,
           upperAnchor.energyWh > lowerAnchor.energyWh + 0.05 {
            let interpolationProgress = min(
                max(
                    (effectiveEnergyWh - lowerAnchor.energyWh) /
                    (upperAnchor.energyWh - lowerAnchor.energyWh),
                    0
                ),
                1
            )
            predictedPercent = min(
                max(
                    lowerAnchor.percent +
                    (upperAnchor.percent - lowerAnchor.percent) * interpolationProgress,
                    0
                ),
                100
            )
        } else {
            guard let inferredCapacityWh, inferredCapacityWh > 0 else {
                return nil
            }

            predictedPercent = BatteryLevelPredictionTuning.predictedPercent(
                anchorPercent: anchor.percent,
                anchorEnergyWh: anchor.energyWh,
                anchorTimestamp: anchor.timestamp,
                anchorIsCheckpoint: anchor.isCheckpoint,
                effectiveEnergyWh: effectiveEnergyWh,
                referenceTimestamp: referenceTimestamp ?? session.lastObservedAt,
                estimatedCapacityWh: inferredCapacityWh
            )
        }

        return BatteryLevelPrediction(
            predictedPercent: predictedPercent,
            estimatedCapacityWh: inferredCapacityWh,
            basis: basis,
            anchorPercent: anchor.percent,
            anchorEnergyWh: anchor.energyWh,
            anchorDescription: anchor.description
        )
    }

    func withStandbyPowerMeasurements(_ measurements: [ChargerStandbyPowerMeasurementSummary]) -> ChargedDeviceSummary {
        ChargedDeviceSummary(
            id: id,
            qrIdentifier: qrIdentifier,
            name: name,
            deviceClass: deviceClass,
            deviceTemplateID: deviceTemplateID,
            templateDefinition: templateDefinition,
            supportsChargingWhileOff: supportsChargingWhileOff,
            chargingStateAvailability: chargingStateAvailability,
            supportsWiredCharging: supportsWiredCharging,
            supportsWirelessCharging: supportsWirelessCharging,
            chargerType: chargerType,
            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,
            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
}