Skip to main content

odio

pyodio

Async Python client for odio — the live server state as high-level Python objects.

Release PyPI License Build GitHub Sponsors

MPRIS PulseAudio Bluetooth systemd Power SSE Events

Part of the odio project — full documentation.

Python aiohttp uv GitHub Actions

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.position projects 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 lost
  • OdioTimeoutError — request or keepalive timeout
  • OdioApiError — 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.

Download files

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

Source Distribution

pyodio-0.2.0.tar.gz (97.4 kB view details)

Uploaded Source

Built Distribution

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

pyodio-0.2.0-py3-none-any.whl (25.6 kB view details)

Uploaded Python 3

File details

Details for the file pyodio-0.2.0.tar.gz.

File metadata

  • Download URL: pyodio-0.2.0.tar.gz
  • Upload date:
  • Size: 97.4 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for pyodio-0.2.0.tar.gz
Algorithm Hash digest
SHA256 96c2910cd1e76ff83fd7521462b48eca63f6e1e5ca4725109ad37ed0ec787280
MD5 8a8b2c583f1d9ce6e8c26c674bda7f37
BLAKE2b-256 a6c57225e6f0370d5b200b9e67a82602a354050d54700cb427c9e4bd14918990

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyodio-0.2.0.tar.gz:

Publisher: build.yml on b0bbywan/pyodio

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pyodio-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: pyodio-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 25.6 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.14

File hashes

Hashes for pyodio-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 a0716e9d93d4190a994ba4687568de10d61292b55cc926ea913abcb34f5e73df
MD5 30a00c324b09985e2e5b3cf0cb88b37a
BLAKE2b-256 7d9eb617c1467f41e6c3c7372927c41af0c093da3c14ca64c1a36090ca7caea0

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyodio-0.2.0-py3-none-any.whl:

Publisher: build.yml on b0bbywan/pyodio

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 files

0.1.0

2 files

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page