Repository: zigpy/zha-device-handlers
Pull request #2822 (2023-12-09 18:32:14)

Add quirk for Aqara P1 contact sensor
## Proposed change
<!--
  Explain your proposed change below.
-->
Title says it all. Fixes: #2349, #1295

TODO:
- [ ] Add config entity for detection distance to ZHA

## 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.
-->

- [ ] The changes are tested and work correctly
- [ ] `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/2822?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:
> Comparison is base [(`aeca76d`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/commit/aeca76da0ea81ecba5b17e0f7314620cd686faae?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) 87.35% compared to head [(`4c6bc0b`)](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/2822?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=zigpy) 87.37%.


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


```diff
@@            Coverage Diff             @@
##              dev    #2822      +/-   ##
==========================================
+ Coverage   87.35%   87.37%   +0.02%     
==========================================
  Files         287      288       +1     
  Lines        8835     8850      +15     
==========================================
+ Hits         7718     7733      +15     
  Misses       1117     1117              
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/zigpy/zha-device-handlers/pull/2822?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/zhaquirks/ikea/somrigsmartbtn.py b/zhaquirks/ikea/somrigsmartbtn.py
new file mode 100644
index 0000000..412249c
--- /dev/null
+++ b/zhaquirks/ikea/somrigsmartbtn.py
@@ -0,0 +1,165 @@
+"""Device handler for IKEA of Sweden SOMRIG shortcut button"""
+from zigpy.profiles import zha
+from zigpy.quirks import CustomDevice
+from zigpy.zcl.clusters.general import (
+    Basic,
+    Groups,
+    Identify,
+    LevelControl,
+    OnOff,
+    Ota,
+    PollControl,
+    PowerConfiguration,
+)
+from zigpy.zcl.clusters.lightlink import LightLink
+
+from zhaquirks.const import (
+    BUTTON_1,
+    BUTTON_2,
+    COMMAND,
+    COMMAND_M_INITIAL_PRESS,
+    COMMAND_M_LONG_PRESS,
+    COMMAND_M_LONG_RELEASE,
+    COMMAND_M_MULTI_PRESS_COMPLETE,
+    COMMAND_M_SHORT_RELEASE,
+    DEVICE_TYPE,
+    DOUBLE_PRESS,
+    ENDPOINT_ID,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    LONG_PRESS,
+    LONG_RELEASE,
+    MODELS_INFO,
+    OUTPUT_CLUSTERS,
+    PRESSED,
+    PROFILE_ID,
+    SHORT_PRESS,
+)
+from zhaquirks.ikea import (
+    IKEA,
+    IKEA_CLUSTER_ID,
+    PowerConfig1AAACluster,
+    ShortcutV2Cluster,
+)
+
+
+class IkeaSomrigSmartButton(CustomDevice):
+    """Custom device representing IKEA SOMRIG shortcut button"""
+
+    signature = {
+        # <SimpleDescriptor endpoint=1 profile=260, device_type=6
+        # device_version=1
+        # input_clusters=[0, 1, 3, 4, 32, 4096, 64636, 64640]
+        # output_clusters=[3, 4, 6, 8, 25, 4096, 64640]>
+        MODELS_INFO: [(IKEA, "SOMRIG shortcut button")],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    PollControl.cluster_id,
+                    LightLink.cluster_id,
+                    IKEA_CLUSTER_ID,
+                    ShortcutV2Cluster.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    OnOff.cluster_id,
+                    LevelControl.cluster_id,
+                    Ota.cluster_id,
+                    LightLink.cluster_id,
+                    ShortcutV2Cluster.cluster_id,
+                ],
+            },
+            # <SimpleDescriptor endpoint=2  profile=260  device_type=6
+            # device_version=1
+            # input_clusters=[0, 3, 4, 64640]
+            # output_clusters=[3, 4, 6, 8, 64640]>
+            2: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    ShortcutV2Cluster.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    OnOff.cluster_id,
+                    LevelControl.cluster_id,
+                    ShortcutV2Cluster.cluster_id,
+                ],
+            },
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfig1AAACluster,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    PollControl.cluster_id,
+                    LightLink.cluster_id,
+                    IKEA_CLUSTER_ID,
+                    ShortcutV2Cluster,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    OnOff.cluster_id,
+                    LevelControl.cluster_id,
+                    Ota.cluster_id,
+                    LightLink.cluster_id,
+                    ShortcutV2Cluster,
+                ],
+            },
+            2: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    ShortcutV2Cluster,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    OnOff.cluster_id,
+                    LevelControl.cluster_id,
+                    ShortcutV2Cluster,
+                ],
+            },
+        },
+    }
+
+    device_automation_triggers = {
+        (PRESSED, BUTTON_1): {ENDPOINT_ID: 1, COMMAND: COMMAND_M_INITIAL_PRESS},
+        (SHORT_PRESS, BUTTON_1): {ENDPOINT_ID: 1, COMMAND: COMMAND_M_SHORT_RELEASE},
+        (DOUBLE_PRESS, BUTTON_1): {
+            ENDPOINT_ID: 1,
+            COMMAND: COMMAND_M_MULTI_PRESS_COMPLETE,
+        },
+        (LONG_PRESS, BUTTON_1): {ENDPOINT_ID: 1, COMMAND: COMMAND_M_LONG_PRESS},
+        (LONG_RELEASE, BUTTON_1): {ENDPOINT_ID: 1, COMMAND: COMMAND_M_LONG_RELEASE},
+        (PRESSED, BUTTON_2): {ENDPOINT_ID: 2, COMMAND: COMMAND_M_INITIAL_PRESS},
+        (SHORT_PRESS, BUTTON_2): {ENDPOINT_ID: 2, COMMAND: COMMAND_M_SHORT_RELEASE},
+        (DOUBLE_PRESS, BUTTON_2): {
+            ENDPOINT_ID: 2,
+            COMMAND: COMMAND_M_MULTI_PRESS_COMPLETE,
+        },
+        (LONG_PRESS, BUTTON_2): {ENDPOINT_ID: 2, COMMAND: COMMAND_M_LONG_PRESS},
+        (LONG_RELEASE, BUTTON_2): {ENDPOINT_ID: 2, COMMAND: COMMAND_M_LONG_RELEASE},
+    }
diff --git a/zhaquirks/inovelli/__init__.py b/zhaquirks/inovelli/__init__.py
index 23ab261..2d38756 100644
--- a/zhaquirks/inovelli/__init__.py
+++ b/zhaquirks/inovelli/__init__.py
@@ -85,12 +85,12 @@ class Inovelli_VZM31SN_Cluster(CustomCluster):
     attributes = {
         0x0001: ("dimming_speed_up_remote", t.uint8_t, True),
         0x0002: ("dimming_speed_up_local", t.uint8_t, True),
-        0x0003: ("ramp_rate_off_to_on_local", t.uint8_t, True),
-        0x0004: ("ramp_rate_off_to_on_remote", t.uint8_t, True),
+        0x0003: ("ramp_rate_off_to_on_remote", t.uint8_t, True),
+        0x0004: ("ramp_rate_off_to_on_local", t.uint8_t, True),
         0x0005: ("dimming_speed_down_remote", t.uint8_t, True),
         0x0006: ("dimming_speed_down_local", t.uint8_t, True),
-        0x0007: ("ramp_rate_on_to_off_local", t.uint8_t, True),
-        0x0008: ("ramp_rate_on_to_off_remote", t.uint8_t, True),
+        0x0007: ("ramp_rate_on_to_off_remote", t.uint8_t, True),
+        0x0008: ("ramp_rate_on_to_off_local", t.uint8_t, True),
         0x0009: ("minimum_level", t.uint8_t, True),
         0x000A: ("maximum_level", t.uint8_t, True),
         0x000B: ("invert_switch", t.Bool, True),
@@ -98,7 +98,6 @@ class Inovelli_VZM31SN_Cluster(CustomCluster):
         0x000D: ("default_level_local", t.uint8_t, True),
         0x000E: ("default_level_remote", t.uint8_t, True),
         0x000F: ("state_after_power_restored", t.uint8_t, True),
-        0x0010: ("disable_remote_control", t.uint8_t, True),
         0x0011: ("load_level_indicator_timeout", t.uint8_t, True),
         0x0012: ("active_power_reports", t.uint8_t, True),
         0x0013: ("periodic_power_and_energy_reports", t.uint8_t, True),
@@ -106,6 +105,9 @@ class Inovelli_VZM31SN_Cluster(CustomCluster):
         0x0015: ("power_type", t.uint8_t, True),
         0x0016: ("switch_type", t.uint8_t, True),
         0x0019: ("increased_non_neutral_output", t.Bool, True),
+        0x001A: ("leading_or_trailing_edge", t.Bool, True),
+        0x0020: ("internal_temp_monitor", t.uint8_t, True),
+        0x0021: ("overheated", t.Bool, True),
         0x0032: ("button_delay", t.uint8_t, True),
         0x0033: ("device_bind_number", t.uint8_t, True),
         0x0034: ("smart_bulb_mode", t.Bool, True),
@@ -248,6 +250,7 @@ VZM35SN_REMOVES = [
     0x0013,
     0x0014,
     0x0019,
+    0x001A,
     0x0034,
     0x0064,
     0x007D,
diff --git a/zhaquirks/sinope/light.py b/zhaquirks/sinope/light.py
index 75e5f15..d4d672c 100644
--- a/zhaquirks/sinope/light.py
+++ b/zhaquirks/sinope/light.py
@@ -88,6 +88,7 @@ class SinopeTechnologiesManufacturerCluster(CustomCluster):
         0x0055: ("min_intensity", t.uint16_t, True),
         0x0056: ("phase_control", PhaseControl, True),
         0x0058: ("double_up_full", DoubleFull, True),
+        0x0090: ("current_summation_delivered", t.uint32_t, True),
         0x00A0: ("timer", t.uint32_t, True),
         0x00A1: ("timer_countdown", t.uint32_t, True),
         0x0119: ("connected_load", t.uint16_t, True),
diff --git a/zhaquirks/sinope/sensor.py b/zhaquirks/sinope/sensor.py
index 0d7f0a6..0ed68f5 100644
--- a/zhaquirks/sinope/sensor.py
+++ b/zhaquirks/sinope/sensor.py
@@ -39,6 +39,9 @@ class SinopeManufacturerCluster(CustomCluster):
     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),
         0x0200: ("status", t.bitmap32, True),
         0xFFFD: ("cluster_revision", t.uint16_t, True),
     }
diff --git a/zhaquirks/sinope/switch.py b/zhaquirks/sinope/switch.py
index 79967a1..c046b6d 100644
--- a/zhaquirks/sinope/switch.py
+++ b/zhaquirks/sinope/switch.py
@@ -97,27 +97,33 @@ class SinopeManufacturerCluster(CustomCluster):
         Active = 0x00
         Off = 0x01
 
-    class TankSize(t.enum8):
-        """tank_size values."""
-
-        Gal_40 = 0x01
-        Gal_50 = 0x02
-        Gal_60 = 0x03
-        Gal_80 = 0x04
-
     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
-        M_45 = 0x0A8C
-        M_60 = 0x0E10
-        M_75 = 0x1194
-        M_90 = 0x1518
+        H_1 = 0x0E10
+        H_2 = 0x1C20
         H_3 = 0x2A30
-        H_6 = 0x5460
-        H_12 = 0xA8C0
-        H_24 = 0x15180
 
     cluster_id = SINOPE_MANUFACTURER_CLUSTER_ID
     name = "Sinopé Manufacturer specific"
@@ -127,12 +133,14 @@ class SinopeManufacturerCluster(CustomCluster):
         0x0003: ("firmware_number", t.uint16_t, True),
         0x0004: ("firmware_version", t.CharacterString, True),
         0x0010: ("outdoor_temp", t.int16s, True),
-        0x0013: ("tank_size", TankSize, 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),
@@ -145,7 +153,11 @@ class SinopeManufacturerCluster(CustomCluster):
         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),
     }
 
@@ -157,6 +169,7 @@ class CustomBasicCluster(CustomCluster, Basic):
         """Power source."""
 
         Unknown = 0x0000
+        DC_mains = 0x0001
         Battery = 0x0003
         DC_source = 0x0004
         ACUPS_01 = 0x0081
@@ -585,16 +598,8 @@ class SinopeTechnologiesCalypso(CustomDevice):
                     Time.cluster_id,
                     Ota.cluster_id,
                 ],
-            },
-            2: {
-                PROFILE_ID: zha_p.PROFILE_ID,
-                DEVICE_TYPE: zha_p.DeviceType.ON_OFF_OUTPUT,
-                INPUT_CLUSTERS: [
-                    TemperatureMeasurement.cluster_id,
-                ],
-                OUTPUT_CLUSTERS: [],
-            },
-        },
+            }
+        }
     }
 
 
diff --git a/zhaquirks/sinope/thermostat.py b/zhaquirks/sinope/thermostat.py
index b9f2293..1ec03ee 100644
--- a/zhaquirks/sinope/thermostat.py
+++ b/zhaquirks/sinope/thermostat.py
@@ -20,6 +20,7 @@ from zigpy.zcl.clusters.homeautomation import Diagnostic, ElectricalMeasurement
 from zigpy.zcl.clusters.hvac import Thermostat, UserInterface
 from zigpy.zcl.clusters.measurement import TemperatureMeasurement
 from zigpy.zcl.clusters.smartenergy import Metering
+from zigpy.zcl.foundation import Array
 
 from zhaquirks.const import (
     DEVICE_TYPE,
@@ -122,9 +123,10 @@ class SinopeTechnologiesManufacturerCluster(CustomCluster):
         0x0012: ("config_2nd_display", Display, True),
         0x0020: ("secs_since_2k", t.uint32_t, True),
         0x0070: ("current_load", t.bitmap8, True),
-        0x0071: ("eco_mode", t.int8s, True),
-        0x0072: ("eco_mode_1", t.uint8_t, True),
-        0x0073: ("eco_mode_2", t.uint8_t, 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),
@@ -174,6 +176,7 @@ class SinopeTechnologiesThermostatCluster(CustomCluster, Thermostat):
         Min_20 = 0x04B0
         Min_25 = 0x05DC
         Min_30 = 0x0708
+        Off = 0xFFFF
 
     attributes = Thermostat.attributes.copy()
     attributes.update(
diff --git a/zhaquirks/sonoff/snzb01p.py b/zhaquirks/sonoff/snzb01p.py
new file mode 100644
index 0000000..5248f09
--- /dev/null
+++ b/zhaquirks/sonoff/snzb01p.py
@@ -0,0 +1,91 @@
+"""Sonoff Smart Button SNZB-01P"""
+from zigpy.profiles import zha
+from zigpy.quirks import CustomDevice
+from zigpy.zcl.clusters.general import (
+    Basic,
+    Identify,
+    OnOff,
+    Ota,
+    PollControl,
+    PowerConfiguration,
+)
+
+from zhaquirks.const import (
+    BUTTON,
+    CLUSTER_ID,
+    COMMAND,
+    COMMAND_OFF,
+    COMMAND_ON,
+    COMMAND_TOGGLE,
+    DEVICE_TYPE,
+    DOUBLE_PRESS,
+    ENDPOINT_ID,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    LONG_PRESS,
+    MODELS_INFO,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+    SHORT_PRESS,
+)
+
+SONOFF_CLUSTER_ID = 0xFC57
+
+
+class SonoffSmartButtonSNZB01P(CustomDevice):
+    """Sonoff smart button remote - model SNZB-01P"""
+
+    signature = {
+        #  <SimpleDescriptor endpoint=1 profile=260 device_type=0
+        #  device_version=1
+        #  input_clusters=[0, 1, 3, 32, 64599]
+        #  output_clusters=[3, 6, 25]>
+        MODELS_INFO: [
+            ("eWeLink", "SNZB-01P"),
+        ],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    Identify.cluster_id,
+                    PollControl.cluster_id,
+                    SONOFF_CLUSTER_ID,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Identify.cluster_id,
+                    OnOff.cluster_id,
+                    Ota.cluster_id,
+                ],
+            },
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    Identify.cluster_id,
+                    PollControl.cluster_id,
+                    SONOFF_CLUSTER_ID,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Identify.cluster_id,
+                    OnOff.cluster_id,
+                    Ota.cluster_id,
+                ],
+            },
+        },
+    }
+
+    device_automation_triggers = {
+        (SHORT_PRESS, BUTTON): {COMMAND: COMMAND_TOGGLE, CLUSTER_ID: 6, ENDPOINT_ID: 1},
+        (DOUBLE_PRESS, BUTTON): {COMMAND: COMMAND_ON, CLUSTER_ID: 6, ENDPOINT_ID: 1},
+        (LONG_PRESS, BUTTON): {COMMAND: COMMAND_OFF, CLUSTER_ID: 6, ENDPOINT_ID: 1},
+    }
diff --git a/zhaquirks/tuya/air/__init__.py b/zhaquirks/tuya/air/__init__.py
index debbe4b..05d1437 100644
--- a/zhaquirks/tuya/air/__init__.py
+++ b/zhaquirks/tuya/air/__init__.py
@@ -4,6 +4,7 @@ from typing import Any, Dict
 
 import zigpy.types as t
 from zigpy.zcl.clusters.measurement import (
+    PM25,
     CarbonDioxideConcentration,
     FormaldehydeConcentration,
     RelativeHumidity,
@@ -67,6 +68,10 @@ class TuyaAirQualityHumidity(RelativeHumidity, TuyaLocalCluster):
     """Tuya relative humidity measurement."""
 
 
+class TuyaAirQualityPM25(PM25, TuyaLocalCluster):
+    """Tuya PM25 concentration measurement"""
+
+
 class TuyaAirQualityCO2(CarbonDioxideConcentration, TuyaLocalCluster):
     """Tuya Carbon Dioxide concentration measurement."""
 
@@ -92,6 +97,9 @@ class TuyaCO2ManufCluster(TuyaNewManufCluster):
         19: DPToAttributeMapping(
             TuyaAirQualityHumidity.ep_attribute, "measured_value", lambda x: x * 10
         ),
+        20: DPToAttributeMapping(
+            TuyaAirQualityPM25.ep_attribute, "measured_value", lambda x: x
+        ),
         21: DPToAttributeMapping(
             TuyaAirQualityVOC.ep_attribute, "measured_value", lambda x: x * 1e-6
         ),
@@ -106,6 +114,7 @@ class TuyaCO2ManufCluster(TuyaNewManufCluster):
         2: "_dp_2_attr_update",
         18: "_dp_2_attr_update",
         19: "_dp_2_attr_update",
+        20: "_dp_2_attr_update",
         21: "_dp_2_attr_update",
         22: "_dp_2_attr_update",
     }
diff --git a/zhaquirks/tuya/air/ts0601_air_quality.py b/zhaquirks/tuya/air/ts0601_air_quality.py
index 790f261..af80ab3 100644
--- a/zhaquirks/tuya/air/ts0601_air_quality.py
+++ b/zhaquirks/tuya/air/ts0601_air_quality.py
@@ -33,8 +33,9 @@ class TuyaCO2Sensor(CustomDevice):
         # output_clusters=[25, 10])
         MODELS_INFO: [
             ("_TZE200_8ygsuhe1", "TS0601"),
+            ("_TZE200_ryfmq5rl", "TS0601"),
             ("_TZE200_yvx5lh6k", "TS0601"),
-            ("_TZE200_dwcarsat", "TS0601"),
+            ("_TZE200_c2fmom5z", "TS0601"),
         ],
         ENDPOINTS: {
             1: {
@@ -82,10 +83,9 @@ class TuyaCO2SensorGPP(CustomDevice):
         # input_clusters=[0, 4, 5, 61184],
         # output_clusters=[25, 10])
         MODELS_INFO: [
-            ("_TZE200_mja3fuja", "TS0601"),
+            ("_TZE200_8ygsuhe1", "TS0601"),
             ("_TZE200_ryfmq5rl", "TS0601"),
             ("_TZE200_yvx5lh6k", "TS0601"),
-            ("_TZE200_dwcarsat", "TS0601"),
             ("_TZE200_c2fmom5z", "TS0601"),
         ],
         ENDPOINTS: {
diff --git a/zhaquirks/tuya/air/ts0601_smart_air.py b/zhaquirks/tuya/air/ts0601_smart_air.py
new file mode 100644
index 0000000..9ba04ce
--- /dev/null
+++ b/zhaquirks/tuya/air/ts0601_smart_air.py
@@ -0,0 +1,137 @@
+"""Tuya Smart Air House keeper."""
+
+from zigpy.profiles import zgp, zha
+from zigpy.quirks import CustomDevice
+from zigpy.zcl.clusters.general import Basic, GreenPowerProxy, Groups, Ota, Scenes, Time
+
+from zhaquirks.const import (
+    DEVICE_TYPE,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    MODELS_INFO,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+)
+from zhaquirks.tuya.air import (
+    TuyaAirQualityCO2,
+    TuyaAirQualityFormaldehyde,
+    TuyaAirQualityHumidity,
+    TuyaAirQualityPM25,
+    TuyaAirQualityTemperature,
+    TuyaAirQualityVOC,
+    TuyaCO2ManufCluster,
+)
+
+
+class TuyaSmartAirSensor(CustomDevice):
+    """Tuya Smart air house keeper device."""
+
+    signature = {
+        # 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=4417, maximum_buffer_size=66, maximum_incoming_transfer_size=66, server_mask=10752, maximum_outgoing_transfer_size=66, 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)
+        # SizePrefixedSimpleDescriptor(endpoint=1, profile=260, device_type=81, device_version=1,
+        # input_clusters=[0, 4, 5, 1026, 1029, 1037, 1067, 1070, 61184],
+        # output_clusters=[25, 10])
+        MODELS_INFO: [
+            ("_TZE200_mja3fuja", "TS0601"),
+            ("_TZE200_dwcarsat", "TS0601"),
+        ],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaCO2ManufCluster.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            }
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaCO2ManufCluster,
+                    TuyaAirQualityCO2,
+                    TuyaAirQualityFormaldehyde,
+                    TuyaAirQualityHumidity,
+                    TuyaAirQualityPM25,
+                    TuyaAirQualityTemperature,
+                    TuyaAirQualityVOC,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            }
+        }
+    }
+
+
+class TuyaSmartAirSensorGPP(CustomDevice):
+    """Tuya Air quality device with GPP."""
+
+    signature = {
+        # 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.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 142>, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=<DescriptorCapability.0: 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)]
+        # device_version=1
+        # SizePrefixedSimpleDescriptor(endpoint=1, profile=260, device_type=81, device_version=1,
+        # input_clusters=[0, 4, 5, 1026, 1029, 1037, 1067, 1070, 61184],
+        # output_clusters=[10, 25])
+        MODELS_INFO: [
+            ("_TZE200_mja3fuja", "TS0601"),
+            ("_TZE200_dwcarsat", "TS0601"),
+        ],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaCO2ManufCluster.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, 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: {
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaCO2ManufCluster,
+                    TuyaAirQualityCO2,
+                    TuyaAirQualityFormaldehyde,
+                    TuyaAirQualityHumidity,
+                    TuyaAirQualityPM25,
+                    TuyaAirQualityTemperature,
+                    TuyaAirQualityVOC,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, 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/tuya/ts011f_plug.py b/zhaquirks/tuya/ts011f_plug.py
index 2a643b8..28b1e88 100644
--- a/zhaquirks/tuya/ts011f_plug.py
+++ b/zhaquirks/tuya/ts011f_plug.py
@@ -1479,3 +1479,81 @@ class Plug_TZ3000_2AC(CustomDevice):
             },
         },
     }
+
+
+class Plug_TZ3000_2AC_var02(EnchantedDevice):
+    """Tuya 2 socket wall outlet."""
+
+    quirk_id = TUYA_PLUG_ONOFF
+
+    signature = {
+        MODEL: "TS011F",
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_PLUG_IN_UNIT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    OnOff.cluster_id,
+                    TuyaZBE000Cluster.cluster_id,
+                    TuyaZBExternalSwitchTypeCluster.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            },
+            2: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_PLUG_IN_UNIT,
+                INPUT_CLUSTERS: [
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    OnOff.cluster_id,
+                    TuyaZBExternalSwitchTypeCluster.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            242: {
+                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.ON_OFF_PLUG_IN_UNIT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaZBOnOffAttributeCluster,
+                    TuyaZBE000Cluster,
+                    TuyaZBExternalSwitchTypeCluster,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            },
+            2: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_PLUG_IN_UNIT,
+                INPUT_CLUSTERS: [
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    TuyaZBOnOffAttributeCluster,
+                    TuyaZBExternalSwitchTypeCluster,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            242: {
+                PROFILE_ID: zgp.PROFILE_ID,
+                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
+                INPUT_CLUSTERS: [],
+                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
+            },
+        },
+    }
diff --git a/zhaquirks/tuya/ts0501bs.py b/zhaquirks/tuya/ts0501bs.py
index 6399b0e..5fbfd60 100644
--- a/zhaquirks/tuya/ts0501bs.py
+++ b/zhaquirks/tuya/ts0501bs.py
@@ -37,6 +37,7 @@ class DimmableLedController(CustomDevice):
             ("_TZ3210_i680rtja", "TS0501B"),
             ("_TZ3210_dxroobu3", "TS0501B"),
             ("_TZ3210_dbilpfqk", "TS0501B"),
