diff --git a/homeassistant/components/deconz/.translations/en.json b/homeassistant/components/deconz/.translations/en.json index f55f64ca430..0c60953db56 100644 --- a/homeassistant/components/deconz/.translations/en.json +++ b/homeassistant/components/deconz/.translations/en.json @@ -12,7 +12,7 @@ "init": { "data": { "host": "Host", - "port": "Port (default value: '80')" + "port": "Port" }, "title": "Define deCONZ gateway" }, diff --git a/homeassistant/components/deconz/.translations/sv.json b/homeassistant/components/deconz/.translations/sv.json index 88cf8742acd..3ab3dae6dcd 100644 --- a/homeassistant/components/deconz/.translations/sv.json +++ b/homeassistant/components/deconz/.translations/sv.json @@ -12,7 +12,7 @@ "init": { "data": { "host": "V\u00e4rd", - "port": "Port (standardv\u00e4rde: '80')" + "port": "Port" }, "title": "Definiera deCONZ-gatewaye" }, diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index c314a1191db..4d3e2cbc6a9 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -11,11 +11,10 @@ from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) from homeassistant.helpers import config_validation as cv from homeassistant.helpers.device_registry import CONNECTION_NETWORK_MAC -from homeassistant.util.json import load_json # Loading the config flow file will register the flow from .config_flow import configured_hosts -from .const import CONFIG_FILE, DOMAIN, _LOGGER +from .const import DEFAULT_PORT, DOMAIN, _LOGGER from .gateway import DeconzGateway REQUIREMENTS = ['pydeconz==47'] @@ -27,7 +26,7 @@ CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ vol.Optional(CONF_API_KEY): cv.string, vol.Optional(CONF_HOST): cv.string, - vol.Optional(CONF_PORT, default=80): cv.port, + vol.Optional(CONF_PORT, default=DEFAULT_PORT): cv.port, }) }, extra=vol.ALLOW_EXTRA) @@ -53,11 +52,7 @@ async def async_setup(hass, config): """ if DOMAIN in config: deconz_config = None - config_file = await hass.async_add_job( - load_json, hass.config.path(CONFIG_FILE)) - if config_file: - deconz_config = config_file - elif CONF_HOST in config[DOMAIN]: + if CONF_HOST in config[DOMAIN]: deconz_config = config[DOMAIN] if deconz_config and not configured_hosts(hass): hass.async_add_job(hass.config_entries.flow.async_init( diff --git a/homeassistant/components/deconz/config_flow.py b/homeassistant/components/deconz/config_flow.py index 293b6c1b540..f7bc71a2398 100644 --- a/homeassistant/components/deconz/config_flow.py +++ b/homeassistant/components/deconz/config_flow.py @@ -6,11 +6,9 @@ from homeassistant import config_entries from homeassistant.core import callback from homeassistant.const import CONF_API_KEY, CONF_HOST, CONF_PORT from homeassistant.helpers import aiohttp_client -from homeassistant.util.json import load_json from .const import ( - CONF_ALLOW_DECONZ_GROUPS, CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DOMAIN) - + CONF_ALLOW_DECONZ_GROUPS, CONF_ALLOW_CLIP_SENSOR, DEFAULT_PORT, DOMAIN) CONF_BRIDGEID = 'bridgeid' @@ -35,6 +33,10 @@ class DeconzFlowHandler(config_entries.ConfigFlow): 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. @@ -51,6 +53,8 @@ class DeconzFlowHandler(config_entries.ConfigFlow): 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) @@ -58,19 +62,24 @@ class DeconzFlowHandler(config_entries.ConfigFlow): 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='user', + step_id='init', data_schema=vol.Schema({ vol.Required(CONF_HOST): vol.In(hosts) }) ) - return self.async_abort( - reason='no_bridges' + return self.async_show_form( + step_id='user', + data_schema=vol.Schema({ + vol.Required(CONF_HOST): str, + vol.Required(CONF_PORT, default=DEFAULT_PORT): int, + }), ) async def async_step_link(self, user_input=None): @@ -135,13 +144,6 @@ class DeconzFlowHandler(config_entries.ConfigFlow): deconz_config[CONF_PORT] = discovery_info.get(CONF_PORT) deconz_config[CONF_BRIDGEID] = discovery_info.get('serial') - config_file = await self.hass.async_add_job( - load_json, self.hass.config.path(CONFIG_FILE)) - if config_file and \ - config_file[CONF_HOST] == deconz_config[CONF_HOST] and \ - CONF_API_KEY in config_file: - deconz_config[CONF_API_KEY] = config_file[CONF_API_KEY] - return await self.async_step_import(deconz_config) async def async_step_import(self, import_config): diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index d856d8c1465..b08f3d71824 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -4,8 +4,8 @@ import logging _LOGGER = logging.getLogger('homeassistant.components.deconz') DOMAIN = 'deconz' -CONFIG_FILE = 'deconz.conf' -DECONZ_DOMAIN = 'deconz' + +DEFAULT_PORT = 80 CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor' CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups' diff --git a/homeassistant/components/deconz/strings.json b/homeassistant/components/deconz/strings.json index 09549a300a0..9ab7b56c0ca 100644 --- a/homeassistant/components/deconz/strings.json +++ b/homeassistant/components/deconz/strings.json @@ -6,7 +6,7 @@ "title": "Define deCONZ gateway", "data": { "host": "Host", - "port": "Port (default value: '80')" + "port": "Port" } }, "link": { diff --git a/tests/components/deconz/test_config_flow.py b/tests/components/deconz/test_config_flow.py index 20b7a88bc05..9e1d6a2fca1 100644 --- a/tests/components/deconz/test_config_flow.py +++ b/tests/components/deconz/test_config_flow.py @@ -1,5 +1,4 @@ """Tests for deCONZ config flow.""" -from unittest.mock import patch import pytest import voluptuous as vol @@ -45,7 +44,7 @@ async def test_flow_already_registered_bridge(hass): flow = config_flow.DeconzFlowHandler() flow.hass = hass - result = await flow.async_step_user() + result = await flow.async_step_init() assert result['type'] == 'abort' @@ -55,8 +54,9 @@ async def test_flow_no_discovered_bridges(hass, aioclient_mock): flow = config_flow.DeconzFlowHandler() flow.hass = hass - result = await flow.async_step_user() - assert result['type'] == 'abort' + result = await flow.async_step_init() + assert result['type'] == 'form' + assert result['step_id'] == 'user' async def test_flow_one_bridge_discovered(hass, aioclient_mock): @@ -67,7 +67,7 @@ async def test_flow_one_bridge_discovered(hass, aioclient_mock): flow = config_flow.DeconzFlowHandler() flow.hass = hass - result = await flow.async_step_user() + result = await flow.async_step_init() assert result['type'] == 'form' assert result['step_id'] == 'link' @@ -81,9 +81,9 @@ async def test_flow_two_bridges_discovered(hass, aioclient_mock): flow = config_flow.DeconzFlowHandler() flow.hass = hass - result = await flow.async_step_user() + result = await flow.async_step_init() assert result['type'] == 'form' - assert result['step_id'] == 'user' + assert result['step_id'] == 'init' with pytest.raises(vol.Invalid): assert result['data_schema']({'host': '0.0.0.0'}) @@ -101,12 +101,26 @@ async def test_flow_two_bridges_selection(hass, aioclient_mock): {'bridgeid': 'id2', 'host': '5.6.7.8', 'port': 80} ] - result = await flow.async_step_user(user_input={'host': '1.2.3.4'}) + result = await flow.async_step_init(user_input={'host': '1.2.3.4'}) assert result['type'] == 'form' assert result['step_id'] == 'link' assert flow.deconz_config['host'] == '1.2.3.4' +async def test_flow_manual_configuration(hass, aioclient_mock): + """Test config flow with manual input.""" + aioclient_mock.get(pydeconz.utils.URL_DISCOVER, json=[]) + flow = config_flow.DeconzFlowHandler() + flow.hass = hass + + user_input = {'host': '1.2.3.4', 'port': 80} + + result = await flow.async_step_init(user_input) + assert result['type'] == 'form' + assert result['step_id'] == 'link' + assert flow.deconz_config == user_input + + async def test_link_no_api_key(hass, aioclient_mock): """Test config flow should abort if no API key was possible to retrieve.""" aioclient_mock.post('http://1.2.3.4:80/api', json=[]) @@ -138,57 +152,14 @@ async def test_link_already_registered_bridge(hass): async def test_bridge_discovery(hass): - """Test a bridge being discovered with no additional config file.""" + """Test a bridge being discovered.""" flow = config_flow.DeconzFlowHandler() flow.hass = hass - with patch.object(config_flow, 'load_json', return_value={}): - result = await flow.async_step_discovery({ - 'host': '1.2.3.4', - 'port': 80, - 'serial': 'id' - }) - - assert result['type'] == 'form' - assert result['step_id'] == 'link' - - -async def test_bridge_discovery_config_file(hass): - """Test a bridge being discovered with a corresponding config file.""" - flow = config_flow.DeconzFlowHandler() - flow.hass = hass - with patch.object(config_flow, 'load_json', - return_value={'host': '1.2.3.4', - 'port': 8080, - 'api_key': '1234567890ABCDEF'}): - result = await flow.async_step_discovery({ - 'host': '1.2.3.4', - 'port': 80, - 'serial': 'id' - }) - - assert result['type'] == 'create_entry' - assert result['title'] == 'deCONZ-id' - assert result['data'] == { - 'bridgeid': 'id', + result = await flow.async_step_discovery({ 'host': '1.2.3.4', 'port': 80, - 'api_key': '1234567890ABCDEF', - 'allow_clip_sensor': True, - 'allow_deconz_groups': True - } - - -async def test_bridge_discovery_other_config_file(hass): - """Test a bridge being discovered with another bridges config file.""" - flow = config_flow.DeconzFlowHandler() - flow.hass = hass - with patch.object(config_flow, 'load_json', - return_value={'host': '5.6.7.8', 'api_key': '5678'}): - result = await flow.async_step_discovery({ - 'host': '1.2.3.4', - 'port': 80, - 'serial': 'id' - }) + 'serial': 'id' + }) assert result['type'] == 'form' assert result['step_id'] == 'link' diff --git a/tests/components/deconz/test_gateway.py b/tests/components/deconz/test_gateway.py new file mode 100644 index 00000000000..3411f96b981 --- /dev/null +++ b/tests/components/deconz/test_gateway.py @@ -0,0 +1,222 @@ +"""Test deCONZ gateway.""" +from unittest.mock import Mock, patch + +from homeassistant.components.deconz import gateway + +from tests.common import mock_coro + +ENTRY_CONFIG = { + "host": "1.2.3.4", + "port": 80, + "api_key": "1234567890ABCDEF", + "bridgeid": "0123456789ABCDEF", + "allow_clip_sensor": True, + "allow_deconz_groups": True, +} + + +async def test_gateway_setup(): + """Successful setup.""" + hass = Mock() + entry = Mock() + entry.data = ENTRY_CONFIG + api = Mock() + api.async_add_remote.return_value = Mock() + api.sensors = {} + + deconz_gateway = gateway.DeconzGateway(hass, entry) + + with patch.object(gateway, 'get_gateway', return_value=mock_coro(api)), \ + patch.object( + gateway, 'async_dispatcher_connect', return_value=Mock()): + assert await deconz_gateway.async_setup() is True + + assert deconz_gateway.api is api + assert len(hass.config_entries.async_forward_entry_setup.mock_calls) == 6 + assert hass.config_entries.async_forward_entry_setup.mock_calls[0][1] == \ + (entry, 'binary_sensor') + assert hass.config_entries.async_forward_entry_setup.mock_calls[1][1] == \ + (entry, 'cover') + assert hass.config_entries.async_forward_entry_setup.mock_calls[2][1] == \ + (entry, 'light') + assert hass.config_entries.async_forward_entry_setup.mock_calls[3][1] == \ + (entry, 'scene') + assert hass.config_entries.async_forward_entry_setup.mock_calls[4][1] == \ + (entry, 'sensor') + assert hass.config_entries.async_forward_entry_setup.mock_calls[5][1] == \ + (entry, 'switch') + assert len(api.start.mock_calls) == 1 + + +async def test_gateway_retry(): + """Retry setup.""" + hass = Mock() + entry = Mock() + entry.data = ENTRY_CONFIG + + deconz_gateway = gateway.DeconzGateway(hass, entry) + + with patch.object(gateway, 'get_gateway', return_value=mock_coro(False)): + assert await deconz_gateway.async_setup() is False + + +async def test_connection_status(hass): + """Make sure that connection status triggers a dispatcher send.""" + entry = Mock() + entry.data = ENTRY_CONFIG + + deconz_gateway = gateway.DeconzGateway(hass, entry) + with patch.object(gateway, 'async_dispatcher_send') as mock_dispatch_send: + deconz_gateway.async_connection_status_callback(True) + + await hass.async_block_till_done() + assert len(mock_dispatch_send.mock_calls) == 1 + assert len(mock_dispatch_send.mock_calls[0]) == 3 + + +async def test_add_device(hass): + """Successful retry setup.""" + entry = Mock() + entry.data = ENTRY_CONFIG + + deconz_gateway = gateway.DeconzGateway(hass, entry) + with patch.object(gateway, 'async_dispatcher_send') as mock_dispatch_send: + deconz_gateway.async_add_device_callback('sensor', Mock()) + + await hass.async_block_till_done() + assert len(mock_dispatch_send.mock_calls) == 1 + assert len(mock_dispatch_send.mock_calls[0]) == 3 + + +async def test_add_remote(): + """Successful add remote.""" + hass = Mock() + entry = Mock() + entry.data = ENTRY_CONFIG + + remote = Mock() + remote.name = 'name' + remote.type = 'ZHASwitch' + remote.register_async_callback = Mock() + + deconz_gateway = gateway.DeconzGateway(hass, entry) + deconz_gateway.async_add_remote([remote]) + + assert len(deconz_gateway.events) == 1 + + +async def test_shutdown(): + """Successful shutdown.""" + hass = Mock() + entry = Mock() + entry.data = ENTRY_CONFIG + + deconz_gateway = gateway.DeconzGateway(hass, entry) + deconz_gateway.api = Mock() + deconz_gateway.shutdown(None) + + assert len(deconz_gateway.api.close.mock_calls) == 1 + + +async def test_reset_cancel_retry(): + """Verify async reset can handle a scheduled retry.""" + hass = Mock() + entry = Mock() + entry.data = ENTRY_CONFIG + + deconz_gateway = gateway.DeconzGateway(hass, entry) + + with patch.object(gateway, 'get_gateway', return_value=mock_coro(False)): + assert await deconz_gateway.async_setup() is False + + assert deconz_gateway._cancel_retry_setup is not None + + assert await deconz_gateway.async_reset() is True + + +async def test_reset_after_successful_setup(): + """Verify that reset works on a setup component.""" + hass = Mock() + entry = Mock() + entry.data = ENTRY_CONFIG + api = Mock() + api.async_add_remote.return_value = Mock() + api.sensors = {} + + deconz_gateway = gateway.DeconzGateway(hass, entry) + + with patch.object(gateway, 'get_gateway', return_value=mock_coro(api)), \ + patch.object( + gateway, 'async_dispatcher_connect', return_value=Mock()): + assert await deconz_gateway.async_setup() is True + + listener = Mock() + deconz_gateway.listeners = [listener] + event = Mock() + event.async_will_remove_from_hass = Mock() + deconz_gateway.events = [event] + deconz_gateway.deconz_ids = {'key': 'value'} + + hass.config_entries.async_forward_entry_unload.return_value = \ + mock_coro(True) + assert await deconz_gateway.async_reset() is True + + assert len(hass.config_entries.async_forward_entry_unload.mock_calls) == 6 + + assert len(listener.mock_calls) == 1 + assert len(deconz_gateway.listeners) == 0 + + assert len(event.async_will_remove_from_hass.mock_calls) == 1 + assert len(deconz_gateway.events) == 0 + + assert len(deconz_gateway.deconz_ids) == 0 + + +async def test_get_gateway(hass): + """Successful call.""" + with patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(True)): + assert await gateway.get_gateway(hass, ENTRY_CONFIG, Mock(), Mock()) + + +async def test_get_gateway_fails(hass): + """Failed call.""" + with patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(False)): + assert await gateway.get_gateway( + hass, ENTRY_CONFIG, Mock(), Mock()) is False + + +async def test_create_event(): + """Successfully created a deCONZ event.""" + hass = Mock() + remote = Mock() + remote.name = 'Name' + + event = gateway.DeconzEvent(hass, remote) + + assert event._id == 'name' + + +async def test_update_event(): + """Successfully update a deCONZ event.""" + hass = Mock() + remote = Mock() + remote.name = 'Name' + + event = gateway.DeconzEvent(hass, remote) + event.async_update_callback({'state': True}) + + assert len(hass.bus.async_fire.mock_calls) == 1 + + +async def test_remove_event(): + """Successfully update a deCONZ event.""" + hass = Mock() + remote = Mock() + remote.name = 'Name' + + event = gateway.DeconzEvent(hass, remote) + event.async_will_remove_from_hass() + + assert event._device is None diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index 3453dd86c12..b83756f6ebb 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -21,8 +21,7 @@ CONFIG = { async def test_config_with_host_passed_to_config_entry(hass): """Test that configured options for a host are loaded via config entry.""" with patch.object(hass, 'config_entries') as mock_config_entries, \ - patch.object(deconz, 'configured_hosts', return_value=[]), \ - patch.object(deconz, 'load_json', return_value={}): + patch.object(deconz, 'configured_hosts', return_value=[]): assert await async_setup_component(hass, deconz.DOMAIN, { deconz.DOMAIN: { deconz.CONF_HOST: '1.2.3.4', @@ -33,24 +32,10 @@ async def test_config_with_host_passed_to_config_entry(hass): assert len(mock_config_entries.flow.mock_calls) == 2 -async def test_config_file_passed_to_config_entry(hass): - """Test that configuration file for a host are loaded via config entry.""" - with patch.object(hass, 'config_entries') as mock_config_entries, \ - patch.object(deconz, 'configured_hosts', return_value=[]), \ - patch.object(deconz, 'load_json', - return_value={'host': '1.2.3.4'}): - assert await async_setup_component(hass, deconz.DOMAIN, { - deconz.DOMAIN: {} - }) is True - # Import flow started - assert len(mock_config_entries.flow.mock_calls) == 2 - - async def test_config_without_host_not_passed_to_config_entry(hass): """Test that a configuration without a host does not initiate an import.""" with patch.object(hass, 'config_entries') as mock_config_entries, \ - patch.object(deconz, 'configured_hosts', return_value=[]), \ - patch.object(deconz, 'load_json', return_value={}): + patch.object(deconz, 'configured_hosts', return_value=[]): assert await async_setup_component(hass, deconz.DOMAIN, { deconz.DOMAIN: {} }) is True @@ -62,8 +47,7 @@ async def test_config_already_registered_not_passed_to_config_entry(hass): """Test that an already registered host does not initiate an import.""" with patch.object(hass, 'config_entries') as mock_config_entries, \ patch.object(deconz, 'configured_hosts', - return_value=['1.2.3.4']), \ - patch.object(deconz, 'load_json', return_value={}): + return_value=['1.2.3.4']): assert await async_setup_component(hass, deconz.DOMAIN, { deconz.DOMAIN: { deconz.CONF_HOST: '1.2.3.4',