Files
esphome-lsm6ds/esphome/components/ethernet/__init__.py
T
e9cc10fedc [core] Native idf full support (#14678)
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <swoboda1337@users.noreply.github.com>
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com>
2026-05-11 02:12:07 +00:00

718 lines
26 KiB
Python

from dataclasses import dataclass
import logging
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components.network import ip_address_literal
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
CONF_CLK_PIN,
CONF_CS_PIN,
CONF_DNS1,
CONF_DNS2,
CONF_DOMAIN,
CONF_GATEWAY,
CONF_ID,
CONF_INTERRUPT_PIN,
CONF_MAC_ADDRESS,
CONF_MANUAL_IP,
CONF_MISO_PIN,
CONF_MODE,
CONF_MOSI_PIN,
CONF_NUMBER,
CONF_ON_CONNECT,
CONF_ON_DISCONNECT,
CONF_PAGE_ID,
CONF_PIN,
CONF_POLLING_INTERVAL,
CONF_RESET_PIN,
CONF_SPI,
CONF_STATIC_IP,
CONF_SUBNET,
CONF_TYPE,
CONF_USE_ADDRESS,
CONF_VALUE,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
Platform,
PlatformFramework,
)
from esphome.core import (
CORE,
CoroPriority,
TimePeriodMilliseconds,
coroutine_with_priority,
)
import esphome.final_validate as fv
from esphome.types import ConfigType
CONFLICTS_WITH = ["wifi"]
AUTO_LOAD = ["network"]
LOGGER = logging.getLogger(__name__)
# Key for tracking IP state listener count in CORE.data
ETHERNET_IP_STATE_LISTENERS_KEY = "ethernet_ip_state_listeners"
# Key for tracking configured ethernet type
ETHERNET_TYPE_KEY = "ethernet_type"
KEY_ETHERNET = "ethernet"
def request_ethernet_ip_state_listener() -> None:
"""Request an IP state listener slot.
Components that implement EthernetIPStateListener should call this
in their to_code() to register for IP state notifications.
"""
CORE.data[ETHERNET_IP_STATE_LISTENERS_KEY] = (
CORE.data.get(ETHERNET_IP_STATE_LISTENERS_KEY, 0) + 1
)
# RMII pins that are hardcoded on ESP32 classic and cannot be changed
# These pins are used by the internal Ethernet MAC when using RMII PHYs
ESP32_RMII_FIXED_PINS = {
19: "EMAC_TXD0",
21: "EMAC_TX_EN",
22: "EMAC_TXD1",
25: "EMAC_RXD0",
26: "EMAC_RXD1",
27: "EMAC_RX_CRS_DV",
}
# RMII default pins for ESP32-P4
# These are the default pins used by ESP-IDF and are configurable in principle,
# but ESPHome's ethernet component currently has no way to change them
ESP32P4_RMII_DEFAULT_PINS = {
34: "EMAC_TXD0",
35: "EMAC_TXD1",
28: "EMAC_RX_CRS_DV",
29: "EMAC_RXD0",
30: "EMAC_RXD1",
49: "EMAC_TX_EN",
}
ethernet_ns = cg.esphome_ns.namespace("ethernet")
PHYRegister = ethernet_ns.struct("PHYRegister")
CONF_PHY_ADDR = "phy_addr"
CONF_MDC_PIN = "mdc_pin"
CONF_MDIO_PIN = "mdio_pin"
CONF_CLK = "clk"
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")
ETHERNET_TYPES = {
"LAN8720": EthernetType.ETHERNET_TYPE_LAN8720,
"RTL8201": EthernetType.ETHERNET_TYPE_RTL8201,
"DP83848": EthernetType.ETHERNET_TYPE_DP83848,
"IP101": EthernetType.ETHERNET_TYPE_IP101,
"JL1101": EthernetType.ETHERNET_TYPE_JL1101,
"KSZ8081": EthernetType.ETHERNET_TYPE_KSZ8081,
"KSZ8081RNA": EthernetType.ETHERNET_TYPE_KSZ8081RNA,
"W5100": EthernetType.ETHERNET_TYPE_W5100,
"W5500": EthernetType.ETHERNET_TYPE_W5500,
"OPENETH": EthernetType.ETHERNET_TYPE_OPENETH,
"DM9051": EthernetType.ETHERNET_TYPE_DM9051,
"LAN8670": EthernetType.ETHERNET_TYPE_LAN8670,
"ENC28J60": EthernetType.ETHERNET_TYPE_ENC28J60,
"W6100": EthernetType.ETHERNET_TYPE_W6100,
"W6300": EthernetType.ETHERNET_TYPE_W6300,
}
# PHY types that need compile-time defines for conditional compilation
# Each RMII PHY type gets a define so unused PHY drivers are excluded by the linker
_PHY_TYPE_TO_DEFINE = {
"LAN8720": "USE_ETHERNET_LAN8720",
"RTL8201": "USE_ETHERNET_RTL8201",
"DP83848": "USE_ETHERNET_DP83848",
"IP101": "USE_ETHERNET_IP101",
"JL1101": "USE_ETHERNET_JL1101",
"KSZ8081": "USE_ETHERNET_KSZ8081",
"KSZ8081RNA": "USE_ETHERNET_KSZ8081",
"W5100": "USE_ETHERNET_W5100",
"W5500": "USE_ETHERNET_W5500",
"DM9051": "USE_ETHERNET_DM9051",
"LAN8670": "USE_ETHERNET_LAN8670",
"ENC28J60": "USE_ETHERNET_ENC28J60",
"W6100": "USE_ETHERNET_W6100",
"W6300": "USE_ETHERNET_W6300",
}
@dataclass(frozen=True)
class IDFRegistryComponent:
"""An ESP-IDF component from the Espressif Component Registry."""
name: str
version: str
# IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry.
_IDF6_ETHERNET_COMPONENTS: dict[str, IDFRegistryComponent] = {
"LAN8720": IDFRegistryComponent("espressif/lan87xx", "1.0.0"),
"RTL8201": IDFRegistryComponent("espressif/rtl8201", "1.0.1"),
"DP83848": IDFRegistryComponent("espressif/dp83848", "1.0.0"),
"IP101": IDFRegistryComponent("espressif/ip101", "1.0.0"),
"KSZ8081": IDFRegistryComponent("espressif/ksz80xx", "1.0.0"),
"KSZ8081RNA": IDFRegistryComponent("espressif/ksz80xx", "1.0.0"),
"W5500": IDFRegistryComponent("espressif/w5500", "1.0.1"),
"DM9051": IDFRegistryComponent("espressif/dm9051", "1.0.0"),
"ENC28J60": IDFRegistryComponent("espressif/enc28j60", "1.0.1"),
"LAN8670": IDFRegistryComponent("espressif/lan867x", "2.0.0"),
}
# These types are always external IDF components (never built-in to ESP-IDF)
_ALWAYS_EXTERNAL_IDF_COMPONENTS = {"LAN8670", "ENC28J60"}
# ESP32-only SPI ethernet types (W5100 is RP2040-only, no ESP-IDF driver)
SPI_ETHERNET_TYPES = {"W5500", "DM9051", "ENC28J60"}
# RP2040-supported ethernet types (SPI and PIO QSPI)
RP2040_ETHERNET_TYPES = {"W5100", "W5500", "W6100", "W6300", "ENC28J60"}
_RP2040_SPI_LIBRARIES = {
"W5100": "lwIP_w5100",
"W5500": "lwIP_w5500",
"ENC28J60": "lwIP_enc28j60",
"W6100": "lwIP_w6100",
"W6300": "lwIP_w6300",
}
SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10)
emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t")
CLK_MODES = {
"CLK_EXT_IN": emac_rmii_clock_mode_t.EMAC_CLK_EXT_IN,
"CLK_OUT": emac_rmii_clock_mode_t.EMAC_CLK_OUT,
}
CLK_MODES_DEPRECATED = {
"GPIO0_IN": ("CLK_EXT_IN", 0),
"GPIO0_OUT": ("CLK_OUT", 0),
"GPIO16_OUT": ("CLK_OUT", 16),
"GPIO17_OUT": ("CLK_OUT", 17),
}
spi_host_device_t = cg.global_ns.enum("spi_host_device_t")
SPI_INTERFACE_MAP = {
"spi2": spi_host_device_t.SPI2_HOST,
"spi3": spi_host_device_t.SPI3_HOST,
}
MANUAL_IP_SCHEMA = cv.Schema(
{
cv.Required(CONF_STATIC_IP): cv.ipv4address,
cv.Required(CONF_GATEWAY): cv.ipv4address,
cv.Required(CONF_SUBNET): cv.ipv4address,
cv.Optional(CONF_DNS1, default="0.0.0.0"): cv.ipv4address,
cv.Optional(CONF_DNS2, default="0.0.0.0"): cv.ipv4address,
}
)
EthernetComponent = ethernet_ns.class_("EthernetComponent", cg.Component)
ManualIP = ethernet_ns.struct("ManualIP")
def _is_framework_spi_polling_mode_supported() -> bool:
"""Check if ESP-IDF framework supports SPI polling mode (ESP32 only).
SPI Ethernet without IRQ feature is added in
esp-idf >= (5.3+, 5.2.1+, 5.1.4)
"""
if not CORE.is_esp32:
return False
from esphome.components.esp32 import idf_version
ver = idf_version()
if ver >= cv.Version(5, 3, 0):
return True
if cv.Version(5, 3, 0) > ver >= cv.Version(5, 2, 1):
return True
if cv.Version(5, 2, 0) > ver >= cv.Version(5, 1, 4): # noqa: SIM103
return True
return False
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 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:
# Only classic ESP32 defaults to spi3; all others default to 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):
if CONF_USE_ADDRESS not in config:
if CONF_MANUAL_IP in config:
use_address = str(config[CONF_MANUAL_IP][CONF_STATIC_IP])
else:
use_address = CORE.name + config[CONF_DOMAIN]
config[CONF_USE_ADDRESS] = use_address
if CORE.is_esp32:
if config[CONF_TYPE] in SPI_ETHERNET_TYPES:
# ENC28J60 driver does not support polling mode - interrupt is required
if config[CONF_TYPE] == "ENC28J60":
if CONF_POLLING_INTERVAL in config:
raise cv.Invalid(
f"'{CONF_POLLING_INTERVAL}' is not supported for ENC28J60. "
f"'{CONF_INTERRUPT_PIN}' is required."
)
if CONF_INTERRUPT_PIN not in config:
raise cv.Invalid(
f"'{CONF_INTERRUPT_PIN}' is a required option for ENC28J60."
)
elif _is_framework_spi_polling_mode_supported():
if CONF_POLLING_INTERVAL in config and CONF_INTERRUPT_PIN in config:
raise cv.Invalid(
f"Cannot specify more than one of {CONF_INTERRUPT_PIN}, {CONF_POLLING_INTERVAL}"
)
if (
CONF_POLLING_INTERVAL not in config
and CONF_INTERRUPT_PIN not in config
):
config[CONF_POLLING_INTERVAL] = (
SPI_ETHERNET_DEFAULT_POLLING_INTERVAL
)
else:
if CONF_POLLING_INTERVAL in config:
raise cv.Invalid(
"In this version of the framework "
f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), "
f"'{CONF_POLLING_INTERVAL}' is not supported."
)
if CONF_INTERRUPT_PIN not in config:
raise cv.Invalid(
"In this version of the framework "
f"({CORE.target_framework} {CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]}), "
f"'{CONF_INTERRUPT_PIN}' is a required option for [ethernet]."
)
elif config[CONF_TYPE] != "OPENETH":
from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32P4,
get_esp32_variant,
)
if CONF_CLK_MODE in config:
mode, pin = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]]
LOGGER.warning(
"[ethernet] The 'clk_mode' option is deprecated. "
"Please replace 'clk_mode: %s' with:\n"
" clk:\n"
" mode: %s\n"
" pin: %s\n"
"Removal scheduled for 2026.7.0.",
config[CONF_CLK_MODE],
mode,
pin,
)
config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode, CONF_PIN: pin})
del config[CONF_CLK_MODE]
elif CONF_CLK not in config:
raise cv.Invalid("'clk' is a required option for [ethernet].")
variant = get_esp32_variant()
if variant not in (VARIANT_ESP32, VARIANT_ESP32P4):
raise cv.Invalid(
f"{config[CONF_TYPE]} PHY requires RMII interface and is only supported "
f"on ESP32 classic and ESP32-P4, not {variant}"
)
elif CORE.is_rp2040 and config[CONF_TYPE] not in RP2040_ETHERNET_TYPES:
raise cv.Invalid(
f"Only {', '.join(sorted(RP2040_ETHERNET_TYPES))} are supported on RP2040, "
f"not {config[CONF_TYPE]}"
)
return config
BASE_SCHEMA = cv.Schema(
{
cv.GenerateID(): cv.declare_id(EthernetComponent),
cv.Optional(CONF_MANUAL_IP): MANUAL_IP_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_ON_CONNECT): automation.validate_automation(single=True),
cv.Optional(CONF_ON_DISCONNECT): automation.validate_automation(single=True),
}
).extend(cv.COMPONENT_SCHEMA)
PHY_REGISTER_SCHEMA = cv.Schema(
{
cv.Required(CONF_ADDRESS): cv.hex_int,
cv.Required(CONF_VALUE): cv.hex_int,
cv.Optional(CONF_PAGE_ID): cv.hex_int,
}
)
CLK_SCHEMA = cv.Schema(
{
cv.Required(CONF_MODE): cv.enum(CLK_MODES, upper=True, space="_"),
cv.Required(CONF_PIN): pins.internal_gpio_pin_number,
}
)
RMII_SCHEMA = cv.All(
BASE_SCHEMA.extend(
cv.Schema(
{
cv.Required(CONF_MDC_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_MDIO_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_CLK_MODE): cv.enum(
CLK_MODES_DEPRECATED, upper=True, space="_"
),
cv.Optional(CONF_CLK): CLK_SCHEMA,
cv.Optional(CONF_PHY_ADDR, default=0): cv.int_range(min=0, max=31),
cv.Optional(CONF_POWER_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_PHY_REGISTERS): cv.ensure_list(PHY_REGISTER_SCHEMA),
}
)
),
cv.only_on([Platform.ESP32]),
)
SPI_SCHEMA = cv.All(
BASE_SCHEMA.extend(
cv.Schema(
{
cv.Required(CONF_CLK_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_MISO_PIN): pins.internal_gpio_input_pin_number,
cv.Required(CONF_MOSI_PIN): pins.internal_gpio_output_pin_number,
cv.Required(CONF_CS_PIN): pins.internal_gpio_output_pin_number,
cv.Optional(CONF_INTERRUPT_PIN): pins.internal_gpio_input_pin_number,
cv.Optional(CONF_RESET_PIN): pins.internal_gpio_output_pin_number,
cv.SplitDefault(CONF_CLOCK_SPEED, esp32="26.67MHz"): cv.All(
cv.only_on_esp32,
cv.frequency,
cv.int_range(int(8e6), int(80e6)),
),
cv.Optional(CONF_INTERFACE): cv.All(
cv.only_on_esp32,
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(
cv.only_on_esp32,
cv.positive_time_period_milliseconds,
cv.Range(min=TimePeriodMilliseconds(milliseconds=1)),
),
}
),
),
cv.only_on([Platform.ESP32, Platform.RP2040]),
_validate_spi_interface,
)
CONFIG_SCHEMA = cv.All(
cv.typed_schema(
{
"LAN8720": RMII_SCHEMA,
"RTL8201": RMII_SCHEMA,
"DP83848": RMII_SCHEMA,
"IP101": RMII_SCHEMA,
"JL1101": RMII_SCHEMA,
"KSZ8081": RMII_SCHEMA,
"KSZ8081RNA": RMII_SCHEMA,
"W5100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])),
"W5500": SPI_SCHEMA,
"OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])),
"DM9051": SPI_SCHEMA,
"ENC28J60": SPI_SCHEMA,
"W6100": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])),
"W6300": cv.All(SPI_SCHEMA, cv.only_on([Platform.RP2040])),
"LAN8670": RMII_SCHEMA,
},
upper=True,
),
_validate,
)
def _final_validate_spi(config):
if not CORE.is_esp32:
return # SPI interface validation is ESP32-only
if config[CONF_TYPE] not in SPI_ETHERNET_TYPES:
return
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
if spi_configs := fv.full_config.get().get(CONF_SPI):
# get_spi_interface() returns strings like "SPI2_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)
if interface == spi_host:
raise cv.Invalid(
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:`."
)
def manual_ip(config):
return cg.StructInitializer(
ManualIP,
("static_ip", ip_address_literal(config[CONF_STATIC_IP])),
("gateway", ip_address_literal(config[CONF_GATEWAY])),
("subnet", ip_address_literal(config[CONF_SUBNET])),
("dns1", ip_address_literal(config[CONF_DNS1])),
("dns2", ip_address_literal(config[CONF_DNS2])),
)
def phy_register(address: int, value: int, page: int):
return cg.StructInitializer(
PHYRegister,
("address", address),
("value", value),
("page", page),
)
@coroutine_with_priority(CoroPriority.COMMUNICATION)
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
if CORE.is_esp32:
await _to_code_esp32(var, config)
elif CORE.is_rp2040:
await _to_code_rp2040(var, config)
cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]]))
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
CORE.data.setdefault(KEY_ETHERNET, {})[ETHERNET_TYPE_KEY] = config[CONF_TYPE]
if CONF_MANUAL_IP in config:
cg.add_define("USE_ETHERNET_MANUAL_IP")
cg.add(var.set_manual_ip(manual_ip(config[CONF_MANUAL_IP])))
# Add compile-time define for PHY types with specific code
if phy_define := _PHY_TYPE_TO_DEFINE.get(config[CONF_TYPE]):
cg.add_define(phy_define)
if mac_address := config.get(CONF_MAC_ADDRESS):
cg.add(var.set_fixed_mac(mac_address.parts))
cg.add_define("USE_ETHERNET")
if on_connect_config := config.get(CONF_ON_CONNECT):
cg.add_define("USE_ETHERNET_CONNECT_TRIGGER")
await automation.build_automation(
var.get_connect_trigger(), [], on_connect_config
)
if on_disconnect_config := config.get(CONF_ON_DISCONNECT):
cg.add_define("USE_ETHERNET_DISCONNECT_TRIGGER")
await automation.build_automation(
var.get_disconnect_trigger(), [], on_disconnect_config
)
CORE.add_job(final_step)
async def _to_code_esp32(var: cg.Pvariable, config: ConfigType) -> None:
from esphome.components.esp32 import (
add_idf_component,
add_idf_sdkconfig_option,
idf_version,
include_builtin_idf_component,
)
if config[CONF_TYPE] in SPI_ETHERNET_TYPES:
cg.add(var.set_clk_pin(config[CONF_CLK_PIN]))
cg.add(var.set_miso_pin(config[CONF_MISO_PIN]))
cg.add(var.set_mosi_pin(config[CONF_MOSI_PIN]))
cg.add(var.set_cs_pin(config[CONF_CS_PIN]))
if CONF_INTERRUPT_PIN in config:
cg.add(var.set_interrupt_pin(config[CONF_INTERRUPT_PIN]))
else:
cg.add(var.set_polling_interval(config[CONF_POLLING_INTERVAL]))
if _is_framework_spi_polling_mode_supported():
cg.add_define("USE_ETHERNET_SPI_POLLING_SUPPORT")
if CONF_RESET_PIN in config:
cg.add(var.set_reset_pin(config[CONF_RESET_PIN]))
cg.add(var.set_clock_speed(config[CONF_CLOCK_SPEED]))
cg.add_define("USE_ETHERNET_SPI")
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
if idf_version() < cv.Version(6, 0, 0) and config[CONF_TYPE] != "ENC28J60":
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)
else:
cg.add(var.set_phy_addr(config[CONF_PHY_ADDR]))
cg.add(var.set_mdc_pin(config[CONF_MDC_PIN]))
cg.add(var.set_mdio_pin(config[CONF_MDIO_PIN]))
cg.add(var.set_clk_mode(config[CONF_CLK][CONF_MODE]))
cg.add(var.set_clk_pin(config[CONF_CLK][CONF_PIN]))
if CONF_POWER_PIN in config:
cg.add(var.set_power_pin(config[CONF_POWER_PIN]))
for register_value in config.get(CONF_PHY_REGISTERS, []):
reg = phy_register(
register_value.get(CONF_ADDRESS),
register_value.get(CONF_VALUE),
register_value.get(CONF_PAGE_ID),
)
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)
# Re-enable ESP-IDF's Ethernet driver (excluded by default to save compile time)
include_builtin_idf_component("esp_eth")
if config[CONF_TYPE] in _ALWAYS_EXTERNAL_IDF_COMPONENTS:
component = _IDF6_ETHERNET_COMPONENTS[config[CONF_TYPE]]
add_idf_component(name=component.name, ref=component.version)
elif idf_version() >= cv.Version(6, 0, 0) and (
# IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry
component := _IDF6_ETHERNET_COMPONENTS.get(config[CONF_TYPE])
):
add_idf_component(name=component.name, ref=component.version)
async def _to_code_rp2040(var: cg.Pvariable, config: ConfigType) -> None:
cg.add(var.set_clk_pin(config[CONF_CLK_PIN]))
cg.add(var.set_miso_pin(config[CONF_MISO_PIN]))
cg.add(var.set_mosi_pin(config[CONF_MOSI_PIN]))
cg.add(var.set_cs_pin(config[CONF_CS_PIN]))
if CONF_INTERRUPT_PIN in config:
cg.add(var.set_interrupt_pin(config[CONF_INTERRUPT_PIN]))
if CONF_RESET_PIN in config:
cg.add(var.set_reset_pin(config[CONF_RESET_PIN]))
cg.add_define("USE_ETHERNET_SPI")
cg.add_library(_RP2040_SPI_LIBRARIES[config[CONF_TYPE]], None)
def _final_validate_rmii_pins(config: ConfigType) -> None:
"""Validate that RMII pins are not used by other components."""
if not CORE.is_esp32:
return # RMII validation is ESP32-only
# Only validate for RMII-based PHYs on ESP32/ESP32P4
if config[CONF_TYPE] in SPI_ETHERNET_TYPES or config[CONF_TYPE] == "OPENETH":
return # SPI and OPENETH don't use RMII
from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32P4,
get_esp32_variant,
)
variant = get_esp32_variant()
if variant == VARIANT_ESP32:
rmii_pins = ESP32_RMII_FIXED_PINS
is_configurable = False
elif variant == VARIANT_ESP32P4:
rmii_pins = ESP32P4_RMII_DEFAULT_PINS
is_configurable = True
else:
return # No RMII validation needed for other variants
# Check all used pins against RMII reserved pins
for pin_list in pins.PIN_SCHEMA_REGISTRY.pins_used.values():
for pin_path, pin_device, pin_config in pin_list:
pin_num = pin_config.get(CONF_NUMBER)
if pin_num not in rmii_pins:
continue
# Skip if pin is not directly on ESP, but at some expander (device set to something else than 'None')
if pin_device is not None:
continue
# Found a conflict - show helpful error message
pin_function = rmii_pins[pin_num]
component_path = ".".join(str(p) for p in pin_path)
if is_configurable:
error_msg = (
f"GPIO{pin_num} is used by Ethernet RMII "
f"({pin_function}) with the current default "
f"configuration. This conflicts with '{component_path}'. "
f"Please choose a different GPIO pin for "
f"'{component_path}'."
)
else:
error_msg = (
f"GPIO{pin_num} is reserved for Ethernet RMII "
f"({pin_function}) and cannot be used. This pin is "
f"hardcoded by ESP-IDF and cannot be changed when using "
f"RMII Ethernet PHYs. Please choose a different GPIO pin "
f"for '{component_path}'."
)
raise cv.Invalid(error_msg, path=pin_path)
def _final_validate(config: ConfigType) -> ConfigType:
"""Final validation for Ethernet component."""
_final_validate_spi(config)
_final_validate_rmii_pins(config)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
@coroutine_with_priority(CoroPriority.FINAL)
async def final_step():
"""Final code generation step to configure optional Ethernet features."""
if ip_state_count := CORE.data.get(ETHERNET_IP_STATE_LISTENERS_KEY, 0):
cg.add_define("USE_ETHERNET_IP_STATE_LISTENERS")
cg.add_define("ESPHOME_ETHERNET_IP_STATE_LISTENERS", ip_state_count)
_platform_filter = filter_source_files_from_platform(
{
"ethernet_component_esp32.cpp": {
PlatformFramework.ESP32_IDF,
PlatformFramework.ESP32_ARDUINO,
},
"ethernet_component_rp2040.cpp": {PlatformFramework.RP2040_ARDUINO},
"esp_eth_phy_jl1101.c": {
PlatformFramework.ESP32_IDF,
PlatformFramework.ESP32_ARDUINO,
},
}
)
def _filter_source_files() -> list[str]:
excluded = _platform_filter()
eth_data = CORE.data.get(KEY_ETHERNET, {})
eth_type = eth_data.get(ETHERNET_TYPE_KEY)
# Only compile the custom JL1101 driver when JL1101 is configured
# and pioarduino doesn't have it builtin (IDF 5.4.2 to 5.x)
if eth_type != "JL1101":
excluded.append("esp_eth_phy_jl1101.c")
elif CORE.is_esp32 and not CORE.using_toolchain_esp_idf:
from esphome.components.esp32 import idf_version
# pioarduino has JL1101 builtin on IDF 5.4.2-5.x; exclude custom driver
# to avoid shadowing. Native IDF builds always need the custom driver.
if cv.Version(5, 4, 2) <= idf_version() < cv.Version(6, 0, 0):
excluded.append("esp_eth_phy_jl1101.c")
return excluded
FILTER_SOURCE_FILES = _filter_source_files