Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Linxura smart controller button quirk #3392

Open
wants to merge 59 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
5174804
first time to commit linxura integration
simon3panda Sep 23, 2024
4d2c6cd
Add new quirks that support Linxura smart controller.
simon3panda Sep 30, 2024
6057d72
Apply pre-commit auto fixes
pre-commit-ci[bot] Sep 30, 2024
b7b1dc1
Update test_linxura.py
simon3panda Sep 30, 2024
f318362
Merge branch 'dev' of https://github.com/simon3panda/zha-device-handl…
simon3panda Sep 30, 2024
7d477de
Apply pre-commit auto fixes
pre-commit-ci[bot] Sep 30, 2024
9e19d0f
fix pre-commit issue
simon3panda Sep 30, 2024
a6d7be0
Merge branch 'dev' of https://github.com/simon3panda/zha-device-handl…
simon3panda Sep 30, 2024
2409dcc
fix pre-commit issue
simon3panda Sep 30, 2024
3ffa306
Apply pre-commit auto fixes
pre-commit-ci[bot] Sep 30, 2024
aa7b1d5
fix pre-commit ruff-format issue
simon3panda Sep 30, 2024
745d997
Merge branch 'dev' of https://github.com/simon3panda/zha-device-handl…
simon3panda Sep 30, 2024
d5f1346
codecov test issue
simon3panda Sep 30, 2024
d687b3f
Apply pre-commit auto fixes
pre-commit-ci[bot] Sep 30, 2024
60a0ef6
fix pre-commit issue
simon3panda Sep 30, 2024
58a4850
Merge branch 'dev' of https://github.com/simon3panda/zha-device-handl…
simon3panda Sep 30, 2024
5ab15e4
update for solving codecov cover issue
simon3panda Oct 6, 2024
ef07035
solve check issue
simon3panda Oct 6, 2024
f494957
Apply pre-commit auto fixes
pre-commit-ci[bot] Oct 6, 2024
8f19301
Update __init__.py
simon3panda Oct 6, 2024
632ffb3
Merge branch 'dev' of https://github.com/simon3panda/zha-device-handl…
simon3panda Oct 6, 2024
33f8183
Update __init__.py
simon3panda Oct 6, 2024
8b7893d
Apply pre-commit auto fixes
pre-commit-ci[bot] Oct 6, 2024
34b03b5
Update __init__.py
simon3panda Oct 6, 2024
0fbfc86
Apply pre-commit auto fixes
pre-commit-ci[bot] Oct 6, 2024
64712a5
Update __init__.py
simon3panda Oct 6, 2024
b7ce98b
Apply pre-commit auto fixes
pre-commit-ci[bot] Oct 6, 2024
e5c6bf3
update __init__.py
simon3panda Oct 6, 2024
08de105
Update __init__.py
simon3panda Oct 6, 2024
b29670e
Apply pre-commit auto fixes
pre-commit-ci[bot] Oct 6, 2024
85ad4c0
Update __init__.py
simon3panda Oct 6, 2024
a83b4c5
Update __init__.py
simon3panda Oct 6, 2024
6761de0
Apply pre-commit auto fixes
pre-commit-ci[bot] Oct 6, 2024
494cf83
Update __init__.py
simon3panda Oct 6, 2024
5dfd683
update for codecov cover
simon3panda Oct 6, 2024
0cc62dd
Apply pre-commit auto fixes
pre-commit-ci[bot] Oct 6, 2024
e178a8c
Update test_linxura.py
simon3panda Oct 6, 2024
9a83e44
Merge branch 'dev' of https://github.com/simon3panda/zha-device-handl…
simon3panda Oct 6, 2024
8e0bd34
Apply pre-commit auto fixes
pre-commit-ci[bot] Oct 6, 2024
9488899
Update __init__.py
simon3panda Oct 6, 2024
b8407dd
Merge branch 'dev' of https://github.com/simon3panda/zha-device-handl…
simon3panda Oct 6, 2024
680a0ad
Update test_linxura.py
simon3panda Oct 6, 2024
4fedb7c
solve codecov issue
simon3panda Oct 8, 2024
44066f2
Update test_linxura.py
simon3panda Oct 8, 2024
28dc389
Apply pre-commit auto fixes
pre-commit-ci[bot] Oct 8, 2024
abed165
Update test_linxura.py
simon3panda Oct 8, 2024
4613b9f
Merge branch 'dev' of https://github.com/simon3panda/zha-device-handl…
simon3panda Oct 8, 2024
4680fd9
Update __init__.py
simon3panda Oct 8, 2024
f7ca32f
Update __init__.py
simon3panda Oct 8, 2024
60c7bd0
Update __init__.py
simon3panda Oct 8, 2024
427a261
Update __init__.py
simon3panda Oct 8, 2024
df1b192
Update __init__.py
simon3panda Oct 8, 2024
72ca717
Update test_linxura.py
simon3panda Oct 8, 2024
0a51caf
Update test_linxura.py
simon3panda Oct 8, 2024
f490a56
Update __init__.py
simon3panda Oct 8, 2024
767cf75
Update __init__.py
simon3panda Oct 8, 2024
0db43ce
Remove the commented out code
simon3panda Oct 9, 2024
d6b4ecb
Update test_linxura.py
simon3panda Oct 9, 2024
e12e9df
Apply pre-commit auto fixes
pre-commit-ci[bot] Oct 9, 2024
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
158 changes: 158 additions & 0 deletions tests/test_linxura.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,158 @@
"""Tests for Linxura quirks."""

