Просмотр исходного кода

motion sensor light: add an "Auto mode" switch

Add a switch for switching between auto and manual.  Since the effect
attribute is not available when the light is off, it is not so
convenient only having it there, because you can't tell the difference
between off and auto modes when the light is off.

Issue #282

To acheive this, the handling of conditions was modified to not try to
set the constraint dp when it is read-only, and instead require an
exact match.  This allows us to choose between "mode_off" and
"mode_on" when turning the auto mode switch off, to avoid changing the
light state as a side effect.  The previous condition handling was
selecting the first match for off and trying to change both dps to
make that condition match.

- as a side effect of these changes, the Fairland IPHCR15 heatpump also
changed behaviour, as it had a mode dp marked as readonly in the
config, used as conditions for hvac_mode.  A comment on #222 says
that despite being described as readonly in the Tuya portal, it
appeared possible to change it, so to avoid unexpected problems, the
readonly attribute was removed so that current behaviour is retained.
Jason Rumney 3 лет назад
Родитель
Сommit
dad58b51e1

+ 60 - 0
custom_components/tuya_local/devices/README.md

@@ -375,6 +375,66 @@ that mapping.  These nested mappings are limited to simple `dps_val` to `value`
 substitutions, as more complex rules would quickly become too complex to
 manage.
 
+When setting a dp which has conditions attached, the behaviour is slightly different depending on whether the constraint dp is readonly or not.
+
+For non-readonly constraints, the constraint dp will be set along with the
+target dp so that the first condition with a value matching the target value
+is met.
+
+For readonly constraints, the condition must match the constraint dp's current value for anything to be set.
+
+**Example**
+```yaml
+  ...
+  name: target_dp
+  mapping:
+    - dps_val: 1
+      constraint: constraint_dp
+      conditions:
+        - dps_val: a
+          value: x
+        - dpa_val: c
+          value: z
+    - dps_val: 2
+      constraint: constraint_dp
+      conditions:
+        - dps_val: b
+          value: x
+        - dps_val: c
+          value: y
+```
+If `constraint_dp` is not readonly:
+|---|---|---|
+| constraint_dp current dps_val | target_dp target value | dps set |
+|---|---|---|
+| a | x | target_dp: 1, constraint_dp: a |
+| a | y | target_dp: 2, constraint_dp: c |
+| a | z | target_dp: 1, constraint_dp: c |
+| b | x | target_dp: 1, constraint_dp: a |
+| b | y | target_dp: 2, constraint_dp: c |
+| b | z | target_dp: 1, constraint_dp: c |
+| c | x | target_dp: 1, constraint_dp: a |
+| c | y | target_dp: 2, constraint_dp: c |
+| c | z | target_dp: 1, constraint_dp: c |
+|---|---|---|
+
+If `constraint_dp` is readonly:
+|---|---|---|
+| current constraint_dp | target target_dp | dps set |
+|---|---|---|
+| a | x | target_dp: 1 |
+| a | y | - |
+| a | z | - |
+| b | x | target_dp: 2 |
+| b | y | - |
+| b | z | - |
+| c | x | - |
+| c | y | target_dp: 2 |
+| c | z | target_dp: 1 |
+|---|---|---|
+
+
+
 ## Entity types
 
 Entities have specific mappings of dp names to functions.  Any unrecognized dp name is added

+ 0 - 1
custom_components/tuya_local/devices/fairland_iphcr15_heatpump.yaml

@@ -34,7 +34,6 @@ primary_entity:
     - id: 105
       name: mode
       type: string
-      readonly: true
       icon_priority: 4
       mapping:
         - dps_val: warm

+ 25 - 0
custom_components/tuya_local/devices/motion_sensor_light.yaml

@@ -24,6 +24,31 @@ primary_entity:
       name: switch
       readonly: true
 secondary_entities:
+  - entity: switch
+    name: Auto mode
+    icon: "mdi:motion-sensor"
+    dps:
+      - id: 101
+        type: string
+        name: switch
+        mapping:
+          - dps_val: mode_auto
+            value: on
+          - dps_val: mode_on
+            constraint: relay
+            conditions:
+              - dps_val: true
+                value: false
+          - dps_val: mode_off
+            constraint: relay
+            conditions:
+              - dps_val: false
+                value: false
+      - id: 102
+        name: relay
+        type: boolean
+        readonly: true
+        hidden: true
   - entity: number
     name: Sensitivity
     icon: "mdi:human-greeting-proximity"

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

@@ -590,7 +590,13 @@ class TuyaDpsConfig:
 
             for c in m.get("conditions", {}):
                 if "value" in c and str(c["value"]) == str(value):
-                    return m
+                    c_dp = self._entity.find_dps(m.get("constraint"))
+                    # only consider the condition a match if we can change
+                    # the dp to match, or it already matches
+                    if not c_dp.readonly or device.get_property(c_dp.id) == c.get(
+                        "dps_val"
+                    ):
+                        return m
                 if "value" not in c and "value_mirror" in c:
                     r_dps = self._entity.find_dps(c["value_mirror"])
                     if str(r_dps.get_value(device)) == str(value):
@@ -627,6 +633,9 @@ class TuyaDpsConfig:
         """Return the dps values that would be set when setting to value"""
         result = value
         dps_map = {}
+        if self.readonly:
+            return dps_map
+
         mapping = self._find_map_for_value(value, device)
         if mapping:
             replaced = False

+ 50 - 1
tests/devices/test_motion_sensor_light.py

@@ -20,7 +20,7 @@ class TestMotionLight(BasicSwitchTests, MultiNumberTests, TuyaDeviceTestCase):
     def setUp(self):
         self.setUpForConfig("motion_sensor_light.yaml", MOTION_LIGHT_PAYLOAD)
         self.subject = self.entities.get("light")
-
+        self.auto_sw = self.entities.get("switch_auto_mode")
         self.setUpBasicSwitch(RESET_DPS, self.entities.get("switch_auto_reset"))
         self.setUpMultiNumber(
             [
@@ -99,3 +99,52 @@ class TestMotionLight(BasicSwitchTests, MultiNumberTests, TuyaDeviceTestCase):
             {EFFECT_DPS: "mode_auto"},
         ):
             await self.subject.async_turn_on(effect="auto")
+
+    def test_auto_mode_is_on(self):
+        self.dps[SWITCH_DPS] = False
+        self.dps[EFFECT_DPS] = "mode_auto"
+        self.assertTrue(self.auto_sw.is_on)
+        self.assertFalse(self.subject.is_on)
+        self.dps[EFFECT_DPS] = "mode_off"
+        self.assertFalse(self.auto_sw.is_on)
+        self.assertFalse(self.subject.is_on)
+        self.dps[SWITCH_DPS] = True
+        self.dps[EFFECT_DPS] = "mode_auto"
+        self.assertTrue(self.auto_sw.is_on)
+        self.assertTrue(self.subject.is_on)
+        self.dps[EFFECT_DPS] = "mode_on"
+        self.assertFalse(self.auto_sw.is_on)
+        self.assertTrue(self.subject.is_on)
+
+    async def test_auto_mode_async_turn_on_off(self):
+        self.dps[SWITCH_DPS] = True
+        self.dps[EFFECT_DPS] = "mode_auto"
+        async with assert_device_properties_set(
+            self.subject._device,
+            {EFFECT_DPS: "mode_on"},
+        ):
+            await self.auto_sw.async_turn_off()
+
+        self.dps[SWITCH_DPS] = False
+        self.dps[EFFECT_DPS] = "mode_auto"
+        async with assert_device_properties_set(
+            self.subject._device,
+            {EFFECT_DPS: "mode_off"},
+        ):
+            await self.auto_sw.async_turn_off()
+
+        self.dps[SWITCH_DPS] = True
+        self.dps[EFFECT_DPS] = "mode_on"
+        async with assert_device_properties_set(
+            self.subject._device,
+            {EFFECT_DPS: "mode_auto"},
+        ):
+            await self.auto_sw.async_turn_on()
+
+        self.dps[SWITCH_DPS] = False
+        self.dps[EFFECT_DPS] = "mode_off"
+        async with assert_device_properties_set(
+            self.subject._device,
+            {EFFECT_DPS: "mode_auto"},
+        ):
+            await self.auto_sw.async_turn_on()