Writing asyncio compatible plugins

Posted on
Sat Jun 25, 2022 1:54 pm
FlyingDiver offline
User avatar
Posts: 7189
Joined: Jun 07, 2014
Location: Southwest Florida, USA

Writing asyncio compatible plugins

Here's an overview of how I'm doing plugins using asyncio libraries. I don't guarantee that these techniques will work in all cases, but so far they're working for me.

The simple case is a library that requires you to call asyncio co-routines from synchronous code, but where you don't mind blocking on the async function call, just like you do for a sync call. In that case, you can just do this anytime you need to call one of those routines:

Code: Select all
asyncio.run(lib.function(args))


A slightly more complicated case is when you need to do a login, or some error checking, etc, in the async call. From the latest MyQ plugin (edited):

Code: Select all
        if action.deviceAction == indigo.kDeviceAction.Unlock:
            self.logger.debug(f"actionControlDevice: Unlock {dev.name}")
            asyncio.run(self.pymyq_open(dev.address))

###################################

    async def pymyq_open(self, myqid):
        async with ClientSession() as web_session:
            try:
                api = await login(self.pluginPrefs['myqLogin'], self.pluginPrefs['myqPassword'], web_session)
            except MyQError as err:
                self.logger.warning(f"Error logging into MyQ server: {err}")
                return
            device = api.devices[myqid]
            try:
                wait_task = await device.open(wait_for_state=False)
            except MyQError as err:
                self.logger.error(f"Error trying to open '{device.name}': {err}")
                return


In both of these cases, the code is blocking until the async function(s) are complete.

More complicated scenarios in following posts...
Last edited by FlyingDiver on Sat Jun 25, 2022 1:56 pm, edited 1 time in total.

joe (aka FlyingDiver)
my plugins: http://forums.indigodomo.com/viewforum.php?f=177

Posted on
Sat Jun 25, 2022 1:55 pm
FlyingDiver offline
User avatar
Posts: 7189
Joined: Jun 07, 2014
Location: Southwest Florida, USA

Re: Writing asyncio compatible plugins

Many plugins that communicate with cloud services or hardware devices receive data asynchronously. Before asyncio, that meant either polling the device constantly, or using a background thread to maintain a websocket or similar communication channel. In general, doing the same thing with asyncio is actually easier, except for the effort of integrating it into the synchronous plugin architecture. Here's how I'm doing it (this is from the new Harmony Hub plugin).

First, the asyncio event loop needs to be set up in it's own Thread. Initially I was doing this using runConcurrentThread() but that didn't work out. The problem was that thread doesn't get started until after the plugin devices are started, and if you need asyncio to start up the devices, well, you see the problem. So:

Code: Select all
    def __init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs):
        indigo.PluginBase.__init__(self, pluginId, pluginDisplayName, pluginVersion, pluginPrefs)
###
        self._event_loop = None
        self._async_thread = None

    def startup(self):
        self._event_loop = asyncio.new_event_loop()
        asyncio.set_event_loop(self._event_loop)
        self._async_thread = threading.Thread(target=self._run_async_thread)
        self._async_thread.start()


    def _run_async_thread(self):
        self.logger.debug("_run_async_thread starting")
        self._event_loop.create_task(self._async_start())
        self._event_loop.run_until_complete(self._async_stop())
        self._event_loop.close()

    async def _async_start(self):
        self.logger.debug("_async_start")
        # add things you need to do at the start of the plugin here

    async def _async_stop(self):
        while True:
            await asyncio.sleep(1.0)
            if self.stopThread:
                break


At this point, there's a running asyncio event loop just waiting for things to do. Add code to _async_start() as needed. You see that the event loop will continue to run until _async_stop() exits, which is done when self.stopThread is set. Cleanup code can be added before the break. Just make sure it'll never block.

Additional tasks can be added to the event loop as needed, for example:
Code: Select all
    def deviceStartComm(self, device):
        if device.deviceTypeId == "harmonyHub":
            self._event_loop.create_task(self._async_start_device(device))

    async def _async_start_device(self, device):
        client = HarmonyAPI(ip_address=device.address, protocol=self.protocol)
        try:
            connected = await client.connect()
        except ConnectionRefusedError as e:
            self.logger.debug(f"{device.name}: connect exception: {e}.")
            return

As written, this does NOT do any cleanup on existing asyncio Tasks. I do some of that other places, like:
Code: Select all
    def deviceStopComm(self, device):
        if device.deviceTypeId == "harmonyHub":
            self._event_loop.create_task(self._async_stop_device(device.address))

    async def _async_stop_device(self, ip_address):
        hub_client = self._async_running_clients[ip_address]
        try:
            await asyncio.wait_for(hub_client.close(), timeout=5)
        except aioharmony.exceptions.TimeOut:
            self.logger.debug(f"{hub_client.name} HUB: Timeout trying to close connection.")
Last edited by FlyingDiver on Sat Jun 25, 2022 2:17 pm, edited 2 times in total.

joe (aka FlyingDiver)
my plugins: http://forums.indigodomo.com/viewforum.php?f=177

Posted on
Sat Jun 25, 2022 1:55 pm
FlyingDiver offline
User avatar
Posts: 7189
Joined: Jun 07, 2014
Location: Southwest Florida, USA

Re: Writing asyncio compatible plugins

Reserved #2

joe (aka FlyingDiver)
my plugins: http://forums.indigodomo.com/viewforum.php?f=177

Posted on
Sat Jun 25, 2022 1:55 pm
FlyingDiver offline
User avatar
Posts: 7189
Joined: Jun 07, 2014
Location: Southwest Florida, USA

Re: Writing asyncio compatible plugins

Reserved #3

joe (aka FlyingDiver)
my plugins: http://forums.indigodomo.com/viewforum.php?f=177

Page 1 of 1

Who is online

Users browsing this forum: No registered users and 11 guests