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
743 stars 679 forks source link

[Device Support Request] Moes Temperature And Humidity Sensor With LED Screen TS0201 (_TZ3000_itnrsufe) #1667

Closed PlusPlus-ua closed 2 years ago

PlusPlus-ua commented 2 years ago

Is your feature request related to a problem? Please describe. Device from https://www.aliexpress.com/item/1005004443497564.html reports relative humidity in a wrong way. Instead of reporting value*100 (as it described in ZCL) it reports value*10. As a result ZHA converts it to actual_relative_humidity/10. Right now LED screen on the device showing 42% but HA showing 4.2%

Describe the solution you'd like I think zhaquirk is needed for the device. The zhaquirk should apply multiplication by 10 for relative humidity value.

Device signature ```yaml { "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": [ "0x0000", "0x0001", "0x0003", "0x0402", "0x0405", "0xe002" ], "out_clusters": [ "0x0003", "0x000a", "0x0019" ] } }, "manufacturer": "_TZ3000_itnrsufe", "model": "TS0201", "class": "zigpy.device.Device" } ```
Diagnostic information ```yaml { "home_assistant": { "installation_type": "Home Assistant OS", "version": "2022.7.6", "dev": false, "hassio": true, "virtualenv": false, "python_version": "3.10.5", "docker": true, "arch": "x86_64", "timezone": "Europe/Kiev", "os_name": "Linux", "os_version": "5.15.55", "supervisor": "2022.07.1", "host_os": "Home Assistant OS 8.4", "docker_version": "20.10.14", "chassis": "embedded", "run_as_root": true }, "custom_components": { "hisense_tv": { "version": "22.05.09", "requirements": [ "wakeonlan==2.0.1" ] }, "xiaomi_cloud_map_extractor": { "version": "v2.2.0", "requirements": [ "pillow", "pybase64", "python-miio", "requests", "pycryptodome" ] }, "hacs": { "version": "1.25.5", "requirements": [ "aiogithubapi>=22.2.4" ] }, "viomi_vacuum_v8": { "version": "0.0.1", "requirements": [ "construct==2.10.59", "python-miio==0.5.4" ] }, "xiaomi_miot": { "version": "0.6.7", "requirements": [ "construct==2.10.56", "python-miio>=0.5.6", "micloud>=0.3" ] } }, "integration_manifest": { "domain": "zha", "name": "Zigbee Home Automation", "config_flow": true, "documentation": "https://www.home-assistant.io/integrations/zha", "requirements": [ "bellows==0.31.1", "pyserial==3.5", "pyserial-asyncio==0.6", "zha-quirks==0.0.77", "zigpy-deconz==0.18.0", "zigpy==0.47.3", "zigpy-xbee==0.15.0", "zigpy-zigate==0.9.0", "zigpy-znp==0.8.1" ], "usb": [ { "vid": "10C4", "pid": "EA60", "description": "*2652*", "known_devices": [ "slae.sh cc2652rb stick" ] }, { "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" ], "zeroconf": [ { "type": "_esphomelib._tcp.local.", "name": "tube*" }, { "type": "_zigate-zigbee-gateway._tcp.local.", "name": "*zigate*" } ], "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": 9904, "manufacturer": "_TZ3000_itnrsufe", "model": "TS0201", "name": "_TZ3000_itnrsufe TS0201", "quirk_applied": false, "quirk_class": "zigpy.device.Device", "manufacturer_code": 4417, "power_source": "Battery or Unknown", "lqi": 134, "rssi": null, "last_seen": "2022-07-29T17:24:12", "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": [ "0x0000", "0x0001", "0x0003", "0x0402", "0x0405", "0xe002" ], "out_clusters": [ "0x0003", "0x000a", "0x0019" ] } } }, "entities": [ { "entity_id": "button.tz3000_itnrsufe_ts0201_fe09270d_identify", "name": "_TZ3000_itnrsufe TS0201" }, { "entity_id": "sensor.tz3000_itnrsufe_ts0201_fe09270d_power", "name": "_TZ3000_itnrsufe TS0201" }, { "entity_id": "sensor.tz3000_itnrsufe_ts0201_fe09270d_temperature", "name": "_TZ3000_itnrsufe TS0201" }, { "entity_id": "sensor.tz3000_itnrsufe_ts0201_fe09270d_humidity", "name": "_TZ3000_itnrsufe TS0201" } ], "neighbors": [], "endpoint_names": [ { "name": "TEMPERATURE_SENSOR" } ], "user_given_name": null, "device_reg_id": "7307ee75763fadf572cf0f05484f1418", "area_id": "vanna_tualet_drugii_poverkh", "cluster_details": { "1": { "device_type": { "name": "TEMPERATURE_SENSOR", "id": 770 }, "profile_id": 260, "in_clusters": { "0x0001": { "endpoint_attribute": "power", "attributes": { "0x0020": { "attribute_name": "battery_voltage", "value": 30 }, "0x0021": { "attribute_name": "battery_percentage_remaining", "value": 200 } }, "unsupported_attributes": {} }, "0x0003": { "endpoint_attribute": "identify", "attributes": {}, "unsupported_attributes": {} }, "0x0402": { "endpoint_attribute": "temperature", "attributes": { "0x0000": { "attribute_name": "measured_value", "value": 2545 } }, "unsupported_attributes": {} }, "0x0405": { "endpoint_attribute": "humidity", "attributes": { "0x0000": { "attribute_name": "measured_value", "value": 422 } }, "unsupported_attributes": {} }, "0xe002": { "endpoint_attribute": null, "attributes": {}, "unsupported_attributes": {} }, "0x0000": { "endpoint_attribute": "basic", "attributes": { "0x0004": { "attribute_name": "manufacturer", "value": "_TZ3000_itnrsufe" }, "0x0005": { "attribute_name": "model", "value": "TS0201" } }, "unsupported_attributes": {} } }, "out_clusters": { "0x0003": { "endpoint_attribute": "identify", "attributes": {}, "unsupported_attributes": {} }, "0x0019": { "endpoint_attribute": "ota", "attributes": {}, "unsupported_attributes": {} }, "0x000a": { "endpoint_attribute": "time", "attributes": {}, "unsupported_attributes": {} } } } } } } ```
Additional logs ``` 2022-07-29 16:23:58 DEBUG (MainThread) [zigpy_znp.api] Received command: AF.IncomingMsg.Callback(GroupId=0x0000, ClusterId=1029, SrcAddr=0x26B0, SrcEndpoint=1, DstEndpoint=1, WasBroadcast=, LQI=163, SecurityUse=, TimeStamp=9513272, TSN=0, Data=b'\x08\xC6\x0A\x00\x00\x21\xC9\x01', MacSrcAddr=0x26B0, MsgResultRadius=29) 2022-07-29 16:23:58 DEBUG (MainThread) [zigpy.zcl] [0x26B0:1:0x0405] Received ZCL frame: b'\x08\xC6\x0A\x00\x00\x21\xC9\x01' 2022-07-29 16:23:58 DEBUG (MainThread) [zigpy.zcl] [0x26B0:1:0x0405] Decoded ZCL frame header: ZCLHeader(frame_control=FrameControl(frame_type=, is_manufacturer_specific=0, is_reply=1, disable_default_response=0, reserved=0, *is_cluster=False, *is_general=True), tsn=198, command_id=10, *is_reply=True) 2022-07-29 16:23:58 DEBUG (MainThread) [zigpy.zcl] [0x26B0:1:0x0405] Decoded ZCL frame: RelativeHumidity:Report_Attributes(attribute_reports=[Attribute(attrid=0x0000, value=TypeValue(type=uint16_t, value=457))]) 2022-07-29 16:23:58 DEBUG (MainThread) [zigpy.zcl] [0x26B0:1:0x0405] Received command 0x0A (TSN 198): Report_Attributes(attribute_reports=[Attribute(attrid=0x0000, value=TypeValue(type=uint16_t, value=457))]) 2022-07-29 16:23:58 DEBUG (MainThread) [zigpy.zcl] [0x26B0:1:0x0405] Attribute report received: measured_value=457 2022-07-29 16:23:58 DEBUG (MainThread) [zigpy.zcl] [0x26B0:1:0x0405] Sending reply header: ZCLHeader(frame_control=FrameControl(frame_type=, is_manufacturer_specific=False, is_reply=1, disable_default_response=1, reserved=0, *is_cluster=False, *is_general=True), tsn=198, command_id=, *is_reply=True) 2022-07-29 16:23:58 DEBUG (MainThread) [zigpy.zcl] [0x26B0:1:0x0405] Sending reply: Default_Response(command_id=10, status=) 2022-07-29 16:23:58 DEBUG (MainThread) [homeassistant.core] Bus:Handling , new_state=> 2022-07-29 16:23:58 DEBUG (MainThread) [zigpy_znp.api] Sending request: AF.DataRequestExt.Req(DstAddrModeAddress=AddrModeAddress(mode=, address=0x26B0), DstEndpoint=1, DstPanId=0x0000, SrcEndpoint=1, ClusterId=1029, TSN=198, Options=, Radius=30, Data=b'\x18\xC6\x0B\x0A\x00') 2022-07-29 16:23:58 DEBUG (MainThread) [zigpy_znp.api] Received command: AF.DataRequestExt.Rsp(Status=) ```
javicalle commented 2 years ago

There is a possible quick and dirty quirk for your device:

"""Neo Tuya Temperature, Humidity and Illumination Sensor."""

from zigpy.profiles import zha
from zigpy.profiles.zha import DeviceType
from zigpy.quirks import CustomCluster, CustomDevice
import zigpy.types as t
from zigpy.zcl.clusters.general import Basic, Identify, Ota, PowerConfiguration, Time
from zigpy.zcl.clusters.measurement import (
    RelativeHumidity,
    TemperatureMeasurement,
)

from zhaquirks.const import (
    DEVICE_TYPE,
    ENDPOINTS,
    INPUT_CLUSTERS,
    MODELS_INFO,
    OUTPUT_CLUSTERS,
    PROFILE_ID,
)

class ValueAlarm(t.enum8):
    """Temperature and Humidity alarm values."""

    ALARM_OFF = 0x02
    MAX_ALARM_ON = 0x01
    MIN_ALARM_ON = 0x00

class NeoTemperatureHumidityAlarmCluster(CustomCluster):
    """Neo Temperature and Humidity Alarm Cluster (0xE002)."""

    name = "Neo Temperature and Humidity Alarm Cluster"
    cluster_id = 0xE002

    attributes = {
        # Alarm settings
        0xD00A: ("alarm_temperature_max", t.uint16_t, True),
        0xD00B: ("alarm_temperature_min", t.uint16_t, True),
        0xD00C: ("alarm_humidity_max", t.uint16_t, True),
        0xD00E: ("alarm_humidity_min", t.uint16_t, True),
        # Alarm information
        0xD00F: ("alarm_humidity", ValueAlarm, True),
        0xD006: ("temperature_humidity", ValueAlarm, True),
        # Unknown
        0xD010: ("unknown", t.uint8_t, True),
    }

