jasonacox / tinytuya

Python API for Tuya WiFi smart devices using a direct local area network (LAN) connection or the cloud (TuyaCloud API).
MIT License
868 stars 157 forks source link

Issue about querying status of another device between two threads #440

Closed 3735943886 closed 6 months ago

3735943886 commented 6 months ago

Attempting to retrieve the status of another device in a separate thread, using Device.status() does not behave as intended. It occasionally returns the correct status but with a delay, or fails with an error 914, or just drops. So, what is the optimal approach to obtain the status of another device that is awaiting Device.receive() within another thread loop?

jasonacox commented 6 months ago

Hi @3735943886 , without seeing more of your code, some questions, thoughts and suggestions:

uzlonewolf commented 6 months ago

Only 1 thread can call Device.receive() at a time. Have 1 thread as the designated receiver and call Device.status( nowait=True ) from the other.

3735943886 commented 6 months ago

I’ve attached my code (a reduced version of the dpsconsumer function). I’m monitoring multiple devices and controlling them using multithreading. In the dpsconsumer, when I receive a Zigbee switch click, I aim to toggle another switch. To achieve this, I need to retrieve the status of that switch. However, a problem arises.

TuyaDevices[GetTuyaId('DEVICE NAME')][1] is device object.

I did not tried Device.status( nowait=True ) yet because the automation script is currently running for my home automation and cannot change during daytime. I'll post another reply at midnight. Thank you.

#!/usr/bin/python3
# -*- coding: utf-8 -*-
import json
import time
import queue
import threading
import tinytuya

# Threads data
TuyaThreads = []

# Global arrays for interthread communication
TuyaDevices = {}
TuyaSensors = {}
TuyaIRQueue = queue.Queue()

# Main threads for devices
def TuyaReceiver(tuyainfo, tuyadpscallback):

    # Connect to device
    tuyadev = None

    # Zigbee device
    if tuyainfo['ip'] == '':

        # No loop
        TuyaDevices[tuyainfo['node_id']] = tuyainfo['name'], tuyadev
        return

    # WiFi device
    tuyadev = tinytuya.Device(dev_id = tuyainfo['id'], address = tuyainfo['ip'], local_key = tuyainfo['key'], version = tuyainfo['version'], persist = True)
    tuyadev.send(tuyadev.generate_payload(tinytuya.DP_QUERY))
    TuyaDevices[tuyainfo['id']] = tuyainfo['name'], tuyadev

    while True:

        # Receiving DPS
        dps = tuyadev.receive()
        if dps != None:

            if 'Error' in dps:
                # Connection error

                # TinyTuya does automatically reconnect.
                # Devices can sometimes take a while to re-connect to the WiFi, so if you get that error you can just wait a bit and retry the send/receive.
                time.sleep(5)
                continue

            else:

                # Interprete DPS
                print(tuyainfo['name'], dps)
                tuyadpscallback(tuyadev, dps)

        # Heartbeat
        tuyadev.send(tuyadev.generate_payload(tinytuya.HEART_BEAT))

def DpsConsumer(tuyadev, data):

    # Tuya gateway triggered
    if '게이트웨이' in TuyaDevices[tuyadev.id][0]:
        if 'data' in data:
            if 'cid' in data['data']:

                # Zigbee devices
                # Light scene switch in entry triggered
                if TuyaDevices[data['data']['cid']][0] == '현관스위치':

                    # 1st switch triggered
                    if '1' in data['dps']:

                        # 1st switch triggered then turn on other device
                        TuyaDevices[GetTuyaId('거실조명')][1].set_value(1, True, nowait = True) # WORKS FINE
                        print(TuyaDevices[GetTuyaId('거실조명')][1].status()) # WEIRED BEHAVIORS

if __name__ == "__main__":
    for tuyadevs in json.load(open('devices.json')):
        TuyaThreads.append(threading.Thread(target = TuyaReceiver, args = (tuyadevs, DpsConsumer)))

    # For IR control
    TuyaThreads.append(threading.Thread(target = IRSender))

    # Threading all devices
    for th in TuyaThreads:
        th.start()

    for th in TuyaThreads:
        th.join()
uzlonewolf commented 6 months ago

Like I said, you're going to need to call it as TuyaDevices[GetTuyaId('거실조명')][1].status(nowait=True)

If you need the current status to decide what action to take then I recommend updating DpsConsumer() to store it somewhere instead of calling status() every time.

uzlonewolf commented 6 months ago

I.e.


...
    # WiFi device
    tuyadev = tinytuya.Device(dev_id = tuyainfo['id'], address = tuyainfo['ip'], local_key = tuyainfo['key'], version = tuyainfo['version'], persist = True)
    tuyadev.current_dps = { 'cid': {} }
    tuyadev.send(tuyadev.generate_payload(tinytuya.DP_QUERY))
    TuyaDevices[tuyainfo['id']] = tuyainfo['name'], tuyadev
...

def DpsConsumer(tuyadev, data):
    if 'data' not in data or not data['data']:
        return

    cid = data['cid'] if 'cid' in data and data['cid'] else False

    # save the current dps value in current_dps
    if 'dps' in data['data']:
        if cid:
            # Zigbee device, so store it under 'cid'
            if cid not in tuyadev.current_dps['cid']:
                tuyadev.current_dps['cid'][cid] = {}
            for dp in data['data']['dps']:
                tuyadev.current_dps['cid'][cid][dp] = data['data']['dps'][dp]
        else:
            # not a Zigbee device, so store it directly
            for dp in data['data']['dps']:
                tuyadev.current_dps[dp] = data['data']['dps'][dp]

    # Tuya gateway triggered
    if '게이트웨이' in TuyaDevices[tuyadev.id][0]:
        if cid:

            # Zigbee devices
            # Light scene switch in entry triggered
            if TuyaDevices[cid][0] == '현관스위치':

                # 1st switch triggered
                if '1' in data['dps']:

                    # 1st switch triggered then turn on other device
                    linked_device = TuyaDevices[GetTuyaId('거실조명')][1]
                    linked_device.set_value(1, True, nowait = True) # WORKS FINE

                    if '1' in linked_device.current_dps:
                        # do something based on linked_device.current_dps['1'] ?
                    else:
                        # linked device offline or in an unknown state?
3735943886 commented 6 months ago

Thank you!