diff --git a/homeassistant/components/verisure/__init__.py b/homeassistant/components/verisure/__init__.py index 5f8b2119310..1a727daac73 100644 --- a/homeassistant/components/verisure/__init__.py +++ b/homeassistant/components/verisure/__init__.py @@ -15,7 +15,7 @@ from homeassistant.components.camera import DOMAIN as CAMERA_DOMAIN from homeassistant.components.lock import DOMAIN as LOCK_DOMAIN from homeassistant.components.sensor import DOMAIN as SENSOR_DOMAIN from homeassistant.components.switch import DOMAIN as SWITCH_DOMAIN -from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry +from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_REAUTH, ConfigEntry from homeassistant.const import ( CONF_EMAIL, CONF_PASSWORD, @@ -35,7 +35,6 @@ from .const import ( CONF_LOCK_DEFAULT_CODE, DEFAULT_LOCK_CODE_DIGITS, DOMAIN, - LOGGER, ) from .coordinator import VerisureDataUpdateCoordinator @@ -125,7 +124,11 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: coordinator = VerisureDataUpdateCoordinator(hass, entry=entry) if not await coordinator.async_login(): - LOGGER.error("Could not login to Verisure, aborting setting up integration") + await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH}, + data={"entry": entry}, + ) return False hass.bus.async_listen_once(EVENT_HOMEASSISTANT_STOP, coordinator.async_logout) diff --git a/homeassistant/components/verisure/config_flow.py b/homeassistant/components/verisure/config_flow.py index ce9c76874f1..f05571d338c 100644 --- a/homeassistant/components/verisure/config_flow.py +++ b/homeassistant/components/verisure/config_flow.py @@ -36,8 +36,9 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): VERSION = 1 CONNECTION_CLASS = CONN_CLASS_CLOUD_POLL - installations: dict[str, str] email: str + entry: ConfigEntry + installations: dict[str, str] password: str # These can be removed after YAML import has been removed. @@ -123,6 +124,55 @@ class VerisureConfigFlowHandler(ConfigFlow, domain=DOMAIN): }, ) + async def async_step_reauth(self, data: dict[str, Any]) -> dict[str, Any]: + """Handle initiation of re-authentication with Verisure.""" + self.entry = data["entry"] + return await self.async_step_reauth_confirm() + + async def async_step_reauth_confirm( + self, user_input: dict[str, Any] | None = None + ) -> dict[str, Any]: + """Handle re-authentication with Verisure.""" + errors: dict[str, str] = {} + + if user_input is not None: + verisure = Verisure( + username=user_input[CONF_EMAIL], password=user_input[CONF_PASSWORD] + ) + try: + await self.hass.async_add_executor_job(verisure.login) + except VerisureLoginError as ex: + LOGGER.debug("Could not log in to Verisure, %s", ex) + errors["base"] = "invalid_auth" + except (VerisureError, VerisureResponseError) as ex: + LOGGER.debug("Unexpected response from Verisure, %s", ex) + errors["base"] = "unknown" + else: + data = self.entry.data.copy() + self.hass.config_entries.async_update_entry( + self.entry, + data={ + **data, + CONF_EMAIL: user_input[CONF_EMAIL], + CONF_PASSWORD: user_input[CONF_PASSWORD], + }, + ) + self.hass.async_create_task( + 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=vol.Schema( + { + vol.Required(CONF_EMAIL, default=self.entry.data[CONF_EMAIL]): str, + vol.Required(CONF_PASSWORD): str, + } + ), + errors=errors, + ) + async def async_step_import(self, user_input: dict[str, Any]) -> dict[str, Any]: """Import Verisure YAML configuration.""" if user_input[CONF_GIID]: diff --git a/homeassistant/components/verisure/strings.json b/homeassistant/components/verisure/strings.json index 0c7f513f8ee..5170bff5faa 100644 --- a/homeassistant/components/verisure/strings.json +++ b/homeassistant/components/verisure/strings.json @@ -13,6 +13,13 @@ "data": { "giid": "Installation" } + }, + "reauth_confirm": { + "data": { + "description": "Re-authenticate with your Verisure My Pages account.", + "email": "[%key:common::config_flow::data::email%]", + "password": "[%key:common::config_flow::data::password%]" + } } }, "error": { @@ -20,7 +27,8 @@ "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%]" } }, "options": { diff --git a/homeassistant/components/verisure/translations/en.json b/homeassistant/components/verisure/translations/en.json index 85c7acc167e..57f73c3772b 100644 --- a/homeassistant/components/verisure/translations/en.json +++ b/homeassistant/components/verisure/translations/en.json @@ -1,7 +1,8 @@ { "config": { "abort": { - "already_configured": "Account is already configured" + "already_configured": "Account is already configured", + "reauth_successful": "Re-authentication was successful" }, "error": { "invalid_auth": "Invalid authentication", @@ -14,6 +15,13 @@ }, "description": "Home Assistant found multiple Verisure installations in your My Pages account. Please, select the installation to add to Home Assistant." }, + "reauth_confirm": { + "data": { + "description": "Re-authenticate with your Verisure My Pages account.", + "email": "Email", + "password": "Password" + } + }, "user": { "data": { "description": "Sign-in with your Verisure My Pages account.", diff --git a/tests/components/verisure/test_config_flow.py b/tests/components/verisure/test_config_flow.py index 97f0f9731b1..c53c418c72b 100644 --- a/tests/components/verisure/test_config_flow.py +++ b/tests/components/verisure/test_config_flow.py @@ -190,6 +190,127 @@ async def test_dhcp(hass: HomeAssistant) -> None: assert result["step_id"] == "user" +async def test_reauth_flow(hass: HomeAssistant) -> None: + """Test a reauthentication flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="12345", + data={ + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_GIID: "12345", + CONF_PASSWORD: "SuperS3cr3t!", + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data={"entry": entry} + ) + assert result["step_id"] == "reauth_confirm" + assert result["type"] == RESULT_TYPE_FORM + assert result["errors"] == {} + + with patch( + "homeassistant.components.verisure.config_flow.Verisure.login", + return_value=True, + ) as mock_verisure, patch( + "homeassistant.components.verisure.async_setup", return_value=True + ) as mock_setup, patch( + "homeassistant.components.verisure.async_setup_entry", + return_value=True, + ) as mock_setup_entry: + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "correct horse battery staple", + }, + ) + await hass.async_block_till_done() + + assert result2["type"] == RESULT_TYPE_ABORT + assert result2["reason"] == "reauth_successful" + assert entry.data == { + CONF_GIID: "12345", + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_PASSWORD: "correct horse battery staple", + } + + assert len(mock_verisure.mock_calls) == 1 + assert len(mock_setup.mock_calls) == 1 + assert len(mock_setup_entry.mock_calls) == 1 + + +async def test_reauth_flow_invalid_login(hass: HomeAssistant) -> None: + """Test a reauthentication flow.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="12345", + data={ + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_GIID: "12345", + CONF_PASSWORD: "SuperS3cr3t!", + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data={"entry": entry} + ) + + with patch( + "homeassistant.components.verisure.config_flow.Verisure.login", + side_effect=VerisureLoginError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "WrOngP4ssw0rd!", + }, + ) + await hass.async_block_till_done() + + assert result2["step_id"] == "reauth_confirm" + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "invalid_auth"} + + +async def test_reauth_flow_unknown_error(hass: HomeAssistant) -> None: + """Test a reauthentication flow, with an unknown error happening.""" + entry = MockConfigEntry( + domain=DOMAIN, + unique_id="12345", + data={ + CONF_EMAIL: "verisure_my_pages@example.com", + CONF_GIID: "12345", + CONF_PASSWORD: "SuperS3cr3t!", + }, + ) + entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, context={"source": config_entries.SOURCE_REAUTH}, data={"entry": entry} + ) + + with patch( + "homeassistant.components.verisure.config_flow.Verisure.login", + side_effect=VerisureError, + ): + result2 = await hass.config_entries.flow.async_configure( + result["flow_id"], + { + "email": "verisure_my_pages@example.com", + "password": "WrOngP4ssw0rd!", + }, + ) + await hass.async_block_till_done() + + assert result2["step_id"] == "reauth_confirm" + assert result2["type"] == RESULT_TYPE_FORM + assert result2["errors"] == {"base": "unknown"} + + @pytest.mark.parametrize( "input,output", [