Repository navigation
Description
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)
LiveState.batteryRatedHoursdefaults toBatteryEstimator.ratedLifeHoursWhoop4(Strand/BLE/LiveState.swift:221). OnlyBLEManager.connectCoreswitches it to the 5/MG figure. The restore path,centralManager(_:willRestoreState:)(Strand/BLE/BLEManager.swift:6507), setsselectedModel = .persistedand re-derives the reassembler / router family, but never touchesbatteryRatedHours. 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 %.LiveState.clearBiometrics()emptiesbatterySampleson every disconnect (LiveState.swift:765), but the 14-day seed from the store runs only insidebootstrapStoreOnce(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
selectedModelin one place (for example adidSetonselectedModel, plus one call aftersuper.init()sincedidSetdoes not run for the initial.persistedvalue), and drop the separate assignment inconnectCore. - Extract the 14-day seed into a method, keep the store from the bootstrap, and call the seed again in
didConnect.seedBatterySamplesalready de-dupes by timestamp, so repeating it is safe. - Tests that need no strap: a manager created with
selectedWhoopModel = "WHOOP 5.0 / MG"persisted hasbatteryRatedHours == ratedLifeHoursWhoop5; afterclearBiometrics(), seeding from a tempWhoopStorewith 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