ESPHome Custom Component

ESP32 BLE Keyboard

Transform your ESP32 into a Bluetooth HID keyboard. Control paired Windows, Android, and iOS devices directly from Home Assistant — send keystrokes, combos, and power commands. Use passkey_mode: legacy for Windows (Just Works for Android), passkey_mode: secure_connections for iOS. Tested on Windows 11, Android 16, and iOS.

View on GitHub ⌚ Keycode Reference

Features

đŸ””

Native BLE HID

Uses the ESP-IDF Bluedroid GATTS API directly. Recognised as a standard keyboard by Windows, Android, and iOS. Full HOGP-compliant BLE HID with Device Information and Battery services.

🔒

Secure Pairing

Optional 6-digit static passkey (PIN) for secure bonding. For fastest pairing, use Just Works (no passkey). With passkey, use passkey_mode: legacy for Windows. For iOS, use passkey_mode: secure_connections (required). Android uses Just Works only.

⌚

Key Combos

Send any modifier + key combination using hex keycodes. Win+R, Ctrl+C, Alt+F4 and more.

đŸ’€

Power Commands

Native HID sleep, hibernate and shutdown — clean OS-level signals, no Run dialog or lingering key state.

đŸŽ”

Media Keys

Volume up/down, mute, play/pause, next/prev track, stop and record via HID consumer control reports.

🏠

Home Assistant

Each button appears as an entity in Home Assistant. Trigger from automations, dashboards or scripts. Direct API implementation.

đŸ“¶

Pairing State Sensor

Optional binary sensor turns on after a successful GAP pairing event and turns off on disconnect/unpair.

✏

Custom Text Input

Type any text in Home Assistant and send it directly to the paired host device. Drive it manually or from automations.

đŸ–±ïž

Mouse Control

Left, right and middle click — plus click-and-hold for drag & drop — relative movement, and scroll wheel via HID mouse reports, and absolute positioning (with mouse_goto for an exact pixel on any monitor). A web Position Finder calibrates and saves it per host. Drive from buttons or automations.

⌚

HA Keyboard Card

Full on-screen QWERTY keyboard card for Home Assistant. Sticky modifiers, Caps Lock, F-keys, and arrow keys.

Home Assistant Keyboard Card
đŸ–„ïž

HA Mouse Card

Custom Lovelace card with a touchpad, 3 mouse buttons, and scroll controls. Drag to move cursor, wheel to scroll. Long-press a button to hold it for drag & drop, tap again to release.

Home Assistant Mouse Card
đŸ“ș

HA Remote Card

Home Assistant remote control card with D-pad, media controls, and app shortcuts. Control media and navigation from your dashboard.

Home Assistant Remote Card
🌐

Web Control

Built-in web page with full keyboard, mouse, remote, and a Position Finder (calibrates mouse_goto per host). Access from any browser — no Home Assistant needed. Enable with web_control: true.

Web Control Page
🔀

Multi-Host Switching

Pair with up to 10 hosts and switch between them with a button press. Uses directed advertising for fast reconnection. Slots persist across reboots.

🌍

Keyboard Layouts

Pick us, uk, de (German QWERTZ), or be (Belgian AZERTY) in YAML and switch live from the web UI (persisted to NVS). Bind a layout per host slot so switching hosts also switches layout. UK adds ÂŁ ÂŹ €; DE adds Ă€ ö ĂŒ ß € § °; BE adds Ă© Ăš Ă  ç Ăč € ÂŁ ÂČ Â§ ” plus dead-key sequences for Ăą ĂȘ Ăź ĂŽ Ă» Ă€ Ă« ĂŻ ö ĂŒ + uppercase via UTF-8. The architecture supports more layouts with three small additions per language.

🔧

Web Macros

Create, edit, and delete macro keys directly from the web UI — no reflash needed. Macros support multi-step commands separated by | with optional delay:N timing. Persist in NVS across reboots.

Quick Start

