Pārlūkot izejas kodu

Use HS color rather than RGBW for colour lights.

The initial implementation of lights made the mistake of choosing
RGBW, thinking that the W would correspond to the brightness dp and
RGB to colour.

After implementing though, brightness dp should not be set for colour
mode, and the lights have either RGBHSV settings or HSV in the colour
dp.  Since HSV is the common factor, HS color fits better (V is taken
from brightness).

This changes the UI to remove the confusing and non-working "Color
brightness" and "White brightness" sliders from the color
tab. Brightness on the main tab still works.

Issue #691
Jason Rumney 3 gadi atpakaļ
vecāks
revīzija
4194235517

+ 2 - 2
custom_components/tuya_local/devices/README.md

@@ -553,8 +553,8 @@ Humidifer can also cover dehumidifiers (use class to specify which).
     will be mapped so the minimum corresponds to 153 mireds (6500K), and max to 500 (2000K).
 - **rgbhsv** (optional, hex): a dp to control the color of the light, using encoded RGB and HSV values.  The `format` field names recognized for decoding this field are `r`, `g`, `b`, `h`, `s`, `v`.
 - **color_mode** (optional, mapping of strings): a dp to control which mode to use if the light supports multiple modes.
-    Special values: `white, color_temp, rgbw, hs, xy, rgb, rgbww`, others will be treated as effects,
-	Note: only white, color_temp and rgbw are currently supported, others listed above are reserved and may be implemented in future when the need arises.
+    Special values: `white, color_temp, hs, xy, rgb, rgbw, rgbww`, others will be treated as effects,
+	Note: only white, color_temp and hs are currently supported, others listed above are reserved and may be implemented in future when the need arises.
     If no `color_mode` dp is available, a single supported color mode will be
     calculated based on which of the above dps are available.
 - **effect** (optional, mapping of strings): a dp to control effects / presets supported by the light.

+ 1 - 1
custom_components/tuya_local/devices/atomi_string_lights.yaml

@@ -12,7 +12,7 @@ primary_entity:
         - dps_val: white
           value: color_temp
         - dps_val: colour
-          value: rgbw
+          value: hs
         - dps_val: scene1
           value: Strobe
         - dps_val: scene2

+ 1 - 1
custom_components/tuya_local/devices/digoo_dgsp01_dual_nightlight_switch.yaml

@@ -20,7 +20,7 @@ secondary_entities:
           - dps_val: white
             value: white
           - dps_val: colour
-            value: rgbw
+            value: hs
           - dps_val: scene
             value: Scene
           - dps_val: music

+ 1 - 1
custom_components/tuya_local/devices/etersky_aroma_diffuser.yaml

@@ -54,7 +54,7 @@ secondary_entities:
           - dps_val: white
             value: white
           - dps_val: colour
-            value: rgbw
+            value: hs
       - id: 10
         name: rgbhsv
         type: hex

+ 1 - 1
custom_components/tuya_local/devices/hdmi_sync_light.yaml

@@ -17,7 +17,7 @@ primary_entity:
         - dps_val: white
           value: brightness
         - dps_val: colour
-          value: rgbw
+          value: hs
       optional: true
     - id: 22
       type: integer

+ 1 - 1
custom_components/tuya_local/devices/ih001_led_controller.yaml

@@ -13,7 +13,7 @@ primary_entity:
       type: string
       mapping:
         - dps_val: colour
-          value: rgbw
+          value: hs
         - dps_val: scene
           value: Scene
         - dps_val: music

+ 1 - 1
custom_components/tuya_local/devices/light_string.yaml

@@ -10,7 +10,7 @@ primary_entity:
       type: string
       mapping:
         - dps_val: colour
-          value: rgbw
+          value: hs
         - dps_val: scene
           value: Scene
         - dps_val: music

+ 1 - 1
custom_components/tuya_local/devices/moes_rgb_socket.yaml

@@ -20,7 +20,7 @@ secondary_entities:
           - dps_val: white
             value: white
           - dps_val: colour
-            value: rgbw
+            value: hs
           - dps_val: scene
             value: Scene
           - dps_val: scene_1

+ 1 - 1
custom_components/tuya_local/devices/rgb_nightlight_outlet.yaml

@@ -18,7 +18,7 @@ secondary_entities:
         name: color_mode
         mapping:
           - dps_val: colour
-            value: rgbw
+            value: hs
           - dps_val: scene
             value: Scene
           - dps_val: music

+ 1 - 1
custom_components/tuya_local/devices/rgbcw_lightbulb.yaml

@@ -21,7 +21,7 @@ primary_entity:
         - dps_val: white
           value: color_temp
         - dps_val: colour
-          value: rgbw
+          value: hs
         - dps_val: scene
           value: Scene
         - dps_val: music

+ 1 - 1
custom_components/tuya_local/devices/rgbcw_lightbulbv2.yaml

@@ -15,7 +15,7 @@ primary_entity:
         - dps_val: white
           value: color_temp
         - dps_val: colour
-          value: rgbw
+          value: hs
     - id: 3
       name: brightness
       type: integer

+ 1 - 1
custom_components/tuya_local/devices/rgbw_lightbulb.yaml

@@ -17,7 +17,7 @@ primary_entity:
         - dps_val: white
           value: white
         - dps_val: colour
-          value: rgbw
+          value: hs
     - id: 3
       name: brightness
       type: integer

+ 1 - 1
custom_components/tuya_local/devices/simple_rgbcw_lightbulb.yaml

@@ -12,7 +12,7 @@ primary_entity:
         - dps_val: white
           value: color_temp
         - dps_val: colour
-          value: rgbw
+          value: hs
     - id: 22
       name: brightness
       type: integer

+ 1 - 1
custom_components/tuya_local/devices/space_dog_music_lamp.yaml

@@ -15,7 +15,7 @@ primary_entity:
         - dps_val: white
           value: white
         - dps_val: colour
-          value: rgbw
+          value: hs
         - dps_val: scene
           value: Scene
         - dps_val: music

+ 1 - 1
custom_components/tuya_local/devices/yym_805SW_aroma_nightlight.yaml

@@ -56,7 +56,7 @@ secondary_entities:
         type: string
         mapping:
           - dps_val: colour
-            value: rgbw
+            value: hs
           - dps_val: colourful1
             value: Colorful
       - id: 8

+ 28 - 26
custom_components/tuya_local/light.py

@@ -6,7 +6,7 @@ from homeassistant.components.light import (
     ATTR_COLOR_MODE,
     ATTR_COLOR_TEMP,
     ATTR_EFFECT,
-    ATTR_RGBW_COLOR,
+    ATTR_HS_COLOR,
     ATTR_WHITE,
     ColorMode,
     LightEntity,
@@ -89,7 +89,7 @@ class TuyaLocalLight(TuyaLocalEntity, LightEntity):
             return from_dp
 
         if self._rgbhsv_dps:
-            return ColorMode.RGBW
+            return ColorMode.HS
         elif self._color_temp_dps:
             return ColorMode.COLOR_TEMP
         elif self._brightness_dps:
@@ -138,12 +138,13 @@ class TuyaLocalLight(TuyaLocalEntity, LightEntity):
             return self._brightness_dps.get_value(self._device)
 
     @property
-    def rgbw_color(self):
-        """Get the current RGBW color of the light"""
+    def hs_color(self):
+        """Get the current hs color of the light"""
         if self._rgbhsv_dps:
             # color data in hex format RRGGBBHHHHSSVV (14 digit hex)
             # can also be base64 encoded.
             # Either RGB or HSV can be used.
+            # Others are color data in hex format HHHHSSSSVVVV (12 digit hex)
             color = self._rgbhsv_dps.decoded_value(self._device)
 
             fmt = self._rgbhsv_dps.format
@@ -156,10 +157,6 @@ class TuyaLocalLight(TuyaLocalEntity, LightEntity):
                     # for h
                     n = fmt["names"][idx]
                     r = fmt["ranges"][idx]
-                    if r["min"] != 0:
-                        raise AttributeError(
-                            f"Unhandled minimum range for {n} in RGBW value"
-                        )
                     mx = r["max"]
                     scale = 1
                     if n == "h":
@@ -173,17 +170,13 @@ class TuyaLocalLight(TuyaLocalEntity, LightEntity):
                     idx += 1
 
                 if "h" in rgbhsv and "s" in rgbhsv and "v" in rgbhsv:
-                    h = rgbhsv["h"]
-                    s = rgbhsv["s"]
-                    # convert RGB from H and S to seperate out the V component
-                    r, g, b = color_util.color_hs_to_RGB(h, s)
-                    w = rgbhsv["v"]
+                    hs = (rgbhsv["h"], rgbhsv["s"])
                 else:
                     r = rgbhsv.get("r")
                     g = rgbhsv.get("g")
                     b = rgbhsv.get("b")
-                    w = self.brightness
-                return (r, g, b, w)
+                    hs = color_util.color_rgb_to_hs(r, g, b)
+                return hs
 
     @property
     def effect_list(self):
@@ -242,18 +235,17 @@ class TuyaLocalLight(TuyaLocalEntity, LightEntity):
                 **self._color_temp_dps.get_values_to_set(self._device, color_temp),
             }
         elif self._rgbhsv_dps and (
-            ATTR_RGBW_COLOR in params
-            or (ATTR_BRIGHTNESS in params and self.raw_color_mode == ColorMode.RGBW)
+            ATTR_HS_COLOR in params
+            or (ATTR_BRIGHTNESS in params and self.raw_color_mode == ColorMode.HS)
         ):
-            if self.raw_color_mode != ColorMode.RGBW:
-                color_mode = ColorMode.RGBW
+            if self.raw_color_mode != ColorMode.HS:
+                color_mode = ColorMode.HS
 
-            rgbw = params.get(ATTR_RGBW_COLOR, self.rgbw_color or (0, 0, 0, 0))
+            hs = params.get(ATTR_HS_COLOR, self.hs_color or (0, 0))
             brightness = params.get(ATTR_BRIGHTNESS, self.brightness or 255)
             fmt = self._rgbhsv_dps.format
-            if rgbw and fmt:
-                rgb = (rgbw[0], rgbw[1], rgbw[2])
-                hs = color_util.color_RGB_to_hs(rgbw[0], rgbw[1], rgbw[2])
+            if hs and fmt:
+                rgb = color_util.color_hsv_to_RGB(*hs, brightness / 2.55)
                 rgbhsv = {
                     "r": rgb[0],
                     "g": rgb[1],
@@ -263,7 +255,7 @@ class TuyaLocalLight(TuyaLocalEntity, LightEntity):
                     "v": brightness,
                 }
                 _LOGGER.debug(
-                    f"Setting RGBW as {rgb[0]},{rgb[1]},{rgb[2]},{hs[0]},{hs[1]},{brightness}"
+                    f"Setting color as {rgb[0]},{rgb[1]},{rgb[2]},{hs[0]},{hs[1]},{brightness}"
                 )
                 ordered = []
                 idx = 0
@@ -276,7 +268,17 @@ class TuyaLocalLight(TuyaLocalEntity, LightEntity):
                         scale = r["max"] / 360
                     else:
                         scale = r["max"] / 255
-                    ordered.append(round(rgbhsv[n] * scale))
+                    val = round(rgbhsv[n] * scale)
+                    if val < r["min"]:
+                        _LOGGER.warning(
+                            "Color data %s=%d constrained to be above %d",
+                            n,
+                            val,
+                            r["min"],
+                        )
+                        val = r["min"]
+                    _LOGGER.warning("%s=%d", n, val)
+                    ordered.append(val)
                     idx += 1
                 binary = pack(fmt["format"], *ordered)
                 settings = {
@@ -307,7 +309,7 @@ class TuyaLocalLight(TuyaLocalEntity, LightEntity):
 
         if (
             ATTR_BRIGHTNESS in params
-            and self.raw_color_mode != ColorMode.RGBW
+            and self.raw_color_mode != ColorMode.HS
             and self._brightness_dps
         ):
             bright = params.get(ATTR_BRIGHTNESS)

+ 15 - 18
tests/devices/test_digoo_dgsp01_dual_nightlight_switch.py

@@ -13,7 +13,7 @@ SWITCH_DPS = "1"
 LIGHTSW_DPS = "27"
 COLORMODE_DPS = "28"
 BRIGHTNESS_DPS = "29"
-RGBW_DPS = "31"
+RGB_DPS = "31"
 UNKNOWN32_DPS = "32"
 UNKNOWN33_DPS = "33"
 UNKNOWN34_DPS = "34"
@@ -48,29 +48,26 @@ class TestDigooNightlightSwitch(BasicSwitchTests, TuyaDeviceTestCase):
 
     def test_light_color_mode(self):
         self.dps[COLORMODE_DPS] = "colour"
-        self.assertEqual(self.light.color_mode, ColorMode.RGBW)
+        self.assertEqual(self.light.color_mode, ColorMode.HS)
         self.dps[COLORMODE_DPS] = "white"
         self.assertEqual(self.light.color_mode, ColorMode.WHITE)
         self.dps[COLORMODE_DPS] = "scene"
-        self.assertEqual(self.light.color_mode, ColorMode.RGBW)
+        self.assertEqual(self.light.color_mode, ColorMode.HS)
         self.dps[COLORMODE_DPS] = "music"
-        self.assertEqual(self.light.color_mode, ColorMode.RGBW)
+        self.assertEqual(self.light.color_mode, ColorMode.HS)
         self.dps[COLORMODE_DPS] = "scene_1"
-        self.assertEqual(self.light.color_mode, ColorMode.RGBW)
+        self.assertEqual(self.light.color_mode, ColorMode.HS)
         self.dps[COLORMODE_DPS] = "scene_2"
-        self.assertEqual(self.light.color_mode, ColorMode.RGBW)
+        self.assertEqual(self.light.color_mode, ColorMode.HS)
         self.dps[COLORMODE_DPS] = "scene_3"
-        self.assertEqual(self.light.color_mode, ColorMode.RGBW)
+        self.assertEqual(self.light.color_mode, ColorMode.HS)
         self.dps[COLORMODE_DPS] = "scene_4"
-        self.assertEqual(self.light.color_mode, ColorMode.RGBW)
+        self.assertEqual(self.light.color_mode, ColorMode.HS)
 
-    def test_light_rgbw_color(self):
-        self.dps[RGBW_DPS] = "ffff00003c6464"
+    def test_light_hs_color(self):
+        self.dps[RGB_DPS] = "ffff00003c6464"
         self.dps[BRIGHTNESS_DPS] = 255
-        self.assertSequenceEqual(
-            self.light.rgbw_color,
-            (255, 255, 0, 255),
-        )
+        self.assertSequenceEqual(self.light.hs_color, (60, 100))
 
     def test_light_effect_list(self):
         self.assertCountEqual(
@@ -106,7 +103,7 @@ class TestDigooNightlightSwitch(BasicSwitchTests, TuyaDeviceTestCase):
     def test_light_supported_color_modes(self):
         self.assertCountEqual(
             self.light.supported_color_modes,
-            {ColorMode.RGBW, ColorMode.WHITE},
+            {ColorMode.HS, ColorMode.WHITE},
         )
 
     def test_light_supported_features(self):
@@ -135,16 +132,16 @@ class TestDigooNightlightSwitch(BasicSwitchTests, TuyaDeviceTestCase):
         ):
             await self.light.async_turn_on(brightness=128)
 
-    async def test_set_rgbw(self):
+    async def test_set_hs_color(self):
         self.dps[BRIGHTNESS_DPS] = 255
         self.dps[COLORMODE_DPS] = "colour"
         async with assert_device_properties_set(
             self.light._device,
             {
-                RGBW_DPS: "ff000000006464",
+                RGB_DPS: "ff000000006464",
             },
         ):
-            await self.light.async_turn_on(rgbw_color=(255, 0, 0, 255))
+            await self.light.async_turn_on(hs_color=(0, 100))
 
     def test_extra_state_attributes_set(self):
         self.dps[UNKNOWN32_DPS] = "32"

+ 17 - 17
tests/devices/test_moes_rgb_socket.py

@@ -1,4 +1,4 @@
-"""Tests for the MoesHouse RGBW smart socket."""
+"""Tests for the MoesHouse RGB smart socket."""
 from homeassistant.components.light import (
     ColorMode,
     LightEntityFeature,
@@ -23,7 +23,7 @@ LIGHT_DPS = "1"
 MODE_DPS = "2"
 BRIGHTNESS_DPS = "3"
 UNKNOWN4_DPS = "4"
-RGBW_DPS = "5"
+RGB_DPS = "5"
 SCENE_DPS = "6"
 SCENE1_DPS = "7"
 SCENE2_DPS = "8"
@@ -36,7 +36,7 @@ POWER_DPS = "105"
 VOLTAGE_DPS = "106"
 
 
-class TestMoesRGBWSocket(
+class TestMoesRGBSocket(
     BasicNumberTests,
     MultiSensorTests,
     BasicSwitchTests,
@@ -110,26 +110,26 @@ class TestMoesRGBWSocket(
 
     def test_light_color_mode(self):
         self.dps[MODE_DPS] = "colour"
-        self.assertEqual(self.light.color_mode, ColorMode.RGBW)
+        self.assertEqual(self.light.color_mode, ColorMode.HS)
         self.dps[MODE_DPS] = "white"
         self.assertEqual(self.light.color_mode, ColorMode.WHITE)
         self.dps[MODE_DPS] = "scene"
-        self.assertEqual(self.light.color_mode, ColorMode.RGBW)
+        self.assertEqual(self.light.color_mode, ColorMode.HS)
         self.dps[MODE_DPS] = "scene_1"
-        self.assertEqual(self.light.color_mode, ColorMode.RGBW)
+        self.assertEqual(self.light.color_mode, ColorMode.HS)
         self.dps[MODE_DPS] = "scene_2"
-        self.assertEqual(self.light.color_mode, ColorMode.RGBW)
+        self.assertEqual(self.light.color_mode, ColorMode.HS)
         self.dps[MODE_DPS] = "scene_3"
-        self.assertEqual(self.light.color_mode, ColorMode.RGBW)
+        self.assertEqual(self.light.color_mode, ColorMode.HS)
         self.dps[MODE_DPS] = "scene_4"
-        self.assertEqual(self.light.color_mode, ColorMode.RGBW)
+        self.assertEqual(self.light.color_mode, ColorMode.HS)
 
-    def test_light_rgbw_color(self):
-        self.dps[RGBW_DPS] = "ffff00003cffff"
+    def test_light_hs_color(self):
+        self.dps[RGB_DPS] = "ffff00003cffff"
         self.dps[BRIGHTNESS_DPS] = 255
         self.assertSequenceEqual(
-            self.light.rgbw_color,
-            (255, 255, 0, 255),
+            self.light.hs_color,
+            (60, 100),
         )
 
     def test_light_effect_list(self):
@@ -159,7 +159,7 @@ class TestMoesRGBWSocket(
     def test_light_supported_color_modes(self):
         self.assertCountEqual(
             self.light.supported_color_modes,
-            {ColorMode.RGBW, ColorMode.WHITE},
+            {ColorMode.HS, ColorMode.WHITE},
         )
 
     def test_light_supported_features(self):
@@ -185,7 +185,7 @@ class TestMoesRGBWSocket(
         ):
             await self.light.async_turn_on(brightness=128)
 
-    async def test_set_rgbw(self):
+    async def test_set_hs_color(self):
         self.dps[BRIGHTNESS_DPS] = 255
         self.dps[LIGHT_DPS] = True
         self.dps[MODE_DPS] = "colour"
@@ -193,10 +193,10 @@ class TestMoesRGBWSocket(
         async with assert_device_properties_set(
             self.light._device,
             {
-                RGBW_DPS: "ff00000000ffff",
+                RGB_DPS: "ff00000000ffff",
             },
         ):
-            await self.light.async_turn_on(rgbw_color=(255, 0, 0, 255))
+            await self.light.async_turn_on(hs_color=(0, 100))
 
     def test_extra_state_attributes_set(self):
         self.dps[UNKNOWN4_DPS] = 4

+ 12 - 15
tests/devices/test_rgbcw_lightbulb.py

@@ -58,27 +58,24 @@ class TestRGBCWLightbulb(BasicNumberTests, TuyaDeviceTestCase):
         self.dps[MODE_DPS] = "white"
         self.assertEqual(self.subject.color_mode, ColorMode.COLOR_TEMP)
         self.dps[MODE_DPS] = "colour"
-        self.assertEqual(self.subject.color_mode, ColorMode.RGBW)
+        self.assertEqual(self.subject.color_mode, ColorMode.HS)
         self.dps[MODE_DPS] = "scene"
-        self.assertEqual(self.subject.color_mode, ColorMode.RGBW)
+        self.assertEqual(self.subject.color_mode, ColorMode.HS)
         self.dps[MODE_DPS] = "music"
-        self.assertEqual(self.subject.color_mode, ColorMode.RGBW)
+        self.assertEqual(self.subject.color_mode, ColorMode.HS)
 
-    def test_rgbw_color(self):
+    def test_hs_color(self):
         self.dps[HSV_DPS] = "003c03e803e8"
         self.dps[BRIGHTNESS_DPS] = 1000
-        self.assertSequenceEqual(
-            self.subject.rgbw_color,
-            (255, 255, 0, 255),
-        )
+        self.assertSequenceEqual(self.subject.hs_color, (60, 100))
 
     # Lights have been observed to return N, O and P mixed in with the hex
     # number.  Maybe it has some special meaning, but since it is undocumented,
     # we just want to reject such values without an exception.
-    def test_invalid_rgbw_color(self):
+    def test_invalid_hs_color(self):
         self.dps[HSV_DPS] = "0010001000OP"
         self.dps[BRIGHTNESS_DPS] = 1000
-        self.assertIsNone(self.subject.rgbw_color)
+        self.assertIsNone(self.subject.hs_color)
 
     def test_effect_list(self):
         self.assertCountEqual(
@@ -99,7 +96,7 @@ class TestRGBCWLightbulb(BasicNumberTests, TuyaDeviceTestCase):
     def test_supported_color_modes(self):
         self.assertCountEqual(
             self.subject.supported_color_modes,
-            {ColorMode.RGBW, ColorMode.COLOR_TEMP},
+            {ColorMode.HS, ColorMode.COLOR_TEMP},
         )
 
     def test_supported_features(self):
@@ -144,7 +141,7 @@ class TestRGBCWLightbulb(BasicNumberTests, TuyaDeviceTestCase):
         ):
             await self.subject.async_turn_on(brightness=128)
 
-    async def test_set_rgbw(self):
+    async def test_set_hs_color(self):
         self.dps[BRIGHTNESS_DPS] = 1000
         self.dps[SWITCH_DPS] = True
         self.dps[MODE_DPS] = "colour"
@@ -155,10 +152,10 @@ class TestRGBCWLightbulb(BasicNumberTests, TuyaDeviceTestCase):
             },
         ):
             await self.subject.async_turn_on(
-                rgbw_color=(255, 0, 0, 255),
+                hs_color=(0, 100),
             )
 
-    async def test_set_rgbw_from_white(self):
+    async def test_set_hs_from_white(self):
         self.dps[BRIGHTNESS_DPS] = 1000
         self.dps[SWITCH_DPS] = True
         self.dps[MODE_DPS] = "white"
@@ -170,7 +167,7 @@ class TestRGBCWLightbulb(BasicNumberTests, TuyaDeviceTestCase):
             },
         ):
             await self.subject.async_turn_on(
-                rgbw_color=(255, 0, 0, 255),
+                hs_color=(0, 100),
             )
 
     def test_extra_state_attributes(self):