Skip to content

iOS: restored WHOOP 5.0/MG battery estimate falls back to the WHOOP 4.0 rated life (108 h) #2887

Open
kvnloo/noop
#146

Description

@DX23876

Summary

On iOS, the strap-battery runtime estimate for a WHOOP 5.0 / MG can fall back to the WHOOP 4.0 rated life (108 h) after the app is relaunched in the background by BLE state restoration. The screen then shows roughly a third of the expected runtime.

Reported by a WHOOP 5.0 user: the Strap battery screen read "About 12 days" one day, then after the night "91 %, 4.30 V, About 4 days remaining". 91 % × 108 h = 98 h ≈ 4.1 days, which matches the 4.0 fallback exactly; the 5.0 figure would be 91 % × 288 h ≈ 10.9 days.

Cause (as of 2a1de76)

  1. LiveState.batteryRatedHours defaults to BatteryEstimator.ratedLifeHoursWhoop4 (Strand/BLE/LiveState.swift:221). Only BLEManager.connectCore switches it to the 5/MG figure. The restore path, centralManager(_:willRestoreState:) (Strand/BLE/BLEManager.swift:6507), sets selectedModel = .persisted and re-derives the reassembler / router family, but never touches batteryRatedHours. A relaunched process therefore estimates a 5/MG against 108 h until the user taps Connect.
    Because of the Battery time remainder estimate #99 cap (ratedHours * 1.5 * current/100), a measured slope cannot rescue it either: with 108 h the estimate is capped at about 6 days at 91 %.
  2. LiveState.clearBiometrics() empties batterySamples on every disconnect (LiveState.swift:765), but the 14-day seed from the store runs only inside bootstrapStoreOnce (BLEManager.swift:1563), i.e. once per process. A reconnect within the same session starts from an empty buffer, so the estimate sits on the rated fallback until 2 h / 2 % of new discharge are banked. Together with (1), that fallback is the wrong one.

Android is not affected: it chooses the rated life from the detected family at each read.

Suggested fix

  • Derive the rated life from selectedModel in one place (for example a didSet on selectedModel, plus one call after super.init() since didSet does not run for the initial .persisted value), and drop the separate assignment in connectCore.
  • Extract the 14-day seed into a method, keep the store from the bootstrap, and call the seed again in didConnect. seedBatterySamples already de-dupes by timestamp, so repeating it is safe.
  • Tests that need no strap: a manager created with selectedWhoopModel = "WHOOP 5.0 / MG" persisted has batteryRatedHours == ratedLifeHoursWhoop5; after clearBiometrics(), seeding from a temp WhoopStore with two SoC rows restores the buffer and the estimate source is .measured.

Verification on hardware still open: the cause is derived from the code and the reported numbers, not from a log with a Restore: line from that night.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions