GUI mit Performance Problemen

Fragen zu Tkinter.
Antworten
Jonas1243
User
Beiträge: 10
Registriert: Dienstag 13. November 2018, 21:27

Hallo zusammen,

ich bin gerade dabei mein erstes größeres Projekt mit Hilfe eines Raspberry PI's als Steuerung umzusetzten. Das ganze ist eine Steuerung für eine etwas komplexere Heizungsanlage mit Wasserführendem Ofen, Wärmepumpe und Pufferspeicher... Aus dem Pufferspeicher kann aber auch eine Brauchwasserwärmepumpe mit einem Zusätzlichen Wärmetauscher erwärmt werden und zusätzlich sollen noch weitere Komfortfunktionen wie Automatisches Pufferspeicher aufheizen durch die Wärmepumpe bei PV Strom Überschuss in abhängigkeit zur Außentemperatur passieren... Also sehr komplex und auch mit keiner fertigen Heizungssteuerung so umsetzbar...

Jetzt zu meinem eigentlichen Problem. Ich habe die Steuerung in einzelne skripte aufgeteilt und das ganze Wartungsfreundlich und "modular" zu gestalten, eines dieser Skripte ist die Haupt Visualisierung, in der die ganzen Messwerte und Betriebsarten usw. der Pumpen angezeigt werden.
Die GUI läuft also dauerhaft... Leider wird sie mit der Zeit sehr träge, das heißt nach ca. 2 bis 3 Stunden Laufzeit merkt man bereits, dass sie langsam auf eingaben an den Button reagiert und nach mehr als 5 Stunden dauert es dann gute 5 Sekunden bis sie auf die Eingaben reagiert und auch die Aktualiserung der Daten kommt dann irgendwann ganz zum erliegen. Ich vermute es liegt an der Datenbank abfrage, bin dort aber noch nicht so tief im Thema um es besser hin zu bekommen.

Wenn jemand einen Tipp für mich hat, wie ich das ganze besser gestaltet bekomme, freue ich mich über Tipps.

Hier der Code:

Code: Alles auswählen

#!/usr/bin/env python3
# -*- coding: utf-8 -*-
import sys
import tkinter as tk
import tkinter.ttk as ttk
from tkinter.constants import *
import os.path
import sqlite3
import subprocess
import threading

sys.path.append('/home/PI/Heizungssteuerung/Log')
from logger_config import setup_logger

# Logger für dieses spezifische Unterprogramm einrichten
logger = setup_logger('Übersicht','main.log')

_location = os.path.dirname(__file__)

_bgcolor = '#d9d9d9'
_fgcolor = '#000000'
_tabfg1 = 'black'
_tabfg2 = 'white'
_bgmode = 'light'
_tabbg1 = '#d9d9d9'
_tabbg2 = 'gray40'

_style_code_ran = 0

def _style_code():
    global _style_code_ran
    if _style_code_ran:
        return
    style = ttk.Style()
    style.theme_use('default')
    style.configure('.', font="TkDefaultFont")
    if sys.platform == "win32":
        style.theme_use('winnative')
    _style_code_ran = 1

class Übersicht:
    def __init__(self, top=None):
        self.top = top
        self.top.attributes('-fullscreen', True)
        top.minsize(1, 1)
        top.maxsize(1024, 600)
        top.resizable(1, 1)
        top.title("Heizungssteuerung Übersicht")

        _style_code()

        self.notaus_state = 0
        self._create_separators()
        self._create_buttons()
        self.create_exit_button()
        self._create_frames()
        self._create_labels()
        self._update_labels_from_database()
        self.update_hk_pumpe_status()
        self.start_update_loop()
        
        
    def _create_separators(self):
        separators = [
            ("TSeparator2", 0.137, 0.25, 0.282, None),
            ("TSeparator2_1", 0.132, 0.765, 0.282, None),
            ("TSeparator1_1", 0.135, 0.613, 0.06, None),
            ("TSeparator3", 0.361, 0.1, 0.059, None),
            ("TSeparator1", 0.137, 0.1, 0.06, None),
            ("TSeparator3_1", 0.362, 0.61, 0.059, None),
            ("TSeparator6", 0.938, 0.1, None, 0.45),
            ("TSeparator7", 0.91, 0.3, 0.025, None),
            ("TSeparator8", 0.713, 0.317, 0.029, None),
            ("TSeparator9", 0.625, 0.383, None, 0.267),
            ("TSeparator10", 0.566, 0.65, 0.059, None),
            ("TSeparator4", 0.566, 0.767, 0.175, None),
            ("TSeparator5", 0.566, 0.1, 0.367, None)
        ]
        
        for name, relx, rely, relwidth, relheight in separators:
            sep = ttk.Separator(self.top)
            sep.place(relx=relx, rely=rely, relwidth=relwidth, relheight=relheight)
            if relheight:
                sep.configure(orient="vertical")
            setattr(self, name, sep)


    def _create_buttons(self):
        buttons = [
            ("Settings", "Einstellungen", 0.869, 0.917, self.open_settings),
            ("PufferLaden", "Puffer Laden", 0.869, 0.85, None),
            ("NotAus", "Not-Aus", 0.742, 0.917, self.toggle_notaus),
#            ("Beenden", "Beenden", 0.742, 0.85, None)
        ]

        for name, text, relx, rely, command in buttons:
            btn = tk.Button(self.top, text=text, font="-family {DejaVu Sans} -size 10")
            btn.place(relx=relx, rely=rely, height=31, width=111)
            btn.configure(activebackground="#d9d9d9")
            if command:
                btn.configure(command=command)
            setattr(self, name, btn)
            
    def create_exit_button(self):
        self.EXIT = tk.Button(self.top, text='Beenden', font="-family {DejaVu Sans} -size 10", command=self.set_stop_flag)
        self.EXIT.place(relx=0.742, rely=0.85, height=31, width=111)
        
        
    def _create_frames(self):
        frames = [
            ("LGThermaV", self.top, 0.742, 0.55, 0.242, 0.22),
            ("HKPumpeFrame", self.top, 0.752, 0.217, 0.158, 0.155),
            ("HKFrame", self.top, 0.605, 0.217, 0.158, 0.104),
            ("PumpeBWWPFrame", self.top, 0.195, 0.583, 0.17, 0.171),
            ("OfenPumpeFrame", self.top, 0.195, 0.067, 0.17, 0.171),
            ("OfenFrame", self.top, 0.02, 0.067, 0.208, 0.114),
            ("BWWPFrame", self.top, 0.02, 0.583, 0.208, 0.114),
            ("WetterFrame", self.top, 0.02, 0.85, 0.125, 0.67),
            ("PufferFrame", self.top, 0.42, 0.067, 0.725, 0.146)
        ]
        
        for name, parent, relx, rely, relheight, relwidth in frames:
            frame = tk.Frame(parent, relief='groove', borderwidth="2")
            frame.place(relx=relx, rely=rely, relheight=relheight, relwidth=relwidth)
            setattr(self, name, frame)

        self.Frame2_2_1 = tk.Frame(self.PumpeBWWPFrame, relief='groove', borderwidth="2")
        self.Frame2_2_1.place(relx=1.897, rely=4.635, relheight=1.0, relwidth=1.0)

        self.Frame2_2 = tk.Frame(self.OfenPumpeFrame, relief='groove', borderwidth="2")
        self.Frame2_2.place(relx=1.897, rely=4.635, relheight=1.0, relwidth=1.0)

        self.Frame2_1 = tk.Frame(self.OfenFrame, relief='groove', borderwidth="2")
        self.Frame2_1.place(relx=1.154, rely=2.84, relheight=1.0, relwidth=1.0)

    def _create_labels(self):
        labels = [
            (self.LGThermaV, "WPStatus", 0.089, 0.276, "Label", 21, 100),
            (self.LGThermaV, "WPFlow", 0.089, 0.483, "Label", 21, 100),
            (self.LGThermaV, "WPText", 0.044, 0.069, "LG Therma V Wärmepumpe", 21, 189),
            (self.top, "WPWaterIN", 0.684, 0.717, "Label", 21, 60),
            (self.top, "WPWaterOUT", 0.88, 0.5, "Label", 21, 60),
            (self.HKPumpeFrame, "HKPumpeBetriebsart", 0.074, 0.4, "Label", 21, 100),
            (self.HKPumpeFrame, "HKPumpeBetr", 0.074, 0.685, "Label", 21, 140),
            (self.HKPumpeFrame, "HKPumpeText", 0.074, 0.211, "Heizkreis Pumpe", 11, 119),
            (self.HKFrame, "HKText", 0.075, 0.137, "Heizkreis", 26, 75),
            (self.PumpeBWWPFrame, "StatusBWWPPumpe", 0.057, 0.38, "Label", 21, 100),
            (self.PumpeBWWPFrame, "BetrBWWPPumpe", 0.057, 0.65, "Label", 21, 100),
            (self.PumpeBWWPFrame, "PumpeBWWPText", 0.057, 0.118, "Pumpe BWWP", 17, 102),
            (self.Frame2_2_1, "PumpeOfen_1_1", 0.08, 0.129, "Pumpe Ofen", 17, 122),
            (self.OfenPumpeFrame, "StatusOfenPumpe", 0.057, 0.38, "Label", 21, 100),
            (self.OfenPumpeFrame, "BetrOfenPumpe", 0.057, 0.65, "Label", 21, 100),            
            (self.Frame2_2, "PumpeOfen_1", 0.08, 0.129, "Pumpe Ofen", 17, 123),
            (self.OfenPumpeFrame, "PumpeOfenText", 0.057, 0.118, "Pumpe Ofen", 17, 93),
            (self.Frame2_1, "Label1_1", 0.085, 0.096, "Ofen", 23, 91),
            (self.OfenFrame, "T21", 0.085, 0.4, "Label", 21, 89),
            (self.OfenFrame, "OfenText", 0.094, 0.08, "Ofen", 13, 91),
            (self.BWWPFrame, "T22", 0.085, 0.4, "Label", 21, 89),
            (self.BWWPFrame, "BWWPText", 0.094, 0.08, "BWWP", 11, 86),
            (self.WetterFrame, "WetterInfoText", 0.015, 0.133, "Wetter", 21, 59),
            (self.WetterFrame, "OutTempText", 0.015, 0.533, "Außentemperatur:", 21, 129),
            (self.WetterFrame, "PufferHeizStatus", 0.845, 0.533, "Label", 21, 59),
            (self.WetterFrame, "PufferHeizText", 0.802, 0.133, "Puffer Heizen", 21, 109),
            (self.WetterFrame, "WetterMorgenText", 0.437, 0.133, "Wetter morgen", 21, 109),
            (self.WetterFrame, "WetterMaxText", 0.452, 0.4, "Max. Temp.:", 21, 89),
            (self.WetterFrame, "WetterMinText", 0.452, 0.667, "Min. Temp.:", 21, 89),
            (self.WetterFrame, "WetterMaxWert", 0.583, 0.4, "Label", 21, 69),
            (self.WetterFrame, "WetterMinWert", 0.583, 0.667, "Label", 21, 70),
            (self.WetterFrame, "T23", 0.204, 0.533, "Label", 21, 89),
            (self.PufferFrame, "T12", 0.133, 0.368, "Label", 21, 109),
            (self.PufferFrame, "T13", 0.133, 0.621, "Label", 21, 109),
            (self.PufferFrame, "T11", 0.133, 0.138, "Label", 21, 109),
            (self.PufferFrame, "PufferText", 0.02, 0.018, "Pufferspeicher 600L", 18, 140),
            (self.PufferFrame, "T14", 0.133, 0.851, "Label", 21, 99),
            (self.top, "UPobentext", 0.02, 0.49, "WW Umwälzung Oben:", 21, 180),
            (self.top, "UPuntentext", 0.02, 0.54, "WW Umwälzung Unten:", 21, 180),
            (self.top, "UPobenStatus", 0.18, 0.49, "Label", 21, 89),
            (self.top, "UPuntenStatus", 0.18, 0.54, "Label", 21, 89)
        ]
        
        for parent, name, relx, rely, text, height, width in labels:
            lbl = tk.Label(parent, text=text, anchor='w', font="-family {DejaVu Sans} -size 10")
            lbl.place(relx=relx, rely=rely, height=height, width=width)
            lbl.configure(activebackground="#d9d9d9", compound='left')
            setattr(self, name, lbl)


    def set_stop_flag(self):
        try:
            conn = sqlite3.connect('/home/PI/Heizungssteuerung/DB/sqlite-database.db')
            cursor = conn.cursor()
            cursor.execute("UPDATE DatenundBits SET Stop = 1 WHERE id = (SELECT MAX(id) FROM DatenundBits)")
            conn.commit()
            conn.close()
            print("Stop-Flag wurde in der Datenbank gesetzt.")
        except Exception as e:
            print(f"Fehler beim Aktualisieren der Datenbank: {e}")
        self.top.quit()

    def toggle_notaus(self):
        conn = sqlite3.connect('/home/PI/Heizungssteuerung/DB/sqlite-database.db')
        cursor = conn.cursor()
        cursor.execute("SELECT Notaus FROM DatenundBits ORDER BY id DESC LIMIT 1")
        current_state = cursor.fetchone()[0]
        new_state = 1 if current_state == 0 else 0
        cursor.execute("UPDATE DatenundBits SET Notaus =? WHERE id = (SELECT MAX(id) FROM DatenundBits)", (new_state,))
        conn.commit()
        conn.close()
        self.update_notaus_button(new_state)
        logger.warning("Not-Aus betätigt")

    def update_notaus_button(self, state):
        if state == 1:
            self.NotAus.configure(background="red", activebackground="red")
        else:
            self.NotAus.configure(background=self.top.cget('background'), activebackground="#d9d9d9")
        self.notaus_state = state
        
        
    def _check_notaus_state(self):
        conn = sqlite3.connect('/home/PI/Heizungssteuerung/DB/sqlite-database.db')
        cursor = conn.cursor()
        cursor.execute("SELECT Notaus FROM DatenundBits ORDER BY id DESC LIMIT 1")
        notaus_state = cursor.fetchone()[0]
        conn.close()

        if notaus_state != self.notaus_state:
            self.update_notaus_button(notaus_state)

    def start_update_loop(self):
        self._update_labels_from_database()
        self._check_notaus_state()
        self.top.after(1000, self.start_update_loop)  # Aktualisiert jede Sekunde

    def open_settings(self):
        try:
            settings_script_path = "/home/PI/Heizungssteuerung/GUI/Einstellungen.py"
            subprocess.Popen(["python", settings_script_path])
        except Exception as e:
            print(f"Fehler beim Öffnen der Einstellungen: {e}")

    def _update_labels_from_database(self):
        # Verbindung zur Datenbank herstellen
        conn = sqlite3.connect('/home/PI/Heizungssteuerung/DB/sqlite-database.db')
        cursor = conn.cursor()

        # Abfrage aller Temperaturen
        cursor.execute("""
            SELECT 
                T11, 
                T12, 
                T13, 
                T14, 
                T21, 
                T22, 
                T23, 
                T24,
                WPWaterInTemp,
                WPWaterOutTemp,
                WPFlow,6
                `K1.1 (Ladepumpe BWWP)`, 
                `K1.2 (Ladepumpe Ofen)`, 
                `K1.3 (Umwälzpumpe oben)`, 
                `K1.4 (Umwälzpumpe unten)`
            FROM ModbusRTUDaten
            ORDER BY id DESC
            LIMIT 1
        """)
        ModbusDaten = cursor.fetchone()
        
        
        cursor.execute("""
            SELECT 
                `WettermorgenMAX`, 
                `WettermorgenMIN`,
                `HeizenPuffer`,
                `HKPumpeStatus`,
                `OfenPumpeStatus`,
                `BWWPPumpeStatus`,
                `StatusWP`,
                `PrztHKPumpe`
            FROM DatenundBits 
            ORder BY id DESC 
            Limit 1
        """)
        DatenundBits = cursor.fetchone()
        
    
        #print("Debug - Datenbankwerte:", output)  # Debugging-Ausgabe

    # Aktualisieren der Labels mit Werten
        if ModbusDaten:
            self.T11.config(text=f"{ModbusDaten[0]:.1f}°C")
            self.T12.config(text=f"{ModbusDaten[1]:.1f}°C")
            self.T13.config(text=f"{ModbusDaten[2]:.1f}°C")
            self.T14.config(text=f"{ModbusDaten[3]:.1f}°C")
            self.T21.config(text=f"{ModbusDaten[4]:.1f}°C")
            self.T22.config(text=f"{ModbusDaten[5]:.1f}°C")
            self.T23.config(text=f"{ModbusDaten[6]:.1f}°C")
            self.WetterMaxWert.config(text=f"{DatenundBits[0]:.1f}°C")
            self.WetterMinWert.config(text=f"{DatenundBits[1]:.1f}°C")
            self.WPWaterIN.config(text=f"{ModbusDaten[8]:.1f}°C")
            self.WPWaterOUT.config(text=f"{ModbusDaten[9]:.1f}°C")
            self.WPFlow.config(text=f"{ModbusDaten[10]:.1f} m³/h")          
            self.update_status_label(self.UPobenStatus, ModbusDaten[13])
            self.update_status_label(self.UPuntenStatus, ModbusDaten[14])
            self.update_status_label(self.PufferHeizStatus, DatenundBits[2])
            self.update_status_label(self.HKPumpeBetriebsart, DatenundBits[3])
            self.update_status_label(self.StatusOfenPumpe, DatenundBits[4])
            self.update_status_label(self.BetrOfenPumpe, ModbusDaten[12])
            self.update_status_label(self.StatusBWWPPumpe, DatenundBits[5])
            self.update_status_label(self.BetrBWWPPumpe, ModbusDaten[11])
            self.update_status_label(self.WPStatus, DatenundBits[6]) 

        # Verbindung schließen
        conn.close()
        
        # Regelmäßige Aktualisierung (z.B. alle 5 Sekunden)
        self.top.after(5000, self._update_labels_from_database)
        
        
    def update_status_label(self, label, value):
        if value == 1:
            status_text = "Betrieb"
            status_farbe = "green"
        elif value == 2:
            status_text = "Handetrieb"
            status_farbe = "orange"
        elif value == 3:
            status_text = "Automatik"
            status_farbe = "green"
        elif value == 4:
            status_text = "Abtauen"
            status_farbe = "blue"
        else:
            status_text = "Aus"
            status_farbe = "red"

        label.config(text=status_text, fg=status_farbe)
        
    def update_hk_pumpe_status(self):
        conn = sqlite3.connect('/home/PI/Heizungssteuerung/DB/sqlite-database.db')
        cursor = conn.cursor()
        cursor.execute("SELECT PrztHKPumpe FROM DatenundBits ORDER BY id DESC LIMIT 1")
        pwm_value = cursor.fetchone()[0]
        conn.close()

        if pwm_value == 0:
            status = "Automatikbetrieb"
            color = "green"
        elif 1 <= pwm_value <= 91:
            drehzahl = 100 - ((pwm_value - 10) * 100 / 74) if pwm_value >= 10 else 100
            status = f"Sollwertvorg {drehzahl:.0f}%"
            color = "orange"
        elif 96 <= pwm_value <= 99:
            status = "Aus"
            color = "red"
        else:
            status = "Unbekannt"
            color = "black"

        self.HKPumpeBetr.config(text=status, fg=color)
        self.top.after(5000, self.update_hk_pumpe_status)                 # Regelmäßige Aktualisierung (z.B. alle 5 Sekunden)

