Architecture¶
This page explains what bleak-esphome does, where the ESPHome Bluetooth Proxy is
implemented, and how the pieces fit together. If you only want to use the library,
see Usage.
What this library is — and what it is not¶
bleak-esphome is the glue layer that lets a Python application talk to remote
Bluetooth Low Energy (BLE) peripherals through an ESP32 running ESPHome with the
Bluetooth Proxy component enabled. It does not implement the Bluetooth Proxy
itself. The proxy is firmware that runs on the ESP32; this library is the host-side
client that consumes the proxy’s API.
The split is:
Layer |
Where it lives |
Repository |
|---|---|---|
ESP32 firmware (Bluetooth Proxy component) |
On the ESP32, written in C++ |
esphome/esphome — see |
Native ESPHome API (protobuf over TCP, port 6053) |
On the ESP32 |
esphome/esphome — see |
Python client for the ESPHome API |
Host (your app) |
|
|
Host (your app) |
this repo |
Remote-scanner / connection-slot bookkeeping primitives |
Host (your app) |
|
Standard BLE client API consumed by user code |
Host (your app) |
So when you call bleak.BleakScanner.discover(...) in an app that has set up an
APIConnectionManager, what really happens is:
The ESP32 scans for BLE advertisements over the air.
The Bluetooth Proxy component forwards advertisements to your host over Wi-Fi via the ESPHome native API.
aioesphomeapidecodes the protobuf messages.bleak-esphome’sESPHomeScannertranslates each advertisement into the shape thathabluetoothandbleakexpect.bleak’s discovery code sees those advertisements as if they had been seen by a local adapter.
Active connections (GATT reads, writes, notifications) work the same way in reverse:
bleak’s BleakClient talks to an ESPHomeClient (in backend/client.py), which
sends connect / read / write requests to the ESP32, which performs the actual BLE
operation against the peripheral.
Where the proxy is implemented¶
Short answer: not here. The Bluetooth Proxy is implemented in the
ESPHome firmware project, in
esphome/components/bluetooth_proxy/. The API messages it exchanges with hosts are
defined in esphome/components/api/api.proto.
You enable the proxy by adding the bluetooth_proxy: component to your ESPHome YAML
configuration and flashing it to an ESP32. ESPHome publishes ready-made proxy
firmwares at https://esphome.io/projects/?type=bluetooth.
What this repository contains:
src/bleak_esphome/connection_manager.py—APIConnectionManager, the convenience wrapper that opens the ESPHome API connection, performsReconnectLogic, and registers a scanner withhabluetooth.src/bleak_esphome/connect.py—connect_scanner(), which wires up anaioesphomeapi.APIClientto anESPHomeScannerandESPHomeClientand subscribes to advertisement / scanner-state / connection-slot streams.src/bleak_esphome/backend/scanner.py—ESPHomeScanner, a remote scanner that feeds advertisements received from the ESP32 intohabluetooth.src/bleak_esphome/backend/client.py—ESPHomeClient, theBleakClient-shaped backend that performs GATT operations through the ESP32.src/bleak_esphome/backend/device.py—ESPHomeBluetoothDevice, bookkeeping for per-ESP32 state (availability, free connection slots, allocations).src/bleak_esphome/backend/cache.py— local GATT service cache.
How features are negotiated¶
When the ESPHome API connection comes up, connect_scanner() reads
DeviceInfo.bluetooth_proxy_feature_flags_compat(api_version) from aioesphomeapi.
That bitmask drives every subsequent decision:
ACTIVE_CONNECTIONS— whether the proxy can open BLE connections, or only forward advertisements (passive listener). If unset, the scanner is registered as non-connectable.RAW_ADVERTISEMENTS— if set, the host subscribes to raw advertisement frames; if not, it falls back to per-advertisement decoded messages.FEATURE_STATE_AND_MODE— if set, the host subscribes to scanner state updates and tracks both the current scanner state (IDLE/STARTING/RUNNING/STOPPING/STOPPED/FAILED) and the active scanning mode (PASSIVE/ACTIVE). It also binds theAPIClientto the scanner sohabluetoothcan request bounded on-demand active-scan windows (see “On-demand active scanning” below).REMOTE_CACHING— gates the cached-services hint sent on connect. When unset the hint is forced off, so the proxy re-discovers services on every connect; the on-host LRU cache still works.PAIRING— required forBleakClient.pairandunpair. If unset, those calls raiseNotImplementedErrorrather than silently no-oping.CACHE_CLEARING— required for Usage’sclear_cache()extension.CONNECTION_PARAMS_SETTING— required forset_connection_params().
The proxy-side BluetoothProxyFeature enum also defines PASSIVE_SCAN, but no
host-side code path in this library currently checks it — passive scanning is
inferred from the absence of ACTIVE_CONNECTIONS rather than from a dedicated
flag. See the Usage “Feature Flag Reference” table for what callers see
when each flag is missing.
Older proxy firmwares simply lack these flags; the library degrades gracefully (it logs a warning and skips the unsupported call) rather than refusing to start.
On-demand active scanning¶
A passive scanner sees advertisement payloads but never the scan-response data
that some devices only return when actively probed. Continuously scanning in
ACTIVE mode costs the proxy radio time and power, so bleak-esphome lets
habluetooth open ACTIVE mode only when it is needed and for a bounded window.
This is gated behind FEATURE_STATE_AND_MODE: when the proxy advertises that
flag, connect_scanner() binds the APIClient to the scanner. habluetooth’s
auto-mode scheduler then calls ESPHomeScanner.async_request_active_window
with a duration, and the scanner:
Flips the proxy to
ACTIVEviabluetooth_scanner_set_mode.Sleeps for the requested duration.
Restores the previously requested mode (or
PASSIVEif none is known), even if the sleep is cancelled.
Only one window may be open at a time — a request that arrives while another is
in flight returns False immediately. Proxies without FEATURE_STATE_AND_MODE
ignore the request (it returns False), so they keep whatever fixed mode they
were configured with.
Who can use it¶
The library has no Home Assistant dependency. It is plain Python, built on
asyncio, and works in any host environment that can reach the ESP32 over TCP and
that can run bleak and habluetooth. Concretely:
Home Assistant uses it under the hood for its ESPHome Bluetooth integration — but it is not the only consumer.
Stand-alone Python applications can use it; see Usage for a minimal example that does not import anything from Home Assistant.
Home Assistant add-ons that run Python can use it the same way a stand-alone app does. There is no special add-on hook; the only requirement is that the add-on can open a TCP connection to the ESP32 on port 6053.
If you are evaluating whether to consume the ESPHome Bluetooth Proxy from an
unrelated application, the practical entry point is APIConnectionManager plus a
list of ESPHomeDeviceConfig entries, as shown in Usage.
See also¶
ESPHome Bluetooth Proxy component: https://esphome.io/components/bluetooth_proxy.html
Pre-built proxy firmwares: https://esphome.io/projects/?type=bluetooth
aioesphomeapi(the protobuf client this library sits on top of): https://github.com/esphome/aioesphomeapihabluetooth(remote-scanner primitives): https://github.com/Bluetooth-Devices/habluetooth