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
747 stars 684 forks source link

[Device Support Request] vendor: 'TuYa' modelID: 'TS0601', manufacturerName: '_TZE200_whkgqxse' model: 'JM-TRH-ZGB-V1' #1771

Closed abstiger closed 1 year ago

abstiger commented 2 years ago

Is your feature request related to a problem? Please describe.

I was able to connect it to the HA zigbee, but neither of the sensors are working in HA.

Device Info: https://www.aliexpress.com/item/3256803794332794.html

Describe the solution you'd like

can support this device with ZHA

Device signature ```yaml Paste the device signature here. Don't remove the extra line breaks outside the ``` marks. ```
Diagnostic information ```yaml { "home_assistant": { "installation_type": "Home Assistant Container", "version": "2022.9.2", "dev": false, "hassio": false, "virtualenv": false, "python_version": "3.10.5", "docker": true, "arch": "x86_64", "timezone": "Asia/Shanghai", "os_name": "Linux", "os_version": "5.4.0-125-generic", "run_as_root": true }, "custom_components": {}, "integration_manifest": { "domain": "zha", "name": "Zigbee Home Automation", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ "bellows==0.33.1", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.79", "zigpy-deconz==0.18.1", "zigpy==0.50.3", "zigpy-xbee==0.15.0", "zigpy-zigate==0.9.2", "zigpy-znp==0.8.2" ], "usb": [ { "vid": "10C4", "pid": "EA60", "description": "*2652*", "known_devices": [ "slae.sh cc2652rb stick" ] }, { "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": "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" ] } ], "codeowners": [ "@dmulcahey", "@adminiuga", "@puddly" ], "zeroconf": [ { "type": "_esphomelib._tcp.local.", "name": "tube*" }, { "type": "_zigate-zigbee-gateway._tcp.local.", "name": "*zigate*" } ], "dependencies": [ "file_upload" ], "after_dependencies": [ "onboarding", "usb", "zeroconf" ], "iot_class": "local_polling", "loggers": [ "aiosqlite", "bellows", "crccheck", "pure_pcapy3", "zhaquirks", "zigpy", "zigpy_deconz", "zigpy_xbee", "zigpy_zigate", "zigpy_znp" ], "is_built_in": true }, "data": { "ieee": "**REDACTED**", "nwk": 41779, "manufacturer": "_TZE200_whkgqxse", "model": "TS0601", "name": "_TZE200_whkgqxse TS0601", "quirk_applied": true, "quirk_class": "tuya.TuyaTempHumiditySensor", "manufacturer_code": 4417, "power_source": "Battery or Unknown", "lqi": null, "rssi": null, "last_seen": "2022-09-20T10:58:33", "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": 260, "device_type": "0x0302", "in_clusters": [ "0x0001", "0x0402", "0x0405", "0xef00" ], "out_clusters": [ "0x000a", "0x0019" ] } } }, "active_coordinator": false, "entities": [ { "entity_id": "sensor.tze200_whkgqxse_ts0601_battery", "name": "_TZE200_whkgqxse TS0601" }, { "entity_id": "sensor.tze200_whkgqxse_ts0601_temperature", "name": "_TZE200_whkgqxse TS0601" }, { "entity_id": "sensor.tze200_whkgqxse_ts0601_humidity", "name": "_TZE200_whkgqxse TS0601" } ], "neighbors": [], "endpoint_names": [ { "name": "TEMPERATURE_SENSOR" } ], "user_given_name": null, "device_reg_id": "4c675c047ff7ec11fd3038c2cb14a6cb", "area_id": "er_tong_fang", "cluster_details": { "1": { "device_type": { "name": "TEMPERATURE_SENSOR", "id": 770 }, "profile_id": 260, "in_clusters": { "0xef00": { "endpoint_attribute": "tuya_manufacturer", "attributes": {}, "unsupported_attributes": {} }, "0x0402": { "endpoint_attribute": "temperature", "attributes": {}, "unsupported_attributes": {} }, "0x0405": { "endpoint_attribute": "humidity", "attributes": {}, "unsupported_attributes": {} }, "0x0001": { "endpoint_attribute": "power", "attributes": {}, "unsupported_attributes": {} } }, "out_clusters": { "0x0019": { "endpoint_attribute": "ota", "attributes": {}, "unsupported_attributes": {} }, "0x000a": { "endpoint_attribute": "time", "attributes": {}, "unsupported_attributes": {} } } } } } } ```
Additional logs ``` Paste any additional debug logs here. Don't remove the extra line breaks outside the ``` marks. ```

Additional context

Zigbee2MQTT has already supported, see this commit: https://github.com/Koenkk/zigbee-herdsman-converters/commit/8b5b6d50bb436c3664c87b12a44c8075dbfb7586

b2un0 commented 1 year ago
File "/config/zha_quirks/tuya_ts0601_tze200_whkgqxse.py", line 53, in TemperatureHumidityManufCluster
    server_commands[TUYA_SET_TIME] = foundation.ZCLCommandDef(
NameError: name 'TUYA_SET_TIME' is not defined

replaced TUYA_SET_TIME with 0x24 and it starts.

now i test the device

javicalle commented 1 year ago

(One more lap)

from zhaquirks.tuya import TuyaLocalCluster, TuyaPowerConfigurationCluster2AAA, TuyaTimePayload, TUYA_SET_TIME
b2un0 commented 1 year ago

whoop whop. Time Sync works. Year not (lacks on 2000)

i removed the battery from the device at 22:02 and it changed immediately to 22:09 on join

2023-02-02 22:09:54.050 INFO (MainThread) [zigpy.application] Device 0x308f (a4:c1:38:42:ec:59:bb:6b) joined the network
2023-02-02 22:09:54.051 DEBUG (MainThread) [zigpy.zdo] [0x308f:zdo] ZDO request ZDOCmd.Device_annce: [0x308F, a4:c1:38:42:ec:59:bb:6b, 128]
2023-02-02 22:09:55.269 DEBUG (MainThread) [zigpy.application] Received a packet: ZigbeePacket(src=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x08CE), src_ep=1, dst=AddrModeAddress(addr_mode=<AddrMode.NWK: 2>, address=0x0000), dst_ep=1, source_route=None, extended_timeout=False, tsn=None, profile_id=260, cluster_id=0, data=Serialized[b'\x1c_\x11\xb1\n\x01\xffB\x1a\x01!\xd1\x0b\x03(\x1b\x04!\xa8\x13\x05!$\x00\x06$\x01\x00\x00\x00\x00\n!%\xde'], tx_options=<TransmitOptions.NONE: 0>, radius=0, non_member_radius=0, lqi=255, rssi=-70)
javicalle commented 1 year ago

πŸ•ΊπŸ»

Year not (lacks on 2000)

Ummm, I would suggest to play now with the set_time_local_offset value:

class TemperatureHumidityManufCluster(TuyaMCUCluster):
    """Tuya Manufacturer Cluster with Temperature and Humidity data points."""

    set_time_offset = 2000  # possible values: 0 | 1970* | 2000)
    set_time_local_offset = 2000

One of the 2 must be responsible for the correct value

b2un0 commented 1 year ago

AND THE WINNER IS

    set_time_offset = 1970  # possible values: 0 | 1970* | 2000)
    set_time_local_offset = 0

so here is the finally working quirks

"""Tuya temp and humidity sensor"""

from typing import Dict

from zigpy.zcl import foundation
from zigpy.profiles import zha
from zigpy.quirks import CustomDevice
import zigpy.types as t
from zigpy.zcl.clusters.general import Basic, Groups, Ota, Scenes, Time
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, TuyaTimePayload, TUYA_SET_TIME
from zhaquirks.tuya.ts0201 import TuyaTemperatureHumidityAlarmCluster
from zhaquirks.tuya.mcu import (
    DPToAttributeMapping,
    EnchantedDevice,
    TuyaDPType,
    TuyaMCUCluster,
)

class TuyaTemperatureMeasurement(TemperatureMeasurement, TuyaLocalCluster):
    """Tuya local TemperatureMeasurement cluster."""

class TuyaRelativeHumidity(RelativeHumidity, TuyaLocalCluster):
    """Tuya local RelativeHumidity cluster."""

class TuyaTHAC(TuyaTemperatureHumidityAlarmCluster):
    """Tuya cluster."""

    ep_attribute = "tuya_temp_hum_alarm_cluster"

class TemperatureHumidityManufCluster(TuyaMCUCluster):
    """Tuya Manufacturer Cluster with Temperature and Humidity data points."""

    set_time_offset = 1970  # possible values: 0 | 1970* | 2000)
    set_time_local_offset = 0

    server_commands = TuyaMCUCluster.server_commands.copy()
    server_commands[TUYA_SET_TIME] = foundation.ZCLCommandDef(
        "set_time", {"time": TuyaTimePayload}, False, is_manufacturer_specific=False
    )

    attributes = TuyaMCUCluster.attributes.copy()
    attributes.update(
        {
            0xD009: ("temperature_unit_converter", t.CharacterString, True),
            # Alarm information
            0xD013: ("temperature_sensitivity", t.CharacterString, True),
            0xD011: ("report_interval", t.uint16_t, True),
            # Unknown
            0xD010: ("unknown", t.uint8_t, True),
        }
    )

    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,
        ),
        4: DPToAttributeMapping(
            TuyaPowerConfigurationCluster2AAA.ep_attribute,
            "battery_percentage_remaining",
            dp_type=TuyaDPType.VALUE,
            converter=lambda x: x * 2,  # double reported percentage
        ),
        10: DPToAttributeMapping(
            TuyaTHAC.ep_attribute,
            "alarm_temperature_max",
            dp_type=TuyaDPType.VALUE,
            converter=lambda x: x * 10,  # decipercent to centipercent
        ),
        11: DPToAttributeMapping(
            TuyaTHAC.ep_attribute,
            "alarm_temperature_min",
            dp_type=TuyaDPType.VALUE,
            converter=lambda x: x * 10,  # decipercent to centipercent
        ),
        12: DPToAttributeMapping(
            TuyaTHAC.ep_attribute,
            "alarm_humidity_max",
            dp_type=TuyaDPType.VALUE,
            converter=lambda x: x * 10,  # decipercent to centipercent
        ),
        13: DPToAttributeMapping(
            TuyaTHAC.ep_attribute,
            "alarm_humidity_min",
            dp_type=TuyaDPType.VALUE,
            converter=lambda x: x * 10,  # decipercent to centipercent
        ),
        14: DPToAttributeMapping(
            TuyaTHAC.ep_attribute,
            "temperature_humidity",
            dp_type=TuyaDPType.VALUE,
            converter=lambda x: x,
        ),
        15: DPToAttributeMapping(
            TuyaTHAC.ep_attribute,
            "alarm_humidity",
            dp_type=TuyaDPType.VALUE,
            converter=lambda x: x,
        ),
    }

    data_point_handlers = {
        1: "_dp_2_attr_update",
        2: "_dp_2_attr_update",
        4: "_dp_2_attr_update",
        10: "_dp_2_attr_update",
        11: "_dp_2_attr_update",
        12: "_dp_2_attr_update",
        13: "_dp_2_attr_update",
        14: "_dp_2_attr_update",
        15: "_dp_2_attr_update",
    }

class TuyaTempHumiditySensor(EnchantedDevice, CustomDevice):
    """Custom device representing tuya temp and humidity sensor with e-ink screen."""

    signature = {
        # <SimpleDescriptor endpoint=1, profile=260, device_type=81
        # device_version=1
        # input_clusters=[4, 5, 61184, 0]
        # output_clusters=[25, 10]>
        MODELS_INFO: [("_TZE200_whkgqxse", "TS0601")],
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
                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: [
                    TemperatureHumidityManufCluster,  # Single bus for temp, humidity, and battery
                    TuyaTHAC,
                    TuyaTemperatureMeasurement,
                    TuyaRelativeHumidity,
                    TuyaPowerConfigurationCluster2AAA,
                ],
                OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id],
            }
        },
    }

thank you so much @javicalle for your time!

javicalle commented 1 year ago

πŸ™‡πŸ»β€β™‚οΈ

set_time_offset = 1970  # possible values: 0 | 1970* | 2000)
set_time_local_offset = 0

If so, you can remove it from your quirk.

I need to figure out a way to handle that is_manufacturer_specific=False but until then, can you PR the working version? I would suggest to put it inside the ts0601_sensor.py file.

blomnik commented 1 year ago

Can confirm! Finally, it's working. Good job, @b2un0 @javicalle . Thank you.

BTW, it is sufficient to restart ZHA integration to apply changes. BTW2, you can press "+" and "-" simultaneously to send "set_time_request" from device. ;)

b2un0 commented 1 year ago

can you PR the working version?

yes. in the next days

b2un0 commented 1 year ago

@javicalle what is the status with the is_manufacturer_specific parameter?

do i have to wait for you, or should i try to make these changes in ts0601_sensor.py?

javicalle commented 1 year ago

No ETC currently so better go ahead with the PR

b2un0 commented 1 year ago

@javicalle the quirks does not work anymore since HA update to >= 2023.03.

some imports fail.

Any suggestions?

blomnik commented 1 year ago

@javicalle the quirks does not work anymore since HA update to >= 2023.03.

some imports fail.

Any suggestions?

Here is my patched version, which works on 2023.3.2

"""Tuya temp and humidity sensor"""

from typing import Dict

from zigpy.zcl import foundation
from zigpy.profiles import zha
from zigpy.quirks import CustomDevice
import zigpy.types as t
from zigpy.zcl.clusters.general import Basic, Groups, Ota, Scenes, Time
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, TuyaTimePayload, TUYA_SET_TIME
from zhaquirks.tuya.ts0201 import TuyaTemperatureHumidityAlarmCluster
from zhaquirks.tuya.mcu import (
    DPToAttributeMapping,
    EnchantedDevice,
    TuyaMCUCluster,
)
from zhaquirks.tuya import (
    TuyaDPType,
)

class TuyaTemperatureMeasurement(TemperatureMeasurement, TuyaLocalCluster):
    """Tuya local TemperatureMeasurement cluster."""

class TuyaRelativeHumidity(RelativeHumidity, TuyaLocalCluster):
    """Tuya local RelativeHumidity cluster."""

class TuyaTHAC(TuyaTemperatureHumidityAlarmCluster):
    """Tuya cluster."""

    ep_attribute = "tuya_temp_hum_alarm_cluster"

class TemperatureHumidityManufCluster(TuyaMCUCluster):
    """Tuya Manufacturer Cluster with Temperature and Humidity data points."""

    set_time_offset = 1970  # possible values: 0 | 1970* | 2000)
    set_time_local_offset = 0

    server_commands = TuyaMCUCluster.server_commands.copy()
    server_commands[TUYA_SET_TIME] = foundation.ZCLCommandDef(
        "set_time", {"time": TuyaTimePayload}, False, is_manufacturer_specific=False
    )

    attributes = TuyaMCUCluster.attributes.copy()
    attributes.update(
        {
            0xD009: ("temperature_unit_converter", t.CharacterString, True),
            # Alarm information
            0xD013: ("temperature_sensitivity", t.CharacterString, True),
            0xD011: ("report_interval", t.uint16_t, True),
            # Unknown
            0xD010: ("unknown", t.uint8_t, True),
        }
    )

    dp_to_attribute: Dict[int, DPToAttributeMapping] = {
        1: DPToAttributeMapping(
            TuyaTemperatureMeasurement.ep_attribute,
            "measured_value",
            converter=lambda x: x * 10,  # decidegree to centidegree
        ),
        2: DPToAttributeMapping(
            TuyaRelativeHumidity.ep_attribute,
            "measured_value",
            converter=lambda x: x * 100,
        ),
        4: DPToAttributeMapping(
            TuyaPowerConfigurationCluster2AAA.ep_attribute,
            "battery_percentage_remaining",
            converter=lambda x: x * 2,  # double reported percentage
        ),
        10: DPToAttributeMapping(
            TuyaTHAC.ep_attribute,
            "alarm_temperature_max",
            converter=lambda x: x * 10,  # decipercent to centipercent
        ),
        11: DPToAttributeMapping(
            TuyaTHAC.ep_attribute,
            "alarm_temperature_min",
            converter=lambda x: x * 10,  # decipercent to centipercent
        ),
        12: DPToAttributeMapping(
            TuyaTHAC.ep_attribute,
            "alarm_humidity_max",
            converter=lambda x: x * 10,  # decipercent to centipercent
        ),
        13: DPToAttributeMapping(
            TuyaTHAC.ep_attribute,
            "alarm_humidity_min",
            converter=lambda x: x * 10,  # decipercent to centipercent
        ),
        14: DPToAttributeMapping(
            TuyaTHAC.ep_attribute,
            "temperature_humidity",
            converter=lambda x: x,
        ),
        15: DPToAttributeMapping(
            TuyaTHAC.ep_attribute,
            "alarm_humidity",
            converter=lambda x: x,
        ),
    }

    data_point_handlers = {
        1: "_dp_2_attr_update",
        2: "_dp_2_attr_update",
        4: "_dp_2_attr_update",
        10: "_dp_2_attr_update",
        11: "_dp_2_attr_update",
        12: "_dp_2_attr_update",
        13: "_dp_2_attr_update",
        14: "_dp_2_attr_update",
        15: "_dp_2_attr_update",
    }

class TuyaTempHumiditySensor(EnchantedDevice, CustomDevice):
    """Custom device representing tuya temp and humidity sensor with e-ink screen."""

    signature = {
        # <SimpleDescriptor endpoint=1, profile=260, device_type=81
        # device_version=1
        # input_clusters=[4, 5, 61184, 0]
        # output_clusters=[25, 10]>
        MODELS_INFO: [("_TZE200_whkgqxse", "TS0601")],
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
                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: [
                    TemperatureHumidityManufCluster,  # Single bus for temp, humidity, and battery
                    TuyaTHAC,
                    TuyaTemperatureMeasurement,
                    TuyaRelativeHumidity,
                    TuyaPowerConfigurationCluster2AAA,
                ],
                OUTPUT_CLUSTERS: [Ota.cluster_id, Time.cluster_id],
            }
        },
    }
marmoute commented 1 year ago

Newbie question: I though official support had landed upstream and I hoped for the new version to add support for the device without the quirk. However this does not seems to be the case.

What's the process and what should I expect then ?

github-actions[bot] commented 1 year ago

There hasn't been any activity on this issue recently. Due to the high number of incoming GitHub notifications, we have to clean some of the old issues, as many of them have already been resolved with the latest updates. Please make sure to update to the latest version and check if that solves the issue. Let us know if that works for you by adding a comment πŸ‘ This issue has now been marked as stale and will be closed if no further activity occurs. Thank you for your contributions.