Măsurarea şi calculul capacităţii unei baterii din sesiunile de încărcare.
Formula:
Capacity (mAh) = Energy (Wh) × 1000 / Nominal Voltage (V)
Exemplu:
Energy = 11.1 Wh
Nominal voltage = 3.7V (typical Li-ion)
Capacity = 11.1 × 1000 / 3.7 = 3000 mAh
Battery: 100% → ... → 0%
Device: ON throughout
Meter: record-ează energie totală
⟹ Capacity = Energy / Nominal_Voltage
Invarianţi: - MUST: Start @ ~100%, end @ ~0% - MUST: Fără interruption (meter disconnect = invalid) - MUST: Device rămâne ON (nu suspend mid-session)
Battery: 20% → 50%
Device: OFF (charging)
⟹ Cannot infer capacity (unknown starting level)
MUST: Ignore pentru capacity learning
Battery: 95% → 100%
Current: <50mA (charging optimizer active)
Duration: >2 hours
⟹ Energy negligible, skip
func learnCapacityFromDischarge(session: ChargeRecord) -> Double? {
// 1. Validate session
guard isCompleteDischargeSession(session) else { return nil }
// 2. Extract energy
let energyWh = session.totalEnergy
// 3. Get nominal voltage
let nominalVoltageV = device.nominalBatteryVoltage // 3.7, 5.0, etc
// 4. Calculate capacity
let capacityMah = energyWh * 1000 / nominalVoltageV
// 5. Validate result
guard capacityMah > 0 && capacityMah <= 10000 else {
logWarning("Capacity outside bounds: \(capacityMah) mAh")
return nil
}
return capacityMah
}
func isCompleteDischargeSession(_ session: ChargeRecord) -> Bool {
// Check battery levels
guard let startBattery = session.startBatteryPercent,
let endBattery = session.endBatteryPercent else {
return false
}
// Range must span ~100%
let range = startBattery - endBattery
guard range >= 90 else {
logWarning("Incomplete discharge: \(range)% only")
return false
}
// Device must be ON during discharge
guard session.isCompletelyRecorded else {
logWarning("Session incomplete (meter disconnect)")
return false
}
return true
}
Pentru stabilitate, combinaţi multiple measurements:
func estimateCapacity(from sessions: [ChargeRecord]) -> Double? {
let validSessions = sessions.compactMap { session in
learnCapacityFromDischarge(session)
}
guard !validSessions.isEmpty else { return nil }
// Weight by recency: newer sessions count more
let weights = validSessions.indices.map { i in
Double(i + 1) / Double(validSessions.count)
}
let weightedSum = zip(validSessions, weights)
.map { capacity, weight in capacity * weight }
.reduce(0, +)
let capacityMah = weightedSum / weights.reduce(0, +)
return capacityMah
}
| Proprietate | Tip | Descriere |
|---|---|---|
nominalBatteryVoltage |
Double | V (ex: 3.7) |
measuredCapacity |
Double? | mAh din learning |
ratedCapacity |
Double? | mAh din device spec |
capacity_Health |
Double | measured / rated (%) |
lastCapacityMeasurement |
Date? | When learned |
// Learning
func learnCapacity(from session: ChargeRecord) -> Double?
func learnCapacity(from sessions: [ChargeRecord]) -> Double?
// Validation
func isValidCapacityMeasurement(_ mah: Double) -> Bool
func isCompleteDischargeSession(_ session: ChargeRecord) -> Bool
func estimateSessionCompleteness(_ session: ChargeRecord) -> Double
// Health
func batteryHealth() -> Double // measured / rated
func isAgedBattery() -> Bool // health < 80%
Device: 30% → 15%
Meter: record-ează 5 Wh
⟹ Skip (partial discharge, nu putem infera capacity)
Device: 100% → 5% (but meter lost signal @ 20%)
⟹ Skip (incomplete recording)
MUST: Validate completeness înainte de learning
Session 1: 100% → 0%, measured 12 Wh → 3200 mAh
Session 2: 100% → 0%, measured 11.5 Wh → 3100 mAh
Session 3: 100% → 0%, measured 11.2 Wh → 3000 mAh
⟹ Average: 3100 mAh (weighted toward recent)
SHOULD: Combinaţi sesiuni (moving average) MUST: Keep history pentru trend analysis
Device created: 2024-01-01, rated 3000 mAh
Measured capacities:
- 2024-01-15: 2950 mAh (98% health)
- 2024-06-15: 2700 mAh (90% health)
- 2025-01-15: 2400 mAh (80% health) ← aged
⟹ Notify user: "Battery health below 80%"
SHOULD: Track degradation curve SHOULD: Notify @ 80% health MAY: Suggest battery replacement @ 70%
test_learnCapacity_ValidDischargeSession()
test_learnCapacity_PartialDischarge_Ignored()
test_learnCapacity_IncompleteRecording_Ignored()
test_isCompleteDischargeSession_ValidRange()
test_isCompleteDischargeSession_MinRange()
test_estimateCapacity_MultiplesSessions()
test_estimateCapacity_WeightedByRecency()
test_capacityMustBePositive()
test_capacityMustBeBounded()
test_batteryHealth_Calculation()
test_isAgedBattery_Threshold()
Battery: 100% → 0% in 30 minutes
Power draw: 50W constant
Energy: large, measured accurately
⟹ Normal discharge, capacity = energy / voltage
SHOULD: Accept și use pentru learning
Battery: 95% → 100%
Charging current: 10mA (very low)
Duration: 4 hours
Energy: negligible (~0.2Wh)
⟹ Skip (too little data)
MUST: Ignore (capacity = energy/voltage → false low)
Nominal voltage: 3.7V
But actual range: 3.2V (low) → 4.2V (full)
Energy measured: 11Wh
Capacity calculation: depends on which voltage used
SHOULD: Use nominal (convention) SHOULD: Log warning dacă voltage deviates
ChargeSession.startBatteryPercent, endBatteryPercent