def start_Übersicht():
    global root
    root = tk.Tk()
    root.protocol('WM_DELETE_WINDOW', root.destroy)
    global _top1, _w1
    _top1 = root
    _w1 = Übersicht(_top1)
    root.mainloop()

if __name__ == '__main__':
    start_Übersicht()

Vielen Dank :D
Benutzeravatar
sparrow
User
Beiträge: 4501
Registriert: Freitag 17. April 2009, 10:28

Namen schreibt man klein_mit_unterstrich.
Außer die Namen von Klassen (PascalCase) und die Namen von Konstanten (KOMPLETT_GROSS).

"global" hat in einem Programm nichts zu suchen - weil globale Variablen in einem Programm nichts zu suchen haben.

Warum wird ein anderes Python Script via subprocess ausgeführt? Das ist in 99% der Fälle falsch.

Das wäre schon einmal ein Anfang um ein bisschen zu entrümpeln.
Sirius3
User
Beiträge: 18215
Registriert: Sonntag 21. Oktober 2012, 17:20

Die Coding-Zeile ist überflüssig, da UTF8 der default ist. *-Importe benutzt man nicht. Konstanten schreibt man KOMPLETT_GROSS und nicht mit _ am Anfang. sys.path sollte man nicht verändern. Module werden in einem venv installiert. global benutzt man nicht.
Bei GUIs benutzt man keine place, sondern grid oder pack.
Bei den vielen Elementen wird das ziemlich unübersichtlich. Zumindest würde man mehrere Klassen, pro Frame eine benutzen.
Literale Strings, wie Dateinamen sollten als Konstanten am Anfang des Programms definiert werden, und nicht mehrfach vorkommen. Die gesamte Datenbankanbindung sollte aus der GUI-Klasse heraus separat in Funktionen stehen.
`start_update_loop` ruft jede Sekunde `_update_labels_from_database` auf, das wiederum eine Kaskade an Aufrufen alle 5 Sekunden auslöst. Nach 60 Sekunden gibt es also insgesamt 60 Loops, die sich alle 5 Sekunden aufrufen, nach einer Stunde werden die Labels 720mal pro Sekunde aktualisiert.
Jonas1243
User
Beiträge: 10
Registriert: Dienstag 13. November 2018, 21:27

Erstmal vielen Dank für die Tips/Anmerkungen, ich werde mich daran mal versuchen ;)
Antworten