johannchopin
johannchopin

Reputation: 14844

Use asyncio with non asynchrone callback method from external library

I'm using the gpiozero python library to handle simple GPIO devices on a Raspberry Pi (I use here a MotionSensor for the example):

import asyncio
from gpiozero import MotionSensor


class MotionSensorHandler():
    __whenMotionCallback = None

    def __init__(self, pin, whenMotionCallback):
        # whenMotionCallback is an async function
        self.__whenMotionCallback = whenMotionCallback

        # Just init the sensor with gpiozero lib
        motionSensor = MotionSensor(pin)

        # Method to call when motion is detected
        motionSensor.when_motion = self.whenMotion

    async def whenMotion(self):
        await self.__whenMotionCallback()

My problem here is that I tried to give an async function has callback to motionSensor.when_motion.

So I get the error that whenMotion function is async but never await but I actually can't await it:

# will not work because MotionSensor() is not using asyncio
motionSensor.when_motion = await self.whenMotion

Do you have any idea how I can assign my async function to a none one ?

Upvotes: 1

Views: 1606

Answers (4)

lbt
lbt

Reputation: 814

When the when_motion property is set gpiozero creates a new thread which executes the callback (this isn't documented very well). If the callback should be executed in the main asyncio loop then you need to pass control back to the main thread.

The call_soon_threadsafe method does that for you. Essentially it adds the callback to the list of tasks the main asyncio loop calls when an await happens.

However asyncio loops are local to each thread: see get_running_loop

So when the gpiozero object is created in the main asyncio thread then you need make that loop object available to the object when the callback is called.

Here's how I do that for a PIR that calls an asyncio MQTT method:

class PIR:
    def __init__(self, mqtt, pin):
        self.pir = MotionSensor(pin=pin)
        self.pir.when_motion = self.motion
        # store the mqtt client we'll need to call
        self.mqtt = mqtt
        # This PIR object is created in the main thread
        # so store that loop object
        self.loop = asyncio.get_running_loop()

    def motion(self):
        # motion is called in the gpiozero monitoring thread
        # it has to use our stored copy of the loop and then
        # tell that loop to call the callback:
        self.loop.call_soon_threadsafe(self.mqtt.publish,
                                       f'sensor/gpiod/pir/kitchen', True)

Upvotes: 2

johannchopin
johannchopin

Reputation: 14844

So after research I found that I have to create a new asyncio loop to execute asynchronous script in a no-asynchronous method. So now my whenMotion() method is no longer async but execute one using ensure_future().

import asyncio
from gpiozero import MotionSensor


class MotionSensorHandler():
    __whenMotionCallback = None

    def __init__(self, pin, whenMotionCallback):
        # whenMotionCallback is an async function
        self.__whenMotionCallback = whenMotionCallback

        # Just init the sensor with gpiozero lib
        motionSensor = MotionSensor(pin)

        # Method to call when motion is detected
        motionSensor.when_motion = self.whenMotion

    def whenMotion(self):
        # Create new asyncio loop
        loop = asyncio.new_event_loop()
        asyncio.set_event_loop(loop)
        future = asyncio.ensure_future(self.__executeWhenMotionCallback()) # Execute async method
        loop.run_until_complete(future)
        loop.close()

    async def __executeWhenMotionCallback(self):
        await self.__whenMotionCallback()

Upvotes: 1

ParkerD
ParkerD

Reputation: 1390

Given that this is running within a loop and when_motion doesn't need a return value, you can do:

        ...
        motionSensor.when_motion = self.whenMotion

    def whenMotion(self):
        asyncio.ensure_future(self.__whenMotionCallback())

This will schedule the async callback in the event loop and keep the calling code synchronous for the library.

Upvotes: 3

PirateNinjas
PirateNinjas

Reputation: 2076

If you're doing this with coroutines, you will need to get and run the event loop. I'm going to assume you're using python 3.7, in which case you can do something like:

import asyncio
from gpiozero import MotionSensor


class MotionSensorHandler():
    __whenMotionCallback = None

    def __init__(self, pin, whenMotionCallback):
        # whenMotionCallback is an async function
        self.__whenMotionCallback = whenMotionCallback

        # Just init the sensor with gpiozero lib
        motionSensor = MotionSensor(pin)

        # Method to call when motion is detected
        loop = asyncio.get_event_loop()
        motionSensor.when_motion = loop.run_until_complete(self.whenMotion())
        loop.close()

    async def whenMotion(self):
        await self.__whenMotionCallback()

If you are on python 3.8, you can just use asyncio.run rather than all the explicitly getting and running the event loop.

Upvotes: 2

Related Questions