mirror of
https://github.com/home-assistant/core.git
synced 2026-09-03 12:02:16 +01:00
Keep a SwitchBot Cloud entry loading when the cloud goes away (#180562)
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user