Skip to content

Commit

Permalink
Fix SimpliSafe to work with new MFA (home-assistant#38097)
Browse files Browse the repository at this point in the history
* Fix SimpliSafe to work with new MFA

* Code review (part 1)

* Input needed from Martin

* Code review

* Code review

* Restore YAML

* Tests

* Code review

* Remove JSON patching in tests

* Add reauth test

* One more reauth test

* Don't abuse the word "conf"

* Update homeassistant/components/simplisafe/config_flow.py

Co-authored-by: Martin Hjelmare <[email protected]>

* Test coverage

Co-authored-by: Martin Hjelmare <[email protected]>
  • Loading branch information
2 people authored and frenck committed Jul 24, 2020
1 parent 3e7ada2 commit 7fe5fee
Show file tree
Hide file tree
Showing 11 changed files with 313 additions and 115 deletions.
77 changes: 47 additions & 30 deletions homeassistant/components/simplisafe/__init__.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
"""Support for SimpliSafe alarm systems."""
import asyncio
import logging
from uuid import UUID

from simplipy import API
from simplipy.errors import InvalidCredentialsError, SimplipyError
Expand Down Expand Up @@ -55,11 +55,10 @@
DATA_CLIENT,
DEFAULT_SCAN_INTERVAL,
DOMAIN,
LOGGER,
VOLUMES,
)

_LOGGER = logging.getLogger(__name__)

CONF_ACCOUNTS = "accounts"

DATA_LISTENER = "listener"
Expand Down Expand Up @@ -161,6 +160,13 @@ def _async_save_refresh_token(hass, config_entry, token):
)


async def async_get_client_id(hass):
"""Get a client ID (based on the HASS unique ID) for the SimpliSafe API."""
hass_id = await hass.helpers.instance_id.async_get()
# SimpliSafe requires full, "dashed" versions of UUIDs:
return str(UUID(hass_id))


async def async_register_base_station(hass, system, config_entry_id):
"""Register a new bridge."""
device_registry = await dr.async_get_registry(hass)
Expand Down Expand Up @@ -220,17 +226,18 @@ async def async_setup_entry(hass, config_entry):

_verify_domain_control = verify_domain_control(hass, DOMAIN)

client_id = await async_get_client_id(hass)
websession = aiohttp_client.async_get_clientsession(hass)

try:
api = await API.login_via_token(
config_entry.data[CONF_TOKEN], session=websession
config_entry.data[CONF_TOKEN], client_id=client_id, session=websession
)
except InvalidCredentialsError:
_LOGGER.error("Invalid credentials provided")
LOGGER.error("Invalid credentials provided")
return False
except SimplipyError as err:
_LOGGER.error("Config entry failed: %s", err)
LOGGER.error("Config entry failed: %s", err)
raise ConfigEntryNotReady

_async_save_refresh_token(hass, config_entry, api.refresh_token)
Expand All @@ -252,7 +259,7 @@ async def decorator(call):
"""Decorate."""
system_id = int(call.data[ATTR_SYSTEM_ID])
if system_id not in simplisafe.systems:
_LOGGER.error("Unknown system ID in service call: %s", system_id)
LOGGER.error("Unknown system ID in service call: %s", system_id)
return
await coro(call)

Expand All @@ -266,7 +273,7 @@ async def decorator(call):
"""Decorate."""
system = simplisafe.systems[int(call.data[ATTR_SYSTEM_ID])]
if system.version != 3:
_LOGGER.error("Service only available on V3 systems")
LOGGER.error("Service only available on V3 systems")
return
await coro(call)

Expand All @@ -280,7 +287,7 @@ async def clear_notifications(call):
try:
await system.clear_notifications()
except SimplipyError as err:
_LOGGER.error("Error during service call: %s", err)
LOGGER.error("Error during service call: %s", err)
return

@verify_system_exists
Expand All @@ -291,7 +298,7 @@ async def remove_pin(call):
try:
await system.remove_pin(call.data[ATTR_PIN_LABEL_OR_VALUE])
except SimplipyError as err:
_LOGGER.error("Error during service call: %s", err)
LOGGER.error("Error during service call: %s", err)
return

@verify_system_exists
Expand All @@ -302,7 +309,7 @@ async def set_pin(call):
try:
await system.set_pin(call.data[ATTR_PIN_LABEL], call.data[ATTR_PIN_VALUE])
except SimplipyError as err:
_LOGGER.error("Error during service call: %s", err)
LOGGER.error("Error during service call: %s", err)
return

@verify_system_exists
Expand All @@ -320,7 +327,7 @@ async def set_system_properties(call):
}
)
except SimplipyError as err:
_LOGGER.error("Error during service call: %s", err)
LOGGER.error("Error during service call: %s", err)
return

