diff --git a/homeassistant/components/simplisafe/__init__.py b/homeassistant/components/simplisafe/__init__.py index c4856788848..14fc4bf9a5a 100644 --- a/homeassistant/components/simplisafe/__init__.py +++ b/homeassistant/components/simplisafe/__init__.py @@ -2,11 +2,11 @@ import asyncio from uuid import UUID -from simplipy import API +from simplipy import get_api from simplipy.errors import EndpointUnavailable, InvalidCredentialsError, SimplipyError import voluptuous as vol -from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_TOKEN, CONF_USERNAME +from homeassistant.const import ATTR_CODE, CONF_CODE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import CoreState, callback from homeassistant.exceptions import ConfigEntryAuthFailed, ConfigEntryNotReady from homeassistant.helpers import ( @@ -105,14 +105,6 @@ 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. @@ -139,6 +131,9 @@ 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: @@ -161,19 +156,19 @@ async def async_setup_entry(hass, config_entry): # noqa: C901 websession = aiohttp_client.async_get_clientsession(hass) try: - api = await API.login_via_token( - config_entry.data[CONF_TOKEN], client_id=client_id, session=websession + api = await get_api( + config_entry.data[CONF_USERNAME], + config_entry.data[CONF_PASSWORD], + client_id=client_id, + session=websession, ) - except InvalidCredentialsError: - LOGGER.error("Invalid credentials provided") - return False + except InvalidCredentialsError as err: + raise ConfigEntryAuthFailed from err except SimplipyError as err: LOGGER.error("Config entry failed: %s", err) raise ConfigEntryNotReady from err - _async_save_refresh_token(hass, config_entry, api.refresh_token) - - simplisafe = SimpliSafe(hass, api, config_entry) + simplisafe = SimpliSafe(hass, config_entry, api) try: await simplisafe.async_init() @@ -296,10 +291,9 @@ async def async_reload_entry(hass, config_entry): class SimpliSafe: """Define a SimpliSafe data object.""" - def __init__(self, hass, api, config_entry): + def __init__(self, hass, config_entry, api): """Initialize.""" self._api = api - self._emergency_refresh_token_used = False self._hass = hass self._system_notifications = {} self.config_entry = config_entry @@ -376,23 +370,7 @@ class SimpliSafe: for result in results: if isinstance(result, InvalidCredentialsError): - if self._emergency_refresh_token_used: - raise ConfigEntryAuthFailed( - "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( # pylint: disable=raise-missing-from - f"Error while using stored refresh token: {err}" - ) + raise ConfigEntryAuthFailed("Invalid credentials") from result if isinstance(result, EndpointUnavailable): # In case the user attempts an action not allowed in their current plan, @@ -403,16 +381,6 @@ 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 ba51356f770..ac31779175f 100644 --- a/homeassistant/components/simplisafe/config_flow.py +++ b/homeassistant/components/simplisafe/config_flow.py @@ -1,5 +1,5 @@ """Config flow to configure the SimpliSafe component.""" -from simplipy import API +from simplipy import get_api from simplipy.errors import ( InvalidCredentialsError, PendingAuthorizationError, @@ -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_TOKEN, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME from homeassistant.core import callback from homeassistant.helpers import aiohttp_client @@ -47,7 +47,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): client_id = await async_get_client_id(self.hass) websession = aiohttp_client.async_get_clientsession(self.hass) - return await API.login_via_credentials( + return await get_api( self._username, self._password, client_id=client_id, @@ -59,7 +59,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): errors = {} try: - simplisafe = await self._async_get_simplisafe_api() + 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_TOKEN: simplisafe.refresh_token, + CONF_PASSWORD: self._password, CONF_CODE: self._code, } ) @@ -89,6 +89,9 @@ 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) @@ -98,7 +101,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return self.async_show_form(step_id="mfa") try: - simplisafe = await self._async_get_simplisafe_api() + await self._async_get_simplisafe_api() except PendingAuthorizationError: LOGGER.error("Still awaiting confirmation of MFA email click") return self.async_show_form( @@ -108,7 +111,7 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN): return await self.async_step_finish( { CONF_USERNAME: self._username, - CONF_TOKEN: simplisafe.refresh_token, + CONF_PASSWORD: self._password, CONF_CODE: self._code, } ) diff --git a/homeassistant/components/simplisafe/manifest.json b/homeassistant/components/simplisafe/manifest.json index 79e11828eaa..eff37bf1548 100644 --- a/homeassistant/components/simplisafe/manifest.json +++ b/homeassistant/components/simplisafe/manifest.json @@ -3,7 +3,7 @@ "name": "SimpliSafe", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/simplisafe", - "requirements": ["simplisafe-python==10.0.0"], + "requirements": ["simplisafe-python==11.0.0"], "codeowners": ["@bachya"], "iot_class": "cloud_polling" } diff --git a/homeassistant/components/simplisafe/strings.json b/homeassistant/components/simplisafe/strings.json index ad973261a0e..23f85495025 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 token has expired or been revoked. Enter your password to re-link your account.", + "description": "Your access 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 b9e274666bb..331eb65ca83 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 token has expired or been revoked. Enter your password to re-link your account.", + "description": "Your access has expired or been revoked. Enter your password to re-link your account.", "title": "Reauthenticate Integration" }, "user": { diff --git a/requirements_all.txt b/requirements_all.txt index 3c68ba9c5de..0e7db016e7c 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -2105,7 +2105,7 @@ simplehound==0.3 simplepush==1.1.4 # homeassistant.components.simplisafe -simplisafe-python==10.0.0 +simplisafe-python==11.0.0 # homeassistant.components.sisyphus sisyphus-control==3.0 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 378846a01b1..d5c89aa1fb9 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -1154,7 +1154,7 @@ sharkiqpy==0.1.8 simplehound==0.3 # homeassistant.components.simplisafe -simplisafe-python==10.0.0 +simplisafe-python==11.0.0 # homeassistant.components.slack slackclient==2.5.0 diff --git a/tests/components/simplisafe/test_config_flow.py b/tests/components/simplisafe/test_config_flow.py index a048e4b0745..4d438965806 100644 --- a/tests/components/simplisafe/test_config_flow.py +++ b/tests/components/simplisafe/test_config_flow.py @@ -1,5 +1,5 @@ """Define tests for the SimpliSafe config flow.""" -from unittest.mock import AsyncMock, MagicMock, PropertyMock, patch +from unittest.mock import AsyncMock, patch from simplipy.errors import ( InvalidCredentialsError, @@ -10,18 +10,11 @@ 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_TOKEN, CONF_USERNAME +from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_USERNAME from tests.common import MockConfigEntry -def mock_api(): - """Mock SimpliSafe API class.""" - api = MagicMock() - type(api).refresh_token = PropertyMock(return_value="12345abc") - return api - - async def test_duplicate_error(hass): """Test that errors are shown when duplicates are added.""" conf = { @@ -33,7 +26,11 @@ async def test_duplicate_error(hass): MockConfigEntry( domain=DOMAIN, unique_id="user@email.com", - data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"}, + data={ + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_CODE: "1234", + }, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -49,7 +46,7 @@ async def test_invalid_credentials(hass): conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} with patch( - "simplipy.API.login_via_credentials", + "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock(side_effect=InvalidCredentialsError), ): result = await hass.config_entries.flow.async_init( @@ -102,7 +99,11 @@ async def test_step_reauth(hass): MockConfigEntry( domain=DOMAIN, unique_id="user@email.com", - data={CONF_USERNAME: "user@email.com", CONF_TOKEN: "12345", CONF_CODE: "1234"}, + data={ + CONF_USERNAME: "user@email.com", + CONF_PASSWORD: "password", + CONF_CODE: "1234", + }, ).add_to_hass(hass) result = await hass.config_entries.flow.async_init( @@ -118,8 +119,8 @@ async def test_step_reauth(hass): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True - ), patch( - "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api()) + ), patch("homeassistant.components.simplisafe.config_flow.get_api"), patch( + "homeassistant.config_entries.ConfigEntries.async_reload" ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={CONF_PASSWORD: "password"} @@ -141,7 +142,7 @@ async def test_step_user(hass): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch( - "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api()) + "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock() ): result = await hass.config_entries.flow.async_init( DOMAIN, context={"source": SOURCE_USER}, data=conf @@ -151,7 +152,7 @@ async def test_step_user(hass): assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", - CONF_TOKEN: "12345abc", + CONF_PASSWORD: "password", CONF_CODE: "1234", } @@ -165,7 +166,7 @@ async def test_step_user_mfa(hass): } with patch( - "simplipy.API.login_via_credentials", + "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock(side_effect=PendingAuthorizationError), ): result = await hass.config_entries.flow.async_init( @@ -174,7 +175,7 @@ async def test_step_user_mfa(hass): assert result["step_id"] == "mfa" with patch( - "simplipy.API.login_via_credentials", + "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock(side_effect=PendingAuthorizationError), ): # Simulate the user pressing the MFA submit button without having clicked @@ -187,7 +188,7 @@ async def test_step_user_mfa(hass): with patch( "homeassistant.components.simplisafe.async_setup_entry", return_value=True ), patch( - "simplipy.API.login_via_credentials", new=AsyncMock(return_value=mock_api()) + "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock() ): result = await hass.config_entries.flow.async_configure( result["flow_id"], user_input={} @@ -197,7 +198,7 @@ async def test_step_user_mfa(hass): assert result["title"] == "user@email.com" assert result["data"] == { CONF_USERNAME: "user@email.com", - CONF_TOKEN: "12345abc", + CONF_PASSWORD: "password", CONF_CODE: "1234", } @@ -207,7 +208,7 @@ async def test_unknown_error(hass): conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"} with patch( - "simplipy.API.login_via_credentials", + "homeassistant.components.simplisafe.config_flow.get_api", new=AsyncMock(side_effect=SimplipyError), ): result = await hass.config_entries.flow.async_init(