diff --git a/tests/test_schneiderelectric.py b/tests/test_schneiderelectric.py index 4d43c58476..377d8a03d3 100644 --- a/tests/test_schneiderelectric.py +++ b/tests/test_schneiderelectric.py @@ -140,6 +140,134 @@ async def test_1gang_shutter_1_lift_percentage_updates(zigpy_device_from_quirk): assert len(cluster_listener.cluster_commands) == 0 +def test_nhpb_shutter_1_signature(assert_signature_matches_quirk): + """Test signature.""" + 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=10752, 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": { + "5": { + "profile_id": 0x0104, + "device_type": "0x0202", + "in_clusters": [ + "0x0000", + "0x0003", + "0x0004", + "0x0005", + "0x0102", + "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": "NHPB/SHUTTER/1", + "class": "zigpy.device.Device", + } + assert_signature_matches_quirk( + zhaquirks.schneiderelectric.shutters.NhpbShutter1, signature + ) + + +async def test_nhpb_shutter_1_go_to_lift_percentage_cmd(zigpy_device_from_quirk): + """Asserts that the go_to_lift_percentage command inverts the percentage value.""" + + device = zigpy_device_from_quirk(zhaquirks.schneiderelectric.shutters.NhpbShutter1) + window_covering_cluster = device.endpoints[5].window_covering + + p = mock.patch.object(window_covering_cluster, "request", mock.AsyncMock()) + with p as request_mock: + request_mock.return_value = (foundation.Status.SUCCESS, "done") + + await window_covering_cluster.go_to_lift_percentage(58) + + assert request_mock.call_count == 1 + assert request_mock.call_args[0][1] == ( + WindowCovering.ServerCommandDefs.go_to_lift_percentage.id + ) + assert request_mock.call_args[0][3] == 42 # 100 - 58 + + +async def test_nhpb_shutter_1_unpatched_cmd(zigpy_device_from_quirk): + """Asserts that unpatched ZCL commands keep working.""" + + device = zigpy_device_from_quirk(zhaquirks.schneiderelectric.shutters.NhpbShutter1) + window_covering_cluster = device.endpoints[5].window_covering + + p = mock.patch.object(window_covering_cluster, "request", mock.AsyncMock()) + with p as request_mock: + request_mock.return_value = (foundation.Status.SUCCESS, "done") + + await window_covering_cluster.up_open() + + assert request_mock.call_count == 1 + assert request_mock.call_args[0][1] == ( + WindowCovering.ServerCommandDefs.up_open.id + ) + + +async def test_nhpb_shutter_1_lift_percentage_updates(zigpy_device_from_quirk): + """Asserts that updates to the ``current_position_lift_percentage`` attribute. + + (e.g., by the device) invert the reported percentage value. + """ + + device = zigpy_device_from_quirk(zhaquirks.schneiderelectric.shutters.NhpbShutter1) + window_covering_cluster = device.endpoints[5].window_covering + cluster_listener = ClusterListener(window_covering_cluster) + + window_covering_cluster.update_attribute( + WindowCovering.AttributeDefs.current_position_lift_percentage.id, + 77, + ) + + assert len(cluster_listener.attribute_updates) == 1 + assert cluster_listener.attribute_updates[0] == ( + WindowCovering.AttributeDefs.current_position_lift_percentage.id, + 23, # 100 - 77 + ) + 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): """Test that instant demand is divided by 1000.""" diff --git a/zhaquirks/schneiderelectric/__init__.py b/zhaquirks/schneiderelectric/__init__.py index 9f8246d121..9b3e2ec534 100644 --- a/zhaquirks/schneiderelectric/__init__.py +++ b/zhaquirks/schneiderelectric/__init__.py @@ -180,47 +180,47 @@ class SEWindowCovering(CustomCluster, WindowCovering): class AttributeDefs(WindowCovering.AttributeDefs): """Attribute definitions.""" - unknown_attribute_65533: Final = ZCLAttributeDef( + se_cluster_revision: Final = ZCLAttributeDef( id=0xFFFD, type=t.uint16_t, is_manufacturer_specific=True, ) - lift_duration: Final = ZCLAttributeDef( + se_drive_close_duration: Final = ZCLAttributeDef( id=0xE000, type=t.uint16_t, is_manufacturer_specific=True, ) - unknown_attribute_57360: Final = ZCLAttributeDef( + se_protection_status: Final = ZCLAttributeDef( id=0xE010, type=t.bitmap8, is_manufacturer_specific=True, ) - unknown_attribute_57362: Final = ZCLAttributeDef( + se_sun_protection_illuminance_threshold: Final = ZCLAttributeDef( id=0xE012, type=t.uint16_t, is_manufacturer_specific=True, ) - unknown_attribute_57363: Final = ZCLAttributeDef( + se_protection_sensor: Final = ZCLAttributeDef( id=0xE013, type=t.bitmap8, is_manufacturer_specific=True, ) - unknown_attribute_57364: Final = ZCLAttributeDef( + se_lift_drive_up_time: Final = ZCLAttributeDef( id=0xE014, type=t.uint16_t, is_manufacturer_specific=True, ) - unknown_attribute_57365: Final = ZCLAttributeDef( + se_lift_drive_down_time: Final = ZCLAttributeDef( id=0xE015, type=t.uint16_t, is_manufacturer_specific=True, ) - unknown_attribute_57366: Final = ZCLAttributeDef( + se_tilt_open_close_and_step_time: Final = ZCLAttributeDef( id=0xE016, type=t.uint16_t, is_manufacturer_specific=True, ) - unknown_attribute_57367: Final = ZCLAttributeDef( + se_tilt_position_percentage_after_move_to_level: Final = ZCLAttributeDef( id=0xE017, type=t.uint8_t, is_manufacturer_specific=True, diff --git a/zhaquirks/schneiderelectric/shutters.py b/zhaquirks/schneiderelectric/shutters.py index 8af61b56a4..e7b1377951 100644 --- a/zhaquirks/schneiderelectric/shutters.py +++ b/zhaquirks/schneiderelectric/shutters.py @@ -1,10 +1,11 @@ """Quirks for Schneider Electric shutters.""" -from zigpy.profiles import zha +from zigpy.profiles import zgp, zha from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.closures import WindowCovering from zigpy.zcl.clusters.general import ( Basic, + GreenPowerProxy, Groups, Identify, LevelControl, @@ -115,3 +116,110 @@ class OneGangShutter1(CustomDevice): }, } } + + +class NhpbShutter1(CustomDevice): + """NHPB/SHUTTER/1 from Schneider Electric.""" + + signature = { + MODELS_INFO: [ + (SE_MANUF_NAME, "NHPB/SHUTTER/1"), + ], + ENDPOINTS: { + # + 5: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + WindowCovering.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: { + 5: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.WINDOW_COVERING_DEVICE, + INPUT_CLUSTERS: [ + SEBasic, + Identify.cluster_id, + Groups.cluster_id, + Scenes.cluster_id, + SEWindowCovering, + 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, + SEWindowCovering, + ], + }, + 242: { + PROFILE_ID: zgp.PROFILE_ID, + DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC, + INPUT_CLUSTERS: [], + OUTPUT_CLUSTERS: [ + GreenPowerProxy.cluster_id, + ], + }, + } + }