fix(mqtt): decouple eventloop from status watcher and publish playlist to state and subtopic

This commit is contained in:
bart
2026-11-07 16:33:53 +01:00
parent 4bb6fe007e
commit 9da6d3db27
+39 -20
View File
@@ -109,19 +109,45 @@ async fn run_mqtt_loop(config: AppConfig, player: Arc<PlayerController>) {
device_name device_name
); );
let mut status_rx = player.status_watch_rx(); let is_connected = Arc::new(std::sync::atomic::AtomicBool::new(false));
let mut last_published: Option<MqttPlaybackState> = None;
loop { loop {
let (client, mut eventloop) = AsyncClient::new(mqtt_opts.clone(), 100); let (client, mut eventloop) = AsyncClient::new(mqtt_opts.clone(), 100);
let mut is_connected = false; is_connected.store(false, std::sync::atomic::Ordering::SeqCst);
let (cancel_tx, mut cancel_rx) = tokio::sync::oneshot::channel::<()>();
let client_clone = client.clone();
let mqtt_cfg_clone = mqtt_cfg.clone();
let device_id_string = device_id.to_string();
let is_connected_clone = is_connected.clone();
let mut status_rx_clone = player.status_watch_rx();
let watcher_handle = tokio::spawn(async move {
let mut last_published: Option<MqttPlaybackState> = None;
loop { loop {
tokio::select! { tokio::select! {
event_res = eventloop.poll() => { _ = &mut cancel_rx => break,
match event_res { res = status_rx_clone.changed() => {
if res.is_err() {
break;
}
if is_connected_clone.load(std::sync::atomic::Ordering::SeqCst) {
let st = status_rx_clone.borrow().clone();
let new_state = MqttPlaybackState::from_status(&st);
if last_published.as_ref() != Some(&new_state) {
publish_playback_state(&client_clone, &mqtt_cfg_clone, &device_id_string, &new_state).await;
last_published = Some(new_state);
}
}
}
}
}
});
loop {
match eventloop.poll().await {
Ok(rumqttc::Event::Incoming(Packet::ConnAck(_))) => { Ok(rumqttc::Event::Incoming(Packet::ConnAck(_))) => {
is_connected = true; is_connected.store(true, std::sync::atomic::Ordering::SeqCst);
tracing::info!("MQTT broker connected ({}:{})", mqtt_cfg.host, mqtt_cfg.port); tracing::info!("MQTT broker connected ({}:{})", mqtt_cfg.host, mqtt_cfg.port);
// 1. Announce online availability // 1. Announce online availability
@@ -139,7 +165,6 @@ async fn run_mqtt_loop(config: AppConfig, player: Arc<PlayerController>) {
MqttPlaybackState::from_status(&guard) MqttPlaybackState::from_status(&guard)
}; };
publish_playback_state(&client, mqtt_cfg, device_id, &current_st).await; publish_playback_state(&client, mqtt_cfg, device_id, &current_st).await;
last_published = Some(current_st);
} }
Ok(rumqttc::Event::Incoming(Packet::Publish(p))) => { Ok(rumqttc::Event::Incoming(Packet::Publish(p))) => {
if p.topic == command_topic { if p.topic == command_topic {
@@ -150,24 +175,15 @@ async fn run_mqtt_loop(config: AppConfig, player: Arc<PlayerController>) {
} }
Ok(_) => {} Ok(_) => {}
Err(e) => { Err(e) => {
is_connected.store(false, std::sync::atomic::Ordering::SeqCst);
tracing::warn!("MQTT connection lost or error: {}. Reconnecting...", e); tracing::warn!("MQTT connection lost or error: {}. Reconnecting...", e);
let _ = cancel_tx.send(());
watcher_handle.abort();
tokio::time::sleep(Duration::from_secs(3)).await; tokio::time::sleep(Duration::from_secs(3)).await;
break; // break inner loop to re-create client/eventloop break; // break inner loop to re-create client/eventloop
} }
} }
} }
changed_res = status_rx.changed() => {
if changed_res.is_ok() && is_connected {
let st = status_rx.borrow().clone();
let new_state = MqttPlaybackState::from_status(&st);
if last_published.as_ref() != Some(&new_state) {
publish_playback_state(&client, mqtt_cfg, device_id, &new_state).await;
last_published = Some(new_state);
}
}
}
}
}
} }
} }
@@ -334,6 +350,7 @@ pub async fn publish_playback_state(
"media_type": state.media_type, "media_type": state.media_type,
"title": state.title, "title": state.title,
"artist": state.artist, "artist": state.artist,
"playlist": state.playlist,
"genre": state.genre, "genre": state.genre,
"stream_title": state.stream_title, "stream_title": state.stream_title,
}); });
@@ -345,15 +362,17 @@ pub async fn publish_playback_state(
let _ = client.publish(format!("{}/{}/media_type", mqtt_cfg.topic_prefix, device_id), QoS::AtLeastOnce, true, state.media_type.as_str()).await; let _ = client.publish(format!("{}/{}/media_type", mqtt_cfg.topic_prefix, device_id), QoS::AtLeastOnce, true, state.media_type.as_str()).await;
let _ = client.publish(format!("{}/{}/title", mqtt_cfg.topic_prefix, device_id), QoS::AtLeastOnce, true, state.title.as_str()).await; let _ = client.publish(format!("{}/{}/title", mqtt_cfg.topic_prefix, device_id), QoS::AtLeastOnce, true, state.title.as_str()).await;
let _ = client.publish(format!("{}/{}/artist", mqtt_cfg.topic_prefix, device_id), QoS::AtLeastOnce, true, state.artist.as_str()).await; let _ = client.publish(format!("{}/{}/artist", mqtt_cfg.topic_prefix, device_id), QoS::AtLeastOnce, true, state.artist.as_str()).await;
let _ = client.publish(format!("{}/{}/playlist", mqtt_cfg.topic_prefix, device_id), QoS::AtLeastOnce, true, state.playlist.as_str()).await;
let _ = client.publish(format!("{}/{}/genre", mqtt_cfg.topic_prefix, device_id), QoS::AtLeastOnce, true, state.genre.as_str()).await; let _ = client.publish(format!("{}/{}/genre", mqtt_cfg.topic_prefix, device_id), QoS::AtLeastOnce, true, state.genre.as_str()).await;
let _ = client.publish(format!("{}/{}/stream_title", mqtt_cfg.topic_prefix, device_id), QoS::AtLeastOnce, true, state.stream_title.as_str()).await; let _ = client.publish(format!("{}/{}/stream_title", mqtt_cfg.topic_prefix, device_id), QoS::AtLeastOnce, true, state.stream_title.as_str()).await;
let _ = client.publish(format!("{}/{}/playback_state", mqtt_cfg.topic_prefix, device_id), QoS::AtLeastOnce, true, state.state.as_str()).await; let _ = client.publish(format!("{}/{}/playback_state", mqtt_cfg.topic_prefix, device_id), QoS::AtLeastOnce, true, state.state.as_str()).await;
tracing::info!( tracing::info!(
"MQTT state published: media_type='{}', title='{}', artist='{}', genre='{}', stream_title='{}', state='{}'", "MQTT state published: media_type='{}', title='{}', artist='{}', playlist='{}', genre='{}', stream_title='{}', state='{}'",
state.media_type, state.media_type,
state.title, state.title,
state.artist, state.artist,
state.playlist,
state.genre, state.genre,
state.stream_title, state.stream_title,
state.state state.state