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…
This commit is contained in:
Paulus Schoutsen 2018-01-30 01:39:39 -08:00 committed by Pascal Vizeli
parent 8e441ba03b
commit e51427b284
47 changed files with 471 additions and 230 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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