1

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 ?

johannchopin
  • 13,720
  • 10
  • 55
  • 101
  • do you already have an asyncio loop running? does the `when_motion` need a return value, or is it okay if it just spins off some async task? – ParkerD Dec 18 '19 at 00:33
  • This full code run in an other loop using `run_until_complete` and no `when_motion` don't need to return any value. – johannchopin Dec 18 '19 at 01:06

4 Answers4

3

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.

ParkerD
  • 1,214
  • 11
  • 18
  • 1
    Thanks for your answer but but I am surprisingly receiving this error `RuntimeError: There is no current event loop in thread 'Thread-1'`. Does `self.__whenMotionCallback()` be more than just an `async/await` function ? – johannchopin Dec 20 '19 at 14:31
2

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.

PirateNinjas
  • 1,908
  • 1
  • 16
  • 21
  • Thanks for your answer but in my case this `loop.run_until_complete()` is blocking and therefore my script can't continue to run asynchronously. Have you an idea of what the problem is ? – johannchopin Dec 20 '19 at 14:35
1

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()
johannchopin
  • 13,720
  • 10
  • 55
  • 101
  • This kinda works but you're making 2 asyncio loops and you will now have multi-threading problems if you use objects from the main thread. I've added an answer which allows you to call back into the main asyncio loop in a thread safe way. – lbt Dec 05 '20 at 10:21
1

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)
lbt
  • 766
  • 6
  • 10