bluetooth-keyboard.yaml
external_components:
  - source:
      type: git
      url: https://github.com/markusg1234/ESPHome-espidf_ble_keyboard
      ref: main  # or pin a release tag, e.g. v1.0.0
    components: [ espidf_ble_keyboard ]

espidf_ble_keyboard:
  id: my_keyboard
  device_name: "ESP32 BLE KB"  # optional, max 29 chars
  key_delay_ms: 80  # optional, increase if characters drop
  passkey: 123456  # optional 6-digit PIN
  passkey_mode: legacy  # use secure_connections for iOS (required)
  web_control: true  # optional, built-in web UI
  api_services: true  # optional, auto-register HA services (needs api:) — cards work without yaml snippets
  host_slots: 4  # optional, multi-host switching (1–10)
  mouse_sensitivity: 1.0  # optional, web mouse base speed (default: 1.0)
  mouse_acceleration: 0.15  # optional, web mouse accel factor (default: 0.15)
  mouse_max_speed: 4.0  # optional, web mouse max sensitivity (default: 4.0)
  scroll_sensitivity: 2.0  # optional, web mouse scroll speed (default: 2.0)
  screen_width: 3840   # optional, desktop px for mouse_abs_px / mouse_goto
  screen_height: 2160  # optional
  mouse_goto_scale_x: 0.57  # optional, mouse_goto X calibration (tune in Position Finder; saved per host)
  mouse_goto_scale_y: 0.57  # optional, mouse_goto Y calibration
  monitors:            # optional, multi-monitor — mark the Windows primary
    - { name: main, x: 0, y: 0, width: 3840, height: 2160, primary: true }
  keyboard_layout: us  # optional, us | uk | de | be — must match host PC layout (default: us)
  custom_text_id:        # optional, link text entities for Send buttons
    - custom_text

button:
  - platform: espidf_ble_keyboard
    keyboard_id: my_keyboard
    name: "Win + R"
    action: "combo:0x08:0x15"

  - platform: espidf_ble_keyboard
    keyboard_id: my_keyboard
    name: "Shutdown PC"
    action: "shutdown"

Pairing State Sensor

binary_sensor
binary_sensor:
  - platform: espidf_ble_keyboard
    keyboard_id: my_keyboard
    name: "BLE Keyboard Paired"

# ON  = GAP pairing completed successfully on current connection
# OFF = disconnected/unpaired, or no successful pairing yet in this session

Sensors

sensor — RSSI & Active Host
sensor:
  # RSSI — signal strength of connected host
  - platform: espidf_ble_keyboard
    keyboard_id: my_keyboard
    name: "BLE Host RSSI"
    update_interval: 10s
  # Active Host — publishes current host slot (0-based)
  # Required for keyboard card host switcher to stay in sync
  - platform: espidf_ble_keyboard
    keyboard_id: my_keyboard
    type: active_host
    name: "BLE Keyboard Active Host"

Built-in Actions

