sonos-api 0.2.1

Type-safe Sonos API for UPnP device control via SOAP
Documentation

sonos-api

A stateless, type-safe Rust library for constructing requests to Sonos speakers and handling their responses.

Overview

The sonos-api crate provides a high-level, stateless layer for interacting with Sonos devices through their UPnP/SOAP interface. It focuses on:

  • Request Construction: Type-safe builders for SOAP payloads
  • Response Parsing: Structured parsing of XML responses into Rust types
  • Operation Modeling: Each UPnP action is modeled as a distinct operation with its own request/response types
  • Service Organization: Operations are grouped by UPnP service (AVTransport, RenderingControl, etc.)

This crate is stateless - it doesn't manage connections, maintain device state, or handle networking. It purely focuses on the request/response transformation layer.

Architecture

The crate is built around the SonosOperation trait, which defines a common interface for all Sonos UPnP operations:

pub trait SonosOperation {
    type Request: Serialize;
    type Response: for<'de> Deserialize<'de>;
    
    const SERVICE: Service;
    const ACTION: &'static str;
    
    fn build_payload(request: &Self::Request) -> String;
    fn parse_response(xml: &Element) -> Result<Self::Response, ApiError>;
}

Each operation implements this trait to provide:

  • Type-safe request and response structures
  • SOAP payload construction from request data
  • XML response parsing into structured data

Supported Services

The crate currently supports operations for these UPnP services:

  • AVTransport: Playback control (play, pause, stop, transport info)
  • RenderingControl: Volume and audio settings
  • DeviceProperties: Device information and capabilities
  • ZoneGroupTopology: Multi-room grouping and topology
  • GroupRenderingControl: Group-level audio control
  • Events: UPnP event subscriptions (subscribe, unsubscribe, renew) for all services

Usage

Using the SonosClient (Recommended)

The easiest way to use the API is through the SonosClient, which handles all the SOAP communication for you:

use sonos_api::{SonosClient, operations::av_transport::{GetTransportInfoOperation, GetTransportInfoRequest}};

// Create a client
let client = SonosClient::new();

// Create a request
let request = GetTransportInfoRequest {
    instance_id: 0,
};

// Execute the operation against a device
let response = client.execute::<GetTransportInfoOperation>("192.168.1.100", &request)?;
println!("Current state: {:?}", response.current_transport_state);

Working with Different Operations

use sonos_api::{SonosClient, operations::{
    av_transport::{PlayOperation, PlayRequest, PauseOperation, PauseRequest},
    rendering_control::{GetVolumeOperation, GetVolumeRequest, SetVolumeOperation, SetVolumeRequest},
}};

let client = SonosClient::new();
let device_ip = "192.168.1.100";

// Play music
let play_request = PlayRequest {
    instance_id: 0,
    speed: "1".to_string(),
};
client.execute::<PlayOperation>(device_ip, &play_request)?;

// Get current volume
let volume_request = GetVolumeRequest {
    instance_id: 0,
    channel: "Master".to_string(),
};
let volume_response = client.execute::<GetVolumeOperation>(device_ip, &volume_request)?;
println!("Current volume: {}", volume_response.current_volume);

// Set volume to 50%
let set_volume_request = SetVolumeRequest {
    instance_id: 0,
    channel: "Master".to_string(),
    desired_volume: 50,
};
client.execute::<SetVolumeOperation>(device_ip, &set_volume_request)?;

// Pause playback
let pause_request = PauseRequest {
    instance_id: 0,
};
client.execute::<PauseOperation>(device_ip, &pause_request)?;

Event Subscriptions

The client also supports UPnP event subscriptions for real-time notifications:

use sonos_api::{SonosClient, Service, operations::events::{SubscribeRequest, UnsubscribeRequest, RenewRequest}};

let client = SonosClient::new();
let device_ip = "192.168.1.100";

// Subscribe to AVTransport events
let subscribe_request = SubscribeRequest {
    callback_url: "http://192.168.1.50:8080/callback".to_string(),
    timeout_seconds: 1800,
};
let subscription = client.subscribe(device_ip, Service::AVTransport, &subscribe_request)?;
println!("Subscribed with SID: {}", subscription.sid);

