From d88849fb04b46c7ee12b7bcaf1f512b39e9324e4 Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Mon, 30 Jan 2023 23:05:48 -0500 Subject: [PATCH] ESPHome handle remove password and no encryption (#86995) * ESPHome handle remove password and no encryption * Start reauth for invalid api password --------- Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- homeassistant/components/esphome/__init__.py | 10 ++++- .../components/esphome/config_flow.py | 13 ++++++ tests/components/esphome/test_config_flow.py | 45 +++++++++++-------- tests/components/esphome/test_dashboard.py | 7 ++- 4 files changed, 54 insertions(+), 21 deletions(-) diff --git a/homeassistant/components/esphome/__init__.py b/homeassistant/components/esphome/__init__.py index 8cefac41859..4c9d8b6362b 100644 --- a/homeassistant/components/esphome/__init__.py +++ b/homeassistant/components/esphome/__init__.py @@ -17,6 +17,7 @@ from aioesphomeapi import ( EntityInfo, EntityState, HomeassistantServiceCall, + InvalidAuthAPIError, InvalidEncryptionKeyAPIError, ReconnectLogic, RequiresEncryptionAPIError, @@ -347,7 +348,14 @@ async def async_setup_entry( # noqa: C901 async def on_connect_error(err: Exception) -> None: """Start reauth flow if appropriate connect error type.""" - if isinstance(err, (RequiresEncryptionAPIError, InvalidEncryptionKeyAPIError)): + if isinstance( + err, + ( + RequiresEncryptionAPIError, + InvalidEncryptionKeyAPIError, + InvalidAuthAPIError, + ), + ): entry.async_start_reauth(hass) reconnect_logic = ReconnectLogic( diff --git a/homeassistant/components/esphome/config_flow.py b/homeassistant/components/esphome/config_flow.py index 550697d5d12..acc94bc7ea0 100644 --- a/homeassistant/components/esphome/config_flow.py +++ b/homeassistant/components/esphome/config_flow.py @@ -92,6 +92,19 @@ class EsphomeFlowHandler(ConfigFlow, domain=DOMAIN): self._name = entry.title self._device_name = entry.data.get(CONF_DEVICE_NAME) + # Device without encryption allows fetching device info. We can then check + # if the device is no longer using a password. If we did try with a password, + # we know setting password to empty will allow us to authenticate. + error = await self.fetch_device_info() + if ( + error is None + and self._password + and self._device_info + and not self._device_info.uses_password + ): + self._password = "" + return await self._async_authenticate_or_add() + return await self.async_step_reauth_confirm() async def async_step_reauth_confirm( diff --git a/tests/components/esphome/test_config_flow.py b/tests/components/esphome/test_config_flow.py index 92e2df5ca39..b629e604410 100644 --- a/tests/components/esphome/test_config_flow.py +++ b/tests/components/esphome/test_config_flow.py @@ -519,23 +519,14 @@ async def test_reauth_fixed_via_dashboard( assert len(mock_get_encryption_key.mock_calls) == 1 -async def test_reauth_fixed_via_dashboard_remove_password( - hass, mock_client, mock_zeroconf, mock_dashboard +async def test_reauth_fixed_via_dashboard_add_encryption_remove_password( + hass, mock_client, mock_zeroconf, mock_dashboard, mock_config_entry ): """Test reauth fixed automatically via dashboard with password removed.""" - - entry = MockConfigEntry( - domain=DOMAIN, - data={ - CONF_HOST: "127.0.0.1", - CONF_PORT: 6053, - CONF_PASSWORD: "hello", - CONF_DEVICE_NAME: "test", - }, + mock_client.device_info.side_effect = ( + InvalidAuthAPIError, + DeviceInfo(uses_password=False, name="test"), ) - entry.add_to_hass(hass) - - mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") mock_dashboard["configured"].append( { @@ -554,19 +545,37 @@ async def test_reauth_fixed_via_dashboard_remove_password( "esphome", context={ "source": config_entries.SOURCE_REAUTH, - "entry_id": entry.entry_id, - "unique_id": entry.unique_id, + "entry_id": mock_config_entry.entry_id, + "unique_id": mock_config_entry.unique_id, }, ) assert result["type"] == FlowResultType.ABORT, result assert result["reason"] == "reauth_successful" - assert entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK - assert entry.data[CONF_PASSWORD] == "" + assert mock_config_entry.data[CONF_NOISE_PSK] == VALID_NOISE_PSK + assert mock_config_entry.data[CONF_PASSWORD] == "" assert len(mock_get_encryption_key.mock_calls) == 1 +async def test_reauth_fixed_via_remove_password(hass, mock_client, mock_config_entry): + """Test reauth fixed automatically by seeing password removed.""" + mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") + + result = await hass.config_entries.flow.async_init( + "esphome", + context={ + "source": config_entries.SOURCE_REAUTH, + "entry_id": mock_config_entry.entry_id, + "unique_id": mock_config_entry.unique_id, + }, + ) + + assert result["type"] == FlowResultType.ABORT, result + assert result["reason"] == "reauth_successful" + assert mock_config_entry.data[CONF_PASSWORD] == "" + + async def test_reauth_fixed_via_dashboard_at_confirm( hass, mock_client, mock_zeroconf, mock_dashboard ): diff --git a/tests/components/esphome/test_dashboard.py b/tests/components/esphome/test_dashboard.py index c14bb06d3d8..0960556503a 100644 --- a/tests/components/esphome/test_dashboard.py +++ b/tests/components/esphome/test_dashboard.py @@ -1,7 +1,7 @@ """Test ESPHome dashboard features.""" from unittest.mock import patch -from aioesphomeapi import DeviceInfo +from aioesphomeapi import DeviceInfo, InvalidAuthAPIError from homeassistant.components.esphome import CONF_NOISE_PSK, dashboard from homeassistant.config_entries import SOURCE_REAUTH, ConfigEntryState @@ -31,7 +31,10 @@ async def test_new_dashboard_fix_reauth( hass, mock_client, mock_config_entry, mock_dashboard ): """Test config entries waiting for reauth are triggered.""" - mock_client.device_info.return_value = DeviceInfo(uses_password=False, name="test") + mock_client.device_info.side_effect = ( + InvalidAuthAPIError, + DeviceInfo(uses_password=False, name="test"), + ) with patch( "homeassistant.components.esphome.dashboard.ESPHomeDashboardAPI.get_encryption_key",