+            ("_TZ3210_agjx0pxt", "TS0501B"),
         ],
         ENDPOINTS: {
             # <SimpleDescriptor endpoint=1 profile=260 device_type=257
diff --git a/zhaquirks/tuya/ts0601_dimmer.py b/zhaquirks/tuya/ts0601_dimmer.py
index ed9afbf..bfa570f 100644
--- a/zhaquirks/tuya/ts0601_dimmer.py
+++ b/zhaquirks/tuya/ts0601_dimmer.py
@@ -148,6 +148,7 @@ class TuyaSingleSwitchDimmerGP(TuyaDimmerSwitch):
             ("_TZE200_3p5ydos3", "TS0601"),
             ("_TZE200_ip2akl4w", "TS0601"),
             ("_TZE200_vucankjx", "TS0601"),  # Loratap
+            ("_TZE200_y8yjulon", "TS0601"),
         ],
         ENDPOINTS: {
             # <SimpleDescriptor endpoint=1 profile=260 device_type=0x0100
diff --git a/zhaquirks/tuya/ts0601_garage.py b/zhaquirks/tuya/ts0601_garage.py
index d4a4fe4..5ce8bdd 100644
--- a/zhaquirks/tuya/ts0601_garage.py
+++ b/zhaquirks/tuya/ts0601_garage.py
@@ -90,6 +90,7 @@ class TuyaGarageSwitchTO(CustomDevice):
         MODELS_INFO: [
             ("_TZE200_nklqjk62", "TS0601"),
             ("_TZE200_wfxuhoea", "TS0601"),
+            ("_TZE204_nklqjk62", "TS0601"),
         ],
         ENDPOINTS: {
             # <SimpleDescriptor endpoint=1 profile=260 device_type=0x0051
diff --git a/zhaquirks/tuya/ts0601_gas.py b/zhaquirks/tuya/ts0601_gas.py
index d1e6885..5670426 100644
--- a/zhaquirks/tuya/ts0601_gas.py
+++ b/zhaquirks/tuya/ts0601_gas.py
@@ -79,7 +79,10 @@ class TuyaGasDetector0601(CustomDevice):
         super().__init__(*args, **kwargs)
 
     signature = {
-        MODELS_INFO: [("_TZE200_ggev5fsl", "TS0601")],
+        MODELS_INFO: [
+            ("_TZE200_ggev5fsl", "TS0601"),
+            ("_TZE200_rjxqso4a", "TS0601"),
+        ],
         ENDPOINTS: {
             1: {
                 PROFILE_ID: zigpy.profiles.zha.PROFILE_ID,
diff --git a/zhaquirks/tuya/ts0601_trv.py b/zhaquirks/tuya/ts0601_trv.py
index bdbe0d3..d480b9c 100644
--- a/zhaquirks/tuya/ts0601_trv.py
+++ b/zhaquirks/tuya/ts0601_trv.py
@@ -1536,6 +1536,7 @@ class MoesHY368_Type1(TuyaThermostat):
             ("_TZE200_9sfg7gm0", "TS0601"),
             ("_TZE200_8whxpsiw", "TS0601"),
             ("_TZE200_8thwkzxl", "TS0601"),  # Tervix Pro Line EVA 2
+            ("_TZE200_xby0s3ta", "TS0601"),  # Sandy Beach HY367
         ],
         ENDPOINTS: {
             1: {
@@ -1698,6 +1699,7 @@ class ZonnsmartTV01_ZG(TuyaThermostat):
             ("_TZE200_kds0pmmv", "TS0601"),  # MOES TV02
             ("_TZE200_sur6q7ko", "TS0601"),  # LSC Smart Connect 3012732
             ("_TZE200_lllliz3p", "TS0601"),  # tuya TV02-Zigbee2
+            ("_TZE200_fsow0qsk", "TS0601"),  # Tesla Smart TV500
         ],
         ENDPOINTS: {
             1: {
diff --git a/zhaquirks/visonic/mct340e.py b/zhaquirks/visonic/mct340.py
old mode 100755
new mode 100644
similarity index 92%
rename from zhaquirks/visonic/mct340e.py
rename to zhaquirks/visonic/mct340.py
index cfd526a..3814e2c
--- a/zhaquirks/visonic/mct340e.py
+++ b/zhaquirks/visonic/mct340.py
@@ -1,4 +1,4 @@
-"""Visonic MCT340E device."""
+"""Visonic MCT340 device."""
 
 from zigpy.profiles import zha
 from zigpy.quirks import CustomDevice
@@ -28,15 +28,15 @@ class CustomPowerConfigurationCluster(PowerConfigurationCluster):
     MAX_VOLTS = 3.0
 
 
-class MCT340E(CustomDevice):
-    """Visonic MCT340E device."""
+class MCT340(CustomDevice):
+    """Visonic MCT340 device."""
 
     signature = {
         #  <SimpleDescriptor endpoint=1 profile=260 device_type=1026
         # device_version=0
         # input_clusters=[0, 1, 3, 1026, 1280, 32, 2821]
         # output_clusters=[25]>
-        MODELS_INFO: [("Visonic", "MCT-340 E")],
+        MODELS_INFO: [("Visonic", "MCT-340 E"), ("Visonic", "MCT-340 SMA")],
         ENDPOINTS: {
             1: {
                 PROFILE_ID: zha.PROFILE_ID,
diff --git a/zhaquirks/xiaomi/aqara/magnet_ac01.py b/zhaquirks/xiaomi/aqara/magnet_ac01.py
new file mode 100644
index 0000000..1a77a14
--- /dev/null
+++ b/zhaquirks/xiaomi/aqara/magnet_ac01.py
@@ -0,0 +1,81 @@
+"""Xiaomi aqara P1 contact sensor device."""
+
+from zigpy.profiles import zha
+import zigpy.types as t
+from zigpy.zcl.clusters.general import Basic, Identify, OnOff, Ota, PowerConfiguration
+from zigpy.zcl.clusters.security import IasZone
+
+from zhaquirks.const import (
+    DEVICE_TYPE,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    MODELS_INFO,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+)
+from zhaquirks.xiaomi import (
+    LUMI,
+    BasicCluster,
+    XiaomiAqaraE1Cluster,
+    XiaomiCustomDevice,
+    XiaomiPowerConfigurationPercent,
+)
+
+
+class OppleCluster(XiaomiAqaraE1Cluster):
+    """Opple cluster."""
+
+    class DetectionDistance(t.enum8):
+        """Detection distance."""
+
+        TenMilimeters = 0x01
+        TwentyMilimeters = 0x02
+        ThirtyMilimeters = 0x03
+
+    attributes = {
+        0x010C: ("detection_distance", t.uint8_t, True),
+    }
+
+
+class LumiMagnetAC01(XiaomiCustomDevice):
+    """Custom device representing lumi.magnet.ac01."""
+
+    def __init__(self, *args, **kwargs):
+        """Init."""
+        self.battery_size = 8
+        super().__init__(*args, **kwargs)
+
+    signature = {
+        # <SimpleDescriptor endpoint=1 profile=260 device_type=1026
+        # device_version=1
+        # input_clusters=[0, 1, 3, 1280]
+        # output_clusters=[25]>
+        MODELS_INFO: [(LUMI, "lumi.magnet.ac01")],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.IAS_ZONE,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    Identify.cluster_id,
+                    IasZone.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Ota.cluster_id],
+            },
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                INPUT_CLUSTERS: [
+                    BasicCluster,
+                    XiaomiPowerConfigurationPercent,
+                    Identify.cluster_id,
+                    OppleCluster,
+                ],
+                OUTPUT_CLUSTERS: [OnOff.cluster_id, Ota.cluster_id],
+            },
+        },
+    }
diff --git a/zhaquirks/xiaomi/aqara/opple_switch.py b/zhaquirks/xiaomi/aqara/opple_switch.py
index d101d21..278f090 100644
--- a/zhaquirks/xiaomi/aqara/opple_switch.py
+++ b/zhaquirks/xiaomi/aqara/opple_switch.py
@@ -1,4 +1,4 @@
-"""Xiaomi Aqara wall switch devices. Also see switch_h1 files similar H1 rocker switches."""
+"""Xiaomi Aqara wall switch devices. Also see switch_h1 files for similar H1 rocker switches."""
 import copy
 from enum import Enum
 
@@ -8,14 +8,18 @@ from zigpy.zcl.clusters.general import (
     Alarms,
     AnalogInput,
     Basic,
+    DeviceTemperature,
     GreenPowerProxy,
     Groups,
     Identify,
+    MultistateInput,
     OnOff,
     Ota,
     Scenes,
     Time,
 )
+from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement
+from zigpy.zcl.clusters.smartenergy import Metering
 
 from zhaquirks.const import (
     ARGS,
@@ -50,8 +54,7 @@ from zhaquirks.xiaomi import (
     OnOffCluster,
     XiaomiCustomDevice,
 )
-
-from .opple_remote import MultistateInputCluster, OppleCluster
+from zhaquirks.xiaomi.aqara.opple_remote import MultistateInputCluster, OppleCluster
 
 BOTH_BUTTONS = "both_buttons"
 PRESS_TYPES = {0: "hold", 1: "single", 2: "double", 3: "triple", 255: "release"}
@@ -138,7 +141,10 @@ class XiaomiOpple2ButtonSwitchBase(XiaomiCustomDevice):
                     ElectricalMeasurementCluster,
                     OppleSwitchCluster,
                 ],
-                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+                OUTPUT_CLUSTERS: [
+                    Time.cluster_id,
+                    Ota.cluster_id,
+                ],
             },
             2: {
                 DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
@@ -211,7 +217,9 @@ class XiaomiOpple2ButtonSwitchBase(XiaomiCustomDevice):
                 PROFILE_ID: zgp.PROFILE_ID,
                 DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
                 INPUT_CLUSTERS: [],
-                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
+                OUTPUT_CLUSTERS: [
+                    GreenPowerProxy.cluster_id,
+                ],
             },
         },
     }
@@ -274,8 +282,8 @@ class XiaomiOpple2ButtonSwitchBase(XiaomiCustomDevice):
     }
 
 
-class XiaomiOpple2ButtonSwitchFace1(XiaomiOpple2ButtonSwitchBase):
-    """Xiaomi Opple 2 Button Switch. Face 1."""
+class XiaomiOpple2ButtonSwitch1(XiaomiOpple2ButtonSwitchBase):
+    """Xiaomi Opple 2 Button Switch. Signature 1."""
 
     signature = {
         MODELS_INFO: [(LUMI, "lumi.switch.b2naus01")],
@@ -286,15 +294,18 @@ class XiaomiOpple2ButtonSwitchFace1(XiaomiOpple2ButtonSwitchBase):
                 DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                 INPUT_CLUSTERS: [
                     Basic.cluster_id,
-                    DeviceTemperatureCluster.cluster_id,
+                    DeviceTemperature.cluster_id,
                     Identify.cluster_id,
                     Groups.cluster_id,
                     Scenes.cluster_id,
                     OnOff.cluster_id,
-                    MultistateInputCluster.cluster_id,
+                    MultistateInput.cluster_id,
                     OppleSwitchCluster.cluster_id,
                 ],
-                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+                OUTPUT_CLUSTERS: [
+                    Time.cluster_id,
+                    Ota.cluster_id,
+                ],
             },
             # input_clusters=[0, 3, 4, 5, 6, 18, 64704], output_clusters=[]
             2: {
@@ -306,7 +317,7 @@ class XiaomiOpple2ButtonSwitchFace1(XiaomiOpple2ButtonSwitchBase):
                     Groups.cluster_id,
                     Scenes.cluster_id,
                     OnOff.cluster_id,
-                    MultistateInputCluster.cluster_id,
+                    MultistateInput.cluster_id,
                     OppleSwitchCluster.cluster_id,
                 ],
                 OUTPUT_CLUSTERS: [],
@@ -334,7 +345,7 @@ class XiaomiOpple2ButtonSwitchFace1(XiaomiOpple2ButtonSwitchBase):
                 PROFILE_ID: zha.PROFILE_ID,
                 DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                 INPUT_CLUSTERS: [
-                    MultistateInputCluster.cluster_id,
+                    MultistateInput.cluster_id,
                 ],
                 OUTPUT_CLUSTERS: [],
             },
@@ -343,7 +354,7 @@ class XiaomiOpple2ButtonSwitchFace1(XiaomiOpple2ButtonSwitchBase):
                 PROFILE_ID: zha.PROFILE_ID,
                 DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                 INPUT_CLUSTERS: [
-                    MultistateInputCluster.cluster_id,
+                    MultistateInput.cluster_id,
                 ],
                 OUTPUT_CLUSTERS: [],
             },
@@ -352,7 +363,59 @@ class XiaomiOpple2ButtonSwitchFace1(XiaomiOpple2ButtonSwitchBase):
                 PROFILE_ID: zha.PROFILE_ID,
                 DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                 INPUT_CLUSTERS: [
-                    MultistateInputCluster.cluster_id,
+                    MultistateInput.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            242: {
+                PROFILE_ID: zgp.PROFILE_ID,
+                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
+                INPUT_CLUSTERS: [],
+                OUTPUT_CLUSTERS: [
+                    GreenPowerProxy.cluster_id,
+                ],
+            },
+        },
+    }
+
+
+class XiaomiOpple2ButtonSwitch2(XiaomiOpple2ButtonSwitchBase):
+    """Xiaomi Opple 2 Button Switch. Signature 2."""
+
+    signature = {
+        MODELS_INFO: [(LUMI, "lumi.switch.b2naus01")],
+        ENDPOINTS: {
+            # input_clusters=[0, 2, 3, 4, 5, 6, 18, 64704], output_clusters=[10, 25]
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    DeviceTemperature.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    OnOff.cluster_id,
+                    MultistateInput.cluster_id,
+                    OppleSwitchCluster.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Time.cluster_id,
+                    Ota.cluster_id,
+                ],
+            },
+            # input_clusters=[0, 3, 4, 5, 6, 18, 64704], output_clusters=[]
+            2: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    OnOff.cluster_id,
+                    MultistateInput.cluster_id,
+                    OppleSwitchCluster.cluster_id,
                 ],
                 OUTPUT_CLUSTERS: [],
             },
@@ -360,7 +423,113 @@ class XiaomiOpple2ButtonSwitchFace1(XiaomiOpple2ButtonSwitchBase):
                 PROFILE_ID: zgp.PROFILE_ID,
                 DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
                 INPUT_CLUSTERS: [],
-                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
+                OUTPUT_CLUSTERS: [
+                    GreenPowerProxy.cluster_id,
+                ],
+            },
+        },
+    }
+
+
+class XiaomiOpple2ButtonSwitch3(XiaomiOpple2ButtonSwitchBase):
+    """Xiaomi Opple 2 Button Switch. Signature 3."""
+
+    signature = {
+        MODELS_INFO: [(LUMI, "lumi.switch.b2naus01")],
+        ENDPOINTS: {
+            # input_clusters=[0, 2, 3, 4, 5, 6, 9, 1794, 2820], output_clusters=[10, 25]
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    DeviceTemperature.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    OnOff.cluster_id,
+                    Alarms.cluster_id,
+                    Metering.cluster_id,
+                    ElectricalMeasurement.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Time.cluster_id,
+                    Ota.cluster_id,
+                ],
+            },
+            # input_clusters=[0, 3, 4, 5, 6, 18, 64704], output_clusters=[]
+            2: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    OnOff.cluster_id,
+                    MultistateInput.cluster_id,
+                    OppleSwitchCluster.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            242: {
+                PROFILE_ID: zgp.PROFILE_ID,
+                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
+                INPUT_CLUSTERS: [],
+                OUTPUT_CLUSTERS: [
+                    GreenPowerProxy.cluster_id,
+                ],
+            },
+        },
+    }
+
+
+class XiaomiOpple2ButtonSwitch4(XiaomiOpple2ButtonSwitchBase):
+    """Xiaomi Opple 2 Button Switch. Signature 4."""
+
+    signature = {
+        MODELS_INFO: [(LUMI, "lumi.switch.b2naus01")],
+        ENDPOINTS: {
+            # input_clusters=[0, 2, 3, 4, 5, 6, 9, 1794, 2820], output_clusters=[10, 25]
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    DeviceTemperature.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    OnOff.cluster_id,
+                    Alarms.cluster_id,
+                    Metering.cluster_id,
+                    ElectricalMeasurement.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Time.cluster_id,
+                    Ota.cluster_id,
+                ],
+            },
+            # input_clusters=[0, 3, 4, 5, 6], output_clusters=[]
+            2: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    OnOff.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            242: {
+                PROFILE_ID: zgp.PROFILE_ID,
+                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
+                INPUT_CLUSTERS: [],
+                OUTPUT_CLUSTERS: [
+                    GreenPowerProxy.cluster_id,
+                ],
             },
         },
     }
diff --git a/zhaquirks/xiaomi/aqara/switch_h1_double.py b/zhaquirks/xiaomi/aqara/switch_h1_double.py
index b897224..2c07fa7 100644
--- a/zhaquirks/xiaomi/aqara/switch_h1_double.py
+++ b/zhaquirks/xiaomi/aqara/switch_h1_double.py
@@ -13,7 +13,6 @@ from zigpy.zcl.clusters.general import (
     Scenes,
     Time,
 )
-from zigpy.zcl.clusters.smartenergy import Metering
 
 from zhaquirks.const import (
     DEVICE_TYPE,
@@ -24,52 +23,39 @@ from zhaquirks.const import (
     PROFILE_ID,
 )
 from zhaquirks.xiaomi import LUMI
-from zhaquirks.xiaomi.aqara.opple_switch import XiaomiOpple2ButtonSwitchBase
+from zhaquirks.xiaomi.aqara.opple_switch import (
+    OppleSwitchCluster,
+    XiaomiOpple2ButtonSwitch1,
+    XiaomiOpple2ButtonSwitch3,
+    XiaomiOpple2ButtonSwitch4,
+    XiaomiOpple2ButtonSwitchBase,
+)
 
 
-class AqaraH1DoubleRockerSwitchWithNeutral(XiaomiOpple2ButtonSwitchBase):
-    """Aqara H1 Double Rocker Switch (with neutral)."""
+class AqaraH1DoubleRockerSwitchWithNeutral1(XiaomiOpple2ButtonSwitchBase):
+    """Aqara H1 Double Rocker Switch (with neutral). Based on signature 1."""
 
     signature = {
         MODELS_INFO: [(LUMI, "lumi.switch.n2aeu1")],
-        ENDPOINTS: {
-            #  input_clusters=[0, 2, 3, 4, 5, 6, 18, 64704], output_clusters=[10, 25]
-            1: {
-                PROFILE_ID: zha.PROFILE_ID,
-                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
-                INPUT_CLUSTERS: [
-                    Basic.cluster_id,
-                    DeviceTemperature.cluster_id,
-                    Identify.cluster_id,
-                    Groups.cluster_id,
-                    Scenes.cluster_id,
-                    OnOff.cluster_id,
-                    Alarms.cluster_id,
-                    Metering.cluster_id,
-                    0x0B04,
-                ],
-                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
-            },
-            #  input_clusters=[0, 3, 4, 5, 6, 18, 64704], output_clusters=[]
-            2: {
-                PROFILE_ID: zha.PROFILE_ID,
-                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
-                INPUT_CLUSTERS: [
-                    Basic.cluster_id,
-                    Identify.cluster_id,
-                    Groups.cluster_id,
-                    Scenes.cluster_id,
-                    OnOff.cluster_id,
-                ],
-                OUTPUT_CLUSTERS: [],
-            },
-            242: {
-                PROFILE_ID: zgp.PROFILE_ID,
-                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
-                INPUT_CLUSTERS: [],
-                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
-            },
-        },
+        ENDPOINTS: XiaomiOpple2ButtonSwitch1.signature[ENDPOINTS],
+    }
+
+
+class AqaraH1DoubleRockerSwitchWithNeutral3(XiaomiOpple2ButtonSwitchBase):
+    """Aqara H1 Double Rocker Switch (with neutral). Based on signature 3."""
+
+    signature = {
+        MODELS_INFO: [(LUMI, "lumi.switch.n2aeu1")],
+        ENDPOINTS: XiaomiOpple2ButtonSwitch3.signature[ENDPOINTS],
+    }
+
+
+class AqaraH1DoubleRockerSwitchWithNeutral4(XiaomiOpple2ButtonSwitchBase):
+    """Aqara H1 Double Rocker Switch (with neutral). Based on signature 4."""
+
+    signature = {
+        MODELS_INFO: [(LUMI, "lumi.switch.n2aeu1")],
+        ENDPOINTS: XiaomiOpple2ButtonSwitch4.signature[ENDPOINTS],
     }
 
 
@@ -79,6 +65,7 @@ class AqaraH1DoubleRockerSwitchNoNeutral(XiaomiOpple2ButtonSwitchBase):
     signature = {
         MODELS_INFO: [(LUMI, "lumi.switch.l2aeu1")],
         ENDPOINTS: {
+            # input_clusters=[0, 2, 3, 4, 5, 6, 18, 64704], output_clusters=[10, 25]
             1: {
                 PROFILE_ID: zha.PROFILE_ID,
                 DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
@@ -90,13 +77,14 @@ class AqaraH1DoubleRockerSwitchNoNeutral(XiaomiOpple2ButtonSwitchBase):
                     Scenes.cluster_id,
                     OnOff.cluster_id,
                     MultistateInput.cluster_id,
-                    0xFCC0,
+                    OppleSwitchCluster.cluster_id,
                 ],
                 OUTPUT_CLUSTERS: [
                     Time.cluster_id,
                     Ota.cluster_id,
                 ],
             },
+            # input_clusters=[0, 3, 4, 5, 6, 18, 64704], output_clusters=[]
             2: {
                 PROFILE_ID: zha.PROFILE_ID,
                 DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
@@ -107,10 +95,11 @@ class AqaraH1DoubleRockerSwitchNoNeutral(XiaomiOpple2ButtonSwitchBase):
                     Scenes.cluster_id,
                     OnOff.cluster_id,
                     MultistateInput.cluster_id,
-                    0xFCC0,
+                    OppleSwitchCluster.cluster_id,
                 ],
                 OUTPUT_CLUSTERS: [],
             },
+            # input_clusters=[18], output_clusters=[]
             41: {
                 PROFILE_ID: zha.PROFILE_ID,
                 DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
@@ -119,6 +108,7 @@ class AqaraH1DoubleRockerSwitchNoNeutral(XiaomiOpple2ButtonSwitchBase):
                 ],
                 OUTPUT_CLUSTERS: [],
             },
+            # input_clusters=[18], output_clusters=[]
             42: {
                 PROFILE_ID: zha.PROFILE_ID,
                 DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
@@ -127,6 +117,7 @@ class AqaraH1DoubleRockerSwitchNoNeutral(XiaomiOpple2ButtonSwitchBase):
                 ],
                 OUTPUT_CLUSTERS: [],
             },
