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