Add Health Connect write commands and Changes API real-time sync - #6799
Add Health Connect write commands and Changes API real-time sync#6799anasmadrhar wants to merge 13 commits into
Conversation
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.
There was a problem hiding this comment.
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!
|
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
There was a problem hiding this comment.
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. |
| 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 }, | ||
| ) |
There was a problem hiding this comment.
💡 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.
|
addressed the review feedback in 50c8863:
|
| val perms = buildSet { | ||
| HealthConnectDataType.all.forEach { dataType -> | ||
| add(dataType.readPermission) | ||
| add(dataType.writePermission) | ||
| } |
|
Adding this will cause the battle with Google to get approved. For now we are not ready to have this fight closing. |
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:
command_health_connect_writepush 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).health_connect_exercise_sessionsensor 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).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_writepayload 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
Screenshots
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.