Skip to content

Commit

Permalink
Support deCONZ library with exception handling (#21952)
Browse files Browse the repository at this point in the history
  • Loading branch information
Kane610 authored Mar 24, 2019
1 parent 89f8203 commit 8d1cf55
Show file tree
Hide file tree
Showing 10 changed files with 190 additions and 65 deletions.
5 changes: 2 additions & 3 deletions homeassistant/components/deconz/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
from .const import DEFAULT_PORT, DOMAIN, _LOGGER
from .gateway import DeconzGateway

REQUIREMENTS = ['pydeconz==52']
REQUIREMENTS = ['pydeconz==53']

CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
Expand Down Expand Up @@ -124,8 +124,7 @@ async def async_refresh_devices(call):
scenes = set(gateway.api.scenes.keys())
sensors = set(gateway.api.sensors.keys())

if not await gateway.api.async_load_parameters():
return
await gateway.api.async_load_parameters()

gateway.async_add_device_callback(
'group', [group
Expand Down
45 changes: 34 additions & 11 deletions homeassistant/components/deconz/config_flow.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
"""Config flow to configure deCONZ component."""
import asyncio
import async_timeout
import voluptuous as vol

from homeassistant import config_entries
Expand Down Expand Up @@ -32,15 +34,12 @@ def __init__(self):
self.deconz_config = {}

async def async_step_user(self, user_input=None):
"""Handle a flow initialized by the user."""
return await self.async_step_init(user_input)

async def async_step_init(self, user_input=None):
"""Handle a deCONZ config flow start.
Only allows one instance to be set up.
If only one bridge is found go to link step.
If more than one bridge is found let user choose bridge to link.
If no bridge is found allow user to manually input configuration.
"""
from pydeconz.utils import async_discovery

Expand All @@ -52,20 +51,29 @@ async def async_step_init(self, user_input=None):
if bridge[CONF_HOST] == user_input[CONF_HOST]:
self.deconz_config = bridge
return await self.async_step_link()

self.deconz_config = user_input
return await self.async_step_link()

session = aiohttp_client.async_get_clientsession(self.hass)
self.bridges = await async_discovery(session)

try:
with async_timeout.timeout(10):
self.bridges = await async_discovery(session)

except asyncio.TimeoutError:
self.bridges = []

if len(self.bridges) == 1:
self.deconz_config = self.bridges[0]
return await self.async_step_link()

if len(self.bridges) > 1:
hosts = []

for bridge in self.bridges:
hosts.append(bridge[CONF_HOST])

return self.async_show_form(
step_id='init',
data_schema=vol.Schema({
Expand All @@ -74,7 +82,7 @@ async def async_step_init(self, user_input=None):
)

return self.async_show_form(
step_id='user',
step_id='init',
data_schema=vol.Schema({
vol.Required(CONF_HOST): str,
vol.Required(CONF_PORT, default=DEFAULT_PORT): int,
Expand All @@ -83,18 +91,27 @@ async def async_step_init(self, user_input=None):

async def async_step_link(self, user_input=None):
"""Attempt to link with the deCONZ bridge."""
from pydeconz.errors import ResponseError, RequestError
from pydeconz.utils import async_get_api_key
errors = {}

if user_input is not None:
if configured_hosts(self.hass):
return self.async_abort(reason='one_instance_only')

session = aiohttp_client.async_get_clientsession(self.hass)
api_key = await async_get_api_key(session, **self.deconz_config)
if api_key:

try:
with async_timeout.timeout(10):
api_key = await async_get_api_key(
session, **self.deconz_config)

except (ResponseError, RequestError, asyncio.TimeoutError):
errors['base'] = 'no_key'

else:
self.deconz_config[CONF_API_KEY] = api_key
return await self.async_step_options()
errors['base'] = 'no_key'

return self.async_show_form(
step_id='link',
Expand All @@ -117,8 +134,14 @@ async def async_step_options(self, user_input=None):

if CONF_BRIDGEID not in self.deconz_config:
session = aiohttp_client.async_get_clientsession(self.hass)
self.deconz_config[CONF_BRIDGEID] = await async_get_bridgeid(
session, **self.deconz_config)
try:
with async_timeout.timeout(10):
self.deconz_config[CONF_BRIDGEID] = \
await async_get_bridgeid(
session, **self.deconz_config)

except asyncio.TimeoutError:
return self.async_abort(reason='no_bridges')

return self.async_create_entry(
title='deCONZ-' + self.deconz_config[CONF_BRIDGEID],
Expand Down
18 changes: 18 additions & 0 deletions homeassistant/components/deconz/errors.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
"""Errors for the deCONZ component."""
from homeassistant.exceptions import HomeAssistantError


class DeconzException(HomeAssistantError):
"""Base class for deCONZ exceptions."""


class AlreadyConfigured(DeconzException):
"""Gateway is already configured."""


class AuthenticationRequired(DeconzException):
"""Unknown error occurred."""


class CannotConnect(DeconzException):
"""Unable to connect to the gateway."""
42 changes: 30 additions & 12 deletions homeassistant/components/deconz/gateway.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
"""Representation of a deCONZ gateway."""
import asyncio
import async_timeout

from homeassistant.exceptions import ConfigEntryNotReady
from homeassistant.const import CONF_EVENT, CONF_ID
from homeassistant.const import CONF_EVENT, CONF_HOST, CONF_ID
from homeassistant.core import EventOrigin, callback
from homeassistant.helpers import aiohttp_client
from homeassistant.helpers.dispatcher import (
Expand All @@ -10,6 +13,7 @@
from .const import (
_LOGGER, DECONZ_REACHABLE, CONF_ALLOW_CLIP_SENSOR, NEW_DEVICE, NEW_SENSOR,
SUPPORTED_PLATFORMS)
from .errors import AuthenticationRequired, CannotConnect


class DeconzGateway:
Expand All @@ -26,18 +30,23 @@ def __init__(self, hass, config_entry):
self.events = []
self.listeners = []

async def async_setup(self, tries=0):
async def async_setup(self):
"""Set up a deCONZ gateway."""
hass = self.hass

self.api = await get_gateway(
hass, self.config_entry.data, self.async_add_device_callback,
self.async_connection_status_callback
)
try:
self.api = await get_gateway(
hass, self.config_entry.data, self.async_add_device_callback,
self.async_connection_status_callback
)

if not self.api:
except CannotConnect:
raise ConfigEntryNotReady

except Exception: # pylint: disable=broad-except
_LOGGER.error('Error connecting with deCONZ gateway.')
return False

for component in SUPPORTED_PLATFORMS:
hass.async_create_task(
hass.config_entries.async_forward_entry_setup(
Expand Down Expand Up @@ -113,17 +122,26 @@ async def async_reset(self):
async def get_gateway(hass, config, async_add_device_callback,
async_connection_status_callback):
"""Create a gateway object and verify configuration."""
from pydeconz import DeconzSession
from pydeconz import DeconzSession, errors

session = aiohttp_client.async_get_clientsession(hass)

deconz = DeconzSession(hass.loop, session, **config,
async_add_device=async_add_device_callback,
connection_status=async_connection_status_callback)
result = await deconz.async_load_parameters()

if result:
try:
with async_timeout.timeout(10):
await deconz.async_load_parameters()
return deconz
return result

except errors.Unauthorized:
_LOGGER.warning("Invalid key for deCONZ at %s.", config[CONF_HOST])
raise AuthenticationRequired

except (asyncio.TimeoutError, errors.RequestError):
_LOGGER.error(
"Error connecting to deCONZ gateway at %s", config[CONF_HOST])
raise CannotConnect


class DeconzEvent:
Expand Down
2 changes: 1 addition & 1 deletion requirements_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -1001,7 +1001,7 @@ pydaikin==1.1.0
pydanfossair==0.0.7

# homeassistant.components.deconz
pydeconz==52
pydeconz==53

# homeassistant.components.zwave
pydispatcher==2.0.5
Expand Down
2 changes: 1 addition & 1 deletion requirements_test_all.txt
Original file line number Diff line number Diff line change
Expand Up @@ -200,7 +200,7 @@ pyHS100==0.3.4
pyblackbird==0.5

# homeassistant.components.deconz
pydeconz==52
pydeconz==53

# homeassistant.components.zwave
pydispatcher==2.0.5
Expand Down
13 changes: 8 additions & 5 deletions tests/components/deconz/test_climate.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,11 +46,14 @@ async def setup_gateway(hass, data, allow_clip_sensor=True):
"""Load the deCONZ sensor platform."""
from pydeconz import DeconzSession

session = Mock(put=asynctest.CoroutineMock(
return_value=Mock(status=200,
json=asynctest.CoroutineMock(),
text=asynctest.CoroutineMock(),
)
response = Mock(
status=200, json=asynctest.CoroutineMock(),
text=asynctest.CoroutineMock())
response.content_type = 'application/json'

session = Mock(
put=asynctest.CoroutineMock(
return_value=response
)
)

Expand Down
Loading

0 comments on commit 8d1cf55

Please sign in to comment.