diff --git a/homeassistant/components/remote/xiaomi_miio.py b/homeassistant/components/remote/xiaomi_miio.py index aa05246c9cd729..32fde57b61a49e 100644 --- a/homeassistant/components/remote/xiaomi_miio.py +++ b/homeassistant/components/remote/xiaomi_miio.py @@ -226,7 +226,6 @@ def async_turn_off(self, **kwargs): _LOGGER.error("Device does not support turn_off, " + "please use 'remote.send_command' to send commands.") - # pylint: enable=R0201 def _send_command(self, payload): """Send a command.""" from miio import DeviceException diff --git a/homeassistant/helpers/entity.py b/homeassistant/helpers/entity.py index c7653d5d5b955a..6b882d2fdad09d 100644 --- a/homeassistant/helpers/entity.py +++ b/homeassistant/helpers/entity.py @@ -80,6 +80,9 @@ class Entity(object): # Process updates in parallel parallel_updates = None + # Name in the entity registry + registry_name = None + @property def should_poll(self) -> bool: """Return True if entity has to be polled for state. @@ -225,7 +228,7 @@ def async_update_ha_state(self, force_refresh=False): if unit_of_measurement is not None: attr[ATTR_UNIT_OF_MEASUREMENT] = unit_of_measurement - name = self.name + name = self.registry_name or self.name if name is not None: attr[ATTR_FRIENDLY_NAME] = name diff --git a/homeassistant/helpers/entity_component.py b/homeassistant/helpers/entity_component.py index 9dfbe580c16b2a..2dcde6fdeda5cd 100644 --- a/homeassistant/helpers/entity_component.py +++ b/homeassistant/helpers/entity_component.py @@ -40,19 +40,19 @@ def __init__(self, logger, domain, hass, self.config = None self._platforms = { - 'core': EntityPlatform( + domain: EntityPlatform( hass=hass, logger=logger, domain=domain, - platform_name='core', + platform_name=domain, scan_interval=self.scan_interval, parallel_updates=0, entity_namespace=None, async_entities_added_callback=self._async_update_group, ) } - self.async_add_entities = self._platforms['core'].async_add_entities - self.add_entities = self._platforms['core'].add_entities + self.async_add_entities = self._platforms[domain].async_add_entities + self.add_entities = self._platforms[domain].add_entities @property def entities(self): @@ -190,7 +190,7 @@ def _async_reset(self): yield from asyncio.wait(tasks, loop=self.hass.loop) self._platforms = { - 'core': self._platforms['core'] + self.domain: self._platforms[self.domain] } self.config = None diff --git a/homeassistant/helpers/entity_platform.py b/homeassistant/helpers/entity_platform.py index 014ec3c500911b..9035973e015d1f 100644 --- a/homeassistant/helpers/entity_platform.py +++ b/homeassistant/helpers/entity_platform.py @@ -213,6 +213,7 @@ def _async_add_entity(self, entity, update_before_add, component_entities, self.domain, self.platform_name, entity.unique_id, suggested_object_id=suggested_object_id) entity.entity_id = entry.entity_id + entity.registry_name = entry.name # 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 diff --git a/homeassistant/helpers/entity_registry.py b/homeassistant/helpers/entity_registry.py index 350c8273232afd..d33ca93f290e25 100644 --- a/homeassistant/helpers/entity_registry.py +++ b/homeassistant/helpers/entity_registry.py @@ -11,22 +11,37 @@ from disk. """ import asyncio -from collections import namedtuple, OrderedDict +from collections import OrderedDict from itertools import chain import logging import os +import attr + 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__) +@attr.s(slots=True, frozen=True) +class RegistryEntry: + """Entity Registry Entry.""" + + entity_id = attr.ib(type=str) + unique_id = attr.ib(type=str) + platform = attr.ib(type=str) + name = attr.ib(type=str, default=None) + domain = attr.ib(type=str, default=None, init=False, repr=False) + + def __attrs_post_init__(self): + """Computed properties.""" + object.__setattr__(self, "domain", split_entity_id(self.entity_id)[0]) + + class EntityRegistry: """Class to hold a registry of entities.""" @@ -65,11 +80,10 @@ def async_get_or_create(self, domain, platform, unique_id, *, entity_id = self.async_generate_entity_id( domain, suggested_object_id or '{}_{}'.format(platform, unique_id)) - entity = Entry( + entity = RegistryEntry( 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', @@ -98,11 +112,11 @@ def _async_load(self): 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], + entities[entity_id] = RegistryEntry( entity_id=entity_id, unique_id=info['unique_id'], - platform=info['platform'] + platform=info['platform'], + name=info.get('name') ) self.entities = entities diff --git a/homeassistant/package_constraints.txt b/homeassistant/package_constraints.txt index ee3a37bbd53e42..7d182aebfa3dd6 100644 --- a/homeassistant/package_constraints.txt +++ b/homeassistant/package_constraints.txt @@ -11,6 +11,7 @@ async_timeout==2.0.0 chardet==3.0.4 astral==1.5 certifi>=2017.4.17 +attrs==17.4.0 # Breaks Python 3.6 and is not needed for our supported Pythons enum34==1000000000.0.0 diff --git a/requirements_all.txt b/requirements_all.txt index a6777657227588..2a5f68791e0c83 100644 --- a/requirements_all.txt +++ b/requirements_all.txt @@ -12,6 +12,7 @@ async_timeout==2.0.0 chardet==3.0.4 astral==1.5 certifi>=2017.4.17 +attrs==17.4.0 # homeassistant.components.nuimo_controller --only-binary=all https://github.com/getSenic/nuimo-linux-python/archive/29fc42987f74d8090d0e2382e8f248ff5990b8c9.zip#nuimo==1.0.0 diff --git a/setup.py b/setup.py index 5af84fc8e0e86b..0a454f9eb4d8b8 100755 --- a/setup.py +++ b/setup.py @@ -61,6 +61,7 @@ 'chardet==3.0.4', 'astral==1.5', 'certifi>=2017.4.17', + 'attrs==17.4.0', ] MIN_PY_VERSION = '.'.join(map( diff --git a/tests/common.py b/tests/common.py index 22af8ecb8a3b48..511d59dbdfeddd 100644 --- a/tests/common.py +++ b/tests/common.py @@ -317,10 +317,10 @@ def mock_component(hass, component): hass.config.components.add(component) -def mock_registry(hass): +def mock_registry(hass, mock_entries=None): """Mock the Entity Registry.""" registry = entity_registry.EntityRegistry(hass) - registry.entities = {} + registry.entities = mock_entries or {} hass.data[entity_platform.DATA_REGISTRY] = registry return registry diff --git a/tests/helpers/test_entity_platform.py b/tests/helpers/test_entity_platform.py index 4c27cc45a00103..e398349cf7a4e9 100644 --- a/tests/helpers/test_entity_platform.py +++ b/tests/helpers/test_entity_platform.py @@ -9,7 +9,7 @@ from homeassistant.helpers.entity import generate_entity_id from homeassistant.helpers.entity_component import ( EntityComponent, DEFAULT_SCAN_INTERVAL) -from homeassistant.helpers import entity_platform +from homeassistant.helpers import entity_platform, entity_registry import homeassistant.util.dt as dt_util @@ -433,3 +433,24 @@ def test_entity_with_name_and_entity_id_getting_registered(hass): MockEntity(unique_id='1234', name='bla', entity_id='test_domain.world')]) assert 'test_domain.world' in hass.states.async_entity_ids() + + +@asyncio.coroutine +def test_overriding_name_from_registry(hass): + """Test that we can override a name via the Entity Registry.""" + component = EntityComponent(_LOGGER, DOMAIN, hass) + mock_registry(hass, { + 'test_domain.world': entity_registry.RegistryEntry( + entity_id='test_domain.world', + unique_id='1234', + # Using component.async_add_entities is equal to platform "domain" + platform='test_domain', + name='Overridden' + ) + }) + yield from component.async_add_entities([ + MockEntity(unique_id='1234', name='Device Name')]) + + state = hass.states.get('test_domain.world') + assert state is not None + assert state.name == 'Overridden' diff --git a/tests/helpers/test_entity_registry.py b/tests/helpers/test_entity_registry.py index d19a3f3fe4978a..7e1150638c1ed1 100644 --- a/tests/helpers/test_entity_registry.py +++ b/tests/helpers/test_entity_registry.py @@ -9,6 +9,9 @@ from tests.common import mock_registry +YAML__OPEN_PATH = 'homeassistant.util.yaml.open' + + @pytest.fixture def registry(hass): """Return an empty, loaded, registry.""" @@ -82,13 +85,12 @@ def test_save_timer_reset_on_subsequent_save(hass, registry): @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: + with patch(YAML__OPEN_PATH, mock_open(), create=True) as mock_write: yield from registry._async_save() # Mock open calls are: open file, context enter, write, context leave @@ -98,7 +100,7 @@ def test_loading_saving_data(hass, registry): registry2 = entity_registry.EntityRegistry(hass) with patch('os.path.isfile', return_value=True), \ - patch(yaml_path, mock_open(read_data=written), create=True): + patch(YAML__OPEN_PATH, mock_open(read_data=written), create=True): yield from registry2._async_load() # Ensure same order @@ -133,3 +135,30 @@ def test_is_registered(registry): 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') + + +@asyncio.coroutine +def test_loading_extra_values(hass): + """Test we load extra data from the registry.""" + written = """ +test.named: + platform: super_platform + unique_id: with-name + name: registry override +test.no_name: + platform: super_platform + unique_id: without-name +""" + + registry = entity_registry.EntityRegistry(hass) + + with patch('os.path.isfile', return_value=True), \ + patch(YAML__OPEN_PATH, mock_open(read_data=written), create=True): + yield from registry._async_load() + + entry_with_name = registry.async_get_or_create( + 'test', 'super_platform', 'with-name') + entry_without_name = registry.async_get_or_create( + 'test', 'super_platform', 'without-name') + assert entry_with_name.name == 'registry override' + assert entry_without_name.name is None