diff --git a/.coveragerc b/.coveragerc index 1d37b7bc05564374f292c293b2ea5385d9c2352a..81f00ab696818a9a3ad4eb9e71b004dbcea4678e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -351,7 +351,8 @@ omit = homeassistant/components/hisense_aehw4a1/* homeassistant/components/hitron_coda/device_tracker.py homeassistant/components/hive/* - homeassistant/components/hlk_sw16/* + homeassistant/components/hlk_sw16/__init__.py + homeassistant/components/hlk_sw16/switch.py homeassistant/components/home_connect/* homeassistant/components/homematic/* homeassistant/components/homematic/climate.py diff --git a/CODEOWNERS b/CODEOWNERS index e69cf26f0734d095ad55de665e9407ba8d43dbe8..1d4d38fa4e17bcb6e17888680622ebd811d377ec 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -172,6 +172,7 @@ homeassistant/components/hikvisioncam/* @fbradyirl homeassistant/components/hisense_aehw4a1/* @bannhead homeassistant/components/history/* @home-assistant/core homeassistant/components/hive/* @Rendili @KJonline +homeassistant/components/hlk_sw16/* @jameshilliard homeassistant/components/home_connect/* @DavidMStraub homeassistant/components/homeassistant/* @home-assistant/core homeassistant/components/homekit/* @bdraco diff --git a/homeassistant/components/hlk_sw16/__init__.py b/homeassistant/components/hlk_sw16/__init__.py index 3319ce6bee7bf4a45b6c6dcf22ba295f2f4f25b8..91b269cc52037bf6adbca21e273f024638e3f370 100644 --- a/homeassistant/components/hlk_sw16/__init__.py +++ b/homeassistant/components/hlk_sw16/__init__.py @@ -4,31 +4,28 @@ import logging from hlk_sw16 import create_hlk_sw16_connection import voluptuous as vol -from homeassistant.const import ( - CONF_HOST, - CONF_NAME, - CONF_PORT, - CONF_SWITCHES, - EVENT_HOMEASSISTANT_STOP, -) +from homeassistant.config_entries import SOURCE_IMPORT +from homeassistant.const import CONF_HOST, CONF_NAME, CONF_PORT, CONF_SWITCHES from homeassistant.core import callback import homeassistant.helpers.config_validation as cv -from homeassistant.helpers.discovery import async_load_platform from homeassistant.helpers.dispatcher import ( async_dispatcher_connect, async_dispatcher_send, ) from homeassistant.helpers.entity import Entity +from .const import ( + CONNECTION_TIMEOUT, + DEFAULT_KEEP_ALIVE_INTERVAL, + DEFAULT_PORT, + DEFAULT_RECONNECT_INTERVAL, + DOMAIN, +) + _LOGGER = logging.getLogger(__name__) DATA_DEVICE_REGISTER = "hlk_sw16_device_register" -DEFAULT_RECONNECT_INTERVAL = 10 -DEFAULT_KEEP_ALIVE_INTERVAL = 3 -CONNECTION_TIMEOUT = 10 -DEFAULT_PORT = 8080 - -DOMAIN = "hlk_sw16" +DATA_DEVICE_LISTENER = "hlk_sw16_device_listener" SWITCH_SCHEMA = vol.Schema({vol.Optional(CONF_NAME): cv.string}) @@ -57,84 +54,112 @@ CONFIG_SCHEMA = vol.Schema( async def async_setup(hass, config): - """Set up the HLK-SW16 switch.""" - # Allow platform to specify function to register new unknown devices - - hass.data[DATA_DEVICE_REGISTER] = {} - - def add_device(device): - switches = config[DOMAIN][device][CONF_SWITCHES] - - host = config[DOMAIN][device][CONF_HOST] - port = config[DOMAIN][device][CONF_PORT] - - @callback - def disconnected(): - """Schedule reconnect after connection has been lost.""" - _LOGGER.warning("HLK-SW16 %s disconnected", device) - async_dispatcher_send(hass, f"hlk_sw16_device_available_{device}", False) - - @callback - def reconnected(): - """Schedule reconnect after connection has been lost.""" - _LOGGER.warning("HLK-SW16 %s connected", device) - async_dispatcher_send(hass, f"hlk_sw16_device_available_{device}", True) - - async def connect(): - """Set up connection and hook it into HA for reconnect/shutdown.""" - _LOGGER.info("Initiating HLK-SW16 connection to %s", device) - - client = await create_hlk_sw16_connection( - host=host, - port=port, - disconnect_callback=disconnected, - reconnect_callback=reconnected, - loop=hass.loop, - timeout=CONNECTION_TIMEOUT, - reconnect_interval=DEFAULT_RECONNECT_INTERVAL, - keep_alive_interval=DEFAULT_KEEP_ALIVE_INTERVAL, + """Component setup, do nothing.""" + if DOMAIN not in config: + return True + + for device_id in config[DOMAIN]: + conf = config[DOMAIN][device_id] + hass.async_create_task( + hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_IMPORT}, + data={CONF_HOST: conf[CONF_HOST], CONF_PORT: conf[CONF_PORT]}, ) + ) + return True - hass.data[DATA_DEVICE_REGISTER][device] = client - # Load platforms - hass.async_create_task( - async_load_platform(hass, "switch", DOMAIN, (switches, device), config) - ) +async def async_setup_entry(hass, entry): + """Set up the HLK-SW16 switch.""" + hass.data.setdefault(DOMAIN, {}) + host = entry.data[CONF_HOST] + port = entry.data[CONF_PORT] + address = f"{host}:{port}" - # handle shutdown of HLK-SW16 asyncio transport - hass.bus.async_listen_once( - EVENT_HOMEASSISTANT_STOP, lambda x: client.stop() - ) + hass.data[DOMAIN][entry.entry_id] = {} + + @callback + def disconnected(): + """Schedule reconnect after connection has been lost.""" + _LOGGER.warning("HLK-SW16 %s disconnected", address) + async_dispatcher_send( + hass, f"hlk_sw16_device_available_{entry.entry_id}", False + ) + + @callback + def reconnected(): + """Schedule reconnect after connection has been lost.""" + _LOGGER.warning("HLK-SW16 %s connected", address) + async_dispatcher_send(hass, f"hlk_sw16_device_available_{entry.entry_id}", True) + + async def connect(): + """Set up connection and hook it into HA for reconnect/shutdown.""" + _LOGGER.info("Initiating HLK-SW16 connection to %s", address) + + client = await create_hlk_sw16_connection( + host=host, + port=port, + disconnect_callback=disconnected, + reconnect_callback=reconnected, + loop=hass.loop, + timeout=CONNECTION_TIMEOUT, + reconnect_interval=DEFAULT_RECONNECT_INTERVAL, + keep_alive_interval=DEFAULT_KEEP_ALIVE_INTERVAL, + ) + + hass.data[DOMAIN][entry.entry_id][DATA_DEVICE_REGISTER] = client + + # Load entities + hass.async_create_task( + hass.config_entries.async_forward_entry_setup(entry, "switch") + ) - _LOGGER.info("Connected to HLK-SW16 device: %s", device) + _LOGGER.info("Connected to HLK-SW16 device: %s", address) - hass.loop.create_task(connect()) + hass.loop.create_task(connect()) - for device in config[DOMAIN]: - add_device(device) return True +async def async_unload_entry(hass, entry): + """Unload a config entry.""" + client = hass.data[DOMAIN][entry.entry_id].pop(DATA_DEVICE_REGISTER) + client.stop() + unload_ok = await hass.config_entries.async_forward_entry_unload(entry, "switch") + + if unload_ok: + if hass.data[DOMAIN][entry.entry_id]: + hass.data[DOMAIN].pop(entry.entry_id) + if not hass.data[DOMAIN]: + hass.data.pop(DOMAIN) + return unload_ok + + class SW16Device(Entity): """Representation of a HLK-SW16 device. Contains the common logic for HLK-SW16 entities. """ - def __init__(self, relay_name, device_port, device_id, client): + def __init__(self, device_port, entry_id, client): """Initialize the device.""" # HLK-SW16 specific attributes for every component type - self._device_id = device_id + self._entry_id = entry_id self._device_port = device_port self._is_on = None self._client = client - self._name = relay_name + self._name = device_port + + @property + def unique_id(self): + """Return a unique ID.""" + return f"{self._entry_id}_{self._device_port}" @callback def handle_event_callback(self, event): """Propagate changes through ha.""" - _LOGGER.debug("Relay %s new state callback: %r", self._device_port, event) + _LOGGER.debug("Relay %s new state callback: %r", self.unique_id, event) self._is_on = event self.async_write_ha_state() @@ -167,7 +192,7 @@ class SW16Device(Entity): self.async_on_remove( async_dispatcher_connect( self.hass, - f"hlk_sw16_device_available_{self._device_id}", + f"hlk_sw16_device_available_{self._entry_id}", self._availability_callback, ) ) diff --git a/homeassistant/components/hlk_sw16/config_flow.py b/homeassistant/components/hlk_sw16/config_flow.py new file mode 100644 index 0000000000000000000000000000000000000000..0a9ac79d1b701fc9f798fa7d1a158b1abd38d172 --- /dev/null +++ b/homeassistant/components/hlk_sw16/config_flow.py @@ -0,0 +1,96 @@ +"""Config flow for HLK-SW16.""" +import asyncio + +from hlk_sw16 import create_hlk_sw16_connection +import voluptuous as vol + +from homeassistant import config_entries +from homeassistant.const import CONF_HOST, CONF_PORT +from homeassistant.core import HomeAssistant + +from .const import ( + CONNECTION_TIMEOUT, + DEFAULT_KEEP_ALIVE_INTERVAL, + DEFAULT_PORT, + DEFAULT_RECONNECT_INTERVAL, + DOMAIN, +) +from .errors import AlreadyConfigured, CannotConnect + +DATA_SCHEMA = vol.Schema( + { + vol.Required(CONF_HOST): str, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): vol.Coerce(int), + } +) + + +async def connect_client(hass, user_input): + """Connect the HLK-SW16 client.""" + client_aw = create_hlk_sw16_connection( + host=user_input[CONF_HOST], + port=user_input[CONF_PORT], + loop=hass.loop, + timeout=CONNECTION_TIMEOUT, + reconnect_interval=DEFAULT_RECONNECT_INTERVAL, + keep_alive_interval=DEFAULT_KEEP_ALIVE_INTERVAL, + ) + return await asyncio.wait_for(client_aw, timeout=CONNECTION_TIMEOUT) + + +async def validate_input(hass: HomeAssistant, user_input): + """Validate the user input allows us to connect.""" + for entry in hass.config_entries.async_entries(DOMAIN): + if ( + entry.data[CONF_HOST] == user_input[CONF_HOST] + and entry.data[CONF_PORT] == user_input[CONF_PORT] + ): + raise AlreadyConfigured + + try: + client = await connect_client(hass, user_input) + except asyncio.TimeoutError: + raise CannotConnect + try: + + def disconnect_callback(): + if client.in_transaction: + client.active_transaction.set_exception(CannotConnect) + + client.disconnect_callback = disconnect_callback + await client.status() + except CannotConnect: + client.disconnect_callback = None + client.stop() + raise CannotConnect + else: + client.disconnect_callback = None + client.stop() + + +class SW16FlowHandler(config_entries.ConfigFlow, domain=DOMAIN): + """Handle a HLK-SW16 config flow.""" + + VERSION = 1 + CONNECTION_CLASS = config_entries.CONN_CLASS_LOCAL_PUSH + + async def async_step_import(self, user_input): + """Handle import.""" + return await self.async_step_user(user_input) + + async def async_step_user(self, user_input=None): + """Handle the initial step.""" + errors = {} + if user_input is not None: + try: + await validate_input(self.hass, user_input) + address = f"{user_input[CONF_HOST]}:{user_input[CONF_PORT]}" + return self.async_create_entry(title=address, data=user_input) + except AlreadyConfigured: + errors["base"] = "already_configured" + except CannotConnect: + errors["base"] = "cannot_connect" + + return self.async_show_form( + step_id="user", data_schema=DATA_SCHEMA, errors=errors + ) diff --git a/homeassistant/components/hlk_sw16/const.py b/homeassistant/components/hlk_sw16/const.py new file mode 100644 index 0000000000000000000000000000000000000000..22bc29e7599875d3bf86d63bb6d3213dbf80163d --- /dev/null +++ b/homeassistant/components/hlk_sw16/const.py @@ -0,0 +1,9 @@ +"""Constants for HLK-SW16 component.""" + +DOMAIN = "hlk_sw16" + +DEFAULT_NAME = "HLK-SW16" +DEFAULT_PORT = 8080 +DEFAULT_RECONNECT_INTERVAL = 10 +DEFAULT_KEEP_ALIVE_INTERVAL = 3 +CONNECTION_TIMEOUT = 10 diff --git a/homeassistant/components/hlk_sw16/errors.py b/homeassistant/components/hlk_sw16/errors.py new file mode 100644 index 0000000000000000000000000000000000000000..5b29587deba64ee7b9bf0d1e6eaa0d22710ac74c --- /dev/null +++ b/homeassistant/components/hlk_sw16/errors.py @@ -0,0 +1,14 @@ +"""Errors for the HLK-SW16 component.""" +from homeassistant.exceptions import HomeAssistantError + + +class SW16Exception(HomeAssistantError): + """Base class for HLK-SW16 exceptions.""" + + +class AlreadyConfigured(SW16Exception): + """HLK-SW16 is already configured.""" + + +class CannotConnect(SW16Exception): + """Unable to connect to the HLK-SW16.""" diff --git a/homeassistant/components/hlk_sw16/manifest.json b/homeassistant/components/hlk_sw16/manifest.json index 7574076fd433fbd7f081c80ae851577e4440cf09..aee829f593a1579ad64731b286e817fc20884cf1 100644 --- a/homeassistant/components/hlk_sw16/manifest.json +++ b/homeassistant/components/hlk_sw16/manifest.json @@ -2,6 +2,11 @@ "domain": "hlk_sw16", "name": "Hi-Link HLK-SW16", "documentation": "https://www.home-assistant.io/integrations/hlk_sw16", - "requirements": ["hlk-sw16==0.0.8"], - "codeowners": [] -} + "requirements": [ + "hlk-sw16==0.0.8" + ], + "codeowners": [ + "@jameshilliard" + ], + "config_flow": true +} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/strings.json b/homeassistant/components/hlk_sw16/strings.json new file mode 100644 index 0000000000000000000000000000000000000000..2480ac6091825080b61c9e18010b95f1d474fcc6 --- /dev/null +++ b/homeassistant/components/hlk_sw16/strings.json @@ -0,0 +1,21 @@ +{ + "config": { + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } + } + }, + "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%]" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/hlk_sw16/switch.py b/homeassistant/components/hlk_sw16/switch.py index e9c190678a652fa460a11b1f7e68e2db40983bed..9bd10ea765daf9a26253ad4644ad72d89e62cf9a 100644 --- a/homeassistant/components/hlk_sw16/switch.py +++ b/homeassistant/components/hlk_sw16/switch.py @@ -1,30 +1,30 @@ """Support for HLK-SW16 switches.""" -import logging - from homeassistant.components.switch import ToggleEntity -from homeassistant.const import CONF_NAME from . import DATA_DEVICE_REGISTER, SW16Device +from .const import DOMAIN + +PARALLEL_UPDATES = 0 -_LOGGER = logging.getLogger(__name__) +async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): + """Set up the HLK-SW16 switches.""" -def devices_from_config(hass, domain_config): + +def devices_from_entities(hass, entry): """Parse configuration and add HLK-SW16 switch devices.""" - switches = domain_config[0] - device_id = domain_config[1] - device_client = hass.data[DATA_DEVICE_REGISTER][device_id] + device_client = hass.data[DOMAIN][entry.entry_id][DATA_DEVICE_REGISTER] devices = [] - for device_port, device_config in switches.items(): - device_name = device_config.get(CONF_NAME, device_port) - device = SW16Switch(device_name, device_port, device_id, device_client) + for i in range(16): + device_port = f"{i:01x}" + device = SW16Switch(device_port, entry.entry_id, device_client) devices.append(device) return devices -async def async_setup_platform(hass, config, async_add_entities, discovery_info=None): +async def async_setup_entry(hass, entry, async_add_entities): """Set up the HLK-SW16 platform.""" - async_add_entities(devices_from_config(hass, discovery_info)) + async_add_entities(devices_from_entities(hass, entry)) class SW16Switch(SW16Device, ToggleEntity): diff --git a/homeassistant/components/hlk_sw16/translations/en.json b/homeassistant/components/hlk_sw16/translations/en.json new file mode 100644 index 0000000000000000000000000000000000000000..75ec99a55125fb0537c8473881006a47f5af6f8a --- /dev/null +++ b/homeassistant/components/hlk_sw16/translations/en.json @@ -0,0 +1,22 @@ +{ + "config": { + "abort": { + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + }, + "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%]" + }, + "step": { + "user": { + "data": { + "host": "[%key:common::config_flow::data::host%]", + "password": "[%key:common::config_flow::data::password%]", + "username": "[%key:common::config_flow::data::username%]" + } + } + } + }, + "title": "Hi-Link HLK-SW16" +} \ No newline at end of file diff --git a/homeassistant/generated/config_flows.py b/homeassistant/generated/config_flows.py index df56e071923dd79e6367d945e57cce963f2189d3..9fab383d7186218a88053879e3836f114976b2f1 100644 --- a/homeassistant/generated/config_flows.py +++ b/homeassistant/generated/config_flows.py @@ -71,6 +71,7 @@ FLOWS = [ "harmony", "heos", "hisense_aehw4a1", + "hlk_sw16", "home_connect", "homekit", "homekit_controller", diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 4a23e0dd8ec31c2cab4eb080340bfe14019b187c..506136abe329c20e122302d33666bbf194ff80db 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -352,6 +352,9 @@ hdate==0.9.5 # homeassistant.components.here_travel_time herepy==2.0.0 +# homeassistant.components.hlk_sw16 +hlk-sw16==0.0.8 + # homeassistant.components.pi_hole hole==0.5.1 diff --git a/tests/components/hlk_sw16/__init__.py b/tests/components/hlk_sw16/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..3b8278ee35336ac6a5706742cd86d6ccdebf519f --- /dev/null +++ b/tests/components/hlk_sw16/__init__.py @@ -0,0 +1 @@ +"""Tests for the Hi-Link HLK-SW16 integration.""" diff --git a/tests/components/hlk_sw16/test_config_flow.py b/tests/components/hlk_sw16/test_config_flow.py new file mode 100644 index 0000000000000000000000000000000000000000..6f9d5592893da9d7df814fd985edff4eb8b6e94f --- /dev/null +++ b/tests/components/hlk_sw16/test_config_flow.py @@ -0,0 +1,193 @@ +"""Test the Hi-Link HLK-SW16 config flow.""" +import asyncio + +from homeassistant import config_entries, setup +from homeassistant.components.hlk_sw16.const import DOMAIN + +from tests.async_mock import patch + + +class MockSW16Client: + """Class to mock the SW16Client client.""" + + def __init__(self, fail): + """Initialise client with failure modes.""" + self.fail = fail + self.disconnect_callback = None + self.in_transaction = False + self.active_transaction = None + + async def setup(self): + """Mock successful setup.""" + fut = asyncio.Future() + fut.set_result(True) + return fut + + async def status(self): + """Mock status based on failure mode.""" + self.in_transaction = True + self.active_transaction = asyncio.Future() + if self.fail: + if self.disconnect_callback: + self.disconnect_callback() + return await self.active_transaction + else: + self.active_transaction.set_result(True) + return self.active_transaction + + def stop(self): + """Mock client stop.""" + self.in_transaction = False + self.active_transaction = None + + +async def create_mock_hlk_sw16_connection(fail): + """Create a mock HLK-SW16 client.""" + client = MockSW16Client(fail) + await client.setup() + return client + + +async def test_form(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + conf = { + "host": "127.0.0.1", + "port": 8080, + } + + mock_hlk_sw16_connection = await create_mock_hlk_sw16_connection(False) + + with patch( + "homeassistant.components.hlk_sw16.config_flow.create_hlk_sw16_connection", + return_value=mock_hlk_sw16_connection, + ), patch( + "homeassistant.components.hlk_sw16.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.hlk_sw16.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], conf, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "127.0.0.1:8080" + assert result2["data"] == { + "host": "127.0.0.1", + "port": 8080, + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + mock_hlk_sw16_connection = await create_mock_hlk_sw16_connection(False) + + with patch( + "homeassistant.components.hlk_sw16.config_flow.create_hlk_sw16_connection", + return_value=mock_hlk_sw16_connection, + ): + result3 = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + assert result3["type"] == "form" + assert result3["errors"] == {} + + result4 = await hass.config_entries.flow.async_configure(result3["flow_id"], conf,) + + assert result4["type"] == "form" + assert result4["errors"] == {"base": "already_configured"} + await hass.async_block_till_done() + + +async def test_import(hass): + """Test we get the form.""" + await setup.async_setup_component(hass, "persistent_notification", {}) + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_IMPORT} + ) + assert result["type"] == "form" + assert result["errors"] == {} + + conf = { + "host": "127.0.0.1", + "port": 8080, + } + + mock_hlk_sw16_connection = await create_mock_hlk_sw16_connection(False) + + with patch( + "homeassistant.components.hlk_sw16.config_flow.connect_client", + return_value=mock_hlk_sw16_connection, + ), patch( + "homeassistant.components.hlk_sw16.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.hlk_sw16.async_setup_entry", return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], conf, + ) + + assert result2["type"] == "create_entry" + assert result2["title"] == "127.0.0.1:8080" + assert result2["data"] == { + "host": "127.0.0.1", + "port": 8080, + } + await hass.async_block_till_done() + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_form_invalid_data(hass): + """Test we handle invalid auth.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + mock_hlk_sw16_connection = await create_mock_hlk_sw16_connection(True) + + conf = { + "host": "127.0.0.1", + "port": 8080, + } + + with patch( + "homeassistant.components.hlk_sw16.config_flow.connect_client", + return_value=mock_hlk_sw16_connection, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], conf, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"} + + +async def test_form_cannot_connect(hass): + """Test we handle cannot connect error.""" + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_USER} + ) + + conf = { + "host": "127.0.0.1", + "port": 8080, + } + + with patch( + "homeassistant.components.hlk_sw16.config_flow.connect_client", + side_effect=asyncio.TimeoutError, + return_value=None, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], conf, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"base": "cannot_connect"}