Merge pull request #14151 from esphome/bump-2026.2.1

2026.2.1
This commit is contained in:
Jonathan Swoboda
2026-02-20 11:24:11 -05:00
committed by GitHub
34 changed files with 497 additions and 126 deletions

View File

@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.2.0
PROJECT_NUMBER = 2026.2.1
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a

View File

@@ -14,12 +14,17 @@ static const int PORT = 5568;
E131Component::E131Component() {}
E131Component::~E131Component() {
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
if (this->socket_) {
this->socket_->close();
}
#elif defined(USE_SOCKET_IMPL_LWIP_TCP)
this->udp_.stop();
#endif
}
void E131Component::setup() {
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
this->socket_ = socket::socket_ip(SOCK_DGRAM, IPPROTO_IP);
int enable = 1;
@@ -50,6 +55,13 @@ void E131Component::setup() {
this->mark_failed();
return;
}
#elif defined(USE_SOCKET_IMPL_LWIP_TCP)
if (!this->udp_.begin(PORT)) {
ESP_LOGW(TAG, "Cannot bind E1.31 to port %d.", PORT);
this->mark_failed();
return;
}
#endif
join_igmp_groups_();
}
@@ -59,19 +71,36 @@ void E131Component::loop() {
int universe = 0;
uint8_t buf[1460];
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
ssize_t len = this->socket_->read(buf, sizeof(buf));
if (len == -1) {
return;
}
if (!this->packet_(buf, (size_t) len, universe, packet)) {
ESP_LOGV(TAG, "Invalid packet received of size %zd.", len);
ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len);
return;
}
if (!this->process_(universe, packet)) {
ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count);
}
#elif defined(USE_SOCKET_IMPL_LWIP_TCP)
while (auto packet_size = this->udp_.parsePacket()) {
auto len = this->udp_.read(buf, sizeof(buf));
if (len <= 0)
continue;
if (!this->packet_(buf, (size_t) len, universe, packet)) {
ESP_LOGV(TAG, "Invalid packet received of size %d.", (int) len);
continue;
}
if (!this->process_(universe, packet)) {
ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count);
}
}
#endif
}
void E131Component::add_effect(E131AddressableLightEffect *light_effect) {

View File

@@ -1,7 +1,11 @@
#pragma once
#include "esphome/core/defines.h"
#ifdef USE_NETWORK
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
#include "esphome/components/socket/socket.h"
#elif defined(USE_SOCKET_IMPL_LWIP_TCP)
#include <WiFiUdp.h>
#endif
#include "esphome/core/component.h"
#include <cinttypes>
@@ -45,7 +49,11 @@ class E131Component : public esphome::Component {
void leave_(int universe);
E131ListenMethod listen_method_{E131_MULTICAST};
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
std::unique_ptr<socket::Socket> socket_;
#elif defined(USE_SOCKET_IMPL_LWIP_TCP)
WiFiUDP udp_;
#endif
std::vector<E131AddressableLightEffect *> light_effects_;
std::map<int, int> universe_consumers_;
};

View File

@@ -62,8 +62,10 @@ const size_t E131_MIN_PACKET_SIZE = reinterpret_cast<size_t>(&((E131RawPacket *)
bool E131Component::join_igmp_groups_() {
if (listen_method_ != E131_MULTICAST)
return false;
#if defined(USE_SOCKET_IMPL_BSD_SOCKETS) || defined(USE_SOCKET_IMPL_LWIP_SOCKETS)
if (this->socket_ == nullptr)
return false;
#endif
for (auto universe : universe_consumers_) {
if (!universe.second)

View File

@@ -9,6 +9,7 @@ from esphome import automation
import esphome.codegen as cg
from esphome.components import socket
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
from esphome.components.esp32.const import VARIANT_ESP32C2
import esphome.config_validation as cv
from esphome.const import (
CONF_ENABLE_ON_BOOT,
@@ -387,6 +388,15 @@ def final_validation(config):
f"Name '{name}' is too long, maximum length is {max_length} characters"
)
# ESP32-C2 has very limited RAM (~272KB). Without releasing BLE IRAM,
# esp_bt_controller_init fails with ESP_ERR_NO_MEM.
# CONFIG_BT_RELEASE_IRAM changes the memory layout so IRAM and DRAM share
# space more flexibly, giving the BT controller enough contiguous memory.
# This requires CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT to be disabled.
if get_esp32_variant() == VARIANT_ESP32C2:
add_idf_sdkconfig_option("CONFIG_BT_RELEASE_IRAM", True)
add_idf_sdkconfig_option("CONFIG_ESP_SYSTEM_PMP_IDRAM_SPLIT", False)
# Set GATT Client/Server sdkconfig options based on which components are loaded
full_config = fv.full_config.get()

View File

@@ -527,7 +527,7 @@ async def to_code_characteristic(service_var, char_conf):
action_conf,
char_conf[CONF_CHAR_VALUE_ACTION_ID_],
cg.TemplateArguments(),
{},
[],
)
cg.add(value_action.play())
else:

View File

@@ -246,9 +246,27 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
if (this->handle_ != param->write.handle)
break;
esp_gatt_status_t status = ESP_GATT_OK;
if (param->write.is_prep) {
this->value_.insert(this->value_.end(), param->write.value, param->write.value + param->write.len);
this->write_event_ = true;
const size_t offset = param->write.offset;
const size_t write_len = param->write.len;
const size_t new_size = offset + write_len;
// Clean the buffer on the first prepared write event
if (offset == 0) {
this->value_.clear();
}
if (offset != this->value_.size()) {
status = ESP_GATT_INVALID_OFFSET;
} else if (new_size > ESP_GATT_MAX_ATTR_LEN) {
status = ESP_GATT_INVALID_ATTR_LEN;
} else {
if (this->value_.size() < new_size) {
this->value_.resize(new_size);
}
memcpy(this->value_.data() + offset, param->write.value, write_len);
}
} else {
this->set_value(ByteBuffer::wrap(param->write.value, param->write.len));
}
@@ -263,7 +281,7 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
memcpy(response.attr_value.value, param->write.value, param->write.len);
esp_err_t err =
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, &response);
esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, status, &response);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err);
@@ -280,9 +298,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
}
case ESP_GATTS_EXEC_WRITE_EVT: {
if (!this->write_event_)
// BLE stack will guarantee that ESP_GATTS_EXEC_WRITE_EVT is only received after prepared writes
if (this->value_.empty())
break;
this->write_event_ = false;
if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) {
if (this->on_write_callback_) {
(*this->on_write_callback_)(this->value_, param->exec_write.conn_id);

View File

@@ -77,7 +77,6 @@ class BLECharacteristic {
}
protected:
bool write_event_{false};
BLEService *service_{};
ESPBTUUID uuid_;
esp_gatt_char_prop_t properties_;

View File

@@ -218,12 +218,19 @@ def _validate(config):
)
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 and will be removed in ESPHome 2026.1. "
"Please update your configuration to use 'clk' instead."
"[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,
)
mode = CLK_MODES_DEPRECATED[config[CONF_CLK_MODE]]
config[CONF_CLK] = CLK_SCHEMA({CONF_MODE: mode[0], CONF_PIN: mode[1]})
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].")

View File

@@ -608,8 +608,9 @@ void LD2410Component::readline_(int readch) {
// We should never get here, but just in case...
ESP_LOGW(TAG, "Max command length exceeded; ignoring");
this->buffer_pos_ = 0;
return;
}
if (this->buffer_pos_ < 4) {
if (this->buffer_pos_ < HEADER_FOOTER_SIZE) {
return; // Not enough data to process yet
}
if (ld2410::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {

View File

@@ -33,8 +33,10 @@ namespace esphome::ld2410 {
using namespace ld24xx;
static constexpr uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer
static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410
// Engineering data frame is 45 bytes; +1 for null terminator, +4 so that a frame footer always
// lands inside the buffer during footer-based resynchronization after losing sync.
static constexpr uint8_t MAX_LINE_LENGTH = 50;
static constexpr uint8_t TOTAL_GATES = 9; // Total number of gates supported by the LD2410
class LD2410Component : public Component, public uart::UARTDevice {
#ifdef USE_BINARY_SENSOR

View File

@@ -63,73 +63,73 @@ namespace esphome::ld2420 {
static const char *const TAG = "ld2420";
// Local const's
static const uint16_t REFRESH_RATE_MS = 1000;
static constexpr uint16_t REFRESH_RATE_MS = 1000;
// Command sets
static const uint16_t CMD_DISABLE_CONF = 0x00FE;
static const uint16_t CMD_ENABLE_CONF = 0x00FF;
static const uint16_t CMD_PARM_HIGH_TRESH = 0x0012;
static const uint16_t CMD_PARM_LOW_TRESH = 0x0021;
static const uint16_t CMD_PROTOCOL_VER = 0x0002;
static const uint16_t CMD_READ_ABD_PARAM = 0x0008;
static const uint16_t CMD_READ_REG_ADDR = 0x0020;
static const uint16_t CMD_READ_REGISTER = 0x0002;
static const uint16_t CMD_READ_SERIAL_NUM = 0x0011;
static const uint16_t CMD_READ_SYS_PARAM = 0x0013;
static const uint16_t CMD_READ_VERSION = 0x0000;
static const uint16_t CMD_RESTART = 0x0068;
static const uint16_t CMD_SYSTEM_MODE = 0x0000;
static const uint16_t CMD_SYSTEM_MODE_GR = 0x0003;
static const uint16_t CMD_SYSTEM_MODE_MTT = 0x0001;
static const uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064;
static const uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000;
static const uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004;
static const uint16_t CMD_SYSTEM_MODE_VS = 0x0002;
static const uint16_t CMD_WRITE_ABD_PARAM = 0x0007;
static const uint16_t CMD_WRITE_REGISTER = 0x0001;
static const uint16_t CMD_WRITE_SYS_PARAM = 0x0012;
static constexpr uint16_t CMD_DISABLE_CONF = 0x00FE;
static constexpr uint16_t CMD_ENABLE_CONF = 0x00FF;
static constexpr uint16_t CMD_PARM_HIGH_TRESH = 0x0012;
static constexpr uint16_t CMD_PARM_LOW_TRESH = 0x0021;
static constexpr uint16_t CMD_PROTOCOL_VER = 0x0002;
static constexpr uint16_t CMD_READ_ABD_PARAM = 0x0008;
static constexpr uint16_t CMD_READ_REG_ADDR = 0x0020;
static constexpr uint16_t CMD_READ_REGISTER = 0x0002;
static constexpr uint16_t CMD_READ_SERIAL_NUM = 0x0011;
static constexpr uint16_t CMD_READ_SYS_PARAM = 0x0013;
static constexpr uint16_t CMD_READ_VERSION = 0x0000;
static constexpr uint16_t CMD_RESTART = 0x0068;
static constexpr uint16_t CMD_SYSTEM_MODE = 0x0000;
static constexpr uint16_t CMD_SYSTEM_MODE_GR = 0x0003;
static constexpr uint16_t CMD_SYSTEM_MODE_MTT = 0x0001;
static constexpr uint16_t CMD_SYSTEM_MODE_SIMPLE = 0x0064;
static constexpr uint16_t CMD_SYSTEM_MODE_DEBUG = 0x0000;
static constexpr uint16_t CMD_SYSTEM_MODE_ENERGY = 0x0004;
static constexpr uint16_t CMD_SYSTEM_MODE_VS = 0x0002;
static constexpr uint16_t CMD_WRITE_ABD_PARAM = 0x0007;
static constexpr uint16_t CMD_WRITE_REGISTER = 0x0001;
static constexpr uint16_t CMD_WRITE_SYS_PARAM = 0x0012;
static const uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04;
static const uint8_t CMD_ABD_DATA_REPLY_START = 0x0A;
static const uint8_t CMD_MAX_BYTES = 0x64;
static const uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02;
static constexpr uint8_t CMD_ABD_DATA_REPLY_SIZE = 0x04;
static constexpr uint8_t CMD_ABD_DATA_REPLY_START = 0x0A;
static constexpr uint8_t CMD_MAX_BYTES = 0x64;
static constexpr uint8_t CMD_REG_DATA_REPLY_SIZE = 0x02;
static const uint8_t LD2420_ERROR_NONE = 0x00;
static const uint8_t LD2420_ERROR_TIMEOUT = 0x02;
static const uint8_t LD2420_ERROR_UNKNOWN = 0x01;
static constexpr uint8_t LD2420_ERROR_NONE = 0x00;
static constexpr uint8_t LD2420_ERROR_TIMEOUT = 0x02;
static constexpr uint8_t LD2420_ERROR_UNKNOWN = 0x01;
// Register address values
static const uint16_t CMD_MIN_GATE_REG = 0x0000;
static const uint16_t CMD_MAX_GATE_REG = 0x0001;
static const uint16_t CMD_TIMEOUT_REG = 0x0004;
static const uint16_t CMD_GATE_MOVE_THRESH[TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015,
0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B,
0x001C, 0x001D, 0x001E, 0x001F};
static const uint16_t CMD_GATE_STILL_THRESH[TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025,
0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B,
0x002C, 0x002D, 0x002E, 0x002F};
static const uint32_t FACTORY_MOVE_THRESH[TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250,
250, 250, 250, 250, 250, 250, 250, 250};
static const uint32_t FACTORY_STILL_THRESH[TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150,
150, 100, 100, 100, 100, 100, 100, 100};
static const uint16_t FACTORY_TIMEOUT = 120;
static const uint16_t FACTORY_MIN_GATE = 1;
static const uint16_t FACTORY_MAX_GATE = 12;
static constexpr uint16_t CMD_MIN_GATE_REG = 0x0000;
static constexpr uint16_t CMD_MAX_GATE_REG = 0x0001;
static constexpr uint16_t CMD_TIMEOUT_REG = 0x0004;
static constexpr uint16_t CMD_GATE_MOVE_THRESH[TOTAL_GATES] = {0x0010, 0x0011, 0x0012, 0x0013, 0x0014, 0x0015,
0x0016, 0x0017, 0x0018, 0x0019, 0x001A, 0x001B,
0x001C, 0x001D, 0x001E, 0x001F};
static constexpr uint16_t CMD_GATE_STILL_THRESH[TOTAL_GATES] = {0x0020, 0x0021, 0x0022, 0x0023, 0x0024, 0x0025,
0x0026, 0x0027, 0x0028, 0x0029, 0x002A, 0x002B,
0x002C, 0x002D, 0x002E, 0x002F};
static constexpr uint32_t FACTORY_MOVE_THRESH[TOTAL_GATES] = {60000, 30000, 400, 250, 250, 250, 250, 250,
250, 250, 250, 250, 250, 250, 250, 250};
static constexpr uint32_t FACTORY_STILL_THRESH[TOTAL_GATES] = {40000, 20000, 200, 200, 200, 200, 200, 150,
150, 100, 100, 100, 100, 100, 100, 100};
static constexpr uint16_t FACTORY_TIMEOUT = 120;
static constexpr uint16_t FACTORY_MIN_GATE = 1;
static constexpr uint16_t FACTORY_MAX_GATE = 12;
// COMMAND_BYTE Header & Footer
static const uint32_t CMD_FRAME_FOOTER = 0x01020304;
static const uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD;
static const uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD;
static const uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA;
static const uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8;
static const uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4;
static const int CALIBRATE_VERSION_MIN = 154;
static const uint8_t CMD_FRAME_COMMAND = 6;
static const uint8_t CMD_FRAME_DATA_LENGTH = 4;
static const uint8_t CMD_FRAME_STATUS = 7;
static const uint8_t CMD_ERROR_WORD = 8;
static const uint8_t ENERGY_SENSOR_START = 9;
static const uint8_t CALIBRATE_REPORT_INTERVAL = 4;
static constexpr uint32_t CMD_FRAME_FOOTER = 0x01020304;
static constexpr uint32_t CMD_FRAME_HEADER = 0xFAFBFCFD;
static constexpr uint32_t DEBUG_FRAME_FOOTER = 0xFAFBFCFD;
static constexpr uint32_t DEBUG_FRAME_HEADER = 0x1410BFAA;
static constexpr uint32_t ENERGY_FRAME_FOOTER = 0xF5F6F7F8;
static constexpr uint32_t ENERGY_FRAME_HEADER = 0xF1F2F3F4;
static constexpr int CALIBRATE_VERSION_MIN = 154;
static constexpr uint8_t CMD_FRAME_COMMAND = 6;
static constexpr uint8_t CMD_FRAME_DATA_LENGTH = 4;
static constexpr uint8_t CMD_FRAME_STATUS = 7;
static constexpr uint8_t CMD_ERROR_WORD = 8;
static constexpr uint8_t ENERGY_SENSOR_START = 9;
static constexpr uint8_t CALIBRATE_REPORT_INTERVAL = 4;
static const char *const OP_NORMAL_MODE_STRING = "Normal";
static const char *const OP_SIMPLE_MODE_STRING = "Simple";

View File

@@ -20,9 +20,11 @@
namespace esphome::ld2420 {
static const uint8_t CALIBRATE_SAMPLES = 64;
static const uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer
static const uint8_t TOTAL_GATES = 16;
static constexpr uint8_t CALIBRATE_SAMPLES = 64;
// Energy frame is 45 bytes; +1 for null terminator, +4 so that a frame footer always lands
// inside the buffer during footer-based resynchronization after losing sync.
static constexpr uint8_t MAX_LINE_LENGTH = 50;
static constexpr uint8_t TOTAL_GATES = 16;
enum OpMode : uint8_t {
OP_NORMAL_MODE = 1,

View File

@@ -776,8 +776,9 @@ void LD2450Component::readline_(int readch) {
// We should never get here, but just in case...
ESP_LOGW(TAG, "Max command length exceeded; ignoring");
this->buffer_pos_ = 0;
return;
}
if (this->buffer_pos_ < 4) {
if (this->buffer_pos_ < HEADER_FOOTER_SIZE) {
return; // Not enough data to process yet
}
if (this->buffer_data_[this->buffer_pos_ - 2] == DATA_FRAME_FOOTER[0] &&

View File

@@ -1,5 +1,6 @@
#pragma once
#include "esphome/core/automation.h"
#include "esphome/core/defines.h"
#include "esphome/core/component.h"
#ifdef USE_SENSOR
@@ -37,9 +38,11 @@ using namespace ld24xx;
// Constants
static constexpr uint8_t DEFAULT_PRESENCE_TIMEOUT = 5; // Timeout to reset presense status 5 sec.
static constexpr uint8_t MAX_LINE_LENGTH = 41; // Max characters for serial buffer
static constexpr uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450
static constexpr uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450
// Zone query response is 40 bytes; +1 for null terminator, +4 so that a frame footer always
// lands inside the buffer during footer-based resynchronization after losing sync.
static constexpr uint8_t MAX_LINE_LENGTH = 45;
static constexpr uint8_t MAX_TARGETS = 3; // Max 3 Targets in LD2450
static constexpr uint8_t MAX_ZONES = 3; // Max 3 Zones in LD2450
enum Direction : uint8_t {
DIRECTION_APPROACHING = 0,

View File

@@ -2,7 +2,7 @@
#include "esphome/core/log.h"
#ifdef HAS_PCNT
#include <esp_clk_tree.h>
#include <esp_private/esp_clk.h>
#include <hal/pcnt_ll.h>
#endif
@@ -117,9 +117,7 @@ bool HwPulseCounterStorage::pulse_counter_setup(InternalGPIOPin *pin) {
}
if (this->filter_us != 0) {
uint32_t apb_freq;
esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_APB, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &apb_freq);
uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000000u / apb_freq;
uint32_t max_glitch_ns = PCNT_LL_MAX_GLITCH_WIDTH * 1000u / ((uint32_t) esp_clk_apb_freq() / 1000000u);
pcnt_glitch_filter_config_t filter_config = {
.max_glitch_ns = std::min(this->filter_us * 1000u, max_glitch_ns),
};

View File

@@ -8,10 +8,10 @@
#if defined(USE_ESP32)
#include <soc/soc_caps.h>
#ifdef SOC_PCNT_SUPPORTED
#if defined(SOC_PCNT_SUPPORTED) && __has_include(<driver/pulse_cnt.h>)
#include <driver/pulse_cnt.h>
#define HAS_PCNT
#endif // SOC_PCNT_SUPPORTED
#endif // defined(SOC_PCNT_SUPPORTED) && __has_include(<driver/pulse_cnt.h>)
#endif // USE_ESP32
namespace esphome {

View File

@@ -11,6 +11,7 @@
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
#include <esp_ota_ops.h>
#include <esp_system.h>
#endif
namespace esphome::safe_mode {
@@ -54,6 +55,10 @@ void SafeModeComponent::dump_config() {
"OTA rollback detected! Rolled back from partition '%s'\n"
"The device reset before the boot was marked successful",
last_invalid->label);
if (esp_reset_reason() == ESP_RST_BROWNOUT) {
ESP_LOGW(TAG, "Last reset was due to brownout - check your power supply!\n"
"See https://esphome.io/guides/faq.html#brownout-detector-was-triggered");
}
}
#endif
}

View File

@@ -59,8 +59,14 @@ size_t format_sockaddr_to(const struct sockaddr *addr_ptr, socklen_t len, std::s
#if USE_NETWORK_IPV6
else if (addr_ptr->sa_family == AF_INET6 && len >= sizeof(sockaddr_in6)) {
const auto *addr = reinterpret_cast<const struct sockaddr_in6 *>(addr_ptr);
#ifndef USE_SOCKET_IMPL_LWIP_TCP
// Format IPv4-mapped IPv6 addresses as regular IPv4 (not supported on ESP8266 raw TCP)
#ifdef USE_HOST
// Format IPv4-mapped IPv6 addresses as regular IPv4 (POSIX layout, no LWIP union)
if (IN6_IS_ADDR_V4MAPPED(&addr->sin6_addr) &&
esphome_inet_ntop4(&addr->sin6_addr.s6_addr[12], buf.data(), buf.size()) != nullptr) {
return strlen(buf.data());
}
#elif !defined(USE_SOCKET_IMPL_LWIP_TCP)
// Format IPv4-mapped IPv6 addresses as regular IPv4 (LWIP layout)
if (addr->sin6_addr.un.u32_addr[0] == 0 && addr->sin6_addr.un.u32_addr[1] == 0 &&
addr->sin6_addr.un.u32_addr[2] == htonl(0xFFFF) &&
esphome_inet_ntop4(&addr->sin6_addr.un.u32_addr[3], buf.data(), buf.size()) != nullptr) {

View File

@@ -19,6 +19,13 @@ namespace esphome::uart {
static const char *const TAG = "uart.idf";
/// Check if a pin number matches one of the default UART0 GPIO pins.
/// These pins may have residual state from the boot console that requires
/// explicit reset before UART reconfiguration (ESP-IDF issue #17459).
static constexpr bool is_default_uart0_pin(int8_t pin_num) {
return pin_num == U0TXD_GPIO_NUM || pin_num == U0RXD_GPIO_NUM;
}
uart_config_t IDFUARTComponent::get_config_() {
uart_parity_t parity = UART_PARITY_DISABLE;
if (this->parity_ == UART_CONFIG_PARITY_EVEN) {
@@ -150,20 +157,26 @@ void IDFUARTComponent::load_settings(bool dump_config) {
// Commit 9ed617fb17 removed gpio_func_sel() calls from uart_set_pin(), which breaks
// UART on default UART0 pins that may have residual state from boot console.
// Reset these pins before configuring UART to ensure they're in a clean state.
if (tx == U0TXD_GPIO_NUM || tx == U0RXD_GPIO_NUM) {
if (is_default_uart0_pin(tx)) {
gpio_reset_pin(static_cast<gpio_num_t>(tx));
}
if (rx == U0TXD_GPIO_NUM || rx == U0RXD_GPIO_NUM) {
if (is_default_uart0_pin(rx)) {
gpio_reset_pin(static_cast<gpio_num_t>(rx));
}
// Setup pins after reset to preserve open drain/pullup/pulldown flags
// Setup pins after reset to configure GPIO direction and pull resistors.
// For UART0 default pins, setup() must always be called because gpio_reset_pin()
// above sets GPIO_MODE_DISABLE which disables the input buffer. Without setup(),
// uart_set_pin() on ESP-IDF 5.4.2+ does not re-enable the input buffer for
// IOMUX-connected pins, so the RX pin cannot receive data (see issue #10132).
// For other pins, only call setup() if pull or open-drain flags are set to avoid
// disturbing the default pin state which breaks some external components (#11823).
auto setup_pin_if_needed = [](InternalGPIOPin *pin) {
if (!pin) {
return;
}
const auto mask = gpio::Flags::FLAG_OPEN_DRAIN | gpio::Flags::FLAG_PULLUP | gpio::Flags::FLAG_PULLDOWN;
if ((pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) {
if (is_default_uart0_pin(pin->get_pin()) || (pin->get_flags() & mask) != gpio::Flags::FLAG_NONE) {
pin->setup();
}
};

View File

@@ -14,6 +14,7 @@ import esphome.config_validation as cv
from esphome.const import CONF_DATA, CONF_ID, CONF_PORT, CONF_TRIGGER_ID
from esphome.core import ID
from esphome.cpp_generator import MockObj
from esphome.types import ConfigType
CODEOWNERS = ["@clydebarrow"]
DEPENDENCIES = ["network"]
@@ -65,33 +66,47 @@ RELOCATED = {
)
}
CONFIG_SCHEMA = cv.COMPONENT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(UDPComponent),
cv.Optional(CONF_PORT, default=18511): cv.Any(
cv.port,
cv.Schema(
def _consume_udp_sockets(config: ConfigType) -> ConfigType:
"""Register socket needs for UDP component."""
from esphome.components import socket
# UDP uses up to 2 sockets: 1 broadcast + 1 listen
# Whether each is used depends on code generation, so register worst case
socket.consume_sockets(2, "udp")(config)
return config
CONFIG_SCHEMA = cv.All(
cv.COMPONENT_SCHEMA.extend(
{
cv.GenerateID(): cv.declare_id(UDPComponent),
cv.Optional(CONF_PORT, default=18511): cv.Any(
cv.port,
cv.Schema(
{
cv.Required(CONF_LISTEN_PORT): cv.port,
cv.Required(CONF_BROADCAST_PORT): cv.port,
}
),
),
cv.Optional(
CONF_LISTEN_ADDRESS, default="255.255.255.255"
): cv.ipv4address_multi_broadcast,
cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list(
cv.ipv4address,
),
cv.Optional(CONF_ON_RECEIVE): automation.validate_automation(
{
cv.Required(CONF_LISTEN_PORT): cv.port,
cv.Required(CONF_BROADCAST_PORT): cv.port,
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
Trigger.template(trigger_args)
),
}
),
),
cv.Optional(
CONF_LISTEN_ADDRESS, default="255.255.255.255"
): cv.ipv4address_multi_broadcast,
cv.Optional(CONF_ADDRESSES, default=["255.255.255.255"]): cv.ensure_list(
cv.ipv4address,
),
cv.Optional(CONF_ON_RECEIVE): automation.validate_automation(
{
cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
Trigger.template(trigger_args)
),
}
),
}
).extend(RELOCATED)
}
).extend(RELOCATED),
_consume_udp_sockets,
)
async def register_udp_client(var, config):

View File

@@ -144,9 +144,10 @@ def _consume_web_server_sockets(config: ConfigType) -> ConfigType:
"""Register socket needs for web_server component."""
from esphome.components import socket
# Web server needs 1 listening socket + typically 2 concurrent client connections
# (browser makes 2 connections for page + event stream)
sockets_needed = 3
# Web server needs 1 listening socket + typically 5 concurrent client connections
# (browser opens connections for page resources, SSE event stream, and POST
# requests for entity control which may linger before closing)
sockets_needed = 6
socket.consume_sockets(sockets_needed, "web_server")(config)
return config

View File

@@ -1913,6 +1913,9 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe
JsonArray modes = root[ESPHOME_F("modes")].to<JsonArray>();
for (auto m : traits.get_supported_modes())
modes.add(PSTR_LOCAL(water_heater::water_heater_mode_to_string(m)));
root[ESPHOME_F("min_temp")] = traits.get_min_temperature();
root[ESPHOME_F("max_temp")] = traits.get_max_temperature();
root[ESPHOME_F("step")] = traits.get_target_temperature_step();
this->add_sorting_info_(root, obj);
}
@@ -1935,10 +1938,6 @@ std::string WebServer::water_heater_json_(water_heater::WaterHeater *obj, JsonDe
root[ESPHOME_F("target_temperature")] = target;
}
root[ESPHOME_F("min_temperature")] = traits.get_min_temperature();
root[ESPHOME_F("max_temperature")] = traits.get_max_temperature();
root[ESPHOME_F("step")] = traits.get_target_temperature_step();
if (traits.get_supports_away_mode()) {
root[ESPHOME_F("away")] = obj->is_away();
}

View File

@@ -1,4 +1,5 @@
import logging
import math
from esphome import automation
from esphome.automation import Condition
@@ -493,6 +494,13 @@ async def to_code(config):
cg.add(var.set_passive_scan(True))
if CONF_OUTPUT_POWER in config:
cg.add(var.set_output_power(config[CONF_OUTPUT_POWER]))
if CORE.is_esp32:
# Set PHY max TX power to match output_power so calibration also uses
# reduced power. This prevents brownout during PHY init on marginal
# power supplies, which is critical for OTA updates with rollback enabled.
# Kconfig range is 10-20, ESPHome allows 8.5-20.5
phy_tx_power = max(10, min(20, math.ceil(config[CONF_OUTPUT_POWER])))
add_idf_sdkconfig_option("CONFIG_ESP_PHY_MAX_WIFI_TX_POWER", phy_tx_power)
# enable_on_boot defaults to true in C++ - only set if false
if not config[CONF_ENABLE_ON_BOOT]:
cg.add(var.set_enable_on_boot(False))

View File

@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2026.2.0"
__version__ = "2026.2.1"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -0,0 +1,5 @@
<<: !include common.yaml
esp32_ble:
io_capability: keyboard_only
disable_bt_logs: false

View File

@@ -0,0 +1,61 @@
#pragma once
#include <cstdint>
#include <cstring>
#include <vector>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "esphome/components/ld2450/ld2450.h"
#include "esphome/components/uart/uart_component.h"
namespace esphome::ld2450::testing {
// Mock UART component to satisfy UARTDevice parent requirement.
class MockUARTComponent : public uart::UARTComponent {
public:
void write_array(const uint8_t *data, size_t len) override {}
MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override));
MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override));
MOCK_METHOD(size_t, available, (), (override));
MOCK_METHOD(void, flush, (), (override));
MOCK_METHOD(void, check_logger_conflict, (), (override));
};
// Expose protected members for testing.
class TestableLD2450 : public LD2450Component {
public:
using LD2450Component::buffer_data_;
using LD2450Component::buffer_pos_;
using LD2450Component::readline_;
void feed(const std::vector<uint8_t> &data) {
for (uint8_t byte : data) {
this->readline_(byte);
}
}
};
// LD2450 periodic data frame: header (4) + 3 targets * 8 bytes + footer (2) = 30 bytes
// All-zero targets means no presence detected.
inline std::vector<uint8_t> make_periodic_frame(uint8_t fill = 0x00) {
std::vector<uint8_t> frame = {0xAA, 0xFF, 0x03, 0x00}; // DATA_FRAME_HEADER
for (int i = 0; i < 24; i++) {
frame.push_back(fill); // 3 targets * 8 bytes
}
frame.push_back(0x55); // DATA_FRAME_FOOTER
frame.push_back(0xCC);
return frame;
}
// LD2450 command ACK frame for CMD_ENABLE_CONF (0xFF), successful.
// header (4) + length (2) + command (2) + result (2) + footer (4) = 14 bytes
inline std::vector<uint8_t> make_ack_frame() {
return {
0xFD, 0xFC, 0xFB, 0xFA, // CMD_FRAME_HEADER
0x04, 0x00, // length = 4
0xFF, 0x01, // command = enable_conf, status = success
0x00, 0x00, // result = ok
0x04, 0x03, 0x02, 0x01 // CMD_FRAME_FOOTER
};
}
} // namespace esphome::ld2450::testing

View File

@@ -0,0 +1,145 @@
#include "common.h"
namespace esphome::ld2450::testing {
class LD2450ReadlineTest : public ::testing::Test {
protected:
void SetUp() override {
this->ld2450_.set_uart_parent(&this->mock_uart_);
// Ensure clean state
ASSERT_EQ(this->ld2450_.buffer_pos_, 0);
}
MockUARTComponent mock_uart_;
TestableLD2450 ld2450_;
};
// --- Good data tests ---
TEST_F(LD2450ReadlineTest, ValidPeriodicFrame) {
auto frame = make_periodic_frame();
this->ld2450_.feed(frame);
// After a complete valid frame, buffer should be reset
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
}
TEST_F(LD2450ReadlineTest, ValidCommandAckFrame) {
auto frame = make_ack_frame();
this->ld2450_.feed(frame);
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
}
TEST_F(LD2450ReadlineTest, BackToBackPeriodicFrames) {
auto frame = make_periodic_frame();
for (int i = 0; i < 5; i++) {
this->ld2450_.feed(frame);
EXPECT_EQ(this->ld2450_.buffer_pos_, 0) << "Frame " << i << " not processed";
}
}
TEST_F(LD2450ReadlineTest, BackToBackMixedFrames) {
auto periodic = make_periodic_frame();
auto ack = make_ack_frame();
this->ld2450_.feed(periodic);
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
this->ld2450_.feed(ack);
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
this->ld2450_.feed(periodic);
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
}
// --- Garbage then valid frame tests ---
TEST_F(LD2450ReadlineTest, GarbageThenValidFrame) {
// Garbage bytes accumulate in the buffer but don't match any footer.
// A valid frame follows; its footer resets the buffer and resyncs.
std::vector<uint8_t> garbage = {0x01, 0x02, 0x03, 0x42, 0x99};
this->ld2450_.feed(garbage);
EXPECT_GT(this->ld2450_.buffer_pos_, 0); // Garbage accumulated
auto frame = make_periodic_frame();
this->ld2450_.feed(frame);
// Footer from the valid frame resyncs the parser
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
}
// --- Footer-based resynchronization tests ---
TEST_F(LD2450ReadlineTest, FooterInGarbageResyncs) {
// Garbage containing a periodic frame footer (0x55 0xCC) triggers
// a buffer reset, allowing the next frame to be parsed cleanly.
std::vector<uint8_t> garbage_with_footer = {0x01, 0x02, 0x03, 0x04, 0x55, 0xCC};
this->ld2450_.feed(garbage_with_footer);
EXPECT_EQ(this->ld2450_.buffer_pos_, 0); // Footer reset the buffer
auto frame = make_periodic_frame();
this->ld2450_.feed(frame);
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
}
TEST_F(LD2450ReadlineTest, CmdFooterInGarbageResyncs) {
// Garbage containing a command frame footer (04 03 02 01) also resyncs.
std::vector<uint8_t> garbage_with_footer = {0x10, 0x20, 0x30, 0x40, 0x04, 0x03, 0x02, 0x01};
this->ld2450_.feed(garbage_with_footer);
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
auto frame = make_periodic_frame();
this->ld2450_.feed(frame);
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
}
// --- Overflow recovery tests ---
TEST_F(LD2450ReadlineTest, OverflowResetsBuffer) {
// Fill the buffer to capacity with filler that won't match any footer.
// MAX_LINE_LENGTH is 45, usable is 44. The 45th byte triggers overflow.
std::vector<uint8_t> overflow_data(MAX_LINE_LENGTH, 0x11);
this->ld2450_.feed(overflow_data);
// After overflow, buffer_pos_ resets to 0 (via the < 4 early return path)
EXPECT_LT(this->ld2450_.buffer_pos_, 4);
}
TEST_F(LD2450ReadlineTest, OverflowThenValidFrame) {
// Overflow, then a valid frame should be processed.
std::vector<uint8_t> overflow_data(MAX_LINE_LENGTH, 0x11);
this->ld2450_.feed(overflow_data);
auto frame = make_periodic_frame();
this->ld2450_.feed(frame);
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
}
TEST_F(LD2450ReadlineTest, BufferLargeEnoughForDesyncedFooter) {
// The key fix: the buffer (45) is large enough that a desynced periodic frame's
// footer (at most 30 bytes into the stream) will land inside the buffer before overflow.
// Simulate starting 10 bytes into a periodic frame, then a full frame follows.
std::vector<uint8_t> mid_frame = {0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39};
// Then a complete periodic frame whose footer will land at position 40 (10 + 30),
// well within the buffer size of 45.
auto frame = make_periodic_frame();
mid_frame.insert(mid_frame.end(), frame.begin(), frame.end());
this->ld2450_.feed(mid_frame);
// The footer from the frame should have triggered a reset
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
}
TEST_F(LD2450ReadlineTest, SimulatedRestartThenFrames) {
// Simulate LD2450 restart: burst of garbage followed by valid periodic frames.
// The garbage + first frame should fit in the buffer so the footer resyncs.
std::vector<uint8_t> restart_noise = {
0x30, 0x31, 0x32, 0x33, 0x34, 0x35, 0x36, 0x37, // 8 bytes of mid-frame data
};
auto frame = make_periodic_frame();
// 8 garbage + 30 frame = 38 bytes, well within buffer of 45
restart_noise.insert(restart_noise.end(), frame.begin(), frame.end());
this->ld2450_.feed(restart_noise);
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
// Subsequent frames should work normally
this->ld2450_.feed(frame);
EXPECT_EQ(this->ld2450_.buffer_pos_, 0);
}
} // namespace esphome::ld2450::testing

View File

@@ -0,0 +1,10 @@
sensor:
- platform: pulse_counter
name: Pulse Counter
pin: 4
use_pcnt: false
count_mode:
rising_edge: INCREMENT
falling_edge: DECREMENT
internal_filter: 13us
update_interval: 15s

View File

@@ -0,0 +1,11 @@
substitutions:
network_enable_ipv6: "false"
socket:
wifi:
ssid: MySSID
password: password1
network:
enable_ipv6: ${network_enable_ipv6}

View File

@@ -0,0 +1,4 @@
substitutions:
network_enable_ipv6: "true"
<<: !include common.yaml

View File

@@ -0,0 +1,4 @@
socket:
network:
enable_ipv6: true

View File

@@ -0,0 +1 @@
<<: !include common.yaml

View File

@@ -0,0 +1,3 @@
socket:
network: