Add config entry for SimpliSafe (#17148)
* Initial move into component * Base functionality in place * Starting tests in place * All config entry tests in place * Made default scan interval more obvious and removed extra logging * Inherit default scan interval from alarm_control_panel * Updated coveragerc and CODEOWNERS * Member-requested changes * Hound * Updated requirements * Updated tests * Member-requested changes * Owner-requested changes * Constant cleanup * Fixed config flow test * Owner-requested updates * Updated requirements * Using async_will_remove_from_hass * Corrected scan interval logic * Fixed tests * Owner-requested changes * Additional logging * Owner-requested changes
This commit is contained in:
parent
1f863830e1
commit
401e22fc0c
14 changed files with 461 additions and 86 deletions
|
@ -290,6 +290,9 @@ omit =
|
|||
homeassistant/components/scsgate.py
|
||||
homeassistant/components/*/scsgate.py
|
||||
|
||||
homeassistant/components/simplisafe/__init__.py
|
||||
homeassistant/components/*/simplisafe.py
|
||||
|
||||
homeassistant/components/sisyphus.py
|
||||
homeassistant/components/*/sisyphus.py
|
||||
|
||||
|
@ -401,7 +404,6 @@ omit =
|
|||
homeassistant/components/alarm_control_panel/ifttt.py
|
||||
homeassistant/components/alarm_control_panel/manual_mqtt.py
|
||||
homeassistant/components/alarm_control_panel/nx584.py
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py
|
||||
homeassistant/components/alarm_control_panel/totalconnect.py
|
||||
homeassistant/components/alarm_control_panel/yale_smart_alarm.py
|
||||
homeassistant/components/apiai.py
|
||||
|
|
|
@ -49,7 +49,6 @@ homeassistant/components/hassio/* @home-assistant/hassio
|
|||
# Individual platforms
|
||||
homeassistant/components/alarm_control_panel/egardia.py @jeroenterheerdt
|
||||
homeassistant/components/alarm_control_panel/manual_mqtt.py @colinodell
|
||||
homeassistant/components/alarm_control_panel/simplisafe.py @bachya
|
||||
homeassistant/components/binary_sensor/hikvision.py @mezz64
|
||||
homeassistant/components/binary_sensor/threshold.py @fabaff
|
||||
homeassistant/components/camera/yi.py @bachya
|
||||
|
@ -192,7 +191,7 @@ homeassistant/components/melissa.py @kennedyshead
|
|||
homeassistant/components/*/melissa.py @kennedyshead
|
||||
homeassistant/components/*/mystrom.py @fabaff
|
||||
|
||||
# U
|
||||
# O
|
||||
homeassistant/components/openuv/* @bachya
|
||||
homeassistant/components/*/openuv.py @bachya
|
||||
|
||||
|
@ -206,6 +205,10 @@ homeassistant/components/*/rainmachine.py @bachya
|
|||
homeassistant/components/*/random.py @fabaff
|
||||
homeassistant/components/*/rfxtrx.py @danielhiversen
|
||||
|
||||
# S
|
||||
homeassistant/components/simplisafe/* @bachya
|
||||
homeassistant/components/*/simplisafe.py @bachya
|
||||
|
||||
# T
|
||||
homeassistant/components/tahoma.py @philklei
|
||||
homeassistant/components/*/tahoma.py @philklei
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
"""
|
||||
Interfaces with SimpliSafe alarm control panel.
|
||||
This platform provides alarm control functionality for SimpliSafe.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/alarm_control_panel.simplisafe/
|
||||
|
@ -7,86 +7,44 @@ https://home-assistant.io/components/alarm_control_panel.simplisafe/
|
|||
import logging
|
||||
import re
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.components.alarm_control_panel import (
|
||||
PLATFORM_SCHEMA, AlarmControlPanel)
|
||||
from homeassistant.components.alarm_control_panel import AlarmControlPanel
|
||||
from homeassistant.components.simplisafe.const import (
|
||||
DATA_CLIENT, DOMAIN, TOPIC_UPDATE)
|
||||
from homeassistant.const import (
|
||||
CONF_CODE, CONF_NAME, CONF_PASSWORD, CONF_USERNAME, STATE_ALARM_ARMED_AWAY,
|
||||
STATE_ALARM_ARMED_HOME, STATE_ALARM_DISARMED)
|
||||
from homeassistant.helpers import aiohttp_client, config_validation as cv
|
||||
from homeassistant.util.json import load_json, save_json
|
||||
|
||||
REQUIREMENTS = ['simplisafe-python==3.1.2']
|
||||
CONF_CODE, STATE_ALARM_ARMED_AWAY, STATE_ALARM_ARMED_HOME,
|
||||
STATE_ALARM_DISARMED)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_connect
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
ATTR_ALARM_ACTIVE = "alarm_active"
|
||||
ATTR_TEMPERATURE = "temperature"
|
||||
|
||||
DATA_FILE = '.simplisafe'
|
||||
|
||||
DEFAULT_NAME = 'SimpliSafe'
|
||||
|
||||
PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
})
|
||||
ATTR_ALARM_ACTIVE = 'alarm_active'
|
||||
ATTR_TEMPERATURE = 'temperature'
|
||||
|
||||
|
||||
async def async_setup_platform(
|
||||
hass, config, async_add_entities, discovery_info=None):
|
||||
"""Set up the SimpliSafe platform."""
|
||||
from simplipy import API
|
||||
from simplipy.errors import SimplipyError
|
||||
"""Set up a SimpliSafe alarm control panel based on existing config."""
|
||||
pass
|
||||
|
||||
username = config[CONF_USERNAME]
|
||||
password = config[CONF_PASSWORD]
|
||||
name = config.get(CONF_NAME)
|
||||
code = config.get(CONF_CODE)
|
||||
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
|
||||
config_data = await hass.async_add_executor_job(
|
||||
load_json, hass.config.path(DATA_FILE))
|
||||
|
||||
try:
|
||||
if config_data:
|
||||
try:
|
||||
simplisafe = await API.login_via_token(
|
||||
config_data['refresh_token'], websession)
|
||||
_LOGGER.debug('Logging in with refresh token')
|
||||
except SimplipyError:
|
||||
_LOGGER.info('Refresh token expired; attempting credentials')
|
||||
simplisafe = await API.login_via_credentials(
|
||||
username, password, websession)
|
||||
else:
|
||||
simplisafe = await API.login_via_credentials(
|
||||
username, password, websession)
|
||||
_LOGGER.debug('Logging in with credentials')
|
||||
except SimplipyError as err:
|
||||
_LOGGER.error("There was an error during setup: %s", err)
|
||||
return
|
||||
|
||||
config_data = {'refresh_token': simplisafe.refresh_token}
|
||||
await hass.async_add_executor_job(
|
||||
save_json, hass.config.path(DATA_FILE), config_data)
|
||||
|
||||
systems = await simplisafe.get_systems()
|
||||
async_add_entities(
|
||||
[SimpliSafeAlarm(system, name, code) for system in systems], True)
|
||||
async def async_setup_entry(hass, entry, async_add_entities):
|
||||
"""Set up a SimpliSafe alarm control panel based on a config entry."""
|
||||
systems = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
|
||||
async_add_entities([
|
||||
SimpliSafeAlarm(system, entry.data.get(CONF_CODE))
|
||||
for system in systems
|
||||
], True)
|
||||
|
||||
|
||||
class SimpliSafeAlarm(AlarmControlPanel):
|
||||
"""Representation of a SimpliSafe alarm."""
|
||||
|
||||
def __init__(self, system, name, code):
|
||||
def __init__(self, system, code):
|
||||
"""Initialize the SimpliSafe alarm."""
|
||||
self._async_unsub_dispatcher_connect = None
|
||||
self._attrs = {}
|
||||
self._code = str(code) if code else None
|
||||
self._name = name
|
||||
self._code = code
|
||||
self._system = system
|
||||
self._state = None
|
||||
|
||||
|
@ -98,9 +56,7 @@ class SimpliSafeAlarm(AlarmControlPanel):
|
|||
@property
|
||||
def name(self):
|
||||
"""Return the name of the device."""
|
||||
if self._name:
|
||||
return self._name
|
||||
return 'Alarm {}'.format(self._system.system_id)
|
||||
return self._system.address
|
||||
|
||||
@property
|
||||
def code_format(self):
|
||||
|
@ -128,6 +84,21 @@ class SimpliSafeAlarm(AlarmControlPanel):
|
|||
_LOGGER.warning("Wrong code entered for %s", state)
|
||||
return check
|
||||
|
||||
async def async_added_to_hass(self):
|
||||
"""Register callbacks."""
|
||||
@callback
|
||||
def update():
|
||||
"""Update the state."""
|
||||
self.async_schedule_update_ha_state(True)
|
||||
|
||||
self._async_unsub_dispatcher_connect = async_dispatcher_connect(
|
||||
self.hass, TOPIC_UPDATE, update)
|
||||
|
||||
async def async_will_remove_from_hass(self) -> None:
|
||||
"""Disconnect dispatcher listener when removed."""
|
||||
if self._async_unsub_dispatcher_connect:
|
||||
self._async_unsub_dispatcher_connect()
|
||||
|
||||
async def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if not self._validate_code(code, 'disarming'):
|
||||
|
@ -151,22 +122,24 @@ class SimpliSafeAlarm(AlarmControlPanel):
|
|||
|
||||
async def async_update(self):
|
||||
"""Update alarm status."""
|
||||
await self._system.update()
|
||||
from simplipy.system import SystemStates
|
||||
|
||||
if self._system.state == self._system.SystemStates.off:
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif self._system.state in (
|
||||
self._system.SystemStates.home,
|
||||
self._system.SystemStates.home_count):
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
elif self._system.state in (
|
||||
self._system.SystemStates.away,
|
||||
self._system.SystemStates.away_count,
|
||||
self._system.SystemStates.exit_delay):
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
self._state = None
|
||||
await self._system.update()
|
||||
|
||||
self._attrs[ATTR_ALARM_ACTIVE] = self._system.alarm_going_off
|
||||
if self._system.temperature:
|
||||
self._attrs[ATTR_TEMPERATURE] = self._system.temperature
|
||||
|
||||
if self._system.state == SystemStates.error:
|
||||
return
|
||||
|
||||
if self._system.state == SystemStates.off:
|
||||
self._state = STATE_ALARM_DISARMED
|
||||
elif self._system.state in (SystemStates.home,
|
||||
SystemStates.home_count):
|
||||
self._state = STATE_ALARM_ARMED_HOME
|
||||
elif self._system.state in (SystemStates.away, SystemStates.away_count,
|
||||
SystemStates.exit_delay):
|
||||
self._state = STATE_ALARM_ARMED_AWAY
|
||||
else:
|
||||
self._state = None
|
||||
|
|
19
homeassistant/components/simplisafe/.translations/en.json
Normal file
19
homeassistant/components/simplisafe/.translations/en.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"config": {
|
||||
"error": {
|
||||
"identifier_exists": "Account already registered",
|
||||
"invalid_credentials": "Invalid credentials"
|
||||
},
|
||||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"code": "Code (for Home Assistant)",
|
||||
"password": "Password",
|
||||
"username": "Email Address"
|
||||
},
|
||||
"title": "Fill in your information"
|
||||
}
|
||||
},
|
||||
"title": "SimpliSafe"
|
||||
}
|
||||
}
|
143
homeassistant/components/simplisafe/__init__.py
Normal file
143
homeassistant/components/simplisafe/__init__.py
Normal file
|
@ -0,0 +1,143 @@
|
|||
"""
|
||||
Support for SimpliSafe alarm systems.
|
||||
|
||||
For more details about this platform, please refer to the documentation at
|
||||
https://home-assistant.io/components/simplisafe/
|
||||
"""
|
||||
import logging
|
||||
from datetime import timedelta
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.config_entries import SOURCE_IMPORT
|
||||
from homeassistant.const import (
|
||||
CONF_CODE, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_USERNAME)
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.exceptions import ConfigEntryNotReady
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
from homeassistant.helpers.dispatcher import async_dispatcher_send
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
from homeassistant.helpers import config_validation as cv
|
||||
|
||||
from .config_flow import configured_instances
|
||||
from .const import DATA_CLIENT, DEFAULT_SCAN_INTERVAL, DOMAIN, TOPIC_UPDATE
|
||||
|
||||
REQUIREMENTS = ['simplisafe-python==3.1.7']
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
CONF_ACCOUNTS = 'accounts'
|
||||
|
||||
DATA_LISTENER = 'listener'
|
||||
|
||||
ACCOUNT_CONFIG_SCHEMA = vol.Schema({
|
||||
vol.Required(CONF_USERNAME): cv.string,
|
||||
vol.Required(CONF_PASSWORD): cv.string,
|
||||
vol.Optional(CONF_CODE): cv.string,
|
||||
vol.Optional(CONF_SCAN_INTERVAL, default=DEFAULT_SCAN_INTERVAL):
|
||||
cv.time_period
|
||||
})
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.Schema({
|
||||
vol.Optional(CONF_ACCOUNTS):
|
||||
vol.All(cv.ensure_list, [ACCOUNT_CONFIG_SCHEMA]),
|
||||
}),
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
|
||||
@callback
|
||||
def _async_save_refresh_token(hass, config_entry, token):
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, data={
|
||||
**config_entry.data, CONF_TOKEN: token
|
||||
})
|
||||
|
||||
|
||||
async def async_setup(hass, config):
|
||||
"""Set up the SimpliSafe component."""
|
||||
hass.data[DOMAIN] = {}
|
||||
hass.data[DOMAIN][DATA_CLIENT] = {}
|
||||
hass.data[DOMAIN][DATA_LISTENER] = {}
|
||||
|
||||
if DOMAIN not in config:
|
||||
return True
|
||||
|
||||
conf = config[DOMAIN]
|
||||
|
||||
for account in conf[CONF_ACCOUNTS]:
|
||||
if account[CONF_USERNAME] in configured_instances(hass):
|
||||
continue
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.flow.async_init(
|
||||
DOMAIN,
|
||||
context={'source': SOURCE_IMPORT},
|
||||
data={
|
||||
CONF_USERNAME: account[CONF_USERNAME],
|
||||
CONF_PASSWORD: account[CONF_PASSWORD],
|
||||
CONF_CODE: account.get(CONF_CODE),
|
||||
CONF_SCAN_INTERVAL: account[CONF_SCAN_INTERVAL],
|
||||
}))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_setup_entry(hass, config_entry):
|
||||
"""Set up SimpliSafe as config entry."""
|
||||
from simplipy import API
|
||||
from simplipy.errors import SimplipyError
|
||||
|
||||
websession = aiohttp_client.async_get_clientsession(hass)
|
||||
|
||||
try:
|
||||
simplisafe = await API.login_via_token(
|
||||
config_entry.data[CONF_TOKEN], websession)
|
||||
except SimplipyError as err:
|
||||
if 403 in str(err):
|
||||
_LOGGER.error('Invalid credentials provided')
|
||||
return False
|
||||
|
||||
_LOGGER.error('Config entry failed: %s', err)
|
||||
raise ConfigEntryNotReady
|
||||
|
||||
_async_save_refresh_token(hass, config_entry, simplisafe.refresh_token)
|
||||
|
||||
systems = await simplisafe.get_systems()
|
||||
hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id] = systems
|
||||
|
||||
hass.async_create_task(
|
||||
hass.config_entries.async_forward_entry_setup(
|
||||
config_entry, 'alarm_control_panel'))
|
||||
|
||||
async def refresh(event_time):
|
||||
"""Refresh data from the SimpliSafe account."""
|
||||
for system in systems:
|
||||
_LOGGER.debug('Updating system data: %s', system.system_id)
|
||||
await system.update()
|
||||
async_dispatcher_send(hass, TOPIC_UPDATE.format(system.system_id))
|
||||
|
||||
if system.api.refresh_token_dirty:
|
||||
_async_save_refresh_token(
|
||||
hass, config_entry, system.api.refresh_token)
|
||||
|
||||
hass.data[DOMAIN][DATA_LISTENER][
|
||||
config_entry.entry_id] = async_track_time_interval(
|
||||
hass,
|
||||
refresh,
|
||||
timedelta(seconds=config_entry.data[CONF_SCAN_INTERVAL]))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def async_unload_entry(hass, entry):
|
||||
"""Unload a SimpliSafe config entry."""
|
||||
await hass.config_entries.async_forward_entry_unload(
|
||||
entry, 'alarm_control_panel')
|
||||
|
||||
hass.data[DOMAIN][DATA_CLIENT].pop(entry.entry_id)
|
||||
remove_listener = hass.data[DOMAIN][DATA_LISTENER].pop(entry.entry_id)
|
||||
remove_listener()
|
||||
|
||||
return True
|
80
homeassistant/components/simplisafe/config_flow.py
Normal file
80
homeassistant/components/simplisafe/config_flow.py
Normal file
|
@ -0,0 +1,80 @@
|
|||
"""Config flow to configure the SimpliSafe component."""
|
||||
|
||||
from collections import OrderedDict
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.const import (
|
||||
CONF_CODE, CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_USERNAME)
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import DEFAULT_SCAN_INTERVAL, DOMAIN
|
||||
|
||||
|
||||
@callback
|
||||
def configured_instances(hass):
|
||||
"""Return a set of configured SimpliSafe instances."""
|
||||
return set(
|
||||
entry.data[CONF_USERNAME]
|
||||
for entry in hass.config_entries.async_entries(DOMAIN))
|
||||
|
||||
|
||||
@config_entries.HANDLERS.register(DOMAIN)
|
||||
class SimpliSafeFlowHandler(config_entries.ConfigFlow):
|
||||
"""Handle a SimpliSafe config flow."""
|
||||
|
||||
VERSION = 1
|
||||
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||
|
||||
def __init__(self):
|
||||
"""Initialize the config flow."""
|
||||
self.data_schema = OrderedDict()
|
||||
self.data_schema[vol.Required(CONF_USERNAME)] = str
|
||||
self.data_schema[vol.Required(CONF_PASSWORD)] = str
|
||||
self.data_schema[vol.Optional(CONF_CODE)] = str
|
||||
|
||||
async def _show_form(self, errors=None):
|
||||
"""Show the form to the user."""
|
||||
return self.async_show_form(
|
||||
step_id='user',
|
||||
data_schema=vol.Schema(self.data_schema),
|
||||
errors=errors if errors else {},
|
||||
)
|
||||
|
||||
async def async_step_import(self, import_config):
|
||||
"""Import a config entry from configuration.yaml."""
|
||||
return await self.async_step_user(import_config)
|
||||
|
||||
async def async_step_user(self, user_input=None):
|
||||
"""Handle the start of the config flow."""
|
||||
from simplipy import API
|
||||
from simplipy.errors import SimplipyError
|
||||
|
||||
if not user_input:
|
||||
return await self._show_form()
|
||||
|
||||
if user_input[CONF_USERNAME] in configured_instances(self.hass):
|
||||
return await self._show_form({CONF_USERNAME: 'identifier_exists'})
|
||||
|
||||
username = user_input[CONF_USERNAME]
|
||||
websession = aiohttp_client.async_get_clientsession(self.hass)
|
||||
|
||||
try:
|
||||
simplisafe = await API.login_via_credentials(
|
||||
username, user_input[CONF_PASSWORD], websession)
|
||||
except SimplipyError:
|
||||
return await self._show_form({'base': 'invalid_credentials'})
|
||||
|
||||
scan_interval = user_input.get(
|
||||
CONF_SCAN_INTERVAL, DEFAULT_SCAN_INTERVAL)
|
||||
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_USERNAME],
|
||||
data={
|
||||
CONF_USERNAME: username,
|
||||
CONF_TOKEN: simplisafe.refresh_token,
|
||||
CONF_SCAN_INTERVAL: scan_interval.seconds,
|
||||
},
|
||||
)
|
10
homeassistant/components/simplisafe/const.py
Normal file
10
homeassistant/components/simplisafe/const.py
Normal file
|
@ -0,0 +1,10 @@
|
|||
"""Define constants for the SimpliSafe component."""
|
||||
from datetime import timedelta
|
||||
|
||||
DOMAIN = 'simplisafe'
|
||||
|
||||
DATA_CLIENT = 'client'
|
||||
|
||||
DEFAULT_SCAN_INTERVAL = timedelta(seconds=30)
|
||||
|
||||
TOPIC_UPDATE = 'update_{0}'
|
19
homeassistant/components/simplisafe/strings.json
Normal file
19
homeassistant/components/simplisafe/strings.json
Normal file
|
@ -0,0 +1,19 @@
|
|||
{
|
||||
"config": {
|
||||
"title": "SimpliSafe",
|
||||
"step": {
|
||||
"user": {
|
||||
"title": "Fill in your information",
|
||||
"data": {
|
||||
"username": "Email Address",
|
||||
"password": "Password",
|
||||
"code": "Code (for Home Assistant)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"error": {
|
||||
"identifier_exists": "Account already registered",
|
||||
"invalid_credentials": "Invalid credentials"
|
||||
}
|
||||
}
|
||||
}
|
|
@ -146,6 +146,7 @@ FLOWS = [
|
|||
'mqtt',
|
||||
'nest',
|
||||
'openuv',
|
||||
'simplisafe',
|
||||
'smhi',
|
||||
'sonos',
|
||||
'tradfri',
|
||||
|
|
|
@ -1346,8 +1346,8 @@ shodan==1.10.4
|
|||
# homeassistant.components.notify.simplepush
|
||||
simplepush==1.1.4
|
||||
|
||||
# homeassistant.components.alarm_control_panel.simplisafe
|
||||
simplisafe-python==3.1.2
|
||||
# homeassistant.components.simplisafe
|
||||
simplisafe-python==3.1.7
|
||||
|
||||
# homeassistant.components.sisyphus
|
||||
sisyphus-control==2.1
|
||||
|
|
|
@ -214,6 +214,9 @@ ring_doorbell==0.2.1
|
|||
# homeassistant.components.media_player.yamaha
|
||||
rxv==0.5.1
|
||||
|
||||
# homeassistant.components.simplisafe
|
||||
simplisafe-python==3.1.7
|
||||
|
||||
# homeassistant.components.sleepiq
|
||||
sleepyq==0.6
|
||||
|
||||
|
|
|
@ -97,6 +97,7 @@ TEST_REQUIREMENTS = (
|
|||
'rflink',
|
||||
'ring_doorbell',
|
||||
'rxv',
|
||||
'simplisafe-python',
|
||||
'sleepyq',
|
||||
'smhi-pkg',
|
||||
'somecomfort',
|
||||
|
|
1
tests/components/simplisafe/__init__.py
Normal file
1
tests/components/simplisafe/__init__.py
Normal file
|
@ -0,0 +1 @@
|
|||
"""Define tests for the SimpliSafe component."""
|
120
tests/components/simplisafe/test_config_flow.py
Normal file
120
tests/components/simplisafe/test_config_flow.py
Normal file
|
@ -0,0 +1,120 @@
|
|||
"""Define tests for the SimpliSafe config flow."""
|
||||
import json
|
||||
from datetime import timedelta
|
||||
from unittest.mock import mock_open, patch, MagicMock, PropertyMock
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.simplisafe import DOMAIN, config_flow
|
||||
from homeassistant.const import (
|
||||
CONF_PASSWORD, CONF_SCAN_INTERVAL, CONF_TOKEN, CONF_USERNAME)
|
||||
|
||||
from tests.common import MockConfigEntry, mock_coro
|
||||
|
||||
|
||||
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 = {
|
||||
CONF_USERNAME: 'user@email.com',
|
||||
CONF_PASSWORD: 'password',
|
||||
}
|
||||
|
||||
MockConfigEntry(domain=DOMAIN, data=conf).add_to_hass(hass)
|
||||
flow = config_flow.SimpliSafeFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
result = await flow.async_step_user(user_input=conf)
|
||||
assert result['errors'] == {CONF_USERNAME: 'identifier_exists'}
|
||||
|
||||
|
||||
async def test_invalid_credentials(hass):
|
||||
"""Test that invalid credentials throws an error."""
|
||||
from simplipy.errors import SimplipyError
|
||||
conf = {
|
||||
CONF_USERNAME: 'user@email.com',
|
||||
CONF_PASSWORD: 'password',
|
||||
}
|
||||
|
||||
flow = config_flow.SimpliSafeFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
with patch('simplipy.API.login_via_credentials',
|
||||
return_value=mock_coro(exception=SimplipyError)):
|
||||
result = await flow.async_step_user(user_input=conf)
|
||||
assert result['errors'] == {'base': 'invalid_credentials'}
|
||||
|
||||
|
||||
async def test_show_form(hass):
|
||||
"""Test that the form is served with no input."""
|
||||
flow = config_flow.SimpliSafeFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
result = await flow.async_step_user(user_input=None)
|
||||
|
||||
assert result['type'] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result['step_id'] == 'user'
|
||||
|
||||
|
||||
async def test_step_import(hass):
|
||||
"""Test that the import step works."""
|
||||
conf = {
|
||||
CONF_USERNAME: 'user@email.com',
|
||||
CONF_PASSWORD: 'password',
|
||||
}
|
||||
|
||||
flow = config_flow.SimpliSafeFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
mop = mock_open(read_data=json.dumps({'refresh_token': '12345'}))
|
||||
|
||||
with patch('simplipy.API.login_via_credentials',
|
||||
return_value=mock_coro(return_value=mock_api())):
|
||||
with patch('homeassistant.util.json.open', mop, create=True):
|
||||
with patch('homeassistant.util.json.os.open', return_value=0):
|
||||
with patch('homeassistant.util.json.os.replace'):
|
||||
result = await flow.async_step_import(import_config=conf)
|
||||
|
||||
assert result[
|
||||
'type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result['title'] == 'user@email.com'
|
||||
assert result['data'] == {
|
||||
CONF_USERNAME: 'user@email.com',
|
||||
CONF_TOKEN: '12345abc',
|
||||
CONF_SCAN_INTERVAL: 30,
|
||||
}
|
||||
|
||||
|
||||
async def test_step_user(hass):
|
||||
"""Test that the user step works."""
|
||||
conf = {
|
||||
CONF_USERNAME: 'user@email.com',
|
||||
CONF_PASSWORD: 'password',
|
||||
CONF_SCAN_INTERVAL: timedelta(seconds=90),
|
||||
}
|
||||
|
||||
flow = config_flow.SimpliSafeFlowHandler()
|
||||
flow.hass = hass
|
||||
|
||||
mop = mock_open(read_data=json.dumps({'refresh_token': '12345'}))
|
||||
|
||||
with patch('simplipy.API.login_via_credentials',
|
||||
return_value=mock_coro(return_value=mock_api())):
|
||||
with patch('homeassistant.util.json.open', mop, create=True):
|
||||
with patch('homeassistant.util.json.os.open', return_value=0):
|
||||
with patch('homeassistant.util.json.os.replace'):
|
||||
result = await flow.async_step_user(user_input=conf)
|
||||
|
||||
assert result[
|
||||
'type'] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert result['title'] == 'user@email.com'
|
||||
assert result['data'] == {
|
||||
CONF_USERNAME: 'user@email.com',
|
||||
CONF_TOKEN: '12345abc',
|
||||
CONF_SCAN_INTERVAL: 90,
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue