A few years back I hacked together a tk script to listen for events from frigate and show the camera feed for a few seconds when someone was at the door. It worked without much effort until I updated to F43 (python 3.14), now I'm stuck looking out the window like one of those people that goes outside!
I suspect the issue lies somewhere between requests and Threading but I'm not sure how to further troubleshoot that. FWIW I am not trying to to thread the request (no aiohttp or asyncio here) just a thread for tk and a thread for mqtt (whose callback is where the request is happening).
This suspicion is fueled by being able to call stream() from main() and it works as expected, but calling anywhere from either of mqtt's callbacks throws this
File "/opt/cameras/./cams.py", line 33, in on_connect
stream("garage", 10)
~~~~~~^^^^^^^^
File "/opt/cameras/./cams.py", line 64, in stream
render = ImageTk.PhotoImage(load)
File "/home/htpc/.local/lib/python3.14/site-packages/PIL/ImageTk.py", line 129, in __init__
self.__photo = tkinter.PhotoImage(**kw)
~~~~~~~~~~~~~~~~~~^^^^^^
File "/usr/lib64/python3.14/tkinter/__init__.py", line 4301, in __init__
Image.__init__(self, 'photo', name, cnf, master, **kw)
~~~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/usr/lib64/python3.14/tkinter/__init__.py", line 4248, in __init__
self.tk.call(('image', 'create', imgtype, name,) + options)
~~~~~~~~~~~~^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
_tkinter.TclError: image type "photo" does not exist
cams.py
#!/usr/bin/env python3
"""
Listen for mqtt events from frigate and show them on the htpc
"""
import sys
import json
from threading import Thread
from io import BytesIO
from tkinter import Tk, Label
#from tkinter import *
from PIL import Image, ImageTk
import paho.mqtt.client as mqtt
import requests
FRIGATE_API = ""
MQTT_SERVER = ""
MQTT_USERNAME = ""
MQTT_PASSWORD = ""
size = 540,540
WIN = Tk()
WIN.overrideredirect(True)
LABEL = Label(WIN)
LABEL.place(x=0, y=0)
LABEL.pack()
def on_connect(client, userdata, flags, rc, properties):
"""
connect callback
"""
stream("", 10) #just here for testing
del userdata, flags, properties
print(f"MQTT connected {rc}")
client.subscribe("frigate/events")
def on_message(client, userdata, msg):
"""
message callback
"""
del client, userdata
event = msg.payload
event = json.loads(event.decode())
if event['before']['stationary'] is False:
if 'front' in event['before']['current_zones']:
stream(event['before']['camera'], 10)
def stream(camera, i):
"""
do the actual drawing
"""
url = f"{FRIGATE_API}/{camera}/latest.jpg"
if i == -1:
WIN.withdraw()
else:
WIN.deiconify()
frame = requests.get(url, timeout=1)
load = Image.open(BytesIO(frame.content))
load.thumbnail(size, Image.Resampling.LANCZOS)
render = ImageTk.PhotoImage(load)
LABEL.configure(image=render)
LABEL.image = render
WIN.after(1000, stream, camera, i-1)
def main():
"""
main
"""
client = mqtt.Client(mqtt.CallbackAPIVersion.VERSION2)
client.username_pw_set(MQTT_USERNAME, MQTT_PASSWORD)
client.on_connect = on_connect
client.on_message = on_message
client.connect(MQTT_SERVER, 1883, 60)
print("dispatch client thread")
client_loop = Thread(target=client.loop_forever, daemon=True)
client_loop.start()
print("start tk main loop")
WIN.mainloop()
if __name__ == '__main__':
sys.exit(main())
As @furas mentioned in the comments, tk is not thread-safe and queue should be used to communicate.
Updated snippet where on_message publishes and tk polls for new events.
def on_message(client, userdata, msg):
"""
message callback
"""
del client, userdata
try:
event = json.loads(msg.payload.decode())
if event['before']['stationary'] is False:
if 'front' in event['before']['current_zones']:
msg_queue.put({
"camera": event['before']['camera'],
"count": 10
})
except json.JSONDecodeError as e:
print(f"Error parsing decoding event: {e}")
def check_queue():
"""
check for events from mqtt callback
"""
try:
task = msg_queue.get_nowait()
stream(task['camera'], task['count'])
except queue.Empty:
pass
WIN.after(100, check_queue)
def stream(camera, i):
"""
do the actual drawing
"""
if i <= -1:
WIN.withdraw()
return
try:
WIN.deiconify()
url = f"{FRIGATE_API}/{camera}/latest.jpg"
frame = requests.get(url, timeout=1)
load = Image.open(BytesIO(frame.content))
load.thumbnail(size, Image.Resampling.LANCZOS)
render = ImageTk.PhotoImage(load, master=WIN)
LABEL.configure(image=render)
LABEL.image = render
WIN.after(1000, stream, camera, i-1)
except Exception as e: # pylint: disable=broad-exception-caught
print(f"Stream error: {e}")
WIN.after(1000, stream, camera, i-1