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
696 stars 641 forks source link

[Device Support Request] TS0601 by _TZE204_nklqjk62 #2533

Open Axel-72 opened 11 months ago

Axel-72 commented 11 months ago

Problem description

I'm new with ZHA, and my first difficulties were this product, it seems to be unsupported at this moment, and i don"t know how to make hard code python for that zigbee device. I don't really know how to do this.

Solution description

I need a python hard code probably

Screenshots/Video

Screenshots/Video [Paste/upload your media here]

image

Device signature

Device signature ```json [Paste the device signature here] "node_descriptor": "NodeDescriptor(logical_type=, omplex_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=False, *is_full_function_device=True, *is_mains_powered=True, *is_receiver_on_when_idle=True, *is_router=True, *is_security_capable=False)", ```

Diagnostic information

Diagnostic information ```json [Paste the diagnostic information here] ```

Logs

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

Custom quirk

Custom quirk ```python [Paste your custom quirk here] ```

Additional information

No response

evlas commented 10 months ago

these quirks seem to work

"""Tuya based cover and blinds.""" from typing import Dict

from zigpy.profiles import zgp, zha from zigpy.quirks import CustomDevice import zigpy.types as t from zigpy.zcl.clusters.general import Basic, GreenPowerProxy, Groups, Ota, Scenes, Time from zigpy.zcl.clusters.security import IasZone

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

from zhaquirks.tuya import NoManufacturerCluster, TuyaLocalCluster from zhaquirks.tuya.mcu import ( DPToAttributeMapping, TuyaMCUCluster, TuyaOnOff, ) from zhaquirks.tuya.ts0601_dimmer import TuyaOnOffNM

ZONE_TYPE = 0x0001

class ContactSwitchCluster(TuyaLocalCluster, IasZone): """Tuya ContactSwitch Sensor."""

_CONSTANT_ATTRIBUTES = {ZONE_TYPE: IasZone.ZoneType.Contact_Switch}

def _update_attribute(self, attrid, value):
    self.debug("_update_attribute '%s': %s", attrid, value)
    super()._update_attribute(attrid, value)

class TuyaGarageManufCluster(NoManufacturerCluster, TuyaMCUCluster): """Tuya garage door opener."""

attributes = TuyaMCUCluster.attributes.copy()
attributes.update(
    {
        # ramdom attribute IDs
        0xEF02: ("dp_2", t.uint32_t, True),
        0xEF04: ("dp_4", t.uint32_t, True),
        0xEF05: ("dp_5", t.uint32_t, True),
        0xEF0B: ("dp_11", t.Bool, True),
        0xEF0C: ("dp_12", t.enum8, True),
    }
)

dp_to_attribute: Dict[int, DPToAttributeMapping] = {
    # garage door trigger ¿on movement, on open, on closed?
    1: DPToAttributeMapping(
        TuyaOnOffNM.ep_attribute,
        "on_off",
    ),
    2: DPToAttributeMapping(
        TuyaMCUCluster.ep_attribute,
        "dp_2",
    ),
    3: DPToAttributeMapping(
        ContactSwitchCluster.ep_attribute,
        "zone_status",
        lambda x: IasZone.ZoneStatus.Alarm_1 if x else 0,
        endpoint_id=2,
    ),
    4: DPToAttributeMapping(
        TuyaMCUCluster.ep_attribute,
        "dp_4",
    ),
    5: DPToAttributeMapping(
        TuyaMCUCluster.ep_attribute,
        "dp_5",
    ),
    11: DPToAttributeMapping(
        TuyaMCUCluster.ep_attribute,
        "dp_11",
    ),
    # garage door status (open, closed, ...)
    12: DPToAttributeMapping(
        TuyaMCUCluster.ep_attribute,
        "dp_12",
    ),
}

data_point_handlers = {
    1: "_dp_2_attr_update",
    2: "_dp_2_attr_update",
    3: "_dp_2_attr_update",
    4: "_dp_2_attr_update",
    5: "_dp_2_attr_update",
    11: "_dp_2_attr_update",
    12: "_dp_2_attr_update",
}

class TuyaGarageSwitchTO(CustomDevice): """Tuya Garage switch."""

