Skip to content

Commit 3d0c1f6

Browse files
authored
Merge branch 'dev' into add-thermostat-_TZE200_viy9ihs7
2 parents 3edb6e8 + c82ffed commit 3d0c1f6

File tree

12 files changed

+619
-227
lines changed

12 files changed

+619
-227
lines changed

tests/test_tuya.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,9 @@
3737
import zhaquirks.tuya.ts0601_electric_heating
3838
import zhaquirks.tuya.ts0601_motion
3939
import zhaquirks.tuya.ts0601_trv
40-
import zhaquirks.tuya.ts0601_valve
4140
import zhaquirks.tuya.ts601_door
4241
import zhaquirks.tuya.ts1201
42+
import zhaquirks.tuya.tuya_valve
4343

4444
zhaquirks.setup()
4545

tests/test_tuya_builder.py

Lines changed: 61 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
"""Tests for TuyaQuirkBuilder."""
22

3+
import datetime
34
from unittest import mock
45

56
import pytest
@@ -9,10 +10,11 @@
910
from zigpy.zcl import foundation
1011
from zigpy.zcl.clusters.general import Basic, BatterySize
1112

12-
from tests.common import ClusterListener, wait_for_zigpy_tasks
13+
from tests.common import ClusterListener, MockDatetime, wait_for_zigpy_tasks
1314
import zhaquirks
1415
from zhaquirks.tuya import (
1516
TUYA_QUERY_DATA,
17+
TUYA_SET_TIME,
1618
TuyaPowerConfigurationCluster,
1719
TuyaPowerConfigurationCluster2AAA,
1820
)
@@ -23,14 +25,18 @@
2325
TuyaIasContact,
2426
TuyaIasFire,
2527
TuyaIasGas,
28+
TuyaIlluminance,
2629
TuyaPM25Concentration,
2730
TuyaQuirkBuilder,
2831
TuyaRelativeHumidity,
2932
TuyaSoilMoisture,
3033
TuyaTemperatureMeasurement,
31-
TuyaValveWaterConsumed,
34+
TuyaValveWaterConsumedNoInstDemand,
3235
)
3336
from zhaquirks.tuya.mcu import TuyaMCUCluster, TuyaOnOffNM
37+
from zhaquirks.tuya.tuya_sensor import NoManufTimeTuyaMCUCluster
38+
39+
ZCL_TUYA_SET_TIME = b"\x09\x12\x24\x0d\x00"
3440

3541
zhaquirks.setup()
3642

@@ -39,7 +45,7 @@
3945
"method_name,attr_name,exp_class",
4046
[
4147
("tuya_battery", "power", TuyaPowerConfigurationCluster),
42-
("tuya_metering", "smartenergy_metering", TuyaValveWaterConsumed),
48+
("tuya_metering", "smartenergy_metering", TuyaValveWaterConsumedNoInstDemand),
4349
("tuya_onoff", "on_off", TuyaOnOffNM),
4450
("tuya_soil_moisture", "soil_moisture", TuyaSoilMoisture),
4551
("tuya_temperature", "temperature", TuyaTemperatureMeasurement),
@@ -55,6 +61,7 @@
5561
TuyaFormaldehydeConcentration,
5662
),
5763
("tuya_gas", "ias_zone", TuyaIasGas),
64+
("tuya_illuminance", "illuminance", TuyaIlluminance),
5865
],
5966
)
6067
async def test_convenience_methods(device_mock, method_name, attr_name, exp_class):
@@ -140,6 +147,9 @@ class TestEnum(t.enum8):
140147
A = 0x00
141148
B = 0x01
142149

150+
class ModTuyaMCUCluster(TuyaMCUCluster):
151+
"""Modified Cluster."""
152+
143153
entry = (
144154
TuyaQuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry)
145155
.tuya_battery(dp_id=1)
@@ -178,7 +188,7 @@ class TestEnum(t.enum8):
178188
fallback_name="Test enum",
179189
)
180190
.skip_configuration()
181-
.add_to_registry()
191+
.add_to_registry(replacement_cluster=ModTuyaMCUCluster)
182192
)
183193

