From e5fe697b567903b03418049527d83fc24fcd375e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 26 Sep 2026 14:11:32 +0100 Subject: [PATCH] Trim the flash and RAM cost of the outgoing connection option Keep the listen socket inline in setup() so no build pays for an extra function, log every reason not to dial from one verbose call site, drop the v4-mapped prefix table in favour of word compares, and route the config tests through the public schema entry points. --- esphome/components/api/__init__.py | 5 + .../api/api_outgoing_connection.cpp | 75 +++++----- .../components/api/api_outgoing_connection.h | 14 +- esphome/components/api/api_server.cpp | 135 ++++++++---------- esphome/components/api/api_server.h | 15 +- .../api/test_outgoing_connection.py | 32 +---- .../api/test_outgoing_connection_ipv6.yaml | 20 --- .../test-outgoing-connection.bk72xx-ard.yaml | 4 +- .../test-outgoing-connection.esp32-idf.yaml | 4 +- .../test-outgoing-connection.esp8266-ard.yaml | 4 +- .../api/test-outgoing-connection.host.yaml | 4 +- .../test-outgoing-connection.rp2040-ard.yaml | 4 +- ...imeout_after_authenticated_disconnect.yaml | 7 - tests/integration/test_api_reboot_timeout.py | 33 +---- 14 files changed, 128 insertions(+), 228 deletions(-) delete mode 100644 tests/component_tests/api/test_outgoing_connection_ipv6.yaml delete mode 100644 tests/integration/fixtures/api_reboot_timeout_after_authenticated_disconnect.yaml diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 9dc4c8f3d3e..34e6b37521f 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -52,6 +52,7 @@ from esphome.core import CORE, ID, CoroPriority, EsphomeError, coroutine_with_pr from esphome.cpp_generator import MockObj, TemplateArgsType import esphome.final_validate as fv from esphome.helpers import fnv1_hash +from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor from esphome.types import ConfigFragmentType, ConfigType # Compat alias: downstream consumers (e.g. device-builder) referenced the @@ -325,9 +326,13 @@ _OUTGOING_CONNECTION_SCHEMA = cv.Schema( ) +@schema_extractor("schema") def _outgoing_connection_schema(config: ConfigType | None) -> ConfigType: # A bare `outgoing_connection:` block is valid; without a host the device # dials the remembered last dial-back client + if config is SCHEMA_EXTRACT: + # Let the language-schema dumper walk host, port and delay + return _OUTGOING_CONNECTION_SCHEMA if config is None: config = {} return _OUTGOING_CONNECTION_SCHEMA(config) diff --git a/esphome/components/api/api_outgoing_connection.cpp b/esphome/components/api/api_outgoing_connection.cpp index d1be8006141..6838885d252 100644 --- a/esphome/components/api/api_outgoing_connection.cpp +++ b/esphome/components/api/api_outgoing_connection.cpp @@ -17,11 +17,10 @@ namespace esphome::api { static const char *const TAG = "api.outgoing"; #ifndef API_OUTGOING_CONNECTION_HOST -#if USE_NETWORK_IPV6 -// A dual-stack listener reports an IPv4 peer as ::ffff:a.b.c.d -static constexpr uint8_t V4_MAPPED_PREFIX[12] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0xFF, 0xFF}; +static constexpr uint32_t OUTGOING_TARGET_PREF_HASH = 629847102UL; #endif +#ifndef API_OUTGOING_CONNECTION_HOST // Read the connection's peer address into target; false when unavailable or // of a family this build cannot dial static bool peer_to_target(APIConnection *conn, SavedOutgoingTarget &target) { @@ -35,9 +34,12 @@ static bool peer_to_target(APIConnection *conn, SavedOutgoingTarget &target) { if (family == AF_INET6) { const auto *addr6 = reinterpret_cast(&peer); const auto *bytes = reinterpret_cast(&addr6->sin6_addr); - if (memcmp(bytes, V4_MAPPED_PREFIX, sizeof(V4_MAPPED_PREFIX)) == 0) { + uint32_t prefix[3]; + memcpy(prefix, bytes, sizeof(prefix)); + // A dual-stack listener reports an IPv4 peer as ::ffff:a.b.c.d + if (prefix[0] == 0 && prefix[1] == 0 && prefix[2] == htonl(0xFFFFUL)) { target.family = AF_INET; - memcpy(target.addr, bytes + sizeof(V4_MAPPED_PREFIX), 4); + memcpy(target.addr, bytes + sizeof(prefix), 4); return true; } target.family = AF_INET6; @@ -83,26 +85,28 @@ socklen_t OutgoingConnectionManager::target_sockaddr_(struct sockaddr_storage *a #endif } +#ifndef API_OUTGOING_CONNECTION_HOST void OutgoingConnectionManager::format_target_(std::span buf) const { struct sockaddr_storage addr; socklen_t addr_len = this->target_sockaddr_(&addr); - if (addr_len == 0 || socket::format_sockaddr_to((struct sockaddr *) &addr, addr_len, buf) == 0) { + if (addr_len == 0) { buf[0] = '\0'; + return; } + // Clears buf itself if it cannot format the address + socket::format_sockaddr_to((struct sockaddr *) &addr, addr_len, buf); } +#endif void OutgoingConnectionManager::setup() { #ifndef API_OUTGOING_CONNECTION_HOST - this->target_pref_ = global_preferences->make_preference(629847102UL, true); + this->target_pref_ = global_preferences->make_preference(OUTGOING_TARGET_PREF_HASH, true); struct sockaddr_storage addr; + // dump_config() prints whichever target this leaves in place if (this->target_pref_.load(&this->saved_) && this->target_sockaddr_(&addr) != 0) { this->host_persisted_ = true; - char host[socket::SOCKADDR_STR_LEN]; - this->format_target_(host); - ESP_LOGD(TAG, "Loaded target %s", host); } else { // Never saved, failed its size or CRC check, or holds an unknown family - ESP_LOGD(TAG, "No saved target"); this->saved_ = {}; } #endif @@ -120,13 +124,8 @@ void OutgoingConnectionManager::loop(APIServer *server) { const uint32_t now = App.get_loop_component_start_time(); switch (this->state_) { case DialState::DIAL_STATE_IDLE: -#ifdef USE_DEEP_SLEEP - // A deep sleep wake window is too short to spend on the delay - this->schedule_wait_(now, BACKOFF_MIN_MS); -#else // Target went away; give it the configured delay to reconnect first - this->schedule_wait_(now, API_OUTGOING_CONNECTION_DELAY); -#endif + this->schedule_wait_(now, IDLE_WAIT_MS); break; case DialState::DIAL_STATE_WAITING: if (now - this->state_ts_ >= this->wait_) { @@ -148,15 +147,13 @@ void OutgoingConnectionManager::try_dial_(APIServer *server, uint32_t now) { } struct sockaddr_storage addr; socklen_t addr_len = this->target_sockaddr_(&addr); - if (addr_len == 0) { - // The steady state until a dial-back client has ever connected - ESP_LOGV(TAG, "Not dialing: no target"); - this->schedule_wait_(now, PRECONDITION_RETRY_MS); - return; - } const bool at_limit = server->at_client_limit_(); - if (at_limit || !server->noise_ctx_.has_psk()) { - ESP_LOGD(TAG, "Not dialing: %s", at_limit ? LOG_STR_LITERAL("max connections") : LOG_STR_LITERAL("no key")); + // No target is the steady state until a dial-back client has ever connected + if (addr_len == 0 || at_limit || !server->noise_ctx_.has_psk()) { + // Repeats for as long as the reason holds, so keep it out of debug logs + ESP_LOGV(TAG, "Not dialing: %s", + addr_len == 0 ? LOG_STR_LITERAL("no target") + : (at_limit ? LOG_STR_LITERAL("max connections") : LOG_STR_LITERAL("no key"))); // Not a dial failure; retry without escalating the backoff this->schedule_wait_(now, PRECONDITION_RETRY_MS); return; @@ -168,9 +165,13 @@ void OutgoingConnectionManager::try_dial_(APIServer *server, uint32_t now) { this->schedule_retry_(now); return; } +#ifdef API_OUTGOING_CONNECTION_HOST + ESP_LOGD(TAG, "Dialing " API_OUTGOING_CONNECTION_HOST ":%u", API_OUTGOING_CONNECTION_PORT); +#else char host[socket::SOCKADDR_STR_LEN]; - this->format_target_(host); + socket::format_sockaddr_to((struct sockaddr *) &addr, addr_len, host); ESP_LOGD(TAG, "Dialing %s:%u", host, API_OUTGOING_CONNECTION_PORT); +#endif int err = this->dial_socket_->connect((struct sockaddr *) &addr, addr_len); if (err == 0) { // Immediate success (possible for localhost) @@ -178,7 +179,7 @@ void OutgoingConnectionManager::try_dial_(APIServer *server, uint32_t now) { return; } if (errno != EINPROGRESS) { - ESP_LOGW(TAG, "Connect failed: errno %d", errno); + ESP_LOGW(TAG, "Connect failed: %d", errno); this->schedule_retry_(now); return; } @@ -279,21 +280,29 @@ void OutgoingConnectionManager::on_target_client(APIConnection *conn) { } char host[socket::SOCKADDR_STR_LEN]; this->format_target_(host); - ESP_LOGD(TAG, "Saved %s as outgoing connection target", host); + ESP_LOGD(TAG, "Remembered %s as the dial target", host); #endif } void OutgoingConnectionManager::dump_config() const { - char buf[socket::SOCKADDR_STR_LEN]; - this->format_target_(buf); - const char *host = buf[0] != '\0' ? buf : "none remembered yet"; // The boot delay differs from delay: on deep sleep builds, so print the // value that actually applies ESP_LOGCONFIG(TAG, " Outgoing connection port: %u\n" - " Outgoing connection host: %s\n" " Outgoing connection boot delay: %" PRIu32 "ms", - API_OUTGOING_CONNECTION_PORT, host, BOOT_WAIT_MS); + API_OUTGOING_CONNECTION_PORT, BOOT_WAIT_MS); + // The text stays in the format string so ESP8266 holds it in flash, not RAM +#ifdef API_OUTGOING_CONNECTION_HOST + ESP_LOGCONFIG(TAG, " Outgoing connection host: " API_OUTGOING_CONNECTION_HOST); +#else + char buf[socket::SOCKADDR_STR_LEN]; + this->format_target_(buf); + if (buf[0] == '\0') { + ESP_LOGCONFIG(TAG, " Outgoing connection host: none remembered yet"); + } else { + ESP_LOGCONFIG(TAG, " Outgoing connection host: %s", buf); + } +#endif } } // namespace esphome::api diff --git a/esphome/components/api/api_outgoing_connection.h b/esphome/components/api/api_outgoing_connection.h index e1f4ee09b94..cc8c1bd34e2 100644 --- a/esphome/components/api/api_outgoing_connection.h +++ b/esphome/components/api/api_outgoing_connection.h @@ -59,12 +59,14 @@ class OutgoingConnectionManager { static constexpr uint32_t CONNECT_POLL_INTERVAL_MS = 250; static constexpr uint32_t NETWORK_RETRY_MS = 500; static constexpr uint32_t PRECONDITION_RETRY_MS = 5000; - // Boot waits for the client to connect in first; a deep sleep wake window - // is short, so connecting out immediately is the wake state + // A deep sleep wake window is too short to spend on the delay, so those + // builds dial out as soon as the target is gone #ifdef USE_DEEP_SLEEP static constexpr uint32_t BOOT_WAIT_MS = 0; + static constexpr uint32_t IDLE_WAIT_MS = BACKOFF_MIN_MS; #else static constexpr uint32_t BOOT_WAIT_MS = API_OUTGOING_CONNECTION_DELAY; + static constexpr uint32_t IDLE_WAIT_MS = API_OUTGOING_CONNECTION_DELAY; #endif void try_dial_(APIServer *server, uint32_t now); @@ -75,17 +77,17 @@ class OutgoingConnectionManager { void schedule_retry_(uint32_t now); // Wait without escalating the backoff (used for unmet preconditions) void schedule_wait_(uint32_t now, uint32_t wait); + /// Fill addr with the target and return its length, or 0 when there is none + socklen_t target_sockaddr_(struct sockaddr_storage *addr) const; #ifndef API_OUTGOING_CONNECTION_HOST // Write saved_ to flash, tracking success in host_persisted_ bool persist_target_() { this->host_persisted_ = this->target_pref_.save(&this->saved_) && global_preferences->sync(); return this->host_persisted_; } -#endif - /// Fill addr with the target and return its length, or 0 when there is none - socklen_t target_sockaddr_(struct sockaddr_storage *addr) const; - /// Format the target for a log line; empty when there is none + /// Format the remembered target for a log line; empty when there is none void format_target_(std::span buf) const; +#endif // Pointers first (4 bytes each on 32-bit) std::unique_ptr dial_socket_; diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index d6fe2c973ea..aba152aa74f 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -56,46 +56,12 @@ APIServer::APIServer() { global_api_server = this; } void APIServer::socket_failed_(const LogString *msg) { ESP_LOGW(TAG, "Socket %s: errno %d", LOG_STR_ARG(msg), errno); this->destroy_socket_(); -} - -bool APIServer::create_listen_socket_() { - this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections - if (this->socket_ == nullptr) { - this->socket_failed_(LOG_STR("creation")); - return false; - } - int enable = 1; - int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); - if (err != 0) { - ESP_LOGW(TAG, "Socket reuseaddr: errno %d", errno); - // we can still continue - } - err = this->socket_->setblocking(false); - if (err != 0) { - this->socket_failed_(LOG_STR("nonblocking")); - return false; - } - - struct sockaddr_storage server; - - socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); - if (sl == 0) { - this->socket_failed_(LOG_STR("set sockaddr")); - return false; - } - - err = this->socket_->bind((struct sockaddr *) &server, sl); - if (err != 0) { - this->socket_failed_(LOG_STR("bind")); - return false; - } - - err = this->socket_->listen(this->listen_backlog_); - if (err != 0) { - this->socket_failed_(LOG_STR("listen")); - return false; - } - return true; +#ifdef USE_API_OUTGOING_CONNECTION + // Dial-out needs no listener; degrade instead of stopping the component + this->status_set_error(LOG_STR("listen socket failed")); +#else + this->mark_failed(); +#endif } void APIServer::setup() { @@ -111,16 +77,6 @@ void APIServer::setup() { #endif #endif - if (!this->create_listen_socket_()) { -#ifdef USE_API_OUTGOING_CONNECTION - // Dial-out needs no listener; degrade instead of stopping the component - this->status_set_error(LOG_STR("listen socket failed")); -#else - this->mark_failed(); - return; -#endif - } - #ifdef USE_LOGGER if (logger::global_logger != nullptr) { logger::global_logger->add_log_callback( @@ -169,6 +125,44 @@ void APIServer::setup() { #ifdef USE_API_OUTGOING_CONNECTION this->outgoing_conn_.setup(); #endif + + // Listener last: on failure socket_failed_() returns early, and an + // outgoing_connection build keeps dialing out without one + this->socket_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0).release(); // monitored for incoming connections + if (this->socket_ == nullptr) { + this->socket_failed_(LOG_STR("creation")); + return; + } + int enable = 1; + int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); + if (err != 0) { + ESP_LOGW(TAG, "Socket reuseaddr: errno %d", errno); + // we can still continue + } + err = this->socket_->setblocking(false); + if (err != 0) { + this->socket_failed_(LOG_STR("nonblocking")); + return; + } + + struct sockaddr_storage server; + + socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_); + if (sl == 0) { + this->socket_failed_(LOG_STR("set sockaddr")); + return; + } + + err = this->socket_->bind((struct sockaddr *) &server, sl); + if (err != 0) { + this->socket_failed_(LOG_STR("bind")); + return; + } + + err = this->socket_->listen(this->listen_backlog_); + if (err != 0) { + this->socket_failed_(LOG_STR("listen")); + } } void APIServer::loop() { @@ -191,12 +185,7 @@ void APIServer::loop() { if (this->reboot_timeout_ != 0 && !this->provisioning_pending_()) { const uint32_t now = App.get_loop_component_start_time(); if (now - this->last_connected_ > this->reboot_timeout_) { - // Distinguish a wrong-key peer from nothing connecting at all - if (this->saw_unauthenticated_client_) { - ESP_LOGE(TAG, "Clients connected but none authenticated; rebooting"); - } else { - ESP_LOGE(TAG, "No clients; rebooting"); - } + ESP_LOGE(TAG, "No clients; rebooting"); App.reboot(); } } @@ -281,9 +270,6 @@ void APIServer::remove_client_(uint8_t client_index) { // healthy session's timestamp and trigger a spurious reboot if (was_authenticated) { this->last_connected_ = App.get_loop_component_start_time(); - this->saw_unauthenticated_client_ = false; - } else { - this->saw_unauthenticated_client_ = true; } if (this->api_connection_count_ == 0 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) { this->status_set_warning(LOG_STR("waiting for client connection")); @@ -317,18 +303,12 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() { ESP_LOGD(TAG, "Accept %s", peername); - this->add_client_(new APIConnection(std::move(sock), this)); + this->add_client_(std::move(sock)); } } -bool APIServer::add_client_(APIConnection *conn) { - if (this->at_client_limit_()) { - // The accept path checks first to skip the allocation; the outgoing - // handoff relies on this check - ESP_LOGW(TAG, "Max connections (%d), dropping client", MAX_API_CONNECTIONS); - delete conn; - return false; - } +APIConnection *APIServer::add_client_(std::unique_ptr sock) { + auto *conn = new APIConnection(std::move(sock), this); // NOLINT(cppcoreguidelines-owning-memory) this->clients_[this->api_connection_count_++].reset(conn); conn->start(); @@ -338,22 +318,21 @@ bool APIServer::add_client_(APIConnection *conn) { if (this->api_connection_count_ == 1 && this->reboot_timeout_ != 0 && !this->provisioning_pending_()) { this->status_clear_warning(); } - return true; + return conn; } #ifdef USE_API_OUTGOING_CONNECTION APIConnection *APIServer::add_outgoing_client_(std::unique_ptr sock) { - // Re-check at the handoff: the PSK may have been cleared since the dial - // started (mark_outgoing() needs the noise helper); add_client_ re-checks - // the slot limit - if (!this->noise_ctx_.has_psk()) { - ESP_LOGW(TAG, "Dropping outgoing connection (no key)"); - return nullptr; - } - auto *conn = new APIConnection(std::move(sock), this); - if (!this->add_client_(conn)) { + // Re-check at the handoff: inbound clients may have taken the last slot and + // the PSK may have been cleared since the dial started (mark_outgoing() + // needs the noise helper) + const bool at_limit = this->at_client_limit_(); + if (at_limit || !this->noise_ctx_.has_psk()) { + ESP_LOGW(TAG, "Dropping outgoing connection (%s)", + at_limit ? LOG_STR_LITERAL("max connections") : LOG_STR_LITERAL("no key")); return nullptr; } + auto *conn = this->add_client_(std::move(sock)); // After start(): sends our server hello first so the peer can pick the key conn->mark_outgoing(); return conn; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index 581c421f1c6..ec6754d90f9 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -272,9 +272,9 @@ class APIServer final : public Component protected: // Accept incoming socket connections. Only called when socket has pending connections. void __attribute__((noinline)) accept_new_connections_(); - // Insert a constructed connection into the client slots and start it. - // Takes ownership; deletes the connection and returns false at the limit - bool add_client_(APIConnection *conn); + /// Takes the socket into a new connection and starts it; callers must have + /// checked at_client_limit_() first + APIConnection *add_client_(std::unique_ptr sock); bool at_client_limit_() const { return this->api_connection_count_ >= MAX_API_CONNECTIONS; } #ifdef USE_API_OUTGOING_CONNECTION // Returns the new connection, or nullptr (socket dropped) when at the limit @@ -322,8 +322,9 @@ class APIServer final : public Component delete this->socket_; this->socket_ = nullptr; } + /// Log the failure, drop the listen socket, and mark the component failed + /// unless this build can still dial out void socket_failed_(const LogString *msg); - bool create_listen_socket_(); // Pointers and pointer-like types first (4 bytes each) socket::ListenSocket *socket_{nullptr}; #ifdef USE_API_CLIENT_CONNECTED_TRIGGER @@ -376,11 +377,7 @@ class APIServer final : public Component // Connection limits - these defaults will be overridden by config values // from cv.SplitDefault in __init__.py which sets platform-specific defaults. uint8_t listen_backlog_{4}; - // Bit-packed so the two flags share one byte - bool shutting_down_ : 1 = false; - // For the reboot log: whether any removal since the last watchdog refresh - // was an unauthenticated session (e.g. a wrong-key peer) - bool saw_unauthenticated_client_ : 1 = false; + bool shutting_down_ = false; uint8_t api_connection_count_{0}; #ifdef USE_API_OUTGOING_CONNECTION // Connected clients whose hello declared them a dial-back target diff --git a/tests/component_tests/api/test_outgoing_connection.py b/tests/component_tests/api/test_outgoing_connection.py index 8a6789414e2..d77bdf96019 100644 --- a/tests/component_tests/api/test_outgoing_connection.py +++ b/tests/component_tests/api/test_outgoing_connection.py @@ -5,17 +5,11 @@ from pathlib import Path import pytest -from esphome.components import socket -from esphome.components.api import ( - CONFIG_SCHEMA, - FINAL_VALIDATE_SCHEMA, - _validate_outgoing_host_ipv6, -) +from esphome.components.api import CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA from esphome.components.esp32 import KEY_BOARD, KEY_VARIANT, VARIANT_ESP32 import esphome.config_validation as cv from esphome.const import PlatformFramework from esphome.core import CORE -import esphome.final_validate as fv from esphome.types import ConfigType from tests.component_tests.types import SetCoreConfigCallable @@ -43,16 +37,6 @@ def test_outgoing_connection_generates_defines( assert str(defines["API_OUTGOING_CONNECTION_DELAY"]) == "60000" -def test_outgoing_connection_defaults( - set_core_config: SetCoreConfigCallable, -) -> None: - set_core_config(PlatformFramework.ESP32_IDF, platform_data=ESP32_PLATFORM_DATA) - config = CONFIG_SCHEMA(_api_config({"host": "192.168.1.2"})) - outgoing = config["outgoing_connection"] - assert outgoing["port"] == 6054 - assert outgoing["delay"].total_milliseconds == 60000 - - def test_outgoing_connection_bare_block( set_core_config: SetCoreConfigCallable, ) -> None: @@ -92,7 +76,6 @@ def test_outgoing_connection_accepts_raw_lwip( ) -> None: """The raw lwip_tcp socket these platforms default to can dial out.""" set_core_config(platform_framework) - fv.full_config.set({"socket": socket.CONFIG_SCHEMA({})}) config = CONFIG_SCHEMA(_api_config({"host": "192.168.1.2"})) assert FINAL_VALIDATE_SCHEMA(config) is not None @@ -111,7 +94,7 @@ def test_outgoing_connection_ipv6_host_requires_ipv6( set_core_config(PlatformFramework.ESP32_IDF, platform_data=ESP32_PLATFORM_DATA) config = CONFIG_SCHEMA(_api_config({"host": "fd00::1"})) with pytest.raises(cv.Invalid, match="IPv6 is not"): - _validate_outgoing_host_ipv6(config) + FINAL_VALIDATE_SCHEMA(config) def test_outgoing_connection_ipv6_host_passes_with_ipv6_enabled( @@ -123,13 +106,4 @@ def test_outgoing_connection_ipv6_host_passes_with_ipv6_enabled( full_config={"network": {"enable_ipv6": True}}, ) config = CONFIG_SCHEMA(_api_config({"host": "fd00::1"})) - assert _validate_outgoing_host_ipv6(config) is config - - -def test_outgoing_connection_ipv6_host_with_ipv6( - generate_main: Callable[[str | Path], str], -) -> None: - generate_main("tests/component_tests/api/test_outgoing_connection_ipv6.yaml") - - defines = {define.name: define.value for define in CORE.defines} - assert str(defines["API_OUTGOING_CONNECTION_HOST"]) == '"fd00::1"' + assert FINAL_VALIDATE_SCHEMA(config) is not None diff --git a/tests/component_tests/api/test_outgoing_connection_ipv6.yaml b/tests/component_tests/api/test_outgoing_connection_ipv6.yaml deleted file mode 100644 index 4a1027a15cd..00000000000 --- a/tests/component_tests/api/test_outgoing_connection_ipv6.yaml +++ /dev/null @@ -1,20 +0,0 @@ -esphome: - name: test - -esp32: - board: esp32dev - -wifi: - ssid: SomeNetwork - password: SomePassword - -network: - enable_ipv6: true - -logger: - -api: - encryption: - key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU= - outgoing_connection: - host: fd00::1 diff --git a/tests/components/api/test-outgoing-connection.bk72xx-ard.yaml b/tests/components/api/test-outgoing-connection.bk72xx-ard.yaml index 6de64e58f03..97648ec6d28 100644 --- a/tests/components/api/test-outgoing-connection.bk72xx-ard.yaml +++ b/tests/components/api/test-outgoing-connection.bk72xx-ard.yaml @@ -1,5 +1,5 @@ packages: - common: !include common-base.yaml + common: !include common.yaml wifi: ssid: MySSID @@ -7,7 +7,5 @@ wifi: # Outgoing connection on the lwip_sockets implementation used by LibreTiny api: - encryption: - key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU= outgoing_connection: host: 192.168.1.2 diff --git a/tests/components/api/test-outgoing-connection.esp32-idf.yaml b/tests/components/api/test-outgoing-connection.esp32-idf.yaml index 0796faf6ab2..04013a89867 100644 --- a/tests/components/api/test-outgoing-connection.esp32-idf.yaml +++ b/tests/components/api/test-outgoing-connection.esp32-idf.yaml @@ -1,5 +1,5 @@ packages: - common: !include common-base.yaml + common: !include common.yaml wifi: ssid: MySSID @@ -8,8 +8,6 @@ wifi: # Outgoing connection: the device dials out when no dial-back client is # connected. Requires encryption so the peer is verified by key. api: - encryption: - key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU= outgoing_connection: host: 192.168.1.2 port: 6054 diff --git a/tests/components/api/test-outgoing-connection.esp8266-ard.yaml b/tests/components/api/test-outgoing-connection.esp8266-ard.yaml index 74f1c23dc73..0cf4b0b4b4f 100644 --- a/tests/components/api/test-outgoing-connection.esp8266-ard.yaml +++ b/tests/components/api/test-outgoing-connection.esp8266-ard.yaml @@ -1,5 +1,5 @@ packages: - common: !include common-base.yaml + common: !include common.yaml wifi: ssid: MySSID @@ -7,7 +7,5 @@ wifi: # Outgoing connection on the raw lwip_tcp implementation used by ESP8266 and RP2040 api: - encryption: - key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU= outgoing_connection: host: 192.168.1.2 diff --git a/tests/components/api/test-outgoing-connection.host.yaml b/tests/components/api/test-outgoing-connection.host.yaml index 0f2e0d62762..deb45b99f9d 100644 --- a/tests/components/api/test-outgoing-connection.host.yaml +++ b/tests/components/api/test-outgoing-connection.host.yaml @@ -1,11 +1,9 @@ packages: - common: !include common-base.yaml + common: !include common.yaml network: # No host set: the device dials the last remembered Home Assistant address api: - encryption: - key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU= outgoing_connection: delay: 30s diff --git a/tests/components/api/test-outgoing-connection.rp2040-ard.yaml b/tests/components/api/test-outgoing-connection.rp2040-ard.yaml index 74f1c23dc73..0cf4b0b4b4f 100644 --- a/tests/components/api/test-outgoing-connection.rp2040-ard.yaml +++ b/tests/components/api/test-outgoing-connection.rp2040-ard.yaml @@ -1,5 +1,5 @@ packages: - common: !include common-base.yaml + common: !include common.yaml wifi: ssid: MySSID @@ -7,7 +7,5 @@ wifi: # Outgoing connection on the raw lwip_tcp implementation used by ESP8266 and RP2040 api: - encryption: - key: bOFFzzvfpg5DB94DuBGLXD/hMnhpDKgP9UQyBulwWVU= outgoing_connection: host: 192.168.1.2 diff --git a/tests/integration/fixtures/api_reboot_timeout_after_authenticated_disconnect.yaml b/tests/integration/fixtures/api_reboot_timeout_after_authenticated_disconnect.yaml deleted file mode 100644 index e3490bbc857..00000000000 --- a/tests/integration/fixtures/api_reboot_timeout_after_authenticated_disconnect.yaml +++ /dev/null @@ -1,7 +0,0 @@ -esphome: - name: api-reboot-test -host: -api: - 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 96654308b39..9cada0a296e 100644 --- a/tests/integration/test_api_reboot_timeout.py +++ b/tests/integration/test_api_reboot_timeout.py @@ -5,7 +5,7 @@ import re import pytest -from .types import APIClientConnectedFactory, RunCompiledFunction +from .types import RunCompiledFunction @pytest.mark.asyncio @@ -16,9 +16,7 @@ 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 always connects without authenticating, so the - # reboot deterministically reports the unauthenticated form - reboot_pattern = re.compile(r"none authenticated; rebooting") + reboot_pattern = re.compile(r"No clients; rebooting") def check_output(line: str) -> None: """Check output for reboot message.""" @@ -35,30 +33,3 @@ async def test_api_reboot_timeout( pytest.fail("Device did not reboot within expected timeout") # Test passes if we get here - reboot was detected - - -@pytest.mark.asyncio -async def test_api_reboot_timeout_after_authenticated_disconnect( - yaml_config: str, - run_compiled: RunCompiledFunction, - api_client_connected: APIClientConnectedFactory, -) -> None: - """An authenticated disconnect resets the flag; the clean branch reboots.""" - loop = asyncio.get_running_loop() - reboot_future = loop.create_future() - reboot_pattern = re.compile(r"No clients; rebooting") - - def check_output(line: str) -> None: - """Check output for reboot message.""" - if not reboot_future.done() and reboot_pattern.search(line): - reboot_future.set_result(True) - - async with run_compiled(yaml_config, line_callback=check_output): - # An authenticated session refreshes the watchdog and clears the - # unauthenticated flag the harness probe set - async with api_client_connected() as client: - await client.device_info() - try: - await asyncio.wait_for(reboot_future, timeout=5.0) - except TimeoutError: - pytest.fail("Device did not reboot within expected timeout")