diff --git a/homeassistant/components/isy994/__init__.py b/homeassistant/components/isy994/__init__.py index ae48fc18b5b..faa2f7cfb5d 100644 --- a/homeassistant/components/isy994/__init__.py +++ b/homeassistant/components/isy994/__init__.py @@ -17,7 +17,7 @@ from homeassistant.const import ( EVENT_HOMEASSISTANT_STOP, ) from homeassistant.core import Event, HomeAssistant, callback -from homeassistant.exceptions import ConfigEntryNotReady +from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import aiohttp_client, config_validation as cv import homeassistant.helpers.device_registry as dr from homeassistant.helpers.typing import ConfigType @@ -181,11 +181,7 @@ async def async_setup_entry( f"Timed out initializing the ISY; device may be busy, trying again later: {err}" ) from err except ISYInvalidAuthError as err: - _LOGGER.error( - "Invalid credentials for the ISY, please adjust settings and try again: %s", - err, - ) - return False + raise ConfigEntryAuthFailed(f"Invalid credentials for the ISY: {err}") from err except ISYConnectionError as err: raise ConfigEntryNotReady( f"Failed to connect to the ISY, please adjust settings and try again: {err}" diff --git a/homeassistant/components/isy994/config_flow.py b/homeassistant/components/isy994/config_flow.py index dea4bce4eeb..34d4738db68 100644 --- a/homeassistant/components/isy994/config_flow.py +++ b/homeassistant/components/isy994/config_flow.py @@ -119,6 +119,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): def __init__(self) -> None: """Initialize the isy994 config flow.""" self.discovered_conf: dict[str, str] = {} + self._existing_entry: config_entries.ConfigEntry | None = None @staticmethod @callback @@ -142,7 +143,7 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): except InvalidHost: errors["base"] = "invalid_host" except InvalidAuth: - errors["base"] = "invalid_auth" + errors[CONF_PASSWORD] = "invalid_auth" except Exception: # pylint: disable=broad-except _LOGGER.exception("Unexpected exception") errors["base"] = "unknown" @@ -252,6 +253,57 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): self.context["title_placeholders"] = self.discovered_conf return await self.async_step_user() + async def async_step_reauth( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: + """Handle reauth.""" + self._existing_entry = await self.async_set_unique_id(self.context["unique_id"]) + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> data_entry_flow.FlowResult: + """Handle reauth input.""" + errors = {} + assert self._existing_entry is not None + existing_entry = self._existing_entry + existing_data = existing_entry.data + if user_input is not None: + new_data = { + **existing_data, + CONF_USERNAME: user_input[CONF_USERNAME], + CONF_PASSWORD: user_input[CONF_PASSWORD], + } + try: + await validate_input(self.hass, new_data) + except CannotConnect: + errors["base"] = "cannot_connect" + except InvalidAuth: + errors[CONF_PASSWORD] = "invalid_auth" + else: + cfg_entries = self.hass.config_entries + cfg_entries.async_update_entry(existing_entry, data=new_data) + await cfg_entries.async_reload(existing_entry.entry_id) + return self.async_abort(reason="reauth_successful") + + self.context["title_placeholders"] = { + CONF_NAME: existing_entry.title, + CONF_HOST: existing_data[CONF_HOST], + } + return self.async_show_form( + description_placeholders={CONF_HOST: existing_data[CONF_HOST]}, + step_id="reauth_confirm", + data_schema=vol.Schema( + { + vol.Required( + CONF_USERNAME, default=existing_data[CONF_USERNAME] + ): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) + class OptionsFlowHandler(config_entries.OptionsFlow): """Handle a option flow for isy994.""" diff --git a/homeassistant/components/isy994/strings.json b/homeassistant/components/isy994/strings.json index eaba5f7a9da..74b54f406a0 100644 --- a/homeassistant/components/isy994/strings.json +++ b/homeassistant/components/isy994/strings.json @@ -10,10 +10,19 @@ "tls": "The TLS version of the ISY controller." }, "description": "The host entry must be in full URL format, e.g., http://192.168.10.100:80", - "title": "Connect to your ISY994" + "title": "Connect to your ISY" + }, + "reauth_confirm": { + "description": "The credentials for {host} is no longer valid.", + "title": "Reauthenticate your ISY", + "data": { + "username": "[%key:common::config_flow::data::username%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unknown": "[%key:common::config_flow::error::unknown%]", "cannot_connect": "[%key:common::config_flow::error::cannot_connect%]", "invalid_auth": "[%key:common::config_flow::error::invalid_auth%]", @@ -26,7 +35,7 @@ "options": { "step": { "init": { - "title": "ISY994 Options", + "title": "ISY Options", "description": "Set the options for the ISY Integration: \n • Node Sensor String: Any device or folder that contains 'Node Sensor String' in the name will be treated as a sensor or binary sensor. \n • Ignore String: Any device with 'Ignore String' in the name will be ignored. \n • Variable Sensor String: Any variable that contains 'Variable Sensor String' will be added as a sensor. \n • Restore Light Brightness: If enabled, the previous brightness will be restored when turning on a light instead of the device's built-in On-Level.", "data": { "sensor_string": "Node Sensor String", diff --git a/homeassistant/components/isy994/translations/en.json b/homeassistant/components/isy994/translations/en.json index dbeaa75acf4..b221765cd99 100644 --- a/homeassistant/components/isy994/translations/en.json +++ b/homeassistant/components/isy994/translations/en.json @@ -7,10 +7,19 @@ "cannot_connect": "Failed to connect", "invalid_auth": "Invalid authentication", "invalid_host": "The host entry was not in full URL format, e.g., http://192.168.10.100:80", + "reauth_successful": "Re-authentication was successful", "unknown": "Unexpected error" }, "flow_title": "{name} ({host})", "step": { + "reauth_confirm": { + "data": { + "password": "Password", + "username": "Username" + }, + "description": "The credentials for {host} is no longer valid.", + "title": "Reauthenticate your ISY" + }, "user": { "data": { "host": "URL", @@ -19,7 +28,7 @@ "username": "Username" }, "description": "The host entry must be in full URL format, e.g., http://192.168.10.100:80", - "title": "Connect to your ISY994" + "title": "Connect to your ISY" } } }, @@ -33,7 +42,7 @@ "variable_sensor_string": "Variable Sensor String" }, "description": "Set the options for the ISY Integration: \n \u2022 Node Sensor String: Any device or folder that contains 'Node Sensor String' in the name will be treated as a sensor or binary sensor. \n \u2022 Ignore String: Any device with 'Ignore String' in the name will be ignored. \n \u2022 Variable Sensor String: Any variable that contains 'Variable Sensor String' will be added as a sensor. \n \u2022 Restore Light Brightness: If enabled, the previous brightness will be restored when turning on a light instead of the device's built-in On-Level.", - "title": "ISY994 Options" + "title": "ISY Options" } } }, diff --git a/tests/components/isy994/test_config_flow.py b/tests/components/isy994/test_config_flow.py index 60e9fd964b6..8458dc0dc67 100644 --- a/tests/components/isy994/test_config_flow.py +++ b/tests/components/isy994/test_config_flow.py @@ -173,7 +173,7 @@ async def test_form_invalid_auth(hass: HomeAssistant): ) assert result2["type"] == data_entry_flow.RESULT_TYPE_FORM - assert result2["errors"] == {"base": "invalid_auth"} + assert result2["errors"] == {CONF_PASSWORD: "invalid_auth"} async def test_form_unknown_exeption(hass: HomeAssistant): @@ -679,3 +679,70 @@ async def test_form_dhcp_existing_ignored_entry(hass: HomeAssistant): assert result["type"] == data_entry_flow.RESULT_TYPE_ABORT assert result["reason"] == "already_configured" + + +async def test_reauth(hass): + """Test we can reauth.""" + entry = MockConfigEntry( + domain=DOMAIN, + data={ + CONF_USERNAME: "bob", + CONF_HOST: f"http://{MOCK_HOSTNAME}:1443{ISY_URL_POSTFIX}", + }, + unique_id=MOCK_UUID, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": config_entries.SOURCE_REAUTH, "unique_id": MOCK_UUID}, + ) + + assert result["type"] == "form" + assert result["step_id"] == "reauth_confirm" + + with patch( + PATCH_CONNECTION, + side_effect=ISYInvalidAuthError(), + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result2["type"] == "form" + assert result2["errors"] == {"password": "invalid_auth"} + + with patch( + PATCH_CONNECTION, + side_effect=ISYConnectionError(), + ): + result3 = await hass.config_entries.flow.async_configure( + result2["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert result3["type"] == "form" + assert result3["errors"] == {"base": "cannot_connect"} + + with patch(PATCH_CONNECTION, return_value=MOCK_CONFIG_RESPONSE), patch( + "homeassistant.components.isy994.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result4 = await hass.config_entries.flow.async_configure( + result3["flow_id"], + { + CONF_USERNAME: "test-username", + CONF_PASSWORD: "test-password", + }, + ) + + assert mock_setup_entry.called + assert result4["type"] == "abort" + assert result4["reason"] == "reauth_successful"