184194
# coverage for overridden __eq__ method
@@ -195,6 +205,7 @@ class TestEnum(t.enum8):
195205
assert isinstance(ep.basic, Basic)
196206

197207
assert ep.tuya_manufacturer is not None
208+
assert isinstance(ep.tuya_manufacturer, ModTuyaMCUCluster)
198209
assert isinstance(ep.tuya_manufacturer, TuyaMCUCluster)
199210

200211
tuya_cluster = ep.tuya_manufacturer
@@ -303,3 +314,49 @@ async def test_tuya_spell(device_mock, read_attr_spell, data_query_spell):
303314
messages += 1
304315

305316
request_mock.reset_mock()
317+
318+
319+
async def test_tuya_mcu_set_time(device_mock):
320+
"""Test TuyaQuirkBuilder replacement cluster, set_time requests (0x24) messages for MCU devices."""
321+
322+
registry = DeviceRegistry()
323+
324+
(
325+
TuyaQuirkBuilder(device_mock.manufacturer, device_mock.model, registry=registry)
326+
.tuya_battery(dp_id=1)
327+
.skip_configuration()
328+
.add_to_registry(replacement_cluster=NoManufTimeTuyaMCUCluster)
329+
)
330+
331+
quirked = registry.get_device(device_mock)
332+
assert isinstance(quirked, CustomDeviceV2)
333+
assert quirked in registry
334+
335+
ep = quirked.endpoints[1]
336+
337+
assert not ep.tuya_manufacturer._is_manuf_specific
338+
assert not ep.tuya_manufacturer.server_commands[
339+
TUYA_SET_TIME
340+
].is_manufacturer_specific
341+
342+
# Mock datetime
343+
origdatetime = datetime.datetime
344+
datetime.datetime = MockDatetime
345+
346+
# simulate a SET_TIME message
347+
hdr, args = ep.tuya_manufacturer.deserialize(ZCL_TUYA_SET_TIME)
348+
assert hdr.command_id == TUYA_SET_TIME
349+
350+
with mock.patch.object(
351+
ep.tuya_manufacturer._endpoint,
352+
"request",
353+
return_value=foundation.Status.SUCCESS,
354+
) as m1:
355+
ep.tuya_manufacturer.handle_message(hdr, args)
356+
await wait_for_zigpy_tasks()
357+
358+
res_hdr = foundation.ZCLHeader.deserialize(m1.await_args[1]["data"])
359+
assert not res_hdr[0].manufacturer
360+
assert not res_hdr[0].frame_control.is_manufacturer_specific
361+
362+
datetime.datetime = origdatetime # restore datetime

tests/test_tuya_sensor.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,14 @@
2828
("_TZE200_bq5c8xfe", "TS0601", 100, 10, True),
2929
("_TZE200_vs0skpuc", "TS0601", 100, 10, True),
3030
("_TZE200_44af8vyi", "TS0601", 100, 10, True),
31+
("_TZE200_lve3dvpy", "TS0601", 100, 10, False), # TH01Z - Temp & humid w/ clock
32+
("_TZE200_c7emyjom", "TS0601", 100, 10, False),
33+
("_TZE200_locansqn", "TS0601", 100, 10, False),
34+
("_TZE200_qrztc3ev", "TS0601", 100, 10, False),
35+
("_TZE200_snloy4rw", "TS0601", 100, 10, False),
36+
("_TZE200_eanjj2pa", "TS0601", 100, 10, False),
37+
("_TZE200_ydrdfkim", "TS0601", 100, 10, False),
38+
("_TZE284_locansqn", "TS0601", 100, 10, False),
3139
],
3240
)
3341
async def test_handle_get_data(

tests/test_tuya_spells.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
TuyaNewManufCluster,
2323
TuyaZBOnOffAttributeCluster,
2424
)
25-
import zhaquirks.tuya.ts0601_valve
25+
import zhaquirks.tuya.tuya_valve
2626

