Dateien nach "/" hochladen

This commit is contained in:
2026-01-19 08:58:08 +00:00
parent bb629e9348
commit 6d7bd73800
5 changed files with 1005 additions and 0 deletions

708
pflanzen_gui.py Normal file
View File

@@ -0,0 +1,708 @@
# pflanzen_gui.py
# -*- coding: utf-8 -*-
import tkinter as tk
from tkinter import messagebox, ttk, filedialog
import csv
import os
import platform
import subprocess
import sys
from datetime import datetime
from PIL import Image, ImageTk
# Importiere die Logik
from db_connector import get_db_connection, setup_database_and_table, insert_pflanzen_data, test_db_connection, fetch_all_data, delete_data_by_id, save_pflanzen_plan, get_pflanzen_plan
from config_manager import load_config, save_config
# Definierte Reihenfolge der Nährstofffelder (muss mit DB übereinstimmen)
PLANNING_FIELDS = [
"phase", "lichtzyklus_h", "root_juice_ml_l", "calmag_ml_l",
"bio_grow_ml_l", "acti_alc_ml_l", "bio_bloom_ml_l", "top_max_ml_l",
"ph_wert_ziel", "ec_wert"
]
# NEU: Optionen für Dropdowns
FIELD_OPTIONS = {
"entry_phase": ["Anzucht", "Wachstum", "Blüte", "Spülen"],
"phase": ["Anzucht", "Wachstum", "Blüte", "Spülen"]
}
class PflanzenApp(tk.Tk):
def __init__(self):
super().__init__()
self.title("🌱 Pflanzenprotokoll Desktop-Anwendung")
# Automatische Maximierung des Fensters
self._set_maximized_state()
self.db_config = load_config()
# NEU: Initialisierung der Tabellen beim Start
self._initialize_db_structure()
self.is_auto_refresh_active = tk.BooleanVar(value=False)
self.refresh_interval = tk.IntVar(value=60)
self.after_id = None
self.plan_labels = {}
self.current_data_columns = None
# --- LOGO INITIALISIERUNG ---
self._setup_logo("diggerwf.jpeg")
self.create_menu_bar()
self.create_main_tabs()
# Initialisiert die Liste beim Start
self._refresh_plan_list()
def _initialize_db_structure(self):
"""Stellt sicher, dass die Datenbank und Tabellen existieren."""
try:
cnx, cursor = get_db_connection(self.db_config)
if cnx:
setup_database_and_table(cursor, self.db_config['database'])
cnx.close()
except Exception as e:
print(f"Fehler bei DB-Initialisierung: {e}")
def _setup_logo(self, image_path):
"""Lädt das Bild, skaliert es und zeigt es oben an."""
if os.path.exists(image_path):
try:
img = Image.open(image_path)
# Skalierung auf eine vernünftige Höhe (z.B. 100 Pixel)
base_height = 100
w_percent = (base_height / float(img.size[1]))
w_size = int((float(img.size[0]) * float(w_percent)))
img = img.resize((w_size, base_height), Image.Resampling.LANCZOS)
self.logo_img = ImageTk.PhotoImage(img)
self.logo_label = tk.Label(self, image=self.logo_img)
self.logo_label.pack(pady=10)
except Exception as e:
print(f"Logo konnte nicht geladen werden: {e}")
def __del__(self):
# Stoppt Auto-Refresh beim Schließen
self._toggle_auto_refresh(stop=True)
def _set_maximized_state(self):
current_os = os.name
if current_os == 'nt': # Windows
try:
self.state('zoomed')
except tk.TclError:
self.attributes('-fullscreen', True)
elif current_os == 'posix': # Linux / macOS
if 'darwin' in os.uname().sysname.lower(): # macOS
self.attributes('-zoomed', True)
else: # Linux
try:
self.attributes('-zoomed', True)
except tk.TclError:
screen_width = self.winfo_screenwidth()
screen_height = self.winfo_screenheight()
self.geometry(f"{screen_width}x{screen_height}+0+0")
def create_menu_bar(self):
menubar = tk.Menu(self)
self.config(menu=menubar)
file_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Datei", menu=file_menu)
file_menu.add_command(label="Beenden", command=self.quit)
db_menu = tk.Menu(menubar, tearoff=0)
menubar.add_cascade(label="Datenbank", menu=db_menu)
db_menu.add_command(label="MySQL Einstellungen", command=self.show_db_settings)
def create_main_tabs(self):
self.notebook = ttk.Notebook(self)
self.notebook.pack(pady=10, padx=10, expand=True, fill="both")
# Event für Tab-Wechsel binden
self.notebook.bind("<<NotebookTabChanged>>", self._handle_tab_change)
self.tab_eingabe = tk.Frame(self.notebook, padx=10, pady=10)
self.notebook.add(self.tab_eingabe, text="🌿 Daten eingeben")
self.create_input_widgets(self.tab_eingabe)
self.tab_anzeige = tk.Frame(self.notebook, padx=10, pady=10)
self.notebook.add(self.tab_anzeige, text="📈 Daten anzeigen")
self.create_display_widgets(self.tab_anzeige)
self.tab_settings = tk.Frame(self.notebook, padx=10, pady=10)
self.notebook.add(self.tab_settings, text="⚙️ Einstellungen")
self.create_settings_tab(self.tab_settings)
# NEUER TAB: Update
self.tab_update = tk.Frame(self.notebook, padx=10, pady=10)
self.notebook.add(self.tab_update, text="🔄 Update")
self.create_update_tab(self.tab_update)
def _handle_tab_change(self, event):
selected_tab = self.notebook.tab(self.notebook.select(), "text")
if selected_tab == "📈 Daten anzeigen":
self.load_data_into_treeview()
self._toggle_auto_refresh(start=True)
else:
self._toggle_auto_refresh(stop=True)
def create_update_tab(self, parent_frame):
frame = tk.Frame(parent_frame)
frame.pack(expand=True)
tk.Label(frame, text="System-Update", font=('Arial', 14, 'bold')).pack(pady=10)
tk.Label(frame, text="Klicken Sie auf den Button, um die Anwendung zu aktualisieren.\nDie Anwendung wird automatisch geschlossen und das Update-Skript gestartet.").pack(pady=5)
update_btn = tk.Button(frame, text="🚀 Update jetzt ausführen",
command=self.run_update_process,
bg='#2196F3', fg='white',
font=('Arial', 10, 'bold'),
padx=20, pady=10)
update_btn.pack(pady=20)
def run_update_process(self):
current_os = platform.system()
try:
if current_os == "Windows":
script_path = "update.bat"
if os.path.exists(script_path):
subprocess.Popen([script_path], shell=True)
else:
raise FileNotFoundError(f"{script_path} wurde im Verzeichnis nicht gefunden.")
else: # Linux / macOS
script_path = "./update.sh"
if os.path.exists(script_path):
os.chmod(script_path, 0o755)
subprocess.Popen([script_path], shell=False)
else:
raise FileNotFoundError(f"{script_path} wurde im Verzeichnis nicht gefunden.")
# App beenden, damit das Skript die Dateien ersetzen kann
self.destroy()
sys.exit()
except Exception as e:
messagebox.showerror("Update Fehler", f"Fehler beim Starten des Updates:\n{str(e)}")
def create_input_widgets(self, parent_frame):
# Haupt-Container für 2 Spalten
main_frame = tk.Frame(parent_frame)
main_frame.pack(padx=10, pady=10)
# Linke Seite: Eingabe (Ist)
input_frame = tk.LabelFrame(main_frame, text="Aktuelle Messwerte (Ist)", padx=10, pady=10)
input_frame.grid(row=0, column=0, padx=10, pady=5, sticky='n')
# Rechte Seite: Planung (Soll)
plan_display_frame = tk.LabelFrame(main_frame, text="Planung (Soll)", padx=10, pady=10)
plan_display_frame.grid(row=0, column=1, padx=10, pady=5, sticky='n')
# Dropdown für Plan-Auswahl oben rechts
tk.Label(plan_display_frame, text="Plan wählen:", font=('Arial', 9, 'bold')).grid(row=0, column=0, sticky='w')
self.plan_auswahl_combobox = ttk.Combobox(plan_display_frame, state="readonly", width=25)
self.plan_auswahl_combobox.grid(row=1, column=0, pady=(0, 5), sticky='w')
self.plan_auswahl_combobox.bind("<<ComboboxSelected>>", self._on_plan_dropdown_select)
# NEU: Dropdown für Wochen-Auswahl direkt darunter
tk.Label(plan_display_frame, text="Woche wählen:", font=('Arial', 9, 'bold')).grid(row=2, column=0, sticky='w')
self.wochen_auswahl_combobox = ttk.Combobox(plan_display_frame, state="readonly", width=25)
self.wochen_auswahl_combobox.grid(row=3, column=0, pady=(0, 10), sticky='w')
self.wochen_auswahl_combobox.bind("<<ComboboxSelected>>", self._on_week_dropdown_select)
self.fields = [
("Datum (JJJJ-MM-TT)", "entry_datum"),
("Name der Pflanze", "entry_name"),
("Woche", "entry_woche"),
("Phase", "entry_phase"),
("Lichtzyklus (h)", "entry_licht"),
("Root·Juice (ml/L)", "entry_root"),
("Calmag (ml/L)", "entry_calmag"),
("Bio·Grow (ml/L)", "entry_grow"),
("Acti·a•alc (ml/L)", "entry_acti"),
("Bio·Bloom (ml/L)", "entry_bloom"),
("Top·Max (ml/L)", "entry_topmax"),
("pH-Wert (Ziel)", "entry_ph"),
("EC-Wert", "entry_ec")
]
self.entries = {}
plan_display_row = 4 # Startreihe für die Soll-Werte angepasst wegen zweitem Dropdown
for i, (label_text, key) in enumerate(self.fields):
# Label in Eingabespalte
tk.Label(input_frame, text=f"{label_text}:").grid(row=i, column=0, padx=5, pady=2, sticky='w')
# Eingabefeld (Ist)
if key == "entry_datum":
date_frame = tk.Frame(input_frame)
date_frame.grid(row=i, column=1, padx=5, pady=2, sticky='ew')
entry = tk.Entry(date_frame, width=15)
entry.pack(side=tk.LEFT, fill='x', expand=True)
tk.Button(date_frame, text="📅", command=lambda: self._set_today_date(entry), width=3).pack(side=tk.LEFT, padx=(5, 0))
self._set_today_date(entry)
elif key in FIELD_OPTIONS:
entry = ttk.Combobox(input_frame, values=FIELD_OPTIONS[key], state="readonly", width=23)
entry.grid(row=i, column=1, padx=5, pady=2)
entry.current(0)
else:
entry = tk.Entry(input_frame, width=25)
entry.grid(row=i, column=1, padx=5, pady=2)
self.entries[key] = entry
# Events für automatisches Laden der Planung
if key in ["entry_name", "entry_woche"]:
# KeyRelease triggert die Suche sofort beim Tippen
entry.bind("<KeyRelease>", self._load_plan_for_current_inputs)
entry.bind("<FocusOut>", self._load_plan_for_current_inputs)
# Label für die Planung (Soll) auf der rechten Seite
if key not in ["entry_datum", "entry_name", "entry_woche"]:
plan_label = tk.Label(plan_display_frame, text="---", anchor='w', width=25)
plan_label.grid(row=plan_display_row, column=0, padx=5, pady=2, sticky='w')
self.plan_labels[key] = plan_label
plan_display_row += 1
# Buttons
tk.Button(input_frame, text="Daten Speichern (IST)", command=self.save_data_to_db, bg='green', fg='white').grid(row=len(self.fields), columnspan=2, pady=15)
btn_plan_frame = tk.Frame(plan_display_frame)
btn_plan_frame.grid(row=plan_display_row, column=0, columnspan=1, pady=15)
tk.Button(btn_plan_frame, text="Planung Bearbeiten (SOLL)", command=self.open_plan_window, bg='orange', fg='white').pack(side=tk.LEFT, padx=5)
tk.Button(btn_plan_frame, text="Planung Löschen", command=self._delete_plan_logic, bg='red', fg='white').pack(side=tk.LEFT, padx=5)
def _refresh_plan_list(self):
"""Aktualisiert die Liste der verfügbaren Planungen aus der Datenbank."""
try:
cnx, _ = get_db_connection(self.db_config)
if cnx:
cursor = cnx.cursor()
cursor.execute(f"USE {self.db_config['database']}")
cursor.execute("SELECT DISTINCT pflanzen_name FROM pflanzenplanung ORDER BY pflanzen_name ASC")
names = [row[0] for row in cursor.fetchall()]
self.plan_auswahl_combobox['values'] = names
cursor.close()
cnx.close()
except Exception as e:
print(f"Fehler beim Laden der Planungsliste: {e}")
def _on_plan_dropdown_select(self, event):
"""Übernimmt den Namen und lädt verfügbare Wochen in das zweite Dropdown."""
name = self.plan_auswahl_combobox.get()
# Merke dir die aktuell gewählte Woche, um sie nach dem Refresh ggf. wieder zu setzen
aktuelle_woche = self.wochen_auswahl_combobox.get()
if name:
self.entries['entry_name'].delete(0, tk.END)
self.entries['entry_name'].insert(0, name)
# Verfügbare Wochen für diesen Namen aus DB holen
try:
cnx, _ = get_db_connection(self.db_config)
if cnx:
cursor = cnx.cursor()
cursor.execute(f"USE {self.db_config['database']}")
cursor.execute("SELECT woche FROM pflanzenplanung WHERE pflanzen_name = %s ORDER BY woche ASC", (name,))
weeks = [row[0] for row in cursor.fetchall()]
self.wochen_auswahl_combobox['values'] = weeks
if weeks:
# Falls die vorherige Woche noch in der neuen Liste ist, behalte sie bei
if aktuelle_woche and str(aktuelle_woche) in [str(w) for w in weeks]:
self.wochen_auswahl_combobox.set(aktuelle_woche)
else:
self.wochen_auswahl_combobox.current(0)
self._on_week_dropdown_select(None)
else:
self.wochen_auswahl_combobox.set('')
cursor.close()
cnx.close()
except Exception as e:
print(f"Fehler beim Laden der Wochen: {e}")
def _on_week_dropdown_select(self, event):
"""Übernimmt die gewählte Woche in das Eingabefeld und lädt den Plan."""
woche = self.wochen_auswahl_combobox.get()
if woche:
self.entries['entry_woche'].delete(0, tk.END)
self.entries['entry_woche'].insert(0, woche)
self._load_plan_for_current_inputs()
def _delete_plan_logic(self):
"""Lösch-Logik für den aktuell gewählten Plan."""
name = self.plan_auswahl_combobox.get()
if not name:
messagebox.showwarning("Warnung", "Bitte wählen Sie zuerst einen Plan zum Löschen aus.")
return
if messagebox.askyesno("Löschen", f"Möchten Sie alle Planungsdaten für '{name}' wirklich unwiderruflich löschen?"):
try:
cnx, _ = get_db_connection(self.db_config)
if cnx:
cursor = cnx.cursor()
cursor.execute(f"USE {self.db_config['database']}")
cursor.execute("DELETE FROM pflanzenplanung WHERE pflanzen_name = %s", (name,))
cnx.commit()
cnx.close()
messagebox.showinfo("Erfolg", f"Planung '{name}' wurde gelöscht.")
self._refresh_plan_list()
self.plan_auswahl_combobox.set('')
self.wochen_auswahl_combobox.set('')
self.wochen_auswahl_combobox['values'] = []
self._load_plan_for_current_inputs()
except Exception as e:
messagebox.showerror("Fehler", f"Plan konnte nicht gelöscht werden: {e}")
def _set_today_date(self, entry_widget):
today = datetime.now().strftime("%Y-%m-%d")
entry_widget.delete(0, tk.END)
entry_widget.insert(0, today)
def _load_plan_for_current_inputs(self, event=None):
"""Prüft beim Verlassen der Felder Name/Woche, ob ein Plan in der DB existiert."""
plant_name = self.entries['entry_name'].get().strip()
week_text = self.entries['entry_woche'].get().strip()
# Nur suchen, wenn Name UND Woche da sind
if not plant_name or not week_text:
# Falls Name oder Woche fehlen, Striche anzeigen
self._update_plan_display(None, None)
return
try:
week = int(week_text)
plan_values_tuple, plan_columns = get_pflanzen_plan(self.db_config, plant_name, week)
self._update_plan_display(plan_values_tuple, plan_columns)
except ValueError:
self._update_plan_display(None, None)
def _update_plan_display(self, plan_values_tuple, plan_columns):
"""Aktualisiert die Soll-Labels auf der rechten Seite."""
plan_data = {}
if plan_values_tuple and plan_columns:
plan_data = dict(zip(plan_columns, plan_values_tuple))
# Mapping von GUI-Feldern auf Datenbank-Spaltennamen
mapping = {
'entry_phase': 'phase',
'entry_licht': 'lichtzyklus_h',
'entry_root': 'root_juice_ml_l',
'entry_calmag': 'calmag_ml_l',
'entry_grow': 'bio_grow_ml_l',
'entry_acti': 'acti_alc_ml_l',
'entry_bloom': 'bio_bloom_ml_l',
'entry_topmax': 'top_max_ml_l',
'entry_ph': 'ph_wert_ziel',
'entry_ec': 'ec_wert'
}
for gui_key, db_field in mapping.items():
if gui_key in self.plan_labels:
value = plan_data.get(db_field, "---")
if value is None or value == "":
display_value = "---"
elif isinstance(value, float):
display_value = f"{value:.2f}"
else:
display_value = str(value)
self.plan_labels[gui_key].config(text=display_value)
def open_plan_window(self):
"""Öffnet ein Fenster zum Erstellen/Bearbeiten von Nährstoffplänen."""
plan_window = tk.Toplevel(self)
plan_window.title("Planung Speichern/Bearbeiten (SOLL)")
# Vorausgefüllte Werte vom Hauptfenster
initial_name = self.entries['entry_name'].get().strip()
initial_woche = self.entries['entry_woche'].get().strip()
plan_window.geometry("450x600")
plan_entries = {}
tk.Label(plan_window, text="Pflanzenname:", font=('Arial', 10, 'bold')).pack(pady=(10, 0))
entry_name = tk.Entry(plan_window, width=50)
entry_name.insert(0, initial_name)
entry_name.pack(pady=2)
tk.Label(plan_window, text="Woche (Gültigkeitsbereich):", font=('Arial', 10, 'bold')).pack(pady=(10, 0))
entry_woche = tk.Entry(plan_window, width=50)
entry_woche.insert(0, initial_woche)
entry_woche.pack(pady=2)
# Automatisches Laden, wenn Name/Woche im Fenster geändert werden
entry_name.bind("<FocusOut>", lambda e: self._load_plan_in_window(entry_name, entry_woche, plan_entries))
entry_woche.bind("<FocusOut>", lambda e: self._load_plan_in_window(entry_name, entry_woche, plan_entries))
tk.Label(plan_window, text="Planungswerte (SOLL):", font=('Arial', 10, 'bold')).pack(pady=(10, 0))
fields_map = {
"phase": "Phase",
"lichtzyklus_h": "Lichtzyklus (h)",
"root_juice_ml_l": "Root·Juice (ml/L)",
"calmag_ml_l": "Calmag (ml/L)",
"bio_grow_ml_l": "Bio·Grow (ml/L)",
"acti_alc_ml_l": "Acti·a•alc (ml/L)",
"bio_bloom_ml_l": "Bio·Bloom (ml/L)",
"top_max_ml_l": "Top·Max (ml/L)",
"ph_wert_ziel": "pH-Wert (Ziel)",
"ec_wert": "EC-Wert (Soll)"
}
plan_frame = tk.Frame(plan_window)
plan_frame.pack(padx=10)
for i, (key, label_text) in enumerate(fields_map.items()):
tk.Label(plan_frame, text=f"{label_text}:", anchor='w').grid(row=i, column=0, padx=5, pady=2, sticky='w')
if key == "phase":
entry = ttk.Combobox(plan_frame, values=FIELD_OPTIONS["phase"], state="readonly", width=18)
entry.current(0)
else:
entry = tk.Entry(plan_frame, width=20)
entry.grid(row=i, column=1, padx=5, pady=2)
plan_entries[key] = entry
# Versuchen sofort zu laden
self._load_plan_in_window(entry_name, entry_woche, plan_entries)
tk.Button(plan_window, text="Planung Speichern (SOLL)",
command=lambda: self._save_plan_from_window(plan_window, entry_name, entry_woche, plan_entries),
bg='blue', fg='white').pack(pady=15)
def _load_plan_in_window(self, entry_name, entry_woche, plan_entries):
try:
plant_name = entry_name.get().strip()
week = int(entry_woche.get())
plan_values_tuple, plan_columns = get_pflanzen_plan(self.db_config, plant_name, week)
if plan_values_tuple and plan_columns:
plan_data = dict(zip(plan_columns, plan_values_tuple))
for key, entry in plan_entries.items():
val = plan_data.get(key, "")
if isinstance(entry, ttk.Combobox):
entry.set(str(val) if val is not None else "")
else:
entry.delete(0, tk.END)
entry.insert(0, f"{val:.2f}" if isinstance(val, float) else str(val if val is not None else ""))
except:
pass
def _save_plan_from_window(self, window, entry_name, entry_woche, plan_entries):
try:
plant_name = entry_name.get().strip()
week = int(entry_woche.get())
except ValueError:
messagebox.showerror("Eingabefehler", "Pflanzenname und Woche müssen gültig sein.")
return
plan_list = [plant_name, week]
for key in PLANNING_FIELDS:
try:
value = plan_entries[key].get().replace(',', '.').strip()
if value == "":
plan_list.append(None)
elif key == "phase":
plan_list.append(value)
elif key == "lichtzyklus_h":
plan_list.append(int(value))
else:
plan_list.append(float(value))
except ValueError:
messagebox.showerror("Eingabefehler", f"Feld '{key}' muss eine Zahl sein.")
return
cnx, result = get_db_connection(self.db_config)
if cnx is None:
messagebox.showerror("Verbindungsfehler", result)
return
cursor = cnx.cursor()
setup_database_and_table(cursor, self.db_config['database'])
cursor.close()
success, message = save_pflanzen_plan(cnx, tuple(plan_list))
cnx.close()
if success:
messagebox.showinfo("Erfolg", message)
window.destroy()
# Aktualisiert Pflanzennamen und stellt sicher, dass Wochen-Dropdown neu geladen wird
self._refresh_plan_list()
self.plan_auswahl_combobox.set(plant_name) # Setzt den Namen im Dropdown wieder auf den gerade gespeicherten
self._on_plan_dropdown_select(None) # Löst das Laden der Wochen aus
self._load_plan_for_current_inputs()
else:
messagebox.showerror("Speicherfehler", message)
def save_data_to_db(self):
try:
log_date_str = self.entries['entry_datum'].get()
datetime.strptime(log_date_str, "%Y-%m-%d")
datensatz = (
self.entries['entry_name'].get().strip(),
int(self.entries['entry_woche'].get()),
self.entries['entry_phase'].get().strip(),
int(self.entries['entry_licht'].get() or 0),
float(self.entries['entry_root'].get() or 0.0),
float(self.entries['entry_calmag'].get() or 0.0),
float(self.entries['entry_grow'].get() or 0.0),
float(self.entries['entry_acti'].get() or 0.0),
float(self.entries['entry_bloom'].get() or 0.0),
float(self.entries['entry_topmax'].get() or 0.0),
float(self.entries['entry_ph'].get() or 0.0),
float(self.entries['entry_ec'].get() or 0.0),
log_date_str
)
except ValueError:
messagebox.showerror("Eingabefehler", "Bitte korrigieren Sie die numerischen Felder und das Datum.")
return
cnx, result = get_db_connection(self.db_config)
if cnx:
cursor = cnx.cursor()
setup_database_and_table(cursor, self.db_config['database'])
cursor.close()
success, message = insert_pflanzen_data(cnx, datensatz)
cnx.close()
if success:
messagebox.showinfo("Erfolg", message)
for key in self.entries:
if key == 'entry_datum':
self._set_today_date(self.entries[key])
elif isinstance(self.entries[key], ttk.Combobox):
self.entries[key].current(0)
else:
self.entries[key].delete(0, tk.END)
if self.notebook.tab(self.notebook.select(), "text") == "📈 Daten anzeigen":
self.load_data_into_treeview()
else:
messagebox.showerror("Speicherfehler", message)
def create_display_widgets(self, parent_frame):
control_frame = tk.Frame(parent_frame)
control_frame.pack(fill='x', padx=5, pady=5)
refresh_frame = tk.LabelFrame(control_frame, text="Aktualisierung", padx=10, pady=5)
refresh_frame.pack(side=tk.LEFT, padx=10)
tk.Checkbutton(refresh_frame, text="Auto-Refresh", variable=self.is_auto_refresh_active,
command=self._toggle_auto_refresh).grid(row=0, column=0, columnspan=2)
tk.Label(refresh_frame, text="Intervall (Sek):").grid(row=1, column=0)
tk.Entry(refresh_frame, textvariable=self.refresh_interval, width=5).grid(row=1, column=1)
action_frame = tk.Frame(control_frame)
action_frame.pack(side=tk.RIGHT, padx=10)
tk.Button(action_frame, text="Refresh", command=self.load_data_into_treeview).pack(side=tk.LEFT, padx=5)
tk.Button(action_frame, text="Export CSV", command=self.export_data_to_csv).pack(side=tk.LEFT, padx=5)
tk.Button(action_frame, text="Löschen", command=self._delete_selected_data, bg='red', fg='white').pack(side=tk.LEFT, padx=5)
tree_frame = tk.Frame(parent_frame)
tree_frame.pack(fill='both', expand=True)
self.tree = ttk.Treeview(tree_frame, selectmode="browse")
self.tree.pack(side=tk.LEFT, fill='both', expand=True)
sb = ttk.Scrollbar(tree_frame, orient="vertical", command=self.tree.yview)
sb.pack(side=tk.RIGHT, fill='y')
self.tree.configure(yscrollcommand=sb.set)
def load_data_into_treeview(self):
data, columns_or_error = fetch_all_data(self.db_config)
for item in self.tree.get_children():
self.tree.delete(item)
if data is not None:
self.tree["columns"] = columns_or_error
self.tree.column("#0", width=0, stretch=tk.NO)
for col in columns_or_error:
self.tree.heading(col, text=col.replace('_', ' ').title())
self.tree.column(col, width=100)
for row in data:
self.tree.insert("", tk.END, values=row)
def _delete_selected_data(self):
selected = self.tree.focus()
if selected:
val = self.tree.item(selected, 'values')
if messagebox.askyesno("Löschen", f"Datensatz mit ID {val[0]} wirklich löschen?"):
delete_data_by_id(self.db_config, int(val[0]))
self.load_data_into_treeview()
def export_data_to_csv(self):
filename = filedialog.asksaveasfilename(defaultextension=".csv", filetypes=[("CSV Datei", "*.csv")])
if filename:
try:
with open(filename, 'w', newline='', encoding='utf-8') as f:
writer = csv.writer(f, delimiter=';')
writer.writerow(self.tree["columns"])
for item in self.tree.get_children():
writer.writerow(self.tree.item(item, 'values'))
messagebox.showinfo("Export", "Erfolgreich als CSV exportiert!")
except Exception as e:
messagebox.showerror("Fehler", f"Export fehlgeschlagen: {e}")
def _toggle_auto_refresh(self, start=False, stop=False):
if self.after_id:
self.after_cancel(self.after_id)
self.after_id = None
if (self.is_auto_refresh_active.get() or start) and not stop:
self.after_id = self.after(self.refresh_interval.get() * 1000, self._auto_refresh_loop)
def _auto_refresh_loop(self):
if self.is_auto_refresh_active.get():
self.load_data_into_treeview()
self._toggle_auto_refresh(start=True)
def create_settings_tab(self, parent_frame):
sf = tk.Frame(parent_frame); sf.pack(padx=10, pady=10)
self.settings_entries = {}
for i, field in enumerate(['Host', 'Port', 'Benutzer', 'Password', 'Database']):
tk.Label(sf, text=f"{field}:").grid(row=i, column=0, sticky='w')
e = tk.Entry(sf, width=30, show='*' if field == 'Password' else '')
e.grid(row=i, column=1)
self.settings_entries[field] = e
key = field.lower().replace('benutzer', 'user')
e.insert(0, str(self.db_config.get(key, '')))
tk.Button(sf, text="Speichern", command=self._save_db_settings).grid(row=5, column=0)
tk.Button(sf, text="Testen", command=self._test_connection).grid(row=5, column=1)
self.status_label = tk.Label(sf, text="Status: Unbekannt"); self.status_label.grid(row=6, columnspan=2)
def _test_connection(self):
ok, msg = test_db_connection(self.db_config)
self.status_label.config(text=msg, fg='green' if ok else 'red')
def _save_db_settings(self):
new_conf = {
'host': self.settings_entries['Host'].get(),
'port': int(self.settings_entries['Port'].get()),
'user': self.settings_entries['Benutzer'].get(),
'password': self.settings_entries['Password'].get(),
'database': self.settings_entries['Database'].get()
}
save_config(new_conf)
self.db_config = new_conf
self._test_connection()
def show_db_settings(self):
self.notebook.select(self.tab_settings)
if __name__ == "__main__":
app = PflanzenApp()
app.mainloop()

46
start.sh Normal file
View File

@@ -0,0 +1,46 @@
#!/bin/bash
# start.sh - Linux/macOS Version
echo "######################################"
echo "# Ueberpruefe Python und Abhaengigkeiten"
echo "######################################"
# --- Hilfsfunktion ---
check_and_install() {
local import_name="$1"
local pip_name="$2"
local description="$3"
echo ""
echo "Prüfe, ob $description ($import_name) installiert ist..."
python3 -c "import $import_name" &> /dev/null
if [ $? -ne 0 ]; then
echo "--------------------------------------------------------------"
echo "Das Modul '$pip_name' ($description) fehlt."
read -r -p "Soll es jetzt installiert werden? (J/N): " answer
if [[ "$answer" =~ ^[Jj]$ ]]; then
pip3 install "$pip_name"
else
echo "Installation übersprungen."
fi
else
echo "$description ist bereits installiert."
fi
}
# --- Python Prüfung ---
if ! command -v python3 &> /dev/null; then
echo "❌ FEHLER: python3 nicht gefunden!"
exit 1
fi
# --- Module prüfen ---
check_and_install "mysql.connector" "mysql-connector-python" "Datenbank"
check_and_install "PIL" "Pillow" "Bildanzeige"
# --- Start ---
echo ""
echo "Starte Pflanzen-GUI..."
python3 pflanzen_gui.py &
exit 0

