Skip to content

BTHome support for ZMK keyboard firmware

License

Notifications You must be signed in to change notification settings

Genteure/zmk-bthome

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

13 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

ZMK BTHome Module

ZMK BTHome is a ZMK module that adds support for sending sensor data and button events over Bluetooth Low Energy (BLE) using the BTHome protocol.

ZMK is an open source, wireless-first keyboard firmware based on Zephyr RTOS. It's modular and composable, making it easy to extend functionality through modules like this one.

BTHome is an open standard for transmitting sensor data and events over BLE, commonly used in smart home devices. Home Assistant supports BTHome natively out of the box.

Features

  • BLE BTHome v2 advertisements
  • Optional encryption
  • Battery Level (percentage) and Battery Voltage
  • Configurable amount of buttons with all press types
  • Optional custom device name

Installation

Add this module to your config/west.yml:

manifest:
  remotes:
    # ... existing remotes ...
    - name: genteure
      url-base: https://github.com/genteure
  projects:
    # ... existing projects ...
    - name: zmk-bthome
      remote: genteure
      revision: main
  # ... other manifest entries ...

See https://zmk.dev/docs/features/modules#building-with-modules for more information on adding modules to your ZMK build.

Then in your keyboard's .conf file (config/keyboard_name.conf), enable ZMK BTHome:

CONFIG_ZMK_BTHOME=y
CONFIG_BT_EXT_ADV_MAX_ADV_SET=2

This module builds with ZMK v0.3 and the to-be-0.4 main branch. Tested working on an actual keyboard with ZMK main branch as of January 2026.

Configuration

Device Name

By default, the BTHome device name will be the value of CONFIG_ZMK_KEYBOARD_NAME, or "ZMK" if keyboard name is not set.

You can override the device name by setting CONFIG_ZMK_BTHOME_DEVICE_NAME in your keyboard's .conf file:

CONFIG_ZMK_BTHOME_DEVICE_NAME="A"

Setting CONFIG_ZMK_BTHOME_DEVICE_NAME="" (empty string) will remove the device name entry from the advertisement entirely, leaving more space for BTHome sensor data. Removing the device name does not affect how Home Assistant identifies the device and is highly recommended if encryption is enabled. See Size Limitations section below for details.

Home Assistant by default will use the device name + last 4 characters of the MAC address as the display name, or "BTHome sensor XXXX" if the device name is empty. You can always change the display name in Home Assistant so setting a custom device name here is not strictly necessary.

Buttons

Buttons are ZMK behaviors. Add any number of bthome button behaviors to your keymap.

#include <dt-bindings/zmk_bthome/button.h>

/ {
    behaviors {
        // name can be anything, but ORDER matters
        bthome0: bthome_button0 {
            compatible = "zmk,behavior-bthome-button";
            #binding-cells = <1>;
            display-name = "BTHome Button 1"; // Optional, shown in ZMK Studio
        };
        bthome1: bthome_button1 {
            compatible = "zmk,behavior-bthome-button";
            #binding-cells = <1>;
            display-name = "BTHome Button 2";
        };
        bthome2: bthome_button2 {
            compatible = "zmk,behavior-bthome-button";
            #binding-cells = <1>;
            display-name = "BTHome Button 3";
        };
        // As many as you want...
    };

    keymap {
        compatible = "zmk,keymap";
        default_layer {
            bindings = <&bthome0 BTHOME_BTN_PRESS &bthome1 BTHOME_BTN_TRIPLE_PRESS ...>;
        };
    };
};

IMPORTANT: The button index is determined by the order in which the behaviors are defined in your keymap, meaning if you have bthome_a, bthome_b, "A" will be button 1 and "B" will be button 2. If you have bthome_b defined before bthome_a, then "B" will be button 1 and "A" will be button 2.

Possible button press types are:

  • BTHOME_BTN_PRESS
  • BTHOME_BTN_DOUBLE_PRESS
  • BTHOME_BTN_TRIPLE_PRESS
  • BTHOME_BTN_LONG_PRESS
  • BTHOME_BTN_LONG_DOUBLE_PRESS
  • BTHOME_BTN_LONG_TRIPLE_PRESS
  • BTHOME_BTN_HOLD_PRESS

Compose with ZMK built-in behaviors like hold-tap and tap-dance to create real "multi-function" buttons, or simply put them on different keys and layers.

// https://zmk.dev/docs/keymaps/behaviors/hold-tap
/ {
    behaviors {
        htbth0: hold_tap_bthome0 {
            compatible = "zmk,behavior-hold-tap";
            #binding-cells = <2>;
            flavor = "hold-preferred";
            tapping-term-ms = <200>;
            bindings = <&bthome0>, <&bthome0>;
            display-name = "Hold-Tap BTHome Button 1";
        };
    };

    keymap {
        compatible = "zmk,keymap";
        default_layer {
            // Hold sends long press, tap sends press
            bindings = <&htbth0 BTHOME_BTN_LONG_PRESS BTHOME_BTN_PRESS>;
        };
    };
};
// https://zmk.dev/docs/keymaps/behaviors/tap-dance
/ {
    behaviors {
        tdbth0: tap_dance_bthome0 {
            compatible = "zmk,behavior-tap-dance";
            #binding-cells = <0>;
            tapping-term-ms = <200>;
            bindings = <&bthome0 BTHOME_BTN_PRESS>, <&bthome0 BTHOME_BTN_DOUBLE_PRESS>, <&bthome0 BTHOME_BTN_TRIPLE_PRESS>;
        };
    };

    keymap {
        compatible = "zmk,keymap";

        default_layer {
            // Single, double, and triple press
            bindings = <&tdbth0>;
        };
    };
};

