| 
 | 1 | +"""Tests for Tuya quirks."""  | 
 | 2 | + | 
 | 3 | +import pytest  | 
 | 4 | +from zigpy.zcl.clusters.homeautomation import ElectricalMeasurement  | 
 | 5 | +from zigpy.zcl.clusters.smartenergy import Metering  | 
 | 6 | + | 
 | 7 | +from tests.common import ClusterListener  | 
 | 8 | +import zhaquirks  | 
 | 9 | +from zhaquirks import LocalDataCluster  | 
 | 10 | +import zhaquirks.tuya  | 
 | 11 | +from zhaquirks.tuya.mcu import TuyaMCUCluster  | 
 | 12 | + | 
 | 13 | +zhaquirks.setup()  | 
 | 14 | + | 
 | 15 | + | 
 | 16 | +@pytest.mark.parametrize(  | 
 | 17 | +    "model,manuf,channels,bidirectional",  | 
 | 18 | +    [  | 
 | 19 | +        (  | 
 | 20 | +            "_TZE204_cjbofhxw",  | 
 | 21 | +            "TS0601",  | 
 | 22 | +            {1},  | 
 | 23 | +            False,  | 
 | 24 | +        ),  | 
 | 25 | +        ("_TZE204_ac0fhfiq", "TS0601", {1}, True),  | 
 | 26 | +        ("_TZE200_rks0sgb7", "TS0601", {1, 2, 11}, True),  | 
 | 27 | +        ("_TZE204_81yrt3lo", "TS0601", {1, 2, 11}, True),  | 
 | 28 | +    ],  | 
 | 29 | +)  | 
 | 30 | +async def test_tuya_energy_meter_quirk_energy_direction_align(  | 
 | 31 | +    zigpy_device_from_v2_quirk,  | 
 | 32 | +    model: str,  | 
 | 33 | +    manuf: str,  | 
 | 34 | +    channels,  | 
 | 35 | +    bidirectional: bool,  | 
 | 36 | +):  | 
 | 37 | +    """Test Tuya Energy Meter Quirk energy direction align in ElectricalMeasurement and Metering clusters."""  | 
 | 38 | +    quirked_device = zigpy_device_from_v2_quirk(model, manuf)  | 
 | 39 | + | 
 | 40 | +    ENERGY_DIRECTION_ATTR = "energy_direction"  | 
 | 41 | +    ENERGY_DIRECTION_ATTR_B = "energy_direction_ch_b"  | 
 | 42 | +    FORWARD = 0  | 
 | 43 | +    REVERSE = 1  | 
 | 44 | + | 
 | 45 | +    CH_A = 1  | 
 | 46 | +    CH_B = 2  | 
 | 47 | +    CH_AB = 11  | 
 | 48 | + | 
 | 49 | +    UNSIGNED_ATTR_SUFFIX = "_attr_unsigned"  | 
 | 50 | + | 
 | 51 | +    CURRENT = 5  | 
 | 52 | +    POWER = 100  | 
 | 53 | +    VOLTAGE = 230  | 
 | 54 | +    SUMM_RECEIVED = 15000  | 
 | 55 | +    DIRECTION_A = REVERSE  | 
 | 56 | +    DIRECTION_B = FORWARD  | 
 | 57 | + | 
 | 58 | +    ep = quirked_device.endpoints[1]  | 
 | 59 | + | 
 | 60 | +    assert ep.tuya_manufacturer is not None  | 
 | 61 | +    assert isinstance(ep.tuya_manufacturer, TuyaMCUCluster)  | 
 | 62 | +    mcu_listener = ClusterListener(ep.tuya_manufacturer)  | 
 | 63 | + | 
 | 64 | +    listeners = {}  | 
 | 65 | +    for channel in channels:  | 
 | 66 | +        channel_ep = quirked_device.endpoints.get(channel, None)  | 
 | 67 | +        assert channel_ep is not None  | 
 | 68 | + | 
 | 69 | +        assert channel_ep.electrical_measurement is not None  | 
 | 70 | +        assert isinstance(channel_ep.electrical_measurement, ElectricalMeasurement)  | 
 | 71 | + | 
 | 72 | +        assert channel_ep.smartenergy_metering is not None  | 
 | 73 | +        assert isinstance(channel_ep.smartenergy_metering, Metering)  | 
 | 74 | + | 
 | 75 | +        listeners[channel] = {  | 
 | 76 | +            "metering": ClusterListener(channel_ep.smartenergy_metering),  | 
 | 77 | +            "electrical_measurement": ClusterListener(  | 
 | 78 | +                channel_ep.electrical_measurement  | 
 | 79 | +            ),  | 
 | 80 | +        }  | 
 | 81 | + | 
 | 82 | +    if bidirectional:  | 
 | 83 | +        # verify the direction attribute is present  | 
 | 84 | +        attr = getattr(ep.tuya_manufacturer.AttributeDefs, ENERGY_DIRECTION_ATTR, None)  | 
 | 85 | +        assert attr is not None  | 
 | 86 | + | 
 | 87 | +        # set the initial direction  | 
 | 88 | +        ep.tuya_manufacturer.update_attribute(ENERGY_DIRECTION_ATTR, DIRECTION_A)  | 
 | 89 | +        assert len(mcu_listener.attribute_updates) == 1  | 
 | 90 | +        assert mcu_listener.attribute_updates[0][0] == attr.id  | 
 | 91 | +        assert mcu_listener.attribute_updates[0][1] == DIRECTION_A  | 
 | 92 | +    else:  | 
 | 93 | +        # verify the direction & direction B attributes are not present  | 
 | 94 | +        attr = getattr(ep.tuya_manufacturer.AttributeDefs, ENERGY_DIRECTION_ATTR, None)  | 
 | 95 | +        assert attr is None  | 
 | 96 | +        attr = getattr(  | 
 | 97 | +            ep.tuya_manufacturer.AttributeDefs,  | 
 | 98 | +            ENERGY_DIRECTION_ATTR_B,  | 
 | 99 | +            None,  | 
 | 100 | +        )  | 
 | 101 | +        assert attr is None  | 
 | 102 | + | 
 | 103 | +    if bidirectional and CH_B in channels:  | 
 | 104 | +        # verify the direction B attribute is present  | 
 | 105 | +        attr = getattr(  | 
 | 106 | +            ep.tuya_manufacturer.AttributeDefs,  | 
 | 107 | +            ENERGY_DIRECTION_ATTR_B,  | 
 | 108 | +            None,  | 
 | 109 | +        )  | 
 | 110 | +        assert attr is not None  | 
 | 111 | + | 
 | 112 | +        # set the initial direction  | 
 | 113 | +        ep.tuya_manufacturer.update_attribute(ENERGY_DIRECTION_ATTR_B, DIRECTION_B)  | 
 | 114 | +        assert len(mcu_listener.attribute_updates) == 2  | 
 | 115 | +        assert mcu_listener.attribute_updates[1][0] == attr.id  | 
 | 116 | +        assert mcu_listener.attribute_updates[1][1] == DIRECTION_B  | 
 | 117 | + | 
 | 118 | +    if CH_AB in channels:  | 
 | 119 | +        # verify the config cluster is present  | 
 | 120 | +        channel_ep = quirked_device.endpoints[1]  | 
 | 121 | +        assert channel_ep.energy_meter_config is not None  | 
 | 122 | +        assert isinstance(channel_ep.energy_meter_config, LocalDataCluster)  | 
 | 123 | + | 
 | 124 | +        config_listener = ClusterListener(ep.energy_meter_config)  | 
 | 125 | + | 
 | 126 | +        # set the initial virtual channel calculation method (sum A and B)  | 
 | 127 | +        channel_ep.energy_meter_config.update_attribute(  | 
 | 128 | +            channel_ep.energy_meter_config.AttributeDefs.virtual_channel_config.id,  | 
 | 129 | +            channel_ep.energy_meter_config.VirtualChannelConfig.A_plus_B,  | 
 | 130 | +        )  | 
 | 131 | +        assert len(config_listener.attribute_updates) == 1  | 
 | 132 | +        assert (  | 
 | 133 | +            config_listener.attribute_updates[0][0]  | 
 | 134 | +            == channel_ep.energy_meter_config.AttributeDefs.virtual_channel_config.id  | 
 | 135 | +        )  | 
 | 136 | +        assert (  | 
 | 137 | +            config_listener.attribute_updates[0][1]  | 
 | 138 | +            == channel_ep.energy_meter_config.VirtualChannelConfig.A_plus_B  | 
 | 139 | +        )  | 
 | 140 | + | 
 | 141 | +    for channel in channels:  | 
 | 142 | +        if channel == CH_A:  | 
 | 143 | +            direction = DIRECTION_A  | 
 | 144 | +        elif channel == CH_B:  | 
 | 145 | +            direction = DIRECTION_B  | 
 | 146 | +        elif channel == CH_AB:  | 
 | 147 | +            # updates to channel AB will occur as a result of the device updates to channels A & B  | 
 | 148 | +            continue  | 
 | 149 | +        assert direction is not None  | 
 | 150 | + | 
 | 151 | +        channel_ep = quirked_device.endpoints[channel]  | 
 | 152 | + | 
 | 153 | +        # update ElectricalMeasurement attributes  | 
 | 154 | +        channel_ep.electrical_measurement.update_attribute(  | 
 | 155 | +            ElectricalMeasurement.AttributeDefs.rms_current.name, CURRENT  | 
 | 156 | +        )  | 
 | 157 | +        channel_ep.electrical_measurement.update_attribute(  | 
 | 158 | +            ElectricalMeasurement.AttributeDefs.rms_voltage.name, VOLTAGE  | 
 | 159 | +        )  | 
 | 160 | +        channel_ep.electrical_measurement.update_attribute(  | 
 | 161 | +            # The UNSIGNED_ATTR_SUFFIX applies energy direction on bidirectional devices  | 
 | 162 | +            ElectricalMeasurement.AttributeDefs.active_power.name  | 
 | 163 | +            + UNSIGNED_ATTR_SUFFIX,  | 
 | 164 | +            POWER,  | 
 | 165 | +        )  | 
 | 166 | + | 
 | 167 | +        # verify the ElectricalMeasurement attributes were updated correctly  | 
 | 168 | +        assert len(listeners[channel]["electrical_measurement"].attribute_updates) == 4  | 
 | 169 | +        assert (  | 
 | 170 | +            listeners[channel]["electrical_measurement"].attribute_updates[0][0]  | 
 | 171 | +            == ElectricalMeasurement.AttributeDefs.rms_current.id  | 
 | 172 | +        )  | 
 | 173 | +        assert (  | 
 | 174 | +            listeners[channel]["electrical_measurement"].attribute_updates[0][1]  | 
 | 175 | +            == CURRENT  | 
 | 176 | +        )  | 
 | 177 | +        assert (  | 
 | 178 | +            listeners[channel]["electrical_measurement"].attribute_updates[1][0]  | 
 | 179 | +            == ElectricalMeasurement.AttributeDefs.rms_voltage.id  | 
 | 180 | +        )  | 
 | 181 | +        assert (  | 
 | 182 | +            listeners[channel]["electrical_measurement"].attribute_updates[1][1]  | 
 | 183 | +            == VOLTAGE  | 
 | 184 | +        )  | 
 | 185 | +        assert (  | 
 | 186 | +            listeners[channel]["electrical_measurement"].attribute_updates[2][0]  | 
 | 187 | +            == ElectricalMeasurement.AttributeDefs.active_power.id  | 
 | 188 | +        )  | 
 | 189 | +        assert (  | 
 | 190 | +            listeners[channel]["electrical_measurement"].attribute_updates[2][1]  | 
 | 191 | +            == POWER  | 
 | 192 | +            if not bidirectional or direction == FORWARD  | 
 | 193 | +            else -POWER  | 
 | 194 | +        )  | 
 | 195 | +        assert (  | 
 | 196 | +            listeners[channel]["electrical_measurement"].attribute_updates[3][0]  | 
 | 197 | +            == ElectricalMeasurement.AttributeDefs.measurement_type.id  | 
 | 198 | +        )  | 
 | 199 | +        assert (  | 
 | 200 | +            listeners[channel]["electrical_measurement"].attribute_updates[3][1]  | 
 | 201 | +            == ElectricalMeasurement.MeasurementType.Active_measurement_AC  | 
 | 202 | +            | ElectricalMeasurement.MeasurementType.Phase_A_measurement  # updated by the _update_measurement_type function  | 
 | 203 | +        )  | 
 | 204 | + | 
 | 205 | +        # update Metering attributes  | 
 | 206 | +        channel_ep.smartenergy_metering.update_attribute(  | 
 | 207 | +            Metering.AttributeDefs.instantaneous_demand.name + UNSIGNED_ATTR_SUFFIX,  | 
 | 208 | +            POWER,  | 
 | 209 | +        )  | 
 | 210 | +        channel_ep.smartenergy_metering.update_attribute(  | 
 | 211 | +            # The UNSIGNED_ATTR_SUFFIX applies energy direction on bidirectional devices  | 
 | 212 | +            Metering.AttributeDefs.current_summ_received.name,  | 
 | 213 | +            SUMM_RECEIVED,  | 
 | 214 | +        )  | 
 | 215 | + | 
 | 216 | +        # verify the Metering attributes were updated correctly  | 
 | 217 | +        assert len(listeners[channel]["metering"].attribute_updates) == 2  | 
 | 218 | +        assert (  | 
 | 219 | +            listeners[channel]["metering"].attribute_updates[0][0]  | 
 | 220 | +            == Metering.AttributeDefs.instantaneous_demand.id  | 
 | 221 | +        )  | 
 | 222 | +        assert (  | 
 | 223 | +            listeners[channel]["metering"].attribute_updates[0][1] == POWER  | 
 | 224 | +            if not bidirectional or direction == FORWARD  | 
 | 225 | +            else -POWER  | 
 | 226 | +        )  | 
 | 227 | +        assert (  | 
 | 228 | +            listeners[channel]["metering"].attribute_updates[1][0]  | 
 | 229 | +            == Metering.AttributeDefs.current_summ_received.id  | 
 | 230 | +        )  | 
 | 231 | +        assert listeners[channel]["metering"].attribute_updates[1][1] == SUMM_RECEIVED  | 
 | 232 | + | 
 | 233 | +    if CH_AB in channels:  | 
 | 234 | +        # verify the ElectricalMeasurement attributes were updated correctly  | 
 | 235 | +        assert len(listeners[CH_AB]["electrical_measurement"].attribute_updates) == 3  | 
 | 236 | +        assert (  | 
 | 237 | +            listeners[CH_AB]["electrical_measurement"].attribute_updates[0][0]  | 
 | 238 | +            == ElectricalMeasurement.AttributeDefs.rms_current.name  | 
 | 239 | +        )  | 
 | 240 | +        assert (  | 
 | 241 | +            listeners[CH_AB]["electrical_measurement"].attribute_updates[0][1]  | 
 | 242 | +            == -CURRENT + CURRENT  # -CURRENT + CURRENT = 0  | 
 | 243 | +        )  | 
 | 244 | +        assert (  | 
 | 245 | +            listeners[CH_AB]["electrical_measurement"].attribute_updates[1][0]  | 
 | 246 | +            == ElectricalMeasurement.AttributeDefs.active_power.name  | 
 | 247 | +        )  | 
 | 248 | +        assert (  | 
 | 249 | +            listeners[CH_AB]["electrical_measurement"].attribute_updates[1][1] == 0  | 
 | 250 | +        )  # -POWER + POWER = 0  | 
 | 251 | +        assert (  | 
 | 252 | +            listeners[CH_AB]["electrical_measurement"].attribute_updates[2][0]  | 
 | 253 | +            == ElectricalMeasurement.AttributeDefs.measurement_type.name  | 
 | 254 | +        )  | 
 | 255 | +        assert (  | 
 | 256 | +            listeners[CH_AB]["electrical_measurement"].attribute_updates[2][1]  | 
 | 257 | +            == ElectricalMeasurement.MeasurementType.Active_measurement_AC  | 
 | 258 | +            | ElectricalMeasurement.MeasurementType.Phase_A_measurement  # updated by the _update_measurement_type function  | 
 | 259 | +        )  | 
 | 260 | + | 
 | 261 | +        # verify the Metering attributes were updated correctly  | 
 | 262 | +        assert len(listeners[CH_AB]["metering"].attribute_updates) == 2  | 
 | 263 | +        assert (  | 
 | 264 | +            listeners[CH_AB]["metering"].attribute_updates[0][0]  | 
 | 265 | +            == Metering.AttributeDefs.instantaneous_demand.name  | 
 | 266 | +        )  | 
 | 267 | +        assert (  | 
 | 268 | +            listeners[CH_AB]["metering"].attribute_updates[0][1] == 0  | 
 | 269 | +        )  # -POWER + POWER = 0  | 
 | 270 | +        assert (  | 
 | 271 | +            listeners[CH_AB]["metering"].attribute_updates[1][0]  | 
 | 272 | +            == Metering.AttributeDefs.current_summ_received.name  | 
 | 273 | +        )  | 
 | 274 | +        assert listeners[CH_AB]["metering"].attribute_updates[1][1] == SUMM_RECEIVED  | 
0 commit comments