diff --git a/homeassistant/components/overkiz/__init__.py b/homeassistant/components/overkiz/__init__.py index 6bd95d6d1fc0..66234eb44607 100644 --- a/homeassistant/components/overkiz/__init__.py +++ b/homeassistant/components/overkiz/__init__.py @@ -18,7 +18,7 @@ from pyoverkiz.models import Device, Scenario from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME, Platform from homeassistant.core import HomeAssistant -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import device_registry as dr from homeassistant.helpers.aiohttp_client import async_create_clientsession @@ -64,9 +64,8 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: client.get_scenarios(), ] ) - except BadCredentialsException: - LOGGER.error("Invalid authentication") - return False + except BadCredentialsException as exception: + raise ConfigEntryAuthFailed("Invalid authentication") from exception except TooManyRequestsException as exception: raise ConfigEntryNotReady("Too many requests, try again later") from exception except (TimeoutError, ClientError, ServerDisconnectedError) as exception: diff --git a/homeassistant/components/overkiz/config_flow.py b/homeassistant/components/overkiz/config_flow.py index 57e5ae12749e..1a0a94198cc3 100644 --- a/homeassistant/components/overkiz/config_flow.py +++ b/homeassistant/components/overkiz/config_flow.py @@ -1,7 +1,7 @@ """Config flow for Overkiz (by Somfy) integration.""" from __future__ import annotations -from typing import Any +from typing import Any, cast from aiohttp import ClientError from pyoverkiz.client import OverkizClient @@ -16,6 +16,7 @@ import voluptuous as vol from homeassistant import config_entries from homeassistant.components import dhcp, zeroconf +from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_PASSWORD, CONF_USERNAME from homeassistant.data_entry_flow import FlowResult from homeassistant.helpers.aiohttp_client import async_get_clientsession @@ -28,6 +29,18 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): VERSION = 1 + _config_entry: ConfigEntry | None + _default_user: None | str + _default_hub: str + + def __init__(self) -> None: + """Initialize Overkiz Config Flow.""" + super().__init__() + + self._config_entry = None + self._default_user = None + self._default_hub = DEFAULT_HUB + async def async_validate_input(self, user_input: dict[str, Any]) -> None: """Validate user credentials.""" username = user_input[CONF_USERNAME] @@ -67,7 +80,30 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): errors["base"] = "unknown" LOGGER.exception(exception) else: + if self._config_entry: + if self._config_entry.unique_id != self.unique_id: + return self.async_abort(reason="reauth_wrong_account") + + # Update existing entry during reauth + self.hass.config_entries.async_update_entry( + self._config_entry, + data={ + **self._config_entry.data, + **user_input, + }, + ) + + self.hass.async_create_task( + self.hass.config_entries.async_reload( + self._config_entry.entry_id + ) + ) + + return self.async_abort(reason="reauth_successful") + + # Create new entry self._abort_if_unique_id_configured() + return self.async_create_entry( title=user_input[CONF_USERNAME], data=user_input ) @@ -76,9 +112,9 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): step_id="user", data_schema=vol.Schema( { - vol.Required(CONF_USERNAME): str, + vol.Required(CONF_USERNAME, default=self._default_user): str, vol.Required(CONF_PASSWORD): str, - vol.Required(CONF_HUB, default=DEFAULT_HUB): vol.In( + vol.Required(CONF_HUB, default=self._default_hub): vol.In( {key: hub.name for key, hub in SUPPORTED_SERVERS.items()} ), } @@ -113,3 +149,17 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self._abort_if_unique_id_configured() return await self.async_step_user() + + async def async_step_reauth( + self, user_input: dict[str, Any] | None = None + ) -> FlowResult: + """Handle reauth.""" + self._config_entry = cast( + ConfigEntry, + self.hass.config_entries.async_get_entry(self.context["entry_id"]), + ) + + self._default_user = self._config_entry.data[CONF_USERNAME] + self._default_hub = self._config_entry.data[CONF_HUB] + + return await self.async_step_user(user_input) diff --git a/homeassistant/components/overkiz/coordinator.py b/homeassistant/components/overkiz/coordinator.py index a931c11c984e..ff7cd429ea5f 100644 --- a/homeassistant/components/overkiz/coordinator.py +++ b/homeassistant/components/overkiz/coordinator.py @@ -16,6 +16,7 @@ from pyoverkiz.exceptions import ( from pyoverkiz.models import Device, Event, Place from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers import device_registry as dr from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed from homeassistant.util.decorator import Registry @@ -65,7 +66,7 @@ class OverkizDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Device]]): try: events = await self.client.fetch_events() except BadCredentialsException as exception: - raise UpdateFailed("Invalid authentication.") from exception + raise ConfigEntryAuthFailed("Invalid authentication.") from exception except TooManyRequestsException as exception: raise UpdateFailed("Too many requests, try again later.") from exception except MaintenanceException as exception: @@ -80,7 +81,7 @@ class OverkizDataUpdateCoordinator(DataUpdateCoordinator[dict[str, Device]]): await self.client.login() self.devices = await self._get_devices() except BadCredentialsException as exception: - raise UpdateFailed("Invalid authentication.") from exception + raise ConfigEntryAuthFailed("Invalid authentication.") from exception except TooManyRequestsException as exception: raise UpdateFailed("Too many requests, try again later.") from exception diff --git a/homeassistant/components/overkiz/strings.json b/homeassistant/components/overkiz/strings.json index 0fc01a8c769b..2bef16ec2dde 100644 --- a/homeassistant/components/overkiz/strings.json +++ b/homeassistant/components/overkiz/strings.json @@ -19,7 +19,9 @@ "unknown": "[%key:common::config_flow::error::unknown%]" }, "abort": { - "already_configured": "[%key:common::config_flow::abort::already_configured_account%]" + "already_configured": "[%key:common::config_flow::abort::already_configured_account%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", + "reauth_wrong_account": "You can only reauthenticate this entry with the same Overkiz account and hub" } } } \ No newline at end of file diff --git a/homeassistant/components/overkiz/translations/en.json b/homeassistant/components/overkiz/translations/en.json index 9b9b28559e4e..423809045835 100644 --- a/homeassistant/components/overkiz/translations/en.json +++ b/homeassistant/components/overkiz/translations/en.json @@ -1,7 +1,9 @@ { "config": { "abort": { - "already_configured": "Account is already configured" + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful", + "reauth_wrong_account": "You can only reauthenticate this entry with the same Overkiz account and hub" }, "error": { "cannot_connect": "Failed to connect", diff --git a/tests/components/overkiz/test_config_flow.py b/tests/components/overkiz/test_config_flow.py index b211d93bcfe7..e80add482b0d 100644 --- a/tests/components/overkiz/test_config_flow.py +++ b/tests/components/overkiz/test_config_flow.py @@ -29,6 +29,7 @@ TEST_GATEWAY_ID = "1234-5678-9123" TEST_GATEWAY_ID2 = "4321-5678-9123" MOCK_GATEWAY_RESPONSE = [Mock(id=TEST_GATEWAY_ID)] +MOCK_GATEWAY2_RESPONSE = [Mock(id=TEST_GATEWAY_ID2)] FAKE_ZERO_CONF_INFO = ZeroconfServiceInfo( host="192.168.0.51", @@ -269,3 +270,85 @@ async def test_zeroconf_flow_already_configured(hass): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + + +async def test_reauth_success(hass): + """Test reauthentication flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_GATEWAY_ID, + data={"username": TEST_EMAIL, "password": TEST_PASSWORD, "hub": TEST_HUB2}, + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data=mock_entry.data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( + "pyoverkiz.client.OverkizClient.get_gateways", + return_value=MOCK_GATEWAY_RESPONSE, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + "username": TEST_EMAIL, + "password": TEST_PASSWORD2, + "hub": TEST_HUB2, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_successful" + assert mock_entry.data["username"] == TEST_EMAIL + assert mock_entry.data["password"] == TEST_PASSWORD2 + + +async def test_reauth_wrong_account(hass): + """Test reauthentication flow.""" + + mock_entry = MockConfigEntry( + domain=DOMAIN, + unique_id=TEST_GATEWAY_ID, + data={"username": TEST_EMAIL, "password": TEST_PASSWORD, "hub": TEST_HUB2}, + ) + mock_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={ + "source": config_entries.SOURCE_REAUTH, + "unique_id": mock_entry.unique_id, + "entry_id": mock_entry.entry_id, + }, + data=mock_entry.data, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_FORM + assert result["step_id"] == "user" + + with patch("pyoverkiz.client.OverkizClient.login", return_value=True), patch( + "pyoverkiz.client.OverkizClient.get_gateways", + return_value=MOCK_GATEWAY2_RESPONSE, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={ + "username": TEST_EMAIL, + "password": TEST_PASSWORD2, + "hub": TEST_HUB2, + }, + ) + + assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT + assert result["reason"] == "reauth_wrong_account"