diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 8b0dd26fc32..04a26f2822b 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -228,7 +228,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: _LOGGER.debug("Setting up elkm1 %s", conf["host"]) - if not entry.unique_id or ":" not in entry.unique_id and is_ip_address(host): + if (not entry.unique_id or ":" not in entry.unique_id) and is_ip_address(host): + _LOGGER.debug( + "Unique id for %s is missing during setup, trying to fill from discovery", + host, + ) if device := await async_discover_device(hass, host): async_update_entry_from_discovery(hass, entry, device) @@ -276,7 +280,7 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: try: if not await async_wait_for_elk_to_sync( - elk, LOGIN_TIMEOUT, SYNC_TIMEOUT, conf[CONF_HOST] + elk, LOGIN_TIMEOUT, SYNC_TIMEOUT, bool(conf[CONF_USERNAME]) ): return False except asyncio.TimeoutError as exc: @@ -327,7 +331,10 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: async def async_wait_for_elk_to_sync( - elk: elkm1.Elk, login_timeout: int, sync_timeout: int, conf_host: str + elk: elkm1.Elk, + login_timeout: int, + sync_timeout: int, + password_auth: bool, ) -> bool: """Wait until the elk has finished sync. Can fail login or timeout.""" @@ -353,15 +360,21 @@ async def async_wait_for_elk_to_sync( success = True elk.add_handler("login", login_status) elk.add_handler("sync_complete", sync_complete) - events = ((login_event, login_timeout), (sync_event, sync_timeout)) + events = [] + if password_auth: + events.append(("login", login_event, login_timeout)) + events.append(("sync_complete", sync_event, sync_timeout)) - for event, timeout in events: + for name, event, timeout in events: + _LOGGER.debug("Waiting for %s event for %s seconds", name, timeout) try: async with async_timeout.timeout(timeout): await event.wait() except asyncio.TimeoutError: + _LOGGER.debug("Timed out waiting for %s event", name) elk.disconnect() raise + _LOGGER.debug("Received %s event", name) return success diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index 2453958b3de..a21cf186005 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -24,6 +24,7 @@ from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers import device_registry as dr from homeassistant.helpers.typing import DiscoveryInfoType from homeassistant.util import slugify +from homeassistant.util.network import is_ip_address from . import async_wait_for_elk_to_sync from .const import CONF_AUTO_CONFIGURE, DISCOVER_SCAN_TIMEOUT, DOMAIN, LOGIN_TIMEOUT @@ -80,7 +81,9 @@ async def validate_input(data: dict[str, str], mac: str | None) -> dict[str, str ) elk.connect() - if not await async_wait_for_elk_to_sync(elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT, url): + if not await async_wait_for_elk_to_sync( + elk, LOGIN_TIMEOUT, VALIDATE_TIMEOUT, bool(userid) + ): raise InvalidAuth short_mac = _short_mac(mac) if mac else None @@ -124,6 +127,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._discovered_device = ElkSystem( discovery_info.macaddress, discovery_info.ip, 0 ) + _LOGGER.debug("Elk discovered from dhcp: %s", self._discovered_device) return await self._async_handle_discovery() async def async_step_integration_discovery( @@ -135,6 +139,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): discovery_info["ip_address"], discovery_info["port"], ) + _LOGGER.debug( + "Elk discovered from integration discovery: %s", self._discovered_device + ) return await self._async_handle_discovery() async def _async_handle_discovery(self) -> FlowResult: @@ -304,11 +311,22 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): async def async_step_import(self, user_input): """Handle import.""" - if device := await async_discover_device( - self.hass, urlparse(user_input[CONF_HOST]).hostname + _LOGGER.debug("Elk is importing from yaml") + url = _make_url_from_data(user_input) + + if self._url_already_configured(url): + return self.async_abort(reason="address_already_configured") + + host = urlparse(url).hostname + _LOGGER.debug( + "Importing is trying to fill unique id from discovery for %s", host + ) + if is_ip_address(host) and ( + device := await async_discover_device(self.hass, host) ): await self.async_set_unique_id(dr.format_mac(device.mac_address)) self._abort_if_unique_id_configured() + return (await self._async_create_or_error(user_input, True))[1] def _url_already_configured(self, url): diff --git a/homeassistant/components/elkm1/const.py b/homeassistant/components/elkm1/const.py index 80d594fce0a..fd4856bd5d5 100644 --- a/homeassistant/components/elkm1/const.py +++ b/homeassistant/components/elkm1/const.py @@ -9,7 +9,7 @@ from homeassistant.const import ATTR_CODE, CONF_ZONE DOMAIN = "elkm1" -LOGIN_TIMEOUT = 15 +LOGIN_TIMEOUT = 20 CONF_AUTO_CONFIGURE = "auto_configure" CONF_AREA = "area" diff --git a/homeassistant/components/elkm1/discovery.py b/homeassistant/components/elkm1/discovery.py index 7055f3958e9..326698c3686 100644 --- a/homeassistant/components/elkm1/discovery.py +++ b/homeassistant/components/elkm1/discovery.py @@ -29,9 +29,11 @@ def async_update_entry_from_discovery( ) -> bool: """Update a config entry from a discovery.""" if not entry.unique_id or ":" not in entry.unique_id: + _LOGGER.debug("Adding unique id from discovery: %s", device) return hass.config_entries.async_update_entry( entry, unique_id=dr.format_mac(device.mac_address) ) + _LOGGER.debug("Unique id is already present from discovery: %s", device) return False diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index d8a0feea670..49402d7b4d5 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -652,6 +652,50 @@ async def test_form_import_device_discovered(hass): assert len(mock_setup_entry.mock_calls) == 1 +async def test_form_import_existing(hass): + """Test we abort on existing import.""" + config_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_HOST: f"elks://{MOCK_IP_ADDRESS}"}, + unique_id="cc:cc:cc:cc:cc:cc", + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_IMPORT}, + data={ + "host": f"elks://{MOCK_IP_ADDRESS}", + "username": "friend", + "password": "love", + "temperature_unit": "C", + "auto_configure": False, + "keypad": { + "enabled": True, + "exclude": [], + "include": [[1, 1], [2, 2], [3, 3]], + }, + "output": {"enabled": False, "exclude": [], "include": []}, + "counter": {"enabled": False, "exclude": [], "include": []}, + "plc": {"enabled": False, "exclude": [], "include": []}, + "prefix": "ohana", + "setting": {"enabled": False, "exclude": [], "include": []}, + "area": {"enabled": False, "exclude": [], "include": []}, + "task": {"enabled": False, "exclude": [], "include": []}, + "thermostat": {"enabled": False, "exclude": [], "include": []}, + "zone": { + "enabled": True, + "exclude": [[15, 15], [28, 208]], + "include": [], + }, + }, + ) + await hass.async_block_till_done() + + assert result["type"] == RESULT_TYPE_ABORT + assert result["reason"] == "address_already_configured" + + @pytest.mark.parametrize( "source, data", [