Skip to content
Back to top
View this page

# Bond key persistence (experimental)

> Experimental. This covers the DBus-free management/L2CAP path (`HaScannerMgmt` + `HaMgmtClient`). The API is not stable and may change or be renamed. Home Assistant does not select this path yet.

When pairing over the BlueZ management socket, the kernel hands back a long-term key (LTK) per bond. `habluetooth` keeps these in memory only (it is a library and does no disk I/O), so without help they are lost on restart and the peer must be re-paired. To survive a restart, the consumer (Home Assistant) persists them and seeds them back on startup.

Keys are per adapter: an LTK is tied to the local adapter identity it paired with, so it is only valid on that adapter. Persist keyed by adapter.

## What `habluetooth` provides

- `LongTermKey` — the bonded key (`habluetooth.LongTermKey`).

- `long_term_key_to_dict()` / `long_term_key_from_dict()` — JSON-safe (de)serialization (the `bytes` fields become hex strings), shaped like the discovery-cache helpers.

- On `HaScannerMgmt`:

  - `export_long_term_keys() -> list[LongTermKey]` — snapshot for saving.

  - `restore_long_term_keys(keys)` — seed the store on startup.

  - `set_long_term_keys_changed_callback(callback)` — `callback` fires whenever the store changes (a pair captured a key, or an unpair removed one), so the consumer can schedule a debounced save.

## Wiring it in Home Assistant

Mirror the existing remote-scanner storage (`Store` + `async_delay_save`), but keyed by adapter and holding a list of serialized keys:

    from habluetooth import (
        HaScannerMgmt,
        LongTermKeyDict,
        long_term_key_from_dict,
        long_term_key_to_dict,
    )
    from homeassistant.helpers.storage import Store

    BOND_STORAGE_VERSION = 1
    BOND_STORAGE_KEY = "bluetooth.bonds"
    SAVE_DELAY = 5

    StoredBonds = dict[str, list[LongTermKeyDict]]  # keyed by adapter (e.g. "hci0")

    class BondStorage:
        def __init__(self, hass):
            self._store: Store[StoredBonds] = Store(
                hass, BOND_STORAGE_VERSION, BOND_STORAGE_KEY
            )
            self._data: StoredBonds = {}

        async def async_setup(self):
            self._data = await self._store.async_load() or {}

        def async_attach(self, scanner: HaScannerMgmt):
            adapter = scanner.adapter
            # Seed the scanner with any keys we saved last run.
            if saved := self._data.get(adapter):
                scanner.restore_long_term_keys(
                    long_term_key_from_dict(key) for key in saved
                )
            # Save (debounced) whenever the scanner's bonds change.
            scanner.set_long_term_keys_changed_callback(
                lambda: self._async_save(scanner)
            )

        def _async_save(self, scanner: HaScannerMgmt):
            self._data[scanner.adapter] = [
                long_term_key_to_dict(key) for key in scanner.export_long_term_keys()
            ]
            self._store.async_delay_save(lambda: self._data, SAVE_DELAY)

Call `async_attach()` when the scanner is created and `set_long_term_keys_changed_callback(None)` if it is torn down.