Support KNX lights with multiple color modes (#130842)

This commit is contained in:
Matthias Alphart 2024-11-18 10:22:10 +01:00 committed by GitHub
parent c154ac26eb
commit 2f1c1d66cb
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 164 additions and 36 deletions

View File

@ -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]

View File

@ -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