2727
zhaquirks.setup()
2828

tests/test_tuya_valve.py

Lines changed: 7 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -173,15 +173,15 @@ async def test_giex_02_quirk(zigpy_device_from_v2_quirk, model, manuf, use_minut
173173
][entity]
174174

175175
if not use_minutes:
176-
assert number_metadata.max == zhaquirks.tuya.ts0601_valve.GIEX_12HRS_AS_SEC
176+
assert number_metadata.max == zhaquirks.tuya.tuya_valve.GIEX_12HRS_AS_SEC
177177
else:
178-
assert number_metadata.max == zhaquirks.tuya.ts0601_valve.GIEX_24HRS_AS_MIN
178+
assert number_metadata.max == zhaquirks.tuya.tuya_valve.GIEX_24HRS_AS_MIN
179179

180180

181181
async def test_giex_functions():
182182
"""Test various Giex Valve functions."""
183-
assert zhaquirks.tuya.ts0601_valve.giex_string_to_td("12:01:05,3") == 43265
184-
assert zhaquirks.tuya.ts0601_valve.giex_string_to_ts("--:--:--") is None
183+
assert zhaquirks.tuya.tuya_valve.giex_string_to_td("12:01:05,3") == 43265
184+
assert zhaquirks.tuya.tuya_valve.giex_string_to_ts("--:--:--") is None
185185

186186
class MockDatetime:
187187
def now(self, tz: timezone):
@@ -192,11 +192,11 @@ def strptime(self, v: str, fmt: str):
192192
"""Mock strptime."""
193193
return datetime.strptime(v, fmt)
194194

195-
with patch("zhaquirks.tuya.ts0601_valve.datetime", MockDatetime()):
195+
with patch("zhaquirks.tuya.tuya_valve.datetime", MockDatetime()):
196196
assert (
197-
zhaquirks.tuya.ts0601_valve.giex_string_to_ts("20:12:01")
197+
zhaquirks.tuya.tuya_valve.giex_string_to_ts("20:12:01")
198198
== datetime.fromisoformat("2024-10-02T12:10:23+04:00").timestamp()
199-
+ zhaquirks.tuya.ts0601_valve.UNIX_EPOCH_TO_ZCL_EPOCH
199+
+ zhaquirks.tuya.tuya_valve.UNIX_EPOCH_TO_ZCL_EPOCH
200200
)
201201

202202

