Skip to content

Commit dfab1cc

Browse files
Add support for Tuya TS1201 IR blaster (#2336)
Co-authored-by: TheJulianJES <[email protected]>
1 parent dca24aa commit dfab1cc

File tree

2 files changed

+878
-0
lines changed

2 files changed

+878
-0
lines changed

tests/test_tuya.py

Lines changed: 313 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
"""Tests for Tuya quirks."""
22

33
import asyncio
4+
import base64
45
import datetime
6+
import struct
57
from unittest import mock
68

79
import pytest
@@ -40,6 +42,7 @@
4042
import zhaquirks.tuya.ts0601_trv
4143
import zhaquirks.tuya.ts0601_valve
4244
import zhaquirks.tuya.ts601_door
45+
import zhaquirks.tuya.ts1201
4346

4447
zhaquirks.setup()
4548

@@ -1662,6 +1665,316 @@ async def test_power_config_no_bind(zigpy_device_from_quirk, quirk):
16621665
assert len(bind_mock.mock_calls) == 0
16631666

16641667

1668+
def test_ts1201_signature(assert_signature_matches_quirk):
1669+
"""Test TS1201 remote signature is matched to its quirk."""
1670+
signature = {
1671+
"node_descriptor": "NodeDescriptor(logical_type=<LogicalType.EndDevice: 2>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress: 128>, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=True, *is_full_function_device=False, *is_mains_powered=False, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False)",
1672+
"endpoints": {
1673+
"1": {
1674+
"profile_id": 260,
1675+
"device_type": "0xf000",
1676+
"in_clusters": [
1677+
"0x0000",
1678+
"0x0001",
1679+
"0x0003",
1680+
"0x0004",
1681+
"0x0005",
1682+
"0x0006",
1683+
"0xe004",
1684+
"0xed00",
1685+
],
1686+
"out_clusters": ["0x000a", "0x0019"],
1687+
}
1688+
},
1689+
"manufacturer": "_TZ3290_ot6ewjvmejq5ekhl",
1690+
"model": "TS1201",
1691+
"class": "zhaquirks.tuya.ts1201.ZosungIRBlaster",
1692+
}
1693+
assert_signature_matches_quirk(zhaquirks.tuya.ts1201.ZosungIRBlaster, signature)
1694+
1695+
1696+
@pytest.mark.parametrize("test_bytes", (b"\x00\x01\x02\x03\x04",))
1697+
def test_ts1201_ir_blaster_bytes(test_bytes):
1698+
"""Test quirk Byte helper class."""
1699+
a, b = zhaquirks.tuya.ts1201.Bytes.deserialize(data=test_bytes)
1700+
assert a == test_bytes
1701+
assert b == b""
1702+
1703+
1704+
async def test_ts1201_ir_blaster(zigpy_device_from_quirk):
1705+
"""Test Tuya TS1201 IR blaster."""
1706+
quirk = zhaquirks.tuya.ts1201.ZosungIRBlaster
1707+
1708+
part_max_length = 0x38
1709+
1710+
ir_code_to_learn = "A/AESQFAAwUIAvAErwHgAQNADwXwBK8BrwFABcADAXYfwAkBrwHACeABBwXwBK8BrwFABcAD4GgvAgRJAQ=="
1711+
ir_code_to_learn_bytes = base64.b64decode(ir_code_to_learn)
1712+
ir_code_to_learn_part1 = ir_code_to_learn_bytes[: part_max_length - 1]
1713+
crc1 = 0
1714+
for x in ir_code_to_learn_part1:
1715+
crc1 = (crc1 + x) % 0x100
1716+
ir_code_to_learn_part2 = ir_code_to_learn_bytes[part_max_length - 1 :]
1717+
crc2 = 0
1718+
for x in ir_code_to_learn_part2:
1719+
crc2 = (crc2 + x) % 0x100
1720+
1721+
# TV power off/on code
1722+
ir_code_to_send = "B3wPfA/5AcoH4AUDAeUDgAPAC+AHB+AHA+ADN+ALBw==" # codespell:ignore
1723+
ir_msg = (
1724+
f'{{"key_num":1,"delay":300,"key1":{{'
1725+
f'"num":1,"freq":38000,"type":1,"key_code":"{ir_code_to_send}"}}}}'
1726+
)
1727+
ir_msg_length = len(ir_msg)
1728+
position = 0
1729+
control_cluster_id = 57348
1730+
transmit_cluster_id = 60672
1731+
1732+
ts1201_dev = zigpy_device_from_quirk(quirk)
1733+
ts1201_control_cluster = ts1201_dev.endpoints[1].zosung_ircontrol
1734+
ts1201_transmit_cluster = ts1201_dev.endpoints[1].zosung_irtransmit
1735+
ts1201_transmit_listener = ClusterListener(ts1201_transmit_cluster)
1736+
1737+
with mock.patch.object(
1738+
ts1201_control_cluster.endpoint,
1739+
"request",
1740+
return_value=foundation.Status.SUCCESS,
1741+
) as m1:
1742+
# study mode on
1743+
rsp = await ts1201_control_cluster.command(0x0001, on_off=True)
1744+
await wait_for_zigpy_tasks()
1745+
m1.assert_called_with(
1746+
control_cluster_id,
1747+
1,
1748+
b"\x01\x01\x00" + b'{"study":0}',
1749+
expect_reply=True,
1750+
command_id=0,
1751+
)
1752+
assert rsp == foundation.Status.SUCCESS
1753+
1754+
# simulate receive_ir_frame_00 (first frame when device sends a learned code)
1755+
hdr, args = ts1201_transmit_cluster.deserialize(
1756+
b"\x01k\x00\x01\x00=\x00\x00\x00\x00\x00\x00\x00\x04\xe0\x01\x04\x00\x00"
1757+
)
1758+
ts1201_transmit_cluster.handle_message(hdr, args)
1759+
await wait_for_zigpy_tasks()
1760+
m1.assert_called_with(
1761+
transmit_cluster_id,
1762+
3,
1763+
b"\x01\x03\x02\x01\x00"
1764+
+ struct.pack("<L", position)
1765+
+ struct.pack("<B", part_max_length),
1766+
expect_reply=True,
1767+
command_id=2,
1768+
)
1769+
assert (
1770+
ts1201_transmit_listener.cluster_commands[0][2].command.name
1771+
== "receive_ir_frame_00"
1772+
)
1773+
assert ts1201_transmit_listener.cluster_commands[0][2].length == 61
1774+
assert (
1775+
ts1201_transmit_listener.cluster_commands[0][2].clusterid
1776+
== control_cluster_id
1777+
)
1778+
assert ts1201_transmit_listener.cluster_commands[0][2].cmd == 0x04
1779+
1780+
# simulate receive_ir_frame_01
1781+
position += part_max_length - 1
1782+
hdr, args = ts1201_transmit_cluster.deserialize(
1783+
b"\tl\x03\x00\x01\x00\x00\x00\x00\x007\x03\xf0\x04I\x01@\x03\x05\x08\x02"
1784+
b"\xf0\x04\xaf\x01\xe0\x01\x03@\x0f\x05\xf0\x04\xaf\x01\xaf\x01@\x05\xc0"
1785+
b"\x03\x01v\x1f\xc0\t\x01\xaf\x01\xc0\t\xe0\x01\x07\x05\xf0\x04\xaf\x01"
1786+
b"\xaf\x01@\x05\xc0\x03\xe0\xcd"
1787+
)
1788+
ts1201_transmit_cluster.handle_message(hdr, args)
1789+
await wait_for_zigpy_tasks()
1790+
m1.assert_called_with(
1791+
transmit_cluster_id,
1792+
4,
1793+
b"\x01\x04\x02\x01\x00"
1794+
+ struct.pack("<L", position)
1795+
+ struct.pack("<B", part_max_length),
1796+
expect_reply=False,
1797+
command_id=2,
1798+
)
1799+
assert (
1800+
ts1201_transmit_listener.cluster_commands[1][2].command.name
1801+
== "resp_ir_frame_03"
1802+
)
1803+
assert ts1201_transmit_listener.cluster_commands[1][2].position == 0
1804+
assert (
1805+
ts1201_transmit_listener.cluster_commands[1][2].msgpart
1806+
== ir_code_to_learn_part1
1807+
)
1808+
assert ts1201_transmit_listener.cluster_commands[1][2].msgpartcrc == crc1
1809+
1810+
# simulate second receive_ir_frame_01
1811+
position += part_max_length - 1
1812+
hdr, args = ts1201_transmit_cluster.deserialize(
1813+
b"\tm\x03\x00\x01\x007\x00\x00\x00\x06h/\x02\x04I\x01\xe7"
1814+
)
1815+
ts1201_transmit_cluster.handle_message(hdr, args)
1816+
await wait_for_zigpy_tasks()
1817+
m1.assert_called_with(
1818+
transmit_cluster_id,
1819+
5,
1820+
b"\x01\x05\x04\x00\x01\x00\x00\x00",
1821+
expect_reply=False,
1822+
command_id=4,
1823+
)
1824+
assert (
1825+
ts1201_transmit_listener.cluster_commands[2][2].command.name
1826+
== "resp_ir_frame_03"
1827+
)
1828+
assert (
1829+
ts1201_transmit_listener.cluster_commands[2][2].position
1830+
== position - part_max_length + 1
1831+
)
1832+
assert (
1833+
ts1201_transmit_listener.cluster_commands[2][2].msgpart
1834+
== ir_code_to_learn_part2
1835+
)
1836+
assert ts1201_transmit_listener.cluster_commands[2][2].msgpartcrc == crc2
1837+
1838+
# simulate last receive_ir_frame_01
1839+
hdr, args = ts1201_transmit_cluster.deserialize(b"\tn\x05\x01\x00\x00\x00")
1840+
ts1201_transmit_cluster.handle_message(hdr, args)
1841+
await wait_for_zigpy_tasks()
1842+
m1.assert_called_with(
1843+
control_cluster_id,
1844+
6,
1845+
b'\x01\x06\x00{"study":1}',
1846+
expect_reply=True,
1847+
command_id=0,
1848+
)
1849+
assert (
1850+
ts1201_transmit_listener.cluster_commands[3][2].command.name
1851+
== "resp_ir_frame_05"
1852+
)
1853+
1854+
# should return learned IR code
1855+
succ, fail = await ts1201_control_cluster.read_attributes(
1856+
("last_learned_ir_code",)
1857+
)
1858+
assert succ[0] == ir_code_to_learn
1859+
1860+
# test unknown attribute
1861+
succ, fail = await ts1201_control_cluster.read_attributes(
1862+
("another_attribute",)
1863+
)
1864+
assert fail[0] == foundation.Status.UNSUPPORTED_ATTRIBUTE
1865+
1866+
# IR send tests
1867+
await ts1201_control_cluster.command(0x0002, code=ir_code_to_send)
1868+
await wait_for_zigpy_tasks()
1869+
# IR send must call ir transmit command id 0x00
1870+
m1.assert_called_with(
1871+
transmit_cluster_id,
1872+
7,
1873+
b"\x01\x07\x00\x01\x00"
1874+
+ struct.pack("<I", ir_msg_length)
1875+
+ b"\x00\x00\x00\x00"
1876+
+ struct.pack("<H", control_cluster_id)
1877+
+ b"\x01\x02\x00\x00",
1878+
expect_reply=False,
1879+
command_id=0,
1880+
)
1881+
1882+
# simulate receive_ir_frame_00
1883+
hdr, args = ts1201_transmit_cluster.deserialize(
1884+
b"\x05\x02\x10\x01\x00\x01\x00z\x00\x00\x00\x00\x00\x00\x00\x04\xe0\x01\x02\x00\x00"
1885+
)
1886+
ts1201_transmit_cluster.handle_message(hdr, args)
1887+
await wait_for_zigpy_tasks()
1888+
m1.assert_called_with(
1889+
transmit_cluster_id,
1890+
9,
1891+
b"\x01\x09\x02\x01\x00\x00\x00\x00\x00"
1892+
+ struct.pack("<B", part_max_length),
1893+
expect_reply=True,
1894+
command_id=2,
1895+
)
1896+
assert (
1897+
ts1201_transmit_listener.cluster_commands[4][2].command.name
1898+
== "receive_ir_frame_00"
1899+
)
1900+
assert ts1201_transmit_listener.cluster_commands[4][2].length == ir_msg_length
1901+
assert (
1902+
ts1201_transmit_listener.cluster_commands[4][2].clusterid
1903+
== control_cluster_id
1904+
)
1905+
assert ts1201_transmit_listener.cluster_commands[4][2].cmd == 2
1906+
1907+
# simulate receive_ir_frame_01
1908+
hdr, args = ts1201_transmit_cluster.deserialize(
1909+
b"\x01f\x01\x00\x01\x00z\x00\x00\x00\x00\x00\x00\x00\x04\xe0\x01\x02\x00\x00"
1910+
)
1911+
ts1201_transmit_cluster.handle_message(hdr, args)
1912+
assert (
1913+
ts1201_transmit_listener.cluster_commands[5][2].command.name
1914+
== "receive_ir_frame_01"
1915+
)
1916+
assert ts1201_transmit_listener.cluster_commands[5][2].length == ir_msg_length
1917+
assert (
1918+
ts1201_transmit_listener.cluster_commands[5][2].clusterid
1919+
== control_cluster_id
1920+
)
1921+
assert ts1201_transmit_listener.cluster_commands[5][2].cmd == 2
1922+
1923+
# simulate receive_ir_frame_02
1924+
hdr, args = ts1201_transmit_cluster.deserialize(
1925+
b"\x11g\x02\x01\x00\x00\x00\x00\x00@"
1926+
)
1927+
ts1201_transmit_cluster.handle_message(hdr, args)
1928+
assert (
1929+
ts1201_transmit_listener.cluster_commands[6][2].command.name
1930+
== "receive_ir_frame_02"
1931+
)
1932+
assert ts1201_transmit_listener.cluster_commands[6][2].position == 0
1933+
assert ts1201_transmit_listener.cluster_commands[6][2].maxlen == 64
1934+
1935+
# simulate receive_ir_frame_04
1936+
hdr, args = ts1201_transmit_cluster.deserialize(
1937+
b"\x01i\x04\x00\x01\x00\x00\x00"
1938+
)
1939+
ts1201_transmit_cluster.handle_message(hdr, args)
1940+
await wait_for_zigpy_tasks()
1941+
m1.assert_called_with(
1942+
transmit_cluster_id,
1943+
11,
1944+
b"\x01\x0b\x05\x01\x00\x00\x00",
1945+
expect_reply=False,
1946+
command_id=5,
1947+
)
1948+
assert (
1949+
ts1201_transmit_listener.cluster_commands[7][2].command.name
1950+
== "receive_ir_frame_04"
1951+
)
1952+
1953+
# test raw data command
1954+
rsp = await ts1201_control_cluster.command(
1955+
0x0000, zhaquirks.tuya.ts1201.Bytes(b"\x00\x01\x02\x03\x04")
1956+
)
1957+
await wait_for_zigpy_tasks()
1958+
m1.assert_called_with(
1959+
control_cluster_id,
1960+
12,
1961+
b"\x01\x0c\x00\x00\x01\x02\x03\x04",
1962+
expect_reply=True,
1963+
command_id=0,
1964+
)
1965+
assert rsp == foundation.Status.SUCCESS
1966+
1967+
# test unknown request from device
1968+
hdr, args = ts1201_transmit_cluster.deserialize(
1969+
b"\x110\x06\x01\x00\x00\x00\x00\x00"
1970+
)
1971+
ts1201_transmit_cluster.handle_message(hdr, args)
1972+
assert (
1973+
ts1201_transmit_listener.cluster_commands[8][2]
1974+
== b"\x01\x00\x00\x00\x00\x00"
1975+
)
1976+
1977+
16651978
def test_ts601_door_sensor_signature(assert_signature_matches_quirk):
16661979
"""Test TS601 Vibration Door Sensor signature against quirk."""
16671980
signature = {

0 commit comments

Comments
 (0)