mirror of
https://github.com/esphome/aioesphomeapi.git
synced 2025-03-11 13:21:25 +01:00
Raise GATT errors on read and write etc (#272)
Co-authored-by: J. Nick Koston <nick@koston.org>
This commit is contained in:
parent
76bd45f8c3
commit
c7edc2e601
@ -1326,3 +1326,31 @@ message BluetoothConnectionsFreeResponse {
|
|||||||
uint32 free = 1;
|
uint32 free = 1;
|
||||||
uint32 limit = 2;
|
uint32 limit = 2;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
message BluetoothGATTErrorResponse {
|
||||||
|
option (id) = 82;
|
||||||
|
option (source) = SOURCE_SERVER;
|
||||||
|
option (ifdef) = "USE_BLUETOOTH_PROXY";
|
||||||
|
|
||||||
|
uint64 address = 1;
|
||||||
|
uint32 handle = 2;
|
||||||
|
int32 error = 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
message BluetoothGATTWriteResponse {
|
||||||
|
option (id) = 83;
|
||||||
|
option (source) = SOURCE_SERVER;
|
||||||
|
option (ifdef) = "USE_BLUETOOTH_PROXY";
|
||||||
|
|
||||||
|
uint64 address = 1;
|
||||||
|
uint32 handle = 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
message BluetoothGATTNotifyResponse {
|
||||||
|
option (id) = 84;
|
||||||
|
option (source) = SOURCE_SERVER;
|
||||||
|
option (ifdef) = "USE_BLUETOOTH_PROXY";
|
||||||
|
|
||||||
|
uint64 address = 1;
|
||||||
|
uint32 handle = 2;
|
||||||
|
}
|
||||||
|
File diff suppressed because one or more lines are too long
@ -23,16 +23,19 @@ from .api_pb2 import ( # type: ignore
|
|||||||
BluetoothConnectionsFreeResponse,
|
BluetoothConnectionsFreeResponse,
|
||||||
BluetoothDeviceConnectionResponse,
|
BluetoothDeviceConnectionResponse,
|
||||||
BluetoothDeviceRequest,
|
BluetoothDeviceRequest,
|
||||||
|
BluetoothGATTErrorResponse,
|
||||||
BluetoothGATTGetServicesDoneResponse,
|
BluetoothGATTGetServicesDoneResponse,
|
||||||
BluetoothGATTGetServicesRequest,
|
BluetoothGATTGetServicesRequest,
|
||||||
BluetoothGATTGetServicesResponse,
|
BluetoothGATTGetServicesResponse,
|
||||||
BluetoothGATTNotifyDataResponse,
|
BluetoothGATTNotifyDataResponse,
|
||||||
BluetoothGATTNotifyRequest,
|
BluetoothGATTNotifyRequest,
|
||||||
|
BluetoothGATTNotifyResponse,
|
||||||
BluetoothGATTReadDescriptorRequest,
|
BluetoothGATTReadDescriptorRequest,
|
||||||
BluetoothGATTReadRequest,
|
BluetoothGATTReadRequest,
|
||||||
BluetoothGATTReadResponse,
|
BluetoothGATTReadResponse,
|
||||||
BluetoothGATTWriteDescriptorRequest,
|
BluetoothGATTWriteDescriptorRequest,
|
||||||
BluetoothGATTWriteRequest,
|
BluetoothGATTWriteRequest,
|
||||||
|
BluetoothGATTWriteResponse,
|
||||||
BluetoothLEAdvertisementResponse,
|
BluetoothLEAdvertisementResponse,
|
||||||
ButtonCommandRequest,
|
ButtonCommandRequest,
|
||||||
CameraImageRequest,
|
CameraImageRequest,
|
||||||
@ -93,7 +96,7 @@ from .api_pb2 import ( # type: ignore
|
|||||||
TextSensorStateResponse,
|
TextSensorStateResponse,
|
||||||
)
|
)
|
||||||
from .connection import APIConnection, ConnectionParams
|
from .connection import APIConnection, ConnectionParams
|
||||||
from .core import APIConnectionError, TimeoutAPIError
|
from .core import APIConnectionError, BluetoothGATTAPIError, TimeoutAPIError
|
||||||
from .host_resolver import ZeroconfInstanceType
|
from .host_resolver import ZeroconfInstanceType
|
||||||
from .model import (
|
from .model import (
|
||||||
APIVersion,
|
APIVersion,
|
||||||
@ -102,6 +105,7 @@ from .model import (
|
|||||||
BluetoothConnectionsFree,
|
BluetoothConnectionsFree,
|
||||||
BluetoothDeviceConnection,
|
BluetoothDeviceConnection,
|
||||||
BluetoothDeviceRequestType,
|
BluetoothDeviceRequestType,
|
||||||
|
BluetoothGATTError,
|
||||||
BluetoothGATTRead,
|
BluetoothGATTRead,
|
||||||
BluetoothGATTServices,
|
BluetoothGATTServices,
|
||||||
BluetoothLEAdvertisement,
|
BluetoothLEAdvertisement,
|
||||||
@ -408,6 +412,33 @@ class APIClient:
|
|||||||
SubscribeHomeassistantServicesRequest(), on_msg
|
SubscribeHomeassistantServicesRequest(), on_msg
|
||||||
)
|
)
|
||||||
|
|
||||||
|
async def _send_bluetooth_message_await_response(
|
||||||
|
self,
|
||||||
|
address: int,
|
||||||
|
handle: int,
|
||||||
|
request: message.Message,
|
||||||
|
response_type: Type[message.Message],
|
||||||
|
timeout: float = 10.0,
|
||||||
|
) -> message.Message:
|
||||||
|
self._check_authenticated()
|
||||||
|
assert self._connection is not None
|
||||||
|
|
||||||
|
def is_response(msg: message.Message) -> bool:
|
||||||
|
return (
|
||||||
|
isinstance(msg, (BluetoothGATTErrorResponse, response_type))
|
||||||
|
and msg.address == address # type: ignore[union-attr]
|
||||||
|
and msg.handle == handle # type: ignore[union-attr]
|
||||||
|
)
|
||||||
|
|
||||||
|
resp = await self._connection.send_message_await_response_complex(
|
||||||
|
request, is_response, is_response, timeout=timeout
|
||||||
|
)
|
||||||
|
|
||||||
|
if isinstance(resp[0], BluetoothGATTErrorResponse):
|
||||||
|
raise BluetoothGATTAPIError(BluetoothGATTError.from_pb(resp[0]))
|
||||||
|
|
||||||
|
return resp[0]
|
||||||
|
|
||||||
async def subscribe_bluetooth_le_advertisements(
|
async def subscribe_bluetooth_le_advertisements(
|
||||||
self, on_bluetooth_le_advertisement: Callable[[BluetoothLEAdvertisement], None]
|
self, on_bluetooth_le_advertisement: Callable[[BluetoothLEAdvertisement], None]
|
||||||
) -> Callable[[], None]:
|
) -> Callable[[], None]:
|
||||||
@ -509,10 +540,21 @@ class APIClient:
|
|||||||
self._check_authenticated()
|
self._check_authenticated()
|
||||||
|
|
||||||
def do_append(msg: message.Message) -> bool:
|
def do_append(msg: message.Message) -> bool:
|
||||||
return isinstance(msg, BluetoothGATTGetServicesResponse)
|
return (
|
||||||
|
isinstance(
|
||||||
|
msg, (BluetoothGATTGetServicesResponse, BluetoothGATTErrorResponse)
|
||||||
|
)
|
||||||
|
and msg.address == address
|
||||||
|
)
|
||||||
|
|
||||||
def do_stop(msg: message.Message) -> bool:
|
def do_stop(msg: message.Message) -> bool:
|
||||||
return isinstance(msg, BluetoothGATTGetServicesDoneResponse)
|
return (
|
||||||
|
isinstance(
|
||||||
|
msg,
|
||||||
|
(BluetoothGATTGetServicesDoneResponse, BluetoothGATTErrorResponse),
|
||||||
|
)
|
||||||
|
and msg.address == address
|
||||||
|
)
|
||||||
|
|
||||||
assert self._connection is not None
|
assert self._connection is not None
|
||||||
resp = await self._connection.send_message_await_response_complex(
|
resp = await self._connection.send_message_await_response_complex(
|
||||||
@ -523,58 +565,56 @@ class APIClient:
|
|||||||
)
|
)
|
||||||
services = []
|
services = []
|
||||||
for msg in resp:
|
for msg in resp:
|
||||||
|
if isinstance(msg, BluetoothGATTErrorResponse):
|
||||||
|
raise BluetoothGATTAPIError(BluetoothGATTError.from_pb(msg))
|
||||||
|
|
||||||
services.extend(BluetoothGATTServices.from_pb(msg).services)
|
services.extend(BluetoothGATTServices.from_pb(msg).services)
|
||||||
|
|
||||||
return ESPHomeBluetoothGATTServices(address=address, services=services)
|
return ESPHomeBluetoothGATTServices(address=address, services=services)
|
||||||
|
|
||||||
async def bluetooth_gatt_read(
|
async def bluetooth_gatt_read(
|
||||||
self,
|
self,
|
||||||
address: int,
|
address: int,
|
||||||
characteristic_handle: int,
|
handle: int,
|
||||||
timeout: float = DEFAULT_BLE_TIMEOUT,
|
timeout: float = DEFAULT_BLE_TIMEOUT,
|
||||||
) -> bytearray:
|
) -> bytearray:
|
||||||
self._check_authenticated()
|
|
||||||
|
|
||||||
req = BluetoothGATTReadRequest()
|
req = BluetoothGATTReadRequest()
|
||||||
req.address = address
|
req.address = address
|
||||||
req.handle = characteristic_handle
|
req.handle = handle
|
||||||
|
|
||||||
def is_response(msg: message.Message) -> bool:
|
resp = await self._send_bluetooth_message_await_response(
|
||||||
if isinstance(msg, BluetoothGATTReadResponse):
|
address,
|
||||||
read = BluetoothGATTRead.from_pb(msg)
|
handle,
|
||||||
|
req,
|
||||||
if read.address == address and read.handle == characteristic_handle:
|
BluetoothGATTReadResponse,
|
||||||
return True
|
timeout=timeout,
|
||||||
return False
|
|
||||||
|
|
||||||
assert self._connection is not None
|
|
||||||
resp = await self._connection.send_message_await_response_complex(
|
|
||||||
req, is_response, is_response, timeout=timeout
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(resp) != 1:
|
read_response = BluetoothGATTRead.from_pb(resp)
|
||||||
raise APIConnectionError(f"Expected one result, got {len(resp)}")
|
|
||||||
|
|
||||||
read_response = BluetoothGATTRead.from_pb(resp[0])
|
|
||||||
|
|
||||||
return bytearray(read_response.data)
|
return bytearray(read_response.data)
|
||||||
|
|
||||||
async def bluetooth_gatt_write(
|
async def bluetooth_gatt_write(
|
||||||
self,
|
self,
|
||||||
address: int,
|
address: int,
|
||||||
characteristic_handle: int,
|
handle: int,
|
||||||
data: bytes,
|
data: bytes,
|
||||||
response: bool,
|
response: bool,
|
||||||
|
timeout: float = 10.0,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._check_authenticated()
|
|
||||||
|
|
||||||
req = BluetoothGATTWriteRequest()
|
req = BluetoothGATTWriteRequest()
|
||||||
req.address = address
|
req.address = address
|
||||||
req.handle = characteristic_handle
|
req.handle = handle
|
||||||
req.response = response
|
req.response = response
|
||||||
req.data = data
|
req.data = data
|
||||||
|
|
||||||
assert self._connection is not None
|
await self._send_bluetooth_message_await_response(
|
||||||
await self._connection.send_message(req)
|
address,
|
||||||
|
handle,
|
||||||
|
req,
|
||||||
|
BluetoothGATTWriteResponse,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
async def bluetooth_gatt_read_descriptor(
|
async def bluetooth_gatt_read_descriptor(
|
||||||
self,
|
self,
|
||||||
@ -582,29 +622,19 @@ class APIClient:
|
|||||||
handle: int,
|
handle: int,
|
||||||
timeout: float = DEFAULT_BLE_TIMEOUT,
|
timeout: float = DEFAULT_BLE_TIMEOUT,
|
||||||
) -> bytearray:
|
) -> bytearray:
|
||||||
self._check_authenticated()
|
|
||||||
|
|
||||||
req = BluetoothGATTReadDescriptorRequest()
|
req = BluetoothGATTReadDescriptorRequest()
|
||||||
req.address = address
|
req.address = address
|
||||||
req.handle = handle
|
req.handle = handle
|
||||||
|
|
||||||
def is_response(msg: message.Message) -> bool:
|
resp = await self._send_bluetooth_message_await_response(
|
||||||
if isinstance(msg, BluetoothGATTReadResponse):
|
address,
|
||||||
read = BluetoothGATTRead.from_pb(msg)
|
handle,
|
||||||
|
req,
|
||||||
if read.address == address and read.handle == handle:
|
BluetoothGATTReadResponse,
|
||||||
return True
|
timeout=timeout,
|
||||||
return False
|
|
||||||
|
|
||||||
assert self._connection is not None
|
|
||||||
resp = await self._connection.send_message_await_response_complex(
|
|
||||||
req, is_response, is_response, timeout=timeout
|
|
||||||
)
|
)
|
||||||
|
|
||||||
if len(resp) != 1:
|
read_response = BluetoothGATTRead.from_pb(resp)
|
||||||
raise APIConnectionError(f"Expected one result, got {len(resp)}")
|
|
||||||
|
|
||||||
read_response = BluetoothGATTRead.from_pb(resp[0])
|
|
||||||
|
|
||||||
return bytearray(read_response.data)
|
return bytearray(read_response.data)
|
||||||
|
|
||||||
@ -613,16 +643,20 @@ class APIClient:
|
|||||||
address: int,
|
address: int,
|
||||||
handle: int,
|
handle: int,
|
||||||
data: bytes,
|
data: bytes,
|
||||||
|
timeout: float = 10.0,
|
||||||
) -> None:
|
) -> None:
|
||||||
self._check_authenticated()
|
|
||||||
|
|
||||||
req = BluetoothGATTWriteDescriptorRequest()
|
req = BluetoothGATTWriteDescriptorRequest()
|
||||||
req.address = address
|
req.address = address
|
||||||
req.handle = handle
|
req.handle = handle
|
||||||
req.data = data
|
req.data = data
|
||||||
|
|
||||||
assert self._connection is not None
|
await self._send_bluetooth_message_await_response(
|
||||||
await self._connection.send_message(req)
|
address,
|
||||||
|
handle,
|
||||||
|
req,
|
||||||
|
BluetoothGATTWriteResponse,
|
||||||
|
timeout=timeout,
|
||||||
|
)
|
||||||
|
|
||||||
async def bluetooth_gatt_start_notify(
|
async def bluetooth_gatt_start_notify(
|
||||||
self,
|
self,
|
||||||
@ -630,7 +664,13 @@ class APIClient:
|
|||||||
handle: int,
|
handle: int,
|
||||||
on_bluetooth_gatt_notify: Callable[[int, bytearray], None],
|
on_bluetooth_gatt_notify: Callable[[int, bytearray], None],
|
||||||
) -> Callable[[], Coroutine[Any, Any, None]]:
|
) -> Callable[[], Coroutine[Any, Any, None]]:
|
||||||
self._check_authenticated()
|
|
||||||
|
await self._send_bluetooth_message_await_response(
|
||||||
|
address,
|
||||||
|
handle,
|
||||||
|
BluetoothGATTNotifyRequest(address=address, handle=handle, enable=True),
|
||||||
|
BluetoothGATTNotifyResponse,
|
||||||
|
)
|
||||||
|
|
||||||
def on_msg(msg: message.Message) -> None:
|
def on_msg(msg: message.Message) -> None:
|
||||||
if isinstance(msg, BluetoothGATTNotifyDataResponse):
|
if isinstance(msg, BluetoothGATTNotifyDataResponse):
|
||||||
@ -639,16 +679,13 @@ class APIClient:
|
|||||||
on_bluetooth_gatt_notify(handle, bytearray(notify.data))
|
on_bluetooth_gatt_notify(handle, bytearray(notify.data))
|
||||||
|
|
||||||
assert self._connection is not None
|
assert self._connection is not None
|
||||||
await self._connection.send_message_callback_response(
|
remove_callback = self._connection.add_message_callback(on_msg)
|
||||||
BluetoothGATTNotifyRequest(address=address, handle=handle, enable=True),
|
|
||||||
on_msg,
|
|
||||||
)
|
|
||||||
|
|
||||||
async def stop_notify() -> None:
|
async def stop_notify() -> None:
|
||||||
if self._connection is None:
|
if self._connection is None:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._connection.remove_message_callback(on_msg)
|
remove_callback()
|
||||||
|
|
||||||
self._check_authenticated()
|
self._check_authenticated()
|
||||||
|
|
||||||
|
@ -403,6 +403,17 @@ class APIConnection:
|
|||||||
await self._report_fatal_error(err)
|
await self._report_fatal_error(err)
|
||||||
raise
|
raise
|
||||||
|
|
||||||
|
def add_message_callback(
|
||||||
|
self, on_message: Callable[[Any], None]
|
||||||
|
) -> Callable[[], None]:
|
||||||
|
"""Add a message callback."""
|
||||||
|
self._message_handlers.append(on_message)
|
||||||
|
|
||||||
|
def unsub() -> None:
|
||||||
|
self._message_handlers.remove(on_message)
|
||||||
|
|
||||||
|
return unsub
|
||||||
|
|
||||||
def remove_message_callback(self, on_message: Callable[[Any], None]) -> None:
|
def remove_message_callback(self, on_message: Callable[[Any], None]) -> None:
|
||||||
"""Remove a message callback."""
|
"""Remove a message callback."""
|
||||||
self._message_handlers.remove(on_message)
|
self._message_handlers.remove(on_message)
|
||||||
@ -417,10 +428,10 @@ class APIConnection:
|
|||||||
async def send_message_await_response_complex(
|
async def send_message_await_response_complex(
|
||||||
self,
|
self,
|
||||||
send_msg: message.Message,
|
send_msg: message.Message,
|
||||||
do_append: Callable[[Any], bool],
|
do_append: Callable[[message.Message], bool],
|
||||||
do_stop: Callable[[Any], bool],
|
do_stop: Callable[[message.Message], bool],
|
||||||
timeout: float = 10.0,
|
timeout: float = 10.0,
|
||||||
) -> List[Any]:
|
) -> List[message.Message]:
|
||||||
"""Send a message to the remote and build up a list response.
|
"""Send a message to the remote and build up a list response.
|
||||||
|
|
||||||
:param send_msg: The message (request) to send.
|
:param send_msg: The message (request) to send.
|
||||||
|
@ -1,18 +1,25 @@
|
|||||||
|
import re
|
||||||
|
|
||||||
|
from aioesphomeapi.model import BluetoothGATTError
|
||||||
|
|
||||||
from .api_pb2 import ( # type: ignore
|
from .api_pb2 import ( # type: ignore
|
||||||
BinarySensorStateResponse,
|
BinarySensorStateResponse,
|
||||||
BluetoothConnectionsFreeResponse,
|
BluetoothConnectionsFreeResponse,
|
||||||
BluetoothDeviceConnectionResponse,
|
BluetoothDeviceConnectionResponse,
|
||||||
BluetoothDeviceRequest,
|
BluetoothDeviceRequest,
|
||||||
|
BluetoothGATTErrorResponse,
|
||||||
BluetoothGATTGetServicesDoneResponse,
|
BluetoothGATTGetServicesDoneResponse,
|
||||||
BluetoothGATTGetServicesRequest,
|
BluetoothGATTGetServicesRequest,
|
||||||
BluetoothGATTGetServicesResponse,
|
BluetoothGATTGetServicesResponse,
|
||||||
BluetoothGATTNotifyDataResponse,
|
BluetoothGATTNotifyDataResponse,
|
||||||
BluetoothGATTNotifyRequest,
|
BluetoothGATTNotifyRequest,
|
||||||
|
BluetoothGATTNotifyResponse,
|
||||||
BluetoothGATTReadDescriptorRequest,
|
BluetoothGATTReadDescriptorRequest,
|
||||||
BluetoothGATTReadRequest,
|
BluetoothGATTReadRequest,
|
||||||
BluetoothGATTReadResponse,
|
BluetoothGATTReadResponse,
|
||||||
BluetoothGATTWriteDescriptorRequest,
|
BluetoothGATTWriteDescriptorRequest,
|
||||||
BluetoothGATTWriteRequest,
|
BluetoothGATTWriteRequest,
|
||||||
|
BluetoothGATTWriteResponse,
|
||||||
BluetoothLEAdvertisementResponse,
|
BluetoothLEAdvertisementResponse,
|
||||||
ButtonCommandRequest,
|
ButtonCommandRequest,
|
||||||
CameraImageRequest,
|
CameraImageRequest,
|
||||||
@ -82,6 +89,8 @@ from .api_pb2 import ( # type: ignore
|
|||||||
TextSensorStateResponse,
|
TextSensorStateResponse,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
TWO_CHAR = re.compile(r".{2}")
|
||||||
|
|
||||||
|
|
||||||
class APIConnectionError(Exception):
|
class APIConnectionError(Exception):
|
||||||
pass
|
pass
|
||||||
@ -139,6 +148,19 @@ class ReadFailedAPIError(APIConnectionError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
def to_human_readable_address(address: int) -> str:
|
||||||
|
"""Convert a MAC address to a human readable format."""
|
||||||
|
return ":".join(TWO_CHAR.findall(f"{address:012X}"))
|
||||||
|
|
||||||
|
|
||||||
|
class BluetoothGATTAPIError(APIConnectionError):
|
||||||
|
def __init__(self, error: BluetoothGATTError) -> None:
|
||||||
|
super().__init__(
|
||||||
|
f"Bluetooth GATT Error address={to_human_readable_address(error.address)} handle={error.handle} error={error.error}"
|
||||||
|
)
|
||||||
|
self.error = error
|
||||||
|
|
||||||
|
|
||||||
MESSAGE_TYPE_TO_PROTO = {
|
MESSAGE_TYPE_TO_PROTO = {
|
||||||
1: HelloRequest,
|
1: HelloRequest,
|
||||||
2: HelloResponse,
|
2: HelloResponse,
|
||||||
@ -221,4 +243,7 @@ MESSAGE_TYPE_TO_PROTO = {
|
|||||||
79: BluetoothGATTNotifyDataResponse,
|
79: BluetoothGATTNotifyDataResponse,
|
||||||
80: SubscribeBluetoothConnectionsFreeRequest,
|
80: SubscribeBluetoothConnectionsFreeRequest,
|
||||||
81: BluetoothConnectionsFreeResponse,
|
81: BluetoothConnectionsFreeResponse,
|
||||||
|
82: BluetoothGATTErrorResponse,
|
||||||
|
83: BluetoothGATTWriteResponse,
|
||||||
|
84: BluetoothGATTNotifyResponse,
|
||||||
}
|
}
|
||||||
|
@ -900,6 +900,13 @@ class BluetoothConnectionsFree(APIModelBase):
|
|||||||
limit: int = 0
|
limit: int = 0
|
||||||
|
|
||||||
|
|
||||||
|
@dataclass(frozen=True)
|
||||||
|
class BluetoothGATTError(APIModelBase):
|
||||||
|
address: int = 0
|
||||||
|
handle: int = 0
|
||||||
|
error: int = 0
|
||||||
|
|
||||||
|
|
||||||
class BluetoothDeviceRequestType(APIIntEnum):
|
class BluetoothDeviceRequestType(APIIntEnum):
|
||||||
CONNECT = 0
|
CONNECT = 0
|
||||||
DISCONNECT = 1
|
DISCONNECT = 1
|
||||||
|
Loading…
Reference in New Issue
Block a user