- 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.
- TemplatableFn = delete now catches both stateful lambdas AND stateless
lambdas with inconvertible return types (e.g., string -> int)
- Add test for to_exp with non-string output_type (lambda-wraps result)