Skip to content

Send all button press events for Sinope SW2500ZB/DM2500ZB #3438

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

Merged
merged 3 commits into from
Oct 26, 2024
Merged
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
62 changes: 49 additions & 13 deletions tests/test_sinope.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,14 @@

from tests.common import ClusterListener
import zhaquirks
from zhaquirks.const import COMMAND_BUTTON_DOUBLE, COMMAND_BUTTON_HOLD
from zhaquirks.const import (
COMMAND_M_INITIAL_PRESS,
COMMAND_M_LONG_RELEASE,
COMMAND_M_MULTI_PRESS_COMPLETE,
COMMAND_M_SHORT_RELEASE,
TURN_OFF,
TURN_ON,
)
from zhaquirks.sinope import SINOPE_MANUFACTURER_CLUSTER_ID
from zhaquirks.sinope.light import (
SinopeTechnologieslight,
Expand Down Expand Up @@ -92,20 +99,22 @@ def _get_packet_data(

@pytest.mark.parametrize("quirk", (SinopeTechnologieslight,))
@pytest.mark.parametrize(
"press_type,exp_event",
"press_type,button,exp_event",
(
(ButtonAction.Single_off, None),
(ButtonAction.Single_on, None),
(ButtonAction.Double_on, COMMAND_BUTTON_DOUBLE),
(ButtonAction.Double_off, COMMAND_BUTTON_DOUBLE),
(ButtonAction.Long_on, COMMAND_BUTTON_HOLD),
(ButtonAction.Long_off, COMMAND_BUTTON_HOLD),
(ButtonAction.Pressed_off, TURN_OFF, COMMAND_M_INITIAL_PRESS),
(ButtonAction.Pressed_on, TURN_ON, COMMAND_M_INITIAL_PRESS),
(ButtonAction.Released_off, TURN_OFF, COMMAND_M_SHORT_RELEASE),
(ButtonAction.Released_on, TURN_ON, COMMAND_M_SHORT_RELEASE),
(ButtonAction.Double_on, TURN_ON, COMMAND_M_MULTI_PRESS_COMPLETE),
(ButtonAction.Double_off, TURN_OFF, COMMAND_M_MULTI_PRESS_COMPLETE),
(ButtonAction.Long_on, TURN_ON, COMMAND_M_LONG_RELEASE),
(ButtonAction.Long_off, TURN_OFF, COMMAND_M_LONG_RELEASE),
# Should gracefully handle broken actions.
(t.uint8_t(0x00), None),
(t.uint8_t(0x00), None, None),
),
)
async def test_sinope_light_switch(
zigpy_device_from_quirk, quirk, press_type, exp_event
zigpy_device_from_quirk, quirk, press_type, button, exp_event
):
"""Test that button presses are sent as events."""
device: Device = zigpy_device_from_quirk(quirk)
Expand All @@ -126,7 +135,16 @@ class Listener:
),
)
data = _get_packet_data(foundation.GeneralCommand.Report_Attributes, attr)
device.handle_message(260, cluster_id, endpoint_id, endpoint_id, data)

device.packet_received(
t.ZigbeePacket(
profile_id=260,
cluster_id=cluster_id,
src_ep=endpoint_id,
dst_ep=endpoint_id,
data=t.SerializableBytes(data),
)
)

if exp_event is None:
assert cluster_listener.zha_send_event.call_count == 0
Expand All @@ -137,6 +155,8 @@ class Listener:
{
"attribute_id": 84,
"attribute_name": "action_report",
"button": button,
"description": press_type.name,
"value": press_type.value,
},
)
Expand All @@ -162,7 +182,15 @@ class Listener:

# read attributes general command
data = _get_packet_data(foundation.GeneralCommand.Read_Attributes)
device.handle_message(260, cluster_id, endpoint_id, endpoint_id, data)
device.packet_received(
t.ZigbeePacket(
profile_id=260,
cluster_id=cluster_id,
src_ep=endpoint_id,
dst_ep=endpoint_id,
data=t.SerializableBytes(data),
)
)
# no ZHA events emitted because we only handle Report_Attributes
assert cluster_listener.zha_send_event.call_count == 0

Expand All @@ -174,7 +202,15 @@ class Listener:
), # 0x29 = t.int16s
)
data = _get_packet_data(foundation.GeneralCommand.Report_Attributes, attr)
device.handle_message(260, cluster_id, endpoint_id, endpoint_id, data)
device.packet_received(
t.ZigbeePacket(
profile_id=260,
cluster_id=cluster_id,
src_ep=endpoint_id,
dst_ep=endpoint_id,
data=t.SerializableBytes(data),
)
)
# ZHA event emitted because we pass non "action_report"
# reports to the base class handler.
assert cluster_listener.zha_send_event.call_count == 1
Expand Down
100 changes: 85 additions & 15 deletions zhaquirks/sinope/__init__.py
Original file line number Diff line number Diff line change
@@ -1,21 +1,25 @@
"""Module for Sinope quirks implementations."""

from zigpy.quirks import CustomCluster
import zigpy.types as t
from zigpy.zcl.clusters.general import DeviceTemperature

from zhaquirks.const import (
ARGS,
ATTRIBUTE_ID,
ATTRIBUTE_NAME,
BUTTON,
CLUSTER_ID,
COMMAND,
COMMAND_BUTTON_DOUBLE,
COMMAND_BUTTON_HOLD,
COMMAND_BUTTON_SINGLE,
COMMAND_M_INITIAL_PRESS,
COMMAND_M_LONG_RELEASE,
COMMAND_M_MULTI_PRESS_COMPLETE,
COMMAND_M_SHORT_RELEASE,
DOUBLE_PRESS,
ENDPOINT_ID,
LONG_PRESS,
SHORT_PRESS,
SHORT_RELEASE,
TURN_OFF,
TURN_ON,
VALUE,
Expand All @@ -25,42 +29,108 @@
SINOPE_MANUFACTURER_CLUSTER_ID = 0xFF01
ATTRIBUTE_ACTION = "action_report"


class ButtonAction(t.enum8):
"""Action_report values."""

Pressed_on = 0x01
Released_on = 0x02
Long_on = 0x03
Double_on = 0x04
Pressed_off = 0x11
Released_off = 0x12
Long_off = 0x13
Double_off = 0x14


LIGHT_DEVICE_TRIGGERS = {
(SHORT_PRESS, TURN_ON): {
ENDPOINT_ID: 1,
CLUSTER_ID: 65281,
COMMAND: COMMAND_BUTTON_SINGLE,
ARGS: {ATTRIBUTE_ID: 84, ATTRIBUTE_NAME: ATTRIBUTE_ACTION, VALUE: 2},
COMMAND: COMMAND_M_INITIAL_PRESS,
ARGS: {
ATTRIBUTE_ID: 84,
ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
BUTTON: TURN_ON,
VALUE: ButtonAction.Pressed_on,
},
},
(SHORT_PRESS, TURN_OFF): {
ENDPOINT_ID: 1,
CLUSTER_ID: 65281,
COMMAND: COMMAND_BUTTON_SINGLE,
ARGS: {ATTRIBUTE_ID: 84, ATTRIBUTE_NAME: ATTRIBUTE_ACTION, VALUE: 18},
COMMAND: COMMAND_M_INITIAL_PRESS,
ARGS: {
ATTRIBUTE_ID: 84,
ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
BUTTON: TURN_OFF,
VALUE: ButtonAction.Pressed_off,
},
},
(SHORT_RELEASE, TURN_ON): {
ENDPOINT_ID: 1,
CLUSTER_ID: 65281,
COMMAND: COMMAND_M_SHORT_RELEASE,
ARGS: {
ATTRIBUTE_ID: 84,
ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
BUTTON: TURN_ON,
VALUE: ButtonAction.Released_on,
},
},
(SHORT_RELEASE, TURN_OFF): {
ENDPOINT_ID: 1,
CLUSTER_ID: 65281,
COMMAND: COMMAND_M_SHORT_RELEASE,
ARGS: {
ATTRIBUTE_ID: 84,
ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
BUTTON: TURN_OFF,
VALUE: ButtonAction.Released_off,
},
},
(DOUBLE_PRESS, TURN_ON): {
ENDPOINT_ID: 1,
CLUSTER_ID: 65281,
COMMAND: COMMAND_BUTTON_DOUBLE,
ARGS: {ATTRIBUTE_ID: 84, ATTRIBUTE_NAME: ATTRIBUTE_ACTION, VALUE: 4},
COMMAND: COMMAND_M_MULTI_PRESS_COMPLETE,
ARGS: {
ATTRIBUTE_ID: 84,
ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
BUTTON: TURN_ON,
VALUE: ButtonAction.Double_on,
},
},
(DOUBLE_PRESS, TURN_OFF): {
ENDPOINT_ID: 1,
CLUSTER_ID: 65281,
COMMAND: COMMAND_BUTTON_DOUBLE,
ARGS: {ATTRIBUTE_ID: 84, ATTRIBUTE_NAME: ATTRIBUTE_ACTION, VALUE: 20},
COMMAND: COMMAND_M_MULTI_PRESS_COMPLETE,
ARGS: {
ATTRIBUTE_ID: 84,
ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
BUTTON: TURN_OFF,
VALUE: ButtonAction.Double_off,
},
},
(LONG_PRESS, TURN_ON): {
ENDPOINT_ID: 1,
CLUSTER_ID: 65281,
COMMAND: COMMAND_BUTTON_HOLD,
ARGS: {ATTRIBUTE_ID: 84, ATTRIBUTE_NAME: ATTRIBUTE_ACTION, VALUE: 3},
COMMAND: COMMAND_M_LONG_RELEASE,
ARGS: {
ATTRIBUTE_ID: 84,
ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
BUTTON: TURN_ON,
VALUE: ButtonAction.Long_on,
},
},
(LONG_PRESS, TURN_OFF): {
ENDPOINT_ID: 1,
CLUSTER_ID: 65281,
COMMAND: COMMAND_BUTTON_HOLD,
ARGS: {ATTRIBUTE_ID: 84, ATTRIBUTE_NAME: ATTRIBUTE_ACTION, VALUE: 19},
COMMAND: COMMAND_M_LONG_RELEASE,
ARGS: {
ATTRIBUTE_ID: 84,
ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
BUTTON: TURN_OFF,
VALUE: ButtonAction.Long_off,
},
},
}

Expand Down
80 changes: 49 additions & 31 deletions zhaquirks/sinope/light.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,14 +29,20 @@
from zhaquirks.const import (
ATTRIBUTE_ID,
ATTRIBUTE_NAME,
COMMAND_BUTTON_DOUBLE,
COMMAND_BUTTON_HOLD,
BUTTON,
COMMAND_M_INITIAL_PRESS,
COMMAND_M_LONG_RELEASE,
COMMAND_M_MULTI_PRESS_COMPLETE,
COMMAND_M_SHORT_RELEASE,
DESCRIPTION,
DEVICE_TYPE,
ENDPOINTS,
INPUT_CLUSTERS,
MODELS_INFO,
OUTPUT_CLUSTERS,
PROFILE_ID,
TURN_OFF,
TURN_ON,
VALUE,
ZHA_SEND_EVENT,
)
Expand All @@ -45,6 +51,7 @@
LIGHT_DEVICE_TRIGGERS,
SINOPE,
SINOPE_MANUFACTURER_CLUSTER_ID,
ButtonAction,
CustomDeviceTemperatureCluster,
)

