Add iOS Live Activity push support#278
Conversation
There was a problem hiding this comment.
Pull request overview
Adds first-class iOS Live Activity push support by introducing a dedicated endpoint that sends directly to APNs (HTTP/2 + JWT), along with an iOS payload builder and tests/fixtures to validate live-activity payload generation and handler behavior.
Changes:
- Introduces a new
iOSLiveActivityV1endpoint (Cloud Functions export + local Fastify route) backed by a newhandleLiveActivityRequesthandler. - Adds APNs sender module (
apns.js) implementing ES256 JWT generation and HTTP/2 session reuse. - Extends
ios.jswithcreateLiveActivityPayloadand adds fixture-driven + integration tests for Live Activity pushes.
Reviewed changes
Copilot reviewed 10 out of 10 changed files in this pull request and generated 5 comments.
Show a summary per file
| File | Description |
|---|---|
| functions/webapp.js | Adds local Fastify route for the new live-activity endpoint. |
| functions/index.js | Exposes iOSLiveActivityV1 Cloud Function and exports the new handler for tests. |
| functions/handlers.js | Implements handleLiveActivityRequest with APNs token validation + rate limiting + APNs send/error mapping. |
| functions/ios.js | Adds createLiveActivityPayload (APS liveactivity structure + headers/environment). |
| functions/apns.js | New APNs HTTP/2 client module with JWT auth + session caching. |
| functions/test/ios.test.js | Adds fixtures + integration/unit coverage for live-activity payload creation and handler behavior. |
| functions/test/fixtures/live-activity/*.json | Adds fixture inputs/expectations for start/update/end live-activity payload generation. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
Add support for iOS Live Activities in the mobile_app integration: - Add `supports_live_activities`, `supports_live_activities_frequent_updates`, `live_activity_push_to_start_token`, and `live_activity_push_to_start_apns_environment` fields to SCHEMA_APP_DATA for explicit validation during device registration - Add `update_live_activity_token` webhook handler: stores per-activity APNs push tokens reported by the iOS companion app when a Live Activity is created locally via ActivityKit - Add `live_activity_dismissed` webhook handler: cleans up stored tokens when a Live Activity ends on the device - Both handlers fire bus events so automations can react to activity lifecycle - Add `supports_live_activities()` utility helper - Add 4 tests covering token storage, default environment, dismissal cleanup, and nonexistent tag dismissal for: home-assistant/mobile-apps-fcm-push#278 for: home-assistant/iOS#4444 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Add support for iOS Live Activities in the mobile_app integration: - Add `supports_live_activities`, `supports_live_activities_frequent_updates`, `live_activity_push_to_start_token`, and `live_activity_push_to_start_apns_environment` fields to SCHEMA_APP_DATA for explicit validation during device registration - Add `update_live_activity_token` webhook handler: stores per-activity APNs push tokens reported by the iOS companion app when a Live Activity is created locally via ActivityKit - Add `live_activity_dismissed` webhook handler: cleans up stored tokens when a Live Activity ends on the device - Both handlers fire bus events so automations can react to activity lifecycle - Add `supports_live_activities()` utility helper - Add 4 tests covering token storage, default environment, dismissal cleanup, and nonexistent tag dismissal for: home-assistant/mobile-apps-fcm-push#278 for: home-assistant/iOS#4444 Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
a776337 to
e00cc0f
Compare
|
It looks fine for me, @jpelgrom any concerns or impact in android that you can spot? |
Codecov Report✅ All modified and coverable lines are covered by tests. Additional details and impacted files@@ Coverage Diff @@
## main #278 +/- ##
==========================================
+ Coverage 67.52% 73.23% +5.71%
==========================================
Files 10 10
Lines 625 665 +40
Branches 177 198 +21
==========================================
+ Hits 422 487 +65
+ Misses 158 145 -13
+ Partials 45 33 -12 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
| "firebase-admin": "^13.5.0", | ||
| "firebase-functions": "^6.1.1" |
There was a problem hiding this comment.
I think we should update these separately from the feature, and make sure to test it thoroughly as merging in this repo directly deploys to production.
package-lock.json isn't updated accordingly.
There was a problem hiding this comment.
-
firebase-admin: The v13.5.0 bump can't be separated from this feature — the apns.liveActivityToken field in the FCM messaging API was introduced in v13.5.0 and is what enables routing the Live Activity APNs token through FCM. There's no way to implement the feature without it.
-
firebase-functions: Happy to revert this to v5 if you'd prefer to keep the scope minimal — it's not required by the feature itself.
-
package-lock.json: Good catch — I'll fix that now.
There was a problem hiding this comment.
I understand the bump for firebase-admin cannot be separated from the feature, but think it should be updated separately (before this PR is merged, in another PR) so we can confirm it works correctly without the feature / isolate changes needed for that bump so it can be traced in the future.
There was a problem hiding this comment.
Agree with @jpelgrom, just extract this to a separate PR, we merge that then you rebase this one
## Summary > For architecture decisions, data model details, iOS version strategy, push token flow, and rate limiting — see [technical-brief.pdf](https://github.com/user-attachments/files/26280274/technical-brief.pdf) Adds iOS Live Activities support, letting Home Assistant automations push real-time state to the Lock Screen — washing machine countdowns, EV charging progress, delivery tracking, alarm states, or anything time-sensitive that benefits from glanceable visibility without unlocking the phone. When an automation sends a notification with `live_update: true` in the data payload, the companion app starts a Live Activity instead of (or in addition to) a standard notification banner. Subsequent pushes with the same `tag` update it in-place silently. `clear_notification` + `tag` ends it. Field names (`tag`, `title`, `message`, `progress`, `progress_max`, `chronometer`, `when`, `when_relative`, `notification_icon`, `notification_icon_color`) are intentionally shared with Android's Live Notifications API. Both platforms use the same `live_update: true` trigger — a single YAML block targets iOS 17.2+ and Android 16+ with no platform-specific keys. ```yaml data: title: "Washing Machine" message: "Cycle in progress" data: tag: washer_cycle live_update: true # Android 16+ and iOS 17.2+ progress: 2700 progress_max: 3600 chronometer: true when: 2700 when_relative: true notification_icon: mdi:washing-machine notification_icon_color: "#2196F3" ``` **New files:** - `Sources/Shared/LiveActivity/HALiveActivityAttributes.swift` — the `ActivityAttributes` type. Field names match the Android payload spec. **Struct name and `CodingKeys` are wire-format frozen** — APNs push-to-start payloads reference the Swift type name directly. - `Sources/Shared/LiveActivity/LiveActivityRegistry.swift` — Swift `actor` managing `Activity<HALiveActivityAttributes>` lifecycle. Uses a reservation pattern to prevent duplicate activities when two pushes with the same `tag` arrive simultaneously. - `Sources/Shared/Notifications/NotificationCommands/HandlerLiveActivity.swift` — start/update and end `NotificationCommandHandler` implementations, guarded against the `PushProvider` extension process where ActivityKit is unavailable. - `Sources/Extensions/Widgets/LiveActivity/` — `ActivityConfiguration` wrapper, Lock Screen / StandBy view, and compact / minimal / expanded Dynamic Island views. - `Sources/App/Settings/LiveActivity/LiveActivitySettingsView.swift` — activity status, active list, privacy disclosure, and 11 debug scenarios for pre-server-side testing. **Modified files:** - `Widgets.swift` — registers `HALiveActivityConfiguration` in all three `WidgetBundle` variants behind `#available(iOSApplicationExtension 17.2, *)` - `NotificationsCommandManager.swift` — registers new handlers; `HandlerClearNotification` now also ends a matching Live Activity when `tag` is present - `HAAPI.swift` — adds `supports_live_activities`, `supports_live_activities_frequent_updates`, `live_activity_push_to_start_token`, `live_activity_push_to_start_apns_environment` to registration payload under a single `#available(iOS 17.2, *)` check - `AppDelegate.swift` — reattaches surviving activities at launch and starts observing push-to-start tokens under a single `#available(iOS 17.2, *)` check - `Info.plist` — `NSSupportsLiveActivities` + `NSSupportsLiveActivitiesFrequentUpdates` - `SettingsItem.swift` / `SettingsView.swift` — Live Activities settings row is gated behind `Current.isTestFlight` and shows a `BetaLabel` badge **Tests:** - *Unit Tests* - 45 new tests (36 handler tests, 9 command manager routing tests). All 490 existing tests continue to pass. - *Device Tests* - I tried to test with my physical device, however I do not have a paid account. Turns out I could not deploy without disabling a lot of entitlements for compilation. Even so, once I did get it deployed and running Live Activities wouldn't show unless some of those settings that I disable were re-enable and leaving me in a particular spot. - I mainly tested with the simulator which is what is shown below ## Screenshots  ## Link to pull request in Documentation repository Documentation: home-assistant/companion.home-assistant#1303 ## Link to pull request in push relay repository Relay server: home-assistant/mobile-apps-fcm-push#278 ## Link to pull request in HA core Core: home-assistant/core#166072 ## Any other notes **iOS version gating at 17.2.** The entire feature is gated at `#available(iOS 17.2, *)` — this is the minimum for push-to-start (the primary server-side start mechanism). A single availability check now covers all Live Activity APIs: `supports_live_activities`, `frequentPushesEnabled`, push-to-start token, and all ActivityKit usage. This eliminates the nested 16.2/17.2 check pattern. **Push-to-start (iOS 17.2+) is client-complete.** The token is observed, stored in Keychain, and included in registration payloads. All companion server-side PRs are now open — relay server at home-assistant/mobile-apps-fcm-push#278 and HA core webhook handlers at home-assistant/core#166072. The relay server uses FCM's native `apns.liveActivityToken` support (Firebase Admin SDK v13.5.0+) — no custom APNs client or credentials needed. > **Server-side work** — all PRs now open: > - ~~`supports_live_activities` field handling in device registration~~ ✓ home-assistant/core#166072 > - ~~`mobile_app_live_activity_token` webhook handler~~ ✓ home-assistant/core#166072 > - ~~`mobile_app_live_activity_dismissed` webhook handler~~ ✓ home-assistant/core#166072 > - ~~Relay server: Live Activity delivery via FCM `apns.liveActivityToken`~~ ✓ home-assistant/mobile-apps-fcm-push#278 > - ~~`notify.py` routing: includes Live Activity APNs token alongside FCM token~~ ✓ home-assistant/core#166072 **Live Activities entry in Settings is gated behind TestFlight.** The settings row only appears when `Current.isTestFlight` is true, preventing it from surfacing in a release build before the feature is fully tested. A `BetaLabel` badge is shown alongside the row title. **iPad:** `areActivitiesEnabled` is always `false` on iPad — Apple system restriction. The Settings screen shows "Not available on iPad." The registry silently no-ops. HA receives `supports_live_activities: false` in the device registration for iPad. **`HALiveActivityAttributes` is frozen post-ship.** The struct name appears as `attributes-type` in APNs push-to-start payloads. Renaming it silently breaks all remote starts. The `ContentState` `CodingKeys` are equally frozen — only additions are safe. Both have comments in the source calling this out. **The debug section in Settings is intentional.** Gated behind `#if DEBUG` so it only appears in debug builds — it never ships to TestFlight or the App Store. It exercises the full ActivityKit lifecycle without requiring the server-side chain. **`UNUserNotificationCenter` in tests.** The `clear_notification` + `tag` → Live Activity dismissal path is covered by code review rather than a unit test. `HandlerClearNotification` calls `UNUserNotificationCenter.current().removeDeliveredNotifications` synchronously, which requires a real app bundle and throws `NSInternalInconsistencyException` in the XCTest host. A comment in the test file explains this. **Rate limiting on iOS 18.** Apple throttles Live Activity updates to ~15 seconds between renders. Automations should trigger on state change events, not polling timers. **Related:** - Community discussion: https://github.com/orgs/home-assistant/discussions/84 - Android companion reference: https://github.com/home-assistant/android - Roadmap: OpenHomeFoundation/roadmap#52 --------- Co-authored-by: Claude Sonnet 4.6 <noreply@anthropic.com> Co-authored-by: Bruno Pantaleão Gonçalves <5808343+bgoncal@users.noreply.github.com>
|
Perhaps now it's about testing it locally to make sure it works, I'll try to reserve some time between this and next week. |
|
Deployed to Firebase after a bit of trouble, but that's related to my setup and experience here, after which the functions still seem to work correctly with the new versions. If you put the version bump in another PR we can do it cleanly and then no further objections to the other changes :) |
Live Activity push tokens from ActivityKit are direct APNs tokens (hex-encoded), not FCM tokens. The existing iOSV1 endpoint cannot handle them — this adds a dedicated iOSLiveActivityV1 endpoint that sends directly to the APNs HTTP/2 API using JWT authentication, bypassing FCM entirely. New files: - apns.js: APNs HTTP/2 client with ES256 JWT auth (Node built-ins only, no new dependencies). JWT cached and rotated every 45 min. HTTP/2 sessions cached per environment (sandbox/production). - live-activity.js: payload builder for start/update/end events. Maps content_state fields to the HALiveActivityAttributes struct expected by the iOS companion app. Sets apns-push-type: liveactivity and routes to correct APNs topic using bundle ID from registration_info. Updated files: - handlers.js: adds handleLiveActivityRequest(), which validates hex APNs tokens, applies rate limiting, sends via apns.js, and handles BadDeviceToken (400) without error reporting. - index.js: exports iOSLiveActivityV1 Cloud Function. - webapp.js: adds /api/sendPush/iOS/liveActivity/v1 route for local dev. Tests: 20 tests across fixture-driven createPayload tests and handleLiveActivityRequest integration tests covering success, token validation, rate limiting, APNs error mapping, and end-event rate-limit bypass. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Moves createLiveActivityPayload() from the standalone live-activity.js into ios.js as a second export, keeping all iOS-specific logic in one place consistent with the existing android.js/ios.js pattern. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Follows the existing test naming convention (legacy.test.js -> legacy.js) now that the Live Activity payload builder lives in ios.js. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix misleading comment in webapp.js (was "Import the functions from index.js") - Validate APNs token length (must be exactly 64 hex chars) in isValidApnsToken; add test - Normalize apnsEnvironment to 'sandbox'|'production' to prevent unbounded session cache growth - Guard JSON.parse in apns.js against non-JSON APNs responses - Fix test: pass body overrides flat to createLiveActivityRequest (not nested under body:) - Add explanatory comments throughout apns.js, ios.js, and handlers.js for non-obvious decisions Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Firebase Admin SDK v13.5.0+ supports the liveActivityToken field in the apns config object, which tells FCM to automatically set apns-push-type: liveactivity and route the notification to APNs. This eliminates the need for a custom direct APNs HTTP/2 client. Changes: - Delete apns.js (custom HTTP/2 + JWT APNs client) — no longer needed - Bump firebase-admin from ^12.1.0 to ^13.5.0 - Rewrite ios.js: createPayload detects live_activity_token in request body and builds FCM payload with apns.liveActivityToken (camelCase) - Remove handleLiveActivityRequest from handlers.js — existing handleRequest + messaging.send() handles everything - Remove iOSLiveActivityV1 endpoint from index.js and webapp.js - Rewrite tests for FCM-based delivery path - Update fixtures to use FCM payload structure The relay server no longer needs APNS_TEAM_ID, APNS_KEY_ID, or APNS_PRIVATE_KEY environment variables. HA core sends both the FCM registration token (push_token) and the Live Activity APNs token (live_activity_token) to the same /api/sendPush/iOS/v1 endpoint. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
All live activity pushes (start, update, end) now count against the rate limit. End events still have an APNs delivery cost, so exempting them was not justified. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
HALiveActivityAttributes must only appear in push-to-start payloads — APNs rejects update/end payloads that include attributes-type. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
d9af41e to
67feffd
Compare
|
Rebased against the merged 293 PR and updated here |
|
Should we wait for the PR in core to be merged? In case comments there require changes in here |
Yeah might be best, good idea |
Summary
Adds iOS Live Activity push support using FCM's native
liveActivityTokenfield (Firebase Admin SDK v13.5.0+).When HA core sends a notification with
live_activity_tokenin the request body, the relay server'sios.jspayload builder detects it and constructs an FCM message withapns.liveActivityToken. FCM then automatically setsapns-push-type: liveactivityand routes the notification to Apple's push servers — no custom APNs client, JWT signing, or HTTP/2 session management needed.Changes:
ios.js:createPayloaddetectslive_activity_tokenin the request body and delegates tobuildLiveActivityPayload(), which sets the FCMapns.liveActivityTokenfield with the correctapsstructure for start/update/end eventspackage.json: Bumpsfirebase-adminfrom^12.1.0to^13.5.0(minimum version withliveActivityTokensupport) andfirebase-functionsfrom^5.0.1to^6.1.1(required peer dependency for firebase-admin v13)index.js: Updated import fromfirebase-functionstofirebase-functions/v1— firebase-functions v6 changes the default export to v2 APIs, but the v1 import retainsfunctions.config(),functions.region(), andfunctions.runWith()used by existing Cloud Function definitionshandleRequest, and backward compatibility with normal notificationsNo new files, no new endpoints — Live Activity notifications flow through the existing
/api/sendPush/iOS/v1endpoint alongside normal iOS notifications. TheAPNS_TEAM_ID,APNS_KEY_ID, andAPNS_PRIVATE_KEYenvironment variables are not needed.Test plan
messaging.send()receives correctliveActivityTokenfor: home-assistant/iOS#4444
for: home-assistant/core#166072