aioesphomeapi/aioesphomeapi/client.py

573 lines
18 KiB
Python
Raw Normal View History

import asyncio
2018-12-13 21:34:57 +01:00
import logging
from typing import (
Any,
Awaitable,
Callable,
Dict,
List,
Optional,
Tuple,
2021-06-29 15:36:14 +02:00
Type,
Union,
cast,
)
2018-12-13 21:34:57 +01:00
import zeroconf
from google.protobuf import message
from aioesphomeapi.api_pb2 import ( # type: ignore
BinarySensorStateResponse,
CameraImageRequest,
CameraImageResponse,
ClimateCommandRequest,
ClimateStateResponse,
CoverCommandRequest,
CoverStateResponse,
DeviceInfoRequest,
DeviceInfoResponse,
ExecuteServiceArgument,
ExecuteServiceRequest,
FanCommandRequest,
FanStateResponse,
HomeassistantServiceResponse,
HomeAssistantStateResponse,
LightCommandRequest,
LightStateResponse,
ListEntitiesBinarySensorResponse,
ListEntitiesCameraResponse,
ListEntitiesClimateResponse,
ListEntitiesCoverResponse,
ListEntitiesDoneResponse,
ListEntitiesFanResponse,
ListEntitiesLightResponse,
2021-06-29 12:42:38 +02:00
ListEntitiesNumberResponse,
ListEntitiesRequest,
ListEntitiesSensorResponse,
ListEntitiesServicesResponse,
ListEntitiesSwitchResponse,
ListEntitiesTextSensorResponse,
2021-06-29 12:42:38 +02:00
NumberCommandRequest,
NumberStateResponse,
SensorStateResponse,
SubscribeHomeassistantServicesRequest,
SubscribeHomeAssistantStateResponse,
SubscribeHomeAssistantStatesRequest,
SubscribeLogsRequest,
SubscribeLogsResponse,
SubscribeStatesRequest,
SwitchCommandRequest,
SwitchStateResponse,
TextSensorStateResponse,
)
2019-04-07 19:03:26 +02:00
from aioesphomeapi.connection import APIConnection, ConnectionParams
from aioesphomeapi.core import APIConnectionError
from aioesphomeapi.model import (
APIVersion,
BinarySensorInfo,
BinarySensorState,
CameraInfo,
CameraState,
ClimateFanMode,
ClimateInfo,
ClimateMode,
ClimatePreset,
ClimateState,
ClimateSwingMode,
CoverInfo,
CoverState,
DeviceInfo,
EntityInfo,
2021-06-29 15:36:14 +02:00
EntityState,
FanDirection,
FanInfo,
FanSpeed,
FanState,
HomeassistantServiceCall,
LegacyCoverCommand,
LightInfo,
LightState,
2021-06-29 15:36:14 +02:00
LogLevel,
2021-06-29 12:42:38 +02:00
NumberInfo,
NumberState,
SensorInfo,
SensorState,
SwitchInfo,
SwitchState,
TextSensorInfo,
TextSensorState,
UserService,
UserServiceArgType,
)
2018-12-13 21:34:57 +01:00
_LOGGER = logging.getLogger(__name__)
ExecuteServiceDataType = Dict[
str, Union[bool, int, float, str, List[bool], List[int], List[float], List[str]]
]
2018-12-13 21:34:57 +01:00
class APIClient:
def __init__(
self,
eventloop: asyncio.AbstractEventLoop,
address: str,
port: int,
password: str,
*,
client_info: str = "aioesphomeapi",
keepalive: float = 15.0,
zeroconf_instance: Optional[zeroconf.Zeroconf] = None
):
self._params = ConnectionParams(
eventloop=eventloop,
address=address,
port=port,
password=password,
client_info=client_info,
keepalive=keepalive,
zeroconf_instance=zeroconf_instance,
)
self._connection = None # type: Optional[APIConnection]
async def connect(
self,
on_stop: Optional[Callable[[], Awaitable[None]]] = None,
login: bool = False,
) -> None:
if self._connection is not None:
raise APIConnectionError("Already connected!")
connected = False
2019-01-19 15:26:29 +01:00
stopped = False
async def _on_stop() -> None:
2019-01-19 15:26:29 +01:00
nonlocal stopped
if stopped:
return
2019-01-19 15:26:29 +01:00
stopped = True
self._connection = None
if connected and on_stop is not None:
await on_stop()
self._connection = APIConnection(self._params, _on_stop)
try:
await self._connection.connect()
if login:
await self._connection.login()
except APIConnectionError:
await _on_stop()
raise
except Exception as e:
await _on_stop()
raise APIConnectionError("Unexpected error while connecting: {}".format(e))
connected = True
async def disconnect(self, force: bool = False) -> None:
if self._connection is None:
return
await self._connection.stop(force=force)
def _check_connected(self) -> None:
if self._connection is None:
raise APIConnectionError("Not connected!")
if not self._connection.is_connected:
raise APIConnectionError("Connection not done!")
def _check_authenticated(self) -> None:
self._check_connected()
assert self._connection is not None
if not self._connection.is_authenticated:
raise APIConnectionError("Not authenticated!")
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-29 15:36:14 +02:00
return DeviceInfo.from_pb(resp)
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,
ListEntitiesCoverResponse: CoverInfo,
ListEntitiesFanResponse: FanInfo,
ListEntitiesLightResponse: LightInfo,
2021-06-29 12:42:38 +02:00
ListEntitiesNumberResponse: NumberInfo,
ListEntitiesSensorResponse: SensorInfo,
ListEntitiesSwitchResponse: SwitchInfo,
ListEntitiesTextSensorResponse: TextSensorInfo,
ListEntitiesServicesResponse: None,
ListEntitiesCameraResponse: CameraInfo,
ListEntitiesClimateResponse: ClimateInfo,
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=5
)
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,
SensorStateResponse: SensorState,
SwitchStateResponse: SwitchState,
TextSensorStateResponse: TextSensorState,
ClimateStateResponse: ClimateState,
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-06-29 15:36:14 +02:00
on_state(CameraState.from_pb(msg))
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,
) -> 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
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 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:
req.has_legacy_command = True
if stop:
req.legacy_command = LegacyCoverCommand.STOP
elif position == 1.0:
req.legacy_command = LegacyCoverCommand.OPEN
else:
req.legacy_command = LegacyCoverCommand.CLOSE
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,
rgb: Optional[Tuple[float, float, float]] = None,
white: Optional[float] = None,
color_temperature: 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 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 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)
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