Repository: zigpy/zha-device-handlers
Pull request #3358 (2024-09-11 13:39:04)

Add quirk for Sonoff TRVZB
## Proposed change

Add a quirk for Sonoff TRVZB. This is an alternative PR to #2831 using the quirks builder.

## Additional information

String translations will need to be added iff / when this is merged:

---------------------------------------------------------------
| translation key              | string                       |
|------------------------------|------------------------------|
| frost_protection_temperature | Frost protection temperature |
| open_window                  | Open window                  |
| valve_opening_degree     | Valve opening degree     |
| valve_closing_degree     | Valve closing degree     |
---------------------------------------------------------------

Fixes https://github.com/zigpy/zha-device-handlers/issues/2772
Closes https://github.com/zigpy/zha-device-handlers/pull/2831

## 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


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3358?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 `95.45455%` with `1 line` in your changes missing coverage. Please review.
> Project coverage is 88.50%. Comparing base [(`acecf70`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/commit/acecf704f2b253c9297d901eed8909dbd589ea97?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) to head [(`19eb734`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/commit/19eb73480936d9d3263090606f561405efb02cb2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy).

| [Files with missing lines](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3358?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/sonoff/trvzb.py](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3358?src=pr&el=tree&filepath=zhaquirks%2Fsonoff%2Ftrvzb.py&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy#diff-emhhcXVpcmtzL3Nvbm9mZi90cnZ6Yi5weQ==) | 95.45% | [1 Missing :warning: ](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/3358?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    #3358      +/-   ##
==========================================
+ Coverage   88.49%   88.50%   +0.01%     
==========================================
  Files         305      306       +1     
  Lines        9621     9643      +22     
==========================================
+ Hits         8514     8535      +21     
- Misses       1107     1108       +1     
```

</details>

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

netdiver: I downloaded this trvzb.py file and put it into custom_zha_quirks, but it doesn't work for me.

This is an excerpt from the log:

```
2024-11-12 15:26:26.846 ERROR (SyncWorker_9) [zhaquirks] Unexpected exception importing custom quirk 'trvzb'
Traceback (most recent call last):
  File "/usr/local/lib/python3.12/site-packages/zhaquirks/__init__.py", line 479, in setup
    spec.loader.exec_module(module)
  File "<frozen importlib._bootstrap_external>", line 995, in exec_module
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "/config/custom_zha_quirks/trvzb.py", line 81, in <module>
    .switch(
     ^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/zigpy/quirks/v2/__init__.py", line 750, in switch
    SwitchMetadata(
  File "<attrs generated init zigpy.quirks.v2.SwitchMetadata>", line 19, in __init__
    __attr_validator_fallback_name(self, __attr_fallback_name, self.fallback_name)
  File "/usr/local/lib/python3.12/site-packages/attr/validators.py", line 101, in __call__
    raise TypeError(
TypeError: ("'fallback_name' must be <class 'str'> (got None that is a <class 'NoneType'>).", Attribute(name='fallback_name', default=NOTHING, validator=<instance_of validator for type <class 'str'>>, repr=True, eq=True, eq_key=None, order=True, order_key=None, hash=None, init=True, metadata=mappingproxy({}), type='str', converter=None, kw_only=True, inherited=True, on_setattr=None, alias='fallback_name'), <class 'str'>, None)

```
please forgive me if I'm doing something wrong.
fgsch: @netdiver Hi. Thanks for the report. I haven't seen this before but I just checked my logs and I'm seeing the same issue.
I will have a look and update this PR accordingly.

fgsch: Ok, I see what the problem is. I will push an update tomorrow.
fgsch: @netdiver the issue should be fixed now. Thank you again for the report.
GregersP: Hi there,

I have got these all over the house, and need to control the opening and closing degrees. So I've extended your code with the two settings - instead of creating yet another PR - I've branched here: https://github.com/zigpy/zha-device-handlers/commit/77ac2e480a1b484f8b45ebf86a6bbbeee9dcb941



netdiver: > @netdiver the issue should be fixed now. Thank you again for the report.

Thanks, I've downloaded the quirk, and it works for me.
I can see the added properties in the configuration
![immagine](https://github.com/user-attachments/assets/a18e0c25-9238-449a-bf9d-f04b9e27a38f)
(sorry, Italian language).
Thank you!
AlbrechtK: Thanks, works for me also.
Is there already some best practice how to use opening and closing degrees to avoid the default on/off behavior?
JGSBP2S: > Hi there,
> 
> I have got these all over the house, and need to control the opening and closing degrees. So I've extended your code with the two settings - instead of creating yet another PR - I've branched here: [77ac2e4](https://github.com/zigpy/zha-device-handlers/commit/77ac2e480a1b484f8b45ebf86a6bbbeee9dcb941)

Added this and valve % are not showing up

GregersP: > > Hi there,
> 
> > 
> 
> > I have got these all over the house, and need to control the opening and closing degrees. So I've extended your code with the two settings - instead of creating yet another PR - I've branched here: [77ac2e4](https://github.com/zigpy/zha-device-handlers/commit/77ac2e480a1b484f8b45ebf86a6bbbeee9dcb941)
> 
> 
> 
> Added this and valve % are not showing up
> 
> 


Very strange - it has been confirmed working here and above(?)

Have you reloaded etc?
JGSBP2S: > > > Hi there,
> > 
> > 
> > > 
> > 
> > 
> > > I have got these all over the house, and need to control the opening and closing degrees. So I've extended your code with the two settings - instead of creating yet another PR - I've branched here: [77ac2e4](https://github.com/zigpy/zha-device-handlers/commit/77ac2e480a1b484f8b45ebf86a6bbbeee9dcb941)
> > 
> > 
> > Added this and valve % are not showing up
> 
> Very strange - it has been confirmed working here and above(?)
> 
> Have you reloaded etc?

Forget it. Trier it on the on l’y valve I forgot to update with latest firmware… Stupid !
netdiver: @GregersP 
only a small report on your code.
fallback_name contains a typo: degree is written with three 'e' in rows 111 and 120.

fgsch: Regarding exposing this, I can add it to this PR in a disabled state but I wonder how often would people require to change this and whether it's something that belongs to the zha toolkit.
AlbrechtK: > Regarding exposing this, I can add it to this PR in a disabled state but I wonder how often would people require to change this and whether it's something that belongs to the zha toolkit.

From my point of view the valve % are the best change to make this device really control temperature instead of either turning fully on or off. E.g. the closer the set temp is to the actual one the narrower the valve % window should be to archive a constant heating flow.
GregersP: > > Regarding exposing this, I can add it to this PR in a disabled state but I wonder how often would people require to change this and whether it's something that belongs to the zha toolkit.
> 
> 
> 
> From my point of view the valve % are the best change to make this device really control temperature instead of either turning fully on or off. E.g. the closer the set temp is to the actual one the narrower the valve % window should be to archive a constant heating flow.

That is also my approach. So I am happy to make it a separate PR if @fgsch finds it out of scope 😊
fgsch: Updated with code from @GregersP to expose the opening and closing degrees.
tr4nt0r: The min/max values for local temperature calibration are wrong, the TRVZB supports value from -12.8 to +12.7 degree. 
I added the following to the quirk to be able to set offsets above the -7+7° limits:
```python
    .number(
        "local_temperature_calibration",
        0x0201,
        min_value=-12.8,
        max_value=12.7,
        step=0.1,
        unit=UnitOfTemperature.CELSIUS,
        multiplier=0.1,
        fallback_name="Local temperature offset",
        translation_key="local_temperature_calibration"
    )
```
Although I did something wrong, because I now have 2 offset entities, but I think you can figure out how to do this correctly.
fgsch: @tr4nt0r thanks, I will update the PR shortly and check if there are other updates worth bringing in from zigbee-herdsman-converters.
fgsch: @tr4nt0r I've opened https://github.com/zigpy/zha/pull/300 to handle the calibration min/max values update.

Patch:
diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml
index a52ccd3..6dbd217 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.7.3
+    rev: v0.7.4
     hooks:
       - id: ruff
         args: ["--fix", "--exit-non-zero-on-fix", "--config", "pyproject.toml"]
diff --git a/README.md b/README.md
index 6539114..43828ff 100644
--- a/README.md
+++ b/README.md
@@ -397,7 +397,7 @@ replacement = {
 
 You can see that we have replaced `ElectricalMeasurement.cluster_id` from endpoint 1 in the `signature` dict with the name of our cluster that we created: `ElectricalMeasurementCluster` and on endpoint 2 we replaced `AnalogInput.cluster_id` with the implementation we created for that: `AnalogInputCluster`. This instructs Zigpy to use these `CustomCluster` derivatives instead of the normal cluster definitions for these clusters and this is why this part of the quirk is called `replacement`.
 
-Now lets put this all together. If you examine the device definition above you will see that we have defined our custom device, we defined the `signature` dict where we transcribed the `SimpleDescriptor` output we obtained when the device joined the network and we defined the `replacement` dict where we swapped the cluster ids for the culsters that we wanted to replace with the `CustomCluster` implementations that we created.
+Now lets put this all together. If you examine the device definition above you will see that we have defined our custom device, we defined the `signature` dict where we transcribed the `SimpleDescriptor` output we obtained when the device joined the network and we defined the `replacement` dict where we swapped the cluster ids for the clusters that we wanted to replace with the `CustomCluster` implementations that we created.
 
 # Contribution Guidelines
 
diff --git a/zhaquirks/philips/rom001.py b/zhaquirks/philips/rom001.py
index 0588ebc..2996d65 100644
--- a/zhaquirks/philips/rom001.py
+++ b/zhaquirks/philips/rom001.py
@@ -65,7 +65,7 @@ class PhilipsROM001(CustomDevice):
         #  device_version=1
         #  input_clusters=[0, 1, 3, 64512, 4096]
         #  output_clusters=[25, 0, 3, 4, 6, 8, 5, 4096]>
-        MODELS_INFO: [(PHILIPS, "ROM001"), (SIGNIFY, "ROM001")],
+        MODELS_INFO: [(PHILIPS, "ROM001"), (SIGNIFY, "ROM001"), (SIGNIFY, "RDM003")],
         ENDPOINTS: {
             1: {
                 PROFILE_ID: zha.PROFILE_ID,
diff --git a/zhaquirks/philips/wall_switch.py b/zhaquirks/philips/wall_switch.py
index d865456..576d61a 100644
--- a/zhaquirks/philips/wall_switch.py
+++ b/zhaquirks/philips/wall_switch.py
@@ -40,8 +40,6 @@ from zhaquirks.philips import (
     PressType,
 )
 
-DEVICE_SPECIFIC_UNKNOWN = 64512
-
 
 class PhilipsWallSwitchBasicCluster(PhilipsBasicCluster):
     """Philips wall switch Basic cluster."""
@@ -99,7 +97,7 @@ class PhilipsWallSwitch(CustomDevice):
                     Basic.cluster_id,
                     PowerConfiguration.cluster_id,
                     Identify.cluster_id,
-                    DEVICE_SPECIFIC_UNKNOWN,
+                    PhilipsWallSwitchRemoteCluster.cluster_id,
                 ],
                 OUTPUT_CLUSTERS: [
                     Identify.cluster_id,
@@ -137,3 +135,58 @@ class PhilipsWallSwitch(CustomDevice):
     device_automation_triggers = (
         PhilipsWallSwitchRemoteCluster.generate_device_automation_triggers()
     )
+
+
+class PhilipsWallSwitchRDM004(CustomDevice):
+    """Philips RDM004 variant."""
+
+    signature = {
+        MODELS_INFO: [(SIGNIFY, "RDM004")],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.NON_COLOR_SCENE_CONTROLLER,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    Identify.cluster_id,
+                    PhilipsWallSwitchRemoteCluster.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    OnOff.cluster_id,
+                    LevelControl.cluster_id,
+                    Ota.cluster_id,
+                ],
+            },
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.NON_COLOR_SCENE_CONTROLLER,
+                INPUT_CLUSTERS: [
+                    PhilipsWallSwitchBasicCluster,
+                    PowerConfiguration.cluster_id,
+                    Identify.cluster_id,
+                    PhilipsWallSwitchRemoteCluster,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    OnOff.cluster_id,
+                    LevelControl.cluster_id,
+                    Ota.cluster_id,
+                ],
+            },
+        },
+    }
+
+    device_automation_triggers = (
+        PhilipsWallSwitchRemoteCluster.generate_device_automation_triggers()
+    )
diff --git a/zhaquirks/schneiderelectric/__init__.py b/zhaquirks/schneiderelectric/__init__.py
index 10981ad..7efb815 100644
--- a/zhaquirks/schneiderelectric/__init__.py
+++ b/zhaquirks/schneiderelectric/__init__.py
@@ -167,10 +167,10 @@ class SEBallast(CustomCluster, Ballast):
             id=0xE000, type=SEControlMode, is_manufacturer_specific=True
         )
         se_wiring_mode: Final = ZCLAttributeDef(
-            id=0xE002, type=t.enum8, is_manufacturer_specific=True
+            id=0xE002, type=SEWiringMode, is_manufacturer_specific=True
         )
         se_dimming_curve: Final = ZCLAttributeDef(
-            id=0xE002, type=t.enum8, is_manufacturer_specific=True
+            id=0xE002, type=SEDimmingCurve, is_manufacturer_specific=True
         )
 
 
diff --git a/zhaquirks/schneiderelectric/dimmers.py b/zhaquirks/schneiderelectric/dimmers.py
index e7803b3..4621b14 100644
--- a/zhaquirks/schneiderelectric/dimmers.py
+++ b/zhaquirks/schneiderelectric/dimmers.py
@@ -1,129 +1,33 @@
 """Schneider Electric dimmers and switches quirks."""
 
-from zigpy.profiles import zgp, zha
-from zigpy.quirks import CustomDevice
-from zigpy.zcl.clusters.closures import WindowCovering
-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.lighting import Ballast
+from zigpy.quirks.v2 import QuirkBuilder
 
-from zhaquirks.const import (
-    DEVICE_TYPE,
-    ENDPOINTS,
-    INPUT_CLUSTERS,
-    MODELS_INFO,
-    OUTPUT_CLUSTERS,
-    PROFILE_ID,
+from zhaquirks.schneiderelectric import (
+    SE_MANUF_NAME,
+    SEBallast,
+    SEBasic,
+    SEOnOff,
+    SESpecific,
 )
-from zhaquirks.schneiderelectric import SE_MANUF_NAME, SEBallast, SEBasic, SESpecific
-
 
-class NHRotaryDimmer1(CustomDevice):
-    """NHROTARY/DIMMER/1 by Schneider Electric."""
+(
+    QuirkBuilder(SE_MANUF_NAME, "NHROTARY/DIMMER/1")
+    .applies_to(SE_MANUF_NAME, "NHROTARY/UNIDIM/1")
+    .applies_to(SE_MANUF_NAME, "NHPB/DIMMER/1")
+    .applies_to(SE_MANUF_NAME, "NHPB/UNIDIM/1")
+    .replaces(SEBasic, endpoint_id=3)
+    .replaces(SEBallast, endpoint_id=3)
+    .replaces(SEOnOff, endpoint_id=3)
+    .replaces(SEBasic, endpoint_id=21)
+    .replaces(SESpecific, endpoint_id=21)
+    .add_to_registry()
+)
 
-    signature = {
-        MODELS_INFO: [
-            (SE_MANUF_NAME, "NHROTARY/DIMMER/1"),
-        ],
-        ENDPOINTS: {
-            3: {
-                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,
-                    Ballast.cluster_id,
-                    Diagnostic.cluster_id,
-                ],
-                OUTPUT_CLUSTERS: [
-                    Ota.cluster_id,
-                ],
-            },
-            21: {
-                PROFILE_ID: zha.PROFILE_ID,
-                DEVICE_TYPE: zha.DeviceType.DIMMER_SWITCH,
-                INPUT_CLUSTERS: [
-                    Basic.cluster_id,
-                    Identify.cluster_id,
-                    Diagnostic.cluster_id,
-                    SESpecific.cluster_id,
-                ],
-                OUTPUT_CLUSTERS: [
-                    Identify.cluster_id,
-                    Groups.cluster_id,
-                    Scenes.cluster_id,
-                    OnOff.cluster_id,
-                    LevelControl.cluster_id,
-                    WindowCovering.cluster_id,
-                ],
-            },
-            242: {
-                PROFILE_ID: zgp.PROFILE_ID,
-                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
-                INPUT_CLUSTERS: [],
-                OUTPUT_CLUSTERS: [
-                    GreenPowerProxy.cluster_id,
-                ],
-            },
-        },
-    }
-    replacement = {
-        ENDPOINTS: {
-            3: {
-                PROFILE_ID: zha.PROFILE_ID,
-                DEVICE_TYPE: zha.DeviceType.DIMMABLE_LIGHT,
-                INPUT_CLUSTERS: [
-                    SEBasic,
-                    Identify.cluster_id,
-                    Groups.cluster_id,
-                    Scenes.cluster_id,
-                    OnOff.cluster_id,
-                    LevelControl.cluster_id,
-                    SEBallast,
-                    Diagnostic.cluster_id,
-                ],
-                OUTPUT_CLUSTERS: [
-                    Ota.cluster_id,
-                ],
-            },
-            21: {
-                PROFILE_ID: zha.PROFILE_ID,
-                DEVICE_TYPE: zha.DeviceType.DIMMER_SWITCH,
-                INPUT_CLUSTERS: [
-                    SEBasic,
-                    Identify.cluster_id,
-                    Diagnostic.cluster_id,
-                    SESpecific,
-                ],
-                OUTPUT_CLUSTERS: [
-                    Identify.cluster_id,
-                    Groups.cluster_id,
-                    Scenes.cluster_id,
-                    OnOff.cluster_id,
-                    LevelControl.cluster_id,
-                    WindowCovering.cluster_id,
-                ],
-            },
-            242: {
-                PROFILE_ID: zgp.PROFILE_ID,
-                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
-                INPUT_CLUSTERS: [],
-                OUTPUT_CLUSTERS: [
-                    GreenPowerProxy.cluster_id,
-                ],
-            },
-        }
-    }
+(
+    QuirkBuilder(SE_MANUF_NAME, "NHPB/SWITCH/1")
+    .replaces(SEBasic)
+    .replaces(SEOnOff)
+    .replaces(SEBasic, endpoint_id=21)
+    .replaces(SESpecific, endpoint_id=21)
+    .add_to_registry()
+)
diff --git a/zhaquirks/schneiderelectric/thermostat.py b/zhaquirks/schneiderelectric/thermostat.py
new file mode 100644
index 0000000..537c9e8
--- /dev/null
+++ b/zhaquirks/schneiderelectric/thermostat.py
@@ -0,0 +1,576 @@
+"""Schneider Electric thermostats quirks."""
+
+from typing import Final
+
+from zigpy.quirks import CustomCluster
+from zigpy.quirks.v2 import QuirkBuilder
+import zigpy.types as t
+from zigpy.zcl.clusters.hvac import SystemMode, Thermostat, UserInterface
+from zigpy.zcl.clusters.measurement import TemperatureMeasurement
+from zigpy.zcl.clusters.smartenergy import Metering
+from zigpy.zcl.foundation import ZCLAttributeDef, ZCLCommandDef
+
+from zhaquirks.schneiderelectric import SE_MANUF_NAME, SEBasic
+
+
+class SETemperatureSensorType(t.enum8):
+    """Temperature sensor connected to thermostat."""
+
+    Sensor2kOhm = 0x01
+    Sensor10kOhm = 0x02
+    Sensor12kOhm = 0x03
+    Sensor15kOhm = 0x04
+    Sensor33kOhm = 0x05
+    Sensor47kOhm = 0x06
+    SensorAbsent = 0xFF
+
+
+class SEControlStatus(t.enum8):
+    """Control status of the thermostat."""
+
+    # Operating normally
+    NormalOperation = 0x00
+
+    # No temperature value is available. The output is using Fallback rules
+    NoTemperature = 0x20
+
+    # Output has been forced using a remote override
+    RemoteDemandOverride = 0x40
+
+    # Demand is constrained by Window Open rules
+    WindowOpen = 0x41
+
+    # Demand forced by "emergency" button
+    LocalForceOn = 0x61
+
+    # The output is being driven as part of a maintenance operation
+    Maintenance = 0x82
+
+    # Output held on, or off, despite a change in demand due to minimum times
+    OutputTemporalLimit = 0x83
+
+    # The output has been placed into a safe state due to a sensor failure
+    SensorFault = 0x84
+
+
+class SELocalTemperatureSourceSelect(t.enum8):
+    """Local temperature source select."""
+
+    # Internal temperature sensor
+    Ambient = 0x02
+
+    # External temperature sensor
+    External = 0x03
+
+
+class SEControlType(t.enum8):
+    """Control type of the thermostat."""
+
+    OnOff = 0x00
+    PI = 0x01
+    NoControl = 0xFF
+
+
+class SEHeatCoolInputMode(t.enum8):
+    """Heat/Cool input mode."""
+
+    CoolingWhenActive = 0x00
+    HeatingWhenActive = 0x01
+    NotConnected = 0xFF
+
+
+class SEThermostatApplication(t.enum8):
+    """Thermostat application."""
+
+    OccupiedSpace = 0x00
+    Floor = 0x01
+    NotKnown = 0xFF
+
+
+class SEHeatingFuel(t.enum8):
+    """Heating fuel."""
+
+    Electricity = 0x00
+    Gas = 0x01
+    Oil = 0x02
+    SolidFuel = 0x03
+    Solar = 0x04
+    ComunityHeating = 0x05
+    HeatPump = 0x06
+    NotSpecified = 0xFF
+
+
+class SEHeatTransferMedium(t.enum8):
+    """Heat transfer medium."""
+
+    Nothing = 0x00
+    Hydronic = 0x01
+    Air = 0x02
+
+
+class SEHeatingEmitterType(t.enum8):
+    """Heating emitter type."""
+
+    Direct = 0x00
+    Radiator = 0x01
+    FanAssistedRadiator = 0x02
+    RadiantPanel = 0x03
+    Floor = 0x04
+    NotSpecified = 0xFF
+
+
+class SEDemandOverrideType(t.enum8):
+    """Demand override type."""
+
+    NoDemandOverride = 0x00
+    CoolingDemandOverride = 0x03
+    HeatingDemandOverride = 0x04
+
+
+class SESetpointChangeReason(t.enum8):
+    """Setpoint change reason."""
+
+    SetpointAdjustment = 0x00
+    BoostUsed = 0x01
+    BoostCancel = 0x02
+    SystemModeChange = 0x03
+    AwayStart = 0x04
+    AwayEnd = 0x05
+
+
+class SEAffectedSetpoint(t.enum8):
+    """Affected setpoint."""
+
+    Nothing = 0x00
+    OccupiedHeating = 0x01
+    OccupiedCooling = 0x02
+    UnoccupiedHeating = 0x03
+    UnoccupiedCooling = 0x04
+
+
+class SECoolingOutputMode(t.enum8):
+    """Cooling output mode."""
+
+    Disabled = 0x00
+    Relay = 0x01
+    RelayNC = 0x04
+
+
+class SEHeatingOutputMode(t.enum8):
+    """Heating output mode."""
+
+    Disabled = 0x00
+    Relay = 0x01
+    OpenTherm = 0x02
+    FilPilote = 0x03
+    RelayNC = 0x04
+
+
+class SEMetering(CustomCluster, Metering):
+    """Schneider Electric Metering cluster."""
+
+    class AttributeDefs(Metering.AttributeDefs):
+        """Schneider Electric Metering cluster attributes."""
+
+        # This attribute specifies the demand of a switched load when it is energised
+        se_fixed_load_demand: Final = ZCLAttributeDef(
+            id=0x4510,
+            type=t.uint24_t,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+
+
+class SETemperatureMeasurement(CustomCluster, TemperatureMeasurement):
+    """Schneider Electric Temperature Measurement cluster."""
+
+    class AttributeDefs(TemperatureMeasurement.AttributeDefs):
+        """Schneider Electric Temperature Measurement cluster attributes."""
+
+        se_sensor_correction: Final = ZCLAttributeDef(
+            id=0xE020,
+            type=t.int16s,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+
+
+class SETemperatureMeasurementExternal(SETemperatureMeasurement):
+    """Schneider Electric Temperature Measurement cluster for external (floor) temperature input."""
+
+    class AttributeDefs(SETemperatureMeasurement.AttributeDefs):
+        """Schneider Electric Temperature Measurement cluster attributes."""
+
+        se_temperature_sensor_type: Final = ZCLAttributeDef(
+            id=0xE021,
+            type=SETemperatureSensorType,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+
+
+class SEThermostat(CustomCluster, Thermostat):
+    """Schneider Electric manufacturer specific Thermostat cluster."""
+
+    class ClientCommandDefs(Thermostat.ClientCommandDefs):
+        """Client command definitions."""
+
+        se_pi_demand_override: Final = ZCLCommandDef(
+            id=0xF1,
+            schema={
+                "type": SEDemandOverrideType,
+                "demand": t.uint8_t,
+                "duration": t.uint8_t,
+            },
+            is_manufacturer_specific=True,
+        )
+
+    class ServerCommandDefs(Thermostat.ServerCommandDefs):
+        """Server command definitions."""
+
+        se_local_setpoint_change_notification_command: Final = ZCLCommandDef(
+            id=0x91,
+            schema={
+                "reason": SESetpointChangeReason,
+                "system_mode": SystemMode,
+                "affected_setpoint": SEAffectedSetpoint,
+                "setpoint_value": t.int16s,
+                "duration": t.uint16_t,
+            },
+            is_manufacturer_specific=True,
+        )
+
+    class AttributeDefs(Thermostat.AttributeDefs):
+        """Attribute definitions."""
+
+        se_open_window_detection_status: Final = ZCLAttributeDef(
+            id=0xE012,
+            type=t.Bool,
+            access="r",
+            is_manufacturer_specific=True,
+        )
+        se_open_window_detection_threshold: Final = ZCLAttributeDef(
+            id=0xE013,
+            type=t.uint8_t,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_open_window_event_duration: Final = ZCLAttributeDef(
+            id=0xE014,
+            type=t.uint16_t,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_open_window_detection_guard_period: Final = ZCLAttributeDef(
+            id=0xE015,
+            type=t.uint16_t,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_fallback_timeout: Final = ZCLAttributeDef(
+            id=0xE200,
+            type=t.uint16_t,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_boost_amount: Final = ZCLAttributeDef(
+            id=0xE210,
+            type=t.uint16_t,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_control_status: Final = ZCLAttributeDef(
+            id=0xE211,
+            type=SEControlStatus,
+            access="r",
+            is_manufacturer_specific=True,
+        )
+        se_local_temperature_souce_select: Final = ZCLAttributeDef(
+            id=0xE212,
+            type=SELocalTemperatureSourceSelect,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_control_type: Final = ZCLAttributeDef(
+            id=0xE213,
+            type=SEControlType,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_heat_cool_input_mode: Final = ZCLAttributeDef(
+            id=0xE214,
+            type=SEHeatCoolInputMode,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_unoccupied_tracking_offset: Final = ZCLAttributeDef(
+            id=0xE215,
+            type=t.uint16_t,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_thermostat_application: Final = ZCLAttributeDef(
+            id=0xE216,
+            type=SEThermostatApplication,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_heating_fuel: Final = ZCLAttributeDef(
+            id=0xE217,
+            type=SEHeatingFuel,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_heat_transfer_medium: Final = ZCLAttributeDef(
+            id=0xE218,
+            type=SEHeatTransferMedium,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_heating_emitter_type: Final = ZCLAttributeDef(
+            id=0xE218,
+            type=SEHeatingEmitterType,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+
+
+class SEUserInterface(CustomCluster, UserInterface):
+    """Schneider Electric Thermostat User Interface cluster."""
+
+    class AttributeDefs(UserInterface.AttributeDefs):
+        """Attribute definitions."""
+
+        se_brightness: Final = ZCLAttributeDef(
+            id=0xE000,
+            type=t.uint8_t,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_inactive_brightness: Final = ZCLAttributeDef(
+            id=0xE001,
+            type=t.uint8_t,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_activity_timeout: Final = ZCLAttributeDef(
+            id=0xE002,
+            type=t.uint16_t,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+
+
+class SECycleTime(CustomCluster):
+    """Schneider Electric Cycle Time cluster."""
+
+    cluster_id = 0xFF16
+    name = "SECycleTime"
+
+    class AttributeDefs(CustomCluster.AttributeDefs):
+        """Attribute definitions."""
+
+        se_demand_percentage: Final = ZCLAttributeDef(
+            id=0x0000,
+            type=t.uint8_t,
+            access="r",
+            is_manufacturer_specific=True,
+        )
+        se_cycle_time: Final = ZCLAttributeDef(
+            id=0x0010,
+            type=t.uint16_t,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_min_cycle_time: Final = ZCLAttributeDef(
+            id=0x0011,
+            type=t.uint16_t,
+            access="r",
+            is_manufacturer_specific=True,
+        )
+        se_max_cycle_time: Final = ZCLAttributeDef(
+            id=0x0012,
+            type=t.uint16_t,
+            access="r",
+            is_manufacturer_specific=True,
+        )
+        se_status_flags: Final = ZCLAttributeDef(
+            id=0x0020,
+            type=t.bitmap8,
+            access="r",
+            is_manufacturer_specific=True,
+        )
+        se_cluster_revision: Final = ZCLAttributeDef(
+            id=0xFFFD,
+            type=t.uint16_t,
+            access="r",
+            is_manufacturer_specific=True,
+        )
+
+
+class SEHeatingCoolingOutput(CustomCluster):
+    """Schneider Electric Heating Cooling Output cluster."""
+
+    cluster_id = 0xFF23
+    name = "SEHeatingCoolingOutput"
+
+    class AttributeDefs(CustomCluster.AttributeDefs):
+        """Attribute definitions."""
+
+        se_measured_temperature: Final = ZCLAttributeDef(
+            id=0x0000,
+            type=t.int16s,
+            access="r",
+            is_manufacturer_specific=True,
+        )
+        se_abs_min_heat_temperature_limit: Final = ZCLAttributeDef(
+            id=0x0003,
+            type=t.int16s,
+            access="r",
+            is_manufacturer_specific=True,
+        )
+        se_abs_max_heat_temperature_limit: Final = ZCLAttributeDef(
+            id=0x0004,
+            type=t.int16s,
+            access="r",
+            is_manufacturer_specific=True,
+        )
+        se_abs_min_cool_temperature_limit: Final = ZCLAttributeDef(
+            id=0x0005,
+            type=t.int16s,
+            access="r",
+            is_manufacturer_specific=True,
+        )
+        se_abs_max_cool_temperature_limit: Final = ZCLAttributeDef(
+            id=0x0006,
+            type=t.int16s,
+            access="r",
+            is_manufacturer_specific=True,
+        )
+        se_min_heat_temperature_limit: Final = ZCLAttributeDef(
+            id=0x0015,
+            type=t.int16s,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_max_heat_temperature_limit: Final = ZCLAttributeDef(
+            id=0x0016,
+            type=t.int16s,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_min_cool_temperature_limit: Final = ZCLAttributeDef(
+            id=0x0017,
+            type=t.int16s,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_max_cool_temperature_limit: Final = ZCLAttributeDef(
+            id=0x0018,
+            type=t.int16s,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_heat_temperature_high_limit: Final = ZCLAttributeDef(
+            id=0x0020,
+            type=t.int16s,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_heat_temperature_low_limit: Final = ZCLAttributeDef(
+            id=0x0021,
+            type=t.int16s,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_cool_temperature_high_limit: Final = ZCLAttributeDef(
+            id=0x0022,
+            type=t.int16s,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_cool_temperature_low_limit: Final = ZCLAttributeDef(
+            id=0x0023,
+            type=t.int16s,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_cooling_output_mode: Final = ZCLAttributeDef(
+            id=0x0030,
+            type=SECoolingOutputMode,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_heating_output_mode: Final = ZCLAttributeDef(
+            id=0x0031,
+            type=SEHeatingOutputMode,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_maximum_idle_time: Final = ZCLAttributeDef(
+            id=0x0041,
+            type=t.uint16_t,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_anti_idle_exercise_time: Final = ZCLAttributeDef(
+            id=0x0042,
+            type=t.uint16_t,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_preffered_exercise_time: Final = ZCLAttributeDef(
+            id=0x0043,
+            type=t.uint16_t,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_min_off_time: Final = ZCLAttributeDef(
+            id=0x0044,
+            type=t.uint16_t,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_min_on_time: Final = ZCLAttributeDef(
+            id=0x0045,
+            type=t.uint16_t,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_max_overall_duty_cycle: Final = ZCLAttributeDef(
+            id=0xE207,
+            type=t.uint16_t,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_overall_duty_cycle_period: Final = ZCLAttributeDef(
+            id=0xE208,
+            type=t.uint16_t,
+            access="rw",
+            is_manufacturer_specific=True,
+        )
+        se_cluster_revision: Final = ZCLAttributeDef(
+            id=0xFFFD,
+            type=t.uint16_t,
+            access="r",
+            is_manufacturer_specific=True,
+        )
+
+
+(
+    QuirkBuilder(SE_MANUF_NAME, "EKO07259")
+    .applies_to(SE_MANUF_NAME, "WDE002497")
+    .applies_to(SE_MANUF_NAME, "WDE011680")
+    .replaces(SEBasic)
+    .replaces(SEBasic, endpoint_id=2)
+    .replaces(SEBasic, endpoint_id=3)
+    .replaces(SEBasic, endpoint_id=5)
+    .replaces(SEThermostat)
+    .replaces(SEUserInterface)
+    .replaces(SETemperatureMeasurement, endpoint_id=2)
+    .replaces(SETemperatureMeasurementExternal, endpoint_id=3)
+    .replaces(SEMetering, endpoint_id=5)
+    .replaces(SECycleTime)
+    .replaces(SEHeatingCoolingOutput)
+    .add_to_registry()
+)
diff --git a/zhaquirks/sinope/__init__.py b/zhaquirks/sinope/__init__.py
index ff59f31..7990b36 100644
--- a/zhaquirks/sinope/__init__.py
+++ b/zhaquirks/sinope/__init__.py
@@ -52,7 +52,7 @@ LIGHT_DEVICE_TRIGGERS = {
             ATTRIBUTE_ID: 84,
             ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
             BUTTON: TURN_ON,
-            VALUE: ButtonAction.Pressed_on,
+            VALUE: int(ButtonAction.Pressed_on),
         },
     },
     (SHORT_PRESS, TURN_OFF): {
@@ -63,7 +63,7 @@ LIGHT_DEVICE_TRIGGERS = {
             ATTRIBUTE_ID: 84,
             ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
             BUTTON: TURN_OFF,
-            VALUE: ButtonAction.Pressed_off,
+            VALUE: int(ButtonAction.Pressed_off),
         },
     },
     (SHORT_RELEASE, TURN_ON): {
@@ -74,7 +74,7 @@ LIGHT_DEVICE_TRIGGERS = {
             ATTRIBUTE_ID: 84,
             ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
             BUTTON: TURN_ON,
-            VALUE: ButtonAction.Released_on,
+            VALUE: int(ButtonAction.Released_on),
         },
     },
     (SHORT_RELEASE, TURN_OFF): {
@@ -85,7 +85,7 @@ LIGHT_DEVICE_TRIGGERS = {
             ATTRIBUTE_ID: 84,
             ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
             BUTTON: TURN_OFF,
-            VALUE: ButtonAction.Released_off,
+            VALUE: int(ButtonAction.Released_off),
         },
     },
     (DOUBLE_PRESS, TURN_ON): {
@@ -96,7 +96,7 @@ LIGHT_DEVICE_TRIGGERS = {
             ATTRIBUTE_ID: 84,
             ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
             BUTTON: TURN_ON,
-            VALUE: ButtonAction.Double_on,
+            VALUE: int(ButtonAction.Double_on),
         },
     },
     (DOUBLE_PRESS, TURN_OFF): {
@@ -107,7 +107,7 @@ LIGHT_DEVICE_TRIGGERS = {
             ATTRIBUTE_ID: 84,
             ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
             BUTTON: TURN_OFF,
-            VALUE: ButtonAction.Double_off,
+            VALUE: int(ButtonAction.Double_off),
         },
     },
     (LONG_PRESS, TURN_ON): {
@@ -118,7 +118,7 @@ LIGHT_DEVICE_TRIGGERS = {
             ATTRIBUTE_ID: 84,
             ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
             BUTTON: TURN_ON,
-            VALUE: ButtonAction.Long_on,
+            VALUE: int(ButtonAction.Long_on),
         },
     },
     (LONG_PRESS, TURN_OFF): {
@@ -129,7 +129,7 @@ LIGHT_DEVICE_TRIGGERS = {
             ATTRIBUTE_ID: 84,
             ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
             BUTTON: TURN_OFF,
-            VALUE: ButtonAction.Long_off,
+            VALUE: int(ButtonAction.Long_off),
         },
     },
 }
diff --git a/zhaquirks/sinope/light.py b/zhaquirks/sinope/light.py
index 328315f..a738bd2 100644
--- a/zhaquirks/sinope/light.py
+++ b/zhaquirks/sinope/light.py
@@ -301,7 +301,7 @@ class SinopeTechnologieslight(CustomDevice):
     device_automation_triggers = LIGHT_DEVICE_TRIGGERS
 
 
-class SinopeDM2500ZB(SinopeTechnologieslight):
+class SinopeDM2500ZB(CustomDevice):
     """DM2500ZB, DM2500ZB-G2 Dimmers."""
 
     signature = {
@@ -366,7 +366,7 @@ class SinopeDM2500ZB(SinopeTechnologieslight):
     device_automation_triggers = LIGHT_DEVICE_TRIGGERS
 
 
-class SinopeDM2550ZB(SinopeTechnologieslight):
+class SinopeDM2550ZB(CustomDevice):
     """DM2550ZB, DM2550ZB-G2 Dimmers."""
 
     signature = {
diff --git a/zhaquirks/sinope/thermostat.py b/zhaquirks/sinope/thermostat.py
index 401015f..3a0d351 100644
--- a/zhaquirks/sinope/thermostat.py
+++ b/zhaquirks/sinope/thermostat.py
@@ -1,4 +1,4 @@
-"""Module to handle quirks of the  Sinopé Technologies thermostat.
+"""Module to handle quirks of the Sinopé Technologies thermostats.
 
 Manufacturer specific cluster implements attributes to control displaying
 of outdoor temperature, setting occupancy on/off and setting device time.
@@ -14,13 +14,14 @@ from zigpy.zcl.clusters.general import (
     Basic,
     Groups,
     Identify,
+    LevelControl,
     Ota,
     PowerConfiguration,
     Scenes,
     Time,
 )
 from zigpy.zcl.clusters.homeautomation import Diagnostic, ElectricalMeasurement
-from zigpy.zcl.clusters.hvac import Thermostat, UserInterface
+from zigpy.zcl.clusters.hvac import Fan, Thermostat, UserInterface
 from zigpy.zcl.clusters.measurement import TemperatureMeasurement
 from zigpy.zcl.clusters.smartenergy import Metering
 from zigpy.zcl.foundation import Array
@@ -401,7 +402,7 @@ class SinopeTechnologiesThermostat(CustomDevice):
     }
 
 
-class SinopeTH1400ZB(SinopeTechnologiesThermostat):
+class SinopeTH1400ZB(CustomDevice):
     """TH1400ZB thermostat."""
 
     signature = {
@@ -459,7 +460,7 @@ class SinopeTH1400ZB(SinopeTechnologiesThermostat):
     }
 
 
-class SinopeTH1300ZB(SinopeTechnologiesThermostat):
+class SinopeTH1300ZB(CustomDevice):
     """TH1300ZB thermostat."""
 
     signature = {
@@ -519,7 +520,7 @@ class SinopeTH1300ZB(SinopeTechnologiesThermostat):
     }
 
 
-class SinopeLineThermostats(SinopeTechnologiesThermostat):
+class SinopeLineThermostats(CustomDevice):
     """TH1123ZB, TH1124ZB, TH1500ZB and OTH3600-GA-ZB thermostats."""
 
     signature = {
@@ -584,7 +585,7 @@ class SinopeLineThermostats(SinopeTechnologiesThermostat):
     }
 
 
-class SinopeG2Thermostats(SinopeTechnologiesThermostat):
+class SinopeG2Thermostats(CustomDevice):
     """TH1123ZB-G2 and TH1124ZB-G2 thermostats."""
 
     signature = {
@@ -645,3 +646,93 @@ class SinopeG2Thermostats(SinopeTechnologiesThermostat):
             }
         }
     }
+
+
+class SinopeHPThermostats(CustomDevice):
+    """HP6000ZB-GE, HP6000ZB-HS and HP6000ZB-MA thermostats."""
+
+    signature = {
+        # <SimpleDescriptor endpoint=1 profile=260 device_type=775 device_version=1
+        # input_clusters=[0, 3, 4, 5, 8, 513, 514, 516, 1026, 2821, 65281]
+        # output_clusters=[25]>
+        MODELS_INFO: [
+            (SINOPE, "HP6000ZB-GE"),
+            (SINOPE, "HP6000ZB-HS"),
+            (SINOPE, "HP6000ZB-MA"),
+        ],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha_p.PROFILE_ID,
+                DEVICE_TYPE: zha_p.DeviceType.MINI_SPLIT_AC,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    LevelControl.cluster_id,
+                    Thermostat.cluster_id,
+                    Fan.cluster_id,
+                    UserInterface.cluster_id,
+                    TemperatureMeasurement.cluster_id,
+                    Diagnostic.cluster_id,
+                    SINOPE_MANUFACTURER_CLUSTER_ID,
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id],
+            },
+            2: {
+                PROFILE_ID: zha_p.PROFILE_ID,
+                DEVICE_TYPE: zha_p.DeviceType.MINI_SPLIT_AC,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    LevelControl.cluster_id,
+                    Thermostat.cluster_id,
+                    Fan.cluster_id,
+                    UserInterface.cluster_id,
+                    TemperatureMeasurement.cluster_id,
+                    Diagnostic.cluster_id,
+                    SINOPE_MANUFACTURER_CLUSTER_ID,
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id],
+            },
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    LevelControl.cluster_id,
+                    Fan.cluster_id,
+                    UserInterface.cluster_id,
+                    TemperatureMeasurement.cluster_id,
+                    Diagnostic.cluster_id,
+                    SinopeTechnologiesThermostatCluster,
+                    SinopeTechnologiesManufacturerCluster,
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id],
+            },
+            2: {
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    LevelControl.cluster_id,
+                    Fan.cluster_id,
+                    UserInterface.cluster_id,
+                    TemperatureMeasurement.cluster_id,
+                    Diagnostic.cluster_id,
+                    SinopeTechnologiesThermostatCluster,
+                    SinopeTechnologiesManufacturerCluster,
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id],
+            },
+        }
+    }
diff --git a/zhaquirks/sonoff/trvzb.py b/zhaquirks/sonoff/trvzb.py
new file mode 100644
index 0000000..1c01f9a
--- /dev/null
+++ b/zhaquirks/sonoff/trvzb.py
@@ -0,0 +1,125 @@
+"""Sonoff TRVZB - Zigbee Thermostatic Radiator Valve."""
+
+from zigpy.quirks import CustomCluster
+from zigpy.quirks.v2 import QuirkBuilder
+from zigpy.quirks.v2.homeassistant import UnitOfTemperature
+import zigpy.types as t
+from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef
+
+
+class CustomSonoffCluster(CustomCluster):
+    """Custom Sonoff cluster."""
+
+    cluster_id = 0xFC11
+
+    class AttributeDefs(BaseAttributeDefs):
+        """Attribute definitions."""
+
+        child_lock = ZCLAttributeDef(
+            id=0x0000,
+            type=t.Bool,
+        )
+
+        open_window = ZCLAttributeDef(
+            id=0x6000,
+            type=t.Bool,
+        )
+
+        frost_protection_temperature = ZCLAttributeDef(
+            id=0x6002,
+            type=t.int16s,
+        )
+
+        idle_steps = ZCLAttributeDef(
+            id=0x6003,
+            type=t.uint16_t,
+            access="r",
+        )
+
+        closing_steps = ZCLAttributeDef(
+            id=0x6004,
+            type=t.uint16_t,
+            access="r",
+        )
+
+        valve_opening_limit_voltage = ZCLAttributeDef(
+            id=0x6005,
+            type=t.uint16_t,
+            access="r",
+        )
+
+        valve_closing_limit_voltage = ZCLAttributeDef(
+            id=0x6006,
+            type=t.uint16_t,
+            access="r",
+        )
+
+        valve_motor_running_voltage = ZCLAttributeDef(
+            id=0x6007,
+            type=t.uint16_t,
+            access="r",
+        )
+
+        valve_opening_degree = ZCLAttributeDef(
+            id=0x600B,
+            type=t.uint8_t,
+        )
+
+        valve_closing_degree = ZCLAttributeDef(
+            id=0x600C,
+            type=t.uint8_t,
+        )
+
+    @property
+    def _is_manuf_specific(self):
+        return False
+
+
+(
+    QuirkBuilder("SONOFF", "TRVZB")
+    .replaces(CustomSonoffCluster)
+    .switch(
+        CustomSonoffCluster.AttributeDefs.child_lock.name,
+        CustomSonoffCluster.cluster_id,
+        translation_key="child_lock",
+        fallback_name="Child lock",
+    )
+    .switch(
+        CustomSonoffCluster.AttributeDefs.open_window.name,
+        CustomSonoffCluster.cluster_id,
+        translation_key="open_window",
+        fallback_name="Open window",
+    )
+    .number(
+        CustomSonoffCluster.AttributeDefs.frost_protection_temperature.name,
+        CustomSonoffCluster.cluster_id,
+        min_value=4.0,
+        max_value=35.0,
+        step=0.5,
+        unit=UnitOfTemperature.CELSIUS,
+        multiplier=0.01,
+        translation_key="frost_protection_temperature",
+        fallback_name="Frost protection temperature",
+    )
+    .number(
+        CustomSonoffCluster.AttributeDefs.valve_opening_degree.name,
+        CustomSonoffCluster.cluster_id,
+        min_value=0.0,
+        max_value=100.0,
+        step=1.0,
+        translation_key="valve_opening_degree",
+        fallback_name="Valve opening degree",
+        initially_disabled=True,
+    )
+    .number(
+        CustomSonoffCluster.AttributeDefs.valve_closing_degree.name,
+        CustomSonoffCluster.cluster_id,
+        min_value=0.0,
+        max_value=100.0,
+        step=1.0,
+        translation_key="valve_closing_degree",
+        fallback_name="Valve closing degree",
+        initially_disabled=True,
+    )
+    .add_to_registry()
+)
diff --git a/zhaquirks/sonoff/zbminir2.py b/zhaquirks/sonoff/zbminir2.py
new file mode 100644
index 0000000..77ab5bb
--- /dev/null
+++ b/zhaquirks/sonoff/zbminir2.py
@@ -0,0 +1,88 @@
+"""Sonoff ZBMINIR2 - Zigbee Switch."""
+
+from zigpy import types
+from zigpy.quirks import CustomCluster
+from zigpy.quirks.v2 import QuirkBuilder
+import zigpy.types as t
+from zigpy.zcl import foundation
+from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef
+
+
+class SonoffCluster(CustomCluster):
+    """Custom Sonoff cluster."""
+
+    cluster_id = 0xFC11
+
+    class AttributeDefs(BaseAttributeDefs):
+        """Attribute definitions."""
+
+        external_trigger_mode = ZCLAttributeDef(
+            id=0x0016,
+            type=t.uint8_t,
+        )
+        detach_relay = ZCLAttributeDef(
+            id=0x0017,
+            type=t.Bool,
+        )
+        turbo_mode = ZCLAttributeDef(
+            id=0x0012,
+            type=t.int16s,
+        )
+
+    async def _read_attributes(
+        self,
+        attribute_ids: list[t.uint16_t],
+        *args,
+        manufacturer: int | t.uint16_t | None = None,
+        **kwargs,
+    ):
+        """Read attributes ZCL foundation command."""
+        return await super()._read_attributes(
+            attribute_ids,
+            *args,
+            manufacturer=foundation.ZCLHeader.NO_MANUFACTURER_ID,
+            **kwargs,
+        )
+
+    @property
+    def _is_manuf_specific(self):
+        return False
+
+
+class SonoffExternalSwitchTriggerType(types.enum8):
+    """extern switch trigger type."""
+
+    Edge_trigger = 0x00
+    Pulse_trigger = 0x01
+    Normally_off_follow_trigger = 0x02
+    Normally_on_follow_trigger = 0x82
+
+
+(
+    QuirkBuilder("SONOFF", "ZBMINIR2")
+    .replaces(SonoffCluster)
+    .enum(
+        SonoffCluster.AttributeDefs.external_trigger_mode.name,
+        SonoffExternalSwitchTriggerType,
+        SonoffCluster.cluster_id,
+        translation_key="external_trigger_mode",
+        fallback_name="External trigger mode",
+    )
+    .switch(
+        SonoffCluster.AttributeDefs.turbo_mode.name,
+        SonoffCluster.cluster_id,
+        off_value=9,
+        on_value=20,
+        translation_key="turbo_mode",
+        fallback_name="Turbo mode",
+    )
+    .switch(
+        SonoffCluster.AttributeDefs.detach_relay.name,
+        SonoffCluster.cluster_id,
+        off_value=0,
+        on_value=1,
+        translation_key="detach_relay",
+        fallback_name="Detach relay",
+    )
+    .add_to_registry()
+)
diff --git a/zhaquirks/tuya/air/ts0601_air_quality.py b/zhaquirks/tuya/air/ts0601_air_quality.py
index 8a56667..ff528c1 100644
--- a/zhaquirks/tuya/air/ts0601_air_quality.py
+++ b/zhaquirks/tuya/air/ts0601_air_quality.py
@@ -37,6 +37,7 @@ class TuyaCO2Sensor(CustomDevice):
             ("_TZE200_yvx5lh6k", "TS0601"),
             ("_TZE204_yvx5lh6k", "TS0601"),
             ("_TZE200_c2fmom5z", "TS0601"),
+            ("_TZE204_c2fmom5z", "TS0601"),
         ],
         ENDPOINTS: {
             1: {
@@ -89,6 +90,7 @@ class TuyaCO2SensorGPP(CustomDevice):
             ("_TZE200_yvx5lh6k", "TS0601"),
             ("_TZE204_yvx5lh6k", "TS0601"),
             ("_TZE200_c2fmom5z", "TS0601"),
+            ("_TZE204_c2fmom5z", "TS0601"),
         ],
         ENDPOINTS: {
             1: {
@@ -152,6 +154,7 @@ class TuyaNDIRCO2SensorGPP(CustomDevice):
         # output_clusters=[25, 10])
         MODELS_INFO: [
             ("_TZE200_ogkdpgy2", "TS0601"),
+            ("_TZE204_ogkdpgy2", "TS0601"),
         ],
         ENDPOINTS: {
             1: {
diff --git a/zhaquirks/tuya/ts0601_cover.py b/zhaquirks/tuya/ts0601_cover.py
index 57f76a4..6bf8e2a 100644
--- a/zhaquirks/tuya/ts0601_cover.py
+++ b/zhaquirks/tuya/ts0601_cover.py
@@ -363,6 +363,7 @@ class TuyaMoesCover0601(TuyaWindowCover):
         # "class": "zigpy.device.Device"
         # }
         MODELS_INFO: [
+            ("_TZE200_vdiuwbkq", "TS0601"),
             ("_TZE200_zah67ekd", "TS0601"),
             ("_TZE200_nueqqe6k", "TS0601"),
             ("_TZE200_gubdgai2", "TS0601"),
diff --git a/zhaquirks/tuya/ts0601_illuminance.py b/zhaquirks/tuya/ts0601_illuminance.py
index 32505d2..0a298bf 100644
--- a/zhaquirks/tuya/ts0601_illuminance.py
+++ b/zhaquirks/tuya/ts0601_illuminance.py
@@ -72,6 +72,7 @@ class TuyaIlluminance(CustomDevice):
         #  input_clusters=[4, 5, 61184, 0], output_clusters=[25, 10])
         MODELS_INFO: [
             ("_TZE200_khx7nnka", "TS0601"),
+            ("_TZE204_khx7nnka", "TS0601"),
             ("_TZE200_yi4jtqq1", "TS0601"),
         ],
         ENDPOINTS: {
diff --git a/zhaquirks/tuya/ts0601_sensor.py b/zhaquirks/tuya/ts0601_sensor.py
index b9c538d..66113fd 100644
--- a/zhaquirks/tuya/ts0601_sensor.py
+++ b/zhaquirks/tuya/ts0601_sensor.py
@@ -1,5 +1,10 @@
 """Tuya temp and humidity sensors."""
 
+from zigpy.quirks.v2.homeassistant import EntityType
+from zigpy.quirks.v2.homeassistant.binary_sensor import BinarySensorDeviceClass
+from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass, SensorStateClass
+import zigpy.types as t
+
 from zhaquirks.tuya.builder import TuyaPowerConfigurationCluster2AAA, TuyaQuirkBuilder
 
 (
@@ -18,6 +23,7 @@ from zhaquirks.tuya.builder import TuyaPowerConfigurationCluster2AAA, TuyaQuirkB
     .applies_to("_TZE200_znbl8dj5", "TS0601")
     .applies_to("_TZE200_qyflbnbj", "TS0601")
     .applies_to("_TZE200_zppcgbdj", "TS0601")
+    .applies_to("_TZE204_s139roas", "TS0601")
     .tuya_temperature(dp_id=1, scale=10)
     .tuya_humidity(dp_id=2)
     .tuya_battery(dp_id=4)
@@ -29,6 +35,7 @@ from zhaquirks.tuya.builder import TuyaPowerConfigurationCluster2AAA, TuyaQuirkB
 (
     TuyaQuirkBuilder("_TZE200_yjjdcqsq", "TS0601")
     .applies_to("_TZE200_9yapgbuv", "TS0601")
+    .applies_to("_TZE204_9yapgbuv", "TS0601")
     .applies_to("_TZE204_yjjdcqsq", "TS0601")
     .applies_to("_TZE200_utkemkbs", "TS0601")
     .applies_to("_TZE204_utkemkbs", "TS0601")
@@ -71,3 +78,26 @@ from zhaquirks.tuya.builder import TuyaPowerConfigurationCluster2AAA, TuyaQuirkB
     .skip_configuration()
     .add_to_registry()
 )
+
+(
+    TuyaQuirkBuilder("_TZE200_pay2byax", "TS0601")  # Cusam ZG-102ZL
+    .applies_to("_TZE200_n8dljorx", "TS0601")
+    .tuya_sensor(
+        dp_id=101,
+        attribute_name="measured_value",
+        type=t.uint16_t,
+        fallback_name="Illuminance",
+        device_class=SensorDeviceClass.ILLUMINANCE,
+        state_class=SensorStateClass.MEASUREMENT,
+    )
+    .tuya_binary_sensor(
+        dp_id=1,
+        attribute_name="zone_status",
+        device_class=BinarySensorDeviceClass.OPENING,
+        fallback_name="Opening",
+        entity_type=EntityType.STANDARD,
+    )
+    .tuya_battery(dp_id=2)
+    .skip_configuration()
+    .add_to_registry()
+)


Test patch:
diff --git a/tests/test_schneiderelectric.py b/tests/test_schneiderelectric.py
index 6695114..0bf8551 100644
--- a/tests/test_schneiderelectric.py
+++ b/tests/test_schneiderelectric.py
@@ -6,7 +6,6 @@ from zigpy.zcl import foundation
 from zigpy.zcl.clusters.closures import WindowCovering
 
 from tests.common import ClusterListener
-import zhaquirks.schneiderelectric.dimmers
 import zhaquirks.schneiderelectric.shutters
 
 zhaquirks.setup()
@@ -136,62 +135,3 @@ async def test_1gang_shutter_1_lift_percentage_updates(zigpy_device_from_quirk):
         23,  # 100 - 77
     )
     assert len(cluster_listener.cluster_commands) == 0
-
-
-def test_nh_rotary_dimmer_1_signature(assert_signature_matches_quirk):
-    """Test signature."""
-    signature = {
-        "node_descriptor": (
-            "NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, "
-            "user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, "
-            "mac_capability_flags=<MACCapabilityFlags.FullFunctionDevice|MainsPowered|RxOnWhenIdle|"
-            "AllocateAddress: 142>, manufacturer_code=4190, maximum_buffer_size=82, maximum_incoming_transfer_size=82, "
-            "server_mask=11264, maximum_outgoing_transfer_size=82, "
-            "descriptor_capability_field=<DescriptorCapability.NONE: 0>, *allocate_address=True, "
-            "*is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, "
-            "*is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, "
-            "*is_security_capable=False)"
-        ),
-        "endpoints": {
-            "3": {
-                "profile_id": 0x0104,
-                "device_type": "0x0101",
-                "in_clusters": [
-                    "0x0000",
-                    "0x0003",
-                    "0x0004",
-                    "0x0005",
-                    "0x0006",
-                    "0x0008",
-                    "0x0301",
-                    "0x0b05",
-                ],
-                "out_clusters": ["0x0019"],
-            },
-            "21": {
-                "profile_id": 0x0104,
-                "device_type": "0x0104",
-                "in_clusters": ["0x0000", "0x0003", "0x0b05", "0xff17"],
-                "out_clusters": [
-                    "0x0003",
-                    "0x0004",
-                    "0x0005",
-                    "0x0006",
-                    "0x0008",
-                    "0x0102",
-                ],
-            },
-            "242": {
-                "profile_id": 0xA1E0,
-                "device_type": "0x0061",
-                "in_clusters": [],
-                "out_clusters": ["0x0021"],
-            },
-        },
-        "manufacturer": "Schneider Electric",
-        "model": "NHROTARY/DIMMER/1",
-        "class": "zigpy.device.Device",
-    }
-    assert_signature_matches_quirk(
-        zhaquirks.schneiderelectric.dimmers.NHRotaryDimmer1, signature
-    )
diff --git a/tests/test_sinope.py b/tests/test_sinope.py
index 3f377a8..72b3e69 100644
--- a/tests/test_sinope.py
+++ b/tests/test_sinope.py
@@ -239,3 +239,18 @@ async def test_sinope_light_switch_reporting(zigpy_device_from_quirk, quirk):
 
         assert len(request_mock.mock_calls) == 1
         assert len(bind_mock.mock_calls) == 1
+
+
+@pytest.mark.parametrize("quirk", (SinopeTechnologieslight,))
+async def test_sinope_light_device_triggers_def(zigpy_device_from_quirk, quirk):
+    """Test device automation triggers.
+
+    Make sure that values are actual ints and not instances of an enum class.
+    """
+
+    device: Device = zigpy_device_from_quirk(quirk)
+
+    for config in device.device_automation_triggers.values():
+        val = config.get("args", {}).get("value")
+        if val is not None:
+            assert type(val) is int, type(val)