diff --git a/tests/test_linxura.py b/tests/test_linxura.py new file mode 100644 index 0000000000..0ac867dc54 --- /dev/null +++ b/tests/test_linxura.py @@ -0,0 +1,116 @@ +"""Tests for Linxura quirks.""" + +from unittest import mock + +import pytest +from zigpy.zcl.clusters.security import IasZone + +import zhaquirks +import zhaquirks.linxura + +zhaquirks.setup() + + +async def test_button_ias(zigpy_device_from_quirk): + """Test Linxura button remotes.""" + + device = zigpy_device_from_quirk(zhaquirks.linxura.button.LinxuraButton) + ias_zone_status_attr_id = IasZone.AttributeDefs.zone_status.id + cluster = device.endpoints[1].ias_zone + listener = mock.MagicMock() + cluster.add_listener(listener) + + for i in range(0, 24): + # button press + cluster.update_attribute(ias_zone_status_attr_id, i) + + # update_attribute on the IasZone cluster is always called + assert listener.attribute_updated.call_args[0][0] == ias_zone_status_attr_id + assert listener.attribute_updated.call_args[0][1] == i + + # we get 20 events, 4 are discarded as invalid (0, 6, 12, 18) + assert listener.attribute_updated.call_count == 24 + assert listener.zha_send_event.call_count == 20 + + +@pytest.mark.parametrize( + "message, button, press_type", + [ + ( + b"\x18\n\n\x02\x00\x19\x01\x00\xfe\xff0\x01", + "button_1", + "remote_button_short_press", + ), + ( + b"\x18\n\n\x02\x00\x19\x03\x00\xfe\xff0\x01", + "button_1", + "remote_button_double_press", + ), + ( + b"\x18\n\n\x02\x00\x19\x05\x00\xfe\xff0\x01", + "button_1", + "remote_button_long_press", + ), + ( + b"\x18\n\n\x02\x00\x19\x07\x00\xfe\xff0\x01", + "button_2", + "remote_button_short_press", + ), + ( + b"\x18\n\n\x02\x00\x19\x09\x00\xfe\xff0\x01", + "button_2", + "remote_button_double_press", + ), + ( + b"\x18\n\n\x02\x00\x19\x0b\x00\xfe\xff0\x01", + "button_2", + "remote_button_long_press", + ), + ( + b"\x18\n\n\x02\x00\x19\x0d\x00\xfe\xff0\x01", + "button_3", + "remote_button_short_press", + ), + ( + b"\x18\n\n\x02\x00\x19\x0f\x00\xfe\xff0\x01", + "button_3", + "remote_button_double_press", + ), + ( + b"\x18\n\n\x02\x00\x19\x11\x00\xfe\xff0\x01", + "button_3", + "remote_button_long_press", + ), + ( + b"\x18\n\n\x02\x00\x19\x13\x00\xfe\xff0\x01", + "button_4", + "remote_button_short_press", + ), + ( + b"\x18\n\n\x02\x00\x19\x15\x00\xfe\xff0\x01", + "button_4", + "remote_button_double_press", + ), + ( + b"\x18\n\n\x02\x00\x19\x17\x00\xfe\xff0\x01", + "button_4", + "remote_button_long_press", + ), + ], +) +async def test_button_triggers(zigpy_device_from_quirk, message, button, press_type): + """Test ZHA_SEND_EVENT case.""" + device = zigpy_device_from_quirk(zhaquirks.linxura.button.LinxuraButton) + cluster = device.endpoints[1].ias_zone + listener = mock.MagicMock() + cluster.add_listener(listener) + + device.handle_message(260, cluster.cluster_id, 1, 1, message) + assert listener.zha_send_event.call_count == 1 + assert listener.zha_send_event.call_args == mock.call( + f"{button}_{press_type}", + { + "button": button, + "press_type": press_type, + }, + ) diff --git a/zhaquirks/linxura/__init__.py b/zhaquirks/linxura/__init__.py new file mode 100644 index 0000000000..dade7c643b --- /dev/null +++ b/zhaquirks/linxura/__init__.py @@ -0,0 +1,3 @@ +"""Linxura devices.""" + +LINXURA = "Linxura" diff --git a/zhaquirks/linxura/button.py b/zhaquirks/linxura/button.py new file mode 100644 index 0000000000..5d1a2f6e61 --- /dev/null +++ b/zhaquirks/linxura/button.py @@ -0,0 +1,109 @@ +"""Linxura button device.""" + +from zigpy.profiles import zha +from zigpy.quirks import CustomCluster, CustomDevice +from zigpy.zcl.clusters.general import Basic +from zigpy.zcl.clusters.security import IasZone + +from zhaquirks.const import ( + BUTTON, + BUTTON_1, + BUTTON_2, + BUTTON_3, + BUTTON_4, + CLUSTER_ID, + COMMAND, + DEVICE_TYPE, + DOUBLE_PRESS, + ENDPOINTS, + INPUT_CLUSTERS, + LONG_PRESS, + MODELS_INFO, + OUTPUT_CLUSTERS, + PRESS_TYPE, + PROFILE_ID, + SHORT_PRESS, + ZHA_SEND_EVENT, +) +from zhaquirks.linxura import LINXURA + +PRESS_TYPES = { + 1: SHORT_PRESS, + 2: DOUBLE_PRESS, + 3: LONG_PRESS, +} + + +class LinxuraIASCluster(CustomCluster, IasZone): + """IAS cluster used for Linxura button.""" + + def _update_attribute(self, attrid, value): + super()._update_attribute(attrid, value) + if attrid == self.AttributeDefs.zone_status.id and 0 < value < 24: + if 0 < value < 6: + button = BUTTON_1 + press_type = PRESS_TYPES[value // 2 + 1] + elif 6 < value < 12: + button = BUTTON_2 + press_type = PRESS_TYPES[value // 2 - 3 + 1] + elif 12 < value < 18: + button = BUTTON_3 + press_type = PRESS_TYPES[value // 2 - 6 + 1] + elif 18 < value < 24: + button = BUTTON_4 + press_type = PRESS_TYPES[value // 2 - 9 + 1] + else: + # discard invalid values: 0, 6, 12, 18 + return + + action = f"{button}_{press_type}" + event_args = { + BUTTON: button, + PRESS_TYPE: press_type, + } + self.listener_event(ZHA_SEND_EVENT, action, event_args) + + +class LinxuraButton(CustomDevice): + """Linxura button device.""" + + signature = { + # input_clusters=[0, 1280] + # output_clusters=[3]>=>output_clusters=[] + MODELS_INFO: [(LINXURA, "Smart Controller")], + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + DEVICE_TYPE: zha.DeviceType.IAS_ZONE, + INPUT_CLUSTERS: [ + Basic.cluster_id, + IasZone.cluster_id, + ], + OUTPUT_CLUSTERS: [], + }, + }, + } + + replacement = { + ENDPOINTS: { + 1: { + PROFILE_ID: zha.PROFILE_ID, + INPUT_CLUSTERS: [ + Basic.cluster_id, + LinxuraIASCluster, + ], + OUTPUT_CLUSTERS: [], + }, + } + } + + device_automation_triggers = { + (press_type, button): { + COMMAND: f"{button}_{press_type}", + CLUSTER_ID: IasZone.cluster_id, + } + for press_type in (SHORT_PRESS, DOUBLE_PRESS, LONG_PRESS) + for button in (BUTTON_1, BUTTON_2, BUTTON_3, BUTTON_4) + }