A Bluetooth MIDI keyboard or pad controller has a row of keys, knobs and pads that could just as well switch lights or start scenes. A new ble_midi component in ESPHome makes that possible without a computer in between. The ESP32 connects to the MIDI device as a BLE client, turns incoming notes and control changes into automation triggers, and can send MIDI back. The pull request by bogdanr was merged into the development branch on 25 October 2026, so it should arrive with ESPHome 2026.11.
Four months on real hardware
The author has run this code for about four months as his own external component, on an M-Vave SMC-PAD and a Korg microKEY Air. For the core he cut it down to the generic part. The component creates no entities on its own and contains no device-specific mappings. Per-device profiles and mapping notes to lights stay in his external repository and build on the triggers that are now in ESPHome.
What it can do
The component is a hub on top of ble_client. It offers these triggers:
on_note_onandon_note_offon_control_change,on_program_changeandon_pitch_bendon_sysexandon_messageon_connectandon_disconnect
To send MIDI there are ble_midi.send_note_on, send_note_off, send_control_change, send_program_change, send_pitch_bend, send_sysex and send_raw, plus a ble_midi.connected condition. This is the example from the pull request:
esp32_ble_tracker:
ble_client:
- mac_address: 01:02:03:04:05:06
id: pad_client
ble_midi:
id: pad
ble_client_id: pad_client
on_note_on:
- logger.log:
format: "Note %u on, velocity %u, channel %u"
args: [note, velocity, channel]
on_control_change:
- logger.log:
format: "CC %u = %u"
args: [control_number, value]
button:
- platform: template
name: "Send a note"
on_press:
- ble_midi.send_note_on:
id: pad
note: 60
velocity: 100
In a real setup the logger.log lines would be replaced with actions such as toggling a light or calling a Home Assistant action.
Details to know
- Channels are numbered 0 to 15 in both triggers and actions, as on the wire. Many MIDI apps show 1 to 16. The author left out a translation because it could not be applied to a channel set with a lambda.
on_sysexgives the data without the0xF0and0xF7bytes, andsend_sysexadds them when they are missing.on_messageexposes the raw bytes of every message, including aftertouch and system messages that have no trigger of their own.- The data in
on_messageandon_sysexis only valid while the automation runs, so these two triggers refuse actions that wait, such asdelayandwait_until. max_sysex_sizesets the size of a static buffer, so nothing is allocated at run time. A longer SysEx message is dropped with a warning.
The pull request was tested on the ESP32 with ESP-IDF. The documentation is still an open pull request (esphome.io #7219).