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)

        # 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 play/pause/play_pause/stop/next/previous/seek/set_position/set_volume/set_loop/set_shuffle, cover_url
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
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.1.0.tar.gz (94.8 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.1.0-py3-none-any.whl (24.0 kB view details)

Uploaded Python 3

File details

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

File metadata

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

File hashes

Hashes for pyodio-0.1.0.tar.gz
Algorithm Hash digest
SHA256 002c14c112e7d4b4f2a843ded7d8c058749129f368745763ed467dd4cc5c684f
MD5 de52a79078b0d4ce03f529ef97b30bbc
BLAKE2b-256 42844615dcfe48efbfade0fb392ac81b125e48ebe664fe74ab92bf03a0ed579a

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyodio-0.1.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.1.0-py3-none-any.whl.

File metadata

  • Download URL: pyodio-0.1.0-py3-none-any.whl
  • Upload date:
  • Size: 24.0 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.1.0-py3-none-any.whl
Algorithm Hash digest
SHA256 7de8baaba13e7ad02ddea7080c73d3f189523002075418cfaa240c2c3c475abf
MD5 8b00c9a18b94b4bbb4d3130decf1bfd4
BLAKE2b-256 8ed93ac05d74e66acac88a11fdef102e65aca767dc3f5baeeda954f905a58a14

See more details on using hashes here.

Provenance

The following attestation bundles were made for pyodio-0.1.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

0.2.0

2 files

This release

0.1.0 This release

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