for service, method, schema in [
Expand Down Expand Up @@ -373,16 +380,16 @@ def __init__(self, hass, websocket):
@staticmethod
def _on_connect():
"""Define a handler to fire when the websocket is connected."""
_LOGGER.info("Connected to websocket")
LOGGER.info("Connected to websocket")

@staticmethod
def _on_disconnect():
"""Define a handler to fire when the websocket is disconnected."""
_LOGGER.info("Disconnected from websocket")
LOGGER.info("Disconnected from websocket")

def _on_event(self, event):
"""Define a handler to fire when a new SimpliSafe event arrives."""
_LOGGER.debug("New websocket event: %s", event)
LOGGER.debug("New websocket event: %s", event)
async_dispatcher_send(
self._hass, TOPIC_UPDATE_WEBSOCKET.format(event.system_id), event
)
Expand Down Expand Up @@ -451,7 +458,7 @@ def _async_process_new_notifications(self, system):
if not to_add:
return

_LOGGER.debug("New system notifications: %s", to_add)
LOGGER.debug("New system notifications: %s", to_add)

self._system_notifications[system.system_id].update(to_add)

Expand Down Expand Up @@ -492,7 +499,7 @@ async def async_init(self):
system.system_id
] = await system.get_latest_event()
except SimplipyError as err:
_LOGGER.error("Error while fetching initial event: %s", err)
LOGGER.error("Error while fetching initial event: %s", err)
self.initial_event_to_use[system.system_id] = {}

async def refresh(event_time):
Expand All @@ -512,7 +519,7 @@ async def update_system(system):
"""Update a system."""
await system.update()
self._async_process_new_notifications(system)
_LOGGER.debug('Updated REST API data for "%s"', system.address)
LOGGER.debug('Updated REST API data for "%s"', system.address)
async_dispatcher_send(
self._hass, TOPIC_UPDATE_REST_API.format(system.system_id)
)
Expand All @@ -523,27 +530,37 @@ async def update_system(system):
for result in results:
if isinstance(result, InvalidCredentialsError):
if self._emergency_refresh_token_used:
_LOGGER.error(
"SimpliSafe authentication disconnected. Please restart HASS"
LOGGER.error(
"Token disconnected or invalid. Please re-auth the "
"SimpliSafe integration in HASS"
)
remove_listener = self._hass.data[DOMAIN][DATA_LISTENER].pop(
self._config_entry.entry_id
self._hass.async_create_task(
self._hass.config_entries.flow.async_init(
DOMAIN,
context={"source": "reauth"},
data=self._config_entry.data,
)
)
remove_listener()
return

_LOGGER.warning("SimpliSafe cloud error; trying stored refresh token")
LOGGER.warning("SimpliSafe cloud error; trying stored refresh token")
self._emergency_refresh_token_used = True
return await self._api.refresh_access_token(
self._config_entry.data[CONF_TOKEN]
)

try:
await self._api.refresh_access_token(
self._config_entry.data[CONF_TOKEN]
)
return
except SimplipyError as err:
LOGGER.error("Error while using stored refresh token: %s", err)
return

if isinstance(result, SimplipyError):
_LOGGER.error("SimpliSafe error while updating: %s", result)
LOGGER.error("SimpliSafe error while updating: %s", result)
return

if isinstance(result, SimplipyError):
_LOGGER.error("Unknown error while updating: %s", result)
if isinstance(result, Exception): # pylint: disable=broad-except
LOGGER.error("Unknown error while updating: %s", result)
return

if self._api.refresh_token != self._config_entry.data[CONF_TOKEN]:
Expand Down
12 changes: 5 additions & 7 deletions homeassistant/components/simplisafe/alarm_control_panel.py
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
"""Support for SimpliSafe alarm control panels."""
import logging
import re

from simplipy.errors import SimplipyError
Expand Down Expand Up @@ -50,11 +49,10 @@
ATTR_VOICE_PROMPT_VOLUME,
DATA_CLIENT,
DOMAIN,
LOGGER,
VOLUME_STRING_MAP,
)

_LOGGER = logging.getLogger(__name__)

ATTR_BATTERY_BACKUP_POWER_LEVEL = "battery_backup_power_level"
ATTR_GSM_STRENGTH = "gsm_strength"
ATTR_PIN_NAME = "pin_name"
Expand Down Expand Up @@ -146,7 +144,7 @@ def _is_code_valid(self, code, state):
return True

if not code or code != self._simplisafe.options[CONF_CODE]:
_LOGGER.warning(
LOGGER.warning(
"Incorrect alarm code entered (target state: %s): %s", state, code
)
return False
Expand All @@ -161,7 +159,7 @@ async def async_alarm_disarm(self, code=None):
try:
await self._system.set_off()
except SimplipyError as err:
_LOGGER.error('Error while disarming "%s": %s', self._system.name, err)
LOGGER.error('Error while disarming "%s": %s', self._system.name, err)
return

self._state = STATE_ALARM_DISARMED
Expand All @@ -174,7 +172,7 @@ async def async_alarm_arm_home(self, code=None):
try:
await self._system.set_home()
except SimplipyError as err:
_LOGGER.error('Error while arming "%s" (home): %s', self._system.name, err)
LOGGER.error('Error while arming "%s" (home): %s', self._system.name, err)
return

self._state = STATE_ALARM_ARMED_HOME
Expand All @@ -187,7 +185,7 @@ async def async_alarm_arm_away(self, code=None):
try:
await self._system.set_away()
except SimplipyError as err:
_LOGGER.error('Error while arming "%s" (away): %s', self._system.name, err)
LOGGER.error('Error while arming "%s" (away): %s', self._system.name, err)
return

self._state = STATE_ALARM_ARMING
Expand Down
Loading

0 comments on commit 7fe5fee

Please sign in to comment.