Repository: zigpy/zha-device-handlers
Pull request #338 (2020-04-16 07:42:36)

Updated README.md with info on how-to test new releases.
Updated README.md with info on how-to test new releases.

Copied this text from zigpy-cc so credits go to @sanyatuning


Patch:
diff --git a/README.md b/README.md
index 71ce3c5..65a4004 100644
--- a/README.md
+++ b/README.md
@@ -130,6 +130,28 @@ Please refer to [xbee.md](xbee.md) for details on configuration and usage exampl
 
 - All supported devices report battery level
 
+# Testing new releases
+
+Testing a new release of the zha-quirks package before it is released in Home Assistant.
+
+If you are using Supervised Home Assistant (formerly known as the Hassio/Hass.io distro):
+- Add https://github.com/home-assistant/hassio-addons-development as "add-on" repository
+- Install "Custom deps deployment" addon
+- Update config like: 
+  ```
+  pypi:
+    - zha-quirks==0.0.38
+  apk: []
+  ```
+  where 0.0.38 is the new version
+- Start the addon
+
+If you are instead using some custom python installation of Home Assistant then do this:
+- Activate your python virtual env
+- Update package with ``pip``
+  ```
+  pip install zha-quirks==0.0.38
+
 # Thanks
 
 - Special thanks to damarco for the majority of the device tracker code
diff --git a/setup.py b/setup.py
index 99bd58a..0c38453 100644
--- a/setup.py
+++ b/setup.py
@@ -2,7 +2,7 @@
 
 from setuptools import find_packages, setup
 
-VERSION = "0.0.38"
+VERSION = "0.0.40"
 
 
 def readme():
@@ -24,6 +24,6 @@ setup(
     keywords="zha quirks homeassistant hass",
     packages=find_packages(exclude=["*.tests"]),
     python_requires=">=3",
-    install_requires=["zigpy-homeassistant>=0.18.1"],
+    install_requires=["zigpy>=0.20.0"],
     tests_require=["pytest"],
 )
diff --git a/zhaquirks/__init__.py b/zhaquirks/__init__.py
index 2013e59..08c4694 100755
--- a/zhaquirks/__init__.py
+++ b/zhaquirks/__init__.py
@@ -33,6 +33,18 @@ class Bus(ListenableMixin):
 class LocalDataCluster(CustomCluster):
     """Cluster meant to prevent remote calls."""
 
+    async def bind(self):
+        """Prevent bind."""
+        return (foundation.Status.SUCCESS,)
+
+    async def unbind(self):
+        """Prevent unbind."""
+        return (foundation.Status.SUCCESS,)
+
+    async def _configure_reporting(self, *args, **kwargs):
+        """Prevent remote configure reporting."""
+        return foundation.ConfigureReportingResponse.deserialize(b"\x00")[0]
+
     async def read_attributes_raw(self, attributes, manufacturer=None):
         """Prevent remote reads."""
         records = [
@@ -142,15 +154,13 @@ class PowerConfigurationCluster(CustomCluster, PowerConfiguration):
 
     def _update_attribute(self, attrid, value):
         super()._update_attribute(attrid, value)
-        if attrid == self.BATTERY_VOLTAGE_ATTR:
+        if attrid == self.BATTERY_VOLTAGE_ATTR and value not in (0, 255):
             super()._update_attribute(
                 self.BATTERY_PERCENTAGE_REMAINING,
                 self._calculate_battery_percentage(value),
             )
 
     def _calculate_battery_percentage(self, raw_value):
-        if raw_value in (0, 255):
-            return -1
         volts = raw_value / 10
         volts = max(volts, self.MIN_VOLTS)
         volts = min(volts, self.MAX_VOLTS)
diff --git a/zhaquirks/centralite/ias.py b/zhaquirks/centralite/ias.py
index f109f5c..ef70d10 100755
--- a/zhaquirks/centralite/ias.py
+++ b/zhaquirks/centralite/ias.py
@@ -132,3 +132,36 @@ class CentraLiteIASSensorV2(CustomDevice):
     }
 
     replacement = CentraLiteIASSensor.replacement
+
+
+class CentraLiteIASSensorV3(CustomDevice):
+    """Custom device representing centralite ias sensors."""
+
+    signature = {
+        #  <SimpleDescriptor endpoint=1 profile=260 device_type=1026
+        #  device_version=0
+        #  input_clusters=[0, 1, 3, 32, 1026, 1280, 2821]
+        #  output_clusters=[25]>
+        MODELS_INFO: CentraLiteIASSensor.signature[MODELS_INFO],
+        ENDPOINTS: {
+            1: CentraLiteIASSensor.signature[ENDPOINTS][1],
+            #  <SimpleDescriptor endpoint=2 profile=49887 device_type=12
+            #  device_version=0
+            #  input_clusters=[0, 1, 3, 15, 2821]
+            #  output_clusters=[3]>
+            2: {
+                PROFILE_ID: MANUFACTURER_SPECIFIC_PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.SIMPLE_SENSOR,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    BinaryInput.cluster_id,
+                    PowerConfigurationCluster.cluster_id,
+                    Identify.cluster_id,
+                    DIAGNOSTICS_CLUSTER_ID,
+                ],
+                OUTPUT_CLUSTERS: [Identify.cluster_id],
+            },
+        },
+    }
+
+    replacement = CentraLiteIASSensor.replacement
diff --git a/zhaquirks/eurotronic/__init__.py b/zhaquirks/eurotronic/__init__.py
new file mode 100644
index 0000000..76c3211
--- /dev/null
+++ b/zhaquirks/eurotronic/__init__.py
@@ -0,0 +1,148 @@
+"""Eurotronic devices."""
+
+import logging
+
+import zigpy.types as types
+from zigpy.quirks import CustomCluster
+from zigpy.zcl import foundation
+from zigpy.zcl.clusters.hvac import Thermostat
+
+
+EUROTRONIC = "Eurotronic"
+
+THERMOSTAT_CHANNEL = "thermostat"
+
+MANUFACTURER = 0x1037  # 4151
+
+OCCUPIED_HEATING_SETPOINT_ATTR = 0x0012
+CTRL_SEQ_OF_OPER_ATTR = 0x001B
+SYSTEM_MODE_ATTR = 0x001C
+
+TRV_MODE_ATTR = 0x4000
+SET_VALVE_POS_ATTR = 0x4001
+ERRORS_ATTR = 0x4002
+CURRENT_TEMP_SETPOINT_ATTR = 0x4003
+HOST_FLAGS_ATTR = 0x4008
+
+
+# Host Flags
+# unknown (defaults to 1)       = 0b00000001 # 1
+MIRROR_SCREEN_FLAG = 0b00000010  # 2
+BOOST_FLAG = 0b00000100  # 4
+# unknown                       = 0b00001000 # 8
+CLR_OFF_MODE_FLAG = 0b00010000  # 16
+SET_OFF_MODE_FLAG = 0b00100000  # 32, reported back as 16
+# unknown                       = 0b01000000 # 64
+CHILD_LOCK_FLAG = 0b10000000  # 128
+
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class ThermostatCluster(CustomCluster, Thermostat):
+    """Thermostat cluster."""
+
+    cluster_id = Thermostat.cluster_id
+
+    attributes = {
+        TRV_MODE_ATTR: ("trv_mode", types.enum8),
+        SET_VALVE_POS_ATTR: ("set_valve_position", types.uint8_t),
+        ERRORS_ATTR: ("errors", types.uint8_t),
+        CURRENT_TEMP_SETPOINT_ATTR: ("current_temperature_setpoint", types.int16s),
+        HOST_FLAGS_ATTR: ("host_flags", types.uint24_t),
+    }
+    attributes.update(Thermostat.attributes)
+
+    def _update_attribute(self, attrid, value):
+        _LOGGER.debug("update attribute %04x to %s... ", attrid, value)
+
+        if attrid == CURRENT_TEMP_SETPOINT_ATTR:
+            super()._update_attribute(OCCUPIED_HEATING_SETPOINT_ATTR, value)
+        elif attrid == HOST_FLAGS_ATTR:
+            if value & CLR_OFF_MODE_FLAG == CLR_OFF_MODE_FLAG:
+                super()._update_attribute(SYSTEM_MODE_ATTR, 0x0)
+                _LOGGER.debug("set system_mode to [off ]")
+            else:
+                super()._update_attribute(SYSTEM_MODE_ATTR, 0x4)
+                _LOGGER.debug("set system_mode to [heat]")
+
+        _LOGGER.debug("update attribute %04x to %s... [ ok ]", attrid, value)
+        super()._update_attribute(attrid, value)
+
+    async def read_attributes_raw(self, attributes, manufacturer=None):
+        """Override wrong attribute reports from the thermostat."""
+        success = []
+        error = []
+
+        if CTRL_SEQ_OF_OPER_ATTR in attributes:
+            rar = foundation.ReadAttributeRecord(
+                CTRL_SEQ_OF_OPER_ATTR, foundation.Status.SUCCESS, foundation.TypeValue()
+            )
+            rar.value.value = 0x2
+            success.append(rar)
+
+        if SYSTEM_MODE_ATTR in attributes:
+            rar = foundation.ReadAttributeRecord(
+                SYSTEM_MODE_ATTR, foundation.Status.SUCCESS, foundation.TypeValue()
+            )
+            rar.value.value = 0x4
+            success.append(rar)
+
+        if OCCUPIED_HEATING_SETPOINT_ATTR in attributes:
+
+            _LOGGER.debug("intercepting OCC_HS")
+
+            values = await super().read_attributes_raw(
+                [CURRENT_TEMP_SETPOINT_ATTR], manufacturer=MANUFACTURER
+            )
+
+            if len(values) == 2:
+                current_temp_setpoint = values[1][0]
+                current_temp_setpoint.attrid = OCCUPIED_HEATING_SETPOINT_ATTR
+
+                error.extend(values[1])
+            else:
+                current_temp_setpoint = values[0][0]
+                current_temp_setpoint.attrid = OCCUPIED_HEATING_SETPOINT_ATTR
+
+                success.extend(values[0])
+
+        attributes = list(
+            filter(
+                lambda x: x
+                not in (
+                    CTRL_SEQ_OF_OPER_ATTR,
+                    SYSTEM_MODE_ATTR,
+                    OCCUPIED_HEATING_SETPOINT_ATTR,
+                ),
+                attributes,
+            )
+        )
+
+        if attributes:
+            values = await super().read_attributes_raw(attributes, manufacturer)
+
+            success.extend(values[0])
+
+            if len(values) == 2:
+                error.extend(values[1])
+
+        return success, error
+
+    def write_attributes(self, attributes, manufacturer=None):
+        """Override wrong writes to thermostat attributes."""
+        if "system_mode" in attributes:
+
+            host_flags = self._attr_cache.get(HOST_FLAGS_ATTR, 1)
+            _LOGGER.debug("current host_flags: %s", host_flags)
+
+            if attributes.get("system_mode") == 0x0:
+                return super().write_attributes(
+                    {"host_flags": host_flags | SET_OFF_MODE_FLAG}, MANUFACTURER
+                )
+            if attributes.get("system_mode") == 0x4:
+                return super().write_attributes(
+                    {"host_flags": host_flags | CLR_OFF_MODE_FLAG}, MANUFACTURER
+                )
+
+        return super().write_attributes(attributes, manufacturer)
diff --git a/zhaquirks/eurotronic/spzb0001.py b/zhaquirks/eurotronic/spzb0001.py
new file mode 100644
index 0000000..ef6441e
--- /dev/null
+++ b/zhaquirks/eurotronic/spzb0001.py
@@ -0,0 +1,85 @@
+"""Eurotronic Spirit Zigbee quirk."""
+
+from zigpy.profiles import zha
+from zigpy.quirks import CustomDevice
+from zigpy.zcl.clusters.general import (
+    Basic,
+    Groups,
+    Identify,
+    Ota,
+    PowerConfiguration,
+    Time,
+)
+from zigpy.zcl.clusters.hvac import Thermostat
+
+from zhaquirks.const import (
+    DEVICE_TYPE,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    MODELS_INFO,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+)
+
+from . import EUROTRONIC, ThermostatCluster
+
+
+class SPZB0001(CustomDevice):
+    """Eurotronic Spirit Zigbee device."""
+
+    signature = {
+        # <SimpleDescriptor endpoint=1 profile=260 device_type=769
+        # device_version=1
+        # input_clusters=[0, 1, 3, 513, 25, 10]
+        # output_clusters=[0, 1, 3, 4, 513, 25, 10]>
+        MODELS_INFO: [(EUROTRONIC, "SPZB0001")],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.THERMOSTAT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    Identify.cluster_id,
+                    Thermostat.cluster_id,
+                    Ota.cluster_id,
+                    Time.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Thermostat.cluster_id,
+                    Ota.cluster_id,
+                    Time.cluster_id,
+                ],
+            }
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.THERMOSTAT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    Identify.cluster_id,
+                    ThermostatCluster,
+                    Ota.cluster_id,
+                    Time.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    ThermostatCluster,
+                    Ota.cluster_id,
+                    Time.cluster_id,
+                ],
+            }
+        }
+    }
diff --git a/zhaquirks/ikea/fivebtnremotezha.py b/zhaquirks/ikea/fivebtnremotezha.py
index e08c44f..f95678f 100644
--- a/zhaquirks/ikea/fivebtnremotezha.py
+++ b/zhaquirks/ikea/fivebtnremotezha.py
@@ -2,6 +2,7 @@
 from zigpy.profiles import zha
 from zigpy.quirks import CustomDevice
 from zigpy.zcl.clusters.general import (
+    Alarms,
     Basic,
     Groups,
     Identify,
@@ -11,6 +12,7 @@ from zigpy.zcl.clusters.general import (
     PollControl,
     PowerConfiguration,
 )
+from zigpy.zcl.clusters.homeautomation import Diagnostic
 from zigpy.zcl.clusters.lightlink import LightLink
 
 from .. import DoublingPowerConfigurationCluster
@@ -166,3 +168,62 @@ class IkeaTradfriRemote(CustomDevice):
             ARGS: [3328, 0],
         },
     }
+
+
+class IkeaTradfriRemote2(IkeaTradfriRemote):
+    """Custom device representing IKEA of Sweden TRADFRI 5 button remote control."""
+
+    signature = {
+        # <SimpleDescriptor endpoint = 1 profile = 260 device_type = 2064
+        # device_version = 2 input_clusters = [0, 1, 3, 9, 2821, 4096]
+        # output_clusters = [3, 4, 5, 6, 8, 25, 4096]
+        MODELS_INFO: [(IKEA, "TRADFRI remote control")],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.COLOR_SCENE_CONTROLLER,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    Identify.cluster_id,
+                    Alarms.cluster_id,
+                    Diagnostic.cluster_id,
+                    LightLink.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    ScenesCluster.cluster_id,
+                    OnOff.cluster_id,
+                    LevelControl.cluster_id,
+                    Ota.cluster_id,
+                    LightLink.cluster_id,
+                ],
+            }
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.COLOR_SCENE_CONTROLLER,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    DoublingPowerConfigurationCluster,
+                    Identify.cluster_id,
+                    Alarms.cluster_id,
+                    LightLinkCluster,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    ScenesCluster,
+                    OnOff.cluster_id,
+                    LevelControl.cluster_id,
+                    Ota.cluster_id,
+                    LightLink.cluster_id,
+                ],
+            }
+        }
+    }
diff --git a/zhaquirks/konke/magnet.py b/zhaquirks/konke/magnet.py
new file mode 100644
index 0000000..e96134e
--- /dev/null
+++ b/zhaquirks/konke/magnet.py
@@ -0,0 +1,62 @@
+"""Konke magnet sensor."""
+
+from zigpy.profiles import zha
+from zigpy.quirks import CustomDevice
+from zigpy.zcl.clusters.security import IasZone
+from zigpy.zcl.clusters.general import Basic, Identify, PowerConfiguration
+
+from . import KONKE
+from .. import PowerConfigurationCluster
+from ..const import (
+    DEVICE_TYPE,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    MODELS_INFO,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+)
+
+KONKE_CLUSTER_ID = 0xFCC0
+
+
+class KonkeMagnet(CustomDevice):
+    """Custom device representing konke magnet sensors."""
+
+    signature = {
+        #  <SimpleDescriptor endpoint=1 profile=260 device_type=1026
+        # device_version=0
+        # input_clusters=[0, 1, 3, 1280, 64704]
+        # output_clusters=[3, 64704]>
+        MODELS_INFO: [(KONKE, "3AFE270104020015")],
+        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,
+                    KONKE_CLUSTER_ID,
+                ],
+                OUTPUT_CLUSTERS: [Identify.cluster_id, KONKE_CLUSTER_ID],
+            }
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.IAS_ZONE,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfigurationCluster,
+                    Identify.cluster_id,
+                    IasZone.cluster_id,
+                    KONKE_CLUSTER_ID,
+                ],
+                OUTPUT_CLUSTERS: [Identify.cluster_id, KONKE_CLUSTER_ID],
+            }
+        }
+    }
diff --git a/zhaquirks/legrand/__init__.py b/zhaquirks/legrand/__init__.py
new file mode 100644
index 0000000..c5c4742
--- /dev/null
+++ b/zhaquirks/legrand/__init__.py
@@ -0,0 +1,2 @@
+"""Module for Legrand devices."""
+LEGRAND = "Legrand"
diff --git a/zhaquirks/legrand/dimmer.py b/zhaquirks/legrand/dimmer.py
new file mode 100644
index 0000000..5ac196e
--- /dev/null
+++ b/zhaquirks/legrand/dimmer.py
@@ -0,0 +1,95 @@
+"""Device handler for Legrand Dimmer switch w/o neutral."""
+from zigpy.profiles import zha
+from zigpy.quirks import CustomCluster, CustomDevice
+import zigpy.types as t
+from zigpy.zcl.clusters.general import (
+    Basic,
+    BinaryInput,
+    Groups,
+    Identify,
+    LevelControl,
+    OnOff,
+    Ota,
+    Scenes,
+)
+from zigpy.zcl.clusters.manufacturer_specific import ManufacturerSpecificCluster
+
+from . import LEGRAND
+from ..const import (
+    DEVICE_TYPE,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    MODELS_INFO,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+)
+
+MANUFACTURER_SPECIFIC_CLUSTER_ID = 0xFC01  # decimal = 64513
+
+
+class LegrandCluster(CustomCluster, ManufacturerSpecificCluster):
+    """LegrandCluster."""
+
+    cluster_id = MANUFACTURER_SPECIFIC_CLUSTER_ID
+    name = "LegrandCluster"
+    ep_attribute = "legrand_cluster"
+    attributes = {
+        0x0000: ("dimmer", t.data16),
+        0x0001: ("led_dark", t.Bool),
+        0x0002: ("led_on", t.Bool),
+    }
+    server_commands = {}
+    client_commands = {}
+
+
+class DimmerWithoutNeutral(CustomDevice):
+    """Dimmer switch w/o neutral."""
+
+    signature = {
+        #  <SimpleDescriptor endpoint=1 profile=260 device_type=256
+        # device_version=1
+        # input_clusters=[0, 3, 4, 8, 6, 5, 15, 64513]
+        # output_clusters=[0, 64513, 25]>
+        MODELS_INFO: [(f" {LEGRAND}", " Dimmer switch w/o neutral")],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    OnOff.cluster_id,
+                    LevelControl.cluster_id,
+                    Scenes.cluster_id,
+                    BinaryInput.cluster_id,
+                    MANUFACTURER_SPECIFIC_CLUSTER_ID,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    MANUFACTURER_SPECIFIC_CLUSTER_ID,
+                    Ota.cluster_id,
+                ],
+            }
+        },
+    }
+
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    OnOff.cluster_id,
+                    LevelControl.cluster_id,
+                    Scenes.cluster_id,
+                    BinaryInput.cluster_id,
+                    LegrandCluster,
+                ],
+                OUTPUT_CLUSTERS: [Basic.cluster_id, LegrandCluster, Ota.cluster_id],
+            }
+        }
+    }
diff --git a/zhaquirks/plaid/soil.py b/zhaquirks/plaid/soil.py
index a85fc14..ae0a960 100644
--- a/zhaquirks/plaid/soil.py
+++ b/zhaquirks/plaid/soil.py
@@ -21,11 +21,30 @@ class PowerConfigurationClusterMains(PowerConfigurationCluster):
     """Common use power configuration cluster."""
 
     MAINS_VOLTAGE_ATTR = 0x0000
+    ATTR_ID_BATT_SIZE = 0x0031
+    ATTR_ID_BATT_QTY = 0x0033
+    _CONSTANT_ATTRIBUTES = {ATTR_ID_BATT_SIZE: 0x08, ATTR_ID_BATT_QTY: 1}
 
     def _update_attribute(self, attrid, value):
         super()._update_attribute(attrid, value)
         if attrid == self.MAINS_VOLTAGE_ATTR:
-            super()._update_attribute(self.self.BATTERY_VOLTAGE_ATTR, value)
+            super()._update_attribute(self.BATTERY_VOLTAGE_ATTR, round(value / 100))
+
+    def _remap(self, attr):
+        """Replace battery voltage attribute name/id with mains_voltage."""
+        if attr in (self.BATTERY_VOLTAGE_ATTR, "battery_voltage"):
+            return self.MAINS_VOLTAGE_ATTR
+        return attr
+
+    def read_attributes(self, attributes, *args, **kwargs):  # pylint: disable=W0221
+        """Replace battery voltage with mains voltage."""
+        return super().read_attributes(
+            [self._remap(attr) for attr in attributes], *args, **kwargs
+        )
+
+    def configure_reporting(self, attribute, *args, **kwargs):  # pylint: disable=W0221
+        """Replace battery voltage with mains voltage."""
+        return super().configure_reporting(self._remap(attribute), *args, **kwargs)
 
 
 class SoilMoisture(CustomDevice):
diff --git a/zhaquirks/xiaomi/__init__.py b/zhaquirks/xiaomi/__init__.py
index f7576e5..9149b91 100644
--- a/zhaquirks/xiaomi/__init__.py
+++ b/zhaquirks/xiaomi/__init__.py
@@ -2,19 +2,23 @@
 import asyncio
 import binascii
 import logging
+import math
+from typing import Optional, Union
 
+import zigpy.zcl.foundation as foundation
 from zigpy import types as t
+from zigpy.profiles import zha
 from zigpy.quirks import CustomCluster, CustomDevice
-from zigpy.zcl.clusters.general import AnalogInput, Basic, PowerConfiguration
+from zigpy.zcl.clusters.general import AnalogInput, Basic, OnOff, PowerConfiguration
 from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement
 from zigpy.zcl.clusters.measurement import (
+    IlluminanceMeasurement,
     OccupancySensing,
     PressureMeasurement,
     RelativeHumidity,
     TemperatureMeasurement,
 )
 from zigpy.zcl.clusters.security import IasZone
-import zigpy.zcl.foundation as foundation
 
 from .. import Bus, LocalDataCluster
 from ..const import (
@@ -56,6 +60,8 @@ TEMPERATURE_REPORTED = "temperature_reported"
 POWER_REPORTED = "power_reported"
 CONSUMPTION_REPORTED = "consumption_reported"
 VOLTAGE_REPORTED = "voltage_reported"
+ILLUMINANCE_MEASUREMENT = "illuminance_measurement"
+ILLUMINANCE_REPORTED = "illuminance_reported"
 XIAOMI_AQARA_ATTRIBUTE = 0xFF01
 XIAOMI_ATTR_3 = "X-attrib-3"
 XIAOMI_ATTR_4 = "X-attrib-4"
@@ -181,6 +187,10 @@ class BasicCluster(CustomCluster, Basic):
             self.endpoint.device.voltage_bus.listener_event(
                 VOLTAGE_REPORTED, attributes[VOLTAGE] * 0.1
             )
+        if ILLUMINANCE_MEASUREMENT in attributes:
+            self.endpoint.device.illuminance_bus.listener_event(
+                ILLUMINANCE_REPORTED, attributes[ILLUMINANCE_MEASUREMENT]
+            )
 
     def _parse_aqara_attributes(self, value):
         """Parse non standard atrributes."""
@@ -213,6 +223,11 @@ class BasicCluster(CustomCluster, Basic):
             "lumi.relay.c2acn01",
         ]:
             attribute_names.update({149: CONSUMPTION, 150: VOLTAGE, 152: POWER})
+        elif (
+            MODEL in self._attr_cache
+            and self._attr_cache[MODEL] == "lumi.sensor_motion.aq2"
+        ):
+            attribute_names.update({11: ILLUMINANCE_MEASUREMENT})
 
         result = {}
         while value:
@@ -235,8 +250,7 @@ class BasicCluster(CustomCluster, Basic):
         return attributes
 
     def _parse_mija_attributes(self, value):
-        """Parse non standard atrributes."""
-        attributes = {}
+        """Parse non standard attributes."""
         attribute_names = (
             STATE,
             BATTERY_VOLTAGE_MV,
@@ -457,3 +471,53 @@ class ElectricalMeasurementCluster(LocalDataCluster, ElectricalMeasurement):
     def consumption_reported(self, value):
         """Consumption reported."""
         self._update_attribute(self.CONSUMPTION_ID, value)
+
+
+class IlluminanceMeasurementCluster(CustomCluster, IlluminanceMeasurement):
+    """Multistate input cluster."""
+
+    cluster_id = IlluminanceMeasurement.cluster_id
+    ATTR_ID = 0
+
+    def __init__(self, *args, **kwargs):
+        """Init."""
+        super().__init__(*args, **kwargs)
+        self.endpoint.device.illuminance_bus.add_listener(self)
+
+    def _update_attribute(self, attrid, value):
+        if attrid == self.ATTR_ID and value > 0:
+            value = 10000 * math.log10(value) + 1
+        super()._update_attribute(attrid, value)
+
+    def illuminance_reported(self, value):
+        """Illuminance reported."""
+        self._update_attribute(self.ATTR_ID, value)
+
+
+class OnOffCluster(OnOff, CustomCluster):
+    """Aqara wall switch cluster."""
+
+    def command(
+        self,
+        command_id: Union[foundation.Command, int, t.uint8_t],
+        *args,
+        manufacturer: Optional[Union[int, t.uint16_t]] = None,
+        expect_reply: bool = True,
+        tsn: Optional[Union[int, t.uint8_t]] = None
+    ):
+        """Command handler."""
+        src_ep = 1
+        dst_ep = self.endpoint.endpoint_id
+        device = self.endpoint.device
+        if tsn is None:
+            tsn = self._endpoint.device.application.get_sequence()
+        return device.request(
+            # device,
+            zha.PROFILE_ID,
+            OnOff.cluster_id,
+            src_ep,
+            dst_ep,
+            tsn,
+            bytes([src_ep, tsn, command_id]),
+            expect_reply=expect_reply,
+        )
diff --git a/zhaquirks/xiaomi/aqara/__init__.py b/zhaquirks/xiaomi/aqara/__init__.py
index 4771c74..d54b08c 100644
--- a/zhaquirks/xiaomi/aqara/__init__.py
+++ b/zhaquirks/xiaomi/aqara/__init__.py
@@ -1,15 +1,1 @@
 """Module for Xiaomi Aqara quirks implementations."""
-import math
-from zigpy.zcl.clusters.measurement import IlluminanceMeasurement
-from zhaquirks import CustomCluster
-
-
-class IlluminanceMeasurementCluster(CustomCluster, IlluminanceMeasurement):
-    """Multistate input cluster."""
-
-    cluster_id = IlluminanceMeasurement.cluster_id
-
-    def _update_attribute(self, attrid, value):
-        if attrid == 0 and value > 0:
-            value = 10000 * math.log10(value) + 1
-        super()._update_attribute(attrid, value)
diff --git a/zhaquirks/xiaomi/aqara/ctrl_ln.py b/zhaquirks/xiaomi/aqara/ctrl_ln.py
new file mode 100644
index 0000000..ed92cf4
--- /dev/null
+++ b/zhaquirks/xiaomi/aqara/ctrl_ln.py
@@ -0,0 +1,135 @@
+"""Xiaomi aqara single key wall switch devices."""
+from zigpy.profiles import zha
+from zigpy.zcl.clusters.general import (
+    AnalogInput,
+    Basic,
+    Groups,
+    Identify,
+    MultistateInput,
+    OnOff,
+    Ota,
+    Scenes,
+    DeviceTemperature,
+    Time,
+    BinaryOutput,
+)
+
+from zhaquirks import Bus
+from .. import (
+    LUMI,
+    AnalogInputCluster,
+    BasicCluster,
+    OnOffCluster,
+    PowerConfigurationCluster,
+    XiaomiCustomDevice,
+)
+from ...const import (
+    DEVICE_TYPE,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    MODELS_INFO,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+    SKIP_CONFIGURATION,
+)
+
+
+class CtrlLn(XiaomiCustomDevice):
+    """Aqara double key switch device."""
+
+    def __init__(self, *args, **kwargs):
+        """Init."""
+        self.power_bus = Bus()
+        super().__init__(*args, **kwargs)
+
+    signature = {
+        MODELS_INFO: [(LUMI, "lumi.ctrl_ln1.aq1"), (LUMI, "lumi.ctrl_ln2.aq1")],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    PowerConfigurationCluster.cluster_id,
+                    DeviceTemperature.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    OnOff.cluster_id,
+                    Time.cluster_id,
+                    BinaryOutput.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            },
+            2: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
+                INPUT_CLUSTERS: [OnOff.cluster_id, BinaryOutput.cluster_id],
+                OUTPUT_CLUSTERS: [],
+            },
+            3: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.MAIN_POWER_OUTLET,
+                INPUT_CLUSTERS: [AnalogInput.cluster_id],
+                OUTPUT_CLUSTERS: [Groups.cluster_id, AnalogInput.cluster_id],
+            },
+            4: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.METER_INTERFACE,
+                INPUT_CLUSTERS: [AnalogInput.cluster_id],
+                OUTPUT_CLUSTERS: [AnalogInput.cluster_id],
+            },
+            5: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [MultistateInput.cluster_id, BinaryOutput.cluster_id],
+                OUTPUT_CLUSTERS: [],
+            },
+            6: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [MultistateInput.cluster_id, BinaryOutput.cluster_id],
+                OUTPUT_CLUSTERS: [],
+            },
+            7: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [MultistateInput.cluster_id, BinaryOutput.cluster_id],
+                OUTPUT_CLUSTERS: [],
+            },
+        },
+    }
+
+    replacement = {
+        SKIP_CONFIGURATION: True,
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
+                INPUT_CLUSTERS: [
+                    BasicCluster,
+                    PowerConfigurationCluster,
+                    DeviceTemperature.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    OnOffCluster,
+                    Time.cluster_id,
+                    BinaryOutput.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
+            },
+            2: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
+                INPUT_CLUSTERS: [OnOffCluster, BinaryOutput.cluster_id],
+                OUTPUT_CLUSTERS: [],
+            },
+            3: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.MAIN_POWER_OUTLET,
+                INPUT_CLUSTERS: [AnalogInputCluster],
+                OUTPUT_CLUSTERS: [Groups.cluster_id, AnalogInput.cluster_id],
+            },
+        },
+    }
diff --git a/zhaquirks/xiaomi/aqara/ctrl_neutral1.py b/zhaquirks/xiaomi/aqara/ctrl_neutral.py
similarity index 79%
rename from zhaquirks/xiaomi/aqara/ctrl_neutral1.py
rename to zhaquirks/xiaomi/aqara/ctrl_neutral.py
index c9ca1c0..f53ff40 100644
--- a/zhaquirks/xiaomi/aqara/ctrl_neutral1.py
+++ b/zhaquirks/xiaomi/aqara/ctrl_neutral.py
@@ -2,7 +2,6 @@
 import logging
 
 from zigpy.profiles import zha
-from zigpy.quirks import CustomCluster
 from zigpy.zcl.clusters.general import (
     AnalogInput,
     Basic,
@@ -17,7 +16,13 @@ from zigpy.zcl.clusters.general import (
     BinaryOutput,
 )
 
-from .. import LUMI, BasicCluster, PowerConfigurationCluster, XiaomiCustomDevice
+from .. import (
+    LUMI,
+    BasicCluster,
+    OnOffCluster,
+    PowerConfigurationCluster,
+    XiaomiCustomDevice,
+)
 from ...const import (
     DEVICE_TYPE,
     ENDPOINTS,
@@ -45,35 +50,11 @@ _LOGGER = logging.getLogger(__name__)
 # double click 0xCFF1F00
 
 
-class XiaomiOnOffCluster(OnOff, CustomCluster):
-    """Aqara wall switch cluster."""
-
-    server_commands = {0x0000: ("off", (), False), 0x0001: ("on", (), False)}
-
-    def command(
-        self, command_id, *args, manufacturer=None, expect_reply=True, tsn=None
-    ):
-        """Command handler."""
-        src_ep = 1
-        dst_ep = 2
-        seq = self._endpoint.device.application.get_sequence()
-        return self._endpoint.device.application.request(
-            self._endpoint.device,
-            zha.PROFILE_ID,
-            OnOff.cluster_id,
-            src_ep,
-            dst_ep,
-            seq,
-            bytes([src_ep, seq, command_id]),
-            expect_reply=expect_reply,
-        )
-
-
-class CtrlNeutral1(XiaomiCustomDevice):
-    """Aqara single key switch device."""
+class CtrlNeutral(XiaomiCustomDevice):
+    """Aqara single and double key switch device."""
 
     signature = {
-        MODELS_INFO: [(LUMI, "lumi.ctrl_neutral1")],
+        MODELS_INFO: [(LUMI, "lumi.ctrl_neutral1"), (LUMI, "lumi.ctrl_neutral2")],
         ENDPOINTS: {
             # <SimpleDescriptor endpoint=1 profile=260 device_type=6
             # device_version=2
@@ -170,12 +151,34 @@ class CtrlNeutral1(XiaomiCustomDevice):
         ENDPOINTS: {
             1: {
                 DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL,
-                INPUT_CLUSTERS: [BasicCluster],
-                OUTPUT_CLUSTERS: [],
+                INPUT_CLUSTERS: [
+                    BasicCluster,
+                    Identify.cluster_id,
+                    PowerConfigurationCluster.cluster_id,
+                    DeviceTemperature.cluster_id,
+                    Ota.cluster_id,
+                    Time.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [Basic.cluster_id, Time.cluster_id, Ota.cluster_id],
             },
             2: {
                 DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
-                INPUT_CLUSTERS: [BasicCluster, XiaomiOnOffCluster],
+                INPUT_CLUSTERS: [
+                    BinaryOutput.cluster_id,
+                    OnOffCluster,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [],
+            },
+            3: {
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_SWITCH,
+                INPUT_CLUSTERS: [
+                    BinaryOutput.cluster_id,
+                    OnOffCluster,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                ],
                 OUTPUT_CLUSTERS: [],
             },
         },
diff --git a/zhaquirks/xiaomi/aqara/light_aqcn2.py b/zhaquirks/xiaomi/aqara/light_aqcn2.py
new file mode 100644
index 0000000..9f0bfff
--- /dev/null
+++ b/zhaquirks/xiaomi/aqara/light_aqcn2.py
@@ -0,0 +1,109 @@
+"""Quirk for Xiaomi Aqara Smart LED bulb ZNLDP12LM."""
+import logging
+
+from zigpy.profiles import zha
+from zigpy.zcl.clusters.closures import WindowCovering
+from zigpy.zcl.clusters.general import (
+    AnalogOutput,
+    Groups,
+    Identify,
+    LevelControl,
+    MultistateOutput,
+    OnOff,
+    Ota,
+    PowerConfiguration,
+    Scenes,
+    Time,
+)
+from zigpy.zcl.clusters.lighting import Color
+from zigpy.zcl.clusters.measurement import (
+    OccupancySensing,
+    PressureMeasurement,
+    RelativeHumidity,
+    TemperatureMeasurement,
+)
+
+from .. import LUMI, BasicCluster, XiaomiCustomDevice
+from ... import CustomCluster
+from ...const import (
+    DEVICE_TYPE,
+    ENDPOINTS,
+    INPUT_CLUSTERS,
+    MODELS_INFO,
+    OUTPUT_CLUSTERS,
+    PROFILE_ID,
+)
+
+_LOGGER = logging.getLogger(__name__)
+
+
+class LightAqcn02(XiaomiCustomDevice):
+    """Custom device for ZNLDP12LM."""
+
+    class ColorCluster(CustomCluster, Color):
+        """Color Cluster."""
+
+        _CONSTANT_ATTRIBUTES = {0x400A: 0x0010, 0x400B: 153, 0x400C: 370}
+
+    signature = {
+        # endpoint=1 profile=260 device_type=258 device_version=1 input_clusters=[0, 4, 3, 5, 10, 258, 13, 19, 6, 1, 1030, 8, 768, 1027, 1029, 1026] output_clusters=[25, 10, 13, 258, 19, 6, 1, 1030, 8, 768]>
+        MODELS_INFO: [(LUMI, "lumi.light.aqcn02")],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT,
+                INPUT_CLUSTERS: [
+                    BasicCluster.cluster_id,
+                    PowerConfiguration.cluster_id,
+                    Identify.cluster_id,
+                    Groups.cluster_id,
+                    Scenes.cluster_id,
+                    OnOff.cluster_id,
+                    LevelControl.cluster_id,
+                    Time.cluster_id,
+                    AnalogOutput.cluster_id,
+                    MultistateOutput.cluster_id,
+                    WindowCovering.cluster_id,
+                    Color.cluster_id,
+                    TemperatureMeasurement.cluster_id,
+                    PressureMeasurement.cluster_id,
+                    RelativeHumidity.cluster_id,
+                    OccupancySensing.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [
+                    PowerConfiguration.cluster_id,
+                    OnOff.cluster_id,
+                    LevelControl.cluster_id,
+                    Time.cluster_id,
+                    AnalogOutput.cluster_id,
+                    MultistateOutput.cluster_id,
+                    Ota.cluster_id,
+                    WindowCovering.cluster_id,
+                    ColorCluster.cluster_id,
+                    OccupancySensing.cluster_id,
+                ],
+            }
+        },
+    }
+    replacement = {
+        ENDPOINTS: {
+            1: {
+                DEVICE_TYPE: zha.DeviceType.COLOR_DIMMABLE_LIGHT,
+                INPUT_CLUSTERS: [
+                    BasicCluster,  # 0
+                    Groups.cluster_id,  # 4
+                    Identify.cluster_id,  # 3
+                    Scenes.cluster_id,  # 5
+                    OnOff.cluster_id,  # 6
+                    PowerConfiguration.cluster_id,  # 1
+                    LevelControl.cluster_id,  # 8
+                    ColorCluster,  # 768
+                ],
+                OUTPUT_CLUSTERS: [
+                    Time.cluster_id,  # 10
+                    Ota.cluster_id,  # 19
+                    OccupancySensing.cluster_id,  # 1030
+                ],
+            }
+        }
+    }
diff --git a/zhaquirks/xiaomi/aqara/motion_aq2.py b/zhaquirks/xiaomi/aqara/motion_aq2.py
index d707f5c..52b35a6 100644
--- a/zhaquirks/xiaomi/aqara/motion_aq2.py
+++ b/zhaquirks/xiaomi/aqara/motion_aq2.py
@@ -5,10 +5,10 @@ from zigpy.zcl.clusters.general import Basic, Identify, Ota, PowerConfiguration
 from zigpy.zcl.clusters.measurement import OccupancySensing
 from zigpy.zcl.clusters.security import IasZone
 
-from . import IlluminanceMeasurementCluster
 from .. import (
     LUMI,
     BasicCluster,
+    IlluminanceMeasurementCluster,
     MotionCluster,
     OccupancyCluster,
     PowerConfigurationCluster,
@@ -35,6 +35,7 @@ class MotionAQ2(XiaomiCustomDevice):
         """Init."""
         self.battery_size = 9
         self.motion_bus = Bus()
+        self.illuminance_bus = Bus()
         super().__init__(*args, **kwargs)
 
     signature = {
diff --git a/zhaquirks/xiaomi/aqara/motion_aq2b.py b/zhaquirks/xiaomi/aqara/motion_aq2b.py
index 08fcea3..8f5ed9d 100644
--- a/zhaquirks/xiaomi/aqara/motion_aq2b.py
+++ b/zhaquirks/xiaomi/aqara/motion_aq2b.py
@@ -4,8 +4,14 @@ from zigpy.profiles import zha
 from zigpy.zcl.clusters.general import Basic, Ota
 from zigpy.zcl.clusters.measurement import OccupancySensing
 
-from . import IlluminanceMeasurementCluster
-from .. import LUMI, BasicCluster, MotionCluster, OccupancyCluster, XiaomiCustomDevice
+from .. import (
+    LUMI,
+    BasicCluster,
+    IlluminanceMeasurementCluster,
+    MotionCluster,
+    OccupancyCluster,
+    XiaomiCustomDevice,
+)
 from ... import Bus
 from ...const import (
     DEVICE_TYPE,
@@ -27,6 +33,7 @@ class MotionAQ2(XiaomiCustomDevice):
         """Init."""
         self.battery_size = 9
         self.motion_bus = Bus()
+        self.illuminance_bus = Bus()
         super().__init__(*args, **kwargs)
 
     signature = {
diff --git a/zhaquirks/xiaomi/aqara/opple_remote.py b/zhaquirks/xiaomi/aqara/opple_remote.py
index 380a1f0..1a0eab2 100644
--- a/zhaquirks/xiaomi/aqara/opple_remote.py
+++ b/zhaquirks/xiaomi/aqara/opple_remote.py
@@ -792,3 +792,131 @@ class RemoteB686OPCN01V2(XiaomiCustomDevice):
     }
 
     device_automation_triggers = RemoteB686OPCN01.device_automation_triggers
+
+
+class RemoteB686OPCN01V3(XiaomiCustomDevice):
+    """Aqara Opple 6 button remote device."""
+
+    signature = {
+        # <SimpleDescriptor endpoint=1 profile=260 device_type=261
+        # device_version=1
+        # input_clusters=[0, 3, 1]
+        # output_clusters=[3, 6, 8, 768]>
+        MODELS_INFO: [(LUMI, "lumi.remote.b686opcn01")],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.COLOR_DIMMER_SWITCH,
+                INPUT_CLUSTERS: [
+                    Basic.cluster_id,
+                    Identify.cluster_id,
+                    PowerConfigurationCluster.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Identify.cluster_id,
+                    OnOff.cluster_id,
+                    LevelControl.cluster_id,
+                    Color.cluster_id,
+                ],
+            },
+            2: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH,
+                INPUT_CLUSTERS: [Identify.cluster_id],
+                OUTPUT_CLUSTERS: [OnOff.cluster_id, Identify.cluster_id],
+            },
+            3: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH,
+                INPUT_CLUSTERS: [
+                    MultistateInputCluster.cluster_id,
+                    Identify.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [OnOff.cluster_id],
+            },
+            4: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH,
+                INPUT_CLUSTERS: [
+                    MultistateInputCluster.cluster_id,
+                    Identify.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [OnOff.cluster_id],
+            },
+            5: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH,
+                INPUT_CLUSTERS: [
+                    MultistateInputCluster.cluster_id,
+                    Identify.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [OnOff.cluster_id],
+            },
+            6: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH,
+                INPUT_CLUSTERS: [
+                    MultistateInputCluster.cluster_id,
+                    Identify.cluster_id,
+                ],
+                OUTPUT_CLUSTERS: [OnOff.cluster_id],
+            },
+        },
+    }
+
+    replacement = {
+        NODE_DESCRIPTOR: NodeDescriptor(
+            0x02, 0x40, 0x80, 0x115F, 0x7F, 0x0064, 0x2C00, 0x0064, 0x00
+        ),
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.COLOR_DIMMER_SWITCH,
+                INPUT_CLUSTERS: [
+                    BasicCluster,
+                    Identify.cluster_id,
+                    PowerConfigurationCluster,
+                    OppleCluster,
+                    MultistateInputCluster,
+                ],
+                OUTPUT_CLUSTERS: [
+                    Identify.cluster_id,
+                    OnOff.cluster_id,
+                    LevelControl.cluster_id,
+                    Color.cluster_id,
+                ],
+            },
+            2: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH,
+                INPUT_CLUSTERS: [Identify.cluster_id],
+                OUTPUT_CLUSTERS: [OnOff.cluster_id, Identify.cluster_id],
+            },
+            3: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH,
+                INPUT_CLUSTERS: [MultistateInputCluster, Identify.cluster_id],
+                OUTPUT_CLUSTERS: [OnOff.cluster_id],
+            },
+            4: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH,
+                INPUT_CLUSTERS: [MultistateInputCluster, Identify.cluster_id],
+                OUTPUT_CLUSTERS: [OnOff.cluster_id],
+            },
+            5: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH,
+                INPUT_CLUSTERS: [MultistateInputCluster, Identify.cluster_id],
+                OUTPUT_CLUSTERS: [OnOff.cluster_id],
+            },
+            6: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT_SWITCH,
+                INPUT_CLUSTERS: [MultistateInputCluster, Identify.cluster_id],
+                OUTPUT_CLUSTERS: [OnOff.cluster_id],
+            },
+        },
+    }
+
+    device_automation_triggers = RemoteB686OPCN01.device_automation_triggers
diff --git a/zhaquirks/xiaomi/aqara/weather.py b/zhaquirks/xiaomi/aqara/weather.py
index b6e58e3..1551d7e 100644
--- a/zhaquirks/xiaomi/aqara/weather.py
+++ b/zhaquirks/xiaomi/aqara/weather.py
@@ -89,3 +89,34 @@ class Weather(XiaomiCustomDevice):
             }
         },
     }
+
+
+class Weather2(Weather):
+    """New Xiaomi weather sensor device."""
+
+    signature = {
+        #  <SimpleDescriptor endpoint=1 profile=260 device_type=24321
+        #  device_version=1
+        #  input_clusters=[0, 3, 65535, 1026, 1027, 1029]
+        #  output_clusters=[0, 4, 65535]>
+        MODELS_INFO: [(LUMI, "lumi.weather")],
+        ENDPOINTS: {
+            1: {
+                PROFILE_ID: zha.PROFILE_ID,
+                DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR,
+                INPUT_CLUSTERS: [
+                    BasicCluster.cluster_id,
+                    Identify.cluster_id,
+                    TemperatureMeasurementCluster.cluster_id,
+                    PressureMeasurement.cluster_id,
+                    RelativeHumidityCluster.cluster_id,
+                    XIAOMI_CLUSTER_ID,
+                ],
+                OUTPUT_CLUSTERS: [
+                    BasicCluster.cluster_id,
+                    Groups.cluster_id,
+                    XIAOMI_CLUSTER_ID,
+                ],
+            }
+        },
+    }


Test patch:
diff --git a/tests/test_ctrl_neutral1.py b/tests/test_ctrl_neutral1.py
index a6a60b6..96436bd 100644
--- a/tests/test_ctrl_neutral1.py
+++ b/tests/test_ctrl_neutral1.py
@@ -5,7 +5,7 @@ from unittest.mock import call
 import zigpy.application
 from zigpy.device import Device
 
-from zhaquirks.xiaomi.aqara.ctrl_neutral1 import CtrlNeutral1
+from zhaquirks.xiaomi.aqara.ctrl_neutral import CtrlNeutral
 
 
 # zigbee-herdsman:controller:endpoint Command 0x00158d00024be541/2 genOnOff.on({},
@@ -24,7 +24,7 @@ from zhaquirks.xiaomi.aqara.ctrl_neutral1 import CtrlNeutral1
 # zigbee-herdsman:adapter:zStack:unpi:writer --> frame [254,13,36,1,31,255,2,1,6,0,16,0,30,3,1,7,0,198]
 
 
-def test_ctrl_neutral1():
+def test_ctrl_neutral():
     """Test ctrl neutral 1 sends correct request."""
     sec = 8
     ieee = 0
@@ -41,10 +41,13 @@ def test_ctrl_neutral1():
     rep = Device(app, ieee, nwk)
     rep.add_endpoint(1)
     rep.add_endpoint(2)
+    rep.add_endpoint(3)
+
+    dev = CtrlNeutral(app, ieee, nwk, rep)
+    dev.request = mock.MagicMock()
 
-    dev = CtrlNeutral1(app, ieee, nwk, rep)
     dev[2].in_clusters[cluster].command(1)
 
-    assert app.request.call_args == call(
-        dev, 260, cluster, src_ep, dst_ep, sec, data, expect_reply=True
+    assert dev.request.call_args == call(
+        260, cluster, src_ep, dst_ep, sec, data, expect_reply=True
     )