Skip to content

BLE: support passkey-protected companion radios (PIN + BlueZ pairing agent)#294

Open
drewmccal wants to merge 2 commits into
meshcore-dev:mainfrom
drewmccal:feature/ble-pin
Open

BLE: support passkey-protected companion radios (PIN + BlueZ pairing agent)#294
drewmccal wants to merge 2 commits into
meshcore-dev:mainfrom
drewmccal:feature/ble-pin

Conversation

@drewmccal

Copy link
Copy Markdown

What

Adds support for BLE companion radios that require a passkey/PIN to pair (e.g. Elecrow ThinkNode-M1). Previously these failed at connect time with org.bluez.Error.AuthenticationFailed.

Why

bleak's client.pair() needs a BlueZ pairing agent to supply the passkey. HAOS's default agent is no-input/just-works, so a radio that demands a PIN can never complete bonding from the integration — the connection just fails.

Changes

  • CONF_BLE_PIN optional field in the BLE config + reconfigure flows (blank = no PIN, normalized to None).
  • ble_pairing_agent.py (new): registers an org.bluez.Agent1 via dbus_fast (already present transitively via bleak/habluetooth, imported lazily) that answers RequestPasskey/RequestPinCode/RequestConfirmation/etc. with the configured PIN. Capability KeyboardDisplay. Registered before create_ble, unregistered in finally.
  • meshcore_api.py: accepts ble_pin, registers/unregisters the agent around the BLE connect.
  • Translations for the new field.
  • Tests: PIN normalization, create_ble forwarding, graceful fallback when the agent can't register.

If agent registration fails, connection still proceeds (works for already-bonded / just-works radios), so this is non-regressive for existing setups.

🤖 Generated with Claude Code

drewmccal and others added 2 commits June 24, 2026 11:27
Radios configured with a Bluetooth PIN couldn't be added: the SDK's
MeshCore.create_ble() accepts a `pin=` for pairing, but the integration
never passed one, so pairing failed and the connection dropped during
GATT service discovery.

- const: CONF_BLE_PIN
- meshcore_api: MeshCoreAPI accepts ble_pin and forwards it to
  create_ble(pin=...); blank/whitespace is normalized to None so radios
  without a PIN don't attempt an empty-passkey pairing
- config_flow: optional "Bluetooth PIN" field in the BLE setup and
  reconfigure steps, persisted in the config entry and threaded through
  validation
- __init__: forward CONF_BLE_PIN into the API kwargs on setup
- en.json translations for the new field
- tests: tests/test_ble_pin.py (PIN normalization + create_ble forwarding)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Passing pin= to create_ble was necessary but not sufficient: bleak's
client.pair() relies on a system BlueZ agent to answer BlueZ's passkey
request, and the default Home Assistant OS agent is no-input, so passkey
radios still failed with org.bluez.Error.AuthenticationFailed.

Add ble_pairing_agent.py: registers a short-lived org.bluez.Agent1 (via
dbus_fast, imported lazily) that returns the configured PIN for
RequestPasskey/RequestPinCode, makes it the default agent for the
duration of the connection attempt, then unregisters it. Once bonded,
reconnects don't need the agent.

meshcore_api.connect() registers the agent before create_ble and always
unregisters it afterward. Everything is best-effort: if dbus_fast is
unavailable or registration fails, it logs and proceeds exactly as
before (no regression).

- tests: agent fallback returns None without dbus_fast; existing connect
  forwarding tests still pass (agent path degrades gracefully)

Note: ideally this agent belongs upstream in meshcore_py/ble_cx; doing it
in the integration works because it registers at the system-bus default-
agent level, independent of who calls pair().

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@awolden

awolden commented Jun 26, 2026

Copy link
Copy Markdown
Collaborator

nice work, this solves a real gap (#265/#83). but it belongs a layer down in meshcore-py, not here.

the SDK already owns this and is just missing the last piece: BLEConnection takes pin= and calls client.pair() (ble_cx.py:113-116), and create_ble(pin=...) exposes it. on BlueZ, pair() raises a passkey request the default agent can't answer, so the SDK's pin= silently does nothing today. your ble_pairing_agent.py is exactly the missing piece that makes it work, and it's pure BLE-transport plumbing (raw system DBus, no HA APIs). every meshcore-py consumer hits the same wall: the meshcore-cli that's in our own manifest, bots, and the SDK's own example all call create_ble(pin=...) and silently fail to pair without it.

since it's not urgent, rather than carry a stopgap here and migrate later, let's do it right. filed meshcore-dev/meshcore_py#92 as the target:

  • contribute the agent to meshcore-py, registered/unregistered around the pair() call in ble_cx.py, guarded by if pin is not None with the lazy dbus_fast import you already have. a test that doesn't just mock pair() would be good (the SDK's current test mocks it, which is why this gap went unnoticed).
  • then this PR reduces to the HA-side bits, which are right: collect CONF_BLE_PIN in the BLE config flow and pass it through to create_ble(pin=...), and update installation.md. drop the DBus module here.

happy to review the meshcore-py PR. the config-flow work stays.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants