Skip to content

Commit

Permalink
Support KNX lights with multiple color modes (#130842)
Browse files Browse the repository at this point in the history
  • Loading branch information
farmio authored Nov 18, 2024
1 parent c154ac2 commit 2f1c1d6
Show file tree
Hide file tree
Showing 2 changed files with 164 additions and 36 deletions.
67 changes: 39 additions & 28 deletions homeassistant/components/knx/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@

from typing import Any, cast

from propcache import cached_property
from xknx import XKNX
from xknx.devices.light import ColorTemperatureType, Light as XknxLight, XYYColor

Expand Down Expand Up @@ -389,39 +390,47 @@ def color_temp_kelvin(self) -> int | None:
)
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
Expand Down Expand Up @@ -500,17 +509,17 @@ async def set_color(
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)
Expand All @@ -533,6 +542,7 @@ def __init__(self, knx_module: KNXModule, config: ConfigType) -> None:
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)
Expand Down Expand Up @@ -566,5 +576,6 @@ def __init__(
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]
133 changes: 125 additions & 8 deletions tests/components/knx/test_light.py
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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),
)
Expand Down Expand Up @@ -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),
)
Expand Down Expand Up @@ -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),
)
Expand Down Expand Up @@ -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),
)
Expand Down Expand Up @@ -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),
)
Expand Down Expand Up @@ -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),
)
Expand Down Expand Up @@ -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),
)
Expand Down Expand Up @@ -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),
)
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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(
Expand All @@ -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

0 comments on commit 2f1c1d6

Please sign in to comment.