jasonacox / tinytuya

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

"[Errno 98] Address already in use" occurs when two clients calls tinytuya.OutletDevice() with address="Auto" #393

Closed yokoyama-flogics closed 1 year ago

yokoyama-flogics commented 1 year ago

Hello,

Thank you for providing the great software.

Today, I found a problem. I'm using tinytuya version 1.12.9 with Python 3.9.11.

So far, I was running a client with the following call:

d = tinytuya.OutletDevice(
    dev_id="xxxxxxxx",
    address="Auto",  # Or set to 'Auto' to auto-discover the IP address
    local_key="yyyyyyyy",
    version=3.3,
)

When I was running one client, it was working fine.

However, today, I added another client using the same approach. After that, both clients started to cause the error very frequently.

[Errno 98] Address already in use

I have two DHCP servers on my network, so the Tuya devices' IP address may change. Therefore, I preferred the "Auto" configuration.

In addition, my client tries to call tinytuya.OutletDevice() to query new data every minute.

Is this idea bad? What are your recommendations or best practices when:

Any suggestions would be appreciated.

Regards, Atsushi

yokoyama-flogics commented 1 year ago

Hello again,

I guess I should avoid calling d = tinytuya.OutletDevice() for every data query and instead reuse the connection.

On the other hand, I should only re-establish the connection if a disconnection is detected when querying by d.status(). What is the recommended way to detect a disconnection after calling d.status()?

I might have overlooked an important description in the documentation. I apologize.

Regards, Atsushi

uzlonewolf commented 1 year ago

That [Errno 98] Address already in use error should be easy to fix by setting SO_REUSEADDR and SO_REUSEPORT on the UDP receiver, but opening a persistent connection and only re-scanning when needed would be better. If the address changes then d.status() would return an error JSON object. Since you're expecting the address to change I'd also reduce the connection attempts and connection timeout to make it give up a bit faster. I'll see if I can whip up an example later tonight or tomorrow.

yokoyama-flogics commented 1 year ago

Thank you for your prompt response.

Based on your suggestion, I modified my code to check the return value of the functions d.status() (or d.turn_on(), etc.) every time as shown below (this is pseudo code):

global d
d = None

def reconnect():
    global d
    while True:
        try:
            d = tinytuya.OutletDevice(
                dev_id="xxxxxxxx",
                address="Auto",
                local_key="yyyyyyyy",
                version=3.3,
            )
            break
        except Exception as e:
            print(e)
            print("Retrying...")
            time.sleep(1)

    d.set_socketRetryDelay(1)
    d.set_socketTimeout(1)

reconnect()  # first connection

while True:
    while True:
        status = d.status()
        if "dps" not in status:
            reconnect()
        else:
            break

    # Do something for d

    time.sleep(10)

I have a couple of questions:

  1. Is the approach outlined above correct?
  2. From what I understand, we need to use both d.set_socketRetryDelay() and d.set_socketTimeout() to minimize timeout delays. Is this understanding accurate?

In my opinion, users of tinytuya will want to maintain a connection to their devices as much as possible. While the above approach might be effective, I believe it's a common need among many users. If the tinytuya library itself could provide this functionality, it might be of great assistance to us.

Regards, Atsushi

uzlonewolf commented 1 year ago

I mean, you could do that, but it's probably one of the worst ways to do it. Most devices will send you asynchronous updates if you open a persistent connection to them, you just need to send a keep-alive every so often (usually <29 seconds). To open a persistent connection, either add persist=True to the Device(...) call, or call d.set_socketPersistent(True).

Looking at it again, I would leave the socket timeout alone and instead reduce the retry limit to 1 with d.set_socketRetryLimit(1).

Also, calls such as d.status() and d.turn_on() may return None, so result checks should be something like if not status or "dps" not in status:. A None return is not an error though, it just means the device did not have any data to send us (such as calling d.turn_on() when it is already on).

Here is what you posted above rewritten to use asynchronous updates. It still polls for status every 30 seconds, though this can be removed if you want. That 2nd while loop can also be eliminated if the "Do something for d" is moved into the final else.

import time
import tinytuya

STATUS_TIMER = 30
KEEPALIVE_TIMER = 12

def reconnect():
    while True:
        try:
            d = tinytuya.OutletDevice(
                dev_id="xxxxxxxx",
                address="Auto",
                local_key="yyyyyyyy",
                #version=3.3, # not required with address="Auto"
                persist=True,
            )
            break
        except Exception as e:
            print(e)
            print("Retrying...")
            time.sleep(1)

    d.set_socketRetryLimit(1)
    return d

d = reconnect()  # first connection
status_time = time.time() + STATUS_TIMER
heartbeat_time = time.time() + KEEPALIVE_TIMER

while True:
    while True:
        if time.time() >= status_time:
            # poll for status
            status = d.status()
            status_time = time.time() + STATUS_TIMER
            heartbeat_time = time.time() + KEEPALIVE_TIMER
        elif time.time() >= heartbeat_time:
            # send a keep-alive
            payload = d.generate_payload(tinytuya.HEART_BEAT)
            status = d.send(payload)
            heartbeat_time = time.time() + KEEPALIVE_TIMER
        else:
            # no need to send anything, just listen for an asynchronous update
            status = d.receive()

        if not status:
            # no data yet, try again
            continue
        elif "Error" in status:
            # error dict, assume the connection has closed on us
            d = reconnect()
            # request status immediately
            status_time = 0
        else:
            # received an update
            # Do something for d?
            break

    # Do something for d

    # do not sleep, let the d.receive() call block
yokoyama-flogics commented 1 year ago

Hi @uzlonewolf,

Thank you for your detailed explanation.

I understand that persist=True is useful for reusing the socket for continuous monitoring of the device, and I appreciate the information that status() may return None.

However, I still don't grasp why frequently calling status() (e.g., every 10 seconds) is less effective than calling send() for a HEART_BEAT every 12 seconds. (It doesn't look so different.)

Could you please explain why d.send() is preferred over _send_receive() in the implementation of status()?

Regards, Atsushi

uzlonewolf commented 1 year ago

It's not that it's preferred, it's just that most people don't need to call status() every 10 seconds and sending HEART_BEAT means they don't need to parse the status() response to look for changed values. If you do want a status() every 10 seconds then it is perfectly acceptable to use it as a keep-alive instead of sending HEART_BEAT.

yokoyama-flogics commented 1 year ago

Hi @uzlonewolf,

OK, I understand that it is acceptable to use status() in this case.

By the way, my application is calling status() every 20 seconds (not actually every 10 seconds). It measures the power consumption of an air conditioner, and I'm getting the following results. Since the power consumption of the air conditioner changes frequently, I believe that measuring it every 20 or 30 seconds is necessary to monitor it precisely.

Visualization by Grafana

In any case, thank you for your answers. I'll consider this topic closed.

Regards, Atsushi

jasonacox commented 1 year ago

Nice graphs @yokoyama-flogics ! Besides Grafana, what are you using to store the data? Do you mind sharing your code?

sjpbailey commented 1 year ago

Hey Jason, His graphics Look exactly like Contemporary Controls inc Dashboard from their BASEdge controller.https://www.ccontrols.com/basautomation/baspiedge.phpOn Aug 10, 2023, at 6:19 PM, Jason Cox @.***> wrote: Nice graphs @yokoyama-flogics ! Besides Grafana, what are you using to store the data? Do you mind sharing your code?

—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you are subscribed to this thread.Message ID: @.***>

yokoyama-flogics commented 1 year ago

@jasonacox,

Of course! I have placed the code below:

The Python code stores data in Elasticsearch (Tuya device reconnection has not been implemented yet). It also publishes to the MQTT broker, though I'm not currently using it for anything specific.

On the other hand, the JSON file is an export from the Grafana dashboard that I created.

Regards, Atsushi

yokoyama-flogics commented 1 year ago

@sjpbailey,

I looked at the website you pointed out. I think the graphs on that site are made with Grafana, although it doesn't seem to be mentioned there.

Regards, Atsushi

yokoyama-flogics commented 1 year ago

FYI,

This isn't directly related to the Tuya topic, but below is a link to another dashboard (which is public).

The Tuya smart plug, which can measure electric power, has been a great discovery for me. I enjoy plotting real-time data for anything and everything. It's a hobby of mine.

Atsushi

jasonacox commented 1 year ago

Nice @yokoyama-flogics ! Thanks for sharing the code and dashboard JSON!

I'm a big fan of Grafana dashboards as well as you can see from my other project, Powerwall-Dashboard. I like your use of ES... makes sense.

sjpbailey commented 1 year ago

Hello Atsushi, Look at the Edge controller, you can add six universal inputs for 0-5vdc, 4-20mA, thermistors, 10k2 etc... You add a touchscreen to the controller and there you can add your Dashboard. Dashboard for the 6 inputs and 6 outputs and if networked BacNet all other inputs and outputs on devices to the dashboard. All for just over $300!https://www.ccontrols.com/pdf/um/BASpi-Edge-user-manual.pdfMaybe this is password protected and you cannot see it here,https://www.ccontrols.com/basautomation/baspiedge.php I have developed some Node Servers for Universal Devices using these controllers and have irrigation, Pool, Garage door controllers also just generic controllers.https://polyglot.universal-devices.com/ Also have some Tuya node server that is Cloud based. Have LED, PIR, Relay boards, 'Curtain, Air circulator'-in development, and a robot vacuum. They would not let me use stored written json files and unfortunately could not run Tinytuya as a Python module.  Yours graphics are very cool! I used to do data centers and high raise buildings mostly in the bay area well west coast and Reno, Arizona with Automated Logic in the 90's.Talk about graphics https://www.automatedlogic.com/en/. Used to take job CAD files and flood fill HVAC zones with different PNG colors.They had tools you would use to link the webpage to the zone and represent it with color as Green is at setpoint.Anyway that my two cents, now disabled from being a pipe fitter for 30 years i hobby around with software now. It would be great if i could call tinytuya with one call to get internal IP addressing device json. However I found it impossible with a year or two versions ago and had to give up. One lad released a node server and everyone could only see his setup however couldn't control it. I am fine with cloud it all depends on your network security just would have rather had local control intranet instead of internet. Cheers dig your everyones hard work here.Bails Out!On Aug 11, 2023, at 2:22 AM, Atsushi Yokoyama @.***> wrote: @sjpbailey, I looked at the website you pointed out. I think the graphs on that site are made with Grafana, although it doesn't seem to be mentioned there. Regards, Atsushi

—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you were mentioned.Message ID: @.***>

yokoyama-flogics commented 1 year ago

@jasonacox,

It's a neat dashboard. I think a lot of information is compactly organized.

By the way, I also tried InfluxDB, but I ended up going back to Elasticsearch.

yokoyama-flogics commented 1 year ago

@sjpbailey,

Thank you for the detailed information and sharing your work experience.

Since childhood, I have been exposed to devices like programmable logic controllers through my family's business, so I have an interest in these kinds of devices. It's also intriguing that such equipment is realized using Raspberry Pi.

Thank you for the information.

sjpbailey commented 1 year ago

Atsushi, Thank You and dig your work!SteveSent from my iPhoneOn Aug 12, 2023, at 4:40 AM, Atsushi Yokoyama @.***> wrote: @sjpbailey, Thank you for the detailed information and sharing your work experience. Since childhood, I have been exposed to devices like programmable logic controllers through my family's business, so I have an interest in these kinds of devices. It's also intriguing that such equipment is realized using Raspberry Pi. Thank you for the information.

—Reply to this email directly, view it on GitHub, or unsubscribe.You are receiving this because you were mentioned.Message ID: @.***>