From f5b4fdd296722cffa42858ee5b2a439e4ce09bd3 Mon Sep 17 00:00:00 2001 From: Till Tomczak Date: Thu, 29 May 2025 23:10:52 +0200 Subject: [PATCH] "Refactor job scheduling in backend using Conventional Commits (feat)" --- backend/app/app.py | 69 +++++++-- backend/app/utils/job_scheduler.py | 222 +++++++++++++++++++++++++++-- 2 files changed, 265 insertions(+), 26 deletions(-) diff --git a/backend/app/app.py b/backend/app/app.py index c529803d..8cf1a44a 100644 --- a/backend/app/app.py +++ b/backend/app/app.py @@ -2034,7 +2034,8 @@ def get_active_jobs(): @measure_execution_time(logger=jobs_logger, task_name="API-Job-Erstellung") 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: { "printer_id": int, @@ -2072,6 +2073,7 @@ def create_job(): # End-Zeit berechnen end_at = start_at + timedelta(minutes=duration_minutes) + now = datetime.now() db_session = get_db_session() @@ -2081,14 +2083,18 @@ def create_job(): db_session.close() return jsonify({"error": "Drucker nicht gefunden"}), 404 - # Prüfen, ob der Drucker online ist - printer_status, printer_active = check_printer_status(printer.plug_ip if printer.plug_ip else "") + # Intelligente Status-Bestimmung + is_immediate_job = start_at <= now # Job soll sofort oder in der Vergangenheit starten - # Status basierend auf Drucker-Verfügbarkeit setzen - if printer_status == "online" and printer_active: - job_status = "scheduled" - else: + if is_immediate_job: + # Sofort-Job: Status auf "waiting_for_printer" setzen für automatische Verarbeitung 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 new_job = Job( @@ -2106,15 +2112,56 @@ def create_job(): db_session.add(new_job) 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() 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") - return jsonify({"job": job_dict}), 201 + jobs_logger.info(f"✅ Job {job_id} erstellt für Drucker {printer_id}, Start: {start_at}, Dauer: {duration_minutes} Minuten, Status: {job_status}") + + # 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: - 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 @app.route('/api/jobs//extend', methods=['POST']) diff --git a/backend/app/utils/job_scheduler.py b/backend/app/utils/job_scheduler.py index ac5e69e4..d7d70f43 100644 --- a/backend/app/utils/job_scheduler.py +++ b/backend/app/utils/job_scheduler.py @@ -392,34 +392,54 @@ class BackgroundTaskScheduler: def _check_jobs(self) -> None: """ - Überprüft und verwaltet Druckjobs: - - Startet anstehende Jobs - - Beendet abgelaufene Jobs + Überprüft und verwaltet Druckjobs mit intelligentem Power Management: + - Startet anstehende Jobs (geplante 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() try: now = datetime.now() - # 1. Anstehende Jobs starten + # 1. Anstehende Jobs starten (geplante Jobs) 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}") + self.logger.info(f"🚀 Starte geplanten 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") + self.logger.info(f"✅ Job {job.id} gestartet - Drucker eingeschaltet") else: 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( Job.status == "running", Job.end_at <= now @@ -428,15 +448,51 @@ class BackgroundTaskScheduler: 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") + # Job als beendet markieren + job.status = "finished" + job.actual_end_time = now + 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: - 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: self.logger.error(f"❌ Fehler bei Überprüfung der Jobs: {str(e)}") @@ -448,6 +504,142 @@ class BackgroundTaskScheduler: finally: 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: """