"Update file structure and tests for job scheduler integration"

This commit is contained in:
2025-05-29 22:37:08 +02:00
parent 482e04723d
commit c7aa7e75b0
6 changed files with 300 additions and 252 deletions

View File

@@ -33,6 +33,7 @@ class BackgroundTaskScheduler:
self._stop_event = threading.Event()
self._running = False
self._start_time: Optional[datetime] = None
self.logger = get_scheduler_logger()
def register_task(self,
task_id: str,
@@ -55,9 +56,8 @@ class BackgroundTaskScheduler:
Returns:
bool: True wenn erfolgreich, False wenn die ID bereits existiert
"""
logger = get_scheduler_logger()
if task_id in self._tasks:
logger.error(f"Task mit ID {task_id} existiert bereits")
self.logger.error(f"Task mit ID {task_id} existiert bereits")
return False
self._tasks[task_id] = {
@@ -70,7 +70,7 @@ class BackgroundTaskScheduler:
"next_run": datetime.now() if enabled else None
}
logger.info(f"Task {task_id} registriert: Intervall {interval}s, Enabled: {enabled}")
self.logger.info(f"Task {task_id} registriert: Intervall {interval}s, Enabled: {enabled}")
return True
def update_task(self,
@@ -92,9 +92,8 @@ class BackgroundTaskScheduler:
Returns:
bool: True wenn erfolgreich, False wenn die ID nicht existiert
"""
logger = get_scheduler_logger()
if task_id not in self._tasks:
logger.error(f"Task mit ID {task_id} existiert nicht")
self.logger.error(f"Task mit ID {task_id} existiert nicht")
return False
task = self._tasks[task_id]
@@ -115,7 +114,7 @@ class BackgroundTaskScheduler:
else:
task["next_run"] = None
logger.info(f"Task {task_id} aktualisiert: Intervall {task['interval']}s, Enabled: {task['enabled']}")
self.logger.info(f"Task {task_id} aktualisiert: Intervall {task['interval']}s, Enabled: {task['enabled']}")
return True
def remove_task(self, task_id: str) -> bool:
@@ -128,13 +127,12 @@ class BackgroundTaskScheduler:
Returns:
bool: True wenn erfolgreich, False wenn die ID nicht existiert
"""
logger = get_scheduler_logger()
if task_id not in self._tasks:
logger.error(f"Task mit ID {task_id} existiert nicht")
self.logger.error(f"Task mit ID {task_id} existiert nicht")
return False
del self._tasks[task_id]
logger.info(f"Task {task_id} entfernt")
self.logger.info(f"Task {task_id} entfernt")
return True
def get_task_info(self, task_id: Optional[str] = None) -> Union[Dict, List[Dict]]:
@@ -217,9 +215,8 @@ class BackgroundTaskScheduler:
Returns:
bool: True wenn erfolgreich gestartet, False wenn bereits läuft
"""
logger = get_scheduler_logger()
if self._running:
logger.warning("Scheduler läuft bereits")
self.logger.warning("Scheduler läuft bereits")
return False
self._stop_event.clear()
@@ -229,7 +226,7 @@ class BackgroundTaskScheduler:
self._running = True
self._start_time = datetime.now()
logger.info("Scheduler gestartet")
self.logger.info("Scheduler gestartet")
return True
def stop(self) -> bool:
@@ -239,9 +236,8 @@ class BackgroundTaskScheduler:
Returns:
bool: True wenn erfolgreich gestoppt, False wenn nicht läuft
"""
logger = get_scheduler_logger()
if not self._running:
logger.warning("Scheduler läuft nicht")
self.logger.warning("Scheduler läuft nicht")
return False
self._stop_event.set()
@@ -250,7 +246,7 @@ class BackgroundTaskScheduler:
self._running = False
self._start_time = None
logger.info("Scheduler gestoppt")
self.logger.info("Scheduler gestoppt")
return True
def is_running(self) -> bool:
@@ -264,8 +260,7 @@ class BackgroundTaskScheduler:
def _run(self) -> None:
"""Hauptloop des Schedulers."""
logger = get_scheduler_logger()
logger.info("Scheduler-Thread gestartet")
self.logger.info("Scheduler-Thread gestartet")
while not self._stop_event.is_set():
now = datetime.now()
@@ -276,221 +271,249 @@ class BackgroundTaskScheduler:
if now >= task["next_run"]:
try:
logger.debug(f"Führe Task {task_id} aus")
self.logger.debug(f"Führe Task {task_id} aus")
task["func"](*task["args"], **task["kwargs"])
task["last_run"] = now
task["next_run"] = now + timedelta(seconds=task["interval"])
logger.debug(f"Task {task_id} erfolgreich ausgeführt, nächste Ausführung: {task['next_run']}")
self.logger.debug(f"Task {task_id} erfolgreich ausgeführt, nächste Ausführung: {task['next_run']}")
except Exception as e:
logger.error(f"Fehler bei Ausführung von Task {task_id}: {str(e)}")
self.logger.error(f"Fehler bei Ausführung von Task {task_id}: {str(e)}")
# Trotzdem nächste Ausführung planen
task["next_run"] = now + timedelta(seconds=task["interval"])
# Schlafenszeit berechnen (1 Sekunde oder weniger)
time.sleep(1)
logger.info("Scheduler-Thread beendet")
self.logger.info("Scheduler-Thread beendet")
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.
Args:
ip: IP-Adresse der Steckdose
state: True = Ein, False = Aus
username: Benutzername für die Steckdose (optional)
password: Passwort für die Steckdose (optional)
Returns:
bool: True wenn erfolgreich geschaltet
"""
try:
# PyP100 importieren
try:
from PyP100 import PyP100
except ImportError:
self.logger.error("❌ PyP100-Modul nicht installiert - Steckdose kann nicht geschaltet werden")
return False
# Anmeldedaten aus Einstellungen verwenden, falls nicht angegeben
if not username or not password:
from config.settings import TAPO_USERNAME, TAPO_PASSWORD
username = TAPO_USERNAME
password = TAPO_PASSWORD
self.logger.debug(f"🔧 Verwende globale Tapo-Anmeldedaten für {ip}")
# P100-Verbindung herstellen (P100 statt P110 verwenden)
p100 = PyP100.P100(ip, username, password)
# Handshake und Login durchführen
p100.handshake()
p100.login()
# Steckdose schalten
if state:
p100.turnOn()
self.logger.info(f"✅ Tapo-Steckdose {ip} erfolgreich eingeschaltet")
else:
p100.turnOff()
self.logger.info(f"✅ Tapo-Steckdose {ip} erfolgreich ausgeschaltet")
return True
except Exception as e:
action = "ein" if state else "aus"
self.logger.error(f"❌ Fehler beim {action}schalten der Tapo-Steckdose {ip}: {str(e)}")
return False
def toggle_plug(printer_id: int, state: bool) -> bool:
"""
Schaltet eine Tapo-Steckdose ein oder aus.
Args:
printer_id: ID des Druckers
state: True für ein, False für aus
def toggle_printer_plug(self, printer_id: int, state: bool) -> bool:
"""
Schaltet die Steckdose eines Druckers ein oder aus.
Returns:
bool: True wenn erfolgreich, False wenn fehlgeschlagen
"""
logger = get_logger("printers")
db_session = get_db_session()
try:
printer = db_session.query(Printer).get(printer_id)
if not printer:
logger.error(f"Drucker mit ID {printer_id} nicht gefunden")
db_session.close()
return False
# Konfiguration validieren
if not printer.plug_ip or not printer.plug_username or not printer.plug_password:
logger.error(f"Unvollständige Steckdosen-Konfiguration für Drucker {printer.name}")
db_session.close()
return False
# Importiere PyP100 für Tapo-Unterstützung
Args:
printer_id: ID des Druckers
state: True für ein, False für aus
Returns:
bool: True wenn erfolgreich, False wenn fehlgeschlagen
"""
try:
from PyP100 import PyP100
except ImportError:
logger.error("PyP100-Modul nicht verfügbar - kann Tapo-Steckdosen nicht steuern")
# Drucker aus Datenbank holen
db_session = get_db_session()
printer = db_session.query(Printer).get(printer_id)
if not printer:
self.logger.error(f"❌ Drucker mit ID {printer_id} nicht gefunden")
db_session.close()
return False
# Konfiguration validieren
if not printer.plug_ip:
self.logger.error(f"❌ Unvollständige Steckdosen-Konfiguration für Drucker {printer.name}")
db_session.close()
return False
# Steckdose schalten
success = self.toggle_plug(
ip=printer.plug_ip,
state=state,
username=printer.plug_username,
password=printer.plug_password
)
if success:
# Status in Datenbank aktualisieren
printer.status = "online" if state else "offline"
printer.last_checked = datetime.now()
db_session.commit()
self.logger.info(f"✅ Status für Drucker {printer.name} aktualisiert: {'online' if state else 'offline'}")
db_session.close()
return success
except Exception as e:
action = "ein" if state else "aus"
self.logger.error(f"❌ Fehler beim {action}schalten der Steckdose für Drucker {printer_id}: {str(e)}")
try:
db_session.close()
except:
pass
return False
def _check_jobs(self) -> None:
"""
Überprüft und verwaltet Druckjobs:
- Startet anstehende Jobs
- Beendet abgelaufene Jobs
"""
db_session = get_db_session()
# Verwende die in der Datenbank gespeicherten Anmeldedaten
# Fallback zu config/settings.py wenn nicht vorhanden
username = printer.plug_username
password = printer.plug_password
if not username or not password:
from config.settings import TAPO_USERNAME, TAPO_PASSWORD
username = TAPO_USERNAME
password = TAPO_PASSWORD
logger.debug(f"Verwende globale Tapo-Anmeldedaten für {printer.name}")
# TP-Link Tapo P100 Verbindung herstellen
p100 = PyP100.P100(printer.plug_ip, username, password)
p100.handshake() # Authentifizierung
p100.login() # Login
# Steckdose schalten
if state:
p100.turnOn()
logger.info(f"Steckdose für {printer.name} eingeschaltet")
else:
p100.turnOff()
logger.info(f"Steckdose für {printer.name} ausgeschaltet")
# Status in Datenbank aktualisieren
printer.status = "online" if state else "offline"
printer.last_checked = datetime.now()
db_session.commit()
db_session.close()
return True
except Exception as e:
logger.error(f"Fehler beim Schalten der Steckdose für Drucker {printer_id}: {str(e)}")
try:
now = datetime.now()
# 1. Anstehende Jobs starten
pending_jobs = db_session.query(Job).filter(
Job.status == "scheduled",
Job.start_at <= now
).all()
for job in pending_jobs:
self.logger.info(f"🚀 Starte Job {job.id}: {job.name}")
# Steckdose einschalten
if self.toggle_printer_plug(job.printer_id, True):
# Job als laufend markieren
job.status = "running"
db_session.commit()
self.logger.info(f"✅ Job {job.id} gestartet")
else:
self.logger.error(f"❌ Konnte Steckdose für Job {job.id} nicht einschalten")
# 2. Abgelaufene Jobs beenden
running_jobs = db_session.query(Job).filter(
Job.status == "running",
Job.end_at <= now
).all()
for job in running_jobs:
self.logger.info(f"🏁 Beende Job {job.id}: {job.name}")
# Steckdose ausschalten
if self.toggle_printer_plug(job.printer_id, False):
# Job als beendet markieren
job.status = "finished"
job.actual_end_time = now
db_session.commit()
self.logger.info(f"✅ Job {job.id} beendet")
else:
self.logger.error(f"❌ Konnte Steckdose für Job {job.id} nicht ausschalten")
except Exception as e:
self.logger.error(f"❌ Fehler bei Überprüfung der Jobs: {str(e)}")
try:
db_session.rollback()
except:
pass
finally:
db_session.close()
except:
pass
return False
def test_tapo_connection(ip_address: str, username: str = None, password: str = None) -> dict:
"""
Testet die Verbindung zu einer Tapo-Steckdose und gibt detaillierte Informationen zurück.
Testet die Verbindung zu einer TP-Link Tapo P110-Steckdose.
Args:
ip_address: IP-Adresse der Steckdose
username: Benutzername (optional, verwendet globale Konfiguration als Fallback)
password: Passwort (optional, verwendet globale Konfiguration als Fallback)
username: Benutzername für die Steckdose (optional)
password: Passwort für die Steckdose (optional)
Returns:
dict: Testergebnis mit Status und Informationen
dict: Ergebnis mit Status und Informationen
"""
logger = get_logger("printers")
logger = get_logger("tapo")
result = {
"success": False,
"error": None,
"message": "",
"device_info": None,
"status": "unknown"
"error": None
}
# Fallback zu globalen Anmeldedaten
if not username or not password:
username = TAPO_USERNAME
password = TAPO_PASSWORD
logger.debug(f"🔧 Verwende globale Tapo-Anmeldedaten für {ip_address}")
try:
logger.debug(f"Teste Tapo-Verbindung zu {ip_address}")
p110 = PyP110.P110(ip_address, username, password)
p110.handshake() # Authentifizierung
p110.login() # Login
# Importiere PyP100 für Tapo-Unterstützung
try:
from PyP100 import PyP100
except ImportError:
result["message"] = "PyP100-Modul nicht verfügbar"
result["error"] = "ModuleNotFound"
logger.error("PyP100-Modul nicht verfügbar - kann Tapo-Steckdosen nicht testen")
return result
# Verwende globale Anmeldedaten falls nicht angegeben
if not username or not password:
from config.settings import TAPO_USERNAME, TAPO_PASSWORD
username = TAPO_USERNAME
password = TAPO_PASSWORD
logger.debug(f"Verwende globale Tapo-Anmeldedaten für {ip_address}")
# TP-Link Tapo P100 Verbindung herstellen
p100 = PyP100.P100(ip_address, username, password)
p100.handshake() # Authentifizierung
p100.login() # Login
# Geräteinformationen abrufen
device_info = p110.getDeviceInfo()
result["device_info"] = device_info
result["status"] = "on" if device_info.get('device_on', False) else "off"
result["success"] = True
device_info = p100.getDeviceInfo()
logger.debug(f"✅ Tapo-Verbindung zu {ip_address} erfolgreich")
result["success"] = True
result["message"] = "Verbindung erfolgreich"
result["device_info"] = device_info
logger.info(f"Tapo-Verbindung zu {ip_address} erfolgreich: {device_info.get('nickname', 'Unbekannt')}")
except Exception as e:
result["success"] = False
result["message"] = f"Verbindungsfehler: {str(e)}"
result["error"] = str(e)
logger.warning(f"❌ Tapo-Verbindung zu {ip_address} fehlgeschlagen: {str(e)}")
logger.error(f"Fehler bei Tapo-Test zu {ip_address}: {str(e)}")
return result
def check_jobs():
"""
Überprüft alle geplanten und laufenden Jobs und schaltet Steckdosen entsprechend.
Diese Funktion wird vom Scheduler regelmäßig aufgerufen und:
1. Prüft, ob geplante Jobs gestartet werden müssen
2. Prüft, ob laufende Jobs beendet werden müssen
3. Aktualisiert den Status der Jobs
"""
logger = get_logger("jobs")
db_session = get_db_session()
try:
now = datetime.now()
# Geplante Jobs abrufen (mit 5 Minuten Puffer für vergangene Jobs)
scheduled_jobs = db_session.query(Job).options(
joinedload(Job.printer)
).filter(
Job.status == "scheduled",
Job.start_at <= now
).all()
# Laufende Jobs abrufen (mit 5 Minuten Sicherheitspuffer)
running_jobs = db_session.query(Job).options(
joinedload(Job.printer)
).filter(
Job.status == "running",
Job.end_at <= now - timedelta(minutes=5) # 5 Minuten Sicherheitspuffer
).all()
# Geplante Jobs starten
for job in scheduled_jobs:
logger.info(f"Starte geplanten Job {job.id}: {job.name} für Drucker {job.printer.name}")
# Steckdose einschalten
if toggle_plug(job.printer_id, True):
# Job als laufend markieren
job.status = "running"
job.end_at = job.start_at + timedelta(minutes=job.duration_minutes)
db_session.commit()
logger.info(f"Job {job.id} gestartet: läuft bis {job.end_at}")
else:
logger.error(f"Fehler beim Starten von Job {job.id}: Steckdose konnte nicht eingeschaltet werden")
# Beendete Jobs stoppen
for job in running_jobs:
logger.info(f"Beende laufenden Job {job.id}: {job.name} für Drucker {job.printer.name}")
# Steckdose ausschalten
if toggle_plug(job.printer_id, False):
# Job als beendet markieren
job.status = "finished"
job.actual_end_time = now
db_session.commit()
logger.info(f"Job {job.id} beendet: tatsächliche Endzeit {job.actual_end_time}")
else:
logger.error(f"Fehler beim Beenden von Job {job.id}: Steckdose konnte nicht ausgeschaltet werden")
db_session.close()
except Exception as e:
logger.error(f"Fehler im Job-Scheduler: {str(e)}")
db_session.close()
# Globaler Scheduler
# Scheduler-Instanz erzeugen
scheduler = BackgroundTaskScheduler()
# Job-Überprüfungs-Task registrieren (alle 60 Sekunden)
scheduler.register_task(
task_id="check_jobs",
func=check_jobs,
interval=60,
enabled=True
)
# Standardaufgaben registrieren
scheduler.register_task("check_jobs", scheduler._check_jobs, interval=60)
# Alias für Kompatibilität
JobScheduler = BackgroundTaskScheduler