From 9c4a08aaadd64947116091ce351651cbca4a6ca0 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 4 Sep 2026 07:25:50 +0200 Subject: [PATCH] Capture errno before it can be clobbered, warn when no UDP address is valid, and give the reboot test timing headroom --- esphome/components/async_tcp/async_tcp_socket.cpp | 8 +++++--- esphome/components/udp/udp_component.cpp | 4 ++++ ...api_reboot_timeout_after_authenticated_disconnect.yaml | 2 +- tests/integration/test_api_reboot_timeout.py | 2 +- 4 files changed, 11 insertions(+), 5 deletions(-) diff --git a/esphome/components/async_tcp/async_tcp_socket.cpp b/esphome/components/async_tcp/async_tcp_socket.cpp index 2c2771b566..9af09da14e 100644 --- a/esphome/components/async_tcp/async_tcp_socket.cpp +++ b/esphome/components/async_tcp/async_tcp_socket.cpp @@ -43,11 +43,13 @@ bool AsyncClient::connect(const char *host, uint16_t port) { } if (socket_->setblocking(false) != 0) { - // A blocking connect()/read() would stall the whole loop - ESP_LOGE(TAG, "Failed to set nonblocking: errno %d", errno); + // Capture before the log and reset() below can clobber errno; a blocking + // connect()/read() would otherwise stall the whole loop + const int saved_errno = errno; + ESP_LOGE(TAG, "Failed to set nonblocking: errno %d", saved_errno); socket_.reset(); if (error_cb_) - error_cb_(error_arg_, this, errno); + error_cb_(error_arg_, this, saved_errno); return false; } diff --git a/esphome/components/udp/udp_component.cpp b/esphome/components/udp/udp_component.cpp index e805fc6cf9..eeb8ba85cb 100644 --- a/esphome/components/udp/udp_component.cpp +++ b/esphome/components/udp/udp_component.cpp @@ -19,6 +19,10 @@ void UDPComponent::setup() { } this->sockaddrs_.push_back(saddr); } + if (!this->addresses_.empty() && this->sockaddrs_.empty()) { + // Sending would be a silent no-op; surface the misconfiguration + this->status_set_warning(LOG_STR("no valid addresses")); + } // set up broadcast socket if (this->should_broadcast_) { this->broadcast_socket_ = socket::socket(AF_INET, SOCK_DGRAM, IPPROTO_IP); diff --git a/tests/integration/fixtures/api_reboot_timeout_after_authenticated_disconnect.yaml b/tests/integration/fixtures/api_reboot_timeout_after_authenticated_disconnect.yaml index 881bb5b2fc..e3490bbc85 100644 --- a/tests/integration/fixtures/api_reboot_timeout_after_authenticated_disconnect.yaml +++ b/tests/integration/fixtures/api_reboot_timeout_after_authenticated_disconnect.yaml @@ -2,6 +2,6 @@ esphome: name: api-reboot-test host: api: - reboot_timeout: 0.5s # Very short timeout for fast testing + reboot_timeout: 2s # Headroom to connect and authenticate a client first logger: level: DEBUG diff --git a/tests/integration/test_api_reboot_timeout.py b/tests/integration/test_api_reboot_timeout.py index 958a98ad68..96654308b3 100644 --- a/tests/integration/test_api_reboot_timeout.py +++ b/tests/integration/test_api_reboot_timeout.py @@ -59,6 +59,6 @@ async def test_api_reboot_timeout_after_authenticated_disconnect( async with api_client_connected() as client: await client.device_info() try: - await asyncio.wait_for(reboot_future, timeout=2.0) + await asyncio.wait_for(reboot_future, timeout=5.0) except TimeoutError: pytest.fail("Device did not reboot within expected timeout")