-
-
Notifications
You must be signed in to change notification settings - Fork 37.6k
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
Changes from 51 commits
7d4713a
e377da7
14a6987
3d7ea81
3f6346d
d1163a5
d299519
b76e405
d9df34f
ecbb296
a16c8c9
336c64b
023065f
23ff061
61a609b
d5e8477
1337547
df217bd
9d9ef58
a1a6db3
d44727e
eb478d4
978d802
1437794
25340ac
883f1f8
b0cb713
16fde1c
83ee21a
e5ae0fa
983ed24
0ec8f4e
0f092eb
917a4fc
8c85dbf
fccb524
bbf2bfe
8b41416
0eea88e
c4f85ec
0d00b7f
4926cec
78b5f66
1c98aeb
3660474
3afe179
4227077
94b4a8d
334f24b
3097854
135cd18
a7f2052
b7ffeac
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 | ||||
|---|---|---|---|---|---|---|
|
|
@@ -8,6 +8,11 @@ | |||||
|
|
||||||
| STORAGE_KEY = DOMAIN | ||||||
| STORAGE_VERSION = 1 | ||||||
| STORAGE_VERSION_MINOR = 2 | ||||||
| STORAGE_SAVE_DELAY = 10 | ||||||
|
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.
Suggested change
I suppose?
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. Good call updated and fixed references |
||||||
|
|
||||||
| # A Live Activity can be active for up to eight hours unless its app or a person ends it before this limit. https://developer.apple.com/documentation/activitykit/displaying-live-data-with-live-activities#Understand-constraints | ||||||
| LIVE_ACTIVITY_TOKEN_TTL_SECONDS = 8 * 3600 | ||||||
|
edenhaus marked this conversation as resolved.
|
||||||
|
|
||||||
| CONF_CLOUDHOOK_URL = "cloudhook_url" | ||||||
| CONF_REMOTE_UI_URL = "remote_ui_url" | ||||||
|
|
@@ -17,6 +22,7 @@ | |||||
| DATA_CONFIG_ENTRIES = "config_entries" | ||||||
| DATA_DELETED_IDS = "deleted_ids" | ||||||
| DATA_DEVICES = "devices" | ||||||
| DATA_LIVE_ACTIVITY_TOKENS = "live_activity_tokens" | ||||||
|
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 we make this specific to iOS in the name to avoid any confusion? Android don't uses this, same for the other new ATTRs.
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 good idea, I will hold off on this until there's an architectural decision with the whole platform specific code going into this. If this is going into a |
||||||
| DATA_STORE = "store" | ||||||
| DATA_NOTIFY = "notify" | ||||||
| DATA_PUSH_CHANNEL = "push_channel" | ||||||
|
|
@@ -40,6 +46,15 @@ | |||||
| ATTR_PUSH_RATE_LIMITS_SUCCESSFUL = "successful" | ||||||
| ATTR_SUPPORTS_ENCRYPTION = "supports_encryption" | ||||||
|
|
||||||
| ATTR_LIVE_UPDATE = "live_update" | ||||||
| ATTR_LIVE_ACTIVITY_TOKEN = "live_activity_token" | ||||||
| ATTR_TAG = "tag" | ||||||
|
|
||||||
| # When a notification with this message arrives with a tag matching a stored | ||||||
| # Live Activity token, end the activity remotely instead of letting it fall | ||||||
| # through to a regular clear_notification banner. | ||||||
| LIVE_ACTIVITY_CLEAR_MESSAGE = "clear_notification" | ||||||
|
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. I'm open to a better name if you feel it's needed. It's iOS-specific. Android dismisses live notifications by tag natively, but iOS ActivityKit needs an explicit event=end push to the activity's token, so this string is what triggers core to attach that token. Happy to rename if
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. Command is not exclusive for live activity and therefore should not prefixed with it.
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. Makes sense, renamed to
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. @tr4nt0r just pinging you in case you are interested to look at this PR that does change a bit the notifications. |
||||||
|
|
||||||
| ATTR_EVENT_DATA = "event_data" | ||||||
| ATTR_EVENT_TYPE = "event_type" | ||||||
|
|
||||||
|
|
@@ -92,6 +107,7 @@ | |||||
| # will connect via websocket channel to receive | ||||||
| # push notifications. | ||||||
| vol.Optional(ATTR_PUSH_WEBSOCKET_CHANNEL): cv.boolean, | ||||||
| vol.Optional(ATTR_LIVE_ACTIVITY_TOKEN): cv.string, | ||||||
| }, | ||||||
| extra=vol.ALLOW_EXTRA, | ||||||
| ) | ||||||
|
|
||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
| @@ -0,0 +1,64 @@ | ||||||
| """Live Activity push token lifecycle: expiry-driven cleanup loop.""" | ||||||
|
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. Add a mention that it is only for iOS.
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. @edenhaus Is it a "valid" pattern to actually move specific code for iOS into a dedicated python module like
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. Will hold on this until we make that architectural decision |
||||||
| # pylint: disable=home-assistant-use-runtime-data # Uses legacy hass.data[DOMAIN] pattern | ||||||
|
|
||||||
| from datetime import datetime | ||||||
|
|
||||||
| from homeassistant.core import HomeAssistant, callback | ||||||
| from homeassistant.helpers.event import async_call_later | ||||||
| from homeassistant.util import dt as dt_util | ||||||
|
|
||||||
| from .const import DATA_LIVE_ACTIVITY_TOKENS, DATA_STORE, DOMAIN | ||||||
| from .helpers import savable_state | ||||||
|
|
||||||
|
|
||||||
| @callback | ||||||
| def async_schedule_next_cleanup(hass: HomeAssistant) -> None: | ||||||
| """Schedule a sweep for the earliest token expiry. | ||||||
|
|
||||||
| Only call when no sweep is already in flight. The invariant is: tokens | ||||||
| non-empty ⟹ sweep scheduled. So the two safe call sites are (1) the | ||||||
| webhook that added the first token (tokens was empty beforehand) and | ||||||
| (2) the tail of a sweep that found surviving tokens (its own timer | ||||||
| just fired). | ||||||
| """ | ||||||
| tokens = hass.data[DOMAIN][DATA_LIVE_ACTIVITY_TOKENS] | ||||||
| earliest_expires_at = min( | ||||||
| ( | ||||||
| token["expires_at"] | ||||||
|
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. Wouldit make sense to extract "expires_at" into a const? It is used in multiple places.
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. Good idea, introduced |
||||||
| for device_tokens in tokens.values() | ||||||
| for token in device_tokens.values() | ||||||
| ), | ||||||
| default=None, | ||||||
| ) | ||||||
| if earliest_expires_at is None: | ||||||
| return | ||||||
|
|
||||||
| delay = earliest_expires_at - dt_util.utcnow().timestamp() | ||||||
|
|
||||||
| async def run_cleanup(_now: datetime) -> None: | ||||||
| await async_cleanup_expired_tokens(hass) | ||||||
|
|
||||||
| async_call_later(hass, delay, run_cleanup) | ||||||
|
|
||||||
|
|
||||||
| async def async_cleanup_expired_tokens(hass: HomeAssistant) -> None: | ||||||
|
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. Shouldn't this be
Suggested change
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. I'm not a big python guy so I'm not sure what the proper format should be. It looks like because both functions are called from I would defer to @edenhaus I might have my understanding mixed up |
||||||
| """Sweep expired tokens, keep the loop alive if any remain, save changes.""" | ||||||
|
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. I'm not sure to understand what is the loop in this context. Maybe because of my lack of context, but maybe it means that we are lacking some documentation.
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. You need a token to update a live activity, these tokens may expire, so
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. +1 to @bgoncal's explanation. I expanded the function's comment with the specific "what's the loop" detail. |
||||||
| now = dt_util.utcnow().timestamp() | ||||||
| tokens = hass.data[DOMAIN][DATA_LIVE_ACTIVITY_TOKENS] | ||||||
| changed = False | ||||||
|
|
||||||
| for webhook_id in list(tokens): | ||||||
| device_tokens = tokens[webhook_id] | ||||||
| for tag, data in list(device_tokens.items()): | ||||||
| if data["expires_at"] <= now: | ||||||
| del device_tokens[tag] | ||||||
| changed = True | ||||||
| if not device_tokens: | ||||||
| del tokens[webhook_id] | ||||||
| changed = True | ||||||
|
|
||||||
| if tokens: | ||||||
| async_schedule_next_cleanup(hass) | ||||||
|
|
||||||
| if changed: | ||||||
| await hass.data[DOMAIN][DATA_STORE].async_save(savable_state(hass)) | ||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This could grow further we add migrations, I would advise to move this into it's own file? Not sure if it's the usual way to do in Python.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
True, will defer to @edenhaus