824 lines
36 KiB
Python
824 lines
36 KiB
Python
"""
|
||
Live-Drucker-Monitor für MYP Platform
|
||
Überwacht Druckerstatus in Echtzeit mit Session-Caching und automatischer Steckdosen-Initialisierung.
|
||
"""
|
||
|
||
import time
|
||
import threading
|
||
import requests
|
||
import subprocess
|
||
import ipaddress
|
||
from datetime import datetime, timedelta
|
||
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, PlugStatusLog
|
||
from utils.logging_config import get_logger
|
||
from config.settings import PRINTERS, TAPO_USERNAME, TAPO_PASSWORD, DEFAULT_TAPO_IPS, TAPO_AUTO_DISCOVERY
|
||
|
||
# TP-Link Tapo P110 Unterstützung hinzufügen
|
||
try:
|
||
from PyP100 import PyP100
|
||
TAPO_AVAILABLE = True
|
||
except ImportError:
|
||
TAPO_AVAILABLE = False
|
||
|
||
# Logger initialisieren
|
||
monitor_logger = get_logger("printer_monitor")
|
||
|
||
class PrinterMonitor:
|
||
"""
|
||
Live-Drucker-Monitor mit Session-Caching und automatischer Initialisierung.
|
||
"""
|
||
|
||
def __init__(self):
|
||
self.session_cache = {} # Session-basierter Cache für schnelle Zugriffe
|
||
self.db_cache = {} # Datenbank-Cache für persistente Daten
|
||
self.cache_lock = threading.Lock()
|
||
self.last_db_sync = datetime.now()
|
||
self.monitoring_active = False
|
||
self.monitor_thread = None
|
||
self.startup_initialized = False
|
||
self.auto_discovered_tapo = False
|
||
|
||
# Cache-Konfiguration
|
||
self.session_cache_ttl = 30 # 30 Sekunden für Session-Cache
|
||
self.db_cache_ttl = 300 # 5 Minuten für DB-Cache
|
||
|
||
monitor_logger.info("🖨️ Drucker-Monitor initialisiert")
|
||
|
||
# Automatische Steckdosenerkennung in separatem Thread starten, falls aktiviert
|
||
if TAPO_AUTO_DISCOVERY:
|
||
discovery_thread = threading.Thread(
|
||
target=self._run_auto_discovery,
|
||
daemon=True,
|
||
name="TapoAutoDiscovery"
|
||
)
|
||
discovery_thread.start()
|
||
monitor_logger.info("🔍 Automatische Tapo-Erkennung in separatem Thread gestartet")
|
||
|
||
def _run_auto_discovery(self):
|
||
"""
|
||
Führt die automatische Tapo-Erkennung in einem separaten Thread aus.
|
||
"""
|
||
try:
|
||
# Kurze Verzögerung um sicherzustellen, dass die Hauptanwendung Zeit hat zu starten
|
||
time.sleep(2)
|
||
self.auto_discover_tapo_outlets()
|
||
except Exception as e:
|
||
monitor_logger.error(f"❌ Fehler bei automatischer Tapo-Erkennung: {str(e)}")
|
||
|
||
def initialize_all_outlets_on_startup(self) -> Dict[str, bool]:
|
||
"""
|
||
Schaltet beim Programmstart alle gespeicherten Steckdosen aus (gleicher Startzustand).
|
||
|
||
Returns:
|
||
Dict[str, bool]: Ergebnis der Initialisierung pro Drucker
|
||
"""
|
||
if self.startup_initialized:
|
||
monitor_logger.info("🔄 Steckdosen bereits beim Start initialisiert")
|
||
return {}
|
||
|
||
monitor_logger.info("🚀 Starte Steckdosen-Initialisierung beim Programmstart...")
|
||
results = {}
|
||
|
||
try:
|
||
db_session = get_db_session()
|
||
printers = db_session.query(Printer).filter(Printer.active == True).all()
|
||
|
||
if not printers:
|
||
monitor_logger.warning("⚠️ Keine aktiven Drucker zur Initialisierung gefunden")
|
||
db_session.close()
|
||
self.startup_initialized = True
|
||
return results
|
||
|
||
# Alle Steckdosen ausschalten für einheitlichen Startzustand
|
||
for printer in printers:
|
||
try:
|
||
if printer.plug_ip and printer.plug_username and printer.plug_password:
|
||
success = self._turn_outlet_off(
|
||
printer.plug_ip,
|
||
printer.plug_username,
|
||
printer.plug_password,
|
||
printer_id=printer.id
|
||
)
|
||
|
||
results[printer.name] = success
|
||
|
||
if success:
|
||
monitor_logger.info(f"✅ {printer.name}: Steckdose ausgeschaltet")
|
||
# Status in Datenbank aktualisieren
|
||
printer.status = "offline"
|
||
printer.last_checked = datetime.now()
|
||
else:
|
||
monitor_logger.warning(f"❌ {printer.name}: Steckdose konnte nicht ausgeschaltet werden")
|
||
else:
|
||
monitor_logger.warning(f"⚠️ {printer.name}: Unvollständige Steckdosen-Konfiguration")
|
||
results[printer.name] = False
|
||
|
||
except Exception as e:
|
||
monitor_logger.error(f"❌ Fehler bei Initialisierung von {printer.name}: {str(e)}")
|
||
results[printer.name] = False
|
||
|
||
# Änderungen speichern
|
||
db_session.commit()
|
||
db_session.close()
|
||
|
||
success_count = sum(1 for success in results.values() if success)
|
||
total_count = len(results)
|
||
|
||
monitor_logger.info(f"🎯 Steckdosen-Initialisierung abgeschlossen: {success_count}/{total_count} erfolgreich")
|
||
self.startup_initialized = True
|
||
|
||
except Exception as e:
|
||
monitor_logger.error(f"❌ Kritischer Fehler bei Steckdosen-Initialisierung: {str(e)}")
|
||
results = {}
|
||
|
||
return results
|
||
|
||
def _turn_outlet_off(self, ip_address: str, username: str, password: str, timeout: int = 5, printer_id: int = None) -> bool:
|
||
"""
|
||
Schaltet eine TP-Link Tapo P110-Steckdose aus.
|
||
|
||
Args:
|
||
ip_address: IP-Adresse der Steckdose
|
||
username: Benutzername für die Steckdose (wird überschrieben)
|
||
password: Passwort für die Steckdose (wird überschrieben)
|
||
timeout: Timeout in Sekunden (wird ignoriert, da PyP100 eigenes Timeout hat)
|
||
printer_id: ID des zugehörigen Druckers (für Logging)
|
||
|
||
Returns:
|
||
bool: True wenn erfolgreich ausgeschaltet
|
||
"""
|
||
if not TAPO_AVAILABLE:
|
||
monitor_logger.error("⚠️ PyP100-Modul nicht verfügbar - kann Tapo-Steckdose nicht schalten")
|
||
# Logging: Fehlgeschlagener Versuch
|
||
if printer_id:
|
||
try:
|
||
PlugStatusLog.log_status_change(
|
||
printer_id=printer_id,
|
||
status="disconnected",
|
||
source="system",
|
||
ip_address=ip_address,
|
||
error_message="PyP100-Modul nicht verfügbar",
|
||
notes="Startup-Initialisierung fehlgeschlagen"
|
||
)
|
||
except Exception as log_error:
|
||
monitor_logger.warning(f"Fehler beim Loggen des Steckdosen-Status: {log_error}")
|
||
return False
|
||
|
||
# IMMER globale Anmeldedaten verwenden (da diese funktionieren)
|
||
username = TAPO_USERNAME
|
||
password = TAPO_PASSWORD
|
||
monitor_logger.debug(f"🔧 Verwende globale Tapo-Anmeldedaten für {ip_address}")
|
||
|
||
start_time = time.time()
|
||
|
||
try:
|
||
# TP-Link Tapo P100 Verbindung herstellen (P100 statt P110)
|
||
from PyP100 import PyP100
|
||
p100 = PyP100.P100(ip_address, username, password)
|
||
p100.handshake() # Authentifizierung
|
||
p100.login() # Login
|
||
|
||
# Steckdose ausschalten
|
||
p100.turnOff()
|
||
|
||
response_time = int((time.time() - start_time) * 1000) # in Millisekunden
|
||
monitor_logger.debug(f"✅ Tapo-Steckdose {ip_address} erfolgreich ausgeschaltet")
|
||
|
||
# Logging: Erfolgreich ausgeschaltet
|
||
if printer_id:
|
||
try:
|
||
PlugStatusLog.log_status_change(
|
||
printer_id=printer_id,
|
||
status="off",
|
||
source="system",
|
||
ip_address=ip_address,
|
||
response_time_ms=response_time,
|
||
notes="Startup-Initialisierung: Steckdose ausgeschaltet"
|
||
)
|
||
except Exception as log_error:
|
||
monitor_logger.warning(f"Fehler beim Loggen des Steckdosen-Status: {log_error}")
|
||
|
||
return True
|
||
|
||
except Exception as e:
|
||
response_time = int((time.time() - start_time) * 1000) # in Millisekunden
|
||
monitor_logger.debug(f"⚠️ Fehler beim Ausschalten der Tapo-Steckdose {ip_address}: {str(e)}")
|
||
|
||
# Logging: Fehlgeschlagener Versuch
|
||
if printer_id:
|
||
try:
|
||
PlugStatusLog.log_status_change(
|
||
printer_id=printer_id,
|
||
status="disconnected",
|
||
source="system",
|
||
ip_address=ip_address,
|
||
response_time_ms=response_time,
|
||
error_message=str(e),
|
||
notes="Startup-Initialisierung fehlgeschlagen"
|
||
)
|
||
except Exception as log_error:
|
||
monitor_logger.warning(f"Fehler beim Loggen des Steckdosen-Status: {log_error}")
|
||
|
||
return False
|
||
|
||
def get_live_printer_status(self, use_session_cache: bool = True) -> Dict[int, Dict]:
|
||
"""
|
||
Holt Live-Druckerstatus mit Session- und DB-Caching.
|
||
|
||
Args:
|
||
use_session_cache: Ob Session-Cache verwendet werden soll
|
||
|
||
Returns:
|
||
Dict[int, Dict]: Status-Dict mit Drucker-ID als Key
|
||
"""
|
||
current_time = datetime.now()
|
||
|
||
# Session-Cache prüfen (nur wenn aktiviert)
|
||
if use_session_cache and hasattr(session, 'get'):
|
||
session_key = "printer_status_cache"
|
||
session_timestamp_key = "printer_status_timestamp"
|
||
|
||
cached_data = session.get(session_key)
|
||
cached_timestamp = session.get(session_timestamp_key)
|
||
|
||
if cached_data and cached_timestamp:
|
||
cache_age = (current_time - datetime.fromisoformat(cached_timestamp)).total_seconds()
|
||
if cache_age < self.session_cache_ttl:
|
||
monitor_logger.debug("📋 Verwende Session-Cache für Druckerstatus")
|
||
return cached_data
|
||
|
||
# DB-Cache prüfen
|
||
with self.cache_lock:
|
||
if self.db_cache and (current_time - self.last_db_sync).total_seconds() < self.db_cache_ttl:
|
||
monitor_logger.debug("🗃️ Verwende DB-Cache für Druckerstatus")
|
||
|
||
# Session-Cache aktualisieren
|
||
if use_session_cache and hasattr(session, '__setitem__'):
|
||
session["printer_status_cache"] = self.db_cache
|
||
session["printer_status_timestamp"] = current_time.isoformat()
|
||
|
||
return self.db_cache
|
||
|
||
# Live-Status von Druckern abrufen
|
||
monitor_logger.info("🔄 Aktualisiere Live-Druckerstatus...")
|
||
status_dict = self._fetch_live_printer_status()
|
||
|
||
# Caches aktualisieren
|
||
with self.cache_lock:
|
||
self.db_cache = status_dict
|
||
self.last_db_sync = current_time
|
||
|
||
if use_session_cache and hasattr(session, '__setitem__'):
|
||
session["printer_status_cache"] = status_dict
|
||
session["printer_status_timestamp"] = current_time.isoformat()
|
||
|
||
return status_dict
|
||
|
||
def _fetch_live_printer_status(self) -> Dict[int, Dict]:
|
||
"""
|
||
Holt den aktuellen Status aller Drucker direkt von den Geräten.
|
||
|
||
Returns:
|
||
Dict[int, Dict]: Status-Dict mit umfassenden Informationen
|
||
"""
|
||
status_dict = {}
|
||
|
||
try:
|
||
db_session = get_db_session()
|
||
printers = db_session.query(Printer).filter(Printer.active == True).all()
|
||
|
||
# Wenn keine aktiven Drucker vorhanden sind, gebe leeres Dict zurück
|
||
if not printers:
|
||
monitor_logger.info("ℹ️ Keine aktiven Drucker gefunden")
|
||
db_session.close()
|
||
return status_dict
|
||
|
||
monitor_logger.info(f"🔍 Prüfe Status von {len(printers)} aktiven Druckern...")
|
||
|
||
# Parallel-Status-Prüfung mit ThreadPoolExecutor
|
||
from concurrent.futures import ThreadPoolExecutor, as_completed
|
||
|
||
# Sicherstellen, dass max_workers mindestens 1 ist
|
||
max_workers = min(max(len(printers), 1), 8)
|
||
|
||
with ThreadPoolExecutor(max_workers=max_workers) as executor:
|
||
future_to_printer = {
|
||
executor.submit(self._check_single_printer_status, printer): printer
|
||
for printer in printers
|
||
}
|
||
|
||
for future in as_completed(future_to_printer, timeout=15):
|
||
printer = future_to_printer[future]
|
||
try:
|
||
status_info = future.result()
|
||
status_dict[printer.id] = status_info
|
||
|
||
# Status in Datenbank aktualisieren
|
||
printer.status = status_info["status"]
|
||
printer.last_checked = datetime.now()
|
||
|
||
except Exception as e:
|
||
monitor_logger.error(f"❌ Fehler bei Status-Check für Drucker {printer.name}: {str(e)}")
|
||
status_dict[printer.id] = {
|
||
"id": printer.id,
|
||
"name": printer.name,
|
||
"status": "offline",
|
||
"active": False,
|
||
"ip_address": printer.ip_address,
|
||
"plug_ip": printer.plug_ip,
|
||
"location": printer.location,
|
||
"last_checked": datetime.now().isoformat(),
|
||
"error": str(e)
|
||
}
|
||
|
||
# Änderungen in Datenbank speichern
|
||
db_session.commit()
|
||
db_session.close()
|
||
|
||
monitor_logger.info(f"✅ Status-Update abgeschlossen für {len(status_dict)} Drucker")
|
||
|
||
except Exception as e:
|
||
monitor_logger.error(f"❌ Kritischer Fehler beim Abrufen des Live-Status: {str(e)}")
|
||
|
||
return status_dict
|
||
|
||
def _check_single_printer_status(self, printer: Printer, timeout: int = 7) -> Dict:
|
||
"""
|
||
Überprüft den Status eines einzelnen Druckers basierend auf der Steckdosen-Logik:
|
||
- Steckdose erreichbar aber AUS = Drucker ONLINE (bereit zum Drucken)
|
||
- Steckdose erreichbar und AN = Drucker PRINTING (druckt gerade)
|
||
- Steckdose nicht erreichbar = Drucker OFFLINE (kritischer Fehler)
|
||
|
||
Args:
|
||
printer: Printer-Objekt aus der Datenbank
|
||
timeout: Timeout in Sekunden
|
||
|
||
Returns:
|
||
Dict: Umfassende Status-Informationen
|
||
"""
|
||
status_info = {
|
||
"id": printer.id,
|
||
"name": printer.name,
|
||
"status": "offline",
|
||
"active": False,
|
||
"ip_address": printer.ip_address,
|
||
"plug_ip": printer.plug_ip,
|
||
"location": printer.location,
|
||
"last_checked": datetime.now().isoformat(),
|
||
"ping_successful": False,
|
||
"outlet_reachable": False,
|
||
"outlet_state": "unknown"
|
||
}
|
||
|
||
try:
|
||
# 1. Ping-Test für Grundkonnektivität
|
||
if printer.plug_ip:
|
||
ping_success = self._ping_address(printer.plug_ip, timeout=3)
|
||
status_info["ping_successful"] = ping_success
|
||
|
||
if ping_success:
|
||
# 2. Smart Plug Status prüfen
|
||
outlet_reachable, outlet_state = self._check_outlet_status(
|
||
printer.plug_ip,
|
||
printer.plug_username,
|
||
printer.plug_password,
|
||
timeout,
|
||
printer_id=printer.id
|
||
)
|
||
|
||
status_info["outlet_reachable"] = outlet_reachable
|
||
status_info["outlet_state"] = outlet_state
|
||
|
||
# 🎯 KORREKTE LOGIK: Steckdose erreichbar = Drucker funktionsfähig
|
||
if outlet_reachable:
|
||
if outlet_state == "off":
|
||
# Steckdose aus = Drucker ONLINE (bereit zum Drucken)
|
||
status_info["status"] = "online"
|
||
status_info["active"] = True
|
||
monitor_logger.debug(f"✅ {printer.name}: ONLINE (Steckdose aus - bereit zum Drucken)")
|
||
elif outlet_state == "on":
|
||
# Steckdose an = Drucker PRINTING (druckt gerade)
|
||
status_info["status"] = "printing"
|
||
status_info["active"] = True
|
||
monitor_logger.debug(f"🖨️ {printer.name}: PRINTING (Steckdose an - druckt gerade)")
|
||
else:
|
||
# Unbekannter Steckdosen-Status
|
||
status_info["status"] = "error"
|
||
status_info["active"] = False
|
||
monitor_logger.warning(f"⚠️ {printer.name}: Unbekannter Steckdosen-Status '{outlet_state}'")
|
||
else:
|
||
# Steckdose nicht erreichbar = kritischer Fehler
|
||
status_info["status"] = "offline"
|
||
status_info["active"] = False
|
||
monitor_logger.warning(f"❌ {printer.name}: OFFLINE (Steckdose nicht erreichbar)")
|
||
else:
|
||
# Ping fehlgeschlagen = Netzwerkproblem
|
||
status_info["status"] = "unreachable"
|
||
status_info["active"] = False
|
||
monitor_logger.warning(f"🔌 {printer.name}: UNREACHABLE (Ping fehlgeschlagen)")
|
||
else:
|
||
# Keine Steckdosen-IP konfiguriert
|
||
status_info["status"] = "unconfigured"
|
||
status_info["active"] = False
|
||
monitor_logger.info(f"⚙️ {printer.name}: UNCONFIGURED (keine Steckdosen-IP)")
|
||
|
||
except Exception as e:
|
||
monitor_logger.error(f"❌ Fehler bei Status-Check für {printer.name}: {str(e)}")
|
||
status_info["error"] = str(e)
|
||
status_info["status"] = "error"
|
||
status_info["active"] = False
|
||
|
||
return status_info
|
||
|
||
def _ping_address(self, ip_address: str, timeout: int = 3) -> bool:
|
||
"""
|
||
Führt einen Konnektivitätstest zu einer IP-Adresse durch.
|
||
Verwendet ausschließlich TCP-Verbindung statt Ping, um Encoding-Probleme zu vermeiden.
|
||
|
||
Args:
|
||
ip_address: Zu testende IP-Adresse
|
||
timeout: Timeout in Sekunden
|
||
|
||
Returns:
|
||
bool: True wenn Verbindung erfolgreich
|
||
"""
|
||
try:
|
||
# IP-Adresse validieren
|
||
ipaddress.ip_address(ip_address.strip())
|
||
|
||
import socket
|
||
|
||
# Erst Port 9999 versuchen (Tapo-Standard)
|
||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||
sock.settimeout(timeout)
|
||
result = sock.connect_ex((ip_address.strip(), 9999))
|
||
sock.close()
|
||
|
||
if result == 0:
|
||
return True
|
||
|
||
# Falls Port 9999 nicht erfolgreich, Port 80 versuchen (HTTP)
|
||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||
sock.settimeout(timeout)
|
||
result = sock.connect_ex((ip_address.strip(), 80))
|
||
sock.close()
|
||
|
||
if result == 0:
|
||
return True
|
||
|
||
# Falls Port 80 nicht erfolgreich, Port 443 versuchen (HTTPS)
|
||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||
sock.settimeout(timeout)
|
||
result = sock.connect_ex((ip_address.strip(), 443))
|
||
sock.close()
|
||
|
||
return result == 0
|
||
|
||
except Exception as e:
|
||
monitor_logger.debug(f"❌ Fehler beim Verbindungstest zu {ip_address}: {str(e)}")
|
||
return False
|
||
|
||
def _check_outlet_status(self, ip_address: str, username: str, password: str, timeout: int = 5, printer_id: int = None) -> Tuple[bool, str]:
|
||
"""
|
||
Ü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 (wird ignoriert, da PyP100 eigenes Timeout hat)
|
||
printer_id: ID des zugehörigen Druckers (für Logging)
|
||
|
||
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")
|
||
|
||
# Logging: Modul nicht verfügbar
|
||
if printer_id:
|
||
try:
|
||
PlugStatusLog.log_status_change(
|
||
printer_id=printer_id,
|
||
status="disconnected",
|
||
source="system",
|
||
ip_address=ip_address,
|
||
error_message="PyP100-Modul nicht verfügbar",
|
||
notes="Status-Check fehlgeschlagen"
|
||
)
|
||
except Exception as log_error:
|
||
monitor_logger.warning(f"Fehler beim Loggen des Steckdosen-Status: {log_error}")
|
||
|
||
return False, "unknown"
|
||
|
||
# IMMER globale Anmeldedaten verwenden (da diese funktionieren)
|
||
username = TAPO_USERNAME
|
||
password = TAPO_PASSWORD
|
||
monitor_logger.debug(f"🔧 Verwende globale Tapo-Anmeldedaten für {ip_address}")
|
||
|
||
start_time = time.time()
|
||
|
||
try:
|
||
# TP-Link Tapo P100 Verbindung herstellen (P100 statt P110)
|
||
from PyP100 import PyP100
|
||
p100 = PyP100.P100(ip_address, username, password)
|
||
p100.handshake() # Authentifizierung
|
||
p100.login() # Login
|
||
|
||
# Geräteinformationen abrufen
|
||
device_info = p100.getDeviceInfo()
|
||
|
||
# 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) # in Millisekunden
|
||
|
||
monitor_logger.debug(f"✅ Tapo-Steckdose {ip_address}: Status = {status}")
|
||
|
||
# Logging: Erfolgreicher Status-Check
|
||
if printer_id:
|
||
try:
|
||
# Hole zusätzliche Geräteinformationen falls verfügbar
|
||
power_consumption = None
|
||
voltage = None
|
||
current = None
|
||
firmware_version = None
|
||
|
||
try:
|
||
# Versuche Energiedaten zu holen (P110 spezifisch)
|
||
energy_usage = p100.getEnergyUsage()
|
||
if energy_usage:
|
||
power_consumption = energy_usage.get('current_power', None)
|
||
voltage = energy_usage.get('voltage', None)
|
||
current = energy_usage.get('current', None)
|
||
except:
|
||
pass # P100 unterstützt keine Energiedaten
|
||
|
||
try:
|
||
firmware_version = device_info.get('fw_ver', None)
|
||
except:
|
||
pass
|
||
|
||
PlugStatusLog.log_status_change(
|
||
printer_id=printer_id,
|
||
status=status,
|
||
source="system",
|
||
ip_address=ip_address,
|
||
power_consumption=power_consumption,
|
||
voltage=voltage,
|
||
current=current,
|
||
response_time_ms=response_time,
|
||
firmware_version=firmware_version,
|
||
notes="Automatischer Status-Check"
|
||
)
|
||
except Exception as log_error:
|
||
monitor_logger.warning(f"Fehler beim Loggen des Steckdosen-Status: {log_error}")
|
||
|
||
return True, status
|
||
|
||
except Exception as e:
|
||
response_time = int((time.time() - start_time) * 1000) # in Millisekunden
|
||
monitor_logger.debug(f"⚠️ Fehler bei Tapo-Steckdosen-Status-Check {ip_address}: {str(e)}")
|
||
|
||
# Logging: Fehlgeschlagener Status-Check
|
||
if printer_id:
|
||
try:
|
||
PlugStatusLog.log_status_change(
|
||
printer_id=printer_id,
|
||
status="disconnected",
|
||
source="system",
|
||
ip_address=ip_address,
|
||
response_time_ms=response_time,
|
||
error_message=str(e),
|
||
notes="Status-Check fehlgeschlagen"
|
||
)
|
||
except Exception as log_error:
|
||
monitor_logger.warning(f"Fehler beim Loggen des Steckdosen-Status: {log_error}")
|
||
|
||
return False, "unknown"
|
||
|
||
def clear_all_caches(self):
|
||
"""Löscht alle Caches (Session und DB)."""
|
||
with self.cache_lock:
|
||
self.db_cache = {}
|
||
self.last_db_sync = datetime.now()
|
||
|
||
if hasattr(session, 'pop'):
|
||
session.pop("printer_status_cache", None)
|
||
session.pop("printer_status_timestamp", None)
|
||
|
||
monitor_logger.info("🧹 Alle Drucker-Caches gelöscht")
|
||
|
||
def get_printer_summary(self) -> Dict[str, int]:
|
||
"""
|
||
Gibt eine Zusammenfassung der Druckerstatus zurück.
|
||
|
||
Returns:
|
||
Dict[str, int]: Anzahl Drucker pro Status
|
||
"""
|
||
status_dict = self.get_live_printer_status()
|
||
|
||
summary = {
|
||
"total": len(status_dict),
|
||
"online": 0,
|
||
"offline": 0,
|
||
"printing": 0, # Neuer Status: Drucker druckt gerade
|
||
"standby": 0,
|
||
"unreachable": 0,
|
||
"unconfigured": 0,
|
||
"error": 0 # Status für unbekannte Fehler
|
||
}
|
||
|
||
for printer_info in status_dict.values():
|
||
status = printer_info.get("status", "offline")
|
||
if status in summary:
|
||
summary[status] += 1
|
||
else:
|
||
# Fallback für unbekannte Status
|
||
summary["offline"] += 1
|
||
|
||
return summary
|
||
|
||
def auto_discover_tapo_outlets(self) -> Dict[str, bool]:
|
||
"""
|
||
Automatische Erkennung und Konfiguration von TP-Link Tapo P110-Steckdosen im Netzwerk.
|
||
Robuste Version mit Timeout-Behandlung und Fehler-Resilience.
|
||
|
||
Returns:
|
||
Dict[str, bool]: Ergebnis der Steckdosenerkennung mit IP als Schlüssel
|
||
"""
|
||
if self.auto_discovered_tapo:
|
||
monitor_logger.info("🔍 Tapo-Steckdosen wurden bereits erkannt")
|
||
return {}
|
||
|
||
monitor_logger.info("🔍 Starte automatische Tapo-Steckdosenerkennung...")
|
||
results = {}
|
||
start_time = time.time()
|
||
|
||
# 1. Zuerst die Standard-IPs aus der Konfiguration testen
|
||
monitor_logger.info(f"🔄 Teste {len(DEFAULT_TAPO_IPS)} Standard-IPs aus der Konfiguration")
|
||
|
||
for i, ip in enumerate(DEFAULT_TAPO_IPS):
|
||
try:
|
||
# Fortschrittsmeldung
|
||
monitor_logger.info(f"🔍 Teste IP {i+1}/{len(DEFAULT_TAPO_IPS)}: {ip}")
|
||
|
||
# Reduzierte Timeouts für schnellere Erkennung
|
||
ping_success = self._ping_address(ip, timeout=2)
|
||
|
||
if ping_success:
|
||
monitor_logger.info(f"✅ Steckdose mit IP {ip} ist erreichbar")
|
||
|
||
# Tapo-Verbindung testen mit Timeout-Schutz
|
||
if TAPO_AVAILABLE:
|
||
try:
|
||
# Timeout für Tapo-Verbindung
|
||
import signal
|
||
|
||
def timeout_handler(signum, frame):
|
||
raise TimeoutError("Tapo-Verbindung Timeout")
|
||
|
||
# Nur unter Unix/Linux verfügbar
|
||
if hasattr(signal, 'SIGALRM'):
|
||
signal.signal(signal.SIGALRM, timeout_handler)
|
||
signal.alarm(5) # 5 Sekunden Timeout
|
||
|
||
try:
|
||
from PyP100 import PyP100
|
||
p100 = PyP100.P100(ip, TAPO_USERNAME, TAPO_PASSWORD)
|
||
p100.handshake()
|
||
p100.login()
|
||
device_info = p100.getDeviceInfo()
|
||
|
||
# Timeout zurücksetzen
|
||
if hasattr(signal, 'SIGALRM'):
|
||
signal.alarm(0)
|
||
|
||
# Steckdose gefunden und verbunden
|
||
nickname = device_info.get('nickname', f"Tapo P110 ({ip})")
|
||
state = "on" if device_info.get('device_on', False) else "off"
|
||
|
||
monitor_logger.info(f"✅ Tapo-Steckdose '{nickname}' ({ip}) gefunden - Status: {state}")
|
||
results[ip] = True
|
||
|
||
# Steckdose in Datenbank speichern/aktualisieren (nicht-blockierend)
|
||
try:
|
||
self._ensure_tapo_in_database(ip, nickname)
|
||
except Exception as db_error:
|
||
monitor_logger.warning(f"⚠️ Fehler beim Speichern in DB für {ip}: {str(db_error)}")
|
||
|
||
except (TimeoutError, Exception) as tapo_error:
|
||
if hasattr(signal, 'SIGALRM'):
|
||
signal.alarm(0) # Timeout zurücksetzen
|
||
monitor_logger.debug(f"❌ IP {ip} ist erreichbar, aber keine Tapo-Steckdose oder Timeout: {str(tapo_error)}")
|
||
results[ip] = False
|
||
|
||
except Exception as outer_error:
|
||
monitor_logger.debug(f"❌ Fehler bei Tapo-Test für {ip}: {str(outer_error)}")
|
||
results[ip] = False
|
||
else:
|
||
monitor_logger.warning("⚠️ PyP100-Modul nicht verfügbar - kann Tapo-Verbindung nicht testen")
|
||
results[ip] = False
|
||
else:
|
||
monitor_logger.debug(f"❌ IP {ip} nicht erreichbar")
|
||
results[ip] = False
|
||
|
||
except Exception as e:
|
||
monitor_logger.warning(f"❌ Fehler bei Steckdosen-Erkennung für IP {ip}: {str(e)}")
|
||
results[ip] = False
|
||
# Weiter mit nächster IP - nicht abbrechen
|
||
continue
|
||
|
||
# Erfolgsstatistik berechnen
|
||
success_count = sum(1 for success in results.values() if success)
|
||
elapsed_time = time.time() - start_time
|
||
|
||
monitor_logger.info(f"✅ Steckdosen-Erkennung abgeschlossen: {success_count}/{len(results)} Steckdosen gefunden in {elapsed_time:.1f}s")
|
||
|
||
# Markieren, dass automatische Erkennung durchgeführt wurde
|
||
self.auto_discovered_tapo = True
|
||
|
||
return results
|
||
|
||
def _ensure_tapo_in_database(self, ip_address: str, nickname: str = None) -> bool:
|
||
"""
|
||
Stellt sicher, dass eine erkannte Tapo-Steckdose in der Datenbank existiert.
|
||
|
||
Args:
|
||
ip_address: IP-Adresse der Steckdose
|
||
nickname: Name der Steckdose (optional)
|
||
|
||
Returns:
|
||
bool: True wenn erfolgreich in Datenbank gespeichert/aktualisiert
|
||
"""
|
||
try:
|
||
db_session = get_db_session()
|
||
|
||
# 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, falls nötig
|
||
if not existing_printer.plug_username or not existing_printer.plug_password:
|
||
existing_printer.plug_username = TAPO_USERNAME
|
||
existing_printer.plug_password = TAPO_PASSWORD
|
||
monitor_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
|
||
monitor_logger.info(f"✅ Drucker {old_name} umbenannt zu {nickname}")
|
||
|
||
# Drucker als aktiv markieren, da Tapo-Steckdose gefunden wurde
|
||
if not existing_printer.active:
|
||
existing_printer.active = True
|
||
monitor_logger.info(f"✅ Drucker {existing_printer.name} als aktiv markiert")
|
||
|
||
# Status aktualisieren
|
||
existing_printer.last_checked = datetime.now()
|
||
db_session.commit()
|
||
db_session.close()
|
||
return True
|
||
else:
|
||
# Neuen Drucker erstellen, falls keiner existiert
|
||
printer_name = nickname or f"Tapo P110 ({ip_address})"
|
||
mac_address = f"tapo:{ip_address.replace('.', '-')}" # Pseudo-MAC-Adresse
|
||
|
||
new_printer = Printer(
|
||
name=printer_name,
|
||
model="TP-Link Tapo P110",
|
||
location="Automatisch erkannt",
|
||
ip_address=ip_address, # Drucker-IP setzen wir gleich Steckdosen-IP
|
||
mac_address=mac_address,
|
||
plug_ip=ip_address,
|
||
plug_username=TAPO_USERNAME,
|
||
plug_password=TAPO_PASSWORD,
|
||
status="offline",
|
||
active=True,
|
||
last_checked=datetime.now()
|
||
)
|
||
|
||
db_session.add(new_printer)
|
||
db_session.commit()
|
||
monitor_logger.info(f"✅ Neuer Drucker '{printer_name}' mit Tapo-Steckdose {ip_address} erstellt")
|
||
db_session.close()
|
||
return True
|
||
|
||
except Exception as e:
|
||
monitor_logger.error(f"❌ Fehler beim Speichern der Tapo-Steckdose {ip_address}: {str(e)}")
|
||
try:
|
||
db_session.rollback()
|
||
db_session.close()
|
||
except:
|
||
pass
|
||
return False
|
||
|
||
# Globale Instanz
|
||
printer_monitor = PrinterMonitor() |