diff --git a/homeassistant/components/vicare/__init__.py b/homeassistant/components/vicare/__init__.py index 7a297ca8113e7004162cbed7dbdc83d0111e12ad..76de3a8a7ace6d6d608567779abfab9c69cc3aa3 100644 --- a/homeassistant/components/vicare/__init__.py +++ b/homeassistant/components/vicare/__init__.py @@ -10,10 +10,15 @@ from typing import Any from PyViCare.PyViCare import PyViCare from PyViCare.PyViCareDevice import Device +from PyViCare.PyViCareUtils import ( + PyViCareInvalidConfigurationError, + PyViCareInvalidCredentialsError, +) from homeassistant.config_entries import ConfigEntry from homeassistant.const import CONF_CLIENT_ID, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant +from homeassistant.exceptions import ConfigEntryAuthFailed from homeassistant.helpers.storage import STORAGE_DIR from .const import ( @@ -53,7 +58,10 @@ async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry) -> bool: hass.data[DOMAIN] = {} hass.data[DOMAIN][entry.entry_id] = {} - await hass.async_add_executor_job(setup_vicare_api, hass, entry) + try: + await hass.async_add_executor_job(setup_vicare_api, hass, entry) + except (PyViCareInvalidConfigurationError, PyViCareInvalidCredentialsError) as err: + raise ConfigEntryAuthFailed("Authentication failed") from err await hass.config_entries.async_forward_entry_setups(entry, PLATFORMS) diff --git a/homeassistant/components/vicare/config_flow.py b/homeassistant/components/vicare/config_flow.py index 5b2d3afa427f3ef40986270c7f5a80c1fd3d454f..87bfcf7b146d900b7238eda20fa2461b2206ca85 100644 --- a/homeassistant/components/vicare/config_flow.py +++ b/homeassistant/components/vicare/config_flow.py @@ -1,6 +1,7 @@ """Config flow for ViCare integration.""" from __future__ import annotations +from collections.abc import Mapping import logging from typing import Any @@ -28,11 +29,28 @@ from .const import ( _LOGGER = logging.getLogger(__name__) +REAUTH_SCHEMA = vol.Schema( + { + vol.Required(CONF_PASSWORD): cv.string, + vol.Required(CONF_CLIENT_ID): cv.string, + } +) + +USER_SCHEMA = REAUTH_SCHEMA.extend( + { + vol.Required(CONF_USERNAME): cv.string, + vol.Required(CONF_HEATING_TYPE, default=DEFAULT_HEATING_TYPE.value): vol.In( + [e.value for e in HeatingType] + ), + } +) + class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): """Handle a config flow for ViCare.""" VERSION = 1 + entry: config_entries.ConfigEntry | None async def async_step_user( self, user_input: dict[str, Any] | None = None @@ -41,14 +59,6 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): if self._async_current_entries(): return self.async_abort(reason="single_instance_allowed") - data_schema = { - vol.Required(CONF_USERNAME): cv.string, - vol.Required(CONF_PASSWORD): cv.string, - vol.Required(CONF_CLIENT_ID): cv.string, - vol.Required(CONF_HEATING_TYPE, default=DEFAULT_HEATING_TYPE.value): vol.In( - [e.value for e in HeatingType] - ), - } errors: dict[str, str] = {} if user_input is not None: @@ -63,7 +73,45 @@ class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form( step_id="user", - data_schema=vol.Schema(data_schema), + data_schema=USER_SCHEMA, + errors=errors, + ) + + async def async_step_reauth(self, entry_data: Mapping[str, Any]) -> FlowResult: + """Handle re-authentication with ViCare.""" + 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 ViCare.""" + errors: dict[str, str] = {} + assert self.entry is not None + + if user_input: + data = { + **self.entry.data, + **user_input, + } + + try: + await self.hass.async_add_executor_job(vicare_login, self.hass, data) + except (PyViCareInvalidConfigurationError, PyViCareInvalidCredentialsError): + errors["base"] = "invalid_auth" + else: + self.hass.config_entries.async_update_entry( + self.entry, + data=data, + ) + 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=self.add_suggested_values_to_schema( + REAUTH_SCHEMA, self.entry.data + ), errors=errors, ) diff --git a/homeassistant/components/vicare/strings.json b/homeassistant/components/vicare/strings.json index 056a4df7920e7c27104e4fb88f43c14628f92155..2dc1eecd1e4aea342d5e4abd44b6dfe5209ce126 100644 --- a/homeassistant/components/vicare/strings.json +++ b/homeassistant/components/vicare/strings.json @@ -10,6 +10,13 @@ "client_id": "Client ID", "heating_type": "Heating type" } + }, + "reauth_confirm": { + "description": "Please verify credentials.", + "data": { + "password": "[%key:common::config_flow::data::password%]", + "client_id": "[%key:component::vicare::config::step::user::data::client_id%]" + } } }, "error": { @@ -17,6 +24,7 @@ }, "abort": { "single_instance_allowed": "[%key:common::config_flow::abort::single_instance_allowed%]", + "reauth_successful": "[%key:common::config_flow::abort::reauth_successful%]", "unknown": "[%key:common::config_flow::error::unknown%]" } }, diff --git a/tests/components/vicare/test_config_flow.py b/tests/components/vicare/test_config_flow.py index 7f70c13f0b01e32b7c44e62138a086bfadc1dfcf..283f06b754d22ddc325e91aa3a632a7e31601f66 100644 --- a/tests/components/vicare/test_config_flow.py +++ b/tests/components/vicare/test_config_flow.py @@ -10,7 +10,7 @@ from syrupy.assertion import SnapshotAssertion from homeassistant.components import dhcp from homeassistant.components.vicare.const import DOMAIN -from homeassistant.config_entries import SOURCE_DHCP, SOURCE_USER +from homeassistant.config_entries import SOURCE_DHCP, SOURCE_REAUTH, SOURCE_USER from homeassistant.const import CONF_CLIENT_ID, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import HomeAssistant from homeassistant.data_entry_flow import FlowResultType @@ -93,6 +93,61 @@ async def test_user_create_entry( mock_setup_entry.assert_called_once() +async def test_step_reauth(hass: HomeAssistant, mock_setup_entry: AsyncMock) -> None: + """Test reauth flow.""" + new_password = "ABCD" + new_client_id = "EFGH" + config_entry = MockConfigEntry( + domain=DOMAIN, + data=VALID_CONFIG, + ) + config_entry.add_to_hass(hass) + + result = await hass.config_entries.flow.async_init( + DOMAIN, + context={"source": SOURCE_REAUTH, "entry_id": config_entry.entry_id}, + data=VALID_CONFIG, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + + # test PyViCareInvalidConfigurationError + with patch( + f"{MODULE}.config_flow.vicare_login", + side_effect=PyViCareInvalidConfigurationError( + {"error": "foo", "error_description": "bar"} + ), + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: new_password, CONF_CLIENT_ID: new_client_id}, + ) + assert result["type"] == FlowResultType.FORM + assert result["step_id"] == "reauth_confirm" + assert result["errors"] == {"base": "invalid_auth"} + + # test success + with patch( + f"{MODULE}.config_flow.vicare_login", + return_value=None, + ): + result = await hass.config_entries.flow.async_configure( + result["flow_id"], + user_input={CONF_PASSWORD: new_password, CONF_CLIENT_ID: new_client_id}, + ) + assert result["type"] == FlowResultType.ABORT + assert result["reason"] == "reauth_successful" + + assert len(hass.config_entries.async_entries()) == 1 + assert ( + hass.config_entries.async_entries()[0].data[CONF_PASSWORD] == new_password + ) + assert ( + hass.config_entries.async_entries()[0].data[CONF_CLIENT_ID] == new_client_id + ) + await hass.async_block_till_done() + + async def test_form_dhcp( hass: HomeAssistant, mock_setup_entry: AsyncMock, snapshot: SnapshotAssertion ) -> None: