diff --git a/bridge/README.md b/bridge/README.md index 17b4cf8..6a1e25f 100644 --- a/bridge/README.md +++ b/bridge/README.md @@ -1,8 +1,16 @@ # Bridge: Life Dashboard Companion → Nextcloud Health v3 -Minimal MVP. Forwards only the `weight` records from the Companion webhook -payload to Health v3's `POST /entries` endpoint, one entry per record, since -weight is currently the only OpenScale-relevant metric Health v3 has built in. +Forwards the Health Connect types that are both sent by the Companion app and +supported by Health v3, split across Health's three write APIs: + +- **Daily Values** (`PUT /daily-values/{metricKey}/{date}`, one upsert per + calendar day): `weight`, `steps`. +- **Measurements** (`POST /measurements`, one call per timestamped reading): + `pulse` (heart rate). + +Which metrics a given Health instance actually knows is detected at startup +via `GET /configuration`, since shipped releases can lag behind the +documented API (see "Not yet covered" below). Requires Python 3.10+ (uses `str | None` type hints). @@ -20,21 +28,50 @@ uv run server.py Point the Companion app's webhook URL at `http://:8080/webhook/health-connect`. +Set `NC_TIMEOUT_SECONDS` (default `30`) if your Nextcloud instance responds slowly. + ## Scope +- On startup, fetches `GET /configuration` and only forwards metrics the + Health instance actually knows; unknown metrics are logged and skipped + instead of failing every request with an error. Metrics it knows but has + disabled for the user are enabled automatically via `PUT /configuration`. - Verifies `X-Signature: sha256=` on every request; rejects with 401 on mismatch (Companion does not retry on 401). -- Only reads the `weight` array; everything else in the payload is ignored. -- Uses the Health Connect record `uuid` as Health's `operationId` for - idempotent retries where it parses as a UUID. +- Reads `weight`, `daily_totals` (for steps), and `heart_rate` from the + payload; everything else is ignored. +- Steps use Companion's `daily_totals`, which Health Connect already + deduplicates across sources (phone, watch, ...) into one total per day; + summing the raw per-window `steps` records ourselves would double-count, + so those are ignored in favor of the daily total. +- Weight uses the UTC calendar date of each reading as the Daily Value's + date key (Health doesn't expose the owning user's timezone to this + bridge), so a reading taken right around local midnight could land on the + "wrong" day. +- Heart rate: handles both raw records (`bpm`/`time`) and bucketed + resolution windows (`bucket_start`/`avg`); bucketed windows get a + deterministic id derived from the window since they carry no HC uuid. +- Daily Value writes are idempotent by construction (one value per + metric/date, last write wins); Measurement writes use the Health Connect + record `uuid` (or a derived id for bucketed windows) as Health's + `operationId` for idempotent retries. - Returns 502 on partial forwarding failure so the Companion app retries the whole payload later (transient failures are retried by the app). ## Not yet covered -- Body fat / muscle mass / body water (not yet built into Health v3; muscle - mass and body water aren't even on Health v3's roadmap — see the main +- Sleep duration: despite being documented in Health v3's data model, it + isn't an actual Daily Value, Measurement, or journal metric in the shipped + app yet (absent from `GET /configuration`'s metric list entirely), so + there's currently no endpoint to send it to. +- Blood pressure, body fat, blood glucose, oxygen saturation, and + temperature: the Companion app doesn't send these Health Connect types at + all yet. +- Muscle mass and body water: not on Health v3's roadmap at all (see the main README's architecture diagram). -- Blood pressure, steps, activity (add once this MVP is proven out). +- Hydration and exercise/movement: both apps have these concepts, but with + incompatible shapes (a volume/duration in Companion vs. a closed set of + event options in Health), so a faithful mapping isn't possible without + lossy guessing. - Containerized deployment (currently a local script only, per the current iteration's scope).