423 lines
17 KiB
Python
423 lines
17 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 utils.settings import PRINTERS, TAPO_USERNAME, TAPO_PASSWORD, DEFAULT_TAPO_IPS, TAPO_AUTO_DISCOVERY
|
||
from utils.tapo_controller import tapo_controller
|
||
|
||
# TP-Link Tapo P110 Unterstützung prüfen
|
||
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 {}
|
||
|
||
# Verwende zentrale tapo_controller Implementierung
|
||
results = tapo_controller.initialize_all_outlets()
|
||
self.startup_initialized = True
|
||
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
|
||
"""
|
||
# Verwende zentrale tapo_controller Implementierung
|
||
return tapo_controller.turn_off(ip_address, username, password, printer_id)
|
||
|
||
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
|
||
"""
|
||
# Verwende zentrale tapo_controller Implementierung
|
||
return tapo_controller.ping_address(ip_address, timeout)
|
||
|
||
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"
|
||
"""
|
||
# Verwende zentrale tapo_controller Implementierung
|
||
return tapo_controller.check_outlet_status(ip_address, username, password, printer_id)
|
||
|
||
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 {}
|
||
|
||
# Verwende zentrale tapo_controller Implementierung
|
||
results = tapo_controller.auto_discover_outlets()
|
||
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
|
||
"""
|
||
# Verwende zentrale tapo_controller Implementierung
|
||
return tapo_controller._ensure_outlet_in_database(ip_address, nickname)
|
||
|
||
# Globale Instanz
|
||
printer_monitor = PrinterMonitor() |