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 — one piano, which is the honest measure of it. If you have another, your report is the most useful thing you can send.
Not affiliated with, endorsed by, or sponsored by Yamaha Corporation. Yamaha, Disklavier and ENSPIRE are trademarks of Yamaha Corporation, used here only to identify the hardware this library talks to.
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 |
| Song database | async_get_song_db, async_lookup_song, async_search |
| 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 |
The song database is the piano's own (/ctrl/song.json): one fetch describes every song in
every library, with what the listings leave out — format, composer, performer, length.
async_search ranks titles across it and returns candidates, where the piano's own search
can only play its single best guess.
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. - The sequencer is deaf while it loads.
load_songanswers at once and the load takes about two seconds, during which a seek or aplayanswers HTTP 200 and is dropped.async_restore_playbackwaits the load out and reads the position back; if you cue and then seek or play yourself, wait untilplayback_statushas leftLOAD. - Radio is a mode, and it takes the piano over. While a channel plays,
play,pause,stop,next_songand everyload_*andplay_*answer HTTP 200 and are ignored, so checkCurrentInfo.is_radiobefore trusting a transport command.current_infogoes blank; the programme is inMasterState.radio_channel/radio_title. Onlyasync_stop_radioends it, andasync_notifydoes that for you and puts the channel back afterwards. A paused position does not survive a visit to the radio. - Reading the radio channel list stops the music.
get_radio_channel_listlooks like a read and stops the sequencer: a playing song falls silent, a paused one rewinds. Soasync_get_radio_channelscaches after its first read, andasync_searchleaves radio out unless you passinclude_radio=True. Read the list once, at a quiet moment. - A status can be
None.power_status,quiet_statusandplayback_statusparse toNone, with one warning logged, for a value this library has no name for — never to a plausible guess.quiet_statusreadsHEADPHONEwhile headphones are plugged in, andplayback_statushasLOADandRADIOas well as the two you would expect. - The sequencer daemon can wedge, and only a reboot clears it. The piano keeps answering and keeps accepting selections, but no song finishes loading and nothing plays. What sets it off is not established; rapid back-to-back commands are the suspect, so this library paces the compound operations it performs, and you should pace yours. See docs/enspire-api.md §7.14.
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 |
|---|---|
| Session | async_connect, async_close — or use it as an async context manager |
| 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. -
Names on the share ignore case. Uploading
Clair de Lune.midover a storedclair de lune.midoverwrites it under its old name.async_sync_directorytherefore matches remote paths case-insensitively, so a file renamed only in its capitalisation keeps the name the share already has — rather than being re-sent and then pruned under the old name, which would delete the only copy. -
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.
Stability
The version is still below 1.0, deliberately: the last incompatible changes are being made now, while they are cheap, so that they need not be made later — the changelog lists them. From 1.0.0 the project follows Semantic Versioning, and this is what the number will and will not promise.
What is covered. The names in aiodisklavier.__all__, imported from aiodisklavier
itself: their signatures, the fields of the models, the members of the enumerations, and the
exception each failure raises. Within a major version those only grow. Models are
keyword-only and new fields arrive with defaults, so code that constructs one — a test
fixture, say — keeps working.
What is not.
- Submodule paths.
aiodisklavier.client,.const,.models,.shareand.exceptionsare an arrangement of files, and may be rearranged. Import from the top level. Anything spelled with a leading underscore is private,python -m aiodisklavier's output included. - The values of tuning constants.
DEFAULT_TIMEOUT,MAX_RESPONSE_BYTESand their kind keep their names and may change their numbers in a minor release, as may the settles and polling intervals behindasync_notifyandasync_restore_playback. - New enumeration members. A firmware state this library learns to name stops parsing to
Noneand starts parsing to a new member, in a minor release. Write yourmatchwith a default arm. - The piano. Features built on the internal
/ctrl/endpoints — seeking, repeat, the extended state, the song database, reindexing, restoring a position — depend on an interface Yamaha has never published or versioned. They are best-effort: a firmware update can break them, and a fix for that is a patch release, not a breach of this policy.
What 1.0 will claim, and no more: that the API above is stable, and that it was verified on an ENSPIRE PRO grand running 5.24.00. Not that it has been tried on other models, regions or firmware — it has not.
Deprecation. Something on its way out keeps working, and warns with
DeprecationWarning, for at least one minor release before it is removed in the next major.
Python. Every version CI tests, currently 3.11 to 3.14. Dropping one that has reached its end of life is a minor release.
Reporting from another piano
Everything here was learned from one instrument, so what yours says is news. One command describes it, read-only and silent:
python -m aiodisklavier 192.168.1.50
It prints which fields your piano reports and which values its status fields take, flagging anything this library does not recognise, and it is made to be pasted into an issue as it stands. It reports shapes rather than contents: no song titles, file names, ids or addresses, and nothing from the account and passcode fields the piano keeps in the same state file. It never asks for the radio channel list, so it will not interrupt anything that is playing.
The Home Assistant integration is built on this library; a problem you meet there that traces back to the piano belongs here too.
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.
A fake can only repeat what one piano once did, so there is also an opt-in suite that runs the same library against a real one — read-only unless you ask for more:
DISKLAVIER_HOST=192.168.1.50 .venv/bin/python -m pytest -m hardware
See CONTRIBUTING.md for its three levels, and for the caution that goes with the third.
Licence
MIT — see LICENSE.
This project documents and interoperates with a local network interface exposed by hardware the owner already has on their own network. It contains no Yamaha source code, firmware or musical content, and circumvents no access control: the piano's local API is unauthenticated as shipped. The protocol was derived by observing traffic, not by decompiling anything.
Yamaha, Disklavier and ENSPIRE are trademarks of Yamaha Corporation. This project is not affiliated with, endorsed by, or sponsored by Yamaha Corporation.
Metadata
Release files for aiodisklavier 0.3.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| aiodisklavier-0.3.0.tar.gz | 148.3 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| aiodisklavier-0.3.0-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 211.0 kB
Release files / aiodisklavier-0.3.0.tar.gz
| Download URL | aiodisklavier-0.3.0.tar.gz |
|---|---|
| Size | 148.3 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
cebc86c02e978cb7b2f7a0bf920eb5933e5561be577616eaf41495dde7eb9e96
|
|
BLAKE2b-256 checksum How to use checksums |
8566bdd0e88840674a94184542973314d337e8a4ae8f69fac96ffbc338529f6b
|
| 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 18, 2026.
Transparency logRelease files / aiodisklavier-0.3.0-py3-none-any.whl
| Download URL | aiodisklavier-0.3.0-py3-none-any.whl |
|---|---|
| Size | 62.8 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
575acdf11b07d84acb84e2fc1bd041769de414eca52f7bc7e8dedaa3fa0e4dc8
|
|
BLAKE2b-256 checksum How to use checksums |
df124ec4b5c3de534ae12c7c35c35941b3a3dc15165025a9407ffaa8bf144de9
|
| 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 18, 2026.
Transparency log