Compare commits

..

9 Commits

Author SHA1 Message Date
Jesse Hills
afa4047089 Merge pull request #13984 from esphome/bump-2026.2.0b2
2026.2.0b2
2026-02-14 16:02:49 +13:00
Jesse Hills
a8a324cbfb Bump version to 2026.2.0b2 2026-02-14 13:53:54 +13:00
J. Nick Koston
f6aeef2e68 [api] Fix ESP8266 noise API handshake deadlock and prompt socket cleanup (#13972) 2026-02-14 13:53:54 +13:00
Jonathan Swoboda
297dfb0db4 [docker] Suppress git detached HEAD advice (#13962)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 13:53:54 +13:00
Jonathan Swoboda
c08356b0c1 [alarm_control_panel] Fix flaky integration test race condition (#13964)
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-14 13:53:54 +13:00
Lukáš Maňas
e9bf9bc691 [pulse_meter] Fix early edge detection (#12360)
Co-authored-by: pre-commit-ci-lite[bot] <117423508+pre-commit-ci-lite[bot]@users.noreply.github.com>
Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com>
2026-02-14 13:53:54 +13:00
J. Nick Koston
ead7937dbf [api] Extract cold code from APIServer::loop() hot path (#13902) 2026-02-14 13:53:54 +13:00
J. Nick Koston
844210519a [uart] Remove redundant mutex, fix flush race, conditional event queue (#13955) 2026-02-14 13:53:54 +13:00
Guillermo Ruffino
7c70b2e04e [schema-gen] fix Windows: ensure UTF-8 encoding when reading component files (#13952) 2026-02-14 13:53:54 +13:00
35 changed files with 249 additions and 479 deletions

View File

@@ -47,7 +47,7 @@ runs:
- name: Build and push to ghcr by digest
id: build-ghcr
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false
@@ -73,7 +73,7 @@ runs:
- name: Build and push to dockerhub by digest
id: build-dockerhub
uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2
uses: docker/build-push-action@601a80b39c9405e50806ae38af30926f9d957c47 # v6.19.1
env:
DOCKER_BUILD_SUMMARY: false
DOCKER_BUILD_RECORD_UPLOAD: false

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.3.0-dev
PROJECT_NUMBER = 2026.2.0b2
# 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

@@ -256,7 +256,7 @@ SYMBOL_PATTERNS = {
"ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"],
# Order matters! More specific categories must come before general ones.
# mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern
"mdns_lib": ["mdns", "packet$"],
"mdns_lib": ["mdns"],
# memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols
"memory_mgmt": [
"mem_",
@@ -794,6 +794,7 @@ SYMBOL_PATTERNS = {
"s_dp",
"s_ni",
"s_reg_dump",
"packet$",
"d_mult_table",
"K",
"fcstab",

View File

@@ -57,14 +57,8 @@ def maybe_conf(conf, *validators):
return validate
def register_action(
name: str,
action_type: MockObjClass,
schema: cv.Schema,
*,
deferred: bool = False,
):
return ACTION_REGISTRY.register(name, action_type, schema, deferred=deferred)
def register_action(name: str, action_type: MockObjClass, schema: cv.Schema):
return ACTION_REGISTRY.register(name, action_type, schema)
def register_condition(name: str, condition_type: MockObjClass, schema: cv.Schema):
@@ -341,10 +335,7 @@ async def component_is_idle_condition_to_code(
@register_action(
"delay",
DelayAction,
cv.templatable(cv.positive_time_period_milliseconds),
deferred=True,
"delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
)
async def delay_action_to_code(
config: ConfigType,
@@ -454,7 +445,7 @@ _validate_wait_until = cv.maybe_simple_value(
)
@register_action("wait_until", WaitUntilAction, _validate_wait_until, deferred=True)
@register_action("wait_until", WaitUntilAction, _validate_wait_until)
async def wait_until_action_to_code(
config: ConfigType,
action_id: ID,
@@ -587,26 +578,6 @@ async def build_condition_list(
return conditions
def has_deferred_actions(actions: ConfigType) -> bool:
"""Check if a validated action list contains any deferred actions.
Deferred actions (delay, wait_until, script.wait) store trigger args
for later execution, making non-owning types like StringRef unsafe.
"""
if isinstance(actions, list):
return any(has_deferred_actions(item) for item in actions)
if isinstance(actions, dict):
for key in actions:
if key in ACTION_REGISTRY and ACTION_REGISTRY[key].deferred:
return True
return any(
has_deferred_actions(v)
for v in actions.values()
if isinstance(v, (list, dict))
)
return False
async def build_automation(
trigger: MockObj, args: TemplateArgsType, config: ConfigType
) -> MockObj:

View File

@@ -76,7 +76,7 @@ SERVICE_ARG_NATIVE_TYPES: dict[str, MockObj] = {
"bool": cg.bool_,
"int": cg.int32,
"float": cg.float_,
"string": cg.StringRef,
"string": cg.std_string,
"bool[]": cg.FixedVector.template(cg.bool_).operator("const").operator("ref"),
"int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"),
"float[]": cg.FixedVector.template(cg.float_).operator("const").operator("ref"),
@@ -380,16 +380,9 @@ async def to_code(config: ConfigType) -> None:
if is_optional:
func_args.append((cg.bool_, "return_response"))
# Check if action chain has deferred actions that would make
# non-owning StringRef dangle (rx_buf_ reused after delay)
has_deferred = automation.has_deferred_actions(conf.get(CONF_THEN, []))
service_arg_names: list[str] = []
for name, var_ in conf[CONF_VARIABLES].items():
native = SERVICE_ARG_NATIVE_TYPES[var_]
# Fall back to std::string for string args if deferred actions exist
if has_deferred and native is cg.StringRef:
native = cg.std_string
service_template_args.append(native)
func_args.append((native, name))
service_arg_names.append(name)

View File

@@ -824,7 +824,7 @@ message HomeAssistantStateResponse {
option (ifdef) = "USE_API_HOMEASSISTANT_STATES";
string entity_id = 1;
string state = 2 [(null_terminate) = true];
string state = 2;
string attribute = 3;
}
@@ -882,7 +882,7 @@ message ExecuteServiceArgument {
bool bool_ = 1;
int32 legacy_int = 2;
float float_ = 3;
string string_ = 4 [(null_terminate) = true];
string string_ = 4;
// ESPHome 1.14 (api v1.3) make int a signed value
sint32 int_ = 5;
repeated bool bool_array = 6 [packed=false, (fixed_vector) = true];

View File

@@ -1683,18 +1683,31 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes
}
for (auto &it : this->parent_->get_state_subs()) {
if (msg.entity_id != it.entity_id) {
// Compare entity_id: check length matches and content matches
size_t entity_id_len = strlen(it.entity_id);
if (entity_id_len != msg.entity_id.size() ||
memcmp(it.entity_id, msg.entity_id.c_str(), msg.entity_id.size()) != 0) {
continue;
}
// Compare attribute: either both have matching attribute, or both have none
// it.attribute can be nullptr (meaning no attribute filter)
if (it.attribute != nullptr ? msg.attribute != it.attribute : !msg.attribute.empty()) {
size_t sub_attr_len = it.attribute != nullptr ? strlen(it.attribute) : 0;
if (sub_attr_len != msg.attribute.size() ||
(sub_attr_len > 0 && memcmp(it.attribute, msg.attribute.c_str(), sub_attr_len) != 0)) {
continue;
}
// msg.state is already null-terminated in-place after protobuf decode
it.callback(msg.state);
// Create null-terminated state for callback (parse_number needs null-termination)
// HA state max length is 255 characters, but attributes can be much longer
// Use stack buffer for common case (states), heap fallback for large attributes
size_t state_len = msg.state.size();
SmallBufferWithHeapFallback<MAX_STATE_LEN + 1> state_buf_alloc(state_len + 1);
char *state_buf = reinterpret_cast<char *>(state_buf_alloc.get());
if (state_len > 0) {
memcpy(state_buf, msg.state.c_str(), state_len);
}
state_buf[state_len] = '\0';
it.callback(StringRef(state_buf, state_len));
}
}
#endif
@@ -1851,8 +1864,6 @@ void APIConnection::on_fatal_error() {
this->flags_.remove = true;
}
void __attribute__((flatten)) APIConnection::DeferredBatch::push_item(const BatchItem &item) { items.push_back(item); }
void APIConnection::DeferredBatch::add_item(EntityBase *entity, uint8_t message_type, uint8_t estimated_size,
uint8_t aux_data_index) {
// Check if we already have a message of this type for this entity
@@ -1869,7 +1880,7 @@ void APIConnection::DeferredBatch::add_item(EntityBase *entity, uint8_t message_
}
}
// No existing item found (or event), add new one
this->push_item({entity, message_type, estimated_size, aux_data_index});
items.push_back({entity, message_type, estimated_size, aux_data_index});
}
void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size) {
@@ -1877,7 +1888,7 @@ void APIConnection::DeferredBatch::add_item_front(EntityBase *entity, uint8_t me
// This avoids expensive vector::insert which shifts all elements
// Note: We only ever have one high-priority message at a time (ping OR disconnect)
// If we're disconnecting, pings are blocked, so this simple swap is sufficient
this->push_item({entity, message_type, estimated_size, AUX_DATA_UNUSED});
items.push_back({entity, message_type, estimated_size, AUX_DATA_UNUSED});
if (items.size() > 1) {
// Swap the new high-priority item to the front
std::swap(items.front(), items.back());

View File

@@ -541,8 +541,6 @@ class APIConnection final : public APIServerConnectionBase {
uint8_t aux_data_index = AUX_DATA_UNUSED);
// Add item to the front of the batch (for high priority messages like ping)
void add_item_front(EntityBase *entity, uint8_t message_type, uint8_t estimated_size);
// Single push_back site to avoid duplicate _M_realloc_insert instantiation
void push_item(const BatchItem &item);
// Clear all items
void clear() {

View File

@@ -201,10 +201,9 @@ APIError APINoiseFrameHelper::try_read_frame_() {
return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
}
// Reserve space for body (+1 for null terminator so protobuf StringRef fields
// can be safely null-terminated in-place after decode)
if (this->rx_buf_.size() != msg_size + 1) {
this->rx_buf_.resize(msg_size + 1);
// Reserve space for body
if (this->rx_buf_.size() != msg_size) {
this->rx_buf_.resize(msg_size);
}
if (rx_buf_len_ < msg_size) {

View File

@@ -163,10 +163,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_() {
}
// header reading done
// Reserve space for body (+1 for null terminator so protobuf StringRef fields
// can be safely null-terminated in-place after decode)
if (this->rx_buf_.size() != this->rx_header_parsed_len_ + 1) {
this->rx_buf_.resize(this->rx_header_parsed_len_ + 1);
// Reserve space for body
if (this->rx_buf_.size() != this->rx_header_parsed_len_) {
this->rx_buf_.resize(this->rx_header_parsed_len_);
}
if (rx_buf_len_ < rx_header_parsed_len_) {

View File

@@ -90,13 +90,4 @@ extend google.protobuf.FieldOptions {
// - uint16_t <field>_length_{0};
// - uint16_t <field>_count_{0};
optional bool packed_buffer = 50015 [default=false];
// null_terminate: Write a null byte after string data in the decode buffer.
// When set on a string field in a SOURCE_CLIENT (decodable) message, the
// generated decode() override writes '\0' at data[length] after decoding.
// This makes the StringRef safe for c_str() usage without copying.
// Safe because: (1) frame helpers reserve +1 byte in rx_buf_, and
// (2) the overwritten byte was already consumed during decode.
// Only mark fields that actually need null-terminated access.
optional bool null_terminate = 50016 [default=false];
}

View File

@@ -953,12 +953,6 @@ bool HomeAssistantStateResponse::decode_length(uint32_t field_id, ProtoLengthDel
}
return true;
}
void HomeAssistantStateResponse::decode(const uint8_t *buffer, size_t length) {
ProtoDecodableMessage::decode(buffer, length);
if (!this->state.empty()) {
const_cast<char *>(this->state.c_str())[this->state.size()] = '\0';
}
}
#endif
bool GetTimeResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
@@ -1063,9 +1057,6 @@ void ExecuteServiceArgument::decode(const uint8_t *buffer, size_t length) {
uint32_t count_string_array = ProtoDecodableMessage::count_repeated_field(buffer, length, 9);
this->string_array.init(count_string_array);
ProtoDecodableMessage::decode(buffer, length);
if (!this->string_.empty()) {
const_cast<char *>(this->string_.c_str())[this->string_.size()] = '\0';
}
}
bool ExecuteServiceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {

View File

@@ -1095,7 +1095,6 @@ class HomeAssistantStateResponse final : public ProtoDecodableMessage {
StringRef entity_id{};
StringRef state{};
StringRef attribute{};
void decode(const uint8_t *buffer, size_t length) override;
#ifdef HAS_PROTO_MESSAGE_DUMP
const char *dump_to(DumpBuffer &out) const override;
#endif

View File

@@ -199,7 +199,7 @@ void APIServer::remove_client_(size_t client_index) {
#endif
}
void __attribute__((flatten)) APIServer::accept_new_connections_() {
void APIServer::accept_new_connections_() {
while (true) {
struct sockaddr_storage source_addr;
socklen_t addr_len = sizeof(source_addr);

View File

@@ -1,6 +1,5 @@
#include "user_services.h"
#include "esphome/core/log.h"
#include "esphome/core/string_ref.h"
namespace esphome::api {
@@ -12,8 +11,6 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &
}
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
// Zero-copy StringRef version for YAML-generated services (string_ is null-terminated after decode)
template<> StringRef get_execute_arg_value<StringRef>(const ExecuteServiceArgument &arg) { return arg.string_; }
// Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
@@ -64,8 +61,6 @@ template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SER
template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; }
template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; }
template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; }
// Zero-copy StringRef version for YAML-generated services
template<> enums::ServiceArgType to_service_arg_type<StringRef>() { return enums::SERVICE_ARG_TYPE_STRING; }
// Legacy std::vector versions for external components using custom_api_device.h
template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; }

View File

@@ -47,8 +47,8 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
request->send(stream);
}
void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
const auto &ssid = request->arg("ssid");
const auto &psk = request->arg("psk");
std::string ssid = request->arg("ssid").c_str(); // NOLINT(readability-redundant-string-cstr)
std::string psk = request->arg("psk").c_str(); // NOLINT(readability-redundant-string-cstr)
ESP_LOGI(TAG,
"Requested WiFi Settings Change:\n"
" SSID='%s'\n"
@@ -56,10 +56,10 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
ssid.c_str(), psk.c_str());
#ifdef USE_ESP8266
// ESP8266 is single-threaded, call directly
wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str());
wifi::global_wifi_component->save_wifi_sta(ssid, psk);
#else
// Defer save to main loop thread to avoid NVS operations from HTTP thread
this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid.c_str(), psk.c_str()); });
this->defer([ssid, psk]() { wifi::global_wifi_component->save_wifi_sta(ssid, psk); });
#endif
request->redirect(ESPHOME_F("/?save"));
}

View File

@@ -110,8 +110,6 @@ class EthernetComponent : public Component {
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();

View File

@@ -1,17 +1,4 @@
from esphome.components.mipi import (
ETMOD,
FRMCTR2,
GMCTRN1,
GMCTRP1,
IFCTR,
MODE_RGB,
PWCTR1,
PWCTR3,
PWCTR4,
PWCTR5,
PWSET,
DriverChip,
)
from esphome.components.mipi import DriverChip
import esphome.config_validation as cv
from .amoled import CO5300
@@ -142,16 +129,6 @@ DriverChip(
),
),
)
ST7789P = DriverChip(
"ST7789P",
# Max supported dimensions
width=240,
height=320,
# SPI: RGB layout
color_order=MODE_RGB,
invert_colors=True,
draw_rounding=1,
)
ILI9488_A.extend(
"PICO-RESTOUCH-LCD-3.5",
@@ -185,61 +162,3 @@ AXS15231.extend(
cs_pin=9,
reset_pin=21,
)
# Waveshare 1.83-v2
#
# Do not use on 1.83-v1: Vendor warning on different chip!
ST7789P.extend(
"WAVESHARE-1.83-V2",
# Panel size smaller than ST7789 max allowed
width=240,
height=284,
# Vendor specific init derived from vendor sample code
# "LCD_1.83_Code_Rev2/ESP32/LCD_1in83/LCD_Driver.cpp"
# Compatible MIT license, see esphome/LICENSE file.
initsequence=(
(FRMCTR2, 0x0C, 0x0C, 0x00, 0x33, 0x33),
(ETMOD, 0x35),
(0xBB, 0x19),
(PWCTR1, 0x2C),
(PWCTR3, 0x01),
(PWCTR4, 0x12),
(PWCTR5, 0x20),
(IFCTR, 0x0F),
(PWSET, 0xA4, 0xA1),
(
GMCTRP1,
0xD0,
0x04,
0x0D,
0x11,
0x13,
0x2B,
0x3F,
0x54,
0x4C,
0x18,
0x0D,
0x0B,
0x1F,
0x23,
),
(
GMCTRN1,
0xD0,
0x04,
0x0C,
0x11,
0x13,
0x2C,
0x3F,
0x44,
0x51,
0x2F,
0x1F,
0x1F,
0x20,
0x23,
),
),
)

View File

@@ -219,7 +219,6 @@ async def script_stop_action_to_code(config, action_id, template_arg, args):
"script.wait",
ScriptWaitAction,
maybe_simple_id({cv.Required(CONF_ID): cv.use_id(Script)}),
deferred=True,
)
async def script_wait_action_to_code(config, action_id, template_arg, args):
full_id, paren = await cg.get_variable_with_full_id(config[CONF_ID])

View File

@@ -148,7 +148,6 @@ class USBClient : public Component {
EventPool<UsbEvent, USB_EVENT_QUEUE_SIZE> event_pool;
protected:
void handle_open_state_();
TransferRequest *get_trq_(); // Lock-free allocation using atomic bitmask (multi-consumer safe)
virtual void disconnect();
virtual void on_connected() {}

View File

@@ -9,7 +9,6 @@
#include <cinttypes>
#include <cstring>
#include <atomic>
#include <span>
namespace esphome {
namespace usb_host {
@@ -143,23 +142,18 @@ static void usb_client_print_config_descriptor(const usb_config_desc_t *cfg_desc
} while (next_desc != NULL);
}
#endif
// USB string descriptors: bLength (uint8_t, max 255) includes the 2-byte header (bLength and bDescriptorType).
// Character count = (bLength - 2) / 2, max 126 chars + null terminator.
static constexpr size_t DESC_STRING_BUF_SIZE = 128;
static const char *get_descriptor_string(const usb_str_desc_t *desc, std::span<char, DESC_STRING_BUF_SIZE> buffer) {
if (desc == nullptr || desc->bLength < 2)
static std::string get_descriptor_string(const usb_str_desc_t *desc) {
char buffer[256];
if (desc == nullptr)
return "(unspecified)";
int char_count = (desc->bLength - 2) / 2;
char *p = buffer.data();
char *end = p + buffer.size() - 1;
for (int i = 0; i != char_count && p < end; i++) {
char *p = buffer;
for (int i = 0; i != desc->bLength / 2; i++) {
auto c = desc->wData[i];
if (c < 0x100)
*p++ = static_cast<char>(c);
}
*p = '\0';
return buffer.data();
return {buffer};
}
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
@@ -265,63 +259,60 @@ void USBClient::loop() {
ESP_LOGW(TAG, "Dropped %u USB events due to queue overflow", dropped);
}
if (this->state_ == USB_CLIENT_OPEN) {
this->handle_open_state_();
}
}
void USBClient::handle_open_state_() {
int err;
ESP_LOGD(TAG, "Open device %d", this->device_addr_);
err = usb_host_device_open(this->handle_, this->device_addr_, &this->device_handle_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err));
this->state_ = USB_CLIENT_INIT;
return;
}
ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_);
const usb_device_desc_t *desc;
err = usb_host_get_device_descriptor(this->device_handle_, &desc);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err));
this->disconnect();
return;
}
ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct);
if (desc->idVendor != this->vid_ || desc->idProduct != this->pid_) {
if (this->vid_ != 0 || this->pid_ != 0) {
ESP_LOGD(TAG, "Not our device, closing");
this->disconnect();
return;
}
}
usb_device_info_t dev_info;
err = usb_host_device_info(this->device_handle_, &dev_info);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err));
this->disconnect();
return;
}
this->state_ = USB_CLIENT_CONNECTED;
char buf_manuf[DESC_STRING_BUF_SIZE];
char buf_product[DESC_STRING_BUF_SIZE];
char buf_serial[DESC_STRING_BUF_SIZE];
ESP_LOGD(TAG, "Device connected: Manuf: %s; Prod: %s; Serial: %s",
get_descriptor_string(dev_info.str_desc_manufacturer, buf_manuf),
get_descriptor_string(dev_info.str_desc_product, buf_product),
get_descriptor_string(dev_info.str_desc_serial_num, buf_serial));
switch (this->state_) {
case USB_CLIENT_OPEN: {
int err;
ESP_LOGD(TAG, "Open device %d", this->device_addr_);
err = usb_host_device_open(this->handle_, this->device_addr_, &this->device_handle_);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Device open failed: %s", esp_err_to_name(err));
this->state_ = USB_CLIENT_INIT;
break;
}
ESP_LOGD(TAG, "Get descriptor device %d", this->device_addr_);
const usb_device_desc_t *desc;
err = usb_host_get_device_descriptor(this->device_handle_, &desc);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Device get_desc failed: %s", esp_err_to_name(err));
this->disconnect();
} else {
ESP_LOGD(TAG, "Device descriptor: vid %X pid %X", desc->idVendor, desc->idProduct);
if (desc->idVendor == this->vid_ && desc->idProduct == this->pid_ || this->vid_ == 0 && this->pid_ == 0) {
usb_device_info_t dev_info;
err = usb_host_device_info(this->device_handle_, &dev_info);
if (err != ESP_OK) {
ESP_LOGW(TAG, "Device info failed: %s", esp_err_to_name(err));
this->disconnect();
break;
}
this->state_ = USB_CLIENT_CONNECTED;
ESP_LOGD(TAG, "Device connected: Manuf: %s; Prod: %s; Serial: %s",
get_descriptor_string(dev_info.str_desc_manufacturer).c_str(),
get_descriptor_string(dev_info.str_desc_product).c_str(),
get_descriptor_string(dev_info.str_desc_serial_num).c_str());
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
const usb_device_desc_t *device_desc;
err = usb_host_get_device_descriptor(this->device_handle_, &device_desc);
if (err == ESP_OK)
usb_client_print_device_descriptor(device_desc);
const usb_config_desc_t *config_desc;
err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc);
if (err == ESP_OK)
usb_client_print_config_descriptor(config_desc, nullptr);
const usb_device_desc_t *device_desc;
err = usb_host_get_device_descriptor(this->device_handle_, &device_desc);
if (err == ESP_OK)
usb_client_print_device_descriptor(device_desc);
const usb_config_desc_t *config_desc;
err = usb_host_get_active_config_descriptor(this->device_handle_, &config_desc);
if (err == ESP_OK)
usb_client_print_config_descriptor(config_desc, nullptr);
#endif
this->on_connected();
this->on_connected();
} else {
ESP_LOGD(TAG, "Not our device, closing");
this->disconnect();
}
}
break;
}
default:
break;
}
}
void USBClient::on_opened(uint8_t addr) {

View File

@@ -198,8 +198,7 @@ EntityMatchResult UrlMatch::match_entity(EntityBase *entity) const {
#if !defined(USE_ESP32) && defined(USE_ARDUINO)
// helper for allowing only unique entries in the queue
void __attribute__((flatten))
DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) {
void DeferredUpdateEventSource::deq_push_back_with_dedup_(void *source, message_generator_t *message_generator) {
DeferredEvent item(source, message_generator);
// Use range-based for loop instead of std::find_if to reduce template instantiation overhead and binary size
@@ -558,9 +557,7 @@ static void set_json_id(JsonObject &root, EntityBase *obj, const char *prefix, J
root[ESPHOME_F("device")] = device_name;
}
#endif
#ifdef USE_ENTITY_ICON
root[ESPHOME_F("icon")] = obj->get_icon_ref();
#endif
root[ESPHOME_F("entity_category")] = obj->get_entity_category();
bool is_disabled = obj->is_disabled_by_default();
if (is_disabled)
@@ -586,7 +583,8 @@ static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const c
// Helper to get request detail parameter
static JsonDetail get_request_detail(AsyncWebServerRequest *request) {
return request->arg(ESPHOME_F("detail")) == "all" ? DETAIL_ALL : DETAIL_STATE;
auto *param = request->getParam(ESPHOME_F("detail"));
return (param && param->value() == "all") ? DETAIL_ALL : DETAIL_STATE;
}
#ifdef USE_SENSOR
@@ -863,10 +861,10 @@ void WebServer::handle_fan_request(AsyncWebServerRequest *request, const UrlMatc
}
auto call = is_on ? obj->turn_on() : obj->turn_off();
parse_num_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed);
parse_int_param_(request, ESPHOME_F("speed_level"), call, &decltype(call)::set_speed);
if (request->hasArg(ESPHOME_F("oscillation"))) {
auto speed = request->arg(ESPHOME_F("oscillation"));
if (request->hasParam(ESPHOME_F("oscillation"))) {
auto speed = request->getParam(ESPHOME_F("oscillation"))->value();
auto val = parse_on_off(speed.c_str());
switch (val) {
case PARSE_ON:
@@ -1042,14 +1040,14 @@ void WebServer::handle_cover_request(AsyncWebServerRequest *request, const UrlMa
}
auto traits = obj->get_traits();
if ((request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) ||
(request->hasArg(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) {
if ((request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) ||
(request->hasParam(ESPHOME_F("tilt")) && !traits.get_supports_tilt())) {
request->send(409);
return;
}
parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
parse_num_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt);
parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
parse_float_param_(request, ESPHOME_F("tilt"), call, &decltype(call)::set_tilt);
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1108,7 +1106,7 @@ void WebServer::handle_number_request(AsyncWebServerRequest *request, const UrlM
}
auto call = obj->make_call();
parse_num_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
parse_float_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_value);
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1176,13 +1174,12 @@ void WebServer::handle_date_request(AsyncWebServerRequest *request, const UrlMat
auto call = obj->make_call();
const auto &value = request->arg(ESPHOME_F("value"));
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
if (value.length() == 0) { // NOLINT(readability-container-size-empty)
if (!request->hasParam(ESPHOME_F("value"))) {
request->send(409);
return;
}
call.set_date(value.c_str(), value.length());
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_date);
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1237,13 +1234,12 @@ void WebServer::handle_time_request(AsyncWebServerRequest *request, const UrlMat
auto call = obj->make_call();
const auto &value = request->arg(ESPHOME_F("value"));
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
if (value.length() == 0) { // NOLINT(readability-container-size-empty)
if (!request->hasParam(ESPHOME_F("value"))) {
request->send(409);
return;
}
call.set_time(value.c_str(), value.length());
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_time);
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1297,13 +1293,12 @@ void WebServer::handle_datetime_request(AsyncWebServerRequest *request, const Ur
auto call = obj->make_call();
const auto &value = request->arg(ESPHOME_F("value"));
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
if (value.length() == 0) { // NOLINT(readability-container-size-empty)
if (!request->hasParam(ESPHOME_F("value"))) {
request->send(409);
return;
}
call.set_datetime(value.c_str(), value.length());
parse_string_param_(request, ESPHOME_F("value"), call, &decltype(call)::set_datetime);
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1482,14 +1477,10 @@ void WebServer::handle_climate_request(AsyncWebServerRequest *request, const Url
parse_string_param_(request, ESPHOME_F("swing_mode"), call, &decltype(call)::set_swing_mode);
// Parse temperature parameters
// static_cast needed to disambiguate overloaded setters (float vs optional<float>)
using ClimateCall = decltype(call);
parse_num_param_(request, ESPHOME_F("target_temperature_high"), call,
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_high));
parse_num_param_(request, ESPHOME_F("target_temperature_low"), call,
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature_low));
parse_num_param_(request, ESPHOME_F("target_temperature"), call,
static_cast<ClimateCall &(ClimateCall::*) (float)>(&ClimateCall::set_target_temperature));
parse_float_param_(request, ESPHOME_F("target_temperature_high"), call,
&decltype(call)::set_target_temperature_high);
parse_float_param_(request, ESPHOME_F("target_temperature_low"), call, &decltype(call)::set_target_temperature_low);
parse_float_param_(request, ESPHOME_F("target_temperature"), call, &decltype(call)::set_target_temperature);
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1730,12 +1721,12 @@ void WebServer::handle_valve_request(AsyncWebServerRequest *request, const UrlMa
}
auto traits = obj->get_traits();
if (request->hasArg(ESPHOME_F("position")) && !traits.get_supports_position()) {
if (request->hasParam(ESPHOME_F("position")) && !traits.get_supports_position()) {
request->send(409);
return;
}
parse_num_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
parse_float_param_(request, ESPHOME_F("position"), call, &decltype(call)::set_position);
DEFER_ACTION(call, call.perform());
request->send(200);
@@ -1879,12 +1870,12 @@ void WebServer::handle_water_heater_request(AsyncWebServerRequest *request, cons
parse_string_param_(request, ESPHOME_F("mode"), base_call, &water_heater::WaterHeaterCall::set_mode);
// Parse temperature parameters
parse_num_param_(request, ESPHOME_F("target_temperature"), base_call,
&water_heater::WaterHeaterCall::set_target_temperature);
parse_num_param_(request, ESPHOME_F("target_temperature_low"), base_call,
&water_heater::WaterHeaterCall::set_target_temperature_low);
parse_num_param_(request, ESPHOME_F("target_temperature_high"), base_call,
&water_heater::WaterHeaterCall::set_target_temperature_high);
parse_float_param_(request, ESPHOME_F("target_temperature"), base_call,
&water_heater::WaterHeaterCall::set_target_temperature);
parse_float_param_(request, ESPHOME_F("target_temperature_low"), base_call,
&water_heater::WaterHeaterCall::set_target_temperature_low);
parse_float_param_(request, ESPHOME_F("target_temperature_high"), base_call,
&water_heater::WaterHeaterCall::set_target_temperature_high);
// Parse away mode parameter
parse_bool_param_(request, ESPHOME_F("away"), base_call, &water_heater::WaterHeaterCall::set_away);
@@ -1988,16 +1979,16 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
auto call = obj->make_call();
// Parse carrier frequency (optional)
{
auto value = parse_number<uint32_t>(request->arg(ESPHOME_F("carrier_frequency")).c_str());
if (request->hasParam(ESPHOME_F("carrier_frequency"))) {
auto value = parse_number<uint32_t>(request->getParam(ESPHOME_F("carrier_frequency"))->value().c_str());
if (value.has_value()) {
call.set_carrier_frequency(*value);
}
}
// Parse repeat count (optional, defaults to 1)
{
auto value = parse_number<uint32_t>(request->arg(ESPHOME_F("repeat_count")).c_str());
if (request->hasParam(ESPHOME_F("repeat_count"))) {
auto value = parse_number<uint32_t>(request->getParam(ESPHOME_F("repeat_count"))->value().c_str());
if (value.has_value()) {
call.set_repeat_count(*value);
}
@@ -2005,12 +1996,18 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
// Parse base64url-encoded raw timings (required)
// Base64url is URL-safe: uses A-Za-z0-9-_ (no special characters needing escaping)
const auto &data_arg = request->arg(ESPHOME_F("data"));
if (!request->hasParam(ESPHOME_F("data"))) {
request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing 'data' parameter"));
return;
}
// Validate base64url is not empty (also catches missing parameter since arg() returns empty string)
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
if (data_arg.length() == 0) { // NOLINT(readability-container-size-empty)
request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Missing or empty 'data' parameter"));
// .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string
std::string encoded =
request->getParam(ESPHOME_F("data"))->value().c_str(); // NOLINT(readability-redundant-string-cstr)
// Validate base64url is not empty
if (encoded.empty()) {
request->send(400, ESPHOME_F("text/plain"), ESPHOME_F("Empty 'data' parameter"));
return;
}
@@ -2018,7 +2015,7 @@ void WebServer::handle_infrared_request(AsyncWebServerRequest *request, const Ur
// it outlives the call - set_raw_timings_base64url stores a pointer, so the string
// must remain valid until perform() completes.
// ESP8266 also needs this because ESPAsyncWebServer callbacks run in "sys" context.
this->defer([call, encoded = std::string(data_arg.c_str(), data_arg.length())]() mutable {
this->defer([call, encoded = std::move(encoded)]() mutable {
call.set_raw_timings_base64url(encoded);
call.perform();
});

View File

@@ -513,9 +513,11 @@ class WebServer : public Controller,
template<typename T, typename Ret>
void parse_light_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float),
float scale = 1.0f) {
auto value = parse_number<float>(request->arg(param_name).c_str());
if (value.has_value()) {
(call.*setter)(*value / scale);
if (request->hasParam(param_name)) {
auto value = parse_number<float>(request->getParam(param_name)->value().c_str());
if (value.has_value()) {
(call.*setter)(*value / scale);
}
}
}
@@ -523,19 +525,34 @@ class WebServer : public Controller,
template<typename T, typename Ret>
void parse_light_param_uint_(AsyncWebServerRequest *request, ParamNameType param_name, T &call,
Ret (T::*setter)(uint32_t), uint32_t scale = 1) {
auto value = parse_number<uint32_t>(request->arg(param_name).c_str());
if (value.has_value()) {
(call.*setter)(*value * scale);
if (request->hasParam(param_name)) {
auto value = parse_number<uint32_t>(request->getParam(param_name)->value().c_str());
if (value.has_value()) {
(call.*setter)(*value * scale);
}
}
}
#endif
// Generic helper to parse and apply a numeric parameter
template<typename NumT, typename T, typename Ret>
void parse_num_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(NumT)) {
auto value = parse_number<NumT>(request->arg(param_name).c_str());
if (value.has_value()) {
(call.*setter)(*value);
// Generic helper to parse and apply a float parameter
template<typename T, typename Ret>
void parse_float_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(float)) {
if (request->hasParam(param_name)) {
auto value = parse_number<float>(request->getParam(param_name)->value().c_str());
if (value.has_value()) {
(call.*setter)(*value);
}
}
}
// Generic helper to parse and apply an int parameter
template<typename T, typename Ret>
void parse_int_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(int)) {
if (request->hasParam(param_name)) {
auto value = parse_number<int>(request->getParam(param_name)->value().c_str());
if (value.has_value()) {
(call.*setter)(*value);
}
}
}
@@ -543,9 +560,10 @@ class WebServer : public Controller,
template<typename T, typename Ret>
void parse_string_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call,
Ret (T::*setter)(const std::string &)) {
if (request->hasArg(param_name)) {
const auto &value = request->arg(param_name);
(call.*setter)(std::string(value.c_str(), value.length()));
if (request->hasParam(param_name)) {
// .c_str() is required for Arduino framework where value() returns Arduino String instead of std::string
std::string value = request->getParam(param_name)->value().c_str(); // NOLINT(readability-redundant-string-cstr)
(call.*setter)(value);
}
}
@@ -555,9 +573,8 @@ class WebServer : public Controller,
// Invalid values are ignored (setter not called)
template<typename T, typename Ret>
void parse_bool_param_(AsyncWebServerRequest *request, ParamNameType param_name, T &call, Ret (T::*setter)(bool)) {
const auto &param_value = request->arg(param_name);
// Arduino String has isEmpty() not empty(), use length() for cross-platform compatibility
if (param_value.length() > 0) { // NOLINT(readability-container-size-empty)
if (request->hasParam(param_name)) {
auto param_value = request->getParam(param_name)->value();
// First check on/off (default), then true/false (custom)
auto val = parse_on_off(param_value.c_str());
if (val == PARSE_NONE) {

View File

@@ -1,13 +1,17 @@
#ifdef USE_ESP32
#include <memory>
#include <cstring>
#include <cctype>
#include "esphome/core/helpers.h"
#include "esphome/core/log.h"
#include "http_parser.h"
#include "utils.h"
namespace esphome::web_server_idf {
static const char *const TAG = "web_server_idf_utils";
size_t url_decode(char *str) {
char *start = str;
char *ptr = str, buf;
@@ -50,15 +54,32 @@ optional<std::string> request_get_header(httpd_req_t *req, const char *name) {
return {str};
}
optional<std::string> request_get_url_query(httpd_req_t *req) {
auto len = httpd_req_get_url_query_len(req);
if (len == 0) {
return {};
}
std::string str;
str.resize(len);
auto res = httpd_req_get_url_query_str(req, &str[0], len + 1);
if (res != ESP_OK) {
ESP_LOGW(TAG, "Can't get query for request: %s", esp_err_to_name(res));
return {};
}
return {str};
}
optional<std::string> query_key_value(const char *query_url, size_t query_len, const char *key) {
if (query_url == nullptr || query_len == 0) {
return {};
}
// Value can't exceed query_len. Use small stack buffer for typical values,
// heap fallback for long ones (e.g. base64 IR data) to limit stack usage
// since callers may also have stack buffers for the query string.
SmallBufferWithHeapFallback<128, char> val(query_len);
// Use stack buffer for typical query strings, heap fallback for large ones
SmallBufferWithHeapFallback<256, char> val(query_len);
if (httpd_query_key_value(query_url, key, val.get(), query_len) != ESP_OK) {
return {};
}
@@ -67,18 +88,6 @@ optional<std::string> query_key_value(const char *query_url, size_t query_len, c
return {val.get()};
}
bool query_has_key(const char *query_url, size_t query_len, const char *key) {
if (query_url == nullptr || query_len == 0) {
return false;
}
// Minimal buffer — we only care if the key exists, not the value
char buf[1];
// httpd_query_key_value returns ESP_OK if found, ESP_ERR_HTTPD_RESULT_TRUNC if found
// but value truncated (expected with 1-byte buffer), or other errors for invalid input
auto err = httpd_query_key_value(query_url, key, buf, sizeof(buf));
return err == ESP_OK || err == ESP_ERR_HTTPD_RESULT_TRUNC;
}
// Helper function for case-insensitive string region comparison
bool str_ncmp_ci(const char *s1, const char *s2, size_t n) {
for (size_t i = 0; i < n; i++) {

View File

@@ -13,8 +13,11 @@ size_t url_decode(char *str);
bool request_has_header(httpd_req_t *req, const char *name);
optional<std::string> request_get_header(httpd_req_t *req, const char *name);
optional<std::string> request_get_url_query(httpd_req_t *req);
optional<std::string> query_key_value(const char *query_url, size_t query_len, const char *key);
bool query_has_key(const char *query_url, size_t query_len, const char *key);
inline optional<std::string> query_key_value(const std::string &query_url, const std::string &key) {
return query_key_value(query_url.c_str(), query_url.size(), key.c_str());
}
// Helper function for case-insensitive character comparison
inline bool char_equals_ci(char a, char b) { return ::tolower(a) == ::tolower(b); }

View File

@@ -393,7 +393,13 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) {
}
// Look up value from query strings
auto val = this->find_query_value_(name);
optional<std::string> val = query_key_value(this->post_query_.c_str(), this->post_query_.size(), name);
if (!val.has_value()) {
auto url_query = request_get_url_query(*this);
if (url_query.has_value()) {
val = query_key_value(url_query.value().c_str(), url_query.value().size(), name);
}
}
// Don't cache misses to avoid wasting memory when handlers check for
// optional parameters that don't exist in the request
@@ -406,50 +412,6 @@ AsyncWebParameter *AsyncWebServerRequest::getParam(const char *name) {
return param;
}
/// Search post_query then URL query with a callback.
/// Returns first truthy result, or value-initialized default.
/// URL query is accessed directly from req->uri (same pattern as url_to()).
template<typename Func>
static auto search_query_sources(httpd_req_t *req, const std::string &post_query, const char *name, Func func)
-> decltype(func(nullptr, size_t{0}, name)) {
if (!post_query.empty()) {
auto result = func(post_query.c_str(), post_query.size(), name);
if (result) {
return result;
}
}
// Use httpd API for query length, then access string directly from URI.
// http_parser identifies components by offset/length without modifying the URI string.
// This is the same pattern used by url_to().
auto len = httpd_req_get_url_query_len(req);
if (len == 0) {
return {};
}
const char *query = strchr(req->uri, '?');
if (query == nullptr) {
return {};
}
query++; // skip '?'
return func(query, len, name);
}
optional<std::string> AsyncWebServerRequest::find_query_value_(const char *name) const {
return search_query_sources(this->req_, this->post_query_, name,
[](const char *q, size_t len, const char *k) { return query_key_value(q, len, k); });
}
bool AsyncWebServerRequest::hasArg(const char *name) {
return search_query_sources(this->req_, this->post_query_, name, query_has_key);
}
std::string AsyncWebServerRequest::arg(const char *name) {
auto val = this->find_query_value_(name);
if (val.has_value()) {
return std::move(val.value());
}
return {};
}
void AsyncWebServerResponse::addHeader(const char *name, const char *value) {
httpd_resp_set_hdr(*this->req_, name, value);
}

View File

@@ -116,8 +116,7 @@ class AsyncWebServerRequest {
/// Write URL (without query string) to buffer, returns StringRef pointing to buffer.
/// URL is decoded (e.g., %20 -> space).
StringRef url_to(std::span<char, URL_BUF_SIZE> buffer) const;
// Remove before 2026.9.0
ESPDEPRECATED("Use url_to() instead. Removed in 2026.9.0", "2026.3.0")
/// Get URL as std::string. Prefer url_to() to avoid heap allocation.
std::string url() const {
char buffer[URL_BUF_SIZE];
return std::string(this->url_to(buffer));
@@ -171,8 +170,14 @@ class AsyncWebServerRequest {
AsyncWebParameter *getParam(const std::string &name) { return this->getParam(name.c_str()); }
// NOLINTNEXTLINE(readability-identifier-naming)
bool hasArg(const char *name);
std::string arg(const char *name);
bool hasArg(const char *name) { return this->hasParam(name); }
std::string arg(const char *name) {
auto *param = this->getParam(name);
if (param) {
return param->value();
}
return {};
}
std::string arg(const std::string &name) { return this->arg(name.c_str()); }
operator httpd_req_t *() const { return this->req_; }
@@ -187,7 +192,6 @@ class AsyncWebServerRequest {
// is faster than tree/hash overhead. AsyncWebParameter stores both name and value to avoid
// duplicate storage. Only successful lookups are cached to prevent cache pollution when
// handlers check for optional parameters that don't exist.
optional<std::string> find_query_value_(const char *name) const;
std::vector<AsyncWebParameter *> params_;
std::string post_query_;
AsyncWebServerRequest(httpd_req_t *req) : req_(req) {}

View File

@@ -487,19 +487,6 @@ bool WiFiComponent::matches_configured_network_(const char *ssid, const uint8_t
return false;
}
void __attribute__((flatten)) WiFiComponent::set_sta_priority(bssid_t bssid, int8_t priority) {
for (auto &it : this->sta_priorities_) {
if (it.bssid == bssid) {
it.priority = priority;
return;
}
}
this->sta_priorities_.push_back(WiFiSTAPriority{
.bssid = bssid,
.priority = priority,
});
}
void WiFiComponent::log_discarded_scan_result_(const char *ssid, const uint8_t *bssid, int8_t rssi, uint8_t channel) {
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
// Skip logging during roaming scans to avoid log buffer overflow

View File

@@ -488,11 +488,20 @@ class WiFiComponent : public Component {
}
return 0;
}
void set_sta_priority(bssid_t bssid, int8_t priority);
void set_sta_priority(const bssid_t bssid, int8_t priority) {
for (auto &it : this->sta_priorities_) {
if (it.bssid == bssid) {
it.priority = priority;
return;
}
}
this->sta_priorities_.push_back(WiFiSTAPriority{
.bssid = bssid,
.priority = priority,
});
}
network::IPAddresses wifi_sta_ip_addresses();
// Remove before 2026.9.0
ESPDEPRECATED("Use wifi_ssid_to() instead. Removed in 2026.9.0", "2026.3.0")
std::string wifi_ssid();
/// Write SSID to buffer without heap allocation.
/// Returns pointer to buffer, or empty string if not connected.

View File

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

View File

@@ -1083,9 +1083,6 @@ template<std::size_t N> std::string format_hex(const std::array<uint8_t, N> &dat
* Each byte is displayed as a two-digit uppercase hex value, separated by the specified separator.
* Optionally includes the total byte count in parentheses at the end.
*
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
* Causes heap fragmentation on long-running devices.
*
* @param data Pointer to the byte array to format.
* @param length Number of bytes in the array.
* @param separator Character to use between hex bytes (default: '.').
@@ -1111,9 +1108,6 @@ std::string format_hex_pretty(const uint8_t *data, size_t length, char separator
*
* Similar to the byte array version, but formats 16-bit words as 4-digit hex values.
*
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
* Causes heap fragmentation on long-running devices.
*
* @param data Pointer to the 16-bit word array to format.
* @param length Number of 16-bit words in the array.
* @param separator Character to use between hex words (default: '.').
@@ -1137,9 +1131,6 @@ std::string format_hex_pretty(const uint16_t *data, size_t length, char separato
* Convenience overload for std::vector<uint8_t>. Formats each byte as a two-digit
* uppercase hex value with customizable separator.
*
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
* Causes heap fragmentation on long-running devices.
*
* @param data Vector of bytes to format.
* @param separator Character to use between hex bytes (default: '.').
* @param show_length Whether to append the byte count in parentheses (default: true).
@@ -1163,9 +1154,6 @@ std::string format_hex_pretty(const std::vector<uint8_t> &data, char separator =
* Convenience overload for std::vector<uint16_t>. Each 16-bit word is formatted
* as a 4-digit uppercase hex value in big-endian order.
*
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
* Causes heap fragmentation on long-running devices.
*
* @param data Vector of 16-bit words to format.
* @param separator Character to use between hex words (default: '.').
* @param show_length Whether to append the word count in parentheses (default: true).
@@ -1188,9 +1176,6 @@ std::string format_hex_pretty(const std::vector<uint16_t> &data, char separator
* Treats each character in the string as a byte and formats it in hex.
* Useful for debugging binary data stored in std::string containers.
*
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
* Causes heap fragmentation on long-running devices.
*
* @param data String whose bytes should be formatted as hex.
* @param separator Character to use between hex bytes (default: '.').
* @param show_length Whether to append the byte count in parentheses (default: true).
@@ -1213,9 +1198,6 @@ std::string format_hex_pretty(const std::string &data, char separator = '.', boo
* Converts the integer to big-endian byte order and formats each byte as hex.
* The most significant byte appears first in the output string.
*
* @warning Allocates heap memory. Use format_hex_pretty_to() with a stack buffer instead.
* Causes heap fragmentation on long-running devices.
*
* @tparam T Unsigned integer type (uint8_t, uint16_t, uint32_t, uint64_t, etc.).
* @param val The unsigned integer value to format.
* @param separator Character to use between hex bytes (default: '.').

View File

@@ -81,19 +81,6 @@ class StringRef {
operator std::string() const { return str(); }
/// Compare with a null-terminated C string (compatible with std::string::compare)
int compare(const char *s) const {
size_t s_len = std::strlen(s);
int result = std::memcmp(base_, s, std::min(len_, s_len));
if (result != 0)
return result;
if (len_ < s_len)
return -1;
if (len_ > s_len)
return 1;
return 0;
}
/// Find first occurrence of substring, returns std::string::npos if not found.
/// Note: Requires the underlying string to be null-terminated.
size_type find(const char *s, size_type pos = 0) const {

View File

@@ -24,14 +24,11 @@ class RegistryEntry:
fun: Callable[..., Any],
type_id: "MockObjClass",
schema: "Schema",
*,
deferred: bool = False,
):
self.name = name
self.fun = fun
self.type_id = type_id
self.raw_schema = schema
self.deferred = deferred
@property
def coroutine_fun(self):
@@ -52,16 +49,9 @@ class Registry(dict[str, RegistryEntry]):
self.base_schema = base_schema or {}
self.type_id_key = type_id_key
def register(
self,
name: str,
type_id: "MockObjClass",
schema: "Schema",
*,
deferred: bool = False,
):
def register(self, name: str, type_id: "MockObjClass", schema: "Schema"):
def decorator(fun: Callable[..., Any]):
self[name] = RegistryEntry(name, fun, type_id, schema, deferred=deferred)
self[name] = RegistryEntry(name, fun, type_id, schema)
return fun
return decorator

View File

@@ -2020,8 +2020,6 @@ def build_message_type(
# Collect fixed_vector fields for custom decode generation
fixed_vector_fields = []
# Collect fields with (null_terminate) = true option
null_terminate_fields = []
for field in desc.field:
# Skip deprecated fields completely
@@ -2064,10 +2062,6 @@ def build_message_type(
ti = create_field_type_info(field, needs_decode, needs_encode)
# Collect fields with (null_terminate) = true for post-decode null-termination
if needs_decode and get_field_opt(field, pb.null_terminate, False):
null_terminate_fields.append(ti.field_name)
# Skip field declarations for fields that are in the base class
# but include their encode/decode logic
if field.name not in common_field_names:
@@ -2174,8 +2168,8 @@ def build_message_type(
prot = "bool decode_64bit(uint32_t field_id, Proto64Bit value) override;"
protected_content.insert(0, prot)
# Generate custom decode() override for messages with FixedVector or null_terminate fields
if fixed_vector_fields or null_terminate_fields:
# Generate custom decode() override for messages with FixedVector fields
if fixed_vector_fields:
# Generate the decode() implementation in cpp
o = f"void {desc.name}::decode(const uint8_t *buffer, size_t length) {{\n"
# Count and init each FixedVector field
@@ -2184,13 +2178,6 @@ def build_message_type(
o += f" this->{field_name}.init(count_{field_name});\n"
# Call parent decode to populate the fields
o += " ProtoDecodableMessage::decode(buffer, length);\n"
# Null-terminate fields marked with (null_terminate) = true in-place.
# Safe: decode is complete, byte after string was already parsed (next field tag)
# or is the +1 reserved byte at end of rx_buf_.
for field_name in null_terminate_fields:
o += f" if (!this->{field_name}.empty()) {{\n"
o += f" const_cast<char *>(this->{field_name}.c_str())[this->{field_name}.size()] = '\\0';\n"
o += " }\n"
o += "}\n"
cpp += o
# Generate the decode() declaration in header (public method)

View File

@@ -3,15 +3,9 @@ display:
spi_16: true
pixel_mode: 18bit
model: ili9488
dc_pin:
allow_other_uses: true
number: ${dc_pin}
cs_pin:
allow_other_uses: true
number: ${cs_pin}
reset_pin:
allow_other_uses: true
number: ${reset_pin}
dc_pin: ${dc_pin}
cs_pin: ${cs_pin}
reset_pin: ${reset_pin}
data_rate: 20MHz
invert_colors: true
show_test_card: true
@@ -30,15 +24,3 @@ display:
height: 200
enable_pin: ${enable_pin}
bus_mode: single
- platform: mipi_spi
model: WAVESHARE-1.83-V2
dc_pin:
allow_other_uses: true
number: ${dc_pin}
cs_pin:
allow_other_uses: true
number: ${cs_pin}
reset_pin:
allow_other_uses: true
number: ${reset_pin}