فهرست منبع

Add support for Eesee Adam dehumidifier

Issue #180

Add some comments about commented out lines in Lefant M213 config.
Update docs for recent changes.
Jason Rumney 3 سال پیش
والد
کامیت
b162ea61f6

+ 3 - 0
ACKNOWLEDGEMENTS.md

@@ -94,3 +94,6 @@ Further device support has been made with the assistance of users.  Please consi
 - [Krispkiwi](https://github.com/Krispkiwi) for assistance with M027 curtain modules and debugging Kogan Kettle.
 - [craibo](https://github.com/craibo) for contributing support for Jiahong ET-72W thermostats.
 - [x-keita](https://github.com/x-keita) for contributing support for Betterlife BL1500 IR heaters.
+- [Der-Nax](https:github.com/Der-Nax) for assistant with a second variant of energy monitoring smart switch.
+- [17hoehbr](https://github.com/17hoehbr) for assistance supporting APOSEN A550
+- [yurgh](https://github.com/yurgh) for assistant supporting Eesee Adam dehumidifier

+ 5 - 1
README.md

@@ -121,6 +121,7 @@ the device will not work despite being listed below.
 - Kogan SmarterHome 7L Desiccant dehumidifier
 - JJPro JPD01 dehumidifer
 - JJPro JPD02 dehumidifier
+- Eesee Adam dehumidifier
 
 ### Humidifiers
 - Eanons QT-JS2014 Purifying humidifier
@@ -158,6 +159,9 @@ Other brands may work with the above configurations
 - Simple Switch - a switch only, can be a fallback for many other unsupported devices, to allow just power to be switched on/off.
 - Simple Switch with Timer - a single switch and timer, will probably work for a lot of smart switches that are not covered by the more advanced configs above.
 
+### Lights
+- Generic RGBCW/RGBWW lightbulb (expected to match many such devices)
+
 ### Covers
 - Simple Garage Door
 - Simple Blind Controller
@@ -166,7 +170,7 @@ Other brands may work with the above configurations
 - M027 Curtain Module (sold under several brands, including zemismart, meterk and others)
 
 ### Vacuum Cleaners
-- Lefant M213 Vacuum Cleaner
+- Lefant M213 Vacuum Cleaner (also works for Lefant M213S and APOSEN A550)
 - Kyvol E30 Vacuum Cleaner
 
 ### Miscellaneous

+ 155 - 0
custom_components/tuya_local/devices/eesee_adam_dehumidifier.yaml

@@ -0,0 +1,155 @@
+name: Eesee Adam dehumidifier
+primary_entity:
+  entity: humidifier
+  class: dehumidifier
+  dps:
+    - id: 1
+      name: switch
+      type: boolean
+      mapping:
+      - dps_val: false
+        icon: "mdi:air-humidifier-off"
+        icon_priority: 1
+      - dps_val: true
+        icon: "mdi:air-humidifier"
+        icon_priority: 4
+    - id: 2
+      name: humidity
+      type: integer
+      range:
+        min: 25
+        max: 80
+      mapping:
+        - step: 5
+    - id: 4
+      name: mode
+      type: string
+      mapping:
+        - dps_val: manual
+          value: Manual
+        - dps_val: laundry
+          value: Dry clothes
+          icon: "mdi:tshirt-crew-outline"
+          icon_priority: 3
+    - id: 19
+      type: bitfield
+      name: error
+      mapping:
+        - dps_val: 1
+          icon: "mdi:cup-water"
+          icon_priority: 2
+      hidden: true
+secondary_entities:
+  - entity: fan
+    dps:
+      - id: 1
+        type: boolean
+        name: switch
+      - id: 4
+        type: string
+        name: dehumidifier_mode
+        hidden: true
+      - id: 5
+        type: string
+        name: speed
+        mapping:
+          - dps_val: low
+            value: 50
+            constraint: dehumidifier_mode
+            conditions:
+              - dps_val: laundry
+                invalid: true
+          - dps_val: high
+            value: 100
+  - entity: sensor
+    name: Current Humidity
+    class: humidity
+    dps:
+      - id: 16
+        type: integer
+        name: sensor
+        unit: "%"
+        class: measurement
+  - entity: binary_sensor
+    name: Tank
+    class: problem
+    category: diagnostic
+    dps:
+      - id: 19
+        type: bitfield
+        name: sensor
+        mapping:
+          - dps_val: 0
+            value: false
+          - value: true
+  - entity: lock
+    name: Child Lock
+    category: config
+    dps:
+      - id: 14
+        type: boolean
+        name: lock
+        mapping:
+          - dps_val: true
+            icon: "mdi:hand-back-right-off"
+          - dps_val: false
+            icon: "mdi:hand-back-right"
+  - entity: select
+    name: Timer
+    icon: "mdi:timer"
+    category: config
+    dps:
+      - id: 17
+        type: string
+        name: option
+        mapping:
+          - dps_val: cancel
+            value: "Off"
+          - dps_val: 1h
+            value: 1 hour
+          - dps_val: 2h
+            value: 2 hours
+          - dps_val: 3h
+            value: 3 hours
+          - dps_val: 4h
+            value: 4 hours
+          - dps_val: 5h
+            value: 5 hours
+          - dps_val: 6h
+            value: 6 hours
+          - dps_val: 7h
+            value: 7 hours
+          - dps_val: 8h
+            value: 8 hours
+          - dps_val: 9h
+            value: 9 hours
+          - dps_val: 10h
+            value: 10 hours
+          - dps_val: 11h
+            value: 11 hours
+          - dps_val: 12h
+            value: 12 hours
+          - dps_val: 13h
+            value: 13 hours
+          - dps_val: 14h
+            value: 14 hours
+          - dps_val: 15h
+            value: 15 hours
+          - dps_val: 16h
+            value: 16 hours
+          - dps_val: 17h
+            value: 17 hours
+          - dps_val: 18h
+            value: 18 hours
+          - dps_val: 19h
+            value: 19 hours
+          - dps_val: 20h
+            value: 20 hours
+          - dps_val: 21h
+            value: 21 hours
+          - dps_val: 22h
+            value: 22 hours
+          - dps_val: 23h
+            value: 23 hours
+          - dps_val: 24h
+            value: 24 hours

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

@@ -48,6 +48,7 @@ primary_entity:
     - id: 13
       type: boolean
       name: locate
+# Listed at iot.tuya.com, but does not seem to be returned by default
 #    - id: 15
 #      type: string
 #      name: clean_record
@@ -82,9 +83,13 @@ primary_entity:
     - id: 104
       type: integer
       name: unknown_104
+# dp 106 returned only by Lefant M213 and M213S, not by APOSEN A550
+# seems to be some encoded ASCII descriptor related to charging status
 #    - id: 106
 #      type: string
 #      name: unknown_106
+# dp 108 returned only by Lefant M213, not be M213S or APOSEN A550
+# seems to be some encoded ASCII descriptor of battery voltage
 #    - id: 108
 #      type: string
 #      name: unknown_108

+ 11 - 0
tests/const.py

@@ -1259,3 +1259,14 @@ BETTERLIFE_BL1500_PAYLOAD = {
     "11": "0",
     "12": 0,
 }
+
+EESEE_ADAM_PAYLOAD = {
+    "1": True,
+    "2": 50,
+    "4": "manual",
+    "5": "low",
+    "14": False,
+    "16": 72,
+    "17": "cancel",
+    "19": 0,
+}

+ 203 - 0
tests/devices/test_eesee_adam_dehumidifier.py

@@ -0,0 +1,203 @@
+from homeassistant.components.binary_sensor import BinarySensorDeviceClass
+from homeassistant.components.fan import FanEntityFeature
+from homeassistant.components.humidifier import HumidifierEntityFeature
+from homeassistant.components.sensor import SensorDeviceClass
+from homeassistant.const import PERCENTAGE
+
+from ..const import EESEE_ADAM_PAYLOAD
+from ..helpers import assert_device_properties_set
+from ..mixins.binary_sensor import BasicBinarySensorTests
+from ..mixins.lock import BasicLockTests
+from ..mixins.select import BasicSelectTests
+from ..mixins.sensor import BasicSensorTests
+from ..mixins.switch import SwitchableTests
+from .base_device_tests import TuyaDeviceTestCase
+
+SWITCH_DPS = "1"
+HUMIDITY_DPS = "2"
+MODE_DPS = "4"
+FAN_DPS = "5"
+LOCK_DPS = "14"
+CURRENTHUMID_DPS = "16"
+TIMER_DPS = "17"
+ERROR_DPS = "19"
+
+
+class TestEeseeAdamDehumidifier(
+    BasicBinarySensorTests,
+    BasicLockTests,
+    BasicSelectTests,
+    BasicSensorTests,
+    SwitchableTests,
+    TuyaDeviceTestCase,
+):
+    __test__ = True
+
+    def setUp(self):
+        self.setUpForConfig("eesee_adam_dehumidifier.yaml", EESEE_ADAM_PAYLOAD)
+        self.subject = self.entities.get("humidifier")
+        self.setUpSwitchable(SWITCH_DPS, self.subject)
+        self.fan = self.entities.get("fan")
+        self.setUpBasicLock(
+            LOCK_DPS,
+            self.entities.get("lock_child_lock"),
+        )
+        self.setUpBasicSelect(
+            TIMER_DPS,
+            self.entities.get("select_timer"),
+            {
+                "cancel": "Off",
+                "1h": "1 hour",
+                "2h": "2 hours",
+                "3h": "3 hours",
+                "4h": "4 hours",
+                "5h": "5 hours",
+                "6h": "6 hours",
+                "7h": "7 hours",
+                "8h": "8 hours",
+                "9h": "9 hours",
+                "10h": "10 hours",
+                "11h": "11 hours",
+                "12h": "12 hours",
+                "13h": "13 hours",
+                "14h": "14 hours",
+                "15h": "15 hours",
+                "16h": "16 hours",
+                "17h": "17 hours",
+                "18h": "18 hours",
+                "19h": "19 hours",
+                "20h": "20 hours",
+                "21h": "21 hours",
+                "22h": "22 hours",
+                "23h": "23 hours",
+                "24h": "24 hours",
+            },
+        )
+        self.setUpBasicBinarySensor(
+            ERROR_DPS,
+            self.entities.get("binary_sensor_tank"),
+            device_class=BinarySensorDeviceClass.PROBLEM,
+            testdata=(1, 0),
+        )
+        self.setUpBasicSensor(
+            CURRENTHUMID_DPS,
+            self.entities.get("sensor_current_humidity"),
+            device_class=SensorDeviceClass.HUMIDITY,
+            state_class="measurement",
+            unit=PERCENTAGE,
+        )
+        self.mark_secondary(["binary_sensor_tank", "lock_child_lock", "select_timer"])
+
+    def test_supported_features(self):
+        self.assertEqual(
+            self.subject.supported_features,
+            HumidifierEntityFeature.MODES,
+        )
+        self.assertEqual(
+            self.fan.supported_features,
+            FanEntityFeature.SET_SPEED,
+        )
+
+    def test_icon(self):
+        """Test that the icon is as expected."""
+        self.dps[SWITCH_DPS] = True
+        self.dps[MODE_DPS] = "manual"
+        self.assertEqual(self.subject.icon, "mdi:air-humidifier")
+
+        self.dps[SWITCH_DPS] = False
+        self.assertEqual(self.subject.icon, "mdi:air-humidifier-off")
+
+        self.dps[MODE_DPS] = "laundry"
+        self.assertEqual(self.subject.icon, "mdi:air-humidifier-off")
+
+        self.dps[SWITCH_DPS] = True
+        self.assertEqual(self.subject.icon, "mdi:tshirt-crew-outline")
+
+        self.dps[ERROR_DPS] = 1
+        self.assertEqual(self.subject.icon, "mdi:cup-water")
+
+    def test_min_target_humidity(self):
+        self.assertEqual(self.subject.min_humidity, 25)
+
+    def test_max_target_humidity(self):
+        self.assertEqual(self.subject.max_humidity, 80)
+
+    def test_target_humidity(self):
+        self.dps[HUMIDITY_DPS] = 55
+        self.assertEqual(self.subject.target_humidity, 55)
+
+    async def test_fan_turn_on(self):
+        async with assert_device_properties_set(
+            self.subject._device, {SWITCH_DPS: True}
+        ):
+            await self.fan.async_turn_on()
+
+    async def test_fan_turn_off(self):
+        async with assert_device_properties_set(
+            self.subject._device, {SWITCH_DPS: False}
+        ):
+            await self.fan.async_turn_off()
+
+    def test_modes(self):
+        self.assertCountEqual(
+            self.subject.available_modes,
+            [
+                "Manual",
+                "Dry clothes",
+            ],
+        )
+
+    def test_mode(self):
+        self.dps[MODE_DPS] = "manual"
+        self.assertEqual(self.subject.mode, "Manual")
+        self.dps[MODE_DPS] = "laundry"
+        self.assertEqual(self.subject.mode, "Dry clothes")
+
+    async def test_set_mode_to_manual(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {
+                MODE_DPS: "manual",
+            },
+        ):
+            await self.subject.async_set_mode("Manual")
+            self.subject._device.anticipate_property_value.assert_not_called()
+
+    async def test_set_mode_to_clothes(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {
+                MODE_DPS: "laundry",
+            },
+        ):
+            await self.subject.async_set_mode("Dry clothes")
+            self.subject._device.anticipate_property_value.assert_not_called()
+
+    def test_fan_speed_steps(self):
+        self.assertEqual(self.fan.speed_count, 2)
+
+    def test_fan_speed(self):
+        self.dps[FAN_DPS] = "low"
+        self.assertEqual(self.fan.percentage, 50)
+        self.dps[FAN_DPS] = "high"
+        self.assertEqual(self.fan.percentage, 100)
+        self.dps[MODE_DPS] = "laundry"
+        self.assertEqual(self.fan.percentage, 100)
+
+    async def test_fan_set_speed_to_low(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {
+                FAN_DPS: "low",
+            },
+        ):
+            await self.fan.async_set_percentage(50)
+
+    async def test_fan_set_speed_to_high(self):
+        async with assert_device_properties_set(
+            self.subject._device,
+            {
+                FAN_DPS: "high",
+            },
+        ):
+            await self.fan.async_set_percentage(100)