Skip to main content

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. stop leaves playback_status reading pause at position zero. Use CurrentInfo.is_stopped rather than looking for a stop constant.
  • Waking takes about twelve seconds, during which power_status reads wakeup and 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_info straight after a load_song or 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_chord makes 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 pysmb and not smbprotocol, 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.mid is 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_directory logs 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_SUFFIXES includes audio for exactly this reason, and async_sync_directory warns 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_directory excludes them — along with .DS_Store and 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 DisklavierShare serialises 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

aiodisklavier-0.2.0.tar.gz (90.7 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

aiodisklavier-0.2.0-py3-none-any.whl (41.9 kB view details)

Uploaded Python 3

File details

Details for the file aiodisklavier-0.2.0.tar.gz.

File metadata

  • Download URL: aiodisklavier-0.2.0.tar.gz
  • Upload date:
  • Size: 90.7 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for aiodisklavier-0.2.0.tar.gz
Algorithm Hash digest
SHA256 15e40ce220b3157ea58f2a2fa141fdb4bd59a8ce0a415fa773b931592ed87835
MD5 43f57a17675225622a7fa29f08d94c0c
BLAKE2b-256 942f7e9607b32445aa2c1898473bbc8462b81ab74b9a90b79c57385c48e97d97

See more details on using hashes here.

Provenance

The following attestation bundles were made for aiodisklavier-0.2.0.tar.gz:

Publisher: publish.yml on reubenbijl/aiodisklavier

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file aiodisklavier-0.2.0-py3-none-any.whl.

File metadata

  • Download URL: aiodisklavier-0.2.0-py3-none-any.whl
  • Upload date:
  • Size: 41.9 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/7.0.0 CPython/3.13.14

File hashes

Hashes for aiodisklavier-0.2.0-py3-none-any.whl
Algorithm Hash digest
SHA256 9a30daae1a1cc048db7e499e681b0f13ca15a8c50d844b99a8d92c76ab5f10e8
MD5 446ff0c86bf437920134edbfc3a16d73
BLAKE2b-256 442400a851c32cd17748a3aceb68a4ae807c9fd9c23d33abc005dd1c111432b3

See more details on using hashes here.

Provenance

The following attestation bundles were made for aiodisklavier-0.2.0-py3-none-any.whl:

Publisher: publish.yml on reubenbijl/aiodisklavier

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

2 files

This release

0.2.0 This release

2 files

0.1.1

2 files

0.1.0

2 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