Fix ESPHome bluetooth client cancel behavior when device unexpectedly disconnects (#96918)

This commit is contained in:
J. Nick Koston 2023-07-21 13:44:13 -05:00 committed by GitHub
parent a2b18e46b9
commit 7814ce06f4
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
4 changed files with 17 additions and 23 deletions

View File

@ -4,7 +4,6 @@ from __future__ import annotations
import asyncio import asyncio
from collections.abc import Callable, Coroutine from collections.abc import Callable, Coroutine
import contextlib import contextlib
from functools import partial
import logging import logging
from typing import Any, TypeVar, cast from typing import Any, TypeVar, cast
import uuid import uuid
@ -17,6 +16,7 @@ from aioesphomeapi import (
) )
from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError from aioesphomeapi.connection import APIConnectionError, TimeoutAPIError
from aioesphomeapi.core import BluetoothGATTAPIError from aioesphomeapi.core import BluetoothGATTAPIError
from async_interrupt import interrupt
import async_timeout import async_timeout
from bleak.backends.characteristic import BleakGATTCharacteristic from bleak.backends.characteristic import BleakGATTCharacteristic
from bleak.backends.client import BaseBleakClient, NotifyCallback from bleak.backends.client import BaseBleakClient, NotifyCallback
@ -57,11 +57,6 @@ def mac_to_int(address: str) -> int:
return int(address.replace(":", ""), 16) return int(address.replace(":", ""), 16)
def _on_disconnected(task: asyncio.Task[Any], _: asyncio.Future[None]) -> None:
if task and not task.done():
task.cancel()
def verify_connected(func: _WrapFuncType) -> _WrapFuncType: def verify_connected(func: _WrapFuncType) -> _WrapFuncType:
"""Define a wrapper throw BleakError if not connected.""" """Define a wrapper throw BleakError if not connected."""
@ -72,25 +67,17 @@ def verify_connected(func: _WrapFuncType) -> _WrapFuncType:
loop = self._loop loop = self._loop
disconnected_futures = self._disconnected_futures disconnected_futures = self._disconnected_futures
disconnected_future = loop.create_future() disconnected_future = loop.create_future()
disconnect_handler = partial(_on_disconnected, asyncio.current_task(loop))
disconnected_future.add_done_callback(disconnect_handler)
disconnected_futures.add(disconnected_future) disconnected_futures.add(disconnected_future)
ble_device = self._ble_device
disconnect_message = (
f"{self._source_name }: {ble_device.name} - {ble_device.address}: "
"Disconnected during operation"
)
try: try:
return await func(self, *args, **kwargs) async with interrupt(disconnected_future, BleakError, disconnect_message):
except asyncio.CancelledError as ex: return await func(self, *args, **kwargs)
if not disconnected_future.done():
# If the disconnected future is not done, the task was cancelled
# externally and we need to raise cancelled error to avoid
# blocking the cancellation.
raise
ble_device = self._ble_device
raise BleakError(
f"{self._source_name }: {ble_device.name} - {ble_device.address}: "
"Disconnected during operation"
) from ex
finally: finally:
disconnected_futures.discard(disconnected_future) disconnected_futures.discard(disconnected_future)
disconnected_future.remove_done_callback(disconnect_handler)
return cast(_WrapFuncType, _async_wrap_bluetooth_connected_operation) return cast(_WrapFuncType, _async_wrap_bluetooth_connected_operation)
@ -340,7 +327,7 @@ class ESPHomeClient(BaseBleakClient):
# exception. # exception.
await connected_future await connected_future
raise raise
except Exception: except Exception as ex:
if connected_future.done(): if connected_future.done():
with contextlib.suppress(BleakError): with contextlib.suppress(BleakError):
# If the connect call throws an exception, # If the connect call throws an exception,
@ -350,7 +337,7 @@ class ESPHomeClient(BaseBleakClient):
# exception from the connect call as it # exception from the connect call as it
# will be more descriptive. # will be more descriptive.
await connected_future await connected_future
connected_future.cancel() connected_future.cancel(f"Unhandled exception in connect call: {ex}")
raise raise
await connected_future await connected_future

View File

@ -15,6 +15,7 @@
"iot_class": "local_push", "iot_class": "local_push",
"loggers": ["aioesphomeapi", "noiseprotocol"], "loggers": ["aioesphomeapi", "noiseprotocol"],
"requirements": [ "requirements": [
"async_interrupt==1.1.1",
"aioesphomeapi==15.1.14", "aioesphomeapi==15.1.14",
"bluetooth-data-tools==1.6.0", "bluetooth-data-tools==1.6.0",
"esphome-dashboard-api==1.2.3" "esphome-dashboard-api==1.2.3"

View File

@ -442,6 +442,9 @@ asterisk-mbox==0.5.0
# homeassistant.components.yeelight # homeassistant.components.yeelight
async-upnp-client==0.33.2 async-upnp-client==0.33.2
# homeassistant.components.esphome
async_interrupt==1.1.1
# homeassistant.components.keyboard_remote # homeassistant.components.keyboard_remote
asyncinotify==4.0.2 asyncinotify==4.0.2

View File

@ -396,6 +396,9 @@ arcam-fmj==1.4.0
# homeassistant.components.yeelight # homeassistant.components.yeelight
async-upnp-client==0.33.2 async-upnp-client==0.33.2
# homeassistant.components.esphome
async_interrupt==1.1.1
# homeassistant.components.sleepiq # homeassistant.components.sleepiq
asyncsleepiq==1.3.5 asyncsleepiq==1.3.5