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
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:

View File

@ -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:

View File

@ -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

View File

@ -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"