diff --git a/homeassistant/components/plugwise/config_flow.py b/homeassistant/components/plugwise/config_flow.py index eb0c56115c8..e611199ccf6 100644 --- a/homeassistant/components/plugwise/config_flow.py +++ b/homeassistant/components/plugwise/config_flow.py @@ -2,13 +2,14 @@ from __future__ import annotations import logging +from typing import Any from plugwise.exceptions import InvalidAuthentication, PlugwiseException from plugwise.smile import Smile import voluptuous as vol -from homeassistant import config_entries, core, exceptions -from homeassistant.components import zeroconf +from homeassistant.components.zeroconf import ZeroconfServiceInfo +from homeassistant.config_entries import ConfigFlow from homeassistant.const import ( CONF_BASE, CONF_HOST, @@ -17,6 +18,7 @@ from homeassistant.const import ( CONF_PORT, CONF_USERNAME, ) +from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -25,11 +27,8 @@ from .const import ( DEFAULT_PORT, DEFAULT_USERNAME, DOMAIN, - FLOW_NET, FLOW_SMILE, FLOW_STRETCH, - FLOW_TYPE, - FLOW_USB, PW_TYPE, SMILE, STRETCH, @@ -39,14 +38,6 @@ from .const import ( _LOGGER = logging.getLogger(__name__) -CONF_MANUAL_PATH = "Enter Manually" - -CONNECTION_SCHEMA = vol.Schema( - {vol.Required(FLOW_TYPE, default=FLOW_NET): vol.In([FLOW_NET, FLOW_USB])} -) - -# PLACEHOLDER USB connection validation - def _base_gw_schema(discovery_info): """Generate base schema for gateways.""" @@ -64,14 +55,13 @@ def _base_gw_schema(discovery_info): return vol.Schema(base_gw_schema) -async def validate_gw_input(hass: core.HomeAssistant, data): +async def validate_gw_input(hass: HomeAssistant, data: dict[str, Any]) -> Smile: """ Validate whether the user input allows us to connect to the gateway. Data has the keys from _base_gw_schema() with values provided by the user. """ websession = async_get_clientsession(hass, verify_ssl=False) - api = Smile( host=data[CONF_HOST], password=data[CONF_PASSWORD], @@ -80,35 +70,25 @@ async def validate_gw_input(hass: core.HomeAssistant, data): timeout=30, websession=websession, ) - - try: - await api.connect() - except InvalidAuthentication as err: - raise InvalidAuth from err - except PlugwiseException as err: - raise CannotConnect from err - + await api.connect() return api -class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class PlugwiseConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Plugwise Smile.""" VERSION = 1 - def __init__(self): - """Initialize the Plugwise config flow.""" - self.discovery_info: zeroconf.ZeroconfServiceInfo | None = None - self._username: str = DEFAULT_USERNAME + discovery_info: ZeroconfServiceInfo | None = None + _username: str = DEFAULT_USERNAME async def async_step_zeroconf( - self, discovery_info: zeroconf.ZeroconfServiceInfo + self, discovery_info: ZeroconfServiceInfo ) -> FlowResult: """Prepare configuration for a discovered Plugwise Smile.""" self.discovery_info = discovery_info _properties = discovery_info.properties - # unique_id is needed here, to be able to determine whether the discovered device is known, or not. unique_id = discovery_info.hostname.split(".")[0] await self.async_set_unique_id(unique_id) self._abort_if_unique_id_configured({CONF_HOST: discovery_info.host}) @@ -125,18 +105,15 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): CONF_PORT: discovery_info.port, CONF_USERNAME: self._username, } - return await self.async_step_user_gateway() + return await self.async_step_user() - # PLACEHOLDER USB step_user - - async def async_step_user_gateway(self, user_input=None): + async def async_step_user( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: """Handle the initial step when using network/gateway setups.""" - api = None errors = {} if user_input is not None: - user_input.pop(FLOW_TYPE, None) - if self.discovery_info: user_input[CONF_HOST] = self.discovery_info.host user_input[CONF_PORT] = self.discovery_info.port @@ -144,16 +121,14 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: api = await validate_gw_input(self.hass, user_input) - - except CannotConnect: - errors[CONF_BASE] = "cannot_connect" - except InvalidAuth: + except InvalidAuthentication: errors[CONF_BASE] = "invalid_auth" + except PlugwiseException: + errors[CONF_BASE] = "cannot_connect" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors[CONF_BASE] = "unknown" - - if not errors: + else: await self.async_set_unique_id( api.smile_hostname or api.gateway_id, raise_on_progress=False ) @@ -163,30 +138,7 @@ class PlugwiseConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_create_entry(title=api.smile_name, data=user_input) return self.async_show_form( - step_id="user_gateway", + step_id="user", data_schema=_base_gw_schema(self.discovery_info), errors=errors, ) - - async def async_step_user(self, user_input=None): - """Handle the initial step when using network/gateway setups.""" - errors = {} - if user_input is not None: - if user_input[FLOW_TYPE] == FLOW_NET: - return await self.async_step_user_gateway() - - # PLACEHOLDER for USB_FLOW - - return self.async_show_form( - step_id="user", - data_schema=CONNECTION_SCHEMA, - errors=errors, - ) - - -class CannotConnect(exceptions.HomeAssistantError): - """Error to indicate we cannot connect.""" - - -class InvalidAuth(exceptions.HomeAssistantError): - """Error to indicate there is invalid auth.""" diff --git a/homeassistant/components/plugwise/const.py b/homeassistant/components/plugwise/const.py index 772bccd92c1..cd31255a040 100644 --- a/homeassistant/components/plugwise/const.py +++ b/homeassistant/components/plugwise/const.py @@ -8,11 +8,9 @@ ATTR_ILLUMINANCE = "illuminance" COORDINATOR = "coordinator" DEVICE_STATE = "device_state" DOMAIN = "plugwise" -FLOW_NET = "Network: Smile/Stretch" FLOW_SMILE = "smile (Adam/Anna/P1)" FLOW_STRETCH = "stretch (Stretch)" FLOW_TYPE = "flow_type" -FLOW_USB = "USB: Stick - Coming soon" GATEWAY = "gateway" PW_TYPE = "plugwise_type" SCHEDULE_OFF = "false" diff --git a/tests/components/plugwise/conftest.py b/tests/components/plugwise/conftest.py index 06f9b56e689..65415285de2 100644 --- a/tests/components/plugwise/conftest.py +++ b/tests/components/plugwise/conftest.py @@ -1,9 +1,11 @@ """Setup mocks for the Plugwise integration tests.""" +from __future__ import annotations +from collections.abc import Generator from functools import partial from http import HTTPStatus import re -from unittest.mock import AsyncMock, Mock, patch +from unittest.mock import AsyncMock, MagicMock, Mock, patch import jsonpickle from plugwise.exceptions import ( @@ -24,16 +26,27 @@ def _read_json(environment, call): return jsonpickle.decode(fixture) -@pytest.fixture(name="mock_smile") -def mock_smile(): - """Create a Mock Smile for testing exceptions.""" +@pytest.fixture +def mock_setup_entry() -> Generator[AsyncMock, None, None]: + """Mock setting up a config entry.""" + with patch( + "homeassistant.components.plugwise.async_setup_entry", return_value=True + ) as mock_setup: + yield mock_setup + + +@pytest.fixture() +def mock_smile_config_flow() -> Generator[None, MagicMock, None]: + """Return a mocked Smile client.""" with patch( "homeassistant.components.plugwise.config_flow.Smile", + autospec=True, ) as smile_mock: - smile_mock.InvalidAuthentication = InvalidAuthentication - smile_mock.ConnectionFailedError = ConnectionFailedError - smile_mock.return_value.connect.return_value = True - yield smile_mock.return_value + smile = smile_mock.return_value + smile.smile_hostname = "smile12345" + smile.smile_name = "Test Smile Name" + smile.connect.return_value = True + yield smile @pytest.fixture(name="mock_smile_unauth") diff --git a/tests/components/plugwise/test_config_flow.py b/tests/components/plugwise/test_config_flow.py index 284be67a386..3e9ce985a5c 100644 --- a/tests/components/plugwise/test_config_flow.py +++ b/tests/components/plugwise/test_config_flow.py @@ -1,5 +1,5 @@ """Test the Plugwise config flow.""" -from unittest.mock import AsyncMock, patch +from unittest.mock import AsyncMock, MagicMock, patch from plugwise.exceptions import ( ConnectionFailedError, @@ -8,15 +8,8 @@ from plugwise.exceptions import ( ) import pytest -from homeassistant.components import zeroconf -from homeassistant.components.plugwise.const import ( - API, - DEFAULT_PORT, - DOMAIN, - FLOW_NET, - FLOW_TYPE, - PW_TYPE, -) +from homeassistant.components.plugwise.const import API, DEFAULT_PORT, DOMAIN, PW_TYPE +from homeassistant.components.zeroconf import ZeroconfServiceInfo from homeassistant.config_entries import SOURCE_USER, SOURCE_ZEROCONF from homeassistant.const import ( CONF_HOST, @@ -26,7 +19,12 @@ from homeassistant.const import ( CONF_SOURCE, CONF_USERNAME, ) -from homeassistant.data_entry_flow import RESULT_TYPE_CREATE_ENTRY, RESULT_TYPE_FORM +from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import ( + RESULT_TYPE_ABORT, + RESULT_TYPE_CREATE_ENTRY, + RESULT_TYPE_FORM, +) from tests.common import MockConfigEntry @@ -38,7 +36,7 @@ TEST_PORT = 81 TEST_USERNAME = "smile" TEST_USERNAME2 = "stretch" -TEST_DISCOVERY = zeroconf.ZeroconfServiceInfo( +TEST_DISCOVERY = ZeroconfServiceInfo( host=TEST_HOST, hostname=f"{TEST_HOSTNAME}.local.", name="mock_name", @@ -50,7 +48,8 @@ TEST_DISCOVERY = zeroconf.ZeroconfServiceInfo( }, type="mock_type", ) -TEST_DISCOVERY2 = zeroconf.ZeroconfServiceInfo( + +TEST_DISCOVERY2 = ZeroconfServiceInfo( host=TEST_HOST, hostname=f"{TEST_HOSTNAME2}.local.", name="mock_name", @@ -77,49 +76,32 @@ def mock_smile(): yield smile_mock.return_value -async def test_form_flow_gateway(hass): - """Test we get the form for Plugwise Gateway product type.""" - +async def test_form( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_smile_config_flow: MagicMock, +) -> None: + """Test the full user configuration flow.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_USER} ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {} - assert result["step_id"] == "user" + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("errors") == {} + assert result.get("step_id") == "user" + assert "flow_id" in result - result = await hass.config_entries.flow.async_configure( - result["flow_id"], user_input={FLOW_TYPE: FLOW_NET} + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + CONF_HOST: TEST_HOST, + CONF_PASSWORD: TEST_PASSWORD, + }, ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {} - assert result["step_id"] == "user_gateway" - - -async def test_form(hass): - """Test we get the form.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET} - ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {} - - with patch( - "homeassistant.components.plugwise.config_flow.Smile.connect", - return_value=True, - ), patch( - "homeassistant.components.plugwise.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, - ) - await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["data"] == { + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "Test Smile Name" + assert result2.get("data") == { CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD, CONF_PORT: DEFAULT_PORT, @@ -128,72 +110,79 @@ async def test_form(hass): } assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_smile_config_flow.connect.mock_calls) == 1 -async def test_zeroconf_form(hass): - """Test we get the form.""" - +@pytest.mark.parametrize( + "discovery,username", + [ + (TEST_DISCOVERY, TEST_USERNAME), + (TEST_DISCOVERY2, TEST_USERNAME2), + ], +) +async def test_zeroconf_flow( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_smile_config_flow: MagicMock, + discovery: ZeroconfServiceInfo, + username: str, +) -> None: + """Test config flow for smile devices.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_ZEROCONF}, - data=TEST_DISCOVERY, + data=discovery, ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {} - - with patch( - "homeassistant.components.plugwise.config_flow.Smile.connect", - return_value=True, - ), patch( - "homeassistant.components.plugwise.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_PASSWORD: TEST_PASSWORD}, - ) + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("errors") == {} + assert result.get("step_id") == "user" + assert "flow_id" in result + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: TEST_PASSWORD}, + ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["data"] == { + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "Test Smile Name" + assert result2.get("data") == { CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD, CONF_PORT: DEFAULT_PORT, - CONF_USERNAME: TEST_USERNAME, + CONF_USERNAME: username, PW_TYPE: API, } assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_smile_config_flow.connect.mock_calls) == 1 -async def test_zeroconf_stretch_form(hass): - """Test we get the form.""" - +async def test_zeroconf_flow_stretch( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_smile_config_flow: MagicMock, +) -> None: + """Test config flow for stretch devices.""" result = await hass.config_entries.flow.async_init( DOMAIN, context={CONF_SOURCE: SOURCE_ZEROCONF}, data=TEST_DISCOVERY2, ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {} - - with patch( - "homeassistant.components.plugwise.config_flow.Smile.connect", - return_value=True, - ), patch( - "homeassistant.components.plugwise.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_PASSWORD: TEST_PASSWORD}, - ) + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("errors") == {} + assert result.get("step_id") == "user" + assert "flow_id" in result + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: TEST_PASSWORD}, + ) await hass.async_block_till_done() - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["data"] == { + assert result2.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result2.get("title") == "Test Smile Name" + assert result2.get("data") == { CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD, CONF_PORT: DEFAULT_PORT, @@ -202,9 +191,10 @@ async def test_zeroconf_stretch_form(hass): } assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_smile_config_flow.connect.mock_calls) == 1 -async def test_zercoconf_discovery_update_configuration(hass): +async def test_zercoconf_discovery_update_configuration(hass: HomeAssistant) -> None: """Test if a discovered device is configured and updated with new host.""" entry = MockConfigEntry( domain=DOMAIN, @@ -222,154 +212,65 @@ async def test_zercoconf_discovery_update_configuration(hass): data=TEST_DISCOVERY, ) - assert result["type"] == "abort" - assert result["reason"] == "already_configured" + assert result.get("type") == RESULT_TYPE_ABORT + assert result.get("reason") == "already_configured" assert entry.data[CONF_HOST] == "1.1.1.1" -async def test_form_username(hass): - """Test we get the username data back.""" - - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET} - ) - assert result["type"] == RESULT_TYPE_FORM - assert result["errors"] == {} - - with patch( - "homeassistant.components.plugwise.config_flow.Smile", - ) as smile_mock, patch( - "homeassistant.components.plugwise.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) - smile_mock.return_value.gateway_id = "abcdefgh12345678" - smile_mock.return_value.smile_hostname = TEST_HOST - smile_mock.return_value.smile_name = "Adam" - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: TEST_HOST, - CONF_PASSWORD: TEST_PASSWORD, - CONF_USERNAME: TEST_USERNAME2, - }, - ) - - await hass.async_block_till_done() - - assert result2["type"] == RESULT_TYPE_CREATE_ENTRY - assert result2["data"] == { - CONF_HOST: TEST_HOST, - CONF_PASSWORD: TEST_PASSWORD, - CONF_PORT: DEFAULT_PORT, - CONF_USERNAME: TEST_USERNAME2, - PW_TYPE: API, - } - - assert len(mock_setup_entry.mock_calls) == 1 - - result3 = await hass.config_entries.flow.async_init( - DOMAIN, - context={CONF_SOURCE: SOURCE_ZEROCONF}, - data=TEST_DISCOVERY, - ) - assert result3["type"] == RESULT_TYPE_FORM - - with patch( - "homeassistant.components.plugwise.config_flow.Smile", - ) as smile_mock, patch( - "homeassistant.components.plugwise.async_setup_entry", - return_value=True, - ) as mock_setup_entry: - smile_mock.return_value.side_effect = AsyncMock(return_value=True) - smile_mock.return_value.connect.side_effect = AsyncMock(return_value=True) - smile_mock.return_value.gateway_id = "abcdefgh12345678" - smile_mock.return_value.smile_hostname = TEST_HOST - smile_mock.return_value.smile_name = "Adam" - - result4 = await hass.config_entries.flow.async_configure( - result3["flow_id"], - user_input={CONF_PASSWORD: TEST_PASSWORD}, - ) - - await hass.async_block_till_done() - - assert result4["type"] == "abort" - assert result4["reason"] == "already_configured" - - -async def test_form_invalid_auth(hass, mock_smile): +@pytest.mark.parametrize( + "side_effect,reason", + [ + (InvalidAuthentication, "invalid_auth"), + (ConnectionFailedError, "cannot_connect"), + (PlugwiseException, "cannot_connect"), + (RuntimeError, "unknown"), + ], +) +async def test_flow_errors( + hass: HomeAssistant, + mock_setup_entry: AsyncMock, + mock_smile_config_flow: MagicMock, + side_effect: Exception, + reason: str, +) -> None: """Test we handle invalid auth.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET} + DOMAIN, + context={CONF_SOURCE: SOURCE_USER}, ) + assert result.get("type") == RESULT_TYPE_FORM + assert result.get("errors") == {} + assert result.get("step_id") == "user" + assert "flow_id" in result - mock_smile.connect.side_effect = InvalidAuthentication - mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a" - + mock_smile_config_flow.connect.side_effect = side_effect result2 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "invalid_auth"} + assert result2.get("type") == RESULT_TYPE_FORM + assert result2.get("errors") == {"base": reason} + assert result2.get("step_id") == "user" + assert len(mock_setup_entry.mock_calls) == 0 + assert len(mock_smile_config_flow.connect.mock_calls) == 1 -async def test_form_cannot_connect(hass, mock_smile): - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET} - ) - - mock_smile.connect.side_effect = ConnectionFailedError - mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a" - - result2 = await hass.config_entries.flow.async_configure( + mock_smile_config_flow.connect.side_effect = None + result3 = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, ) - assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "cannot_connect"} + assert result3.get("type") == RESULT_TYPE_CREATE_ENTRY + assert result3.get("title") == "Test Smile Name" + assert result3.get("data") == { + CONF_HOST: TEST_HOST, + CONF_PASSWORD: TEST_PASSWORD, + CONF_PORT: DEFAULT_PORT, + CONF_USERNAME: TEST_USERNAME, + PW_TYPE: API, + } - -async def test_form_cannot_connect_port(hass, mock_smile): - """Test we handle cannot connect to port error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET} - ) - - mock_smile.connect.side_effect = ConnectionFailedError - mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a" - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={ - CONF_HOST: TEST_HOST, - CONF_PASSWORD: TEST_PASSWORD, - CONF_PORT: TEST_PORT, - }, - ) - - assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "cannot_connect"} - - -async def test_form_other_problem(hass, mock_smile): - """Test we handle cannot connect error.""" - result = await hass.config_entries.flow.async_init( - DOMAIN, context={CONF_SOURCE: SOURCE_USER}, data={FLOW_TYPE: FLOW_NET} - ) - - mock_smile.connect.side_effect = TimeoutError - mock_smile.gateway_id = "0a636a4fc1704ab4a24e4f7e37fb187a" - - result2 = await hass.config_entries.flow.async_configure( - result["flow_id"], - user_input={CONF_HOST: TEST_HOST, CONF_PASSWORD: TEST_PASSWORD}, - ) - - assert result2["type"] == RESULT_TYPE_FORM - assert result2["errors"] == {"base": "unknown"} + assert len(mock_setup_entry.mock_calls) == 1 + assert len(mock_smile_config_flow.connect.mock_calls) == 2