Skip to main content

🔊 sonosify

CI Python PyPI License

Programmatic Python API for discovering and controlling Sonos speakers on a local network — plus an optional, agent-friendly command-line interface built on top of it.

sonosify banner

This package ports the API core of steipete/sonoscli into Python. It ships two layers you can use independently:

  • Core library (sonosify): an async Python API for discovery, playback, volume, queue, groups, favorites, and live UPnP event subscriptions.
  • Cloud API (sonosify.cloud): OAuth2 and async access to the Sonos Control API, including Audio Clips with automatic ducking. It is part of the main package and adds no separate install extra.
  • CLI (sonosify command, via the cli extra): a scriptable, JSON-first command-line interface designed to be driven by humans and automation/agents alike.

Runnable scripts covering discovery, playback, volume, queue, groups, favorites, playback modes, and live events are in examples/.

Installation

Requires Python 3.13 or 3.14. Sonos speakers must be reachable on the same local network (SSDP discovery uses UDP multicast).

Core library only:

uv add sonosify
# or: pip install sonosify

With the CLI (adds Typer + Rich):

uv add "sonosify[cli]"
# or: pip install "sonosify[cli]"

From a local checkout of this repository:

uv pip install -e ".[cli]"

Setting up a development environment (dependency groups, tests, linting, pre-commit) is covered in CONTRIBUTING.md.

Core Python API

The core API is fully async and centers on two entry points: SonosController for household-wide operations (discovery, groups, watching) and SonosClient for talking to one speaker directly.

import asyncio

from sonosify import SonosController


async def main():
    sonos = SonosController()
    system = await sonos.discover()

    print([speaker.room_name for speaker in system.speakers])

    async with await sonos.client("Kitchen") as kitchen:
        await kitchen.pause()
        await kitchen.set_volume(25)
        print(await kitchen.now_playing())


asyncio.run(main())

Lower-level direct device access is also available, bypassing discovery entirely:

import asyncio

from sonosify import SonosClient


async def main():
    async with SonosClient("192.168.1.42") as speaker:
        await speaker.play_uri("https://example.com/live.mp3", radio=True, title="Example Radio")


asyncio.run(main())

Setting up cloud credentials

The Sonos Control API (sonosify.cloud) talks to Sonos's cloud service instead of your local network, so it needs its own OAuth2 credentials. Get them from the Sonos Developer Platform:

  1. Sign in at the Sonos integration manager and create a new integration.
  2. Register a redirect URI, e.g. http://localhost:8000/callback for local use. Sonos requires it to exactly match the URI your app uses to complete the OAuth flow.
  3. Copy the generated Client ID and Client Secret.

Compared to the local UPnP-based core library, the Cloud API:

  • Works from anywhere, without being on the same network as the speakers — useful for cloud-hosted bots, voice assistants, or remote automation.
  • Supports Audio Clips with automatic ducking, playing short announcements over whatever is currently playing.
  • Is an official, stable API maintained by Sonos, rather than the unofficial UPnP protocol the core library uses.

Once you have credentials, copy .env.example to .env and fill them in:

Copy-Item .env.example .env

Configuration is loaded through pydantic-settings; process environment variables take precedence over .env.

Sonos Control API (sonosify.cloud)

See Setting up cloud credentials above to obtain a Client ID, Client Secret, and redirect URI before using it.

The reusable API also accepts credentials explicitly:

import asyncio

from sonosify import SonosCloudAuth, SonosCloudClient


async def main():
    auth = SonosCloudAuth(
        client_id="YOUR_CLIENT_ID",
        client_secret="YOUR_CLIENT_SECRET",
        redirect_uri="https://agent.example.com/oauth/sonos",
    )

    # Exchange the callback's authorization code once:
    # await auth.async_exchange_code("CODE_FROM_CALLBACK")

    async with SonosCloudClient(auth, app_id="com.example.voice-agent") as sonos:
        clip = await sonos.load_audio_clip(
            "RINCON_12345678901400:1",
            "http://192.168.1.50:8000/tts/response.mp3",
            name="Agent Voice",
            volume=30,
        )
        print(clip.id)


