Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add Tuya TRV _TZE204_ogx8u5z6 #3682

Merged
merged 11 commits into from
Jan 16, 2025
86 changes: 86 additions & 0 deletions tests/test_tuya_trv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
"""Test for Tuya TRV."""

from unittest import mock

import pytest
import zigpy.types as t
from zigpy.zcl import foundation
from zigpy.zcl.clusters.hvac import Thermostat

from tests.common import ClusterListener, wait_for_zigpy_tasks
import zhaquirks
from zhaquirks.tuya.mcu import TuyaMCUCluster

zhaquirks.setup()


@pytest.mark.parametrize(
"msg,attr,value",
[
(
b"\t\xc2\x02\x00q\x02\x04\x00\x01\x00",
Thermostat.AttributeDefs.system_mode,
Thermostat.SystemMode.Auto,
), # Set to Auto (0x00), dp 2
(
b"\t\xc3\x02\x00r\x02\x04\x00\x01\x01",
Thermostat.AttributeDefs.system_mode,
Thermostat.SystemMode.Heat,
), # Set to Heat (0x01), dp 2
(
b"\t\xc2\x02\x00q\x02\x04\x00\x01\x02",
Thermostat.AttributeDefs.system_mode,
Thermostat.SystemMode.Off,
), # Set to Off (0x02), dp 2
],
)
async def test_handle_get_data(zigpy_device_from_v2_quirk, msg, attr, value):
"""Test handle_get_data for multiple attributes."""

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

assert ep.tuya_manufacturer is not None
assert isinstance(ep.tuya_manufacturer, TuyaMCUCluster)

assert ep.thermostat is not None
assert isinstance(ep.thermostat, Thermostat)

thermostat_listener = ClusterListener(ep.thermostat)

hdr, data = ep.tuya_manufacturer.deserialize(msg)
status = ep.tuya_manufacturer.handle_get_data(data.data)
assert status == foundation.Status.SUCCESS

assert len(thermostat_listener.attribute_updates) == 1
assert thermostat_listener.attribute_updates[0][0] == attr.id
assert thermostat_listener.attribute_updates[0][1] == value

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

async def async_success(*args, **kwargs):
return foundation.Status.SUCCESS

with mock.patch.object(
ep.tuya_manufacturer.endpoint, "request", side_effect=async_success
) as m1:
(status,) = await ep.thermostat.write_attributes(
{
"occupied_heating_setpoint": 2500,
}
)
await wait_for_zigpy_tasks()
m1.assert_called_with(
cluster=0xEF00,
sequence=1,
data=b"\x01\x01\x00\x00\x01\x04\x02\x00\x04\x00\x00\x00\xfa",
command_id=0,
timeout=5,
expect_reply=False,
use_ieee=False,
ask_for_ack=None,
priority=t.PacketPriority.NORMAL,
)
assert status == [
foundation.WriteAttributesStatusRecord(foundation.Status.SUCCESS)
]
111 changes: 111 additions & 0 deletions zhaquirks/tuya/ts0601_trv.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,8 @@
TuyaThermostatCluster,
TuyaUserInterfaceCluster,
)
from zhaquirks.tuya.builder import TuyaQuirkBuilder
from zhaquirks.tuya.mcu import TuyaAttributesCluster

# info from https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/converters/common.js#L113
# and https://github.com/Koenkk/zigbee-herdsman-converters/blob/master/converters/fromZigbee.js#L362
Expand All @@ -50,6 +52,42 @@
_LOGGER = logging.getLogger(__name__)


class TuyaThermostatSystemMode(t.enum8):
"""Tuya thermostat system mode enum."""

Auto = 0x00
Heat = 0x01
Off = 0x02


class TuyaThermostatV2(Thermostat, TuyaAttributesCluster):
"""Tuya local thermostat cluster."""

manufacturer_id_override: t.uint16_t = foundation.ZCLHeader.NO_MANUFACTURER_ID

_CONSTANT_ATTRIBUTES = {
Thermostat.AttributeDefs.ctrl_sequence_of_oper.id: Thermostat.ControlSequenceOfOperation.Heating_Only
}

def __init__(self, *args, **kwargs):
"""Init a TuyaThermostat cluster."""
super().__init__(*args, **kwargs)
self.add_unsupported_attribute(
Thermostat.AttributeDefs.setpoint_change_source.id
)
self.add_unsupported_attribute(
Thermostat.AttributeDefs.setpoint_change_source_timestamp.id
)
self.add_unsupported_attribute(Thermostat.AttributeDefs.pi_heating_demand.id)

async def write_attributes(self, attributes, manufacturer=None):
"""Overwrite to force manufacturer code."""

return await super().write_attributes(
attributes, manufacturer=foundation.ZCLHeader.NO_MANUFACTURER_ID
)


class SiterwellManufCluster(TuyaManufClusterAttributes):
"""Manufacturer Specific Cluster of some thermostatic valves."""

Expand Down Expand Up @@ -1751,3 +1789,76 @@ def __init__(self, *args, **kwargs):
},
}
}


(
TuyaQuirkBuilder("_TZE204_ogx8u5z6", "TS0601")
.tuya_dp(
dp_id=2,
ep_attribute=TuyaThermostatV2.ep_attribute,
attribute_name=TuyaThermostatV2.AttributeDefs.system_mode.name,
converter=lambda x: {
TuyaThermostatSystemMode.Auto: Thermostat.SystemMode.Auto,
TuyaThermostatSystemMode.Heat: Thermostat.SystemMode.Heat,
TuyaThermostatSystemMode.Off: Thermostat.SystemMode.Off,
}[x],
dp_converter=lambda x: {
Thermostat.SystemMode.Auto: TuyaThermostatSystemMode.Auto,
Thermostat.SystemMode.Heat: TuyaThermostatSystemMode.Heat,
Thermostat.SystemMode.Off: TuyaThermostatSystemMode.Off,
}[x],
)
.tuya_dp(
dp_id=3,
ep_attribute=TuyaThermostatV2.ep_attribute,
attribute_name=TuyaThermostatV2.AttributeDefs.running_state.name,
converter=lambda x: 0x01 if not x else 0x00, # Heat, Idle
)
.tuya_dp(
dp_id=4,
ep_attribute=TuyaThermostatV2.ep_attribute,
attribute_name=TuyaThermostatV2.AttributeDefs.occupied_heating_setpoint.name,
converter=lambda x: x * 10,
dp_converter=lambda x: x // 10,
)
.tuya_dp(
dp_id=5,
ep_attribute=TuyaThermostatV2.ep_attribute,
attribute_name=TuyaThermostatV2.AttributeDefs.local_temperature.name,
converter=lambda x: x * 10,
)
.tuya_dp(
dp_id=47,
ep_attribute=TuyaThermostatV2.ep_attribute,
attribute_name=TuyaThermostatV2.AttributeDefs.local_temperature_calibration.name,
converter=lambda x: x,
dp_converter=lambda x: x + 0x100000000 if x < 0 else x,
)
.tuya_switch(
dp_id=7,
attribute_name="child_lock",
translation_key="child_lock",
fallback_name="Child lock",
)
.tuya_switch(
dp_id=35,
attribute_name="frost_protection",
translation_key="frost_protection",
fallback_name="Frost protection",
)
.tuya_switch(
dp_id=39,
attribute_name="scale_protection",
translation_key="scale_protection",
fallback_name="Scale protection",
)
.tuya_binary_sensor(
dp_id=35,
attribute_name="error_or_battery_low",
translation_key="error_or_battery_low",
fallback_name="Error or battery low",
)
.adds(TuyaThermostatV2)
.skip_configuration()
.add_to_registry()
)
Loading