Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions tests/test_quirks.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
43 changes: 43 additions & 0 deletions tests/test_yale.py
Original file line number Diff line number Diff line change
@@ -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"),

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to hardcode this or does HA have a built-in converter? According to this document, these should be ISO-639-1 two character codes.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to Yale's official integrators guide, these are the only three languages that all their locks support, so I think this will do for now.

("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
124 changes: 124 additions & 0 deletions zhaquirks/yale/lock.py
Original file line number Diff line number Diff line change
@@ -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,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

According to the integrator's guide, this value should be able to be set to 0 to disable auto-relock from Zigbee, however, on my lock, this does not work, the auto-relock time only goes from 10-180, all other values fail.

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,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This should also be set from Zigbee, but the value won't update.

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,

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as this, Yale did mention that only 0 and 2 (mute and high vol) can be set on push-button type locks, however from my testing none can be set.

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()
)
108 changes: 0 additions & 108 deletions zhaquirks/yale/realliving.py

This file was deleted.

Loading