Skip to content

Adds iOS Live Activities support#4444

Draft
rwarner wants to merge 22 commits intohome-assistant:mainfrom
rwarner:feat/live-activities
Draft

Adds iOS Live Activities support#4444
rwarner wants to merge 22 commits intohome-assistant:mainfrom
rwarner:feat/live-activities

Conversation

@rwarner
Copy link

@rwarner rwarner commented Mar 19, 2026

Summary

For architecture decisions, data model details, iOS version strategy, push token flow, and rate limiting — see 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_activity: 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 — a single YAML automation block targets both platforms.

data:
  title: "Washing Machine"
  message: "Cycle in progress"
  data:
    tag: washer_cycle
    live_update: true           # Android 16+
    live_activity: true         # iOS 16.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 16.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
  • AppDelegate.swift — reattaches surviving activities at launch; observes push-to-start token stream (iOS 17.2+)
  • Info.plistNSSupportsLiveActivities + NSSupportsLiveActivitiesFrequentUpdates

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

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 ActivityContent API (required for staleDate and 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.liveActivityToken support (Firebase Admin SDK v13.5.0+) — no custom APNs client or credentials needed.

Server-side work — all PRs now open:

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:

Copy link
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 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 + tag dismissal).
  • Adds widget extension ActivityConfiguration UI, 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).

Copy link
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

Copilot reviewed 21 out of 21 changed files in this pull request and generated 4 comments.

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>
@bgoncal
Copy link
Member

bgoncal commented Mar 23, 2026

tip for lint you can use bundle exec fastlane autocorrect

@bgoncal
Copy link
Member

bgoncal commented Mar 23, 2026

Can you make a screen recording showing the whole flow? Showing HA UI sending the push and iPhone handling it

@rwarner
Copy link
Author

rwarner commented Mar 23, 2026

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

@bgoncal
Copy link
Member

bgoncal commented Mar 23, 2026

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

@rwarner
Copy link
Author

rwarner commented Mar 23, 2026

Thanks, I'll look at this and see if I can produce some more examples for you / video on top of the previous gif

@rwarner
Copy link
Author

rwarner commented Mar 23, 2026

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 for the link! I looked into xcrun simctl push — from my testing, it delivers through UNUserNotificationCenter rather than ActivityKit's push token channel, so Live Activity payloads (event, content-state, etc.) arrive as regular notifications instead of updating the running activity. The simulator also returns nil for ActivityKit push tokens, so real APNs delivery to a Live Activity isn't possible either. Happy to be corrected if there's a way I'm missing!

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 — I'll try to set that up.
Update: Would need a new Hass instance running my new core feature branch

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.mp4
Debug.sample.mp4

@rwarner
Copy link
Author

rwarner commented Mar 23, 2026

Progress with xcrun simctl actually, I was able to get a "push" to work using the following file and a small recent code change I just pushed

live_activity_test.apns

{
  "Simulator Target Bundle": "com.rwarner.ha.HomeAssistant.dev",
  "aps": {
    "alert": {
      "title": "Washing Machine",
      "body": "Cycle in progress"
    },
    "mutable-content": 1,
    "content-available": 1
  },
  "homeassistant": {
    "live_activity": true,
    "tag": "washer_cycle",
    "title": "Washing Machine",
    "message": "Cycle in progress",
    "progress": 2700,
    "progress_max": 3600,
    "chronometer": true,
    "when": 2700,
    "when_relative": true,
    "notification_icon": "mdi:washing-machine",
    "notification_icon_color": "#2196F3"
  }
}

xcrun simctl push booted com.rwarner.ha.HomeAssistant.dev /tmp/live_activity_test.apns

Which works while the application is in the foreground of the iPhone:

For some reason the dynamic island in the emulator is fussy but pops up very quickly
Screenshot 2026-03-23 at 3 38 23 PM

trimmed.mp4

Tests: push payload → NotificationCommandManager → HandlerStartOrUpdateLiveActivity → LiveActivityRegistry → ActivityKit → Lock Screen

However to test an actual background push Live Activity handler from what I can figure out I would need:

  • Local WebSocket — HA instance compiled with my core changes on same network
  • Real APNs — paid developer account
  • FCM — production Firebase project matching the bundle ID
  • ?Apple's Push Notification console in their paid dev area?

@bgoncal
Copy link
Member

bgoncal commented Mar 24, 2026

Local WebSocket — HA instance compiled with my core changes on same network

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

@home-assistant home-assistant bot marked this pull request as draft March 24, 2026 10:15
@home-assistant
Copy link

Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍

Learn more about our pull request process.

@codecov
Copy link

codecov bot commented Mar 24, 2026

Codecov Report

❌ Patch coverage is 41.26074% with 205 lines in your changes missing coverage. Please review.
⚠️ Please upload report for BASE (main@db92bab). Learn more about missing BASE report.

Files with missing lines Patch % Lines
...ces/Shared/LiveActivity/LiveActivityRegistry.swift 0.00% 172 Missing ⚠️
Sources/Shared/API/HAAPI.swift 26.08% 17 Missing ⚠️
...Shared/LiveActivity/HALiveActivityAttributes.swift 60.00% 6 Missing ⚠️
Sources/Shared/Environment/Environment.swift 64.28% 5 Missing ⚠️
...ficationCommands/NotificationsCommandManager.swift 77.27% 5 Missing ⚠️
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.
📢 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.

@bgoncal
Copy link
Member

bgoncal commented Mar 24, 2026

In your example we have 2 different tags, one for android and one for iOS:

    live_update: true           # Android 16+
    live_activity: true         # iOS 16.2+

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?

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 and others added 2 commits March 24, 2026 10:16
…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>
rwarner and others added 17 commits March 24, 2026 10:16
…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>
…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>
@rwarner rwarner force-pushed the feat/live-activities branch from 8af7679 to c4ad13d Compare March 24, 2026 14:29
rwarner and others added 2 commits March 24, 2026 10:47
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>
@rwarner
Copy link
Author

rwarner commented Mar 24, 2026

Local WebSocket — HA instance compiled with my core changes on same network

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

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.


In your example we have 2 different tags, one for android and one for iOS:

    live_update: true           # Android 16+
    live_activity: true         # iOS 16.2+

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?

Great point actually. I can change this to just use the pre-existing live_update YAML that Android already uses and nobody has to make YAML additions / changes. (This would also require updating the other repos but honestly might be worth it)

Unless you think it would be handy to have the following example:

they might want a Live Activity on iOS but a regular sticky notification on Android (or vice versa).

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>
@rwarner
Copy link
Author

rwarner commented Mar 24, 2026

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

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.

3 participants