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.