@@ -156,17 +156,17 @@ def pencil_icon(self):
156156
157157
158158class BulbFrame (tkinter .Frame ):
159- def _hue_callback (self , event ):
160- asyncio . run ( update_bulb (self .bulb , None , self .hue_slider .get () ))
159+ async def _hue_callback (self ):
160+ return await update_bulb (self .bulb , None , self .hue_slider .get ())
161161
162- def _saturation_callback (self , event ):
163- asyncio . run ( update_bulb (self .bulb , saturation = self .saturation_slider .get () ))
162+ async def _saturation_callback (self ):
163+ return await update_bulb (self .bulb , saturation = self .saturation_slider .get ())
164164
165- def _brightness_callback (self , event ):
166- asyncio . run ( update_bulb (self .bulb , self .brightness_slider .get () ))
165+ async def _brightness_callback (self ):
166+ return await update_bulb (self .bulb , self .brightness_slider .get ())
167167
168168 @classmethod
169- def for_bulb (cls , bulb : kasa .SmartBulb , config , * args , ** kwargs ):
169+ def for_bulb (cls , loop , bulb : kasa .SmartBulb , config , * args , ** kwargs ):
170170 """Create a new bulb frame given a SmartBulb
171171
172172 ... note:: I think using a classmethod here is a better approach than
@@ -182,19 +182,34 @@ def for_bulb(cls, bulb: kasa.SmartBulb, config, *args, **kwargs):
182182 self .hue_slider = tkinter .Scale (
183183 self , from_ = 0 , to = 360 , orient = tkinter .HORIZONTAL
184184 )
185- self .hue_slider .bind ("<ButtonRelease-1>" , self ._hue_callback )
185+ self .hue_slider .bind (
186+ "<ButtonRelease-1>" ,
187+ lambda event , self = self , loop = loop : asyncio .run_coroutine_threadsafe (
188+ self ._hue_callback (), loop
189+ ),
190+ )
186191 self .hue_slider .set (self .bulb .hsv [0 ])
187192
188193 self .saturation_slider = tkinter .Scale (
189194 self , from_ = 0 , to = 100 , orient = tkinter .HORIZONTAL
190195 )
191- self .saturation_slider .bind ("<ButtonRelease-1>" , self ._saturation_callback )
196+ self .saturation_slider .bind (
197+ "<ButtonRelease-1>" ,
198+ lambda event , self = self , loop = loop : asyncio .run_coroutine_threadsafe (
199+ self ._saturation_callback (), loop
200+ ),
201+ )
192202 self .saturation_slider .set (self .bulb .hsv [1 ])
193203
194204 self .brightness_slider = tkinter .Scale (
195205 self , from_ = 0 , to = 100 , orient = tkinter .HORIZONTAL
196206 )
197- self .brightness_slider .bind ("<ButtonRelease-1>" , self ._brightness_callback )
207+ self .brightness_slider .bind (
208+ "<ButtonRelease-1>" ,
209+ lambda event , self = self , loop = loop : asyncio .run_coroutine_threadsafe (
210+ self ._brightness_callback (), loop
211+ ),
212+ )
198213 self .brightness_slider .set (self .bulb .brightness )
199214
200215 bulb_name = getattr (self .bulb , "alias" , None ) or self .bulb .mac
@@ -221,6 +236,17 @@ class KasaDevices(tkinter.Frame):
221236 def __init__ (self , * args , ** kwargs ):
222237 super (self .__class__ , self ).__init__ (* args , ** kwargs )
223238
239+ # create an asyncio event loop running in a secondary thread
240+ def exception_handler (loop , context ):
241+ loop .call_soon_threadsafe (
242+ logger .error , "Caught exception {}" .format (context )
243+ )
244+
245+ self .event_loop = asyncio .new_event_loop ()
246+ self .event_loop .set_exception_handler (exception_handler )
247+ self .event_thread = threading .Thread (target = self .event_loop .run_forever )
248+ self .event_thread .start ()
249+
224250 self .device_lock = asyncio .Lock ()
225251 # list of kasa devices
226252 self .kasa_devices = []
@@ -241,7 +267,7 @@ def _bulbs(self):
241267 async def update_widgets (self ):
242268 for bulb in self ._bulbs :
243269 if bulb .mac not in self .device_widgets :
244- w = BulbFrame .for_bulb (bulb , self .config , master = self )
270+ w = BulbFrame .for_bulb (self . event_loop , bulb , self .config , master = self )
245271 w .pack ()
246272 self .device_widgets [bulb .mac ] = w
247273
@@ -273,12 +299,22 @@ async def clear_devices(self):
273299 self .kasa_devices .clear ()
274300
275301 def start_refresh (self ):
276- def thread_loop ():
277- asyncio .run (self ._do_refresh ())
302+ """Returns a *concurrent* future, rather than an *asyncio* future. You
303+ can block on the result from a *synchronous* thread using
304+ self.start_refresh().result().
278305
306+ :rtype: concurrent.futures.Future
307+ """
279308 logger .info ("KasaDevices.start_refresh() called" )
280- threading .Thread (target = thread_loop ).start ()
281- logger .info ("KasaDevices.start_refresh() completed" )
309+
310+ async def call_later (coro , * args , ** kwargs ):
311+ # call later isn't particularly necessary, but by using it, out
312+ # exceptions will go to the asyncio exceptions handler
313+ self .event_loop .create_task (coro (* args , ** kwargs ))
314+
315+ return asyncio .run_coroutine_threadsafe (
316+ call_later (self ._do_refresh ), self .event_loop
317+ )
282318
283319
284320def main ():
0 commit comments