📚 Improved documentation for TAPO issue resolution in backend/TAPO_PROBLEMBEHEBUNG.md

This commit is contained in:
2025-06-18 06:53:06 +02:00
parent a44b1da2e6
commit f06c882c5a
9 changed files with 1205 additions and 91 deletions

View File

@@ -23,14 +23,30 @@ import time
import socket
import threading
import ipaddress
import requests
import subprocess
from datetime import datetime, timedelta
from typing import Dict, List, Any, Optional, Tuple
from concurrent.futures import ThreadPoolExecutor, as_completed
from flask import session
from sqlalchemy import func
from sqlalchemy.orm import Session
# Optional Imports mit Fallback
try:
import requests
REQUESTS_AVAILABLE = True
except ImportError:
REQUESTS_AVAILABLE = False
try:
from flask import session
FLASK_AVAILABLE = True
except ImportError:
FLASK_AVAILABLE = False
try:
from sqlalchemy import func
from sqlalchemy.orm import Session
SQLALCHEMY_AVAILABLE = True
except ImportError:
SQLALCHEMY_AVAILABLE = False
# MYP Models & Utils
from models import get_db_session, Printer, PlugStatusLog
@@ -269,11 +285,17 @@ class TapoController:
Tuple[bool, str]: (erreichbar, status) - status: "on", "off", "unknown"
"""
if not TAPO_AVAILABLE:
tapo_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")
return False, "unknown"
if debug:
tapo_logger.warning("⚠️ PyP100-modul nicht verfügbar - verwende Fallback-Netzwerktest")
# Fallback: Einfacher Ping-Test
ping_reachable = self.ping_address(ip, timeout=3)
if ping_reachable:
tapo_logger.debug(f"📡 Fallback: {ip} ist über Netzwerk erreichbar, aber Status unbekannt")
return True, "unknown"
else:
tapo_logger.debug(f"❌ Fallback: {ip} ist nicht erreichbar")
return False, "unreachable"
# Immer globale Anmeldedaten verwenden
username = self.username
@@ -315,7 +337,7 @@ class TapoController:
tapo_logger.info(f"✅ Tapo-Steckdose {ip}: Status = {status}")
# Erweiterte Informationen sammeln
extra_info = self._collect_device_info(p100, device_info, debug=debug)
extra_info = self._collect_device_info(p100, device_info, debug)
if debug and extra_info:
tapo_logger.debug(f"🔋 Zusätzliche Informationen für {ip}: {extra_info}")
@@ -405,8 +427,8 @@ class TapoController:
def ping_address(self, ip: str, timeout: int = 5) -> bool:
"""
Führt einen Konnektivitätstest zu einer IP-Adresse durch
Verwendet TCP-Verbindung statt Ping für bessere Kompatibilität
Führt einen erweiterten Konnektivitätstest zu einer IP-Adresse durch
Verwendet TCP-Verbindung und ICMP-Ping für maximale Kompatibilität
Args:
ip: Zu testende IP-Adresse
@@ -419,24 +441,57 @@ class TapoController:
# IP-Adresse validieren
ipaddress.ip_address(ip.strip())
# Standard-Ports für Tapo-Steckdosen testen
test_ports = [9999, 80, 443] # Tapo-Standard, HTTP, HTTPS
# 1. ICMP-Ping versuchen
try:
import subprocess
result = subprocess.run(
['ping', '-c', '1', '-W', str(timeout), ip.strip()],
capture_output=True,
timeout=timeout + 2
)
if result.returncode == 0:
tapo_logger.debug(f"✅ ICMP-Ping zu {ip} erfolgreich")
return True
except (subprocess.TimeoutExpired, FileNotFoundError, Exception) as e:
tapo_logger.debug(f"⚠️ ICMP-Ping zu {ip} fehlgeschlagen: {e}")
# 2. TCP-Port-Tests für Tapo-Steckdosen
test_ports = [9999, 80, 443, 22, 23] # Tapo-Standard, HTTP, HTTPS, SSH, Telnet
for port in test_ports:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
result = sock.connect_ex((ip.strip(), port))
sock.close()
if result == 0:
tapo_logger.debug(f"✅ verbindung zu {ip}:{port} erfolgreich")
return True
try:
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
sock.settimeout(timeout)
result = sock.connect_ex((ip.strip(), port))
sock.close()
if result == 0:
tapo_logger.debug(f"✅ TCP-Verbindung zu {ip}:{port} erfolgreich")
return True
except Exception as e:
tapo_logger.debug(f"⚠️ TCP-Test zu {ip}:{port} fehlgeschlagen: {e}")
continue
tapo_logger.debug(f"❌ keine verbindung zu {ip} auf standard-ports möglich")
# 3. Erweiterte Netzwerk-Tests
try:
# ARP-Test (falls möglich)
import subprocess
arp_result = subprocess.run(
['ping', '-c', '1', '-W', '1', ip.strip()],
capture_output=True,
timeout=3
)
if arp_result.returncode == 0:
tapo_logger.debug(f"✅ Erweiterte Netzwerkerreichbarkeit für {ip} bestätigt")
return True
except Exception as e:
tapo_logger.debug(f"⚠️ Erweiterter Netzwerktest für {ip} fehlgeschlagen: {e}")
tapo_logger.debug(f"❌ Alle Konnektivitätstests zu {ip} fehlgeschlagen")
return False
except Exception as e:
tapo_logger.debug(f"fehler beim verbindungstest zu {ip}: {str(e)}")
tapo_logger.debug(f"Kritischer Fehler beim Konnektivitätstest zu {ip}: {str(e)}")
return False
def auto_discover_outlets(self) -> Dict[str, bool]:
@@ -626,13 +681,14 @@ class TapoController:
return status_dict
def _collect_device_info(self, p100: 'PyP100.P100', device_info: dict, debug: bool = False) -> dict:
def _collect_device_info(self, p100, device_info, debug: bool = False) -> dict:
"""
Sammelt erweiterte Geräteinformationen von der Tapo-Steckdose
Args:
p100: P100-Instanz
device_info: Basis-Geräteinformationen
debug: Debug-Modus aktivieren
Returns:
Dict: Erweiterte Informationen
@@ -771,62 +827,6 @@ class TapoController:
tapo_logger.error(f"❌ Fehler beim Speichern der Steckdose {ip_address} in Datenbank: {str(e)}")
return False
def _collect_device_info(self, p110, device_info):
"""
Sammelt erweiterte Geräteinformationen einschließlich Energiedaten.
Args:
p110: PyP110 Instanz
device_info: Basis-Geräteinformationen
Returns:
Dict: Erweiterte Geräteinformationen
"""
extra_info = {}
try:
# Firmware-Version extrahieren
if 'fw_ver' in device_info.get('result', {}):
extra_info['firmware_version'] = device_info['result']['fw_ver']
# Energiedaten abrufen (nur für P110)
if 'P110' in device_info.get('result', {}).get('model', ''):
try:
energy_usage = p110.getEnergyUsage()
if energy_usage and 'result' in energy_usage:
energy_data = energy_usage['result']
# Aktuelle Leistungsdaten
extra_info['current_power'] = energy_data.get('current_power', 0) / 1000 # mW zu W
extra_info['power_consumption'] = extra_info['current_power']
# Historische Energiedaten
extra_info['today_energy'] = energy_data.get('today_energy', 0)
extra_info['month_energy'] = energy_data.get('month_energy', 0)
extra_info['today_runtime'] = energy_data.get('today_runtime', 0)
extra_info['month_runtime'] = energy_data.get('month_runtime', 0)
# 24h Verbrauchsdaten
extra_info['past24h'] = energy_data.get('past24h', [])
extra_info['past30d'] = energy_data.get('past30d', [])
extra_info['past1y'] = energy_data.get('past1y', [])
# Zusätzliche Metriken
if 'voltage' in energy_data:
extra_info['voltage'] = energy_data['voltage'] / 1000 # mV zu V
if 'current' in energy_data:
extra_info['current'] = energy_data['current'] / 1000 # mA zu A
hardware_logger.debug(f"Energiedaten erfolgreich abgerufen: {extra_info['current_power']}W")
except Exception as e:
hardware_logger.warning(f"Konnte Energiedaten nicht abrufen: {str(e)}")
except Exception as e:
hardware_logger.warning(f"Fehler beim Sammeln erweiterter Geräteinformationen: {str(e)}")
return extra_info
def get_energy_statistics(self) -> Dict[str, Any]:
"""
@@ -969,6 +969,32 @@ class TapoController:
'error': str(e)
}
def turn_off_outlet(self, ip: str, printer_id: int = None) -> bool:
"""
Wrapper für Legacy-Kompatibilität - schaltet eine Tapo-Steckdose aus
Args:
ip: IP-Adresse der Steckdose
printer_id: ID des zugehörigen Druckers für Logging (optional)
Returns:
bool: True wenn erfolgreich ausgeschaltet
"""
return self.turn_off(ip, printer_id=printer_id)
def turn_on_outlet(self, ip: str, printer_id: int = None) -> bool:
"""
Wrapper für Legacy-Kompatibilität - schaltet eine Tapo-Steckdose ein
Args:
ip: IP-Adresse der Steckdose
printer_id: ID des zugehörigen Druckers für Logging (optional)
Returns:
bool: True wenn erfolgreich eingeschaltet
"""
return self.toggle_plug(ip, True)
# ===== PRINTER MONITOR =====
class PrinterMonitor:

View File

@@ -196,7 +196,7 @@ class TapoStatusManager:
def _check_tapo_status(self, printer: Printer) -> Dict[str, any]:
"""
Prüft den Tapo-Steckdosen-Status
Prüft den Tapo-Steckdosen-Status mit erweiterten Fallback-Mechanismen
Args:
printer: Printer-Objekt
@@ -209,6 +209,7 @@ class TapoStatusManager:
from utils.hardware_integration import tapo_controller
if not tapo_controller:
logger.warning(f"Tapo-Controller nicht verfügbar für {printer.name}")
return {
"plug_status": self.STATUS_UNREACHABLE,
"plug_reachable": False,
@@ -217,38 +218,69 @@ class TapoStatusManager:
"error": "Tapo-Controller nicht verfügbar"
}
# Status abrufen
# Status abrufen mit Debug-Informationen
logger.debug(f"Prüfe Tapo-Status für {printer.name} ({printer.plug_ip})")
reachable, plug_status = tapo_controller.check_outlet_status(
printer.plug_ip,
printer_id=printer.id
printer_id=printer.id,
debug=False # Weniger Debug-Output für bessere Performance
)
if reachable:
# Erfolgreiche Verbindung
logger.debug(f"✅ Tapo-Steckdose {printer.plug_ip} erreichbar - Status: {plug_status}")
# Status normalisieren
if plug_status in ["on", "true", "1", True]:
normalized_status = self.STATUS_ON
power_status = "on"
elif plug_status in ["off", "false", "0", False]:
normalized_status = self.STATUS_OFF
power_status = "off"
else:
# Unbekannter Status, aber erreichbar
normalized_status = self.STATUS_UNREACHABLE
power_status = "unknown"
logger.warning(f"Unbekannter Tapo-Status '{plug_status}' für {printer.name}")
return {
"plug_status": self.STATUS_ON if plug_status == "on" else self.STATUS_OFF,
"plug_status": normalized_status,
"plug_reachable": True,
"power_status": plug_status,
"can_control": True
"power_status": power_status,
"can_control": True,
"last_check": datetime.now().isoformat()
}
else:
# Steckdose nicht erreichbar
logger.warning(f"⚠️ Tapo-Steckdose {printer.plug_ip} nicht erreichbar für {printer.name}")
return {
"plug_status": self.STATUS_UNREACHABLE,
"plug_reachable": False,
"power_status": None,
"can_control": False,
"error": "Steckdose nicht erreichbar"
"error": "Steckdose nicht erreichbar",
"last_check": datetime.now().isoformat()
}
except Exception as e:
logger.error(f"Fehler beim Prüfen des Tapo-Status für {printer.name}: {str(e)}")
except ImportError as e:
logger.error(f"Import-Fehler beim Tapo-Controller für {printer.name}: {str(e)}")
return {
"plug_status": self.STATUS_UNREACHABLE,
"plug_reachable": False,
"power_status": None,
"can_control": False,
"error": str(e)
"error": f"Import-Fehler: {str(e)}",
"fallback_used": True
}
except Exception as e:
logger.error(f"Unerwarteter Fehler beim Prüfen des Tapo-Status für {printer.name}: {str(e)}")
return {
"plug_status": self.STATUS_UNREACHABLE,
"plug_reachable": False,
"power_status": None,
"can_control": False,
"error": str(e),
"last_check": datetime.now().isoformat()
}
def control_plug(self, printer_id: int, action: str) -> Tuple[bool, str]: