Skip to content

Commit 363598d

Browse files
authored
Expose extra entities for Nimly Locks (#4138)
Expose extra entities for Nimly Locks: - Last action source - Last action - Last action user - Last pin code - Auto relock - Sound volume
1 parent e3124b5 commit 363598d

File tree

2 files changed

+216
-0
lines changed

2 files changed

+216
-0
lines changed

tests/test_nimly.py

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
"""Tests for Nimly lock quirks."""
2+
3+
import zhaquirks
4+
from zhaquirks.nimly.lock import (
5+
last_action_converter,
6+
last_action_source_converter,
7+
last_action_user_converter,
8+
)
9+
10+
zhaquirks.setup()
11+
12+
13+
def test_last_action_source_converter() -> None:
14+
"""Test the last action source converter for Nimly Door Lock cluster."""
15+
assert last_action_source_converter(0x00020001) == "zigbee", (
16+
"Expected 'zigbee' for value 0x00010001"
17+
)
18+
assert last_action_source_converter(0x03010001) == "fingerprint", (
19+
"Expected 'fingerprint' for value 0x03010001"
20+
)
21+
assert last_action_source_converter(0x99010001) is None, (
22+
"Expected None for value 0x99010001"
23+
)
24+
25+
26+
def test_last_action_converter() -> None:
27+
"""Test the last action converter for Nimly Door Lock cluster."""
28+
assert last_action_converter(0x0A010001) == "lock", (
29+
"Expected 'lock' for value 0x0a010001"
30+
)
31+
assert last_action_converter(0x00020001) == "unlock", (
32+
"Expected 'unlock' for value 0x00020001"
33+
)
34+
assert last_action_converter(0x01030001) is None, (
35+
"Expected None for value 0x01030001"
36+
)
37+
38+
39+
def test_last_action_user_converter() -> None:
40+
"""Test the last action user converter for Nimly Door Lock cluster."""
41+
assert last_action_user_converter(0x01020001) == 1, (
42+
"Expected user ID 1 for value 0x01020001"
43+
)
44+
assert last_action_user_converter(0x02020010) == 16, (
45+
"Expected user ID 16 for value 0x02020010"
46+
)

zhaquirks/nimly/lock.py

Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
11
"""Device handler for Nimly Smart Locks."""
22

3+
from typing import Final
4+
5+
from zigpy.quirks import CustomCluster
36
from zigpy.quirks.v2 import QuirkBuilder
7+
from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass
8+
import zigpy.types as t
9+
from zigpy.zcl.clusters.closures import DoorLock
10+
from zigpy.zcl.foundation import ZCLAttributeDef
411
from zigpy.zdo.types import NodeDescriptor
512

613
from zhaquirks import DoublingPowerConfigurationCluster
@@ -25,11 +32,117 @@
2532
)
2633

2734

35+
class NimlyDoorLock(CustomCluster, DoorLock):
36+
"""Nimly Door Lock cluster."""
37+
38+
class AttributeDefs(DoorLock.AttributeDefs):
39+
"""Nimly Door Lock attribute definitions."""
40+
41+
nimly_last_lock_unlock_source: Final = ZCLAttributeDef(
42+
id=0x100,
43+
type=t.bitmap32,
44+
access="r",
45+
is_manufacturer_specific=True,
46+
)
47+
nimly_last_pin_code: Final = ZCLAttributeDef(
48+
id=0x101,
49+
type=t.LVBytes,
50+
access="r",
51+
is_manufacturer_specific=True,
52+
)
53+
54+
55+
def last_action_source_converter(value: int) -> str:
56+
"""Extract last action source value."""
57+
value_hex = hex(value)[2:].zfill(8)
58+
sources = {
59+
"00": "zigbee",
60+
"02": "keypad",
61+
"03": "fingerprint",
62+
"04": "rfid",
63+
"0a": "self",
64+
}
65+
return sources.get(value_hex[0:2])
66+
67+
68+
def last_action_converter(value: int) -> str:
69+
"""Extract last action value."""
70+
value_hex = hex(value)[2:].zfill(8)
71+
actions = {
72+
"01": "lock",
73+
"02": "unlock",
74+
}
75+
return actions.get(value_hex[2:4])
76+
77+
78+
def last_action_user_converter(value: int) -> int:
79+
"""Extract last action user value."""
80+
value_hex = hex(value)[2:].zfill(8)
81+
user_id = int(value_hex[4:8], 16)
82+
return user_id
83+
84+
2885
(
2986
QuirkBuilder(NIMLY, "EasyFingerTouch")
3087
.applies_to(NIMLY, "EasyCodeTouch")
3188
.applies_to(NIMLY, "easyCodeTouch_v1")
3289
.node_descriptor(NIMLY_LOCK_NODE_DESCRIPTOR)
90+
.sensor(
91+
endpoint_id=11,
92+
cluster_id=NimlyDoorLock.cluster_id,
93+
attribute_name=NimlyDoorLock.AttributeDefs.nimly_last_lock_unlock_source.name,
94+
unique_id_suffix="last_action_source",
95+
attribute_converter=last_action_source_converter,
96+
device_class=SensorDeviceClass.ENUM,
97+
translation_key="last_action_source",
98+
fallback_name="Last action source",
99+
)
100+
.sensor(
101+
endpoint_id=11,
102+
cluster_id=NimlyDoorLock.cluster_id,
103+
attribute_name=NimlyDoorLock.AttributeDefs.nimly_last_lock_unlock_source.name,
104+
unique_id_suffix="last_action",
105+
attribute_converter=last_action_converter,
106+
device_class=SensorDeviceClass.ENUM,
107+
translation_key="last_action",
108+
fallback_name="Last action",
109+
)
110+
.sensor(
111+
endpoint_id=11,
112+
cluster_id=NimlyDoorLock.cluster_id,
113+
attribute_name=NimlyDoorLock.AttributeDefs.nimly_last_lock_unlock_source.name,
114+
unique_id_suffix="last_action_user",
115+
attribute_converter=last_action_user_converter,
116+
translation_key="last_action_user",
117+
fallback_name="Last action user",
118+
)
119+
.sensor(
120+
endpoint_id=11,
121+
cluster_id=NimlyDoorLock.cluster_id,
122+
attribute_name=NimlyDoorLock.AttributeDefs.nimly_last_pin_code.name,
123+
unique_id_suffix="last_pin_code",
124+
attribute_converter=lambda value: value.hex(),
125+
initially_disabled=True,
126+
translation_key="last_pin_code",
127+
fallback_name="Last pin code",
128+
)
129+
.switch(
130+
endpoint_id=11,
131+
cluster_id=NimlyDoorLock.cluster_id,
132+
attribute_name=NimlyDoorLock.AttributeDefs.auto_relock_time.name,
133+
translation_key="auto_relock",
134+
fallback_name="Auto relock",
135+
)
136+
.number(
137+
endpoint_id=11,
138+
cluster_id=NimlyDoorLock.cluster_id,
139+
attribute_name=NimlyDoorLock.AttributeDefs.sound_volume.name,
140+
min_value=0,
141+
max_value=2,
142+
step=1,
143+
translation_key="sound_volume",
144+
fallback_name="Sound volume",
145+
)
33146
.add_to_registry()
34147
)
35148

@@ -41,5 +154,62 @@
41154
.applies_to(NIMLY, "NimlyIn")
42155
.node_descriptor(NIMLY_LOCK_NODE_DESCRIPTOR)
43156
.replaces(DoublingPowerConfigurationCluster, endpoint_id=11)
157+
.replaces(NimlyDoorLock, endpoint_id=11)
158+
.sensor(
159+
endpoint_id=11,
160+
cluster_id=NimlyDoorLock.cluster_id,
161+
attribute_name=NimlyDoorLock.AttributeDefs.nimly_last_lock_unlock_source.name,
162+
unique_id_suffix="last_action_source",
163+
attribute_converter=last_action_source_converter,
164+
device_class=SensorDeviceClass.ENUM,
165+
translation_key="last_action_source",
166+
fallback_name="Last action source",
167+
)
168+
.sensor(
169+
endpoint_id=11,
170+
cluster_id=NimlyDoorLock.cluster_id,
171+
attribute_name=NimlyDoorLock.AttributeDefs.nimly_last_lock_unlock_source.name,
172+
unique_id_suffix="last_action",
173+
attribute_converter=last_action_converter,
174+
device_class=SensorDeviceClass.ENUM,
175+
translation_key="last_action",
176+
fallback_name="Last action",
177+
)
178+
.sensor(
179+
endpoint_id=11,
180+
cluster_id=NimlyDoorLock.cluster_id,
181+
attribute_name=NimlyDoorLock.AttributeDefs.nimly_last_lock_unlock_source.name,
182+
unique_id_suffix="last_action_user",
183+
attribute_converter=last_action_user_converter,
184+
translation_key="last_action_user",
185+
fallback_name="Last action user",
186+
)
187+
.sensor(
188+
endpoint_id=11,
189+
cluster_id=NimlyDoorLock.cluster_id,
190+
attribute_name=NimlyDoorLock.AttributeDefs.nimly_last_pin_code.name,
191+
unique_id_suffix="last_pin_code",
192+
attribute_converter=lambda value: value.hex(),
193+
initially_disabled=True,
194+
translation_key="last_pin_code",
195+
fallback_name="Last pin code",
196+
)
197+
.switch(
198+
endpoint_id=11,
199+
cluster_id=NimlyDoorLock.cluster_id,
200+
attribute_name=NimlyDoorLock.AttributeDefs.auto_relock_time.name,
201+
translation_key="auto_relock",
202+
fallback_name="Auto relock",
203+
)
204+
.number(
205+
endpoint_id=11,
206+
cluster_id=NimlyDoorLock.cluster_id,
207+
attribute_name=NimlyDoorLock.AttributeDefs.sound_volume.name,
208+
min_value=0,
209+
max_value=2,
210+
step=1,
211+
translation_key="sound_volume",
212+
fallback_name="Sound volume",
213+
)
44214
.add_to_registry()
45215
)

0 commit comments

Comments
 (0)