From e91a1529e43a9ce08a9858d4fce59f4bcc7607e0 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Fri, 24 Aug 2018 19:37:22 +0200 Subject: [PATCH] deCONZ - Support device registry (#16115) Add support for device registry in deCONZ component --- .../components/binary_sensor/deconz.py | 19 ++++++- homeassistant/components/deconz/__init__.py | 11 +++- homeassistant/components/deconz/const.py | 1 + homeassistant/components/light/deconz.py | 19 ++++++- homeassistant/components/sensor/deconz.py | 35 ++++++++++++- homeassistant/components/switch/deconz.py | 19 ++++++- homeassistant/helpers/device_registry.py | 13 +++-- homeassistant/helpers/entity_platform.py | 7 ++- requirements_all.txt | 2 +- requirements_test_all.txt | 2 +- tests/components/deconz/test_init.py | 50 +++++++++++++------ tests/helpers/test_device_registry.py | 20 +++++--- 12 files changed, 162 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/binary_sensor/deconz.py b/homeassistant/components/binary_sensor/deconz.py index d3d27c053335..9aa0c446f2bd 100644 --- a/homeassistant/components/binary_sensor/deconz.py +++ b/homeassistant/components/binary_sensor/deconz.py @@ -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, + } diff --git a/homeassistant/components/deconz/__init__.py b/homeassistant/components/deconz/__init__.py index cf8d891661ec..d435e9e3c04b 100644 --- a/homeassistant/components/deconz/__init__.py +++ b/homeassistant/components/deconz/__init__.py @@ -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. diff --git a/homeassistant/components/deconz/const.py b/homeassistant/components/deconz/const.py index e7bc5605aee4..e629d57f2017 100644 --- a/homeassistant/components/deconz/const.py +++ b/homeassistant/components/deconz/const.py @@ -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' diff --git a/homeassistant/components/light/deconz.py b/homeassistant/components/light/deconz.py index 6dce6b7fdb85..067f1474f962 100644 --- a/homeassistant/components/light/deconz.py +++ b/homeassistant/components/light/deconz.py @@ -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, + } diff --git a/homeassistant/components/sensor/deconz.py b/homeassistant/components/sensor/deconz.py index a32f1e5e2106..45c604a74ee6 100644 --- a/homeassistant/components/sensor/deconz.py +++ b/homeassistant/components/sensor/deconz.py @@ -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, + } diff --git a/homeassistant/components/switch/deconz.py b/homeassistant/components/switch/deconz.py index 11f7f42c6c92..7d861e4c29cf 100644 --- a/homeassistant/components/switch/deconz.py +++ b/homeassistant/components/switch/deconz.py @@ -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.""" diff --git a/homeassistant/helpers/device_registry.py b/homeassistant/helpers/device_registry.py index 3276763a9676..19a6eaa62dc2 100644 --- a/homeassistant/helpers/device_registry.py +++ b/homeassistant/helpers/device_registry.py @@ -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 diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index c65aa5e98c20..ffac68c5f07e 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -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: diff --git a/requirements_all.txt b/requirements_all.txt index 25480a023ec0..447c6348500e 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -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 diff --git a/requirements_test_all.txt b/requirements_test_all.txt index 71cbc724c599..52688beaa268 100644 --- a/requirements_test_all.txt +++ b/requirements_test_all.txt @@ -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 diff --git a/tests/components/deconz/test_init.py b/tests/components/deconz/test_init.py index c6fc130a4a41..049a3b961b61 100644 --- a/tests/components/deconz/test_init.py +++ b/tests/components/deconz/test_init.py @@ -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]) diff --git a/tests/helpers/test_device_registry.py b/tests/helpers/test_device_registry.py index 41e7d39e977a..f7792eb52504 100644 --- a/tests/helpers/test_device_registry.py +++ b/tests/helpers/test_device_registry.py @@ -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'