From 6a07a6b1a21f4eee800004d8873a985ae482c68a Mon Sep 17 00:00:00 2001 From: "alexei.dolgolyov" Date: Sun, 10 May 2026 22:39:18 +0300 Subject: [PATCH] fix(shutdown): apply target stop actions before tearing down HA/MQTT MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reorder the lifespan shutdown so processor_manager.stop_all() runs before ha_manager.shutdown(), mqtt_manager.shutdown(), and mqtt_service.stop(). HA-light targets check `_ha_runtime.is_connected` before applying their `stop_action` (turn_off / restore) and silently skip when HA is already disconnected; MQTT-output devices need the broker connection alive to send restore frames. The previous order tore those down first, turning "stop_targets" into a no-op for those targets — most visible when closing via the tray Shutdown button. Also moves automation_engine.stop(), discovery_watcher.stop(), and the OS notification listener stop ahead of processor stop so they can no longer fire events into a shutting-down processor manager. Independent services (weather, update checker, auto-backup) now run last, where their order does not matter. Bonus: if the daemon-thread join times out (10 s) and the rest of shutdown is cut short, the user-visible part — targets stopping — has already run. --- server/src/ledgrab/main.py | 64 ++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 31 deletions(-) diff --git a/server/src/ledgrab/main.py b/server/src/ledgrab/main.py index 5a39a60..54bd8e3 100644 --- a/server/src/ledgrab/main.py +++ b/server/src/ledgrab/main.py @@ -394,52 +394,34 @@ async def lifespan(app: FastAPI): # where no CRUD happened during the session. _save_all_stores() - # Stop Home Assistant manager - try: - await ha_manager.shutdown() - except Exception as e: - logger.error(f"Error stopping Home Assistant manager: {e}") - - # Stop weather manager - try: - weather_manager.shutdown() - except Exception as e: - logger.error(f"Error stopping weather manager: {e}") - - # Stop update checker - try: - await update_service.stop() - except Exception as e: - logger.error(f"Error stopping update checker: {e}") - - # Stop auto-backup engine - try: - await auto_backup_engine.stop() - except Exception as e: - logger.error(f"Error stopping auto-backup engine: {e}") - - # Stop automation engine first (deactivates automation-managed scenes) + # Stop automation engine first so it can no longer activate scenes that + # would talk to processors mid-shutdown. try: await automation_engine.stop() logger.info("Stopped automation engine") except Exception as e: logger.error(f"Error stopping automation engine: {e}") - # Stop discovery watcher (before health monitor stop so events still flow) + # Stop discovery watcher and OS notification listener so they stop + # firing events into a shutting-down processor manager. if discovery_watcher is not None: try: await discovery_watcher.stop() except Exception as e: logger.error(f"Error stopping discovery watcher: {e}") - # Stop OS notification listener try: os_notif_listener.stop() except Exception as e: logger.error(f"Error stopping OS notification listener: {e}") - # Stop all processing. - # The shutdown action setting controls whether per-device restore + # Stop all processing BEFORE tearing down ha_manager / mqtt_manager / + # mqtt_service. HA-light targets need a live HA runtime to apply their + # stop_action (turn_off / restore), and MQTT-output devices need a live + # MQTT broker connection to send restore frames. Shutting those down + # first silently turns "stop_targets" into a no-op for those targets. + # + # The shutdown_action setting controls whether per-device restore # frames are sent: "stop_targets" (default) runs the normal stop # sequence; "nothing" cancels capture tasks so the LEDs freeze on # their last frame. @@ -458,18 +440,38 @@ async def lifespan(app: FastAPI): except Exception as e: logger.error(f"Error stopping processors: {e}") - # Stop MQTT manager (entity-based broker connections) + # Now safe to tear down the connections that processors depended on. + try: + await ha_manager.shutdown() + except Exception as e: + logger.error(f"Error stopping Home Assistant manager: {e}") + try: await mqtt_manager.shutdown() except Exception as e: logger.error(f"Error stopping MQTT manager: {e}") - # Stop MQTT service (legacy global connection) try: await mqtt_service.stop() except Exception as e: logger.error(f"Error stopping MQTT service: {e}") + # Independent services — order doesn't matter relative to processors. + try: + weather_manager.shutdown() + except Exception as e: + logger.error(f"Error stopping weather manager: {e}") + + try: + await update_service.stop() + except Exception as e: + logger.error(f"Error stopping update checker: {e}") + + try: + await auto_backup_engine.stop() + except Exception as e: + logger.error(f"Error stopping auto-backup engine: {e}") + # Create FastAPI application app = FastAPI(