Keep a SwitchBot Cloud entry loading when the cloud goes away (#180562)

This commit is contained in:
Franck Nijhof
2026-08-29 01:49:28 +02:00
committed by GitHub
parent 4a55700258
commit 0cdbba4ead
2 changed files with 74 additions and 10 deletions
@@ -1,6 +1,6 @@
"""SwitchBot via API integration."""
from asyncio import gather
from asyncio import Lock, gather
from collections.abc import Awaitable, Callable
import contextlib
from dataclasses import dataclass, field
@@ -310,9 +310,13 @@ async def async_setup_entry(
)
entry.runtime_data = SwitchbotCloudData(api=api, devices=switchbot_devices)
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
# One at a time, so the cloud connecting cannot register a webhook next to
# the one being registered here
webhook_lock = Lock()
await _initialize_webhook(hass, entry, api, coordinators_by_id)
async def _async_initialize_webhook() -> None:
async with webhook_lock:
await _initialize_webhook(hass, entry, api, coordinators_by_id)
async def _handle_cloud_connection_change(
state: cloud.CloudConnectionState,
@@ -324,12 +328,20 @@ async def async_setup_entry(
and re-register it with SwitchBot's cloud so push devices work.
"""
if state is cloud.CloudConnectionState.CLOUD_CONNECTED:
await _initialize_webhook(hass, entry, api, coordinators_by_id)
await _async_initialize_webhook()
# Listening before the first attempt, so a cloud that connects while the
# entry is still setting up is not missed
entry.async_on_unload(
cloud.async_listen_connection_change(hass, _handle_cloud_connection_change)
)
await _async_initialize_webhook()
# Forwarded last, so a failure above cannot leave the platforms set up for a
# retry to set up a second time
await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS)
return True
@@ -445,12 +457,16 @@ async def _async_get_webhook_url(
return str(entry.data[CONF_CLOUDHOOK_URL])
if cloud.async_is_connected(hass):
webhook_id = entry.data[CONF_WEBHOOK_ID]
cloudhook_url = await cloud.async_get_or_create_cloudhook(hass, webhook_id)
hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_CLOUDHOOK_URL: cloudhook_url}
)
_LOGGER.debug("Created SwitchBot Cloud cloudhook: %s", cloudhook_url)
return cloudhook_url
# The cloud can go away between being asked and being used
with contextlib.suppress(cloud.CloudNotAvailable):
cloudhook_url = await cloud.async_get_or_create_cloudhook(
hass, webhook_id
)
hass.config_entries.async_update_entry(
entry, data={**entry.data, CONF_CLOUDHOOK_URL: cloudhook_url}
)
_LOGGER.debug("Created SwitchBot Cloud cloudhook: %s", cloudhook_url)
return cloudhook_url
return webhook.async_generate_url(
hass,
@@ -425,6 +425,54 @@ async def test_setup_creates_cloudhook_when_cloud_active(
mock_setup_webhook.assert_called_once_with(CLOUDHOOK_URL)
async def test_setup_survives_the_cloud_going_away(
hass: HomeAssistant,
mock_list_devices: AsyncMock,
mock_get_status: AsyncMock,
mock_get_webook_configuration: AsyncMock,
mock_delete_webhook: AsyncMock,
mock_setup_webhook: AsyncMock,
) -> None:
"""Test the entry still loads when the cloud goes away mid-setup.
The connection is checked before the cloudhook is created, so it can be
gone by the time it is used. The local URL carries it until the
connection change listener creates the cloudhook.
"""
await async_process_ha_core_config(
hass,
{"external_url": "https://example.com"},
)
await mock_cloud(hass)
await hass.async_block_till_done()
mock_get_webook_configuration.return_value = {"urls": []}
mock_list_devices.return_value = [_water_detector()]
mock_get_status.return_value = {"battery": 100}
mock_delete_webhook.return_value = {}
mock_setup_webhook.return_value = {}
with (
patch("homeassistant.components.cloud.async_is_logged_in", return_value=True),
patch("homeassistant.components.cloud.async_is_connected", return_value=True),
patch.object(cloud, "async_active_subscription", return_value=True),
patch(
"homeassistant.components.cloud.async_get_or_create_cloudhook",
side_effect=CloudNotAvailable,
),
patch("homeassistant.components.cloud.async_delete_cloudhook"),
):
entry = await configure_integration(hass)
assert entry.state is ConfigEntryState.LOADED
assert CONF_CLOUDHOOK_URL not in entry.data
# SwitchBot was given the local URL to push to in the meantime
mock_setup_webhook.assert_called_once()
assert mock_setup_webhook.call_args[0][0].startswith(
"https://example.com/api/webhook/"
)
async def test_setup_reuses_persisted_cloudhook(
hass: HomeAssistant,
mock_list_devices,