Skip to content

Add live activity token retention, notify usage and clean up cycle to mobile_app#172928

Draft
bgoncal wants to merge 71 commits into
home-assistant:devfrom
bgoncal:live-activity-last-mile
Draft

Add live activity token retention, notify usage and clean up cycle to mobile_app#172928
bgoncal wants to merge 71 commits into
home-assistant:devfrom
bgoncal:live-activity-last-mile

Conversation

@bgoncal
Copy link
Copy Markdown
Member

@bgoncal bgoncal commented Jun 3, 2026

Breaking change

Proposed change

This PR takes over #166072

This PR makes it possible to store live activity tokens and retain in mobile_app so Home Assistant is able to initiate live activities in iOS devices.

This PR also has a clean up cycle for those tokens and an endpoint to expire them in case they are not needed (ended by the user)

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

Checklist

  • I understand the code I am submitting and can explain how it works.
  • The code change is tested and works locally.
  • Local tests pass. Your PR cannot be merged unless tests pass
  • There is no commented out code in this PR.
  • I have followed the development checklist
  • I have followed the perfect PR recommendations
  • The code has been formatted using Ruff (ruff format homeassistant tests)
  • Tests have been added to verify that the new code works.
  • Any generated code has been carefully reviewed for correctness and compliance with project standards.

If user exposed functionality or configuration variables are added/changed:

If the code communicates with devices, web services, or third-party tools:

  • The manifest file has all fields filled out correctly.
    Updated and included derived files by running: python3 -m script.hassfest.
  • New or updated dependencies have been added to requirements_all.txt.
    Updated by running python3 -m script.gen_requirements_all.
  • For the updated dependencies a diff between library versions and ideally a link to the changelog/release notes is added to the PR description.

To help with the load of incoming pull requests:

rwarner and others added 30 commits April 29, 2026 13:12
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>
- 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>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…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>
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>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
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>
Copilot AI review requested due to automatic review settings June 3, 2026 15:57
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 9 out of 9 changed files in this pull request and generated 4 comments.

Comment thread tests/components/mobile_app/test_init.py
Comment thread tests/components/mobile_app/test_init.py
Comment thread homeassistant/components/mobile_app/live_activity/__init__.py Outdated
Comment thread homeassistant/components/mobile_app/__init__.py
Comment thread homeassistant/components/mobile_app/notify.py Outdated
Copilot AI review requested due to automatic review settings June 4, 2026 09:32
@bgoncal bgoncal marked this pull request as ready for review June 4, 2026 11:46
@bgoncal bgoncal requested a review from a team as a code owner June 4, 2026 11:46
@@ -0,0 +1,87 @@
"""Live Activity webhook handlers."""
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.

Please move the websocket commands to the websocket class

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Webhook you mean? Why can't we separate based on feature? Otherwise we will have a "monster class" one day

Comment thread homeassistant/components/mobile_app/const.py Outdated
"""Send a message to a target."""
# Applies Apple ActivityKit Live Activity routing when the payload asks
# for it; otherwise it returns the original generic mobile push data.
remote_push = prepare_live_activity_remote_push(self.hass, entry.data, data)
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.

We are calling this function always not not just for apple devices. It make no sense to put this function in a live activity file if we are even calling it always.
Live activity stuff should just be called when needed

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Do you see a way to differentiate while keeping code separation in mind? Open for ideas

Comment thread homeassistant/components/mobile_app/live_activity/__init__.py Outdated
@home-assistant home-assistant Bot marked this pull request as draft June 4, 2026 14:05
@home-assistant
Copy link
Copy Markdown
Contributor

home-assistant Bot commented Jun 4, 2026

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.

bgoncal and others added 3 commits June 4, 2026 16:11
Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: Robert Resch <robert@resch.dev>
@bgoncal
Copy link
Copy Markdown
Member Author

bgoncal commented Jun 4, 2026

Re-tested for local push and firebase, in the current state both work ✅

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