sonos-sdk 0.4.0

Sync-first, DOM-like SDK for controlling Sonos speakers via UPnP
Documentation

sonos-sdk

A sync-first, DOM-like SDK for controlling Sonos speakers. Access properties directly on speaker objects with a consistent three-method pattern.

Features

  • Sync-First API: All methods are synchronous - no async/await required
  • DOM-like Access: Properties accessed directly on speaker objects (speaker.volume.get())
  • Three Access Patterns: get() for cached, fetch() for fresh, watch() for reactive
  • RAII Subscriptions: UPnP subscriptions managed automatically via WatchHandle (drop to unsubscribe)
  • Type Safety: All properties are strongly typed
  • Blocking Iteration: Event loop pattern for reactive applications

Quick Start

use sonos_sdk::{SonosSystem, SdkError};

fn main() -> Result<(), SdkError> {
    // Create system with automatic device discovery (sync)
    let system = SonosSystem::new()?;

    // Get speaker by name
    let speaker = system.get_speaker_by_name("Living Room")
        .ok_or_else(|| SdkError::SpeakerNotFound("Living Room".to_string()))?;

    // Access properties directly on the speaker object
    let volume = speaker.volume.get();           // Cached value (instant)
    let fresh = speaker.volume.fetch()?;         // Fresh from device (API call)
    let current = speaker.volume.watch()?;       // Start watching for changes

    println!("Volume: {:?}", volume);
    Ok(())
}

The Get/Fetch/Watch Pattern

Every property on a speaker provides three methods:

get() - Cached Value (Instant)

Returns the cached value without any network calls. Fast and always available.

// Get cached volume - returns Option<Volume>
if let Some(vol) = speaker.volume.get() {
    println!("Volume: {}%", vol.0);
}

// Get cached playback state
if let Some(state) = speaker.playback_state.get() {
    println!("State: {:?}", state);
}

fetch() - Fresh Value (API Call)

Makes a synchronous API call to the device and updates the cache.

// Fetch fresh volume from device
let volume = speaker.volume.fetch()?;
println!("Fresh volume: {}%", volume.0);

// Fetch fresh playback state
let state = speaker.playback_state.fetch()?;
println!("Fresh state: {:?}", state);

watch() - Reactive Updates

Returns a WatchHandle that keeps the subscription alive. Changes appear in system.iter(). Dropping the handle starts a 50ms grace period before unsubscribing.

// Start watching volume — hold the handle to keep the subscription alive
let vol_handle = speaker.volume.watch()?;

// Start watching playback state
let _playback_handle = speaker.playback_state.watch()?;

// Access the current value via the handle
if let Some(vol) = vol_handle.value() {
    println!("Volume: {}%", vol.0);
}

// Dropping the handle starts the grace period; subscription persists for 50ms
drop(vol_handle);

Event Loop Pattern

Build reactive applications by iterating over property changes:

use sonos_sdk::{SonosSystem, SdkError};
use std::time::Duration;

fn main() -> Result<(), SdkError> {
    let system = SonosSystem::new()?;
    
    // Get a speaker
    let speaker = system.get_speaker_by_name("Living Room")
        .ok_or_else(|| SdkError::SpeakerNotFound("Living Room".to_string()))?;

    // Watch properties of interest — hold handles to keep subscriptions alive
    let _vol = speaker.volume.watch()?;
    let _playback = speaker.playback_state.watch()?;
    let _track = speaker.current_track.watch()?;

    println!("Listening for changes... (Ctrl+C to exit)");

    // Event loop - blocks until changes occur
    for event in system.iter() {
        println!("Property '{}' changed on speaker {}", 
            event.property_key, event.speaker_id);

        // React to specific property changes
        match event.property_key {
            "volume" => {
                if let Some(vol) = speaker.volume.get() {
                    println!("  New volume: {}%", vol.0);
                }
            }
            "playback_state" => {
                if let Some(state) = speaker.playback_state.get() {
                    println!("  New state: {:?}", state);
                }
            }
            "current_track" => {
                if let Some(track) = speaker.current_track.get() {
                    println!("  Now playing: {} - {}", 
                        track.title.as_deref().unwrap_or("Unknown"),
                        track.artist.as_deref().unwrap_or("Unknown"));
                }
            }
            _ => {}
        }
    }

    Ok(())
}

Non-Blocking Iteration

For applications that need to check for events without blocking:

// Check for events without blocking
for event in system.iter().try_iter() {
    println!("Event: {:?}", event);
}

// Wait with timeout
if let Some(event) = system.iter().recv_timeout(Duration::from_secs(1)) {
    println!("Got event: {:?}", event);
}

Available Properties

Audio Control (RenderingControl)

Property Type Description
volume Volume (u8) Master volume (0-100)
mute Mute (bool) Mute state
bass Bass (i8) Bass EQ (-10 to +10)
treble Treble (i8) Treble EQ (-10 to +10)
loudness Loudness (bool) Loudness compensation

Playback (AVTransport)

Property Type Description
playback_state PlaybackState Playing/Paused/Stopped/Transitioning
position Position Current position and duration
current_track CurrentTrack Track metadata (title, artist, album)

Grouping (ZoneGroupTopology)

Property Type Description
group_membership GroupMembership Group ID and coordinator status

Speaker Lookup

// Get speaker by friendly name
let speaker = system.get_speaker_by_name("Kitchen")?;

// Get speaker by unique ID
let speaker = system.get_speaker_by_id(&speaker_id)?;

// Get all speakers
for speaker in system.speakers() {
    println!("{}: {} ({})", speaker.name, speaker.model_name, speaker.ip);
}

// Get all speaker names
let names = system.speaker_names();

Error Handling

The SDK provides structured error types:

use sonos_sdk::SdkError;

match speaker.volume.fetch() {
    Ok(vol) => println!("Volume: {}%", vol.0),
    Err(SdkError::ApiError(e)) => println!("API error: {}", e),
    Err(SdkError::SpeakerNotFound(name)) => println!("Speaker not found: {}", name),
    Err(e) => println!("Other error: {}", e),
}

Architecture

sonos-sdk (Sync-First DOM-like API)
    ↓
sonos-state (State Management) ←→ sonos-event-manager (Event Subscriptions)
    ↓                                    ↓
sonos-api (UPnP Operations)         sonos-stream (Event Processing)

License

MIT License

See Also