use std::sync::Arc;
use std::thread;
use std::time::Duration;
use sonos_event_manager::SonosEventManager;
use sonos_state::{
model::SpeakerId,
property::{CurrentTrack, Mute, PlaybackState, Position, Volume},
StateManager,
};
fn main() -> std::result::Result<(), Box<dyn std::error::Error>> {
tracing_subscriber::fmt()
.with_env_filter(
tracing_subscriber::EnvFilter::from_default_env()
.add_directive("sonos_stream=debug".parse().unwrap())
.add_directive("sonos_event_manager=debug".parse().unwrap())
.add_directive("sonos_state=debug".parse().unwrap()),
)
.init();
println!("=== Sonos Smart Dashboard (Sync-First API) ===\n");
let event_manager = Arc::new(SonosEventManager::new()?);
println!("Created event manager");
let manager = StateManager::builder()
.with_event_manager(Arc::clone(&event_manager))
.build()?;
println!("Created state manager with event integration");
println!("\nDiscovering Sonos devices...");
let devices = sonos_discovery::get_with_timeout(Duration::from_secs(5));
if devices.is_empty() {
println!("No Sonos devices found on the network.");
return Ok(());
}
manager.add_devices(devices.clone())?;
println!("Found {} devices:", devices.len());
for device in &devices {
println!(
" - {} ({}) at {}",
device.name, device.model_name, device.ip_address
);
}
println!("Added all devices to state manager");
let speaker_ids: Vec<(SpeakerId, String)> = devices
.iter()
.map(|d| (SpeakerId::new(&d.id), d.name.clone()))
.collect();
if speaker_ids.is_empty() {
println!("No speakers available for monitoring");
return Ok(());
}
println!("\nSetting up property subscriptions...");
for (speaker_id, name) in &speaker_ids {
if let Err(e) = manager.watch_property_with_subscription::<Volume>(speaker_id) {
println!(" Failed to watch volume for {name}: {e}");
} else {
println!(" Watching volume for {name}");
}
if let Err(e) = manager.watch_property_with_subscription::<Mute>(speaker_id) {
println!(" Failed to watch mute for {name}: {e}");
} else {
println!(" Watching mute for {name}");
}
if let Err(e) = manager.watch_property_with_subscription::<PlaybackState>(speaker_id) {
println!(" Failed to watch playback state for {name}: {e}");
} else {
println!(" Watching playback state for {name}");
}
}
println!("\n=== Live Property Dashboard ===");
println!("Waiting for events (Ctrl+C to quit)...\n");
thread::sleep(Duration::from_secs(2));
let running = Arc::new(std::sync::atomic::AtomicBool::new(true));
let r = Arc::clone(&running);
ctrlc::set_handler(move || {
r.store(false, std::sync::atomic::Ordering::SeqCst);
})?;
display_dashboard(&manager, &speaker_ids);
let iter = manager.iter();
while running.load(std::sync::atomic::Ordering::SeqCst) {
if let Some(event) = iter.recv_timeout(Duration::from_secs(1)) {
println!(
"\n[Event] {} changed for {}",
event.property_key,
event.speaker_id.as_str()
);
display_dashboard(&manager, &speaker_ids);
}
}
println!("\nShutting down gracefully...");
println!("\nDemo complete! All subscriptions cleaned up automatically.");
println!("Key benefits demonstrated:");
println!(" - Single StateManager handles both state and events");
println!(" - Pure synchronous API - no async/await required");
println!(" - Properties automatically trigger UPnP subscriptions");
println!(" - Multiple properties efficiently share subscriptions");
println!(" - Reference counting ensures optimal resource usage");
println!(" - Automatic cleanup when manager is dropped");
Ok(())
}
fn display_dashboard(manager: &StateManager, speaker_ids: &[(SpeakerId, String)]) {
println!("\n--- Dashboard Update ---");
println!("Time: {}", chrono::Utc::now().format("%H:%M:%S"));
for (speaker_id, name) in speaker_ids {
println!("\n{name}");
println!("{}", "=".repeat(name.len()));
if let Some(vol) = manager.get_property::<Volume>(speaker_id) {
let mute_str = manager
.get_property::<Mute>(speaker_id)
.map(|m| if m.0 { " [MUTED]" } else { "" })
.unwrap_or("");
println!(" Volume: {}%{}", vol.0, mute_str);
} else {
println!(" Volume: Not available");
}
if let Some(state) = manager.get_property::<PlaybackState>(speaker_id) {
let state_str = match state {
PlaybackState::Playing => "Playing",
PlaybackState::Paused => "Paused",
PlaybackState::Stopped => "Stopped",
PlaybackState::Transitioning => "Transitioning",
};
println!(" State: {state_str}");
} else {
println!(" State: Not available");
}
if let Some(track) = manager.get_property::<CurrentTrack>(speaker_id) {
if let Some(title) = &track.title {
println!(" Track: {title}");
}
if let Some(artist) = &track.artist {
println!(" Artist: {artist}");
}
}
if let Some(pos) = manager.get_property::<Position>(speaker_id) {
if pos.duration_ms > 0 {
let progress = pos.progress();
let pos_str = format_time(pos.position_ms);
let dur_str = format_time(pos.duration_ms);
println!(" Position: {pos_str} / {dur_str} ({progress:.0}%)");
}
}
}
}
fn format_time(ms: u64) -> String {
let total_secs = ms / 1000;
let mins = total_secs / 60;
let secs = total_secs % 60;
format!("{mins:02}:{secs:02}")
}