Commit Graph
28100 Commits
Author SHA1 Message Date
J. Nick Koston 35163e5bca Merge branch 'inline-continuation-actions' into integration 2026-04-26 22:22:06 -05:00
J. Nick Koston f40eb9b780 [core] Inline ContinuationAction in IfAction/WhileAction/RepeatAction
Replace heap-allocated ContinuationAction/WhileLoopContinuation/
RepeatLoopContinuation instances with inline members, eliminating one
heap allocation per IfAction/WhileAction/RepeatAction at setup.

Each parent already needs exactly one continuation as the chain
terminator that hands control back. Heap-allocating it costs the
~16-byte object plus an ~8-byte heap header per instance, plus heap
fragmentation. Inlining moves the same 16 bytes from heap to BSS
and drops the heap header overhead.

Per-parent net change:
- BSS: +16 B (the inline continuation; +32 B for IfAction<true>)
- Heap: -24 B per heap allocation eliminated (16 B object + ~8 B header)
- Net RAM saved: ~8 B per simple parent, ~16 B for IfAction<true>
- Plus: one fewer heap allocation per parent at setup, less fragmentation

For IfAction<HasElse=false>, the else continuation is elided via
[[no_unique_address]] + an empty wrapper struct, so it costs 0 B.

Note: CI memory analysis only measures static RAM (BSS), not heap.
This change moves bytes from heap to BSS, so the report will show
BSS increasing while the actual heap savings (and fragmentation
reduction) are not directly visible.
2026-04-26 22:17:26 -05:00
J. Nick Koston 79b741b8dc [core] Combine entity register + configure_entity_ into one call (#16030) 2026-04-27 03:03:39 +00:00
tomaszduda23 112646a9c4 [zigbee] add router for nrf52 (#16034) 2026-04-26 23:02:09 -04:00
J. Nick Koston 63e7e70d18 Merge remote-tracking branch 'upstream-ssh/light-control-action-bitmask' into integration 2026-04-26 21:55:05 -05:00
J. Nick Koston 9a468eef8c [light] Undef LIGHT_CONTROL_FIELDS macro and assert <=16 fields 2026-04-26 21:54:33 -05:00
J. Nick Koston 2e096bb036 [core] Combine set_component_source_ + register_component_ into one call (#16029) 2026-04-26 21:54:15 -05:00
J. Nick Koston ad9f21cf04 Merge remote-tracking branch 'upstream-ssh/light-control-action-bitmask' into integration 2026-04-26 21:53:27 -05:00
J. Nick Koston dae15301e1 [light] Shorten Empty<Tag> comment to one line 2026-04-26 21:41:24 -05:00
J. Nick Koston 9f776eb517 [light] Shorten FIELDS comment 2026-04-26 21:40:52 -05:00
J. Nick Koston 0b8c04983e [light] Fold CONF_EFFECT into FIELDS table 2026-04-26 21:40:05 -05:00
J. Nick Koston 3f0c669b4c [light] Reduce LOC via X-macro pattern, drop unused tag/BIT args
Apply the X-macro pattern from #15132 so the field list is declared
once and expanded into setters, play(), and storage. Drop the unused
BIT_NAME and tag arguments — only (type, name, idx) is needed since
idx doubles as both the bit position and the Empty<> tag.

Net change vs upstream is +10 LOC instead of +64.
2026-04-26 21:37:55 -05:00
J. Nick Koston 19df452771 [light] Use bitmask template parameter for LightControlAction unused fields
Parameterize LightControlAction on a uint16_t Fields bitmask encoding
which of its 14 templatable fields are configured. Unset fields are
elided from the instance via [[no_unique_address]] and skipped at
compile time in play() via if constexpr.

Real-world configs typically use only 1-5 of the 14 fields. Codegen
computes the bitmask from the YAML and passes it as the leading
template argument, so each unique field combination produces its own
type with only the storage and play() branches it actually needs.

Measured on apollo-pump-1-5d9bdc.yaml (7 instances, 3 unique masks):
- state-only (2 instances): 72 B -> 20 B = 52 B saved each
- state+RGB (2 instances): 72 B -> 32 B = 40 B saved each
- state+brightness+RGB (3 instances): 72 B -> 36 B = 36 B saved each
- Total: ~292 B RAM saved
- Flash cost: ~30-70 B for 3 play() variants + vtables
2026-04-26 21:33:57 -05:00
J. Nick Koston 771e58eb00 Merge branch 'light-dim-relative-constexpr-template' into integration 2026-04-26 21:27:15 -05:00
J. Nick Koston 19e0d05219 [light] Use constexpr template parameter for DimRelativeAction transition_length
Parameterize DimRelativeAction on a HasTransitionLength bool, mirroring
the same trick applied to ToggleAction. When transition_length is not
configured in YAML, the TemplatableFn field is elided via
[[no_unique_address]] and LightCall::set_transition_length is skipped
via if constexpr.

Saves 4 bytes RAM per dim_relative action instance and shrinks play()
by ~48 bytes when transition_length is unused.
2026-04-26 21:23:26 -05:00
J. Nick Koston 14d61c67bc Merge remote-tracking branch 'upstream-ssh/light-toggle-action-constexpr-template' into integration 2026-04-26 21:17:54 -05:00
J. Nick Koston 0cebb9014f [light] Use constexpr template parameter for ToggleAction transition_length
Parameterize ToggleAction on a HasTransitionLength bool, mirroring the
IfAction<HasElse> pattern. When transition_length is not configured in
YAML, the TemplatableFn field is elided via [[no_unique_address]] and
LightCall::set_transition_length is skipped via if constexpr.

Saves 4 bytes RAM per toggle action instance and shrinks play() from
79 to 28 bytes when transition_length is unused.
2026-04-26 21:13:58 -05:00
J. Nick Koston 84aa308557 Merge remote-tracking branch 'upstream-ssh/analyze-memory-main-cpp-attribution' into integration 2026-04-26 15:12:28 -05:00
J. Nick Koston 82ba873ea7 [analyze_memory] Attribute main.cpp setup()/loop() to esphome core
The generated src/main.cpp.o was being skipped by _scan_source_symbols
(which only walked src/esphome/) and _source_file_to_component had no
rule for it. As a result setup()/loop() fell through to the
"app_framework" heuristic in const.py instead of being counted as
[esphome]core, both via the nm scan path and the linker map path.

Scan all of src/ and recognize .../src/main.cpp.o as core.
2026-04-26 15:10:48 -05:00
J. Nick Koston c32f6f629b [infrared,radio_frequency] queue_entity_register before setup_core_
The setup_<X>_core_ functions are decorated with @setup_entity, which calls
finalize_entity_strings() before returning. queue_entity_register() must run
before that so the combined App.register_<entity>(var, name, hash, packed)
emission is selected; otherwise finalize falls back to configure_entity_ and
no register call is emitted.

Reported by Copilot review on PR #16030.
2026-04-26 14:54:15 -05:00
J. Nick Koston 97e8417ac6 Merge branch 'combine-entity-register-clean' into integration 2026-04-26 14:38:54 -05:00
J. Nick Koston 61976ca117 Merge branch 'register-component-source-overload' into integration 2026-04-26 14:38:50 -05:00
J. Nick Koston 01e0cda3c0 [core] Combine entity register + configure_entity_ into one call
Add 4-arg overloads to App.register_<entity>() that call configure_entity_()
and push_back in a single function. Codegen defers App.register_<entity>(var)
emission until finalize_entity_strings(), which then emits a single combined
App.register_<entity>(var, name, hash, packed) call instead of the previous
two-statement pair (App.register_X(var); var->configure_entity_(...)).

Apollo R-PRO-1 (ESP32-S3 IDF, 122 components, 164 entities), same toolchain:
  text:     -1248 bytes
  main.cpp: -164 lines

No behavior change. configure_entity_ remains protected on EntityBase; the
Application class is now a friend so the new overloads can call it.
2026-04-26 14:37:37 -05:00
J. Nick Koston e1ca77e025 Update tests for combined register_component_ call 2026-04-26 14:19:43 -05:00
J. Nick Koston 7aaef02eed [core] Combine set_component_source_ + register_component_ into one call
Add an optional `source_index` parameter to `App.register_component_()`
so codegen can emit a single registration line per component instead of
two. On a typical large config (~120 components) this saves ~120 lines
in generated main.cpp and one virtual-method dispatch per component
during setup().
2026-04-26 14:15:06 -05:00
J. Nick Koston 0fadf1470c Merge remote-tracking branch 'origin/external-files-content-error' into integration
# Conflicts:
#	tests/unit_tests/test_external_files.py
2026-04-26 10:40:14 -05:00
J. Nick Koston 6e13182120 Address Copilot review on PR #16023
Replace `type(mock_response).content = property(...)` with a tiny
_BodyReadErrorResponse stub class. The previous form mutates
MagicMock's class globally, which can leak into every other
MagicMock-using test in this file.
2026-04-26 10:39:29 -05:00
J. Nick Koston a94290c08c Merge remote-tracking branch 'origin/external-files-content-error' into integration
# Conflicts:
#	esphome/external_files.py
#	tests/unit_tests/test_external_files.py
2026-04-26 10:36:05 -05:00
J. Nick Koston 24a3c2202f [core] Catch body-read errors in download_content
`requests.Response.content` reads the body lazily, so chunked-decode,
gzip-decode, and mid-stream connection drops all surface as
RequestException subclasses on first access -- not from requests.get
itself. The previous code accessed `.content` outside the surrounding
try/except, so any of those (rare but real) errors would propagate
out of download_content instead of falling back to the cached file
or raising the user-friendly cv.Invalid.

Move `data = req.content` inside the try block so the existing error
path handles it. Two new tests cover the with-cache and no-cache
branches using a `ChunkedEncodingError` injected on `.content`.
2026-04-26 10:35:06 -05:00
J. Nick Koston b22e46f090 Merge remote-tracking branch 'origin/external-files-etag' into integration 2026-04-26 10:16:00 -05:00
J. Nick Koston b303cc68f6 Cover the two _write_etag error branches
EsphomeError from write_file and OSError from os.utime are both
swallowed and logged at DEBUG; add tests so neither branch can regress
silently.
2026-04-26 10:15:54 -05:00
J. Nick Koston 04bae026e7 Merge remote-tracking branch 'origin/external-files-parallel' into integration
# Conflicts:
#	esphome/external_files.py
2026-04-26 10:12:25 -05:00
J. Nick Koston a90d840c26 Address Copilot follow-up on PR #16021
- has_remote_file_changed now accepts a `timeout` argument and passes
  it to requests.head; download_content forwards its own timeout in.
  Without this, callers couldn't actually control the end-to-end
  timeout -- the GET respected it but the conditional HEAD didn't.
- Replace the (path, url) -> (url, path) lambda inside ex.map with a
  named local helper that unpacks the tuple. Easier to read; the
  awkward (item[1], item[0]) indexing was a maintenance hazard.
2026-04-26 10:11:52 -05:00
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 5686f9c415 Merge remote-tracking branch 'origin/external-files-parallel' into integration 2026-04-26 10:00:51 -05:00
J. Nick Koston 2e037308de Address Copilot follow-up on PR #16021
- download_content_many docstring corrected: ex.map yields in input order,
  so worker exceptions propagate when iteration reaches their position,
  not necessarily as the first failure.
- WEB_TYPE comment no longer claims TYPE_WEB exists in esphome.const --
  it doesn't; each component just defines its own local literal.
2026-04-26 10:00: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 fe6809da54 Merge remote-tracking branch 'origin/external-files-parallel' into integration 2026-04-26 09:45:04 -05:00
J. Nick Koston 5b28ab1c49 Cap download_content_many at 8 workers (was 16) 2026-04-26 09:44:57 -05:00
J. Nick Koston 7983ebf54a Merge remote-tracking branch 'origin/external-files-etag' into integration 2026-04-26 09:42:36 -05:00