Explain empty SpO2/respiration Deep-timeline tracks on WHOOP 5.0 (#623) - #673
Merged
Conversation
On a 5.0/MG the Deep-timeline SpO2 and Respiration tracks are permanently empty — both plot raw per-second ADC streams that only a WHOOP 4.0 emits. The 5.0 v18 wire has no SpO2 channel at all (and a calibrated % needs WHOOP's proprietary curve, which NOOP won't fabricate), and no raw respiration channel (respiration on a 5.0 is instead an R-R/RSA estimate shown on the Health screen). Previously both showed a generic "nothing offloaded", which reads as a bug — the report in #623. Give those two tracks a model-aware empty state (resolved via the canonical DeviceFamily.forRegistryModel, not a string compare): SpO2 points at import, respiration points at the Health screen's estimate. Strap view only; with imports included the generic copy still applies. Both platforms (Android FullDayChartScreen + iOS FullDayChartView, new Repository.activeStrapFamily). No data change — this only makes an existing, documented limitation legible. New strings localized de/es/fr/zh (Android). Verified: compileFullDebugKotlin, i18n_audit (0 gaps), lintVitalFullRelease -PstagingRelease. iOS is app-target Swift (no Linux/CI build) — compiles on the next staging build; de/es/fr catalog entries for the 2 new iOS strings are a translator follow-up (English fallback until then). Author: ryanbr <mp3geek@gmail.com>
DeviceFamily.forRegistryModel resolves nil/unknown/legacy-bare-"WHOOP" models to .whoop5 (its documented fallback), so a legacy-paired WHOOP 4.0 on a firmware layout that decodes no SpO2 (v25/v5/7/9 → empty track) would resolve isWhoop5=true and be told "WHOOP 5.0 doesn't send SpO2" — a false claim about its model. Keep the canonical resolver as the gate (per #171), but make the copy model-agnostic ("This strap ...") so it's true for a real 5.0 AND for any ambiguous device that reaches it. Both platforms + locales. Author: ryanbr <mp3geek@gmail.com>
…623) Re-review follow-up. The empty-state message was gated only on the 5.0 FAMILY, but DeviceFamily.forRegistryModel maps legacy bare-"WHOOP" models to the 5.0 family, and a 4.0-v24 DOES bank SpO2. So a legacy-paired 4.0-v24 could show its SpO2 track on some days yet say "this strap doesn't send SpO2" on empty windows — a self-contradiction. Gate the copy on 5.0-family AND the strap having NEVER produced that metric (a cheap limit-1 sample read on both platforms), so a strap with any history keeps the neutral "nothing offloaded". everSpo2/everResp default true so a 4.0-v24 never flashes the wrong message before the reads resolve. Also add docs/WHOOP5_DEEP_DATA.md § "Why SpO2 ... aren't available on 5.0" — the canonical, linkable explanation (not encrypted; absent from the wire; needs WHOOP's calibration; 4.0-v24 difference; import workaround) for the recurring "why isn't SpO2 supported" question. Verified: compileFullDebugKotlin, i18n_audit (0 gaps), lintVitalFullRelease -PstagingRelease. iOS (Repository.strapHasEverProduced + view) compiles on the next staging build. Author: ryanbr <mp3geek@gmail.com>
ryanbr
added a commit
that referenced
this pull request
Jul 20, 2026
Marketing version stays 9.0.3; Android versionCode 296->297 and iOS CURRENT_PROJECT_VERSION 206->207 so the new staging build installs over the previous one. Content since the last testing build: Health Connect writeback failure surfacing (#672), SpO2/respiration empty-track explanation (#673), WHOOP 5/MG raw-IMU offload persist (#675), hide stale sleep recompute entries (#679), Oura activity MET research corpus + cap/rotation (#676/#680), trend chart date alignment (#528), and corrupt-DB quarantine preservation (#681).
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
…nbr#623) (ryanbr#673) Model-aware empty state for the Deep-timeline SpO2/respiration tracks: a 5.0-family strap that has never produced the metric shows why (SpO2 → import; respiration → Health screen's RSA estimate) instead of a generic blank that reads as broken. Resolved via DeviceFamily.forRegistryModel + a never-produced check so a legacy 4.0-v24 with data keeps the neutral copy. Both platforms; iOS compile verified. Adds docs/WHOOP5_DEEP_DATA.md § on why SpO2 isn't decodable on 5.0. No data change. Fixes ryanbr#623.
DX23876
pushed a commit
to DX23876/noop
that referenced
this pull request
Jul 22, 2026
Marketing version stays 9.0.3; Android versionCode 296->297 and iOS CURRENT_PROJECT_VERSION 206->207 so the new staging build installs over the previous one. Content since the last testing build: Health Connect writeback failure surfacing (ryanbr#672), SpO2/respiration empty-track explanation (ryanbr#673), WHOOP 5/MG raw-IMU offload persist (ryanbr#675), hide stale sleep recompute entries (ryanbr#679), Oura activity MET research corpus + cap/rotation (ryanbr#676/ryanbr#680), trend chart date alignment (ryanbr#528), and corrupt-DB quarantine preservation (ryanbr#681).
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Addresses #623 (WHOOP 5.0: "no SpO2 and respiration data captured or shown").
What's actually happening
This isn't recoverable data that's hiding — for a 5.0/MG both Deep-timeline tracks are permanently empty, and the app just never said why:
README.md:255. Not encrypted — decoded, just not present as a usable value.SleepStager.respRateFromRR), a different surface with a different source. That mismatch is what confused the reporter.So there is no data fix possible here — this makes the existing, documented limitation legible instead of a blank that reads as broken.
Change
Model-aware empty state for the SpO2 + Respiration tracks, resolved via the canonical
DeviceFamily.forRegistryModel(#171 — never a string compare):Strap view only (
ownedOnly) — with other sources included, the generic "nothing offloaded" copy still applies (imported SpO2 can exist). No model-gating hides the pills; they render the honest reason.Both platforms:
FullDayChartScreen.EmptyTimelineState+isWhoop5resolved fromvm.pairedDevices().FullDayChartView.emptyReason+ newRepository.activeStrapFamily()(mirrors the existingskinTempFamiliesregistry read).Not in scope
The related SpO2 capability-label / Health-card work (registry advertises
spo2it never produces) is #548 — left there. This PR is only the Deep-timeline empty state the reporter hit.Verification
./gradlew compileFullDebugKotlin✓python3 Tools/i18n_audit.py✓ (0 gaps; new Android strings localized de/es/fr/zh)./gradlew lintVitalFullRelease -PstagingRelease✓Suggested issue reply
Close as a documented 5.0 limitation with the import workaround: WHOOP CSV export carries
blood_oxygen_pct, whichWhoopCsvImportermaps straight to the Blood Oxygen card — the only way to see SpO2 alongside NOOP on a 5.0.