|
@@ -64,6 +64,9 @@ class MQTTHandler(BaseMQTTHandler):
|
|
|
if self.username and self.password:
|
|
if self.username and self.password:
|
|
|
self.client.username_pw_set(self.username, 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):
|
|
def setup_ha_discovery(self):
|
|
|
"""Publish Home Assistant MQTT discovery configurations."""
|
|
"""Publish Home Assistant MQTT discovery configurations."""
|
|
|
if not self.is_enabled:
|
|
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"
|
|
discovery_topic = f"{self.discovery_prefix}/{component}/{self.device_id}/{config_type}/config"
|
|
|
self.client.publish(discovery_topic, json.dumps(config), retain=True)
|
|
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:
|
|
if not self.is_enabled:
|
|
|
return
|
|
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:
|
|
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):
|
|
def on_connect(self, client, userdata, flags, rc):
|
|
|
"""Callback when connected to MQTT broker."""
|
|
"""Callback when connected to MQTT broker."""
|
|
@@ -294,11 +265,11 @@ class MQTTHandler(BaseMQTTHandler):
|
|
|
self.callback_registry['stop']()
|
|
self.callback_registry['stop']()
|
|
|
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(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']()
|
|
self.callback_registry['pause']()
|
|
|
elif msg.topic == f"{self.device_id}/command/play":
|
|
elif msg.topic == f"{self.device_id}/command/play":
|
|
|
# Handle play command - only if in paused state
|
|
# 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']()
|
|
self.callback_registry['resume']()
|
|
|
else:
|
|
else:
|
|
|
# Handle other commands
|
|
# Handle other commands
|
|
@@ -321,8 +292,8 @@ class MQTTHandler(BaseMQTTHandler):
|
|
|
while self.running:
|
|
while self.running:
|
|
|
try:
|
|
try:
|
|
|
# Get current state
|
|
# 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/'):
|
|
if current_file and current_file.startswith('./patterns/'):
|
|
|
current_file = current_file[len('./patterns/'):]
|
|
current_file = current_file[len('./patterns/'):]
|
|
|
elif current_file:
|
|
elif current_file:
|
|
@@ -331,7 +302,7 @@ class MQTTHandler(BaseMQTTHandler):
|
|
|
# Determine running state
|
|
# Determine running state
|
|
|
if not is_running:
|
|
if not is_running:
|
|
|
running_state = "idle"
|
|
running_state = "idle"
|
|
|
- elif state.pause_requested:
|
|
|
|
|
|
|
+ elif self.state.pause_requested:
|
|
|
running_state = "paused"
|
|
running_state = "paused"
|
|
|
else:
|
|
else:
|
|
|
running_state = "running"
|
|
running_state = "running"
|
|
@@ -356,11 +327,11 @@ class MQTTHandler(BaseMQTTHandler):
|
|
|
self.client.publish(f"{self.pattern_select_topic}/state", "None", retain=True)
|
|
self.client.publish(f"{self.pattern_select_topic}/state", "None", retain=True)
|
|
|
|
|
|
|
|
# Update speed state
|
|
# 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
|
|
# 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)
|
|
self.client.publish(f"{self.playlist_select_topic}/state", current_playlist_name, retain=True)
|
|
|
else:
|
|
else:
|
|
|
self.client.publish(f"{self.playlist_select_topic}/state", "None", retain=True)
|
|
self.client.publish(f"{self.playlist_select_topic}/state", "None", retain=True)
|
|
@@ -377,15 +348,15 @@ class MQTTHandler(BaseMQTTHandler):
|
|
|
"timestamp": time.time(),
|
|
"timestamp": time.time(),
|
|
|
"client_id": self.client_id,
|
|
"client_id": self.client_id,
|
|
|
"current_file": current_file or '',
|
|
"current_file": current_file or '',
|
|
|
- "speed": state.speed,
|
|
|
|
|
|
|
+ "speed": self.state.speed,
|
|
|
"position": {
|
|
"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))
|
|
self.client.publish(self.status_topic, json.dumps(status))
|
|
|
|
|
|
|
|
# Wait for next interval
|
|
# Wait for next interval
|
|
@@ -408,10 +379,10 @@ class MQTTHandler(BaseMQTTHandler):
|
|
|
self.status_thread.start()
|
|
self.status_thread.start()
|
|
|
|
|
|
|
|
# Get initial states from modules
|
|
# Get initial states from modules
|
|
|
- is_running = bool(state.current_playing_file)
|
|
|
|
|
|
|
+ is_running = bool(self.state.current_playing_file)
|
|
|
running_state = "idle"
|
|
running_state = "idle"
|
|
|
if is_running:
|
|
if is_running:
|
|
|
- if state.pause_requested:
|
|
|
|
|
|
|
+ if self.state.pause_requested:
|
|
|
running_state = "paused"
|
|
running_state = "paused"
|
|
|
else:
|
|
else:
|
|
|
running_state = "running"
|
|
running_state = "running"
|
|
@@ -429,7 +400,7 @@ class MQTTHandler(BaseMQTTHandler):
|
|
|
"status": running_state,
|
|
"status": running_state,
|
|
|
"timestamp": time.time(),
|
|
"timestamp": time.time(),
|
|
|
"client_id": self.client_id,
|
|
"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.status_topic, json.dumps(status), retain=True)
|
|
|
self.client.publish(self.running_state_topic, running_state, retain=True)
|
|
self.client.publish(self.running_state_topic, running_state, retain=True)
|
|
@@ -445,8 +416,8 @@ class MQTTHandler(BaseMQTTHandler):
|
|
|
self.playlists = playlists
|
|
self.playlists = playlists
|
|
|
|
|
|
|
|
# Update playlist select state if a playlist is active
|
|
# 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)
|
|
self.client.publish(f"{self.playlist_select_topic}/state", current_playlist_name, retain=True)
|
|
|
else:
|
|
else:
|
|
|
self.client.publish(f"{self.playlist_select_topic}/state", "None", retain=True)
|
|
self.client.publish(f"{self.playlist_select_topic}/state", "None", retain=True)
|