USB-Meter / Documentation / API Reference / IdleConsumptionMeasurement.md
1 contributor
590 lines | 14.635kb

Idle Consumption Measurement Operation

Măsurarea consumului rezidual (standby) al unui dispozitiv încărcat la maxim.

Responsabilități

  • Determinarea consumului inactiv real al unui dispozitiv (după 100% + top-up)
  • Detectare automată a stării "idle" (power stabilizat)
  • Selectare manuală a intervalului idle din grafic
  • Salvare profil idle per device
  • Utilizare pentru terminare automată sesiune + bilanț energetic

Context

Problema

După ce dispozitivul raportează 100% battery, încărcătorul nu se opreşte imediat:

Timeline:
00:00 — Device reaches 100%
00:00-00:15 — Top-up phase (unele devices încarcă semnificativ)
               Current: 1.5A → 0.8A → 0.3A → 0.05A
00:15 — Top-up terminat, device intra în idle
00:15-∞ — Idle phase (consum rezidual constant)
         Current: 0.05A constant
         Power: V × 0.05A ≈ 0.25W

Necesar: - Distinge top-up phase de idle phase - Măsoară idle power real - Folosește-o pentru bilanț energetic final

Consumul idle = baseline pentru bilanț

Total energy from charger:      50 Wh (measured)
Time charging:                  2 hours
Idle power:                      0.25 W
Energy to idle consumption:     0.25W × 2h = 0.5 Wh

Real energy to device battery: 50 - 0.5 = 49.5 Wh

⟹ Capacity learning mai precis
⟹ Bilanț charger vs device vs loss

Invarianţi

  • MUST: Device la 100% battery la start
  • MUST: Idle power < 5W (nu active usage)
  • MUST: Idle power > 0.01W (detectabil)
  • MUST: Idle interval ≥ 30 secunde (stabilitate)
  • MUST: Idle power salvat per (device, charger type)
  • SHOULD: Idle consumul constant (variance < 10%)
  • MAY: Idle profile salvat in Core Data

Lifecycle

1. Setup & initialization

Utilizatorul: 1. Încarcă device la maxim (100%) 2. Lasă conectat la charger după 100% 3. Deschide app → "Measure idle consumption" 4. Selectează dispozitiv + charger type 5. Pornește măsurare

struct IdleConsumptionSession {
    let id: UUID
    let deviceID: UUID
    let chargerType: ChargerType?
    let startTime: Date
    let measuredAt100Percent: Bool  // MUST: true
    let sessionID: UUID
}

Precondition: - Device must report 100% battery - Device connected to charger - Meter recording power

2. Wait for top-up completion

App observă power trend:

func detectIdlePhase(measurements: [Measurement]) -> Int? {
    // Last 60 measurements (1 minute)
    let recent = measurements.suffix(60)
    
    // Calculate power variance in recent window
    let powers = recent.map { $0.power }
    let mean = powers.reduce(0, +) / Double(powers.count)
    let variance = powers.map { pow($0 - mean, 2) }.reduce(0, +) / Double(powers.count)
    let stdDev = sqrt(variance)
    
    // Idle criteria:
    // 1. Power < 5W (no active charging)
    // 2. Variance < 10% of mean (stable)
    // 3. Duration > 30s at this level
    
    if mean < 5.0 && stdDev < (mean * 0.1) {
        return measurements.count - 60  // Index of phase start
    }
    return nil
}

Detection criteria: - Power < 5W (no active charging) - StdDev < 10% of mean (stable, not fluctuating) - Duration > 30 seconds (confirm stability)

Timeline: 10:00 — User starts measurement 10:15 — Top-up ends, power drops to 0.25W App auto-detects idle phase 10:15-10:45 — User reviews, confirms

MUST: Detect automatically when possible SHOULD: Notify user "Idle phase detected @ 10:15"

3. User review & selection

User vede graficul cu highlight pentru idle phase detectată:

Power (W)
20 ├───────────────────┐
   │ Top-up phase      │
10 ├───────────┐       │
   │           │       │
 5 ├───────────┤       │
   │  ↓ Idle detected   │
 0.5 ├───────────────┐ ◄─ Auto-selected interval
   │                │
   └────┬──────────┬──┴───────────→ time
        10:15      10:45
    (start idle)   (user confirmed)

User actions: - Tap to confirm auto-selected interval - Drag to refine start position - Drag to refine end position - Tap "Save idle profile"

MUST: Allow manual override SHOULD: Offer auto-selection if confidence high

4. Compute idle power

func computeIdlePower(
    measurements: [Measurement],
    from startTime: Date,
    to endTime: Date
) -> IdleConsumptionProfile {
    let filtered = measurements.filter { m in
        m.timestamp >= startTime && m.timestamp <= endTime
    }
    
    guard !filtered.isEmpty else { return nil }
    
    let powers = filtered.map { $0.power }
    let currents = filtered.map { $0.current }
    let voltages = filtered.map { $0.voltage }
    
    let avgPower = powers.reduce(0, +) / Double(powers.count)
    let stdDev = sqrt(
        powers.map { pow($0 - avgPower, 2) }.reduce(0, +) / Double(powers.count)
    )
    
    // Energy over idle period
    let duration = endTime.timeIntervalSince(startTime)
    let energy = avgPower * duration / 3600  // Wh
    
    return IdleConsumptionProfile(
        deviceID: device.id,
        chargerID: charger?.id,
        idlePower: avgPower,  // W
        stdDev: stdDev,
        energy: energy,
        sampleCount: filtered.count,
        measuredAt: Date.now,
        interval: (startTime, endTime)
    )
}

5. Save idle profile

Salvează: swift struct IdleConsumptionProfile { let id: UUID let deviceID: UUID let chargerID: UUID? // Optional, may be unknown charger let idlePower: Double // W, avg let stdDev: Double // W, variance let energy: Double // Wh, cumulative idle energy let sampleCount: Int let measuredAt: Date let interval: (start: Date, end: Date) let notes: String? }

NU salvează: Raw measurements

Storage: Core Data DeviceIdleProfile table

MUST: Salvare explicită (user confirm) SHOULD: Allow update (measure again if suspicious)

API Public

Measurement

// Start idle measurement session
func startIdleConsumptionSession(
    for device: ChargedDevice,
    charger: ChargerType?
) -> IdleConsumptionSession

// Detect idle phase automatically
func detectIdlePhase(
    in measurements: [Measurement]
) -> (startIndex: Int, confidence: Double)?

// Compute idle profile for interval
func computeIdlePower(
    measurements: [Measurement],
    from: Date,
    to: Date
) -> IdleConsumptionProfile?

// Save profile
func saveIdleProfile(_ profile: IdleConsumptionProfile) -> Bool

Query & usage

// Get idle profile for device
func loadIdleProfile(for device: ChargedDevice) -> IdleConsumptionProfile?

// Get all profiles for device (multiple charger types)
func loadIdleProfiles(for device: ChargedDevice) 
    -> [IdleConsumptionProfile]

// Get idle power estimate for device+charger combo
func getIdlePower(
    device: ChargedDevice,
    charger: ChargerType?
) -> Double?

// Update profile after new measurement
func updateIdleProfile(_ profile: IdleConsumptionProfile) -> Bool

// Delete old profile
func deleteIdleProfile(_ id: UUID) -> Bool

Comportamente critice

Top-up phase confusion

Timeline misreading:
User thinks 10:10 = idle start
But device still trickling charge @ 0.2A
Power = 1.0W (not idle yet!)
⟹ False idle profile

MUST: Require variance < 10% (not just low power) SHOULD: Alert user if variance high MAY: Auto-suggest earlier/later boundary

Multiple charger types

Device A measured with:
- Charger X: idle = 0.25W
- Charger Y: idle = 0.30W (different charger)

⟹ Store separate profiles
⟹ User selects charger type at measurement time

MUST: Key by (device, charger type) SHOULD: Allow null charger (unknown) SHOULD: Group by device in UI

Device still charging (false idle)

User hits "save" while device still @ 100%
But charger delivering 0.5A (top-up)
Power = 2.5W
⟹ This is NOT idle, it's slow charge!

MUST: Warn if battery % still 100% during interval SHOULD: Reject if power > 3W (too high for idle) MAY: Ask "Are you sure this is idle?"

Idle profile very high (> 1W)

Device idle measurement: 1.5W
(Typical idle: 0.1-0.5W)
⟹ Something unusual:
   - Screen still on?
   - Background app running?
   - Device malfunctioning?

SHOULD: Warn "Idle power seems high" SHOULD: Suggest retry with device fully idle MAY: Store with dataQuality: "warning"

Use cases

1. Auto-terminate charging session

Charging session started:
  Start battery: 20%
  Start energy: 0 Wh

During charging:
  Monitor power trend
  When power drops to idle level...
  
Idle detection trigger:
  Power < idle_threshold (e.g., 0.3W)
  Duration > 2 minutes stable
  Battery = 100%
  
⟹ Auto-terminate session
⟹ Calculate final energy

Pseudocode: ```swift func shouldAutoTerminateCharging( currentPower: Double, currentBattery: Double, duration: TimeInterval ) -> Bool { let idleProfile = loadIdleProfile(for: device) let idleThreshold = (idleProfile?.idlePower ?? 0.5) * 1.5 // 50% margin

return currentPower < idleThreshold &&
       currentBattery >= 99.0 &&
       duration > 120  // 2 min stable

} ```

2. Bilanț energetic final

Charging session:
  Meter energy:      50.0 Wh
  Duration:          2 hours
  Idle power:        0.25 W
  Idle energy:       0.25W × 2h = 0.5 Wh
  
Charger idle:        0.10 W
Charger idle energy: 0.10W × 2h = 0.2 Wh

Energy to device:    50.0 - 0.5 - 0.2 = 49.3 Wh

Accuracy improvement: +1-2% fidelity

3. Anomaly detection

Device A (iPhone):
  Normal idle: 0.15W
  Today's measurement: 0.45W  (3× higher!)
  
⟹ Alert user:
   "Device drawing 3× idle power"
   "Battery drain issue?"
   "Check background apps"

Statistici generate

Metric Unit Descriere
Idle Power W Average power during idle
StdDev W Variance (should be low)
Idle Energy Wh Total over measurement period
Sample Count # Measurements in interval
Confidence % Stability metric
Data Quality flag "good" / "warning" / "poor"

Confidence calculation

func calculateConfidence(stdDev: Double, mean: Double) -> Double {
    let relativeStdDev = stdDev / mean
    
    if relativeStdDev < 0.05 {
        return 0.95  // Excellent
    } else if relativeStdDev < 0.10 {
        return 0.85  // Good
    } else if relativeStdDev < 0.20 {
        return 0.70  // Fair
    } else {
        return 0.50  // Poor (high variance)
    }
}

Testare

Unit tests

test_detectIdlePhase_ValidData()
test_detectIdlePhase_HighVariance_NotDetected()
test_detectIdlePhase_HighPower_NotDetected()
test_detectIdlePhase_MinimumDuration()
test_computeIdlePower_ValidInterval()
test_computeIdlePower_EmptyInterval()
test_computeIdlePower_HighVariance_Warning()
test_saveIdleProfile_Persists()
test_loadIdleProfile_ByDevice()
test_multipleChargerTypes_SeparateProfiles()
test_shouldAutoTerminateCharging_WithIdleProfile()
test_energyBilantz_SubtractsIdleEnergy()
test_confidenceCalculation_RelativeVariance()

Integration tests

  • [ ] Measure idle: device 100%, charger connected
  • [ ] Auto-detect idle phase when variance drops
  • [ ] User overrides auto-selection with manual interval
  • [ ] Save idle profile → reload → same values
  • [ ] Multiple profiles per device (different chargers)
  • [ ] Auto-terminate charging when idle detected
  • [ ] Energy bilanț matches meter - idle correction
  • [ ] Anomaly detection for 3× idle power

Edge cases

Very low idle (< 0.01W)

Device truly idle: 0.008W
Below detection threshold
⟹ Impossible to measure accurately

SHOULD: "Idle power too low to measure" MAY: Skip measurement

Device powered off during measurement

User powers off device mid-measurement
Power drops to 0W
⟹ Not idle, device is off

MUST: Detect power drop to 0 SHOULD: Discard measurement MAY: Suggest "Check device is still on"

Charger unplugged mid-measurement

User unplugs charger
Power drops immediately
⟹ Not valid idle state

SHOULD: Detect sudden power drop SHOULD: Alert user "Charger disconnected?"

Very long idle measurement

User measures 24 hours of idle
Device behavior may change:
- OS updates background activity
- Bluetooth periodic sync
- GPS location update
⟹ Not constant idle

SHOULD: Warn if > 2 hours MAY: Suggest shorter interval (30 min typical)

Storage & syncing

What IS saved

{
  "id": "UUID",
  "deviceID": "UUID",
  "chargerID": "UUID?",
  "idlePower": 0.25,
  "stdDev": 0.02,
  "energy": 0.5,
  "sampleCount": 3600,
  "measuredAt": "2026-05-23T10:45:00Z",
  "confidence": 0.85,
  "dataQuality": "good"
}

What is NOT saved

❌ individual measurements
❌ raw power samples
❌ voltage/current timeseries
❌ interval timestamps (only duration)

Reason: Storage efficiency

CloudKit sync

  • SHOULD: Sync idle profiles to iCloud
  • REASON: Use on other devices (same charger type)
  • Example: iPhone idle measured on device A, used on device B

Dependenţe

Related features

Auto-terminate charging session

func shouldAutoTerminate(
    session: ChargeSession,
    currentPower: Double,
    battery: Double
) -> Bool {
    guard battery >= 99.0 else { return false }
    
    let idle = loadIdleProfile(for: session.device)
    let threshold = (idle?.idlePower ?? 0.5) * 1.5
    
    return currentPower < threshold
}

Energy balance correction

func correctedDeviceEnergy(
    total: Double,
    idleProfile: IdleConsumptionProfile,
    duration: TimeInterval
) -> Double {
    let idleEnergy = idleProfile.idlePower * duration / 3600
    return total - idleEnergy
}

Notes

  • Measurement window: Tipic 30 min — 2 ore
  • Update frequency: Every few weeks (device behavior stable)
  • Per charger: Different chargers may have different idle power
  • Privacy: No raw data, just aggregates
  • Related: Charge Session Integrity