Files
Projektarbeit-MYP/backend/blueprints/printers.py

1884 lines
75 KiB
Python

"""
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
}
# Tapo Status Manager importieren
try:
from utils.tapo_status_manager import TapoStatusManager
tapo_manager = TapoStatusManager()
except ImportError:
tapo_manager = None
printers_logger.warning("⚠️ TapoStatusManager 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/<int:printer_id>/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:
# Tapo Status Manager für Force-Refresh verwenden
from utils.tapo_status_manager import tapo_status_manager
# Force-Network-Refresh durchführen
refresh_results = tapo_status_manager.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/<int:printer_id>", 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/<int:printer_id>/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("/<int:printer_id>/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("/<int:printer_id>/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("/<int:printer_id>/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).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
if not tapo_controller.ping_address(ip, timeout=3):
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("/<int:printer_id>/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:
# Fake JSON für control_printer_power
original_json = request.get_json()
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/<int:printer_id>", 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