pyodio
Async Python client for odio — the live server state as high-level Python objects.
Part of the odio project — full documentation.
pyodio
Async Python client for odio-api — the universal remote for your Linux multimedia server.
pyodio gives you a high-level, stateful, event-driven API: connect once, and the library keeps a live mirror of the server state (MPRIS players, audio streams and outputs, systemd services, Bluetooth, upgrades) through the odio-api SSE stream, with automatic reconnection and resynchronization. Every entity carries both its current state and the commands that act on it.
Install
pip install pyodio
Requires Python ≥ 3.12. The only dependency is aiohttp.
Quick start
import asyncio
import pyodio
async def main():
async with pyodio.connect("http://odio.local:8018") as odio:
print(f"Connected to {odio.server.hostname} (odio-api {odio.server.api_version})")
# MPRIS players — live objects with transport controls
player = odio.players.find("spotify")
if player:
print(f"{player.title} — {player.artist} [{player.playback_status}]")
await player.play_pause()
await player.set_volume(0.5)
# Tracklist (players implementing MPRIS TrackList)
if player.tracklist_supported:
for track in player.tracks:
print(f"{'>' if track == player.current_track else ' '} {track.title}")
await player.go_to(player.tracks[-1])
# Master volume / outputs
await odio.audio.set_volume(0.4)
for output in odio.audio.outputs.values():
print(f"{'*' if output.is_default else ' '} {output.description}")
# React to live changes pushed by the server
odio.players.on_change(lambda change, p: print(f"[{change}] {p.app_name}: {p.title}"))
await asyncio.sleep(60)
asyncio.run(main())
The URL defaults to http://localhost:8018; pyodio.connect() also works with the odio = await pyodio.connect(...) style (then call await odio.close() yourself).
The two layers
OdioHub — high-level (recommended)
pyodio.connect() / pyodio.OdioHub fetches a full snapshot, then applies SSE deltas forever:
| Domain | State | Commands |
|---|---|---|
odio.players |
live Player entities by bus name, find(), playing, tracks/current_track |
play/pause/play_pause/stop/next/previous/seek/set_position/set_volume/set_loop/set_shuffle, cover_url, go_to/add_track/remove_track |
odio.audio |
master volume/muted, clients, outputs, default_output |
set_volume, set_muted, per-client/output volume & mute, make_default() |
odio.services |
Service entities by scope/name |
start/stop/restart/enable/disable |
odio.bluetooth |
adapter state + devices by MAC |
power_up/down, pairing_mode, scan, connect/disconnect |
odio.power |
can_reboot, can_power_off |
reboot(), power_off() |
odio.upgrade |
available, versions, live progress_percent |
check(), start() |
Only the domains whose backend is enabled server-side (odio.backends) are populated.
Niceties handled for you:
- Positions extrapolated:
player.positionprojects the last server beacon with the playback rate, so it is accurate between events (microseconds, like MPRIS). - Mute semantics: the server only supports toggling mute;
set_muted(True/False)compares with the live state and toggles only when needed. - Reconnection: the SSE stream reconnects with exponential backoff (1 s → 5 min) and re-snapshots the whole state after every reconnect. Watch it with
odio.connected/odio.on_connection_change(cb).
Subscriptions (all return an unsubscribe callable, listeners must not block):
odio.players.on_change(cb) # cb(change, player) change: added/updated/removed/position/tracklist
odio.audio.on_change(cb) # cb(change, client_or_output)
odio.services.on_change(cb)
odio.bluetooth.on_change(cb) # includes "discovered" during scans
odio.upgrade.on_change(cb) # includes "progress" during upgrades
odio.on_event(cb) # every raw SSE event (pyodio.OdioEvent)
odio.on_connection_change(cb) # cb(connected: bool)
OdioClient — low-level
A stateless, typed, one-method-per-endpoint REST client, if you want full control:
from pyodio import OdioClient
async with OdioClient("http://odio.local:8018") as client:
info = await client.get_server_info()
players = await client.get_players()
await client.player_play(players[0].bus_name)
Pass an existing aiohttp.ClientSession as second argument to reuse it (e.g. Home Assistant's shared session) — the client then never closes it. pyodio.stream_events(client) exposes the raw SSE stream as an async generator, and pyodio.EventStream adds supervision (reconnect, listeners) without the stateful hub.
Errors
All errors derive from pyodio.OdioError:
OdioConnectionError— server unreachable, stream lostOdioTimeoutError— request or keepalive timeoutOdioApiError— HTTP error from the server (.status,.message)
Development
uv sync
uv run pytest
uv run ruff check src tests
uv run mypy src
License
MIT — see LICENSE.
Metadata
Release files for pyodio 0.2.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pyodio-0.2.1.tar.gz | 97.7 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pyodio-0.2.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 123.3 kB
Release files / pyodio-0.2.1.tar.gz
| Download URL | pyodio-0.2.1.tar.gz |
|---|---|
| Size | 97.7 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
dc8a198a6346c6f7f9f846d3d10500f0df205bf0461124c6955d315533415768
|
|
BLAKE2b-256 checksum How to use checksums |
986958f5b4d14758dcf95d8e68763eac7846815688f395602e8458b987792239
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 26, 2026.
Transparency logRelease files / pyodio-0.2.1-py3-none-any.whl
| Download URL | pyodio-0.2.1-py3-none-any.whl |
|---|---|
| Size | 25.6 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
c5b37393d60e54a545cc116932d67f809651bea2511b48583884899d9160299c
|
|
BLAKE2b-256 checksum How to use checksums |
b856e37dba4413ef0287e8c8195a362e6d18853c920647eef62c144aca4c933d
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
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.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Aug 26, 2026.
Transparency log