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.71%
=======================================
Files ? 269
Lines ? 15918
Branches ? 0
=======================================
Hits ? 6799
Misses ? 9119
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>
…tUI views Adds the ActivityKit foundation for Home Assistant iOS Live Activities, targeting feature parity with Android's live_update notification system. - HALiveActivityAttributes: ActivityAttributes struct with static tag/title and dynamic ContentState (message, criticalText, progress, progressMax, chronometer, countdownEnd, icon, color). Field names mirror Android companion notification fields. Never rename post-ship — struct name is used as the APNs attributes-type identifier. - LiveActivityRegistry (actor): Thread-safe lifecycle manager with TOCTOU- safe reservation pattern. Handles start, update, end, and reattachment to activities surviving process termination. Supports both iOS 16.1 (contentState:) and iOS 16.2+ (content: ActivityContent) API shapes. - HandlerStartOrUpdateLiveActivity / HandlerEndLiveActivity: New NotificationCommandHandler structs registered for "live_activity" and "end_live_activity" commands, bridging PromiseKit to async/await. - HandlerClearNotification extended: clear_notification + tag now also ends any matching Live Activity — identical YAML dismisses on both iOS and Android. - HALiveActivityConfiguration: WidgetKit ActivityConfiguration wiring registered in WidgetsBundle18 (iOS 18+ widget bundle). - HALockScreenView: Lock Screen / StandBy view with icon, timer or message, and optional ProgressView. Stays within 160pt system height limit. - HADynamicIslandView: All four DynamicIsland presentations (compact leading/trailing, minimal, expanded) using SwiftUI result builder API. - AppEnvironment: liveActivityRegistry property added under #if os(iOS) && canImport(ActivityKit) using Any? backing store to avoid @available stored property compiler restriction. All ActivityKit code guarded by #if canImport(ActivityKit) and @available(iOS 16.1, *). iPad gracefully returns areActivitiesEnabled==false. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…g, capability advertisement Wires Live Activities into the existing notification pipeline so HA automations can start, update, and end activities from YAML without app changes. Notification routing: - NotificationCommandManager.handle() now intercepts live_activity: true in the homeassistant dict as an alternative to the command-based approach (message: live_activity). This matches Android's data.live_update: true pattern — the message field can be real body text while live_activity: true in data triggers ActivityKit. Both paths funnel into HandlerStartOrUpdateLiveActivity. - HandlerStartOrUpdateLiveActivity/End guard Current.isAppExtension — PushProvider (NEAppPushProvider) runs in a separate OS process where ActivityKit is unavailable. The same notification is re-delivered to the main app via UNUserNotificationCenter, where the main app processes it correctly. Tag validation: - Tag validated as [a-zA-Z0-9_-], max 64 chars, matching safe APNs collapse ID subset. Invalid tags log an error and fulfill cleanly instead of crashing or infinite-retrying. Push token + lifecycle observation: - LiveActivityRegistry.makeObservationTask() now runs two concurrent async streams via withTaskGroup: pushTokenUpdates (reports each new token to all HA servers) and activityStateUpdates (reports dismissal to HA so it stops sending updates). - Webhooks use type "mobile_app_live_activity_token" and "mobile_app_live_activity_dismissed" via the existing WebhookManager.sendEphemeral path. - APNs environment (sandbox/production) is included in token reports for relay routing. Capability advertisement: - mobileAppRegistrationRequestModel() adds supports_live_activities: true on iOS 16.1+ and supports_live_activities_frequent_updates on iOS 17.2+ to AppData, so the HA device registry shows the capability and can gate Live Activity UI in automations. App launch recovery: - AppDelegate.willFinishLaunchingWithOptions calls setupLiveActivityReattachment(), which runs liveActivityRegistry.reattach() on a Task. This restores observation (token + lifecycle) for any activities that survived process termination — ensuring no token updates are missed between launches. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…reporting Adds iOS 17.2+ push-to-start support so HA can start a Live Activity entirely via APNs without the app being in the foreground (best-effort, ~50% success from terminated state — the primary flow remains notification command → app). Token observation: - LiveActivityRegistry.startObservingPushToStartToken() observes the async stream Activity<HALiveActivityAttributes>.pushToStartTokenUpdates (iOS 17.2+). - Each new token is stored in Keychain (not UserDefaults — this token can start any new activity so it warrants stronger storage) under a stable key. - Triggers api.updateRegistration() on all connected servers so the token is immediately available in the HA device registry. AppDelegate: - setupLiveActivityReattachment() now runs both reattach() and, on iOS 17.2+, startObservingPushToStartToken() sequentially in a single long-lived Task. The push-to-start stream is infinite and is kept alive for the app's lifetime. Registration payload: - mobileAppRegistrationRequestModel() includes live_activity_push_to_start_token and live_activity_push_to_start_apns_environment in AppData on iOS 17.2+ when a token is stored. The relay server uses these to route push-to-start APNs payloads to the correct environment (sandbox vs production). - Extracted apnsEnvironmentString() helper shared by push-to-start and per-activity token reporting. Protocol: - LiveActivityRegistryProtocol gains startObservingPushToStartToken() @available(iOS 17.2, *) for testability and mock injection. Note: relay server changes (new APNs endpoint, JWT routing, push-to-start payload format) are required for end-to-end functionality and are tracked separately. 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add LiveActivitySettingsView with status, active activities list, privacy notice, and frequent-updates section (iOS 17.2+) - Add SettingsItem.liveActivities wired into the Settings navigation - Add hasSeenLiveActivityDisclosure flag to SettingsStore - Implement showPrivacyDisclosureIfNeeded() one-time local notification shown the first time a Live Activity is started, reminding the user that lock screen content is visible without authentication 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
Architecture & correctness: - Pre-warm liveActivityRegistry on main thread before spawning Tasks to eliminate lazy-init race between concurrent callers (notification handler + reattach Task). Split reattach and startObservingPushToStartToken into two independent Tasks so an infinite stream doesn't block reattach. - Bridge HandlerClearNotification Live Activity end into the returned Promise so the background fetch window stays open until end() completes. - Use registered "live_activity" handler instance in the intercept path instead of constructing a second HandlerStartOrUpdateLiveActivity(). Deduplication & simplification: - Extract apnsEnvironmentString() to Current.apnsEnvironment, removing identical private methods from LiveActivityRegistry and HAAPI. - Remove duplicate Color(hex:)/UIColor(hex:) extensions from HALockScreenView — Shared already provides superior versions that handle CSS color names, nil, 3/4/6/8-digit hex, and log errors on bad input. - Consolidate "#03A9F4" (HA blue) to a shared haBlueHex constant used by both HALockScreenView and HADynamicIslandView. - Hoist staleDate interval to kLiveActivityStaleInterval constant (was hardcoded as 30 * 60 in three call sites). Bug fixes: - Fix progress/progressMax/when JSON number coercion: `as? Int` silently returns nil when the JSON number is Double-backed. Use NSNumber coercion so both Int and Double values decode correctly. Parse `when` as Double to preserve sub-second Unix timestamps. Correctness cleanup: - Remove dead Task.isCancelled guard in pushTokenUpdates loop — Swift cooperative cancellation exits the for-await at the suspension point, not at the next iteration. - Remove misleading async from reportPushToStartToken — it was fire-and- forget internally; removing async makes the calling convention honest. Co-Authored-By: Claude <noreply@anthropic.com>
…mprove privacy disclosure - Add L10n keys (live_activity.*) to Localizable.strings and Strings.swift for all user-facing strings in LiveActivitySettingsView and SettingsItem - Wire L10n.LiveActivity.* throughout LiveActivitySettingsView and SettingsItem - Remove LiveActivitySettingsView availability wrapper — deployment target is iOS 15, the settings item is already filtered out below iOS 16.1 in allVisibleCases, so the unreachable fallback Text() added noise without providing safety - Replace UNNotificationRequest privacy disclosure with flag-only recording: a local notification silently fails when notification permission is not granted, meaning the users who need the warning most (new users) never see it. The permanent privacy section in LiveActivitySettingsView is the correct disclosure surface. Co-Authored-By: Claude <noreply@anthropic.com>
Security (P3): - Apply isValidTag() to HandlerEndLiveActivity — end handler now rejects invalid tags the same way the start handler does (consistency fix) - Cap dismissal_policy "after:<timestamp>" to 24 hours maximum — iOS enforces its own ceiling but this is defensive against future OS changes Performance (P2): - Run endAllActivities() concurrently via withTaskGroup instead of sequentially awaiting each end() call across the actor boundary Co-Authored-By: Claude <noreply@anthropic.com>
Auto-formatted by swiftformat --config .swiftformat. Changes include argument wrapping, Preview macro formatting, and comment block style. Co-Authored-By: Claude <noreply@anthropic.com>
This reverts commit dc8ad44.
…project - Add import ActivityKit to HAAPI.swift so ActivityAuthorizationInfo resolves - Fix Activity.end() availability: use end(using:dismissalPolicy:) on iOS 16.1, end(_:dismissalPolicy:) on iOS 16.2+ (API signature changed between versions) - Fix MaterialDesignIcons usage: init is non-failable, remove if-let binding - Fix L10n references: EndAll.confirmTitle/confirmButton → EndAll.Confirm.title/button - Fix SFSymbol names: remove erroneous Icon suffix (.livephoto, .lockShield, .bolt) - Remove #Preview blocks incompatible with iOS 26 SDK's PreviewActivityBuilder - Register all new files in Xcode project (pbxproj) with correct targets - Add NSSupportsLiveActivities and NSSupportsLiveActivitiesFrequentUpdates to Info.plist Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Lets Home Assistant automations push real-time state to the Lock Screen and Dynamic Island. Matches Android Live Notifications field names (tag, title, message, progress, chronometer, notification_icon, etc.) so a single YAML automation block targets both platforms. - HALiveActivityAttributes: wire-format frozen ActivityAttributes struct - LiveActivityRegistry: Swift actor with reservation pattern to prevent duplicate activities from simultaneous pushes with the same tag - HandlerLiveActivity: start/update and end NotificationCommandHandler implementations, guarded against the PushProvider extension process - HALiveActivityConfiguration / HALockScreenView / HADynamicIslandView: widget views for Lock Screen, StandBy, and Dynamic Island - LiveActivitySettingsView: status, active activities list, privacy disclosure, and 11 debug scenarios for pre-server-side testing - Registers handlers in NotificationCommandManager; clear_notification with a tag now also ends a matching Live Activity - Adds supports_live_activities, supports_live_activities_frequent_updates, and live_activity_push_to_start_token to the registration payload - AppDelegate reattaches surviving activities at launch and observes push-to-start token stream (iOS 17.2+) - Info.plist: NSSupportsLiveActivities + NSSupportsLiveActivitiesFrequentUpdates - Gated at iOS 16.2 throughout (ActivityContent / staleDate API) - iPad: areActivitiesEnabled is always false; UI and registry handle gracefully Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
45 new tests across 3 files: HandlerLiveActivityTests (36 tests): - isValidTag: alphanumeric, hyphen, underscore, length boundary, invalid chars (space, dot, slash, @), empty string vacuous-true - contentState(from:): minimal defaults, full field mapping, Double→Int truncation, absolute/relative when, missing when - handle(): app extension guard, missing/empty/invalid tag, missing/empty title, successful path, registry-throws rejection, privacy disclosure flag HandlerEndLiveActivityTests (12 tests): - App extension guard - Tag validation (missing, empty, invalid) - Dismissal policy: no policy → immediate, "default", "after:<ts>" → .after, "after:<ts>" capped at 24h, invalid timestamp → immediate, unknown → immediate NotificationsCommandManagerLiveActivityTests (9 tests): - live_activity command routing → startOrUpdate - live_activity: true flag routing → startOrUpdate - live_activity: false falls through (no registry call) - end_live_activity command → registry.end (immediate) - end_live_activity with dismissal_policy: default → registry.end (default) - clear_notification without tag → registry.end not called - Missing homeassistant key → .notCommand error - Unknown command → .unknownCommand error Note: clear_notification+tag → registry.end path is covered by code review rather than a unit test. HandlerClearNotification calls UNUserNotificationCenter.current().removeDeliveredNotifications which requires a real app bundle and throws NSInternalInconsistencyException in the XCTest host. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove DEVELOPMENT_TEAM = 4Q9RHLUX47 from Widgets Debug pbxproj config - Remove unused PromiseKit import from LiveActivityRegistry - Fix reservation race: track cancelledReservations so end() arriving while Activity.request() is in-flight dismisses on confirmReservation - Add isAppExtension guard to clear_notification live activity path - Report supports_live_activities via areActivitiesEnabled (fixes iPad and devices with Live Activities disabled in Settings) - Fix doc comment 16.1 → 16.2 in LiveActivitySettingsView Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Fix SwiftGen doc comment: u203A escaped literal -> actual › character - Fix apnsEnvironment: TestFlight uses production APNs endpoint, not sandbox — remove isTestFlight ? "sandbox" branch - PR description: document live_activity_push_to_start_apns_environment field that was missing from the HAAPI.swift modified files list Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Add missing blank line after MARK comment in LiveActivitySettingsView - Wrap long log line in LiveActivityRegistry to stay within line limit Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
… Dynamic Island - NotificationManager willPresent: detect command notifications (live_activity or command key in homeassistant dict) and route through commandManager.handle() before returning. Suppresses the standard notification banner for commands so the user sees only the Live Activity, not a duplicate. - LiveActivityRegistry: immediately update with AlertConfiguration after Activity.request() to trigger the expanded Dynamic Island presentation. request() alone only shows the compact pill; the expanded "bloom" animation requires an update with an alert config per Apple's ActivityKit docs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Use DesignSystem.Spaces for all hardcoded padding/spacing values in HALockScreenView and HADynamicIslandView - Replace inline colors with Color.haPrimary fallback; scope haBlueHex as private static constants where UIColor(hex:) still needs it - Extract duplicated icon size (20pt) and compact trailing maxWidth (50pt) into private constants - Extract lock screen background tint into private constant - Remove redundant #available check in WidgetsBundle17 (iOS 17 > 16.2) - Remove unnecessary comment in WidgetsBundle18 - Add doc comment on isValidTag explaining allowed characters and why Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
8af7679 to
c4ad13d
Compare
Tests that validate values which must never change because they appear in APNs payloads, webhook requests, and notification routing: - HALiveActivityAttributes struct name (APNs attributes-type) - ContentState CodingKeys (JSON field names matching Android) - ContentState JSON round-trip preservation - Push-to-start Keychain key - Command strings: "live_activity", "end_live_activity" - Data flag: live_activity: true (Android-compat pattern) If any test fails, a wire-format contract was broken — the expected value should not be updated without coordinating with server-side PRs. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…me-assistant#15) Extract webhook type strings and dictionary key sets as static constants on LiveActivityRegistry so they can be tested directly: - webhookTypeToken / tokenWebhookKeys (for push token reporting) - webhookTypeDismissed / dismissedWebhookKeys (for activity dismissal) Private methods now reference these constants instead of inline strings. Contract tests validate the exact values match what HA core expects. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
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>
|
Great news, I was able to get core up and running locally and run the web socket test. Found two bugs I pushed up involving using the simulator. Got the live activity coming in via YAML: Websocket.Demo.mov |

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_activity: 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 — a single YAML automation block targets both platforms.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 16.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 payloadAppDelegate.swift— reattaches surviving activities at launch; observes push-to-start token stream (iOS 17.2+)Info.plist—NSSupportsLiveActivities+NSSupportsLiveActivitiesFrequentUpdatesTests:
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 16.2, not 16.1. The
ActivityContentAPI (required forstaleDateand anything practically useful) is 16.2+. The 16.1-only API is deprecated and produces blank activity cards. iOS 16.1 was a 3-week window in late 2022 — gating at 16.2 removes ~35 lines of dead deprecated code and eliminates the blank card scenario.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.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: