diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json new file mode 100644 index 00000000000..69165dbbbaf --- /dev/null +++ b/homeassistant/components/deconz/.translations/en.json @@ -0,0 +1,25 @@ +{ + "config": { + "title": "deCONZ", + "step": { + "init": { + "title": "Define deCONZ gateway", + "data": { + "host": "Host", + "port": "Port (default value: '80')" + } + }, + "link": { + "title": "Link with deCONZ", + "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button" + } + }, + "error": { + "no_key": "Couldn't get an API key" + }, + "abort": { + "no_bridges": "No deCONZ bridges discovered", + "one_instance_only": "Component only supports one deCONZ instance" + } + } +} \ No newline at end of file diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 26d9fb401e4..85ba271ec3a 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -8,16 +8,17 @@ import logging import voluptuous as vol +from homeassistant import config_entries from homeassistant.components.discovery import SERVICE_DECONZ from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) from homeassistant.core import callback from homeassistant.helpers import config_validation as cv -from homeassistant.helpers import discovery +from homeassistant.helpers import discovery, aiohttp_client from homeassistant.helpers.aiohttp_client import async_get_clientsession from homeassistant.util.json import load_json, save_json -REQUIREMENTS = ['pydeconz==32'] +REQUIREMENTS = ['pydeconz==35'] _LOGGER = logging.getLogger(__name__) @@ -160,7 +161,8 @@ async def async_request_configuration(hass, config, deconz_config): async def async_configuration_callback(data): """Set up actions to do when our configuration callback is called.""" from pydeconz.utils import async_get_api_key - api_key = await async_get_api_key(hass.loop, **deconz_config) + websession = async_get_clientsession(hass) + api_key = await async_get_api_key(websession, **deconz_config) if api_key: deconz_config[CONF_API_KEY] = api_key result = await async_setup_deconz(hass, config, deconz_config) @@ -186,3 +188,85 @@ async def async_request_configuration(hass, config, deconz_config): entity_picture="/static/images/logo_deconz.jpeg", submit_caption="I have unlocked the gateway", ) + + +@config_entries.HANDLERS.register(DOMAIN) +class DeconzFlowHandler(config_entries.ConfigFlowHandler): + """Handle a deCONZ config flow.""" + + VERSION = 1 + + def __init__(self): + """Initialize the deCONZ flow.""" + self.bridges = [] + self.deconz_config = {} + + async def async_step_init(self, user_input=None): + """Handle a flow start.""" + from pydeconz.utils import async_discovery + + if DOMAIN in self.hass.data: + return self.async_abort( + reason='one_instance_only' + ) + + if user_input is not None: + for bridge in self.bridges: + if bridge[CONF_HOST] == user_input[CONF_HOST]: + self.deconz_config = bridge + return await self.async_step_link() + + session = aiohttp_client.async_get_clientsession(self.hass) + self.bridges = await async_discovery(session) + + if len(self.bridges) == 1: + self.deconz_config = self.bridges[0] + return await self.async_step_link() + elif 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({ + vol.Required(CONF_HOST): vol.In(hosts) + }) + ) + + return self.async_abort( + reason='no_bridges' + ) + + async def async_step_link(self, user_input=None): + """Attempt to link with the deCONZ bridge.""" + from pydeconz.utils import async_get_api_key + errors = {} + + if user_input is not None: + session = aiohttp_client.async_get_clientsession(self.hass) + api_key = await async_get_api_key(session, **self.deconz_config) + if api_key: + self.deconz_config[CONF_API_KEY] = api_key + return self.async_create_entry( + title='deCONZ', + data=self.deconz_config + ) + else: + errors['base'] = 'no_key' + + return self.async_show_form( + step_id='link', + errors=errors, + ) + + +async def async_setup_entry(hass, entry): + """Set up a bridge for a config entry.""" + if DOMAIN in hass.data: + _LOGGER.error( + "Config entry failed since one deCONZ instance already exists") + return False + result = await async_setup_deconz(hass, None, entry.data) + if result: + return True + return False diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json new file mode 100644 index 00000000000..69165dbbbaf --- /dev/null +++ b/homeassistant/components/deconz/strings.json @@ -0,0 +1,25 @@ +{ + "config": { + "title": "deCONZ", + "step": { + "init": { + "title": "Define deCONZ gateway", + "data": { + "host": "Host", + "port": "Port (default value: '80')" + } + }, + "link": { + "title": "Link with deCONZ", + "description": "Unlock your deCONZ gateway to register with Home Assistant.\n\n1. Go to deCONZ system settings\n2. Press \"Unlock Gateway\" button" + } + }, + "error": { + "no_key": "Couldn't get an API key" + }, + "abort": { + "no_bridges": "No deCONZ bridges discovered", + "one_instance_only": "Component only supports one deCONZ instance" + } + } +} \ No newline at end of file diff --git a/homeassistant/config_entries.py b/homeassistant/config_entries.py index b02026ac6dd..6b2000b2ea6 100644 --- a/homeassistant/config_entries.py +++ b/homeassistant/config_entries.py @@ -128,6 +128,7 @@ HANDLERS = Registry() FLOWS = [ 'config_entry_example', 'hue', + 'deconz', ] SOURCE_USER = 'user' diff --git a/requirements_all.txt b/requirements_all.txt index b21452dc385..676945eb42e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -714,7 +714,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==32 +pydeconz==35 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 31a7874409a..456bec7d6a8 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -129,6 +129,9 @@ pushbullet.py==0.11.0 # homeassistant.components.canary py-canary==0.4.1 +# homeassistant.components.deconz +pydeconz==35 + # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/script/gen_requirements_all.py b/script/gen_requirements_all.py index 1f5348136c6..fa39c307f18 100755 --- a/script/gen_requirements_all.py +++ b/script/gen_requirements_all.py @@ -67,6 +67,7 @@ TEST_REQUIREMENTS = ( 'prometheus_client', 'pushbullet.py', 'py-canary', + 'pydeconz', 'pydispatcher', 'PyJWT', 'pylitejet', diff --git a/tests/components/test_deconz.py b/tests/components/test_deconz.py new file mode 100644 index 00000000000..2c7c656d560 --- /dev/null +++ b/tests/components/test_deconz.py @@ -0,0 +1,97 @@ +"""Tests for deCONZ config flow.""" +import pytest + +import voluptuous as vol + +import homeassistant.components.deconz as deconz +import pydeconz + + +async def test_flow_works(hass, aioclient_mock): + """Test config flow.""" + aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[ + {'id': 'id', 'internalipaddress': '1.2.3.4', 'internalport': '80'} + ]) + aioclient_mock.post('http://1.2.3.4:80/api', json=[ + {"success": {"username": "1234567890ABCDEF"}} + ]) + + flow = deconz.DeconzFlowHandler() + flow.hass = hass + await flow.async_step_init() + result = await flow.async_step_link(user_input={}) + + assert result['type'] == 'create_entry' + assert result['title'] == 'deCONZ' + assert result['data'] == { + 'bridgeid': 'id', + 'host': '1.2.3.4', + 'port': '80', + 'api_key': '1234567890ABCDEF' + } + + +async def test_flow_already_registered_bridge(hass, aioclient_mock): + """Test config flow don't allow more than one bridge to be registered.""" + flow = deconz.DeconzFlowHandler() + flow.hass = hass + flow.hass.data[deconz.DOMAIN] = True + + result = await flow.async_step_init() + assert result['type'] == 'abort' + + +async def test_flow_no_discovered_bridges(hass, aioclient_mock): + """Test config flow discovers no bridges.""" + aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[]) + flow = deconz.DeconzFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'abort' + + +async def test_flow_one_bridge_discovered(hass, aioclient_mock): + """Test config flow discovers one bridge.""" + aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[ + {'id': 'id', 'internalipaddress': '1.2.3.4', 'internalport': '80'} + ]) + flow = deconz.DeconzFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'form' + assert result['step_id'] == 'link' + + +async def test_flow_two_bridges_discovered(hass, aioclient_mock): + """Test config flow discovers two bridges.""" + aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[ + {'id': 'id1', 'internalipaddress': '1.2.3.4', 'internalport': '80'}, + {'id': 'id2', 'internalipaddress': '5.6.7.8', 'internalport': '80'} + ]) + flow = deconz.DeconzFlowHandler() + flow.hass = hass + + result = await flow.async_step_init() + assert result['type'] == 'form' + assert result['step_id'] == 'init' + + with pytest.raises(vol.Invalid): + assert result['data_schema']({'host': '0.0.0.0'}) + + result['data_schema']({'host': '1.2.3.4'}) + result['data_schema']({'host': '5.6.7.8'}) + + +async def test_flow_no_api_key(hass, aioclient_mock): + """Test config flow discovers no bridges.""" + aioclient_mock.post('http://1.2.3.4:80/api', json=[]) + flow = deconz.DeconzFlowHandler() + flow.hass = hass + flow.deconz_config = {'host': '1.2.3.4', 'port': 80} + + result = await flow.async_step_link(user_input={}) + assert result['type'] == 'form' + assert result['step_id'] == 'link' + assert result['errors'] == {'base': 'no_key'}