diff --git a/tests/test_schneiderelectric.py b/tests/test_schneiderelectric.py index 4d43c58476..00c8cc050b 100644 --- a/tests/test_schneiderelectric.py +++ b/tests/test_schneiderelectric.py @@ -8,77 +8,19 @@ from zigpy.zcl.clusters.smartenergy import Metering from tests.common import ClusterListener +from zhaquirks.schneiderelectric import SE_MANUF_NAME import zhaquirks.schneiderelectric.outlet -import zhaquirks.schneiderelectric.shutters zhaquirks.setup() -def test_1gang_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", - "0x0005", - "0x0006", - "0x0008", - "0x0019", - "0x0102", - ], - }, - }, - "manufacturer": "Schneider Electric", - "model": "1GANG/SHUTTER/1", - "class": "zigpy.device.Device", - } - assert_signature_matches_quirk( - zhaquirks.schneiderelectric.shutters.OneGangShutter1, signature - ) - - -async def test_1gang_shutter_1_go_to_lift_percentage_cmd(zigpy_device_from_quirk): +async def test_1gang_shutter_1_go_to_lift_percentage_cmd(zigpy_device_from_v2_quirk): """Asserts that the go_to_lift_percentage command inverts the percentage value.""" - device = zigpy_device_from_quirk( - zhaquirks.schneiderelectric.shutters.OneGangShutter1 + device = zigpy_device_from_v2_quirk( + manufacturer=SE_MANUF_NAME, + model="1GANG/SHUTTER/1", + endpoint_ids=[5, 21], ) window_covering_cluster = device.endpoints[5].window_covering @@ -95,11 +37,13 @@ async def test_1gang_shutter_1_go_to_lift_percentage_cmd(zigpy_device_from_quirk assert request_mock.call_args[0][3] == 42 # 100 - 58 -async def test_1gang_shutter_1_unpatched_cmd(zigpy_device_from_quirk): +async def test_1gang_shutter_1_unpatched_cmd(zigpy_device_from_v2_quirk): """Asserts that unpatched ZCL commands keep working.""" - device = zigpy_device_from_quirk( - zhaquirks.schneiderelectric.shutters.OneGangShutter1 + device = zigpy_device_from_v2_quirk( + manufacturer=SE_MANUF_NAME, + model="1GANG/SHUTTER/1", + endpoint_ids=[5, 21], ) window_covering_cluster = device.endpoints[5].window_covering @@ -115,14 +59,16 @@ async def test_1gang_shutter_1_unpatched_cmd(zigpy_device_from_quirk): ) -async def test_1gang_shutter_1_lift_percentage_updates(zigpy_device_from_quirk): +async def test_1gang_shutter_1_lift_percentage_updates(zigpy_device_from_v2_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.OneGangShutter1 + device = zigpy_device_from_v2_quirk( + manufacturer=SE_MANUF_NAME, + model="1GANG/SHUTTER/1", + endpoint_ids=[5, 21], ) window_covering_cluster = device.endpoints[5].window_covering cluster_listener = ClusterListener(window_covering_cluster) diff --git a/zhaquirks/schneiderelectric/__init__.py b/zhaquirks/schneiderelectric/__init__.py index 9f8246d121..db10abdf46 100644 --- a/zhaquirks/schneiderelectric/__init__.py +++ b/zhaquirks/schneiderelectric/__init__.py @@ -155,6 +155,20 @@ class SEDimmingCurve(t.enum8): Exponential = 2 # Not supported in current FW, but defined in the spec +class SESunProtectionStatus(t.bitmap8): + """Window covering protetion status.""" + + NotActive = 0 + Active = 1 + + +class SESunProtectionSensor(t.bitmap8): + """Window covering protetion status.""" + + NotConnected = 0 + Connected = 1 + + class SEBallast(CustomCluster, Ballast): """Schneider Electric Ballast cluster.""" @@ -180,49 +194,85 @@ class SEWindowCovering(CustomCluster, WindowCovering): class AttributeDefs(WindowCovering.AttributeDefs): """Attribute definitions.""" - unknown_attribute_65533: Final = ZCLAttributeDef( - id=0xFFFD, - type=t.uint16_t, - is_manufacturer_specific=True, - ) - lift_duration: Final = ZCLAttributeDef( + # Obsolete, use se_lift_drive_up_time and se_lift_drive_down_time instead. + se_lift_duration: Final = ZCLAttributeDef( id=0xE000, type=t.uint16_t, + access="rw", is_manufacturer_specific=True, ) - unknown_attribute_57360: Final = ZCLAttributeDef( + se_sun_protection_status: Final = ZCLAttributeDef( id=0xE010, - type=t.bitmap8, + type=SESunProtectionStatus, + access="r", is_manufacturer_specific=True, ) - unknown_attribute_57362: Final = ZCLAttributeDef( + # SunProtectionIlluminanceThreshold = 10,000 x log10 Illuminance. + # Where 1 lx <= Illuminance <=3.576 Mlx, corresponding to a SunProtectionIlluminanceThreshold in the range 0 to 0xfffe. + # A value of 0xffff indicates that this attribute is not valid. + se_sun_protection_illuminance_thershold: Final = ZCLAttributeDef( id=0xE012, type=t.uint16_t, + access="rw", is_manufacturer_specific=True, ) - unknown_attribute_57363: Final = ZCLAttributeDef( + se_sun_protection_sensor: Final = ZCLAttributeDef( id=0xE013, - type=t.bitmap8, + type=SESunProtectionSensor, + access="rw", is_manufacturer_specific=True, ) - unknown_attribute_57364: Final = ZCLAttributeDef( + # Driving time from fully close to fully open state in 1/10 seconds + se_lift_drive_up_time: Final = ZCLAttributeDef( id=0xE014, type=t.uint16_t, + access="rw", is_manufacturer_specific=True, ) - unknown_attribute_57365: Final = ZCLAttributeDef( + # Driving time from fully open to fully close state in 1/10 seconds + se_lift_drive_down_time: Final = ZCLAttributeDef( id=0xE015, type=t.uint16_t, + access="rw", is_manufacturer_specific=True, ) - unknown_attribute_57366: Final = ZCLAttributeDef( + # Time from fully open to fully close tilt position in 1/100 seconds. + # This time is also taken as base for calculation of step size in Schneider manufacture specific command StopOrStepLiftPercentage. + # If set to 0, WindosCoveringType attribute is automatically set to 0 (lift only). + se_tilt_open_close_and_step_time: Final = ZCLAttributeDef( id=0xE016, type=t.uint16_t, + access="rw", is_manufacturer_specific=True, ) - unknown_attribute_57367: Final = ZCLAttributeDef( + # Tilt position in percent adopted by tilt after receiving go to lift percentage command. + # Values 0-100 are absolute position of tilt with following meaning: + # 100: Position of tilt when shutter is moving up (usually up). + # 0: Position of tilt when shutter is moving down (usually down). + # 255: No action after command. + # 101-254: Tilt position before movement is restored. + se_tilt_position_percentage_after_move_to_level: Final = ZCLAttributeDef( id=0xE017, type=t.uint8_t, + access="rw", + is_manufacturer_specific=True, + ) + se_cluster_revision: Final = ZCLAttributeDef( + id=0xFFFD, + type=t.uint16_t, + access="r", + is_manufacturer_specific=True, + ) + + class ServerCommandDefs(WindowCovering.ServerCommandDefs): + """Server command definitions.""" + + se_stop_or_step_lift_percentage: Final = foundation.ZCLCommandDef( + id=0x80, + schema={ + "direction": t.uint8_t, + "step_value": t.uint8_t, + }, is_manufacturer_specific=True, ) @@ -305,11 +355,11 @@ class SESwitchAction(t.enum8): NotUsed = 0x7F -class SESpecific(CustomCluster): - """Schneider Electric manufacturer specific cluster.""" +class SESwitchConfiguration(CustomCluster): + """Schneider Electric manufacturer specific switch configuration cluster.""" - name = "Schneider Electric Manufacturer Specific" - ep_attribute = "schneider_electric_manufacturer" + name = "Schneider Electric Manufacturer Specific Switch Configuration" + ep_attribute = "switch_configuration" cluster_id = 0xFF17 class AttributeDefs(BaseAttributeDefs): diff --git a/zhaquirks/schneiderelectric/dimmers.py b/zhaquirks/schneiderelectric/dimmers.py index 098a113740..969a168a23 100644 --- a/zhaquirks/schneiderelectric/dimmers.py +++ b/zhaquirks/schneiderelectric/dimmers.py @@ -7,7 +7,7 @@ SEBallast, SEBasic, SEOnOff, - SESpecific, + SESwitchConfiguration, ) ( @@ -19,7 +19,7 @@ .replaces(SEBallast, endpoint_id=3) .replaces(SEOnOff, endpoint_id=3) .replaces(SEBasic, endpoint_id=21) - .replaces(SESpecific, endpoint_id=21) + .replaces(SESwitchConfiguration, endpoint_id=21) .add_to_registry() ) @@ -29,6 +29,6 @@ .replaces(SEBasic) .replaces(SEOnOff) .replaces(SEBasic, endpoint_id=21) - .replaces(SESpecific, endpoint_id=21) + .replaces(SESwitchConfiguration, endpoint_id=21) .add_to_registry() ) diff --git a/zhaquirks/schneiderelectric/shutters.py b/zhaquirks/schneiderelectric/shutters.py index 8af61b56a4..7550f69612 100644 --- a/zhaquirks/schneiderelectric/shutters.py +++ b/zhaquirks/schneiderelectric/shutters.py @@ -1,117 +1,131 @@ """Quirks for Schneider Electric shutters.""" -from zigpy.profiles import zha -from zigpy.quirks import CustomDevice -from zigpy.zcl.clusters.closures import WindowCovering -from zigpy.zcl.clusters.general import ( - Basic, - Groups, - Identify, - LevelControl, - OnOff, - Ota, - Scenes, -) -from zigpy.zcl.clusters.homeautomation import Diagnostic +from zigpy.quirks.v2 import QuirkBuilder +from zigpy.quirks.v2.homeassistant import UnitOfTime +from zigpy.zcl import ClusterType -from zhaquirks.const import ( - DEVICE_TYPE, - ENDPOINTS, - INPUT_CLUSTERS, - MODELS_INFO, - OUTPUT_CLUSTERS, - PROFILE_ID, -) from zhaquirks.schneiderelectric import ( SE_MANUF_NAME, SEBasic, - SESpecific, + SESwitchAction, + SESwitchConfiguration, + SESwitchIndication, SEWindowCovering, ) +( + QuirkBuilder(SE_MANUF_NAME, "1GANG/SHUTTER/1") + .applies_to(SE_MANUF_NAME, "NHPB/SHUTTER/1") + .replaces(SEBasic, endpoint_id=5) + .replaces(SEWindowCovering, endpoint_id=5) + .replaces(SEBasic, endpoint_id=21) + .replaces(SESwitchConfiguration, endpoint_id=21) + .replaces(SEWindowCovering, endpoint_id=21, cluster_type=ClusterType.Client) + .enum( + endpoint_id=21, + cluster_id=SESwitchConfiguration.cluster_id, + attribute_name=SESwitchConfiguration.AttributeDefs.se_switch_indication.name, + enum_class=SESwitchIndication, + translation_key="switch_indication", + fallback_name="Switch indication", + ) + .enum( + endpoint_id=21, + cluster_id=SESwitchConfiguration.cluster_id, + attribute_name=SESwitchConfiguration.AttributeDefs.se_switch_actions.name, + enum_class=SESwitchAction, + translation_key="switch_actions", + fallback_name="Switch actions", + ) + .number( + endpoint_id=5, + cluster_id=SEWindowCovering.cluster_id, + attribute_name=SEWindowCovering.AttributeDefs.se_lift_drive_up_time.name, + unit=UnitOfTime.SECONDS, + multiplier=0.1, + min_value=1, + max_value=300, + step=0.1, + translation_key="lift_drive_up_time", + fallback_name="Lift drive up time", + ) + .number( + endpoint_id=5, + cluster_id=SEWindowCovering.cluster_id, + attribute_name=SEWindowCovering.AttributeDefs.se_lift_drive_down_time.name, + unit=UnitOfTime.SECONDS, + multiplier=0.1, + min_value=1, + max_value=300, + step=0.1, + translation_key="lift_drive_down_time", + fallback_name="Lift drive down time", + ) + .number( + endpoint_id=5, + cluster_id=SEWindowCovering.cluster_id, + attribute_name=SEWindowCovering.AttributeDefs.se_tilt_open_close_and_step_time.name, + unit=UnitOfTime.SECONDS, + multiplier=0.01, + min_value=0, + max_value=30, + step=0.01, + translation_key="tilt_open_close_and_step_time", + fallback_name="Tilt open close and step time", + ) + .number( + endpoint_id=5, + cluster_id=SEWindowCovering.cluster_id, + attribute_name=SEWindowCovering.AttributeDefs.se_tilt_position_percentage_after_move_to_level.name, + min_value=0, + max_value=255, + step=1, + translation_key="tilt_position_percentage_after_move_to_level", + fallback_name="Tilt position percentage after move to level", + ) + .add_to_registry() +) -class OneGangShutter1(CustomDevice): - """1GANG/SHUTTER/1 from Schneider Electric.""" - - signature = { - MODELS_INFO: [ - (SE_MANUF_NAME, "1GANG/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, - Scenes.cluster_id, - OnOff.cluster_id, - LevelControl.cluster_id, - Ota.cluster_id, - WindowCovering.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, - Scenes.cluster_id, - OnOff.cluster_id, - LevelControl.cluster_id, - Ota.cluster_id, - SEWindowCovering, - ], - }, - } - } +( + QuirkBuilder(SE_MANUF_NAME, "PUCK/SHUTTER/1") + .replaces(SEBasic, endpoint_id=5) + .replaces(SEWindowCovering, endpoint_id=5) + .number( + endpoint_id=5, + cluster_id=SEWindowCovering.cluster_id, + attribute_name=SEWindowCovering.AttributeDefs.se_lift_drive_up_time.name, + unit=UnitOfTime.SECONDS, + multiplier=0.1, + translation_key="lift_drive_up_time", + fallback_name="Lift drive up time", + ) + .number( + endpoint_id=5, + cluster_id=SEWindowCovering.cluster_id, + attribute_name=SEWindowCovering.AttributeDefs.se_lift_drive_down_time.name, + unit=UnitOfTime.SECONDS, + multiplier=0.1, + translation_key="lift_drive_down_time", + fallback_name="Lift drive down time", + ) + .number( + endpoint_id=5, + cluster_id=SEWindowCovering.cluster_id, + attribute_name=SEWindowCovering.AttributeDefs.se_tilt_open_close_and_step_time.name, + unit=UnitOfTime.SECONDS, + multiplier=0.01, + translation_key="tilt_open_close_and_step_time", + fallback_name="Tilt open close and step time", + ) + .number( + endpoint_id=5, + cluster_id=SEWindowCovering.cluster_id, + attribute_name=SEWindowCovering.AttributeDefs.se_tilt_position_percentage_after_move_to_level.name, + min_value=0, + max_value=255, + step=1, + translation_key="tilt_position_percentage_after_move_to_level", + fallback_name="Tilt position percentage after move to level", + ) + .add_to_registry() +)