11"""Tests for Tuya quirks."""
22
33import pytest
4+ from zigpy .zcl .clusters .general import Basic
45from zigpy .zcl .clusters .homeautomation import ElectricalMeasurement
56from zigpy .zcl .clusters .smartenergy import Metering
67
@@ -42,9 +43,9 @@ async def test_tuya_energy_meter_quirk_energy_direction_align(
4243 FORWARD = 0
4344 REVERSE = 1
4445
45- CH_A = 1
46- CH_B = 2
47- CH_AB = 11
46+ CHANNEL_A = 1
47+ CHANNEL_B = 2
48+ CHANNEL_AB = 11
4849
4950 UNSIGNED_ATTR_SUFFIX = "_attr_unsigned"
5051
@@ -100,7 +101,7 @@ async def test_tuya_energy_meter_quirk_energy_direction_align(
100101 )
101102 assert attr is None
102103
103- if bidirectional and CH_B in channels :
104+ if bidirectional and CHANNEL_B in channels :
104105 # verify the direction B attribute is present
105106 attr = getattr (
106107 ep .tuya_manufacturer .AttributeDefs ,
@@ -115,7 +116,7 @@ async def test_tuya_energy_meter_quirk_energy_direction_align(
115116 assert mcu_listener .attribute_updates [1 ][0 ] == attr .id
116117 assert mcu_listener .attribute_updates [1 ][1 ] == DIRECTION_B
117118
118- if CH_AB in channels :
119+ if CHANNEL_AB in channels :
119120 # verify the config cluster is present
120121 channel_ep = quirked_device .endpoints [1 ]
121122 assert channel_ep .energy_meter_config is not None
@@ -139,11 +140,11 @@ async def test_tuya_energy_meter_quirk_energy_direction_align(
139140 )
140141
141142 for channel in channels :
142- if channel == CH_A :
143+ if channel == CHANNEL_A :
143144 direction = DIRECTION_A
144- elif channel == CH_B :
145+ elif channel == CHANNEL_B :
145146 direction = DIRECTION_B
146- elif channel == CH_AB :
147+ elif channel == CHANNEL_AB :
147148 # updates to channel AB will occur as a result of the device updates to channels A & B
148149 continue
149150 assert direction is not None
@@ -230,40 +231,205 @@ async def test_tuya_energy_meter_quirk_energy_direction_align(
230231 )
231232 assert listeners [channel ]["metering" ].attribute_updates [1 ][1 ] == SUMM_RECEIVED
232233
233- if CH_AB in channels :
234+ if CHANNEL_AB in channels :
234235 # verify the ElectricalMeasurement attributes were updated correctly
235- assert len (listeners [CH_AB ]["electrical_measurement" ].attribute_updates ) == 3
236236 assert (
237- listeners [CH_AB ]["electrical_measurement" ].attribute_updates [0 ][0 ]
237+ len (listeners [CHANNEL_AB ]["electrical_measurement" ].attribute_updates ) == 3
238+ )
239+ assert (
240+ listeners [CHANNEL_AB ]["electrical_measurement" ].attribute_updates [0 ][0 ]
238241 == ElectricalMeasurement .AttributeDefs .rms_current .id
239242 )
240243 assert (
241- listeners [CH_AB ]["electrical_measurement" ].attribute_updates [0 ][1 ]
244+ listeners [CHANNEL_AB ]["electrical_measurement" ].attribute_updates [0 ][1 ]
242245 == - CURRENT + CURRENT # -CURRENT + CURRENT = 0
243246 )
244247 assert (
245- listeners [CH_AB ]["electrical_measurement" ].attribute_updates [1 ][0 ]
248+ listeners [CHANNEL_AB ]["electrical_measurement" ].attribute_updates [1 ][0 ]
246249 == ElectricalMeasurement .AttributeDefs .active_power .id
247250 )
248251 assert (
249- listeners [CH_AB ]["electrical_measurement" ].attribute_updates [1 ][1 ] == 0
252+ listeners [CHANNEL_AB ]["electrical_measurement" ].attribute_updates [1 ][1 ] == 0
250253 ) # -POWER + POWER = 0
251254 assert (
252- listeners [CH_AB ]["electrical_measurement" ].attribute_updates [2 ][0 ]
255+ listeners [CHANNEL_AB ]["electrical_measurement" ].attribute_updates [2 ][0 ]
253256 == ElectricalMeasurement .AttributeDefs .measurement_type .id
254257 )
255258 assert (
256- listeners [CH_AB ]["electrical_measurement" ].attribute_updates [2 ][1 ]
259+ listeners [CHANNEL_AB ]["electrical_measurement" ].attribute_updates [2 ][1 ]
257260 == ElectricalMeasurement .MeasurementType .Active_measurement_AC
258261 | ElectricalMeasurement .MeasurementType .Phase_A_measurement # updated by the _update_measurement_type function
259262 )
260263
261264 # verify the Metering attributes were updated correctly
262- assert len (listeners [CH_AB ]["metering" ].attribute_updates ) == 1
265+ assert len (listeners [CHANNEL_AB ]["metering" ].attribute_updates ) == 1
263266 assert (
264- listeners [CH_AB ]["metering" ].attribute_updates [0 ][0 ]
267+ listeners [CHANNEL_AB ]["metering" ].attribute_updates [0 ][0 ]
265268 == Metering .AttributeDefs .instantaneous_demand .id
266269 )
267270 assert (
268- listeners [CH_AB ]["metering" ].attribute_updates [0 ][1 ] == 0
271+ listeners [CHANNEL_AB ]["metering" ].attribute_updates [0 ][1 ] == 0
269272 ) # -POWER + POWER = 0
273+
274+
275+ @pytest .mark .parametrize (
276+ "model,manuf,mitigation_config,basic_cluster_match" ,
277+ [
278+ ("_TZE204_cjbofhxw" , "TS0601" , 0 , None ), # Automatic
279+ ("_TZE204_ac0fhfiq" , "TS0601" , 0 , None ), # Automatic
280+ ("_TZE200_rks0sgb7" , "TS0601" , 1 , None ), # Disabled
281+ ("_TZE204_81yrt3lo" , "TS0601" , 2 , None ), # Enabled
282+ (
283+ "_TZE204_81yrt3lo" ,
284+ "TS0601" ,
285+ 0 , # Automatic
286+ {
287+ "app_version" : 74 ,
288+ "hw_version" : 1 ,
289+ "stack_version" : 0 ,
290+ },
291+ ),
292+ ],
293+ )
294+ async def test_tuya_energy_meter_quirk_energy_direction_delay_mitigation (
295+ zigpy_device_from_v2_quirk ,
296+ model : str ,
297+ manuf : str ,
298+ mitigation_config : None | int ,
299+ basic_cluster_match : dict ,
300+ ):
301+ """Test Tuya Energy Meter Quirk energy direction report mitigation."""
302+ quirked_device = zigpy_device_from_v2_quirk (model , manuf )
303+
304+ UNSIGNED_ATTR_SUFFIX = "_attr_unsigned"
305+
306+ POWER_1 = 100
307+ POWER_2 = 200
308+ POWER_3 = 300
309+
310+ AUTOMATIC = 0
311+ DISABLED = 1
312+ ENABLED = 2
313+
314+ ep = quirked_device .endpoints [1 ]
315+
316+ # verify the config cluster is present
317+ assert ep .energy_meter_config is not None
318+ assert isinstance (ep .energy_meter_config , LocalDataCluster )
319+
320+ # set the mitigation config value
321+ config_listener = ClusterListener (ep .energy_meter_config )
322+ ep .energy_meter_config .update_attribute (
323+ ep .energy_meter_config .AttributeDefs .energy_direction_mitigation .id ,
324+ mitigation_config ,
325+ )
326+ assert len (config_listener .attribute_updates ) == 1
327+ assert (
328+ config_listener .attribute_updates [0 ][0 ]
329+ == ep .energy_meter_config .AttributeDefs .energy_direction_mitigation .id
330+ )
331+ assert config_listener .attribute_updates [0 ][1 ] == mitigation_config
332+
333+ if basic_cluster_match :
334+ # verify the basic cluster is present
335+ assert ep .basic is not None
336+ assert isinstance (ep .basic , Basic )
337+
338+ # populate match details for automatic mitigation
339+ basic_listener = ClusterListener (ep .basic )
340+ ep .basic .update_attribute (
341+ Basic .AttributeDefs .app_version .id ,
342+ basic_cluster_match ["app_version" ],
343+ )
344+ ep .basic .update_attribute (
345+ Basic .AttributeDefs .hw_version .id ,
346+ basic_cluster_match ["hw_version" ],
347+ )
348+ ep .basic .update_attribute (
349+ Basic .AttributeDefs .stack_version .id ,
350+ basic_cluster_match ["stack_version" ],
351+ )
352+ assert len (basic_listener .attribute_updates ) == 3
353+ assert (
354+ basic_listener .attribute_updates [0 ][0 ] == Basic .AttributeDefs .app_version .id
355+ )
356+ assert (
357+ basic_listener .attribute_updates [0 ][1 ] == basic_cluster_match ["app_version" ]
358+ )
359+ assert (
360+ basic_listener .attribute_updates [1 ][0 ] == Basic .AttributeDefs .hw_version .id
361+ )
362+ assert (
363+ basic_listener .attribute_updates [1 ][1 ] == basic_cluster_match ["hw_version" ]
364+ )
365+ assert (
366+ basic_listener .attribute_updates [2 ][0 ]
367+ == Basic .AttributeDefs .stack_version .id
368+ )
369+ assert (
370+ basic_listener .attribute_updates [2 ][1 ]
371+ == basic_cluster_match ["stack_version" ]
372+ )
373+
374+ # verify the reporting cluster is present
375+ assert ep .smartenergy_metering is not None
376+ assert isinstance (ep .smartenergy_metering , Metering )
377+
378+ # update the reporting cluster
379+ metering_listener = ClusterListener (ep .smartenergy_metering )
380+ ep .smartenergy_metering .update_attribute (
381+ Metering .AttributeDefs .instantaneous_demand .name + UNSIGNED_ATTR_SUFFIX ,
382+ POWER_1 ,
383+ )
384+ ep .smartenergy_metering .update_attribute (
385+ Metering .AttributeDefs .instantaneous_demand .name + UNSIGNED_ATTR_SUFFIX ,
386+ POWER_2 ,
387+ )
388+ ep .smartenergy_metering .update_attribute (
389+ Metering .AttributeDefs .instantaneous_demand .name + UNSIGNED_ATTR_SUFFIX ,
390+ POWER_3 ,
391+ )
392+
393+ # cluster values are delayed until their next update when the mitigation is active
394+ assert (
395+ len (metering_listener .attribute_updates ) == 3
396+ if mitigation_config == DISABLED
397+ or mitigation_config == AUTOMATIC
398+ and not basic_cluster_match
399+ else 2
400+ )
401+
402+ assert (
403+ metering_listener .attribute_updates [0 ][0 ]
404+ == Metering .AttributeDefs .instantaneous_demand .id
405+ )
406+ assert (
407+ metering_listener .attribute_updates [0 ][1 ] == POWER_1
408+ if mitigation_config == DISABLED
409+ or mitigation_config == AUTOMATIC
410+ and not basic_cluster_match
411+ else None
412+ )
413+
414+ assert (
415+ metering_listener .attribute_updates [1 ][0 ]
416+ == Metering .AttributeDefs .instantaneous_demand .id
417+ )
418+ assert (
419+ metering_listener .attribute_updates [1 ][1 ] == POWER_2
420+ if mitigation_config == DISABLED
421+ or mitigation_config == AUTOMATIC
422+ and not basic_cluster_match
423+ else POWER_1
424+ )
425+
426+ if (
427+ mitigation_config == ENABLED
428+ or mitigation_config == AUTOMATIC
429+ and basic_cluster_match
430+ ):
431+ assert (
432+ metering_listener .attribute_updates [2 ][0 ]
433+ == Metering .AttributeDefs .instantaneous_demand .id
434+ )
435+ assert metering_listener .attribute_updates [2 ][1 ] == POWER_2
0 commit comments