""" Drucker-Blueprint für MYP Platform Enthält alle Routen und Funktionen zur Druckerverwaltung, Statusüberwachung und Steuerung. """ import os import json import time from datetime import datetime, timedelta from flask import Blueprint, request, jsonify, current_app, abort, Response from flask_login import login_required, current_user from werkzeug.utils import secure_filename from werkzeug.exceptions import NotFound, BadRequest from sqlalchemy import func, desc, asc from sqlalchemy.exc import SQLAlchemyError from typing import Dict, List, Tuple, Any, Optional from models import Printer, User, Job, get_db_session from utils.logging_config import get_logger, measure_execution_time from utils.security_suite import require_permission, Permission, check_permission from utils.hardware_integration import printer_monitor, tapo_controller from utils.drag_drop_system import drag_drop_manager # Logger initialisieren printers_logger = get_logger("printers") # Blueprint erstellen printers_blueprint = Blueprint("printers", __name__, url_prefix="/api/printers") @printers_blueprint.route("", methods=["POST"]) @login_required @require_permission(Permission.ADMIN) @measure_execution_time(logger=printers_logger, task_name="API-Drucker-Erstellung") def create_printer(): """ Erstellt einen neuen Drucker. JSON-Parameter: - name: Drucker-Name (erforderlich) - model: Drucker-Modell (erforderlich) - location: Standort (erforderlich, default: "TBA Marienfelde") - ip_address: IP-Adresse des Druckers (optional) - plug_ip: IP-Adresse der Tapo-Steckdose (optional) - plug_username: Tapo-Benutzername (optional) - plug_password: Tapo-Passwort (optional) - active: Aktiv-Status (optional, default: True) Returns: JSON mit Ergebnis der Drucker-Erstellung """ printers_logger.info(f"🖨️ Drucker-Erstellung von Admin {current_user.name}") # Parameter validieren data = request.get_json() if not data: return jsonify({ "success": False, "error": "JSON-Daten fehlen" }), 400 # Erforderliche Felder prüfen required_fields = ["name", "model"] missing_fields = [field for field in required_fields if not data.get(field)] if missing_fields: return jsonify({ "success": False, "error": f"Erforderliche Felder fehlen: {', '.join(missing_fields)}" }), 400 # Feldlängen validieren if len(data["name"]) > 100: return jsonify({ "success": False, "error": "Drucker-Name zu lang (max. 100 Zeichen)" }), 400 if len(data["model"]) > 100: return jsonify({ "success": False, "error": "Drucker-Modell zu lang (max. 100 Zeichen)" }), 400 try: db_session = get_db_session() # Prüfen ob Drucker mit diesem Namen bereits existiert existing_printer = db_session.query(Printer).filter( func.lower(Printer.name) == func.lower(data["name"]) ).first() if existing_printer: db_session.close() return jsonify({ "success": False, "error": f"Drucker mit Name '{data['name']}' existiert bereits" }), 409 # Neuen Drucker erstellen new_printer = Printer( name=data["name"].strip(), model=data["model"].strip(), location=data.get("location", "TBA Marienfelde").strip(), ip_address=data.get("ip_address", "").strip() or None, plug_ip=data.get("plug_ip", "").strip() or None, plug_username=data.get("plug_username", "").strip() or None, plug_password=data.get("plug_password", "").strip() or None, active=data.get("active", True), status="offline", created_at=datetime.now(), last_checked=None ) db_session.add(new_printer) db_session.commit() # Drucker-ID für Response speichern printer_id = new_printer.id printer_name = new_printer.name db_session.close() printers_logger.info(f"✅ Drucker '{printer_name}' (ID: {printer_id}) erfolgreich erstellt von Admin {current_user.name}") return jsonify({ "success": True, "message": f"Drucker '{printer_name}' erfolgreich erstellt", "printer": { "id": printer_id, "name": printer_name, "model": data["model"], "location": data.get("location", "TBA Marienfelde"), "ip_address": data.get("ip_address"), "plug_ip": data.get("plug_ip"), "active": data.get("active", True), "status": "offline", "created_at": datetime.now().isoformat() }, "created_by": { "id": current_user.id, "name": current_user.name }, "timestamp": datetime.now().isoformat() }), 201 except SQLAlchemyError as e: printers_logger.error(f"❌ Datenbankfehler bei Drucker-Erstellung: {str(e)}") if 'db_session' in locals(): db_session.rollback() db_session.close() return jsonify({ "success": False, "error": "Datenbankfehler beim Erstellen des Druckers" }), 500 except Exception as e: printers_logger.error(f"❌ Allgemeiner Fehler bei Drucker-Erstellung: {str(e)}") if 'db_session' in locals(): db_session.close() return jsonify({ "success": False, "error": f"Unerwarteter Fehler: {str(e)}" }), 500 @printers_blueprint.route("/monitor/live-status", methods=["GET"]) @login_required @measure_execution_time(logger=printers_logger, task_name="API-Live-Drucker-Status-Abfrage") def get_live_printer_status(): """ Liefert den aktuellen Live-Status aller Drucker. Query-Parameter: - use_cache: ob Cache verwendet werden soll (default: true) Returns: JSON mit Live-Status aller Drucker """ printers_logger.info(f"🔄 Live-Status-Abfrage von Benutzer {current_user.name} (ID: {current_user.id})") # Parameter auslesen use_cache_param = request.args.get("use_cache", "true").lower() use_cache = use_cache_param == "true" try: # Live-Status über den PrinterMonitor abrufen status_data = printer_monitor.get_live_printer_status(use_session_cache=use_cache) # Zusammenfassung der Druckerstatus erstellen summary = printer_monitor.get_printer_summary() # Antwort mit Status und Zusammenfassung response = { "success": True, "status": status_data, "summary": summary, "timestamp": datetime.now().isoformat(), "cache_used": use_cache } printers_logger.info(f"✅ Live-Status-Abfrage erfolgreich: {len(status_data)} Drucker") return jsonify(response) except Exception as e: printers_logger.error(f"❌ Fehler bei Live-Status-Abfrage: {str(e)}") return jsonify({ "success": False, "error": "Fehler bei Abfrage des Druckerstatus", "message": str(e) }), 500 @printers_blueprint.route("/status", methods=["GET"]) @login_required @measure_execution_time(logger=printers_logger, task_name="API-Drucker-Status-Abfrage") def get_printer_status(): """ Liefert den aktuellen Status aller Drucker. Dieser Endpunkt ist kompatibel mit dem Frontend printer_monitor.js Query-Parameter: - force_refresh: true = Cache umgehen und echte Netzwerk-Tests (default: false) Returns: JSON mit Status aller Drucker """ # Force-Refresh Parameter prüfen force_refresh = request.args.get('force_refresh', 'false').lower() == 'true' refresh_type = "Force-Refresh" if force_refresh else "Normal" printers_logger.info(f"🔄 {refresh_type} Status-Abfrage von Benutzer {current_user.name} (ID: {current_user.id})") try: # Nur TBA Marienfelde Drucker aus Datenbank holen db_session = get_db_session() printers = db_session.query(Printer).filter( Printer.location == "TBA Marienfelde" ).all() # Status-Daten für jeden Drucker sammeln - MIT LIVE TAPO-STATUS printer_data = [] status_summary = { 'total': len(printers), 'available': 0, # Erreichbar & aus → frei 'busy': 0, # Erreichbar & an → besetzt 'unreachable': 0, # Nicht erreichbar 'unconfigured': 0, # Keine Steckdose konfiguriert 'error': 0 } # Hardware Integration Monitor importieren try: from utils.hardware_integration import printer_monitor tapo_manager = printer_monitor except ImportError: tapo_manager = None printers_logger.warning("⚠️ Hardware Integration Monitor nicht verfügbar") for printer in printers: # Basis-Drucker-Daten printer_info = { 'id': printer.id, 'name': printer.name, 'model': printer.model, 'location': printer.location, 'ip_address': printer.ip_address, 'plug_ip': printer.plug_ip, 'has_plug': bool(printer.plug_ip), 'last_checked': printer.last_checked.isoformat() if printer.last_checked else None, 'created_at': printer.created_at.isoformat() if printer.created_at else None } # LIVE TAPO-STATUS ABRUFEN (Kernlogik mit Force-Refresh) if printer.plug_ip and tapo_manager: try: # Live-Status über Tapo-Manager abrufen (mit Cache-Bypass bei force_refresh) live_status = tapo_manager.get_printer_status(printer.id, force_refresh=force_refresh) # Status basierend auf Tapo-Erreichbarkeit und Schaltzustand plug_reachable = live_status.get('plug_reachable', False) power_status = live_status.get('power_status', None) if not plug_reachable: # Steckdose nicht erreichbar printer_info['status'] = 'unreachable' printer_info['status_detail'] = 'Steckdose nicht erreichbar' elif power_status == 'on': # Steckdose erreichbar & an → Drucker läuft printer_info['status'] = 'busy' printer_info['status_detail'] = 'Drucker läuft - besetzt' elif power_status == 'off': # Steckdose erreichbar & aus → Drucker verfügbar printer_info['status'] = 'available' printer_info['status_detail'] = 'Verfügbar - kann reserviert werden' else: # Unbekannter Status printer_info['status'] = 'error' printer_info['status_detail'] = 'Status unbekannt' # Zusätzliche Tapo-Informationen printer_info['plug_reachable'] = plug_reachable printer_info['power_status'] = power_status printer_info['can_control'] = live_status.get('can_control', False) printer_info['last_tapo_check'] = live_status.get('last_checked') except Exception as e: printers_logger.error(f"❌ Fehler bei Tapo-Status für Drucker {printer.id}: {e}") printer_info['status'] = 'error' printer_info['status_detail'] = f'Tapo-Fehler: {str(e)}' printer_info['plug_reachable'] = False printer_info['power_status'] = None printer_info['can_control'] = False else: # Keine Steckdose konfiguriert oder Tapo-Manager nicht verfügbar printer_info['status'] = 'unconfigured' printer_info['status_detail'] = 'Keine Smart Plug konfiguriert' printer_info['plug_reachable'] = False printer_info['power_status'] = None printer_info['can_control'] = False # Status-Zusammenfassung aktualisieren status = printer_info['status'] if status in status_summary: status_summary[status] += 1 else: status_summary['error'] += 1 # Aktive Jobs zählen active_jobs = db_session.query(Job).filter( Job.printer_id == printer.id, Job.status.in_(["running", "printing", "active", "scheduled"]) ).count() printer_info['active_jobs'] = active_jobs printer_info['has_active_jobs'] = active_jobs > 0 # Verfügbarkeit für Reservierung printer_info['can_reserve'] = ( printer_info['status'] == 'available' and active_jobs == 0 and printer_info['can_control'] ) printer_data.append(printer_info) printers_logger.debug( f"📊 Drucker {printer.name}: Status={printer_info['status']}, " f"Plug-IP={printer.plug_ip}, Erreichbar={printer_info['plug_reachable']}, " f"Power={printer_info['power_status']}" ) db_session.close() # Antwort mit Status und Zusammenfassung response = { "success": True, "printers": printer_data, "summary": status_summary, "timestamp": datetime.now().isoformat() } printers_logger.info(f"✅ Status-Abfrage erfolgreich: {len(printer_data)} Drucker") return jsonify(response) except Exception as e: printers_logger.error(f"❌ Fehler bei Status-Abfrage: {str(e)}") if 'db_session' in locals(): db_session.close() return jsonify({ "success": False, "error": "Fehler bei Abfrage des Druckerstatus", "message": str(e) }), 500 @printers_blueprint.route("/control//power", methods=["POST"]) @login_required @require_permission(Permission.CONTROL_PRINTER) # Verwende die bereits vorhandene Berechtigung @measure_execution_time(logger=printers_logger, task_name="API-Drucker-Stromversorgung-Steuerung") def control_printer_power(printer_id): """ Steuert die Stromversorgung eines Druckers (ein-/ausschalten). Args: printer_id: ID des zu steuernden Druckers JSON-Parameter: - action: "on" oder "off" Returns: JSON mit Ergebnis der Steuerungsaktion """ printers_logger.info(f"🔌 Stromsteuerung für Drucker {printer_id} von Benutzer {current_user.name}") # Parameter validieren data = request.get_json() if not data or "action" not in data: return jsonify({ "success": False, "error": "Parameter 'action' fehlt" }), 400 action = data["action"] if action not in ["on", "off"]: return jsonify({ "success": False, "error": "Ungültige Aktion. Erlaubt sind 'on' oder 'off'." }), 400 try: # Drucker aus Datenbank holen db_session = get_db_session() printer = db_session.query(Printer).filter(Printer.id == printer_id).first() if not printer: db_session.close() return jsonify({ "success": False, "error": f"Drucker mit ID {printer_id} nicht gefunden" }), 404 # Prüfen, ob Drucker eine Steckdose konfiguriert hat if not printer.plug_ip or not printer.plug_username or not printer.plug_password: db_session.close() return jsonify({ "success": False, "error": f"Drucker {printer.name} hat keine Steckdose konfiguriert" }), 400 # Steckdose steuern from PyP100 import PyP110 try: # TP-Link Tapo P110 Verbindung herstellen p110 = PyP110.P110(printer.plug_ip, printer.plug_username, printer.plug_password) p110.handshake() # Authentifizierung p110.login() # Login # Steckdose ein- oder ausschalten if action == "on": p110.turnOn() success = True message = "Steckdose erfolgreich eingeschaltet" printer.status = "starting" # Status aktualisieren else: p110.turnOff() success = True message = "Steckdose erfolgreich ausgeschaltet" printer.status = "offline" # Status aktualisieren # Zeitpunkt der letzten Prüfung aktualisieren printer.last_checked = datetime.now() db_session.commit() # Cache leeren, damit neue Status-Abfragen aktuell sind printer_monitor.clear_all_caches() printers_logger.info(f"✅ {action.upper()}: Drucker {printer.name} erfolgreich {message}") except Exception as e: printers_logger.error(f"❌ Fehler bei Steckdosensteuerung für {printer.name}: {str(e)}") db_session.close() return jsonify({ "success": False, "error": f"Fehler bei Steckdosensteuerung: {str(e)}" }), 500 db_session.close() return jsonify({ "success": True, "message": message, "printer_id": printer_id, "printer_name": printer.name, "action": action, "timestamp": datetime.now().isoformat() }) except Exception as e: printers_logger.error(f"❌ Allgemeiner Fehler bei Stromsteuerung: {str(e)}") return jsonify({ "success": False, "error": f"Allgemeiner Fehler: {str(e)}" }), 500 @printers_blueprint.route("/force-refresh", methods=["POST"]) @login_required @measure_execution_time(logger=printers_logger, task_name="API-Force-Refresh-Alle-Drucker") def force_refresh_all_printer_status(): """ Forciert komplette Netzwerk-Neuprüfung aller Drucker-Status. Invalidiert alle Caches und führt echte Netzwerk-Tests durch. Für Verwendung nach Netzwerkwechseln oder bei Cache-Problemen. Returns: JSON mit Force-Refresh-Ergebnissen """ printers_logger.info(f"🔄 Force-Refresh aller Drucker von Benutzer {current_user.name} (ID: {current_user.id})") try: # Hardware Integration Monitor für Force-Refresh verwenden from utils.hardware_integration import printer_monitor # Force-Network-Refresh durchführen refresh_results = printer_monitor.force_network_refresh() if refresh_results.get("success", False): printers_logger.info(f"✅ Force-Refresh erfolgreich: {refresh_results.get('printers_refreshed', 0)} Drucker aktualisiert") return jsonify({ "success": True, "message": "Alle Drucker-Status erfolgreich aktualisiert", "refresh_results": refresh_results, "performed_by": { "id": current_user.id, "name": current_user.name }, "timestamp": datetime.now().isoformat() }) else: printers_logger.error(f"❌ Force-Refresh fehlgeschlagen: {refresh_results.get('error', 'Unbekannter Fehler')}") return jsonify({ "success": False, "error": "Force-Refresh fehlgeschlagen", "details": refresh_results, "timestamp": datetime.now().isoformat() }), 500 except Exception as e: printers_logger.error(f"❌ Allgemeiner Fehler bei Force-Refresh: {str(e)}") return jsonify({ "success": False, "error": f"Fehler beim Force-Refresh: {str(e)}", "timestamp": datetime.now().isoformat() }), 500 @printers_blueprint.route("/test/socket/", methods=["GET"]) @login_required @require_permission(Permission.ADMIN) @measure_execution_time(logger=printers_logger, task_name="API-Steckdosen-Test-Status") def test_socket_status(printer_id): """ Prüft den aktuellen Status einer Steckdose für Testzwecke (nur für Ausbilder/Administratoren). Args: printer_id: ID des Druckers dessen Steckdose getestet werden soll Returns: JSON mit detailliertem Status der Steckdose und Warnungen """ printers_logger.info(f"🔍 Steckdosen-Test-Status für Drucker {printer_id} von Admin {current_user.name}") try: # Drucker aus Datenbank holen db_session = get_db_session() printer = db_session.query(Printer).filter(Printer.id == printer_id).first() if not printer: db_session.close() return jsonify({ "success": False, "error": f"Drucker mit ID {printer_id} nicht gefunden" }), 404 # Prüfen, ob Drucker eine Steckdose konfiguriert hat if not printer.plug_ip or not printer.plug_username or not printer.plug_password: db_session.close() return jsonify({ "success": False, "error": f"Drucker {printer.name} hat keine Steckdose konfiguriert", "warning": "Steckdose kann nicht getestet werden - Konfiguration fehlt" }), 400 # Prüfen, ob der Drucker gerade aktive Jobs hat active_jobs = db_session.query(Job).filter( Job.printer_id == printer_id, Job.status.in_(["running", "printing", "active"]) ).all() db_session.close() # Steckdosen-Status prüfen from PyP100 import PyP110 socket_status = None socket_info = None error_message = None try: # TP-Link Tapo P110 Verbindung herstellen p110 = PyP110.P110(printer.plug_ip, printer.plug_username, printer.plug_password) p110.handshake() # Authentifizierung p110.login() # Login # Geräteinformationen abrufen device_info = p110.getDeviceInfo() socket_status = "online" if device_info["result"]["device_on"] else "offline" # Energieverbrauch abrufen (falls verfügbar) try: energy_info = p110.getEnergyUsage() current_power = energy_info.get("result", {}).get("current_power", 0) except: current_power = None socket_info = { "device_on": device_info["result"]["device_on"], "signal_level": device_info["result"].get("signal_level", 0), "current_power": current_power, "device_id": device_info["result"].get("device_id", "Unbekannt"), "model": device_info["result"].get("model", "Unbekannt"), "hw_ver": device_info["result"].get("hw_ver", "Unbekannt"), "fw_ver": device_info["result"].get("fw_ver", "Unbekannt") } except Exception as e: printers_logger.warning(f"⚠️ Fehler bei Steckdosen-Status-Abfrage für {printer.name}: {str(e)}") socket_status = "error" error_message = str(e) # Warnungen und Empfehlungen zusammenstellen warnings = [] recommendations = [] risk_level = "low" if active_jobs: warnings.append(f"ACHTUNG: Drucker hat {len(active_jobs)} aktive(n) Job(s)!") risk_level = "high" recommendations.append("Warten Sie bis alle Jobs abgeschlossen sind bevor Sie die Steckdose ausschalten") if socket_status == "online" and socket_info and socket_info.get("device_on"): if socket_info.get("current_power", 0) > 10: # Mehr als 10W Verbrauch warnings.append(f"Drucker verbraucht aktuell {socket_info['current_power']}W - vermutlich aktiv") risk_level = "medium" if risk_level == "low" else risk_level recommendations.append("Prüfen Sie den Druckerstatus bevor Sie die Steckdose ausschalten") else: recommendations.append("Drucker scheint im Standby-Modus zu sein - Test sollte sicher möglich sein") if socket_status == "error": warnings.append("Steckdose nicht erreichbar - Netzwerk oder Konfigurationsproblem") recommendations.append("Prüfen Sie die Netzwerkverbindung und Steckdosen-Konfiguration") if not warnings and socket_status == "offline": recommendations.append("Steckdose ist ausgeschaltet - Test kann sicher durchgeführt werden") printers_logger.info(f"✅ Steckdosen-Test-Status erfolgreich abgerufen für {printer.name}") return jsonify({ "success": True, "printer": { "id": printer.id, "name": printer.name, "model": printer.model, "location": printer.location, "status": printer.status }, "socket": { "status": socket_status, "info": socket_info, "error": error_message, "ip_address": printer.plug_ip }, "safety": { "risk_level": risk_level, "warnings": warnings, "recommendations": recommendations, "active_jobs_count": len(active_jobs), "safe_to_test": len(warnings) == 0 }, "timestamp": datetime.now().isoformat() }) except Exception as e: printers_logger.error(f"❌ Allgemeiner Fehler bei Steckdosen-Test-Status: {str(e)}") return jsonify({ "success": False, "error": f"Allgemeiner Fehler: {str(e)}" }), 500 @printers_blueprint.route("/test/socket//control", methods=["POST"]) @login_required @require_permission(Permission.ADMIN) @measure_execution_time(logger=printers_logger, task_name="API-Steckdosen-Test-Steuerung") def test_socket_control(printer_id): """ Steuert eine Steckdose für Testzwecke (nur für Ausbilder/Administratoren). Diese Funktion zeigt Warnungen an, erlaubt aber trotzdem die Steuerung für Tests. Args: printer_id: ID des Druckers dessen Steckdose gesteuert werden soll JSON-Parameter: - action: "on" oder "off" - force: boolean - überschreibt Sicherheitswarnungen (default: false) - test_reason: string - Grund für den Test (optional) Returns: JSON mit Ergebnis der Steuerungsaktion und Warnungen """ printers_logger.info(f"🧪 Steckdosen-Test-Steuerung für Drucker {printer_id} von Admin {current_user.name}") # Parameter validieren data = request.get_json() if not data or "action" not in data: return jsonify({ "success": False, "error": "Parameter 'action' fehlt" }), 400 action = data["action"] if action not in ["on", "off"]: return jsonify({ "success": False, "error": "Ungültige Aktion. Erlaubt sind 'on' oder 'off'." }), 400 force = data.get("force", False) test_reason = data.get("test_reason", "Routinetest") try: # Drucker aus Datenbank holen db_session = get_db_session() printer = db_session.query(Printer).filter(Printer.id == printer_id).first() if not printer: db_session.close() return jsonify({ "success": False, "error": f"Drucker mit ID {printer_id} nicht gefunden" }), 404 # Prüfen, ob Drucker eine Steckdose konfiguriert hat if not printer.plug_ip or not printer.plug_username or not printer.plug_password: db_session.close() return jsonify({ "success": False, "error": f"Drucker {printer.name} hat keine Steckdose konfiguriert" }), 400 # Aktive Jobs prüfen active_jobs = db_session.query(Job).filter( Job.printer_id == printer_id, Job.status.in_(["running", "printing", "active"]) ).all() # Sicherheitsprüfungen warnings = [] should_block = False if active_jobs and action == "off": warnings.append(f"WARNUNG: {len(active_jobs)} aktive Job(s) würden abgebrochen!") if not force: should_block = True if should_block: db_session.close() return jsonify({ "success": False, "error": "Aktion blockiert aufgrund von Sicherheitsbedenken", "warnings": warnings, "hint": "Verwenden Sie 'force': true um die Aktion trotzdem auszuführen", "requires_force": True }), 409 # Conflict # Steckdose steuern from PyP100 import PyP110 try: # TP-Link Tapo P110 Verbindung herstellen p110 = PyP110.P110(printer.plug_ip, printer.plug_username, printer.plug_password) p110.handshake() # Authentifizierung p110.login() # Login # Aktuellen Status vor der Änderung abrufen device_info_before = p110.getDeviceInfo() status_before = device_info_before["result"]["device_on"] # Steckdose ein- oder ausschalten if action == "on": p110.turnOn() success = True message = "Steckdose für Test erfolgreich eingeschaltet" new_printer_status = "starting" else: p110.turnOff() success = True message = "Steckdose für Test erfolgreich ausgeschaltet" new_printer_status = "offline" # Kurz warten und neuen Status prüfen time.sleep(2) device_info_after = p110.getDeviceInfo() status_after = device_info_after["result"]["device_on"] # Drucker-Status aktualisieren printer.status = new_printer_status printer.last_checked = datetime.now() db_session.commit() # Cache leeren, damit neue Status-Abfragen aktuell sind printer_monitor.clear_all_caches() # Test-Eintrag für Audit-Log printers_logger.info(f"🧪 TEST DURCHGEFÜHRT: {action.upper()} für {printer.name} | " f"Admin: {current_user.name} | Grund: {test_reason} | " f"Force: {force} | Status: {status_before} → {status_after}") except Exception as e: printers_logger.error(f"❌ Fehler bei Test-Steckdosensteuerung für {printer.name}: {str(e)}") db_session.close() return jsonify({ "success": False, "error": f"Fehler bei Steckdosensteuerung: {str(e)}" }), 500 db_session.close() return jsonify({ "success": True, "message": message, "test_info": { "admin": current_user.name, "reason": test_reason, "forced": force, "status_before": status_before, "status_after": status_after }, "printer": { "id": printer_id, "name": printer.name, "status": new_printer_status }, "action": action, "warnings": warnings, "timestamp": datetime.now().isoformat() }) except Exception as e: printers_logger.error(f"❌ Allgemeiner Fehler bei Test-Steckdosensteuerung: {str(e)}") return jsonify({ "success": False, "error": f"Allgemeiner Fehler: {str(e)}" }), 500 @printers_blueprint.route("/test/all-sockets", methods=["GET"]) @login_required @require_permission(Permission.ADMIN) @measure_execution_time(logger=printers_logger, task_name="API-Alle-Steckdosen-Test-Status") def test_all_sockets_status(): """ Liefert den Test-Status aller konfigurierten Steckdosen (nur für Ausbilder/Administratoren). Returns: JSON mit Status aller Steckdosen und Gesamtübersicht """ printers_logger.info(f"🔍 Alle-Steckdosen-Test-Status von Admin {current_user.name}") try: # Alle Drucker mit Steckdosen-Konfiguration holen db_session = get_db_session() printers = db_session.query(Printer).filter( Printer.plug_ip.isnot(None), Printer.plug_username.isnot(None), Printer.plug_password.isnot(None) ).all() results = [] total_online = 0 total_offline = 0 total_error = 0 total_warnings = 0 from PyP100 import PyP110 for printer in printers: # Aktive Jobs für diesen Drucker prüfen active_jobs = db_session.query(Job).filter( Job.printer_id == printer.id, Job.status.in_(["running", "printing", "active"]) ).count() # Steckdosen-Status prüfen socket_status = "unknown" device_on = False current_power = None error_message = None warnings = [] try: p110 = PyP110.P110(printer.plug_ip, printer.plug_username, printer.plug_password) p110.handshake() p110.login() device_info = p110.getDeviceInfo() device_on = device_info["result"]["device_on"] socket_status = "online" if device_on else "offline" # Energieverbrauch abrufen try: energy_info = p110.getEnergyUsage() current_power = energy_info.get("result", {}).get("current_power", 0) except: current_power = None # Warnungen generieren if active_jobs > 0: warnings.append(f"{active_jobs} aktive Job(s)") if device_on and current_power and current_power > 10: warnings.append(f"Hoher Verbrauch: {current_power}W") except Exception as e: socket_status = "error" error_message = str(e) warnings.append(f"Verbindungsfehler: {str(e)[:50]}") # Statistiken aktualisieren if socket_status == "online": total_online += 1 elif socket_status == "offline": total_offline += 1 else: total_error += 1 if warnings: total_warnings += 1 results.append({ "printer": { "id": printer.id, "name": printer.name, "model": printer.model, "location": printer.location }, "socket": { "status": socket_status, "device_on": device_on, "current_power": current_power, "ip_address": printer.plug_ip, "error": error_message }, "warnings": warnings, "active_jobs": active_jobs, "safe_to_test": len(warnings) == 0 }) db_session.close() # Gesamtübersicht erstellen summary = { "total_sockets": len(results), "online": total_online, "offline": total_offline, "error": total_error, "with_warnings": total_warnings, "safe_to_test": len(results) - total_warnings } printers_logger.info(f"✅ Alle-Steckdosen-Status erfolgreich abgerufen: {len(results)} Steckdosen") return jsonify({ "success": True, "sockets": results, "summary": summary, "timestamp": datetime.now().isoformat() }) except Exception as e: printers_logger.error(f"❌ Fehler bei Alle-Steckdosen-Test-Status: {str(e)}") return jsonify({ "success": False, "error": f"Allgemeiner Fehler: {str(e)}" }), 500 # ============================================================================= # DRAG & DROP API - JOB-REIHENFOLGE-MANAGEMENT # ============================================================================= @printers_blueprint.route("//jobs/order", methods=["GET"]) @login_required @measure_execution_time(logger=printers_logger, task_name="API-Job-Reihenfolge-Abfrage") def get_job_order(printer_id): """ Holt die aktuelle Job-Reihenfolge für einen Drucker. Args: printer_id: ID des Druckers Returns: JSON mit Jobs in der korrekten Reihenfolge """ printers_logger.info(f"📋 Job-Reihenfolge-Abfrage für Drucker {printer_id} von Benutzer {current_user.name}") try: # Drucker existiert prüfen db_session = get_db_session() printer = db_session.query(Printer).filter(Printer.id == printer_id).first() if not printer: db_session.close() return jsonify({ "success": False, "error": f"Drucker mit ID {printer_id} nicht gefunden" }), 404 db_session.close() # Job-Reihenfolge und Details holen ordered_jobs = drag_drop_manager.get_ordered_jobs_for_printer(printer_id) job_order_ids = drag_drop_manager.get_job_order(printer_id) # Job-Details für Response aufbereiten jobs_data = [] for job in ordered_jobs: jobs_data.append({ "id": job.id, "name": job.name, "description": job.description, "user_name": job.user.name if job.user else "Unbekannt", "user_id": job.user_id, "duration_minutes": job.duration_minutes, "created_at": job.created_at.isoformat() if job.created_at else None, "start_at": job.start_at.isoformat() if job.start_at else None, "status": job.status, "file_path": job.file_path }) printers_logger.info(f"✅ Job-Reihenfolge erfolgreich abgerufen: {len(jobs_data)} Jobs für Drucker {printer.name}") return jsonify({ "success": True, "printer": { "id": printer.id, "name": printer.name, "model": printer.model, "location": printer.location }, "jobs": jobs_data, "job_order": job_order_ids, "total_jobs": len(jobs_data), "total_duration_minutes": sum(job.duration_minutes for job in ordered_jobs), "timestamp": datetime.now().isoformat() }) except Exception as e: printers_logger.error(f"❌ Fehler bei Job-Reihenfolge-Abfrage für Drucker {printer_id}: {str(e)}") return jsonify({ "success": False, "error": f"Fehler beim Laden der Job-Reihenfolge: {str(e)}" }), 500 @printers_blueprint.route("//jobs/order", methods=["POST"]) @login_required @require_permission(Permission.APPROVE_JOBS) # Nur Benutzer mit Job-Genehmigungsrechten können Reihenfolge ändern @measure_execution_time(logger=printers_logger, task_name="API-Job-Reihenfolge-Update") def update_job_order(printer_id): """ Aktualisiert die Job-Reihenfolge für einen Drucker per Drag & Drop. Args: printer_id: ID des Druckers JSON-Parameter: - job_ids: Liste der Job-IDs in der gewünschten Reihenfolge Returns: JSON mit Bestätigung der Aktualisierung """ printers_logger.info(f"🔄 Job-Reihenfolge-Update für Drucker {printer_id} von Benutzer {current_user.name}") # Parameter validieren data = request.get_json() if not data or "job_ids" not in data: return jsonify({ "success": False, "error": "Parameter 'job_ids' fehlt" }), 400 job_ids = data["job_ids"] if not isinstance(job_ids, list): return jsonify({ "success": False, "error": "Parameter 'job_ids' muss eine Liste sein" }), 400 if not all(isinstance(job_id, int) for job_id in job_ids): return jsonify({ "success": False, "error": "Alle Job-IDs müssen Zahlen sein" }), 400 try: # Drucker existiert prüfen db_session = get_db_session() printer = db_session.query(Printer).filter(Printer.id == printer_id).first() if not printer: db_session.close() return jsonify({ "success": False, "error": f"Drucker mit ID {printer_id} nicht gefunden" }), 404 # Validierung: Alle Jobs gehören zum Drucker und sind editierbar valid_jobs = db_session.query(Job).filter( Job.id.in_(job_ids), Job.printer_id == printer_id, Job.status.in_(['scheduled', 'paused']) ).all() db_session.close() if len(valid_jobs) != len(job_ids): invalid_ids = set(job_ids) - {job.id for job in valid_jobs} return jsonify({ "success": False, "error": f"Ungültige oder nicht editierbare Job-IDs: {list(invalid_ids)}" }), 400 # Berechtigung prüfen: Benutzer kann nur eigene Jobs oder als Admin alle verschieben if not current_user.is_admin: user_job_ids = {job.id for job in valid_jobs if job.user_id == current_user.id} if user_job_ids != set(job_ids): unauthorized_ids = set(job_ids) - user_job_ids return jsonify({ "success": False, "error": f"Keine Berechtigung für Jobs: {list(unauthorized_ids)}" }), 403 # Job-Reihenfolge aktualisieren success = drag_drop_manager.update_job_order(printer_id, job_ids) if success: # Neue Reihenfolge zur Bestätigung laden updated_order = drag_drop_manager.get_job_order(printer_id) printers_logger.info(f"✅ Job-Reihenfolge erfolgreich aktualisiert für Drucker {printer.name}") printers_logger.info(f" Neue Reihenfolge: {job_ids}") printers_logger.info(f" Benutzer: {current_user.name} (ID: {current_user.id})") return jsonify({ "success": True, "message": "Job-Reihenfolge erfolgreich aktualisiert", "printer": { "id": printer.id, "name": printer.name }, "old_order": job_ids, # Eingabe des Benutzers "new_order": updated_order, # Bestätigung aus Datenbank "total_jobs": len(job_ids), "updated_by": { "id": current_user.id, "name": current_user.name }, "timestamp": datetime.now().isoformat() }) else: return jsonify({ "success": False, "error": "Fehler beim Speichern der Job-Reihenfolge" }), 500 except Exception as e: printers_logger.error(f"❌ Fehler bei Job-Reihenfolge-Update für Drucker {printer_id}: {str(e)}") return jsonify({ "success": False, "error": f"Unerwarteter Fehler: {str(e)}" }), 500 @printers_blueprint.route("//jobs/summary", methods=["GET"]) @login_required @measure_execution_time(logger=printers_logger, task_name="API-Drucker-Job-Zusammenfassung") def get_printer_job_summary(printer_id): """ Erstellt eine detaillierte Zusammenfassung der Jobs für einen Drucker. Args: printer_id: ID des Druckers Returns: JSON mit Zusammenfassung, Statistiken und Zeitschätzungen """ printers_logger.info(f"📊 Drucker-Job-Zusammenfassung für Drucker {printer_id} von Benutzer {current_user.name}") try: # Drucker existiert prüfen db_session = get_db_session() printer = db_session.query(Printer).filter(Printer.id == printer_id).first() if not printer: db_session.close() return jsonify({ "success": False, "error": f"Drucker mit ID {printer_id} nicht gefunden" }), 404 db_session.close() # Zusammenfassung über Drag-Drop-Manager erstellen summary = drag_drop_manager.get_printer_summary(printer_id) printers_logger.info(f"✅ Drucker-Job-Zusammenfassung erfolgreich erstellt für {printer.name}") return jsonify({ "success": True, "printer": { "id": printer.id, "name": printer.name, "model": printer.model, "location": printer.location, "status": printer.status }, "summary": summary, "timestamp": datetime.now().isoformat() }) except Exception as e: printers_logger.error(f"❌ Fehler bei Drucker-Job-Zusammenfassung für Drucker {printer_id}: {str(e)}") return jsonify({ "success": False, "error": f"Fehler beim Erstellen der Zusammenfassung: {str(e)}" }), 500 @printers_blueprint.route("/jobs/cleanup-orders", methods=["POST"]) @login_required @require_permission(Permission.ADMIN) @measure_execution_time(logger=printers_logger, task_name="API-Job-Reihenfolgen-Bereinigung") def cleanup_job_orders(): """ Bereinigt ungültige Job-Reihenfolgen (nur für Administratoren). Entfernt Einträge für abgeschlossene oder gelöschte Jobs. Returns: JSON mit Bereinigungsergebnis """ printers_logger.info(f"🧹 Job-Reihenfolgen-Bereinigung von Admin {current_user.name}") try: # Bereinigung durchführen drag_drop_manager.cleanup_invalid_orders() printers_logger.info(f"✅ Job-Reihenfolgen-Bereinigung erfolgreich abgeschlossen") return jsonify({ "success": True, "message": "Job-Reihenfolgen erfolgreich bereinigt", "admin": { "id": current_user.id, "name": current_user.name }, "timestamp": datetime.now().isoformat() }) except Exception as e: printers_logger.error(f"❌ Fehler bei Job-Reihenfolgen-Bereinigung: {str(e)}") return jsonify({ "success": False, "error": f"Fehler bei der Bereinigung: {str(e)}" }), 500 @printers_blueprint.route("/drag-drop/config", methods=["GET"]) @login_required def get_drag_drop_config(): """ Liefert die Konfiguration für das Drag & Drop System. Returns: JSON mit Drag & Drop Konfiguration und JavaScript/CSS """ printers_logger.info(f"⚙️ Drag-Drop-Konfiguration abgerufen von Benutzer {current_user.name}") try: from utils.drag_drop_system import get_drag_drop_javascript, get_drag_drop_css # Benutzerberechtigungen prüfen can_reorder_jobs = check_permission(current_user, Permission.APPROVE_JOBS) can_upload_files = check_permission(current_user, Permission.CREATE_JOB) config = { "permissions": { "can_reorder_jobs": can_reorder_jobs, "can_upload_files": can_upload_files, "is_admin": current_user.is_admin }, "settings": { "max_file_size": 50 * 1024 * 1024, # 50MB "accepted_file_types": ["gcode", "stl", "3mf", "obj"], "auto_upload": False, "show_preview": True, "enable_progress_tracking": True }, "endpoints": { "get_job_order": f"/api/printers/{{printer_id}}/jobs/order", "update_job_order": f"/api/printers/{{printer_id}}/jobs/order", "get_summary": f"/api/printers/{{printer_id}}/jobs/summary" }, "javascript": get_drag_drop_javascript(), "css": get_drag_drop_css() } return jsonify({ "success": True, "config": config, "user": { "id": current_user.id, "name": current_user.name, "role": current_user.role }, "timestamp": datetime.now().isoformat() }) except Exception as e: printers_logger.error(f"❌ Fehler bei Drag-Drop-Konfiguration: {str(e)}") return jsonify({ "success": False, "error": f"Fehler beim Laden der Konfiguration: {str(e)}" }), 500 # ============================================================================= # ENDE DRAG & DROP API # ============================================================================= @printers_blueprint.route("/tapo/status-check", methods=["POST"]) @login_required @require_permission(Permission.CONTROL_PRINTER) @measure_execution_time(logger=printers_logger, task_name="API-Massenhafte-Tapo-Status-Prüfung") def mass_tapo_status_check(): """ Führt eine vollständige Tapo-Status-Überprüfung für alle Drucker durch. Returns: JSON mit detailliertem Status aller Tapo-Steckdosen """ printers_logger.info(f"Massenhafte Tapo-Status-Prüfung von Benutzer {current_user.name}") try: db_session = get_db_session() # Alle Drucker laden all_printers = db_session.query(Printer).order_by(Printer.name).limit(50).all() # Tapo-Controller laden try: from utils.hardware_integration import tapo_controller tapo_available = True except Exception as e: db_session.close() return jsonify({ "success": False, "error": f"Tapo-Controller nicht verfügbar: {str(e)}", "tapo_available": False }), 500 printer_status = [] summary = { "total_printers": len(all_printers), "printers_with_tapo": 0, "printers_without_tapo": 0, "tapo_online": 0, "tapo_offline": 0, "tapo_unreachable": 0, "configuration_issues": 0 } for printer in all_printers: printer_info = { "id": printer.id, "name": printer.name, "model": printer.model, "location": printer.location, "active": printer.active, "has_tapo_config": bool(printer.plug_ip), "plug_ip": printer.plug_ip, "last_checked": datetime.now() } if not printer.plug_ip: # Drucker ohne Tapo-Konfiguration summary["printers_without_tapo"] += 1 printer_info.update({ "tapo_status": "not_configured", "tapo_reachable": False, "power_status": None, "recommendations": ["Tapo-Steckdose konfigurieren für automatische Steuerung"] }) else: # Drucker mit Tapo-Konfiguration summary["printers_with_tapo"] += 1 # Konfigurationsprüfung config_issues = [] if not printer.plug_username: config_issues.append("Tapo-Benutzername fehlt") if not printer.plug_password: config_issues.append("Tapo-Passwort fehlt") if config_issues: summary["configuration_issues"] += 1 printer_info.update({ "tapo_status": "configuration_error", "tapo_reachable": False, "power_status": None, "config_issues": config_issues, "recommendations": ["Tapo-Anmeldedaten vervollständigen"] }) else: # Vollständige Konfiguration - Status prüfen try: reachable, status = tapo_controller.check_outlet_status( printer.plug_ip, printer_id=printer.id ) if reachable: if status == "on": summary["tapo_online"] += 1 status_type = "online" recommendations = [] else: summary["tapo_offline"] += 1 status_type = "offline" recommendations = ["Steckdose kann bei Bedarf eingeschaltet werden"] else: summary["tapo_unreachable"] += 1 status_type = "unreachable" recommendations = ["Netzwerkverbindung prüfen", "IP-Adresse überprüfen"] printer_info.update({ "tapo_status": status_type, "tapo_reachable": reachable, "power_status": status, "recommendations": recommendations }) # Drucker-Status in DB aktualisieren if reachable: printer.last_checked = datetime.now() if status == "on": printer.status = "online" else: printer.status = "offline" else: printer.status = "unreachable" except Exception as tapo_error: summary["tapo_unreachable"] += 1 printer_info.update({ "tapo_status": "error", "tapo_reachable": False, "power_status": None, "error": str(tapo_error), "recommendations": ["Tapo-Verbindung prüfen", "Anmeldedaten überprüfen"] }) printers_logger.warning(f"Tapo-Fehler für {printer.name}: {str(tapo_error)}") # Aktuelle Jobs für zusätzliche Info active_jobs = db_session.query(Job).filter( Job.printer_id == printer.id, Job.status.in_(["running", "printing", "active", "scheduled"]) ).count() printer_info["active_jobs"] = active_jobs if active_jobs > 0: printer_info.setdefault("recommendations", []).append( f"Vorsicht: {active_jobs} aktive Job(s) bei Steckdosen-Änderungen" ) printer_status.append(printer_info) # Änderungen in DB speichern db_session.commit() db_session.close() # Übersicht der Ergebnisse coverage_percentage = (summary["printers_with_tapo"] / summary["total_printers"] * 100) if summary["total_printers"] > 0 else 0 health_score = ((summary["tapo_online"] + summary["tapo_offline"]) / summary["printers_with_tapo"] * 100) if summary["printers_with_tapo"] > 0 else 0 printers_logger.info(f"Tapo-Status-Check abgeschlossen: {summary['printers_with_tapo']} konfiguriert, " f"{summary['tapo_online']} online, {summary['tapo_unreachable']} nicht erreichbar") return jsonify({ "success": True, "tapo_available": tapo_available, "printers": printer_status, "summary": summary, "metrics": { "coverage_percentage": round(coverage_percentage, 1), "health_score": round(health_score, 1), "needs_attention": summary["configuration_issues"] + summary["tapo_unreachable"] }, "timestamp": datetime.now().isoformat() }) except Exception as e: printers_logger.error(f"Unerwarteter Fehler bei Massenhafte-Tapo-Status-Prüfung: {str(e)}") if 'db_session' in locals(): db_session.close() return jsonify({ "success": False, "error": f"Systemfehler: {str(e)}" }), 500 @printers_blueprint.route("/tapo/configuration-wizard", methods=["POST"]) @login_required @require_permission(Permission.ADMIN) @measure_execution_time(logger=printers_logger, task_name="API-Tapo-Konfigurationsassistent") def tapo_configuration_wizard(): """ Automatischer Konfigurationsassistent für Tapo-Steckdosen. Versucht automatisch verfügbare Steckdosen zu erkennen und zu konfigurieren. """ printers_logger.info(f"Tapo-Konfigurationsassistent von Admin {current_user.name}") try: data = request.get_json() auto_configure = data.get('auto_configure', True) test_ips = data.get('test_ips', []) # Tapo-Controller laden try: from utils.hardware_integration import tapo_controller except Exception as e: return jsonify({ "success": False, "error": f"Tapo-Controller nicht verfügbar: {str(e)}" }), 500 db_session = get_db_session() # Standard-IP-Bereich für Mercedes-Benz TBA (normalerweise 192.168.1.201-206) if not test_ips: test_ips = [f"192.168.1.{i}" for i in range(201, 207)] # 6 Standard-Arbeitsplätze discovery_results = { "tested_ips": test_ips, "discovered_devices": [], "configured_printers": [], "errors": [] } printers_logger.info(f"Teste {len(test_ips)} IP-Adressen auf Tapo-Geräte...") # Discovery für jede IP-Adresse for ip in test_ips: try: printers_logger.debug(f"Teste IP: {ip}") # Ping-Test mit 5 Sekunden Timeout if not tapo_controller.ping_address(ip, timeout=5): discovery_results["errors"].append(f"{ip}: Nicht erreichbar (Ping fehlgeschlagen)") continue # Tapo-Verbindungstest test_result = tapo_controller.test_connection(ip) if test_result["success"]: device_info = test_result.get("device_info", {}) discovered_device = { "ip": ip, "device_info": device_info, "nickname": device_info.get("nickname", f"Tapo Device {ip}"), "model": device_info.get("model", "Unknown"), "device_on": device_info.get("device_on", False) } discovery_results["discovered_devices"].append(discovered_device) printers_logger.info(f"✅ Tapo-Gerät gefunden: {ip} - {discovered_device['nickname']}") # Auto-Konfiguration wenn gewünscht if auto_configure: # Suche nach Drucker ohne Tapo-Konfiguration unconfigured_printer = db_session.query(Printer).filter( Printer.plug_ip.is_(None), Printer.active == True ).first() if unconfigured_printer: # Konfiguriere den ersten verfügbaren Drucker unconfigured_printer.plug_ip = ip unconfigured_printer.plug_username = "admin" # Standard für Tapo unconfigured_printer.plug_password = "admin" # Standard für Tapo unconfigured_printer.last_checked = datetime.now() configured_info = { "printer_id": unconfigured_printer.id, "printer_name": unconfigured_printer.name, "tapo_ip": ip, "tapo_nickname": discovered_device['nickname'] } discovery_results["configured_printers"].append(configured_info) printers_logger.info(f"✅ Drucker '{unconfigured_printer.name}' automatisch mit {ip} verknüpft") else: discovery_results["errors"].append(f"{ip}: Tapo-Gerät gefunden, aber kein unkonfigurierter Drucker verfügbar") else: discovery_results["errors"].append(f"{ip}: Erreichbar, aber kein Tapo-Gerät oder Authentifizierung fehlgeschlagen") except Exception as ip_error: discovery_results["errors"].append(f"{ip}: Fehler beim Test - {str(ip_error)}") printers_logger.warning(f"Fehler beim Testen von {ip}: {str(ip_error)}") # Änderungen speichern db_session.commit() db_session.close() # Zusammenfassung summary = { "tested_ips": len(test_ips), "discovered_devices": len(discovery_results["discovered_devices"]), "configured_printers": len(discovery_results["configured_printers"]), "errors": len(discovery_results["errors"]) } printers_logger.info(f"Tapo-Konfigurationsassistent abgeschlossen: {summary}") return jsonify({ "success": True, "message": f"Discovery abgeschlossen: {summary['discovered_devices']} Geräte gefunden, " f"{summary['configured_printers']} Drucker konfiguriert", "results": discovery_results, "summary": summary, "timestamp": datetime.now().isoformat() }) except Exception as e: printers_logger.error(f"Fehler beim Tapo-Konfigurationsassistent: {str(e)}") if 'db_session' in locals(): db_session.close() return jsonify({ "success": False, "error": f"Systemfehler: {str(e)}" }), 500 @printers_blueprint.route("//connect", methods=["POST"]) @login_required @require_permission(Permission.CONTROL_PRINTER) @measure_execution_time(logger=printers_logger, task_name="API-Drucker-Verbindung") def connect_printer(printer_id): """ Verbindet einen Drucker (schaltet die Steckdose ein). Wrapper für control_printer_power mit action='on' Args: printer_id: ID des zu verbindenden Druckers Returns: JSON mit Ergebnis der Verbindungsaktion """ printers_logger.info(f"🔗 Drucker-Verbindung für Drucker {printer_id} von Benutzer {current_user.name}") try: # Sichere JSON-Handhabung für control_printer_power try: original_json = request.get_json(silent=True) except: original_json = None request._cached_json = ({"action": "on"}, True) # Delegiere an existing control_printer_power function result = control_printer_power(printer_id) # Response für connect-API anpassen if hasattr(result, 'get_json') and result.get_json().get('success'): data = result.get_json() return jsonify({ "success": True, "message": f"Verbindung zu Drucker {printer_id} hergestellt", "printer_id": printer_id, "printer_name": data.get('printer_name'), "action": "connect", "timestamp": data.get('timestamp') }) return result except Exception as e: printers_logger.error(f"❌ Fehler bei Drucker-Verbindung: {str(e)}") return jsonify({ "success": False, "error": f"Verbindungsfehler: {str(e)}" }), 500 @printers_blueprint.route("/tapo/validate-configuration/", methods=["POST"]) @login_required @require_permission(Permission.ADMIN) @measure_execution_time(logger=printers_logger, task_name="API-Tapo-Konfigurationsvalidierung") def validate_tapo_configuration(printer_id): """ Validiert die Tapo-Konfiguration eines spezifischen Druckers. Führt umfassende Tests durch: Ping, Authentifizierung, Funktionalität. """ printers_logger.info(f"Tapo-Konfigurationsvalidierung für Drucker {printer_id} von Admin {current_user.name}") try: db_session = get_db_session() printer = db_session.query(Printer).filter(Printer.id == printer_id).first() if not printer: db_session.close() return jsonify({ "success": False, "error": "Drucker nicht gefunden" }), 404 # Tapo-Controller laden try: from utils.hardware_integration import tapo_controller except Exception as e: db_session.close() return jsonify({ "success": False, "error": f"Tapo-Controller nicht verfügbar: {str(e)}" }), 500 validation_results = { "printer": { "id": printer.id, "name": printer.name, "model": printer.model, "location": printer.location }, "configuration": { "has_ip": bool(printer.plug_ip), "has_username": bool(printer.plug_username), "has_password": bool(printer.plug_password), "ip_address": printer.plug_ip }, "tests": { "ping": {"status": "not_run", "message": ""}, "authentication": {"status": "not_run", "message": ""}, "functionality": {"status": "not_run", "message": ""}, "device_info": {"status": "not_run", "message": ""} }, "overall_status": "unknown", "recommendations": [] } # Konfigurationsprüfung if not printer.plug_ip: validation_results["overall_status"] = "not_configured" validation_results["recommendations"].append("IP-Adresse der Tapo-Steckdose eintragen") elif not printer.plug_username or not printer.plug_password: validation_results["overall_status"] = "incomplete_config" validation_results["recommendations"].append("Benutzername und Passwort für Tapo-Steckdose eintragen") else: # Umfassende Tests durchführen all_tests_passed = True # Test 1: Ping/Erreichbarkeit try: ping_success = tapo_controller.ping_address(printer.plug_ip, timeout=5) if ping_success: validation_results["tests"]["ping"] = { "status": "passed", "message": "Steckdose ist im Netzwerk erreichbar" } else: validation_results["tests"]["ping"] = { "status": "failed", "message": "Steckdose nicht erreichbar - Netzwerkproblem oder falsche IP" } all_tests_passed = False validation_results["recommendations"].append("IP-Adresse überprüfen") validation_results["recommendations"].append("Netzwerkverbindung der Steckdose prüfen") except Exception as ping_error: validation_results["tests"]["ping"] = { "status": "error", "message": f"Ping-Test fehlgeschlagen: {str(ping_error)}" } all_tests_passed = False # Test 2: Authentifizierung (nur wenn Ping erfolgreich) if validation_results["tests"]["ping"]["status"] == "passed": try: auth_result = tapo_controller.test_connection( printer.plug_ip, username=printer.plug_username, password=printer.plug_password ) if auth_result["success"]: validation_results["tests"]["authentication"] = { "status": "passed", "message": "Authentifizierung erfolgreich" } # Geräteinformationen extrahieren device_info = auth_result.get("device_info", {}) validation_results["tests"]["device_info"] = { "status": "passed", "message": "Geräteinformationen abgerufen", "data": { "nickname": device_info.get("nickname", "Unbekannt"), "model": device_info.get("model", "Unbekannt"), "device_on": device_info.get("device_on", False), "signal_level": device_info.get("signal_level", 0) } } else: validation_results["tests"]["authentication"] = { "status": "failed", "message": f"Authentifizierung fehlgeschlagen: {auth_result.get('error', 'Unbekannt')}" } all_tests_passed = False validation_results["recommendations"].append("Benutzername und Passwort überprüfen") except Exception as auth_error: validation_results["tests"]["authentication"] = { "status": "error", "message": f"Authentifizierungstest fehlgeschlagen: {str(auth_error)}" } all_tests_passed = False # Test 3: Funktionalität (nur wenn Authentifizierung erfolgreich) if validation_results["tests"]["authentication"]["status"] == "passed": try: reachable, status = tapo_controller.check_outlet_status( printer.plug_ip, printer_id=printer_id ) if reachable: validation_results["tests"]["functionality"] = { "status": "passed", "message": f"Status erfolgreich abgerufen: {status}", "current_status": status } # Drucker-Status in DB aktualisieren printer.last_checked = datetime.now() printer.status = "online" if status == "on" else "offline" else: validation_results["tests"]["functionality"] = { "status": "failed", "message": "Status konnte nicht abgerufen werden" } all_tests_passed = False except Exception as func_error: validation_results["tests"]["functionality"] = { "status": "error", "message": f"Funktionalitätstest fehlgeschlagen: {str(func_error)}" } all_tests_passed = False # Gesamtstatus bestimmen if all_tests_passed: validation_results["overall_status"] = "fully_functional" validation_results["recommendations"].append("Konfiguration ist vollständig und funktional") else: failed_tests = [test for test, result in validation_results["tests"].items() if result["status"] in ["failed", "error"]] if "ping" in failed_tests: validation_results["overall_status"] = "network_issue" elif "authentication" in failed_tests: validation_results["overall_status"] = "auth_issue" elif "functionality" in failed_tests: validation_results["overall_status"] = "functionality_issue" else: validation_results["overall_status"] = "partial_failure" # Aktuelle Jobs als Sicherheitshinweis active_jobs = db_session.query(Job).filter( Job.printer_id == printer_id, Job.status.in_(["running", "printing", "active"]) ).count() if active_jobs > 0: validation_results["safety_warning"] = f"{active_jobs} aktive Job(s) - Vorsicht bei Steckdosen-Tests" db_session.commit() db_session.close() printers_logger.info(f"Tapo-Validierung für {printer.name} abgeschlossen: {validation_results['overall_status']}") return jsonify({ "success": True, "validation": validation_results, "timestamp": datetime.now().isoformat() }) except Exception as e: printers_logger.error(f"Fehler bei Tapo-Konfigurationsvalidierung: {str(e)}") if 'db_session' in locals(): db_session.close() return jsonify({ "success": False, "error": f"Systemfehler: {str(e)}" }), 500