Skip to content

Add iOS Live Activity webhook handlers to mobile_app#166072

Open
rwarner wants to merge 37 commits into
home-assistant:devfrom
rwarner:feat/ios-live-activity
Open

Add iOS Live Activity webhook handlers to mobile_app#166072
rwarner wants to merge 37 commits into
home-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

Part of epic: home-assistant/epics#61
Fixes: home-assistant/iOS#4623

@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 Outdated
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
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 Outdated
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
…anup

Tokens are now stored via a dedicated Store (mobile_app.live_activity_tokens)
so they survive HA restarts. Each token is saved with a stored_at timestamp;
tokens older than 8 hours are filtered on load and cleaned up lazily on lookup.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rwarner
Copy link
Copy Markdown
Author

rwarner commented May 7, 2026

Re-instated the accidental removal of that comment above

Integrated the storage capability for tokens: 25340ac

Let me know how its looking overall and anything else to do

@edenhaus
Copy link
Copy Markdown
Member

edenhaus commented May 7, 2026

Please fix the merge conflict. If you think the PR is ready for another review please mark it as ready for review. Will wait for the next review until it's marked ready for review

@rwarner
Copy link
Copy Markdown
Author

rwarner commented May 7, 2026

Please fix the merge conflict. If you think the PR is ready for another review please mark it as ready for review. Will wait for the next review until it's marked ready for review

Sounds good. I was hesitant to mess it up again with the whole rebase and update from last time

@rwarner rwarner marked this pull request as ready for review May 7, 2026 13:46
@home-assistant home-assistant Bot requested a review from edenhaus May 7, 2026 13:46
Comment thread homeassistant/components/mobile_app/__init__.py Outdated
Comment thread homeassistant/components/mobile_app/__init__.py Outdated
@home-assistant home-assistant Bot marked this pull request as draft May 7, 2026 14:01
Removes the separate live_activity_tokens store. Tokens are now saved
in the main mobile_app store (STORAGE_VERSION bumped to 2). Existing
v1 data is migrated inline by defaulting the new key to {}. Stores
timestamps as floats so no custom datetime parsing is needed.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rwarner
Copy link
Copy Markdown
Author

rwarner commented May 7, 2026

Added to existing store: b0cb713

Removed custom datetime parsing

@rwarner rwarner marked this pull request as ready for review May 7, 2026 15:32
@home-assistant home-assistant Bot requested a review from edenhaus May 7, 2026 15:32
rwarner and others added 2 commits May 7, 2026 11:58
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@bgoncal
Copy link
Copy Markdown
Member

bgoncal commented May 19, 2026

@edenhaus Is the current state good to be fully tested locally? Let me know so I can setup my environment using all open PRs (iOS, FCM, Core)

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.

Sorry forgotten to submit the review

Comment on lines +251 to +252
if hass.data[DOMAIN][DATA_LIVE_ACTIVITY_TOKENS].pop(webhook_id, None) is not None:
await hass.data[DOMAIN][DATA_STORE].async_save(savable_state(hass))
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Why is this needed on unload?

Should we not remove the tokens on remove (function below)

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Sorry, good call. I moved the cleanup to async_remove_entry. Tokens should now survive unload and reload. Re-ran the tests and updated them

Comment on lines +82 to +83
elif DATA_LIVE_ACTIVITY_TOKENS not in app_config:
app_config[DATA_LIVE_ACTIVITY_TOKENS] = {}
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
elif DATA_LIVE_ACTIVITY_TOKENS not in app_config:
app_config[DATA_LIVE_ACTIVITY_TOKENS] = {}

not needed. If loaded it's there. Migration will make sure that existing one will get the field on upgrade. Where is the migration?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Sounds good. Added a _mobileappstore subclass with _async_migrate_func to handle version 1 -> 2 which adds live_activity_tokens on upgrade. Removed the elif like suggested and added a migration test

