ESPHome BLE scanner support (#77123)
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>pull/77226/head
parent
c975146146
commit
7f001cc1d1
|
@ -317,6 +317,7 @@ omit =
|
|||
homeassistant/components/escea/__init__.py
|
||||
homeassistant/components/esphome/__init__.py
|
||||
homeassistant/components/esphome/binary_sensor.py
|
||||
homeassistant/components/esphome/bluetooth.py
|
||||
homeassistant/components/esphome/button.py
|
||||
homeassistant/components/esphome/camera.py
|
||||
homeassistant/components/esphome/climate.py
|
||||
|
|
|
@ -52,6 +52,8 @@ from homeassistant.helpers.service import async_set_service_schema
|
|||
from homeassistant.helpers.storage import Store
|
||||
from homeassistant.helpers.template import Template
|
||||
|
||||
from .bluetooth import async_connect_scanner
|
||||
|
||||
# Import config flow so that it's added to the registry
|
||||
from .entry_data import RuntimeEntryData
|
||||
|
||||
|
@ -286,6 +288,8 @@ async def async_setup_entry( # noqa: C901
|
|||
await cli.subscribe_states(entry_data.async_update_state)
|
||||
await cli.subscribe_service_calls(async_on_service_call)
|
||||
await cli.subscribe_home_assistant_states(async_on_state_subscription)
|
||||
if entry_data.device_info.has_bluetooth_proxy:
|
||||
await async_connect_scanner(hass, entry, cli)
|
||||
|
||||
hass.async_create_task(entry_data.async_save_to_store())
|
||||
except APIConnectionError as err:
|
||||
|
|
|
@ -0,0 +1,113 @@
|
|||
"""Bluetooth scanner for esphome."""
|
||||
|
||||
from collections.abc import Callable
|
||||
import datetime
|
||||
from datetime import timedelta
|
||||
import re
|
||||
import time
|
||||
|
||||
from aioesphomeapi import APIClient, BluetoothLEAdvertisement
|
||||
from bleak.backends.device import BLEDevice
|
||||
from bleak.backends.scanner import AdvertisementData
|
||||
|
||||
from homeassistant.components.bluetooth import (
|
||||
BaseHaScanner,
|
||||
async_get_advertisement_callback,
|
||||
async_register_scanner,
|
||||
)
|
||||
from homeassistant.components.bluetooth.models import BluetoothServiceInfoBleak
|
||||
from homeassistant.config_entries import ConfigEntry
|
||||
from homeassistant.core import CALLBACK_TYPE, HomeAssistant, callback
|
||||
from homeassistant.helpers.event import async_track_time_interval
|
||||
|
||||
ADV_STALE_TIME = 180 # seconds
|
||||
|
||||
TWO_CHAR = re.compile("..")
|
||||
|
||||
|
||||
async def async_connect_scanner(
|
||||
hass: HomeAssistant, entry: ConfigEntry, cli: APIClient
|
||||
) -> None:
|
||||
"""Connect scanner."""
|
||||
assert entry.unique_id is not None
|
||||
new_info_callback = async_get_advertisement_callback(hass)
|
||||
scanner = ESPHomeScannner(hass, entry.unique_id, new_info_callback)
|
||||
entry.async_on_unload(async_register_scanner(hass, scanner, False))
|
||||
entry.async_on_unload(scanner.async_setup())
|
||||
await cli.subscribe_bluetooth_le_advertisements(scanner.async_on_advertisement)
|
||||
|
||||
|
||||
class ESPHomeScannner(BaseHaScanner):
|
||||
"""Scanner for esphome."""
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
hass: HomeAssistant,
|
||||
scanner_id: str,
|
||||
new_info_callback: Callable[[BluetoothServiceInfoBleak], None],
|
||||
) -> None:
|
||||
"""Initialize the scanner."""
|
||||
self._hass = hass
|
||||
self._new_info_callback = new_info_callback
|
||||
self._discovered_devices: dict[str, BLEDevice] = {}
|
||||
self._discovered_device_timestamps: dict[str, float] = {}
|
||||
self._source = scanner_id
|
||||
|
||||
@callback
|
||||
def async_setup(self) -> CALLBACK_TYPE:
|
||||
"""Set up the scanner."""
|
||||
return async_track_time_interval(
|
||||
self._hass, self._async_expire_devices, timedelta(seconds=30)
|
||||
)
|
||||
|
||||
def _async_expire_devices(self, _datetime: datetime.datetime) -> None:
|
||||
"""Expire old devices."""
|
||||
now = time.monotonic()
|
||||
expired = [
|
||||
address
|
||||
for address, timestamp in self._discovered_device_timestamps.items()
|
||||
if now - timestamp > ADV_STALE_TIME
|
||||
]
|
||||
for address in expired:
|
||||
del self._discovered_devices[address]
|
||||
del self._discovered_device_timestamps[address]
|
||||
|
||||
@property
|
||||
def discovered_devices(self) -> list[BLEDevice]:
|
||||
"""Return a list of discovered devices."""
|
||||
return list(self._discovered_devices.values())
|
||||
|
||||
@callback
|
||||
def async_on_advertisement(self, adv: BluetoothLEAdvertisement) -> None:
|
||||
"""Call the registered callback."""
|
||||
now = time.monotonic()
|
||||
address = ":".join(TWO_CHAR.findall("%012X" % adv.address)) # must be upper
|
||||
advertisement_data = AdvertisementData( # type: ignore[no-untyped-call]
|
||||
local_name=None if adv.name == "" else adv.name,
|
||||
manufacturer_data=adv.manufacturer_data,
|
||||
service_data=adv.service_data,
|
||||
service_uuids=adv.service_uuids,
|
||||
)
|
||||
device = BLEDevice( # type: ignore[no-untyped-call]
|
||||
address=address,
|
||||
name=adv.name,
|
||||
details={},
|
||||
rssi=adv.rssi,
|
||||
)
|
||||
self._discovered_devices[address] = device
|
||||
self._discovered_device_timestamps[address] = now
|
||||
self._new_info_callback(
|
||||
BluetoothServiceInfoBleak(
|
||||
name=advertisement_data.local_name or device.name or device.address,
|
||||
address=device.address,
|
||||
rssi=device.rssi,
|
||||
manufacturer_data=advertisement_data.manufacturer_data,
|
||||
service_data=advertisement_data.service_data,
|
||||
service_uuids=advertisement_data.service_uuids,
|
||||
source=self._source,
|
||||
device=device,
|
||||
advertisement=advertisement_data,
|
||||
connectable=False,
|
||||
time=now,
|
||||
)
|
||||
)
|
|
@ -3,11 +3,11 @@
|
|||
"name": "ESPHome",
|
||||
"config_flow": true,
|
||||
"documentation": "https://www.home-assistant.io/integrations/esphome",
|
||||
"requirements": ["aioesphomeapi==10.11.0"],
|
||||
"requirements": ["aioesphomeapi==10.13.0"],
|
||||
"zeroconf": ["_esphomelib._tcp.local."],
|
||||
"dhcp": [{ "registered_devices": true }],
|
||||
"codeowners": ["@OttoWinter", "@jesserockz"],
|
||||
"after_dependencies": ["zeroconf", "tag"],
|
||||
"after_dependencies": ["bluetooth", "zeroconf", "tag"],
|
||||
"iot_class": "local_push",
|
||||
"loggers": ["aioesphomeapi", "noiseprotocol"]
|
||||
}
|
||||
|
|
|
@ -150,7 +150,7 @@ aioeagle==1.1.0
|
|||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==10.11.0
|
||||
aioesphomeapi==10.13.0
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
|
|
|
@ -137,7 +137,7 @@ aioeagle==1.1.0
|
|||
aioemonitor==1.0.5
|
||||
|
||||
# homeassistant.components.esphome
|
||||
aioesphomeapi==10.11.0
|
||||
aioesphomeapi==10.13.0
|
||||
|
||||
# homeassistant.components.flo
|
||||
aioflo==2021.11.0
|
||||
|
|
Loading…
Reference in New Issue