mirror of
https://github.com/home-assistant/core.git
synced 2025-04-23 08:47:57 +00:00
Reload groups (#3203)
* Allow reloading groups without restart * Test to make sure automation listeners are removed. * Remove unused imports for group tests * Simplify group config validation * Add prepare_reload function to entity component * Migrate group to use entity_component.prepare_reload * Migrate automation to use entity_component.prepare_reload * Clean up group.get_entity_ids * Use cv.boolean for group config validation
This commit is contained in:
parent
91028cbc13
commit
35b388edce
@ -14,7 +14,7 @@ import voluptuous as vol
|
||||
from voluptuous.humanize import humanize_error
|
||||
|
||||
import homeassistant.components as core_components
|
||||
from homeassistant.components import group, persistent_notification
|
||||
from homeassistant.components import persistent_notification
|
||||
import homeassistant.config as conf_util
|
||||
import homeassistant.core as core
|
||||
import homeassistant.loader as loader
|
||||
@ -118,7 +118,7 @@ def _setup_component(hass: core.HomeAssistant, domain: str, config) -> bool:
|
||||
|
||||
# Assumption: if a component does not depend on groups
|
||||
# it communicates with devices
|
||||
if group.DOMAIN not in getattr(component, 'DEPENDENCIES', []):
|
||||
if 'group' not in getattr(component, 'DEPENDENCIES', []):
|
||||
hass.pool.add_worker()
|
||||
|
||||
hass.bus.fire(
|
||||
|
@ -10,8 +10,7 @@ import os
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
from homeassistant.bootstrap import (
|
||||
prepare_setup_platform, prepare_setup_component)
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_PLATFORM, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF,
|
||||
@ -183,19 +182,9 @@ def setup(hass, config):
|
||||
|
||||
def reload_service_handler(service_call):
|
||||
"""Remove all automations and load new ones from config."""
|
||||
try:
|
||||
path = conf_util.find_config_file(hass.config.config_dir)
|
||||
conf = conf_util.load_yaml_config_file(path)
|
||||
except HomeAssistantError as err:
|
||||
_LOGGER.error(err)
|
||||
return
|
||||
|
||||
conf = prepare_setup_component(hass, conf, DOMAIN)
|
||||
|
||||
conf = component.prepare_reload()
|
||||
if conf is None:
|
||||
return
|
||||
|
||||
component.reset()
|
||||
_process_config(hass, conf, component)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_TRIGGER, trigger_service_handler,
|
||||
|
@ -4,17 +4,19 @@ Provides functionality to group entities.
|
||||
For more details about this component, please refer to the documentation at
|
||||
https://home-assistant.io/components/group/
|
||||
"""
|
||||
import logging
|
||||
import os
|
||||
import threading
|
||||
from collections import OrderedDict
|
||||
|
||||
import voluptuous as vol
|
||||
|
||||
import homeassistant.core as ha
|
||||
from homeassistant import config as conf_util, core as ha
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_ICON, CONF_NAME, STATE_CLOSED, STATE_HOME,
|
||||
STATE_NOT_HOME, STATE_OFF, STATE_ON, STATE_OPEN, STATE_LOCKED,
|
||||
STATE_UNLOCKED, STATE_UNKNOWN, ATTR_ASSUMED_STATE)
|
||||
from homeassistant.helpers.entity import Entity, generate_entity_id
|
||||
from homeassistant.helpers.entity_component import EntityComponent
|
||||
from homeassistant.helpers.event import track_state_change
|
||||
import homeassistant.helpers.config_validation as cv
|
||||
|
||||
@ -29,36 +31,27 @@ ATTR_AUTO = 'auto'
|
||||
ATTR_ORDER = 'order'
|
||||
ATTR_VIEW = 'view'
|
||||
|
||||
SERVICE_RELOAD = 'reload'
|
||||
RELOAD_SERVICE_SCHEMA = vol.Schema({})
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def _conf_preprocess(value):
|
||||
"""Preprocess alternative configuration formats."""
|
||||
if isinstance(value, (str, list)):
|
||||
if not isinstance(value, dict):
|
||||
value = {CONF_ENTITIES: value}
|
||||
|
||||
return value
|
||||
|
||||
_SINGLE_GROUP_CONFIG = vol.Schema(vol.All(_conf_preprocess, {
|
||||
vol.Optional(CONF_ENTITIES): vol.Any(cv.entity_ids, None),
|
||||
CONF_VIEW: bool,
|
||||
CONF_NAME: str,
|
||||
CONF_ICON: cv.icon,
|
||||
}))
|
||||
|
||||
|
||||
def _group_dict(value):
|
||||
"""Validate a dictionary of group definitions."""
|
||||
config = OrderedDict()
|
||||
for key, group in value.items():
|
||||
try:
|
||||
config[key] = _SINGLE_GROUP_CONFIG(group)
|
||||
except vol.MultipleInvalid as ex:
|
||||
raise vol.Invalid('Group {} is invalid: {}'.format(key, ex))
|
||||
|
||||
return config
|
||||
|
||||
|
||||
CONFIG_SCHEMA = vol.Schema({
|
||||
DOMAIN: vol.All(dict, _group_dict)
|
||||
DOMAIN: {cv.match_all: vol.Schema(vol.All(_conf_preprocess, {
|
||||
vol.Optional(CONF_ENTITIES): vol.Any(cv.entity_ids, None),
|
||||
CONF_VIEW: cv.boolean,
|
||||
CONF_NAME: cv.string,
|
||||
CONF_ICON: cv.icon,
|
||||
}))}
|
||||
}, extra=vol.ALLOW_EXTRA)
|
||||
|
||||
# List of ON/OFF state tuples for groupable states
|
||||
@ -88,6 +81,11 @@ def is_on(hass, entity_id):
|
||||
return False
|
||||
|
||||
|
||||
def reload(hass):
|
||||
"""Reload the automation from config."""
|
||||
hass.services.call(DOMAIN, SERVICE_RELOAD)
|
||||
|
||||
|
||||
def expand_entity_ids(hass, entity_ids):
|
||||
"""Return entity_ids with group entity ids replaced by their members."""
|
||||
found_ids = []
|
||||
@ -121,35 +119,59 @@ def expand_entity_ids(hass, entity_ids):
|
||||
|
||||
def get_entity_ids(hass, entity_id, domain_filter=None):
|
||||
"""Get members of this group."""
|
||||
entity_id = entity_id.lower()
|
||||
group = hass.states.get(entity_id)
|
||||
|
||||
try:
|
||||
entity_ids = hass.states.get(entity_id).attributes[ATTR_ENTITY_ID]
|
||||
|
||||
if domain_filter:
|
||||
domain_filter = domain_filter.lower()
|
||||
|
||||
return [ent_id for ent_id in entity_ids
|
||||
if ent_id.startswith(domain_filter)]
|
||||
else:
|
||||
return entity_ids
|
||||
|
||||
except (AttributeError, KeyError):
|
||||
# AttributeError if state did not exist
|
||||
# KeyError if key did not exist in attributes
|
||||
if not group or ATTR_ENTITY_ID not in group.attributes:
|
||||
return []
|
||||
|
||||
entity_ids = group.attributes[ATTR_ENTITY_ID]
|
||||
|
||||
if not domain_filter:
|
||||
return entity_ids
|
||||
|
||||
domain_filter = domain_filter.lower() + '.'
|
||||
|
||||
return [ent_id for ent_id in entity_ids
|
||||
if ent_id.startswith(domain_filter)]
|
||||
|
||||
|
||||
def setup(hass, config):
|
||||
"""Setup all groups found definded in the configuration."""
|
||||
component = EntityComponent(_LOGGER, DOMAIN, hass)
|
||||
|
||||
success = _process_config(hass, config, component)
|
||||
|
||||
if not success:
|
||||
return False
|
||||
|
||||
descriptions = conf_util.load_yaml_config_file(
|
||||
os.path.join(os.path.dirname(__file__), 'services.yaml'))
|
||||
|
||||
def reload_service_handler(service_call):
|
||||
"""Remove all groups and load new ones from config."""
|
||||
conf = component.prepare_reload()
|
||||
if conf is None:
|
||||
return
|
||||
_process_config(hass, conf, component)
|
||||
|
||||
hass.services.register(DOMAIN, SERVICE_RELOAD, reload_service_handler,
|
||||
descriptions[DOMAIN][SERVICE_RELOAD],
|
||||
schema=RELOAD_SERVICE_SCHEMA)
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def _process_config(hass, config, component):
|
||||
"""Process group configuration."""
|
||||
for object_id, conf in config.get(DOMAIN, {}).items():
|
||||
name = conf.get(CONF_NAME, object_id)
|
||||
entity_ids = conf.get(CONF_ENTITIES) or []
|
||||
icon = conf.get(CONF_ICON)
|
||||
view = conf.get(CONF_VIEW)
|
||||
|
||||
Group(hass, name, entity_ids, icon=icon, view=view,
|
||||
object_id=object_id)
|
||||
group = Group(hass, name, entity_ids, icon=icon, view=view,
|
||||
object_id=object_id)
|
||||
component.add_entities((group,))
|
||||
|
||||
return True
|
||||
|
||||
@ -242,17 +264,21 @@ class Group(Entity):
|
||||
|
||||
def stop(self):
|
||||
"""Unregister the group from Home Assistant."""
|
||||
self.hass.states.remove(self.entity_id)
|
||||
|
||||
if self._unsub_state_changed:
|
||||
self._unsub_state_changed()
|
||||
self._unsub_state_changed = None
|
||||
self.remove()
|
||||
|
||||
def update(self):
|
||||
"""Query all members and determine current group state."""
|
||||
self._state = STATE_UNKNOWN
|
||||
self._update_group_state()
|
||||
|
||||
def remove(self):
|
||||
"""Remove group from HASS."""
|
||||
super().remove()
|
||||
|
||||
if self._unsub_state_changed:
|
||||
self._unsub_state_changed()
|
||||
self._unsub_state_changed = None
|
||||
|
||||
def _state_changed_listener(self, entity_id, old_state, new_state):
|
||||
"""Respond to a member state changing."""
|
||||
self._update_group_state(new_state)
|
||||
|
@ -39,6 +39,11 @@ foursquare:
|
||||
description: Vertical accuracy of the user's location, in meters.
|
||||
example: 1
|
||||
|
||||
group:
|
||||
reload:
|
||||
description: "Reload group configuration."
|
||||
fields:
|
||||
|
||||
persistent_notification:
|
||||
create:
|
||||
description: Show a notification in the frontend
|
||||
|
@ -1,11 +1,14 @@
|
||||
"""Helpers for components that manage entities."""
|
||||
from threading import Lock
|
||||
|
||||
from homeassistant.bootstrap import prepare_setup_platform
|
||||
from homeassistant.components import group
|
||||
from homeassistant import config as conf_util
|
||||
from homeassistant.bootstrap import (prepare_setup_platform,
|
||||
prepare_setup_component)
|
||||
from homeassistant.const import (
|
||||
ATTR_ENTITY_ID, CONF_SCAN_INTERVAL, CONF_ENTITY_NAMESPACE,
|
||||
DEVICE_DEFAULT_NAME)
|
||||
from homeassistant.exceptions import HomeAssistantError
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.helpers import config_per_platform, discovery
|
||||
from homeassistant.helpers.entity import generate_entity_id
|
||||
from homeassistant.helpers.event import track_utc_time_change
|
||||
@ -135,6 +138,7 @@ class EntityComponent(object):
|
||||
def update_group(self):
|
||||
"""Set up and/or update component group."""
|
||||
if self.group is None and self.group_name is not None:
|
||||
group = get_component('group')
|
||||
self.group = group.Group(self.hass, self.group_name,
|
||||
user_defined=False)
|
||||
|
||||
@ -157,6 +161,23 @@ class EntityComponent(object):
|
||||
self.group.stop()
|
||||
self.group = None
|
||||
|
||||
def prepare_reload(self):
|
||||
"""Prepare reloading this entity component."""
|
||||
try:
|
||||
path = conf_util.find_config_file(self.hass.config.config_dir)
|
||||
conf = conf_util.load_yaml_config_file(path)
|
||||
except HomeAssistantError as err:
|
||||
self.logger.error(err)
|
||||
return None
|
||||
|
||||
conf = prepare_setup_component(self.hass, conf, self.domain)
|
||||
|
||||
if conf is None:
|
||||
return None
|
||||
|
||||
self.reset()
|
||||
return conf
|
||||
|
||||
|
||||
class EntityPlatform(object):
|
||||
"""Keep track of entities for a single platform."""
|
||||
|
@ -6,11 +6,11 @@ import logging
|
||||
import jinja2
|
||||
from jinja2.sandbox import ImmutableSandboxedEnvironment
|
||||
|
||||
from homeassistant.components import group
|
||||
from homeassistant.const import STATE_UNKNOWN, ATTR_LATITUDE, ATTR_LONGITUDE
|
||||
from homeassistant.core import State
|
||||
from homeassistant.exceptions import TemplateError
|
||||
from homeassistant.helpers import location as loc_helper
|
||||
from homeassistant.loader import get_component
|
||||
from homeassistant.util import convert, dt as dt_util, location as loc_util
|
||||
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
@ -169,6 +169,8 @@ class LocationMethods(object):
|
||||
else:
|
||||
gr_entity_id = str(entities)
|
||||
|
||||
group = get_component('group')
|
||||
|
||||
states = [self._hass.states.get(entity_id) for entity_id
|
||||
in group.expand_entity_ids(self._hass, [gr_entity_id])]
|
||||
|
||||
|
@ -450,6 +450,9 @@ class TestAutomation(unittest.TestCase):
|
||||
})
|
||||
assert self.hass.states.get('automation.hello') is not None
|
||||
assert self.hass.states.get('automation.bye') is None
|
||||
listeners = self.hass.bus.listeners
|
||||
assert listeners.get('test_event') == 1
|
||||
assert listeners.get('test_event2') is None
|
||||
|
||||
self.hass.bus.fire('test_event')
|
||||
self.hass.pool.block_till_done()
|
||||
@ -462,6 +465,9 @@ class TestAutomation(unittest.TestCase):
|
||||
|
||||
assert self.hass.states.get('automation.hello') is None
|
||||
assert self.hass.states.get('automation.bye') is not None
|
||||
listeners = self.hass.bus.listeners
|
||||
assert listeners.get('test_event') is None
|
||||
assert listeners.get('test_event2') == 1
|
||||
|
||||
self.hass.bus.fire('test_event')
|
||||
self.hass.pool.block_till_done()
|
||||
|
@ -1,6 +1,7 @@
|
||||
"""The tests for the Group components."""
|
||||
# pylint: disable=protected-access,too-many-public-methods
|
||||
import unittest
|
||||
from unittest.mock import patch
|
||||
|
||||
from homeassistant.bootstrap import _setup_component
|
||||
from homeassistant.const import (
|
||||
@ -308,3 +309,33 @@ class TestComponentsGroup(unittest.TestCase):
|
||||
self.assertEqual(STATE_NOT_HOME,
|
||||
self.hass.states.get(
|
||||
group.ENTITY_ID_FORMAT.format('peeps')).state)
|
||||
|
||||
def test_reloading_groups(self):
|
||||
"""Test reloading the group config."""
|
||||
_setup_component(self.hass, 'group', {'group': {
|
||||
'second_group': {
|
||||
'entities': 'light.Bowl',
|
||||
'icon': 'mdi:work',
|
||||
'view': True,
|
||||
},
|
||||
'test_group': 'hello.world,sensor.happy',
|
||||
'empty_group': {'name': 'Empty Group', 'entities': None},
|
||||
}
|
||||
})
|
||||
|
||||
assert sorted(self.hass.states.entity_ids()) == \
|
||||
['group.empty_group', 'group.second_group', 'group.test_group']
|
||||
assert self.hass.bus.listeners['state_changed'] == 3
|
||||
|
||||
with patch('homeassistant.config.load_yaml_config_file', return_value={
|
||||
'group': {
|
||||
'hello': {
|
||||
'entities': 'light.Bowl',
|
||||
'icon': 'mdi:work',
|
||||
'view': True,
|
||||
}}}):
|
||||
group.reload(self.hass)
|
||||
self.hass.pool.block_till_done()
|
||||
|
||||
assert self.hass.states.entity_ids() == ['group.hello']
|
||||
assert self.hass.bus.listeners['state_changed'] == 1
|
||||
|
Loading…
x
Reference in New Issue
Block a user