aioesphomeapi/aioesphomeapi/client.py

564 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, Union, cast
2018-12-13 21:34:57 +01:00
import attr
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,
ListEntitiesRequest,
ListEntitiesSensorResponse,
ListEntitiesServicesResponse,
ListEntitiesSwitchResponse,
ListEntitiesTextSensorResponse,
LogLevel,
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,
ClimateState,
ClimateSwingMode,
CoverInfo,
CoverState,
DeviceInfo,
EntityInfo,
FanDirection,
FanInfo,
FanSpeed,
FanState,
HomeassistantServiceCall,
LegacyCoverCommand,
LightInfo,
LightState,
SensorInfo,
SensorState,
SwitchInfo,
SwitchState,
TextSensorInfo,
TextSensorState,
UserService,
UserServiceArg,
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
)
return DeviceInfo(
uses_password=resp.uses_password,
name=resp.name,
mac_address=resp.mac_address,
esphome_version=resp.esphome_version,
compilation_time=resp.compilation_time,
model=resp.model,
has_deep_sleep=resp.has_deep_sleep,
)
async def list_entities_services(
self,
) -> Tuple[List[EntityInfo], List[UserService]]:
2018-12-13 21:34:57 +01:00
self._check_authenticated()
response_types = {
ListEntitiesBinarySensorResponse: BinarySensorInfo,
ListEntitiesCoverResponse: CoverInfo,
ListEntitiesFanResponse: FanInfo,
ListEntitiesLightResponse: LightInfo,
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):
2019-02-24 18:16:12 +01:00
args = []
for arg in msg.args:
args.append(
UserServiceArg(
name=arg.name,
type_=arg.type,
)
)
services.append(
UserService(
name=msg.name,
key=msg.key,
args=args, # type: ignore
)
)
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
cls = cast(type, cls)
2018-12-13 21:34:57 +01:00
kwargs = {}
for key, _ in attr.fields_dict(cls).items():
kwargs[key] = getattr(msg, key)
entities.append(cls(**kwargs))
2019-02-24 18:16:12 +01:00
return entities, services
2018-12-13 21:34:57 +01:00
async def subscribe_states(self, on_state: Callable[[Any], None]) -> None:
self._check_authenticated()
response_types = {
BinarySensorStateResponse: BinarySensorState,
CoverStateResponse: CoverState,
FanStateResponse: FanState,
LightStateResponse: LightState,
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:
on_state(CameraState(key=msg.key, image=data))
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
kwargs = {}
2020-07-14 20:00:12 +02:00
# pylint: disable=undefined-loop-variable
2018-12-13 21:34:57 +01:00
for key, _ in attr.fields_dict(cls).items():
kwargs[key] = getattr(msg, key)
on_state(cls(**kwargs))
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],
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):
2018-12-16 18:03:03 +01:00
kwargs = {}
for key, _ in attr.fields_dict(HomeassistantServiceCall).items():
2018-12-16 18:03:03 +01:00
kwargs[key] = getattr(msg, key)
on_service_call(HomeassistantServiceCall(**kwargs))
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,
away: Optional[bool] = None,
fan_mode: Optional[ClimateFanMode] = None,
swing_mode: Optional[ClimateSwingMode] = 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 away is not None:
req.has_away = True
req.away = away
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
assert self._connection is not None
2019-03-27 22:10:33 +01:00
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",
}
2020-07-14 20:00:12 +02:00
# pylint: disable=redefined-outer-name
if arg_desc.type_ in map_array:
attr = getattr(arg, map_array[arg_desc.type_])
attr.extend(val)
else:
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