pysnapdog: async Python client for the SnapDog multi-room audio server
Typed. Fully asynchronous. Built for Home Assistant, usable everywhere.
pysnapdog talks to a SnapDog server over its REST API
and, as the library grows, over its WebSocket. It is the client library behind the SnapDog
integration for Home Assistant, and it has no Home Assistant
dependency of its own: you hand it an aiohttp session and it hands you typed objects.
Features
| ⚡ Async only | Built on aiohttp. You own the session, the library never opens or closes one. |
| 🧩 Typed models | Frozen dataclasses with slots. A response that lacks a field raises an error instead of producing a half-filled object. |
| 📡 Live updates | SnapDogListener keeps the WebSocket open, reconnects with a growing wait, and delivers events only after your initial read, so nothing is lost in between. |
| 📜 Checked against the contract | The tests run against a fake server built from the server's own openapi.json and asyncapi.json, which are pinned in spec/. |
| 🔐 API key in the header | Authorization: Bearer, never in a URL. TLS with or without certificate verification. |
| 🧯 One error family | SnapDogConnectionError, SnapDogAuthError, SnapDogResponseError (with SnapDogNotFoundError, SnapDogUnsupportedError, SnapDogBusyError), all derived from SnapDogError. |
| 🧪 Strict typing | mypy --strict clean, ships py.typed. |
| 🪶 No extra dependencies | aiohttp and nothing else. |
Installation
pip install pysnapdog
Requires Python 3.14 or newer, the version Home Assistant 2026.10 runs on.
Quick start
import asyncio
import aiohttp
from pysnapdog import SnapDogClient
async def main() -> None:
async with aiohttp.ClientSession() as session:
client = SnapDogClient(session, "snapdog.local", api_key="…")
version = await client.version()
print(f"{version.name} {version.version}")
for zone in await client.zones():
print(f"{zone.index}: {zone.name} ({zone.playback}, volume {zone.volume})")
asyncio.run(main())
api_key is only needed when the server has api_keys configured. For a server behind TLS pass
tls=True, and verify_tls=False for a self-signed certificate.
Control a zone
Indexes of zones and clients start at 1, as in the server's API. Zone commands return nothing, the new state arrives as an event. Calls on clients return the new value.
await client.set_zone_volume(1, 35) # absolute, or "+5" / "-3" for relative
await client.play_playlist(1, playlist=2)
await client.set_zone_repeat(1, RepeatMode.PLAYLIST)
await client.apply_zone_eq_preset(1, "rock")
await client.set_client_zone(3, 2) # move client 3 into zone 2
cover = await client.zone_cover(1) # Image(content=b"...", content_type="image/jpeg")
Live updates
The server sends no initial state. SnapDogListener opens the socket first, lets you read the state
over REST, and only then delivers the events that arrived meanwhile, in order. It does the same again
after every reconnect.
from pysnapdog import SnapDogListener, ZoneChanged, ZoneVolumeChanged
zones: dict[int, Zone] = {}
async def load() -> None:
zones.update({z.index: z for z in await client.zones()})
async def on_event(event) -> None:
if isinstance(event, ZoneVolumeChanged):
print(f"zone {event.zone}: volume {event.volume}, muted {event.muted}")
listener = SnapDogListener(client, on_event=on_event, on_connected=load)
listener.start() # from a running event loop
...
await listener.stop()
Events carry absolute values, never a delta. A client that falls behind is closed by the server with
code 1013, and the listener reconnects and calls on_connected again. A rejected API key ends the
listener, because retrying cannot help: the error is in listener.stopped_reason.
Errors
from pysnapdog import (
SnapDogAuthError,
SnapDogConnectionError,
SnapDogError,
SnapDogUnsupportedError,
)
try:
eq = await client.client_eq(3)
except SnapDogAuthError:
... # the key is missing or wrong
except SnapDogUnsupportedError:
... # HTTP 422: the client is not a SnapDog client, so it has no EQ
except SnapDogConnectionError:
... # the server is away, or the connection broke off
except SnapDogError:
... # anything else the library raises
SnapDogResponseError carries status and the server's own message in detail.
What is covered
| Area | Status |
|---|---|
| System: version, status, listeners, self-update | ✅ |
| Zones: transport, volume, mute, shuffle, repeat, seek, presence | ✅ |
| Zones: playlists, play track, playlist, Subsonic track, URL, cover | ✅ |
| Clients: volume, mute, latency, zone, name | ✅ |
| Equalizer for zones and clients, presets, speaker correction | ✅ |
| Media: playlists, tracks, covers | ✅ |
KNX programming mode, product info, .knxprod download |
✅ |
| WebSocket: seven event types, reconnect, resync | ✅ |
The server has 100 REST operations. Every one is either a library call that a test exercises, or on a
short list of single-field reads that the Zone, Client, TrackMetadata and ZonePlaylist models
already carry. scripts/check_coverage.py compares the library with the pinned openapi.json and
asyncapi.json and fails when an operation or event is neither covered nor excluded with a reason.
CI runs it, so the gap cannot grow unnoticed.
Development
The project uses uv.
git clone https://github.com/SnapDogRocks/pysnapdog.git
cd pysnapdog
uv sync
uv run ruff check . # lint
uv run ruff format --check . # format
uv run mypy # types, strict
uv run pytest --cov # tests, coverage floor 95 %
uv run python scripts/check_coverage.py # every operation and event of the server
Commits follow Conventional Commits. A local hook checks
them once you run lefthook install (see CONTRIBUTING.md).
Related projects
- SnapDog — the server this library talks to.
- snapdog-homeassistant — the Home Assistant integration built on this library (in preparation).
License
Metadata
Release files for pysnapdog 0.1.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| pysnapdog-0.1.0.tar.gz | 130.8 kB | Without provenance | Details for pysnapdog-0.1.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| pysnapdog-0.1.0-py3-none-any.whl | Python 3 | none | any | Details for pysnapdog-0.1.0-py3-none-any.whl |
Total release size: 154.9 kB
Release files / pysnapdog-0.1.0.tar.gz
| Download URL | pysnapdog-0.1.0.tar.gz |
|---|---|
| Size | 130.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cbf7fb468fb76ea47b93bbd81bbafa44a0615de1f3f3209c51b660b1956639ea
|
|
BLAKE2b-256 checksum How to use checksums |
5e4f8e21098d969f1fa637f6d6adadd0e3205aa602de4d012ae7bbc9af261ef3
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Release files / pysnapdog-0.1.0-py3-none-any.whl
| Download URL | pysnapdog-0.1.0-py3-none-any.whl |
|---|---|
| Size | 24.1 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
7d5cc8b66c9a2ae507e0ba24256e44baf02c2a729ca20aac78f4ebb4f2c5a6fb
|
|
BLAKE2b-256 checksum How to use checksums |
6fdbf874d1242944914f75b5316fcffbd4e3606bb4550ab40a2be09ecbec7ef1
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
No |
| Uploaded via |
uv/0.13.6 {"installer":{"name":"uv","version":"0.13.6","subcommand":["publish"]},"python":null,"implementation":{"name":null,"version":null},"distro":{"name":"Ubuntu","version":"24.04","id":"noble","libc":null},"system":{"name":null,"release":null},"cpu":null,"openssl_version":null,"setuptools_version":null,"rustc_version":null,"ci":true}
|
Security for pysnapdog 0.1.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
No provenance found
None of the files in this release were uploaded with provenance metadata.
A note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.