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

Add support for WHM04 doorbell

- support added for configuring whether a dp is persisted when not
  returned by a status poll.  Required for the bell event to be used
  as a trigger, as we need it to go to None between presses.

Issue #504
Jason Rumney 2 лет назад
Родитель
Сommit
8128351482

+ 1 - 0
ACKNOWLEDGEMENTS.md

@@ -208,3 +208,4 @@ Further device support has been made with the assistance of users.  Please consi
 - [denveronly](https://github.com/denveronly) for assisting with support for Dooya curtain motors.
 - [pasqualehun](https://github/com/pasqualehun) for contributing support for ZX-G30 alarm systems.
 - [maksmink](https://github.com/maksmink) for assisting with support for Eurom Mon Soleil 800 heaters (different again than the two 600 models already supported).
+- [nijel](https://github.com/nijel) for assisting with support for WHM-04 doorbells.

+ 4 - 0
DEVICES.md

@@ -302,6 +302,10 @@ of device.
 
 - Orion Grid Connect outdoor siren
 
+### Doorbells
+
+- WHM-04 doorbell (sold under various brands)
+
 ### Cameras
 
 - BCom Majic IPBox intercom camera

+ 13 - 0
custom_components/tuya_local/device.py

@@ -188,9 +188,15 @@ class TuyaLocalDevice(object):
                         self.name,
                         log_json(poll),
                     )
+                    full_poll = poll.pop("full_poll", False)
                     self._cached_state = self._cached_state | poll
                     self._cached_state["updated_at"] = time()
                     for entity in self._children:
+                        # clear non-persistant dps that were not in a full poll
+                        if full_poll:
+                            for dp in entity.dps():
+                                if not dp.persist and dp.id not in poll:
+                                    self._cached_state.pop(dp.id, None)
                         entity.async_schedule_update_ha_state()
                 else:
                     _LOGGER.debug(
@@ -229,6 +235,7 @@ class TuyaLocalDevice(object):
             try:
                 last_cache = self._cached_state.get("updated_at", 0)
                 now = time()
+                full_poll = False
                 if persist == self.should_poll:
                     # use persistent connections after initial communication
                     # has been established.  Until then, we need to rotate
@@ -254,6 +261,7 @@ class TuyaLocalDevice(object):
                             f"Failed to refresh device state for {self.name}",
                         )
                         dps_updated = False
+                        full_poll = True
                 else:
                     await self._hass.async_add_executor_job(
                         self._api.heartbeat,
@@ -277,6 +285,7 @@ class TuyaLocalDevice(object):
                     else:
                         if "dps" in poll:
                             poll = poll["dps"]
+                        poll["full_poll"] = full_poll
                         yield poll
 
                 await asyncio.sleep(0.1 if self.has_returned_state else 5)
@@ -369,6 +378,10 @@ class TuyaLocalDevice(object):
             self._cached_state = self._cached_state | new_state.get("dps", {})
             self._cached_state["updated_at"] = time()
             for entity in self._children:
+                for dp in entity.dps():
+                    # Clear non-persistant dps that were not in the poll
+                    if not dp.persist and dp.id not in new_state.get("dps", {}):
+                        self._cached_state.pop(dp.id, None)
                 entity.async_schedule_update_ha_state()
         _LOGGER.debug(
             "%s refreshed device state: %s",

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

@@ -158,6 +158,17 @@ matched even if it is not sending the dp at the time when adding a new device.
 It can also be used to match a range of devices that have variations in the extra
 attributes that are sent.
 
+### `persist`
+
+*Optional, default true.*
+
+Whether to persist the value if the device does not return it on every status
+refresh.  Some devices don't return every value on every status poll. In most
+cases, it is better to remember the previous value, but in some cases the
+dp is used to signal an event, so when it is next sent, it should trigger
+automations even if it is the same value as previously sent.  In that case 
+the value needs to go to null in between when the device is not sending it.
+
 ### `force`
 
 *Optional, default false.*

+ 146 - 0
custom_components/tuya_local/devices/whm04_doorbell.yaml

@@ -0,0 +1,146 @@
+name: Doorbell
+products:
+  - id: 26xtgamy3tv1awhl
+    name: WHM-04
+primary_entity:
+  entity: siren
+  icon: "mdi:bell"
+  dps:
+    - id: 2
+      type: integer
+      name: tone
+      mapping:
+        - dps_val: 0
+          value: Tone 0
+        - dps_val: 1
+          value: Tone 1
+        - dps_val: 2
+          value: Tone 2
+        - dps_val: 3
+          value: Tone 3
+        - dps_val: 4
+          value: Tone 4
+        - dps_val: 5
+          value: Tone 5
+        - dps_val: 6
+          value: Tone 6
+        - dps_val: 7
+          value: Tone 7
+        - dps_val: 8
+          value: Tone 8
+        - dps_val: 9
+          value: Tone 9
+        - dps_val: 10
+          value: Tone 10
+        - dps_val: 11
+          value: Tone 11
+        - dps_val: 12
+          value: Tone 12
+        - dps_val: 13
+          value: Tone 13
+        - dps_val: 14
+          value: Tone 14
+        - dps_val: 15
+          value: Tone 15
+        - dps_val: 16
+          value: Tone 16
+        - dps_val: 17
+          value: Tone 17
+        - dps_val: 18
+          value: Tone 18
+        - dps_val: 19
+          value: Tone 19
+        - dps_val: 20
+          value: Tone 20
+        - dps_val: 21
+          value: Tone 21
+        - dps_val: 22
+          value: Tone 22
+        - dps_val: 23
+          value: Tone 23
+        - dps_val: 24
+          value: Tone 24
+        - dps_val: 25
+          value: Tone 25
+        - dps_val: 26
+          value: Tone 26
+        - dps_val: 27
+          value: Tone 27
+        - dps_val: 28
+          value: Tone 28
+        - dps_val: 29
+          value: Tone 29
+        - dps_val: 30
+          value: Tone 30
+        - dps_val: 31
+          value: Tone 31
+        - dps_val: 32
+          value: Tone 32
+        - dps_val: 33
+          value: Tone 33
+        - dps_val: 34
+          value: Tone 34
+        - dps_val: 35
+          value: Tone 35
+        - dps_val: 36
+          value: Tone 36
+        - dps_val: 37
+          value: Tone 37
+        - dps_val: 38
+          value: Tone 38
+        - dps_val: 39
+          value: Tone 39
+        - dps_val: 40
+          value: Tone 40
+    - id: 3
+      type: integer
+      name: volume_level
+      range:
+        min: 0
+        max: 100
+      mapping:
+        - scale: 100
+    - id: 1
+      type: string
+      name: button_config
+      optional: true
+    - id: 7
+      type: string
+      name: undisturb_schedule
+      optional: true
+secondary_entities:
+  - entity: switch
+    name: Mobile notifications
+    category: config
+    icon: "mdi:message-alert"
+    dps:
+      - id: 6
+        type: boolean
+        name: switch
+  - entity: sensor
+    name: Button
+    category: diagnostic
+    dps:
+      - id: 10
+        type: integer
+        name: sensor
+        optional: true
+        persist: false
+      - id: 5
+        type: base64
+        name: name
+        optional: true
+        persist: false
+  - entity: binary_sensor
+    name: Ringing
+    class: sound
+    category: diagnostic
+    dps:
+      - id: 10
+        type: integer
+        name: sensor
+        optional: true
+        mapping:
+          - dps_val: null
+            value: false
+          - value: true

+ 5 - 0
custom_components/tuya_local/helpers/device_config.py

@@ -314,6 +314,10 @@ class TuyaDpsConfig:
     def optional(self):
         return self._config.get("optional", False)
 
+    @property
+    def persist(self):
+        return self._config.get("persist", True)
+
     @property
     def force(self):
         return self._config.get("force", False)
@@ -508,6 +512,7 @@ class TuyaDpsConfig:
         if mapping:
             _LOGGER.debug("Considering mapping for step of %s", self.name)
             step = mapping.get("step", 1)
+
             cond = self._active_condition(mapping, device)
             if cond:
                 constraint = mapping.get("constraint", self.name)

+ 3 - 3
tests/test_device.py

@@ -1,7 +1,7 @@
 from datetime import datetime
 from time import time
 from unittest import IsolatedAsyncioTestCase
-from unittest.mock import AsyncMock, Mock, call, patch
+from unittest.mock import AsyncMock, Mock, call, patch, ANY
 
 from homeassistant.const import (
     EVENT_HOMEASSISTANT_STARTED,
@@ -573,7 +573,7 @@ class TestDevice(IsolatedAsyncioTestCase):
         self.mock_api().set_socketPersistent.assert_called_once_with(False)
         # Check that a full poll was done
         self.mock_api().status.assert_called_once()
-        self.assertDictEqual(result, {"1": "INIT", "2": 2})
+        self.assertDictEqual(result, {"1": "INIT", "2": 2, "full_poll": ANY})
         # Prepare for next round
         self.subject._cached_state = self.subject._cached_state | result
         self.mock_api().set_socketPersistent.reset_mock()
@@ -588,7 +588,7 @@ class TestDevice(IsolatedAsyncioTestCase):
         self.mock_api().status.assert_not_called()
         self.mock_api().heartbeat.assert_called_once()
         self.mock_api().receive.assert_called_once()
-        self.assertDictEqual(result, {"1": "UPDATED"})
+        self.assertDictEqual(result, {"1": "UPDATED", "full_poll": ANY})
         # Check that the connection was made persistent now that data has been
         # returned
         self.mock_api().set_socketPersistent.assert_called_once_with(True)