signature = {
    MODELS_INFO: [
        ("_TZE200_nklqjk62", "TS0601"),
        ("_TZE200_wfxuhoea", "TS0601"),
        ("_TZE204_nklqjk62", "TS0601"),
    ],
    ENDPOINTS: {
        # <SimpleDescriptor endpoint=1 profile=260 device_type=0x0051
        # input_clusters=[0, 4, 5, 61184]
        # output_clusters=[10, 25]>
        1: {
            PROFILE_ID: zha.PROFILE_ID,
            DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
            INPUT_CLUSTERS: [
                Basic.cluster_id,
                Groups.cluster_id,
                Scenes.cluster_id,
                TuyaGarageManufCluster.cluster_id,
            ],
            OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
        },
        # <SimpleDescriptor endpoint=242 profile=41440 device_type=97
        # input_clusters=[]
        # output_clusters=[33]
        242: {
            PROFILE_ID: zgp.PROFILE_ID,
            DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
            INPUT_CLUSTERS: [],
            OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
        },
    },
}

replacement = {
    ENDPOINTS: {
        1: {
            DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
            INPUT_CLUSTERS: [
                Basic.cluster_id,
                Groups.cluster_id,
                Scenes.cluster_id,
                TuyaGarageManufCluster,
                TuyaOnOffNM,
            ],
            OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
        },
        2: {
            PROFILE_ID: zha.PROFILE_ID,
            DEVICE_TYPE: zha.DeviceType.IAS_ZONE,
            INPUT_CLUSTERS: [
                ContactSwitchCluster
            ],
            OUTPUT_CLUSTERS: [],
        },
        242: {
            PROFILE_ID: zgp.PROFILE_ID,
            DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
            INPUT_CLUSTERS: [],
            OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
        },
    },
}
evlas commented 10 months ago
"""Tuya based cover and blinds."""
from typing import Dict

from zigpy.profiles import zgp, zha
from zigpy.quirks import CustomDevice
import zigpy.types as t
from zigpy.zcl.clusters.general import Basic, GreenPowerProxy, Groups, Ota, Scenes, Time
from zigpy.zcl.clusters.security import IasZone

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

from zhaquirks.tuya import NoManufacturerCluster, TuyaLocalCluster
from zhaquirks.tuya.mcu import (
    DPToAttributeMapping,
    TuyaMCUCluster,
    TuyaOnOff,
)
from zhaquirks.tuya.ts0601_dimmer import TuyaOnOffNM

ZONE_TYPE = 0x0001

class ContactSwitchCluster(TuyaLocalCluster, IasZone):
    """Tuya ContactSwitch Sensor."""

    _CONSTANT_ATTRIBUTES = {ZONE_TYPE: IasZone.ZoneType.Contact_Switch}

    def _update_attribute(self, attrid, value):
        self.debug("_update_attribute '%s': %s", attrid, value)
        super()._update_attribute(attrid, value)

class TuyaGarageManufCluster(NoManufacturerCluster, TuyaMCUCluster):
    """Tuya garage door opener."""

    attributes = TuyaMCUCluster.attributes.copy()
    attributes.update(
        {
            # ramdom attribute IDs
            0xEF02: ("dp_2", t.uint32_t, True),
            0xEF04: ("dp_4", t.uint32_t, True),
            0xEF05: ("dp_5", t.uint32_t, True),
            0xEF0B: ("dp_11", t.Bool, True),
            0xEF0C: ("dp_12", t.enum8, True),
        }
    )

    dp_to_attribute: Dict[int, DPToAttributeMapping] = {
        # garage door trigger ¿on movement, on open, on closed?
        1: DPToAttributeMapping(
            TuyaOnOffNM.ep_attribute,
            "on_off",
        ),
        2: DPToAttributeMapping(
            TuyaMCUCluster.ep_attribute,
            "dp_2",
        ),
        3: DPToAttributeMapping(
            ContactSwitchCluster.ep_attribute,
            "zone_status",
            lambda x: IasZone.ZoneStatus.Alarm_1 if x else 0,
            endpoint_id=2,
        ),
        4: DPToAttributeMapping(
            TuyaMCUCluster.ep_attribute,
            "dp_4",
        ),
        5: DPToAttributeMapping(
            TuyaMCUCluster.ep_attribute,
            "dp_5",
        ),
        11: DPToAttributeMapping(
            TuyaMCUCluster.ep_attribute,
            "dp_11",
        ),
        # garage door status (open, closed, ...)
        12: DPToAttributeMapping(
            TuyaMCUCluster.ep_attribute,
            "dp_12",
        ),
    }

    data_point_handlers = {
        1: "_dp_2_attr_update",
        2: "_dp_2_attr_update",
        3: "_dp_2_attr_update",
        4: "_dp_2_attr_update",
        5: "_dp_2_attr_update",
        11: "_dp_2_attr_update",
        12: "_dp_2_attr_update",
    }

