Skip to content

Add iOS Live Activity webhook handlers to mobile_app#166072

Draft
rwarner wants to merge 23 commits intohome-assistant:devfrom
rwarner:feat/ios-live-activity
Draft

Add iOS Live Activity webhook handlers to mobile_app#166072
rwarner wants to merge 23 commits intohome-assistant:devfrom
rwarner:feat/ios-live-activity

Conversation

@rwarner
Copy link
Copy Markdown

@rwarner rwarner commented Mar 20, 2026

Proposed change

Adds server-side support for iOS Live Activities in the `mobile_app` integration. This is the HA core companion to the iOS companion app PRs and the relay server PR.

Live Activities let Home Assistant automations push real-time state to the iOS Lock Screen and Dynamic Island. The iOS app handles the ActivityKit lifecycle; this PR adds the webhook handlers and notification routing that HA core needs.

How it works: When a notification contains `live_update: true` and a `tag`, the notify service looks up the stored APNs Live Activity token for that tag and includes it alongside the normal FCM registration token in the relay request. The relay places it in the FCM message's `apns.liveActivityToken` field — no separate APNs endpoint or credentials needed. If no per-activity token exists, it falls back to the device's push-to-start token (iOS 17.2+) to start a new activity remotely.

What this adds:

  • `live_activity_token` webhook — stores per-activity APNs push tokens sent by the iOS app when an activity is created via ActivityKit
  • `live_activity_dismissed` webhook — removes the stored token when the activity ends on device
  • `SCHEMA_APP_DATA` validation for the optional `live_activity_push_to_start_token` field in device registration
  • Notification routing in `notify.py` — extracts and forwards the Live Activity APNs token when `live_update: true` is present
  • In-memory `DATA_LIVE_ACTIVITY_TOKENS` store, initialized in `init.py` and cleaned up on config entry unload

Type of change

  • Dependency upgrade
  • Bugfix (non-breaking change which fixes an issue)
  • New integration (thank you!)
  • New feature (which adds functionality to an existing integration)
  • Deprecation (breaking change to happen in the future)
  • Breaking change (fix/feature causing existing functionality to break)
  • Code quality improvements to existing code or addition of tests

Additional information

@home-assistant
Copy link
Copy Markdown
Contributor

Hey there @home-assistant/core, mind taking a look at this pull request as it has been labeled with an integration (mobile_app) you are listed as a code owner for? Thanks!

Code owner commands

Code owners of mobile_app can trigger bot actions by commenting:

  • @home-assistant close Closes the pull request.
  • @home-assistant rename Awesome new title Renames the pull request.
  • @home-assistant reopen Reopen the pull request.
  • @home-assistant unassign mobile_app Removes the current integration label and assignees on the pull request, add the integration domain after the command.
  • @home-assistant add-label needs-more-information Add a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) to the pull request.
  • @home-assistant remove-label needs-more-information Remove a label (needs-more-information, problem in dependency, problem in custom component, problem in config, problem in device, feature-request) on the pull request.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Adds Home Assistant Core support in the mobile_app integration for iOS Live Activities by introducing webhook handlers that store and clear per-activity APNs push tokens and emit lifecycle events for automations.

Changes:

  • Extend SCHEMA_APP_DATA and constants to support Live Activities capability flags and push-to-start registration fields.
  • Add update_live_activity_token and live_activity_dismissed webhooks that manage an in-memory token store and fire remote-origin bus events.
  • Add a supports_live_activities() helper and webhook tests covering token storage, defaults, and cleanup.

Reviewed changes

Copilot reviewed 5 out of 5 changed files in this pull request and generated 2 comments.

