2016-03-09 22:49:54 +00:00
|
|
|
"""Helpers for components that manage entities."""
|
2016-10-16 16:35:46 +00:00
|
|
|
import asyncio
|
2017-01-05 22:05:16 +00:00
|
|
|
from datetime import timedelta
|
2018-01-23 06:54:41 +00:00
|
|
|
from itertools import chain
|
2015-11-28 23:55:01 +00:00
|
|
|
|
2016-09-07 13:59:16 +00:00
|
|
|
from homeassistant import config as conf_util
|
2017-03-05 09:41:54 +00:00
|
|
|
from homeassistant.setup import async_prepare_setup_platform
|
2016-04-23 04:34:49 +00:00
|
|
|
from homeassistant.const import (
|
2018-02-08 11:16:51 +00:00
|
|
|
ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE)
|
|
|
|
from homeassistant.core import callback
|
|
|
|
from homeassistant.exceptions import HomeAssistantError
|
2016-06-12 00:43:13 +00:00
|
|
|
from homeassistant.helpers import config_per_platform, discovery
|
2016-01-24 06:57:14 +00:00
|
|
|
from homeassistant.helpers.service import extract_entity_ids
|
2017-06-15 22:52:28 +00:00
|
|
|
from homeassistant.util import slugify
|
2018-02-08 11:16:51 +00:00
|
|
|
from .entity_platform import EntityPlatform
|
2015-03-22 01:49:30 +00:00
|
|
|
|
2017-01-05 23:16:12 +00:00
|
|
|
DEFAULT_SCAN_INTERVAL = timedelta(seconds=15)
|
2015-03-22 01:49:30 +00:00
|
|
|
|
|
|
|
|
|
|
|
class EntityComponent(object):
|
2018-01-23 06:54:41 +00:00
|
|
|
"""The EntityComponent manages platforms that manages entities.
|
|
|
|
|
|
|
|
This class has the following responsibilities:
|
|
|
|
- Process the configuration and set up a platform based component.
|
|
|
|
- Manage the platforms and their entities.
|
|
|
|
- Help extract the entities from a service call.
|
|
|
|
- Maintain a group that tracks all platform entities.
|
|
|
|
- Listen for discovery events for platforms related to the domain.
|
|
|
|
"""
|
2016-01-31 02:55:52 +00:00
|
|
|
|
2015-03-22 01:49:30 +00:00
|
|
|
def __init__(self, logger, domain, hass,
|
2016-06-12 00:43:13 +00:00
|
|
|
scan_interval=DEFAULT_SCAN_INTERVAL, group_name=None):
|
2016-01-31 02:55:52 +00:00
|
|
|
"""Initialize an entity component."""
|
2015-03-22 01:49:30 +00:00
|
|
|
self.logger = logger
|
|
|
|
self.hass = hass
|
|
|
|
self.domain = domain
|
|
|
|
self.scan_interval = scan_interval
|
|
|
|
self.group_name = group_name
|
|
|
|
|
2015-05-15 04:36:12 +00:00
|
|
|
self.config = None
|
|
|
|
|
2016-09-04 15:15:52 +00:00
|
|
|
self._platforms = {
|
2018-04-08 03:04:50 +00:00
|
|
|
domain: self._async_init_entity_platform(domain, None)
|
2016-09-04 15:15:52 +00:00
|
|
|
}
|
2018-02-11 17:16:01 +00:00
|
|
|
self.async_add_entities = self._platforms[domain].async_add_entities
|
|
|
|
self.add_entities = self._platforms[domain].add_entities
|
2016-01-31 08:55:46 +00:00
|
|
|
|
2018-01-23 06:54:41 +00:00
|
|
|
@property
|
|
|
|
def entities(self):
|
|
|
|
"""Return an iterable that returns all entities."""
|
|
|
|
return chain.from_iterable(platform.entities.values() for platform
|
|
|
|
in self._platforms.values())
|
|
|
|
|
|
|
|
def get_entity(self, entity_id):
|
|
|
|
"""Helper method to get an entity."""
|
|
|
|
for platform in self._platforms.values():
|
|
|
|
entity = platform.entities.get(entity_id)
|
|
|
|
if entity is not None:
|
|
|
|
return entity
|
|
|
|
return None
|
|
|
|
|
2015-03-22 01:49:30 +00:00
|
|
|
def setup(self, config):
|
2016-03-07 22:39:52 +00:00
|
|
|
"""Set up a full entity component.
|
2016-01-31 02:55:52 +00:00
|
|
|
|
2017-03-01 04:33:19 +00:00
|
|
|
This doesn't block the executor to protect from deadlocks.
|
2015-03-22 01:49:30 +00:00
|
|
|
"""
|
2017-03-01 04:33:19 +00:00
|
|
|
self.hass.add_job(self.async_setup(config))
|
2016-10-16 16:35:46 +00:00
|
|
|
|
2018-02-25 11:38:46 +00:00
|
|
|
async def async_setup(self, config):
|
2016-10-16 16:35:46 +00:00
|
|
|
"""Set up a full entity component.
|
|
|
|
|
|
|
|
Loads the platforms from the config and will listen for supported
|
|
|
|
discovered platforms.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
2015-05-15 04:36:12 +00:00
|
|
|
self.config = config
|
|
|
|
|
2015-03-22 01:49:30 +00:00
|
|
|
# Look in config for Domain, Domain 2, Domain 3 etc and load them
|
2016-10-16 16:35:46 +00:00
|
|
|
tasks = []
|
2016-03-28 01:48:51 +00:00
|
|
|
for p_type, p_config in config_per_platform(config, self.domain):
|
2016-10-16 16:35:46 +00:00
|
|
|
tasks.append(self._async_setup_platform(p_type, p_config))
|
|
|
|
|
2016-11-06 17:26:40 +00:00
|
|
|
if tasks:
|
2018-02-25 11:38:46 +00:00
|
|
|
await asyncio.wait(tasks, loop=self.hass.loop)
|
2015-03-22 01:49:30 +00:00
|
|
|
|
2016-05-12 04:58:22 +00:00
|
|
|
# Generic discovery listener for loading platform dynamically
|
|
|
|
# Refer to: homeassistant.components.discovery.load_platform()
|
2018-02-25 11:38:46 +00:00
|
|
|
async def component_platform_discovered(platform, info):
|
2017-05-02 16:18:47 +00:00
|
|
|
"""Handle the loading of a platform."""
|
2018-02-25 11:38:46 +00:00
|
|
|
await self._async_setup_platform(platform, {}, info)
|
2016-06-12 00:43:13 +00:00
|
|
|
|
2016-10-16 16:35:46 +00:00
|
|
|
discovery.async_listen_platform(
|
|
|
|
self.hass, self.domain, component_platform_discovered)
|
2016-05-10 05:48:03 +00:00
|
|
|
|
2018-04-09 14:09:08 +00:00
|
|
|
async def async_setup_entry(self, config_entry):
|
|
|
|
"""Setup a config entry."""
|
|
|
|
platform_type = config_entry.domain
|
|
|
|
platform = await async_prepare_setup_platform(
|
|
|
|
self.hass, self.config, self.domain, platform_type)
|
|
|
|
|
|
|
|
if platform is None:
|
|
|
|
return False
|
|
|
|
|
|
|
|
key = config_entry.entry_id
|
|
|
|
|
|
|
|
if key in self._platforms:
|
|
|
|
raise ValueError('Config entry has already been setup!')
|
|
|
|
|
|
|
|
self._platforms[key] = self._async_init_entity_platform(
|
|
|
|
platform_type, platform
|
|
|
|
)
|
|
|
|
|
|
|
|
return await self._platforms[key].async_setup_entry(config_entry)
|
|
|
|
|
2017-11-17 05:03:05 +00:00
|
|
|
@callback
|
2016-10-29 23:54:26 +00:00
|
|
|
def async_extract_from_service(self, service, expand_group=True):
|
2017-04-11 15:59:46 +00:00
|
|
|
"""Extract all known and available entities from a service call.
|
2016-10-16 16:35:46 +00:00
|
|
|
|
|
|
|
Will return all entities if no entities specified in call.
|
|
|
|
Will return an empty list if entities specified but unknown.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
|
|
|
if ATTR_ENTITY_ID not in service.data:
|
2018-01-23 06:54:41 +00:00
|
|
|
return [entity for entity in self.entities if entity.available]
|
2015-11-28 23:55:01 +00:00
|
|
|
|
2018-01-23 06:54:41 +00:00
|
|
|
entity_ids = set(extract_entity_ids(self.hass, service, expand_group))
|
|
|
|
return [entity for entity in self.entities
|
|
|
|
if entity.available and entity.entity_id in entity_ids]
|
2015-03-22 01:49:30 +00:00
|
|
|
|
2018-02-25 11:38:46 +00:00
|
|
|
async def _async_setup_platform(self, platform_type, platform_config,
|
|
|
|
discovery_info=None):
|
2018-02-08 11:16:51 +00:00
|
|
|
"""Set up a platform for this component."""
|
2018-02-25 11:38:46 +00:00
|
|
|
platform = await async_prepare_setup_platform(
|
2016-10-27 07:16:23 +00:00
|
|
|
self.hass, self.config, self.domain, platform_type)
|
2015-03-22 01:49:30 +00:00
|
|
|
|
2016-04-18 05:07:53 +00:00
|
|
|
if platform is None:
|
|
|
|
return
|
|
|
|
|
2018-04-08 03:04:50 +00:00
|
|
|
# Use config scan interval, fallback to platform if none set
|
|
|
|
scan_interval = platform_config.get(
|
|
|
|
CONF_SCAN_INTERVAL, getattr(platform, 'SCAN_INTERVAL', None))
|
2016-04-23 04:34:49 +00:00
|
|
|
entity_namespace = platform_config.get(CONF_ENTITY_NAMESPACE)
|
2015-03-22 01:49:30 +00:00
|
|
|
|
2016-09-04 15:15:52 +00:00
|
|
|
key = (platform_type, scan_interval, entity_namespace)
|
|
|
|
|
|
|
|
if key not in self._platforms:
|
2018-04-08 03:04:50 +00:00
|
|
|
self._platforms[key] = self._async_init_entity_platform(
|
|
|
|
platform_type, platform, scan_interval, entity_namespace
|
2018-02-08 11:16:51 +00:00
|
|
|
)
|
2016-09-04 15:15:52 +00:00
|
|
|
|
2018-04-08 03:04:50 +00:00
|
|
|
await self._platforms[key].async_setup(platform_config, discovery_info)
|
2015-09-10 06:37:15 +00:00
|
|
|
|
2017-06-15 22:52:28 +00:00
|
|
|
@callback
|
2018-02-08 11:16:51 +00:00
|
|
|
def _async_update_group(self):
|
2016-10-16 16:35:46 +00:00
|
|
|
"""Set up and/or update component group.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
2018-01-23 06:54:41 +00:00
|
|
|
if self.group_name is None:
|
|
|
|
return
|
|
|
|
|
|
|
|
ids = [entity.entity_id for entity in
|
|
|
|
sorted(self.entities,
|
|
|
|
key=lambda entity: entity.name or entity.entity_id)]
|
|
|
|
|
|
|
|
self.hass.components.group.async_set_group(
|
|
|
|
slugify(self.group_name), name=self.group_name,
|
|
|
|
visible=False, entity_ids=ids
|
|
|
|
)
|
2016-01-31 08:55:46 +00:00
|
|
|
|
2018-02-25 11:38:46 +00:00
|
|
|
async def _async_reset(self):
|
2016-10-16 16:35:46 +00:00
|
|
|
"""Remove entities and reset the entity component to initial values.
|
2016-09-04 15:15:52 +00:00
|
|
|
|
2016-10-16 16:35:46 +00:00
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
|
|
|
tasks = [platform.async_reset() for platform
|
|
|
|
in self._platforms.values()]
|
|
|
|
|
2016-11-06 17:26:40 +00:00
|
|
|
if tasks:
|
2018-02-25 11:38:46 +00:00
|
|
|
await asyncio.wait(tasks, loop=self.hass.loop)
|
2016-10-16 16:35:46 +00:00
|
|
|
|
|
|
|
self._platforms = {
|
2018-02-11 17:16:01 +00:00
|
|
|
self.domain: self._platforms[self.domain]
|
2016-10-16 16:35:46 +00:00
|
|
|
}
|
|
|
|
self.config = None
|
|
|
|
|
2017-06-15 22:52:28 +00:00
|
|
|
if self.group_name is not None:
|
2018-01-23 06:54:41 +00:00
|
|
|
self.hass.components.group.async_remove(slugify(self.group_name))
|
|
|
|
|
2018-02-25 11:38:46 +00:00
|
|
|
async def async_remove_entity(self, entity_id):
|
2018-01-23 06:54:41 +00:00
|
|
|
"""Remove an entity managed by one of the platforms."""
|
|
|
|
for platform in self._platforms.values():
|
|
|
|
if entity_id in platform.entities:
|
2018-02-25 11:38:46 +00:00
|
|
|
await platform.async_remove_entity(entity_id)
|
2016-09-04 15:15:52 +00:00
|
|
|
|
2018-02-25 11:38:46 +00:00
|
|
|
async def async_prepare_reload(self):
|
2016-10-27 07:16:23 +00:00
|
|
|
"""Prepare reloading this entity component.
|
|
|
|
|
|
|
|
This method must be run in the event loop.
|
|
|
|
"""
|
2016-09-07 13:59:16 +00:00
|
|
|
try:
|
2018-02-25 11:38:46 +00:00
|
|
|
conf = await \
|
2016-10-27 07:16:23 +00:00
|
|
|
conf_util.async_hass_config_yaml(self.hass)
|
2016-09-07 13:59:16 +00:00
|
|
|
except HomeAssistantError as err:
|
|
|
|
self.logger.error(err)
|
|
|
|
return None
|
|
|
|
|
2017-03-01 04:33:19 +00:00
|
|
|
conf = conf_util.async_process_component_config(
|
2016-10-27 07:16:23 +00:00
|
|
|
self.hass, conf, self.domain)
|
2016-09-07 13:59:16 +00:00
|
|
|
|
|
|
|
if conf is None:
|
|
|
|
return None
|
|
|
|
|
2018-02-25 11:38:46 +00:00
|
|
|
await self._async_reset()
|
2016-10-16 16:35:46 +00:00
|
|
|
return conf
|
2018-04-08 03:04:50 +00:00
|
|
|
|
|
|
|
def _async_init_entity_platform(self, platform_type, platform,
|
|
|
|
scan_interval=None, entity_namespace=None):
|
|
|
|
"""Helper to initialize an entity platform."""
|
|
|
|
if scan_interval is None:
|
|
|
|
scan_interval = self.scan_interval
|
|
|
|
|
|
|
|
return EntityPlatform(
|
|
|
|
hass=self.hass,
|
|
|
|
logger=self.logger,
|
|
|
|
domain=self.domain,
|
|
|
|
platform_name=platform_type,
|
|
|
|
platform=platform,
|
|
|
|
scan_interval=scan_interval,
|
|
|
|
entity_namespace=entity_namespace,
|
|
|
|
async_entities_added_callback=self._async_update_group,
|
|
|
|
)
|