From e51427b28458b86da21f7f317d03c09e5136225a Mon Sep 17 00:00:00 2001 From: Paulus Schoutsen Date: Tue, 30 Jan 2018 01:39:39 -0800 Subject: [PATCH] Entity registry (#11979) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Entity#unique_id defaults to None * Initial commit entity registry * Clean up unique_id property * Lint * Add tests to entity component * Lint * Restore some unique ids * Spelling * Remove use of IP address for unique ID * Add tests * Add tests * Fix tests * Add some docs * Add one more test * Fix new test… --- .../components/binary_sensor/bloomsky.py | 6 - .../components/binary_sensor/ecobee.py | 5 - .../components/binary_sensor/hikvision.py | 2 +- .../components/binary_sensor/netatmo.py | 4 +- .../components/binary_sensor/wemo.py | 2 +- homeassistant/components/camera/netatmo.py | 8 +- homeassistant/components/climate/daikin.py | 5 - homeassistant/components/climate/nest.py | 5 + homeassistant/components/cover/rpi_gpio.py | 5 - homeassistant/components/fan/insteon_local.py | 2 +- homeassistant/components/group/__init__.py | 7 +- homeassistant/components/light/avion.py | 2 +- homeassistant/components/light/decora.py | 2 +- homeassistant/components/light/flux_led.py | 5 - homeassistant/components/light/hue.py | 9 +- .../components/light/insteon_local.py | 2 +- homeassistant/components/light/tikteck.py | 2 +- homeassistant/components/light/wemo.py | 5 +- homeassistant/components/light/yeelight.py | 5 - homeassistant/components/light/zengge.py | 2 +- homeassistant/components/media_player/emby.py | 2 +- homeassistant/components/media_player/plex.py | 3 +- .../components/media_player/yamaha.py | 5 - homeassistant/components/sensor/blink.py | 5 - homeassistant/components/sensor/bloomsky.py | 6 - homeassistant/components/sensor/canary.py | 3 +- homeassistant/components/sensor/daikin.py | 5 - homeassistant/components/sensor/ecobee.py | 7 +- homeassistant/components/sensor/ios.py | 2 +- homeassistant/components/sensor/isy994.py | 5 - homeassistant/components/sensor/netatmo.py | 3 +- .../components/switch/insteon_local.py | 2 +- .../components/switch/rainmachine.py | 3 +- homeassistant/components/switch/wemo.py | 2 +- homeassistant/components/tahoma.py | 8 -- homeassistant/components/zwave/__init__.py | 4 +- homeassistant/helpers/entity.py | 20 ++- homeassistant/helpers/entity_component.py | 60 +++++--- homeassistant/helpers/entity_registry.py | 134 +++++++++++++++++ homeassistant/util/yaml.py | 8 ++ tests/common.py | 9 ++ tests/components/camera/test_local_file.py | 4 + tests/components/light/test_hue.py | 70 +-------- tests/components/sensor/test_file.py | 3 +- tests/components/zwave/test_init.py | 2 +- tests/helpers/test_entity_component.py | 106 ++++++++++---- tests/helpers/test_entity_registry.py | 135 ++++++++++++++++++ 47 files changed, 471 insertions(+), 230 deletions(-) create mode 100644 homeassistant/helpers/entity_registry.py create mode 100644 tests/helpers/test_entity_registry.py diff --git a/homeassistant/components/binary_sensor/bloomsky.py b/homeassistant/components/binary_sensor/bloomsky.py index 5e69dcc9109..1d0849b255e 100644 --- a/homeassistant/components/binary_sensor/bloomsky.py +++ b/homeassistant/components/binary_sensor/bloomsky.py @@ -50,7 +50,6 @@ class BloomSkySensor(BinarySensorDevice): self._device_id = device['DeviceID'] self._sensor_name = sensor_name self._name = '{} {}'.format(device['DeviceName'], sensor_name) - self._unique_id = 'bloomsky_binary_sensor {}'.format(self._name) self._state = None @property @@ -58,11 +57,6 @@ class BloomSkySensor(BinarySensorDevice): """Return the name of the BloomSky device and this sensor.""" return self._name - @property - def unique_id(self): - """Return the unique ID for this sensor.""" - return self._unique_id - @property def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" diff --git a/homeassistant/components/binary_sensor/ecobee.py b/homeassistant/components/binary_sensor/ecobee.py index 214efb870b9..15efa21b226 100644 --- a/homeassistant/components/binary_sensor/ecobee.py +++ b/homeassistant/components/binary_sensor/ecobee.py @@ -50,11 +50,6 @@ class EcobeeBinarySensor(BinarySensorDevice): """Return the status of the sensor.""" return self._state == 'true' - @property - def unique_id(self): - """Return the unique ID of this sensor.""" - return "binary_sensor_ecobee_{}_{}".format(self._name, self.index) - @property def device_class(self): """Return the class of this sensor, from DEVICE_CLASSES.""" diff --git a/homeassistant/components/binary_sensor/hikvision.py b/homeassistant/components/binary_sensor/hikvision.py index 3ec70896426..ec64bdf07b8 100644 --- a/homeassistant/components/binary_sensor/hikvision.py +++ b/homeassistant/components/binary_sensor/hikvision.py @@ -212,7 +212,7 @@ class HikvisionBinarySensor(BinarySensorDevice): @property def unique_id(self): """Return an unique ID.""" - return '{}.{}'.format(self.__class__, self._id) + return self._id @property def is_on(self): diff --git a/homeassistant/components/binary_sensor/netatmo.py b/homeassistant/components/binary_sensor/netatmo.py index e597f1d0bbe..4d8aaa7d0d9 100644 --- a/homeassistant/components/binary_sensor/netatmo.py +++ b/homeassistant/components/binary_sensor/netatmo.py @@ -131,10 +131,8 @@ class NetatmoBinarySensor(BinarySensorDevice): self._name += ' / ' + module_name self._sensor_name = sensor self._name += ' ' + sensor - camera_id = data.camera_data.cameraByName( + self._unique_id = data.camera_data.cameraByName( camera=camera_name, home=home)['id'] - self._unique_id = "Netatmo_binary_sensor {0} - {1}".format( - self._name, camera_id) self._cameratype = camera_type self._state = None diff --git a/homeassistant/components/binary_sensor/wemo.py b/homeassistant/components/binary_sensor/wemo.py index 857c0c40777..cc1f602d871 100644 --- a/homeassistant/components/binary_sensor/wemo.py +++ b/homeassistant/components/binary_sensor/wemo.py @@ -58,7 +58,7 @@ class WemoBinarySensor(BinarySensorDevice): @property def unique_id(self): """Return the id of this WeMo device.""" - return '{}.{}'.format(self.__class__, self.wemo.serialnumber) + return self.wemo.serialnumber @property def name(self): diff --git a/homeassistant/components/camera/netatmo.py b/homeassistant/components/camera/netatmo.py index c1ec2db0a08..0a9a3fbdca4 100644 --- a/homeassistant/components/camera/netatmo.py +++ b/homeassistant/components/camera/netatmo.py @@ -64,13 +64,11 @@ class NetatmoCamera(Camera): self._name = home + ' / ' + camera_name else: self._name = camera_name - camera_id = data.camera_data.cameraByName( - camera=camera_name, home=home)['id'] - self._unique_id = "Welcome_camera {0} - {1}".format( - self._name, camera_id) self._vpnurl, self._localurl = self._data.camera_data.cameraUrls( camera=camera_name ) + self._unique_id = data.camera_data.cameraByName( + camera=camera_name, home=home)['id'] self._cameratype = camera_type def camera_image(self): @@ -117,5 +115,5 @@ class NetatmoCamera(Camera): @property def unique_id(self): - """Return the unique ID for this sensor.""" + """Return the unique ID for this camera.""" return self._unique_id diff --git a/homeassistant/components/climate/daikin.py b/homeassistant/components/climate/daikin.py index fea1fcee3a3..0ed4ebe8942 100644 --- a/homeassistant/components/climate/daikin.py +++ b/homeassistant/components/climate/daikin.py @@ -183,11 +183,6 @@ class DaikinClimate(ClimateDevice): self._force_refresh = True self._api.device.set(values) - @property - def unique_id(self): - """Return the ID of this AC.""" - return "{}.{}".format(self.__class__, self._api.ip_address) - @property def supported_features(self): """Return the list of supported features.""" diff --git a/homeassistant/components/climate/nest.py b/homeassistant/components/climate/nest.py index b4492821b1f..d8d7d6c901a 100644 --- a/homeassistant/components/climate/nest.py +++ b/homeassistant/components/climate/nest.py @@ -97,6 +97,11 @@ class NestThermostat(ClimateDevice): """Return the list of supported features.""" return SUPPORT_FLAGS + @property + def unique_id(self): + """Unique ID for this device.""" + return self.device.serial + @property def name(self): """Return the name of the nest, if any.""" diff --git a/homeassistant/components/cover/rpi_gpio.py b/homeassistant/components/cover/rpi_gpio.py index 1ee3ea00476..981312140eb 100644 --- a/homeassistant/components/cover/rpi_gpio.py +++ b/homeassistant/components/cover/rpi_gpio.py @@ -89,11 +89,6 @@ class RPiGPIOCover(CoverDevice): rpi_gpio.setup_input(self._state_pin, self._state_pull_mode) rpi_gpio.write_output(self._relay_pin, not self._invert_relay) - @property - def unique_id(self): - """Return the ID of this cover.""" - return '{}.{}'.format(self.__class__, self._name) - @property def name(self): """Return the name of the cover if any.""" diff --git a/homeassistant/components/fan/insteon_local.py b/homeassistant/components/fan/insteon_local.py index 85e603c8c81..e6f9424d852 100644 --- a/homeassistant/components/fan/insteon_local.py +++ b/homeassistant/components/fan/insteon_local.py @@ -60,7 +60,7 @@ class InsteonLocalFanDevice(FanEntity): @property def unique_id(self): """Return the ID of this Insteon node.""" - return 'insteon_local_{}_fan'.format(self.node.device_id) + return self.node.device_id @property def speed(self) -> str: diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 3881b6211c2..5e4dfdb0bdc 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -371,7 +371,6 @@ def async_setup(hass, config): @asyncio.coroutine def _async_process_config(hass, config, component): """Process group configuration.""" - groups = [] for object_id, conf in config.get(DOMAIN, {}).items(): name = conf.get(CONF_NAME, object_id) entity_ids = conf.get(CONF_ENTITIES) or [] @@ -381,13 +380,9 @@ def _async_process_config(hass, config, component): # Don't create tasks and await them all. The order is important as # groups get a number based on creation order. - group = yield from Group.async_create_group( + yield from Group.async_create_group( hass, name, entity_ids, icon=icon, view=view, control=control, object_id=object_id) - groups.append(group) - - if groups: - yield from component.async_add_entities(groups) class Group(Entity): diff --git a/homeassistant/components/light/avion.py b/homeassistant/components/light/avion.py index f214d47fa1b..5344c3dce6d 100644 --- a/homeassistant/components/light/avion.py +++ b/homeassistant/components/light/avion.py @@ -83,7 +83,7 @@ class AvionLight(Light): @property def unique_id(self): """Return the ID of this light.""" - return "{}.{}".format(self.__class__, self._address) + return self._address @property def name(self): diff --git a/homeassistant/components/light/decora.py b/homeassistant/components/light/decora.py index 3b6b22faba9..03441dd8ea6 100644 --- a/homeassistant/components/light/decora.py +++ b/homeassistant/components/light/decora.py @@ -88,7 +88,7 @@ class DecoraLight(Light): @property def unique_id(self): """Return the ID of this light.""" - return "{}.{}".format(self.__class__, self._address) + return self._address @property def name(self): diff --git a/homeassistant/components/light/flux_led.py b/homeassistant/components/light/flux_led.py index 396ddc984fa..075b98117f8 100644 --- a/homeassistant/components/light/flux_led.py +++ b/homeassistant/components/light/flux_led.py @@ -167,11 +167,6 @@ class FluxLight(Light): """Return True if entity is available.""" return self._bulb is not None - @property - def unique_id(self): - """Return the ID of this light.""" - return '{}.{}'.format(self.__class__, self._ipaddr) - @property def name(self): """Return the name of the device if any.""" diff --git a/homeassistant/components/light/hue.py b/homeassistant/components/light/hue.py index cbabaafd3fb..07ba069d831 100644 --- a/homeassistant/components/light/hue.py +++ b/homeassistant/components/light/hue.py @@ -228,14 +228,7 @@ class HueLight(Light): @property def unique_id(self): """Return the ID of this Hue light.""" - lid = self.info.get('uniqueid') - - if lid is None: - default_type = 'Group' if self.is_group else 'Light' - ltype = self.info.get('type', default_type) - lid = '{}.{}.{}'.format(self.name, ltype, self.light_id) - - return '{}.{}'.format(self.__class__, lid) + return self.info.get('uniqueid') @property def name(self): diff --git a/homeassistant/components/light/insteon_local.py b/homeassistant/components/light/insteon_local.py index 88d621d4060..bd7814df8f3 100644 --- a/homeassistant/components/light/insteon_local.py +++ b/homeassistant/components/light/insteon_local.py @@ -57,7 +57,7 @@ class InsteonLocalDimmerDevice(Light): @property def unique_id(self): """Return the ID of this Insteon node.""" - return 'insteon_local_{}'.format(self.node.device_id) + return self.node.device_id @property def brightness(self): diff --git a/homeassistant/components/light/tikteck.py b/homeassistant/components/light/tikteck.py index 07d4b63e99a..c39748e4430 100644 --- a/homeassistant/components/light/tikteck.py +++ b/homeassistant/components/light/tikteck.py @@ -70,7 +70,7 @@ class TikteckLight(Light): @property def unique_id(self): """Return the ID of this light.""" - return "{}.{}".format(self.__class__, self._address) + return self._address @property def name(self): diff --git a/homeassistant/components/light/wemo.py b/homeassistant/components/light/wemo.py index 693e40c0292..540c718b04d 100644 --- a/homeassistant/components/light/wemo.py +++ b/homeassistant/components/light/wemo.py @@ -76,8 +76,7 @@ class WemoLight(Light): @property def unique_id(self): """Return the ID of this light.""" - deviceid = self.device.uniqueID - return '{}.{}'.format(self.__class__, deviceid) + return self.device.uniqueID @property def name(self): @@ -176,7 +175,7 @@ class WemoDimmer(Light): @property def unique_id(self): """Return the ID of this WeMo dimmer.""" - return "{}.{}".format(self.__class__, self.wemo.serialnumber) + return self.wemo.serialnumber @property def name(self): diff --git a/homeassistant/components/light/yeelight.py b/homeassistant/components/light/yeelight.py index c31bfec4927..33c84df14be 100644 --- a/homeassistant/components/light/yeelight.py +++ b/homeassistant/components/light/yeelight.py @@ -175,11 +175,6 @@ class YeelightLight(Light): """Return the list of supported effects.""" return YEELIGHT_EFFECT_LIST - @property - def unique_id(self) -> str: - """Return the ID of this light.""" - return "{}.{}".format(self.__class__, self._ipaddr) - @property def color_temp(self) -> int: """Return the color temperature.""" diff --git a/homeassistant/components/light/zengge.py b/homeassistant/components/light/zengge.py index b453218c7c9..7071c8c43bb 100644 --- a/homeassistant/components/light/zengge.py +++ b/homeassistant/components/light/zengge.py @@ -67,7 +67,7 @@ class ZenggeLight(Light): @property def unique_id(self): """Return the ID of this light.""" - return "{}.{}".format(self.__class__, self._address) + return self._address @property def name(self): diff --git a/homeassistant/components/media_player/emby.py b/homeassistant/components/media_player/emby.py index 35d3ed35095..a3fe62c5a42 100644 --- a/homeassistant/components/media_player/emby.py +++ b/homeassistant/components/media_player/emby.py @@ -182,7 +182,7 @@ class EmbyDevice(MediaPlayerDevice): @property def unique_id(self): """Return the id of this emby client.""" - return '{}.{}'.format(self.__class__, self.device_id) + return self.device_id @property def supports_remote_control(self): diff --git a/homeassistant/components/media_player/plex.py b/homeassistant/components/media_player/plex.py index c96b0f3c2ae..38a84053263 100644 --- a/homeassistant/components/media_player/plex.py +++ b/homeassistant/components/media_player/plex.py @@ -459,8 +459,7 @@ class PlexClient(MediaPlayerDevice): @property def unique_id(self): """Return the id of this plex client.""" - return '{}.{}'.format(self.__class__, self.machine_identifier or - self.name) + return self.machine_identifier @property def name(self): diff --git a/homeassistant/components/media_player/yamaha.py b/homeassistant/components/media_player/yamaha.py index 8e4729a4409..f102d8a490d 100644 --- a/homeassistant/components/media_player/yamaha.py +++ b/homeassistant/components/media_player/yamaha.py @@ -149,11 +149,6 @@ class YamahaDevice(MediaPlayerDevice): self._name = name self._zone = receiver.zone - @property - def unique_id(self): - """Return an unique ID.""" - return '{0}:{1}'.format(self.receiver.ctrl_url, self._zone) - def update(self): """Get the latest details from the device.""" self._play_status = self.receiver.play_status() diff --git a/homeassistant/components/sensor/blink.py b/homeassistant/components/sensor/blink.py index 44557978117..db7ab7c2e9e 100644 --- a/homeassistant/components/sensor/blink.py +++ b/homeassistant/components/sensor/blink.py @@ -61,11 +61,6 @@ class BlinkSensor(Entity): """Return the camera's current state.""" return self._state - @property - def unique_id(self): - """Return the unique camera sensor identifier.""" - return "sensor_{}_{}".format(self._name, self.index) - @property def unit_of_measurement(self): """Return the unit of measurement.""" diff --git a/homeassistant/components/sensor/bloomsky.py b/homeassistant/components/sensor/bloomsky.py index 660cb5ede6e..ce44abdb087 100644 --- a/homeassistant/components/sensor/bloomsky.py +++ b/homeassistant/components/sensor/bloomsky.py @@ -64,7 +64,6 @@ class BloomSkySensor(Entity): self._device_id = device['DeviceID'] self._sensor_name = sensor_name self._name = '{} {}'.format(device['DeviceName'], sensor_name) - self._unique_id = 'bloomsky_sensor {}'.format(self._name) self._state = None @property @@ -72,11 +71,6 @@ class BloomSkySensor(Entity): """Return the name of the BloomSky device and this sensor.""" return self._name - @property - def unique_id(self): - """Return the unique ID for this sensor.""" - return self._unique_id - @property def state(self): """Return the current state, eg. value, of this sensor.""" diff --git a/homeassistant/components/sensor/canary.py b/homeassistant/components/sensor/canary.py index 56da1c4deea..ded8f36203e 100644 --- a/homeassistant/components/sensor/canary.py +++ b/homeassistant/components/sensor/canary.py @@ -70,8 +70,7 @@ class CanarySensor(Entity): @property def unique_id(self): """Return the unique ID of this sensor.""" - return "sensor_canary_{}_{}".format(self._device_id, - self._sensor_type[0]) + return "{}_{}".format(self._device_id, self._sensor_type[0]) @property def unit_of_measurement(self): diff --git a/homeassistant/components/sensor/daikin.py b/homeassistant/components/sensor/daikin.py index 3ea3418db4e..0b2f6495b45 100644 --- a/homeassistant/components/sensor/daikin.py +++ b/homeassistant/components/sensor/daikin.py @@ -95,11 +95,6 @@ class DaikinClimateSensor(Entity): return value - @property - def unique_id(self): - """Return the ID of this AC.""" - return "{}.{}".format(self.__class__, self._api.ip_address) - @property def icon(self): """Icon to use in the frontend, if any.""" diff --git a/homeassistant/components/sensor/ecobee.py b/homeassistant/components/sensor/ecobee.py index a0c6f7a92e4..dad770d5bab 100644 --- a/homeassistant/components/sensor/ecobee.py +++ b/homeassistant/components/sensor/ecobee.py @@ -50,18 +50,13 @@ class EcobeeSensor(Entity): @property def name(self): """Return the name of the Ecobee sensor.""" - return self._name.rstrip() + return self._name @property def state(self): """Return the state of the sensor.""" return self._state - @property - def unique_id(self): - """Return the unique ID of this sensor.""" - return "sensor_ecobee_{}_{}".format(self._name, self.index) - @property def unit_of_measurement(self): """Return the unit of measurement this sensor expresses itself in.""" diff --git a/homeassistant/components/sensor/ios.py b/homeassistant/components/sensor/ios.py index 9a23da48a6b..398c0b350ee 100644 --- a/homeassistant/components/sensor/ios.py +++ b/homeassistant/components/sensor/ios.py @@ -58,7 +58,7 @@ class IOSSensor(Entity): def unique_id(self): """Return the unique ID of this sensor.""" device_id = self._device[ios.ATTR_DEVICE_ID] - return "sensor_ios_battery_{}_{}".format(self.type, device_id) + return "{}_{}".format(self.type, device_id) @property def unit_of_measurement(self): diff --git a/homeassistant/components/sensor/isy994.py b/homeassistant/components/sensor/isy994.py index 76f026bba10..39c9d8a3b9d 100644 --- a/homeassistant/components/sensor/isy994.py +++ b/homeassistant/components/sensor/isy994.py @@ -317,11 +317,6 @@ class ISYWeatherDevice(ISYDevice): """Initialize the ISY994 weather device.""" super().__init__(node) - @property - def unique_id(self) -> str: - """Return the unique identifier for the node.""" - return self._node.name - @property def raw_units(self) -> str: """Return the raw unit of measurement.""" diff --git a/homeassistant/components/sensor/netatmo.py b/homeassistant/components/sensor/netatmo.py index 8ace931a8cc..c20e0a59408 100644 --- a/homeassistant/components/sensor/netatmo.py +++ b/homeassistant/components/sensor/netatmo.py @@ -113,8 +113,7 @@ class NetAtmoSensor(Entity): module_id = self.netatmo_data.\ station_data.moduleByName(module=module_name)['_id'] self.module_id = module_id[1] - self._unique_id = "Netatmo Sensor {0} - {1} ({2})".format( - self._name, module_id, self.type) + self._unique_id = '{}-{}'.format(self.module_id, self.type) @property def name(self): diff --git a/homeassistant/components/switch/insteon_local.py b/homeassistant/components/switch/insteon_local.py index c20a638c00f..4456436ea61 100644 --- a/homeassistant/components/switch/insteon_local.py +++ b/homeassistant/components/switch/insteon_local.py @@ -54,7 +54,7 @@ class InsteonLocalSwitchDevice(SwitchDevice): @property def unique_id(self): """Return the ID of this Insteon node.""" - return 'insteon_local_{}'.format(self.node.device_id) + return self.node.device_id @util.Throttle(MIN_TIME_BETWEEN_SCANS, MIN_TIME_BETWEEN_FORCED_SCANS) def update(self): diff --git a/homeassistant/components/switch/rainmachine.py b/homeassistant/components/switch/rainmachine.py index 9425b61f0e5..3147ded96bd 100644 --- a/homeassistant/components/switch/rainmachine.py +++ b/homeassistant/components/switch/rainmachine.py @@ -180,8 +180,7 @@ class RainMachineEntity(SwitchDevice): @property def unique_id(self) -> str: """Return a unique, HASS-friendly identifier for this entity.""" - return '{}.{}.{}'.format(self.__class__, self._device_name, - self.rainmachine_id) + return self.rainmachine_id @aware_throttle('local') def _local_update(self) -> None: diff --git a/homeassistant/components/switch/wemo.py b/homeassistant/components/switch/wemo.py index 7b97ece337b..4339c92bb60 100644 --- a/homeassistant/components/switch/wemo.py +++ b/homeassistant/components/switch/wemo.py @@ -81,7 +81,7 @@ class WemoSwitch(SwitchDevice): @property def unique_id(self): """Return the ID of this WeMo switch.""" - return "{}.{}".format(self.__class__, self.wemo.serialnumber) + return self.wemo.serialnumber @property def name(self): diff --git a/homeassistant/components/tahoma.py b/homeassistant/components/tahoma.py index 4488b4e836b..0db055f7d92 100644 --- a/homeassistant/components/tahoma.py +++ b/homeassistant/components/tahoma.py @@ -13,7 +13,6 @@ from homeassistant.const import CONF_USERNAME, CONF_PASSWORD, CONF_EXCLUDE from homeassistant.helpers import discovery from homeassistant.helpers import config_validation as cv from homeassistant.helpers.entity import Entity -from homeassistant.util import (slugify) REQUIREMENTS = ['tahoma-api==0.0.10'] @@ -101,15 +100,8 @@ class TahomaDevice(Entity): """Initialize the device.""" self.tahoma_device = tahoma_device self.controller = controller - self._unique_id = TAHOMA_ID_FORMAT.format( - slugify(tahoma_device.label), slugify(tahoma_device.url)) self._name = self.tahoma_device.label - @property - def unique_id(self): - """Return the unique ID for this cover.""" - return self._unique_id - @property def name(self): """Return the name of the device.""" diff --git a/homeassistant/components/zwave/__init__.py b/homeassistant/components/zwave/__init__.py index abfd353e1f4..10942de8097 100644 --- a/homeassistant/components/zwave/__init__.py +++ b/homeassistant/components/zwave/__init__.py @@ -865,8 +865,8 @@ class ZWaveDeviceEntity(ZWaveBaseEntity): self.values.primary.set_change_verified(False) self._name = _value_name(self.values.primary) - self._unique_id = "ZWAVE-{}-{}".format(self.node.node_id, - self.values.primary.object_id) + self._unique_id = "{}-{}".format(self.node.node_id, + self.values.primary.object_id) self._update_attributes() dispatcher.connect( diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index f9570ac5858..d1e5c0d82a0 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -91,7 +91,7 @@ class Entity(object): @property def unique_id(self) -> str: """Return an unique ID.""" - return "{}.{}".format(self.__class__, id(self)) + return None @property def name(self) -> Optional[str]: @@ -338,8 +338,22 @@ class Entity(object): def __eq__(self, other): """Return the comparison.""" - return (isinstance(other, Entity) and - other.unique_id == self.unique_id) + if not isinstance(other, self.__class__): + return False + + # Can only decide equality if both have a unique id + if self.unique_id is None or other.unique_id is None: + return False + + # Ensure they belong to the same platform + if self.platform is not None or other.platform is not None: + if self.platform is None or other.platform is None: + return False + + if self.platform.platform != other.platform.platform: + return False + + return self.unique_id == other.unique_id def __repr__(self): """Return the representation.""" diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 4a791d12e52..2c928f184e8 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -8,10 +8,9 @@ from homeassistant.setup import async_prepare_setup_platform from homeassistant.const import ( ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE, DEVICE_DEFAULT_NAME) -from homeassistant.core import callback, valid_entity_id +from homeassistant.core import callback, valid_entity_id, split_entity_id from homeassistant.exceptions import HomeAssistantError, PlatformNotReady from homeassistant.helpers import config_per_platform, discovery -from homeassistant.helpers.entity import async_generate_entity_id from homeassistant.helpers.event import ( async_track_time_interval, async_track_point_in_time) from homeassistant.helpers.service import extract_entity_ids @@ -19,11 +18,13 @@ from homeassistant.util import slugify from homeassistant.util.async import ( run_callback_threadsafe, run_coroutine_threadsafe) import homeassistant.util.dt as dt_util +from .entity_registry import EntityRegistry DEFAULT_SCAN_INTERVAL = timedelta(seconds=15) SLOW_SETUP_WARNING = 10 SLOW_SETUP_MAX_WAIT = 60 PLATFORM_NOT_READY_RETRIES = 10 +DATA_REGISTRY = 'entity_registry' class EntityComponent(object): @@ -357,12 +358,20 @@ class EntityPlatform(object): if not new_entities: return + hass = self.component.hass component_entities = set(entity.entity_id for entity in self.component.entities) + registry = hass.data.get(DATA_REGISTRY) + + if registry is None: + registry = hass.data[DATA_REGISTRY] = EntityRegistry(hass) + + yield from registry.async_ensure_loaded() + tasks = [ self._async_add_entity(entity, update_before_add, - component_entities) + component_entities, registry) for entity in new_entities] yield from asyncio.wait(tasks, loop=self.component.hass.loop) @@ -378,15 +387,12 @@ class EntityPlatform(object): ) @asyncio.coroutine - def _async_add_entity(self, entity, update_before_add, component_entities): + def _async_add_entity(self, entity, update_before_add, component_entities, + registry): """Helper method to add an entity to the platform.""" if entity is None: raise ValueError('Entity cannot be None') - # Do nothing if entity has already been added based on unique id. - if entity in self.component.entities: - return - entity.hass = self.component.hass entity.platform = self entity.parallel_updates = self.parallel_updates @@ -400,17 +406,39 @@ class EntityPlatform(object): "%s: Error on device update!", self.platform) return - # Write entity_id to entity - if getattr(entity, 'entity_id', None) is None: - object_id = entity.name or DEVICE_DEFAULT_NAME + suggested_object_id = None + + # Get entity_id from unique ID registration + if entity.unique_id is not None: + if entity.entity_id is not None: + suggested_object_id = split_entity_id(entity.entity_id)[1] + else: + suggested_object_id = entity.name + + entry = registry.async_get_or_create( + self.component.domain, self.platform, entity.unique_id, + suggested_object_id=suggested_object_id) + entity.entity_id = entry.entity_id + + # We won't generate an entity ID if the platform has already set one + # We will however make sure that platform cannot pick a registered ID + elif (entity.entity_id is not None and + registry.async_is_registered(entity.entity_id)): + # If entity already registered, convert entity id to suggestion + suggested_object_id = split_entity_id(entity.entity_id)[1] + entity.entity_id = None + + # Generate entity ID + if entity.entity_id is None: + suggested_object_id = \ + suggested_object_id or entity.name or DEVICE_DEFAULT_NAME if self.entity_namespace is not None: - object_id = '{} {}'.format(self.entity_namespace, - object_id) + suggested_object_id = '{} {}'.format(self.entity_namespace, + suggested_object_id) - entity.entity_id = async_generate_entity_id( - self.component.entity_id_format, object_id, - component_entities) + entity.entity_id = registry.async_generate_entity_id( + self.component.domain, suggested_object_id) # Make sure it is valid in case an entity set the value themselves if not valid_entity_id(entity.entity_id): diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py new file mode 100644 index 00000000000..350c8273232 --- /dev/null +++ b/homeassistant/helpers/entity_registry.py @@ -0,0 +1,134 @@ +"""Provide a registry to track entity IDs. + +The Entity Registry keeps a registry of entities. Entities are uniquely +identified by their domain, platform and a unique id provided by that platform. + +The Entity Registry will persist itself 10 seconds after a new entity is +registered. Registering a new entity while a timer is in progress resets the +timer. + +After initializing, call EntityRegistry.async_ensure_loaded to load the data +from disk. +""" +import asyncio +from collections import namedtuple, OrderedDict +from itertools import chain +import logging +import os + +from ..core import callback, split_entity_id +from ..util import ensure_unique_string, slugify +from ..util.yaml import load_yaml, save_yaml + +PATH_REGISTRY = 'entity_registry.yaml' +SAVE_DELAY = 10 +Entry = namedtuple('EntityRegistryEntry', + 'entity_id,unique_id,platform,domain') +_LOGGER = logging.getLogger(__name__) + + +class EntityRegistry: + """Class to hold a registry of entities.""" + + def __init__(self, hass): + """Initialize the registry.""" + self.hass = hass + self.entities = None + self._load_task = None + self._sched_save = None + + @callback + def async_is_registered(self, entity_id): + """Check if an entity_id is currently registered.""" + return entity_id in self.entities + + @callback + def async_generate_entity_id(self, domain, suggested_object_id): + """Generate an entity ID that does not conflict. + + Conflicts checked against registered and currently existing entities. + """ + return ensure_unique_string( + '{}.{}'.format(domain, slugify(suggested_object_id)), + chain(self.entities.keys(), + self.hass.states.async_entity_ids(domain)) + ) + + @callback + def async_get_or_create(self, domain, platform, unique_id, *, + suggested_object_id=None): + """Get entity. Create if it doesn't exist.""" + for entity in self.entities.values(): + if entity.domain == domain and entity.platform == platform and \ + entity.unique_id == unique_id: + return entity + + entity_id = self.async_generate_entity_id( + domain, suggested_object_id or '{}_{}'.format(platform, unique_id)) + entity = Entry( + entity_id=entity_id, + unique_id=unique_id, + platform=platform, + domain=domain, + ) + self.entities[entity_id] = entity + _LOGGER.info('Registered new %s.%s entity: %s', + domain, platform, entity_id) + self.async_schedule_save() + return entity + + @asyncio.coroutine + def async_ensure_loaded(self): + """Load the registry from disk.""" + if self.entities is not None: + return + + if self._load_task is None: + self._load_task = self.hass.async_add_job(self._async_load) + + yield from self._load_task + + @asyncio.coroutine + def _async_load(self): + """Load the entity registry.""" + path = self.hass.config.path(PATH_REGISTRY) + entities = OrderedDict() + + if os.path.isfile(path): + data = yield from self.hass.async_add_job(load_yaml, path) + + for entity_id, info in data.items(): + entities[entity_id] = Entry( + domain=split_entity_id(entity_id)[0], + entity_id=entity_id, + unique_id=info['unique_id'], + platform=info['platform'] + ) + + self.entities = entities + self._load_task = None + + @callback + def async_schedule_save(self): + """Schedule saving the entity registry.""" + if self._sched_save is not None: + self._sched_save.cancel() + + self._sched_save = self.hass.loop.call_later( + SAVE_DELAY, self.hass.async_add_job, self._async_save + ) + + @asyncio.coroutine + def _async_save(self): + """Save the entity registry to a file.""" + self._sched_save = None + data = OrderedDict() + + for entry in self.entities.values(): + data[entry.entity_id] = { + 'unique_id': entry.unique_id, + 'platform': entry.platform, + } + + yield from self.hass.async_add_job( + save_yaml, self.hass.config.path(PATH_REGISTRY), data) diff --git a/homeassistant/util/yaml.py b/homeassistant/util/yaml.py index 48d709bc549..d0d5199e0f4 100644 --- a/homeassistant/util/yaml.py +++ b/homeassistant/util/yaml.py @@ -83,6 +83,14 @@ def dump(_dict: dict) -> str: .replace(': null\n', ':\n') +def save_yaml(path, data): + """Save YAML to a file.""" + # Dump before writing to not truncate the file if dumping fails + data = dump(data) + with open(path, 'w', encoding='utf-8') as outfile: + outfile.write(data) + + def clear_secret_cache() -> None: """Clear the secret cache. diff --git a/tests/common.py b/tests/common.py index 3823a1e2b4e..ed4439c1c49 100644 --- a/tests/common.py +++ b/tests/common.py @@ -22,6 +22,7 @@ from homeassistant.const import ( STATE_ON, STATE_OFF, DEVICE_DEFAULT_NAME, EVENT_TIME_CHANGED, EVENT_STATE_CHANGED, EVENT_PLATFORM_DISCOVERED, ATTR_SERVICE, ATTR_DISCOVERED, SERVER_PORT, EVENT_HOMEASSISTANT_CLOSE) +from homeassistant.helpers import entity_component, entity_registry from homeassistant.components import mqtt, recorder from homeassistant.components.http.auth import auth_middleware from homeassistant.components.http.const import ( @@ -315,6 +316,14 @@ def mock_component(hass, component): hass.config.components.add(component) +def mock_registry(hass): + """Mock the Entity Registry.""" + registry = entity_registry.EntityRegistry(hass) + registry.entities = {} + hass.data[entity_component.DATA_REGISTRY] = registry + return registry + + class MockModule(object): """Representation of a fake module.""" diff --git a/tests/components/camera/test_local_file.py b/tests/components/camera/test_local_file.py index 812dd399a48..42ce7bd7add 100644 --- a/tests/components/camera/test_local_file.py +++ b/tests/components/camera/test_local_file.py @@ -8,10 +8,14 @@ from mock_open import MockOpen from homeassistant.setup import async_setup_component +from tests.common import mock_registry + @asyncio.coroutine def test_loading_file(hass, test_client): """Test that it loads image from disk.""" + mock_registry(hass) + with mock.patch('os.path.isfile', mock.Mock(return_value=True)), \ mock.patch('os.access', mock.Mock(return_value=True)): yield from async_setup_component(hass, 'camera', { diff --git a/tests/components/light/test_hue.py b/tests/components/light/test_hue.py index 659bc4abe16..e1d1cdaadec 100644 --- a/tests/components/light/test_hue.py +++ b/tests/components/light/test_hue.py @@ -282,29 +282,11 @@ class TestSetup(unittest.TestCase): self.assertEqual(len(args), 1) self.assertEqual(len(kwargs), 0) - # one argument, a list of lights in bridge one; each of them is an - # object of type HueLight so we can't straight up compare them - lights = args[0] - self.assertEqual( - lights[0].unique_id, - '{}.b1l1.Light.1'.format(hue_light.HueLight)) - self.assertEqual( - lights[1].unique_id, - '{}.b1l2.Light.2'.format(hue_light.HueLight)) - # second call works the same name, args, kwargs = self.mock_add_devices.mock_calls[1] self.assertEqual(len(args), 1) self.assertEqual(len(kwargs), 0) - lights = args[0] - self.assertEqual( - lights[0].unique_id, - '{}.b2l1.Light.1'.format(hue_light.HueLight)) - self.assertEqual( - lights[1].unique_id, - '{}.b2l3.Light.3'.format(hue_light.HueLight)) - def test_process_lights_api_error(self): """Test the process_lights function when the bridge errors out.""" self.setup_mocks_for_process_lights() @@ -506,60 +488,16 @@ class TestHueLight(unittest.TestCase): def test_unique_id_for_light(self): """Test the unique_id method with lights.""" - class_name = "" - light = self.buildLight(info={'uniqueid': 'foobar'}) - self.assertEqual( - class_name+'.foobar', - light.unique_id) + self.assertEqual('foobar', light.unique_id) light = self.buildLight(info={}) - self.assertEqual( - class_name+'.Unnamed Device.Light.42', - light.unique_id) - - light = self.buildLight(info={'name': 'my-name'}) - self.assertEqual( - class_name+'.my-name.Light.42', - light.unique_id) - - light = self.buildLight(info={'type': 'my-type'}) - self.assertEqual( - class_name+'.Unnamed Device.my-type.42', - light.unique_id) - - light = self.buildLight(info={'name': 'a name', 'type': 'my-type'}) - self.assertEqual( - class_name+'.a name.my-type.42', - light.unique_id) + self.assertIsNone(light.unique_id) def test_unique_id_for_group(self): """Test the unique_id method with groups.""" - class_name = "" - light = self.buildLight(info={'uniqueid': 'foobar'}, is_group=True) - self.assertEqual( - class_name+'.foobar', - light.unique_id) + self.assertEqual('foobar', light.unique_id) light = self.buildLight(info={}, is_group=True) - self.assertEqual( - class_name+'.Unnamed Device.Group.42', - light.unique_id) - - light = self.buildLight(info={'name': 'my-name'}, is_group=True) - self.assertEqual( - class_name+'.my-name.Group.42', - light.unique_id) - - light = self.buildLight(info={'type': 'my-type'}, is_group=True) - self.assertEqual( - class_name+'.Unnamed Device.my-type.42', - light.unique_id) - - light = self.buildLight( - info={'name': 'a name', 'type': 'my-type'}, - is_group=True) - self.assertEqual( - class_name+'.a name.my-type.42', - light.unique_id) + self.assertIsNone(light.unique_id) diff --git a/tests/components/sensor/test_file.py b/tests/components/sensor/test_file.py index 00e8f2ba525..aa048f7a62e 100644 --- a/tests/components/sensor/test_file.py +++ b/tests/components/sensor/test_file.py @@ -9,7 +9,7 @@ from mock_open import MockOpen from homeassistant.setup import setup_component from homeassistant.const import STATE_UNKNOWN -from tests.common import get_test_home_assistant +from tests.common import get_test_home_assistant, mock_registry class TestFileSensor(unittest.TestCase): @@ -18,6 +18,7 @@ class TestFileSensor(unittest.TestCase): def setup_method(self, method): """Set up things to be run when tests are started.""" self.hass = get_test_home_assistant() + mock_registry(self.hass) def teardown_method(self, method): """Stop everything that was started.""" diff --git a/tests/components/zwave/test_init.py b/tests/components/zwave/test_init.py index 77de7d5d1dd..828385b9ded 100644 --- a/tests/components/zwave/test_init.py +++ b/tests/components/zwave/test_init.py @@ -198,7 +198,7 @@ def test_device_entity(hass, mock_openzwave): yield from hass.async_block_till_done() assert not device.should_poll - assert device.unique_id == "ZWAVE-10-11" + assert device.unique_id == "10-11" assert device.name == 'Mock Node Sensor' assert device.device_state_attributes[zwave.ATTR_POWER] == 50.123 diff --git a/tests/helpers/test_entity_component.py b/tests/helpers/test_entity_component.py index f2416fc3a31..349766d025e 100644 --- a/tests/helpers/test_entity_component.py +++ b/tests/helpers/test_entity_component.py @@ -22,7 +22,7 @@ import homeassistant.util.dt as dt_util from tests.common import ( get_test_home_assistant, MockPlatform, MockModule, fire_time_changed, - mock_coro, async_fire_time_changed) + mock_coro, async_fire_time_changed, mock_registry) _LOGGER = logging.getLogger(__name__) DOMAIN = "test_domain" @@ -210,30 +210,6 @@ class TestHelpersEntityComponent(unittest.TestCase): assert 1 == len(self.hass.states.entity_ids()) assert not ent.update.called - def test_not_adding_duplicate_entities(self): - """Test for not adding duplicate entities.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - assert 0 == len(self.hass.states.entity_ids()) - - component.add_entities([EntityTest(unique_id='not_very_unique')]) - - assert 1 == len(self.hass.states.entity_ids()) - - component.add_entities([EntityTest(unique_id='not_very_unique')]) - - assert 1 == len(self.hass.states.entity_ids()) - - def test_not_assigning_entity_id_if_prescribes_one(self): - """Test for not assigning an entity ID.""" - component = EntityComponent(_LOGGER, DOMAIN, self.hass) - - assert 'hello.world' not in self.hass.states.entity_ids() - - component.add_entities([EntityTest(entity_id='hello.world')]) - - assert 'hello.world' in self.hass.states.entity_ids() - def test_extract_from_service_returns_all_if_no_entity_id(self): """Test the extraction of everything from service.""" component = EntityComponent(_LOGGER, DOMAIN, self.hass) @@ -684,3 +660,83 @@ def test_async_remove_with_platform(hass): assert len(hass.states.async_entity_ids()) == 1 yield from entity1.async_remove() assert len(hass.states.async_entity_ids()) == 0 + + +@asyncio.coroutine +def test_not_adding_duplicate_entities_with_unique_id(hass): + """Test for not adding duplicate entities.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + yield from component.async_add_entities([ + EntityTest(name='test1', unique_id='not_very_unique')]) + + assert len(hass.states.async_entity_ids()) == 1 + + yield from component.async_add_entities([ + EntityTest(name='test2', unique_id='not_very_unique')]) + + assert len(hass.states.async_entity_ids()) == 1 + + +@asyncio.coroutine +def test_using_prescribed_entity_id(hass): + """Test for using predefined entity ID.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + yield from component.async_add_entities([ + EntityTest(name='bla', entity_id='hello.world')]) + assert 'hello.world' in hass.states.async_entity_ids() + + +@asyncio.coroutine +def test_using_prescribed_entity_id_with_unique_id(hass): + """Test for ammending predefined entity ID because currently exists.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + + yield from component.async_add_entities([ + EntityTest(entity_id='test_domain.world')]) + yield from component.async_add_entities([ + EntityTest(entity_id='test_domain.world', unique_id='bla')]) + + assert 'test_domain.world_2' in hass.states.async_entity_ids() + + +@asyncio.coroutine +def test_using_prescribed_entity_id_which_is_registered(hass): + """Test not allowing predefined entity ID that already registered.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + registry = mock_registry(hass) + # Register test_domain.world + registry.async_get_or_create( + DOMAIN, 'test', '1234', suggested_object_id='world') + + # This entity_id will be rewritten + yield from component.async_add_entities([ + EntityTest(entity_id='test_domain.world')]) + + assert 'test_domain.world_2' in hass.states.async_entity_ids() + + +@asyncio.coroutine +def test_name_which_conflict_with_registered(hass): + """Test not generating conflicting entity ID based on name.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + registry = mock_registry(hass) + + # Register test_domain.world + registry.async_get_or_create( + DOMAIN, 'test', '1234', suggested_object_id='world') + + yield from component.async_add_entities([ + EntityTest(name='world')]) + + assert 'test_domain.world_2' in hass.states.async_entity_ids() + + +@asyncio.coroutine +def test_entity_with_name_and_entity_id_getting_registered(hass): + """Ensure that entity ID is used for registration.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + yield from component.async_add_entities([ + EntityTest(unique_id='1234', name='bla', + entity_id='test_domain.world')]) + assert 'test_domain.world' in hass.states.async_entity_ids() diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py new file mode 100644 index 00000000000..d19a3f3fe49 --- /dev/null +++ b/tests/helpers/test_entity_registry.py @@ -0,0 +1,135 @@ +"""Tests for the Entity Registry.""" +import asyncio +from unittest.mock import patch, mock_open + +import pytest + +from homeassistant.helpers import entity_registry + +from tests.common import mock_registry + + +@pytest.fixture +def registry(hass): + """Return an empty, loaded, registry.""" + return mock_registry(hass) + + +@asyncio.coroutine +def test_get_or_create_returns_same_entry(registry): + """Make sure we do not duplicate entries.""" + entry = registry.async_get_or_create('light', 'hue', '1234') + entry2 = registry.async_get_or_create('light', 'hue', '1234') + + assert len(registry.entities) == 1 + assert entry is entry2 + assert entry.entity_id == 'light.hue_1234' + + +@asyncio.coroutine +def test_get_or_create_suggested_object_id(registry): + """Test that suggested_object_id works.""" + entry = registry.async_get_or_create( + 'light', 'hue', '1234', suggested_object_id='beer') + + assert entry.entity_id == 'light.beer' + + +@asyncio.coroutine +def test_get_or_create_suggested_object_id_conflict_register(registry): + """Test that we don't generate an entity id that is already registered.""" + entry = registry.async_get_or_create( + 'light', 'hue', '1234', suggested_object_id='beer') + entry2 = registry.async_get_or_create( + 'light', 'hue', '5678', suggested_object_id='beer') + + assert entry.entity_id == 'light.beer' + assert entry2.entity_id == 'light.beer_2' + + +@asyncio.coroutine +def test_get_or_create_suggested_object_id_conflict_existing(hass, registry): + """Test that we don't generate an entity id that currently exists.""" + hass.states.async_set('light.hue_1234', 'on') + entry = registry.async_get_or_create('light', 'hue', '1234') + assert entry.entity_id == 'light.hue_1234_2' + + +@asyncio.coroutine +def test_create_triggers_save(hass, registry): + """Test that registering entry triggers a save.""" + with patch.object(hass.loop, 'call_later') as mock_call_later: + registry.async_get_or_create('light', 'hue', '1234') + + assert len(mock_call_later.mock_calls) == 1 + + +@asyncio.coroutine +def test_save_timer_reset_on_subsequent_save(hass, registry): + """Test we reset the save timer on a new create.""" + with patch.object(hass.loop, 'call_later') as mock_call_later: + registry.async_get_or_create('light', 'hue', '1234') + + assert len(mock_call_later.mock_calls) == 1 + + with patch.object(hass.loop, 'call_later') as mock_call_later_2: + registry.async_get_or_create('light', 'hue', '5678') + + assert len(mock_call_later().cancel.mock_calls) == 1 + assert len(mock_call_later_2.mock_calls) == 1 + + +@asyncio.coroutine +def test_loading_saving_data(hass, registry): + """Test that we load/save data correctly.""" + yaml_path = 'homeassistant.util.yaml.open' + orig_entry1 = registry.async_get_or_create('light', 'hue', '1234') + orig_entry2 = registry.async_get_or_create('light', 'hue', '5678') + + assert len(registry.entities) == 2 + + with patch(yaml_path, mock_open(), create=True) as mock_write: + yield from registry._async_save() + + # Mock open calls are: open file, context enter, write, context leave + written = mock_write.mock_calls[2][1][0] + + # Now load written data in new registry + registry2 = entity_registry.EntityRegistry(hass) + + with patch('os.path.isfile', return_value=True), \ + patch(yaml_path, mock_open(read_data=written), create=True): + yield from registry2._async_load() + + # Ensure same order + assert list(registry.entities) == list(registry2.entities) + new_entry1 = registry.async_get_or_create('light', 'hue', '1234') + new_entry2 = registry.async_get_or_create('light', 'hue', '5678') + + assert orig_entry1 == new_entry1 + assert orig_entry2 == new_entry2 + + +@asyncio.coroutine +def test_generate_entity_considers_registered_entities(registry): + """Test that we don't create entity id that are already registered.""" + entry = registry.async_get_or_create('light', 'hue', '1234') + assert entry.entity_id == 'light.hue_1234' + assert registry.async_generate_entity_id('light', 'hue_1234') == \ + 'light.hue_1234_2' + + +@asyncio.coroutine +def test_generate_entity_considers_existing_entities(hass, registry): + """Test that we don't create entity id that currently exists.""" + hass.states.async_set('light.kitchen', 'on') + assert registry.async_generate_entity_id('light', 'kitchen') == \ + 'light.kitchen_2' + + +@asyncio.coroutine +def test_is_registered(registry): + """Test that is_registered works.""" + entry = registry.async_get_or_create('light', 'hue', '1234') + assert registry.async_is_registered(entry.entity_id) + assert not registry.async_is_registered('light.non_existing')