|
3 | 3 | import copy
|
4 | 4 | import json
|
5 | 5 | import logging
|
| 6 | +import struct |
6 | 7 | from threading import Thread
|
7 | 8 |
|
8 | 9 | import aioblescan as aiobs
|
|
11 | 12 | from homeassistant.config_entries import SOURCE_IMPORT, ConfigEntry
|
12 | 13 | from homeassistant.const import (CONF_DEVICES, CONF_DISCOVERY, CONF_MAC,
|
13 | 14 | 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 |
16 | 18 | from homeassistant.helpers import config_validation as cv
|
17 | 19 | from homeassistant.helpers.entity_registry import async_entries_for_device
|
18 | 20 | from homeassistant.util import dt
|
| 21 | +try: |
| 22 | + from homeassistant.components import bluetooth |
| 23 | +except ImportError: |
| 24 | + bluetooth = None |
19 | 25 |
|
20 | 26 | from .ble_parser import BleParser
|
21 | 27 | from .bt_helpers import (BT_INTERFACES, BT_MULTI_SELECT, DEFAULT_BT_INTERFACE,
|
|
28 | 34 | CONF_DEVICE_RESET_TIMER, CONF_DEVICE_RESTORE_STATE,
|
29 | 35 | CONF_DEVICE_TRACK, CONF_DEVICE_TRACKER_CONSIDER_HOME,
|
30 | 36 | 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, |
32 | 38 | CONF_PACKET, CONF_PERIOD, CONF_REPORT_UNKNOWN,
|
33 | 39 | CONF_RESTORE_STATE, CONF_USE_MEDIAN, CONF_UUID,
|
34 | 40 | CONFIG_IS_FLOW, DEFAULT_ACTIVE_SCAN, DEFAULT_BATT_ENTITIES,
|
|
130 | 136 | SERVICE_PARSE_DATA_SCHEMA = vol.Schema(
|
131 | 137 | {
|
132 | 138 | 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 |
134 | 142 | }
|
135 | 143 | )
|
136 | 144 |
|
@@ -423,8 +431,10 @@ async def async_parse_data_service(hass: HomeAssistant, service_data):
|
423 | 431 | blemonitor: BLEmonitor = hass.data[DOMAIN]["blemonitor"]
|
424 | 432 | if blemonitor:
|
425 | 433 | 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] |
428 | 438 | )
|
429 | 439 |
|
430 | 440 |
|
@@ -506,6 +516,7 @@ def __init__(self, config, dataqueue):
|
506 | 516 | self.report_unknown = False
|
507 | 517 | self.report_unknown_whitelist = []
|
508 | 518 | self.last_bt_reset = dt.now()
|
| 519 | + self.scanners = {} |
509 | 520 | if self.config[CONF_REPORT_UNKNOWN]:
|
510 | 521 | if self.config[CONF_REPORT_UNKNOWN] != "Off":
|
511 | 522 | self.report_unknown = self.config[CONF_REPORT_UNKNOWN]
|
@@ -578,11 +589,118 @@ def __init__(self, config, dataqueue):
|
578 | 589 | aeskeys=self.aeskeys,
|
579 | 590 | )
|
580 | 591 |
|
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): |
582 | 683 | """Parse HCI events."""
|
583 | 684 | self.evt_cnt += 1
|
584 | 685 | if len(data) < 12:
|
585 | 686 | 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()) |
586 | 704 | sensor_msg, tracker_msg = self.ble_parser.parse_raw_data(data)
|
587 | 705 | if sensor_msg:
|
588 | 706 | measurements = list(sensor_msg.keys())
|
|
0 commit comments