asyncio.run(main())

SonosCloudAuth builds authorization URLs, exchanges authorization codes, caches access/refresh tokens in the platform config directory, and refreshes expired tokens. SonosCloudClient exposes households, groups and players; player/group lookup by name; Audio Clips; playback controls, status, metadata and seek; player/group volume; and home-theater Night Mode/Speech Enhancement.

Token persistence is injectable through CacheHandler. The default CacheFileHandler stores the token with owner-only permissions on POSIX systems; MemoryCacheHandler is useful for services that manage persistence elsewhere or deliberately keep credentials process-local:

from sonosify import SonosCloudAuth
from sonosify.cloud.cache_handler import MemoryCacheHandler

auth = SonosCloudAuth(
    client_id="YOUR_CLIENT_ID",
    client_secret="YOUR_CLIENT_SECRET",
    redirect_uri="https://agent.example.com/oauth/sonos",
    cache_handler=MemoryCacheHandler(),
)

The main clients, their common models and enums, and the shared SonosCloudError can be imported directly from sonosify or sonosify.cloud. Specialized cache handlers and errors remain available from their named sonosify.cloud modules. Names prefixed with _, including endpoint URLs, OAuth scope values, settings implementation, and cache-path selection, are private.

Cloud CLI configuration

The cloud command group is always present when the CLI is installed:

# Interactive onboarding wizard: prints the URL to open, prompts for the
# redirect URL (or bare code) once you're back, and saves the token. Run
# again later and it recognizes an existing session, offering to keep,
# refresh, or remove it.
sonosify cloud login
sonosify cloud logout

sonosify cloud households
sonosify cloud groups
sonosify cloud players
sonosify cloud clip "http://192.168.1.50:8000/tts/response.mp3" --player Kitchen --voice --volume 30
sonosify cloud pause --group Kitchen
sonosify cloud play --group Kitchen

SONOSIFY_CLOUD_ACCESS_TOKEN can replace the login/token-cache flow for short-lived automation. The client secret is never written to the token cache and must remain available whenever an expired token needs refreshing. Missing configuration produces a typed CloudConfigurationError; JSON CLI output includes a missing list and never includes credentials or tokens.

SonosClient covers transport control (play, pause, stop, next, previous, seek), volume/mute (get_volume, set_volume, adjust_volume, set_mute, toggle_mute), the queue (queue, enqueue_uri, clear_queue, remove_queue_item, seek_queue), playback modes (set_shuffle, set_repeat, set_crossfade, configure_sleep_timer), grouping (join, unjoin), favorites (favorites, open_favorite), and generic playback (open, open_track).

Live updates use Sonos UPnP event subscriptions:

import asyncio

from sonosify import AVTransportEvent, RenderingControlEvent, SonosController


async def main():
    sonos = SonosController()
    async with sonos.watch("Kitchen") as watcher:
        async for event in watcher:
            match event:
                case AVTransportEvent(transport_state=state, track=track):
                    print(state, track.title if track else "")
                case RenderingControlEvent(volume=volume, muted=muted):
                    print(volume, muted)


asyncio.run(main())

The watch API starts a local HTTP callback server. Your OS firewall may ask whether Python can accept incoming connections.

Errors are typed and derive from SonosifyError: SpeakerNotFoundError, AmbiguousSpeakerError (carries matches), DiscoveryError, NetworkError, UPnPError (carries code/description), and UnsupportedFeatureError.

See examples/ for runnable scripts:

uv run python examples/discover.py
uv run python examples/now_playing.py Kitchen
uv run python examples/watch.py Kitchen
uv run python examples/play_radio.py Kitchen https://example.com/live.mp3 "Example Radio"
uv run python examples/resume_playback.py Kitchen
uv run python examples/volume.py Kitchen 25
uv run python examples/favorites.py Kitchen
uv run python examples/favorites.py Kitchen "Jazz FM"
uv run python examples/track_queue.py Kitchen
uv run python examples/track_queue.py Kitchen add "x-rincon-mp3radio://example.com/live.mp3"
uv run python examples/groups.py
uv run python examples/group.py join Kitchen "Living Room"
uv run python examples/group.py leave Kitchen
uv run python examples/playback_modes.py Kitchen shuffle on
uv run python examples/playback_modes.py Kitchen sleep 0:30:00

Command-line interface

Install the optional CLI dependencies (Typer + Rich) as shown above, then use the sonosify command:

sonosify discover --format json
sonosify status --room Kitchen --format json
sonosify now-playing --room Kitchen
sonosify play --room Kitchen
sonosify pause --all
sonosify next --room Kitchen
sonosify previous --room Kitchen
sonosify set-volume 25 --room Kitchen
sonosify set-volume 20 --group "Living Room"
sonosify volume-up Kitchen 10
sonosify volume-down Kitchen 10
sonosify mute --room Kitchen --on
sonosify queue --room Kitchen
sonosify queue jump 7 --room Kitchen
sonosify queue remove 3 --room Kitchen
sonosify queue clear --room Kitchen
sonosify enqueue "x-rincon-mp3radio://example.com/live.mp3" --room Kitchen
sonosify open "https://example.com/live.mp3" --radio --title "Example Radio" --room Kitchen
sonosify track SPOTIFY_TRACK_ID --room Kitchen
sonosify favorites list --room Kitchen
sonosify favorites play "Jazz FM" --room Kitchen
sonosify seek 1:30 --room Kitchen
sonosify shuffle on --room Kitchen
sonosify repeat all --room Kitchen
sonosify crossfade on --room Kitchen
sonosify sleep 30m --room Kitchen
sonosify group "Living Room" --with Kitchen --with Office
sonosify ungroup --room Kitchen
sonosify groups --format json
sonosify ping --room Kitchen --format json
sonosify doctor --room Kitchen --format json
sonosify watch --room Kitchen --count 5 --format json
sonosify cloud players --format json
sonosify cloud clip "http://192.168.1.50:8000/tts/response.mp3" --player Kitchen
sonosify commands --format json
sonosify --version --format json

Targeting a speaker

Every speaker command accepts --room/-r and --ip. Existing positional room arguments remain available for compatibility. --ip opens the device directly and does not perform SSDP discovery. A successful discover refreshes the persistent room-to-IP cache; exact room names use that cache and fall back to discovery on a cache miss.

Default speaker

Set a default speaker once and omit the room name from then on:

sonosify config set --room Kitchen      # or: sonosify config set --ip 192.168.1.42
sonosify config show                    # show current defaults
sonosify config clear                   # remove defaults

sonosify play                           # now targets Kitchen automatically
sonosify volume 25                      # set Kitchen to 25
sonosify volume-up

An explicit room name or --ip on a command always overrides the configured default. The config is stored as JSON in your platform's app-config directory.

The same defaults can be supplied without writing a file:

$env:SONOSIFY_ROOM = "Kitchen"
$env:SONOSIFY_IP = "192.168.1.42"
$env:SONOSIFY_FORMAT = "json"
$env:SONOSIFY_TIMEOUT = "5"
$env:SONOSIFY_DEBUG = "1"

Precedence is command-line flag, then environment, then config file.

Output format

Use --format before or after a command:

sonosify --format json discover         # JSON speaker-list envelope
sonosify --format tsv queue --room Kitchen  # tab-separated rows
sonosify status --format json            # JSON object

plain (the default) renders rich tables and colored text for interactive use. Machine-readable stdout contains only result data. Diagnostics and debug traces go to stderr.

JSON output has schema_version: 1. Object/action commands add their fields beside it. List commands use this stable envelope:

{"schema_version": 1, "items": [{"room": "Kitchen", "ip": "192.168.1.42"}]}

now-playing and status include numeric position_s and duration_s. status combines playback state, volume, mute state, track, group identifier, and timing in one call.

watch --format json emits one object per line (NDJSON). It can terminate itself with --count N, --duration 10s, or --until PLAYING.

Errors and exit codes

