use sonos_api::services::av_transport::{
GetTransportInfoOperation, GetTransportInfoOperationRequest,
};
use sonos_api::services::rendering_control::{GetVolumeOperation, GetVolumeOperationRequest};
use sonos_api::{OperationBuilder, Service, SonosClient};
use sonos_stream::{BrokerConfig, EventBroker, EventData, PollingReason};
use std::net::IpAddr;
#[derive(Debug, Clone)]
struct LocalTransportState {
transport_state: String,
current_track_uri: String,
track_duration: String,
rel_time: String,
track_metadata: String,
}
#[derive(Debug, Clone)]
struct LocalVolumeState {
volume: u16,
mute: bool,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
println!("๐ต Sonos Stream - Basic State Management Example");
println!("=================================================");
let mut broker = EventBroker::new(BrokerConfig::default()).await?;
let client = SonosClient::new();
println!("\n๐ Discovering Sonos devices on the network...");
let devices = tokio::task::spawn_blocking(|| {
sonos_discovery::get_with_timeout(std::time::Duration::from_secs(5))
})
.await?;
if devices.is_empty() {
println!("โ No Sonos devices found on the network!");
println!(
" Make sure your Sonos speakers are powered on and connected to the same network."
);
return Ok(());
}
println!("โ
Found {} Sonos device(s):", devices.len());
for (i, device) in devices.iter().enumerate() {
println!(
" {}. {} ({}) - {} at {}:{}",
i + 1,
device.name,
device.room_name,
device.model_name,
device.ip_address,
device.port
);
}
let selected_device = devices
.iter()
.find(|d| d.model_name.contains("Playbar") || d.model_name.contains("Amp"))
.unwrap_or(&devices[0]);
let device_ip: IpAddr = selected_device.ip_address.parse()?;
println!(
"\n๐ฏ Using device: {} ({}) at {}",
selected_device.name, selected_device.room_name, device_ip
);
println!("\n๐ Registering Sonos services...");
let transport_reg = broker
.register_speaker_service(device_ip, Service::AVTransport)
.await?;
let volume_reg = broker
.register_speaker_service(device_ip, Service::RenderingControl)
.await?;
let group_mgmt_reg = broker
.register_speaker_service(device_ip, Service::GroupManagement)
.await?;
let group_rc_reg = broker
.register_speaker_service(device_ip, Service::GroupRenderingControl)
.await?;
println!("\n๐ Registration Results:");
print_registration_feedback(
&transport_reg.firewall_status,
transport_reg.polling_reason.as_ref(),
"AVTransport",
);
print_registration_feedback(
&volume_reg.firewall_status,
volume_reg.polling_reason.as_ref(),
"RenderingControl",
);
print_registration_feedback(
&group_mgmt_reg.firewall_status,
group_mgmt_reg.polling_reason.as_ref(),
"GroupManagement",
);
print_registration_feedback(
&group_rc_reg.firewall_status,
group_rc_reg.polling_reason.as_ref(),
"GroupRenderingControl",
);
println!("\n๐ STEP 1: Initialize local state through direct queries");
println!("(This is how consumers should handle initial state population)");
let mut local_transport_state = query_initial_transport_state(&client, &device_ip).await?;
let mut local_volume_state = query_initial_volume_state(&client, &device_ip).await?;
println!("โ
Initial state loaded:");
println!(
" Transport: {} | Track: {} | Position: {}",
local_transport_state.transport_state,
extract_track_title(&local_transport_state.track_metadata),
local_transport_state.rel_time
);
println!(
" Volume: {} | Muted: {}",
local_volume_state.volume, local_volume_state.mute
);
println!("\n๐ STEP 2: Process change events to maintain local state");
println!("(Using async iterator for real-time event processing)");
println!(
"Waiting for events... (try changing volume or playing/pausing on your Sonos device)\n"
);
let mut events = broker.event_iterator()?;
let mut event_count = 0;
while let Some(event) = events.next_async().await {
event_count += 1;
println!(
"๐จ Event #{} received from {} ({})",
event_count,
event.speaker_ip,
format_event_source(&event.event_source)
);
match event.event_data {
EventData::AVTransport(transport_event) => {
println!("๐ต Transport event received:");
if let Some(ref state) = transport_event.transport_state {
println!(" โ Transport state: {state}");
local_transport_state.transport_state = state.clone();
}
if let Some(ref uri) = transport_event.current_track_uri {
println!(" โ Track URI: {uri}");
local_transport_state.current_track_uri = uri.clone();
}
if let Some(ref position) = transport_event.rel_time {
println!(" โ Position: {position}");
local_transport_state.rel_time = position.clone();
}
if let Some(ref metadata) = transport_event.track_metadata {
local_transport_state.track_metadata = metadata.clone();
}
if let Some(ref duration) = transport_event.track_duration {
local_transport_state.track_duration = duration.clone();
}
println!(
" โ Updated state: {} | Position: {}",
local_transport_state.transport_state, local_transport_state.rel_time
);
}
EventData::RenderingControl(volume_event) => {
println!("๐ Volume event received:");
if let Some(ref volume) = volume_event.master_volume {
if let Ok(vol_num) = volume.parse::<u16>() {
println!(" โ Volume level: {vol_num}");
local_volume_state.volume = vol_num;
}
}
if let Some(ref mute) = volume_event.master_mute {
let mute_bool = mute == "1" || mute.to_lowercase() == "true";
println!(" โ Mute state: {mute_bool}");
local_volume_state.mute = mute_bool;
}
println!(
" โ Updated state: Volume {} | Muted: {}",
local_volume_state.volume, local_volume_state.mute
);
}
EventData::ZoneGroupTopology(topology) => {
println!("๐ Speaker topology event received:");
println!(" โ {} zone group(s) found", topology.zone_groups.len());
for (i, group) in topology.zone_groups.iter().enumerate() {
println!(
" โ Group {}: Coordinator {} with {} member(s)",
i + 1,
group.coordinator,
group.members.len()
);
for member in &group.members {
let wireless_status = if member.network_info.wifi_enabled == "1" {
format!("WiFi ({}MHz)", member.network_info.channel_freq)
} else {
"Ethernet".to_string()
};
println!(
" โข {} ({}) - {} - {}",
member.zone_name, member.uuid, member.software_version, wireless_status
);
if !member.satellites.is_empty() {
println!(" โโ {} satellite speaker(s)", member.satellites.len());
}
}
}
}
EventData::DeviceProperties(device_event) => {
println!("โ๏ธ Device properties event received:");
if let Some(ref zone_name) = device_event.zone_name {
println!(" โ Zone name: {zone_name}");
}
if let Some(ref model) = device_event.model_name {
println!(" โ Model: {model}");
}
if let Some(ref version) = device_event.software_version {
println!(" โ Software version: {version}");
}
}
EventData::GroupManagement(gm_event) => {
println!("๐ Group management event received:");
if let Some(is_local) = gm_event.group_coordinator_is_local {
println!(" โ Coordinator is local: {is_local}");
}
if let Some(ref group_uuid) = gm_event.local_group_uuid {
println!(" โ Local group UUID: {group_uuid}");
}
if let Some(reset_vol) = gm_event.reset_volume_after {
println!(" โ Reset volume after ungroup: {reset_vol}");
}
}
EventData::GroupRenderingControl(grc_event) => {
println!("๐ Group rendering control event received:");
if let Some(volume) = grc_event.group_volume {
println!(" โ Group volume: {volume}");
}
if let Some(mute) = grc_event.group_mute {
println!(" โ Group mute: {mute}");
}
if let Some(changeable) = grc_event.group_volume_changeable {
println!(" โ Group volume changeable: {changeable}");
}
}
}
println!("๐ Current State Summary:");
println!(
" Transport: {} | Track: {} | Position: {}",
local_transport_state.transport_state,
extract_track_title(&local_transport_state.track_metadata),
local_transport_state.rel_time
);
println!(
" Volume: {} | Muted: {}",
local_volume_state.volume, local_volume_state.mute
);
println!();
if event_count >= 10 {
println!("๐ Processed {event_count} events, stopping demonstration");
break;
}
}
println!("\n๐ Shutting down EventBroker...");
broker.shutdown().await?;
println!("โ
Example completed successfully!");
Ok(())
}
async fn query_initial_transport_state(
client: &SonosClient,
device_ip: &IpAddr,
) -> Result<LocalTransportState, Box<dyn std::error::Error>> {
let request = GetTransportInfoOperationRequest { instance_id: 0 };
let operation = OperationBuilder::<GetTransportInfoOperation>::new(request).build()?;
let transport_info = client.execute_enhanced(&device_ip.to_string(), operation)?;
Ok(LocalTransportState {
transport_state: transport_info.current_transport_state,
current_track_uri: "N/A".to_string(), track_duration: "N/A".to_string(),
rel_time: "N/A".to_string(),
track_metadata: "N/A".to_string(),
})
}
async fn query_initial_volume_state(
client: &SonosClient,
device_ip: &IpAddr,
) -> Result<LocalVolumeState, Box<dyn std::error::Error>> {
let request = GetVolumeOperationRequest {
instance_id: 0,
channel: "Master".to_string(),
};
let operation = OperationBuilder::<GetVolumeOperation>::new(request).build()?;
let volume_response = client.execute_enhanced(&device_ip.to_string(), operation)?;
Ok(LocalVolumeState {
volume: volume_response.current_volume as u16,
mute: false, })
}
fn print_registration_feedback(
firewall_status: &callback_server::firewall_detection::FirewallStatus,
polling_reason: Option<&PollingReason>,
service_name: &str,
) {
use callback_server::firewall_detection::FirewallStatus;
match firewall_status {
FirewallStatus::Accessible => {
if let Some(reason) = polling_reason {
match reason {
PollingReason::EventTimeout => {
println!(
" {service_name} ๐กโ๐ UPnP events timed out - switched to polling"
);
}
PollingReason::SubscriptionFailed => {
println!(" {service_name} โโ๐ UPnP subscription failed - using polling");
}
_ => {
println!(" {service_name} ๐ Using polling mode: {reason:?}");
}
}
} else {
println!(" {service_name} ๐ก UPnP events active - real-time updates enabled");
}
}
FirewallStatus::Blocked => {
println!(" {service_name} ๐ฅ Firewall detected - using polling for immediate updates");
}
FirewallStatus::Unknown => {
if polling_reason.is_some() {
println!(" {service_name} โ Firewall status unknown - using polling as fallback");
} else {
println!(" {service_name} โ Firewall status unknown - monitoring events closely");
}
}
FirewallStatus::Error => {
println!(
" {service_name} โ ๏ธ Firewall detection error - using polling as safe fallback"
);
}
}
}
fn format_event_source(source: &sonos_stream::events::types::EventSource) -> String {
use sonos_stream::events::types::EventSource;
match source {
EventSource::UPnPNotification { .. } => "UPnP Event".to_string(),
EventSource::PollingDetection { poll_interval } => {
format!("Polling ({}s interval)", poll_interval.as_secs())
}
}
}
fn extract_track_title(metadata: &str) -> String {
if metadata.is_empty() || metadata == "NOT_IMPLEMENTED" {
return "No Track".to_string();
}
if metadata.contains("<dc:title>") {
if let Some(start) = metadata.find("<dc:title>") {
let start = start + "<dc:title>".len();
if let Some(end) = metadata[start..].find("</dc:title>") {
return metadata[start..start + end].to_string();
}
}
}
"Unknown Track".to_string()
}