Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
f22ee00
Add support for Tuya DPs mapped to multiple cluster attributes
abmantis Dec 25, 2024
0cf12e5
Add test
abmantis Dec 25, 2024
2b63c35
Add Tuya PC321-Z-TY 3 phase power meter
abmantis Dec 25, 2024
d36c1c2
Merge branch 'dev' into tuya_multi_attr
abmantis Jan 2, 2025
af12ce4
Merge branch 'dev' of github.com:zigpy/zha-device-handlers into TS060…
abmantis Jan 6, 2025
98ff595
Merge branch 'dev' of github.com:zigpy/zha-device-handlers into tuya_…
abmantis Jan 17, 2025
32c3fbb
Make dp_to_attribute backwards compatible
abmantis Jan 20, 2025
47bd335
Merge branch 'dev' of github.com:zigpy/zha-device-handlers into tuya_…
abmantis Jan 20, 2025
6142170
Merge branch 'dev' of github.com:zigpy/zha-device-handlers into tuya_…
abmantis Jan 31, 2025
965a3ac
Move _dp_to_attributes to init; add tests
abmantis Jan 31, 2025
5d58046
Merge branch 'dev' of github.com:zigpy/zha-device-handlers into tuya_…
abmantis Feb 6, 2025
581bc0f
Call tuya_dp_multi on tuya_dp to avoid duplicating
abmantis Feb 6, 2025
08eab65
Merge branch 'tuya_multi_attr' of github.com:abmantis/zha-device-hand…
abmantis Feb 8, 2025
3792e0d
Remove uneeded constant attrs
abmantis Feb 8, 2025
2c09161
Merge branch 'dev' of https://github.com/zigpy/zha-device-handlers in…
abmantis Feb 23, 2025
8700364
Add final tweaks and fixes
abmantis Feb 23, 2025
0ad7fad
Add tests for multi_dp converters; fix casing
abmantis Feb 26, 2025
c93a045
Merge branch 'dev' into TS0601_TZE200_nslr42tt
abmantis Mar 7, 2025
99f3b52
Merge branch 'dev' into TS0601_TZE200_nslr42tt
abmantis Mar 19, 2025
b0e7a9c
Add missing negative power tests
abmantis Mar 19, 2025
a8c3034
Use decimal DPs
abmantis Mar 25, 2025
687dbe8
Revert old method names
abmantis Mar 25, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
90 changes: 90 additions & 0 deletions tests/test_tuya_power.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
"""Tests for Tuya quirks."""

import pytest
from zigpy.zcl import foundation

import zhaquirks
import zhaquirks.tuya

zhaquirks.setup()


@pytest.mark.parametrize(
"msg,attr_suffix,expected_power,expected_current,expected_volt",
[
(b"\t0\x02\x00\xd3\x06\x00\x00\x08\tn\x00\nA\x00\x01\xa6", "", 422, 2625, 2414),
(b"\t2\x02\x00Z\x06\x00\x00\x08\ts\x00\n9\x00\x01\xa5", "", 421, 2617, 2419),
(b"\t2\x02\x00Z\x06\x00\x00\x08\ts\x00\n9\x00\x91\xa5", "", -2037, 2617, 2419),
(
b"\t0\x02\x00\xd3\x07\x00\x00\x08\tn\x00\nA\x00\x01\xa6",
"_ph_b",
422,
2625,
2414,
),
(
b"\t2\x02\x00Z\x07\x00\x00\x08\ts\x00\n9\x00\x01\xa5",
"_ph_b",
421,
2617,
2419,
),
(
b"\t0\x02\x00\xd3\x08\x00\x00\x08\tn\x00\nA\x00\x01\xa6",
"_ph_c",
422,
2625,
2414,
),
(
b"\t2\x02\x00Z\x08\x00\x00\x08\ts\x00\n9\x00\x01\xa5",
"_ph_c",
421,
2617,
2419,
),
],
)
async def test_ts0601_electrical_measurement_multi_dp_converter(
zigpy_device_from_v2_quirk,
msg,
attr_suffix,
expected_power,
expected_current,
expected_volt,
):
"""Test converter for multiple electrical attributes mapped to the same tuya datapoint."""

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

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

electrical_meas_cluster = ep.electrical_measurement
assert electrical_meas_cluster.get("active_power" + attr_suffix) == expected_power
assert electrical_meas_cluster.get("rms_current" + attr_suffix) == expected_current
assert electrical_meas_cluster.get("rms_voltage" + attr_suffix) == expected_volt


@pytest.mark.parametrize(
"msg,expected_power",
[
(b"\x19\x8a\x02\x00\x0f\t\x02\x00\x04\x00\x00\x00\x80", 128),
(b"\x19\x8a\x02\x00\x0f\t\x02\x00\x04\x19\x99\x99\x00", -156),
],
)
async def test_ts0601_power_converter(zigpy_device_from_v2_quirk, msg, expected_power):
"""Test converter for power."""

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

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

assert tuya_manufacturer.get("power") == expected_power
218 changes: 218 additions & 0 deletions zhaquirks/tuya/ts0601_power.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,218 @@
"""Tuya Power Meter."""

from collections.abc import ByteString

from zigpy.quirks.v2 import SensorDeviceClass, SensorStateClass
from zigpy.quirks.v2.homeassistant import (
UnitOfElectricCurrent,
UnitOfEnergy,
UnitOfPower,
)
import zigpy.types as t
from zigpy.zcl.clusters.general import LevelControl, OnOff
from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement

from zhaquirks.tuya import DPToAttributeMapping, TuyaLocalCluster
from zhaquirks.tuya.builder import TuyaQuirkBuilder


def dp_to_power(data: ByteString) -> int:
"""Convert DP data to power value."""
# From https://github.com/Koenkk/zigbee2mqtt/issues/18603#issuecomment-2277697295
power = int(data)
if power > 0x0FFFFFFF:
power = (0x1999999C - power) * -1
return power


def multi_dp_to_power(data: ByteString) -> int:
"""Convert DP data to power value."""
# Support negative power readings
# From https://github.com/Koenkk/zigbee2mqtt/issues/18603#issuecomment-2277697295
power = data[7] | (data[6] << 8)
if power > 0x7FFF:
power = (0x999A - power) * -1
return power


def multi_dp_to_current(data: ByteString) -> int:
"""Convert DP data to current value."""
return data[4] | (data[3] << 8)


def multi_dp_to_voltage(data: ByteString) -> int:
"""Convert DP data to voltage value."""
return data[1] | (data[0] << 8)


class Tuya3PhaseElectricalMeasurement(ElectricalMeasurement, TuyaLocalCluster):
"""Tuya Electrical Measurement cluster."""

_CONSTANT_ATTRIBUTES = {
ElectricalMeasurement.AttributeDefs.ac_current_multiplier.id: 1,
ElectricalMeasurement.AttributeDefs.ac_current_divisor.id: 1000,
ElectricalMeasurement.AttributeDefs.ac_voltage_multiplier: 1,
ElectricalMeasurement.AttributeDefs.ac_voltage_divisor.id: 10,
}


(
TuyaQuirkBuilder("_TZE200_nslr42tt", "TS0601")
.tuya_temperature(dp_id=133, scale=10)
.tuya_sensor(
dp_id=134,
attribute_name="device_status",
type=t.int32s,
fallback_name="Device status",
translation_key="device_status",
)
Comment on lines +62 to +68
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What does this sensor show? Since it's only numerical..

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have no ideia, I just added it since it is also on Z2M :/ On mine it is always 0.
Should I remove it?

.tuya_dp(
dp_id=132,
ep_attribute=Tuya3PhaseElectricalMeasurement.ep_attribute,
attribute_name="ac_frequency",
)
# Energy
.tuya_sensor(
dp_id=1,
attribute_name="energy",
type=t.int32s,
divisor=100,
state_class=SensorStateClass.TOTAL,
device_class=SensorDeviceClass.ENERGY,
unit=UnitOfEnergy.KILO_WATT_HOUR,
fallback_name="Total energy",
)
.tuya_sensor(
dp_id=101,
attribute_name="energy_ph_a",
type=t.int32s,
divisor=1000,
state_class=SensorStateClass.TOTAL,
device_class=SensorDeviceClass.ENERGY,
unit=UnitOfEnergy.KILO_WATT_HOUR,
translation_key="energy_ph_a",
fallback_name="Energy phase A",
)
.tuya_sensor(
dp_id=111,
attribute_name="energy_ph_b",
type=t.int32s,
divisor=1000,
state_class=SensorStateClass.TOTAL,
device_class=SensorDeviceClass.ENERGY,
unit=UnitOfEnergy.KILO_WATT_HOUR,
translation_key="energy_ph_b",
fallback_name="Energy phase B",
)
.tuya_sensor(
dp_id=121,
attribute_name="energy_ph_c",
type=t.int32s,
divisor=1000,
state_class=SensorStateClass.TOTAL,
device_class=SensorDeviceClass.ENERGY,
unit=UnitOfEnergy.KILO_WATT_HOUR,
translation_key="energy_ph_c",
fallback_name="Energy phase C",
)
.tuya_sensor(
dp_id=9,
attribute_name="power",
type=t.int32s,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.POWER,
unit=UnitOfPower.WATT,
fallback_name="Total power",
converter=dp_to_power,
)
.tuya_sensor(
dp_id=131,
attribute_name="current",
type=t.int32s,
divisor=1000,
state_class=SensorStateClass.MEASUREMENT,
device_class=SensorDeviceClass.CURRENT,
unit=UnitOfElectricCurrent.AMPERE,
fallback_name="Total current",
)
.tuya_dp_multi(
dp_id=6,
attribute_mapping=[
DPToAttributeMapping(
ep_attribute=Tuya3PhaseElectricalMeasurement.ep_attribute,
attribute_name="active_power",
converter=multi_dp_to_power,
),
DPToAttributeMapping(
ep_attribute=Tuya3PhaseElectricalMeasurement.ep_attribute,
attribute_name="rms_voltage",
converter=multi_dp_to_voltage,
),
DPToAttributeMapping(
ep_attribute=Tuya3PhaseElectricalMeasurement.ep_attribute,
attribute_name="rms_current",
converter=multi_dp_to_current,
),
],
)
.tuya_dp_multi(
dp_id=7,
attribute_mapping=[
DPToAttributeMapping(
ep_attribute=Tuya3PhaseElectricalMeasurement.ep_attribute,
attribute_name="active_power_ph_b",
converter=multi_dp_to_power,
),
DPToAttributeMapping(
ep_attribute=Tuya3PhaseElectricalMeasurement.ep_attribute,
attribute_name="rms_voltage_ph_b",
converter=multi_dp_to_voltage,
),
DPToAttributeMapping(
ep_attribute=Tuya3PhaseElectricalMeasurement.ep_attribute,
attribute_name="rms_current_ph_b",
converter=multi_dp_to_current,
),
],
)
.tuya_dp_multi(
dp_id=8,
attribute_mapping=[
DPToAttributeMapping(
ep_attribute=Tuya3PhaseElectricalMeasurement.ep_attribute,
attribute_name="active_power_ph_c",
converter=multi_dp_to_power,
),
DPToAttributeMapping(
ep_attribute=Tuya3PhaseElectricalMeasurement.ep_attribute,
attribute_name="rms_voltage_ph_c",
converter=multi_dp_to_voltage,
),
DPToAttributeMapping(
ep_attribute=Tuya3PhaseElectricalMeasurement.ep_attribute,
attribute_name="rms_current_ph_c",
converter=multi_dp_to_current,
),
],
)
.tuya_dp(
dp_id=102,
ep_attribute=Tuya3PhaseElectricalMeasurement.ep_attribute,
attribute_name="power_factor",
)
.tuya_dp(
dp_id=112,
ep_attribute=Tuya3PhaseElectricalMeasurement.ep_attribute,
attribute_name="power_factor_ph_b",
)
.tuya_dp(
dp_id=122,
ep_attribute=Tuya3PhaseElectricalMeasurement.ep_attribute,
attribute_name="power_factor_ph_c",
)
.adds(Tuya3PhaseElectricalMeasurement)
.removes(LevelControl.cluster_id)
.removes(OnOff.cluster_id)
Comment on lines +214 to +215
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not sure if these clusters have any use at all, but we can now use prevent_default_entity_creation(endpoint_id=1, cluster_id=OnOff.cluster_id) as well (instead of completely "removing" the clusters).

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

They did not seem to do anything at all. Should prevent_default_entity_creation be used instead for these cases?

.skip_configuration()
.add_to_registry()
)
Loading