Repository: zigpy/zha-device-handlers
Pull request #3313 (2024-08-23 22:18:21)

Convert Sinope light to new style AttributeDefs, add ZHA events
## Proposed change
Add ability for Sinope light switches to handle routing attribute reports on the manufacturer cluster attribute `action_report` as ZHA events.

## Additional information
Related PR zigpy/zha/pull/153 made it so that `action_report` doesn't need to be manually configured for reporting (as shown by @claudegel here: https://github.com/claudegel/sinope-zha?tab=readme-ov-file#a-create-the-reporting-).

## Checklist

- [x] The changes are tested and work correctly
- [x] `pre-commit` checks pass / the code has been formatted using Black
- [x] Tests have been added to verify that the new code works


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3313?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) Report
Attention: Patch coverage is `74.19355%` with `8 lines` in your changes missing coverage. Please review.
> Project coverage is 88.20%. Comparing base [(`156aaa6`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/commit/156aaa6bb704a13fe9500b833c293405fc5ffd79?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) to head [(`6a92ddd`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/commit/6a92dddc6a62302eadfb8dfcd8c7636c6fc5d1b0?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy).

| [Files](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3313?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) | Patch % | Lines |
|---|---|---|
| [zhaquirks/sinope/light.py](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3313?src=pr&el=tree&filepath=zhaquirks%2Fsinope%2Flight.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy#diff-emhhcXVpcmtzL3Npbm9wZS9saWdodC5weQ==) | 73.33% | [8 Missing :warning: ](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3313?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##              dev    #3313      +/-   ##
==========================================
- Coverage   88.25%   88.20%   -0.05%     
==========================================
  Files         302      302              
  Lines        9434     9463      +29     
==========================================
+ Hits         8326     8347      +21     
- Misses       1108     1116       +8     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3313?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy).

claudegel: So we will need to do the same in sinope thermostat.py, sensor.py and switch.py
Can I help to reformat some of them ?
claudegel: Ok I'm almost finish with the changes for thermostat.py, switch.py and sensor.py.
This is clear for the manufacturer cluster but in those files there are attribute addition in other cluster like Thermostat cluster
```
    attributes = Thermostat.attributes.copy()
    attributes.update(
        {
            0x0400: ("set_occupancy", Occupancy, True),
            0x0401: ("main_cycle_output", CycleOutput, True),
            ...
```
How do you change the AttributeDefs
```
    class AttributeDefs(foundation.BaseAttributeDefs):
        """Sinope Thermostat Custom Cluster Attributes."""

        set_occupancy: Final = foundation.ZCLAttributeDef(
            id=0x0400, type=Occupancy, access="rw", is_manufacturer_specific=True
        )
        ...
        )

        attributes = Thermostat.attributes.copy()
        attributes.update(
        {
        set_occupancy
        ...
        }
```
ckm2k1: @claudegel I don't know for sure, but my guess would be that you can probably inherit from the ZHA thermostat cluster attribute defs, something like this:

```python
class AttributeDefs(Thermostat.AttributeDefs):
 ...
```


Btw, thank you for all the work you've done on supporting Sinope devices in ZHA!
claudegel: Thanks I'll push a PR tomorrow for thermostat, switch and sensor
claudegel: I've tried this but the attribute is not added to IasZone cluster:
```
class SinopeTechnologiesIasZoneCluster(CustomCluster, IasZone):
    """SinopeTechnologiesIasZoneCluster custom cluster."""

    LeakStatus: Final = LeakStatus

    class AttributeDefs(IasZone.AttributeDefs):
        """Sinope Manufacturer IasZone Cluster Attributes."""

    leak_status: Final = foundation.ZCLAttributeDef(
        id=0x0030, type=LeakStatus, access="rw", is_manufacturer_specific=True
    )
```
Any idea ?
claudegel: Was able to fix like this: 
Attribute is added to IasZone cluster but I still have an error
```
    leak_status: Final = foundation.BaseAttributeDefs(
                         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
TypeError: BaseAttributeDefs() takes no arguments
```
```
    class AttributeDefs(IasZone.AttributeDefs):
        """Sinope Manufacturer IasZone Cluster Attributes."""

    leak_status: Final = foundation.BaseAttributeDefs(
        id=0x0030, type=LeakStatus, access="rw", is_manufacturer_specific=True
    )
```
If I replace foundation.BaseAttributeDefs by foundation.ZCLAttributeDef it is not working and atribute leak_status is not added
 I've another strange thing. In the SinopeManufacturerCluster I have some Unknown_attr_1... attributes that I'm still working on . I need them to appear in the cluster but with this new style AttributeDefs they are just skipped. All other named attributes are there but the unknown one are missing. Is it because there is a number in them ?
claudegel: I got it ... a stupid indentation that was not noticed in the log
ready for a PR
ckm2k1: @TheJulianJES - Anything missing in this PR to merge up? 
claudegel: Maybe change line
```
cluster_revision: Final = foundation.ZCLAttributeDef(
            id=0xFFFD, type=t.uint16_t, access="r", is_manufacturer_specific=True
        )
```
by
`cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR`
ckm2k1: > Maybe change line
> 
> ```
> cluster_revision: Final = foundation.ZCLAttributeDef(
>             id=0xFFFD, type=t.uint16_t, access="r", is_manufacturer_specific=True
>         )
> ```
> 
> by ` cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR`

Good point, changed it.
ckm2k1: @TheJulianJES - another bump, anything else needed to merge this? 
Review feedback is addressed, all tests are passing. 
TheJulianJES: There's no need to ping me. It'll be looked at when I (or someone else) has time.
The next zha-quirks release will only be at the end of the month anyway, as they're always made a couple of days before the next major HA beta is released.
claudegel: Look good. Thank you

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index d680730..8b9834f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -17,7 +17,7 @@ repos:
       - id: mypy
   
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.6.4
+    rev: v0.6.7
     hooks:
       - id: ruff
         args: ["--fix", "--exit-non-zero-on-fix", "--config", "pyproject.toml"]
diff --git a/zhaquirks/hzc/__init__.py b/zhaquirks/hzc/__init__.py
new file mode 100644
index 0000000..da8fcd5
--- /dev/null
+++ b/zhaquirks/hzc/__init__.py
@@ -0,0 +1,1 @@
+"""Module for HZC quirks implementations."""
diff --git a/zhaquirks/hzc/dimmerswitch.py b/zhaquirks/hzc/dimmerswitch.py
new file mode 100644
index 0000000..b09fe80
--- /dev/null
+++ b/zhaquirks/hzc/dimmerswitch.py
@@ -0,0 +1,99 @@
+"""Quirk for HZC Dimmer-Switch-ZB3.0 (e.g. D688-ZG)."""
+
+from zigpy.profiles import zgp, zha
+from zigpy.quirks import CustomCluster, CustomDevice
+from zigpy.zcl.clusters.general import (
+    Basic,
+    GreenPowerProxy,
+    Groups,
+    Identify,
+    LevelControl,
+    OnOff,
+    Ota,
+    Scenes,
+)
+from zigpy.zcl.clusters.homeautomation import Diagnostic
+from zigpy.zcl.clusters.lightlink import LightLink
+
+from zhaquirks import NoReplyMixin
+from zhaquirks.const import (
+    DEVICE_TYPE,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    MODELS_INFO,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+)
+
+
+class HzcOnOff(NoReplyMixin, CustomCluster, OnOff):
+    """HZC On Off Cluster."""
+
+    void_input_commands = {cmd.id for cmd in OnOff.commands_by_name.values()}
+
+
+class DimmerSwitch(CustomDevice):
+    """Dimmer-Switch-ZB3.0 by HZC / Shyugj."""
+
+    signature = {
+        MODELS_INFO: [
+            ("HZC", "Dimmer-Switch-ZB3.0"),
+            ("Shyugj", "Dimmer-Switch-ZB3.0"),
+        ],
+        ENDPOINTS: {
+            1: {
+                # <SimpleDescriptor endpoint=1 profile=260 device_type=257
+                # input_clusters=[0, 3, 4, 5, 6, 8, 2821, 4096]
+                # output_clusters=[25]>
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.DIMMABLE_LIGHT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    OnOff.cluster_id,
+                    LevelControl.cluster_id,
+                    Diagnostic.cluster_id,
+                    LightLink.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id],
+            },
+            242: {
+                # <SimpleDescriptor endpoint=242 profile=41440 device_type=97
+                # input_clusters=[]
+                # output_clusters=[33]
+                PROFILE_ID: zgp.PROFILE_ID,
+                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
+                INPUT_CLUSTERS: [],
+                OUTPUT_CLUSTERS: [
+                    GreenPowerProxy.cluster_id,
+                ],
+            },
+        },
+    }
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.DIMMABLE_LIGHT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    HzcOnOff,  # OnOff.cluster_id,
+                    LevelControl.cluster_id,
+                    Diagnostic.cluster_id,
+                    LightLink.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id],
+            },
+            242: {
+                PROFILE_ID: zgp.PROFILE_ID,
+                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
+                INPUT_CLUSTERS: [],
+                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
+            },
+        },
+    }
diff --git a/zhaquirks/ikea/plug.py b/zhaquirks/ikea/plug.py
index 860d60d..f4591de 100644
--- a/zhaquirks/ikea/plug.py
+++ b/zhaquirks/ikea/plug.py
@@ -9,6 +9,7 @@ from zhaquirks.ikea import IKEA
 (
     QuirkBuilder(IKEA, "TRADFRI control outlet")
     .also_applies_to(IKEA, "TRETAKT Smart plug")
+    .also_applies_to(IKEA, "INSPELNING Smart plug")
     .removes(LevelControl.cluster_id)
     .add_to_registry()
 )
