Browse Source

Implement live update of mqtt
also fix playlist select handling (again)

Fabio De Simone 11 months ago
parent
commit
da68532452

+ 4 - 3
dune_weaver_flask/modules/core/pattern_manager.py

@@ -178,8 +178,9 @@ def run_theta_rho_file(file_path, schedule_hours=None):
         return
 
     state.execution_progress = (0, total_coordinates, None)
-
-    stop_actions()
+    
+    # stop actions without resetting the playlist
+    stop_actions(clear_playlist=False)
 
     with serial_manager.serial_lock:
         state.current_playing_file = file_path
@@ -280,7 +281,7 @@ def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_mode="
             break
     logger.info("All requested patterns completed (or stopped)")
 
-def stop_actions(clear_playlist = False):
+def stop_actions(clear_playlist = True):
     """Stop all current actions."""
     with state.pause_condition:
         state.pause_requested = False

+ 68 - 17
dune_weaver_flask/modules/core/state.py

@@ -5,43 +5,94 @@ import os
 
 class AppState:
     def __init__(self):
-        # Execution state variables
+        # Private variables for properties
+        self._current_playing_file = None
+        self._pause_requested = False
+        self._speed = 250
+        self._current_playlist = None
+        
+        # Regular state variables
         self.stop_requested = False
-        self.pause_requested = False
         self.pause_condition = threading.Condition()
-        self.current_playing_file = None
         self.execution_progress = None
         self.is_clearing = False
         self.current_theta = 0
         self.current_rho = 0
-        self.speed = 250
+        self.current_playlist_index = 0
+        self.playlist_mode = None
         
         # Machine position variables
         self.machine_x = 0.0
         self.machine_y = 0.0
         
-        # Playlist state variables
-        self.current_playlist = None
-        self.current_playlist_index = None
-        self.playlist_mode = None  # single, loop, etc.
-        
         self.STATE_FILE = "state.json"
+        self.mqtt_handler = None  # Will be set by the MQTT handler
         self.load()
 
+    @property
+    def current_playing_file(self):
+        return self._current_playing_file
+
+    @current_playing_file.setter
+    def current_playing_file(self, value):
+        self._current_playing_file = value
+        
+        # force an empty string (and not None) if we need to unset
+        if value == None:
+            value = ""
+        if self.mqtt_handler:
+            is_running = bool(value and not self._pause_requested)
+            self.mqtt_handler.update_state(current_file=value, is_running=is_running)
+
+    @property
+    def pause_requested(self):
+        return self._pause_requested
+
+    @pause_requested.setter
+    def pause_requested(self, value):
+        self._pause_requested = value
+        if self.mqtt_handler:
+            is_running = bool(self._current_playing_file and not value)
+            self.mqtt_handler.update_state(is_running=is_running)
+
+    @property
+    def speed(self):
+        return self._speed
+
+    @speed.setter
+    def speed(self, value):
+        self._speed = value
+        if self.mqtt_handler and self.mqtt_handler.is_enabled:
+            self.mqtt_handler.client.publish(f"{self.mqtt_handler.speed_topic}/state", value, retain=True)
+
+    @property
+    def current_playlist(self):
+        return self._current_playlist
+
+    @current_playlist.setter
+    def current_playlist(self, value):
+        self._current_playlist = value
+        
+        # force an empty string (and not None) if we need to unset
+        if value == None:
+            value = ""
+        if self.mqtt_handler:
+            self.mqtt_handler.update_state(playlist=value)
+
     def to_dict(self):
         """Return a dictionary representation of the state."""
         return {
             "stop_requested": self.stop_requested,
-            "pause_requested": self.pause_requested,
-            "current_playing_file": self.current_playing_file,
+            "pause_requested": self._pause_requested,
+            "current_playing_file": self._current_playing_file,
             "execution_progress": self.execution_progress,
             "is_clearing": self.is_clearing,
             "current_theta": self.current_theta,
             "current_rho": self.current_rho,
-            "speed": self.speed,
+            "speed": self._speed,
             "machine_x": self.machine_x,
             "machine_y": self.machine_y,
-            "current_playlist": self.current_playlist,
+            "current_playlist": self._current_playlist,
             "current_playlist_index": self.current_playlist_index,
             "playlist_mode": self.playlist_mode
         }
@@ -49,16 +100,16 @@ class AppState:
     def from_dict(self, data):
         """Update state from a dictionary."""
         self.stop_requested = data.get("stop_requested", False)
-        self.pause_requested = data.get("pause_requested", False)
-        self.current_playing_file = data.get("current_playing_file")
+        self._pause_requested = data.get("pause_requested", False)
+        self._current_playing_file = data.get("current_playing_file")
         self.execution_progress = data.get("execution_progress")
         self.is_clearing = data.get("is_clearing", False)
         self.current_theta = data.get("current_theta", 0)
         self.current_rho = data.get("current_rho", 0)
-        self.speed = data.get("speed", 250)
+        self._speed = data.get("speed", 250)
         self.machine_x = data.get("machine_x", 0.0)
         self.machine_y = data.get("machine_y", 0.0)
-        self.current_playlist = data.get("current_playlist")
+        self._current_playlist = data.get("current_playlist")
         self.current_playlist_index = data.get("current_playlist_index")
         self.playlist_mode = data.get("playlist_mode")
 

+ 47 - 76
dune_weaver_flask/modules/mqtt/handler.py

@@ -64,6 +64,9 @@ class MQTTHandler(BaseMQTTHandler):
             if self.username and self.password:
                 self.client.username_pw_set(self.username, self.password)
 
+        self.state = state
+        self.state.mqtt_handler = self  # Set reference to self in state, needed so that state setters can update the state
+
     def setup_ha_discovery(self):
         """Publish Home Assistant MQTT discovery configurations."""
         if not self.is_enabled:
@@ -191,69 +194,37 @@ class MQTTHandler(BaseMQTTHandler):
         discovery_topic = f"{self.discovery_prefix}/{component}/{self.device_id}/{config_type}/config"
         self.client.publish(discovery_topic, json.dumps(config), retain=True)
 
-    def update_state(self, is_running: Optional[bool] = None, 
-                    current_file: Optional[str] = None,
-                    patterns: Optional[List[str]] = None, 
-                    serial: Optional[str] = None,
-                    playlist: Optional[Dict[str, Any]] = None) -> None:
-        """Update the state of the sand table and publish to MQTT."""
+    def update_state(self, current_file=None, is_running=None, playlist=None):
+        """Update state in Home Assistant. Only publishes the attributes that are explicitly passed."""
         if not self.is_enabled:
             return
 
+        # Update pattern state if current_file is provided
+        if current_file:
+            self.client.publish(f"{self.pattern_select_topic}/state", current_file[len('./patterns/'):], retain=True)
+        elif current_file == "":
+             # if empty string, we unset the topic, otherwise we don't do anything
+            self.client.publish(f"{self.pattern_select_topic}/state", "None", retain=True)
+            
+        # Update running state and button availability if is_running is provided
         if is_running is not None:
-            self.is_running_state = is_running
-            self.client.publish(self.running_state_topic, "ON" if is_running else "OFF", retain=True)
-        
-        if current_file is not None:
-            if current_file:  # Only publish if there's actually a file
-                # Extract just the filename without path and normalize it 
-                if current_file.startswith('./patterns/'):
-                    file_name = current_file[len('./patterns/'):]
-                else:
-                    file_name = current_file.split("/")[-1].split("\\")[-1]
-                
-                self.current_file = file_name
-                # Update both the current file topic and the pattern select state
-                self.client.publish(f"{self.pattern_select_topic}/state", file_name, retain=True)
-            else:
-                # Clear both states when no file is playing
-                self.client.publish(f"{self.pattern_select_topic}/state", "", retain=True)
-
-        if patterns is not None:
-            # Only proceed if patterns have actually changed
-            if set(patterns) != set(self.patterns):
-                self.patterns = patterns
-                # Republish discovery config with updated pattern options
-                self.setup_ha_discovery()
-        
-        if serial is not None:
-            # Format serial state as "connected to <port>" or "disconnected"
-            if "connected" in serial.lower():
-                port = serial.split(" ")[-1]  # Extract port from status message
-                formatted_state = f"connected to {port}"
-            else:
-                formatted_state = "disconnected"
+            running_state = "running" if is_running else "paused" if self.state.current_playing_file else "idle"
+            self.client.publish(self.running_state_topic, running_state, retain=True)
             
-            self.serial_state = formatted_state
-            self.client.publish(self.serial_state_topic, formatted_state, retain=True)
+            # Update button availability based on running state
+            self.client.publish(f"{self.device_id}/command/pause/available", 
+                              "true" if running_state == "running" else "false", 
+                              retain=True)
+            self.client.publish(f"{self.device_id}/command/play/available", 
+                              "true" if running_state == "paused" else "false", 
+                              retain=True)
         
-        if playlist is not None:
-            # Update playlist list if needed
-            if playlist.get('all_playlists'):
-                self.playlists = playlist['all_playlists']
-                self.setup_ha_discovery()  # Republish discovery to update playlist options
-            
-            # Publish playlist active state
-            self.client.publish(f"{self.device_id}/state/playlist", json.dumps({
-                "active": bool(playlist.get('current_playlist')),
-            }), retain=True)
-            
-            # Update playlist select state if a playlist is active
-            if playlist.get('current_playlist'):
-                current_playlist_name = playlist['current_playlist'][0]  # Use first file as playlist name
-                self.client.publish(f"{self.playlist_select_topic}/state", current_playlist_name, retain=True)
-            else:
-                self.client.publish(f"{self.playlist_select_topic}/state", "", retain=True)
+        # Update playlist state if playlist info is provided
+        if playlist:
+            self.client.publish(f"{self.playlist_select_topic}/state", playlist, retain=True)
+        elif playlist == "":
+            # if empty string, we unset the topic, otherwise we don't do anything
+            self.client.publish(f"{self.playlist_select_topic}/state", "None", retain=True)
 
     def on_connect(self, client, userdata, flags, rc):
         """Callback when connected to MQTT broker."""
@@ -294,11 +265,11 @@ class MQTTHandler(BaseMQTTHandler):
                 self.callback_registry['stop']()
             elif msg.topic == f"{self.device_id}/command/pause":
                 # Handle pause command - only if in running state
-                if bool(state.current_playing_file) and not state.pause_requested:
+                if bool(self.state.current_playing_file) and not self.state.pause_requested:
                     self.callback_registry['pause']()
             elif msg.topic == f"{self.device_id}/command/play":
                 # Handle play command - only if in paused state
-                if bool(state.current_playing_file) and state.pause_requested:
+                if bool(self.state.current_playing_file) and self.state.pause_requested:
                     self.callback_registry['resume']()
             else:
                 # Handle other commands
@@ -321,8 +292,8 @@ class MQTTHandler(BaseMQTTHandler):
         while self.running:
             try:
                 # Get current state
-                is_running = bool(state.current_playing_file)
-                current_file = state.current_playing_file
+                is_running = bool(self.state.current_playing_file)
+                current_file = self.state.current_playing_file
                 if current_file and current_file.startswith('./patterns/'):
                     current_file = current_file[len('./patterns/'):]
                 elif current_file:
@@ -331,7 +302,7 @@ class MQTTHandler(BaseMQTTHandler):
                 # Determine running state
                 if not is_running:
                     running_state = "idle"
-                elif state.pause_requested:
+                elif self.state.pause_requested:
                     running_state = "paused"
                 else:
                     running_state = "running"
@@ -356,11 +327,11 @@ class MQTTHandler(BaseMQTTHandler):
                     self.client.publish(f"{self.pattern_select_topic}/state", "None", retain=True)
 
                 # Update speed state
-                self.client.publish(f"{self.speed_topic}/state", state.speed, retain=True)
+                self.client.publish(f"{self.speed_topic}/state", self.state.speed, retain=True)
 
                 # Update playlist select state
-                if state.current_playlist:
-                    current_playlist_name = state.current_playlist
+                if self.state.current_playlist:
+                    current_playlist_name = self.state.current_playlist
                     self.client.publish(f"{self.playlist_select_topic}/state", current_playlist_name, retain=True)
                 else:
                     self.client.publish(f"{self.playlist_select_topic}/state", "None", retain=True)
@@ -377,15 +348,15 @@ class MQTTHandler(BaseMQTTHandler):
                     "timestamp": time.time(),
                     "client_id": self.client_id,
                     "current_file": current_file or '',
-                    "speed": state.speed,
+                    "speed": self.state.speed,
                     "position": {
-                        "theta": state.current_theta,
-                        "rho": state.current_rho,
-                        "x": state.machine_x,
-                        "y": state.machine_y
+                        "theta": self.state.current_theta,
+                        "rho": self.state.current_rho,
+                        "x": self.state.machine_x,
+                        "y": self.state.machine_y
                     }
                 }
-                logger.info(f"publishing status: {status}, {state.current_playlist}" )
+                logger.info(f"publishing status: {status}, {self.state.current_playlist}" )
                 self.client.publish(self.status_topic, json.dumps(status))
                 
                 # Wait for next interval
@@ -408,10 +379,10 @@ class MQTTHandler(BaseMQTTHandler):
             self.status_thread.start()
             
             # Get initial states from modules
-            is_running = bool(state.current_playing_file)
+            is_running = bool(self.state.current_playing_file)
             running_state = "idle"
             if is_running:
-                if state.pause_requested:
+                if self.state.pause_requested:
                     running_state = "paused"
                 else:
                     running_state = "running"
@@ -429,7 +400,7 @@ class MQTTHandler(BaseMQTTHandler):
                 "status": running_state,
                 "timestamp": time.time(),
                 "client_id": self.client_id,
-                "current_file": state.current_playing_file or ''
+                "current_file": self.state.current_playing_file or ''
             }
             self.client.publish(self.status_topic, json.dumps(status), retain=True)
             self.client.publish(self.running_state_topic, running_state, retain=True)
@@ -445,8 +416,8 @@ class MQTTHandler(BaseMQTTHandler):
             self.playlists = playlists
             
             # Update playlist select state if a playlist is active
-            if state.current_playlist:
-                current_playlist_name = state.current_playlist[0]
+            if self.state.current_playlist:
+                current_playlist_name = self.state.current_playlist[0]
                 self.client.publish(f"{self.playlist_select_topic}/state", current_playlist_name, retain=True)
             else:
                 self.client.publish(f"{self.playlist_select_topic}/state", "None", retain=True)