Skip to content

Commit 431ae4d

Browse files
authored
Refactor Tuya spell tests (#2941)
This moves spell-related Tuya tests from `test_tuya.py` to `test_tuya_spells.py`.
1 parent c55555e commit 431ae4d

File tree

2 files changed

+226
-208
lines changed

2 files changed

+226
-208
lines changed

tests/test_tuya.py

Lines changed: 2 additions & 208 deletions
Original file line numberDiff line numberDiff line change
@@ -2,26 +2,14 @@
22

33
import asyncio
44
import datetime
5-
import itertools
65
from unittest import mock
76

87
import pytest
9-
import zigpy
108
from zigpy.profiles import zha
119
from zigpy.quirks import CustomDevice, get_device
1210
import zigpy.types as t
1311
from zigpy.zcl import foundation
14-
from zigpy.zcl.clusters import CLUSTERS_BY_ID
15-
from zigpy.zcl.clusters.general import (
16-
Basic,
17-
GreenPowerProxy,
18-
Groups,
19-
Identify,
20-
OnOff,
21-
Ota,
22-
PowerConfiguration,
23-
)
24-
from zigpy.zcl.clusters.lightlink import LightLink
12+
from zigpy.zcl.clusters.general import PowerConfiguration
2513

2614
import zhaquirks
2715
from zhaquirks.const import (
@@ -33,18 +21,9 @@
3321
ON,
3422
OUTPUT_CLUSTERS,
3523
PROFILE_ID,
36-
SKIP_CONFIGURATION,
3724
ZONE_STATUS_CHANGE_COMMAND,
3825
)
39-
from zhaquirks.tuya import (
40-
TUYA_QUERY_DATA,
41-
Data,
42-
EnchantedDevice,
43-
TuyaEnchantableCluster,
44-
TuyaManufClusterAttributes,
45-
TuyaNewManufCluster,
46-
TuyaZBOnOffAttributeCluster,
47-
)
26+
from zhaquirks.tuya import Data, TuyaManufClusterAttributes, TuyaNewManufCluster
4827
import zhaquirks.tuya.sm0202_motion
4928
import zhaquirks.tuya.ts011f_plug
5029
import zhaquirks.tuya.ts0041
@@ -1610,188 +1589,3 @@ async def test_power_config_no_bind(zigpy_device_from_quirk, quirk):
16101589

16111590
assert len(request_mock.mock_calls) == 0
16121591
assert len(bind_mock.mock_calls) == 0
1613-
1614-
1615-
class TuyaTestSpellDevice(EnchantedDevice):
1616-
"""Tuya test spell device."""
1617-
1618-
tuya_spell_data_query = True # enable additional data query spell
1619-
1620-
signature = {
1621-
MODELS_INFO: [("UjqjHq6ZErY23tgs", "zo9WD7q5dbvDj96y")],
1622-
ENDPOINTS: {
1623-
1: {
1624-
PROFILE_ID: zha.PROFILE_ID,
1625-
DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
1626-
INPUT_CLUSTERS: [
1627-
Basic.cluster_id,
1628-
OnOff.cluster_id,
1629-
TuyaNewManufCluster.cluster_id,
1630-
],
1631-
OUTPUT_CLUSTERS: [],
1632-
}
1633-
},
1634-
}
1635-
1636-
replacement = {
1637-
ENDPOINTS: {
1638-
1: {
1639-
PROFILE_ID: zha.PROFILE_ID,
1640-
DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
1641-
INPUT_CLUSTERS: [
1642-
Basic.cluster_id,
1643-
TuyaZBOnOffAttributeCluster,
1644-
TuyaNewManufCluster,
1645-
],
1646-
OUTPUT_CLUSTERS: [],
1647-
}
1648-
}
1649-
}
1650-
1651-
1652-
ENCHANTED_QUIRKS = [TuyaTestSpellDevice]
1653-
for manufacturer in zigpy.quirks._DEVICE_REGISTRY._registry.values():
1654-
for model_quirk_list in manufacturer.values():
1655-
for quirk_entry in model_quirk_list:
1656-
if quirk_entry in ENCHANTED_QUIRKS:
1657-
continue
1658-
if issubclass(quirk_entry, EnchantedDevice):
1659-
ENCHANTED_QUIRKS.append(quirk_entry)
1660-
1661-
del quirk_entry, model_quirk_list, manufacturer
1662-
1663-
1664-
@mock.patch("zigpy.zcl.Cluster.bind", mock.AsyncMock())
1665-
async def test_tuya_spell(zigpy_device_from_quirk):
1666-
"""Test that enchanted Tuya devices have their spell applied when binding bindable cluster."""
1667-
non_bindable_cluster_ids = [
1668-
Basic.cluster_id,
1669-
Identify.cluster_id,
1670-
Groups.cluster_id,
1671-
Ota.cluster_id,
1672-
GreenPowerProxy.cluster_id,
1673-
LightLink.cluster_id,
1674-
]
1675-
1676-
request_patch = mock.patch("zigpy.zcl.Cluster.request", mock.AsyncMock())
1677-
with request_patch as request_mock:
1678-
request_mock.return_value = (foundation.Status.SUCCESS, "done")
1679-
1680-
for quirk in ENCHANTED_QUIRKS:
1681-
device = zigpy_device_from_quirk(quirk)
1682-
assert isinstance(device, EnchantedDevice)
1683-
1684-
# fail if SKIP_CONFIGURATION is set, as that will cause ZHA to not call bind()
1685-
if getattr(device, SKIP_CONFIGURATION, False):
1686-
pytest.fail(
1687-
f"Enchanted quirk {quirk} has SKIP_CONFIGURATION set. "
1688-
f"This is not allowed for enchanted devices."
1689-
)
1690-
1691-
for cluster in itertools.chain(
1692-
device.endpoints[1].in_clusters.values(),
1693-
device.endpoints[1].out_clusters.values(),
1694-
):
1695-
# emulate ZHA calling bind() on most default clusters with an unchanged ep_attribute
1696-
if (
1697-
not isinstance(cluster, int)
1698-
and cluster.cluster_id not in non_bindable_cluster_ids
1699-
and cluster.cluster_id in CLUSTERS_BY_ID
1700-
and CLUSTERS_BY_ID[cluster.cluster_id].ep_attribute
1701-
== cluster.ep_attribute
1702-
):
1703-
await cluster.bind()
1704-
1705-
# the number of Tuya spells that are allowed to be cast, so the sum of enabled Tuya spells
1706-
enabled_tuya_spells_num = (
1707-
device.tuya_spell_read_attributes + device.tuya_spell_data_query
1708-
)
1709-
1710-
# skip if no Tuya spells are enabled,
1711-
# this case is already handled in the test_tuya_spell_devices_valid() test
1712-
if enabled_tuya_spells_num == 0:
1713-
continue
1714-
1715-
# check that exactly a Tuya spell was cast
1716-
if len(request_mock.mock_calls) == 0:
1717-
pytest.fail(
1718-
f"Enchanted quirk {quirk} did not cast a Tuya spell. "
1719-
f"One bindable cluster subclassing `TuyaEnchantableCluster` on endpoint 1 needs to be implemented. "
1720-
f"Also check that enchanted bindable clusters do not modify their `ep_attribute`, "
1721-
f"as ZHA will not call bind() in that case."
1722-
)
1723-
# check that no more than one call was made for each enabled spell
1724-
elif len(request_mock.mock_calls) > enabled_tuya_spells_num:
1725-
pytest.fail(
1726-
f"Enchanted quirk {quirk} cast more than one Tuya spell. "
1727-
f"Make sure to only implement one cluster subclassing `TuyaEnchantableCluster` on endpoint 1."
1728-
)
1729-
1730-
# used to check list of mock calls below
1731-
messages = 0
1732-
1733-
# check 'attribute read spell' was cast correctly (if enabled)
1734-
if device.tuya_spell_read_attributes:
1735-
assert (
1736-
request_mock.mock_calls[messages][1][1]
1737-
== foundation.GeneralCommand.Read_Attributes
1738-
)
1739-
assert request_mock.mock_calls[messages][1][3] == [4, 0, 1, 5, 7, 65534]
1740-
messages += 1
1741-
1742-
# check 'query data spell' was cast correctly (if enabled)
1743-
if device.tuya_spell_data_query:
1744-
assert not request_mock.mock_calls[messages][1][0]
1745-
assert request_mock.mock_calls[messages][1][1] == TUYA_QUERY_DATA
1746-
messages += 1
1747-
1748-
request_mock.reset_mock()
1749-
1750-
1751-
def test_tuya_spell_devices_valid():
1752-
"""Test that all enchanted Tuya devices implement at least one enchanted cluster."""
1753-
1754-
for quirk in ENCHANTED_QUIRKS:
1755-
# check that at least one Tuya spell is enabled for an EnchantedDevice
1756-
if not quirk.tuya_spell_read_attributes and not quirk.tuya_spell_data_query:
1757-
pytest.fail(
1758-
f"Enchanted quirk {quirk} does not have any Tuya spells enabled. "
1759-
f"Enable at least one Tuya spell by setting `TUYA_SPELL_READ_ATTRIBUTES` or `TUYA_SPELL_DATA_QUERY` "
1760-
f"or inherit CustomDevice rather than EnchantedDevice."
1761-
)
1762-
1763-
enchanted_clusters = 0 # number of clusters subclassing TuyaEnchantableCluster
1764-
tuya_cluster_exists = False # cluster subclassing TuyaNewManufCluster existing
1765-
1766-
# iterate over all clusters in the replacement
1767-
for endpoint_id, endpoint in quirk.replacement[ENDPOINTS].items():
1768-
if endpoint_id != 1: # spell is only activated on endpoint 1 for now
1769-
continue
1770-
for cluster in endpoint[INPUT_CLUSTERS] + endpoint[OUTPUT_CLUSTERS]:
1771-
if not isinstance(cluster, int):
1772-
# count all clusters which would apply the spell on bind()
1773-
if issubclass(cluster, TuyaEnchantableCluster):
1774-
enchanted_clusters += 1
1775-
# check if there's a valid Tuya cluster where the id wasn't modified
1776-
if (
1777-
issubclass(cluster, TuyaNewManufCluster)
1778-
and cluster.cluster_id == TuyaNewManufCluster.cluster_id
1779-
):
1780-
tuya_cluster_exists = True
1781-
1782-
# an EnchantedDevice must have exactly one enchanted cluster on endpoint 1
1783-
if enchanted_clusters == 0:
1784-
pytest.fail(
1785-
f"{quirk} does not have a cluster subclassing `TuyaEnchantableCluster` on endpoint 1 "
1786-
f"as required by the Tuya spell."
1787-
)
1788-
elif enchanted_clusters > 1:
1789-
pytest.fail(
1790-
f"{quirk} has more than one cluster subclassing `TuyaEnchantableCluster` on endpoint 1"
1791-
)
1792-
1793-
# an EnchantedDevice with the data query spell must also have a cluster subclassing TuyaNewManufCluster
1794-
if quirk.tuya_spell_data_query and not tuya_cluster_exists:
1795-
pytest.fail(
1796-
f"{quirk} set Tuya data query spell but has no cluster subclassing `TuyaNewManufCluster` on endpoint 1"
1797-
)

0 commit comments

Comments
 (0)