From 0a1b24c4ef994edde047d494cfb40f057e8a5a78 Mon Sep 17 00:00:00 2001 From: Till Tomczak Date: Thu, 5 Jun 2025 11:13:43 +0200 Subject: [PATCH] =?UTF-8?q?=F0=9F=94=A7=20Aktualisierung=20der=20Tapo-Stec?= =?UTF-8?q?kdosen-Integration:=20Umstellung=20auf=20zentralen=20tapo=5Fcon?= =?UTF-8?q?troller=20f=C3=BCr=20Statuspr=C3=BCfungen=20und=20Verbindungs-T?= =?UTF-8?q?ests.=20Verbesserung=20der=20Fehlerbehandlung=20und=20Protokoll?= =?UTF-8?q?ierung.=20Anpassungen=20in=20der=20Dokumentation=20und=20Umbene?= =?UTF-8?q?nnung=20von=20Funktionen=20zur=20besseren=20Lesbarkeit.=20?= =?UTF-8?q?=F0=9F=9B=A0=EF=B8=8F?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/app.py | 39 +- backend/instance/printer_manager.db | Bin 90112 -> 90112 bytes backend/logs/analytics/analytics.log | 1 + backend/logs/app/app.log | 28 ++ backend/logs/backup/backup.log | 1 + backend/logs/calendar/calendar.log | 1 + backend/logs/dashboard/dashboard.log | 4 + backend/logs/database/database.log | 1 + .../email_notification/email_notification.log | 1 + .../logs/error_recovery/error_recovery.log | 23 ++ backend/logs/jobs/jobs.log | 1 + backend/logs/maintenance/maintenance.log | 2 + .../logs/multi_location/multi_location.log | 2 + backend/logs/permissions/permissions.log | 1 + .../logs/printer_monitor/printer_monitor.log | 6 + backend/logs/printers/printers.log | 4 + backend/logs/scheduler/scheduler.log | 3 + backend/logs/security/security.log | 1 + .../shutdown_manager/shutdown_manager.log | 1 + backend/logs/startup/startup.log | 9 + .../logs/system_control/system_control.log | 13 + .../logs/tapo_controller/tapo_controller.log | 12 + backend/logs/windows_fixes/windows_fixes.log | 4 + .../__pycache__/job_scheduler.cpython-313.pyc | Bin 31892 -> 28542 bytes .../printer_monitor.cpython-313.pyc | Bin 34326 -> 18016 bytes .../tapo_controller.cpython-313.pyc | Bin 0 -> 28919 bytes backend/utils/tapo_controller.py | 358 +++++++++--------- 27 files changed, 320 insertions(+), 196 deletions(-) create mode 100644 backend/logs/tapo_controller/tapo_controller.log create mode 100644 backend/utils/__pycache__/tapo_controller.cpython-313.pyc diff --git a/backend/app.py b/backend/app.py index 3ea75b49..1c1289f8 100644 --- a/backend/app.py +++ b/backend/app.py @@ -2217,24 +2217,29 @@ def check_printer_status(ip_address: str, timeout: int = 7) -> Tuple[str, bool]: if result == 0: reachable = True try: - # TP-Link Tapo Steckdose mit PyP100 überprüfen - from PyP100 import PyP100 - p100 = PyP100.P100(ip_address, TAPO_USERNAME, TAPO_PASSWORD) - p100.handshake() # Authentifizierung - p100.login() # Login - - # Geräteinformationen abrufen - device_info = p100.getDeviceInfo() + # TP-Link Tapo Steckdose mit zentralem tapo_controller überprüfen + from utils.tapo_controller import tapo_controller + reachable, outlet_status = tapo_controller.check_outlet_status(ip_address) # 🎯 KORREKTE LOGIK: Status auswerten - if device_info.get('device_on', False): - # Steckdose an = Drucker PRINTING (druckt gerade) - status = "printing" - printers_logger.info(f"🖨️ Drucker {ip_address}: PRINTING (Steckdose an - druckt gerade)") + if reachable: + if outlet_status == "on": + # Steckdose an = Drucker PRINTING (druckt gerade) + status = "printing" + printers_logger.info(f"🖨️ Drucker {ip_address}: PRINTING (Steckdose an - druckt gerade)") + elif outlet_status == "off": + # Steckdose aus = Drucker ONLINE (bereit zum Drucken) + status = "online" + printers_logger.info(f"[OK] Drucker {ip_address}: ONLINE (Steckdose aus - bereit zum Drucken)") + else: + # Unbekannter Status + status = "error" + printers_logger.warning(f"[WARNING] Drucker {ip_address}: Unbekannter Steckdosen-Status") else: - # Steckdose aus = Drucker ONLINE (bereit zum Drucken) - status = "online" - printers_logger.info(f"[OK] Drucker {ip_address}: ONLINE (Steckdose aus - bereit zum Drucken)") + # Steckdose nicht erreichbar + reachable = False + status = "error" + printers_logger.error(f"[ERROR] Drucker {ip_address}: Steckdose nicht erreichbar") except Exception as e: printers_logger.error(f"[ERROR] Fehler bei Tapo-Status-Check für {ip_address}: {str(e)}") @@ -3288,7 +3293,7 @@ def test_printer_tapo_connection(printer_id): db_session.close() # Tapo-Verbindung testen - from utils.job_scheduler import test_tapo_connection + from utils.tapo_controller import test_tapo_connection test_result = test_tapo_connection( printer.plug_ip, printer.plug_username, @@ -3325,7 +3330,7 @@ def test_all_printers_tapo_connection(): }) # Alle Drucker testen - from utils.job_scheduler import test_tapo_connection + from utils.tapo_controller import test_tapo_connection results = [] for printer in printers: diff --git a/backend/instance/printer_manager.db b/backend/instance/printer_manager.db index 0f975077a2db852c5b73385d4d9c3b0c2f5fc9f5..5e18a39d91ab464932aaee3b5243b095eb47b1f5 100644 GIT binary patch delta 91 zcmZoTz}j$tbwi-MX@sl4e@oZN;v=kpe-IJ delta 91 zcmZoTz}j$tbwi-MsehVJW@eG0lfR#jX|i*!XPHGvpk-=kWO7+qxx2T%e@c-{kh#Bq vv7?hqfQ!GUUVcf2zS-n9`5-<6D?=kIQv*E%V?$GO^X6yr+n>oZN;v=kdqy1; diff --git a/backend/logs/analytics/analytics.log b/backend/logs/analytics/analytics.log index ff4d0960..264fa372 100644 --- a/backend/logs/analytics/analytics.log +++ b/backend/logs/analytics/analytics.log @@ -3,3 +3,4 @@ 2025-06-05 01:01:11 - [analytics] analytics - [INFO] INFO - 📈 Analytics Engine initialisiert 2025-06-05 09:31:05 - [analytics] analytics - [INFO] INFO - 📈 Analytics Engine initialisiert 2025-06-05 10:12:41 - [analytics] analytics - [INFO] INFO - 📈 Analytics Engine initialisiert +2025-06-05 11:12:32 - [analytics] analytics - [INFO] INFO - 📈 Analytics Engine initialisiert diff --git a/backend/logs/app/app.log b/backend/logs/app/app.log index 2b8f35d5..262e27cc 100644 --- a/backend/logs/app/app.log +++ b/backend/logs/app/app.log @@ -171,3 +171,31 @@ WHERE jobs.status = ?) AS anon_1] 2025-06-05 10:20:04 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 0, 'total_jobs': 0, 'pending_jobs': 0, 'success_rate': 0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 0} 2025-06-05 10:20:04 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 2025-06-05 10:20:04 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 0, 'total_jobs': 0, 'pending_jobs': 0, 'success_rate': 0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 0} +2025-06-05 11:12:31 - [app] app - [INFO] INFO - Optimierte SQLite-Engine erstellt: C:\Users\TTOMCZA.EMEA\Dev\Projektarbeit-MYP\backend\instance\printer_manager.db +2025-06-05 11:12:34 - [app] app - [INFO] INFO - SQLite für Raspberry Pi optimiert (reduzierte Cache-Größe, SD-Karten I/O) +2025-06-05 11:12:34 - [app] app - [INFO] INFO - [OK] Timeout Force-Quit Manager geladen +2025-06-05 11:12:34 - [app] app - [INFO] INFO - [LIST] Standard-Konfiguration verwendet +2025-06-05 11:12:35 - [app] app - [INFO] INFO - [OK] Zentraler Shutdown-Manager initialisiert +2025-06-05 11:12:35 - [app] app - [INFO] INFO - [OK] Error-Recovery-Monitoring gestartet +2025-06-05 11:12:35 - [app] app - [INFO] INFO - [OK] System-Control-Manager initialisiert +2025-06-05 11:12:35 - [app] app - [WARNING] WARNING - [WARN] Kiosk-Service nicht gefunden - Kiosk-Funktionen eventuell eingeschränkt +2025-06-05 11:12:35 - [app] app - [INFO] INFO - [RESTART] Starte Datenbank-Setup und Migrationen... +2025-06-05 11:12:37 - [app] app - [INFO] INFO - Datenbank mit Optimierungen initialisiert +2025-06-05 11:12:38 - [app] app - [INFO] INFO - [OK] JobOrder-Tabelle bereits vorhanden +2025-06-05 11:12:38 - [app] app - [INFO] INFO - Admin-Benutzer admin (admin@mercedes-benz.com) existiert bereits. Passwort wurde zurückgesetzt. +2025-06-05 11:12:38 - [app] app - [INFO] INFO - [OK] Datenbank-Setup und Migrationen erfolgreich abgeschlossen +2025-06-05 11:12:38 - [app] app - [INFO] INFO - [LIST] Standard-Konfiguration aktiv (keine Optimierungen) +2025-06-05 11:12:38 - [app] app - [INFO] INFO - 🖨️ Starte automatische Steckdosen-Initialisierung... +2025-06-05 11:12:38 - [app] app - [INFO] INFO - [INFO] Keine Drucker zur Initialisierung gefunden +2025-06-05 11:12:38 - [app] app - [INFO] INFO - [RESTART] Debug-Modus: Queue Manager deaktiviert für Entwicklung +2025-06-05 11:12:38 - [app] app - [INFO] INFO - Job-Scheduler gestartet +2025-06-05 11:12:38 - [app] app - [INFO] INFO - 🔧 Starte Debug-Server auf 0.0.0.0:5000 (HTTP) +2025-06-05 11:12:38 - [app] app - [INFO] INFO - Windows-Debug-Modus: Auto-Reload deaktiviert +2025-06-05 11:13:02 - [app] app - [INFO] INFO - Admin-Check für Funktion admin_page: User authenticated: True, User ID: 1, Is Admin: True +2025-06-05 11:13:02 - [app] app - [INFO] INFO - Admin-Check für Funktion api_admin_system_health: User authenticated: True, User ID: 1, Is Admin: True +2025-06-05 11:13:04 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-05 11:13:04 - [app] app - [INFO] INFO - Dashboard-Refresh angefordert von User 1 +2025-06-05 11:13:04 - [app] app - [ERROR] ERROR - Fehler beim Abrufen der Dashboard-Statistiken: tuple index out of range +2025-06-05 11:13:04 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 0, 'total_jobs': 0, 'pending_jobs': 0, 'success_rate': 0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 0, 'online_printers': 0, 'offline_printers': 0} +2025-06-05 11:13:04 - [app] app - [INFO] INFO - Dashboard-Refresh erfolgreich: {'active_jobs': 0, 'available_printers': 0, 'total_jobs': 0, 'pending_jobs': 0, 'success_rate': 0, 'completed_jobs': 0, 'failed_jobs': 0, 'cancelled_jobs': 0, 'total_users': 1, 'online_printers': 0, 'offline_printers': 0} +2025-06-05 11:13:06 - [app] app - [INFO] INFO - Benutzer admin@mercedes-benz.com hat sich abgemeldet diff --git a/backend/logs/backup/backup.log b/backend/logs/backup/backup.log index 3a9624fe..3d206aea 100644 --- a/backend/logs/backup/backup.log +++ b/backend/logs/backup/backup.log @@ -3,3 +3,4 @@ 2025-06-05 01:01:11 - [backup] backup - [INFO] INFO - BackupManager initialisiert (minimal implementation) 2025-06-05 09:31:05 - [backup] backup - [INFO] INFO - BackupManager initialisiert (minimal implementation) 2025-06-05 10:12:41 - [backup] backup - [INFO] INFO - BackupManager initialisiert (minimal implementation) +2025-06-05 11:12:32 - [backup] backup - [INFO] INFO - BackupManager initialisiert (minimal implementation) diff --git a/backend/logs/calendar/calendar.log b/backend/logs/calendar/calendar.log index 4a0c6f18..41687b81 100644 --- a/backend/logs/calendar/calendar.log +++ b/backend/logs/calendar/calendar.log @@ -1 +1,2 @@ 2025-06-04 23:36:31 - [calendar] calendar - [INFO] INFO - 📅 Kalender-Events abgerufen: 0 Einträge für Zeitraum 2025-06-01 00:00:00 bis 2025-06-08 00:00:00 +2025-06-05 11:12:52 - [calendar] calendar - [INFO] INFO - 📅 Kalender-Events abgerufen: 0 Einträge für Zeitraum 2025-06-01 00:00:00 bis 2025-06-08 00:00:00 diff --git a/backend/logs/dashboard/dashboard.log b/backend/logs/dashboard/dashboard.log index 9960239f..7f54364d 100644 --- a/backend/logs/dashboard/dashboard.log +++ b/backend/logs/dashboard/dashboard.log @@ -116,3 +116,7 @@ WHERE guest_requests.status = ?) AS anon_1] 2025-06-05 10:12:45 - [dashboard] dashboard - [INFO] INFO - Dashboard-Background-Worker gestartet 2025-06-05 10:12:45 - [dashboard] dashboard - [INFO] INFO - Dashboard WebSocket-Server wird mit threading initialisiert (eventlet-Fallback) 2025-06-05 10:12:45 - [dashboard] dashboard - [INFO] INFO - Dashboard WebSocket-Server initialisiert (async_mode: threading) +2025-06-05 11:12:34 - [dashboard] dashboard - [INFO] INFO - Dashboard-Background-Worker gestartet +2025-06-05 11:12:34 - [dashboard] dashboard - [INFO] INFO - Dashboard-Background-Worker gestartet +2025-06-05 11:12:34 - [dashboard] dashboard - [INFO] INFO - Dashboard WebSocket-Server wird mit threading initialisiert (eventlet-Fallback) +2025-06-05 11:12:34 - [dashboard] dashboard - [INFO] INFO - Dashboard WebSocket-Server initialisiert (async_mode: threading) diff --git a/backend/logs/database/database.log b/backend/logs/database/database.log index dc5e0f6d..4d3bf79e 100644 --- a/backend/logs/database/database.log +++ b/backend/logs/database/database.log @@ -3,3 +3,4 @@ 2025-06-05 01:01:11 - [database] database - [INFO] INFO - Datenbank-Wartungs-Scheduler gestartet 2025-06-05 09:31:05 - [database] database - [INFO] INFO - Datenbank-Wartungs-Scheduler gestartet 2025-06-05 10:12:41 - [database] database - [INFO] INFO - Datenbank-Wartungs-Scheduler gestartet +2025-06-05 11:12:32 - [database] database - [INFO] INFO - Datenbank-Wartungs-Scheduler gestartet diff --git a/backend/logs/email_notification/email_notification.log b/backend/logs/email_notification/email_notification.log index a6b08512..054419c6 100644 --- a/backend/logs/email_notification/email_notification.log +++ b/backend/logs/email_notification/email_notification.log @@ -2,3 +2,4 @@ 2025-06-04 23:47:09 - [email_notification] email_notification - [INFO] INFO - 📧 Offline-E-Mail-Benachrichtigung initialisiert (kein echter E-Mail-Versand) 2025-06-05 09:31:08 - [email_notification] email_notification - [INFO] INFO - 📧 Offline-E-Mail-Benachrichtigung initialisiert (kein echter E-Mail-Versand) 2025-06-05 10:12:45 - [email_notification] email_notification - [INFO] INFO - 📧 Offline-E-Mail-Benachrichtigung initialisiert (kein echter E-Mail-Versand) +2025-06-05 11:12:34 - [email_notification] email_notification - [INFO] INFO - 📧 Offline-E-Mail-Benachrichtigung initialisiert (kein echter E-Mail-Versand) diff --git a/backend/logs/error_recovery/error_recovery.log b/backend/logs/error_recovery/error_recovery.log index 9b073f73..da42029a 100644 --- a/backend/logs/error_recovery/error_recovery.log +++ b/backend/logs/error_recovery/error_recovery.log @@ -144,3 +144,26 @@ 2025-06-05 10:14:01 - [error_recovery] error_recovery - [INFO] INFO - 🔧 Führe Recovery-Aktion aus: restart_system 2025-06-05 10:14:01 - [error_recovery] error_recovery - [INFO] INFO - ✅ Recovery erfolgreich: restart_system 2025-06-05 10:14:06 - [error_recovery] error_recovery - [INFO] INFO - 🛑 Error-Monitoring gestoppt +2025-06-05 11:12:35 - [error_recovery] error_recovery - [INFO] INFO - 🛡️ Error-Recovery-Manager initialisiert +2025-06-05 11:12:35 - [error_recovery] error_recovery - [INFO] INFO - 🔍 Error-Monitoring gestartet +2025-06-05 11:12:35 - [error_recovery] error_recovery - [WARNING] WARNING - 🚨 Fehler erkannt: flask_error - 2025-06-04 23:38:35 - [app] app - [ERROR] ERROR - Interner Serverfehler: 500 Internal Server Error: The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application. +2025-06-05 11:12:35 - [error_recovery] error_recovery - [INFO] INFO - 🔧 Führe Recovery-Aktion aus: restart_service +2025-06-05 11:12:39 - [error_recovery] error_recovery - [INFO] INFO - ✅ Recovery erfolgreich: restart_service +2025-06-05 11:12:39 - [error_recovery] error_recovery - [WARNING] WARNING - 🚨 Fehler erkannt: flask_error - 2025-06-04 23:38:35 - [app] app - [ERROR] ERROR - Interner Serverfehler: 500 Internal Server Error: The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application. +2025-06-05 11:12:39 - [error_recovery] error_recovery - [INFO] INFO - 🔧 Führe Recovery-Aktion aus: restart_service +2025-06-05 11:12:41 - [error_recovery] error_recovery - [INFO] INFO - ✅ Recovery erfolgreich: restart_service +2025-06-05 11:12:41 - [error_recovery] error_recovery - [WARNING] WARNING - 🚨 Fehler erkannt: flask_error - 2025-06-04 23:39:03 - [app] app - [ERROR] ERROR - Interner Serverfehler: 500 Internal Server Error: The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application. +2025-06-05 11:12:41 - [error_recovery] error_recovery - [INFO] INFO - 🔧 Führe Recovery-Aktion aus: restart_service +2025-06-05 11:12:45 - [error_recovery] error_recovery - [INFO] INFO - ✅ Recovery erfolgreich: restart_service +2025-06-05 11:12:45 - [error_recovery] error_recovery - [WARNING] WARNING - 🚨 Fehler erkannt: flask_error - 2025-06-04 23:39:03 - [app] app - [ERROR] ERROR - Interner Serverfehler: 500 Internal Server Error: The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application. +2025-06-05 11:12:45 - [error_recovery] error_recovery - [ERROR] ERROR - 🔥 Eskalation für flask_error: 3 Vorkommen in 300s +2025-06-05 11:12:45 - [error_recovery] error_recovery - [INFO] INFO - 🔧 Führe Recovery-Aktion aus: restart_system +2025-06-05 11:12:45 - [error_recovery] error_recovery - [INFO] INFO - ✅ Recovery erfolgreich: restart_system +2025-06-05 11:12:45 - [error_recovery] error_recovery - [WARNING] WARNING - 🚨 Fehler erkannt: flask_error - 2025-06-04 23:39:33 - [app] app - [ERROR] ERROR - Interner Serverfehler: 500 Internal Server Error: The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application. +2025-06-05 11:12:45 - [error_recovery] error_recovery - [ERROR] ERROR - 🔥 Eskalation für flask_error: 4 Vorkommen in 300s +2025-06-05 11:12:45 - [error_recovery] error_recovery - [INFO] INFO - 🔧 Führe Recovery-Aktion aus: restart_system +2025-06-05 11:12:45 - [error_recovery] error_recovery - [INFO] INFO - ✅ Recovery erfolgreich: restart_system +2025-06-05 11:12:45 - [error_recovery] error_recovery - [WARNING] WARNING - 🚨 Fehler erkannt: flask_error - 2025-06-04 23:39:33 - [app] app - [ERROR] ERROR - Interner Serverfehler: 500 Internal Server Error: The server encountered an internal error and was unable to complete your request. Either the server is overloaded or there is an error in the application. +2025-06-05 11:12:45 - [error_recovery] error_recovery - [ERROR] ERROR - 🔥 Eskalation für flask_error: 5 Vorkommen in 300s +2025-06-05 11:12:45 - [error_recovery] error_recovery - [INFO] INFO - 🔧 Führe Recovery-Aktion aus: restart_system +2025-06-05 11:12:45 - [error_recovery] error_recovery - [INFO] INFO - ✅ Recovery erfolgreich: restart_system diff --git a/backend/logs/jobs/jobs.log b/backend/logs/jobs/jobs.log index e69de29b..5dc1137b 100644 --- a/backend/logs/jobs/jobs.log +++ b/backend/logs/jobs/jobs.log @@ -0,0 +1 @@ +2025-06-05 11:12:49 - [jobs] jobs - [INFO] INFO - Jobs abgerufen: 0 von 0 (Seite 1) diff --git a/backend/logs/maintenance/maintenance.log b/backend/logs/maintenance/maintenance.log index 1ea8537b..4194aa42 100644 --- a/backend/logs/maintenance/maintenance.log +++ b/backend/logs/maintenance/maintenance.log @@ -6,3 +6,5 @@ 2025-06-05 09:31:08 - [maintenance] maintenance - [INFO] INFO - Wartungs-Scheduler gestartet 2025-06-05 10:12:45 - [maintenance] maintenance - [INFO] INFO - Wartungs-Scheduler gestartet 2025-06-05 10:12:45 - [maintenance] maintenance - [INFO] INFO - Wartungs-Scheduler gestartet +2025-06-05 11:12:34 - [maintenance] maintenance - [INFO] INFO - Wartungs-Scheduler gestartet +2025-06-05 11:12:34 - [maintenance] maintenance - [INFO] INFO - Wartungs-Scheduler gestartet diff --git a/backend/logs/multi_location/multi_location.log b/backend/logs/multi_location/multi_location.log index da5b0ebb..e4fa769c 100644 --- a/backend/logs/multi_location/multi_location.log +++ b/backend/logs/multi_location/multi_location.log @@ -6,3 +6,5 @@ 2025-06-05 09:31:08 - [multi_location] multi_location - [INFO] INFO - Standard-Standort erstellt 2025-06-05 10:12:45 - [multi_location] multi_location - [INFO] INFO - Standard-Standort erstellt 2025-06-05 10:12:45 - [multi_location] multi_location - [INFO] INFO - Standard-Standort erstellt +2025-06-05 11:12:34 - [multi_location] multi_location - [INFO] INFO - Standard-Standort erstellt +2025-06-05 11:12:34 - [multi_location] multi_location - [INFO] INFO - Standard-Standort erstellt diff --git a/backend/logs/permissions/permissions.log b/backend/logs/permissions/permissions.log index 7e267acf..14a60aa8 100644 --- a/backend/logs/permissions/permissions.log +++ b/backend/logs/permissions/permissions.log @@ -9,3 +9,4 @@ 2025-06-05 01:01:18 - [permissions] permissions - [INFO] INFO - 🔐 Permission Template Helpers registriert 2025-06-05 09:31:08 - [permissions] permissions - [INFO] INFO - 🔐 Permission Template Helpers registriert 2025-06-05 10:12:45 - [permissions] permissions - [INFO] INFO - 🔐 Permission Template Helpers registriert +2025-06-05 11:12:34 - [permissions] permissions - [INFO] INFO - 🔐 Permission Template Helpers registriert diff --git a/backend/logs/printer_monitor/printer_monitor.log b/backend/logs/printer_monitor/printer_monitor.log index 7b8762c0..8a181a0d 100644 --- a/backend/logs/printer_monitor/printer_monitor.log +++ b/backend/logs/printer_monitor/printer_monitor.log @@ -159,3 +159,9 @@ 2025-06-05 10:19:49 - [printer_monitor] printer_monitor - [INFO] INFO - ℹ️ Keine aktiven Drucker gefunden 2025-06-05 10:19:49 - [printer_monitor] printer_monitor - [INFO] INFO - 🔄 Aktualisiere Live-Druckerstatus... 2025-06-05 10:19:49 - [printer_monitor] printer_monitor - [INFO] INFO - ℹ️ Keine aktiven Drucker gefunden +2025-06-05 11:12:32 - [printer_monitor] printer_monitor - [INFO] INFO - 🖨️ Drucker-Monitor initialisiert +2025-06-05 11:12:32 - [printer_monitor] printer_monitor - [INFO] INFO - 🔍 Automatische Tapo-Erkennung in separatem Thread gestartet +2025-06-05 11:13:02 - [printer_monitor] printer_monitor - [INFO] INFO - 🔄 Aktualisiere Live-Druckerstatus... +2025-06-05 11:13:02 - [printer_monitor] printer_monitor - [INFO] INFO - ℹ️ Keine aktiven Drucker gefunden +2025-06-05 11:13:02 - [printer_monitor] printer_monitor - [INFO] INFO - 🔄 Aktualisiere Live-Druckerstatus... +2025-06-05 11:13:02 - [printer_monitor] printer_monitor - [INFO] INFO - ℹ️ Keine aktiven Drucker gefunden diff --git a/backend/logs/printers/printers.log b/backend/logs/printers/printers.log index 16f61dab..bf7aa0f0 100644 --- a/backend/logs/printers/printers.log +++ b/backend/logs/printers/printers.log @@ -68,3 +68,7 @@ 2025-06-05 10:19:49 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) 2025-06-05 10:19:49 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 0 Drucker 2025-06-05 10:19:49 - [printers] printers - [INFO] INFO - [OK] API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 3.54ms +2025-06-05 11:12:49 - [printers] printers - [INFO] INFO - Schnelles Laden abgeschlossen: 0 Drucker geladen (ohne Status-Check) +2025-06-05 11:13:02 - [printers] printers - [INFO] INFO - 🔄 Live-Status-Abfrage von Benutzer Administrator (ID: 1) +2025-06-05 11:13:02 - [printers] printers - [INFO] INFO - ✅ Live-Status-Abfrage erfolgreich: 0 Drucker +2025-06-05 11:13:02 - [printers] printers - [INFO] INFO - [OK] API-Live-Drucker-Status-Abfrage 'get_live_printer_status' erfolgreich in 25.19ms diff --git a/backend/logs/scheduler/scheduler.log b/backend/logs/scheduler/scheduler.log index 2dc6c550..13943ec2 100644 --- a/backend/logs/scheduler/scheduler.log +++ b/backend/logs/scheduler/scheduler.log @@ -22,3 +22,6 @@ 2025-06-05 10:12:50 - [scheduler] scheduler - [INFO] INFO - Scheduler gestartet 2025-06-05 10:14:01 - [scheduler] scheduler - [INFO] INFO - Scheduler-Thread beendet 2025-06-05 10:14:01 - [scheduler] scheduler - [INFO] INFO - Scheduler gestoppt +2025-06-05 11:12:32 - [scheduler] scheduler - [INFO] INFO - Task check_jobs registriert: Intervall 30s, Enabled: True +2025-06-05 11:12:38 - [scheduler] scheduler - [INFO] INFO - Scheduler-Thread gestartet +2025-06-05 11:12:38 - [scheduler] scheduler - [INFO] INFO - Scheduler gestartet diff --git a/backend/logs/security/security.log b/backend/logs/security/security.log index 84c1f4c7..805916f2 100644 --- a/backend/logs/security/security.log +++ b/backend/logs/security/security.log @@ -9,3 +9,4 @@ 2025-06-05 01:01:18 - [security] security - [INFO] INFO - 🔒 Security System initialisiert 2025-06-05 09:31:08 - [security] security - [INFO] INFO - 🔒 Security System initialisiert 2025-06-05 10:12:45 - [security] security - [INFO] INFO - 🔒 Security System initialisiert +2025-06-05 11:12:34 - [security] security - [INFO] INFO - 🔒 Security System initialisiert diff --git a/backend/logs/shutdown_manager/shutdown_manager.log b/backend/logs/shutdown_manager/shutdown_manager.log index 0d49a1c0..0b499787 100644 --- a/backend/logs/shutdown_manager/shutdown_manager.log +++ b/backend/logs/shutdown_manager/shutdown_manager.log @@ -16,3 +16,4 @@ 2025-06-05 10:14:01 - [shutdown_manager] shutdown_manager - [INFO] INFO - 🔄 Beende Scheduler mit stop()... 2025-06-05 10:14:01 - [shutdown_manager] shutdown_manager - [INFO] INFO - ✅ Scheduler erfolgreich gestoppt 2025-06-05 10:14:06 - [shutdown_manager] shutdown_manager - [INFO] INFO - 💾 Führe sicheres Datenbank-Cleanup durch... +2025-06-05 11:12:34 - [shutdown_manager] shutdown_manager - [INFO] INFO - 🔧 Shutdown-Manager initialisiert diff --git a/backend/logs/startup/startup.log b/backend/logs/startup/startup.log index 81b88e54..fee9d9f0 100644 --- a/backend/logs/startup/startup.log +++ b/backend/logs/startup/startup.log @@ -97,3 +97,12 @@ 2025-06-05 10:12:45 - [startup] startup - [INFO] INFO - 🪟 Windows-Modus: Aktiviert 2025-06-05 10:12:45 - [startup] startup - [INFO] INFO - 🔒 Windows-sichere Log-Rotation: Aktiviert 2025-06-05 10:12:45 - [startup] startup - [INFO] INFO - ================================================== +2025-06-05 11:12:34 - [startup] startup - [INFO] INFO - ================================================== +2025-06-05 11:12:34 - [startup] startup - [INFO] INFO - [START] MYP Platform Backend wird gestartet... +2025-06-05 11:12:34 - [startup] startup - [INFO] INFO - 🐍 Python Version: 3.13.3 (tags/v3.13.3:6280bb5, Apr 8 2025, 14:47:33) [MSC v.1943 64 bit (AMD64)] +2025-06-05 11:12:34 - [startup] startup - [INFO] INFO - 💻 Betriebssystem: nt (win32) +2025-06-05 11:12:34 - [startup] startup - [INFO] INFO - 📁 Arbeitsverzeichnis: C:\Users\TTOMCZA.EMEA\Dev\Projektarbeit-MYP\backend +2025-06-05 11:12:34 - [startup] startup - [INFO] INFO - ⏰ Startzeit: 2025-06-05T11:12:34.706758 +2025-06-05 11:12:34 - [startup] startup - [INFO] INFO - 🪟 Windows-Modus: Aktiviert +2025-06-05 11:12:34 - [startup] startup - [INFO] INFO - 🔒 Windows-sichere Log-Rotation: Aktiviert +2025-06-05 11:12:34 - [startup] startup - [INFO] INFO - ================================================== diff --git a/backend/logs/system_control/system_control.log b/backend/logs/system_control/system_control.log index a71ca04f..b1af4e6b 100644 --- a/backend/logs/system_control/system_control.log +++ b/backend/logs/system_control/system_control.log @@ -139,3 +139,16 @@ 2025-06-05 10:15:01 - [system_control] system_control - [INFO] INFO - ▶️ Führe Operation aus: restart 2025-06-05 10:15:01 - [system_control] system_control - [WARNING] WARNING - 🔄 System-Neustart wird ausgeführt... 2025-06-05 10:15:01 - [system_control] system_control - [INFO] INFO - 🧹 Cleanup vor Neustart/Shutdown... +2025-06-05 11:12:35 - [system_control] system_control - [INFO] INFO - 🔧 System-Control-Manager initialisiert +2025-06-05 11:12:39 - [system_control] system_control - [INFO] INFO - 🕐 Operation geplant: service_restart in 5s +2025-06-05 11:12:41 - [system_control] system_control - [INFO] INFO - 🕐 Operation geplant: service_restart in 5s +2025-06-05 11:12:44 - [system_control] system_control - [INFO] INFO - ▶️ Führe Operation aus: service_restart +2025-06-05 11:12:44 - [system_control] system_control - [INFO] INFO - 🔄 Services werden neugestartet... +2025-06-05 11:12:45 - [system_control] system_control - [INFO] INFO - 🕐 Operation geplant: service_restart in 5s +2025-06-05 11:12:45 - [system_control] system_control - [INFO] INFO - 🕐 Operation geplant: restart in 60s +2025-06-05 11:12:45 - [system_control] system_control - [INFO] INFO - 🕐 Operation geplant: restart in 60s +2025-06-05 11:12:45 - [system_control] system_control - [INFO] INFO - 🕐 Operation geplant: restart in 60s +2025-06-05 11:12:46 - [system_control] system_control - [INFO] INFO - ▶️ Führe Operation aus: service_restart +2025-06-05 11:12:46 - [system_control] system_control - [INFO] INFO - 🔄 Services werden neugestartet... +2025-06-05 11:12:50 - [system_control] system_control - [INFO] INFO - ▶️ Führe Operation aus: service_restart +2025-06-05 11:12:50 - [system_control] system_control - [INFO] INFO - 🔄 Services werden neugestartet... diff --git a/backend/logs/tapo_controller/tapo_controller.log b/backend/logs/tapo_controller/tapo_controller.log new file mode 100644 index 00000000..276ac0ad --- /dev/null +++ b/backend/logs/tapo_controller/tapo_controller.log @@ -0,0 +1,12 @@ +2025-06-05 11:12:32 - [tapo_controller] tapo_controller - [INFO] INFO - ✅ tapo controller initialisiert +2025-06-05 11:12:34 - [tapo_controller] tapo_controller - [INFO] INFO - 🔍 starte automatische tapo-steckdosenerkennung... +2025-06-05 11:12:34 - [tapo_controller] tapo_controller - [INFO] INFO - 🔄 teste 6 standard-ips aus der konfiguration +2025-06-05 11:12:34 - [tapo_controller] tapo_controller - [INFO] INFO - 🔍 teste ip 1/6: 192.168.0.103 +2025-06-05 11:12:38 - [tapo_controller] tapo_controller - [INFO] INFO - 🚀 starte steckdosen-initialisierung... +2025-06-05 11:12:38 - [tapo_controller] tapo_controller - [WARNING] WARNING - ⚠️ keine aktiven drucker zur initialisierung gefunden +2025-06-05 11:12:40 - [tapo_controller] tapo_controller - [INFO] INFO - 🔍 teste ip 2/6: 192.168.0.104 +2025-06-05 11:12:46 - [tapo_controller] tapo_controller - [INFO] INFO - 🔍 teste ip 3/6: 192.168.0.100 +2025-06-05 11:12:52 - [tapo_controller] tapo_controller - [INFO] INFO - 🔍 teste ip 4/6: 192.168.0.101 +2025-06-05 11:12:58 - [tapo_controller] tapo_controller - [INFO] INFO - 🔍 teste ip 5/6: 192.168.0.102 +2025-06-05 11:13:04 - [tapo_controller] tapo_controller - [INFO] INFO - 🔍 teste ip 6/6: 192.168.0.105 +2025-06-05 11:13:10 - [tapo_controller] tapo_controller - [INFO] INFO - ✅ steckdosen-erkennung abgeschlossen: 0/6 steckdosen gefunden in 36.7s diff --git a/backend/logs/windows_fixes/windows_fixes.log b/backend/logs/windows_fixes/windows_fixes.log index de3a26a6..7a35e82d 100644 --- a/backend/logs/windows_fixes/windows_fixes.log +++ b/backend/logs/windows_fixes/windows_fixes.log @@ -50,3 +50,7 @@ 2025-06-05 10:12:39 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Alle Windows-Fixes erfolgreich angewendet 2025-06-05 10:14:06 - [windows_fixes] windows_fixes - [INFO] INFO - 🔄 Starte Windows Thread-Shutdown... 2025-06-05 10:14:06 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Windows Thread-Shutdown abgeschlossen +2025-06-05 11:12:31 - [windows_fixes] windows_fixes - [INFO] INFO - 🔧 Wende Windows-spezifische Fixes an... +2025-06-05 11:12:31 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Subprocess automatisch gepatcht für UTF-8 Encoding (run + Popen) +2025-06-05 11:12:31 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Globaler subprocess-Patch angewendet +2025-06-05 11:12:31 - [windows_fixes] windows_fixes - [INFO] INFO - ✅ Alle Windows-Fixes erfolgreich angewendet diff --git a/backend/utils/__pycache__/job_scheduler.cpython-313.pyc b/backend/utils/__pycache__/job_scheduler.cpython-313.pyc index cf90958ea8e424ad828621aec84a5625e5bfa980..44a9d77014304230d3ce47349ea9d1acc6f1d914 100644 GIT binary patch delta 3363 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
  • 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 diff --git a/backend/utils/__pycache__/tapo_controller.cpython-313.pyc b/backend/utils/__pycache__/tapo_controller.cpython-313.pyc new file mode 100644 index 0000000000000000000000000000000000000000..82127f9be955adbbd88506d23588682e97ac05d7 GIT binary patch literal 28919 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 literal 0 HcmV?d00001 diff --git a/backend/utils/tapo_controller.py b/backend/utils/tapo_controller.py index 2f7bd30c..1c8887cc 100644 --- a/backend/utils/tapo_controller.py +++ b/backend/utils/tapo_controller.py @@ -1,6 +1,6 @@ """ -TP-Link Tapo P110 Zentraler Controller für MYP Platform -Sammelt alle operativen Tapo-Steckdosen-Funktionalitäten an einem Ort. +tp-link tapo p110 zentraler controller für myp platform +sammelt alle operativen tapo-steckdosen-funktionalitäten an einem ort. """ import time @@ -15,24 +15,24 @@ from models import get_db_session, Printer, PlugStatusLog from utils.logging_config import get_logger from utils.settings import TAPO_USERNAME, TAPO_PASSWORD, DEFAULT_TAPO_IPS, TAPO_TIMEOUT, TAPO_RETRY_COUNT -# TP-Link Tapo P110 Unterstützung prüfen +# tp-link tapo p110 unterstützung prüfen try: from PyP100 import PyP100 TAPO_AVAILABLE = True except ImportError: TAPO_AVAILABLE = False -# Logger initialisieren +# logger initialisieren logger = get_logger("tapo_controller") class TapoController: """ - Zentraler Controller für alle TP-Link Tapo P110 Operationen. + zentraler controller für alle tp-link tapo p110 operationen. """ def __init__(self): - """Initialisiere den Tapo Controller.""" + """initialisiere den tapo controller.""" self.username = TAPO_USERNAME self.password = TAPO_PASSWORD self.timeout = TAPO_TIMEOUT @@ -40,107 +40,107 @@ class TapoController: self.auto_discovered = False if not TAPO_AVAILABLE: - logger.error("❌ PyP100-Modul nicht installiert - Tapo-Funktionalität eingeschränkt") + logger.error("❌ PyP100-modul nicht installiert - tapo-funktionalität eingeschränkt") else: - logger.info("✅ Tapo Controller initialisiert") + logger.info("✅ tapo controller initialisiert") def toggle_plug(self, ip: str, state: bool, username: str = None, password: str = None) -> bool: """ - Schaltet eine TP-Link Tapo P100/P110-Steckdose ein oder aus. + schaltet eine tp-link tapo p100/p110-steckdose ein oder aus. - Args: - ip: IP-Adresse der Steckdose - state: True = Ein, False = Aus - username: Benutzername (optional, nutzt Standard wenn nicht angegeben) - password: Passwort (optional, nutzt Standard wenn nicht angegeben) + args: + ip: ip-adresse der steckdose + state: true = ein, false = aus + username: benutzername (optional, nutzt standard wenn nicht angegeben) + password: passwort (optional, nutzt standard wenn nicht angegeben) - Returns: - bool: True wenn erfolgreich geschaltet + returns: + bool: true wenn erfolgreich geschaltet """ if not TAPO_AVAILABLE: - logger.error("❌ PyP100-Modul nicht installiert - Steckdose kann nicht geschaltet werden") + logger.error("❌ PyP100-modul nicht installiert - steckdose kann nicht geschaltet werden") return False - # IMMER globale Anmeldedaten verwenden + # immer globale anmeldedaten verwenden username = self.username password = self.password - logger.debug(f"🔧 Verwende globale Tapo-Anmeldedaten für {ip}") + logger.debug(f"🔧 verwende globale tapo-anmeldedaten für {ip}") for attempt in range(self.retry_count): try: - # P100-Verbindung herstellen + # p100-verbindung herstellen p100 = PyP100.P100(ip, username, password) p100.handshake() p100.login() - # Steckdose schalten + # steckdose schalten if state: p100.turnOn() - logger.info(f"✅ Tapo-Steckdose {ip} erfolgreich eingeschaltet") + logger.info(f"✅ tapo-steckdose {ip} erfolgreich eingeschaltet") else: p100.turnOff() - logger.info(f"✅ Tapo-Steckdose {ip} erfolgreich ausgeschaltet") + logger.info(f"✅ tapo-steckdose {ip} erfolgreich ausgeschaltet") return True except Exception as e: action = "ein" if state else "aus" - logger.warning(f"⚠️ Versuch {attempt+1}/{self.retry_count} fehlgeschlagen beim {action}schalten von {ip}: {str(e)}") + logger.warning(f"⚠️ versuch {attempt+1}/{self.retry_count} fehlgeschlagen beim {action}schalten von {ip}: {str(e)}") if attempt < self.retry_count - 1: - time.sleep(1) # Kurze Pause vor erneutem Versuch + time.sleep(1) # kurze pause vor erneutem versuch else: - logger.error(f"❌ Fehler beim {action}schalten der Tapo-Steckdose {ip}: {str(e)}") + logger.error(f"❌ fehler beim {action}schalten der tapo-steckdose {ip}: {str(e)}") return False def turn_off(self, ip: str, username: str = None, password: str = None, printer_id: int = None) -> bool: """ - Schaltet eine TP-Link Tapo P110-Steckdose aus. + schaltet eine tp-link tapo p110-steckdose aus. - Args: - ip: IP-Adresse der Steckdose - username: Benutzername (optional) - password: Passwort (optional) - printer_id: ID des zugehörigen Druckers für Logging (optional) + args: + ip: ip-adresse der steckdose + username: benutzername (optional) + password: passwort (optional) + printer_id: id des zugehörigen druckers für logging (optional) - Returns: - bool: True wenn erfolgreich ausgeschaltet + returns: + bool: true wenn erfolgreich ausgeschaltet """ if not TAPO_AVAILABLE: - logger.error("⚠️ PyP100-Modul nicht verfügbar - kann Tapo-Steckdose nicht schalten") - self._log_plug_status(printer_id, "disconnected", ip, error_message="PyP100-Modul nicht verfügbar") + logger.error("⚠️ PyP100-modul nicht verfügbar - kann tapo-steckdose nicht schalten") + self._log_plug_status(printer_id, "disconnected", ip, error_message="PyP100-modul nicht verfügbar") return False - # IMMER globale Anmeldedaten verwenden + # immer globale anmeldedaten verwenden username = self.username password = self.password start_time = time.time() try: - # TP-Link Tapo P100 Verbindung herstellen + # tp-link tapo p100 verbindung herstellen p100 = PyP100.P100(ip, username, password) p100.handshake() p100.login() - # Steckdose ausschalten + # steckdose ausschalten p100.turnOff() - response_time = int((time.time() - start_time) * 1000) # in Millisekunden - logger.debug(f"✅ Tapo-Steckdose {ip} erfolgreich ausgeschaltet") + response_time = int((time.time() - start_time) * 1000) # in millisekunden + logger.debug(f"✅ tapo-steckdose {ip} erfolgreich ausgeschaltet") - # Logging: Erfolgreich ausgeschaltet + # logging: erfolgreich ausgeschaltet self._log_plug_status(printer_id, "off", ip, response_time_ms=response_time) return True except Exception as e: response_time = int((time.time() - start_time) * 1000) - logger.debug(f"⚠️ Fehler beim Ausschalten der Tapo-Steckdose {ip}: {str(e)}") + logger.debug(f"⚠️ fehler beim ausschalten der tapo-steckdose {ip}: {str(e)}") - # Logging: Fehlgeschlagener Versuch + # logging: fehlgeschlagener versuch self._log_plug_status(printer_id, "disconnected", ip, response_time_ms=response_time, error_message=str(e)) @@ -150,83 +150,83 @@ class TapoController: def check_outlet_status(self, ip: str, username: str = None, password: str = None, printer_id: int = None) -> Tuple[bool, str]: """ - Überprüft den Status einer TP-Link Tapo P110-Steckdose. + überprüft den status einer tp-link tapo p110-steckdose. - Args: - ip: IP-Adresse der Steckdose - username: Benutzername (optional) - password: Passwort (optional) - printer_id: ID des zugehörigen Druckers für Logging (optional) + args: + ip: ip-adresse der steckdose + username: benutzername (optional) + password: passwort (optional) + printer_id: id des zugehörigen druckers für logging (optional) - Returns: - Tuple[bool, str]: (Erreichbar, Status) - Status: "on", "off", "unknown" + returns: + tuple[bool, str]: (erreichbar, status) - status: "on", "off", "unknown" """ if not TAPO_AVAILABLE: - logger.debug("⚠️ PyP100-Modul nicht verfügbar - kann Tapo-Steckdosen-Status nicht abfragen") + logger.debug("⚠️ PyP100-modul nicht verfügbar - kann tapo-steckdosen-status nicht abfragen") self._log_plug_status(printer_id, "disconnected", ip, - error_message="PyP100-Modul nicht verfügbar", - notes="Status-Check fehlgeschlagen") + error_message="PyP100-modul nicht verfügbar", + notes="status-check fehlgeschlagen") return False, "unknown" - # IMMER globale Anmeldedaten verwenden + # immer globale anmeldedaten verwenden username = self.username password = self.password start_time = time.time() try: - # TP-Link Tapo P100 Verbindung herstellen + # tp-link tapo p100 verbindung herstellen p100 = PyP100.P100(ip, username, password) p100.handshake() p100.login() - # Geräteinformationen abrufen + # geräteinformationen abrufen device_info = p100.getDeviceInfo() - # Status auswerten + # status auswerten device_on = device_info.get('device_on', False) status = "on" if device_on else "off" response_time = int((time.time() - start_time) * 1000) - logger.debug(f"✅ Tapo-Steckdose {ip}: Status = {status}") + logger.debug(f"✅ tapo-steckdose {ip}: status = {status}") - # Erweiterte Informationen sammeln + # erweiterte informationen sammeln extra_info = self._collect_device_info(p100, device_info) - # Logging: Erfolgreicher Status-Check + # logging: erfolgreicher status-check self._log_plug_status(printer_id, status, ip, response_time_ms=response_time, power_consumption=extra_info.get('power_consumption'), voltage=extra_info.get('voltage'), current=extra_info.get('current'), firmware_version=extra_info.get('firmware_version'), - notes="Automatischer Status-Check") + notes="automatischer status-check") return True, status except Exception as e: response_time = int((time.time() - start_time) * 1000) - logger.debug(f"⚠️ Fehler bei Tapo-Steckdosen-Status-Check {ip}: {str(e)}") + logger.debug(f"⚠️ fehler bei tapo-steckdosen-status-check {ip}: {str(e)}") - # Logging: Fehlgeschlagener Status-Check + # logging: fehlgeschlagener status-check self._log_plug_status(printer_id, "disconnected", ip, response_time_ms=response_time, error_message=str(e), - notes="Status-Check fehlgeschlagen") + notes="status-check fehlgeschlagen") return False, "unknown" def test_connection(self, ip: str, username: str = None, password: str = None) -> dict: """ - Testet die Verbindung zu einer TP-Link Tapo P110-Steckdose. + testet die verbindung zu einer tp-link tapo p110-steckdose. - Args: - ip: IP-Adresse der Steckdose - username: Benutzername (optional) - password: Passwort (optional) + args: + ip: ip-adresse der steckdose + username: benutzername (optional) + password: passwort (optional) - Returns: - dict: Ergebnis mit Status und Informationen + returns: + dict: ergebnis mit status und informationen """ result = { "success": False, @@ -236,58 +236,58 @@ class TapoController: } if not TAPO_AVAILABLE: - result["message"] = "PyP100-Modul nicht verfügbar" + result["message"] = "PyP100-modul nicht verfügbar" result["error"] = "ModuleNotFound" - logger.error("PyP100-Modul nicht verfügbar - kann Tapo-Steckdosen nicht testen") + logger.error("PyP100-modul nicht verfügbar - kann tapo-steckdosen nicht testen") return result - # Verwende globale Anmeldedaten falls nicht angegeben + # verwende globale anmeldedaten falls nicht angegeben if not username or not password: username = self.username password = self.password - logger.debug(f"Verwende globale Tapo-Anmeldedaten für {ip}") + logger.debug(f"verwende globale tapo-anmeldedaten für {ip}") try: - # TP-Link Tapo P100 Verbindung herstellen + # tp-link tapo p100 verbindung herstellen p100 = PyP100.P100(ip, username, password) p100.handshake() p100.login() - # Geräteinformationen abrufen + # geräteinformationen abrufen device_info = p100.getDeviceInfo() result["success"] = True - result["message"] = "Verbindung erfolgreich" + result["message"] = "verbindung erfolgreich" result["device_info"] = device_info - logger.info(f"Tapo-Verbindung zu {ip} erfolgreich: {device_info.get('nickname', 'Unbekannt')}") + logger.info(f"tapo-verbindung zu {ip} erfolgreich: {device_info.get('nickname', 'unbekannt')}") except Exception as e: result["success"] = False - result["message"] = f"Verbindungsfehler: {str(e)}" + result["message"] = f"verbindungsfehler: {str(e)}" result["error"] = str(e) - logger.error(f"Fehler bei Tapo-Test zu {ip}: {str(e)}") + logger.error(f"fehler bei tapo-test zu {ip}: {str(e)}") return result def ping_address(self, ip: str, timeout: int = 3) -> bool: """ - Führt einen Konnektivitätstest zu einer IP-Adresse durch. - Verwendet TCP-Verbindung statt Ping für bessere Kompatibilität. + führt einen konnektivitätstest zu einer ip-adresse durch. + verwendet tcp-verbindung statt ping für bessere kompatibilität. - Args: - ip: Zu testende IP-Adresse - timeout: Timeout in Sekunden + args: + ip: zu testende ip-adresse + timeout: timeout in sekunden - Returns: - bool: True wenn Verbindung erfolgreich + returns: + bool: true wenn verbindung erfolgreich """ try: - # IP-Adresse validieren + # ip-adresse validieren ipaddress.ip_address(ip.strip()) - # Standard-Ports für Tapo-Steckdosen testen - test_ports = [9999, 80, 443] # Tapo-Standard, HTTP, HTTPS + # standard-ports für tapo-steckdosen testen + test_ports = [9999, 80, 443] # tapo-standard, http, https for port in test_ports: sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) @@ -296,87 +296,87 @@ class TapoController: sock.close() if result == 0: - logger.debug(f"✅ Verbindung zu {ip}:{port} erfolgreich") + logger.debug(f"✅ verbindung zu {ip}:{port} erfolgreich") return True - logger.debug(f"❌ Keine Verbindung zu {ip} auf Standard-Ports möglich") + logger.debug(f"❌ keine verbindung zu {ip} auf standard-ports möglich") return False except Exception as e: - logger.debug(f"❌ Fehler beim Verbindungstest zu {ip}: {str(e)}") + logger.debug(f"❌ fehler beim verbindungstest zu {ip}: {str(e)}") return False def auto_discover_outlets(self) -> Dict[str, bool]: """ - Automatische Erkennung und Konfiguration von TP-Link Tapo P110-Steckdosen im Netzwerk. + automatische erkennung und konfiguration von tp-link tapo p110-steckdosen im netzwerk. - Returns: - Dict[str, bool]: Ergebnis der Steckdosenerkennung mit IP als Schlüssel + returns: + dict[str, bool]: ergebnis der steckdosenerkennung mit ip als schlüssel """ if self.auto_discovered: - logger.info("🔍 Tapo-Steckdosen wurden bereits erkannt") + logger.info("🔍 tapo-steckdosen wurden bereits erkannt") return {} - logger.info("🔍 Starte automatische Tapo-Steckdosenerkennung...") + logger.info("🔍 starte automatische tapo-steckdosenerkennung...") results = {} start_time = time.time() - # Standard-IPs aus der Konfiguration testen - logger.info(f"🔄 Teste {len(DEFAULT_TAPO_IPS)} Standard-IPs aus der Konfiguration") + # standard-ips aus der konfiguration testen + logger.info(f"🔄 teste {len(DEFAULT_TAPO_IPS)} standard-ips aus der konfiguration") for i, ip in enumerate(DEFAULT_TAPO_IPS): try: - logger.info(f"🔍 Teste IP {i+1}/{len(DEFAULT_TAPO_IPS)}: {ip}") + logger.info(f"🔍 teste ip {i+1}/{len(DEFAULT_TAPO_IPS)}: {ip}") - # Schneller Ping-Test + # schneller ping-test if self.ping_address(ip, timeout=2): - logger.info(f"✅ Steckdose mit IP {ip} ist erreichbar") + logger.info(f"✅ steckdose mit ip {ip} ist erreichbar") - # Tapo-Verbindung testen + # tapo-verbindung testen test_result = self.test_connection(ip) if test_result["success"]: device_info = test_result["device_info"] - nickname = device_info.get('nickname', f"Tapo P110 ({ip})") + nickname = device_info.get('nickname', f"tapo p110 ({ip})") state = "on" if device_info.get('device_on', False) else "off" - logger.info(f"✅ Tapo-Steckdose '{nickname}' ({ip}) gefunden - Status: {state}") + logger.info(f"✅ tapo-steckdose '{nickname}' ({ip}) gefunden - status: {state}") results[ip] = True - # Steckdose in Datenbank speichern/aktualisieren + # steckdose in datenbank speichern/aktualisieren try: self._ensure_outlet_in_database(ip, nickname) except Exception as db_error: - logger.warning(f"⚠️ Fehler beim Speichern in DB für {ip}: {str(db_error)}") + logger.warning(f"⚠️ fehler beim speichern in db für {ip}: {str(db_error)}") else: - logger.debug(f"❌ IP {ip} ist erreichbar, aber keine Tapo-Steckdose") + logger.debug(f"❌ ip {ip} ist erreichbar, aber keine tapo-steckdose") results[ip] = False else: - logger.debug(f"❌ IP {ip} nicht erreichbar") + logger.debug(f"❌ ip {ip} nicht erreichbar") results[ip] = False except Exception as e: - logger.warning(f"❌ Fehler bei Steckdosen-Erkennung für IP {ip}: {str(e)}") + logger.warning(f"❌ fehler bei steckdosen-erkennung für ip {ip}: {str(e)}") results[ip] = False continue - # Erfolgsstatistik + # erfolgsstatistik success_count = sum(1 for success in results.values() if success) elapsed_time = time.time() - start_time - logger.info(f"✅ Steckdosen-Erkennung abgeschlossen: {success_count}/{len(results)} Steckdosen gefunden in {elapsed_time:.1f}s") + logger.info(f"✅ steckdosen-erkennung abgeschlossen: {success_count}/{len(results)} steckdosen gefunden in {elapsed_time:.1f}s") self.auto_discovered = True return results def initialize_all_outlets(self) -> Dict[str, bool]: """ - Schaltet alle gespeicherten Steckdosen aus (einheitlicher Startzustand). + schaltet alle gespeicherten steckdosen aus (einheitlicher startzustand). - Returns: - Dict[str, bool]: Ergebnis der Initialisierung pro Drucker + returns: + dict[str, bool]: ergebnis der initialisierung pro drucker """ - logger.info("🚀 Starte Steckdosen-Initialisierung...") + logger.info("🚀 starte steckdosen-initialisierung...") results = {} try: @@ -384,11 +384,11 @@ class TapoController: printers = db_session.query(Printer).filter(Printer.active == True).all() if not printers: - logger.warning("⚠️ Keine aktiven Drucker zur Initialisierung gefunden") + logger.warning("⚠️ keine aktiven drucker zur initialisierung gefunden") db_session.close() return results - # Alle Steckdosen ausschalten + # alle steckdosen ausschalten for printer in printers: try: if printer.plug_ip: @@ -400,39 +400,39 @@ class TapoController: results[printer.name] = success if success: - logger.info(f"✅ {printer.name}: Steckdose ausgeschaltet") + logger.info(f"✅ {printer.name}: steckdose ausgeschaltet") printer.status = "offline" printer.last_checked = datetime.now() else: - logger.warning(f"❌ {printer.name}: Steckdose konnte nicht ausgeschaltet werden") + logger.warning(f"❌ {printer.name}: steckdose konnte nicht ausgeschaltet werden") else: - logger.warning(f"⚠️ {printer.name}: Keine Steckdosen-IP konfiguriert") + logger.warning(f"⚠️ {printer.name}: keine steckdosen-ip konfiguriert") results[printer.name] = False except Exception as e: - logger.error(f"❌ Fehler bei Initialisierung von {printer.name}: {str(e)}") + logger.error(f"❌ fehler bei initialisierung von {printer.name}: {str(e)}") results[printer.name] = False - # Änderungen speichern + # änderungen speichern db_session.commit() db_session.close() success_count = sum(1 for success in results.values() if success) total_count = len(results) - logger.info(f"🎯 Steckdosen-Initialisierung abgeschlossen: {success_count}/{total_count} erfolgreich") + logger.info(f"🎯 steckdosen-initialisierung abgeschlossen: {success_count}/{total_count} erfolgreich") except Exception as e: - logger.error(f"❌ Kritischer Fehler bei Steckdosen-Initialisierung: {str(e)}") + logger.error(f"❌ kritischer fehler bei steckdosen-initialisierung: {str(e)}") return results def get_all_outlet_status(self) -> Dict[str, Dict[str, Any]]: """ - Holt den Status aller konfigurierten Tapo-Steckdosen. + holt den status aller konfigurierten tapo-steckdosen. - Returns: - Dict[str, Dict]: Status aller Steckdosen mit IP als Schlüssel + returns: + dict[str, dict]: status aller steckdosen mit ip als schlüssel """ status_dict = {} @@ -444,13 +444,13 @@ class TapoController: ).all() if not printers: - logger.info("ℹ️ Keine Drucker mit Tapo-Steckdosen konfiguriert") + logger.info("ℹ️ keine drucker mit tapo-steckdosen konfiguriert") db_session.close() return status_dict - logger.info(f"🔍 Prüfe Status von {len(printers)} Tapo-Steckdosen...") + logger.info(f"🔍 prüfe status von {len(printers)} tapo-steckdosen...") - # Parallel-Status-Prüfung + # parallel-status-prüfung with ThreadPoolExecutor(max_workers=min(len(printers), 8)) as executor: future_to_printer = { executor.submit( @@ -474,7 +474,7 @@ class TapoController: "last_checked": datetime.now().isoformat() } except Exception as e: - logger.error(f"❌ Fehler bei Status-Check für {printer.name}: {str(e)}") + logger.error(f"❌ fehler bei status-check für {printer.name}: {str(e)}") status_dict[printer.plug_ip] = { "printer_name": printer.name, "printer_id": printer.id, @@ -486,31 +486,31 @@ class TapoController: } db_session.close() - logger.info(f"✅ Status-Update abgeschlossen für {len(status_dict)} Steckdosen") + logger.info(f"✅ status-update abgeschlossen für {len(status_dict)} steckdosen") except Exception as e: - logger.error(f"❌ Kritischer Fehler beim Abrufen des Steckdosen-Status: {str(e)}") + logger.error(f"❌ kritischer fehler beim abrufen des steckdosen-status: {str(e)}") return status_dict def _collect_device_info(self, p100: PyP100.P100, device_info: dict) -> dict: """ - Sammelt erweiterte Geräteinformationen von der Tapo-Steckdose. + sammelt erweiterte geräteinformationen von der tapo-steckdose. - Args: - p100: PyP100-Instanz - device_info: Basis-Geräteinformationen + args: + p100: pyp100-instanz + device_info: basis-geräteinformationen - Returns: - dict: Erweiterte Informationen + returns: + dict: erweiterte informationen """ extra_info = {} try: - # Firmware-Version + # firmware-version extra_info['firmware_version'] = device_info.get('fw_ver', None) - # Versuche Energiedaten zu holen (nur P110) + # versuche energiedaten zu holen (nur p110) try: energy_usage = p100.getEnergyUsage() if energy_usage: @@ -518,22 +518,22 @@ class TapoController: extra_info['voltage'] = energy_usage.get('voltage', None) extra_info['current'] = energy_usage.get('current', None) except: - pass # P100 unterstützt keine Energiedaten + pass # p100 unterstützt keine energiedaten except Exception as e: - logger.debug(f"Fehler beim Sammeln erweiterter Geräteinformationen: {str(e)}") + logger.debug(f"fehler beim sammeln erweiterter geräteinformationen: {str(e)}") return extra_info def _log_plug_status(self, printer_id: int, status: str, ip_address: str, **kwargs): """ - Protokolliert Steckdosen-Status in der Datenbank. + protokolliert steckdosen-status in der datenbank. - Args: - printer_id: ID des Druckers - status: Status der Steckdose - ip_address: IP-Adresse der Steckdose - **kwargs: Zusätzliche Parameter für das Logging + args: + printer_id: id des druckers + status: status der steckdose + ip_address: ip-adresse der steckdose + **kwargs: zusätzliche parameter für das logging """ if not printer_id: return @@ -547,57 +547,57 @@ class TapoController: **kwargs ) except Exception as e: - logger.warning(f"Fehler beim Loggen des Steckdosen-Status: {e}") + logger.warning(f"fehler beim loggen des steckdosen-status: {e}") def _ensure_outlet_in_database(self, ip_address: str, nickname: str = None) -> bool: """ - Stellt sicher, dass eine erkannte Tapo-Steckdose in der Datenbank existiert. + stellt sicher, dass eine erkannte tapo-steckdose in der datenbank existiert. - Args: - ip_address: IP-Adresse der Steckdose - nickname: Name der Steckdose (optional) + args: + ip_address: ip-adresse der steckdose + nickname: name der steckdose (optional) - Returns: - bool: True wenn erfolgreich gespeichert/aktualisiert + returns: + bool: true wenn erfolgreich gespeichert/aktualisiert """ try: db_session = get_db_session() - # Prüfen, ob Drucker mit dieser IP bereits existiert + # prüfen, ob drucker mit dieser ip bereits existiert existing_printer = db_session.query(Printer).filter( Printer.plug_ip == ip_address ).first() if existing_printer: - # Drucker aktualisieren + # drucker aktualisieren if not existing_printer.plug_username or not existing_printer.plug_password: existing_printer.plug_username = self.username existing_printer.plug_password = self.password - logger.info(f"✅ Drucker {existing_printer.name} mit Tapo-Anmeldedaten aktualisiert") + logger.info(f"✅ drucker {existing_printer.name} mit tapo-anmeldedaten aktualisiert") if nickname and existing_printer.name != nickname and "Tapo P110" not in existing_printer.name: old_name = existing_printer.name existing_printer.name = nickname - logger.info(f"✅ Drucker {old_name} umbenannt zu {nickname}") + logger.info(f"✅ drucker {old_name} umbenannt zu {nickname}") - # Drucker als aktiv markieren + # drucker als aktiv markieren if not existing_printer.active: existing_printer.active = True - logger.info(f"✅ Drucker {existing_printer.name} als aktiv markiert") + logger.info(f"✅ drucker {existing_printer.name} als aktiv markiert") existing_printer.last_checked = datetime.now() db_session.commit() db_session.close() return True else: - # Neuen Drucker erstellen - printer_name = nickname or f"Tapo P110 ({ip_address})" + # neuen drucker erstellen + printer_name = nickname or f"tapo p110 ({ip_address})" mac_address = f"tapo:{ip_address.replace('.', '-')}" new_printer = Printer( name=printer_name, model="TP-Link Tapo P110", - location="Automatisch erkannt", + location="automatisch erkannt", ip_address=ip_address, mac_address=mac_address, plug_ip=ip_address, @@ -610,12 +610,12 @@ class TapoController: db_session.add(new_printer) db_session.commit() - logger.info(f"✅ Neuer Drucker '{printer_name}' mit Tapo-Steckdose {ip_address} erstellt") + logger.info(f"✅ neuer drucker '{printer_name}' mit tapo-steckdose {ip_address} erstellt") db_session.close() return True except Exception as e: - logger.error(f"❌ Fehler beim Speichern der Tapo-Steckdose {ip_address}: {str(e)}") + logger.error(f"❌ fehler beim speichern der tapo-steckdose {ip_address}: {str(e)}") try: db_session.rollback() db_session.close() @@ -624,32 +624,32 @@ class TapoController: return False -# Globale Instanz für einfachen Zugriff +# globale instanz für einfachen zugriff tapo_controller = TapoController() -# Convenience-Funktionen für Rückwärtskompatibilität +# convenience-funktionen für rückwärtskompatibilität def toggle_plug(ip: str, state: bool, username: str = None, password: str = None) -> bool: - """Schaltet eine Tapo-Steckdose ein/aus.""" + """schaltet eine tapo-steckdose ein/aus.""" return tapo_controller.toggle_plug(ip, state, username, password) def test_tapo_connection(ip: str, username: str = None, password: str = None) -> dict: - """Testet die Verbindung zu einer Tapo-Steckdose.""" + """testet die verbindung zu einer tapo-steckdose.""" return tapo_controller.test_connection(ip, username, password) def check_outlet_status(ip: str, username: str = None, password: str = None, printer_id: int = None) -> Tuple[bool, str]: - """Prüft den Status einer Tapo-Steckdose.""" + """prüft den status einer tapo-steckdose.""" return tapo_controller.check_outlet_status(ip, username, password, printer_id) def auto_discover_tapo_outlets() -> Dict[str, bool]: - """Führt automatische Erkennung von Tapo-Steckdosen durch.""" + """führt automatische erkennung von tapo-steckdosen durch.""" return tapo_controller.auto_discover_outlets() def initialize_all_outlets() -> Dict[str, bool]: - """Initialisiert alle Tapo-Steckdosen (schaltet sie aus).""" + """initialisiert alle tapo-steckdosen (schaltet sie aus).""" return tapo_controller.initialize_all_outlets() \ No newline at end of file