diff --git a/homeassistant/components/group/light.py b/homeassistant/components/group/light.py index 26cc8e1c11c..84c218b5d72 100644 --- a/homeassistant/components/group/light.py +++ b/homeassistant/components/group/light.py @@ -12,6 +12,7 @@ import voluptuous as vol from homeassistant.components import light from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_EFFECT_LIST, @@ -19,16 +20,20 @@ from homeassistant.components.light import ( ATTR_HS_COLOR, ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, + ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_WHITE_VALUE, + ATTR_XY_COLOR, PLATFORM_SCHEMA, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, SUPPORT_WHITE_VALUE, + color_supported, + color_temp_supported, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -59,13 +64,7 @@ PLATFORM_SCHEMA = PLATFORM_SCHEMA.extend( ) SUPPORT_GROUP_LIGHT = ( - SUPPORT_BRIGHTNESS - | SUPPORT_COLOR_TEMP - | SUPPORT_EFFECT - | SUPPORT_FLASH - | SUPPORT_COLOR - | SUPPORT_TRANSITION - | SUPPORT_WHITE_VALUE + SUPPORT_EFFECT | SUPPORT_FLASH | SUPPORT_TRANSITION | SUPPORT_WHITE_VALUE ) @@ -89,13 +88,19 @@ class LightGroup(GroupEntity, light.LightEntity): self._available = False self._icon = "mdi:lightbulb-group" self._brightness: int | None = None + self._color_mode: str | None = None self._hs_color: tuple[float, float] | None = None + self._rgb_color: tuple[int, int, int] | None = None + self._rgbw_color: tuple[int, int, int, int] | None = None + self._rgbww_color: tuple[int, int, int, int, int] | None = None + self._xy_color: tuple[float, float] | None = None self._color_temp: int | None = None self._min_mireds: int = 154 self._max_mireds: int = 500 self._white_value: int | None = None self._effect_list: list[str] | None = None self._effect: str | None = None + self._supported_color_modes: set[str] | None = None self._supported_features: int = 0 async def async_added_to_hass(self) -> None: @@ -143,11 +148,36 @@ class LightGroup(GroupEntity, light.LightEntity): """Return the brightness of this light group between 0..255.""" return self._brightness + @property + def color_mode(self) -> str | None: + """Return the color mode of the light.""" + return self._color_mode + @property def hs_color(self) -> tuple[float, float] | None: """Return the HS color value [float, float].""" return self._hs_color + @property + def rgb_color(self) -> tuple[int, int, int] | None: + """Return the rgb color value [int, int, int].""" + return self._rgb_color + + @property + def rgbw_color(self) -> tuple[int, int, int, int] | None: + """Return the rgbw color value [int, int, int, int].""" + return self._rgbw_color + + @property + def rgbww_color(self) -> tuple[int, int, int, int, int] | None: + """Return the rgbww color value [int, int, int, int, int].""" + return self._rgbww_color + + @property + def xy_color(self) -> tuple[float, float] | None: + """Return the xy color value [float, float].""" + return self._xy_color + @property def color_temp(self) -> int | None: """Return the CT color value in mireds.""" @@ -178,6 +208,11 @@ class LightGroup(GroupEntity, light.LightEntity): """Return the current effect.""" return self._effect + @property + def supported_color_modes(self) -> set | None: + """Flag supported color modes.""" + return self._supported_color_modes + @property def supported_features(self) -> int: """Flag supported features.""" @@ -204,6 +239,18 @@ class LightGroup(GroupEntity, light.LightEntity): if ATTR_HS_COLOR in kwargs: data[ATTR_HS_COLOR] = kwargs[ATTR_HS_COLOR] + if ATTR_RGB_COLOR in kwargs: + data[ATTR_RGB_COLOR] = kwargs[ATTR_RGB_COLOR] + + if ATTR_RGBW_COLOR in kwargs: + data[ATTR_RGBW_COLOR] = kwargs[ATTR_RGBW_COLOR] + + if ATTR_RGBWW_COLOR in kwargs: + data[ATTR_RGBWW_COLOR] = kwargs[ATTR_RGBWW_COLOR] + + if ATTR_XY_COLOR in kwargs: + data[ATTR_XY_COLOR] = kwargs[ATTR_XY_COLOR] + if ATTR_COLOR_TEMP in kwargs: data[ATTR_COLOR_TEMP] = kwargs[ATTR_COLOR_TEMP] @@ -215,11 +262,9 @@ class LightGroup(GroupEntity, light.LightEntity): state = self.hass.states.get(entity_id) if not state: continue - support = state.attributes.get(ATTR_SUPPORTED_FEATURES) + support = state.attributes.get(ATTR_SUPPORTED_COLOR_MODES) # Only pass color temperature to supported entity_ids - if bool(support & SUPPORT_COLOR) and not bool( - support & SUPPORT_COLOR_TEMP - ): + if color_supported(support) and not color_temp_supported(support): emulate_color_temp_entity_ids.append(entity_id) updated_entities.remove(entity_id) data[ATTR_ENTITY_ID] = updated_entities @@ -300,6 +345,16 @@ class LightGroup(GroupEntity, light.LightEntity): self._brightness = _reduce_attribute(on_states, ATTR_BRIGHTNESS) self._hs_color = _reduce_attribute(on_states, ATTR_HS_COLOR, reduce=_mean_tuple) + self._rgb_color = _reduce_attribute( + on_states, ATTR_RGB_COLOR, reduce=_mean_tuple + ) + self._rgbw_color = _reduce_attribute( + on_states, ATTR_RGBW_COLOR, reduce=_mean_tuple + ) + self._rgbww_color = _reduce_attribute( + on_states, ATTR_RGBWW_COLOR, reduce=_mean_tuple + ) + self._xy_color = _reduce_attribute(on_states, ATTR_XY_COLOR, reduce=_mean_tuple) self._white_value = _reduce_attribute(on_states, ATTR_WHITE_VALUE) @@ -324,6 +379,21 @@ class LightGroup(GroupEntity, light.LightEntity): effects_count = Counter(itertools.chain(all_effects)) self._effect = effects_count.most_common(1)[0][0] + self._color_mode = None + all_color_modes = list(_find_state_attributes(on_states, ATTR_COLOR_MODE)) + if all_color_modes: + # Report the most common color mode. + color_mode_count = Counter(itertools.chain(all_color_modes)) + self._color_mode = color_mode_count.most_common(1)[0][0] + + self._supported_color_modes = None + all_supported_color_modes = list( + _find_state_attributes(states, ATTR_SUPPORTED_COLOR_MODES) + ) + if all_supported_color_modes: + # Merge all color modes. + self._supported_color_modes = set().union(*all_supported_color_modes) + self._supported_features = 0 for support in _find_state_attributes(states, ATTR_SUPPORTED_FEATURES): # Merge supported features by emulating support for every feature diff --git a/tests/components/group/test_light.py b/tests/components/group/test_light.py index 136da458f66..c9b861a46a9 100644 --- a/tests/components/group/test_light.py +++ b/tests/components/group/test_light.py @@ -8,6 +8,7 @@ from homeassistant.components.group import DOMAIN, SERVICE_RELOAD import homeassistant.components.group.light as group from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_EFFECT_LIST, @@ -16,13 +17,24 @@ from homeassistant.components.light import ( ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, + ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, DOMAIN as LIGHT_DOMAIN, SERVICE_TOGGLE, SERVICE_TURN_OFF, SERVICE_TURN_ON, + SUPPORT_BRIGHTNESS, + SUPPORT_COLOR, + SUPPORT_COLOR_TEMP, ) from homeassistant.const import ( ATTR_ENTITY_ID, @@ -104,85 +116,281 @@ async def test_state_reporting(hass): async def test_brightness(hass): """Test brightness reporting.""" - await async_setup_component( + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_BRIGHTNESS} + entity0.color_mode = COLOR_MODE_BRIGHTNESS + entity0.brightness = 255 + + entity1 = platform.ENTITIES[1] + entity1.supported_features = SUPPORT_BRIGHTNESS + + assert await async_setup_component( hass, LIGHT_DOMAIN, { - LIGHT_DOMAIN: { - "platform": DOMAIN, - "entities": ["light.test1", "light.test2"], - } + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + }, + ] }, ) await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() - hass.states.async_set( - "light.test1", STATE_ON, {ATTR_BRIGHTNESS: 255, ATTR_SUPPORTED_FEATURES: 1} - ) - await hass.async_block_till_done() state = hass.states.get("light.light_group") assert state.state == STATE_ON - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 1 assert state.attributes[ATTR_BRIGHTNESS] == 255 + assert state.attributes[ATTR_COLOR_MODE] == "brightness" + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["brightness"] - hass.states.async_set( - "light.test2", STATE_ON, {ATTR_BRIGHTNESS: 100, ATTR_SUPPORTED_FEATURES: 1} + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity1.entity_id], ATTR_BRIGHTNESS: 100}, + blocking=True, ) await hass.async_block_till_done() state = hass.states.get("light.light_group") assert state.state == STATE_ON assert state.attributes[ATTR_BRIGHTNESS] == 177 + assert state.attributes[ATTR_COLOR_MODE] == "brightness" + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["brightness"] - hass.states.async_set( - "light.test1", STATE_OFF, {ATTR_BRIGHTNESS: 255, ATTR_SUPPORTED_FEATURES: 1} + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id]}, + blocking=True, ) await hass.async_block_till_done() state = hass.states.get("light.light_group") assert state.state == STATE_ON - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 1 assert state.attributes[ATTR_BRIGHTNESS] == 100 + assert state.attributes[ATTR_COLOR_MODE] == "brightness" + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["brightness"] -async def test_color(hass): - """Test RGB reporting.""" - await async_setup_component( +async def test_color_hs(hass): + """Test hs color reporting.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_HS} + entity0.color_mode = COLOR_MODE_HS + entity0.brightness = 255 + entity0.hs_color = (0, 100) + + entity1 = platform.ENTITIES[1] + entity1.supported_features = SUPPORT_COLOR + + assert await async_setup_component( hass, LIGHT_DOMAIN, { - LIGHT_DOMAIN: { - "platform": DOMAIN, - "entities": ["light.test1", "light.test2"], - } + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + }, + ] }, ) await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() - hass.states.async_set( - "light.test1", STATE_ON, {ATTR_HS_COLOR: (0, 100), ATTR_SUPPORTED_FEATURES: 16} - ) - await hass.async_block_till_done() state = hass.states.get("light.light_group") assert state.state == STATE_ON - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 16 + assert state.attributes[ATTR_COLOR_MODE] == "hs" assert state.attributes[ATTR_HS_COLOR] == (0, 100) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["hs"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 - hass.states.async_set( - "light.test2", STATE_ON, {ATTR_HS_COLOR: (0, 50), ATTR_SUPPORTED_FEATURES: 16} + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity1.entity_id], ATTR_HS_COLOR: (0, 50)}, + blocking=True, ) await hass.async_block_till_done() state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "hs" assert state.attributes[ATTR_HS_COLOR] == (0, 75) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["hs"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 - hass.states.async_set( - "light.test1", STATE_OFF, {ATTR_HS_COLOR: (0, 0), ATTR_SUPPORTED_FEATURES: 16} + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id]}, + blocking=True, ) await hass.async_block_till_done() state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "hs" assert state.attributes[ATTR_HS_COLOR] == (0, 50) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["hs"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + +async def test_color_rgbw(hass): + """Test rgbw color reporting.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_RGBW} + entity0.color_mode = COLOR_MODE_RGBW + entity0.brightness = 255 + entity0.rgbw_color = (0, 64, 128, 255) + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {COLOR_MODE_RGBW} + entity1.color_mode = COLOR_MODE_RGBW + entity1.brightness = 255 + entity1.rgbw_color = (255, 128, 64, 0) + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.light_group") + assert state.state == STATE_ON + assert state.attributes[ATTR_COLOR_MODE] == "rgbw" + assert state.attributes[ATTR_RGBW_COLOR] == (0, 64, 128, 255) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgbw"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity1.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "rgbw" + assert state.attributes[ATTR_RGBW_COLOR] == (127, 96, 96, 127) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgbw"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "rgbw" + assert state.attributes[ATTR_RGBW_COLOR] == (255, 128, 64, 0) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgbw"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + +async def test_color_rgbww(hass): + """Test rgbww color reporting.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_RGBWW} + entity0.color_mode = COLOR_MODE_RGBWW + entity0.brightness = 255 + entity0.rgbww_color = (0, 32, 64, 128, 255) + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {COLOR_MODE_RGBWW} + entity1.color_mode = COLOR_MODE_RGBWW + entity1.brightness = 255 + entity1.rgbww_color = (255, 128, 64, 32, 0) + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.light_group") + assert state.state == STATE_ON + assert state.attributes[ATTR_COLOR_MODE] == "rgbww" + assert state.attributes[ATTR_RGBWW_COLOR] == (0, 32, 64, 128, 255) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgbww"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity1.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "rgbww" + assert state.attributes[ATTR_RGBWW_COLOR] == (127, 80, 64, 80, 127) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgbww"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "rgbww" + assert state.attributes[ATTR_RGBWW_COLOR] == (255, 128, 64, 32, 0) + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["rgbww"] + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 async def test_white_value(hass): @@ -206,6 +414,7 @@ async def test_white_value(hass): ) await hass.async_block_till_done() state = hass.states.get("light.light_group") + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 128 assert state.attributes[ATTR_WHITE_VALUE] == 255 hass.states.async_set( @@ -213,6 +422,7 @@ async def test_white_value(hass): ) await hass.async_block_till_done() state = hass.states.get("light.light_group") + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 128 assert state.attributes[ATTR_WHITE_VALUE] == 177 hass.states.async_set( @@ -220,62 +430,36 @@ async def test_white_value(hass): ) await hass.async_block_till_done() state = hass.states.get("light.light_group") + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 128 assert state.attributes[ATTR_WHITE_VALUE] == 100 async def test_color_temp(hass): """Test color temp reporting.""" - await async_setup_component( - hass, - LIGHT_DOMAIN, - { - LIGHT_DOMAIN: { - "platform": DOMAIN, - "entities": ["light.test1", "light.test2"], - } - }, - ) - await hass.async_block_till_done() - await hass.async_start() - await hass.async_block_till_done() + platform = getattr(hass.components, "test.light") + platform.init(empty=True) - hass.states.async_set( - "light.test1", STATE_ON, {"color_temp": 2, ATTR_SUPPORTED_FEATURES: 2} - ) - await hass.async_block_till_done() - state = hass.states.get("light.light_group") - assert state.attributes[ATTR_COLOR_TEMP] == 2 + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) - hass.states.async_set( - "light.test2", STATE_ON, {"color_temp": 1000, ATTR_SUPPORTED_FEATURES: 2} - ) - await hass.async_block_till_done() - state = hass.states.get("light.light_group") - assert state.attributes[ATTR_COLOR_TEMP] == 501 + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_COLOR_TEMP} + entity0.color_mode = COLOR_MODE_COLOR_TEMP + entity0.brightness = 255 + entity0.color_temp = 2 - hass.states.async_set( - "light.test1", STATE_OFF, {"color_temp": 2, ATTR_SUPPORTED_FEATURES: 2} - ) - await hass.async_block_till_done() - state = hass.states.get("light.light_group") - assert state.attributes[ATTR_COLOR_TEMP] == 1000 + entity1 = platform.ENTITIES[1] + entity1.supported_features = SUPPORT_COLOR_TEMP - -async def test_emulated_color_temp_group(hass): - """Test emulated color temperature in a group.""" - await async_setup_component( + assert await async_setup_component( hass, LIGHT_DOMAIN, { LIGHT_DOMAIN: [ - {"platform": "demo"}, + {"platform": "test"}, { "platform": DOMAIN, - "entities": [ - "light.bed_light", - "light.ceiling_lights", - "light.kitchen_lights", - ], + "entities": ["light.test1", "light.test2"], }, ] }, @@ -284,13 +468,78 @@ async def test_emulated_color_temp_group(hass): await hass.async_start() await hass.async_block_till_done() - hass.states.async_set("light.bed_light", STATE_ON, {ATTR_SUPPORTED_FEATURES: 2}) - hass.states.async_set( - "light.ceiling_lights", STATE_ON, {ATTR_SUPPORTED_FEATURES: 63} + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "color_temp" + assert state.attributes[ATTR_COLOR_TEMP] == 2 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp"] + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity1.entity_id], ATTR_COLOR_TEMP: 1000}, + blocking=True, ) - hass.states.async_set( - "light.kitchen_lights", STATE_ON, {ATTR_SUPPORTED_FEATURES: 61} + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "color_temp" + assert state.attributes[ATTR_COLOR_TEMP] == 501 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp"] + + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id]}, + blocking=True, ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == "color_temp" + assert state.attributes[ATTR_COLOR_TEMP] == 1000 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + assert state.attributes[ATTR_SUPPORTED_COLOR_MODES] == ["color_temp"] + + +async def test_emulated_color_temp_group(hass): + """Test emulated color temperature in a group.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + platform.ENTITIES.append(platform.MockLight("test3", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_COLOR_TEMP} + entity0.color_mode = COLOR_MODE_COLOR_TEMP + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} + entity1.color_mode = COLOR_MODE_COLOR_TEMP + + entity2 = platform.ENTITIES[2] + entity2.supported_color_modes = {COLOR_MODE_HS} + entity2.color_mode = COLOR_MODE_HS + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2", "light.test3"], + }, + ] + }, + ) + + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + await hass.async_block_till_done() await hass.services.async_call( LIGHT_DOMAIN, @@ -300,61 +549,82 @@ async def test_emulated_color_temp_group(hass): ) await hass.async_block_till_done() - state = hass.states.get("light.bed_light") + state = hass.states.get("light.test1") assert state.state == STATE_ON assert state.attributes[ATTR_COLOR_TEMP] == 200 assert ATTR_HS_COLOR not in state.attributes.keys() - state = hass.states.get("light.ceiling_lights") + state = hass.states.get("light.test2") assert state.state == STATE_ON assert state.attributes[ATTR_COLOR_TEMP] == 200 assert ATTR_HS_COLOR not in state.attributes.keys() - state = hass.states.get("light.kitchen_lights") + state = hass.states.get("light.test3") assert state.state == STATE_ON assert state.attributes[ATTR_HS_COLOR] == (27.001, 19.243) async def test_min_max_mireds(hass): - """Test min/max mireds reporting.""" - await async_setup_component( + """Test min/max mireds reporting. + + min/max mireds is reported both when light is on and off + """ + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_COLOR_TEMP} + entity0.color_mode = COLOR_MODE_COLOR_TEMP + entity0.color_temp = 2 + entity0.min_mireds = 2 + entity0.max_mireds = 5 + + entity1 = platform.ENTITIES[1] + entity1.supported_features = SUPPORT_COLOR_TEMP + entity1.min_mireds = 1 + entity1.max_mireds = 1234567890 + + assert await async_setup_component( hass, LIGHT_DOMAIN, { - LIGHT_DOMAIN: { - "platform": DOMAIN, - "entities": ["light.test1", "light.test2"], - } + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2"], + }, + ] }, ) await hass.async_block_till_done() await hass.async_start() await hass.async_block_till_done() - hass.states.async_set( - "light.test1", - STATE_ON, - {ATTR_MIN_MIREDS: 2, ATTR_MAX_MIREDS: 5, ATTR_SUPPORTED_FEATURES: 2}, - ) await hass.async_block_till_done() state = hass.states.get("light.light_group") - assert state.attributes[ATTR_MIN_MIREDS] == 2 - assert state.attributes[ATTR_MAX_MIREDS] == 5 - - hass.states.async_set( - "light.test2", - STATE_ON, - {ATTR_MIN_MIREDS: 7, ATTR_MAX_MIREDS: 1234567890, ATTR_SUPPORTED_FEATURES: 2}, - ) - await hass.async_block_till_done() - state = hass.states.get("light.light_group") - assert state.attributes[ATTR_MIN_MIREDS] == 2 + assert state.attributes[ATTR_MIN_MIREDS] == 1 assert state.attributes[ATTR_MAX_MIREDS] == 1234567890 - hass.states.async_set( - "light.test1", - STATE_OFF, - {ATTR_MIN_MIREDS: 1, ATTR_MAX_MIREDS: 2, ATTR_SUPPORTED_FEATURES: 2}, + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity0.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_MIN_MIREDS] == 1 + assert state.attributes[ATTR_MAX_MIREDS] == 1234567890 + + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id]}, + blocking=True, ) await hass.async_block_till_done() state = hass.states.get("light.light_group") @@ -465,6 +735,123 @@ async def test_effect(hass): assert state.attributes[ATTR_EFFECT] == "Random" +async def test_supported_color_modes(hass): + """Test supported_color_modes reporting.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + platform.ENTITIES.append(platform.MockLight("test3", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {COLOR_MODE_RGBW, COLOR_MODE_RGBWW} + + entity2 = platform.ENTITIES[2] + entity2.supported_features = SUPPORT_BRIGHTNESS + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2", "light.test3"], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.light_group") + assert set(state.attributes[ATTR_SUPPORTED_COLOR_MODES]) == { + "brightness", + "color_temp", + "hs", + "rgbw", + "rgbww", + } + + +async def test_color_mode(hass): + """Test color_mode reporting.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("test1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("test2", STATE_OFF)) + platform.ENTITIES.append(platform.MockLight("test3", STATE_OFF)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} + entity0.color_mode = COLOR_MODE_COLOR_TEMP + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} + entity1.color_mode = COLOR_MODE_COLOR_TEMP + + entity2 = platform.ENTITIES[2] + entity2.supported_color_modes = {COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS} + entity2.color_mode = COLOR_MODE_HS + + assert await async_setup_component( + hass, + LIGHT_DOMAIN, + { + LIGHT_DOMAIN: [ + {"platform": "test"}, + { + "platform": DOMAIN, + "entities": ["light.test1", "light.test2", "light.test3"], + }, + ] + }, + ) + await hass.async_block_till_done() + await hass.async_start() + await hass.async_block_till_done() + + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == COLOR_MODE_COLOR_TEMP + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity1.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == COLOR_MODE_COLOR_TEMP + + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": [entity2.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == COLOR_MODE_COLOR_TEMP + + await hass.services.async_call( + "light", + "turn_off", + {"entity_id": [entity0.entity_id, entity1.entity_id]}, + blocking=True, + ) + await hass.async_block_till_done() + state = hass.states.get("light.light_group") + assert state.attributes[ATTR_COLOR_MODE] == COLOR_MODE_HS + + async def test_supported_features(hass): """Test supported features reporting.""" await async_setup_component( @@ -486,20 +873,26 @@ async def test_supported_features(hass): state = hass.states.get("light.light_group") assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + # SUPPORT_COLOR_TEMP = 2 + # SUPPORT_COLOR_TEMP = 2 will be blocked in favour of COLOR_MODE_COLOR_TEMP hass.states.async_set("light.test2", STATE_ON, {ATTR_SUPPORTED_FEATURES: 2}) await hass.async_block_till_done() state = hass.states.get("light.light_group") - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 2 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 0 + # SUPPORT_TRANSITION | SUPPORT_FLASH | SUPPORT_BRIGHTNESS = 41 + # SUPPORT_BRIGHTNESS = 1 will be translated to COLOR_MODE_BRIGHTNESS hass.states.async_set("light.test1", STATE_OFF, {ATTR_SUPPORTED_FEATURES: 41}) await hass.async_block_till_done() state = hass.states.get("light.light_group") - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 43 + # SUPPORT_TRANSITION | SUPPORT_FLASH = 40 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 40 + # Test that unknown feature 256 is blocked hass.states.async_set("light.test2", STATE_OFF, {ATTR_SUPPORTED_FEATURES: 256}) await hass.async_block_till_done() state = hass.states.get("light.light_group") - assert state.attributes[ATTR_SUPPORTED_FEATURES] == 41 + assert state.attributes[ATTR_SUPPORTED_FEATURES] == 40 async def test_service_calls(hass): @@ -629,8 +1022,6 @@ async def test_invalid_service_calls(hass): } await grouped_light.async_turn_on(**data) data[ATTR_ENTITY_ID] = ["light.test1", "light.test2"] - data.pop(ATTR_RGB_COLOR) - data.pop(ATTR_XY_COLOR) mock_call.assert_called_once_with( LIGHT_DOMAIN, SERVICE_TURN_ON, data, blocking=True, context=None ) diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 752de867542..ef781b56a56 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -748,9 +748,9 @@ async def test_light_brightness_step(hass): ) _, data = entity0.last_call("turn_on") - assert data["brightness"] == 126 # 100 + (255 * 0.10) + assert data["brightness"] == 116 # 90 + (255 * 0.10) _, data = entity1.last_call("turn_on") - assert data["brightness"] == 76 # 50 + (255 * 0.10) + assert data["brightness"] == 66 # 40 + (255 * 0.10) async def test_light_brightness_pct_conversion(hass): diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index 84008d90c27..88ce04bdc92 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -36,18 +36,33 @@ async def async_setup_platform( class MockLight(MockToggleEntity, LightEntity): """Mock light class.""" - brightness = None + color_mode = None + max_mireds = 500 + min_mireds = 153 supported_color_modes = None supported_features = 0 - color_mode = None - + brightness = None + color_temp = None hs_color = None - xy_color = None rgb_color = None rgbw_color = None rgbww_color = None - - color_temp = None - + xy_color = None white_value = None + + def turn_on(self, **kwargs): + """Turn the entity on.""" + super().turn_on(**kwargs) + for key, value in kwargs.items(): + if key in [ + "brightness", + "hs_color", + "xy_color", + "rgb_color", + "rgbw_color", + "rgbww_color", + "color_temp", + "white_value", + ]: + setattr(self, key, value)