Skip to content

Commit

Permalink
Subscribe to Withings webhooks outside of coordinator (home-assistant…
Browse files Browse the repository at this point in the history
…#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 <[email protected]>

* Update homeassistant/components/withings/__init__.py

Co-authored-by: J. Nick Koston <[email protected]>

---------

Co-authored-by: J. Nick Koston <[email protected]>
  • Loading branch information
joostlek and bdraco authored Oct 10, 2023
1 parent 9b785ef commit ffb752c
Show file tree
Hide file tree
Showing 4 changed files with 64 additions and 58 deletions.
57 changes: 55 additions & 2 deletions homeassistant/components/withings/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,10 @@
"""
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
Expand Down Expand Up @@ -78,6 +80,8 @@
},
extra=vol.ALLOW_EXTRA,
)
SUBSCRIBE_DELAY = timedelta(seconds=5)
UNSUBSCRIBE_DELAY = timedelta(seconds=1)


async def async_setup(hass: HomeAssistant, config: ConfigType) -> bool:
Expand Down Expand Up @@ -141,7 +145,8 @@ async def unregister_webhook(
) -> 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,
Expand Down Expand Up @@ -170,7 +175,8 @@ async def register_webhook(
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)
Expand Down Expand Up @@ -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:
Expand Down
59 changes: 6 additions & 53 deletions homeassistant/components/withings/coordinator.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""Withings coordinator."""
import asyncio
from collections.abc import Callable
from datetime import timedelta
from typing import Any
Expand All @@ -24,9 +23,6 @@
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
] = {
Expand Down Expand Up @@ -84,55 +80,12 @@ def __init__(self, hass: HomeAssistant, client: ConfigEntryWithingsApi) -> None:
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:
Expand Down
4 changes: 2 additions & 2 deletions tests/components/withings/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
2 changes: 1 addition & 1 deletion tests/components/withings/test_init.py
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down

0 comments on commit ffb752c

Please sign in to comment.