Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add support for Tuya TS1201 IR blaster #2336

Merged
merged 36 commits into from
Aug 27, 2024
Merged

Add support for Tuya TS1201 IR blaster #2336

merged 36 commits into from
Aug 27, 2024

Conversation

ferehcarb
Copy link
Contributor

@ferehcarb ferehcarb commented Apr 13, 2023

TS1201 is an IR blaster, this quirk is able to send and to receive raw IR codes using Zigbee commands.
Received IR code is available in a Zigbee attribute.
Device is also known as:

  • Moes UFO-R11
  • Aubess ZXZIR-02

Should fix #1687 fix #1782 and fix #1955

@TheJulianJES TheJulianJES added Tuya Request/PR regarding a Tuya device new quirk Adds support for a new device labels Apr 13, 2023
@codecov
Copy link

codecov bot commented Apr 13, 2023

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 88.48%. Comparing base (a257c6c) to head (fd90c39).
Report is 3 commits behind head on dev.

Additional details and impacted files
@@            Coverage Diff             @@
##              dev    #2336      +/-   ##
==========================================
+ Coverage   88.33%   88.48%   +0.15%     
==========================================
  Files         304      305       +1     
  Lines        9497     9624     +127     
==========================================
+ Hits         8389     8516     +127     
  Misses       1108     1108              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

@TheJulianJES TheJulianJES added the missing tests PR has no tests and might need them label Apr 28, 2023
@ferehcarb
Copy link
Contributor Author

@TheJulianJES , I don't know what to test and how to do it.
Can you give some pointers to docs or examples please ?

@PyjamasBeforeChrist
Copy link

PyjamasBeforeChrist commented Jun 23, 2023

@TheJulianJES @ferehcarb did the tests pass on the 6 May per the updated checks? Keen to see this go in so I can use my IR blaster which needs this code to work. Cheers for putting in the effort so the community benefits btw.

@ferehcarb
Copy link
Contributor Author

@TheJulianJES @ferehcarb

RE Tests, something like this? https://github.com/zigpy/zha-device-handlers/pull/2386/files#diff-04503d570ebd62cfb4a36b2204e1acabd7972b973a7ac1ea8d70e15d34305f89

Hi, test for signature is already included in this PR, missing tests are hilighted by codecov, but I don't know what to test.
I'm not familiar with tests and codecov and don't have yet time to dig into it.

@neutrinus
Copy link

As I would love to see it merged, I'm thinking about donating some money to have it finished (as I understand the tests don't cover all new code). If you need I can even order Moes UFO-R11 to you so you can test it onsite.

Is there any nice way to find a willing developer and finalize it?

@tobyworks
Copy link

I would love this aswell!

@darckense
Copy link

Well, I would love this too ! Everything is working fine with ZHA, and I don't really want to move to Z2MQTT.

Comment on lines 55 to 76
attributes = {0x0000: ("last_learned_ir_code", t.CharacterString, True)}

server_commands = {
0x00: foundation.ZCLCommandDef(
"data",
schema={"data": Bytes},
direction=foundation.Direction.Server_to_Client,
is_manufacturer_specific=True,
),
0x01: foundation.ZCLCommandDef(
"IRLearn",
schema={"on_off": t.Bool},
direction=foundation.Direction.Server_to_Client,
is_manufacturer_specific=True,
),
0x02: foundation.ZCLCommandDef(
"IRSend",
schema={"code": t.CharacterString},
direction=foundation.Direction.Server_to_Client,
is_manufacturer_specific=True,
),
}
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, this would be switched to the new zigpy AttributeDefs and ServerCommandDefs (similar to this example).

**kwargs: Any,
):
"""Override the default Cluster command."""
if command_id == 1:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new ServerCommandDefs could be used here (name.id) instead of repeating the command id again here.

Copy link
Collaborator

@TheJulianJES TheJulianJES left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's still on my eventual todo-list to try and add tests for this, but I just haven't found the time for it yet.

