Procházet zdrojové kódy

Cover: refactor state logic into a single function.

is_opening, is_closing and is_closed had independent logic, so there
was a possibility that they were returning inconsistent values.  Use a
single function to work out the current state of the cover based on
whatever information is available, returning None if there is any
ambiguity, to avoid leaving it stuck in a state.  Modify the API
functions to use the new function.

- update tests for some of the simpler devices, as the state now
returns None in more cases.

- mark zemismart_roller_blinds action dp as unreliable. Leave it for
monitoring, but don't use it to determine cover state.

Trying to resolve the issues raised in PR #672, #655 without adding
internal state, which is likely to get out of sync with the actual state.
Jason Rumney před 2 roky
rodič
revize
4aad42fe08

+ 59 - 27
custom_components/tuya_local/cover.py

@@ -104,47 +104,79 @@ class TuyaLocalCover(TuyaLocalEntity, CoverEntity):
             return self._state_to_percent(state)
 
     @property
-    def is_opening(self):
-        """Return if the cover is opening or not."""
-        # If dps is available to inform current action, use that
+    def _current_state(self):
+        """Return the current state of the cover if it can be determined,
+        or None if it is inconclusive.
+        """
         if self._action_dp:
             action = self._action_dp.get_value(self._device)
-            if action is not None:
-                return action == "opening"
-        # Otherwise use last command and check it hasn't completed
+            if action in ["opening", "closing", "open", "closed"]:
+                return action
+
+        if self._currentpos_dp:
+            pos = self._currentpos_dp.get_value(self._device)
+            # we have a current pos dp, but it isn't telling us where the
+            # curtain is... we can't tell the state.
+            if pos is None:
+                return None
+            if pos < 5:
+                return "closed"
+            elif pos > 95:
+                return "open"
+            if self._position_dp:
+                setpos = self._position_dp.get_value(self._device)
+                if setpos == pos:
+                    # if the current position is around the set position,
+                    # probably the curtain is as set, somewhere in the middle
+                    # so none of open, closed, opening or closing
+                    return None
         if self._control_dp:
             cmd = self._control_dp.get_value(self._device)
             pos = self.current_cover_position
             if pos is not None:
-                return (
-                    cmd != "close"
-                    and cmd != "stop"
-                    and self.current_cover_position < 95
-                )
+                if cmd == "open":
+                    if pos > 95:
+                        return "open"
+                    else:
+                        return "opening"
+                elif cmd == "close":
+                    if pos < 5:
+                        return "closed"
+                    else:
+                        return "closing"
+
+    @property
+    def is_opening(self):
+        """Return if the cover is opening or not."""
+        state = self._current_state
+        if state is None:
+            # If we return false, and is_closing and is_opening are also false,
+            # HA assumes open.  If we don't know, return None.
+            return None
+        else:
+            return state == "opening"
 
     @property
     def is_closing(self):
         """Return if the cover is closing or not."""
-        # If dps is available to inform current action, use that
-        if self._action_dp:
-            action = self._action_dp.get_value(self._device)
-            if action is not None:
-                return action == "closing"
-        # Otherwise use last command and check it hasn't completed
-        if self._control_dp:
-            closed = self.is_closed
-            cmd = self._control_dp.get_value(self._device)
-            if closed is not None:
-                return cmd != "open" and cmd != "stop" and not closed
+        state = self._current_state
+        if state is None:
+            # If we return false, and is_closing and is_opening are also false,
+            # HA assumes open.  If we don't know, return None.
+            return None
+        else:
+            return state == "closing"
 
     @property
     def is_closed(self):
         """Return if the cover is closed or not, if it can be determined."""
-        # Only use position if it is reliable, otherwise curtain can become
-        # stuck in "open" state when we don't actually know what state it is.
-        pos = self.current_cover_position
-        if isinstance(pos, int):
-            return pos < 5
+        state = self._current_state
+        if state is None:
+            # If we return false, and is_closing and is_opening are also false,
+            # HA assumes open.  If we don't know, return None.
+            return None
+        else:
+            return state == "closed"
 
     async def async_open_cover(self, **kwargs):
         """Open the cover."""

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

@@ -33,7 +33,7 @@ primary_entity:
         max: 100
       invert: true
     - id: 7
-      name: action
+      name: unreliable_action
       type: string
     - id: 12
       name: fault_code

+ 1 - 0
tests/devices/test_garage_door_opener.py

@@ -57,6 +57,7 @@ class TestSimpleGarageOpener(TuyaDeviceTestCase):
         self.assertFalse(self.subject.is_closing)
 
     def test_is_closed(self):
+        self.dps[SWITCH_DPS] = False
         self.dps[OPEN_DPS] = True
         self.assertFalse(self.subject.is_closed)
         self.dps[OPEN_DPS] = False

+ 1 - 0
tests/devices/test_qs_c01_curtain.py

@@ -81,6 +81,7 @@ class TestQSC01Curtains(BasicNumberTests, BasicSelectTests, TuyaDeviceTestCase):
         self.assertFalse(self.subject.is_closing)
 
     def test_is_closed(self):
+        self.dps[COMMAND_DPS] = "close"
         self.dps[POSITION_DPS] = 100
         self.assertFalse(self.subject.is_closed)
         self.dps[POSITION_DPS] = 0

+ 3 - 0
tests/devices/test_simple_blinds.py

@@ -62,10 +62,13 @@ class TestSimpleBlinds(TuyaDeviceTestCase):
         self.assertFalse(self.subject.is_closing)
 
     def test_is_closed(self):
+        self.dps[COMMAND_DPS] = "close"
         self.dps[POSITION_DPS] = 0
         self.assertFalse(self.subject.is_closed)
         self.dps[POSITION_DPS] = 100
         self.assertTrue(self.subject.is_closed)
+        self.dps[COMMAND_DPS] = "stop"
+        self.assertIsNone(self.subject.is_closed)
 
     async def test_open_cover(self):
         async with assert_device_properties_set(