import pytest
from zigpy.zcl.clusters.security import IasZone
from zigpy.zcl.foundation import ZCLHeader

from tests.common import ClusterListener
import zhaquirks
import zhaquirks.linxura

zhaquirks.setup()


@pytest.mark.parametrize("quirk", (zhaquirks.linxura.button.LinxuraButton,))
async def test_Linxura_button(zigpy_device_from_quirk, quirk):
"""Test Linxura button remotes."""

device = zigpy_device_from_quirk(quirk)

# endpoint 1
cluster = device.endpoints[1].ias_zone
ias_zone_listener = ClusterListener(cluster)
ias_zone_status_attr_id = IasZone.AttributeDefs.zone_status.id

# single press
# ZHA calls update_attribute on the IasZone cluster when it receives a status_change_notification
cluster.update_attribute(ias_zone_status_attr_id, 1)

# button 1 single press
assert len(ias_zone_listener.attribute_updates) == 1
assert ias_zone_listener.attribute_updates[0][0] == ias_zone_status_attr_id
assert ias_zone_listener.attribute_updates[0][1] == 1

# button1 double press
# ZHA calls update_attribute on the IasZone cluster when it receives a status_change_notification
cluster.update_attribute(ias_zone_status_attr_id, 2)

# button 1 double press
assert len(ias_zone_listener.attribute_updates) == 2
assert ias_zone_listener.attribute_updates[1][0] == ias_zone_status_attr_id
assert ias_zone_listener.attribute_updates[1][1] == 2

# button1 long press
# ZHA calls update_attribute on the IasZone cluster when it receives a status_change_notification
cluster.update_attribute(ias_zone_status_attr_id, 3)

# button 1 long press
assert len(ias_zone_listener.attribute_updates) == 3
assert ias_zone_listener.attribute_updates[2][0] == ias_zone_status_attr_id
assert ias_zone_listener.attribute_updates[2][1] == 3

# endpoint 2
cluster2 = device.endpoints[2].ias_zone
ias_zone_listener2 = ClusterListener(cluster2)
ias_zone_status_attr_id = IasZone.AttributeDefs.zone_status.id

# single press
# ZHA calls update_attribute on the IasZone cluster when it receives a status_change_notification
cluster2.update_attribute(ias_zone_status_attr_id, 1)

# button 2 single press
assert len(ias_zone_listener2.attribute_updates) == 1
assert ias_zone_listener2.attribute_updates[0][0] == ias_zone_status_attr_id
assert ias_zone_listener2.attribute_updates[0][1] == 1

# button 2 double press
cluster2.update_attribute(ias_zone_status_attr_id, 2)
assert len(ias_zone_listener2.attribute_updates) == 2
assert ias_zone_listener2.attribute_updates[1][0] == ias_zone_status_attr_id
assert ias_zone_listener2.attribute_updates[1][1] == 2

# button 2 long press
cluster2.update_attribute(ias_zone_status_attr_id, 3)
assert len(ias_zone_listener2.attribute_updates) == 3
assert ias_zone_listener2.attribute_updates[2][0] == ias_zone_status_attr_id
assert ias_zone_listener2.attribute_updates[2][1] == 3

# endpoint 3
cluster3 = device.endpoints[3].ias_zone
ias_zone_listener3 = ClusterListener(cluster3)
ias_zone_status_attr_id = IasZone.AttributeDefs.zone_status.id

# single press
# ZHA calls update_attribute on the IasZone cluster when it receives a status_change_notification
cluster3.update_attribute(ias_zone_status_attr_id, 1)

# button 3 single press
assert len(ias_zone_listener3.attribute_updates) == 1
assert ias_zone_listener3.attribute_updates[0][0] == ias_zone_status_attr_id
assert ias_zone_listener3.attribute_updates[0][1] == 1

# button 3 double press
cluster3.update_attribute(ias_zone_status_attr_id, 2)
assert len(ias_zone_listener3.attribute_updates) == 2
assert ias_zone_listener3.attribute_updates[1][0] == ias_zone_status_attr_id
assert ias_zone_listener3.attribute_updates[1][1] == 2