Those tests should try to send an "IR command" (that the quirk then converts) and can just test if the "Zigbee messages" (packets) are as expected.
A general orientation can be the Tuya test files. (Maybe something similar to like test_singleswitch_requests)

So, the code in this quirk should really be tested.

@danielguppy
Copy link

danielguppy commented Aug 7, 2024

Same here

Logger: zhaquirks
Source: /usr/local/lib/python3.12/site-packages/zhaquirks/__init__.py:461
First occurred: 9:14:15 AM (1 occurrences)
Last logged: 9:14:15 AM
Unexpected exception importing custom quirk 'ts1201'

Traceback (most recent call last):
  File "/usr/local/lib/python3.12/site-packages/zhaquirks/__init__.py", line 459, in setup
    spec.loader.exec_module(module)
  File "<frozen importlib._bootstrap_external>", line 995, in exec_module
  File "<frozen importlib._bootstrap>", line 488, in _call_with_frames_removed
  File "/config/custom_zha_quirks/ts1201.py", line 377, in <module>
    class ZosungIRBlaster(CustomDevice):
  File "/config/custom_zha_quirks/ts1201.py", line 382, in ZosungIRBlaster
    last_learned_ir_code = t.CharacterString()
                           ^^^^^^^^^^^^^^^^^^^
TypeError: CharacterString.__new__() missing 1 required positional argument: 'value'

@deezid
Copy link

deezid commented Aug 8, 2024

Here is a fixed quirk.

"""Tuya TS1201 IR blaster.

Heavily inspired by work from @mak-42
https://github.com/Koenkk/zigbee-herdsman-converters/blob/9d5e7b902479582581615cbfac3148d66d4c675c/lib/zosung.js
"""
import base64
from typing import Any, Final, Optional, Union

from zigpy.profiles import zgp, zha
from zigpy.quirks import CustomCluster, CustomDevice
import zigpy.types as t
from zigpy.zcl import BaseAttributeDefs, BaseCommandDefs, foundation
from zigpy.zcl.clusters.general import (
    Basic,
    GreenPowerProxy,
    Groups,
    Identify,
    OnOff,
    Ota,
    PowerConfiguration,
    Scenes,
    Time,
)

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


class Bytes(bytes):
    """Bytes serializable class."""

    def serialize(self):
        """Serialize Bytes."""
        return self

    @classmethod
    def deserialize(cls, data):
        """Deserialize Bytes."""
        return cls(data), b""


