|
Bogdan Timofte
authored
2 weeks ago
|
1
|
# Charging Monitoring Operation
|
|
|
2
|
|
|
|
3
|
Monitorizarea unei sesiuni de încărcare: colectarea măsurătorilor, izolarea curbei şi calculul energiei.
|
|
|
4
|
|
|
|
5
|
## Responsabilități
|
|
|
6
|
|
|
|
7
|
- Orchestrează o sesiune de încărcare complet (start → record → end)
|
|
|
8
|
- Colectează măsurători de la meter la intervale regulate (~1Hz)
|
|
|
9
|
- Izolează porţiunea relevantă a curbei (fără pre/post charging noise)
|
|
|
10
|
- Calculează totalul de energie consumată (Wh)
|
|
|
11
|
- Persistă date în Core Data + iCloud
|
|
|
12
|
|
|
|
13
|
## Invarianţi
|
|
|
14
|
|
|
|
15
|
- **MUST**: Doar o sesiune activă per meter la orice moment
|
|
|
16
|
- **MUST**: Măsurătorile sunt cronologice (sortate după timestamp)
|
|
|
17
|
- **MUST**: Start time < end time (sauf pentru sesiunile active)
|
|
|
18
|
- **MUST**: Energia totală ≥ 0 (nu poate fi negativă)
|
|
|
19
|
- **MUST**: O sesiune completată este imutabilă
|
|
|
20
|
- **SHOULD**: O sesiune durează între 5 minute şi 48 ore
|
|
|
21
|
- **MAY**: Sesiuni orphane (meter deconectat) pot fi finalizate automat
|
|
|
22
|
|
|
|
23
|
## Stadiile sesiunii
|
|
|
24
|
|
|
|
25
|
```
|
|
|
26
|
idle
|
|
|
27
|
↓
|
|
|
28
|
startSession() → starting
|
|
|
29
|
↓
|
|
|
30
|
recordMeasurement() → active (repeat)
|
|
|
31
|
↓
|
|
|
32
|
endSession() → ended
|
|
|
33
|
↓
|
|
|
34
|
completed (immutable)
|
|
|
35
|
```
|
|
|
36
|
|
|
|
37
|
## Lifecycle
|
|
|
38
|
|
|
|
39
|
### 1. Start sesiune
|
|
|
40
|
|
|
|
41
|
```swift
|
|
|
42
|
let session = meter.startChargeRecord(for: device)
|
|
|
43
|
// ⟹ ChargeRecord(
|
|
|
44
|
// id: UUID(),
|
|
|
45
|
// sessionID: UUID(),
|
|
|
46
|
// startTime: Date(),
|
|
|
47
|
// chargedDeviceID: device.id,
|
|
|
48
|
// meterID: meter.id,
|
|
|
49
|
// measurements: [],
|
|
|
50
|
// totalEnergy: 0
|
|
|
51
|
// )
|
|
|
52
|
```
|
|
|
53
|
|
|
|
54
|
**MUST:**
|
|
|
55
|
- `startTime` = `Date.now`
|
|
|
56
|
- `sessionID` = UUID unic
|
|
|
57
|
- `measurements` array = empty
|
|
|
58
|
- `totalEnergy` = 0
|
|
|
59
|
- Marchez sesiunea ca "active"
|
|
|
60
|
|
|
|
61
|
**SHOULD:**
|
|
|
62
|
- Notify UI: "Recording started"
|
|
|
63
|
- Start periodic measurement requests (1Hz timer)
|
|
|
64
|
- Log sesiune în analytics
|
|
|
65
|
|
|
|
66
|
### 2. Record measurement
|
|
|
67
|
|
|
|
68
|
```swift
|
|
|
69
|
while session.isActive {
|
|
|
70
|
let measurement = meter.lastDataPoint
|
|
|
71
|
session.addMeasurement(measurement)
|
|
|
72
|
}
|
|
|
73
|
```
|
|
|
74
|
|
|
|
75
|
**Frecvenţă**: 1Hz (1000ms interval)
|
|
|
76
|
|
|
|
77
|
**MUST:**
|
|
|
78
|
- `measurement.timestamp > session.startTime`
|
|
|
79
|
- Măsurătorile ordonate cronologic
|
|
|
80
|
- Ignore duplicate timestamps
|
|
|
81
|
- Validare: voltage, current ≥ 0
|
|
|
82
|
|
|
|
83
|
**SHOULD:**
|
|
|
84
|
- Drop măsurători dacă queue > 500 items
|
|
|
85
|
- Skip interval dacă meter nu are date nouă
|
|
|
86
|
- Log invalid measurements (don't crash)
|
|
|
87
|
|
|
|
88
|
**MAY:**
|
|
|
89
|
- Adjust frecvenţă dacă battery low (500ms → 2s)
|
|
|
90
|
- Throttle dacă temperatura critică
|
|
|
91
|
|
|
|
92
|
### 3. End sesiune
|
|
|
93
|
|
|
|
94
|
```swift
|
|
|
95
|
meter.endChargeRecord(session)
|
|
|
96
|
```
|
|
|
97
|
|
|
|
98
|
**MUST:**
|
|
|
99
|
- `endTime` = `Date.now`
|
|
|
100
|
- Calculate `totalEnergy` = ∑(V × A × Δt) pentru fiecare interval
|
|
|
101
|
- Call [Charge Curve Isolation](./ChargeCurveIsolation.md) pentru obţine valid range
|
|
|
102
|
- Marchez sesiunea ca "completed"
|
|
|
103
|
- Salvează în Core Data sync
|
|
|
104
|
|
|
|
105
|
**SHOULD:**
|
|
|
106
|
- Recalculează media power (watts)
|
|
|
107
|
- Calculează duration (seconds)
|
|
|
108
|
- Notify UI: "Recording completed"
|
|
|
109
|
- Archive sesiunea (backup iCloud)
|
|
|
110
|
|
|
|
111
|
**MAY:**
|
|
|
112
|
- Trigger conflict resolution (dacă detecta duplicate-uri)
|
|
|
113
|
- Trigger capacity learning (dacă sesiune completă)
|
|
|
114
|
|
|
|
115
|
## API Public
|
|
|
116
|
|
|
|
117
|
### Proprietăţi sesiune
|
|
|
118
|
|
|
|
119
|
| Proprietate | Tip | Descriere |
|
|
|
120
|
|---|---|---|
|
|
|
121
|
| `id` | UUID | Identificator unic |
|
|
|
122
|
| `sessionID` | UUID | Reference în persistență |
|
|
|
123
|
| `chargedDeviceID` | UUID | Device monitorizat |
|
|
|
124
|
| `meterID` | UUID | Meter folosit |
|
|
|
125
|
| `startTime` | Date | Moment start |
|
|
|
126
|
| `endTime` | Date? | Moment end (nil dacă active) |
|
|
|
127
|
| `measurements` | [Measurement] | Puncte colectate |
|
|
|
128
|
| `totalEnergy` | Double | Wh total |
|
|
|
129
|
| `isActive` | Bool | Status curent |
|
|
|
130
|
| `duration` | TimeInterval | end - start |
|
|
|
131
|
| `peakPower` | Double | Max watts |
|
|
|
132
|
| `averagePower` | Double | Mean watts |
|
|
|
133
|
|
|
|
134
|
### Metode
|
|
|
135
|
|
|
|
136
|
```swift
|
|
|
137
|
// Orchestrare
|
|
|
138
|
func startChargeRecord(for device: ChargedDevice) -> ChargeRecord
|
|
|
139
|
func recordMeasurement(_ measurement: Measurement, in session: ChargeRecord)
|
|
|
140
|
func endChargeRecord(_ session: ChargeRecord) -> ChargeRecord
|
|
|
141
|
|
|
|
142
|
// Calculaţii
|
|
|
143
|
func calculateTotalEnergy(measurements: [Measurement]) -> Double
|
|
|
144
|
func calculatePeakPower(measurements: [Measurement]) -> Double
|
|
|
145
|
func calculateAveragePower(measurements: [Measurement]) -> Double
|
|
|
146
|
|
|
|
147
|
// Persistență
|
|
|
148
|
func saveSession(_ record: ChargeRecord)
|
|
|
149
|
func loadSession(id: UUID) -> ChargeRecord?
|
|
|
150
|
```
|
|
|
151
|
|
|
|
152
|
## Comportamente critice
|
|
|
153
|
|
|
|
154
|
### Timeout pe sesiuni
|
|
|
155
|
|
|
|
156
|
- **MUST**: Dacă nu primim măsurători > 5 min, sesiunea e considerat "stale"
|
|
|
157
|
- **SHOULD**: Notifică utilizator: "No data received, disconnect?"
|
|
|
158
|
- **MAY**: Auto-finalizează după 10 min inactivitate
|
|
|
159
|
|
|
|
160
|
### Validare măsurători
|
|
|
161
|
|
|
|
162
|
```swift
|
|
|
163
|
func isValidMeasurement(_ m: Measurement) -> Bool {
|
|
|
164
|
// Voltage: 0V — 30V (USB + other)
|
|
|
165
|
// Current: 0A — 10A (typical chargers)
|
|
|
166
|
// Power: 0W — 300W (practical limit)
|
|
|
167
|
return m.voltage >= 0 && m.voltage <= 30 &&
|
|
|
168
|
m.current >= 0 && m.current <= 10 &&
|
|
|
169
|
m.power >= 0 && m.power <= 300
|
|
|
170
|
}
|
|
|
171
|
```
|
|
|
172
|
|
|
|
173
|
**MUST**: Drop invalide measurements
|
|
|
174
|
**SHOULD**: Log warnings pentru edge-case values
|
|
|
175
|
|
|
|
176
|
### Merge duplicate sessions
|
|
|
177
|
|
|
|
178
|
Se declanşează când:
|
|
|
179
|
- Două sesiuni cu overlap temporal > 50%
|
|
|
180
|
- Aceeaşi device + meter
|
|
|
181
|
|
|
|
182
|
Rezoluţie:
|
|
|
183
|
- **MUST**: Keep sesiunea cu mai multe măsurători
|
|
|
184
|
- **MUST**: Combină energiile: `total = session1.energy + session2.energy`
|
|
|
185
|
- **SHOULD**: Archive old session version
|
|
|
186
|
|
|
|
187
|
### Energia negativă (edge case)
|
|
|
188
|
|
|
|
189
|
Cauze posibile:
|
|
|
190
|
1. Meter inversează polarity (rare)
|
|
|
191
|
2. Măsurători corupte
|
|
|
192
|
3. Bug în parsing
|
|
|
193
|
|
|
|
194
|
**SHOULD**: Log warning
|
|
|
195
|
**MAY**: Absolute value: `energy = abs(energy)`
|
|
|
196
|
**NEVER**: Discard sesiune
|
|
|
197
|
|
|
|
198
|
## Testare
|
|
|
199
|
|
|
|
200
|
### Unit tests
|
|
|
201
|
|
|
|
202
|
```swift
|
|
|
203
|
// Start/end
|
|
|
204
|
test_startSessionCreatesValidRecord()
|
|
|
205
|
test_endSessionCalculatesTotalEnergy()
|
|
|
206
|
|
|
|
207
|
// Measurements
|
|
|
208
|
test_recordMeasurement_AddsToSession()
|
|
|
209
|
test_recordMeasurement_FailsIfSessionEnded()
|
|
|
210
|
test_measurementsAreOrdered()
|
|
|
211
|
test_invalidMeasurement_IsDropped()
|
|
|
212
|
|
|
|
213
|
// Calculations
|
|
|
214
|
test_calculateTotalEnergy_WithValidData()
|
|
|
215
|
test_calculateTotalEnergy_WithGaps()
|
|
|
216
|
test_calculatePeakPower()
|
|
|
217
|
test_calculateAveragePower()
|
|
|
218
|
|
|
|
219
|
// Validation
|
|
|
220
|
test_sessionTimeValidity()
|
|
|
221
|
test_sessionDuration_Max48Hours()
|
|
|
222
|
test_activeSessions_OnlyOne()
|
|
|
223
|
```
|
|
|
224
|
|
|
|
225
|
### Integration tests
|
|
|
226
|
|
|
|
227
|
- [ ] Full cycle: start → record 100 samples → end
|
|
|
228
|
- [ ] Sesiune pe device A, apoi device B (same meter)
|
|
|
229
|
- [ ] Meter disconnect → auto-finalize
|
|
|
230
|
- [ ] Energy = ∑ = match manual calculation
|
|
|
231
|
- [ ] Sesiune persistent după app restart
|
|
|
232
|
|
|
|
233
|
## Dependenţe
|
|
|
234
|
|
|
|
235
|
- [Meter.md](./Meter.md): `meter.lastDataPoint`, state
|
|
|
236
|
- [Charge Curve Isolation](./ChargeCurveIsolation.md): extract valid range
|
|
|
237
|
- [Capacity Measurement](./CapacityMeasurement.md): capacity learning
|
|
|
238
|
- [Charge Curve Storage](./ChargeCurveStorage.md): persistence
|
|
|
239
|
- Core Data: `ChargeSession` entity
|
|
|
240
|
|
|
|
241
|
## Notes
|
|
|
242
|
|
|
|
243
|
- Time-series resolution: 1Hz (1000ms)
|
|
|
244
|
- Energy formula: `E = ∑(V × A × Δt)` (integral numerical)
|
|
|
245
|
- Legat: [Charge Session Integrity](../Charge%20Session%20Integrity%20and%20Conflict%20Healing.md)
|