Skip to content

Add iOS Live Activity push support#278

Open
rwarner wants to merge 8 commits intohome-assistant:mainfrom
rwarner:feat/ios-live-activity
Open

Add iOS Live Activity push support#278
rwarner wants to merge 8 commits intohome-assistant:mainfrom
rwarner:feat/ios-live-activity

Conversation

@rwarner
Copy link
Copy Markdown
Contributor

@rwarner rwarner commented Mar 20, 2026

Summary

Adds iOS Live Activity push support using FCM's native liveActivityToken field (Firebase Admin SDK v13.5.0+).

When HA core sends a notification with live_activity_token in the request body, the relay server's ios.js payload builder detects it and constructs an FCM message with apns.liveActivityToken. FCM then automatically sets apns-push-type: liveactivity and routes the notification to Apple's push servers — no custom APNs client, JWT signing, or HTTP/2 session management needed.

Changes:

  • ios.js: createPayload detects live_activity_token in the request body and delegates to buildLiveActivityPayload(), which sets the FCM apns.liveActivityToken field with the correct aps structure for start/update/end events
  • package.json: Bumps firebase-admin from ^12.1.0 to ^13.5.0 (minimum version with liveActivityToken support) and firebase-functions from ^5.0.1 to ^6.1.1 (required peer dependency for firebase-admin v13)
  • index.js: Updated import from firebase-functions to firebase-functions/v1 — firebase-functions v6 changes the default export to v2 APIs, but the v1 import retains functions.config(), functions.region(), and functions.runWith() used by existing Cloud Function definitions
  • 20 tests covering fixture-driven payload validation, FCM integration via handleRequest, and backward compatibility with normal notifications

No new files, no new endpoints — Live Activity notifications flow through the existing /api/sendPush/iOS/v1 endpoint alongside normal iOS notifications. The APNS_TEAM_ID, APNS_KEY_ID, and APNS_PRIVATE_KEY environment variables are not needed.

Test plan

  • All 20 Live Activity tests pass
  • All 129 non-Live-Activity tests pass (2 pre-existing timezone failures in valkey-rate-limiter unrelated to this PR)
  • Fixture-driven tests validate payload structure for start, update, end, and full-field scenarios
  • Integration tests verify messaging.send() receives correct liveActivityToken

for: home-assistant/iOS#4444
for: home-assistant/core#166072

Copilot AI review requested due to automatic review settings March 20, 2026 14:44
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

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 iOSLiveActivityV1 endpoint (Cloud Functions export + local Fastify route) backed by a new handleLiveActivityRequest handler.
  • Adds APNs sender module (apns.js) implementing ES256 JWT generation and HTTP/2 session reuse.
  • Extends ios.js with createLiveActivityPayload and 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.

Comment thread functions/test/ios.test.js Outdated
Comment thread functions/webapp.js Outdated
Comment thread functions/handlers.js Outdated
Comment thread functions/ios.js Outdated
Comment thread functions/apns.js Outdated
rwarner added a commit to rwarner/core that referenced this pull request Mar 20, 2026
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>
rwarner added a commit to rwarner/core that referenced this pull request Mar 24, 2026
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>
@rwarner rwarner force-pushed the feat/ios-live-activity branch from a776337 to e00cc0f Compare March 26, 2026 13:12
Comment thread functions/ios.js
Comment thread functions/ios.js Outdated
Comment thread functions/ios.js
@rwarner rwarner requested a review from bgoncal April 1, 2026 18:25
@bgoncal bgoncal requested a review from jpelgrom April 2, 2026 12:18
@bgoncal
Copy link
Copy Markdown
Member

bgoncal commented Apr 2, 2026

It looks fine for me, @jpelgrom any concerns or impact in android that you can spot?

@codecov
Copy link
Copy Markdown

codecov Bot commented Apr 2, 2026

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 73.23%. Comparing base (6c3d2b5) to head (67feffd).

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.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

Comment thread functions/package.json Outdated
Comment on lines +20 to +21
"firebase-admin": "^13.5.0",
"firebase-functions": "^6.1.1"
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

  • 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.

Copy link
Copy Markdown
Member

@jpelgrom jpelgrom Apr 21, 2026

Choose a reason for hiding this comment

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

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.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Agree with @jpelgrom, just extract this to a separate PR, we merge that then you rebase this one

bgoncal added a commit to home-assistant/iOS that referenced this pull request Apr 7, 2026
## 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


![full-debug-scenarios](https://github.com/user-attachments/assets/b6f61b15-8f41-4fb5-b89d-75b5de719056)

## 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>
@rwarner rwarner requested a review from jpelgrom April 7, 2026 14:17
@rwarner
Copy link
Copy Markdown
Contributor Author

rwarner commented Apr 20, 2026

Looking to follow up on everything here: @jpelgrom @bgoncal

@bgoncal
Copy link
Copy Markdown
Member

bgoncal commented Apr 21, 2026

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.
@jpelgrom if you can give it a try as well would be nice (just validating it doesn't break what exists now)

bgoncal
bgoncal previously approved these changes Apr 22, 2026
Copy link
Copy Markdown
Member

@bgoncal bgoncal left a comment

Choose a reason for hiding this comment

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

Tested locally and works as expected ✅

@jpelgrom
Copy link
Copy Markdown
Member

jpelgrom commented Apr 22, 2026

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 :)

@rwarner
Copy link
Copy Markdown
Contributor Author

rwarner commented Apr 23, 2026

Separated the version bumps into #293 as requested. Once that merges I'll rebase #278 on top of it so the Live Activities changes sit on the already-upgraded base.

rwarner and others added 4 commits April 28, 2026 12:44
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>
rwarner and others added 4 commits April 28, 2026 12:45
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>
@rwarner
Copy link
Copy Markdown
Contributor Author

rwarner commented Apr 28, 2026

Rebased against the merged 293 PR and updated here

@rwarner rwarner requested a review from bgoncal April 28, 2026 16:51
@bgoncal
Copy link
Copy Markdown
Member

bgoncal commented Apr 28, 2026

Should we wait for the PR in core to be merged? In case comments there require changes in here
home-assistant/core#166072

@rwarner
Copy link
Copy Markdown
Contributor Author

rwarner commented Apr 28, 2026

Should we wait for the PR in core to be merged? In case comments there require changes in here home-assistant/core#166072

Yeah might be best, good idea

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.

4 participants