From 8a10fcd9852411cb3e209965e0286c540498f7c4 Mon Sep 17 00:00:00 2001 From: Kane610 Date: Mon, 23 Apr 2018 18:00:16 +0200 Subject: [PATCH] deCONZ use forward entry setup (#13990) * Use forward entry setup with light platform * Move sensor to forward entry setup * Use forward entry setup with binary sensors * Use forward entry setup with scene platform * Remove import of unused functionality * Move deconz setup in to setup entry Create initial negative tests for setup entry * Fix hound comment * Improved tests * Add test for scene platform * Add test for binary sensor platform * Add test for light platform * Add test for light platform * Add test for sensor platform * Fix hound comment * More asserts on sensor types --- .../components/binary_sensor/__init__.py | 7 +- .../components/binary_sensor/deconz.py | 8 +- homeassistant/components/deconz/__init__.py | 31 +++---- homeassistant/components/light/deconz.py | 8 +- homeassistant/components/scene/__init__.py | 7 +- homeassistant/components/scene/deconz.py | 8 +- homeassistant/components/sensor/__init__.py | 7 +- homeassistant/components/sensor/deconz.py | 8 +- tests/components/binary_sensor/test_deconz.py | 55 +++++++++++++ tests/components/deconz/test_init.py | 42 +++++++++- tests/components/light/test_deconz.py | 74 +++++++++++++++++ tests/components/scene/test_deconz.py | 57 +++++++++++++ tests/components/sensor/test_deconz.py | 82 +++++++++++++++++++ 13 files changed, 358 insertions(+), 36 deletions(-) create mode 100644 tests/components/binary_sensor/test_deconz.py create mode 100644 tests/components/light/test_deconz.py create mode 100644 tests/components/scene/test_deconz.py create mode 100644 tests/components/sensor/test_deconz.py diff --git a/homeassistant/components/binary_sensor/__init__.py b/homeassistant/components/binary_sensor/__init__.py index ad475be76ca..ee2a0ce712d 100644 --- a/homeassistant/components/binary_sensor/__init__.py +++ b/homeassistant/components/binary_sensor/__init__.py @@ -50,13 +50,18 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) async def async_setup(hass, config): """Track states and offer events for binary sensors.""" - component = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent( logging.getLogger(__name__), DOMAIN, hass, SCAN_INTERVAL) await component.async_setup(config) return True +async def async_setup_entry(hass, entry): + """Setup a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + # pylint: disable=no-self-use class BinarySensorDevice(Entity): """Represent a binary sensor.""" diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index ef3ec506e3a..a9a3e28f4be 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -15,10 +15,12 @@ DEPENDENCIES = ['deconz'] async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the deCONZ binary sensor.""" - if discovery_info is None: - return + """Old way of setting up deCONZ binary sensors.""" + pass + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the deCONZ binary sensor.""" from pydeconz.sensor import DECONZ_BINARY_SENSOR sensors = hass.data[DATA_DECONZ].sensors entities = [] diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 064725eda95..d68edac9e59 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -9,8 +9,7 @@ import voluptuous as vol from homeassistant.const import ( CONF_API_KEY, CONF_HOST, CONF_PORT, EVENT_HOMEASSISTANT_STOP) from homeassistant.core import callback -from homeassistant.helpers import ( - aiohttp_client, discovery, config_validation as cv) +from homeassistant.helpers import aiohttp_client, config_validation as cv from homeassistant.util.json import load_json # Loading the config flow file will register the flow @@ -58,28 +57,20 @@ async def async_setup(hass, config): return True -async def async_setup_entry(hass, entry): - """Set up a deCONZ 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 - - -async def async_setup_deconz(hass, config, deconz_config): - """Set up a deCONZ session. +async def async_setup_entry(hass, config_entry): + """Set up a deCONZ bridge for a config entry. Load config, group, light and sensor data for server information. Start websocket for push notification of state changes from deCONZ. """ - _LOGGER.debug("deCONZ config %s", deconz_config) from pydeconz import DeconzSession + if DOMAIN in hass.data: + _LOGGER.error( + "Config entry failed since one deCONZ instance already exists") + return False + session = aiohttp_client.async_get_clientsession(hass) - deconz = DeconzSession(hass.loop, session, **deconz_config) + deconz = DeconzSession(hass.loop, session, **config_entry.data) result = await deconz.async_load_parameters() if result is False: _LOGGER.error("Failed to communicate with deCONZ") @@ -89,8 +80,8 @@ async def async_setup_deconz(hass, config, deconz_config): hass.data[DATA_DECONZ_ID] = {} for component in ['binary_sensor', 'light', 'scene', 'sensor']: - hass.async_add_job(discovery.async_load_platform( - hass, component, DOMAIN, {}, config)) + hass.async_add_job(hass.config_entries.async_forward_entry_setup( + config_entry, component)) deconz.start() async def async_configure(call): diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 020f43d9935..36ad572a263 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -19,10 +19,12 @@ DEPENDENCIES = ['deconz'] async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the deCONZ light.""" - if discovery_info is None: - return + """Old way of setting up deCONZ lights.""" + pass + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the deCONZ lights from a config entry.""" lights = hass.data[DATA_DECONZ].lights groups = hass.data[DATA_DECONZ].groups entities = [] diff --git a/homeassistant/components/scene/__init__.py b/homeassistant/components/scene/__init__.py index 8f0b9d5c7ab..a3e3a5b38a7 100644 --- a/homeassistant/components/scene/__init__.py +++ b/homeassistant/components/scene/__init__.py @@ -71,7 +71,7 @@ def activate(hass, entity_id=None): async def async_setup(hass, config): """Set up the scenes.""" logger = logging.getLogger(__name__) - component = EntityComponent(logger, DOMAIN, hass) + component = hass.data[DOMAIN] = EntityComponent(logger, DOMAIN, hass) await component.async_setup(config) @@ -90,6 +90,11 @@ async def async_setup(hass, config): return True +async def async_setup_entry(hass, entry): + """Setup a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) + + class Scene(Entity): """A scene is a group of entities and the states we want them to be.""" diff --git a/homeassistant/components/scene/deconz.py b/homeassistant/components/scene/deconz.py index dffc7720776..3eb73736717 100644 --- a/homeassistant/components/scene/deconz.py +++ b/homeassistant/components/scene/deconz.py @@ -13,10 +13,12 @@ DEPENDENCIES = ['deconz'] async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up scenes for deCONZ component.""" - if discovery_info is None: - return + """Old way of setting up deCONZ scenes.""" + pass + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up scenes for deCONZ component.""" scenes = hass.data[DATA_DECONZ].scenes entities = [] diff --git a/homeassistant/components/sensor/__init__.py b/homeassistant/components/sensor/__init__.py index 2bc35a034f4..2887d32b987 100644 --- a/homeassistant/components/sensor/__init__.py +++ b/homeassistant/components/sensor/__init__.py @@ -31,8 +31,13 @@ DEVICE_CLASSES_SCHEMA = vol.All(vol.Lower, vol.In(DEVICE_CLASSES)) async def async_setup(hass, config): """Track states and offer events for sensors.""" - component = EntityComponent( + component = hass.data[DOMAIN] = EntityComponent( _LOGGER, DOMAIN, hass, SCAN_INTERVAL) await component.async_setup(config) return True + + +async def async_setup_entry(hass, entry): + """Setup a config entry.""" + return await hass.data[DOMAIN].async_setup_entry(entry) diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index e569c5578ac..dc28a181aa0 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -22,10 +22,12 @@ ATTR_EVENT_ID = 'event_id' async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Set up the deCONZ sensors.""" - if discovery_info is None: - return + """Old way of setting up deCONZ sensors.""" + pass + +async def async_setup_entry(hass, config_entry, async_add_devices): + """Set up the deCONZ sensors.""" from pydeconz.sensor import DECONZ_SENSOR, SWITCH as DECONZ_REMOTE sensors = hass.data[DATA_DECONZ].sensors entities = [] diff --git a/tests/components/binary_sensor/test_deconz.py b/tests/components/binary_sensor/test_deconz.py new file mode 100644 index 00000000000..84ed059e97e --- /dev/null +++ b/tests/components/binary_sensor/test_deconz.py @@ -0,0 +1,55 @@ +"""deCONZ binary sensor platform tests.""" +from unittest.mock import Mock, patch + +from homeassistant import config_entries +from homeassistant.components import deconz + +from tests.common import mock_coro + + +SENSOR = { + "1": { + "id": "Sensor 1 id", + "name": "Sensor 1 name", + "type": "ZHAPresence", + "state": {"presence": False}, + "config": {} + } +} + + +async def setup_bridge(hass, data): + """Load the deCONZ binary sensor platform.""" + from pydeconz import DeconzSession + loop = Mock() + session = Mock() + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + bridge = DeconzSession(loop, session, **entry.data) + with patch('pydeconz.DeconzSession.async_get_state', + return_value=mock_coro(data)): + await bridge.async_load_parameters() + hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_ID] = {} + config_entry = config_entries.ConfigEntry( + 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + await hass.config_entries.async_forward_entry_setup( + config_entry, 'binary_sensor') + # To flush out the service call to update the group + await hass.async_block_till_done() + + +async def test_no_binary_sensors(hass): + """Test the update_lights function with some lights.""" + data = {} + await setup_bridge(hass, data) + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + assert len(hass.states.async_all()) == 0 + + +async def test_binary_sensors(hass): + """Test the update_lights function with some lights.""" + data = {"sensors": SENSOR} + await setup_bridge(hass, data) + assert "binary_sensor.sensor_1_name" in hass.data[deconz.DATA_DECONZ_ID] + assert len(hass.states.async_all()) == 1 diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index cbc8a373972..ce231e3d162 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -1,9 +1,11 @@ """Test deCONZ component setup process.""" -from unittest.mock import patch +from unittest.mock import Mock, patch from homeassistant.setup import async_setup_component from homeassistant.components import deconz +from tests.common import mock_coro + async def test_config_with_host_passed_to_config_entry(hass): """Test that configured options for a host are loaded via config entry.""" @@ -67,3 +69,41 @@ async def test_config_discovery(hass): assert await async_setup_component(hass, deconz.DOMAIN, {}) is True # No flow started assert len(mock_config_entries.flow.mock_calls) == 0 + + +async def test_setup_entry_already_registered_bridge(hass): + """Test setup entry doesn't allow more than one instance of deCONZ.""" + hass.data[deconz.DOMAIN] = True + assert await deconz.async_setup_entry(hass, {}) is False + + +async def test_setup_entry_no_available_bridge(hass): + """Test setup entry fails if deCONZ is not available.""" + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + with patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(False)): + assert await deconz.async_setup_entry(hass, entry) is False + + +async def test_setup_entry_successful(hass): + """Test setup entry is successful.""" + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + with patch.object(hass, 'async_add_job') as mock_add_job, \ + patch.object(hass, 'config_entries') as mock_config_entries, \ + patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(True)): + assert await deconz.async_setup_entry(hass, entry) is True + assert hass.data[deconz.DOMAIN] + assert hass.data[deconz.DATA_DECONZ_ID] == {} + assert len(mock_add_job.mock_calls) == 4 + assert len(mock_config_entries.async_forward_entry_setup.mock_calls) == 4 + assert mock_config_entries.async_forward_entry_setup.mock_calls[0][1] == \ + (entry, 'binary_sensor') + assert mock_config_entries.async_forward_entry_setup.mock_calls[1][1] == \ + (entry, 'light') + assert mock_config_entries.async_forward_entry_setup.mock_calls[2][1] == \ + (entry, 'scene') + assert mock_config_entries.async_forward_entry_setup.mock_calls[3][1] == \ + (entry, 'sensor') diff --git a/tests/components/light/test_deconz.py b/tests/components/light/test_deconz.py new file mode 100644 index 00000000000..d907697354e --- /dev/null +++ b/tests/components/light/test_deconz.py @@ -0,0 +1,74 @@ +"""deCONZ light platform tests.""" +from unittest.mock import Mock, patch + +from homeassistant import config_entries +from homeassistant.components import deconz + +from tests.common import mock_coro + + +LIGHT = { + "1": { + "id": "Light 1 id", + "name": "Light 1 name", + "state": {} + } +} + +GROUP = { + "1": { + "id": "Group 1 id", + "name": "Group 1 name", + "state": {}, + "action": {}, + "scenes": [], + "lights": [ + "1", + "2" + ] + }, + "2": { + "id": "Group 2 id", + "name": "Group 2 name", + "state": {}, + "action": {}, + "scenes": [] + }, +} + + +async def setup_bridge(hass, data): + """Load the deCONZ light platform.""" + from pydeconz import DeconzSession + loop = Mock() + session = Mock() + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + bridge = DeconzSession(loop, session, **entry.data) + with patch('pydeconz.DeconzSession.async_get_state', + return_value=mock_coro(data)): + await bridge.async_load_parameters() + hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_ID] = {} + config_entry = config_entries.ConfigEntry( + 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + await hass.config_entries.async_forward_entry_setup(config_entry, 'light') + # To flush out the service call to update the group + await hass.async_block_till_done() + + +async def test_no_lights_or_groups(hass): + """Test the update_lights function with some lights.""" + data = {} + await setup_bridge(hass, data) + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + assert len(hass.states.async_all()) == 0 + + +async def test_lights_and_groups(hass): + """Test the update_lights function with some lights.""" + await setup_bridge(hass, {"lights": LIGHT, "groups": GROUP}) + assert "light.light_1_name" in hass.data[deconz.DATA_DECONZ_ID] + assert "light.group_1_name" in hass.data[deconz.DATA_DECONZ_ID] + assert "light.group_2_name" not in hass.data[deconz.DATA_DECONZ_ID] + assert len(hass.states.async_all()) == 3 diff --git a/tests/components/scene/test_deconz.py b/tests/components/scene/test_deconz.py new file mode 100644 index 00000000000..53f25808be2 --- /dev/null +++ b/tests/components/scene/test_deconz.py @@ -0,0 +1,57 @@ +"""deCONZ scenes platform tests.""" +from unittest.mock import Mock, patch + +from homeassistant import config_entries +from homeassistant.components import deconz + +from tests.common import mock_coro + + +GROUP = { + "1": { + "id": "Group 1 id", + "name": "Group 1 name", + "state": {}, + "action": {}, + "scenes": [{ + "id": "1", + "name": "Scene 1" + }], + } +} + + +async def setup_bridge(hass, data): + """Load the deCONZ scene platform.""" + from pydeconz import DeconzSession + loop = Mock() + session = Mock() + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + bridge = DeconzSession(loop, session, **entry.data) + with patch('pydeconz.DeconzSession.async_get_state', + return_value=mock_coro(data)): + await bridge.async_load_parameters() + hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_ID] = {} + config_entry = config_entries.ConfigEntry( + 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + await hass.config_entries.async_forward_entry_setup(config_entry, 'scene') + # To flush out the service call to update the group + await hass.async_block_till_done() + + +async def test_no_scenes(hass): + """Test the update_lights function with some lights.""" + data = {} + await setup_bridge(hass, data) + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + assert len(hass.states.async_all()) == 0 + + +async def test_scenes(hass): + """Test the update_lights function with some lights.""" + data = {"groups": GROUP} + await setup_bridge(hass, data) + assert "scene.group_1_name_scene_1" in hass.data[deconz.DATA_DECONZ_ID] + assert len(hass.states.async_all()) == 1 diff --git a/tests/components/sensor/test_deconz.py b/tests/components/sensor/test_deconz.py new file mode 100644 index 00000000000..b70fb396686 --- /dev/null +++ b/tests/components/sensor/test_deconz.py @@ -0,0 +1,82 @@ +"""deCONZ sensor platform tests.""" +from unittest.mock import Mock, patch + +from homeassistant import config_entries +from homeassistant.components import deconz + +from tests.common import mock_coro + + +SENSOR = { + "1": { + "id": "Sensor 1 id", + "name": "Sensor 1 name", + "type": "ZHATemperature", + "state": {"temperature": False}, + "config": {} + }, + "2": { + "id": "Sensor 2 id", + "name": "Sensor 2 name", + "type": "ZHAPresence", + "state": {"presence": False}, + "config": {} + }, + "3": { + "id": "Sensor 3 id", + "name": "Sensor 3 name", + "type": "ZHASwitch", + "state": {"buttonevent": 1000}, + "config": {} + }, + "4": { + "id": "Sensor 4 id", + "name": "Sensor 4 name", + "type": "ZHASwitch", + "state": {"buttonevent": 1000}, + "config": {"battery": 100} + } +} + + +async def setup_bridge(hass, data): + """Load the deCONZ sensor platform.""" + from pydeconz import DeconzSession + loop = Mock() + session = Mock() + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + bridge = DeconzSession(loop, session, **entry.data) + with patch('pydeconz.DeconzSession.async_get_state', + return_value=mock_coro(data)): + await bridge.async_load_parameters() + hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_ID] = {} + config_entry = config_entries.ConfigEntry( + 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') + await hass.config_entries.async_forward_entry_setup(config_entry, 'sensor') + # To flush out the service call to update the group + await hass.async_block_till_done() + + +async def test_no_sensors(hass): + """Test the update_lights function with some lights.""" + data = {} + await setup_bridge(hass, data) + assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + assert len(hass.states.async_all()) == 0 + + +async def test_binary_sensors(hass): + """Test the update_lights function with some lights.""" + data = {"sensors": SENSOR} + await setup_bridge(hass, data) + assert "sensor.sensor_1_name" in hass.data[deconz.DATA_DECONZ_ID] + assert "sensor.sensor_2_name" not in hass.data[deconz.DATA_DECONZ_ID] + assert "sensor.sensor_3_name" not in hass.data[deconz.DATA_DECONZ_ID] + assert "sensor.sensor_3_name_battery_level" not in \ + hass.data[deconz.DATA_DECONZ_ID] + assert "sensor.sensor_4_name" not in hass.data[deconz.DATA_DECONZ_ID] + assert "sensor.sensor_4_name_battery_level" in \ + hass.data[deconz.DATA_DECONZ_ID] + assert len(hass.states.async_all()) == 2