Commit Graph
621 Commits
Author SHA1 Message Date
J. Nick Koston af52f6a170 Merge remote-tracking branch 'origin/external-files-etag' into integration
# Conflicts:
#	tests/unit_tests/test_external_files.py
2026-04-26 10:10:11 -05:00
J. Nick Koston b78bca0dc7 Simplify _read_etag, walrus the etag/new_etag binds, fixturize requests + helper patches
- _read_etag collapses into a single try/except that handles both stat
  and read failures (and the unlink) uniformly.
- _mtime_seconds() helper compares mtimes at whole-second resolution
  instead of nanoseconds. Sub-second precision varies by filesystem
  (FAT does 2s, NTFS 100ns, APFS/ext4 ns); whole seconds is the only
  resolution every supported filesystem can preserve through a
  set+read round-trip.
- has_remote_file_changed: walrus-assign `etag` and `new_etag` instead
  of an explicit binding + immediate check.
- New test fixtures `mock_requests_head`, `mock_requests_get`,
  `mock_has_remote_file_changed`, `mock_write_file` replace the
  per-test `@patch(...)` decorator stacks. Tests are simpler and the
  fixtures can be reused by future tests in the file.
2026-04-26 10:09:38 -05:00
J. Nick Koston 1cecb98f56 Merge remote-tracking branch 'origin/external-files-parallel' into integration 2026-04-26 10:03:03 -05:00
J. Nick Koston 2def20c646 Use partial() for download_web_files_in_config call sites; collapse dedup loop into a dict comp
- Both audio_file and speaker.media_player now use functools.partial to
  bind path_for=_compute_local_file_path instead of an inline lambda.
- download_content_many's dedup loop becomes a dict comprehension.
  Last-URL wins now (dict-comp semantics) instead of first-URL; in
  practice duplicate paths only arise when the URL itself is duplicated,
  so the choice is meaningless. Test updated accordingly.
2026-04-26 10:02:44 -05:00
J. Nick Koston 9153c1be67 Merge remote-tracking branch 'origin/external-files-etag' into integration 2026-04-26 09:58:29 -05:00
J. Nick Koston fea64833b0 Merge remote-tracking branch 'origin/external-files-parallel' into integration
# Conflicts:
#	tests/unit_tests/test_external_files.py
2026-04-26 09:58:27 -05:00
J. Nick Koston 267590fc36 Extract mock_download_content[_many] fixtures in test_external_files 2026-04-26 09:57:23 -05:00
J. Nick Koston a877acf1d1 Address Copilot review on PR #16021
- download_content_many now de-duplicates by `path` so two callers
  asking for the same cache file (e.g. the same URL referenced twice
  in a config) can't race on download_content's non-atomic write. When
  duplicates are present, the first-seen URL for that path wins.
- Clamp `max_workers` to at least 1 so an invalid caller value can't
  raise ValueError out of ThreadPoolExecutor.
2026-04-26 09:55:27 -05:00
J. Nick Koston da322356e1 Address Copilot review on PR #16020
- _write_etag: suppress OSError (not just FileNotFoundError) when removing
  a stale sidecar -- ETag persistence is best-effort and a permission
  error there must not abort an otherwise-successful download.
- Atomic-write test: patch write_file directly so the failure happens
  inside the helper that does the rename, instead of before
  download_content even calls it.
2026-04-26 09:54:38 -05:00
J. Nick Koston eb3bf5baed Merge remote-tracking branch 'origin/external-files-parallel' into integration
# Conflicts:
#	esphome/external_files.py
2026-04-26 09:53:29 -05:00
J. Nick Koston 54493faafc Fix download_web_files_in_config tests for generator argument 2026-04-26 09:53:07 -05:00
J. Nick Koston 540696282b Hoist duplicated _download_all_web_files into external_files.download_web_files_in_config
audio_file and speaker.media_player had byte-identical helpers that
collected web URLs from a list-of-file-configs and handed them to
download_content_many. Move that logic into external_files as
`download_web_files_in_config(config, path_for)` -- callers only have
to pass the per-component cache-path callback now.
2026-04-26 09:52:29 -05:00
J. Nick Koston 7983ebf54a Merge remote-tracking branch 'origin/external-files-etag' into integration 2026-04-26 09:42:36 -05:00
J. Nick Koston 5ae259ccba Sync ETag sidecar mtime with cache file; treat mismatched sidecars as stale
The ETag sidecar describes a specific snapshot of the cache file. If the
cache file is replaced or edited out-of-band (manual edit, restore from
backup, another tool overwriting it), the sidecar's recorded ETag no
longer matches the bytes on disk -- using it would cause the server to
return 304 and we'd serve the wrong content from cache.

- _write_etag now os.utime()'s the sidecar to share the cache file's
  st_mtime_ns immediately after writing it.
- _read_etag compares the two mtimes; if they don't match it returns
  None and removes the sidecar so subsequent calls don't re-check it.
2026-04-26 09:41:20 -05:00
J. Nick Koston 3f793e682a Merge remote-tracking branch 'origin/external-files-parallel' into integration
# Conflicts:
#	esphome/external_files.py
#	tests/unit_tests/test_external_files.py
2026-04-26 09:39:21 -05:00
J. Nick Koston 2d9d1eabfa [core] Download external_files in parallel
Each component that uses external_files (audio_file, speaker
media_player, ...) currently calls download_content once per file
inline inside a per-item config validator. With ~24 cached audio
files in a Home Assistant Voice PE config, that means ~24 sequential
HEAD round-trips, even when every response is a 304.

This adds download_content_many(items, ...) which fans the per-file
checks out across a ThreadPoolExecutor (capped at 16 workers so
configs with hundreds of files don't open hundreds of sockets), then
refactors audio_file and speaker.media_player to collect URLs at the
list level and call the batch helper once instead of downloading
inside each per-item validator.

Wall time for the validation phase drops from sum(latency) to roughly
max(latency) when the cache is warm.
2026-04-26 09:36:13 -05:00
J. Nick Koston 68b754d741 Merge remote-tracking branch 'origin/external-files-etag' into integration 2026-04-26 09:28:44 -05:00
J. Nick Koston 9f058ac1a6 [core] Use ETag in external_files cache to fix re-downloads from raw.githubusercontent.com
raw.githubusercontent.com ignores If-Modified-Since (always returns
200), but honors If-None-Match with ETag (returns 304). This caused
every esphome compile/config run to re-download every cached external
file (audio_file, micro_wake_word, image, font, bme68x_bsec2, etc.)
sourced from a /raw/ URL.

- Send If-None-Match with the cached ETag when present
- Persist the ETag from each download in a hidden sidecar file
  (.{name}.etag) and refresh it when a 304 carries a new ETag
- Replace path.write_bytes() with helpers.write_file() so downloads are
  written atomically and can no longer leave partially-written cache
  files behind on crash
2026-04-26 09:26:20 -05:00
J. Nick Koston 0f25d91e68 [core] Unify skip_external_update and honor it in external_files for faster esphome logs (#16016) 2026-04-26 07:24:33 -05:00
J. Nick Koston 8096b95910 Merge remote-tracking branch 'upstream/skip_external_files_update_logs' into integration 2026-04-26 04:51:47 -05:00
J. Nick Koston 3438ff817a Address review: drop redundant comments and rely on existing CORE.reset() between tests 2026-04-26 04:49:04 -05:00
J. Nick Koston 4654f427ca Merge remote-tracking branch 'upstream/skip_external_files_update_logs' into integration 2026-04-26 04:44:42 -05:00
J. Nick Koston 5150a32224 [external_files] Unify skip_external_update via CORE flag for git as well 2026-04-26 04:43:53 -05:00
J. Nick Koston 26ba4850e4 [external_files] Skip remote freshness checks during esphome logs
Avoid per-file HTTP HEAD requests during config validation when running
esphome logs against a previously-cached project. The skip_external_update
flag was already plumbed for git operations, but external_files.download_content
ignored it. Thread it through CORE so audio_file, micro_wake_word,
speaker/media_player, image, font, and bme68x_bsec2 reuse cached files
without per-URL network round-trips when the file already exists locally.
2026-04-26 04:37:35 -05:00
J. Nick Koston a8d767cc5c Merge remote-tracking branch 'upstream/dev' into integration 2026-04-26 04:16:55 -05:00
J. Nick Koston 9028cc8c3e Merge remote-tracking branch 'origin/git_shallow_fetch' into integration 2026-04-26 04:13:13 -05:00
68625a1b76 [core] Isolate generated build metadata (#16007)
Co-authored-by: J. Nick Koston <nick@koston.org>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-26 09:11:09 +00:00
J. Nick Koston e907e71101 [git] Address PR review feedback
- Add `--` terminator before submodule paths (both clone and refresh
  paths) so a path beginning with `-` cannot be parsed as a git option.
- Reword the refresh-fetch comment: this fetch also runs when ref is
  None, in which case it pulls the remote default branch.
2026-04-26 04:08:43 -05:00
J. Nick Koston b3a57d1395 [git] Make ref fetches and submodule updates shallow
Fixes esphome/esphome#11550

When clone_or_update is called with a ref or submodules, the initial
clone uses --depth=1 but the subsequent git fetch and git submodule
update commands run deep, pulling the full history on every refresh.
This causes excess network traffic and disk usage for external_components,
packages, and dashboard imports.

Add --depth=1 to all four call sites (initial fetch, initial submodule
init, refresh fetch, refresh submodule update). Shallow fetches still
advance the local clone to the current remote tip when it moves, so the
refresh path keeps working as before.
2026-04-26 03:57:35 -05:00
J. Nick Koston f797090f1d Merge remote-tracking branch 'origin/output-power-scaling-optional' into integration 2026-04-25 12:42:52 -05:00
b5ccd55f4e [packages] Fix premature substitution of vars in remote package files (#15997)
Co-authored-by: J. Nick Koston <nick+github@koston.org>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-04-25 17:06:58 +00:00
J. Nick Koston 835b9a55a0 Merge remote-tracking branch 'upstream/dev' into integration 2026-04-23 14:53:33 -05:00
Paulus Schoutsen 9b45b046a8 [core] Allow finding all devices as target that match mac suffix (#13135) 2026-04-23 08:43:32 -05:00
J. Nick Koston bb3ad6e431 Merge remote-tracking branch 'upstream/dev' into integration 2026-04-22 22:32:06 -05:00
Jesse Hills e1d629f0d2 [time] Handle Windows EINVAL when validating POSIX TZ strings (#15934) 2026-04-23 14:35:13 +12:00
J. Nick Koston d9cb0a8f2d Merge remote-tracking branch 'upstream/dev' into integration 2026-04-22 05:41:38 +02:00
Javier PeletierandJ. Nick Koston 9cebce1b6e [substitutions] Improve error messages with include stack trace (#15874)
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
2026-04-22 03:19:01 +02:00
J. Nick Koston 574bdc9a00 Merge remote-tracking branch 'origin/decouple_scheduler_loop_cadence' into integration 2026-04-20 12:06:28 +02:00
aad1318b4a [packages] Improve error messages with include stack and fix missing path propagation (#15844)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: J. Nick Koston <nick@home-assistant.io>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-04-19 21:09:14 +00:00
J. Nick Koston 7a23a339e9 [substitutions] Fix substitutions: !include file.yaml regression (#15850) 2026-04-20 09:00:31 +12:00
J. Nick Koston 0d43973380 Merge remote-tracking branch 'upstream/fix/substitutions-include-15848' into integration 2026-04-19 02:52:38 -05:00
J. Nick Koston 78853260a5 [substitutions] Lock in shape check and document single-shot resolve
- Add unit tests asserting cv.Invalid when `substitutions: !include list.yaml`
  resolves to a non-mapping, covering both do_substitution_pass and
  do_packages_pass.
- Note in resolve_substitutions_block that the resolve is single-shot and
  chained top-level includes are not supported (matches _walk_packages for
  `packages: !include`).
2026-04-19 02:50:20 -05:00
J. Nick Koston e67b65b669 [substitutions] [packages] Address review feedback
- Seed `resolve_include` context with `command_line_substitutions` so
  `substitutions: !include ${var}.yaml` can reference CLI-provided vars
  in the include filename (parallels the `packages: !include` path).
- Validate shape of resolved substitutions in `do_packages_pass` and raise
  `cv.Invalid` under `CONF_SUBSTITUTIONS` instead of letting `UserDict()`
  fail with a low-level exception on a non-mapping.
- Fixture 17 exercises the CLI-templated include filename.
2026-04-19 02:42:03 -05:00
J. Nick Koston ba8e729443 [substitutions] [packages] Fix substitutions: !include file.yaml regression
Resolve a deferred IncludeFile before validating the substitutions shape in
do_substitution_pass, and before wrapping it in UserDict in do_packages_pass.

Fixes esphome/esphome#15848
2026-04-19 02:25:00 -05:00
J. Nick Koston fa2a75acd0 Merge remote-tracking branch 'origin/core-chunked-setup' into integration 2026-04-17 17:46:08 -05:00
J. Nick Koston 00f08ba6ed [core] Drop per-component begin/end labels from generated main.cpp
The labels were there to help humans scanning the generated main.cpp
find component boundaries, but they were:

- Unreliable: CORE.flush_tasks can interleave coroutines on each
  await, so a component's later statements can land in another
  component's begin/end block.
- Load-bearing for a pile of complexity: a tuple return from
  _wrap_in_iifes, a has_iife flag, a comment-only detector to
  suppress trailing end-markers for comment-only components, and
  a brittle `"[]()" in line` check that could false-positive on
  YAML dumps containing lambda syntax.
- Not actually needed — generated main.cpp is a build artifact
  rarely read by anyone, and cg.LineComment("name:") already puts
  the component name at the start of its block.

ComponentMarker stays as a pure chunking sentinel — it tells
cpp_main_section where component boundaries are (for grouping) but
produces no C++ output. _wrap_in_iifes returns a plain list again.
Added a regression test for the now-defused case of a comment
containing "[]()" that was previously flagged by review.
2026-04-17 15:19:48 -05:00
J. Nick Koston f82401a504 [core] Address Copilot review: robust brace depth, accurate docstrings
- Count { and } characters per line instead of matching whole-line
  tokens. Current codegen only emits scope braces as standalone lines
  (from cg.with_local_variable()), but the defensive change is robust
  against future codegen emitting inline control flow like
  `if (cond) {` or `} else {` on one line.
- Add a regression test covering those inline-brace patterns.
- Fix stale docstrings on ComponentMarker and cpp_main_section that
  still claimed "stack frame released on return" and described the
  IIFEs as "noinline". The IIFEs have no noinline attribute and rely
  on scope-based lifetime shortening rather than guaranteed frames.
2026-04-17 15:06:42 -05:00
J. Nick Koston 178f23a7aa [core] Use begin/end marker pairs around each component's IIFE
Rename the bracket markers from "// === X ===" (same on both sides)
to "// === begin X ===" and "// === end X ===" so the generated
main.cpp reads unambiguously when scanning by component. Comment-only
components still get a single "begin X" marker since they have no
IIFE to close.
2026-04-17 15:06:42 -05:00
J. Nick Koston 864d31aa65 [core] Put ComponentMarker outside the IIFE as a visual bracket
The marker comment was being emitted as the first line *inside* each
IIFE:

  []() {
    // === logger ===
    // logger:
    //   ...
    ...
  }();

That works but buries the component label inside the lambda body, so
scanning generated main.cpp to find "where does component X's setup
live" is harder than it needs to be. Emit the marker before and after
the IIFE instead:

  // === logger ===
  []() {
    // logger:
    //   ...
    ...
  }();
  // === logger ===

Comment-only components (e.g. sha256, async_tcp, empty platforms like
binary_sensor:) don't grow a useless trailing duplicate marker —
when there's no IIFE to bracket, the marker is emitted once.
2026-04-17 15:06:42 -05:00
J. Nick Koston 936694af2c [core] Don't emit IIFE for comment-only chunks
Some components (sha256, async_tcp, network, empty text_sensor:, etc.)
emit only a ComponentMarker plus config-dump comments and no actual
C++ statements. Wrapping those in a `[]() { ... }();` IIFE is pure
clutter in the generated main.cpp — the IIFE has no body.

When _wrap_in_iifes sees a chunk whose lines are all // comments,
emit them verbatim instead of wrapping. Peak stack and flash are
unchanged on apollo and neargaragedoor since GCC was already
eliding the empty IIFEs; this just makes the generated code read
cleanly to humans.
2026-04-17 15:06:42 -05:00