mirror of
https://github.com/esphome/aioesphomeapi.git
synced 2024-09-27 04:22:46 +02:00
Handle Bluetooth connection drops in more places
This commit is contained in:
parent
72a8f70bcd
commit
23c3959dd2
@ -86,6 +86,7 @@ from .core import (
|
|||||||
BluetoothGATTAPIError,
|
BluetoothGATTAPIError,
|
||||||
TimeoutAPIError,
|
TimeoutAPIError,
|
||||||
to_human_readable_address,
|
to_human_readable_address,
|
||||||
|
to_human_readable_gatt_error,
|
||||||
)
|
)
|
||||||
from .model import (
|
from .model import (
|
||||||
AlarmControlPanelCommand,
|
AlarmControlPanelCommand,
|
||||||
@ -155,6 +156,10 @@ ExecuteServiceDataType = dict[
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
class BluetoothConnectionDroppedError(APIConnectionError):
|
||||||
|
"""Raised when a Bluetooth connection is dropped."""
|
||||||
|
|
||||||
|
|
||||||
def _stringify_or_none(value: str | None) -> str | None:
|
def _stringify_or_none(value: str | None) -> str | None:
|
||||||
"""Convert a string like object to a str or None.
|
"""Convert a string like object to a str or None.
|
||||||
|
|
||||||
@ -632,26 +637,11 @@ class APIClient:
|
|||||||
async def bluetooth_device_pair(
|
async def bluetooth_device_pair(
|
||||||
self, address: int, timeout: float = DEFAULT_BLE_TIMEOUT
|
self, address: int, timeout: float = DEFAULT_BLE_TIMEOUT
|
||||||
) -> BluetoothDevicePairing:
|
) -> BluetoothDevicePairing:
|
||||||
def predicate_func(
|
|
||||||
msg: BluetoothDevicePairingResponse | BluetoothDeviceConnectionResponse,
|
|
||||||
) -> bool:
|
|
||||||
if msg.address != address:
|
|
||||||
return False
|
|
||||||
if isinstance(msg, BluetoothDeviceConnectionResponse):
|
|
||||||
raise APIConnectionError(
|
|
||||||
f"Peripheral changed connections status while pairing: {msg.error}"
|
|
||||||
)
|
|
||||||
return True
|
|
||||||
|
|
||||||
return BluetoothDevicePairing.from_pb(
|
return BluetoothDevicePairing.from_pb(
|
||||||
await self._bluetooth_device_request(
|
await self._bluetooth_device_request_watch_connection(
|
||||||
address,
|
address,
|
||||||
BluetoothDeviceRequestType.PAIR,
|
BluetoothDeviceRequestType.PAIR,
|
||||||
predicate_func,
|
(BluetoothDevicePairingResponse,),
|
||||||
(
|
|
||||||
BluetoothDevicePairingResponse,
|
|
||||||
BluetoothDeviceConnectionResponse,
|
|
||||||
),
|
|
||||||
timeout,
|
timeout,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -660,10 +650,9 @@ class APIClient:
|
|||||||
self, address: int, timeout: float = DEFAULT_BLE_TIMEOUT
|
self, address: int, timeout: float = DEFAULT_BLE_TIMEOUT
|
||||||
) -> BluetoothDeviceUnpairing:
|
) -> BluetoothDeviceUnpairing:
|
||||||
return BluetoothDeviceUnpairing.from_pb(
|
return BluetoothDeviceUnpairing.from_pb(
|
||||||
await self._bluetooth_device_request(
|
await self._bluetooth_device_request_watch_connection(
|
||||||
address,
|
address,
|
||||||
BluetoothDeviceRequestType.UNPAIR,
|
BluetoothDeviceRequestType.UNPAIR,
|
||||||
lambda msg: msg.address == address,
|
|
||||||
(BluetoothDeviceUnpairingResponse,),
|
(BluetoothDeviceUnpairingResponse,),
|
||||||
timeout,
|
timeout,
|
||||||
)
|
)
|
||||||
@ -673,10 +662,9 @@ class APIClient:
|
|||||||
self, address: int, timeout: float = DEFAULT_BLE_TIMEOUT
|
self, address: int, timeout: float = DEFAULT_BLE_TIMEOUT
|
||||||
) -> BluetoothDeviceClearCache:
|
) -> BluetoothDeviceClearCache:
|
||||||
return BluetoothDeviceClearCache.from_pb(
|
return BluetoothDeviceClearCache.from_pb(
|
||||||
await self._bluetooth_device_request(
|
await self._bluetooth_device_request_watch_connection(
|
||||||
address,
|
address,
|
||||||
BluetoothDeviceRequestType.CLEAR_CACHE,
|
BluetoothDeviceRequestType.CLEAR_CACHE,
|
||||||
lambda msg: msg.address == address,
|
|
||||||
(BluetoothDeviceClearCacheResponse,),
|
(BluetoothDeviceClearCacheResponse,),
|
||||||
timeout,
|
timeout,
|
||||||
)
|
)
|
||||||
@ -694,6 +682,30 @@ class APIClient:
|
|||||||
timeout,
|
timeout,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _bluetooth_device_request_watch_connection(
|
||||||
|
self,
|
||||||
|
address: int,
|
||||||
|
request_type: BluetoothDeviceRequestType,
|
||||||
|
msg_types: tuple[type[message.Message], ...],
|
||||||
|
timeout: float,
|
||||||
|
) -> message.Message:
|
||||||
|
"""Send a BluetoothDeviceRequest watch for the connection state to change."""
|
||||||
|
response = await self._bluetooth_device_request(
|
||||||
|
address,
|
||||||
|
request_type,
|
||||||
|
lambda msg: msg.address == address,
|
||||||
|
(BluetoothDeviceConnectionResponse, *msg_types),
|
||||||
|
timeout,
|
||||||
|
)
|
||||||
|
if type(response) is BluetoothDeviceConnectionResponse:
|
||||||
|
response_names = ", ".join(t.__name__ for t in msg_types)
|
||||||
|
raise BluetoothConnectionDroppedError(
|
||||||
|
"Peripheral changed connection status while waiting for "
|
||||||
|
f"{response_names}: {to_human_readable_gatt_error(response.error)} "
|
||||||
|
f"({response.error})"
|
||||||
|
)
|
||||||
|
return response
|
||||||
|
|
||||||
async def _bluetooth_device_request(
|
async def _bluetooth_device_request(
|
||||||
self,
|
self,
|
||||||
address: int,
|
address: int,
|
||||||
@ -702,6 +714,7 @@ class APIClient:
|
|||||||
msg_types: tuple[type[message.Message], ...],
|
msg_types: tuple[type[message.Message], ...],
|
||||||
timeout: float,
|
timeout: float,
|
||||||
) -> message.Message:
|
) -> message.Message:
|
||||||
|
"""Send a BluetoothDeviceRequest and wait for a response."""
|
||||||
[response] = await self._get_connection().send_messages_await_response_complex(
|
[response] = await self._get_connection().send_messages_await_response_complex(
|
||||||
(
|
(
|
||||||
BluetoothDeviceRequest(
|
BluetoothDeviceRequest(
|
||||||
@ -941,7 +954,6 @@ class APIClient:
|
|||||||
elif position == 0.0:
|
elif position == 0.0:
|
||||||
req.legacy_command = LegacyCoverCommand.CLOSE
|
req.legacy_command = LegacyCoverCommand.CLOSE
|
||||||
req.has_legacy_command = True
|
req.has_legacy_command = True
|
||||||
|
|
||||||
self._get_connection().send_message(req)
|
self._get_connection().send_message(req)
|
||||||
|
|
||||||
async def fan_command(
|
async def fan_command(
|
||||||
@ -969,7 +981,6 @@ class APIClient:
|
|||||||
if direction is not None:
|
if direction is not None:
|
||||||
req.has_direction = True
|
req.has_direction = True
|
||||||
req.direction = direction
|
req.direction = direction
|
||||||
|
|
||||||
self._get_connection().send_message(req)
|
self._get_connection().send_message(req)
|
||||||
|
|
||||||
async def light_command( # pylint: disable=too-many-branches
|
async def light_command( # pylint: disable=too-many-branches
|
||||||
@ -1027,7 +1038,6 @@ class APIClient:
|
|||||||
if effect is not None:
|
if effect is not None:
|
||||||
req.has_effect = True
|
req.has_effect = True
|
||||||
req.effect = effect
|
req.effect = effect
|
||||||
|
|
||||||
self._get_connection().send_message(req)
|
self._get_connection().send_message(req)
|
||||||
|
|
||||||
async def switch_command(self, key: int, state: bool) -> None:
|
async def switch_command(self, key: int, state: bool) -> None:
|
||||||
@ -1079,7 +1089,6 @@ class APIClient:
|
|||||||
if custom_preset is not None:
|
if custom_preset is not None:
|
||||||
req.has_custom_preset = True
|
req.has_custom_preset = True
|
||||||
req.custom_preset = custom_preset
|
req.custom_preset = custom_preset
|
||||||
|
|
||||||
self._get_connection().send_message(req)
|
self._get_connection().send_message(req)
|
||||||
|
|
||||||
async def number_command(self, key: int, state: float) -> None:
|
async def number_command(self, key: int, state: float) -> None:
|
||||||
@ -1109,7 +1118,6 @@ class APIClient:
|
|||||||
if duration is not None:
|
if duration is not None:
|
||||||
req.duration = duration
|
req.duration = duration
|
||||||
req.has_duration = True
|
req.has_duration = True
|
||||||
|
|
||||||
self._get_connection().send_message(req)
|
self._get_connection().send_message(req)
|
||||||
|
|
||||||
async def button_command(self, key: int) -> None:
|
async def button_command(self, key: int) -> None:
|
||||||
@ -1144,7 +1152,6 @@ class APIClient:
|
|||||||
if media_url is not None:
|
if media_url is not None:
|
||||||
req.media_url = media_url
|
req.media_url = media_url
|
||||||
req.has_media_url = True
|
req.has_media_url = True
|
||||||
|
|
||||||
self._get_connection().send_message(req)
|
self._get_connection().send_message(req)
|
||||||
|
|
||||||
async def text_command(self, key: int, state: str) -> None:
|
async def text_command(self, key: int, state: str) -> None:
|
||||||
|
@ -66,7 +66,7 @@ from aioesphomeapi.api_pb2 import (
|
|||||||
VoiceAssistantRequest,
|
VoiceAssistantRequest,
|
||||||
VoiceAssistantResponse,
|
VoiceAssistantResponse,
|
||||||
)
|
)
|
||||||
from aioesphomeapi.client import APIClient
|
from aioesphomeapi.client import APIClient, BluetoothConnectionDroppedError
|
||||||
from aioesphomeapi.connection import APIConnection
|
from aioesphomeapi.connection import APIConnection
|
||||||
from aioesphomeapi.core import (
|
from aioesphomeapi.core import (
|
||||||
APIConnectionError,
|
APIConnectionError,
|
||||||
@ -956,8 +956,50 @@ async def test_bluetooth_pair_connection_drops(
|
|||||||
)
|
)
|
||||||
mock_data_received(protocol, generate_plaintext_packet(response))
|
mock_data_received(protocol, generate_plaintext_packet(response))
|
||||||
with pytest.raises(
|
with pytest.raises(
|
||||||
APIConnectionError,
|
BluetoothConnectionDroppedError,
|
||||||
match="Peripheral changed connections status while pairing: 13",
|
match="Peripheral changed connection status while waiting for BluetoothDevicePairingResponse: Invalid attribute length",
|
||||||
|
):
|
||||||
|
await pair_task
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_bluetooth_unpair_connection_drops(
|
||||||
|
api_client: tuple[
|
||||||
|
APIClient, APIConnection, asyncio.Transport, APIPlaintextFrameHelper
|
||||||
|
],
|
||||||
|
) -> None:
|
||||||
|
"""Test connection drop during bluetooth_device_unpair."""
|
||||||
|
client, connection, transport, protocol = api_client
|
||||||
|
pair_task = asyncio.create_task(client.bluetooth_device_unpair(1234))
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
response: message.Message = BluetoothDeviceConnectionResponse(
|
||||||
|
address=1234, connected=False, error=13
|
||||||
|
)
|
||||||
|
mock_data_received(protocol, generate_plaintext_packet(response))
|
||||||
|
with pytest.raises(
|
||||||
|
BluetoothConnectionDroppedError,
|
||||||
|
match="Peripheral changed connection status while waiting for BluetoothDeviceUnpairingResponse: Invalid attribute length",
|
||||||
|
):
|
||||||
|
await pair_task
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.asyncio
|
||||||
|
async def test_bluetooth_clear_cache_connection_drops(
|
||||||
|
api_client: tuple[
|
||||||
|
APIClient, APIConnection, asyncio.Transport, APIPlaintextFrameHelper
|
||||||
|
],
|
||||||
|
) -> None:
|
||||||
|
"""Test connection drop during bluetooth_device_clear_cache."""
|
||||||
|
client, connection, transport, protocol = api_client
|
||||||
|
pair_task = asyncio.create_task(client.bluetooth_device_clear_cache(1234))
|
||||||
|
await asyncio.sleep(0)
|
||||||
|
response: message.Message = BluetoothDeviceConnectionResponse(
|
||||||
|
address=1234, connected=False, error=13
|
||||||
|
)
|
||||||
|
mock_data_received(protocol, generate_plaintext_packet(response))
|
||||||
|
with pytest.raises(
|
||||||
|
BluetoothConnectionDroppedError,
|
||||||
|
match="Peripheral changed connection status while waiting for BluetoothDeviceClearCacheResponse: Invalid attribute length",
|
||||||
):
|
):
|
||||||
await pair_task
|
await pair_task
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user