idle_timeout_manager.py 4.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113
  1. """
  2. Idle LED Timeout Manager
  3. Handles automatic LED turn-off after a period of inactivity.
  4. """
  5. import asyncio
  6. import logging
  7. from datetime import datetime
  8. from typing import Optional
  9. logger = logging.getLogger(__name__)
  10. class IdleTimeoutManager:
  11. """
  12. Manages idle timeout for LED effects.
  13. When idle effect is played, starts a timer. When timer expires,
  14. checks if table is still idle and turns off LEDs if so.
  15. """
  16. def __init__(self):
  17. self._timeout_task: Optional[asyncio.Task] = None
  18. self._last_idle_time: Optional[datetime] = None
  19. def start_idle_timeout(self, timeout_minutes: float, state, check_idle_callback):
  20. """
  21. Start or restart the idle timeout timer.
  22. Args:
  23. timeout_minutes: Minutes to wait before turning off LEDs
  24. state: Application state object
  25. check_idle_callback: Async callback to check if table is still idle
  26. """
  27. # Cancel any existing timeout
  28. self.cancel_timeout()
  29. if timeout_minutes <= 0:
  30. logger.debug("Idle timeout disabled (timeout <= 0)")
  31. return
  32. # Record when idle effect was started
  33. self._last_idle_time = datetime.now()
  34. logger.info(f"Starting idle LED timeout: {timeout_minutes} minutes")
  35. # Create background task to handle timeout
  36. # Handle being called from a thread without an event loop (e.g., via asyncio.to_thread)
  37. try:
  38. loop = asyncio.get_running_loop()
  39. self._timeout_task = loop.create_task(
  40. self._timeout_handler(timeout_minutes, state, check_idle_callback)
  41. )
  42. except RuntimeError:
  43. # No running event loop in this thread - try to get the main loop
  44. try:
  45. loop = asyncio.get_event_loop()
  46. if loop.is_running():
  47. # Schedule on the running loop from another thread
  48. asyncio.run_coroutine_threadsafe(
  49. self._timeout_handler(timeout_minutes, state, check_idle_callback),
  50. loop
  51. )
  52. logger.debug("Scheduled idle timeout on main event loop from thread")
  53. else:
  54. logger.warning("Event loop exists but not running, cannot start idle timeout")
  55. except Exception as e:
  56. logger.warning(f"Could not start idle timeout: {e}")
  57. async def _timeout_handler(self, timeout_minutes: float, state, check_idle_callback):
  58. """
  59. Background task that waits for timeout and turns off LEDs if still idle.
  60. """
  61. try:
  62. # Wait for the specified timeout
  63. timeout_seconds = timeout_minutes * 60
  64. await asyncio.sleep(timeout_seconds)
  65. # Check if we should turn off the LEDs
  66. logger.debug("Idle timeout expired, checking table state...")
  67. # Check if table is still idle (not playing anything)
  68. is_idle = await check_idle_callback()
  69. if is_idle:
  70. logger.info("Table is still idle after timeout - turning off LEDs")
  71. if state.led_controller:
  72. try:
  73. state.led_controller.set_power(0) # Turn off LEDs
  74. logger.info("LEDs turned off successfully")
  75. except Exception as e:
  76. logger.error(f"Failed to turn off LEDs: {e}")
  77. else:
  78. logger.warning("LED controller not configured")
  79. else:
  80. logger.debug("Table is not idle - skipping LED turn-off")
  81. except asyncio.CancelledError:
  82. logger.debug("Idle timeout cancelled")
  83. except Exception as e:
  84. logger.error(f"Error in idle timeout handler: {e}")
  85. def cancel_timeout(self):
  86. """Cancel any running timeout task."""
  87. if self._timeout_task and not self._timeout_task.done():
  88. logger.debug("Cancelling existing idle timeout")
  89. self._timeout_task.cancel()
  90. self._timeout_task = None
  91. def is_timeout_active(self) -> bool:
  92. """Check if a timeout is currently active."""
  93. return self._timeout_task is not None and not self._timeout_task.done()
  94. # Singleton instance
  95. idle_timeout_manager = IdleTimeoutManager()