aiodisklavier
Async Python client for the Yamaha Disklavier ENSPIRE local API — HTTP for control and state, SMB for getting your own music onto the instrument.
Talks to the piano directly over your own network. Verified against firmware 5.24.00 on a Disklavier ENSPIRE PRO grand.
Install
pip install aiodisklavier
Use
import asyncio
import aiohttp
from aiodisklavier import Disklavier, SongGroup
async def main() -> None:
async with aiohttp.ClientSession() as session:
piano = Disklavier("192.168.1.50", session)
info = await piano.async_get_current_info()
print(info.song_title, info.playback_status, info.position_seconds)
# Fuzzy title search runs on the piano itself.
await piano.async_play_search("Clair de lune")
# Play one song and stop, rather than continuing through the library.
await piano.async_play_song(24, SongGroup.DOWNLOADED_SONGS, single=True)
asyncio.run(main())
Finding the piano is a plain SSDP M-SEARCH for urn:schemas-upnp-org:device:Disklavier:1; the library exposes that device type as UPNP_DEVICE_TYPE.
What it covers
| Area | Methods |
|---|---|
| State | async_get_static_info, async_get_current_info, async_get_master_state |
| Transport | async_play, async_pause, async_stop, async_play_pause, async_next_song, async_previous_song, async_restart_song, async_seek |
| Volume | async_set_volume, async_volume_up, async_volume_down |
| Power | async_turn_on, async_turn_off, async_set_power |
| Voicing | async_set_quiet_mode, async_set_repeat |
| Playback | async_play_song, async_play_search, async_play_genre, async_play_album, async_play_playlist, async_play_playlist_item |
| Browsing | async_get_songs, async_get_albums, async_get_songs_in_album, async_get_playlists, async_get_playlist_items |
| Radio | async_get_radio_channels, async_play_radio, async_stop_radio |
| Notifications | async_notify, async_snapshot_playback, async_restore_playback, async_play_test_chord |
| Library | async_refresh_library |
Firmware behaviours worth knowing
These are properties of the piano, not of this library, and each is easy to get wrong. The full reasoning, with provenance for every claim, is in docs/enspire-api.md.
- There is no stop state.
stopleavesplayback_statusreadingpauseat position zero. UseCurrentInfo.is_stoppedrather than looking for a stop constant. - Waking takes about twelve seconds, during which
power_statusreadswakeupand the piano ignores commands. The HTTP API answers normally while asleep, so reachability tells you nothing about power state. - Empty libraries are an error, not an empty list — HTTP 200 carrying
{"status": "error", "error_info": "no song"}. The browse methods translate that envelope back into the empty list it denotes, so callers just see[]. - State reads can come back truncated while a song is playing, because the daemon
rewrites those files in place. Reads retry automatically. Payloads may also carry a
trailing
\n\0, which is stripped rather than retried. - State lags a command. Reading
current_infostraight after aload_songor reselect returns the previous song. Allow a short settle before trusting a post-command read. - Radio's interaction with transport commands is not established. There is reason to think playback behaves differently while a radio channel is playing, but it has not been exercised on hardware — treat transport during radio as unknown.
async_play_test_chordmakes a sound — a C major triad for one second. It goes to the MIDI daemon rather than the sequencer, so it will not disturb a loaded song.
Getting music onto the piano
The piano also exports an SMB share — the PC Sharing Folder — and that is the only route
for adding your own MIDI. DisklavierShare covers it in the same async, typed idiom, so a
consumer of this library can browse, upload and mirror without shelling out to mount.
from aiodisklavier import Disklavier, DisklavierShare, PLAYABLE_SUFFIXES
async with DisklavierShare("192.168.1.50") as share:
for entry in await share.async_list():
print(entry.path, entry.size, entry.modified)
await share.async_upload("doorbell.mid", "ImpromptuApp/doorbell.mid")
# Mirror a local library. Only what changed is sent, so repeat runs are cheap and an
# interrupted one resumes.
result = await share.async_sync_directory(
"~/Music/disklavier", "ImpromptuApp", suffixes=PLAYABLE_SUFFIXES
)
if result.changed:
await piano.async_refresh_library() # nothing is playable until the piano reindexes
| Area | Methods |
|---|---|
| Browsing | async_list, async_walk, async_stat, async_exists, async_list_shares |
| Writing | async_upload, async_upload_bytes, async_makedirs, async_rename |
| Reading | async_download, async_download_bytes |
| Removing | async_delete, async_remove_directory, async_delete_tree |
| Mirroring | async_sync_directory |
Things worth knowing about the share, all covered in docs/enspire-api.md §8:
-
It speaks SMB1 and nothing newer. Samba 3.0.37 is the server software; SMB1/NT1 is the newest protocol dialect it can offer, because Samba did not gain SMB2 until 3.6. An SMB2 negotiate gets the socket closed. This is why the dependency is
pysmband notsmbprotocol, which starts at SMB 2.0.2 and cannot connect to the piano at all. -
The piano indexes exactly two folder levels.
<folder>/<subfolder>/song.midis the deepest path it will ever list, and each subfolder holding songs becomes an album. Anything below that copies without complaint and then simply is not in the library — no error from the write, none from the reindex.async_sync_directorylogs a warning when it uploads past the limit, but it will not restructure a tree for you:ImpromptuApp/Frédéric Chopin/Ballade No. 1 in G Minor, Op. 23.mid indexed ImpromptuApp/maestro/Frédéric Chopin/Ballade No. 1 in G Minor.mid invisible
-
Audio next to a MIDI file is that song's backing track.
song.mid+song.wav(or.mp3) with matching basenames is one SMF+Audio song, not two: the keys play the MIDI, the speakers play the audio, and the audio's length becomes the song's duration. Filter a sync to{".mid"}and every transcription still copies, indexes and plays — as a bare piano part with the band missing, silently.PLAYABLE_SUFFIXESincludes audio for exactly this reason, andasync_sync_directorywarns if it sends a MIDI whose companion was filtered out. -
Never copy to the share from macOS directly. Finder leaves a
._AppleDouble stub beside every file, the piano indexes those as songs in their own right, and loading one silently resets the piano to the first built-in song.async_sync_directoryexcludes them — along with.DS_Storeand friends — by default. -
Nothing you upload is playable until you reindex with
async_refresh_library, and the reindex reassigns song ids, so resolve songs by title afterwards rather than reusing an id. -
The share is served to guests on stock firmware — no password, full write access. Same trust model as the HTTP API: the LAN is the security boundary.
-
Transfers are serial. One SMB session carries one request, so
DisklavierShareserialises operations behind a lock. Use separate instances if you want parallelism.
Two APIs, one preferred
The piano exposes a versioned open API at /api/1.0/<command> and an internal, unversioned
set of endpoints under /ctrl/ that its own web UI drives. This library uses the open API
wherever possible and drops to /ctrl/ only for what the open API cannot do: seeking, repeat
and shuffle, the extended state block, reindexing, and the test chord.
The open API takes some finding: nothing the piano normally serves links to it, and neither
the phone app nor the piano's own web UI calls it. The one client-side trail is
/ctrl/api_test.html, a test harness Yamaha ships on the device — that is where the
/api/1.0/ form is visible. /api/api.php?_com=<command> is the same surface by another
name, verified equivalent down to the error codes.
Security
The piano's API is plaintext HTTP with no authentication (unless a passcode is set on the piano), and SSDP discovery answers are unauthenticated multicast — any host on the LAN can observe or impersonate the piano. The client hardens itself against a hostile device: response bodies are read against a size ceiling, redirects are refused, and device-supplied strings are treated as data. The transport itself still has no confidentiality or integrity, so keep this traffic on a trusted network and do not expose the piano or this client across an untrusted one.
The SMB share is the same picture, and a little worse: stock firmware serves it to guests
with full write access, and SMB1 has no meaningful integrity protection. DisklavierShare
refuses paths containing .. or control characters before they reach the wire, so a
device- or config-supplied path cannot be steered outside the share, and it never follows
a name the server invents. Treat write access to the share as equivalent to LAN access.
Development
python3 -m venv .venv
.venv/bin/pip install -e ".[test]"
.venv/bin/python -m pytest
Tests run against a real aiohttp test server that imitates the piano, so no hardware is
needed and the suite does not depend on any mocking library's grip on aiohttp internals.
Several tests encode behaviour found only on real hardware — those are commented as such,
because they look arbitrary otherwise.
Licence
MIT
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file aiodisklavier-0.2.1.tar.gz.
File metadata
- Download URL: aiodisklavier-0.2.1.tar.gz
- Upload date:
- Size: 98.8 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
80f0a6ab4811256d3de45294c20bc72761fc9ceb3b9e5dd210388f139d8579cb
|
|
| MD5 |
7d740bc1986e297df752be45125f20a3
|
|
| BLAKE2b-256 |
455d0b3ed3f528761e27025bdb8d734d49792d709ab266230f4886eeaca2253b
|
Provenance
The following attestation bundles were made for aiodisklavier-0.2.1.tar.gz:
Publisher:
publish.yml on reubenbijl/aiodisklavier
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aiodisklavier-0.2.1.tar.gz -
Subject digest:
80f0a6ab4811256d3de45294c20bc72761fc9ceb3b9e5dd210388f139d8579cb - Sigstore transparency entry: 2483167990
- Sigstore integration time:
-
Permalink:
reubenbijl/aiodisklavier@96698690542933196174990007ada01e08c0c754 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/reubenbijl
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@96698690542933196174990007ada01e08c0c754 -
Trigger Event:
release
-
Statement type:
File details
Details for the file aiodisklavier-0.2.1-py3-none-any.whl.
File metadata
- Download URL: aiodisklavier-0.2.1-py3-none-any.whl
- Upload date:
- Size: 45.6 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
a8cc3b3d2c9f5cca782bc94fcdf8a033726ed7cae95c89c210c19ca583777904
|
|
| MD5 |
161e5b8c3bd5a9fcc7ea31c2f79478b2
|
|
| BLAKE2b-256 |
94f1a84a599a1ec8eb63be711ecac862568ca4926f16dcaf472777d0ff2c5cab
|
Provenance
The following attestation bundles were made for aiodisklavier-0.2.1-py3-none-any.whl:
Publisher:
publish.yml on reubenbijl/aiodisklavier
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
aiodisklavier-0.2.1-py3-none-any.whl -
Subject digest:
a8cc3b3d2c9f5cca782bc94fcdf8a033726ed7cae95c89c210c19ca583777904 - Sigstore transparency entry: 2483168077
- Sigstore integration time:
-
Permalink:
reubenbijl/aiodisklavier@96698690542933196174990007ada01e08c0c754 -
Branch / Tag:
refs/tags/v0.2.1 - Owner: https://github.com/reubenbijl
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish.yml@96698690542933196174990007ada01e08c0c754 -
Trigger Event:
release
-
Statement type: