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

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.

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)

Source distribution for pyodio 0.2.1
File Size Uploaded
pyodio-0.2.1.tar.gz 97.7 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for pyodio 0.2.1
File Interpreter ABI Platform
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 log

Release 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

Release history Release notifications | RSS feed

This release

0.2.1 This release

2 release files

0.2.0

2 release files

0.1.0

2 release 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