Преглед на файлове

Change light color temperature from mireds to Kelvin.

Use the new color_temp_kelvin API for lights instead of the
old mired API which was apparently deprecated in November.

Implement min and max by setting the _attr variables in the base class
when initialising.  To support this, added a new general
`target_range` mapping to replace the hardcoded shifting of the
color_temp range into the 152 - 500 mired range (2000-6500K).

Configs using color_temp were updated by removing the `invert: true`
mapping, and adding a `target_range`.  Where no information about the
range could be found from a quick web search, 2700-6500 was used, as
this seems to be most common for full-range CCT bulbs.

The following devices did not have the color_temp inverted.  It is not
certain whether this is because they are following a mired scale,
or this was a bug that hadn't been noticed.  Since all known Tuya
bulbs follow the Kelvin scale, no change was made to these to invert
them (assuming it was a bug that they were not inverted before).

 - atomi_ceiling_fan
 - rgbcw_lightbulb2
 - tampa_led_system
 - windcalm_fan_with_light

The following two had some evidence that the lack of inversion was
deliberate, so the `invert: true` was added to maintain that.

 - arlec_fan_light
 - fanco_ecosilentdeluxe

Issue #1086
Jason Rumney преди 2 години
родител
ревизия
1764d0c376
променени са 25 файла, в които са добавени 150 реда и са изтрити 47 реда
  1. 18 2
      custom_components/tuya_local/devices/README.md
  2. 4 0
      custom_components/tuya_local/devices/arlec_fan_light.yaml
  3. 4 0
      custom_components/tuya_local/devices/atomi_ceiling_fan.yaml
  4. 3 1
      custom_components/tuya_local/devices/atomi_string_lights.yaml
  5. 3 1
      custom_components/tuya_local/devices/ble_solar_light.yaml
  6. 3 1
      custom_components/tuya_local/devices/cct_lightbulb.yaml
  7. 3 1
      custom_components/tuya_local/devices/chanfok_fan_light.yaml
  8. 3 1
      custom_components/tuya_local/devices/desk_lamp.yaml
  9. 5 0
      custom_components/tuya_local/devices/fanco_ecosilentdeluxe.yaml
  10. 4 2
      custom_components/tuya_local/devices/ledkia_fan_light.yaml
  11. 3 2
      custom_components/tuya_local/devices/mantra_fan.yaml
  12. 3 1
      custom_components/tuya_local/devices/marpou_ceiling_lamp_ledlight.yaml
  13. 3 1
      custom_components/tuya_local/devices/ovlaim_ceiling_fan_light.yaml
  14. 3 1
      custom_components/tuya_local/devices/pir_rgbcw_light.yaml
  15. 3 1
      custom_components/tuya_local/devices/pir_spotlight.yaml
  16. 3 1
      custom_components/tuya_local/devices/rgbcw_lightbulb.yaml
  17. 4 0
      custom_components/tuya_local/devices/rgbcw_lightbulbv2.yaml
  18. 3 1
      custom_components/tuya_local/devices/simple_rgbcw_lightbulb.yaml
  19. 7 3
      custom_components/tuya_local/devices/skyfan_fan_light.yaml
  20. 4 0
      custom_components/tuya_local/devices/tampa_led_system.yaml
  21. 4 0
      custom_components/tuya_local/devices/windcalm_fan_with_light.yaml
  22. 31 1
      custom_components/tuya_local/helpers/device_config.py
  23. 15 20
      custom_components/tuya_local/light.py
  24. 6 2
      tests/devices/test_arlec_fan_light.py
  25. 8 4
      tests/devices/test_rgbcw_lightbulb.py

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

@@ -345,6 +345,22 @@ step.  It can also be set in a conditions block so that the steps change only
 under certain conditions.  An example is where a value has a range of 0-100, but
 only allows settings that are divisible by 10, so a step of 10 would be set.
 
+### `target_range`
+
+*Optional, has `min` and `max` child attributes, like `range`*
+
+A target range is used together with `range` on a numeric value, to
+map the value into a new range.  Unlike `scale`, this can shift the
+value as well as scale it into the new range.  Color temperature is a
+major use of this, as Tuya devices often use a range of 0 - 100, 0 -
+255 or 0 - 1000, and this needs to be mapped to the Kelvin like 2200 -
+6500.
+
+This should normally only be used on a default mapping, as the code
+that uses this feature often needs to inform HA of the min and max
+values for the UI, which may not handle multipe different mappings
+across the range.
+
 ### `icon`
 
 *Optional.*
@@ -577,8 +593,8 @@ Humidifer can also cover dehumidifiers (use class to specify which).
 ### `light`
 - **switch** (optional, boolean): a dp to control the on/off state of the light
 - **brightness** (optional, number 0-255): a dp to control the dimmer if available.
-- **color_temp** (optional, number): a dp to control the color temperature if available.
-    will be mapped so the minimum corresponds to 153 mireds (6500K), and max to 500 (2000K).
+- **color_temp** (optional, number): a dp to control the color temperature if available.  See `target_range` above for mapping Tuya's range into Kelvin.
+
 - **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, hs, xy, rgb, rgbw, rgbww`, others will be treated as effects,

+ 4 - 0
custom_components/tuya_local/devices/arlec_fan_light.yaml

@@ -48,6 +48,10 @@ secondary_entities:
           max: 100
         mapping:
           - step: 2
+            invert: true
+            target_range:
+              min: 2700
+              max: 6500
   - entity: select
     name: Timer
     icon: "mdi:timer"

+ 4 - 0
custom_components/tuya_local/devices/atomi_ceiling_fan.yaml

@@ -54,6 +54,10 @@ secondary_entities:
         range:
           min: 0
           max: 1000
+        mapping:
+          - target_range:
+              min: 2700
+              max: 6500
   - entity: number
     name: Light timer
     category: config

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

@@ -44,7 +44,9 @@ primary_entity:
         min: 0
         max: 255
       mapping:
-        - invert: true
+        - target_range:
+            min: 2700
+            max: 6500
     - id: 24
       name: rgbhsv
       type: hex

+ 3 - 1
custom_components/tuya_local/devices/ble_solar_light.yaml

@@ -39,7 +39,9 @@ primary_entity:
         min: 0
         max: 1000
       mapping:
-        - invert: true
+        - target_range:
+            min: 2700
+            max: 6500
     - id: 5
       name: rgbhsv
       type: hex

+ 3 - 1
custom_components/tuya_local/devices/cct_lightbulb.yaml

@@ -32,7 +32,9 @@ primary_entity:
         min: 0
         max: 1000
       mapping:
-        - invert: true
+        - target_range:
+            min: 2700
+            max: 6500
     - id: 25
       name: scene
       type: hex

+ 3 - 1
custom_components/tuya_local/devices/chanfok_fan_light.yaml

@@ -52,7 +52,9 @@ secondary_entities:
           min: 0
           max: 1000
         mapping:
-          - invert: true
+          - target_range:
+              min: 2700
+              max: 6500
       - id: 28
         name: control_data
         type: hex

+ 3 - 1
custom_components/tuya_local/devices/desk_lamp.yaml

@@ -26,4 +26,6 @@ primary_entity:
         min: 0
         max: 255
       mapping:
-        - invert: true
+        - target_range:
+            min: 2700
+            max: 6500

+ 5 - 0
custom_components/tuya_local/devices/fanco_ecosilentdeluxe.yaml

@@ -39,6 +39,11 @@ secondary_entities:
         range:
           min: 0
           max: 1000
+        mapping:
+          - invert: true
+            target_range:
+              min: 3000
+              max: 5000
   - entity: select
     name: Timer
     icon: "mdi:timer"

+ 4 - 2
custom_components/tuya_local/devices/ledkia_fan_light.yaml

@@ -48,8 +48,10 @@ secondary_entities:
           max: 1000
         optional: true
         mapping:
-          - invert: true
-            step: 500
+          - step: 500
+            target_range:
+              min: 2700
+              max: 6500
       - id: 59
         type: string
         name: light_type

+ 3 - 2
custom_components/tuya_local/devices/mantra_fan.yaml

@@ -138,5 +138,6 @@ secondary_entities:
           min: 0
           max: 1000
         mapping:
-          - invert: true
-
+          - target_range:
+              min: 2700
+              max: 5000

+ 3 - 1
custom_components/tuya_local/devices/marpou_ceiling_lamp_ledlight.yaml

@@ -40,7 +40,9 @@ primary_entity:
         min: 0
         max: 1000
       mapping:
-        - invert: true
+        - target_range:
+            min: 3000
+            max: 6500
     - id: 24
       name: rgbhsv
       type: hex

+ 3 - 1
custom_components/tuya_local/devices/ovlaim_ceiling_fan_light.yaml

@@ -59,7 +59,9 @@ secondary_entities:
           min: 0
           max: 100
         mapping:
-          - invert: true
+          - target_range:
+              min: 3000
+              max: 6000
   - entity: select
     name: Timer
     icon: "mdi:timer"

+ 3 - 1
custom_components/tuya_local/devices/pir_rgbcw_light.yaml

@@ -34,7 +34,9 @@ primary_entity:
         min: 0
         max: 1000
       mapping:
-        - invert: true
+        - target_range:
+            min: 2700
+            max: 6500
     - id: 24
       name: rgbhsv
       type: hex

+ 3 - 1
custom_components/tuya_local/devices/pir_spotlight.yaml

@@ -29,7 +29,9 @@ primary_entity:
         min: 0
         max: 1000
       mapping:
-        - invert: true
+        - target_range:
+            min: 2700
+            max: 6500
 secondary_entities:
   - entity: number
     name: Timer

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

@@ -47,7 +47,9 @@ primary_entity:
         min: 0
         max: 1000
       mapping:
-        - invert: true
+        - target_range:
+            min: 2700
+            max: 6500
     - id: 24
       name: rgbhsv
       type: hex

+ 4 - 0
custom_components/tuya_local/devices/rgbcw_lightbulbv2.yaml

@@ -28,6 +28,10 @@ primary_entity:
       range:
         min: 0
         max: 255
+      mapping:
+        - target_range:
+            min: 2700
+            max: 6500
     - id: 5
       name: rgbhsv
       type: hex

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

@@ -29,7 +29,9 @@ primary_entity:
         min: 0
         max: 1000
       mapping:
-        - invert: true
+        - target_range:
+            min: 2700
+            max: 6500
     - id: 24
       name: rgbhsv
       type: hex

+ 7 - 3
custom_components/tuya_local/devices/skyfan_fan_light.yaml

@@ -49,11 +49,15 @@ secondary_entities:
         type: string
         mapping:
           - dps_val: Coolwhite
-            value: 154
+            value: 6500
           - dps_val: Naturalwhite
-            value: 250
+            value: 4200
           - dps_val: Warmwhite
-            value: 454
+            value: 3000
+          - target_range:
+              min: 3000
+              max: 6500
+            hidden: true
   - entity: select
     name: Timer
     icon: "mdi:timer"

+ 4 - 0
custom_components/tuya_local/devices/tampa_led_system.yaml

@@ -25,6 +25,10 @@ primary_entity:
       range:
         min: 0
         max: 1000
+      mapping:
+        - target_range:
+            min: 3000
+            max: 6000
     - id: 6
       name: scene
       type: hex

+ 4 - 0
custom_components/tuya_local/devices/windcalm_fan_with_light.yaml

@@ -43,3 +43,7 @@ secondary_entities:
         range:
           min: 0
           max: 1000
+        mapping:
+          - target_range:
+              min: 3000
+              max: 6500

+ 31 - 1
custom_components/tuya_local/helpers/device_config.py

@@ -659,6 +659,8 @@ class TuyaDpsConfig:
             mirror = mapping.get("value_mirror")
             replaced = "value" in mapping
             result = mapping.get("value", result)
+            target_range = mapping.get("target_range")
+
             cond = self._active_condition(mapping, device)
             if cond:
                 if cond.get("invalid", False):
@@ -667,6 +669,8 @@ class TuyaDpsConfig:
                 result = cond.get("value", result)
                 redirect = cond.get("value_redirect", redirect)
                 mirror = cond.get("value_mirror", mirror)
+                target_range = cond.get("target_range", target_range)
+
                 for m in cond.get("mapping", {}):
                     if str(m.get("dps_val")) == str(result):
                         replaced = "value" in m
@@ -686,6 +690,18 @@ class TuyaDpsConfig:
                     result = -1 * result + r["min"] + r["max"]
                     replaced = True
 
+            if target_range and isinstance(result, Number):
+                r = self._config.get("range")
+                if r and "max" in r and "max" in target_range:
+                    from_min = r.get("min", 0)
+                    from_max = r["max"]
+                    to_min = target_range.get("min", 0)
+                    to_max = target_range["max"]
+                    result = to_min + (
+                        (result - from_min) * (to_max - to_min) / (from_max - from_min)
+                    )
+                    replaced = True
+
             if scale != 1 and isinstance(result, Number):
                 result = result / scale
                 replaced = True
@@ -693,7 +709,7 @@ class TuyaDpsConfig:
             if self.rawtype == "unixtime" and isinstance(result, int):
                 try:
                     result = datetime.fromtimestamp(result)
-                    replaced = true
+                    replaced = True
                 except:
                     _LOGGER.warning("Invalid timestamp %d", result)
 
@@ -809,6 +825,7 @@ class TuyaDpsConfig:
             invert = mapping.get("invert", False)
             mask = mapping.get("mask")
             endianness = mapping.get("endianness", "big")
+            target_range = mapping.get("target_range")
             step = mapping.get("step")
             if not isinstance(step, Number):
                 step = None
@@ -844,6 +861,7 @@ class TuyaDpsConfig:
 
                 step = cond.get("step", step)
                 redirect = cond.get("value_redirect", redirect)
+                target_range = cond.get("target_range", target_range)
 
             if redirect:
                 _LOGGER.debug("Redirecting %s to %s", self.name, redirect)
@@ -863,6 +881,18 @@ class TuyaDpsConfig:
                     result = remap["dps_val"]
                 replaced = True
 
+            if target_range and isinstance(result, Number):
+                r = self._config.get("range")
+                if r and "max" in r and "max" in target_range:
+                    from_min = target_range.get("min", 0)
+                    from_max = target_range["max"]
+                    to_min = r.get("min", 0)
+                    to_max = r["max"]
+                    result = to_min + (
+                        (result - from_min) * (to_max - to_min) / (from_max - from_min)
+                    )
+                    replaced = True
+
             if invert:
                 r = self._config.get("range")
                 if r and "min" in r and "max" in r:

+ 15 - 20
custom_components/tuya_local/light.py

@@ -7,7 +7,7 @@ from struct import pack, unpack
 import homeassistant.util.color as color_util
 from homeassistant.components.light import (
     ATTR_BRIGHTNESS,
-    ATTR_COLOR_TEMP,
+    ATTR_COLOR_TEMP_KELVIN,
     ATTR_EFFECT,
     ATTR_HS_COLOR,
     ATTR_WHITE,
@@ -55,6 +55,15 @@ class TuyaLocalLight(TuyaLocalEntity, LightEntity):
         self._effect_dps = dps_map.pop("effect", None)
         self._init_end(dps_map)
 
+        # Set min and max color temp
+        if self._color_temp_dps:
+            m = self._color_temp_dps._find_map_for_dps(0)
+            if m:
+                tr = m.get("target_range")
+                if tr:
+                    self._attr_min_color_temp_kelvin = tr.get("min")
+                    self._attr_max_color_temp_kelvin = tr.get("max")
+
     @property
     def supported_color_modes(self):
         """Return the supported color modes for this light."""
@@ -111,17 +120,10 @@ class TuyaLocalLight(TuyaLocalEntity, LightEntity):
                 return ColorMode(mode)
 
     @property
-    def color_temp(self):
-        """Return the color temperature in mireds"""
+    def color_temp_kelvin(self):
+        """Return the color temperature in kelvin."""
         if self._color_temp_dps:
-            unscaled = self._color_temp_dps.get_value(self._device)
-            r = self._color_temp_dps.range(self._device)
-            if r and isinstance(unscaled, (int, float)):
-                return round(
-                    unscaled * 347 / (r["max"] - r["min"]) + 153 - r["min"],
-                )
-            else:
-                return unscaled
+            return self._color_temp_dps.get_value(self._device)
 
     @property
     def is_on(self):
@@ -240,18 +242,11 @@ class TuyaLocalLight(TuyaLocalEntity, LightEntity):
                         bright,
                     ),
                 }
-        elif self._color_temp_dps and ATTR_COLOR_TEMP in params:
+        elif self._color_temp_dps and ATTR_COLOR_TEMP_KELVIN in params:
             if self.color_mode != ColorMode.COLOR_TEMP:
                 color_mode = ColorMode.COLOR_TEMP
 
-            color_temp = params.get(ATTR_COLOR_TEMP)
-            r = self._color_temp_dps.range(self._device)
-
-            if r and color_temp:
-                color_temp = round(
-                    (color_temp - 153 + r["min"]) * (r["max"] - r["min"]) / 347
-                )
-
+            color_temp = params.get(ATTR_COLOR_TEMP_KELVIN)
             _LOGGER.debug("Setting color temp to %d", color_temp)
             settings = {
                 **settings,

+ 6 - 2
tests/devices/test_arlec_fan_light.py

@@ -136,7 +136,11 @@ class TestArlecFan(SwitchableTests, BasicSelectTests, TuyaDeviceTestCase):
 
     def test_light_color_temp(self):
         self.dps[COLORTEMP_DPS] = 70
-        self.assertEqual(self.light.color_temp, 396)
+        self.assertEqual(self.light.color_temp_kelvin, 5360)
+
+    def test_light_color_temp_range(self):
+        self.assertEqual(self.light.min_color_temp_kelvin, 2700)
+        self.assertEqual(self.light.max_color_temp_kelvin, 6500)
 
     async def test_light_async_turn_on(self):
         async with assert_device_properties_set(
@@ -145,5 +149,5 @@ class TestArlecFan(SwitchableTests, BasicSelectTests, TuyaDeviceTestCase):
         ):
             await self.light.async_turn_on(
                 brightness=112,
-                color_temp=396,
+                color_temp_kelvin=5360,
             )

+ 8 - 4
tests/devices/test_rgbcw_lightbulb.py

@@ -43,13 +43,17 @@ class TestRGBCWLightbulb(BasicNumberTests, TuyaDeviceTestCase):
 
     def test_color_temp(self):
         self.dps[COLORTEMP_DPS] = 500
-        self.assertAlmostEqual(self.subject.color_temp, 326, 0)
+        self.assertEqual(self.subject.color_temp_kelvin, 4600)
         self.dps[COLORTEMP_DPS] = 1000
-        self.assertAlmostEqual(self.subject.color_temp, 153, 0)
+        self.assertEqual(self.subject.color_temp_kelvin, 6500)
         self.dps[COLORTEMP_DPS] = 0
-        self.assertAlmostEqual(self.subject.color_temp, 500, 0)
+        self.assertEqual(self.subject.color_temp_kelvin, 2700)
         self.dps[COLORTEMP_DPS] = None
-        self.assertEqual(self.subject.color_temp, None)
+        self.assertEqual(self.subject.color_temp_kelvin, None)
+
+    def test_color_temp_range(self):
+        self.assertEqual(self.subject.min_color_temp_kelvin, 2700)
+        self.assertEqual(self.subject.max_color_temp_kelvin, 6500)
 
     def test_color_mode(self):
         self.dps[MODE_DPS] = "white"