Add switchbot cloud integration (#99607)

* Switches via API

* Using external library

* UT and checlist

* Updating file .coveragerc

* Update homeassistant/components/switchbot_via_api/switch.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/switchbot_via_api/switch.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Update homeassistant/components/switchbot_via_api/switch.py

Co-authored-by: J. Nick Koston <nick@koston.org>

* Review fixes

* Apply suggestions from code review

Co-authored-by: J. Nick Koston <nick@koston.org>

* This base class shouldn't know about Remote

* Fixing suggestion

* Sometimes, the state from the API is not updated immediately

* Review changes

* Some review changes

* Review changes

* Review change: Adding type on commands

* Parameterizing some tests

* Review changes

* Updating .coveragerc

* Fixing error handling in coordinator

* Review changes

* Review changes

* Adding switchbot brand

* Apply suggestions from code review

Co-authored-by: J. Nick Koston <nick@koston.org>

* Review changes

* Adding strict typing

* Removing log in constructor

---------

Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
Ravaka Razafimanantsoa
2023-09-16 16:00:41 +02:00
committed by GitHub
co-authored by J. Nick Koston
parent 568974fcc4
commit f99dedfb42
22 changed files with 623 additions and 4 deletions
@@ -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
@@ -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
@@ -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
)
@@ -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()