Skip to main content

aiolibresync

An asyncio client for audio hubs built on the Libre Wireless LibreSync platform, such as the Platin Stereo Hub. It controls them over the local network, with no vendor app and no cloud.

The same hardware and protocol are sold under at least a dozen brands, including Platin, Buchardt, System Audio, Econik and Triangle. See docs/devices.md for the list and how confident each entry is.

  • Pure Python, no runtime dependencies, fully typed.
  • Push-driven: the hub announces volume, source, transport and metadata as they change, and a slow poll covers the two properties nothing announces.
  • Discovery over SSDP, plus a probe for an address you already know.
  • Written against the Home Assistant integration quality scale, but it does not depend on Home Assistant.

Status

Alpha. Everything the library sends has been sent to a real hub and its effect observed, on a Platin Stereo Hub running firmware 1.52. Other brands and firmware versions have not been tested yet, and reports from owners are very welcome. diagnostics() produces a redacted dump designed for exactly that.

Install

pip install aiolibresync

Python 3.12 or later.

Finding a hub

from aiolibresync import async_discover, async_probe

for device in await async_discover():
    print(device.host, device.udn, device.name)

device = await async_probe("192.168.1.50")  # an address you already have

async_discover() sends an SSDP search, fetches the device description from whatever answers, and confirms that the control port is open. The probe sends nothing to the control port: it only opens a TCP connection.

Two stable identifiers are available. The factory serial (await async_read_serial(host), or state.serial on a connected client) does not depend on the UPnP daemon and is the one to prefer. The UDN (device.udn) is what discovery sees. Either can be None: a unit may lack a valid factory serial, and the UDN is served by a UPnP daemon that occasionally stops on its own until the hub is power-cycled. See docs/devices.md.

Controlling it

import asyncio
from aiolibresync import DeviceState, LibreSyncClient

async def main() -> None:
    client = LibreSyncClient("192.168.1.50")
    ready = asyncio.Event()

    def on_state(state: DeviceState) -> None:
        print(state)
        if state.available:
            ready.set()

    client.subscribe(on_state)
    await client.async_connect()
    await ready.wait()
    await client.async_set_volume(30)
    await asyncio.sleep(60)
    await client.async_disconnect()

asyncio.run(main())

async_connect() returns as soon as the two sockets have been scheduled, not once they are open, so a command issued on the next line raises NotConnectedError. state.available is the readiness signal: it becomes true when both ports are connected and false again on any disconnection. The client reconnects on its own, so treat available as a condition that can change at any time.

Method What it does
async_set_power(on) reads the power state first and toggles only if needed, because the device has no discrete on or off
async_select_source(index) selects a source by the device's own index, from state.sources
async_set_volume(level) 0–100
async_media_play(), _pause(), _stop(), _next_track(), _previous_track() transport, for the streaming renderer
async_set_room_correction(enabled), async_set_manual_eq(enabled) the two DSP switches
async_select_eq_preset(preset) 1–3, the presets built in the vendor's app
async_refresh() re-reads everything
subscribe(callback) called with a new DeviceState on every change; returns an unsubscribe function
diagnostics() a redacted snapshot with frame counters and any unrecognised frames

Every command waits for the device to confirm the new state and raises ConfirmationTimeout if it does not.

Things that will surprise you

Power is not power. Switching the hub off is a stop: it ends the playback session. Switching it back on restores nothing and only allows playback again. When playback starts on a hub that is off, power-on arrives last, as a consequence of the session starting. The hub answers on both ports while "off". Keep available and power apart.

There is no mute you can set. The device accepts a mute write, reports the new value back, and leaves the audio alone. state.muted reflects mute set from the remote or the vendor's app, but the library offers no way to set it. See docs/protocol-media.md.

Read state.playback, not state.play_state. The hub reports playback twice. play_state is the streaming renderer's transport, and it says PLAYING on any physical input whether or not anything is connected. audio_state says whether sound is actually coming out. playback gives you the one to believe.

Room correction is never announced. It is polled, so a change made in the vendor's app shows up within one poll interval (POLL_INTERVAL, 30 s).

Preset 0 is the vendor app's EQ editor. While someone has the editor open, state.eq_preset is 0. It is a legitimate state, not an error.

Documentation

docs/devices.md what the device is, which brands share it, how a hub is found and identified
docs/protocol-system-control.md port 50006: framing, events, power, what must be polled
docs/protocol-media.md port 7777: the LUCI media session, message boxes, metadata
docs/commands.md every known frame on both ports, with how firmly each is known

Everything here was established by observing a real hub. Each frame in the reference is labelled with how it is known, from sent-and-observed down to inferred.

Development

python -m venv .venv
.venv/bin/pip install -e ".[dev]"
.venv/bin/pytest
.venv/bin/ruff check . && .venv/bin/ruff format --check . && .venv/bin/mypy

The tests run the codec against real traffic captured from a hub, with personal data replaced (see tests/fixtures/README.md), and the client against a fake hub that speaks both ports.

Independent reverse engineering, for interoperability. Not affiliated with Platin, Hansong, or Libre Wireless Technologies. LibreSync is a trademark of Libre Wireless Technologies, Inc. MIT licensed.

Release files for aiolibresync 0.4.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for aiolibresync 0.4.0
File Size Uploaded
aiolibresync-0.4.0.tar.gz 108.0 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for aiolibresync 0.4.0
File Interpreter ABI Platform
aiolibresync-0.4.0-py3-none-any.whl Python 3 none any Details

Total release size: 148.7 kB

Release files / aiolibresync-0.4.0.tar.gz

Download URL aiolibresync-0.4.0.tar.gz
Size 108.0 kB
Tags Source
SHA-256 checksum
How to use checksums
af88806df31e48fbb9805ef254bba352c716d244df3c7576762addb8a87fc4f5
BLAKE2b-256 checksum
How to use checksums
bcc0c7656e654d2b2629ad98204bd8410e3e783d096d366d0770181d95910c46
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 Sep 26, 2026.

Transparency log

Release files / aiolibresync-0.4.0-py3-none-any.whl

Download URL aiolibresync-0.4.0-py3-none-any.whl
Size 40.7 kB
Tags Python 3
SHA-256 checksum
How to use checksums
6f90d3923749df87ec680709247a0372d43fecc9b73d4a4867393f706fd02b31
BLAKE2b-256 checksum
How to use checksums
2d4a8559d5bd55ccef365f70a4899ade3487a891e466bb4874076c7de261b245
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 Sep 26, 2026.

Transparency log

Release history Release notifications | RSS feed

This release

0.4.0 This release

2 release files

0.3.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