// Renew the subscription before it expires
let renew_request = RenewRequest {
    sid: subscription.sid.clone(),
    timeout_seconds: 1800,
};
let renewal = client.renew_subscription(device_ip, Service::AVTransport, &renew_request)?;
println!("Renewed for {} seconds", renewal.timeout_seconds);

// Unsubscribe when done
let unsubscribe_request = UnsubscribeRequest {
    sid: subscription.sid,
};
client.unsubscribe(device_ip, Service::AVTransport, &unsubscribe_request)?;

Low-Level Operation Usage (Advanced)

For advanced use cases, you can work directly with operations without the client:

use sonos_api::operations::av_transport::{GetTransportInfoOperation, GetTransportInfoRequest};
use sonos_api::SonosOperation;

// Create a request
let request = GetTransportInfoRequest {
    instance_id: 0,
};

// Build the SOAP payload
let payload = GetTransportInfoOperation::build_payload(&request);
// Returns: "<InstanceID>0</InstanceID>"

// Parse a response (after receiving XML from the device)
let xml = /* parsed XML response */;
let response = GetTransportInfoOperation::parse_response(&xml)?;
println!("Current state: {:?}", response.current_transport_state);

Working with Different Operations

use sonos_api::operations::av_transport::{PlayOperation, PlayRequest};
use sonos_api::operations::rendering_control::{GetVolumeOperation, GetVolumeRequest};

// Play operation
let play_request = PlayRequest {
    instance_id: 0,
    speed: "1".to_string(),
};
let play_payload = PlayOperation::build_payload(&play_request);

// Volume operation  
let volume_request = GetVolumeRequest {
    instance_id: 0,
    channel: "Master".to_string(),
};
let volume_payload = GetVolumeOperation::build_payload(&volume_request);

Error Handling

The crate provides structured error handling through the ApiError type:

use sonos_api::{SonosClient, ApiError, operations::av_transport::{GetTransportInfoOperation, GetTransportInfoRequest}};

let client = SonosClient::new();
let request = GetTransportInfoRequest { instance_id: 0 };

match client.execute::<GetTransportInfoOperation>("192.168.1.100", &request) {
    Ok(response) => println!("Success: {:?}", response),
    Err(ApiError::NetworkError(msg)) => eprintln!("Network error: {}", msg),
    Err(ApiError::ParseError(msg)) => eprintln!("Parse error: {}", msg),
    Err(ApiError::SoapFault(code)) => eprintln!("Device returned error code: {}", code),
    Err(e) => eprintln!("Other error: {}", e),
}

Integration with Other Crates

This crate is designed to work with other crates in the Sonos SDK ecosystem:

  • soap-client: Handles the actual SOAP communication and networking
  • sonos-discovery: Discovers devices on the network
  • sonos-stream: Manages event subscriptions and real-time updates

The typical flow is:

  1. Use sonos-discovery to find devices
  2. Use sonos-api to construct requests and parse responses
  3. Use soap-client to send the requests over the network
  4. Use sonos-stream for real-time event handling

Design Principles

  • Stateless: No connection management or device state tracking
  • Type Safety: Strong typing for all requests and responses
  • Separation of Concerns: Pure request/response transformation
  • Extensible: Easy to add new operations following the same patterns
  • Error Transparency: Clear error types for different failure modes

Adding New Operations

To add a new operation:

  1. Create a new module under the appropriate service directory
  2. Define request and response types with proper serde annotations
  3. Implement the SonosOperation trait
  4. Add comprehensive tests for payload construction and response parsing

Example structure:

pub struct MyOperation;

#[derive(Serialize)]
pub struct MyRequest {
    // request fields
}

#[derive(Deserialize)]
pub struct MyResponse {
    // response fields  
}

impl SonosOperation for MyOperation {
    type Request = MyRequest;
    type Response = MyResponse;
    
    const SERVICE: Service = Service::AVTransport;
    const ACTION: &'static str = "MyAction";
    
    fn build_payload(request: &Self::Request) -> String {
        // construct XML payload
    }
    
    fn parse_response(xml: &Element) -> Result<Self::Response, ApiError> {
        // parse XML response
    }
}

Testing

The crate includes comprehensive tests for all operations, covering:

  • Payload construction with various input parameters
  • Response parsing with valid XML
  • Error handling for malformed or missing XML elements
  • Edge cases and validation scenarios

Run tests with:

cargo test -p sonos-api