From b1022ce84e6ff06e9b94e5b59985cb619019dc36 Mon Sep 17 00:00:00 2001 From: Robert Svensson Date: Thu, 10 Jun 2021 08:51:58 +0200 Subject: [PATCH] Use supported color modes in deCONZ integration (#51656) * Initial commit everything is working, need to reevaluate tests * Fix supported color modes and hs_color * Attest color mode --- homeassistant/components/deconz/light.py | 101 ++++++++++++++++------- tests/components/deconz/test_light.py | 28 +++++-- 2 files changed, 93 insertions(+), 36 deletions(-) diff --git a/homeassistant/components/deconz/light.py b/homeassistant/components/deconz/light.py index 60aa02c153e..adcfb324ebe 100644 --- a/homeassistant/components/deconz/light.py +++ b/homeassistant/components/deconz/light.py @@ -1,5 +1,8 @@ """Support for deCONZ lights.""" +from __future__ import annotations + +from pydeconz.group import DeconzGroup as Group from pydeconz.light import Light from homeassistant.components.light import ( @@ -9,13 +12,16 @@ from homeassistant.components.light import ( ATTR_FLASH, ATTR_HS_COLOR, ATTR_TRANSITION, + ATTR_XY_COLOR, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_ONOFF, + COLOR_MODE_XY, DOMAIN, EFFECT_COLORLOOP, FLASH_LONG, FLASH_SHORT, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, SUPPORT_EFFECT, SUPPORT_FLASH, SUPPORT_TRANSITION, @@ -23,7 +29,6 @@ from homeassistant.components.light import ( ) from homeassistant.core import callback from homeassistant.helpers.dispatcher import async_dispatcher_connect -import homeassistant.util.color as color_util from .const import ( COVER_TYPES, @@ -106,24 +111,50 @@ class DeconzBaseLight(DeconzDevice, LightEntity): """Set up light.""" super().__init__(device, gateway) + self._attr_supported_color_modes = set() self.update_features(self._device) - def update_features(self, device): + def update_features(self, device: Light | Group) -> None: """Calculate supported features of device.""" + supported_color_modes = self._attr_supported_color_modes + + if device.ct is not None: + supported_color_modes.add(COLOR_MODE_COLOR_TEMP) + + if device.hue is not None and device.sat is not None: + supported_color_modes.add(COLOR_MODE_HS) + + if device.xy is not None: + supported_color_modes.add(COLOR_MODE_XY) + + if not supported_color_modes and device.brightness is not None: + supported_color_modes.add(COLOR_MODE_BRIGHTNESS) + + if not supported_color_modes: + supported_color_modes.add(COLOR_MODE_ONOFF) + if device.brightness is not None: - self._attr_supported_features |= SUPPORT_BRIGHTNESS self._attr_supported_features |= SUPPORT_FLASH self._attr_supported_features |= SUPPORT_TRANSITION - if device.ct is not None: - self._attr_supported_features |= SUPPORT_COLOR_TEMP - - if device.xy is not None or (device.hue is not None and device.sat is not None): - self._attr_supported_features |= SUPPORT_COLOR - if device.effect is not None: self._attr_supported_features |= SUPPORT_EFFECT + @property + def color_mode(self) -> str: + """Return the color mode of the light.""" + if self._device.colormode == "ct": + color_mode = COLOR_MODE_COLOR_TEMP + elif self._device.colormode == "hs": + color_mode = COLOR_MODE_HS + elif self._device.colormode == "xy": + color_mode = COLOR_MODE_XY + elif self._device.brightness is not None: + color_mode = COLOR_MODE_BRIGHTNESS + else: + color_mode = COLOR_MODE_ONOFF + return color_mode + @property def brightness(self): """Return the brightness of this light between 0..255.""" @@ -137,20 +168,17 @@ class DeconzBaseLight(DeconzDevice, LightEntity): @property def color_temp(self): """Return the CT color value.""" - if self._device.colormode != "ct": - return None - return self._device.ct @property - def hs_color(self): + def hs_color(self) -> tuple: """Return the hs color value.""" - if self._device.colormode in ("xy", "hs"): - if self._device.xy: - return color_util.color_xy_to_hs(*self._device.xy) - if self._device.hue and self._device.sat: - return (self._device.hue / 65535 * 360, self._device.sat / 255 * 100) - return None + return (self._device.hue / 65535 * 360, self._device.sat / 255 * 100) + + @property + def xy_color(self) -> tuple | None: + """Return the XY color value.""" + return self._device.xy @property def is_on(self): @@ -161,18 +189,18 @@ class DeconzBaseLight(DeconzDevice, LightEntity): """Turn on light.""" data = {"on": True} + if ATTR_BRIGHTNESS in kwargs: + data["bri"] = kwargs[ATTR_BRIGHTNESS] + if ATTR_COLOR_TEMP in kwargs: data["ct"] = kwargs[ATTR_COLOR_TEMP] if ATTR_HS_COLOR in kwargs: - if self._device.xy is not None: - data["xy"] = color_util.color_hs_to_xy(*kwargs[ATTR_HS_COLOR]) - else: - data["hue"] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535) - data["sat"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255) + data["hue"] = int(kwargs[ATTR_HS_COLOR][0] / 360 * 65535) + data["sat"] = int(kwargs[ATTR_HS_COLOR][1] / 100 * 255) - if ATTR_BRIGHTNESS in kwargs: - data["bri"] = kwargs[ATTR_BRIGHTNESS] + if ATTR_XY_COLOR in kwargs: + data["xy"] = kwargs[ATTR_XY_COLOR] if ATTR_TRANSITION in kwargs: data["transitiontime"] = int(kwargs[ATTR_TRANSITION] * 10) @@ -250,6 +278,21 @@ class DeconzGroup(DeconzBaseLight): if light.ZHATYPE == Light.ZHATYPE: self.update_features(light) + for exclusive_color_mode in [COLOR_MODE_ONOFF, COLOR_MODE_BRIGHTNESS]: + if ( + exclusive_color_mode in self._attr_supported_color_modes + and len(self._attr_supported_color_modes) > 1 + ): + self._attr_supported_color_modes.remove(exclusive_color_mode) + + @property + def hs_color(self) -> tuple | None: + """Return the hs color value.""" + try: + return super().hs_color + except TypeError: + return None + @property def unique_id(self): """Return a unique identifier for this device.""" diff --git a/tests/components/deconz/test_light.py b/tests/components/deconz/test_light.py index 6ec73085322..28d7b1c59bd 100644 --- a/tests/components/deconz/test_light.py +++ b/tests/components/deconz/test_light.py @@ -7,13 +7,19 @@ import pytest from homeassistant.components.deconz.const import CONF_ALLOW_DECONZ_GROUPS from homeassistant.components.light import ( ATTR_BRIGHTNESS, + ATTR_COLOR_MODE, ATTR_COLOR_TEMP, ATTR_EFFECT, ATTR_FLASH, ATTR_HS_COLOR, ATTR_MAX_MIREDS, ATTR_MIN_MIREDS, + ATTR_SUPPORTED_COLOR_MODES, ATTR_TRANSITION, + ATTR_XY_COLOR, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_ONOFF, + COLOR_MODE_XY, DOMAIN as LIGHT_DOMAIN, EFFECT_COLORLOOP, FLASH_LONG, @@ -73,7 +79,7 @@ async def test_lights_and_groups(hass, aioclient_mock, mock_deconz_websocket): "bri": 255, "colormode": "xy", "effect": "colorloop", - "xy": (500, 500), + "xy": (0.5, 0.5), "reachable": True, }, "type": "Extended color light", @@ -117,16 +123,22 @@ async def test_lights_and_groups(hass, aioclient_mock, mock_deconz_websocket): rgb_light = hass.states.get("light.rgb_light") assert rgb_light.state == STATE_ON assert rgb_light.attributes[ATTR_BRIGHTNESS] == 255 - assert rgb_light.attributes[ATTR_HS_COLOR] == (224.235, 100.0) + assert rgb_light.attributes[ATTR_XY_COLOR] == (0.5, 0.5) + assert rgb_light.attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_XY] + assert rgb_light.attributes[ATTR_COLOR_MODE] == COLOR_MODE_XY + assert rgb_light.attributes[ATTR_SUPPORTED_FEATURES] == 44 assert rgb_light.attributes["is_deconz_group"] is False - assert rgb_light.attributes[ATTR_SUPPORTED_FEATURES] == 61 tunable_white_light = hass.states.get("light.tunable_white_light") assert tunable_white_light.state == STATE_ON assert tunable_white_light.attributes[ATTR_COLOR_TEMP] == 2500 assert tunable_white_light.attributes[ATTR_MAX_MIREDS] == 454 assert tunable_white_light.attributes[ATTR_MIN_MIREDS] == 155 - assert tunable_white_light.attributes[ATTR_SUPPORTED_FEATURES] == 2 + assert tunable_white_light.attributes[ATTR_SUPPORTED_COLOR_MODES] == [ + COLOR_MODE_COLOR_TEMP + ] + assert tunable_white_light.attributes[ATTR_COLOR_MODE] == COLOR_MODE_COLOR_TEMP + assert tunable_white_light.attributes[ATTR_SUPPORTED_FEATURES] == 0 tunable_white_light_bad_maxmin = hass.states.get( "light.tunable_white_light_with_bad_maxmin_values" @@ -135,10 +147,12 @@ async def test_lights_and_groups(hass, aioclient_mock, mock_deconz_websocket): assert tunable_white_light_bad_maxmin.attributes[ATTR_COLOR_TEMP] == 2500 assert tunable_white_light_bad_maxmin.attributes[ATTR_MAX_MIREDS] == 650 assert tunable_white_light_bad_maxmin.attributes[ATTR_MIN_MIREDS] == 140 - assert tunable_white_light_bad_maxmin.attributes[ATTR_SUPPORTED_FEATURES] == 2 + assert tunable_white_light_bad_maxmin.attributes[ATTR_SUPPORTED_FEATURES] == 0 on_off_light = hass.states.get("light.on_off_light") assert on_off_light.state == STATE_ON + assert on_off_light.attributes[ATTR_SUPPORTED_COLOR_MODES] == [COLOR_MODE_ONOFF] + assert on_off_light.attributes[ATTR_COLOR_MODE] == COLOR_MODE_ONOFF assert on_off_light.attributes[ATTR_SUPPORTED_FEATURES] == 0 assert hass.states.get("light.light_group").state == STATE_ON @@ -191,7 +205,7 @@ async def test_lights_and_groups(hass, aioclient_mock, mock_deconz_websocket): SERVICE_TURN_ON, { ATTR_ENTITY_ID: "light.rgb_light", - ATTR_HS_COLOR: (20, 30), + ATTR_XY_COLOR: (0.411, 0.351), ATTR_FLASH: FLASH_LONG, ATTR_EFFECT: "None", }, @@ -598,4 +612,4 @@ async def test_verify_group_supported_features(hass, aioclient_mock): assert len(hass.states.async_all()) == 4 assert hass.states.get("light.group").state == STATE_ON - assert hass.states.get("light.group").attributes[ATTR_SUPPORTED_FEATURES] == 63 + assert hass.states.get("light.group").attributes[ATTR_SUPPORTED_FEATURES] == 44