Files
core/tests/components/switchbot_cloud/test_init.py
T

821 lines
28 KiB
Python

"""Tests for the SwitchBot Cloud integration init."""
from unittest.mock import AsyncMock, patch
from freezegun.api import FrozenDateTimeFactory
import pytest
from switchbot_api import (
Device,
PowerState,
Remote,
SwitchBotAuthenticationError,
SwitchBotConnectionError,
)
from homeassistant.components import cloud
from homeassistant.components.cloud import CloudNotAvailable
from homeassistant.components.switchbot_cloud import SwitchBotAPI
from homeassistant.components.switchbot_cloud.const import (
CONF_CLOUDHOOK_URL,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
)
from homeassistant.components.switchbot_cloud.coordinator import SwitchBotCoordinator
from homeassistant.components.webhook import DOMAIN as WEBHOOK_DOMAIN
from homeassistant.config_entries import ConfigEntryState
from homeassistant.const import (
CONF_API_KEY,
CONF_API_TOKEN,
CONF_WEBHOOK_ID,
EVENT_HOMEASSISTANT_START,
)
from homeassistant.core import HomeAssistant
from homeassistant.core_config import async_process_ha_core_config
from . import configure_integration
from tests.common import (
MockConfigEntry,
async_fire_time_changed,
async_mock_cloud_connection_status,
)
from tests.components.cloud import mock_cloud
from tests.typing import ClientSessionGenerator
CLOUDHOOK_URL = "https://hooks.nabu.casa/ABCD"
def _water_detector() -> Device:
"""Return a webhook-manageable device (Water Detector)."""
return Device(
version="V1.0",
deviceId="water-detector-1",
deviceName="water-detector-name-1",
deviceType="Water Detector",
hubDeviceId="test-hub-id",
)
@pytest.fixture
def mock_list_devices():
"""Mock list_devices."""
with patch.object(SwitchBotAPI, "list_devices") as mock_list_devices:
yield mock_list_devices
@pytest.fixture
def mock_get_status():
"""Mock get_status."""
with patch.object(SwitchBotAPI, "get_status") as mock_get_status:
yield mock_get_status
@pytest.fixture
def mock_get_webook_configuration():
"""Mock get_status."""
with patch.object(
SwitchBotAPI, "get_webook_configuration"
) as mock_get_webook_configuration:
yield mock_get_webook_configuration
@pytest.fixture
def mock_delete_webhook():
"""Mock get_status."""
with patch.object(SwitchBotAPI, "delete_webhook") as mock_delete_webhook:
yield mock_delete_webhook
@pytest.fixture
def mock_setup_webhook():
"""Mock get_status."""
with patch.object(SwitchBotAPI, "setup_webhook") as mock_setup_webhook:
yield mock_setup_webhook
async def test_setup_entry_success(
hass: HomeAssistant,
mock_list_devices,
mock_get_status,
mock_get_webook_configuration,
mock_delete_webhook,
mock_setup_webhook,
) -> None:
"""Test successful setup of entry."""
await async_process_ha_core_config(
hass,
{"external_url": "https://example.com"},
)
mock_get_webook_configuration.return_value = {"urls": ["https://example.com"]}
mock_list_devices.return_value = [
Remote(
version="V1.0",
deviceId="air-conditonner-id-1",
deviceName="air-conditonner-name-1",
remoteType="Air Conditioner",
hubDeviceId="test-hub-id",
),
Device(
version="V1.0",
deviceId="plug-id-1",
deviceName="plug-name-1",
deviceType="Plug",
hubDeviceId="test-hub-id",
),
Remote(
version="V1.0",
deviceId="plug-id-2",
deviceName="plug-name-2",
remoteType="DIY Plug",
hubDeviceId="test-hub-id",
),
Remote(
version="V1.0",
deviceId="meter-pro-1",
deviceName="meter-pro-name-1",
deviceType="MeterPro(CO2)",
hubDeviceId="test-hub-id",
),
Remote(
version="V1.0",
deviceId="hub2-1",
deviceName="hub2-name-1",
deviceType="Hub 2",
hubDeviceId="test-hub-id",
),
Device(
deviceId="vacuum-1",
deviceName="vacuum-name-1",
deviceType="K10+",
hubDeviceId=None,
),
]
mock_get_status.return_value = {"power": PowerState.ON.value}
entry = await configure_integration(hass)
assert entry.state is ConfigEntryState.LOADED
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
mock_list_devices.assert_called_once()
mock_get_status.assert_called()
mock_get_webook_configuration.assert_called_once()
mock_delete_webhook.assert_called_once()
mock_setup_webhook.assert_called_once()
@pytest.mark.parametrize(
("error", "state"),
[
(SwitchBotAuthenticationError, ConfigEntryState.SETUP_ERROR),
(SwitchBotConnectionError, ConfigEntryState.SETUP_RETRY),
],
)
async def test_setup_entry_fails_when_listing_devices(
hass: HomeAssistant,
error: Exception,
state: ConfigEntryState,
mock_list_devices,
mock_get_status,
) -> None:
"""Test error handling when list_devices in setup of entry."""
mock_list_devices.side_effect = error
entry = await configure_integration(hass)
assert entry.state is state
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
mock_list_devices.assert_called_once()
mock_get_status.assert_not_called()
async def test_setup_entry_fails_when_refreshing(
hass: HomeAssistant, mock_list_devices, mock_get_status
) -> None:
"""Test error handling in get_status in setup of entry."""
mock_list_devices.return_value = [
Device(
version="V1.0",
deviceId="test-id",
deviceName="test-name",
deviceType="Plug",
hubDeviceId="test-hub-id",
)
]
mock_get_status.side_effect = SwitchBotConnectionError
entry = await configure_integration(hass)
assert entry.state is ConfigEntryState.SETUP_RETRY
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
mock_list_devices.assert_called_once()
mock_get_status.assert_called()
async def test_posting_to_webhook(
hass: HomeAssistant,
mock_list_devices,
mock_get_status,
mock_get_webook_configuration,
mock_delete_webhook,
mock_setup_webhook,
hass_client_no_auth: ClientSessionGenerator,
) -> None:
"""Test handler webhook call."""
await async_process_ha_core_config(
hass,
{"external_url": "https://example.com"},
)
mock_get_webook_configuration.return_value = {"urls": ["https://example.com"]}
mock_list_devices.return_value = [
Device(
deviceId="vacuum-1",
deviceName="vacuum-name-1",
deviceType="K10+",
hubDeviceId=None,
),
]
mock_get_status.return_value = {"power": PowerState.ON.value}
mock_delete_webhook.return_value = {}
mock_setup_webhook.return_value = {}
entry = await configure_integration(hass)
assert entry.state is ConfigEntryState.LOADED
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
webhook_id = entry.data[CONF_WEBHOOK_ID]
client = await hass_client_no_auth()
# fire webhook
await client.post(
f"/api/webhook/{webhook_id}",
json={
"eventType": "changeReport",
"eventVersion": "1",
"context": {"deviceType": "...", "deviceMac": "vacuum-1"},
},
)
await hass.async_block_till_done()
mock_setup_webhook.assert_called_once()
async def test_polling_is_only_disabled_after_webhook_delivery(
hass: HomeAssistant,
mock_list_devices,
mock_get_status,
mock_get_webook_configuration,
mock_delete_webhook,
mock_setup_webhook,
hass_client_no_auth: ClientSessionGenerator,
freezer: FrozenDateTimeFactory,
) -> None:
"""Test polling stays enabled until a webhook is received."""
await async_process_ha_core_config(
hass,
{"external_url": "https://example.com"},
)
mock_get_webook_configuration.return_value = {"urls": ["https://example.com"]}
mock_list_devices.return_value = [
Device(
deviceId="vacuum-1",
deviceName="vacuum-name-1",
deviceType="K10+",
hubDeviceId=None,
),
]
mock_get_status.return_value = {
"battery": 71,
"deviceId": "vacuum-1",
"onlineStatus": "online",
"workingStatus": "Paused",
}
mock_delete_webhook.return_value = {}
mock_setup_webhook.return_value = {}
entry = await configure_integration(hass)
assert entry.state is ConfigEntryState.LOADED
# Validate the state is fetched initially
entity_id = "vacuum.vacuum_name_1"
state = hass.states.get(entity_id)
assert state is not None
# Change API return values and wait for update
mock_get_status.return_value = {
"battery": 60,
"deviceId": "vacuum-1",
"onlineStatus": "online",
"workingStatus": "Paused",
}
freezer.tick(DEFAULT_SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
# Validate that the state was updated again via fetch
state = hass.states.get(entity_id)
assert state is not None
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
webhook_id = entry.data[CONF_WEBHOOK_ID]
client = await hass_client_no_auth()
await client.post(
f"/api/webhook/{webhook_id}",
json={
"eventType": "changeReport",
"eventVersion": "1",
"context": {
"battery": 74,
"deviceType": "WoSweeperMini",
"deviceMac": "vacuum-1",
"onlineStatus": "online",
"workingStatus": "Clearing",
},
},
)
await hass.async_block_till_done()
mock_get_status.reset_mock()
freezer.tick(DEFAULT_SCAN_INTERVAL)
async_fire_time_changed(hass)
await hass.async_block_till_done(wait_background_tasks=True)
# After receiving a webhook, no fetch should happen
mock_get_status.assert_not_called()
async def test_setup_entry_skips_webhook_without_external_url(
hass: HomeAssistant,
mock_list_devices,
mock_get_status,
mock_get_webook_configuration,
mock_delete_webhook,
mock_setup_webhook,
caplog: pytest.LogCaptureFixture,
) -> None:
"""Test webhook registration is skipped without an external URL."""
mock_list_devices.return_value = [
Device(
deviceId="vacuum-1",
deviceName="vacuum-name-1",
deviceType="K10+",
hubDeviceId=None,
),
]
mock_get_status.return_value = {"power": PowerState.ON.value}
entry = await configure_integration(hass)
assert entry.state is ConfigEntryState.LOADED
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
mock_get_webook_configuration.assert_not_called()
mock_delete_webhook.assert_not_called()
mock_setup_webhook.assert_not_called()
assert entry.data[CONF_WEBHOOK_ID] not in hass.data.get(WEBHOOK_DOMAIN, {})
assert "no external URL is available" in caplog.text
async def test_setup_creates_cloudhook_when_cloud_active(
hass: HomeAssistant,
mock_list_devices,
mock_get_status,
mock_get_webook_configuration,
mock_delete_webhook,
mock_setup_webhook,
) -> None:
"""Test a cloudhook is created and registered when cloud is active.
This is the local-only-HA case: no external_url is configured, but a Nabu
Casa subscription is active, so SwitchBot should be given the cloudhook URL.
"""
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",
return_value=CLOUDHOOK_URL,
) as fake_create_cloudhook,
patch("homeassistant.components.cloud.async_delete_cloudhook"),
):
entry = await configure_integration(hass)
assert entry.state is ConfigEntryState.LOADED
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
# Cloudhook was created and persisted in entry.data ...
fake_create_cloudhook.assert_called_once()
assert entry.data[CONF_CLOUDHOOK_URL] == CLOUDHOOK_URL
# ... and registered with SwitchBot's cloud.
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,
mock_get_status,
mock_get_webook_configuration,
mock_delete_webhook,
mock_setup_webhook,
) -> None:
"""Test a previously persisted cloudhook is reused, not recreated."""
await mock_cloud(hass)
await hass.async_block_till_done()
# SwitchBot already has the cloudhook registered (so nothing to re-add).
mock_get_webook_configuration.return_value = {"urls": [CLOUDHOOK_URL]}
mock_list_devices.return_value = [_water_detector()]
mock_get_status.return_value = {"battery": 100}
mock_setup_webhook.return_value = {}
# An entry that already has both a webhook id and a persisted cloudhook.
entry = MockConfigEntry(
domain=DOMAIN,
data={
CONF_API_TOKEN: "test-token",
CONF_API_KEY: "test-api-key",
CONF_WEBHOOK_ID: "test-webhook-id",
CONF_CLOUDHOOK_URL: CLOUDHOOK_URL,
},
entry_id="123456",
unique_id="123456",
)
entry.add_to_hass(hass)
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",
return_value=CLOUDHOOK_URL,
) as fake_create_cloudhook,
patch("homeassistant.components.cloud.async_delete_cloudhook"),
):
await hass.config_entries.async_setup(entry.entry_id)
await hass.async_block_till_done()
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
# No new cloudhook should have been created and SwitchBot's already-set
# URL (the cloudhook) needs no re-registration.
fake_create_cloudhook.assert_not_called()
mock_setup_webhook.assert_not_called()
assert entry.data[CONF_CLOUDHOOK_URL] == CLOUDHOOK_URL
async def test_setup_falls_back_to_local_url_without_cloud(
hass: HomeAssistant,
mock_list_devices,
mock_get_status,
mock_get_webook_configuration,
mock_delete_webhook,
mock_setup_webhook,
) -> None:
"""Test the local external URL is used when no cloud subscription exists."""
await async_process_ha_core_config(
hass,
{"external_url": "https://example.com"},
)
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.object(cloud, "async_active_subscription", return_value=False):
entry = await configure_integration(hass)
assert entry.state is ConfigEntryState.LOADED
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
assert CONF_CLOUDHOOK_URL not in entry.data
mock_setup_webhook.assert_called_once()
registered_url = mock_setup_webhook.call_args[0][0]
assert registered_url.startswith("https://example.com")
async def test_cloud_connects_after_setup(
hass: HomeAssistant,
mock_list_devices,
mock_get_status,
mock_get_webook_configuration,
mock_delete_webhook,
mock_setup_webhook,
) -> None:
"""Test a cloudhook is created when the cloud connects after setup.
On a local-only install the cloud may only become available after the entry
has already been set up; the integration must react to the connection
change and register the cloudhook with SwitchBot.
"""
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 = {}
# Cloud is not active at setup time.
with patch.object(cloud, "async_active_subscription", return_value=False):
entry = await configure_integration(hass)
assert entry.state is ConfigEntryState.LOADED
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
assert CONF_CLOUDHOOK_URL not in entry.data
# Cloud becomes active and connects.
with (
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",
return_value=CLOUDHOOK_URL,
) as fake_create_cloudhook,
patch("homeassistant.components.cloud.async_delete_cloudhook"),
):
async_mock_cloud_connection_status(hass, True)
await hass.async_block_till_done()
fake_create_cloudhook.assert_called_once()
assert entry.data[CONF_CLOUDHOOK_URL] == CLOUDHOOK_URL
mock_setup_webhook.assert_called_with(CLOUDHOOK_URL)
async def test_setup_active_subscription_not_connected(
hass: HomeAssistant,
mock_list_devices,
mock_get_status,
mock_get_webook_configuration,
mock_delete_webhook,
mock_setup_webhook,
) -> None:
"""Test no cloudhook is created while the cloud is not yet connected.
``async_active_subscription`` is true as soon as the user is logged in with a
valid subscription, but ``async_create_cloudhook`` raises ``CloudNotConnected``
until the cloud connection is established. Setup must not call it (which would
propagate and abort setup); the connection-change listener creates the
cloudhook once connected.
"""
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 = {}
# Subscription active, but the cloud connection is not established yet.
with (
patch("homeassistant.components.cloud.async_is_logged_in", return_value=True),
patch("homeassistant.components.cloud.async_is_connected", return_value=False),
patch.object(cloud, "async_active_subscription", return_value=True),
patch(
"homeassistant.components.cloud.async_get_or_create_cloudhook",
side_effect=cloud.CloudNotConnected,
) as fake_create_cloudhook,
patch("homeassistant.components.cloud.async_delete_cloudhook"),
):
entry = await configure_integration(hass)
await hass.async_block_till_done()
# Setup completed without aborting, and no cloudhook was created.
assert entry.state is ConfigEntryState.LOADED
fake_create_cloudhook.assert_not_called()
assert CONF_CLOUDHOOK_URL not in entry.data
# Once the cloud connects, the cloudhook is created and registered.
with (
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",
return_value=CLOUDHOOK_URL,
) as fake_create_cloudhook,
patch("homeassistant.components.cloud.async_delete_cloudhook"),
):
async_mock_cloud_connection_status(hass, True)
await hass.async_block_till_done()
fake_create_cloudhook.assert_called_once()
assert entry.data[CONF_CLOUDHOOK_URL] == CLOUDHOOK_URL
mock_setup_webhook.assert_called_with(CLOUDHOOK_URL)
async def test_cloudhook_deleted_on_entry_removal(
hass: HomeAssistant,
mock_list_devices,
mock_get_status,
mock_get_webook_configuration,
mock_delete_webhook,
mock_setup_webhook,
) -> None:
"""Test the cloudhook is deleted when the config entry is removed."""
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",
return_value=CLOUDHOOK_URL,
),
patch(
"homeassistant.components.cloud.async_delete_cloudhook"
) as fake_delete_cloudhook,
):
entry = await configure_integration(hass)
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
assert entry.data[CONF_CLOUDHOOK_URL] == CLOUDHOOK_URL
# Ignore the pre-create cleanup delete that happened during setup; we
# only care that removal deletes the cloudhook.
fake_delete_cloudhook.reset_mock()
await hass.config_entries.async_remove(entry.entry_id)
await hass.async_block_till_done()
fake_delete_cloudhook.assert_called_once_with(hass, entry.data[CONF_WEBHOOK_ID])
async def test_remove_entry_without_cloudhook_skips_delete(
hass: HomeAssistant,
mock_list_devices,
mock_get_status,
mock_get_webook_configuration,
mock_delete_webhook,
mock_setup_webhook,
) -> None:
"""Test no cloudhook delete is attempted when none was created."""
await async_process_ha_core_config(
hass,
{"external_url": "https://example.com"},
)
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.object(cloud, "async_active_subscription", return_value=False),
patch(
"homeassistant.components.cloud.async_delete_cloudhook"
) as fake_delete_cloudhook,
):
entry = await configure_integration(hass)
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
assert CONF_CLOUDHOOK_URL not in entry.data
await hass.config_entries.async_remove(entry.entry_id)
await hass.async_block_till_done()
fake_delete_cloudhook.assert_not_called()
async def test_remove_entry_with_cloud_unavailable(
hass: HomeAssistant,
mock_list_devices,
mock_get_status,
mock_get_webook_configuration,
mock_delete_webhook,
mock_setup_webhook,
) -> None:
"""Test CloudNotAvailable is handled gracefully on entry removal."""
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",
return_value=CLOUDHOOK_URL,
),
patch(
"homeassistant.components.cloud.async_delete_cloudhook",
side_effect=CloudNotAvailable(),
),
):
entry = await configure_integration(hass)
hass.bus.async_fire(EVENT_HOMEASSISTANT_START)
await hass.async_block_till_done()
assert entry.data[CONF_CLOUDHOOK_URL] == CLOUDHOOK_URL
# Should not raise even though delete raises CloudNotAvailable.
await hass.config_entries.async_remove(entry.entry_id)
await hass.async_block_till_done()
assert not hass.config_entries.async_entries("switchbot_cloud")
async def test_single_coordinator_for_multi_platform_device(
hass: HomeAssistant, mock_list_devices: AsyncMock, mock_get_status: AsyncMock
) -> None:
"""Test that a multi-platform device creates only one coordinator."""
mock_list_devices.return_value = [
Device(
version="V1.0",
deviceId="relay-switch-pm-id-1",
deviceName="relay-switch-pm-1",
deviceType="Relay Switch 1PM",
hubDeviceId="test-hub-id",
),
]
mock_get_status.return_value = {"switchStatus": 0}
with patch(
"homeassistant.components.switchbot_cloud.SwitchBotCoordinator",
wraps=SwitchBotCoordinator,
) as coordinator_cls:
entry = await configure_integration(hass)
assert entry.state is ConfigEntryState.LOADED
assert coordinator_cls.call_count == 1