diff --git a/zhaquirks/sinope/__init__.py b/zhaquirks/sinope/__init__.py
index 285b283..1a2bffb 100644
--- a/zhaquirks/sinope/__init__.py
+++ b/zhaquirks/sinope/__init__.py
@@ -23,7 +23,7 @@ from zhaquirks.const import (
 
 SINOPE = "Sinope Technologies"
 SINOPE_MANUFACTURER_CLUSTER_ID = 0xFF01
-ATTRIBUTE_ACTION = "actionReport"
+ATTRIBUTE_ACTION = "action_report"
 
 LIGHT_DEVICE_TRIGGERS = {
     (SHORT_PRESS, TURN_ON): {
diff --git a/zhaquirks/sinope/light.py b/zhaquirks/sinope/light.py
index 381fedf..a95d4b3 100644
--- a/zhaquirks/sinope/light.py
+++ b/zhaquirks/sinope/light.py
@@ -4,9 +4,13 @@ Supported devices SW2500ZB, SW2500ZB-G2 dimmer DM2500ZB, DM2500ZB-G2, DM2550ZB,
 DM2550ZB-G2.
 """
 
+import logging
+from typing import Any, Final, Optional, Union
+
 import zigpy.profiles.zha as zha_p
 from zigpy.quirks import CustomCluster, CustomDevice
 import zigpy.types as t
+from zigpy.zcl import foundation
 from zigpy.zcl.clusters.general import (
     Basic,
     DeviceTemperature,
@@ -23,79 +27,194 @@ from zigpy.zcl.clusters.smartenergy import Metering
 
 from zhaquirks import EventableCluster
 from zhaquirks.const import (
+    ATTRIBUTE_ID,
+    ATTRIBUTE_NAME,
+    COMMAND_BUTTON_DOUBLE,
+    COMMAND_BUTTON_HOLD,
     DEVICE_TYPE,
     ENDPOINTS,
     INPUT_CLUSTERS,
     MODELS_INFO,
     OUTPUT_CLUSTERS,
     PROFILE_ID,
+    VALUE,
+    ZHA_SEND_EVENT,
 )
 from zhaquirks.sinope import (
+    ATTRIBUTE_ACTION,
     LIGHT_DEVICE_TRIGGERS,
     SINOPE,
     SINOPE_MANUFACTURER_CLUSTER_ID,
     CustomDeviceTemperatureCluster,
 )
 
+_LOGGER = logging.getLogger(__name__)
+
+
+class KeypadLock(t.enum8):
+    """Keypad_lockout values."""
+
+    Unlocked = 0x00
+    Locked = 0x01
+    Partial_lock = 0x02
+
+
+class PhaseControl(t.enum8):
+    """Phase control value, reverse / forward."""
+
+    Forward = 0x00
+    Reverse = 0x01
+
+
+class DoubleFull(t.enum8):
+    """Double click up set full intensity."""
+
+    Off = 0x00
+    On = 0x01
+
+
+class ButtonAction(t.enum8):
+    """Action_report values."""
+
+    Single_on = 0x01
+    Single_release_on = 0x02
+    Long_on = 0x03
+    Double_on = 0x04
+    Single_off = 0x11
+    Single_release_off = 0x12
+    Long_off = 0x13
+    Double_off = 0x14
+
 
 class SinopeTechnologiesManufacturerCluster(CustomCluster):
     """SinopeTechnologiesManufacturerCluster manufacturer cluster."""
 
-    class KeypadLock(t.enum8):
-        """Keypad_lockout values."""
-
-        Unlocked = 0x00
-        Locked = 0x01
-        Partial_lock = 0x02
-
-    class PhaseControl(t.enum8):
-        """Phase control value, reverse / forward."""
-
-        Forward = 0x00
-        Reverse = 0x01
-
-    class DoubleFull(t.enum8):
-        """Double click up set full intensity."""
-
-        Off = 0x00
-        On = 0x01
-
-    class Action(t.enum8):
-        """Action_report values."""
-
-        Single_on = 0x01
-        Single_release_on = 0x02
-        Long_on = 0x03
-        Double_on = 0x04
-        Single_off = 0x11
-        Single_release_off = 0x12
-        Long_off = 0x13
-        Double_off = 0x14
-
-    cluster_id = SINOPE_MANUFACTURER_CLUSTER_ID
-    name = "Sinopé Technologies Manufacturer specific"
-    ep_attribute = "sinope_manufacturer_specific"
-    attributes = {
-        0x0002: ("keypad_lockout", KeypadLock, True),
-        0x0003: ("firmware_number", t.uint16_t, True),
-        0x0004: ("firmware_version", t.CharacterString, True),
-        0x0010: ("on_intensity", t.int16s, True),
-        0x0050: ("on_led_color", t.uint24_t, True),
-        0x0051: ("off_led_color", t.uint24_t, True),
-        0x0052: ("on_led_intensity", t.uint8_t, True),
-        0x0053: ("off_led_intensity", t.uint8_t, True),
-        0x0054: ("action_report", Action, True),
-        0x0055: ("min_intensity", t.uint16_t, True),
-        0x0056: ("phase_control", PhaseControl, True),
-        0x0058: ("double_up_full", DoubleFull, True),
-        0x0090: ("current_summation_delivered", t.uint32_t, True),
-        0x00A0: ("timer", t.uint32_t, True),
-        0x00A1: ("timer_countdown", t.uint32_t, True),
-        0x0119: ("connected_load", t.uint16_t, True),
-        0x0200: ("status", t.bitmap32, True),
-        0xFFFD: ("cluster_revision", t.uint16_t, True),
+    KeypadLock: Final = KeypadLock
+    PhaseControl: Final = PhaseControl
+    DoubleFull: Final = DoubleFull
+    Action: Final = ButtonAction
+
+    cluster_id: Final[t.uint16_t] = SINOPE_MANUFACTURER_CLUSTER_ID
+    name: Final = "SinopeTechnologiesManufacturerCluster"
+    ep_attribute: Final = "sinope_manufacturer_specific"
+
+    class AttributeDefs(foundation.BaseAttributeDefs):
+        """Sinope Manufacturer Cluster Attributes."""
+
+        keypad_lockout: Final = foundation.ZCLAttributeDef(
+            id=0x0002, type=KeypadLock, access="rw", is_manufacturer_specific=True
+        )
+        firmware_number: Final = foundation.ZCLAttributeDef(
+            id=0x0003, type=t.uint16_t, access="r", is_manufacturer_specific=True
+        )
+        firmware_version: Final = foundation.ZCLAttributeDef(
+            id=0x0004, type=t.CharacterString, access="r", is_manufacturer_specific=True
+        )
+        on_intensity: Final = foundation.ZCLAttributeDef(
+            id=0x0010, type=t.int16s, access="rw", is_manufacturer_specific=True
+        )
+        on_led_color: Final = foundation.ZCLAttributeDef(
+            id=0x0050, type=t.uint24_t, access="rw", is_manufacturer_specific=True
+        )
+        off_led_color: Final = foundation.ZCLAttributeDef(
+            id=0x0051, type=t.uint24_t, access="rw", is_manufacturer_specific=True
+        )
+        on_led_intensity: Final = foundation.ZCLAttributeDef(
+            id=0x0052, type=t.uint8_t, access="rw", is_manufacturer_specific=True
+        )
+        off_led_intensity: Final = foundation.ZCLAttributeDef(
+            id=0x0053, type=t.uint8_t, access="rw", is_manufacturer_specific=True
+        )
+        action_report: Final = foundation.ZCLAttributeDef(
+            id=0x0054, type=ButtonAction, access="rp", is_manufacturer_specific=True
+        )
+        min_intensity: Final = foundation.ZCLAttributeDef(
+            id=0x0055, type=t.uint16_t, access="rw", is_manufacturer_specific=True
+        )
+        phase_control: Final = foundation.ZCLAttributeDef(
+            id=0x0056, type=PhaseControl, access="rw", is_manufacturer_specific=True
+        )
+        double_up_full: Final = foundation.ZCLAttributeDef(
+            id=0x0058, type=DoubleFull, access="rw", is_manufacturer_specific=True
+        )
+        current_summation_delivered: Final = foundation.ZCLAttributeDef(
+            id=0x0090, type=t.uint32_t, access="rp", is_manufacturer_specific=True
+        )
+        timer: Final = foundation.ZCLAttributeDef(
+            id=0x00A0, type=t.uint32_t, access="rw", is_manufacturer_specific=True
+        )
+        timer_countdown: Final = foundation.ZCLAttributeDef(
+            id=0x00A1, type=t.uint32_t, access="r", is_manufacturer_specific=True
+        )
+        connected_load: Final = foundation.ZCLAttributeDef(
+            id=0x0119, type=t.uint16_t, access="rw", is_manufacturer_specific=True
+        )
+        status: Final = foundation.ZCLAttributeDef(
+            id=0x0200, type=t.bitmap32, access="rp", is_manufacturer_specific=True
+        )
+        cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR
+
+    server_commands = {
+        0x54: foundation.ZCLCommandDef(
+            "button_press",
+            {"command": t.uint8_t},
+            direction=foundation.Direction.Server_to_Client,
+            is_manufacturer_specific=True,
+        )
     }
 
+    def handle_cluster_general_request(
+        self,
+        hdr: foundation.ZCLHeader,
+        args: list[Any],
+        *,
+        dst_addressing: Optional[
+            Union[t.Addressing.Group, t.Addressing.IEEE, t.Addressing.NWK]
+        ] = None,
+    ):
+        """Handle the cluster command."""
+        self.debug(
+            "SINOPE cluster general request: hdr: %s - args: [%s]",
+            hdr,
+            args,
+        )
+
+        if hdr.command_id != foundation.GeneralCommand.Report_Attributes:
+            return super().handle_cluster_general_request(
+                hdr, args, dst_addressing=dst_addressing
+            )
+
+        attr = args[0][0]
+
+        if attr.attrid != self.AttributeDefs.action_report.id:
+            return super().handle_cluster_general_request(
+                hdr, args, dst_addressing=dst_addressing
+            )
+
+        value = attr.value.value
+        event_args = {
+            ATTRIBUTE_ID: 84,
+            ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
+            VALUE: value.value,
+        }
+        action = self._get_command_from_action(self.Action(value))
+        if not action:
+            return
+        self.listener_event(ZHA_SEND_EVENT, action, event_args)
+
+    def _get_command_from_action(self, action: ButtonAction) -> str | None:
+        # const lookup = {2: 'up_single', 3: 'up_hold', 4: 'up_double',
+        #             18: 'down_single', 19: 'down_hold', 20: 'down_double'};
+        match action:
+            case self.Action.Single_off | self.Action.Single_on:
+                return None
+            case self.Action.Double_off | self.Action.Double_on:
+                return COMMAND_BUTTON_DOUBLE
+            case self.Action.Long_off | self.Action.Long_on:
+                return COMMAND_BUTTON_HOLD
+            case _:
+                return None
+
 
 class LightManufacturerCluster(EventableCluster, SinopeTechnologiesManufacturerCluster):
     """LightManufacturerCluster: fire events corresponding to press type."""
diff --git a/zhaquirks/sinope/sensor.py b/zhaquirks/sinope/sensor.py
index b28fccf..7fbdf1d 100644
--- a/zhaquirks/sinope/sensor.py
+++ b/zhaquirks/sinope/sensor.py
@@ -4,9 +4,12 @@ It add manufacturer attributes for IasZone cluster for the water leak alarm.
 Supported devices are WL4200, WL4200S and LM4110-ZB
 """
 
+from typing import Final
+
 import zigpy.profiles.zha as zha_p
 from zigpy.quirks import CustomCluster, CustomDevice
 import zigpy.types as t
+from zigpy.zcl import foundation
 from zigpy.zcl.clusters.general import (
     AnalogInput,
     Basic,
@@ -30,39 +33,58 @@ from zhaquirks.const import (
 from zhaquirks.sinope import SINOPE, SINOPE_MANUFACTURER_CLUSTER_ID
 
 
+class LeakStatus(t.enum8):
+    """Leak_status values."""
+
+    Dry = 0x00
+    Leak = 0x01
+
+
 class SinopeManufacturerCluster(CustomCluster):
     """SinopeManufacturerCluster manufacturer cluster."""
 
-    cluster_id = SINOPE_MANUFACTURER_CLUSTER_ID
-    name = "Sinopé Manufacturer specific"
-    ep_attribute = "sinope_manufacturer_specific"
-    attributes = {
-        0x0003: ("firmware_number", t.uint16_t, True),
-        0x0004: ("firmware_version", t.CharacterString, True),
-        0x0032: ("min_temperature_limit", t.int16s, True),
-        0x0033: ("max_temperature_limit", t.int16s, True),
-        0x0034: ("device_status", t.bitmap8, True),
-        0x0036: ("battery_type", t.uint16_t, True),
-        0x0200: ("status", t.bitmap32, True),
-        0xFFFD: ("cluster_revision", t.uint16_t, True),
-    }
+    cluster_id: Final[t.uint16_t] = SINOPE_MANUFACTURER_CLUSTER_ID
+    name: Final = "SinopeManufacturerCluster"
+    ep_attribute: Final = "sinope_manufacturer_specific"
+
+    class AttributeDefs(foundation.BaseAttributeDefs):
+        """Sinope Manufacturer Cluster Attributes."""
+
+        firmware_number: Final = foundation.ZCLAttributeDef(
+            id=0x0003, type=t.uint16_t, access="r", is_manufacturer_specific=True
+        )
+        firmware_version: Final = foundation.ZCLAttributeDef(
+            id=0x0004, type=t.CharacterString, access="r", is_manufacturer_specific=True
+        )
+        min_temperature_limit: Final = foundation.ZCLAttributeDef(
+            id=0x0032, type=t.int16s, access="rw", is_manufacturer_specific=True
+        )
+        max_temperature_limit: Final = foundation.ZCLAttributeDef(
+            id=0x0033, type=t.int16s, access="rw", is_manufacturer_specific=True
+        )
+        device_status: Final = foundation.ZCLAttributeDef(
+            id=0x0034, type=t.bitmap8, access="rp", is_manufacturer_specific=True
+        )
+        battery_type: Final = foundation.ZCLAttributeDef(
+            id=0x0036, type=t.uint16_t, access="rw", is_manufacturer_specific=True
+        )
+        status: Final = foundation.ZCLAttributeDef(
+            id=0x0200, type=t.bitmap32, access="rp", is_manufacturer_specific=True
+        )
+        cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR
 
 
 class SinopeTechnologiesIasZoneCluster(CustomCluster, IasZone):
     """SinopeTechnologiesIasZoneCluster custom cluster."""
 
-    class LeakStatus(t.enum8):
-        """Leak_status values."""
+    LeakStatus: Final = LeakStatus
 
-        Dry = 0x00
-        Leak = 0x01
+    class AttributeDefs(IasZone.AttributeDefs):
+        """Sinope Manufacturer IasZone Cluster Attributes."""
 
-    attributes = IasZone.attributes.copy()
-    attributes.update(
-        {
-            0x0030: ("leak_status", LeakStatus, True),
-        }
-    )
+        leak_status: Final = foundation.ZCLAttributeDef(
+            id=0x0030, type=LeakStatus, access="rw", is_manufacturer_specific=True
+        )
 
 
 class SinopeTechnologiesSensor(CustomDevice):
diff --git a/zhaquirks/sinope/switch.py b/zhaquirks/sinope/switch.py
index ee345b8..e3eada7 100644
--- a/zhaquirks/sinope/switch.py
+++ b/zhaquirks/sinope/switch.py
@@ -5,9 +5,12 @@ VA4200WZ, VA4201WZ, VA4200ZB, VA4201ZB, VA4220ZB, VA4221ZB and MC3100ZB,
 2nd gen VA4220ZB, VA4221ZB with flow meeter FS4220, FS4221.
 """
 
+from typing import Final
+
 import zigpy.profiles.zha as zha_p
 from zigpy.quirks import CustomCluster, CustomDevice
 import zigpy.types as t
+from zigpy.zcl import foundation
 from zigpy.zcl.clusters.general import (
     Basic,
     BinaryInput,
@@ -47,173 +50,259 @@ from zhaquirks.sinope import (
 )
 
 
-class SinopeManufacturerCluster(CustomCluster):
-    """SinopeManufacturerCluster manufacturer cluster."""
+class KeypadLock(t.enum8):
+    """Keypad_lockout values."""
 
-    class KeypadLock(t.enum8):
-        """Keypad_lockout values."""
-
-        Unlocked = 0x00
-        Locked = 0x01
-        Partial_lock = 0x02
-
-    class FlowAlarm(t.enum8):
-        """Abnormal flow alarm."""
-
-        Off = 0x00
-        On = 0x01
-
-    class AlarmAction(t.enum8):
-        """Flow alarm action."""
-
-        Nothing = 0x00
-        Notify = 0x01
-        Close = 0x02
-        Close_notify = 0x03
-        No_flow = 0x04
-
-    class PowerSource(t.uint32_t):
-        """Valve power source types."""
-
-        Battery = 0x00000000
-        ACUPS_01 = 0x00000001
-        DC_power = 0x0001D4C0
-
-    class EmergencyPower(t.uint32_t):
-        """Valve emergency power source types."""
-
-        Battery = 0x00000000
-        ACUPS_01 = 0x00000001
-        Battery_ACUPS_01 = 0x0000003C
-
-    class AbnormalAction(t.bitmap16):
-        """Action in case of abnormal flow detected."""
-
-        Nothing = 0x0000
-        Close_valve = 0x0001
-        Close_notify = 0x0003
-
-    class ColdStatus(t.enum8):
-        """Cold_load_pickup_status values."""
-
-        Active = 0x00
-        Off = 0x01
-
-    class FlowDuration(t.uint32_t):
-        """Abnormal flow duration."""
-
-        M_15 = 0x00000384
-        M_30 = 0x00000708
-        M_45 = 0x00000A8C
-        M_60 = 0x00000E10
-        M_75 = 0x00001194
-        M_90 = 0x00001518
-        H_3 = 0x00002A30
-        H_6 = 0x00005460
-        H_12 = 0x0000A8C0
-        H_24 = 0x00015180
-
-    class InputDelay(t.uint16_t):
-        """Delay for on/off input."""
-
-        Off = 0x0000
-        M_1 = 0x003C
-        M_2 = 0x0078
-        M_5 = 0x012C
-        M_10 = 0x0258
-        M_15 = 0x0384
-        M_30 = 0x0708
-        H_1 = 0x0E10
-        H_2 = 0x1C20
-        H_3 = 0x2A30
-
-    cluster_id = SINOPE_MANUFACTURER_CLUSTER_ID
-    name = "Sinopé Manufacturer specific"
-    ep_attribute = "sinope_manufacturer_specific"
-    attributes = {
-        0x0002: ("keypad_lockout", KeypadLock, True),
-        0x0003: ("firmware_number", t.uint16_t, True),
-        0x0004: ("firmware_version", t.CharacterString, True),
-        0x0010: ("outdoor_temp", t.int16s, True),
-        0x0013: ("unknown_attr_1", t.enum8, True),
-        0x0060: ("connected_load", t.uint16_t, True),
-        0x0070: ("current_load", t.bitmap8, True),
-        0x0076: ("dr_config_water_temp_min", t.uint8_t, True),
-        0x0077: ("dr_config_water_temp_time", t.uint8_t, True),
-        0x0078: ("dr_wt_time_on", t.uint16_t, True),
-        0x007C: ("min_measured_temp", t.int16s, True),
-        0x007D: ("max_measured_temp", t.int16s, True),
-        0x0090: ("current_summation_delivered", t.uint32_t, True),
-        0x00A0: ("timer", t.uint32_t, True),
-        0x00A1: ("timer_countdown", t.uint32_t, True),
-        0x0200: ("status", t.bitmap32, True),
-        0x0230: ("alarm_flow_threshold", FlowAlarm, True),
-        0x0231: ("alarm_options", AlarmAction, True),
-        0x0240: ("flow_meter_config", Array, True),
-        0x0241: ("valve_countdown", t.uint32_t, True),
-        0x0250: ("power_source", PowerSource, True),
-        0x0251: ("emergency_power_source", EmergencyPower, True),
-        0x0252: ("abnormal_flow_duration", FlowDuration, True),
-        0x0253: ("abnormal_flow_action", AbnormalAction, True),
-        0x0280: ("max_measured_value", t.int16s, True),
-        0x0283: ("cold_load_pickup_status", ColdStatus, True),
-        0x0284: ("cold_load_pickup_remaining_time", t.uint16_t, True),
-        0x02A0: ("input_on_delay", InputDelay, True),
-        0x02A1: ("input_off_delay", InputDelay, True),
-        0xFFFD: ("cluster_revision", t.uint16_t, True),
-    }
+    Unlocked = 0x00
+    Locked = 0x01
+    Partial_lock = 0x02
 
 
-class CustomBasicCluster(CustomCluster, Basic):
-    """Custom Basic Cluster."""
+class FlowAlarm(t.enum8):
+    """Abnormal flow alarm."""
 
-    class PowerSource(t.enum8):
-        """Power source."""
+    Off = 0x00
+    On = 0x01
 
-        Unknown = 0x0000
-        DC_mains = 0x0001
-        Battery = 0x0003
-        DC_source = 0x0004
-        ACUPS_01 = 0x0081
-        ACUPS01 = 0x0082
 
-    attributes = Basic.attributes.copy()
-    attributes.update(
-        {
-            0x0007: ("power_source", PowerSource, True),
-        }
-    )
+class AlarmAction(t.enum8):
+    """Flow alarm action."""
+
+    Nothing = 0x00
+    Notify = 0x01
+    Close = 0x02
+    Close_notify = 0x03
+    No_flow = 0x04
+
+
+class PowerSource(t.uint32_t):
+    """Valve power source types."""
+
+    Battery = 0x00000000
+    ACUPS_01 = 0x00000001
+    DC_power = 0x0001D4C0
+
+
+class EmergencyPower(t.uint32_t):
+    """Valve emergency power source types."""
+
+    Battery = 0x00000000
+    ACUPS_01 = 0x00000001
+    Battery_ACUPS_01 = 0x0000003C
+
+
+class AbnormalAction(t.bitmap16):
+    """Action in case of abnormal flow detected."""
+
+    Nothing = 0x0000
+    Close_valve = 0x0001
+    Close_notify = 0x0003
+
+
+class ColdStatus(t.enum8):
+    """Cold_load_pickup_status values."""
 
+    Active = 0x00
+    Off = 0x01
 
-class CustomMeteringCluster(CustomCluster, Metering):
-    """Custom Metering Cluster."""
 
-    class ValveStatus(t.bitmap8):
-        """Valve_status."""
+class FlowDuration(t.uint32_t):
+    """Abnormal flow duration."""
 
-        Off = 0x00
-        Off_armed = 0x01
-        On = 0x02
+    M_15 = 0x00000384
+    M_30 = 0x00000708
+    M_45 = 0x00000A8C
+    M_60 = 0x00000E10
+    M_75 = 0x00001194
+    M_90 = 0x00001518
+    H_3 = 0x00002A30
+    H_6 = 0x00005460
+    H_12 = 0x0000A8C0
+    H_24 = 0x00015180
 
-    class UnitOfMeasure(t.enum8):
-        """Unit_of_measure."""
 
-        KWh = 0x00
-        Lh = 0x07
+class InputDelay(t.uint16_t):
+    """Delay for on/off input."""
+
+    Off = 0x0000
+    M_1 = 0x003C
+    M_2 = 0x0078
+    M_5 = 0x012C
+    M_10 = 0x0258
+    M_15 = 0x0384
+    M_30 = 0x0708
+    H_1 = 0x0E10
+    H_2 = 0x1C20
+    H_3 = 0x2A30
+
+
+class EnergySource(t.enum8):
+    """Power source."""
+
+    Unknown = 0x0000
+    DC_mains = 0x0001
+    Battery = 0x0003
+    DC_source = 0x0004
+    ACUPS_01 = 0x0081
+    ACUPS01 = 0x0082
+
+
+class ValveStatus(t.bitmap8):
+    """Valve_status."""
+
+    Off = 0x00
+    Off_armed = 0x01
+    On = 0x02
+
+
+class UnitOfMeasure(t.enum8):
+    """Unit_of_measure."""
+
+    KWh = 0x00
+    Lh = 0x07
+
+
+class SinopeManufacturerCluster(CustomCluster):
+    """SinopeManufacturerCluster manufacturer cluster."""
+
+    KeypadLock: Final = KeypadLock
+    FlowAlarm: Final = FlowAlarm
+    AlarmAction: Final = AlarmAction
+    PowerSource: Final = PowerSource
+    EmergencyPower: Final = EmergencyPower
+    AbnormalAction: Final = AbnormalAction
+    ColdStatus: Final = ColdStatus
+    FlowDuration: Final = FlowDuration
+    InputDelay: Final = InputDelay
+
+    cluster_id: Final[t.uint16_t] = SINOPE_MANUFACTURER_CLUSTER_ID
+    name: Final = "SinopeManufacturerCluster"
+    ep_attribute: Final = "sinope_manufacturer_specific"
+
+    class AttributeDefs(foundation.BaseAttributeDefs):
+        """Sinope Manufacturer Cluster Attributes."""
+
+        keypad_lockout: Final = foundation.ZCLAttributeDef(
+            id=0x0002, type=KeypadLock, access="rw", is_manufacturer_specific=True
+        )
+        firmware_number: Final = foundation.ZCLAttributeDef(
+            id=0x0003, type=t.uint16_t, access="r", is_manufacturer_specific=True
+        )
+        outdoor_temp: Final = foundation.ZCLAttributeDef(
+            id=0x0010, type=t.int16s, access="rp", is_manufacturer_specific=True
+        )
+        unknown_attr_1: Final = foundation.ZCLAttributeDef(
+            id=0x0013, type=t.enum8, access="rw", is_manufacturer_specific=True
+        )
+        connected_load: Final = foundation.ZCLAttributeDef(
+            id=0x0060, type=t.uint16_t, access="r", is_manufacturer_specific=True
+        )
+        current_load: Final = foundation.ZCLAttributeDef(
+            id=0x0070, type=t.bitmap8, access="r", is_manufacturer_specific=True
+        )
+        dr_config_water_temp_min: Final = foundation.ZCLAttributeDef(
+            id=0x0076, type=t.uint8_t, access="rwp", is_manufacturer_specific=True
+        )
+        dr_config_water_temp_time: Final = foundation.ZCLAttributeDef(
+            id=0x0077, type=t.uint8_t, access="rwp", is_manufacturer_specific=True
+        )
+        dr_wt_time_on: Final = foundation.ZCLAttributeDef(
+            id=0x0078, type=t.uint16_t, access="rwp", is_manufacturer_specific=True
+        )
+        min_measured_temp: Final = foundation.ZCLAttributeDef(
+            id=0x007C, type=t.int16s, access="rp", is_manufacturer_specific=True
+        )
+        max_measured_temp: Final = foundation.ZCLAttributeDef(
+            id=0x007D, type=t.int16s, access="rp", is_manufacturer_specific=True
+        )
+        current_summation_delivered: Final = foundation.ZCLAttributeDef(
+            id=0x0090, type=t.uint32_t, access="rp", is_manufacturer_specific=True
+        )
+        timer: Final = foundation.ZCLAttributeDef(
+            id=0x00A0, type=t.uint32_t, access="rwp", is_manufacturer_specific=True
+        )
+        timer_countdown: Final = foundation.ZCLAttributeDef(
+            id=0x00A1, type=t.uint32_t, access="rp", is_manufacturer_specific=True
+        )
+        status: Final = foundation.ZCLAttributeDef(
+            id=0x0200, type=t.bitmap32, access="rp", is_manufacturer_specific=True
+        )
+        alarm_flow_threshold: Final = foundation.ZCLAttributeDef(
+            id=0x0230, type=FlowAlarm, access="rw", is_manufacturer_specific=True
+        )
+        alarm_options: Final = foundation.ZCLAttributeDef(
+            id=0x0231, type=Array, access="r", is_manufacturer_specific=True
+        )
+        flow_meter_config: Final = foundation.ZCLAttributeDef(
+            id=0x0240, type=AlarmAction, access="rw", is_manufacturer_specific=True
+        )
+        valve_countdown: Final = foundation.ZCLAttributeDef(
+            id=0x0241, type=t.uint32_t, access="rw", is_manufacturer_specific=True
+        )
+        power_source: Final = foundation.ZCLAttributeDef(
+            id=0x0250, type=PowerSource, access="rw", is_manufacturer_specific=True
+        )
+        emergency_power_source: Final = foundation.ZCLAttributeDef(
+            id=0x0251, type=EmergencyPower, access="rw", is_manufacturer_specific=True
+        )
+        abnormal_flow_duration: Final = foundation.ZCLAttributeDef(
+            id=0x0252, type=FlowDuration, access="rw", is_manufacturer_specific=True
+        )
+        abnormal_flow_action: Final = foundation.ZCLAttributeDef(
+            id=0x0253, type=AbnormalAction, access="rw", is_manufacturer_specific=True
+        )
+        max_measured_value: Final = foundation.ZCLAttributeDef(
+            id=0x0280, type=t.int16s, access="rwp", is_manufacturer_specific=True
+        )
+        cold_load_pickup_status: Final = foundation.ZCLAttributeDef(
+            id=0x0283, type=ColdStatus, access="r", is_manufacturer_specific=True
+        )
+        cold_load_pickup_remaining_time: Final = foundation.ZCLAttributeDef(
+            id=0x0284, type=t.uint16_t, access="r", is_manufacturer_specific=True
+        )
+        input_on_delay: Final = foundation.ZCLAttributeDef(
+            id=0x02A0, type=InputDelay, access="rw", is_manufacturer_specific=True
+        )
+        input_off_delay: Final = foundation.ZCLAttributeDef(
+            id=0x02A1, type=InputDelay, access="rw", is_manufacturer_specific=True
+        )
+        cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR
+
+
+class SinopeTechnologiesBasicCluster(CustomCluster, Basic):
+    """SinopetechnologiesBasicCluster custom cluster ."""
+
+    EnergySource: Final = EnergySource
+
+    class AttributeDefs(Basic.AttributeDefs):
+        """Sinope Manufacturer Basic Cluster Attributes."""
+
+        power_source: Final = foundation.ZCLAttributeDef(
+            id=0x0007, type=EnergySource, access="r", is_manufacturer_specific=True
+        )
+
+
+class SinopeTechnologiesMeteringCluster(CustomCluster, Metering):
+    """SinopeTechnologiesMeteringCluster custom cluster."""
+
+    ValveStatus: Final = ValveStatus
+    UnitOfMeasure: Final = UnitOfMeasure
 
     DIVISOR = 0x0302
     _CONSTANT_ATTRIBUTES = {DIVISOR: 1000}
 
-    attributes = Metering.attributes.copy()
-    attributes.update(
-        {
-            0x0200: ("status", ValveStatus, True),
-            0x0300: ("unit_of_measure", UnitOfMeasure, True),
-        }
-    )
+    class AttributeDefs(Metering.AttributeDefs):
+        """Sinope Manufacturer Metering Cluster Attributes."""
+
+        status: Final = foundation.ZCLAttributeDef(
+            id=0x0200, type=ValveStatus, access="r", is_manufacturer_specific=True
+        )
+        unit_of_measure: Final = foundation.ZCLAttributeDef(
+            id=0x0300, type=UnitOfMeasure, access="r", is_manufacturer_specific=True
+        )
 
 
-class CustomFlowMeasurementCluster(CustomCluster, FlowMeasurement):
+class SinopeTechnologiesFlowMeasurementCluster(CustomCluster, FlowMeasurement):
     """Custom flow measurement cluster that divides value by 10."""
 
     def _update_attribute(self, attrid, value):
@@ -258,7 +347,7 @@ class SinopeTechnologiesSwitch(CustomDevice):
                     Basic.cluster_id,
                     Identify.cluster_id,
                     OnOff.cluster_id,
-                    CustomMeteringCluster,
+                    SinopeTechnologiesMeteringCluster,
                     ElectricalMeasurement.cluster_id,
                     SinopeManufacturerCluster,
                 ],
@@ -431,7 +520,7 @@ class SinopeTechnologiesValve(CustomDevice):
         ENDPOINTS: {
             1: {
                 INPUT_CLUSTERS: [
-                    CustomBasicCluster,
+                    SinopeTechnologiesBasicCluster,
                     PowerConfiguration.cluster_id,
                     Identify.cluster_id,
                     Groups.cluster_id,
@@ -493,7 +582,7 @@ class SinopeTechnologiesValveG2(CustomDevice):
         ENDPOINTS: {
             1: {
                 INPUT_CLUSTERS: [
-                    CustomBasicCluster,
+                    SinopeTechnologiesBasicCluster,
                     PowerConfiguration.cluster_id,
                     Identify.cluster_id,
                     Groups.cluster_id,
@@ -501,9 +590,9 @@ class SinopeTechnologiesValveG2(CustomDevice):
                     OnOff.cluster_id,
                     LevelControl.cluster_id,
                     TemperatureMeasurement.cluster_id,
-                    CustomFlowMeasurementCluster,
+                    SinopeTechnologiesFlowMeasurementCluster,
                     IasZone.cluster_id,
-                    CustomMeteringCluster,
+                    SinopeTechnologiesMeteringCluster,
                     Diagnostic.cluster_id,
                     SinopeManufacturerCluster,
                 ],
@@ -707,7 +796,7 @@ class SinopeTechnologiesSwitch_V2(CustomDevice):
                     Basic.cluster_id,
                     Identify.cluster_id,
                     OnOff.cluster_id,
-                    CustomMeteringCluster,
+                    SinopeTechnologiesMeteringCluster,
                     ElectricalMeasurement.cluster_id,
                     LightLink.cluster_id,
                     SinopeManufacturerCluster,
diff --git a/zhaquirks/sinope/thermostat.py b/zhaquirks/sinope/thermostat.py
index eda3e38..401015f 100644
--- a/zhaquirks/sinope/thermostat.py
+++ b/zhaquirks/sinope/thermostat.py
@@ -4,9 +4,12 @@ Manufacturer specific cluster implements attributes to control displaying
 of outdoor temperature, setting occupancy on/off and setting device time.
 """
 
+from typing import Final
+
 import zigpy.profiles.zha as zha_p
 from zigpy.quirks import CustomCluster, CustomDevice
 import zigpy.types as t
+from zigpy.zcl import foundation
 from zigpy.zcl.clusters.general import (
     Basic,
     Groups,
@@ -33,170 +36,284 @@ from zhaquirks.const import (
 from zhaquirks.sinope import SINOPE, SINOPE_MANUFACTURER_CLUSTER_ID
 
 
+class KeypadLock(t.enum8):
+    """Keypad_lockout values."""
+
+    Unlocked = 0x00
+    Locked = 0x01
+    Partial_lock = 0x02
+
+
+class Display(t.enum8):
+    """Config_2nd_display values."""
+
+    Auto = 0x00
+    Setpoint = 0x01
+    Outside_temperature = 0x02
+
+
+class FloorMode(t.enum8):
+    """Air_floor_mode values."""
+
+    Air_by_floor = 0x01
+    Floor = 0x02
+
+
+class AuxMode(t.enum8):
+    """Aux_output_mode values."""
+
+    Off = 0x00
+    On = 0x01
+
+
+class PumpStatus(t.uint8_t):
+    """Pump protection status."""
+
+    Off = 0x00
+    On = 0x01
+
+
+class LimitStatus(t.uint8_t):
+    """Floor limit status values."""
+
+    Ok = 0x00
+    Low_reached = 0x01
+    Max_reached = 0x02
+    Max_air_reached = 0x03
+
+
+class SensorType(t.enum8):
+    """Temp_sensor_type values."""
+
+    Sensor_10k = 0x00
+    Sensor_12k = 0x01
+
+
+class TimeFormat(t.enum8):
+    """Time_format values."""
+
+    Format_24h = 0x00
+    Format_12h = 0x01
+
+
+class GfciStatus(t.enum8):
+    """Gfci_status values."""
+
+    Ok = 0x00
+    Error = 0x01
+
+
+class SystemMode(t.enum8):
+    """System mode values."""
+
+    Off = 0x00
+    Auto = 0x01
+    Cool = 0x03
+    Heat = 0x04
+
+
+class PumpDuration(t.enum8):
+    """Pump protection duration period values."""
+
+    T5 = 0x05
+    T10 = 0x0A
+    T15 = 0x0F
+    T20 = 0x14
+    T30 = 0x1E
+    T60 = 0x3C
+
+
+class CycleLength(t.uint16_t):
+    """Cycle length, 15 sec (15) or 15 min (900 sec)."""
+
+    Sec_15 = 0x000F
+    Min_15 = 0x0384
+
+
+class Occupancy(t.enum8):
+    """Set_occupancy values."""
+
+    Away = 0x00
+    Home = 0x01
+
+
+class Backlight(t.enum8):
+    """Backlight_auto_dim_param values."""
+
+    On_demand = 0x00
+    Always_on = 0x01
+    Bedroom = 0x02
+
+
+class CycleOutput(t.uint16_t):
+    """Main and aux cycle period values."""
+
+    Sec_15 = 0x000F
+    Min_5 = 0x012C
+    Min_10 = 0x0258
+    Min_15 = 0x0384
+    Min_20 = 0x04B0
+    Min_25 = 0x05DC
+    Min_30 = 0x0708
+    Off = 0xFFFF
+
+
 class SinopeTechnologiesManufacturerCluster(CustomCluster):
     """SinopeTechnologiesManufacturerCluster manufacturer cluster."""
 
-    class KeypadLock(t.enum8):
-        """Keypad_lockout values."""
-
-        Unlocked = 0x00
-        Locked = 0x01
-        Partial_lock = 0x02
-
-    class Display(t.enum8):
-        """Config_2nd_display values."""
-
-        Auto = 0x00
-        Setpoint = 0x01
-        Outside_temperature = 0x02
-
-    class FloorMode(t.enum8):
-        """Air_floor_mode values."""
-
-        Air_by_floor = 0x01
-        Floor = 0x02
-
-    class AuxMode(t.enum8):
-        """Aux_output_mode values."""
-
-        Off = 0x00
-        On = 0x01
-
-    class PumpStatus(t.uint8_t):
-        """Pump protection status."""
-
-        Off = 0x00
-        On = 0x01
-
-    class LimitStatus(t.uint8_t):
-        """Floor limit status values."""
-
-        Ok = 0x00
-        Low_reached = 0x01
-        Max_reached = 0x02
-        Max_air_reached = 0x03
-
-    class SensorType(t.enum8):
-        """Temp_sensor_type values."""
-
-        Sensor_10k = 0x00
-        Sensor_12k = 0x01
-
-    class TimeFormat(t.enum8):
-        """Time_format values."""
-
-        Format_24h = 0x00
-        Format_12h = 0x01
-
-    class GfciStatus(t.enum8):
-        """Gfci_status values."""
-
-        Ok = 0x00
-        Error = 0x01
-
-    class SystemMode(t.enum8):
-        """System mode values."""
-
-        Off = 0x00
-        Auto = 0x01
-        Cool = 0x03
-        Heat = 0x04
-
-    class PumpDuration(t.enum8):
-        """Pump protection duration period values."""
-
-        T5 = 0x05
-        T10 = 0x0A
-        T15 = 0x0F
-        T20 = 0x14
-        T30 = 0x1E
-        T60 = 0x3C
-
-    class CycleLength(t.uint16_t):
-        """Cycle length, 15 sec (15) or 15 min (900 sec)."""
-
-        Sec_15 = 0x000F
-        Min_15 = 0x0384
-
-    cluster_id = SINOPE_MANUFACTURER_CLUSTER_ID
-    name = "Sinopé Technologies Manufacturer specific"
-    ep_attribute = "sinope_manufacturer_specific"
-    attributes = {
-        0x0002: ("keypad_lockout", KeypadLock, True),
-        0x0003: ("firmware_number", t.uint16_t, True),
-        0x0004: ("firmware_version", t.CharacterString, True),
-        0x0010: ("outdoor_temp", t.int16s, True),
-        0x0011: ("outdoor_temp_timeout", t.uint16_t, True),
-        0x0012: ("config_2nd_display", Display, True),
-        0x0020: ("secs_since_2k", t.uint32_t, True),
-        0x0070: ("current_load", t.bitmap8, True),
-        0x0071: ("eco_delta_setpoint", t.int8s, True),
-        0x0072: ("eco_max_pi_heating_demand", t.uint8_t, True),
-        0x0073: ("eco_safety_temperature_delta", t.uint8_t, True),
-        0x0101: ("unknown_attr_1", Array, True),
-        0x0104: ("setpoint", t.int16s, True),
-        0x0105: ("air_floor_mode", FloorMode, True),
-        0x0106: ("aux_output_mode", AuxMode, True),
-        0x0107: ("floor_temperature", t.int16s, True),
-        0x0108: ("air_max_limit", t.int16s, True),
-        0x0109: ("floor_min_setpoint", t.int16s, True),
-        0x010A: ("floor_max_setpoint", t.int16s, True),
-        0x010B: ("floor_sensor_type_param", SensorType, True),
-        0x010C: ("floor_limit_status", LimitStatus, True),
-        0x010D: ("room_temperature", t.int16s, True),
-        0x0114: ("time_format", TimeFormat, True),
-        0x0115: ("gfci_status", GfciStatus, True),
-        0x0116: ("hvac_mode", SystemMode, True),
-        0x0118: ("aux_connected_load", t.uint16_t, True),
-        0x0119: ("connected_load", t.uint16_t, True),
-        0x0128: ("pump_protection_status", PumpStatus, True),
-        0x012A: ("pump_protection_duration", PumpDuration, True),
-        0x012B: ("current_setpoint", t.int16s, True),
-        0x012D: ("report_local_temperature", t.int16s, True),
-        0x0200: ("status", t.bitmap32, True),
-        0x0281: ("cycle_length", CycleLength, True),
-        0xFFFD: ("cluster_revision", t.uint16_t, True),
-    }
+    KeypadLock: Final = KeypadLock
+    Display: Final = Display
+    FloorMode: Final = FloorMode
+    AuxMode: Final = AuxMode
+    PumpStatus: Final = PumpStatus
+    LimitStatus: Final = LimitStatus
+    SensorType: Final = SensorType
+    TimeFormat: Final = TimeFormat
+    GfciStatus: Final = GfciStatus
+    SystemMode: Final = SystemMode
+    PumpDuration: Final = PumpDuration
+    CycleLength: Final = CycleLength
+
+    cluster_id: Final[t.uint16_t] = SINOPE_MANUFACTURER_CLUSTER_ID
+    name: Final = "SinopeTechnologiesManufacturerCluster"
+    ep_attribute: Final = "sinope_manufacturer_specific"
+
+    class AttributeDefs(foundation.BaseAttributeDefs):
+        """Sinope Manufacturer Cluster Attributes."""
+
+        keypad_lockout: Final = foundation.ZCLAttributeDef(
+            id=0x0002, type=KeypadLock, access="rwp", is_manufacturer_specific=True
+        )
+        firmware_number: Final = foundation.ZCLAttributeDef(
+            id=0x0003, type=t.uint16_t, access="rp", is_manufacturer_specific=True
+        )
+        firmware_version: Final = foundation.ZCLAttributeDef(
+            id=0x0004,
+            type=t.CharacterString,
+            access="rp",
+            is_manufacturer_specific=True,
+        )
+        outdoor_temp: Final = foundation.ZCLAttributeDef(
+            id=0x0010, type=t.int16s, access="rwp", is_manufacturer_specific=True
+        )
+        outdoor_temp_timeout: Final = foundation.ZCLAttributeDef(
+            id=0x0011, type=t.uint16_t, access="rwp", is_manufacturer_specific=True
+        )
+        config_2nd_display: Final = foundation.ZCLAttributeDef(
+            id=0x0012, type=Display, access="rwp", is_manufacturer_specific=True
+        )
+        secs_since_2k: Final = foundation.ZCLAttributeDef(
+            id=0x0020, type=t.uint32_t, access="rwp", is_manufacturer_specific=True
+        )
+        current_load: Final = foundation.ZCLAttributeDef(
+            id=0x0070, type=t.bitmap8, access="rp", is_manufacturer_specific=True
+        )
+        eco_delta_setpoint: Final = foundation.ZCLAttributeDef(
+            id=0x0071, type=t.int8s, access="rwp", is_manufacturer_specific=True
+        )
+        eco_max_pi_heating_demand: Final = foundation.ZCLAttributeDef(
+            id=0x0072, type=t.uint8_t, access="rwp", is_manufacturer_specific=True
+        )
+        eco_safety_temperature_delta: Final = foundation.ZCLAttributeDef(
+            id=0x0073, type=t.uint8_t, access="rwp", is_manufacturer_specific=True
+        )
+        unknown_attr_1: Final = foundation.ZCLAttributeDef(
+            id=0x0101, type=Array, access="r", is_manufacturer_specific=True
+        )
+        setpoint: Final = foundation.ZCLAttributeDef(
+            id=0x0104, type=t.int16s, access="rw", is_manufacturer_specific=True
+        )
+        air_floor_mode: Final = foundation.ZCLAttributeDef(
+            id=0x0105, type=FloorMode, access="rw", is_manufacturer_specific=True
+        )
+        aux_output_mode: Final = foundation.ZCLAttributeDef(
+            id=0x0106, type=AuxMode, access="rw", is_manufacturer_specific=True
+        )
+        floor_temperature: Final = foundation.ZCLAttributeDef(
+            id=0x0107, type=t.int16s, access="r", is_manufacturer_specific=True
+        )
+        air_max_limit: Final = foundation.ZCLAttributeDef(
+            id=0x0108, type=t.int16s, access="rw", is_manufacturer_specific=True
+        )
+        floor_min_setpoint: Final = foundation.ZCLAttributeDef(
+            id=0x0109, type=t.int16s, access="rw", is_manufacturer_specific=True
+        )
+        floor_max_setpoint: Final = foundation.ZCLAttributeDef(
+            id=0x010A, type=t.int16s, access="rw", is_manufacturer_specific=True
+        )
+        floor_sensor_type_param: Final = foundation.ZCLAttributeDef(
+            id=0x010B, type=SensorType, access="rw", is_manufacturer_specific=True
+        )
+        floor_limit_status: Final = foundation.ZCLAttributeDef(
+            id=0x010C, type=LimitStatus, access="rp", is_manufacturer_specific=True
+        )
+        room_temperature: Final = foundation.ZCLAttributeDef(
+            id=0x010D, type=t.int16s, access="r", is_manufacturer_specific=True
+        )
+        time_format: Final = foundation.ZCLAttributeDef(
+            id=0x0114, type=TimeFormat, access="rwp", is_manufacturer_specific=True
+        )
+        gfci_status: Final = foundation.ZCLAttributeDef(
+            id=0x0115, type=GfciStatus, access="rp", is_manufacturer_specific=True
+        )
+        hvac_mode: Final = foundation.ZCLAttributeDef(
+            id=0x0116, type=SystemMode, access="r", is_manufacturer_specific=True
+        )
+        aux_connected_load: Final = foundation.ZCLAttributeDef(
+            id=0x0118, type=t.uint16_t, access="rw", is_manufacturer_specific=True
+        )
+        connected_load: Final = foundation.ZCLAttributeDef(
+            id=0x0119, type=t.uint16_t, access="rw", is_manufacturer_specific=True
+        )
+        pump_protection_status: Final = foundation.ZCLAttributeDef(
+            id=0x0128, type=PumpStatus, access="rw", is_manufacturer_specific=True
+        )
+        pump_protection_duration: Final = foundation.ZCLAttributeDef(
+            id=0x012A, type=PumpDuration, access="rwp", is_manufacturer_specific=True
+        )
+        current_setpoint: Final = foundation.ZCLAttributeDef(
+            id=0x012B, type=t.int16s, access="rwp", is_manufacturer_specific=True
+        )
+        report_local_temperature: Final = foundation.ZCLAttributeDef(
+            id=0x012D, type=t.int16s, access="rp", is_manufacturer_specific=True
+        )
+        unknown_attr_11: Final = foundation.ZCLAttributeDef(
+            id=0x013B, type=t.bitmap8, access="rp", is_manufacturer_specific=True
+        )
+        status: Final = foundation.ZCLAttributeDef(
+            id=0x0200, type=t.bitmap32, access="rp", is_manufacturer_specific=True
+        )
+        unknown_attr_12: Final = foundation.ZCLAttributeDef(
+            id=0x0202, type=t.enum8, access="r", is_manufacturer_specific=True
+        )
+        cycle_length: Final = foundation.ZCLAttributeDef(
+            id=0x0281, type=CycleLength, access="rwp", is_manufacturer_specific=True
+        )
+        cluster_revision: Final = foundation.ZCL_CLUSTER_REVISION_ATTR
 
 
 class SinopeTechnologiesThermostatCluster(CustomCluster, Thermostat):
     """SinopeTechnologiesThermostatCluster custom cluster."""
 
-    class Occupancy(t.enum8):
-        """Set_occupancy values."""
-
-        Away = 0x00
-        Home = 0x01
-
-    class Backlight(t.enum8):
-        """Backlight_auto_dim_param values."""
-
-        On_demand = 0x00
-        Always_on = 0x01
-        Bedroom = 0x02
-
-    class CycleOutput(t.uint16_t):
-        """Main and aux cycle period values."""
-
-        Sec_15 = 0x000F
-        Min_5 = 0x012C
-        Min_10 = 0x0258
-        Min_15 = 0x0384
-        Min_20 = 0x04B0
-        Min_25 = 0x05DC
-        Min_30 = 0x0708
-        Off = 0xFFFF
-
-    attributes = Thermostat.attributes.copy()
-    attributes.update(
-        {
-            0x0400: ("set_occupancy", Occupancy, True),
-            0x0401: ("main_cycle_output", CycleOutput, True),
-            0x0402: ("backlight_auto_dim_param", Backlight, True),
-            0x0404: ("aux_cycle_output", CycleOutput, True),
-            0xFFFD: ("cluster_revision", t.uint16_t, True),
-        }
-    )
+    Occupancy: Final = Occupancy
+    Backlight: Final = Backlight
+    CycleOutput: Final = CycleOutput
+
+    class AttributeDefs(Thermostat.AttributeDefs):
+        """Sinope Manufacturer Thermostat Cluster Attributes."""
+
+        set_occupancy: Final = foundation.ZCLAttributeDef(
+            id=0x0400, type=Occupancy, access="rw", is_manufacturer_specific=True
+        )
+        main_cycle_output: Final = foundation.ZCLAttributeDef(
+            id=0x0401, type=CycleOutput, access="rw", is_manufacturer_specific=True
+        )
+        backlight_auto_dim_param: Final = foundation.ZCLAttributeDef(
+            id=0x0402, type=Backlight, access="rw", is_manufacturer_specific=True
+        )
+        aux_cycle_output: Final = foundation.ZCLAttributeDef(
+            id=0x0404, type=CycleOutput, access="rw", is_manufacturer_specific=True
+        )
 
 
 class SinopeTechnologiesElectricalMeasurementCluster(
@@ -204,14 +321,18 @@ class SinopeTechnologiesElectricalMeasurementCluster(
 ):
     """SinopeTechnologiesElectricalMeasurementCluster custom cluster."""
 
-    attributes = ElectricalMeasurement.attributes.copy()
-    attributes.update(
-        {
-            0x0551: ("current_summation_delivered", t.uint32_t, True),
-            0x0552: ("aux_setpoint_min", t.uint32_t, True),
-            0x0553: ("aux_setpoint_max", t.uint32_t, True),
-        }
-    )
+    class AttributeDefs(ElectricalMeasurement.AttributeDefs):
+        """Sinope Manufacturer ElectricalMeasurement Cluster Attributes."""
+
+        current_summation_delivered: Final = foundation.ZCLAttributeDef(
+            id=0x0551, type=t.uint32_t, access="rwp", is_manufacturer_specific=True
+        )
+        aux_setpoint_min: Final = foundation.ZCLAttributeDef(
+            id=0x0552, type=t.uint32_t, access="rw", is_manufacturer_specific=True
+        )
+        aux_setpoint_max: Final = foundation.ZCLAttributeDef(
+            id=0x0553, type=t.uint32_t, access="rw", is_manufacturer_specific=True
+        )
 
 
 class SinopeTechnologiesThermostat(CustomDevice):
diff --git a/zhaquirks/tuya/__init__.py b/zhaquirks/tuya/__init__.py
index c522bde..9683512 100644
--- a/zhaquirks/tuya/__init__.py
+++ b/zhaquirks/tuya/__init__.py
@@ -33,6 +33,7 @@ TUYA_CLUSTER_ID = 0xEF00
 TUYA_CLUSTER_E000_ID = 0xE000
 TUYA_CLUSTER_E001_ID = 0xE001
 TUYA_CLUSTER_1888_ID = 0x1888
+TUYA_CLUSTER_ED00_ID = 0xED00
 # ---------------------------------------------------------
 # Tuya Cluster Commands
 # ---------------------------------------------------------
diff --git a/zhaquirks/tuya/ts0042.py b/zhaquirks/tuya/ts0042.py
index 3ea0f8c..b783e0e 100644
--- a/zhaquirks/tuya/ts0042.py
+++ b/zhaquirks/tuya/ts0042.py
@@ -237,3 +237,67 @@ class TuyaSmartRemote0042TOPlusA(CustomDevice):
         (LONG_PRESS, BUTTON_2): {ENDPOINT_ID: 2, COMMAND: LONG_PRESS},
         (DOUBLE_PRESS, BUTTON_2): {ENDPOINT_ID: 2, COMMAND: DOUBLE_PRESS},
     }
+
+
+class TuyaSmartRemote0042NO(CustomDevice):
+    """Tuya 2-button remote device with no time cluster."""
+
+    signature = {
+        # SizePrefixedSimpleDescriptor(endpoint=1, profile=260, device_type=0, device_version=1, input_clusters=[0, 1, 6], output_clusters=[25]))
+        # SizePrefixedSimpleDescriptor(endpoint=2, profile=260, device_type=0, device_version=1, input_clusters=[0, 1, 6], output_clusters=[25])
+        MODEL: "TS0042",
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    OnOff.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id],
+            },
+            2: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    OnOff.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id],
+            },
+        },
+    }
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    TuyaNoBindPowerConfigurationCluster,
+                    TuyaSmartRemoteOnOffCluster,
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id],
+            },
+            2: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL,
+                INPUT_CLUSTERS: [
+                    TuyaNoBindPowerConfigurationCluster,
+                    TuyaSmartRemoteOnOffCluster,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+        },
+    }
+
+    device_automation_triggers = {
+        (SHORT_PRESS, BUTTON_1): {ENDPOINT_ID: 1, COMMAND: SHORT_PRESS},
+        (LONG_PRESS, BUTTON_1): {ENDPOINT_ID: 1, COMMAND: LONG_PRESS},
+        (DOUBLE_PRESS, BUTTON_1): {ENDPOINT_ID: 1, COMMAND: DOUBLE_PRESS},
+        (SHORT_PRESS, BUTTON_2): {ENDPOINT_ID: 2, COMMAND: SHORT_PRESS},
+        (LONG_PRESS, BUTTON_2): {ENDPOINT_ID: 2, COMMAND: LONG_PRESS},
+        (DOUBLE_PRESS, BUTTON_2): {ENDPOINT_ID: 2, COMMAND: DOUBLE_PRESS},
+    }
diff --git a/zhaquirks/tuya/ts0043.py b/zhaquirks/tuya/ts0043.py
index aae0760..eec0caa 100644
--- a/zhaquirks/tuya/ts0043.py
+++ b/zhaquirks/tuya/ts0043.py
@@ -378,3 +378,90 @@ class TuyaSmartRemote0043TOPlusB(CustomDevice):
         (LONG_PRESS, BUTTON_3): {ENDPOINT_ID: 3, COMMAND: LONG_PRESS},
         (DOUBLE_PRESS, BUTTON_3): {ENDPOINT_ID: 3, COMMAND: DOUBLE_PRESS},
     }
+
+
+class TuyaSmartRemote0043NO(CustomDevice):
+    """Tuya 3-button remote device with no time cluster."""
+
+    signature = {
+        # SizePrefixedSimpleDescriptor(endpoint=1, profile=260, device_type=0, device_version=1, input_clusters=[0, 1, 6], output_clusters=[25]))
+        # SizePrefixedSimpleDescriptor(endpoint=2, profile=260, device_type=0, device_version=1, input_clusters=[0, 1, 6], output_clusters=[25])
+        # SizePrefixedSimpleDescriptor(endpoint=3, profile=260, device_type=0, device_version=1, input_clusters=[0, 1, 6], output_clusters=[25])
+        MODEL: "TS0043",
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    OnOff.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id],
+            },
+            2: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    OnOff.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id],
+            },
+            3: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    OnOff.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id],
+            },
+        },
+    }
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    TuyaNoBindPowerConfigurationCluster,
+                    TuyaSmartRemoteOnOffCluster,
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id],
+            },
+            2: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL,
+                INPUT_CLUSTERS: [
+                    TuyaNoBindPowerConfigurationCluster,
+                    TuyaSmartRemoteOnOffCluster,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            3: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL,
+                INPUT_CLUSTERS: [
+                    TuyaNoBindPowerConfigurationCluster,
+                    TuyaSmartRemoteOnOffCluster,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+        },
+    }
+
+    device_automation_triggers = {
+        (SHORT_PRESS, BUTTON_1): {ENDPOINT_ID: 1, COMMAND: SHORT_PRESS},
+        (LONG_PRESS, BUTTON_1): {ENDPOINT_ID: 1, COMMAND: LONG_PRESS},
+        (DOUBLE_PRESS, BUTTON_1): {ENDPOINT_ID: 1, COMMAND: DOUBLE_PRESS},
+        (SHORT_PRESS, BUTTON_2): {ENDPOINT_ID: 2, COMMAND: SHORT_PRESS},
+        (LONG_PRESS, BUTTON_2): {ENDPOINT_ID: 2, COMMAND: LONG_PRESS},
+        (DOUBLE_PRESS, BUTTON_2): {ENDPOINT_ID: 2, COMMAND: DOUBLE_PRESS},
+        (SHORT_PRESS, BUTTON_3): {ENDPOINT_ID: 3, COMMAND: SHORT_PRESS},
+        (LONG_PRESS, BUTTON_3): {ENDPOINT_ID: 3, COMMAND: LONG_PRESS},
+        (DOUBLE_PRESS, BUTTON_3): {ENDPOINT_ID: 3, COMMAND: DOUBLE_PRESS},
+    }
diff --git a/zhaquirks/tuya/ts0601_sensor.py b/zhaquirks/tuya/ts0601_sensor.py
index cc0190e..86dfdb5 100644
--- a/zhaquirks/tuya/ts0601_sensor.py
+++ b/zhaquirks/tuya/ts0601_sensor.py
@@ -297,6 +297,7 @@ class TuyaTempHumiditySensorVar04(CustomDevice):
             ("_TZE200_utkemkbs", "TS0601"),
             ("_TZE204_utkemkbs", "TS0601"),
             ("_TZE204_yjjdcqsq", "TS0601"),
+            ("_TZE204_ksz749x8", "TS0601"),
         ],
         ENDPOINTS: {
             1: {
diff --git a/zhaquirks/tuya/ts0601_siren.py b/zhaquirks/tuya/ts0601_siren.py
index 5bc321b..4ce637f 100644
--- a/zhaquirks/tuya/ts0601_siren.py
+++ b/zhaquirks/tuya/ts0601_siren.py
@@ -356,6 +356,7 @@ class TuyaSirenGPP_NoSensors(CustomDevice):
         MODELS_INFO: [
             ("_TZE200_t1blo2bj", "TS0601"),
             ("_TZE204_t1blo2bj", "TS0601"),
+            ("_TZE204_hcxvyxa5", "TS0601"),
         ],
         ENDPOINTS: {
             1: {
diff --git a/zhaquirks/tuya/ts0601_switch.py b/zhaquirks/tuya/ts0601_switch.py
index 727c466..7ecf639 100644
--- a/zhaquirks/tuya/ts0601_switch.py
+++ b/zhaquirks/tuya/ts0601_switch.py
@@ -127,8 +127,10 @@ class TuyaSingleSwitch_GP(TuyaSwitch):
 
     signature = {
         MODELS_INFO: [
+            ("_TZE200_d0ypnbvn", "TS0601"),  # reported in #1335
             ("_TZE200_gbagoilo", "TS0601"),  # reported in #1634
             ("_TZE204_6fk3gewc", "TS0601"),
+            ("_TZE204_d0ypnbvn", "TS0601"),  # reported in #3220 & #3291
             ("_TZE204_ptaqh9tk", "TS0601"),  # reported in #2780
         ],
         ENDPOINTS: {
diff --git a/zhaquirks/tuya/ts0601_valve.py b/zhaquirks/tuya/ts0601_valve.py
index 311c3b5..fbb29a0 100644
--- a/zhaquirks/tuya/ts0601_valve.py
+++ b/zhaquirks/tuya/ts0601_valve.py
@@ -16,7 +16,7 @@ from zhaquirks.const import (
     OUTPUT_CLUSTERS,
     PROFILE_ID,
 )
-from zhaquirks.tuya import EnchantedDevice, TuyaLocalCluster
+from zhaquirks.tuya import TUYA_CLUSTER_ED00_ID, EnchantedDevice, TuyaLocalCluster
 from zhaquirks.tuya.mcu import (
     DPToAttributeMapping,
     TuyaMCUCluster,
@@ -422,3 +422,48 @@ class GiexValve(CustomDevice):
             }
         }
     }
+
+
+class GiexValveVar02(CustomDevice):
+    """GiEX valve device, variant 2."""
+
+    signature = {
+        MODELS_INFO: [
+            ("_TZE284_7ytb3h8u", "TS0601"),
+        ],
+        ENDPOINTS: {
+            # <SimpleDescriptor endpoint=1 profile=260 device_type=0x0051
+            # input_clusters=[0x0000, 0x0004, 0x0005, 0xed00, 0xef00]
+            # output_clusters=[0x000a, 0x0019]>
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TUYA_CLUSTER_ED00_ID,
+                    GiexValveManufCluster.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            }
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaOnOffNM,
+                    TuyaPowerConfigurationCluster,
+                    TuyaValveWaterConsumed,
+                    GiexValveManufCluster,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            }
+        }
+    }
diff --git a/zhaquirks/tuya/ts1201.py b/zhaquirks/tuya/ts1201.py
index 0fed401..cdf9852 100644
--- a/zhaquirks/tuya/ts1201.py
+++ b/zhaquirks/tuya/ts1201.py
@@ -506,6 +506,7 @@ class ZosungIRBlaster_ZS06(ZosungIRBlaster):
             ("_TZ3290_7v1k4vufotpowp9z", "TS1201"),
             ("_TZ3290_acv1iuslxi3shaaj", "TS1201"),
             ("_TZ3290_gnl5a6a5xvql7c2a", "TS1201"),
+            ("_TZ3290_rlkmy85q4pzoxobl", "TS1201"),
         ],
         ENDPOINTS: {
             1: {
diff --git a/zhaquirks/tuya/ts130f.py b/zhaquirks/tuya/ts130f.py
index 9364ae8..06a2ed3 100644
--- a/zhaquirks/tuya/ts130f.py
+++ b/zhaquirks/tuya/ts130f.py
@@ -164,6 +164,52 @@ class TuyaZemismartTS130F(CustomDevice):
     }
 
 
+class TuyaZemismartWNEC1ETS130F(CustomDevice):
+    """Tuya ZemiSmart smart curtain roller shutter. WN-EC1E version."""
+
+    signature = {
+        MODEL: "TS130F",
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    OnOff.cluster_id,
+                    WindowCovering.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Time.cluster_id,
+                    Ota.cluster_id,
+                ],
+            },
+        },
+    }
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaWithBacklightOnOffCluster,
+                    TuyaCoveringCluster,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Time.cluster_id,
+                    Ota.cluster_id,
+                ],
+            },
+        },
+    }
+
+
 class TuyaTS130FTOGP(CustomDevice):
     """Tuya Oxt smart curtain roller shutter."""
 


Test patch:
diff --git a/tests/test_sinope.py b/tests/test_sinope.py
index 1cba912..0680d45 100644
--- a/tests/test_sinope.py
+++ b/tests/test_sinope.py
@@ -1,16 +1,32 @@
 """Tests for Sinope."""
 
+from unittest import mock
+
 import pytest
+from zigpy.device import Device
+import zigpy.types as t
+from zigpy.zcl import foundation
 from zigpy.zcl.clusters.general import DeviceTemperature
 from zigpy.zcl.clusters.measurement import FlowMeasurement
 
 from tests.common import ClusterListener
-import zhaquirks.sinope.switch
+import zhaquirks
+from zhaquirks.const import COMMAND_BUTTON_DOUBLE, COMMAND_BUTTON_HOLD
+from zhaquirks.sinope import SINOPE_MANUFACTURER_CLUSTER_ID
+from zhaquirks.sinope.light import (
+    SinopeTechnologieslight,
+    SinopeTechnologiesManufacturerCluster,
+)
+from zhaquirks.sinope.switch import SinopeTechnologiesCalypso, SinopeTechnologiesValveG2
 
 zhaquirks.setup()
 
+ButtonAction = SinopeTechnologiesManufacturerCluster.Action
+
+SINOPE_MANUFACTURER_ID = 4508  # 0x119C
 
-@pytest.mark.parametrize("quirk", (zhaquirks.sinope.switch.SinopeTechnologiesCalypso,))
+
+@pytest.mark.parametrize("quirk", (SinopeTechnologiesCalypso,))
 async def test_sinope_device_temp(zigpy_device_from_quirk, quirk):
     """Test that device temperature is multiplied."""
     device = zigpy_device_from_quirk(quirk)
@@ -33,7 +49,7 @@ async def test_sinope_device_temp(zigpy_device_from_quirk, quirk):
     assert dev_temp_listener.attribute_updates[1][1] == 25  # not modified
 
 
-@pytest.mark.parametrize("quirk", (zhaquirks.sinope.switch.SinopeTechnologiesValveG2,))
+@pytest.mark.parametrize("quirk", (SinopeTechnologiesValveG2,))
 async def test_sinope_flow_measurement(zigpy_device_from_quirk, quirk):
     """Test that flow measurement measured value is divided."""
     device = zigpy_device_from_quirk(quirk)
@@ -57,3 +73,133 @@ async def test_sinope_flow_measurement(zigpy_device_from_quirk, quirk):
         == flow_measurement_other_attr_id
     )
     assert flow_measurement_listener.attribute_updates[1][1] == 25  # not modified
+
+
+def _get_packet_data(
+    command: foundation.GeneralCommand,
+    attr: foundation.Attribute | None = None,
+    dirc: foundation.Direction = foundation.Direction.Server_to_Client,
+) -> bytes:
+    hdr = foundation.ZCLHeader.general(
+        1, command, SINOPE_MANUFACTURER_ID, dirc
+    ).serialize()
+    if attr is not None:
+        cmd = foundation.GENERAL_COMMANDS[command].schema([attr]).serialize()
+    else:
+        cmd = b""
+    return t.SerializableBytes(hdr + cmd).serialize()
+
+
+@pytest.mark.parametrize("quirk", (SinopeTechnologieslight,))
+@pytest.mark.parametrize(
+    "press_type,exp_event",
+    (
+        (ButtonAction.Single_off, None),
+        (ButtonAction.Single_on, None),
+        (ButtonAction.Double_on, COMMAND_BUTTON_DOUBLE),
+        (ButtonAction.Double_off, COMMAND_BUTTON_DOUBLE),
+        (ButtonAction.Long_on, COMMAND_BUTTON_HOLD),
+        (ButtonAction.Long_off, COMMAND_BUTTON_HOLD),
+        # Should gracefully handle broken actions.
+        (t.uint8_t(0x00), None),
+    ),
+)
+async def test_sinope_light_switch(
+    zigpy_device_from_quirk, quirk, press_type, exp_event
+):
+    """Test that button presses are sent as events."""
+    device: Device = zigpy_device_from_quirk(quirk)
+    cluster_id = SINOPE_MANUFACTURER_CLUSTER_ID
+    endpoint_id = 1
+
+    class Listener:
+        zha_send_event = mock.MagicMock()
+
+    cluster_listener = Listener()
+    device.endpoints[endpoint_id].in_clusters[cluster_id].add_listener(cluster_listener)
+
+    attr = foundation.Attribute(
+        attrid=0x54,  # "action_report" attribute
+        value=foundation.TypeValue(
+            type=t.enum8(0x30),
+            value=press_type,
+        ),
+    )
+    data = _get_packet_data(foundation.GeneralCommand.Report_Attributes, attr)
+    device.handle_message(260, cluster_id, endpoint_id, endpoint_id, data)
+
+    if exp_event is None:
+        assert cluster_listener.zha_send_event.call_count == 0
+    else:
+        assert cluster_listener.zha_send_event.call_count == 1
+        assert cluster_listener.zha_send_event.call_args == mock.call(
+            exp_event,
+            {
+                "attribute_id": 84,
+                "attribute_name": "action_report",
+                "value": press_type.value,
+            },
+        )
+
+
+@pytest.mark.parametrize("quirk", (SinopeTechnologieslight,))
+async def test_sinope_light_switch_non_action_report(zigpy_device_from_quirk, quirk):
+    """Test commands not handled by custom handler.
+
+    Make sure that non attribute report commands and attribute reports that don't
+    concern action_report are passed through to base class.
+    """
+
+    device: Device = zigpy_device_from_quirk(quirk)
+    cluster_id = SINOPE_MANUFACTURER_CLUSTER_ID
+    endpoint_id = 1
+
+    class Listener:
+        zha_send_event = mock.MagicMock()
+
+    cluster_listener = Listener()
+    device.endpoints[endpoint_id].in_clusters[cluster_id].add_listener(cluster_listener)
+
+    # read attributes general command
+    data = _get_packet_data(foundation.GeneralCommand.Read_Attributes)
+    device.handle_message(260, cluster_id, endpoint_id, endpoint_id, data)
+    # no ZHA events emitted because we only handle Report_Attributes
+    assert cluster_listener.zha_send_event.call_count == 0
+
+    # report attributes command, but not "action_report"
+    attr = foundation.Attribute(
+        attrid=0x10,  # "on_intensity" attribute
+        value=foundation.TypeValue(
+            type=t.int16s(0x29), value=t.int16s(50)
+        ),  # 0x29 = t.int16s
+    )
+    data = _get_packet_data(foundation.GeneralCommand.Report_Attributes, attr)
+    device.handle_message(260, cluster_id, endpoint_id, endpoint_id, data)
+    # ZHA event emitted because we pass non "action_report"
+    # reports to the base class handler.
+    assert cluster_listener.zha_send_event.call_count == 1
+
+
+@pytest.mark.parametrize("quirk", (SinopeTechnologieslight,))
+async def test_sinope_light_switch_reporting(zigpy_device_from_quirk, quirk):
+    """Test that configuring reporting for action_report works."""
+    device: Device = zigpy_device_from_quirk(quirk)
+
+    manu_cluster = device.endpoints[1].in_clusters[SINOPE_MANUFACTURER_CLUSTER_ID]
+
+    request_patch = mock.patch("zigpy.zcl.Cluster.request", mock.AsyncMock())
+    bind_patch = mock.patch("zigpy.zcl.Cluster.bind", mock.AsyncMock())
+
+    with request_patch as request_mock, bind_patch as bind_mock:
+        request_mock.return_value = (foundation.Status.SUCCESS, "done")
+
+        await manu_cluster.bind()
+        await manu_cluster.configure_reporting(
+            SinopeTechnologiesManufacturerCluster.AttributeDefs.action_report.id,
+            3600,
+            10800,
+            1,
+        )
+
+        assert len(request_mock.mock_calls) == 1
+        assert len(bind_mock.mock_calls) == 1
diff --git a/tests/test_tuya.py b/tests/test_tuya.py
index b377ba0..6d131b9 100644
--- a/tests/test_tuya.py
+++ b/tests/test_tuya.py
@@ -1423,12 +1423,18 @@ async def test_eheat_send_attribute(zigpy_device_from_quirk, quirk):
         (zhaquirks.tuya.ts0042.TuyaSmartRemote0042TO, "_TZ3000_adkvzooy"),
         (zhaquirks.tuya.ts0042.TuyaSmartRemote0042TO, "_TZ3400_keyjhapk"),
         (zhaquirks.tuya.ts0042.TuyaSmartRemote0042TO, "another random manufacturer"),
+        (zhaquirks.tuya.ts0042.TuyaSmartRemote0042NO, "_TZ3000_adkvzooy"),
+        (zhaquirks.tuya.ts0042.TuyaSmartRemote0042NO, "_TYZB02_keyjhapk"),
+        (zhaquirks.tuya.ts0042.TuyaSmartRemote0042NO, "another random manufacturer"),
         (zhaquirks.tuya.ts0043.TuyaSmartRemote0043TI, "_TZ3000_bi6lpsew"),
         (zhaquirks.tuya.ts0043.TuyaSmartRemote0043TI, "_TZ3000_a7ouggvs"),
         (zhaquirks.tuya.ts0043.TuyaSmartRemote0043TI, "another random manufacturer"),
         (zhaquirks.tuya.ts0043.TuyaSmartRemote0043TO, "_TZ3000_qzjcsmar"),
         (zhaquirks.tuya.ts0043.TuyaSmartRemote0043TO, "_TZ3000_qzjcsmhd"),
         (zhaquirks.tuya.ts0043.TuyaSmartRemote0043TO, "another random manufacturer"),
+        (zhaquirks.tuya.ts0043.TuyaSmartRemote0043NO, "_TZ3000_qzjcsmar"),
+        (zhaquirks.tuya.ts0043.TuyaSmartRemote0043NO, "_TYZB02_key8kk7r"),
+        (zhaquirks.tuya.ts0043.TuyaSmartRemote0043NO, "another random manufacturer"),
         (zhaquirks.tuya.ts0044.TuyaSmartRemote0044TI, "_TZ3000_hjgcnkgs"),
         (zhaquirks.tuya.ts0044.TuyaSmartRemote0044TI, "_TZ3000_ojgcnkkl"),
         (zhaquirks.tuya.ts0044.TuyaSmartRemote0044TI, "_some_random_manuf"),