Read glucose and body weight back out of Apple Health - #128
Open
ak710 wants to merge 2 commits into
Open
Conversation
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>
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.
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
bloodGlucoseto 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:
HKSource.MeasurementSource.appleHealth, which the export predicates now exclude — three predicates inHealthSyncServiceneeded 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.mdcovers both directions.