The wake-hook call (esphome_wake_ota_component_any_context / App.wake_ota_component_any_context)
was placed AFTER xTaskNotifyGive()/wake_loop_any_context() in both the fast-select callback and
the raw-TCP accept callback. That opened a race: the main task could wake, run a full iteration
(draining has_pending_enable_loop_requests_), and finish before the flag-set ran — losing the
pending-enable request until the next unrelated socket event happened to re-trigger the path.
Swap the order so the pending-enable flags are set first, then the main task is woken. The
main-loop iteration triggered by the wake is now guaranteed to see the pending request.
Note: host's yield_with_select_ path already sets and consumes the flag on the main thread
with no cross-task wake in between, so it has no race and is unchanged.
The wake hook is called only from:
- esphome_socket_event_callback (lwip fast select, LwIP TCP/IP task context)
- LWIPRawListenImpl::accept_fn_ (raw TCP accept callback)
- Application::yield_with_select_ (host select fallback, main thread)
None of those contexts require IRAM-resident code. The LwIP fast-select
event callback itself is not IRAM_ATTR; the raw-TCP accept callback runs
from a low-priority user IRQ on RP2040 where IRAM_ATTR is a no-op anyway
(it's an ESP32-specific section attribute for code that must run while
flash cache is disabled). This is not a real ISR path the way
enable_loop_soon_any_context() is (which is called from GPIO ISRs and
genuinely does need IRAM).
Removing IRAM_ATTR frees scarce IRAM on ESP32.
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.
Address copilot review on #15636.
1. Enforce single ESPHome OTA instance (BREAKING CHANGE).
The `ota_esphome_final_validate` hook has always merged multiple
`ota: - platform: esphome` configs by port so a user config and a
remote package that both define OTA would merge rather than break.
That merge behavior is preserved. But if two configs survive on
*different* ports they produce two independent listening sockets,
which is not a sane deployment: it creates ambiguity for safe_mode
coordination and for the socket wake hook added in this PR.
Raise cv.Invalid when more than one port remains after merging.
2. Drop redundant main-loop wake in Application::wake_ota_component_any_context.
Every caller (lwip fast-select callback already calls xTaskNotifyGive,
raw-tcp accept callback already calls wake_loop_any_context(), host
path is already running post-select in the main loop) has woken the
main loop by the time we reach this hook. Calling
enable_loop_soon_any_context() would re-wake it. Application is a
friend of Component, so set pending_enable_loop_ and
has_pending_enable_loop_requests_ directly instead.
ESPHomeOTAComponent::loop() previously ran every main-loop tick just to
check `client_ != nullptr || server_->ready()` — a wasted dispatch on
every device, since OTA is idle the vast majority of the time.
OTA now disables its own loop after setup() and after cleanup_connection_().
A single 4-byte Component* slot in Application (only compiled in under
USE_OTA) lets the existing socket-wake paths call
enable_loop_soon_any_context() on the registered OTA component:
- ESP32 / LibreTiny (lwip fast select): hooked in
esphome_socket_event_callback on NETCONN_EVT_RCVPLUS.
- ESP8266 / RP2040 (raw TCP): hooked in LWIPRawListenImpl::accept_fn_
right after the existing wake_loop_any_context() call.
- Host (select fallback): called after select() returns ready in
Application::yield_with_select_.
False wakes (e.g. an API-socket event firing the fast-select callback)
land in ESPHomeOTAComponent::loop(), which re-disables itself immediately
when idle. Net cost is still far below running every tick.
This is deliberately an OTA-only hook: OTA is the only component that
benefits, and a single global slot avoids adding per-socket Component*
storage, wake-callback lists, or any new API churn to the socket layer.
The previous LwIPLock for RP2040+Ethernet only called
ethernet_arch_lwip_begin/end, taking the async_context lock but never
bumping arduino-pico's __inLWIP counter. The W5500 GPIO IRQ path
(LwipIntfDev::_irq) checks __inLWIP to decide whether to defer packet
processing; with __inLWIP == 0 it would proceed to run _lwipCallback
immediately from IRQ context even while the main thread held the
async_context lock, re-entering lwIP and corrupting pbuf chains.
The corruption surfaced later as the pbuf_cat assertion
"p->tot_len == p->len (of last pbuf in chain)" panicking the device
on wiznet-w5500-evb-pico boards configured with an interrupt pin.
Mirror arduino-pico's LWIPMutex (cores/rp2040/lwip_wrap.h) exactly:
bump __inLWIP, take the lock, and on release re-unmask any GPIO IRQs
that were deferred while we held it (__needsIRQEN). We can't write
`using LwIPLock = LWIPMutex;` in core/helpers.h because pulling
lwip_wrap.h there transitively poisons many translation units with
lwIP types in an order that breaks err_t resolution, so the body is
inlined in the rp2040 helpers.cpp where the include is scoped.
The X-macro file requires ENTITY_TYPE_ and ENTITY_CONTROLLER_TYPE_
macros to be defined before inclusion. The generated esphome.h includes
all .h files bare, which causes compilation errors.
Neither PlatformIO nor ESP-IDF/CMake copy .inc files to the build
directory. Rename to .h so it's recognized by both build systems.
Exempt from pragma-once lint since this file is intentionally included
multiple times with different macro definitions.
clang-tidy flags macro arguments that aren't parenthesized, but
wrapping template type arguments in parentheses would break C++ syntax.
Add NOLINTBEGIN/END around the remaining macro blocks.
PlatformIO only copies files with extensions listed in
SOURCE_FILE_EXTENSIONS to the build directory. Add .inc so that
X-macro include files (entity_types.inc) are available during
compilation.
Introduce entity_types.inc X-macro and entity_includes.h shared header
to eliminate ~1000 lines of repetitive #ifdef/entity-type blocks across
core files.
entity_types.inc defines each entity type once with two macros:
- ENTITY_TYPE_ for non-controller entities (button, infrared)
- ENTITY_CONTROLLER_TYPE_ for entities with controller callbacks
Each consumer includes the file with appropriate macro definitions.
Sites wanting all entities delegate ENTITY_CONTROLLER_TYPE_ to
ENTITY_TYPE_. Sites wanting only controller entities define
ENTITY_TYPE_ as empty.
entity_includes.h consolidates the conditional entity header includes
shared by application.h, controller.h, and controller_registry.h.
Also makes ComponentIterator::on_media_player pure virtual for
consistency (trivial override added to web_server::ListEntitiesIterator).