ActionDescription
"Hello\n"Type a string. Printable ASCII supported on every layout; non-ASCII (e.g. ÂŁ ÂŹ € on UK, Ă€ ö ĂŒ ß on DE) works via UTF-8 when the active layout exposes it. Use \n for Enter.
"combo:0x08:0x15"Key combo — modifier + keycode in hex. Use 0x00 as modifier for a plain keypress. See keycode reference.
type: comboDict format alternative — type: combo, modifier: 0x01, key: 0x04. More readable for complex actions.
type: consumerDict format for consumer codes — type: consumer, code: 0x0192.
"ctrl_alt_del"Send Ctrl+Alt+Del secure login sequence.
"sleep"HID System Sleep signal — clean OS-level sleep.
"hibernate"Hibernate via Run dialog — saves to disk, full power off.
"shutdown"HID System Power Down signal — clean OS-level shutdown (Windows).
"power"HID power button — triggers the host device power button action (Windows).
"mute"Toggle mute.
"volume_up"Volume up.
"volume_down"Volume down.
"play_pause"Play / pause media.
"next_track"Skip to next track.
"prev_track"Previous track.
"stop"Stop media playback.
"record"Start / stop recording (HID Record, 0x00B2). Works on TV / DVR hosts; Windows ignores it — remap it per host with actions:, e.g. "combo:0x0C:0x15" for Game Bar.
"rewind" / "fast_forward"Rewind (0x00B4) / fast forward (0x00B3).
"remote_power"Remote Power key — HID consumer Power (0x0030). Distinct from "power", which is a System Power Down report.
"up" / "down" / "left" / "right"D-pad navigation — HID Menu usages (0x0042–0x0045).
"ok"D-pad select — keyboard Enter (0x28), which more hosts accept than HID Menu Pick. Override per host with "consumer:0x0041" if a host needs Menu Pick.
"home" / "back" / "search" / "info"AC Home / Back / Search / More Info (0x0223, 0x0224, 0x0221, 0x0209).
"channel_up" / "channel_down"Channel surf — Page Up / Page Down keypress.
"color_red" / "color_green" / "color_yellow" / "color_blue"Coloured remote keys — F1–F4.
"app_explorer" / "app_browser" / "app_email" / "app_calc"App launch keys (0x0194, 0x0223, 0x018A, 0x0192).
"consumer:0x0192"Send any HID consumer control code — open apps, control brightness and more. See keycode reference.
"key_hold:0x00:0x3A"Press and hold a key until release — the host sees it held down, not tapped, which is what push-to-talk needs. Same format as combo:; a keycode of 0x00 holds the modifier alone.
"consumer_hold:0x00E9"Hold a consumer usage. Only one at a time — that report carries a single usage.
"hold:<action>"Hold whatever the action resolves to: combo:, consumer:, a mouse click, or a named action including one remapped per host. Anything that can't be held runs once instead.
"release"Release everything held — keys, consumer usage and mouse buttons. key_release is an alias.
"left_click"Mouse left click.
"right_click"Mouse right click.
"middle_click"Mouse middle click.
"left_click_hold"Press and hold the left button until mouse_release — moving, scrolling or mouse_goto while held performs a drag. Also right_click_hold / middle_click_hold, or mouse_hold:0x01 with a button mask.
"mouse_release"Release all held mouse buttons. A normal click also releases them.
"mouse_move:50:0"Move mouse cursor — relative X:Y pixels (-127 to 127).
"mouse_scroll:3"Scroll mouse wheel — positive = up, negative = down (-127 to 127).
type: mouse_clickDict format — type: mouse_click, buttons: 0x01. 0x01=left, 0x02=right, 0x04=middle.
"mouse_abs:50:50"Move cursor to an exact position — percent of screen (0–100). mouse_abs:50:50 = center.
"mouse_abs_px:1280:720"Move cursor to an exact position in pixels (uses screen_width/screen_height).
"mouse_abs_mon:1:50:50"Move cursor to a percent within declared monitors[1] (multi-monitor).
"mouse_abs_save" / "mouse_abs_restore"Remember the current absolute position and jump back to it later.
"mouse_goto:4394:42"Move to a Windows virtual-desktop pixel across all monitors (homes absolute to 0,0 then steps relatively). X/Y = Windows coords (primary top-left = 0,0; negatives allowed); read a spot's with the bundled docs/cursorpos.bat. Use when the absolute pointer is stuck on the primary monitor. Needs "Enhance pointer precision" off + per-axis calibration (mouse_goto_scale_x/_y), dialed in via the web Position Finder.
"send_custom_text"Send linked text entity content. Use send_custom_text:N for multiple entities. Requires custom_text_id in config.
"switch_host:0"Switch to host slot 0–9. Reconnects to stored host or advertises for new pairing.
"forget_host:0"Remove BLE bond for host slot 0–9 and clear the stored address.
"string:hello"Explicit text typing — useful in multi-step macros to distinguish text from action names.
"delay:200"Pause for N milliseconds (max 10000). Used between steps in multi-step macros.
"a | b | c"Multi-step macro — chain actions with |. 50ms auto-delay between steps. E.g. "combo:2:6 | delay:100 | combo:2:25" (Copy, wait, Paste).
execute_action()Run any action string from a lambda — id(kb).execute_action("combo:2:6 | delay:100 | combo:2:25");. Supports multi-step.
execute_macro(N)Run a web-defined macro by index — id(kb).execute_macro(0);. Index shown in web UI as [0], [1], etc.
run_macro / run_actionExpose macros to Home Assistant: set api_services: true (auto-registers these and all card services), then call esphome.<device>_run_macro (data: index) or _run_action (data: action) from HA. Web macros aren't individual HA entities (created at runtime); use these services or a button: entry for a named entity.

