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(