Skip to content

Architecture

The Sonos SDK is built as a layered Rust workspace. Each layer has a single responsibility, and you only interact with the top layer (sonos-sdk).

┌─────────────────────────────────────────────┐
│ sonos-sdk (your entry point) │
│ SonosSystem → Speaker → Property Handles │
├─────────────────────────────────────────────┤
│ sonos-discovery │ sonos-api │
│ (SSDP multicast) │ (UPnP operations) │
├─────────────────────────────────────────────┤
│ sonos-state (reactive state) │
│ sonos-event-manager (subscription refcount)│
│ sonos-stream (event/polling switch) │
├─────────────────────────────────────────────┤
│ callback-server │ soap-client │
│ (HTTP listener) │ (SOAP transport) │
└─────────────────────────────────────────────┘
Sonos Devices (port 1400)

When you call SonosSystem::new(), the SDK sends an SSDP multicast to find all Sonos devices on your network. This is fast (~2 seconds) and happens once.

let sonos = SonosSystem::new()?; // discovers all speakers
let speakers = sonos.speakers(); // vec of Speaker handles

Every property has a fetch() method that makes a live SOAP call to the device. No event infrastructure is involved — it’s a simple HTTP request to port 1400.

let volume = speaker.volume.fetch()?; // GET request to device
speaker.volume.set(40)?; // SET request to device

The first time you call watch() on any property, the SDK lazily initializes:

  • A callback HTTP server to receive UPnP NOTIFY events
  • A subscription to the relevant service on the device
  • Automatic firewall detection with fallback to polling
for event in sonos.iter() {
let volume = speaker.volume.watch()?; // initializes event system on first call
println!("Volume: {:?}", volume.value());
}

The SDK is designed so you pay only for what you use:

ActionWhat happens
SonosSystem::new()SSDP discovery only (~2s)
.fetch() or .set()Single HTTP request per call
.watch() (first call)Starts callback server + subscribes
.watch() (subsequent)Reuses existing subscription (ref-counted)

If you never call watch(), the event system never starts. If you only read properties with fetch(), no background threads are spawned.

When a firewall blocks UPnP event callbacks (common on corporate networks), the SDK detects this proactively and switches to polling mode. Your code doesn’t change — watch() and sonos.iter() work identically regardless of the underlying transport.

CrateRoleYou use it?
sonos-sdkPublic API, property handles, navigationYes
sonos-discoverySSDP network scanningVia SonosSystem
sonos-apiUPnP operation types and executionVia property handles
sonos-stateReactive state with watch channelsVia .watch()
sonos-event-managerRef-counted subscription lifecycleInternal
sonos-streamEvent/polling transport switchingInternal
callback-serverHTTP server for UPnP callbacksInternal
soap-clientSOAP envelope building and HTTPInternal