import json import os import tkinter as tk from tkinter import ttk, filedialog, messagebox from dataclasses import dataclass, asdict from typing import Dict, List, Any APP_TITLE = "MultiFlow UI + Schritt-für-Schritt-Anleitung" DEFAULT_OUTPUT = "multiflow_anleitung.txt" @dataclass class ProtocolData: # Allgemein geraete_nummer: str = "18DC0023" zaehler_bez: str = "?" personal_nr: str = "000001" software_version: str = "5.01C5.270E" datum_uhrzeit: str = "06.09.2023 14:34" # Produktdefinitionen (laut Anforderung fix) produkt_1_name: str = "Heizöl EL" produkt_1_art: str = "Flüssigprodukt" produkt_1_ptb: str = "1" produkt_1_einheit: str = "Liter" produkt_2_name: str = "Diesel" produkt_2_art: str = "Flüssigprodukt" produkt_2_ptb: str = "2" produkt_2_einheit: str = "Liter" produkt_3_name: str = "Super E5" produkt_3_art: str = "Flüssigprodukt" produkt_3_ptb: str = "15" produkt_3_einheit: str = "Liter" # Veränderbare technische Parameter sensor_verschiebung_0c: float = 0.0 sensor_verschiebung_100c: float = 0.0 sensor_ausgeschaltet: bool = False kompensation_typ: str = "VCF" kompensation_temp_c: float = 15.0 produktgruppe: str = "Raffi. Öl" mittlere_dichte: float = 836.0 meter_faktor_1: float = 1.0 meter_faktor_2: float = 1.0 meter_faktor_3: float = 1.0 meter_faktor_4: float = 1.0 durchfluss_rate_1: float = 1000.0 durchfluss_rate_2: float = 0.0 durchfluss_rate_3: float = 0.0 durchfluss_rate_4: float = 0.0 min_temp_c: float = -20.0 max_temp_c: float = 50.0 aenderungsfaktor: float = 0.0 minimum_abgaben: int = 0 minimal_vorlauf_l: float = 200.0 minimal_volumen_l: float = 23.0 preiskorrektur: bool = True additiv_ausweisen: bool = False auto_abgabe_stop_min: int = 0 zusatz_pruefung: str = "#############" pulswertigkeit_l: float = 1.0 max_durchfluss: float = 800.0 max_fehlerpulse: int = 4 sensor_typ: str = "TK-Typ" min_durchfluss: float = 20.0 drehrichtung: int = 0 drucker_auswahl: str = "TM-U295" drucker_protokoll: bool = False max_fehlversuche: int = 0 entgasung_steuerung: str = "Aus" abfuell_sicherung: bool = False ventil_steuerung: str = "Basis-Steuerung" # Protokoll-/Zählerwerte: bewusst read-only / nicht editierbar unkompensiertes_volumen_l: float = 16995.0 kompensiertes_volumen_l: float = 17211.0 kompensierte_masse_kg: float = 14309.0 additive_gesamt_l: float = 0.0 nicht_berechnet_l: float = 0.0 class MultiFlowApp: def __init__(self, root: tk.Tk): self.root = root self.root.title(APP_TITLE) self.data = ProtocolData() self.variables: Dict[str, Any] = {} self._build_ui() self._load_defaults_into_ui() def _build_ui(self): self.root.geometry("1180x820") self.root.minsize(1020, 720) main = ttk.Frame(self.root, padding=12) main.pack(fill="both", expand=True) title = ttk.Label( main, text="MultiFlow – variable Parameter pflegen und Anleitung erzeugen", font=("Segoe UI", 15, "bold"), ) title.pack(anchor="w", pady=(0, 8)) info = ttk.Label( main, text=( "Annahme in dieser Version: Produktdefinitionen bleiben fest. " "Veränderbar sind nur technische Parameter und Kalibrierwerte. " "Summenstände / Gesamtzähler gelten als reine Protokollwerte und werden nicht geändert." ), wraplength=1100, justify="left", ) info.pack(anchor="w", pady=(0, 10)) toolbar = ttk.Frame(main) toolbar.pack(fill="x", pady=(0, 10)) ttk.Button(toolbar, text="Neu / Standardwerte", command=self.reset_defaults).pack(side="left", padx=(0, 6)) ttk.Button(toolbar, text="JSON laden", command=self.load_json).pack(side="left", padx=6) ttk.Button(toolbar, text="JSON speichern", command=self.save_json).pack(side="left", padx=6) ttk.Button(toolbar, text="Anleitung als TXT exportieren", command=self.export_manual).pack(side="left", padx=6) ttk.Button(toolbar, text="Variablen prüfen", command=self.show_variable_summary).pack(side="left", padx=6) self.notebook = ttk.Notebook(main) self.notebook.pack(fill="both", expand=True) self.tab_allgemein = ttk.Frame(self.notebook, padding=10) self.tab_kalibrierung = ttk.Frame(self.notebook, padding=10) self.tab_geraet = ttk.Frame(self.notebook, padding=10) self.tab_readonly = ttk.Frame(self.notebook, padding=10) self.tab_manual = ttk.Frame(self.notebook, padding=10) self.notebook.add(self.tab_allgemein, text="Allgemein") self.notebook.add(self.tab_kalibrierung, text="Kalibrierwerte") self.notebook.add(self.tab_geraet, text="Gerät / Sensorik") self.notebook.add(self.tab_readonly, text="Nur Anzeige") self.notebook.add(self.tab_manual, text="Vorschau Anleitung") self._build_general_tab() self._build_calibration_tab() self._build_device_tab() self._build_readonly_tab() self._build_manual_tab() def _add_entry(self, parent, row, label, key, width=18): ttk.Label(parent, text=label).grid(row=row, column=0, sticky="w", padx=(0, 10), pady=4) var = tk.StringVar() ent = ttk.Entry(parent, textvariable=var, width=width) ent.grid(row=row, column=1, sticky="w", pady=4) self.variables[key] = var return ent def _add_check(self, parent, row, label, key): var = tk.BooleanVar() chk = ttk.Checkbutton(parent, text=label, variable=var) chk.grid(row=row, column=0, columnspan=2, sticky="w", pady=4) self.variables[key] = var return chk def _build_general_tab(self): left = ttk.LabelFrame(self.tab_allgemein, text="Allgemeine Daten", padding=10) left.pack(side="left", fill="both", expand=True, padx=(0, 8)) self._add_entry(left, 0, "Geräte-Nummer", "geraete_nummer") self._add_entry(left, 1, "Zähler-Bezeichnung", "zaehler_bez") self._add_entry(left, 2, "Personal-Nr.", "personal_nr") self._add_entry(left, 3, "Software-Version", "software_version") self._add_entry(left, 4, "Datum / Uhrzeit", "datum_uhrzeit") right = ttk.LabelFrame(self.tab_allgemein, text="Feste Produktdefinitionen (nur Anzeige)", padding=10) right.pack(side="left", fill="both", expand=True) text = tk.Text(right, height=20, width=58, wrap="word") text.pack(fill="both", expand=True) text.insert( "1.0", "Produkt 1: Heizöl EL / Flüssigprodukt / PTB 1 / Liter\n" "Produkt 2: Diesel / Flüssigprodukt / PTB 2 / Liter\n" "Produkt 3: Super E5 / Flüssigprodukt / PTB 15 / Liter\n\n" "Diese Werte sind in dieser Programmversion absichtlich fest, weil du geschrieben hast, " "dass die Produktdefinitionen immer gleich sind." ) text.configure(state="disabled") def _build_calibration_tab(self): left = ttk.LabelFrame(self.tab_kalibrierung, text="Temperatur / Kompensation", padding=10) left.pack(side="left", fill="both", expand=True, padx=(0, 8)) self._add_entry(left, 0, "Temperatur-Verschiebung bei 0 °C", "sensor_verschiebung_0c") self._add_entry(left, 1, "Temperatur-Verschiebung bei 100 °C", "sensor_verschiebung_100c") self._add_check(left, 2, "Sensor ausgeschaltet", "sensor_ausgeschaltet") self._add_entry(left, 3, "Kompensationstyp", "kompensation_typ") self._add_entry(left, 4, "Kompensationstemperatur °C", "kompensation_temp_c") self._add_entry(left, 5, "Produktgruppe", "produktgruppe") self._add_entry(left, 6, "Mittlere Dichte", "mittlere_dichte") self._add_entry(left, 7, "Min. Temperatur °C", "min_temp_c") self._add_entry(left, 8, "Max. Temperatur °C", "max_temp_c") self._add_entry(left, 9, "Änderungsfaktor", "aenderungsfaktor") right = ttk.LabelFrame(self.tab_kalibrierung, text="Meterfaktoren / Durchflussraten", padding=10) right.pack(side="left", fill="both", expand=True) self._add_entry(right, 0, "Meter-Faktor 1", "meter_faktor_1") self._add_entry(right, 1, "Meter-Faktor 2", "meter_faktor_2") self._add_entry(right, 2, "Meter-Faktor 3", "meter_faktor_3") self._add_entry(right, 3, "Meter-Faktor 4", "meter_faktor_4") self._add_entry(right, 4, "Durchfluss-Rate 1", "durchfluss_rate_1") self._add_entry(right, 5, "Durchfluss-Rate 2", "durchfluss_rate_2") self._add_entry(right, 6, "Durchfluss-Rate 3", "durchfluss_rate_3") self._add_entry(right, 7, "Durchfluss-Rate 4", "durchfluss_rate_4") def _build_device_tab(self): left = ttk.LabelFrame(self.tab_geraet, text="Geräteeinstellungen", padding=10) left.pack(side="left", fill="both", expand=True, padx=(0, 8)) self._add_entry(left, 0, "Minimum Abgaben", "minimum_abgaben") self._add_entry(left, 1, "Minimal-Vorlauf (L)", "minimal_vorlauf_l") self._add_entry(left, 2, "Minimal-Volumen (L)", "minimal_volumen_l") self._add_check(left, 3, "Preiskorrektur aktiv", "preiskorrektur") self._add_check(left, 4, "Additiv ausweisen", "additiv_ausweisen") self._add_entry(left, 5, "Auto-Abgabe-Stop (min)", "auto_abgabe_stop_min") self._add_entry(left, 6, "Zusatzprüfung", "zusatz_pruefung") self._add_entry(left, 7, "Ventil-Steuerung", "ventil_steuerung") self._add_entry(left, 8, "Entgasung-Steuerung", "entgasung_steuerung") self._add_check(left, 9, "Abfüll-Sicherung", "abfuell_sicherung") right = ttk.LabelFrame(self.tab_geraet, text="Pulszähler / Drucker", padding=10) right.pack(side="left", fill="both", expand=True) self._add_entry(right, 0, "Pulswertigkeit (L)", "pulswertigkeit_l") self._add_entry(right, 1, "Max. Durchfluss", "max_durchfluss") self._add_entry(right, 2, "Max. Fehlerpulse", "max_fehlerpulse") self._add_entry(right, 3, "Sensor-Typ", "sensor_typ") self._add_entry(right, 4, "Min. Durchfluss", "min_durchfluss") self._add_entry(right, 5, "Drehrichtung", "drehrichtung") self._add_entry(right, 6, "Drucker-Auswahl", "drucker_auswahl") self._add_check(right, 7, "Drucker-Protokoll aktiv", "drucker_protokoll") self._add_entry(right, 8, "Max. Fehlversuche", "max_fehlversuche") def _build_readonly_tab(self): frame = ttk.LabelFrame(self.tab_readonly, text="Nur Anzeige – typische Protokoll-/Summenstände", padding=10) frame.pack(fill="both", expand=True) cols = ("parameter", "wert", "hinweis") tree = ttk.Treeview(frame, columns=cols, show="headings", height=12) tree.pack(fill="both", expand=True) tree.heading("parameter", text="Parameter") tree.heading("wert", text="Beispielwert") tree.heading("hinweis", text="Hinweis") rows = [ ("Unkompensiertes Volumen", "16995 L", "Read-only; Summenzähler"), ("Kompensiertes Volumen", "17211 L", "Read-only; Summenzähler"), ("Kompensierte Masse", "14309 kg", "Read-only; Summenzähler"), ("Additive Gesamt", "0.0 L", "Read-only; Summenzähler"), ("Nicht berechnet", "0.0 L", "Read-only; Summenzähler"), ("Seriennummer", "16CH0111", "Gerätekennung; nicht Arbeitsparameter"), ("Siegelzahl", "000013", "Eich-/Sicherheitsinformation"), ("Berichtsdatum", "06.09.2023 14:34", "Ausdruckwert; kein Sollparameter"), ] for row in rows: tree.insert("", "end", values=row) note = ttk.Label( frame, text=( "Einschätzung für deine UI: Summenstände wie 'Unkompensiertes Volumen' sollten in der Regel nicht editierbar sein. " "Das sind Gesamtzähler bzw. Protokollwerte. Veränderbar sind eher Kalibrier- und Geräteeinstellungen." ), wraplength=1050, justify="left", ) note.pack(anchor="w", pady=(10, 0)) def _build_manual_tab(self): wrapper = ttk.Frame(self.tab_manual) wrapper.pack(fill="both", expand=True) self.manual_text = tk.Text(wrapper, wrap="word") self.manual_text.pack(side="left", fill="both", expand=True) scroll = ttk.Scrollbar(wrapper, orient="vertical", command=self.manual_text.yview) scroll.pack(side="right", fill="y") self.manual_text.configure(yscrollcommand=scroll.set) btns = ttk.Frame(self.tab_manual) btns.pack(fill="x", pady=(8, 0)) ttk.Button(btns, text="Vorschau aktualisieren", command=self.update_manual_preview).pack(side="left") def _load_defaults_into_ui(self): for key, value in asdict(self.data).items(): var = self.variables.get(key) if var is None: continue if isinstance(var, tk.BooleanVar): var.set(bool(value)) else: var.set(str(value)) self.update_manual_preview() def _read_ui_into_data(self): current = asdict(self.data) for key, old_value in current.items(): var = self.variables.get(key) if var is None: continue raw = var.get() try: if isinstance(old_value, bool): current[key] = bool(raw) elif isinstance(old_value, int) and not isinstance(old_value, bool): current[key] = int(float(raw)) elif isinstance(old_value, float): current[key] = float(str(raw).replace(",", ".")) else: current[key] = str(raw) except Exception as exc: raise ValueError(f"Ungültiger Wert für {key}: {raw}") from exc self.data = ProtocolData(**current) def reset_defaults(self): self.data = ProtocolData() self._load_defaults_into_ui() messagebox.showinfo(APP_TITLE, "Standardwerte geladen.") def save_json(self): try: self._read_ui_into_data() except ValueError as exc: messagebox.showerror(APP_TITLE, str(exc)) return path = filedialog.asksaveasfilename( defaultextension=".json", filetypes=[("JSON-Dateien", "*.json")], title="Parameter als JSON speichern", ) if not path: return with open(path, "w", encoding="utf-8") as f: json.dump(asdict(self.data), f, ensure_ascii=False, indent=2) messagebox.showinfo(APP_TITLE, f"Gespeichert:\n{path}") def load_json(self): path = filedialog.askopenfilename( filetypes=[("JSON-Dateien", "*.json")], title="Parameter-JSON laden", ) if not path: return try: with open(path, "r", encoding="utf-8") as f: data = json.load(f) self.data = ProtocolData(**data) self._load_defaults_into_ui() messagebox.showinfo(APP_TITLE, f"Geladen:\n{path}") except Exception as exc: messagebox.showerror(APP_TITLE, f"Datei konnte nicht geladen werden:\n{exc}") def _build_manual(self) -> str: d = self.data lines: List[str] = [] lines.append("MULTIFLOW – SCHRITT-FÜR-SCHRITT-ANLEITUNG ZUR PARAMETERPFLEGE") lines.append("=" * 68) lines.append("") lines.append("Wichtige Annahmen dieser Anleitung:") lines.append("- Die Produktdefinitionen bleiben unverändert.") lines.append("- Geändert werden nur technische Parameter und Kalibrierwerte.") lines.append("- Summenstände wie unkompensiertes Volumen sind nur Anzeigewerte.") lines.append("") lines.append("Welche Parameter in dieser UI als variabel behandelt werden:") lines.extend([ f"- Temperatur-Verschiebung bei 0 °C: {d.sensor_verschiebung_0c}", f"- Temperatur-Verschiebung bei 100 °C: {d.sensor_verschiebung_100c}", f"- Sensor ausgeschaltet: {'Ja' if d.sensor_ausgeschaltet else 'Nein'}", f"- Kompensationstyp: {d.kompensation_typ}", f"- Kompensationstemperatur: {d.kompensation_temp_c} °C", f"- Produktgruppe: {d.produktgruppe}", f"- Mittlere Dichte: {d.mittlere_dichte}", f"- Meter-Faktoren 1–4: {d.meter_faktor_1}, {d.meter_faktor_2}, {d.meter_faktor_3}, {d.meter_faktor_4}", f"- Durchfluss-Raten 1–4: {d.durchfluss_rate_1}, {d.durchfluss_rate_2}, {d.durchfluss_rate_3}, {d.durchfluss_rate_4}", f"- Temperaturgrenzen: {d.min_temp_c} bis {d.max_temp_c} °C", f"- Änderungsfaktor: {d.aenderungsfaktor}", f"- Minimal-Vorlauf: {d.minimal_vorlauf_l} L", f"- Minimal-Volumen: {d.minimal_volumen_l} L", f"- Preiskorrektur: {'Ja' if d.preiskorrektur else 'Nein'}", f"- Additiv ausweisen: {'Ja' if d.additiv_ausweisen else 'Nein'}", f"- Auto-Abgabe-Stop: {d.auto_abgabe_stop_min} min", f"- Pulswertigkeit: {d.pulswertigkeit_l} L", f"- Max. Durchfluss: {d.max_durchfluss}", f"- Max. Fehlerpulse: {d.max_fehlerpulse}", f"- Sensor-Typ: {d.sensor_typ}", f"- Min. Durchfluss: {d.min_durchfluss}", f"- Drehrichtung: {d.drehrichtung}", f"- Drucker-Auswahl: {d.drucker_auswahl}", f"- Drucker-Protokoll: {'Ja' if d.drucker_protokoll else 'Nein'}", f"- Max. Fehlversuche: {d.max_fehlversuche}", f"- Ventil-Steuerung: {d.ventil_steuerung}", f"- Entgasung-Steuerung: {d.entgasung_steuerung}", f"- Abfüll-Sicherung: {'Ja' if d.abfuell_sicherung else 'Nein'}", ]) lines.append("") lines.append("Nicht variabel in dieser UI:") lines.extend([ f"- Unkompensiertes Volumen: {d.unkompensiertes_volumen_l} L (nur Summenzähler)", f"- Kompensiertes Volumen: {d.kompensiertes_volumen_l} L (nur Summenzähler)", f"- Kompensierte Masse: {d.kompensierte_masse_kg} kg (nur Summenzähler)", "- Produktdefinitionen Heizöl EL / Diesel / Super E5 (in dieser Version fest)", ]) lines.append("") lines.append("SCHRITT-FÜR-SCHRITT FÜR EINE UNGELERNTE KRAFT") lines.append("-" * 68) steps = [ "1. Starten Sie das Programm 'MultiFlow UI + Schritt-für-Schritt-Anleitung'.", "2. Klicken Sie oben auf den Reiter 'Kalibrierwerte'.", "3. Tragen Sie bei 'Temperatur-Verschiebung bei 0 °C' den gewünschten Wert ein.", "4. Tragen Sie bei 'Temperatur-Verschiebung bei 100 °C' den gewünschten Wert ein.", "5. Prüfen Sie, ob das Häkchen 'Sensor ausgeschaltet' gesetzt ist. Nur setzen, wenn der Sensor wirklich deaktiviert werden soll.", "6. Tragen Sie den gewünschten Kompensationstyp ein, zum Beispiel VCF.", "7. Tragen Sie die Kompensationstemperatur ein, normalerweise 15,0 °C.", "8. Tragen Sie die mittlere Dichte ein, wenn diese geändert werden soll.", "9. Geben Sie die Meter-Faktoren 1 bis 4 ein. Wenn nur ein Meter-Faktor benutzt wird, lassen Sie Faktor 2 bis 4 auf 1,0 und die Raten 2 bis 4 auf 0.", "10. Geben Sie die Durchfluss-Rate 1 ein. Diese ist meist die maximale Durchflussmenge.", "11. Klicken Sie auf den Reiter 'Gerät / Sensorik'.", "12. Prüfen oder ändern Sie dort Minimal-Vorlauf, Minimal-Volumen, Pulswertigkeit, max. Durchfluss und Sensor-Typ.", "13. Wenn ein Parameter nicht geändert werden soll, lassen Sie den vorhandenen Wert einfach stehen.", "14. Klicken Sie oben auf 'Variablen prüfen'. Kontrollieren Sie die Zusammenfassung im Hinweisfenster.", "15. Wenn alles richtig ist, klicken Sie auf 'JSON speichern', damit die Werte dokumentiert werden.", "16. Klicken Sie danach auf 'Anleitung als TXT exportieren'.", f"17. Speichern Sie die Datei an einem leicht auffindbaren Ort, zum Beispiel auf dem Desktop als '{DEFAULT_OUTPUT}'.", "18. Öffnen Sie den Reiter 'Vorschau Anleitung', wenn Sie die automatisch erzeugte Anleitung direkt lesen möchten.", "19. Die Summenstände im Reiter 'Nur Anzeige' dienen nur zur Kontrolle. Diese Werte bitte nicht als Eingabewerte behandeln.", "20. Beenden Sie das Programm erst, wenn JSON und Anleitung erfolgreich gespeichert wurden.", ] lines.extend(steps) lines.append("") lines.append("Praxishinweis:") lines.append( "Wenn du später doch weitere Felder editierbar machen willst, kannst du sie im Code sehr einfach von 'Nur Anzeige' " "nach 'Gerät / Sensorik' oder 'Kalibrierwerte' verschieben." ) return "\n".join(lines) def update_manual_preview(self): try: self._read_ui_into_data() except ValueError as exc: messagebox.showerror(APP_TITLE, str(exc)) return manual = self._build_manual() self.manual_text.delete("1.0", "end") self.manual_text.insert("1.0", manual) def export_manual(self): try: self._read_ui_into_data() except ValueError as exc: messagebox.showerror(APP_TITLE, str(exc)) return manual = self._build_manual() path = filedialog.asksaveasfilename( defaultextension=".txt", initialfile=DEFAULT_OUTPUT, filetypes=[("Textdateien", "*.txt")], title="Anleitung exportieren", ) if not path: return with open(path, "w", encoding="utf-8") as f: f.write(manual) messagebox.showinfo(APP_TITLE, f"Anleitung gespeichert:\n{path}") def show_variable_summary(self): try: self._read_ui_into_data() except ValueError as exc: messagebox.showerror(APP_TITLE, str(exc)) return d = self.data summary = ( "Als veränderbar eingeordnet:\n\n" "- Temperaturversätze\n" "- Kompensation / Dichte / Temperaturgrenzen\n" "- Meter-Faktoren und Durchfluss-Raten\n" "- Minimal-Vorlauf / Minimal-Volumen\n" "- Pulszähler- und Sensorwerte\n" "- Drucker- und Ventileinstellungen\n\n" "Nicht veränderbar in dieser Version:\n\n" "- Unkompensiertes Volumen\n" "- Kompensiertes Volumen\n" "- Kompensierte Masse\n" "- Additive Gesamt\n" "- Produktdefinitionen\n\n" f"Aktuell eingetragene Temperaturverschiebung: 0 °C = {d.sensor_verschiebung_0c}, 100 °C = {d.sensor_verschiebung_100c}" ) messagebox.showinfo("Variable Parameter", summary) def main(): root = tk.Tk() style = ttk.Style(root) try: style.theme_use("clam") except tk.TclError: pass app = MultiFlowApp(root) root.mainloop() if __name__ == "__main__": main()