USB-Meter / Documentation / API Reference / IdleConsumptionMeasurement.md
Newer Older
590 lines | 14.635kb
Bogdan Timofte authored 2 weeks ago
1
# Idle Consumption Measurement Operation
2

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

            
5
## Responsabilități
6

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

            
13
## Context
14

            
15
### Problema
16

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

            
19
```
20
Timeline:
21
00:00 — Device reaches 100%
22
00:00-00:15 — Top-up phase (unele devices încarcă semnificativ)
23
               Current: 1.5A → 0.8A → 0.3A → 0.05A
24
00:15 — Top-up terminat, device intra în idle
25
00:15-∞ — Idle phase (consum rezidual constant)
26
         Current: 0.05A constant
27
         Power: V × 0.05A ≈ 0.25W
28
```
29

            
30
**Necesar:**
31
- Distinge top-up phase de idle phase
32
- Măsoară idle power real
33
- Folosește-o pentru bilanț energetic final
34

            
35
### Consumul idle = baseline pentru bilanț
36

            
37
```
38
Total energy from charger:      50 Wh (measured)
39
Time charging:                  2 hours
40
Idle power:                      0.25 W
41
Energy to idle consumption:     0.25W × 2h = 0.5 Wh
42

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

            
45
⟹ Capacity learning mai precis
46
⟹ Bilanț charger vs device vs loss
47
```
48

            
49
## Invarianţi
50

            
51
- **MUST**: Device la 100% battery la start
52
- **MUST**: Idle power < 5W (nu active usage)
53
- **MUST**: Idle power > 0.01W (detectabil)
54
- **MUST**: Idle interval ≥ 30 secunde (stabilitate)
55
- **MUST**: Idle power salvat per (device, charger type)
56
- **SHOULD**: Idle consumul constant (variance < 10%)
57
- **MAY**: Idle profile salvat in Core Data
58

            
59
## Lifecycle
60

            
61
### 1. Setup & initialization
62

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

            
70
```swift
71
struct IdleConsumptionSession {
72
    let id: UUID
73
    let deviceID: UUID
74
    let chargerType: ChargerType?
75
    let startTime: Date
76
    let measuredAt100Percent: Bool  // MUST: true
77
    let sessionID: UUID
78
}
79
```
80

            
81
**Precondition:**
82
- Device must report 100% battery
83
- Device connected to charger
84
- Meter recording power
85

            
86
### 2. Wait for top-up completion
87

            
88
App observă power trend:
89

            
90
```swift
91
func detectIdlePhase(measurements: [Measurement]) -> Int? {
92
    // Last 60 measurements (1 minute)
93
    let recent = measurements.suffix(60)
94

            
95
    // Calculate power variance in recent window
96
    let powers = recent.map { $0.power }
97
    let mean = powers.reduce(0, +) / Double(powers.count)
98
    let variance = powers.map { pow($0 - mean, 2) }.reduce(0, +) / Double(powers.count)
99
    let stdDev = sqrt(variance)
100

            
101
    // Idle criteria:
102
    // 1. Power < 5W (no active charging)
103
    // 2. Variance < 10% of mean (stable)
104
    // 3. Duration > 30s at this level
105

            
106
    if mean < 5.0 && stdDev < (mean * 0.1) {
107
        return measurements.count - 60  // Index of phase start
108
    }
109
    return nil
110
}
111
```
112

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

            
118
**Timeline:**
119
```
120
10:00 — User starts measurement
121
10:15 — Top-up ends, power drops to 0.25W
122
        App auto-detects idle phase
123
10:15-10:45 — User reviews, confirms
124
```
125

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

            
129
### 3. User review & selection
130

            
131
User vede graficul cu highlight pentru idle phase detectată:
132

            
133
```
134
Power (W)
135
20 ├───────────────────┐
136
   │ Top-up phase      │
137
10 ├───────────┐       │
138
   │           │       │
139
 5 ├───────────┤       │
140
   │  ↓ Idle detected   │
141
 0.5 ├───────────────┐ ◄─ Auto-selected interval
142
   │                │
143
   └────┬──────────┬──┴───────────→ time
144
        10:15      10:45
145
    (start idle)   (user confirmed)
146
```
147

            
148
**User actions:**
149
- Tap to confirm auto-selected interval
150
- Drag to refine start position
151
- Drag to refine end position
152
- Tap "Save idle profile"
153

            
154
**MUST**: Allow manual override
155
**SHOULD**: Offer auto-selection if confidence high
156

            
157
### 4. Compute idle power
158

            
159
```swift
160
func computeIdlePower(
161
    measurements: [Measurement],
162
    from startTime: Date,
163
    to endTime: Date
164
) -> IdleConsumptionProfile {
165
    let filtered = measurements.filter { m in
166
        m.timestamp >= startTime && m.timestamp <= endTime
167
    }
168

            
169
    guard !filtered.isEmpty else { return nil }
170

            
171
    let powers = filtered.map { $0.power }
172
    let currents = filtered.map { $0.current }
173
    let voltages = filtered.map { $0.voltage }
174

            
175
    let avgPower = powers.reduce(0, +) / Double(powers.count)
176
    let stdDev = sqrt(
177
        powers.map { pow($0 - avgPower, 2) }.reduce(0, +) / Double(powers.count)
178
    )
179

            
180
    // Energy over idle period
181
    let duration = endTime.timeIntervalSince(startTime)
182
    let energy = avgPower * duration / 3600  // Wh
183

            
184
    return IdleConsumptionProfile(
185
        deviceID: device.id,
186
        chargerID: charger?.id,
187
        idlePower: avgPower,  // W
188
        stdDev: stdDev,
189
        energy: energy,
190
        sampleCount: filtered.count,
191
        measuredAt: Date.now,
192
        interval: (startTime, endTime)
193
    )
194
}
195
```
196

            
197
### 5. Save idle profile
198

            
199
**Salvează:**
200
```swift
201
struct IdleConsumptionProfile {
202
    let id: UUID
203
    let deviceID: UUID
204
    let chargerID: UUID?  // Optional, may be unknown charger
205
    let idlePower: Double  // W, avg
206
    let stdDev: Double     // W, variance
207
    let energy: Double     // Wh, cumulative idle energy
208
    let sampleCount: Int
209
    let measuredAt: Date
210
    let interval: (start: Date, end: Date)
211
    let notes: String?
212
}
213
```
214

            
215
**NU salvează:** Raw measurements
216

            
217
**Storage:** Core Data `DeviceIdleProfile` table
218

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

            
222
## API Public
223

            
224
### Measurement
225

            
226
```swift
227
// Start idle measurement session
228
func startIdleConsumptionSession(
229
    for device: ChargedDevice,
230
    charger: ChargerType?
231
) -> IdleConsumptionSession
232

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

            
238
// Compute idle profile for interval
239
func computeIdlePower(
240
    measurements: [Measurement],
241
    from: Date,
242
    to: Date
243
) -> IdleConsumptionProfile?
244

            
245
// Save profile
246
func saveIdleProfile(_ profile: IdleConsumptionProfile) -> Bool
247
```
248

            
249
### Query & usage
250

            
251
```swift
252
// Get idle profile for device
253
func loadIdleProfile(for device: ChargedDevice) -> IdleConsumptionProfile?
254

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

            
259
// Get idle power estimate for device+charger combo
260
func getIdlePower(
261
    device: ChargedDevice,
262
    charger: ChargerType?
263
) -> Double?
264

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

            
268
// Delete old profile
269
func deleteIdleProfile(_ id: UUID) -> Bool
270
```
271

            
272
## Comportamente critice
273

            
274
### Top-up phase confusion
275

            
276
```
277
Timeline misreading:
278
User thinks 10:10 = idle start
279
But device still trickling charge @ 0.2A
280
Power = 1.0W (not idle yet!)
281
⟹ False idle profile
282
```
283

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

            
288
### Multiple charger types
289

            
290
```
291
Device A measured with:
292
- Charger X: idle = 0.25W
293
- Charger Y: idle = 0.30W (different charger)
294

            
295
⟹ Store separate profiles
296
⟹ User selects charger type at measurement time
297
```
298

            
299
**MUST**: Key by (device, charger type)
300
**SHOULD**: Allow null charger (unknown)
301
**SHOULD**: Group by device in UI
302

            
303
### Device still charging (false idle)
304

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

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

            
316
### Idle profile very high (> 1W)
317

            
318
```
319
Device idle measurement: 1.5W
320
(Typical idle: 0.1-0.5W)
321
⟹ Something unusual:
322
   - Screen still on?
323
   - Background app running?
324
   - Device malfunctioning?
325
```
326

            
327
**SHOULD**: Warn "Idle power seems high"
328
**SHOULD**: Suggest retry with device fully idle
329
**MAY**: Store with `dataQuality: "warning"`
330

            
331
## Use cases
332

            
333
### 1. Auto-terminate charging session
334

            
335
```
336
Charging session started:
337
  Start battery: 20%
338
  Start energy: 0 Wh
339

            
340
During charging:
341
  Monitor power trend
342
  When power drops to idle level...
343

            
344
Idle detection trigger:
345
  Power < idle_threshold (e.g., 0.3W)
346
  Duration > 2 minutes stable
347
  Battery = 100%
348

            
349
⟹ Auto-terminate session
350
⟹ Calculate final energy
351
```
352

            
353
**Pseudocode:**
354
```swift
355
func shouldAutoTerminateCharging(
356
    currentPower: Double,
357
    currentBattery: Double,
358
    duration: TimeInterval
359
) -> Bool {
360
    let idleProfile = loadIdleProfile(for: device)
361
    let idleThreshold = (idleProfile?.idlePower ?? 0.5) * 1.5  // 50% margin
362

            
363
    return currentPower < idleThreshold &&
364
           currentBattery >= 99.0 &&
365
           duration > 120  // 2 min stable
366
}
367
```
368

            
369
### 2. Bilanț energetic final
370

            
371
```
372
Charging session:
373
  Meter energy:      50.0 Wh
374
  Duration:          2 hours
375
  Idle power:        0.25 W
376
  Idle energy:       0.25W × 2h = 0.5 Wh
377

            
378
Charger idle:        0.10 W
379
Charger idle energy: 0.10W × 2h = 0.2 Wh
380

            
381
Energy to device:    50.0 - 0.5 - 0.2 = 49.3 Wh
382
```
383

            
384
**Accuracy improvement**: +1-2% fidelity
385

            
386
### 3. Anomaly detection
387

            
388
```
389
Device A (iPhone):
390
  Normal idle: 0.15W
391
  Today's measurement: 0.45W  (3× higher!)
392

            
393
⟹ Alert user:
394
   "Device drawing 3× idle power"
395
   "Battery drain issue?"
396
   "Check background apps"
397
```
398

            
399
## Statistici generate
400

            
401
| Metric | Unit | Descriere |
402
|---|---|---|
403
| Idle Power | W | Average power during idle |
404
| StdDev | W | Variance (should be low) |
405
| Idle Energy | Wh | Total over measurement period |
406
| Sample Count | # | Measurements in interval |
407
| Confidence | % | Stability metric |
408
| Data Quality | flag | "good" / "warning" / "poor" |
409

            
410
### Confidence calculation
411

            
412
```swift
413
func calculateConfidence(stdDev: Double, mean: Double) -> Double {
414
    let relativeStdDev = stdDev / mean
415

            
416
    if relativeStdDev < 0.05 {
417
        return 0.95  // Excellent
418
    } else if relativeStdDev < 0.10 {
419
        return 0.85  // Good
420
    } else if relativeStdDev < 0.20 {
421
        return 0.70  // Fair
422
    } else {
423
        return 0.50  // Poor (high variance)
424
    }
425
}
426
```
427

            
428
## Testare
429

            
430
### Unit tests
431

            
432
```swift
433
test_detectIdlePhase_ValidData()
434
test_detectIdlePhase_HighVariance_NotDetected()
435
test_detectIdlePhase_HighPower_NotDetected()
436
test_detectIdlePhase_MinimumDuration()
437
test_computeIdlePower_ValidInterval()
438
test_computeIdlePower_EmptyInterval()
439
test_computeIdlePower_HighVariance_Warning()
440
test_saveIdleProfile_Persists()
441
test_loadIdleProfile_ByDevice()
442
test_multipleChargerTypes_SeparateProfiles()
443
test_shouldAutoTerminateCharging_WithIdleProfile()
444
test_energyBilantz_SubtractsIdleEnergy()
445
test_confidenceCalculation_RelativeVariance()
446
```
447

            
448
### Integration tests
449

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

            
459
## Edge cases
460

            
461
### Very low idle (< 0.01W)
462

            
463
```
464
Device truly idle: 0.008W
465
Below detection threshold
466
⟹ Impossible to measure accurately
467
```
468

            
469
**SHOULD**: "Idle power too low to measure"
470
**MAY**: Skip measurement
471

            
472
### Device powered off during measurement
473

            
474
```
475
User powers off device mid-measurement
476
Power drops to 0W
477
⟹ Not idle, device is off
478
```
479

            
480
**MUST**: Detect power drop to 0
481
**SHOULD**: Discard measurement
482
**MAY**: Suggest "Check device is still on"
483

            
484
### Charger unplugged mid-measurement
485

            
486
```
487
User unplugs charger
488
Power drops immediately
489
⟹ Not valid idle state
490
```
491

            
492
**SHOULD**: Detect sudden power drop
493
**SHOULD**: Alert user "Charger disconnected?"
494

            
495
### Very long idle measurement
496

            
497
```
498
User measures 24 hours of idle
499
Device behavior may change:
500
- OS updates background activity
501
- Bluetooth periodic sync
502
- GPS location update
503
⟹ Not constant idle
504
```
505

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

            
509
## Storage & syncing
510

            
511
### What IS saved
512

            
513
```json
514
{
515
  "id": "UUID",
516
  "deviceID": "UUID",
517
  "chargerID": "UUID?",
518
  "idlePower": 0.25,
519
  "stdDev": 0.02,
520
  "energy": 0.5,
521
  "sampleCount": 3600,
522
  "measuredAt": "2026-05-23T10:45:00Z",
523
  "confidence": 0.85,
524
  "dataQuality": "good"
525
}
526
```
527

            
528
### What is NOT saved
529

            
530
```
531
❌ individual measurements
532
❌ raw power samples
533
❌ voltage/current timeseries
534
❌ interval timestamps (only duration)
535
```
536

            
537
**Reason**: Storage efficiency
538

            
539
### CloudKit sync
540

            
541
- **SHOULD**: Sync idle profiles to iCloud
542
- **REASON**: Use on other devices (same charger type)
543
- **Example**: iPhone idle measured on device A, used on device B
544

            
545
## Dependenţe
546

            
547
- [Charging Monitoring](./ChargingMonitoring.md): session context
548
- [Consumption Measurement](./ConsumptionMeasurement.md): power analysis
549
- Core Data: `DeviceIdleProfile` entity
550
- UI: Graph with selection + auto-detection highlight
551

            
552
## Related features
553

            
554
### Auto-terminate charging session
555

            
556
```swift
557
func shouldAutoTerminate(
558
    session: ChargeSession,
559
    currentPower: Double,
560
    battery: Double
561
) -> Bool {
562
    guard battery >= 99.0 else { return false }
563

            
564
    let idle = loadIdleProfile(for: session.device)
565
    let threshold = (idle?.idlePower ?? 0.5) * 1.5
566

            
567
    return currentPower < threshold
568
}
569
```
570

            
571
### Energy balance correction
572

            
573
```swift
574
func correctedDeviceEnergy(
575
    total: Double,
576
    idleProfile: IdleConsumptionProfile,
577
    duration: TimeInterval
578
) -> Double {
579
    let idleEnergy = idleProfile.idlePower * duration / 3600
580
    return total - idleEnergy
581
}
582
```
583

            
584
## Notes
585

            
586
- **Measurement window**: Tipic 30 min — 2 ore
587
- **Update frequency**: Every few weeks (device behavior stable)
588
- **Per charger**: Different chargers may have different idle power
589
- **Privacy**: No raw data, just aggregates
590
- **Related**: [Charge Session Integrity](../Charge%20Session%20Integrity%20and%20Conflict%20Healing.md)