diff --git a/homeassistant/components/elkm1/__init__.py b/homeassistant/components/elkm1/__init__.py index 146a9a21fb8..28c75f9c2ea 100644 --- a/homeassistant/components/elkm1/__init__.py +++ b/homeassistant/components/elkm1/__init__.py @@ -249,18 +249,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry): for keypad in elk.keypads: # pylint: disable=no-member keypad.add_callback(_element_changed) - if not await async_wait_for_elk_to_sync(elk, SYNC_TIMEOUT): - _LOGGER.error( - "Timed out after %d seconds while trying to sync with ElkM1 at %s", - SYNC_TIMEOUT, - conf[CONF_HOST], - ) - elk.disconnect() - raise ConfigEntryNotReady - - if elk.invalid_auth: - _LOGGER.error("Authentication failed for ElkM1") - return False + try: + if not await async_wait_for_elk_to_sync(elk, SYNC_TIMEOUT, conf[CONF_HOST]): + return False + except asyncio.TimeoutError as exc: + raise ConfigEntryNotReady from exc hass.data[DOMAIN][entry.entry_id] = { "elk": elk, @@ -312,16 +305,40 @@ async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry): return unload_ok -async def async_wait_for_elk_to_sync(elk, timeout): - """Wait until the elk system has finished sync.""" +async def async_wait_for_elk_to_sync(elk, timeout, conf_host): + """Wait until the elk has finished sync. Can fail login or timeout.""" + + def login_status(succeeded): + nonlocal success + + success = succeeded + if succeeded: + _LOGGER.info("ElkM1 login succeeded.") + else: + elk.disconnect() + _LOGGER.error("ElkM1 login failed; invalid username or password.") + event.set() + + def sync_complete(): + event.set() + + success = True + event = asyncio.Event() + elk.add_handler("login", login_status) + elk.add_handler("sync_complete", sync_complete) try: with async_timeout.timeout(timeout): - await elk.sync_complete() - return True + await event.wait() except asyncio.TimeoutError: + _LOGGER.error( + "Timed out after %d seconds while trying to sync with ElkM1 at %s", + SYNC_TIMEOUT, + conf_host, + ) elk.disconnect() + raise - return False + return success def _create_elk_services(hass): diff --git a/homeassistant/components/elkm1/config_flow.py b/homeassistant/components/elkm1/config_flow.py index 419e4df7552..0248025795b 100644 --- a/homeassistant/components/elkm1/config_flow.py +++ b/homeassistant/components/elkm1/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Elk-M1 Control integration.""" +import asyncio import logging from urllib.parse import urlparse @@ -65,20 +66,7 @@ async def validate_input(data): ) elk.connect() - timed_out = False - if not await async_wait_for_elk_to_sync(elk, VALIDATE_TIMEOUT): - _LOGGER.error( - "Timed out after %d seconds while trying to sync with ElkM1 at %s", - VALIDATE_TIMEOUT, - url, - ) - timed_out = True - - elk.disconnect() - - if timed_out: - raise CannotConnect - if elk.invalid_auth: + if not await async_wait_for_elk_to_sync(elk, VALIDATE_TIMEOUT, url): raise InvalidAuth device_name = data[CONF_PREFIX] if data[CONF_PREFIX] else "ElkM1" @@ -116,7 +104,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): try: info = await validate_input(user_input) - except CannotConnect: + except asyncio.TimeoutError: errors["base"] = "cannot_connect" except InvalidAuth: errors["base"] = "invalid_auth" @@ -161,9 +149,5 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return urlparse(url).hostname in existing_hosts -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/elkm1/manifest.json b/homeassistant/components/elkm1/manifest.json index 0e6fdcfa40c..d7e7e226ea0 100644 --- a/homeassistant/components/elkm1/manifest.json +++ b/homeassistant/components/elkm1/manifest.json @@ -2,7 +2,7 @@ "domain": "elkm1", "name": "Elk-M1 Control", "documentation": "https://www.home-assistant.io/integrations/elkm1", - "requirements": ["elkm1-lib==0.8.4"], + "requirements": ["elkm1-lib==0.8.5"], "codeowners": ["@gwww", "@bdraco"], "config_flow": true } diff --git a/requirements_all.txt b/requirements_all.txt index 9ed9c4f83f5..49b249edf3f 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -535,7 +535,7 @@ elgato==0.2.0 eliqonline==1.2.2 # homeassistant.components.elkm1 -elkm1-lib==0.8.4 +elkm1-lib==0.8.5 # homeassistant.components.mobile_app emoji==0.5.4 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index fcfb3166f02..163ca96cfa7 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -278,7 +278,7 @@ eebrightbox==0.0.4 elgato==0.2.0 # homeassistant.components.elkm1 -elkm1-lib==0.8.4 +elkm1-lib==0.8.5 # homeassistant.components.mobile_app emoji==0.5.4 diff --git a/tests/components/elkm1/test_config_flow.py b/tests/components/elkm1/test_config_flow.py index ba58d8cb68c..fdb4c63499c 100644 --- a/tests/components/elkm1/test_config_flow.py +++ b/tests/components/elkm1/test_config_flow.py @@ -3,14 +3,24 @@ from homeassistant import config_entries, setup from homeassistant.components.elkm1.const import DOMAIN -from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch +from tests.async_mock import MagicMock, patch def mock_elk(invalid_auth=None, sync_complete=None): """Mock m1lib Elk.""" + + def handler_callbacks(type_, callback): + nonlocal invalid_auth, sync_complete + + if type_ == "login": + if invalid_auth is not None: + callback(not invalid_auth) + elif type_ == "sync_complete": + if sync_complete: + callback() + mocked_elk = MagicMock() - type(mocked_elk).invalid_auth = PropertyMock(return_value=invalid_auth) - type(mocked_elk).sync_complete = AsyncMock() + mocked_elk.add_handler.side_effect = handler_callbacks return mocked_elk @@ -23,7 +33,7 @@ async def test_form_user_with_secure_elk(hass): assert result["type"] == "form" assert result["errors"] == {} - mocked_elk = mock_elk(invalid_auth=False) + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) with patch( "homeassistant.components.elkm1.config_flow.elkm1.Elk", @@ -70,7 +80,7 @@ async def test_form_user_with_non_secure_elk(hass): assert result["type"] == "form" assert result["errors"] == {} - mocked_elk = mock_elk(invalid_auth=False) + mocked_elk = mock_elk(invalid_auth=None, sync_complete=True) with patch( "homeassistant.components.elkm1.config_flow.elkm1.Elk", @@ -115,7 +125,7 @@ async def test_form_user_with_serial_elk(hass): assert result["type"] == "form" assert result["errors"] == {} - mocked_elk = mock_elk(invalid_auth=False) + mocked_elk = mock_elk(invalid_auth=None, sync_complete=True) with patch( "homeassistant.components.elkm1.config_flow.elkm1.Elk", @@ -153,19 +163,21 @@ async def test_form_user_with_serial_elk(hass): async def test_form_cannot_connect(hass): """Test we handle cannot connect error.""" + from asyncio import TimeoutError + result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mocked_elk = mock_elk(invalid_auth=False) + mocked_elk = mock_elk(invalid_auth=None, sync_complete=None) with patch( "homeassistant.components.elkm1.config_flow.elkm1.Elk", return_value=mocked_elk, ), patch( - "homeassistant.components.elkm1.config_flow.async_wait_for_elk_to_sync", - return_value=False, - ): # async_wait_for_elk_to_sync is being patched to avoid making the test wait 45s + "homeassistant.components.elkm1.async_timeout.timeout", + side_effect=TimeoutError, + ): result2 = await hass.config_entries.flow.async_configure( result["flow_id"], { @@ -188,7 +200,7 @@ async def test_form_invalid_auth(hass): DOMAIN, context={"source": config_entries.SOURCE_USER} ) - mocked_elk = mock_elk(invalid_auth=True) + mocked_elk = mock_elk(invalid_auth=True, sync_complete=True) with patch( "homeassistant.components.elkm1.config_flow.elkm1.Elk", @@ -214,7 +226,7 @@ async def test_form_import(hass): """Test we get the form with import source.""" await setup.async_setup_component(hass, "persistent_notification", {}) - mocked_elk = mock_elk(invalid_auth=False) + mocked_elk = mock_elk(invalid_auth=False, sync_complete=True) with patch( "homeassistant.components.elkm1.config_flow.elkm1.Elk", return_value=mocked_elk,