mirror of
https://github.com/home-assistant/core.git
synced 2025-07-24 21:57:51 +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
|
@callback
|
||||||
def _async_check_firmware_version(
|
def _async_check_firmware_version(
|
||||||
hass: HomeAssistant, device_info: EsphomeDeviceInfo
|
hass: HomeAssistant, device_info: EsphomeDeviceInfo, api_version: APIVersion
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Create or delete an the ble_firmware_outdated issue."""
|
"""Create or delete an the ble_firmware_outdated issue."""
|
||||||
# ESPHome device_info.mac_address is the unique_id
|
# ESPHome device_info.mac_address is the unique_id
|
||||||
issue = f"ble_firmware_outdated-{device_info.mac_address}"
|
issue = f"ble_firmware_outdated-{device_info.mac_address}"
|
||||||
if (
|
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
|
# 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
|
# 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)
|
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:
|
if entry_data.device_info.name:
|
||||||
reconnect_logic.name = 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(
|
entry_data.disconnect_callbacks.append(
|
||||||
await async_connect_scanner(hass, entry, cli, entry_data)
|
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
|
# Re-connection logic will trigger after this
|
||||||
await cli.disconnect()
|
await cli.disconnect()
|
||||||
else:
|
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_check_using_api_password(hass, device_info, bool(password))
|
||||||
|
|
||||||
async def on_disconnect() -> None:
|
async def on_disconnect() -> None:
|
||||||
|
@ -5,7 +5,7 @@ from collections.abc import Callable
|
|||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
|
|
||||||
from aioesphomeapi import APIClient
|
from aioesphomeapi import APIClient, BluetoothProxyFeature
|
||||||
|
|
||||||
from homeassistant.components.bluetooth import (
|
from homeassistant.components.bluetooth import (
|
||||||
HaBluetoothConnector,
|
HaBluetoothConnector,
|
||||||
@ -59,13 +59,15 @@ async def async_connect_scanner(
|
|||||||
source = str(entry.unique_id)
|
source = str(entry.unique_id)
|
||||||
new_info_callback = async_get_advertisement_callback(hass)
|
new_info_callback = async_get_advertisement_callback(hass)
|
||||||
assert entry_data.device_info is not None
|
assert entry_data.device_info is not None
|
||||||
version = entry_data.device_info.bluetooth_proxy_version
|
feature_flags = entry_data.device_info.bluetooth_proxy_feature_flags_compat(
|
||||||
connectable = version >= 2
|
entry_data.api_version
|
||||||
|
)
|
||||||
|
connectable = bool(feature_flags & BluetoothProxyFeature.ACTIVE_CONNECTIONS)
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
"%s [%s]: Connecting scanner version=%s, connectable=%s",
|
"%s [%s]: Connecting scanner feature_flags=%s, connectable=%s",
|
||||||
entry.title,
|
entry.title,
|
||||||
source,
|
source,
|
||||||
version,
|
feature_flags,
|
||||||
connectable,
|
connectable,
|
||||||
)
|
)
|
||||||
connector = HaBluetoothConnector(
|
connector = HaBluetoothConnector(
|
||||||
@ -89,7 +91,12 @@ async def async_connect_scanner(
|
|||||||
async_register_scanner(hass, scanner, connectable),
|
async_register_scanner(hass, scanner, connectable),
|
||||||
scanner.async_setup(),
|
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
|
@hass_callback
|
||||||
def _async_unload() -> None:
|
def _async_unload() -> None:
|
||||||
|
@ -12,6 +12,7 @@ from aioesphomeapi import (
|
|||||||
ESP_CONNECTION_ERROR_DESCRIPTION,
|
ESP_CONNECTION_ERROR_DESCRIPTION,
|
||||||
ESPHOME_GATT_ERRORS,
|
ESPHOME_GATT_ERRORS,
|
||||||
BLEConnectionError,
|
BLEConnectionError,
|
||||||
|
BluetoothProxyFeature,
|
||||||
)
|
)
|
||||||
from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError
|
from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError
|
||||||
from aioesphomeapi.core import BluetoothGATTAPIError
|
from aioesphomeapi.core import BluetoothGATTAPIError
|
||||||
@ -42,10 +43,6 @@ CCCD_UUID = "00002902-0000-1000-8000-00805f9b34fb"
|
|||||||
CCCD_NOTIFY_BYTES = b"\x01\x00"
|
CCCD_NOTIFY_BYTES = b"\x01\x00"
|
||||||
CCCD_INDICATE_BYTES = b"\x02\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
|
DEFAULT_MAX_WRITE_WITHOUT_RESPONSE = DEFAULT_MTU - GATT_HEADER_SIZE
|
||||||
_LOGGER = logging.getLogger(__name__)
|
_LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@ -158,7 +155,10 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
self._disconnected_event: asyncio.Event | None = None
|
self._disconnected_event: asyncio.Event | None = None
|
||||||
device_info = self.entry_data.device_info
|
device_info = self.entry_data.device_info
|
||||||
assert device_info is not None
|
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._address_type = address_or_ble_device.details["address_type"]
|
||||||
self._source_name = f"{config_entry.title} [{self._source}]"
|
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)
|
self._mtu = domain_data.get_gatt_mtu_cache(self._address_as_int)
|
||||||
has_cache = bool(
|
has_cache = bool(
|
||||||
dangerous_use_bleak_cache
|
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 domain_data.get_gatt_services_cache(self._address_as_int)
|
||||||
and self._mtu
|
and self._mtu
|
||||||
)
|
)
|
||||||
@ -319,7 +319,7 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
_on_bluetooth_connection_state,
|
_on_bluetooth_connection_state,
|
||||||
timeout=timeout,
|
timeout=timeout,
|
||||||
has_cache=has_cache,
|
has_cache=has_cache,
|
||||||
version=self._connection_version,
|
feature_flags=self._feature_flags,
|
||||||
address_type=self._address_type,
|
address_type=self._address_type,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -397,9 +397,10 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
@api_error_as_bleak_error
|
@api_error_as_bleak_error
|
||||||
async def pair(self, *args: Any, **kwargs: Any) -> bool:
|
async def pair(self, *args: Any, **kwargs: Any) -> bool:
|
||||||
"""Attempt to pair."""
|
"""Attempt to pair."""
|
||||||
if self._connection_version < MIN_BLUETOOTH_PROXY_HAS_PAIRING:
|
if not self._feature_flags & BluetoothProxyFeature.PAIRING:
|
||||||
raise NotImplementedError(
|
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)
|
response = await self._client.bluetooth_device_pair(self._address_as_int)
|
||||||
if response.paired:
|
if response.paired:
|
||||||
@ -413,9 +414,10 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
@api_error_as_bleak_error
|
@api_error_as_bleak_error
|
||||||
async def unpair(self) -> bool:
|
async def unpair(self) -> bool:
|
||||||
"""Attempt to unpair."""
|
"""Attempt to unpair."""
|
||||||
if self._connection_version < MIN_BLUETOOTH_PROXY_HAS_PAIRING:
|
if not self._feature_flags & BluetoothProxyFeature.PAIRING:
|
||||||
raise NotImplementedError(
|
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)
|
response = await self._client.bluetooth_device_unpair(self._address_as_int)
|
||||||
if response.success:
|
if response.success:
|
||||||
@ -441,7 +443,7 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
# because the esp has already wiped the services list to
|
# because the esp has already wiped the services list to
|
||||||
# save memory.
|
# save memory.
|
||||||
if (
|
if (
|
||||||
self._connection_version >= MIN_BLUETOOTH_PROXY_VERSION_HAS_CACHE
|
self._feature_flags & BluetoothProxyFeature.REMOTE_CACHING
|
||||||
or dangerous_use_bleak_cache
|
or dangerous_use_bleak_cache
|
||||||
) and (cached_services := domain_data.get_gatt_services_cache(address_as_int)):
|
) and (cached_services := domain_data.get_gatt_services_cache(address_as_int)):
|
||||||
_LOGGER.debug(
|
_LOGGER.debug(
|
||||||
@ -524,12 +526,11 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
"""Clear the GATT cache."""
|
"""Clear the GATT cache."""
|
||||||
self.domain_data.clear_gatt_services_cache(self._address_as_int)
|
self.domain_data.clear_gatt_services_cache(self._address_as_int)
|
||||||
self.domain_data.clear_gatt_mtu_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(
|
_LOGGER.warning(
|
||||||
"On device cache clear is not available with ESPHome Bluetooth version %s, "
|
"On device cache clear is not available with this ESPHome version; "
|
||||||
"version %s is needed; Only memory cache will be cleared",
|
"Upgrade the ESPHome version on the device %s; Only memory cache will be cleared",
|
||||||
self._connection_version,
|
self._device_info.name,
|
||||||
MIN_BLUETOOTH_PROXY_HAS_CLEAR_CACHE,
|
|
||||||
)
|
)
|
||||||
return True
|
return True
|
||||||
response = await self._client.bluetooth_device_clear_cache(self._address_as_int)
|
response = await self._client.bluetooth_device_clear_cache(self._address_as_int)
|
||||||
@ -673,7 +674,7 @@ class ESPHomeClient(BaseBleakClient):
|
|||||||
lambda handle, data: callback(data),
|
lambda handle, data: callback(data),
|
||||||
)
|
)
|
||||||
|
|
||||||
if self._connection_version < MIN_BLUETOOTH_PROXY_VERSION_HAS_CACHE:
|
if not self._feature_flags & BluetoothProxyFeature.REMOTE_CACHING:
|
||||||
return
|
return
|
||||||
|
|
||||||
# For connection v3 we are responsible for enabling notifications
|
# For connection v3 we are responsible for enabling notifications
|
||||||
|
@ -1,8 +1,8 @@
|
|||||||
"""Bluetooth scanner for esphome."""
|
"""Bluetooth scanner for esphome."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from aioesphomeapi import BluetoothLEAdvertisement
|
from aioesphomeapi import BluetoothLEAdvertisement, BluetoothLERawAdvertisement
|
||||||
from bluetooth_data_tools import int_to_bluetooth_address
|
from bluetooth_data_tools import int_to_bluetooth_address, parse_advertisement_data
|
||||||
|
|
||||||
from homeassistant.components.bluetooth import BaseHaRemoteScanner
|
from homeassistant.components.bluetooth import BaseHaRemoteScanner
|
||||||
from homeassistant.core import callback
|
from homeassistant.core import callback
|
||||||
@ -25,3 +25,21 @@ class ESPHomeScanner(BaseHaRemoteScanner):
|
|||||||
None,
|
None,
|
||||||
{"address_type": adv.address_type},
|
{"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",
|
"iot_class": "local_push",
|
||||||
"loggers": ["aioesphomeapi", "noiseprotocol"],
|
"loggers": ["aioesphomeapi", "noiseprotocol"],
|
||||||
"requirements": [
|
"requirements": [
|
||||||
"aioesphomeapi==13.9.0",
|
"aioesphomeapi==14.0.0",
|
||||||
"bluetooth-data-tools==0.4.0",
|
"bluetooth-data-tools==0.4.0",
|
||||||
"esphome-dashboard-api==1.2.3"
|
"esphome-dashboard-api==1.2.3"
|
||||||
],
|
],
|
||||||
|
@ -234,7 +234,7 @@ aioecowitt==2023.5.0
|
|||||||
aioemonitor==1.0.5
|
aioemonitor==1.0.5
|
||||||
|
|
||||||
# homeassistant.components.esphome
|
# homeassistant.components.esphome
|
||||||
aioesphomeapi==13.9.0
|
aioesphomeapi==14.0.0
|
||||||
|
|
||||||
# homeassistant.components.flo
|
# homeassistant.components.flo
|
||||||
aioflo==2021.11.0
|
aioflo==2021.11.0
|
||||||
|
@ -212,7 +212,7 @@ aioecowitt==2023.5.0
|
|||||||
aioemonitor==1.0.5
|
aioemonitor==1.0.5
|
||||||
|
|
||||||
# homeassistant.components.esphome
|
# homeassistant.components.esphome
|
||||||
aioesphomeapi==13.9.0
|
aioesphomeapi==14.0.0
|
||||||
|
|
||||||
# homeassistant.components.flo
|
# homeassistant.components.flo
|
||||||
aioflo==2021.11.0
|
aioflo==2021.11.0
|
||||||
|
@ -63,7 +63,7 @@ def mock_device_info() -> DeviceInfo:
|
|||||||
return DeviceInfo(
|
return DeviceInfo(
|
||||||
uses_password=False,
|
uses_password=False,
|
||||||
name="test",
|
name="test",
|
||||||
bluetooth_proxy_version=0,
|
legacy_bluetooth_proxy_version=0,
|
||||||
mac_address="11:22:33:44:55:aa",
|
mac_address="11:22:33:44:55:aa",
|
||||||
esphome_version="1.0.0",
|
esphome_version="1.0.0",
|
||||||
)
|
)
|
||||||
|
Loading…
x
Reference in New Issue
Block a user