Make group component more flexible

This commit is contained in:
Paulus Schoutsen 2015-01-08 20:02:34 -08:00
parent f5683797aa
commit e0b424c88f
6 changed files with 106 additions and 115 deletions

View File

@ -9,7 +9,7 @@ import unittest
import logging import logging
import homeassistant as ha import homeassistant as ha
from homeassistant.const import STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME from homeassistant.const import STATE_ON, STATE_OFF, STATE_HOME, STATE_UNKNOWN
import homeassistant.components.group as group import homeassistant.components.group as group
@ -40,38 +40,41 @@ class TestComponentsGroup(unittest.TestCase):
""" Stop down stuff we started. """ """ Stop down stuff we started. """
self.hass.stop() self.hass.stop()
def test_setup_group(self): def test_setup_group_with_mixed_groupable_states(self):
""" Test setup_group method. """ """ Try to setup a group with mixed groupable states """
# Try to setup a group with mixed groupable states
self.hass.states.set('device_tracker.Paulus', STATE_HOME) self.hass.states.set('device_tracker.Paulus', STATE_HOME)
self.assertTrue(group.setup_group( group.setup_group(
self.hass, 'person_and_light', self.hass, 'person_and_light',
['light.Bowl', 'device_tracker.Paulus'])) ['light.Bowl', 'device_tracker.Paulus'])
self.assertEqual( self.assertEqual(
STATE_ON, STATE_ON,
self.hass.states.get( self.hass.states.get(
group.ENTITY_ID_FORMAT.format('person_and_light')).state) group.ENTITY_ID_FORMAT.format('person_and_light')).state)
# Try to setup a group with a non existing state def test_setup_group_with_a_non_existing_state(self):
self.assertNotIn('non.existing', self.hass.states.entity_ids()) """ Try to setup a group with a non existing state """
self.assertTrue(group.setup_group( grp = group.setup_group(
self.hass, 'light_and_nothing', self.hass, 'light_and_nothing',
['light.Bowl', 'non.existing'])) ['light.Bowl', 'non.existing'])
self.assertEqual(
STATE_ON,
self.hass.states.get(
group.ENTITY_ID_FORMAT.format('light_and_nothing')).state)
# Try to setup a group with non groupable states self.assertEqual(STATE_ON, grp.state.state)
def test_setup_group_with_non_groupable_states(self):
self.hass.states.set('cast.living_room', "Plex") self.hass.states.set('cast.living_room', "Plex")
self.hass.states.set('cast.bedroom', "Netflix") self.hass.states.set('cast.bedroom', "Netflix")
self.assertFalse(
group.setup_group(
self.hass, 'chromecasts',
['cast.living_room', 'cast.bedroom']))
# Try to setup an empty group grp = group.setup_group(
self.assertFalse(group.setup_group(self.hass, 'nothing', [])) self.hass, 'chromecasts',
['cast.living_room', 'cast.bedroom'])
self.assertEqual(STATE_UNKNOWN, grp.state.state)
def test_setup_empty_group(self):
""" Try to setup an empty group. """
grp = group.setup_group(self.hass, 'nothing', [])
self.assertEqual(STATE_UNKNOWN, grp.state.state)
def test_monitor_group(self): def test_monitor_group(self):
""" Test if the group keeps track of states. """ """ Test if the group keeps track of states. """

View File

@ -111,19 +111,16 @@ class DeviceTracker(object):
""" Triggers update of the device states. """ """ Triggers update of the device states. """
self.update_devices(now) self.update_devices(now)
dev_group = group.Group(hass, GROUP_NAME_ALL_DEVICES)
# pylint: disable=unused-argument # pylint: disable=unused-argument
def reload_known_devices_service(service): def reload_known_devices_service(service):
""" Reload known devices file. """ """ Reload known devices file. """
group.remove_group(self.hass, GROUP_NAME_ALL_DEVICES)
self._read_known_devices_file() self._read_known_devices_file()
self.update_devices(datetime.now()) self.update_devices(datetime.now())
if self.tracked: dev_group.update_tracked_entity_ids(self.device_entity_ids)
group.setup_group(
self.hass, GROUP_NAME_ALL_DEVICES,
self.device_entity_ids, False)
reload_known_devices_service(None) reload_known_devices_service(None)

View File

@ -5,12 +5,11 @@ homeassistant.components.groups
Provides functionality to group devices that can be turned on or off. Provides functionality to group devices that can be turned on or off.
""" """
import logging
import homeassistant as ha import homeassistant as ha
import homeassistant.util as util import homeassistant.util as util
from homeassistant.const import ( from homeassistant.const import (
ATTR_ENTITY_ID, STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME) ATTR_ENTITY_ID, STATE_ON, STATE_OFF, STATE_HOME, STATE_NOT_HOME,
STATE_UNKNOWN)
DOMAIN = "group" DOMAIN = "group"
DEPENDENCIES = [] DEPENDENCIES = []
@ -22,8 +21,6 @@ ATTR_AUTO = "auto"
# List of ON/OFF state tuples for groupable states # List of ON/OFF state tuples for groupable states
_GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME)] _GROUP_TYPES = [(STATE_ON, STATE_OFF), (STATE_HOME, STATE_NOT_HOME)]
_GROUPS = {}
def _get_group_on_off(state): def _get_group_on_off(state):
""" Determine the group on/off states based on a state. """ """ Determine the group on/off states based on a state. """
@ -101,114 +98,109 @@ def setup(hass, config):
return True return True
def setup_group(hass, name, entity_ids, user_defined=True): class Group(object):
""" Sets up a group state that is the combined state of """ Tracks a group of entity ids. """
several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """ def __init__(self, hass, name, entity_ids=None, user_defined=True):
logger = logging.getLogger(__name__) self.hass = hass
self.name = name
self.user_defined = user_defined
self.entity_id = ENTITY_ID_FORMAT.format(util.slugify(name))
# In case an iterable is passed in self.tracking = []
entity_ids = list(entity_ids) self.group_on, self.group_off = None, None
if not entity_ids: if entity_ids is not None:
logger.error( self.update_tracked_entity_ids(entity_ids)
'Error setting up group %s: no entities passed in to track', name)
return False @property
def state(self):
""" Return the current state from the group. """
return self.hass.states.get(self.entity_id)
# Loop over the given entities to: @property
# - determine which group type this is (on_off, device_home) def state_attr(self):
# - determine which states exist and have groupable states """ State attributes of this group. """
# - determine the current state of the group return {
warnings = [] ATTR_ENTITY_ID: self.tracking,
group_ids = [] ATTR_AUTO: not self.user_defined
group_on, group_off = None, None }
group_state = False
for entity_id in entity_ids: def update_tracked_entity_ids(self, entity_ids):
state = hass.states.get(entity_id) """ Update the tracked entity IDs. """
self.stop()
# Try to determine group type if we didn't yet self.tracking = list(entity_ids)
if group_on is None and state: self.group_on, self.group_off = None, None
group_on, group_off = _get_group_on_off(state.state)
if group_on is None: self.force_update()
# We did not find a matching group_type
warnings.append(
"Entity {} has ungroupable state '{}'".format(
name, state.state))
continue self.start()
# Check if entity exists def force_update(self):
if not state: """ Query all the tracked states and update group state. """
warnings.append("Entity {} does not exist".format(entity_id)) for entity_id in self.tracking:
state = self.hass.states.get(entity_id)
# Check if entity is invalid state if state is not None:
elif state.state != group_off and state.state != group_on: self._update_group_state(state.entity_id, None, state)
warnings.append("State of {} is {} (expected: {} or {})".format( # If parsing the entitys did not result in a state, set UNKNOWN
entity_id, state.state, group_off, group_on)) if self.state is None:
self.hass.states.set(self.entity_id, STATE_UNKNOWN)
# We have a valid group state def start(self):
else: """ Starts the tracking. """
group_ids.append(entity_id) self.hass.states.track_change(self.tracking, self._update_group_state)
# Keep track of the group state to init later on def stop(self):
group_state = group_state or state.state == group_on """ Unregisters the group from Home Assistant. """
self.hass.states.remove(self.entity_id)
# If none of the entities could be found during setup self.hass.bus.remove_listener(
if not group_ids: ha.EVENT_STATE_CHANGED, self._update_group_state)
logger.error('Unable to find any entities to track for group %s', name)
return False
elif warnings:
logger.warning(
'Warnings during setting up group %s: %s',
name, ", ".join(warnings))
group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name))
state = group_on if group_state else group_off
state_attr = {ATTR_ENTITY_ID: group_ids, ATTR_AUTO: not user_defined}
# pylint: disable=unused-argument # pylint: disable=unused-argument
def update_group_state(entity_id, old_state, new_state): def _update_group_state(self, entity_id, old_state, new_state):
""" Updates the group state based on a state change by """ Updates the group state based on a state change by
a tracked entity. """ a tracked entity. """
cur_gr_state = hass.states.get(group_entity_id).state # We have not determined type of group yet
if self.group_on is None:
self.group_on, self.group_off = _get_group_on_off(new_state.state)
if self.group_on is not None:
# New state of the group is going to be based on the first
# state that we can recognize
self.hass.states.set(
self.entity_id, new_state.state, self.state_attr)
return
# There is already a group state
cur_gr_state = self.hass.states.get(self.entity_id).state
# if cur_gr_state = OFF and new_state = ON: set ON # if cur_gr_state = OFF and new_state = ON: set ON
# if cur_gr_state = ON and new_state = OFF: research # if cur_gr_state = ON and new_state = OFF: research
# else: ignore # else: ignore
if cur_gr_state == group_off and new_state.state == group_on: if cur_gr_state == self.group_off and new_state.state == self.group_on:
hass.states.set(group_entity_id, group_on, state_attr) self.hass.states.set(
self.entity_id, self.group_on, self.state_attr)
elif cur_gr_state == group_on and new_state.state == group_off: elif (cur_gr_state == self.group_on and
new_state.state == self.group_off):
# Check if any of the other states is still on # Check if any of the other states is still on
if not any([hass.states.is_state(ent_id, group_on) if not any([self.hass.states.is_state(ent_id, self.group_on)
for ent_id in group_ids for ent_id in self.tracking
if entity_id != ent_id]): if entity_id != ent_id]):
hass.states.set(group_entity_id, group_off, state_attr) self.hass.states.set(
self.entity_id, self.group_off, self.state_attr)
_GROUPS[group_entity_id] = hass.states.track_change(
group_ids, update_group_state)
hass.states.set(group_entity_id, state, state_attr)
return True
def remove_group(hass, name): def setup_group(hass, name, entity_ids, user_defined=True):
""" Remove a group and its state listener from Home Assistant. """ """ Sets up a group state that is the combined state of
group_entity_id = ENTITY_ID_FORMAT.format(util.slugify(name)) several states. Supports ON/OFF and DEVICE_HOME/DEVICE_NOT_HOME. """
if hass.states.get(group_entity_id) is not None: return Group(hass, name, entity_ids, user_defined)
hass.states.remove(group_entity_id)
if group_entity_id in _GROUPS:
hass.bus.remove_listener(
ha.EVENT_STATE_CHANGED, _GROUPS.pop(group_entity_id))

View File

@ -178,8 +178,7 @@ def setup(hass, config):
update_lights_state(None) update_lights_state(None)
# Track all lights in a group # Track all lights in a group
group.setup_group( group.Group(hass, GROUP_NAME_ALL_LIGHTS, lights.keys(), False)
hass, GROUP_NAME_ALL_LIGHTS, lights.keys(), False)
def handle_light_service(service): def handle_light_service(service):
""" Hande a turn light on or off service call. """ """ Hande a turn light on or off service call. """

View File

@ -91,8 +91,7 @@ def setup(hass, config):
switch.update_ha_state(hass) switch.update_ha_state(hass)
# Track all switches in a group # Track all switches in a group
group.setup_group(hass, GROUP_NAME_ALL_SWITCHES, group.Group(hass, GROUP_NAME_ALL_SWITCHES, switches.keys(), False)
switches.keys(), False)
# Update state every 30 seconds # Update state every 30 seconds
hass.track_time_change(update_states, second=[0, 30]) hass.track_time_change(update_states, second=[0, 30])

View File

@ -29,6 +29,7 @@ STATE_ON = 'on'
STATE_OFF = 'off' STATE_OFF = 'off'
STATE_HOME = 'home' STATE_HOME = 'home'
STATE_NOT_HOME = 'not_home' STATE_NOT_HOME = 'not_home'
STATE_UNKNOWN = "unknown"
# #### STATE AND EVENT ATTRIBUTES #### # #### STATE AND EVENT ATTRIBUTES ####
# Contains current time for a TIME_CHANGED event # Contains current time for a TIME_CHANGED event