feat(network): Unit A — explicit default-route management

Builds on PR #14012's NetworkComponent + PR #14255's priority list to make
the user's stated interface priority actually drive runtime default-route
selection. Without this, ESP-IDF's auto-selection picks the default netif by
each netif's hardcoded `route_prio` field (WiFi STA = 100, Ethernet = 50,
WiFi AP = 10) — which inverts the user's intent on same-subnet
multi-homing configurations where wifi+ethernet share a broadcast domain.

Changes:

- NetworkComponent gains an IP_EVENT handler registered in setup() that
  re-arbitrates the default netif on every interface up/down. The handler
  walks the priority list in order, picks the highest-priority netif that
  is up, and calls esp_netif_set_default_netif() on it. ESP-IDF then sets
  its internal "manual override" flag so subsequent auto-selection events
  don't undo our choice.
- New StaticVector<NetworkPriorityEntry, 4> stores the priority list with
  zero heap allocation. The interface-name string pointer is a YAML literal
  with static storage duration.
- The timeout_ms field is parsed and stored but not yet consumed by Unit A;
  it's wired up for Unit D (runtime timeout fallback).
- New getters get_active_interface() / get_active_netif() expose the
  currently-active interface for Unit C consumers.
- Python codegen iterates CORE.data[KEY_NETWORK_PRIORITY] and emits
  add_priority_entry() calls per YAML order.

Field-tested on ESP32-S3 with W5500 SPI ethernet + WiFi STA on the same
subnet. The log line "[network] Default interface: <name>" confirms the
arbitration logic fires correctly on IP_EVENT_*_GOT_IP.

Standalone — no schema changes, single-interface configs unaffected.
This commit is contained in:
kbx81
2026-05-21 21:29:47 -05:00
parent 9d9af645ac
commit 8f3010ac64
3 changed files with 134 additions and 1 deletions

View File

@@ -456,3 +456,10 @@ async def to_code(config):
async def network_component_to_code(config):
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))

View File

@@ -3,9 +3,13 @@
#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_netif.h"
#include "esp_event.h"
#include "esp_netif.h"
namespace esphome::network {
static const char *const TAG = "network";
@@ -27,6 +31,88 @@ void NetworkComponent::setup() {
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

View File

@@ -2,13 +2,53 @@
#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