-
-
Notifications
You must be signed in to change notification settings - Fork 37.4k
Add iOS Live Activity webhook handlers to mobile_app #166072
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: dev
Are you sure you want to change the base?
Changes from 22 commits
7d4713a
e377da7
14a6987
3d7ea81
3f6346d
d1163a5
d299519
b76e405
d9df34f
ecbb296
a16c8c9
336c64b
023065f
23ff061
61a609b
d5e8477
1337547
df217bd
9d9ef58
a1a6db3
d44727e
eb478d4
978d802
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -32,9 +32,24 @@ | |
| ATTR_NO_LEGACY_ENCRYPTION = "no_legacy_encryption" | ||
| ATTR_OS_NAME = "os_name" | ||
| ATTR_OS_VERSION = "os_version" | ||
| ATTR_PUSH_TAG = "tag" | ||
| ATTR_PUSH_WEBSOCKET_CHANNEL = "push_websocket_channel" | ||
| ATTR_PUSH_TOKEN = "push_token" | ||
| ATTR_PUSH_URL = "push_url" | ||
|
|
||
| ATTR_LIVE_UPDATE = "live_update" | ||
| ATTR_LIVE_ACTIVITY_TOKEN = "live_activity_token" | ||
| ATTR_LIVE_ACTIVITY_PUSH_TO_START_TOKEN = "live_activity_push_to_start_token" | ||
| ATTR_LIVE_ACTIVITY_PUSH_TO_START_APNS_ENVIRONMENT = ( | ||
| "live_activity_push_to_start_apns_environment" | ||
| ) | ||
| # Tag identifying a specific Live Activity instance in the iOS companion app webhooks. | ||
| ATTR_LIVE_ACTIVITY_TAG = "live_activity_tag" | ||
|
|
||
| # In-memory store for per-device Live Activity push tokens, keyed by webhook_id → live_activity_tag. | ||
| # Populated by mobile_app_live_activity_token and cleared by mobile_app_live_activity_dismissed webhooks. | ||
| DATA_LIVE_ACTIVITY_TOKENS = "live_activity_tokens" | ||
|
|
||
| ATTR_PUSH_RATE_LIMITS = "rateLimits" | ||
| ATTR_PUSH_RATE_LIMITS_ERRORS = "errors" | ||
| ATTR_PUSH_RATE_LIMITS_MAXIMUM = "maximum" | ||
|
|
@@ -92,6 +107,15 @@ | |
| # Set to True to indicate that this registration will connect via websocket channel | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. keep this PR as it is not related to the PR |
||
| # to receive push notifications. | ||
| vol.Optional(ATTR_PUSH_WEBSOCKET_CHANNEL): cv.boolean, | ||
| # Push-to-start token and environment must be provided together — a token | ||
| # without an environment is ambiguous (sandbox tokens fail on production). | ||
| vol.Inclusive( | ||
| ATTR_LIVE_ACTIVITY_PUSH_TO_START_TOKEN, "live_activity_push_to_start" | ||
| ): vol.All(cv.string, vol.Length(min=1)), | ||
| vol.Inclusive( | ||
| ATTR_LIVE_ACTIVITY_PUSH_TO_START_APNS_ENVIRONMENT, | ||
| "live_activity_push_to_start", | ||
| ): vol.In(["sandbox", "production"]), | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Also this one is not used in core
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Yes pls. Anything what is not needed should be removed. If we later need it we can always add it in the PR, where we need it. Please also update the iOS app, so we don't send data what we don't actually need. Not used data is useless but still uses some resources, which can be avoided.
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done, removed both fields from
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was messed up from my rebase, fix has been re-applied. Leaving unresolved until confirmed (it's hard to find these resolved discussion when they're marked resolved) |
||
| }, | ||
| extra=vol.ALLOW_EXTRA, | ||
| ) | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -67,10 +67,12 @@ | |
| ATTR_DEVICE_NAME, | ||
| ATTR_EVENT_DATA, | ||
| ATTR_EVENT_TYPE, | ||
| ATTR_LIVE_ACTIVITY_TAG, | ||
| ATTR_MANUFACTURER, | ||
| ATTR_MODEL, | ||
| ATTR_NO_LEGACY_ENCRYPTION, | ||
| ATTR_OS_VERSION, | ||
| ATTR_PUSH_TOKEN, | ||
| ATTR_SENSOR_ATTRIBUTES, | ||
| ATTR_SENSOR_DEVICE_CLASS, | ||
| ATTR_SENSOR_DISABLED, | ||
|
|
@@ -99,6 +101,7 @@ | |
| DATA_CONFIG_ENTRIES, | ||
| DATA_DELETED_IDS, | ||
| DATA_DEVICES, | ||
| DATA_LIVE_ACTIVITY_TOKENS, | ||
| DATA_PENDING_UPDATES, | ||
| DOMAIN, | ||
| ERR_ENCRYPTION_ALREADY_ENABLED, | ||
|
|
@@ -798,3 +801,60 @@ async def webhook_scan_tag( | |
| registration_context(config_entry.data), | ||
| ) | ||
| return empty_okay_response() | ||
|
|
||
|
|
||
| @WEBHOOK_COMMANDS.register("live_activity_token") | ||
| @validate_schema( | ||
| { | ||
| vol.Required(ATTR_LIVE_ACTIVITY_TAG): cv.string, | ||
|
rwarner marked this conversation as resolved.
|
||
| vol.Required(ATTR_PUSH_TOKEN): cv.string, | ||
|
rwarner marked this conversation as resolved.
|
||
| } | ||
| ) | ||
| async def webhook_update_live_activity_token( | ||
| hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, Any] | ||
| ) -> Response: | ||
| """Handle a Live Activity token update from the iOS companion app. | ||
|
|
||
| When the iOS app creates a Live Activity locally, ActivityKit provides | ||
| a per-activity APNs push token. The app sends this token so HA can | ||
| later include it as live_activity_token in the push relay request. | ||
| The relay server places it in the FCM message's apns.liveActivityToken | ||
| field, and FCM handles APNs delivery automatically. | ||
| """ | ||
| webhook_id = config_entry.data[CONF_WEBHOOK_ID] | ||
| activity_tag = data[ATTR_LIVE_ACTIVITY_TAG] | ||
|
|
||
| live_activity_tokens = hass.data[DOMAIN][DATA_LIVE_ACTIVITY_TOKENS] | ||
| live_activity_tokens.setdefault(webhook_id, {})[activity_tag] = { | ||
| ATTR_PUSH_TOKEN: data[ATTR_PUSH_TOKEN] | ||
| } | ||
|
rwarner marked this conversation as resolved.
rwarner marked this conversation as resolved.
|
||
|
|
||
| return empty_okay_response() | ||
|
|
||
|
|
||
| @WEBHOOK_COMMANDS.register("live_activity_dismissed") | ||
| @validate_schema( | ||
| { | ||
| vol.Required(ATTR_LIVE_ACTIVITY_TAG): cv.string, | ||
|
rwarner marked this conversation as resolved.
|
||
| } | ||
| ) | ||
| async def webhook_live_activity_dismissed( | ||
| hass: HomeAssistant, config_entry: ConfigEntry, data: dict[str, str] | ||
| ) -> Response: | ||
| """Handle a Live Activity dismissal from the iOS companion app. | ||
|
|
||
| When a Live Activity ends on the device (user dismissal, expiration, | ||
| or an explicit end event), the app notifies HA so the stored push | ||
| token for that activity can be cleaned up. | ||
| """ | ||
| webhook_id = config_entry.data[CONF_WEBHOOK_ID] | ||
| activity_tag = data[ATTR_LIVE_ACTIVITY_TAG] | ||
|
|
||
| live_activity_tokens = hass.data[DOMAIN][DATA_LIVE_ACTIVITY_TOKENS] | ||
| if webhook_id in live_activity_tokens: | ||
| live_activity_tokens[webhook_id].pop(activity_tag, None) | ||
| # Clean up the device key if no activities remain. | ||
| if not live_activity_tokens[webhook_id]: | ||
| del live_activity_tokens[webhook_id] | ||
|
Comment on lines
+843
to
+846
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Can the dismissed be called multiple time for a activitiy tag?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Dismissed is only called once per tag in normal flow, but since
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. So just double checking... And if it is correct, is there a technical limitation, why we don't allow live activity to be survive a restart?
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, that's correct. HA restart loses all in-memory Live Activity tokens. The existing activity continues showing the last known state on the device (iOS keeps it alive for up to 8 hours), but HA can no longer send updates to it. The iOS app does handle recovery automatically in one case: if the iOS companion app is also restarted (or even just brought to foreground after reconnecting), it calls reattach() at launch, re-observes Activity.pushTokenUpdates, and immediately re-sends the current token to HA. So a full device/app restart recovers cleanly. The specific gap is HA restarting while the iOS app stays running. The app has no trigger to proactively re-push existing tokens on HA reconnect. We could persist tokens to the config entry, but a stored token from before the restart may have rotated in the interim, and pushing to a stale token returns BadDeviceToken from APN, which we don't currently handle. For now this is a known limitation. A future improvement could be a "request token re-registration" webhook that HA calls on startup to prompt the iOS app to re-send all active tokens. Open to suggestions or concerns, or marking down the code with a comment about this
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Why aren't we handling it as it could always happen that a token is invalid? We could just simply use a store to store, together with the actual time, so we know after 8h, maybe for safety use a longer timeframe, we can clean them up The comments sound like written directly by AI, which is not allowed. Please write them in your own words. Using AI for help is fine but the human must stay in the loop
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Sorry, yes that was used in that instance because I thought it would do a better job explaining the overall scope. My apologies.
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We should store the token so there feature survives a restart. A dict is fine, the token as key and the time as value maybe. Okay we leave the bad token for a follow up PR. |
||
|
|
||
| return empty_okay_response() | ||
Uh oh!
There was an error while loading. Please reload this page.