Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
33 commits
Select commit Hold shift + click to select a range
c07c896
add 3r project py
jintj Aug 30, 2024
138ae97
[pre-commit.ci] auto fixes from pre-commit.com hooks
pre-commit-ci[bot] Aug 30, 2024
ef0efaa
update
jintj Sep 20, 2025
0148d86
update
jintj Sep 20, 2025
20f777e
update
jintj Sep 20, 2025
0e87d69
Apply pre-commit auto fixes
pre-commit-ci[bot] Sep 20, 2025
126ecd8
update button tes
jintj Sep 24, 2025
36eee8f
Apply pre-commit auto fixes
pre-commit-ci[bot] Sep 24, 2025
2cc011f
update
jintj Sep 24, 2025
b4cdbf6
Merge branch 'master' of https://github.com/3reality-support/zha-devi…
jintj Sep 24, 2025
89e80cb
Apply pre-commit auto fixes
pre-commit-ci[bot] Sep 24, 2025
115e9e4
Update test_third_button.py
3reality-support Sep 24, 2025
b484bc7
Apply pre-commit auto fixes
pre-commit-ci[bot] Sep 24, 2025
c798a96
Update test_third_button.py
3reality-support Sep 25, 2025
88c9847
Apply pre-commit auto fixes
pre-commit-ci[bot] Sep 25, 2025
4f74b2e
Update test_third_button.py
3reality-support Sep 25, 2025
e25a838
Apply pre-commit auto fixes
pre-commit-ci[bot] Sep 25, 2025
e275a43
Update test_third_button.py
3reality-support Sep 25, 2025
c5b8f9a
Apply pre-commit auto fixes
pre-commit-ci[bot] Sep 25, 2025
86ac6c4
Update test_third_button.py
3reality-support Sep 25, 2025
44748cb
Apply pre-commit auto fixes
pre-commit-ci[bot] Sep 25, 2025
4a8b815
Update test_third_button.py
3reality-support Sep 25, 2025
58b6ed2
Update test_third_button.py
3reality-support Sep 25, 2025
b95f82c
Apply pre-commit auto fixes
pre-commit-ci[bot] Sep 25, 2025
9f0034f
Update test_third_button.py
3reality-support Sep 25, 2025
cfd1145
Apply pre-commit auto fixes
pre-commit-ci[bot] Sep 25, 2025
63a1b15
Update test_third_button.py
3reality-support Sep 25, 2025
60447b7
update
jintj Sep 25, 2025
26a4356
Merge branch 'dev' into master
3reality-support Sep 25, 2025
183d16b
Clean up quirk, replace old v1 quirk
TheJulianJES Oct 28, 2025
60f7652
Remove from quirks v1 `KNOWN_DUPLICATE_TRIGGERS` test
TheJulianJES Oct 28, 2025
68fcff7
Move and clean up test
TheJulianJES Oct 28, 2025
61dc2e5
Remove unnecessary MockListener from test
TheJulianJES Oct 28, 2025
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
6 changes: 0 additions & 6 deletions tests/test_quirks.py
Original file line number Diff line number Diff line change
Expand Up @@ -661,12 +661,6 @@ def test_migrated_lighting_automation_triggers(quirk: CustomDevice) -> None:
(const.LONG_RELEASE, const.BUTTON_4),
],
],
zhaquirks.thirdreality.button.Button: [
[
(const.LONG_PRESS, const.LONG_PRESS),
(const.LONG_RELEASE, const.LONG_RELEASE),
]
],
}


Expand Down
38 changes: 38 additions & 0 deletions tests/test_thirdreality.py
Original file line number Diff line number Diff line change
@@ -1,10 +1,13 @@
"""Tests for Third Reality quirks."""

from unittest import mock

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

from tests.common import ClusterListener
import zhaquirks
from zhaquirks.thirdreality.button import MultistateInputCluster
import zhaquirks.thirdreality.night_light

zhaquirks.setup()
Expand Down Expand Up @@ -36,3 +39,38 @@ async def test_third_reality_nightlight(zigpy_device_from_quirk, quirk):
assert len(ias_zone_listener.attribute_updates) == 2
assert ias_zone_listener.attribute_updates[1][0] == ias_zone_status_id
assert ias_zone_listener.attribute_updates[1][1] == 0


@pytest.mark.parametrize(
("attr_value", "expected_action"),
[
(1, "single"), # 1 corresponds to single click
(2, "double"), # 2 corresponds to double click
(0, "hold"), # 0 corresponds to hold
(255, "release"), # 255 corresponds to release
],
)
@pytest.mark.parametrize(
("manufacturer", "model"),
[("Third Reality, Inc", "3RSB22BZ")],
)
async def test_third_reality_button_v2(
zigpy_device_from_v2_quirk, manufacturer, model, attr_value, expected_action
):
"""Test Third Reality button event conversion and triggering functionality."""
device = zigpy_device_from_v2_quirk(manufacturer, model)
multistate_cluster = device.endpoints[1].in_clusters[
MultistateInputCluster.cluster_id
]

# Create mock listener and register it with the cluster
listener = mock.MagicMock()
multistate_cluster.add_listener(listener)

multistate_cluster.update_attribute(
0x0055, attr_value
) # 1 corresponds to single click
assert listener.zha_send_event.call_count == 1
assert listener.zha_send_event.call_args_list[0] == mock.call(
expected_action, {"value": attr_value}
)
134 changes: 51 additions & 83 deletions zhaquirks/thirdreality/button.py
Original file line number Diff line number Diff line change
@@ -1,41 +1,28 @@
"""Third Reality button devices."""

