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

Support deCONZ library with exception handling #21952

Merged
merged 11 commits into from Mar 24, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
5 changes: 2 additions & 3 deletions homeassistant/components/deconz/__init__.py
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
@@ -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',
Kane610 marked this conversation as resolved.
Show resolved Hide resolved
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',
Kane610 marked this conversation as resolved.
Show resolved Hide resolved
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
@@ -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
@@ -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.')
Kane610 marked this conversation as resolved.
Show resolved Hide resolved
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])
Kane610 marked this conversation as resolved.
Show resolved Hide resolved
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
Expand Up @@ -1001,7 +1001,7 @@ pydaikin==0.9
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
Expand Up @@ -194,7 +194,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
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