diff --git a/CHANGELOG.md b/CHANGELOG.md index 2c915d4aa..d7ce45e42 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -30,6 +30,23 @@ Each injected dose now carries a chip with the site next to its status, in the table and on the mobile cards, so the rotation can be read back from the history. Doses without a site look as before. +- **Garmin, directly.** Garmin Connect now offers a Personal API, and + HealthLog reads it: create a key in Garmin Connect (Settings, Personal + API), paste it into the new Garmin card, and steps, distance, floors, + active calories, resting heart rate, sleep stages and sleep score, + overnight SpO₂, breathing rate and HRV, weigh-ins with body composition, + activities and logged period days sync every hour, with the whole + history filled in behind. Body Battery and Garmin's all-day stress level + arrive as types of their own (`BODY_BATTERY`, `STRESS_LEVEL`), never mixed + into the computed scores. Garmin keys expire after 30 to 365 days; the + card warns a week ahead and asks for a new key when Garmin stops + accepting the old one. Accounts on Garmin's China service work too. The + key is stored encrypted and only ever sent to Garmin. (#1333) +- **Training Readiness and acute training load.** The Personal API does + not carry them, so they are new types (`TRAINING_READINESS`, + `ACUTE_TRAINING_LOAD`) a Home Assistant bridge can push through an ingest + token, and they chart on the recovery page next to Body Battery. The Home + Assistant guide has the automation. (#1333) ### Changed @@ -96,6 +113,8 @@ - @Oguz3003 for staying with the Raspberry Pi 4 until it reads PDFs (#1311). - @mills1975 for asking how blood pressure differs over the day (#1331). +- @kruemelro for asking for a direct Garmin connection on Garmin's new + Personal API (#1333). ## [1.46.6] — 2026-11-02 diff --git a/docs/README.md b/docs/README.md index 225e5a1f8..b500f6784 100644 --- a/docs/README.md +++ b/docs/README.md @@ -50,7 +50,7 @@ notes, and machine-readable specs that ship alongside the source: . - [`integrations/`](./integrations/) — per-integration reference: Withings, WHOOP, Fitbit, Google Health, Strava, Nightscout, Dawarich and - the Garmin route; the Apple Health, Health Connect, document and data + Garmin; the Apple Health, Health Connect, document and data imports; AI providers; and Home Assistant with its blueprint. Oura and Polar are covered on the docs site only. - [`adr/`](./adr/) — architecture decision records. diff --git a/docs/integrations/apple-health.md b/docs/integrations/apple-health.md index 33f72b545..7a74f821e 100644 --- a/docs/integrations/apple-health.md +++ b/docs/integrations/apple-health.md @@ -314,7 +314,7 @@ id for debugging. ## Garmin owners -Garmin has no direct connector for a self-hosted instance. Garmin Connect writes -to Apple Health automatically, so the import path on this page is how Garmin data -reaches HealthLog on iOS. See [garmin.md](./garmin.md) for what comes through and -what Garmin withholds. +Garmin Connect writes to Apple Health automatically, so the import path on this +page brings Garmin data in on iOS. The direct [Garmin](./garmin.md) integration +reads more (Body Battery, stress, Garmin's sleep score) with a key from your +own Garmin account. diff --git a/docs/integrations/garmin.md b/docs/integrations/garmin.md index b1aa7117e..5e92aa7c0 100644 --- a/docs/integrations/garmin.md +++ b/docs/integrations/garmin.md @@ -1,52 +1,104 @@ -# Garmin (via Apple Health or Google Health Connect) +# Garmin -**There is no direct Garmin connector for a self-hosted HealthLog instance, and -there cannot be one.** Garmin's data-sharing program is business-partner-only: -it is approval-gated, requires a public callback endpoint, and is not the -"register your own app, paste a key" model the wearable integrations here rely -on. So instead of a broken half-integration, HealthLog reads Garmin data the -honest way — through the phone health platform your Garmin already feeds. +HealthLog reads Garmin Connect directly through Garmin's **Personal API**: you +create a key for your own account in Garmin Connect, paste it into HealthLog, +and the server pulls your data every hour. Nothing is pushed by Garmin, and +nothing is written back. -## The path that works +The Personal API is for your own data only. Each person on an instance +creates and pastes their own key; there is no shared app and no operator +setup. -``` -Garmin device → Garmin Connect → Apple Health (iOS) ─┐ - → Google Health Connect ├─→ HealthLog - (Android) ─┘ -``` +## Create a key -- **iOS:** Garmin Connect writes to Apple Health automatically. Import it with - the existing [Apple Health](./apple-health.md) path (the `export.zip` upload or - the native client's background sync). -- **Android:** since Garmin Connect 5.14.1 (July 2025) the app writes to **Google - Health Connect**. Connect HealthLog through the existing - [Google Health](./google-health.md) integration. +1. Open [Garmin Connect → Settings → Personal API](https://connect.garmin.com/app/settings/personalApiToken). +2. Create a key and choose how long it stays valid: 30, 90, 180 days or one + year. +3. Copy it right away. Garmin shows the key once. -No extra code, no Garmin developer account, no approval queue — if your Garmin -already syncs to your phone's health platform, HealthLog already reads it. +## Connect + +Open **Settings → Integrations → Garmin**, paste the key, and pick the same +validity you chose in Garmin Connect. HealthLog checks the key with Garmin +before storing anything, so a mistyped or revoked key is refused on the spot. +The key is stored encrypted at rest and is never shown again, logged or sent +anywhere but Garmin. + +Accounts registered with Garmin's China service work too: a key the global +service does not know is tried there once, and the service that accepts it is +remembered. + +After connecting, the last 30 days arrive within a minute and the rest of your +history follows in the background. The card says "Older history is still +loading" until it is done. + +## Key expiry + +Garmin keys expire and cannot be extended. Garmin does not report a key's end +date through the API, so HealthLog works it out from the validity you picked +at connect, and replaces that estimate with Garmin's own date once the key is +in its last day (the short-lived tokens it buys are cut off at the key's end). + +From seven days before the end the card says when the key expires. When it +has expired, or you revoked it in Garmin Connect, syncing stops and the card +asks for a new key: create one, open **Replace key** in the card's menu and +paste it. Nothing imported is lost. If you did not pick a validity, the card +says that it cannot warn ahead. ## What comes through -Everything Garmin shares with Apple Health / Health Connect flows in: steps, -heart rate (including beat-to-beat variability), sleep stages (REM / deep / -light), calories (active and total), distance, weight and body composition, -cadence, and — partially — blood-oxygen saturation. +| Garmin data | HealthLog type | Unit | +| --------------------------------------------------------------------- | -------------------------- | ------------- | +| Steps | `ACTIVITY_STEPS` | steps | +| Step distance | `WALKING_RUNNING_DISTANCE` | m | +| Floors climbed | `FLIGHTS_CLIMBED` | flights | +| Active calories | `ACTIVE_ENERGY_BURNED` | kcal | +| Resting heart rate | `RESTING_HEART_RATE` | bpm | +| All-day average stress level | `STRESS_LEVEL` | 0–100 | +| Body Battery (the day's high) | `BODY_BATTERY` | 0–100 | +| Sleep stages (light, deep, REM, awake) | `SLEEP_DURATION` per stage | minutes | +| Sleep score | `SLEEP_SCORE` | 0–100 | +| Average overnight SpO₂ | `OXYGEN_SATURATION` | % | +| Average overnight breathing rate | `RESPIRATORY_RATE` | breaths/min | +| Overnight HRV (RMSSD) | `HRV_RMSSD` | ms | +| Weight, BMI, body fat, body water, muscle and bone mass, visceral fat | the body-composition types | kg, %, rating | +| Activities | workouts | | +| Logged period days | the cycle tracker | | -## What Garmin withholds +Notes: -These are Garmin's **proprietary** scores. Garmin does **not** share them with -Apple Health or Health Connect, so no self-hosted integration — and no amount of -configuration — can obtain them. They are only available inside Garmin Connect -itself: +- Garmin's HRV is RMSSD and lands on `HRV_RMSSD`. Garmin reports no SDNN, so + nothing is written to `HEART_RATE_VARIABILITY`. +- Stress and Body Battery are Garmin's own scores and keep types of their own. + They are not mixed into HealthLog's computed stress score. +- Days you did not wear the watch import nothing, not zeros. +- Period days are only added on days you have not logged yourself; your own + entries are never changed. Cycles Garmin only predicts are not imported. +- When the same data also arrives through Apple Health or Health Connect, the + source order in **Settings → Sources** decides which one a chart shows. + Garmin ranks with the other watches. -- Body Battery -- HRV Status -- VO₂ max -- Training Load and Training Effect -- Recovery Time -- Intensity Minutes -- Stress score -- Running dynamics +## What the Personal API does not carry -If you rely on those specific numbers, keep the Garmin Connect app for them; -everything else lands in HealthLog through the platform path above. +Training Readiness, Training Status, acute training load and recovery time +are not part of the Personal API. If you read them elsewhere, for example +through a Home Assistant integration, push them through an ingest token: see +[Home Assistant](./home-assistant.md#garmin-training-figures). They land as +`TRAINING_READINESS` and `ACUTE_TRAINING_LOAD` and show on the recovery page +beside Body Battery. + +## Limits + +- Garmin sets how many requests a key may make. HealthLog reads one resource + at a time and stops the hour's run at Garmin's first rate-limit answer; the + history backfill waits as long as Garmin asks before it goes on. +- Disconnecting removes the key from HealthLog. It stays valid at Garmin until + you revoke it there. + +## Without a key + +Garmin Connect also writes to Apple Health on iOS and to Google Health Connect +on Android, so the [Apple Health](./apple-health.md) and +[Google Health](./google-health.md) paths keep working for anyone who prefers +not to create a key. Body Battery, stress and Garmin's sleep score only come +through the Personal API. diff --git a/docs/integrations/google-health.md b/docs/integrations/google-health.md index 3176a827d..226a973cf 100644 --- a/docs/integrations/google-health.md +++ b/docs/integrations/google-health.md @@ -315,7 +315,7 @@ Previously synced measurements stay in the database. ## Garmin owners -Garmin has no direct connector for a self-hosted instance. Since Garmin Connect -5.14.1 (July 2025) the app writes to Google Health Connect, so this integration -is how Garmin data reaches HealthLog on Android. See [garmin.md](./garmin.md) for -what comes through and what Garmin withholds. +Since Garmin Connect 5.14.1 (July 2025) the app writes to Google Health Connect, +so this path brings Garmin data in on Android. The direct [Garmin](./garmin.md) +integration reads more (Body Battery, stress, Garmin's sleep score) with a key +from your own Garmin account. diff --git a/docs/integrations/home-assistant.md b/docs/integrations/home-assistant.md index d858e7e80..7af9fb050 100644 --- a/docs/integrations/home-assistant.md +++ b/docs/integrations/home-assistant.md @@ -149,6 +149,38 @@ Units are derived server-side from the type, so send the number in the unit above and do not send a `unit` field. The full enum is in `docs/api/openapi.yaml` under `MeasurementType`. +## Garmin training figures + +The Garmin Personal API does not carry Training Readiness or the acute +training load (see [Garmin](./garmin.md)). If Home Assistant reads them, for +example through a Garmin Connect integration, send them through the same +ingest token. They have types of their own and appear on the recovery page: + +| What the sensor reports | `type` | Unit expected | +| -------------------------- | --------------------- | ------------- | +| Garmin Training Readiness | `TRAINING_READINESS` | score 0–100 | +| Garmin acute training load | `ACUTE_TRAINING_LOAD` | load, no unit | + +Both change about once a day. This automation pushes Training Readiness +whenever the sensor changes, through the `healthlog_measurement` command +above; the acute load is the same with its own sensor and type: + +```yaml +automation: + - alias: "HealthLog: Garmin training readiness" + triggers: + - trigger: state + entity_id: sensor.garmin_training_readiness + conditions: + - condition: template + value_template: "{{ trigger.to_state.state | is_number }}" + actions: + - action: rest_command.healthlog_measurement + data: + type: TRAINING_READINESS + value: "{{ trigger.to_state.state | float }}" +``` + ## Room climate Indoor CO₂, temperature and humidity are their own types, kept apart from the