class TuyaGarageSwitchTO(CustomDevice):
    """Tuya Garage switch."""

    signature = {
        MODELS_INFO: [
            ("_TZE200_nklqjk62", "TS0601"),
            ("_TZE200_wfxuhoea", "TS0601"),
            ("_TZE204_nklqjk62", "TS0601"),
        ],
        ENDPOINTS: {
            # <SimpleDescriptor endpoint=1 profile=260 device_type=0x0051
            # input_clusters=[0, 4, 5, 61184]
            # output_clusters=[10, 25]>
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.SMART_PLUG,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaGarageManufCluster.cluster_id,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            # <SimpleDescriptor endpoint=242 profile=41440 device_type=97
            # input_clusters=[]
            # output_clusters=[33]
            242: {
                PROFILE_ID: zgp.PROFILE_ID,
                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                DEVICE_TYPE: zha.DeviceType.ON_OFF_LIGHT,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    Groups.cluster_id,
                    Scenes.cluster_id,
                    TuyaGarageManufCluster,
                    TuyaOnOffNM,
                ],
                OUTPUT_CLUSTERS: [Time.cluster_id, Ota.cluster_id],
            },
            2: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: zha.DeviceType.IAS_ZONE,
                INPUT_CLUSTERS: [
                    ContactSwitchCluster
                ],
                OUTPUT_CLUSTERS: [],
            },
            242: {
                PROFILE_ID: zgp.PROFILE_ID,
                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [GreenPowerProxy.cluster_id],
            },
        },
    }
landaisbenj commented 10 months ago

hello @evlas did you know where can we have some documentation to test this ? (i'm new too with quirks)

dualmacops commented 7 months ago

This works, but it's detected as a light instead of a garage door image

tszdr commented 7 months ago

Paired, quirk seems to be loaded but no enitities shown: image Works fine when setting/reading attributes: image I'm not sure what I did wrong... Any idea I should try/check?

intra-aud commented 6 months ago

I have the exact same issue, I woke up one morning to find the entity unavailable. Removed the working device to do a re-pair in ZHA and now the light entity is missing and the quirk appears to be loaded

Manually writing to the cluster works.

Has anyone else this or have any suggestions?

aussiebish commented 6 months ago

Same issue here, although I've never been able to get the Entities showing. Please help! (also, I'm assuming I don't need local tuya or similiar installed, just using ZHA directly)

I've followed the steps, added the custom ZHA py file, adn config entries, but still no entities. image

image I have the following file entries (but with carriage returns, not sure why they don't display): Configuration.yaml zha: database_path: /config/zigbee.db enable_quirks: true custom_quirks_path: /config/custom_zha_quirks

Folders created: image

and ts0601_garage.py is as #above

And I can see this in the logs: image

I also note, that I can set the value to Bool.False manually by managing the Zigbee device (opposite of 'tszdr's image above, and it opens my garage door).

Now, how do I get a proper entity and button that I can add to a dashboard and/or automation?

intra-aud commented 6 months ago

So i didn't get the entity part working but i did figure out how to call the service directly from HA.

Just as a workaround until someone can input why the entity is missing.

Use the call service command - select "Zigbee Home Automation: Set zigbee cluster attribute"

set the conditions as follows (i'll include the yaml)

service: zha.set_zigbee_cluster_attribute data: ieee: endpoint_id: 1 cluster_id: 61184 attribute: 61185 value: 0

That should allow you to atleast write an automation assuming you can call the attribute directly.

aussiebish commented 6 months ago

Thanks @intra-aud , this works perfectly. Obviously not the ideal solution, but this achieves everything I needed it to on my dashboard. I'm calling the set attribute command from a Script, so that I can just add a 'Run' command to my Dashboard under the existing 'Garage' entity card. I guess we just wait and see if the original tuya entity starts getting fully detected in the future. :)

mfdlr commented 5 months ago

I had the same problem of no entities being detected.

As I was continously updating the file I missed deleting the cache, basically

Clement-B commented 5 months ago

I used the quirks that create a light and binary sensor, it works for me but even if light was a swtich, I was disapointed to not have a real garage door or gate entity.

Downsides:

So, to compensates those inconvenience, I have set up multiple configurations.

In my case, it was for a gate.

Open/close only when turning on

I use a simple automation because I dit not spend much time on this, seems a dirty solution but was a quick fix. I just turn off light/switch just after it has been turned during 1 second.

alias: "Reset state of Garage door / Gate"
description: ""
trigger:
  - platform: state
    entity_id:
      - light.gate # Or switch.garage_door depending of quirks used
    to: "on"
    for:
      hours: 0
      minutes: 0
      seconds: 1
condition: []
action:
  - service: light.turn_off
    target:
      entity_id: light.gate
    data: {}
mode: single

There's certainly a more elegant way to fix this.

State of opening/closing

I created some helpers in HA interface:

Then a simple automation to switch on/off the boolean input depending of the state and duration:

alias: "Update gate status"
description: ""
trigger:
  - platform: state
    entity_id:
      - light.gate
    to: "on"
condition: []
action:
  - if:
      - condition: state
        entity_id: binary_sensor.gate_sensor
        state: "off" # Mean that gate is open in my configuration
    then:
      - service: input_boolean.turn_on
        metadata: {}
        data: {}
        target:
          entity_id: input_boolean.gate_closing
      - delay:
          hours: 0
          minutes: 0
          seconds: "{{ states('input_number.duration_closing_gate') | int }}"
          milliseconds: 0
      - service: input_boolean.turn_off
        target:
          entity_id: input_boolean.gate_closing
        data: {}
    else:
      - service: input_boolean.turn_on
        metadata: {}
        data: {}
        target:
          entity_id: input_boolean.gate_opening
      - delay:
          hours: 0
          minutes: 0
          seconds: "{{ states('input_number.duration_opening_gate') | int }}"
          milliseconds: 0
      - service: input_boolean.turn_off
        metadata: {}
        data: {}
        target:
          entity_id: input_boolean.gate_opening
mode: single

With this automation, I have 2 switchs, one will be on when gate is opening the other one when gate is closing.

Obviously, it's not the more reliable you can have. Multiple calls could mess up those switch states, wind will speed up or slow down closing/opening action, gate security could occurs etc.

Gate/Garage door entity

Finally, I can create a custom entity that I could add to my dashboard. I simply add to my configuration.yaml file a new custom cover entity following HA documentation:

cover:
  - platform: template
    covers:
    gate:
      device_class: gate # can be 'garage' as well
      friendly_name: "Gate (custom)"
      icon_template: >-
          {% if is_state('binary_sensor.gate_sensor', 'on') %}
            mdi:gate
          {% else %}
            mdi:gate-open
          {% endif %}
      value_template: >-
          {% if is_state('input_boolean.gate_opening', 'on') %}
            opening
          {% elif is_state('input_boolean.gate_closing', 'on') %}
            closing
          {% elif is_state('binary_sensor.gate_sensor', 'on') %}
            closed
          {% elif is_state('binary_sensor.gate_sensor', 'off') %}
            open
          {% endif %}
      availability_template: "{{ is_state('binary_sensor.gate_sensor', 'on') or is_state('binary_sensor.gate_sensor', 'off') }}"
      open_cover:
        service: light.turn_on
        data: {}
        target:
          entity_id: light.gate
      close_cover:
        service: light.turn_on
        data: {}
        target:
          entity_id: light.gate

Now I can have a well working card of my gate on my dashboard without any specific card configuration or script calls.

Go further ?

When using this entity in a dashboard, when gate is opening, we will only see a message saying "opening", not really how much time it remain to be fully opened. Like a classic cover, we miss the percentage progression of opening.

I haven't tested it really but it should be pretty easy with the use of attribut position_template instead of value_template in custom entity.

It should looks like something like this:

      position_template: >-
          {% if is_state('input_boolean.gate_opening', 'on') %}
            {{ ((as_timestamp(now()) - as_timestamp(states.input_boolean.gate_opening.last_changed))/states('input_number.duration_opening_gate')|int)|int }} 
          {% elif is_state('input_boolean.gate_closing', 'on') %}
            {{ ((as_timestamp(now()) - as_timestamp(states.input_boolean.gate_closing.last_changed))/states('input_number.duration_closing_gate')|int)|int }} 
          {% elif is_state('binary_sensor.gate_sensor', 'on') %}
            0
          {% elif is_state('binary_sensor.gate_sensor', 'off') %}
            100
          {% endif %}

Hope it can help some people while we wait a fully integrated module in ZHA 🙂

liouma commented 3 months ago

Thanks for the quirk, it's working fine. Turning on the switch activate the physical connection for a second, then it is automaticaly deactivated. This is the expected behaviour for a garage door but not for the device I use. I need a normal on/off switch.

Is this behaviour built in the hardware or is it programmed in the quirk? Do you know if there is a software way to modify it?? Thanks a lot!

noseshimself commented 1 month ago

the built-in quirk is definitely not working and I can't override it:

image

Siress commented 1 month ago

the built-in quirk is definitely not working and I can't override it:

image

I'm having the exact same issue.

image

If I go to 'manage zigbee device', select cluster 'TuyaGarage...', attribute 'contact_sensor (id:0xef03' and click read - it correctly reports false/true depending on if the reed switch has a magnet near it or not. image