Add options flow for SimpliSafe (#32631)
* Fix bug where SimpliSafe ignored code from UI * Fix tests * Add options flow * Fix tests * Code review * Code review * Code review
This commit is contained in:
parent
94b6ab2862
commit
4f0997f6e9
6 changed files with 186 additions and 78 deletions
|
@ -10,7 +10,6 @@
|
|||
"step": {
|
||||
"user": {
|
||||
"data": {
|
||||
"code": "Code (for Home Assistant)",
|
||||
"password": "Password",
|
||||
"username": "Email Address"
|
||||
},
|
||||
|
@ -18,5 +17,15 @@
|
|||
}
|
||||
},
|
||||
"title": "SimpliSafe"
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"data": {
|
||||
"code": "Code (used in Home Assistant UI)"
|
||||
},
|
||||
"title": "Configure SimpliSafe"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -201,10 +201,21 @@ async def async_setup(hass, config):
|
|||
|
||||
async def async_setup_entry(hass, config_entry):
|
||||
"""Set up SimpliSafe as config entry."""
|
||||
entry_updates = {}
|
||||
if not config_entry.unique_id:
|
||||
hass.config_entries.async_update_entry(
|
||||
config_entry, unique_id=config_entry.data[CONF_USERNAME]
|
||||
)
|
||||
# If the config entry doesn't already have a unique ID, set one:
|
||||
entry_updates["unique_id"] = config_entry.data[CONF_USERNAME]
|
||||
if CONF_CODE in config_entry.data:
|
||||
# If an alarm code was provided as part of configuration.yaml, pop it out of
|
||||
# the config entry's data and move it to options:
|
||||
data = {**config_entry.data}
|
||||
entry_updates["data"] = data
|
||||
entry_updates["options"] = {
|
||||
**config_entry.options,
|
||||
CONF_CODE: data.pop(CONF_CODE),
|
||||
}
|
||||
if entry_updates:
|
||||
hass.config_entries.async_update_entry(config_entry, **entry_updates)
|
||||
|
||||
_verify_domain_control = verify_domain_control(hass, DOMAIN)
|
||||
|
||||
|
@ -309,6 +320,8 @@ async def async_setup_entry(hass, config_entry):
|
|||
]:
|
||||
async_register_admin_service(hass, DOMAIN, service, method, schema=schema)
|
||||
|
||||
config_entry.add_update_listener(async_update_options)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
|
@ -328,6 +341,12 @@ async def async_unload_entry(hass, entry):
|
|||
return True
|
||||
|
||||
|
||||
async def async_update_options(hass, config_entry):
|
||||
"""Handle an options update."""
|
||||
simplisafe = hass.data[DOMAIN][DATA_CLIENT][config_entry.entry_id]
|
||||
simplisafe.options = config_entry.options
|
||||
|
||||
|
||||
class SimpliSafeWebsocket:
|
||||
"""Define a SimpliSafe websocket "manager" object."""
|
||||
|
||||
|
@ -394,6 +413,7 @@ class SimpliSafe:
|
|||
self._emergency_refresh_token_used = False
|
||||
self._hass = hass
|
||||
self._system_notifications = {}
|
||||
self.options = config_entry.options or {}
|
||||
self.initial_event_to_use = {}
|
||||
self.systems = {}
|
||||
self.websocket = SimpliSafeWebsocket(hass, api.websocket)
|
||||
|
|
|
@ -67,10 +67,7 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
|||
"""Set up a SimpliSafe alarm control panel based on a config entry."""
|
||||
simplisafe = hass.data[DOMAIN][DATA_CLIENT][entry.entry_id]
|
||||
async_add_entities(
|
||||
[
|
||||
SimpliSafeAlarm(simplisafe, system, entry.data.get(CONF_CODE))
|
||||
for system in simplisafe.systems.values()
|
||||
],
|
||||
[SimpliSafeAlarm(simplisafe, system) for system in simplisafe.systems.values()],
|
||||
True,
|
||||
)
|
||||
|
||||
|
@ -78,11 +75,10 @@ async def async_setup_entry(hass, entry, async_add_entities):
|
|||
class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel):
|
||||
"""Representation of a SimpliSafe alarm."""
|
||||
|
||||
def __init__(self, simplisafe, system, code):
|
||||
def __init__(self, simplisafe, system):
|
||||
"""Initialize the SimpliSafe alarm."""
|
||||
super().__init__(simplisafe, system, "Alarm Control Panel")
|
||||
self._changed_by = None
|
||||
self._code = code
|
||||
self._last_event = None
|
||||
|
||||
if system.alarm_going_off:
|
||||
|
@ -125,9 +121,11 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel):
|
|||
@property
|
||||
def code_format(self):
|
||||
"""Return one or more digits/characters."""
|
||||
if not self._code:
|
||||
if not self._simplisafe.options.get(CONF_CODE):
|
||||
return None
|
||||
if isinstance(self._code, str) and re.search("^\\d+$", self._code):
|
||||
if isinstance(self._simplisafe.options[CONF_CODE], str) and re.search(
|
||||
"^\\d+$", self._simplisafe.options[CONF_CODE]
|
||||
):
|
||||
return FORMAT_NUMBER
|
||||
return FORMAT_TEXT
|
||||
|
||||
|
@ -141,16 +139,23 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel):
|
|||
"""Return the list of supported features."""
|
||||
return SUPPORT_ALARM_ARM_HOME | SUPPORT_ALARM_ARM_AWAY
|
||||
|
||||
def _validate_code(self, code, state):
|
||||
"""Validate given code."""
|
||||
check = self._code is None or code == self._code
|
||||
if not check:
|
||||
_LOGGER.warning("Wrong code entered for %s", state)
|
||||
return check
|
||||
@callback
|
||||
def _is_code_valid(self, code, state):
|
||||
"""Validate that a code matches the required one."""
|
||||
if not self._simplisafe.options.get(CONF_CODE):
|
||||
return True
|
||||
|
||||
if not code or code != self._simplisafe.options[CONF_CODE]:
|
||||
_LOGGER.warning(
|
||||
"Incorrect alarm code entered (target state: %s): %s", state, code
|
||||
)
|
||||
return False
|
||||
|
||||
return True
|
||||
|
||||
async def async_alarm_disarm(self, code=None):
|
||||
"""Send disarm command."""
|
||||
if not self._validate_code(code, "disarming"):
|
||||
if not self._is_code_valid(code, STATE_ALARM_DISARMED):
|
||||
return
|
||||
|
||||
try:
|
||||
|
@ -163,7 +168,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel):
|
|||
|
||||
async def async_alarm_arm_home(self, code=None):
|
||||
"""Send arm home command."""
|
||||
if not self._validate_code(code, "arming home"):
|
||||
if not self._is_code_valid(code, STATE_ALARM_ARMED_HOME):
|
||||
return
|
||||
|
||||
try:
|
||||
|
@ -176,7 +181,7 @@ class SimpliSafeAlarm(SimpliSafeEntity, AlarmControlPanel):
|
|||
|
||||
async def async_alarm_arm_away(self, code=None):
|
||||
"""Send arm away command."""
|
||||
if not self._validate_code(code, "arming away"):
|
||||
if not self._is_code_valid(code, STATE_ALARM_ARMED_AWAY):
|
||||
return
|
||||
|
||||
try:
|
||||
|
|
|
@ -5,6 +5,7 @@ import voluptuous as vol
|
|||
|
||||
from homeassistant import config_entries
|
||||
from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
|
||||
from homeassistant.core import callback
|
||||
from homeassistant.helpers import aiohttp_client
|
||||
|
||||
from .const import DOMAIN # pylint: disable=unused-import
|
||||
|
@ -34,6 +35,12 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
errors=errors if errors else {},
|
||||
)
|
||||
|
||||
@staticmethod
|
||||
@callback
|
||||
def async_get_options_flow(config_entry):
|
||||
"""Define the config flow to handle options."""
|
||||
return SimpliSafeOptionsFlowHandler(config_entry)
|
||||
|
||||
async def async_step_import(self, import_config):
|
||||
"""Import a config entry from configuration.yaml."""
|
||||
return await self.async_step_user(import_config)
|
||||
|
@ -46,17 +53,44 @@ class SimpliSafeFlowHandler(config_entries.ConfigFlow, domain=DOMAIN):
|
|||
await self.async_set_unique_id(user_input[CONF_USERNAME])
|
||||
self._abort_if_unique_id_configured()
|
||||
|
||||
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
|
||||
user_input[CONF_USERNAME], user_input[CONF_PASSWORD], websession
|
||||
)
|
||||
except SimplipyError:
|
||||
return await self._show_form(errors={"base": "invalid_credentials"})
|
||||
|
||||
return self.async_create_entry(
|
||||
title=user_input[CONF_USERNAME],
|
||||
data={CONF_USERNAME: username, CONF_TOKEN: simplisafe.refresh_token},
|
||||
data={
|
||||
CONF_USERNAME: user_input[CONF_USERNAME],
|
||||
CONF_TOKEN: simplisafe.refresh_token,
|
||||
CONF_CODE: user_input.get(CONF_CODE),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class SimpliSafeOptionsFlowHandler(config_entries.OptionsFlow):
|
||||
"""Handle a SimpliSafe options flow."""
|
||||
|
||||
def __init__(self, config_entry):
|
||||
"""Initialize."""
|
||||
self.config_entry = config_entry
|
||||
|
||||
async def async_step_init(self, user_input=None):
|
||||
"""Manage the options."""
|
||||
if user_input is not None:
|
||||
return self.async_create_entry(title="", data=user_input)
|
||||
|
||||
return self.async_show_form(
|
||||
step_id="init",
|
||||
data_schema=vol.Schema(
|
||||
{
|
||||
vol.Optional(
|
||||
CONF_CODE, default=self.config_entry.options.get(CONF_CODE),
|
||||
): str
|
||||
}
|
||||
),
|
||||
)
|
||||
|
|
|
@ -6,8 +6,7 @@
|
|||
"title": "Fill in your information",
|
||||
"data": {
|
||||
"username": "Email Address",
|
||||
"password": "Password",
|
||||
"code": "Code (for Home Assistant)"
|
||||
"password": "Password"
|
||||
}
|
||||
}
|
||||
},
|
||||
|
@ -18,5 +17,15 @@
|
|||
"abort": {
|
||||
"already_configured": "This SimpliSafe account is already in use."
|
||||
}
|
||||
},
|
||||
"options": {
|
||||
"step": {
|
||||
"init": {
|
||||
"title": "Configure SimpliSafe",
|
||||
"data": {
|
||||
"code": "Code (used in Home Assistant UI)"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,15 +1,16 @@
|
|||
"""Define tests for the SimpliSafe config flow."""
|
||||
import json
|
||||
from unittest.mock import MagicMock, PropertyMock, mock_open, patch
|
||||
from unittest.mock import MagicMock, PropertyMock, mock_open
|
||||
|
||||
from asynctest import patch
|
||||
from simplipy.errors import SimplipyError
|
||||
|
||||
from homeassistant import data_entry_flow
|
||||
from homeassistant.components.simplisafe import DOMAIN, config_flow
|
||||
from homeassistant.config_entries import SOURCE_USER
|
||||
from homeassistant.const import CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
|
||||
from homeassistant.components.simplisafe import DOMAIN
|
||||
from homeassistant.config_entries import SOURCE_IMPORT, SOURCE_USER
|
||||
from homeassistant.const import CONF_CODE, CONF_PASSWORD, CONF_TOKEN, CONF_USERNAME
|
||||
|
||||
from tests.common import MockConfigEntry, mock_coro
|
||||
from tests.common import MockConfigEntry
|
||||
|
||||
|
||||
def mock_api():
|
||||
|
@ -39,55 +40,83 @@ async def test_invalid_credentials(hass):
|
|||
"""Test that invalid credentials throws an error."""
|
||||
conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}
|
||||
|
||||
flow = config_flow.SimpliSafeFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_USER}
|
||||
with patch(
|
||||
"simplipy.API.login_via_credentials", side_effect=SimplipyError,
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=conf
|
||||
)
|
||||
assert result["errors"] == {"base": "invalid_credentials"}
|
||||
|
||||
|
||||
async def test_options_flow(hass):
|
||||
"""Test config flow options."""
|
||||
conf = {CONF_USERNAME: "user@email.com", CONF_PASSWORD: "password"}
|
||||
|
||||
config_entry = MockConfigEntry(
|
||||
domain=DOMAIN, unique_id="abcde12345", data=conf, options={CONF_CODE: "1234"},
|
||||
)
|
||||
config_entry.add_to_hass(hass)
|
||||
|
||||
with patch(
|
||||
"simplipy.API.login_via_credentials",
|
||||
return_value=mock_coro(exception=SimplipyError),
|
||||
"homeassistant.components.simplisafe.async_setup_entry", return_value=True
|
||||
):
|
||||
result = await flow.async_step_user(user_input=conf)
|
||||
assert result["errors"] == {"base": "invalid_credentials"}
|
||||
result = await hass.config_entries.options.async_init(config_entry.entry_id)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "init"
|
||||
|
||||
result = await hass.config_entries.options.async_configure(
|
||||
result["flow_id"], user_input={CONF_CODE: "4321"}
|
||||
)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_CREATE_ENTRY
|
||||
assert config_entry.options == {CONF_CODE: "4321"}
|
||||
|
||||
|
||||
async def test_show_form(hass):
|
||||
"""Test that the form is served with no input."""
|
||||
flow = config_flow.SimpliSafeFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_USER}
|
||||
with patch(
|
||||
"homeassistant.components.simplisafe.async_setup_entry", return_value=True
|
||||
):
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_IMPORT}
|
||||
)
|
||||
|
||||
result = await flow.async_step_user(user_input=None)
|
||||
|
||||
assert result["type"] == data_entry_flow.RESULT_TYPE_FORM
|
||||
assert result["step_id"] == "user"
|
||||
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
|
||||
flow.context = {"source": SOURCE_USER}
|
||||
conf = {
|
||||
CONF_USERNAME: "user@email.com",
|
||||
CONF_PASSWORD: "password",
|
||||
CONF_CODE: "1234",
|
||||
}
|
||||
|
||||
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()),
|
||||
"homeassistant.components.simplisafe.async_setup_entry", return_value=True
|
||||
), patch("simplipy.API.login_via_credentials", return_value=mock_api()), patch(
|
||||
"homeassistant.util.json.open", mop, create=True
|
||||
), patch(
|
||||
"homeassistant.util.json.os.open", return_value=0
|
||||
), patch(
|
||||
"homeassistant.util.json.os.replace"
|
||||
):
|
||||
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)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=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",
|
||||
}
|
||||
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_CODE: "1234",
|
||||
}
|
||||
|
||||
|
||||
async def test_step_user(hass):
|
||||
|
@ -95,26 +124,28 @@ async def test_step_user(hass):
|
|||
conf = {
|
||||
CONF_USERNAME: "user@email.com",
|
||||
CONF_PASSWORD: "password",
|
||||
CONF_CODE: "1234",
|
||||
}
|
||||
|
||||
flow = config_flow.SimpliSafeFlowHandler()
|
||||
flow.hass = hass
|
||||
flow.context = {"source": SOURCE_USER}
|
||||
|
||||
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()),
|
||||
"homeassistant.components.simplisafe.async_setup_entry", return_value=True
|
||||
), patch("simplipy.API.login_via_credentials", return_value=mock_api()), patch(
|
||||
"homeassistant.util.json.open", mop, create=True
|
||||
), patch(
|
||||
"homeassistant.util.json.os.open", return_value=0
|
||||
), patch(
|
||||
"homeassistant.util.json.os.replace"
|
||||
):
|
||||
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)
|
||||
result = await hass.config_entries.flow.async_init(
|
||||
DOMAIN, context={"source": SOURCE_USER}, data=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",
|
||||
}
|
||||
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_CODE: "1234",
|
||||
}
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue