Jelajahi Sumber

port remaing changes

Tuan Nguyen 11 bulan lalu
induk
melakukan
5f73a8cd0d

+ 8 - 15
app.py

@@ -9,7 +9,7 @@ from modules.core import playlist_manager
 from modules.update import update_manager
 from modules.update import update_manager
 from modules.core.state import state
 from modules.core.state import state
 from modules import mqtt
 from modules import mqtt
-
+from modules.led.led_controller import LEDController, effect_idle
 
 
 # Configure logging
 # Configure logging
 logging.basicConfig(
 logging.basicConfig(
@@ -363,22 +363,9 @@ def run_playlist():
     run_mode = data.get("run_mode", "single")
     run_mode = data.get("run_mode", "single")
     shuffle = data.get("shuffle", False)
     shuffle = data.get("shuffle", False)
     
     
-    schedule_hours = None
     start_time = data.get("start_time")
     start_time = data.get("start_time")
     end_time = data.get("end_time")
     end_time = data.get("end_time")
     
     
-    if start_time and end_time:
-        try:
-            start_time_obj = datetime.strptime(start_time, "%H:%M").time()
-            end_time_obj = datetime.strptime(end_time, "%H:%M").time()
-            if start_time_obj >= end_time_obj:
-                logger.error(f"Invalid schedule times: start_time {start_time} >= end_time {end_time}")
-                return jsonify({"success": False, "error": "'start_time' must be earlier than 'end_time'"}), 400
-            schedule_hours = (start_time_obj, end_time_obj)
-            logger.info(f"Playlist {playlist_name} scheduled to run between {start_time} and {end_time}")
-        except ValueError:
-            logger.error(f"Invalid time format provided: start_time={start_time}, end_time={end_time}")
-            return jsonify({"success": False, "error": "Invalid time format. Use HH:MM (e.g., '09:30')"}), 400
 
 
     logger.info(f"Starting playlist '{playlist_name}' with mode={run_mode}, shuffle={shuffle}")
     logger.info(f"Starting playlist '{playlist_name}' with mode={run_mode}, shuffle={shuffle}")
     success, message = playlist_manager.run_playlist(
     success, message = playlist_manager.run_playlist(
@@ -387,7 +374,6 @@ def run_playlist():
         clear_pattern=clear_pattern,
         clear_pattern=clear_pattern,
         run_mode=run_mode,
         run_mode=run_mode,
         shuffle=shuffle,
         shuffle=shuffle,
-        schedule_hours=schedule_hours
     )
     )
 
 
     if not success:
     if not success:
@@ -464,6 +450,13 @@ def get_wled_ip():
 
 
     return jsonify({"success": True, "wled_ip": state.wled_ip})
     return jsonify({"success": True, "wled_ip": state.wled_ip})
 
 
+@app.route('/skip_pattern', methods=['POST'])
+def skip_pattern():
+    if not state.current_playlist:
+        return jsonify({"success": False, "error": "No playlist is currently running"})
+    state.skip_requested = True
+    return jsonify({"success": True})
+
 def on_exit():
 def on_exit():
     """Function to execute on application shutdown."""
     """Function to execute on application shutdown."""
     logger.info("Shutting down gracefully, please wait for execution to complete")
     logger.info("Shutting down gracefully, please wait for execution to complete")

+ 32 - 12
modules/connection/connection_manager.py

@@ -7,7 +7,7 @@ import websocket
 
 
 from modules.core.state import state
 from modules.core.state import state
 from modules.core.pattern_manager import move_polar, reset_theta
 from modules.core.pattern_manager import move_polar, reset_theta
-
+from modules.led.led_controller import effect_loading, effect_idle, effect_connected, effect_error, LEDController
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 IGNORE_PORTS = ['/dev/cu.debug-console', '/dev/cu.Bluetooth-Incoming-Port']
 IGNORE_PORTS = ['/dev/cu.debug-console', '/dev/cu.Bluetooth-Incoming-Port']
@@ -76,6 +76,8 @@ class SerialConnection(BaseConnection):
         with self.lock:
         with self.lock:
             if self.ser.is_open:
             if self.ser.is_open:
                 self.ser.close()
                 self.ser.close()
+        # Release the lock resources
+        self.lock = None
 
 
 ###############################################################################
 ###############################################################################
 # WebSocket Connection Implementation
 # WebSocket Connection Implementation
@@ -122,6 +124,8 @@ class WebSocketConnection(BaseConnection):
         with self.lock:
         with self.lock:
             if self.ws:
             if self.ws:
                 self.ws.close()
                 self.ws.close()
+        # Release the lock resources
+        self.lock = None
                 
                 
 def list_serial_ports():
 def list_serial_ports():
     """Return a list of available serial ports."""
     """Return a list of available serial ports."""
@@ -152,15 +156,12 @@ def device_init(homing=True):
 
 
     time.sleep(2)  # Allow time for the connection to establish
     time.sleep(2)  # Allow time for the connection to establish
 
 
-    try:
-        if get_machine_steps():
-            logger.info(f"x_steps_per_mm: {state.x_steps_per_mm}, x_steps_per_mm: {state.y_steps_per_mm}, gear_ratio: {state.gear_ratio}")
-            return True
-    except:
-        logger.fatal("Not GRBL firmware")
-        return False
 
 
 def connect_device(homing=True):
 def connect_device(homing=True):
+    if state.wled_ip:
+        state.led_controller = LEDController(state.wled_ip)
+        effect_loading(state.led_controller)
+        
     ports = list_serial_ports()
     ports = list_serial_ports()
 
 
     if state.port and state.port in ports:
     if state.port and state.port in ports:
@@ -173,6 +174,9 @@ def connect_device(homing=True):
         return
         return
     if (state.conn.is_connected() if state.conn else False):
     if (state.conn.is_connected() if state.conn else False):
         device_init(homing)
         device_init(homing)
+        
+    if state.led_controller:
+        effect_connected(state.led_controller)
 
 
 def get_status_response() -> str:
 def get_status_response() -> str:
     """
     """
@@ -271,6 +275,16 @@ def get_machine_steps(timeout=10):
             
             
             # If all parameters are received, exit early
             # If all parameters are received, exit early
             if x_steps_per_mm is not None and y_steps_per_mm is not None and gear_ratio is not None:
             if x_steps_per_mm is not None and y_steps_per_mm is not None and gear_ratio is not None:
+                if y_steps_per_mm == 180:
+                    state.table_type = 'dune_weaver_mini'
+                elif y_steps_per_mm == 320:
+                    state.table_type = 'dune_weaver_pro'
+                elif y_steps_per_mm == 287:
+                    state.table_type = 'dune_weaver'
+                else:
+                    state.table_type = None
+                    logger.warning(f"Unknown table type. Check connection_manager.py")
+                logger.info(f"Machine type: {state.table_type}")
                 return True
                 return True
 
 
         except Exception as e:
         except Exception as e:
@@ -292,13 +306,16 @@ def home():
         state.conn.send("$H\n")
         state.conn.send("$H\n")
         state.conn.send("G1 Y0 F100\n")
         state.conn.send("G1 Y0 F100\n")
     else:
     else:
+        homing_speed = 400
+        if state.table_type == 'dune_weaver_mini':
+            homing_speed = 120
         logger.info("Sensorless homing not supported. Using crash homing")
         logger.info("Sensorless homing not supported. Using crash homing")
-        logger.info(f"Homing with speed {state.speed}")
+        logger.info(f"Homing with speed {homing_speed}")
         if state.gear_ratio == 6.25:
         if state.gear_ratio == 6.25:
-            send_grbl_coordinates(0, - 30, state.speed, home=True)
+            send_grbl_coordinates(0, - 30, homing_speed, home=True)
             state.machine_y -= 30
             state.machine_y -= 30
         else:
         else:
-            send_grbl_coordinates(0, -22, state.speed, home=True)
+            send_grbl_coordinates(0, -22, homing_speed, home=True)
             state.machine_y -= 22
             state.machine_y -= 22
 
 
     state.current_theta = state.current_rho = 0
     state.current_theta = state.current_rho = 0
@@ -315,7 +332,7 @@ def check_idle():
             update_machine_position()
             update_machine_position()
             return True
             return True
         time.sleep(1)
         time.sleep(1)
-
+        
 
 
 def get_machine_position(timeout=5):
 def get_machine_position(timeout=5):
     """
     """
@@ -345,6 +362,7 @@ def update_machine_position():
         logger.info('Saving machine position')
         logger.info('Saving machine position')
         state.machine_x, state.machine_y = get_machine_position()
         state.machine_x, state.machine_y = get_machine_position()
         state.save()
         state.save()
+        logger.info(f'Machine position saved: {state.machine_x}, {state.machine_y}')
     
     
 def restart_connection(homing=False):
 def restart_connection(homing=False):
     """
     """
@@ -376,4 +394,6 @@ def restart_connection(homing=False):
             return False
             return False
     except Exception as e:
     except Exception as e:
         logger.error(f"Error restarting connection: {e}")
         logger.error(f"Error restarting connection: {e}")
+        if state.led_controller:
+            effect_error(state.led_controller)
         return False
         return False

+ 141 - 105
modules/core/pattern_manager.py

@@ -8,17 +8,13 @@ from tqdm import tqdm
 from modules.connection import connection_manager
 from modules.connection import connection_manager
 from modules.core.state import state
 from modules.core.state import state
 from math import pi
 from math import pi
+from modules.led.led_controller import effect_playing, effect_idle
 
 
 # Configure logging
 # Configure logging
 logger = logging.getLogger(__name__)
 logger = logging.getLogger(__name__)
 
 
 # Global state
 # Global state
 THETA_RHO_DIR = './patterns'
 THETA_RHO_DIR = './patterns'
-CLEAR_PATTERNS = {
-    "clear_from_in":  "./patterns/clear_from_in.thr",
-    "clear_from_out": "./patterns/clear_from_out.thr",
-    "clear_sideway":  "./patterns/clear_sideway.thr"
-}
 os.makedirs(THETA_RHO_DIR, exist_ok=True)
 os.makedirs(THETA_RHO_DIR, exist_ok=True)
 
 
 def list_theta_rho_files():
 def list_theta_rho_files():
@@ -60,58 +56,56 @@ def parse_theta_rho_file(file_path):
     return coordinates
     return coordinates
 
 
 def get_clear_pattern_file(clear_pattern_mode, path=None):
 def get_clear_pattern_file(clear_pattern_mode, path=None):
-    """Return a .thr file path based on pattern_name."""
+    """Return a .thr file path based on pattern_name and table type."""
     if not clear_pattern_mode or clear_pattern_mode == 'none':
     if not clear_pattern_mode or clear_pattern_mode == 'none':
         return
         return
-    logger.info("Clear pattern mode: " + clear_pattern_mode)
+    
+    # Define patterns for each table type
+    clear_patterns = {
+        'dune_weaver': {
+            'clear_from_out': './patterns/clear_from_out.thr',
+            'clear_from_in': './patterns/clear_from_in.thr',
+            'clear_sideway': './patterns/clear_sideway.thr'
+        },
+        'dune_weaver_mini': {
+            'clear_from_out': './patterns/clear_from_out_mini.thr',
+            'clear_from_in': './patterns/clear_from_in_mini.thr',
+            'clear_sideway': './patterns/clear_sideway_mini.thr'
+        },
+        'dune_weaver_pro': {
+            'clear_from_out': './patterns/clear_from_out_pro.thr',
+            'clear_from_in': './patterns/clear_from_in_pro.thr',
+            'clear_sideway': './patterns/clear_sideway_pro.thr'
+        }
+    }
+    
+    # Get patterns for current table type, fallback to standard patterns if type not found
+    table_patterns = clear_patterns.get(state.table_type, clear_patterns['dune_weaver'])
+    
+    logger.debug(f"Clear pattern mode: {clear_pattern_mode} for table type: {state.table_type}")
+    
     if clear_pattern_mode == "random":
     if clear_pattern_mode == "random":
-        return random.choice(list(CLEAR_PATTERNS.values()))
+        return random.choice(list(table_patterns.values()))
 
 
     if clear_pattern_mode == 'adaptive':
     if clear_pattern_mode == 'adaptive':
-        _, first_rho = parse_theta_rho_file(path)[0]
+        if not path:
+            logger.warning("No path provided for adaptive clear pattern")
+            return random.choice(list(table_patterns.values()))
+            
+        coordinates = parse_theta_rho_file(path)
+        if not coordinates:
+            logger.warning("No valid coordinates found in file for adaptive clear pattern")
+            return random.choice(list(table_patterns.values()))
+            
+        first_rho = coordinates[0][1]
         if first_rho < 0.5:
         if first_rho < 0.5:
-            return CLEAR_PATTERNS['clear_from_out']
+            return table_patterns['clear_from_out']
         else:
         else:
-            return random.choice([CLEAR_PATTERNS['clear_from_in'], CLEAR_PATTERNS['clear_sideway']])
-    else:
-        return CLEAR_PATTERNS[clear_pattern_mode]
-
-def schedule_checker(schedule_hours):
-    """Pauses/resumes execution based on a given time range."""
-    if not schedule_hours:
-        return
-
-    start_time, end_time = schedule_hours
-    now = datetime.now().time()
-
-    if start_time <= now < end_time:
-        if state.pause_requested:
-            logger.info("Starting execution: Within schedule")
-            connection_manager.update_machine_position()
-        state.pause_requested = False
-        with state.pause_condition:
-            state.pause_condition.notify_all()
+            return random.choice([table_patterns['clear_from_in'], table_patterns['clear_sideway']])
     else:
     else:
-        if not state.pause_requested:
-            logger.info("Pausing execution: Outside schedule")
-        state.pause_requested = True
-        connection_manager.update_machine_position()
-        threading.Thread(target=wait_for_start_time, args=(schedule_hours,), daemon=True).start()
-
-def wait_for_start_time(schedule_hours):
-    """Keep checking every 30 seconds if the time is within the schedule to resume execution."""
-    start_time, end_time = schedule_hours
-
-    while state.pause_requested:
-        now = datetime.now().time()
-        if start_time <= now < end_time:
-            logger.info("Resuming execution: Within schedule")
-            state.pause_requested = False
-            with state.pause_condition:
-                state.pause_condition.notify_all()
-            break
-        else:
-            time.sleep(30)
+        if clear_pattern_mode not in table_patterns:
+            return False
+        return table_patterns[clear_pattern_mode]
             
             
             
             
 def move_polar(theta, rho):
 def move_polar(theta, rho):
@@ -193,7 +187,7 @@ def set_speed(new_speed):
     state.speed = new_speed
     state.speed = new_speed
     logger.info(f'Set new state.speed {new_speed}')
     logger.info(f'Set new state.speed {new_speed}')
 
 
-def run_theta_rho_file(file_path, schedule_hours=None):
+def run_theta_rho_file(file_path):
     """Run a theta-rho file by sending data in optimized batches with tqdm ETA tracking."""
     """Run a theta-rho file by sending data in optimized batches with tqdm ETA tracking."""
     
     
     # Check if connection is still valid, if not, restart
     # Check if connection is still valid, if not, restart
@@ -226,6 +220,11 @@ def run_theta_rho_file(file_path, schedule_hours=None):
     logger.info(f"Starting pattern execution: {file_path}")
     logger.info(f"Starting pattern execution: {file_path}")
     logger.info(f"t: {state.current_theta}, r: {state.current_rho}")
     logger.info(f"t: {state.current_theta}, r: {state.current_rho}")
     reset_theta()
     reset_theta()
+    
+    if state.led_controller:
+            effect_playing(state.led_controller)
+            
+    
     with tqdm(
     with tqdm(
         total=total_coordinates,
         total=total_coordinates,
         unit="coords",
         unit="coords",
@@ -237,15 +236,28 @@ def run_theta_rho_file(file_path, schedule_hours=None):
         for i, coordinate in enumerate(coordinates):
         for i, coordinate in enumerate(coordinates):
             theta, rho = coordinate
             theta, rho = coordinate
             if state.stop_requested:
             if state.stop_requested:
-                logger.info("Execution stopped by user after completing the current batch")
+                logger.info("Execution stopped by user")
+                if state.led_controller:
+                    effect_idle(state.led_controller)
+                break
+            
+            if state.skip_requested:
+                logger.info("Skipping pattern...")
+                connection_manager.check_idle()
+                if state.led_controller:
+                    effect_idle(state.led_controller)
                 break
                 break
 
 
-            with state.pause_condition:
-                while state.pause_requested:
-                    logger.info("Execution paused...")
-                    state.pause_condition.wait()
+            # Wait for resume if paused
+            if state.pause_requested:
+                logger.info("Execution paused...")
+                if state.led_controller:
+                    effect_idle(state.led_controller)
+                pause_event.wait()
+                logger.info("Execution resumed...")
+                if state.led_controller:
+                    effect_playing(state.led_controller)
 
 
-            schedule_checker(schedule_hours)
             move_polar(theta, rho)
             move_polar(theta, rho)
             
             
             if i != 0:
             if i != 0:
@@ -260,71 +272,95 @@ def run_theta_rho_file(file_path, schedule_hours=None):
     state.execution_progress = None
     state.execution_progress = None
     logger.info("Pattern execution completed")
     logger.info("Pattern execution completed")
 
 
-def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_mode="single", shuffle=False, schedule_hours=None):
+def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_mode="single", shuffle=False):
     """Run multiple .thr files in sequence with options."""
     """Run multiple .thr files in sequence with options."""
     state.stop_requested = False
     state.stop_requested = False
     
     
     # Set initial playlist state
     # Set initial playlist state
     state.playlist_mode = run_mode
     state.playlist_mode = run_mode
     state.current_playlist_index = 0
     state.current_playlist_index = 0
-    
-    if shuffle:
-        random.shuffle(file_paths)
-        logger.info("Playlist shuffled")
 
 
-    while True:
-        for idx, path in enumerate(file_paths):
-            logger.info(f"Upcoming pattern: {path}")
-            state.current_playlist_index = idx
-            schedule_checker(schedule_hours)
-            if state.stop_requested:
-                logger.info("Execution stopped before starting next pattern")
-                state.current_playlist = None
-                state.current_playlist_index = None
-                state.playlist_mode = None
-                return
+    try:
+        while True:
+            # Construct the complete pattern sequence
+            pattern_sequence = []
+            for path in file_paths:
+                # Add clear pattern if specified
+                if clear_pattern and clear_pattern != 'none':
+                    clear_file_path = get_clear_pattern_file(clear_pattern, path)
+                    if clear_file_path:
+                        pattern_sequence.append(clear_file_path)
+                
+                # Add main pattern
+                pattern_sequence.append(path)
 
 
-            if clear_pattern:
+            # Shuffle if requested
+            if shuffle:
+                # Get pairs of patterns (clear + main) to keep them together
+                pairs = [pattern_sequence[i:i+2] for i in range(0, len(pattern_sequence), 2)]
+                random.shuffle(pairs)
+                # Flatten the pairs back into a single list
+                pattern_sequence = [pattern for pair in pairs for pattern in pair]
+                logger.info("Playlist shuffled")
+
+            # Set the playlist to the first pattern
+            state.current_playlist = pattern_sequence
+            # Execute the pattern sequence
+            for idx, file_path in enumerate(pattern_sequence):
+                state.current_playlist_index = idx
                 if state.stop_requested:
                 if state.stop_requested:
-                    logger.info("Execution stopped before running the next clear pattern")
-                    state.current_playlist = None
-                    state.current_playlist_index = None
-                    state.playlist_mode = None
+                    logger.info("Execution stopped")
                     return
                     return
 
 
-                clear_file_path = get_clear_pattern_file(clear_pattern, path)
-                logger.info(f"Running clear pattern: {clear_file_path}")
-                run_theta_rho_file(clear_file_path, schedule_hours)
-
-            if not state.stop_requested:
+                # Update state for main patterns only
+                logger.info(f"Running pattern {file_path}")
                 
                 
-                logger.info(f"Running pattern {idx + 1} of {len(file_paths)}: {path}")
-                run_theta_rho_file(path, schedule_hours)
+                # Execute the pattern
+                run_theta_rho_file(file_path)
 
 
-            if idx < len(file_paths) - 1:
-                if state.stop_requested:
-                    logger.info("Execution stopped before running the next clear pattern")
-                    return
-                if pause_time > 0:
+                # Handle pause between patterns
+                if idx < len(pattern_sequence) - 1 and not state.stop_requested and pause_time > 0 and not state.skip_requested:
                     logger.info(f"Pausing for {pause_time} seconds")
                     logger.info(f"Pausing for {pause_time} seconds")
+                    pause_start = time.time()
+                    while time.time() - pause_start < pause_time:
+                        if state.skip_requested:
+                            logger.info("Pause interrupted by stop/skip request")
+                            break
+                        time.sleep(1)
+                    
+                state.skip_requested = False
+
+            if run_mode == "indefinite":
+                logger.info("Playlist completed. Restarting as per 'indefinite' run mode")
+                if pause_time > 0:
+                    logger.debug(f"Pausing for {pause_time} seconds before restarting")
                     time.sleep(pause_time)
                     time.sleep(pause_time)
+                continue
+            else:
+                logger.info("Playlist completed")
+                break
 
 
-        if run_mode == "indefinite":
-            logger.info("Playlist completed. Restarting as per 'indefinite' run mode")
-            if pause_time > 0:
-                logger.debug(f"Pausing for {pause_time} seconds before restarting")
-                time.sleep(pause_time)
-            if shuffle:
-                random.shuffle(file_paths)
-                logger.info("Playlist reshuffled for the next loop")
-            continue
-        else:
-            logger.info("Playlist completed")
-            state.current_playlist = None
-            state.current_playlist_index = None
-            state.playlist_mode = None
-            break
-    logger.info("All requested patterns completed (or stopped)")
+    finally:
+        # Clean up progress update task
+        # if progress_update_task:
+        #     progress_update_task.cancel()
+        #     try:
+        #         await progress_update_task
+        #     except asyncio.CancelledError:
+        #         pass
+        #     progress_update_task = None
+            
+        # Clear all state variables
+        state.current_playing_file = None
+        state.execution_progress = None
+        state.current_playlist = None
+        state.current_playlist_index = None
+        state.playlist_mode = None
+        
+        if state.led_controller:
+            effect_idle(state.led_controller)
+        
+        logger.info("All requested patterns completed (or stopped) and state cleared")
 
 
 def stop_actions(clear_playlist = True):
 def stop_actions(clear_playlist = True):
     """Stop all current actions."""
     """Stop all current actions."""

+ 1 - 2
modules/core/playlist_manager.py

@@ -84,7 +84,7 @@ def add_to_playlist(playlist_name, pattern):
     logger.info(f"Added pattern '{pattern}' to playlist '{playlist_name}'")
     logger.info(f"Added pattern '{pattern}' to playlist '{playlist_name}'")
     return True
     return True
 
 
-def run_playlist(playlist_name, pause_time=0, clear_pattern=None, run_mode="single", shuffle=False, schedule_hours=None):
+def run_playlist(playlist_name, pause_time=0, clear_pattern=None, run_mode="single", shuffle=False):
     """Run a playlist with the given options."""
     """Run a playlist with the given options."""
     playlists = load_playlists()
     playlists = load_playlists()
     if playlist_name not in playlists:
     if playlist_name not in playlists:
@@ -109,7 +109,6 @@ def run_playlist(playlist_name, pause_time=0, clear_pattern=None, run_mode="sing
                 'clear_pattern': clear_pattern,
                 'clear_pattern': clear_pattern,
                 'run_mode': run_mode,
                 'run_mode': run_mode,
                 'shuffle': shuffle,
                 'shuffle': shuffle,
-                'schedule_hours': schedule_hours
             },
             },
             daemon=True
             daemon=True
         ).start()
         ).start()

+ 57 - 4
modules/core/state.py

@@ -2,6 +2,9 @@
 import threading
 import threading
 import json
 import json
 import os
 import os
+import logging
+
+logger = logging.getLogger(__name__)
 
 
 class AppState:
 class AppState:
     def __init__(self):
     def __init__(self):
@@ -10,6 +13,7 @@ class AppState:
         self._pause_requested = False
         self._pause_requested = False
         self._speed = 150
         self._speed = 150
         self._current_playlist = None
         self._current_playlist = None
+        self._current_playlist_name = None  # New variable for playlist name
         
         
         # Regular state variables
         # Regular state variables
         self.stop_requested = False
         self.stop_requested = False
@@ -19,7 +23,7 @@ class AppState:
         self.current_theta = 0
         self.current_theta = 0
         self.current_rho = 0
         self.current_rho = 0
         self.current_playlist_index = 0
         self.current_playlist_index = 0
-        self.playlist_mode = None
+        self.playlist_mode = "loop"
         
         
         # Machine position variables
         # Machine position variables
         self.machine_x = 0.0
         self.machine_x = 0.0
@@ -35,6 +39,12 @@ class AppState:
         self.conn = None
         self.conn = None
         self.port = None
         self.port = None
         self.wled_ip = None
         self.wled_ip = None
+        self.led_controller = None
+        self.skip_requested = False
+        self.table_type = None
+        self._playlist_mode = "loop"
+        self._pause_time = 0
+        self._clear_pattern = "none"
         self.load()
         self.load()
 
 
     @property
     @property
@@ -84,8 +94,44 @@ class AppState:
         # force an empty string (and not None) if we need to unset
         # force an empty string (and not None) if we need to unset
         if value == None:
         if value == None:
             value = ""
             value = ""
+            # Also clear the playlist name when playlist is cleared
+            self._current_playlist_name = None
+        if self.mqtt_handler:
+            self.mqtt_handler.update_state(playlist=value, playlist_name=None)
+
+    @property
+    def current_playlist_name(self):
+        return self._current_playlist_name
+
+    @current_playlist_name.setter
+    def current_playlist_name(self, value):
+        self._current_playlist_name = value
         if self.mqtt_handler:
         if self.mqtt_handler:
-            self.mqtt_handler.update_state(playlist=value)
+            self.mqtt_handler.update_state(playlist_name=value)
+
+    @property
+    def playlist_mode(self):
+        return self._playlist_mode
+
+    @playlist_mode.setter
+    def playlist_mode(self, value):
+        self._playlist_mode = value
+
+    @property
+    def pause_time(self):
+        return self._pause_time
+
+    @pause_time.setter
+    def pause_time(self, value):
+        self._pause_time = value
+
+    @property
+    def clear_pattern(self):
+        return self._clear_pattern
+
+    @clear_pattern.setter
+    def clear_pattern(self, value):
+        self._clear_pattern = value
 
 
     def to_dict(self):
     def to_dict(self):
         """Return a dictionary representation of the state."""
         """Return a dictionary representation of the state."""
@@ -105,8 +151,11 @@ class AppState:
             "gear_ratio": self.gear_ratio,
             "gear_ratio": self.gear_ratio,
             "homing": self.homing,
             "homing": self.homing,
             "current_playlist": self._current_playlist,
             "current_playlist": self._current_playlist,
+            "current_playlist_name": self._current_playlist_name,
             "current_playlist_index": self.current_playlist_index,
             "current_playlist_index": self.current_playlist_index,
-            "playlist_mode": self.playlist_mode,
+            "playlist_mode": self._playlist_mode,
+            "pause_time": self._pause_time,
+            "clear_pattern": self._clear_pattern,
             "port": self.port,
             "port": self.port,
             "wled_ip": self.wled_ip
             "wled_ip": self.wled_ip
         }
         }
@@ -128,8 +177,11 @@ class AppState:
         self.gear_ratio = data.get('gear_ratio', 10)
         self.gear_ratio = data.get('gear_ratio', 10)
         self.homing = data.get('homing', 0)
         self.homing = data.get('homing', 0)
         self._current_playlist = data.get("current_playlist")
         self._current_playlist = data.get("current_playlist")
+        self._current_playlist_name = data.get("current_playlist_name")
         self.current_playlist_index = data.get("current_playlist_index")
         self.current_playlist_index = data.get("current_playlist_index")
-        self.playlist_mode = data.get("playlist_mode")
+        self._playlist_mode = data.get("playlist_mode", "loop")
+        self._pause_time = data.get("pause_time", 0)
+        self._clear_pattern = data.get("clear_pattern", "none")
         self.port = data.get("port", None)
         self.port = data.get("port", None)
         self.wled_ip = data.get('wled_ip', None)
         self.wled_ip = data.get('wled_ip', None)
 
 
@@ -165,5 +217,6 @@ class AppState:
         self.__init__()  # Reinitialize the state
         self.__init__()  # Reinitialize the state
         self.save()
         self.save()
 
 
+
 # Create a singleton instance that you can import elsewhere:
 # Create a singleton instance that you can import elsewhere:
 state = AppState()
 state = AppState()

+ 240 - 0
modules/led/led_controller.py

@@ -0,0 +1,240 @@
+import requests
+import json
+from typing import Dict, Optional
+import time
+import logging
+logger = logging.getLogger(__name__)
+
+
+class LEDController:
+    def __init__(self, ip_address: Optional[str] = None):
+        self.ip_address = ip_address
+
+    def _get_base_url(self) -> str:
+        """Get base URL for WLED JSON API"""
+        if not self.ip_address:
+            raise ValueError("No WLED IP configured")
+        return f"http://{self.ip_address}/json"
+
+    def set_ip(self, ip_address: str) -> None:
+        """Update the WLED IP address"""
+        self.ip_address = ip_address
+
+    def _send_command(self, state_params: Dict = None) -> Dict:
+        """Send command to WLED and return status"""
+        try:
+            url = self._get_base_url()
+            
+            # First check current state
+            response = requests.get(f"{url}/state", timeout=2)
+            response.raise_for_status()
+            current_state = response.json()
+            
+            # If WLED is off and we're trying to set something, turn it on first
+            if not current_state.get('on', False) and state_params and 'on' not in state_params:
+                # Turn on power first
+                requests.post(f"{url}/state", json={"on": True}, timeout=2)
+            
+            # Now send the actual command if there are parameters
+            if state_params:
+                response = requests.post(f"{url}/state", json=state_params, timeout=2)
+                response.raise_for_status()
+                response = requests.get(f"{url}/state", timeout=2)
+                response.raise_for_status()
+                current_state = response.json()
+                
+            preset_id = current_state.get('ps', -1)
+            playlist_id = current_state.get('pl', -1)
+
+            # Use True as default since WLED is typically on when responding
+            is_on = current_state.get('on', True)
+            
+            return {
+                "connected": True,
+                "is_on": is_on,
+                "preset_id": preset_id,
+                "playlist_id": playlist_id,
+                "brightness": current_state.get('bri', 0),
+                "message": "WLED is ON" if is_on else "WLED is OFF"
+            }
+
+        except ValueError as e:
+            return {"connected": False, "message": str(e)}
+        except requests.RequestException as e:
+            return {"connected": False, "message": f"Cannot connect to WLED: {str(e)}"}
+        except json.JSONDecodeError as e:
+            return {"connected": False, "message": f"Error parsing WLED response: {str(e)}"}
+
+    def check_wled_status(self) -> Dict:
+        """Check WLED connection status and brightness"""
+        return self._send_command()
+
+    def set_brightness(self, value: int) -> Dict:
+        """Set WLED brightness (0-255)"""
+        if not 0 <= value <= 255:
+            return {"connected": False, "message": "Brightness must be between 0 and 255"}
+        return self._send_command({"bri": value})
+
+    def set_power(self, state: int) -> Dict:
+        """Set WLED power state (0=Off, 1=On, 2=Toggle)"""
+        if state not in [0, 1, 2]:
+            return {"connected": False, "message": "Power state must be 0 (Off), 1 (On), or 2 (Toggle)"}
+        if state == 2:
+            return self._send_command({"on": "t"})  # Toggle
+        return self._send_command({"on": bool(state)})
+
+    def _hex_to_rgb(self, hex_color: str) -> tuple:
+        """Convert hex color string to RGB tuple"""
+        hex_color = hex_color.lstrip('#')
+        if len(hex_color) != 6:
+            raise ValueError("Hex color must be 6 characters long (without #)")
+        return tuple(int(hex_color[i:i+2], 16) for i in (0, 2, 4))
+
+    def set_color(self, r: int = None, g: int = None, b: int = None, w: int = None, hex: str = None) -> Dict:
+        """Set WLED color using RGB(W) values or hex color code"""
+        if hex is not None:
+            try:
+                r, g, b = self._hex_to_rgb(hex)
+            except ValueError as e:
+                return {"connected": False, "message": str(e)}
+
+        # Prepare segment with color
+        seg = {"col": [[r or 0, g or 0, b or 0]]}
+        if w is not None:
+            if not 0 <= w <= 255:
+                return {"connected": False, "message": "White value must be between 0 and 255"}
+            seg["col"][0].append(w)
+
+        return self._send_command({"seg": [seg]})
+
+    def set_effect(self, effect_index: int, speed: int = None, intensity: int = None, 
+                   brightness: int = None, palette: int = None,
+                   # Primary color
+                   r: int = None, g: int = None, b: int = None, w: int = None, hex: str = None,
+                   # Secondary color
+                   r2: int = None, g2: int = None, b2: int = None, w2: int = None, hex2: str = None,
+                   # Transition
+                   transition: int = 0) -> Dict:
+        """
+        Set WLED effect with optional parameters
+        Args:
+            effect_index: Effect index (0-101)
+            speed: Effect speed (0-255)
+            intensity: Effect intensity (0-255)
+            brightness: LED brightness (0-255)
+            palette: FastLED palette index (0-46)
+            r, g, b: Primary RGB color values (0-255)
+            w: Primary White value for RGBW (0-255)
+            hex: Primary hex color code (e.g., '#ff0000' or 'ff0000')
+            r2, g2, b2: Secondary RGB color values (0-255)
+            w2: Secondary White value for RGBW (0-255)
+            hex2: Secondary hex color code
+            transition: Duration of crossfade in 100ms units (e.g. 7 = 700ms). Default 0 for instant change.
+        """
+        try:
+            effect_index = int(effect_index)
+        except (ValueError, TypeError):
+            return {"connected": False, "message": "Effect index must be a valid integer between 0 and 101"}
+
+        if not 0 <= effect_index <= 101:
+            return {"connected": False, "message": "Effect index must be between 0 and 101"}
+
+        # Convert primary hex to RGB if provided
+        if hex is not None:
+            try:
+                r, g, b = self._hex_to_rgb(hex)
+            except ValueError as e:
+                return {"connected": False, "message": f"Primary color: {str(e)}"}
+
+        # Convert secondary hex to RGB if provided
+        if hex2 is not None:
+            try:
+                r2, g2, b2 = self._hex_to_rgb(hex2)
+            except ValueError as e:
+                return {"connected": False, "message": f"Secondary color: {str(e)}"}
+
+        # Build segment parameters
+        seg = {"fx": effect_index}
+        
+        if speed is not None:
+            if not 0 <= speed <= 255:
+                return {"connected": False, "message": "Speed must be between 0 and 255"}
+            seg["sx"] = speed
+        
+        if intensity is not None:
+            if not 0 <= intensity <= 255:
+                return {"connected": False, "message": "Intensity must be between 0 and 255"}
+            seg["ix"] = intensity
+
+        # Prepare colors array
+        colors = []
+        
+        # Add primary color
+        primary = [r or 0, g or 0, b or 0]
+        if w is not None:
+            if not 0 <= w <= 255:
+                return {"connected": False, "message": "Primary white value must be between 0 and 255"}
+            primary.append(w)
+        colors.append(primary)
+        
+        # Add secondary color if any secondary color parameter is provided
+        if any(x is not None for x in [r2, g2, b2, w2, hex2]):
+            secondary = [r2 or 0, g2 or 0, b2 or 0]
+            if w2 is not None:
+                if not 0 <= w2 <= 255:
+                    return {"connected": False, "message": "Secondary white value must be between 0 and 255"}
+                secondary.append(w2)
+            colors.append(secondary)
+
+        if colors:
+            seg["col"] = colors
+
+        if palette is not None:
+            if not 0 <= palette <= 46:
+                return {"connected": False, "message": "Palette index must be between 0 and 46"}
+            seg["pal"] = palette
+
+        # Combine with global parameters
+        state = {"seg": [seg], "transition": transition}
+        if brightness is not None:
+            if not 0 <= brightness <= 255:
+                return {"connected": False, "message": "Brightness must be between 0 and 255"}
+            state["bri"] = brightness
+
+        return self._send_command(state)
+
+    def set_preset(self, preset_id: int) -> bool:
+        preset_id = int(preset_id)
+        # Send the command and get response
+        response = self._send_command({"ps": preset_id})
+        logger.debug(response)
+        return response
+
+def effect_loading(led_controller: LEDController):
+    res = led_controller.set_effect(47, hex='#ffa000', hex2='#000000', palette=0, speed=150, intensity=150)
+    if res.get('is_on', False):
+        return True
+    else:
+        return False
+
+def effect_idle(led_controller: LEDController):
+    led_controller.set_preset(1)
+
+def effect_connected(led_controller: LEDController):
+    res = led_controller.set_effect(0, hex='#08ff00', brightness=100)
+    time.sleep(1)
+    led_controller.set_effect(0, brightness=0)  # Turn off
+    time.sleep(0.5)
+    res = led_controller.set_effect(0, hex='#08ff00', brightness=100)
+    time.sleep(1)
+    effect_idle(led_controller)
+    if res.get('is_on', False):
+        return True
+    else:
+        return False
+
+def effect_playing(led_controller: LEDController):
+    led_controller.set_preset(2)
+
+def effect_error(led_controller: LEDController):
+    res = led_controller.set_effect(0, hex='#ff0000', brightness=100)

+ 106 - 18
modules/mqtt/handler.py

@@ -6,6 +6,7 @@ import json
 from typing import Dict, Callable, List, Optional, Any
 from typing import Dict, Callable, List, Optional, Any
 import paho.mqtt.client as mqtt
 import paho.mqtt.client as mqtt
 import logging
 import logging
+from functools import partial
 
 
 from .base import BaseMQTTHandler
 from .base import BaseMQTTHandler
 from modules.core.state import state
 from modules.core.state import state
@@ -155,9 +156,7 @@ class MQTTHandler(BaseMQTTHandler):
             "state_topic": f"{self.speed_topic}/state",
             "state_topic": f"{self.speed_topic}/state",
             "device": base_device,
             "device": base_device,
             "icon": "mdi:speedometer",
             "icon": "mdi:speedometer",
-            "min": 50,
-            "max": 1000,
-            "step": 50
+            "mode": "box",
         }
         }
         self._publish_discovery("number", "speed", speed_config)
         self._publish_discovery("number", "speed", speed_config)
 
 
@@ -185,6 +184,48 @@ class MQTTHandler(BaseMQTTHandler):
         }
         }
         self._publish_discovery("select", "playlist", playlist_config)
         self._publish_discovery("select", "playlist", playlist_config)
 
 
+        # Playlist Run Mode Select
+        playlist_mode_config = {
+            "name": f"{self.device_name} Playlist Mode",
+            "unique_id": f"{self.device_id}_playlist_mode",
+            "command_topic": f"{self.device_id}/playlist/mode/set",
+            "state_topic": f"{self.device_id}/playlist/mode/state",
+            "options": ["single", "loop"],
+            "device": base_device,
+            "icon": "mdi:repeat",
+            "entity_category": "config"
+        }
+        self._publish_discovery("select", "playlist_mode", playlist_mode_config)
+
+        # Playlist Pause Time Number Input
+        pause_time_config = {
+            "name": f"{self.device_name} Playlist Pause Time",
+            "unique_id": f"{self.device_id}_pause_time",
+            "command_topic": f"{self.device_id}/playlist/pause_time/set",
+            "state_topic": f"{self.device_id}/playlist/pause_time/state",
+            "device": base_device,
+            "icon": "mdi:timer",
+            "entity_category": "config",
+            "mode": "box",
+            "unit_of_measurement": "seconds",
+            "min": 0,
+            "max": 86400,
+        }
+        self._publish_discovery("number", "pause_time", pause_time_config)
+
+        # Clear Pattern Select
+        clear_pattern_config = {
+            "name": f"{self.device_name} Clear Pattern",
+            "unique_id": f"{self.device_id}_clear_pattern",
+            "command_topic": f"{self.device_id}/playlist/clear_pattern/set",
+            "state_topic": f"{self.device_id}/playlist/clear_pattern/state",
+            "options": ["none", "random", "adaptive", "clear_from_in", "clear_from_out", "clear_sideway"],
+            "device": base_device,
+            "icon": "mdi:eraser",
+            "entity_category": "config"
+        }
+        self._publish_discovery("select", "clear_pattern", clear_pattern_config)
+
     def _publish_discovery(self, component: str, config_type: str, config: dict):
     def _publish_discovery(self, component: str, config_type: str, config: dict):
         """Helper method to publish HA discovery configs."""
         """Helper method to publish HA discovery configs."""
         if not self.is_enabled:
         if not self.is_enabled:
@@ -225,16 +266,18 @@ class MQTTHandler(BaseMQTTHandler):
                 current_file = current_file.split("/")[-1].split("\\")[-1]
                 current_file = current_file.split("/")[-1].split("\\")[-1]
             self.client.publish(f"{self.pattern_select_topic}/state", current_file, retain=True)
             self.client.publish(f"{self.pattern_select_topic}/state", current_file, retain=True)
         else:
         else:
+            # Clear the pattern selection
             self.client.publish(f"{self.pattern_select_topic}/state", "None", retain=True)
             self.client.publish(f"{self.pattern_select_topic}/state", "None", retain=True)
             
             
-    def _publish_playlist_state(self, playlist=None):
+    def _publish_playlist_state(self, playlist_name=None):
         """Helper to publish playlist state."""
         """Helper to publish playlist state."""
-        if playlist is None:
-            playlist = self.state.current_playlist
+        if playlist_name is None:
+            playlist_name = self.state.current_playlist_name
             
             
-        if playlist:
-            self.client.publish(f"{self.playlist_select_topic}/state", playlist, retain=True)
+        if playlist_name:
+            self.client.publish(f"{self.playlist_select_topic}/state", playlist_name, retain=True)
         else:
         else:
+            # Clear the playlist selection
             self.client.publish(f"{self.playlist_select_topic}/state", "None", retain=True)
             self.client.publish(f"{self.playlist_select_topic}/state", "None", retain=True)
             
             
     def _publish_serial_state(self):
     def _publish_serial_state(self):
@@ -244,7 +287,7 @@ class MQTTHandler(BaseMQTTHandler):
         serial_status = f"connected to {serial_port}" if serial_connected else "disconnected"
         serial_status = f"connected to {serial_port}" if serial_connected else "disconnected"
         self.client.publish(self.serial_state_topic, serial_status, retain=True)
         self.client.publish(self.serial_state_topic, serial_status, retain=True)
 
 
-    def update_state(self, current_file=None, is_running=None, playlist=None):
+    def update_state(self, current_file=None, is_running=None, playlist=None, playlist_name=None):
         """Update state in Home Assistant. Only publishes the attributes that are explicitly passed."""
         """Update state in Home Assistant. Only publishes the attributes that are explicitly passed."""
         if not self.is_enabled:
         if not self.is_enabled:
             return
             return
@@ -259,8 +302,8 @@ class MQTTHandler(BaseMQTTHandler):
             self._publish_running_state(running_state)
             self._publish_running_state(running_state)
         
         
         # Update playlist state if playlist info is provided
         # Update playlist state if playlist info is provided
-        if playlist is not None:
-            self._publish_playlist_state(playlist)
+        if playlist_name is not None:
+            self._publish_playlist_state(playlist_name)
 
 
     def on_connect(self, client, userdata, flags, rc):
     def on_connect(self, client, userdata, flags, rc):
         """Callback when connected to MQTT broker."""
         """Callback when connected to MQTT broker."""
@@ -274,7 +317,10 @@ class MQTTHandler(BaseMQTTHandler):
                 (self.speed_topic, 0),
                 (self.speed_topic, 0),
                 (f"{self.device_id}/command/stop", 0),
                 (f"{self.device_id}/command/stop", 0),
                 (f"{self.device_id}/command/pause", 0),
                 (f"{self.device_id}/command/pause", 0),
-                (f"{self.device_id}/command/play", 0)
+                (f"{self.device_id}/command/play", 0),
+                (f"{self.device_id}/playlist/mode/set", 0),
+                (f"{self.device_id}/playlist/pause_time/set", 0),
+                (f"{self.device_id}/playlist/clear_pattern/set", 0),
             ])
             ])
             # Publish discovery configurations
             # Publish discovery configurations
             self.setup_ha_discovery()
             self.setup_ha_discovery()
@@ -295,7 +341,7 @@ class MQTTHandler(BaseMQTTHandler):
         """Callback when message is received."""
         """Callback when message is received."""
         try:
         try:
             if msg.topic == self.pattern_select_topic:
             if msg.topic == self.pattern_select_topic:
-                from .modules.core.pattern_manager import THETA_RHO_DIR
+                from modules.core.pattern_manager import THETA_RHO_DIR
                 # Handle pattern selection
                 # Handle pattern selection
                 pattern_name = msg.payload.decode()
                 pattern_name = msg.payload.decode()
                 if pattern_name in self.patterns:
                 if pattern_name in self.patterns:
@@ -305,7 +351,10 @@ class MQTTHandler(BaseMQTTHandler):
                 # Handle playlist selection
                 # Handle playlist selection
                 playlist_name = msg.payload.decode()
                 playlist_name = msg.payload.decode()
                 if playlist_name in self.playlists:
                 if playlist_name in self.playlists:
-                    self.callback_registry['run_playlist'](playlist_name=playlist_name)
+                    self.callback_registry['run_playlist'](playlist_name=playlist_name,
+                            run_mode=self.state.playlist_mode,
+                            pause_time=self.state.pause_time,
+                            clear_pattern=self.state.clear_pattern)
                     self.client.publish(f"{self.playlist_select_topic}/state", playlist_name, retain=True)
                     self.client.publish(f"{self.playlist_select_topic}/state", playlist_name, retain=True)
             elif msg.topic == self.speed_topic:
             elif msg.topic == self.speed_topic:
                 speed = int(msg.payload.decode())
                 speed = int(msg.payload.decode())
@@ -313,6 +362,9 @@ class MQTTHandler(BaseMQTTHandler):
             elif msg.topic == f"{self.device_id}/command/stop":
             elif msg.topic == f"{self.device_id}/command/stop":
                 # Handle stop command
                 # Handle stop command
                 self.callback_registry['stop']()
                 self.callback_registry['stop']()
+                # Clear both pattern and playlist selections
+                self._publish_pattern_state(None)
+                self._publish_playlist_state(None)
             elif msg.topic == f"{self.device_id}/command/pause":
             elif msg.topic == f"{self.device_id}/command/pause":
                 # Handle pause command - only if in running state
                 # Handle pause command - only if in running state
                 if bool(self.state.current_playing_file) and not self.state.pause_requested:
                 if bool(self.state.current_playing_file) and not self.state.pause_requested:
@@ -321,6 +373,21 @@ class MQTTHandler(BaseMQTTHandler):
                 # Handle play command - only if in paused state
                 # Handle play command - only if in paused state
                 if bool(self.state.current_playing_file) and self.state.pause_requested:
                 if bool(self.state.current_playing_file) and self.state.pause_requested:
                     self.callback_registry['resume']()
                     self.callback_registry['resume']()
+            elif msg.topic == f"{self.device_id}/playlist/mode/set":
+                mode = msg.payload.decode()
+                if mode in ["single", "loop"]:
+                    state.playlist_mode = mode
+                    self.client.publish(f"{self.device_id}/playlist/mode/state", mode, retain=True)
+            elif msg.topic == f"{self.device_id}/playlist/pause_time/set":
+                pause_time = float(msg.payload.decode())
+                if 0 <= pause_time <= 60:
+                    state.pause_time = pause_time
+                    self.client.publish(f"{self.device_id}/playlist/pause_time/state", pause_time, retain=True)
+            elif msg.topic == f"{self.device_id}/playlist/clear_pattern/set":
+                clear_pattern = msg.payload.decode()
+                if clear_pattern in ["none", "random", "adaptive", "clear_from_in", "clear_from_out", "clear_sideway"]:
+                    state.clear_pattern = clear_pattern
+                    self.client.publish(f"{self.device_id}/playlist/clear_pattern/state", clear_pattern, retain=True)
             else:
             else:
                 # Handle other commands
                 # Handle other commands
                 payload = json.loads(msg.payload.decode())
                 payload = json.loads(msg.payload.decode())
@@ -402,11 +469,32 @@ class MQTTHandler(BaseMQTTHandler):
         if not self.is_enabled:
         if not self.is_enabled:
             return
             return
 
 
+        # First stop the running flag to prevent new iterations
         self.running = False
         self.running = False
-        if self.status_thread:
-            self.status_thread.join(timeout=1)
-        self.client.loop_stop()
-        self.client.disconnect()
+        
+        # Clean up status thread
+        local_status_thread = self.status_thread  # Keep a local reference
+        if local_status_thread and local_status_thread.is_alive():
+            try:
+                local_status_thread.join(timeout=5)
+                if local_status_thread.is_alive():
+                    logger.warning("MQTT status thread did not terminate cleanly")
+            except Exception as e:
+                logger.error(f"Error joining status thread: {e}")
+        self.status_thread = None
+            
+        # Clean up MQTT client
+        try:
+            if hasattr(self, 'client'):
+                self.client.loop_stop()
+                self.client.disconnect()
+        except Exception as e:
+            logger.error(f"Error disconnecting MQTT client: {e}")
+        
+        # Clean up main loop reference
+        self.main_loop = None
+        
+        logger.info("MQTT handler stopped")
 
 
     @property
     @property
     def is_enabled(self) -> bool:
     def is_enabled(self) -> bool:

+ 0 - 8
static/js/main.js

@@ -931,14 +931,6 @@ function openPlaylistEditor(playlistName) {
     loadPlaylist(playlistName);
     loadPlaylist(playlistName);
 }
 }
 
 
-function clearSchedule() {
-    document.getElementById("start_time").value = "";
-    document.getElementById("end_time").value = "";
-    document.getElementById('clear_time').style.display = 'none';
-    setCookie('start_time', '', 365);
-    setCookie('end_time', '', 365);
-}
-
 // Function to run the selected playlist with specified parameters
 // Function to run the selected playlist with specified parameters
 async function runPlaylist() {
 async function runPlaylist() {
     const playlistName = document.getElementById('playlist_name_display').textContent;
     const playlistName = document.getElementById('playlist_name_display').textContent;