Speed up ESPHome connection setup (#104304)
This commit is contained in:
parent
a3c0f36592
commit
a59076d140
2 changed files with 86 additions and 40 deletions
|
@ -1,8 +1,11 @@
|
||||||
"""Bluetooth support for esphome."""
|
"""Bluetooth support for esphome."""
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import asyncio
|
||||||
|
from collections.abc import Coroutine
|
||||||
from functools import partial
|
from functools import partial
|
||||||
import logging
|
import logging
|
||||||
|
from typing import Any
|
||||||
|
|
||||||
from aioesphomeapi import APIClient, BluetoothProxyFeature
|
from aioesphomeapi import APIClient, BluetoothProxyFeature
|
||||||
|
|
||||||
|
@ -43,6 +46,13 @@ def _async_can_connect(
|
||||||
return can_connect
|
return can_connect
|
||||||
|
|
||||||
|
|
||||||
|
@hass_callback
|
||||||
|
def _async_unload(unload_callbacks: list[CALLBACK_TYPE]) -> None:
|
||||||
|
"""Cancel all the callbacks on unload."""
|
||||||
|
for callback in unload_callbacks:
|
||||||
|
callback()
|
||||||
|
|
||||||
|
|
||||||
async def async_connect_scanner(
|
async def async_connect_scanner(
|
||||||
hass: HomeAssistant,
|
hass: HomeAssistant,
|
||||||
entry: ConfigEntry,
|
entry: ConfigEntry,
|
||||||
|
@ -92,27 +102,36 @@ async def async_connect_scanner(
|
||||||
hass, source, entry.title, new_info_callback, connector, connectable
|
hass, source, entry.title, new_info_callback, connector, connectable
|
||||||
)
|
)
|
||||||
client_data.scanner = scanner
|
client_data.scanner = scanner
|
||||||
|
coros: list[Coroutine[Any, Any, CALLBACK_TYPE]] = []
|
||||||
|
# These calls all return a callback that can be used to unsubscribe
|
||||||
|
# but we never unsubscribe so we don't care about the return value
|
||||||
|
|
||||||
if connectable:
|
if connectable:
|
||||||
# If its connectable be sure not to register the scanner
|
# If its connectable be sure not to register the scanner
|
||||||
# until we know the connection is fully setup since otherwise
|
# until we know the connection is fully setup since otherwise
|
||||||
# there is a race condition where the connection can fail
|
# there is a race condition where the connection can fail
|
||||||
await cli.subscribe_bluetooth_connections_free(
|
coros.append(
|
||||||
bluetooth_device.async_update_ble_connection_limits
|
cli.subscribe_bluetooth_connections_free(
|
||||||
|
bluetooth_device.async_update_ble_connection_limits
|
||||||
|
)
|
||||||
)
|
)
|
||||||
unload_callbacks = [
|
|
||||||
async_register_scanner(hass, scanner, connectable),
|
|
||||||
scanner.async_setup(),
|
|
||||||
]
|
|
||||||
if feature_flags & BluetoothProxyFeature.RAW_ADVERTISEMENTS:
|
if feature_flags & BluetoothProxyFeature.RAW_ADVERTISEMENTS:
|
||||||
await cli.subscribe_bluetooth_le_raw_advertisements(
|
coros.append(
|
||||||
scanner.async_on_raw_advertisements
|
cli.subscribe_bluetooth_le_raw_advertisements(
|
||||||
|
scanner.async_on_raw_advertisements
|
||||||
|
)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
await cli.subscribe_bluetooth_le_advertisements(scanner.async_on_advertisement)
|
coros.append(
|
||||||
|
cli.subscribe_bluetooth_le_advertisements(scanner.async_on_advertisement)
|
||||||
|
)
|
||||||
|
|
||||||
@hass_callback
|
await asyncio.gather(*coros)
|
||||||
def _async_unload() -> None:
|
return partial(
|
||||||
for callback in unload_callbacks:
|
_async_unload,
|
||||||
callback()
|
[
|
||||||
|
async_register_scanner(hass, scanner, connectable),
|
||||||
return _async_unload
|
scanner.async_setup(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import asyncio
|
import asyncio
|
||||||
|
from collections.abc import Coroutine
|
||||||
import logging
|
import logging
|
||||||
from typing import TYPE_CHECKING, Any, NamedTuple
|
from typing import TYPE_CHECKING, Any, NamedTuple
|
||||||
|
|
||||||
|
@ -10,6 +11,7 @@ from aioesphomeapi import (
|
||||||
APIConnectionError,
|
APIConnectionError,
|
||||||
APIVersion,
|
APIVersion,
|
||||||
DeviceInfo as EsphomeDeviceInfo,
|
DeviceInfo as EsphomeDeviceInfo,
|
||||||
|
EntityInfo,
|
||||||
HomeassistantServiceCall,
|
HomeassistantServiceCall,
|
||||||
InvalidAuthAPIError,
|
InvalidAuthAPIError,
|
||||||
InvalidEncryptionKeyAPIError,
|
InvalidEncryptionKeyAPIError,
|
||||||
|
@ -26,7 +28,14 @@ import voluptuous as vol
|
||||||
from homeassistant.components import tag, zeroconf
|
from homeassistant.components import tag, zeroconf
|
||||||
from homeassistant.config_entries import ConfigEntry
|
from homeassistant.config_entries import ConfigEntry
|
||||||
from homeassistant.const import ATTR_DEVICE_ID, CONF_MODE, EVENT_HOMEASSISTANT_STOP
|
from homeassistant.const import ATTR_DEVICE_ID, CONF_MODE, EVENT_HOMEASSISTANT_STOP
|
||||||
from homeassistant.core import Event, HomeAssistant, ServiceCall, State, callback
|
from homeassistant.core import (
|
||||||
|
CALLBACK_TYPE,
|
||||||
|
Event,
|
||||||
|
HomeAssistant,
|
||||||
|
ServiceCall,
|
||||||
|
State,
|
||||||
|
callback,
|
||||||
|
)
|
||||||
from homeassistant.exceptions import TemplateError
|
from homeassistant.exceptions import TemplateError
|
||||||
from homeassistant.helpers import template
|
from homeassistant.helpers import template
|
||||||
import homeassistant.helpers.config_validation as cv
|
import homeassistant.helpers.config_validation as cv
|
||||||
|
@ -372,13 +381,20 @@ class ESPHomeManager:
|
||||||
stored_device_name = entry.data.get(CONF_DEVICE_NAME)
|
stored_device_name = entry.data.get(CONF_DEVICE_NAME)
|
||||||
unique_id_is_mac_address = unique_id and ":" in unique_id
|
unique_id_is_mac_address = unique_id and ":" in unique_id
|
||||||
try:
|
try:
|
||||||
device_info = await cli.device_info()
|
results = await asyncio.gather(
|
||||||
|
cli.device_info(),
|
||||||
|
cli.list_entities_services(),
|
||||||
|
)
|
||||||
except APIConnectionError as err:
|
except APIConnectionError as err:
|
||||||
_LOGGER.warning("Error getting device info for %s: %s", self.host, err)
|
_LOGGER.warning("Error getting device info for %s: %s", self.host, err)
|
||||||
# Re-connection logic will trigger after this
|
# Re-connection logic will trigger after this
|
||||||
await cli.disconnect()
|
await cli.disconnect()
|
||||||
return
|
return
|
||||||
|
|
||||||
|
device_info: EsphomeDeviceInfo = results[0]
|
||||||
|
entity_infos_services: tuple[list[EntityInfo], list[UserService]] = results[1]
|
||||||
|
entity_infos, services = entity_infos_services
|
||||||
|
|
||||||
device_mac = format_mac(device_info.mac_address)
|
device_mac = format_mac(device_info.mac_address)
|
||||||
mac_address_matches = unique_id == device_mac
|
mac_address_matches = unique_id == device_mac
|
||||||
#
|
#
|
||||||
|
@ -439,44 +455,55 @@ class ESPHomeManager:
|
||||||
if device_info.name:
|
if device_info.name:
|
||||||
reconnect_logic.name = device_info.name
|
reconnect_logic.name = device_info.name
|
||||||
|
|
||||||
|
self.device_id = _async_setup_device_registry(hass, entry, entry_data)
|
||||||
|
entry_data.async_update_device_state(hass)
|
||||||
|
await asyncio.gather(
|
||||||
|
entry_data.async_update_static_infos(
|
||||||
|
hass, entry, entity_infos, device_info.mac_address
|
||||||
|
),
|
||||||
|
_setup_services(hass, entry_data, services),
|
||||||
|
)
|
||||||
|
|
||||||
|
setup_coros_with_disconnect_callbacks: list[
|
||||||
|
Coroutine[Any, Any, CALLBACK_TYPE]
|
||||||
|
] = []
|
||||||
if device_info.bluetooth_proxy_feature_flags_compat(cli.api_version):
|
if device_info.bluetooth_proxy_feature_flags_compat(cli.api_version):
|
||||||
entry_data.disconnect_callbacks.add(
|
setup_coros_with_disconnect_callbacks.append(
|
||||||
await async_connect_scanner(
|
async_connect_scanner(
|
||||||
hass, entry, cli, entry_data, self.domain_data.bluetooth_cache
|
hass, entry, cli, entry_data, self.domain_data.bluetooth_cache
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.device_id = _async_setup_device_registry(hass, entry, entry_data)
|
if device_info.voice_assistant_version:
|
||||||
entry_data.async_update_device_state(hass)
|
setup_coros_with_disconnect_callbacks.append(
|
||||||
|
cli.subscribe_voice_assistant(
|
||||||
|
self._handle_pipeline_start,
|
||||||
|
self._handle_pipeline_stop,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
entity_infos, services = await cli.list_entities_services()
|
setup_results = await asyncio.gather(
|
||||||
await entry_data.async_update_static_infos(
|
*setup_coros_with_disconnect_callbacks,
|
||||||
hass, entry, entity_infos, device_info.mac_address
|
|
||||||
)
|
|
||||||
await _setup_services(hass, entry_data, services)
|
|
||||||
await asyncio.gather(
|
|
||||||
cli.subscribe_states(entry_data.async_update_state),
|
cli.subscribe_states(entry_data.async_update_state),
|
||||||
cli.subscribe_service_calls(self.async_on_service_call),
|
cli.subscribe_service_calls(self.async_on_service_call),
|
||||||
cli.subscribe_home_assistant_states(self.async_on_state_subscription),
|
cli.subscribe_home_assistant_states(self.async_on_state_subscription),
|
||||||
)
|
)
|
||||||
|
|
||||||
if device_info.voice_assistant_version:
|
|
||||||
entry_data.disconnect_callbacks.add(
|
|
||||||
await cli.subscribe_voice_assistant(
|
|
||||||
self._handle_pipeline_start,
|
|
||||||
self._handle_pipeline_stop,
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
hass.async_create_task(entry_data.async_save_to_store())
|
|
||||||
except APIConnectionError as err:
|
except APIConnectionError as err:
|
||||||
_LOGGER.warning("Error getting initial data for %s: %s", self.host, err)
|
_LOGGER.warning("Error getting initial data for %s: %s", self.host, err)
|
||||||
# Re-connection logic will trigger after this
|
# Re-connection logic will trigger after this
|
||||||
await cli.disconnect()
|
await cli.disconnect()
|
||||||
else:
|
return
|
||||||
_async_check_firmware_version(hass, device_info, entry_data.api_version)
|
|
||||||
_async_check_using_api_password(hass, device_info, bool(self.password))
|
for result_idx in range(len(setup_coros_with_disconnect_callbacks)):
|
||||||
|
cancel_callback = setup_results[result_idx]
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
assert cancel_callback is not None
|
||||||
|
entry_data.disconnect_callbacks.add(cancel_callback)
|
||||||
|
|
||||||
|
hass.async_create_task(entry_data.async_save_to_store())
|
||||||
|
_async_check_firmware_version(hass, device_info, entry_data.api_version)
|
||||||
|
_async_check_using_api_password(hass, device_info, bool(self.password))
|
||||||
|
|
||||||
async def on_disconnect(self, expected_disconnect: bool) -> None:
|
async def on_disconnect(self, expected_disconnect: bool) -> None:
|
||||||
"""Run disconnect callbacks on API disconnect."""
|
"""Run disconnect callbacks on API disconnect."""
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue