diff --git a/tests/test_quirks.py b/tests/test_quirks.py index 36f68f1ee3..b818e5ad79 100644 --- a/tests/test_quirks.py +++ b/tests/test_quirks.py @@ -913,9 +913,6 @@ def check_for_duplicate_cluster_ids(clusters) -> None: zhaquirks.ikea.twobtnremote.IkeaTradfriRemote2BtnZLL, # # -- other devices -- - # adds DoorLock cluster to output clusters (Yale door locks): - zhaquirks.yale.realliving.YRD210PBDB220TSLL, - zhaquirks.yale.realliving.YRD220240TSDB, # remove LevelControl input cluster (Adurolight remote): zhaquirks.aduro.adurolightncc.AdurolightNCC, # add a bunch of output clusters (Zhongxing motion sensor): diff --git a/tests/test_yale.py b/tests/test_yale.py new file mode 100644 index 0000000000..f6bac193fd --- /dev/null +++ b/tests/test_yale.py @@ -0,0 +1,43 @@ +"""Tests for Yale locks.""" + +import pytest + +import zhaquirks +from zhaquirks.yale.lock import lang_converter, sound_volume_converter + +zhaquirks.setup() + + +@pytest.mark.parametrize( + ("raw_lang", "expected"), + [ + # accounted for mappings + ("en", "English"), + ("fr", "French"), + ("es", "Spanish"), + # fallback + ("de", "de"), + ("unknown_code", "unknown_code"), + ], +) +def test_lang_converter(raw_lang: str, expected: str): + """Test language converter.""" + assert lang_converter(raw_lang) == expected + + +@pytest.mark.parametrize( + ("raw_level", "expected"), + [ + # accounted for mappings + (0, "Silent"), + (1, "Low volume"), + (2, "High volume"), + # fallback + (3, "Unknown (3)"), + (-1, "Unknown (-1)"), + (None, "Unknown (None)"), + ], +) +def test_sound_volume_converter(raw_level: int | None, expected: str): + """Test sound volume level conversions.""" + assert sound_volume_converter(raw_level) == expected diff --git a/zhaquirks/yale/lock.py b/zhaquirks/yale/lock.py new file mode 100644 index 0000000000..bdecca1e90 --- /dev/null +++ b/zhaquirks/yale/lock.py @@ -0,0 +1,124 @@ +"""Device handler for Yale Zigbee Network Modules.""" + +from zigpy import types as t +from zigpy.zcl.clusters.closures import DoorLock + +from zhaquirks import DoublingPowerConfigurationCluster +from zhaquirks.builder import ( + NumberDeviceClass, + QuirkBuilder, + ReportingConfig, + SensorDeviceClass, + UnitOfTime, +) + + +def lang_converter(lang: str) -> str: + """Convert language string into frontend UI strings.""" + mapping = { + "en": "English", + "fr": "French", + "es": "Spanish", + } + return mapping.get(lang.strip().lower(), lang) + + +def sound_volume_converter(level: int | None) -> str | None: + """Convert lock volume int into frontend UI strings.""" + mapping = { + 0: "Silent", + 1: "Low volume", + 2: "High volume", + } + return mapping.get(level, f"Unknown ({level})") + + +class yale_lock_mode(t.enum8): + """Lock operation mode enum.""" + + Normal = 0x00 + Vacation = 0x01 + Privacy = 0x02 + + +( + QuirkBuilder("Yale", "YRD220/240 TSDB") + .applies_to("Yale", "YRD210 PB DB") + .applies_to("Yale", "YRL220 TS LL") + .replaces(DoublingPowerConfigurationCluster) + .binary_sensor( + attribute_name=DoorLock.AttributeDefs.auto_relock_time.name, + cluster_id=DoorLock.cluster_id, + attribute_converter=lambda value: bool(value > 0), + reporting_config=ReportingConfig( + min_interval=3600, + max_interval=10800, + reportable_change=1, + ), + unique_id_suffix="auto_relock_enabled", + translation_key="auto_relock_enabled", + fallback_name="Auto-relock enabled", + ) + .switch( + attribute_name=DoorLock.AttributeDefs.enable_one_touch_locking.name, + cluster_id=DoorLock.cluster_id, + unique_id_suffix="one_touch_locking", + translation_key="one_touch_locking", + fallback_name="One touch locking", + ) + .switch( + attribute_name=DoorLock.AttributeDefs.enable_inside_status_led.name, + cluster_id=DoorLock.cluster_id, + unique_id_suffix="inside_status_led", + translation_key="inside_status_led", + fallback_name="Inside status LED", + ) + .switch( + attribute_name=DoorLock.AttributeDefs.enable_privacy_mode_button.name, + cluster_id=DoorLock.cluster_id, + unique_id_suffix="button_privacy", + translation_key="button_privacy", + fallback_name="Button privacy", + ) + .enum( + attribute_name=DoorLock.AttributeDefs.operating_mode.name, + cluster_id=DoorLock.cluster_id, + enum_class=yale_lock_mode, + unique_id_suffix="operating_mode", + translation_key="operating_mode", + fallback_name="Operating mode", + ) + .number( + attribute_name=DoorLock.AttributeDefs.auto_relock_time.name, + cluster_id=DoorLock.cluster_id, + step=10, + min_value=10, + max_value=180, + unique_id_suffix="auto_relock_time", + device_class=NumberDeviceClass.DURATION, + unit=UnitOfTime.SECONDS, + translation_key="auto_relock_time", + fallback_name="Auto-relock time", + ) + .sensor( + endpoint_id=1, + cluster_id=DoorLock.cluster_id, + attribute_name=DoorLock.AttributeDefs.language.name, + unique_id_suffix="announcement_language", + device_class=SensorDeviceClass.ENUM, + attribute_converter=lang_converter, + translation_key="announcement_language", + fallback_name="Announcement language", + ) + .sensor( + endpoint_id=1, + cluster_id=DoorLock.cluster_id, + attribute_name=DoorLock.AttributeDefs.sound_volume.name, + unique_id_suffix="keypad_sound_volume", + device_class=SensorDeviceClass.ENUM, + attribute_converter=sound_volume_converter, + translation_key="keypad_sound_volume", + fallback_name="Keypad sound volume", + ) + .add_to_registry() +) diff --git a/zhaquirks/yale/realliving.py b/zhaquirks/yale/realliving.py deleted file mode 100644 index 383fb13e96..0000000000 --- a/zhaquirks/yale/realliving.py +++ /dev/null @@ -1,108 +0,0 @@ -"""Device handler for Yale Real Living.""" - -from zigpy.profiles import zha -from zigpy.zcl.clusters.closures import DoorLock -from zigpy.zcl.clusters.general import Alarms, Basic, Identify, Ota, PollControl, Time - -from zhaquirks import DoublingPowerConfigurationCluster -from zhaquirks.const import ( - DEVICE_TYPE, - ENDPOINTS, - INPUT_CLUSTERS, - MODELS_INFO, - OUTPUT_CLUSTERS, - PROFILE_ID, -) -from zhaquirks.legacy import CustomDevice - - -class YRD210PBDB220TSLL(CustomDevice): - """Yale YRD210 PB BP and Yale YRL220 TS LL Locks.""" - - signature = { - # - MODELS_INFO: [("Yale", "YRD210 PB DB"), ("Yale", "YRL220 TS LL")], - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.DOOR_LOCK, - INPUT_CLUSTERS: [ - Basic.cluster_id, - DoublingPowerConfigurationCluster.cluster_id, - Identify.cluster_id, - Alarms.cluster_id, - Time.cluster_id, - DoorLock.cluster_id, - PollControl.cluster_id, - ], - OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], - } - }, - } - - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.DOOR_LOCK, - INPUT_CLUSTERS: [ - Basic.cluster_id, - DoublingPowerConfigurationCluster, - Identify.cluster_id, - Alarms.cluster_id, - Time.cluster_id, - DoorLock.cluster_id, - PollControl.cluster_id, - ], - OUTPUT_CLUSTERS: [DoorLock.cluster_id, Ota.cluster_id], - } - } - } - - -class YRD220240TSDB(CustomDevice): - """Yale YRD220/240 TSDB Lock.""" - - signature = { - # - MODELS_INFO: [("Yale", "YRD220/240 TSDB")], - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.DOOR_LOCK, - INPUT_CLUSTERS: [ - Basic.cluster_id, - DoublingPowerConfigurationCluster.cluster_id, - Alarms.cluster_id, - Time.cluster_id, - PollControl.cluster_id, - DoorLock.cluster_id, - ], - OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id], - } - }, - } - - replacement = { - ENDPOINTS: { - 1: { - PROFILE_ID: zha.PROFILE_ID, - DEVICE_TYPE: zha.DeviceType.DOOR_LOCK, - INPUT_CLUSTERS: [ - Basic.cluster_id, - DoublingPowerConfigurationCluster, - Alarms.cluster_id, - Time.cluster_id, - PollControl.cluster_id, - DoorLock.cluster_id, - ], - OUTPUT_CLUSTERS: [DoorLock.cluster_id, Ota.cluster_id], - } - } - }