Home Assistant Cards

Set api_services: true on the component and every service the cards need (mouse_move, send_string, send_key, send_consumer, switch_host, 
) is registered automatically — no api: services: yaml snippets required. Copy the card JS to config/www/, add it as a dashboard resource, done.

Mouse Card — dashboard YAML
type: custom:ble-mouse-card
device: bluetooth_keyboard
name: Mouse Control        # optional card title
sensitivity: 1.5            # optional cursor speed (default: 1.5)
mouse_acceleration: 0.15   # optional acceleration factor (default: 0.15)
mouse_max_speed: 4.5       # optional max sensitivity cap (default: 4.5)
scroll_sensitivity: 2      # optional scroll speed (default: 2)
tap_to_click: true          # optional tap = left click (default: true)
Keyboard Card — dashboard YAML
type: custom:ble-keyboard-card
device: bluetooth_keyboard
name: BLE Keyboard          # optional card title
show_fkeys: true            # optional show F1-F12 row (default: true)
host_slots: 4              # optional host switcher (default: 0 = hidden)
host_names:                  # optional custom names per slot
  - TV
  - Phone
Remote Card — dashboard YAML
type: custom:ble-remote-card
device: bluetooth_keyboard
name: Media Remote           # optional card title (auto from HA if omitted)
show_numpad: true            # optional number pad (default: false)
show_apps: true              # optional app launch row (default: true)
show_color: true             # optional color buttons (default: false)
ESPHome services required for cards
api:
  services:
    # Mouse card services
    - service: mouse_move
      variables:
        x: int
        y: int
      then:
        - lambda: |-
            id(my_keyboard).send_mouse_move(x, y);
    - service: mouse_scroll
      variables:
        amount: int
      then:
        - lambda: |-
            id(my_keyboard).send_mouse_scroll(amount);
    - service: mouse_click
      variables:
        btn: int
      then:
        - lambda: |-
            id(my_keyboard).send_mouse_click(btn);
    - service: mouse_hold          # press & hold for dragging — release with mouse_release
      variables:
        btn: int
      then:
        - lambda: |-
            id(my_keyboard).send_mouse_click_start(btn);
    - service: mouse_release
      then:
        - lambda: |-
            id(my_keyboard).send_mouse_click_release();
    - service: mouse_abs          # move cursor to exact position, % of screen
      variables:
        x: float
        y: float
      then:
        - lambda: |-
            id(my_keyboard).execute_action("mouse_abs:" + to_string(x) + ":" + to_string(y));
    # Keyboard card services
    - service: send_string
      variables:
        keys: string
      then:
        - lambda: |-
            id(my_keyboard).send_string(keys);
    - service: send_key
      variables:
        modifier: int
        keycode: int
      then:
        - lambda: |-
            id(my_keyboard).send_key_combo(modifier, keycode);
    # Remote card service
    - service: send_consumer
      variables:
        code: int
      then:
        - lambda: |-
            id(my_keyboard).send_consumer(code);
    # Host switching service
    - service: switch_host
      variables:
        slot: int
      then:
        - lambda: |-
            id(my_keyboard).switch_host(slot);