Merge branch 'dev' into socket-lwip-raw-udp

This commit is contained in:
J. Nick Koston
2026-03-16 10:44:13 -10:00
committed by GitHub
98 changed files with 2160 additions and 1319 deletions
+1 -1
View File
@@ -27,7 +27,7 @@ jobs:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v2
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
+2 -2
View File
@@ -40,7 +40,7 @@ jobs:
echo "You have modified clang-tidy configuration but have not updated the hash." | tee -a $GITHUB_STEP_SUMMARY
echo "Please run 'script/clang_tidy_hash.py --update' and commit the changes." | tee -a $GITHUB_STEP_SUMMARY
- if: failure()
- if: failure() && github.event.pull_request.head.repo.full_name == github.repository
name: Request changes
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
@@ -53,7 +53,7 @@ jobs:
body: 'You have modified clang-tidy configuration but have not updated the hash.\nPlease run `script/clang_tidy_hash.py --update` and commit the changes.'
})
- if: success()
- if: success() && github.event.pull_request.head.repo.full_name == github.repository
name: Dismiss review
uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
with:
+2 -2
View File
@@ -58,7 +58,7 @@ jobs:
# Initializes the CodeQL tools for scanning.
- name: Initialize CodeQL
uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
uses: github/codeql-action/init@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
with:
languages: ${{ matrix.language }}
build-mode: ${{ matrix.build-mode }}
@@ -86,6 +86,6 @@ jobs:
exit 1
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6
uses: github/codeql-action/analyze@b1bff81932f5cdfc8695c7752dcee935dcd061c8 # v4.33.0
with:
category: "/language:${{matrix.language}}"
+3 -3
View File
@@ -221,7 +221,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -256,7 +256,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
@@ -287,7 +287,7 @@ jobs:
steps:
- name: Generate a token
id: generate-token
uses: actions/create-github-app-token@29824e69f54612133e76f7eaac726eef6c875baf # v2.2.1
uses: actions/create-github-app-token@f8d387b68d61c58ab83c6c016672934102569859 # v3.0.0
with:
app-id: ${{ secrets.ESPHOME_GITHUB_APP_ID }}
private-key: ${{ secrets.ESPHOME_GITHUB_APP_PRIVATE_KEY }}
+1 -1
View File
@@ -35,7 +35,7 @@ class Am43 : public esphome::ble_client::BLEClientNode, public PollingComponent
uint8_t current_sensor_;
// The AM43 often gets into a state where it spams loads of battery update
// notifications. Here we will limit to no more than every 10s.
uint8_t last_battery_update_;
uint32_t last_battery_update_;
};
} // namespace am43
+6 -4
View File
@@ -113,10 +113,11 @@ APIError APIFrameHelper::loop() {
// Common socket write error handling
APIError APIFrameHelper::handle_socket_write_error_() {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
const int err = errno;
if (err == EWOULDBLOCK || err == EAGAIN) {
return APIError::WOULD_BLOCK;
}
HELPER_LOG("Socket write failed with errno %d", errno);
HELPER_LOG("Socket write failed with errno %d", err);
this->state_ = State::FAILED;
return APIError::SOCKET_WRITE_FAILED;
}
@@ -278,11 +279,12 @@ APIError APIFrameHelper::init_common_() {
APIError APIFrameHelper::handle_socket_read_result_(ssize_t received) {
if (received == -1) {
if (errno == EWOULDBLOCK || errno == EAGAIN) {
const int err = errno;
if (err == EWOULDBLOCK || err == EAGAIN) {
return APIError::WOULD_BLOCK;
}
state_ = State::FAILED;
HELPER_LOG("Socket read failed with errno %d", errno);
HELPER_LOG("Socket read failed with errno %d", err);
return APIError::SOCKET_READ_FAILED;
} else if (received == 0) {
state_ = State::FAILED;
+9 -3
View File
@@ -12,6 +12,7 @@
#include "esphome/components/socket/socket.h"
#include "esphome/core/application.h"
#include "esphome/core/log.h"
#include "proto.h"
namespace esphome::api {
@@ -37,8 +38,6 @@ static constexpr uint16_t RX_BUF_NULL_TERMINATOR = 1;
// Must be >= MAX_INITIAL_PER_BATCH in api_connection.h (enforced by static_assert there)
static constexpr size_t MAX_MESSAGES_PER_BATCH = 34;
class ProtoWriteBuffer;
// Max client name length (e.g., "Home Assistant 2026.1.0.dev0" = 28 chars)
static constexpr size_t CLIENT_INFO_NAME_MAX_LEN = 32;
@@ -161,7 +160,14 @@ class APIFrameHelper {
this->nodelay_counter_ = 0;
}
}
virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
// Resize buffer to include footer space if needed (e.g. Noise MAC)
if (frame_footer_size_)
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
MessageInfo msg{type, 0,
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
}
// Write multiple protobuf messages in a single operation
// messages contains (message_type, offset, length) for each message in the buffer
// The buffer contains all messages with appropriate padding before each
@@ -450,14 +450,6 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = type;
return APIError::OK;
}
APIError APINoiseFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
// Resize to include MAC space (required for Noise encryption)
buffer.get_buffer()->resize(buffer.get_buffer()->size() + frame_footer_size_);
MessageInfo msg{type, 0,
static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_ - frame_footer_size_)};
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
}
APIError APINoiseFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) {
APIError aerr = this->check_data_state_();
if (aerr != APIError::OK)
@@ -22,7 +22,6 @@ class APINoiseFrameHelper final : public APIFrameHelper {
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
protected:
@@ -235,11 +235,6 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
buffer->type = this->rx_header_parsed_type_;
return APIError::OK;
}
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
MessageInfo msg{type, 0, static_cast<uint16_t>(buffer.get_buffer()->size() - frame_header_padding_)};
return write_protobuf_messages(buffer, std::span<const MessageInfo>(&msg, 1));
}
APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffer,
std::span<const MessageInfo> messages) {
APIError aerr = this->check_data_state_();
@@ -257,9 +252,11 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
uint16_t total_write_len = 0;
for (const auto &msg : messages) {
// Calculate varint sizes for header layout
uint8_t size_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(msg.payload_size));
uint8_t type_varint_len = api::ProtoSize::varint(static_cast<uint32_t>(msg.message_type));
// Calculate varint sizes for header layout using inline ternary to avoid varint_slow call overhead
uint8_t size_varint_len = msg.payload_size < ProtoSize::VARINT_THRESHOLD_1_BYTE
? 1
: (msg.payload_size < ProtoSize::VARINT_THRESHOLD_2_BYTE ? 2 : 3);
uint8_t type_varint_len = msg.message_type < ProtoSize::VARINT_THRESHOLD_1_BYTE ? 1 : 2;
uint8_t total_header_len = 1 + size_varint_len + type_varint_len;
// Calculate where to start writing the header
@@ -281,8 +278,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_messages(ProtoWriteBuffer buffe
//
// Example 3 (large values): total_header_len = 6, header_offset = 6 - 6 = 0
// [0] - 0x00 indicator byte
// [1-3] - Payload size varint (3 bytes, for sizes 16384-2097151)
// [4-5] - Message type varint (2 bytes, for types 128-32767)
// [1-3] - Payload size varint (3 bytes, for sizes 16384-65535)
// [4-5] - Message type varint (2 bytes, for types 128-16383)
// [6...] - Actual payload data
//
// The message starts at offset + frame_header_padding_
@@ -19,7 +19,6 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
APIError init() override;
APIError loop() override;
APIError read_packet(ReadPacketBuffer *buffer) override;
APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) override;
APIError write_protobuf_messages(ProtoWriteBuffer buffer, std::span<const MessageInfo> messages) override;
protected:
+3 -3
View File
@@ -36,11 +36,11 @@ struct SavedNoisePsk {
} PACKED; // NOLINT
#endif
class APIServer : public Component,
public Controller
class APIServer final : public Component,
public Controller
#ifdef USE_CAMERA
,
public camera::CameraListener
public camera::CameraListener
#endif
{
public:
+10 -4
View File
@@ -473,6 +473,12 @@ class ProtoDecodableMessage : public ProtoMessage {
class ProtoSize {
public:
// Varint encoding thresholds: values below each threshold fit in N bytes
static constexpr uint32_t VARINT_THRESHOLD_1_BYTE = 1 << 7; // 128
static constexpr uint32_t VARINT_THRESHOLD_2_BYTE = 1 << 14; // 16384
static constexpr uint32_t VARINT_THRESHOLD_3_BYTE = 1 << 21; // 2097152
static constexpr uint32_t VARINT_THRESHOLD_4_BYTE = 1 << 28; // 268435456
/**
* @brief Calculates the size in bytes needed to encode a uint32_t value as a varint
*
@@ -480,7 +486,7 @@ class ProtoSize {
* @return The number of bytes needed to encode the value
*/
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint(uint32_t value) {
if (value < 128) [[likely]]
if (value < VARINT_THRESHOLD_1_BYTE) [[likely]]
return 1; // Fast path: 7 bits, most common case
if (__builtin_is_constant_evaluated())
return varint_wide(value);
@@ -492,11 +498,11 @@ class ProtoSize {
static uint32_t varint_slow(uint32_t value) __attribute__((noinline));
// Shared cascade for values >= 128 (used by both constexpr and noinline paths)
static constexpr inline uint32_t ESPHOME_ALWAYS_INLINE varint_wide(uint32_t value) {
if (value < 16384)
if (value < VARINT_THRESHOLD_2_BYTE)
return 2;
if (value < 2097152)
if (value < VARINT_THRESHOLD_3_BYTE)
return 3;
if (value < 268435456)
if (value < VARINT_THRESHOLD_4_BYTE)
return 4;
return 5;
}
+1 -1
View File
@@ -41,7 +41,7 @@ enum AS3935RegisterMasks {
INT_MASK = 0xF0,
THRESH_MASK = 0x0F,
R_SPIKE_MASK = 0xF0,
ENERGY_MASK = 0xF0,
ENERGY_MASK = 0xE0,
CAP_MASK = 0xF0,
LIGHT_MASK = 0xCF,
DISTURB_MASK = 0xDF,
@@ -52,11 +52,12 @@ bool AsyncClient::connect(const char *host, uint16_t port) {
connect_cb_(connect_arg_, this);
return true;
}
if (errno != EINPROGRESS) {
ESP_LOGE(TAG, "Connect failed: %d", errno);
const int saved_errno = errno;
if (saved_errno != EINPROGRESS) {
ESP_LOGE(TAG, "Connect failed: %d", saved_errno);
close();
if (error_cb_)
error_cb_(error_arg_, this, errno);
error_cb_(error_arg_, this, saved_errno);
return false;
}
@@ -79,11 +80,12 @@ size_t AsyncClient::write(const char *data, size_t len) {
ssize_t sent = socket_->write(data, len);
if (sent < 0) {
if (errno != EAGAIN && errno != EWOULDBLOCK) {
ESP_LOGE(TAG, "Write error: %d", errno);
const int err = errno;
if (err != EAGAIN && err != EWOULDBLOCK) {
ESP_LOGE(TAG, "Write error: %d", err);
close();
if (error_cb_)
error_cb_(error_arg_, this, errno);
error_cb_(error_arg_, this, err);
}
return 0;
}
@@ -129,10 +131,11 @@ void AsyncClient::loop() {
error_cb_(error_arg_, this, error);
}
} else if (ret < 0) {
ESP_LOGE(TAG, "Select error: %d", errno);
const int err = errno;
ESP_LOGE(TAG, "Select error: %d", err);
close();
if (error_cb_)
error_cb_(error_arg_, this, errno);
error_cb_(error_arg_, this, err);
}
} else if (connected_) {
// For connected sockets, use the Application's select() results
@@ -148,11 +151,14 @@ void AsyncClient::loop() {
} else if (len > 0) {
if (data_cb_)
data_cb_(data_arg_, this, buf, len);
} else if (errno != EAGAIN && errno != EWOULDBLOCK) {
ESP_LOGW(TAG, "Read error: %d", errno);
close();
if (error_cb_)
error_cb_(error_arg_, this, errno);
} else {
const int err = errno;
if (err != EAGAIN && err != EWOULDBLOCK) {
ESP_LOGW(TAG, "Read error: %d", err);
close();
if (error_cb_)
error_cb_(error_arg_, this, err);
}
}
}
}
+1 -1
View File
@@ -37,7 +37,7 @@ class TimeoutFilter : public Filter, public Component {
TemplatableValue<uint32_t> timeout_delay_{};
};
class DelayedOnOffFilter : public Filter, public Component {
class DelayedOnOffFilter final : public Filter, public Component {
public:
optional<bool> new_value(bool value) override;
+4 -4
View File
@@ -67,14 +67,14 @@ bool BLENUS::read_array(uint8_t *data, size_t len) {
// First, use the peek buffer if available
if (this->has_peek_) {
#ifdef USE_UART_DEBUGGER
this->debug_callback_.call(uart::UART_DIRECTION_RX, this->peek_buffer_);
#endif
data[0] = this->peek_buffer_;
this->has_peek_ = false;
data++;
if (--len == 0) { // Decrement len first, then check it...
#ifdef USE_UART_DEBUGGER
this->debug_callback_.call(uart::UART_DIRECTION_RX, this->peek_buffer_);
#endif
return true; // No more to read
return true; // No more to read
}
}
@@ -100,8 +100,9 @@ void DNSServer::process_next_request() {
&client_addr_len);
if (len < 0) {
if (errno != EAGAIN && errno != EWOULDBLOCK && errno != EINTR) {
ESP_LOGE(TAG, "recvfrom failed: %d", errno);
const int err = errno;
if (err != EAGAIN && err != EWOULDBLOCK && err != EINTR) {
ESP_LOGE(TAG, "recvfrom failed: %d", err);
}
return;
}
@@ -136,6 +136,9 @@ bool DallasTemperatureSensor::check_scratch_pad_() {
float DallasTemperatureSensor::get_temp_c_() {
int16_t temp = (this->scratch_pad_[1] << 8) | this->scratch_pad_[0];
if ((this->address_ & 0xff) == DALLAS_MODEL_DS18S20) {
if (this->scratch_pad_[7] == 0) {
return NAN;
}
return (temp >> 1) + (this->scratch_pad_[7] - this->scratch_pad_[6]) / float(this->scratch_pad_[7]) - 0.25;
}
switch (this->resolution_) {
@@ -3,6 +3,7 @@
#include "driver/gpio.h"
#include "deep_sleep_component.h"
#include "esphome/core/log.h"
#include <esp_idf_version.h>
namespace esphome {
namespace deep_sleep {
@@ -26,7 +27,7 @@ namespace deep_sleep {
// - ext0: Single pin wakeup using RTC GPIO (esp_sleep_enable_ext0_wakeup)
// - ext1: Multiple pin wakeup (esp_sleep_enable_ext1_wakeup)
// - Touch: Touch pad wakeup (esp_sleep_enable_touchpad_wakeup)
// - GPIO wakeup: GPIO wakeup for RTC pins (esp_deep_sleep_enable_gpio_wakeup)
// - GPIO wakeup: GPIO wakeup for RTC pins
static const char *const TAG = "deep_sleep";
@@ -135,8 +136,13 @@ void DeepSleepComponent::deep_sleep_() {
}
// Internal pullup/pulldown resistors are enabled automatically, when
// ESP_SLEEP_GPIO_ENABLE_INTERNAL_RESISTORS is set (by default it is)
esp_deep_sleep_enable_gpio_wakeup(1 << this->wakeup_pin_->get_pin(),
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
esp_sleep_enable_gpio_wakeup_on_hp_periph_powerdown(1ULL << this->wakeup_pin_->get_pin(),
static_cast<esp_sleep_gpio_wake_up_mode_t>(level));
#else
esp_deep_sleep_enable_gpio_wakeup(1ULL << this->wakeup_pin_->get_pin(),
static_cast<esp_deepsleep_gpio_wake_up_mode_t>(level));
#endif
}
#endif
+4 -2
View File
@@ -612,10 +612,12 @@ def _format_framework_espidf_version(
ext = "tar.xz"
else:
ext = "zip"
# Build version string with dot-separated extra (e.g., "5.5.3.1" not "5.5.3-1")
# Build version string with extra separator based on type:
# numeric extra uses dot (e.g., "5.5.3.1"), string extra uses dash (e.g., "6.0.0-rc1")
ver_str = f"{ver.major}.{ver.minor}.{ver.patch}"
if ver.extra:
ver_str += f".{ver.extra}"
sep = "." if str(ver.extra).isdigit() else "-"
ver_str += f"{sep}{ver.extra}"
if release:
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{ver_str}.{release}/esp-idf-v{ver_str}.{ext}"
return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{ver_str}/esp-idf-v{ver_str}.{ext}"
-6
View File
@@ -20,12 +20,6 @@ bool random_bytes(uint8_t *data, size_t len) {
return true;
}
Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); }
Mutex::~Mutex() {}
void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); }
bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; }
void Mutex::unlock() { xSemaphoreGive(this->handle_); }
// only affects the executing core
// so should not be used as a mutex lock, only to get accurate timing
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
+2 -6
View File
@@ -12,12 +12,8 @@ namespace esphome {
uint32_t random_uint32() { return os_random(); }
bool random_bytes(uint8_t *data, size_t len) { return os_get_random(data, len) == 0; }
// ESP8266 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS.
Mutex::Mutex() {}
Mutex::~Mutex() {}
void Mutex::lock() {}
bool Mutex::try_lock() { return true; }
void Mutex::unlock() {}
// ESP8266 Mutex is defined inline as a no-op in helpers.h when USE_ESP8266 (or USE_RP2040) is set,
// independent of the ESPHOME_THREAD_SINGLE thread model define.
IRAM_ATTR InterruptLock::InterruptLock() { state_ = xt_rsil(15); }
IRAM_ATTR InterruptLock::~InterruptLock() { xt_wsr_ps(state_); }
@@ -332,12 +332,13 @@ void ESPHomeOTAComponent::handle_data_() {
size_t requested = remaining < OTA_BUFFER_SIZE ? remaining : OTA_BUFFER_SIZE;
ssize_t read = this->client_->read(buf, requested);
if (read == -1) {
if (this->would_block_(errno)) {
const int err = errno;
if (this->would_block_(err)) {
// read() already waited up to SO_RCVTIMEO for data, just feed WDT
App.feed_wdt();
continue;
}
ESP_LOGW(TAG, "Read err %d", errno);
ESP_LOGW(TAG, "Read err %d", err);
goto error; // NOLINT(cppcoreguidelines-avoid-goto)
} else if (read == 0) {
ESP_LOGW(TAG, "Remote closed");
@@ -426,8 +427,9 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) {
ssize_t read = this->client_->read(buf + at, len - at);
if (read == -1) {
if (!this->would_block_(errno)) {
ESP_LOGW(TAG, "Read err %zu bytes, errno %d", len, errno);
const int err = errno;
if (!this->would_block_(err)) {
ESP_LOGW(TAG, "Read err %zu bytes, errno %d", len, err);
return false;
}
} else if (read == 0) {
@@ -455,8 +457,9 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) {
ssize_t written = this->client_->write(buf + at, len - at);
if (written == -1) {
if (!this->would_block_(errno)) {
ESP_LOGW(TAG, "Write err %zu bytes, errno %d", len, errno);
const int err = errno;
if (!this->would_block_(err)) {
ESP_LOGW(TAG, "Write err %zu bytes, errno %d", len, err);
return false;
}
// EWOULDBLOCK: on raw TCP writes never block, delay(1) prevents spinning
+269 -125
View File
@@ -1,24 +1,10 @@
from dataclasses import dataclass
import logging
from esphome import automation, pins
import esphome.codegen as cg
from esphome.components.esp32 import (
VARIANT_ESP32,
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
add_idf_component,
add_idf_sdkconfig_option,
get_esp32_variant,
idf_version,
include_builtin_idf_component,
)
from esphome.components.network import ip_address_literal
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
from esphome.config_helpers import filter_source_files_from_platform
import esphome.config_validation as cv
from esphome.const import (
CONF_ADDRESS,
@@ -50,6 +36,9 @@ from esphome.const import (
CONF_VALUE,
KEY_CORE,
KEY_FRAMEWORK_VERSION,
KEY_NATIVE_IDF,
Platform,
PlatformFramework,
)
from esphome.core import (
CORE,
@@ -61,12 +50,14 @@ import esphome.final_validate as fv
from esphome.types import ConfigType
CONFLICTS_WITH = ["wifi"]
DEPENDENCIES = ["esp32"]
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:
@@ -140,10 +131,35 @@ _PHY_TYPE_TO_DEFINE = {
"JL1101": "USE_ETHERNET_JL1101",
"KSZ8081": "USE_ETHERNET_KSZ8081",
"KSZ8081RNA": "USE_ETHERNET_KSZ8081",
"W5500": "USE_ETHERNET_W5500",
"DM9051": "USE_ETHERNET_DM9051",
"LAN8670": "USE_ETHERNET_LAN8670",
}
@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"),
}
SPI_ETHERNET_TYPES = ["W5500", "DM9051"]
# RP2040-supported SPI ethernet types
RP2040_SPI_ETHERNET_TYPES = ["W5500"]
SPI_ETHERNET_DEFAULT_POLLING_INTERVAL = TimePeriodMilliseconds(milliseconds=10)
emac_rmii_clock_mode_t = cg.global_ns.enum("emac_rmii_clock_mode_t")
@@ -174,9 +190,16 @@ EthernetComponent = ethernet_ns.class_("EthernetComponent", cg.Component)
ManualIP = ethernet_ns.struct("ManualIP")
def _is_framework_spi_polling_mode_supported():
# SPI Ethernet without IRQ feature is added in
# esp-idf >= (5.3+ ,5.2.1+, 5.1.4)
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
@@ -195,52 +218,68 @@ def _validate(config):
use_address = CORE.name + config[CONF_DOMAIN]
config[CONF_USE_ADDRESS] = use_address
if config[CONF_TYPE] in SPI_ETHERNET_TYPES:
if _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":
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}"
if CORE.is_esp32:
if config[CONF_TYPE] in SPI_ETHERNET_TYPES:
if _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_SPI_ETHERNET_TYPES:
raise cv.Invalid(
f"Only {', '.join(RP2040_SPI_ETHERNET_TYPES)} are supported on RP2040, "
f"not {config[CONF_TYPE]}"
)
return config
@@ -269,41 +308,50 @@ CLK_SCHEMA = cv.Schema(
cv.Required(CONF_PIN): pins.internal_gpio_pin_number,
}
)
RMII_SCHEMA = 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),
}
)
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 = 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.Optional(CONF_CLOCK_SPEED, default="26.67MHz"): cv.All(
cv.frequency, cv.int_range(int(8e6), int(80e6))
),
# Set default value (SPI_ETHERNET_DEFAULT_POLLING_INTERVAL) at _validate()
cv.Optional(CONF_POLLING_INTERVAL): cv.All(
cv.positive_time_period_milliseconds,
cv.Range(min=TimePeriodMilliseconds(milliseconds=1)),
),
}
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)),
),
# 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]),
)
CONFIG_SCHEMA = cv.All(
@@ -317,7 +365,7 @@ CONFIG_SCHEMA = cv.All(
"KSZ8081": RMII_SCHEMA,
"KSZ8081RNA": RMII_SCHEMA,
"W5500": SPI_SCHEMA,
"OPENETH": BASE_SCHEMA,
"OPENETH": cv.All(BASE_SCHEMA, cv.only_on([Platform.ESP32])),
"DM9051": SPI_SCHEMA,
"LAN8670": RMII_SCHEMA,
},
@@ -328,8 +376,21 @@ CONFIG_SCHEMA = cv.All(
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.esp32 import (
VARIANT_ESP32C3,
VARIANT_ESP32C5,
VARIANT_ESP32C6,
VARIANT_ESP32C61,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
get_esp32_variant,
)
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
if spi_configs := fv.full_config.get().get(CONF_SPI):
variant = get_esp32_variant()
if variant in (
@@ -378,6 +439,51 @@ 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]))
@@ -395,7 +501,11 @@ async def to_code(config):
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)
# CONFIG_ETH_SPI_ETHERNET_{TYPE} Kconfig options were removed in IDF 6.0
if idf_version() < cv.Version(6, 0, 0):
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)
@@ -415,22 +525,6 @@ async def to_code(config):
)
cg.add(var.add_phy_register(reg))
cg.add(var.set_type(ETHERNET_TYPES[config[CONF_TYPE]]))
cg.add(var.set_use_address(config[CONF_USE_ADDRESS]))
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")
# 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
@@ -443,27 +537,41 @@ async def to_code(config):
# Add LAN867x 10BASE-T1S PHY support component
add_idf_component(name="espressif/lan867x", ref="2.0.0")
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
)
# IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry
if idf_version() >= cv.Version(6, 0, 0) and (
component := _IDF6_ETHERNET_COMPONENTS.get(config[CONF_TYPE])
):
add_idf_component(name=component.name, ref=component.version)
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_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("lwIP_w5500", 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
@@ -521,3 +629,39 @@ async def final_step():
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.data.get(KEY_NATIVE_IDF, False):
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
@@ -29,7 +29,8 @@
#include "esp_rom_sys.h"
#include "esp_idf_version.h"
#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO))
#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) || \
ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO))
static const char *TAG = "jl1101";
#define PHY_CHECK(a, str, goto_tag, ...) \
@@ -1,547 +1,28 @@
#include "ethernet_component.h"
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#ifdef USE_ETHERNET
#include "esphome/core/log.h"
#include "esphome/core/util.h"
#ifdef USE_ESP32
#include <lwip/dns.h>
#include <cinttypes>
#include "esp_event.h"
#ifdef USE_ETHERNET_LAN8670
#include "esp_eth_phy_lan867x.h"
#endif
#ifdef USE_ETHERNET_SPI
#include <driver/gpio.h>
#include <driver/spi_master.h>
#endif
namespace esphome::ethernet {
static const char *const TAG = "ethernet";
// PHY register size for hex logging
static constexpr size_t PHY_REG_SIZE = 2;
EthernetComponent *global_eth_component; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) {
ESP_LOGE(TAG, "%s: (%d) %s", message, err, esp_err_to_name(err));
this->mark_failed();
}
#define ESPHL_ERROR_CHECK(err, message) \
if ((err) != ESP_OK) { \
this->log_error_and_mark_failed_(err, message); \
return; \
}
#define ESPHL_ERROR_CHECK_RET(err, message, ret) \
if ((err) != ESP_OK) { \
this->log_error_and_mark_failed_(err, message); \
return ret; \
}
EthernetComponent::EthernetComponent() { global_eth_component = this; }
void EthernetComponent::setup() {
if (esp_reset_reason() != ESP_RST_DEEPSLEEP) {
// Delay here to allow power to stabilise before Ethernet is initialized.
delay(300); // NOLINT
}
esp_err_t err;
#ifdef USE_ETHERNET_SPI
// Install GPIO ISR handler to be able to service SPI Eth modules interrupts
gpio_install_isr_service(0);
spi_bus_config_t buscfg = {
.mosi_io_num = this->mosi_pin_,
.miso_io_num = this->miso_pin_,
.sclk_io_num = this->clk_pin_,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.data4_io_num = -1,
.data5_io_num = -1,
.data6_io_num = -1,
.data7_io_num = -1,
.max_transfer_sz = 0,
.flags = 0,
.intr_flags = 0,
};
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || \
defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
auto host = SPI2_HOST;
#else
auto host = SPI3_HOST;
#endif
err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO);
ESPHL_ERROR_CHECK(err, "SPI bus initialize error");
#endif
err = esp_netif_init();
ESPHL_ERROR_CHECK(err, "ETH netif init error");
err = esp_event_loop_create_default();
ESPHL_ERROR_CHECK(err, "ETH event loop error");
esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH();
this->eth_netif_ = esp_netif_new(&cfg);
// Init MAC and PHY configs to default
eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
#ifdef USE_ETHERNET_SPI // Configure SPI interface and Ethernet driver for specific SPI module
spi_device_interface_config_t devcfg = {
.command_bits = 0,
.address_bits = 0,
.dummy_bits = 0,
.mode = 0,
.duty_cycle_pos = 0,
.cs_ena_pretrans = 0,
.cs_ena_posttrans = 0,
.clock_speed_hz = this->clock_speed_,
.input_delay_ns = 0,
.spics_io_num = this->cs_pin_,
.flags = 0,
.queue_size = 20,
.pre_cb = nullptr,
.post_cb = nullptr,
};
#if CONFIG_ETH_SPI_ETHERNET_W5500
eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg);
#endif
#if CONFIG_ETH_SPI_ETHERNET_DM9051
eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg);
#endif
#if CONFIG_ETH_SPI_ETHERNET_W5500
w5500_config.int_gpio_num = this->interrupt_pin_;
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
w5500_config.poll_period_ms = this->polling_interval_;
#endif
#endif
#if CONFIG_ETH_SPI_ETHERNET_DM9051
dm9051_config.int_gpio_num = this->interrupt_pin_;
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
dm9051_config.poll_period_ms = this->polling_interval_;
#endif
#endif
phy_config.phy_addr = this->phy_addr_spi_;
phy_config.reset_gpio_num = this->reset_pin_;
esp_eth_mac_t *mac = nullptr;
#elif defined(USE_ETHERNET_OPENETH)
esp_eth_mac_t *mac = esp_eth_mac_new_openeth(&mac_config);
#else
phy_config.phy_addr = this->phy_addr_;
phy_config.reset_gpio_num = this->power_pin_;
eth_esp32_emac_config_t esp32_emac_config = eth_esp32_emac_default_config();
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
esp32_emac_config.smi_gpio.mdc_num = this->mdc_pin_;
esp32_emac_config.smi_gpio.mdio_num = this->mdio_pin_;
#else
esp32_emac_config.smi_mdc_gpio_num = this->mdc_pin_;
esp32_emac_config.smi_mdio_gpio_num = this->mdio_pin_;
#endif
esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_;
esp32_emac_config.clock_config.rmii.clock_gpio = (emac_rmii_clock_gpio_t) this->clk_pin_;
esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config);
#endif
switch (this->type_) {
#ifdef USE_ETHERNET_OPENETH
case ETHERNET_TYPE_OPENETH: {
phy_config.autonego_timeout_ms = 1000;
this->phy_ = esp_eth_phy_new_dp83848(&phy_config);
break;
}
#endif
#if CONFIG_ETH_USE_ESP32_EMAC
#ifdef USE_ETHERNET_LAN8720
case ETHERNET_TYPE_LAN8720: {
this->phy_ = esp_eth_phy_new_lan87xx(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_RTL8201
case ETHERNET_TYPE_RTL8201: {
this->phy_ = esp_eth_phy_new_rtl8201(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_DP83848
case ETHERNET_TYPE_DP83848: {
this->phy_ = esp_eth_phy_new_dp83848(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_IP101
case ETHERNET_TYPE_IP101: {
this->phy_ = esp_eth_phy_new_ip101(&phy_config);
break;
}
#endif
#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO))
case ETHERNET_TYPE_JL1101: {
this->phy_ = esp_eth_phy_new_jl1101(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_KSZ8081
case ETHERNET_TYPE_KSZ8081:
case ETHERNET_TYPE_KSZ8081RNA: {
this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_LAN8670
case ETHERNET_TYPE_LAN8670: {
this->phy_ = esp_eth_phy_new_lan867x(&phy_config);
break;
}
#endif
#endif
#ifdef USE_ETHERNET_SPI
#if CONFIG_ETH_SPI_ETHERNET_W5500
case ETHERNET_TYPE_W5500: {
mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config);
this->phy_ = esp_eth_phy_new_w5500(&phy_config);
break;
}
#endif
#if CONFIG_ETH_SPI_ETHERNET_DM9051
case ETHERNET_TYPE_DM9051: {
mac = esp_eth_mac_new_dm9051(&dm9051_config, &mac_config);
this->phy_ = esp_eth_phy_new_dm9051(&phy_config);
break;
}
#endif
#endif
default: {
this->mark_failed();
return;
}
}
esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, this->phy_);
this->eth_handle_ = nullptr;
err = esp_eth_driver_install(&eth_config, &this->eth_handle_);
ESPHL_ERROR_CHECK(err, "ETH driver install error");
#ifndef USE_ETHERNET_SPI
#ifdef USE_ETHERNET_KSZ8081
if (this->type_ == ETHERNET_TYPE_KSZ8081RNA && this->clk_mode_ == EMAC_CLK_OUT) {
// KSZ8081RNA default is incorrect. It expects a 25MHz clock instead of the 50MHz we provide.
this->ksz8081_set_clock_reference_(mac);
}
#endif // USE_ETHERNET_KSZ8081
for (const auto &phy_register : this->phy_registers_) {
this->write_phy_register_(mac, phy_register);
}
#endif
// use ESP internal eth mac
uint8_t mac_addr[6];
if (this->fixed_mac_.has_value()) {
memcpy(mac_addr, this->fixed_mac_->data(), 6);
} else {
esp_read_mac(mac_addr, ESP_MAC_ETH);
}
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_MAC_ADDR, mac_addr);
ESPHL_ERROR_CHECK(err, "set mac address error");
/* attach Ethernet driver to TCP/IP stack */
err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_));
ESPHL_ERROR_CHECK(err, "ETH netif attach error");
// Register user defined event handers
err = esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, &EthernetComponent::eth_event_handler, nullptr);
ESPHL_ERROR_CHECK(err, "ETH event handler register error");
err = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &EthernetComponent::got_ip_event_handler, nullptr);
ESPHL_ERROR_CHECK(err, "GOT IP event handler register error");
#if USE_NETWORK_IPV6
err = esp_event_handler_register(IP_EVENT, IP_EVENT_GOT_IP6, &EthernetComponent::got_ip6_event_handler, nullptr);
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");
}
void EthernetComponent::loop() {
const uint32_t now = App.get_loop_component_start_time();
switch (this->state_) {
case EthernetComponentState::STOPPED:
if (this->started_) {
ESP_LOGI(TAG, "Starting connection");
this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_();
}
break;
case EthernetComponentState::CONNECTING:
if (!this->started_) {
ESP_LOGI(TAG, "Stopped connection");
this->state_ = EthernetComponentState::STOPPED;
} else if (this->connected_) {
// connection established
ESP_LOGI(TAG, "Connected");
this->state_ = EthernetComponentState::CONNECTED;
this->dump_connect_params_();
this->status_clear_warning();
#ifdef USE_ETHERNET_CONNECT_TRIGGER
this->connect_trigger_.trigger();
#endif
} else if (now - this->connect_begin_ > 15000) {
ESP_LOGW(TAG, "Connecting failed; reconnecting");
this->start_connect_();
}
break;
case EthernetComponentState::CONNECTED:
if (!this->started_) {
ESP_LOGI(TAG, "Stopped connection");
this->state_ = EthernetComponentState::STOPPED;
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
this->disconnect_trigger_.trigger();
#endif
} else if (!this->connected_) {
ESP_LOGW(TAG, "Connection lost; reconnecting");
this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_();
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
this->disconnect_trigger_.trigger();
#endif
} else {
this->finish_connect_();
// When connected and stable, disable the loop to save CPU cycles
this->disable_loop();
}
break;
}
}
void EthernetComponent::dump_config() {
const char *eth_type;
switch (this->type_) {
#ifdef USE_ETHERNET_LAN8720
case ETHERNET_TYPE_LAN8720:
eth_type = "LAN8720";
break;
#endif
#ifdef USE_ETHERNET_RTL8201
case ETHERNET_TYPE_RTL8201:
eth_type = "RTL8201";
break;
#endif
#ifdef USE_ETHERNET_DP83848
case ETHERNET_TYPE_DP83848:
eth_type = "DP83848";
break;
#endif
#ifdef USE_ETHERNET_IP101
case ETHERNET_TYPE_IP101:
eth_type = "IP101";
break;
#endif
#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO))
case ETHERNET_TYPE_JL1101:
eth_type = "JL1101";
break;
#endif
#ifdef USE_ETHERNET_KSZ8081
case ETHERNET_TYPE_KSZ8081:
eth_type = "KSZ8081";
break;
case ETHERNET_TYPE_KSZ8081RNA:
eth_type = "KSZ8081RNA";
break;
#endif
#if CONFIG_ETH_SPI_ETHERNET_W5500
case ETHERNET_TYPE_W5500:
eth_type = "W5500";
break;
#endif
#if CONFIG_ETH_SPI_ETHERNET_DM9051
case ETHERNET_TYPE_DM9051:
eth_type = "DM9051";
break;
#endif
#ifdef USE_ETHERNET_OPENETH
case ETHERNET_TYPE_OPENETH:
eth_type = "OPENETH";
break;
#endif
#ifdef USE_ETHERNET_LAN8670
case ETHERNET_TYPE_LAN8670:
eth_type = "LAN8670";
break;
#endif
default:
eth_type = "Unknown";
break;
}
ESP_LOGCONFIG(TAG,
"Ethernet:\n"
" Connected: %s",
YESNO(this->is_connected()));
this->dump_connect_params_();
#ifdef USE_ETHERNET_SPI
ESP_LOGCONFIG(TAG,
" CLK Pin: %u\n"
" MISO Pin: %u\n"
" MOSI Pin: %u\n"
" CS Pin: %u",
this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_);
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
if (this->polling_interval_ != 0) {
ESP_LOGCONFIG(TAG, " Polling Interval: %lu ms", this->polling_interval_);
} else
#endif
{
ESP_LOGCONFIG(TAG, " IRQ Pin: %d", this->interrupt_pin_);
}
ESP_LOGCONFIG(TAG,
" Reset Pin: %d\n"
" Clock Speed: %d MHz",
this->reset_pin_, this->clock_speed_ / 1000000);
#else
if (this->power_pin_ != -1) {
ESP_LOGCONFIG(TAG, " Power Pin: %u", this->power_pin_);
}
ESP_LOGCONFIG(TAG,
" CLK Pin: %u\n"
" MDC Pin: %u\n"
" MDIO Pin: %u\n"
" PHY addr: %u",
this->clk_pin_, this->mdc_pin_, this->mdio_pin_, this->phy_addr_);
#endif
ESP_LOGCONFIG(TAG, " Type: %s", eth_type);
}
float EthernetComponent::get_setup_priority() const { return setup_priority::WIFI; }
network::IPAddresses EthernetComponent::get_ip_addresses() {
network::IPAddresses addresses;
esp_netif_ip_info_t ip;
esp_err_t err = esp_netif_get_ip_info(this->eth_netif_, &ip);
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err));
// TODO: do something smarter
// return false;
} else {
addresses[0] = network::IPAddress(&ip.ip);
}
#if USE_NETWORK_IPV6
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
uint8_t count = 0;
count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s);
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
assert(count < addresses.size());
for (int i = 0; i < count; i++) {
addresses[i + 1] = network::IPAddress(&if_ip6s[i]);
}
#endif /* USE_NETWORK_IPV6 */
void EthernetComponent::set_type(EthernetType type) { this->type_ = type; }
return addresses;
}
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
LwIPLock lock;
const ip_addr_t *dns_ip = dns_getserver(num);
return dns_ip;
}
void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event, void *event_data) {
const char *event_name;
switch (event) {
case ETHERNET_EVENT_START:
event_name = "ETH started";
global_eth_component->started_ = true;
global_eth_component->enable_loop_soon_any_context();
break;
case ETHERNET_EVENT_STOP:
event_name = "ETH stopped";
global_eth_component->started_ = false;
global_eth_component->connected_ = false;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
break;
case ETHERNET_EVENT_CONNECTED:
event_name = "ETH connected";
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#if defined(USE_ETHERNET_IP_STATE_LISTENERS) && defined(USE_ETHERNET_MANUAL_IP)
if (global_eth_component->manual_ip_.has_value()) {
global_eth_component->notify_ip_state_listeners_();
}
#ifdef USE_ETHERNET_MANUAL_IP
void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
#endif
break;
case ETHERNET_EVENT_DISCONNECTED:
event_name = "ETH disconnected";
global_eth_component->connected_ = false;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
break;
default:
return;
}
ESP_LOGV(TAG, "[Ethernet event] %s (num=%" PRId32 ")", event_name, event);
}
// set_use_address() is guaranteed to be called during component setup by Python code generation,
// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
const char *EthernetComponent::get_use_address() const { return this->use_address_; }
void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id,
void *event_data) {
ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data;
const esp_netif_ip_info_t *ip_info = &event->ip_info;
ESP_LOGV(TAG, "[Ethernet event] ETH Got IP " IPSTR, IP2STR(&ip_info->ip));
global_eth_component->got_ipv4_address_ = true;
#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#else
global_eth_component->connected_ = true;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif /* USE_NETWORK_IPV6 */
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->notify_ip_state_listeners_();
#endif
}
#if USE_NETWORK_IPV6
void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id,
void *event_data) {
ip_event_got_ip6_t *event = (ip_event_got_ip6_t *) event_data;
ESP_LOGV(TAG, "[Ethernet event] ETH Got IPv6: " IPV6STR, IPV62STR(event->ip6_info.ip));
global_eth_component->ipv6_count_ += 1;
#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ =
global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT);
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#else
global_eth_component->connected_ = global_eth_component->got_ipv4_address_;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->notify_ip_state_listeners_();
#endif
}
#endif /* USE_NETWORK_IPV6 */
void EthernetComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; }
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
void EthernetComponent::notify_ip_state_listeners_() {
@@ -554,315 +35,6 @@ void EthernetComponent::notify_ip_state_listeners_() {
}
#endif // USE_ETHERNET_IP_STATE_LISTENERS
void EthernetComponent::finish_connect_() {
#if USE_NETWORK_IPV6
// Retry IPv6 link-local setup if it failed during initial connect
// This handles the case where min_ipv6_addr_count is NOT set (or is 0),
// allowing us to reach CONNECTED state with just IPv4.
// If IPv6 setup failed in start_connect_() because the interface wasn't ready:
// - Bootup timing issues (#10281)
// - Cable unplugged/network interruption (#10705)
// We can now retry since we're in CONNECTED state and the interface is definitely up.
if (!this->ipv6_setup_done_) {
esp_err_t err = esp_netif_create_ip6_linklocal(this->eth_netif_);
if (err == ESP_OK) {
ESP_LOGD(TAG, "IPv6 link-local address created (retry succeeded)");
}
// Always set the flag to prevent continuous retries
// If IPv6 setup fails here with the interface up and stable, it's
// likely a persistent issue (IPv6 disabled at router, hardware
// limitation, etc.) that won't be resolved by further retries.
// The device continues to work with IPv4.
this->ipv6_setup_done_ = true;
}
#endif /* USE_NETWORK_IPV6 */
}
void EthernetComponent::start_connect_() {
global_eth_component->got_ipv4_address_ = false;
#if USE_NETWORK_IPV6
global_eth_component->ipv6_count_ = 0;
this->ipv6_setup_done_ = false;
#endif /* USE_NETWORK_IPV6 */
this->connect_begin_ = millis();
this->status_set_warning(LOG_STR("waiting for IP configuration"));
esp_err_t err;
err = esp_netif_set_hostname(this->eth_netif_, App.get_name().c_str());
if (err != ERR_OK) {
ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err));
}
esp_netif_ip_info_t info;
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
info.ip = this->manual_ip_->static_ip;
info.gw = this->manual_ip_->gateway;
info.netmask = this->manual_ip_->subnet;
} else
#endif
{
info.ip.addr = 0;
info.gw.addr = 0;
info.netmask.addr = 0;
}
esp_netif_dhcp_status_t status = ESP_NETIF_DHCP_INIT;
err = esp_netif_dhcpc_get_status(this->eth_netif_, &status);
ESPHL_ERROR_CHECK(err, "DHCPC Get Status Failed!");
ESP_LOGV(TAG, "DHCP Client Status: %d", status);
err = esp_netif_dhcpc_stop(this->eth_netif_);
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
ESPHL_ERROR_CHECK(err, "DHCPC stop error");
}
err = esp_netif_set_ip_info(this->eth_netif_, &info);
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
LwIPLock lock;
if (this->manual_ip_->dns1.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns1;
dns_setserver(0, &d);
}
if (this->manual_ip_->dns2.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns2;
dns_setserver(1, &d);
}
} else
#endif
{
err = esp_netif_dhcpc_start(this->eth_netif_);
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) {
ESPHL_ERROR_CHECK(err, "DHCPC start error");
}
}
#if USE_NETWORK_IPV6
// Attempt to create IPv6 link-local address
// We MUST attempt this here, not just in finish_connect_(), because with
// min_ipv6_addr_count set, the component won't reach CONNECTED state without IPv6.
// However, this may fail with ESP_FAIL if the interface is not up yet:
// - At bootup when link isn't ready (#10281)
// - After disconnection/cable unplugged (#10705)
// We'll retry in finish_connect_() if it fails here.
err = esp_netif_create_ip6_linklocal(this->eth_netif_);
if (err != ESP_OK) {
if (err == ESP_ERR_ESP_NETIF_INVALID_PARAMS) {
// This is a programming error, not a transient failure
ESPHL_ERROR_CHECK(err, "esp_netif_create_ip6_linklocal invalid parameters");
} else {
// ESP_FAIL means the interface isn't up yet
// This is expected and non-fatal, happens in multiple scenarios:
// - During reconnection after network interruptions (#10705)
// - At bootup when the link isn't ready yet (#10281)
// We'll retry once we reach CONNECTED state and the interface is up
ESP_LOGW(TAG, "esp_netif_create_ip6_linklocal failed: %s", esp_err_to_name(err));
// Don't mark component as failed - this is a transient error
}
}
#endif /* USE_NETWORK_IPV6 */
this->connect_begin_ = millis();
this->status_set_warning();
}
void EthernetComponent::dump_connect_params_() {
esp_netif_ip_info_t ip;
esp_netif_get_ip_info(this->eth_netif_, &ip);
const ip_addr_t *dns_ip1;
const ip_addr_t *dns_ip2;
{
LwIPLock lock;
dns_ip1 = dns_getserver(0);
dns_ip2 = dns_getserver(1);
}
// Use stack buffers for IP address formatting to avoid heap allocations
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGCONFIG(TAG,
" IP Address: %s\n"
" Hostname: '%s'\n"
" Subnet: %s\n"
" Gateway: %s\n"
" DNS1: %s\n"
" DNS2: %s\n"
" MAC Address: %s\n"
" Is Full Duplex: %s\n"
" Link Speed: %u",
network::IPAddress(&ip.ip).str_to(ip_buf), App.get_name().c_str(),
network::IPAddress(&ip.netmask).str_to(subnet_buf), network::IPAddress(&ip.gw).str_to(gateway_buf),
network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf),
this->get_eth_mac_address_pretty_into_buffer(mac_buf),
YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10);
#if USE_NETWORK_IPV6
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
uint8_t count = 0;
count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s);
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
for (int i = 0; i < count; i++) {
ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(if_ip6s[i]));
}
#endif /* USE_NETWORK_IPV6 */
}
#ifdef USE_ETHERNET_SPI
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
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_ETHERNET_SPI_POLLING_SUPPORT
void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; }
#endif
#else
void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; }
void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; }
void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; }
void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; }
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; }
void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); }
#endif
void EthernetComponent::set_type(EthernetType type) { this->type_ = type; }
#ifdef USE_ETHERNET_MANUAL_IP
void EthernetComponent::set_manual_ip(const ManualIP &manual_ip) { this->manual_ip_ = manual_ip; }
#endif
// set_use_address() is guaranteed to be called during component setup by Python code generation,
// so use_address_ will always be valid when get_use_address() is called - no fallback needed.
const char *EthernetComponent::get_use_address() const { return this->use_address_; }
void EthernetComponent::set_use_address(const char *use_address) { this->use_address_ = use_address; }
void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
esp_err_t err;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, mac);
ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error");
}
std::string EthernetComponent::get_eth_mac_address_pretty() {
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
return std::string(this->get_eth_mac_address_pretty_into_buffer(buf));
}
const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
uint8_t mac[6];
get_eth_mac_address_raw(mac);
format_mac_addr_upper(mac, buf.data());
return buf.data();
}
eth_duplex_t EthernetComponent::get_duplex_mode() {
esp_err_t err;
eth_duplex_t duplex_mode;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode);
ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_DUPLEX_MODE error", ETH_DUPLEX_HALF);
return duplex_mode;
}
eth_speed_t EthernetComponent::get_link_speed() {
esp_err_t err;
eth_speed_t speed;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_SPEED, &speed);
ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_SPEED error", ETH_SPEED_10M);
return speed;
}
bool EthernetComponent::powerdown() {
ESP_LOGI(TAG, "Powering down ethernet PHY");
if (this->phy_ == nullptr) {
ESP_LOGE(TAG, "Ethernet PHY not assigned");
return false;
}
this->connected_ = false;
this->started_ = false;
// No need to enable_loop() here as this is only called during shutdown/reboot
if (this->phy_->pwrctl(this->phy_, false) != ESP_OK) {
ESP_LOGE(TAG, "Error powering down ethernet PHY");
return false;
}
return true;
}
#ifndef USE_ETHERNET_SPI
#ifdef USE_ETHERNET_KSZ8081
constexpr uint8_t KSZ80XX_PC2R_REG_ADDR = 0x1F;
void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
esp_err_t err;
uint32_t phy_control_2;
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)];
#endif
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
/*
* Bit 7 is `RMII Reference Clock Select`. Default is `0`.
* KSZ8081RNA:
* 0 - clock input to XI (Pin 8) is 25 MHz for RMII - 25 MHz clock mode.
* 1 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode.
* KSZ8081RND:
* 0 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode.
* 1 - clock input to XI (Pin 8) is 25 MHz (driven clock only, not a crystal) for RMII - 25 MHz clock mode.
*/
if ((phy_control_2 & (1 << 7)) != (1 << 7)) {
phy_control_2 |= 1 << 7;
err = mac->write_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, phy_control_2);
ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed");
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s",
format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
}
}
#endif // USE_ETHERNET_KSZ8081
void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data) {
esp_err_t err;
#ifdef USE_ETHERNET_RTL8201
constexpr uint8_t eth_phy_psr_reg_addr = 0x1F;
if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) {
ESP_LOGD(TAG, "Select PHY Register Page: 0x%02" PRIX32, register_data.page);
err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, register_data.page);
ESPHL_ERROR_CHECK(err, "Select PHY Register page failed");
}
#endif
ESP_LOGD(TAG, "Writing PHY reg 0x%02" PRIX32 " = 0x%04" PRIX32, register_data.address, register_data.value);
err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value);
ESPHL_ERROR_CHECK(err, "Writing PHY Register failed");
#ifdef USE_ETHERNET_RTL8201
if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) {
ESP_LOGD(TAG, "Select PHY Register Page 0x00");
err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, 0x0);
ESPHL_ERROR_CHECK(err, "Select PHY Register Page 0 failed");
}
#endif
}
#endif
} // namespace esphome::ethernet
#endif // USE_ESP32
#endif // USE_ETHERNET
@@ -7,8 +7,9 @@
#include "esphome/core/automation.h"
#include "esphome/components/network/ip_address.h"
#ifdef USE_ESP32
#ifdef USE_ETHERNET
#ifdef USE_ESP32
#include "esp_eth.h"
#include "esp_eth_mac.h"
#include "esp_eth_mac_esp.h"
@@ -19,6 +20,11 @@
#if CONFIG_ETH_USE_ESP32_EMAC
extern "C" eth_esp32_emac_config_t eth_esp32_emac_default_config(void);
#endif
#endif // USE_ESP32
#ifdef USE_RP2040
#include <W5500lwIP.h>
#endif
namespace esphome::ethernet {
@@ -73,6 +79,12 @@ enum class EthernetComponentState : uint8_t {
CONNECTED,
};
// Platform-neutral duplex/speed types
#ifndef USE_ESP32
enum eth_duplex_t { ETH_DUPLEX_HALF, ETH_DUPLEX_FULL };
enum eth_speed_t { ETH_SPEED_10M, ETH_SPEED_100M };
#endif
class EthernetComponent : public Component {
public:
EthernetComponent();
@@ -83,6 +95,28 @@ class EthernetComponent : public Component {
void on_powerdown() override { powerdown(); }
bool is_connected() { return this->state_ == EthernetComponentState::CONNECTED; }
void set_type(EthernetType type);
#ifdef USE_ETHERNET_MANUAL_IP
void set_manual_ip(const ManualIP &manual_ip);
#endif
void set_fixed_mac(const std::array<uint8_t, 6> &mac) { this->fixed_mac_ = mac; }
network::IPAddresses get_ip_addresses();
network::IPAddress get_dns_address(uint8_t num);
const char *get_use_address() const;
void set_use_address(const char *use_address);
void get_eth_mac_address_raw(uint8_t *mac);
// Remove before 2026.9.0
ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0")
std::string get_eth_mac_address_pretty();
const char *get_eth_mac_address_pretty_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
eth_duplex_t get_duplex_mode();
eth_speed_t get_link_speed();
bool powerdown();
#ifdef USE_ESP32
esp_eth_handle_t get_eth_handle() const { return this->eth_handle_; }
#ifdef USE_ETHERNET_SPI
void set_clk_pin(uint8_t clk_pin);
void set_miso_pin(uint8_t miso_pin);
@@ -102,26 +136,17 @@ class EthernetComponent : public Component {
void set_clk_pin(uint8_t clk_pin);
void set_clk_mode(emac_rmii_clock_mode_t clk_mode);
void add_phy_register(PHYRegister register_value);
#endif
void set_type(EthernetType type);
#ifdef USE_ETHERNET_MANUAL_IP
void set_manual_ip(const ManualIP &manual_ip);
#endif
void set_fixed_mac(const std::array<uint8_t, 6> &mac) { this->fixed_mac_ = mac; }
#endif // USE_ETHERNET_SPI
#endif // USE_ESP32
network::IPAddresses get_ip_addresses();
network::IPAddress get_dns_address(uint8_t num);
const char *get_use_address() const;
void set_use_address(const char *use_address);
void get_eth_mac_address_raw(uint8_t *mac);
// Remove before 2026.9.0
ESPDEPRECATED("Use get_eth_mac_address_pretty_into_buffer() instead. Removed in 2026.9.0", "2026.3.0")
std::string get_eth_mac_address_pretty();
const char *get_eth_mac_address_pretty_into_buffer(std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf);
eth_duplex_t get_duplex_mode();
eth_speed_t get_link_speed();
esp_eth_handle_t get_eth_handle() const { return this->eth_handle_; }
bool powerdown();
#ifdef USE_RP2040
void set_clk_pin(uint8_t clk_pin);
void set_miso_pin(uint8_t miso_pin);
void set_mosi_pin(uint8_t mosi_pin);
void set_cs_pin(uint8_t cs_pin);
void set_interrupt_pin(int8_t interrupt_pin);
void set_reset_pin(int8_t reset_pin);
#endif // USE_RP2040
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
void add_ip_state_listener(EthernetIPStateListener *listener) { this->ip_state_listeners_.push_back(listener); }
@@ -133,19 +158,22 @@ class EthernetComponent : public Component {
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
Trigger<> *get_disconnect_trigger() { return &this->disconnect_trigger_; }
#endif
protected:
void start_connect_();
void finish_connect_();
void dump_connect_params_();
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
void notify_ip_state_listeners_();
#endif
#ifdef USE_ESP32
static void eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
static void got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
#if LWIP_IPV6
static void got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id, void *event_data);
#endif /* LWIP_IPV6 */
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
void notify_ip_state_listeners_();
#endif
void start_connect_();
void finish_connect_();
void dump_connect_params_();
void log_error_and_mark_failed_(esp_err_t err, const char *message);
#ifdef USE_ETHERNET_KSZ8081
/// @brief Set `RMII Reference Clock Select` bit for KSZ8081.
@@ -177,7 +205,27 @@ class EthernetComponent : public Component {
uint8_t phy_addr_{0};
uint8_t mdc_pin_{23};
uint8_t mdio_pin_{18};
#endif
#endif // USE_ETHERNET_SPI
// ESP32 pointers
esp_netif_t *eth_netif_{nullptr};
esp_eth_handle_t eth_handle_;
esp_eth_phy_t *phy_{nullptr};
#endif // USE_ESP32
#ifdef USE_RP2040
static constexpr uint32_t LINK_CHECK_INTERVAL = 500; // ms between link/IP polls
Wiznet5500lwIP *eth_{nullptr};
uint32_t last_link_check_{0};
uint8_t clk_pin_;
uint8_t miso_pin_;
uint8_t mosi_pin_;
uint8_t cs_pin_;
int8_t interrupt_pin_{-1};
int8_t reset_pin_{-1};
#endif // USE_RP2040
// Common members
#ifdef USE_ETHERNET_MANUAL_IP
optional<ManualIP> manual_ip_{};
#endif
@@ -194,10 +242,6 @@ class EthernetComponent : public Component {
bool ipv6_setup_done_{false};
#endif /* LWIP_IPV6 */
// Pointers at the end (naturally aligned)
esp_netif_t *eth_netif_{nullptr};
esp_eth_handle_t eth_handle_;
esp_eth_phy_t *phy_{nullptr};
optional<std::array<uint8_t, 6>> fixed_mac_;
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
@@ -219,10 +263,13 @@ class EthernetComponent : public Component {
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
extern EthernetComponent *global_eth_component;
#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO))
#ifdef USE_ESP32
#if defined(USE_ETHERNET_JL1101) && (ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0) || \
ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2) || !defined(PLATFORMIO))
extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config);
#endif
#endif // USE_ESP32
} // namespace esphome::ethernet
#endif // USE_ESP32
#endif // USE_ETHERNET
@@ -0,0 +1,878 @@
#include "ethernet_component.h"
#if defined(USE_ETHERNET) && defined(USE_ESP32)
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include <lwip/dns.h>
#include <cinttypes>
#include "esp_event.h"
// IDF 6.0 moved per-chip PHY/MAC drivers to the Espressif Component Registry;
// they are no longer included via esp_eth.h and need explicit includes.
// On IDF 5.x these headers don't exist as standalone files.
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
#ifdef USE_ETHERNET_LAN8720
#include "esp_eth_phy_lan87xx.h"
#endif
#ifdef USE_ETHERNET_RTL8201
#include "esp_eth_phy_rtl8201.h"
#endif
#ifdef USE_ETHERNET_DP83848
#include "esp_eth_phy_dp83848.h"
#endif
#ifdef USE_ETHERNET_IP101
#include "esp_eth_phy_ip101.h"
#endif
#ifdef USE_ETHERNET_KSZ8081
#include "esp_eth_phy_ksz80xx.h"
#endif
#ifdef USE_ETHERNET_W5500
#include "esp_eth_mac_w5500.h"
#include "esp_eth_phy_w5500.h"
#endif
#ifdef USE_ETHERNET_DM9051
#include "esp_eth_mac_dm9051.h"
#include "esp_eth_phy_dm9051.h"
#endif
#endif // ESP_IDF_VERSION >= 6.0.0
// LAN867x header exists on all IDF versions (external component since IDF 5.3)
#ifdef USE_ETHERNET_LAN8670
#include "esp_eth_phy_lan867x.h"
#endif
#ifdef USE_ETHERNET_SPI
#include <driver/gpio.h>
#include <driver/spi_master.h>
#endif
namespace esphome::ethernet {
static const char *const TAG = "ethernet";
// PHY register size for hex logging
static constexpr size_t PHY_REG_SIZE = 2;
void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) {
ESP_LOGE(TAG, "%s: (%d) %s", message, err, esp_err_to_name(err));
this->mark_failed();
}
#define ESPHL_ERROR_CHECK(err, message) \
if ((err) != ESP_OK) { \
this->log_error_and_mark_failed_(err, message); \
return; \
}
#define ESPHL_ERROR_CHECK_RET(err, message, ret) \
if ((err) != ESP_OK) { \
this->log_error_and_mark_failed_(err, message); \
return ret; \
}
void EthernetComponent::loop() {
const uint32_t now = App.get_loop_component_start_time();
switch (this->state_) {
case EthernetComponentState::STOPPED:
if (this->started_) {
ESP_LOGI(TAG, "Starting connection");
this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_();
}
break;
case EthernetComponentState::CONNECTING:
if (!this->started_) {
ESP_LOGI(TAG, "Stopped connection");
this->state_ = EthernetComponentState::STOPPED;
} else if (this->connected_) {
// connection established
ESP_LOGI(TAG, "Connected");
this->state_ = EthernetComponentState::CONNECTED;
this->dump_connect_params_();
this->status_clear_warning();
#ifdef USE_ETHERNET_CONNECT_TRIGGER
this->connect_trigger_.trigger();
#endif
} else if (now - this->connect_begin_ > 15000) {
ESP_LOGW(TAG, "Connecting failed; reconnecting");
this->start_connect_();
}
break;
case EthernetComponentState::CONNECTED:
if (!this->started_) {
ESP_LOGI(TAG, "Stopped connection");
this->state_ = EthernetComponentState::STOPPED;
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
this->disconnect_trigger_.trigger();
#endif
} else if (!this->connected_) {
ESP_LOGW(TAG, "Connection lost; reconnecting");
this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_();
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
this->disconnect_trigger_.trigger();
#endif
} else {
this->finish_connect_();
// When connected and stable, disable the loop to save CPU cycles
this->disable_loop();
}
break;
}
}
void EthernetComponent::setup() {
if (esp_reset_reason() != ESP_RST_DEEPSLEEP) {
// Delay here to allow power to stabilise before Ethernet is initialized.
delay(300); // NOLINT
}
esp_err_t err;
#ifdef USE_ETHERNET_SPI
// Install GPIO ISR handler to be able to service SPI Eth modules interrupts
gpio_install_isr_service(0);
spi_bus_config_t buscfg = {
.mosi_io_num = this->mosi_pin_,
.miso_io_num = this->miso_pin_,
.sclk_io_num = this->clk_pin_,
.quadwp_io_num = -1,
.quadhd_io_num = -1,
.data4_io_num = -1,
.data5_io_num = -1,
.data6_io_num = -1,
.data7_io_num = -1,
.max_transfer_sz = 0,
.flags = 0,
.intr_flags = 0,
};
#if defined(USE_ESP32_VARIANT_ESP32C3) || defined(USE_ESP32_VARIANT_ESP32C5) || defined(USE_ESP32_VARIANT_ESP32C6) || \
defined(USE_ESP32_VARIANT_ESP32C61) || defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3)
auto host = SPI2_HOST;
#else
auto host = SPI3_HOST;
#endif
err = spi_bus_initialize(host, &buscfg, SPI_DMA_CH_AUTO);
ESPHL_ERROR_CHECK(err, "SPI bus initialize error");
#endif
err = esp_netif_init();
ESPHL_ERROR_CHECK(err, "ETH netif init error");
err = esp_event_loop_create_default();
ESPHL_ERROR_CHECK(err, "ETH event loop error");
esp_netif_config_t cfg = ESP_NETIF_DEFAULT_ETH();
this->eth_netif_ = esp_netif_new(&cfg);
// Init MAC and PHY configs to default
eth_phy_config_t phy_config = ETH_PHY_DEFAULT_CONFIG();
eth_mac_config_t mac_config = ETH_MAC_DEFAULT_CONFIG();
#ifdef USE_ETHERNET_SPI // Configure SPI interface and Ethernet driver for specific SPI module
spi_device_interface_config_t devcfg = {
.command_bits = 0,
.address_bits = 0,
.dummy_bits = 0,
.mode = 0,
.duty_cycle_pos = 0,
.cs_ena_pretrans = 0,
.cs_ena_posttrans = 0,
.clock_speed_hz = this->clock_speed_,
.input_delay_ns = 0,
.spics_io_num = this->cs_pin_,
.flags = 0,
.queue_size = 20,
.pre_cb = nullptr,
.post_cb = nullptr,
};
#ifdef USE_ETHERNET_W5500
eth_w5500_config_t w5500_config = ETH_W5500_DEFAULT_CONFIG(host, &devcfg);
#endif
#ifdef USE_ETHERNET_DM9051
eth_dm9051_config_t dm9051_config = ETH_DM9051_DEFAULT_CONFIG(host, &devcfg);
#endif
#ifdef USE_ETHERNET_W5500
w5500_config.int_gpio_num = this->interrupt_pin_;
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
w5500_config.poll_period_ms = this->polling_interval_;
#endif
#endif
#ifdef USE_ETHERNET_DM9051
dm9051_config.int_gpio_num = this->interrupt_pin_;
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
dm9051_config.poll_period_ms = this->polling_interval_;
#endif
#endif
phy_config.phy_addr = this->phy_addr_spi_;
phy_config.reset_gpio_num = this->reset_pin_;
esp_eth_mac_t *mac = nullptr;
#elif defined(USE_ETHERNET_OPENETH)
esp_eth_mac_t *mac = esp_eth_mac_new_openeth(&mac_config);
#else
phy_config.phy_addr = this->phy_addr_;
phy_config.reset_gpio_num = this->power_pin_;
eth_esp32_emac_config_t esp32_emac_config = eth_esp32_emac_default_config();
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 3, 0)
esp32_emac_config.smi_gpio.mdc_num = this->mdc_pin_;
esp32_emac_config.smi_gpio.mdio_num = this->mdio_pin_;
#else
esp32_emac_config.smi_mdc_gpio_num = this->mdc_pin_;
esp32_emac_config.smi_mdio_gpio_num = this->mdio_pin_;
#endif
esp32_emac_config.clock_config.rmii.clock_mode = this->clk_mode_;
esp32_emac_config.clock_config.rmii.clock_gpio =
static_cast<decltype(esp32_emac_config.clock_config.rmii.clock_gpio)>(this->clk_pin_);
esp_eth_mac_t *mac = esp_eth_mac_new_esp32(&esp32_emac_config, &mac_config);
#endif
switch (this->type_) {
#ifdef USE_ETHERNET_OPENETH
case ETHERNET_TYPE_OPENETH: {
phy_config.autonego_timeout_ms = 1000;
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
this->phy_ = esp_eth_phy_new_generic(&phy_config);
#else
this->phy_ = esp_eth_phy_new_dp83848(&phy_config);
#endif
break;
}
#endif
#if CONFIG_ETH_USE_ESP32_EMAC
#ifdef USE_ETHERNET_LAN8720
case ETHERNET_TYPE_LAN8720: {
this->phy_ = esp_eth_phy_new_lan87xx(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_RTL8201
case ETHERNET_TYPE_RTL8201: {
this->phy_ = esp_eth_phy_new_rtl8201(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_DP83848
case ETHERNET_TYPE_DP83848: {
this->phy_ = esp_eth_phy_new_dp83848(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_IP101
case ETHERNET_TYPE_IP101: {
this->phy_ = esp_eth_phy_new_ip101(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_JL1101
case ETHERNET_TYPE_JL1101: {
// PlatformIO (pioarduino): builtin esp_eth_phy_new_jl1101() on all IDF versions
// Non-PlatformIO: custom ESPHome driver (esp_eth_phy_jl1101.c)
this->phy_ = esp_eth_phy_new_jl1101(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_KSZ8081
case ETHERNET_TYPE_KSZ8081:
case ETHERNET_TYPE_KSZ8081RNA: {
this->phy_ = esp_eth_phy_new_ksz80xx(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_LAN8670
case ETHERNET_TYPE_LAN8670: {
this->phy_ = esp_eth_phy_new_lan867x(&phy_config);
break;
}
#endif
#endif
#ifdef USE_ETHERNET_SPI
#ifdef USE_ETHERNET_W5500
case ETHERNET_TYPE_W5500: {
mac = esp_eth_mac_new_w5500(&w5500_config, &mac_config);
this->phy_ = esp_eth_phy_new_w5500(&phy_config);
break;
}
#endif
#ifdef USE_ETHERNET_DM9051
case ETHERNET_TYPE_DM9051: {
mac = esp_eth_mac_new_dm9051(&dm9051_config, &mac_config);
this->phy_ = esp_eth_phy_new_dm9051(&phy_config);
break;
}
#endif
#endif
default: {
this->mark_failed();
return;
}
}
esp_eth_config_t eth_config = ETH_DEFAULT_CONFIG(mac, this->phy_);
this->eth_handle_ = nullptr;
err = esp_eth_driver_install(&eth_config, &this->eth_handle_);
ESPHL_ERROR_CHECK(err, "ETH driver install error");
#ifndef USE_ETHERNET_SPI
#ifdef USE_ETHERNET_KSZ8081
if (this->type_ == ETHERNET_TYPE_KSZ8081RNA && this->clk_mode_ == EMAC_CLK_OUT) {
// KSZ8081RNA default is incorrect. It expects a 25MHz clock instead of the 50MHz we provide.
this->ksz8081_set_clock_reference_(mac);
}
#endif // USE_ETHERNET_KSZ8081
for (const auto &phy_register : this->phy_registers_) {
this->write_phy_register_(mac, phy_register);
}
#endif
// use ESP internal eth mac
uint8_t mac_addr[6];
if (this->fixed_mac_.has_value()) {
memcpy(mac_addr, this->fixed_mac_->data(), 6);
} else {
esp_read_mac(mac_addr, ESP_MAC_ETH);
}
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_S_MAC_ADDR, mac_addr);
ESPHL_ERROR_CHECK(err, "set mac address error");
/* attach Ethernet driver to TCP/IP stack */
err = esp_netif_attach(this->eth_netif_, esp_eth_new_netif_glue(this->eth_handle_));
ESPHL_ERROR_CHECK(err, "ETH netif attach error");
// Register user defined event handers
err = esp_event_handler_register(ETH_EVENT, ESP_EVENT_ANY_ID, &EthernetComponent::eth_event_handler, nullptr);
ESPHL_ERROR_CHECK(err, "ETH event handler register error");
err = esp_event_handler_register(IP_EVENT, IP_EVENT_ETH_GOT_IP, &EthernetComponent::got_ip_event_handler, nullptr);
ESPHL_ERROR_CHECK(err, "GOT IP event handler register error");
#if USE_NETWORK_IPV6
err = esp_event_handler_register(IP_EVENT, IP_EVENT_GOT_IP6, &EthernetComponent::got_ip6_event_handler, nullptr);
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");
}
void EthernetComponent::dump_config() {
const char *eth_type;
switch (this->type_) {
#ifdef USE_ETHERNET_LAN8720
case ETHERNET_TYPE_LAN8720:
eth_type = "LAN8720";
break;
#endif
#ifdef USE_ETHERNET_RTL8201
case ETHERNET_TYPE_RTL8201:
eth_type = "RTL8201";
break;
#endif
#ifdef USE_ETHERNET_DP83848
case ETHERNET_TYPE_DP83848:
eth_type = "DP83848";
break;
#endif
#ifdef USE_ETHERNET_IP101
case ETHERNET_TYPE_IP101:
eth_type = "IP101";
break;
#endif
#ifdef USE_ETHERNET_JL1101
case ETHERNET_TYPE_JL1101:
eth_type = "JL1101";
break;
#endif
#ifdef USE_ETHERNET_KSZ8081
case ETHERNET_TYPE_KSZ8081:
eth_type = "KSZ8081";
break;
case ETHERNET_TYPE_KSZ8081RNA:
eth_type = "KSZ8081RNA";
break;
#endif
#ifdef USE_ETHERNET_W5500
case ETHERNET_TYPE_W5500:
eth_type = "W5500";
break;
#endif
#ifdef USE_ETHERNET_DM9051
case ETHERNET_TYPE_DM9051:
eth_type = "DM9051";
break;
#endif
#ifdef USE_ETHERNET_OPENETH
case ETHERNET_TYPE_OPENETH:
eth_type = "OPENETH";
break;
#endif
#ifdef USE_ETHERNET_LAN8670
case ETHERNET_TYPE_LAN8670:
eth_type = "LAN8670";
break;
#endif
default:
eth_type = "Unknown";
break;
}
ESP_LOGCONFIG(TAG,
"Ethernet:\n"
" Connected: %s",
YESNO(this->is_connected()));
this->dump_connect_params_();
#ifdef USE_ETHERNET_SPI
ESP_LOGCONFIG(TAG,
" CLK Pin: %u\n"
" MISO Pin: %u\n"
" MOSI Pin: %u\n"
" CS Pin: %u",
this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_);
#ifdef USE_ETHERNET_SPI_POLLING_SUPPORT
if (this->polling_interval_ != 0) {
ESP_LOGCONFIG(TAG, " Polling Interval: %" PRIu32 " ms", this->polling_interval_);
} else
#endif
{
ESP_LOGCONFIG(TAG, " IRQ Pin: %d", this->interrupt_pin_);
}
ESP_LOGCONFIG(TAG,
" Reset Pin: %d\n"
" Clock Speed: %d MHz",
this->reset_pin_, this->clock_speed_ / 1000000);
#else
if (this->power_pin_ != -1) {
ESP_LOGCONFIG(TAG, " Power Pin: %u", this->power_pin_);
}
ESP_LOGCONFIG(TAG,
" CLK Pin: %u\n"
" MDC Pin: %u\n"
" MDIO Pin: %u\n"
" PHY addr: %u",
this->clk_pin_, this->mdc_pin_, this->mdio_pin_, this->phy_addr_);
#endif
ESP_LOGCONFIG(TAG, " Type: %s", eth_type);
}
network::IPAddresses EthernetComponent::get_ip_addresses() {
network::IPAddresses addresses;
esp_netif_ip_info_t ip;
esp_err_t err = esp_netif_get_ip_info(this->eth_netif_, &ip);
if (err != ESP_OK) {
ESP_LOGV(TAG, "esp_netif_get_ip_info failed: %s", esp_err_to_name(err));
// TODO: do something smarter
// return false;
} else {
addresses[0] = network::IPAddress(&ip.ip);
}
#if USE_NETWORK_IPV6
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
uint8_t count = 0;
count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s);
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
assert(count < addresses.size());
for (int i = 0; i < count; i++) {
addresses[i + 1] = network::IPAddress(&if_ip6s[i]);
}
#endif /* USE_NETWORK_IPV6 */
return addresses;
}
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
LwIPLock lock;
const ip_addr_t *dns_ip = dns_getserver(num);
return dns_ip;
}
void EthernetComponent::eth_event_handler(void *arg, esp_event_base_t event_base, int32_t event, void *event_data) {
const char *event_name;
switch (event) {
case ETHERNET_EVENT_START:
event_name = "ETH started";
global_eth_component->started_ = true;
global_eth_component->enable_loop_soon_any_context();
break;
case ETHERNET_EVENT_STOP:
event_name = "ETH stopped";
global_eth_component->started_ = false;
global_eth_component->connected_ = false;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
break;
case ETHERNET_EVENT_CONNECTED:
event_name = "ETH connected";
// For static IP configurations, GOT_IP event may not fire, so notify IP listeners here
#if defined(USE_ETHERNET_IP_STATE_LISTENERS) && defined(USE_ETHERNET_MANUAL_IP)
if (global_eth_component->manual_ip_.has_value()) {
global_eth_component->notify_ip_state_listeners_();
}
#endif
break;
case ETHERNET_EVENT_DISCONNECTED:
event_name = "ETH disconnected";
global_eth_component->connected_ = false;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
break;
default:
return;
}
ESP_LOGV(TAG, "[Ethernet event] %s (num=%" PRId32 ")", event_name, event);
}
void EthernetComponent::got_ip_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id,
void *event_data) {
ip_event_got_ip_t *event = (ip_event_got_ip_t *) event_data;
const esp_netif_ip_info_t *ip_info = &event->ip_info;
ESP_LOGV(TAG, "[Ethernet event] ETH Got IP " IPSTR, IP2STR(&ip_info->ip));
global_eth_component->got_ipv4_address_ = true;
#if USE_NETWORK_IPV6 && (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ = global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#else
global_eth_component->connected_ = true;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif /* USE_NETWORK_IPV6 */
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->notify_ip_state_listeners_();
#endif
}
#if USE_NETWORK_IPV6
void EthernetComponent::got_ip6_event_handler(void *arg, esp_event_base_t event_base, int32_t event_id,
void *event_data) {
ip_event_got_ip6_t *event = (ip_event_got_ip6_t *) event_data;
ESP_LOGV(TAG, "[Ethernet event] ETH Got IPv6: " IPV6STR, IPV62STR(event->ip6_info.ip));
global_eth_component->ipv6_count_ += 1;
#if (USE_NETWORK_MIN_IPV6_ADDR_COUNT > 0)
global_eth_component->connected_ =
global_eth_component->got_ipv4_address_ && (global_eth_component->ipv6_count_ >= USE_NETWORK_MIN_IPV6_ADDR_COUNT);
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#else
global_eth_component->connected_ = global_eth_component->got_ipv4_address_;
global_eth_component->enable_loop_soon_any_context(); // Enable loop when connection state changes
#endif
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
global_eth_component->notify_ip_state_listeners_();
#endif
}
#endif /* USE_NETWORK_IPV6 */
void EthernetComponent::finish_connect_() {
#if USE_NETWORK_IPV6
// Retry IPv6 link-local setup if it failed during initial connect
// This handles the case where min_ipv6_addr_count is NOT set (or is 0),
// allowing us to reach CONNECTED state with just IPv4.
// If IPv6 setup failed in start_connect_() because the interface wasn't ready:
// - Bootup timing issues (#10281)
// - Cable unplugged/network interruption (#10705)
// We can now retry since we're in CONNECTED state and the interface is definitely up.
if (!this->ipv6_setup_done_) {
esp_err_t err = esp_netif_create_ip6_linklocal(this->eth_netif_);
if (err == ESP_OK) {
ESP_LOGD(TAG, "IPv6 link-local address created (retry succeeded)");
}
// Always set the flag to prevent continuous retries
// If IPv6 setup fails here with the interface up and stable, it's
// likely a persistent issue (IPv6 disabled at router, hardware
// limitation, etc.) that won't be resolved by further retries.
// The device continues to work with IPv4.
this->ipv6_setup_done_ = true;
}
#endif /* USE_NETWORK_IPV6 */
}
void EthernetComponent::start_connect_() {
global_eth_component->got_ipv4_address_ = false;
#if USE_NETWORK_IPV6
global_eth_component->ipv6_count_ = 0;
this->ipv6_setup_done_ = false;
#endif /* USE_NETWORK_IPV6 */
this->connect_begin_ = millis();
this->status_set_warning(LOG_STR("waiting for IP configuration"));
esp_err_t err;
err = esp_netif_set_hostname(this->eth_netif_, App.get_name().c_str());
if (err != ERR_OK) {
ESP_LOGW(TAG, "esp_netif_set_hostname failed: %s", esp_err_to_name(err));
}
esp_netif_ip_info_t info;
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
info.ip = this->manual_ip_->static_ip;
info.gw = this->manual_ip_->gateway;
info.netmask = this->manual_ip_->subnet;
} else
#endif
{
info.ip.addr = 0;
info.gw.addr = 0;
info.netmask.addr = 0;
}
esp_netif_dhcp_status_t status = ESP_NETIF_DHCP_INIT;
err = esp_netif_dhcpc_get_status(this->eth_netif_, &status);
ESPHL_ERROR_CHECK(err, "DHCPC Get Status Failed!");
ESP_LOGV(TAG, "DHCP Client Status: %d", status);
err = esp_netif_dhcpc_stop(this->eth_netif_);
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STOPPED) {
ESPHL_ERROR_CHECK(err, "DHCPC stop error");
}
err = esp_netif_set_ip_info(this->eth_netif_, &info);
ESPHL_ERROR_CHECK(err, "DHCPC set IP info error");
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
LwIPLock lock;
if (this->manual_ip_->dns1.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns1;
dns_setserver(0, &d);
}
if (this->manual_ip_->dns2.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns2;
dns_setserver(1, &d);
}
} else
#endif
{
err = esp_netif_dhcpc_start(this->eth_netif_);
if (err != ESP_ERR_ESP_NETIF_DHCP_ALREADY_STARTED) {
ESPHL_ERROR_CHECK(err, "DHCPC start error");
}
}
#if USE_NETWORK_IPV6
// Attempt to create IPv6 link-local address
// We MUST attempt this here, not just in finish_connect_(), because with
// min_ipv6_addr_count set, the component won't reach CONNECTED state without IPv6.
// However, this may fail with ESP_FAIL if the interface is not up yet:
// - At bootup when link isn't ready (#10281)
// - After disconnection/cable unplugged (#10705)
// We'll retry in finish_connect_() if it fails here.
err = esp_netif_create_ip6_linklocal(this->eth_netif_);
if (err != ESP_OK) {
if (err == ESP_ERR_ESP_NETIF_INVALID_PARAMS) {
// This is a programming error, not a transient failure
ESPHL_ERROR_CHECK(err, "esp_netif_create_ip6_linklocal invalid parameters");
} else {
// ESP_FAIL means the interface isn't up yet
// This is expected and non-fatal, happens in multiple scenarios:
// - During reconnection after network interruptions (#10705)
// - At bootup when the link isn't ready yet (#10281)
// We'll retry once we reach CONNECTED state and the interface is up
ESP_LOGW(TAG, "esp_netif_create_ip6_linklocal failed: %s", esp_err_to_name(err));
// Don't mark component as failed - this is a transient error
}
}
#endif /* USE_NETWORK_IPV6 */
this->connect_begin_ = millis();
this->status_set_warning();
}
void EthernetComponent::dump_connect_params_() {
esp_netif_ip_info_t ip;
esp_netif_get_ip_info(this->eth_netif_, &ip);
const ip_addr_t *dns_ip1;
const ip_addr_t *dns_ip2;
{
LwIPLock lock;
dns_ip1 = dns_getserver(0);
dns_ip2 = dns_getserver(1);
}
// Use stack buffers for IP address formatting to avoid heap allocations
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
ESP_LOGCONFIG(TAG,
" IP Address: %s\n"
" Hostname: '%s'\n"
" Subnet: %s\n"
" Gateway: %s\n"
" DNS1: %s\n"
" DNS2: %s\n"
" MAC Address: %s\n"
" Is Full Duplex: %s\n"
" Link Speed: %u",
network::IPAddress(&ip.ip).str_to(ip_buf), App.get_name().c_str(),
network::IPAddress(&ip.netmask).str_to(subnet_buf), network::IPAddress(&ip.gw).str_to(gateway_buf),
network::IPAddress(dns_ip1).str_to(dns1_buf), network::IPAddress(dns_ip2).str_to(dns2_buf),
this->get_eth_mac_address_pretty_into_buffer(mac_buf),
YESNO(this->get_duplex_mode() == ETH_DUPLEX_FULL), this->get_link_speed() == ETH_SPEED_100M ? 100 : 10);
#if USE_NETWORK_IPV6
struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES];
uint8_t count = 0;
count = esp_netif_get_all_ip6(this->eth_netif_, if_ip6s);
assert(count <= CONFIG_LWIP_IPV6_NUM_ADDRESSES);
for (int i = 0; i < count; i++) {
ESP_LOGCONFIG(TAG, " IPv6: " IPV6STR, IPV62STR(if_ip6s[i]));
}
#endif /* USE_NETWORK_IPV6 */
}
#ifdef USE_ETHERNET_SPI
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
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_ETHERNET_SPI_POLLING_SUPPORT
void EthernetComponent::set_polling_interval(uint32_t polling_interval) { this->polling_interval_ = polling_interval; }
#endif
#else
void EthernetComponent::set_phy_addr(uint8_t phy_addr) { this->phy_addr_ = phy_addr; }
void EthernetComponent::set_power_pin(int power_pin) { this->power_pin_ = power_pin; }
void EthernetComponent::set_mdc_pin(uint8_t mdc_pin) { this->mdc_pin_ = mdc_pin; }
void EthernetComponent::set_mdio_pin(uint8_t mdio_pin) { this->mdio_pin_ = mdio_pin; }
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
void EthernetComponent::set_clk_mode(emac_rmii_clock_mode_t clk_mode) { this->clk_mode_ = clk_mode; }
void EthernetComponent::add_phy_register(PHYRegister register_value) { this->phy_registers_.push_back(register_value); }
#endif
void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
esp_err_t err;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_MAC_ADDR, mac);
ESPHL_ERROR_CHECK(err, "ETH_CMD_G_MAC error");
}
std::string EthernetComponent::get_eth_mac_address_pretty() {
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
return std::string(this->get_eth_mac_address_pretty_into_buffer(buf));
}
const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
uint8_t mac[6];
get_eth_mac_address_raw(mac);
format_mac_addr_upper(mac, buf.data());
return buf.data();
}
eth_duplex_t EthernetComponent::get_duplex_mode() {
esp_err_t err;
eth_duplex_t duplex_mode;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_DUPLEX_MODE, &duplex_mode);
ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_DUPLEX_MODE error", ETH_DUPLEX_HALF);
return duplex_mode;
}
eth_speed_t EthernetComponent::get_link_speed() {
esp_err_t err;
eth_speed_t speed;
err = esp_eth_ioctl(this->eth_handle_, ETH_CMD_G_SPEED, &speed);
ESPHL_ERROR_CHECK_RET(err, "ETH_CMD_G_SPEED error", ETH_SPEED_10M);
return speed;
}
bool EthernetComponent::powerdown() {
ESP_LOGI(TAG, "Powering down ethernet PHY");
if (this->phy_ == nullptr) {
ESP_LOGE(TAG, "Ethernet PHY not assigned");
return false;
}
this->connected_ = false;
this->started_ = false;
// No need to enable_loop() here as this is only called during shutdown/reboot
if (this->phy_->pwrctl(this->phy_, false) != ESP_OK) {
ESP_LOGE(TAG, "Error powering down ethernet PHY");
return false;
}
return true;
}
#ifndef USE_ETHERNET_SPI
#ifdef USE_ETHERNET_KSZ8081
constexpr uint8_t KSZ80XX_PC2R_REG_ADDR = 0x1F;
void EthernetComponent::ksz8081_set_clock_reference_(esp_eth_mac_t *mac) {
esp_err_t err;
uint32_t phy_control_2;
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
char hex_buf[format_hex_pretty_size(PHY_REG_SIZE)];
#endif
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s", format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
/*
* Bit 7 is `RMII Reference Clock Select`. Default is `0`.
* KSZ8081RNA:
* 0 - clock input to XI (Pin 8) is 25 MHz for RMII - 25 MHz clock mode.
* 1 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode.
* KSZ8081RND:
* 0 - clock input to XI (Pin 8) is 50 MHz for RMII - 50 MHz clock mode.
* 1 - clock input to XI (Pin 8) is 25 MHz (driven clock only, not a crystal) for RMII - 25 MHz clock mode.
*/
if ((phy_control_2 & (1 << 7)) != (1 << 7)) {
phy_control_2 |= 1 << 7;
err = mac->write_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, phy_control_2);
ESPHL_ERROR_CHECK(err, "Write PHY Control 2 failed");
err = mac->read_phy_reg(mac, this->phy_addr_, KSZ80XX_PC2R_REG_ADDR, &(phy_control_2));
ESPHL_ERROR_CHECK(err, "Read PHY Control 2 failed");
ESP_LOGVV(TAG, "KSZ8081 PHY Control 2: %s",
format_hex_pretty_to(hex_buf, (uint8_t *) &phy_control_2, PHY_REG_SIZE));
}
}
#endif // USE_ETHERNET_KSZ8081
void EthernetComponent::write_phy_register_(esp_eth_mac_t *mac, PHYRegister register_data) {
esp_err_t err;
#ifdef USE_ETHERNET_RTL8201
constexpr uint8_t eth_phy_psr_reg_addr = 0x1F;
if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) {
ESP_LOGD(TAG, "Select PHY Register Page: 0x%02" PRIX32, register_data.page);
err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, register_data.page);
ESPHL_ERROR_CHECK(err, "Select PHY Register page failed");
}
#endif
ESP_LOGD(TAG, "Writing PHY reg 0x%02" PRIX32 " = 0x%04" PRIX32, register_data.address, register_data.value);
err = mac->write_phy_reg(mac, this->phy_addr_, register_data.address, register_data.value);
ESPHL_ERROR_CHECK(err, "Writing PHY Register failed");
#ifdef USE_ETHERNET_RTL8201
if (this->type_ == ETHERNET_TYPE_RTL8201 && register_data.page) {
ESP_LOGD(TAG, "Select PHY Register Page 0x00");
err = mac->write_phy_reg(mac, this->phy_addr_, eth_phy_psr_reg_addr, 0x0);
ESPHL_ERROR_CHECK(err, "Select PHY Register Page 0 failed");
}
#endif
}
#endif
} // namespace esphome::ethernet
#endif // USE_ETHERNET && USE_ESP32
@@ -0,0 +1,315 @@
#include "ethernet_component.h"
#if defined(USE_ETHERNET) && defined(USE_RP2040)
#include "esphome/core/application.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/components/rp2040/gpio.h"
#include <SPI.h>
#include <lwip/dns.h>
#include <lwip/netif.h>
namespace esphome::ethernet {
static const char *const TAG = "ethernet";
void EthernetComponent::setup() {
// Configure SPI pins
SPI.setRX(this->miso_pin_);
SPI.setTX(this->mosi_pin_);
SPI.setSCK(this->clk_pin_);
// Toggle reset pin if configured
if (this->reset_pin_ >= 0) {
rp2040::RP2040GPIOPin reset_pin;
reset_pin.set_pin(this->reset_pin_);
reset_pin.set_flags(gpio::FLAG_OUTPUT);
reset_pin.setup();
reset_pin.digital_write(false);
delay(1); // NOLINT
reset_pin.digital_write(true);
delay(10); // NOLINT - wait for W5500 to initialize after reset
}
// Create the W5500 device instance
this->eth_ = new Wiznet5500lwIP(this->cs_pin_, SPI, this->interrupt_pin_); // NOLINT
// Set hostname before begin() so the LWIP netif gets it
this->eth_->hostname(App.get_name().c_str());
// Configure static IP if set (must be done before begin())
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
IPAddress ip(this->manual_ip_->static_ip);
IPAddress gateway(this->manual_ip_->gateway);
IPAddress subnet(this->manual_ip_->subnet);
IPAddress dns1(this->manual_ip_->dns1);
IPAddress dns2(this->manual_ip_->dns2);
this->eth_->config(ip, gateway, subnet, dns1, dns2);
}
#endif
// Begin with fixed MAC or auto-generated
bool success;
if (this->fixed_mac_.has_value()) {
success = this->eth_->begin(this->fixed_mac_->data());
} else {
success = this->eth_->begin();
}
if (!success) {
ESP_LOGE(TAG, "Failed to initialize W5500 Ethernet");
delete this->eth_; // NOLINT(cppcoreguidelines-owning-memory)
this->eth_ = nullptr;
this->mark_failed();
return;
}
// Make this the default interface for routing
this->eth_->setDefault(true);
// The arduino-pico LwipIntfDev automatically handles packet processing
// via __addEthernetPacketHandler when no interrupt pin is used,
// or via GPIO interrupt when one is provided.
// Don't set started_ here — let the link polling in loop() set it
// when the W5500 link is actually up. Setting it prematurely causes
// a "Starting → Stopped → Starting" log sequence because the W5500
// needs time after begin() before the PHY link is ready.
}
void EthernetComponent::loop() {
// On RP2040, we need to poll connection state since there are no events.
const uint32_t now = App.get_loop_component_start_time();
// Throttle link/IP polling to avoid excessive SPI transactions from linkStatus()
// which reads the W5500 PHY register via SPI on every call.
// 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) {
this->last_link_check_ = now;
bool link_up = this->eth_->linkStatus() == LinkON;
bool has_ip = this->eth_->connected();
if (!link_up) {
if (this->started_) {
this->started_ = false;
this->connected_ = false;
}
} else {
if (!this->started_) {
this->started_ = true;
}
bool was_connected = this->connected_;
this->connected_ = has_ip;
if (this->connected_ && !was_connected) {
#ifdef USE_ETHERNET_IP_STATE_LISTENERS
this->notify_ip_state_listeners_();
#endif
}
}
}
// State machine
switch (this->state_) {
case EthernetComponentState::STOPPED:
if (this->started_) {
ESP_LOGI(TAG, "Starting connection");
this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_();
}
break;
case EthernetComponentState::CONNECTING:
if (!this->started_) {
ESP_LOGI(TAG, "Stopped connection");
this->state_ = EthernetComponentState::STOPPED;
} else if (this->connected_) {
// connection established
ESP_LOGI(TAG, "Connected");
this->state_ = EthernetComponentState::CONNECTED;
this->dump_connect_params_();
this->status_clear_warning();
#ifdef USE_ETHERNET_CONNECT_TRIGGER
this->connect_trigger_.trigger();
#endif
} else if (now - this->connect_begin_ > 15000) {
ESP_LOGW(TAG, "Connecting failed; reconnecting");
this->start_connect_();
}
break;
case EthernetComponentState::CONNECTED:
if (!this->started_) {
ESP_LOGI(TAG, "Stopped connection");
this->state_ = EthernetComponentState::STOPPED;
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
this->disconnect_trigger_.trigger();
#endif
} else if (!this->connected_) {
ESP_LOGW(TAG, "Connection lost; reconnecting");
this->state_ = EthernetComponentState::CONNECTING;
this->start_connect_();
#ifdef USE_ETHERNET_DISCONNECT_TRIGGER
this->disconnect_trigger_.trigger();
#endif
} else {
this->finish_connect_();
}
break;
}
}
void EthernetComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"Ethernet:\n"
" Type: W5500\n"
" Connected: %s\n"
" CLK Pin: %u\n"
" MISO Pin: %u\n"
" MOSI Pin: %u\n"
" CS Pin: %u\n"
" IRQ Pin: %d\n"
" Reset Pin: %d",
YESNO(this->is_connected()), this->clk_pin_, this->miso_pin_, this->mosi_pin_, this->cs_pin_,
this->interrupt_pin_, this->reset_pin_);
this->dump_connect_params_();
}
network::IPAddresses EthernetComponent::get_ip_addresses() {
network::IPAddresses addresses;
if (this->eth_ != nullptr) {
LwIPLock lock;
addresses[0] = network::IPAddress(this->eth_->localIP());
}
return addresses;
}
network::IPAddress EthernetComponent::get_dns_address(uint8_t num) {
LwIPLock lock;
const ip_addr_t *dns_ip = dns_getserver(num);
return dns_ip;
}
void EthernetComponent::get_eth_mac_address_raw(uint8_t *mac) {
if (this->eth_ != nullptr) {
this->eth_->macAddress(mac);
} else {
memset(mac, 0, 6);
}
}
std::string EthernetComponent::get_eth_mac_address_pretty() {
char buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
return std::string(this->get_eth_mac_address_pretty_into_buffer(buf));
}
const char *EthernetComponent::get_eth_mac_address_pretty_into_buffer(
std::span<char, MAC_ADDRESS_PRETTY_BUFFER_SIZE> buf) {
uint8_t mac[6];
get_eth_mac_address_raw(mac);
format_mac_addr_upper(mac, buf.data());
return buf.data();
}
eth_duplex_t EthernetComponent::get_duplex_mode() {
// W5500 is always full duplex
return ETH_DUPLEX_FULL;
}
eth_speed_t EthernetComponent::get_link_speed() {
// W5500 is always 100Mbps
return ETH_SPEED_100M;
}
bool EthernetComponent::powerdown() {
ESP_LOGI(TAG, "Powering down ethernet");
if (this->eth_ != nullptr) {
this->eth_->end();
}
this->connected_ = false;
this->started_ = false;
return true;
}
void EthernetComponent::start_connect_() {
this->got_ipv4_address_ = false;
this->connect_begin_ = millis();
this->status_set_warning(LOG_STR("waiting for IP configuration"));
// Hostname is already set in setup() via LwipIntf::setHostname()
#ifdef USE_ETHERNET_MANUAL_IP
if (this->manual_ip_.has_value()) {
// Static IP was already configured before begin() in setup()
// Set DNS servers
LwIPLock lock;
if (this->manual_ip_->dns1.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns1;
dns_setserver(0, &d);
}
if (this->manual_ip_->dns2.is_set()) {
ip_addr_t d;
d = this->manual_ip_->dns2;
dns_setserver(1, &d);
}
}
#endif
}
void EthernetComponent::finish_connect_() {
// No additional work needed on RP2040 for now
// IPv6 link-local could be added here in the future
}
void EthernetComponent::dump_connect_params_() {
if (this->eth_ == nullptr) {
return;
}
char ip_buf[network::IP_ADDRESS_BUFFER_SIZE];
char subnet_buf[network::IP_ADDRESS_BUFFER_SIZE];
char gateway_buf[network::IP_ADDRESS_BUFFER_SIZE];
char dns1_buf[network::IP_ADDRESS_BUFFER_SIZE];
char dns2_buf[network::IP_ADDRESS_BUFFER_SIZE];
char mac_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE];
// Copy all lwIP state under the lock to avoid races with IRQ callbacks
ip_addr_t ip_addr, netmask, gw, dns1_addr, dns2_addr;
{
LwIPLock lock;
auto *netif = this->eth_->getNetIf();
ip_addr = netif->ip_addr;
netmask = netif->netmask;
gw = netif->gw;
dns1_addr = *dns_getserver(0);
dns2_addr = *dns_getserver(1);
}
ESP_LOGCONFIG(TAG,
" IP Address: %s\n"
" Hostname: '%s'\n"
" Subnet: %s\n"
" Gateway: %s\n"
" DNS1: %s\n"
" DNS2: %s\n"
" MAC Address: %s",
network::IPAddress(&ip_addr).str_to(ip_buf), App.get_name().c_str(),
network::IPAddress(&netmask).str_to(subnet_buf), network::IPAddress(&gw).str_to(gateway_buf),
network::IPAddress(&dns1_addr).str_to(dns1_buf), network::IPAddress(&dns2_addr).str_to(dns2_buf),
this->get_eth_mac_address_pretty_into_buffer(mac_buf));
}
void EthernetComponent::set_clk_pin(uint8_t clk_pin) { this->clk_pin_ = clk_pin; }
void EthernetComponent::set_miso_pin(uint8_t miso_pin) { this->miso_pin_ = miso_pin; }
void EthernetComponent::set_mosi_pin(uint8_t mosi_pin) { this->mosi_pin_ = mosi_pin; }
void EthernetComponent::set_cs_pin(uint8_t cs_pin) { this->cs_pin_ = cs_pin; }
void EthernetComponent::set_interrupt_pin(int8_t interrupt_pin) { this->interrupt_pin_ = interrupt_pin; }
void EthernetComponent::set_reset_pin(int8_t reset_pin) { this->reset_pin_ = reset_pin; }
} // namespace esphome::ethernet
#endif // USE_ETHERNET && USE_RP2040
@@ -1,3 +1,5 @@
#include "esphome/core/defines.h"
#ifdef USE_ESP32
#include "esp_eth_mac_esp.h"
// ETH_ESP32_EMAC_DEFAULT_CONFIG() uses out-of-order designated initializers
@@ -8,3 +10,4 @@ eth_esp32_emac_config_t eth_esp32_emac_default_config(void) {
return (eth_esp32_emac_config_t) ETH_ESP32_EMAC_DEFAULT_CONFIG();
}
#endif
#endif // USE_ESP32
@@ -1,7 +1,7 @@
#include "ethernet_info_text_sensor.h"
#include "esphome/core/log.h"
#ifdef USE_ESP32
#ifdef USE_ETHERNET
namespace esphome::ethernet_info {
@@ -49,4 +49,4 @@ void MACAddressEthernetInfo::dump_config() { LOG_TEXT_SENSOR("", "EthernetInfo M
} // namespace esphome::ethernet_info
#endif // USE_ESP32
#endif // USE_ETHERNET
@@ -4,7 +4,7 @@
#include "esphome/components/text_sensor/text_sensor.h"
#include "esphome/components/ethernet/ethernet_component.h"
#ifdef USE_ESP32
#ifdef USE_ETHERNET
namespace esphome::ethernet_info {
@@ -50,4 +50,4 @@ class MACAddressEthernetInfo final : public Component, public text_sensor::TextS
} // namespace esphome::ethernet_info
#endif // USE_ESP32
#endif // USE_ETHERNET
@@ -7,6 +7,7 @@ from esphome.const import (
CONF_OUTPUT_ID,
CONF_RGB_ORDER,
)
from esphome.core import CORE
CODEOWNERS = ["@OttoWinter"]
fastled_base_ns = cg.esphome_ns.namespace("fastled_base")
@@ -41,5 +42,9 @@ async def new_fastled_light(config):
cg.add(var.set_max_refresh_rate(config[CONF_MAX_REFRESH_RATE]))
cg.add_library("fastled/FastLED", "3.9.16")
if CORE.is_esp32:
from esphome.components.esp32 import include_builtin_idf_component
include_builtin_idf_component("esp_lcd")
await light.register_light(var, config)
return var
@@ -39,7 +39,7 @@ class GPIOBinarySensorStore {
Component *component_{nullptr}; // Pointer to the component for enable_loop_soon_any_context()
};
class GPIOBinarySensor : public binary_sensor::BinarySensor, public Component {
class GPIOBinarySensor final : public binary_sensor::BinarySensor, public Component {
public:
// No destructor needed: ESPHome components are created at boot and live forever.
// Interrupts are only detached on reboot when memory is cleared anyway.
@@ -131,7 +131,7 @@ uint8_t IRAM_ATTR GPIOOneWireBus::read8() {
uint64_t IRAM_ATTR GPIOOneWireBus::read64() {
InterruptLock lock;
uint64_t ret = 0;
for (uint8_t i = 0; i < 8; i++) {
for (uint8_t i = 0; i < 64; i++) {
ret |= (uint64_t(this->read_bit_()) << i);
}
return ret;
+1 -1
View File
@@ -8,7 +8,7 @@
namespace esphome {
namespace gpio {
class GPIOSwitch : public switch_::Switch, public Component {
class GPIOSwitch final : public switch_::Switch, public Component {
public:
void set_pin(GPIOPin *pin) { pin_ = pin; }
@@ -7,7 +7,7 @@
namespace esphome {
namespace homeassistant {
class HomeassistantTime : public time::RealTimeClock {
class HomeassistantTime final : public time::RealTimeClock {
public:
void setup() override;
void update() override;
+1 -1
View File
@@ -587,7 +587,7 @@ def _build_config_struct(
async def to_code(config: ConfigType) -> None:
add_idf_component(
name="esphome/esp-hub75",
ref="0.3.2",
ref="0.3.4",
)
# Set compile-time configuration via build flags (so external library sees them)
+1 -1
View File
@@ -185,7 +185,7 @@ ErrorCode IDFI2CBus::write_readv(uint8_t address, const uint8_t *write_buffer, s
jobs[num_jobs++].command = I2C_MASTER_CMD_STOP;
ESP_LOGV(TAG, "Sending %zu jobs", num_jobs);
esp_err_t err = i2c_master_execute_defined_operations(this->dev_, jobs, num_jobs, 100);
if (err == ESP_ERR_INVALID_STATE) {
if (err == ESP_ERR_INVALID_STATE || err == ESP_ERR_INVALID_RESPONSE) {
ESP_LOGV(TAG, "TX to %02X failed: not acked", address);
return ERROR_NOT_ACKNOWLEDGED;
} else if (err == ESP_ERR_TIMEOUT) {
+70
View File
@@ -1,3 +1,5 @@
from dataclasses import dataclass, field
from esphome import pins
import esphome.codegen as cg
from esphome.components.esp32 import (
@@ -26,6 +28,9 @@ CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["esp32"]
MULTI_CONF = True
CONF_PDM = "pdm"
CONF_ADC_TYPE = "adc_type"
CONF_I2S_DOUT_PIN = "i2s_dout_pin"
CONF_I2S_DIN_PIN = "i2s_din_pin"
CONF_I2S_MCLK_PIN = "i2s_mclk_pin"
@@ -254,7 +259,65 @@ CONFIG_SCHEMA = cv.All(
)
@dataclass
class I2SAudioData:
"""I2S audio component state stored in CORE.data."""
port_map: dict[str, int] = field(default_factory=dict)
def _get_data() -> I2SAudioData:
if CONF_I2S_AUDIO not in CORE.data:
CORE.data[CONF_I2S_AUDIO] = I2SAudioData()
return CORE.data[CONF_I2S_AUDIO]
def _assign_ports() -> None:
"""Assign I2S port numbers, prioritizing instances with microphone children.
Microphones (especially PDM) require port 0 on most ESP32 variants.
This runs once and stores the mapping in CORE.data.
"""
data = _get_data()
if data.port_map:
return
full_config = fv.full_config.get()
i2s_configs = full_config[CONF_I2S_AUDIO]
# Find i2s_audio instances with microphones that require port 0
# (PDM and internal ADC only work on I2S port 0)
port0_parent_id = None
for mic_config in full_config.get("microphone", []):
if CONF_I2S_AUDIO_ID not in mic_config:
continue
if mic_config.get(CONF_PDM) or mic_config.get(CONF_ADC_TYPE) == "internal":
if port0_parent_id is not None:
raise cv.Invalid(
"Only one PDM/ADC microphone is supported (requires I2S port 0)"
)
port0_parent_id = str(mic_config[CONF_I2S_AUDIO_ID])
# Assign ports: port 0 parent first (if any), rest get sequential
next_port = 0
if port0_parent_id is not None:
data.port_map[port0_parent_id] = next_port
next_port += 1
for config in i2s_configs:
config_id = str(config[CONF_ID])
if config_id != port0_parent_id:
data.port_map[config_id] = next_port
next_port += 1
def _final_validate(_):
from esphome.components.esp32 import idf_version
if use_legacy() and idf_version() >= cv.Version(6, 0, 0):
raise cv.Invalid(
"The legacy I2S driver is not available in ESP-IDF 6.0+. "
"Set 'use_legacy: false' in i2s_audio configuration."
)
i2s_audio_configs = fv.full_config.get()[CONF_I2S_AUDIO]
variant = get_esp32_variant()
if variant not in I2S_PORTS:
@@ -263,6 +326,7 @@ def _final_validate(_):
raise cv.Invalid(
f"Only {I2S_PORTS[variant]} I2S audio ports are supported on {variant}"
)
_assign_ports()
def use_legacy():
@@ -276,6 +340,12 @@ async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
# Assign I2S port from _final_validate computed mapping
data = _get_data()
if (port := data.port_map.get(str(config[CONF_ID]))) is None:
raise ValueError(f"No I2S port assigned for {config[CONF_ID]}")
cg.add(var.set_port(port))
# Re-enable ESP-IDF's I2S driver (excluded by default to save compile time)
include_builtin_idf_component("esp_driver_i2s")
@@ -1,27 +0,0 @@
#include "i2s_audio.h"
#ifdef USE_ESP32
#include "esphome/core/log.h"
namespace esphome {
namespace i2s_audio {
static const char *const TAG = "i2s_audio";
void I2SAudioComponent::setup() {
static i2s_port_t next_port_num = I2S_NUM_0;
if (next_port_num >= SOC_I2S_NUM) {
ESP_LOGE(TAG, "Too many components");
this->mark_failed();
return;
}
this->port_ = next_port_num;
next_port_num = (i2s_port_t) (next_port_num + 1);
}
} // namespace i2s_audio
} // namespace esphome
#endif // USE_ESP32
+8 -5
View File
@@ -5,6 +5,7 @@
#include "esphome/core/component.h"
#include "esphome/core/defines.h"
#include "esphome/core/helpers.h"
#include <esp_idf_version.h>
#ifdef USE_I2S_LEGACY
#include <driver/i2s.h>
#else
@@ -56,8 +57,6 @@ class I2SAudioOut : public I2SAudioBase {};
class I2SAudioComponent : public Component {
public:
void setup() override;
#ifdef USE_I2S_LEGACY
i2s_pin_config_t get_pin_config() const {
return {
@@ -86,13 +85,17 @@ class I2SAudioComponent : public Component {
void set_mclk_pin(int pin) { this->mclk_pin_ = pin; }
void set_bclk_pin(int pin) { this->bclk_pin_ = pin; }
void set_lrclk_pin(int pin) { this->lrclk_pin_ = pin; }
void set_port(int port) { this->port_ = port; }
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(6, 0, 0)
int get_port() const { return this->port_; }
#else
i2s_port_t get_port() const { return static_cast<i2s_port_t>(this->port_); }
#endif
void lock() { this->lock_.lock(); }
bool try_lock() { return this->lock_.try_lock(); }
void unlock() { this->lock_.unlock(); }
i2s_port_t get_port() const { return this->port_; }
protected:
Mutex lock_;
@@ -106,7 +109,7 @@ class I2SAudioComponent : public Component {
int bclk_pin_{I2S_GPIO_UNUSED};
#endif
int lrclk_pin_;
i2s_port_t port_{};
int port_{};
};
} // namespace i2s_audio
@@ -13,9 +13,11 @@ from esphome.const import (
)
from .. import (
CONF_ADC_TYPE,
CONF_I2S_DIN_PIN,
CONF_LEFT,
CONF_MONO,
CONF_PDM,
CONF_RIGHT,
I2SAudioIn,
i2s_audio_component_schema,
@@ -29,9 +31,7 @@ CODEOWNERS = ["@jesserockz"]
DEPENDENCIES = ["i2s_audio"]
CONF_ADC_PIN = "adc_pin"
CONF_ADC_TYPE = "adc_type"
CONF_CORRECT_DC_OFFSET = "correct_dc_offset"
CONF_PDM = "pdm"
I2SAudioMicrophone = i2s_audio_ns.class_(
"I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component
@@ -37,27 +37,6 @@ enum MicrophoneEventGroupBits : uint32_t {
};
void I2SAudioMicrophone::setup() {
#ifdef USE_I2S_LEGACY
#if SOC_I2S_SUPPORTS_ADC
if (this->adc_) {
if (this->parent_->get_port() != I2S_NUM_0) {
ESP_LOGE(TAG, "Internal ADC only works on I2S0");
this->mark_failed();
return;
}
} else
#endif
#endif
{
if (this->pdm_) {
if (this->parent_->get_port() != I2S_NUM_0) {
ESP_LOGE(TAG, "PDM only works on I2S0");
this->mark_failed();
return;
}
}
}
this->active_listeners_semaphore_ = xSemaphoreCreateCounting(MAX_LISTENERS, MAX_LISTENERS);
if (this->active_listeners_semaphore_ == nullptr) {
ESP_LOGE(TAG, "Creating semaphore failed");
+2 -1
View File
@@ -3,6 +3,7 @@
#ifdef USE_ESP32
#include <driver/gpio.h>
#include <driver/ledc.h>
#include <cinttypes>
#include <esp_idf_version.h>
@@ -189,7 +190,7 @@ void LEDCOutput::setup() {
this->phase_angle_, hpoint);
ledc_channel_config_t chan_conf{};
chan_conf.gpio_num = this->pin_->get_pin();
chan_conf.gpio_num = static_cast<gpio_num_t>(this->pin_->get_pin());
chan_conf.speed_mode = speed_mode;
chan_conf.channel = chan_num;
chan_conf.intr_type = LEDC_INTR_DISABLE;
-6
View File
@@ -15,12 +15,6 @@ bool random_bytes(uint8_t *data, size_t len) {
return true;
}
Mutex::Mutex() { handle_ = xSemaphoreCreateMutex(); }
Mutex::~Mutex() {}
void Mutex::lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); }
bool Mutex::try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; }
void Mutex::unlock() { xSemaphoreGive(this->handle_); }
// only affects the executing core
// so should not be used as a mutex lock, only to get accurate timing
IRAM_ATTR InterruptLock::InterruptLock() { portDISABLE_INTERRUPTS(); }
@@ -42,7 +42,7 @@ void LilygoT547Touchscreen::setup() {
this->x_raw_max_ = this->display_->get_native_width();
}
if (this->y_raw_max_ == this->y_raw_min_) {
this->x_raw_max_ = this->display_->get_native_height();
this->y_raw_max_ = this->display_->get_native_height();
}
}
}
@@ -64,6 +64,10 @@ void LilygoT547Touchscreen::update_touches() {
}
point = buffer[5] & 0xF;
if (point > 2) {
ESP_LOGW(TAG, "Invalid touch point count: %d", point);
point = 2;
}
if (point == 1) {
err = this->write_register(TOUCH_REGISTER, READ_TOUCH, 1);
+19 -12
View File
@@ -111,7 +111,12 @@ struct LogBuffer {
}
#endif
void write_body(const char *text, uint16_t text_length) {
this->write_(text, text_length);
const uint16_t available = this->remaining_();
const uint16_t copy_len = (text_length < available) ? text_length : available;
if (copy_len > 0) {
memcpy(this->current_(), text, copy_len);
this->pos += copy_len;
}
this->finalize_();
}
@@ -119,21 +124,23 @@ struct LogBuffer {
bool full_() const { return this->pos >= this->size; }
uint16_t remaining_() const { return this->size - this->pos; }
char *current_() { return this->data + this->pos; }
void write_(const char *value, uint16_t length) {
const uint16_t available = this->remaining_();
const uint16_t copy_len = (length < available) ? length : available;
if (copy_len > 0) {
memcpy(this->current_(), value, copy_len);
this->pos += copy_len;
}
}
void finalize_() {
// Write color reset sequence
static constexpr uint16_t RESET_COLOR_LEN = sizeof(ESPHOME_LOG_RESET_COLOR) - 1;
this->write_(ESPHOME_LOG_RESET_COLOR, RESET_COLOR_LEN);
this->write_ansi_reset_();
// Null terminate
this->data[this->full_() ? this->size - 1 : this->pos] = '\0';
}
// Write ANSI reset sequence inline ("\033[0m") - avoids write_() call overhead
static constexpr uint16_t ANSI_RESET_LEN = 4; // "\033[0m"
void write_ansi_reset_() {
if (this->remaining_() >= ANSI_RESET_LEN) {
char *p = this->current_();
*p++ = '\033';
*p++ = '[';
*p++ = '0';
*p++ = 'm';
this->pos += ANSI_RESET_LEN;
}
}
void strip_trailing_newlines_() {
while (this->pos > 0 && this->data[this->pos - 1] == '\n')
this->pos--;
+17 -1
View File
@@ -233,7 +233,11 @@ class Logger final : public Component {
void cdc_loop_();
#endif
void process_messages_();
#if defined(USE_HOST) || defined(USE_ZEPHYR)
void write_msg_(const char *msg, uint16_t len);
#else
inline void write_msg_(const char *msg, uint16_t len); // Defined in platform-specific logger_*.h
#endif
// Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator
// thread_name: name of the calling thread/task, or nullptr for main task (callers already know which task they're on)
@@ -366,7 +370,7 @@ class Logger final : public Component {
bool non_main_task_recursion_guard_{false}; // Shared guard for all non-main tasks on LibreTiny
#endif
#else
bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms
bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms
#endif
// Large buffer placed last to keep frequently-accessed member offsets small
@@ -498,3 +502,15 @@ class LoggerMessageTrigger final : public Trigger<uint8_t, const char *, const c
};
} // namespace esphome::logger
// Platform-specific inline implementations of write_msg_()
// Must be included after the Logger class definition is complete
#if defined(USE_ESP32)
#include "logger_esp32.h"
#elif defined(USE_ESP8266)
#include "logger_esp8266.h"
#elif defined(USE_RP2040)
#include "logger_rp2040.h"
#elif defined(USE_LIBRETINY)
#include "logger_libretiny.h"
#endif
@@ -123,23 +123,6 @@ void Logger::pre_setup() {
#endif
}
void HOT Logger::write_msg_(const char *msg, uint16_t len) {
#if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG)
// USB CDC/JTAG - single write including newline (already in buffer)
// Use fwrite to stdout which goes through VFS to USB console
//
// Note: These defines indicate the user's YAML configuration choice (hardware_uart: USB_CDC/USB_SERIAL_JTAG).
// They are ONLY defined when the user explicitly selects USB as the logger output in their config.
// This is compile-time selection, not runtime detection - if USB is configured, it's always used.
// There is no fallback to regular UART if "USB isn't connected" - that's the user's responsibility
// to configure correctly for their hardware. This approach eliminates runtime overhead.
fwrite(msg, 1, len, stdout);
#else
// Regular UART - single write including newline (already in buffer)
uart_write_bytes(this->uart_num_, msg, len);
#endif
}
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_UART0:
+28
View File
@@ -0,0 +1,28 @@
#pragma once
#ifdef USE_ESP32
#include "esphome/core/helpers.h"
#include <driver/uart.h>
namespace esphome::logger {
inline void HOT Logger::write_msg_(const char *msg, uint16_t len) {
#if defined(USE_LOGGER_UART_SELECTION_USB_CDC) || defined(USE_LOGGER_UART_SELECTION_USB_SERIAL_JTAG)
// USB CDC/JTAG - single write including newline (already in buffer)
// Use fwrite to stdout which goes through VFS to USB console
//
// Note: These defines indicate the user's YAML configuration choice (hardware_uart: USB_CDC/USB_SERIAL_JTAG).
// They are ONLY defined when the user explicitly selects USB as the logger output in their config.
// This is compile-time selection, not runtime detection - if USB is configured, it's always used.
// There is no fallback to regular UART if "USB isn't connected" - that's the user's responsibility
// to configure correctly for their hardware. This approach eliminates runtime overhead.
fwrite(msg, 1, len, stdout);
#else
// Regular UART - single write including newline (already in buffer)
uart_write_bytes(this->uart_num_, msg, len);
#endif
}
} // namespace esphome::logger
#endif
@@ -28,11 +28,6 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg, uint16_t len) {
// Single write with newline already in buffer (added by caller)
this->hw_serial_->write(msg, len);
}
const LogString *Logger::get_uart_selection_() {
#if defined(USE_ESP8266_LOGGER_SERIAL)
if (this->uart_ == UART_SELECTION_UART0_SWAP) {
@@ -0,0 +1,13 @@
#pragma once
#ifdef USE_ESP8266
#include "esphome/core/helpers.h"
namespace esphome::logger {
// Single write with newline already in buffer (added by caller)
inline void HOT Logger::write_msg_(const char *msg, uint16_t len) { this->hw_serial_->write(msg, len); }
} // namespace esphome::logger
#endif
@@ -49,8 +49,6 @@ void Logger::pre_setup() {
ESP_LOGI(TAG, "Log initialized");
}
void HOT Logger::write_msg_(const char *msg, uint16_t len) { this->hw_serial_->write(msg, len); }
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_DEFAULT:
@@ -0,0 +1,13 @@
#pragma once
#ifdef USE_LIBRETINY
#include "esphome/core/helpers.h"
namespace esphome::logger {
// Single write with newline already in buffer (added by caller)
inline void HOT Logger::write_msg_(const char *msg, uint16_t len) { this->hw_serial_->write(msg, len); }
} // namespace esphome::logger
#endif
@@ -34,11 +34,6 @@ void Logger::pre_setup() {
#endif
}
void HOT Logger::write_msg_(const char *msg, uint16_t len) {
// Single write with newline already in buffer (added by caller)
this->hw_serial_->write(msg, len);
}
const LogString *Logger::get_uart_selection_() {
switch (this->uart_) {
case UART_SELECTION_UART0:
+13
View File
@@ -0,0 +1,13 @@
#pragma once
#ifdef USE_RP2040
#include "esphome/core/helpers.h"
namespace esphome::logger {
// Single write with newline already in buffer (added by caller)
inline void HOT Logger::write_msg_(const char *msg, uint16_t len) { this->hw_serial_->write(msg, len); }
} // namespace esphome::logger
#endif
+1 -1
View File
@@ -32,7 +32,7 @@
namespace esphome {
namespace md5 {
class MD5Digest : public HashBase {
class MD5Digest final : public HashBase {
public:
MD5Digest() = default;
~MD5Digest() override;
+9 -8
View File
@@ -4,7 +4,8 @@
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esp_lcd_panel_rgb.h"
#include <driver/gpio.h>
#include <esp_lcd_panel_rgb.h>
#include <span>
namespace esphome {
@@ -153,18 +154,18 @@ void MipiRgb::common_setup_() {
config.clk_src = LCD_CLK_SRC_PLL160M;
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
for (size_t i = 0; i != data_pin_count; i++) {
config.data_gpio_nums[i] = this->data_pins_[i]->get_pin();
config.data_gpio_nums[i] = static_cast<gpio_num_t>(this->data_pins_[i]->get_pin());
}
config.data_width = data_pin_count;
config.disp_gpio_num = -1;
config.hsync_gpio_num = this->hsync_pin_->get_pin();
config.vsync_gpio_num = this->vsync_pin_->get_pin();
config.disp_gpio_num = GPIO_NUM_NC;
config.hsync_gpio_num = static_cast<gpio_num_t>(this->hsync_pin_->get_pin());
config.vsync_gpio_num = static_cast<gpio_num_t>(this->vsync_pin_->get_pin());
if (this->de_pin_) {
config.de_gpio_num = this->de_pin_->get_pin();
config.de_gpio_num = static_cast<gpio_num_t>(this->de_pin_->get_pin());
} else {
config.de_gpio_num = -1;
config.de_gpio_num = GPIO_NUM_NC;
}
config.pclk_gpio_num = this->pclk_pin_->get_pin();
config.pclk_gpio_num = static_cast<gpio_num_t>(this->pclk_pin_->get_pin());
esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_);
if (err == ESP_OK)
err = esp_lcd_panel_reset(this->handle_);
+7 -1
View File
@@ -3,7 +3,9 @@ from esphome.automation import Condition
import esphome.codegen as cg
from esphome.components import logger, socket
from esphome.components.esp32 import (
add_idf_component,
add_idf_sdkconfig_option,
idf_version,
include_builtin_idf_component,
)
from esphome.config_helpers import filter_source_files_from_platform
@@ -351,7 +353,11 @@ async def to_code(config):
if CORE.is_esp32:
socket.require_wake_loop_threadsafe()
# Re-enable ESP-IDF's mqtt component (excluded by default to save compile time)
include_builtin_idf_component("mqtt")
# IDF 6.0 moved esp-mqtt to an external component
if idf_version() >= cv.Version(6, 0, 0):
add_idf_component(name="espressif/mqtt", ref="1.0.0")
else:
include_builtin_idf_component("mqtt")
cg.add_define("USE_MQTT")
cg.add_global(mqtt_ns.using)
+1 -1
View File
@@ -6,7 +6,7 @@
namespace esphome {
namespace preferences {
class IntervalSyncer : public Component {
class IntervalSyncer final : public Component {
public:
void set_write_interval(uint32_t write_interval) { this->write_interval_ = write_interval; }
void setup() override {
@@ -2,6 +2,7 @@
#include "esphome/core/log.h"
#ifdef HAS_PCNT
#include <driver/gpio.h>
#include <esp_private/esp_clk.h>
#include <hal/pcnt_ll.h>
#endif
@@ -76,8 +77,8 @@ bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) {
}
pcnt_chan_config_t chan_config = {
.edge_gpio_num = this->pin->get_pin(),
.level_gpio_num = -1,
.edge_gpio_num = static_cast<gpio_num_t>(this->pin->get_pin()),
.level_gpio_num = GPIO_NUM_NC,
};
error = pcnt_new_channel(this->pcnt_unit, &chan_config, &this->pcnt_channel);
if (error != ESP_OK) {
@@ -6,7 +6,7 @@
namespace esphome {
namespace restart {
class RestartButton : public button::Button, public Component {
class RestartButton final : public button::Button, public Component {
public:
void dump_config() override;
+18 -12
View File
@@ -8,6 +8,8 @@
#if defined(USE_WIFI)
#include <WiFi.h>
#include <pico/cyw43_arch.h> // For cyw43_arch_lwip_begin/end (LwIPLock)
#elif defined(USE_ETHERNET)
#include <LwipEthernet.h> // For ethernet_arch_lwip_begin/end (LwIPLock)
#endif
#include <hardware/structs/rosc.h>
#include <hardware/sync.h>
@@ -35,28 +37,32 @@ bool random_bytes(uint8_t *data, size_t len) {
return true;
}
// RP2040 doesn't have mutexes, but that shouldn't be an issue as it's single-core and non-preemptive OS.
Mutex::Mutex() {}
Mutex::~Mutex() {}
void Mutex::lock() {}
bool Mutex::try_lock() { return true; }
void Mutex::unlock() {}
// RP2040 Mutex is defined inline in helpers.h for RP2040/ESP8266 builds.
IRAM_ATTR InterruptLock::InterruptLock() { state_ = save_and_disable_interrupts(); }
IRAM_ATTR InterruptLock::~InterruptLock() { restore_interrupts(state_); }
// On RP2040 (Pico W), arduino-pico sets PICO_CYW43_ARCH_THREADSAFE_BACKGROUND=1.
// This means lwip callbacks run from a low-priority user IRQ context, not the
// On RP2040, lwip callbacks run from a low-priority user IRQ context, not the
// main loop (see low_priority_irq_handler() in pico-sdk
// async_context_threadsafe_background.c). cyw43_arch_lwip_begin/end acquires the
// async_context recursive mutex to prevent IRQ callbacks from firing during
// critical sections. See esphome#10681.
// async_context_threadsafe_background.c). This applies to both WiFi (CYW43) and
// Ethernet (W5500) — both use async_context_threadsafe_background.
//
// When CYW43 is not available (non-WiFi RP2040 boards), this is a no-op since
// Without locking, recv_fn() from IRQ context races with read_locked_() on the
// main loop, corrupting the shared rx_buf_ pbuf chain (use-after-free, pbuf_cat
// assertion failures). See esphome#10681.
//
// WiFi uses cyw43_arch_lwip_begin/end; Ethernet uses ethernet_arch_lwip_begin/end.
// Both acquire the async_context recursive mutex to prevent IRQ callbacks from
// firing during critical sections.
//
// When neither WiFi nor Ethernet is configured, this is a no-op since
// there's no network stack and no lwip callbacks to race with.
#if defined(USE_WIFI)
LwIPLock::LwIPLock() { cyw43_arch_lwip_begin(); }
LwIPLock::~LwIPLock() { cyw43_arch_lwip_end(); }
#elif defined(USE_ETHERNET)
LwIPLock::LwIPLock() { ethernet_arch_lwip_begin(); }
LwIPLock::~LwIPLock() { ethernet_arch_lwip_end(); }
#else
LwIPLock::LwIPLock() {}
LwIPLock::~LwIPLock() {}
@@ -2,6 +2,7 @@
#include "rpi_dpi_rgb.h"
#include "esphome/core/gpio.h"
#include "esphome/core/log.h"
#include <driver/gpio.h>
namespace esphome {
namespace rpi_dpi_rgb {
@@ -25,14 +26,14 @@ void RpiDpiRgb::setup() {
config.clk_src = LCD_CLK_SRC_PLL160M;
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
for (size_t i = 0; i != data_pin_count; i++) {
config.data_gpio_nums[i] = this->data_pins_[i]->get_pin();
config.data_gpio_nums[i] = static_cast<gpio_num_t>(this->data_pins_[i]->get_pin());
}
config.data_width = data_pin_count;
config.disp_gpio_num = -1;
config.hsync_gpio_num = this->hsync_pin_->get_pin();
config.vsync_gpio_num = this->vsync_pin_->get_pin();
config.de_gpio_num = this->de_pin_->get_pin();
config.pclk_gpio_num = this->pclk_pin_->get_pin();
config.disp_gpio_num = GPIO_NUM_NC;
config.hsync_gpio_num = static_cast<gpio_num_t>(this->hsync_pin_->get_pin());
config.vsync_gpio_num = static_cast<gpio_num_t>(this->vsync_pin_->get_pin());
config.de_gpio_num = static_cast<gpio_num_t>(this->de_pin_->get_pin());
config.pclk_gpio_num = static_cast<gpio_num_t>(this->pclk_pin_->get_pin());
esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_);
if (err != ESP_OK) {
ESP_LOGE(TAG, "lcd_new_rgb_panel failed: %s", esp_err_to_name(err));
@@ -11,6 +11,7 @@ from esphome.components.image import (
)
import esphome.config_validation as cv
from esphome.const import CONF_FORMAT, CONF_ID, CONF_RESIZE, CONF_TYPE
from esphome.core import CORE
AUTO_LOAD = ["image"]
CODEOWNERS = ["@guillempages", "@clydebarrow", "@kahrendt"]
@@ -75,6 +76,13 @@ class JPEGFormat(Format):
def actions(self) -> None:
cg.add_define("USE_RUNTIME_IMAGE_JPEG")
cg.add_library("JPEGDEC", "1.8.4", "https://github.com/bitbank2/JPEGDEC#1.8.4")
if CORE.is_esp32:
from esphome.components.esp32 import add_idf_component
# JPEGDEC uses ESP32-S3 SIMD optimizations (guarded by board-level
# ARDUINO_ESP32S3_DEV define) that require esp-dsp headers.
# On Arduino this overwrites the stub; on IDF it adds the component.
add_idf_component(name="espressif/esp-dsp", ref="1.7.1")
class PNGFormat(Format):
+3 -3
View File
@@ -68,7 +68,7 @@ void RX8130Component::dump_config() {
void RX8130Component::read_time() {
uint8_t date[7];
if (this->read_register(RX8130_REG_SEC, date, 7) != i2c::ERROR_OK) {
this->status_set_warning(ESP_LOG_MSG_COMM_FAIL);
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
ESPTime rtc_time{
@@ -109,7 +109,7 @@ void RX8130Component::write_time() {
buff[6] = dec2bcd(now.year % 100);
this->stop_(true);
if (this->write_register(RX8130_REG_SEC, buff, 7) != i2c::ERROR_OK) {
this->status_set_warning(ESP_LOG_MSG_COMM_FAIL);
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
} else {
ESP_LOGD(TAG, "Wrote UTC time: %04d-%02d-%02d %02d:%02d:%02d", now.year, now.month, now.day_of_month, now.hour,
now.minute, now.second);
@@ -120,7 +120,7 @@ void RX8130Component::write_time() {
void RX8130Component::stop_(bool stop) {
const uint8_t data = stop ? RX8130_BIT_CTRL_STOP : RX8130_CLEAR_FLAGS;
if (this->write_register(RX8130_REG_CTRL0, &data, 1) != i2c::ERROR_OK) {
this->status_set_warning(ESP_LOG_MSG_COMM_FAIL);
this->status_set_warning(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
}
}
@@ -7,7 +7,7 @@
namespace esphome {
namespace safe_mode {
class SafeModeButton : public button::Button, public Component {
class SafeModeButton final : public button::Button, public Component {
public:
void dump_config() override;
void set_safe_mode(SafeModeComponent *safe_mode_component);
@@ -297,19 +297,17 @@ void MR24HPC1Component::r24_split_data_frame_(uint8_t value) {
this->sg_recv_data_state_ = FRAME_DATA_LEN_H;
break;
case FRAME_DATA_LEN_H:
if (value <= 4) {
this->sg_data_len_ = value * 256;
if (value == 0) {
this->sg_frame_buf_[4] = value;
this->sg_recv_data_state_ = FRAME_DATA_LEN_L;
} else {
this->sg_data_len_ = 0;
this->sg_recv_data_state_ = FRAME_IDLE;
ESP_LOGD(TAG, "FRAME_DATA_LEN_H ERROR value:%x", value);
}
break;
case FRAME_DATA_LEN_L:
this->sg_data_len_ += value;
if (this->sg_data_len_ > 32) {
this->sg_data_len_ = value;
if (this->sg_data_len_ == 0 || this->sg_data_len_ > 32) {
ESP_LOGD(TAG, "len=%d, FRAME_DATA_LEN_L ERROR value:%x", this->sg_data_len_, value);
this->sg_data_len_ = 0;
this->sg_recv_data_state_ = FRAME_IDLE;
@@ -320,9 +318,8 @@ void MR24HPC1Component::r24_split_data_frame_(uint8_t value) {
}
break;
case FRAME_DATA_BYTES:
this->sg_data_len_ -= 1;
this->sg_frame_buf_[this->sg_frame_len_++] = value;
if (this->sg_data_len_ <= 0) {
if (--this->sg_data_len_ == 0) {
this->sg_recv_data_state_ = FRAME_DATA_CRC;
}
break;
+1 -1
View File
@@ -48,7 +48,7 @@ namespace esphome::sha256 {
/// hasher.init();
/// hasher.add(data, len);
/// hasher.calculate();
class SHA256 : public esphome::HashBase {
class SHA256 final : public esphome::HashBase {
public:
SHA256() = default;
~SHA256() override;
@@ -134,7 +134,8 @@ void socket_wake() {
// code (CONT context) — they never preempt each other, so no locking is needed.
//
// esphome::LwIPLock is the platform-provided RAII guard (see helpers.h/helpers.cpp).
// On RP2040, it acquires cyw43_arch_lwip_begin/end. On ESP8266, it's a no-op.
// On RP2040, it acquires cyw43_arch_lwip_begin/end (WiFi) or ethernet_arch_lwip_begin/end
// (Ethernet). On ESP8266, it's a no-op.
#define LWIP_LOCK() esphome::LwIPLock lwip_lock_guard // NOLINT
static const char *const TAG = "socket.lwip";
+7 -6
View File
@@ -2,6 +2,7 @@
#include "st7701s.h"
#include "esphome/core/gpio.h"
#include "esphome/core/log.h"
#include <driver/gpio.h>
namespace esphome {
namespace st7701s {
@@ -27,14 +28,14 @@ void ST7701S::setup() {
config.clk_src = LCD_CLK_SRC_PLL160M;
size_t data_pin_count = sizeof(this->data_pins_) / sizeof(this->data_pins_[0]);
for (size_t i = 0; i != data_pin_count; i++) {
config.data_gpio_nums[i] = this->data_pins_[i]->get_pin();
config.data_gpio_nums[i] = static_cast<gpio_num_t>(this->data_pins_[i]->get_pin());
}
config.data_width = data_pin_count;
config.disp_gpio_num = -1;
config.hsync_gpio_num = this->hsync_pin_->get_pin();
config.vsync_gpio_num = this->vsync_pin_->get_pin();
config.de_gpio_num = this->de_pin_->get_pin();
config.pclk_gpio_num = this->pclk_pin_->get_pin();
config.disp_gpio_num = GPIO_NUM_NC;
config.hsync_gpio_num = static_cast<gpio_num_t>(this->hsync_pin_->get_pin());
config.vsync_gpio_num = static_cast<gpio_num_t>(this->vsync_pin_->get_pin());
config.de_gpio_num = static_cast<gpio_num_t>(this->de_pin_->get_pin());
config.pclk_gpio_num = static_cast<gpio_num_t>(this->pclk_pin_->get_pin());
esp_err_t err = esp_lcd_new_rgb_panel(&config, &this->handle_);
if (err != ESP_OK) {
esph_log_e(TAG, "lcd_new_rgb_panel failed: %s", esp_err_to_name(err));
@@ -2,6 +2,7 @@
#include "tinyusb_component.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "tinyusb_default_config.h"
namespace esphome::tinyusb {
@@ -15,19 +16,19 @@ void TinyUSB::setup() {
this->string_descriptor_[SERIAL_NUMBER] = mac_addr_buf;
}
this->tusb_cfg_ = {
.port = TINYUSB_PORT_FULL_SPEED_0,
.phy = {.skip_setup = false},
.descriptor =
{
.device = &this->usb_descriptor_,
.string = this->string_descriptor_,
.string_count = SIZE,
},
// Start from esp_tinyusb defaults to keep required task settings valid across esp_tinyusb updates.
this->tusb_cfg_ = TINYUSB_DEFAULT_CONFIG();
this->tusb_cfg_.port = TINYUSB_PORT_FULL_SPEED_0;
this->tusb_cfg_.phy.skip_setup = false;
this->tusb_cfg_.descriptor = {
.device = &this->usb_descriptor_,
.string = this->string_descriptor_,
.string_count = SIZE,
};
esp_err_t result = tinyusb_driver_install(&this->tusb_cfg_);
if (result != ESP_OK) {
ESP_LOGE(TAG, "tinyusb_driver_install failed: %s", esp_err_to_name(result));
this->mark_failed();
}
}
+5
View File
@@ -4,7 +4,9 @@ from esphome.components.esp32 import (
VARIANT_ESP32P4,
VARIANT_ESP32S2,
VARIANT_ESP32S3,
add_idf_component,
add_idf_sdkconfig_option,
idf_version,
only_on_variant,
)
import esphome.config_validation as cv
@@ -64,6 +66,9 @@ async def register_usb_client(config):
async def to_code(config: ConfigType) -> None:
# IDF 6.0 moved USB host to an external component
if idf_version() >= cv.Version(6, 0, 0):
add_idf_component(name="espressif/usb", ref="1.3.0")
add_idf_sdkconfig_option("CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE", 1024)
if config.get(CONF_ENABLE_HUBS):
add_idf_sdkconfig_option("CONFIG_USB_HOST_HUBS_SUPPORTED", True)
+1 -1
View File
@@ -416,7 +416,7 @@ void USBUartTypeCdcAcm::on_connected() {
for (auto *channel : this->channels_) {
if (i == cdc_devs.size()) {
ESP_LOGE(TAG, "No configuration found for channel %d", channel->index_);
this->status_set_warning("No configuration found for channel");
this->status_set_warning(LOG_STR("No configuration found for channel"));
break;
}
channel->cdc_dev_ = cdc_devs[i++];
@@ -5,7 +5,7 @@
namespace esphome::version {
class VersionTextSensor : public text_sensor::TextSensor, public Component {
class VersionTextSensor final : public text_sensor::TextSensor, public Component {
public:
void set_hide_hash(bool hide_hash);
void set_hide_timestamp(bool hide_timestamp);
@@ -74,12 +74,13 @@ int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_
// Use MSG_DONTWAIT to prevent blocking when TCP send buffer is full
int ret = send(sockfd, buf, buf_len, flags | MSG_DONTWAIT);
if (ret < 0) {
if (errno == EAGAIN || errno == EWOULDBLOCK) {
const int err = errno;
if (err == EAGAIN || err == EWOULDBLOCK) {
// Buffer full - retry later
return HTTPD_SOCK_ERR_TIMEOUT;
}
// Real error
ESP_LOGD(TAG, "send error: errno %d", errno);
ESP_LOGD(TAG, "send error: errno %d", err);
return HTTPD_SOCK_ERR_FAIL;
}
return ret;
+1 -1
View File
@@ -399,7 +399,7 @@ class WiFiPowerSaveListener {
};
/// This component is responsible for managing the ESP WiFi interface.
class WiFiComponent : public Component {
class WiFiComponent final : public Component {
public:
/// Construct a WiFiComponent.
WiFiComponent();
@@ -90,7 +90,7 @@ void ZWaveProxy::process_uart_() {
while (this->available()) {
uint8_t byte;
if (!this->read_byte(&byte)) {
this->status_set_warning("UART read failed");
this->status_set_warning(LOG_STR("UART read failed"));
return;
}
if (this->parse_byte_(byte)) {
+1 -1
View File
@@ -314,7 +314,7 @@ class Version:
@classmethod
def parse(cls, value: str) -> Version:
match = re.match(r"^(\d+).(\d+).(\d+)-?(\w*)$", value)
match = re.match(r"^(\d+).(\d+).(\d+)[-.]?(\w*)$", value)
if match is None:
raise ValueError(f"Not a valid version number {value}")
major = int(match[1])
+7 -3
View File
@@ -698,18 +698,22 @@ void Application::yield_with_select_(uint32_t delay_ms) {
#endif
// Process select() result:
// ret < 0: error (except EINTR which is normal)
// ret > 0: socket(s) have data ready - normal and expected
// ret == 0: timeout occurred - normal and expected
if (ret >= 0 || errno == EINTR) [[likely]] {
if (ret >= 0) [[likely]] {
// Yield if zero timeout since select(0) only polls without yielding
if (delay_ms == 0) [[unlikely]] {
yield();
}
return;
}
// ret < 0: error (EINTR is normal, anything else is unexpected)
const int err = errno;
if (err == EINTR) {
return;
}
// select() error - log and fall through to delay()
ESP_LOGW(TAG, "select() failed with errno %d", errno);
ESP_LOGW(TAG, "select() failed with errno %d", err);
}
// No sockets registered or select() failed - use regular delay
delay(delay_ms);
+5 -9
View File
@@ -392,6 +392,7 @@ bool Component::set_status_flag_(uint8_t flag) {
return true;
}
void Component::status_set_warning() { this->status_set_warning((const LogString *) nullptr); }
void Component::status_set_warning(const char *message) {
if (!this->set_status_flag_(STATUS_LED_WARNING))
return;
@@ -509,7 +510,8 @@ void PollingComponent::stop_poller() {
uint32_t PollingComponent::get_update_interval() const { return this->update_interval_; }
void PollingComponent::set_update_interval(uint32_t update_interval) { this->update_interval_ = update_interval; }
static void __attribute__((noinline, cold)) warn_blocking(Component *component, uint32_t blocking_time) {
void __attribute__((noinline, cold))
WarnIfComponentBlockingGuard::warn_blocking(Component *component, uint32_t blocking_time) {
bool should_warn;
if (component != nullptr) {
should_warn = component->should_warn_of_blocking(blocking_time);
@@ -523,10 +525,8 @@ static void __attribute__((noinline, cold)) warn_blocking(Component *component,
}
}
uint32_t WarnIfComponentBlockingGuard::finish() {
uint32_t curr_time = millis();
uint32_t blocking_time = curr_time - this->started_;
#ifdef USE_RUNTIME_STATS
void WarnIfComponentBlockingGuard::record_runtime_stats_() {
// Use micros() for accurate sub-millisecond timing. millis() has insufficient
// resolution — most components complete in microseconds but millis() only has
// 1ms granularity, so results were essentially random noise.
@@ -534,12 +534,8 @@ uint32_t WarnIfComponentBlockingGuard::finish() {
uint32_t duration_us = micros() - this->started_us_;
global_runtime_stats->record_component_time(this->component_, duration_us);
}
#endif
if (blocking_time > WARN_IF_BLOCKING_OVER_MS) {
warn_blocking(this->component_, blocking_time);
}
return curr_time;
}
#endif
#ifdef USE_SETUP_PRIORITY_OVERRIDE
void clear_setup_priority_overrides() {
+21 -5
View File
@@ -6,6 +6,7 @@
#include <string>
#include "esphome/core/defines.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "esphome/core/optional.h"
@@ -240,7 +241,8 @@ class Component {
bool status_has_error() const { return this->component_state_ & STATUS_LED_ERROR; }
void status_set_warning(const char *message = nullptr);
void status_set_warning(); // Set warning flag without message
void status_set_warning(const char *message);
void status_set_warning(const LogString *message);
void status_set_error(); // Set error flag without message
@@ -574,9 +576,7 @@ class PollingComponent : public Component {
uint32_t update_interval_;
};
#ifdef USE_RUNTIME_STATS
uint32_t micros(); // Forward declare for inline constructor
#endif
// millis() and micros() are available via hal.h
class WarnIfComponentBlockingGuard {
public:
@@ -591,7 +591,18 @@ class WarnIfComponentBlockingGuard {
}
// Finish the timing operation and return the current time
uint32_t finish();
// Inlined: the fast path is just millis() + subtract + compare
inline uint32_t HOT finish() {
uint32_t curr_time = millis();
uint32_t blocking_time = curr_time - this->started_;
#ifdef USE_RUNTIME_STATS
this->record_runtime_stats_();
#endif
if (blocking_time > WARN_IF_BLOCKING_OVER_MS) [[unlikely]] {
warn_blocking(this->component_, blocking_time);
}
return curr_time;
}
~WarnIfComponentBlockingGuard() = default;
@@ -600,7 +611,12 @@ class WarnIfComponentBlockingGuard {
Component *component_;
#ifdef USE_RUNTIME_STATS
uint32_t started_us_;
void record_runtime_stats_();
#endif
private:
// Cold path for blocking warning - defined in component.cpp
static void __attribute__((noinline, cold)) warn_blocking(Component *component, uint32_t blocking_time);
};
// Function to clear setup priority overrides after all components are set up
+14
View File
@@ -278,6 +278,14 @@
#define USE_ETHERNET_JL1101
#define USE_ETHERNET_KSZ8081
#define USE_ETHERNET_LAN8670
#define USE_ETHERNET_SPI
#define USE_ETHERNET_SPI_POLLING_SUPPORT
#define USE_ETHERNET_OPENETH
#define USE_ETHERNET_W5500
#define USE_ETHERNET_DM9051
#define CONFIG_ETH_SPI_ETHERNET_W5500 1
#define CONFIG_ETH_SPI_ETHERNET_DM9051 1
#define CONFIG_ETH_USE_ESP32_EMAC 1
#define USE_ETHERNET_MANUAL_IP
#define USE_ETHERNET_IP_STATE_LISTENERS
#define USE_ETHERNET_CONNECT_TRIGGER
@@ -345,6 +353,12 @@
#define USE_SOCKET_IMPL_LWIP_TCP
#define USE_RP2040_BLE
#define USE_SPI
#ifndef USE_ETHERNET
#define USE_ETHERNET
#endif
#ifndef USE_ETHERNET_SPI
#define USE_ETHERNET_SPI
#endif
#endif
#ifdef USE_LIBRETINY
+21 -6
View File
@@ -1866,19 +1866,34 @@ template<typename T> class Parented {
*/
class Mutex {
public:
Mutex();
Mutex(const Mutex &) = delete;
Mutex &operator=(const Mutex &) = delete;
#if defined(USE_ESP8266) || defined(USE_RP2040)
// Single-threaded platforms: inline no-ops so the compiler eliminates all call overhead.
Mutex() = default;
~Mutex() = default;
void lock() {}
bool try_lock() { return true; }
void unlock() {}
#elif defined(USE_ESP32) || defined(USE_LIBRETINY)
// FreeRTOS platforms: inline to avoid out-of-line call overhead.
Mutex() { handle_ = xSemaphoreCreateMutex(); }
~Mutex() = default;
void lock() { xSemaphoreTake(this->handle_, portMAX_DELAY); }
bool try_lock() { return xSemaphoreTake(this->handle_, 0) == pdTRUE; }
void unlock() { xSemaphoreGive(this->handle_); }
private:
SemaphoreHandle_t handle_;
#else
Mutex();
~Mutex();
void lock();
bool try_lock();
void unlock();
Mutex &operator=(const Mutex &) = delete;
private:
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
SemaphoreHandle_t handle_;
#else
// d-pointer to store private data on new platforms
void *handle_; // NOLINT(clang-diagnostic-unused-private-field)
#endif
+39 -1
View File
@@ -5,6 +5,8 @@ dependencies:
version: 2.0.3
esphome/micro-opus:
version: 0.3.5
espressif/esp-dsp:
version: "1.7.1"
espressif/esp-tflite-micro:
version: 1.3.3~1
espressif/esp32-camera:
@@ -29,13 +31,49 @@ dependencies:
version: "2.0.0"
rules:
- if: "target in [esp32, esp32p4]"
espressif/lan87xx:
version: "1.0.0"
rules:
- if: "idf_version >=6.0.0 && target in [esp32, esp32p4]"
espressif/rtl8201:
version: "1.0.1"
rules:
- if: "idf_version >=6.0.0 && target in [esp32, esp32p4]"
espressif/dp83848:
version: "1.0.0"
rules:
- if: "idf_version >=6.0.0 && target in [esp32, esp32p4]"
espressif/ip101:
version: "1.0.0"
rules:
- if: "idf_version >=6.0.0 && target in [esp32, esp32p4]"
espressif/ksz80xx:
version: "1.0.0"
rules:
- if: "idf_version >=6.0.0 && target in [esp32, esp32p4]"
espressif/w5500:
version: "1.0.1"
rules:
- if: "idf_version >=6.0.0"
espressif/dm9051:
version: "1.0.0"
rules:
- if: "idf_version >=6.0.0"
espressif/esp_tinyusb:
version: "2.1.1"
rules:
- if: "target in [esp32s2, esp32s3, esp32p4]"
esphome/esp-hub75:
version: 0.3.2
version: 0.3.4
rules:
- if: "target in [esp32, esp32s2, esp32s3, esp32c6, esp32p4]"
espressif/mqtt:
version: "1.0.0"
rules:
- if: "idf_version >=6.0.0"
espressif/usb:
version: "1.3.0"
rules:
- if: "idf_version >=6.0.0 && target in [esp32s2, esp32s3, esp32p4]"
esp32async/asynctcp:
version: 3.4.91
+1 -1
View File
@@ -12,7 +12,7 @@ platformio==6.1.19
esptool==5.2.0
click==8.3.1
esphome-dashboard==20260210.0
aioesphomeapi==44.5.1
aioesphomeapi==44.5.2
zeroconf==0.148.0
puremagic==1.30
ruamel.yaml==0.19.1 # dashboard_import
+12 -3
View File
@@ -7,17 +7,26 @@ import subprocess
import sys
import tempfile
from esphome import config_validation as cv
from esphome.components.esp32 import PLATFORM_VERSION_LOOKUP
from esphome.helpers import write_file_if_changed
ver = PLATFORM_VERSION_LOOKUP["recommended"]
version_str = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}"
root = Path(__file__).parent.parent
boards_file_path = root / "esphome" / "components" / "esp32" / "boards.py"
def get_boards():
with tempfile.TemporaryDirectory() as tempdir:
if isinstance(ver, cv.Version):
branch = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}"
if ver.extra:
branch += f"-{ver.extra}"
repo = "https://github.com/pioarduino/platform-espressif32"
else:
# URL format: "https://github.com/user/repo.git#branch"
url = str(ver)
repo, branch = url.rsplit("#", 1) if "#" in url else (url, "main")
subprocess.run(
[
"git",
@@ -28,8 +37,8 @@ def get_boards():
"--depth",
"1",
"--branch",
f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}",
"https://github.com/pioarduino/platform-espressif32",
branch,
repo,
tempdir,
],
check=True,
@@ -0,0 +1,18 @@
ethernet:
type: W5500
clk_pin: 18
mosi_pin: 19
miso_pin: 16
cs_pin: 17
interrupt_pin: 21
reset_pin: 20
manual_ip:
static_ip: 192.168.178.56
gateway: 192.168.178.1
subnet: 255.255.255.0
domain: .local
mac_address: "02:AA:BB:CC:DD:01"
on_connect:
- logger.log: "Ethernet connected!"
on_disconnect:
- logger.log: "Ethernet disconnected!"
@@ -0,0 +1 @@
<<: !include common-w5500-rp2040.yaml
@@ -0,0 +1 @@
<<: !include common.yaml
@@ -0,0 +1,19 @@
packages:
spi: !include ../../test_build_components/common/spi/esp32-s3-ard.yaml
<<: !include common.yaml
http_request:
display:
- platform: ili9xxx
spi_id: spi_bus
id: main_lcd
model: ili9342
cs_pin: 20
dc_pin: 13
reset_pin: 21
invert_colors: true
lambda: |-
it.fill(Color(0, 0, 0));
it.image(0, 0, id(online_rgba_image));
@@ -0,0 +1,19 @@
packages:
spi: !include ../../test_build_components/common/spi/esp32-s3-idf.yaml
<<: !include common.yaml
http_request:
display:
- platform: ili9xxx
spi_id: spi_bus
id: main_lcd
model: ili9342
cs_pin: 20
dc_pin: 13
reset_pin: 21
invert_colors: true
lambda: |-
it.fill(Color(0, 0, 0));
it.image(0, 0, id(online_rgba_image));
@@ -3,7 +3,7 @@ esphome:
host:
api:
batch_delay: 0ms # Disable batching to receive all state updates
batch_delay: 0ms # Disable batching to receive all state updates
logger:
level: DEBUG
@@ -15,8 +15,8 @@ sensor:
- platform: copy
source_id: source_nan_sensor
name: "Min NaN Sensor"
id: min_nan_sensor
name: "Min NaN"
id: min_nan
filters:
- min:
window_size: 5
@@ -25,8 +25,8 @@ sensor:
- platform: copy
source_id: source_nan_sensor
name: "Max NaN Sensor"
id: max_nan_sensor
name: "Max NaN"
id: max_nan
filters:
- max:
window_size: 5
@@ -42,7 +42,7 @@ script:
- delay: 20ms
- sensor.template.publish:
id: source_nan_sensor
state: !lambda 'return NAN;'
state: !lambda "return NAN;"
- delay: 20ms
- sensor.template.publish:
id: source_nan_sensor
@@ -50,7 +50,7 @@ script:
- delay: 20ms
- sensor.template.publish:
id: source_nan_sensor
state: !lambda 'return NAN;'
state: !lambda "return NAN;"
- delay: 20ms
- sensor.template.publish:
id: source_nan_sensor
@@ -62,7 +62,7 @@ script:
- delay: 20ms
- sensor.template.publish:
id: source_nan_sensor
state: !lambda 'return NAN;'
state: !lambda "return NAN;"
- delay: 20ms
- sensor.template.publish:
id: source_nan_sensor
@@ -74,7 +74,7 @@ script:
- delay: 20ms
- sensor.template.publish:
id: source_nan_sensor
state: !lambda 'return NAN;'
state: !lambda "return NAN;"
button:
- platform: template
@@ -3,7 +3,7 @@ esphome:
host:
api:
batch_delay: 0ms # Disable batching to receive all state updates
batch_delay: 0ms # Disable batching to receive all state updates
logger:
level: DEBUG
@@ -18,8 +18,8 @@ sensor:
# Window of 5, send every 2 values
- platform: copy
source_id: source_sensor
name: "Sliding Min Sensor"
id: sliding_min_sensor
name: "Sliding Min"
id: sliding_min
filters:
- min:
window_size: 5
@@ -28,8 +28,8 @@ sensor:
- platform: copy
source_id: source_sensor
name: "Sliding Max Sensor"
id: sliding_max_sensor
name: "Sliding Max"
id: sliding_max
filters:
- max:
window_size: 5
@@ -38,8 +38,8 @@ sensor:
- platform: copy
source_id: source_sensor
name: "Sliding Median Sensor"
id: sliding_median_sensor
name: "Sliding Median"
id: sliding_median
filters:
- median:
window_size: 5
@@ -48,8 +48,8 @@ sensor:
- platform: copy
source_id: source_sensor
name: "Sliding Moving Avg Sensor"
id: sliding_moving_avg_sensor
name: "Sliding Moving Avg"
id: sliding_moving_avg
filters:
- sliding_window_moving_average:
window_size: 5
@@ -3,20 +3,20 @@ esphome:
host:
api:
batch_delay: 0ms # Disable batching to receive all state updates
batch_delay: 0ms # Disable batching to receive all state updates
logger:
level: DEBUG
sensor:
- platform: template
name: "Source Wraparound Sensor"
name: "Source Wraparound"
id: source_wraparound
accuracy_decimals: 2
- platform: copy
source_id: source_wraparound
name: "Wraparound Min Sensor"
id: wraparound_min_sensor
name: "Wraparound Min"
id: wraparound_min
filters:
- min:
window_size: 3
+2 -2
View File
@@ -88,7 +88,7 @@ def build_key_to_entity_mapping(
Args:
entities: List of entity info objects from the API
entity_names: List of entity names to search for in object_ids
entity_names: List of entity names to match exactly against object_ids
Returns:
Dictionary mapping entity keys to entity names
@@ -97,7 +97,7 @@ def build_key_to_entity_mapping(
for entity in entities:
obj_id = entity.object_id.lower()
for entity_name in entity_names:
if entity_name in obj_id:
if entity_name == obj_id:
key_to_entity[entity.key] = entity_name
break
return key_to_entity