1 contributor
301 lines | 7.659kb

Capacity Measurement Operation

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

Responsabilități

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

Concepte

Capacity vs Energy

  • Capacity (mAh): Cantitate de sarcină. Ex: 3000 mAh
  • Energy (Wh): Capacitate × voltaj nominal. Ex: 3000mAh × 3.7V = 11.1Wh

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

Invarianţi

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

Types of sessions

Full discharge (valid pentru learning)

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)

Partial charge

Battery: 20% → 50%
Device: OFF (charging)

⟹ Cannot infer capacity (unknown starting level)

MUST: Ignore pentru capacity learning

Trickle charge (edge case)

Battery: 95% → 100%
Current: <50mA (charging optimizer active)
Duration: >2 hours

⟹ Energy negligible, skip

Algorithm

Capacity learning from discharge

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
}

Weighted moving average

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
}

API Public

Proprietăţi

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

Metode

// 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%

Comportamente critice

Incompletă sesiune → skip learning

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

Multiple discharge cycles

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

Battery aging

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%

Testare

Unit tests

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

Integration tests

  • [ ] Single complete discharge 100%→0%: capacity measured
  • [ ] Partial discharge: capacity NOT measured
  • [ ] Multiple sessions: weighted average
  • [ ] Aged battery detected @ 80%
  • [ ] Capacity persistent în Core Data
  • [ ] Battery health calculated correctly

Edge cases

Rapid discharge (high power draw)

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

Trickle charge (low power)

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)

Voltage variance

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

Dependencies

References

  • Powerbank Category — battery reporting modes
  • Scientific: https://en.wikipedia.org/wiki/Battery_capacity