From 8d1379a2752291d2f4e33d5831d51c2afd59f7ce Mon Sep 17 00:00:00 2001 From: Rene Guca <45061891+rguca@users.noreply.github.com> Date: Thu, 22 Jan 2026 13:54:10 +0100 Subject: [PATCH 1/6] [dht] Increase delay for DHT22 and RHT03 (#13446) --- esphome/components/dht/dht.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/esphome/components/dht/dht.cpp b/esphome/components/dht/dht.cpp index 6cb204c8de..276ea24717 100644 --- a/esphome/components/dht/dht.cpp +++ b/esphome/components/dht/dht.cpp @@ -89,10 +89,8 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r delayMicroseconds(500); } else if (this->model_ == DHT_MODEL_DHT22_TYPE2) { delayMicroseconds(2000); - } else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) { - delayMicroseconds(1000); } else { - delayMicroseconds(800); + delayMicroseconds(1000); } #ifdef USE_ESP32 From d6a41ed51ebfaeae140b13775f3059d68bd746f4 Mon Sep 17 00:00:00 2001 From: Sven Kocksch Date: Thu, 22 Jan 2026 16:31:38 +0100 Subject: [PATCH 2/6] [mipi_dsi] Add M5Stack Tab5 (Rev2/V2) DriverChip (#12074) Co-authored-by: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/mipi_dsi/models/m5stack.py | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/esphome/components/mipi_dsi/models/m5stack.py b/esphome/components/mipi_dsi/models/m5stack.py index 6055c77f8f..2298f76cd4 100644 --- a/esphome/components/mipi_dsi/models/m5stack.py +++ b/esphome/components/mipi_dsi/models/m5stack.py @@ -55,3 +55,44 @@ DriverChip( (0x35,), (0xFE,), ], ) + +DriverChip( + "M5STACK-TAB5-V2", + height=1280, + width=720, + hsync_back_porch=40, + hsync_pulse_width=2, + hsync_front_porch=40, + vsync_back_porch=8, + vsync_pulse_width=2, + vsync_front_porch=220, + pclk_frequency="80MHz", + lane_bit_rate="960Mbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + initsequence=[ + (0x60, 0x71, 0x23, 0xa2), + (0x60, 0x71, 0x23, 0xa3), + (0x60, 0x71, 0x23, 0xa4), + (0xA4, 0x31), + (0xD7, 0x10, 0x0A, 0x10, 0x2A, 0x80, 0x80), + (0x90, 0x71, 0x23, 0x5A, 0x20, 0x24, 0x09, 0x09), + (0xA3, 0x80, 0x01, 0x88, 0x30, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x1E, 0x5C, 0x1E, 0x80, 0x00, 0x4F, 0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x46, 0x00, 0x00, 0x1E, 0x5C, 0x1E, 0x80, 0x00, 0x6F, 0x58, 0x00, 0x00, 0x00, 0xFF), + (0xA6, 0x03, 0x00, 0x24, 0x55, 0x36, 0x00, 0x39, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x00, 0x24, 0x55, 0x38, 0x00, 0x37, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x00, 0x24, 0x11, 0x00, 0x00, 0x00, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x00, 0xEC, 0x11, 0x00, 0x03, 0x00, 0x03, 0x6E, 0x6E, 0xFF, 0xFF, 0x00, 0x08, 0x80, 0x08, 0x80, 0x06, 0x00, 0x00, 0x00, 0x00), + (0xA7, 0x19, 0x19, 0x80, 0x64, 0x40, 0x07, 0x16, 0x40, 0x00, 0x44, 0x03, 0x6E, 0x6E, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x25, 0x34, 0x40, 0x00, 0x02, 0x01, 0x6E, 0x6E, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x00, 0x00, 0x40, 0x00, 0x00, 0x00, 0x6E, 0x6E, 0x91, 0xFF, 0x08, 0x80, 0x64, 0x40, 0x00, 0x00, 0x00, 0x00, 0x20, 0x00, 0x6E, 0x6E, 0x84, 0xFF, 0x08, 0x80, 0x44), + (0xAC, 0x03, 0x19, 0x19, 0x18, 0x18, 0x06, 0x13, 0x13, 0x11, 0x11, 0x08, 0x08, 0x0A, 0x0A, 0x1C, 0x1C, 0x07, 0x07, 0x00, 0x00, 0x02, 0x02, 0x01, 0x19, 0x19, 0x18, 0x18, 0x06, 0x12, 0x12, 0x10, 0x10, 0x09, 0x09, 0x0B, 0x0B, 0x1C, 0x1C, 0x07, 0x07, 0x03, 0x03, 0x01, 0x01), + (0xAD, 0xF0, 0x00, 0x46, 0x00, 0x03, 0x50, 0x50, 0xFF, 0xFF, 0xF0, 0x40, 0x06, 0x01, 0x07, 0x42, 0x42, 0xFF, 0xFF, 0x01, 0x00, 0x00, 0xFF, 0xFF, 0xFF, 0xFF), + (0xAE, 0xFE, 0x3F, 0x3F, 0xFE, 0x3F, 0x3F, 0x00), + (0xB2, 0x15, 0x19, 0x05, 0x23, 0x49, 0xAF, 0x03, 0x2E, 0x5C, 0xD2, 0xFF, 0x10, 0x20, 0xFD, 0x20, 0xC0, 0x00), + (0xE8, 0x20, 0x6F, 0x04, 0x97, 0x97, 0x3E, 0x04, 0xDC, 0xDC, 0x3E, 0x06, 0xFA, 0x26, 0x3E), + (0x75, 0x03, 0x04), + (0xE7, 0x3B, 0x00, 0x00, 0x7C, 0xA1, 0x8C, 0x20, 0x1A, 0xF0, 0xB1, 0x50, 0x00, 0x50, 0xB1, 0x50, 0xB1, 0x50, 0xD8, 0x00, 0x55, 0x00, 0xB1, 0x00, 0x45, 0xC9, 0x6A, 0xFF, 0x5A, 0xD8, 0x18, 0x88, 0x15, 0xB1, 0x01, 0x01, 0x77), + (0xEA, 0x13, 0x00, 0x04, 0x00, 0x00, 0x00, 0x00, 0x2C), + (0xB0, 0x22, 0x43, 0x11, 0x61, 0x25, 0x43, 0x43), + (0xb7, 0x00, 0x00, 0x73, 0x73), + (0xBF, 0xA6, 0xAA), + (0xA9, 0x00, 0x00, 0x73, 0xFF, 0x00, 0x00, 0x03, 0x00, 0x00, 0x03), + (0xC8, 0x00, 0x00, 0x10, 0x1F, 0x36, 0x00, 0x5D, 0x04, 0x9D, 0x05, 0x10, 0xF2, 0x06, 0x60, 0x03, 0x11, 0xAD, 0x00, 0xEF, 0x01, 0x22, 0x2E, 0x0E, 0x74, 0x08, 0x32, 0xDC, 0x09, 0x33, 0x0F, 0xF3, 0x77, 0x0D, 0xB0, 0xDC, 0x03, 0xFF), + (0xC9, 0x00, 0x00, 0x10, 0x1F, 0x36, 0x00, 0x5D, 0x04, 0x9D, 0x05, 0x10, 0xF2, 0x06, 0x60, 0x03, 0x11, 0xAD, 0x00, 0xEF, 0x01, 0x22, 0x2E, 0x0E, 0x74, 0x08, 0x32, 0xDC, 0x09, 0x33, 0x0F, 0xF3, 0x77, 0x0D, 0xB0, 0xDC, 0x03, 0xFF), + ], +) From 4ac7fe84b41fe383d681339e900228b0817a6e98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?H=2E=20=C3=81rkosi=20R=C3=B3bert?= Date: Thu, 22 Jan 2026 17:14:14 +0100 Subject: [PATCH 3/6] [bthome_mithermometer] add encrypted beacon support (#13428) --- .../bthome_mithermometer/__init__.py | 10 +- .../bthome_mithermometer/bthome_ble.cpp | 135 +++++++++++++++--- .../bthome_mithermometer/bthome_ble.h | 7 + .../bthome_mithermometer/common.yaml | 1 + 4 files changed, 136 insertions(+), 17 deletions(-) diff --git a/esphome/components/bthome_mithermometer/__init__.py b/esphome/components/bthome_mithermometer/__init__.py index 0e84278afa..8ce216da22 100644 --- a/esphome/components/bthome_mithermometer/__init__.py +++ b/esphome/components/bthome_mithermometer/__init__.py @@ -1,7 +1,8 @@ import esphome.codegen as cg from esphome.components import esp32_ble_tracker import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_MAC_ADDRESS +from esphome.const import CONF_BINDKEY, CONF_ID, CONF_MAC_ADDRESS +from esphome.core import HexInt CODEOWNERS = ["@nagyrobi"] DEPENDENCIES = ["esp32_ble_tracker"] @@ -22,6 +23,7 @@ def bthome_mithermometer_base_schema(extra_schema=None): { cv.GenerateID(CONF_ID): cv.declare_id(BTHomeMiThermometer), cv.Required(CONF_MAC_ADDRESS): cv.mac_address, + cv.Optional(CONF_BINDKEY): cv.bind_key, } ) .extend(BLE_DEVICE_SCHEMA) @@ -34,3 +36,9 @@ async def setup_bthome_mithermometer(var, config): await cg.register_component(var, config) await esp32_ble_tracker.register_ble_device(var, config) cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) + if bindkey := config.get(CONF_BINDKEY): + bindkey_bytes = [ + HexInt(int(bindkey[index : index + 2], 16)) + for index in range(0, len(bindkey), 2) + ] + cg.add(var.set_bindkey(cg.ArrayInitializer(*bindkey_bytes))) diff --git a/esphome/components/bthome_mithermometer/bthome_ble.cpp b/esphome/components/bthome_mithermometer/bthome_ble.cpp index d1c5165896..2b73d8735c 100644 --- a/esphome/components/bthome_mithermometer/bthome_ble.cpp +++ b/esphome/components/bthome_mithermometer/bthome_ble.cpp @@ -3,15 +3,23 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include #include +#include #include #ifdef USE_ESP32 +#include "mbedtls/ccm.h" + namespace esphome { namespace bthome_mithermometer { static const char *const TAG = "bthome_mithermometer"; +static constexpr size_t BTHOME_BINDKEY_SIZE = 16; +static constexpr size_t BTHOME_NONCE_SIZE = 13; +static constexpr size_t BTHOME_MIC_SIZE = 4; +static constexpr size_t BTHOME_COUNTER_SIZE = 4; static const char *format_mac_address(std::span buffer, uint64_t address) { std::array mac{}; @@ -130,6 +138,10 @@ void BTHomeMiThermometer::dump_config() { char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; ESP_LOGCONFIG(TAG, "BTHome MiThermometer"); ESP_LOGCONFIG(TAG, " MAC Address: %s", format_mac_address(addr_buf, this->address_)); + if (this->has_bindkey_) { + char bindkey_hex[format_hex_pretty_size(BTHOME_BINDKEY_SIZE)]; + ESP_LOGCONFIG(TAG, " Bindkey: %s", format_hex_pretty_to(bindkey_hex, this->bindkey_, BTHOME_BINDKEY_SIZE, '.')); + } LOG_SENSOR(" ", "Temperature", this->temperature_); LOG_SENSOR(" ", "Humidity", this->humidity_); LOG_SENSOR(" ", "Battery Level", this->battery_level_); @@ -150,6 +162,60 @@ bool BTHomeMiThermometer::parse_device(const esp32_ble_tracker::ESPBTDevice &dev return matched; } +void BTHomeMiThermometer::set_bindkey(std::initializer_list bindkey) { + if (bindkey.size() != sizeof(this->bindkey_)) { + ESP_LOGW(TAG, "BTHome bindkey size mismatch: %zu", bindkey.size()); + return; + } + std::copy(bindkey.begin(), bindkey.end(), this->bindkey_); + this->has_bindkey_ = true; +} + +bool BTHomeMiThermometer::decrypt_bthome_payload_(const std::vector &data, uint64_t source_address, + std::vector &payload) const { + if (data.size() <= 1 + BTHOME_COUNTER_SIZE + BTHOME_MIC_SIZE) { + ESP_LOGVV(TAG, "Encrypted BTHome payload too short: %zu", data.size()); + return false; + } + + const size_t ciphertext_size = data.size() - 1 - BTHOME_COUNTER_SIZE - BTHOME_MIC_SIZE; + payload.resize(ciphertext_size); + + std::array mac{}; + for (size_t i = 0; i < MAC_ADDRESS_SIZE; i++) { + mac[i] = (source_address >> ((MAC_ADDRESS_SIZE - 1 - i) * 8)) & 0xFF; + } + + std::array nonce{}; + memcpy(nonce.data(), mac.data(), mac.size()); + nonce[6] = 0xD2; + nonce[7] = 0xFC; + nonce[8] = data[0]; + memcpy(nonce.data() + 9, &data[data.size() - BTHOME_COUNTER_SIZE - BTHOME_MIC_SIZE], BTHOME_COUNTER_SIZE); + + const uint8_t *ciphertext = data.data() + 1; + const uint8_t *mic = data.data() + data.size() - BTHOME_MIC_SIZE; + + mbedtls_ccm_context ctx; + mbedtls_ccm_init(&ctx); + + int ret = mbedtls_ccm_setkey(&ctx, MBEDTLS_CIPHER_ID_AES, this->bindkey_, BTHOME_BINDKEY_SIZE * 8); + if (ret) { + ESP_LOGVV(TAG, "mbedtls_ccm_setkey() failed."); + mbedtls_ccm_free(&ctx); + return false; + } + + ret = mbedtls_ccm_auth_decrypt(&ctx, ciphertext_size, nonce.data(), nonce.size(), nullptr, 0, ciphertext, + payload.data(), mic, BTHOME_MIC_SIZE); + mbedtls_ccm_free(&ctx); + if (ret) { + ESP_LOGVV(TAG, "BTHome decryption failed (ret=%d).", ret); + return false; + } + return true; +} + bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceData &service_data, const esp32_ble_tracker::ESPBTDevice &device) { if (!service_data.uuid.contains(0xD2, 0xFC)) { @@ -173,51 +239,88 @@ bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceD return false; } - char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; - if (is_encrypted) { - ESP_LOGV(TAG, "Ignoring encrypted BTHome frame from %s", device.address_str_to(addr_buf)); + uint64_t source_address = device.address_uint64(); + bool address_matches = source_address == this->address_; + if (!is_encrypted && mac_included && data.size() >= 7) { + uint64_t advertised_address = 0; + for (int i = 5; i >= 0; i--) { + advertised_address = (advertised_address << 8) | data[1 + i]; + } + address_matches = address_matches || advertised_address == this->address_; + } + + if (is_encrypted && !this->has_bindkey_) { + if (address_matches) { + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + ESP_LOGE(TAG, "Encrypted BTHome frame received but no bindkey configured for %s", + device.address_str_to(addr_buf)); + } return false; } - size_t payload_index = 1; - uint64_t source_address = device.address_uint64(); + if (!is_encrypted && this->has_bindkey_) { + if (address_matches) { + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + ESP_LOGE(TAG, "Unencrypted BTHome frame received with bindkey configured for %s", + device.address_str_to(addr_buf)); + } + return false; + } + std::vector decrypted_payload; + const uint8_t *payload = nullptr; + size_t payload_size = 0; + + if (is_encrypted) { + if (!this->decrypt_bthome_payload_(data, source_address, decrypted_payload)) { + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; + ESP_LOGVV(TAG, "Failed to decrypt BTHome frame from %s", device.address_str_to(addr_buf)); + return false; + } + payload = decrypted_payload.data(); + payload_size = decrypted_payload.size(); + } else { + payload = data.data() + 1; + payload_size = data.size() - 1; + } if (mac_included) { - if (data.size() < 7) { + if (payload_size < 6) { ESP_LOGVV(TAG, "BTHome payload missing MAC address"); return false; } source_address = 0; for (int i = 5; i >= 0; i--) { - source_address = (source_address << 8) | data[1 + i]; + source_address = (source_address << 8) | payload[i]; } - payload_index = 7; + payload += 6; + payload_size -= 6; } + char addr_buf[MAC_ADDRESS_PRETTY_BUFFER_SIZE]; if (source_address != this->address_) { ESP_LOGVV(TAG, "BTHome frame from unexpected device %s", format_mac_address(addr_buf, source_address)); return false; } - if (payload_index >= data.size()) { + if (payload_size == 0) { ESP_LOGVV(TAG, "BTHome payload empty after header"); return false; } bool reported = false; - size_t offset = payload_index; + size_t offset = 0; uint8_t last_type = 0; - while (offset < data.size()) { - const uint8_t obj_type = data[offset++]; + while (offset < payload_size) { + const uint8_t obj_type = payload[offset++]; size_t value_length = 0; bool has_length_byte = obj_type == 0x53; // text objects include explicit length if (has_length_byte) { - if (offset >= data.size()) { + if (offset >= payload_size) { break; } - value_length = data[offset++]; + value_length = payload[offset++]; } else { if (!get_bthome_value_length(obj_type, value_length)) { ESP_LOGVV(TAG, "Unknown BTHome object 0x%02X", obj_type); @@ -229,12 +332,12 @@ bool BTHomeMiThermometer::handle_service_data_(const esp32_ble_tracker::ServiceD break; } - if (offset + value_length > data.size()) { + if (offset + value_length > payload_size) { ESP_LOGVV(TAG, "BTHome object length exceeds payload"); break; } - const uint8_t *value = &data[offset]; + const uint8_t *value = &payload[offset]; offset += value_length; if (obj_type < last_type) { diff --git a/esphome/components/bthome_mithermometer/bthome_ble.h b/esphome/components/bthome_mithermometer/bthome_ble.h index 3d2380b48d..ef3038ec93 100644 --- a/esphome/components/bthome_mithermometer/bthome_ble.h +++ b/esphome/components/bthome_mithermometer/bthome_ble.h @@ -5,6 +5,8 @@ #include "esphome/core/component.h" #include +#include +#include #ifdef USE_ESP32 @@ -14,6 +16,7 @@ namespace bthome_mithermometer { class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, public Component { public: void set_address(uint64_t address) { this->address_ = address; } + void set_bindkey(std::initializer_list bindkey); void set_temperature(sensor::Sensor *temperature) { this->temperature_ = temperature; } void set_humidity(sensor::Sensor *humidity) { this->humidity_ = humidity; } @@ -27,9 +30,13 @@ class BTHomeMiThermometer : public esp32_ble_tracker::ESPBTDeviceListener, publi protected: bool handle_service_data_(const esp32_ble_tracker::ServiceData &service_data, const esp32_ble_tracker::ESPBTDevice &device); + bool decrypt_bthome_payload_(const std::vector &data, uint64_t source_address, + std::vector &payload) const; uint64_t address_{0}; optional last_packet_id_{}; + bool has_bindkey_{false}; + uint8_t bindkey_[16]; sensor::Sensor *temperature_{nullptr}; sensor::Sensor *humidity_{nullptr}; diff --git a/tests/components/bthome_mithermometer/common.yaml b/tests/components/bthome_mithermometer/common.yaml index ba94e46878..7a68fae966 100644 --- a/tests/components/bthome_mithermometer/common.yaml +++ b/tests/components/bthome_mithermometer/common.yaml @@ -3,6 +3,7 @@ esp32_ble_tracker: sensor: - platform: bthome_mithermometer mac_address: A4:C1:38:4E:16:78 + bindkey: eef418daf699a0c188f3bfd17e4565d9 temperature: name: "BTHome Temperature" humidity: From ddb762f8f53006a6ac53fe70ca066704de556830 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 22 Jan 2026 08:09:14 -1000 Subject: [PATCH 4/6] [api] Limit Nagle batching for log messages to reduce LWIP buffer pressure (#13439) --- esphome/components/api/api_connection.cpp | 19 +------ esphome/components/api/api_frame_helper.h | 67 +++++++++++++++-------- 2 files changed, 46 insertions(+), 40 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 0364879ccd..1626f395e6 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1844,23 +1844,8 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) { return false; } - // Toggle Nagle's algorithm based on message type to prevent log messages from - // filling the TCP send buffer and crowding out important state updates. - // - // This honors the `no_delay` proto option - SubscribeLogsResponse is the only - // message with `option (no_delay) = false;` in api.proto, indicating it should - // allow Nagle coalescing. This option existed since 2019 but was never implemented. - // - // - Log messages: Enable Nagle (NODELAY=false) so small log packets coalesce - // into fewer, larger packets. They flush naturally via TCP delayed ACK timer - // (~200ms), buffer filling, or when a state update triggers a flush. - // - // - All other messages (state updates, responses): Disable Nagle (NODELAY=true) - // for immediate delivery. These are time-sensitive and should not be delayed. - // - // This must be done proactively BEFORE the buffer fills up - checking buffer - // state here would be too late since we'd already be in a degraded state. - this->helper_->set_nodelay(!is_log_message); + // Set TCP_NODELAY based on message type - see set_nodelay_for_message() for details + this->helper_->set_nodelay_for_message(is_log_message); APIError err = this->helper_->write_protobuf_packet(message_type, buffer); if (err == APIError::WOULD_BLOCK) diff --git a/esphome/components/api/api_frame_helper.h b/esphome/components/api/api_frame_helper.h index 27ec1ff915..f311e34fd7 100644 --- a/esphome/components/api/api_frame_helper.h +++ b/esphome/components/api/api_frame_helper.h @@ -120,26 +120,39 @@ class APIFrameHelper { } return APIError::OK; } - /// Toggle TCP_NODELAY socket option to control Nagle's algorithm. - /// - /// This is used to allow log messages to coalesce (Nagle enabled) while keeping - /// state updates low-latency (NODELAY enabled). Without this, many small log - /// packets fill the TCP send buffer, crowding out important state updates. - /// - /// State is tracked to minimize setsockopt() overhead - on lwip_raw (ESP8266/RP2040) - /// this is just a boolean assignment; on other platforms it's a lightweight syscall. - /// - /// @param enable true to enable NODELAY (disable Nagle), false to enable Nagle - /// @return true if successful or already in desired state - bool set_nodelay(bool enable) { - if (this->nodelay_enabled_ == enable) - return true; - int val = enable ? 1 : 0; - int err = this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int)); - if (err == 0) { - this->nodelay_enabled_ = enable; + // Manage TCP_NODELAY (Nagle's algorithm) based on message type. + // + // For non-log messages (sensor data, state updates): Always disable Nagle + // (NODELAY on) for immediate delivery - these are time-sensitive. + // + // For log messages: Use Nagle to coalesce multiple small log packets into + // fewer larger packets, reducing WiFi overhead. However, we limit batching + // to 3 messages to avoid excessive LWIP buffer pressure on memory-constrained + // devices like ESP8266. LWIP's TCP_OVERSIZE option coalesces the data into + // shared pbufs, but holding data too long waiting for Nagle's timer causes + // buffer exhaustion and dropped messages. + // + // Flow: Log 1 (Nagle on) -> Log 2 (Nagle on) -> Log 3 (NODELAY, flush all) + // + void set_nodelay_for_message(bool is_log_message) { + if (!is_log_message) { + if (this->nodelay_state_ != NODELAY_ON) { + this->set_nodelay_raw_(true); + this->nodelay_state_ = NODELAY_ON; + } + return; + } + + // Log messages 1-3: state transitions -1 -> 1 -> 2 -> -1 (flush on 3rd) + if (this->nodelay_state_ == NODELAY_ON) { + this->set_nodelay_raw_(false); + this->nodelay_state_ = 1; + } else if (this->nodelay_state_ >= LOG_NAGLE_COUNT) { + this->set_nodelay_raw_(true); + this->nodelay_state_ = NODELAY_ON; + } else { + this->nodelay_state_++; } - return err == 0; } virtual APIError write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) = 0; // Write multiple protobuf messages in a single operation @@ -229,10 +242,18 @@ class APIFrameHelper { uint8_t tx_buf_head_{0}; uint8_t tx_buf_tail_{0}; uint8_t tx_buf_count_{0}; - // Tracks TCP_NODELAY state to minimize setsockopt() calls. Initialized to true - // since init_common_() enables NODELAY. Used by set_nodelay() to allow log - // messages to coalesce while keeping state updates low-latency. - bool nodelay_enabled_{true}; + // Nagle batching state for log messages. NODELAY_ON (-1) means NODELAY is enabled + // (immediate send). Values 1-2 count log messages in the current Nagle batch. + // After LOG_NAGLE_COUNT logs, we switch to NODELAY to flush and reset. + static constexpr int8_t NODELAY_ON = -1; + static constexpr int8_t LOG_NAGLE_COUNT = 2; + int8_t nodelay_state_{NODELAY_ON}; + + // Internal helper to set TCP_NODELAY socket option + void set_nodelay_raw_(bool enable) { + int val = enable ? 1 : 0; + this->socket_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &val, sizeof(int)); + } // Common initialization for both plaintext and noise protocols APIError init_common_(); From 512dd1b661fc073791be5e0a9efa5674a6b39952 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 22 Jan 2026 08:49:43 -1000 Subject: [PATCH 5/6] [wifi] Fix stale error_from_callback_ causing immediate connection failures --- esphome/components/wifi/wifi_component.cpp | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index ff6284c073..007e25b711 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -565,7 +565,6 @@ void WiFiComponent::start() { void WiFiComponent::restart_adapter() { ESP_LOGW(TAG, "Restarting adapter"); this->wifi_mode_(false, {}); - this->error_from_callback_ = false; } void WiFiComponent::loop() { @@ -618,8 +617,6 @@ void WiFiComponent::loop() { if (!this->is_connected()) { ESP_LOGW(TAG, "Connection lost; reconnecting"); this->state_ = WIFI_COMPONENT_STATE_STA_CONNECTING; - // Clear error flag before reconnecting so first attempt is not seen as immediate failure - this->error_from_callback_ = false; this->retry_connect(); } else { this->status_clear_warning(); @@ -963,6 +960,9 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { ESP_LOGV(TAG, " Hidden: %s", YESNO(ap.get_hidden())); #endif + // Clear any stale error from previous connection attempt + this->error_from_callback_ = false; + if (!this->wifi_sta_connect_(ap)) { ESP_LOGE(TAG, "wifi_sta_connect_ failed"); // Enter cooldown to allow WiFi hardware to stabilize @@ -1068,7 +1068,6 @@ void WiFiComponent::enable() { return; ESP_LOGD(TAG, "Enabling"); - this->error_from_callback_ = false; this->state_ = WIFI_COMPONENT_STATE_OFF; this->start(); } @@ -1329,11 +1328,6 @@ void WiFiComponent::check_connecting_finished(uint32_t now) { // Reset to initial phase on successful connection (don't log transition, just reset state) this->retry_phase_ = WiFiRetryPhase::INITIAL_CONNECT; this->num_retried_ = 0; - // Ensure next connection attempt does not inherit error state - // so when WiFi disconnects later we start fresh and don't see - // the first connection as a failure. - this->error_from_callback_ = false; - if (this->has_ap()) { #ifdef USE_CAPTIVE_PORTAL if (this->is_captive_portal_active_()) { @@ -1844,8 +1838,6 @@ void WiFiComponent::retry_connect() { this->advance_to_next_target_or_increment_retry_(); } - this->error_from_callback_ = false; - yield(); // Check if we have a valid target before building params // After exhausting all networks in a phase, selected_sta_index_ may be -1 @@ -2171,7 +2163,6 @@ void WiFiComponent::process_roaming_scan_() { this->roaming_state_ = RoamingState::CONNECTING; // Connect directly - wifi_sta_connect_ handles disconnect internally - this->error_from_callback_ = false; this->start_connecting(roam_params); } From 7866662611639bee1b30e44b0aea1ff47c7933c7 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Thu, 22 Jan 2026 09:26:16 -1000 Subject: [PATCH 6/6] comment, improve --- esphome/components/wifi/wifi_component.cpp | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/esphome/components/wifi/wifi_component.cpp b/esphome/components/wifi/wifi_component.cpp index 007e25b711..52d9b2b442 100644 --- a/esphome/components/wifi/wifi_component.cpp +++ b/esphome/components/wifi/wifi_component.cpp @@ -565,6 +565,12 @@ void WiFiComponent::start() { void WiFiComponent::restart_adapter() { ESP_LOGW(TAG, "Restarting adapter"); this->wifi_mode_(false, {}); + // Clear error flag here because restart_adapter() enters COOLDOWN state, + // and check_connecting_finished() is called after cooldown without going + // through start_connecting() first. Without this clear, stale errors would + // trigger spurious "failed (callback)" logs. The canonical clear location + // is in start_connecting(); this is the only exception to that pattern. + this->error_from_callback_ = false; } void WiFiComponent::loop() { @@ -960,7 +966,10 @@ void WiFiComponent::start_connecting(const WiFiAP &ap) { ESP_LOGV(TAG, " Hidden: %s", YESNO(ap.get_hidden())); #endif - // Clear any stale error from previous connection attempt + // Clear any stale error from previous connection attempt. + // This is the canonical location for clearing the flag since all connection + // attempts go through start_connecting(). The only other clear is in + // restart_adapter() which enters COOLDOWN without calling start_connecting(). this->error_from_callback_ = false; if (!this->wifi_sta_connect_(ap)) {