In JSON mode, failures are emitted as a JSON object on stderr while stdout remains empty:

{"schema_version": 1, "error": "no speaker matching 'Kitcen'", "code": "speaker_not_found", "query": "Kitcen"}

Stable exit codes are:

Exit Meaning
0 Success
1 Other sonosify error
2 Speaker not found
3 Ambiguous speaker (matches is included in JSON)
4 Discovery or network failure / timeout
5 Sonos UPnP error (upnp_code and description are included)

Favorites and media

favorites list lists the Sonos favorites configured for a speaker's household; favorites play NAME plays one by exact title or unique substring match (an ambiguous match is rejected with the candidate titles listed). open URL starts playback of an arbitrary stream URL or Sonos-playable URI (--radio/--title set radio metadata for plain stream URLs), and track TRACK_ID plays a track by id, URI, or URL from an already-linked music service (--next enqueues as the next track, --enqueue only enqueues without starting playback).

Queue, groups, and playback modes

Queue management is available through queue (show), enqueue, queue clear, queue remove POSITION, and queue jump POSITION. group, ungroup, and groups expose multi-room grouping. Playback automation includes in-track seek, shuffle, repeat, crossfade, and the sleep timer.

The legacy overloaded volume command remains available. Agents should prefer the unambiguous get-volume and set-volume commands.

Use sonosify commands --format json for recursive command/parameter introspection and sonosify --version --format json for feature detection. sonosify doctor runs a basic connectivity and service health check on a speaker (round-trip latency plus a live volume read), separate from the plain reachability check of ping.

Debugging

Add --debug to print the underlying SOAP request/response traces to stderr:

sonosify --debug volume Kitchen

Playback of track and open works when the corresponding music service is already linked on your Sonos household.

Exports are collected in sonosify.__init__ for library consumers. The agent CLI work also adds RepeatMode, NetworkError, SonosClient.seek, get_play_mode/set_play_mode, set_shuffle, set_repeat, get_crossfade/set_crossfade, and configure_sleep_timer to the Python API.

Contributing

Bug reports and pull requests are welcome. See CONTRIBUTING.md for the development setup, test/lint commands, and what CI checks on every push.

License

MIT — see LICENSE.

Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

sonosify-0.3.0.tar.gz (9.8 MB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

sonosify-0.3.0-py3-none-any.whl (61.6 kB view details)

Uploaded Python 3

File details

Details for the file sonosify-0.3.0.tar.gz.

File metadata

  • Download URL: sonosify-0.3.0.tar.gz
  • Upload date:
  • Size: 9.8 MB
  • Tags: Source
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.2

File hashes

Hashes for sonosify-0.3.0.tar.gz
Algorithm Hash digest
SHA256 b80868513a506bed74e42fa5354f5371cc23143c7a8acd4240b13128e8d753e8
MD5 10bf270a688196227de5b9e353a82785
BLAKE2b-256 226963249888f7aa866f94a809a33b164cb7b3691367e866d3157eedaf67fd08

See more details on using hashes here.

File details

Details for the file sonosify-0.3.0-py3-none-any.whl.

File metadata

  • Download URL: sonosify-0.3.0-py3-none-any.whl
  • Upload date:
  • Size: 61.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? No
  • Uploaded via: uv/0.9.2

File hashes

Hashes for sonosify-0.3.0-py3-none-any.whl
Algorithm Hash digest
SHA256 687e50271a583fe52bd84037e0c4ba4c7e91dfb266762725cadbc9430476ef6a
MD5 1469f21bae4eec1389ea9012975f69b6
BLAKE2b-256 13bd56c6a9c50a4f7a9942043b05cae73c7a1750c5a7f3f087b00b5556f8b972

See more details on using hashes here.

Release history Release notifications | RSS feed

0.5.0

2 files

0.4.4

2 files

0.4.3

2 files

0.4.2

2 files

0.4.1

2 files

0.4.0

2 files

0.3.1

2 files

This release

0.3.0 This release

2 files

0.2.0

2 files

0.1.0

2 files

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page