Skip to content

Commit 278817c

Browse files
authored
Merge branch 'dev' into add-SZ-T04
2 parents f05e022 + 57bcec8 commit 278817c

File tree

11 files changed

+146
-31
lines changed

11 files changed

+146
-31
lines changed

pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@ readme = "README.md"
1414
license = {text = "Apache-2.0"}
1515
requires-python = ">=3.12"
1616
dependencies = [
17-
"zigpy>=0.70.0",
17+
"zigpy>=0.74.0",
1818
]
1919

2020
[tool.setuptools.packages.find]

requirements_test.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,5 +13,5 @@ pytest-sugar
1313
pytest-timeout
1414
pytest-asyncio
1515
pytest>=7.1.3
16-
zigpy>=0.70
16+
zigpy>=0.74.0
1717
ruff==0.0.261

tests/conftest.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import pytest
66
import zigpy.application
77
import zigpy.device
8+
from zigpy.device import Device
89
import zigpy.quirks
910
import zigpy.types
1011
from zigpy.zcl import foundation
@@ -21,6 +22,8 @@
2122
PROFILE_ID,
2223
)
2324

25+
from .async_mock import sentinel
26+
2427

2528
class MockApp(zigpy.application.ControllerApplication):
2629
"""App Controller."""
@@ -204,6 +207,25 @@ def _dev(
204207
return _dev
205208

206209

210+
@pytest.fixture(name="device_mock")
211+
def real_device(MockAppController):
212+
"""Device fixture with a single endpoint."""
213+
ieee = sentinel.ieee
214+
nwk = 0x2233
215+
device = Device(MockAppController, ieee, nwk)
216+
217+
device.add_endpoint(1)
218+
device[1].profile_id = 0x0104
219+
device[1].device_type = 0x0051
220+
device.model = "model"
221+
device.manufacturer = "manufacturer"
222+
device[1].add_input_cluster(0x0000)
223+
device[1].add_input_cluster(0xEF00)
224+
device[1].add_output_cluster(0x000A)
225+
device[1].add_output_cluster(0x0019)
226+
return device
227+
228+
207229
@pytest.fixture
208230
def assert_signature_matches_quirk():
209231
"""Return a function that can be used to check if a given quirk matches a signature."""

tests/test_quirks.py

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
import zigpy.endpoint
1515
import zigpy.profiles
1616
import zigpy.quirks as zq
17-
from zigpy.quirks import CustomDevice
17+
from zigpy.quirks import CustomDevice, DeviceRegistry
1818
from zigpy.quirks.v2 import QuirkBuilder
1919
import zigpy.types
2020
from zigpy.zcl import foundation
@@ -845,8 +845,9 @@ def check_for_duplicate_cluster_ids(clusters) -> None:
845845
check_for_duplicate_cluster_ids(ep_data.get(OUTPUT_CLUSTERS, []))
846846

847847

848-
async def test_local_data_cluster(zigpy_device_from_v2_quirk) -> None:
848+
async def test_local_data_cluster(device_mock) -> None:
849849
"""Ensure reading attributes from a LocalDataCluster works as expected."""
850+
registry = DeviceRegistry()
850851

851852
class TestLocalCluster(zhaquirks.LocalDataCluster):
852853
"""Test cluster."""
@@ -856,11 +857,11 @@ class TestLocalCluster(zhaquirks.LocalDataCluster):
856857
_VALID_ATTRIBUTES = [2]
857858

858859
(
859-
QuirkBuilder("manufacturer-local-test", "model")
860+
QuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry)
860861
.adds(TestLocalCluster)
861862
.add_to_registry()
862863
)
863-
device = zigpy_device_from_v2_quirk("manufacturer-local-test", "model")
864+
device = registry.get_device(device_mock)
864865
assert isinstance(device.endpoints[1].in_clusters[0x1234], TestLocalCluster)
865866

866867
# reading invalid attribute return unsupported attribute

tests/test_tuya_builder.py

Lines changed: 71 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,6 @@
33
from unittest import mock
44

55
import pytest
6-
from zigpy.device import Device
76
from zigpy.quirks.registry import DeviceRegistry
87
from zigpy.quirks.v2 import CustomDeviceV2
98
import zigpy.types as t
@@ -12,6 +11,7 @@
1211

1312
from tests.common import ClusterListener, wait_for_zigpy_tasks
1413
import zhaquirks
14+
from zhaquirks.tuya import TUYA_QUERY_DATA
1515
from zhaquirks.tuya.builder import (
1616
TuyaIasContact,
1717
TuyaIasFire,
@@ -24,30 +24,9 @@
2424
)
2525
from zhaquirks.tuya.mcu import TuyaMCUCluster, TuyaOnOffNM
2626

27-
from .async_mock import sentinel
28-
2927
zhaquirks.setup()
3028

3129

32-
@pytest.fixture(name="device_mock")
33-
def real_device(MockAppController):
34-
"""Device fixture with a single endpoint."""
35-
ieee = sentinel.ieee
36-
nwk = 0x2233
37-
device = Device(MockAppController, ieee, nwk)
38-
39-
device.add_endpoint(1)
40-
device[1].profile_id = 0x0104
41-
device[1].device_type = 0x0051
42-
device.model = "model"
43-
device.manufacturer = "manufacturer"
44-
device[1].add_input_cluster(0x0000)
45-
device[1].add_input_cluster(0xEF00)
46-
device[1].add_output_cluster(0x000A)
47-
device[1].add_output_cluster(0x0019)
48-
return device
49-
50-
5130
@pytest.mark.parametrize(
5231
"method_name,attr_name,exp_class",
5332
[
@@ -191,3 +170,73 @@ class ModTuyaMCUCluster(TuyaMCUCluster):
191170

192171
assert tuya_listener.attribute_updates[0][0] == 0xEF0A
193172
assert tuya_listener.attribute_updates[0][1] == TestEnum.B
173+
174+
175+
@pytest.mark.parametrize(
176+
"read_attr_spell,data_query_spell",
177+
[
178+
(True, False),
179+
(False, True),
180+
(True, True),
181+
(False, False),
182+
],
183+
)
184+
async def test_tuya_spell(device_mock, read_attr_spell, data_query_spell):
185+
"""Test that enchanted Tuya devices have their spells applied during configuration."""
186+
registry = DeviceRegistry()
187+
188+
entry = (
189+
TuyaQuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry)
190+
.tuya_battery(dp_id=1)
191+
.tuya_onoff(dp_id=3)
192+
.tuya_enchantment(
193+
read_attr_spell=read_attr_spell, data_query_spell=data_query_spell
194+
)
195+
.skip_configuration()
196+
.add_to_registry()
197+
)
198+
199+
# coverage for overridden __eq__ method
200+
assert entry.adds_metadata[0] != entry.adds_metadata[1]
201+
assert entry.adds_metadata[0] != entry
202+
203+
quirked = registry.get_device(device_mock)
204+
205+
assert isinstance(quirked, CustomDeviceV2)
206+
assert quirked in registry
207+
208+
request_patch = mock.patch("zigpy.zcl.Cluster.request", mock.AsyncMock())
209+
with request_patch as request_mock:
210+
request_mock.return_value = (foundation.Status.SUCCESS, "done")
211+
212+
# call apply_custom_configuration() on each EnchantedDevice
213+
# ZHA does this during device configuration normally
214+
await quirked.apply_custom_configuration()
215+
216+
# the number of Tuya spells that are allowed to be cast, so the sum of enabled Tuya spells
217+
enabled_tuya_spells_num = (
218+
quirked.tuya_spell_read_attributes + quirked.tuya_spell_data_query
219+
)
220+
221+
# verify request was called the correct number of times
222+
assert request_mock.call_count == enabled_tuya_spells_num
223+
224+
# used to check list of mock calls below
225+
messages = 0
226+
227+
# check 'attribute read spell' was cast correctly (if enabled)
228+
if quirked.tuya_spell_read_attributes:
229+
assert (
230+
request_mock.mock_calls[messages][1][1]
231+
== foundation.GeneralCommand.Read_Attributes
232+
)
233+
assert request_mock.mock_calls[messages][1][3] == [4, 0, 1, 5, 7, 65534]
234+
messages += 1
235+
236+
# check 'query data spell' was cast correctly (if enabled)
237+
if quirked.tuya_spell_data_query:
238+
assert not request_mock.mock_calls[messages][1][0]
239+
assert request_mock.mock_calls[messages][1][1] == TUYA_QUERY_DATA
240+
messages += 1
241+
242+
request_mock.reset_mock()

tests/test_tuya_motion.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
"model,manuf,occ_msg",
2525
[
2626
("_TZE200_ya4ft0w4", "TS0601", ZCL_TUYA_MOTION),
27+
("_TZE204_ya4ft0w4", "TS0601", ZCL_TUYA_MOTION),
2728
("_TZE200_7hfcudw5", "TS0601", ZCL_TUYA_MOTION_V2),
2829
("_TZE200_ppuj1vem", "TS0601", ZCL_TUYA_MOTION_V2),
2930
("_TZE200_ar0slwnd", "TS0601", ZCL_TUYA_MOTION),

tests/test_xiaomi.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1804,3 +1804,20 @@ async def test_aqara_fp1e_sensor(
18041804
assert len(ias_listener.attribute_updates) == 1
18051805
assert ias_listener.attribute_updates[0][0] == IasZone.AttributeDefs.zone_status.id
18061806
assert ias_listener.attribute_updates[0][1] == expected_motion_status
1807+
1808+
1809+
def test_h1_wireless_remotes(zigpy_device_from_v2_quirk):
1810+
"""Test Aqara H1 wireless remote quirk adds missing endpoints."""
1811+
# create device with endpoint 1 only and verify we don't get a KeyError
1812+
quirk = zigpy_device_from_v2_quirk(LUMI, "lumi.remote.b28ac1")
1813+
1814+
# verify the quirk adds endpoints 2 and 3
1815+
assert 2 in quirk.endpoints
1816+
assert 3 in quirk.endpoints
1817+
1818+
# verify the quirk adds the correct clusters to the new endpoints
1819+
assert OnOff.cluster_id in quirk.endpoints[2].out_clusters
1820+
assert OnOff.cluster_id in quirk.endpoints[3].out_clusters
1821+
1822+
assert MultistateInput.cluster_id in quirk.endpoints[2].in_clusters
1823+
assert MultistateInput.cluster_id in quirk.endpoints[3].in_clusters

zhaquirks/tuya/__init__.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
import logging
88
from typing import Any, Optional, Union
99

10-
from zigpy.quirks import CustomCluster, CustomDevice
10+
from zigpy.quirks import BaseCustomDevice, CustomCluster, CustomDevice
1111
import zigpy.types as t
1212
from zigpy.zcl import BaseAttributeDefs, foundation
1313
from zigpy.zcl.clusters.closures import WindowCovering
@@ -529,7 +529,7 @@ async def write_attributes(self, attributes, manufacturer=None):
529529
return [[foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS)]]
530530

531531

532-
class EnchantedDevice(CustomDevice):
532+
class BaseEnchantedDevice(BaseCustomDevice):
533533
"""Class for Tuya devices which need to be unlocked by casting a 'spell'.
534534
535535
The spell is applied during device configuration.
@@ -570,6 +570,10 @@ async def spell_data_query(self):
570570
self.debug("Executed data query spell on Tuya device %s", self.ieee)
571571

572572

573+
class EnchantedDevice(CustomDevice, BaseEnchantedDevice):
574+
"""Enchanted device class for v1 quirks."""
575+
576+
573577
class TuyaOnOff(CustomCluster, OnOff):
574578
"""Tuya On/Off cluster for On/Off device."""
575579

zhaquirks/tuya/builder/__init__.py

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66

77
from zigpy.quirks import _DEVICE_REGISTRY
88
from zigpy.quirks.registry import DeviceRegistry
9-
from zigpy.quirks.v2 import QuirkBuilder, QuirksV2RegistryEntry
9+
from zigpy.quirks.v2 import CustomDeviceV2, QuirkBuilder, QuirksV2RegistryEntry
1010
from zigpy.quirks.v2.homeassistant import EntityPlatform, EntityType
1111
from zigpy.quirks.v2.homeassistant.binary_sensor import BinarySensorDeviceClass
1212
from zigpy.quirks.v2.homeassistant.number import NumberDeviceClass
@@ -23,6 +23,7 @@
2323

2424
from zhaquirks.tuya import (
2525
TUYA_CLUSTER_ID,
26+
BaseEnchantedDevice,
2627
PowerConfiguration,
2728
TuyaLocalCluster,
2829
TuyaPowerConfigurationCluster2AAA,
@@ -522,6 +523,21 @@ def tuya_sensor(
522523

523524
return self
524525

526+
def tuya_enchantment(
527+
self, read_attr_spell: bool = True, data_query_spell: bool = False
528+
) -> QuirkBuilder:
529+
"""Set the Tuya enchantment spells."""
530+
531+
class EnchantedDeviceV2(CustomDeviceV2, BaseEnchantedDevice):
532+
"""Enchanted device class for v2 quirks."""
533+
534+
EnchantedDeviceV2.tuya_spell_read_attributes = read_attr_spell
535+
EnchantedDeviceV2.tuya_spell_data_query = data_query_spell
536+
537+
self.device_class(EnchantedDeviceV2)
538+
539+
return self
540+
525541
def add_to_registry(self, mcu_cluster=TuyaMCUCluster) -> QuirksV2RegistryEntry:
526542
"""Build the quirks v2 registry entry."""
527543

zhaquirks/tuya/ts0601_motion.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -126,6 +126,7 @@ class TuyaSelfCheckResult(t.enum8):
126126
(
127127
base_tuya_motion.clone()
128128
.applies_to("_TZE200_ya4ft0w4", "TS0601")
129+
.applies_to("_TZE204_ya4ft0w4", "TS0601")
129130
.tuya_dp(
130131
dp_id=1,
131132
ep_attribute=TuyaOccupancySensing.ep_attribute,

0 commit comments

Comments
 (0)