From 74aa428bd1b400c29813408b64dd559a81999f3a Mon Sep 17 00:00:00 2001 From: Erik Montnemery Date: Sun, 27 Jun 2021 21:00:27 +0200 Subject: [PATCH] Implement color_mode support for ozw (#52063) --- homeassistant/components/ozw/light.py | 85 ++++++++++++++++++--------- tests/components/ozw/test_light.py | 42 +++++++++++-- 2 files changed, 93 insertions(+), 34 deletions(-) diff --git a/homeassistant/components/ozw/light.py b/homeassistant/components/ozw/light.py index b5fffbcf34f..7c52da23fb4 100644 --- a/homeassistant/components/ozw/light.py +++ b/homeassistant/components/ozw/light.py @@ -5,14 +5,14 @@ from homeassistant.components.light import ( ATTR_BRIGHTNESS, ATTR_COLOR_TEMP, ATTR_HS_COLOR, + ATTR_RGBW_COLOR, ATTR_TRANSITION, - ATTR_WHITE_VALUE, + COLOR_MODE_BRIGHTNESS, + COLOR_MODE_COLOR_TEMP, + COLOR_MODE_HS, + COLOR_MODE_RGBW, DOMAIN as LIGHT_DOMAIN, - SUPPORT_BRIGHTNESS, - SUPPORT_COLOR, - SUPPORT_COLOR_TEMP, SUPPORT_TRANSITION, - SUPPORT_WHITE_VALUE, LightEntity, ) from homeassistant.core import callback @@ -65,9 +65,11 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity): super().__init__(values) self._color_channels = None self._hs = None - self._white = None + self._rgbw_color = None self._ct = None - self._supported_features = SUPPORT_BRIGHTNESS + self._attr_color_mode = None + self._attr_supported_features = 0 + self._attr_supported_color_modes = set() self._min_mireds = 153 # 6500K as a safe default self._max_mireds = 370 # 2700K as a safe default @@ -78,23 +80,29 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity): def on_value_update(self): """Call when the underlying value(s) is added or updated.""" if self.values.dimming_duration is not None: - self._supported_features |= SUPPORT_TRANSITION - - if self.values.color is not None: - self._supported_features |= SUPPORT_COLOR + self._attr_supported_features |= SUPPORT_TRANSITION if self.values.color_channels is not None: # Support Color Temp if both white channels if (self.values.color_channels.value & COLOR_CHANNEL_WARM_WHITE) and ( self.values.color_channels.value & COLOR_CHANNEL_COLD_WHITE ): - self._supported_features |= SUPPORT_COLOR_TEMP + self._attr_supported_color_modes.add(COLOR_MODE_COLOR_TEMP) + self._attr_supported_color_modes.add(COLOR_MODE_HS) # Support White value if only a single white channel if ((self.values.color_channels.value & COLOR_CHANNEL_WARM_WHITE) != 0) ^ ( (self.values.color_channels.value & COLOR_CHANNEL_COLD_WHITE) != 0 ): - self._supported_features |= SUPPORT_WHITE_VALUE + self._attr_supported_color_modes.add(COLOR_MODE_RGBW) + + if not self._attr_supported_color_modes and self.values.color is not None: + self._attr_supported_color_modes.add(COLOR_MODE_HS) + + if not self._attr_supported_color_modes: + self._attr_supported_color_modes.add(COLOR_MODE_BRIGHTNESS) + # Default: Brightness (no color) + self._attr_color_mode = COLOR_MODE_BRIGHTNESS if self.values.color is not None: self._calculate_color_values() @@ -116,20 +124,15 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity): return self.values.target.value > 0 return self.values.primary.value > 0 - @property - def supported_features(self): - """Flag supported features.""" - return self._supported_features - @property def hs_color(self): """Return the hs color.""" return self._hs @property - def white_value(self): - """Return the white value of this light between 0..255.""" - return self._white + def rgbw_color(self): + """Return the rgbw color.""" + return self._rgbw_color @property def color_temp(self): @@ -196,8 +199,8 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity): self.async_set_duration(**kwargs) rgbw = None - white = kwargs.get(ATTR_WHITE_VALUE) hs_color = kwargs.get(ATTR_HS_COLOR) + rgbw_color = kwargs.get(ATTR_RGBW_COLOR) color_temp = kwargs.get(ATTR_COLOR_TEMP) if hs_color is not None: @@ -211,12 +214,16 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity): rgbw += "00" # white LED must be off in order for color to work - elif white is not None: + elif rgbw_color is not None: + red = rgbw_color[0] + green = rgbw_color[1] + blue = rgbw_color[2] + white = rgbw_color[3] if self._color_channels & COLOR_CHANNEL_WARM_WHITE: # trim the CW value or it will not work correctly - rgbw = f"#000000{white:02x}" + rgbw = f"#{red:02x}{green:02x}{blue:02x}{white:02x}" else: - rgbw = f"#00000000{white:02x}" + rgbw = f"#{red:02x}{green:02x}{blue:02x}00{white:02x}" elif color_temp is not None: # Limit color temp to min/max values @@ -262,6 +269,9 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity): rgb = [int(data[1:3], 16), int(data[3:5], 16), int(data[5:7], 16)] self._hs = color_util.color_RGB_to_hs(*rgb) + # Light supports color, set color mode to hs + self._attr_color_mode = COLOR_MODE_HS + if self.values.color_channels is None: return @@ -286,15 +296,21 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity): # Warm white if self._color_channels & COLOR_CHANNEL_WARM_WHITE: - self._white = int(data[index : index + 2], 16) - temp_warm = self._white + white = int(data[index : index + 2], 16) + self._rgbw_color = [rgb[0], rgb[1], rgb[2], white] + temp_warm = white + # Light supports rgbw, set color mode to rgbw + self._attr_color_mode = COLOR_MODE_RGBW index += 2 # Cold white if self._color_channels & COLOR_CHANNEL_COLD_WHITE: - self._white = int(data[index : index + 2], 16) - temp_cold = self._white + white = int(data[index : index + 2], 16) + self._rgbw_color = [rgb[0], rgb[1], rgb[2], white] + temp_cold = white + # Light supports rgbw, set color mode to rgbw + self._attr_color_mode = COLOR_MODE_RGBW # Calculate color temps based on white LED status if temp_cold or temp_warm: @@ -303,6 +319,17 @@ class ZwaveLight(ZWaveDeviceEntity, LightEntity): - ((temp_cold / 255) * (self._max_mireds - self._min_mireds)) ) + if ( + self._color_channels & COLOR_CHANNEL_WARM_WHITE + and self._color_channels & COLOR_CHANNEL_COLD_WHITE + ): + # Light supports 5 channels, set color_mode to color_temp or hs + if rgb[0] == 0 and rgb[1] == 0 and rgb[2] == 0: + # Color channels turned off, set color mode to color_temp + self._attr_color_mode = COLOR_MODE_COLOR_TEMP + else: + self._attr_color_mode = COLOR_MODE_HS + if not ( self._color_channels & COLOR_CHANNEL_RED or self._color_channels & COLOR_CHANNEL_GREEN diff --git a/tests/components/ozw/test_light.py b/tests/components/ozw/test_light.py index 6388629be8c..a8ed4352f9a 100644 --- a/tests/components/ozw/test_light.py +++ b/tests/components/ozw/test_light.py @@ -1,4 +1,5 @@ """Test Z-Wave Lights.""" +from homeassistant.components.light import SUPPORT_TRANSITION from homeassistant.components.ozw.light import byte_to_zwave_brightness from .common import setup_ozw @@ -12,6 +13,8 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages): state = hass.states.get("light.led_bulb_6_multi_colour_level") assert state is not None assert state.state == "off" + assert state.attributes["supported_features"] == SUPPORT_TRANSITION + assert state.attributes["supported_color_modes"] == ["color_temp", "hs"] # Test turning on # Beware that due to rounding, a roundtrip conversion does not always work @@ -51,6 +54,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages): assert state is not None assert state.state == "on" assert state.attributes["brightness"] == new_brightness + assert state.attributes["color_mode"] == "color_temp" # Test turning off new_transition = 6553 @@ -119,6 +123,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages): assert state is not None assert state.state == "on" assert state.attributes["brightness"] == new_brightness + assert state.attributes["color_mode"] == "color_temp" # Test set brightness to 0 new_brightness = 0 @@ -183,6 +188,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages): assert state is not None assert state.state == "on" assert state.attributes["rgb_color"] == (0, 0, 255) + assert state.attributes["color_mode"] == "hs" # Test setting hs_color new_color = [300, 70] @@ -216,6 +222,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages): assert state is not None assert state.state == "on" assert state.attributes["hs_color"] == (300.0, 70.196) + assert state.attributes["color_mode"] == "hs" # Test setting rgb_color new_color = [255, 154, 0] @@ -249,6 +256,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages): assert state is not None assert state.state == "on" assert state.attributes["rgb_color"] == (255, 153, 0) + assert state.attributes["color_mode"] == "hs" # Test setting xy_color new_color = [0.52, 0.43] @@ -282,6 +290,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages): assert state is not None assert state.state == "on" assert state.attributes["xy_color"] == (0.519, 0.429) + assert state.attributes["color_mode"] == "hs" # Test setting color temp new_color = 200 @@ -315,6 +324,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages): assert state is not None assert state.state == "on" assert state.attributes["color_temp"] == 200 + assert state.attributes["color_mode"] == "color_temp" # Test setting invalid color temp new_color = 120 @@ -348,6 +358,7 @@ async def test_light(hass, light_data, light_msg, light_rgb_msg, sent_messages): assert state is not None assert state.state == "on" assert state.attributes["color_temp"] == 153 + assert state.attributes["color_mode"] == "color_temp" async def test_pure_rgb_dimmer_light( @@ -360,7 +371,9 @@ async def test_pure_rgb_dimmer_light( state = hass.states.get("light.kitchen_rgb_strip_level") assert state is not None assert state.state == "on" - assert state.attributes["supported_features"] == 17 + assert state.attributes["supported_features"] == 0 + assert state.attributes["supported_color_modes"] == ["hs"] + assert state.attributes["color_mode"] == "hs" # Test setting hs_color new_color = [300, 70] @@ -390,6 +403,7 @@ async def test_pure_rgb_dimmer_light( assert state is not None assert state.state == "on" assert state.attributes["hs_color"] == (300.0, 70.196) + assert state.attributes["color_mode"] == "hs" async def test_no_rgb_light(hass, light_data, light_no_rgb_msg, sent_messages): @@ -400,6 +414,8 @@ async def test_no_rgb_light(hass, light_data, light_no_rgb_msg, sent_messages): state = hass.states.get("light.master_bedroom_l_level") assert state is not None assert state.state == "off" + assert state.attributes["supported_features"] == 0 + assert state.attributes["supported_color_modes"] == ["brightness"] # Turn on the light new_brightness = 44 @@ -429,6 +445,7 @@ async def test_no_rgb_light(hass, light_data, light_no_rgb_msg, sent_messages): assert state is not None assert state.state == "on" assert state.attributes["brightness"] == new_brightness + assert state.attributes["color_mode"] == "brightness" async def test_no_ww_light( @@ -441,6 +458,8 @@ async def test_no_ww_light( state = hass.states.get("light.led_bulb_6_multi_colour_level") assert state is not None assert state.state == "off" + assert state.attributes["supported_features"] == 0 + assert state.attributes["supported_color_modes"] == ["rgbw"] # Turn on the light white_color = 190 @@ -449,7 +468,7 @@ async def test_no_ww_light( "turn_on", { "entity_id": "light.led_bulb_6_multi_colour_level", - "white_value": white_color, + "rgbw_color": [0, 0, 0, white_color], }, blocking=True, ) @@ -472,7 +491,8 @@ async def test_no_ww_light( state = hass.states.get("light.led_bulb_6_multi_colour_level") assert state is not None assert state.state == "on" - assert state.attributes["white_value"] == 190 + assert state.attributes["color_mode"] == "rgbw" + assert state.attributes["rgbw_color"] == (0, 0, 0, 190) async def test_no_cw_light( @@ -485,6 +505,8 @@ async def test_no_cw_light( state = hass.states.get("light.led_bulb_6_multi_colour_level") assert state is not None assert state.state == "off" + assert state.attributes["supported_features"] == 0 + assert state.attributes["supported_color_modes"] == ["rgbw"] # Turn on the light white_color = 190 @@ -493,7 +515,7 @@ async def test_no_cw_light( "turn_on", { "entity_id": "light.led_bulb_6_multi_colour_level", - "white_value": white_color, + "rgbw_color": [0, 0, 0, white_color], }, blocking=True, ) @@ -516,7 +538,8 @@ async def test_no_cw_light( state = hass.states.get("light.led_bulb_6_multi_colour_level") assert state is not None assert state.state == "on" - assert state.attributes["white_value"] == 190 + assert state.attributes["color_mode"] == "rgbw" + assert state.attributes["rgbw_color"] == (0, 0, 0, 190) async def test_wc_light(hass, light_wc_data, light_msg, light_rgb_msg, sent_messages): @@ -527,6 +550,8 @@ async def test_wc_light(hass, light_wc_data, light_msg, light_rgb_msg, sent_mess state = hass.states.get("light.led_bulb_6_multi_colour_level") assert state is not None assert state.state == "off" + assert state.attributes["supported_features"] == 0 + assert state.attributes["supported_color_modes"] == ["color_temp", "hs"] assert state.attributes["min_mireds"] == 153 assert state.attributes["max_mireds"] == 370 @@ -559,6 +584,7 @@ async def test_wc_light(hass, light_wc_data, light_msg, light_rgb_msg, sent_mess assert state is not None assert state.state == "on" assert state.attributes["color_temp"] == 190 + assert state.attributes["color_mode"] == "color_temp" async def test_new_ozw_light(hass, light_new_ozw_data, light_msg, sent_messages): @@ -569,6 +595,8 @@ async def test_new_ozw_light(hass, light_new_ozw_data, light_msg, sent_messages) state = hass.states.get("light.led_bulb_6_multi_colour_level") assert state is not None assert state.state == "off" + assert state.attributes["supported_features"] == SUPPORT_TRANSITION + assert state.attributes["supported_color_modes"] == ["color_temp", "hs"] # Test turning on with new duration (newer openzwave) new_transition = 4180 @@ -597,6 +625,8 @@ async def test_new_ozw_light(hass, light_new_ozw_data, light_msg, sent_messages) light_msg.encode() receive_message(light_msg) await hass.async_block_till_done() + state = hass.states.get("light.led_bulb_6_multi_colour_level") + assert state.attributes["color_mode"] == "color_temp" # Test turning off with new duration (newer openzwave)(new max) await hass.services.async_call( @@ -649,3 +679,5 @@ async def test_new_ozw_light(hass, light_new_ozw_data, light_msg, sent_messages) light_msg.encode() receive_message(light_msg) await hass.async_block_till_done() + state = hass.states.get("light.led_bulb_6_multi_colour_level") + assert state.attributes["color_mode"] == "color_temp"