Skip to content

Add Health Connect write commands and Changes API real-time sync - #6799

Closed
anasmadrhar wants to merge 13 commits into
home-assistant:mainfrom
anasmadrhar:feature/health-connect-sync
Closed

Add Health Connect write commands and Changes API real-time sync#6799
anasmadrhar wants to merge 13 commits into
home-assistant:mainfrom
anasmadrhar:feature/health-connect-sync

Conversation

@anasmadrhar

@anasmadrhar anasmadrhar commented May 2, 2026

Copy link
Copy Markdown

Summary

builds on #5650. wires up two-way sync between Home Assistant and Health Connect so HA can write back into HC, plus picks up changes from other apps faster than the existing 15-min poll.

what's in it:

  1. command_health_connect_write push command that writes a record into HC, dispatched via the existing MessagingManager. covers all 26 existing data types plus 4 new ones (ExerciseSession + Speed / Power / CyclingPedalingCadence). payload is a flat FCM data map (data_type, value or systolic+diastolic, optional unit, ISO-8601 time / start_time+end_time, optional client_record_id), plus JSON arrays for series records (heart rate samples, sleep stages, speed/power/cadence).
  2. HC to HA real-time read path via the Changes API. opt-in toggle on the new Health Connect settings screen, periodic worker per data type, drains hasMore, handles changesTokenExpired by re-minting, triggers a sensor refresh on upsertions/deletions. catches writes from Samsung Health / Google Fit / smart-scale apps within a few minutes instead of waiting for the next 15-min cycle.
  3. per-sensor "Allow writes from HA" toggle on each HC sensor's detail screen. enabling it adds the matching WRITE_* to the sensor's required permissions and reuses the existing perm-grant flow.
  4. new Settings -> Sensors -> Health Connect screen with the real-time sync toggle and a bulk-enable row that flips on every sensor, persists the write toggles, fires a single combined permission request. live "X / Y sensors enabled" status driven by a SensorDao flow.
  5. new health_connect_exercise_session sensor surfaces the latest HC workout with rich attributes computed over the session window: avg/min/max HR, distance, derived avg pace, recorded speed, power, cadence, calories, steps, segments and laps (segment reps double as swim stroke counts when the source app populates them).
  6. unit conversion delegates to HC's typed unit classes, no hardcoded factors on the app side. accepts kg/g/lb/oz, m/cm/km/mi/ft/in, C/F, mmol/L vs mg/dL, L/mL/fl_oz, kcal/cal/J/kJ, W vs kcal/day, m/s vs km/h vs mph.
  7. ISO-8601 timestamps accept both ...Z and ...+HH:MM offset (HA's now().isoformat() defaults to the latter), blank time field falls back to now.

motivation: #5650 has been sitting unclaimed for a while. the missing piece is reliably pushing values from HA into HC for hardware that doesn't speak HC natively (bluetooth scales without a Play Store app, dumb treadmills with vibration sensors, off-brand pulse oximeters that only expose MQTT, anything ESPHome). also the inverse: faster pickup of writes from other apps so dashboards reflect a watch run before the 15-min cycle catches up.

ran on my own setup before opening this. weight readings from a smart scale push into HC via the new command end-to-end including idempotency by client_record_id. simulate-run script wraps Distance + Steps + ActiveCalories + HeartRate + Speed + Power + CyclingPedalingCadence inside an ExerciseSession and HC shows it as one workout entry with full stats.

a heads-up on Play declaration: the new WRITE_* permissions (and three new READ_* ones for Speed / Power / CyclingPedalingCadence) need a refreshed Health Apps declaration plus a Data Safety update. that's an org-owner step that a contributor PR can't ship on its own. happy to scope down to a smaller initial subset if that's easier to defend, or split into waves: weight + hydration + blood pressure + body temperature first, then activity types, then everything else. let me know which works.

honest gaps: the enriched session sensor's aggregation reads aren't unit-tested (would need ~50 lines of mocks for aggregate(...) against six metric sets, all thin wrappers over the SDK). validated end-to-end on device. HealthConnectChangesWorker itself is glue calling the (tested) repository, no Robolectric test. no screenshot tests for the new settings UI. personal-best sensors (longest run, fastest 5K) are flagged as out-of-scope here, HC has no PB API so it's an iterate-history-and-track problem worth its own change.

a side note on the proxy: while running this on my own deployment of the open-source FCM proxy I hit two pre-existing bugs unrelated to HC that prevented HC payloads (and any object/array data values) from getting through. fixes are up at home-assistant/mobile-apps-fcm-push#294 — independent of this PR but worth landing alongside if HC payloads are going to be exercised in production. the matching whitelist update for the new command_health_connect_write payload keys + ignore-list entry is at home-assistant/mobile-apps-fcm-push#295 (depends on this PR for the wire format; primitives work standalone, the array-typed fields need #294 too).

Checklist

  • New or updated tests have been added to cover the changes following the testing guidelines.
  • The code follows the project's code style and best_practices.
  • The changes have been thoroughly tested, and edge cases have been considered.
  • Changes are backward compatible whenever feasible. Any breaking changes are documented in the changelog for users and/or in the code for developers depending on the relevance.

Screenshots

Screenshot_20260502_173042_Home Assistant Screenshot_20260502_173045_Home Assistant Screenshot_20260502_173049_Home Assistant Screenshot_20260502_173111_Home Assistant Screenshot_20260502_173105_Home Assistant

Link to pull

request in documentation repositories

User Documentation: home-assistant/companion.home-assistant#1321 (draft, will mark ready once this PR is on track to merge)

Developer Documentation: home-assistant/developers.home-assistant#

Any other notes

closes (or unblocks, depending on Play declaration outcome) #5650.

anasmadrhar added 11 commits May 1, 2026 13:40
Add the 25 android.permission.health.WRITE_* uses-permission entries
matching the existing READ_* set. No runtime behavior change yet — these
are required so subsequent commits can request and use write access via
PermissionController.

Part of two-way Health Connect sync (issue home-assistant#5650).
Foundation for two-way Health Connect sync:

* HealthConnectDataType — sealed class enumerating all 25 supported HC
  data types. Each entry binds an FCM payload key, the corresponding
  androidx.health.connect.client Record subclass, and the existing
  HealthConnectSensorManager sensor IDs that surface the data type to
  Home Assistant. Read/write permission strings are derived from the
  record class via HealthPermission.getRead/WritePermission so they do
  not need to be hard-coded. Blood pressure intentionally maps to two
  sensor IDs (systolic + diastolic) since both surface from one record.
* HealthConnectModule — Hilt @provides for HealthConnectClient. Returns
  null when the SDK status is not SDK_AVAILABLE, so injection sites can
  no-op gracefully on devices without Health Connect.

No behavior change yet; subsequent commits build the write path on top.

Part of two-way Health Connect sync (issue home-assistant#5650).
Implements Commit B of the two-way Health Connect sync plan:
introduces a typed write repository with one method per supported
data type, an FCM `command_health_connect_write` payload parser,
and wires the new command into MessagingManager so HA automations
can push records into Health Connect.
Implements the HC -> HA real-time sync path. A periodic, opt-in
WorkManager job polls Health Connect's changes-token API for each
supported data type and triggers SensorReceiver.updateAllSensors
whenever upsertions or deletions are observed, so writes from
third-party apps (Samsung Health, Google Fit, smart-scale apps)
surface in HA without waiting for the 15-min SensorWorker cycle.

Tokens are persisted per data type in a SharedPreferences-backed
LocalStorage and rotated automatically on changesTokenExpired.
Adds a Health Connect entry under the Sensors preference category
that opens a Compose-based settings screen. The screen exposes the
"Real-time sync" toggle which persists the opt-in flag and starts
or cancels HealthConnectChangesWorker accordingly, plus an
informational section describing the new command_health_connect_write
push command for HA -> HC writes.
Each Health Connect sensor now has an "Allow writes from Home
Assistant" toggle on its detail screen. When flipped on, the
matching `WRITE_*` permission is added to that sensor's required
permissions and the existing permission-grant flow surfaces a
prompt so the user can authorize it through the standard Health
Connect permission UI without leaving the app.

Also swaps the Health Connect settings entry icon from the generic
sensor icon to a heart drawable.
- Drop the duplicate `sensorPermissionMap` from HealthConnectSensorManager
  and route every permission lookup through HealthConnectDataType so
  read and write share the single source of truth the plan called for.
- Wrap getChangesToken / getChanges in withContext(Dispatchers.IO) so
  the changes worker doesn't pin the WorkManager dispatcher on
  long-running HC client calls.
- Replace use of SleepSessionRecord.STAGE_TYPE_STRING_TO_INT_MAP
  (RestrictTo to androidx.health.connect) with a local map.
- Wrap the settings screen sub-composables in a Column so each one
  emits a single root, fixing the ComposeMultipleContentEmitters lint.
…tamps

HA's now().isoformat() Jinja helper emits +HH:MM offset by default, so
the strict java.time.Instant.parse rejected every payload built from
the obvious template. Falls back to OffsetDateTime parsing, and treats
a blank time field as "not supplied" so the default-to-now behavior
kicks in there too.
Adds a `unit` field to the FCM payload (e.g. "lb", "ft", "F", "mg/dL")
so HA automations can send values in whatever unit they have on hand
without doing unit math in Jinja.

Conversion delegates to Health Connect's own typed unit classes
(Mass.pounds(...).inKilograms etc.) — no hardcoded constants on the
app side, so we automatically inherit any precision tweaks the SDK
ships. Missing or blank unit means "use the canonical unit", which
preserves backward-compat with existing automations.
Read + write coverage for four new Health Connect data types:

- ExerciseSession: a new sensor (state = exercise type slug, attributes
  carry computed stats over the session window — avg/min/max HR, total
  distance, avg pace, recorded speed, power, cadence, calories, steps,
  segments and laps). Also writeable via command_health_connect_write
  with an `exercise_type` field accepting either an int constant or a
  slug like "running" / "biking" / "swimming_pool".
- Speed, Power, CyclingPedalingCadence: series records, write-only via
  the new generic Series payload variant. Their values surface to HA
  as aggregated attributes on the exercise-session sensor (no
  standalone sensors). Unit conversion accepts m/s, km/h, mph for
  speed and W or kcal/day for power.

Adds 8 new manifest permissions (READ_EXERCISE, READ_SPEED, READ_POWER,
READ_CYCLING_PEDALING_CADENCE plus matching writes).

The exercise-type slug ↔ int map is mirrored locally because the SDK
exposes its own map as @RestrictTo(LIBRARY); same approach we took
earlier for sleep stages.
Adds an "Enable everything" row under the Real-time sync toggle for
users who want the full surface in one tap:

- Enables every Health Connect sensor across every server, persists
  the per-sensor "Allow writes from HA" toggle, and surfaces a single
  Health Connect permission request that asks for read + write on all
  supported data types at once.
- Shows live "X / Y sensors enabled" status driven by a SensorDao
  flow so the row reflects subsequent changes from the per-sensor
  detail screen too.
- Confirmation dialog is themed via HATextStyle + HAPlainButton so
  contrast holds in dark mode.

Drops the now-redundant "Writes from Home Assistant" info card —
the bulk row covers the same UX in a tighter shape.
Copilot AI review requested due to automatic review settings May 2, 2026 16:41

@home-assistant home-assistant Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @anasmadrhar

It seems you haven't yet signed a CLA. Please do so here.

Once you do that we will be able to review and accept this pull request.

Thanks!

@home-assistant
home-assistant Bot marked this pull request as draft May 2, 2026 16:41
@home-assistant

home-assistant Bot commented May 2, 2026

Copy link
Copy Markdown

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@anasmadrhar anasmadrhar changed the title dd Health Connect write commands and Changes API real-time sync Add Health Connect write commands and Changes API real-time sync May 2, 2026
@anasmadrhar
anasmadrhar marked this pull request as ready for review May 2, 2026 16:43
@home-assistant
home-assistant Bot dismissed their stale review May 2, 2026 16:43

Stale

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds a substantial Health Connect expansion to the Android companion app: it introduces HA→Health Connect write commands, a faster Health Connect→HA sync path via the Changes API, and new settings/UI to manage those capabilities. It fits into the existing sensors/notifications/settings architecture by extending MessagingManager, HealthConnectSensorManager, and the sensor settings flows rather than adding a separate integration path.

Changes:

  • Add Health Connect write infrastructure: typed data-type catalog, payload parsing, unit conversion, write repository/result models, DI wiring, and FCM command handling.
  • Add real-time-ish Health Connect sync support using the Changes API, token persistence, a periodic worker, and a dedicated Health Connect settings screen.
  • Extend Health Connect sensors/UI with write-permission toggles, bulk enable, a new exercise session sensor, strings/icons, tests, and changelog/manifest updates.

Reviewed changes

Copilot reviewed 35 out of 35 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
common/src/main/res/values/strings.xml Adds strings for the new Health Connect settings UI and exercise session sensor.
common/src/main/res/drawable/ic_heart_pulse.xml Adds the settings entry icon used for Health Connect.
common/src/main/kotlin/io/homeassistant/companion/android/di/qualifiers/Qualifiers.kt Adds a qualifier for Health Connect-specific local storage.
common/src/main/kotlin/io/homeassistant/companion/android/di/DataModule.kt Provides Health Connect local storage through Hilt.
app/src/test/kotlin/io/homeassistant/companion/android/settings/sensor/healthconnect/HealthConnectSettingsViewModelTest.kt Tests settings VM behavior for realtime sync and bulk enable.
app/src/test/kotlin/io/homeassistant/companion/android/sensors/healthconnect/command/HealthConnectWriteCommandHandlerTest.kt Tests payload parsing/dispatch for Health Connect write commands.
app/src/test/kotlin/io/homeassistant/companion/android/sensors/healthconnect/command/HealthConnectUnitConversionTest.kt Tests unit normalization for write payloads.
app/src/test/kotlin/io/homeassistant/companion/android/sensors/healthconnect/HealthConnectWriteRepositoryImplTest.kt Tests record construction and permission checks in the write repository.
app/src/test/kotlin/io/homeassistant/companion/android/sensors/healthconnect/HealthConnectExerciseTypesTest.kt Verifies the mirrored exercise type slug/int mapping.
app/src/test/kotlin/io/homeassistant/companion/android/sensors/healthconnect/HealthConnectChangesTokenStoreTest.kt Tests token persistence for the Changes API worker.
app/src/test/kotlin/io/homeassistant/companion/android/sensors/healthconnect/HealthConnectChangesRepositoryTest.kt Tests Changes API polling, pagination, and token expiry handling.
app/src/test/kotlin/io/homeassistant/companion/android/sensors/HealthConnectSensorManagerTest.kt Extends sensor manager tests for write-permission toggles.
app/src/main/res/xml/preferences.xml Adds a Health Connect entry to sensor settings.
app/src/main/res/xml/changelog_master.xml Adds a user-facing changelog entry for two-way Health Connect sync.
app/src/main/kotlin/io/homeassistant/companion/android/settings/sensor/healthconnect/HealthConnectSettingsViewModel.kt Implements state and actions for the new Health Connect settings screen.
app/src/main/kotlin/io/homeassistant/companion/android/settings/sensor/healthconnect/HealthConnectSettingsScreen.kt Implements the Compose UI for realtime sync and bulk enable.
app/src/main/kotlin/io/homeassistant/companion/android/settings/sensor/healthconnect/HealthConnectSettingsFragment.kt Hosts the Compose settings screen and permission launcher.
app/src/main/kotlin/io/homeassistant/companion/android/settings/sensor/SensorDetailViewModel.kt Extends sensor-setting changes to re-request permissions when needed.
app/src/main/kotlin/io/homeassistant/companion/android/settings/SettingsFragment.kt Wires navigation to the Health Connect settings screen.
app/src/main/kotlin/io/homeassistant/companion/android/sensors/healthconnect/command/HealthConnectWriteCommandPayload.kt Parses and validates incoming write-command payloads.
app/src/main/kotlin/io/homeassistant/companion/android/sensors/healthconnect/command/HealthConnectWriteCommandHandler.kt Dispatches parsed commands to repository write methods.
app/src/main/kotlin/io/homeassistant/companion/android/sensors/healthconnect/command/HealthConnectUnitConversion.kt Converts payload units into Health Connect canonical units.
app/src/main/kotlin/io/homeassistant/companion/android/sensors/healthconnect/HealthConnectWriteResult.kt Models success/failure outcomes for write attempts.
app/src/main/kotlin/io/homeassistant/companion/android/sensors/healthconnect/HealthConnectWriteRepositoryImpl.kt Builds and inserts Health Connect records for supported write types.
app/src/main/kotlin/io/homeassistant/companion/android/sensors/healthconnect/HealthConnectWriteRepository.kt Defines the write API used by the command handler.
app/src/main/kotlin/io/homeassistant/companion/android/sensors/healthconnect/HealthConnectSyncPreferences.kt Stores realtime sync preferences.
app/src/main/kotlin/io/homeassistant/companion/android/sensors/healthconnect/HealthConnectModule.kt Adds Health Connect DI bindings/providers.
app/src/main/kotlin/io/homeassistant/companion/android/sensors/healthconnect/HealthConnectExerciseTypes.kt Adds the mirrored exercise type mapping used for reads/writes.
app/src/main/kotlin/io/homeassistant/companion/android/sensors/healthconnect/HealthConnectDataType.kt Centralizes supported Health Connect data types, permissions, and sensor IDs.
app/src/main/kotlin/io/homeassistant/companion/android/sensors/healthconnect/HealthConnectChangesWorker.kt Adds periodic worker support for Changes API polling.
app/src/main/kotlin/io/homeassistant/companion/android/sensors/healthconnect/HealthConnectChangesTokenStore.kt Persists per-type Changes API cursors.
app/src/main/kotlin/io/homeassistant/companion/android/sensors/healthconnect/HealthConnectChangesRepository.kt Implements polling and token lifecycle for the Changes API.
app/src/main/kotlin/io/homeassistant/companion/android/sensors/HealthConnectSensorManager.kt Adds exercise session sensing, write-toggle permissions, and new data-type mappings.
app/src/main/kotlin/io/homeassistant/companion/android/notifications/MessagingManager.kt Adds the new command_health_connect_write handling path.
app/src/main/AndroidManifest.xml Declares new Health Connect read/write permissions needed by the feature.

Comment on lines +60 to +77
private val length: Map<String, (Double) -> Double> = mapOf(
"m" to { it },
"meter" to { it },
"meters" to { it },
"metres" to { it },
"km" to { Length.kilometers(it).inMeters },
"kilometer" to { Length.kilometers(it).inMeters },
"kilometers" to { Length.kilometers(it).inMeters },
"mi" to { Length.miles(it).inMeters },
"mile" to { Length.miles(it).inMeters },
"miles" to { Length.miles(it).inMeters },
"ft" to { Length.feet(it).inMeters },
"foot" to { Length.feet(it).inMeters },
"feet" to { Length.feet(it).inMeters },
"in" to { Length.inches(it).inMeters },
"inch" to { Length.inches(it).inMeters },
"inches" to { Length.inches(it).inMeters },
)

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 500f1ff606

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

- Surface specific failure reason in fallback notification instead of
  reposting `command_health_connect_write` as the body. Permission and
  validation errors now produce notifications that tell the automation
  author what went wrong.
- Reject non-integer step counts and resting-heart-rate BPM values
  instead of silently truncating fractional input via `toLong()`. Adds
  regression tests for both.
- Add cm/centimeter(s)/centimetre(s) length units for Height. Adds a
  regression test.
- Drop `@Singleton` from the HealthConnectClient provider. The previous
  binding cached the first SDK-status query for the lifetime of the app
  process, so a user who installed or updated Health Connect mid-session
  kept seeing `null` until cold start. `getOrCreate` is itself idempotent
  so re-querying per `provider.get()` is fine.
@anasmadrhar

Copy link
Copy Markdown
Author

addressed the review feedback in 50c8863:

  • MessagingManager.kt — fallback notification now rewrites title/message with the actual failure reason instead of reposting command_health_connect_write as the body. permission errors point at the settings screen, validation errors carry the exact rejection reason, internal failures carry the exception message.
  • HealthConnectUnitConversion.kt — added cm / centimeter(s) / centimetre(s) for length, plus a regression test in HealthConnectUnitConversionTest.
  • HealthConnectWriteCommandHandler.ktRestingHeartRate and Steps now reject fractional input via a shared requireIntegral helper instead of silently truncating with toLong(). payloads like 1234.9 come back as InvalidPayload with a message naming the field. regression tests added for both.
  • HealthConnectModule.kt — dropped @Singleton from the provider so each provider.get() re-queries SDK status. HealthConnectClient.getOrCreate is itself idempotent so this is cheap, and a user who installs/updates Health Connect mid-session no longer gets stuck seeing null until the next cold start.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 35 out of 35 changed files in this pull request and generated 1 comment.

Comment on lines +184 to +188
val perms = buildSet {
HealthConnectDataType.all.forEach { dataType ->
add(dataType.readPermission)
add(dataType.writePermission)
}
@TimoPtr
TimoPtr marked this pull request as draft May 4, 2026 12:56
@TimoPtr

TimoPtr commented May 5, 2026

Copy link
Copy Markdown
Member

Adding this will cause the battle with Google to get approved. For now we are not ready to have this fight closing.

@TimoPtr TimoPtr closed this May 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants