// // ChargerStandbyPowerWizardView.swift // USB Meter // // Created by Codex on 13/04/2026. // import SwiftUI struct ChargerStandbyPowerWizardView: View { @EnvironmentObject private var appData: AppData @State private var chargerLibraryVisibility = false @State private var discardConfirmationVisibility = false @State private var selectedMeterMACAddress: String? @State private var selectedChargerID: UUID? let preferredMeterMACAddress: String? let preferredChargerID: UUID? let locksChargerSelection: Bool init( preferredMeterMACAddress: String? = nil, preferredChargerID: UUID? = nil, locksChargerSelection: Bool = false ) { self.preferredMeterMACAddress = preferredMeterMACAddress self.preferredChargerID = preferredChargerID self.locksChargerSelection = locksChargerSelection _selectedMeterMACAddress = State(initialValue: nil) _selectedChargerID = State(initialValue: preferredChargerID) } var body: some View { ScrollView { VStack(spacing: 18) { if let session = activeSession { activeMeasurementCard(session) liveSessionCard(session) } else { newMeasurementWizardCard } } .padding() } .background( LinearGradient( colors: [.orange.opacity(0.16), Color.clear], startPoint: .topLeading, endPoint: .bottomTrailing ) .ignoresSafeArea() ) .navigationTitle(navigationTitleText) .sheet(isPresented: $chargerLibraryVisibility) { ChargedDeviceLibrarySheetView( visibility: $chargerLibraryVisibility, meterMACAddress: selectedMeterSummary?.macAddress ?? "", meterTint: selectedMeter?.color ?? .orange, mode: .charger ) .environmentObject(appData) } .confirmationDialog( "Discard the current standby measurement?", isPresented: $discardConfirmationVisibility, titleVisibility: .visible ) { Button("Discard", role: .destructive) { if let activeSession { _ = appData.finishChargerStandbyMeasurement(for: activeSession.meterMACAddress, save: false) } } Button("Cancel", role: .cancel) {} } message: { Text("The current sample set will be removed and nothing will be saved for this charger.") } } private var liveMeterSummaries: [AppData.MeterSummary] { appData.meterSummaries.filter { $0.meter != nil } } private var availableChargers: [ChargedDeviceSummary] { appData.chargerSummaries } private var preferredChargerMeterMACAddress: String? { preferredChargerID.flatMap { appData.chargedDeviceSummary(id: $0)?.lastAssociatedMeterMAC } } private var activeSession: ChargerStandbyPowerMonitorSession? { let candidateMACAddresses = [ selectedMeterMACAddress ?? "", preferredMeterMACAddress ?? "", preferredChargerMeterMACAddress ?? "" ] .filter { $0.isEmpty == false } for macAddress in candidateMACAddresses { if let session = appData.chargerStandbyMeasurementSession(for: macAddress) { return session } } for meterSummary in liveMeterSummaries { if let session = appData.chargerStandbyMeasurementSession(for: meterSummary.macAddress) { return session } } return nil } private var suggestedMeterSummary: AppData.MeterSummary? { if let preferredMeterMACAddress { return liveMeterSummaries.first(where: { $0.macAddress == preferredMeterMACAddress }) } if let preferredChargerMeterMACAddress { return liveMeterSummaries.first(where: { $0.macAddress == preferredChargerMeterMACAddress }) } return liveMeterSummaries.count == 1 ? liveMeterSummaries.first : nil } private var selectedMeterSummary: AppData.MeterSummary? { if let activeSession { return liveMeterSummaries.first(where: { $0.macAddress == activeSession.meterMACAddress }) } guard let selectedMeterMACAddress else { return nil } return liveMeterSummaries.first(where: { $0.macAddress == selectedMeterMACAddress }) } private var selectedMeter: Meter? { selectedMeterSummary?.meter } private var isChargerSelectionLocked: Bool { locksChargerSelection || preferredChargerID != nil } private var meterSelectionBinding: Binding { Binding( get: { selectedMeterMACAddress }, set: { selectedMeterMACAddress = $0 } ) } private var selectedCharger: ChargedDeviceSummary? { if let activeSession { return appData.chargedDeviceSummary(id: activeSession.chargerID) } guard let selectedChargerID else { return nil } return appData.chargedDeviceSummary(id: selectedChargerID) } private var chargerSelectionBinding: Binding { Binding( get: { selectedChargerID }, set: { selectedChargerID = $0 } ) } private var preferredChargerSummary: ChargedDeviceSummary? { guard let preferredChargerID else { return nil } return appData.chargedDeviceSummary(id: preferredChargerID) } private var navigationTitleText: String { "New Standby Consumption Measurement" } private var wizardCardTitle: String { if let selectedMeterSummary { return selectedMeterSummary.displayName } if let suggestedMeterSummary { return suggestedMeterSummary.displayName } return "Use Meter" } private var newMeasurementWizardCard: some View { MeterInfoCardView( title: wizardCardTitle, tint: .orange ) { if liveMeterSummaries.isEmpty { Text("Connect a live meter first. Standby measurement uses a live feed, so meter selection happens here in the wizard.") .font(.footnote) .foregroundColor(.secondary) .frame(maxWidth: .infinity, alignment: .leading) } else { VStack(alignment: .leading, spacing: 12) { if isChargerSelectionLocked == false { HStack(spacing: 8) { Text("Charger") .font(.subheadline.weight(.semibold)) ContextInfoButton( title: "Charger", message: "Choose the charger whose standby consumption you want to measure in this run." ) } if availableChargers.isEmpty { Text("No charger available yet. Open the charger library to create one first.") .font(.caption) .foregroundColor(.secondary) } else { Picker("Charger", selection: chargerSelectionBinding) { Text("Select Charger").tag(Optional.none) ForEach(availableChargers) { charger in Text(charger.name).tag(Optional(charger.id)) } } .pickerStyle(.menu) } } HStack(spacing: 8) { Text("Use Meter") .font(.subheadline.weight(.semibold)) ContextInfoButton( title: "Use Meter", message: "Choose the live meter explicitly. Standby consumption can vary with the upstream source or when the charger is connected to a computer, so re-run after setup changes." ) } Picker("Use Meter", selection: meterSelectionBinding) { Text("Select Meter").tag(Optional.none) ForEach(liveMeterSummaries) { meterSummary in Text(meterSummary.displayName).tag(Optional(meterSummary.macAddress)) } } .pickerStyle(.menu) .disabled(activeSession != nil) if activeSession == nil, let suggestedMeterSummary, selectedMeterSummary == nil { Text("Suggested from the current context: \(suggestedMeterSummary.displayName). Select it explicitly if this is the meter you want to use.") .font(.caption) .foregroundColor(.secondary) } HStack(spacing: 12) { if isChargerSelectionLocked == false { Button("Manage Charger Library") { chargerLibraryVisibility = true } .disabled(selectedMeter == nil) } Button("Start Measurement") { startMeasurement() } .disabled(selectedCharger == nil || selectedMeter == nil) } .buttonStyle(.borderedProminent) } } if selectedMeter == nil { Text("Choose the live meter explicitly before starting. The wizard no longer auto-confirms a suggested meter.") .font(.caption) .foregroundColor(.secondary) } else if activeSession == nil, selectedCharger == nil { Text("Select the charger you want to measure, then start the run.") .font(.caption) .foregroundColor(.secondary) } else if activeSession == nil, selectedMeter?.operationalState != .dataIsAvailable { Text("The wizard can start now, but samples will only be captured while live meter data is available.") .font(.caption) .foregroundColor(.secondary) } else if let activeSession { Text( "\(activeSession.readinessDescription) • \(formattedDuration(Date().timeIntervalSince(activeSession.startedAt))) • \(activeSession.sampleCount) samples" ) .font(.caption) .foregroundColor(.secondary) } } } private func activeMeasurementCard(_ session: ChargerStandbyPowerMonitorSession) -> some View { MeterInfoCardView( title: "Measurement Running", infoMessage: "The run keeps collecting samples while this meter stays live. Save when you are happy with the sample set, or discard to cancel it.", tint: .orange ) { MeterInfoRowView(label: "Meter", value: selectedMeterSummary?.displayName ?? session.meterMACAddress) MeterInfoRowView(label: "Charger", value: selectedCharger?.name ?? "Selected charger") MeterInfoRowView(label: "Status", value: session.readinessDescription) MeterInfoRowView(label: "Samples", value: "\(session.sampleCount)") HStack(spacing: 12) { Button("Save Result") { _ = appData.finishChargerStandbyMeasurement(for: session.meterMACAddress, save: true) } .disabled(session.hasSamples == false) Button("Discard") { discardConfirmationVisibility = true } .foregroundColor(.red) } .buttonStyle(.borderedProminent) } } private func liveSessionCard(_ session: ChargerStandbyPowerMonitorSession) -> some View { VStack(spacing: 18) { if let statistics = session.statistics { stabilityCard( isStable: statistics.isStable, averagePowerWatts: statistics.averagePowerWatts, stabilityDeltaWatts: statistics.stabilityDeltaWatts, stabilityToleranceWatts: statistics.stabilityToleranceWatts, sampleCount: statistics.sampleCount ) projectionCard( averagePowerWatts: statistics.averagePowerWatts, projectedDailyEnergyWh: statistics.projectedDailyEnergyWh, projectedWeeklyEnergyWh: statistics.projectedWeeklyEnergyWh, projectedMonthlyEnergyWh: statistics.projectedMonthlyEnergyWh, projectedYearlyEnergyWh: statistics.projectedYearlyEnergyWh ) distributionCard( histogram: statistics.histogram, averagePowerWatts: statistics.averagePowerWatts, standardDeviationPowerWatts: statistics.standardDeviationPowerWatts, tint: .orange ) statisticsCard( averagePowerWatts: statistics.averagePowerWatts, medianPowerWatts: statistics.medianPowerWatts, minimumPowerWatts: statistics.minimumPowerWatts, maximumPowerWatts: statistics.maximumPowerWatts, standardDeviationPowerWatts: statistics.standardDeviationPowerWatts, coefficientOfVariation: statistics.coefficientOfVariation, averageCurrentAmps: statistics.averageCurrentAmps, averageVoltageVolts: statistics.averageVoltageVolts ) } else { MeterInfoCardView(title: "Live Stats", tint: .orange) { Text("Waiting for the first valid power samples from the meter.") .font(.footnote) .foregroundColor(.secondary) } } } } private func stabilityCard( isStable: Bool, averagePowerWatts: Double, stabilityDeltaWatts: Double, stabilityToleranceWatts: Double, sampleCount: Int, subtitle: String? = nil ) -> some View { VStack(alignment: .leading, spacing: 10) { HStack { VStack(alignment: .leading, spacing: 4) { Text(isStable ? "Enough Samples" : "Still Settling") .font(.headline) Text(subtitle ?? (isStable ? "The running average has stabilised." : "The wizard is still watching the average drift.")) .font(.caption) .foregroundColor(.secondary) } Spacer() Text(isStable ? "Ready" : "Live") .font(.caption.weight(.semibold)) .padding(.horizontal, 10) .padding(.vertical, 6) .foregroundColor(isStable ? .green : .orange) .meterCard( tint: isStable ? .green : .orange, fillOpacity: 0.10, strokeOpacity: 0.16, cornerRadius: 999 ) } Text("\(averagePowerWatts.format(decimalDigits: 3)) W") .font(.system(.largeTitle, design: .rounded).weight(.bold)) .monospacedDigit() Text( "Recent drift: \((stabilityDeltaWatts * 1000).format(decimalDigits: 2)) mW, tolerance \((stabilityToleranceWatts * 1000).format(decimalDigits: 2)) mW over \(sampleCount) samples." ) .font(.footnote) .foregroundColor(.secondary) } .frame(maxWidth: .infinity, alignment: .leading) .padding(18) .meterCard(tint: isStable ? .green : .orange, fillOpacity: 0.18, strokeOpacity: 0.24) } private func projectionCard( averagePowerWatts: Double, projectedDailyEnergyWh: Double, projectedWeeklyEnergyWh: Double, projectedMonthlyEnergyWh: Double, projectedYearlyEnergyWh: Double ) -> some View { MeterInfoCardView( title: "Consumption Projection", infoMessage: "These projections extrapolate only from the measured standby average. They do not say anything about charger behaviour under load.", tint: .teal ) { MeterInfoRowView(label: "Average Power", value: "\(averagePowerWatts.format(decimalDigits: 3)) W") MeterInfoRowView(label: "24 Hours", value: standbyEnergyLabel(projectedDailyEnergyWh)) MeterInfoRowView(label: "7 Days", value: standbyEnergyLabel(projectedWeeklyEnergyWh)) MeterInfoRowView(label: "30 Days", value: standbyEnergyLabel(projectedMonthlyEnergyWh)) MeterInfoRowView(label: "1 Year", value: standbyEnergyLabel(projectedYearlyEnergyWh)) } } private func distributionCard( histogram: [ChargerStandbyPowerDistributionBin], averagePowerWatts: Double, standardDeviationPowerWatts: Double, tint: Color ) -> some View { MeterInfoCardView( title: "Distribution", infoMessage: "Bars show how often each power interval appeared. The curve overlays a normal approximation around the observed mean and deviation.", tint: tint ) { StandbyPowerHistogramView( histogram: histogram, averagePowerWatts: averagePowerWatts, standardDeviationPowerWatts: standardDeviationPowerWatts, tint: tint ) .frame(height: 220) if let firstBin = histogram.first, let lastBin = histogram.last { HStack { Text("\(firstBin.lowerBoundWatts.format(decimalDigits: 3)) W") Spacer() Text("\(lastBin.upperBoundWatts.format(decimalDigits: 3)) W") } .font(.caption) .foregroundColor(.secondary) .monospacedDigit() } } } private func statisticsCard( averagePowerWatts: Double, medianPowerWatts: Double, minimumPowerWatts: Double, maximumPowerWatts: Double, standardDeviationPowerWatts: Double, coefficientOfVariation: Double, averageCurrentAmps: Double, averageVoltageVolts: Double ) -> some View { MeterInfoCardView(title: "Interesting Stats", tint: .indigo) { MeterInfoRowView(label: "Median", value: "\(medianPowerWatts.format(decimalDigits: 3)) W") MeterInfoRowView(label: "Minimum", value: "\(minimumPowerWatts.format(decimalDigits: 3)) W") MeterInfoRowView(label: "Maximum", value: "\(maximumPowerWatts.format(decimalDigits: 3)) W") MeterInfoRowView(label: "Spread σ", value: "\(standardDeviationPowerWatts.format(decimalDigits: 4)) W") MeterInfoRowView(label: "Variation", value: "\(Int((coefficientOfVariation * 100).rounded()))%") MeterInfoRowView(label: "Mean Current", value: "\(averageCurrentAmps.format(decimalDigits: 3)) A") MeterInfoRowView(label: "Mean Voltage", value: "\(averageVoltageVolts.format(decimalDigits: 3)) V") MeterInfoRowView(label: "Power Density", value: "\(averagePowerWatts.format(decimalDigits: 3)) W steady") } } private func startMeasurement() { guard let selectedCharger, let selectedMeter else { return } _ = appData.startChargerStandbyMeasurement(for: selectedCharger.id, on: selectedMeter) } private func standbyEnergyLabel(_ wattHours: Double) -> String { if wattHours >= 1000 { return "\((wattHours / 1000).format(decimalDigits: 3)) kWh" } return "\(wattHours.format(decimalDigits: 2)) Wh" } private func formattedDuration(_ duration: TimeInterval) -> String { let formatter = DateComponentsFormatter() formatter.allowedUnits = duration >= 3600 ? [.hour, .minute, .second] : [.minute, .second] formatter.unitsStyle = .abbreviated formatter.zeroFormattingBehavior = .pad return formatter.string(from: max(duration, 0)) ?? "0s" } } private struct StandbyPowerHistogramView: View { let histogram: [ChargerStandbyPowerDistributionBin] let averagePowerWatts: Double let standardDeviationPowerWatts: Double let tint: Color var body: some View { GeometryReader { proxy in let maxCount = max(Double(histogram.map(\.count).max() ?? 1), 1) ZStack { HStack(alignment: .bottom, spacing: 6) { ForEach(histogram) { bin in RoundedRectangle(cornerRadius: 8, style: .continuous) .fill(tint.opacity(0.24)) .overlay( RoundedRectangle(cornerRadius: 8, style: .continuous) .stroke(tint.opacity(0.22), lineWidth: 1) ) .frame(height: max(10, (Double(bin.count) / maxCount) * proxy.size.height)) } } .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottom) gaussianCurve(in: proxy.size) .stroke(tint, style: StrokeStyle(lineWidth: 2.5, lineCap: .round, lineJoin: .round)) meanMarker(in: proxy.size) .stroke(tint.opacity(0.9), style: StrokeStyle(lineWidth: 1.5, dash: [6, 4])) } } } private func gaussianCurve(in size: CGSize) -> Path { guard histogram.count > 1, standardDeviationPowerWatts > 0, let firstBin = histogram.first, let lastBin = histogram.last else { return Path() } let minimum = firstBin.lowerBoundWatts let maximum = lastBin.upperBoundWatts let span = max(maximum - minimum, 0.000_001) let sampleCount = 48 let peakDensity = 1 / (standardDeviationPowerWatts * sqrt(2 * .pi)) return Path { path in for index in 0...sampleCount { let progress = Double(index) / Double(sampleCount) let value = minimum + (span * progress) let zScore = (value - averagePowerWatts) / standardDeviationPowerWatts let density = exp(-0.5 * zScore * zScore) / (standardDeviationPowerWatts * sqrt(2 * .pi)) let normalizedHeight = density / peakDensity let x = progress * size.width let y = size.height - (normalizedHeight * (Double(size.height) * 0.92)) let point = CGPoint(x: x, y: y) if index == 0 { path.move(to: point) } else { path.addLine(to: point) } } } } private func meanMarker(in size: CGSize) -> Path { guard let firstBin = histogram.first, let lastBin = histogram.last else { return Path() } let minimum = firstBin.lowerBoundWatts let maximum = lastBin.upperBoundWatts let span = max(maximum - minimum, 0.000_001) let normalizedX = min(max((averagePowerWatts - minimum) / span, 0), 1) let x = normalizedX * size.width return Path { path in path.move(to: CGPoint(x: x, y: 0)) path.addLine(to: CGPoint(x: x, y: size.height)) } } } struct ChargerStandbyPowerMeasurementsView: View { @EnvironmentObject private var appData: AppData let chargerID: UUID var body: some View { Group { if let charger = appData.chargedDeviceSummary(id: chargerID) { List { if charger.standbyPowerMeasurements.isEmpty { Text("No standby measurements saved yet.") .foregroundColor(.secondary) } else { ForEach(charger.standbyPowerMeasurements) { measurement in NavigationLink( destination: ChargerStandbyPowerMeasurementDetailView( chargerID: charger.id, measurementID: measurement.id ) ) { VStack(alignment: .leading, spacing: 6) { HStack { Text(measurement.endedAt.format()) .font(.subheadline.weight(.semibold)) Spacer() Text("\(measurement.averagePowerWatts.format(decimalDigits: 3)) W") .font(.subheadline.weight(.bold)) .monospacedDigit() } Text( "\(formattedDuration(measurement.duration)) • \(measurement.sampleCount) samples • \(standbyEnergyLabel(measurement.projectedYearlyEnergyWh)) / year" ) .font(.caption) .foregroundColor(.secondary) } .frame(maxWidth: .infinity, alignment: .leading) } } } } .navigationTitle("Saved Measurements") } else { Text("This charger is no longer available.") .foregroundColor(.secondary) .navigationTitle("Saved Measurements") } } } private func standbyEnergyLabel(_ wattHours: Double) -> String { if wattHours >= 1000 { return "\((wattHours / 1000).format(decimalDigits: 3)) kWh" } return "\(wattHours.format(decimalDigits: 2)) Wh" } private func formattedDuration(_ duration: TimeInterval) -> String { let formatter = DateComponentsFormatter() formatter.allowedUnits = duration >= 3600 ? [.hour, .minute, .second] : [.minute, .second] formatter.unitsStyle = .abbreviated formatter.zeroFormattingBehavior = .pad return formatter.string(from: max(duration, 0)) ?? "0s" } } struct ChargerStandbyPowerMeasurementDetailView: View { @EnvironmentObject private var appData: AppData let chargerID: UUID let measurementID: UUID var body: some View { Group { if let charger = appData.chargedDeviceSummary(id: chargerID), let measurement = charger.standbyPowerMeasurements.first(where: { $0.id == measurementID }) { ScrollView { VStack(spacing: 18) { MeterInfoCardView(title: charger.name, tint: .orange) { MeterInfoRowView(label: "Saved", value: measurement.endedAt.format()) MeterInfoRowView(label: "Average", value: "\(measurement.averagePowerWatts.format(decimalDigits: 3)) W") MeterInfoRowView(label: "Samples", value: "\(measurement.sampleCount)") MeterInfoRowView(label: "Duration", value: formattedDuration(measurement.duration)) } ChargerStandbyPowerMeasurementSnapshotView(measurement: measurement) } .padding() } .background( LinearGradient( colors: [.orange.opacity(0.16), Color.clear], startPoint: .topLeading, endPoint: .bottomTrailing ) .ignoresSafeArea() ) .navigationTitle("Measurement") } else { Text("This measurement is no longer available.") .foregroundColor(.secondary) .navigationTitle("Measurement") } } } private func formattedDuration(_ duration: TimeInterval) -> String { let formatter = DateComponentsFormatter() formatter.allowedUnits = duration >= 3600 ? [.hour, .minute, .second] : [.minute, .second] formatter.unitsStyle = .abbreviated formatter.zeroFormattingBehavior = .pad return formatter.string(from: max(duration, 0)) ?? "0s" } } private struct ChargerStandbyPowerMeasurementSnapshotView: View { let measurement: ChargerStandbyPowerMeasurementSummary var body: some View { VStack(spacing: 18) { stabilityCard projectionCard distributionCard statisticsCard } } private var stabilityCard: some View { VStack(alignment: .leading, spacing: 10) { HStack { VStack(alignment: .leading, spacing: 4) { Text(measurement.isStable ? "Enough Samples" : "Still Settling") .font(.headline) Text("Saved \(measurement.endedAt.format())") .font(.caption) .foregroundColor(.secondary) } Spacer() Text(measurement.isStable ? "Ready" : "Live") .font(.caption.weight(.semibold)) .padding(.horizontal, 10) .padding(.vertical, 6) .foregroundColor(measurement.isStable ? .green : .orange) .meterCard( tint: measurement.isStable ? .green : .orange, fillOpacity: 0.10, strokeOpacity: 0.16, cornerRadius: 999 ) } Text("\(measurement.averagePowerWatts.format(decimalDigits: 3)) W") .font(.system(.largeTitle, design: .rounded).weight(.bold)) .monospacedDigit() Text( "Recent drift: \((measurement.stabilityDeltaWatts * 1000).format(decimalDigits: 2)) mW, tolerance \((measurement.stabilityToleranceWatts * 1000).format(decimalDigits: 2)) mW over \(measurement.sampleCount) samples." ) .font(.footnote) .foregroundColor(.secondary) } .frame(maxWidth: .infinity, alignment: .leading) .padding(18) .meterCard( tint: measurement.isStable ? .green : .orange, fillOpacity: 0.18, strokeOpacity: 0.24 ) } private var projectionCard: some View { MeterInfoCardView( title: "Consumption Projection", infoMessage: "These projections extrapolate only from the measured standby average. They do not say anything about charger behaviour under load.", tint: .teal ) { MeterInfoRowView(label: "Average Power", value: "\(measurement.averagePowerWatts.format(decimalDigits: 3)) W") MeterInfoRowView(label: "24 Hours", value: standbyEnergyLabel(measurement.projectedDailyEnergyWh)) MeterInfoRowView(label: "7 Days", value: standbyEnergyLabel(measurement.projectedWeeklyEnergyWh)) MeterInfoRowView(label: "30 Days", value: standbyEnergyLabel(measurement.projectedMonthlyEnergyWh)) MeterInfoRowView(label: "1 Year", value: standbyEnergyLabel(measurement.projectedYearlyEnergyWh)) } } private var distributionCard: some View { MeterInfoCardView( title: "Distribution", infoMessage: "Bars show how often each power interval appeared. The curve overlays a normal approximation around the observed mean and deviation.", tint: .orange ) { StandbyPowerHistogramView( histogram: measurement.histogram, averagePowerWatts: measurement.averagePowerWatts, standardDeviationPowerWatts: measurement.standardDeviationPowerWatts, tint: .orange ) .frame(height: 220) if let firstBin = measurement.histogram.first, let lastBin = measurement.histogram.last { HStack { Text("\(firstBin.lowerBoundWatts.format(decimalDigits: 3)) W") Spacer() Text("\(lastBin.upperBoundWatts.format(decimalDigits: 3)) W") } .font(.caption) .foregroundColor(.secondary) .monospacedDigit() } } } private var statisticsCard: some View { MeterInfoCardView(title: "Interesting Stats", tint: .indigo) { MeterInfoRowView(label: "Median", value: "\(measurement.medianPowerWatts.format(decimalDigits: 3)) W") MeterInfoRowView(label: "Minimum", value: "\(measurement.minimumPowerWatts.format(decimalDigits: 3)) W") MeterInfoRowView(label: "Maximum", value: "\(measurement.maximumPowerWatts.format(decimalDigits: 3)) W") MeterInfoRowView(label: "Spread σ", value: "\(measurement.standardDeviationPowerWatts.format(decimalDigits: 4)) W") MeterInfoRowView(label: "Variation", value: "\(Int((measurement.coefficientOfVariation * 100).rounded()))%") MeterInfoRowView(label: "Mean Current", value: "\(measurement.averageCurrentAmps.format(decimalDigits: 3)) A") MeterInfoRowView(label: "Mean Voltage", value: "\(measurement.averageVoltageVolts.format(decimalDigits: 3)) V") MeterInfoRowView(label: "Power Density", value: "\(measurement.averagePowerWatts.format(decimalDigits: 3)) W steady") } } private func standbyEnergyLabel(_ wattHours: Double) -> String { if wattHours >= 1000 { return "\((wattHours / 1000).format(decimalDigits: 3)) kWh" } return "\(wattHours.format(decimalDigits: 2)) Wh" } }