mirror of
https://github.com/esphome/esphome.git
synced 2026-05-23 11:16:52 +08:00
[ethernet] Allow coexistence with wifi for boot-time interface selection
Removes the unconditional CONFLICTS_WITH = ["wifi"] on the ethernet component and only disables CONFIG_ESP_WIFI_ENABLED when wifi is not also configured. This unblocks YAML configs that have both wifi: and ethernet:, where the user picks one at boot based on cable presence. Adds: - enable_on_boot config option on ethernet (mirrors wifi) - has_link() method that exposes PHY link state for runtime probing - ethernet.enable / ethernet.disable actions - ethernet.connected / ethernet.has_link conditions - A dual-net YAML compile test (tests/components/ethernet/common-dual-net.yaml) PLAN.md captures the full design and verification steps. https://claude.ai/code/session_01LyXyqhVQMuab7tdhxRv5sf
This commit is contained in:
@@ -0,0 +1,179 @@
|
||||
# Allow `wifi:` and `ethernet:` to coexist with boot-time link detection
|
||||
|
||||
## Context
|
||||
|
||||
A user wants a single YAML config that:
|
||||
- At boot, checks whether the Ethernet cable is plugged in.
|
||||
- If the cable is plugged in, brings up Ethernet only.
|
||||
- If the cable is unplugged, brings up WiFi (with `improv_serial` / `esp32_improv` provisioning fallback).
|
||||
- Restarts the device whenever the cable plug state changes.
|
||||
|
||||
This is impossible on `dev` today. Two specific things in the codebase prevent it:
|
||||
|
||||
1. `esphome/components/ethernet/__init__.py:52` declares `CONFLICTS_WITH = ["wifi"]`, so config validation rejects any YAML that has both.
|
||||
2. `esphome/components/ethernet/__init__.py:581-583` unconditionally turns off the ESP-IDF WiFi stack (`CONFIG_ESP_WIFI_ENABLED = False`, `CONFIG_SW_COEXIST_ENABLE = False`) when ethernet is configured, so even if the Python conflict were removed, the WiFi driver would not be linked into the firmware.
|
||||
|
||||
ESP-IDF itself supports running WiFi and Ethernet simultaneously — they share `esp_netif` / lwIP and routing is decided per netif. ESPHome opted out of dual-stack early to save flash/RAM, but the user's use case (one or the other selected at boot) is reasonable and worth supporting.
|
||||
|
||||
The plan removes those two blocks and adds the minimum scaffolding needed to make a "select interface at boot based on PHY link" YAML work.
|
||||
|
||||
## Approach
|
||||
|
||||
Five focused changes in ESPHome, then the user's YAML works directly.
|
||||
|
||||
### 1. Drop the `CONFLICTS_WITH` and make WiFi sdkconfig conditional
|
||||
|
||||
File: `esphome/components/ethernet/__init__.py`
|
||||
|
||||
- Line 52: remove `CONFLICTS_WITH = ["wifi"]`.
|
||||
- Lines 580-583: only force `CONFIG_ESP_WIFI_ENABLED=False` / `CONFIG_SW_COEXIST_ENABLE=False` when the wifi component is **not** in the config. Use `CORE.config` (or a `final_validate` hook) to check.
|
||||
```python
|
||||
if "wifi" not in CORE.config:
|
||||
add_idf_sdkconfig_option("CONFIG_ESP_WIFI_ENABLED", False)
|
||||
add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", False)
|
||||
else:
|
||||
add_idf_sdkconfig_option("CONFIG_SW_COEXIST_ENABLE", True)
|
||||
```
|
||||
When both are enabled, `CONFIG_SW_COEXIST_ENABLE` is harmless (it gates BT/WiFi sharing) but does cost a few KB; keep an eye on flash budgets and add a warning in `dump_config` if both coexist.
|
||||
|
||||
### 2. Add `enable_on_boot` to the ethernet schema
|
||||
|
||||
File: `esphome/components/ethernet/__init__.py` and `ethernet_component.h` / `.cpp`
|
||||
|
||||
Mirror the existing wifi pattern:
|
||||
- Schema (`__init__.py` around the rest of `CONFIG_SCHEMA`): `cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean`.
|
||||
- C++ (`ethernet_component.h:117+`): add `bool enable_on_boot_{true};` field and `set_enable_on_boot(bool)` setter — see the wifi equivalent at `wifi_component.h:536` and `wifi_component.h:891`.
|
||||
- `setup()` in `ethernet_component_esp32.cpp` should still install the driver (so the PHY can be probed) but should leave `started_ = false` if `enable_on_boot_` is false. The state machine at `ethernet_component_esp32.cpp:81-131` already gates everything off `started_`, so this is a small change.
|
||||
|
||||
### 3. Add `ethernet.enable` / `ethernet.disable` actions and `ethernet.connected` / `ethernet.has_link` conditions
|
||||
|
||||
New file: `esphome/components/ethernet/automation.h` (model after `esphome/components/wifi/automation.h:26-78`).
|
||||
|
||||
- `EthernetEnableAction`: sets `started_ = true`, kicks the loop.
|
||||
- `EthernetDisableAction`: calls existing `powerdown()` (`ethernet_component.h:144`, already implemented at `ethernet_component_esp32.cpp:812-819`) and sets `started_ = false`.
|
||||
- `EthernetConnectedCondition`: returns `is_connected()` (already exists at `ethernet_component.h:125`).
|
||||
- `EthernetHasLinkCondition`: returns the PHY link bit. ESP-IDF exposes this via `esp_eth_ioctl(eth_handle_, ETH_CMD_G_PHY_ADDR, ...)` plus a PHY register read of `MII_BMSR` bit 2; the existing code already does PHY reads (`ethernet_component_esp32.cpp:837,857` for KSZ80XX). Wrap that into a `bool has_link()` method on `EthernetComponent` and have the condition call it.
|
||||
|
||||
Register them in `__init__.py` next to the existing trigger registration:
|
||||
```python
|
||||
@automation.register_action("ethernet.enable", EthernetEnableAction, ...)
|
||||
@automation.register_action("ethernet.disable", EthernetDisableAction, ...)
|
||||
@automation.register_condition("ethernet.connected", EthernetConnectedCondition, ...)
|
||||
@automation.register_condition("ethernet.has_link", EthernetHasLinkCondition, ...)
|
||||
```
|
||||
|
||||
### 4. Make the ethernet driver install (PHY probe) cheap and fast
|
||||
|
||||
File: `esphome/components/ethernet/ethernet_component_esp32.cpp`
|
||||
|
||||
`setup()` already does `esp_eth_driver_install` regardless of state. The new `has_link()` method must work right after `setup()` finishes, before `esp_eth_start()`. ESP-IDF reads PHY registers as soon as the driver is installed and `phy->reset()` runs, so this is already true — just expose it. No new init flow needed; the only requirement is that the user's `on_boot` priority runs **after** ethernet `setup_priority`. Ethernet's `get_setup_priority()` returns `setup_priority::WIFI` (verify in `ethernet_component_esp32.cpp`); user's `on_boot` should use `priority: 250` (after WiFi/Ethernet setup, before connection).
|
||||
|
||||
### 5. Document the expected boot sequence
|
||||
|
||||
Update `esphome/components/ethernet/__init__.py` docstring / `dump_config()` to spell out:
|
||||
- When both `wifi:` and `ethernet:` are present, the user owns the choice via `enable_on_boot: false` + an `on_boot` automation. ESPHome will not auto-select.
|
||||
- Flash overhead is roughly +N KB (measure during PR; document in PR description per CLAUDE.md breaking-change guidance).
|
||||
|
||||
## Resulting YAML config
|
||||
|
||||
Once the changes above land, this YAML works:
|
||||
|
||||
```yaml
|
||||
esphome:
|
||||
name: dual-net
|
||||
on_boot:
|
||||
# Run after both ethernet and wifi setup() but before either is told to connect.
|
||||
priority: 250
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
ethernet.has_link:
|
||||
then:
|
||||
- logger.log: "Ethernet cable detected, using Ethernet"
|
||||
- ethernet.enable:
|
||||
else:
|
||||
- logger.log: "No Ethernet link, using WiFi"
|
||||
- wifi.enable:
|
||||
|
||||
esp32:
|
||||
board: esp32dev
|
||||
framework:
|
||||
type: esp-idf
|
||||
|
||||
ethernet:
|
||||
type: LAN8720
|
||||
mdc_pin: GPIO23
|
||||
mdio_pin: GPIO18
|
||||
clk_mode: GPIO0_IN
|
||||
phy_addr: 0
|
||||
enable_on_boot: false # NEW — added in change #2
|
||||
on_disconnect:
|
||||
then:
|
||||
- logger.log: "Ethernet disconnected, restarting"
|
||||
- delay: 2s
|
||||
- button.press: restart_btn
|
||||
|
||||
wifi:
|
||||
ssid: !secret wifi_ssid
|
||||
password: !secret wifi_password
|
||||
enable_on_boot: false # already exists today
|
||||
ap: # AP fallback for captive_portal / improv
|
||||
ssid: "Dual-Net Fallback"
|
||||
|
||||
captive_portal:
|
||||
improv_serial:
|
||||
|
||||
# esp32_improv is optional — only if you have a button and want BLE provisioning
|
||||
# esp32_improv:
|
||||
# authorizer: !push_button gpio0
|
||||
|
||||
button:
|
||||
- platform: restart
|
||||
id: restart_btn
|
||||
name: "Restart"
|
||||
|
||||
# Detect cable insertion when running on WiFi: poll PHY link, restart if it changes.
|
||||
interval:
|
||||
- interval: 5s
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
and:
|
||||
- not: ethernet.connected
|
||||
- ethernet.has_link
|
||||
then:
|
||||
- logger.log: "Ethernet cable plugged in, restarting"
|
||||
- button.press: restart_btn
|
||||
```
|
||||
|
||||
Notes on the YAML:
|
||||
- `ethernet.has_link` is the new cheap PHY-only check (change #3); it works even when ethernet is disabled because the driver was installed in `setup()`.
|
||||
- `ethernet.enable` / `wifi.enable` are mutually exclusive in this YAML — only one path runs at boot.
|
||||
- The `interval` block handles "cable plugged in while running on WiFi" → restart so ethernet takes over on the next boot. The existing `on_disconnect` trigger handles the inverse (cable unplugged while on ethernet → restart).
|
||||
- `improv_serial` works as long as wifi is enabled (it depends on wifi per `improv_serial/__init__.py:10`).
|
||||
|
||||
## Critical files to modify
|
||||
|
||||
- `esphome/components/ethernet/__init__.py` — drop `CONFLICTS_WITH` (line 52), make sdkconfig conditional (lines 580-583), add `enable_on_boot` schema option, register new actions/conditions.
|
||||
- `esphome/components/ethernet/ethernet_component.h` — add `enable_on_boot_` field, `set_enable_on_boot`, `has_link()` method.
|
||||
- `esphome/components/ethernet/ethernet_component_esp32.cpp` — implement `has_link()` via existing PHY read path (model after KSZ80XX read at lines 837/857), gate `started_` initialization on `enable_on_boot_`.
|
||||
- `esphome/components/ethernet/ethernet_component_rp2040.cpp` — implement `has_link()` using the existing `linkStatus()` Arduino API (already used at line 117 in that file).
|
||||
- `esphome/components/ethernet/automation.h` — new file, model after `esphome/components/wifi/automation.h`.
|
||||
- `tests/components/ethernet/` — add `common.yaml` test that combines `wifi:` + `ethernet:` and uses the new actions/conditions.
|
||||
|
||||
## Reused existing infrastructure
|
||||
|
||||
- WiFi already has `enable_on_boot` (`wifi_component.h:536, 891`), `wifi.enable` / `wifi.disable` actions (`wifi/automation.h:26, 31`), and `wifi.connected` / `wifi.enabled` / `wifi.ap_active` conditions.
|
||||
- Ethernet already has `is_connected()` (`ethernet_component.h:125`), `powerdown()` (`ethernet_component.h:144`), `on_connect` / `on_disconnect` triggers (`ethernet_component.h:185-190`).
|
||||
- `improv_serial` (`esphome/components/improv_serial/__init__.py`) and `captive_portal` (`esphome/components/captive_portal/__init__.py`) need no changes; they just need wifi to be enabled at boot.
|
||||
- Restart action: `button.press` on a `platform: restart` button (used in the YAML above) — no need for a new action; this matches existing ESPHome patterns.
|
||||
|
||||
## Verification
|
||||
|
||||
1. Config validation: `esphome config dual-net.yaml` should pass (today it errors with `Component conflicts with wifi`).
|
||||
2. Compile both paths: `esphome compile` with cable connected and disconnected — verify only one stack negotiates an IP.
|
||||
3. Hotplug: boot with cable, then unplug → device restarts within ~5s, comes up on WiFi. Boot without cable, then plug in → device restarts within ~5s, comes up on Ethernet.
|
||||
4. Improv fallback: boot without cable and with bad WiFi credentials → `improv_serial` should accept new credentials over USB.
|
||||
5. Run `pytest tests/component_tests/ethernet` and the YAML compile-test under `tests/components/ethernet/test.esp32-idf.yaml` (add a new `dual_net` variant).
|
||||
6. Flash size sanity check: compare `.bin` size against `dev` for an ethernet-only config to confirm the conditional sdkconfig change doesn't regress single-stack builds.
|
||||
7. Memory check at runtime: confirm with `top` / `esp_get_free_heap_size()` that boot-time selection actually leaves the unused stack idle (WiFi shouldn't allocate buffers if `wifi.enable` is never called).
|
||||
@@ -13,6 +13,7 @@ from esphome.const import (
|
||||
CONF_DNS1,
|
||||
CONF_DNS2,
|
||||
CONF_DOMAIN,
|
||||
CONF_ENABLE_ON_BOOT,
|
||||
CONF_GATEWAY,
|
||||
CONF_ID,
|
||||
CONF_INTERRUPT_PIN,
|
||||
@@ -49,7 +50,6 @@ from esphome.core import (
|
||||
import esphome.final_validate as fv
|
||||
from esphome.types import ConfigType
|
||||
|
||||
CONFLICTS_WITH = ["wifi"]
|
||||
AUTO_LOAD = ["network"]
|
||||
LOGGER = logging.getLogger(__name__)
|
||||
|
||||
@@ -219,6 +219,15 @@ MANUAL_IP_SCHEMA = cv.Schema(
|
||||
EthernetComponent = ethernet_ns.class_("EthernetComponent", cg.Component)
|
||||
ManualIP = ethernet_ns.struct("ManualIP")
|
||||
|
||||
EthernetConnectedCondition = ethernet_ns.class_(
|
||||
"EthernetConnectedCondition", automation.Condition
|
||||
)
|
||||
EthernetHasLinkCondition = ethernet_ns.class_(
|
||||
"EthernetHasLinkCondition", automation.Condition
|
||||
)
|
||||
EthernetEnableAction = ethernet_ns.class_("EthernetEnableAction", automation.Action)
|
||||
EthernetDisableAction = ethernet_ns.class_("EthernetDisableAction", automation.Action)
|
||||
|
||||
|
||||
def _is_framework_spi_polling_mode_supported() -> bool:
|
||||
"""Check if ESP-IDF framework supports SPI polling mode (ESP32 only).
|
||||
@@ -349,6 +358,7 @@ BASE_SCHEMA = cv.Schema(
|
||||
cv.Optional(CONF_DOMAIN, default=".local"): cv.domain_name,
|
||||
cv.Optional(CONF_USE_ADDRESS): cv.string_strict,
|
||||
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_DISCONNECT): automation.validate_automation(single=True),
|
||||
}
|
||||
@@ -508,6 +518,10 @@ async def to_code(config):
|
||||
if mac_address := config.get(CONF_MAC_ADDRESS):
|
||||
cg.add(var.set_fixed_mac(mac_address.parts))
|
||||
|
||||
# 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))
|
||||
|
||||
cg.add_define("USE_ETHERNET")
|
||||
|
||||
if on_connect_config := config.get(CONF_ON_CONNECT):
|
||||
@@ -577,10 +591,12 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None:
|
||||
)
|
||||
cg.add(var.add_phy_register(reg))
|
||||
|
||||
# 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)
|
||||
# When wifi is not also configured, disable the WiFi stack to save flash/RAM.
|
||||
# When both are configured the user wants dual-stack (e.g. boot-time link
|
||||
# selection), so leave WiFi enabled.
|
||||
if "wifi" not in CORE.config:
|
||||
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")
|
||||
@@ -716,3 +732,31 @@ def _filter_source_files() -> list[str]:
|
||||
|
||||
|
||||
FILTER_SOURCE_FILES = _filter_source_files
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"ethernet.connected", EthernetConnectedCondition, cv.Schema({})
|
||||
)
|
||||
async def ethernet_connected_to_code(config, condition_id, template_arg, args):
|
||||
return cg.new_Pvariable(condition_id, template_arg)
|
||||
|
||||
|
||||
@automation.register_condition(
|
||||
"ethernet.has_link", EthernetHasLinkCondition, cv.Schema({})
|
||||
)
|
||||
async def ethernet_has_link_to_code(config, condition_id, template_arg, args):
|
||||
return cg.new_Pvariable(condition_id, template_arg)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"ethernet.enable", EthernetEnableAction, cv.Schema({}), synchronous=True
|
||||
)
|
||||
async def ethernet_enable_to_code(config, action_id, template_arg, args):
|
||||
return cg.new_Pvariable(action_id, template_arg)
|
||||
|
||||
|
||||
@automation.register_action(
|
||||
"ethernet.disable", EthernetDisableAction, cv.Schema({}), synchronous=True
|
||||
)
|
||||
async def ethernet_disable_to_code(config, action_id, template_arg, args):
|
||||
return cg.new_Pvariable(action_id, template_arg)
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
#pragma once
|
||||
|
||||
#include "esphome/core/defines.h"
|
||||
#ifdef USE_ETHERNET
|
||||
#include "esphome/core/automation.h"
|
||||
#include "ethernet_component.h"
|
||||
|
||||
namespace esphome::ethernet {
|
||||
|
||||
template<typename... Ts> class EthernetConnectedCondition : public Condition<Ts...> {
|
||||
public:
|
||||
bool check(const Ts &...x) override { return global_eth_component->is_connected(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class EthernetHasLinkCondition : public Condition<Ts...> {
|
||||
public:
|
||||
bool check(const Ts &...x) override { return global_eth_component->has_link(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class EthernetEnableAction : public Action<Ts...> {
|
||||
public:
|
||||
void play(const Ts &...x) override { global_eth_component->enable(); }
|
||||
};
|
||||
|
||||
template<typename... Ts> class EthernetDisableAction : public Action<Ts...> {
|
||||
public:
|
||||
void play(const Ts &...x) override { global_eth_component->disable(); }
|
||||
};
|
||||
|
||||
} // namespace esphome::ethernet
|
||||
#endif
|
||||
@@ -124,6 +124,18 @@ class EthernetComponent final : public Component {
|
||||
void on_powerdown() override { powerdown(); }
|
||||
bool is_connected() { return this->state_ == EthernetComponentState::CONNECTED; }
|
||||
|
||||
// Returns true when the PHY reports the cable is plugged in and link is up.
|
||||
// Safe to call once setup() has completed - does not require the connection
|
||||
// state machine to have run.
|
||||
bool has_link();
|
||||
|
||||
// Start the ethernet connection state machine. No-op if already started.
|
||||
void enable();
|
||||
// Stop the ethernet connection state machine and power down the interface.
|
||||
void disable();
|
||||
|
||||
void set_enable_on_boot(bool enable_on_boot) { this->enable_on_boot_ = enable_on_boot; }
|
||||
|
||||
void set_type(EthernetType type);
|
||||
#ifdef USE_ETHERNET_MANUAL_IP
|
||||
void set_manual_ip(const ManualIP &manual_ip);
|
||||
@@ -284,6 +296,7 @@ class EthernetComponent final : public Component {
|
||||
// Group all uint8_t types together (enums and bools)
|
||||
EthernetType type_{ETHERNET_TYPE_UNKNOWN};
|
||||
EthernetComponentState state_{EthernetComponentState::STOPPED};
|
||||
bool enable_on_boot_{true};
|
||||
bool started_{false};
|
||||
bool connected_{false};
|
||||
bool got_ipv4_address_{false};
|
||||
|
||||
@@ -370,9 +370,41 @@ void EthernetComponent::setup() {
|
||||
ESPHL_ERROR_CHECK(err, "GOT IPv6 event handler register error");
|
||||
#endif /* USE_NETWORK_IPV6 */
|
||||
|
||||
/* start Ethernet driver state machine */
|
||||
err = esp_eth_start(this->eth_handle_);
|
||||
ESPHL_ERROR_CHECK(err, "ETH start error");
|
||||
if (this->enable_on_boot_) {
|
||||
/* start Ethernet driver state machine */
|
||||
err = esp_eth_start(this->eth_handle_);
|
||||
ESPHL_ERROR_CHECK(err, "ETH start error");
|
||||
} else {
|
||||
ESP_LOGCONFIG(TAG, "Ethernet not started (enable_on_boot is false)");
|
||||
}
|
||||
}
|
||||
|
||||
void EthernetComponent::enable() {
|
||||
if (this->started_ || this->eth_handle_ == nullptr)
|
||||
return;
|
||||
esp_err_t err = esp_eth_start(this->eth_handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "ETH start error: (%d) %s", err, esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
this->enable_loop_soon_any_context();
|
||||
}
|
||||
|
||||
void EthernetComponent::disable() {
|
||||
if (!this->started_ || this->eth_handle_ == nullptr)
|
||||
return;
|
||||
esp_err_t err = esp_eth_stop(this->eth_handle_);
|
||||
if (err != ESP_OK) {
|
||||
ESP_LOGE(TAG, "ETH stop error: (%d) %s", err, esp_err_to_name(err));
|
||||
return;
|
||||
}
|
||||
this->enable_loop_soon_any_context();
|
||||
}
|
||||
|
||||
bool EthernetComponent::has_link() {
|
||||
// connected_ is set by ETHERNET_EVENT_CONNECTED and cleared by ETHERNET_EVENT_DISCONNECTED.
|
||||
// It reflects the PHY link state once the driver has been started.
|
||||
return this->connected_;
|
||||
}
|
||||
|
||||
void EthernetComponent::dump_config() {
|
||||
|
||||
@@ -95,6 +95,34 @@ void EthernetComponent::setup() {
|
||||
// when the link is actually up. Setting it prematurely causes
|
||||
// a "Starting → Stopped → Starting" log sequence because the chip
|
||||
// needs time after begin() before the PHY link is ready.
|
||||
|
||||
if (!this->enable_on_boot_) {
|
||||
ESP_LOGCONFIG(TAG, "Ethernet not started (enable_on_boot is false)");
|
||||
}
|
||||
}
|
||||
|
||||
void EthernetComponent::enable() {
|
||||
this->enable_on_boot_ = true;
|
||||
this->enable_loop_soon_any_context();
|
||||
}
|
||||
|
||||
void EthernetComponent::disable() {
|
||||
this->enable_on_boot_ = false;
|
||||
this->started_ = false;
|
||||
this->connected_ = false;
|
||||
this->state_ = EthernetComponentState::STOPPED;
|
||||
this->enable_loop_soon_any_context();
|
||||
}
|
||||
|
||||
bool EthernetComponent::has_link() {
|
||||
if (this->eth_ == nullptr)
|
||||
return false;
|
||||
#if defined(USE_ETHERNET_W5100)
|
||||
// W5100 cannot detect link state - assume up after successful begin()
|
||||
return true;
|
||||
#else
|
||||
return this->eth_->linkStatus() == LinkON;
|
||||
#endif
|
||||
}
|
||||
|
||||
void EthernetComponent::loop() {
|
||||
@@ -107,7 +135,7 @@ void EthernetComponent::loop() {
|
||||
// connected() reads netif->ip_addr without LwIPLock, but this is a single
|
||||
// 32-bit aligned read (atomic on ARM) — worst case is a one-iteration-stale
|
||||
// value, which is benign for polling.
|
||||
if (this->eth_ != nullptr && now - this->last_link_check_ >= LINK_CHECK_INTERVAL) {
|
||||
if (this->eth_ != nullptr && this->enable_on_boot_ && now - this->last_link_check_ >= LINK_CHECK_INTERVAL) {
|
||||
this->last_link_check_ = now;
|
||||
#if defined(USE_ETHERNET_W5100)
|
||||
// W5100 can't detect link (isLinkDetectable() returns false), so linkStatus()
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
esphome:
|
||||
on_boot:
|
||||
# Run after both ethernet and wifi setup() but before either has had time
|
||||
# to connect. Decide which interface to use based on the PHY link state.
|
||||
priority: 250
|
||||
then:
|
||||
- delay: 3s # give the PHY time to autonegotiate
|
||||
- if:
|
||||
condition:
|
||||
ethernet.has_link:
|
||||
then:
|
||||
- logger.log: "Ethernet cable detected, using Ethernet"
|
||||
- wifi.disable
|
||||
- ethernet.enable
|
||||
else:
|
||||
- logger.log: "No Ethernet link, using WiFi"
|
||||
- ethernet.disable
|
||||
- wifi.enable
|
||||
|
||||
ethernet:
|
||||
type: LAN8720
|
||||
mdc_pin: 23
|
||||
mdio_pin: 32
|
||||
clk:
|
||||
pin: 0
|
||||
mode: CLK_EXT_IN
|
||||
phy_addr: 0
|
||||
power_pin: 33
|
||||
enable_on_boot: false
|
||||
on_disconnect:
|
||||
then:
|
||||
- logger.log: "Ethernet disconnected, restarting"
|
||||
- delay: 2s
|
||||
- button.press: restart_btn
|
||||
|
||||
wifi:
|
||||
ssid: MySSID
|
||||
password: password1
|
||||
enable_on_boot: false
|
||||
ap:
|
||||
ssid: "Dual-Net Fallback"
|
||||
|
||||
button:
|
||||
- platform: restart
|
||||
id: restart_btn
|
||||
name: "Restart"
|
||||
|
||||
# Detect cable insertion when running on WiFi: poll PHY link, restart if it
|
||||
# changes. The ethernet driver is installed but stopped; calling has_link
|
||||
# requires it to be enabled briefly. Restart on a positive transition.
|
||||
interval:
|
||||
- interval: 30s
|
||||
then:
|
||||
- if:
|
||||
condition:
|
||||
and:
|
||||
- wifi.connected:
|
||||
- not:
|
||||
ethernet.connected:
|
||||
then:
|
||||
- ethernet.enable
|
||||
- delay: 3s
|
||||
- if:
|
||||
condition:
|
||||
ethernet.has_link:
|
||||
then:
|
||||
- logger.log: "Ethernet cable plugged in, restarting"
|
||||
- button.press: restart_btn
|
||||
else:
|
||||
- ethernet.disable
|
||||
@@ -0,0 +1 @@
|
||||
<<: !include common-dual-net.yaml
|
||||
Reference in New Issue
Block a user