"Refactor job scheduling in backend using Conventional Commits (feat)"
This commit is contained in:
parent
4f4de4a84c
commit
f5b4fdd296
@ -2034,7 +2034,8 @@ def get_active_jobs():
|
|||||||
@measure_execution_time(logger=jobs_logger, task_name="API-Job-Erstellung")
|
@measure_execution_time(logger=jobs_logger, task_name="API-Job-Erstellung")
|
||||||
def create_job():
|
def create_job():
|
||||||
"""
|
"""
|
||||||
Erstellt einen neuen Job mit dem Status "scheduled".
|
Erstellt einen neuen Job mit intelligentem Power Management.
|
||||||
|
Jobs die sofort starten sollen, werden automatisch verarbeitet.
|
||||||
|
|
||||||
Body: {
|
Body: {
|
||||||
"printer_id": int,
|
"printer_id": int,
|
||||||
@ -2072,6 +2073,7 @@ def create_job():
|
|||||||
|
|
||||||
# End-Zeit berechnen
|
# End-Zeit berechnen
|
||||||
end_at = start_at + timedelta(minutes=duration_minutes)
|
end_at = start_at + timedelta(minutes=duration_minutes)
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
db_session = get_db_session()
|
db_session = get_db_session()
|
||||||
|
|
||||||
@ -2081,14 +2083,18 @@ def create_job():
|
|||||||
db_session.close()
|
db_session.close()
|
||||||
return jsonify({"error": "Drucker nicht gefunden"}), 404
|
return jsonify({"error": "Drucker nicht gefunden"}), 404
|
||||||
|
|
||||||
# Prüfen, ob der Drucker online ist
|
# Intelligente Status-Bestimmung
|
||||||
printer_status, printer_active = check_printer_status(printer.plug_ip if printer.plug_ip else "")
|
is_immediate_job = start_at <= now # Job soll sofort oder in der Vergangenheit starten
|
||||||
|
|
||||||
# Status basierend auf Drucker-Verfügbarkeit setzen
|
if is_immediate_job:
|
||||||
if printer_status == "online" and printer_active:
|
# Sofort-Job: Status auf "waiting_for_printer" setzen für automatische Verarbeitung
|
||||||
job_status = "scheduled"
|
|
||||||
else:
|
|
||||||
job_status = "waiting_for_printer"
|
job_status = "waiting_for_printer"
|
||||||
|
jobs_logger.info(f"📦 Erstelle Sofort-Job für Drucker {printer.name} (Start: {start_at})")
|
||||||
|
else:
|
||||||
|
# Geplanter Job: Status auf "scheduled" setzen
|
||||||
|
job_status = "scheduled"
|
||||||
|
time_until_start = (start_at - now).total_seconds() / 60
|
||||||
|
jobs_logger.info(f"⏰ Erstelle geplanten Job für Drucker {printer.name} (Start in {time_until_start:.1f} Min)")
|
||||||
|
|
||||||
# Neuen Job erstellen
|
# Neuen Job erstellen
|
||||||
new_job = Job(
|
new_job = Job(
|
||||||
@ -2106,15 +2112,56 @@ def create_job():
|
|||||||
db_session.add(new_job)
|
db_session.add(new_job)
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
|
|
||||||
# Job-Objekt für die Antwort serialisieren
|
# Job-ID für weitere Verarbeitung speichern
|
||||||
|
job_id = new_job.id
|
||||||
job_dict = new_job.to_dict()
|
job_dict = new_job.to_dict()
|
||||||
db_session.close()
|
db_session.close()
|
||||||
|
|
||||||
jobs_logger.info(f"Neuer Job {new_job.id} erstellt für Drucker {printer_id}, Start: {start_at}, Dauer: {duration_minutes} Minuten")
|
jobs_logger.info(f"✅ Job {job_id} erstellt für Drucker {printer_id}, Start: {start_at}, Dauer: {duration_minutes} Minuten, Status: {job_status}")
|
||||||
return jsonify({"job": job_dict}), 201
|
|
||||||
|
# Intelligentes Power Management: Sofort-Jobs automatisch verarbeiten
|
||||||
|
if is_immediate_job:
|
||||||
|
try:
|
||||||
|
from utils.job_scheduler import get_job_scheduler
|
||||||
|
scheduler = get_job_scheduler()
|
||||||
|
|
||||||
|
# Versuche den Job sofort zu starten (schaltet Drucker automatisch ein)
|
||||||
|
if scheduler.handle_immediate_job(job_id):
|
||||||
|
jobs_logger.info(f"⚡ Sofort-Job {job_id} erfolgreich gestartet - Drucker automatisch eingeschaltet")
|
||||||
|
# Status in der Antwort aktualisieren
|
||||||
|
job_dict["status"] = "running"
|
||||||
|
job_dict["message"] = "Job wurde sofort gestartet - Drucker automatisch eingeschaltet"
|
||||||
|
else:
|
||||||
|
jobs_logger.warning(f"⚠️ Sofort-Job {job_id} konnte nicht gestartet werden - bleibt im Status 'waiting_for_printer'")
|
||||||
|
job_dict["message"] = "Job erstellt - wartet auf Drucker-Verfügbarkeit"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
jobs_logger.error(f"❌ Fehler beim automatischen Starten von Sofort-Job {job_id}: {str(e)}")
|
||||||
|
job_dict["message"] = "Job erstellt - automatischer Start fehlgeschlagen"
|
||||||
|
else:
|
||||||
|
# Geplanter Job: Power Management für zukünftige Optimierung
|
||||||
|
try:
|
||||||
|
from utils.job_scheduler import get_job_scheduler
|
||||||
|
scheduler = get_job_scheduler()
|
||||||
|
|
||||||
|
# Prüfe und manage Power für diesen Drucker (für optimale Vorbereitung)
|
||||||
|
scheduler.check_and_manage_printer_power(printer_id)
|
||||||
|
|
||||||
|
time_until_start = (start_at - now).total_seconds() / 60
|
||||||
|
job_dict["message"] = f"Job geplant - startet automatisch in {time_until_start:.1f} Minuten"
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
jobs_logger.warning(f"⚠️ Power-Management-Fehler für geplanten Job {job_id}: {str(e)}")
|
||||||
|
job_dict["message"] = "Job geplant - startet automatisch zur geplanten Zeit"
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
"job": job_dict,
|
||||||
|
"success": True,
|
||||||
|
"immediate_start": is_immediate_job
|
||||||
|
}), 201
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
jobs_logger.error(f"Fehler beim Erstellen eines Jobs: {str(e)}")
|
jobs_logger.error(f"❌ Fehler beim Erstellen eines Jobs: {str(e)}")
|
||||||
return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500
|
return jsonify({"error": "Interner Serverfehler", "details": str(e)}), 500
|
||||||
|
|
||||||
@app.route('/api/jobs/<int:job_id>/extend', methods=['POST'])
|
@app.route('/api/jobs/<int:job_id>/extend', methods=['POST'])
|
||||||
|
@ -392,34 +392,54 @@ class BackgroundTaskScheduler:
|
|||||||
|
|
||||||
def _check_jobs(self) -> None:
|
def _check_jobs(self) -> None:
|
||||||
"""
|
"""
|
||||||
Überprüft und verwaltet Druckjobs:
|
Überprüft und verwaltet Druckjobs mit intelligentem Power Management:
|
||||||
- Startet anstehende Jobs
|
- Startet anstehende Jobs (geplante Jobs)
|
||||||
- Beendet abgelaufene Jobs
|
- Beendet abgelaufene Jobs (schaltet Steckdose aus)
|
||||||
|
- Schaltet Drucker automatisch aus bei Leerlauf
|
||||||
|
- Schaltet Drucker automatisch ein bei neuen Jobs
|
||||||
"""
|
"""
|
||||||
db_session = get_db_session()
|
db_session = get_db_session()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
now = datetime.now()
|
now = datetime.now()
|
||||||
|
|
||||||
# 1. Anstehende Jobs starten
|
# 1. Anstehende Jobs starten (geplante Jobs)
|
||||||
pending_jobs = db_session.query(Job).filter(
|
pending_jobs = db_session.query(Job).filter(
|
||||||
Job.status == "scheduled",
|
Job.status == "scheduled",
|
||||||
Job.start_at <= now
|
Job.start_at <= now
|
||||||
).all()
|
).all()
|
||||||
|
|
||||||
for job in pending_jobs:
|
for job in pending_jobs:
|
||||||
self.logger.info(f"🚀 Starte Job {job.id}: {job.name}")
|
self.logger.info(f"🚀 Starte geplanten Job {job.id}: {job.name}")
|
||||||
|
|
||||||
# Steckdose einschalten
|
# Steckdose einschalten
|
||||||
if self.toggle_printer_plug(job.printer_id, True):
|
if self.toggle_printer_plug(job.printer_id, True):
|
||||||
# Job als laufend markieren
|
# Job als laufend markieren
|
||||||
job.status = "running"
|
job.status = "running"
|
||||||
db_session.commit()
|
db_session.commit()
|
||||||
self.logger.info(f"✅ Job {job.id} gestartet")
|
self.logger.info(f"✅ Job {job.id} gestartet - Drucker eingeschaltet")
|
||||||
else:
|
else:
|
||||||
self.logger.error(f"❌ Konnte Steckdose für Job {job.id} nicht einschalten")
|
self.logger.error(f"❌ Konnte Steckdose für Job {job.id} nicht einschalten")
|
||||||
|
|
||||||
# 2. Abgelaufene Jobs beenden
|
# 2. Sofort-Jobs starten (Jobs die bereits hätten starten sollen)
|
||||||
|
immediate_jobs = db_session.query(Job).filter(
|
||||||
|
Job.status == "waiting_for_printer",
|
||||||
|
Job.start_at <= now
|
||||||
|
).all()
|
||||||
|
|
||||||
|
for job in immediate_jobs:
|
||||||
|
self.logger.info(f"⚡ Starte Sofort-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"✅ Sofort-Job {job.id} gestartet - Drucker automatisch eingeschaltet")
|
||||||
|
else:
|
||||||
|
self.logger.error(f"❌ Konnte Steckdose für Sofort-Job {job.id} nicht einschalten")
|
||||||
|
|
||||||
|
# 3. Abgelaufene Jobs beenden
|
||||||
running_jobs = db_session.query(Job).filter(
|
running_jobs = db_session.query(Job).filter(
|
||||||
Job.status == "running",
|
Job.status == "running",
|
||||||
Job.end_at <= now
|
Job.end_at <= now
|
||||||
@ -428,15 +448,51 @@ class BackgroundTaskScheduler:
|
|||||||
for job in running_jobs:
|
for job in running_jobs:
|
||||||
self.logger.info(f"🏁 Beende Job {job.id}: {job.name}")
|
self.logger.info(f"🏁 Beende Job {job.id}: {job.name}")
|
||||||
|
|
||||||
# Steckdose ausschalten
|
# Job als beendet markieren
|
||||||
if self.toggle_printer_plug(job.printer_id, False):
|
job.status = "finished"
|
||||||
# Job als beendet markieren
|
job.actual_end_time = now
|
||||||
job.status = "finished"
|
db_session.commit()
|
||||||
job.actual_end_time = now
|
self.logger.info(f"✅ Job {job.id} beendet")
|
||||||
db_session.commit()
|
|
||||||
self.logger.info(f"✅ Job {job.id} beendet")
|
# Prüfen ob weitere Jobs für diesen Drucker anstehen
|
||||||
|
pending_jobs_for_printer = db_session.query(Job).filter(
|
||||||
|
Job.printer_id == job.printer_id,
|
||||||
|
Job.status.in_(["scheduled", "running", "waiting_for_printer"])
|
||||||
|
).count()
|
||||||
|
|
||||||
|
if pending_jobs_for_printer == 0:
|
||||||
|
# Keine weiteren Jobs - Drucker ausschalten (Leerlauf-Management)
|
||||||
|
if self.toggle_printer_plug(job.printer_id, False):
|
||||||
|
self.logger.info(f"💤 Drucker {job.printer_id} automatisch ausgeschaltet - Leerlauf erkannt")
|
||||||
|
else:
|
||||||
|
self.logger.warning(f"⚠️ Konnte Drucker {job.printer_id} nicht ausschalten")
|
||||||
else:
|
else:
|
||||||
self.logger.error(f"❌ Konnte Steckdose für Job {job.id} nicht ausschalten")
|
self.logger.info(f"🔄 Drucker {job.printer_id} bleibt eingeschaltet - {pending_jobs_for_printer} weitere Jobs anstehend")
|
||||||
|
|
||||||
|
# 4. Intelligentes Leerlauf-Management für alle aktiven Drucker
|
||||||
|
active_printers = db_session.query(Printer).filter(
|
||||||
|
Printer.active == True,
|
||||||
|
Printer.plug_ip.isnot(None),
|
||||||
|
Printer.status == "online"
|
||||||
|
).all()
|
||||||
|
|
||||||
|
for printer in active_printers:
|
||||||
|
# Prüfen ob Jobs für diesen Drucker anstehen
|
||||||
|
active_jobs_count = db_session.query(Job).filter(
|
||||||
|
Job.printer_id == printer.id,
|
||||||
|
Job.status.in_(["scheduled", "running", "waiting_for_printer"])
|
||||||
|
).count()
|
||||||
|
|
||||||
|
if active_jobs_count == 0:
|
||||||
|
# Keine Jobs anstehend - prüfen ob Drucker schon längere Zeit im Leerlauf ist
|
||||||
|
if printer.last_checked:
|
||||||
|
idle_time = now - printer.last_checked
|
||||||
|
# Drucker ausschalten wenn länger als 5 Minuten im Leerlauf
|
||||||
|
if idle_time.total_seconds() > 300: # 5 Minuten
|
||||||
|
if self.toggle_printer_plug(printer.id, False):
|
||||||
|
self.logger.info(f"💤 Drucker {printer.name} nach {idle_time.total_seconds()//60:.0f} Min Leerlauf ausgeschaltet")
|
||||||
|
else:
|
||||||
|
self.logger.warning(f"⚠️ Konnte Drucker {printer.name} nach Leerlauf nicht ausschalten")
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(f"❌ Fehler bei Überprüfung der Jobs: {str(e)}")
|
self.logger.error(f"❌ Fehler bei Überprüfung der Jobs: {str(e)}")
|
||||||
@ -448,6 +504,142 @@ class BackgroundTaskScheduler:
|
|||||||
finally:
|
finally:
|
||||||
db_session.close()
|
db_session.close()
|
||||||
|
|
||||||
|
def handle_immediate_job(self, job_id: int) -> bool:
|
||||||
|
"""
|
||||||
|
Behandelt einen Job sofort (für Sofort-Start bei Job-Erstellung).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
job_id: ID des zu startenden Jobs
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True wenn Job erfolgreich gestartet wurde
|
||||||
|
"""
|
||||||
|
db_session = get_db_session()
|
||||||
|
|
||||||
|
try:
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
# Job aus Datenbank laden
|
||||||
|
job = db_session.query(Job).get(job_id)
|
||||||
|
if not job:
|
||||||
|
self.logger.error(f"❌ Job {job_id} nicht gefunden")
|
||||||
|
db_session.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Nur Jobs behandeln die sofort starten sollen
|
||||||
|
if job.start_at > now:
|
||||||
|
self.logger.info(f"⏰ Job {job_id} ist für später geplant ({job.start_at}) - kein Sofort-Start")
|
||||||
|
db_session.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Nur Jobs in passenden Status
|
||||||
|
if job.status not in ["scheduled", "waiting_for_printer"]:
|
||||||
|
self.logger.info(f"ℹ️ Job {job_id} hat Status '{job.status}' - kein Sofort-Start nötig")
|
||||||
|
db_session.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
self.logger.info(f"⚡ Starte Sofort-Job {job_id}: {job.name} für Drucker {job.printer_id}")
|
||||||
|
|
||||||
|
# Steckdose einschalten
|
||||||
|
if self.toggle_printer_plug(job.printer_id, True):
|
||||||
|
# Job als laufend markieren
|
||||||
|
job.status = "running"
|
||||||
|
db_session.commit()
|
||||||
|
db_session.close()
|
||||||
|
|
||||||
|
self.logger.info(f"✅ Sofort-Job {job_id} erfolgreich gestartet - Drucker automatisch eingeschaltet")
|
||||||
|
return True
|
||||||
|
else:
|
||||||
|
self.logger.error(f"❌ Konnte Steckdose für Sofort-Job {job_id} nicht einschalten")
|
||||||
|
db_session.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"❌ Fehler beim Starten von Sofort-Job {job_id}: {str(e)}")
|
||||||
|
try:
|
||||||
|
db_session.rollback()
|
||||||
|
db_session.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
def check_and_manage_printer_power(self, printer_id: int) -> bool:
|
||||||
|
"""
|
||||||
|
Prüft und verwaltet die Stromversorgung eines spezifischen Druckers.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
printer_id: ID des zu prüfenden Druckers
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
bool: True wenn Power-Management erfolgreich
|
||||||
|
"""
|
||||||
|
db_session = get_db_session()
|
||||||
|
|
||||||
|
try:
|
||||||
|
now = datetime.now()
|
||||||
|
|
||||||
|
# Drucker laden
|
||||||
|
printer = db_session.query(Printer).get(printer_id)
|
||||||
|
if not printer or not printer.plug_ip:
|
||||||
|
db_session.close()
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Aktive Jobs für diesen Drucker prüfen
|
||||||
|
active_jobs = db_session.query(Job).filter(
|
||||||
|
Job.printer_id == printer_id,
|
||||||
|
Job.status.in_(["scheduled", "running", "waiting_for_printer"])
|
||||||
|
).all()
|
||||||
|
|
||||||
|
current_jobs = [job for job in active_jobs if job.start_at <= now]
|
||||||
|
future_jobs = [job for job in active_jobs if job.start_at > now]
|
||||||
|
|
||||||
|
if current_jobs:
|
||||||
|
# Jobs laufen oder sollten laufen - Drucker einschalten
|
||||||
|
self.logger.info(f"🔋 Drucker {printer.name} benötigt Strom - {len(current_jobs)} aktive Jobs")
|
||||||
|
success = self.toggle_printer_plug(printer_id, True)
|
||||||
|
|
||||||
|
# Jobs von waiting_for_printer auf running umstellen
|
||||||
|
for job in current_jobs:
|
||||||
|
if job.status == "waiting_for_printer":
|
||||||
|
job.status = "running"
|
||||||
|
self.logger.info(f"🚀 Job {job.id} von 'waiting_for_printer' auf 'running' umgestellt")
|
||||||
|
|
||||||
|
db_session.commit()
|
||||||
|
db_session.close()
|
||||||
|
return success
|
||||||
|
|
||||||
|
elif future_jobs:
|
||||||
|
# Nur zukünftige Jobs - Drucker kann ausgeschaltet bleiben
|
||||||
|
next_job_time = min(job.start_at for job in future_jobs)
|
||||||
|
time_until_next = (next_job_time - now).total_seconds() / 60
|
||||||
|
|
||||||
|
self.logger.info(f"⏳ Drucker {printer.name} hat {len(future_jobs)} zukünftige Jobs, nächster in {time_until_next:.1f} Min")
|
||||||
|
|
||||||
|
# Drucker ausschalten wenn nächster Job erst in mehr als 10 Minuten
|
||||||
|
if time_until_next > 10:
|
||||||
|
success = self.toggle_printer_plug(printer_id, False)
|
||||||
|
db_session.close()
|
||||||
|
return success
|
||||||
|
else:
|
||||||
|
self.logger.info(f"🔄 Drucker {printer.name} bleibt eingeschaltet - nächster Job bald")
|
||||||
|
db_session.close()
|
||||||
|
return True
|
||||||
|
|
||||||
|
else:
|
||||||
|
# Keine Jobs - Drucker ausschalten (Leerlauf)
|
||||||
|
self.logger.info(f"💤 Drucker {printer.name} hat keine anstehenden Jobs - ausschalten")
|
||||||
|
success = self.toggle_printer_plug(printer_id, False)
|
||||||
|
db_session.close()
|
||||||
|
return success
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
self.logger.error(f"❌ Fehler beim Power-Management für Drucker {printer_id}: {str(e)}")
|
||||||
|
try:
|
||||||
|
db_session.close()
|
||||||
|
except:
|
||||||
|
pass
|
||||||
|
return False
|
||||||
|
|
||||||
|
|
||||||
def test_tapo_connection(ip_address: str, username: str = None, password: str = None) -> dict:
|
def test_tapo_connection(ip_address: str, username: str = None, password: str = None) -> dict:
|
||||||
"""
|
"""
|
||||||
|
Loading…
x
Reference in New Issue
Block a user