Conversation
Implements a new packages/ble-proxy package that enables Matter BLE commissioning through a remote BLE-capable client (e.g. Home Assistant with ESPHome BLE proxies) over a WebSocket protocol on the /ble endpoint. New package (packages/ble-proxy): - BleProxyProtocol: Protocol types, typed command map, binary frame codec - BleProxyHandler: WebSocket server for /ble with hello handshake, Observable events - ProxyBle/ProxyBleClient/ProxyBleScanner: matter.js Ble abstract class implementation - ProxyBleCentralInterface/ProxyBleChannel: GATT operations and BTP over binary frames - Noble-based reference client (npm run noble-ble-proxy) for testing and standalone use - Integration tests (18 tests) and test utilities (BleProxyTestClient, MockBleDevice) Server integration: - New --ble-proxy CLI flag (mutually exclusive with --bluetooth-adapter) - Registers ProxyBle with matter.js environment for transparent BLE commissioning Documentation: - docs/ble-proxy-protocol.md: Standalone protocol specification - docs/plans/: Architecture design and implementation plan Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Documents the Home Assistant matter integration changes needed for BLE proxy support, including file-by-file change descriptions, design decisions, and testing instructions. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
# Conflicts: # package.json # packages/matter-server/package.json
- Resolved conflict in packages/matter-server/package.json: kept ble-proxy dependency while taking new matter.js version from main - Updated packages/ble-proxy/package.json matter.js deps to match Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
# Conflicts: # packages/matter-server/package.json # packages/matter-server/src/MatterServer.ts
# Conflicts: # packages/matter-server/package.json
# Conflicts: # packages/matter-server/package.json
Adds a working BLE proxy package (@matter-server/ble-proxy) that lets a remote BLE adapter drive Matter BLE commissioning over a WebSocket, plus the matter-server integration that wires it onto the /ble endpoint and the noble-based reference client. Notable correctness fixes for an actual macOS-noble proxy run: - UUID format: noble emits 32-char compact lowercase, matter.js consts use canonical dashed uppercase. Added toCanonicalUuid + MatterBle.isServiceUuid matching so the server accepts any case, with or without dashes, plus the 16-bit short form. ESPHome-style clients and noble-style clients both work. - BTP write semantics: Matter BTP C1 uses ATT Write Request (with response). Previous proxy default was withoutResponse, which the peripheral silently dropped — BTP handshake response never came. Initial JSON write and all binary-frame WRITE_DATA fragments now use response=true. - scan-during-GATT hang on macOS: while noble scans (allowDuplicates=true) for the same service UUID, service.discoverCharacteristicsAsync never fires its delegate. The noble proxy client now stops scanning around connect + GATT discovery and resumes after. - ProxyBle shutdown: register with env.runtime so close cascades to ProxyBleScanner. Fix a microtask race where cancelResolver and finishWaiter were both queued in the same tick — the discovery loop's awaiter could resume before canceled flipped true, re-arming a fresh waiter and hanging shutdown. Trip cancelResolvers, yield a microtask, then unblock awaiters. - Override env.Ble AFTER MatterController.create — that call dynamically imports @matter/nodejs-ble, whose install.js auto-installs NodeJsBle when ble.enable=true. The proxy implementation must win. Quality of life: - Reference noble client adds per-line timestamps, dedup of repeated device_discovered events on unchanged advertisements, and noble warning + stateChange surfaces for diagnostics. - Protocol spec gains an explicit UUID-format section, clarifies the response field on write_characteristic, and documents the with-response constraint on WRITE_DATA binary frames. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add a new `matter-ble-proxy` Python package (under python_ble_proxy/)
that implements the client side of the BLE proxy WebSocket protocol.
The library is consumed by the Home Assistant Matter integration and
shipped with a Bleak-based reference CLI for standalone testing.
Highlights:
- `MatterBleProxy` handles the full protocol surface (start_scan,
stop_scan, connect, disconnect, discover_services,
discover_characteristics, read/write/subscribe/unsubscribe,
request_mtu, plus WRITE_DATA / NOTIFICATION binary frames). Errors
are mapped to spec-defined codes (read_failed, write_failed,
subscribe_failed, not_subscribed, ...).
- Pluggable backends via `BleScanSource` and `BleDeviceResolver` ABCs.
Default `BleakScanSource` / `BleakDeviceResolver` drive Bleak
directly; integrators (e.g. Home Assistant) supply their own.
- Resource-optimized: the scanner only runs between matter-server's
start_scan / stop_scan and is fully torn down on proxy disconnect.
- Per-address advertisement dedup (fingerprint of name, connectable,
service_uuids, service_data) so the matter-server is not flooded
with redundant `device_discovered` events during an in-flight
commission.
- UUID normalization that collapses standard Bluetooth Base UUIDs to
the short form so `service_uuids=["fff6"]` matches Bleak's
canonical `0000fff6-0000-1000-8000-00805f9b34fb` advertisements.
- `matter-ble-proxy` console entry exposes the same flag set as the
JS `noble-ble-proxy` example (`--server`, `--log-level`).
- INFO-level command log (`[←CMD] id=... command ...`) for visibility
without DEBUG.
Repo plumbing:
- `npm run python-ble-proxy:{install,lint,lint-fix,typecheck,test,build}`
- `.github/workflows/test-python-ble-proxy.yml` runs ruff + mypy +
pytest on python_ble_proxy/** changes.
- `release-npm.yml` mirrors the python_client PyPI publish flow for
matter-ble-proxy: a `pypi_ble_proxy_needed` gate patches
python_ble_proxy/pyproject.toml with the same PEP 440 version and
builds + uploads alongside matter-python-client.
ProxyBleScanner shutdown hardening:
- Added a `#closed` flag the discovery loop now checks alongside
`canceled`. Survives the case where commissioning supplies its own
cancelSignal (and our per-query cancelResolver is therefore
undefined), which previously left the loop re-arming a fresh waiter
on shutdown and hanging the matter-server process.
Docs:
- `python_ble_proxy/README.md` covers install, CLI, library API.
- Top-level README and `packages/ble-proxy/README.md` cross-link to
the new Python package and protocol spec.
- `docs/ble-proxy-protocol.md` gained a dedicated UUID-format section
and clarified `response` semantics on write_characteristic and
WRITE_DATA binary frames (Matter BTP requires ATT Write Request).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This was referenced May 19, 2026
…ocol spec version - Remove `docs/plans/*.md` from the repo — these are local-only working notes per CLAUDE.md; the directory is already in `.gitignore` so new ones stay out, but the older tracked entries needed an explicit removal. - Add explicit `superpowers/` / `.superpowers/` entries to `.gitignore` alongside the existing `/.*` catch-all, so AI-tooling scratch dirs are obviously excluded. - Drop the `(Draft)` marker on `docs/ble-proxy-protocol.md` v1.0 now that the protocol is shipping. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… base extension, document python client - `ProxyBleChannel`: every fire-and-forget promise (`btpSession.handleIncomingBleData`, `channel.close()` on unexpected disconnect, BTP-session disconnect callback, openChannel failure cleanup) now attaches a `.catch(err => logger.debug/warn(...))` so failures stay visible instead of becoming unhandled rejections. - `NobleBleProxyClient` (example): same treatment for `#handleCommand` dispatch, peripheral disconnect on shutdown, and the post-failure scan restart. - `ProxyBleScanner`: replaced ~200 lines of duplicated waiter / query / cancellation logic with a 30-line subclass of matter.js's `BleScanner`. We only narrow `getDiscoveredDevice` to expose `ProxyPeripheral` and override `closeClient` to route through `ProxyBleClient`. Behavior parity comes for free (and stays in sync with future matter.js fixes). - `packages/ble-proxy/README.md`: added a "Python Example Client" section pointing at the bundled `matter-ble-proxy` PyPI package + `npm run python-ble-proxy:run` so the two reference clients sit side-by-side. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Add the new BLE proxy mode flag to both: - `docs/cli.md`: row in the options table, cross-referenced with `--bluetooth-adapter` (mutually exclusive). - `docs/docker.md`: `BLE_PROXY` row in the env-var table. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Adds end-to-end BLE proxy commissioning to the Matter server by introducing a dedicated /ble WebSocket endpoint plus a new @matter-server/ble-proxy package, and ships a new matter-ble-proxy Python library/CLI intended for Home Assistant and standalone bridging.
Changes:
- Add BLE proxy server endpoint support (
/ble) and wire it into commissioning via aProxyBleimplementation. - Introduce new
packages/ble-proxyworkspace (protocol, handler, ProxyBle transport, noble reference client) with tests. - Add new
python_ble_proxy/package (library + Bleak CLI) plus CI workflow, docs, and release pipeline updates.
Reviewed changes
Copilot reviewed 43 out of 48 changed files in this pull request and generated 4 comments.
Show a summary per file
| File | Description |
|---|---|
| tsconfig.json | Adds TS project reference for the new packages/ble-proxy workspace. |
| README.md | Documents “BLE Proxy mode” and links to protocol + reference clients. |
| python_ble_proxy/tests/test_protocol.py | Adds unit tests for protocol constants and UUID normalization. |
| python_ble_proxy/tests/init.py | Initializes Python test package. |
| python_ble_proxy/README.md | Documents install, CLI usage, and library API for matter-ble-proxy. |
| python_ble_proxy/pyproject.toml | Defines Python package metadata, deps, and tooling (ruff/mypy/pytest). |
| python_ble_proxy/matter_ble_proxy/py.typed | Marks package as typed. |
| python_ble_proxy/matter_ble_proxy/protocol.py | Adds protocol constants/types for the Python client. |
| python_ble_proxy/matter_ble_proxy/example/cli.py | Adds Bleak-based reference CLI entrypoint. |
| python_ble_proxy/matter_ble_proxy/example/init.py | Documents example module purpose. |
| python_ble_proxy/matter_ble_proxy/client.py | Implements core Python BLE proxy client and command handling. |
| python_ble_proxy/matter_ble_proxy/bleak_backend.py | Provides default Bleak-backed scan + resolve backend. |
| python_ble_proxy/matter_ble_proxy/init.py | Exposes public Python API symbols. |
| python_ble_proxy/.gitignore | Adds Python package-local ignores (venv, caches, dist). |
| packages/ws-controller/src/server/WebSocketControllerHandler.ts | Switches /ws to noServer upgrade handling to avoid breaking other WS endpoints. |
| packages/ws-controller/package.json | Bumps Matter.js SDK versions and optional @matter/nodejs-ble. |
| packages/ws-client/package.json | Bumps @matter/testing version. |
| packages/matter-server/src/MatterServer.ts | Adds --ble-proxy mode, registers /ble handler, and installs ProxyBle into the environment. |
| packages/matter-server/src/cli.ts | Adds --ble-proxy CLI option + env var mapping. |
| packages/matter-server/package.json | Adds dependency on @matter-server/ble-proxy and bumps Matter.js SDK versions. |
| packages/dashboard/package.json | Bumps @matter/main version. |
| packages/custom-clusters/package.json | Bumps @matter/main version. |
| packages/ble-proxy/tsconfig.json | Adds composite TS config for the new workspace. |
| packages/ble-proxy/test/tsconfig.json | Adds test TS config and references. |
| packages/ble-proxy/test/MockBleDevice.ts | Adds mock peripheral generator used by BLE proxy integration tests. |
| packages/ble-proxy/test/BleProxyTestClient.ts | Adds a test WS client that simulates the proxy side. |
| packages/ble-proxy/test/BleProxyProtocolTest.ts | Adds unit tests for binary frame codec. |
| packages/ble-proxy/test/BleProxyIntegrationTest.ts | Adds integration tests for /ble handler + ProxyBle flows. |
| packages/ble-proxy/src/tsconfig.json | Adds library TS config and references. |
| packages/ble-proxy/src/ProxyBleScanner.ts | Implements scanner wrapper using the proxy client. |
| packages/ble-proxy/src/ProxyBleClient.ts | Implements proxy-side discovery client for server-side scanning. |
| packages/ble-proxy/src/ProxyBleChannel.ts | Implements BLE central interface + BTP channel over the proxy protocol. |
| packages/ble-proxy/src/ProxyBle.ts | Implements Ble environment provider that routes via the proxy. |
| packages/ble-proxy/src/index.ts | Exports public API for @matter-server/ble-proxy. |
| packages/ble-proxy/src/example/NobleBleProxyClient.ts | Adds noble-based reference implementation of the client side. |
| packages/ble-proxy/src/example/noble-ble-proxy.ts | Adds CLI wrapper for the noble reference client. |
| packages/ble-proxy/src/BleProxyProtocol.ts | Defines protocol messages/types and binary frame codec for TS. |
| packages/ble-proxy/src/BleProxyHandler.ts | Implements /ble WebSocket handler (handshake + command routing). |
| packages/ble-proxy/README.md | Documents server enablement and reference clients (JS + Python). |
| packages/ble-proxy/package.json | Adds new published workspace package definition and scripts. |
| package.json | Adds workspace entry + helper scripts for noble + python BLE proxy workflows. |
| package-lock.json | Updates lockfile for new workspace, deps, and version bumps. |
| docs/docker.md | Documents new BLE_PROXY env var. |
| docs/cli.md | Documents new --ble-proxy CLI option and exclusivity with --bluetooth-adapter. |
| docs/ble-proxy-protocol.md | Adds full BLE proxy protocol specification and clarifications. |
| .gitignore | Ignores local AI-tooling scratch directories. |
| .github/workflows/test-python-ble-proxy.yml | Adds CI workflow for Python BLE proxy lint/typecheck/tests. |
| .github/workflows/release-npm.yml | Extends release pipeline to optionally publish matter-ble-proxy to PyPI. |
…662 review - Add `ble_proxy_enabled` to server_info handshake so clients can distinguish BLE proxy mode from local-adapter mode (`--ble-proxy` vs `--bluetooth-adapter`). - Add a fallback `upgrade` listener in `WebServer.ts` that 404s + destroys unmatched WebSocket upgrades (handlers tag the request via `_matterHandledUpgrade`). - `ProxyBleChannel`: wrap handshake observer in try/finally so the listener and timer always clean up, even on handshake timeout/reject. - `python_ble_proxy.client._handle_discover_services`: guard against `client.services` being None instead of iterating None on platforms where Bleak's cache hasn't populated. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… services guard - Trim verbose explanations in `_handle_command`, `_handle_start_scan`, and `_handle_discover_services` to the non-obvious WHY. - Annotate `services` as `BleakGATTServiceCollection | None` so mypy accepts the None guard added in the previous commit (Bleak's stub claims the property is always populated post-connect). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Contributor
There was a problem hiding this comment.
Pull request overview
Copilot reviewed 48 out of 53 changed files in this pull request and generated 4 comments.
Comments suppressed due to low confidence (1)
packages/ble-proxy/src/BleProxyHandler.ts:202
sendCommand()has no timeout, so if the proxy client never responds (e.g., BLE stack stalls) the returned promise stays pending and the entry remains in#pendingCommandsuntil disconnect. This can hang commissioning flows indefinitely. Consider adding a per-command timeout (and clearing the pending map entry on timeout) similar topackages/ws-client/src/client.ts'ssendCommandimplementation.
async sendCommand<C extends BleProxyCommandName>(
command: C,
...rest: BleProxyCommandMap[C]["args"] extends undefined ? [] : [args: BleProxyCommandMap[C]["args"]]
): Promise<BleProxyCommandMap[C]["result"]> {
if (!this.connected || !this.#client) {
throw new Error("BLE proxy client not connected");
}
const args = rest[0];
const id = this.#nextCommandId;
this.#nextCommandId = (this.#nextCommandId + 1) & 0xffff;
const message: CommandMessage = { id, command };
if (args !== undefined) {
message.args = args as Record<string, unknown>;
}
const { promise, resolver, rejecter } = createPromise<Record<string, unknown> | undefined>();
this.#pendingCommands.set(id, { resolver, rejecter });
this.#client.send(JSON.stringify(message), err => {
if (err) {
this.#pendingCommands.delete(id);
rejecter(new Error(`Failed to send command ${command}: ${err.message}`));
}
});
return promise as Promise<BleProxyCommandMap[C]["result"]>;
}
…bind cleanup; python_ble_proxy WS-close recovery - `loadBleSupport`: short-circuit when `ble.proxy.enable` is true so proxy mode no longer hard-requires `@matter/nodejs-ble` to be installed. - WebSocketControllerHandler / BleProxyHandler: track upgrade-listener removers as arrays so multiple `register()` calls (one per listen address) don't drop earlier servers' listeners. - python_ble_proxy: factor scan/peripheral teardown into `_release_ble_resources` and invoke it from `_message_loop` finally so an unexpected WS close immediately stops scanning and disconnects peripherals instead of leaking until the caller invokes `disconnect()`. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
… not env vars VariableService rejects `ble.proxy.enable` once `ble.enable` has been set (the `ble` segment is already a leaf, not a map), so the previous attempt at a parallel env var fataled on startup: ImplementationError: Can't set ble.proxy.enable because segment proxy is not a map Drop the env var and thread the boolean through MatterControllerOptions instead. Verified by booting `--ble-proxy` end-to-end on macOS without `@matter/nodejs-ble` installed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Visibility for operators waiting on a proxy client to attach before commissioning can start.
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
…or allow_duplicates; fix deprecated get_event_loop BleProxyHandler: - Replace setTimeout/clearTimeout with `Time.getTimer(...)` (handshake) and `withTimeout(...)` (per-command) so timer reporting matches the rest of the stack and the cancel path naturally evicts the pending command on timeout. Uses `Seconds(...)` for the Duration brand. python_ble_proxy: - `_spawn_task`: prefer the loop captured in `connect()` and fall back to `asyncio.get_running_loop()`; `asyncio.get_event_loop()` is deprecated in Python 3.12+ and raises in threads without a running loop. - `_handle_start_scan`: honor the protocol's `allow_duplicates` arg. When true (the server-side default) forward every advertisement; otherwise keep the per-address fingerprint dedup so 10 Hz Matter beacons don't spam the WebSocket. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Comment on lines
145
to
+153
| async register(server: HttpServer) { | ||
| logger.info(`Starting server: matter-server/${this.#serverVersion} (matter.js/${MATTER_VERSION})`); | ||
| const wss = (this.#wss = new WebSocketServer({ server: server, path: "/ws" })); | ||
| // Use noServer mode with a path-filtered upgrade listener. | ||
| // ws 8.x calls handleUpgrade unconditionally from its own upgrade listener, which | ||
| // sends HTTP 400 for non-matching paths and destroys the socket — breaking other | ||
| // WebSocket endpoints on the same server. By handling upgrade ourselves we only | ||
| // call handleUpgrade when the path actually matches. | ||
| const wss = (this.#wss = new WebSocketServer({ noServer: true })); | ||
| const upgradeHandler = ( |
| // ws 8.x calls handleUpgrade unconditionally from its own upgrade listener, which | ||
| // sends HTTP 400 for non-matching paths and destroys the socket — breaking other | ||
| // WebSocket endpoints on the same server. | ||
| const wss = (this.#wss = new WebSocketServer({ noServer: true })); |
Comment on lines
+59
to
+66
| async startScanning(): Promise<void> { | ||
| if (this.#isScanning) { | ||
| return; | ||
| } | ||
|
|
||
| if (!this.#handler.connected) { | ||
| logger.info("BLE proxy not connected, deferring scan start"); | ||
| return; |
Comment on lines
+335
to
+342
| service_uuids: list[str] = args.get("service_uuids", []) | ||
| service_uuid_set = {_normalize_uuid(u) for u in service_uuids} if service_uuids else None | ||
| allow_duplicates: bool = bool(args.get("allow_duplicates", False)) | ||
|
|
||
| # When `allow_duplicates` is false, dedup by content fingerprint — Matter peripherals | ||
| # broadcast at ~10 Hz and the server only needs state changes. When true, the server | ||
| # explicitly opts in to the full stream (e.g. to track RSSI updates). | ||
| last_fingerprint: dict[str, tuple[str | None, bool, tuple[str, ...], tuple[tuple[str, bytes], ...]]] = {} |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
End-to-end BLE proxy commissioning support, plus a brand-new
matter-ble-proxyPython client library that the Home Assistant Matterintegration consumes (see companion HA core PR).
This is two logical changes squashed onto one branch:
End-to-end BLE proxy commissioning — Use an external BLE proxy to commission Matter devices directly into the Matter server. This lifts the limitation to only support local ble adapters. Matter device commissioning over the proxy now completes end-to-end (PASE → CASE → operational).
matter-ble-proxyPython package — new top-levelpython_ble_proxy/package. Ships protocol logic + Bleak-based default backend + CLI. Used by the HA Matter integration to integrate with this new BLE proxy feature of the matter server, and provides a standalone reference CLI for testing.matter server BLE Proxy support
--ble-proxyCLI option it supports a dedicated BLE proxy websocket protocol at the /ble path of the matter server. Details see https://github.com/matter-js/matterjs-server/blob/590e85dcfa560d03c55f3f3db2aeaf3efd74433c/docs/ble-proxy-protocol.mdJavaScript example using noble
matter-ble-proxy Python library
`BleDeviceResolver` ABCs, `AdvertisementData` dataclass,
`BleakScanSource` / `BleakDeviceResolver` defaults.
matter-server's `start_scan` / `stop_scan`. Fully torn down on
proxy disconnect. No idle scanning.
(`read_failed`, `write_failed`, `subscribe_failed`,
`not_subscribed`, …) instead of bucketing into
`internal_error`.
example (`[←CMD] id=… command …`), with base64 payloads stripped
for readability.
reference (`--server`, `--log-level`).
Docs
at both reference clients + the protocol spec.
`response` field on `write_characteristic`, and the ATT Write
Request requirement on `WRITE_DATA` binary frames.
🤖 Generated with Claude Code