diff --git a/backend/app/FEHLER_BEHOBEN.md b/backend/app/FEHLER_BEHOBEN.md index 3b9990a2..b9966f79 100644 --- a/backend/app/FEHLER_BEHOBEN.md +++ b/backend/app/FEHLER_BEHOBEN.md @@ -1,3 +1,106 @@ +# MYP Platform - Behobene Fehler und Verbesserungen + +## 🎯 **KRITISCH BEHOBEN: 2025-05-29 21:16 - Druckererkennung mit TP-Link Tapo P110-Steckdosen** ✅ + +### Problem +Die Erkennung der "Drucker" (TP-Link Tapo P110-Steckdosen) funktionierte nicht zuverlässig: +- Veraltete HTTP-basierte API-Anfragen anstatt des PyP100-Moduls +- Fehlende Unterstützung für das spezielle Tapo-Protokoll +- Keine zuverlässige Steckdosen-Status-Erkennung +- Unvollständige Fehlerdiagnose-Tools + +### Behebung durchgeführt + +#### 1. **PyP100-Modul korrekt implementiert** +- **`utils/printer_monitor.py`**: + - `_check_outlet_status()` komplett überarbeitet für PyP100 + - `_turn_outlet_off()` modernisiert mit Tapo P110-Unterstützung + - Direkte Verwendung von `PyP110.P110()` mit `handshake()` und `login()` + +- **`app.py`**: + - `check_printer_status()` Funktion modernisiert für Tapo P110 + - Ersetzt HTTP-Anfragen durch PyP100-Modul + - Verbesserte Fehlerbehandlung und Logging + +- **`utils/job_scheduler.py`**: + - `toggle_plug()` verbessert mit Konfigurationvalidierung + - Bessere Fehlerbehandlung und detailliertes Logging + - Neue `test_tapo_connection()` Funktion hinzugefügt + +#### 2. **Neue Test- und Debug-Funktionen** +- **API-Routen hinzugefügt:** + - `POST /api/admin/printers//test-tapo` - Einzeltest einer Steckdose + - `POST /api/admin/printers/test-all-tapo` - Massentest aller Steckdosen + - Beide nur für Administratoren verfügbar + +- **Debug-Skript erweitert:** + - `utils/debug_drucker_erkennung.py` um `test_tapo_connections()` erweitert + - Detaillierte Fehleranalyse mit spezifischen Ursachen + - Zusammenfassung mit Erfolgsrate und Empfehlungen + +#### 3. **Verbesserte Fehlerdiagnose** +- **Detaillierte Logging-Nachrichten:** + - Emojis für bessere Lesbarkeit (✅ ❌ ⚠️ 🔐 🌐) + - Spezifische Fehlermeldungen für verschiedene Verbindungsprobleme + - Konfigurationvalidierung vor Verbindungsversuchen + +- **Erweiterte Geräteinformationen:** + - Gerätename (nickname) aus Tapo-Steckdose + - Ein/Aus-Status mit device_on + - Betriebszeit und Stromverbrauch (falls verfügbar) + +### Technische Details + +#### Ersetzt +```python +# ALT: HTTP-Anfragen mit Basic Auth +response = requests.get(f"http://{ip}/status", auth=(user, pass)) +``` + +#### Durch +```python +# NEU: PyP100-Modul mit Tapo-Protokoll +p110 = PyP110.P110(ip_address, username, password) +p110.handshake() # Authentifizierung +p110.login() # Login +device_info = p110.getDeviceInfo() +status = "on" if device_info.get('device_on', False) else "off" +``` + +### Verwendung + +#### Test einer einzelnen Steckdose +```python +from utils.job_scheduler import test_tapo_connection +result = test_tapo_connection("192.168.1.100", "username", "password") +print(f"Verbindung: {'✅' if result['success'] else '❌'}") +print(f"Status: {result['status']}") +``` + +#### Test aller Drucker via API +```bash +curl -X POST http://localhost:5000/api/admin/printers/test-all-tapo \ + -H "Authorization: Bearer " +``` + +#### Debug-Kommandos +```bash +# Vollständige Diagnose inkl. Tapo-Tests +python utils/debug_drucker_erkennung.py + +# Test der Steckdosen-Initialisierung +curl -X POST http://localhost:5000/api/printers/monitor/initialize-outlets +``` + +### Ergebnis +✅ **Zuverlässige Druckererkennung über TP-Link Tapo P110-Steckdosen** +✅ **Korrekte Verwendung des PyP100-Moduls** +✅ **Umfassende Test- und Debug-Tools** +✅ **Detaillierte Fehlerdiagnose und Logging** +✅ **Production-ready mit Admin-API-Endpunkten** + +--- + # JavaScript-Fehler behoben - MYP Platform ## Übersicht der behobenen Probleme @@ -319,4 +422,101 @@ Falsche Verwendung des Rate-Limiting-Decorators: - `blueprints/printer_monitor.py` - Decorator-Syntax korrigiert - `docs/live_drucker_system.md` - Dokumentation aktualisiert ---- \ No newline at end of file +--- + +## 2025-05-29 20:55 - ThreadPoolExecutor und Rate-Limiting-Probleme behoben + +### Problem +Das Live-Drucker-Monitor-System warf zwei kritische Fehler: +1. **ThreadPoolExecutor-Fehler**: "max_workers must be greater than 0" wenn keine aktiven Drucker vorhanden sind +2. **Rate-Limiting**: Die Limits waren zu niedrig gesetzt: + - `printer_monitor_live`: nur 5 Requests/60s + - `printer_monitor_summary`: nur 10 Requests/30s + +### Ursache +1. **ThreadPoolExecutor**: Der Code versuchte einen ThreadPool mit `min(len(printers), 8)` Workers zu erstellen, was bei 0 Druckern zu max_workers=0 führte +2. **Rate-Limiting**: Die Limits waren zu niedrig gesetzt: + - `printer_monitor_live`: nur 5 Requests/60s + - `printer_monitor_summary`: nur 10 Requests/30s + +### Lösung +1. **ThreadPoolExecutor-Fix** in `utils/printer_monitor.py` und `app.py`: + ```python + # Fehlerhafte Version: + with ThreadPoolExecutor(max_workers=min(len(printers), 8)) as executor: + + # Korrigierte Version: + if not printers: + monitor_logger.info("ℹ️ Keine aktiven Drucker gefunden") + return status_dict + + max_workers = min(max(len(printers), 1), 8) + with ThreadPoolExecutor(max_workers=max_workers) as executor: + ``` + +2. **Rate-Limiting gelockert** in `utils/rate_limiter.py`: + ```python + # Alte Limits: + 'printer_monitor_live': RateLimit(5, 60, "..."), + 'printer_monitor_summary': RateLimit(10, 30, "..."), + + # Neue Limits: + 'printer_monitor_live': RateLimit(30, 60, "..."), + 'printer_monitor_summary': RateLimit(60, 60, "..."), + ``` + +### Auswirkungen +- ✅ Keine ThreadPoolExecutor-Fehler mehr bei leeren Drucker-Listen +- ✅ Live-Monitoring funktioniert auch ohne konfigurierte Drucker +- ✅ Rate-Limits ermöglichen häufigere Live-Updates +- ✅ Bessere Logging-Ausgaben für Debugging +- ✅ Robustere Fehlerbehandlung + +### Getestete Szenarien +- System-Start ohne konfigurierte Drucker +- Live-Status-Updates mit häufigen API-Aufrufen +- Parallel-Status-Checks mit verschiedenen Drucker-Anzahlen + +--- + +## 2025-05-29 17:45 - Live-Drucker-Überwachungssystem implementiert + +### Implementierte Features +1. **PrinterMonitor-Klasse** (`utils/printer_monitor.py`) + - Multi-Level-Caching (Session 30s, DB 5min) + - Thread-sichere Implementierung + - Parallele Drucker-Abfragen via ThreadPoolExecutor + +2. **Automatische Steckdosen-Initialisierung** + - Beim Systemstart werden alle Steckdosen ausgeschaltet + - Fehlertolerante Implementierung mit detailliertem Logging + - Manuelle Admin-Initialisierung möglich + +3. **Smart-Plug-Integration** + - Unterstützung für TP-Link Tapo und generische APIs + - Ping-Tests + HTTP-Status-Abfragen + - Multiple Endpunkt-Tests mit Fallbacks + +4. **API-Endpunkte** + - `GET /api/printers/monitor/live-status` - Live-Status mit Caching + - `GET /api/printers/monitor/summary` - Schnelle Status-Übersicht + - `POST /api/printers/monitor/clear-cache` - Cache-Verwaltung + - `POST /api/printers/monitor/initialize-outlets` - Admin-Initialisierung + +5. **Frontend-Integration** + - JavaScript PrinterMonitor-Klasse (`static/js/printer_monitor.js`) + - Auto-Start auf relevanten Seiten + - Event-basierte Updates mit adaptiven Intervallen + +6. **Status-Kategorien** + - Online, Standby, Offline, Unreachable, Unconfigured + - Umfassende Status-Verfolgung mit visuellen Indikatoren + +7. **Performance-Features** + - Parallele Abfragen (max 8 Workers) + - Multi-Level-Caching + - Adaptive Timeouts und Exponential Backoff + - Sichtbarkeits-basierte Update-Intervalle + +### Systemdokumentation +Vollständige Dokumentation erstellt in `docs/live_drucker_system.md` mit Architektur, API-Beispielen und Troubleshooting-Guide. \ No newline at end of file diff --git a/backend/app/app.py b/backend/app/app.py index 3386f235..abf334b1 100644 --- a/backend/app/app.py +++ b/backend/app/app.py @@ -1281,7 +1281,7 @@ def kiosk_restart_system(): @measure_execution_time(logger=printers_logger, task_name="Drucker-Status-Prüfung") def check_printer_status(ip_address: str, timeout: int = 7) -> Tuple[str, bool]: """ - Überprüft den Status eines Druckers über Steckdosenabfrage mit Timeout. + Überprüft den Status eines Druckers über TP-Link Tapo P110-Steckdosenabfrage. Args: ip_address: IP-Adresse der Drucker-Steckdose @@ -1303,7 +1303,7 @@ def check_printer_status(ip_address: str, timeout: int = 7) -> Tuple[str, bool]: printers_logger.warning(f"Ungültige IP-Adresse: {ip_address}") return "offline", False - # Zuerst prüfen, ob die Steckdose erreichbar ist + # Zuerst prüfen, ob die Steckdose erreichbar ist (Ping) if os.name == 'nt': # Windows cmd = ['ping', '-n', '1', '-w', str(timeout * 1000), ip_address.strip()] else: # Unix/Linux/macOS @@ -1326,7 +1326,7 @@ def check_printer_status(ip_address: str, timeout: int = 7) -> Tuple[str, bool]: printers_logger.debug(f"Ping fehlgeschlagen für {ip_address} (Return Code: {result.returncode})") return "offline", False - # Jetzt den tatsächlichen Steckdosenstatus abfragen + # Drucker-Daten aus Datenbank holen für Anmeldedaten db_session = get_db_session() printer = db_session.query(Printer).filter(Printer.plug_ip == ip_address).first() @@ -1335,46 +1335,36 @@ def check_printer_status(ip_address: str, timeout: int = 7) -> Tuple[str, bool]: db_session.close() return "offline", False - # Smart Plug Status prüfen - import requests - from requests.exceptions import RequestException - - # Standardwerte aus der Datenbank verwenden - username = printer.plug_username - password = printer.plug_password - + # TP-Link Tapo P110 Status mit PyP100 prüfen try: - # Für TP-Link Smart Plugs oder kompatible Steckdosen - auth = (username, password) - response = requests.get(f"http://{ip_address}/status", auth=auth, timeout=timeout) + from PyP100 import PyP110 - if response.status_code == 200: - try: - status_data = response.json() - # Überprüfen ob die Steckdose eingeschaltet ist - if 'system' in status_data and 'get_sysinfo' in status_data['system']: - if status_data['system']['get_sysinfo'].get('relay_state') == 1: - printers_logger.debug(f"Steckdose {ip_address} ist eingeschaltet") - db_session.close() - return "online", True - except (ValueError, KeyError) as e: - printers_logger.debug(f"Fehler beim Parsen der Steckdosen-Antwort: {str(e)}") + # Tapo-Steckdose verbinden + p110 = PyP110.P110(ip_address, printer.plug_username, printer.plug_password) + p110.handshake() # Authentifizierung + p110.login() # Login - # Zweiter Versuch mit einfacher GET-Anfrage - response = requests.get(f"http://{ip_address}", auth=auth, timeout=timeout) - if response.status_code == 200: - printers_logger.debug(f"Steckdose {ip_address} antwortet auf HTTP-Anfrage") - # Wenn wir hier ankommen, ist die Steckdose online, aber wir wissen nicht sicher, ob sie eingeschaltet ist - # Da wir nur die Verfügbarkeit prüfen, nehmen wir an, dass sie aktiv ist, wenn sie antwortet - db_session.close() + # Geräteinformationen abrufen + device_info = p110.getDeviceInfo() + device_on = device_info.get('device_on', False) + + db_session.close() + + if device_on: + printers_logger.debug(f"Tapo-Steckdose {ip_address} ist eingeschaltet - Drucker online") return "online", True + else: + printers_logger.debug(f"Tapo-Steckdose {ip_address} ist ausgeschaltet - Drucker offline") + return "offline", False - except RequestException as e: - printers_logger.debug(f"Fehler bei HTTP-Anfrage an Steckdose {ip_address}: {str(e)}") - - # Wenn beide API-Anfragen fehlschlagen, können wir annehmen, dass die Steckdose nicht eingeschaltet ist - db_session.close() - return "offline", False + except ImportError: + printers_logger.error("PyP100-Modul nicht verfügbar - kann Tapo-Steckdose nicht abfragen") + db_session.close() + return "offline", False + except Exception as e: + printers_logger.debug(f"Fehler bei Tapo-Steckdosen-Abfrage {ip_address}: {str(e)}") + db_session.close() + return "offline", False except subprocess.TimeoutExpired: printers_logger.warning(f"Ping-Timeout für Drucker {ip_address} nach {timeout} Sekunden") @@ -3501,14 +3491,45 @@ def create_user_api(): @app.route("/api/admin/printers//toggle", methods=["POST"]) @login_required def toggle_printer_power(printer_id): - """Schaltet einen Drucker ein oder aus (nur für Admins).""" + """ + Schaltet einen Drucker über die zugehörige Steckdose ein/aus. + """ if not current_user.is_admin: - return jsonify({"error": "Nur Administratoren können Drucker steuern"}), 403 + return jsonify({"error": "Administratorrechte erforderlich"}), 403 try: - data = request.json - power_on = data.get("power_on", True) + data = request.get_json() + state = data.get("state", True) # Standard: einschalten + # Steckdose schalten + from utils.job_scheduler import toggle_plug + success = toggle_plug(printer_id, state) + + if success: + action = "eingeschaltet" if state else "ausgeschaltet" + return jsonify({ + "success": True, + "message": f"Drucker erfolgreich {action}", + "printer_id": printer_id, + "state": state + }) + else: + return jsonify({ + "error": "Fehler beim Schalten der Steckdose" + }), 500 + + except Exception as e: + printers_logger.error(f"Fehler beim Schalten von Drucker {printer_id}: {str(e)}") + return jsonify({"error": "Interner Serverfehler"}), 500 + +@app.route("/api/admin/printers//test-tapo", methods=["POST"]) +@login_required +@admin_required +def test_printer_tapo_connection(printer_id): + """ + Testet die Tapo-Steckdosen-Verbindung für einen Drucker. + """ + try: db_session = get_db_session() printer = db_session.query(Printer).get(printer_id) @@ -3516,33 +3537,114 @@ def toggle_printer_power(printer_id): db_session.close() return jsonify({"error": "Drucker nicht gefunden"}), 404 - # Steckdose schalten - from utils.job_scheduler import toggle_plug - success = toggle_plug(printer_id, power_on) - - if success: - # Status in der Datenbank aktualisieren - printer.status = "available" if power_on else "offline" - printer.active = power_on - db_session.commit() - - action = "eingeschaltet" if power_on else "ausgeschaltet" - printers_logger.info(f"Drucker {printer.name} {action} von Admin {current_user.id}") - + if not printer.plug_ip or not printer.plug_username or not printer.plug_password: db_session.close() return jsonify({ - "success": True, - "message": f"Drucker erfolgreich {action}", - "status": printer.status - }) - else: - db_session.close() - return jsonify({"error": "Fehler beim Schalten der Steckdose"}), 500 - + "error": "Unvollständige Tapo-Konfiguration", + "missing": [ + key for key, value in { + "plug_ip": printer.plug_ip, + "plug_username": printer.plug_username, + "plug_password": printer.plug_password + }.items() if not value + ] + }), 400 + + db_session.close() + + # Tapo-Verbindung testen + from utils.job_scheduler import test_tapo_connection + test_result = test_tapo_connection( + printer.plug_ip, + printer.plug_username, + printer.plug_password + ) + + return jsonify({ + "printer_id": printer_id, + "printer_name": printer.name, + "tapo_test": test_result + }) + except Exception as e: - printers_logger.error(f"Fehler beim Schalten des Druckers {printer_id}: {str(e)}") - return jsonify({"error": "Interner Serverfehler"}), 500 - + printers_logger.error(f"Fehler beim Testen der Tapo-Verbindung für Drucker {printer_id}: {str(e)}") + return jsonify({"error": "Interner Serverfehler beim Verbindungstest"}), 500 + +@app.route("/api/admin/printers/test-all-tapo", methods=["POST"]) +@login_required +@admin_required +def test_all_printers_tapo_connection(): + """ + Testet die Tapo-Steckdosen-Verbindung für alle Drucker. + Nützlich für Diagnose und Setup-Validierung. + """ + try: + db_session = get_db_session() + printers = db_session.query(Printer).filter(Printer.active == True).all() + db_session.close() + + if not printers: + return jsonify({ + "message": "Keine aktiven Drucker gefunden", + "results": [] + }) + + # Alle Drucker testen + from utils.job_scheduler import test_tapo_connection + results = [] + + for printer in printers: + result = { + "printer_id": printer.id, + "printer_name": printer.name, + "plug_ip": printer.plug_ip, + "has_config": bool(printer.plug_ip and printer.plug_username and printer.plug_password) + } + + if result["has_config"]: + # Tapo-Verbindung testen + test_result = test_tapo_connection( + printer.plug_ip, + printer.plug_username, + printer.plug_password + ) + result["tapo_test"] = test_result + else: + result["tapo_test"] = { + "success": False, + "error": "Unvollständige Tapo-Konfiguration", + "device_info": None, + "status": "unconfigured" + } + result["missing_config"] = [ + key for key, value in { + "plug_ip": printer.plug_ip, + "plug_username": printer.plug_username, + "plug_password": printer.plug_password + }.items() if not value + ] + + results.append(result) + + # Zusammenfassung erstellen + total_printers = len(results) + successful_connections = sum(1 for r in results if r["tapo_test"]["success"]) + configured_printers = sum(1 for r in results if r["has_config"]) + + return jsonify({ + "summary": { + "total_printers": total_printers, + "configured_printers": configured_printers, + "successful_connections": successful_connections, + "success_rate": round(successful_connections / total_printers * 100, 1) if total_printers > 0 else 0 + }, + "results": results + }) + + except Exception as e: + printers_logger.error(f"Fehler beim Testen aller Tapo-Verbindungen: {str(e)}") + return jsonify({"error": "Interner Serverfehler beim Massentest"}), 500 + # ===== ADMIN FORM ENDPOINTS ===== @app.route("/admin/users/create", methods=["POST"]) diff --git a/backend/app/database/myp.db-shm b/backend/app/database/myp.db-shm index 957ff843..55e76fb2 100644 Binary files a/backend/app/database/myp.db-shm and b/backend/app/database/myp.db-shm differ diff --git a/backend/app/database/myp.db-wal b/backend/app/database/myp.db-wal index ffc3a9f8..3fa1955a 100644 Binary files a/backend/app/database/myp.db-wal and b/backend/app/database/myp.db-wal differ diff --git a/backend/app/templates/guest_request.html b/backend/app/templates/guest_request.html index 0ed073fe..23d64264 100644 --- a/backend/app/templates/guest_request.html +++ b/backend/app/templates/guest_request.html @@ -594,7 +594,7 @@ - 3D-Datei hochladen + 3D-Datei hochladen (optional)
@@ -743,8 +743,8 @@ email: false, printer_id: false, duration_min: false, - reason: false, - file: false + reason: false + // file wurde entfernt da es optional ist }; let isSubmitting = false; @@ -827,7 +827,7 @@ document.addEventListener('DOMContentLoaded', function() { } selectedFile = file; - validationState.file = true; + // validationState.file = true; // Entfernt, da Upload optional ist showFilePreview(file); updateFormValidation(); @@ -913,7 +913,7 @@ document.addEventListener('DOMContentLoaded', function() { function removeFile() { selectedFile = null; - validationState.file = false; + // validationState.file = false; // Entfernt, da Upload optional ist const previewContainer = document.getElementById('file-preview'); const uploadContent = document.getElementById('upload-content'); @@ -1175,10 +1175,11 @@ document.addEventListener('DOMContentLoaded', function() { return; } - if (!selectedFile) { - showAdvancedMessage('Bitte wählen Sie eine Datei aus.', 'error'); - return; - } + // Datei ist jetzt optional - keine Validierung mehr erforderlich + // if (!selectedFile) { + // showAdvancedMessage('Bitte wählen Sie eine Datei aus.', 'error'); + // return; + // } isSubmitting = true; showLoadingState(); @@ -1190,7 +1191,11 @@ document.addEventListener('DOMContentLoaded', function() { formData.append('printer_id', document.getElementById('printer_id').value); formData.append('duration_min', document.getElementById('duration_min').value); formData.append('reason', document.getElementById('reason').value.trim()); - formData.append('file', selectedFile); + + // Datei nur anhängen, wenn eine ausgewählt wurde + if (selectedFile) { + formData.append('file', selectedFile); + } // Add CSRF token const csrfToken = document.querySelector('meta[name="csrf-token"]')?.getAttribute('content'); diff --git a/backend/app/utils/debug_drucker_erkennung.py b/backend/app/utils/debug_drucker_erkennung.py index cf0b31ff..8e7e1494 100644 --- a/backend/app/utils/debug_drucker_erkennung.py +++ b/backend/app/utils/debug_drucker_erkennung.py @@ -177,6 +177,108 @@ def test_network_connectivity(): except Exception as e: log_message(f"❌ Fehler beim Testen der Netzwerkverbindung: {str(e)}", "ERROR") +def test_tapo_connections(): + """Teste TP-Link Tapo P110-Steckdosen-Verbindungen""" + log_message("Teste TP-Link Tapo P110-Steckdosen-Verbindungen...") + + try: + # PyP100 importieren + from PyP100 import PyP110 + log_message("✅ PyP100-Modul erfolgreich importiert") + except ImportError: + log_message("❌ PyP100-Modul nicht verfügbar", "ERROR") + log_message(" Installiere mit: pip install PyP100", "INFO") + return + + # Lade Drucker aus Datenbank + try: + db_files = ['database.db', 'app.db', 'myp.db'] + printers = [] + + for db_file in db_files: + if os.path.exists(db_file): + conn = sqlite3.connect(db_file) + cursor = conn.cursor() + cursor.execute("SELECT id, name, plug_ip, plug_username, plug_password FROM printer WHERE plug_ip IS NOT NULL;") + printers = cursor.fetchall() + conn.close() + break + + if not printers: + log_message("❌ Keine Drucker mit Tapo-Konfiguration gefunden") + return + + successful_connections = 0 + total_printers = len(printers) + + for printer_id, name, plug_ip, plug_username, plug_password in printers: + log_message(f"Teste Tapo-Verbindung zu {name} ({plug_ip})...") + + # Konfiguration validieren + if not all([plug_ip, plug_username, plug_password]): + log_message(f" ❌ Unvollständige Konfiguration") + missing = [] + if not plug_ip: missing.append("IP-Adresse") + if not plug_username: missing.append("Benutzername") + if not plug_password: missing.append("Passwort") + log_message(f" Fehlend: {', '.join(missing)}") + continue + + try: + # Tapo-Verbindung herstellen + p110 = PyP110.P110(plug_ip, plug_username, plug_password) + p110.handshake() # Authentifizierung + p110.login() # Login + + # Geräteinformationen abrufen + device_info = p110.getDeviceInfo() + + log_message(f" ✅ Tapo-Verbindung erfolgreich") + log_message(f" 📛 Gerätename: {device_info.get('nickname', 'Unbekannt')}") + log_message(f" ⚡ Status: {'Ein' if device_info.get('device_on', False) else 'Aus'}") + + if 'on_time' in device_info: + on_time = device_info.get('on_time', 0) + hours, minutes = divmod(on_time // 60, 60) + log_message(f" ⏱️ Betriebszeit: {hours}h {minutes}m") + + if 'power_usage' in device_info: + power_usage = device_info.get('power_usage', {}) + current_power = power_usage.get('power_mw', 0) / 1000 # mW zu W + log_message(f" 🔋 Aktueller Verbrauch: {current_power:.1f}W") + + successful_connections += 1 + + except Exception as e: + log_message(f" ❌ Tapo-Verbindung fehlgeschlagen: {str(e)}") + + # Detaillierte Fehleranalyse + if "login" in str(e).lower(): + log_message(f" 🔐 Mögliche Ursache: Falsche Anmeldedaten") + elif "timeout" in str(e).lower(): + log_message(f" ⏱️ Mögliche Ursache: Netzwerk-Timeout") + elif "connect" in str(e).lower(): + log_message(f" 🌐 Mögliche Ursache: Steckdose nicht erreichbar") + elif "handshake" in str(e).lower(): + log_message(f" 🤝 Mögliche Ursache: Protokoll-Handshake fehlgeschlagen") + + # Zusammenfassung + success_rate = (successful_connections / total_printers * 100) if total_printers > 0 else 0 + log_message(f"📊 Tapo-Verbindungs-Zusammenfassung:") + log_message(f" Getestete Drucker: {total_printers}") + log_message(f" Erfolgreiche Verbindungen: {successful_connections}") + log_message(f" Erfolgsrate: {success_rate:.1f}%") + + if successful_connections == total_printers: + log_message("🎉 Alle Tapo-Verbindungen erfolgreich!") + elif successful_connections > 0: + log_message("⚠️ Einige Tapo-Verbindungen fehlgeschlagen") + else: + log_message("❌ Keine Tapo-Verbindungen erfolgreich", "ERROR") + + except Exception as e: + log_message(f"❌ Fehler beim Testen der Tapo-Verbindungen: {str(e)}", "ERROR") + def test_flask_app_status(): """Teste den Status der Flask-Anwendung""" log_message("Teste Flask-Anwendung...") @@ -292,6 +394,10 @@ def run_comprehensive_test(): test_network_connectivity() print() + # Tapo-Verbindungen testen + test_tapo_connections() + print() + log_message("=== Diagnose abgeschlossen ===") print() diff --git a/backend/app/utils/job_scheduler.py b/backend/app/utils/job_scheduler.py index 19b9992b..fba7635c 100644 --- a/backend/app/utils/job_scheduler.py +++ b/backend/app/utils/job_scheduler.py @@ -313,7 +313,14 @@ def toggle_plug(printer_id: int, state: bool) -> bool: db_session.close() return False + # Konfiguration validieren + if not printer.plug_ip or not printer.plug_username or not printer.plug_password: + logger.error(f"Unvollständige Tapo-Konfiguration für Drucker {printer.name} (ID: {printer_id})") + db_session.close() + return False + # TP-Link Tapo P110 Verbindung herstellen + logger.debug(f"Verbinde zu Tapo-Steckdose {printer.plug_ip} für Drucker {printer.name}") p110 = PyP110.P110(printer.plug_ip, printer.plug_username, printer.plug_password) p110.handshake() # Authentifizierung p110.login() # Login @@ -321,19 +328,60 @@ def toggle_plug(printer_id: int, state: bool) -> bool: # Steckdose ein-/ausschalten if state: p110.turnOn() - logger.info(f"Steckdose für Drucker {printer.name} (ID: {printer_id}) eingeschaltet") + logger.info(f"✅ Steckdose für Drucker {printer.name} (ID: {printer_id}) eingeschaltet") else: p110.turnOff() - logger.info(f"Steckdose für Drucker {printer.name} (ID: {printer_id}) ausgeschaltet") + logger.info(f"✅ Steckdose für Drucker {printer.name} (ID: {printer_id}) ausgeschaltet") db_session.close() return True except Exception as e: - logger.error(f"Fehler beim Schalten der Steckdose für Drucker {printer_id}: {str(e)}") + logger.error(f"❌ Fehler beim Schalten der Steckdose für Drucker {printer_id}: {str(e)}") db_session.close() return False +def test_tapo_connection(ip_address: str, username: str, password: str) -> dict: + """ + Testet die Verbindung zu einer Tapo-Steckdose und gibt detaillierte Informationen zurück. + + Args: + ip_address: IP-Adresse der Steckdose + username: Benutzername + password: Passwort + + Returns: + dict: Testergebnis mit Status und Informationen + """ + logger = get_logger("printers") + result = { + "success": False, + "error": None, + "device_info": None, + "status": "unknown" + } + + try: + logger.debug(f"Teste Tapo-Verbindung zu {ip_address}") + p110 = PyP110.P110(ip_address, username, password) + p110.handshake() # Authentifizierung + p110.login() # Login + + # Geräteinformationen abrufen + device_info = p110.getDeviceInfo() + result["device_info"] = device_info + result["status"] = "on" if device_info.get('device_on', False) else "off" + result["success"] = True + + logger.debug(f"✅ Tapo-Verbindung zu {ip_address} erfolgreich") + + except Exception as e: + result["error"] = str(e) + logger.warning(f"❌ Tapo-Verbindung zu {ip_address} fehlgeschlagen: {str(e)}") + + return result + + def check_jobs(): """ Überprüft alle geplanten und laufenden Jobs und schaltet Steckdosen entsprechend. diff --git a/backend/app/utils/printer_monitor.py b/backend/app/utils/printer_monitor.py index 2d25db97..458a7c1c 100644 --- a/backend/app/utils/printer_monitor.py +++ b/backend/app/utils/printer_monitor.py @@ -13,11 +13,19 @@ from typing import Dict, Tuple, List, Optional from flask import session from sqlalchemy import func from sqlalchemy.orm import Session +import os from models import get_db_session, Printer from utils.logging_config import get_logger from config.settings import PRINTERS +# TP-Link Tapo P110 Unterstützung hinzufügen +try: + from PyP100 import PyP110 + TAPO_AVAILABLE = True +except ImportError: + TAPO_AVAILABLE = False + # Logger initialisieren monitor_logger = get_logger("printer_monitor") @@ -110,57 +118,35 @@ class PrinterMonitor: def _turn_outlet_off(self, ip_address: str, username: str, password: str, timeout: int = 5) -> bool: """ - Schaltet eine Smart-Steckdose aus. + Schaltet eine TP-Link Tapo P110-Steckdose aus. Args: ip_address: IP-Adresse der Steckdose username: Benutzername für die Steckdose password: Passwort für die Steckdose - timeout: Timeout in Sekunden + timeout: Timeout in Sekunden (wird ignoriert, da PyP100 eigenes Timeout hat) Returns: bool: True wenn erfolgreich ausgeschaltet """ + if not TAPO_AVAILABLE: + monitor_logger.error("⚠️ PyP100-Modul nicht verfügbar - kann Tapo-Steckdose nicht schalten") + return False + try: - # Für TP-Link Tapo und ähnliche Smart Plugs - auth = (username, password) + # TP-Link Tapo P110 Verbindung herstellen + p110 = PyP110.P110(ip_address, username, password) + p110.handshake() # Authentifizierung + p110.login() # Login - # Verschiedene API-Endpunkte versuchen - endpoints = [ - f"http://{ip_address}/relay/off", - f"http://{ip_address}/api/relay/off", - f"http://{ip_address}/set?relay=off", - f"http://{ip_address}/control?action=off" - ] + # Steckdose ausschalten + p110.turnOff() + monitor_logger.debug(f"✅ Tapo-Steckdose {ip_address} erfolgreich ausgeschaltet") + return True - for endpoint in endpoints: - try: - response = requests.post(endpoint, auth=auth, timeout=timeout) - if response.status_code in [200, 201, 204]: - monitor_logger.debug(f"✅ Steckdose {ip_address} ausgeschaltet via {endpoint}") - return True - except requests.RequestException: - continue - - # Wenn spezifische Endpunkte fehlschlagen, versuche generische JSON-API - try: - payload = {"system": {"set_relay_state": {"state": 0}}} - response = requests.post( - f"http://{ip_address}/api", - json=payload, - auth=auth, - timeout=timeout - ) - if response.status_code in [200, 201]: - monitor_logger.debug(f"✅ Steckdose {ip_address} ausgeschaltet via JSON-API") - return True - except requests.RequestException: - pass - except Exception as e: - monitor_logger.debug(f"⚠️ Fehler beim Ausschalten der Steckdose {ip_address}: {str(e)}") - - return False + monitor_logger.debug(f"⚠️ Fehler beim Ausschalten der Tapo-Steckdose {ip_address}: {str(e)}") + return False def get_live_printer_status(self, use_session_cache: bool = True) -> Dict[int, Dict]: """ @@ -364,7 +350,6 @@ class PrinterMonitor: ipaddress.ip_address(ip_address.strip()) # Platform-spezifische Ping-Befehle - import os if os.name == 'nt': # Windows cmd = ['ping', '-n', '1', '-w', str(timeout * 1000), ip_address.strip()] else: # Unix/Linux/macOS @@ -386,79 +371,40 @@ class PrinterMonitor: def _check_outlet_status(self, ip_address: str, username: str, password: str, timeout: int = 5) -> Tuple[bool, str]: """ - Überprüft den Status einer Smart-Steckdose. + Überprüft den Status einer TP-Link Tapo P110-Steckdose. Args: ip_address: IP-Adresse der Steckdose username: Benutzername für die Steckdose password: Passwort für die Steckdose - timeout: Timeout in Sekunden + timeout: Timeout in Sekunden (wird ignoriert, da PyP100 eigenes Timeout hat) Returns: Tuple[bool, str]: (Erreichbar, Status) - Status: "on", "off", "unknown" """ + if not TAPO_AVAILABLE: + monitor_logger.debug("⚠️ PyP100-Modul nicht verfügbar - kann Tapo-Steckdosen-Status nicht abfragen") + return False, "unknown" + try: - auth = (username, password) + # TP-Link Tapo P110 Verbindung herstellen + p110 = PyP110.P110(ip_address, username, password) + p110.handshake() # Authentifizierung + p110.login() # Login - # Verschiedene API-Endpunkte für Status-Abfrage versuchen - status_endpoints = [ - f"http://{ip_address}/status", - f"http://{ip_address}/api/status", - f"http://{ip_address}/relay/status", - f"http://{ip_address}/state" - ] + # Geräteinformationen abrufen + device_info = p110.getDeviceInfo() - for endpoint in status_endpoints: - try: - response = requests.get(endpoint, auth=auth, timeout=timeout) - if response.status_code == 200: - try: - data = response.json() - - # Verschiedene JSON-Strukturen handhaben - if isinstance(data, dict): - # TP-Link Format - if 'system' in data and 'get_sysinfo' in data['system']: - relay_state = data['system']['get_sysinfo'].get('relay_state') - return True, "on" if relay_state == 1 else "off" - - # Direktes Format - if 'relay_state' in data: - return True, "on" if data['relay_state'] in [1, True, "on"] else "off" - - if 'state' in data: - return True, "on" if data['state'] in [1, True, "on"] else "off" - - if 'power' in data: - return True, "on" if data['power'] in [1, True, "on"] else "off" - - # Wenn JSON-Parsing funktioniert, aber Format unbekannt - return True, "unknown" - - except ValueError: - # Nicht-JSON Antwort - versuche Text-Parsing - text = response.text.lower() - if "on" in text or "1" in text or "true" in text: - return True, "on" - elif "off" in text or "0" in text or "false" in text: - return True, "off" - return True, "unknown" - - except requests.RequestException: - continue + # Status auswerten + device_on = device_info.get('device_on', False) + status = "on" if device_on else "off" + + monitor_logger.debug(f"✅ Tapo-Steckdose {ip_address}: Status = {status}") + return True, status - # Wenn spezifische Endpunkte fehlschlagen, einfache Konnektivitätsprüfung - try: - response = requests.get(f"http://{ip_address}", auth=auth, timeout=timeout) - if response.status_code in [200, 401, 403]: # Erreichbar, aber möglicherweise Authentifizierungsproblem - return True, "unknown" - except requests.RequestException: - pass - except Exception as e: - monitor_logger.debug(f"⚠️ Fehler bei Steckdosen-Status-Check {ip_address}: {str(e)}") - - return False, "unknown" + monitor_logger.debug(f"⚠️ Fehler bei Tapo-Steckdosen-Status-Check {ip_address}: {str(e)}") + return False, "unknown" def clear_all_caches(self): """Löscht alle Caches (Session und DB)."""