sonoscli – Design & Specification
This document describes the overall architecture, command surface, and key implementation details of sonoscli.
#Goals
- Discover all speakers reliably and present room names consistent with the Sonos app.
- Provide fast, scriptable playback control from the terminal.
- Be coordinator-aware so commands behave like the Sonos controller apps.
- Support Spotify enqueue/play without requiring Spotify credentials (using Sonos-linked Spotify).
- Support Sonos-side music-service search (SMAPI) when services are linked in the Sonos app.
- Optionally support Spotify search via Spotify Web API (requires credentials).
- Keep the implementation small, modern Go, and easy to extend.
Non-goals (for now):
- Full music-service browsing trees (Sonos SMAPI catalog browsing is large/complex and service-dependent).
- Full credential management (keychain/encryption, profiles) beyond the current local token store.
#High-level Architecture
cmd/sonos/ # main entrypoint
internal/cli/ # Cobra commands and output formatting
internal/sonos/ # Sonos UPnP/SOAP, SSDP discovery, topology parsing
internal/spotify/ # Spotify Web API (client credentials) search helper
docs/spec.md # this document
#Data flow
- Discovery
- Primary: SSDP M-SEARCH → find any Sonos responder → query topology (
ZoneGroupTopology.GetZoneGroupState) for full room list. - Fallback: local subnet scan for TCP
1400+device_description.xml→ then topology query. - Output is based on topology members, which match the Sonos app’s room list.
- Control
- Commands resolve to a group coordinator when required (transport controls must go to the coordinator).
- Commands call UPnP SOAP actions on port
1400using a minimal SOAP client.
#Sonos Protocols Used
#SSDP (discovery)
- Multicast:
239.255.255.250:1900 - Query:
M-SEARCHforurn:schemas-upnp-org:device:ZonePlayer:1 - Result: device
LOCATIONpointing athttp://<ip>:1400/xml/device_description.xml
SSDP can be unreliable on some networks (multicast blocked, flaky Wi‑Fi), so we do not depend on it for the final device list.
#UPnP SOAP (control and topology)
All calls are HTTP POST SOAP requests to http://<speaker-ip>:1400/.../Control.
Key services/actions:
ZoneGroupTopology:GetZoneGroupState→ returns aZoneGroupStateXML payload which describes groups and members.
AVTransport:Play,Pause,Stop,Next,PreviousSetAVTransportURI(used for grouping join, and queue management)AddURIToQueue(enqueue Spotify items)BecomeCoordinatorOfStandaloneGroup(ungroup)
RenderingControl:GetVolume,SetVolume,GetMute,SetMute(plus group volume where supported)
#Command Surface
#Discovery
sonos discover– list speakers (room name, IP, UDN)--format jsonsupported.
#Status
sonos status --name "<Room>"(orsonos now) – show playback status, current URI, time, volume/mute, and parsed now-playing metadata when available (Title/Artist/Album/AlbumArt).--format jsonsupported.
#Transport
sonos play|pause|stop|next|prev --name "<Room>"
#Watch (events)
sonos watch --name "<Room>" [--duration 30s]- Subscribes to
AVTransportandRenderingControlUPnP events and prints changes as they arrive. --format jsonprints one JSON object per line (stream-friendly).
#Volume / mute
sonos volume get|set --name "<Room>" <0-100>sonos mute get|on|off|toggle --name "<Room>"
#Queue
sonos queue list --name "<Room>" [--start N] [--limit N](and--format json|tsv)sonos queue play --name "<Room>" <pos>(1-based)sonos queue remove --name "<Room>" <pos>(1-based)sonos queue clear --name "<Room>"
#Favorites
sonos favorites list --name "<Room>" [--start N] [--limit N](and--format json|tsv)sonos favorites open --name "<Room>" --index <N>sonos favorites open --name "<Room>" "<title>"
#Other sources
sonos play-uri --name "<Room>" "<uri>" [--title "..."] [--radio]sonos linein --name "<Room>" [--from "<RoomWithLineIn>"]sonos tv --name "<Room>"
#Scenes
sonos scene save <name>– capture grouping + per-room volume/mutesonos scene apply <name>– restore grouping + per-room volume/mutesonos scene list– list saved scenes (--format json|tsvsupported)sonos scene delete <name>– delete a scene
#Spotify (no Spotify credentials required)
Spotify must already be linked in the Sonos app.
sonos open --name "<Room>" <spotify-uri-or-share-link>- Adds to queue and starts playback.
sonos enqueue --name "<Room>" <spotify-uri-or-share-link>- Adds to queue without playing.
Accepted Spotify refs:
spotify:track:<id>,spotify:album:<id>,spotify:playlist:<id>,spotify:show:<id>,spotify:episode:<id>https://open.spotify.com/...share links
Implementation detail: we generate Sonos-compatible DIDL metadata similar to SoCo’s ShareLink logic and try common Spotify Sonos service numbers (2311, 3079).
#Spotify search (requires Spotify Web API credentials)
sonos search spotify "<query>" [--type track|album|playlist|show|episode]- Requires
SPOTIFY_CLIENT_IDandSPOTIFY_CLIENT_SECRET(or--client-id/--client-secret). - Prints
spotify:<type>:<id>URIs usable withsonos open/sonos enqueue. --open/--enqueueoptionally play/enqueue the selected result (--index).
#Sonos-side music-service search (SMAPI; no Spotify Web API credentials)
Spotify must be linked in the Sonos app. Some services also require a one-time DeviceLink/AppLink flow.
sonos smapi services– list available services and auth types.sonos smapi categories --service "Spotify"– list available search categories for a service.sonos auth smapi begin|complete --service "Spotify"– link your account for SMAPI access.sonos smapi search --service "Spotify" --category tracks "<query>"– prints canonical Spotify URIs usable withsonos open/sonos enqueue.sonos smapi browse --service "Spotify" --id root– browse containers via SMAPIgetMetadata(drill down by passing returned ids).
#Grouping
sonos group status– show all groups, coordinators, and members--format json|tsvsupported.sonos group join --name "<Room>" --to "<OtherRoomOrIP>"- Sends
AVTransport.SetAVTransportURIto the joining speaker withx-rincon:<COORDINATOR_UUID>. - Room selection supports fuzzy substring matching; ambiguous matches return suggestions.
sonos group unjoin --name "<Room>"- Sends
AVTransport.BecomeCoordinatorOfStandaloneGroupto the target speaker. sonos group party --to "<RoomOrIP>"- Joins all visible speakers to the target group.
sonos group dissolve --name "<Room>"- Ungroups every member of the target group (leaves members first, coordinator last).
sonos group volume get|set --name "<Room>" <0-100>sonos group mute get|on|off|toggle --name "<Room>"
#Coordinator Awareness
For transport-like actions (play/pause/stop/next/prev, queue operations, Spotify enqueue/open), the effective target should be the group coordinator. sonoscli resolves the coordinator via topology and sends commands to that device.
Grouping actions are different:
group join: sent to the joining speaker.group unjoin: sent to the target speaker.
#Output Formats
- Human-readable output is tab/line oriented and intended for terminal use.
--format plain|json|tsvcontrols output formatting where applicable.--jsonis retained as a deprecated alias for--format json.
#Testing Strategy
- Pure parsing and transformation logic has unit tests:
- SSDP parsing
- SOAP response/error parsing
- Topology parsing (
ZoneGroupState) - Spotify ref parsing and Spotify Web API search parsing
- CLI commands with external dependencies are tested using dependency injection:
- Spotify search CLI tests stub a searcher and a Sonos enqueuer.
- Grouping CLI tests stub a topology getter and a grouping client.
Integration tests (real speakers) are intentionally not part of CI.
#Tooling / CI
- Formatting:
gofmt - Lint:
golangci-lint(configured in.golangci.yml) - Tests:
go test ./... - CI: GitHub Actions runs format check,
go vet, tests, and lint.
#Inspiration
SoCo (Python) is a major reference for Sonos protocol patterns and music-service mechanics:
https://github.com/SoCo/SoCo