mirror of
https://github.com/esphome/esphome.git
synced 2026-09-15 00:58:40 +00:00
[core] Make validated-config cache automatic, no flag
Drop the `--from-storage-json` CLI flag. `esphome upload` and `esphome logs` now always try the validated-config cache and transparently fall back to `read_config()` when it's missing, stale (YAML mtime > cache mtime), or corrupt. No caller change is required to benefit, and a cold cache never produces a worse outcome than today. `compile` continues to write the cache unconditionally so the next upload / logs against this YAML can skip validation; it itself always re-validates since code generation needs the fully validated config.
This commit is contained in:
+14
-45
@@ -2117,16 +2117,6 @@ def parse_args(argv):
|
||||
help="Upload as bootloader (OTA).",
|
||||
action="store_true",
|
||||
)
|
||||
parser_upload.add_argument(
|
||||
"--from-storage-json",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Skip YAML schema validation by loading device metadata from "
|
||||
"the .esphome/storage/<file>.json sidecar produced by the last "
|
||||
"successful compile. Falls back to a full validation pass when "
|
||||
"the sidecar is missing or older than the YAML."
|
||||
),
|
||||
)
|
||||
|
||||
parser_logs = subparsers.add_parser(
|
||||
"logs",
|
||||
@@ -2154,16 +2144,6 @@ def parse_args(argv):
|
||||
action="store_true",
|
||||
help="Do not show entity state changes in log output.",
|
||||
)
|
||||
parser_logs.add_argument(
|
||||
"--from-storage-json",
|
||||
action="store_true",
|
||||
help=(
|
||||
"Skip YAML schema validation by loading device metadata from "
|
||||
"the .esphome/storage/<file>.json sidecar produced by the last "
|
||||
"successful compile. Falls back to a full validation pass when "
|
||||
"the sidecar is missing or older than the YAML."
|
||||
),
|
||||
)
|
||||
|
||||
parser_discover = subparsers.add_parser(
|
||||
"discover",
|
||||
@@ -2439,44 +2419,33 @@ def run_esphome(argv):
|
||||
skip_external = args.command in ("logs", "clean")
|
||||
command_line_substitutions = dict(args.substitution) if args.substitution else {}
|
||||
|
||||
# Fast path for `upload --from-storage-json` and `logs --from-storage-json`:
|
||||
# the caller already has a binary on disk and only needs the CLI to ship
|
||||
# bytes to a device or stream logs back. Re-running the full
|
||||
# `read_config()` pipeline (parse + schema validate + final-validate +
|
||||
# external component refresh) for those two subcommands is dead work and
|
||||
# produces a wall of "Reading configuration ..." log lines for every
|
||||
# remote install. Reload the validated config that the last compile
|
||||
# cached alongside the StorageJSON sidecar; fall back to full
|
||||
# validation if the cache is missing or older than the YAML so a
|
||||
# cold cache never produces a worse outcome.
|
||||
# Fast path for `upload` and `logs`: the caller already has a binary
|
||||
# on disk and only needs the CLI to ship bytes to a device or stream
|
||||
# logs back. Re-running the full `read_config()` pipeline (parse +
|
||||
# schema validate + final-validate + external component refresh) is
|
||||
# dead work and produces a wall of "Reading configuration ..." log
|
||||
# lines on every install. Reload the validated config the last
|
||||
# compile cached alongside the StorageJSON sidecar; fall back to
|
||||
# full validation if the cache is missing or older than the YAML so
|
||||
# a cold cache never produces a worse outcome.
|
||||
config = None
|
||||
if getattr(args, "from_storage_json", False) and args.command in (
|
||||
"upload",
|
||||
"logs",
|
||||
):
|
||||
if args.command in ("upload", "logs"):
|
||||
from esphome.storage_json import (
|
||||
StorageJSON,
|
||||
ext_storage_path,
|
||||
load_compiled_config,
|
||||
)
|
||||
|
||||
config = load_compiled_config(conf_path)
|
||||
if config is not None:
|
||||
cached = load_compiled_config(conf_path)
|
||||
if cached is not None:
|
||||
storage = StorageJSON.load(ext_storage_path(conf_path.name))
|
||||
if storage is None:
|
||||
config = None
|
||||
else:
|
||||
if storage is not None:
|
||||
storage.apply_to_core()
|
||||
config = cached
|
||||
_LOGGER.info(
|
||||
"Loaded validated config cache for %s, skipping validation.",
|
||||
conf_path.name,
|
||||
)
|
||||
if config is None:
|
||||
_LOGGER.warning(
|
||||
"Validated config cache for %s is missing or older than the "
|
||||
"YAML; falling back to full config validation.",
|
||||
conf_path,
|
||||
)
|
||||
|
||||
if config is None:
|
||||
config = read_config(
|
||||
|
||||
+4
-4
@@ -110,10 +110,10 @@ def update_storage_json() -> None:
|
||||
old = StorageJSON.load(path)
|
||||
new = StorageJSON.from_esphome_core(CORE, old)
|
||||
|
||||
# Always refresh the validated-config cache so `esphome upload
|
||||
# --from-storage-json` and `esphome logs --from-storage-json` can
|
||||
# skip re-validating after this compile. Lives in its own file
|
||||
# next to the sidecar; mtime gates staleness on the read side.
|
||||
# Refresh the validated-config cache. `esphome upload` and
|
||||
# `esphome logs` look for it next time they run and skip the full
|
||||
# validation pipeline when it's present and fresh (mtime >= YAML
|
||||
# mtime). Cheap to write, big win to read.
|
||||
if CORE.config is not None:
|
||||
save_compiled_config(CORE.config)
|
||||
|
||||
|
||||
+17
-20
@@ -1,4 +1,4 @@
|
||||
"""Tests for the `--from-storage-json` validated-config cache fast path."""
|
||||
"""Tests for the validated-config cache fast path used by upload/logs."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
@@ -169,10 +169,10 @@ def test_storage_json_apply_to_core_populates_target_platform(tmp_path: Path) ->
|
||||
|
||||
|
||||
@pytest.mark.parametrize("command", ["upload", "logs"])
|
||||
def test_run_esphome_from_storage_json_skips_read_config(
|
||||
def test_run_esphome_upload_and_logs_use_cache_when_fresh(
|
||||
command: str, fresh_cache_files: Path
|
||||
) -> None:
|
||||
"""`--from-storage-json` makes the dispatcher skip read_config()."""
|
||||
"""When the cache is fresh, upload/logs skip read_config() entirely."""
|
||||
yaml_path = fresh_cache_files
|
||||
|
||||
captured = {}
|
||||
@@ -185,9 +185,7 @@ def test_run_esphome_from_storage_json_skips_read_config(
|
||||
patch("esphome.__main__.read_config") as mock_read,
|
||||
patch.dict("esphome.__main__.POST_CONFIG_ACTIONS", {command: _stub}),
|
||||
):
|
||||
result = run_esphome(
|
||||
["esphome", command, "--from-storage-json", str(yaml_path)]
|
||||
)
|
||||
result = run_esphome(["esphome", command, str(yaml_path)])
|
||||
|
||||
mock_read.assert_not_called()
|
||||
assert result == 0
|
||||
@@ -197,7 +195,7 @@ def test_run_esphome_from_storage_json_skips_read_config(
|
||||
|
||||
|
||||
@pytest.mark.parametrize("command", ["upload", "logs"])
|
||||
def test_run_esphome_from_storage_json_falls_back_when_missing(
|
||||
def test_run_esphome_upload_and_logs_fall_back_when_no_cache(
|
||||
tmp_path: Path, command: str
|
||||
) -> None:
|
||||
"""With no cache on disk, the dispatcher falls back to read_config()."""
|
||||
@@ -211,17 +209,13 @@ def test_run_esphome_from_storage_json_falls_back_when_missing(
|
||||
{command: lambda args, config: 0},
|
||||
),
|
||||
):
|
||||
result = run_esphome(
|
||||
["esphome", command, "--from-storage-json", str(yaml_path)]
|
||||
)
|
||||
result = run_esphome(["esphome", command, str(yaml_path)])
|
||||
|
||||
mock_read.assert_called_once()
|
||||
assert result == 2
|
||||
|
||||
|
||||
def test_run_esphome_from_storage_json_falls_back_when_stale(
|
||||
tmp_path: Path,
|
||||
) -> None:
|
||||
def test_run_esphome_upload_falls_back_when_cache_stale(tmp_path: Path) -> None:
|
||||
"""If YAML mtime > cache mtime, dispatcher falls back to read_config()."""
|
||||
yaml_path = tmp_path / "lite_test.yaml"
|
||||
yaml_path.write_text("esphome:\n name: lite_test\n")
|
||||
@@ -241,24 +235,27 @@ def test_run_esphome_from_storage_json_falls_back_when_stale(
|
||||
{"upload": lambda args, config: 0},
|
||||
),
|
||||
):
|
||||
run_esphome(["esphome", "upload", "--from-storage-json", str(yaml_path)])
|
||||
run_esphome(["esphome", "upload", str(yaml_path)])
|
||||
|
||||
mock_read.assert_called_once()
|
||||
|
||||
|
||||
def test_run_esphome_without_flag_still_calls_read_config(
|
||||
fresh_cache_files: Path,
|
||||
) -> None:
|
||||
"""Sanity: omitting the flag preserves the current behaviour."""
|
||||
def test_run_esphome_compile_does_not_use_cache(fresh_cache_files: Path) -> None:
|
||||
"""`compile` always re-validates, even with a fresh cache on disk.
|
||||
|
||||
The fast path is only for upload / logs -- compile is what writes
|
||||
the cache in the first place, and it needs a fully validated
|
||||
config to drive code generation.
|
||||
"""
|
||||
yaml_path = fresh_cache_files
|
||||
|
||||
with (
|
||||
patch("esphome.__main__.read_config", return_value=None) as mock_read,
|
||||
patch.dict(
|
||||
"esphome.__main__.POST_CONFIG_ACTIONS",
|
||||
{"upload": lambda args, config: 0},
|
||||
{"compile": lambda args, config: 0},
|
||||
),
|
||||
):
|
||||
run_esphome(["esphome", "upload", str(yaml_path)])
|
||||
run_esphome(["esphome", "compile", str(yaml_path)])
|
||||
|
||||
mock_read.assert_called_once()
|
||||
Reference in New Issue
Block a user