For split keyboards, button presses will be reported from the central side only, since keymap processing happens there.

TODO support sending from the source side?

Battery

Battery level and battery voltage reporting are enabled by default. To disable them, add the following to your keyboard's .conf file:

# Disable battery level reporting
CONFIG_ZMK_BTHOME_BATTERY_LEVEL=n
# Disable battery voltage reporting
CONFIG_ZMK_BTHOME_BATTERY_VOLTAGE=n

For split keyboards, each keyboard part will independently report its own battery level and voltage.

Packet ID

Packet ID can help receivers identify distinct advertisement packets and discard duplicates. It is enabled by default unless encryption is enabled or if building for peripheral side of a split keyboard. To explicitly enable or disable packet ID, set the following option in your keyboard's .conf file:

# Disable packet ID in BTHome advertisements
CONFIG_ZMK_BTHOME_PACKET_ID=n

When encryption is enabled, the encryption counter serves the same purpose as packet ID, so packet ID is disabled to save 2 bytes in the advertisement packet.

For split peripherals, BTHome receviers should provide enough deduplication on their own side. Having packet ID enabled could potentially reduce data reliability if it's frequently reset (e.g., deep sleep) and keeps sending packet IDs that have already been seen by the receiver.

Encryption

By default, BTHome advertisements are unencrypted. You can enable encryption by setting the following options in your keyboard's .conf file:

# 16-byte (32 hex characters) encryption key
CONFIG_ZMK_BTHOME_ENCRYPTION_KEY="00112233445566778899AABBCCDDEEFF"

You can use the same encryption key across multiple keyboard parts. Home Assistant will prompt you to enter the bind key (a.k.a. encryption key) in the Integrations/Devices page.

Enabling encryption takes up more space in the advertisement packet, see the Size Limitations section below.

As of Home Assistant 2026.1, you can't remove the bind key from an existing BTHome device. See home-assistant/core#159646.

Encryption prevents observers from seeing your button presses and battery status, but doesn't fully prevent replay attacks. As of writing (January 2026), advertisements with encryption counter less than 100 are accepted even if they are less than the last received counter to allow device restarts, and the assumption is the counter will start from 0 on restart.

Here's a cryptographically secure one-liner to paste into your browser console:

[...crypto.getRandomValues(new Uint8Array(16))].map(b => b.toString(16).padStart(2, '0')).join('')

Size Limitations

BLE advertisement packets have a maximum size of 31 bytes. BLE flags take 3 bytes and BTHome related header takes another 5, leaving just 23 bytes available for the payload.

Each type of data takes different amounts of space:

  • Packet ID: 2 bytes
  • Battery Level: 2 bytes
  • Battery Voltage: 3 bytes
  • Button: 2 bytes each

Encryption will take an additional 8 bytes if enabled.

If CONFIG_ZMK_BTHOME_DEVICE_NAME is set, that takes up an additional sizeof(CONFIG_ZMK_BTHOME_DEVICE_NAME) + 2 bytes in the advertisement packet.

If the total data exceeds the size limit, build will fail with error BTHome advertisement payload exceeds maximum advertisement size.

Example 1

  • 0 bytes: CONFIG_ZMK_BTHOME_DEVICE_NAME="" (empty string)
  • 0 bytes: No Packet ID
  • 2 bytes: Battery Level
  • 3 bytes: Battery Voltage
  • 8 bytes: 4 buttons
  • 8 bytes: Encryption overhead
  • Total: 21 bytes

Example 2

  • 5 bytes: CONFIG_ZMK_BTHOME_DEVICE_NAME="ZMK" (3 characters + 2 bytes header)
  • 2 bytes: Packet ID
  • 2 bytes: Battery Level
  • 3 bytes: Battery Voltage
  • 2 bytes: 1 button
  • 0 bytes: No encryption
  • Total: 14 bytes

Example 3

  • 7 bytes: CONFIG_ZMK_BTHOME_DEVICE_NAME="Corne" (5 characters + 2 bytes header)
  • 2 bytes: Packet ID
  • 2 bytes: Battery Level
  • 3 bytes: Battery Voltage
  • 8 bytes: 4 buttons
  • 0 bytes: No encryption
  • Total: 22 bytes

Example 4

  • 11 bytes: CONFIG_ZMK_BTHOME_DEVICE_NAME="nice name" (9 characters + 2 bytes header)
  • 0 bytes: Packet ID
  • 2 bytes: Battery Level
  • 3 bytes: Battery Voltage
  • 0 bytes: No buttons
  • 8 bytes: Encryption overhead
  • Total: 24 bytes -> Build fails

Advertising Parameters

You can customize the advertising timeout and number of packets sent per interval by setting the following options in your keyboard's .conf file:

# Advertising timeout in units of 10 ms (default: 500, i.e., 5000 ms)
CONFIG_ZMK_BTHOME_ADV_TIMEOUT=500
# Number of advertising packets sent per interval (default: 24)
CONFIG_ZMK_BTHOME_ADV_PACKETS=24

By default, each BTHome event is advertised for up to 1 second (100 * 10 ms) or up to 10 times during that period, whichever comes first. Multiple BTHome button presses in quick succession will be combined into a single advertisement payload, with the last event for each button taking precedence.

You can adjust these values to balance between time spent advertising each BTHome event and reliability of receiving the advertisements. Too low values may result in missed events.

(To avoid confusion, we're using "events" to refer to BTHome/Home Assistant events and "packets" for what Zephyr calls "advertising events".)

License

This project is licensed under the MIT License. See the LICENSE file for details.

Credits

About

BTHome support for ZMK keyboard firmware

Topics

Resources

License

Stars

Watchers

Forks