2021-06-18 17:57:02 +02:00
|
|
|
import asyncio
|
2018-12-13 21:34:57 +01:00
|
|
|
import logging
|
2021-06-24 09:55:35 +02:00
|
|
|
from typing import (
|
|
|
|
TYPE_CHECKING,
|
|
|
|
Any,
|
|
|
|
Awaitable,
|
|
|
|
Callable,
|
|
|
|
Dict,
|
|
|
|
List,
|
|
|
|
Optional,
|
|
|
|
Tuple,
|
|
|
|
Union,
|
|
|
|
cast,
|
|
|
|
)
|
2018-12-13 21:34:57 +01:00
|
|
|
|
2021-06-18 17:57:02 +02: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,
|
2021-06-29 12:42:38 +02:00
|
|
|
ListEntitiesNumberResponse,
|
2021-06-18 17:57:02 +02:00
|
|
|
ListEntitiesRequest,
|
|
|
|
ListEntitiesSensorResponse,
|
|
|
|
ListEntitiesServicesResponse,
|
|
|
|
ListEntitiesSwitchResponse,
|
|
|
|
ListEntitiesTextSensorResponse,
|
2021-06-29 12:42:38 +02:00
|
|
|
NumberCommandRequest,
|
|
|
|
NumberStateResponse,
|
2021-06-18 17:57:02 +02:00
|
|
|
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
|
2021-06-18 17:57:02 +02:00
|
|
|
from aioesphomeapi.model import (
|
|
|
|
APIVersion,
|
|
|
|
BinarySensorInfo,
|
|
|
|
BinarySensorState,
|
|
|
|
CameraInfo,
|
|
|
|
CameraState,
|
|
|
|
ClimateFanMode,
|
|
|
|
ClimateInfo,
|
|
|
|
ClimateMode,
|
2021-06-23 23:40:41 +02:00
|
|
|
ClimatePreset,
|
2021-06-18 17:57:02 +02:00
|
|
|
ClimateState,
|
|
|
|
ClimateSwingMode,
|
|
|
|
CoverInfo,
|
|
|
|
CoverState,
|
|
|
|
DeviceInfo,
|
|
|
|
EntityInfo,
|
|
|
|
FanDirection,
|
|
|
|
FanInfo,
|
|
|
|
FanSpeed,
|
|
|
|
FanState,
|
|
|
|
HomeassistantServiceCall,
|
|
|
|
LegacyCoverCommand,
|
|
|
|
LightInfo,
|
|
|
|
LightState,
|
2021-06-29 12:42:38 +02:00
|
|
|
NumberInfo,
|
|
|
|
NumberState,
|
2021-06-18 17:57:02 +02:00
|
|
|
SensorInfo,
|
|
|
|
SensorState,
|
|
|
|
SwitchInfo,
|
|
|
|
SwitchState,
|
|
|
|
TextSensorInfo,
|
|
|
|
TextSensorState,
|
|
|
|
UserService,
|
|
|
|
UserServiceArg,
|
|
|
|
UserServiceArgType,
|
|
|
|
)
|
2018-12-13 21:34:57 +01:00
|
|
|
|
2021-06-24 09:55:35 +02:00
|
|
|
if TYPE_CHECKING:
|
|
|
|
from aioesphomeapi.api_pb2 import LogLevel # type: ignore
|
|
|
|
|
2018-12-13 21:34:57 +01:00
|
|
|
_LOGGER = logging.getLogger(__name__)
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
ExecuteServiceDataType = Dict[
|
|
|
|
str, Union[bool, int, float, str, List[bool], List[int], List[float], List[str]]
|
|
|
|
]
|
|
|
|
|
2018-12-13 21:34:57 +01:00
|
|
|
|
2019-01-04 18:35:38 +01:00
|
|
|
class APIClient:
|
2021-06-18 17:57:02 +02:00
|
|
|
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
|
|
|
|
):
|
2019-01-04 18:35:38 +01:00
|
|
|
self._params = ConnectionParams(
|
|
|
|
eventloop=eventloop,
|
|
|
|
address=address,
|
|
|
|
port=port,
|
|
|
|
password=password,
|
|
|
|
client_info=client_info,
|
|
|
|
keepalive=keepalive,
|
2021-06-18 17:57:02 +02:00
|
|
|
zeroconf_instance=zeroconf_instance,
|
2019-01-04 18:35:38 +01:00
|
|
|
)
|
|
|
|
self._connection = None # type: Optional[APIConnection]
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
async def connect(
|
|
|
|
self,
|
|
|
|
on_stop: Optional[Callable[[], Awaitable[None]]] = None,
|
|
|
|
login: bool = False,
|
|
|
|
) -> None:
|
2019-01-04 18:35:38 +01:00
|
|
|
if self._connection is not None:
|
|
|
|
raise APIConnectionError("Already connected!")
|
|
|
|
|
|
|
|
connected = False
|
2019-01-19 15:26:29 +01:00
|
|
|
stopped = False
|
2019-01-04 18:35:38 +01:00
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
async def _on_stop() -> None:
|
2019-01-19 15:26:29 +01:00
|
|
|
nonlocal stopped
|
|
|
|
|
|
|
|
if stopped:
|
2019-01-04 18:35:38 +01:00
|
|
|
return
|
2019-01-19 15:26:29 +01:00
|
|
|
stopped = True
|
2019-01-04 18:35:38 +01:00
|
|
|
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()
|
2021-06-18 17:57:02 +02:00
|
|
|
raise APIConnectionError("Unexpected error while connecting: {}".format(e))
|
2019-01-04 18:35:38 +01:00
|
|
|
|
|
|
|
connected = True
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
async def disconnect(self, force: bool = False) -> None:
|
2019-01-04 18:35:38 +01:00
|
|
|
if self._connection is None:
|
|
|
|
return
|
|
|
|
await self._connection.stop(force=force)
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
def _check_connected(self) -> None:
|
2019-01-04 18:35:38 +01:00
|
|
|
if self._connection is None:
|
|
|
|
raise APIConnectionError("Not connected!")
|
|
|
|
if not self._connection.is_connected:
|
|
|
|
raise APIConnectionError("Connection not done!")
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
def _check_authenticated(self) -> None:
|
2019-01-04 18:35:38 +01:00
|
|
|
self._check_connected()
|
2021-06-18 17:57:02 +02:00
|
|
|
assert self._connection is not None
|
2019-01-04 18:35:38 +01:00
|
|
|
if not self._connection.is_authenticated:
|
|
|
|
raise APIConnectionError("Not authenticated!")
|
|
|
|
|
|
|
|
async def device_info(self) -> DeviceInfo:
|
|
|
|
self._check_connected()
|
2021-06-18 17:57:02 +02:00
|
|
|
assert self._connection is not None
|
2019-01-04 18:35:38 +01:00
|
|
|
resp = await self._connection.send_message_await_response(
|
2021-06-18 17:57:02 +02:00
|
|
|
DeviceInfoRequest(), DeviceInfoResponse
|
|
|
|
)
|
2019-01-04 18:35:38 +01:00
|
|
|
return DeviceInfo(
|
|
|
|
uses_password=resp.uses_password,
|
|
|
|
name=resp.name,
|
|
|
|
mac_address=resp.mac_address,
|
2019-06-17 23:40:23 +02:00
|
|
|
esphome_version=resp.esphome_version,
|
2019-01-04 18:35:38 +01:00
|
|
|
compilation_time=resp.compilation_time,
|
|
|
|
model=resp.model,
|
|
|
|
has_deep_sleep=resp.has_deep_sleep,
|
|
|
|
)
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
async def list_entities_services(
|
|
|
|
self,
|
|
|
|
) -> Tuple[List[EntityInfo], List[UserService]]:
|
2018-12-13 21:34:57 +01:00
|
|
|
self._check_authenticated()
|
|
|
|
response_types = {
|
2021-06-18 17:57:02 +02:00
|
|
|
ListEntitiesBinarySensorResponse: BinarySensorInfo,
|
|
|
|
ListEntitiesCoverResponse: CoverInfo,
|
|
|
|
ListEntitiesFanResponse: FanInfo,
|
|
|
|
ListEntitiesLightResponse: LightInfo,
|
2021-06-29 12:42:38 +02:00
|
|
|
ListEntitiesNumberResponse: NumberInfo,
|
2021-06-18 17:57:02 +02:00
|
|
|
ListEntitiesSensorResponse: SensorInfo,
|
|
|
|
ListEntitiesSwitchResponse: SwitchInfo,
|
|
|
|
ListEntitiesTextSensorResponse: TextSensorInfo,
|
|
|
|
ListEntitiesServicesResponse: None,
|
|
|
|
ListEntitiesCameraResponse: CameraInfo,
|
|
|
|
ListEntitiesClimateResponse: ClimateInfo,
|
2018-12-13 21:34:57 +01:00
|
|
|
}
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
def do_append(msg: message.Message) -> bool:
|
2018-12-13 21:34:57 +01:00
|
|
|
return isinstance(msg, tuple(response_types.keys()))
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
def do_stop(msg: message.Message) -> bool:
|
|
|
|
return isinstance(msg, ListEntitiesDoneResponse)
|
2018-12-13 21:34:57 +01:00
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
assert self._connection is not None
|
2019-01-04 18:35:38 +01:00
|
|
|
resp = await self._connection.send_message_await_response_complex(
|
2021-06-18 17:57:02 +02:00
|
|
|
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:
|
2021-06-18 17:57:02 +02:00
|
|
|
if isinstance(msg, ListEntitiesServicesResponse):
|
2019-02-24 18:16:12 +01:00
|
|
|
args = []
|
|
|
|
for arg in msg.args:
|
2021-06-18 17:57:02 +02:00
|
|
|
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
|
2021-06-18 17:57:02 +02:00
|
|
|
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 = {
|
2021-06-18 17:57:02 +02:00
|
|
|
BinarySensorStateResponse: BinarySensorState,
|
|
|
|
CoverStateResponse: CoverState,
|
|
|
|
FanStateResponse: FanState,
|
|
|
|
LightStateResponse: LightState,
|
2021-06-29 12:42:38 +02:00
|
|
|
NumberStateResponse: NumberState,
|
2021-06-18 17:57:02 +02:00
|
|
|
SensorStateResponse: SensorState,
|
|
|
|
SwitchStateResponse: SwitchState,
|
|
|
|
TextSensorStateResponse: TextSensorState,
|
|
|
|
ClimateStateResponse: ClimateState,
|
2018-12-13 21:34:57 +01:00
|
|
|
}
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
image_stream: Dict[int, bytes] = {}
|
2019-01-19 15:10:00 +01:00
|
|
|
|
2021-06-18 17:57:02 +02: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))
|
|
|
|
|
2021-06-18 17:57:02 +02: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
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
async def subscribe_logs(
|
|
|
|
self,
|
|
|
|
on_log: Callable[[SubscribeLogsResponse], None],
|
2021-06-24 09:55:35 +02:00
|
|
|
log_level: Optional["LogLevel"] = None,
|
2021-06-18 17:57:02 +02:00
|
|
|
) -> None:
|
2018-12-13 21:34:57 +01:00
|
|
|
self._check_authenticated()
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
def on_msg(msg: message.Message) -> None:
|
|
|
|
if isinstance(msg, SubscribeLogsResponse):
|
2018-12-13 21:34:57 +01:00
|
|
|
on_log(msg)
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
req = SubscribeLogsRequest()
|
2018-12-13 21:34:57 +01:00
|
|
|
if log_level is not None:
|
|
|
|
req.level = log_level
|
2021-06-18 17:57:02 +02:00
|
|
|
assert self._connection is not None
|
2019-01-04 18:35:38 +01:00
|
|
|
await self._connection.send_message_callback_response(req, on_msg)
|
2018-12-13 21:34:57 +01:00
|
|
|
|
2021-06-18 17:57:02 +02: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()
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
def on_msg(msg: message.Message) -> None:
|
|
|
|
if isinstance(msg, HomeassistantServiceResponse):
|
2018-12-16 18:03:03 +01:00
|
|
|
kwargs = {}
|
2019-06-18 11:10:32 +02:00
|
|
|
for key, _ in attr.fields_dict(HomeassistantServiceCall).items():
|
2018-12-16 18:03:03 +01:00
|
|
|
kwargs[key] = getattr(msg, key)
|
2019-06-18 11:10:32 +02:00
|
|
|
on_service_call(HomeassistantServiceCall(**kwargs))
|
2018-12-16 18:03:03 +01:00
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
assert self._connection is not None
|
2021-05-12 10:57:01 +02:00
|
|
|
await self._connection.send_message_callback_response(
|
2021-06-18 17:57:02 +02:00
|
|
|
SubscribeHomeassistantServicesRequest(), on_msg
|
2021-05-12 10:57:01 +02:00
|
|
|
)
|
2018-12-16 18:03:03 +01:00
|
|
|
|
2021-05-12 10:57:01 +02: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()
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
def on_msg(msg: message.Message) -> None:
|
|
|
|
if isinstance(msg, SubscribeHomeAssistantStateResponse):
|
2021-05-12 10:57:01 +02:00
|
|
|
on_state_sub(msg.entity_id, msg.attribute)
|
2018-12-18 14:53:52 +01:00
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
assert self._connection is not None
|
2019-01-04 18:35:38 +01:00
|
|
|
await self._connection.send_message_callback_response(
|
2021-06-18 17:57:02 +02:00
|
|
|
SubscribeHomeAssistantStatesRequest(), on_msg
|
2021-05-12 10:57:01 +02:00
|
|
|
)
|
2018-12-18 14:53:52 +01:00
|
|
|
|
2021-05-12 10:57:01 +02: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()
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
assert self._connection is not None
|
2021-05-12 10:57:01 +02:00
|
|
|
await self._connection.send_message(
|
2021-06-18 17:57:02 +02:00
|
|
|
HomeAssistantStateResponse(
|
2021-05-12 10:57:01 +02:00
|
|
|
entity_id=entity_id,
|
|
|
|
state=state,
|
|
|
|
attribute=attribute,
|
|
|
|
)
|
|
|
|
)
|
2018-12-18 14:53:52 +01:00
|
|
|
|
2021-06-18 17:57:02 +02: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()
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
req = CoverCommandRequest()
|
2018-12-13 21:34:57 +01:00
|
|
|
req.key = key
|
2021-06-18 17:57:02 +02:00
|
|
|
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
|
2021-06-18 17:57:02 +02:00
|
|
|
assert self._connection is not None
|
2019-01-04 18:35:38 +01:00
|
|
|
await self._connection.send_message(req)
|
2018-12-13 21:34:57 +01:00
|
|
|
|
2021-06-18 17:57:02 +02: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()
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
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
|
2021-03-14 01:21:09 +01:00
|
|
|
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
|
2021-06-18 17:57:02 +02:00
|
|
|
assert self._connection is not None
|
2019-01-04 18:35:38 +01:00
|
|
|
await self._connection.send_message(req)
|
2018-12-13 21:34:57 +01:00
|
|
|
|
2021-06-18 17:57:02 +02: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()
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
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
|
2019-01-19 15:25:31 +01:00
|
|
|
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
|
2019-01-19 15:25:31 +01:00
|
|
|
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
|
2021-06-18 17:57:02 +02:00
|
|
|
assert self._connection is not None
|
2019-01-04 18:35:38 +01:00
|
|
|
await self._connection.send_message(req)
|
2018-12-13 21:34:57 +01:00
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
async def switch_command(self, key: int, state: bool) -> None:
|
2018-12-13 21:34:57 +01:00
|
|
|
self._check_authenticated()
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
req = SwitchCommandRequest()
|
2018-12-13 21:34:57 +01:00
|
|
|
req.key = key
|
|
|
|
req.state = state
|
2021-06-18 17:57:02 +02:00
|
|
|
assert self._connection is not None
|
2019-01-04 18:35:38 +01:00
|
|
|
await self._connection.send_message(req)
|
2019-02-24 18:16:12 +01:00
|
|
|
|
2021-06-18 17:57:02 +02: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,
|
2021-06-23 23:40:41 +02:00
|
|
|
custom_fan_mode: Optional[str] = None,
|
|
|
|
preset: Optional[ClimatePreset] = None,
|
|
|
|
custom_preset: Optional[str] = None,
|
2021-06-18 17:57:02 +02:00
|
|
|
) -> None:
|
2019-03-27 22:10:33 +01:00
|
|
|
self._check_authenticated()
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
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
|
2019-11-16 16:34:14 +01:00
|
|
|
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
|
2021-06-23 23:40:41 +02:00
|
|
|
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
|
2021-06-18 17:57:02 +02:00
|
|
|
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-06-18 17:57:02 +02:00
|
|
|
async def execute_service(
|
|
|
|
self, service: UserService, data: ExecuteServiceDataType
|
|
|
|
) -> None:
|
2019-02-24 18:16:12 +01:00
|
|
|
self._check_authenticated()
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
req = ExecuteServiceRequest()
|
2019-02-24 18:16:12 +01:00
|
|
|
req.key = service.key
|
|
|
|
args = []
|
|
|
|
for arg_desc in service.args:
|
2021-06-18 17:57:02 +02:00
|
|
|
arg = ExecuteServiceArgument()
|
2019-02-24 18:16:12 +01:00
|
|
|
val = data[arg_desc.name]
|
2021-06-18 17:57:02 +02:00
|
|
|
apiv = cast(APIVersion, self.api_version)
|
|
|
|
int_type = "int_" if apiv >= APIVersion(1, 3) else "legacy_int"
|
2019-06-17 23:40:23 +02:00
|
|
|
map_single = {
|
2021-06-18 17:57:02 +02:00
|
|
|
UserServiceArgType.BOOL: "bool_",
|
2019-06-17 23:40:23 +02:00
|
|
|
UserServiceArgType.INT: int_type,
|
2021-06-18 17:57:02 +02:00
|
|
|
UserServiceArgType.FLOAT: "float_",
|
|
|
|
UserServiceArgType.STRING: "string_",
|
2019-06-17 23:40:23 +02:00
|
|
|
}
|
|
|
|
map_array = {
|
2021-06-18 17:57:02 +02:00
|
|
|
UserServiceArgType.BOOL_ARRAY: "bool_array",
|
|
|
|
UserServiceArgType.INT_ARRAY: "int_array",
|
|
|
|
UserServiceArgType.FLOAT_ARRAY: "float_array",
|
|
|
|
UserServiceArgType.STRING_ARRAY: "string_array",
|
2019-06-17 23:40:23 +02:00
|
|
|
}
|
2020-07-14 20:00:12 +02:00
|
|
|
# pylint: disable=redefined-outer-name
|
2019-06-17 23:40:23 +02:00
|
|
|
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)
|
2021-06-18 17:57:02 +02:00
|
|
|
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
|
|
|
|
2021-06-18 17:57:02 +02: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
|
2021-06-18 17:57:02 +02:00
|
|
|
assert self._connection is not None
|
2019-03-09 11:02:44 +01:00
|
|
|
await self._connection.send_message(req)
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
async def request_single_image(self) -> None:
|
2019-03-09 11:02:44 +01:00
|
|
|
await self._request_image(single=True)
|
|
|
|
|
2021-06-18 17:57:02 +02:00
|
|
|
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
|