Skip to content

Commit 8a6d943

Browse files
committed
Allow proxy HCI packets to Home Assistant Bluetooth stack
1 parent af07174 commit 8a6d943

File tree

2 files changed

+126
-7
lines changed

2 files changed

+126
-7
lines changed

custom_components/ble_monitor/__init__.py

+125-7
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import copy
44
import json
55
import logging
6+
import struct
67
from threading import Thread
78

89
import aioblescan as aiobs
@@ -11,11 +12,16 @@
1112
from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
1213
from homeassistant.const import (CONF_DEVICES, CONF_DISCOVERY, CONF_MAC,
1314
CONF_NAME, CONF_TEMPERATURE_UNIT,
14-
CONF_UNIQUE_ID, EVENT_HOMEASSISTANT_STOP)
15-
from homeassistant.core import HomeAssistant
15+
CONF_UNIQUE_ID, EVENT_HOMEASSISTANT_STOP,
16+
ATTR_DEVICE_ID)
17+
from homeassistant.core import HomeAssistant, async_get_hass
1618
from homeassistant.helpers import config_validation as cv
1719
from homeassistant.helpers.entity_registry import async_entries_for_device
1820
from homeassistant.util import dt
21+
try:
22+
from homeassistant.components import bluetooth
23+
except ImportError:
24+
bluetooth = None
1925

2026
from .ble_parser import BleParser
2127
from .bt_helpers import (BT_INTERFACES, BT_MULTI_SELECT, DEFAULT_BT_INTERFACE,
@@ -28,7 +34,7 @@
2834
CONF_DEVICE_RESET_TIMER, CONF_DEVICE_RESTORE_STATE,
2935
CONF_DEVICE_TRACK, CONF_DEVICE_TRACKER_CONSIDER_HOME,
3036
CONF_DEVICE_TRACKER_SCAN_INTERVAL, CONF_DEVICE_USE_MEDIAN,
31-
CONF_GATEWAY_ID, CONF_HCI_INTERFACE, CONF_LOG_SPIKES,
37+
CONF_GATEWAY_ID, CONF_PROXY, CONF_HCI_INTERFACE, CONF_LOG_SPIKES,
3238
CONF_PACKET, CONF_PERIOD, CONF_REPORT_UNKNOWN,
3339
CONF_RESTORE_STATE, CONF_USE_MEDIAN, CONF_UUID,
3440
CONFIG_IS_FLOW, DEFAULT_ACTIVE_SCAN, DEFAULT_BATT_ENTITIES,
@@ -130,7 +136,9 @@
130136
SERVICE_PARSE_DATA_SCHEMA = vol.Schema(
131137
{
132138
vol.Required(CONF_PACKET): cv.string,
133-
vol.Optional(CONF_GATEWAY_ID): cv.string
139+
vol.Optional(ATTR_DEVICE_ID, default=None): cv.string,
140+
vol.Optional(CONF_GATEWAY_ID, default=DOMAIN): cv.string,
141+
vol.Optional(CONF_PROXY, default=False): cv.boolean
134142
}
135143
)
136144

@@ -423,8 +431,10 @@ async def async_parse_data_service(hass: HomeAssistant, service_data):
423431
blemonitor: BLEmonitor = hass.data[DOMAIN]["blemonitor"]
424432
if blemonitor:
425433
blemonitor.dumpthread.process_hci_events(
426-
bytes.fromhex(service_data["packet"]),
427-
service_data[CONF_GATEWAY_ID] if CONF_GATEWAY_ID in service_data else DOMAIN
434+
data=bytes.fromhex(service_data["packet"]),
435+
device_id=service_data[ATTR_DEVICE_ID],
436+
gateway_id=service_data[CONF_GATEWAY_ID],
437+
proxy=service_data[CONF_PROXY]
428438
)
429439

430440

@@ -506,6 +516,7 @@ def __init__(self, config, dataqueue):
506516
self.report_unknown = False
507517
self.report_unknown_whitelist = []
508518
self.last_bt_reset = dt.now()
519+
self.scanners = {}
509520
if self.config[CONF_REPORT_UNKNOWN]:
510521
if self.config[CONF_REPORT_UNKNOWN] != "Off":
511522
self.report_unknown = self.config[CONF_REPORT_UNKNOWN]
@@ -578,11 +589,118 @@ def __init__(self, config, dataqueue):
578589
aeskeys=self.aeskeys,
579590
)
580591

581-
def process_hci_events(self, data, gateway_id=DOMAIN):
592+
@staticmethod
593+
def hci_packet_on_advertisement(scanner, packet):
594+
def _format_uuid(uuid: bytes) -> str:
595+
if len(uuid) == 2 or len(uuid) == 4:
596+
return "{:08x}-0000-1000-8000-00805f9b34fb".format(
597+
struct.unpack('<H' if len(uuid) == 2 else '<I', bytes(uuid))[0]
598+
)
599+
elif len(uuid) == 16:
600+
reversed_uuid = uuid[::-1]
601+
return '-'.join([
602+
''.join(format(x, '02x') for x in reversed_uuid[:4]),
603+
''.join(format(x, '02x') for x in reversed_uuid[4:6]),
604+
''.join(format(x, '02x') for x in reversed_uuid[6:8]),
605+
''.join(format(x, '02x') for x in reversed_uuid[8:10]),
606+
''.join(format(x, '02x') for x in reversed_uuid[10:])
607+
])
608+
else:
609+
raise Exception(f"Wrong UUID size {len(uuid)}")
610+
611+
packet_size = packet[2] + 3
612+
is_ext_packet = packet[3] == 0x0D
613+
if packet_size != len(packet):
614+
raise Exception(f"Wrong packet size {packet_size}, expected {len(packet)}")
615+
payload_start = 29 if is_ext_packet else 14
616+
if payload_start > packet_size:
617+
raise Exception(f"Wrong payload start index {payload_start}")
618+
payload_size = packet[payload_start - 1]
619+
payload_packet_size = payload_start + payload_size + (0 if is_ext_packet else 1)
620+
if packet_size != payload_packet_size:
621+
raise Exception(f"Wrong packet size {packet_size}, expected {payload_packet_size}")
622+
623+
tx_power = None
624+
rssi = packet[18 if is_ext_packet else packet_size - 1]
625+
if rssi < 128:
626+
raise Exception(f"Positive RSSI {rssi}")
627+
rssi -= 256
628+
629+
address_index = 8 if is_ext_packet else 7
630+
address_type = packet[address_index - 1]
631+
address = ':'.join(f'{i:02X}' for i in packet[address_index:address_index + 6][::-1])
632+
local_name = None
633+
service_uuids = []
634+
service_data = {}
635+
manufacturer_data = {}
636+
637+
while payload_size > 1:
638+
record_size = packet[payload_start] + 1
639+
if 1 < record_size <= payload_size:
640+
record = packet[payload_start:payload_start + record_size]
641+
if record[0] != record_size - 1:
642+
raise Exception(f"Wrong record size {record[0]}, expected {record_size - 1}")
643+
record_type = record[1]
644+
record = record[2:]
645+
# Incomplete/Complete List of 16/32/128-bit Service Class UUIDs
646+
if record_type in [0x02, 0x03, 0x04, 0x05, 0x06, 0x07]:
647+
service_uuids.append(_format_uuid(record))
648+
# Shortened/Complete local name
649+
elif record_type in [0x08, 0x09]:
650+
name = record.decode("utf-8", errors="replace")
651+
if local_name is None or len(name) > len(local_name):
652+
local_name = name
653+
# TX Power
654+
elif record_type == 0x0A:
655+
tx_power = record[0]
656+
# Service Data of 16/32/128-bit UUID
657+
elif record_type in [0x16, 0x20, 0x21]:
658+
record_type_sizes = {0x16: 2, 0x20: 4, 0x21: 16}
659+
uuid_size = record_type_sizes[record_type]
660+
if len(record) < uuid_size:
661+
raise Exception("Wrong service data 0x{:02X} size {}, expected {}".format(
662+
record_type, len(record), record_type_sizes[record_type]))
663+
service_data[_format_uuid(record[:uuid_size])] = record[uuid_size:]
664+
# Manufacturer Specific Data
665+
elif record_type == 0xFF:
666+
manufacturer_data[(record[1] << 8) | record[0]] = record[2:]
667+
payload_size -= record_size
668+
payload_start += record_size
669+
670+
scanner._async_on_advertisement(
671+
address=address,
672+
rssi=rssi,
673+
local_name=local_name,
674+
service_uuids=service_uuids,
675+
service_data=service_data,
676+
manufacturer_data=manufacturer_data,
677+
tx_power=tx_power,
678+
details={"address_type": address_type},
679+
advertisement_monotonic_time=bluetooth.MONOTONIC_TIME(),
680+
)
681+
682+
def process_hci_events(self, data, device_id=None, gateway_id=DOMAIN, proxy=False):
582683
"""Parse HCI events."""
583684
self.evt_cnt += 1
584685
if len(data) < 12:
585686
return
687+
if bluetooth is not None and proxy:
688+
try:
689+
scanner_name = device_id or gateway_id
690+
scanner = self.scanners.get(scanner_name)
691+
if not scanner:
692+
hass = async_get_hass()
693+
device = hass.data["device_registry"].devices.get(device_id) if device_id \
694+
else next((entry for entry in hass.data["device_registry"].devices.data.values()
695+
if entry.name.lower() == gateway_id.lower()), None)
696+
source = next((connection[1] for connection in device.connections if
697+
connection[0] in ["mac", "bluetooth"]), gateway_id) if device else gateway_id
698+
scanner = bluetooth.BaseHaRemoteScanner(source, gateway_id, None, False)
699+
bluetooth.async_register_scanner(hass, scanner)
700+
self.scanners[scanner_name] = scanner
701+
self.hci_packet_on_advertisement(scanner, data)
702+
except Exception as e:
703+
_LOGGER.error("%s: %s: %s", gateway_id, e, data.hex().upper())
586704
sensor_msg, tracker_msg = self.ble_parser.parse_raw_data(data)
587705
if sensor_msg:
588706
measurements = list(sensor_msg.keys())

custom_components/ble_monitor/const.py

+1
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@
4747
CONF_DEVICE_DELETE_DEVICE = "delete device"
4848
CONF_PACKET = "packet"
4949
CONF_GATEWAY_ID = "gateway_id"
50+
CONF_PROXY = "proxy"
5051
CONF_UUID = "uuid"
5152
CONFIG_IS_FLOW = "is_flow"
5253

0 commit comments

Comments
 (0)