Skip to content
Draft
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ This page shows a detailed overview of the changes between versions without the
## **WORK IN PROGRESS**
-->

## **WORK IN PROGRESS**

- Enhancement: `matter_ble_proxy` Python library: add `AdvertisementData.from_bleak(address, connectable, bleak_advertisement)` factory so integrators with a `bleak.backends.scanner.AdvertisementData` can skip the field-by-field translation step

## 0.7.1 (2026-05-21)

- Feature: Added BLE proxy commissioning support (enabled with `--ble-proxy` CLI option)
Expand Down
19 changes: 19 additions & 0 deletions python_ble_proxy/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,26 @@ class MyScanSource(BleScanSource):

class MyDeviceResolver(BleDeviceResolver):
async def resolve(self, address): ... # return a bleak.BLEDevice / address / None
```

If your backend already has a `bleak.backends.scanner.AdvertisementData`
(e.g. via `BluetoothServiceInfoBleak.advertisement` in Home Assistant),
build the library's dataclass with the `from_bleak` factory instead of
copying fields manually:

```python
callback(
AdvertisementData.from_bleak(
service_info.address,
service_info.connectable,
service_info.advertisement,
)
)
```

Wire it up:

```python
proxy = MatterBleProxy(
ws_url="ws://localhost:5580/ble",
scan_source=MyScanSource(),
Expand Down
19 changes: 9 additions & 10 deletions python_ble_proxy/matter_ble_proxy/bleak_backend.py
Original file line number Diff line number Diff line change
Expand Up @@ -72,17 +72,16 @@ def _on_detection(self, device: BLEDevice, advertisement: BleakAdvertisementData
cb = self._callback
if cb is None:
return
ad = AdvertisementData(
address=device.address,
name=advertisement.local_name or device.name,
rssi=advertisement.rssi,
connectable=True, # Bleak does not expose this directly; assume true.
service_data=dict(advertisement.service_data),
manufacturer_data=dict(advertisement.manufacturer_data),
service_uuids=list(advertisement.service_uuids),
)
# Bleak's per-scan advertisement carries the local_name only when it is
# actually in the current packet; fall back to the BLEDevice's cached
# name so peripherals that alternate name-bearing and name-less ads
# still surface a name to the matter-server.
local_name = advertisement.local_name or device.name
if local_name and local_name != advertisement.local_name:
advertisement = advertisement._replace(local_name=local_name)
try:
cb(ad)
# Bleak does not expose `connectable` at scan time; assume True.
cb(AdvertisementData.from_bleak(device.address, True, advertisement))
except Exception:
_LOGGER.exception("BLE proxy advertisement callback raised")

Expand Down
35 changes: 35 additions & 0 deletions python_ble_proxy/matter_ble_proxy/protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,10 @@

from dataclasses import dataclass, field
import struct
from typing import TYPE_CHECKING

if TYPE_CHECKING:
from bleak.backends.scanner import AdvertisementData as BleakAdvertisementData

# Current protocol version. Must match the matter-server's
# `BLE_PROXY_PROTOCOL_VERSION` constant; the server rejects clients that send
Expand Down Expand Up @@ -61,3 +65,34 @@ class AdvertisementData:

service_uuids: list[str] = field(default_factory=list)
"""List of advertised service UUIDs."""

@classmethod
def from_bleak(
cls,
address: str,
connectable: bool,
advertisement: BleakAdvertisementData,
) -> AdvertisementData:
"""Build from a `bleak.backends.scanner.AdvertisementData` plus the two fields it lacks.

:class:`bleak.backends.scanner.AdvertisementData` intentionally carries
neither the peripheral address (it lives on the paired ``BLEDevice``)
nor the connectable bit (it lives on the scan context), so both are
passed explicitly. ``tx_power`` and ``platform_data`` are not part of
the wire schema and are dropped.

Use this helper from any Bleak-backed :class:`BleScanSource` to avoid a
field-by-field translation step in the integrator. Home Assistant's
``BluetoothServiceInfoBleak.advertisement`` already materializes a
:class:`bleak.backends.scanner.AdvertisementData`, so the HA backend can
forward it directly.
"""
return cls(
address=address,
name=advertisement.local_name,
rssi=advertisement.rssi,
connectable=connectable,
service_data=dict(advertisement.service_data),
manufacturer_data=dict(advertisement.manufacturer_data),
service_uuids=list(advertisement.service_uuids),
)
39 changes: 39 additions & 0 deletions python_ble_proxy/tests/test_protocol.py
Original file line number Diff line number Diff line change
Expand Up @@ -60,3 +60,42 @@ def test_advertisement_data_defaults():
assert ad.service_data == {}
assert ad.manufacturer_data == {}
assert ad.service_uuids == []


def test_advertisement_data_from_bleak():
from bleak.backends.scanner import AdvertisementData as BleakAdvertisementData

bleak_ad = BleakAdvertisementData(
local_name="dev",
manufacturer_data={0x004C: b"\x01\x02"},
service_data={"0000fff6-0000-1000-8000-00805f9b34fb": b"\xaa"},
service_uuids=["0000fff6-0000-1000-8000-00805f9b34fb"],
tx_power=-12,
rssi=-50,
platform_data=(),
)
ad = AdvertisementData.from_bleak("aa:bb:cc:dd:ee:ff", True, bleak_ad)
assert ad.address == "aa:bb:cc:dd:ee:ff"
assert ad.connectable is True
assert ad.name == "dev"
assert ad.rssi == -50
assert ad.service_data == {"0000fff6-0000-1000-8000-00805f9b34fb": b"\xaa"}
assert ad.manufacturer_data == {0x004C: b"\x01\x02"}
assert ad.service_uuids == ["0000fff6-0000-1000-8000-00805f9b34fb"]


def test_advertisement_data_from_bleak_uses_local_name():
from bleak.backends.scanner import AdvertisementData as BleakAdvertisementData

bleak_ad = BleakAdvertisementData(
local_name=None,
manufacturer_data={},
service_data={},
service_uuids=[],
tx_power=None,
rssi=-72,
platform_data=(),
)
ad = AdvertisementData.from_bleak("aa:bb:cc:dd:ee:ff", False, bleak_ad)
assert ad.name is None
assert ad.connectable is False
Loading