Repository navigation
Description
Summary
An Oura-only wearer currently gets no on-device calorie estimate: the Today "Calories" tile is imported-first (Apple Health / Health Connect active_kcal), and DailyMetric.activeKcalEst — the HR-only Keytel/Harris-Benedict estimate — is never fed by the ring (OuraStreamMapping does not touch MET, and the ring's HR does not reach the estimator either).
Against the wearer's own Oura export the current activeKcalEst correlates at r = −0.10 with Oura's active_calories.
The ring already sends the right signal. 0x50 activity_info carries one MET value per 60 s (decoded on both platforms by decodeActivityInfo, but Tier B: logged to oura-activity.jsonl, never scored — OURA_PROTOCOL.md §6, "NOT ground-truth-validated"). Two things changed that this week:
- The decode is now validated at day scale against Oura's own numbers. The account export
(dailyactivity.csv) carries a 1440-minutemetseries per day at 60 s / one decimal — the same object as the wire values. Over 16 overlapping days (47k decoded minutes): Σ over minutes with MET ≥ 1.5 of (MET − 1), NOOP vs export, matches on 10/10 clean days within 1–8 % (e.g. 947 / 947, 308 / 304, 418 / 414). The four days NOOP reads 15–25 % low are the known sidecar coverage holes (minutes missing during drains), not decode error. Minute-for-minute the export is re-binned (best r 0.79 at a −2 min shift), so validation must compare sums over shared minutes. - Oura's
active_caloriesis, by construction, that sum. Over 574 full-wear days (before the May-2025
HR-intensity update):active_calories = k × Σ_{MET≥1.5}(MET − 1)with r = 0.992, single-k MAPE 12.9 %. The excess form beats full MET decisively (r 0.976 vs 0.871).total − activesits at a constant ≈ RMR. From
late 2025 the account's k steps up (HR intensity folded in) and r drops to 0.976 — still the same shape. This is what Oura documents: "1.5 METs as the lowest threshold for activities that contribute to your daily active calorie burn", and what Kristiansson et al. 2023 (BMC Med Res Methodol 23:50, Gen 2) state: "AEE is defined and starts accumulating at > 1.5 MET", relative to an undisclosed profile-based RMR.
Proposal
Compute, for an Oura ring as the active device:
active_kcal(day) = Σ over 60 s MET samples with MET ≥ 1.5 of (MET − 1) × RMR_kcal_per_day / 1440
RMRfrom the revised Harris-Benedict NOOP already has (Calories.restingKcalPerS, profile weight / height / age / sex). Zero free parameters — it is the definition of a MET-minute. Oura's own k is person-specific and undisclosed; the goal is a defensible estimate of the same quantity, not a reproduction of Oura's number.- Coverage-aware: the sum must be scaled to the covered minutes (or missing minutes filled as 1.0 MET) and the coverage exposed, because the MET stream has holes during drains — a naive sum undercounts silently.
- Feed
DailyMetric.activeKcalEst(and the Today tile'sactive_kcalsparkline) for ring days, both platforms, so theMetricsCache/ Trends path stays unchanged. Resting share via the same RMR sototal = resting + activelike Oura. - Ship default-off, Experimental, per the project's rule for a derived physiological signal, with the validation
below in the PR; flip the default once it holds on a second wearer. - This replaces the earlier idea of wiring the ring's HR into the Keytel estimator: Keytel is fitted for exercise HR
ranges, MET is the all-day signal Oura itself uses, and only the MET path has a ground truth to validate against.
Validation plan (already possible with data on file)
- 08-13 → 08-26: NOOP active_kcal vs the export's
active_caloriesover the same days — expect r ≈ 0.99 and a stable ratio (the ratio IS Oura's k / our RMR-based constant; it should not drift day to day). Current baseline r = −0.10. - Day-sum check of the decode on every new capture (Σ_{≥1.5}(MET−1) over shared minutes vs the export) is the regression test for the decode itself; it lifts
0x50from Tier B for this use. - Honest ceiling to state in the UI copy: Oura's own AEE is only within ±46–90 % of research accelerometers in free living (Kristiansson 2023, MAPE), and the ring's MET under-estimates hard efforts (runs MAPE 20–30 % vs calorimetry). An estimate, not a measurement.
Not in scope
Steps (0x7E/0x7F are model features, not counts — #9 on open_oura), strain/effort from MET (killed by the swim falsifier), and any change to WHOOP's calorie path.
Activity
feat(analytics): day energy from a MET stream — Oura's documented rul…
feat(store): persist the Oura ring's per-minute MET series — ouraMetS…
feat(analytics): analyzeDay scores calories from the owner's MET seri…
feat(oura): persist the ring's MET records and feed them to the score…
feat(settings): Experimental · Oura Calories toggle, copy and docs (r…
feat(analytics): day energy from a MET stream — Oura's documented rul…
feat(store): persist the Oura ring's per-minute MET series — ouraMetS…
feat(analytics): analyzeDay scores calories from the owner's MET seri…
feat(oura): persist the ring's MET records and feed them to the score…
feat(settings): Experimental · Oura Calories toggle, copy and docs (r…
fix(oura): a MET minute re-served under another session anchor counts…
fix(analytics): the day-cycle fold keeps the MET energy decision inst…
feat(analytics): day energy from a MET stream — Oura's documented rul…
feat(store): persist the Oura ring's per-minute MET series — ouraMetS…
121 remaining items
feat(analytics): day energy from a MET stream — Oura's documented rul…
feat(store): persist the Oura ring's per-minute MET series — ouraMetS…
feat(analytics): analyzeDay scores calories from the owner's MET seri…
feat(oura): score active calories from the ring's MET records, behind…
feat(oura): pair the MET store with its twins in the parity ledger
doc(calories): say at the MET assignment that activeKcalEst holds a t…
fix(oura): key an ended cycle's MET calories on the MET series and th…
feat(oura): active calories from the ring's own MET stream, behind a …
feat(oura): active calories from the ring's own MET stream, behind a …
feat(oura): active calories from the ring's own MET stream, behind a …
feat(oura): active calories from the ring's own MET stream, behind a …
feat(oura): active calories from the ring's own MET stream, behind a …
feat(oura): active calories from the ring's own MET stream, behind a …
feat(oura): active calories from the ring's own MET stream, behind a …
feat(oura): active calories from the ring's own MET stream, behind a …