Show a summary per file
File Description
homeassistant/components/mobile_app/const.py Adds Live Activity-related constants/events and extends registration SCHEMA_APP_DATA.
homeassistant/components/mobile_app/__init__.py Initializes a new in-memory DATA_LIVE_ACTIVITY_TOKENS store under hass.data[DOMAIN].
homeassistant/components/mobile_app/webhook.py Implements the new Live Activity webhook handlers and fires lifecycle events.
homeassistant/components/mobile_app/util.py Adds supports_live_activities() helper based on stored app_data.
tests/components/mobile_app/test_webhook.py Adds tests for storing tokens, default env behavior, and dismiss cleanup/event firing.
Comments suppressed due to low confidence (1)

tests/components/mobile_app/test_webhook.py:1398

  • This test only asserts the HTTP status. To fully validate the contract of live_activity_dismissed (which returns empty_okay_response()), also assert the JSON body is {} (and optionally that no tokens were removed when none existed) to prevent regressions in response shape/side effects.
    resp = await webhook_client.post(
        f"/api/webhook/{webhook_id}",
        json={
            "type": "live_activity_dismissed",
            "data": {
                "tag": "nonexistent_activity",
            },
        },
    )

    assert resp.status == HTTPStatus.OK

Comment thread homeassistant/components/mobile_app/const.py Outdated
Comment thread homeassistant/components/mobile_app/__init__.py
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated no new comments.

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Comment thread homeassistant/components/mobile_app/notify.py
Comment thread homeassistant/components/mobile_app/webhook.py Outdated
Comment thread homeassistant/components/mobile_app/notify.py Outdated
Copilot AI review requested due to automatic review settings March 23, 2026 13:03
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 2 comments.

Comment thread homeassistant/components/mobile_app/webhook.py Outdated
Comment thread homeassistant/components/mobile_app/webhook.py Outdated
Copilot AI review requested due to automatic review settings March 23, 2026 18:29
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 1 comment.

Comment thread homeassistant/components/mobile_app/notify.py
Copilot AI review requested due to automatic review settings March 23, 2026 19:55
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Copilot reviewed 7 out of 7 changed files in this pull request and generated 3 comments.

Comment thread homeassistant/components/mobile_app/const.py Outdated
Comment thread homeassistant/components/mobile_app/notify.py Outdated
Comment thread homeassistant/components/mobile_app/webhook.py
Copilot AI review requested due to automatic review settings March 24, 2026 14:09
rwarner and others added 21 commits April 29, 2026 13:12
- Define EVENT_LIVE_ACTIVITY_TOKEN_UPDATED and EVENT_LIVE_ACTIVITY_DISMISSED
  constants in const.py instead of inline f-strings
- Add ATTR_APNS_ENVIRONMENT constant for schema and data access
- Add EventOrigin.remote to async_fire calls, matching webhook_fire_event pattern
- Use DATA_LIVE_ACTIVITY_TOKENS constant in tests instead of string literals
- Import event constants in tests for consistency

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Make push-to-start token and environment vol.Inclusive so they must be
  provided together — a token without an environment is ambiguous since
  sandbox tokens are rejected by the production APNs endpoint
- Clean up DATA_LIVE_ACTIVITY_TOKENS for the webhook_id in
  async_unload_entry to prevent stale tokens accumulating in memory
  when devices are removed or re-added

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
When a notification contains live_activity: true and a tag, the notify
service now routes it through the dedicated APNs relay endpoint instead
of FCM. This completes the direct APNs delivery path:

1. Per-activity token — if the iOS app has registered a push token for
   the given tag (via update_live_activity_token webhook), use that token
   and its stored push_url to deliver directly to the running activity.

2. Push-to-start fallback — if no per-activity token exists but the device
   has a push-to-start token in app_data (iOS 17.2+), use that to start
   a new activity remotely without the app being open.

3. Normal FCM — if live_activity is not set, or no tag is provided,
   the notification flows through the existing FCM path unchanged.

The apns_environment (sandbox/production) is included in registration_info
so the relay server can route to the correct APNs endpoint.

