from __future__ import annotations # Helper script and aioesphomeapi to view logs from an esphome device import argparse import asyncio import contextlib from datetime import datetime import logging import sys from typing import TYPE_CHECKING from .client import APIClient from .log_parser import parse_log_message from .log_runner import async_run if TYPE_CHECKING: from .api_pb2 import SubscribeLogsResponse # type: ignore[attr-defined] async def main(argv: list[str]) -> None: parser = argparse.ArgumentParser("aioesphomeapi-logs") parser.add_argument("--port", type=int, default=6053) parser.add_argument("--password", type=str) parser.add_argument("--noise-psk", type=str) parser.add_argument("-v", "--verbose", action="store_true") parser.add_argument( "--no-states", action="store_true", help="Do not show entity state changes in log output.", ) parser.add_argument( "--allow-plaintext-fallback", action="store_true", help=( "If --noise-psk is set and the device is running plaintext " "firmware, downgrade to plaintext (with a warning) instead of " "failing repeatedly." ), ) parser.add_argument( "--strip-ansi-escapes", action="store_true", help=( "Strip ANSI escape sequences (colors, cursor moves) from log " "output. Useful when piping to a file or to a terminal that " "doesn't render them." ), ) parser.add_argument("address") args = parser.parse_args(argv[1:]) logging.basicConfig( format="%(asctime)s.%(msecs)03d %(levelname)-8s %(message)s", level=logging.DEBUG if args.verbose else logging.INFO, datefmt="%Y-%m-%d %H:%M:%S", ) cli = APIClient( args.address, args.port, password=args.password, noise_psk=args.noise_psk, keepalive=10, ) def on_log(msg: SubscribeLogsResponse) -> None: time_ = datetime.now().astimezone() message: bytes = msg.message text = message.decode("utf8", "backslashreplace") milliseconds = time_.microsecond // 1000 timestamp = ( f"[{time_.hour:02}:{time_.minute:02}:{time_.second:02}.{milliseconds:03}]" ) # Parse and print the log message for line in parse_log_message( text, timestamp, strip_ansi_escapes=args.strip_ansi_escapes ): print(line) stop = await async_run( cli, on_log, subscribe_states=not args.no_states, allow_plaintext_fallback=args.allow_plaintext_fallback, ) try: await asyncio.Event().wait() finally: await stop() def cli_entry_point() -> None: """Run the CLI.""" with contextlib.suppress(KeyboardInterrupt): asyncio.run(main(sys.argv)) if __name__ == "__main__": cli_entry_point() sys.exit(0)