diff --git a/homeassistant/components/light/__init__.py b/homeassistant/components/light/__init__.py index 693eb1a573a..56fd5841388 100644 --- a/homeassistant/components/light/__init__.py +++ b/homeassistant/components/light/__init__.py @@ -6,7 +6,7 @@ import dataclasses from datetime import timedelta import logging import os -from typing import Dict, List, Optional, Tuple, cast +from typing import Dict, List, Optional, Set, Tuple, cast import voluptuous as vol @@ -38,19 +38,49 @@ DATA_PROFILES = "light_profiles" ENTITY_ID_FORMAT = DOMAIN + ".{}" # Bitfield of features supported by the light entity -SUPPORT_BRIGHTNESS = 1 -SUPPORT_COLOR_TEMP = 2 +SUPPORT_BRIGHTNESS = 1 # Deprecated, replaced by color modes +SUPPORT_COLOR_TEMP = 2 # Deprecated, replaced by color modes SUPPORT_EFFECT = 4 SUPPORT_FLASH = 8 -SUPPORT_COLOR = 16 +SUPPORT_COLOR = 16 # Deprecated, replaced by color modes SUPPORT_TRANSITION = 32 -SUPPORT_WHITE_VALUE = 128 +SUPPORT_WHITE_VALUE = 128 # Deprecated, replaced by color modes + +# Color mode of the light +ATTR_COLOR_MODE = "color_mode" +# List of color modes supported by the light +ATTR_SUPPORTED_COLOR_MODES = "supported_color_modes" +# Possible color modes +COLOR_MODE_UNKNOWN = "unknown" # Ambiguous color mode +COLOR_MODE_ONOFF = "onoff" # Must be the only supported mode +COLOR_MODE_BRIGHTNESS = "brightness" # Must be the only supported mode +COLOR_MODE_COLOR_TEMP = "color_temp" +COLOR_MODE_HS = "hs" +COLOR_MODE_XY = "xy" +COLOR_MODE_RGB = "rgb" +COLOR_MODE_RGBW = "rgbw" +COLOR_MODE_RGBWW = "rgbww" + +VALID_COLOR_MODES = { + COLOR_MODE_ONOFF, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_XY, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, +} +COLOR_MODES_BRIGHTNESS = VALID_COLOR_MODES - {COLOR_MODE_ONOFF} +COLOR_MODES_COLOR = {COLOR_MODE_HS, COLOR_MODE_RGB, COLOR_MODE_XY} # Float that represents transition time in seconds to make change. ATTR_TRANSITION = "transition" # Lists holding color values ATTR_RGB_COLOR = "rgb_color" +ATTR_RGBW_COLOR = "rgbw_color" +ATTR_RGBWW_COLOR = "rgbww_color" ATTR_XY_COLOR = "xy_color" ATTR_HS_COLOR = "hs_color" ATTR_COLOR_TEMP = "color_temp" @@ -104,7 +134,13 @@ LIGHT_TURN_ON_SCHEMA = { vol.Exclusive(ATTR_BRIGHTNESS_STEP_PCT, ATTR_BRIGHTNESS): VALID_BRIGHTNESS_STEP_PCT, vol.Exclusive(ATTR_COLOR_NAME, COLOR_GROUP): cv.string, vol.Exclusive(ATTR_RGB_COLOR, COLOR_GROUP): vol.All( - vol.ExactSequence((cv.byte, cv.byte, cv.byte)), vol.Coerce(tuple) + vol.ExactSequence((cv.byte,) * 3), vol.Coerce(tuple) + ), + vol.Exclusive(ATTR_RGBW_COLOR, COLOR_GROUP): vol.All( + vol.ExactSequence((cv.byte,) * 4), vol.Coerce(tuple) + ), + vol.Exclusive(ATTR_RGBWW_COLOR, COLOR_GROUP): vol.All( + vol.ExactSequence((cv.byte,) * 5), vol.Coerce(tuple) ), vol.Exclusive(ATTR_XY_COLOR, COLOR_GROUP): vol.All( vol.ExactSequence((cv.small_float, cv.small_float)), vol.Coerce(tuple) @@ -166,14 +202,6 @@ def preprocess_turn_on_alternatives(hass, params): if brightness_pct is not None: params[ATTR_BRIGHTNESS] = round(255 * brightness_pct / 100) - xy_color = params.pop(ATTR_XY_COLOR, None) - if xy_color is not None: - params[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color) - - rgb_color = params.pop(ATTR_RGB_COLOR, None) - if rgb_color is not None: - params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) - def filter_turn_off_params(params): """Filter out params not used in turn off.""" @@ -228,6 +256,52 @@ async def async_setup(hass, config): if ATTR_PROFILE not in params: profiles.apply_default(light.entity_id, params) + supported_color_modes = light.supported_color_modes + # Backwards compatibility: if an RGBWW color is specified, convert to RGB + W + # for legacy lights + if ATTR_RGBW_COLOR in params: + legacy_supported_color_modes = ( + light._light_internal_supported_color_modes # pylint: disable=protected-access + ) + if ( + COLOR_MODE_RGBW in legacy_supported_color_modes + and not supported_color_modes + ): + rgbw_color = params.pop(ATTR_RGBW_COLOR) + params[ATTR_RGB_COLOR] = rgbw_color[0:3] + params[ATTR_WHITE_VALUE] = rgbw_color[3] + + # If a color is specified, convert to the color space supported by the light + # Backwards compatibility: Fall back to hs color if light.supported_color_modes + # is not implemented + if not supported_color_modes: + if (rgb_color := params.pop(ATTR_RGB_COLOR, None)) is not None: + params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) + elif (xy_color := params.pop(ATTR_XY_COLOR, None)) is not None: + params[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color) + elif ATTR_HS_COLOR in params and COLOR_MODE_HS not in supported_color_modes: + hs_color = params.pop(ATTR_HS_COLOR) + if COLOR_MODE_RGB in supported_color_modes: + params[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color) + elif COLOR_MODE_XY in supported_color_modes: + params[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color) + elif ATTR_RGB_COLOR in params and COLOR_MODE_RGB not in supported_color_modes: + rgb_color = params.pop(ATTR_RGB_COLOR) + if COLOR_MODE_HS in supported_color_modes: + params[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) + elif COLOR_MODE_XY in supported_color_modes: + params[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color) + elif ATTR_XY_COLOR in params and COLOR_MODE_XY not in supported_color_modes: + xy_color = params.pop(ATTR_XY_COLOR) + if COLOR_MODE_HS in supported_color_modes: + params[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color) + elif COLOR_MODE_RGB in supported_color_modes: + params[ATTR_RGB_COLOR] = color_util.color_xy_to_RGB(*xy_color) + + # Remove deprecated white value if the light supports color mode + if supported_color_modes: + params.pop(ATTR_WHITE_VALUE, None) + # Zero brightness: Light will be turned off if params.get(ATTR_BRIGHTNESS) == 0: await light.async_turn_off(**filter_turn_off_params(params)) @@ -411,11 +485,83 @@ class LightEntity(ToggleEntity): """Return the brightness of this light between 0..255.""" return None + @property + def color_mode(self) -> Optional[str]: + """Return the color mode of the light.""" + return None + + @property + def _light_internal_color_mode(self) -> str: + """Return the color mode of the light with backwards compatibility.""" + color_mode = self.color_mode + + if color_mode is None: + # Backwards compatibility for color_mode added in 2021.4 + # Add warning in 2021.6, remove in 2021.10 + supported = self._light_internal_supported_color_modes + + if ( + COLOR_MODE_RGBW in supported + and self.white_value is not None + and self.hs_color is not None + ): + return COLOR_MODE_RGBW + if COLOR_MODE_HS in supported and self.hs_color is not None: + return COLOR_MODE_HS + if COLOR_MODE_COLOR_TEMP in supported and self.color_temp is not None: + return COLOR_MODE_COLOR_TEMP + if COLOR_MODE_BRIGHTNESS in supported and self.brightness is not None: + return COLOR_MODE_BRIGHTNESS + if COLOR_MODE_ONOFF in supported: + return COLOR_MODE_ONOFF + return COLOR_MODE_UNKNOWN + + return color_mode + @property def hs_color(self) -> Optional[Tuple[float, float]]: """Return the hue and saturation color value [float, float].""" return None + @property + def xy_color(self) -> Optional[Tuple[float, float]]: + """Return the xy color value [float, float].""" + return None + + @property + def rgb_color(self) -> Optional[Tuple[int, int, int]]: + """Return the rgb color value [int, int, int].""" + return None + + @property + def rgbw_color(self) -> Optional[Tuple[int, int, int, int]]: + """Return the rgbw color value [int, int, int, int].""" + return None + + @property + def _light_internal_rgbw_color(self) -> Optional[Tuple[int, int, int, int]]: + """Return the rgbw color value [int, int, int, int].""" + rgbw_color = self.rgbw_color + if ( + rgbw_color is None + and self.hs_color is not None + and self.white_value is not None + ): + # Backwards compatibility for rgbw_color added in 2021.4 + # Add warning in 2021.6, remove in 2021.10 + r, g, b = color_util.color_hs_to_RGB( # pylint: disable=invalid-name + *self.hs_color + ) + w = self.white_value # pylint: disable=invalid-name + rgbw_color = (r, g, b, w) + + return rgbw_color + + @property + def rgbww_color(self) -> Optional[Tuple[int, int, int, int, int]]: + """Return the rgbww color value [int, int, int, int, int].""" + return None + @property def color_temp(self) -> Optional[int]: """Return the CT color value in mireds.""" @@ -463,6 +609,29 @@ class LightEntity(ToggleEntity): if supported_features & SUPPORT_EFFECT: data[ATTR_EFFECT_LIST] = self.effect_list + data[ATTR_SUPPORTED_COLOR_MODES] = sorted( + list(self._light_internal_supported_color_modes) + ) + + return data + + def _light_internal_convert_color(self, color_mode: str) -> dict: + data: Dict[str, Tuple] = {} + if color_mode == COLOR_MODE_HS and self.hs_color: + hs_color = self.hs_color + data[ATTR_HS_COLOR] = (round(hs_color[0], 3), round(hs_color[1], 3)) + data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color) + data[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color) + elif color_mode == COLOR_MODE_XY and self.xy_color: + xy_color = self.xy_color + data[ATTR_HS_COLOR] = color_util.color_xy_to_hs(*xy_color) + data[ATTR_RGB_COLOR] = color_util.color_xy_to_RGB(*xy_color) + data[ATTR_XY_COLOR] = (round(xy_color[0], 6), round(xy_color[1], 6)) + elif color_mode == COLOR_MODE_RGB and self.rgb_color: + rgb_color = self.rgb_color + data[ATTR_HS_COLOR] = color_util.color_RGB_to_hs(*rgb_color) + data[ATTR_RGB_COLOR] = tuple(int(x) for x in rgb_color[0:3]) + data[ATTR_XY_COLOR] = color_util.color_RGB_to_xy(*rgb_color) return data @property @@ -473,27 +642,85 @@ class LightEntity(ToggleEntity): data = {} supported_features = self.supported_features + color_mode = self._light_internal_color_mode - if supported_features & SUPPORT_BRIGHTNESS: + if color_mode not in self._light_internal_supported_color_modes: + # Increase severity to warning in 2021.6, reject in 2021.10 + _LOGGER.debug( + "%s: set to unsupported color_mode: %s, supported_color_modes: %s", + self.entity_id, + color_mode, + self._light_internal_supported_color_modes, + ) + + data[ATTR_COLOR_MODE] = color_mode + + if color_mode in COLOR_MODES_BRIGHTNESS: + data[ATTR_BRIGHTNESS] = self.brightness + elif supported_features & SUPPORT_BRIGHTNESS: + # Backwards compatibility for ambiguous / incomplete states + # Add warning in 2021.6, remove in 2021.10 data[ATTR_BRIGHTNESS] = self.brightness - if supported_features & SUPPORT_COLOR_TEMP: + if color_mode == COLOR_MODE_COLOR_TEMP: data[ATTR_COLOR_TEMP] = self.color_temp - if supported_features & SUPPORT_COLOR and self.hs_color: - hs_color = self.hs_color - data[ATTR_HS_COLOR] = (round(hs_color[0], 3), round(hs_color[1], 3)) - data[ATTR_RGB_COLOR] = color_util.color_hs_to_RGB(*hs_color) - data[ATTR_XY_COLOR] = color_util.color_hs_to_xy(*hs_color) + if color_mode in COLOR_MODES_COLOR: + data.update(self._light_internal_convert_color(color_mode)) - if supported_features & SUPPORT_WHITE_VALUE: + if color_mode == COLOR_MODE_RGBW: + data[ATTR_RGBW_COLOR] = self._light_internal_rgbw_color + + if color_mode == COLOR_MODE_RGBWW: + data[ATTR_RGBWW_COLOR] = self.rgbww_color + + if supported_features & SUPPORT_COLOR_TEMP and not self.supported_color_modes: + # Backwards compatibility + # Add warning in 2021.6, remove in 2021.10 + data[ATTR_COLOR_TEMP] = self.color_temp + + if supported_features & SUPPORT_WHITE_VALUE and not self.supported_color_modes: + # Backwards compatibility + # Add warning in 2021.6, remove in 2021.10 data[ATTR_WHITE_VALUE] = self.white_value + if self.hs_color is not None: + data.update(self._light_internal_convert_color(COLOR_MODE_HS)) if supported_features & SUPPORT_EFFECT: data[ATTR_EFFECT] = self.effect return {key: val for key, val in data.items() if val is not None} + @property + def _light_internal_supported_color_modes(self) -> Set: + """Calculate supported color modes with backwards compatibility.""" + supported_color_modes = self.supported_color_modes + + if supported_color_modes is None: + # Backwards compatibility for supported_color_modes added in 2021.4 + # Add warning in 2021.6, remove in 2021.10 + supported_features = self.supported_features + supported_color_modes = set() + + if supported_features & SUPPORT_COLOR_TEMP: + supported_color_modes.add(COLOR_MODE_COLOR_TEMP) + if supported_features & SUPPORT_COLOR: + supported_color_modes.add(COLOR_MODE_HS) + if supported_features & SUPPORT_WHITE_VALUE: + supported_color_modes.add(COLOR_MODE_RGBW) + if supported_features & SUPPORT_BRIGHTNESS and not supported_color_modes: + supported_color_modes = {COLOR_MODE_BRIGHTNESS} + + if not supported_color_modes: + supported_color_modes = {COLOR_MODE_ONOFF} + + return supported_color_modes + + @property + def supported_color_modes(self) -> Optional[Set]: + """Flag supported color modes.""" + return None + @property def supported_features(self) -> int: """Flag supported features.""" diff --git a/homeassistant/components/light/reproduce_state.py b/homeassistant/components/light/reproduce_state.py index a7939beb91e..863790cff71 100644 --- a/homeassistant/components/light/reproduce_state.py +++ b/homeassistant/components/light/reproduce_state.py @@ -17,6 +17,7 @@ from homeassistant.helpers.typing import HomeAssistantType from . import ( ATTR_BRIGHTNESS, ATTR_BRIGHTNESS_PCT, + ATTR_COLOR_MODE, ATTR_COLOR_NAME, ATTR_COLOR_TEMP, ATTR_EFFECT, @@ -25,9 +26,18 @@ from . import ( ATTR_KELVIN, ATTR_PROFILE, ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, ATTR_TRANSITION, ATTR_WHITE_VALUE, ATTR_XY_COLOR, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, + COLOR_MODE_RGBWW, + COLOR_MODE_UNKNOWN, + COLOR_MODE_XY, DOMAIN, ) @@ -48,6 +58,8 @@ COLOR_GROUP = [ ATTR_HS_COLOR, ATTR_COLOR_TEMP, ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_RGBWW_COLOR, ATTR_XY_COLOR, # The following color attributes are deprecated ATTR_PROFILE, @@ -55,6 +67,15 @@ COLOR_GROUP = [ ATTR_KELVIN, ] +COLOR_MODE_TO_ATTRIBUTE = { + COLOR_MODE_COLOR_TEMP: ATTR_COLOR_TEMP, + COLOR_MODE_HS: ATTR_HS_COLOR, + COLOR_MODE_RGB: ATTR_RGB_COLOR, + COLOR_MODE_RGBW: ATTR_RGBW_COLOR, + COLOR_MODE_RGBWW: ATTR_RGBWW_COLOR, + COLOR_MODE_XY: ATTR_XY_COLOR, +} + DEPRECATED_GROUP = [ ATTR_BRIGHTNESS_PCT, ATTR_COLOR_NAME, @@ -114,11 +135,29 @@ async def _async_reproduce_state( if attr in state.attributes: service_data[attr] = state.attributes[attr] - for color_attr in COLOR_GROUP: - # Choose the first color that is specified - if color_attr in state.attributes: + if ( + state.attributes.get(ATTR_COLOR_MODE, COLOR_MODE_UNKNOWN) + != COLOR_MODE_UNKNOWN + ): + # Remove deprecated white value if we got a valid color mode + service_data.pop(ATTR_WHITE_VALUE, None) + color_mode = state.attributes[ATTR_COLOR_MODE] + if color_attr := COLOR_MODE_TO_ATTRIBUTE.get(color_mode): + if color_attr not in state.attributes: + _LOGGER.warning( + "Color mode %s specified but attribute %s missing for: %s", + color_mode, + color_attr, + state.entity_id, + ) + return service_data[color_attr] = state.attributes[color_attr] - break + else: + # Fall back to Choosing the first color that is specified + for color_attr in COLOR_GROUP: + if color_attr in state.attributes: + service_data[color_attr] = state.attributes[color_attr] + break elif state.state == STATE_OFF: service = SERVICE_TURN_OFF diff --git a/tests/components/kulersky/test_light.py b/tests/components/kulersky/test_light.py index 9dd13fad18b..c08dd10d597 100644 --- a/tests/components/kulersky/test_light.py +++ b/tests/components/kulersky/test_light.py @@ -8,10 +8,15 @@ from homeassistant import setup from homeassistant.components.kulersky.light import DOMAIN from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, ATTR_HS_COLOR, ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + ATTR_SUPPORTED_COLOR_MODES, ATTR_WHITE_VALUE, ATTR_XY_COLOR, + COLOR_MODE_HS, + COLOR_MODE_RGBW, SCAN_INTERVAL, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, @@ -65,6 +70,7 @@ async def test_init(hass, mock_light): assert state.state == STATE_OFF assert state.attributes == { ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW], ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE, @@ -168,6 +174,7 @@ async def test_light_update(hass, mock_light): assert state.state == STATE_OFF assert state.attributes == { ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW], ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE, @@ -183,6 +190,7 @@ async def test_light_update(hass, mock_light): assert state.state == STATE_UNAVAILABLE assert state.attributes == { ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW], ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE, @@ -198,12 +206,15 @@ async def test_light_update(hass, mock_light): assert state.state == STATE_ON assert state.attributes == { ATTR_FRIENDLY_NAME: "Bedroom", + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS, COLOR_MODE_RGBW], ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR | SUPPORT_WHITE_VALUE, + ATTR_COLOR_MODE: COLOR_MODE_RGBW, ATTR_BRIGHTNESS: 200, ATTR_HS_COLOR: (200, 60), ATTR_RGB_COLOR: (102, 203, 255), + ATTR_RGBW_COLOR: (102, 203, 255, 240), ATTR_WHITE_VALUE: 240, ATTR_XY_COLOR: (0.184, 0.261), } diff --git a/tests/components/light/test_init.py b/tests/components/light/test_init.py index 10f475a580d..3adb146a225 100644 --- a/tests/components/light/test_init.py +++ b/tests/components/light/test_init.py @@ -915,3 +915,482 @@ invalid_no_brightness_no_color_no_transition,,, "invalid_no_brightness_no_color_no_transition", ): assert invalid_profile_name not in profiles.data + + +@pytest.mark.parametrize("light_state", (STATE_ON, STATE_OFF)) +async def test_light_backwards_compatibility_supported_color_modes(hass, light_state): + """Test supported_color_modes if not implemented by the entity.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("Test_0", light_state)) + platform.ENTITIES.append(platform.MockLight("Test_1", light_state)) + platform.ENTITIES.append(platform.MockLight("Test_2", light_state)) + platform.ENTITIES.append(platform.MockLight("Test_3", light_state)) + platform.ENTITIES.append(platform.MockLight("Test_4", light_state)) + platform.ENTITIES.append(platform.MockLight("Test_5", light_state)) + platform.ENTITIES.append(platform.MockLight("Test_6", light_state)) + + entity0 = platform.ENTITIES[0] + + entity1 = platform.ENTITIES[1] + entity1.supported_features = light.SUPPORT_BRIGHTNESS + + entity2 = platform.ENTITIES[2] + entity2.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR_TEMP + + entity3 = platform.ENTITIES[3] + entity3.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR + + entity4 = platform.ENTITIES[4] + entity4.supported_features = ( + light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_WHITE_VALUE + ) + + entity5 = platform.ENTITIES[5] + entity5.supported_features = ( + light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP + ) + + entity6 = platform.ENTITIES[6] + entity6.supported_features = ( + light.SUPPORT_BRIGHTNESS + | light.SUPPORT_COLOR + | light.SUPPORT_COLOR_TEMP + | light.SUPPORT_WHITE_VALUE + ) + + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_ONOFF] + if light_state == STATE_OFF: + assert "color_mode" not in state.attributes + else: + assert state.attributes["color_mode"] == light.COLOR_MODE_ONOFF + + state = hass.states.get(entity1.entity_id) + assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_BRIGHTNESS] + if light_state == STATE_OFF: + assert "color_mode" not in state.attributes + else: + assert state.attributes["color_mode"] == light.COLOR_MODE_UNKNOWN + + state = hass.states.get(entity2.entity_id) + assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_COLOR_TEMP] + if light_state == STATE_OFF: + assert "color_mode" not in state.attributes + else: + assert state.attributes["color_mode"] == light.COLOR_MODE_UNKNOWN + + state = hass.states.get(entity3.entity_id) + assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_HS] + if light_state == STATE_OFF: + assert "color_mode" not in state.attributes + else: + assert state.attributes["color_mode"] == light.COLOR_MODE_UNKNOWN + + state = hass.states.get(entity4.entity_id) + assert state.attributes["supported_color_modes"] == [ + light.COLOR_MODE_HS, + light.COLOR_MODE_RGBW, + ] + if light_state == STATE_OFF: + assert "color_mode" not in state.attributes + else: + assert state.attributes["color_mode"] == light.COLOR_MODE_UNKNOWN + + state = hass.states.get(entity5.entity_id) + assert state.attributes["supported_color_modes"] == [ + light.COLOR_MODE_COLOR_TEMP, + light.COLOR_MODE_HS, + ] + if light_state == STATE_OFF: + assert "color_mode" not in state.attributes + else: + assert state.attributes["color_mode"] == light.COLOR_MODE_UNKNOWN + + state = hass.states.get(entity6.entity_id) + assert state.attributes["supported_color_modes"] == [ + light.COLOR_MODE_COLOR_TEMP, + light.COLOR_MODE_HS, + light.COLOR_MODE_RGBW, + ] + if light_state == STATE_OFF: + assert "color_mode" not in state.attributes + else: + assert state.attributes["color_mode"] == light.COLOR_MODE_UNKNOWN + + +async def test_light_backwards_compatibility_color_mode(hass): + """Test color_mode if not implemented by the entity.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("Test_0", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_1", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_2", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_3", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_4", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_5", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_6", STATE_ON)) + + entity0 = platform.ENTITIES[0] + + entity1 = platform.ENTITIES[1] + entity1.supported_features = light.SUPPORT_BRIGHTNESS + entity1.brightness = 100 + + entity2 = platform.ENTITIES[2] + entity2.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR_TEMP + entity2.color_temp = 100 + + entity3 = platform.ENTITIES[3] + entity3.supported_features = light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR + entity3.hs_color = (240, 100) + + entity4 = platform.ENTITIES[4] + entity4.supported_features = ( + light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_WHITE_VALUE + ) + entity4.hs_color = (240, 100) + entity4.white_value = 100 + + entity5 = platform.ENTITIES[5] + entity5.supported_features = ( + light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_COLOR_TEMP + ) + entity5.hs_color = (240, 100) + entity5.color_temp = 100 + + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_ONOFF] + assert state.attributes["color_mode"] == light.COLOR_MODE_ONOFF + + state = hass.states.get(entity1.entity_id) + assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_BRIGHTNESS] + assert state.attributes["color_mode"] == light.COLOR_MODE_BRIGHTNESS + + state = hass.states.get(entity2.entity_id) + assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_COLOR_TEMP] + assert state.attributes["color_mode"] == light.COLOR_MODE_COLOR_TEMP + + state = hass.states.get(entity3.entity_id) + assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_HS] + assert state.attributes["color_mode"] == light.COLOR_MODE_HS + + state = hass.states.get(entity4.entity_id) + assert state.attributes["supported_color_modes"] == [ + light.COLOR_MODE_HS, + light.COLOR_MODE_RGBW, + ] + assert state.attributes["color_mode"] == light.COLOR_MODE_RGBW + + state = hass.states.get(entity5.entity_id) + assert state.attributes["supported_color_modes"] == [ + light.COLOR_MODE_COLOR_TEMP, + light.COLOR_MODE_HS, + ] + # hs color prioritized over color_temp, light should report mode COLOR_MODE_HS + assert state.attributes["color_mode"] == light.COLOR_MODE_HS + + +async def test_light_service_call_rgbw(hass): + """Test backwards compatibility for rgbw functionality in service calls.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("Test_legacy_white_value", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_rgbw", STATE_ON)) + + entity0 = platform.ENTITIES[0] + entity0.supported_features = ( + light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_WHITE_VALUE + ) + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {light.COLOR_MODE_RGBW} + + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.attributes["supported_color_modes"] == [ + light.COLOR_MODE_HS, + light.COLOR_MODE_RGBW, + ] + + state = hass.states.get(entity1.entity_id) + assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_RGBW] + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [entity0.entity_id, entity1.entity_id], + "brightness_pct": 100, + "rgbw_color": (10, 20, 30, 40), + }, + blocking=True, + ) + + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 255, "hs_color": (210.0, 66.667), "white_value": 40} + _, data = entity1.last_call("turn_on") + assert data == {"brightness": 255, "rgbw_color": (10, 20, 30, 40)} + + +async def test_light_state_rgbw(hass): + """Test rgbw color conversion in state updates.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("Test_legacy_white_value", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_rgbw", STATE_ON)) + + entity0 = platform.ENTITIES[0] + legacy_supported_features = ( + light.SUPPORT_BRIGHTNESS | light.SUPPORT_COLOR | light.SUPPORT_WHITE_VALUE + ) + entity0.supported_features = legacy_supported_features + entity0.hs_color = (210.0, 66.667) + entity0.rgb_color = "Invalid" # Should be ignored + entity0.rgbww_color = "Invalid" # Should be ignored + entity0.white_value = 40 + entity0.xy_color = "Invalid" # Should be ignored + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {light.COLOR_MODE_RGBW} + entity1.color_mode = light.COLOR_MODE_RGBW + entity1.hs_color = "Invalid" # Should be ignored + entity1.rgb_color = "Invalid" # Should be ignored + entity1.rgbw_color = (1, 2, 3, 4) + entity1.rgbww_color = "Invalid" # Should be ignored + entity1.white_value = "Invalid" # Should be ignored + entity1.xy_color = "Invalid" # Should be ignored + + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.attributes == { + "color_mode": light.COLOR_MODE_RGBW, + "friendly_name": "Test_legacy_white_value", + "supported_color_modes": [light.COLOR_MODE_HS, light.COLOR_MODE_RGBW], + "supported_features": legacy_supported_features, + "hs_color": (210.0, 66.667), + "rgb_color": (84, 169, 255), + "rgbw_color": (84, 169, 255, 40), + "white_value": 40, + "xy_color": (0.173, 0.207), + } + + state = hass.states.get(entity1.entity_id) + assert state.attributes == { + "color_mode": light.COLOR_MODE_RGBW, + "friendly_name": "Test_rgbw", + "supported_color_modes": [light.COLOR_MODE_RGBW], + "supported_features": 0, + "rgbw_color": (1, 2, 3, 4), + } + + +async def test_light_service_call_color_conversion(hass): + """Test color conversion in service calls.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("Test_hs", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_rgb", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_xy", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_all", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_legacy", STATE_ON)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {light.COLOR_MODE_HS} + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {light.COLOR_MODE_RGB} + + entity2 = platform.ENTITIES[2] + entity2.supported_color_modes = {light.COLOR_MODE_XY} + + entity3 = platform.ENTITIES[3] + entity3.supported_color_modes = { + light.COLOR_MODE_HS, + light.COLOR_MODE_RGB, + light.COLOR_MODE_XY, + } + + entity4 = platform.ENTITIES[4] + entity4.supported_features = light.SUPPORT_COLOR + + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_HS] + + state = hass.states.get(entity1.entity_id) + assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_RGB] + + state = hass.states.get(entity2.entity_id) + assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_XY] + + state = hass.states.get(entity3.entity_id) + assert state.attributes["supported_color_modes"] == [ + light.COLOR_MODE_HS, + light.COLOR_MODE_RGB, + light.COLOR_MODE_XY, + ] + + state = hass.states.get(entity4.entity_id) + assert state.attributes["supported_color_modes"] == [light.COLOR_MODE_HS] + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + entity1.entity_id, + entity2.entity_id, + entity3.entity_id, + entity4.entity_id, + ], + "brightness_pct": 100, + "hs_color": (240, 100), + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 255, "hs_color": (240.0, 100.0)} + _, data = entity1.last_call("turn_on") + assert data == {"brightness": 255, "rgb_color": (0, 0, 255)} + _, data = entity2.last_call("turn_on") + assert data == {"brightness": 255, "xy_color": (0.136, 0.04)} + _, data = entity3.last_call("turn_on") + assert data == {"brightness": 255, "hs_color": (240.0, 100.0)} + _, data = entity4.last_call("turn_on") + assert data == {"brightness": 255, "hs_color": (240.0, 100.0)} + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + entity1.entity_id, + entity2.entity_id, + entity3.entity_id, + entity4.entity_id, + ], + "brightness_pct": 50, + "rgb_color": (128, 0, 0), + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (0.0, 100.0)} + _, data = entity1.last_call("turn_on") + assert data == {"brightness": 128, "rgb_color": (128, 0, 0)} + _, data = entity2.last_call("turn_on") + assert data == {"brightness": 128, "xy_color": (0.701, 0.299)} + _, data = entity3.last_call("turn_on") + assert data == {"brightness": 128, "rgb_color": (128, 0, 0)} + _, data = entity4.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (0.0, 100.0)} + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": [ + entity0.entity_id, + entity1.entity_id, + entity2.entity_id, + entity3.entity_id, + entity4.entity_id, + ], + "brightness_pct": 50, + "xy_color": (0.1, 0.8), + }, + blocking=True, + ) + _, data = entity0.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (125.176, 100.0)} + _, data = entity1.last_call("turn_on") + assert data == {"brightness": 128, "rgb_color": (0, 255, 22)} + _, data = entity2.last_call("turn_on") + assert data == {"brightness": 128, "xy_color": (0.1, 0.8)} + _, data = entity3.last_call("turn_on") + assert data == {"brightness": 128, "xy_color": (0.1, 0.8)} + _, data = entity4.last_call("turn_on") + assert data == {"brightness": 128, "hs_color": (125.176, 100.0)} + + +async def test_light_state_color_conversion(hass): + """Test color conversion in state updates.""" + platform = getattr(hass.components, "test.light") + platform.init(empty=True) + + platform.ENTITIES.append(platform.MockLight("Test_hs", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_rgb", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_xy", STATE_ON)) + platform.ENTITIES.append(platform.MockLight("Test_legacy", STATE_ON)) + + entity0 = platform.ENTITIES[0] + entity0.supported_color_modes = {light.COLOR_MODE_HS} + entity0.color_mode = light.COLOR_MODE_HS + entity0.hs_color = (240, 100) + entity0.rgb_color = "Invalid" # Should be ignored + entity0.xy_color = "Invalid" # Should be ignored + + entity1 = platform.ENTITIES[1] + entity1.supported_color_modes = {light.COLOR_MODE_RGB} + entity1.color_mode = light.COLOR_MODE_RGB + entity1.hs_color = "Invalid" # Should be ignored + entity1.rgb_color = (128, 0, 0) + entity1.xy_color = "Invalid" # Should be ignored + + entity2 = platform.ENTITIES[2] + entity2.supported_color_modes = {light.COLOR_MODE_XY} + entity2.color_mode = light.COLOR_MODE_XY + entity2.hs_color = "Invalid" # Should be ignored + entity2.rgb_color = "Invalid" # Should be ignored + entity2.xy_color = (0.1, 0.8) + + entity3 = platform.ENTITIES[3] + entity3.hs_color = (240, 100) + entity3.supported_features = light.SUPPORT_COLOR + + assert await async_setup_component(hass, "light", {"light": {"platform": "test"}}) + await hass.async_block_till_done() + + state = hass.states.get(entity0.entity_id) + assert state.attributes["color_mode"] == light.COLOR_MODE_HS + assert state.attributes["hs_color"] == (240, 100) + assert state.attributes["rgb_color"] == (0, 0, 255) + assert state.attributes["xy_color"] == (0.136, 0.04) + + state = hass.states.get(entity1.entity_id) + assert state.attributes["color_mode"] == light.COLOR_MODE_RGB + assert state.attributes["hs_color"] == (0.0, 100.0) + assert state.attributes["rgb_color"] == (128, 0, 0) + assert state.attributes["xy_color"] == (0.701, 0.299) + + state = hass.states.get(entity2.entity_id) + assert state.attributes["color_mode"] == light.COLOR_MODE_XY + assert state.attributes["hs_color"] == (125.176, 100.0) + assert state.attributes["rgb_color"] == (0, 255, 22) + assert state.attributes["xy_color"] == (0.1, 0.8) + + state = hass.states.get(entity3.entity_id) + assert state.attributes["color_mode"] == light.COLOR_MODE_HS + assert state.attributes["hs_color"] == (240, 100) + assert state.attributes["rgb_color"] == (0, 0, 255) + assert state.attributes["xy_color"] == (0.136, 0.04) diff --git a/tests/components/light/test_reproduce_state.py b/tests/components/light/test_reproduce_state.py index e96f4ff4528..815b8831d37 100644 --- a/tests/components/light/test_reproduce_state.py +++ b/tests/components/light/test_reproduce_state.py @@ -1,4 +1,7 @@ """Test reproduce state for Light.""" +import pytest + +from homeassistant.components import light from homeassistant.components.light.reproduce_state import DEPRECATION_WARNING from homeassistant.core import State @@ -15,6 +18,8 @@ VALID_HS_COLOR = {"hs_color": (345, 75)} VALID_KELVIN = {"kelvin": 4000} VALID_PROFILE = {"profile": "relax"} VALID_RGB_COLOR = {"rgb_color": (255, 63, 111)} +VALID_RGBW_COLOR = {"rgbw_color": (255, 63, 111, 10)} +VALID_RGBWW_COLOR = {"rgbww_color": (255, 63, 111, 10, 20)} VALID_XY_COLOR = {"xy_color": (0.59, 0.274)} @@ -91,51 +96,51 @@ async def test_reproducing_states(hass, caplog): expected_calls = [] - expected_off = VALID_BRIGHTNESS + expected_off = dict(VALID_BRIGHTNESS) expected_off["entity_id"] = "light.entity_off" expected_calls.append(expected_off) - expected_bright = VALID_WHITE_VALUE + expected_bright = dict(VALID_WHITE_VALUE) expected_bright["entity_id"] = "light.entity_bright" expected_calls.append(expected_bright) - expected_white = VALID_FLASH + expected_white = dict(VALID_FLASH) expected_white["entity_id"] = "light.entity_white" expected_calls.append(expected_white) - expected_flash = VALID_EFFECT + expected_flash = dict(VALID_EFFECT) expected_flash["entity_id"] = "light.entity_flash" expected_calls.append(expected_flash) - expected_effect = VALID_TRANSITION + expected_effect = dict(VALID_TRANSITION) expected_effect["entity_id"] = "light.entity_effect" expected_calls.append(expected_effect) - expected_trans = VALID_COLOR_NAME + expected_trans = dict(VALID_COLOR_NAME) expected_trans["entity_id"] = "light.entity_trans" expected_calls.append(expected_trans) - expected_name = VALID_COLOR_TEMP + expected_name = dict(VALID_COLOR_TEMP) expected_name["entity_id"] = "light.entity_name" expected_calls.append(expected_name) - expected_temp = VALID_HS_COLOR + expected_temp = dict(VALID_HS_COLOR) expected_temp["entity_id"] = "light.entity_temp" expected_calls.append(expected_temp) - expected_hs = VALID_KELVIN + expected_hs = dict(VALID_KELVIN) expected_hs["entity_id"] = "light.entity_hs" expected_calls.append(expected_hs) - expected_kelvin = VALID_PROFILE + expected_kelvin = dict(VALID_PROFILE) expected_kelvin["entity_id"] = "light.entity_kelvin" expected_calls.append(expected_kelvin) - expected_profile = VALID_RGB_COLOR + expected_profile = dict(VALID_RGB_COLOR) expected_profile["entity_id"] = "light.entity_profile" expected_calls.append(expected_profile) - expected_rgb = VALID_XY_COLOR + expected_rgb = dict(VALID_XY_COLOR) expected_rgb["entity_id"] = "light.entity_rgb" expected_calls.append(expected_rgb) @@ -156,6 +161,59 @@ async def test_reproducing_states(hass, caplog): assert turn_off_calls[0].data == {"entity_id": "light.entity_xy"} +@pytest.mark.parametrize( + "color_mode", + ( + light.COLOR_MODE_COLOR_TEMP, + light.COLOR_MODE_BRIGHTNESS, + light.COLOR_MODE_HS, + light.COLOR_MODE_ONOFF, + light.COLOR_MODE_RGB, + light.COLOR_MODE_RGBW, + light.COLOR_MODE_RGBWW, + light.COLOR_MODE_UNKNOWN, + light.COLOR_MODE_XY, + ), +) +async def test_filter_color_modes(hass, caplog, color_mode): + """Test filtering of parameters according to color mode.""" + hass.states.async_set("light.entity", "off", {}) + all_colors = { + **VALID_WHITE_VALUE, + **VALID_COLOR_NAME, + **VALID_COLOR_TEMP, + **VALID_HS_COLOR, + **VALID_KELVIN, + **VALID_RGB_COLOR, + **VALID_RGBW_COLOR, + **VALID_RGBWW_COLOR, + **VALID_XY_COLOR, + } + + turn_on_calls = async_mock_service(hass, "light", "turn_on") + + await hass.helpers.state.async_reproduce_state( + [State("light.entity", "on", {**all_colors, "color_mode": color_mode})] + ) + + expected_map = { + light.COLOR_MODE_COLOR_TEMP: VALID_COLOR_TEMP, + light.COLOR_MODE_BRIGHTNESS: {}, + light.COLOR_MODE_HS: VALID_HS_COLOR, + light.COLOR_MODE_ONOFF: {}, + light.COLOR_MODE_RGB: VALID_RGB_COLOR, + light.COLOR_MODE_RGBW: VALID_RGBW_COLOR, + light.COLOR_MODE_RGBWW: VALID_RGBWW_COLOR, + light.COLOR_MODE_UNKNOWN: {**VALID_HS_COLOR, **VALID_WHITE_VALUE}, + light.COLOR_MODE_XY: VALID_XY_COLOR, + } + expected = expected_map[color_mode] + + assert len(turn_on_calls) == 1 + assert turn_on_calls[0].domain == "light" + assert dict(turn_on_calls[0].data) == {"entity_id": "light.entity", **expected} + + async def test_deprecation_warning(hass, caplog): """Test deprecation warning.""" hass.states.async_set("light.entity_off", "off", {}) diff --git a/tests/components/yeelight/test_light.py b/tests/components/yeelight/test_light.py index 90a8d1f9e6e..b6ce2fa4faf 100644 --- a/tests/components/yeelight/test_light.py +++ b/tests/components/yeelight/test_light.py @@ -430,6 +430,8 @@ async def test_device_types(hass: HomeAssistant): "effect_list": YEELIGHT_MONO_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT, "brightness": bright, + "color_mode": "brightness", + "supported_color_modes": ["brightness"], }, ) @@ -441,6 +443,8 @@ async def test_device_types(hass: HomeAssistant): "effect_list": YEELIGHT_MONO_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT, "brightness": bright, + "color_mode": "brightness", + "supported_color_modes": ["brightness"], }, ) @@ -463,8 +467,14 @@ async def test_device_types(hass: HomeAssistant): "hs_color": hs_color, "rgb_color": rgb_color, "xy_color": xy_color, + "color_mode": "hs", + "supported_color_modes": ["color_temp", "hs"], + }, + { + "supported_features": 0, + "color_mode": "onoff", + "supported_color_modes": ["onoff"], }, - {"supported_features": 0}, ) # WhiteTemp @@ -483,11 +493,15 @@ async def test_device_types(hass: HomeAssistant): ), "brightness": current_brightness, "color_temp": ct, + "color_mode": "color_temp", + "supported_color_modes": ["color_temp"], }, { "effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT, "brightness": nl_br, + "color_mode": "brightness", + "supported_color_modes": ["brightness"], }, ) @@ -512,11 +526,15 @@ async def test_device_types(hass: HomeAssistant): ), "brightness": current_brightness, "color_temp": ct, + "color_mode": "color_temp", + "supported_color_modes": ["color_temp"], }, { "effect_list": YEELIGHT_TEMP_ONLY_EFFECT_LIST, "supported_features": SUPPORT_YEELIGHT, "brightness": nl_br, + "color_mode": "brightness", + "supported_color_modes": ["brightness"], }, ) await _async_test( @@ -532,6 +550,8 @@ async def test_device_types(hass: HomeAssistant): "hs_color": bg_hs_color, "rgb_color": bg_rgb_color, "xy_color": bg_xy_color, + "color_mode": "hs", + "supported_color_modes": ["color_temp", "hs"], }, name=f"{UNIQUE_NAME} ambilight", entity_id=f"{ENTITY_LIGHT}_ambilight", diff --git a/tests/components/zerproc/test_light.py b/tests/components/zerproc/test_light.py index 1dc608e18df..9fac5085986 100644 --- a/tests/components/zerproc/test_light.py +++ b/tests/components/zerproc/test_light.py @@ -7,9 +7,12 @@ import pyzerproc from homeassistant import setup from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, ATTR_HS_COLOR, ATTR_RGB_COLOR, + ATTR_SUPPORTED_COLOR_MODES, ATTR_XY_COLOR, + COLOR_MODE_HS, SCAN_INTERVAL, SUPPORT_BRIGHTNESS, SUPPORT_COLOR, @@ -96,6 +99,7 @@ async def test_init(hass, mock_entry): assert state.state == STATE_OFF assert state.attributes == { ATTR_FRIENDLY_NAME: "LEDBlue-CCDDEEFF", + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS], ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR, ATTR_ICON: "mdi:string-lights", } @@ -104,8 +108,10 @@ async def test_init(hass, mock_entry): assert state.state == STATE_ON assert state.attributes == { ATTR_FRIENDLY_NAME: "LEDBlue-33445566", + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS], ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR, ATTR_ICON: "mdi:string-lights", + ATTR_COLOR_MODE: COLOR_MODE_HS, ATTR_BRIGHTNESS: 255, ATTR_HS_COLOR: (221.176, 100.0), ATTR_RGB_COLOR: (0, 80, 255), @@ -272,6 +278,7 @@ async def test_light_update(hass, mock_light): assert state.state == STATE_OFF assert state.attributes == { ATTR_FRIENDLY_NAME: "LEDBlue-CCDDEEFF", + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS], ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR, ATTR_ICON: "mdi:string-lights", } @@ -290,6 +297,7 @@ async def test_light_update(hass, mock_light): assert state.state == STATE_UNAVAILABLE assert state.attributes == { ATTR_FRIENDLY_NAME: "LEDBlue-CCDDEEFF", + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS], ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR, ATTR_ICON: "mdi:string-lights", } @@ -307,6 +315,7 @@ async def test_light_update(hass, mock_light): assert state.state == STATE_OFF assert state.attributes == { ATTR_FRIENDLY_NAME: "LEDBlue-CCDDEEFF", + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS], ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR, ATTR_ICON: "mdi:string-lights", } @@ -324,8 +333,10 @@ async def test_light_update(hass, mock_light): assert state.state == STATE_ON assert state.attributes == { ATTR_FRIENDLY_NAME: "LEDBlue-CCDDEEFF", + ATTR_SUPPORTED_COLOR_MODES: [COLOR_MODE_HS], ATTR_SUPPORTED_FEATURES: SUPPORT_BRIGHTNESS | SUPPORT_COLOR, ATTR_ICON: "mdi:string-lights", + ATTR_COLOR_MODE: COLOR_MODE_HS, ATTR_BRIGHTNESS: 220, ATTR_HS_COLOR: (261.429, 31.818), ATTR_RGB_COLOR: (202, 173, 255), diff --git a/tests/testing_config/custom_components/test/light.py b/tests/testing_config/custom_components/test/light.py index 863412fe747..84008d90c27 100644 --- a/tests/testing_config/custom_components/test/light.py +++ b/tests/testing_config/custom_components/test/light.py @@ -37,4 +37,17 @@ class MockLight(MockToggleEntity, LightEntity): """Mock light class.""" brightness = None + supported_color_modes = None supported_features = 0 + + color_mode = None + + hs_color = None + xy_color = None + rgb_color = None + rgbw_color = None + rgbww_color = None + + color_temp = None + + white_value = None