Files
private_health/bridge/README.md
T
Matthias Jacob 9865b43945 bridge: update README for the Daily Values / Measurements architecture
Replace the outdated /entries-only description with the three-endpoint
model, document daily_totals-based steps sourcing, and move sleep_duration
to "not yet covered" with an explanation of why (not implemented in
Health v3 at all, not just disabled for this user).
2026-09-17 03:49:04 +02:00

78 lines
3.6 KiB
Markdown

# Bridge: Life Dashboard Companion → Nextcloud Health v3
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).
## Setup
```bash
# One-time: obtain a Nextcloud app password via Login Flow v2.
# Opens a browser tab, confirm the login there, then this writes credentials.json.
uv run nc_login_flow.py https://your-nextcloud.example.com
# Set the same HMAC secret configured under "Webhook Headers" in the Companion app.
export NC_WEBHOOK_SECRET="..."
uv run server.py
```
Point the Companion app's webhook URL at `http://<this-host>: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=<hmac>` on every request; rejects with 401 on
mismatch (Companion does not retry on 401).
- 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
- 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).
- 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).