Add live activity token retention, notify usage and clean up cycle to mobile_app#172928
Add live activity token retention, notify usage and clean up cycle to mobile_app#172928bgoncal wants to merge 71 commits into
Conversation
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>
| @@ -0,0 +1,87 @@ | |||
| """Live Activity webhook handlers.""" | |||
There was a problem hiding this comment.
Please move the websocket commands to the websocket class
There was a problem hiding this comment.
Webhook you mean? Why can't we separate based on feature? Otherwise we will have a "monster class" one day
| """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) |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Do you see a way to differentiate while keeping code separation in mind? Open for ideas
|
Please take a look at the requested changes, and use the Ready for review button when you are done, thanks 👍 |
Co-authored-by: Robert Resch <robert@resch.dev>
Co-authored-by: Robert Resch <robert@resch.dev>
|
Re-tested for local push and firebase, in the current state both work ✅ |
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
Additional information
Checklist
ruff format homeassistant tests)If user exposed functionality or configuration variables are added/changed:
If the code communicates with devices, web services, or third-party tools:
Updated and included derived files by running:
python3 -m script.hassfest.requirements_all.txt.Updated by running
python3 -m script.gen_requirements_all.To help with the load of incoming pull requests: