Browse Source

Lefant M213: add fan_speed dp, separate cmd from status.

- additional dps identified for status and fan speed (suction level)
- changes in generic vacuum to allow separation of commands from status.

Status basically follows command, though it does distinguish charging and standby states when docked.  There are possibly other states such as errors...
Jason Rumney 3 years ago
parent
commit
bccc9d9cab

+ 29 - 5
custom_components/tuya_local/devices/lefant_m213_vacuum.yaml

@@ -10,7 +10,7 @@ primary_entity:
       name: activate
       name: activate
     - id: 3
     - id: 3
       type: string
       type: string
-      name: status
+      name: command
       mapping:
       mapping:
         - dps_val: standby
         - dps_val: standby
           value: standby
           value: standby
@@ -40,7 +40,24 @@ primary_entity:
           value: stop
           value: stop
     - id: 5
     - id: 5
       type: string
       type: string
-      name: unknown_5
+      name: status
+      mapping:
+        - dps_val: "0"
+          value: paused
+        - dps_val: "1"
+          value: smart
+        - dps_val: "2"
+          value: wall follow
+        - dps_val: "3"
+          value: spiral
+        - dps_val: "4"
+          value: returning
+        - dps_val: "5"
+          value: charging
+        - dps_val: "6"
+          value: random
+        - dps_val: "7"
+          value: standby
     - id: 6
     - id: 6
       name: battery
       name: battery
       type: integer
       type: integer
@@ -73,13 +90,20 @@ primary_entity:
           value: collision
           value: collision
     - id: 101
     - id: 101
       type: string
       type: string
-      name: unknown_101
+      name: fan_speed
+      mapping:
+        - dps_val: low
+          value: Low
+        - dps_val: nar
+          value: Medium
+        - dps_val: high
+          value: High
     - id: 102
     - id: 102
       type: integer
       type: integer
       name: unknown_102
       name: unknown_102
     - id: 103
     - id: 103
       type: integer
       type: integer
-      name: unknown_103
+      name: heading
     - id: 104
     - id: 104
       type: integer
       type: integer
       name: unknown_104
       name: unknown_104
@@ -88,7 +112,7 @@ primary_entity:
 #    - id: 106
 #    - id: 106
 #      type: string
 #      type: string
 #      name: unknown_106
 #      name: unknown_106
-# dp 108 returned only by Lefant M213, not be M213S or APOSEN A550
+# dp 108 returned only sometimes?
 # seems to be some encoded ASCII descriptor of battery voltage
 # seems to be some encoded ASCII descriptor of battery voltage
 #    - id: 108
 #    - id: 108
 #      type: string
 #      type: string

+ 17 - 16
custom_components/tuya_local/generic/vacuum.py

@@ -28,6 +28,7 @@ class TuyaLocalVacuum(TuyaLocalEntity, StateVacuumEntity):
         """
         """
         dps_map = self._init_begin(device, config)
         dps_map = self._init_begin(device, config)
         self._status_dps = dps_map.get("status")
         self._status_dps = dps_map.get("status")
+        self._command_dps = dps_map.get("command")
         self._locate_dps = dps_map.get("locate")
         self._locate_dps = dps_map.get("locate")
         self._power_dps = dps_map.get("power")
         self._power_dps = dps_map.get("power")
         self._active_dps = dps_map.get("activate")
         self._active_dps = dps_map.get("activate")
@@ -59,10 +60,11 @@ class TuyaLocalVacuum(TuyaLocalEntity, StateVacuumEntity):
         if self._locate_dps:
         if self._locate_dps:
             support |= VacuumEntityFeature.LOCATE
             support |= VacuumEntityFeature.LOCATE
 
 
-        status_support = self._status_dps.values(self._device)
-        if SERVICE_RETURN_TO_BASE in status_support:
+        cmd_dps = self._command_dps or self._status_dps
+        cmd_support = cmd_dps.values(self._device)
+        if SERVICE_RETURN_TO_BASE in cmd_support:
             support |= VacuumEntityFeature.RETURN_HOME
             support |= VacuumEntityFeature.RETURN_HOME
-        if SERVICE_CLEAN_SPOT in status_support:
+        if SERVICE_CLEAN_SPOT in cmd_support:
             support |= VacuumEntityFeature.CLEAN_SPOT
             support |= VacuumEntityFeature.CLEAN_SPOT
         return support
         return support
 
 
@@ -80,12 +82,12 @@ class TuyaLocalVacuum(TuyaLocalEntity, StateVacuumEntity):
     @property
     @property
     def state(self):
     def state(self):
         """Return the state of the vacuum cleaner."""
         """Return the state of the vacuum cleaner."""
-        status = self._status_dps.get_value(self._device)
+        status = self.status
         if self._error_dps and self._error_dps.get_value(self._device) != 0:
         if self._error_dps and self._error_dps.get_value(self._device) != 0:
             return STATE_ERROR
             return STATE_ERROR
-        elif status == SERVICE_RETURN_TO_BASE:
+        elif status in [SERVICE_RETURN_TO_BASE, "returning"]:
             return STATE_RETURNING
             return STATE_RETURNING
-        elif status == "standby":
+        elif status in ["standby", "charging"]:
             return STATE_DOCKED
             return STATE_DOCKED
         elif self._power_dps and not self._power_dps.get_value(self._device):
         elif self._power_dps and not self._power_dps.get_value(self._device):
             return STATE_DOCKED
             return STATE_DOCKED
@@ -124,17 +126,15 @@ class TuyaLocalVacuum(TuyaLocalEntity, StateVacuumEntity):
 
 
     async def async_return_to_base(self, **kwargs):
     async def async_return_to_base(self, **kwargs):
         """Tell the vacuum cleaner to return to its base."""
         """Tell the vacuum cleaner to return to its base."""
-        if self._status_dps and SERVICE_RETURN_TO_BASE in self._status_dps.values(
-            self._device
-        ):
-            await self._status_dps.async_set_value(self._device, SERVICE_RETURN_TO_BASE)
+        dps = self._command_dps or self._status_dps
+        if dps and SERVICE_RETURN_TO_BASE in dps.values(self._device):
+            await dps.async_set_value(self._device, SERVICE_RETURN_TO_BASE)
 
 
     async def async_clean_spot(self, **kwargs):
     async def async_clean_spot(self, **kwargs):
         """Tell the vacuum cleaner do a spot clean."""
         """Tell the vacuum cleaner do a spot clean."""
-        if self._status_dps and SERVICE_CLEAN_SPOT in self._status_dps.values(
-            self._device
-        ):
-            await self._status_dps.async_set_value(self._device, SERVICE_CLEAN_SPOT)
+        dps = self._command_dps or self._status_dps
+        if dps and SERVICE_CLEAN_SPOT in dps.values(self._device):
+            await dps.async_set_value(self._device, SERVICE_CLEAN_SPOT)
 
 
     async def async_locate(self, **kwargs):
     async def async_locate(self, **kwargs):
         """Locate the vacuum cleaner."""
         """Locate the vacuum cleaner."""
@@ -143,8 +143,9 @@ class TuyaLocalVacuum(TuyaLocalEntity, StateVacuumEntity):
 
 
     async def async_send_command(self, command, params=None, **kwargs):
     async def async_send_command(self, command, params=None, **kwargs):
         """Send a command to the vacuum cleaner."""
         """Send a command to the vacuum cleaner."""
-        if command in self._status_dps.values(self._device):
-            await self._status_dps.async_set_value(self._device, command)
+        dps = self._command_dps or self._status_dps
+        if command in dps.values(self._device):
+            await dps.async_set_value(self._device, command)
         elif self._direction_dps and command in self._direction_dps.values(
         elif self._direction_dps and command in self._direction_dps.values(
             self._device
             self._device
         ):
         ):

+ 1 - 1
requirements-dev.txt

@@ -4,5 +4,5 @@ pytest-homeassistant-custom-component>=0.9.0
 pytest
 pytest
 pytest-asyncio
 pytest-asyncio
 pytest-cov
 pytest-cov
-pycryptodome~=3.14.1
+pycryptodome~=3.15.0
 tinytuya~=1.6.0
 tinytuya~=1.6.0

+ 1 - 1
requirements.txt

@@ -1,2 +1,2 @@
-pycryptodome~=3.14.1
+pycryptodome~=3.15.0
 tinytuya~=1.6.0
 tinytuya~=1.6.0

+ 35 - 22
tests/devices/test_lefant_m213_vacuum.py

@@ -17,15 +17,15 @@ from .base_device_tests import TuyaDeviceTestCase
 
 
 POWER_DPS = "1"
 POWER_DPS = "1"
 SWITCH_DPS = "2"
 SWITCH_DPS = "2"
-STATUS_DPS = "3"
+COMMAND_DPS = "3"
 DIRECTION_DPS = "4"
 DIRECTION_DPS = "4"
-UNKNOWN5_DPS = "5"
+STATUS_DPS = "5"
 BATTERY_DPS = "6"
 BATTERY_DPS = "6"
 LOCATE_DPS = "13"
 LOCATE_DPS = "13"
 AREA_DPS = "16"
 AREA_DPS = "16"
 TIME_DPS = "17"
 TIME_DPS = "17"
 ERROR_DPS = "18"
 ERROR_DPS = "18"
-UNKNOWN101_DPS = "101"
+FAN_DPS = "101"
 UNKNOWN102_DPS = "102"
 UNKNOWN102_DPS = "102"
 UNKNOWN103_DPS = "103"
 UNKNOWN103_DPS = "103"
 UNKNOWN104_DPS = "104"
 UNKNOWN104_DPS = "104"
@@ -70,6 +70,7 @@ class TestLefantM213Vacuum(MultiSensorTests, TuyaDeviceTestCase):
                 | VacuumEntityFeature.LOCATE
                 | VacuumEntityFeature.LOCATE
                 | VacuumEntityFeature.RETURN_HOME
                 | VacuumEntityFeature.RETURN_HOME
                 | VacuumEntityFeature.CLEAN_SPOT
                 | VacuumEntityFeature.CLEAN_SPOT
+                | VacuumEntityFeature.FAN_SPEED
             ),
             ),
         )
         )
 
 
@@ -77,29 +78,41 @@ class TestLefantM213Vacuum(MultiSensorTests, TuyaDeviceTestCase):
         self.dps[BATTERY_DPS] = 50
         self.dps[BATTERY_DPS] = 50
         self.assertEqual(self.subject.battery_level, 50)
         self.assertEqual(self.subject.battery_level, 50)
 
 
+    def test_fan_speed(self):
+        self.dps[FAN_DPS] = "low"
+        self.assertEqual(self.subject.fan_speed, "Low")
+        self.dps[FAN_DPS] = "nar"
+        self.assertEqual(self.subject.fan_speed, "Medium")
+        self.dps[FAN_DPS] = "high"
+        self.assertEqual(self.subject.fan_speed, "High")
+
     def test_status(self):
     def test_status(self):
-        self.dps[STATUS_DPS] = "standby"
-        self.assertEqual(self.subject.status, "standby")
-        self.dps[STATUS_DPS] = "smart"
+        self.dps[STATUS_DPS] = "0"
+        self.assertEqual(self.subject.status, "paused")
+        self.dps[STATUS_DPS] = "1"
         self.assertEqual(self.subject.status, "smart")
         self.assertEqual(self.subject.status, "smart")
-        self.dps[STATUS_DPS] = "chargego"
-        self.assertEqual(self.subject.status, "return_to_base")
-        self.dps[STATUS_DPS] = "random"
+        self.dps[STATUS_DPS] = "2"
+        self.assertEqual(self.subject.status, "wall follow")
+        self.dps[STATUS_DPS] = "3"
+        self.assertEqual(self.subject.status, "spiral")
+        self.dps[STATUS_DPS] = "4"
+        self.assertEqual(self.subject.status, "returning")
+        self.dps[STATUS_DPS] = "5"
+        self.assertEqual(self.subject.status, "charging")
+        self.dps[STATUS_DPS] = "6"
         self.assertEqual(self.subject.status, "random")
         self.assertEqual(self.subject.status, "random")
-        self.dps[STATUS_DPS] = "wall_follow"
-        self.assertEqual(self.subject.status, "wall_follow")
-        self.dps[STATUS_DPS] = "spiral"
-        self.assertEqual(self.subject.status, "clean_spot")
+        self.dps[STATUS_DPS] = "7"
+        self.assertEqual(self.subject.status, "standby")
 
 
     def test_state(self):
     def test_state(self):
         self.dps[POWER_DPS] = True
         self.dps[POWER_DPS] = True
         self.dps[SWITCH_DPS] = True
         self.dps[SWITCH_DPS] = True
         self.dps[ERROR_DPS] = 0
         self.dps[ERROR_DPS] = 0
-        self.dps[STATUS_DPS] = "return_to_base"
+        self.dps[STATUS_DPS] = "4"
         self.assertEqual(self.subject.state, STATE_RETURNING)
         self.assertEqual(self.subject.state, STATE_RETURNING)
-        self.dps[STATUS_DPS] = "standby"
+        self.dps[STATUS_DPS] = "7"
         self.assertEqual(self.subject.state, STATE_DOCKED)
         self.assertEqual(self.subject.state, STATE_DOCKED)
-        self.dps[STATUS_DPS] = "random"
+        self.dps[STATUS_DPS] = "6"
         self.assertEqual(self.subject.state, STATE_CLEANING)
         self.assertEqual(self.subject.state, STATE_CLEANING)
         self.dps[POWER_DPS] = False
         self.dps[POWER_DPS] = False
         self.assertEqual(self.subject.state, STATE_DOCKED)
         self.assertEqual(self.subject.state, STATE_DOCKED)
@@ -148,14 +161,14 @@ class TestLefantM213Vacuum(MultiSensorTests, TuyaDeviceTestCase):
     async def test_async_return_to_base(self):
     async def test_async_return_to_base(self):
         async with assert_device_properties_set(
         async with assert_device_properties_set(
             self.subject._device,
             self.subject._device,
-            {STATUS_DPS: "chargego"},
+            {COMMAND_DPS: "chargego"},
         ):
         ):
             await self.subject.async_return_to_base()
             await self.subject.async_return_to_base()
 
 
     async def test_async_clean_spot(self):
     async def test_async_clean_spot(self):
         async with assert_device_properties_set(
         async with assert_device_properties_set(
             self.subject._device,
             self.subject._device,
-            {STATUS_DPS: "spiral"},
+            {COMMAND_DPS: "spiral"},
         ):
         ):
             await self.subject.async_clean_spot()
             await self.subject.async_clean_spot()
 
 
@@ -169,28 +182,28 @@ class TestLefantM213Vacuum(MultiSensorTests, TuyaDeviceTestCase):
     async def test_async_send_standby_command(self):
     async def test_async_send_standby_command(self):
         async with assert_device_properties_set(
         async with assert_device_properties_set(
             self.subject._device,
             self.subject._device,
-            {STATUS_DPS: "standby"},
+            {COMMAND_DPS: "standby"},
         ):
         ):
             await self.subject.async_send_command("standby")
             await self.subject.async_send_command("standby")
 
 
     async def test_async_send_smart_command(self):
     async def test_async_send_smart_command(self):
         async with assert_device_properties_set(
         async with assert_device_properties_set(
             self.subject._device,
             self.subject._device,
-            {STATUS_DPS: "smart"},
+            {COMMAND_DPS: "smart"},
         ):
         ):
             await self.subject.async_send_command("smart")
             await self.subject.async_send_command("smart")
 
 
     async def test_async_send_random_command(self):
     async def test_async_send_random_command(self):
         async with assert_device_properties_set(
         async with assert_device_properties_set(
             self.subject._device,
             self.subject._device,
-            {STATUS_DPS: "random"},
+            {COMMAND_DPS: "random"},
         ):
         ):
             await self.subject.async_send_command("random")
             await self.subject.async_send_command("random")
 
 
     async def test_async_send_wall_follow_command(self):
     async def test_async_send_wall_follow_command(self):
         async with assert_device_properties_set(
         async with assert_device_properties_set(
             self.subject._device,
             self.subject._device,
-            {STATUS_DPS: "wall_follow"},
+            {COMMAND_DPS: "wall_follow"},
         ):
         ):
             await self.subject.async_send_command("wall_follow")
             await self.subject.async_send_command("wall_follow")