diff --git a/backend/app/app.py b/backend/app/app.py index abf334b1..76dfb9f2 100644 --- a/backend/app/app.py +++ b/backend/app/app.py @@ -3498,8 +3498,40 @@ def toggle_printer_power(printer_id): return jsonify({"error": "Administratorrechte erforderlich"}), 403 try: - data = request.get_json() - state = data.get("state", True) # Standard: einschalten + # Robuste JSON-Datenverarbeitung + data = {} + try: + if request.is_json and request.get_json(): + data = request.get_json() + elif request.form: + # Fallback für Form-Daten + data = request.form.to_dict() + except Exception as json_error: + printers_logger.warning(f"Fehler beim Parsen der JSON-Daten für Drucker {printer_id}: {str(json_error)}") + # Verwende Standard-Werte wenn JSON-Parsing fehlschlägt + data = {} + + # Standard-Zustand ermitteln (Toggle-Verhalten) + db_session = get_db_session() + printer = db_session.query(Printer).get(printer_id) + + if not printer: + db_session.close() + return jsonify({"error": "Drucker nicht gefunden"}), 404 + + # Aktuellen Status ermitteln für Toggle-Verhalten + current_status = getattr(printer, 'status', 'offline') + current_active = getattr(printer, 'active', False) + + # Zielzustand bestimmen + if 'state' in data: + # Expliziter Zustand angegeben + state = bool(data.get("state", True)) + else: + # Toggle-Verhalten: Umschalten basierend auf aktuellem Status + state = not (current_status == "available" and current_active) + + db_session.close() # Steckdose schalten from utils.job_scheduler import toggle_plug @@ -3507,20 +3539,31 @@ def toggle_printer_power(printer_id): if success: action = "eingeschaltet" if state else "ausgeschaltet" + printers_logger.info(f"Drucker {printer.name} (ID: {printer_id}) erfolgreich {action} von Admin {current_user.name}") + return jsonify({ "success": True, "message": f"Drucker erfolgreich {action}", "printer_id": printer_id, - "state": state + "printer_name": printer.name, + "state": state, + "action": action }) else: + printers_logger.error(f"Fehler beim Schalten der Steckdose für Drucker {printer_id}") return jsonify({ - "error": "Fehler beim Schalten der Steckdose" + "success": False, + "error": "Fehler beim Schalten der Steckdose", + "printer_id": printer_id }), 500 except Exception as e: printers_logger.error(f"Fehler beim Schalten von Drucker {printer_id}: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 + return jsonify({ + "success": False, + "error": "Interner Serverfehler", + "details": str(e) + }), 500 @app.route("/api/admin/printers//test-tapo", methods=["POST"]) @login_required @@ -6293,6 +6336,7 @@ if __name__ == "__main__": app_logger.error(f"❌ Fehler beim Shutdown: {str(e)}") sys.exit(1) + # Signal-Handler registrieren (Windows-kompatibel) if os.name == 'nt': # Windows signal.signal(signal.SIGINT, signal_handler) @@ -6696,3 +6740,270 @@ def cleanup_temp_files(): except Exception as e: app_logger.error(f"Fehler beim Aufräumen temporärer Dateien: {str(e)}") return jsonify({'error': f'Fehler beim Aufräumen: {str(e)}'}), 500 + +# ===== FEHLENDE DRUCKER-SPEZIFISCHE API-ENDPOINTS ===== + +@app.route("/api/printers//jobs", methods=["GET"]) +@login_required +def get_printer_jobs(printer_id): + """Gibt alle Jobs für einen spezifischen Drucker zurück.""" + try: + db_session = get_db_session() + + # Prüfen ob Drucker existiert + printer = db_session.query(Printer).get(printer_id) + if not printer: + db_session.close() + return jsonify({"error": "Drucker nicht gefunden"}), 404 + + # Jobs für diesen Drucker abrufen + jobs = db_session.query(Job).filter(Job.printer_id == printer_id).order_by(Job.created_at.desc()).all() + + jobs_data = [] + for job in jobs: + job_data = { + "id": job.id, + "title": job.title, + "status": job.status, + "priority": job.priority, + "created_at": job.created_at.isoformat() if job.created_at else None, + "scheduled_time": job.scheduled_time.isoformat() if job.scheduled_time else None, + "started_at": job.started_at.isoformat() if job.started_at else None, + "finished_at": job.finished_at.isoformat() if job.finished_at else None, + "estimated_duration": job.estimated_duration, + "user_id": job.user_id, + "printer_id": job.printer_id, + "printer_name": printer.name + } + jobs_data.append(job_data) + + db_session.close() + + return jsonify({ + "jobs": jobs_data, + "total": len(jobs_data), + "printer": { + "id": printer.id, + "name": printer.name, + "status": printer.status + } + }) + + except Exception as e: + printers_logger.error(f"Fehler beim Abrufen der Jobs für Drucker {printer_id}: {str(e)}") + return jsonify({"error": "Interner Serverfehler"}), 500 + +@app.route("/api/printers//stats", methods=["GET"]) +@login_required +def get_printer_stats(printer_id): + """Gibt Statistiken für einen spezifischen Drucker zurück.""" + try: + db_session = get_db_session() + + # Prüfen ob Drucker existiert + printer = db_session.query(Printer).get(printer_id) + if not printer: + db_session.close() + return jsonify({"error": "Drucker nicht gefunden"}), 404 + + # Statistiken berechnen + total_jobs = db_session.query(Job).filter(Job.printer_id == printer_id).count() + completed_jobs = db_session.query(Job).filter( + Job.printer_id == printer_id, + Job.status == "completed" + ).count() + failed_jobs = db_session.query(Job).filter( + Job.printer_id == printer_id, + Job.status == "failed" + ).count() + active_jobs = db_session.query(Job).filter( + Job.printer_id == printer_id, + Job.status.in_(["scheduled", "running"]) + ).count() + + # Durchschnittliche Job-Dauer berechnen + avg_duration_result = db_session.query(func.avg(Job.estimated_duration)).filter( + Job.printer_id == printer_id, + Job.status == "completed", + Job.estimated_duration.isnot(None) + ).scalar() + + avg_duration = round(avg_duration_result, 2) if avg_duration_result else 0 + + # Erfolgsrate berechnen + success_rate = round((completed_jobs / total_jobs * 100), 2) if total_jobs > 0 else 0 + + # Letzte Aktivität + last_job = db_session.query(Job).filter(Job.printer_id == printer_id).order_by(Job.created_at.desc()).first() + last_activity = last_job.created_at.isoformat() if last_job and last_job.created_at else None + + db_session.close() + + stats_data = { + "printer": { + "id": printer.id, + "name": printer.name, + "status": printer.status, + "location": printer.location + }, + "jobs": { + "total": total_jobs, + "completed": completed_jobs, + "failed": failed_jobs, + "active": active_jobs, + "success_rate": success_rate + }, + "performance": { + "average_duration": avg_duration, + "last_activity": last_activity + }, + "generated_at": datetime.now().isoformat() + } + + return jsonify(stats_data) + + except Exception as e: + printers_logger.error(f"Fehler beim Abrufen der Statistiken für Drucker {printer_id}: {str(e)}") + return jsonify({"error": "Interner Serverfehler"}), 500 + +@app.route("/api/printers//test", methods=["POST"]) +@login_required +def test_printer_connection(printer_id): + """Testet die Verbindung zu einem spezifischen Drucker.""" + try: + db_session = get_db_session() + + # Prüfen ob Drucker existiert + printer = db_session.query(Printer).get(printer_id) + if not printer: + db_session.close() + return jsonify({"error": "Drucker nicht gefunden"}), 404 + + # IP-Adresse für Test ermitteln + ip_to_test = printer.plug_ip if printer.plug_ip else getattr(printer, 'ip_address', None) + + if not ip_to_test: + db_session.close() + return jsonify({ + "success": False, + "error": "Keine IP-Adresse für Drucker konfiguriert", + "printer": { + "id": printer.id, + "name": printer.name + } + }), 400 + + # Verbindungstest durchführen + printers_logger.info(f"Teste Verbindung zu Drucker {printer.name} (ID: {printer_id}) auf IP {ip_to_test}") + + status, active = check_printer_status(ip_to_test, timeout=10) + + # Status in Datenbank aktualisieren + printer.status = "available" if status == "online" else "offline" + if hasattr(printer, 'active'): + printer.active = active + db_session.commit() + + test_result = { + "success": status == "online", + "status": status, + "active": active, + "ip_address": ip_to_test, + "printer": { + "id": printer.id, + "name": printer.name, + "location": printer.location, + "model": printer.model + }, + "test_time": datetime.now().isoformat(), + "message": f"Drucker ist {'online und erreichbar' if status == 'online' else 'offline oder nicht erreichbar'}" + } + + db_session.close() + + printers_logger.info(f"Verbindungstest für Drucker {printer.name}: {status}") + + return jsonify(test_result) + + except Exception as e: + printers_logger.error(f"Fehler beim Testen der Verbindung zu Drucker {printer_id}: {str(e)}") + return jsonify({ + "success": False, + "error": "Interner Serverfehler beim Verbindungstest", + "details": str(e) + }), 500 + +# ===== ADMIN-SPEZIFISCHE DRUCKER-ENDPOINTS ===== + +@app.route("/api/admin/printers/create", methods=["POST"]) +@login_required +@admin_required +def admin_create_printer_api(): + """Admin-Endpoint zum Erstellen neuer Drucker.""" + try: + data = request.get_json() + + if not data: + return jsonify({"error": "Keine Daten empfangen"}), 400 + + # Pflichtfelder prüfen + required_fields = ["name", "plug_ip"] + for field in required_fields: + if field not in data or not data[field]: + return jsonify({"error": f"Feld '{field}' ist erforderlich"}), 400 + + db_session = get_db_session() + + # Prüfen, ob bereits ein Drucker mit diesem Namen existiert + existing_printer = db_session.query(Printer).filter(Printer.name == data["name"]).first() + if existing_printer: + db_session.close() + return jsonify({"error": "Ein Drucker mit diesem Namen existiert bereits"}), 400 + + # Neuen Drucker erstellen + new_printer = Printer( + name=data["name"], + model=data.get("model", ""), + location=data.get("location", ""), + mac_address=data.get("mac_address", ""), + plug_ip=data["plug_ip"], + status="offline", + active=True, + created_at=datetime.now() + ) + + db_session.add(new_printer) + db_session.commit() + + # Sofortiger Status-Check + if new_printer.plug_ip: + status, active = check_printer_status(new_printer.plug_ip) + new_printer.status = "available" if status == "online" else "offline" + new_printer.active = active + db_session.commit() + + printer_data = { + "id": new_printer.id, + "name": new_printer.name, + "model": new_printer.model, + "location": new_printer.location, + "mac_address": new_printer.mac_address, + "plug_ip": new_printer.plug_ip, + "status": new_printer.status, + "active": new_printer.active, + "created_at": new_printer.created_at.isoformat() + } + + db_session.close() + + printers_logger.info(f"Admin {current_user.name} hat Drucker '{new_printer.name}' erstellt") + + return jsonify({ + "success": True, + "message": "Drucker erfolgreich erstellt", + "printer": printer_data + }), 201 + + except Exception as e: + printers_logger.error(f"Fehler beim Erstellen eines Druckers durch Admin: {str(e)}") + return jsonify({"error": "Interner Serverfehler"}), 500 diff --git a/backend/app/test_tapo_sofort.py b/backend/app/test_tapo_sofort.py index 0519ecba..92dc7ebc 100644 --- a/backend/app/test_tapo_sofort.py +++ b/backend/app/test_tapo_sofort.py @@ -1 +1,207 @@ - \ No newline at end of file +#!/usr/bin/env python3 +# -*- coding: utf-8 -*- + +""" +SOFORT-TEST für TP-Link Tapo P110-Steckdosen +Testet die Verbindung mit echten Anmeldedaten und findet alle verfügbaren Steckdosen. +""" + +import sys +import time +import subprocess +from datetime import datetime + +# Tapo-Anmeldedaten (HARDKODIERT) +TAPO_USERNAME = "till.tomczak@mercedes-benz.com" +TAPO_PASSWORD = "744563017196" + +# IP-Bereiche zum Testen +IP_RANGES_TO_SCAN = [ + "192.168.1.{}", + "192.168.0.{}", + "10.0.0.{}", + "172.16.0.{}" +] + +def log_message(message, level="INFO"): + """Logge eine Nachricht mit Zeitstempel""" + timestamp = datetime.now().strftime("%H:%M:%S") + print(f"[{timestamp}] [{level}] {message}") + +def test_ping(ip_address): + """Teste ob eine IP erreichbar ist""" + try: + result = subprocess.run( + ['ping', '-n', '1', '-w', '1000', ip_address], + capture_output=True, + text=True, + timeout=2 + ) + return result.returncode == 0 + except: + return False + +def test_tapo_connection(ip_address): + """Teste Tapo-Verbindung zu einer IP""" + try: + from PyP100 import PyP110 + + log_message(f"Teste Tapo-Verbindung zu {ip_address}...") + p110 = PyP110.P110(ip_address, TAPO_USERNAME, TAPO_PASSWORD) + p110.handshake() # Authentifizierung + p110.login() # Login + + # Geräteinformationen abrufen + device_info = p110.getDeviceInfo() + + log_message(f"✅ ERFOLG: {ip_address} ist eine Tapo-Steckdose!") + log_message(f" 📛 Name: {device_info.get('nickname', 'Unbekannt')}") + log_message(f" ⚡ Status: {'EIN' if device_info.get('device_on', False) else 'AUS'}") + + if 'on_time' in device_info: + on_time = device_info.get('on_time', 0) + hours, minutes = divmod(on_time // 60, 60) + log_message(f" ⏱️ Betriebszeit: {hours}h {minutes}m") + + return True, device_info + + except Exception as e: + error_msg = str(e).lower() + if "login" in error_msg or "credentials" in error_msg: + log_message(f"❌ {ip_address}: Falsche Anmeldedaten") + elif "timeout" in error_msg: + log_message(f"❌ {ip_address}: Timeout") + elif "connect" in error_msg or "unreachable" in error_msg: + log_message(f"❌ {ip_address}: Nicht erreichbar") + else: + log_message(f"❌ {ip_address}: {str(e)}") + return False, None + +def main(): + """Hauptfunktion: Scanne Netzwerk nach Tapo-Steckdosen""" + log_message("🚀 SOFORT-TEST: TP-Link Tapo P110-Steckdosen") + log_message(f"👤 Benutzername: {TAPO_USERNAME}") + log_message(f"🔐 Passwort: {'*' * len(TAPO_PASSWORD)}") + print() + + # PyP100 Import testen + try: + from PyP100 import PyP110 + log_message("✅ PyP100-Modul erfolgreich importiert") + except ImportError: + log_message("❌ FEHLER: PyP100-Modul nicht gefunden!", "ERROR") + log_message(" Installiere mit: pip install PyP100", "ERROR") + return + + print() + log_message("🔍 Scanne Netzwerk nach erreichbaren Geräten...") + + # Erst bekannte IP-Adressen testen + known_ips = [ + "192.168.1.100", "192.168.1.101", "192.168.1.102", + "192.168.1.103", "192.168.1.104", "192.168.1.105", + "192.168.0.100", "192.168.0.101", "192.168.0.102" + ] + + found_steckdosen = [] + + for ip in known_ips: + if test_ping(ip): + log_message(f"🌐 {ip} ist erreichbar - teste Tapo...") + success, device_info = test_tapo_connection(ip) + if success: + found_steckdosen.append((ip, device_info)) + else: + log_message(f"⚫ {ip} nicht erreichbar") + + # Wenn keine gefunden, erweiterte Suche + if not found_steckdosen: + log_message("🔍 Keine Steckdosen in Standard-IPs gefunden - erweitere Suche...") + + for ip_range in IP_RANGES_TO_SCAN: + log_message(f"Scanne Bereich {ip_range.format('1-20')}...") + + for i in range(1, 21): # Erste 20 IPs pro Bereich + ip = ip_range.format(i) + if test_ping(ip): + log_message(f"🌐 {ip} erreichbar - teste Tapo...") + success, device_info = test_tapo_connection(ip) + if success: + found_steckdosen.append((ip, device_info)) + + # Ergebnisse + print() + log_message("📊 ERGEBNISSE:") + + if found_steckdosen: + log_message(f"🎉 {len(found_steckdosen)} Tapo-Steckdose(n) gefunden!") + print() + + for i, (ip, device_info) in enumerate(found_steckdosen, 1): + log_message(f"Steckdose #{i}:") + log_message(f" IP: {ip}") + log_message(f" Name: {device_info.get('nickname', 'Unbekannt')}") + log_message(f" Status: {'EIN' if device_info.get('device_on', False) else 'AUS'}") + log_message(f" Modell: {device_info.get('model', 'Unbekannt')}") + print() + + # Steckdosen-Test durchführen + log_message("🧪 Teste Ein/Aus-Schaltung der ersten Steckdose...") + test_ip, _ = found_steckdosen[0] + + try: + from PyP100 import PyP110 + p110 = PyP110.P110(test_ip, TAPO_USERNAME, TAPO_PASSWORD) + p110.handshake() + p110.login() + + # Aktuelle Status abrufen + device_info = p110.getDeviceInfo() + current_state = device_info.get('device_on', False) + + log_message(f"Aktueller Status: {'EIN' if current_state else 'AUS'}") + + # Gegenteil schalten + if current_state: + log_message("Schalte AUS...") + p110.turnOff() + time.sleep(2) + log_message("✅ Erfolgreich ausgeschaltet!") + + log_message("Schalte wieder EIN...") + p110.turnOn() + time.sleep(2) + log_message("✅ Erfolgreich eingeschaltet!") + else: + log_message("Schalte EIN...") + p110.turnOn() + time.sleep(2) + log_message("✅ Erfolgreich eingeschaltet!") + + log_message("Schalte wieder AUS...") + p110.turnOff() + time.sleep(2) + log_message("✅ Erfolgreich ausgeschaltet!") + + log_message("🎉 STECKDOSEN-STEUERUNG FUNKTIONIERT PERFEKT!") + + except Exception as e: + log_message(f"❌ Fehler beim Testen der Schaltung: {str(e)}", "ERROR") + + else: + log_message("❌ KEINE Tapo-Steckdosen gefunden!", "ERROR") + log_message("Mögliche Ursachen:", "ERROR") + log_message("1. Steckdosen sind in einem anderen IP-Bereich", "ERROR") + log_message("2. Anmeldedaten sind falsch", "ERROR") + log_message("3. Netzwerkprobleme", "ERROR") + log_message("4. Steckdosen sind nicht im Netzwerk", "ERROR") + +if __name__ == "__main__": + try: + main() + except KeyboardInterrupt: + log_message("Test abgebrochen durch Benutzer", "INFO") + except Exception as e: + log_message(f"Unerwarteter Fehler: {str(e)}", "ERROR") + import traceback + traceback.print_exc() \ No newline at end of file diff --git a/backend/app/utils/job_scheduler.py b/backend/app/utils/job_scheduler.py index fba7635c..beaf8d4b 100644 --- a/backend/app/utils/job_scheduler.py +++ b/backend/app/utils/job_scheduler.py @@ -9,6 +9,7 @@ from sqlalchemy.orm import joinedload from utils.logging_config import get_logger from models import Job, Printer, get_db_session +from config.settings import TAPO_USERNAME, TAPO_PASSWORD # Lazy logger initialization _logger = None @@ -313,15 +314,22 @@ def toggle_plug(printer_id: int, state: bool) -> bool: db_session.close() return False - # Konfiguration validieren - if not printer.plug_ip or not printer.plug_username or not printer.plug_password: - logger.error(f"Unvollständige Tapo-Konfiguration für Drucker {printer.name} (ID: {printer_id})") + # Konfiguration validieren und Fallback verwenden + plug_ip = printer.plug_ip + plug_username = printer.plug_username or TAPO_USERNAME + plug_password = printer.plug_password or TAPO_PASSWORD + + if not plug_ip: + logger.error(f"Keine Steckdosen-IP für Drucker {printer.name} (ID: {printer_id}) konfiguriert") db_session.close() return False + if not plug_username or not plug_password: + logger.error(f"Unvollständige Tapo-Konfiguration für Drucker {printer.name} (ID: {printer_id}) - verwende globale Anmeldedaten") + # TP-Link Tapo P110 Verbindung herstellen - logger.debug(f"Verbinde zu Tapo-Steckdose {printer.plug_ip} für Drucker {printer.name}") - p110 = PyP110.P110(printer.plug_ip, printer.plug_username, printer.plug_password) + logger.debug(f"Verbinde zu Tapo-Steckdose {plug_ip} für Drucker {printer.name}") + p110 = PyP110.P110(plug_ip, plug_username, plug_password) p110.handshake() # Authentifizierung p110.login() # Login @@ -341,14 +349,14 @@ def toggle_plug(printer_id: int, state: bool) -> bool: return False -def test_tapo_connection(ip_address: str, username: str, password: str) -> dict: +def test_tapo_connection(ip_address: str, username: str = None, password: str = None) -> dict: """ Testet die Verbindung zu einer Tapo-Steckdose und gibt detaillierte Informationen zurück. Args: ip_address: IP-Adresse der Steckdose - username: Benutzername - password: Passwort + username: Benutzername (optional, verwendet globale Konfiguration als Fallback) + password: Passwort (optional, verwendet globale Konfiguration als Fallback) Returns: dict: Testergebnis mit Status und Informationen @@ -361,6 +369,12 @@ def test_tapo_connection(ip_address: str, username: str, password: str) -> dict: "status": "unknown" } + # Fallback zu globalen Anmeldedaten + if not username or not password: + username = TAPO_USERNAME + password = TAPO_PASSWORD + logger.debug(f"🔧 Verwende globale Tapo-Anmeldedaten für {ip_address}") + try: logger.debug(f"Teste Tapo-Verbindung zu {ip_address}") p110 = PyP110.P110(ip_address, username, password) diff --git a/backend/app/utils/printer_monitor.py b/backend/app/utils/printer_monitor.py index 458a7c1c..bd636a02 100644 --- a/backend/app/utils/printer_monitor.py +++ b/backend/app/utils/printer_monitor.py @@ -17,7 +17,7 @@ import os from models import get_db_session, Printer from utils.logging_config import get_logger -from config.settings import PRINTERS +from config.settings import PRINTERS, TAPO_USERNAME, TAPO_PASSWORD # TP-Link Tapo P110 Unterstützung hinzufügen try: @@ -386,6 +386,12 @@ class PrinterMonitor: monitor_logger.debug("⚠️ PyP100-Modul nicht verfügbar - kann Tapo-Steckdosen-Status nicht abfragen") return False, "unknown" + # Fallback zu globalen Anmeldedaten wenn keine lokalen vorhanden + if not username or not password: + username = TAPO_USERNAME + password = TAPO_PASSWORD + monitor_logger.debug(f"🔧 Verwende globale Tapo-Anmeldedaten für {ip_address}") + try: # TP-Link Tapo P110 Verbindung herstellen p110 = PyP110.P110(ip_address, username, password)