Add Risco integration (#36930)
* Risco integration * Fix lint errors * Raise ConfigEntryNotReady if can't connect * Gracefully handle shutdown * pass session to pyrisco * minor change to init * Fix retries * Add exception log * Remove retries * Address code review comments * Remove log
This commit is contained in:
parent
83b9c6188d
commit
1b8d9f7cc4
13 changed files with 706 additions and 0 deletions
|
@ -346,6 +346,7 @@ homeassistant/components/random/* @fabaff
|
||||||
homeassistant/components/repetier/* @MTrab
|
homeassistant/components/repetier/* @MTrab
|
||||||
homeassistant/components/rfxtrx/* @danielhiversen @elupus
|
homeassistant/components/rfxtrx/* @danielhiversen @elupus
|
||||||
homeassistant/components/ring/* @balloob
|
homeassistant/components/ring/* @balloob
|
||||||
|
homeassistant/components/risco/* @OnFreund
|
||||||
homeassistant/components/rmvtransport/* @cgtobi
|
homeassistant/components/rmvtransport/* @cgtobi
|
||||||
homeassistant/components/roku/* @ctalkington
|
homeassistant/components/roku/* @ctalkington
|
||||||
homeassistant/components/roomba/* @pschmitt @cyr-ius @shenxn
|
homeassistant/components/roomba/* @pschmitt @cyr-ius @shenxn
|
||||||
|
|
89
homeassistant/components/risco/__init__.py
Normal file
89
homeassistant/components/risco/__init__.py
Normal file
|
@ -0,0 +1,89 @@
|
||||||
|
"""The Risco integration."""
|
||||||
|
import asyncio
|
||||||
|
from datetime import timedelta
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pyrisco import CannotConnectError, OperationError, RiscoAPI, UnauthorizedError
|
||||||
|
|
||||||
|
from homeassistant.config_entries import ConfigEntry
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_PIN, CONF_USERNAME
|
||||||
|
from homeassistant.core import HomeAssistant
|
||||||
|
from homeassistant.exceptions import ConfigEntryNotReady
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
from homeassistant.helpers.update_coordinator import DataUpdateCoordinator, UpdateFailed
|
||||||
|
|
||||||
|
from .const import DATA_COORDINATOR, DOMAIN
|
||||||
|
|
||||||
|
PLATFORMS = ["alarm_control_panel"]
|
||||||
|
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup(hass: HomeAssistant, config: dict):
|
||||||
|
"""Set up the Risco component."""
|
||||||
|
hass.data.setdefault(DOMAIN, {})
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
"""Set up Risco from a config entry."""
|
||||||
|
data = entry.data
|
||||||
|
risco = RiscoAPI(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN])
|
||||||
|
try:
|
||||||
|
await risco.login(async_get_clientsession(hass))
|
||||||
|
except CannotConnectError as error:
|
||||||
|
raise ConfigEntryNotReady() from error
|
||||||
|
except UnauthorizedError:
|
||||||
|
_LOGGER.exception("Failed to login to Risco cloud")
|
||||||
|
return False
|
||||||
|
|
||||||
|
coordinator = RiscoDataUpdateCoordinator(hass, risco)
|
||||||
|
await coordinator.async_refresh()
|
||||||
|
|
||||||
|
hass.data[DOMAIN][entry.entry_id] = {
|
||||||
|
DATA_COORDINATOR: coordinator,
|
||||||
|
}
|
||||||
|
|
||||||
|
for component in PLATFORMS:
|
||||||
|
hass.async_create_task(
|
||||||
|
hass.config_entries.async_forward_entry_setup(entry, component)
|
||||||
|
)
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
|
async def async_unload_entry(hass: HomeAssistant, entry: ConfigEntry):
|
||||||
|
"""Unload a config entry."""
|
||||||
|
unload_ok = all(
|
||||||
|
await asyncio.gather(
|
||||||
|
*[
|
||||||
|
hass.config_entries.async_forward_entry_unload(entry, component)
|
||||||
|
for component in PLATFORMS
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
if unload_ok:
|
||||||
|
hass.data[DOMAIN].pop(entry.entry_id)
|
||||||
|
|
||||||
|
return unload_ok
|
||||||
|
|
||||||
|
|
||||||
|
class RiscoDataUpdateCoordinator(DataUpdateCoordinator):
|
||||||
|
"""Class to manage fetching risco data."""
|
||||||
|
|
||||||
|
def __init__(self, hass, risco):
|
||||||
|
"""Initialize global risco data updater."""
|
||||||
|
self.risco = risco
|
||||||
|
interval = timedelta(seconds=30)
|
||||||
|
super().__init__(
|
||||||
|
hass, _LOGGER, name=DOMAIN, update_interval=interval,
|
||||||
|
)
|
||||||
|
|
||||||
|
async def _async_update_data(self):
|
||||||
|
"""Fetch data from risco."""
|
||||||
|
try:
|
||||||
|
return await self.risco.get_state()
|
||||||
|
except (CannotConnectError, UnauthorizedError, OperationError) as error:
|
||||||
|
raise UpdateFailed from error
|
162
homeassistant/components/risco/alarm_control_panel.py
Normal file
162
homeassistant/components/risco/alarm_control_panel.py
Normal file
|
@ -0,0 +1,162 @@
|
||||||
|
"""Support for Risco alarms."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from homeassistant.components.alarm_control_panel import AlarmControlPanelEntity
|
||||||
|
from homeassistant.components.alarm_control_panel.const import (
|
||||||
|
SUPPORT_ALARM_ARM_AWAY,
|
||||||
|
SUPPORT_ALARM_ARM_CUSTOM_BYPASS,
|
||||||
|
SUPPORT_ALARM_ARM_HOME,
|
||||||
|
SUPPORT_ALARM_ARM_NIGHT,
|
||||||
|
)
|
||||||
|
from homeassistant.const import (
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_ARMED_NIGHT,
|
||||||
|
STATE_ALARM_ARMING,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_ALARM_TRIGGERED,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
)
|
||||||
|
|
||||||
|
from .const import DATA_COORDINATOR, DOMAIN
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
SUPPORTED_STATES = [
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_ARMED_NIGHT,
|
||||||
|
STATE_ALARM_ARMED_CUSTOM_BYPASS,
|
||||||
|
STATE_ALARM_TRIGGERED,
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
async def async_setup_entry(hass, config_entry, async_add_entities):
|
||||||
|
"""Set up the Risco alarm control panel."""
|
||||||
|
coordinator = hass.data[DOMAIN][config_entry.entry_id][DATA_COORDINATOR]
|
||||||
|
entities = [
|
||||||
|
RiscoAlarm(hass, coordinator, partition_id)
|
||||||
|
for partition_id in coordinator.data.partitions.keys()
|
||||||
|
]
|
||||||
|
|
||||||
|
async_add_entities(entities, False)
|
||||||
|
|
||||||
|
|
||||||
|
class RiscoAlarm(AlarmControlPanelEntity):
|
||||||
|
"""Representation of a Risco partition."""
|
||||||
|
|
||||||
|
def __init__(self, hass, coordinator, partition_id):
|
||||||
|
"""Init the partition."""
|
||||||
|
self._hass = hass
|
||||||
|
self._coordinator = coordinator
|
||||||
|
self._partition_id = partition_id
|
||||||
|
self._partition = self._coordinator.data.partitions[self._partition_id]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def should_poll(self):
|
||||||
|
"""No need to poll. Coordinator notifies entity of updates."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
@property
|
||||||
|
def available(self):
|
||||||
|
"""Return if entity is available."""
|
||||||
|
return self._coordinator.last_update_success
|
||||||
|
|
||||||
|
def _refresh_from_coordinator(self):
|
||||||
|
self._partition = self._coordinator.data.partitions[self._partition_id]
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_added_to_hass(self):
|
||||||
|
"""When entity is added to hass."""
|
||||||
|
self.async_on_remove(
|
||||||
|
self._coordinator.async_add_listener(self._refresh_from_coordinator)
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def _risco(self):
|
||||||
|
"""Return the Risco API object."""
|
||||||
|
return self._coordinator.risco
|
||||||
|
|
||||||
|
@property
|
||||||
|
def device_info(self):
|
||||||
|
"""Return device info for this device."""
|
||||||
|
return {
|
||||||
|
"identifiers": {(DOMAIN, self.unique_id)},
|
||||||
|
"name": self.name,
|
||||||
|
"manufacturer": "Risco",
|
||||||
|
}
|
||||||
|
|
||||||
|
@property
|
||||||
|
def name(self):
|
||||||
|
"""Return the name of the partition."""
|
||||||
|
return f"Risco {self._risco.site_name} Partition {self._partition_id}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def unique_id(self):
|
||||||
|
"""Return a unique id for that partition."""
|
||||||
|
return f"{self._risco.site_uuid}_{self._partition_id}"
|
||||||
|
|
||||||
|
@property
|
||||||
|
def state(self):
|
||||||
|
"""Return the state of the device."""
|
||||||
|
if self._partition.triggered:
|
||||||
|
return STATE_ALARM_TRIGGERED
|
||||||
|
if self._partition.arming:
|
||||||
|
return STATE_ALARM_ARMING
|
||||||
|
if self._partition.armed:
|
||||||
|
return STATE_ALARM_ARMED_AWAY
|
||||||
|
if self._partition.partially_armed:
|
||||||
|
return STATE_ALARM_ARMED_HOME
|
||||||
|
if self._partition.disarmed:
|
||||||
|
return STATE_ALARM_DISARMED
|
||||||
|
|
||||||
|
return STATE_UNKNOWN
|
||||||
|
|
||||||
|
@property
|
||||||
|
def supported_features(self):
|
||||||
|
"""Return the list of supported features."""
|
||||||
|
return (
|
||||||
|
SUPPORT_ALARM_ARM_HOME
|
||||||
|
| SUPPORT_ALARM_ARM_AWAY
|
||||||
|
| SUPPORT_ALARM_ARM_NIGHT
|
||||||
|
| SUPPORT_ALARM_ARM_CUSTOM_BYPASS
|
||||||
|
)
|
||||||
|
|
||||||
|
@property
|
||||||
|
def code_arm_required(self):
|
||||||
|
"""Whether the code is required for arm actions."""
|
||||||
|
return False
|
||||||
|
|
||||||
|
async def async_alarm_disarm(self, code=None):
|
||||||
|
"""Send disarm command."""
|
||||||
|
await self._call_alarm_method("disarm")
|
||||||
|
|
||||||
|
async def async_alarm_arm_home(self, code=None):
|
||||||
|
"""Send arm home command."""
|
||||||
|
await self._call_alarm_method("partial_arm")
|
||||||
|
|
||||||
|
async def async_alarm_arm_night(self, code=None):
|
||||||
|
"""Send arm night command."""
|
||||||
|
await self._call_alarm_method("partial_arm")
|
||||||
|
|
||||||
|
async def async_alarm_arm_custom_bypass(self, code=None):
|
||||||
|
"""Send arm custom bypass command."""
|
||||||
|
await self._call_alarm_method("partial_arm")
|
||||||
|
|
||||||
|
async def async_alarm_arm_away(self, code=None):
|
||||||
|
"""Send arm away command."""
|
||||||
|
await self._call_alarm_method("arm")
|
||||||
|
|
||||||
|
async def _call_alarm_method(self, method, code=None):
|
||||||
|
alarm = await getattr(self._risco, method)(self._partition_id)
|
||||||
|
self._partition = alarm.partitions[self._partition_id]
|
||||||
|
self.async_write_ha_state()
|
||||||
|
|
||||||
|
async def async_update(self):
|
||||||
|
"""Update the entity.
|
||||||
|
|
||||||
|
Only used by the generic entity update service.
|
||||||
|
"""
|
||||||
|
await self._coordinator.async_request_refresh()
|
58
homeassistant/components/risco/config_flow.py
Normal file
58
homeassistant/components/risco/config_flow.py
Normal file
|
@ -0,0 +1,58 @@
|
||||||
|
"""Config flow for Risco integration."""
|
||||||
|
import logging
|
||||||
|
|
||||||
|
from pyrisco import CannotConnectError, RiscoAPI, UnauthorizedError
|
||||||
|
import voluptuous as vol
|
||||||
|
|
||||||
|
from homeassistant import config_entries, core
|
||||||
|
from homeassistant.const import CONF_PASSWORD, CONF_PIN, CONF_USERNAME
|
||||||
|
from homeassistant.helpers.aiohttp_client import async_get_clientsession
|
||||||
|
|
||||||
|
from .const import DOMAIN # pylint:disable=unused-import
|
||||||
|
|
||||||
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
|
DATA_SCHEMA = vol.Schema({CONF_USERNAME: str, CONF_PASSWORD: str, CONF_PIN: str})
|
||||||
|
|
||||||
|
|
||||||
|
async def validate_input(hass: core.HomeAssistant, data):
|
||||||
|
"""Validate the user input allows us to connect.
|
||||||
|
|
||||||
|
Data has the keys from DATA_SCHEMA with values provided by the user.
|
||||||
|
"""
|
||||||
|
risco = RiscoAPI(data[CONF_USERNAME], data[CONF_PASSWORD], data[CONF_PIN])
|
||||||
|
|
||||||
|
try:
|
||||||
|
await risco.login(async_get_clientsession(hass))
|
||||||
|
finally:
|
||||||
|
await risco.close()
|
||||||
|
|
||||||
|
return {"title": risco.site_name}
|
||||||
|
|
||||||
|
|
||||||
|
class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
|
||||||
|
"""Handle a config flow for Risco."""
|
||||||
|
|
||||||
|
VERSION = 1
|
||||||
|
CONNECTION_CLASS = config_entries.CONN_CLASS_CLOUD_POLL
|
||||||
|
|
||||||
|
async def async_step_user(self, user_input=None):
|
||||||
|
"""Handle the initial step."""
|
||||||
|
errors = {}
|
||||||
|
if user_input is not None:
|
||||||
|
try:
|
||||||
|
info = await validate_input(self.hass, user_input)
|
||||||
|
|
||||||
|
return self.async_create_entry(title=info["title"], data=user_input)
|
||||||
|
except CannotConnectError:
|
||||||
|
errors["base"] = "cannot_connect"
|
||||||
|
except UnauthorizedError:
|
||||||
|
errors["base"] = "invalid_auth"
|
||||||
|
except Exception: # pylint: disable=broad-except
|
||||||
|
_LOGGER.exception("Unexpected exception")
|
||||||
|
errors["base"] = "unknown"
|
||||||
|
|
||||||
|
return self.async_show_form(
|
||||||
|
step_id="user", data_schema=DATA_SCHEMA, errors=errors
|
||||||
|
)
|
5
homeassistant/components/risco/const.py
Normal file
5
homeassistant/components/risco/const.py
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
"""Constants for the Risco integration."""
|
||||||
|
|
||||||
|
DOMAIN = "risco"
|
||||||
|
|
||||||
|
DATA_COORDINATOR = "risco"
|
12
homeassistant/components/risco/manifest.json
Normal file
12
homeassistant/components/risco/manifest.json
Normal file
|
@ -0,0 +1,12 @@
|
||||||
|
{
|
||||||
|
"domain": "risco",
|
||||||
|
"name": "Risco",
|
||||||
|
"config_flow": true,
|
||||||
|
"documentation": "https://www.home-assistant.io/integrations/risco",
|
||||||
|
"requirements": [
|
||||||
|
"pyrisco==0.2.1"
|
||||||
|
],
|
||||||
|
"codeowners": [
|
||||||
|
"@OnFreund"
|
||||||
|
]
|
||||||
|
}
|
21
homeassistant/components/risco/strings.json
Normal file
21
homeassistant/components/risco/strings.json
Normal file
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"config": {
|
||||||
|
"step": {
|
||||||
|
"user": {
|
||||||
|
"data": {
|
||||||
|
"username": "[%key:common::config_flow::data::username%]",
|
||||||
|
"password": "[%key:common::config_flow::data::password%]",
|
||||||
|
"pin": "Pin code"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"error": {
|
||||||
|
"cannot_connect": "[%key:common::config_flow::error::cannot_connect%]",
|
||||||
|
"invalid_auth": "[%key:common::config_flow::error::invalid_auth%]",
|
||||||
|
"unknown": "[%key:common::config_flow::error::unknown%]"
|
||||||
|
},
|
||||||
|
"abort": {
|
||||||
|
"already_configured": "[%key:common::config_flow::abort::already_configured_device%]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -144,6 +144,7 @@ FLOWS = [
|
||||||
"rachio",
|
"rachio",
|
||||||
"rainmachine",
|
"rainmachine",
|
||||||
"ring",
|
"ring",
|
||||||
|
"risco",
|
||||||
"roku",
|
"roku",
|
||||||
"roomba",
|
"roomba",
|
||||||
"roon",
|
"roon",
|
||||||
|
|
|
@ -1592,6 +1592,9 @@ pyrecswitch==1.0.2
|
||||||
# homeassistant.components.repetier
|
# homeassistant.components.repetier
|
||||||
pyrepetier==3.0.5
|
pyrepetier==3.0.5
|
||||||
|
|
||||||
|
# homeassistant.components.risco
|
||||||
|
pyrisco==0.2.1
|
||||||
|
|
||||||
# homeassistant.components.sabnzbd
|
# homeassistant.components.sabnzbd
|
||||||
pysabnzbd==1.1.0
|
pysabnzbd==1.1.0
|
||||||
|
|
||||||
|
|
|
@ -754,6 +754,9 @@ pyps4-2ndscreen==1.1.1
|
||||||
# homeassistant.components.qwikswitch
|
# homeassistant.components.qwikswitch
|
||||||
pyqwikswitch==0.93
|
pyqwikswitch==0.93
|
||||||
|
|
||||||
|
# homeassistant.components.risco
|
||||||
|
pyrisco==0.2.1
|
||||||
|
|
||||||
# homeassistant.components.acer_projector
|
# homeassistant.components.acer_projector
|
||||||
# homeassistant.components.zha
|
# homeassistant.components.zha
|
||||||
pyserial==3.4
|
pyserial==3.4
|
||||||
|
|
1
tests/components/risco/__init__.py
Normal file
1
tests/components/risco/__init__.py
Normal file
|
@ -0,0 +1 @@
|
||||||
|
"""Tests for the Risco integration."""
|
237
tests/components/risco/test_alarm_control_panel.py
Normal file
237
tests/components/risco/test_alarm_control_panel.py
Normal file
|
@ -0,0 +1,237 @@
|
||||||
|
"""Tests for the Risco alarm control panel device."""
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from homeassistant.components.alarm_control_panel import DOMAIN as ALARM_DOMAIN
|
||||||
|
from homeassistant.components.risco import CannotConnectError, UnauthorizedError
|
||||||
|
from homeassistant.components.risco.const import DOMAIN
|
||||||
|
from homeassistant.const import (
|
||||||
|
CONF_PASSWORD,
|
||||||
|
CONF_PIN,
|
||||||
|
CONF_USERNAME,
|
||||||
|
SERVICE_ALARM_ARM_AWAY,
|
||||||
|
SERVICE_ALARM_ARM_CUSTOM_BYPASS,
|
||||||
|
SERVICE_ALARM_ARM_HOME,
|
||||||
|
SERVICE_ALARM_ARM_NIGHT,
|
||||||
|
SERVICE_ALARM_DISARM,
|
||||||
|
STATE_ALARM_ARMED_AWAY,
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
STATE_ALARM_ARMING,
|
||||||
|
STATE_ALARM_DISARMED,
|
||||||
|
STATE_ALARM_TRIGGERED,
|
||||||
|
STATE_UNKNOWN,
|
||||||
|
)
|
||||||
|
from homeassistant.helpers.entity_component import async_update_entity
|
||||||
|
|
||||||
|
from tests.async_mock import AsyncMock, MagicMock, PropertyMock, patch
|
||||||
|
from tests.common import MockConfigEntry
|
||||||
|
|
||||||
|
TEST_CONFIG = {
|
||||||
|
CONF_USERNAME: "test-username",
|
||||||
|
CONF_PASSWORD: "test-password",
|
||||||
|
CONF_PIN: "1234",
|
||||||
|
}
|
||||||
|
TEST_SITE_UUID = "test-site-uuid"
|
||||||
|
TEST_SITE_NAME = "test-site-name"
|
||||||
|
FIRST_ENTITY_ID = "alarm_control_panel.risco_test_site_name_partition_0"
|
||||||
|
SECOND_ENTITY_ID = "alarm_control_panel.risco_test_site_name_partition_1"
|
||||||
|
|
||||||
|
|
||||||
|
def _partition_mock():
|
||||||
|
return MagicMock(
|
||||||
|
triggered=False,
|
||||||
|
arming=False,
|
||||||
|
armed=False,
|
||||||
|
disarmed=False,
|
||||||
|
partially_armed=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def two_part_alarm():
|
||||||
|
"""Fixture to mock alarm with two partitions."""
|
||||||
|
partition_mocks = {0: _partition_mock(), 1: _partition_mock()}
|
||||||
|
alarm_mock = MagicMock()
|
||||||
|
with patch.object(
|
||||||
|
partition_mocks[0], "id", new_callable=PropertyMock(return_value=0)
|
||||||
|
), patch.object(
|
||||||
|
partition_mocks[1], "id", new_callable=PropertyMock(return_value=1)
|
||||||
|
), patch.object(
|
||||||
|
alarm_mock,
|
||||||
|
"partitions",
|
||||||
|
new_callable=PropertyMock(return_value=partition_mocks),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.risco.RiscoAPI.get_state",
|
||||||
|
AsyncMock(return_value=alarm_mock),
|
||||||
|
):
|
||||||
|
yield alarm_mock
|
||||||
|
|
||||||
|
|
||||||
|
async def _setup_risco(hass, alarm=MagicMock()):
|
||||||
|
config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG)
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.risco.RiscoAPI.login", return_value=True,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.risco.RiscoAPI.site_uuid",
|
||||||
|
new_callable=PropertyMock(return_value=TEST_SITE_UUID),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.risco.RiscoAPI.site_name",
|
||||||
|
new_callable=PropertyMock(return_value=TEST_SITE_NAME),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.risco.RiscoAPI.close", AsyncMock()
|
||||||
|
):
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
return config_entry
|
||||||
|
|
||||||
|
|
||||||
|
async def test_cannot_connect(hass):
|
||||||
|
"""Test connection error."""
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.risco.RiscoAPI.login", side_effect=CannotConnectError,
|
||||||
|
):
|
||||||
|
config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
registry = await hass.helpers.entity_registry.async_get_registry()
|
||||||
|
assert not registry.async_is_registered(FIRST_ENTITY_ID)
|
||||||
|
assert not registry.async_is_registered(SECOND_ENTITY_ID)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_unauthorized(hass):
|
||||||
|
"""Test unauthorized error."""
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.risco.RiscoAPI.login", side_effect=UnauthorizedError,
|
||||||
|
):
|
||||||
|
config_entry = MockConfigEntry(domain=DOMAIN, data=TEST_CONFIG)
|
||||||
|
config_entry.add_to_hass(hass)
|
||||||
|
await hass.config_entries.async_setup(config_entry.entry_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
registry = await hass.helpers.entity_registry.async_get_registry()
|
||||||
|
assert not registry.async_is_registered(FIRST_ENTITY_ID)
|
||||||
|
assert not registry.async_is_registered(SECOND_ENTITY_ID)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_setup(hass, two_part_alarm):
|
||||||
|
"""Test entity setup."""
|
||||||
|
registry = await hass.helpers.entity_registry.async_get_registry()
|
||||||
|
|
||||||
|
assert not registry.async_is_registered(FIRST_ENTITY_ID)
|
||||||
|
assert not registry.async_is_registered(SECOND_ENTITY_ID)
|
||||||
|
|
||||||
|
await _setup_risco(hass, two_part_alarm)
|
||||||
|
|
||||||
|
assert registry.async_is_registered(FIRST_ENTITY_ID)
|
||||||
|
assert registry.async_is_registered(SECOND_ENTITY_ID)
|
||||||
|
|
||||||
|
registry = await hass.helpers.device_registry.async_get_registry()
|
||||||
|
device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_0")}, {})
|
||||||
|
assert device is not None
|
||||||
|
assert device.manufacturer == "Risco"
|
||||||
|
|
||||||
|
device = registry.async_get_device({(DOMAIN, TEST_SITE_UUID + "_1")}, {})
|
||||||
|
assert device is not None
|
||||||
|
assert device.manufacturer == "Risco"
|
||||||
|
|
||||||
|
|
||||||
|
async def _check_state(hass, alarm, property, state, entity_id, partition_id):
|
||||||
|
with patch.object(alarm.partitions[partition_id], property, return_value=True):
|
||||||
|
await async_update_entity(hass, entity_id)
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
|
||||||
|
assert hass.states.get(entity_id).state == state
|
||||||
|
|
||||||
|
|
||||||
|
async def test_states(hass, two_part_alarm):
|
||||||
|
"""Test the various alarm states."""
|
||||||
|
await _setup_risco(hass, two_part_alarm)
|
||||||
|
|
||||||
|
assert hass.states.get(FIRST_ENTITY_ID).state == STATE_UNKNOWN
|
||||||
|
await _check_state(
|
||||||
|
hass, two_part_alarm, "triggered", STATE_ALARM_TRIGGERED, FIRST_ENTITY_ID, 0
|
||||||
|
)
|
||||||
|
await _check_state(
|
||||||
|
hass, two_part_alarm, "triggered", STATE_ALARM_TRIGGERED, SECOND_ENTITY_ID, 1
|
||||||
|
)
|
||||||
|
await _check_state(
|
||||||
|
hass, two_part_alarm, "arming", STATE_ALARM_ARMING, FIRST_ENTITY_ID, 0
|
||||||
|
)
|
||||||
|
await _check_state(
|
||||||
|
hass, two_part_alarm, "arming", STATE_ALARM_ARMING, SECOND_ENTITY_ID, 1
|
||||||
|
)
|
||||||
|
await _check_state(
|
||||||
|
hass, two_part_alarm, "armed", STATE_ALARM_ARMED_AWAY, FIRST_ENTITY_ID, 0
|
||||||
|
)
|
||||||
|
await _check_state(
|
||||||
|
hass, two_part_alarm, "armed", STATE_ALARM_ARMED_AWAY, SECOND_ENTITY_ID, 1
|
||||||
|
)
|
||||||
|
await _check_state(
|
||||||
|
hass,
|
||||||
|
two_part_alarm,
|
||||||
|
"partially_armed",
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
FIRST_ENTITY_ID,
|
||||||
|
0,
|
||||||
|
)
|
||||||
|
await _check_state(
|
||||||
|
hass,
|
||||||
|
two_part_alarm,
|
||||||
|
"partially_armed",
|
||||||
|
STATE_ALARM_ARMED_HOME,
|
||||||
|
SECOND_ENTITY_ID,
|
||||||
|
1,
|
||||||
|
)
|
||||||
|
await _check_state(
|
||||||
|
hass, two_part_alarm, "disarmed", STATE_ALARM_DISARMED, FIRST_ENTITY_ID, 0
|
||||||
|
)
|
||||||
|
await _check_state(
|
||||||
|
hass, two_part_alarm, "disarmed", STATE_ALARM_DISARMED, SECOND_ENTITY_ID, 1
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def _test_servie_call(hass, service, method, entity_id, partition_id):
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.risco.RiscoAPI." + method, AsyncMock()
|
||||||
|
) as set_mock:
|
||||||
|
await _call_alarm_service(hass, service, entity_id)
|
||||||
|
set_mock.assert_awaited_once_with(partition_id)
|
||||||
|
|
||||||
|
|
||||||
|
async def _call_alarm_service(hass, service, entity_id):
|
||||||
|
data = {"entity_id": entity_id}
|
||||||
|
|
||||||
|
await hass.services.async_call(
|
||||||
|
ALARM_DOMAIN, service, service_data=data, blocking=True
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
async def test_sets(hass, two_part_alarm):
|
||||||
|
"""Test settings the various modes."""
|
||||||
|
await _setup_risco(hass, two_part_alarm)
|
||||||
|
|
||||||
|
await _test_servie_call(hass, SERVICE_ALARM_DISARM, "disarm", FIRST_ENTITY_ID, 0)
|
||||||
|
await _test_servie_call(hass, SERVICE_ALARM_DISARM, "disarm", SECOND_ENTITY_ID, 1)
|
||||||
|
await _test_servie_call(hass, SERVICE_ALARM_ARM_AWAY, "arm", FIRST_ENTITY_ID, 0)
|
||||||
|
await _test_servie_call(hass, SERVICE_ALARM_ARM_AWAY, "arm", SECOND_ENTITY_ID, 1)
|
||||||
|
await _test_servie_call(
|
||||||
|
hass, SERVICE_ALARM_ARM_HOME, "partial_arm", FIRST_ENTITY_ID, 0
|
||||||
|
)
|
||||||
|
await _test_servie_call(
|
||||||
|
hass, SERVICE_ALARM_ARM_HOME, "partial_arm", SECOND_ENTITY_ID, 1
|
||||||
|
)
|
||||||
|
await _test_servie_call(
|
||||||
|
hass, SERVICE_ALARM_ARM_NIGHT, "partial_arm", FIRST_ENTITY_ID, 0
|
||||||
|
)
|
||||||
|
await _test_servie_call(
|
||||||
|
hass, SERVICE_ALARM_ARM_NIGHT, "partial_arm", SECOND_ENTITY_ID, 1
|
||||||
|
)
|
||||||
|
await _test_servie_call(
|
||||||
|
hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "partial_arm", FIRST_ENTITY_ID, 0
|
||||||
|
)
|
||||||
|
await _test_servie_call(
|
||||||
|
hass, SERVICE_ALARM_ARM_CUSTOM_BYPASS, "partial_arm", SECOND_ENTITY_ID, 1
|
||||||
|
)
|
113
tests/components/risco/test_config_flow.py
Normal file
113
tests/components/risco/test_config_flow.py
Normal file
|
@ -0,0 +1,113 @@
|
||||||
|
"""Test the Risco config flow."""
|
||||||
|
from homeassistant import config_entries, setup
|
||||||
|
from homeassistant.components.risco.config_flow import (
|
||||||
|
CannotConnectError,
|
||||||
|
UnauthorizedError,
|
||||||
|
)
|
||||||
|
from homeassistant.components.risco.const import DOMAIN
|
||||||
|
|
||||||
|
from tests.async_mock import AsyncMock, PropertyMock, patch
|
||||||
|
|
||||||
|
TEST_SITE_NAME = "test-site-name"
|
||||||
|
TEST_DATA = {
|
||||||
|
"username": "test-username",
|
||||||
|
"password": "test-password",
|
||||||
|
"pin": "1234",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form(hass):
|
||||||
|
"""Test we get the form."""
|
||||||
|
await setup.async_setup_component(hass, "persistent_notification", {})
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
assert result["type"] == "form"
|
||||||
|
assert result["errors"] == {}
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.risco.config_flow.RiscoAPI.login", return_value=True,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.risco.config_flow.RiscoAPI.site_name",
|
||||||
|
new_callable=PropertyMock(return_value=TEST_SITE_NAME),
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.risco.config_flow.RiscoAPI.close", AsyncMock()
|
||||||
|
) as mock_close, patch(
|
||||||
|
"homeassistant.components.risco.async_setup", return_value=True
|
||||||
|
) as mock_setup, patch(
|
||||||
|
"homeassistant.components.risco.async_setup_entry", return_value=True,
|
||||||
|
) as mock_setup_entry:
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], TEST_DATA
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "create_entry"
|
||||||
|
assert result2["title"] == TEST_SITE_NAME
|
||||||
|
assert result2["data"] == TEST_DATA
|
||||||
|
await hass.async_block_till_done()
|
||||||
|
assert len(mock_setup.mock_calls) == 1
|
||||||
|
assert len(mock_setup_entry.mock_calls) == 1
|
||||||
|
mock_close.assert_awaited_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_invalid_auth(hass):
|
||||||
|
"""Test we handle invalid auth."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.risco.config_flow.RiscoAPI.login",
|
||||||
|
side_effect=UnauthorizedError,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.risco.config_flow.RiscoAPI.close", AsyncMock()
|
||||||
|
) as mock_close:
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], TEST_DATA
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "form"
|
||||||
|
assert result2["errors"] == {"base": "invalid_auth"}
|
||||||
|
mock_close.assert_awaited_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_cannot_connect(hass):
|
||||||
|
"""Test we handle cannot connect error."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.risco.config_flow.RiscoAPI.login",
|
||||||
|
side_effect=CannotConnectError,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.risco.config_flow.RiscoAPI.close", AsyncMock()
|
||||||
|
) as mock_close:
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], TEST_DATA
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "form"
|
||||||
|
assert result2["errors"] == {"base": "cannot_connect"}
|
||||||
|
mock_close.assert_awaited_once()
|
||||||
|
|
||||||
|
|
||||||
|
async def test_form_exception(hass):
|
||||||
|
"""Test we handle unknown exception."""
|
||||||
|
result = await hass.config_entries.flow.async_init(
|
||||||
|
DOMAIN, context={"source": config_entries.SOURCE_USER}
|
||||||
|
)
|
||||||
|
|
||||||
|
with patch(
|
||||||
|
"homeassistant.components.risco.config_flow.RiscoAPI.login",
|
||||||
|
side_effect=Exception,
|
||||||
|
), patch(
|
||||||
|
"homeassistant.components.risco.config_flow.RiscoAPI.close", AsyncMock()
|
||||||
|
) as mock_close:
|
||||||
|
result2 = await hass.config_entries.flow.async_configure(
|
||||||
|
result["flow_id"], TEST_DATA
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result2["type"] == "form"
|
||||||
|
assert result2["errors"] == {"base": "unknown"}
|
||||||
|
mock_close.assert_awaited_once()
|
Loading…
Add table
Add a link
Reference in a new issue