🎉 Feature: Enhanced Admin Guest Requests API & Startup Initialization Documentation 📚

This commit is contained in:
Till Tomczak
2025-06-20 08:36:07 +02:00
parent 939f14199d
commit cbea4cb765
27 changed files with 1653 additions and 52 deletions

View File

@ -33,6 +33,7 @@ except ImportError:
# MYP Models & Utils
from models import get_db_session, Printer, PlugStatusLog
from utils.logging_config import get_logger
import os
# Logger
hardware_logger = get_logger("hardware_integration")
@ -353,27 +354,55 @@ class DruckerSteuerung:
hardware_logger.warning(f"⚠️ Simulation: Steckdose {ip} würde {'eingeschaltet' if einschalten else 'ausgeschaltet'}")
return True # Simulation immer erfolgreich
try:
action = "einschalten" if einschalten else "ausschalten"
hardware_logger.debug(f"🔌 Versuche Steckdose {ip} zu {action}")
# P100-Verbindung herstellen
p100 = PyP100(ip, self.tapo_username, self.tapo_password)
p100.handshake()
p100.login()
# Schalten
if einschalten:
p100.turnOn()
else:
p100.turnOff()
hardware_logger.info(f"✅ Steckdose {ip} erfolgreich {action}")
return True
except Exception as e:
hardware_logger.error(f"❌ Fehler beim Schalten der Steckdose {ip}: {e}")
# Zuerst Netzwerk-Erreichbarkeit prüfen
if not self._erweiterte_netzwerk_prüfung(ip):
hardware_logger.error(f" Steckdose {ip} ist im Netzwerk nicht erreichbar")
return False
retry_count = 0
max_retries = 3
while retry_count < max_retries:
try:
action = "einschalten" if einschalten else "ausschalten"
hardware_logger.debug(f"🔌 Versuche Steckdose {ip} zu {action} (Versuch {retry_count + 1}/{max_retries})")
# P100-Verbindung herstellen mit Timeout
p100 = PyP100(ip, self.tapo_username, self.tapo_password)
p100.handshake()
p100.login()
# Schalten
if einschalten:
p100.turnOn()
else:
p100.turnOff()
hardware_logger.info(f"✅ Steckdose {ip} erfolgreich {action}")
return True
except Exception as e:
retry_count += 1
error_msg = str(e)
# Spezifische Fehlerbehandlung
if "Connection refused" in error_msg:
hardware_logger.error(f"❌ Verbindung zu {ip} verweigert - Steckdose antwortet nicht auf Port 80")
elif "timeout" in error_msg.lower():
hardware_logger.error(f"❌ Zeitüberschreitung bei Verbindung zu {ip}")
elif "handshake" in error_msg.lower():
hardware_logger.error(f"❌ Tapo-Handshake fehlgeschlagen für {ip} - Möglicherweise falsche Credentials")
elif "login" in error_msg.lower():
hardware_logger.error(f"❌ Tapo-Login fehlgeschlagen für {ip} - Benutzername/Passwort prüfen")
else:
hardware_logger.error(f"❌ Fehler beim Schalten der Steckdose {ip}: {e}")
if retry_count < max_retries:
hardware_logger.info(f"🔄 Warte 2 Sekunden vor erneutem Versuch...")
time.sleep(2)
hardware_logger.error(f"❌ Alle {max_retries} Versuche für Steckdose {ip} fehlgeschlagen")
return False
def _drucker_status_pruefen(self, drucker: Printer) -> str:
"""Prüft den aktuellen Status eines Druckers"""
@ -397,6 +426,51 @@ class DruckerSteuerung:
except:
return False
def _erweiterte_netzwerk_prüfung(self, ip: str) -> bool:
"""
Erweiterte Netzwerk-Prüfung mit mehreren Tests.
Args:
ip: IP-Adresse zum Prüfen
Returns:
bool: True wenn erreichbar
"""
hardware_logger.debug(f"🔍 Erweiterte Netzwerk-Prüfung für {ip}")
# Test 1: Port 80 (HTTP)
if self._ping_test(ip, timeout=2):
hardware_logger.debug(f"{ip} auf Port 80 erreichbar")
return True
# Test 2: Port 9999 (Tapo-spezifisch für manche Modelle)
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(2)
result = sock.connect_ex((ip, 9999))
sock.close()
if result == 0:
hardware_logger.debug(f"{ip} auf Port 9999 erreichbar")
return True
except:
pass
# Test 3: ICMP Ping (falls verfügbar)
try:
import subprocess
# Windows und Linux kompatibel
param = '-n' if os.name == 'nt' else '-c'
command = ['ping', param, '1', '-w', '2000', ip]
result = subprocess.run(command, capture_output=True, text=True, timeout=3)
if result.returncode == 0:
hardware_logger.debug(f"{ip} via ICMP Ping erreichbar")
return True
except:
pass
hardware_logger.warning(f"⚠️ {ip} ist über keine Methode erreichbar")
return False
def _status_log_erstellen(self, drucker_id: int, action: str, grund: str):
"""Erstellt einen Eintrag im Status-Log"""
try:
@ -443,33 +517,171 @@ class DruckerSteuerung:
# Legacy-Format: (reachable, status)
return (True, 'online')
# Zuerst Netzwerk-Erreichbarkeit prüfen
if not self._erweiterte_netzwerk_prüfung(ip):
hardware_logger.warning(f"⚠️ Steckdose {ip} ist im Netzwerk nicht erreichbar")
return (False, 'unreachable')
retry_count = 0
max_retries = 2
while retry_count < max_retries:
try:
# Tapo P100/P110 Verbindung
p100 = PyP100(ip, self.tapo_username, self.tapo_password)
p100.handshake()
p100.login()
# Device Info abrufen
device_info = p100.getDeviceInfo()
if device_info and 'error_code' in device_info:
if device_info['error_code'] == 0:
device_on = device_info.get('result', {}).get('device_on', False)
hardware_logger.debug(f"✅ Steckdose {ip}: {'EIN' if device_on else 'AUS'}")
# Legacy-Format: (reachable, status)
return (True, 'on' if device_on else 'off')
else:
hardware_logger.warning(f"⚠️ Steckdose {ip} Error Code: {device_info['error_code']}")
return (False, 'error')
else:
hardware_logger.error(f"❌ Steckdose {ip}: Keine gültige Antwort")
return (False, 'unreachable')
except Exception as e:
retry_count += 1
error_msg = str(e)
if "Connection refused" in error_msg:
hardware_logger.error(f"❌ Verbindung zu {ip} verweigert")
elif "timeout" in error_msg.lower():
hardware_logger.error(f"❌ Zeitüberschreitung bei {ip}")
elif "handshake" in error_msg.lower():
hardware_logger.error(f"❌ Handshake-Fehler bei {ip}")
else:
hardware_logger.error(f"❌ Fehler beim Prüfen von Steckdose {ip}: {e}")
if retry_count < max_retries:
time.sleep(1)
return (False, 'unreachable')
def ping_address(self, ip: str, timeout: int = 5) -> bool:
"""
Prüft die Netzwerk-Erreichbarkeit einer IP-Adresse.
Args:
ip: IP-Adresse zum Testen
timeout: Timeout in Sekunden
Returns:
bool: True wenn erreichbar, False sonst
"""
hardware_logger.debug(f"📡 Teste Netzwerk-Erreichbarkeit: {ip}")
try:
# Tapo P100/P110 Verbindung
# Socket-basierter Ping-Test auf Port 80 (HTTP)
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
result = sock.connect_ex((ip, 80))
sock.close()
is_reachable = (result == 0)
hardware_logger.debug(f"📡 {ip}: {'✅ erreichbar' if is_reachable else '❌ nicht erreichbar'}")
return is_reachable
except Exception as e:
hardware_logger.debug(f"❌ Ping-Test für {ip} fehlgeschlagen: {e}")
return False
def turn_off(self, ip: str, username: str = None, password: str = None, printer_id: int = None) -> bool:
"""
Schaltet eine Tapo-Steckdose aus.
Args:
ip: IP-Adresse der Steckdose
username: Benutzername (wird ignoriert, verwendet interne Credentials)
password: Passwort (wird ignoriert, verwendet interne Credentials)
printer_id: Optional - ID des Druckers für Logging
Returns:
bool: True wenn erfolgreich ausgeschaltet
"""
hardware_logger.debug(f"🔴 Schalte Steckdose aus: {ip}" + (f" (Drucker ID: {printer_id})" if printer_id else ""))
if not TAPO_AVAILABLE:
hardware_logger.info(f"🔄 SIMULATION: Steckdose {ip} ausgeschaltet")
return True
try:
# P100-Verbindung mit internen Credentials
p100 = PyP100(ip, self.tapo_username, self.tapo_password)
p100.handshake()
p100.login()
# Device Info abrufen
device_info = p100.getDeviceInfo()
# Steckdose ausschalten
p100.turnOff()
hardware_logger.info(f"✅ Steckdose {ip} erfolgreich ausgeschaltet")
# Status-Log erstellen falls Drucker-ID verfügbar
if printer_id:
try:
self._status_log_erstellen(printer_id, 'turned_off', 'Startup-Initialisierung')
except:
pass # Fehler beim Logging nicht kritisch
return True
if device_info and 'error_code' in device_info:
if device_info['error_code'] == 0:
device_on = device_info.get('result', {}).get('device_on', False)
hardware_logger.debug(f"✅ Steckdose {ip}: {'EIN' if device_on else 'AUS'}")
# Legacy-Format: (reachable, status)
return (True, 'online' if device_on else 'offline')
else:
hardware_logger.warning(f"⚠️ Steckdose {ip} Error Code: {device_info['error_code']}")
return (False, 'error')
else:
hardware_logger.error(f"❌ Steckdose {ip}: Keine gültige Antwort")
return (False, 'unreachable')
except Exception as e:
hardware_logger.error(f"❌ Fehler beim Prüfen von Steckdose {ip}: {e}")
return (False, 'unreachable')
hardware_logger.error(f"❌ Fehler beim Ausschalten der Steckdose {ip}: {e}")
return False
def turn_on(self, ip: str, username: str = None, password: str = None, printer_id: int = None) -> bool:
"""
Schaltet eine Tapo-Steckdose ein.
Args:
ip: IP-Adresse der Steckdose
username: Benutzername (wird ignoriert, verwendet interne Credentials)
password: Passwort (wird ignoriert, verwendet interne Credentials)
printer_id: Optional - ID des Druckers für Logging
Returns:
bool: True wenn erfolgreich eingeschaltet
"""
hardware_logger.debug(f"🟢 Schalte Steckdose ein: {ip}" + (f" (Drucker ID: {printer_id})" if printer_id else ""))
if not TAPO_AVAILABLE:
hardware_logger.info(f"🔄 SIMULATION: Steckdose {ip} eingeschaltet")
return True
try:
# P100-Verbindung mit internen Credentials
p100 = PyP100(ip, self.tapo_username, self.tapo_password)
p100.handshake()
p100.login()
# Steckdose einschalten
p100.turnOn()
hardware_logger.info(f"✅ Steckdose {ip} erfolgreich eingeschaltet")
# Status-Log erstellen falls Drucker-ID verfügbar
if printer_id:
try:
self._status_log_erstellen(printer_id, 'turned_on', 'Manuell')
except:
pass # Fehler beim Logging nicht kritisch
return True
except Exception as e:
hardware_logger.error(f"❌ Fehler beim Einschalten der Steckdose {ip}: {e}")
return False
# ===== GLOBALE INSTANZ =====

View File

@ -39,6 +39,7 @@ class BackgroundTaskScheduler:
self._running = False
self._start_time: Optional[datetime] = None
self.logger = get_scheduler_logger()
self._outlets_initialized = False # Flag für einmalige Initialisierung
def register_task(self,
task_id: str,
@ -713,6 +714,188 @@ class BackgroundTaskScheduler:
db_session.rollback()
db_session.close()
def initialize_all_outlets_on_startup(self) -> Dict[str, bool]:
"""
Initialisiert alle konfigurierten Steckdosen beim Systemstart.
Schaltet alle im Netzwerk erreichbaren Tapo-Steckdosen aus, um einen
einheitlichen Startzustand (aus = frei) zu gewährleisten.
Returns:
Dict[str, bool]: Ergebnis der Initialisierung pro Drucker
"""
if self._outlets_initialized:
self.logger.info("🔄 Steckdosen bereits initialisiert - überspringe")
return {}
self.logger.info("🚀 Starte Steckdosen-Initialisierung beim Systemstart...")
results = {}
success_count = 0
total_count = 0
unreachable_count = 0
try:
db_session = get_db_session()
# Alle aktiven Drucker mit Steckdosen-Konfiguration laden
printers = db_session.query(Printer).filter(
Printer.active == True,
Printer.plug_ip.isnot(None)
).all()
if not printers:
self.logger.warning("⚠️ Keine aktiven Drucker mit Steckdosen-Konfiguration gefunden")
db_session.close()
return results
total_count = len(printers)
self.logger.info(f"🔍 Prüfe {total_count} konfigurierte Steckdosen...")
# Tapo-Controller für die Operationen verwenden
tapo_controller = get_tapo_controller()
# Jede Steckdose einzeln verarbeiten
for printer in printers:
printer_name = printer.name
plug_ip = printer.plug_ip
try:
self.logger.debug(f"🔌 Verarbeite {printer_name} ({plug_ip})...")
# 1. Netzwerk-Erreichbarkeit prüfen
is_reachable = tapo_controller.ping_address(plug_ip, timeout=3)
if not is_reachable:
self.logger.warning(f"📡 {printer_name}: Steckdose {plug_ip} nicht erreichbar")
results[printer_name] = {
'success': False,
'reason': 'nicht_erreichbar',
'ip': plug_ip
}
unreachable_count += 1
continue
# 2. Aktuellen Status prüfen
reachable, current_status = tapo_controller.check_outlet_status(
plug_ip,
printer_id=printer.id,
debug=True
)
if not reachable:
self.logger.warning(f"🔗 {printer_name}: Tapo-Verbindung fehlgeschlagen")
results[printer_name] = {
'success': False,
'reason': 'verbindung_fehlgeschlagen',
'ip': plug_ip
}
unreachable_count += 1
continue
# 3. Steckdose ausschalten (nur wenn nötig)
if current_status == "on":
self.logger.info(f"🔄 {printer_name}: Schalte Steckdose von 'an' auf 'aus' um...")
success = tapo_controller.turn_off(
plug_ip,
printer_id=printer.id
)
if success:
self.logger.info(f"{printer_name}: Erfolgreich ausgeschaltet")
# Drucker-Status in Datenbank aktualisieren
printer.status = "offline"
printer.last_checked = datetime.now()
results[printer_name] = {
'success': True,
'action': 'ausgeschaltet',
'previous_status': 'an',
'ip': plug_ip
}
success_count += 1
else:
self.logger.error(f"{printer_name}: Ausschalten fehlgeschlagen")
results[printer_name] = {
'success': False,
'reason': 'ausschalten_fehlgeschlagen',
'ip': plug_ip
}
elif current_status == "off":
self.logger.info(f"{printer_name}: Bereits ausgeschaltet - keine Aktion nötig")
# Status in Datenbank aktualisieren
printer.status = "offline"
printer.last_checked = datetime.now()
results[printer_name] = {
'success': True,
'action': 'bereits_aus',
'previous_status': 'aus',
'ip': plug_ip
}
success_count += 1
else:
self.logger.warning(f"⚠️ {printer_name}: Unbekannter Status '{current_status}'")
results[printer_name] = {
'success': False,
'reason': 'unbekannter_status',
'status': current_status,
'ip': plug_ip
}
except Exception as e:
self.logger.error(f"{printer_name}: Fehler bei Initialisierung - {str(e)}")
results[printer_name] = {
'success': False,
'reason': 'ausnahme',
'error': str(e),
'ip': plug_ip
}
# Änderungen in der Datenbank speichern
try:
db_session.commit()
self.logger.debug("💾 Datenbank-Änderungen gespeichert")
except Exception as e:
self.logger.error(f"❌ Fehler beim Speichern der Datenbank-Änderungen: {str(e)}")
db_session.rollback()
db_session.close()
# Zusammenfassung loggen
self.logger.info("=" * 60)
self.logger.info("🎯 STECKDOSEN-INITIALISIERUNG ABGESCHLOSSEN")
self.logger.info(f"📊 Gesamt: {total_count} Steckdosen")
self.logger.info(f"✅ Erfolgreich: {success_count}")
self.logger.info(f"📡 Nicht erreichbar: {unreachable_count}")
self.logger.info(f"❌ Fehlgeschlagen: {total_count - success_count - unreachable_count}")
if success_count == total_count:
self.logger.info("🌟 ALLE Steckdosen erfolgreich initialisiert!")
elif success_count > 0:
self.logger.info(f"{success_count}/{total_count} Steckdosen erfolgreich initialisiert")
else:
self.logger.warning("⚠️ KEINE Steckdose konnte initialisiert werden!")
self.logger.info("=" * 60)
# Flag setzen um Mehrfach-Initialisierung zu verhindern
self._outlets_initialized = True
except Exception as e:
self.logger.error(f"❌ Kritischer Fehler bei Steckdosen-Initialisierung: {str(e)}")
try:
db_session.rollback()
db_session.close()
except:
pass
return results
# Scheduler-Instanz erzeugen
scheduler = BackgroundTaskScheduler()