diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index fce1890b280..c4856788848 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -6,7 +6,7 @@ from simplipy import API from simplipy.errors import EndpointUnavailable, InvalidCredentialsError, SimplipyError import voluptuous as vol -from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_TOKEN, CONF_USERNAME from homeassistant.core import CoreState, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( @@ -105,6 +105,14 @@ SERVICE_SET_SYSTEM_PROPERTIES_SCHEMA = SERVICE_BASE_SCHEMA.extend( CONFIG_SCHEMA = cv.deprecated(DOMAIN) +@callback +def _async_save_refresh_token(hass, config_entry, token): + """Save a refresh token to the config entry.""" + hass.config_entries.async_update_entry( + config_entry, data={**config_entry.data, CONF_TOKEN: token} + ) + + async def async_get_client_id(hass): """Get a client ID (based on the HASS unique ID) for the SimpliSafe API. @@ -131,9 +139,6 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 hass.data.setdefault(DOMAIN, {DATA_CLIENT: {}}) hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = [] - if CONF_PASSWORD not in config_entry.data: - raise ConfigEntryAuthFailed("Config schema change requires re-authentication") - entry_updates = {} if not config_entry.unique_id: # If the config entry doesn't already have a unique ID, set one: @@ -155,24 +160,20 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 client_id = await async_get_client_id(hass) websession = aiohttp_client.async_get_clientsession(hass) - async def async_get_api(): - """Define a helper to get an authenticated SimpliSafe API object.""" - return await API.login_via_credentials( - config_entry.data[CONF_USERNAME], - config_entry.data[CONF_PASSWORD], - client_id=client_id, - session=websession, - ) - try: - api = await async_get_api() - except InvalidCredentialsError as err: - raise ConfigEntryAuthFailed from err + api = await API.login_via_token( + config_entry.data[CONF_TOKEN], client_id=client_id, session=websession + ) + except InvalidCredentialsError: + LOGGER.error("Invalid credentials provided") + return False except SimplipyError as err: LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err - simplisafe = SimpliSafe(hass, config_entry, api, async_get_api) + _async_save_refresh_token(hass, config_entry, api.refresh_token) + + simplisafe = SimpliSafe(hass, api, config_entry) try: await simplisafe.async_init() @@ -295,10 +296,10 @@ async def async_reload_entry(hass, config_entry): class SimpliSafe: """Define a SimpliSafe data object.""" - def __init__(self, hass, config_entry, api, async_get_api): + def __init__(self, hass, api, config_entry): """Initialize.""" self._api = api - self._async_get_api = async_get_api + self._emergency_refresh_token_used = False self._hass = hass self._system_notifications = {} self.config_entry = config_entry @@ -375,17 +376,23 @@ class SimpliSafe: for result in results: if isinstance(result, InvalidCredentialsError): - try: - self._api = await self._async_get_api() - return - except InvalidCredentialsError as err: + if self._emergency_refresh_token_used: raise ConfigEntryAuthFailed( - "Unable to re-authenticate with SimpliSafe" - ) from err + "Update failed with stored refresh token" + ) + + LOGGER.warning("SimpliSafe cloud error; trying stored refresh token") + self._emergency_refresh_token_used = True + + try: + await self._api.refresh_access_token( + self.config_entry.data[CONF_TOKEN] + ) + return except SimplipyError as err: - raise UpdateFailed( - f"SimpliSafe error while updating: {err}" - ) from err + raise UpdateFailed( # pylint: disable=raise-missing-from + f"Error while using stored refresh token: {err}" + ) if isinstance(result, EndpointUnavailable): # In case the user attempts an action not allowed in their current plan, @@ -396,6 +403,16 @@ class SimpliSafe: if isinstance(result, SimplipyError): raise UpdateFailed(f"SimpliSafe error while updating: {result}") + if self._api.refresh_token != self.config_entry.data[CONF_TOKEN]: + _async_save_refresh_token( + self._hass, self.config_entry, self._api.refresh_token + ) + + # If we've reached this point using an emergency refresh token, we're in the + # clear and we can discard it: + if self._emergency_refresh_token_used: + self._emergency_refresh_token_used = False + class SimpliSafeEntity(CoordinatorEntity): """Define a base SimpliSafe entity.""" diff --git a/homeassistant/components/simplisafe/config_flow.py b/homeassistant/components/simplisafe/config_flow.py index 0faa07221aa..ba51356f770 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -8,7 +8,7 @@ from simplipy.errors import ( import voluptuous as vol from homeassistant import config_entries -from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -59,7 +59,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} try: - await self._async_get_simplisafe_api() + simplisafe = await self._async_get_simplisafe_api() except PendingAuthorizationError: LOGGER.info("Awaiting confirmation of MFA email click") return await self.async_step_mfa() @@ -79,7 +79,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_finish( { CONF_USERNAME: self._username, - CONF_PASSWORD: self._password, + CONF_TOKEN: simplisafe.refresh_token, CONF_CODE: self._code, } ) @@ -89,9 +89,6 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): existing_entry = await self.async_set_unique_id(self._username) if existing_entry: self.hass.config_entries.async_update_entry(existing_entry, data=user_input) - self.hass.async_create_task( - self.hass.config_entries.async_reload(existing_entry.entry_id) - ) return self.async_abort(reason="reauth_successful") return self.async_create_entry(title=self._username, data=user_input) @@ -101,7 +98,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="mfa") try: - await self._async_get_simplisafe_api() + simplisafe = await self._async_get_simplisafe_api() except PendingAuthorizationError: LOGGER.error("Still awaiting confirmation of MFA email click") return self.async_show_form( @@ -111,7 +108,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_finish( { CONF_USERNAME: self._username, - CONF_PASSWORD: self._password, + CONF_TOKEN: simplisafe.refresh_token, CONF_CODE: self._code, } ) diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index 23f85495025..ad973261a0e 100644 --- a/homeassistant/components/simplisafe/strings.json +++ b/homeassistant/components/simplisafe/strings.json @@ -7,7 +7,7 @@ }, "reauth_confirm": { "title": "[%key:common::config_flow::title::reauth%]", - "description": "Your access has expired or been revoked. Enter your password to re-link your account.", + "description": "Your access token has expired or been revoked. Enter your password to re-link your account.", "data": { "password": "[%key:common::config_flow::data::password%]" } diff --git a/homeassistant/components/simplisafe/translations/en.json b/homeassistant/components/simplisafe/translations/en.json index 331eb65ca83..b9e274666bb 100644 --- a/homeassistant/components/simplisafe/translations/en.json +++ b/homeassistant/components/simplisafe/translations/en.json @@ -19,7 +19,7 @@ "data": { "password": "Password" }, - "description": "Your access has expired or been revoked. Enter your password to re-link your account.", + "description": "Your access token has expired or been revoked. Enter your password to re-link your account.", "title": "Reauthenticate Integration" }, "user": { diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index c2397e9f89e..a048e4b0745 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -10,7 +10,7 @@ from simplipy.errors import ( from homeassistant import data_entry_flow from homeassistant.components.simplisafe import DOMAIN from homeassistant.config_entries import SOURCE_REAUTH, SOURCE_USER -from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME from tests.common import MockConfigEntry @@ -33,11 +33,7 @@ async def test_duplicate_error(hass): MockConfigEntry( domain=DOMAIN, unique_id="user@email.com", - data={ - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", - CONF_CODE: "1234", - }, + data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"}, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -106,11 +102,7 @@ async def test_step_reauth(hass): MockConfigEntry( domain=DOMAIN, unique_id="user@email.com", - data={ - CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", - CONF_CODE: "1234", - }, + data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"}, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -128,8 +120,6 @@ async def test_step_reauth(hass): "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch( "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api()) - ), patch( - "homeassistant.config_entries.ConfigEntries.async_reload" ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} @@ -161,7 +151,7 @@ async def test_step_user(hass): assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", + CONF_TOKEN: "12345abc", CONF_CODE: "1234", } @@ -207,7 +197,7 @@ async def test_step_user_mfa(hass): assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", - CONF_PASSWORD: "password", + CONF_TOKEN: "12345abc", CONF_CODE: "1234", }