From 03970764d81f4ed87673e737625d6b3f0f31a598 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 2 Mar 2018 02:14:26 +0100 Subject: [PATCH] Add light.group platform (#12229) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * Add grouped_light platform * 📝 Fix Lint issues * 🎨 Reformat code with yapf * A Few changes * ✨ Python 3.5 magic * Improvements Included the comments from #11323 * Fixes * Updates * Fixes & Tests * Fix bad-whitespace * Domain Config Validation ... by rebasing onto #12592 * Style changes & Improvements * Lint * Changes according to Review Comments * Use blocking light.async_turn_* * Revert "Use blocking light.async_turn_*" This reverts commit 9e83198552af9347aede9efb547f91793275cc5f. * Update service calls and state reporting * Add group service call tests * Remove unused constant. --- .coveragerc | 1 + homeassistant/components/light/__init__.py | 17 +- homeassistant/components/light/group.py | 289 ++++++++++++++ tests/components/light/test_group.py | 417 +++++++++++++++++++++ 4 files changed, 720 insertions(+), 4 deletions(-) create mode 100644 homeassistant/components/light/group.py create mode 100644 tests/components/light/test_group.py diff --git a/.coveragerc b/.coveragerc index 640db5765a2..cb57afd317e 100644 --- a/.coveragerc +++ b/.coveragerc @@ -409,6 +409,7 @@ omit = homeassistant/components/light/decora_wifi.py homeassistant/components/light/flux_led.py homeassistant/components/light/greenwave.py + homeassistant/components/light/group.py homeassistant/components/light/hue.py homeassistant/components/light/hyperion.py homeassistant/components/light/iglo.py diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 14e86eeb1fb..d7862f81975 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -12,7 +12,8 @@ import os import voluptuous as vol -from homeassistant.components import group +from homeassistant.components.group import \ + ENTITY_ID_FORMAT as GROUP_ENTITY_ID_FORMAT from homeassistant.const import ( ATTR_ENTITY_ID, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, STATE_ON) @@ -30,7 +31,7 @@ DEPENDENCIES = ['group'] SCAN_INTERVAL = timedelta(seconds=30) GROUP_NAME_ALL_LIGHTS = 'all lights' -ENTITY_ID_ALL_LIGHTS = group.ENTITY_ID_FORMAT.format('all_lights') +ENTITY_ID_ALL_LIGHTS = GROUP_ENTITY_ID_FORMAT.format('all_lights') ENTITY_ID_FORMAT = DOMAIN + '.{}' @@ -209,8 +210,9 @@ def async_turn_off(hass, entity_id=None, transition=None): DOMAIN, SERVICE_TURN_OFF, data)) +@callback @bind_hass -def toggle(hass, entity_id=None, transition=None): +def async_toggle(hass, entity_id=None, transition=None): """Toggle all or specified light.""" data = { key: value for key, value in [ @@ -219,7 +221,14 @@ def toggle(hass, entity_id=None, transition=None): ] if value is not None } - hass.services.call(DOMAIN, SERVICE_TOGGLE, data) + hass.async_add_job(hass.services.async_call( + DOMAIN, SERVICE_TOGGLE, data)) + + +@bind_hass +def toggle(hass, entity_id=None, transition=None): + """Toggle all or specified light.""" + hass.add_job(async_toggle, hass, entity_id, transition) def preprocess_turn_on_alternatives(params): diff --git a/homeassistant/components/light/group.py b/homeassistant/components/light/group.py new file mode 100644 index 00000000000..15e874db8f4 --- /dev/null +++ b/homeassistant/components/light/group.py @@ -0,0 +1,289 @@ +""" +This component allows several lights to be grouped into one light. + +For more details about this platform, please refer to the documentation at +https://home-assistant.io/components/light.group/ +""" +import logging +import itertools +from typing import List, Tuple, Optional, Iterator, Any, Callable +from collections import Counter + +import voluptuous as vol + +from homeassistant.core import State, callback +from homeassistant.components import light +from homeassistant.const import (STATE_ON, ATTR_ENTITY_ID, CONF_NAME, + CONF_ENTITIES, STATE_UNAVAILABLE, + ATTR_SUPPORTED_FEATURES) +from homeassistant.helpers.event import async_track_state_change +from homeassistant.helpers.typing import HomeAssistantType, ConfigType +from homeassistant.components.light import ( + SUPPORT_BRIGHTNESS, SUPPORT_RGB_COLOR, SUPPORT_COLOR_TEMP, + SUPPORT_TRANSITION, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_XY_COLOR, + SUPPORT_WHITE_VALUE, PLATFORM_SCHEMA, ATTR_BRIGHTNESS, ATTR_XY_COLOR, + ATTR_RGB_COLOR, ATTR_WHITE_VALUE, ATTR_COLOR_TEMP, ATTR_MIN_MIREDS, + ATTR_MAX_MIREDS, ATTR_EFFECT_LIST, ATTR_EFFECT, ATTR_FLASH, + ATTR_TRANSITION) +import homeassistant.helpers.config_validation as cv + +_LOGGER = logging.getLogger(__name__) + +DEFAULT_NAME = 'Group Light' + +PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend({ + vol.Optional(CONF_NAME, default=DEFAULT_NAME): cv.string, + vol.Required(CONF_ENTITIES): cv.entities_domain('light') +}) + +SUPPORT_GROUP_LIGHT = (SUPPORT_BRIGHTNESS | SUPPORT_COLOR_TEMP | SUPPORT_EFFECT + | SUPPORT_FLASH | SUPPORT_RGB_COLOR | SUPPORT_TRANSITION + | SUPPORT_XY_COLOR | SUPPORT_WHITE_VALUE) + + +async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, + async_add_devices, discovery_info=None) -> None: + """Initialize light.group platform.""" + async_add_devices([GroupLight(config.get(CONF_NAME), + config[CONF_ENTITIES])]) + + +class GroupLight(light.Light): + """Representation of a group light.""" + + def __init__(self, name: str, entity_ids: List[str]) -> None: + """Initialize a group light.""" + self._name = name # type: str + self._entity_ids = entity_ids # type: List[str] + self._is_on = False # type: bool + self._available = False # type: bool + self._brightness = None # type: Optional[int] + self._xy_color = None # type: Optional[Tuple[float, float]] + self._rgb_color = None # type: Optional[Tuple[int, int, int]] + self._color_temp = None # type: Optional[int] + self._min_mireds = 154 # type: Optional[int] + self._max_mireds = 500 # type: Optional[int] + self._white_value = None # type: Optional[int] + self._effect_list = None # type: Optional[List[str]] + self._effect = None # type: Optional[str] + self._supported_features = 0 # type: int + self._async_unsub_state_changed = None + + async def async_added_to_hass(self) -> None: + """Register callbacks.""" + @callback + def async_state_changed_listener(entity_id: str, old_state: State, + new_state: State): + """Handle child updates.""" + self.async_schedule_update_ha_state(True) + + self._async_unsub_state_changed = async_track_state_change( + self.hass, self._entity_ids, async_state_changed_listener) + + async def async_will_remove_from_hass(self): + """Callback when removed from HASS.""" + if self._async_unsub_state_changed: + self._async_unsub_state_changed() + self._async_unsub_state_changed = None + + @property + def name(self) -> str: + """Return the name of the entity.""" + return self._name + + @property + def is_on(self) -> bool: + """Return the on/off state of the light.""" + return self._is_on + + @property + def available(self) -> bool: + """Return whether the light is available.""" + return self._available + + @property + def brightness(self) -> Optional[int]: + """Return the brightness of this light between 0..255.""" + return self._brightness + + @property + def xy_color(self) -> Optional[Tuple[float, float]]: + """Return the XY color value [float, float].""" + return self._xy_color + + @property + def rgb_color(self) -> Optional[Tuple[int, int, int]]: + """Return the RGB color value [int, int, int].""" + return self._rgb_color + + @property + def color_temp(self) -> Optional[int]: + """Return the CT color value in mireds.""" + return self._color_temp + + @property + def min_mireds(self) -> Optional[int]: + """Return the coldest color_temp that this light supports.""" + return self._min_mireds + + @property + def max_mireds(self) -> Optional[int]: + """Return the warmest color_temp that this light supports.""" + return self._max_mireds + + @property + def white_value(self) -> Optional[int]: + """Return the white value of this light between 0..255.""" + return self._white_value + + @property + def effect_list(self) -> Optional[List[str]]: + """Return the list of supported effects.""" + return self._effect_list + + @property + def effect(self) -> Optional[str]: + """Return the current effect.""" + return self._effect + + @property + def supported_features(self) -> int: + """Flag supported features.""" + return self._supported_features + + @property + def should_poll(self) -> bool: + """No polling needed for a group light.""" + return False + + async def async_turn_on(self, **kwargs): + """Forward the turn_on command to all lights in the group.""" + data = {ATTR_ENTITY_ID: self._entity_ids} + + if ATTR_BRIGHTNESS in kwargs: + data[ATTR_BRIGHTNESS] = kwargs[ATTR_BRIGHTNESS] + + if ATTR_XY_COLOR in kwargs: + data[ATTR_XY_COLOR] = kwargs[ATTR_XY_COLOR] + + if ATTR_RGB_COLOR in kwargs: + data[ATTR_RGB_COLOR] = kwargs[ATTR_RGB_COLOR] + + if ATTR_COLOR_TEMP in kwargs: + data[ATTR_COLOR_TEMP] = kwargs[ATTR_COLOR_TEMP] + + if ATTR_WHITE_VALUE in kwargs: + data[ATTR_WHITE_VALUE] = kwargs[ATTR_WHITE_VALUE] + + if ATTR_EFFECT in kwargs: + data[ATTR_EFFECT] = kwargs[ATTR_EFFECT] + + if ATTR_TRANSITION in kwargs: + data[ATTR_TRANSITION] = kwargs[ATTR_TRANSITION] + + if ATTR_FLASH in kwargs: + data[ATTR_FLASH] = kwargs[ATTR_FLASH] + + await self.hass.services.async_call( + light.DOMAIN, light.SERVICE_TURN_ON, data, blocking=True) + + async def async_turn_off(self, **kwargs): + """Forward the turn_off command to all lights in the group.""" + data = {ATTR_ENTITY_ID: self._entity_ids} + + if ATTR_TRANSITION in kwargs: + data[ATTR_TRANSITION] = kwargs[ATTR_TRANSITION] + + await self.hass.services.async_call( + light.DOMAIN, light.SERVICE_TURN_OFF, data, blocking=True) + + async def async_update(self): + """Query all members and determine the group state.""" + all_states = [self.hass.states.get(x) for x in self._entity_ids] + states = list(filter(None, all_states)) + on_states = [state for state in states if state.state == STATE_ON] + + self._is_on = len(on_states) > 0 + self._available = any(state.state != STATE_UNAVAILABLE + for state in states) + + self._brightness = _reduce_attribute(on_states, ATTR_BRIGHTNESS) + + self._xy_color = _reduce_attribute( + on_states, ATTR_XY_COLOR, reduce=_mean_tuple) + + self._rgb_color = _reduce_attribute( + on_states, ATTR_RGB_COLOR, reduce=_mean_tuple) + if self._rgb_color is not None: + self._rgb_color = tuple(map(int, self._rgb_color)) + + self._white_value = _reduce_attribute(on_states, ATTR_WHITE_VALUE) + + self._color_temp = _reduce_attribute(on_states, ATTR_COLOR_TEMP) + self._min_mireds = _reduce_attribute( + states, ATTR_MIN_MIREDS, default=154, reduce=min) + self._max_mireds = _reduce_attribute( + states, ATTR_MAX_MIREDS, default=500, reduce=max) + + self._effect_list = None + all_effect_lists = list( + _find_state_attributes(states, ATTR_EFFECT_LIST)) + if all_effect_lists: + # Merge all effects from all effect_lists with a union merge. + self._effect_list = list(set().union(*all_effect_lists)) + + self._effect = None + all_effects = list(_find_state_attributes(on_states, ATTR_EFFECT)) + if all_effects: + # Report the most common effect. + effects_count = Counter(itertools.chain(all_effects)) + self._effect = effects_count.most_common(1)[0][0] + + self._supported_features = 0 + for support in _find_state_attributes(states, ATTR_SUPPORTED_FEATURES): + # Merge supported features by emulating support for every feature + # we find. + self._supported_features |= support + # Bitwise-and the supported features with the GroupedLight's features + # so that we don't break in the future when a new feature is added. + self._supported_features &= SUPPORT_GROUP_LIGHT + + +def _find_state_attributes(states: List[State], + key: str) -> Iterator[Any]: + """Find attributes with matching key from states.""" + for state in states: + value = state.attributes.get(key) + if value is not None: + yield value + + +def _mean_int(*args): + """Return the mean of the supplied values.""" + return int(sum(args) / len(args)) + + +def _mean_tuple(*args): + """Return the mean values along the columns of the supplied values.""" + return tuple(sum(l) / len(l) for l in zip(*args)) + + +# https://github.com/PyCQA/pylint/issues/1831 +# pylint: disable=bad-whitespace +def _reduce_attribute(states: List[State], + key: str, + default: Optional[Any] = None, + reduce: Callable[..., Any] = _mean_int) -> Any: + """Find the first attribute matching key from states. + + If none are found, return default. + """ + attrs = list(_find_state_attributes(states, key)) + + if not attrs: + return default + + if len(attrs) == 1: + return attrs[0] + + return reduce(*attrs) diff --git a/tests/components/light/test_group.py b/tests/components/light/test_group.py new file mode 100644 index 00000000000..ac19f407066 --- /dev/null +++ b/tests/components/light/test_group.py @@ -0,0 +1,417 @@ +"""The tests for the Group Light platform.""" +from unittest.mock import MagicMock + +import asynctest + +from homeassistant.components import light +from homeassistant.components.light import group +from homeassistant.setup import async_setup_component + + +async def test_default_state(hass): + """Test light group default state.""" + await async_setup_component(hass, 'light', {'light': { + 'platform': 'group', 'entities': [], 'name': 'Bedroom Group' + }}) + await hass.async_block_till_done() + + state = hass.states.get('light.bedroom_group') + assert state is not None + assert state.state == 'unavailable' + assert state.attributes['supported_features'] == 0 + assert state.attributes.get('brightness') is None + assert state.attributes.get('rgb_color') is None + assert state.attributes.get('xy_color') is None + assert state.attributes.get('color_temp') is None + assert state.attributes.get('white_value') is None + assert state.attributes.get('effect_list') is None + assert state.attributes.get('effect') is None + + +async def test_state_reporting(hass): + """Test the state reporting.""" + await async_setup_component(hass, 'light', {'light': { + 'platform': 'group', 'entities': ['light.test1', 'light.test2'] + }}) + + hass.states.async_set('light.test1', 'on') + hass.states.async_set('light.test2', 'unavailable') + await hass.async_block_till_done() + assert hass.states.get('light.group_light').state == 'on' + + hass.states.async_set('light.test1', 'on') + hass.states.async_set('light.test2', 'off') + await hass.async_block_till_done() + assert hass.states.get('light.group_light').state == 'on' + + hass.states.async_set('light.test1', 'off') + hass.states.async_set('light.test2', 'off') + await hass.async_block_till_done() + assert hass.states.get('light.group_light').state == 'off' + + hass.states.async_set('light.test1', 'unavailable') + hass.states.async_set('light.test2', 'unavailable') + await hass.async_block_till_done() + assert hass.states.get('light.group_light').state == 'unavailable' + + +async def test_brightness(hass): + """Test brightness reporting.""" + await async_setup_component(hass, 'light', {'light': { + 'platform': 'group', 'entities': ['light.test1', 'light.test2'] + }}) + + hass.states.async_set('light.test1', 'on', + {'brightness': 255, 'supported_features': 1}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.state == 'on' + assert state.attributes['supported_features'] == 1 + assert state.attributes['brightness'] == 255 + + hass.states.async_set('light.test2', 'on', + {'brightness': 100, 'supported_features': 1}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.state == 'on' + assert state.attributes['brightness'] == 177 + + hass.states.async_set('light.test1', 'off', + {'brightness': 255, 'supported_features': 1}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.state == 'on' + assert state.attributes['supported_features'] == 1 + assert state.attributes['brightness'] == 100 + + +async def test_xy_color(hass): + """Test XY reporting.""" + await async_setup_component(hass, 'light', {'light': { + 'platform': 'group', 'entities': ['light.test1', 'light.test2'] + }}) + + hass.states.async_set('light.test1', 'on', + {'xy_color': (1.0, 1.0), 'supported_features': 64}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.state == 'on' + assert state.attributes['supported_features'] == 64 + assert state.attributes['xy_color'] == (1.0, 1.0) + + hass.states.async_set('light.test2', 'on', + {'xy_color': (0.5, 0.5), 'supported_features': 64}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.state == 'on' + assert state.attributes['xy_color'] == (0.75, 0.75) + + hass.states.async_set('light.test1', 'off', + {'xy_color': (1.0, 1.0), 'supported_features': 64}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.state == 'on' + assert state.attributes['xy_color'] == (0.5, 0.5) + + +async def test_rgb_color(hass): + """Test RGB reporting.""" + await async_setup_component(hass, 'light', {'light': { + 'platform': 'group', 'entities': ['light.test1', 'light.test2'] + }}) + + hass.states.async_set('light.test1', 'on', + {'rgb_color': (255, 0, 0), 'supported_features': 16}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.state == 'on' + assert state.attributes['supported_features'] == 16 + assert state.attributes['rgb_color'] == (255, 0, 0) + + hass.states.async_set('light.test2', 'on', + {'rgb_color': (255, 255, 255), + 'supported_features': 16}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.attributes['rgb_color'] == (255, 127, 127) + + hass.states.async_set('light.test1', 'off', + {'rgb_color': (255, 0, 0), 'supported_features': 16}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.attributes['rgb_color'] == (255, 255, 255) + + +async def test_white_value(hass): + """Test white value reporting.""" + await async_setup_component(hass, 'light', {'light': { + 'platform': 'group', 'entities': ['light.test1', 'light.test2'] + }}) + + hass.states.async_set('light.test1', 'on', + {'white_value': 255, 'supported_features': 128}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.attributes['white_value'] == 255 + + hass.states.async_set('light.test2', 'on', + {'white_value': 100, 'supported_features': 128}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.attributes['white_value'] == 177 + + hass.states.async_set('light.test1', 'off', + {'white_value': 255, 'supported_features': 128}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.attributes['white_value'] == 100 + + +async def test_color_temp(hass): + """Test color temp reporting.""" + await async_setup_component(hass, 'light', {'light': { + 'platform': 'group', 'entities': ['light.test1', 'light.test2'] + }}) + + hass.states.async_set('light.test1', 'on', + {'color_temp': 2, 'supported_features': 2}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.attributes['color_temp'] == 2 + + hass.states.async_set('light.test2', 'on', + {'color_temp': 1000, 'supported_features': 2}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.attributes['color_temp'] == 501 + + hass.states.async_set('light.test1', 'off', + {'color_temp': 2, 'supported_features': 2}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.attributes['color_temp'] == 1000 + + +async def test_min_max_mireds(hass): + """Test min/max mireds reporting.""" + await async_setup_component(hass, 'light', {'light': { + 'platform': 'group', 'entities': ['light.test1', 'light.test2'] + }}) + + hass.states.async_set('light.test1', 'on', + {'min_mireds': 2, 'max_mireds': 5, + 'supported_features': 2}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.attributes['min_mireds'] == 2 + assert state.attributes['max_mireds'] == 5 + + hass.states.async_set('light.test2', 'on', + {'min_mireds': 7, 'max_mireds': 1234567890, + 'supported_features': 2}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.attributes['min_mireds'] == 2 + assert state.attributes['max_mireds'] == 1234567890 + + hass.states.async_set('light.test1', 'off', + {'min_mireds': 1, 'max_mireds': 2, + 'supported_features': 2}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.attributes['min_mireds'] == 1 + assert state.attributes['max_mireds'] == 1234567890 + + +async def test_effect_list(hass): + """Test effect_list reporting.""" + await async_setup_component(hass, 'light', {'light': { + 'platform': 'group', 'entities': ['light.test1', 'light.test2'] + }}) + + hass.states.async_set('light.test1', 'on', + {'effect_list': ['None', 'Random', 'Colorloop']}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert set(state.attributes['effect_list']) == { + 'None', 'Random', 'Colorloop'} + + hass.states.async_set('light.test2', 'on', + {'effect_list': ['None', 'Random', 'Rainbow']}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert set(state.attributes['effect_list']) == { + 'None', 'Random', 'Colorloop', 'Rainbow'} + + hass.states.async_set('light.test1', 'off', + {'effect_list': ['None', 'Colorloop', 'Seven']}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert set(state.attributes['effect_list']) == { + 'None', 'Random', 'Colorloop', 'Seven', 'Rainbow'} + + +async def test_effect(hass): + """Test effect reporting.""" + await async_setup_component(hass, 'light', {'light': { + 'platform': 'group', 'entities': ['light.test1', 'light.test2', + 'light.test3'] + }}) + + hass.states.async_set('light.test1', 'on', + {'effect': 'None', 'supported_features': 2}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.attributes['effect'] == 'None' + + hass.states.async_set('light.test2', 'on', + {'effect': 'None', 'supported_features': 2}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.attributes['effect'] == 'None' + + hass.states.async_set('light.test3', 'on', + {'effect': 'Random', 'supported_features': 2}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.attributes['effect'] == 'None' + + hass.states.async_set('light.test1', 'off', + {'effect': 'None', 'supported_features': 2}) + hass.states.async_set('light.test2', 'off', + {'effect': 'None', 'supported_features': 2}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.attributes['effect'] == 'Random' + + +async def test_supported_features(hass): + """Test supported features reporting.""" + await async_setup_component(hass, 'light', {'light': { + 'platform': 'group', 'entities': ['light.test1', 'light.test2'] + }}) + + hass.states.async_set('light.test1', 'on', + {'supported_features': 0}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.attributes['supported_features'] == 0 + + hass.states.async_set('light.test2', 'on', + {'supported_features': 2}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.attributes['supported_features'] == 2 + + hass.states.async_set('light.test1', 'off', + {'supported_features': 41}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.attributes['supported_features'] == 43 + + hass.states.async_set('light.test2', 'off', + {'supported_features': 256}) + await hass.async_block_till_done() + state = hass.states.get('light.group_light') + assert state.attributes['supported_features'] == 41 + + +async def test_service_calls(hass): + """Test service calls.""" + await async_setup_component(hass, 'light', {'light': [ + {'platform': 'demo'}, + {'platform': 'group', 'entities': ['light.bed_light', + 'light.ceiling_lights', + 'light.kitchen_lights']} + ]}) + await hass.async_block_till_done() + + assert hass.states.get('light.group_light').state == 'on' + light.async_toggle(hass, 'light.group_light') + await hass.async_block_till_done() + + assert hass.states.get('light.bed_light').state == 'off' + assert hass.states.get('light.ceiling_lights').state == 'off' + assert hass.states.get('light.kitchen_lights').state == 'off' + + light.async_turn_on(hass, 'light.group_light') + await hass.async_block_till_done() + + assert hass.states.get('light.bed_light').state == 'on' + assert hass.states.get('light.ceiling_lights').state == 'on' + assert hass.states.get('light.kitchen_lights').state == 'on' + + light.async_turn_off(hass, 'light.group_light') + await hass.async_block_till_done() + + assert hass.states.get('light.bed_light').state == 'off' + assert hass.states.get('light.ceiling_lights').state == 'off' + assert hass.states.get('light.kitchen_lights').state == 'off' + + light.async_turn_on(hass, 'light.group_light', brightness=128, + effect='Random', rgb_color=(42, 255, 255)) + await hass.async_block_till_done() + + state = hass.states.get('light.bed_light') + assert state.state == 'on' + assert state.attributes['brightness'] == 128 + assert state.attributes['effect'] == 'Random' + assert state.attributes['rgb_color'] == (42, 255, 255) + + state = hass.states.get('light.ceiling_lights') + assert state.state == 'on' + assert state.attributes['brightness'] == 128 + assert state.attributes['effect'] == 'Random' + assert state.attributes['rgb_color'] == (42, 255, 255) + + state = hass.states.get('light.kitchen_lights') + assert state.state == 'on' + assert state.attributes['brightness'] == 128 + assert state.attributes['effect'] == 'Random' + assert state.attributes['rgb_color'] == (42, 255, 255) + + +async def test_invalid_service_calls(hass): + """Test invalid service call arguments get discarded.""" + add_devices = MagicMock() + await group.async_setup_platform(hass, { + 'entities': ['light.test1', 'light.test2'] + }, add_devices) + + assert add_devices.call_count == 1 + grouped_light = add_devices.call_args[0][0][0] + grouped_light.hass = hass + + with asynctest.patch.object(hass.services, 'async_call') as mock_call: + await grouped_light.async_turn_on(brightness=150, four_oh_four='404') + data = { + 'entity_id': ['light.test1', 'light.test2'], + 'brightness': 150 + } + mock_call.assert_called_once_with('light', 'turn_on', data, + blocking=True) + mock_call.reset_mock() + + await grouped_light.async_turn_off(transition=4, four_oh_four='404') + data = { + 'entity_id': ['light.test1', 'light.test2'], + 'transition': 4 + } + mock_call.assert_called_once_with('light', 'turn_off', data, + blocking=True) + mock_call.reset_mock() + + data = { + 'brightness': 150, + 'xy_color': (0.5, 0.42), + 'rgb_color': (80, 120, 50), + 'color_temp': 1234, + 'white_value': 1, + 'effect': 'Sunshine', + 'transition': 4, + 'flash': 'long' + } + await grouped_light.async_turn_on(**data) + data['entity_id'] = ['light.test1', 'light.test2'] + mock_call.assert_called_once_with('light', 'turn_on', data, + blocking=True)