From 5af3e5caefa4d29669db9112f8e635c11c8e8325 Mon Sep 17 00:00:00 2001 From: Roy Walker Date: Sun, 22 Feb 2026 18:22:47 -0600 Subject: [PATCH 01/12] Fix stab at network priority support. --- esphome/components/ethernet/__init__.py | 18 ++++++ esphome/components/network/__init__.py | 81 +++++++++++++++++++++++++ esphome/components/wifi/__init__.py | 6 ++ 3 files changed, 105 insertions(+) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 935d2004d49..746b55f5456 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -57,6 +57,19 @@ from esphome.core import ( coroutine_with_priority, ) import esphome.final_validate as fv +from esphome.components.network import CONF_PRIORITY + +def _final_validate(config): + full = fv.full_config.get() + net_priority = full.get("network", {}).get(CONF_PRIORITY, []) + has_priority_config = "ethernet" in net_priority and "wifi" in net_priority + + if "wifi" in full and not has_priority_config: + raise cv.Invalid( + "Ethernet and WiFi cannot be used together unless both are listed " + "under 'network: priority:'" + ) + from esphome.types import ConfigType CONFLICTS_WITH = ["wifi"] @@ -378,6 +391,11 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) + # Apply network priority if configured, otherwise use the existing default + prio = get_network_priority("ethernet") + if prio is not None: + cg.add(var.set_setup_priority(prio)) + if config[CONF_TYPE] in SPI_ETHERNET_TYPES: cg.add(var.set_clk_pin(config[CONF_CLK_PIN])) cg.add(var.set_miso_pin(config[CONF_MISO_PIN])) diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 1f75b12178a..0e3345d0223 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -7,6 +7,7 @@ from esphome.components.psram import is_guaranteed as psram_is_guaranteed import esphome.config_validation as cv from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT from esphome.core import CORE, CoroPriority, coroutine_with_priority +import esphome.final_validate as fv CODEOWNERS = ["@esphome/core"] AUTO_LOAD = ["mdns"] @@ -18,6 +19,13 @@ _LOGGER = logging.getLogger(__name__) KEY_HIGH_PERFORMANCE_NETWORKING = "high_performance_networking" CONF_ENABLE_HIGH_PERFORMANCE = "enable_high_performance" +# Network priority tracking +KEY_NETWORK_PRIORITY = "network_priority" +CONF_PRIORITY = "priority" +VALID_NETWORK_TYPES = ["ethernet", "wifi"] +# Setup priority base values — first in list gets the highest priority +NETWORK_PRIORITY_BASE = 300.0 +NETWORK_PRIORITY_STEP = 100.0 network_ns = cg.esphome_ns.namespace("network") IPAddress = network_ns.class_("IPAddress") @@ -105,6 +113,55 @@ def has_high_performance_networking() -> bool: return CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False) +def get_network_priority(iface: str) -> float | None: + """Get the setup priority for the given network interface type. + + Returns the float setup priority for ``iface`` based on the order declared + under ``network: priority:``. Interfaces listed first receive a higher + setup priority so they are initialised before lower-priority ones. + + If no ``network: priority:`` has been configured this returns ``None`` and + the calling component should fall back to its own default setup priority. + + Args: + iface: Interface type string — one of ``"ethernet"`` or ``"wifi"`` + (case-insensitive). + + Returns: + float setup priority, or None if no priority list was configured. + + Example usage inside a component's ``to_code``:: + + from esphome.components import network + + async def to_code(config): + var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + prio = network.get_network_priority("ethernet") + if prio is not None: + cg.add(var.set_setup_priority(prio)) + ... + """ + priority_list = CORE.data.get(KEY_NETWORK_PRIORITY) + if priority_list is None: + return None + iface_lower = iface.lower() + try: + idx = priority_list.index(iface_lower) + except ValueError: + return None + return NETWORK_PRIORITY_BASE - (idx * NETWORK_PRIORITY_STEP) + + +def _validate_priority_list(value): + """Ensure the priority list has no duplicates and only valid interface names.""" + value = cv.ensure_list(cv.one_of(*VALID_NETWORK_TYPES, lower=True))(value) + if len(value) != len(set(value)): + raise cv.Invalid("Duplicate entries are not allowed in 'priority'") + return value + + CONFIG_SCHEMA = cv.Schema( { cv.SplitDefault( @@ -130,15 +187,39 @@ CONFIG_SCHEMA = cv.Schema( ), cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int, cv.Optional(CONF_ENABLE_HIGH_PERFORMANCE): cv.All(cv.boolean, cv.only_on_esp32), + cv.Optional(CONF_PRIORITY): _validate_priority_list, } ) +def _final_validate(config): + """Check that every interface named in 'priority' has a corresponding component block.""" + full = fv.full_config.get() + for iface in config.get(CONF_PRIORITY, []): + if iface not in full: + raise cv.Invalid( + f"'{iface}' is listed in 'network: priority:' but no '{iface}:' " + f"component is configured", + [CONF_PRIORITY], + ) + + +FINAL_VALIDATE_SCHEMA = _final_validate + + @coroutine_with_priority(CoroPriority.NETWORK) async def to_code(config): cg.add_define("USE_NETWORK") # ESP32 with Arduino uses ESP-IDF network APIs directly, no Arduino Network library needed + # Store the user-declared network priority list in CORE.data so that the + # ethernet and wifi components can query it via get_network_priority() during + # their own to_code phase. + if CONF_PRIORITY in config: + priority_list = config[CONF_PRIORITY] + CORE.data[KEY_NETWORK_PRIORITY] = priority_list + _LOGGER.info("Network interface priority: %s", " > ".join(priority_list)) + # Apply high performance networking settings # Config can explicitly enable/disable, or default to component-driven behavior enable_high_perf = config.get(CONF_ENABLE_HIGH_PERFORMANCE) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 8c1deb62178..c10509ee724 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -13,6 +13,7 @@ from esphome.components.esp32 import ( ) from esphome.components.network import ( has_high_performance_networking, + get_network_priority, ip_address_literal, ) from esphome.components.psram import is_guaranteed as psram_is_guaranteed @@ -454,6 +455,11 @@ def wifi_network(config, ap, static_ip): @coroutine_with_priority(CoroPriority.COMMUNICATION) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) + await cg.register_component(var, config) + + prio = get_network_priority("wifi") + if prio is not None: + cg.add(var.set_setup_priority(prio)) cg.add(var.set_use_address(config[CONF_USE_ADDRESS])) # Track if any network uses Enterprise authentication From 0fe2310db4f3ada3003e15453698133b6d633f7d Mon Sep 17 00:00:00 2001 From: Roy Walker Date: Sun, 22 Feb 2026 18:42:35 -0600 Subject: [PATCH 02/12] Fix wifi and ethernet coexisting. --- esphome/components/ethernet/__init__.py | 36 ++++++++++++------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 746b55f5456..57b69149b81 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -16,6 +16,7 @@ from esphome.components.esp32 import ( get_esp32_variant, include_builtin_idf_component, ) +from esphome.components.network import CONF_PRIORITY, KEY_NETWORK_PRIORITY, get_network_priority from esphome.components.network import ip_address_literal from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface import esphome.config_validation as cv @@ -57,22 +58,8 @@ from esphome.core import ( coroutine_with_priority, ) import esphome.final_validate as fv -from esphome.components.network import CONF_PRIORITY - -def _final_validate(config): - full = fv.full_config.get() - net_priority = full.get("network", {}).get(CONF_PRIORITY, []) - has_priority_config = "ethernet" in net_priority and "wifi" in net_priority - - if "wifi" in full and not has_priority_config: - raise cv.Invalid( - "Ethernet and WiFi cannot be used together unless both are listed " - "under 'network: priority:'" - ) - from esphome.types import ConfigType -CONFLICTS_WITH = ["wifi"] DEPENDENCIES = ["esp32"] AUTO_LOAD = ["network"] LOGGER = logging.getLogger(__name__) @@ -449,10 +436,13 @@ async def to_code(config): cg.add_define("USE_ETHERNET") - # Disable WiFi when using Ethernet to save memory - add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENABLED", False) - # Also disable WiFi/BT coexistence since WiFi is disabled - add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", False) + # Disable WiFi when using Ethernet alone to save memory. + # When network: priority: lists both interfaces, WiFi must remain enabled. + net_priority = CORE.data.get(KEY_NETWORK_PRIORITY, []) + running_with_wifi = "wifi" in net_priority and "ethernet" in net_priority + if not running_with_wifi: + add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENABLED", False) + add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", False) # Re-enable ESP-IDF's Ethernet driver (excluded by default to save compile time) include_builtin_idf_component("esp_eth") @@ -525,6 +515,16 @@ def _final_validate_rmii_pins(config: ConfigType) -> None: def _final_validate(config: ConfigType) -> ConfigType: """Final validation for Ethernet component.""" + # Allow ethernet + wifi coexistence only when both are declared in network: priority: + full = fv.full_config.get() + net_priority = full.get("network", {}).get(CONF_PRIORITY, []) + has_priority_config = "ethernet" in net_priority and "wifi" in net_priority + if "wifi" in full and not has_priority_config: + raise cv.Invalid( + "Component ethernet cannot be used together with component wifi " + "unless both are listed under 'network: priority:'" + ) + _final_validate_spi(config) _final_validate_rmii_pins(config) return config From 549b9f85ae104d14eafb229cbc46ee5714783e38 Mon Sep 17 00:00:00 2001 From: Roy Walker Date: Sun, 22 Feb 2026 19:20:13 -0600 Subject: [PATCH 03/12] Fix wifi so it doesn't double register. --- esphome/components/wifi/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index c10509ee724..34eb8ec5050 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -455,7 +455,6 @@ def wifi_network(config, ap, static_ip): @coroutine_with_priority(CoroPriority.COMMUNICATION) async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) - await cg.register_component(var, config) prio = get_network_priority("wifi") if prio is not None: From 20c975103bdeaca4e65b80836c209cd15870bfcd Mon Sep 17 00:00:00 2001 From: Roy Walker Date: Sun, 22 Feb 2026 20:30:34 -0600 Subject: [PATCH 04/12] Fix Wifi not connecting with Ethernet config but disconnected. --- esphome/components/wifi/wifi_component_esp_idf.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 57bbceb1b85..4f6f60cf6fc 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -155,10 +155,11 @@ void WiFiComponent::wifi_pre_setup_() { return; } err = esp_event_loop_create_default(); - if (err != ERR_OK) { +if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { ESP_LOGE(TAG, "esp_event_loop_create_default failed: %s", esp_err_to_name(err)); + this->mark_failed(); return; - } +} esp_event_handler_instance_t instance_wifi_id, instance_ip_id; err = esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, nullptr, &instance_wifi_id); if (err != ERR_OK) { From 4a1f9af31971e92173c71d279847cba73fff194e Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Tue, 24 Feb 2026 17:08:56 +0000 Subject: [PATCH 05/12] [pre-commit.ci lite] apply automatic fixes --- esphome/components/ethernet/__init__.py | 8 ++++++-- esphome/components/wifi/__init__.py | 2 +- esphome/components/wifi/wifi_component_esp_idf.cpp | 4 ++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 57b69149b81..91565a6aa57 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -16,8 +16,12 @@ from esphome.components.esp32 import ( get_esp32_variant, include_builtin_idf_component, ) -from esphome.components.network import CONF_PRIORITY, KEY_NETWORK_PRIORITY, get_network_priority -from esphome.components.network import ip_address_literal +from esphome.components.network import ( + CONF_PRIORITY, + KEY_NETWORK_PRIORITY, + get_network_priority, + ip_address_literal, +) from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface import esphome.config_validation as cv from esphome.const import ( diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 1913a02f80d..80a483f6027 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -12,8 +12,8 @@ from esphome.components.esp32 import ( only_on_variant, ) from esphome.components.network import ( - has_high_performance_networking, get_network_priority, + has_high_performance_networking, ip_address_literal, ) from esphome.components.psram import is_guaranteed as psram_is_guaranteed diff --git a/esphome/components/wifi/wifi_component_esp_idf.cpp b/esphome/components/wifi/wifi_component_esp_idf.cpp index 4f6f60cf6fc..e424b4beb20 100644 --- a/esphome/components/wifi/wifi_component_esp_idf.cpp +++ b/esphome/components/wifi/wifi_component_esp_idf.cpp @@ -155,11 +155,11 @@ void WiFiComponent::wifi_pre_setup_() { return; } err = esp_event_loop_create_default(); -if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { + if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) { ESP_LOGE(TAG, "esp_event_loop_create_default failed: %s", esp_err_to_name(err)); this->mark_failed(); return; -} + } esp_event_handler_instance_t instance_wifi_id, instance_ip_id; err = esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, nullptr, &instance_wifi_id); if (err != ERR_OK) { From 3a02c2f8afba2dd4f95c5b00c257efe91c39d3b4 Mon Sep 17 00:00:00 2001 From: Roy Walker Date: Tue, 24 Feb 2026 11:16:21 -0600 Subject: [PATCH 06/12] Fix validation on priority import. --- esphome/components/network/__init__.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 0e3345d0223..070d9c6e35f 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -5,7 +5,7 @@ import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.psram import is_guaranteed as psram_is_guaranteed import esphome.config_validation as cv -from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT +from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT, CONF_PRIORITY from esphome.core import CORE, CoroPriority, coroutine_with_priority import esphome.final_validate as fv @@ -21,7 +21,6 @@ CONF_ENABLE_HIGH_PERFORMANCE = "enable_high_performance" # Network priority tracking KEY_NETWORK_PRIORITY = "network_priority" -CONF_PRIORITY = "priority" VALID_NETWORK_TYPES = ["ethernet", "wifi"] # Setup priority base values — first in list gets the highest priority NETWORK_PRIORITY_BASE = 300.0 From e44365abca21be3b5189d5b881bfb2fb94c6990a Mon Sep 17 00:00:00 2001 From: Roy Walker Date: Thu, 26 Feb 2026 11:30:20 -0600 Subject: [PATCH 07/12] Remove duplicate CONF_OUTPUT_POWER from import. --- esphome/components/wifi/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 80a483f6027..c3d55bff182 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -43,7 +43,6 @@ from esphome.const import ( CONF_ON_CONNECT, CONF_ON_DISCONNECT, CONF_ON_ERROR, - CONF_OUTPUT_POWER, CONF_PASSWORD, CONF_POWER_SAVE_MODE, CONF_PRIORITY, From c915a2b8f50ebb1a695a54fc25e30734af34eb7f Mon Sep 17 00:00:00 2001 From: Roy Walker Date: Thu, 26 Feb 2026 11:34:18 -0600 Subject: [PATCH 08/12] Remove duplicate logger and fix import. --- esphome/components/wifi/__init__.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index c3d55bff182..192238d298e 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -67,8 +67,6 @@ _LOGGER = logging.getLogger(__name__) AUTO_LOAD = ["network"] -_LOGGER = logging.getLogger(__name__) - NO_WIFI_VARIANTS = [const.VARIANT_ESP32H2, const.VARIANT_ESP32P4] CONF_SAVE = "save" CONF_BAND_MODE = "band_mode" @@ -344,7 +342,7 @@ def _validate(config): return config - +CONF_OUTPUT_POWER = "output_power" CONF_PASSIVE_SCAN = "passive_scan" CONFIG_SCHEMA = cv.All( cv.Schema( From 26bdf58daf9ecf2c2d3ffea72f56c3f4a9b6cfe6 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Thu, 26 Feb 2026 17:36:14 +0000 Subject: [PATCH 09/12] [pre-commit.ci lite] apply automatic fixes --- esphome/components/wifi/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/esphome/components/wifi/__init__.py b/esphome/components/wifi/__init__.py index 192238d298e..7a0c958063a 100644 --- a/esphome/components/wifi/__init__.py +++ b/esphome/components/wifi/__init__.py @@ -342,6 +342,7 @@ def _validate(config): return config + CONF_OUTPUT_POWER = "output_power" CONF_PASSIVE_SCAN = "passive_scan" CONFIG_SCHEMA = cv.All( From 1a61cd622e19e07599aafe1c5ae87ca520b21937 Mon Sep 17 00:00:00 2001 From: Roy Walker Date: Sat, 28 Feb 2026 13:41:55 -0600 Subject: [PATCH 10/12] Add support for timeouts before next network connection is turned up and add support for openthread and modem. --- esphome/components/network/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 070d9c6e35f..e22f97e4667 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -21,7 +21,7 @@ CONF_ENABLE_HIGH_PERFORMANCE = "enable_high_performance" # Network priority tracking KEY_NETWORK_PRIORITY = "network_priority" -VALID_NETWORK_TYPES = ["ethernet", "wifi"] +VALID_NETWORK_TYPES = ["ethernet", "openthread", "wifi", "modem"] # Setup priority base values — first in list gets the highest priority NETWORK_PRIORITY_BASE = 300.0 NETWORK_PRIORITY_STEP = 100.0 From d9b712ee5fa26e61566f1d1639ba81cf7e102592 Mon Sep 17 00:00:00 2001 From: Roy Walker Date: Sat, 28 Feb 2026 16:22:28 -0600 Subject: [PATCH 11/12] Fix timeout to use ESPhome built-in function. --- esphome/components/network/__init__.py | 153 ++++++++++++++++++++++--- 1 file changed, 136 insertions(+), 17 deletions(-) diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index e22f97e4667..0d3aebc70c4 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -5,7 +5,7 @@ import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.psram import is_guaranteed as psram_is_guaranteed import esphome.config_validation as cv -from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT, CONF_PRIORITY +from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT, CONF_PRIORITY, CONF_TIMEOUT from esphome.core import CORE, CoroPriority, coroutine_with_priority import esphome.final_validate as fv @@ -19,12 +19,19 @@ _LOGGER = logging.getLogger(__name__) KEY_HIGH_PERFORMANCE_NETWORKING = "high_performance_networking" CONF_ENABLE_HIGH_PERFORMANCE = "enable_high_performance" -# Network priority tracking +# Network priority tracking infrastructure +# Components can query this to determine their relative setup priority and fallback timeout. +# CORE.data[KEY_NETWORK_PRIORITY] is a list of dicts: +# [{"interface": "ethernet", "timeout": 30000}, {"interface": "wifi", "timeout": None}, ...] +# where timeout is in milliseconds, or None meaning "start the next interface immediately". KEY_NETWORK_PRIORITY = "network_priority" + VALID_NETWORK_TYPES = ["ethernet", "openthread", "wifi", "modem"] + # Setup priority base values — first in list gets the highest priority NETWORK_PRIORITY_BASE = 300.0 NETWORK_PRIORITY_STEP = 100.0 + network_ns = cg.esphome_ns.namespace("network") IPAddress = network_ns.class_("IPAddress") @@ -112,6 +119,18 @@ def has_high_performance_networking() -> bool: return CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False) +def _get_priority_entry(iface: str) -> dict | None: + """Return the priority entry dict for the given interface, or None if not configured.""" + priority_list = CORE.data.get(KEY_NETWORK_PRIORITY) + if priority_list is None: + return None + iface_lower = iface.lower() + for entry in priority_list: + if entry["interface"] == iface_lower: + return entry + return None + + def get_network_priority(iface: str) -> float | None: """Get the setup priority for the given network interface type. @@ -123,8 +142,8 @@ def get_network_priority(iface: str) -> float | None: the calling component should fall back to its own default setup priority. Args: - iface: Interface type string — one of ``"ethernet"`` or ``"wifi"`` - (case-insensitive). + iface: Interface type string — one of ``"ethernet"``, ``"wifi"``, + ``"openthread"`` or ``"modem"`` (case-insensitive). Returns: float setup priority, or None if no priority list was configured. @@ -146,19 +165,109 @@ def get_network_priority(iface: str) -> float | None: if priority_list is None: return None iface_lower = iface.lower() - try: - idx = priority_list.index(iface_lower) - except ValueError: + for idx, entry in enumerate(priority_list): + if entry["interface"] == iface_lower: + return NETWORK_PRIORITY_BASE - (idx * NETWORK_PRIORITY_STEP) + return None + + +def get_network_timeout(iface: str) -> int | None: + """Get the fallback timeout in milliseconds for the given network interface. + + Returns the timeout (in ms) that the runtime should wait for ``iface`` to + connect before attempting to bring up the next interface in the priority + list. Returns ``None`` if no timeout was configured for this interface, + meaning the next interface should start immediately. + + Args: + iface: Interface type string — one of ``"ethernet"``, ``"wifi"``, + ``"openthread"`` or ``"modem"`` (case-insensitive). + + Returns: + int timeout in milliseconds, or None if no timeout is configured. + + Example usage inside a component's ``to_code``:: + + from esphome.components import network + + async def to_code(config): + ... + timeout_ms = network.get_network_timeout("ethernet") + if timeout_ms is not None: + cg.add(var.set_fallback_timeout(timeout_ms)) + ... + """ + entry = _get_priority_entry(iface) + if entry is None: return None - return NETWORK_PRIORITY_BASE - (idx * NETWORK_PRIORITY_STEP) + return entry.get("timeout") + + +def _validate_timeout(value): + """Accept any common ESPHome/HA time period format, or a plain integer as seconds. + + Accepted formats: 30s, 10sec, 1min, 500ms, 1h, 1.5h, 30 (plain int → 30s). + """ + if isinstance(value, int): + # Plain integer — treat as seconds, e.g. timeout: 30 means 30s + return cv.positive_time_period_milliseconds(f"{value}s") + return cv.positive_time_period_milliseconds(value) + + +def _priority_entry_schema(value): + """Validate a single priority list entry in either plain string or mapping form. + + Plain string form (no timeout): + - ethernet + + Mapping form with optional timeout: + - ethernet: + timeout: 30s + """ + if isinstance(value, str): + return cv.one_of(*VALID_NETWORK_TYPES, lower=True)(value) + if isinstance(value, dict): + if len(value) != 1: + raise cv.Invalid( + "Each priority entry must have exactly one interface name as its key" + ) + iface = next(iter(value)) + cv.one_of(*VALID_NETWORK_TYPES, lower=True)(iface) + opts = cv.Schema( + { + cv.Optional(CONF_TIMEOUT): _validate_timeout, + } + )(value[iface] or {}) + return {iface: opts} + raise cv.Invalid( + f"Expected an interface name string or a mapping, got {type(value).__name__}" + ) + + +def _normalize_priority_entry(value) -> dict: + """Normalize a validated priority entry to a canonical dict. + + Returns a dict with keys: + - ``interface``: str, lowercase interface name + - ``timeout``: int milliseconds, or None + """ + if isinstance(value, str): + return {"interface": value, "timeout": None} + # Mapping form — exactly one key (the interface name) + iface, opts = next(iter(value.items())) + timeout = opts.get(CONF_TIMEOUT) + timeout_ms = int(timeout.total_milliseconds) if timeout is not None else None + return {"interface": iface, "timeout": timeout_ms} def _validate_priority_list(value): - """Ensure the priority list has no duplicates and only valid interface names.""" - value = cv.ensure_list(cv.one_of(*VALID_NETWORK_TYPES, lower=True))(value) - if len(value) != len(set(value)): + """Validate and normalize the full priority list, rejecting duplicates.""" + raw = cv.ensure_list(_priority_entry_schema)(value) + entries = [_normalize_priority_entry(e) for e in raw] + interfaces = [e["interface"] for e in entries] + if len(interfaces) != len(set(interfaces)): raise cv.Invalid("Duplicate entries are not allowed in 'priority'") - return value + return entries CONFIG_SCHEMA = cv.Schema( @@ -194,7 +303,8 @@ CONFIG_SCHEMA = cv.Schema( def _final_validate(config): """Check that every interface named in 'priority' has a corresponding component block.""" full = fv.full_config.get() - for iface in config.get(CONF_PRIORITY, []): + for entry in config.get(CONF_PRIORITY, []): + iface = entry["interface"] if iface not in full: raise cv.Invalid( f"'{iface}' is listed in 'network: priority:' but no '{iface}:' " @@ -211,13 +321,22 @@ async def to_code(config): cg.add_define("USE_NETWORK") # ESP32 with Arduino uses ESP-IDF network APIs directly, no Arduino Network library needed - # Store the user-declared network priority list in CORE.data so that the - # ethernet and wifi components can query it via get_network_priority() during - # their own to_code phase. + # Store the user-declared network priority list in CORE.data so that ethernet, + # wifi and other network components can query it via get_network_priority() and + # get_network_timeout() during their own to_code phase. if CONF_PRIORITY in config: priority_list = config[CONF_PRIORITY] CORE.data[KEY_NETWORK_PRIORITY] = priority_list - _LOGGER.info("Network interface priority: %s", " > ".join(priority_list)) + + def _fmt(entry): + if entry["timeout"] is not None: + return f"{entry['interface']} (timeout: {entry['timeout']}ms)" + return entry["interface"] + + _LOGGER.info( + "Network interface priority: %s", + " > ".join(_fmt(e) for e in priority_list), + ) # Apply high performance networking settings # Config can explicitly enable/disable, or default to component-driven behavior From 578196ab853403c6e9fdfc78d426f421d1783867 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Sat, 28 Feb 2026 22:24:08 +0000 Subject: [PATCH 12/12] [pre-commit.ci lite] apply automatic fixes --- esphome/components/network/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/network/__init__.py b/esphome/components/network/__init__.py index 0d3aebc70c4..c7268db6d56 100644 --- a/esphome/components/network/__init__.py +++ b/esphome/components/network/__init__.py @@ -5,7 +5,12 @@ import esphome.codegen as cg from esphome.components.esp32 import add_idf_sdkconfig_option from esphome.components.psram import is_guaranteed as psram_is_guaranteed import esphome.config_validation as cv -from esphome.const import CONF_ENABLE_IPV6, CONF_MIN_IPV6_ADDR_COUNT, CONF_PRIORITY, CONF_TIMEOUT +from esphome.const import ( + CONF_ENABLE_IPV6, + CONF_MIN_IPV6_ADDR_COUNT, + CONF_PRIORITY, + CONF_TIMEOUT, +) from esphome.core import CORE, CoroPriority, coroutine_with_priority import esphome.final_validate as fv