mirror of
https://github.com/home-assistant/core.git
synced 2025-07-23 13:17:32 +00:00
Add support for ESPHome raw bluetooth advertisements (#94138)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
This commit is contained in:
parent
8e9eb400bf
commit
88bfd94800
@ -82,13 +82,13 @@ DEFAULT_URL = f"https://esphome.io/changelog/{STABLE_BLE_VERSION_STR}.html"
|
||||
|
||||
@callback
|
||||
def _async_check_firmware_version(
|
||||
hass: HomeAssistant, device_info: EsphomeDeviceInfo
|
||||
hass: HomeAssistant, device_info: EsphomeDeviceInfo, api_version: APIVersion
|
||||
) -> None:
|
||||
"""Create or delete an the ble_firmware_outdated issue."""
|
||||
# ESPHome device_info.mac_address is the unique_id
|
||||
issue = f"ble_firmware_outdated-{device_info.mac_address}"
|
||||
if (
|
||||
not device_info.bluetooth_proxy_version
|
||||
not device_info.bluetooth_proxy_feature_flags_compat(api_version)
|
||||
# If the device has a project name its up to that project
|
||||
# to tell them about the firmware version update so we don't notify here
|
||||
or (device_info.project_name and device_info.project_name not in PROJECT_URLS)
|
||||
@ -360,7 +360,7 @@ async def async_setup_entry( # noqa: C901
|
||||
if entry_data.device_info.name:
|
||||
reconnect_logic.name = entry_data.device_info.name
|
||||
|
||||
if device_info.bluetooth_proxy_version:
|
||||
if device_info.bluetooth_proxy_feature_flags_compat(cli.api_version):
|
||||
entry_data.disconnect_callbacks.append(
|
||||
await async_connect_scanner(hass, entry, cli, entry_data)
|
||||
)
|
||||
@ -391,7 +391,7 @@ async def async_setup_entry( # noqa: C901
|
||||
# Re-connection logic will trigger after this
|
||||
await cli.disconnect()
|
||||
else:
|
||||
_async_check_firmware_version(hass, device_info)
|
||||
_async_check_firmware_version(hass, device_info, entry_data.api_version)
|
||||
_async_check_using_api_password(hass, device_info, bool(password))
|
||||
|
||||
async def on_disconnect() -> None:
|
||||
|
@ -5,7 +5,7 @@ from collections.abc import Callable
|
||||
from functools import partial
|
||||
import logging
|
||||
|
||||
from aioesphomeapi import APIClient
|
||||
from aioesphomeapi import APIClient, BluetoothProxyFeature
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
HaBluetoothConnector,
|
||||
@ -59,13 +59,15 @@ async def async_connect_scanner(
|
||||
source = str(entry.unique_id)
|
||||
new_info_callback = async_get_advertisement_callback(hass)
|
||||
assert entry_data.device_info is not None
|
||||
version = entry_data.device_info.bluetooth_proxy_version
|
||||
connectable = version >= 2
|
||||
feature_flags = entry_data.device_info.bluetooth_proxy_feature_flags_compat(
|
||||
entry_data.api_version
|
||||
)
|
||||
connectable = bool(feature_flags & BluetoothProxyFeature.ACTIVE_CONNECTIONS)
|
||||
_LOGGER.debug(
|
||||
"%s [%s]: Connecting scanner version=%s, connectable=%s",
|
||||
"%s [%s]: Connecting scanner feature_flags=%s, connectable=%s",
|
||||
entry.title,
|
||||
source,
|
||||
version,
|
||||
feature_flags,
|
||||
connectable,
|
||||
)
|
||||
connector = HaBluetoothConnector(
|
||||
@ -89,7 +91,12 @@ async def async_connect_scanner(
|
||||
async_register_scanner(hass, scanner, connectable),
|
||||
scanner.async_setup(),
|
||||
]
|
||||
await cli.subscribe_bluetooth_le_advertisements(scanner.async_on_advertisement)
|
||||
if feature_flags & BluetoothProxyFeature.RAW_ADVERTISEMENTS:
|
||||
await cli.subscribe_bluetooth_le_raw_advertisements(
|
||||
scanner.async_on_raw_advertisements
|
||||
)
|
||||
else:
|
||||
await cli.subscribe_bluetooth_le_advertisements(scanner.async_on_advertisement)
|
||||
|
||||
@hass_callback
|
||||
def _async_unload() -> None:
|
||||
|
@ -12,6 +12,7 @@ from aioesphomeapi import (
|
||||
ESP_CONNECTION_ERROR_DESCRIPTION,
|
||||
ESPHOME_GATT_ERRORS,
|
||||
BLEConnectionError,
|
||||
BluetoothProxyFeature,
|
||||
)
|
||||
from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError
|
||||
from aioesphomeapi.core import BluetoothGATTAPIError
|
||||
@ -42,10 +43,6 @@ CCCD_UUID = "00002902-0000-1000-8000-00805f9b34fb"
|
||||
CCCD_NOTIFY_BYTES = b"\x01\x00"
|
||||
CCCD_INDICATE_BYTES = b"\x02\x00"
|
||||
|
||||
MIN_BLUETOOTH_PROXY_VERSION_HAS_CACHE = 3
|
||||
MIN_BLUETOOTH_PROXY_HAS_PAIRING = 4
|
||||
MIN_BLUETOOTH_PROXY_HAS_CLEAR_CACHE = 5
|
||||
|
||||
DEFAULT_MAX_WRITE_WITHOUT_RESPONSE = DEFAULT_MTU - GATT_HEADER_SIZE
|
||||
_LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@ -158,7 +155,10 @@ class ESPHomeClient(BaseBleakClient):
|
||||
self._disconnected_event: asyncio.Event | None = None
|
||||
device_info = self.entry_data.device_info
|
||||
assert device_info is not None
|
||||
self._connection_version = device_info.bluetooth_proxy_version
|
||||
self._device_info = device_info
|
||||
self._feature_flags = device_info.bluetooth_proxy_feature_flags_compat(
|
||||
self.entry_data.api_version
|
||||
)
|
||||
self._address_type = address_or_ble_device.details["address_type"]
|
||||
self._source_name = f"{config_entry.title} [{self._source}]"
|
||||
|
||||
@ -247,7 +247,7 @@ class ESPHomeClient(BaseBleakClient):
|
||||
self._mtu = domain_data.get_gatt_mtu_cache(self._address_as_int)
|
||||
has_cache = bool(
|
||||
dangerous_use_bleak_cache
|
||||
and self._connection_version >= MIN_BLUETOOTH_PROXY_VERSION_HAS_CACHE
|
||||
and self._feature_flags & BluetoothProxyFeature.REMOTE_CACHING
|
||||
and domain_data.get_gatt_services_cache(self._address_as_int)
|
||||
and self._mtu
|
||||
)
|
||||
@ -319,7 +319,7 @@ class ESPHomeClient(BaseBleakClient):
|
||||
_on_bluetooth_connection_state,
|
||||
timeout=timeout,
|
||||
has_cache=has_cache,
|
||||
version=self._connection_version,
|
||||
feature_flags=self._feature_flags,
|
||||
address_type=self._address_type,
|
||||
)
|
||||
)
|
||||
@ -397,9 +397,10 @@ class ESPHomeClient(BaseBleakClient):
|
||||
@api_error_as_bleak_error
|
||||
async def pair(self, *args: Any, **kwargs: Any) -> bool:
|
||||
"""Attempt to pair."""
|
||||
if self._connection_version < MIN_BLUETOOTH_PROXY_HAS_PAIRING:
|
||||
if not self._feature_flags & BluetoothProxyFeature.PAIRING:
|
||||
raise NotImplementedError(
|
||||
"Pairing is not available in ESPHome with version {self._connection_version}."
|
||||
"Pairing is not available in this version ESPHome; "
|
||||
f"Upgrade the ESPHome version on the {self._device_info.name} device."
|
||||
)
|
||||
response = await self._client.bluetooth_device_pair(self._address_as_int)
|
||||
if response.paired:
|
||||
@ -413,9 +414,10 @@ class ESPHomeClient(BaseBleakClient):
|
||||
@api_error_as_bleak_error
|
||||
async def unpair(self) -> bool:
|
||||
"""Attempt to unpair."""
|
||||
if self._connection_version < MIN_BLUETOOTH_PROXY_HAS_PAIRING:
|
||||
if not self._feature_flags & BluetoothProxyFeature.PAIRING:
|
||||
raise NotImplementedError(
|
||||
"Unpairing is not available in ESPHome with version {self._connection_version}."
|
||||
"Unpairing is not available in this version ESPHome; "
|
||||
f"Upgrade the ESPHome version on the {self._device_info.name} device."
|
||||
)
|
||||
response = await self._client.bluetooth_device_unpair(self._address_as_int)
|
||||
if response.success:
|
||||
@ -441,7 +443,7 @@ class ESPHomeClient(BaseBleakClient):
|
||||
# because the esp has already wiped the services list to
|
||||
# save memory.
|
||||
if (
|
||||
self._connection_version >= MIN_BLUETOOTH_PROXY_VERSION_HAS_CACHE
|
||||
self._feature_flags & BluetoothProxyFeature.REMOTE_CACHING
|
||||
or dangerous_use_bleak_cache
|
||||
) and (cached_services := domain_data.get_gatt_services_cache(address_as_int)):
|
||||
_LOGGER.debug(
|
||||
@ -524,12 +526,11 @@ class ESPHomeClient(BaseBleakClient):
|
||||
"""Clear the GATT cache."""
|
||||
self.domain_data.clear_gatt_services_cache(self._address_as_int)
|
||||
self.domain_data.clear_gatt_mtu_cache(self._address_as_int)
|
||||
if self._connection_version < MIN_BLUETOOTH_PROXY_HAS_CLEAR_CACHE:
|
||||
if not self._feature_flags & BluetoothProxyFeature.CACHE_CLEARING:
|
||||
_LOGGER.warning(
|
||||
"On device cache clear is not available with ESPHome Bluetooth version %s, "
|
||||
"version %s is needed; Only memory cache will be cleared",
|
||||
self._connection_version,
|
||||
MIN_BLUETOOTH_PROXY_HAS_CLEAR_CACHE,
|
||||
"On device cache clear is not available with this ESPHome version; "
|
||||
"Upgrade the ESPHome version on the device %s; Only memory cache will be cleared",
|
||||
self._device_info.name,
|
||||
)
|
||||
return True
|
||||
response = await self._client.bluetooth_device_clear_cache(self._address_as_int)
|
||||
@ -673,7 +674,7 @@ class ESPHomeClient(BaseBleakClient):
|
||||
lambda handle, data: callback(data),
|
||||
)
|
||||
|
||||
if self._connection_version < MIN_BLUETOOTH_PROXY_VERSION_HAS_CACHE:
|
||||
if not self._feature_flags & BluetoothProxyFeature.REMOTE_CACHING:
|
||||
return
|
||||
|
||||
# For connection v3 we are responsible for enabling notifications
|
||||
|
@ -1,8 +1,8 @@
|
||||
"""Bluetooth scanner for esphome."""
|
||||
from __future__ import annotations
|
||||
|
||||
from aioesphomeapi import BluetoothLEAdvertisement
|
||||
from bluetooth_data_tools import int_to_bluetooth_address
|
||||
from aioesphomeapi import BluetoothLEAdvertisement, BluetoothLERawAdvertisement
|
||||
from bluetooth_data_tools import int_to_bluetooth_address, parse_advertisement_data
|
||||
|
||||
from homeassistant.components.bluetooth import BaseHaRemoteScanner
|
||||
from homeassistant.core import callback
|
||||
@ -25,3 +25,21 @@ class ESPHomeScanner(BaseHaRemoteScanner):
|
||||
None,
|
||||
{"address_type": adv.address_type},
|
||||
)
|
||||
|
||||
@callback
|
||||
def async_on_raw_advertisements(
|
||||
self, advertisements: list[BluetoothLERawAdvertisement]
|
||||
) -> None:
|
||||
"""Call the registered callback."""
|
||||
for adv in advertisements:
|
||||
parsed = parse_advertisement_data((adv.data,))
|
||||
self._async_on_advertisement(
|
||||
int_to_bluetooth_address(adv.address),
|
||||
adv.rssi,
|
||||
parsed.local_name,
|
||||
parsed.service_uuids,
|
||||
parsed.service_data,
|
||||
parsed.manufacturer_data,
|
||||
None,
|
||||
{"address_type": adv.address_type},
|
||||
)
|
||||
|
@ -15,7 +15,7 @@
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol"],
|
||||
"requirements": [
|
||||
"aioesphomeapi==13.9.0",
|
||||
"aioesphomeapi==14.0.0",
|
||||
"bluetooth-data-tools==0.4.0",
|
||||
"esphome-dashboard-api==1.2.3"
|
||||
],
|
||||
|
@ -234,7 +234,7 @@ aioecowitt==2023.5.0
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==13.9.0
|
||||
aioesphomeapi==14.0.0
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
|
@ -212,7 +212,7 @@ aioecowitt==2023.5.0
|
||||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==13.9.0
|
||||
aioesphomeapi==14.0.0
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
|
@ -63,7 +63,7 @@ def mock_device_info() -> DeviceInfo:
|
||||
return DeviceInfo(
|
||||
uses_password=False,
|
||||
name="test",
|
||||
bluetooth_proxy_version=0,
|
||||
legacy_bluetooth_proxy_version=0,
|
||||
mac_address="11:22:33:44:55:aa",
|
||||
esphome_version="1.0.0",
|
||||
)
|
||||
|
Loading…
x
Reference in New Issue
Block a user