Metadata-Version: 2.4
Name: aiodisklavier
Version: 0.1.1
Summary: Async client for the Yamaha Disklavier ENSPIRE local HTTP API
Author: Reuben Bijl
License-Expression: MIT
Project-URL: Homepage, https://github.com/reubenbijl/aiodisklavier
Project-URL: Changelog, https://github.com/reubenbijl/aiodisklavier/releases
Project-URL: Issues, https://github.com/reubenbijl/aiodisklavier/issues
Project-URL: Protocol reference, https://github.com/reubenbijl/aiodisklavier/blob/main/docs/enspire-api.md
Keywords: yamaha,disklavier,enspire,piano,home-assistant
Classifier: Development Status :: 4 - Beta
Classifier: Framework :: AsyncIO
Classifier: Intended Audience :: Developers
Classifier: Operating System :: OS Independent
Classifier: Programming Language :: Python :: 3
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Programming Language :: Python :: 3.13
Classifier: Programming Language :: Python :: 3.14
Classifier: Topic :: Home Automation
Classifier: Typing :: Typed
Requires-Python: >=3.11
Description-Content-Type: text/markdown
License-File: LICENSE
Requires-Dist: aiohttp>=3.9
Requires-Dist: yarl>=1.9
Provides-Extra: test
Requires-Dist: pytest>=8; extra == "test"
Requires-Dist: pytest-asyncio>=0.23; extra == "test"
Requires-Dist: pytest-cov>=5; extra == "test"
Provides-Extra: lint
Requires-Dist: ruff>=0.6; extra == "lint"
Requires-Dist: mypy>=1.11; extra == "lint"
Provides-Extra: dev
Requires-Dist: aiodisklavier[lint,test]; extra == "dev"
Dynamic: license-file

# aiodisklavier

Async Python client for the **Yamaha Disklavier ENSPIRE** local HTTP API.

Talks to the piano directly over your own network. Verified against firmware **5.24.00** on a Disklavier ENSPIRE PRO grand.

## Install

```bash
pip install aiodisklavier
```

## Use

```python
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](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.

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

## Development

```bash
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