tuya.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -70,7 +70,7 @@ Adds a battery power cluster.
7070
.tuya_battery(dp_id=2, battery_type=BatterySize.AA, battery_qty=4)
7171
```
7272

73-
#### tuya_metering(dp_id: int, metering_cfg: TuyaLocalCluster = TuyaValveWaterConsumed)
73+
#### tuya_metering(dp_id: int, metering_cfg: TuyaLocalCluster = TuyaValveWaterConsumedNoInstDemand)
7474

7575
Adds a metering cluster.
7676

zhaquirks/schneiderelectric/thermostat.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
from zigpy.quirks import CustomCluster
66
from zigpy.quirks.v2 import EntityType, QuirkBuilder
77
from zigpy.quirks.v2.homeassistant import (
8+
PERCENTAGE,
89
EntityPlatform,
910
UnitOfPower,
1011
UnitOfTemperature,
@@ -597,7 +598,7 @@ class AttributeDefs(CustomCluster.AttributeDefs):
597598
attribute_name=SEUserInterface.AttributeDefs.se_brightness.name,
598599
translation_key="display_brightness",
599600
fallback_name="Display brightness",
600-
# unit="%",
601+
unit=PERCENTAGE,
601602
min_value=0,
602603
max_value=100,
603604
step=1,
@@ -608,7 +609,7 @@ class AttributeDefs(CustomCluster.AttributeDefs):
608609
attribute_name=SEUserInterface.AttributeDefs.se_inactive_brightness.name,
609610
translation_key="display_inactive_brightness",
610611
fallback_name="Display inactive brightness",
611-
# unit="%",
612+
unit=PERCENTAGE,
612613
min_value=0,
613614
max_value=100,
614615
step=1,

zhaquirks/tuya/builder/__init__.py

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
from collections.abc import Callable
44
from enum import Enum
5+
import math
56
from typing import Any, Optional
67

78
from zigpy.quirks import _DEVICE_REGISTRY
@@ -18,6 +19,7 @@
1819
PM25,
1920
CarbonDioxideConcentration,
2021
FormaldehydeConcentration,
22+
IlluminanceMeasurement,
2123
RelativeHumidity,
2224
SoilMoisture,
2325
TemperatureMeasurement,
@@ -115,8 +117,12 @@ class TuyaValveWaterConsumed(Metering, TuyaLocalCluster):
115117
Metering.AttributeDefs.metering_device_type.id: WATER_METERING,
116118
}
117119

120+
121+
class TuyaValveWaterConsumedNoInstDemand(TuyaValveWaterConsumed):
122+
"""Tuya Valve Water consumed cluster without instantaneous demand."""
123+
118124
def __init__(self, *args, **kwargs):
119-
"""Init a TuyaValveWaterConsumed cluster."""
125+
"""Init a TuyaValveWaterConsumedNoInstDemand cluster."""
120126
super().__init__(*args, **kwargs)
121127
self.add_unsupported_attribute(Metering.AttributeDefs.instantaneous_demand.id)
122128

@@ -157,6 +163,14 @@ class AttributeDefs(BaseAttributeDefs):
157163
)
158164

159165

166+
class TuyaIlluminance(IlluminanceMeasurement, TuyaLocalCluster):
167+
"""Tuya local illuminance cluster."""
168+
169+
_CONSTANT_ATTRIBUTES = {
170+
IlluminanceMeasurement.AttributeDefs.light_sensor_type.id: IlluminanceMeasurement.LightSensorType.Photodiode
171+
}
172+
173+
160174
class TuyaQuirkBuilder(QuirkBuilder):
161175
"""Tuya QuirkBuilder."""
162176

@@ -218,6 +232,24 @@ class TuyaPowerConfigurationClusterBattery(TuyaPowerConfigurationCluster):
218232
dp_id=dp_id, power_cfg=TuyaPowerConfigurationClusterBattery, scale=scale
219233
)
220234

235+
def tuya_illuminance(
236+
self,
237+
dp_id: int,
238+
illuminance_cfg: TuyaLocalCluster = TuyaIlluminance,
239+
converter: Optional[Callable[[Any], Any]] = (
240+
lambda x: 10000 * math.log10(x) + 1 if x != 0 else 0
241+
),
242+
) -> QuirkBuilder:
243+
"""Add a Tuya Illuminance Configuration."""
244+
self.tuya_dp(
245+
dp_id,
246+
illuminance_cfg.ep_attribute,
247+
IlluminanceMeasurement.AttributeDefs.measured_value.name,
248+
converter=converter,
249+
)
250+
self.adds(illuminance_cfg)
251+
return self
252+
221253
def tuya_contact(self, dp_id: int):
222254
"""Add a Tuya IAS contact sensor."""
223255
self.tuya_ias(
@@ -316,7 +348,7 @@ def tuya_ias(
316348
def tuya_metering(
317349
self,
318350
dp_id: int,
319-
metering_cfg: TuyaLocalCluster = TuyaValveWaterConsumed,
351+
metering_cfg: TuyaLocalCluster = TuyaValveWaterConsumedNoInstDemand,
320352
scale: float = 1,
321353
) -> QuirkBuilder:
322354
"""Add a Tuya Metering Configuration."""

0 commit comments

Comments
 (0)