[log] improve/refactor log (#8708)

This commit is contained in:
Thomas Rupprecht 2025-05-15 11:45:07 +02:00 committed by Jesse Hills
parent 41f860c2a3
commit 5a84bab9ec
No known key found for this signature in database
GPG Key ID: BEAAE804EFD8E83A
5 changed files with 91 additions and 83 deletions

View File

@ -43,7 +43,7 @@ from esphome.const import (
)
from esphome.core import CORE, EsphomeError, coroutine
from esphome.helpers import get_bool_env, indent, is_ip_address
from esphome.log import Fore, color, setup_log
from esphome.log import AnsiFore, color, setup_log
from esphome.util import (
get_serial_ports,
list_yaml_files,
@ -83,7 +83,7 @@ def choose_prompt(options, purpose: str = None):
raise ValueError
break
except ValueError:
safe_print(color(Fore.RED, f"Invalid option: '{opt}'"))
safe_print(color(AnsiFore.RED, f"Invalid option: '{opt}'"))
return options[opt - 1][1]
@ -596,30 +596,30 @@ def command_update_all(args):
click.echo(f"{half_line}{middle_text}{half_line}")
for f in files:
print(f"Updating {color(Fore.CYAN, f)}")
print(f"Updating {color(AnsiFore.CYAN, f)}")
print("-" * twidth)
print()
rc = run_external_process(
"esphome", "--dashboard", "run", f, "--no-logs", "--device", "OTA"
)
if rc == 0:
print_bar(f"[{color(Fore.BOLD_GREEN, 'SUCCESS')}] {f}")
print_bar(f"[{color(AnsiFore.BOLD_GREEN, 'SUCCESS')}] {f}")
success[f] = True
else:
print_bar(f"[{color(Fore.BOLD_RED, 'ERROR')}] {f}")
print_bar(f"[{color(AnsiFore.BOLD_RED, 'ERROR')}] {f}")
success[f] = False
print()
print()
print()
print_bar(f"[{color(Fore.BOLD_WHITE, 'SUMMARY')}]")
print_bar(f"[{color(AnsiFore.BOLD_WHITE, 'SUMMARY')}]")
failed = 0
for f in files:
if success[f]:
print(f" - {f}: {color(Fore.GREEN, 'SUCCESS')}")
print(f" - {f}: {color(AnsiFore.GREEN, 'SUCCESS')}")
else:
print(f" - {f}: {color(Fore.BOLD_RED, 'FAILED')}")
print(f" - {f}: {color(AnsiFore.BOLD_RED, 'FAILED')}")
failed += 1
return failed
@ -645,7 +645,7 @@ def command_rename(args, config):
if c not in ALLOWED_NAME_CHARS:
print(
color(
Fore.BOLD_RED,
AnsiFore.BOLD_RED,
f"'{c}' is an invalid character for names. Valid characters are: "
f"{ALLOWED_NAME_CHARS} (lowercase, no spaces)",
)
@ -658,7 +658,9 @@ def command_rename(args, config):
yaml = yaml_util.load_yaml(CORE.config_path)
if CONF_ESPHOME not in yaml or CONF_NAME not in yaml[CONF_ESPHOME]:
print(
color(Fore.BOLD_RED, "Complex YAML files cannot be automatically renamed.")
color(
AnsiFore.BOLD_RED, "Complex YAML files cannot be automatically renamed."
)
)
return 1
old_name = yaml[CONF_ESPHOME][CONF_NAME]
@ -681,7 +683,7 @@ def command_rename(args, config):
)
> 1
):
print(color(Fore.BOLD_RED, "Too many matches in YAML to safely rename"))
print(color(AnsiFore.BOLD_RED, "Too many matches in YAML to safely rename"))
return 1
new_raw = re.sub(
@ -693,7 +695,7 @@ def command_rename(args, config):
new_path = os.path.join(CORE.config_dir, args.name + ".yaml")
print(
f"Updating {color(Fore.CYAN, CORE.config_path)} to {color(Fore.CYAN, new_path)}"
f"Updating {color(AnsiFore.CYAN, CORE.config_path)} to {color(AnsiFore.CYAN, new_path)}"
)
print()
@ -702,7 +704,7 @@ def command_rename(args, config):
rc = run_external_process("esphome", "config", new_path)
if rc != 0:
print(color(Fore.BOLD_RED, "Rename failed. Reverting changes."))
print(color(AnsiFore.BOLD_RED, "Rename failed. Reverting changes."))
os.remove(new_path)
return 1
@ -728,7 +730,7 @@ def command_rename(args, config):
if CORE.config_path != new_path:
os.remove(CORE.config_path)
print(color(Fore.BOLD_GREEN, "SUCCESS"))
print(color(AnsiFore.BOLD_GREEN, "SUCCESS"))
print()
return 0

View File

@ -28,7 +28,7 @@ import esphome.core.config as core_config
import esphome.final_validate as fv
from esphome.helpers import indent
from esphome.loader import ComponentManifest, get_component, get_platform
from esphome.log import Fore, color
from esphome.log import AnsiFore, color
from esphome.types import ConfigFragmentType, ConfigType
from esphome.util import OrderedDict, safe_print
from esphome.voluptuous_schema import ExtraKeysInvalid
@ -959,7 +959,7 @@ def line_info(config, path, highlight=True):
if obj:
mark = obj.start_mark
source = f"[source {mark.document}:{mark.line + 1}]"
return color(Fore.CYAN, source)
return color(AnsiFore.CYAN, source)
return "None"
@ -983,7 +983,7 @@ def dump_dict(
if at_root:
error = config.get_error_for_path(path)
if error is not None:
ret += f"\n{color(Fore.BOLD_RED, _format_vol_invalid(error, config))}\n"
ret += f"\n{color(AnsiFore.BOLD_RED, _format_vol_invalid(error, config))}\n"
if isinstance(conf, (list, tuple)):
multiline = True
@ -995,11 +995,11 @@ def dump_dict(
path_ = path + [i]
error = config.get_error_for_path(path_)
if error is not None:
ret += f"\n{color(Fore.BOLD_RED, _format_vol_invalid(error, config))}\n"
ret += f"\n{color(AnsiFore.BOLD_RED, _format_vol_invalid(error, config))}\n"
sep = "- "
if config.is_in_error_path(path_):
sep = color(Fore.RED, sep)
sep = color(AnsiFore.RED, sep)
msg, _ = dump_dict(config, path_, at_root=False)
msg = indent(msg)
inf = line_info(config, path_, highlight=config.is_in_error_path(path_))
@ -1018,11 +1018,11 @@ def dump_dict(
path_ = path + [k]
error = config.get_error_for_path(path_)
if error is not None:
ret += f"\n{color(Fore.BOLD_RED, _format_vol_invalid(error, config))}\n"
ret += f"\n{color(AnsiFore.BOLD_RED, _format_vol_invalid(error, config))}\n"
st = f"{k}: "
if config.is_in_error_path(path_):
st = color(Fore.RED, st)
st = color(AnsiFore.RED, st)
msg, m = dump_dict(config, path_, at_root=False)
inf = line_info(config, path_, highlight=config.is_in_error_path(path_))
@ -1044,7 +1044,7 @@ def dump_dict(
if len(conf) > 80:
conf = f"|-\n{indent(conf)}"
error = config.get_error_for_path(path)
col = Fore.BOLD_RED if error else Fore.KEEP
col = AnsiFore.BOLD_RED if error else AnsiFore.KEEP
ret += color(col, str(conf))
elif isinstance(conf, core.Lambda):
if is_secret(conf):
@ -1052,13 +1052,13 @@ def dump_dict(
conf = f"!lambda |-\n{indent(str(conf.value))}"
error = config.get_error_for_path(path)
col = Fore.BOLD_RED if error else Fore.KEEP
col = AnsiFore.BOLD_RED if error else AnsiFore.KEEP
ret += color(col, conf)
elif conf is None:
pass
else:
error = config.get_error_for_path(path)
col = Fore.BOLD_RED if error else Fore.KEEP
col = AnsiFore.BOLD_RED if error else AnsiFore.KEEP
ret += color(col, str(conf))
multiline = "\n" in ret
@ -1100,13 +1100,13 @@ def read_config(command_line_substitutions):
if not CORE.verbose:
res = strip_default_ids(res)
safe_print(color(Fore.BOLD_RED, "Failed config"))
safe_print(color(AnsiFore.BOLD_RED, "Failed config"))
safe_print("")
for path, domain in res.output_paths:
if not res.is_in_error_path(path):
continue
errstr = color(Fore.BOLD_RED, f"{domain}:")
errstr = color(AnsiFore.BOLD_RED, f"{domain}:")
errline = line_info(res, path)
if errline:
errstr += f" {errline}"
@ -1121,7 +1121,7 @@ def read_config(command_line_substitutions):
safe_print(indent("\n".join(split_dump[:i])))
for err in res.errors:
safe_print(color(Fore.BOLD_RED, err.msg))
safe_print(color(AnsiFore.BOLD_RED, err.msg))
safe_print("")
return None

View File

@ -1,9 +1,10 @@
from enum import Enum
import logging
from esphome.core import CORE
class AnsiFore:
class AnsiFore(Enum):
KEEP = ""
BLACK = "\033[30m"
RED = "\033[31m"
@ -26,7 +27,7 @@ class AnsiFore:
BOLD_RESET = "\033[1;39m"
class AnsiStyle:
class AnsiStyle(Enum):
BRIGHT = "\033[1m"
BOLD = "\033[1m"
DIM = "\033[2m"
@ -35,16 +36,10 @@ class AnsiStyle:
RESET_ALL = "\033[0m"
Fore = AnsiFore()
Style = AnsiStyle()
def color(col: str, msg: str, reset: bool = True) -> bool:
if col and not col.startswith("\033["):
raise ValueError("Color must be value from esphome.log.Fore")
s = str(col) + msg
def color(col: AnsiFore, msg: str, reset: bool = True) -> str:
s = col.value + msg
if reset and col:
s += str(Style.RESET_ALL)
s += AnsiStyle.RESET_ALL.value
return s
@ -54,20 +49,21 @@ class ESPHomeLogFormatter(logging.Formatter):
fmt += "%(levelname)s %(message)s"
super().__init__(fmt=fmt, style="%")
def format(self, record):
# @override
def format(self, record: logging.LogRecord) -> str:
formatted = super().format(record)
prefix = {
"DEBUG": Fore.CYAN,
"INFO": Fore.GREEN,
"WARNING": Fore.YELLOW,
"ERROR": Fore.RED,
"CRITICAL": Fore.RED,
"DEBUG": AnsiFore.CYAN.value,
"INFO": AnsiFore.GREEN.value,
"WARNING": AnsiFore.YELLOW.value,
"ERROR": AnsiFore.RED.value,
"CRITICAL": AnsiFore.RED.value,
}.get(record.levelname, "")
return f"{prefix}{formatted}{Style.RESET_ALL}"
return f"{prefix}{formatted}{AnsiStyle.RESET_ALL.value}"
def setup_log(
log_level=logging.INFO,
log_level: int = logging.INFO,
include_timestamp: bool = False,
) -> None:
import colorama

View File

@ -28,7 +28,7 @@ from esphome.const import (
)
from esphome.core import CORE, EsphomeError
from esphome.helpers import get_int_env, get_str_env
from esphome.log import Fore, color
from esphome.log import AnsiFore, color
from esphome.util import safe_print
_LOGGER = logging.getLogger(__name__)
@ -291,7 +291,7 @@ def get_fingerprint(config):
sha1 = hashlib.sha1(cert_der).hexdigest()
safe_print(f"SHA1 Fingerprint: {color(Fore.CYAN, sha1)}")
safe_print(f"SHA1 Fingerprint: {color(AnsiFore.CYAN, sha1)}")
safe_print(
f"Copy the string above into mqtt.ssl_fingerprints section of {CORE.config_path}"
)

View File

@ -9,7 +9,7 @@ import esphome.config_validation as cv
from esphome.const import ALLOWED_NAME_CHARS, ENV_QUICKWIZARD
from esphome.core import CORE
from esphome.helpers import get_bool_env, write_file
from esphome.log import Fore, color
from esphome.log import AnsiFore, color
from esphome.storage_json import StorageJSON, ext_storage_path
from esphome.util import safe_input, safe_print
@ -219,7 +219,7 @@ def wizard_write(path, **kwargs):
elif board in rtl87xx_boards.BOARDS:
platform = "RTL87XX"
else:
safe_print(color(Fore.RED, f'The board "{board}" is unknown.'))
safe_print(color(AnsiFore.RED, f'The board "{board}" is unknown.'))
return False
kwargs["platform"] = platform
hardware = kwargs["platform"]
@ -274,12 +274,12 @@ def wizard(path):
if not path.endswith(".yaml") and not path.endswith(".yml"):
safe_print(
f"Please make your configuration file {color(Fore.CYAN, path)} have the extension .yaml or .yml"
f"Please make your configuration file {color(AnsiFore.CYAN, path)} have the extension .yaml or .yml"
)
return 1
if os.path.exists(path):
safe_print(
f"Uh oh, it seems like {color(Fore.CYAN, path)} already exists, please delete that file first or chose another configuration file."
f"Uh oh, it seems like {color(AnsiFore.CYAN, path)} already exists, please delete that file first or chose another configuration file."
)
return 2
@ -298,17 +298,19 @@ def wizard(path):
sleep(3.0)
safe_print()
safe_print_step(1, CORE_BIG)
safe_print(f"First up, please choose a {color(Fore.GREEN, 'name')} for your node.")
safe_print(
f"First up, please choose a {color(AnsiFore.GREEN, 'name')} for your node."
)
safe_print(
"It should be a unique name that can be used to identify the device later."
)
sleep(1)
safe_print(
f"For example, I like calling the node in my living room {color(Fore.BOLD_WHITE, 'livingroom')}."
f"For example, I like calling the node in my living room {color(AnsiFore.BOLD_WHITE, 'livingroom')}."
)
safe_print()
sleep(1)
name = safe_input(color(Fore.BOLD_WHITE, "(name): "))
name = safe_input(color(AnsiFore.BOLD_WHITE, "(name): "))
while True:
try:
@ -317,7 +319,7 @@ def wizard(path):
except vol.Invalid:
safe_print(
color(
Fore.RED,
AnsiFore.RED,
f'Oh noes, "{name}" isn\'t a valid name. Names can only '
f"include numbers, lower-case letters and hyphens. ",
)
@ -325,11 +327,13 @@ def wizard(path):
name = strip_accents(name).lower().replace(" ", "-")
name = strip_accents(name).lower().replace("_", "-")
name = "".join(c for c in name if c in ALLOWED_NAME_CHARS)
safe_print(f'Shall I use "{color(Fore.CYAN, name)}" as the name instead?')
safe_print(
f'Shall I use "{color(AnsiFore.CYAN, name)}" as the name instead?'
)
sleep(0.5)
name = default_input("(name [{}]): ", name)
safe_print(f'Great! Your node is now called "{color(Fore.CYAN, name)}".')
safe_print(f'Great! Your node is now called "{color(AnsiFore.CYAN, name)}".')
sleep(1)
safe_print_step(2, ESP_BIG)
safe_print(
@ -346,7 +350,7 @@ def wizard(path):
sleep(0.5)
safe_print()
platform = safe_input(
color(Fore.BOLD_WHITE, f"({'/'.join(wizard_platforms)}): ")
color(AnsiFore.BOLD_WHITE, f"({'/'.join(wizard_platforms)}): ")
)
try:
platform = vol.All(vol.Upper, vol.Any(*wizard_platforms))(platform.upper())
@ -355,7 +359,9 @@ def wizard(path):
safe_print(
f'Unfortunately, I can\'t find an espressif microcontroller called "{platform}". Please try again.'
)
safe_print(f"Thanks! You've chosen {color(Fore.CYAN, platform)} as your platform.")
safe_print(
f"Thanks! You've chosen {color(AnsiFore.CYAN, platform)} as your platform."
)
safe_print()
sleep(1)
@ -376,27 +382,29 @@ def wizard(path):
else:
raise NotImplementedError("Unknown platform!")
safe_print(f"Next, I need to know what {color(Fore.GREEN, 'board')} you're using.")
safe_print(
f"Next, I need to know what {color(AnsiFore.GREEN, 'board')} you're using."
)
sleep(0.5)
safe_print(f"Please go to {color(Fore.GREEN, board_link)} and choose a board.")
safe_print(f"Please go to {color(AnsiFore.GREEN, board_link)} and choose a board.")
if platform == "ESP32":
safe_print(f"(Type {color(Fore.GREEN, 'esp01_1m')} for Sonoff devices)")
safe_print(f"(Type {color(AnsiFore.GREEN, 'esp01_1m')} for Sonoff devices)")
safe_print()
# Don't sleep because user needs to copy link
if platform == "ESP32":
safe_print(f'For example "{color(Fore.BOLD_WHITE, "nodemcu-32s")}".')
safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "nodemcu-32s")}".')
boards_list = esp32_boards.BOARDS.items()
elif platform == "ESP8266":
safe_print(f'For example "{color(Fore.BOLD_WHITE, "nodemcuv2")}".')
safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "nodemcuv2")}".')
boards_list = esp8266_boards.BOARDS.items()
elif platform == "BK72XX":
safe_print(f'For example "{color(Fore.BOLD_WHITE, "cb2s")}".')
safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "cb2s")}".')
boards_list = bk72xx_boards.BOARDS.items()
elif platform == "RTL87XX":
safe_print(f'For example "{color(Fore.BOLD_WHITE, "wr3")}".')
safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "wr3")}".')
boards_list = rtl87xx_boards.BOARDS.items()
elif platform == "RP2040":
safe_print(f'For example "{color(Fore.BOLD_WHITE, "rpipicow")}".')
safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "rpipicow")}".')
boards_list = rp2040_boards.BOARDS.items()
else:
@ -409,19 +417,21 @@ def wizard(path):
boards.append(board_id)
while True:
board = safe_input(color(Fore.BOLD_WHITE, "(board): "))
board = safe_input(color(AnsiFore.BOLD_WHITE, "(board): "))
try:
board = vol.All(vol.Lower, vol.Any(*boards))(board)
break
except vol.Invalid:
safe_print(
color(Fore.RED, f'Sorry, I don\'t think the board "{board}" exists.')
color(
AnsiFore.RED, f'Sorry, I don\'t think the board "{board}" exists.'
)
)
safe_print()
sleep(0.25)
safe_print()
safe_print(f"Way to go! You've chosen {color(Fore.CYAN, board)} as your board.")
safe_print(f"Way to go! You've chosen {color(AnsiFore.CYAN, board)} as your board.")
safe_print()
sleep(1)
@ -432,19 +442,19 @@ def wizard(path):
safe_print()
sleep(1)
safe_print(
f"First, what's the {color(Fore.GREEN, 'SSID')} (the name) of the WiFi network {name} should connect to?"
f"First, what's the {color(AnsiFore.GREEN, 'SSID')} (the name) of the WiFi network {name} should connect to?"
)
sleep(1.5)
safe_print(f'For example "{color(Fore.BOLD_WHITE, "Abraham Linksys")}".')
safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "Abraham Linksys")}".')
while True:
ssid = safe_input(color(Fore.BOLD_WHITE, "(ssid): "))
ssid = safe_input(color(AnsiFore.BOLD_WHITE, "(ssid): "))
try:
ssid = cv.ssid(ssid)
break
except vol.Invalid:
safe_print(
color(
Fore.RED,
AnsiFore.RED,
f'Unfortunately, "{ssid}" doesn\'t seem to be a valid SSID. Please try again.',
)
)
@ -452,18 +462,18 @@ def wizard(path):
sleep(1)
safe_print(
f'Thank you very much! You\'ve just chosen "{color(Fore.CYAN, ssid)}" as your SSID.'
f'Thank you very much! You\'ve just chosen "{color(AnsiFore.CYAN, ssid)}" as your SSID.'
)
safe_print()
sleep(0.75)
safe_print(
f"Now please state the {color(Fore.GREEN, 'password')} of the WiFi network so that I can connect to it (Leave empty for no password)"
f"Now please state the {color(AnsiFore.GREEN, 'password')} of the WiFi network so that I can connect to it (Leave empty for no password)"
)
safe_print()
safe_print(f'For example "{color(Fore.BOLD_WHITE, "PASSWORD42")}"')
safe_print(f'For example "{color(AnsiFore.BOLD_WHITE, "PASSWORD42")}"')
sleep(0.5)
psk = safe_input(color(Fore.BOLD_WHITE, "(PSK): "))
psk = safe_input(color(AnsiFore.BOLD_WHITE, "(PSK): "))
safe_print(
"Perfect! WiFi is now set up (you can create static IPs and so on later)."
)
@ -475,12 +485,12 @@ def wizard(path):
"(over the air) and integrates into Home Assistant with a native API."
)
safe_print(
f"This can be insecure if you do not trust the WiFi network. Do you want to set a {color(Fore.GREEN, 'password')} for connecting to this ESP?"
f"This can be insecure if you do not trust the WiFi network. Do you want to set a {color(AnsiFore.GREEN, 'password')} for connecting to this ESP?"
)
safe_print()
sleep(0.25)
safe_print("Press ENTER for no password")
password = safe_input(color(Fore.BOLD_WHITE, "(password): "))
password = safe_input(color(AnsiFore.BOLD_WHITE, "(password): "))
else:
ssid, password, psk = "", "", ""
@ -497,8 +507,8 @@ def wizard(path):
safe_print()
safe_print(
color(Fore.CYAN, "DONE! I've now written a new configuration file to ")
+ color(Fore.BOLD_CYAN, path)
color(AnsiFore.CYAN, "DONE! I've now written a new configuration file to ")
+ color(AnsiFore.BOLD_CYAN, path)
)
safe_print()
safe_print("Next steps:")