deCONZ - Support device registry (#16115)

Add support for device registry in deCONZ component
This commit is contained in:
Robert Svensson 2018-08-24 19:37:22 +02:00 committed by GitHub
parent e8775ba2b4
commit e91a1529e4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
12 changed files with 162 additions and 36 deletions

View File

@ -7,9 +7,10 @@ https://home-assistant.io/components/binary_sensor.deconz/
from homeassistant.components.binary_sensor import BinarySensorDevice
from homeassistant.components.deconz.const import (
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ,
DATA_DECONZ_ID, DATA_DECONZ_UNSUB)
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN)
from homeassistant.const import ATTR_BATTERY_LEVEL
from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['deconz']
@ -113,3 +114,19 @@ class DeconzBinarySensor(BinarySensorDevice):
if self._sensor.type in PRESENCE and self._sensor.dark is not None:
attr[ATTR_DARK] = self._sensor.dark
return attr
@property
def device(self):
"""Return a device description for device registry."""
if (self._sensor.uniqueid is None or
self._sensor.uniqueid.count(':') != 7):
return None
serial = self._sensor.uniqueid.split('-', 1)[0]
return {
'connection': [[CONNECTION_ZIGBEE, serial]],
'identifiers': [[DECONZ_DOMAIN, serial]],
'manufacturer': self._sensor.manufacturer,
'model': self._sensor.modelid,
'name': self._sensor.name,
'sw_version': self._sensor.swversion,
}

View File

@ -12,6 +12,7 @@ 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.device_registry import CONNECTION_NETWORK_MAC
from homeassistant.helpers.dispatcher import (
async_dispatcher_connect, async_dispatcher_send)
from homeassistant.util import slugify
@ -23,7 +24,7 @@ from .const import (
CONF_ALLOW_CLIP_SENSOR, CONFIG_FILE, DATA_DECONZ_EVENT,
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DOMAIN, _LOGGER)
REQUIREMENTS = ['pydeconz==43']
REQUIREMENTS = ['pydeconz==44']
CONFIG_SCHEMA = vol.Schema({
DOMAIN: vol.Schema({
@ -119,6 +120,14 @@ async def async_setup_entry(hass, config_entry):
deconz.start()
device_registry = await \
hass.helpers.device_registry.async_get_registry()
device_registry.async_get_or_create(
connection=[[CONNECTION_NETWORK_MAC, deconz.config.mac]],
identifiers=[[DOMAIN, deconz.config.bridgeid]],
manufacturer='Dresden Elektronik', model=deconz.config.modelid,
name=deconz.config.name, sw_version=deconz.config.swversion)
async def async_configure(call):
"""Set attribute of device in deCONZ.

View File

@ -8,6 +8,7 @@ CONFIG_FILE = 'deconz.conf'
DATA_DECONZ_EVENT = 'deconz_events'
DATA_DECONZ_ID = 'deconz_entities'
DATA_DECONZ_UNSUB = 'deconz_dispatchers'
DECONZ_DOMAIN = 'deconz'
CONF_ALLOW_CLIP_SENSOR = 'allow_clip_sensor'
CONF_ALLOW_DECONZ_GROUPS = 'allow_deconz_groups'

View File

@ -6,13 +6,14 @@ https://home-assistant.io/components/light.deconz/
"""
from homeassistant.components.deconz.const import (
CONF_ALLOW_DECONZ_GROUPS, DOMAIN as DATA_DECONZ,
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, SWITCH_TYPES)
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN, SWITCH_TYPES)
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.device_registry import CONNECTION_ZIGBEE
from homeassistant.helpers.dispatcher import async_dispatcher_connect
import homeassistant.util.color as color_util
@ -199,3 +200,19 @@ class DeconzLight(Light):
if self._light.type == 'LightGroup':
attributes['all_on'] = self._light.all_on
return attributes
@property
def device(self):
"""Return a device description for device registry."""
if (self._light.uniqueid is None or
self._light.uniqueid.count(':') != 7):
return None
serial = self._light.uniqueid.split('-', 1)[0]
return {
'connection': [[CONNECTION_ZIGBEE, serial]],
'identifiers': [[DECONZ_DOMAIN, serial]],
'manufacturer': self._light.manufacturer,
'model': self._light.modelid,
'name': self._light.name,
'sw_version': self._light.swversion,
}

View File

@ -6,10 +6,11 @@ https://home-assistant.io/components/sensor.deconz/
"""
from homeassistant.components.deconz.const import (
ATTR_DARK, ATTR_ON, CONF_ALLOW_CLIP_SENSOR, DOMAIN as DATA_DECONZ,
DATA_DECONZ_ID, DATA_DECONZ_UNSUB)
DATA_DECONZ_ID, DATA_DECONZ_UNSUB, DECONZ_DOMAIN)
from homeassistant.const import (
ATTR_BATTERY_LEVEL, ATTR_VOLTAGE, DEVICE_CLASS_BATTERY)
from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
from homeassistant.helpers.dispatcher import async_dispatcher_connect
from homeassistant.helpers.entity import Entity
from homeassistant.util import slugify
@ -134,6 +135,22 @@ class DeconzSensor(Entity):
attr[ATTR_DAYLIGHT] = self._sensor.daylight
return attr
@property
def device(self):
"""Return a device description for device registry."""
if (self._sensor.uniqueid is None or
self._sensor.uniqueid.count(':') != 7):
return None
serial = self._sensor.uniqueid.split('-', 1)[0]
return {
'connection': [[CONNECTION_ZIGBEE, serial]],
'identifiers': [[DECONZ_DOMAIN, serial]],
'manufacturer': self._sensor.manufacturer,
'model': self._sensor.modelid,
'name': self._sensor.name,
'sw_version': self._sensor.swversion,
}
class DeconzBattery(Entity):
"""Battery class for when a device is only represented as an event."""
@ -192,3 +209,19 @@ class DeconzBattery(Entity):
ATTR_EVENT_ID: slugify(self._device.name),
}
return attr
@property
def device(self):
"""Return a device description for device registry."""
if (self._device.uniqueid is None or
self._device.uniqueid.count(':') != 7):
return None
serial = self._device.uniqueid.split('-', 1)[0]
return {
'connection': [[CONNECTION_ZIGBEE, serial]],
'identifiers': [[DECONZ_DOMAIN, serial]],
'manufacturer': self._device.manufacturer,
'model': self._device.modelid,
'name': self._device.name,
'sw_version': self._device.swversion,
}

View File

@ -6,9 +6,10 @@ https://home-assistant.io/components/switch.deconz/
"""
from homeassistant.components.deconz.const import (
DOMAIN as DATA_DECONZ, DATA_DECONZ_ID, DATA_DECONZ_UNSUB,
POWER_PLUGS, SIRENS)
DECONZ_DOMAIN, POWER_PLUGS, SIRENS)
from homeassistant.components.switch import SwitchDevice
from homeassistant.core import callback
from homeassistant.helpers.device_registry import CONNECTION_ZIGBEE
from homeassistant.helpers.dispatcher import async_dispatcher_connect
DEPENDENCIES = ['deconz']
@ -79,6 +80,22 @@ class DeconzSwitch(SwitchDevice):
"""No polling needed."""
return False
@property
def device(self):
"""Return a device description for device registry."""
if (self._switch.uniqueid is None or
self._switch.uniqueid.count(':') != 7):
return None
serial = self._switch.uniqueid.split('-', 1)[0]
return {
'connection': [[CONNECTION_ZIGBEE, serial]],
'identifiers': [[DECONZ_DOMAIN, serial]],
'manufacturer': self._switch.manufacturer,
'model': self._switch.modelid,
'name': self._switch.name,
'sw_version': self._switch.swversion,
}
class DeconzPowerPlug(DeconzSwitch):
"""Representation of power plugs from deCONZ."""

View File

@ -15,15 +15,18 @@ STORAGE_KEY = 'core.device_registry'
STORAGE_VERSION = 1
SAVE_DELAY = 10
CONNECTION_NETWORK_MAC = 'mac'
CONNECTION_ZIGBEE = 'zigbee'
@attr.s(slots=True, frozen=True)
class DeviceEntry:
"""Device Registry Entry."""
connection = attr.ib(type=list)
identifiers = attr.ib(type=list)
manufacturer = attr.ib(type=str)
model = attr.ib(type=str)
connection = attr.ib(type=list)
name = attr.ib(type=str, default=None)
sw_version = attr.ib(type=str, default=None)
id = attr.ib(type=str, default=attr.Factory(lambda: uuid.uuid4().hex))
@ -48,8 +51,8 @@ class DeviceRegistry:
return None
@callback
def async_get_or_create(self, identifiers, manufacturer, model,
connection, *, name=None, sw_version=None):
def async_get_or_create(self, *, connection, identifiers, manufacturer,
model, name=None, sw_version=None):
"""Get device. Create if it doesn't exist."""
device = self.async_get_device(identifiers, connection)
@ -57,10 +60,10 @@ class DeviceRegistry:
return device
device = DeviceEntry(
connection=connection,
identifiers=identifiers,
manufacturer=manufacturer,
model=model,
connection=connection,
name=name,
sw_version=sw_version
)
@ -93,10 +96,10 @@ class DeviceRegistry:
data['devices'] = [
{
'id': entry.id,
'connection': entry.connection,
'identifiers': entry.identifiers,
'manufacturer': entry.manufacturer,
'model': entry.model,
'connection': entry.connection,
'name': entry.name,
'sw_version': entry.sw_version,
} for entry in self.devices

View File

@ -275,8 +275,11 @@ class EntityPlatform:
device = entity.device
if device is not None:
device = device_registry.async_get_or_create(
device['identifiers'], device['manufacturer'],
device['model'], device['connection'],
connection=device['connection'],
identifiers=device['identifiers'],
manufacturer=device['manufacturer'],
model=device['model'],
name=device.get('name'),
sw_version=device.get('sw_version'))
device_id = device.id
else:

View File

@ -810,7 +810,7 @@ pycsspeechtts==1.0.2
pydaikin==0.4
# homeassistant.components.deconz
pydeconz==43
pydeconz==44
# homeassistant.components.zwave
pydispatcher==2.0.5

View File

@ -139,7 +139,7 @@ py-canary==0.5.0
pyblackbird==0.5
# homeassistant.components.deconz
pydeconz==43
pydeconz==44
# homeassistant.components.zwave
pydispatcher==2.0.5

View File

@ -7,6 +7,16 @@ from homeassistant.components import deconz
from tests.common import mock_coro
CONFIG = {
"config": {
"bridgeid": "0123456789ABCDEF",
"mac": "12:34:56:78:90:ab",
"modelid": "deCONZ",
"name": "Phoscon",
"swversion": "2.05.35"
}
}
async def test_config_with_host_passed_to_config_entry(hass):
"""Test that configured options for a host are loaded via config entry."""
@ -93,8 +103,11 @@ async def test_setup_entry_successful(hass):
entry.data = {'host': '1.2.3.4', 'port': 80, 'api_key': '1234567890ABCDEF'}
with patch.object(hass, 'async_create_task') as mock_add_job, \
patch.object(hass, 'config_entries') as mock_config_entries, \
patch('pydeconz.DeconzSession.async_load_parameters',
return_value=mock_coro(True)):
patch('pydeconz.DeconzSession.async_get_state',
return_value=mock_coro(CONFIG)), \
patch('pydeconz.DeconzSession.start', return_value=True), \
patch('homeassistant.helpers.device_registry.async_get_registry',
return_value=mock_coro(Mock())):
assert await deconz.async_setup_entry(hass, entry) is True
assert hass.data[deconz.DOMAIN]
assert hass.data[deconz.DATA_DECONZ_ID] == {}
@ -117,10 +130,15 @@ async def test_unload_entry(hass):
"""Test being able to unload an entry."""
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(True)):
entry.async_unload.return_value = mock_coro(True)
deconzmock = Mock()
deconzmock.async_load_parameters.return_value = mock_coro(True)
deconzmock.sensors = {}
with patch('pydeconz.DeconzSession', return_value=deconzmock):
assert await deconz.async_setup_entry(hass, entry) is True
assert deconz.DATA_DECONZ_EVENT in hass.data
hass.data[deconz.DATA_DECONZ_EVENT].append(Mock())
hass.data[deconz.DATA_DECONZ_ID] = {'id': 'deconzid'}
assert await deconz.async_unload_entry(hass, entry)
@ -132,6 +150,9 @@ async def test_unload_entry(hass):
async def test_add_new_device(hass):
"""Test adding a new device generates a signal for platforms."""
entry = Mock()
entry.data = {'host': '1.2.3.4', 'port': 80,
'api_key': '1234567890ABCDEF', 'allow_clip_sensor': False}
new_event = {
"t": "event",
"e": "added",
@ -147,11 +168,10 @@ async def test_add_new_device(hass):
"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)):
patch('pydeconz.DeconzSession.async_get_state',
return_value=mock_coro(CONFIG)), \
patch('pydeconz.DeconzSession.start', return_value=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()
@ -162,15 +182,16 @@ async def test_add_new_device(hass):
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'}
entry.data = {'host': '1.2.3.4', 'port': 80,
'api_key': '1234567890ABCDEF', 'allow_clip_sensor': False}
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)):
with patch('pydeconz.DeconzSession.async_get_state',
return_value=mock_coro(CONFIG)), \
patch('pydeconz.DeconzSession.start', return_value=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
@ -185,8 +206,9 @@ async def test_do_not_allow_clip_sensor(hass):
remote.name = 'name'
remote.type = 'CLIPSwitch'
remote.register_async_callback = Mock()
with patch('pydeconz.DeconzSession.async_load_parameters',
return_value=mock_coro(True)):
with patch('pydeconz.DeconzSession.async_get_state',
return_value=mock_coro(CONFIG)), \
patch('pydeconz.DeconzSession.start', return_value=True):
assert await deconz.async_setup_entry(hass, entry) is True
async_dispatcher_send(hass, 'deconz_new_sensor', [remote])

View File

@ -26,14 +26,17 @@ def registry(hass):
async def test_get_or_create_returns_same_entry(registry):
"""Make sure we do not duplicate entries."""
entry = registry.async_get_or_create(
[['bridgeid', '0123']], 'manufacturer', 'model',
[['ethernet', '12:34:56:78:90:AB:CD:EF']])
connection=[['ethernet', '12:34:56:78:90:AB:CD:EF']],
identifiers=[['bridgeid', '0123']],
manufacturer='manufacturer', model='model')
entry2 = registry.async_get_or_create(
[['bridgeid', '0123']], 'manufacturer', 'model',
[['ethernet', '11:22:33:44:55:66:77:88']])
connection=[['ethernet', '11:22:33:44:55:66:77:88']],
identifiers=[['bridgeid', '0123']],
manufacturer='manufacturer', model='model')
entry3 = registry.async_get_or_create(
[['bridgeid', '1234']], 'manufacturer', 'model',
[['ethernet', '12:34:56:78:90:AB:CD:EF']])
connection=[['ethernet', '12:34:56:78:90:AB:CD:EF']],
identifiers=[['bridgeid', '1234']],
manufacturer='manufacturer', model='model')
assert len(registry.devices) == 1
assert entry is entry2
@ -73,6 +76,7 @@ async def test_loading_from_storage(hass, hass_storage):
registry = await device_registry.async_get_registry(hass)
entry = registry.async_get_or_create(
[['serial', '12:34:56:78:90:AB:CD:EF']], 'manufacturer',
'model', [['Zigbee', '01.23.45.67.89']])
connection=[['Zigbee', '01.23.45.67.89']],
identifiers=[['serial', '12:34:56:78:90:AB:CD:EF']],
manufacturer='manufacturer', model='model')
assert entry.id == 'abcdefghijklm'