Adds 4 tests: stored token routing, push-to-start fallback, no-tag
fallthrough, and normal notification ignoring stored tokens.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
FCM v1 API natively supports Live Activities via apns.liveActivityToken.
This simplifies the core integration:

- notify.py: instead of routing to a separate relay URL, sends both the
  FCM token (push_token) and Live Activity APNs token (live_activity_token)
  to the SAME relay endpoint. The relay server places it in the FCM
  message's apns.liveActivityToken field, and FCM handles APNs delivery.
- webhook.py: update_live_activity_token schema simplified — removed
  push_url and apns_environment (FCM handles routing automatically)
- const.py: removed ATTR_APNS_ENVIRONMENT (no longer needed)
- Tests updated to match simplified token storage and routing

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- notify.py: guard against non-string tag values in notification payload
  to avoid runtime errors when used as dict key
- webhook.py: use ATTR_DEVICE_ID and CONF_WEBHOOK_ID constants in event
  data instead of string literals for consistency

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- webhook.py: reject empty push tokens with vol.Length(min=1) in
  update_live_activity_token schema
- notify.py: use `is not True` for live_activity flag to prevent truthy
  non-bool values like string "false" from triggering Live Activity routing
- const.py: reject empty push-to-start tokens with vol.Length(min=1) in
  SCHEMA_APP_DATA

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…ssed tag, add unload test

- Replace CONF_WEBHOOK_ID with ATTR_WEBHOOK_ID as the key in
  EVENT_LIVE_ACTIVITY_TOKEN_UPDATED and EVENT_LIVE_ACTIVITY_DISMISSED
  payloads to keep runtime event data semantically separate from config
  constants
- Require non-empty ATTR_LIVE_ACTIVITY_TAG in the live_activity_dismissed
  webhook schema (vol.Length(min=1)) to match the update handler and
  prevent stale token store entries from empty tags
- Add test_unload_removes_live_activity_tokens to verify live activity
  tokens are purged from hass.data when a config entry is unloaded

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…ivities

Unifies the iOS and Android notification data field: live_update: true now
triggers Live Activity routing on iOS, matching the field Android already uses.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…y _get_live_activity_token signature

- Drop EVENT_LIVE_ACTIVITY_TOKEN_UPDATED and EVENT_LIVE_ACTIVITY_DISMISSED — nothing
  consumes these events in any of the three repos (no automation triggers, no iOS
  listener, no relay usage), so they add noise without value
- Remove supports_live_activities() from util.py — defined but never called
- Pass app_data directly into _get_live_activity_token instead of the full
  registration dict (edenhaus review feedback)
- Group Live Activity push constants with other PUSH attrs in const.py
- Update tests to remove event-capture assertions

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The generic 'tag' field name collides with the notification tag used
elsewhere in mobile_app. Using 'live_activity_tag' makes the webhook
contract unambiguous. notify.py continues to read 'tag' from the
notification payload (user YAML) unchanged.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Core registered 'update_live_activity_token' and 'live_activity_dismissed'
but the iOS app sends 'mobile_app_live_activity_token' and
'mobile_app_live_activity_dismissed', matching the mobile_app_ prefix
convention used elsewhere in the integration. Rename core handlers to match.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
live_update is the cross-platform YAML key shared with Android; on iOS
it maps to ActivityKit Live Activities, on Android to a different
mechanism. The live_activity naming in webhook handlers and token storage
is intentionally iOS-specific.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove unused ATTR_SUPPORTS_LIVE_ACTIVITIES and ATTR_SUPPORTS_LIVE_ACTIVITIES_FREQUENT_UPDATES constants and schema entries
- Add ATTR_LIVE_UPDATE, ATTR_LIVE_ACTIVITY_TOKEN, and ATTR_PUSH_TAG constants; replace hardcoded strings
- Simplify tag check: remove redundant isinstance(tag, str) guard
- Use walrus operator for live activity token assignment in notify.py
- Store live activity push token as a plain string instead of a dict
- Remove comment before webhook registration already described in docstring
- Simplify push-to-start token return to app_data.get(...)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Drop vol.Length(min=1) from the tag and push token fields — consistent
with webhook_scan_tag which uses plain cv.string. APNs tokens are
explicitly variable length per Apple docs; hardcoding a minimum is
both inconsistent and fragile.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lution

Two Live Activity test docstrings lost their indentation when resolving
rebase conflicts, causing a syntax error.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Consistent with all other webhook types in this integration which use
short names (scan_tag, update_location, etc.) without a mobile_app_ prefix.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Remove unused AsyncGenerator import from notify.py
- Sort ATTR_PUSH_TAG import alphabetically (after ATTR_PUSH_RATE_LIMITS_SUCCESSFUL)
- Wrap long line in webhook_update_live_activity_token

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Store per-activity tokens as {"push_token": <token>} dict in
  DATA_LIVE_ACTIVITY_TOKENS so the structure matches test expectations
  and leaves room for additional fields without a schema change
- Update _get_live_activity_token to read the push_token key from the dict
- Update test_notify.py setups to use dict format
- Fix stale "mobile_app_live_activity_token" type name in test_init.py
  (should be "live_activity_token" after the prefix-drop rename)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rwarner rwarner force-pushed the feat/ios-live-activity branch from 9cf6a0e to eb478d4 Compare April 29, 2026 17:13
@rwarner rwarner marked this pull request as ready for review April 29, 2026 18:27
@home-assistant home-assistant Bot requested a review from edenhaus April 29, 2026 18:27
@edenhaus
Copy link
Copy Markdown
Member

Please don't rebase after a review have started as this means a reviewer needs to review again the whole PR instead of the changes. Keep this in mind and also instruct claude to do it

@edenhaus
Copy link
Copy Markdown
Member

Previously requested changes are not implemented. Drafting the PR. Not sure if the AI has reintroduced them or if they still pending to implement but due the rebase each commit is marked as new and I will not check each commit. Please make sure all previous requested changes are implemented correctly before marking the PR as ready for review.

and again we require the human in the loop and it feels from the comments that the are written completely by the AI

Copy link
Copy Markdown
Member

@edenhaus edenhaus left a comment

Choose a reason for hiding this comment

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

PR description not in sync with the changes

@home-assistant home-assistant Bot marked this pull request as draft April 30, 2026 08:07
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rwarner
Copy link
Copy Markdown
Author

rwarner commented Apr 30, 2026

Okay addressing some of the things here. This has unfortunately gotten to be a mess

we require the human in the loop and it feels from the comments that the are written completely by the AI

Yes, I did use AI for some responses that I felt like it could explain more sufficiently than I could. But I am personally involved in all of this. I apologize, as I was unaware this was against policy with OHF

PR description not in sync with the changes

Updated the description to match with current fork branch changes

don't rebase after a review have started

Sorry, again I was unaware this was a bad thing and now understand. It was 100+commits behind and I was simply doing this to try to fix CI solely.


Items that were tended to, as some things were lost during the rebase and I thought they were still in there:

  • ATTR_LIVE_ACTIVITY_PUSH_TO_START_APNS_ENVIRONMENT removed from const.py: had been removed, rebase brought it back
  • PUSH constants reordered in const.py: all push-related attrs grouped together, rebase undid the ordering
  • SCHEMA_APP_DATA simplified: vol.Inclusive pair for APNS environment removed, push-to-start token changed to vol.Optional with plain cv.string, rebase reverted all of this
  • Push-to-start return in notify.py: simplified to return app_data.get(...), rebase reverted it back to the walrus pattern
  • Verbose/AI-sounding docstrings and inline comments: had been written in a verbose style, @edenhaus flagged them all as AI-sounding on Apr 30. After the rebase, it exposed them again

I have "unresolved" the comments that were lost during the rebase so they can be re-verified.

Would it be better for you @edenhaus if I just started a new PR at this point?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants