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
This commit is contained in:
Daniel Perna 2018-10-09 10:14:55 +02:00 committed by Paulus Schoutsen
parent 26cf5acd5b
commit 9d56730b8d
3 changed files with 59 additions and 8 deletions

View File

@ -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)

View File

@ -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.

View File

@ -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)