From 9d56730b8de62c5aa1e16d25fe8b0054479c3065 Mon Sep 17 00:00:00 2001 From: Daniel Perna Date: Tue, 9 Oct 2018 10:14:55 +0200 Subject: [PATCH] Add optional "all" parameter for groups (#17179) * Added optional mode parameter * Cleanup * Using boolean configuration * Fix invalid syntax * Added tests for all-parameter * Grammar * Lint * Docstrings * Better description --- homeassistant/components/group/__init__.py | 34 +++++++++++++++----- homeassistant/components/group/services.yaml | 3 ++ tests/components/group/test_init.py | 30 +++++++++++++++++ 3 files changed, 59 insertions(+), 8 deletions(-) diff --git a/homeassistant/components/group/__init__.py b/homeassistant/components/group/__init__.py index 39fd7567c98..4dd3571e69c 100644 --- a/homeassistant/components/group/__init__.py +++ b/homeassistant/components/group/__init__.py @@ -30,6 +30,7 @@ ENTITY_ID_FORMAT = DOMAIN + '.{}' CONF_ENTITIES = 'entities' CONF_VIEW = 'view' CONF_CONTROL = 'control' +CONF_ALL = 'all' ATTR_ADD_ENTITIES = 'add_entities' ATTR_AUTO = 'auto' @@ -39,6 +40,7 @@ ATTR_OBJECT_ID = 'object_id' ATTR_ORDER = 'order' ATTR_VIEW = 'view' ATTR_VISIBLE = 'visible' +ATTR_ALL = 'all' SERVICE_SET_VISIBILITY = 'set_visibility' SERVICE_SET = 'set' @@ -60,6 +62,7 @@ SET_SERVICE_SCHEMA = vol.Schema({ vol.Optional(ATTR_ICON): cv.string, vol.Optional(ATTR_CONTROL): CONTROL_TYPES, vol.Optional(ATTR_VISIBLE): cv.boolean, + vol.Optional(ATTR_ALL): cv.boolean, vol.Exclusive(ATTR_ENTITIES, 'entities'): cv.entity_ids, vol.Exclusive(ATTR_ADD_ENTITIES, 'entities'): cv.entity_ids, }) @@ -85,6 +88,7 @@ GROUP_SCHEMA = vol.Schema({ CONF_NAME: cv.string, CONF_ICON: cv.icon, CONF_CONTROL: CONTROL_TYPES, + CONF_ALL: cv.boolean, }) CONFIG_SCHEMA = vol.Schema({ @@ -223,6 +227,7 @@ async def async_setup(hass, config): object_id=object_id, entity_ids=entity_ids, user_defined=False, + mode=service.data.get(ATTR_ALL), **extra_arg ) return @@ -265,6 +270,10 @@ async def async_setup(hass, config): group.view = service.data[ATTR_VIEW] need_update = True + if ATTR_ALL in service.data: + group.mode = all if service.data[ATTR_ALL] else any + need_update = True + if need_update: await group.async_update_ha_state() @@ -310,19 +319,21 @@ async def _async_process_config(hass, config, component): icon = conf.get(CONF_ICON) view = conf.get(CONF_VIEW) control = conf.get(CONF_CONTROL) + mode = conf.get(CONF_ALL) # Don't create tasks and await them all. The order is important as # groups get a number based on creation order. await Group.async_create_group( hass, name, entity_ids, icon=icon, view=view, - control=control, object_id=object_id) + control=control, object_id=object_id, mode=mode) class Group(Entity): """Track a group of entity ids.""" def __init__(self, hass, name, order=None, visible=True, icon=None, - view=False, control=None, user_defined=True, entity_ids=None): + view=False, control=None, user_defined=True, entity_ids=None, + mode=None): """Initialize a group. This Object has factory function for creation. @@ -341,6 +352,9 @@ class Group(Entity): self.visible = visible self.control = control self.user_defined = user_defined + self.mode = any + if mode: + self.mode = all self._order = order self._assumed_state = False self._async_unsub_state_changed = None @@ -348,18 +362,19 @@ class Group(Entity): @staticmethod def create_group(hass, name, entity_ids=None, user_defined=True, visible=True, icon=None, view=False, control=None, - object_id=None): + object_id=None, mode=None): """Initialize a group.""" return run_coroutine_threadsafe( Group.async_create_group( hass, name, entity_ids, user_defined, visible, icon, view, - control, object_id), + control, object_id, mode), hass.loop).result() @staticmethod async def async_create_group(hass, name, entity_ids=None, user_defined=True, visible=True, icon=None, - view=False, control=None, object_id=None): + view=False, control=None, object_id=None, + mode=None): """Initialize a group. This method must be run in the event loop. @@ -368,7 +383,7 @@ class Group(Entity): hass, name, order=len(hass.states.async_entity_ids(DOMAIN)), visible=visible, icon=icon, view=view, control=control, - user_defined=user_defined, entity_ids=entity_ids + user_defined=user_defined, entity_ids=entity_ids, mode=mode ) group.entity_id = async_generate_entity_id( @@ -557,13 +572,16 @@ class Group(Entity): if gr_on is None: return + # pylint: disable=too-many-boolean-expressions if tr_state is None or ((gr_state == gr_on and tr_state.state == gr_off) or + (gr_state == gr_off and + tr_state.state == gr_on) or tr_state.state not in (gr_on, gr_off)): if states is None: states = self._tracking_states - if any(state.state == gr_on for state in states): + if self.mode(state.state == gr_on for state in states): self._state = gr_on else: self._state = gr_off @@ -576,7 +594,7 @@ class Group(Entity): if states is None: states = self._tracking_states - self._assumed_state = any( + self._assumed_state = self.mode( state.attributes.get(ATTR_ASSUMED_STATE) for state in states) diff --git a/homeassistant/components/group/services.yaml b/homeassistant/components/group/services.yaml index f51f8b909d4..68c2f04f064 100644 --- a/homeassistant/components/group/services.yaml +++ b/homeassistant/components/group/services.yaml @@ -40,6 +40,9 @@ set: add_entities: description: List of members they will change on group listening. example: domain.entity_id1, domain.entity_id2 + all: + description: Enable this option if the group should only turn on when all entities are on. + example: True remove: description: Remove a user group. diff --git a/tests/components/group/test_init.py b/tests/components/group/test_init.py index 55c8a7778cb..104d1427dc9 100644 --- a/tests/components/group/test_init.py +++ b/tests/components/group/test_init.py @@ -108,6 +108,36 @@ class TestComponentsGroup(unittest.TestCase): group_state = self.hass.states.get(test_group.entity_id) self.assertEqual(STATE_ON, group_state.state) + def test_allgroup_stays_off_if_all_are_off_and_one_turns_on(self): + """Group with all: true, stay off if one device turns on.""" + self.hass.states.set('light.Bowl', STATE_OFF) + self.hass.states.set('light.Ceiling', STATE_OFF) + test_group = group.Group.create_group( + self.hass, 'init_group', ['light.Bowl', 'light.Ceiling'], False, + mode=True) + + # Turn one on + self.hass.states.set('light.Ceiling', STATE_ON) + self.hass.block_till_done() + + group_state = self.hass.states.get(test_group.entity_id) + self.assertEqual(STATE_OFF, group_state.state) + + def test_allgroup_turn_on_if_last_turns_on(self): + """Group with all: true, turn on if all devices are on.""" + self.hass.states.set('light.Bowl', STATE_ON) + self.hass.states.set('light.Ceiling', STATE_OFF) + test_group = group.Group.create_group( + self.hass, 'init_group', ['light.Bowl', 'light.Ceiling'], False, + mode=True) + + # Turn one on + self.hass.states.set('light.Ceiling', STATE_ON) + self.hass.block_till_done() + + group_state = self.hass.states.get(test_group.entity_id) + self.assertEqual(STATE_ON, group_state.state) + def test_is_on(self): """Test is_on method.""" self.hass.states.set('light.Bowl', STATE_ON)