Skip to content
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

Add Risco integration #36930

Merged
merged 11 commits into from
Aug 22, 2020
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
Next Next commit
Risco integration
  • Loading branch information
OnFreund committed Aug 14, 2020
commit 0b072632ca8880d4525681f5913c7eee7c179721
1 change: 1 addition & 0 deletions CODEOWNERS
Validating CODEOWNERS rules …
Original file line number Diff line number Diff line change
Expand Up @@ -345,6 +345,7 @@ homeassistant/components/random/* @fabaff
homeassistant/components/repetier/* @MTrab
homeassistant/components/rfxtrx/* @danielhiversen @elupus
homeassistant/components/ring/* @balloob
homeassistant/components/risco/* @OnFreund
homeassistant/components/rmvtransport/* @cgtobi
homeassistant/components/roku/* @ctalkington
homeassistant/components/roomba/* @pschmitt @cyr-ius @shenxn
Expand Down
49 changes: 49 additions & 0 deletions homeassistant/components/risco/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
"""The Risco integration."""
import asyncio

from pyrisco import RiscoAPI

from homeassistant.config_entries import ConfigEntry
from homeassistant.const import CONF_PASSWORD, CONF_PIN, CONF_USERNAME
from homeassistant.core import HomeAssistant

from .const import DOMAIN

PLATFORMS = ["alarm_control_panel"]


async def async_setup(hass: HomeAssistant, config: dict):
"""Set up the Risco component."""
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])
await risco.login()
OnFreund marked this conversation as resolved.
Show resolved Hide resolved
hass.data.setdefault(DOMAIN, {})[entry.entry_id] = risco

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:
risco = hass.data[DOMAIN].pop(entry.entry_id)
await risco.close()
OnFreund marked this conversation as resolved.
Show resolved Hide resolved

return unload_ok
133 changes: 133 additions & 0 deletions homeassistant/components/risco/alarm_control_panel.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
"""Support for Risco alarms."""
import logging

import homeassistant.components.alarm_control_panel as alarm
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 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."""
risco = hass.data[DOMAIN][config_entry.entry_id]
alarm = await risco.get_state()
entities = [RiscoAlarm(hass, risco, partition) for partition in alarm.partitions]

async_add_entities(entities, False)


class RiscoAlarm(alarm.AlarmControlPanelEntity):
"""Representation of a Risco partition."""

def __init__(self, hass, risco, partition):
"""Init the partition."""
self._hass = hass
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Remove this.

self._risco = risco
self._state = partition
self._partition_id = partition.id

@property
def device_info(self):
"""Return device info for this device."""
return {
"identifiers": {(DOMAIN, self.unique_id)},
"name": self.unique_id,
"manufacturer": "Risco",
}

@property
def unique_id(self):
"""Return a unique id for that partition."""
return f"{self._risco.site_id}_{self._partition_id}"

@property
def state(self):
"""Return the state of the device."""
if self._state.triggered:
return STATE_ALARM_TRIGGERED
elif self._state.arming:
return STATE_ALARM_ARMING
elif self._state.armed:
return STATE_ALARM_ARMED_AWAY
elif self._state.partially_armed:
return STATE_ALARM_ARMED_HOME
elif self._state.disarmed:
return STATE_ALARM_DISARMED

return STATE_UNKNOWN
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Use None to represent unknown state.


@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."""
alarm = await self._risco.disarm(self._partition_id)
self._state = alarm.partitions[self._partition_id]
self.async_write_ha_state()

async def async_alarm_arm_home(self, code=None):
"""Send arm home command."""
alarm = await self._risco.partial_arm(self._partition_id)
self._state = alarm.partitions[self._partition_id]
self.async_write_ha_state()

async def async_alarm_arm_night(self, code=None):
"""Send arm night command."""
alarm = await self._risco.partial_arm(self._partition_id)
self._state = alarm.partitions[self._partition_id]
self.async_write_ha_state()

async def async_alarm_arm_custom_bypass(self, code=None):
"""Send arm custom bypass command."""
alarm = await self._risco.partial_arm(self._partition_id)
self._state = alarm.partitions[self._partition_id]
self.async_write_ha_state()

async def async_alarm_arm_away(self, code=None):
"""Send arm away command."""
alarm = await self._risco.arm(self._partition_id)
self._state = alarm.partitions[self._partition_id]
self.async_write_ha_state()

async def async_update(self):
"""Retrieve latest state."""
alarm = await self._risco.get_state()
bdraco marked this conversation as resolved.
Show resolved Hide resolved
self._state = alarm.partitions[self._partition_id]
71 changes: 71 additions & 0 deletions homeassistant/components/risco/config_flow.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,71 @@
"""Config flow for Risco integration."""
import logging

from pyrisco import CannotConnectError, RiscoAPI, UnauthorizedError
import voluptuous as vol

from homeassistant import config_entries, core, exceptions
from homeassistant.const import CONF_PASSWORD, CONF_PIN, CONF_USERNAME

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()
return {"title": risco.site_id}
except UnauthorizedError as e:
_LOGGER.error("Unauthorized connection to Risco Cloud")
raise InvalidAuth from e
except CannotConnectError as e:
_LOGGER.error("Error connecting to Risco Cloud")
raise CannotConnect from e
finally:
await risco.close()


class ConfigFlow(config_entries.ConfigFlow, domain=DOMAIN):
"""Handle a config flow for Risco."""

VERSION = 1
# TODO pick one of the available connection classes in homeassistant/config_entries.py
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)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Please move the entry creation to an else: block.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should use the username as unique_id and guard from creating more than one entry with the same username.

except CannotConnect:
errors["base"] = "cannot_connect"
except InvalidAuth:
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
)


class CannotConnect(exceptions.HomeAssistantError):
"""Error to indicate we cannot connect."""


class InvalidAuth(exceptions.HomeAssistantError):
"""Error to indicate there is invalid auth."""
OnFreund marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 3 additions & 0 deletions homeassistant/components/risco/const.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Constants for the Risco integration."""

DOMAIN = "risco"
16 changes: 16 additions & 0 deletions homeassistant/components/risco/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
{
"domain": "risco",
"name": "Risco",
"config_flow": true,
"documentation": "https://www.home-assistant.io/integrations/risco",
"requirements": [
"pyrisco==0.1.0"
],
"ssdp": [],
"zeroconf": [],
"homekit": {},
"dependencies": [],
OnFreund marked this conversation as resolved.
Show resolved Hide resolved
"codeowners": [
"@OnFreund"
]
}
22 changes: 22 additions & 0 deletions homeassistant/components/risco/strings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
{
"title": "Risco",
OnFreund marked this conversation as resolved.
Show resolved Hide resolved
"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%]"
}
}
}
1 change: 1 addition & 0 deletions homeassistant/generated/config_flows.py
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,7 @@
"rachio",
"rainmachine",
"ring",
"risco",
"roku",
"roomba",
"roon",
Expand Down
3 changes: 3 additions & 0 deletions requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1594,6 +1594,9 @@ pyrecswitch==1.0.2
# homeassistant.components.repetier
pyrepetier==3.0.5

# homeassistant.components.risco
pyrisco==0.1.0

# homeassistant.components.sabnzbd
pysabnzbd==1.1.0

Expand Down
3 changes: 3 additions & 0 deletions requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -750,6 +750,9 @@ pyps4-2ndscreen==1.1.1
# homeassistant.components.qwikswitch
pyqwikswitch==0.93

# homeassistant.components.risco
pyrisco==0.1.0

# homeassistant.components.acer_projector
# homeassistant.components.zha
pyserial==3.4
Expand Down
1 change: 1 addition & 0 deletions tests/components/risco/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
"""Tests for the Risco integration."""
Loading