aioesphomeapi/aioesphomeapi/client.py

1127 lines
37 KiB
Python
Raw Normal View History

2022-09-28 18:50:37 +02:00
# pylint: disable=too-many-lines
import asyncio
2018-12-13 21:34:57 +01:00
import logging
from typing import (
Any,
Awaitable,
Callable,
2022-09-28 18:50:37 +02:00
Coroutine,
Dict,
List,
Optional,
Tuple,
2021-06-29 15:36:14 +02:00
Type,
Union,
cast,
)
2018-12-13 21:34:57 +01:00
2022-09-28 18:50:37 +02:00
import async_timeout
from google.protobuf import message
from .api_pb2 import ( # type: ignore
BinarySensorStateResponse,
2022-09-28 18:50:37 +02:00
BluetoothConnectionsFreeResponse,
BluetoothDeviceConnectionResponse,
BluetoothDeviceRequest,
BluetoothGATTErrorResponse,
2022-09-28 18:50:37 +02:00
BluetoothGATTGetServicesDoneResponse,
BluetoothGATTGetServicesRequest,
BluetoothGATTGetServicesResponse,
BluetoothGATTNotifyDataResponse,
BluetoothGATTNotifyRequest,
BluetoothGATTNotifyResponse,
2022-09-28 18:50:37 +02:00
BluetoothGATTReadDescriptorRequest,
BluetoothGATTReadRequest,
BluetoothGATTReadResponse,
BluetoothGATTWriteDescriptorRequest,
BluetoothGATTWriteRequest,
BluetoothGATTWriteResponse,
BluetoothLEAdvertisementResponse,
2021-11-29 01:59:23 +01:00
ButtonCommandRequest,
CameraImageRequest,
CameraImageResponse,
ClimateCommandRequest,
ClimateStateResponse,
CoverCommandRequest,
CoverStateResponse,
DeviceInfoRequest,
DeviceInfoResponse,
ExecuteServiceArgument,
ExecuteServiceRequest,
FanCommandRequest,
FanStateResponse,
HomeassistantServiceResponse,
HomeAssistantStateResponse,
LightCommandRequest,
LightStateResponse,
ListEntitiesBinarySensorResponse,
2021-11-29 01:59:23 +01:00
ListEntitiesButtonResponse,
ListEntitiesCameraResponse,
ListEntitiesClimateResponse,
ListEntitiesCoverResponse,
ListEntitiesDoneResponse,
ListEntitiesFanResponse,
ListEntitiesLightResponse,
2022-01-11 02:29:19 +01:00
ListEntitiesLockResponse,
2022-05-18 03:28:40 +02:00
ListEntitiesMediaPlayerResponse,
2021-06-29 12:42:38 +02:00
ListEntitiesNumberResponse,
ListEntitiesRequest,
2021-07-26 20:51:12 +02:00
ListEntitiesSelectResponse,
ListEntitiesSensorResponse,
ListEntitiesServicesResponse,
2021-09-09 03:11:51 +02:00
ListEntitiesSirenResponse,
ListEntitiesSwitchResponse,
ListEntitiesTextSensorResponse,
2022-01-11 02:29:19 +01:00
LockCommandRequest,
LockStateResponse,
2022-05-18 03:28:40 +02:00
MediaPlayerCommandRequest,
MediaPlayerStateResponse,
2021-06-29 12:42:38 +02:00
NumberCommandRequest,
NumberStateResponse,
2021-07-26 20:51:12 +02:00
SelectCommandRequest,
SelectStateResponse,
SensorStateResponse,
2021-09-09 03:11:51 +02:00
SirenCommandRequest,
SirenStateResponse,
2022-09-28 18:50:37 +02:00
SubscribeBluetoothConnectionsFreeRequest,
SubscribeBluetoothLEAdvertisementsRequest,
SubscribeHomeassistantServicesRequest,
SubscribeHomeAssistantStateResponse,
SubscribeHomeAssistantStatesRequest,
SubscribeLogsRequest,
SubscribeLogsResponse,
SubscribeStatesRequest,
SwitchCommandRequest,
SwitchStateResponse,
TextSensorStateResponse,
)
from .connection import APIConnection, ConnectionParams
from .core import (
APIConnectionError,
BluetoothGATTAPIError,
TimeoutAPIError,
to_human_readable_address,
)
from .host_resolver import ZeroconfInstanceType
from .model import (
APIVersion,
BinarySensorInfo,
BinarySensorState,
2022-09-28 18:50:37 +02:00
BluetoothConnectionsFree,
BluetoothDeviceConnection,
BluetoothDeviceRequestType,
BluetoothGATTError,
2022-09-28 18:50:37 +02:00
BluetoothGATTRead,
BluetoothGATTServices,
BluetoothLEAdvertisement,
2021-11-29 01:59:23 +01:00
ButtonInfo,
CameraInfo,
CameraState,
ClimateFanMode,
ClimateInfo,
ClimateMode,
ClimatePreset,
ClimateState,
ClimateSwingMode,
CoverInfo,
CoverState,
DeviceInfo,
EntityInfo,
2021-06-29 15:36:14 +02:00
EntityState,
2022-09-29 20:25:18 +02:00
ESPHomeBluetoothGATTServices,
FanDirection,
FanInfo,
FanSpeed,
FanState,
HomeassistantServiceCall,
LegacyCoverCommand,
LightInfo,
LightState,
2022-01-11 02:29:19 +01:00
LockCommand,
LockEntityState,
LockInfo,
2021-06-29 15:36:14 +02:00
LogLevel,
2022-05-18 03:28:40 +02:00
MediaPlayerCommand,
MediaPlayerEntityState,
MediaPlayerInfo,
2021-06-29 12:42:38 +02:00
NumberInfo,
NumberState,
2021-07-26 20:51:12 +02:00
SelectInfo,
SelectState,
SensorInfo,
SensorState,
2021-09-09 03:11:51 +02:00
SirenInfo,
SirenState,
SwitchInfo,
SwitchState,
TextSensorInfo,
TextSensorState,
UserService,
UserServiceArgType,
)
2018-12-13 21:34:57 +01:00
_LOGGER = logging.getLogger(__name__)
DEFAULT_BLE_TIMEOUT = 30.0
DEFAULT_BLE_DISCONNECT_TIMEOUT = 5.0
ExecuteServiceDataType = Dict[
str, Union[bool, int, float, str, List[bool], List[int], List[float], List[str]]
]
2018-12-13 21:34:57 +01:00
# pylint: disable=too-many-public-methods
class APIClient:
def __init__(
self,
address: str,
port: int,
2021-06-30 17:05:44 +02:00
password: Optional[str],
*,
client_info: str = "aioesphomeapi",
keepalive: float = 15.0,
zeroconf_instance: ZeroconfInstanceType = None,
2021-09-08 23:12:07 +02:00
noise_psk: Optional[str] = None,
expected_name: Optional[str] = None,
):
"""Create a client, this object is shared across sessions.
:param address: The address to connect to; for example an IP address
or .local name for mDNS lookup.
:param port: The port to connect to
:param password: Optional password to send to the device for authentication
:param client_info: User Agent string to send.
:param keepalive: The keepalive time in seconds (ping interval) for detecting stale connections.
Every keepalive seconds a ping is sent, if no pong is received the connection is closed.
:param zeroconf_instance: Pass a zeroconf instance to use if an mDNS lookup is necessary.
:param noise_psk: Encryption preshared key for noise transport encrypted sessions.
:param expected_name: Require the devices name to match the given expected name.
Can be used to prevent accidentally connecting to a different device if
IP passed as address but DHCP reassigned IP.
"""
self._params = ConnectionParams(
address=address,
port=port,
password=password,
client_info=client_info,
keepalive=keepalive,
zeroconf_instance=zeroconf_instance,
# treat empty psk string as missing (like password)
noise_psk=noise_psk or None,
expected_name=expected_name,
)
self._connection: Optional[APIConnection] = None
2021-06-30 17:10:30 +02:00
self._cached_name: Optional[str] = None
@property
def expected_name(self) -> Optional[str]:
return self._params.expected_name
@expected_name.setter
def expected_name(self, value: Optional[str]) -> None:
self._params.expected_name = value
2021-06-30 17:10:30 +02:00
@property
def address(self) -> str:
return self._params.address
@property
def _log_name(self) -> str:
if self._cached_name is not None:
return f"{self._cached_name} @ {self.address}"
return self.address
async def connect(
self,
on_stop: Optional[Callable[[], Awaitable[None]]] = None,
login: bool = False,
) -> None:
if self._connection is not None:
2021-06-30 17:10:30 +02:00
raise APIConnectionError(f"Already connected to {self._log_name}!")
async def _on_stop() -> None:
2021-10-21 19:20:05 +02:00
# Hook into on_stop handler to clear connection when stopped
self._connection = None
2021-10-21 19:20:05 +02:00
if on_stop is not None:
await on_stop()
self._connection = APIConnection(self._params, _on_stop)
2021-06-30 17:10:30 +02:00
self._connection.log_name = self._log_name
try:
2021-10-21 19:20:05 +02:00
await self._connection.connect(login=login)
except APIConnectionError:
2021-10-21 19:24:03 +02:00
self._connection = None
raise
except Exception as e:
2021-10-21 19:24:03 +02:00
self._connection = None
2021-06-30 17:10:30 +02:00
raise APIConnectionError(
f"Unexpected error while connecting to {self._log_name}: {e}"
) from e
async def disconnect(self, force: bool = False) -> None:
if self._connection is None:
return
if force:
await self._connection.force_disconnect()
else:
await self._connection.disconnect()
def _check_connected(self) -> None:
if self._connection is None:
2021-06-30 17:10:30 +02:00
raise APIConnectionError(f"Not connected to {self._log_name}!")
if not self._connection.is_connected:
2021-06-30 17:10:30 +02:00
raise APIConnectionError(f"Connection not done for {self._log_name}!")
def _check_authenticated(self) -> None:
self._check_connected()
assert self._connection is not None
if not self._connection.is_authenticated:
2021-06-30 17:10:30 +02:00
raise APIConnectionError(f"Not authenticated for {self._log_name}!")
async def device_info(self) -> DeviceInfo:
self._check_connected()
assert self._connection is not None
resp = await self._connection.send_message_await_response(
DeviceInfoRequest(), DeviceInfoResponse
)
2021-06-30 17:10:30 +02:00
info = DeviceInfo.from_pb(resp)
self._cached_name = info.name
self._connection.log_name = self._log_name
return info
async def list_entities_services(
self,
) -> Tuple[List[EntityInfo], List[UserService]]:
2018-12-13 21:34:57 +01:00
self._check_authenticated()
2021-06-29 15:36:14 +02:00
response_types: Dict[Any, Optional[Type[EntityInfo]]] = {
ListEntitiesBinarySensorResponse: BinarySensorInfo,
2021-11-29 01:59:23 +01:00
ListEntitiesButtonResponse: ButtonInfo,
ListEntitiesCoverResponse: CoverInfo,
ListEntitiesFanResponse: FanInfo,
ListEntitiesLightResponse: LightInfo,
2021-06-29 12:42:38 +02:00
ListEntitiesNumberResponse: NumberInfo,
2021-07-26 20:51:12 +02:00
ListEntitiesSelectResponse: SelectInfo,
ListEntitiesSensorResponse: SensorInfo,
2021-09-09 03:11:51 +02:00
ListEntitiesSirenResponse: SirenInfo,
ListEntitiesSwitchResponse: SwitchInfo,
ListEntitiesTextSensorResponse: TextSensorInfo,
ListEntitiesServicesResponse: None,
ListEntitiesCameraResponse: CameraInfo,
ListEntitiesClimateResponse: ClimateInfo,
2022-01-11 02:29:19 +01:00
ListEntitiesLockResponse: LockInfo,
2022-05-18 03:28:40 +02:00
ListEntitiesMediaPlayerResponse: MediaPlayerInfo,
2018-12-13 21:34:57 +01:00
}
def do_append(msg: message.Message) -> bool:
2018-12-13 21:34:57 +01:00
return isinstance(msg, tuple(response_types.keys()))
def do_stop(msg: message.Message) -> bool:
return isinstance(msg, ListEntitiesDoneResponse)
2018-12-13 21:34:57 +01:00
assert self._connection is not None
resp = await self._connection.send_message_await_response_complex(
ListEntitiesRequest(), do_append, do_stop, timeout=60
)
entities: List[EntityInfo] = []
services: List[UserService] = []
2018-12-13 21:34:57 +01:00
for msg in resp:
if isinstance(msg, ListEntitiesServicesResponse):
2021-06-29 15:36:14 +02:00
services.append(UserService.from_pb(msg))
2019-02-24 18:16:12 +01:00
continue
2018-12-13 21:34:57 +01:00
cls = None
for resp_type, cls in response_types.items():
if isinstance(msg, resp_type):
break
else:
continue
2021-06-29 15:36:14 +02:00
assert cls is not None
entities.append(cls.from_pb(msg))
2019-02-24 18:16:12 +01:00
return entities, services
2018-12-13 21:34:57 +01:00
2021-06-29 15:36:14 +02:00
async def subscribe_states(self, on_state: Callable[[EntityState], None]) -> None:
2018-12-13 21:34:57 +01:00
self._check_authenticated()
2021-06-29 15:36:14 +02:00
response_types: Dict[Any, Type[EntityState]] = {
BinarySensorStateResponse: BinarySensorState,
CoverStateResponse: CoverState,
FanStateResponse: FanState,
LightStateResponse: LightState,
2021-06-29 12:42:38 +02:00
NumberStateResponse: NumberState,
2021-07-26 20:51:12 +02:00
SelectStateResponse: SelectState,
SensorStateResponse: SensorState,
2021-09-09 03:11:51 +02:00
SirenStateResponse: SirenState,
SwitchStateResponse: SwitchState,
TextSensorStateResponse: TextSensorState,
ClimateStateResponse: ClimateState,
2022-01-11 02:29:19 +01:00
LockStateResponse: LockEntityState,
2022-05-18 03:28:40 +02:00
MediaPlayerStateResponse: MediaPlayerEntityState,
2018-12-13 21:34:57 +01:00
}
image_stream: Dict[int, bytes] = {}
2019-01-19 15:10:00 +01:00
def on_msg(msg: message.Message) -> None:
if isinstance(msg, CameraImageResponse):
2019-01-19 15:10:00 +01:00
data = image_stream.pop(msg.key, bytes()) + msg.data
if msg.done:
2021-07-09 09:19:39 +02:00
# Return CameraState with the merged data
on_state(CameraState(key=msg.key, data=data))
2019-01-19 15:10:00 +01:00
else:
image_stream[msg.key] = data
return
2018-12-13 21:34:57 +01:00
for resp_type, cls in response_types.items():
if isinstance(msg, resp_type):
break
else:
return
2020-07-14 20:00:12 +02:00
# pylint: disable=undefined-loop-variable
2021-06-29 15:36:14 +02:00
on_state(cls.from_pb(msg))
2018-12-13 21:34:57 +01:00
assert self._connection is not None
await self._connection.send_message_callback_response(
SubscribeStatesRequest(), on_msg
)
2018-12-13 21:34:57 +01:00
async def subscribe_logs(
self,
on_log: Callable[[SubscribeLogsResponse], None],
2021-06-29 15:36:14 +02:00
log_level: Optional[LogLevel] = None,
2021-09-08 23:12:07 +02:00
dump_config: Optional[bool] = None,
) -> None:
2018-12-13 21:34:57 +01:00
self._check_authenticated()
def on_msg(msg: message.Message) -> None:
if isinstance(msg, SubscribeLogsResponse):
2018-12-13 21:34:57 +01:00
on_log(msg)
req = SubscribeLogsRequest()
2018-12-13 21:34:57 +01:00
if log_level is not None:
req.level = log_level
2021-09-08 23:12:07 +02:00
if dump_config is not None:
req.dump_config = dump_config
assert self._connection is not None
await self._connection.send_message_callback_response(req, on_msg)
2018-12-13 21:34:57 +01:00
async def subscribe_service_calls(
self, on_service_call: Callable[[HomeassistantServiceCall], None]
) -> None:
2018-12-16 18:03:03 +01:00
self._check_authenticated()
def on_msg(msg: message.Message) -> None:
if isinstance(msg, HomeassistantServiceResponse):
2021-06-29 15:36:14 +02:00
on_service_call(HomeassistantServiceCall.from_pb(msg))
2018-12-16 18:03:03 +01:00
assert self._connection is not None
await self._connection.send_message_callback_response(
SubscribeHomeassistantServicesRequest(), on_msg
)
2018-12-16 18:03:03 +01:00
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(
self, on_bluetooth_le_advertisement: Callable[[BluetoothLEAdvertisement], None]
2022-09-28 18:50:37 +02:00
) -> Callable[[], None]:
self._check_authenticated()
def on_msg(msg: message.Message) -> None:
if isinstance(msg, BluetoothLEAdvertisementResponse):
on_bluetooth_le_advertisement(BluetoothLEAdvertisement.from_pb(msg))
assert self._connection is not None
await self._connection.send_message_callback_response(
SubscribeBluetoothLEAdvertisementsRequest(), on_msg
)
2022-09-28 18:50:37 +02:00
def unsub() -> None:
if self._connection is not None:
self._connection.remove_message_callback(on_msg)
2022-09-28 18:50:37 +02:00
return unsub
async def subscribe_bluetooth_connections_free(
self, on_bluetooth_connections_free_update: Callable[[int, int], None]
) -> Callable[[], None]:
self._check_authenticated()
def on_msg(msg: message.Message) -> None:
if isinstance(msg, BluetoothConnectionsFreeResponse):
resp = BluetoothConnectionsFree.from_pb(msg)
on_bluetooth_connections_free_update(resp.free, resp.limit)
assert self._connection is not None
await self._connection.send_message_callback_response(
SubscribeBluetoothConnectionsFreeRequest(), on_msg
)
def unsub() -> None:
if self._connection is not None:
self._connection.remove_message_callback(on_msg)
2022-09-28 18:50:37 +02:00
return unsub
async def bluetooth_device_connect(
self,
address: int,
on_bluetooth_connection_state: Callable[[bool, int, int], None],
timeout: float = DEFAULT_BLE_TIMEOUT,
disconnect_timeout: float = DEFAULT_BLE_DISCONNECT_TIMEOUT,
2022-11-29 03:06:13 +01:00
version: int = 1,
has_cache: bool = False,
address_type: Optional[int] = None,
2022-09-28 18:50:37 +02:00
) -> Callable[[], None]:
self._check_authenticated()
event = asyncio.Event()
def on_msg(msg: message.Message) -> None:
if isinstance(msg, BluetoothDeviceConnectionResponse):
resp = BluetoothDeviceConnection.from_pb(msg)
if address == resp.address:
on_bluetooth_connection_state(resp.connected, resp.mtu, resp.error)
event.set()
assert self._connection is not None
2022-11-29 03:06:13 +01:00
if has_cache:
# Version 3 with cache: requestor has services and mtu cached
_LOGGER.debug("%s: Using connection version 3 with cache", address)
request_type = BluetoothDeviceRequestType.CONNECT_V3_WITH_CACHE
elif version >= 3:
# Version 3 without cache: esp will wipe the service list after sending to save memory
_LOGGER.debug("%s: Using connection version 3 without cache", address)
request_type = BluetoothDeviceRequestType.CONNECT_V3_WITHOUT_CACHE
else:
# Older than v3 without cache: esp will hold the service list in memory for the duration
# of the connection. This can crash the esp if the service list is too large.
_LOGGER.debug("%s: Using connection version 1", address)
request_type = BluetoothDeviceRequestType.CONNECT
2022-09-28 18:50:37 +02:00
await self._connection.send_message_callback_response(
BluetoothDeviceRequest(
address=address,
request_type=request_type,
has_address_type=address_type is not None,
address_type=address_type or 0,
),
2022-09-28 18:50:37 +02:00
on_msg,
)
2022-09-29 20:25:18 +02:00
def unsub() -> None:
if self._connection is not None:
self._connection.remove_message_callback(on_msg)
2022-09-29 20:25:18 +02:00
2022-09-28 18:50:37 +02:00
try:
try:
async with async_timeout.timeout(timeout):
await event.wait()
except asyncio.TimeoutError as err:
# Disconnect before raising the exception to ensure
# the slot is recovered before the timeout is raised
# to avoid race were we run out even though we have a slot.
await self.bluetooth_device_disconnect(address)
addr = to_human_readable_address(address)
_LOGGER.debug("%s: Connecting timed out, waiting for disconnect", addr)
try:
async with async_timeout.timeout(disconnect_timeout):
await event.wait()
disconnect_timed_out = False
except asyncio.TimeoutError:
disconnect_timed_out = True
_LOGGER.debug(
"%s: Disconnect timed out: %s", addr, disconnect_timed_out
)
try:
unsub()
except ValueError:
_LOGGER.warning(
"%s: Bluetooth device connection timed out but already unsubscribed",
addr,
)
raise TimeoutAPIError(
f"Timeout waiting for connect response while connecting to {addr} "
f"after {timeout}s, disconnect timed out: {disconnect_timed_out}, "
f" after {disconnect_timeout}s"
) from err
except asyncio.CancelledError:
try:
unsub()
except ValueError:
_LOGGER.warning(
"%s: Bluetooth device connection canceled but already unsubscribed",
addr,
)
raise
2022-09-28 18:50:37 +02:00
return unsub
async def bluetooth_device_disconnect(self, address: int) -> None:
self._check_authenticated()
assert self._connection is not None
await self._connection.send_message(
BluetoothDeviceRequest(
address=address,
request_type=BluetoothDeviceRequestType.DISCONNECT,
)
)
2022-09-29 20:25:18 +02:00
async def bluetooth_gatt_get_services(
self, address: int
) -> ESPHomeBluetoothGATTServices:
2022-09-28 18:50:37 +02:00
self._check_authenticated()
def do_append(msg: message.Message) -> bool:
return (
isinstance(
msg, (BluetoothGATTGetServicesResponse, BluetoothGATTErrorResponse)
)
and msg.address == address
)
2022-09-28 18:50:37 +02:00
def do_stop(msg: message.Message) -> bool:
return (
isinstance(
msg,
(BluetoothGATTGetServicesDoneResponse, BluetoothGATTErrorResponse),
)
and msg.address == address
)
2022-09-28 18:50:37 +02:00
assert self._connection is not None
resp = await self._connection.send_message_await_response_complex(
BluetoothGATTGetServicesRequest(address=address),
do_append,
do_stop,
timeout=DEFAULT_BLE_TIMEOUT,
2022-09-28 18:50:37 +02:00
)
services = []
for msg in resp:
if isinstance(msg, BluetoothGATTErrorResponse):
raise BluetoothGATTAPIError(BluetoothGATTError.from_pb(msg))
2022-09-28 18:50:37 +02:00
services.extend(BluetoothGATTServices.from_pb(msg).services)
2022-09-29 20:25:18 +02:00
return ESPHomeBluetoothGATTServices(address=address, services=services)
2022-09-28 18:50:37 +02:00
async def bluetooth_gatt_read(
self,
address: int,
handle: int,
timeout: float = DEFAULT_BLE_TIMEOUT,
2022-09-28 18:50:37 +02:00
) -> bytearray:
req = BluetoothGATTReadRequest()
req.address = address
req.handle = handle
2022-09-28 18:50:37 +02:00
resp = await self._send_bluetooth_message_await_response(
address,
handle,
req,
BluetoothGATTReadResponse,
timeout=timeout,
2022-09-28 18:50:37 +02:00
)
read_response = BluetoothGATTRead.from_pb(resp)
2022-09-28 18:50:37 +02:00
return bytearray(read_response.data)
async def bluetooth_gatt_write(
self,
address: int,
handle: int,
2022-09-28 18:50:37 +02:00
data: bytes,
response: bool,
2022-11-06 20:32:32 +01:00
timeout: float = DEFAULT_BLE_TIMEOUT,
2022-09-28 18:50:37 +02:00
) -> None:
req = BluetoothGATTWriteRequest()
req.address = address
req.handle = handle
2022-09-28 18:50:37 +02:00
req.response = response
req.data = data
await self._send_bluetooth_message_await_response(
address,
handle,
req,
BluetoothGATTWriteResponse,
timeout=timeout,
)
2022-09-28 18:50:37 +02:00
async def bluetooth_gatt_read_descriptor(
self,
address: int,
handle: int,
timeout: float = DEFAULT_BLE_TIMEOUT,
2022-09-28 18:50:37 +02:00
) -> bytearray:
req = BluetoothGATTReadDescriptorRequest()
req.address = address
req.handle = handle
resp = await self._send_bluetooth_message_await_response(
address,
handle,
req,
BluetoothGATTReadResponse,
timeout=timeout,
2022-09-28 18:50:37 +02:00
)
read_response = BluetoothGATTRead.from_pb(resp)
2022-09-28 18:50:37 +02:00
return bytearray(read_response.data)
async def bluetooth_gatt_write_descriptor(
self,
address: int,
handle: int,
data: bytes,
2022-11-06 20:32:32 +01:00
timeout: float = DEFAULT_BLE_TIMEOUT,
2022-11-29 03:06:13 +01:00
wait_for_response: bool = True,
2022-09-28 18:50:37 +02:00
) -> None:
req = BluetoothGATTWriteDescriptorRequest()
req.address = address
req.handle = handle
req.data = data
2022-11-29 03:06:13 +01:00
if not wait_for_response:
assert self._connection is not None
await self._connection.send_message(req)
return
await self._send_bluetooth_message_await_response(
address,
handle,
req,
BluetoothGATTWriteResponse,
timeout=timeout,
)
2022-09-28 18:50:37 +02:00
async def bluetooth_gatt_start_notify(
self,
address: int,
handle: int,
on_bluetooth_gatt_notify: Callable[[int, bytearray], None],
) -> Callable[[], Coroutine[Any, Any, None]]:
await self._send_bluetooth_message_await_response(
address,
handle,
BluetoothGATTNotifyRequest(address=address, handle=handle, enable=True),
BluetoothGATTNotifyResponse,
)
2022-09-28 18:50:37 +02:00
def on_msg(msg: message.Message) -> None:
if isinstance(msg, BluetoothGATTNotifyDataResponse):
notify = BluetoothGATTRead.from_pb(msg)
if address == notify.address and handle == notify.handle:
on_bluetooth_gatt_notify(handle, bytearray(notify.data))
assert self._connection is not None
remove_callback = self._connection.add_message_callback(on_msg)
2022-09-28 18:50:37 +02:00
async def stop_notify() -> None:
if self._connection is None:
return
remove_callback()
2022-09-28 18:50:37 +02:00
self._check_authenticated()
await self._connection.send_message(
BluetoothGATTNotifyRequest(address=address, handle=handle, enable=False)
)
return stop_notify
async def subscribe_home_assistant_states(
self, on_state_sub: Callable[[str, Optional[str]], None]
) -> None:
2018-12-18 14:53:52 +01:00
self._check_authenticated()
def on_msg(msg: message.Message) -> None:
if isinstance(msg, SubscribeHomeAssistantStateResponse):
on_state_sub(msg.entity_id, msg.attribute)
2018-12-18 14:53:52 +01:00
assert self._connection is not None
await self._connection.send_message_callback_response(
SubscribeHomeAssistantStatesRequest(), on_msg
)
2018-12-18 14:53:52 +01:00
async def send_home_assistant_state(
self, entity_id: str, attribute: Optional[str], state: str
) -> None:
2018-12-18 14:53:52 +01:00
self._check_authenticated()
assert self._connection is not None
await self._connection.send_message(
HomeAssistantStateResponse(
entity_id=entity_id,
state=state,
attribute=attribute,
)
)
2018-12-18 14:53:52 +01:00
async def cover_command(
self,
key: int,
position: Optional[float] = None,
tilt: Optional[float] = None,
stop: bool = False,
) -> None:
2018-12-13 21:34:57 +01:00
self._check_authenticated()
req = CoverCommandRequest()
2018-12-13 21:34:57 +01:00
req.key = key
apiv = cast(APIVersion, self.api_version)
if apiv >= APIVersion(1, 1):
2019-04-07 19:03:26 +02:00
if position is not None:
req.has_position = True
req.position = position
if tilt is not None:
req.has_tilt = True
req.tilt = tilt
if stop:
req.stop = stop
else:
if stop:
req.legacy_command = LegacyCoverCommand.STOP
2021-07-12 20:09:17 +02:00
req.has_legacy_command = True
2019-04-07 19:03:26 +02:00
elif position == 1.0:
req.legacy_command = LegacyCoverCommand.OPEN
2021-07-12 20:09:17 +02:00
req.has_legacy_command = True
elif position == 0.0:
2019-04-07 19:03:26 +02:00
req.legacy_command = LegacyCoverCommand.CLOSE
2021-07-12 20:09:17 +02:00
req.has_legacy_command = True
assert self._connection is not None
await self._connection.send_message(req)
2018-12-13 21:34:57 +01:00
async def fan_command(
self,
key: int,
state: Optional[bool] = None,
speed: Optional[FanSpeed] = None,
speed_level: Optional[int] = None,
oscillating: Optional[bool] = None,
direction: Optional[FanDirection] = None,
) -> None:
2018-12-13 21:34:57 +01:00
self._check_authenticated()
req = FanCommandRequest()
2018-12-13 21:34:57 +01:00
req.key = key
if state is not None:
req.has_state = True
req.state = state
if speed is not None:
req.has_speed = True
req.speed = speed
if speed_level is not None:
req.has_speed_level = True
req.speed_level = speed_level
2018-12-13 21:34:57 +01:00
if oscillating is not None:
req.has_oscillating = True
req.oscillating = oscillating
2020-12-14 04:16:37 +01:00
if direction is not None:
req.has_direction = True
req.direction = direction
assert self._connection is not None
await self._connection.send_message(req)
2018-12-13 21:34:57 +01:00
async def light_command(
self,
key: int,
state: Optional[bool] = None,
brightness: Optional[float] = None,
color_mode: Optional[int] = None,
color_brightness: Optional[float] = None,
rgb: Optional[Tuple[float, float, float]] = None,
white: Optional[float] = None,
color_temperature: Optional[float] = None,
cold_white: Optional[float] = None,
warm_white: Optional[float] = None,
transition_length: Optional[float] = None,
flash_length: Optional[float] = None,
effect: Optional[str] = None,
) -> None:
2018-12-13 21:34:57 +01:00
self._check_authenticated()
req = LightCommandRequest()
2018-12-13 21:34:57 +01:00
req.key = key
if state is not None:
req.has_state = True
req.state = state
if brightness is not None:
req.has_brightness = True
req.brightness = brightness
if color_mode is not None:
req.has_color_mode = True
req.color_mode = color_mode
if color_brightness is not None:
req.has_color_brightness = True
req.color_brightness = color_brightness
2018-12-13 21:34:57 +01:00
if rgb is not None:
req.has_rgb = True
req.red = rgb[0]
req.green = rgb[1]
req.blue = rgb[2]
if white is not None:
req.has_white = True
req.white = white
if color_temperature is not None:
req.has_color_temperature = True
req.color_temperature = color_temperature
if cold_white is not None:
req.has_cold_white = True
req.cold_white = cold_white
if warm_white is not None:
req.has_warm_white = True
req.warm_white = warm_white
2018-12-13 21:34:57 +01:00
if transition_length is not None:
req.has_transition_length = True
req.transition_length = int(round(transition_length * 1000))
2018-12-13 21:34:57 +01:00
if flash_length is not None:
req.has_flash_length = True
req.flash_length = int(round(flash_length * 1000))
2018-12-13 21:34:57 +01:00
if effect is not None:
req.has_effect = True
req.effect = effect
assert self._connection is not None
await self._connection.send_message(req)
2018-12-13 21:34:57 +01:00
async def switch_command(self, key: int, state: bool) -> None:
2018-12-13 21:34:57 +01:00
self._check_authenticated()
req = SwitchCommandRequest()
2018-12-13 21:34:57 +01:00
req.key = key
req.state = state
assert self._connection is not None
await self._connection.send_message(req)
2019-02-24 18:16:12 +01:00
async def climate_command(
self,
key: int,
mode: Optional[ClimateMode] = None,
target_temperature: Optional[float] = None,
target_temperature_low: Optional[float] = None,
target_temperature_high: Optional[float] = None,
fan_mode: Optional[ClimateFanMode] = None,
swing_mode: Optional[ClimateSwingMode] = None,
custom_fan_mode: Optional[str] = None,
preset: Optional[ClimatePreset] = None,
custom_preset: Optional[str] = None,
) -> None:
2019-03-27 22:10:33 +01:00
self._check_authenticated()
req = ClimateCommandRequest()
2019-03-27 22:10:33 +01:00
req.key = key
if mode is not None:
req.has_mode = True
req.mode = mode
if target_temperature is not None:
req.has_target_temperature = True
req.target_temperature = target_temperature
if target_temperature_low is not None:
req.has_target_temperature_low = True
req.target_temperature_low = target_temperature_low
if target_temperature_high is not None:
req.has_target_temperature_high = True
req.target_temperature_high = target_temperature_high
if fan_mode is not None:
req.has_fan_mode = True
req.fan_mode = fan_mode
if swing_mode is not None:
req.has_swing_mode = True
req.swing_mode = swing_mode
if custom_fan_mode is not None:
req.has_custom_fan_mode = True
req.custom_fan_mode = custom_fan_mode
if preset is not None:
apiv = cast(APIVersion, self.api_version)
if apiv < APIVersion(1, 5):
req.has_legacy_away = True
req.legacy_away = preset == ClimatePreset.AWAY
else:
req.has_preset = True
req.preset = preset
if custom_preset is not None:
req.has_custom_preset = True
req.custom_preset = custom_preset
assert self._connection is not None
2019-03-27 22:10:33 +01:00
await self._connection.send_message(req)
2021-06-29 12:42:38 +02:00
async def number_command(self, key: int, state: float) -> None:
self._check_authenticated()
req = NumberCommandRequest()
req.key = key
req.state = state
assert self._connection is not None
await self._connection.send_message(req)
2021-07-26 20:51:12 +02:00
async def select_command(self, key: int, state: str) -> None:
self._check_authenticated()
req = SelectCommandRequest()
req.key = key
req.state = state
assert self._connection is not None
await self._connection.send_message(req)
2021-09-09 03:11:51 +02:00
async def siren_command(
self,
key: int,
state: Optional[bool] = None,
tone: Optional[str] = None,
volume: Optional[float] = None,
duration: Optional[int] = None,
) -> None:
self._check_authenticated()
req = SirenCommandRequest()
req.key = key
if state is not None:
req.state = state
req.has_state = True
if tone is not None:
req.tone = tone
req.has_tone = True
if volume is not None:
req.volume = volume
req.has_volume = True
if duration is not None:
req.duration = duration
req.has_duration = True
assert self._connection is not None
await self._connection.send_message(req)
2021-11-29 01:59:23 +01:00
async def button_command(self, key: int) -> None:
self._check_authenticated()
req = ButtonCommandRequest()
req.key = key
assert self._connection is not None
await self._connection.send_message(req)
2022-01-11 02:29:19 +01:00
async def lock_command(
self,
key: int,
command: LockCommand,
code: Optional[str] = None,
) -> None:
self._check_authenticated()
req = LockCommandRequest()
req.key = key
req.command = command
if code is not None:
req.code = code
assert self._connection is not None
await self._connection.send_message(req)
2022-05-18 03:28:40 +02:00
async def media_player_command(
self,
key: int,
*,
command: Optional[MediaPlayerCommand] = None,
volume: Optional[float] = None,
media_url: Optional[str] = None,
) -> None:
self._check_authenticated()
req = MediaPlayerCommandRequest()
req.key = key
if command is not None:
req.command = command
req.has_command = True
if volume is not None:
req.volume = volume
req.has_volume = True
if media_url is not None:
req.media_url = media_url
req.has_media_url = True
assert self._connection is not None
await self._connection.send_message(req)
async def execute_service(
self, service: UserService, data: ExecuteServiceDataType
) -> None:
2019-02-24 18:16:12 +01:00
self._check_authenticated()
req = ExecuteServiceRequest()
2019-02-24 18:16:12 +01:00
req.key = service.key
args = []
for arg_desc in service.args:
arg = ExecuteServiceArgument()
2019-02-24 18:16:12 +01:00
val = data[arg_desc.name]
apiv = cast(APIVersion, self.api_version)
int_type = "int_" if apiv >= APIVersion(1, 3) else "legacy_int"
map_single = {
UserServiceArgType.BOOL: "bool_",
UserServiceArgType.INT: int_type,
UserServiceArgType.FLOAT: "float_",
UserServiceArgType.STRING: "string_",
}
map_array = {
UserServiceArgType.BOOL_ARRAY: "bool_array",
UserServiceArgType.INT_ARRAY: "int_array",
UserServiceArgType.FLOAT_ARRAY: "float_array",
UserServiceArgType.STRING_ARRAY: "string_array",
}
2021-06-29 15:36:14 +02:00
if arg_desc.type in map_array:
attr = getattr(arg, map_array[arg_desc.type])
attr.extend(val)
else:
2021-06-29 15:36:14 +02:00
assert arg_desc.type in map_single
setattr(arg, map_single[arg_desc.type], val)
2019-02-24 18:16:12 +01:00
args.append(arg)
2020-07-14 20:00:12 +02:00
# pylint: disable=no-member
2019-02-24 18:16:12 +01:00
req.args.extend(args)
assert self._connection is not None
2019-02-24 18:16:12 +01:00
await self._connection.send_message(req)
2019-03-09 11:02:44 +01:00
async def _request_image(
self, *, single: bool = False, stream: bool = False
) -> None:
req = CameraImageRequest()
2019-03-09 11:02:44 +01:00
req.single = single
req.stream = stream
assert self._connection is not None
2019-03-09 11:02:44 +01:00
await self._connection.send_message(req)
async def request_single_image(self) -> None:
2019-03-09 11:02:44 +01:00
await self._request_image(single=True)
async def request_image_stream(self) -> None:
2019-03-09 11:02:44 +01:00
await self._request_image(stream=True)
2019-04-07 19:03:26 +02:00
@property
def api_version(self) -> Optional[APIVersion]:
if self._connection is None:
return None
return self._connection.api_version