Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Reload groups #3203

Merged
merged 9 commits into from Sep 7, 2016
4 changes: 2 additions & 2 deletions homeassistant/bootstrap.py
Expand Up @@ -14,7 +14,7 @@
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
Expand Down Expand Up @@ -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', []):
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't it be safer to keep the group.DOMAIN in case, for some reason, it changes in the future?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Circular imports.. I could use get component method

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be the alternative, not sure if prettier?

group = loader.get_component('group')

if group.DOMAIN not in getattr(component, 'DEPENDENCIES', []):
    …

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not prettier, I'm good with these changes.

On Tue, Sep 6, 2016 at 8:13 PM, Paulus Schoutsen notifications@github.com
wrote:

In homeassistant/bootstrap.py
#3203 (comment)
:

@@ -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', []):
    

This would be the alternative, not sure if prettier?

group = loader.get_component('group')
if group.DOMAIN not in getattr(component, 'DEPENDENCIES', []):


You are receiving this because you commented.
Reply to this email directly, view it on GitHub
https://github.com/home-assistant/home-assistant/pull/3203/files/6232ed440ba416b98afad51dc1353c3b16ad7a79#r77750626,
or mute the thread
https://github.com/notifications/unsubscribe-auth/AC2fZddcglEfGxJ8cne4QoHYJ9VZUT3aks5qnh3AgaJpZM4J1Cl0
.

hass.pool.add_worker()

hass.bus.fire(
Expand Down
15 changes: 2 additions & 13 deletions homeassistant/components/automation/__init__.py
Expand Up @@ -10,8 +10,7 @@

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,
Expand Down Expand Up @@ -183,19 +182,9 @@ def service_handler(service_call):

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,
Expand Down
112 changes: 69 additions & 43 deletions homeassistant/components/group.py
Expand Up @@ -4,17 +4,19 @@
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

Expand All @@ -29,36 +31,27 @@
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
Expand Down Expand Up @@ -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 = []
Expand Down Expand Up @@ -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 not group or ATTR_ENTITY_ID not in group.attributes:
return []

if domain_filter:
domain_filter = domain_filter.lower()
entity_ids = group.attributes[ATTR_ENTITY_ID]

return [ent_id for ent_id in entity_ids
if ent_id.startswith(domain_filter)]
else:
return entity_ids
if not domain_filter:
return entity_ids

except (AttributeError, KeyError):
# AttributeError if state did not exist
# KeyError if key did not exist in attributes
return []
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

Expand Down Expand Up @@ -242,17 +264,21 @@ def start(self):

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)
Expand Down
5 changes: 5 additions & 0 deletions homeassistant/components/services.yaml
Expand Up @@ -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
Expand Down
25 changes: 23 additions & 2 deletions homeassistant/helpers/entity_component.py
@@ -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
Expand Down Expand Up @@ -135,6 +138,7 @@ def add_entity(self, entity, platform=None):
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)

Expand All @@ -157,6 +161,23 @@ def reset(self):
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."""
Expand Down
4 changes: 3 additions & 1 deletion homeassistant/helpers/template.py
Expand Up @@ -6,11 +6,11 @@
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__)
Expand Down Expand Up @@ -169,6 +169,8 @@ def closest(self, *args):
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])]

Expand Down
6 changes: 6 additions & 0 deletions tests/components/automation/test_init.py
Expand Up @@ -450,6 +450,9 @@ def test_reload_config_service(self, mock_load_yaml):
})
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()
Expand All @@ -462,6 +465,9 @@ def test_reload_config_service(self, mock_load_yaml):

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()
Expand Down
31 changes: 31 additions & 0 deletions tests/components/test_group.py
@@ -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 (
Expand Down Expand Up @@ -308,3 +309,33 @@ def test_group_updated_after_device_tracker_zone_change(self):
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