Skip to content

Read glucose and body weight back out of Apple Health - #128

Open
ak710 wants to merge 2 commits into
saksham2001:mainfrom
ak710:feat/healthkit-import
Open

Read glucose and body weight back out of Apple Health#128
ak710 wants to merge 2 commits into
saksham2001:mainfrom
ak710:feat/healthkit-import

Conversation

@ak710

@ak710 ak710 commented Aug 3, 2026

Copy link
Copy Markdown

Closes #119

Merge order: 9 of 9. ⚠️ Stacked on #122 — this diff also contains that commit. Merge #122 first.

What

The Health integration only ever wrote. Reading is what makes CGM data work: a continuous glucose monitor writes bloodGlucose to Health, PulseLoop reads it, and from then on it sits in the same store the coach already queries beside the ring's sleep and heart rate. Oura's whole metabolic-health story is this read.

Import is its own opt-in, default off, separate from the export master toggle. "Show my ring data elsewhere" and "let other apps' data into mine" are different decisions with different privacy weight; one switch would make one of them implicit.

The loop guard

This is the part that needed most care. With #122 merged PulseLoop exports glucose and imports it, so unguarded one reading goes round forever: import → export as ours → import back → export again.

Guarded on both sides, because they fail differently:

  1. Every read excludes this app's own HKSource.
  2. Imported rows carry MeasurementSource.appleHealth, which the export predicates now exclude — three predicates in HealthSyncService needed that added.

Either alone would close the loop; both are in place, and a test pins each.

Dedup

Imported readings key on (kind, instant, imported) — the same rule every other history path uses. A CGM revising a reading in place updates the row rather than stacking a second beside it, and an unchanged re-import writes nothing.

An import never edits a ring row at the same instant: those are two claims about one moment, and the ring's isn't something an import may overwrite (testImportDoesNotTouchRingRowsAtTheSameInstant).

Values pass through RingEventBridge's own gate, so an implausible third-party reading is refused on exactly the same terms as an implausible ring one.

Deliberately not imported

Steps and workouts. Both double-count against ring data: Health's step count includes the iPhone's pedometer, and a ring workout PulseLoop exported would return as a second session. Merging those needs provenance-aware reconciliation this doesn't have, so it doesn't pretend to.

Body mass updates the profile, not a measurement row — that's where the calorie model and BMI already read weight from, and a second home would let the two disagree.

Import watermarks are a separate map from the export ones, so a full re-export never also re-imports a year of glucose.

Testing

HealthImportTests — 13 tests on provenance, dedup, the loop guard and prefs. Full suite green (895). Docs: docs/project/apple-health.md covers both directions.

⚠️ I don't have a CGM, so the live read path is unverified on device — the tests cover the dedup and provenance logic, which is where the correctness risk sits, but someone with a CGM confirming the end-to-end pull would be worth having.

ak710 and others added 2 commits August 2, 2026 14:32
Respiratory rate, VO2max, blood glucose and blood pressure were all decoded,
stored and displayed in-app, but the export path mapped every one of them to
nil. Three were a genuine follow-up; blood pressure needed a different shape
entirely.

Respiratory rate, VO2max and glucose join the quantity path with the units
HealthKit expects (count/min, mL/(kg*min), mg/dL) and the same plausibility
bounds RingEventBridge already applies on the way into the store.

Blood pressure gets its own pass, because Health only recognises a reading when
systolic and diastolic are saved together in an HKCorrelation — saved separately
they are stored but never surface in the Health app, which is indistinguishable
from a silent failure. The two halves are written from one packet at one
instant, so that shared timestamp is the pairing key; a half without its partner
is skipped rather than guessed at, and a pair with systolic at or below
diastolic is rejected as a misframed packet. Its watermark reuses the
bloodPressureSystolic slot, which the quantity path never touches, so backfill
and reset keep working unchanged.

Stress and fatigue stay unmapped, and the comment now says why properly: this
isn't a follow-up, HealthKit has no type for a device-derived wellness score.
HKStateOfMind is a self-reported mood log and would misrepresent both.

The four new toggles only appear for rings that can produce the metric — or that
already have — since respiratory rate and VO2max come only from the YCBT
records and glucose and BP only from jring and some YCBT units. A jring owner
never sees a VO2max switch that could not write anything.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The Health integration only ever wrote. Reading is what makes CGM data work: a
continuous glucose monitor writes bloodGlucose to Health, PulseLoop reads it,
and from then on it sits in the same store the coach already queries beside the
ring's sleep and heart rate. Oura's whole metabolic-health story is this read.

Import is its own opt-in, default off, separate from the export master toggle.
"Show my ring data elsewhere" and "let other apps' data into mine" are different
decisions with different privacy weight; one switch would make one of them
implicit.

The loop guard is the part that matters. PulseLoop now both exports and imports
glucose, so unguarded, one reading would go round forever: import it, export it
as ours, import it back. Both directions are guarded, because they fail
differently — every read excludes this app's own HKSource, and imported rows
carry MeasurementSource.appleHealth, which the export predicates now exclude.
A test pins each.

Imported readings dedup on (kind, instant, imported), the same rule every other
history path uses, so a CGM revising a reading in place updates the row rather
than stacking a second beside it. An import never edits a ring row at the same
instant: those are two claims about one moment, and the ring's isn't something
an import may overwrite. Values pass through RingEventBridge's own gate, so an
implausible third-party reading is refused on exactly the same terms as an
implausible ring one.

Body mass updates the profile rather than becoming a measurement row — that is
where the calorie model and BMI already read weight from, and a second home
would let the two disagree.

Steps and workouts are deliberately not imported. Both would double-count
against what the ring records: Health's step count includes the iPhone's own
pedometer, and a ring workout PulseLoop exported would return as a second
session. Merging those needs provenance-aware reconciliation this doesn't have,
so it doesn't pretend to.

Import watermarks are a separate map from the export ones, so a full re-export
never also re-imports a year of glucose.

Both directions documented in docs/project/apple-health.md.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@ak710
ak710 requested a review from saksham2001 as a code owner August 3, 2026 06:25
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Apple Health is export-only — no CGM or scale data can come in

1 participant