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 <nick@koston.org>

* Update homeassistant/components/withings/__init__.py

Co-authored-by: J. Nick Koston <nick@koston.org>

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Joost Lekkerkerker 2023-10-11 00:06:42 +02:00 committed by GitHub
parent 9b785ef766
commit ffb752c804
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 64 additions and 58 deletions

View File

@ -4,8 +4,10 @@ For more details about this platform, please refer to the documentation at
""" """
from __future__ import annotations from __future__ import annotations
import asyncio
from collections.abc import Awaitable, Callable from collections.abc import Awaitable, Callable
import contextlib import contextlib
from datetime import timedelta
from typing import Any from typing import Any
from aiohttp.hdrs import METH_HEAD, METH_POST from aiohttp.hdrs import METH_HEAD, METH_POST
@ -78,6 +80,8 @@ CONFIG_SCHEMA = vol.Schema(
}, },
extra=vol.ALLOW_EXTRA, extra=vol.ALLOW_EXTRA,
) )
SUBSCRIBE_DELAY = timedelta(seconds=5)
UNSUBSCRIBE_DELAY = timedelta(seconds=1)
async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool: async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
@ -141,7 +145,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
) -> None: ) -> None:
LOGGER.debug("Unregister Withings webhook (%s)", entry.data[CONF_WEBHOOK_ID]) LOGGER.debug("Unregister Withings webhook (%s)", entry.data[CONF_WEBHOOK_ID])
webhook_unregister(hass, 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( async def register_webhook(
_: Any, _: Any,
@ -170,7 +175,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool:
get_webhook_handler(coordinator), 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) LOGGER.debug("Register Withings webhook: %s", webhook_url)
entry.async_on_unload( entry.async_on_unload(
hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, unregister_webhook) 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) 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: async def async_cloudhook_generate_url(hass: HomeAssistant, entry: ConfigEntry) -> str:
"""Generate the full URL for a webhook_id.""" """Generate the full URL for a webhook_id."""
if CONF_CLOUDHOOK_URL not in entry.data: if CONF_CLOUDHOOK_URL not in entry.data:

View File

@ -1,5 +1,4 @@
"""Withings coordinator.""" """Withings coordinator."""
import asyncio
from collections.abc import Callable from collections.abc import Callable
from datetime import timedelta from datetime import timedelta
from typing import Any from typing import Any
@ -24,9 +23,6 @@ from homeassistant.util import dt as dt_util
from .api import ConfigEntryWithingsApi from .api import ConfigEntryWithingsApi
from .const import LOGGER, Measurement from .const import LOGGER, Measurement
SUBSCRIBE_DELAY = timedelta(seconds=5)
UNSUBSCRIBE_DELAY = timedelta(seconds=1)
WITHINGS_MEASURE_TYPE_MAP: dict[ WITHINGS_MEASURE_TYPE_MAP: dict[
NotifyAppli | GetSleepSummaryField | MeasureType, Measurement NotifyAppli | GetSleepSummaryField | MeasureType, Measurement
] = { ] = {
@ -84,55 +80,12 @@ class WithingsDataUpdateCoordinator(DataUpdateCoordinator[dict[Measurement, Any]
super().__init__(hass, LOGGER, name="Withings", update_interval=UPDATE_INTERVAL) super().__init__(hass, LOGGER, name="Withings", update_interval=UPDATE_INTERVAL)
self._client = client self._client = client
async def async_subscribe_webhooks(self, webhook_url: str) -> None: def webhook_subscription_listener(self, connected: bool) -> None:
"""Subscribe to webhooks.""" """Call when webhook status changed."""
await self.async_unsubscribe_webhooks() if connected:
self.update_interval = None
current_webhooks = await self._client.async_notify_list() else:
self.update_interval = UPDATE_INTERVAL
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
async def _async_update_data(self) -> dict[Measurement, Any]: async def _async_update_data(self) -> dict[Measurement, Any]:
try: try:

View File

@ -160,10 +160,10 @@ def disable_webhook_delay():
mock = AsyncMock() mock = AsyncMock()
with patch( with patch(
"homeassistant.components.withings.coordinator.SUBSCRIBE_DELAY", "homeassistant.components.withings.SUBSCRIBE_DELAY",
timedelta(seconds=0), timedelta(seconds=0),
), patch( ), patch(
"homeassistant.components.withings.coordinator.UNSUBSCRIBE_DELAY", "homeassistant.components.withings.UNSUBSCRIBE_DELAY",
timedelta(seconds=0), timedelta(seconds=0),
): ):
yield mock yield mock

View File

@ -126,7 +126,7 @@ async def test_data_manager_webhook_subscription(
async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1)) async_fire_time_changed(hass, dt_util.utcnow() + timedelta(seconds=1))
await hass.async_block_till_done() 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" webhook_url = "https://example.local:8123/api/webhook/55a7335ea8dee830eed4ef8f84cda8f6d80b83af0847dc74032e86120bffed5e"