mirror of
https://github.com/esphome/esphome.git
synced 2026-09-08 14:06:10 +00:00
Compare commits
42
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
70c6021986 | ||
|
|
1485675928 | ||
|
|
3ed1356bb6 | ||
|
|
d6bc4fea1c | ||
|
|
f818bddac8 | ||
|
|
1f0af903ea | ||
|
|
ed289390df | ||
|
|
8f3010ac64 | ||
|
|
9d9af645ac | ||
|
|
9bfae9e782 | ||
|
|
eb64707d94 | ||
|
|
7814e99b6f | ||
|
|
8ad6813d44 | ||
|
|
1aa0a489f6 | ||
|
|
be3ccd29f6 | ||
|
|
73b8491936 | ||
|
|
1332ebe729 | ||
|
|
028a54422e | ||
|
|
de53e7a6b1 | ||
|
|
0d1d00b654 | ||
|
|
578196ab85 | ||
|
|
d9b712ee5f | ||
|
|
1a61cd622e | ||
|
|
26bdf58daf | ||
|
|
c915a2b8f5 | ||
|
|
7fdb95c2ef | ||
|
|
e44365abca | ||
|
|
532641d523 | ||
|
|
bc36892e7d | ||
|
|
3a02c2f8af | ||
|
|
4a1f9af319 | ||
|
|
9e29bdfdad | ||
|
|
20c975103b | ||
|
|
549b9f85ae | ||
|
|
0fe2310db4 | ||
|
|
5af3e5caef | ||
|
|
47854ff9de | ||
|
|
8a1ddfb1cc | ||
|
|
cde89212fc | ||
|
|
0a518c1e4c | ||
|
|
8c7d2d984e | ||
|
|
8390a98614 |
@@ -17,7 +17,7 @@ from esphome.core import HexInt
|
|||||||
from esphome.types import ConfigType
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
CODEOWNERS = ["@jesserockz"]
|
CODEOWNERS = ["@jesserockz"]
|
||||||
|
AUTO_LOAD = ["network"]
|
||||||
|
|
||||||
byte_vector = cg.std_vector.template(cg.uint8)
|
byte_vector = cg.std_vector.template(cg.uint8)
|
||||||
peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6)
|
peer_address_t = cg.std_ns.class_("array").template(cg.uint8, 6)
|
||||||
|
|||||||
@@ -149,12 +149,6 @@ bool ESPNowComponent::is_wifi_enabled() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void ESPNowComponent::setup() {
|
void ESPNowComponent::setup() {
|
||||||
#ifndef USE_WIFI
|
|
||||||
// Initialize LwIP stack for wake_loop_threadsafe() socket support
|
|
||||||
// When WiFi component is present, it handles esp_netif_init()
|
|
||||||
ESP_ERROR_CHECK(esp_netif_init());
|
|
||||||
#endif
|
|
||||||
|
|
||||||
if (this->enable_on_boot_) {
|
if (this->enable_on_boot_) {
|
||||||
this->enable_();
|
this->enable_();
|
||||||
} else {
|
} else {
|
||||||
@@ -174,8 +168,6 @@ void ESPNowComponent::enable() {
|
|||||||
|
|
||||||
void ESPNowComponent::enable_() {
|
void ESPNowComponent::enable_() {
|
||||||
if (!this->is_wifi_enabled()) {
|
if (!this->is_wifi_enabled()) {
|
||||||
esp_event_loop_create_default();
|
|
||||||
|
|
||||||
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
|
||||||
|
|
||||||
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
ESP_ERROR_CHECK(esp_wifi_init(&cfg));
|
||||||
|
|||||||
@@ -3,7 +3,11 @@ import logging
|
|||||||
|
|
||||||
from esphome import automation, pins
|
from esphome import automation, pins
|
||||||
import esphome.codegen as cg
|
import esphome.codegen as cg
|
||||||
from esphome.components.network import ip_address_literal
|
from esphome.components.network import (
|
||||||
|
KEY_NETWORK_PRIORITY,
|
||||||
|
get_network_priority,
|
||||||
|
ip_address_literal,
|
||||||
|
)
|
||||||
from esphome.config_helpers import filter_source_files_from_platform
|
from esphome.config_helpers import filter_source_files_from_platform
|
||||||
import esphome.config_validation as cv
|
import esphome.config_validation as cv
|
||||||
from esphome.const import (
|
from esphome.const import (
|
||||||
@@ -13,6 +17,7 @@ from esphome.const import (
|
|||||||
CONF_DNS1,
|
CONF_DNS1,
|
||||||
CONF_DNS2,
|
CONF_DNS2,
|
||||||
CONF_DOMAIN,
|
CONF_DOMAIN,
|
||||||
|
CONF_ENABLE_ON_BOOT,
|
||||||
CONF_GATEWAY,
|
CONF_GATEWAY,
|
||||||
CONF_ID,
|
CONF_ID,
|
||||||
CONF_INTERRUPT_PIN,
|
CONF_INTERRUPT_PIN,
|
||||||
@@ -27,6 +32,7 @@ from esphome.const import (
|
|||||||
CONF_PAGE_ID,
|
CONF_PAGE_ID,
|
||||||
CONF_PIN,
|
CONF_PIN,
|
||||||
CONF_POLLING_INTERVAL,
|
CONF_POLLING_INTERVAL,
|
||||||
|
CONF_PRIORITY,
|
||||||
CONF_RESET_PIN,
|
CONF_RESET_PIN,
|
||||||
CONF_SPI,
|
CONF_SPI,
|
||||||
CONF_STATIC_IP,
|
CONF_STATIC_IP,
|
||||||
@@ -48,7 +54,6 @@ from esphome.core import (
|
|||||||
import esphome.final_validate as fv
|
import esphome.final_validate as fv
|
||||||
from esphome.types import ConfigType
|
from esphome.types import ConfigType
|
||||||
|
|
||||||
CONFLICTS_WITH = ["wifi"]
|
|
||||||
AUTO_LOAD = ["network"]
|
AUTO_LOAD = ["network"]
|
||||||
LOGGER = logging.getLogger(__name__)
|
LOGGER = logging.getLogger(__name__)
|
||||||
|
|
||||||
@@ -348,6 +353,7 @@ BASE_SCHEMA = cv.Schema(
|
|||||||
cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name,
|
cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name,
|
||||||
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
|
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
|
||||||
cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
|
cv.Optional(CONF_MAC_ADDRESS): cv.mac_address,
|
||||||
|
cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
|
||||||
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True),
|
cv.Optional(CONF_ON_CONNECT): automation.validate_automation(single=True),
|
||||||
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True),
|
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True),
|
||||||
}
|
}
|
||||||
@@ -487,6 +493,11 @@ async def to_code(config):
|
|||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
await cg.register_component(var, config)
|
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 CORE.is_esp32:
|
if CORE.is_esp32:
|
||||||
await _to_code_esp32(var, config)
|
await _to_code_esp32(var, config)
|
||||||
elif CORE.is_rp2040:
|
elif CORE.is_rp2040:
|
||||||
@@ -494,6 +505,9 @@ async def to_code(config):
|
|||||||
|
|
||||||
cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]]))
|
cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]]))
|
||||||
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
|
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
|
||||||
|
# enable_on_boot defaults to true in C++ - only set if false
|
||||||
|
if not config[CONF_ENABLE_ON_BOOT]:
|
||||||
|
cg.add(var.set_enable_on_boot(False))
|
||||||
CORE.data.setdefault(KEY_ETHERNET, {})[ETHERNET_TYPE_KEY] = config[CONF_TYPE]
|
CORE.data.setdefault(KEY_ETHERNET, {})[ETHERNET_TYPE_KEY] = config[CONF_TYPE]
|
||||||
|
|
||||||
if CONF_MANUAL_IP in config:
|
if CONF_MANUAL_IP in config:
|
||||||
@@ -576,10 +590,16 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None:
|
|||||||
)
|
)
|
||||||
cg.add(var.add_phy_register(reg))
|
cg.add(var.add_phy_register(reg))
|
||||||
|
|
||||||
# Disable WiFi when using Ethernet to save memory
|
# Disable WiFi when using Ethernet alone to save memory.
|
||||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENABLED", False)
|
# When network: priority: lists both interfaces, WiFi must remain enabled.
|
||||||
# Also disable WiFi/BT coexistence since WiFi is disabled
|
net_priority = CORE.data.get(KEY_NETWORK_PRIORITY, [])
|
||||||
add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", False)
|
priority_ifaces = {e["interface"] for e in net_priority}
|
||||||
|
running_with_wifi = "wifi" in priority_ifaces and "ethernet" in priority_ifaces
|
||||||
|
if not running_with_wifi:
|
||||||
|
# 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)
|
||||||
|
|
||||||
# Re-enable ESP-IDF's Ethernet driver (excluded by default to save compile time)
|
# Re-enable ESP-IDF's Ethernet driver (excluded by default to save compile time)
|
||||||
include_builtin_idf_component("esp_eth")
|
include_builtin_idf_component("esp_eth")
|
||||||
@@ -665,6 +685,17 @@ def _final_validate_rmii_pins(config: ConfigType) -> None:
|
|||||||
|
|
||||||
def _final_validate(config: ConfigType) -> ConfigType:
|
def _final_validate(config: ConfigType) -> ConfigType:
|
||||||
"""Final validation for Ethernet component."""
|
"""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, [])
|
||||||
|
priority_ifaces = {e["interface"] for e in net_priority}
|
||||||
|
has_priority_config = "ethernet" in priority_ifaces and "wifi" in priority_ifaces
|
||||||
|
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_spi(config)
|
||||||
_final_validate_rmii_pins(config)
|
_final_validate_rmii_pins(config)
|
||||||
return config
|
return config
|
||||||
|
|||||||
@@ -124,6 +124,17 @@ class EthernetComponent final : public Component {
|
|||||||
void on_powerdown() override { powerdown(); }
|
void on_powerdown() override { powerdown(); }
|
||||||
bool is_connected() { return this->state_ == EthernetComponentState::CONNECTED; }
|
bool is_connected() { return this->state_ == EthernetComponentState::CONNECTED; }
|
||||||
|
|
||||||
|
// Per-interface lifecycle (parallels WiFiComponent::enable/disable/is_disabled).
|
||||||
|
// enable_on_boot defaults to true; when false, setup() runs all the driver/netif
|
||||||
|
// installation but skips esp_eth_start(), keeping the link cold until enable() is
|
||||||
|
// called. This is the primary lever for memory reclamation in multi-interface
|
||||||
|
// configurations where only one interface should carry traffic at a time.
|
||||||
|
void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; }
|
||||||
|
void enable();
|
||||||
|
void disable();
|
||||||
|
bool is_disabled() { return this->disabled_; }
|
||||||
|
bool is_enabled() { return !this->disabled_; }
|
||||||
|
|
||||||
void set_type(EthernetType type);
|
void set_type(EthernetType type);
|
||||||
#ifdef USE_ETHERNET_MANUAL_IP
|
#ifdef USE_ETHERNET_MANUAL_IP
|
||||||
void set_manual_ip(const ManualIP &manual_ip);
|
void set_manual_ip(const ManualIP &manual_ip);
|
||||||
@@ -194,6 +205,16 @@ class EthernetComponent final : public Component {
|
|||||||
void finish_connect_();
|
void finish_connect_();
|
||||||
void dump_connect_params_();
|
void dump_connect_params_();
|
||||||
|
|
||||||
|
#ifdef USE_ESP32
|
||||||
|
// ESP-IDF only: defers the SPI bus init, netif creation, MAC/PHY install, driver
|
||||||
|
// install, netif attach, and event handler registration (which together allocate
|
||||||
|
// ~3-8KB of DMA-capable internal SRAM via SPI driver state + eth driver RX queue)
|
||||||
|
// until ethernet actually needs to come up. Idempotent — guarded by the
|
||||||
|
// ethernet_initialized_ flag. Called from setup() when enable_on_boot_=true, or
|
||||||
|
// from enable() on first runtime enable. Mirrors wifi_lazy_init_() in WiFi.
|
||||||
|
void ethernet_lazy_init_();
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
|
||||||
void notify_ip_state_listeners_();
|
void notify_ip_state_listeners_();
|
||||||
#endif
|
#endif
|
||||||
@@ -287,6 +308,17 @@ class EthernetComponent final : public Component {
|
|||||||
bool started_{false};
|
bool started_{false};
|
||||||
bool connected_{false};
|
bool connected_{false};
|
||||||
bool got_ipv4_address_{false};
|
bool got_ipv4_address_{false};
|
||||||
|
// Codegen-time YAML option. When false, setup() defers esp_eth_start().
|
||||||
|
bool enable_on_boot_{true};
|
||||||
|
// Mirror of "is the link intentionally stopped" — set when setup() honors
|
||||||
|
// enable_on_boot=false, cleared by enable(), set again by disable().
|
||||||
|
bool disabled_{false};
|
||||||
|
#ifdef USE_ESP32
|
||||||
|
// Tracks whether ethernet_lazy_init_() has completed successfully. Allows enable()
|
||||||
|
// to be called at runtime after enable_on_boot:false without re-allocating, and
|
||||||
|
// ensures setup() skips the heavy init when enable_on_boot_ is false.
|
||||||
|
bool ethernet_initialized_{false};
|
||||||
|
#endif
|
||||||
#if LWIP_IPV6
|
#if LWIP_IPV6
|
||||||
uint8_t ipv6_count_{0};
|
uint8_t ipv6_count_{0};
|
||||||
bool ipv6_setup_done_{false};
|
bool ipv6_setup_done_{false};
|
||||||
|
|||||||
@@ -138,6 +138,24 @@ void EthernetComponent::setup() {
|
|||||||
delay(300); // NOLINT
|
delay(300); // NOLINT
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this->enable_on_boot_) {
|
||||||
|
this->ethernet_lazy_init_();
|
||||||
|
if (!this->ethernet_initialized_) {
|
||||||
|
// lazy_init bailed early via ESPHL_ERROR_CHECK or mark_failed; nothing more to do.
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
esp_err_t err = esp_eth_start(this->eth_handle_);
|
||||||
|
ESPHL_ERROR_CHECK(err, "ETH start error");
|
||||||
|
} else {
|
||||||
|
ESP_LOGCONFIG(TAG, "Skipping init (enable_on_boot: false)");
|
||||||
|
this->disabled_ = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void EthernetComponent::ethernet_lazy_init_() {
|
||||||
|
if (this->ethernet_initialized_)
|
||||||
|
return;
|
||||||
|
|
||||||
esp_err_t err;
|
esp_err_t err;
|
||||||
|
|
||||||
#ifdef USE_ETHERNET_SPI
|
#ifdef USE_ETHERNET_SPI
|
||||||
@@ -164,11 +182,7 @@ void EthernetComponent::setup() {
|
|||||||
err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO);
|
err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO);
|
||||||
ESPHL_ERROR_CHECK(err, "SPI bus initialize error");
|
ESPHL_ERROR_CHECK(err, "SPI bus initialize error");
|
||||||
#endif
|
#endif
|
||||||
|
// Network interface setup handled by network component
|
||||||
err = esp_netif_init();
|
|
||||||
ESPHL_ERROR_CHECK(err, "ETH netif init error");
|
|
||||||
err = esp_event_loop_create_default();
|
|
||||||
ESPHL_ERROR_CHECK(err, "ETH event loop error");
|
|
||||||
|
|
||||||
esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH();
|
esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH();
|
||||||
this->eth_netif_ = esp_netif_new(&cfg);
|
this->eth_netif_ = esp_netif_new(&cfg);
|
||||||
@@ -375,9 +389,41 @@ void EthernetComponent::setup() {
|
|||||||
ESPHL_ERROR_CHECK(err, "GOT IPv6 event handler register error");
|
ESPHL_ERROR_CHECK(err, "GOT IPv6 event handler register error");
|
||||||
#endif /* USE_NETWORK_IPV6 */
|
#endif /* USE_NETWORK_IPV6 */
|
||||||
|
|
||||||
/* start Ethernet driver state machine */
|
this->ethernet_initialized_ = true;
|
||||||
err = esp_eth_start(this->eth_handle_);
|
}
|
||||||
ESPHL_ERROR_CHECK(err, "ETH start error");
|
|
||||||
|
void EthernetComponent::enable() {
|
||||||
|
if (!this->disabled_)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Enabling");
|
||||||
|
this->ethernet_lazy_init_();
|
||||||
|
if (!this->ethernet_initialized_) {
|
||||||
|
ESP_LOGE(TAG, "Cannot enable - init failed");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
esp_err_t err = esp_eth_start(this->eth_handle_);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "esp_eth_start failed: %s", esp_err_to_name(err));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->disabled_ = false;
|
||||||
|
// The ETH_EVENT_START handler will set started_=true; the loop state machine
|
||||||
|
// will then drive the STOPPED -> CONNECTING -> CONNECTED transitions.
|
||||||
|
this->enable_loop();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EthernetComponent::disable() {
|
||||||
|
if (this->disabled_)
|
||||||
|
return;
|
||||||
|
|
||||||
|
ESP_LOGD(TAG, "Disabling");
|
||||||
|
esp_err_t err = esp_eth_stop(this->eth_handle_);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "esp_eth_stop failed: %s — disabling anyway", esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
this->disabled_ = true;
|
||||||
|
// ETH_EVENT_STOP will clear started_; loop() will transition to STOPPED.
|
||||||
}
|
}
|
||||||
|
|
||||||
void EthernetComponent::dump_config() {
|
void EthernetComponent::dump_config() {
|
||||||
@@ -491,6 +537,8 @@ void EthernetComponent::dump_config() {
|
|||||||
|
|
||||||
network::IPAddresses EthernetComponent::get_ip_addresses() {
|
network::IPAddresses EthernetComponent::get_ip_addresses() {
|
||||||
network::IPAddresses addresses;
|
network::IPAddresses addresses;
|
||||||
|
if (!this->ethernet_initialized_)
|
||||||
|
return addresses; // all-zero IPs
|
||||||
esp_netif_ip_info_t ip;
|
esp_netif_ip_info_t ip;
|
||||||
esp_err_t err = esp_netif_get_ip_info(this->eth_netif_, &ip);
|
esp_err_t err = esp_netif_get_ip_info(this->eth_netif_, &ip);
|
||||||
if (err != ESP_OK) {
|
if (err != ESP_OK) {
|
||||||
@@ -713,6 +761,10 @@ void EthernetComponent::start_connect_() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void EthernetComponent::dump_connect_params_() {
|
void EthernetComponent::dump_connect_params_() {
|
||||||
|
if (!this->ethernet_initialized_) {
|
||||||
|
ESP_LOGCONFIG(TAG, " uninitialized/disabled");
|
||||||
|
return;
|
||||||
|
}
|
||||||
esp_netif_ip_info_t ip;
|
esp_netif_ip_info_t ip;
|
||||||
esp_netif_get_ip_info(this->eth_netif_, &ip);
|
esp_netif_get_ip_info(this->eth_netif_, &ip);
|
||||||
const ip_addr_t *dns_ip1;
|
const ip_addr_t *dns_ip1;
|
||||||
@@ -780,6 +832,13 @@ void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
|
void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
|
||||||
|
if (!this->ethernet_initialized_) {
|
||||||
|
// External callers (sendspin, ethernet_info, mdns, etc.) may ask for the MAC
|
||||||
|
// before/regardless of whether ethernet is enabled. Fall back to the system MAC
|
||||||
|
// assigned to the ETH interface — same value the driver would have returned.
|
||||||
|
esp_read_mac(mac, ESP_MAC_ETH);
|
||||||
|
return;
|
||||||
|
}
|
||||||
esp_err_t err;
|
esp_err_t err;
|
||||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, mac);
|
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, mac);
|
||||||
ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error");
|
ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error");
|
||||||
@@ -799,6 +858,8 @@ const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
|
|||||||
}
|
}
|
||||||
|
|
||||||
eth_duplex_t EthernetComponent::get_duplex_mode() {
|
eth_duplex_t EthernetComponent::get_duplex_mode() {
|
||||||
|
if (!this->ethernet_initialized_)
|
||||||
|
return ETH_DUPLEX_HALF;
|
||||||
esp_err_t err;
|
esp_err_t err;
|
||||||
eth_duplex_t duplex_mode;
|
eth_duplex_t duplex_mode;
|
||||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode);
|
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode);
|
||||||
@@ -807,6 +868,8 @@ eth_duplex_t EthernetComponent::get_duplex_mode() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
eth_speed_t EthernetComponent::get_link_speed() {
|
eth_speed_t EthernetComponent::get_link_speed() {
|
||||||
|
if (!this->ethernet_initialized_)
|
||||||
|
return ETH_SPEED_10M;
|
||||||
esp_err_t err;
|
esp_err_t err;
|
||||||
eth_speed_t speed;
|
eth_speed_t speed;
|
||||||
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_SPEED, &speed);
|
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_SPEED, &speed);
|
||||||
|
|||||||
@@ -361,6 +361,23 @@ void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
|
|||||||
void EthernetComponent::set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
|
void EthernetComponent::set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
|
||||||
void EthernetComponent::set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; }
|
void EthernetComponent::set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; }
|
||||||
|
|
||||||
|
void EthernetComponent::enable() {
|
||||||
|
// RP2040 uses arduino-pico's LwipIntfDev which manages link state internally;
|
||||||
|
// there is no clean enable/disable hook today. The YAML option is accepted on
|
||||||
|
// RP2040 for schema parity but has no effect.
|
||||||
|
if (!this->disabled_)
|
||||||
|
return;
|
||||||
|
ESP_LOGW(TAG, "enable_on_boot/disable not supported");
|
||||||
|
this->disabled_ = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void EthernetComponent::disable() {
|
||||||
|
if (this->disabled_)
|
||||||
|
return;
|
||||||
|
ESP_LOGW(TAG, "enable_on_boot/disable not supported");
|
||||||
|
this->disabled_ = true;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace esphome::ethernet
|
} // namespace esphome::ethernet
|
||||||
|
|
||||||
#endif // USE_ETHERNET && USE_RP2040
|
#endif // USE_ETHERNET && USE_RP2040
|
||||||
|
|||||||
@@ -5,8 +5,15 @@ import esphome.codegen as cg
|
|||||||
from esphome.components.esp32 import add_idf_sdkconfig_option
|
from esphome.components.esp32 import add_idf_sdkconfig_option
|
||||||
from esphome.components.psram import is_guaranteed as psram_is_guaranteed
|
from esphome.components.psram import is_guaranteed as psram_is_guaranteed
|
||||||
import esphome.config_validation as cv
|
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_ID,
|
||||||
|
CONF_MIN_IPV6_ADDR_COUNT,
|
||||||
|
CONF_PRIORITY,
|
||||||
|
CONF_TIMEOUT,
|
||||||
|
)
|
||||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
from esphome.core import CORE, CoroPriority, coroutine_with_priority
|
||||||
|
import esphome.final_validate as fv
|
||||||
|
|
||||||
CODEOWNERS = ["@esphome/core"]
|
CODEOWNERS = ["@esphome/core"]
|
||||||
AUTO_LOAD = ["mdns"]
|
AUTO_LOAD = ["mdns"]
|
||||||
@@ -18,7 +25,30 @@ _LOGGER = logging.getLogger(__name__)
|
|||||||
KEY_HIGH_PERFORMANCE_NETWORKING = "high_performance_networking"
|
KEY_HIGH_PERFORMANCE_NETWORKING = "high_performance_networking"
|
||||||
CONF_ENABLE_HIGH_PERFORMANCE = "enable_high_performance"
|
CONF_ENABLE_HIGH_PERFORMANCE = "enable_high_performance"
|
||||||
|
|
||||||
|
# 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.
|
||||||
|
#
|
||||||
|
# The base equals the historical setup_priority::WIFI / ::ETHERNET default
|
||||||
|
# (250.0), so a single-entry priority list yields exactly the same setup order
|
||||||
|
# as a config with no priority block. Subsequent entries step down by a small
|
||||||
|
# amount to break ties without crossing other priority bands.
|
||||||
|
#
|
||||||
|
# Important: must stay strictly less than setup_priority::AFTER_BLUETOOTH
|
||||||
|
# (300.0), which NetworkComponent itself uses — otherwise the highest-priority
|
||||||
|
# interface could tie with NetworkComponent and run before esp_netif_init().
|
||||||
|
NETWORK_PRIORITY_BASE = 250.0
|
||||||
|
NETWORK_PRIORITY_STEP = 5.0
|
||||||
|
|
||||||
network_ns = cg.esphome_ns.namespace("network")
|
network_ns = cg.esphome_ns.namespace("network")
|
||||||
|
NetworkComponent = network_ns.class_("NetworkComponent", cg.Component)
|
||||||
IPAddress = network_ns.class_("IPAddress")
|
IPAddress = network_ns.class_("IPAddress")
|
||||||
|
|
||||||
|
|
||||||
@@ -105,8 +135,160 @@ def has_high_performance_networking() -> bool:
|
|||||||
return CORE.data.get(KEY_HIGH_PERFORMANCE_NETWORKING, False)
|
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.
|
||||||
|
|
||||||
|
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"``, ``"wifi"``,
|
||||||
|
``"openthread"`` or ``"modem"`` (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()
|
||||||
|
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 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):
|
||||||
|
"""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 entries
|
||||||
|
|
||||||
|
|
||||||
CONFIG_SCHEMA = cv.Schema(
|
CONFIG_SCHEMA = cv.Schema(
|
||||||
{
|
{
|
||||||
|
cv.GenerateID(): cv.declare_id(NetworkComponent),
|
||||||
cv.SplitDefault(
|
cv.SplitDefault(
|
||||||
CONF_ENABLE_IPV6,
|
CONF_ENABLE_IPV6,
|
||||||
bk72xx=False,
|
bk72xx=False,
|
||||||
@@ -130,15 +312,53 @@ CONFIG_SCHEMA = cv.Schema(
|
|||||||
),
|
),
|
||||||
cv.Optional(CONF_MIN_IPV6_ADDR_COUNT, default=0): cv.positive_int,
|
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_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 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}:' "
|
||||||
|
f"component is configured",
|
||||||
|
[CONF_PRIORITY],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
FINAL_VALIDATE_SCHEMA = _final_validate
|
||||||
|
|
||||||
|
|
||||||
@coroutine_with_priority(CoroPriority.NETWORK)
|
@coroutine_with_priority(CoroPriority.NETWORK)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
cg.add_define("USE_NETWORK")
|
cg.add_define("USE_NETWORK")
|
||||||
# ESP32 with Arduino uses ESP-IDF network APIs directly, no Arduino Network library needed
|
# 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 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
|
||||||
|
# Enable Component::set_setup_priority() so the per-interface to_code
|
||||||
|
# calls below have a defined symbol to link against. Without this define
|
||||||
|
# the implementation in core/component.cpp is compiled out.
|
||||||
|
cg.add_define("USE_SETUP_PRIORITY_OVERRIDE")
|
||||||
|
|
||||||
|
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
|
# Apply high performance networking settings
|
||||||
# Config can explicitly enable/disable, or default to component-driven behavior
|
# Config can explicitly enable/disable, or default to component-driven behavior
|
||||||
enable_high_perf = config.get(CONF_ENABLE_HIGH_PERFORMANCE)
|
enable_high_perf = config.get(CONF_ENABLE_HIGH_PERFORMANCE)
|
||||||
@@ -224,3 +444,22 @@ async def to_code(config):
|
|||||||
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY")
|
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_LWIP2_IPV6_LOW_MEMORY")
|
||||||
if CORE.is_rp2040:
|
if CORE.is_rp2040:
|
||||||
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_IPV6")
|
cg.add_build_flag("-DPIO_FRAMEWORK_ARDUINO_ENABLE_IPV6")
|
||||||
|
# Pvariable creation lives in a separate coroutine at NETWORK_SERVICES so it
|
||||||
|
# emits after wifi/ethernet at COMMUNICATION. This keeps compile-time config
|
||||||
|
# (above) separate from C++ object lifecycle and allows wiring in interface
|
||||||
|
# pointers via get_variable().
|
||||||
|
if CORE.is_esp32:
|
||||||
|
CORE.add_job(network_component_to_code, config)
|
||||||
|
|
||||||
|
|
||||||
|
@coroutine_with_priority(CoroPriority.NETWORK_SERVICES)
|
||||||
|
async def network_component_to_code(config: ConfigType) -> None:
|
||||||
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
await cg.register_component(var, config)
|
||||||
|
|
||||||
|
# Pass the priority list to the C++ component. NetworkComponent::add_priority_entry
|
||||||
|
# captures the interface-name string literal pointer; CORE.data[KEY_NETWORK_PRIORITY]
|
||||||
|
# holds the normalized list of dicts (`{"interface": str, "timeout": int|None}`).
|
||||||
|
for entry in CORE.data.get(KEY_NETWORK_PRIORITY, []):
|
||||||
|
timeout_ms = entry["timeout"] if entry["timeout"] is not None else 0
|
||||||
|
cg.add(var.add_priority_entry(entry["interface"], timeout_ms))
|
||||||
|
|||||||
@@ -0,0 +1,119 @@
|
|||||||
|
#include "network_component.h"
|
||||||
|
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#if defined(USE_NETWORK) && defined(USE_ESP32)
|
||||||
|
#include "esphome/core/log.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
|
||||||
|
#include "esp_err.h"
|
||||||
|
#include "esp_event.h"
|
||||||
|
#include "esp_netif.h"
|
||||||
|
|
||||||
|
namespace esphome::network {
|
||||||
|
|
||||||
|
static const char *const TAG = "network";
|
||||||
|
|
||||||
|
void NetworkComponent::setup() {
|
||||||
|
// Initialize ESP-IDF network interfaces and ensure the default event loop exists
|
||||||
|
esp_err_t err;
|
||||||
|
err = esp_netif_init();
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGE(TAG, "esp_netif_init failed: (%d) %s", err, esp_err_to_name(err));
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
err = esp_event_loop_create_default();
|
||||||
|
// ESP_ERR_INVALID_STATE is returned if the default loop already exists,
|
||||||
|
// which is fine since we just want to make sure it exists
|
||||||
|
if (err != ESP_OK && err != ESP_ERR_INVALID_STATE) {
|
||||||
|
ESP_LOGE(TAG, "esp_event_loop_create_default failed: (%d) %s", err, esp_err_to_name(err));
|
||||||
|
this->mark_failed();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Register an IP_EVENT handler so we can re-arbitrate the default netif on every
|
||||||
|
// interface up/down. ESP-IDF's built-in auto-selection picks by route_prio (WiFi STA = 100
|
||||||
|
// > Ethernet = 50), which inverts the user's stated priority for same-subnet configurations.
|
||||||
|
// We register AFTER esp-idf's internal handler, so our esp_netif_set_default_netif() call
|
||||||
|
// wins and stays sticky thanks to esp-idf's "manual override" flag.
|
||||||
|
err = esp_event_handler_register(IP_EVENT, ESP_EVENT_ANY_ID, &NetworkComponent::event_handler_, this);
|
||||||
|
if (err != ESP_OK) {
|
||||||
|
ESP_LOGW(TAG, "IP_EVENT handler register failed: %s — default route arbitration disabled",
|
||||||
|
esp_err_to_name(err));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Defensive: arbitrate now in case an interface came up before our handler registered
|
||||||
|
// (unlikely given our AFTER_BLUETOOTH priority but cheap).
|
||||||
|
this->update_default_netif_();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkComponent::add_priority_entry(const char *interface, uint32_t timeout_ms) {
|
||||||
|
if (this->priority_list_.size() >= MAX_NETWORK_PRIORITY_ENTRIES) {
|
||||||
|
ESP_LOGW(TAG, "Priority list full; ignoring '%s'", interface);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this->priority_list_.push_back({interface, timeout_ms});
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *NetworkComponent::interface_to_ifkey_(const char *interface) {
|
||||||
|
// Standard ESP-IDF netif keys. esphome's wifi/ethernet/openthread components create
|
||||||
|
// netifs using these defaults.
|
||||||
|
if (std::strcmp(interface, "ethernet") == 0)
|
||||||
|
return "ETH_DEF";
|
||||||
|
if (std::strcmp(interface, "wifi") == 0)
|
||||||
|
return "WIFI_STA_DEF"; // STA carries uplink; AP never wins default route
|
||||||
|
if (std::strcmp(interface, "openthread") == 0)
|
||||||
|
return "OT_DEF";
|
||||||
|
if (std::strcmp(interface, "modem") == 0)
|
||||||
|
return "PPP_DEF";
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkComponent::event_handler_(void *arg, esp_event_base_t /*base*/, int32_t /*id*/, void * /*data*/) {
|
||||||
|
auto *self = static_cast<NetworkComponent *>(arg);
|
||||||
|
self->update_default_netif_();
|
||||||
|
}
|
||||||
|
|
||||||
|
void NetworkComponent::update_default_netif_() {
|
||||||
|
// No priority list configured → leave ESP-IDF's route_prio-based auto-selection alone.
|
||||||
|
// Single-interface configs behave exactly as before.
|
||||||
|
if (this->priority_list_.empty()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto &entry : this->priority_list_) {
|
||||||
|
const char *ifkey = interface_to_ifkey_(entry.interface);
|
||||||
|
if (ifkey == nullptr)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
esp_netif_t *netif = esp_netif_get_handle_from_ifkey(ifkey);
|
||||||
|
if (netif == nullptr)
|
||||||
|
continue; // component for this interface hasn't run setup() yet
|
||||||
|
|
||||||
|
// is_netif_up returns true only when the netif has link + IP, which is what
|
||||||
|
// we want for "this interface can carry traffic right now."
|
||||||
|
if (!esp_netif_is_netif_up(netif))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
if (netif != this->active_netif_) {
|
||||||
|
ESP_LOGI(TAG, "Default interface: %s", entry.interface);
|
||||||
|
esp_netif_set_default_netif(netif);
|
||||||
|
this->active_interface_ = entry.interface;
|
||||||
|
this->active_netif_ = netif;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// No priority-listed interface is currently up.
|
||||||
|
if (this->active_netif_ != nullptr) {
|
||||||
|
ESP_LOGD(TAG, "No active interface in priority list");
|
||||||
|
this->active_interface_ = nullptr;
|
||||||
|
this->active_netif_ = nullptr;
|
||||||
|
// We intentionally don't clear esp-idf's default — the next interface that comes
|
||||||
|
// up will trigger our handler again and we'll re-pick.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace esphome::network
|
||||||
|
#endif
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
#pragma once
|
||||||
|
#include "esphome/core/defines.h"
|
||||||
|
#if defined(USE_NETWORK) && defined(USE_ESP32)
|
||||||
|
#include "esphome/core/component.h"
|
||||||
|
#include "esphome/core/helpers.h"
|
||||||
|
|
||||||
|
#include "esp_event.h"
|
||||||
|
#include "esp_netif.h"
|
||||||
|
|
||||||
|
namespace esphome::network {
|
||||||
|
|
||||||
|
// Cap matches the number of interface types the priority list accepts in YAML
|
||||||
|
// (ethernet, wifi, openthread, modem). StaticVector keeps zero heap allocation.
|
||||||
|
inline constexpr size_t MAX_NETWORK_PRIORITY_ENTRIES = 4;
|
||||||
|
|
||||||
|
struct NetworkPriorityEntry {
|
||||||
|
const char *interface; // YAML name: "ethernet", "wifi", "openthread", "modem"
|
||||||
|
uint32_t timeout_ms; // 0 = no timeout; consumed by Unit D (runtime fallback)
|
||||||
|
};
|
||||||
|
|
||||||
|
class NetworkComponent : public Component {
|
||||||
|
public:
|
||||||
|
void setup() override;
|
||||||
|
// AFTER_BLUETOOTH: BLE controller must initialize before esp_netif_init per IDF guidance.
|
||||||
|
float get_setup_priority() const override { return setup_priority::AFTER_BLUETOOTH; }
|
||||||
|
|
||||||
|
// Codegen-time priority list construction. Called once per `network: priority:` entry
|
||||||
|
// in YAML order. The interface name pointer must have static storage duration.
|
||||||
|
void add_priority_entry(const char *interface, uint32_t timeout_ms);
|
||||||
|
|
||||||
|
// Currently-active interface in priority order (the one set as default netif).
|
||||||
|
// Returns nullptr if no priority list is configured or no interface is up.
|
||||||
|
const char *get_active_interface() const { return this->active_interface_; }
|
||||||
|
esp_netif_t *get_active_netif() const { return this->active_netif_; }
|
||||||
|
|
||||||
|
protected:
|
||||||
|
// Maps a YAML interface name to its ESP-IDF netif if-key.
|
||||||
|
// Returns nullptr if the interface name is not recognized.
|
||||||
|
static const char *interface_to_ifkey_(const char *interface);
|
||||||
|
|
||||||
|
// ESP-IDF event handler trampoline. Fires on IP_EVENT_* and re-arbitrates the default netif.
|
||||||
|
static void event_handler_(void *arg, esp_event_base_t base, int32_t id, void *data);
|
||||||
|
|
||||||
|
// Walk priority_list_ in order. Set the highest-priority netif that is up as the
|
||||||
|
// ESP-IDF default. No-op if priority_list_ is empty (single-interface configs).
|
||||||
|
void update_default_netif_();
|
||||||
|
|
||||||
|
StaticVector<NetworkPriorityEntry, MAX_NETWORK_PRIORITY_ENTRIES> priority_list_;
|
||||||
|
const char *active_interface_{nullptr};
|
||||||
|
esp_netif_t *active_netif_{nullptr};
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace esphome::network
|
||||||
|
#endif
|
||||||
@@ -35,9 +35,8 @@ void OpenThreadComponent::setup() {
|
|||||||
esp_vfs_eventfd_config_t eventfd_config = {
|
esp_vfs_eventfd_config_t eventfd_config = {
|
||||||
.max_fds = 3,
|
.max_fds = 3,
|
||||||
};
|
};
|
||||||
|
// Network interface setup handled by network component
|
||||||
ESP_ERROR_CHECK(nvs_flash_init());
|
ESP_ERROR_CHECK(nvs_flash_init());
|
||||||
ESP_ERROR_CHECK(esp_event_loop_create_default());
|
|
||||||
ESP_ERROR_CHECK(esp_netif_init());
|
|
||||||
ESP_ERROR_CHECK(esp_vfs_eventfd_register(&eventfd_config));
|
ESP_ERROR_CHECK(esp_vfs_eventfd_register(&eventfd_config));
|
||||||
|
|
||||||
xTaskCreate(
|
xTaskCreate(
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from esphome.components.esp32 import (
|
|||||||
only_on_variant,
|
only_on_variant,
|
||||||
)
|
)
|
||||||
from esphome.components.network import (
|
from esphome.components.network import (
|
||||||
|
get_network_priority,
|
||||||
has_high_performance_networking,
|
has_high_performance_networking,
|
||||||
ip_address_literal,
|
ip_address_literal,
|
||||||
)
|
)
|
||||||
@@ -42,7 +43,6 @@ from esphome.const import (
|
|||||||
CONF_ON_CONNECT,
|
CONF_ON_CONNECT,
|
||||||
CONF_ON_DISCONNECT,
|
CONF_ON_DISCONNECT,
|
||||||
CONF_ON_ERROR,
|
CONF_ON_ERROR,
|
||||||
CONF_OUTPUT_POWER,
|
|
||||||
CONF_PASSWORD,
|
CONF_PASSWORD,
|
||||||
CONF_POWER_SAVE_MODE,
|
CONF_POWER_SAVE_MODE,
|
||||||
CONF_PRIORITY,
|
CONF_PRIORITY,
|
||||||
@@ -440,6 +440,7 @@ def _validate(config):
|
|||||||
return config
|
return config
|
||||||
|
|
||||||
|
|
||||||
|
CONF_OUTPUT_POWER = "output_power"
|
||||||
CONF_PASSIVE_SCAN = "passive_scan"
|
CONF_PASSIVE_SCAN = "passive_scan"
|
||||||
CONFIG_SCHEMA = cv.All(
|
CONFIG_SCHEMA = cv.All(
|
||||||
cv.Schema(
|
cv.Schema(
|
||||||
@@ -573,6 +574,10 @@ def wifi_network(config, ap, static_ip):
|
|||||||
@coroutine_with_priority(CoroPriority.COMMUNICATION)
|
@coroutine_with_priority(CoroPriority.COMMUNICATION)
|
||||||
async def to_code(config):
|
async def to_code(config):
|
||||||
var = cg.new_Pvariable(config[CONF_ID])
|
var = cg.new_Pvariable(config[CONF_ID])
|
||||||
|
|
||||||
|
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]))
|
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
|
||||||
|
|
||||||
# Track if any network uses Enterprise authentication
|
# Track if any network uses Enterprise authentication
|
||||||
|
|||||||
@@ -632,11 +632,11 @@ void WiFiComponent::setup() {
|
|||||||
#endif
|
#endif
|
||||||
|
|
||||||
if (this->enable_on_boot_) {
|
if (this->enable_on_boot_) {
|
||||||
|
#ifdef USE_ESP32
|
||||||
|
this->wifi_lazy_init_();
|
||||||
|
#endif
|
||||||
this->start();
|
this->start();
|
||||||
} else {
|
} else {
|
||||||
#ifdef USE_ESP32
|
|
||||||
esp_netif_init();
|
|
||||||
#endif
|
|
||||||
this->state_ = WIFI_COMPONENT_STATE_DISABLED;
|
this->state_ = WIFI_COMPONENT_STATE_DISABLED;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1278,6 +1278,11 @@ void WiFiComponent::enable() {
|
|||||||
|
|
||||||
ESP_LOGD(TAG, "Enabling");
|
ESP_LOGD(TAG, "Enabling");
|
||||||
this->state_ = WIFI_COMPONENT_STATE_OFF;
|
this->state_ = WIFI_COMPONENT_STATE_OFF;
|
||||||
|
#ifdef USE_ESP32
|
||||||
|
// Idempotent — only allocates DMA buffers + netifs on the first call. After this,
|
||||||
|
// start() can safely run.
|
||||||
|
this->wifi_lazy_init_();
|
||||||
|
#endif
|
||||||
this->start();
|
this->start();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -694,6 +694,12 @@ class WiFiComponent final : public Component {
|
|||||||
bool wifi_apply_hostname_();
|
bool wifi_apply_hostname_();
|
||||||
bool wifi_sta_connect_(const WiFiAP &ap);
|
bool wifi_sta_connect_(const WiFiAP &ap);
|
||||||
void wifi_pre_setup_();
|
void wifi_pre_setup_();
|
||||||
|
#ifdef USE_ESP32
|
||||||
|
// ESP-IDF only: defers esp_wifi_init() + netif creation (which allocate ~15-30KB of
|
||||||
|
// DMA-capable internal SRAM) until wifi actually needs to come up. Idempotent.
|
||||||
|
// Called from setup() only when enable_on_boot_=true, and from enable() on first use.
|
||||||
|
void wifi_lazy_init_();
|
||||||
|
#endif
|
||||||
WiFiSTAConnectStatus wifi_sta_connect_status_() const;
|
WiFiSTAConnectStatus wifi_sta_connect_status_() const;
|
||||||
bool is_connected_() const {
|
bool is_connected_() const {
|
||||||
return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED &&
|
return this->state_ == WIFI_COMPONENT_STATE_STA_CONNECTED &&
|
||||||
@@ -889,6 +895,12 @@ class WiFiComponent final : public Component {
|
|||||||
bool rrm_{false};
|
bool rrm_{false};
|
||||||
#endif
|
#endif
|
||||||
bool enable_on_boot_{true};
|
bool enable_on_boot_{true};
|
||||||
|
#ifdef USE_ESP32
|
||||||
|
// Tracks whether esp_wifi_init() + netif creation has happened. Allows enable()
|
||||||
|
// to be called at runtime without re-allocating, and ensures the heavy init is
|
||||||
|
// skipped entirely when enable_on_boot_ is false until first enable().
|
||||||
|
bool wifi_initialized_{false};
|
||||||
|
#endif
|
||||||
bool got_ipv4_address_{false};
|
bool got_ipv4_address_{false};
|
||||||
bool keep_scan_results_{false};
|
bool keep_scan_results_{false};
|
||||||
bool has_completed_scan_after_captive_portal_start_{
|
bool has_completed_scan_after_captive_portal_start_{
|
||||||
|
|||||||
@@ -145,23 +145,15 @@ void WiFiComponent::wifi_pre_setup_() {
|
|||||||
get_mac_address_raw(mac);
|
get_mac_address_raw(mac);
|
||||||
set_mac_address(mac);
|
set_mac_address(mac);
|
||||||
}
|
}
|
||||||
esp_err_t err = esp_netif_init();
|
// Network interface setup handled by network component
|
||||||
if (err != ERR_OK) {
|
|
||||||
ESP_LOGE(TAG, "esp_netif_init failed: %s", esp_err_to_name(err));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
s_wifi_event_group = xEventGroupCreate();
|
s_wifi_event_group = xEventGroupCreate();
|
||||||
if (s_wifi_event_group == nullptr) {
|
if (s_wifi_event_group == nullptr) {
|
||||||
ESP_LOGE(TAG, "xEventGroupCreate failed");
|
ESP_LOGE(TAG, "xEventGroupCreate failed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
err = esp_event_loop_create_default();
|
|
||||||
if (err != ERR_OK) {
|
|
||||||
ESP_LOGE(TAG, "esp_event_loop_create_default failed: %s", esp_err_to_name(err));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
esp_event_handler_instance_t instance_wifi_id, instance_ip_id;
|
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);
|
esp_err_t err =
|
||||||
|
esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &event_handler, nullptr, &instance_wifi_id);
|
||||||
if (err != ERR_OK) {
|
if (err != ERR_OK) {
|
||||||
ESP_LOGE(TAG, "esp_event_handler_instance_register failed: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "esp_event_handler_instance_register failed: %s", esp_err_to_name(err));
|
||||||
return;
|
return;
|
||||||
@@ -171,6 +163,16 @@ void WiFiComponent::wifi_pre_setup_() {
|
|||||||
ESP_LOGE(TAG, "esp_event_handler_instance_register failed: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "esp_event_handler_instance_register failed: %s", esp_err_to_name(err));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
// NOTE: netif creation + esp_wifi_init() used to live here. They allocate ~15-30KB of
|
||||||
|
// DMA-capable internal SRAM, which competes with W5500 SPI DMA and I2S DMA on
|
||||||
|
// memory-tight devices. They are now deferred to wifi_lazy_init_(), called from
|
||||||
|
// setup() when enable_on_boot_ is true, or from enable() on first runtime enable.
|
||||||
|
// This makes enable_on_boot:false genuinely skip the wifi DMA allocation.
|
||||||
|
}
|
||||||
|
|
||||||
|
void WiFiComponent::wifi_lazy_init_() {
|
||||||
|
if (this->wifi_initialized_)
|
||||||
|
return;
|
||||||
|
|
||||||
s_sta_netif = esp_netif_create_default_wifi_sta();
|
s_sta_netif = esp_netif_create_default_wifi_sta();
|
||||||
|
|
||||||
@@ -183,7 +185,7 @@ void WiFiComponent::wifi_pre_setup_() {
|
|||||||
ESP_LOGW(TAG, "starting wifi without nvs");
|
ESP_LOGW(TAG, "starting wifi without nvs");
|
||||||
cfg.nvs_enable = false;
|
cfg.nvs_enable = false;
|
||||||
}
|
}
|
||||||
err = esp_wifi_init(&cfg);
|
esp_err_t err = esp_wifi_init(&cfg);
|
||||||
if (err != ERR_OK) {
|
if (err != ERR_OK) {
|
||||||
ESP_LOGE(TAG, "esp_wifi_init failed: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "esp_wifi_init failed: %s", esp_err_to_name(err));
|
||||||
return;
|
return;
|
||||||
@@ -193,6 +195,7 @@ void WiFiComponent::wifi_pre_setup_() {
|
|||||||
ESP_LOGE(TAG, "esp_wifi_set_storage failed: %s", esp_err_to_name(err));
|
ESP_LOGE(TAG, "esp_wifi_set_storage failed: %s", esp_err_to_name(err));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
this->wifi_initialized_ = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
|
bool WiFiComponent::wifi_mode_(optional<bool> sta, optional<bool> ap) {
|
||||||
|
|||||||
Reference in New Issue
Block a user