async def resolve_ip_address_getaddrinfo( eventloop: asyncio.events.AbstractEventLoop, host: str, port: int) -> Tuple[Any, ...]: try: socket.inet_aton(host) except OSError: pass else: return (host, port) try: res = await eventloop.getaddrinfo(host, port, family=socket.AF_INET, proto=socket.IPPROTO_TCP) except OSError as err: raise APIConnectionError("Error resolving IP address: {}".format(err)) if not res: raise APIConnectionError("Error resolving IP address: No matches!") _, _, _, _, sockaddr = res[0] return sockaddr
async def send_message_await_response_complex(self, send_msg: message.Message, do_append: Callable[[Any], bool], do_stop: Callable[[Any], bool], timeout: float = 5.0) -> List[Any]: fut = self._params.eventloop.create_future() responses = [] def on_message(resp): if fut.done(): return if do_append(resp): responses.append(resp) if do_stop(resp): fut.set_result(responses) self._message_handlers.append(on_message) await self.send_message(send_msg) try: await asyncio.wait_for(fut, timeout) except asyncio.TimeoutError: if self._stopped: raise APIConnectionError( "Disconnected while waiting for API response!") raise APIConnectionError("Timeout while waiting for API response!") try: self._message_handlers.remove(on_message) except ValueError: pass return responses
async def _run_once(self) -> None: preamble = await self._recv(1) if preamble[0] != 0x00: raise APIConnectionError("Invalid preamble") length = await self._recv_varint() msg_type = await self._recv_varint() raw_msg = await self._recv(length) if msg_type not in MESSAGE_TYPE_TO_PROTO: _LOGGER.debug("%s: Skipping message type %s", self._params.address, msg_type) return msg = MESSAGE_TYPE_TO_PROTO[msg_type]() try: msg.ParseFromString(raw_msg) except Exception as e: raise APIConnectionError("Invalid protobuf message: {}".format(e)) _LOGGER.debug("%s: Got message of type %s: %s", self._params.address, type(msg), msg) for msg_handler in self._message_handlers[:]: msg_handler(msg) await self._handle_internal_messages(msg) self._last_traffic = datetime.now()
async def connect(self, on_stop=None, login=False): if self._connection is not None: raise APIConnectionError("Already connected!") connected = False stopped = False async def _on_stop(): nonlocal stopped if stopped: return 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
def resolve_host( host: str, timeout: float = 3.0, zeroconf_instance: Optional[zeroconf.Zeroconf] = None, ) -> str: from aioesphomeapi.core import APIConnectionError try: zc = zeroconf_instance or zeroconf.Zeroconf() except Exception: raise APIConnectionError( "Cannot start mDNS sockets, is this a docker container without " "host network mode?" ) try: info = HostResolver(host + ".") assert info.address is not None address = None if info.request(zc, timeout): address = socket.inet_ntoa(info.address) except Exception as err: raise APIConnectionError( "Error resolving mDNS hostname: {}".format(err) ) from err finally: if not zeroconf_instance: zc.close() if address is None: raise APIConnectionError( "Error resolving address with mDNS: Did not respond. " "Maybe the device is offline." ) return address
async def _write(self, data: bytes) -> None: # _LOGGER.debug("%s: Write: %s", self._params.address, # ' '.join('{:02X}'.format(x) for x in data)) if not self._socket_connected: raise APIConnectionError("Socket is not connected") try: async with self._write_lock: self._socket_writer.write(data) await self._socket_writer.drain() except OSError as err: await self._on_error() raise APIConnectionError( "Error while writing data: {}".format(err))
async def login(self) -> None: self._check_connected() if self._authenticated: raise APIConnectionError("Already logged in!") connect = pb.ConnectRequest() if self._params.password is not None: connect.password = self._params.password resp = await self.send_message_await_response(connect, pb.ConnectResponse) if resp.invalid_password: raise APIConnectionError("Invalid password!") self._authenticated = True
async def _recv(self, amount: int) -> bytes: if amount == 0: return bytes() try: ret = await self._socket_reader.readexactly(amount) except (asyncio.IncompleteReadError, OSError, TimeoutError) as err: raise APIConnectionError( "Error while receiving data: {}".format(err)) return ret
async def connect(self) -> None: if self._stopped: raise APIConnectionError("Connection is closed!") if self._connected: raise APIConnectionError("Already connected!") try: coro = resolve_ip_address(self._params.eventloop, self._params.address, self._params.port, self._params.zeroconf_instance) sockaddr = await asyncio.wait_for(coro, 30.0) except APIConnectionError as err: await self._on_error() raise err except asyncio.TimeoutError: await self._on_error() raise APIConnectionError("Timeout while resolving IP address") self._socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM) self._socket.setblocking(False) self._socket.setsockopt(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1) _LOGGER.debug("%s: Connecting to %s:%s (%s)", self._params.address, self._params.address, self._params.port, sockaddr) try: coro = self._params.eventloop.sock_connect(self._socket, sockaddr) await asyncio.wait_for(coro, 30.0) except OSError as err: await self._on_error() raise APIConnectionError( "Error connecting to {}: {}".format(sockaddr, err)) except asyncio.TimeoutError: await self._on_error() raise APIConnectionError( "Timeout while connecting to {}".format(sockaddr)) _LOGGER.debug("%s: Opened socket for", self._params.address) self._socket_reader, self._socket_writer = await asyncio.open_connection(sock=self._socket) self._socket_connected = True self._params.eventloop.create_task(self.run_forever()) hello = pb.HelloRequest() hello.client_info = self._params.client_info try: resp = await self.send_message_await_response(hello, pb.HelloResponse) except APIConnectionError as err: await self._on_error() raise err _LOGGER.debug("%s: Successfully connected ('%s' API=%s.%s)", self._params.address, resp.server_info, resp.api_version_major, resp.api_version_minor) self._api_version = APIVersion( resp.api_version_major, resp.api_version_minor) if self._api_version.major > 2: _LOGGER.error("%s: Incompatible version %s! Closing connection", self._params.address, self._api_version.major) await self._on_error() raise APIConnectionError("Incompatible API version.") self._connected = True self._start_ping()
async def send_message_await_response(self, send_msg: message.Message, response_type: Any, timeout: float = 5.0) -> Any: def is_response(msg): return isinstance(msg, response_type) res = await self.send_message_await_response_complex( send_msg, is_response, is_response, timeout=timeout) if len(res) != 1: raise APIConnectionError( "Expected one result, got {}".format(len(res))) return res[0]
def _check_authenticated(self) -> None: if not self._authenticated: raise APIConnectionError("Must login first!")
def _check_connected(self) -> None: if not self._connected: raise APIConnectionError("Must be connected!")
def _check_authenticated(self) -> None: self._check_connected() assert self._connection is not None if not self._connection.is_authenticated: raise APIConnectionError("Not authenticated!")
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): self._check_connected() if not self._connection.is_authenticated: raise APIConnectionError("Not authenticated!")