Adds iOS Live Activities support#4444
Conversation
There was a problem hiding this comment.
Pull request overview
Adds ActivityKit-based Live Activities support to the Home Assistant iOS app, enabling notifications to start/update/end a Lock Screen/Dynamic Island Live Activity via homeassistant.command or homeassistant.live_activity payload fields, plus UI/settings and device registration support.
Changes:
- Adds a Live Activity attributes model and an actor-based registry to manage activity lifecycle, push tokens, and dismissal reporting.
- Extends notification command routing/handlers to start/update/end Live Activities (including
clear_notification+tagdismissal). - Adds widget extension
ActivityConfigurationUI, settings UI, localization strings, and new unit tests for routing/handlers.
Reviewed changes
Copilot reviewed 21 out of 21 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| Tests/Shared/LiveActivity/NotificationsCommandManagerLiveActivityTests.swift | Adds routing tests for live_activity, end_live_activity, and flag-based routing. |
| Tests/Shared/LiveActivity/MockLiveActivityRegistry.swift | Provides a registry test double for handler/manager tests. |
| Tests/Shared/LiveActivity/HandlerLiveActivityTests.swift | Adds validation/parsing/guard/dismissal-policy tests for the new handlers. |
| Sources/Shared/Settings/SettingsStore.swift | Persists a “privacy disclosure seen” flag for Live Activities. |
| Sources/Shared/Resources/Swiftgen/Strings.swift | Adds generated localization accessors for Live Activities settings strings. |
| Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift | Registers Live Activity commands and adds live_activity: true routing + clear_notification Live Activity end behavior. |
| Sources/Shared/Notifications/NotificationCommands/HandlerLiveActivity.swift | Implements start/update and end handlers, including payload parsing and validation. |
| Sources/Shared/LiveActivity/LiveActivityRegistry.swift | Adds an actor to manage activities, observe token/lifecycle streams, and report webhooks. |
| Sources/Shared/LiveActivity/HALiveActivityAttributes.swift | Defines ActivityAttributes / ContentState wire model for Live Activities. |
| Sources/Shared/Environment/Environment.swift | Adds apnsEnvironment helper and a lazily created liveActivityRegistry environment dependency. |
| Sources/Shared/API/HAAPI.swift | Extends registration payload with Live Activities capability and token fields. |
| Sources/Extensions/Widgets/Widgets.swift | Registers the Live Activity widget configuration in widget bundles with iOS 16.2 gating. |
| Sources/Extensions/Widgets/LiveActivity/HALockScreenView.swift | Implements the Lock Screen/StandBy UI for the activity. |
| Sources/Extensions/Widgets/LiveActivity/HALiveActivityConfiguration.swift | Adds ActivityConfiguration wrapper for the Live Activity widget. |
| Sources/Extensions/Widgets/LiveActivity/HADynamicIslandView.swift | Implements Dynamic Island compact/minimal/expanded views. |
| Sources/App/Settings/Settings/SettingsItem.swift | Adds Live Activities to Settings navigation and gates it behind iOS 16.2. |
| Sources/App/Settings/LiveActivity/LiveActivitySettingsView.swift | Adds a settings screen for status, active activities list, privacy text, and debug scenarios. |
| Sources/App/Resources/en.lproj/Localizable.strings | Adds English strings for Live Activities settings UI. |
| Sources/App/Resources/Info.plist | Enables Live Activities + Frequent Updates support via Info.plist keys. |
| Sources/App/AppDelegate.swift | Reattaches surviving activities at launch and starts observing push-to-start tokens (iOS 17.2+). |
| HomeAssistant.xcodeproj/project.pbxproj | Adds new source files/groups and adjusts build settings (including a Widgets debug signing team). |
Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift
Show resolved
Hide resolved
Sources/App/Settings/LiveActivity/LiveActivitySettingsView.swift
Outdated
Show resolved
Hide resolved
Sources/Shared/Notifications/NotificationCommands/NotificationsCommandManager.swift
Show resolved
Hide resolved
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>
|
tip for lint you can use |
|
Can you make a screen recording showing the whole flow? Showing HA UI sending the push and iPhone handling it |
So I don't have a paid Apple dev account so apparently the only way I could get it to compile to an actual device (to try to get push notifs) was to rip a bunch of entitlements out to compile for my physical device. For the emulator, I wasn't sure how to do a full-end-to-end test with the four repo changes altogether and run that locally. Theoretically it seemed like a possibility to do a local push to the emulated device but didn't get to that point. Mainly why I created the debug options in the screenshot section showcasing all of the possibilities with the YAML at the moment. (This is in App Companion Settings -> Live Activity -> DEBUG options) I don't mind putting some more stuff together, lemme know any specifics you would like to see and I can try. There's also more screenshots in the documentation repo here: home-assistant/companion.home-assistant#1303 |
|
Nowadays you can already test push notification on simulator as well from what I can remember, check this https://www.tiagohenriques.dev/blog/testing-push-notifications-ios-simulators |
|
Thanks, I'll look at this and see if I can produce some more examples for you / video on top of the previous gif |
For the local WebSocket path, the simulator is currently connecting through Nabu Casa (cloud relay), so notifications route through FCM rather than the local WebSocket channel. I'd need to be on the same local network as the HA instance for WebSocket-based delivery to work — In the meantime, I've added two screen recordings demonstrating start/update/end with various payload configurations on the simulator. The debug section in Settings also exercises all the UI states. As well as correcting the linting issues Settings.Area.mp4Debug.sample.mp4 |
You have that already right? Because you opened a PR for core as well, so you can use that instance you used to test core to connect to the iOS App |
Sources/Extensions/Widgets/LiveActivity/HADynamicIslandView.swift
Outdated
Show resolved
Hide resolved
Sources/Extensions/Widgets/LiveActivity/HADynamicIslandView.swift
Outdated
Show resolved
Hide resolved
Sources/Extensions/Widgets/LiveActivity/HALiveActivityConfiguration.swift
Outdated
Show resolved
Hide resolved
|
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## main #4444 +/- ##
=======================================
Coverage ? 42.49%
=======================================
Files ? 270
Lines ? 16012
Branches ? 0
=======================================
Hits ? 6804
Misses ? 9208
Partials ? 0 ☔ View full report in Codecov by Sentry. 🚀 New features to boost your workflow:
|
|
In your example we have 2 different tags, one for android and one for iOS: Should we use the same as android has so the user does not need 2 different approaches based on platform? Are there more differences that we could merge into a single approach? |
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>
8af7679 to
c4ad13d
Compare
Correct yeah, I haven't stood up core before locally from the repo will take a stab and see if I can ping HASS Core directly from the localhost from the iPhone sim. Was just using my personal instance to try at first.
Great point actually. I can change this to just use the pre-existing Unless you think it would be handy to have the following example:
But overall, the field names inside data (tag, progress, chronometer, etc.) are already unified across both platforms — it's only the opt-in trigger that's separate. Responded to all comments and questions, updated suggested code changes and integrated requested tests. |
Two bugs uncovered while testing Live Activities via local WebSocket push: 1. Use InterfaceDirect on Simulator — NEAppPushProvider (Network Extension) does not run in the Simulator, so the local push channel was never opened and notifications fell back to remote APNs/FCM which also fails on Simulator. 2. Promote live_activity fields into homeassistant payload in LegacyNotificationParserImpl — the WebSocket delivery path produces a flat payload where data.live_activity was never mapped into payload["homeassistant"]. NotificationCommandManager checks payload["homeassistant"]["live_activity"], so Live Activity notifications arrived as regular banners instead of starting a Live Activity. This bug also affects real devices on a local/LAN connection. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- LiveActivityRegistry: silence AlertConfiguration sound on start (sound: nil)
so the Dynamic Island bloom animation doesn't trigger an audible alert
- LiveActivityRegistry: track pending state for in-flight reservations so a
second rapid push with the same tag applies its newer state on confirm
instead of being silently dropped
- NotificationManager: willPresent now awaits handle() result before
suppressing the banner; unknown commands fall back to normal presentation
instead of being swallowed silently
- NotificationParserLegacy: only promote message to homeassistant dict when
it is a String, preventing Optional("...") from leaking into the payload
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
… activities - Use fixed frame(width: 44) for countdown timer in compact trailing slot instead of maxWidth: 50, preventing the Dynamic Island from squeezing the text narrower than M:SS requires - Add contentTransition(.numericText(countsDown: true)) to all three timer Text views (compact trailing, expanded bottom, lock screen) for smooth digit animation - Add computeStaleDate(for:) helper in LiveActivityRegistry: when chronometer is active, sets staleDate = countdownEnd + 2 s so the system marks the activity stale shortly after the timer ends rather than 30 min later; the +2 s offset also prevents the system spinner overlay that appears when staleDate == exactly countdownEnd - Fix iOS 26 SDK breaking change: AlertConfiguration.sound is now non-optional, changed sound: nil to .default - Handle new .pending ActivityState case added in iOS 26 - Fix NotificationCommandManager typo (was NotificationsCommandManager) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
4b3c244 to
ed7e1f6
Compare
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
I'm manually testing the debug options you included and it looks great, I think we can improve the UI for the live activity itself in lock screen but this can be an iteration. Can you add 2 things? Also have you checked how it behaves in macOS? |
- Show BetaLabel next to Live Activities in settings (same as Kiosk) - Gate Live Activities settings entry behind Current.isTestFlight to prevent it surfacing in a release build before fully tested Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Awesome. Sounds good
Added, let me know if this is right: ff65853
I have not, not familiar with running this directly in macOS. Let me know what I should look at in regard to that. Also, I know we still gotta decide about the differentiating flags between iOS/Android |
Maybe I am confusing how mac live activities work, I think they only display the ones coming from your iPhone, anyway, your checks for "supports live activity" should be enough for now.
I missed where I commented about it but yes, I want android and iOS to have as minimum differences as possible, if one ignores tags that the other uses thats fine, but I dont want to duplicate tags that represent the same |
…ivities Unifies the iOS and Android notification data field: live_update: true now triggers a Live Activity on iOS, matching the field Android already uses for Live Updates. A single YAML automation now targets both platforms with no platform-specific keys. Internal command strings, webhook types, and keychain keys are unchanged. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
No idea, not sure. Sounds good.
No problem, has grown into a large PR here lol
Let me know if we want to cutoff the feature for iOS 17+ rather than 16.2+ and I can do that too. |
|
I see many checks for iOS 16.2 but since we can only start activities from server side (Home Assistant) on 17.2+, doesnt it make more sense to support starting on 17.2? EDIT: I see my comment overlapped yours haha sorry, yes let's move for 17 |
Replaces all iOS 16.2 availability checks with iOS 17.2, flattens the nested 16.2/17.2 blocks in AppDelegate and HAAPI into a single 17.2 check, and removes now-redundant @available(iOS 17.2, *) method annotations from the protocol and actor (the outer type annotation is sufficient). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
|
Are you on discord? If so can you reach out to me, I want to have a quick chat about the full picture and If I understood your implementation correctly (bgoncal2) |
Yes I am, I'll reach out! |
Sources/Shared/Notifications/NotificationCommands/HandlerLiveActivity.swift
Outdated
Show resolved
Hide resolved
Tests/Shared/LiveActivity/NotificationsCommandManagerLiveActivityTests.swift
Show resolved
Hide resolved
…ard, live_update test key - isValidTag: replace CharacterSet.alphanumerics (Unicode-inclusive) with explicit ASCII character set to match the stated [a-zA-Z0-9_-] contract - WidgetsBundle17: wrap HALiveActivityConfiguration() in #available(17.2) guard, matching WidgetsBundleLegacy — avoids availability error on iOS 17.0/17.1 - Tests: change live_activity flag to live_update to match production routing (NotificationsCommandManager routes on live_update, not live_activity) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…cking Per bgoncal review feedback: remove the Any? backing store workaround and the @available annotation on the stored property. Move @available(iOS 17.2, *) to individual protocol methods so LiveActivityRegistryProtocol can be referenced without an availability guard, then store it as LiveActivityRegistryProtocol? and return nil at runtime when on iOS < 17.2. Update all call sites to optional-chain (?.); AppDelegate uses guard-let since it already holds a non-nil reference after pre-warming inside #available(17.2). Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Restores the original single-line form to keep the diff focused on Live Activities changes, per bgoncal review feedback. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…me-assistant#4453) This property was added in an earlier commit but main removed the TestFlight gate for mTLS entirely (PR home-assistant#4453). Drop it to avoid a merge conflict and align with the direction main took. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…iveActivities BetaLabel was removed in main (home-assistant#4453). Resolve conflict by keeping LabsLabel() for both .kiosk and .liveActivities entries in the settings list. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
| private var _liveActivityRegistryBacking: LiveActivityRegistryProtocol? | ||
|
|
||
| /// Call `_ = Current.liveActivityRegistry` on the main thread at launch (before any | ||
| /// background thread can access it) to avoid a lazy-init race between concurrent callers. | ||
| public var liveActivityRegistry: LiveActivityRegistryProtocol? { | ||
| get { | ||
| if let existing = _liveActivityRegistryBacking { | ||
| return existing | ||
| } | ||
| if #available(iOS 17.2, *) { | ||
| let registry = LiveActivityRegistry() | ||
| _liveActivityRegistryBacking = registry | ||
| return registry | ||
| } | ||
| return nil | ||
| } | ||
| set { | ||
| _liveActivityRegistryBacking = newValue | ||
| } | ||
| } |
There was a problem hiding this comment.
Why do we need both? Based on your comment one lazy var would be enough right?
















Summary
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: truein the data payload, the companion app starts a Live Activity instead of (or in addition to) a standard notification banner. Subsequent pushes with the sametagupdate it in-place silently.clear_notification+tagends 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 samelive_update: truetrigger — a single YAML block targets iOS 17.2+ and Android 16+ with no platform-specific keys.New files:
Sources/Shared/LiveActivity/HALiveActivityAttributes.swift— theActivityAttributestype. Field names match the Android payload spec. Struct name andCodingKeysare wire-format frozen — APNs push-to-start payloads reference the Swift type name directly.Sources/Shared/LiveActivity/LiveActivityRegistry.swift— SwiftactormanagingActivity<HALiveActivityAttributes>lifecycle. Uses a reservation pattern to prevent duplicate activities when two pushes with the sametagarrive simultaneously.Sources/Shared/Notifications/NotificationCommands/HandlerLiveActivity.swift— start/update and endNotificationCommandHandlerimplementations, guarded against thePushProviderextension process where ActivityKit is unavailable.Sources/Extensions/Widgets/LiveActivity/—ActivityConfigurationwrapper, 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— registersHALiveActivityConfigurationin all threeWidgetBundlevariants behind#available(iOSApplicationExtension 17.2, *)NotificationsCommandManager.swift— registers new handlers;HandlerClearNotificationnow also ends a matching Live Activity whentagis presentHAAPI.swift— addssupports_live_activities,supports_live_activities_frequent_updates,live_activity_push_to_start_token,live_activity_push_to_start_apns_environmentto registration payload under a single#available(iOS 17.2, *)checkAppDelegate.swift— reattaches surviving activities at launch and starts observing push-to-start tokens under a single#available(iOS 17.2, *)checkInfo.plist—NSSupportsLiveActivities+NSSupportsLiveActivitiesFrequentUpdatesSettingsItem.swift/SettingsView.swift— Live Activities settings row is gated behindCurrent.isTestFlightand shows aBetaLabelbadgeTests:
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.liveActivityTokensupport (Firebase Admin SDK v13.5.0+) — no custom APNs client or credentials needed.Live Activities entry in Settings is gated behind TestFlight. The settings row only appears when
Current.isTestFlightis true, preventing it from surfacing in a release build before the feature is fully tested. ABetaLabelbadge is shown alongside the row title.iPad:
areActivitiesEnabledis alwaysfalseon iPad — Apple system restriction. The Settings screen shows "Not available on iPad." The registry silently no-ops. HA receivessupports_live_activities: falsein the device registration for iPad.HALiveActivityAttributesis frozen post-ship. The struct name appears asattributes-typein APNs push-to-start payloads. Renaming it silently breaks all remote starts. TheContentStateCodingKeysare 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 DEBUGso 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.UNUserNotificationCenterin tests. Theclear_notification+tag→ Live Activity dismissal path is covered by code review rather than a unit test.HandlerClearNotificationcallsUNUserNotificationCenter.current().removeDeliveredNotificationssynchronously, which requires a real app bundle and throwsNSInternalInconsistencyExceptionin 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: