8000 Fresh attempt at SimpliSafe auto-relogin by bachya · Pull Request #52567 · home-assistant/core · GitHub
[go: up one dir, main page]
More Web Proxy on the site http://driver.im/
Skip to content

Fresh attempt at SimpliSafe auto-relogin #52567

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jul 6, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 15 additions & 47 deletions homeassistant/components/simplisafe/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 (
Expand Down Expand Up @@ -105,14 +105,6 @@
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.

Expand All @@ -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:
Expand All @@ -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()
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -376,23 +370,7 @@ async def async_update_system(system):

for result in results:
if isinstance(result, InvalidCredentialsError):
if self._emergency_refresh_token_used:
8000 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,
Expand All @@ -403,16 +381,6 @@ async def async_update_system(system):
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."""
Expand Down
17 changes: 10 additions & 7 deletions homeassistant/components/simplisafe/config_flow.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -8,7 +8,7 @@
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

Expand Down Expand Up @@ -47,7 +47,7 @@ async def _async_get_simplisafe_api(self):
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,
Expand All @@ -59,7 +59,7 @@ async def _async_login_during_step(self, *, step_id, form_schema):
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()
Expand All @@ -79,7 +79,7 @@ async def _async_login_during_step(self, *, step_id, form_schema):
return await self.async_step_finish(
{
CONF_USERNAME: self._username,
CONF_TOKEN: simplisafe.refresh_token,
CONF_PASSWORD: self._password,
CONF_CODE: self._code,
}
)
Expand All @@ -89,6 +89,9 @@ async def async_step_finish(self, user_input=None):
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)

Expand All @@ -98,7 +101,7 @@ async def async_step_mfa(self, user_input=None):
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(
Expand All @@ -108,7 +111,7 @@ async def async_step_mfa(self, user_input=None):
return await self.async_step_finish(
{
CONF_USERNAME: self._username,
CONF_TOKEN: simplisafe.refresh_token,
CONF_PASSWORD: self._password,
CONF_CODE: self._code,
}
)
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/simplisafe/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -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"
}
2 changes: 1 addition & 1 deletion homeassistant/components/simplisafe/strings.json
Original file line number Diff line number Diff line change
Expand Up @@ -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%]"
}
Expand Down
2 changes: 1 addition & 1 deletion homeassistant/components/simplisafe/translations/en.json
Original file line number Diff line number Diff line change
Expand Up @@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
43 changes: 22 additions & 21 deletions tests/components/simplisafe/test_config_flow.py
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -10,18 +10,11 @@
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 = {
Expand All @@ -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(
Expand All @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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"}
Expand All @@ -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
Expand All @@ -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",
}

Expand All @@ -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(
Expand All @@ -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
Expand All @@ -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={}
Expand All @@ -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",
}

Expand All @@ -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(
Expand Down
0