From cc2e0fd9213e38de0dc1262306c019952d5900db Mon Sep 17 00:00:00 2001 From: Joost Lekkerkerker Date: Fri, 19 Apr 2024 18:18:32 +0200 Subject: [PATCH] Make Withings recoverable after internet outage (#115124) --- homeassistant/components/withings/__init__.py | 7 +- tests/components/withings/test_init.py | 105 ++++++++++++++++++ 2 files changed, 111 insertions(+), 1 deletion(-) diff --git a/homeassistant/components/withings/__init__.py b/homeassistant/components/withings/__init__.py index 1fe85f180da..0b86a2b5201 100644 --- a/homeassistant/components/withings/__init__.py +++ b/homeassistant/components/withings/__init__.py @@ -12,6 +12,7 @@ from dataclasses import dataclass, field from datetime import timedelta from typing import TYPE_CHECKING, Any, cast +from aiohttp import ClientError from aiohttp.hdrs import METH_POST from aiohttp.web import Request, Response from aiowithings import NotificationCategory, WithingsClient @@ -274,7 +275,11 @@ class WithingsWebhookManager: async def async_unsubscribe_webhooks(client: WithingsClient) -> None: """Unsubscribe to all Withings webhooks.""" - current_webhooks = await client.list_notification_configurations() + try: + current_webhooks = await client.list_notification_configurations() + except ClientError: + LOGGER.exception("Error when unsubscribing webhooks") + return for webhook_configuration in current_webhooks: LOGGER.debug( diff --git a/tests/components/withings/test_init.py b/tests/components/withings/test_init.py index ff0a098a7cb..3ade0fb7c3a 100644 --- a/tests/components/withings/test_init.py +++ b/tests/components/withings/test_init.py @@ -5,6 +5,7 @@ from typing import Any from unittest.mock import AsyncMock, patch from urllib.parse import urlparse +from aiohttp import ClientConnectionError from aiohttp.hdrs import METH_HEAD from aiowithings import ( NotificationCategory, @@ -425,6 +426,110 @@ async def test_cloud_disconnect( assert withings.subscribe_notification.call_count == 12 +async def test_internet_disconnect( + hass: HomeAssistant, + withings: AsyncMock, + webhook_config_entry: MockConfigEntry, + hass_client_no_auth: ClientSessionGenerator, + freezer: FrozenDateTimeFactory, +) -> None: + """Test we can recover from internet disconnects.""" + await mock_cloud(hass) + await hass.async_block_till_done() + + with ( + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch.object(cloud, "async_is_connected", return_value=True), + patch.object(cloud, "async_active_subscription", return_value=True), + patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value="https://hooks.nabu.casa/ABCD", + ), + patch( + "homeassistant.components.withings.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.cloud.async_delete_cloudhook", + ), + patch( + "homeassistant.components.withings.webhook_generate_url", + ), + ): + await setup_integration(hass, webhook_config_entry) + await prepare_webhook_setup(hass, freezer) + + assert cloud.async_active_subscription(hass) is True + assert cloud.async_is_connected(hass) is True + assert withings.revoke_notification_configurations.call_count == 3 + assert withings.subscribe_notification.call_count == 6 + + await hass.async_block_till_done() + + withings.list_notification_configurations.side_effect = ClientConnectionError + + async_mock_cloud_connection_status(hass, False) + await hass.async_block_till_done() + + assert withings.revoke_notification_configurations.call_count == 3 + + async_mock_cloud_connection_status(hass, True) + await hass.async_block_till_done() + + assert withings.subscribe_notification.call_count == 12 + + +async def test_cloud_disconnect_retry( + hass: HomeAssistant, + withings: AsyncMock, + webhook_config_entry: MockConfigEntry, + hass_client_no_auth: ClientSessionGenerator, + freezer: FrozenDateTimeFactory, +) -> None: + """Test we retry to create webhook connection again after cloud disconnects.""" + await mock_cloud(hass) + await hass.async_block_till_done() + + with ( + patch("homeassistant.components.cloud.async_is_logged_in", return_value=True), + patch.object(cloud, "async_is_connected", return_value=True), + patch.object( + cloud, "async_active_subscription", return_value=True + ) as mock_async_active_subscription, + patch( + "homeassistant.components.cloud.async_create_cloudhook", + return_value="https://hooks.nabu.casa/ABCD", + ), + patch( + "homeassistant.components.withings.async_get_config_entry_implementation", + ), + patch( + "homeassistant.components.cloud.async_delete_cloudhook", + ), + patch( + "homeassistant.components.withings.webhook_generate_url", + ), + ): + await setup_integration(hass, webhook_config_entry) + await prepare_webhook_setup(hass, freezer) + + assert cloud.async_active_subscription(hass) is True + assert cloud.async_is_connected(hass) is True + assert mock_async_active_subscription.call_count == 3 + + await hass.async_block_till_done() + + async_mock_cloud_connection_status(hass, False) + await hass.async_block_till_done() + + assert mock_async_active_subscription.call_count == 3 + + freezer.tick(timedelta(seconds=30)) + async_fire_time_changed(hass) + await hass.async_block_till_done() + + assert mock_async_active_subscription.call_count == 4 + + @pytest.mark.parametrize( ("body", "expected_code"), [