+            # input_clusters=[18], output_clusters=[]
             51: {
                 PROFILE_ID: zha.PROFILE_ID,
                 DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
@@ -153,6 +144,7 @@ class AqaraH1DoubleRockerSwitchNoNeutralAlt(XiaomiOpple2ButtonSwitchBase):
     signature = {
         MODELS_INFO: [(LUMI, "lumi.switch.l2aeu1")],
         ENDPOINTS: {
+            # input_clusters=[0, 2, 3, 4, 5, 6, 9], output_clusters=[10, 25]
             1: {
                 PROFILE_ID: zha.PROFILE_ID,
                 DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
@@ -170,6 +162,7 @@ class AqaraH1DoubleRockerSwitchNoNeutralAlt(XiaomiOpple2ButtonSwitchBase):
                     Ota.cluster_id,
                 ],
             },
+            # input_clusters=[0, 3, 4, 5, 6], output_clusters=[]
             2: {
                 PROFILE_ID: zha.PROFILE_ID,
                 DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
diff --git a/zhaquirks/xiaomi/aqara/switch_t1.py b/zhaquirks/xiaomi/aqara/switch_t1.py
index dae8182..8cb02fb 100644
--- a/zhaquirks/xiaomi/aqara/switch_t1.py
+++ b/zhaquirks/xiaomi/aqara/switch_t1.py
@@ -43,6 +43,7 @@ class OppleCluster(XiaomiAqaraE1Cluster):
     """Xiaomi Aqara T1 relay cluster."""
 
     attributes = {
+        0x0009: ("mode", t.uint8_t, True),
         0x000A: ("switch_type", t.uint8_t, True),
         0x0201: ("power_outage_memory", t.Bool, True),
         0x0203: ("do_not_disturb", t.Bool, True),


Test patch:
diff --git a/tests/conftest.py b/tests/conftest.py
index d104978..6a0e8b2 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -70,6 +70,9 @@ class MockApp(zigpy.application.ControllerApplication):
     async def start_network(self, *args, **kwargs):
         """Mock start_network."""
 
+    async def permit_with_link_key(self, *args, **kwargs):
+        """Mock permit_with_link_key"""
+
     async def write_network_info(self, *args, **kwargs):
         """Mock write_network_info."""
 
diff --git a/tests/test_sonoff.py b/tests/test_sonoff.py
new file mode 100644
index 0000000..2e5b47a
--- /dev/null
+++ b/tests/test_sonoff.py
@@ -0,0 +1,30 @@
+"""Tests for Sonoff quirks."""
+
+
+import zhaquirks
+import zhaquirks.sonoff.snzb01p
+
+zhaquirks.setup()
+
+
+def test_sonoff_snzb01p(assert_signature_matches_quirk):
+    """Test 'Sonoff SNZB-01P smart button' signature is matched to its quirk."""
+
+    signature = {
+        "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.EndDevice: 2>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress: 128>, manufacturer_code=4742, maximum_buffer_size=82, maximum_incoming_transfer_size=255, server_mask=11264, maximum_outgoing_transfer_size=255, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=True, *is_full_function_device=False, *is_mains_powered=False, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False)",
+        "endpoints": {
+            "1": {
+                "profile_id": 260,
+                "device_type": "0x0000",
+                "in_clusters": ["0x0000", "0x0001", "0x0003", "0x0020", "0xfc57"],
+                "out_clusters": ["0x0003", "0x0006", "0x0019"],
+            }
+        },
+        "manufacturer": "eWeLink",
+        "model": "SNZB-01P",
+        "class": "sonoff.snzb01p.SonoffSmartButtonSNZB01P",
+    }
+
+    assert_signature_matches_quirk(
+        zhaquirks.sonoff.snzb01p.SonoffSmartButtonSNZB01P, signature
+    )
diff --git a/tests/test_tuya_air.py b/tests/test_tuya_air.py
index 427edc4..ded8c3f 100644
--- a/tests/test_tuya_air.py
+++ b/tests/test_tuya_air.py
@@ -8,6 +8,7 @@ import zigpy.profiles.zha
 import zhaquirks
 from zhaquirks.tuya import TuyaNewManufCluster
 from zhaquirks.tuya.air.ts0601_air_quality import TuyaCO2Sensor
+from zhaquirks.tuya.air.ts0601_smart_air import TuyaSmartAirSensor
 
 zhaquirks.setup()
 
@@ -69,3 +70,67 @@ def test_co2_sensor(air_quality_device, data, ep_attr, expected_value):
     )
     cluster = getattr(air_quality_device.endpoints[1], ep_attr)
     assert cluster.get("measured_value") == expected_value
+
+
+@pytest.fixture
+def smart_air_quality_device(zigpy_device_from_quirk):
+    """Tuya Smart Air Quality Sensor."""
+    dev = zigpy_device_from_quirk(TuyaSmartAirSensor)
+    cluster = dev.endpoints[1].in_clusters[TuyaNewManufCluster.cluster_id]
+    with mock.patch.object(cluster, "send_default_rsp"):
+        yield dev
+
+
+@pytest.mark.parametrize(
+    "data, ep_attr, expected_value",
+    (
+        (
+            b"\t2\x01\x00\x02\x02\x02\x00\x04\x00\x00\x01r",
+            "carbon_dioxide_concentration",
+            370 * 1e-6,
+        ),
+        (
+            b"\t$\x01\x00\x00\x13\x02\x00\x04\x00\x00\x02\xd6",
+            "humidity",
+            7260,
+        ),
+        (
+            b"\t\x03\x01\x00\x01\x15\x02\x00\x04\x00\x00\x00\x01",
+            "voc_level",
+            1 * 1e-6,
+        ),
+        (
+            b"\t\x02\x01\x00\x01\x16\x02\x00\x04\x00\x00\x00\x02",
+            "formaldehyde_concentration",
+            2 * 1e-6,
+        ),
+        (
+            b"\t\x02\x01\x00\x00\x12\x02\x00\x04\x00\x00\x01 ",
+            "temperature",
+            2880,
+        ),
+        (
+            b"\t\x02\x01\x00\x00\x12\x02\x00\x04\x00\x00\xff\xfb",
+            "temperature",
+            -50,
+        ),
+        (
+            b"\t\x02\x01\x00\x00\x12\x02\x00\x04\x00\x00\xff\xef",
+            "temperature",
+            -170,
+        ),
+        (
+            b"\t\xa5\x02\x00\x01\x14\x02\x00\x04\x00\x00\x00\x01",
+            "pm25",
+            1,
+        ),
+    ),
+)
+def test_smart_air_sensor(smart_air_quality_device, data, ep_attr, expected_value):
+    """Test Tuya Smart Air Sensor."""
+
+    smart_air_quality_device.handle_message(
+        zigpy.profiles.zha.PROFILE_ID, TuyaNewManufCluster.cluster_id, 1, 1, data
+    )
+    cluster = getattr(smart_air_quality_device.endpoints[1], ep_attr)
+    assert cluster.get("measured_value") == expected_value