Reputation: 14844
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
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
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
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
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