diff --git a/homeassistant/components/honeywell/__init__.py b/homeassistant/components/honeywell/__init__.py index 3316d2852e7..c0ebcd9b1cf 100644 --- a/homeassistant/components/honeywell/__init__.py +++ b/homeassistant/components/honeywell/__init__.py @@ -7,7 +7,7 @@ import AIOSomecomfort from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers.aiohttp_client import async_get_clientsession from .const import ( @@ -57,15 +57,12 @@ async def async_setup_entry(hass: HomeAssistant, config_entry: ConfigEntry) -> b await client.login() await client.discover() - except AIOSomecomfort.AuthError as ex: - raise ConfigEntryNotReady( - "Failed to initialize the Honeywell client: " - "Check your configuration (username, password), " - ) from ex + except AIOSomecomfort.device.AuthError as ex: + raise ConfigEntryAuthFailed("Incorrect Password") from ex except ( - AIOSomecomfort.ConnectionError, - AIOSomecomfort.ConnectionTimeout, + AIOSomecomfort.device.ConnectionError, + AIOSomecomfort.device.ConnectionTimeout, asyncio.TimeoutError, ) as ex: raise ConfigEntryNotReady( diff --git a/homeassistant/components/honeywell/climate.py b/homeassistant/components/honeywell/climate.py index 9cc2bc1f14e..3467e5586ab 100644 --- a/homeassistant/components/honeywell/climate.py +++ b/homeassistant/components/honeywell/climate.py @@ -336,7 +336,6 @@ class HoneywellUSThermostat(ClimateEntity): await self._device.set_setpoint_heat(self._heat_away_temp) except AIOSomecomfort.SomeComfortError: - _LOGGER.error( "Temperature out of range. Mode: %s, Heat Temperature: %.1f, Cool Temperature: %.1f", mode, diff --git a/homeassistant/components/honeywell/config_flow.py b/homeassistant/components/honeywell/config_flow.py index 9f630d90fbe..0f9d5663b3e 100644 --- a/homeassistant/components/honeywell/config_flow.py +++ b/homeassistant/components/honeywell/config_flow.py @@ -2,6 +2,8 @@ from __future__ import annotations import asyncio +from collections.abc import Mapping +from typing import Any import AIOSomecomfort import voluptuous as vol @@ -20,11 +22,67 @@ from .const import ( DOMAIN, ) +REAUTH_SCHEMA = vol.Schema({vol.Required(CONF_PASSWORD): str}) + class HoneywellConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a honeywell config flow.""" VERSION = 1 + entry: config_entries.ConfigEntry | None + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle re-authentication with Honeywell.""" + + self.entry = self.hass.config_entries.async_get_entry(self.context["entry_id"]) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Confirm re-authentication with Honeywell.""" + errors: dict[str, str] = {} + + if user_input: + assert self.entry is not None + password = user_input[CONF_PASSWORD] + data = { + CONF_USERNAME: self.entry.data[CONF_USERNAME], + CONF_PASSWORD: password, + } + + try: + await self.is_valid( + username=data[CONF_USERNAME], password=data[CONF_PASSWORD] + ) + + except AIOSomecomfort.AuthError: + errors["base"] = "invalid_auth" + + except ( + AIOSomecomfort.ConnectionError, + AIOSomecomfort.ConnectionTimeout, + asyncio.TimeoutError, + ): + errors["base"] = "cannot_connect" + + else: + + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **self.entry.data, + CONF_PASSWORD: password, + }, + ) + await self.hass.config_entries.async_reload(self.entry.entry_id) + return self.async_abort(reason="reauth_successful") + + return self.async_show_form( + step_id="reauth_confirm", + data_schema=REAUTH_SCHEMA, + errors=errors, + ) async def async_step_user(self, user_input=None) -> FlowResult: """Create config entry. Show the setup form to the user.""" diff --git a/tests/components/honeywell/test_config_flow.py b/tests/components/honeywell/test_config_flow.py index 46ab48572f8..2d76962b028 100644 --- a/tests/components/honeywell/test_config_flow.py +++ b/tests/components/honeywell/test_config_flow.py @@ -1,7 +1,9 @@ """Tests for honeywell config flow.""" +import asyncio from unittest.mock import MagicMock, patch import AIOSomecomfort +import pytest from homeassistant import data_entry_flow from homeassistant.components.honeywell.const import ( @@ -9,8 +11,10 @@ from homeassistant.components.honeywell.const import ( CONF_HEAT_AWAY_TEMPERATURE, DOMAIN, ) -from homeassistant.config_entries import SOURCE_USER, ConfigEntryState +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER, ConfigEntryState +from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.data_entry_flow import FlowResultType from tests.common import MockConfigEntry @@ -35,8 +39,7 @@ async def test_show_authenticate_form(hass: HomeAssistant) -> None: async def test_connection_error(hass: HomeAssistant, client: MagicMock) -> None: """Test that an error message is shown on connection fail.""" - client.login.side_effect = AIOSomecomfort.ConnectionError - + client.login.side_effect = AIOSomecomfort.device.ConnectionError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG ) @@ -45,7 +48,7 @@ async def test_connection_error(hass: HomeAssistant, client: MagicMock) -> None: async def test_auth_error(hass: HomeAssistant, client: MagicMock) -> None: """Test that an error message is shown on login fail.""" - client.login.side_effect = AIOSomecomfort.AuthError + client.login.side_effect = AIOSomecomfort.device.AuthError result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=FAKE_CONFIG @@ -116,3 +119,137 @@ async def test_create_option_entry( CONF_COOL_AWAY_TEMPERATURE: 1, CONF_HEAT_AWAY_TEMPERATURE: 2, } + + +async def test_reauth_flow(hass: HomeAssistant) -> None: + """Test a successful reauth flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, + unique_id="test-username", + ) + mock_entry.add_to_hass(hass) + with patch( + "homeassistant.components.honeywell.async_setup_entry", + return_value=True, + ): + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password"}, + ) + + await hass.async_block_till_done() + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.honeywell.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.ABORT + assert result2["reason"] == "reauth_successful" + assert mock_entry.data == { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "new-password", + } + + +async def test_reauth_flow_auth_error(hass: HomeAssistant, client: MagicMock) -> None: + """Test an authorization error reauth flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, + unique_id="test-username", + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + client.login.side_effect = AIOSomecomfort.device.AuthError + with patch( + "homeassistant.components.honeywell.async_setup_entry", + return_value=True, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +@pytest.mark.parametrize( + "error", + [ + AIOSomecomfort.device.ConnectionError, + AIOSomecomfort.device.ConnectionTimeout, + asyncio.TimeoutError, + ], +) +async def test_reauth_flow_connnection_error( + hass: HomeAssistant, client: MagicMock, error +) -> None: + """Test a connection error reauth flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "test-password"}, + unique_id="test-username", + ) + mock_entry.add_to_hass(hass) + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data={CONF_USERNAME: "test-username", CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result["step_id"] == "reauth_confirm" + assert result["type"] == FlowResultType.FORM + assert result["errors"] == {} + + client.login.side_effect = error + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + {CONF_PASSWORD: "new-password"}, + ) + await hass.async_block_till_done() + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == {"base": "cannot_connect"}