PlatformIO's idedata may list flash images that do not exist on disk
(e.g. a tasmota tinyuf2.bin referenced by the adafruit_qtpy_esp32s3_n4r2
board). Previously the CLI passed every entry straight to esptool, which
aborted the entire flash with "No such file or directory". The dashboard
path is unaffected because it flashes the pre-merged firmware.factory.bin
produced by the post-build step, which already tolerates missing inputs.
Filter non-existent extra_flash_images with a warning so a stale or
incorrect platform-declared image no longer breaks esphome run.
Fixes https://github.com/esphome/esphome/issues/15634
- get_app_state(): say 'STATUS_LED_* only', not 'STATUS_LED_* and lifecycle'
since lifecycle bits are no longer maintained in app_state_.
- STATUS_LED_SETTLE_S: remove incorrect mention of feed_wdt re-dispatch;
status_led_light is driven by the main loop, not feed_wdt.
- snapshot_led service: say 'status_led_light output' not 'pin state'
since the fixture uses a template output, not GPIO.
Extract the 0.3s magic sleep into a named constant explaining why that
duration is chosen (feed_wdt re-dispatches every ~3 ms; 300 ms gives
~100 opportunities). Fix the idle-write check to snapshot AFTER the
clear instead of before it, so writes still in-flight from the error
phase don't inflate the delta.
Verifies that after clearing all status flags, re-setting a flag makes
status_led_light resume writing to its output. Guards against a future
idle optimization (like #15642) where status_led disables its own
loop() when idle: if the re-enable path were broken, the second set
would not produce writes.
Also checks that writes STOP after all flags are cleared (counter
should not keep growing), proving status_led_light correctly stops
blinking in steady state.
Drop the trailing underscore from any_component_has_status_flag now
that the method is public. Trailing underscores in the codebase are
reserved for protected/private members per clang-tidy naming rules,
which caused a CI failure on the previously-named public helper.
Add integration tests covering:
- Single-component status_set/clear for warning and error
- Multi-component OR semantics (both clear orders)
- Warning and error independence
- End-to-end proof that status_led_light::loop() reads App.app_state_
and writes its output when the bits are set (via a fake template
output whose write_action bumps a counter exposed as a sensor)
Addresses copilot review on #15636.
1. Fix cleanup_connection_() race with queued listener events.
While an OTA session was active, a second incoming connection would
fire esphome_socket_event_callback → esphome_wake_ota_component_any_context,
which sets pending_enable_loop_ on the (still-active) OTA component.
enable_pending_loops_() only scans the inactive section, so that flag
goes invisible. When cleanup_connection_() then called disable_loop(),
the component dropped to LOOP_DONE with a stale pending flag and
nothing to re-trigger the scan — the queued client sat forever until
some unrelated socket activity woke the main loop.
Fix: don't call disable_loop() from cleanup_connection_(). loop() has
the idempotent idle check at its top; one more dispatch after cleanup
is cheap and guarantees we re-read server_->ready() and either accept
the queued client or disable cleanly.
2. Tighten the multi-port error message. Merging is fine — the constraint
is single-port. Reworded: "Only a single port is supported for 'ota'
'platform: esphome'. Got ports [...]. Consolidate onto a single port;
configs sharing a port are merged automatically."
3. Comment drift: three call sites and the fast-select extern declaration
still referred to enable_loop_soon_any_context() and implied the hook
wakes the main loop. Updated to reflect the current mechanism (sets
pending-enable flags only; callers have already woken the main loop).
Also clarified that esphome_wake_ota_component_any_context fires on
every RCVPLUS event across all monitored sockets, so false wakes are
expected and OTA::loop() disables itself again when idle.
4. Added tests/component_tests/ota/test_esphome_ota.py covering
ota_esphome_final_validate: single instance accepted, same-port
configs merge, different-port configs rejected with cv.Invalid,
non-esphome platforms unaffected.