From 1804face26f29e31b8b5c8b4a506c00d232fc6ac Mon Sep 17 00:00:00 2001 From: Ross Tyler Date: Mon, 18 Aug 2025 10:15:03 -0700 Subject: [PATCH 01/28] enhance ethernet component for spi interface choice on esp-idf --- esphome/components/ethernet/__init__.py | 40 +++++++++++++++---- .../ethernet/ethernet_component.cpp | 25 +++++++++++- .../components/ethernet/ethernet_component.h | 14 +++++++ 3 files changed, 69 insertions(+), 10 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 7384bb26d3..5d6af4490e 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -68,6 +68,8 @@ CONF_CLK_MODE = "clk_mode" CONF_POWER_PIN = "power_pin" CONF_PHY_REGISTERS = "phy_registers" +CONF_INTERFACE = "interface" + CONF_CLOCK_SPEED = "clock_speed" EthernetType = ethernet_ns.enum("EthernetType") @@ -110,6 +112,12 @@ CLK_MODES_DEPRECATED = { "GPIO17_OUT": ("CLK_OUT", 17), } +SPI_INTERFACE_MAP = { + "spi1": "SPI1_HOST", + "spi2": "SPI2_HOST", + "spi3": "SPI3_HOST", +} + MANUAL_IP_SCHEMA = cv.Schema( { cv.Required(CONF_STATIC_IP): cv.ipv4address, @@ -243,6 +251,10 @@ SPI_SCHEMA = BASE_SCHEMA.extend( cv.Optional(CONF_CLOCK_SPEED, default="26.67MHz"): cv.All( cv.frequency, cv.int_range(int(8e6), int(80e6)) ), + cv.Optional(CONF_INTERFACE): cv.All( + cv.only_with_framework("esp-idf"), + cv.one_of(*SPI_INTERFACE_MAP.keys()), + ), # Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate() cv.Optional(CONF_POLLING_INTERVAL): cv.All( cv.positive_time_period_milliseconds, @@ -277,18 +289,21 @@ def _final_validate(config): if config[CONF_TYPE] not in SPI_ETHERNET_TYPES: return if spi_configs := fv.full_config.get().get(CONF_SPI): - variant = get_esp32_variant() - if variant in (VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3): - spi_host = "SPI2_HOST" + if CORE.using_esp_idf and CONF_INTERFACE in config: + spi_host = SPI_INTERFACE_MAP[config[CONF_INTERFACE]] else: - spi_host = "SPI3_HOST" + variant = get_esp32_variant() + if variant in (VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3): + spi_host = "SPI2_HOST" + else: + spi_host = "SPI3_HOST" for spi_conf in spi_configs: if (index := spi_conf.get(CONF_INTERFACE_INDEX)) is not None: interface = get_spi_interface(index) if interface == spi_host: raise cv.Invalid( - f"`spi` component is using interface '{interface}'. " - f"To use {config[CONF_TYPE]}, you must change the `interface` on the `spi` component.", + f"this and an `spi` component are both using interface '{interface}'. " + f"To use {config[CONF_TYPE]}, you must change the `interface` on one of these." ) @@ -336,8 +351,17 @@ async def to_code(config): cg.add(var.set_clock_speed(config[CONF_CLOCK_SPEED])) cg.add_define("USE_ETHERNET_SPI") - add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) - add_idf_sdkconfig_option(f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True) + if CORE.using_esp_idf: + if CONF_INTERFACE in config: + cg.add( + var.set_interface( + cg.RawExpression(SPI_INTERFACE_MAP[config[CONF_INTERFACE]]) + ) + ) + add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) + add_idf_sdkconfig_option( + f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True + ) elif config[CONF_TYPE] == "OPENETH": cg.add_define("USE_ETHERNET_OPENETH") add_idf_sdkconfig_option("CONFIG_ETH_USE_OPENETH", True) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 28043dd969..818aec9d7e 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -87,11 +87,20 @@ void EthernetComponent::setup() { .intr_flags = 0, }; + spi_host_device_t host; +#ifdef USE_ESP_IDF + if (this->interface_ != SPI_HOST_MAX) { + host = this->interface_; + } else { +#endif #if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \ defined(USE_ESP32_VARIANT_ESP32C6) - auto host = SPI2_HOST; + host = SPI2_HOST; #else - auto host = SPI3_HOST; + host = SPI3_HOST; +#endif +#if USE_ESP_IDF + } #endif err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO); @@ -390,6 +399,15 @@ void EthernetComponent::dump_config() { " MOSI Pin: %u\n" " CS Pin: %u", this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_); +#ifdef USE_ESP_IDF + { + constexpr std::array values{"spi1", "spi2", "spi3"}; + char const *const value{this->interface_ < values.size() ? values[this->interface_] : nullptr}; + if (value) { + ESP_LOGCONFIG(TAG, " Interface: %s", value); + } + } +#endif #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT if (this->polling_interval_ != 0) { ESP_LOGCONFIG(TAG, " Polling Interval: %lu ms", this->polling_interval_); @@ -674,6 +692,9 @@ void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; } void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; } void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; } +#ifdef USE_ESP_IDF +void EthernetComponent::set_interface(SPIInterface interface) { this->interface_ = interface; } +#endif #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; } #endif diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 6b4e342df5..2d4353b782 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -5,6 +5,14 @@ #include "esphome/core/hal.h" #include "esphome/components/network/ip_address.h" +#ifdef USE_ESP_IDF + +#include "driver/spi_master.h" + +using SPIInterface = spi_host_device_t; + +#endif // USE_ESP_IDF + #ifdef USE_ESP32 #include "esp_eth.h" @@ -70,6 +78,9 @@ class EthernetComponent : public Component { void set_interrupt_pin(uint8_t interrupt_pin); void set_reset_pin(uint8_t reset_pin); void set_clock_speed(int clock_speed); +#ifdef USE_ESP_IDF + void set_interface(SPIInterface interface); +#endif #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT void set_polling_interval(uint32_t polling_interval); #endif @@ -124,6 +135,9 @@ class EthernetComponent : public Component { int reset_pin_{-1}; int phy_addr_spi_{-1}; int clock_speed_; +#ifdef USE_ESP_IDF + SPIInterface interface_{SPI_HOST_MAX}; // Default to invalid, will be set based on ESP32 variant +#endif #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT uint32_t polling_interval_{0}; #endif From 1c1895664c1e0fd089c0f3f3252f42b3800b8057 Mon Sep 17 00:00:00 2001 From: Ross Tyler Date: Sat, 30 Aug 2025 14:47:12 -0700 Subject: [PATCH 02/28] remove spi1 as an ethernet interface user choice on esp-idf, per espressif-docs --- esphome/components/ethernet/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 5d6af4490e..670116bdb9 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -113,7 +113,6 @@ CLK_MODES_DEPRECATED = { } SPI_INTERFACE_MAP = { - "spi1": "SPI1_HOST", "spi2": "SPI2_HOST", "spi3": "SPI3_HOST", } From dd545c824ec0fde5db78e879e4de932c95097519 Mon Sep 17 00:00:00 2001 From: Ross Tyler Date: Sat, 30 Aug 2025 14:48:18 -0700 Subject: [PATCH 03/28] simplify ethernet spi interface override logic --- .../components/ethernet/ethernet_component.cpp | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 818aec9d7e..f2a91bfdf9 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -87,19 +87,15 @@ void EthernetComponent::setup() { .intr_flags = 0, }; - spi_host_device_t host; +#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \ + defined(USE_ESP32_VARIANT_ESP32C6) + auto host = SPI2_HOST; +#else + auto host = SPI3_HOST; +#endif #ifdef USE_ESP_IDF if (this->interface_ != SPI_HOST_MAX) { host = this->interface_; - } else { -#endif -#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \ - defined(USE_ESP32_VARIANT_ESP32C6) - host = SPI2_HOST; -#else - host = SPI3_HOST; -#endif -#if USE_ESP_IDF } #endif From bbd7f7368c490ed02232d2e91ec352afb7b0d292 Mon Sep 17 00:00:00 2001 From: Ross Tyler Date: Mon, 1 Sep 2025 05:47:16 -0700 Subject: [PATCH 04/28] generate code using spi_host_device_t enum --- esphome/components/ethernet/__init__.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 670116bdb9..302a7c0e7c 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -352,9 +352,14 @@ async def to_code(config): cg.add_define("USE_ETHERNET_SPI") if CORE.using_esp_idf: if CONF_INTERFACE in config: + spi_host_device_t = cg.global_ns.enum("spi_host_device_t") + map = { + "spi2": spi_host_device_t.SPI2_HOST, + "spi3": spi_host_device_t.SPI3_HOST, + } cg.add( var.set_interface( - cg.RawExpression(SPI_INTERFACE_MAP[config[CONF_INTERFACE]]) + map[config[CONF_INTERFACE]] ) ) add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) From bb03f8e63f5dd7924f5f49ee08b19e26838b5524 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 31 Aug 2025 08:39:48 -0500 Subject: [PATCH 05/28] update w5500 test to use spi3 --- .../ethernet/test-w5500.esp32-idf.yaml | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/tests/components/ethernet/test-w5500.esp32-idf.yaml b/tests/components/ethernet/test-w5500.esp32-idf.yaml index 36f1b5365f..151f220bc0 100644 --- a/tests/components/ethernet/test-w5500.esp32-idf.yaml +++ b/tests/components/ethernet/test-w5500.esp32-idf.yaml @@ -1 +1,15 @@ -<<: !include common-w5500.yaml +ethernet: + type: W5500 + clk_pin: 19 + mosi_pin: 21 + miso_pin: 23 + cs_pin: 18 + interrupt_pin: 36 + reset_pin: 22 + clock_speed: 10Mhz + manual_ip: + static_ip: 192.168.178.56 + gateway: 192.168.178.1 + subnet: 255.255.255.0 + domain: .local + interface: spi3 From c28e6b11ee6f29755995985440b7e88bab862808 Mon Sep 17 00:00:00 2001 From: Ross Tyler Date: Mon, 1 Sep 2025 07:16:50 -0700 Subject: [PATCH 06/28] moved ethernet interface choice entirely to config verified that default choices that were made in C++ are still made now, in config. that is, c3, c6, s2 and s3 variants choose spi2 and everything else (h2) chooses spi3. these default choices can be overridden by explicit config grep -H board idf.* idf.c3.yaml: board: esp32-c3-devkitm-1 idf.c6.yaml: board: esp32-c6-devkitc-1 idf.h2.yaml: board: esp32-h2-devkitm-1 idf.s2.yaml: board: esp32-s2-saola-1 idf.s3.yaml: board: esp32-s3-devkitc-1 for y in idf.*; do esphome config $y 2>/dev/null | grep ' interface'; echo $y; done interface: spi2 idf.c3.yaml interface: spi2 idf.c6.yaml interface: spi3 idf.h2.yaml interface: spi2 idf.s2.yaml interface: spi2 idf.s3.yaml --- esphome/components/ethernet/__init__.py | 13 ++++++++----- esphome/components/ethernet/ethernet_component.cpp | 12 +----------- 2 files changed, 9 insertions(+), 16 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 302a7c0e7c..5fde2cf02e 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -13,6 +13,7 @@ from esphome.components.esp32.const import ( VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, + VARIANT_ESP32C6, ) from esphome.components.network import IPAddress from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface @@ -174,6 +175,12 @@ def _validate(config): f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), " f"'{CONF_INTERRUPT_PIN}' is a required option for [ethernet]." ) + if CORE.using_esp_idf and CONF_INTERFACE not in config: + variant = get_esp32_variant() + if variant in (VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3, VARIANT_ESP32C6): + config[CONF_INTERFACE] = "spi2" + else: + config[CONF_INTERFACE] = "spi3" elif config[CONF_TYPE] != "OPENETH": if CONF_CLK_MODE in config: LOGGER.warning( @@ -357,11 +364,7 @@ async def to_code(config): "spi2": spi_host_device_t.SPI2_HOST, "spi3": spi_host_device_t.SPI3_HOST, } - cg.add( - var.set_interface( - map[config[CONF_INTERFACE]] - ) - ) + cg.add(var.set_interface(map[config[CONF_INTERFACE]])) add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) add_idf_sdkconfig_option( f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index f2a91bfdf9..7861c5683a 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -87,17 +87,7 @@ void EthernetComponent::setup() { .intr_flags = 0, }; -#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || \ - defined(USE_ESP32_VARIANT_ESP32C6) - auto host = SPI2_HOST; -#else - auto host = SPI3_HOST; -#endif -#ifdef USE_ESP_IDF - if (this->interface_ != SPI_HOST_MAX) { - host = this->interface_; - } -#endif + auto host = this->interface_; err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO); ESPHL_ERROR_CHECK(err, "SPI bus initialize error"); From 499872e4fb78960eff16646d22839d17a6cf0e96 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci-lite[bot]" <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com> Date: Mon, 1 Sep 2025 14:23:08 +0000 Subject: [PATCH 07/28] [pre-commit.ci lite] apply automatic fixes --- esphome/components/ethernet/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 5fde2cf02e..300107576d 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -11,9 +11,9 @@ from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C3, VARIANT_ESP32P4, + VARIANT_ESP32C6, VARIANT_ESP32S2, VARIANT_ESP32S3, - VARIANT_ESP32C6, ) from esphome.components.network import IPAddress from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface @@ -177,7 +177,12 @@ def _validate(config): ) if CORE.using_esp_idf and CONF_INTERFACE not in config: variant = get_esp32_variant() - if variant in (VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3, VARIANT_ESP32C6): + if variant in ( + VARIANT_ESP32C3, + VARIANT_ESP32S2, + VARIANT_ESP32S3, + VARIANT_ESP32C6, + ): config[CONF_INTERFACE] = "spi2" else: config[CONF_INTERFACE] = "spi3" From 3a9bc34bc94b69a06a6232a3783ca1d9ebe88abd Mon Sep 17 00:00:00 2001 From: Ross Tyler Date: Mon, 1 Sep 2025 08:10:05 -0700 Subject: [PATCH 08/28] remove ESP-IDF guard around ethernet interface __init__.py new interface config is already guarded by config[CONF_TYPE] in SPI_ETHERNET_TYPES in (only) such cases, _validate will provide a default if one not in config _final_validate will use this to check for collision with spi component to_code will generate #define USE_ETHERNET_SPI ethernet_component.cpp #ifdef USE_ETHERNET_SPI is the guard we want. we don't need (and don't want) #ifdef ESP-IDF ethernet_component.h no need for using SPIInterface = spi_host_device_t; just use spi_host_device_t remove ESP_IDF guards they are all nested under USE_ETHERNET_SPI guards --- esphome/components/ethernet/__init__.py | 16 +++------------- .../components/ethernet/ethernet_component.cpp | 6 +----- esphome/components/ethernet/ethernet_component.h | 10 ++-------- 3 files changed, 6 insertions(+), 26 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 300107576d..4a2bc3d957 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -175,7 +175,7 @@ def _validate(config): f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), " f"'{CONF_INTERRUPT_PIN}' is a required option for [ethernet]." ) - if CORE.using_esp_idf and CONF_INTERFACE not in config: + if CONF_INTERFACE not in config: variant = get_esp32_variant() if variant in ( VARIANT_ESP32C3, @@ -262,10 +262,7 @@ SPI_SCHEMA = BASE_SCHEMA.extend( cv.Optional(CONF_CLOCK_SPEED, default="26.67MHz"): cv.All( cv.frequency, cv.int_range(int(8e6), int(80e6)) ), - cv.Optional(CONF_INTERFACE): cv.All( - cv.only_with_framework("esp-idf"), - cv.one_of(*SPI_INTERFACE_MAP.keys()), - ), + cv.Optional(CONF_INTERFACE): cv.one_of(*SPI_INTERFACE_MAP.keys()), # Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate() cv.Optional(CONF_POLLING_INTERVAL): cv.All( cv.positive_time_period_milliseconds, @@ -300,14 +297,7 @@ def _final_validate(config): if config[CONF_TYPE] not in SPI_ETHERNET_TYPES: return if spi_configs := fv.full_config.get().get(CONF_SPI): - if CORE.using_esp_idf and CONF_INTERFACE in config: - spi_host = SPI_INTERFACE_MAP[config[CONF_INTERFACE]] - else: - variant = get_esp32_variant() - if variant in (VARIANT_ESP32C3, VARIANT_ESP32S2, VARIANT_ESP32S3): - spi_host = "SPI2_HOST" - else: - spi_host = "SPI3_HOST" + spi_host = SPI_INTERFACE_MAP[config[CONF_INTERFACE]] for spi_conf in spi_configs: if (index := spi_conf.get(CONF_INTERFACE_INDEX)) is not None: interface = get_spi_interface(index) diff --git a/esphome/components/ethernet/ethernet_component.cpp b/esphome/components/ethernet/ethernet_component.cpp index 7861c5683a..99ada8c256 100644 --- a/esphome/components/ethernet/ethernet_component.cpp +++ b/esphome/components/ethernet/ethernet_component.cpp @@ -385,7 +385,6 @@ void EthernetComponent::dump_config() { " MOSI Pin: %u\n" " CS Pin: %u", this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_); -#ifdef USE_ESP_IDF { constexpr std::array values{"spi1", "spi2", "spi3"}; char const *const value{this->interface_ < values.size() ? values[this->interface_] : nullptr}; @@ -393,7 +392,6 @@ void EthernetComponent::dump_config() { ESP_LOGCONFIG(TAG, " Interface: %s", value); } } -#endif #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT if (this->polling_interval_ != 0) { ESP_LOGCONFIG(TAG, " Polling Interval: %lu ms", this->polling_interval_); @@ -678,9 +676,7 @@ void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; } void EthernetComponent::set_interrupt_pin(uint8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; } void EthernetComponent::set_reset_pin(uint8_t reset_pin) { this->reset_pin_ = reset_pin; } void EthernetComponent::set_clock_speed(int clock_speed) { this->clock_speed_ = clock_speed; } -#ifdef USE_ESP_IDF -void EthernetComponent::set_interface(SPIInterface interface) { this->interface_ = interface; } -#endif +void EthernetComponent::set_interface(spi_host_device_t interface) { this->interface_ = interface; } #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; } #endif diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 2d4353b782..709d3e4b11 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -9,8 +9,6 @@ #include "driver/spi_master.h" -using SPIInterface = spi_host_device_t; - #endif // USE_ESP_IDF #ifdef USE_ESP32 @@ -78,9 +76,7 @@ class EthernetComponent : public Component { void set_interrupt_pin(uint8_t interrupt_pin); void set_reset_pin(uint8_t reset_pin); void set_clock_speed(int clock_speed); -#ifdef USE_ESP_IDF - void set_interface(SPIInterface interface); -#endif + void set_interface(spi_host_device_t interface); #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT void set_polling_interval(uint32_t polling_interval); #endif @@ -135,9 +131,7 @@ class EthernetComponent : public Component { int reset_pin_{-1}; int phy_addr_spi_{-1}; int clock_speed_; -#ifdef USE_ESP_IDF - SPIInterface interface_{SPI_HOST_MAX}; // Default to invalid, will be set based on ESP32 variant -#endif + spi_host_device_t interface_; #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT uint32_t polling_interval_{0}; #endif From 327b366660eb0ddc0e335ddef805026fbd0721ec Mon Sep 17 00:00:00 2001 From: Ross Tyler Date: Thu, 9 Oct 2025 09:50:38 -0700 Subject: [PATCH 09/28] remove ESP-IDF guard around ethernet interface __init__.py new interface config is already guarded by config[CONF_TYPE] in SPI_ETHERNET_TYPES in (only) such cases, _validate will provide a default if one not in config _final_validate will use this to check for collision with spi component to_code will generate #define USE_ETHERNET_SPI --- esphome/components/ethernet/__init__.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 4a2bc3d957..b64ada90e3 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -352,18 +352,16 @@ async def to_code(config): cg.add(var.set_clock_speed(config[CONF_CLOCK_SPEED])) cg.add_define("USE_ETHERNET_SPI") - if CORE.using_esp_idf: - if CONF_INTERFACE in config: - spi_host_device_t = cg.global_ns.enum("spi_host_device_t") - map = { - "spi2": spi_host_device_t.SPI2_HOST, - "spi3": spi_host_device_t.SPI3_HOST, - } - cg.add(var.set_interface(map[config[CONF_INTERFACE]])) - add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) - add_idf_sdkconfig_option( - f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True - ) + + if CONF_INTERFACE in config: + spi_host_device_t = cg.global_ns.enum("spi_host_device_t") + map = { + "spi2": spi_host_device_t.SPI2_HOST, + "spi3": spi_host_device_t.SPI3_HOST, + } + cg.add(var.set_interface(map[config[CONF_INTERFACE]])) + add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) + add_idf_sdkconfig_option(f"CONFIG_ETH_SPI_ETHERNET_{config[CONF_TYPE]}", True) elif config[CONF_TYPE] == "OPENETH": cg.add_define("USE_ETHERNET_OPENETH") add_idf_sdkconfig_option("CONFIG_ETH_USE_OPENETH", True) From ada22e0053db4b33671677785d6e970b71442615 Mon Sep 17 00:00:00 2001 From: Ross Tyler Date: Thu, 9 Oct 2025 10:25:20 -0700 Subject: [PATCH 10/28] remove trailing whitespace --- esphome/components/ethernet/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index b64ada90e3..7d06e533c0 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -352,7 +352,7 @@ async def to_code(config): cg.add(var.set_clock_speed(config[CONF_CLOCK_SPEED])) cg.add_define("USE_ETHERNET_SPI") - + if CONF_INTERFACE in config: spi_host_device_t = cg.global_ns.enum("spi_host_device_t") map = { From 9af525daf92244f344c1e3da3e4212ab71112299 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, 9 Oct 2025 17:28:52 +0000 Subject: [PATCH 11/28] [pre-commit.ci lite] apply automatic fixes --- esphome/components/ethernet/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 7d06e533c0..6996d39d1d 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -10,8 +10,8 @@ from esphome.components.esp32 import ( from esphome.components.esp32.const import ( VARIANT_ESP32, VARIANT_ESP32C3, - VARIANT_ESP32P4, VARIANT_ESP32C6, + VARIANT_ESP32P4, VARIANT_ESP32S2, VARIANT_ESP32S3, ) From 23cfeaac952da81872112157a05ba881169fed28 Mon Sep 17 00:00:00 2001 From: Ross Tyler Date: Wed, 28 Jan 2026 19:50:47 -0800 Subject: [PATCH 12/28] fix merge mistake --- esphome/components/ethernet/__init__.py | 13 +------------ 1 file changed, 1 insertion(+), 12 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 59f62c62bc..f656b9a012 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -333,18 +333,7 @@ def _final_validate_spi(config): if config[CONF_TYPE] not in SPI_ETHERNET_TYPES: return if spi_configs := fv.full_config.get().get(CONF_SPI): - variant = get_esp32_variant() - if variant in ( - VARIANT_ESP32C3, - VARIANT_ESP32C5, - VARIANT_ESP32C6, - VARIANT_ESP32C61, - VARIANT_ESP32S2, - VARIANT_ESP32S3, - ): - spi_host = "SPI2_HOST" - else: - spi_host = "SPI3_HOST" + spi_host = SPI_INTERFACE_MAP[config[CONF_INTERFACE]] for spi_conf in spi_configs: if (index := spi_conf.get(CONF_INTERFACE_INDEX)) is not None: interface = get_spi_interface(index) From ca3e097b1c8f4a7ac393595b8b00a25b487590fa Mon Sep 17 00:00:00 2001 From: Ross Tyler Date: Thu, 29 Jan 2026 07:37:26 -0800 Subject: [PATCH 13/28] VARIANT_ESP32C5, C6 & C61 interface default to spi2 per pre-merge --- esphome/components/ethernet/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index f656b9a012..8b3061036c 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -219,9 +219,11 @@ def _validate(config): variant = get_esp32_variant() if variant in ( VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32C61, VARIANT_ESP32S2, VARIANT_ESP32S3, - VARIANT_ESP32C6, ): config[CONF_INTERFACE] = "spi2" else: From c47bdf568b555f1c74cd866cb80f0927be70759b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 00:49:06 -1000 Subject: [PATCH 14/28] preen --- esphome/components/ethernet/ethernet_component.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 72d308ad54..33240ceedf 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -205,7 +205,7 @@ class EthernetComponent final : public Component { int reset_pin_{-1}; int phy_addr_spi_{-1}; int clock_speed_; - spi_host_device_t interface_; + spi_host_device_t interface_{SPI2_HOST}; #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT uint32_t polling_interval_{0}; #endif From a7dbbb9e15e13c6bd26db7b7b929935c6c8b865e Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 00:54:27 -1000 Subject: [PATCH 15/28] address review, validate --- esphome/components/ethernet/__init__.py | 60 +++++++++++++------------ 1 file changed, 32 insertions(+), 28 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 8918cec193..226ac7155d 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -193,9 +193,11 @@ CLK_MODES_DEPRECATED = { "GPIO17_OUT": ("CLK_OUT", 17), } +spi_host_device_t = cg.global_ns.enum("spi_host_device_t") + SPI_INTERFACE_MAP = { - "spi2": "SPI2_HOST", - "spi3": "SPI3_HOST", + "spi2": spi_host_device_t.SPI2_HOST, + "spi3": spi_host_device_t.SPI3_HOST, } MANUAL_IP_SCHEMA = cv.Schema( @@ -278,29 +280,35 @@ def _validate(config): f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), " f"'{CONF_INTERRUPT_PIN}' is a required option for [ethernet]." ) - if CONF_INTERFACE not in config: - from esphome.components.esp32 import ( - VARIANT_ESP32C3, - VARIANT_ESP32C5, - VARIANT_ESP32C6, - VARIANT_ESP32C61, - VARIANT_ESP32S2, - VARIANT_ESP32S3, - get_esp32_variant, - ) + from esphome.components.esp32 import ( + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32C61, + VARIANT_ESP32H2, + get_esp32_variant, + ) - variant = get_esp32_variant() - if variant in ( - VARIANT_ESP32C3, - VARIANT_ESP32C5, - VARIANT_ESP32C6, - VARIANT_ESP32C61, - VARIANT_ESP32S2, - VARIANT_ESP32S3, - ): + spi2_only_variants = { + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32C61, + VARIANT_ESP32H2, + } + variant = get_esp32_variant() + if CONF_INTERFACE not in config: + if variant in spi2_only_variants: config[CONF_INTERFACE] = "spi2" else: config[CONF_INTERFACE] = "spi3" + elif config[CONF_INTERFACE] == "spi3" and variant in spi2_only_variants: + raise cv.Invalid( + f"Interface 'spi3' is not available on {variant}. " + f"Only 'spi2' is supported on this variant." + ) elif config[CONF_TYPE] != "OPENETH": from esphome.components.esp32 import ( VARIANT_ESP32, @@ -445,7 +453,8 @@ def _final_validate_spi(config): from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface if spi_configs := fv.full_config.get().get(CONF_SPI): - spi_host = SPI_INTERFACE_MAP[config[CONF_INTERFACE]] + # get_spi_interface() returns strings like "SPI2_HOST" + spi_host = config[CONF_INTERFACE].upper() + "_HOST" for spi_conf in spi_configs: if (index := spi_conf.get(CONF_INTERFACE_INDEX)) is not None: interface = get_spi_interface(index) @@ -544,12 +553,7 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None: cg.add_define("USE_ETHERNET_SPI") if CONF_INTERFACE in config: - spi_host_device_t = cg.global_ns.enum("spi_host_device_t") - map = { - "spi2": spi_host_device_t.SPI2_HOST, - "spi3": spi_host_device_t.SPI3_HOST, - } - cg.add(var.set_interface(map[config[CONF_INTERFACE]])) + cg.add(var.set_interface(SPI_INTERFACE_MAP[config[CONF_INTERFACE]])) add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) # CONFIG_ETH_SPI_ETHERNET_{TYPE} Kconfig options were removed in IDF 6.0 # ENC28J60 was never built-in to IDF, so it has no Kconfig option From 5e3bdc4613ec85cbcca52df3faafb4a799d55a37 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 00:55:17 -1000 Subject: [PATCH 16/28] address review, validate --- esphome/components/ethernet/__init__.py | 65 ++++++++++++++----------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 226ac7155d..017d057aff 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -234,6 +234,41 @@ def _is_framework_spi_polling_mode_supported() -> bool: return False +def _has_spi3(variant: str) -> bool: + """Check if the ESP32 variant has SPI3_HOST available.""" + from esphome.components.esp32 import ( + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32C61, + VARIANT_ESP32H2, + ) + + return variant not in { + VARIANT_ESP32C2, + VARIANT_ESP32C3, + VARIANT_ESP32C5, + VARIANT_ESP32C6, + VARIANT_ESP32C61, + VARIANT_ESP32H2, + } + + +def _validate_spi_interface(config): + """Set default SPI interface or validate user choice against the variant.""" + from esphome.components.esp32 import get_esp32_variant + + variant = get_esp32_variant() + if CONF_INTERFACE not in config: + config[CONF_INTERFACE] = "spi3" if _has_spi3(variant) else "spi2" + elif config[CONF_INTERFACE] == "spi3" and not _has_spi3(variant): + raise cv.Invalid( + f"Interface 'spi3' is not available on {variant}. " + f"Only 'spi2' is supported on this variant." + ) + + def _validate(config): if CONF_USE_ADDRESS not in config: if CONF_MANUAL_IP in config: @@ -280,35 +315,7 @@ def _validate(config): f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), " f"'{CONF_INTERRUPT_PIN}' is a required option for [ethernet]." ) - from esphome.components.esp32 import ( - VARIANT_ESP32C2, - VARIANT_ESP32C3, - VARIANT_ESP32C5, - VARIANT_ESP32C6, - VARIANT_ESP32C61, - VARIANT_ESP32H2, - get_esp32_variant, - ) - - spi2_only_variants = { - VARIANT_ESP32C2, - VARIANT_ESP32C3, - VARIANT_ESP32C5, - VARIANT_ESP32C6, - VARIANT_ESP32C61, - VARIANT_ESP32H2, - } - variant = get_esp32_variant() - if CONF_INTERFACE not in config: - if variant in spi2_only_variants: - config[CONF_INTERFACE] = "spi2" - else: - config[CONF_INTERFACE] = "spi3" - elif config[CONF_INTERFACE] == "spi3" and variant in spi2_only_variants: - raise cv.Invalid( - f"Interface 'spi3' is not available on {variant}. " - f"Only 'spi2' is supported on this variant." - ) + _validate_spi_interface(config) elif config[CONF_TYPE] != "OPENETH": from esphome.components.esp32 import ( VARIANT_ESP32, From aa8f087ea620d5c523c95779e661da9adcd75188 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 00:55:57 -1000 Subject: [PATCH 17/28] address review, validate --- esphome/components/ethernet/__init__.py | 35 +++++-------------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 017d057aff..cf3ab55374 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -234,38 +234,17 @@ def _is_framework_spi_polling_mode_supported() -> bool: return False -def _has_spi3(variant: str) -> bool: - """Check if the ESP32 variant has SPI3_HOST available.""" - from esphome.components.esp32 import ( - VARIANT_ESP32C2, - VARIANT_ESP32C3, - VARIANT_ESP32C5, - VARIANT_ESP32C6, - VARIANT_ESP32C61, - VARIANT_ESP32H2, - ) - - return variant not in { - VARIANT_ESP32C2, - VARIANT_ESP32C3, - VARIANT_ESP32C5, - VARIANT_ESP32C6, - VARIANT_ESP32C61, - VARIANT_ESP32H2, - } - - -def _validate_spi_interface(config): +def _validate_spi_interface(config: ConfigType) -> None: """Set default SPI interface or validate user choice against the variant.""" - from esphome.components.esp32 import get_esp32_variant + from esphome.components.spi import get_hw_interface_list - variant = get_esp32_variant() + available = sum(get_hw_interface_list(), []) if CONF_INTERFACE not in config: - config[CONF_INTERFACE] = "spi3" if _has_spi3(variant) else "spi2" - elif config[CONF_INTERFACE] == "spi3" and not _has_spi3(variant): + config[CONF_INTERFACE] = "spi3" if "spi3" in available else "spi2" + elif config[CONF_INTERFACE] not in available: raise cv.Invalid( - f"Interface 'spi3' is not available on {variant}. " - f"Only 'spi2' is supported on this variant." + f"Interface '{config[CONF_INTERFACE]}' is not available on this variant. " + f"Available: {', '.join(sorted(k for k in available if k in SPI_INTERFACE_MAP))}" ) From 01f2a8622209c5ee197a893cd7a52d52cf968bfb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 00:59:44 -1000 Subject: [PATCH 18/28] address review, validate --- esphome/components/ethernet/__init__.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index cf3ab55374..ac97588ef5 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -240,10 +240,13 @@ def _validate_spi_interface(config: ConfigType) -> None: available = sum(get_hw_interface_list(), []) if CONF_INTERFACE not in config: - config[CONF_INTERFACE] = "spi3" if "spi3" in available else "spi2" - elif config[CONF_INTERFACE] not in available: + key = "spi3" if "spi3" in available else "spi2" + config[CONF_INTERFACE] = SPI_INTERFACE_MAP[key] + elif ( + config[CONF_INTERFACE] == SPI_INTERFACE_MAP["spi3"] and "spi3" not in available + ): raise cv.Invalid( - f"Interface '{config[CONF_INTERFACE]}' is not available on this variant. " + "Interface 'spi3' is not available on this variant. " f"Available: {', '.join(sorted(k for k in available if k in SPI_INTERFACE_MAP))}" ) @@ -394,7 +397,7 @@ SPI_SCHEMA = cv.All( ), cv.Optional(CONF_INTERFACE): cv.All( cv.only_on_esp32, - cv.one_of(*SPI_INTERFACE_MAP.keys()), + cv.enum(SPI_INTERFACE_MAP), ), # Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate() cv.Optional(CONF_POLLING_INTERVAL): cv.All( @@ -439,8 +442,9 @@ def _final_validate_spi(config): from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface if spi_configs := fv.full_config.get().get(CONF_SPI): + # config[CONF_INTERFACE] is a codegen enum (e.g. ::SPI2_HOST) # get_spi_interface() returns strings like "SPI2_HOST" - spi_host = config[CONF_INTERFACE].upper() + "_HOST" + spi_host = str(config[CONF_INTERFACE]).removeprefix("::") for spi_conf in spi_configs: if (index := spi_conf.get(CONF_INTERFACE_INDEX)) is not None: interface = get_spi_interface(index) @@ -539,7 +543,7 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None: cg.add_define("USE_ETHERNET_SPI") if CONF_INTERFACE in config: - cg.add(var.set_interface(SPI_INTERFACE_MAP[config[CONF_INTERFACE]])) + cg.add(var.set_interface(config[CONF_INTERFACE])) add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) # CONFIG_ETH_SPI_ETHERNET_{TYPE} Kconfig options were removed in IDF 6.0 # ENC28J60 was never built-in to IDF, so it has no Kconfig option From 73029a5494f32588e51100241e8f060d60be6151 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 01:01:32 -1000 Subject: [PATCH 19/28] address review, validate --- esphome/components/ethernet/__init__.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index ac97588ef5..e5f8f465fe 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -238,17 +238,13 @@ def _validate_spi_interface(config: ConfigType) -> None: """Set default SPI interface or validate user choice against the variant.""" from esphome.components.spi import get_hw_interface_list - available = sum(get_hw_interface_list(), []) + has_spi3 = "spi3" in sum(get_hw_interface_list(), []) if CONF_INTERFACE not in config: - key = "spi3" if "spi3" in available else "spi2" - config[CONF_INTERFACE] = SPI_INTERFACE_MAP[key] - elif ( - config[CONF_INTERFACE] == SPI_INTERFACE_MAP["spi3"] and "spi3" not in available - ): - raise cv.Invalid( - "Interface 'spi3' is not available on this variant. " - f"Available: {', '.join(sorted(k for k in available if k in SPI_INTERFACE_MAP))}" + config[CONF_INTERFACE] = ( + spi_host_device_t.SPI3_HOST if has_spi3 else spi_host_device_t.SPI2_HOST ) + elif config[CONF_INTERFACE] == spi_host_device_t.SPI3_HOST and not has_spi3: + raise cv.Invalid("Interface 'spi3' is not available on this variant.") def _validate(config): From c56e8cf7d876930e9ef0a82ea6c53aa0ada6f9d1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 01:09:31 -1000 Subject: [PATCH 20/28] fix validation --- esphome/components/ethernet/__init__.py | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index e5f8f465fe..350480f560 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -240,10 +240,8 @@ def _validate_spi_interface(config: ConfigType) -> None: has_spi3 = "spi3" in sum(get_hw_interface_list(), []) if CONF_INTERFACE not in config: - config[CONF_INTERFACE] = ( - spi_host_device_t.SPI3_HOST if has_spi3 else spi_host_device_t.SPI2_HOST - ) - elif config[CONF_INTERFACE] == spi_host_device_t.SPI3_HOST and not has_spi3: + config[CONF_INTERFACE] = "spi3" if has_spi3 else "spi2" + elif config[CONF_INTERFACE] == "spi3" and not has_spi3: raise cv.Invalid("Interface 'spi3' is not available on this variant.") @@ -393,7 +391,7 @@ SPI_SCHEMA = cv.All( ), cv.Optional(CONF_INTERFACE): cv.All( cv.only_on_esp32, - cv.enum(SPI_INTERFACE_MAP), + cv.one_of(*SPI_INTERFACE_MAP.keys()), ), # Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate() cv.Optional(CONF_POLLING_INTERVAL): cv.All( @@ -438,9 +436,8 @@ def _final_validate_spi(config): from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface if spi_configs := fv.full_config.get().get(CONF_SPI): - # config[CONF_INTERFACE] is a codegen enum (e.g. ::SPI2_HOST) # get_spi_interface() returns strings like "SPI2_HOST" - spi_host = str(config[CONF_INTERFACE]).removeprefix("::") + spi_host = config[CONF_INTERFACE].upper() + "_HOST" for spi_conf in spi_configs: if (index := spi_conf.get(CONF_INTERFACE_INDEX)) is not None: interface = get_spi_interface(index) @@ -539,7 +536,7 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None: cg.add_define("USE_ETHERNET_SPI") if CONF_INTERFACE in config: - cg.add(var.set_interface(config[CONF_INTERFACE])) + cg.add(var.set_interface(SPI_INTERFACE_MAP[config[CONF_INTERFACE]])) add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) # CONFIG_ETH_SPI_ETHERNET_{TYPE} Kconfig options were removed in IDF 6.0 # ENC28J60 was never built-in to IDF, so it has no Kconfig option From 1329ad245e1620864787603dc9f15af17e386eda Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 4 Apr 2026 01:14:18 -1000 Subject: [PATCH 21/28] bot coments --- esphome/components/ethernet/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 350480f560..6bb51d4843 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -391,7 +391,7 @@ SPI_SCHEMA = cv.All( ), cv.Optional(CONF_INTERFACE): cv.All( cv.only_on_esp32, - cv.one_of(*SPI_INTERFACE_MAP.keys()), + cv.one_of(*SPI_INTERFACE_MAP.keys(), lower=True), ), # Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate() cv.Optional(CONF_POLLING_INTERVAL): cv.All( From 4e9e505829f4418ec1ed2c651ca453fee77fb9d4 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 09:27:37 -1000 Subject: [PATCH 22/28] touch ups --- esphome/components/ethernet/__init__.py | 5 ++--- esphome/components/ethernet/ethernet_component.h | 2 +- esphome/components/ethernet/ethernet_component_esp32.cpp | 9 ++++----- 3 files changed, 7 insertions(+), 9 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 6bb51d4843..04729aa891 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -437,7 +437,7 @@ def _final_validate_spi(config): if spi_configs := fv.full_config.get().get(CONF_SPI): # get_spi_interface() returns strings like "SPI2_HOST" - spi_host = config[CONF_INTERFACE].upper() + "_HOST" + spi_host = f"{config[CONF_INTERFACE].upper()}_HOST" for spi_conf in spi_configs: if (index := spi_conf.get(CONF_INTERFACE_INDEX)) is not None: interface = get_spi_interface(index) @@ -535,8 +535,7 @@ async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None: cg.add_define("USE_ETHERNET_SPI") - if CONF_INTERFACE in config: - cg.add(var.set_interface(SPI_INTERFACE_MAP[config[CONF_INTERFACE]])) + cg.add(var.set_interface(SPI_INTERFACE_MAP[config[CONF_INTERFACE]])) add_idf_sdkconfig_option("CONFIG_ETH_USE_SPI_ETHERNET", True) # CONFIG_ETH_SPI_ETHERNET_{TYPE} Kconfig options were removed in IDF 6.0 # ENC28J60 was never built-in to IDF, so it has no Kconfig option diff --git a/esphome/components/ethernet/ethernet_component.h b/esphome/components/ethernet/ethernet_component.h index 33240ceedf..b760ba2af7 100644 --- a/esphome/components/ethernet/ethernet_component.h +++ b/esphome/components/ethernet/ethernet_component.h @@ -205,7 +205,7 @@ class EthernetComponent final : public Component { int reset_pin_{-1}; int phy_addr_spi_{-1}; int clock_speed_; - spi_host_device_t interface_{SPI2_HOST}; + spi_host_device_t interface_{SPI3_HOST}; #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT uint32_t polling_interval_{0}; #endif diff --git a/esphome/components/ethernet/ethernet_component_esp32.cpp b/esphome/components/ethernet/ethernet_component_esp32.cpp index 5745ccf3a7..d4585bf100 100644 --- a/esphome/components/ethernet/ethernet_component_esp32.cpp +++ b/esphome/components/ethernet/ethernet_component_esp32.cpp @@ -453,12 +453,11 @@ void EthernetComponent::dump_config() { " MOSI Pin: %u\n" " CS Pin: %u", this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_); - { - constexpr std::array values{"spi1", "spi2", "spi3"}; - if (this->interface_ < values.size()) { - ESP_LOGCONFIG(TAG, " Interface: %s", values[this->interface_]); - } + const char *spi_interface = "spi3"; + if (this->interface_ == SPI2_HOST) { + spi_interface = "spi2"; } + ESP_LOGCONFIG(TAG, " Interface: %s", spi_interface); #ifdef USE_ETHERNET_SPI_POLLING_SUPPORT if (this->polling_interval_ != 0) { ESP_LOGCONFIG(TAG, " Polling Interval: %" PRIu32 " ms", this->polling_interval_); From 380aa0b3ee4803c50d1db391d05dbedcad4fae85 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 09:28:36 -1000 Subject: [PATCH 23/28] touch ups --- tests/components/ethernet/test-w5500.esp32-idf.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/components/ethernet/test-w5500.esp32-idf.yaml b/tests/components/ethernet/test-w5500.esp32-idf.yaml index 151f220bc0..486c533759 100644 --- a/tests/components/ethernet/test-w5500.esp32-idf.yaml +++ b/tests/components/ethernet/test-w5500.esp32-idf.yaml @@ -12,4 +12,4 @@ ethernet: gateway: 192.168.178.1 subnet: 255.255.255.0 domain: .local - interface: spi3 + interface: spi2 From 8f26903c8cad93232b27e16efbf14beea7ef0979 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 09:34:26 -1000 Subject: [PATCH 24/28] match --- esphome/components/ethernet/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 04729aa891..b940321e50 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -236,11 +236,17 @@ def _is_framework_spi_polling_mode_supported() -> bool: def _validate_spi_interface(config: ConfigType) -> None: """Set default SPI interface or validate user choice against the variant.""" + from esphome.components.esp32 import get_esp32_variant from esphome.components.spi import get_hw_interface_list has_spi3 = "spi3" in sum(get_hw_interface_list(), []) if CONF_INTERFACE not in config: - config[CONF_INTERFACE] = "spi3" if has_spi3 else "spi2" + # Match the original C++ variant-based defaults: + # Only classic ESP32 defaults to spi3; all others default to spi2 + variant = get_esp32_variant() + from esphome.components.esp32 import VARIANT_ESP32 + + config[CONF_INTERFACE] = "spi3" if variant == VARIANT_ESP32 else "spi2" elif config[CONF_INTERFACE] == "spi3" and not has_spi3: raise cv.Invalid("Interface 'spi3' is not available on this variant.") From bfb6f2d0fda8a17f5ed150d709f87f4747d303f8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 09:36:18 -1000 Subject: [PATCH 25/28] match --- esphome/components/ethernet/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index b940321e50..3d21d6e7da 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -297,7 +297,6 @@ def _validate(config): f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), " f"'{CONF_INTERRUPT_PIN}' is a required option for [ethernet]." ) - _validate_spi_interface(config) elif config[CONF_TYPE] != "OPENETH": from esphome.components.esp32 import ( VARIANT_ESP32, @@ -409,6 +408,7 @@ SPI_SCHEMA = cv.All( ), ), cv.only_on([Platform.ESP32, Platform.RP2040]), + _validate_spi_interface, ) CONFIG_SCHEMA = cv.All( From c2f5b5f77023bc667a73b98f8f6447dee27754eb Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 09:36:37 -1000 Subject: [PATCH 26/28] amek proper validator --- esphome/components/ethernet/__init__.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index 3d21d6e7da..ba80af74db 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -234,21 +234,22 @@ def _is_framework_spi_polling_mode_supported() -> bool: return False -def _validate_spi_interface(config: ConfigType) -> None: +def _validate_spi_interface(config): """Set default SPI interface or validate user choice against the variant.""" - from esphome.components.esp32 import get_esp32_variant + if not CORE.is_esp32: + return config + from esphome.components.esp32 import VARIANT_ESP32, get_esp32_variant from esphome.components.spi import get_hw_interface_list has_spi3 = "spi3" in sum(get_hw_interface_list(), []) if CONF_INTERFACE not in config: - # Match the original C++ variant-based defaults: # Only classic ESP32 defaults to spi3; all others default to spi2 - variant = get_esp32_variant() - from esphome.components.esp32 import VARIANT_ESP32 - - config[CONF_INTERFACE] = "spi3" if variant == VARIANT_ESP32 else "spi2" + config[CONF_INTERFACE] = ( + "spi3" if get_esp32_variant() == VARIANT_ESP32 else "spi2" + ) elif config[CONF_INTERFACE] == "spi3" and not has_spi3: raise cv.Invalid("Interface 'spi3' is not available on this variant.") + return config def _validate(config): From f070ef6d08cbafa5f984accae910bddfc32fe176 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 09:43:58 -1000 Subject: [PATCH 27/28] preen --- esphome/components/ethernet/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index ba80af74db..fe358f293d 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -234,7 +234,7 @@ def _is_framework_spi_polling_mode_supported() -> bool: return False -def _validate_spi_interface(config): +def _validate_spi_interface(config: ConfigType) -> ConfigType: """Set default SPI interface or validate user choice against the variant.""" if not CORE.is_esp32: return config From 47c573479336347828247b10bc2c4f1adaa60ded Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 5 Apr 2026 10:00:45 -1000 Subject: [PATCH 28/28] bot comments --- esphome/components/ethernet/__init__.py | 4 ++-- tests/components/ethernet/test-w5500.esp32-idf.yaml | 5 +++++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/esphome/components/ethernet/__init__.py b/esphome/components/ethernet/__init__.py index fe358f293d..d9f51c677e 100644 --- a/esphome/components/ethernet/__init__.py +++ b/esphome/components/ethernet/__init__.py @@ -450,8 +450,8 @@ def _final_validate_spi(config): interface = get_spi_interface(index) if interface == spi_host: raise cv.Invalid( - f"this and an `spi` component are both using interface '{interface}'. " - f"To use {config[CONF_TYPE]}, you must change the `interface` on one of these." + f"The `ethernet` and `spi` components are both using interface '{interface}'. " + f"To use {config[CONF_TYPE]}, change the `interface` on either `ethernet:` or `spi:`." ) diff --git a/tests/components/ethernet/test-w5500.esp32-idf.yaml b/tests/components/ethernet/test-w5500.esp32-idf.yaml index 486c533759..f1551fef60 100644 --- a/tests/components/ethernet/test-w5500.esp32-idf.yaml +++ b/tests/components/ethernet/test-w5500.esp32-idf.yaml @@ -12,4 +12,9 @@ ethernet: gateway: 192.168.178.1 subnet: 255.255.255.0 domain: .local + mac_address: "02:AA:BB:CC:DD:01" interface: spi2 + on_connect: + - logger.log: "Ethernet connected!" + on_disconnect: + - logger.log: "Ethernet disconnected!"