Newer Older
301 lines | 7.659kb
Bogdan Timofte authored 2 weeks ago
1
# Capacity Measurement Operation
2

            
3
Măsurarea şi calculul capacităţii unei baterii din sesiunile de încărcare.
4

            
5
## Responsabilități
6

            
7
- Determinarea capacităţii reale a unei baterii (mAh)
8
- Validarea completitudinii datelor (0% → 100%)
9
- Calculul din energie + voltaje nominal
10
- Tracking capacity degradation (baterie veche)
11

            
12
## Concepte
13

            
14
### Capacity vs Energy
15

            
16
- **Capacity (mAh)**: Cantitate de sarcină. Ex: 3000 mAh
17
- **Energy (Wh)**: Capacitate × voltaj nominal. Ex: 3000mAh × 3.7V = 11.1Wh
18

            
19
Formula:
20
```
21
Capacity (mAh) = Energy (Wh) × 1000 / Nominal Voltage (V)
22
```
23

            
24
Exemplu:
25
```
26
Energy = 11.1 Wh
27
Nominal voltage = 3.7V (typical Li-ion)
28
Capacity = 11.1 × 1000 / 3.7 = 3000 mAh
29
```
30

            
31
## Invarianţi
32

            
33
- **MUST**: Capacity > 0 mAh (nu poate fi 0 sau negativ)
34
- **MUST**: Capacity ≤ 10000 mAh pentru device-uri obişnuite
35
- **MUST**: Sesiune validă pentru capacity learning trebuie: battery 0% → 100%
36
- **MUST**: Nominal voltage trebuie specificat (nu se calculează)
37
- **MUST**: Capacity learning doar din **complete discharge cycles**
38
- **SHOULD**: Capacity măsurat ≥ 80% din rated capacity (bateria e "OK")
39
- **MAY**: Capacity < 80% = aged battery, notify user
40

            
41
## Types of sessions
42

            
43
### Full discharge (valid pentru learning)
44

            
45
```
46
Battery: 100% → ... → 0%
47
Device: ON throughout
48
Meter: record-ează energie totală
49

            
50
⟹ Capacity = Energy / Nominal_Voltage
51
```
52

            
53
**Invarianţi:**
54
- **MUST**: Start @ ~100%, end @ ~0%
55
- **MUST**: Fără interruption (meter disconnect = invalid)
56
- **MUST**: Device rămâne ON (nu suspend mid-session)
57

            
58
### Partial charge
59

            
60
```
61
Battery: 20% → 50%
62
Device: OFF (charging)
63

            
64
⟹ Cannot infer capacity (unknown starting level)
65
```
66

            
67
**MUST**: Ignore pentru capacity learning
68

            
69
### Trickle charge (edge case)
70

            
71
```
72
Battery: 95% → 100%
73
Current: <50mA (charging optimizer active)
74
Duration: >2 hours
75

            
76
⟹ Energy negligible, skip
77
```
78

            
79
## Algorithm
80

            
81
### Capacity learning from discharge
82

            
83
```swift
84
func learnCapacityFromDischarge(session: ChargeRecord) -> Double? {
85
    // 1. Validate session
86
    guard isCompleteDischargeSession(session) else { return nil }
87

            
88
    // 2. Extract energy
89
    let energyWh = session.totalEnergy
90

            
91
    // 3. Get nominal voltage
92
    let nominalVoltageV = device.nominalBatteryVoltage // 3.7, 5.0, etc
93

            
94
    // 4. Calculate capacity
95
    let capacityMah = energyWh * 1000 / nominalVoltageV
96

            
97
    // 5. Validate result
98
    guard capacityMah > 0 && capacityMah <= 10000 else {
99
        logWarning("Capacity outside bounds: \(capacityMah) mAh")
100
        return nil
101
    }
102

            
103
    return capacityMah
104
}
105

            
106
func isCompleteDischargeSession(_ session: ChargeRecord) -> Bool {
107
    // Check battery levels
108
    guard let startBattery = session.startBatteryPercent,
109
          let endBattery = session.endBatteryPercent else {
110
        return false
111
    }
112

            
113
    // Range must span ~100%
114
    let range = startBattery - endBattery
115
    guard range >= 90 else {
116
        logWarning("Incomplete discharge: \(range)% only")
117
        return false
118
    }
119

            
120
    // Device must be ON during discharge
121
    guard session.isCompletelyRecorded else {
122
        logWarning("Session incomplete (meter disconnect)")
123
        return false
124
    }
125

            
126
    return true
127
}
128
```
129

            
130
### Weighted moving average
131

            
132
Pentru stabilitate, combinaţi multiple measurements:
133

            
134
```swift
135
func estimateCapacity(from sessions: [ChargeRecord]) -> Double? {
136
    let validSessions = sessions.compactMap { session in
137
        learnCapacityFromDischarge(session)
138
    }
139

            
140
    guard !validSessions.isEmpty else { return nil }
141

            
142
    // Weight by recency: newer sessions count more
143
    let weights = validSessions.indices.map { i in
144
        Double(i + 1) / Double(validSessions.count)
145
    }
146

            
147
    let weightedSum = zip(validSessions, weights)
148
        .map { capacity, weight in capacity * weight }
149
        .reduce(0, +)
150

            
151
    let capacityMah = weightedSum / weights.reduce(0, +)
152

            
153
    return capacityMah
154
}
155
```
156

            
157
## API Public
158

            
159
### Proprietăţi
160

            
161
| Proprietate | Tip | Descriere |
162
|---|---|---|
163
| `nominalBatteryVoltage` | Double | V (ex: 3.7) |
164
| `measuredCapacity` | Double? | mAh din learning |
165
| `ratedCapacity` | Double? | mAh din device spec |
166
| `capacity_Health` | Double | measured / rated (%) |
167
| `lastCapacityMeasurement` | Date? | When learned |
168

            
169
### Metode
170

            
171
```swift
172
// Learning
173
func learnCapacity(from session: ChargeRecord) -> Double?
174
func learnCapacity(from sessions: [ChargeRecord]) -> Double?
175

            
176
// Validation
177
func isValidCapacityMeasurement(_ mah: Double) -> Bool
178
func isCompleteDischargeSession(_ session: ChargeRecord) -> Bool
179
func estimateSessionCompleteness(_ session: ChargeRecord) -> Double
180

            
181
// Health
182
func batteryHealth() -> Double // measured / rated
183
func isAgedBattery() -> Bool // health < 80%
184
```
185

            
186
## Comportamente critice
187

            
188
### Incompletă sesiune → skip learning
189

            
190
```
191
Device: 30% → 15%
192
Meter: record-ează 5 Wh
193
⟹ Skip (partial discharge, nu putem infera capacity)
194

            
195
Device: 100% → 5% (but meter lost signal @ 20%)
196
⟹ Skip (incomplete recording)
197
```
198

            
199
**MUST**: Validate completeness înainte de learning
200

            
201
### Multiple discharge cycles
202

            
203
```
204
Session 1: 100% → 0%, measured 12 Wh → 3200 mAh
205
Session 2: 100% → 0%, measured 11.5 Wh → 3100 mAh
206
Session 3: 100% → 0%, measured 11.2 Wh → 3000 mAh
207
⟹ Average: 3100 mAh (weighted toward recent)
208
```
209

            
210
**SHOULD**: Combinaţi sesiuni (moving average)
211
**MUST**: Keep history pentru trend analysis
212

            
213
### Battery aging
214

            
215
```
216
Device created: 2024-01-01, rated 3000 mAh
217
Measured capacities:
218
  - 2024-01-15: 2950 mAh (98% health)
219
  - 2024-06-15: 2700 mAh (90% health)
220
  - 2025-01-15: 2400 mAh (80% health) ← aged
221
⟹ Notify user: "Battery health below 80%"
222
```
223

            
224
**SHOULD**: Track degradation curve
225
**SHOULD**: Notify @ 80% health
226
**MAY**: Suggest battery replacement @ 70%
227

            
228
## Testare
229

            
230
### Unit tests
231

            
232
```swift
233
test_learnCapacity_ValidDischargeSession()
234
test_learnCapacity_PartialDischarge_Ignored()
235
test_learnCapacity_IncompleteRecording_Ignored()
236
test_isCompleteDischargeSession_ValidRange()
237
test_isCompleteDischargeSession_MinRange()
238
test_estimateCapacity_MultiplesSessions()
239
test_estimateCapacity_WeightedByRecency()
240
test_capacityMustBePositive()
241
test_capacityMustBeBounded()
242
test_batteryHealth_Calculation()
243
test_isAgedBattery_Threshold()
244
```
245

            
246
### Integration tests
247

            
248
- [ ] Single complete discharge 100%→0%: capacity measured
249
- [ ] Partial discharge: capacity NOT measured
250
- [ ] Multiple sessions: weighted average
251
- [ ] Aged battery detected @ 80%
252
- [ ] Capacity persistent în Core Data
253
- [ ] Battery health calculated correctly
254

            
255
## Edge cases
256

            
257
### Rapid discharge (high power draw)
258

            
259
```
260
Battery: 100% → 0% in 30 minutes
261
Power draw: 50W constant
262
Energy: large, measured accurately
263
⟹ Normal discharge, capacity = energy / voltage
264
```
265

            
266
**SHOULD**: Accept și use pentru learning
267

            
268
### Trickle charge (low power)
269

            
270
```
271
Battery: 95% → 100%
272
Charging current: 10mA (very low)
273
Duration: 4 hours
274
Energy: negligible (~0.2Wh)
275
⟹ Skip (too little data)
276
```
277

            
278
**MUST**: Ignore (capacity = energy/voltage → false low)
279

            
280
### Voltage variance
281

            
282
```
283
Nominal voltage: 3.7V
284
But actual range: 3.2V (low) → 4.2V (full)
285
Energy measured: 11Wh
286
Capacity calculation: depends on which voltage used
287
```
288

            
289
**SHOULD**: Use nominal (convention)
290
**SHOULD**: Log warning dacă voltage deviates
291

            
292
## Dependencies
293

            
294
- [Charging Monitoring](./ChargingMonitoring.md): session data
295
- [Charge Curve Storage](./ChargeCurveStorage.md): historical curves
296
- Core Data: `ChargeSession.startBatteryPercent`, `endBatteryPercent`
297

            
298
## References
299

            
300
- [Powerbank Category](../Powerbank%20Category.md) — battery reporting modes
301
- Scientific: https://en.wikipedia.org/wiki/Battery_capacity