[cli] Add config-hash command (#15548)

Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
This commit is contained in:
Cody Cutrer
2026-05-12 14:43:48 -06:00
committed by GitHub
parent b512cc42a8
commit 76d3433425
2 changed files with 45 additions and 0 deletions
+17
View File
@@ -1415,6 +1415,15 @@ def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
return 0
def command_config_hash(args: ArgsProtocol, config: ConfigType) -> int | None:
# generating code might modify config, so it must be done in order to generate
# a hash that will match what was generated when compiling and then running
# on the device
generate_cpp_contents(config)
safe_print(f"0x{CORE.config_hash:08x}")
return 0
def command_vscode(args: ArgsProtocol) -> int | None:
from esphome import vscode
@@ -1950,6 +1959,7 @@ PRE_CONFIG_ACTIONS = {
POST_CONFIG_ACTIONS = {
"config": command_config,
"config-hash": command_config_hash,
"compile": command_compile,
"upload": command_upload,
"logs": command_logs,
@@ -2063,6 +2073,13 @@ def parse_args(argv):
"--show-secrets", help="Show secrets in output.", action="store_true"
)
parser_config_hash = subparsers.add_parser(
"config-hash", help="Calculate the hash of the configuration."
)
parser_config_hash.add_argument(
"configuration", help="Your YAML configuration file(s).", nargs="+"
)
parser_compile = subparsers.add_parser(
"compile", help="Read the configuration and compile a program."
)
+28
View File
@@ -29,6 +29,7 @@ from esphome.__main__ import (
command_analyze_memory,
command_bundle,
command_clean_all,
command_config_hash,
command_rename,
command_run,
command_update_all,
@@ -3439,6 +3440,33 @@ def test_command_wizard(tmp_path: Path) -> None:
mock_wizard.assert_called_once_with(config_file)
def test_command_config_hash(
tmp_path: Path,
capfd: CaptureFixture[str],
) -> None:
"""command_config_hash runs codegen then prints CORE.config_hash.
The printed format must match `0x{config_hash:08x}` used by
generate_build_info_data_cpp so the value can be compared byte-for-byte
against the ESPHOME_CONFIG_HASH embedded in firmware.
"""
setup_core(tmp_path=tmp_path, config={"esphome": {"name": "test"}})
args = MockArgs()
# generate_cpp_contents requires real components to be loaded; mock it out
# so this test isolates the command's output contract. The command must
# still call it (codegen can mutate config, which affects the hash).
with patch("esphome.__main__.generate_cpp_contents") as mock_generate:
result = command_config_hash(args, CORE.config)
assert result == 0
mock_generate.assert_called_once_with(CORE.config)
output = strip_ansi_codes(capfd.readouterr().out).strip()
assert re.fullmatch(r"0x[0-9a-f]{8}", output)
assert output == f"0x{CORE.config_hash:08x}"
def test_command_rename_invalid_characters(
tmp_path: Path, capfd: CaptureFixture[str]
) -> None: