فهرست منبع

Fix Still Sands LED control: pause loops, clear pattern flicker, idle monitor

- Monitor Still Sands transitions during inter-pattern pauses so LEDs turn
  off when a quiet period starts mid-pause (e.g. 24h pause time)
- Skip idle LED trigger after clear pattern to prevent ~0.3s flicker
- Add background monitor to turn off LEDs when table is idle and Still
  Sands period begins (no playlist running)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
tuanchris 4 ماه پیش
والد
کامیت
f92088a0ce
2فایلهای تغییر یافته به همراه99 افزوده شده و 7 حذف شده
  1. 56 0
      main.py
  2. 43 7
      modules/core/pattern_manager.py

+ 56 - 0
main.py

@@ -270,6 +270,62 @@ async def lifespan(app: FastAPI):
 
     asyncio.create_task(idle_timeout_monitor())
 
+    # Start Still Sands LED monitor for idle table
+    async def still_sands_led_monitor():
+        """Monitor Still Sands transitions when the table is idle and no playlist is running.
+
+        Handles the case where a Still Sands period starts/ends while the table is completely
+        idle (no pattern or playlist active). Without this, LEDs would stay on all night
+        if the table was idle when the quiet period began.
+        """
+        import time
+        from modules.core.pattern_manager import is_in_scheduled_pause_period, start_idle_led_timeout
+        from modules.led.idle_timeout_manager import idle_timeout_manager
+
+        was_in_still_sands = False
+        while True:
+            try:
+                await asyncio.sleep(30)  # Check every 30 seconds
+
+                # Skip if LED control during Still Sands is disabled
+                if not state.scheduled_pause_control_wled:
+                    was_in_still_sands = False
+                    continue
+
+                # Skip if no LED controller configured
+                if not state.led_controller or not state.led_controller.is_configured:
+                    was_in_still_sands = False
+                    continue
+
+                # Skip if a pattern or playlist is actively running —
+                # the pattern_manager handles Still Sands in that case
+                is_playing = bool(state.current_playing_file or state.current_playlist)
+                if is_playing:
+                    was_in_still_sands = False
+                    continue
+
+                in_still_sands = is_in_scheduled_pause_period()
+
+                if in_still_sands and not was_in_still_sands:
+                    # Entering Still Sands while idle — turn off LEDs
+                    status = state.led_controller.check_status()
+                    is_powered_on = status.get("power", False) or status.get("power_on", False)
+                    if is_powered_on:
+                        logger.info("Still Sands period started while idle, turning off LEDs")
+                        state.led_controller.set_power(0)
+                elif not in_still_sands and was_in_still_sands:
+                    # Leaving Still Sands while idle — restore idle effect and restart timeout
+                    logger.info("Still Sands period ended while idle, restoring idle LED effect")
+                    await start_idle_led_timeout(check_still_sands=False)
+
+                was_in_still_sands = in_still_sands
+
+            except Exception as e:
+                logger.error(f"Error in Still Sands LED monitor: {e}")
+                await asyncio.sleep(60)  # Wait longer on error
+
+    asyncio.create_task(still_sands_led_monitor())
+
     yield  # This separates startup from shutdown code
 
     # Shutdown

+ 43 - 7
modules/core/pattern_manager.py

@@ -1443,7 +1443,9 @@ async def _execute_pattern_internal(file_path):
 
     # Set LED back to idle when pattern completes normally (not stopped early)
     # This also handles Still Sands: turns off LEDs if in scheduled pause period with LED control
-    if not state.stop_requested:
+    # Skip during clear pattern - the main pattern starts immediately after, so triggering
+    # idle LED here would cause a brief flicker (idle effect → playing effect in ~0.3s)
+    if not state.stop_requested and not state.is_clearing:
         await start_idle_led_timeout()
 
     return was_completed
@@ -1632,14 +1634,31 @@ async def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_
                     # This will be set again when the next pattern starts
                     state.current_playing_file = None
                     # Trigger idle LED state during pause between patterns
-                    await start_idle_led_timeout(check_still_sands=False)
+                    await start_idle_led_timeout(check_still_sands=True)
                     state.original_pause_time = pause_time
                     pause_start = time.time()
+                    # Track Still Sands state for edge detection during long pauses
+                    was_in_still_sands = is_in_scheduled_pause_period() and state.scheduled_pause_control_wled
                     while time.time() - pause_start < pause_time:
                         state.pause_time_remaining = pause_start + pause_time - time.time()
-                        if state.skip_requested:
-                            logger.info("Pause interrupted by skip request")
+                        if state.skip_requested or state.stop_requested:
+                            if state.stop_requested:
+                                logger.info("Pause interrupted by stop request")
+                            else:
+                                logger.info("Pause interrupted by skip request")
                             break
+                        # Monitor Still Sands transitions during pause
+                        in_still_sands = is_in_scheduled_pause_period() and state.scheduled_pause_control_wled
+                        if in_still_sands and not was_in_still_sands:
+                            # Entering Still Sands period — turn off LEDs
+                            logger.info("Still Sands period started during pause, turning off LEDs")
+                            if state.led_controller:
+                                await state.led_controller.set_power_async(0)
+                        elif not in_still_sands and was_in_still_sands:
+                            # Leaving Still Sands period — restore idle effect
+                            logger.info("Still Sands period ended during pause, restoring idle LED effect")
+                            await start_idle_led_timeout(check_still_sands=False)
+                        was_in_still_sands = in_still_sands
                         await asyncio.sleep(1)
                     # Clear both pause state vars immediately (so UI updates right away)
                     state.pause_time_remaining = 0
@@ -1671,14 +1690,31 @@ async def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_
                     # Clear current_playing_file to report "idle" state to MQTT/HA during pause
                     state.current_playing_file = None
                     # Trigger idle LED state during pause between playlist cycles
-                    await start_idle_led_timeout(check_still_sands=False)
+                    await start_idle_led_timeout(check_still_sands=True)
                     state.original_pause_time = pause_time
                     pause_start = time.time()
+                    # Track Still Sands state for edge detection during long pauses
+                    was_in_still_sands = is_in_scheduled_pause_period() and state.scheduled_pause_control_wled
                     while time.time() - pause_start < pause_time:
                         state.pause_time_remaining = pause_start + pause_time - time.time()
-                        if state.skip_requested:
-                            logger.info("Pause interrupted by skip request")
+                        if state.skip_requested or state.stop_requested:
+                            if state.stop_requested:
+                                logger.info("Pause interrupted by stop request")
+                            else:
+                                logger.info("Pause interrupted by skip request")
                             break
+                        # Monitor Still Sands transitions during pause
+                        in_still_sands = is_in_scheduled_pause_period() and state.scheduled_pause_control_wled
+                        if in_still_sands and not was_in_still_sands:
+                            # Entering Still Sands period — turn off LEDs
+                            logger.info("Still Sands period started during pause, turning off LEDs")
+                            if state.led_controller:
+                                await state.led_controller.set_power_async(0)
+                        elif not in_still_sands and was_in_still_sands:
+                            # Leaving Still Sands period — restore idle effect
+                            logger.info("Still Sands period ended during pause, restoring idle LED effect")
+                            await start_idle_led_timeout(check_still_sands=False)
+                        was_in_still_sands = in_still_sands
                         await asyncio.sleep(1)
                     # Clear both pause state vars immediately (so UI updates right away)
                     state.pause_time_remaining = 0