diff --git a/homeassistant/components/tedee/config_flow.py b/homeassistant/components/tedee/config_flow.py index e31bcd91693..47a35089e66 100644 --- a/homeassistant/components/tedee/config_flow.py +++ b/homeassistant/components/tedee/config_flow.py @@ -1,4 +1,5 @@ """Config flow for Tedee integration.""" +from collections.abc import Mapping from typing import Any from pytedee_async import ( @@ -9,16 +10,18 @@ from pytedee_async import ( ) import voluptuous as vol -from homeassistant import config_entries +from homeassistant.config_entries import ConfigEntry, ConfigFlow from homeassistant.const import CONF_HOST from homeassistant.data_entry_flow import FlowResult from .const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN, NAME -class TedeeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): +class TedeeConfigFlow(ConfigFlow, domain=DOMAIN): """Handle a config flow for Tedee.""" + reauth_entry: ConfigEntry | None = None + async def async_step_user( self, user_input: dict[str, Any] | None = None ) -> FlowResult: @@ -26,7 +29,10 @@ class TedeeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors: dict[str, str] = {} if user_input is not None: - host = user_input[CONF_HOST] + if self.reauth_entry: + host = self.reauth_entry.data[CONF_HOST] + else: + host = user_input[CONF_HOST] local_access_token = user_input[CONF_LOCAL_ACCESS_TOKEN] tedee_client = TedeeClient(local_token=local_access_token, local_ip=host) try: @@ -35,8 +41,16 @@ class TedeeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors[CONF_LOCAL_ACCESS_TOKEN] = "invalid_api_key" except TedeeClientException: errors[CONF_HOST] = "invalid_host" - else: + if self.reauth_entry: + self.hass.config_entries.async_update_entry( + self.reauth_entry, + data={**self.reauth_entry.data, **user_input}, + ) + await self.hass.config_entries.async_reload( + self.context["entry_id"] + ) + return self.async_abort(reason="reauth_successful") await self.async_set_unique_id(local_bridge.serial) self._abort_if_unique_id_configured() return self.async_create_entry(title=NAME, data=user_input) @@ -45,9 +59,30 @@ class TedeeConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=vol.Schema( { - vol.Required(CONF_HOST): str, - vol.Required(CONF_LOCAL_ACCESS_TOKEN): str, + vol.Required( + CONF_HOST, + ): str, + vol.Required( + CONF_LOCAL_ACCESS_TOKEN, + ): str, } ), errors=errors, ) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Perform reauth upon an API authentication error.""" + self.reauth_entry = self.hass.config_entries.async_get_entry( + self.context["entry_id"] + ) + return self.async_show_form( + step_id="user", + data_schema=vol.Schema( + { + vol.Required( + CONF_LOCAL_ACCESS_TOKEN, + default=entry_data[CONF_LOCAL_ACCESS_TOKEN], + ): str, + } + ), + ) diff --git a/homeassistant/components/tedee/coordinator.py b/homeassistant/components/tedee/coordinator.py index 13e26541557..18fca035532 100644 --- a/homeassistant/components/tedee/coordinator.py +++ b/homeassistant/components/tedee/coordinator.py @@ -15,7 +15,7 @@ from pytedee_async import ( from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryError +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from .const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN @@ -74,7 +74,7 @@ class TedeeApiCoordinator(DataUpdateCoordinator[dict[int, TedeeLock]]): try: await update_fn() except TedeeLocalAuthException as ex: - raise ConfigEntryError( + raise ConfigEntryAuthFailed( "Authentication failed. Local access token is invalid" ) from ex diff --git a/homeassistant/components/tedee/strings.json b/homeassistant/components/tedee/strings.json index db6a450c1f3..1f0a5f0dc7e 100644 --- a/homeassistant/components/tedee/strings.json +++ b/homeassistant/components/tedee/strings.json @@ -11,10 +11,21 @@ "host": "The IP address of the bridge you want to connect to.", "local_access_token": "You can find it in the tedee app under \"Bridge Settings\" -> \"Local API\"." } + }, + "reauth_confirm": { + "title": "Update of access key required", + "description": "Tedee needs an updated access key, because the existing one is invalid, or might have expired.", + "data": { + "local_access_token": "[%key:component::tedee::config::step::user::data::local_access_token%]" + }, + "data_description": { + "local_access_token": "[%key:component::tedee::config::step::user::data_description::local_access_token%]" + } } }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_device%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_device%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]" }, "error": { "invalid_api_key": "[%key:common::config_flow::error::invalid_api_key%]", diff --git a/tests/components/tedee/test_config_flow.py b/tests/components/tedee/test_config_flow.py index 73132d3bd78..4feb9bb8ca5 100644 --- a/tests/components/tedee/test_config_flow.py +++ b/tests/components/tedee/test_config_flow.py @@ -4,8 +4,8 @@ from unittest.mock import MagicMock from pytedee_async import TedeeClientException, TedeeLocalAuthException import pytest -from homeassistant import config_entries from homeassistant.components.tedee.const import CONF_LOCAL_ACCESS_TOKEN, DOMAIN +from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_HOST from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -19,7 +19,7 @@ LOCAL_ACCESS_TOKEN = "api_token" async def test_flow(hass: HomeAssistant, mock_tedee: MagicMock) -> None: """Test config flow with one bridge.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM @@ -48,7 +48,7 @@ async def test_flow_already_configured( mock_config_entry.add_to_hass(hass) result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) await hass.async_block_till_done() assert result["type"] == FlowResultType.FORM @@ -82,7 +82,7 @@ async def test_config_flow_errors( ) -> None: """Test the config flow errors.""" result = await hass.config_entries.flow.async_init( - DOMAIN, context={"source": config_entries.SOURCE_USER} + DOMAIN, context={"source": SOURCE_USER} ) assert result["type"] == FlowResultType.FORM @@ -100,3 +100,81 @@ async def test_config_flow_errors( assert result2["type"] == FlowResultType.FORM assert result2["errors"] == error assert len(mock_tedee.get_local_bridge.mock_calls) == 1 + + +async def test_reauth_flow( + hass: HomeAssistant, mock_config_entry: MockConfigEntry, mock_tedee: MagicMock +) -> None: + """Test that the reauth flow works.""" + + mock_config_entry.add_to_hass(hass) + + reauth_result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": mock_config_entry.unique_id, + "entry_id": mock_config_entry.entry_id, + }, + data={ + CONF_LOCAL_ACCESS_TOKEN: LOCAL_ACCESS_TOKEN, + CONF_HOST: "192.168.1.42", + }, + ) + + result = await hass.config_entries.flow.async_configure( + reauth_result["flow_id"], + { + CONF_LOCAL_ACCESS_TOKEN: LOCAL_ACCESS_TOKEN, + }, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + +@pytest.mark.parametrize( + ("side_effect", "error"), + [ + (TedeeClientException("boom."), {CONF_HOST: "invalid_host"}), + ( + TedeeLocalAuthException("boom."), + {CONF_LOCAL_ACCESS_TOKEN: "invalid_api_key"}, + ), + ], +) +async def test_reauth_flow_errors( + hass: HomeAssistant, + mock_config_entry: MockConfigEntry, + mock_tedee: MagicMock, + side_effect: Exception, + error: dict[str, str], +) -> None: + """Test that the reauth flow errors.""" + + mock_config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": SOURCE_REAUTH, + "unique_id": mock_config_entry.unique_id, + "entry_id": mock_config_entry.entry_id, + }, + data={ + CONF_LOCAL_ACCESS_TOKEN: LOCAL_ACCESS_TOKEN, + CONF_HOST: "192.168.1.42", + }, + ) + + mock_tedee.get_local_bridge.side_effect = side_effect + + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_LOCAL_ACCESS_TOKEN: LOCAL_ACCESS_TOKEN, + }, + ) + + assert result2["type"] == FlowResultType.FORM + assert result2["errors"] == error + assert len(mock_tedee.get_local_bridge.mock_calls) == 1