Expand Down Expand Up @@ -73,19 +80,6 @@ class DoubleFull(t.enum8):
On = 0x01


class ButtonAction(t.enum8):
"""Action_report values."""

Single_on = 0x01
Single_release_on = 0x02
Long_on = 0x03
Double_on = 0x04
Single_off = 0x11
Single_release_off = 0x12
Long_off = 0x13
Double_off = 0x14


class SinopeTechnologiesManufacturerCluster(CustomCluster):
"""SinopeTechnologiesManufacturerCluster manufacturer cluster."""

Expand Down Expand Up @@ -191,29 +185,53 @@ def handle_cluster_general_request(
hdr, args, dst_addressing=dst_addressing
)

value = attr.value.value
action = self.Action(attr.value.value)

command, button = self._get_command_from_action(action)
if not command or not button:
return

event_args = {
ATTRIBUTE_ID: 84,
ATTRIBUTE_NAME: ATTRIBUTE_ACTION,
VALUE: value.value,
BUTTON: button,
DESCRIPTION: action.name,
VALUE: action.value,
}
action = self._get_command_from_action(self.Action(value))
if not action:
return
self.listener_event(ZHA_SEND_EVENT, action, event_args)

def _get_command_from_action(self, action: ButtonAction) -> str | None:
# const lookup = {2: 'up_single', 3: 'up_hold', 4: 'up_double',
# 18: 'down_single', 19: 'down_hold', 20: 'down_double'};
self.debug(
"SINOPE ZHA_SEND_EVENT command: '%s' event_args: %s",
command,
event_args,
)

self.listener_event(ZHA_SEND_EVENT, command, event_args)

def _get_command_from_action(
self, action: ButtonAction
) -> tuple[str | None, str | None]:
# const lookup = {1: 'up_single', 2: 'up_single_released', 3: 'up_hold', 4: 'up_double',
# 17: 'down_single, 18: 'down_single_released', 19: 'down_hold', 20: 'down_double'};
match action:
case self.Action.Single_off | self.Action.Single_on:
return None
case self.Action.Double_off | self.Action.Double_on:
return COMMAND_BUTTON_DOUBLE
case self.Action.Long_off | self.Action.Long_on:
return COMMAND_BUTTON_HOLD
case self.Action.Pressed_off:
return COMMAND_M_INITIAL_PRESS, TURN_OFF
case self.Action.Pressed_on:
return COMMAND_M_INITIAL_PRESS, TURN_ON
case self.Action.Released_off:
return COMMAND_M_SHORT_RELEASE, TURN_OFF
case self.Action.Released_on:
return COMMAND_M_SHORT_RELEASE, TURN_ON
case self.Action.Double_off:
return COMMAND_M_MULTI_PRESS_COMPLETE, TURN_OFF
case self.Action.Double_on:
return COMMAND_M_MULTI_PRESS_COMPLETE, TURN_ON
case self.Action.Long_off:
return COMMAND_M_LONG_RELEASE, TURN_OFF
case self.Action.Long_on:
return COMMAND_M_LONG_RELEASE, TURN_ON
case _:
return None
self.debug("SINOPE unhandled action: %s", action)
return None, None


class LightManufacturerCluster(EventableCluster, SinopeTechnologiesManufacturerCluster):
Expand Down