class ZosungIRControl(CustomCluster):
    """Zosung IR Control Cluster (0xE004)."""

    name = "Zosung IR Control Cluster"
    cluster_id = 0xE004
    ep_attribute = "zosung_ircontrol"

    class AttributeDefs(BaseAttributeDefs):
        """Attribute definitions."""

        last_learned_ir_code: Final = foundation.ZCLAttributeDef(
            id=0x0000, type=t.CharacterString, access="r", mandatory=True
        )

        
    class ServerCommandDefs(BaseCommandDefs):
        """Server command definitions."""

        data: Final = foundation.ZCLCommandDef(
            id=0x00,
            schema={"data": Bytes},
            direction=foundation.Direction.Server_to_Client,
            is_manufacturer_specific=True,
        )
        IRLearn: Final = foundation.ZCLCommandDef(
            id=0x01,
            schema={"on_off": t.Bool},
            direction=foundation.Direction.Server_to_Client,
            is_manufacturer_specific=True,
        )
        IRSend: Final = foundation.ZCLCommandDef(
            id=0x02,
            schema={"code": t.CharacterString},
            direction=foundation.Direction.Server_to_Client,
            is_manufacturer_specific=True,
        )

    async def read_attributes(
        self, attributes, allow_cache=False, only_cache=False, manufacturer=None
    ):
        """Read attributes ZCL foundation command."""
        if (
            self.AttributeDefs.last_learned_ir_code.id in attributes
            or "last_learned_ir_code" in attributes
        ):
            return {0: self.endpoint.device.last_learned_ir_code}, {}
        else:
            return {}, {0: foundation.Status.UNSUPPORTED_ATTRIBUTE}

    async def command(
        self,
        command_id: Union[foundation.GeneralCommand, int, t.uint8_t],
        *args,
        manufacturer: Optional[Union[int, t.uint16_t]] = None,
        expect_reply: bool = True,
        tsn: Optional[Union[int, t.uint8_t]] = None,
        **kwargs: Any,
    ):
        """Override the default Cluster command."""
        if command_id == self.ServerCommandDefs.IRLearn.id:
            if kwargs["on_off"]:
                cmd_args = {Bytes(b'{"study":0}')}
            else:
                cmd_args = {Bytes(b'{"study":1}')}
            return await super().command(
                0x00,
                *cmd_args,
                manufacturer=manufacturer,
                expect_reply=True,
                tsn=tsn,
            )
        elif command_id == self.ServerCommandDefs.IRSend.id:
            ir_msg = f"""{{"key_num":1,"delay":300,"key1":{{"num":1,"freq":38000,"type":1,"key_code":"{kwargs["code"]}"}}}}"""
            self.debug("ir_msg to send: %s", ir_msg)
            seq = self.endpoint.device.next_seq()
            self.endpoint.device.ir_msg_to_send = {seq: ir_msg}
            self.create_catching_task(
                self.endpoint.zosung_irtransmit.command(
                    0x00,
                    seq=seq,
                    length=len(ir_msg),
                    unk1=0x00000000,
                    clusterid=0xE004,
                    unk2=0x01,
                    cmd=0x02,
                    unk3=0x0000,
                    expect_reply=False,
                    tsn=tsn,
                )
            )
        else:
            return await super().command(
                command_id,
                *args,
                manufacturer=manufacturer,
                expect_reply=expect_reply,
                tsn=tsn,
            )


class ZosungIRTransmit(CustomCluster):
    """Zosung IR Transmit Cluster (0xED00)."""

    name = "Zosung IR Transmit Cluster"
    cluster_id = 0xED00
    ep_attribute = "zosung_irtransmit"

    current_position = 0
    msg_length = 0
    ir_msg = []

    class ServerCommandDefs(BaseCommandDefs):
        """Server command definitions."""

        receive_ir_frame_00: Final = foundation.ZCLCommandDef(
            id=0x00,
            schema={
                "seq": t.uint16_t,
                "length": t.uint32_t,
                "unk1": t.uint32_t,
                "clusterid": t.uint16_t,
                "unk2": t.uint8_t,
                "cmd": t.uint8_t,
                "unk3": t.uint16_t,
            },
            direction=foundation.Direction.Server_to_Client,
            is_manufacturer_specific=True,
        )
        receive_ir_frame_01: Final = foundation.ZCLCommandDef(
            id=0x01,
            schema={
                "zero": t.uint8_t,
                "seq": t.uint16_t,
                "length": t.uint32_t,
                "unk1": t.uint32_t,
                "clusterid": t.uint16_t,
                "unk2": t.uint8_t,
                "cmd": t.uint8_t,
                "unk3": t.uint16_t,
            },
            direction=foundation.Direction.Server_to_Client,
            is_manufacturer_specific=True,
        )
        receive_ir_frame_02: Final = foundation.ZCLCommandDef(
            id=0x02,
            schema={
                "seq": t.uint16_t,
                "position": t.uint32_t,
                "maxlen": t.uint8_t,
            },
            direction=foundation.Direction.Server_to_Client,
            is_manufacturer_specific=True,
        )
        receive_ir_frame_03: Final = foundation.ZCLCommandDef(
            id=0x03,
            schema={
                "zero": t.uint8_t,
                "seq": t.uint16_t,
                "position": t.uint32_t,
                "msgpart": t.LVBytes,
                "msgpartcrc": t.uint8_t,
            },
            direction=foundation.Direction.Client_to_Server,
            is_manufacturer_specific=False,
        )
        receive_ir_frame_04: Final = foundation.ZCLCommandDef(
            id=0x04,
            schema={
                "zero0": t.uint8_t,
                "seq": t.uint16_t,
                "zero1": t.uint16_t,
            },
            direction=foundation.Direction.Server_to_Client,
            is_manufacturer_specific=True,
        )
        receive_ir_frame_05: Final = foundation.ZCLCommandDef(
            id=0x05,
            schema={
                "seq": t.uint16_t,
                "zero": t.uint16_t,
            },
            direction=foundation.Direction.Server_to_Client,
            is_manufacturer_specific=True,
        )

    class ClientCommandDefs(BaseCommandDefs):
        """Client command definitions."""

        resp_ir_frame_03: Final = foundation.ZCLCommandDef(
            id=0x03,
            schema={
                "zero": t.uint8_t,
                "seq": t.uint16_t,
                "position": t.uint32_t,
                "msgpart": t.LVBytes,
                "msgpartcrc": t.uint8_t,
            },
            direction=foundation.Direction.Client_to_Server,
            is_manufacturer_specific=False,
        )
        resp_ir_frame_05: Final = foundation.ZCLCommandDef(
            id=0x05,
            schema={
                "seq": t.uint16_t,
                "zero": t.uint16_t,
            },
            direction=foundation.Direction.Server_to_Client,
            is_manufacturer_specific=True,
        )

    def handle_cluster_request(
        self,
        hdr: foundation.ZCLHeader,
        args: list[Any],
        *,
        dst_addressing: Optional[
            Union[t.Addressing.Group, t.Addressing.IEEE, t.Addressing.NWK]
        ] = None,
    ):
        """Handle a cluster request."""

        # send default response, so avoid repeated zclframe from device
        if not hdr.frame_control.disable_default_response:
            self.debug("Send default response")
            self.send_default_rsp(hdr, status=foundation.Status.SUCCESS)

        if hdr.command_id == self.ServerCommandDefs.receive_ir_frame_00.id:
            self.debug("hdr.command_id == 0x00")

            self.current_position = 0
            self.ir_msg.clear()
            self.msg_length = args.length

            cmd_01_args = {
                "zero": 0,
                "seq": args.seq,
                "length": args.length,
                "unk1": args.unk1,
                "clusterid": args.clusterid,
                "unk2": args.unk2,
                "cmd": args.cmd,
                "unk3": args.unk3,
            }
            self.create_catching_task(
                super().command(0x01, **cmd_01_args, expect_reply=True)
            )
            cmd_02_args = {"seq": args.seq, "position": 0, "maxlen": 0x38}
            self.create_catching_task(
                super().command(0x02, **cmd_02_args, expect_reply=True)
            )
        elif hdr.command_id == self.ServerCommandDefs.receive_ir_frame_01.id:
            self.debug("IR-Message-Code01 received, sequence: %s", args.seq)
            self.debug("msg to send: %s", self.endpoint.device.ir_msg_to_send[args.seq])
        elif hdr.command_id == self.ServerCommandDefs.receive_ir_frame_02.id:
            position = args.position
            seq = args.seq
            maxlen = args.maxlen
            irmsg = self.endpoint.device.ir_msg_to_send[seq]
            msgpart = irmsg[position : position + maxlen]
            calculated_crc = 0
            for x in msgpart:
                calculated_crc = (calculated_crc + ord(x)) % 0x100
            self.debug(
                "hdr.command_id == 0x02 ; msgcrc=%s ; position=%s ; msgpart=%s",
                calculated_crc,
                position,
                msgpart,
            )
            cmd_03_args = {
                "zero": 0,
                "seq": seq,
                "position": position,
                "msgpart": msgpart.encode("utf-8"),
                "msgpartcrc": calculated_crc,
            }
            self.create_catching_task(
                super().command(0x03, **cmd_03_args, expect_reply=True)
            )
        elif hdr.command_id == self.ServerCommandDefs.receive_ir_frame_03.id:
            msg_part_crc = args.msgpartcrc
            calculated_crc = 0
            for x in args.msgpart:
                calculated_crc = (calculated_crc + x) % 0x100
            self.debug(
                "hdr.command_id == 0x03 ; msgcrc=%s ; calculated_crc=%s ; position=%s",
                msg_part_crc,
                calculated_crc,
                args.position,
            )
            self.ir_msg[args.position :] = args.msgpart
            if args.position + len(args.msgpart) < self.msg_length:
                cmd_02_args = {
                    "seq": args.seq,
                    "position": args.position + len(args.msgpart),
                    "maxlen": 0x38,
                }
                self.create_catching_task(
                    super().command(0x02, **cmd_02_args, expect_reply=False)
                )
            else:
                self.debug("Ir message totally received.")
                cmd_04_args = {"zero0": 0, "seq": args.seq, "zero1": 0}
                self.create_catching_task(
                    super().command(0x04, **cmd_04_args, expect_reply=False)
                )
        elif hdr.command_id == self.ServerCommandDefs.receive_ir_frame_04.id:
            seq = args.seq
            self.debug("Command 0x04: IRCode has been successfully sent. (seq:%s)", seq)
            cmd_05_args = {"seq": seq, "zero": 0}
            self.create_catching_task(
                super().command(0x05, **cmd_05_args, expect_reply=False)
            )
        elif hdr.command_id == self.ServerCommandDefs.receive_ir_frame_05.id:
            self.endpoint.device.last_learned_ir_code = base64.b64encode(
                bytes(self.ir_msg)
            ).decode()
            self.info(
                "Command 0x05: Ir message really totally received: %s",
                self.endpoint.device.last_learned_ir_code,
            )
            self.debug("Stopping learning mode on device.")
            self.create_catching_task(
                self.endpoint.zosung_ircontrol.command(
                    0x01, on_off=False, expect_reply=False
                )
            )
        else:
            self.debug("hdr.command_id: %s", hdr.command_id)


class ZosungIRBlaster(CustomDevice):
    """Zosung IR Blaster."""

    seq = -1
    ir_msg_to_send = {}
    last_learned_ir_code = t.CharacterString(value="")

    def __init__(self, *args, **kwargs):
        """Init device."""
        self.seq = 0
        super().__init__(*args, **kwargs)

    def next_seq(self):
        """Next local sequence."""
        self.seq = (self.seq + 1) % 0x10000
        return self.seq

    signature = {
        # "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.EndDevice: 2>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress: 128>, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *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)",
        # input_clusters=[0x0000, 0x0001, 0x0003, 0x0004, 0x0005, 0x0006,
        #                 0xe004, 0xed00]
        # output_clusters=[0x000a, 0x0019]
        #  <SimpleDescriptor endpoint=1, profile=260, device_type=61440
        #  device_version=1
        #  input_clusters=[0, 1, 3, 4, 5, 6, 57348, 60672]
        #  output_clusters=[10, 25]>
        MODELS_INFO: [
            ("_TZ3290_ot6ewjvmejq5ekhl", "TS1201"),
            ("_TZ3290_j37rooaxrcdcqo5n", "TS1201"),
        ],
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: 0xF000,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    ZosungIRTransmit.cluster_id,
                    ZosungIRControl.cluster_id,
                    Groups.cluster_id,
                    Identify.cluster_id,
                    OnOff.cluster_id,
                    PowerConfiguration.cluster_id,
                    Scenes.cluster_id,
                ],
                OUTPUT_CLUSTERS: [
                    Time.cluster_id,
                    Ota.cluster_id,
                ],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    ZosungIRTransmit,
                    ZosungIRControl,
                    Groups.cluster_id,
                    Identify.cluster_id,
                    OnOff.cluster_id,
                    PowerConfiguration.cluster_id,
                    Scenes.cluster_id,
                ],
                OUTPUT_CLUSTERS: [
                    Time.cluster_id,
                    Ota.cluster_id,
                ],
            },
        },
    }


class ZosungIRBlaster_ZS06(ZosungIRBlaster):
    """Zosung IR Blaster ZS06."""

    signature = {
        #   "node_descriptor": "NodeDescriptor(logical_type=<LogicalType.Router: 1>, complex_descriptor_available=0, user_descriptor_available=0, reserved=0, aps_flags=0, frequency_band=<FrequencyBand.Freq2400MHz: 8>, mac_capability_flags=<MACCapabilityFlags.AllocateAddress|RxOnWhenIdle|MainsPowered|FullFunctionDevice: 142>, manufacturer_code=4098, maximum_buffer_size=82, maximum_incoming_transfer_size=82, server_mask=11264, maximum_outgoing_transfer_size=82, descriptor_capability_field=<DescriptorCapability.NONE: 0>, *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)",
        #  <SimpleDescriptor endpoint=1, profile=260, device_type=61440
        #  device_version=1
        #  input_clusters=[0, 3, 4, 5, 6, 57348, 60672]
        #  output_clusters=[10, 25]>
        #  <SimpleDescriptor endpoint=242, profile=41440, device_type=97
        #  device_version=1
        #  input_clusters=[]
        #  output_clusters=[33]>
        MODELS_INFO: [
            ("_TZ3290_7v1k4vufotpowp9z", "TS1201"),
            ("_TZ3290_acv1iuslxi3shaaj", "TS1201"),
            ("_TZ3290_gnl5a6a5xvql7c2a", "TS1201"),
        ],
        ENDPOINTS: {
            1: {
                PROFILE_ID: zha.PROFILE_ID,
                DEVICE_TYPE: 0xF000,
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    ZosungIRTransmit.cluster_id,
                    ZosungIRControl.cluster_id,
                    Groups.cluster_id,
                    Identify.cluster_id,
                    OnOff.cluster_id,
                    Scenes.cluster_id,
                ],
                OUTPUT_CLUSTERS: [
                    Time.cluster_id,
                    Ota.cluster_id,
                ],
            },
            242: {
                PROFILE_ID: zgp.PROFILE_ID,
                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [
                    GreenPowerProxy.cluster_id,
                ],
            },
        },
    }

    replacement = {
        ENDPOINTS: {
            1: {
                INPUT_CLUSTERS: [
                    Basic.cluster_id,
                    ZosungIRTransmit,
                    ZosungIRControl,
                    Groups.cluster_id,
                    Identify.cluster_id,
                    OnOff.cluster_id,
                    Scenes.cluster_id,
                ],
                OUTPUT_CLUSTERS: [
                    Time.cluster_id,
                    Ota.cluster_id,
                ],
            },
            242: {
                PROFILE_ID: zgp.PROFILE_ID,
                DEVICE_TYPE: zgp.DeviceType.PROXY_BASIC,
                INPUT_CLUSTERS: [],
                OUTPUT_CLUSTERS: [
                    GreenPowerProxy.cluster_id,
                ],
            },
        },
    }

Download
ts1201.py.zip

@ehedlund76
Copy link

ehedlund76 commented Aug 8, 2024

Hi @ferehcarb

Have you looked in to the keyerror issue yet?
#2336 (comment)

I get a lot if these at times, in particular when I'm testing my IR-codes and send a lot of them without delays betwen or only short delays like 1 sec. I've got an automation that runs through all combinations of hvac-modes, presets, fan-modes and temperatures for my climate configuration. The first 50-100 calls (zha.issue_zigbee_cluster_command) normally goes well, but then the keyerrors starts to show up, and almost for every call:

Traceback (most recent call last):
  File "/usr/local/lib/python3.12/site-packages/bellows/ezsp/__init__.py", line 505, in handle_callback
    handler(*args)
  File "/usr/local/lib/python3.12/site-packages/bellows/zigbee/application.py", line 475, in ezsp_callback_handler
    self._handle_frame(
  File "/usr/local/lib/python3.12/site-packages/bellows/zigbee/application.py", line 558, in _handle_frame
    self.packet_received(
  File "/usr/local/lib/python3.12/site-packages/zigpy/application.py", line 1024, in packet_received
    return device.packet_received(packet)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/zigpy/device.py", line 497, in packet_received
    endpoint.handle_message(
  File "/usr/local/lib/python3.12/site-packages/zigpy/endpoint.py", line 247, in handle_message
    handler(hdr, args, dst_addressing=dst_addressing)
  File "/usr/local/lib/python3.12/site-packages/zigpy/zcl/__init__.py", line 430, in handle_message
    self.handle_cluster_request(hdr, args, dst_addressing=dst_addressing)
  File "/config/custom_zha_quirks/ts1201.py", line 304, in handle_cluster_request
    irmsg = self.endpoint.device.ir_msg_to_send[seq]
            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
KeyError: 75

It seems like something is building up and then start to fail.

@gromgsxr
Copy link
Contributor

gromgsxr commented Aug 8, 2024

Hi @ferehcarb

Have you looked in to the keyerror issue yet? #2336 (comment)

I get a lot if these at times, in particular when I'm testing my IR-codes and send a lot of them without or short delays. I've got an automation that runs through all combinations of hvac-modes, presets, fan-modes and temperatures for my climate configuration. The first 50-100 calls (zha.iisue normally goes well, but then the keyerrors starts to show up, and almost for every call: Traceback (most recent call last): File "/usr/local/lib/python3.12/site-packages/bellows/ezsp/init.py", line 505, in handle_callback handler(*args) File "/usr/local/lib/python3.12/site-packages/bellows/zigbee/application.py", line 475, in ezsp_callback_handler self._handle_frame( File "/usr/local/lib/python3.12/site-packages/bellows/zigbee/application.py", line 558, in _handle_frame self.packet_received( File "/usr/local/lib/python3.12/site-packages/zigpy/application.py", line 1024, in packet_received return device.packet_received(packet) ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ File "/usr/local/lib/python3.12/site-packages/zigpy/device.py", line 497, in packet_received endpoint.handle_message( File "/usr/local/lib/python3.12/site-packages/zigpy/endpoint.py", line 247, in handle_message handler(hdr, args, dst_addressing=dst_addressing) File "/usr/local/lib/python3.12/site-packages/zigpy/zcl/init.py", line 430, in handle_message self.handle_cluster_request(hdr, args, dst_addressing=dst_addressing) File "/config/custom_zha_quirks/ts1201.py", line 304, in handle_cluster_request irmsg = self.endpoint.device.ir_msg_to_send[seq]

It seems like something is building up and then start to fail.

I can't send commands even sending one code I get the error the item it totally unusable

Error doing job: Exception in callback SerialTransport._read_ready() (None)
Traceback (most recent call last):
  File "/usr/local/lib/python3.12/asyncio/events.py", line 88, in _run
    self._context.run(self._callback, *self._args)
  File "/usr/local/lib/python3.12/site-packages/serial_asyncio_fast/__init__.py", line 146, in _read_ready
    self._protocol.data_received(data)
  File "/usr/local/lib/python3.12/site-packages/zigpy_xbee/uart.py", line 95, in data_received
    self.frame_received(frame)
  File "/usr/local/lib/python3.12/site-packages/zigpy_xbee/uart.py", line 103, in frame_received
    self._api.frame_received(frame)
  File "/usr/local/lib/python3.12/site-packages/zigpy_xbee/api.py", line 392, in frame_received
    getattr(self, f"_handle_{command}")(*data)
  File "/usr/local/lib/python3.12/site-packages/zigpy_xbee/api.py", line 451, in _handle_explicit_rx_indicator
    self._app.handle_rx(ieee, nwk, src_ep, dst_ep, cluster, profile, rx_opts, data)
  File "/usr/local/lib/python3.12/site-packages/zigpy_xbee/zigbee/application.py", line 382, in handle_rx
    self.packet_received(
  File "/usr/local/lib/python3.12/site-packages/zigpy/application.py", line 1024, in packet_received
    return device.packet_received(packet)
           ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
  File "/usr/local/lib/python3.12/site-packages/zigpy/device.py", line 497, in packet_received
    endpoint.handle_message(
  File "/usr/local/lib/python3.12/site-packages/zigpy/endpoint.py", line 247, in handle_message
    handler(hdr, args, dst_addressing=dst_addressing)
  File "/usr/local/lib/python3.12/site-packages/zigpy/zcl/__init__.py", line 430, in handle_message
    self.handle_cluster_request(hdr, args, dst_addressing=dst_addressing)
  File "/config/custom_zha_quirks/ts1201.py", line 305, in handle_cluster_request
    irmsg = self.endpoint.device.ir_msg_to_send[seq]
            ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~^^^^^
KeyError: 3

@Harrod200
Copy link

Harrod200 commented Aug 8, 2024

Here is a fixed quirk.
...
Download ts1201.py.zip

Perfect fix for me; dropped in, reloaded, all my old automations work again. <3

pyproject.toml Outdated Show resolved Hide resolved
@TheJulianJES TheJulianJES removed the missing tests PR has no tests and might need them label Aug 11, 2024
@sjors-lemniscap
Copy link

sjors-lemniscap commented Aug 20, 2024

Quickly went over the last 3 commits from @ferehcarb and it looks like the inline codespell has been applied as well as some formatting fixes (instead of the ruff/codespell hack from before).

@TheJulianJES do you have any other feedback?

@sjors-lemniscap
Copy link

Quickly went over the last 3 commits from @ferehcarb and it looks like the inline codespell has been applied as well as some formatting fixes (instead of the ruff/codespell hack from before).

@TheJulianJES do you have any other feedback?

I'm running the dev branch for a week now and seems to be working fine. Weirdly enough some of the IR commands that I had previously "learned" seem to not work anymore and I had to re-learn them. That's when I figured the IR command changed while the remote it learned the command from is the same. FWIW below the old and new IR command as example:

Old command: CzIj7BEUAssGFAJkAoADwAtAB0AD4AMXwA/AB0AbQAtAAUAH4BMDQAFAL0ADQAELZAIUAmQCFAJkAhQC
New command: CXAjpxFCAokGQgLAAcALQAcEiQZjAkKgAQGJBoADQAFAC8ABwAtAB+ATA+AQAQICQgI=

@TheJulianJES TheJulianJES added smash This PR is close to be merged soon and removed needs review This PR should be reviewed soon, as it generally looks good. labels Aug 26, 2024
This replaces the zigpy logger with a custom quirks logger whilst also logging the IEEE of the device.
The formatting is also slightly changed to split long lines into multiple.
Copy link
Collaborator

@TheJulianJES TheJulianJES left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All right, I think we can try this. I made some small adjustments that shouldn't break anything, mostly just logging and slightly polishing it up a bit.

Fake clusters aren't ideal, but most Tuya devices already use them and there's nothing we can do to avoid them at moment, so this should be fine.

Thanks for all your work!

@TheJulianJES TheJulianJES merged commit dfab1cc into zigpy:dev Aug 27, 2024
7 checks passed
@gromgsxr
Copy link
Contributor

@TheJulianJES any luck with looking at #2336 (comment)

this quirk still does not work properly i can use it to learn codes but it never seems to send any

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
new quirk Adds support for a new device smash This PR is close to be merged soon Tuya Request/PR regarding a Tuya device
Projects
None yet