Entity registry (#11979)
* 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…pull/12057/head
parent
8e441ba03b
commit
e51427b284
|
@ -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."""
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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:
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -865,7 +865,7 @@ 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._unique_id = "{}-{}".format(self.node.node_id,
|
||||
self.values.primary.object_id)
|
||||
self._update_attributes()
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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):
|
||||
|
|
|
@ -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)
|
|
@ -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.
|
||||
|
||||
|
|
|
@ -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."""
|
||||
|
||||
|
|
|
@ -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', {
|
||||
|
|
|
@ -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 = "<class 'homeassistant.components.light.hue.HueLight'>"
|
||||
|
||||
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 = "<class 'homeassistant.components.light.hue.HueLight'>"
|
||||
|
||||
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)
|
||||
|
|
|
@ -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."""
|
||||
|
|
|
@ -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
|
||||
|
||||
|
|
|
@ -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()
|
||||
|
|
|
@ -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')
|
Loading…
Reference in New Issue