From ffb752c8040d573cb630536f1e02946d04a47f9f Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Wed, 11 Oct 2023 00:06:42 +0200 Subject: [PATCH] Subscribe to Withings webhooks outside of coordinator (#101759) * Subscribe to Withings webhooks outside of coordinator * Subscribe to Withings webhooks outside of coordinator * Update homeassistant/components/withings/__init__.py Co-authored-by: J. Nick Koston * Update homeassistant/components/withings/__init__.py Co-authored-by: J. Nick Koston --------- Co-authored-by: J. Nick Koston --- homeassistant/components/withings/__init__.py | 57 +++++++++++++++++- .../components/withings/coordinator.py | 59 ++----------------- tests/components/withings/conftest.py | 4 +- tests/components/withings/test_init.py | 2 +- 4 files changed, 64 insertions(+), 58 deletions(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 810ad49171c..16606a40645 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -4,8 +4,10 @@ For more details about this platform, please refer to the documentation at """ from __future__ import annotations +import asyncio from collections.abc import Awaitable, Callable import contextlib +from datetime import timedelta from typing import Any from aiohttp.hdrs import METH_HEAD, METH_POST @@ -78,6 +80,8 @@ CONFIG_SCHEMA = vol.Schema( }, extra=vol.ALLOW_EXTRA, ) +SUBSCRIBE_DELAY = timedelta(seconds=5) +UNSUBSCRIBE_DELAY = timedelta(seconds=1) async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: @@ -141,7 +145,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: ) -> None: LOGGER.debug("Unregister Withings webhook (%s)", entry.data[CONF_WEBHOOK_ID]) webhook_unregister(hass, entry.data[CONF_WEBHOOK_ID]) - await hass.data[DOMAIN][entry.entry_id].async_unsubscribe_webhooks() + await async_unsubscribe_webhooks(client) + coordinator.webhook_subscription_listener(False) async def register_webhook( _: Any, @@ -170,7 +175,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: get_webhook_handler(coordinator), ) - await hass.data[DOMAIN][entry.entry_id].async_subscribe_webhooks(webhook_url) + await async_subscribe_webhooks(client, webhook_url) + coordinator.webhook_subscription_listener(True) LOGGER.debug("Register Withings webhook: %s", webhook_url) entry.async_on_unload( hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) @@ -213,6 +219,53 @@ async def update_listener(hass: HomeAssistant, entry: ConfigEntry) -> None: await hass.config_entries.async_reload(entry.entry_id) +async def async_subscribe_webhooks( + client: ConfigEntryWithingsApi, webhook_url: str +) -> None: + """Subscribe to Withings webhooks.""" + await async_unsubscribe_webhooks(client) + + notification_to_subscribe = { + NotifyAppli.WEIGHT, + NotifyAppli.CIRCULATORY, + NotifyAppli.ACTIVITY, + NotifyAppli.SLEEP, + NotifyAppli.BED_IN, + NotifyAppli.BED_OUT, + } + + for notification in notification_to_subscribe: + LOGGER.debug( + "Subscribing %s for %s in %s seconds", + webhook_url, + notification, + SUBSCRIBE_DELAY.total_seconds(), + ) + # Withings will HTTP HEAD the callback_url and needs some downtime + # between each call or there is a higher chance of failure. + await asyncio.sleep(SUBSCRIBE_DELAY.total_seconds()) + await client.async_notify_subscribe(webhook_url, notification) + + +async def async_unsubscribe_webhooks(client: ConfigEntryWithingsApi) -> None: + """Unsubscribe to all Withings webhooks.""" + current_webhooks = await client.async_notify_list() + + for webhook_configuration in current_webhooks.profiles: + LOGGER.debug( + "Unsubscribing %s for %s in %s seconds", + webhook_configuration.callbackurl, + webhook_configuration.appli, + UNSUBSCRIBE_DELAY.total_seconds(), + ) + # Quick calls to Withings can result in the service returning errors. + # Give them some time to cool down. + await asyncio.sleep(UNSUBSCRIBE_DELAY.total_seconds()) + await client.async_notify_revoke( + webhook_configuration.callbackurl, webhook_configuration.appli + ) + + async def async_cloudhook_generate_url(hass: HomeAssistant, entry: ConfigEntry) -> str: """Generate the full URL for a webhook_id.""" if CONF_CLOUDHOOK_URL not in entry.data: diff --git a/homeassistant/components/withings/coordinator.py b/homeassistant/components/withings/coordinator.py index 128d4e39193..2ec2804814b 100644 --- a/homeassistant/components/withings/coordinator.py +++ b/homeassistant/components/withings/coordinator.py @@ -1,5 +1,4 @@ """Withings coordinator.""" -import asyncio from collections.abc import Callable from datetime import timedelta from typing import Any @@ -24,9 +23,6 @@ from homeassistant.util import dt as dt_util from .api import ConfigEntryWithingsApi from .const import LOGGER, Measurement -SUBSCRIBE_DELAY = timedelta(seconds=5) -UNSUBSCRIBE_DELAY = timedelta(seconds=1) - WITHINGS_MEASURE_TYPE_MAP: dict[ NotifyAppli | GetSleepSummaryField | MeasureType, Measurement ] = { @@ -84,55 +80,12 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[dict[Measurement, Any] super().__init__(hass, LOGGER, name="Withings", update_interval=UPDATE_INTERVAL) self._client = client - async def async_subscribe_webhooks(self, webhook_url: str) -> None: - """Subscribe to webhooks.""" - await self.async_unsubscribe_webhooks() - - current_webhooks = await self._client.async_notify_list() - - subscribed_notifications = frozenset( - profile.appli - for profile in current_webhooks.profiles - if profile.callbackurl == webhook_url - ) - - notification_to_subscribe = ( - set(NotifyAppli) - - subscribed_notifications - - {NotifyAppli.USER, NotifyAppli.UNKNOWN} - ) - - for notification in notification_to_subscribe: - LOGGER.debug( - "Subscribing %s for %s in %s seconds", - webhook_url, - notification, - SUBSCRIBE_DELAY.total_seconds(), - ) - # Withings will HTTP HEAD the callback_url and needs some downtime - # between each call or there is a higher chance of failure. - await asyncio.sleep(SUBSCRIBE_DELAY.total_seconds()) - await self._client.async_notify_subscribe(webhook_url, notification) - self.update_interval = None - - async def async_unsubscribe_webhooks(self) -> None: - """Unsubscribe to webhooks.""" - current_webhooks = await self._client.async_notify_list() - - for webhook_configuration in current_webhooks.profiles: - LOGGER.debug( - "Unsubscribing %s for %s in %s seconds", - webhook_configuration.callbackurl, - webhook_configuration.appli, - UNSUBSCRIBE_DELAY.total_seconds(), - ) - # Quick calls to Withings can result in the service returning errors. - # Give them some time to cool down. - await asyncio.sleep(UNSUBSCRIBE_DELAY.total_seconds()) - await self._client.async_notify_revoke( - webhook_configuration.callbackurl, webhook_configuration.appli - ) - self.update_interval = UPDATE_INTERVAL + def webhook_subscription_listener(self, connected: bool) -> None: + """Call when webhook status changed.""" + if connected: + self.update_interval = None + else: + self.update_interval = UPDATE_INTERVAL async def _async_update_data(self) -> dict[Measurement, Any]: try: diff --git a/tests/components/withings/conftest.py b/tests/components/withings/conftest.py index 3fc2a3c6461..ad310639b43 100644 --- a/tests/components/withings/conftest.py +++ b/tests/components/withings/conftest.py @@ -160,10 +160,10 @@ def disable_webhook_delay(): mock = AsyncMock() with patch( - "homeassistant.components.withings.coordinator.SUBSCRIBE_DELAY", + "homeassistant.components.withings.SUBSCRIBE_DELAY", timedelta(seconds=0), ), patch( - "homeassistant.components.withings.coordinator.UNSUBSCRIBE_DELAY", + "homeassistant.components.withings.UNSUBSCRIBE_DELAY", timedelta(seconds=0), ): yield mock diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index dd112671945..ab83bbcfb36 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -126,7 +126,7 @@ async def test_data_manager_webhook_subscription( async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1)) await hass.async_block_till_done() - assert withings.async_notify_subscribe.call_count == 4 + assert withings.async_notify_subscribe.call_count == 6 webhook_url = "https://example.local:8123/api/webhook/55a7335ea8dee830eed4ef8f84cda8f6d80b83af0847dc74032e86120bffed5e"