deCONZ add new device without restart (#14221)

* Add new device without restarting hass

* Remove debug prints

* Fix copy paste error

* Fix comments from balloob
Add tests to verify signalling with new added devices

* Fix hound comments
Add test to verify when new sensor is added

* Fix tests

* Unload entry should unsubscribe all deconz dispatchers

* Make sure mock setup also creates unsub in hass data

* Fix copy paste issue

* Lint
This commit is contained in:
Robert Svensson 2018-05-05 16:11:00 +02:00 committed by Paulus Schoutsen
parent af8cd63838
commit 8410b63d9c
11 changed files with 212 additions and 48 deletions

View file

@ -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):

View file

@ -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

View file

@ -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'

View file

@ -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):

View file

@ -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):

View file

@ -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

View file

@ -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

View file

@ -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]

View file

@ -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

View file

@ -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]

View file

@ -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]