mirror of
https://github.com/esphome/esphome.git
synced 2026-08-22 22:26:21 +00:00
115 lines
3.4 KiB
Python
115 lines
3.4 KiB
Python
from enum import Enum
|
|
import logging
|
|
import sys
|
|
from typing import TextIO
|
|
|
|
from esphome.core import CORE
|
|
|
|
|
|
class AnsiFore(Enum):
|
|
KEEP = ""
|
|
BLACK = "\033[30m"
|
|
RED = "\033[31m"
|
|
GREEN = "\033[32m"
|
|
YELLOW = "\033[33m"
|
|
BLUE = "\033[34m"
|
|
MAGENTA = "\033[35m"
|
|
CYAN = "\033[36m"
|
|
WHITE = "\033[37m"
|
|
RESET = "\033[39m"
|
|
|
|
BOLD_BLACK = "\033[1;30m"
|
|
BOLD_RED = "\033[1;31m"
|
|
BOLD_GREEN = "\033[1;32m"
|
|
BOLD_YELLOW = "\033[1;33m"
|
|
BOLD_BLUE = "\033[1;34m"
|
|
BOLD_MAGENTA = "\033[1;35m"
|
|
BOLD_CYAN = "\033[1;36m"
|
|
BOLD_WHITE = "\033[1;37m"
|
|
BOLD_RESET = "\033[1;39m"
|
|
|
|
|
|
class AnsiStyle(Enum):
|
|
# BOLD/BRIGHT and THIN/DIM are intentional ANSI synonyms; Enum treats the
|
|
# second name in each pair as an alias of the first.
|
|
BRIGHT = "\033[1m"
|
|
BOLD = "\033[1m" # noqa: PIE796
|
|
DIM = "\033[2m"
|
|
THIN = "\033[2m" # noqa: PIE796
|
|
NORMAL = "\033[22m"
|
|
RESET_ALL = "\033[0m"
|
|
|
|
|
|
def color(col: AnsiFore, msg: str, reset: bool = True) -> str:
|
|
if col == AnsiFore.KEEP:
|
|
return msg
|
|
s = col.value + msg
|
|
if reset and col:
|
|
s += AnsiStyle.RESET_ALL.value
|
|
return s
|
|
|
|
|
|
class ESPHomeLogFormatter(logging.Formatter):
|
|
def __init__(self, *, include_timestamp: bool):
|
|
fmt = "%(asctime)s " if include_timestamp else ""
|
|
fmt += "%(levelname)s %(message)s"
|
|
super().__init__(fmt=fmt, style="%")
|
|
|
|
# @override
|
|
def format(self, record: logging.LogRecord) -> str:
|
|
formatted = super().format(record)
|
|
prefix = {
|
|
"DEBUG": AnsiFore.CYAN.value,
|
|
"INFO": AnsiFore.GREEN.value,
|
|
"WARNING": AnsiFore.YELLOW.value,
|
|
"ERROR": AnsiFore.RED.value,
|
|
"CRITICAL": AnsiFore.RED.value,
|
|
}.get(record.levelname, "")
|
|
message = f"{prefix}{formatted}{AnsiStyle.RESET_ALL.value}"
|
|
if CORE.dashboard:
|
|
try: # noqa: SIM105
|
|
message = message.replace("\033", "\\033")
|
|
except UnicodeEncodeError:
|
|
pass
|
|
return message
|
|
|
|
|
|
def _is_tty(stream: TextIO | None) -> bool:
|
|
# A stream can be missing, closed, or not a real file object; colorama
|
|
# tolerates all three, so treat them like a redirect and let its own
|
|
# handling apply.
|
|
if stream is None or getattr(stream, "closed", True):
|
|
return False
|
|
return hasattr(stream, "isatty") and stream.isatty()
|
|
|
|
|
|
def setup_log(
|
|
log_level: int = logging.INFO,
|
|
include_timestamp: bool = False,
|
|
) -> None:
|
|
# colorama translates ANSI escapes for old Windows consoles and strips
|
|
# them from redirected output. POSIX terminals render ANSI natively, and
|
|
# dashboard runs escape their color codes before printing, so both would
|
|
# use colorama as a plain passthrough; skip the import there (it pulls
|
|
# in ctypes, ~3ms on every CLI invocation).
|
|
if sys.platform == "win32" or not (
|
|
CORE.dashboard or (_is_tty(sys.stdout) and _is_tty(sys.stderr))
|
|
):
|
|
import colorama
|
|
|
|
colorama.init()
|
|
|
|
# Setup logging - will map log level from string to constant
|
|
logging.basicConfig(level=log_level)
|
|
|
|
if logging.root.level == logging.DEBUG:
|
|
CORE.verbose = True
|
|
elif logging.root.level == logging.CRITICAL:
|
|
CORE.quiet = True
|
|
|
|
logging.getLogger("urllib3").setLevel(logging.WARNING)
|
|
|
|
logging.getLogger().handlers[0].setFormatter(
|
|
ESPHomeLogFormatter(include_timestamp=include_timestamp)
|
|
)
|