diff --git a/kobrax_moonraker_bridge.py b/kobrax_moonraker_bridge.py index 6ac5e90..ca46c95 100644 --- a/kobrax_moonraker_bridge.py +++ b/kobrax_moonraker_bridge.py @@ -844,6 +844,7 @@ class KobraXBridge: self._camera_autostarted: bool = False self._camera_user_stopped: bool = False # User hat Kamera während Druck manuell gestoppt self.camera_cache: CameraCache = CameraCache() + self._loop: "asyncio.AbstractEventLoop | None" = None self._prev_kobra_state: str = "" self._print_active: bool = False self._notify_every_minutes: int = 0 @@ -1108,6 +1109,61 @@ class KobraXBridge: # Notifications (apprise) # ------------------------------------------------------------------------- + async def _capture_notification_image(self, timeout: float = 15.0) -> bytes: + """Liefert einen aktuellen JPEG-Frame für Bild-Benachrichtigungen. + + Falls die Kamera nicht aktiv streamt, wird sie kurz eingeschaltet, ein + Frame abgegriffen und – sofern wir sie selbst gestartet haben – wieder + ausgeschaltet (toggle on → snapshot → toggle off). + """ + loop = asyncio.get_event_loop() + # 1) Bereits ein frischer Frame im Cache (z. B. WebUI schaut zu)? → direkt nutzen. + if self.camera_cache.latest_jpeg and (time.time() - self.camera_cache.latest_jpeg_ts) < 3.0: + return self.camera_cache.latest_jpeg + + started_here = False + try: + # 2) Kamera am Drucker einschalten, falls sie nicht ohnehin läuft. + if not self._camera_autostarted: + try: + await loop.run_in_executor(None, self.client.start_camera) + started_here = True + log.debug("Kamera für Bild-Benachrichtigung temporär gestartet") + except Exception as e: + log.warning(f"Kamera-Start für Benachrichtigung fehlgeschlagen: {e}") + # rtspUrl aktualisieren (kommt im info/report mit, wenn Kamera streamt) + try: + info = await loop.run_in_executor(None, self.client.query_info) + if info: + self._on_info(info) + except Exception: + pass + + url = self._state.get("camera_url", "") + if not url: + return b"" + self.camera_cache.set_url(url) + await self.camera_cache.ensure_running() + + # 3) Auf einen frischen Frame warten (ffmpeg + RTSP-Warmup). + ref_ts = time.time() + deadline = ref_ts + timeout + while time.time() < deadline: + if self.camera_cache.latest_jpeg and self.camera_cache.latest_jpeg_ts >= ref_ts: + return self.camera_cache.latest_jpeg + await asyncio.sleep(0.3) + # Kein frischer Frame – notfalls den letzten vorhandenen nehmen. + return self.camera_cache.latest_jpeg + finally: + # 4) Wieder ausschalten, wenn wir sie nur für den Snapshot gestartet + # haben (nicht während eines laufenden Drucks mit Auto-Kamera). + if started_here and not self._camera_autostarted: + try: + await loop.run_in_executor(None, self.client.stop_camera) + log.debug("Kamera nach Bild-Benachrichtigung wieder gestoppt") + except Exception as e: + log.debug(f"Kamera-Stop nach Benachrichtigung fehlgeschlagen: {e}") + def _notify(self, event: str, filename: str = ""): if self._notifications_paused: return @@ -1161,13 +1217,17 @@ class KobraXBridge: ap2.add(u) attach = None tmppath = None - jpeg = self.camera_cache.latest_jpeg - if not jpeg and self.camera_cache._url: - # Camera URL is set but no frame yet — wait briefly (camera may be starting) - deadline = time.monotonic() + 5.0 - while not jpeg and time.monotonic() < deadline: - time.sleep(0.3) + jpeg = b"" + if self._loop is not None: + try: + fut = asyncio.run_coroutine_threadsafe( + self._capture_notification_image(timeout=15.0), self._loop) + jpeg = fut.result(timeout=30.0) or b"" + except Exception as e: + log.warning(f"Kamera-Snapshot für Benachrichtigung fehlgeschlagen: {e}") jpeg = self.camera_cache.latest_jpeg + else: + jpeg = self.camera_cache.latest_jpeg if jpeg: try: with tempfile.NamedTemporaryFile(suffix=".jpg", delete=False) as f: @@ -5494,6 +5554,7 @@ async def run_bridge(args): client, args=per_args, store=store, printer_id=pid, all_bridges=all_bridges, ) + bridge._loop = loop # printer_name aus config.ini übernehmen falls gesetzt if p.get("name"): bridge._state["printer_name"] = p["name"]