From 1eab314b1c1034bab2dde7d697620282c2489b92 Mon Sep 17 00:00:00 2001 From: Otto Winter Date: Fri, 23 Feb 2018 10:44:32 +0100 Subject: [PATCH] Fixes & Tests --- homeassistant/components/light/__init__.py | 12 +- homeassistant/components/light/group.py | 64 ++-- tests/components/light/test_group.py | 371 +++++++++++++++++++++ 3 files changed, 417 insertions(+), 30 deletions(-) create mode 100644 tests/components/light/test_group.py diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index b7e8966394e..3357de1ff53 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -206,8 +206,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 [ @@ -216,7 +217,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 index f0d1843b56d..e64539baf93 100644 --- a/homeassistant/components/light/group.py +++ b/homeassistant/components/light/group.py @@ -18,13 +18,15 @@ from homeassistant.components import light from homeassistant.const import (STATE_OFF, STATE_ON, SERVICE_TURN_ON, SERVICE_TURN_OFF, ATTR_ENTITY_ID, CONF_NAME, CONF_ENTITIES, STATE_UNAVAILABLE, - STATE_UNKNOWN, EVENT_HOMEASSISTANT_START) + STATE_UNKNOWN, 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) + 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) import homeassistant.helpers.config_validation as cv _LOGGER = logging.getLogger(__name__) @@ -47,7 +49,7 @@ async def async_setup_platform(hass: HomeAssistantType, config: ConfigType, async_add_devices, discovery_info=None) -> None: """Initialize light.group platform.""" async_add_devices( - [GroupLight(hass, config.get(CONF_NAME), config[CONF_ENTITIES])]) + [GroupLight(hass, config.get(CONF_NAME), config[CONF_ENTITIES])], True) class GroupLight(light.Light): @@ -70,26 +72,24 @@ class GroupLight(light.Light): 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""" - + """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) - @callback - def async_homeassistant_start(event): - """Track child state changes on startup.""" - async_track_state_change(self.hass, self._entity_ids, - async_state_changed_listener) + self._async_unsub_state_changed = async_track_state_change( + self.hass, self._entity_ids, async_state_changed_listener) - self.async_schedule_update_ha_state(True) - - self.hass.bus.async_listen_once(EVENT_HOMEASSISTANT_START, - async_homeassistant_start) + 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: @@ -168,7 +168,7 @@ class GroupLight(light.Light): payload = deepcopy(kwargs) payload[ATTR_ENTITY_ID] = entity_id tasks.append(self.hass.services.async_call( - 'light', service, payload, blocking=True)) + light.DOMAIN, service, payload, blocking=True)) if tasks: await asyncio.wait(tasks, loop=self.hass.loop) @@ -188,35 +188,40 @@ class GroupLight(light.Light): self._state = _determine_on_off_state(states) - self._brightness = _reduce_attribute(on_states, 'brightness') + self._brightness = _reduce_attribute(on_states, ATTR_BRIGHTNESS) + self._xy_color = _reduce_attribute( - on_states, 'xy_color', reduce=_average_tuple) + on_states, ATTR_XY_COLOR, reduce=_average_tuple) + self._rgb_color = _reduce_attribute( - on_states, 'rgb_color', reduce=_average_tuple) + on_states, ATTR_RGB_COLOR, reduce=_average_tuple) if self._rgb_color is not None: self._rgb_color = tuple(map(int, self._rgb_color)) - self._white_value = _reduce_attribute(on_states, 'white_value') - self._color_temp = _reduce_attribute(on_states, 'color_temp') + + 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, 'min_mireds', default=154, reduce=min) + states, ATTR_MIN_MIREDS, default=154, reduce=min) self._max_mireds = _reduce_attribute( - states, 'max_mireds', default=500, reduce=max) + states, ATTR_MAX_MIREDS, default=500, reduce=max) self._effect_list = None - all_effect_lists = list(_find_state_attributes(states, 'effect_list')) + 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(states, 'effect')) + 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, 'supported_features'): + 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 @@ -239,9 +244,9 @@ def _find_state_attributes(states: List[State], yield value -def _average(*args): +def _average_int(*args): """Return the average of the supplied values.""" - return sum(args) / len(args) + return int(sum(args) / len(args)) def _average_tuple(*args): @@ -252,7 +257,7 @@ def _average_tuple(*args): def _reduce_attribute(states: List[State], key: str, default: Optional[Any] = None, - reduce: Callable[..., Any] = _average) -> Any: + reduce: Callable[..., Any] = _average_int) -> Any: """Find the first attribute matching key from states. If none are found, return default. @@ -262,6 +267,9 @@ def _reduce_attribute(states: List[State], 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..d1a63fad664 --- /dev/null +++ b/tests/components/light/test_group.py @@ -0,0 +1,371 @@ +"""The tests for the Group Light platform.""" +from homeassistant.components import light +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', 'unknown') + hass.states.async_set('light.test2', 'unknown') + await hass.async_block_till_done() + assert hass.states.get('light.group_light').state == 'unknown' + + 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)