# button 3 long press
cluster3.update_attribute(ias_zone_status_attr_id, 3)
assert len(ias_zone_listener3.attribute_updates) == 3
assert ias_zone_listener3.attribute_updates[2][0] == ias_zone_status_attr_id
assert ias_zone_listener3.attribute_updates[2][1] == 3

# endpoint 4
cluster4 = device.endpoints[4].ias_zone
ias_zone_listener4 = ClusterListener(cluster4)
ias_zone_status_attr_id = IasZone.AttributeDefs.zone_status.id

# single press
# ZHA calls update_attribute on the IasZone cluster when it receives a status_change_notification
cluster4.update_attribute(ias_zone_status_attr_id, 1)

# button 4 single press
assert len(ias_zone_listener4.attribute_updates) == 1
assert ias_zone_listener4.attribute_updates[0][0] == ias_zone_status_attr_id
assert ias_zone_listener4.attribute_updates[0][1] == 1

# button 4 double press
cluster4.update_attribute(ias_zone_status_attr_id, 2)
assert len(ias_zone_listener4.attribute_updates) == 2
assert ias_zone_listener4.attribute_updates[1][0] == ias_zone_status_attr_id
assert ias_zone_listener4.attribute_updates[1][1] == 2

# button 4 long press
cluster4.update_attribute(ias_zone_status_attr_id, 3)
assert len(ias_zone_listener4.attribute_updates) == 3
assert ias_zone_listener4.attribute_updates[2][0] == ias_zone_status_attr_id
assert ias_zone_listener4.attribute_updates[2][1] == 3


@pytest.mark.parametrize("quirk", (zhaquirks.linxura.button.LinxuraButton,))
async def test_handle_cluster_request(zigpy_device_from_quirk, quirk):
"""Test handle cluster request."""

device = zigpy_device_from_quirk(quirk)
cluster = device.endpoints[1].ias_zone

hdr = ZCLHeader.general(tsn=0, command_id=0)
args = [1] # Simulate single press state
cluster.handle_cluster_general_request(hdr, args)
# Validate event listener triggers the right event


@pytest.mark.parametrize("quirk", (zhaquirks.linxura.button.LinxuraButton,))
async def test_edge_case_request(zigpy_device_from_quirk, quirk):
"""Test edge case."""

device = zigpy_device_from_quirk(quirk)

# endpoint 1
cluster = device.endpoints[1].ias_zone
ias_zone_listener = ClusterListener(cluster)
ias_zone_status_attr_id = IasZone.AttributeDefs.zone_status.id

cluster.update_attribute(ias_zone_status_attr_id, 4)
assert (
len(ias_zone_listener.attribute_updates) == 1
) # No update should occur for state >= 4
53 changes: 53 additions & 0 deletions zhaquirks/linxura/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
"""Linxura button device."""

from typing import Any, Optional, Union

from zigpy.quirks import CustomCluster
import zigpy.types as t
from zigpy.types import Addressing
from zigpy.zcl import foundation
import zigpy.zcl.clusters.security

from zhaquirks.const import ARGS, COMMAND_ID, PRESS_TYPE, ZHA_SEND_EVENT

DOUBLE = 2
HOLD = 3
LINXURA = "Linxura"
SINGLE = 1
ACT_ERROR = 4
STATUS_PARAM = 0
CLICK_TYPES = {SINGLE: "single", DOUBLE: "double", HOLD: "hold"}


class LinxuraIASCluster(CustomCluster, zigpy.zcl.clusters.security.IasZone):
"""Occupancy cluster."""

server_commands = {
0x00: foundation.ZCLCommandDef(
"button_event",
{"press_type": t.uint8_t},
),
}

def handle_cluster_general_request(
self,
hdr: zigpy.zcl.foundation.ZCLHeader,
args: list[Any],
*,
dst_addressing: Optional[
Union[Addressing.Group, Addressing.IEEE, Addressing.NWK]
] = None,
):
"""Handle a cluster command received on this cluster."""
if hdr.command_id == 0:
state = args[STATUS_PARAM]
if state >= ACT_ERROR:
return

Check warning on line 45 in zhaquirks/linxura/__init__.py

View check run for this annotation

Codecov / codecov/patch

zhaquirks/linxura/__init__.py#L45

Added line #L45 was not covered by tests
else:
event_args = {
PRESS_TYPE: CLICK_TYPES[state],
COMMAND_ID: hdr.command_id,
ARGS: args,
}
action = f"button_{CLICK_TYPES[state]}"
self.listener_event(ZHA_SEND_EVENT, action, event_args)
180 changes: 180 additions & 0 deletions zhaquirks/linxura/button.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,180 @@
"""Linxura button device."""

from zigpy.profiles import zha
from zigpy.quirks import CustomDevice
from zigpy.zcl.clusters.general import Basic
from zigpy.zcl.clusters.security import IasZone

from zhaquirks.const import (
CLUSTER_ID,
COMMAND,
COMMAND_BUTTON_DOUBLE,
COMMAND_BUTTON_HOLD,
COMMAND_BUTTON_SINGLE,
DEVICE_TYPE,
DOUBLE_PRESS,
ENDPOINT_ID,
ENDPOINTS,
INPUT_CLUSTERS,
LONG_PRESS,
MODELS_INFO,
OUTPUT_CLUSTERS,
PROFILE_ID,
SHORT_PRESS,
)
from zhaquirks.linxura import LINXURA, LinxuraIASCluster

BTN_1 = "Button 1"
BTN_2 = "Button 2"
BTN_3 = "Button 3"
BTN_4 = "Button 4"


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: [],
},
2: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.IAS_ZONE,
INPUT_CLUSTERS: [
Basic.cluster_id,
IasZone.cluster_id,
],
OUTPUT_CLUSTERS: [],
},
3: {
PROFILE_ID: zha.PROFILE_ID,
DEVICE_TYPE: zha.DeviceType.IAS_ZONE,
INPUT_CLUSTERS: [
Basic.cluster_id,
IasZone.cluster_id,
],
OUTPUT_CLUSTERS: [],
},
4: {
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: [],
},
2: {
PROFILE_ID: zha.PROFILE_ID,
INPUT_CLUSTERS: [
Basic.cluster_id,
LinxuraIASCluster,
],
OUTPUT_CLUSTERS: [],
},
3: {
PROFILE_ID: zha.PROFILE_ID,
INPUT_CLUSTERS: [
Basic.cluster_id,
LinxuraIASCluster,
],
OUTPUT_CLUSTERS: [],
},
4: {
PROFILE_ID: zha.PROFILE_ID,
INPUT_CLUSTERS: [
Basic.cluster_id,
LinxuraIASCluster,
],
OUTPUT_CLUSTERS: [],
},
}
}

device_automation_triggers = {
(DOUBLE_PRESS, BTN_1): {
COMMAND: COMMAND_BUTTON_DOUBLE,
CLUSTER_ID: IasZone.cluster_id,
ENDPOINT_ID: 1,
},
(SHORT_PRESS, BTN_1): {
COMMAND: COMMAND_BUTTON_SINGLE,
CLUSTER_ID: IasZone.cluster_id,
ENDPOINT_ID: 1,
},
(LONG_PRESS, BTN_1): {
COMMAND: COMMAND_BUTTON_HOLD,
CLUSTER_ID: IasZone.cluster_id,
ENDPOINT_ID: 1,
},
(DOUBLE_PRESS, BTN_2): {
COMMAND: COMMAND_BUTTON_DOUBLE,
CLUSTER_ID: IasZone.cluster_id,
ENDPOINT_ID: 2,
},
(SHORT_PRESS, BTN_2): {
COMMAND: COMMAND_BUTTON_SINGLE,
CLUSTER_ID: IasZone.cluster_id,
ENDPOINT_ID: 2,
},
(LONG_PRESS, BTN_2): {
COMMAND: COMMAND_BUTTON_HOLD,
CLUSTER_ID: IasZone.cluster_id,
ENDPOINT_ID: 2,
},
(DOUBLE_PRESS, BTN_3): {
COMMAND: COMMAND_BUTTON_DOUBLE,
CLUSTER_ID: IasZone.cluster_id,
ENDPOINT_ID: 3,
},
(SHORT_PRESS, BTN_3): {
COMMAND: COMMAND_BUTTON_SINGLE,
CLUSTER_ID: IasZone.cluster_id,
ENDPOINT_ID: 3,
},
(LONG_PRESS, BTN_3): {
COMMAND: COMMAND_BUTTON_HOLD,
CLUSTER_ID: IasZone.cluster_id,
ENDPOINT_ID: 3,
},
(DOUBLE_PRESS, BTN_4): {
COMMAND: COMMAND_BUTTON_DOUBLE,
CLUSTER_ID: IasZone.cluster_id,
ENDPOINT_ID: 4,
},
(SHORT_PRESS, BTN_4): {
COMMAND: COMMAND_BUTTON_SINGLE,
CLUSTER_ID: IasZone.cluster_id,
ENDPOINT_ID: 4,
},
(LONG_PRESS, BTN_4): {
COMMAND: COMMAND_BUTTON_HOLD,
CLUSTER_ID: IasZone.cluster_id,
ENDPOINT_ID: 4,
},
}
Loading