pythonpython-3.xtkinterfedora

tkinter TclError After Update to Fedora 43


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())

Solution

  • 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