from zigpy.profiles import zha
from zigpy.quirks import CustomDevice
from zigpy.zcl.clusters.general import Basic, LevelControl, MultistateInput, OnOff, Ota
from typing import Final

from zigpy.quirks import CustomCluster
from zigpy.quirks.v2 import QuirkBuilder
import zigpy.types as t
from zigpy.zcl.clusters.general import MultistateInput
from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef

from zhaquirks import CustomCluster, PowerConfigurationCluster
from zhaquirks.const import (
COMMAND,
COMMAND_DOUBLE,
COMMAND_HOLD,
COMMAND_RELEASE,
COMMAND_SINGLE,
DEVICE_TYPE,
DOUBLE_PRESS,
ENDPOINTS,
INPUT_CLUSTERS,
LONG_PRESS,
LONG_RELEASE,
MODELS_INFO,
OUTPUT_CLUSTERS,
PROFILE_ID,
SHORT_PRESS,
SKIP_CONFIGURATION,
VALUE,
ZHA_SEND_EVENT,
)
from zhaquirks.thirdreality import THIRD_REALITY


class CustomPowerConfigurationCluster(PowerConfigurationCluster):
"""Custom PowerConfigurationCluster."""

MIN_VOLTS = 2.1
MAX_VOLTS = 3.0


MOVEMENT_TYPE = {
PRESS_TYPE = {
0: COMMAND_HOLD,
1: COMMAND_SINGLE,
2: COMMAND_DOUBLE,
Expand All @@ -46,67 +33,48 @@ class CustomPowerConfigurationCluster(PowerConfigurationCluster):
class MultistateInputCluster(CustomCluster, MultistateInput):
"""Multistate input cluster."""

def __init__(self, *args, **kwargs):
"""Init."""
self._current_state = {}
super().__init__(*args, **kwargs)

def _update_attribute(self, attrid, value):
super()._update_attribute(attrid, value)
if attrid == 0x0055:
self._current_state[0x0055] = action = MOVEMENT_TYPE.get(value)
event_args = {VALUE: value}
if action is not None:
self.listener_event(ZHA_SEND_EVENT, action, event_args)

# show something in the sensor in HA
super()._update_attribute(0, action)


class Button(CustomDevice):
"""thirdreality button device - alternate version."""

signature = {
MODELS_INFO: [(THIRD_REALITY, "3RSB22BZ")],
ENDPOINTS: {
1: {
PROFILE_ID: 0x0104,
DEVICE_TYPE: 0x0006,
INPUT_CLUSTERS: [
Basic.cluster_id,
MultistateInput.cluster_id,
CustomPowerConfigurationCluster.cluster_id,
],
OUTPUT_CLUSTERS: [
OnOff.cluster_id,
LevelControl.cluster_id,
Ota.cluster_id,
],
}
},
}
replacement = {
SKIP_CONFIGURATION: True,
ENDPOINTS: {
1: {
DEVICE_TYPE: zha.DeviceType.REMOTE_CONTROL,
INPUT_CLUSTERS: [
Basic.cluster_id,
CustomPowerConfigurationCluster,
MultistateInputCluster,
],
OUTPUT_CLUSTERS: [
OnOff.cluster_id,
LevelControl.cluster_id,
Ota.cluster_id,
],
}
},
}

device_automation_triggers = {
(DOUBLE_PRESS, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE},
(SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE},
(LONG_PRESS, LONG_PRESS): {COMMAND: COMMAND_HOLD},
(LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_RELEASE},
}
if attrid == 0x0055 and (action := PRESS_TYPE.get(value)) is not None:
self.listener_event(ZHA_SEND_EVENT, action, {VALUE: value})


class ThirdRealityButtonCluster(CustomCluster):
"""Third Reality's button private cluster."""

cluster_id = 0xFF01

class AttributeDefs(BaseAttributeDefs):
"""Define the attributes of a private cluster."""

cancel_double_click: Final = ZCLAttributeDef(
id=0x0000,
type=t.uint8_t,
is_manufacturer_specific=True,
)


(
QuirkBuilder("Third Reality, Inc", "3RSB22BZ")
.replaces(ThirdRealityButtonCluster)
.replaces(MultistateInputCluster)
.number(
attribute_name=ThirdRealityButtonCluster.AttributeDefs.cancel_double_click.name,
cluster_id=ThirdRealityButtonCluster.cluster_id,
endpoint_id=1,
min_value=0,
max_value=65535,
step=1,
translation_key="cancel_double_click",
fallback_name="Cancel double click",
)
.device_automation_triggers(
{
(DOUBLE_PRESS, DOUBLE_PRESS): {COMMAND: COMMAND_DOUBLE},
(SHORT_PRESS, SHORT_PRESS): {COMMAND: COMMAND_SINGLE},
(LONG_PRESS, LONG_PRESS): {COMMAND: COMMAND_HOLD},
(LONG_RELEASE, LONG_RELEASE): {COMMAND: COMMAND_RELEASE},
}
)
Comment on lines +71 to +78
Copy link
Collaborator

@TheJulianJES TheJulianJES Oct 28, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are unchanged, but I don't think (DOUBLE_PRESS, DOUBLE_PRESS) is really nice. I think this is (press_type, button) normally. Device triggers will be replaced at some point anyways.

We'll have to keep this to not break existing automations though.

.add_to_registry()
)
Loading