diff --git a/tests/test_schneiderelectric.py b/tests/test_schneiderelectric.py index d35cdfb888..e1b7743209 100644 --- a/tests/test_schneiderelectric.py +++ b/tests/test_schneiderelectric.py @@ -4,6 +4,7 @@ from zigpy.zcl import foundation from zigpy.zcl.clusters.closures import WindowCovering +import zhaquirks.schneiderelectric.dimmers import zhaquirks.schneiderelectric.shutters from tests.common import ClusterListener @@ -132,3 +133,61 @@ async def test_1gang_shutter_1_lift_percentage_updates(zigpy_device_from_quirk): 23, # 100 - 77 ) assert len(cluster_listener.cluster_commands) == 0 + + +def test_nh_rotary_dimmer_1_signature(assert_signature_matches_quirk): + signature = { + "node_descriptor": ( + "NodeDescriptor(logical_type=, complex_descriptor_available=0, " + "user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=, " + "mac_capability_flags=, manufacturer_code=4190, maximum_buffer_size=82, maximum_incoming_transfer_size=82, " + "server_mask=11264, maximum_outgoing_transfer_size=82, " + "descriptor_capability_field=, *allocate_address=True, " + "*is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=False, " + "*is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, " + "*is_security_capable=False)" + ), + "endpoints": { + "3": { + "profile_id": 0x0104, + "device_type": "0x0101", + "in_clusters": [ + "0x0000", + "0x0003", + "0x0004", + "0x0005", + "0x0006", + "0x0008", + "0x0301", + "0x0b05", + ], + "out_clusters": ["0x0019"], + }, + "21": { + "profile_id": 0x0104, + "device_type": "0x0104", + "in_clusters": ["0x0000", "0x0003", "0x0b05", "0xff17"], + "out_clusters": [ + "0x0003", + "0x0004", + "0x0005", + "0x0006", + "0x0008", + "0x0102", + ], + }, + "242": { + "profile_id": 0xA1E0, + "device_type": "0x0061", + "in_clusters": [], + "out_clusters": ["0x0021"], + }, + }, + "manufacturer": "Schneider Electric", + "model": "NHROTARY/DIMMER/1", + "class": "zigpy.device.Device", + } + assert_signature_matches_quirk( + zhaquirks.schneiderelectric.dimmers.NHRotaryDimmer1, signature + ) diff --git a/zhaquirks/schneiderelectric/dimmers.py b/zhaquirks/schneiderelectric/dimmers.py new file mode 100644 index 0000000000..32e2303726 --- /dev/null +++ b/zhaquirks/schneiderelectric/dimmers.py @@ -0,0 +1,155 @@ +from typing import Final + +from zigpy.profiles import zgp, zha +from zigpy.quirks import CustomCluster, CustomDevice +import zigpy.types as t +from zigpy.zcl.clusters.closures import WindowCovering +from zigpy.zcl.clusters.general import ( + Basic, + GreenPowerProxy, + Groups, + Identify, + LevelControl, + OnOff, + Ota, + Scenes, +) +from zigpy.zcl.clusters.homeautomation import Diagnostic +from zigpy.zcl.clusters.lighting import Ballast +from zigpy.zcl.foundation import ZCLAttributeDef + +from zhaquirks.const import ( + DEVICE_TYPE, + ENDPOINTS, + INPUT_CLUSTERS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PROFILE_ID, +) +from zhaquirks.schneiderelectric import SE_MANUF_ID, SE_MANUF_NAME, SEBasic, SESpecific + + +class ControlMode(t.enum8): + """Dimming mode for PUCK/DIMMER/* and NHROTARY/DIMMER/1""" + + Auto = 0 + RC = 1 + RL = 2 + RL_LED = 3 + + +class SEBallast(CustomCluster, Ballast): + manufacturer_id_override = SE_MANUF_ID + + class AttributeDefs(Ballast.AttributeDefs): + control_mode: Final = ZCLAttributeDef( + id=0xE000, type=ControlMode, is_manufacturer_specific=True + ) + unknown_attribute_e001: Final = ZCLAttributeDef( + id=0xE002, type=t.enum8, is_manufacturer_specific=True + ) + unknown_attribute_e002: Final = ZCLAttributeDef( + id=0xE002, type=t.enum8, is_manufacturer_specific=True + ) + + +class NHRotaryDimmer1(CustomDevice): + """NHROTARY/DIMMER/1 by Schneider Electric""" + + signature = { + MODELS_INFO: [ + (SE_MANUF_NAME, "NHROTARY/DIMMER/1"), + ], + ENDPOINTS: { + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.DIMMABLE_LIGHT, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + Ballast.cluster_id, + Diagnostic.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + }, + 21: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.DIMMER_SWITCH, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Diagnostic.cluster_id, + SESpecific.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + WindowCovering.cluster_id, + ], + }, + 242: { + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [ + GreenPowerProxy.cluster_id, + ], + }, + }, + } + replacement = { + ENDPOINTS: { + 3: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.DIMMABLE_LIGHT, + INPUT_CLUSTERS: [ + SEBasic, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + SEBallast, + Diagnostic.cluster_id, + ], + OUTPUT_CLUSTERS: [ + Ota.cluster_id, + ], + }, + 21: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.DIMMER_SWITCH, + INPUT_CLUSTERS: [ + SEBasic, + Identify.cluster_id, + Diagnostic.cluster_id, + SESpecific, + ], + OUTPUT_CLUSTERS: [ + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + OnOff.cluster_id, + LevelControl.cluster_id, + WindowCovering.cluster_id, + ], + }, + 242: { + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [ + GreenPowerProxy.cluster_id, + ], + }, + } + }