65
start4.bat Normal file
View File

@@ -0,0 +1,65 @@
@echo off
setlocal enabledelayedexpansion
title Pflanzenprotokoll Starter - Diagnose Modus
set "SELF_NAME=start4.bat"
echo ######################################
echo # Ueberpruefe Systemumgebung...
echo ######################################
REM --- 1. Python-Installation pruefen ---
echo Suche nach Python...
python --version >nul 2>&1
if %errorlevel% NEQ 0 (
echo.
echo ❌ Python wurde nicht gefunden.
echo 🛠️ Versuche Installation via Winget...
REM Wir nutzen hier die ID fuer Python 3.12, die vorhin bei dir funktioniert hat
winget install --id Python.PythonInstallManager -e --source winget --accept-package-agreements --accept-source-agreements
if !errorlevel! EQU 0 (
echo.
echo ✅ Installation erfolgreich eingeleitet!
echo ⚠️ WICHTIG: Schließe dieses Fenster jetzt und starte es NEU.
echo Erst beim Neustart erkennt Windows das neue Python.
call "%SELF_NAME%"
exit
) else (
echo.
echo ❌ Winget-Installation ist fehlgeschlagen.
echo 📥 Oeffne den Microsoft Store manuell fuer dich...
start ms-windows-store://pdp/?ProductId=9NCVDN91XZQP
echo.
echo Bitte installiere Python im Store und starte diese Batch danach neu.
pause
call "%SELF_NAME%"
exit
)
)
REM --- 2. Hauptprogramm starten ---
echo ✅ Python gefunden:
python --version
echo.
echo 🚀 Starte start_app.py...
REM Prüfen ob die Datei überhaupt existiert
if not exist "start_app.py" (
echo ❌ FEHLER: Die Datei 'start_app.py' wurde im Ordner nicht gefunden!
echo Aktueller Ordner: %cd%
pause
exit
)
python start_app.py
if %errorlevel% NEQ 0 (
echo.
echo ❌ Das Python-Programm wurde mit Fehlern beendet (Code: %errorlevel%).
pause
)

87
start_app.py Normal file
View File

@@ -0,0 +1,87 @@
import sys
import subprocess
import tkinter as tk
from tkinter import messagebox
import os
# Konfiguration der benötigten Bibliotheken
# Format: (Modulname für import, Name für pip, Kurzbeschreibung)
REQUIRED_PACKAGES = [
("PIL", "Pillow", "fuer die Bildanzeige (Logo)"),
("mysql.connector", "mysql-connector-python", "fuer die Datenbankverbindung (MySQL)")
]
def ensure_pip():
"""Stellt sicher, dass pip aktuell ist, bevor Pakete installiert werden."""
try:
print("🔍 Bereite Paket-Manager (pip) vor...")
# Aktualisiert pip im Hintergrund (ohne Bestätigung)
subprocess.check_call([sys.executable, "-m", "pip", "install", "--upgrade", "pip"],
stdout=subprocess.DEVNULL)
return True
except Exception as e:
print(f"⚠️ Warnung beim pip-Update: {e}")
return True # Wir versuchen es trotzdem weiter
def check_and_install_packages():
"""Prüft Abhängigkeiten und installiert sie bei Bedarf grafisch."""
# Unsichtbares Tkinter-Hauptfenster für Dialoge
root = tk.Tk()
root.withdraw()
all_ready = True
for import_name, pip_name, description in REQUIRED_PACKAGES:
try:
# Versuch, das Modul zu laden
__import__(import_name)
print(f"✅ Modul vorhanden: {import_name}")
except ImportError:
print(f"❌ Modul fehlt: {import_name}")
# Grafische Abfrage beim Nutzer
frage = (f"Die Bibliothek '{pip_name}' ({description}) fehlt.\n\n"
f"Soll sie jetzt automatisch installiert werden?")
if messagebox.askyesno("Abhängigkeit installieren", frage):
try:
# Pip vorbereiten (nur wenn wirklich installiert werden muss)
ensure_pip()
print(f"📥 Installiere {pip_name}...")
subprocess.check_call([sys.executable, "-m", "pip", "install", pip_name])
print(f"{pip_name} erfolgreich installiert.")
except Exception as e:
messagebox.showerror("Fehler", f"Installation von {pip_name} fehlgeschlagen:\n{e}")
all_ready = False
else:
messagebox.showwarning("Warnung", f"Ohne {pip_name} wird die App wahrscheinlich abstuerzen.")
all_ready = False
root.destroy()
return all_ready
def start_main_app():
"""Startet die eigentliche GUI-Datei."""
try:
print("🚀 Lade Pflanzenprotokoll-Oberflaeche...")
# Hier wird deine eigentliche Datei importiert und gestartet
import pflanzen_gui
app = pflanzen_gui.PflanzenApp()
app.mainloop()
except Exception as e:
error_msg = f"Kritischer Fehler beim Starten von pflanzen_gui.py:\n{e}"
print(error_msg)
# Kurzes Notfall-Fenster für den Fehler
temp_root = tk.Tk()
temp_root.withdraw()
messagebox.showerror("Programmfehler", error_msg)
temp_root.destroy()
if __name__ == "__main__":
# Schritt 1: Pakete prüfen
if check_and_install_packages():
# Schritt 2: Wenn alles okay ist, Haupt-App starten
start_main_app()
else:
print("❌ Start abgebrochen, da Komponenten fehlen.")

99
update.bat Normal file
View File

@@ -0,0 +1,99 @@
@echo off
chcp 65001 >nul
setlocal enabledelayedexpansion
:: 🎨 KONFIGURATION
set "REPO_URL=https://github.com/diggerwf/Brokkoli-Gie-planung-helfer.git"
set "BRANCH=main"
set "REPO_DIR=%~dp0"
set "START_FILE=start4.bat"
set "SELF_NAME=update.bat"
set "TEMP_NAME=temp_updater.bat"
:: 🛡️ AUSNAHMEN-KONFIGURATION
:: Diese Dateien werden von Git beim Aufräumen (clean) ignoriert
set SKIP_PARAMS=-e "config.json" -e "settings.txt" -e "db_config.ini" -e "logs/" -e "saves/" -e "__pycache__"
cd /d "%REPO_DIR%"
:: 🔄 SCHRITT 0: BIN ICH DIE KOPIE (DER HELFER)?
:: Dieser Teil wird nur ausgeführt, wenn die temp_updater.bat aktiv ist
if "%~nx0"=="%TEMP_NAME%" (
echo 🛠️ Update-Modus aktiv...
timeout /t 2 >nul
:: Hier wird das Original auf der Festplatte überschrieben
git fetch origin %BRANCH% --quiet
git reset --hard origin/%BRANCH% --quiet
git clean -fd %SKIP_PARAMS% >nul
echo ✅ Dateien wurden auf der Festplatte aktualisiert.
echo 🚀 Starte das neue Hauptskript...
:: Wir starten das Original-Skript neu und beenden die Kopie
call "%SELF_NAME%"
exit
)
:: 🗑️ SCHRITT 1: AUFRÄUMEN
:: Wenn das Original startet, löscht es eine eventuell vorhandene Kopie
if exist "%TEMP_NAME%" del /f /q "%TEMP_NAME%"
echo 🔍 Prüfe auf Updates für: !REPO_URL!
:: 🛠️ 2. GIT CHECK
git --version >nul 2>&1
if %errorlevel% neq 0 (
echo ❌ Git nicht gefunden! Installiere...
winget install --id Git.Git -e --source winget --accept-package-agreements --accept-source-agreements
)
:: 🔄 3. UPDATE LOGIK
if exist ".git\" (
git remote set-url origin "!REPO_URL!"
git fetch origin %BRANCH% --quiet
for /f "tokens=*" %%a in ('git rev-parse HEAD') do set "LOCAL_HASH=%%a"
for /f "tokens=1" %%a in ('git ls-remote origin %BRANCH%') do set "REMOTE_HASH=%%a"
echo 🏠 Lokal: !LOCAL_HASH:~0,7!
echo 🌐 Online: !REMOTE_HASH:~0,7!
if "!LOCAL_HASH!" neq "!REMOTE_HASH!" (
echo 🆕 Update verfügbar!
echo 📦 Erstelle temporäre Kopie zur Aktualisierung...
:: Wir kopieren uns selbst, damit die Kopie das Original überschreiben kann
copy /y "%SELF_NAME%" "%TEMP_NAME%" >nul
:: Wir starten die Kopie und BEENDEN dieses Skript sofort (Wichtig!)
call "%TEMP_NAME%"
exit
) else (
echo ✅ Alles aktuell!
)
) else (
echo 🏗️ Ersteinrichtung läuft... 🔧
git init --quiet
git remote add origin "!REPO_URL!" 2>nul
git fetch --all --quiet
git reset --hard origin/%BRANCH% --quiet
git clean -fd %SKIP_PARAMS% >nul
echo 🔗 Repository erfolgreich eingerichtet!
)
echo.
echo ✨ System ist bereit.
:: 🚀 4. START DES HAUPTPROGRAMMS
if exist "!START_FILE!" (
echo 🚀 Starte !START_FILE! via CALL...
:: Hier wird CALL genutzt, damit das Fenster für dein Programm offen bleibt
call "!START_FILE!"
) else (
echo ⚠️ !START_FILE! wurde nicht gefunden.
pause
)
exit
::test