Procházet zdrojové kódy

Add support for event platform.

Issue #1068

Following button controllers modified to use the new platform:
- Linkoze dual button
- Loratap Zigbee six switch remote

Deprecated the binary_sensor entities for detecting individual press
types (which have issues detecting close events of the same type due
to having to maintain state.
Jason Rumney před 2 roky
rodič
revize
e8fbe34c3f

+ 2 - 0
custom_components/tuya_local/device.py

@@ -208,6 +208,8 @@ class TuyaLocalDevice(object):
                     self._remove_properties_from_pending_updates(poll)
 
                     for entity in self._children:
+                        # let entities trigger off poll contents directly
+                        entity.on_receive(poll)
                         # clear non-persistant dps that were not in a full poll
                         if full_poll:
                             for dp in entity._config.dps():

+ 41 - 6
custom_components/tuya_local/devices/linkoze_dual_button.yaml

@@ -3,20 +3,50 @@ products:
   - id: l8yaz4um5b3pwyvf
     name: Linkoze LKWSW201
 primary_entity:
-  entity: binary_sensor
-  name: Click 1
+  entity: event
+  name: Button 1
+  class: button
   dps:
     - id: 1
       type: string
-      name: sensor
-      persist: false
+      name: event
       mapping:
         - dps_val: single_click
-          value: true
-        - value: false
+          value: single_click
+        - dps_val: long_press
+          value: long_press
+        - dps_val: double_click
+          value: double_click
 secondary_entities:
+  - entity: event
+    name: Button 2
+    class: button
+    dps:
+      - id: 2
+        type: string
+        name: event
+        mapping:
+          - dps_val: single_click
+            value: single_click
+          - dps_val: long_press
+            value: long_press
+          - dps_val: double_click
+            value: double_click
+  - entity: binary_sensor
+    name: Click 1
+    deprecated: event_button_1
+    dps:
+      - id: 1
+        type: string
+        name: sensor
+        persist: false
+        mapping:
+          - dps_val: single_click
+            value: true
+          - value: false
   - entity: binary_sensor
     name: Click 2
+    deprecated: event_button_2
     dps:
       - id: 2
         type: string
@@ -28,6 +58,7 @@ secondary_entities:
           - value: false
   - entity: binary_sensor
     name: Press 1
+    deprecated: event_button_1
     dps:
       - id: 1
         type: string
@@ -39,6 +70,7 @@ secondary_entities:
           - value: false
   - entity: binary_sensor
     name: Press 2
+    deprecated: event_button_2
     dps:
       - id: 2
         type: string
@@ -50,6 +82,7 @@ secondary_entities:
           - value: false
   - entity: binary_sensor
     name: Double click 1
+    deprecated: event_button_1
     dps:
       - id: 1
         type: string
@@ -61,6 +94,7 @@ secondary_entities:
           - value: false
   - entity: binary_sensor
     name: Double click 2
+    deprecated: event_button_2
     dps:
       - id: 2
         type: string
@@ -78,3 +112,4 @@ secondary_entities:
         type: integer
         name: sensor
         unit: "%"
+        optional: true

+ 116 - 8
custom_components/tuya_local/devices/loratap_zigbee_six_switch.yaml

@@ -1,23 +1,115 @@
 name: Six button remote
-products: 
+products:
   - id: iszegwpd
-    name: 6 gang zigBee remote
+    name: Six gang Zigbee remote
 primary_entity:
-  entity: binary_sensor
-  name: Click 1
+  entity: event
+  name: Button 1
+  class: button
   dps:
     - id: 1
       type: string
-      name: sensor
+      name: event
       optional: true
-      persist: false
       mapping:
         - dps_val: single_click
-          value: true
-        - value: false
+          value: single_click
+        - dps_val: long_press
+          value: long_press
+        - dps_val: double_click
+          value: double_click
 secondary_entities:
+  - entity: event
+    name: Button 2
+    class: button
+    dps:
+      - id: 2
+        type: string
+        name: event
+        optional: true
+        mapping:
+          - dps_val: single_click
+            value: single_click
+          - dps_val: long_press
+            value: long_press
+          - dps_val: double_click
+            value: double_click
+  - entity: event
+    name: Button 3
+    class: button
+    dps:
+      - id: 3
+        type: string
+        name: event
+        optional: true
+        mapping:
+          - dps_val: single_click
+            value: single_click
+          - dps_val: long_press
+            value: long_press
+          - dps_val: double_click
+            value: double_click
+  - entity: event
+    name: Button 4
+    class: button
+    dps:
+      - id: 4
+        type: string
+        name: event
+        optional: true
+        mapping:
+          - dps_val: single_click
+            value: single_click
+          - dps_val: long_press
+            value: long_press
+          - dps_val: double_click
+            value: double_click
+  - entity: event
+    name: Button 5
+    class: button
+    dps:
+      - id: 5
+        type: string
+        name: event
+        optional: true
+        mapping:
+          - dps_val: single_click
+            value: single_click
+          - dps_val: long_press
+            value: long_press
+          - dps_val: double_click
+            value: double_click
+  - entity: event
+    name: Button 6
+    class: button
+    dps:
+      - id: 6
+        type: string
+        name: event
+        optional: true
+        mapping:
+          - dps_val: single_click
+            value: single_click
+          - dps_val: long_press
+            value: long_press
+          - dps_val: double_click
+            value: double_click
+  - entity: binary_sensor
+    name: Click 1
+    deprecated: event_button_1
+    dps:
+      - id: 1
+        type: string
+        name: sensor
+        optional: true
+        persist: false
+        mapping:
+          - dps_val: single_click
+            value: true
+          - value: false
   - entity: binary_sensor
     name: Click 2
+    deprecated: event_button_2
     dps:
       - id: 2
         type: string
@@ -30,6 +122,7 @@ secondary_entities:
           - value: false
   - entity: binary_sensor
     name: Click 3
+    deprecated: event_button_3
     dps:
       - id: 3
         type: string
@@ -42,6 +135,7 @@ secondary_entities:
           - value: false
   - entity: binary_sensor
     name: Click 4
+    deprecated: event_button_4
     dps:
       - id: 4
         type: string
@@ -54,6 +148,7 @@ secondary_entities:
           - value: false
   - entity: binary_sensor
     name: Click 5
+    deprecated: event_button_5
     dps:
       - id: 5
         type: string
@@ -66,6 +161,7 @@ secondary_entities:
           - value: false
   - entity: binary_sensor
     name: Click 6
+    deprecated: event_button_6
     dps:
       - id: 6
         type: string
@@ -79,6 +175,7 @@ secondary_entities:
 
   - entity: binary_sensor
     name: Long click 1
+    deprecated: event_button_1
     dps:
       - id: 1
         type: string
@@ -91,6 +188,7 @@ secondary_entities:
           - value: false
   - entity: binary_sensor
     name: Long click 2
+    deprecated: event_button_2
     dps:
       - id: 2
         type: string
@@ -103,6 +201,7 @@ secondary_entities:
           - value: false
   - entity: binary_sensor
     name: Long click 3
+    deprecated: event_button_3
     dps:
       - id: 3
         type: string
@@ -115,6 +214,7 @@ secondary_entities:
           - value: false
   - entity: binary_sensor
     name: Long click 4
+    deprecated: event_button_4
     dps:
       - id: 4
         type: string
@@ -127,6 +227,7 @@ secondary_entities:
           - value: false
   - entity: binary_sensor
     name: Long click 5
+    deprecated: event_button_5
     dps:
       - id: 5
         type: string
@@ -139,6 +240,7 @@ secondary_entities:
           - value: false
   - entity: binary_sensor
     name: Long click 6
+    deprecated: event_button_6
     dps:
       - id: 6
         type: string
@@ -152,6 +254,7 @@ secondary_entities:
 
   - entity: binary_sensor
     name: Double click 1
+    deprecated: event_button_1
     dps:
       - id: 1
         type: string
@@ -164,6 +267,7 @@ secondary_entities:
           - value: false
   - entity: binary_sensor
     name: Double click 2
+    deprecated: event_button_2
     dps:
       - id: 2
         type: string
@@ -176,6 +280,7 @@ secondary_entities:
           - value: false
   - entity: binary_sensor
     name: Double click 3
+    deprecated: event_button_3
     dps:
       - id: 3
         type: string
@@ -188,6 +293,7 @@ secondary_entities:
           - value: false
   - entity: binary_sensor
     name: Double click 4
+    deprecated: event_button_4
     dps:
       - id: 4
         type: string
@@ -200,6 +306,7 @@ secondary_entities:
           - value: false
   - entity: binary_sensor
     name: Double click 5
+    deprecated: event_button_5
     dps:
       - id: 5
         type: string
@@ -212,6 +319,7 @@ secondary_entities:
           - value: false
   - entity: binary_sensor
     name: Double click 6
+    deprecated: event_button_6
     dps:
       - id: 6
         type: string

+ 58 - 0
custom_components/tuya_local/event.py

@@ -0,0 +1,58 @@
+"""
+Implementation of Tuya events
+"""
+from homeassistant.components.event import EventDeviceClass, EventEntity
+
+from .device import TuyaLocalDevice
+from .helpers.config import async_tuya_setup_platform
+from .helpers.device_config import TuyaEntityConfig
+from .helpers.mixin import TuyaLocalEntity
+
+
+async def async_setup_entry(hass, config_entry, async_add_entities):
+    config = {**config_entry.data, **config_entry.options}
+    await async_tuya_setup_platform(
+        hass,
+        async_add_entities,
+        config,
+        "event",
+        TuyaLocalEvent,
+    )
+
+
+class TuyaLocalEvent(TuyaLocalEntity, EventEntity):
+    """Representation of a Tuya Event"""
+
+    def __init__(self, device: TuyaLocalDevice, config: TuyaEntityConfig):
+        """
+        Initialise the event.
+        Args:
+            device (TuyaLocalDevice): the device API instance.
+            config (TuyaEntityConfig): the configuration for this entity
+        """
+        super().__init__()
+        dps_map = self._init_begin(device, config)
+        self._event_dp = dps_map.pop("event")
+        self._init_end(dps_map)
+
+        # Set up device_class via parent class attribute 
+        try:
+            self._attr_device_class = EventDeviceClass(self._config.device_class)
+        except ValueError:
+            if self._config.device_class:
+                _LOGGER.warning(
+                    "%s/%s: Unreecognised event device class of %s ignored",
+                    self._config._device.config,
+                    self.name or "event",
+                    self._config.device_class,
+                )
+        # Set up event_types via parent class attribute
+        self._attr_event_types = self._event_dp.values(device)
+
+    def on_receive(self, dps):
+        """Trigger the event when dp is received"""
+        if self._event_dp.id in dps:
+            self._trigger_event(
+                self._event_dp.get_value(self._device),
+                self.extra_state_attributes(),
+            )

+ 3 - 0
custom_components/tuya_local/helpers/mixin.py

@@ -105,6 +105,9 @@ class TuyaLocalEntity:
     async def async_will_remove_from_hass(self):
         await self._device.async_unregister_entity(self)
 
+    def on_receive(self, dps):
+        """Override to process dps directly as they are received"""
+        pass
 
 UNIT_ASCII_MAP = {
     "C": UnitOfTemperature.CELSIUS,

+ 91 - 0
tests/test_event.py

@@ -0,0 +1,91 @@
+"""Tests for the event entity."""
+from unittest.mock import AsyncMock, Mock
+
+import pytest
+from pytest_homeassistant_custom_component.common import MockConfigEntry
+
+from custom_components.tuya_local.event import (
+    TuyaLocalEvent,
+    async_setup_entry,
+)
+from custom_components.tuya_local.const import (
+    CONF_DEVICE_ID,
+    CONF_PROTOCOL_VERSION,
+    CONF_TYPE,
+    DOMAIN,
+)
+
+
+@pytest.mark.asyncio
+async def test_init_entry(hass):
+    """Test the initialisation."""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={
+            CONF_TYPE: "linkoze_dual_button",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
+    )
+    m_add_entities = Mock()
+    m_device = AsyncMock()
+
+    hass.data[DOMAIN] = {
+        "dummy": {"device": m_device},
+    }
+
+    await async_setup_entry(hass, entry, m_add_entities)
+    assert (
+        type(hass.data[DOMAIN]["dummy"]["event_button_1"]) == TuyaLocalEvent
+    )
+    m_add_entities.assert_called_once()
+
+
+@pytest.mark.asyncio
+async def test_init_entry_fails_if_device_has_no_event(hass):
+    """Test initialisation when device has no matching entity"""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={
+            CONF_TYPE: "mirabella_genio_usb",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
+    )
+    m_add_entities = Mock()
+    m_device = AsyncMock()
+
+    hass.data[DOMAIN] = {
+        "dummy": {"device": m_device},
+    }
+    try:
+        await async_setup_entry(hass, entry, m_add_entities)
+        assert False
+    except ValueError:
+        pass
+    m_add_entities.assert_not_called()
+
+
+@pytest.mark.asyncio
+async def test_init_entry_fails_if_config_is_missing(hass):
+    """Test initialisation when device has no matching entity"""
+    entry = MockConfigEntry(
+        domain=DOMAIN,
+        data={
+            CONF_TYPE: "non_existing",
+            CONF_DEVICE_ID: "dummy",
+            CONF_PROTOCOL_VERSION: "auto",
+        },
+    )
+    m_add_entities = Mock()
+    m_device = AsyncMock()
+
+    hass.data[DOMAIN] = {
+        "dummy": {"device": m_device},
+    }
+    try:
+        await async_setup_entry(hass, entry, m_add_entities)
+        assert False
+    except ValueError:
+        pass
+    m_add_entities.assert_not_called()