diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index a9a3e28f4be..9faa703d13c 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -6,9 +6,10 @@ https://home-assistant.io/components/binary_sensor.deconz/ """ from homeassistant.components.binary_sensor import BinarySensorDevice from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) + DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) from homeassistant.const import ATTR_BATTERY_LEVEL from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect DEPENDENCIES = ['deconz'] @@ -21,14 +22,19 @@ async def async_setup_platform(hass, config, async_add_devices, 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 = [] + @callback + def async_add_sensor(sensors): + """Add binary sensor from deCONZ.""" + from pydeconz.sensor import DECONZ_BINARY_SENSOR + entities = [] + for sensor in sensors: + if sensor.type in DECONZ_BINARY_SENSOR: + entities.append(DeconzBinarySensor(sensor)) + async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) - for sensor in sensors.values(): - if sensor and sensor.type in DECONZ_BINARY_SENSOR: - entities.append(DeconzBinarySensor(sensor)) - async_add_devices(entities, True) + async_add_sensor(hass.data[DATA_DECONZ].sensors.values()) class DeconzBinarySensor(BinarySensorDevice): diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index 75414598693..47573be6add 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -11,15 +11,18 @@ from homeassistant.const import ( CONF_ID, CONF_PORT, EVENT_HOMEASSISTANT_STOP) from homeassistant.core import EventOrigin, callback from homeassistant.helpers import aiohttp_client, config_validation as cv +from homeassistant.helpers.dispatcher import ( + async_dispatcher_connect, async_dispatcher_send) from homeassistant.util import slugify 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, DATA_DECONZ_EVENT, DATA_DECONZ_ID, DOMAIN, _LOGGER) + CONFIG_FILE, DATA_DECONZ_EVENT, DATA_DECONZ_ID, + DATA_DECONZ_UNSUB, DOMAIN, _LOGGER) -REQUIREMENTS = ['pydeconz==36'] +REQUIREMENTS = ['pydeconz==37'] CONFIG_SCHEMA = vol.Schema({ DOMAIN: vol.Schema({ @@ -69,14 +72,20 @@ async def async_setup_entry(hass, config_entry): Start websocket for push notification of state changes from deCONZ. """ from pydeconz import DeconzSession - from pydeconz.sensor import SWITCH as DECONZ_REMOTE if DOMAIN in hass.data: _LOGGER.error( "Config entry failed since one deCONZ instance already exists") return False + @callback + def async_add_device_callback(device_type, device): + """Called when a new device has been created in deCONZ.""" + async_dispatcher_send( + hass, 'deconz_new_{}'.format(device_type), [device]) + session = aiohttp_client.async_get_clientsession(hass) - deconz = DeconzSession(hass.loop, session, **config_entry.data) + deconz = DeconzSession(hass.loop, session, **config_entry.data, + async_add_device=async_add_device_callback) result = await deconz.async_load_parameters() if result is False: _LOGGER.error("Failed to communicate with deCONZ") @@ -84,14 +93,24 @@ async def async_setup_entry(hass, config_entry): hass.data[DOMAIN] = deconz hass.data[DATA_DECONZ_ID] = {} + hass.data[DATA_DECONZ_EVENT] = [] + hass.data[DATA_DECONZ_UNSUB] = [] for component in ['binary_sensor', 'light', 'scene', 'sensor']: hass.async_add_job(hass.config_entries.async_forward_entry_setup( config_entry, component)) - hass.data[DATA_DECONZ_EVENT] = [DeconzEvent( - hass, sensor) for sensor in deconz.sensors.values() - if sensor.type in DECONZ_REMOTE] + @callback + def async_add_remote(sensors): + """Setup remote from deCONZ.""" + from pydeconz.sensor import SWITCH as DECONZ_REMOTE + for sensor in sensors: + if sensor.type in DECONZ_REMOTE: + hass.data[DATA_DECONZ_EVENT].append(DeconzEvent(hass, sensor)) + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_remote)) + + async_add_remote(deconz.sensors.values()) deconz.start() @@ -148,6 +167,10 @@ async def async_unload_entry(hass, config_entry): for component in ['binary_sensor', 'light', 'scene', 'sensor']: await hass.config_entries.async_forward_entry_unload( config_entry, component) + dispatchers = hass.data[DATA_DECONZ_UNSUB] + for unsub_dispatcher in dispatchers: + unsub_dispatcher() + hass.data[DATA_DECONZ_UNSUB] = [] hass.data[DATA_DECONZ_EVENT] = [] hass.data[DATA_DECONZ_ID] = [] return True diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index e6d393c8ee7..48e5ea75d68 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -7,3 +7,4 @@ DOMAIN = 'deconz' CONFIG_FILE = 'deconz.conf' DATA_DECONZ_EVENT = 'deconz_events' DATA_DECONZ_ID = 'deconz_entities' +DATA_DECONZ_UNSUB = 'deconz_dispatchers' diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 36ad572a263..916e60c00b1 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -5,13 +5,14 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/light.deconz/ """ from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) + DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, ATTR_TRANSITION, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, Light) from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect import homeassistant.util.color as color_util DEPENDENCIES = ['deconz'] @@ -19,23 +20,35 @@ DEPENDENCIES = ['deconz'] async def async_setup_platform(hass, config, async_add_devices, discovery_info=None): - """Old way of setting up deCONZ lights.""" + """Old way of setting up deCONZ lights and group.""" 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 = [] + """Set up the deCONZ lights and groups from a config entry.""" + @callback + def async_add_light(lights): + """Add light from deCONZ.""" + entities = [] + for light in lights: + entities.append(DeconzLight(light)) + async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_light', async_add_light)) - for light in lights.values(): - entities.append(DeconzLight(light)) + @callback + def async_add_group(groups): + """Add group from deCONZ.""" + entities = [] + for group in groups: + if group.lights: + entities.append(DeconzLight(group)) + async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_group', async_add_group)) - for group in groups.values(): - if group.lights: # Don't create entity for group not containing light - entities.append(DeconzLight(group)) - async_add_devices(entities, True) + async_add_light(hass.data[DATA_DECONZ].lights.values()) + async_add_group(hass.data[DATA_DECONZ].groups.values()) class DeconzLight(Light): diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index b4a3cb8c6c5..221cdf2129e 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -5,10 +5,11 @@ For more details about this component, please refer to the documentation at https://home-assistant.io/components/sensor.deconz/ """ from homeassistant.components.deconz import ( - DOMAIN as DATA_DECONZ, DATA_DECONZ_ID) + DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB) from homeassistant.const import ( ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY) from homeassistant.core import callback +from homeassistant.helpers.dispatcher import async_dispatcher_connect from homeassistant.helpers.entity import Entity from homeassistant.util import slugify @@ -27,18 +28,23 @@ async def async_setup_platform(hass, config, async_add_devices, 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 = [] + @callback + def async_add_sensor(sensors): + """Add sensors from deCONZ.""" + from pydeconz.sensor import DECONZ_SENSOR, SWITCH as DECONZ_REMOTE + entities = [] + for sensor in sensors: + if sensor.type in DECONZ_SENSOR: + if sensor.type in DECONZ_REMOTE: + if sensor.battery: + entities.append(DeconzBattery(sensor)) + else: + entities.append(DeconzSensor(sensor)) + async_add_devices(entities, True) + hass.data[DATA_DECONZ_UNSUB].append( + async_dispatcher_connect(hass, 'deconz_new_sensor', async_add_sensor)) - for sensor in sensors.values(): - if sensor and sensor.type in DECONZ_SENSOR: - if sensor.type in DECONZ_REMOTE: - if sensor.battery: - entities.append(DeconzBattery(sensor)) - else: - entities.append(DeconzSensor(sensor)) - async_add_devices(entities, True) + async_add_sensor(hass.data[DATA_DECONZ].sensors.values()) class DeconzSensor(Entity): diff --git a/requirements_all.txt b/requirements_all.txt index 74f9ff8e195..406b460d0a4 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -745,7 +745,7 @@ pycsspeechtts==1.0.2 pydaikin==0.4 # homeassistant.components.deconz -pydeconz==36 +pydeconz==37 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 7816d9c6f24..df0f5722b86 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -133,7 +133,7 @@ py-canary==0.5.0 pyblackbird==0.5 # homeassistant.components.deconz -pydeconz==36 +pydeconz==37 # homeassistant.components.zwave pydispatcher==2.0.5 diff --git a/tests/components/binary_sensor/test_deconz.py b/tests/components/binary_sensor/test_deconz.py index 84ed059e97e..88dd0dae737 100644 --- a/tests/components/binary_sensor/test_deconz.py +++ b/tests/components/binary_sensor/test_deconz.py @@ -3,6 +3,7 @@ from unittest.mock import Mock, patch from homeassistant import config_entries from homeassistant.components import deconz +from homeassistant.helpers.dispatcher import async_dispatcher_send from tests.common import mock_coro @@ -14,6 +15,13 @@ SENSOR = { "type": "ZHAPresence", "state": {"presence": False}, "config": {} + }, + "2": { + "id": "Sensor 2 id", + "name": "Sensor 2 name", + "type": "ZHATemperature", + "state": {"temperature": False}, + "config": {} } } @@ -30,6 +38,7 @@ async def setup_bridge(hass, data): return_value=mock_coro(data)): await bridge.async_load_parameters() hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_UNSUB] = [] hass.data[deconz.DATA_DECONZ_ID] = {} config_entry = config_entries.ConfigEntry( 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') @@ -40,7 +49,7 @@ async def setup_bridge(hass, data): async def test_no_binary_sensors(hass): - """Test the update_lights function with some lights.""" + """Test that no sensors in deconz results in no sensor entities.""" data = {} await setup_bridge(hass, data) assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 @@ -48,8 +57,23 @@ async def test_no_binary_sensors(hass): async def test_binary_sensors(hass): - """Test the update_lights function with some lights.""" + """Test successful creation of binary sensor entities.""" data = {"sensors": SENSOR} await setup_bridge(hass, data) assert "binary_sensor.sensor_1_name" in hass.data[deconz.DATA_DECONZ_ID] + assert "binary_sensor.sensor_2_name" not in \ + hass.data[deconz.DATA_DECONZ_ID] assert len(hass.states.async_all()) == 1 + + +async def test_add_new_sensor(hass): + """Test successful creation of sensor entities.""" + data = {} + await setup_bridge(hass, data) + sensor = Mock() + sensor.name = 'name' + sensor.type = 'ZHAPresence' + sensor.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) + await hass.async_block_till_done() + assert "binary_sensor.name" in hass.data[deconz.DATA_DECONZ_ID] diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index b09edf42a87..888094deea6 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -1,6 +1,7 @@ """Test deCONZ component setup process.""" from unittest.mock import Mock, patch +from homeassistant.helpers.dispatcher import async_dispatcher_send from homeassistant.setup import async_setup_component from homeassistant.components import deconz @@ -97,6 +98,7 @@ async def test_setup_entry_successful(hass): assert await deconz.async_setup_entry(hass, entry) is True assert hass.data[deconz.DOMAIN] assert hass.data[deconz.DATA_DECONZ_ID] == {} + assert len(hass.data[deconz.DATA_DECONZ_UNSUB]) == 1 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] == \ @@ -121,5 +123,52 @@ async def test_unload_entry(hass): hass.data[deconz.DATA_DECONZ_ID] = {'id': 'deconzid'} assert await deconz.async_unload_entry(hass, entry) assert deconz.DOMAIN not in hass.data + assert len(hass.data[deconz.DATA_DECONZ_UNSUB]) == 0 assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 0 assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 + + +async def test_add_new_device(hass): + """Test adding a new device generates a signal for platforms.""" + new_event = { + "t": "event", + "e": "added", + "r": "sensors", + "id": "1", + "sensor": { + "config": { + "on": "True", + "reachable": "True" + }, + "name": "event", + "state": {}, + "type": "ZHASwitch" + } + } + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + with patch.object(deconz, 'async_dispatcher_send') as mock_dispatch_send, \ + patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(True)): + assert await deconz.async_setup_entry(hass, entry) is True + hass.data[deconz.DOMAIN].async_event_handler(new_event) + 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_new_remote(hass): + """Test new added device creates a new remote.""" + entry = Mock() + entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'} + remote = Mock() + remote.name = 'name' + remote.type = 'ZHASwitch' + remote.register_async_callback = Mock() + with patch('pydeconz.DeconzSession.async_load_parameters', + return_value=mock_coro(True)): + assert await deconz.async_setup_entry(hass, entry) is True + + async_dispatcher_send(hass, 'deconz_new_sensor', [remote]) + await hass.async_block_till_done() + assert len(hass.data[deconz.DATA_DECONZ_EVENT]) == 1 diff --git a/tests/components/light/test_deconz.py b/tests/components/light/test_deconz.py index d907697354e..2608d77ce2a 100644 --- a/tests/components/light/test_deconz.py +++ b/tests/components/light/test_deconz.py @@ -3,6 +3,7 @@ from unittest.mock import Mock, patch from homeassistant import config_entries from homeassistant.components import deconz +from homeassistant.helpers.dispatcher import async_dispatcher_send from tests.common import mock_coro @@ -49,6 +50,7 @@ async def setup_bridge(hass, data): return_value=mock_coro(data)): await bridge.async_load_parameters() hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_UNSUB] = [] hass.data[deconz.DATA_DECONZ_ID] = {} config_entry = config_entries.ConfigEntry( 1, deconz.DOMAIN, 'Mock Title', {'host': 'mock-host'}, 'test') @@ -58,7 +60,7 @@ async def setup_bridge(hass, data): async def test_no_lights_or_groups(hass): - """Test the update_lights function with some lights.""" + """Test that no lights or groups entities are created.""" data = {} await setup_bridge(hass, data) assert len(hass.data[deconz.DATA_DECONZ_ID]) == 0 @@ -66,9 +68,33 @@ async def test_no_lights_or_groups(hass): async def test_lights_and_groups(hass): - """Test the update_lights function with some lights.""" + """Test that lights or groups entities are created.""" 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 + + +async def test_add_new_light(hass): + """Test successful creation of light entities.""" + data = {} + await setup_bridge(hass, data) + light = Mock() + light.name = 'name' + light.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_light', [light]) + await hass.async_block_till_done() + assert "light.name" in hass.data[deconz.DATA_DECONZ_ID] + + +async def test_add_new_group(hass): + """Test successful creation of group entities.""" + data = {} + await setup_bridge(hass, data) + group = Mock() + group.name = 'name' + group.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_group', [group]) + await hass.async_block_till_done() + assert "light.name" in hass.data[deconz.DATA_DECONZ_ID] diff --git a/tests/components/sensor/test_deconz.py b/tests/components/sensor/test_deconz.py index d6c026e88bd..8f6a53e6e65 100644 --- a/tests/components/sensor/test_deconz.py +++ b/tests/components/sensor/test_deconz.py @@ -1,8 +1,10 @@ """deCONZ sensor platform tests.""" from unittest.mock import Mock, patch + from homeassistant import config_entries from homeassistant.components import deconz +from homeassistant.helpers.dispatcher import async_dispatcher_send from tests.common import mock_coro @@ -51,6 +53,7 @@ async def setup_bridge(hass, data): return_value=mock_coro(data)): await bridge.async_load_parameters() hass.data[deconz.DOMAIN] = bridge + hass.data[deconz.DATA_DECONZ_UNSUB] = [] hass.data[deconz.DATA_DECONZ_EVENT] = [] hass.data[deconz.DATA_DECONZ_ID] = {} config_entry = config_entries.ConfigEntry( @@ -61,15 +64,15 @@ async def setup_bridge(hass, data): async def test_no_sensors(hass): - """Test the update_lights function with some lights.""" + """Test that no sensors in deconz results in no sensor entities.""" 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.""" +async def test_sensors(hass): + """Test successful creation of sensor entities.""" data = {"sensors": SENSOR} await setup_bridge(hass, data) assert "sensor.sensor_1_name" in hass.data[deconz.DATA_DECONZ_ID] @@ -81,3 +84,16 @@ async def test_binary_sensors(hass): assert "sensor.sensor_4_name_battery_level" in \ hass.data[deconz.DATA_DECONZ_ID] assert len(hass.states.async_all()) == 2 + + +async def test_add_new_sensor(hass): + """Test successful creation of sensor entities.""" + data = {} + await setup_bridge(hass, data) + sensor = Mock() + sensor.name = 'name' + sensor.type = 'ZHATemperature' + sensor.register_async_callback = Mock() + async_dispatcher_send(hass, 'deconz_new_sensor', [sensor]) + await hass.async_block_till_done() + assert "sensor.name" in hass.data[deconz.DATA_DECONZ_ID]