diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index 693816635af..d3086cacd0f 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -2,7 +2,7 @@ from __future__ import annotations from collections.abc import Iterable -from typing import Any, Callable +from typing import Any, Callable, cast from xknx.devices import Light as XknxLight from xknx.telegram.address import parse_device_group_address @@ -10,12 +10,13 @@ from xknx.telegram.address import parse_device_group_address from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, - ATTR_HS_COLOR, - ATTR_WHITE_VALUE, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, - SUPPORT_WHITE_VALUE, + ATTR_RGB_COLOR, + ATTR_RGBW_COLOR, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_ONOFF, + COLOR_MODE_RGB, + COLOR_MODE_RGBW, LightEntity, ) from homeassistant.core import HomeAssistant, callback @@ -28,10 +29,6 @@ from .const import DOMAIN, KNX_ADDRESS from .knx_entity import KnxEntity from .schema import LightSchema -DEFAULT_COLOR = (0.0, 0.0) -DEFAULT_BRIGHTNESS = 255 -DEFAULT_WHITE_VALUE = 255 - async def async_setup_platform( hass: HomeAssistant, @@ -146,39 +143,39 @@ class KNXLight(KnxEntity, LightEntity): f"{self._device.blue.brightness.group_address}" ) + @property + def is_on(self) -> bool: + """Return true if light is on.""" + return bool(self._device.state) + @property def brightness(self) -> int | None: """Return the brightness of this light between 0..255.""" if self._device.supports_brightness: return self._device.current_brightness - hsv_color = self._hsv_color - if self._device.supports_color and hsv_color: - return round(hsv_color[-1] / 100 * 255) + if (rgb := self.rgb_color) is not None: + return max(rgb) return None @property - def hs_color(self) -> tuple[float, float] | None: - """Return the HS color value.""" - rgb: tuple[int, int, int] | None = None - if self._device.supports_rgbw or self._device.supports_color: + def rgb_color(self) -> tuple[int, int, int] | None: + """Return the rgb color value [int, int, int].""" + if (rgbw := self.rgbw_color) is not None: + # used in brightness calculation when no address is given + return color_util.color_rgbw_to_rgb(*rgbw) + if self._device.supports_color: rgb, _ = self._device.current_color - return color_util.color_RGB_to_hs(*rgb) if rgb else None + return rgb + return None @property - def _hsv_color(self) -> tuple[float, float, float] | None: - """Return the HSV color value.""" - rgb: tuple[int, int, int] | None = None - if self._device.supports_rgbw or self._device.supports_color: - rgb, _ = self._device.current_color - return color_util.color_RGB_to_hsv(*rgb) if rgb else None - - @property - def white_value(self) -> int | None: - """Return the white value.""" - white: int | None = None + def rgbw_color(self) -> tuple[int, int, int, int] | None: + """Return the rgbw color value [int, int, int, int].""" if self._device.supports_rgbw: - _, white = self._device.current_color - return white + rgb, white = self._device.current_color + if rgb is not None and white is not None: + return (*rgb, white) + return None @property def color_temp(self) -> int | None: @@ -210,83 +207,80 @@ class KNXLight(KnxEntity, LightEntity): return self._max_mireds @property - def effect_list(self) -> list[str] | None: - """Return the list of supported effects.""" - return None - - @property - def effect(self) -> str | None: - """Return the current effect.""" - return None - - @property - def is_on(self) -> bool: - """Return true if light is on.""" - return bool(self._device.state) - - @property - def supported_features(self) -> int: - """Flag supported features.""" - flags = 0 - if self._device.supports_brightness: - flags |= SUPPORT_BRIGHTNESS - if self._device.supports_color: - flags |= SUPPORT_COLOR | SUPPORT_BRIGHTNESS + def color_mode(self) -> str | None: + """Return the color mode of the light.""" if self._device.supports_rgbw: - flags |= SUPPORT_COLOR | SUPPORT_WHITE_VALUE + return COLOR_MODE_RGBW + if self._device.supports_color: + return COLOR_MODE_RGB if ( self._device.supports_color_temperature or self._device.supports_tunable_white ): - flags |= SUPPORT_COLOR_TEMP - return flags + return COLOR_MODE_COLOR_TEMP + if self._device.supports_brightness: + return COLOR_MODE_BRIGHTNESS + return COLOR_MODE_ONOFF + + @property + def supported_color_modes(self) -> set | None: + """Flag supported color modes.""" + return {self.color_mode} async def async_turn_on(self, **kwargs: Any) -> None: """Turn the light on.""" - brightness = kwargs.get(ATTR_BRIGHTNESS, self.brightness) - hs_color = kwargs.get(ATTR_HS_COLOR, self.hs_color) - white_value = kwargs.get(ATTR_WHITE_VALUE, self.white_value) - mireds = kwargs.get(ATTR_COLOR_TEMP, self.color_temp) + # ignore arguments if not supported to fall back to set_on() + brightness = ( + kwargs.get(ATTR_BRIGHTNESS) + if self._device.supports_brightness + or self.color_mode in (COLOR_MODE_RGB, COLOR_MODE_RGBW) + else None + ) + mireds = ( + kwargs.get(ATTR_COLOR_TEMP) + if self.color_mode == COLOR_MODE_COLOR_TEMP + else None + ) + rgb = kwargs.get(ATTR_RGB_COLOR) if self.color_mode == COLOR_MODE_RGB else None + rgbw = ( + kwargs.get(ATTR_RGBW_COLOR) if self.color_mode == COLOR_MODE_RGBW else None + ) - update_brightness = ATTR_BRIGHTNESS in kwargs - update_color = ATTR_HS_COLOR in kwargs - update_white_value = ATTR_WHITE_VALUE in kwargs - update_color_temp = ATTR_COLOR_TEMP in kwargs - - # avoid conflicting changes and weird effects - if not ( - self.is_on - or update_brightness - or update_color - or update_white_value - or update_color_temp + if ( + not self.is_on + and brightness is None + and mireds is None + and rgb is None + and rgbw is None ): await self._device.set_on() + return - if self._device.supports_brightness and ( - update_brightness and not update_color - ): - # if we don't need to update the color, try updating brightness - # directly if supported; don't do it if color also has to be - # changed, as RGB color implicitly sets the brightness as well - await self._device.set_brightness(brightness) - elif (self._device.supports_rgbw or self._device.supports_color) and ( - update_brightness or update_color or update_white_value - ): - # change RGB color, white value (if supported), and brightness - # if brightness or hs_color was not yet set use the default value - # to calculate RGB from as a fallback - if brightness is None: - brightness = DEFAULT_BRIGHTNESS - if hs_color is None: - hs_color = DEFAULT_COLOR - if white_value is None and self._device.supports_rgbw: - white_value = DEFAULT_WHITE_VALUE - hsv_color = hs_color + (brightness * 100 / 255,) - rgb = color_util.color_hsv_to_RGB(*hsv_color) - await self._device.set_color(rgb, white_value) + async def set_color( + rgb: tuple[int, int, int], white: int | None, brightness: int | None + ) -> None: + """Set color of light. Normalize colors for brightness when not writable.""" + if brightness: + if self._device.brightness.writable: + await self._device.set_color(rgb, white) + await self._device.set_brightness(brightness) + return + rgb = cast( + tuple[int, int, int], + tuple(color * brightness // 255 for color in rgb), + ) + white = white * brightness // 255 if white is not None else None + await self._device.set_color(rgb, white) - if update_color_temp: + # return after RGB(W) color has changed as it implicitly sets the brightness + if rgbw is not None: + await set_color(rgbw[:3], rgbw[3], brightness) + return + if rgb is not None: + await set_color(rgb, None, brightness) + return + + if mireds is not None: kelvin = int(color_util.color_temperature_mired_to_kelvin(mireds)) kelvin = min(self._max_kelvin, max(self._min_kelvin, kelvin)) @@ -300,6 +294,9 @@ class KNXLight(KnxEntity, LightEntity): ) await self._device.set_tunable_white(relative_ct) + if brightness is not None: + await self._device.set_brightness(brightness) + async def async_turn_off(self, **kwargs: Any) -> None: """Turn the light off.""" await self._device.set_off()