class RelativeHumidityX10(CustomCluster, RelativeHumidity):
    """Handles invalid values for Humidity."""

    def _update_attribute(self, attrid, value):
        # x10 factor in `measured_value` (attrid=0)
        if attrid == 0:
            value = value * 10
        super()._update_attribute(attrid, value)

class MoesTemperatureHumidtyIlluminanceSensorWithScreen(CustomDevice):
    """Moes Tuya ZigBee Smart Home Temperature And Humidity Sensor With LED screen."""

    signature = {
        #  <SimpleDescriptor endpoint=1, profile=260, device_type="0x0302"
        #  input_clusters=["0x0000", "0x0001", "0x0003", "0x0402", "0x0405", "0xe002"]
        #  output_clusters=["0x0003", "0x000a", "0x0019"]>
        MODELS_INFO: [("_TZ3000_itnrsufe", "TS0201")],
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: DeviceType.LIGHT_SENSOR,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    PowerConfiguration.cluster_id,
                    Identify.cluster_id,
                    TemperatureMeasurement.cluster_id,
                    RelativeHumidity.cluster_id,
                    NeoTemperatureHumidityAlarmCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [
                    Identify.cluster_id,
                    Time.cluster_id,
                    Ota.cluster_id,
                ],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    PowerConfiguration.cluster_id,
                    Identify.cluster_id,
                    TemperatureMeasurement.cluster_id,
                    RelativeHumidityX10,
                    NeoTemperatureHumidityAlarmCluster,
                ],
                OUTPUT_CLUSTERS: [
                    Identify.cluster_id,
                    Time.cluster_id,
                    Ota.cluster_id,
                ],
            },
        },
    }

If you are not familiar, there are some guides about it. Maybe the most popular can be:

You will need to create the ts0201.py file in your local custom_zha_quirks folder.

Save & restart.

Then check if device signature changes, that would mean that the device loads the quirk.

PlusPlus-ua commented 2 years ago

@javicalle thanks, it did the trick. I think it has a sense to merge ts0201_neo.py and ts0201_zemismart.py in one file and add there this new class. What do you think?

javicalle commented 2 years ago

I think it has a sense to merge ts0201_neo.py and ts0201_zemismart.py in one file and add there this new class.

That's exactly the same thing I was thinking. I second to put all classes in the same ts0201.py file. I don't really know if all the attributes from NeoTemperatureHumidityAlarmCluster are usefull, but since cluster ID is the same I would prefer to have just one cluster definition.

PlusPlus-ua commented 2 years ago

I don't really know if all the attributes from NeoTemperatureHumidityAlarmCluster are usefull, but since cluster ID is the same I would prefer to have just one cluster definition.

In Tuya Smart app alarm settings for this device is grayed, but I think it's better to keep it as is.

javicalle commented 2 years ago

Sorry @PlusPlus-ua but I may have misunderstood your intention. Would you mind creating the PR with the new version? I can give you support if you have any questions.

Regards.

PlusPlus-ua commented 2 years ago

Yes, I'll prepare PR. Just have spent rest of weekend with my family, so had no time for it.

javicalle commented 2 years ago

No problem. The important things first! It was just to confirm it.

Thanks.