Repository: zigpy/zha-device-handlers
Pull request #3342 (2024-09-02 13:42:08)

Convert Sinope files to new style AttributeDefs
## Proposed change
<!--
  Explain your proposed change below.
-->
Update Sinope file format to new style AttributeDefs for thermostat.py, switch.py and sensor.py
Light.py is already modified in another PR #3313

## Additional information
<!--
  Please include any additional information that is important to this PR.
  For example, if this PR is a potentially breaking change, mention that here.
  If this PR requires other PRs to be merged in HA Core or other projects, mention that.
  Lastly, if this PR fixes a specific issue, please include "Fixes #xxxx".
-->


## Checklist
<!--
  Put an 'x' in all boxes that apply.
  Note: You do not need to tick all boxes before creating a PR.
-->

- [x] The changes are tested and work correctly
- [x] `pre-commit` checks pass / the code has been formatted using Black
- [ ] Tests have been added to verify that the new code works


claudegel: Can anybody explain what's wrong and how to fix please
TheJulianJES: It seems to be an issue with our CI at the moment, don't worry about it for now.
codecov[bot]: ## [Codecov](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3342?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 88.61%. Comparing base [(`5c4e08d`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/commit/5c4e08d94091a4c5f37cea7b21fd918474e98f82?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) to head [(`c0a0570`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/commit/c0a0570eebf7220f6bb39d5b532d88511cc5014c?dropdown=coverage&el=desc&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    #3342      +/-   ##
==========================================
+ Coverage   88.48%   88.61%   +0.13%     
==========================================
  Files         305      305              
  Lines        9623     9736     +113     
==========================================
+ Hits         8515     8628     +113     
  Misses       1108     1108              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3342?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).


Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index 26c8b86..8b9834f 100644
--- a/.pre-commit-config.yaml
+++ b/.pre-commit-config.yaml
@@ -1,3 +1,8 @@
+ci:
+  autofix_commit_msg: "Apply pre-commit auto fixes"
+  autoupdate_commit_msg: "Auto-update pre-commit hooks"
+  skip: [mypy]
+
 repos:
   - repo: https://github.com/codespell-project/codespell
     rev: v2.3.0
@@ -7,12 +12,12 @@ repos:
         args: ["--toml", "pyproject.toml"]
 
   - repo: https://github.com/pre-commit/mirrors-mypy
-    rev: v1.9.0
+    rev: v1.11.2
     hooks:
       - id: mypy
   
   - repo: https://github.com/astral-sh/ruff-pre-commit
-    rev: v0.3.2
+    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/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/terncy/__init__.py b/zhaquirks/terncy/__init__.py
index a2fbca5..a3252b9 100644
--- a/zhaquirks/terncy/__init__.py
+++ b/zhaquirks/terncy/__init__.py
@@ -164,8 +164,7 @@ class TerncyRawCluster(CustomCluster):
                 return  # ignore repeated event for single action.
             else:
                 self._last_clicks.append((state, count))
-            if state > 5:
-                state = 5
+            state = min(state, 5)
             event_args = {PRESS_TYPE: CLICK_TYPES[state], "count": count, VALUE: state}
             action = f"button_{CLICK_TYPES[state]}"
             self.listener_event(ZHA_SEND_EVENT, action, event_args)
diff --git a/zhaquirks/tuya/__init__.py b/zhaquirks/tuya/__init__.py
index 63b37cd..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
 # ---------------------------------------------------------
@@ -46,7 +47,6 @@ TUYA_SET_TIME = 0x24
 # TODO: To be checked
 TUYA_MCU_VERSION_REQ = 0x10
 TUYA_MCU_VERSION_RSP = 0x11
-#
 TUYA_LEVEL_COMMAND = 514
 
 LEVEL_EVENT = "level_event"
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/ts0501bs.py b/zhaquirks/tuya/ts0501bs.py
index 59d5243..22baa8d 100644
--- a/zhaquirks/tuya/ts0501bs.py
+++ b/zhaquirks/tuya/ts0501bs.py
@@ -39,6 +39,7 @@ class DimmableLedController(CustomDevice):
             ("_TZ3210_dxroobu3", "TS0501B"),
             ("_TZ3210_dbilpfqk", "TS0501B"),
             ("_TZ3210_agjx0pxt", "TS0501B"),
+            ("_TZ3210_d062rv7j", "TS0501B"),
         ],
         ENDPOINTS: {
             # <SimpleDescriptor endpoint=1 profile=260 device_type=257
diff --git a/zhaquirks/tuya/ts0601_rcbo.py b/zhaquirks/tuya/ts0601_rcbo.py
index 3c65eae..b1b2b82 100644
--- a/zhaquirks/tuya/ts0601_rcbo.py
+++ b/zhaquirks/tuya/ts0601_rcbo.py
@@ -250,8 +250,7 @@ class TuyaRCBOElectricalMeasurement(ElectricalMeasurement, TuyaAttributesCluster
             apparent_power = self.get("apparent_power")
             if apparent_power:
                 power_factor = value / apparent_power * 100
-                if power_factor > 100:
-                    power_factor = 100
+                power_factor = min(power_factor, 100)
                 super().update_attribute("power_factor", round(power_factor))
 
 
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_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"),