diff --git a/homeassistant/components/knx/light.py b/homeassistant/components/knx/light.py index bee96270c36..4841b056ebc 100644 --- a/homeassistant/components/knx/light.py +++ b/homeassistant/components/knx/light.py @@ -268,19 +268,28 @@ class KNXLight(KnxEntity, LightEntity): if self._device.current_xyy_color is not None: _, brightness = self._device.current_xyy_color return brightness - if (rgb := self.rgb_color) is not None: - return max(rgb) + if self._device.supports_color or self._device.supports_rgbw: + rgb, white = self._device.current_color + if rgb is None: + return white + if white is None: + return max(rgb) + return max(*rgb, white) return None @property 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 rgb + if rgb is not None: + if not self._device.supports_brightness: + # brightness will be calculated from color so color must not hold brightness again + # pylint: disable=protected-access + return cast( + Tuple[int, int, int], color_util.match_max_scale((255,), rgb) + ) + return rgb return None @property @@ -289,6 +298,13 @@ class KNXLight(KnxEntity, LightEntity): if self._device.supports_rgbw: rgb, white = self._device.current_color if rgb is not None and white is not None: + if not self._device.supports_brightness: + # brightness will be calculated from color so color must not hold brightness again + # pylint: disable=protected-access + return cast( + Tuple[int, int, int, int], + color_util.match_max_scale((255,), (*rgb, white)), + ) return (*rgb, white) return None @@ -376,16 +392,21 @@ class KNXLight(KnxEntity, LightEntity): 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) + if self._device.brightness.writable: + # let the KNX light controller handle brightness + await self._device.set_color(rgb, white) + if brightness: 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 + return + + if brightness is None: + # normalize for brightness if brightness is derived from color + brightness = self.brightness or 255 + 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) # return after RGB(W) color has changed as it implicitly sets the brightness @@ -433,18 +454,16 @@ class KNXLight(KnxEntity, LightEntity): return # default to white if color not known for RGB(W) if self.color_mode == COLOR_MODE_RGBW: - rgbw = self.rgbw_color - if not rgbw or not any(rgbw): - await self._device.set_color((0, 0, 0), brightness) - return - await set_color(rgbw[:3], rgbw[3], brightness) + _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 == COLOR_MODE_RGB: - rgb = self.rgb_color - if not rgb or not any(rgb): - await self._device.set_color((brightness, brightness, brightness)) - return - await set_color(rgb, None, brightness) + _rgb = self.rgb_color + if not _rgb or not any(_rgb): + _rgb = (255, 255, 255) + await set_color(_rgb, None, brightness) return async def async_turn_off(self, **kwargs: Any) -> None: diff --git a/tests/components/knx/test_light.py b/tests/components/knx/test_light.py index bb7a67d6bb4..aa9a373e5ef 100644 --- a/tests/components/knx/test_light.py +++ b/tests/components/knx/test_light.py @@ -8,6 +8,7 @@ from homeassistant.components.light import ( ATTR_COLOR_NAME, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + ATTR_RGBW_COLOR, COLOR_MODE_BRIGHTNESS, COLOR_MODE_COLOR_TEMP, COLOR_MODE_HS, @@ -539,20 +540,31 @@ async def test_light_rgb_individual(hass: HomeAssistant, knx: KNXTestKit): await knx.assert_write(test_red, (200,)) await knx.assert_write(test_green, (0,)) await knx.assert_write(test_blue, (0,)) - knx.assert_state("light.test", STATE_ON, brightness=200, rgb_color=(200, 0, 0)) + knx.assert_state("light.test", STATE_ON, brightness=200, rgb_color=(255, 0, 0)) - # change color and brightness from HA + # change only color, keep brightness from HA await hass.services.async_call( "light", "turn_on", {"entity_id": "light.test", ATTR_COLOR_NAME: "hotpink"}, blocking=True, ) - # - await knx.assert_write(test_red, (255,)) - await knx.assert_write(test_green, (105,)) - await knx.assert_write(test_blue, (180,)) - knx.assert_state("light.test", STATE_ON, brightness=255, rgb_color=(255, 105, 180)) + await knx.assert_write(test_red, (200,)) + await knx.assert_write(test_green, (82,)) + await knx.assert_write(test_blue, (141,)) + knx.assert_state("light.test", STATE_ON, brightness=200, rgb_color=(255, 105, 180)) + + # change color and brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_BRIGHTNESS: 100, ATTR_COLOR_NAME: "yellow"}, + blocking=True, + ) + await knx.assert_write(test_red, (100,)) + await knx.assert_write(test_green, (100,)) + await knx.assert_write(test_blue, (0,)) + knx.assert_state("light.test", STATE_ON, brightness=100, rgb_color=(255, 255, 0)) # turn OFF from KNX await knx.receive_write(test_red, (0,)) @@ -563,7 +575,7 @@ async def test_light_rgb_individual(hass: HomeAssistant, knx: KNXTestKit): await knx.receive_write(test_red, (0,)) await knx.receive_write(test_green, (180,)) await knx.receive_write(test_blue, (0,)) - knx.assert_state("light.test", STATE_ON, brightness=180, rgb_color=(0, 180, 0)) + knx.assert_state("light.test", STATE_ON, brightness=180, rgb_color=(0, 255, 0)) # turn OFF from HA await hass.services.async_call( @@ -684,40 +696,52 @@ async def test_light_rgbw_individual(hass: HomeAssistant, knx: KNXTestKit): await knx.assert_write(test_green, (0,)) await knx.assert_write(test_blue, (0,)) await knx.assert_write(test_white, (0,)) - knx.assert_state("light.test", STATE_ON, brightness=200, rgbw_color=(200, 0, 0, 0)) + knx.assert_state("light.test", STATE_ON, brightness=200, rgbw_color=(255, 0, 0, 0)) - # change color and brightness from HA + # change only color, keep brightness from HA await hass.services.async_call( "light", "turn_on", {"entity_id": "light.test", ATTR_COLOR_NAME: "hotpink"}, blocking=True, ) - await knx.assert_write(test_red, (255,)) + await knx.assert_write(test_red, (200,)) await knx.assert_write(test_green, (0,)) - await knx.assert_write(test_blue, (128,)) - await knx.assert_write(test_white, (178,)) + await knx.assert_write(test_blue, (100,)) + await knx.assert_write(test_white, (139,)) knx.assert_state( "light.test", STATE_ON, - brightness=255, - rgb_color=(255, 105, 180), - rgbw_color=(255, 0, 128, 178), + brightness=200, + rgb_color=(255, 104, 179), # minor rounding error - expected (255, 105, 180) + rgbw_color=(255, 0, 127, 177), # expected (255, 0, 128, 178) + ) + + # change color and brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_BRIGHTNESS: 100, ATTR_COLOR_NAME: "yellow"}, + blocking=True, + ) + await knx.assert_write(test_red, (100,)) + await knx.assert_write(test_green, (100,)) + await knx.assert_write(test_blue, (0,)) + await knx.assert_write(test_white, (0,)) + knx.assert_state( + "light.test", STATE_ON, brightness=100, rgbw_color=(255, 255, 0, 0) ) # turn OFF from KNX await knx.receive_write(test_red, (0,)) await knx.receive_write(test_green, (0,)) - await knx.receive_write(test_blue, (0,)) - knx.assert_state("light.test", STATE_ON) - await knx.receive_write(test_white, (0,)) knx.assert_state("light.test", STATE_OFF) # turn ON from KNX await knx.receive_write(test_red, (0,)) await knx.receive_write(test_green, (180,)) await knx.receive_write(test_blue, (0,)) await knx.receive_write(test_white, (0,)) - knx.assert_state("light.test", STATE_ON, brightness=180, rgbw_color=(0, 180, 0, 0)) + knx.assert_state("light.test", STATE_ON, brightness=180, rgbw_color=(0, 255, 0, 0)) # turn OFF from HA await hass.services.async_call( @@ -815,18 +839,31 @@ async def test_light_rgb(hass: HomeAssistant, knx: KNXTestKit): blocking=True, ) await knx.assert_write(test_rgb, (200, 0, 0)) - knx.assert_state("light.test", STATE_ON, brightness=200, rgb_color=(200, 0, 0)) + knx.assert_state("light.test", STATE_ON, brightness=200, rgb_color=(255, 0, 0)) + # change color, keep brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_COLOR_NAME: "hotpink"}, + blocking=True, + ) + await knx.assert_write(test_rgb, (200, 82, 141)) + knx.assert_state( + "light.test", + STATE_ON, + brightness=200, + rgb_color=(255, 105, 180), + ) # change color and brightness from HA await hass.services.async_call( "light", "turn_on", - {"entity_id": "light.test", ATTR_BRIGHTNESS: 128, ATTR_COLOR_NAME: "hotpink"}, + {"entity_id": "light.test", ATTR_BRIGHTNESS: 100, ATTR_COLOR_NAME: "yellow"}, blocking=True, ) - # - await knx.assert_write(test_rgb, (128, 52, 90)) - knx.assert_state("light.test", STATE_ON, brightness=128, rgb_color=(128, 52, 90)) + await knx.assert_write(test_rgb, (100, 100, 0)) + knx.assert_state("light.test", STATE_ON, brightness=100, rgb_color=(255, 255, 0)) # turn OFF from KNX await knx.receive_write(test_address_state, False) @@ -836,7 +873,7 @@ async def test_light_rgb(hass: HomeAssistant, knx: KNXTestKit): knx.assert_state("light.test", STATE_OFF) # turn ON from KNX - include color update await knx.receive_write(test_address_state, True) - knx.assert_state("light.test", STATE_ON, brightness=180, rgb_color=(0, 180, 0)) + knx.assert_state("light.test", STATE_ON, brightness=180, rgb_color=(0, 255, 0)) # turn OFF from HA await hass.services.async_call( @@ -857,7 +894,7 @@ async def test_light_rgb(hass: HomeAssistant, knx: KNXTestKit): ) # color will be restored in no other state was received await knx.assert_write(test_address, True) - knx.assert_state("light.test", STATE_ON, brightness=180, rgb_color=(0, 180, 0)) + knx.assert_state("light.test", STATE_ON, brightness=180, rgb_color=(0, 255, 0)) async def test_light_rgbw(hass: HomeAssistant, knx: KNXTestKit): @@ -883,14 +920,14 @@ async def test_light_rgbw(hass: HomeAssistant, knx: KNXTestKit): await knx.assert_read(test_address_state) await knx.assert_read(test_rgbw_state) await knx.receive_response(test_address_state, True) - await knx.receive_response(test_rgbw_state, (0x64, 0x65, 0x66, 0x67, 0x00, 0x0F)) + await knx.receive_response(test_rgbw_state, (0xFF, 0x65, 0x66, 0x67, 0x00, 0x0F)) knx.assert_state( "light.test", STATE_ON, - brightness=103, + brightness=255, color_mode=COLOR_MODE_RGBW, - rgbw_color=(100, 101, 102, 103), + rgbw_color=(255, 101, 102, 103), ) # change color from HA await hass.services.async_call( @@ -910,22 +947,33 @@ async def test_light_rgbw(hass: HomeAssistant, knx: KNXTestKit): blocking=True, ) await knx.assert_write(test_rgbw, (0xC8, 0x00, 0x00, 0x00, 0x00, 0x0F)) - knx.assert_state("light.test", STATE_ON, brightness=200, rgbw_color=(200, 0, 0, 0)) + knx.assert_state("light.test", STATE_ON, brightness=200, rgbw_color=(255, 0, 0, 0)) + # change color, keep brightness from HA + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_COLOR_NAME: "hotpink"}, + blocking=True, + ) + await knx.assert_write(test_rgbw, (200, 0, 100, 139, 0x00, 0x0F)) + knx.assert_state( + "light.test", + STATE_ON, + brightness=200, + rgb_color=(255, 104, 179), # minor rounding error - expected (255, 105, 180) + rgbw_color=(255, 0, 127, 177), # expected (255, 0, 128, 178) + ) # change color and brightness from HA await hass.services.async_call( "light", "turn_on", - {"entity_id": "light.test", ATTR_BRIGHTNESS: 128, ATTR_COLOR_NAME: "hotpink"}, + {"entity_id": "light.test", ATTR_BRIGHTNESS: 100, ATTR_COLOR_NAME: "yellow"}, blocking=True, ) - await knx.assert_write(test_rgbw, (128, 0, 64, 89, 0x00, 0x0F)) + await knx.assert_write(test_rgbw, (100, 100, 0, 0, 0x00, 0x0F)) knx.assert_state( - "light.test", - STATE_ON, - brightness=128, - rgb_color=(128, 52, 90), - rgbw_color=(128, 0, 64, 89), + "light.test", STATE_ON, brightness=100, rgbw_color=(255, 255, 0, 0) ) # turn OFF from KNX @@ -936,7 +984,7 @@ async def test_light_rgbw(hass: HomeAssistant, knx: KNXTestKit): knx.assert_state("light.test", STATE_OFF) # turn ON from KNX - include color update await knx.receive_write(test_address_state, True) - knx.assert_state("light.test", STATE_ON, brightness=180, rgbw_color=(0, 180, 0, 0)) + knx.assert_state("light.test", STATE_ON, brightness=180, rgbw_color=(0, 255, 0, 0)) # turn OFF from HA await hass.services.async_call( @@ -955,9 +1003,9 @@ async def test_light_rgbw(hass: HomeAssistant, knx: KNXTestKit): {"entity_id": "light.test"}, blocking=True, ) - # color will be restored in no other state was received + # color will be restored if no other state was received await knx.assert_write(test_address, True) - knx.assert_state("light.test", STATE_ON, brightness=180, rgbw_color=(0, 180, 0, 0)) + knx.assert_state("light.test", STATE_ON, brightness=180, rgbw_color=(0, 255, 0, 0)) async def test_light_rgbw_brightness(hass: HomeAssistant, knx: KNXTestKit): @@ -987,16 +1035,16 @@ async def test_light_rgbw_brightness(hass: HomeAssistant, knx: KNXTestKit): await knx.assert_read(test_address_state) await knx.assert_read(test_brightness_state) await knx.receive_response(test_address_state, True) - await knx.receive_response(test_brightness_state, (0x67,)) + await knx.receive_response(test_brightness_state, (0xFF,)) await knx.assert_read(test_rgbw_state) - await knx.receive_response(test_rgbw_state, (0x64, 0x65, 0x66, 0x67, 0x00, 0x0F)) + await knx.receive_response(test_rgbw_state, (0xFF, 0x65, 0x66, 0x67, 0x00, 0x0F)) knx.assert_state( "light.test", STATE_ON, - brightness=103, + brightness=255, color_mode=COLOR_MODE_RGBW, - rgbw_color=(100, 101, 102, 103), + rgbw_color=(255, 101, 102, 103), ) # change color from HA await hass.services.async_call( @@ -1006,25 +1054,22 @@ async def test_light_rgbw_brightness(hass: HomeAssistant, knx: KNXTestKit): blocking=True, ) await knx.assert_write(test_rgbw, (0xFF, 0x00, 0x00, 0x00, 0x00, 0x0F)) - knx.assert_state("light.test", STATE_ON, brightness=103, rgbw_color=(255, 0, 0, 0)) - # # relies on dedicated brightness state - await knx.receive_write(test_brightness_state, (0xFF,)) knx.assert_state("light.test", STATE_ON, brightness=255, rgbw_color=(255, 0, 0, 0)) + # # update from dedicated brightness state + await knx.receive_write(test_brightness_state, (0xF0,)) + knx.assert_state("light.test", STATE_ON, brightness=240, rgbw_color=(255, 0, 0, 0)) - # change brightness from HA + # single encoded brightness - at least one primary color = 255 + # # change brightness from HA await hass.services.async_call( "light", "turn_on", - {"entity_id": "light.test", ATTR_BRIGHTNESS: 200}, + {"entity_id": "light.test", ATTR_BRIGHTNESS: 128}, blocking=True, ) - await knx.assert_write(test_brightness, (200,)) - knx.assert_state("light.test", STATE_ON, brightness=200, rgbw_color=(255, 0, 0, 0)) - # # relies on dedicated rgbw state - await knx.receive_write(test_rgbw_state, (0xC8, 0x00, 0x00, 0x00, 0x00, 0x0F)) - knx.assert_state("light.test", STATE_ON, brightness=200, rgbw_color=(200, 0, 0, 0)) - - # change color and brightness from HA + await knx.assert_write(test_brightness, (128,)) + knx.assert_state("light.test", STATE_ON, brightness=128, rgbw_color=(255, 0, 0, 0)) + # # change color and brightness from HA await hass.services.async_call( "light", "turn_on", @@ -1040,3 +1085,36 @@ async def test_light_rgbw_brightness(hass: HomeAssistant, knx: KNXTestKit): rgb_color=(255, 105, 180), rgbw_color=(255, 0, 128, 178), ) + + # doubly encoded brightness + # brightness is handled by dedicated brightness address only + # # from dedicated rgbw state + await knx.receive_write(test_rgbw_state, (0xC8, 0x00, 0x00, 0x00, 0x00, 0x0F)) + knx.assert_state("light.test", STATE_ON, brightness=128, rgbw_color=(200, 0, 0, 0)) + # # from HA - only color + await hass.services.async_call( + "light", + "turn_on", + {"entity_id": "light.test", ATTR_RGBW_COLOR: (20, 30, 40, 50)}, + blocking=True, + ) + await knx.assert_write(test_rgbw, (20, 30, 40, 50, 0x00, 0x0F)) + knx.assert_state( + "light.test", STATE_ON, brightness=128, rgbw_color=(20, 30, 40, 50) + ) + # # from HA - brightness and color + await hass.services.async_call( + "light", + "turn_on", + { + "entity_id": "light.test", + ATTR_BRIGHTNESS: 50, + ATTR_RGBW_COLOR: (100, 200, 55, 12), + }, + blocking=True, + ) + await knx.assert_write(test_rgbw, (100, 200, 55, 12, 0x00, 0x0F)) + await knx.assert_write(test_brightness, (50,)) + knx.assert_state( + "light.test", STATE_ON, brightness=50, rgbw_color=(100, 200, 55, 12) + )