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.
Features
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.
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.
Send any modifier + key combination using hex keycodes. Win+R, Ctrl+C, Alt+F4 and more.
Native HID sleep, hibernate and shutdown β clean OS-level signals, no Run dialog or lingering key state.
Volume up/down, mute, play/pause, next/prev track and stop via HID consumer control reports.
Each button appears as an entity in Home Assistant. Trigger from automations, dashboards or scripts. Direct API implementation.
Optional binary sensor turns on after a successful GAP pairing event and turns off on disconnect/unpair.
Type any text in Home Assistant and send it directly to the paired host device. Drive it manually or from automations.
Left, right and middle click, cursor movement, and scroll wheel via HID mouse reports. Drive from buttons or automations.
Full on-screen QWERTY keyboard card for Home Assistant. Sticky modifiers, Caps Lock, F-keys, and arrow keys.
Custom Lovelace card with a touchpad, 3 mouse buttons, and scroll controls. Drag to move cursor, wheel to scroll.
Home Assistant remote control card with D-pad, media controls, and app shortcuts. Control media and navigation from your dashboard.
Built-in web page with full keyboard, mouse, and remote UI. Access from any browser β no Home Assistant needed. Enable with web_control: true.
Pair with up to 10 hosts and switch between them with a button press. Uses directed advertising for fast reconnection. Slots persist across reboots.
Pick us, uk, or de (German QWERTZ) 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 Γ€ ΓΆ ΓΌ Γ β¬ Β§ Β° via UTF-8 (dead keys auto-composed). The architecture supports more layouts with three small additions per language.
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
external_components: - source: type: git url: https://github.com/markusg1234/ESPHome-espidf_ble_keyboard ref: main 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 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) keyboard_layout: us # optional, us | uk | de β 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: - 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 β 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
| Action | Description |
|---|---|
"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: combo | Dict format alternative β type: combo, modifier: 0x01, key: 0x04. More readable for complex actions. |
type: consumer | Dict 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. |
"consumer:0x0192" | Send any HID consumer control code β open apps, control brightness and more. See keycode reference. |
"left_click" | Mouse left click. |
"right_click" | Mouse right click. |
"middle_click" | Mouse middle click. |
"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_click | Dict format β type: mouse_click, buttons: 0x01. 0x01=left, 0x02=right, 0x04=middle. |
"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. |
Home Assistant Cards
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)
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
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)
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); # 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);