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_on and on_note_off
  • on_control_change, on_program_change and on_pitch_bend
  • on_sysex and on_message
  • on_connect and on_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_sysex gives the data without the 0xF0 and 0xF7 bytes, and send_sysex adds them when they are missing.
  • on_message exposes the raw bytes of every message, including aftertouch and system messages that have no trigger of their own.
  • The data in on_message and on_sysex is only valid while the automation runs, so these two triggers refuse actions that wait, such as delay and wait_until.
  • max_sysex_size sets 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).