Newer Older
245 lines | 6.627kb
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)