From 18b5277254bc9458d0221cdac008d1d85df3669a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Wed, 2 Sep 2026 12:26:19 +0200 Subject: [PATCH] Accept a bare outgoing_connection block and log the bound enforcement --- esphome/components/api/__init__.py | 12 ++++++++++-- esphome/components/api/api_server.cpp | 1 + .../component_tests/api/test_outgoing_connection.py | 12 ++++++++++++ 3 files changed, 23 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/__init__.py b/esphome/components/api/__init__.py index 545013fdec..485a55e493 100644 --- a/esphome/components/api/__init__.py +++ b/esphome/components/api/__init__.py @@ -314,7 +314,7 @@ def _validate_outgoing_connection(config: ConfigType) -> ConfigType: return config -OUTGOING_CONNECTION_SCHEMA = cv.Schema( +_OUTGOING_CONNECTION_SCHEMA = cv.Schema( { cv.Optional(CONF_HOST): cv.ipaddress, cv.Optional(CONF_PORT, default=6054): cv.port, @@ -323,6 +323,14 @@ OUTGOING_CONNECTION_SCHEMA = cv.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 None: + config = {} + return _OUTGOING_CONNECTION_SCHEMA(config) + + CONFIG_SCHEMA = cv.All( cv.Schema( { @@ -347,7 +355,7 @@ CONFIG_SCHEMA = cv.All( ): ACTIONS_SCHEMA, cv.Exclusive(CONF_ACTIONS, group_of_exclusion=CONF_ACTIONS): ACTIONS_SCHEMA, cv.Optional(CONF_ENCRYPTION): encryption_schema, - cv.Optional(CONF_OUTGOING_CONNECTION): OUTGOING_CONNECTION_SCHEMA, + cv.Optional(CONF_OUTGOING_CONNECTION): _outgoing_connection_schema, cv.Optional(CONF_BATCH_DELAY, default="100ms"): cv.All( cv.positive_time_period_milliseconds, cv.Range(max=cv.TimePeriod(milliseconds=65535)), diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 6e97e2a2f6..b32c4af704 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -285,6 +285,7 @@ void __attribute__((flatten)) APIServer::accept_new_connections_() { bool APIServer::add_client_(APIConnection *conn) { if (this->at_client_limit_()) { // Callers check first; enforce the array bound where the write happens + ESP_LOGW(TAG, "Max connections (%d), dropping client", MAX_API_CONNECTIONS); delete conn; return false; } diff --git a/tests/component_tests/api/test_outgoing_connection.py b/tests/component_tests/api/test_outgoing_connection.py index 780adf2ac6..c8040a2120 100644 --- a/tests/component_tests/api/test_outgoing_connection.py +++ b/tests/component_tests/api/test_outgoing_connection.py @@ -51,6 +51,18 @@ def test_outgoing_connection_defaults( assert outgoing["delay"].total_milliseconds == 60000 +def test_outgoing_connection_bare_block( + set_core_config: SetCoreConfigCallable, +) -> None: + """A bare outgoing_connection: block is valid; the device dials the + remembered last dial-back client.""" + set_core_config(PlatformFramework.ESP32_IDF, platform_data=ESP32_PLATFORM_DATA) + config = CONFIG_SCHEMA(_api_config(None)) + outgoing = config["outgoing_connection"] + assert "host" not in outgoing + assert outgoing["port"] == 6054 + + def test_outgoing_connection_requires_encryption( set_core_config: SetCoreConfigCallable, ) -> None: