diff --git a/.coveragerc b/.coveragerc index ddde800cd77ea5..e226b22381b068 100644 --- a/.coveragerc +++ b/.coveragerc @@ -1252,6 +1252,9 @@ omit = homeassistant/components/switchbot/sensor.py homeassistant/components/switchbot/switch.py homeassistant/components/switchbot/lock.py + homeassistant/components/switchbot_cloud/coordinator.py + homeassistant/components/switchbot_cloud/entity.py + homeassistant/components/switchbot_cloud/switch.py homeassistant/components/switchmate/switch.py homeassistant/components/syncthing/__init__.py homeassistant/components/syncthing/sensor.py diff --git a/.strict-typing b/.strict-typing index c1138119f5f34d..56c7bf248e18c4 100644 --- a/.strict-typing +++ b/.strict-typing @@ -318,6 +318,7 @@ homeassistant.components.sun.* homeassistant.components.surepetcare.* homeassistant.components.switch.* homeassistant.components.switchbee.* +homeassistant.components.switchbot_cloud.* homeassistant.components.switcher_kis.* homeassistant.components.synology_dsm.* homeassistant.components.systemmonitor.* diff --git a/CODEOWNERS b/CODEOWNERS index 7c96042caa392e..8453a4893fedd8 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -1238,6 +1238,8 @@ build.json @home-assistant/supervisor /tests/components/switchbee/ @jafar-atili /homeassistant/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski /tests/components/switchbot/ @danielhiversen @RenierM26 @murtas @Eloston @dsypniewski +/homeassistant/components/switchbot_cloud/ @SeraphicRav +/tests/components/switchbot_cloud/ @SeraphicRav /homeassistant/components/switcher_kis/ @thecode /tests/components/switcher_kis/ @thecode /homeassistant/components/switchmate/ @danielhiversen @qiz-li diff --git a/homeassistant/brands/switchbot.json b/homeassistant/brands/switchbot.json new file mode 100644 index 00000000000000..0909b24a146990 --- /dev/null +++ b/homeassistant/brands/switchbot.json @@ -0,0 +1,5 @@ +{ + "domain": "switchbot", + "name": "SwitchBot", + "integrations": ["switchbot", "switchbot_cloud"] +} diff --git a/homeassistant/components/switchbot/manifest.json b/homeassistant/components/switchbot/manifest.json index 2259a450559114..49a6af2b179a21 100644 --- a/homeassistant/components/switchbot/manifest.json +++ b/homeassistant/components/switchbot/manifest.json @@ -1,6 +1,6 @@ { "domain": "switchbot", - "name": "SwitchBot", + "name": "SwitchBot Bluetooth", "bluetooth": [ { "service_data_uuid": "00000d00-0000-1000-8000-00805f9b34fb", diff --git a/homeassistant/components/switchbot_cloud/__init__.py b/homeassistant/components/switchbot_cloud/__init__.py new file mode 100644 index 00000000000000..cf711fcc4311ce --- /dev/null +++ b/homeassistant/components/switchbot_cloud/__init__.py @@ -0,0 +1,81 @@ +"""The SwitchBot via API integration.""" +from asyncio import gather +from dataclasses import dataclass +from logging import getLogger + +from switchbot_api import CannotConnect, Device, InvalidAuth, Remote, SwitchBotAPI + +from homeassistant.config_entries import ConfigEntry +from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN, Platform +from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryNotReady + +from .const import DOMAIN +from .coordinator import SwitchBotCoordinator + +_LOGGER = getLogger(__name__) +PLATFORMS: list[Platform] = [Platform.SWITCH] + + +@dataclass +class SwitchbotDevices: + """Switchbot devices data.""" + + switches: list[Device | Remote] + + +@dataclass +class SwitchbotCloudData: + """Data to use in platforms.""" + + api: SwitchBotAPI + devices: SwitchbotDevices + + +async def async_setup_entry(hass: HomeAssistant, config: ConfigEntry) -> bool: + """Set up SwitchBot via API from a config entry.""" + token = config.data[CONF_API_TOKEN] + secret = config.data[CONF_API_KEY] + + api = SwitchBotAPI(token=token, secret=secret) + try: + devices = await api.list_devices() + except InvalidAuth as ex: + _LOGGER.error( + "Invalid authentication while connecting to SwitchBot API: %s", ex + ) + return False + except CannotConnect as ex: + raise ConfigEntryNotReady from ex + _LOGGER.debug("Devices: %s", devices) + devices_and_coordinators = [ + (device, SwitchBotCoordinator(hass, api, device)) for device in devices + ] + hass.data.setdefault(DOMAIN, {}) + data = SwitchbotCloudData( + api=api, + devices=SwitchbotDevices( + switches=[ + (device, coordinator) + for device, coordinator in devices_and_coordinators + if isinstance(device, Device) + and device.device_type.startswith("Plug") + or isinstance(device, Remote) + ], + ), + ) + hass.data[DOMAIN][config.entry_id] = data + _LOGGER.debug("Switches: %s", data.devices.switches) + await hass.config_entries.async_forward_entry_setups(config, PLATFORMS) + await gather( + *[coordinator.async_refresh() for _, coordinator in devices_and_coordinators] + ) + return True + + +async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: + """Unload a config entry.""" + if unload_ok := await hass.config_entries.async_unload_platforms(entry, PLATFORMS): + hass.data[DOMAIN].pop(entry.entry_id) + + return unload_ok diff --git a/homeassistant/components/switchbot_cloud/config_flow.py b/homeassistant/components/switchbot_cloud/config_flow.py new file mode 100644 index 00000000000000..5c99567968c1e6 --- /dev/null +++ b/homeassistant/components/switchbot_cloud/config_flow.py @@ -0,0 +1,56 @@ +"""Config flow for SwitchBot via API integration.""" + +from logging import getLogger +from typing import Any + +from switchbot_api import CannotConnect, InvalidAuth, SwitchBotAPI +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN +from homeassistant.data_entry_flow import FlowResult + +from .const import DOMAIN, ENTRY_TITLE + +_LOGGER = getLogger(__name__) + +STEP_USER_DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_API_TOKEN): str, + vol.Required(CONF_API_KEY): str, + } +) + + +class SwitchBotCloudConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a config flow for SwitchBot via API.""" + + VERSION = 1 + + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle the initial step.""" + errors: dict[str, str] = {} + if user_input is not None: + try: + await SwitchBotAPI( + token=user_input[CONF_API_TOKEN], secret=user_input[CONF_API_KEY] + ).list_devices() + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors["base"] = "invalid_auth" + except Exception: # pylint: disable=broad-except + _LOGGER.exception("Unexpected exception") + errors["base"] = "unknown" + else: + await self.async_set_unique_id( + user_input[CONF_API_TOKEN], raise_on_progress=False + ) + self._abort_if_unique_id_configured() + return self.async_create_entry(title=ENTRY_TITLE, data=user_input) + + return self.async_show_form( + step_id="user", data_schema=STEP_USER_DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/switchbot_cloud/const.py b/homeassistant/components/switchbot_cloud/const.py new file mode 100644 index 00000000000000..ef69c9c1d02d29 --- /dev/null +++ b/homeassistant/components/switchbot_cloud/const.py @@ -0,0 +1,7 @@ +"""Constants for the SwitchBot Cloud integration.""" +from datetime import timedelta +from typing import Final + +DOMAIN: Final = "switchbot_cloud" +ENTRY_TITLE = "SwitchBot Cloud" +SCAN_INTERVAL = timedelta(seconds=600) diff --git a/homeassistant/components/switchbot_cloud/coordinator.py b/homeassistant/components/switchbot_cloud/coordinator.py new file mode 100644 index 00000000000000..92099ccde4337b --- /dev/null +++ b/homeassistant/components/switchbot_cloud/coordinator.py @@ -0,0 +1,50 @@ +"""SwitchBot Cloud coordinator.""" +from asyncio import timeout +from logging import getLogger +from typing import Any + +from switchbot_api import CannotConnect, Device, Remote, SwitchBotAPI + +from homeassistant.core import HomeAssistant +from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed + +from .const import DOMAIN, SCAN_INTERVAL + +_LOGGER = getLogger(__name__) + +Status = dict[str, Any] | None + + +class SwitchBotCoordinator(DataUpdateCoordinator[Status]): + """SwitchBot Cloud coordinator.""" + + _api: SwitchBotAPI + _device_id: str + _should_poll = False + + def __init__( + self, hass: HomeAssistant, api: SwitchBotAPI, device: Device | Remote + ) -> None: + """Initialize SwitchBot Cloud.""" + super().__init__( + hass, + _LOGGER, + name=DOMAIN, + update_interval=SCAN_INTERVAL, + ) + self._api = api + self._device_id = device.device_id + self._should_poll = not isinstance(device, Remote) + + async def _async_update_data(self) -> Status: + """Fetch data from API endpoint.""" + if not self._should_poll: + return None + try: + _LOGGER.debug("Refreshing %s", self._device_id) + async with timeout(10): + status: Status = await self._api.get_status(self._device_id) + _LOGGER.debug("Refreshing %s with %s", self._device_id, status) + return status + except CannotConnect as err: + raise UpdateFailed(f"Error communicating with API: {err}") from err diff --git a/homeassistant/components/switchbot_cloud/entity.py b/homeassistant/components/switchbot_cloud/entity.py new file mode 100644 index 00000000000000..5d0e2ff09c34c4 --- /dev/null +++ b/homeassistant/components/switchbot_cloud/entity.py @@ -0,0 +1,49 @@ +"""Base class for SwitchBot via API entities.""" +from typing import Any + +from switchbot_api import Commands, Device, Remote, SwitchBotAPI + +from homeassistant.helpers.device_registry import DeviceInfo +from homeassistant.helpers.update_coordinator import CoordinatorEntity + +from .const import DOMAIN +from .coordinator import SwitchBotCoordinator + + +class SwitchBotCloudEntity(CoordinatorEntity[SwitchBotCoordinator]): + """Representation of a SwitchBot Cloud entity.""" + + _api: SwitchBotAPI + _switchbot_state: dict[str, Any] | None = None + _attr_has_entity_name = True + + def __init__( + self, + api: SwitchBotAPI, + device: Device | Remote, + coordinator: SwitchBotCoordinator, + ) -> None: + """Initialize the entity.""" + super().__init__(coordinator) + self._api = api + self._attr_unique_id = device.device_id + self._attr_device_info = DeviceInfo( + identifiers={(DOMAIN, device.device_id)}, + name=device.device_name, + manufacturer="SwitchBot", + model=device.device_type, + ) + + async def send_command( + self, + command: Commands, + command_type: str = "command", + parameters: dict | str = "default", + ) -> None: + """Send command to device.""" + await self._api.send_command( + self._attr_unique_id, + command, + command_type, + parameters, + ) diff --git a/homeassistant/components/switchbot_cloud/manifest.json b/homeassistant/components/switchbot_cloud/manifest.json new file mode 100644 index 00000000000000..0451217ca5f34d --- /dev/null +++ b/homeassistant/components/switchbot_cloud/manifest.json @@ -0,0 +1,10 @@ +{ + "domain": "switchbot_cloud", + "name": "SwitchBot Cloud", + "codeowners": ["@SeraphicRav"], + "config_flow": true, + "documentation": "https://www.home-assistant.io/integrations/switchbot_cloud", + "iot_class": "cloud_polling", + "loggers": ["switchbot-api"], + "requirements": ["switchbot-api==1.1.0"] +} diff --git a/homeassistant/components/switchbot_cloud/strings.json b/homeassistant/components/switchbot_cloud/strings.json new file mode 100644 index 00000000000000..11e92e6dfa38f9 --- /dev/null +++ b/homeassistant/components/switchbot_cloud/strings.json @@ -0,0 +1,20 @@ +{ + "config": { + "step": { + "user": { + "data": { + "api_token": "[%key:common::config_flow::data::api_token%]", + "api_key": "[%key:common::config_flow::data::api_key%]" + } + } + }, + "error": { + "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", + "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", + "unknown": "[%key:common::config_flow::error::unknown%]" + }, + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + } + } +} diff --git a/homeassistant/components/switchbot_cloud/switch.py b/homeassistant/components/switchbot_cloud/switch.py new file mode 100644 index 00000000000000..c63b1713b8de6c --- /dev/null +++ b/homeassistant/components/switchbot_cloud/switch.py @@ -0,0 +1,82 @@ +"""Support for SwitchBot switch.""" +from typing import Any + +from switchbot_api import CommonCommands, Device, PowerState, Remote, SwitchBotAPI + +from homeassistant.components.switch import SwitchDeviceClass, SwitchEntity +from homeassistant.config_entries import ConfigEntry +from homeassistant.core import HomeAssistant, callback +from homeassistant.helpers.entity_platform import AddEntitiesCallback +from homeassistant.helpers.typing import DiscoveryInfoType + +from . import SwitchbotCloudData +from .const import DOMAIN +from .coordinator import SwitchBotCoordinator +from .entity import SwitchBotCloudEntity + + +async def async_setup_entry( + hass: HomeAssistant, + config: ConfigEntry, + async_add_entities: AddEntitiesCallback, + discovery_info: DiscoveryInfoType | None = None, +) -> None: + """Set up SwitchBot Cloud entry.""" + data: SwitchbotCloudData = hass.data[DOMAIN][config.entry_id] + async_add_entities( + _async_make_entity(data.api, device, coordinator) + for device, coordinator in data.devices.switches + ) + + +class SwitchBotCloudSwitch(SwitchBotCloudEntity, SwitchEntity): + """Representation of a SwitchBot switch.""" + + _attr_device_class = SwitchDeviceClass.SWITCH + _attr_name = None + + async def async_turn_on(self, **kwargs: Any) -> None: + """Turn the device on.""" + await self.send_command(CommonCommands.ON) + self._attr_is_on = True + self.async_write_ha_state() + + async def async_turn_off(self, **kwargs: Any) -> None: + """Turn the device off.""" + await self.send_command(CommonCommands.OFF) + self._attr_is_on = False + self.async_write_ha_state() + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + if not self.coordinator.data: + return + self._attr_is_on = self.coordinator.data.get("power") == PowerState.ON.value + self.async_write_ha_state() + + +class SwitchBotCloudRemoteSwitch(SwitchBotCloudSwitch): + """Representation of a SwitchBot switch provider by a remote.""" + + @callback + def _handle_coordinator_update(self) -> None: + """Handle updated data from the coordinator.""" + + +class SwitchBotCloudPlugSwitch(SwitchBotCloudSwitch): + """Representation of a SwitchBot plug switch.""" + + _attr_device_class = SwitchDeviceClass.OUTLET + + +@callback +def _async_make_entity( + api: SwitchBotAPI, device: Device | Remote, coordinator: SwitchBotCoordinator +) -> SwitchBotCloudSwitch: + """Make a SwitchBotCloudSwitch or SwitchBotCloudRemoteSwitch.""" + if isinstance(device, Remote): + return SwitchBotCloudRemoteSwitch(api, device, coordinator) + if "Plug" in device.device_type: + return SwitchBotCloudPlugSwitch(api, device, coordinator) + raise NotImplementedError(f"Unsupported device type: {device.device_type}") diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index 98935086b88e49..229682eff1d645 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -455,6 +455,7 @@ "surepetcare", "switchbee", "switchbot", + "switchbot_cloud", "switcher_kis", "syncthing", "syncthru", diff --git a/homeassistant/generated/integrations.json b/homeassistant/generated/integrations.json index 779ee92e9fe37f..a65239316ed098 100644 --- a/homeassistant/generated/integrations.json +++ b/homeassistant/generated/integrations.json @@ -5514,9 +5514,20 @@ }, "switchbot": { "name": "SwitchBot", - "integration_type": "hub", - "config_flow": true, - "iot_class": "local_push" + "integrations": { + "switchbot": { + "integration_type": "hub", + "config_flow": true, + "iot_class": "local_push", + "name": "SwitchBot Bluetooth" + }, + "switchbot_cloud": { + "integration_type": "hub", + "config_flow": true, + "iot_class": "cloud_polling", + "name": "SwitchBot Cloud" + } + } }, "switcher_kis": { "name": "Switcher", diff --git a/mypy.ini b/mypy.ini index 3d6e4e1b2b6ca8..d2c2a66d738aa6 100644 --- a/mypy.ini +++ b/mypy.ini @@ -2943,6 +2943,16 @@ disallow_untyped_defs = true warn_return_any = true warn_unreachable = true +[mypy-homeassistant.components.switchbot_cloud.*] +check_untyped_defs = true +disallow_incomplete_defs = true +disallow_subclassing_any = true +disallow_untyped_calls = true +disallow_untyped_decorators = true +disallow_untyped_defs = true +warn_return_any = true +warn_unreachable = true + [mypy-homeassistant.components.switcher_kis.*] check_untyped_defs = true disallow_incomplete_defs = true diff --git a/requirements_all.txt b/requirements_all.txt index a87177e296df1c..52341321ced1c6 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2502,6 +2502,9 @@ surepy==0.8.0 # homeassistant.components.swiss_hydrological_data swisshydrodata==0.1.0 +# homeassistant.components.switchbot_cloud +switchbot-api==1.1.0 + # homeassistant.components.synology_srm synology-srm==0.2.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index c2d44de0327568..1de5c8ae574d15 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1847,6 +1847,9 @@ sunwatcher==0.2.1 # homeassistant.components.surepetcare surepy==0.8.0 +# homeassistant.components.switchbot_cloud +switchbot-api==1.1.0 + # homeassistant.components.system_bridge systembridgeconnector==3.8.2 diff --git a/tests/components/switchbot_cloud/__init__.py b/tests/components/switchbot_cloud/__init__.py new file mode 100644 index 00000000000000..72d23c837ac7c6 --- /dev/null +++ b/tests/components/switchbot_cloud/__init__.py @@ -0,0 +1,20 @@ +"""Tests for the SwitchBot Cloud integration.""" +from homeassistant.components.switchbot_cloud.const import DOMAIN +from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN +from homeassistant.core import HomeAssistant + +from tests.common import MockConfigEntry + + +def configure_integration(hass: HomeAssistant) -> MockConfigEntry: + """Configure the integration.""" + config = { + CONF_API_TOKEN: "test-token", + CONF_API_KEY: "test-api-key", + } + entry = MockConfigEntry( + domain=DOMAIN, data=config, entry_id="123456", unique_id="123456" + ) + entry.add_to_hass(hass) + + return entry diff --git a/tests/components/switchbot_cloud/conftest.py b/tests/components/switchbot_cloud/conftest.py new file mode 100644 index 00000000000000..b96d76387975ad --- /dev/null +++ b/tests/components/switchbot_cloud/conftest.py @@ -0,0 +1,15 @@ +"""Common fixtures for the SwitchBot via API tests.""" +from collections.abc import Generator +from unittest.mock import AsyncMock, patch + +import pytest + + +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Override async_setup_entry.""" + with patch( + "homeassistant.components.switchbot_cloud.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + yield mock_setup_entry diff --git a/tests/components/switchbot_cloud/test_config_flow.py b/tests/components/switchbot_cloud/test_config_flow.py new file mode 100644 index 00000000000000..6fdf8fecdb7119 --- /dev/null +++ b/tests/components/switchbot_cloud/test_config_flow.py @@ -0,0 +1,90 @@ +"""Test the SwitchBot via API config flow.""" +from unittest.mock import AsyncMock, patch + +import pytest + +from homeassistant import config_entries +from homeassistant.components.switchbot_cloud.config_flow import ( + CannotConnect, + InvalidAuth, +) +from homeassistant.components.switchbot_cloud.const import DOMAIN, ENTRY_TITLE +from homeassistant.const import CONF_API_KEY, CONF_API_TOKEN +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType + + +async def _fill_out_form_and_assert_entry_created( + hass: HomeAssistant, flow_id: str, mock_setup_entry: AsyncMock +) -> None: + """Util function to fill out a form and assert that a config entry is created.""" + with patch( + "homeassistant.components.switchbot_cloud.config_flow.SwitchBotAPI.list_devices", + return_value=[], + ): + result_configure = await hass.config_entries.flow.async_configure( + flow_id, + { + CONF_API_TOKEN: "test-token", + CONF_API_KEY: "test-secret-key", + }, + ) + await hass.async_block_till_done() + + assert result_configure["type"] == FlowResultType.CREATE_ENTRY + assert result_configure["title"] == ENTRY_TITLE + assert result_configure["data"] == { + CONF_API_TOKEN: "test-token", + CONF_API_KEY: "test-secret-key", + } + mock_setup_entry.assert_called_once() + + +async def test_form(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test we get the form.""" + result_init = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result_init["type"] == FlowResultType.FORM + assert not result_init["errors"] + + await _fill_out_form_and_assert_entry_created( + hass, result_init["flow_id"], mock_setup_entry + ) + + +@pytest.mark.parametrize( + ("error", "message"), + [ + (InvalidAuth, "invalid_auth"), + (CannotConnect, "cannot_connect"), + (Exception, "unknown"), + ], +) +async def test_form_fails( + hass: HomeAssistant, error: Exception, message: str, mock_setup_entry: AsyncMock +) -> None: + """Test we handle error cases.""" + result_init = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + with patch( + "homeassistant.components.switchbot_cloud.config_flow.SwitchBotAPI.list_devices", + side_effect=error, + ): + result_configure = await hass.config_entries.flow.async_configure( + result_init["flow_id"], + { + CONF_API_TOKEN: "test-token", + CONF_API_KEY: "test-secret-key", + }, + ) + + assert result_configure["type"] == FlowResultType.FORM + assert result_configure["errors"] == {"base": message} + await hass.async_block_till_done() + + await _fill_out_form_and_assert_entry_created( + hass, result_init["flow_id"], mock_setup_entry + ) diff --git a/tests/components/switchbot_cloud/test_init.py b/tests/components/switchbot_cloud/test_init.py new file mode 100644 index 00000000000000..48f0021bdb46d2 --- /dev/null +++ b/tests/components/switchbot_cloud/test_init.py @@ -0,0 +1,100 @@ +"""Tests for the SwitchBot Cloud integration init.""" + +from unittest.mock import patch + +import pytest +from switchbot_api import CannotConnect, Device, InvalidAuth, PowerState + +from homeassistant.components.switchbot_cloud import SwitchBotAPI +from homeassistant.config_entries import ConfigEntryState +from homeassistant.const import EVENT_HOMEASSISTANT_START +from homeassistant.core import HomeAssistant + +from . import configure_integration + + +@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 + + +async def test_setup_entry_success( + hass: HomeAssistant, mock_list_devices, mock_get_status +) -> None: + """Test successful setup of entry.""" + mock_list_devices.return_value = [ + Device( + deviceId="test-id", + deviceName="test-name", + deviceType="Plug", + hubDeviceId="test-hub-id", + ) + ] + mock_get_status.return_value = {"power": PowerState.ON.value} + entry = configure_integration(hass) + await hass.config_entries.async_setup(entry.entry_id) + await hass.async_block_till_done() + assert entry.state == 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() + + +@pytest.mark.parametrize( + ("error", "state"), + [ + (InvalidAuth, ConfigEntryState.SETUP_ERROR), + (CannotConnect, 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 = configure_integration(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == 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( + deviceId="test-id", + deviceName="test-name", + deviceType="Plug", + hubDeviceId="test-hub-id", + ) + ] + mock_get_status.side_effect = CannotConnect + entry = configure_integration(hass) + await hass.config_entries.async_setup(entry.entry_id) + assert entry.state == 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()