zigpy / zha-device-handlers

ZHA device handlers bridge the functionality gap created when manufacturers deviate from the ZCL specification, handling deviations and exceptions by parsing custom messages to and from Zigbee devices.
Apache License 2.0
682 stars 631 forks source link

[Device Support Request] Tuya _TZE200_cirvgep4 #3041

Open dekar91 opened 3 months ago

dekar91 commented 3 months ago

Problem description

The Quirk from https://github.com/zigpy/zha-device-handlers/issues/1944 works partually for me, apart from the battery and firmware fields. I would like to have it fixed.

Solution description

Battery level should be displayed. Zigbee firmware should also be exposed, if possible.

Screenshots/Video

Screenshots/Video ![image](https://github.com/zigpy/zha-device-handlers/assets/13837421/242027c2-3587-4e27-97e4-85e951a9c2f1)

Device signature

Device signature ```json { "node_descriptor": "NodeDescriptor(logical_type=, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=, mac_capability_flags=, manufacturer_code=4417, maximum_buffer_size=66, maximum_incoming_transfer_size=66, server_mask=10752, maximum_outgoing_transfer_size=66, descriptor_capability_field=, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=True, *is_full_function_device=False, *is_mains_powered=False, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False)", "endpoints": { "1": { "profile_id": "0x0104", "device_type": "0x0302", "input_clusters": [ "0x0000", "0x0001", "0x0402", "0x0405", "0xef00" ], "output_clusters": [ "0x000a", "0x0019" ] } }, "manufacturer": "_TZE200_cirvgep4", "model": "TS0601", "class": "ts0601_temperature.TuyaNousE6TempHumiditySensor" } ```

Diagnostic information

Diagnostic information ```json { "home_assistant": { "installation_type": "Home Assistant OS", "version": "2024.3.1", "dev": false, "hassio": true, "virtualenv": false, "python_version": "3.12.2", "docker": true, "arch": "aarch64", "timezone": "Europe/Berlin", "os_name": "Linux", "os_version": "6.1.73-haos-raspi", "supervisor": "2024.03.0", "host_os": "Home Assistant OS 12.1", "docker_version": "24.0.7", "chassis": "embedded", "run_as_root": true }, "custom_components": { "hacs": { "version": "1.34.0", "requirements": [ "aiogithubapi>=22.10.1" ] }, "watchman": { "version": "0.5.1", "requirements": [ "prettytable==3.0.0" ] }, "dwains_dashboard": { "version": "3.6.0", "requirements": [] }, "localtuya": { "version": "5.2.1", "requirements": [] }, "o365": { "version": "v4.7.0", "requirements": [ "O365==2.0.34", "BeautifulSoup4>=4.10.0" ] }, "sonoff": { "version": "3.6.0", "requirements": [ "pycryptodome>=3.6.6" ] }, "tuya_ble": { "version": "0.1.8", "requirements": [ "tuya-iot-py-sdk==0.6.6", "pycountry==22.3.5" ] } }, "integration_manifest": { "domain": "zha", "name": "Zigbee Home Automation", "after_dependencies": [ "onboarding", "usb" ], "codeowners": [ "@dmulcahey", "@adminiuga", "@puddly", "@TheJulianJES" ], "config_flow": true, "dependencies": [ "file_upload" ], "documentation": "https://www.home-assistant.io/integrations/zha", "import_executor": true, "iot_class": "local_polling", "loggers": [ "aiosqlite", "bellows", "crccheck", "pure_pcapy3", "zhaquirks", "zigpy", "zigpy_deconz", "zigpy_xbee", "zigpy_zigate", "zigpy_znp", "universal_silabs_flasher" ], "requirements": [ "bellows==0.38.1", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.112", "zigpy-deconz==0.23.1", "zigpy==0.63.4", "zigpy-xbee==0.20.1", "zigpy-zigate==0.12.0", "zigpy-znp==0.12.1", "universal-silabs-flasher==0.0.18", "pyserial-asyncio-fast==0.11" ], "usb": [ { "vid": "10C4", "pid": "EA60", "description": "*2652*", "known_devices": [ "slae.sh cc2652rb stick" ] }, { "vid": "10C4", "pid": "EA60", "description": "*slzb-07*", "known_devices": [ "smlight slzb-07" ] }, { "vid": "1A86", "pid": "55D4", "description": "*sonoff*plus*", "known_devices": [ "sonoff zigbee dongle plus v2" ] }, { "vid": "10C4", "pid": "EA60", "description": "*sonoff*plus*", "known_devices": [ "sonoff zigbee dongle plus" ] }, { "vid": "10C4", "pid": "EA60", "description": "*tubeszb*", "known_devices": [ "TubesZB Coordinator" ] }, { "vid": "1A86", "pid": "7523", "description": "*tubeszb*", "known_devices": [ "TubesZB Coordinator" ] }, { "vid": "1A86", "pid": "7523", "description": "*zigstar*", "known_devices": [ "ZigStar Coordinators" ] }, { "vid": "1CF1", "pid": "0030", "description": "*conbee*", "known_devices": [ "Conbee II" ] }, { "vid": "0403", "pid": "6015", "description": "*conbee*", "known_devices": [ "Conbee III" ] }, { "vid": "10C4", "pid": "8A2A", "description": "*zigbee*", "known_devices": [ "Nortek HUSBZB-1" ] }, { "vid": "0403", "pid": "6015", "description": "*zigate*", "known_devices": [ "ZiGate+" ] }, { "vid": "10C4", "pid": "EA60", "description": "*zigate*", "known_devices": [ "ZiGate" ] }, { "vid": "10C4", "pid": "8B34", "description": "*bv 2010/10*", "known_devices": [ "Bitron Video AV2010/10" ] } ], "zeroconf": [ { "type": "_esphomelib._tcp.local.", "name": "tube*" }, { "type": "_zigate-zigbee-gateway._tcp.local.", "name": "*zigate*" }, { "type": "_zigstar_gw._tcp.local.", "name": "*zigstar*" }, { "type": "_uzg-01._tcp.local.", "name": "uzg-01*" }, { "type": "_slzb-06._tcp.local.", "name": "slzb-06*" } ], "is_built_in": true }, "data": { "ieee": "**REDACTED**", "nwk": 50604, "manufacturer": "_TZE200_cirvgep4", "model": "TS0601", "name": "_TZE200_cirvgep4 TS0601", "quirk_applied": true, "quirk_class": "ts0601_temperature.TuyaNousE6TempHumiditySensor", "quirk_id": null, "manufacturer_code": 4417, "power_source": "Battery or Unknown", "lqi": 248, "rssi": -38, "last_seen": "2024-03-15T11:03:04", "available": true, "device_type": "EndDevice", "signature": { "node_descriptor": "NodeDescriptor(logical_type=, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=, mac_capability_flags=, manufacturer_code=4417, maximum_buffer_size=66, maximum_incoming_transfer_size=66, server_mask=10752, maximum_outgoing_transfer_size=66, descriptor_capability_field=, *allocate_address=True, *is_alternate_pan_coordinator=False, *is_coordinator=False, *is_end_device=True, *is_full_function_device=False, *is_mains_powered=False, *is_receiver_on_when_idle=False, *is_router=False, *is_security_capable=False)", "endpoints": { "1": { "profile_id": "0x0104", "device_type": "0x0302", "input_clusters": [ "0x0000", "0x0001", "0x0402", "0x0405", "0xef00" ], "output_clusters": [ "0x000a", "0x0019" ] } }, "manufacturer": "_TZE200_cirvgep4", "model": "TS0601" }, "active_coordinator": false, "entities": [ { "entity_id": "sensor.bath_clock_battery", "name": "_TZE200_cirvgep4 TS0601" }, { "entity_id": "sensor.bath_clock_temperature", "name": "_TZE200_cirvgep4 TS0601" }, { "entity_id": "sensor.bath_clock_humidity", "name": "_TZE200_cirvgep4 TS0601" }, { "entity_id": "update.bath_clock_firmware", "name": "_TZE200_cirvgep4 TS0601" } ], "neighbors": [], "routes": [], "endpoint_names": [ { "name": "TEMPERATURE_SENSOR" } ], "user_given_name": "Bath clock", "device_reg_id": "029f5a4334aab87d89896e17580e9b7d", "area_id": "kitchen", "cluster_details": { "1": { "device_type": { "name": "TEMPERATURE_SENSOR", "id": 770 }, "profile_id": 260, "in_clusters": { "0x0000": { "endpoint_attribute": "basic", "attributes": { "0x0001": { "attribute_name": "app_version", "value": 72 }, "0x0004": { "attribute_name": "manufacturer", "value": "_TZE200_cirvgep4" }, "0x0005": { "attribute_name": "model", "value": "TS0601" } }, "unsupported_attributes": {} }, "0xef00": { "endpoint_attribute": "tuya_manufacturer", "attributes": { "0xef00": { "attribute_name": "mcu_version", "value": "1.0.0" } }, "unsupported_attributes": {} }, "0x0402": { "endpoint_attribute": "temperature", "attributes": { "0x0000": { "attribute_name": "measured_value", "value": 2580 } }, "unsupported_attributes": {} }, "0x0405": { "endpoint_attribute": "humidity", "attributes": { "0x0000": { "attribute_name": "measured_value", "value": 3400 } }, "unsupported_attributes": {} }, "0x0001": { "endpoint_attribute": "power", "attributes": { "0x0033": { "attribute_name": "battery_quantity", "value": 2 }, "0x0034": { "attribute_name": "battery_rated_voltage", "value": 15 }, "0x0031": { "attribute_name": "battery_size", "value": 4 } }, "unsupported_attributes": {} } }, "out_clusters": { "0x0019": { "endpoint_attribute": "ota", "attributes": { "0x0002": { "attribute_name": "current_file_version", "value": 72 } }, "unsupported_attributes": {} }, "0x000a": { "endpoint_attribute": "time", "attributes": {}, "unsupported_attributes": {} } } } } } } ```

Logs

Logs ```python [Paste the logs here] ```

Custom quirk

Custom quirk ```python """Tuya temp and humidity sensor with screen.""" from typing import Dict ################## clean this up import zigpy.types as t from zigpy.zcl import foundation from zhaquirks.tuya import TuyaTimePayload, TuyaCommand import datetime from typing import Tuple, Optional, Union ################## from zigpy.profiles import zha from zigpy.quirks import CustomDevice from zigpy.zcl.clusters.general import Basic, Groups, Ota, Scenes, Time, AnalogOutput from zigpy.zcl.clusters.measurement import RelativeHumidity, TemperatureMeasurement from zhaquirks.const import ( DEVICE_TYPE, ENDPOINTS, INPUT_CLUSTERS, MODELS_INFO, OUTPUT_CLUSTERS, PROFILE_ID, SKIP_CONFIGURATION, ) from zhaquirks.tuya import TuyaLocalCluster, TuyaPowerConfigurationCluster2AAA # old from zhaquirks.tuya.mcu import DPToAttributeMapping, TuyaDPType, TuyaMCUCluster from zhaquirks.tuya.mcu import DPToAttributeMapping, TuyaMCUCluster TUYA_SET_TIME = 0x24 # NOTES: # The data comes in as a string on cluster, if there is nothing set up you may see these lines in the logs: # Unknown message (b'19830100a40102000400000118') on cluster 61184: unknown endpoint or cluster id: 'No cluster ID 0xef00 on (a4:c1:38:d0:18:8b:64:aa, 1)' # 28.0 degrees # Unknown message (b'19840100a5020200040000022c') on cluster 61184: unknown endpoint or cluster id: 'No cluster ID 0xef00 on (a4:c1:38:d0:18:8b:64:aa, 1)' # 55.6% humid # Unknown message (b'19850100a60402000400000064') on cluster 61184: unknown endpoint or cluster id: 'No cluster ID 0xef00 on (a4:c1:38:d0:18:8b:64:aa, 1)' # 100% battery class TemperatureUnitConvert(t.enum8): """Tuya Temp unit convert enum.""" Celsius = 0x00 Fahrenheit = 0x01 class TuyaTemperatureMeasurement(TemperatureMeasurement, TuyaLocalCluster): """Tuya local TemperatureMeasurement cluster.""" attributes = TemperatureMeasurement.attributes.copy() attributes.update( { 0x8001: ("temp_unit_convert", t.enum8), 0x8002: ("alarm_max_temperature", t.Single), 0x8003: ("alarm_min_temperature", t.Single), 0x8004: ("temperature_sensitivity", t.Single), } ) class TuyaRelativeHumidity(RelativeHumidity, TuyaLocalCluster): """Tuya local RelativeHumidity cluster.""" class TemperatureHumidityManufCluster(TuyaMCUCluster): """Tuya Manufacturer Cluster with Temperature and Humidity data points.""" dp_to_attribute: Dict[int, DPToAttributeMapping] = { 1: DPToAttributeMapping( TuyaTemperatureMeasurement.ep_attribute, "measured_value", # dp_type=TuyaDPType.VALUE, converter=lambda x: x * 10, # decidegree to centidegree ), 2: DPToAttributeMapping( TuyaRelativeHumidity.ep_attribute, "measured_value", # dp_type=TuyaDPType.VALUE, converter=lambda x: x * 100, # 0.01 to 1.0 ), 4: DPToAttributeMapping( TuyaPowerConfigurationCluster2AAA.ep_attribute, "battery_percentage_remaining", # dp_type=TuyaDPType.VALUE, converter=lambda x: x * 2, # reported percentage is doubled ), 9: DPToAttributeMapping( TuyaTemperatureMeasurement.ep_attribute, "temp_unit_convert", # dp_type=TuyaDPType.ENUM, converter=lambda x: TemperatureUnitConvert(x) ), 10: DPToAttributeMapping( TuyaTemperatureMeasurement.ep_attribute, "alarm_max_temperature", # dp_type=TuyaDPType.VALUE, converter=lambda x: x / 10 ), 11: DPToAttributeMapping( TuyaTemperatureMeasurement.ep_attribute, "alarm_min_temperature", # dp_type=TuyaDPType.VALUE, converter=lambda x: x / 10 ), 19: DPToAttributeMapping( TuyaTemperatureMeasurement.ep_attribute, "temperature_sensitivity", # dp_type=TuyaDPType.VALUE, converter=lambda x: x / 10 ) } set_time_offset = 1970 set_time_local_offset = 1970 data_point_handlers = { 1: "_dp_2_attr_update", 2: "_dp_2_attr_update", 4: "_dp_2_attr_update", 9: "_dp_2_attr_update", 10: "_dp_2_attr_update", 11: "_dp_2_attr_update", 19: "_dp_2_attr_update", } def handle_set_time_request(self, sequence_number: t.uint16_t) -> foundation.Status: payload = TuyaTimePayload() utc_now = datetime.datetime.utcnow() now = datetime.datetime.now() offset_time = datetime.datetime(self.set_time_offset, 1, 1) offset_time_local = datetime.datetime(self.set_time_local_offset, 1, 1) utc_timestamp = int((utc_now - offset_time).total_seconds()) local_timestamp = int((now - offset_time).total_seconds()) payload.extend(utc_timestamp.to_bytes(4, "big", signed=False)) payload.extend(local_timestamp.to_bytes(4, "big", signed=False)) self.create_catching_task( self.command(TUYA_SET_TIME, payload, manufacturer=foundation.ZCLHeader.NO_MANUFACTURER_ID, expect_reply=False) ) return foundation.Status.SUCCESS class TuyaNousE6TempHumiditySensor(CustomDevice): """Custom device representing tuya temp and humidity sensor with a screen (NOUS E6).""" signature = { # MODELS_INFO: [("_TZE200_locansqn", "TS0601"), ("_TZE200_cirvgep4", "TS0601")], ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, DEVICE_TYPE: zha.DeviceType.SMART_PLUG, # this is how the device reports itself INPUT_CLUSTERS: [ Basic.cluster_id, Groups.cluster_id, Scenes.cluster_id, TemperatureHumidityManufCluster.cluster_id, ], OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], } }, } replacement = { SKIP_CONFIGURATION: True, ENDPOINTS: { 1: { PROFILE_ID: zha.PROFILE_ID, DEVICE_TYPE: zha.DeviceType.TEMPERATURE_SENSOR, INPUT_CLUSTERS: [ Basic.cluster_id, TemperatureHumidityManufCluster, # Single bus for temp, humidity, and battery TuyaTemperatureMeasurement, TuyaRelativeHumidity, TuyaPowerConfigurationCluster2AAA, ], OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id], } }, } ```

Additional information

No response

jacekk015 commented 3 months ago

Your quirk already support battery state under DP4. Device just doesn't send it. Take out battery, wait few seconds, put it inside. Then probably it will send it. That's battery operated device and it sends as low data as it can.

dekar91 commented 3 months ago

Thanks for the response @jacekk015! This is weird, because when the device is connected to "Smart Life" it updates the battery status. If your have some debugging guide or docs, so I could check, could you please share it with me?

jacekk015 commented 3 months ago

If your have some debugging guide or docs, so I could check, could you please share it with me?

You cant find in the logs data that haven't been sent. If device sends something from DP4 you can be more than sure that quirk will forward this to HA.

SmartLife just caches last read. Same as HA do, it will just show you last read from the cache/database. You could always try to hack SmartLife <-> Device communication, but I'm pretty sure it's not too wise to often ask battery device about battery status, because that drains battery even more. All battery driven devices are known to be lazy Zigbee devices.

nono031 commented 3 months ago

duplicated with https://github.com/zigpy/zha-device-handlers/issues/2304