diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index ba1194220c2..8e64b46c890 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -4,6 +4,7 @@ from __future__ import annotations from typing import Any, cast +from propcache import cached_property from xknx import XKNX from xknx.devices.light import ColorTemperatureType, Light as XknxLight, XYYColor @@ -389,39 +390,47 @@ class _KnxLight(LightEntity): ) return None - @property - def color_mode(self) -> ColorMode: - """Return the color mode of the light.""" - if self._device.supports_xyy_color: - return ColorMode.XY - if self._device.supports_hs_color: - return ColorMode.HS - if self._device.supports_rgbw: - return ColorMode.RGBW - if self._device.supports_color: - return ColorMode.RGB + @cached_property + def supported_color_modes(self) -> set[ColorMode]: + """Get supported color modes.""" + color_mode = set() if ( self._device.supports_color_temperature or self._device.supports_tunable_white ): - return ColorMode.COLOR_TEMP - if self._device.supports_brightness: - return ColorMode.BRIGHTNESS - return ColorMode.ONOFF - - @property - def supported_color_modes(self) -> set[ColorMode]: - """Flag supported color modes.""" - return {self.color_mode} + color_mode.add(ColorMode.COLOR_TEMP) + if self._device.supports_xyy_color: + color_mode.add(ColorMode.XY) + if self._device.supports_rgbw: + color_mode.add(ColorMode.RGBW) + elif self._device.supports_color: + # one of RGB or RGBW so individual color configurations work properly + color_mode.add(ColorMode.RGB) + if self._device.supports_hs_color: + color_mode.add(ColorMode.HS) + if not color_mode: + # brightness or on/off must be the only supported mode + if self._device.supports_brightness: + color_mode.add(ColorMode.BRIGHTNESS) + else: + color_mode.add(ColorMode.ONOFF) + return color_mode async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" brightness = kwargs.get(ATTR_BRIGHTNESS) - color_temp = kwargs.get(ATTR_COLOR_TEMP_KELVIN) - rgb = kwargs.get(ATTR_RGB_COLOR) - rgbw = kwargs.get(ATTR_RGBW_COLOR) - hs_color = kwargs.get(ATTR_HS_COLOR) - xy_color = kwargs.get(ATTR_XY_COLOR) + # LightEntity color translation will ensure that only attributes of supported + # color modes are passed to this method - so we can't set unsupported mode here + if color_temp := kwargs.get(ATTR_COLOR_TEMP_KELVIN): + self._attr_color_mode = ColorMode.COLOR_TEMP + if rgb := kwargs.get(ATTR_RGB_COLOR): + self._attr_color_mode = ColorMode.RGB + if rgbw := kwargs.get(ATTR_RGBW_COLOR): + self._attr_color_mode = ColorMode.RGBW + if hs_color := kwargs.get(ATTR_HS_COLOR): + self._attr_color_mode = ColorMode.HS + if xy_color := kwargs.get(ATTR_XY_COLOR): + self._attr_color_mode = ColorMode.XY if ( not self.is_on @@ -500,17 +509,17 @@ class _KnxLight(LightEntity): await self._device.set_brightness(brightness) return # brightness without color in kwargs; set via color - if self.color_mode == ColorMode.XY: + if self._attr_color_mode == ColorMode.XY: await self._device.set_xyy_color(XYYColor(brightness=brightness)) return # default to white if color not known for RGB(W) - if self.color_mode == ColorMode.RGBW: + if self._attr_color_mode == ColorMode.RGBW: _rgbw = self.rgbw_color if not _rgbw or not any(_rgbw): _rgbw = (0, 0, 0, 255) await set_color(_rgbw[:3], _rgbw[3], brightness) return - if self.color_mode == ColorMode.RGB: + if self._attr_color_mode == ColorMode.RGB: _rgb = self.rgb_color if not _rgb or not any(_rgb): _rgb = (255, 255, 255) @@ -533,6 +542,7 @@ class KnxYamlLight(_KnxLight, KnxYamlEntity): knx_module=knx_module, device=_create_yaml_light(knx_module.xknx, config), ) + self._attr_color_mode = next(iter(self.supported_color_modes)) self._attr_max_color_temp_kelvin: int = config[LightSchema.CONF_MAX_KELVIN] self._attr_min_color_temp_kelvin: int = config[LightSchema.CONF_MIN_KELVIN] self._attr_entity_category = config.get(CONF_ENTITY_CATEGORY) @@ -566,5 +576,6 @@ class KnxUiLight(_KnxLight, KnxUiEntity): self._device = _create_ui_light( knx_module.xknx, config[DOMAIN], config[CONF_ENTITY][CONF_NAME] ) + self._attr_color_mode = next(iter(self.supported_color_modes)) self._attr_max_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MAX] self._attr_min_color_temp_kelvin: int = config[DOMAIN][CONF_COLOR_TEMP_MIN] diff --git a/tests/components/knx/test_light.py b/tests/components/knx/test_light.py index 88f76a163d5..6ba6090d60d 100644 --- a/tests/components/knx/test_light.py +++ b/tests/components/knx/test_light.py @@ -41,7 +41,11 @@ async def test_light_simple(hass: HomeAssistant, knx: KNXTestKit) -> None: } ) - knx.assert_state("light.test", STATE_OFF) + knx.assert_state( + "light.test", + STATE_OFF, + supported_color_modes=[ColorMode.ONOFF], + ) # turn on light await hass.services.async_call( "light", @@ -110,6 +114,7 @@ async def test_light_brightness(hass: HomeAssistant, knx: KNXTestKit) -> None: "light.test", STATE_ON, brightness=80, + supported_color_modes=[ColorMode.BRIGHTNESS], color_mode=ColorMode.BRIGHTNESS, ) # receive brightness changes from KNX @@ -165,6 +170,7 @@ async def test_light_color_temp_absolute(hass: HomeAssistant, knx: KNXTestKit) - "light.test", STATE_ON, brightness=255, + supported_color_modes=[ColorMode.COLOR_TEMP], color_mode=ColorMode.COLOR_TEMP, color_temp=370, color_temp_kelvin=2700, @@ -227,6 +233,7 @@ async def test_light_color_temp_relative(hass: HomeAssistant, knx: KNXTestKit) - "light.test", STATE_ON, brightness=255, + supported_color_modes=[ColorMode.COLOR_TEMP], color_mode=ColorMode.COLOR_TEMP, color_temp=250, color_temp_kelvin=4000, @@ -300,6 +307,7 @@ async def test_light_hs_color(hass: HomeAssistant, knx: KNXTestKit) -> None: "light.test", STATE_ON, brightness=255, + supported_color_modes=[ColorMode.HS], color_mode=ColorMode.HS, hs_color=(360, 100), ) @@ -375,6 +383,7 @@ async def test_light_xyy_color(hass: HomeAssistant, knx: KNXTestKit) -> None: "light.test", STATE_ON, brightness=204, + supported_color_modes=[ColorMode.XY], color_mode=ColorMode.XY, xy_color=(0.8, 0.8), ) @@ -457,6 +466,7 @@ async def test_light_xyy_color_with_brightness( "light.test", STATE_ON, brightness=255, # brightness form xyy_color ignored when extra brightness GA is used + supported_color_modes=[ColorMode.XY], color_mode=ColorMode.XY, xy_color=(0.8, 0.8), ) @@ -543,6 +553,7 @@ async def test_light_rgb_individual(hass: HomeAssistant, knx: KNXTestKit) -> Non "light.test", STATE_ON, brightness=255, + supported_color_modes=[ColorMode.RGB], color_mode=ColorMode.RGB, rgb_color=(255, 255, 255), ) @@ -699,6 +710,7 @@ async def test_light_rgbw_individual( "light.test", STATE_ON, brightness=255, + supported_color_modes=[ColorMode.RGBW], color_mode=ColorMode.RGBW, rgbw_color=(0, 0, 0, 255), ) @@ -853,6 +865,7 @@ async def test_light_rgb(hass: HomeAssistant, knx: KNXTestKit) -> None: "light.test", STATE_ON, brightness=255, + supported_color_modes=[ColorMode.RGB], color_mode=ColorMode.RGB, rgb_color=(255, 255, 255), ) @@ -961,6 +974,7 @@ async def test_light_rgbw(hass: HomeAssistant, knx: KNXTestKit) -> None: "light.test", STATE_ON, brightness=255, + supported_color_modes=[ColorMode.RGBW], color_mode=ColorMode.RGBW, rgbw_color=(255, 101, 102, 103), ) @@ -1078,6 +1092,7 @@ async def test_light_rgbw_brightness(hass: HomeAssistant, knx: KNXTestKit) -> No "light.test", STATE_ON, brightness=255, + supported_color_modes=[ColorMode.RGBW], color_mode=ColorMode.RGBW, rgbw_color=(255, 101, 102, 103), ) @@ -1174,8 +1189,12 @@ async def test_light_ui_create( # created entity sends read-request to KNX bus await knx.assert_read("2/2/2") await knx.receive_response("2/2/2", True) - state = hass.states.get("light.test") - assert state.state is STATE_ON + knx.assert_state( + "light.test", + STATE_ON, + supported_color_modes=[ColorMode.ONOFF], + color_mode=ColorMode.ONOFF, + ) @pytest.mark.parametrize( @@ -1216,9 +1235,103 @@ async def test_light_ui_color_temp( blocking=True, ) await knx.assert_write("3/3/3", raw_ct) - state = hass.states.get("light.test") - assert state.state is STATE_ON - assert state.attributes[ATTR_COLOR_TEMP_KELVIN] == pytest.approx(4200, abs=1) + knx.assert_state( + "light.test", + STATE_ON, + supported_color_modes=[ColorMode.COLOR_TEMP], + color_mode=ColorMode.COLOR_TEMP, + color_temp_kelvin=pytest.approx(4200, abs=1), + ) + + +async def test_light_ui_multi_mode( + hass: HomeAssistant, + knx: KNXTestKit, + create_ui_entity: KnxEntityGenerator, +) -> None: + """Test creating a light with multiple color modes.""" + await knx.setup_integration({}) + await create_ui_entity( + platform=Platform.LIGHT, + entity_data={"name": "test"}, + knx_data={ + "color_temp_min": 2700, + "color_temp_max": 6000, + "_light_color_mode_schema": "default", + "ga_switch": { + "write": "1/1/1", + "passive": [], + "state": "2/2/2", + }, + "sync_state": True, + "ga_brightness": { + "write": "0/6/0", + "state": "0/6/1", + "passive": [], + }, + "ga_color_temp": { + "write": "0/6/2", + "dpt": "7.600", + "state": "0/6/3", + "passive": [], + }, + "ga_color": { + "write": "0/6/4", + "dpt": "251.600", + "state": "0/6/5", + "passive": [], + }, + }, + ) + await knx.assert_read("2/2/2", True) + await knx.assert_read("0/6/1", (0xFF,)) + await knx.assert_read("0/6/5", (0xFF, 0x65, 0x66, 0x67, 0x00, 0x0F)) + await knx.assert_read("0/6/3", (0x12, 0x34)) + + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": "light.test", + ATTR_COLOR_NAME: "hotpink", + }, + blocking=True, + ) + await knx.assert_write("0/6/4", (255, 0, 128, 178, 0, 15)) + knx.assert_state( + "light.test", + STATE_ON, + brightness=255, + color_temp_kelvin=None, + rgbw_color=(255, 0, 128, 178), + supported_color_modes=[ + ColorMode.COLOR_TEMP, + ColorMode.RGBW, + ], + color_mode=ColorMode.RGBW, + ) + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": "light.test", + ATTR_COLOR_TEMP_KELVIN: 4200, + }, + blocking=True, + ) + await knx.assert_write("0/6/2", (0x10, 0x68)) + knx.assert_state( + "light.test", + STATE_ON, + brightness=255, + color_temp_kelvin=4200, + rgbw_color=None, + supported_color_modes=[ + ColorMode.COLOR_TEMP, + ColorMode.RGBW, + ], + color_mode=ColorMode.COLOR_TEMP, + ) async def test_light_ui_load( @@ -1234,8 +1347,12 @@ async def test_light_ui_load( # unrelated switch in config store await knx.assert_read("1/0/45", response=True, ignore_order=True) - state = hass.states.get("light.test") - assert state.state is STATE_ON + knx.assert_state( + "light.test", + STATE_ON, + supported_color_modes=[ColorMode.ONOFF], + color_mode=ColorMode.ONOFF, + ) entity = entity_registry.async_get("light.test") assert entity.entity_category is EntityCategory.CONFIG