diff --git a/tests/test_tuya.py b/tests/test_tuya.py index d0498a290c..cfaf2f2f4d 100644 --- a/tests/test_tuya.py +++ b/tests/test_tuya.py @@ -2023,3 +2023,103 @@ async def test_ts601_door_sensor( attrs = await cluster.read_attributes(attributes=[attribute]) assert attrs[0].get(attribute) == expected_value + + +async def test_hobeian_zg_303z_sensor(zigpy_device_from_v2_quirk): + """Test HOBEIAN ZG-303Z sensor.""" + + from zigpy.zcl.clusters.general import PowerConfiguration + from zigpy.zcl.clusters.measurement import ( + RelativeHumidity, + SoilMoisture, + TemperatureMeasurement, + ) + + from tests.common import ClusterListener + from zhaquirks.tuya import TuyaCommand, TuyaData, TuyaDatapointData + from zhaquirks.tuya.tuya_sensor import TuyaTempUnitConvert + + device = zigpy_device_from_v2_quirk("HOBEIAN", "ZG-303Z") + + ep = device.endpoints[1] + tuya_cluster = ep.tuya_manufacturer + + # 1. Test that the original RelativeHumidity cluster is removed + assert RelativeHumidity.cluster_id not in ep.in_clusters + + # 2. Setup listeners for standard clusters + soil_listener = ClusterListener(ep.in_clusters[SoilMoisture.cluster_id]) + temp_listener = ClusterListener(ep.in_clusters[TemperatureMeasurement.cluster_id]) + battery_listener = ClusterListener(ep.in_clusters[PowerConfiguration.cluster_id]) + + # 3. Test DP 3: Soil Moisture + dp_data = TuyaDatapointData(dp=3, data=TuyaData(value=55)) + tuya_cluster.handle_get_data(TuyaCommand(status=0, tsn=0, datapoints=[dp_data])) + assert len(soil_listener.attribute_updates) == 1 + assert soil_listener.attribute_updates[0][0] == 0 + assert soil_listener.attribute_updates[0][1] == 5500 + + # 4. Test DP 5: Temperature + dp_data = TuyaDatapointData(dp=5, data=TuyaData(value=251)) + tuya_cluster.handle_get_data(TuyaCommand(status=0, tsn=0, datapoints=[dp_data])) + assert len(temp_listener.attribute_updates) == 1 + assert temp_listener.attribute_updates[0][0] == 0 + assert temp_listener.attribute_updates[0][1] == 2510 + + # 5. Test DP 9: Display Unit + dp_data = TuyaDatapointData( + dp=9, data=TuyaData(value=TuyaTempUnitConvert.Fahrenheit) + ) + tuya_cluster.handle_get_data(TuyaCommand(status=0, tsn=0, datapoints=[dp_data])) + assert tuya_cluster.get("display_unit") == TuyaTempUnitConvert.Fahrenheit + + # 6. Test DP 15: Battery + dp_data = TuyaDatapointData(dp=15, data=TuyaData(value=88)) + tuya_cluster.handle_get_data(TuyaCommand(status=0, tsn=0, datapoints=[dp_data])) + assert len(battery_listener.attribute_updates) == 1 + assert battery_listener.attribute_updates[0][0] == 33 + assert battery_listener.attribute_updates[0][1] == 176 # Default scale is 2 + + # 7. Test DP 102: Soil Moisture Calibration + dp_data = TuyaDatapointData(dp=102, data=TuyaData(value=15)) + tuya_cluster.handle_get_data(TuyaCommand(status=0, tsn=0, datapoints=[dp_data])) + assert tuya_cluster.get("soil_moisture_calibration") == 15 + + # 8. Test DP 104: Temperature Calibration + dp_data = TuyaDatapointData(dp=104, data=TuyaData(value=-12)) # -1.2 + tuya_cluster.handle_get_data(TuyaCommand(status=0, tsn=0, datapoints=[dp_data])) + assert tuya_cluster.get("temperature_calibration") == -12 + + # 9. Test DP 105: Humidity Calibration + dp_data = TuyaDatapointData(dp=105, data=TuyaData(value=22)) + tuya_cluster.handle_get_data(TuyaCommand(status=0, tsn=0, datapoints=[dp_data])) + assert tuya_cluster.get("humidity_calibration") == 22 + + # 10. Test DP 106: Dry binary sensor + dp_data = TuyaDatapointData(dp=106, data=TuyaData(value=True)) # Not dry + tuya_cluster.handle_get_data(TuyaCommand(status=0, tsn=0, datapoints=[dp_data])) + assert not tuya_cluster.get("dry") + + dp_data = TuyaDatapointData(dp=106, data=TuyaData(value=False)) # Dry + tuya_cluster.handle_get_data(TuyaCommand(status=0, tsn=0, datapoints=[dp_data])) + assert tuya_cluster.get("dry") + + # 11. Test DP 109: Humidity Sensor + dp_data = TuyaDatapointData(dp=109, data=TuyaData(value=68)) + tuya_cluster.handle_get_data(TuyaCommand(status=0, tsn=0, datapoints=[dp_data])) + assert tuya_cluster.get("humidity_value") == 68 + + # 12. Test DP 110: Alarm Soil Moisture Min + dp_data = TuyaDatapointData(dp=110, data=TuyaData(value=10)) + tuya_cluster.handle_get_data(TuyaCommand(status=0, tsn=0, datapoints=[dp_data])) + assert tuya_cluster.get("alarm_soil_moisture_min") == 10 + + # 13. Test DP 111: Temperature Sampling + dp_data = TuyaDatapointData(dp=111, data=TuyaData(value=120)) + tuya_cluster.handle_get_data(TuyaCommand(status=0, tsn=0, datapoints=[dp_data])) + assert tuya_cluster.get("temperature_sampling") == 120 + + # 14. Test DP 112: Soil Moisture Sampling + dp_data = TuyaDatapointData(dp=112, data=TuyaData(value=180)) + tuya_cluster.handle_get_data(TuyaCommand(status=0, tsn=0, datapoints=[dp_data])) + assert tuya_cluster.get("soil_moisture_sampling") == 180 diff --git a/zhaquirks/tuya/tuya_sensor.py b/zhaquirks/tuya/tuya_sensor.py index d37be72437..023dd24cce 100644 --- a/zhaquirks/tuya/tuya_sensor.py +++ b/zhaquirks/tuya/tuya_sensor.py @@ -4,10 +4,14 @@ from zigpy.quirks.v2 import EntityPlatform, EntityType from zigpy.quirks.v2.homeassistant import PERCENTAGE, UnitOfTemperature, UnitOfTime +from zigpy.quirks.v2.homeassistant.binary_sensor import BinarySensorDeviceClass +from zigpy.quirks.v2.homeassistant.number import NumberDeviceClass from zigpy.quirks.v2.homeassistant.sensor import SensorDeviceClass import zigpy.types as t from zigpy.zcl import foundation +from zigpy.zcl.clusters.measurement import RelativeHumidity +from zhaquirks.const import BatterySize from zhaquirks.tuya import ( TUYA_SET_TIME, TuyaPowerConfigurationCluster2AAA, @@ -333,3 +337,129 @@ class ServerCommandDefs(TuyaMCUCluster.ServerCommandDefs): .skip_configuration() .add_to_registry() ) + + +( + TuyaQuirkBuilder("HOBEIAN", "ZG-303Z") + .applies_to("_TZE200_npj9bug3", "TS0601") # COOLO CS-201Z + .tuya_soil_moisture(dp_id=3) + .tuya_temperature(dp_id=5, scale=10) + .tuya_enum( + dp_id=9, + attribute_name="display_unit", + enum_class=TuyaTempUnitConvert, + entity_type=EntityType.CONFIG, + translation_key="display_unit", + fallback_name="Display unit", + ) + .tuya_battery(dp_id=15, battery_type=BatterySize.AAA) + .tuya_number( + dp_id=102, + attribute_name="soil_moisture_calibration", + type=t.uint16_t, + unit=PERCENTAGE, + min_value=-30, + max_value=30, + step=1, + entity_type=EntityType.CONFIG, + device_class=NumberDeviceClass.HUMIDITY, + translation_key="soil_moisture_calibration", + fallback_name="Soil moisture Calibration", + ) + .tuya_number( + dp_id=104, + attribute_name="temperature_calibration", + type=t.uint16_t, + unit=UnitOfTemperature.KELVIN, + min_value=-2, + max_value=2, + step=0.1, + multiplier=10, + entity_type=EntityType.CONFIG, + device_class=NumberDeviceClass.TEMPERATURE, + translation_key="temperature_calibration", + fallback_name="Temperature Calibration", + ) + .tuya_number( + dp_id=105, + attribute_name="humidity_calibration", + type=t.uint16_t, + unit=PERCENTAGE, + min_value=-30, + max_value=30, + step=1, + entity_type=EntityType.CONFIG, + device_class=NumberDeviceClass.HUMIDITY, + translation_key="humidity_calibration", + fallback_name="Humidity Calibration", + ) + .tuya_dp_attribute( + dp_id=106, + attribute_name="dry", + converter=lambda x: not bool(x), + ) + .binary_sensor( + attribute_name="dry", + cluster_id=TuyaMCUCluster.cluster_id, + endpoint_id=1, + entity_type=EntityType.STANDARD, + device_class=BinarySensorDeviceClass.MOISTURE, + initially_disabled=False, + attribute_initialized_from_cache=False, + translation_key="dry", + fallback_name="dry", + ) + .removes(RelativeHumidity.cluster_id) + .tuya_sensor( + dp_id=109, + type=t.uint16_t, + attribute_name="humidity_value", + unit=PERCENTAGE, + device_class=SensorDeviceClass.HUMIDITY, + entity_type=EntityType.STANDARD, + translation_key="humidity", + fallback_name="Humidity", + ) + .tuya_number( + dp_id=110, + attribute_name="alarm_soil_moisture_min", + type=t.uint16_t, + unit=PERCENTAGE, + min_value=0, + max_value=100, + step=1, + entity_type=EntityType.CONFIG, + device_class=NumberDeviceClass.MOISTURE, + translation_key="alarm_soil_moisture_min", + fallback_name="Alarm soil moisture min", + ) + .tuya_number( + dp_id=111, + attribute_name="temperature_sampling", + type=t.uint16_t, + unit=UnitOfTime.SECONDS, + min_value=5, + max_value=3600, + step=1, + entity_type=EntityType.CONFIG, + device_class=NumberDeviceClass.DURATION, + translation_key="temperature_sampling", + fallback_name="Temperature Sampling", + ) + .tuya_number( + dp_id=112, + attribute_name="soil_moisture_sampling", + type=t.uint16_t, + unit=UnitOfTime.SECONDS, + min_value=5, + max_value=3600, + step=1, + entity_type=EntityType.CONFIG, + device_class=NumberDeviceClass.DURATION, + translation_key="soil_moisture_sampling", + fallback_name="Soil Moisture Sampling", + ) + .tuya_enchantment(data_query_spell=True) + .skip_configuration() + .add_to_registry() +)