17
17
GreenPowerProxy ,
18
18
Groups ,
19
19
Identify ,
20
+ OnOff ,
20
21
Ota ,
21
22
PowerConfiguration ,
22
23
)
36
37
ZONE_STATUS_CHANGE_COMMAND ,
37
38
)
38
39
from zhaquirks .tuya import (
40
+ TUYA_QUERY_DATA ,
39
41
Data ,
42
+ EnchantedDevice ,
40
43
TuyaEnchantableCluster ,
41
44
TuyaManufClusterAttributes ,
42
45
TuyaNewManufCluster ,
46
+ TuyaZBOnOffAttributeCluster ,
43
47
)
44
48
import zhaquirks .tuya .sm0202_motion
45
49
import zhaquirks .tuya .ts011f_plug
@@ -1608,22 +1612,58 @@ async def test_power_config_no_bind(zigpy_device_from_quirk, quirk):
1608
1612
assert len (bind_mock .mock_calls ) == 0
1609
1613
1610
1614
1611
- ENCHANTED_QUIRKS = []
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 ]
1612
1653
for manufacturer in zigpy .quirks ._DEVICE_REGISTRY ._registry .values ():
1613
1654
for model_quirk_list in manufacturer .values ():
1614
1655
for quirk_entry in model_quirk_list :
1615
1656
if quirk_entry in ENCHANTED_QUIRKS :
1616
1657
continue
1617
- # right now, this basically includes `issubclass(quirk, EnchantedDevice)`, as that sets `TUYA_SPELL`
1618
- if getattr (quirk_entry , "TUYA_SPELL" , False ):
1658
+ if issubclass (quirk_entry , EnchantedDevice ):
1619
1659
ENCHANTED_QUIRKS .append (quirk_entry )
1620
1660
1621
1661
del quirk_entry , model_quirk_list , manufacturer
1622
1662
1623
1663
1624
1664
@mock .patch ("zigpy.zcl.Cluster.bind" , mock .AsyncMock ())
1625
1665
async def test_tuya_spell (zigpy_device_from_quirk ):
1626
- """Test that enchanted Tuya devices have their spell applied when binding OnOff cluster."""
1666
+ """Test that enchanted Tuya devices have their spell applied when binding bindable cluster."""
1627
1667
non_bindable_cluster_ids = [
1628
1668
Basic .cluster_id ,
1629
1669
Identify .cluster_id ,
@@ -1639,6 +1679,7 @@ async def test_tuya_spell(zigpy_device_from_quirk):
1639
1679
1640
1680
for quirk in ENCHANTED_QUIRKS :
1641
1681
device = zigpy_device_from_quirk (quirk )
1682
+ assert isinstance (device , EnchantedDevice )
1642
1683
1643
1684
# fail if SKIP_CONFIGURATION is set, as that will cause ZHA to not call bind()
1644
1685
if getattr (device , SKIP_CONFIGURATION , False ):
@@ -1661,45 +1702,84 @@ async def test_tuya_spell(zigpy_device_from_quirk):
1661
1702
):
1662
1703
await cluster .bind ()
1663
1704
1664
- # check that exactly one Tuya spell was cast
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
1665
1716
if len (request_mock .mock_calls ) == 0 :
1666
1717
pytest .fail (
1667
1718
f"Enchanted quirk { quirk } did not cast a Tuya spell. "
1668
1719
f"One bindable cluster subclassing `TuyaEnchantableCluster` on endpoint 1 needs to be implemented. "
1669
1720
f"Also check that enchanted bindable clusters do not modify their `ep_attribute`, "
1670
1721
f"as ZHA will not call bind() in that case."
1671
1722
)
1672
- elif len (request_mock .mock_calls ) > 1 :
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 :
1673
1725
pytest .fail (
1674
1726
f"Enchanted quirk { quirk } cast more than one Tuya spell. "
1675
1727
f"Make sure to only implement one cluster subclassing `TuyaEnchantableCluster` on endpoint 1."
1676
1728
)
1677
1729
1678
- assert (
1679
- request_mock .mock_calls [0 ][1 ][1 ]
1680
- == foundation .GeneralCommand .Read_Attributes
1681
- ) # read attributes
1682
- assert request_mock .mock_calls [0 ][1 ][3 ] == [4 , 0 , 1 , 5 , 7 , 65534 ] # spell
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
+
1683
1748
request_mock .reset_mock ()
1684
1749
1685
1750
1686
1751
def test_tuya_spell_devices_valid ():
1687
1752
"""Test that all enchanted Tuya devices implement at least one enchanted cluster."""
1688
1753
1689
1754
for quirk in ENCHANTED_QUIRKS :
1690
- enchanted_clusters = 0
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
1691
1765
1692
1766
# iterate over all clusters in the replacement
1693
1767
for endpoint_id , endpoint in quirk .replacement [ENDPOINTS ].items ():
1694
1768
if endpoint_id != 1 : # spell is only activated on endpoint 1 for now
1695
1769
continue
1696
1770
for cluster in endpoint [INPUT_CLUSTERS ] + endpoint [OUTPUT_CLUSTERS ]:
1697
- if not isinstance (cluster , int ) and issubclass (
1698
- cluster , TuyaEnchantableCluster
1699
- ):
1700
- enchanted_clusters += 1
1701
-
1702
- # one EnchantedDevice must have exactly one enchanted cluster on endpoint 1
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
1703
1783
if enchanted_clusters == 0 :
1704
1784
pytest .fail (
1705
1785
f"{ quirk } does not have a cluster subclassing `TuyaEnchantableCluster` on endpoint 1 "
@@ -1709,3 +1789,9 @@ def test_tuya_spell_devices_valid():
1709
1789
pytest .fail (
1710
1790
f"{ quirk } has more than one cluster subclassing `TuyaEnchantableCluster` on endpoint 1"
1711
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