Skip to content

Commit

Permalink
Add Linxura smart controller button quirk (#3392)
Browse files Browse the repository at this point in the history
Co-authored-by: TheJulianJES <[email protected]>
  • Loading branch information
simon3panda and TheJulianJES authored Nov 26, 2024
1 parent bb63900 commit 213ce10
Show file tree
Hide file tree
Showing 3 changed files with 228 additions and 0 deletions.
116 changes: 116 additions & 0 deletions tests/test_linxura.py
Original file line number Diff line number Diff line change
@@ -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,
},
)
3 changes: 3 additions & 0 deletions zhaquirks/linxura/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
"""Linxura devices."""

LINXURA = "Linxura"
109 changes: 109 additions & 0 deletions zhaquirks/linxura/button.py
Original file line number Diff line number Diff line change
@@ -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 = {
# <SimpleDescriptor endpoint=1 profile=260 device_type=1026
# device_version=0
# input_clusters=[0, 3, 1280]=>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)
}

0 comments on commit 213ce10

Please sign in to comment.