Skip to content

Commit

Permalink
Merge branch 'dev' into add-rain-_TZ3210_tgvtvdoc
Browse files Browse the repository at this point in the history
  • Loading branch information
prairiesnpr authored Jan 27, 2025
2 parents ca7a3d5 + 2f1dd9b commit 9b2f241
Show file tree
Hide file tree
Showing 24 changed files with 1,735 additions and 290 deletions.
8 changes: 8 additions & 0 deletions .github/ISSUE_TEMPLATE/bug_report.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
name: Bug report
description: Report a bug in an existing quirk or a general issue with the project.
title: "[BUG] "
labels: ["possible bug"]
type: "bug"
body:
- type: markdown
attributes:
Expand Down Expand Up @@ -64,7 +66,9 @@ body:
<details><summary>Device signature</summary>
```json
[Paste the device signature here]
```
</details>
Expand All @@ -79,7 +83,9 @@ body:
<details><summary>Diagnostic information</summary>
```json
[Paste the diagnostic information here]
```
</details>
Expand All @@ -99,7 +105,9 @@ body:
<details><summary>Logs</summary>
```python
[Paste the logs here]
```
</details>
Expand Down
10 changes: 10 additions & 0 deletions .github/ISSUE_TEMPLATE/device_support_request.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
name: Device support request
description: Request support for an unsupported device or a missing device feature.
title: "[Device Support Request] "
labels: ["device support request"]
type: "feature"
body:
- type: markdown
attributes:
Expand Down Expand Up @@ -53,7 +55,9 @@ body:
<details><summary>Device signature</summary>
```json
[Paste the device signature here]
```
</details>
Expand All @@ -68,7 +72,9 @@ body:
<details><summary>Diagnostic information</summary>
```json
[Paste the diagnostic information here]
```
</details>
Expand All @@ -88,7 +94,9 @@ body:
<details><summary>Logs</summary>
```python
[Paste the logs here]
```
</details>
Expand All @@ -101,7 +109,9 @@ body:
<details><summary>Custom quirk</summary>
```python
[Paste your custom quirk here]
```
</details>
Expand Down
2 changes: 1 addition & 1 deletion tests/test_tuya.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,10 @@
import zhaquirks.tuya.ts011f_plug
import zhaquirks.tuya.ts0501_fan_switch
import zhaquirks.tuya.ts0601_electric_heating
import zhaquirks.tuya.ts0601_motion
import zhaquirks.tuya.ts0601_trv
import zhaquirks.tuya.ts601_door
import zhaquirks.tuya.ts1201
import zhaquirks.tuya.tuya_motion
import zhaquirks.tuya.tuya_valve

zhaquirks.setup()
Expand Down
31 changes: 31 additions & 0 deletions tests/test_tuya_builder.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,3 +364,34 @@ async def test_tuya_mcu_set_time(device_mock):
assert not res_hdr[0].frame_control.is_manufacturer_specific

datetime.datetime = origdatetime # restore datetime


@pytest.mark.parametrize(
"force",
[
(False),
(True),
],
)
async def test_tuya_quirkbuilder_force(device_mock, force):
"""Test adding an empty TuyaQuirkBuilder doesn't add a MCU cluster unless forced to."""

registry = DeviceRegistry()

(
TuyaQuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry)
.skip_configuration()
.add_to_registry(force_add_cluster=force)
)

quirked = registry.get_device(device_mock)
assert isinstance(quirked, CustomDeviceV2)
assert quirked in registry

ep = quirked.endpoints[1]

if force:
assert ep.tuya_manufacturer is not None
assert isinstance(ep.tuya_manufacturer, TuyaMCUCluster)
else:
assert not hasattr(ep, "tuya_manufacturer")
38 changes: 23 additions & 15 deletions tests/test_tuya_sensor.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@
from zhaquirks.tuya import TuyaLocalCluster
from zhaquirks.tuya.mcu import TuyaMCUCluster

# Temp DP 1, Humidity DP 2, Battery DP 3
TUYA_TEMP01_HUM02_BAT03 = b"\x09\xe0\x02\x0b\x33\x01\x02\x00\x04\x00\x00\x00\xfd\x02\x02\x00\x04\x00\x00\x00\x47\x03\x02\x00\x04\x00\x00\x00\x01"
# Temp DP 1, Humidity DP 2, Battery DP 4
TUYA_TEMP01_HUM02_BAT04 = b"\x09\xe0\x02\x0b\x33\x01\x02\x00\x04\x00\x00\x00\xfd\x02\x02\x00\x04\x00\x00\x00\x47\x04\x02\x00\x04\x00\x00\x00\x01"
TUYA_USP = b"\x09\xe0\x02\x0b\x33\x01\x02\x00\x04\x00\x00\x00\xfd\x02\x02\x00\x04\x00\x00\x00\x47\xff\x02\x00\x04\x00\x00\x00\x64"

ZCL_TUYA_VERSION_RSP = b"\x09\x06\x11\x01\x6d\x82"

zhaquirks.setup()


Expand Down Expand Up @@ -36,6 +44,7 @@
("_TZE200_eanjj2pa", "TS0601", 100, 10, False),
("_TZE200_ydrdfkim", "TS0601", 100, 10, False),
("_TZE284_locansqn", "TS0601", 100, 10, False),
("_TZE200_vvmbj46n", "TS0601", 100, 10, True),
],
)
async def test_handle_get_data(
Expand Down Expand Up @@ -73,8 +82,7 @@ async def test_handle_get_data(
== data.data.datapoints[2].data.payload * 2
)

message = b"\x09\xe0\x02\x0b\x33\x01\x02\x00\x04\x00\x00\x00\xfd\x02\x02\x00\x04\x00\x00\x00\x47\xff\x02\x00\x04\x00\x00\x00\x64"
hdr, data = ep.tuya_manufacturer.deserialize(message)
hdr, data = ep.tuya_manufacturer.deserialize(TUYA_USP)

status = ep.tuya_manufacturer.handle_get_data(data.data)
assert status == foundation.Status.UNSUPPORTED_ATTRIBUTE
Expand All @@ -90,19 +98,21 @@ async def test_handle_get_data(


@pytest.mark.parametrize(
"model,manuf,rh_scale,temp_scale",
"model,manuf,rh_scale,temp_scale,state_rpt",
[
("_TZE200_yjjdcqsq", "TS0601", 100, 10),
("_TZE200_9yapgbuv", "TS0601", 100, 10),
("_TZE204_yjjdcqsq", "TS0601", 100, 10),
("_TZE200_utkemkbs", "TS0601", 100, 10),
("_TZE204_utkemkbs", "TS0601", 100, 10),
("_TZE204_yjjdcqsq", "TS0601", 100, 10),
("_TZE204_ksz749x8", "TS0601", 100, 10),
("_TZE200_yjjdcqsq", "TS0601", 100, 10, TUYA_TEMP01_HUM02_BAT04),
("_TZE200_9yapgbuv", "TS0601", 100, 10, TUYA_TEMP01_HUM02_BAT04),
("_TZE204_yjjdcqsq", "TS0601", 100, 10, TUYA_TEMP01_HUM02_BAT04),
("_TZE200_utkemkbs", "TS0601", 100, 10, TUYA_TEMP01_HUM02_BAT04),
("_TZE204_utkemkbs", "TS0601", 100, 10, TUYA_TEMP01_HUM02_BAT04),
("_TZE204_yjjdcqsq", "TS0601", 100, 10, TUYA_TEMP01_HUM02_BAT04),
("_TZE204_ksz749x8", "TS0601", 100, 10, TUYA_TEMP01_HUM02_BAT04),
("_TZE204_upagmta9", "TS0601", 100, 10, TUYA_TEMP01_HUM02_BAT03),
("_TZE204_upagmta9", "TS0601", 100, 10, TUYA_TEMP01_HUM02_BAT03),
],
)
async def test_handle_get_data_enum_batt(
zigpy_device_from_v2_quirk, model, manuf, rh_scale, temp_scale
zigpy_device_from_v2_quirk, model, manuf, rh_scale, temp_scale, state_rpt
):
"""Test handle_get_data for multiple attributes - enum battery."""

Expand All @@ -115,8 +125,7 @@ async def test_handle_get_data_enum_batt(
assert ep.tuya_manufacturer is not None
assert isinstance(ep.tuya_manufacturer, TuyaMCUCluster)

message = b"\x09\xe0\x02\x0b\x33\x01\x02\x00\x04\x00\x00\x00\xfd\x02\x02\x00\x04\x00\x00\x00\x47\x04\x02\x00\x04\x00\x00\x00\x01"
hdr, data = ep.tuya_manufacturer.deserialize(message)
hdr, data = ep.tuya_manufacturer.deserialize(state_rpt)

status = ep.tuya_manufacturer.handle_get_data(data.data)

Expand All @@ -134,8 +143,7 @@ async def test_handle_get_data_enum_batt(

assert ep.power.get("battery_percentage_remaining") == 100

message = b"\x09\xe0\x02\x0b\x33\x01\x02\x00\x04\x00\x00\x00\xfd\x02\x02\x00\x04\x00\x00\x00\x47\xff\x02\x00\x04\x00\x00\x00\x64"
hdr, data = ep.tuya_manufacturer.deserialize(message)
hdr, data = ep.tuya_manufacturer.deserialize(TUYA_USP)

status = ep.tuya_manufacturer.handle_get_data(data.data)
assert status == foundation.Status.UNSUPPORTED_ATTRIBUTE
Expand Down
58 changes: 55 additions & 3 deletions tests/test_tuya_thermostat.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,50 +6,84 @@

from tests.common import ClusterListener
import zhaquirks
from zhaquirks.tuya import TUYA_MCU_VERSION_RSP
from zhaquirks.tuya.mcu import TuyaMCUCluster

zhaquirks.setup()

ZCL_TUYA_VERSION_RSP = b"\x09\x06\x11\x01\x6d\x82"
ZCL_TUYA_SET_TIME = b"\x09\x12\x24\x0d\x00"


@pytest.mark.parametrize(
"msg,attr,value",
"manuf,msg,attr,value",
[
(
"_TZE204_p3lqqy2r",
b"\t\x13\x02\x00\x06\x01\x01\x00\x01\x01",
Thermostat.AttributeDefs.system_mode,
Thermostat.SystemMode.Heat,
), # Set to heat, dp 1
(
"_TZE204_p3lqqy2r",
b"\t\x16\x02\x00\t\x18\x02\x00\x04\x00\x00\x00\x18",
Thermostat.AttributeDefs.local_temperature,
2400,
), # Current temp 24, dp 24
(
"_TZE204_p3lqqy2r",
b"\t\x15\x02\x00\x08\x10\x02\x00\x04\x00\x00\x00\x19",
Thermostat.AttributeDefs.occupied_heating_setpoint,
2500,
), # Setpoint to 25, dp 16
(
"_TZE204_p3lqqy2r",
b"\t\x17\x02\x00\n\x1c\x02\x00\x04\x00\x00\x00\x00",
Thermostat.AttributeDefs.local_temperature_calibration,
0,
), # Local calibration to 0, dp 28
(
"_TZE204_p3lqqy2r",
b"\t\x1c\x02\x00\x0fh\x01\x00\x01\x01",
Thermostat.AttributeDefs.running_state,
Thermostat.RunningState.Heat_State_On,
), # Running state, dp 104
(
"_TZE204_p3lqqy2r",
b"\t\x1d\x02\x00\x10k\x02\x00\x04\x00\x00\x00\x1b",
Thermostat.AttributeDefs.max_heat_setpoint_limit,
2700,
), # Max heat set point, dp 107
(
"_TZE204_lzriup1j",
b"\t\x13\x02\x00\x06\x01\x01\x00\x01\x01",
Thermostat.AttributeDefs.system_mode,
Thermostat.SystemMode.Heat,
), # Set to heat, dp 1
(
"_TZE200_viy9ihs7",
b"\t\x13\x02\x00\x06\x01\x01\x00\x01\x01",
Thermostat.AttributeDefs.system_mode,
Thermostat.SystemMode.Heat,
), # Set to heat, dp 1
(
"_TZE204_xnbkhhdr",
b"\t\x13\x02\x00\x06\x01\x01\x00\x01\x01",
Thermostat.AttributeDefs.system_mode,
Thermostat.SystemMode.Heat,
), # Set to heat, dp 1
(
"_TZE284_xnbkhhdr",
b"\t\x13\x02\x00\x06\x01\x01\x00\x01\x01",
Thermostat.AttributeDefs.system_mode,
Thermostat.SystemMode.Heat,
), # Set to heat, dp 1
],
)
async def test_handle_get_data(zigpy_device_from_v2_quirk, msg, attr, value):
async def test_handle_get_data(zigpy_device_from_v2_quirk, manuf, msg, attr, value):
"""Test handle_get_data for multiple attributes."""

quirked = zigpy_device_from_v2_quirk("_TZE204_p3lqqy2r", "TS0601")
quirked = zigpy_device_from_v2_quirk(manuf, "TS0601")
ep = quirked.endpoints[1]

assert ep.tuya_manufacturer is not None
Expand All @@ -69,3 +103,21 @@ async def test_handle_get_data(zigpy_device_from_v2_quirk, msg, attr, value):
assert thermostat_listener.attribute_updates[0][1] == value

assert ep.thermostat.get(attr.id) == value


async def test_tuya_no_mcu_version(zigpy_device_from_v2_quirk):
"""Test lack of TUYA_MCU_VERSION_RSP messages."""

tuya_device = zigpy_device_from_v2_quirk("_TZE284_xnbkhhdr", "TS0601")

tuya_cluster = tuya_device.endpoints[1].tuya_manufacturer
cluster_listener = ClusterListener(tuya_cluster)

assert len(cluster_listener.attribute_updates) == 0

# simulate a TUYA_MCU_VERSION_RSP message
hdr, args = tuya_cluster.deserialize(ZCL_TUYA_VERSION_RSP)
assert hdr.command_id == TUYA_MCU_VERSION_RSP

tuya_cluster.handle_message(hdr, args)
assert len(cluster_listener.attribute_updates) == 0
52 changes: 52 additions & 0 deletions zhaquirks/ikea/vallhorn.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
"""IKEA Vallhorn quirk."""

from typing import Final

from zigpy import types as t
from zigpy.quirks import CustomCluster
from zigpy.quirks.v2 import QuirkBuilder
from zigpy.quirks.v2.homeassistant import UnitOfTime
from zigpy.zcl.foundation import BaseAttributeDefs, ZCLAttributeDef


class IkeaVallhornManufSpecificConfig(CustomCluster):
"""Ikea Vallhorn manufacturer specific config cluster."""

name = "IKEA manufacturer specific config"
cluster_id = 0xFC81

class AttributeDefs(BaseAttributeDefs):
"""Attribute definitions."""

on_only_when_dark: Final = ZCLAttributeDef(
id=0x0000,
type=t.Bool,
)

on_time: Final = ZCLAttributeDef(
id=0x0002,
type=t.uint16_t,
)


(
QuirkBuilder("IKEA of Sweden", "VALLHORN Wireless Motion Sensor")
.replaces(IkeaVallhornManufSpecificConfig)
.switch(
IkeaVallhornManufSpecificConfig.AttributeDefs.on_only_when_dark.name,
IkeaVallhornManufSpecificConfig.cluster_id,
translation_key="on_only_when_dark",
fallback_name="On only when dark",
)
.number(
IkeaVallhornManufSpecificConfig.AttributeDefs.on_time.name,
IkeaVallhornManufSpecificConfig.cluster_id,
step=1,
min_value=10,
max_value=65534,
unit=UnitOfTime.SECONDS,
translation_key="on_time",
fallback_name="On time",
)
.add_to_registry()
)
Loading

0 comments on commit 9b2f241

Please sign in to comment.