From 30da0e81fab32197b2e915789837efc97d6b3699 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 3 Sep 2026 18:54:23 +0200 Subject: [PATCH] Adopt the shared connect poll in async_tcp and name the refused implementation --- esphome/components/api/__init__.py | 7 ++- .../components/async_tcp/async_tcp_socket.cpp | 43 +++++-------------- esphome/components/socket/__init__.py | 4 +- esphome/components/statsd/__init__.py | 2 +- tests/integration/test_api_reboot_timeout.py | 6 +-- 5 files changed, 19 insertions(+), 43 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 932cb941cb..4a5112c490 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -480,11 +480,10 @@ def _validate_outgoing_socket_implementation(config: ConfigType) -> ConfigType: socket_conf = fv.full_config.get().get("socket") or {} if ( - socket_conf.get(socket.CONF_IMPLEMENTATION) - in socket.IMPLEMENTATIONS_WITHOUT_CONNECT - ): + impl := socket_conf.get(socket.CONF_IMPLEMENTATION) + ) in socket.IMPLEMENTATIONS_WITHOUT_CONNECT: raise cv.Invalid( - "outgoing_connection is not supported with the lwip_tcp socket " + f"outgoing_connection is not supported with the {impl} socket " "implementation because it cannot make outgoing connections", path=[CONF_OUTGOING_CONNECTION], ) diff --git a/esphome/components/async_tcp/async_tcp_socket.cpp b/esphome/components/async_tcp/async_tcp_socket.cpp index 10cbc981c7..59100166d7 100644 --- a/esphome/components/async_tcp/async_tcp_socket.cpp +++ b/esphome/components/async_tcp/async_tcp_socket.cpp @@ -97,45 +97,22 @@ void AsyncClient::loop() { return; if (connecting_) { - // For connecting, we need to check writability, not readability - // The Application's select() only monitors read FDs, so we do our own check here - // For ESP platforms lwip_select() might be faster, but this code isn't used - // on those platforms anyway. If it was, we'd fix the Application select() - // to report writability instead of doing it this way. - int fd = socket_->get_fd(); - if (fd < 0) { - ESP_LOGW(TAG, "Invalid socket fd"); - close(); - return; - } - - fd_set writefds; - FD_ZERO(&writefds); - FD_SET(fd, &writefds); - - struct timeval tv = {0, 0}; - int ret = select(fd + 1, nullptr, &writefds, nullptr, &tv); - - if (ret > 0 && FD_ISSET(fd, &writefds)) { - int error = 0; - socklen_t len = sizeof(error); - if (socket_->getsockopt(SOL_SOCKET, SO_ERROR, &error, &len) == 0 && error == 0) { + int err = 0; + switch (socket::poll_connect(*socket_, err)) { + case socket::ConnectPollResult::CONNECT_POLL_PENDING: + break; + case socket::ConnectPollResult::CONNECT_POLL_CONNECTED: connecting_ = false; connected_ = true; if (connect_cb_) connect_cb_(connect_arg_, this); - } else { - ESP_LOGW(TAG, "Connection failed: %d", error); + break; + case socket::ConnectPollResult::CONNECT_POLL_ERROR: + ESP_LOGW(TAG, "Connection failed: %d", err); close(); if (error_cb_) - error_cb_(error_arg_, this, error); - } - } else if (ret < 0) { - const int err = errno; - ESP_LOGE(TAG, "Select error: %d", err); - close(); - if (error_cb_) - error_cb_(error_arg_, this, err); + error_cb_(error_arg_, this, err); + break; } } else if (connected_) { // For connected sockets, use the Application's select() results diff --git a/esphome/components/socket/__init__.py b/esphome/components/socket/__init__.py index 04f90ca360..f6ea69965b 100644 --- a/esphome/components/socket/__init__.py +++ b/esphome/components/socket/__init__.py @@ -15,10 +15,10 @@ CODEOWNERS = ["@esphome/core"] CONF_IMPLEMENTATION = "implementation" IMPLEMENTATION_LWIP_TCP = "lwip_tcp" -# Implementations whose sockets cannot make outgoing connections -IMPLEMENTATIONS_WITHOUT_CONNECT = frozenset({IMPLEMENTATION_LWIP_TCP}) IMPLEMENTATION_LWIP_SOCKETS = "lwip_sockets" IMPLEMENTATION_BSD_SOCKETS = "bsd_sockets" +# Implementations whose sockets cannot make outgoing connections +IMPLEMENTATIONS_WITHOUT_CONNECT = frozenset({IMPLEMENTATION_LWIP_TCP}) # Socket tracking infrastructure # Components register their socket needs and platforms read this to configure appropriately diff --git a/esphome/components/statsd/__init__.py b/esphome/components/statsd/__init__.py index b5a0586e31..58d4be97fa 100644 --- a/esphome/components/statsd/__init__.py +++ b/esphome/components/statsd/__init__.py @@ -1,5 +1,6 @@ import esphome.codegen as cg from esphome.components import binary_sensor, sensor +from esphome.components.const import CONF_HOST import esphome.config_validation as cv from esphome.const import ( CONF_BINARY_SENSORS, @@ -14,7 +15,6 @@ AUTO_LOAD = ["socket"] CODEOWNERS = ["@Links2004"] DEPENDENCIES = ["network"] -CONF_HOST = "host" CONF_PREFIX = "prefix" statsd_component_ns = cg.esphome_ns.namespace("statsd") diff --git a/tests/integration/test_api_reboot_timeout.py b/tests/integration/test_api_reboot_timeout.py index 953fe47648..1a0881ece5 100644 --- a/tests/integration/test_api_reboot_timeout.py +++ b/tests/integration/test_api_reboot_timeout.py @@ -16,9 +16,9 @@ async def test_api_reboot_timeout( """Test that the device reboots when no API clients connect within the timeout.""" loop = asyncio.get_running_loop() reboot_future = loop.create_future() - # The harness port probe counts as an unauthenticated client, so the - # reboot may report either form - reboot_pattern = re.compile(r"(No clients|none authenticated); rebooting") + # The harness port probe always connects without authenticating, so the + # reboot deterministically reports the unauthenticated form + reboot_pattern = re.compile(r"none authenticated; rebooting") def check_output(line: str) -> None: """Check output for reboot message."""