Async update guidelines

Hi there,

I have written a custom sensor for a platform, and I’d like to code the update part so that it’s asynchronous: the update includes listening to UDP messages sent by the device every 60 seconds, so it is slow by design - and HA correctly issues warnings if I do the update synchronously.

The code is at https://github.com/glpatcern/domotica/blob/master/homeass/code/owlintuition.py

I tried to replace the update() at L117 with async_update() decorated by @asyncio.coroutine, but the net result is that HA started to log many errors of other sensors not updating. With the current code, everything works except the regular HA warnings about my platform taking more than 30 seconds to update…

Any hint/guideline to follow for this? Thanks!

I literally delved in to this two weeks ago! Unfortunately, the docs around this in the HASS developer site are a bit confusing.

Disclaimer: this might not be 100% correct - I’m still getting my head around asyncio myself!

The first thing to remember: you can still block the event loop, which is what is happening on line 192.

You can rewrite using the asyncio Datagram Transport https://docs.python.org/3/library/asyncio-protocol.html

This example is pretty helpful: https://docs.python.org/3/library/asyncio-protocol.html#udp-echo-client-protocol

To set it up in HASS do something like this:

import asyncio

class OwlIntuitionData(object):
    # Bunch of stuff omitted
    def onPacketReceived(self, packet):
        self._xml = ET.fromstring(packet)

class StateUpdater(asyncio.DatagramProtocol):
    def __init__(self, loop, device):
        self.loop = loop
        self.device = device
        self.transport = None

    def connection_made(self, transport):
        self.transport = transport

    def datagram_received(self, data, addr):
        self.device.onPacketReceived(data)

    def wait(self, device):
        return self.device.wait_for_response(device, self.loop)

    def cleanup(self):
        if self.transport:
            self.transport.close()
            self.transport = None

def async_setup_platform(hass, config, async_add_entities, discovery_info=None):
  data = OwlIntuitionData(config)
  coro = hass.loop.create_datagram_endpoint(lambda: StateUpdater(hass.loop, data), local_addr=(ip_address, port))
  return hass.async_add_job(coro)

I hope that helps (or at least points you in the right direction…)

1 Like

Hi! Thanks for the suggestions, looks like a promising direction - and as I also have to properly get my way around asyncio in python, this can be an interesting challenge.
No promises for now but I hope to report back some positive experience.

Hi there,

For the record, I add some time to play around with create_datagram_endpoint() and the direction was right: I have my sensor asynchronously receiving data, and HA does not show any warning any longer :slight_smile:

I’ve put the updated code on my repo (linked above), maybe this is useful for the community.

Thanks again for the tip!

Update, for the benefit of the community: it turns out that appending your own listener/updater class to HA’s main loop leads to race conditions and bad surprises, and using locks or loop.call_soon_threadsafe() does not help either, HA keeps crashing and rebooting itself :frowning:

In lots of cases (but not always…) I get Bad file descriptor errors logged in a very tight loop, quite similarly to the issue below:

https://github.com/home-assistant/home-assistant/issues/15169

I’m back to synch updates for now. I’m sure I don’t understand enough of the core HA engine, but I wonder if there’s any fundamental issue here that makes this approach break.