Comment on lines +86 to +92
live_activity_tokens: dict[str, Any] = {
wh_id: {
tag: entry
for tag, entry in tags.items()
if entry.get("stored_at", 0) > cutoff
}
for wh_id, tags in app_config[DATA_LIVE_ACTIVITY_TOKENS].items()
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

if statement is duplicated for example. Can we make the code nicer?

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Replaced the nest with a loop and re ran the tests once more for this. Let me know if I can make further improvements

if entry.get("stored_at", 0) > cutoff
}
for wh_id, tags in app_config[DATA_LIVE_ACTIVITY_TOKENS].items()
if any(entry.get("stored_at", 0) > cutoff for entry in tags.values())
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

If we found invalid entries during setup we should update the store.
As we are already iterating over all, we should identify the next token that will expire and setup a task that will be called at that time to remove it. Just one single task which will remove the token and schedule itself again with the next one

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Sounds good. Now going to track the earliest expiration during the startup loop. If expired tokens are found we save immediately and if there are valid ones, we schedule a single cleanup task that

  • Removes what's expired
  • Saves
  • Reschedules itself for the next one

Also added tests for both behaviors.


# Per-activity token — the activity is already running on the device.
webhook_id = entry.data[ATTR_WEBHOOK_ID]
live_activity_tokens = self.hass.data[DOMAIN].get(DATA_LIVE_ACTIVITY_TOKENS, {})
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Suggested change
live_activity_tokens = self.hass.data[DOMAIN].get(DATA_LIVE_ACTIVITY_TOKENS, {})
live_activity_tokens = self.hass.data[DOMAIN][DATA_LIVE_ACTIVITY_TOKENS]

use .get only when there could be a possibility that the data is not there.
Adopt also all the other places and other variables

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ok thanks sounds good. Fixed in both places as well as app_config.get(DATA_DELETED_IDS) in __init__.py

Comment on lines +263 to +269
# Token expired — remove it lazily.
device_tokens.pop(tag, None)
if not device_tokens:
live_activity_tokens.pop(webhook_id, None)
await self.hass.data[DOMAIN][DATA_STORE].async_save(
savable_state(self.hass)
)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Comment is not matching the code as you remove it immediately.
We probably can omit it here as we will have a clean up task. So remvoing is lazily

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Thanks for catching that. I removed the eager cleanup and now the scheduled task handles it. Notification path just skips the expired token and falls through to push-to-start

@home-assistant home-assistant Bot marked this pull request as draft May 19, 2026 13:55
rwarner and others added 7 commits May 19, 2026 10:32
Tokens should survive unload (restart/reload) so they are available
when the entry loads again. Remove them only in async_remove_entry,
when the device is permanently deleted.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Subclass Store to provide a migration function that adds the
live_activity_tokens key when upgrading from v1. Removes the ad-hoc
elif fallback that was filling in the missing key after load.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace nested dict comprehension with a plain loop so the expiry
check appears only once.

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

If expired tokens are found during setup, persist the cleaned state
immediately. Track the earliest expiry among valid tokens and schedule
a single cleanup task that removes expired tokens, saves, and
reschedules itself for the next expiry.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Replace .get() with direct key access on hass.data[DOMAIN] and
app_config where setup and migration guarantee the keys are present.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
The scheduled cleanup task handles expired live activity token removal.
The notification path now just skips the expired token and falls through
to push-to-start without touching the store.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Resolve conflict in savable_state: keep DATA_LIVE_ACTIVITY_TOKENS key
alongside DATA_DELETED_IDS and adopt updated pylint disable comment format.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@rwarner
Copy link
Copy Markdown
Author

rwarner commented May 19, 2026

Pushed up all suggested changes from @edenhaus in the most recent review. I left them marked as unresolved to not lose track of anything until @edenhaus is satisfied with it.

I also fixed the merge conflict and was sure to not to rebase this time :)

cc: @bgoncal

Marking as ready for review once again 🚂

@rwarner rwarner marked this pull request as ready for review May 19, 2026 15:10
@home-assistant home-assistant Bot requested a review from edenhaus May 19, 2026 15:10
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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.

Live updates - main feature

4 participants