From ecfd2e861331fd175e4d0502eb2b902bc07a8013 Mon Sep 17 00:00:00 2001 From: Nikita Uvarov Date: Tue, 25 Nov 2025 13:23:34 +0100 Subject: [PATCH 1/7] Convert Schneider outlet to quirks v2, add missing controls Fixes #4503. --- zhaquirks/schneiderelectric/outlet.py | 197 +++++++++++++------------- 1 file changed, 100 insertions(+), 97 deletions(-) diff --git a/zhaquirks/schneiderelectric/outlet.py b/zhaquirks/schneiderelectric/outlet.py index 5397a71a3c..da6ca73ed6 100644 --- a/zhaquirks/schneiderelectric/outlet.py +++ b/zhaquirks/schneiderelectric/outlet.py @@ -1,31 +1,76 @@ """Schneider Electric (Wiser) Outlet Quirks.""" -from zigpy.profiles import zgp, zha -from zigpy.quirks import CustomCluster, CustomDevice -from zigpy.zcl.clusters.general import ( - Basic, - GreenPowerProxy, - Groups, - Identify, - OnOff, - Ota, - Scenes, -) -from zigpy.zcl.clusters.homeautomation import Diagnostic, ElectricalMeasurement -from zigpy.zcl.clusters.smartenergy import DeviceManagement, Metering - -from zhaquirks.const import ( - DEVICE_TYPE, - ENDPOINTS, - INPUT_CLUSTERS, - MODELS_INFO, - OUTPUT_CLUSTERS, - PROFILE_ID, -) -from zhaquirks.schneiderelectric import SE_MANUF_NAME +from typing import Final + +from zigpy import types as t +from zigpy.quirks import CustomCluster +from zigpy.quirks.v2 import QuirkBuilder +from zigpy.zcl.clusters.smartenergy import Metering +from zigpy.zcl.foundation import DataTypeId, ZCLAttributeDef + +from zhaquirks.schneiderelectric import SE_MANUF_NAME, SEBasic + + +class SEIndicatorLuminanceLevel(t.enum8): + """Indicator luminance level.""" + + Level100 = 0x00 + Level80 = 0x01 + Level60 = 0x02 + Level40 = 0x03 + Level20 = 0x04 + Level0 = 0x05 + + +class SEIndicatorMode(t.enum8): + """Indicator mode.""" + + InverseOfOutput = 0x00 + FollowsOutput = 0x01 + AlwaysOff = 0x02 + AlwaysOn = 0x03 -class MeteringCluster(CustomCluster, Metering): +class SELocalControlMode(t.enum8): + """Local control mode.""" + + Active = 0x00 + Inactive = 0x01 + + +class SEOutletConfiguration(CustomCluster): + """Schneider Electric Outlet Configuration cluster.""" + + cluster_id = 0xFC04 + name = "SEOutletConfiguration" + + class AttributeDefs(CustomCluster.AttributeDefs): + """Attribute definitions.""" + + se_indicator_luminance_level: Final = ZCLAttributeDef( + id=0x0000, + type=t.uint8_t, + zcl_type=DataTypeId.uint8, + access="rw", + is_manufacturer_specific=True, + ) + se_indicator_mode: Final = ZCLAttributeDef( + id=0x0002, + type=SEIndicatorMode, + zcl_type=DataTypeId.uint8, + access="rw", + is_manufacturer_specific=True, + ) + se_local_control_mode: Final = ZCLAttributeDef( + id=0x0050, + type=SELocalControlMode, + zcl_type=DataTypeId.uint8, + access="rw", + is_manufacturer_specific=True, + ) + + +class SEMeteringCluster(CustomCluster, Metering): """Custom Metering cluster to fix instantaneous demand value multiplied by 1000.""" def _update_attribute(self, attrid, value): @@ -34,76 +79,34 @@ def _update_attribute(self, attrid, value): super()._update_attribute(attrid, value) -class SocketOutlet(CustomDevice): - """Schneider Electric Socket outlet WDE002182, WDE002172.""" - - signature = { - MODELS_INFO: [ - (SE_MANUF_NAME, "SOCKET/OUTLET/1"), - (SE_MANUF_NAME, "SOCKET/OUTLET/2"), - ], - ENDPOINTS: { - # - 6: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.MAIN_POWER_OUTLET, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - Groups.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - Metering.cluster_id, - DeviceManagement.cluster_id, - ElectricalMeasurement.cluster_id, - Diagnostic.cluster_id, - 0xFC04, - ], - OUTPUT_CLUSTERS: [ - Ota.cluster_id, - ], - }, - 242: { - PROFILE_ID: zgp.PROFILE_ID, - DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, - INPUT_CLUSTERS: [], - OUTPUT_CLUSTERS: [ - GreenPowerProxy.cluster_id, - ], - }, - }, - } - - replacement = { - ENDPOINTS: { - 6: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.MAIN_POWER_OUTLET, - INPUT_CLUSTERS: [ - Basic.cluster_id, - Identify.cluster_id, - Groups.cluster_id, - Scenes.cluster_id, - OnOff.cluster_id, - MeteringCluster, - DeviceManagement.cluster_id, - ElectricalMeasurement.cluster_id, - Diagnostic.cluster_id, - 0xFC04, - ], - OUTPUT_CLUSTERS: [ - Ota.cluster_id, - ], - }, - 242: { - PROFILE_ID: zgp.PROFILE_ID, - DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, - INPUT_CLUSTERS: [], - OUTPUT_CLUSTERS: [ - GreenPowerProxy.cluster_id, - ], - }, - }, - } +( + QuirkBuilder(SE_MANUF_NAME, "SOCKET/OUTLET/1") + .applies_to(SE_MANUF_NAME, "SOCKET/OUTLET/2") + .replaces(SEBasic, endpoint_id=6) + .replaces(SEMeteringCluster, endpoint_id=6) + .replaces(SEOutletConfiguration, endpoint_id=6) + .enum( + cluster_id=SEOutletConfiguration.cluster_id, + endpoint_id=6, + attribute_name=SEOutletConfiguration.AttributeDefs.se_indicator_luminance_level.name, + enum_class=SEIndicatorLuminanceLevel, + translation_key="indicator_luminance_level", + fallback_name="Indicator luminance level", + ) + .enum( + cluster_id=SEOutletConfiguration.cluster_id, + endpoint_id=6, + attribute_name=SEOutletConfiguration.AttributeDefs.se_indicator_mode.name, + enum_class=SEIndicatorMode, + translation_key="indicator_mode", + fallback_name="Indicator mode", + ) + .enum( + cluster_id=SEOutletConfiguration.cluster_id, + endpoint_id=6, + attribute_name=SEOutletConfiguration.AttributeDefs.se_local_control_mode.name, + enum_class=SELocalControlMode, + translation_key="local_control_mode", + fallback_name="Local control mode", + ) +) From ee1cac9f6041ac55822633e10cd11393836f7169 Mon Sep 17 00:00:00 2001 From: Nikita Uvarov Date: Tue, 25 Nov 2025 13:42:53 +0100 Subject: [PATCH 2/7] Add missing .add_to_registry() --- zhaquirks/schneiderelectric/outlet.py | 1 + 1 file changed, 1 insertion(+) diff --git a/zhaquirks/schneiderelectric/outlet.py b/zhaquirks/schneiderelectric/outlet.py index da6ca73ed6..934f3a69dc 100644 --- a/zhaquirks/schneiderelectric/outlet.py +++ b/zhaquirks/schneiderelectric/outlet.py @@ -109,4 +109,5 @@ def _update_attribute(self, attrid, value): translation_key="local_control_mode", fallback_name="Local control mode", ) + .add_to_registry() ) From caf2a1e59b6f58947ae402cc14e03f609af9aff8 Mon Sep 17 00:00:00 2001 From: Nikita Uvarov Date: Tue, 25 Nov 2025 13:43:12 +0100 Subject: [PATCH 3/7] Fix Schneider outlet tests --- tests/test_schneiderelectric.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/test_schneiderelectric.py b/tests/test_schneiderelectric.py index 00c8cc050b..23e4d709e4 100644 --- a/tests/test_schneiderelectric.py +++ b/tests/test_schneiderelectric.py @@ -3,7 +3,7 @@ from unittest import mock import pytest -from zigpy.zcl import foundation +from zigpy.zcl import ClusterType, foundation from zigpy.zcl.clusters.closures import WindowCovering from zigpy.zcl.clusters.smartenergy import Metering @@ -86,10 +86,13 @@ async def test_1gang_shutter_1_lift_percentage_updates(zigpy_device_from_v2_quir assert len(cluster_listener.cluster_commands) == 0 -@pytest.mark.parametrize("quirk", (zhaquirks.schneiderelectric.outlet.SocketOutlet,)) -async def test_schneider_device_temp(zigpy_device_from_quirk, quirk): +async def test_schneider_device_temp(zigpy_device_from_v2_quirk): """Test that instant demand is divided by 1000.""" - device = zigpy_device_from_quirk(quirk) + device = zigpy_device_from_v2_quirk( + manufacturer=SE_MANUF_NAME, + model="SOCKET/OUTLET/1", + cluster_ids={6: {Metering.cluster_id: ClusterType.Server}}, + ) metering_cluster = device.endpoints[6].smartenergy_metering metering_listener = ClusterListener(metering_cluster) From 551e7684b9422d3d1a298f8e835fed54364c694d Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 25 Nov 2025 12:44:27 +0000 Subject: [PATCH 4/7] Apply pre-commit auto fixes --- tests/test_schneiderelectric.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/test_schneiderelectric.py b/tests/test_schneiderelectric.py index 23e4d709e4..48313985d5 100644 --- a/tests/test_schneiderelectric.py +++ b/tests/test_schneiderelectric.py @@ -2,7 +2,6 @@ from unittest import mock -import pytest from zigpy.zcl import ClusterType, foundation from zigpy.zcl.clusters.closures import WindowCovering from zigpy.zcl.clusters.smartenergy import Metering From c7b2ffe2a50ed06ffdc1797fbf5eec91e4765df2 Mon Sep 17 00:00:00 2001 From: Nikita Uvarov Date: Tue, 25 Nov 2025 13:46:10 +0100 Subject: [PATCH 5/7] Change import --- tests/test_schneiderelectric.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_schneiderelectric.py b/tests/test_schneiderelectric.py index 48313985d5..8375506c77 100644 --- a/tests/test_schneiderelectric.py +++ b/tests/test_schneiderelectric.py @@ -7,8 +7,8 @@ from zigpy.zcl.clusters.smartenergy import Metering from tests.common import ClusterListener +import zhaquirks from zhaquirks.schneiderelectric import SE_MANUF_NAME -import zhaquirks.schneiderelectric.outlet zhaquirks.setup() From acd8f23f6ffe153a54caf60b5b202b5a6fd9a1a6 Mon Sep 17 00:00:00 2001 From: Nikita Uvarov Date: Wed, 26 Nov 2025 10:47:56 +0100 Subject: [PATCH 6/7] Expose lminance level as number --- zhaquirks/schneiderelectric/outlet.py | 18 ++++-------------- 1 file changed, 4 insertions(+), 14 deletions(-) diff --git a/zhaquirks/schneiderelectric/outlet.py b/zhaquirks/schneiderelectric/outlet.py index 934f3a69dc..ab7704835b 100644 --- a/zhaquirks/schneiderelectric/outlet.py +++ b/zhaquirks/schneiderelectric/outlet.py @@ -11,17 +11,6 @@ from zhaquirks.schneiderelectric import SE_MANUF_NAME, SEBasic -class SEIndicatorLuminanceLevel(t.enum8): - """Indicator luminance level.""" - - Level100 = 0x00 - Level80 = 0x01 - Level60 = 0x02 - Level40 = 0x03 - Level20 = 0x04 - Level0 = 0x05 - - class SEIndicatorMode(t.enum8): """Indicator mode.""" @@ -50,7 +39,6 @@ class AttributeDefs(CustomCluster.AttributeDefs): se_indicator_luminance_level: Final = ZCLAttributeDef( id=0x0000, type=t.uint8_t, - zcl_type=DataTypeId.uint8, access="rw", is_manufacturer_specific=True, ) @@ -85,11 +73,13 @@ def _update_attribute(self, attrid, value): .replaces(SEBasic, endpoint_id=6) .replaces(SEMeteringCluster, endpoint_id=6) .replaces(SEOutletConfiguration, endpoint_id=6) - .enum( + .number( cluster_id=SEOutletConfiguration.cluster_id, endpoint_id=6, attribute_name=SEOutletConfiguration.AttributeDefs.se_indicator_luminance_level.name, - enum_class=SEIndicatorLuminanceLevel, + min_value=0, + max_value=5, + step=1, translation_key="indicator_luminance_level", fallback_name="Indicator luminance level", ) From 427659d8f8ff22d5180dc7d511f91c5c03f96c36 Mon Sep 17 00:00:00 2001 From: Nikita Uvarov Date: Thu, 27 Nov 2025 20:03:49 +0100 Subject: [PATCH 7/7] Fix se_local_control_mode zcl_type --- zhaquirks/schneiderelectric/outlet.py | 1 - 1 file changed, 1 deletion(-) diff --git a/zhaquirks/schneiderelectric/outlet.py b/zhaquirks/schneiderelectric/outlet.py index ab7704835b..7764fcb19c 100644 --- a/zhaquirks/schneiderelectric/outlet.py +++ b/zhaquirks/schneiderelectric/outlet.py @@ -52,7 +52,6 @@ class AttributeDefs(CustomCluster.AttributeDefs): se_local_control_mode: Final = ZCLAttributeDef( id=0x0050, type=SELocalControlMode, - zcl_type=DataTypeId.uint8, access="rw", is_manufacturer_specific=True, )