From 8f74b027b48e102f96551a6a6b8d0857ce72fc44 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Mon, 9 Feb 2026 16:40:32 -0600 Subject: [PATCH 01/15] Bump setuptools from 80.10.2 to 82.0.0 (#13897) --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 1bd43ea2f12..339bc65eed0 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,5 @@ [build-system] -requires = ["setuptools==80.10.2", "wheel>=0.43,<0.47"] +requires = ["setuptools==82.0.0", "wheel>=0.43,<0.47"] build-backend = "setuptools.build_meta" [project] From 475db750e08ebc6c76f4a05695f66c49bca3efce Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Feb 2026 17:41:16 -0500 Subject: [PATCH 02/15] [uart] Change available() return type from int to size_t (#13893) Co-authored-by: Claude Opus 4.6 --- esphome/components/bl0942/bl0942.cpp | 6 +++--- esphome/components/tormatic/tormatic_cover.cpp | 2 +- esphome/components/uart/uart.h | 2 +- esphome/components/uart/uart_component.cpp | 6 +++--- esphome/components/uart/uart_component.h | 2 +- .../components/uart/uart_component_esp8266.cpp | 15 +++++++++------ esphome/components/uart/uart_component_esp8266.h | 4 ++-- .../components/uart/uart_component_esp_idf.cpp | 2 +- esphome/components/uart/uart_component_esp_idf.h | 2 +- esphome/components/uart/uart_component_host.cpp | 7 ++++--- esphome/components/uart/uart_component_host.h | 2 +- .../components/uart/uart_component_libretiny.cpp | 2 +- .../components/uart/uart_component_libretiny.h | 2 +- esphome/components/uart/uart_component_rp2040.cpp | 2 +- esphome/components/uart/uart_component_rp2040.h | 2 +- esphome/components/usb_cdc_acm/usb_cdc_acm.h | 2 +- .../components/usb_cdc_acm/usb_cdc_acm_esp32.cpp | 4 ++-- esphome/components/usb_uart/usb_uart.h | 2 +- esphome/components/weikai/weikai.cpp | 2 +- esphome/components/weikai/weikai.h | 2 +- tests/components/uart/common.h | 2 +- 21 files changed, 38 insertions(+), 34 deletions(-) diff --git a/esphome/components/bl0942/bl0942.cpp b/esphome/components/bl0942/bl0942.cpp index 95dd689b074..b408c5549c1 100644 --- a/esphome/components/bl0942/bl0942.cpp +++ b/esphome/components/bl0942/bl0942.cpp @@ -46,16 +46,16 @@ static const uint32_t PKT_TIMEOUT_MS = 200; void BL0942::loop() { DataPacket buffer; - int avail = this->available(); + size_t avail = this->available(); if (!avail) { return; } - if (static_cast(avail) < sizeof(buffer)) { + if (avail < sizeof(buffer)) { if (!this->rx_start_) { this->rx_start_ = millis(); } else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) { - ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%d bytes)", avail); + ESP_LOGW(TAG, "Junk on wire. Throwing away partial message (%zu bytes)", avail); this->read_array((uint8_t *) &buffer, avail); this->rx_start_ = 0; } diff --git a/esphome/components/tormatic/tormatic_cover.cpp b/esphome/components/tormatic/tormatic_cover.cpp index ef93964a283..be412d62a84 100644 --- a/esphome/components/tormatic/tormatic_cover.cpp +++ b/esphome/components/tormatic/tormatic_cover.cpp @@ -251,7 +251,7 @@ void Tormatic::stop_at_target_() { // Read a GateStatus from the unit. The unit only sends messages in response to // status requests or commands, so a message needs to be sent first. optional Tormatic::read_gate_status_() { - if (this->available() < static_cast(sizeof(MessageHeader))) { + if (this->available() < sizeof(MessageHeader)) { return {}; } diff --git a/esphome/components/uart/uart.h b/esphome/components/uart/uart.h index 72c282f1c4f..bb91e5cd7c0 100644 --- a/esphome/components/uart/uart.h +++ b/esphome/components/uart/uart.h @@ -43,7 +43,7 @@ class UARTDevice { return res; } - int available() { return this->parent_->available(); } + size_t available() { return this->parent_->available(); } void flush() { this->parent_->flush(); } diff --git a/esphome/components/uart/uart_component.cpp b/esphome/components/uart/uart_component.cpp index 30fc208fc91..762e56c3990 100644 --- a/esphome/components/uart/uart_component.cpp +++ b/esphome/components/uart/uart_component.cpp @@ -5,13 +5,13 @@ namespace esphome::uart { static const char *const TAG = "uart"; bool UARTComponent::check_read_timeout_(size_t len) { - if (this->available() >= int(len)) + if (this->available() >= len) return true; uint32_t start_time = millis(); - while (this->available() < int(len)) { + while (this->available() < len) { if (millis() - start_time > 100) { - ESP_LOGE(TAG, "Reading from UART timed out at byte %u!", this->available()); + ESP_LOGE(TAG, "Reading from UART timed out at byte %zu!", this->available()); return false; } yield(); diff --git a/esphome/components/uart/uart_component.h b/esphome/components/uart/uart_component.h index ea6e1562f4b..b6ffbbd51f5 100644 --- a/esphome/components/uart/uart_component.h +++ b/esphome/components/uart/uart_component.h @@ -69,7 +69,7 @@ class UARTComponent { // Pure virtual method to return the number of bytes available for reading. // @return Number of available bytes. - virtual int available() = 0; + virtual size_t available() = 0; // Pure virtual method to block until all bytes have been written to the UART bus. virtual void flush() = 0; diff --git a/esphome/components/uart/uart_component_esp8266.cpp b/esphome/components/uart/uart_component_esp8266.cpp index 504d494e2e9..3ebf381c846 100644 --- a/esphome/components/uart/uart_component_esp8266.cpp +++ b/esphome/components/uart/uart_component_esp8266.cpp @@ -206,7 +206,7 @@ bool ESP8266UartComponent::read_array(uint8_t *data, size_t len) { #endif return true; } -int ESP8266UartComponent::available() { +size_t ESP8266UartComponent::available() { if (this->hw_serial_ != nullptr) { return this->hw_serial_->available(); } else { @@ -329,11 +329,14 @@ uint8_t ESP8266SoftwareSerial::peek_byte() { void ESP8266SoftwareSerial::flush() { // Flush is a NO-OP with software serial, all bytes are written immediately. } -int ESP8266SoftwareSerial::available() { - int avail = int(this->rx_in_pos_) - int(this->rx_out_pos_); - if (avail < 0) - return avail + this->rx_buffer_size_; - return avail; +size_t ESP8266SoftwareSerial::available() { + // Read volatile rx_in_pos_ once to avoid TOCTOU race with ISR. + // When in >= out, data is contiguous: [out..in). + // When in < out, data wraps: [out..buf_size) + [0..in). + size_t in = this->rx_in_pos_; + if (in >= this->rx_out_pos_) + return in - this->rx_out_pos_; + return this->rx_buffer_size_ - this->rx_out_pos_ + in; } } // namespace esphome::uart diff --git a/esphome/components/uart/uart_component_esp8266.h b/esphome/components/uart/uart_component_esp8266.h index e33dd00644a..e84cbe386d5 100644 --- a/esphome/components/uart/uart_component_esp8266.h +++ b/esphome/components/uart/uart_component_esp8266.h @@ -23,7 +23,7 @@ class ESP8266SoftwareSerial { void write_byte(uint8_t data); - int available(); + size_t available(); protected: static void gpio_intr(ESP8266SoftwareSerial *arg); @@ -57,7 +57,7 @@ class ESP8266UartComponent : public UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; - int available() override; + size_t available() override; void flush() override; uint32_t get_config(); diff --git a/esphome/components/uart/uart_component_esp_idf.cpp b/esphome/components/uart/uart_component_esp_idf.cpp index 90997787aa0..19b9a4077fa 100644 --- a/esphome/components/uart/uart_component_esp_idf.cpp +++ b/esphome/components/uart/uart_component_esp_idf.cpp @@ -338,7 +338,7 @@ bool IDFUARTComponent::read_array(uint8_t *data, size_t len) { return read_len == (int32_t) length_to_read; } -int IDFUARTComponent::available() { +size_t IDFUARTComponent::available() { size_t available = 0; esp_err_t err; diff --git a/esphome/components/uart/uart_component_esp_idf.h b/esphome/components/uart/uart_component_esp_idf.h index bd6d0c792e6..1ecb02d7ab6 100644 --- a/esphome/components/uart/uart_component_esp_idf.h +++ b/esphome/components/uart/uart_component_esp_idf.h @@ -22,7 +22,7 @@ class IDFUARTComponent : public UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; - int available() override; + size_t available() override; void flush() override; uint8_t get_hw_serial_number() { return this->uart_num_; } diff --git a/esphome/components/uart/uart_component_host.cpp b/esphome/components/uart/uart_component_host.cpp index 69b24607d18..0e5ef3c6bd3 100644 --- a/esphome/components/uart/uart_component_host.cpp +++ b/esphome/components/uart/uart_component_host.cpp @@ -265,7 +265,7 @@ bool HostUartComponent::read_array(uint8_t *data, size_t len) { return true; } -int HostUartComponent::available() { +size_t HostUartComponent::available() { if (this->file_descriptor_ == -1) { return 0; } @@ -275,9 +275,10 @@ int HostUartComponent::available() { this->update_error_(strerror(errno)); return 0; } + size_t result = available; if (this->has_peek_) - available++; - return available; + result++; + return result; }; void HostUartComponent::flush() { diff --git a/esphome/components/uart/uart_component_host.h b/esphome/components/uart/uart_component_host.h index a4a6946c0c2..89b951093b6 100644 --- a/esphome/components/uart/uart_component_host.h +++ b/esphome/components/uart/uart_component_host.h @@ -17,7 +17,7 @@ class HostUartComponent : public UARTComponent, public Component { void write_array(const uint8_t *data, size_t len) override; bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; - int available() override; + size_t available() override; void flush() override; void set_name(std::string port_name) { port_name_ = port_name; }; diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index 863732c88d1..cb4465068d8 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -169,7 +169,7 @@ bool LibreTinyUARTComponent::read_array(uint8_t *data, size_t len) { return true; } -int LibreTinyUARTComponent::available() { return this->serial_->available(); } +size_t LibreTinyUARTComponent::available() { return this->serial_->available(); } void LibreTinyUARTComponent::flush() { ESP_LOGVV(TAG, " Flushing"); this->serial_->flush(); diff --git a/esphome/components/uart/uart_component_libretiny.h b/esphome/components/uart/uart_component_libretiny.h index ec13e7da5a3..31f082d31e1 100644 --- a/esphome/components/uart/uart_component_libretiny.h +++ b/esphome/components/uart/uart_component_libretiny.h @@ -21,7 +21,7 @@ class LibreTinyUARTComponent : public UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; - int available() override; + size_t available() override; void flush() override; uint16_t get_config(); diff --git a/esphome/components/uart/uart_component_rp2040.cpp b/esphome/components/uart/uart_component_rp2040.cpp index 5799d26a54a..0c6834055c3 100644 --- a/esphome/components/uart/uart_component_rp2040.cpp +++ b/esphome/components/uart/uart_component_rp2040.cpp @@ -186,7 +186,7 @@ bool RP2040UartComponent::read_array(uint8_t *data, size_t len) { #endif return true; } -int RP2040UartComponent::available() { return this->serial_->available(); } +size_t RP2040UartComponent::available() { return this->serial_->available(); } void RP2040UartComponent::flush() { ESP_LOGVV(TAG, " Flushing"); this->serial_->flush(); diff --git a/esphome/components/uart/uart_component_rp2040.h b/esphome/components/uart/uart_component_rp2040.h index d626d11a2e4..4ca58e8dc60 100644 --- a/esphome/components/uart/uart_component_rp2040.h +++ b/esphome/components/uart/uart_component_rp2040.h @@ -24,7 +24,7 @@ class RP2040UartComponent : public UARTComponent, public Component { bool peek_byte(uint8_t *data) override; bool read_array(uint8_t *data, size_t len) override; - int available() override; + size_t available() override; void flush() override; uint16_t get_config(); diff --git a/esphome/components/usb_cdc_acm/usb_cdc_acm.h b/esphome/components/usb_cdc_acm/usb_cdc_acm.h index 065d7282d5f..ddcc65232d4 100644 --- a/esphome/components/usb_cdc_acm/usb_cdc_acm.h +++ b/esphome/components/usb_cdc_acm/usb_cdc_acm.h @@ -81,7 +81,7 @@ class USBCDCACMInstance : public uart::UARTComponent, public Parentedusb_rx_ringbuf_ != nullptr) { vRingbufferGetInfo(this->usb_rx_ringbuf_, nullptr, nullptr, nullptr, nullptr, &waiting); } - return static_cast(waiting) + (this->has_peek_ ? 1 : 0); + return waiting + (this->has_peek_ ? 1 : 0); } void USBCDCACMInstance::flush() { diff --git a/esphome/components/usb_uart/usb_uart.h b/esphome/components/usb_uart/usb_uart.h index 96c17bd1559..94e61204575 100644 --- a/esphome/components/usb_uart/usb_uart.h +++ b/esphome/components/usb_uart/usb_uart.h @@ -97,7 +97,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented(this->input_buffer_.get_available()); } + size_t available() override { return this->input_buffer_.get_available(); } void flush() override {} void check_logger_conflict() override {} void set_parity(UARTParityOptions parity) { this->parity_ = parity; } diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp index d0a8f8366b6..1d835daf1ef 100644 --- a/esphome/components/weikai/weikai.cpp +++ b/esphome/components/weikai/weikai.cpp @@ -401,7 +401,7 @@ bool WeikaiChannel::peek_byte(uint8_t *buffer) { return this->receive_buffer_.peek(*buffer); } -int WeikaiChannel::available() { +size_t WeikaiChannel::available() { size_t available = this->receive_buffer_.count(); if (!available) available = xfer_fifo_to_buffer_(); diff --git a/esphome/components/weikai/weikai.h b/esphome/components/weikai/weikai.h index 4440d9414e1..43c3a1e4f4c 100644 --- a/esphome/components/weikai/weikai.h +++ b/esphome/components/weikai/weikai.h @@ -374,7 +374,7 @@ class WeikaiChannel : public uart::UARTComponent { /// @brief Returns the number of bytes in the receive buffer /// @return the number of bytes available in the receiver fifo - int available() override; + size_t available() override; /// @brief Flush the output fifo. /// @details If we refer to Serial.flush() in Arduino it says: ** Waits for the transmission of outgoing serial data diff --git a/tests/components/uart/common.h b/tests/components/uart/common.h index 5597b864103..1f9bfa15e7f 100644 --- a/tests/components/uart/common.h +++ b/tests/components/uart/common.h @@ -29,7 +29,7 @@ class MockUARTComponent : public UARTComponent { MOCK_METHOD(bool, read_array, (uint8_t * data, size_t len), (override)); MOCK_METHOD(bool, peek_byte, (uint8_t * data), (override)); - MOCK_METHOD(int, available, (), (override)); + MOCK_METHOD(size_t, available, (), (override)); MOCK_METHOD(void, flush, (), (override)); MOCK_METHOD(void, check_logger_conflict, (), (override)); }; From 7c1327f96ae904641800650deb4c95348fc23fd4 Mon Sep 17 00:00:00 2001 From: George Joseph Date: Mon, 9 Feb 2026 15:44:47 -0700 Subject: [PATCH 03/15] [mipi_dsi] Add WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD 3.4C and 4C (#13840) --- .../components/mipi_dsi/models/waveshare.py | 98 +++++++++++++++++++ 1 file changed, 98 insertions(+) diff --git a/esphome/components/mipi_dsi/models/waveshare.py b/esphome/components/mipi_dsi/models/waveshare.py index f83cacc5a3e..bf4f9063bb8 100644 --- a/esphome/components/mipi_dsi/models/waveshare.py +++ b/esphome/components/mipi_dsi/models/waveshare.py @@ -120,3 +120,101 @@ DriverChip( (0xB2, 0x10), ], ) + +DriverChip( + "WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-3.4C", + height=800, + width=800, + hsync_back_porch=20, + hsync_pulse_width=20, + hsync_front_porch=40, + vsync_back_porch=12, + vsync_pulse_width=4, + vsync_front_porch=24, + pclk_frequency="80MHz", + lane_bit_rate="1.5Gbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + initsequence=[ + (0xE0, 0x00), # select userpage + (0xE1, 0x93), (0xE2, 0x65), (0xE3, 0xF8), + (0x80, 0x01), # Select number of lanes (2) + (0xE0, 0x01), # select page 1 + (0x00, 0x00), (0x01, 0x41), (0x03, 0x10), (0x04, 0x44), (0x17, 0x00), (0x18, 0xD0), (0x19, 0x00), (0x1A, 0x00), + (0x1B, 0xD0), (0x1C, 0x00), (0x24, 0xFE), (0x35, 0x26), (0x37, 0x09), (0x38, 0x04), (0x39, 0x08), (0x3A, 0x0A), + (0x3C, 0x78), (0x3D, 0xFF), (0x3E, 0xFF), (0x3F, 0xFF), (0x40, 0x00), (0x41, 0x64), (0x42, 0xC7), (0x43, 0x18), + (0x44, 0x0B), (0x45, 0x14), (0x55, 0x02), (0x57, 0x49), (0x59, 0x0A), (0x5A, 0x1B), (0x5B, 0x19), (0x5D, 0x7F), + (0x5E, 0x56), (0x5F, 0x43), (0x60, 0x37), (0x61, 0x33), (0x62, 0x25), (0x63, 0x2A), (0x64, 0x16), (0x65, 0x30), + (0x66, 0x2F), (0x67, 0x32), (0x68, 0x53), (0x69, 0x43), (0x6A, 0x4C), (0x6B, 0x40), (0x6C, 0x3D), (0x6D, 0x31), + (0x6E, 0x20), (0x6F, 0x0F), (0x70, 0x7F), (0x71, 0x56), (0x72, 0x43), (0x73, 0x37), (0x74, 0x33), (0x75, 0x25), + (0x76, 0x2A), (0x77, 0x16), (0x78, 0x30), (0x79, 0x2F), (0x7A, 0x32), (0x7B, 0x53), (0x7C, 0x43), (0x7D, 0x4C), + (0x7E, 0x40), (0x7F, 0x3D), (0x80, 0x31), (0x81, 0x20), (0x82, 0x0F), + (0xE0, 0x02), # select page 2 + (0x00, 0x5F), (0x01, 0x5F), (0x02, 0x5E), (0x03, 0x5E), (0x04, 0x50), (0x05, 0x48), (0x06, 0x48), (0x07, 0x4A), + (0x08, 0x4A), (0x09, 0x44), (0x0A, 0x44), (0x0B, 0x46), (0x0C, 0x46), (0x0D, 0x5F), (0x0E, 0x5F), (0x0F, 0x57), + (0x10, 0x57), (0x11, 0x77), (0x12, 0x77), (0x13, 0x40), (0x14, 0x42), (0x15, 0x5F), (0x16, 0x5F), (0x17, 0x5F), + (0x18, 0x5E), (0x19, 0x5E), (0x1A, 0x50), (0x1B, 0x49), (0x1C, 0x49), (0x1D, 0x4B), (0x1E, 0x4B), (0x1F, 0x45), + (0x20, 0x45), (0x21, 0x47), (0x22, 0x47), (0x23, 0x5F), (0x24, 0x5F), (0x25, 0x57), (0x26, 0x57), (0x27, 0x77), + (0x28, 0x77), (0x29, 0x41), (0x2A, 0x43), (0x2B, 0x5F), (0x2C, 0x1E), (0x2D, 0x1E), (0x2E, 0x1F), (0x2F, 0x1F), + (0x30, 0x10), (0x31, 0x07), (0x32, 0x07), (0x33, 0x05), (0x34, 0x05), (0x35, 0x0B), (0x36, 0x0B), (0x37, 0x09), + (0x38, 0x09), (0x39, 0x1F), (0x3A, 0x1F), (0x3B, 0x17), (0x3C, 0x17), (0x3D, 0x17), (0x3E, 0x17), (0x3F, 0x03), + (0x40, 0x01), (0x41, 0x1F), (0x42, 0x1E), (0x43, 0x1E), (0x44, 0x1F), (0x45, 0x1F), (0x46, 0x10), (0x47, 0x06), + (0x48, 0x06), (0x49, 0x04), (0x4A, 0x04), (0x4B, 0x0A), (0x4C, 0x0A), (0x4D, 0x08), (0x4E, 0x08), (0x4F, 0x1F), + (0x50, 0x1F), (0x51, 0x17), (0x52, 0x17), (0x53, 0x17), (0x54, 0x17), (0x55, 0x02), (0x56, 0x00), (0x57, 0x1F), + (0xE0, 0x02), # select page 2 + (0x58, 0x40), (0x59, 0x00), (0x5A, 0x00), (0x5B, 0x30), (0x5C, 0x01), (0x5D, 0x30), (0x5E, 0x01), (0x5F, 0x02), + (0x60, 0x30), (0x61, 0x03), (0x62, 0x04), (0x63, 0x04), (0x64, 0xA6), (0x65, 0x43), (0x66, 0x30), (0x67, 0x73), + (0x68, 0x05), (0x69, 0x04), (0x6A, 0x7F), (0x6B, 0x08), (0x6C, 0x00), (0x6D, 0x04), (0x6E, 0x04), (0x6F, 0x88), + (0x75, 0xD9), (0x76, 0x00), (0x77, 0x33), (0x78, 0x43), + (0xE0, 0x00), # select userpage + ], +) + +DriverChip( + "WAVESHARE-ESP32-P4-WIFI6-TOUCH-LCD-4C", + height=720, + width=720, + hsync_back_porch=20, + hsync_pulse_width=20, + hsync_front_porch=40, + vsync_back_porch=12, + vsync_pulse_width=4, + vsync_front_porch=24, + pclk_frequency="80MHz", + lane_bit_rate="1.5Gbps", + swap_xy=cv.UNDEFINED, + color_order="RGB", + initsequence=[ + (0xE0, 0x00), # select userpage + (0xE1, 0x93), (0xE2, 0x65), (0xE3, 0xF8), + (0x80, 0x01), # Select number of lanes (2) + (0xE0, 0x01), # select page 1 + (0x00, 0x00), (0x01, 0x41), (0x03, 0x10), (0x04, 0x44), (0x17, 0x00), (0x18, 0xD0), (0x19, 0x00), (0x1A, 0x00), + (0x1B, 0xD0), (0x1C, 0x00), (0x24, 0xFE), (0x35, 0x26), (0x37, 0x09), (0x38, 0x04), (0x39, 0x08), (0x3A, 0x0A), + (0x3C, 0x78), (0x3D, 0xFF), (0x3E, 0xFF), (0x3F, 0xFF), (0x40, 0x04), (0x41, 0x64), (0x42, 0xC7), (0x43, 0x18), + (0x44, 0x0B), (0x45, 0x14), (0x55, 0x02), (0x57, 0x49), (0x59, 0x0A), (0x5A, 0x1B), (0x5B, 0x19), (0x5D, 0x7F), + (0x5E, 0x56), (0x5F, 0x43), (0x60, 0x37), (0x61, 0x33), (0x62, 0x25), (0x63, 0x2A), (0x64, 0x16), (0x65, 0x30), + (0x66, 0x2F), (0x67, 0x32), (0x68, 0x53), (0x69, 0x43), (0x6A, 0x4C), (0x6B, 0x40), (0x6C, 0x3D), (0x6D, 0x31), + (0x6E, 0x20), (0x6F, 0x0F), (0x70, 0x7F), (0x71, 0x56), (0x72, 0x43), (0x73, 0x37), (0x74, 0x33), (0x75, 0x25), + (0x76, 0x2A), (0x77, 0x16), (0x78, 0x30), (0x79, 0x2F), (0x7A, 0x32), (0x7B, 0x53), (0x7C, 0x43), (0x7D, 0x4C), + (0x7E, 0x40), (0x7F, 0x3D), (0x80, 0x31), (0x81, 0x20), (0x82, 0x0F), + (0xE0, 0x02), # select page 2 + (0x00, 0x5F), (0x01, 0x5F), (0x02, 0x5E), (0x03, 0x5E), (0x04, 0x50), (0x05, 0x48), (0x06, 0x48), (0x07, 0x4A), + (0x08, 0x4A), (0x09, 0x44), (0x0A, 0x44), (0x0B, 0x46), (0x0C, 0x46), (0x0D, 0x5F), (0x0E, 0x5F), (0x0F, 0x57), + (0x10, 0x57), (0x11, 0x77), (0x12, 0x77), (0x13, 0x40), (0x14, 0x42), (0x15, 0x5F), (0x16, 0x5F), (0x17, 0x5F), + (0x18, 0x5E), (0x19, 0x5E), (0x1A, 0x50), (0x1B, 0x49), (0x1C, 0x49), (0x1D, 0x4B), (0x1E, 0x4B), (0x1F, 0x45), + (0x20, 0x45), (0x21, 0x47), (0x22, 0x47), (0x23, 0x5F), (0x24, 0x5F), (0x25, 0x57), (0x26, 0x57), (0x27, 0x77), + (0x28, 0x77), (0x29, 0x41), (0x2A, 0x43), (0x2B, 0x5F), (0x2C, 0x1E), (0x2D, 0x1E), (0x2E, 0x1F), (0x2F, 0x1F), + (0x30, 0x10), (0x31, 0x07), (0x32, 0x07), (0x33, 0x05), (0x34, 0x05), (0x35, 0x0B), (0x36, 0x0B), (0x37, 0x09), + (0x38, 0x09), (0x39, 0x1F), (0x3A, 0x1F), (0x3B, 0x17), (0x3C, 0x17), (0x3D, 0x17), (0x3E, 0x17), (0x3F, 0x03), + (0x40, 0x01), (0x41, 0x1F), (0x42, 0x1E), (0x43, 0x1E), (0x44, 0x1F), (0x45, 0x1F), (0x46, 0x10), (0x47, 0x06), + (0x48, 0x06), (0x49, 0x04), (0x4A, 0x04), (0x4B, 0x0A), (0x4C, 0x0A), (0x4D, 0x08), (0x4E, 0x08), (0x4F, 0x1F), + (0x50, 0x1F), (0x51, 0x17), (0x52, 0x17), (0x53, 0x17), (0x54, 0x17), (0x55, 0x02), (0x56, 0x00), (0x57, 0x1F), + (0xE0, 0x02), # select page 2 + (0x58, 0x40), (0x59, 0x00), (0x5A, 0x00), (0x5B, 0x30), (0x5C, 0x01), (0x5D, 0x30), (0x5E, 0x01), (0x5F, 0x02), + (0x60, 0x30), (0x61, 0x03), (0x62, 0x04), (0x63, 0x04), (0x64, 0xA6), (0x65, 0x43), (0x66, 0x30), (0x67, 0x73), + (0x68, 0x05), (0x69, 0x04), (0x6A, 0x7F), (0x6B, 0x08), (0x6C, 0x00), (0x6D, 0x04), (0x6E, 0x04), (0x6F, 0x88), + (0x75, 0xD9), (0x76, 0x00), (0x77, 0x33), (0x78, 0x43), + (0xE0, 0x00), # select userpage + ] +) From 3767c5ec91c7ab483a8a68982b82cdd028cc5546 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Feb 2026 16:48:08 -0600 Subject: [PATCH 04/15] [scheduler] Make core timer ID collisions impossible with type-safe internal IDs (#13882) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> From dacc557a16232b4e51e47609fa94ce7f66808c70 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Feb 2026 18:15:48 -0600 Subject: [PATCH 05/15] [uart] Convert parity_to_str to PROGMEM_STRING_TABLE (#13805) --- esphome/components/uart/uart.cpp | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/esphome/components/uart/uart.cpp b/esphome/components/uart/uart.cpp index 6cfd6537a59..337fa352c14 100644 --- a/esphome/components/uart/uart.cpp +++ b/esphome/components/uart/uart.cpp @@ -3,12 +3,16 @@ #include "esphome/core/defines.h" #include "esphome/core/helpers.h" #include "esphome/core/log.h" +#include "esphome/core/progmem.h" #include namespace esphome::uart { static const char *const TAG = "uart"; +// UART parity strings indexed by UARTParityOptions enum (0-2): NONE, EVEN, ODD +PROGMEM_STRING_TABLE(UARTParityStrings, "NONE", "EVEN", "ODD", "UNKNOWN"); + void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UARTParityOptions parity, uint8_t data_bits) { if (this->parent_->get_baud_rate() != baud_rate) { @@ -30,16 +34,7 @@ void UARTDevice::check_uart_settings(uint32_t baud_rate, uint8_t stop_bits, UART } const LogString *parity_to_str(UARTParityOptions parity) { - switch (parity) { - case UART_CONFIG_PARITY_NONE: - return LOG_STR("NONE"); - case UART_CONFIG_PARITY_EVEN: - return LOG_STR("EVEN"); - case UART_CONFIG_PARITY_ODD: - return LOG_STR("ODD"); - default: - return LOG_STR("UNKNOWN"); - } + return UARTParityStrings::get_log_str(static_cast(parity), UARTParityStrings::LAST_INDEX); } } // namespace esphome::uart From 78df8be31fd061cba7a09179db45977aba08e6e5 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Feb 2026 18:16:27 -0600 Subject: [PATCH 06/15] [logger] Resolve thread name once and pass through logging chain (#13836) --- esphome/components/logger/logger.cpp | 43 ++++--- esphome/components/logger/logger.h | 114 +++++++++--------- .../logger/task_log_buffer_esp32.cpp | 3 +- .../components/logger/task_log_buffer_esp32.h | 2 +- .../logger/task_log_buffer_host.cpp | 12 +- .../components/logger/task_log_buffer_host.h | 3 +- .../logger/task_log_buffer_libretiny.cpp | 3 +- .../logger/task_log_buffer_libretiny.h | 2 +- 8 files changed, 93 insertions(+), 89 deletions(-) diff --git a/esphome/components/logger/logger.cpp b/esphome/components/logger/logger.cpp index 4cbd4f1bf1b..bb20c403e53 100644 --- a/esphome/components/logger/logger.cpp +++ b/esphome/components/logger/logger.cpp @@ -36,8 +36,9 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch #endif // Fast path: main thread, no recursion (99.9% of all logs) + // Pass nullptr for thread_name since we already know this is the main task if (is_main_task && !this->main_task_recursion_guard_) [[likely]] { - this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args); + this->log_message_to_buffer_and_send_(this->main_task_recursion_guard_, level, tag, line, format, args, nullptr); return; } @@ -47,21 +48,23 @@ void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const ch } // Non-main thread handling (~0.1% of logs) + // Resolve thread name once and pass it through the logging chain. + // ESP32/LibreTiny: use TaskHandle_t overload to avoid redundant xTaskGetCurrentTaskHandle() + // (we already have the handle from the main task check above). + // Host: pass a stack buffer for pthread_getname_np to write into. #if defined(USE_ESP32) || defined(USE_LIBRETINY) - this->log_vprintf_non_main_thread_(level, tag, line, format, args, current_task); + const char *thread_name = get_thread_name_(current_task); #else // USE_HOST - this->log_vprintf_non_main_thread_(level, tag, line, format, args); + char thread_name_buf[THREAD_NAME_BUF_SIZE]; + const char *thread_name = this->get_thread_name_(thread_name_buf); #endif + this->log_vprintf_non_main_thread_(level, tag, line, format, args, thread_name); } // Handles non-main thread logging only // Kept separate from hot path to improve instruction cache performance -#if defined(USE_ESP32) || defined(USE_LIBRETINY) void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args, - TaskHandle_t current_task) { -#else // USE_HOST -void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args) { -#endif + const char *thread_name) { // Check if already in recursion for this non-main thread/task if (this->is_non_main_task_recursive_()) { return; @@ -73,12 +76,8 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li bool message_sent = false; #ifdef USE_ESPHOME_TASK_LOG_BUFFER // For non-main threads/tasks, queue the message for callbacks -#if defined(USE_ESP32) || defined(USE_LIBRETINY) message_sent = - this->log_buffer_->send_message_thread_safe(level, tag, static_cast(line), current_task, format, args); -#else // USE_HOST - message_sent = this->log_buffer_->send_message_thread_safe(level, tag, static_cast(line), format, args); -#endif + this->log_buffer_->send_message_thread_safe(level, tag, static_cast(line), thread_name, format, args); if (message_sent) { // Enable logger loop to process the buffered message // This is safe to call from any context including ISRs @@ -101,19 +100,27 @@ void Logger::log_vprintf_non_main_thread_(uint8_t level, const char *tag, int li #endif char console_buffer[MAX_CONSOLE_LOG_MSG_SIZE]; // MUST be stack allocated for thread safety LogBuffer buf{console_buffer, MAX_CONSOLE_LOG_MSG_SIZE}; - this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf); + this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name); this->write_to_console_(buf); } // RAII guard automatically resets on return } #else -// Implementation for all other platforms (single-task, no threading) +// Implementation for single-task platforms (ESP8266, RP2040, Zephyr) +// TODO: Zephyr may have multiple threads (work queues, etc.) but uses this single-task path. +// Logging calls are NOT thread-safe: global_recursion_guard_ is a plain bool and tx_buffer_ has no locking. +// Not a problem in practice yet since Zephyr has no API support (logs are console-only). void HOT Logger::log_vprintf_(uint8_t level, const char *tag, int line, const char *format, va_list args) { // NOLINT if (level > this->level_for(tag) || global_recursion_guard_) return; - - this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args); +#ifdef USE_ZEPHYR + char tmp[MAX_POINTER_REPRESENTATION]; + this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, + this->get_thread_name_(tmp)); +#else // Other single-task platforms don't have thread names, so pass nullptr + this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr); +#endif } #endif // USE_ESP32 / USE_HOST / USE_LIBRETINY @@ -129,7 +136,7 @@ void Logger::log_vprintf_(uint8_t level, const char *tag, int line, const __Flas if (level > this->level_for(tag) || global_recursion_guard_) return; - this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args); + this->log_message_to_buffer_and_send_(global_recursion_guard_, level, tag, line, format, args, nullptr); } #endif // USE_STORE_LOG_STR_IN_FLASH diff --git a/esphome/components/logger/logger.h b/esphome/components/logger/logger.h index 1678fed5f5e..ecf032ee0ec 100644 --- a/esphome/components/logger/logger.h +++ b/esphome/components/logger/logger.h @@ -2,6 +2,7 @@ #include #include +#include #include #if defined(USE_ESP32) || defined(USE_HOST) #include @@ -124,6 +125,10 @@ static constexpr uint16_t MAX_HEADER_SIZE = 128; // "0x" + 2 hex digits per byte + '\0' static constexpr size_t MAX_POINTER_REPRESENTATION = 2 + sizeof(void *) * 2 + 1; +// Stack buffer size for retrieving thread/task names from the OS +// macOS allows up to 64 bytes, Linux up to 16 +static constexpr size_t THREAD_NAME_BUF_SIZE = 64; + // Buffer wrapper for log formatting functions struct LogBuffer { char *data; @@ -408,34 +413,24 @@ class Logger : public Component { #if defined(USE_ESP32) || defined(USE_HOST) || defined(USE_LIBRETINY) // Handles non-main thread logging only (~0.1% of calls) -#if defined(USE_ESP32) || defined(USE_LIBRETINY) - // ESP32/LibreTiny: Pass task handle to avoid calling xTaskGetCurrentTaskHandle() twice + // thread_name is resolved by the caller from the task handle, avoiding redundant lookups void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args, - TaskHandle_t current_task); -#else // USE_HOST - // Host: No task handle parameter needed (not used in send_message_thread_safe) - void log_vprintf_non_main_thread_(uint8_t level, const char *tag, int line, const char *format, va_list args); -#endif + const char *thread_name); #endif void process_messages_(); void write_msg_(const char *msg, uint16_t len); // Format a log message with printf-style arguments and write it to a buffer with header, footer, and null terminator + // thread_name: name of the calling thread/task, or nullptr for main task (callers already know which task they're on) inline void HOT format_log_to_buffer_with_terminator_(uint8_t level, const char *tag, int line, const char *format, - va_list args, LogBuffer &buf) { -#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_HOST) - buf.write_header(level, tag, line, this->get_thread_name_()); -#elif defined(USE_ZEPHYR) - char tmp[MAX_POINTER_REPRESENTATION]; - buf.write_header(level, tag, line, this->get_thread_name_(tmp)); -#else - buf.write_header(level, tag, line, nullptr); -#endif + va_list args, LogBuffer &buf, const char *thread_name) { + buf.write_header(level, tag, line, thread_name); buf.format_body(format, args); } #ifdef USE_STORE_LOG_STR_IN_FLASH // Format a log message with flash string format and write it to a buffer with header, footer, and null terminator + // ESP8266-only (single-task), thread_name is always nullptr inline void HOT format_log_to_buffer_with_terminator_P_(uint8_t level, const char *tag, int line, const __FlashStringHelper *format, va_list args, LogBuffer &buf) { @@ -466,9 +461,10 @@ class Logger : public Component { // Helper to format and send a log message to both console and listeners // Template handles both const char* (RAM) and __FlashStringHelper* (flash) format strings + // thread_name: name of the calling thread/task, or nullptr for main task template inline void HOT log_message_to_buffer_and_send_(bool &recursion_guard, uint8_t level, const char *tag, int line, - FormatType format, va_list args) { + FormatType format, va_list args, const char *thread_name) { RecursionGuard guard(recursion_guard); LogBuffer buf{this->tx_buffer_, this->tx_buffer_size_}; #ifdef USE_STORE_LOG_STR_IN_FLASH @@ -477,7 +473,7 @@ class Logger : public Component { } else #endif { - this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf); + this->format_log_to_buffer_with_terminator_(level, tag, line, format, args, buf, thread_name); } this->notify_listeners_(level, tag, buf); this->write_log_buffer_to_console_(buf); @@ -565,37 +561,57 @@ class Logger : public Component { bool global_recursion_guard_{false}; // Simple global recursion guard for single-task platforms #endif -#if defined(USE_ESP32) || defined(USE_LIBRETINY) || defined(USE_ZEPHYR) - const char *HOT get_thread_name_( -#ifdef USE_ZEPHYR - char *buff + // --- get_thread_name_ overloads (per-platform) --- + +#if defined(USE_ESP32) || defined(USE_LIBRETINY) + // Primary overload - takes a task handle directly to avoid redundant xTaskGetCurrentTaskHandle() calls + // when the caller already has the handle (e.g. from the main task check in log_vprintf_) + const char *get_thread_name_(TaskHandle_t task) { + if (task == this->main_task_) { + return nullptr; // Main task + } +#if defined(USE_ESP32) + return pcTaskGetName(task); +#elif defined(USE_LIBRETINY) + return pcTaskGetTaskName(task); #endif - ) { -#ifdef USE_ZEPHYR + } + + // Convenience overload - gets the current task handle and delegates + const char *HOT get_thread_name_() { return this->get_thread_name_(xTaskGetCurrentTaskHandle()); } + +#elif defined(USE_HOST) + // Takes a caller-provided buffer for the thread name (stack-allocated for thread safety) + const char *HOT get_thread_name_(std::span buff) { + pthread_t current_thread = pthread_self(); + if (pthread_equal(current_thread, main_thread_)) { + return nullptr; // Main thread + } + // For non-main threads, get the thread name into the caller-provided buffer + if (pthread_getname_np(current_thread, buff.data(), buff.size()) == 0) { + return buff.data(); + } + return nullptr; + } + +#elif defined(USE_ZEPHYR) + const char *HOT get_thread_name_(std::span buff) { k_tid_t current_task = k_current_get(); -#else - TaskHandle_t current_task = xTaskGetCurrentTaskHandle(); -#endif if (current_task == main_task_) { return nullptr; // Main task - } else { -#if defined(USE_ESP32) - return pcTaskGetName(current_task); -#elif defined(USE_LIBRETINY) - return pcTaskGetTaskName(current_task); -#elif defined(USE_ZEPHYR) - const char *name = k_thread_name_get(current_task); - if (name) { - // zephyr print task names only if debug component is present - return name; - } - std::snprintf(buff, MAX_POINTER_REPRESENTATION, "%p", current_task); - return buff; -#endif } + const char *name = k_thread_name_get(current_task); + if (name) { + // zephyr print task names only if debug component is present + return name; + } + std::snprintf(buff.data(), buff.size(), "%p", current_task); + return buff.data(); } #endif + // --- Non-main task recursion guards (per-platform) --- + #if defined(USE_ESP32) || defined(USE_HOST) // RAII guard for non-main task recursion using pthread TLS class NonMainTaskRecursionGuard { @@ -635,22 +651,6 @@ class Logger : public Component { inline RecursionGuard make_non_main_task_guard_() { return RecursionGuard(non_main_task_recursion_guard_); } #endif -#ifdef USE_HOST - const char *HOT get_thread_name_() { - pthread_t current_thread = pthread_self(); - if (pthread_equal(current_thread, main_thread_)) { - return nullptr; // Main thread - } - // For non-main threads, return the thread name - // We store it in thread-local storage to avoid allocation - static thread_local char thread_name_buf[32]; - if (pthread_getname_np(current_thread, thread_name_buf, sizeof(thread_name_buf)) == 0) { - return thread_name_buf; - } - return nullptr; - } -#endif - #if defined(USE_ESP32) || defined(USE_LIBRETINY) // Disable loop when task buffer is empty (with USB CDC check on ESP32) inline void disable_loop_when_buffer_empty_() { diff --git a/esphome/components/logger/task_log_buffer_esp32.cpp b/esphome/components/logger/task_log_buffer_esp32.cpp index b9dfe45b7fa..56c0a4ae2d4 100644 --- a/esphome/components/logger/task_log_buffer_esp32.cpp +++ b/esphome/components/logger/task_log_buffer_esp32.cpp @@ -59,7 +59,7 @@ void TaskLogBuffer::release_message_main_loop(void *token) { last_processed_counter_ = message_counter_.load(std::memory_order_relaxed); } -bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle, +bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, const char *format, va_list args) { // First, calculate the exact length needed using a null buffer (no actual writing) va_list args_copy; @@ -95,7 +95,6 @@ bool TaskLogBuffer::send_message_thread_safe(uint8_t level, const char *tag, uin // Store the thread name now instead of waiting until main loop processing // This avoids crashes if the task completes or is deleted between when this message // is enqueued and when it's processed by the main loop - const char *thread_name = pcTaskGetName(task_handle); if (thread_name != nullptr) { strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1); msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; // Ensure null termination diff --git a/esphome/components/logger/task_log_buffer_esp32.h b/esphome/components/logger/task_log_buffer_esp32.h index fde9bd60d5e..6c1bafaeba3 100644 --- a/esphome/components/logger/task_log_buffer_esp32.h +++ b/esphome/components/logger/task_log_buffer_esp32.h @@ -58,7 +58,7 @@ class TaskLogBuffer { void release_message_main_loop(void *token); // Thread-safe - send a message to the ring buffer from any thread - bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle, + bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, const char *format, va_list args); // Check if there are messages ready to be processed using an atomic counter for performance diff --git a/esphome/components/logger/task_log_buffer_host.cpp b/esphome/components/logger/task_log_buffer_host.cpp index 0660aeb0611..676686304a3 100644 --- a/esphome/components/logger/task_log_buffer_host.cpp +++ b/esphome/components/logger/task_log_buffer_host.cpp @@ -70,8 +70,8 @@ void TaskLogBufferHost::commit_write_slot_(int slot_index) { } } -bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format, - va_list args) { +bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, + const char *format, va_list args) { // Acquire a slot int slot_index = this->acquire_write_slot_(); if (slot_index < 0) { @@ -85,11 +85,9 @@ bool TaskLogBufferHost::send_message_thread_safe(uint8_t level, const char *tag, msg.tag = tag; msg.line = line; - // Get thread name using pthread - char thread_name_buf[LogMessage::MAX_THREAD_NAME_SIZE]; - // pthread_getname_np works the same on Linux and macOS - if (pthread_getname_np(pthread_self(), thread_name_buf, sizeof(thread_name_buf)) == 0) { - strncpy(msg.thread_name, thread_name_buf, sizeof(msg.thread_name) - 1); + // Store the thread name now to avoid crashes if thread exits before processing + if (thread_name != nullptr) { + strncpy(msg.thread_name, thread_name, sizeof(msg.thread_name) - 1); msg.thread_name[sizeof(msg.thread_name) - 1] = '\0'; } else { msg.thread_name[0] = '\0'; diff --git a/esphome/components/logger/task_log_buffer_host.h b/esphome/components/logger/task_log_buffer_host.h index d421d50ec6a..f8e4ee7bee5 100644 --- a/esphome/components/logger/task_log_buffer_host.h +++ b/esphome/components/logger/task_log_buffer_host.h @@ -86,7 +86,8 @@ class TaskLogBufferHost { // Thread-safe - send a message to the buffer from any thread // Returns true if message was queued, false if buffer is full - bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *format, va_list args); + bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, + const char *format, va_list args); // Check if there are messages ready to be processed inline bool HOT has_messages() const { diff --git a/esphome/components/logger/task_log_buffer_libretiny.cpp b/esphome/components/logger/task_log_buffer_libretiny.cpp index 580066e621d..5a22857dcb1 100644 --- a/esphome/components/logger/task_log_buffer_libretiny.cpp +++ b/esphome/components/logger/task_log_buffer_libretiny.cpp @@ -101,7 +101,7 @@ void TaskLogBufferLibreTiny::release_message_main_loop() { } bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, - TaskHandle_t task_handle, const char *format, va_list args) { + const char *thread_name, const char *format, va_list args) { // First, calculate the exact length needed using a null buffer (no actual writing) va_list args_copy; va_copy(args_copy, args); @@ -162,7 +162,6 @@ bool TaskLogBufferLibreTiny::send_message_thread_safe(uint8_t level, const char msg->line = line; // Store the thread name now to avoid crashes if task is deleted before processing - const char *thread_name = pcTaskGetTaskName(task_handle); if (thread_name != nullptr) { strncpy(msg->thread_name, thread_name, sizeof(msg->thread_name) - 1); msg->thread_name[sizeof(msg->thread_name) - 1] = '\0'; diff --git a/esphome/components/logger/task_log_buffer_libretiny.h b/esphome/components/logger/task_log_buffer_libretiny.h index bf6b2d2fa4b..bf315e828a5 100644 --- a/esphome/components/logger/task_log_buffer_libretiny.h +++ b/esphome/components/logger/task_log_buffer_libretiny.h @@ -70,7 +70,7 @@ class TaskLogBufferLibreTiny { void release_message_main_loop(); // Thread-safe - send a message to the buffer from any thread - bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, TaskHandle_t task_handle, + bool send_message_thread_safe(uint8_t level, const char *tag, uint16_t line, const char *thread_name, const char *format, va_list args); // Fast check using volatile counter - no lock needed From bcd4a9fc39a3a35e4ea58d28f2bfc9495e7790bd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Feb 2026 18:20:53 -0600 Subject: [PATCH 07/15] [pylontech] Batch UART reads to reduce loop overhead (#13824) --- esphome/components/pylontech/pylontech.cpp | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/esphome/components/pylontech/pylontech.cpp b/esphome/components/pylontech/pylontech.cpp index 1dc7caaf166..d7242532564 100644 --- a/esphome/components/pylontech/pylontech.cpp +++ b/esphome/components/pylontech/pylontech.cpp @@ -56,17 +56,23 @@ void PylontechComponent::setup() { void PylontechComponent::update() { this->write_str("pwr\n"); } void PylontechComponent::loop() { - if (this->available() > 0) { + int avail = this->available(); + if (avail > 0) { // pylontech sends a lot of data very suddenly // we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow - uint8_t data; int recv = 0; - while (this->available() > 0) { - if (this->read_byte(&data)) { - buffer_[buffer_index_write_] += (char) data; - recv++; - if (buffer_[buffer_index_write_].back() == static_cast(ASCII_LF) || - buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) { + uint8_t buf[64]; + while (avail > 0) { + size_t to_read = std::min(static_cast(avail), sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; + } + avail -= to_read; + recv += to_read; + + for (size_t i = 0; i < to_read; i++) { + buffer_[buffer_index_write_] += (char) buf[i]; + if (buf[i] == ASCII_LF || buffer_[buffer_index_write_].length() >= MAX_DATA_LENGTH_BYTES) { // complete line received buffer_index_write_ = (buffer_index_write_ + 1) % NUM_BUFFERS; } From 2edfcf278fb8cb549b175701b4a70fe96c742eda Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Feb 2026 18:21:10 -0600 Subject: [PATCH 08/15] [hlk_fm22x] Replace per-cycle vector allocation with member buffer (#13859) --- esphome/components/hlk_fm22x/hlk_fm22x.cpp | 62 +++++++++++++--------- esphome/components/hlk_fm22x/hlk_fm22x.h | 10 ++-- 2 files changed, 45 insertions(+), 27 deletions(-) diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.cpp b/esphome/components/hlk_fm22x/hlk_fm22x.cpp index c0f14c7105c..18d26f057a8 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.cpp +++ b/esphome/components/hlk_fm22x/hlk_fm22x.cpp @@ -1,20 +1,16 @@ #include "hlk_fm22x.h" #include "esphome/core/log.h" #include "esphome/core/helpers.h" -#include #include namespace esphome::hlk_fm22x { static const char *const TAG = "hlk_fm22x"; -// Maximum response size is 36 bytes (VERIFY reply: face_id + 32-byte name) -static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36; - void HlkFm22xComponent::setup() { ESP_LOGCONFIG(TAG, "Setting up HLK-FM22X..."); this->set_enrolling_(false); - while (this->available()) { + while (this->available() > 0) { this->read(); } this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_STATUS); }); @@ -35,7 +31,7 @@ void HlkFm22xComponent::update() { } void HlkFm22xComponent::enroll_face(const std::string &name, HlkFm22xFaceDirection direction) { - if (name.length() > 31) { + if (name.length() > HLK_FM22X_NAME_SIZE - 1) { ESP_LOGE(TAG, "enroll_face(): name too long '%s'", name.c_str()); return; } @@ -88,7 +84,7 @@ void HlkFm22xComponent::send_command_(HlkFm22xCommand command, const uint8_t *da } this->wait_cycles_ = 0; this->active_command_ = command; - while (this->available()) + while (this->available() > 0) this->read(); this->write((uint8_t) (START_CODE >> 8)); this->write((uint8_t) (START_CODE & 0xFF)); @@ -137,17 +133,24 @@ void HlkFm22xComponent::recv_command_() { checksum ^= byte; length |= byte; - std::vector data; - data.reserve(length); + if (length > HLK_FM22X_MAX_RESPONSE_SIZE) { + ESP_LOGE(TAG, "Response too large: %u bytes", length); + // Discard exactly the remaining payload and checksum for this frame + for (uint16_t i = 0; i < length + 1 && this->available() > 0; ++i) + this->read(); + return; + } + for (uint16_t idx = 0; idx < length; ++idx) { byte = this->read(); checksum ^= byte; - data.push_back(byte); + this->recv_buf_[idx] = byte; } #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE char hex_buf[format_hex_pretty_size(HLK_FM22X_MAX_RESPONSE_SIZE)]; - ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, format_hex_pretty_to(hex_buf, data.data(), data.size())); + ESP_LOGV(TAG, "Recv type: 0x%.2X, data: %s", response_type, + format_hex_pretty_to(hex_buf, this->recv_buf_.data(), length)); #endif byte = this->read(); @@ -157,10 +160,10 @@ void HlkFm22xComponent::recv_command_() { } switch (response_type) { case HlkFm22xResponseType::NOTE: - this->handle_note_(data); + this->handle_note_(this->recv_buf_.data(), length); break; case HlkFm22xResponseType::REPLY: - this->handle_reply_(data); + this->handle_reply_(this->recv_buf_.data(), length); break; default: ESP_LOGW(TAG, "Unexpected response type: 0x%.2X", response_type); @@ -168,11 +171,15 @@ void HlkFm22xComponent::recv_command_() { } } -void HlkFm22xComponent::handle_note_(const std::vector &data) { +void HlkFm22xComponent::handle_note_(const uint8_t *data, size_t length) { + if (length < 1) { + ESP_LOGE(TAG, "Empty note data"); + return; + } switch (data[0]) { case HlkFm22xNoteType::FACE_STATE: - if (data.size() < 17) { - ESP_LOGE(TAG, "Invalid face note data size: %u", data.size()); + if (length < 17) { + ESP_LOGE(TAG, "Invalid face note data size: %zu", length); break; } { @@ -209,9 +216,13 @@ void HlkFm22xComponent::handle_note_(const std::vector &data) { } } -void HlkFm22xComponent::handle_reply_(const std::vector &data) { +void HlkFm22xComponent::handle_reply_(const uint8_t *data, size_t length) { auto expected = this->active_command_; this->active_command_ = HlkFm22xCommand::NONE; + if (length < 2) { + ESP_LOGE(TAG, "Reply too short: %zu bytes", length); + return; + } if (data[0] != (uint8_t) expected) { ESP_LOGE(TAG, "Unexpected response command. Expected: 0x%.2X, Received: 0x%.2X", expected, data[0]); return; @@ -238,16 +249,20 @@ void HlkFm22xComponent::handle_reply_(const std::vector &data) { } switch (expected) { case HlkFm22xCommand::VERIFY: { + if (length < 4 + HLK_FM22X_NAME_SIZE) { + ESP_LOGE(TAG, "VERIFY response too short: %zu bytes", length); + break; + } int16_t face_id = ((int16_t) data[2] << 8) | data[3]; - std::string name(data.begin() + 4, data.begin() + 36); - ESP_LOGD(TAG, "Face verified. ID: %d, name: %s", face_id, name.c_str()); + const char *name_ptr = reinterpret_cast(data + 4); + ESP_LOGD(TAG, "Face verified. ID: %d, name: %.*s", face_id, (int) HLK_FM22X_NAME_SIZE, name_ptr); if (this->last_face_id_sensor_ != nullptr) { this->last_face_id_sensor_->publish_state(face_id); } if (this->last_face_name_text_sensor_ != nullptr) { - this->last_face_name_text_sensor_->publish_state(name); + this->last_face_name_text_sensor_->publish_state(name_ptr, HLK_FM22X_NAME_SIZE); } - this->face_scan_matched_callback_.call(face_id, name); + this->face_scan_matched_callback_.call(face_id, std::string(name_ptr, HLK_FM22X_NAME_SIZE)); break; } case HlkFm22xCommand::ENROLL: { @@ -266,9 +281,8 @@ void HlkFm22xComponent::handle_reply_(const std::vector &data) { this->defer([this]() { this->send_command_(HlkFm22xCommand::GET_VERSION); }); break; case HlkFm22xCommand::GET_VERSION: - if (this->version_text_sensor_ != nullptr) { - std::string version(data.begin() + 2, data.end()); - this->version_text_sensor_->publish_state(version); + if (this->version_text_sensor_ != nullptr && length > 2) { + this->version_text_sensor_->publish_state(reinterpret_cast(data + 2), length - 2); } this->defer([this]() { this->get_face_count_(); }); break; diff --git a/esphome/components/hlk_fm22x/hlk_fm22x.h b/esphome/components/hlk_fm22x/hlk_fm22x.h index 9c981d3c445..0ea4636281c 100644 --- a/esphome/components/hlk_fm22x/hlk_fm22x.h +++ b/esphome/components/hlk_fm22x/hlk_fm22x.h @@ -7,12 +7,15 @@ #include "esphome/components/text_sensor/text_sensor.h" #include "esphome/components/uart/uart.h" +#include #include -#include namespace esphome::hlk_fm22x { static const uint16_t START_CODE = 0xEFAA; +static constexpr size_t HLK_FM22X_NAME_SIZE = 32; +// Maximum response payload: command(1) + result(1) + face_id(2) + name(32) = 36 +static constexpr size_t HLK_FM22X_MAX_RESPONSE_SIZE = 36; enum HlkFm22xCommand { NONE = 0x00, RESET = 0x10, @@ -118,10 +121,11 @@ class HlkFm22xComponent : public PollingComponent, public uart::UARTDevice { void get_face_count_(); void send_command_(HlkFm22xCommand command, const uint8_t *data = nullptr, size_t size = 0); void recv_command_(); - void handle_note_(const std::vector &data); - void handle_reply_(const std::vector &data); + void handle_note_(const uint8_t *data, size_t length); + void handle_reply_(const uint8_t *data, size_t length); void set_enrolling_(bool enrolling); + std::array recv_buf_; HlkFm22xCommand active_command_ = HlkFm22xCommand::NONE; uint16_t wait_cycles_ = 0; sensor::Sensor *face_count_sensor_{nullptr}; From 57b85a840017e6aa8662c1a0228bcb50052a597a Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Feb 2026 18:24:20 -0600 Subject: [PATCH 09/15] [dlms_meter] Batch UART reads to reduce per-loop overhead (#13828) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/dlms_meter/dlms_meter.cpp | 27 +++++++++++++++----- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/esphome/components/dlms_meter/dlms_meter.cpp b/esphome/components/dlms_meter/dlms_meter.cpp index 6aa465143e5..11d05b3a088 100644 --- a/esphome/components/dlms_meter/dlms_meter.cpp +++ b/esphome/components/dlms_meter/dlms_meter.cpp @@ -28,15 +28,28 @@ void DlmsMeterComponent::dump_config() { void DlmsMeterComponent::loop() { // Read while data is available, netznoe uses two frames so allow 2x max frame length - while (this->available()) { - if (this->receive_buffer_.size() >= MBUS_MAX_FRAME_LENGTH * 2) { + size_t avail = this->available(); + if (avail > 0) { + size_t remaining = MBUS_MAX_FRAME_LENGTH * 2 - this->receive_buffer_.size(); + if (remaining == 0) { ESP_LOGW(TAG, "Receive buffer full, dropping remaining bytes"); - break; + } else { + // Read all available bytes in batches to reduce UART call overhead. + // Cap reads to remaining buffer capacity. + if (avail > remaining) { + avail = remaining; + } + uint8_t buf[64]; + while (avail > 0) { + size_t to_read = std::min(avail, sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; + } + avail -= to_read; + this->receive_buffer_.insert(this->receive_buffer_.end(), buf, buf + to_read); + this->last_read_ = millis(); + } } - uint8_t c; - this->read_byte(&c); - this->receive_buffer_.push_back(c); - this->last_read_ = millis(); } if (!this->receive_buffer_.empty() && millis() - this->last_read_ > this->read_timeout_) { From 01a90074ba94e9661aa077ba272bc6e2d350ee51 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Feb 2026 18:25:34 -0600 Subject: [PATCH 10/15] [ld2420] Batch UART reads to reduce loop overhead (#13821) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/ld2420/ld2420.cpp | 22 ++++++++++++++++++++-- esphome/components/ld2420/ld2420.h | 2 ++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index 10c623bce06..69b69f4a61e 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -335,9 +335,10 @@ void LD2420Component::revert_config_action() { void LD2420Component::loop() { // If there is a active send command do not process it here, the send command call will handle it. - while (!this->cmd_active_ && this->available()) { - this->readline_(this->read(), this->buffer_data_, MAX_LINE_LENGTH); + if (this->cmd_active_) { + return; } + this->read_batch_(this->buffer_data_); } void LD2420Component::update_radar_data(uint16_t const *gate_energy, uint8_t sample_number) { @@ -539,6 +540,23 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) { } } +void LD2420Component::read_batch_(std::span buffer) { + // Read all available bytes in batches to reduce UART call overhead. + size_t avail = this->available(); + uint8_t buf[MAX_LINE_LENGTH]; + while (avail > 0) { + size_t to_read = std::min(avail, sizeof(buf)); + if (!this->read_array(buf, to_read)) { + break; + } + avail -= to_read; + + for (size_t i = 0; i < to_read; i++) { + this->readline_(buf[i], buffer.data(), buffer.size()); + } + } +} + void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { this->cmd_reply_.command = buffer[CMD_FRAME_COMMAND]; this->cmd_reply_.length = buffer[CMD_FRAME_DATA_LENGTH]; diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index 50ddf45264a..6d81f864970 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -4,6 +4,7 @@ #include "esphome/components/uart/uart.h" #include "esphome/core/automation.h" #include "esphome/core/helpers.h" +#include #ifdef USE_TEXT_SENSOR #include "esphome/components/text_sensor/text_sensor.h" #endif @@ -165,6 +166,7 @@ class LD2420Component : public Component, public uart::UARTDevice { void handle_energy_mode_(uint8_t *buffer, int len); void handle_ack_data_(uint8_t *buffer, int len); void readline_(int rx_data, uint8_t *buffer, int len); + void read_batch_(std::span buffer); void set_calibration_(bool state) { this->calibration_ = state; }; bool get_calibration_() { return this->calibration_; }; From 097901e9c89e284e424bb51eb6cc504a07ba261b Mon Sep 17 00:00:00 2001 From: Sean Kelly Date: Mon, 9 Feb 2026 16:30:37 -0800 Subject: [PATCH 11/15] [aqi] Fix AQI calculation for specific pm2.5 or pm10 readings (#13770) --- esphome/components/aqi/aqi_calculator.h | 38 +++++++++++++++++++----- esphome/components/aqi/caqi_calculator.h | 30 ++++++++++++++++--- 2 files changed, 56 insertions(+), 12 deletions(-) diff --git a/esphome/components/aqi/aqi_calculator.h b/esphome/components/aqi/aqi_calculator.h index 993504c1e94..d624af0432f 100644 --- a/esphome/components/aqi/aqi_calculator.h +++ b/esphome/components/aqi/aqi_calculator.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include "abstract_aqi_calculator.h" @@ -14,7 +15,11 @@ class AQICalculator : public AbstractAQICalculator { float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID); - return static_cast(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index)); + float aqi = std::max(pm2_5_index, pm10_0_index); + if (aqi < 0.0f) { + aqi = 0.0f; + } + return static_cast(std::lround(aqi)); } protected: @@ -22,13 +27,27 @@ class AQICalculator : public AbstractAQICalculator { static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 50}, {51, 100}, {101, 150}, {151, 200}, {201, 300}, {301, 500}}; - static constexpr float PM2_5_GRID[NUM_LEVELS][2] = {{0.0f, 9.0f}, {9.1f, 35.4f}, - {35.5f, 55.4f}, {55.5f, 125.4f}, - {125.5f, 225.4f}, {225.5f, std::numeric_limits::max()}}; + static constexpr float PM2_5_GRID[NUM_LEVELS][2] = { + // clang-format off + {0.0f, 9.1f}, + {9.1f, 35.5f}, + {35.5f, 55.5f}, + {55.5f, 125.5f}, + {125.5f, 225.5f}, + {225.5f, std::numeric_limits::max()} + // clang-format on + }; - static constexpr float PM10_0_GRID[NUM_LEVELS][2] = {{0.0f, 54.0f}, {55.0f, 154.0f}, - {155.0f, 254.0f}, {255.0f, 354.0f}, - {355.0f, 424.0f}, {425.0f, std::numeric_limits::max()}}; + static constexpr float PM10_0_GRID[NUM_LEVELS][2] = { + // clang-format off + {0.0f, 55.0f}, + {55.0f, 155.0f}, + {155.0f, 255.0f}, + {255.0f, 355.0f}, + {355.0f, 425.0f}, + {425.0f, std::numeric_limits::max()} + // clang-format on + }; static float calculate_index(float value, const float array[NUM_LEVELS][2]) { int grid_index = get_grid_index(value, array); @@ -45,7 +64,10 @@ class AQICalculator : public AbstractAQICalculator { static int get_grid_index(float value, const float array[NUM_LEVELS][2]) { for (int i = 0; i < NUM_LEVELS; i++) { - if (value >= array[i][0] && value <= array[i][1]) { + const bool in_range = + (value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive + : (value < array[i][1])); // others exclusive on hi + if (in_range) { return i; } } diff --git a/esphome/components/aqi/caqi_calculator.h b/esphome/components/aqi/caqi_calculator.h index d2ec4bb98f6..fe2efe7059d 100644 --- a/esphome/components/aqi/caqi_calculator.h +++ b/esphome/components/aqi/caqi_calculator.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include "abstract_aqi_calculator.h" @@ -12,7 +13,11 @@ class CAQICalculator : public AbstractAQICalculator { float pm2_5_index = calculate_index(pm2_5_value, PM2_5_GRID); float pm10_0_index = calculate_index(pm10_0_value, PM10_0_GRID); - return static_cast(std::round((pm2_5_index < pm10_0_index) ? pm10_0_index : pm2_5_index)); + float aqi = std::max(pm2_5_index, pm10_0_index); + if (aqi < 0.0f) { + aqi = 0.0f; + } + return static_cast(std::lround(aqi)); } protected: @@ -21,10 +26,24 @@ class CAQICalculator : public AbstractAQICalculator { static constexpr int INDEX_GRID[NUM_LEVELS][2] = {{0, 25}, {26, 50}, {51, 75}, {76, 100}, {101, 400}}; static constexpr float PM2_5_GRID[NUM_LEVELS][2] = { - {0.0f, 15.0f}, {15.1f, 30.0f}, {30.1f, 55.0f}, {55.1f, 110.0f}, {110.1f, std::numeric_limits::max()}}; + // clang-format off + {0.0f, 15.1f}, + {15.1f, 30.1f}, + {30.1f, 55.1f}, + {55.1f, 110.1f}, + {110.1f, std::numeric_limits::max()} + // clang-format on + }; static constexpr float PM10_0_GRID[NUM_LEVELS][2] = { - {0.0f, 25.0f}, {25.1f, 50.0f}, {50.1f, 90.0f}, {90.1f, 180.0f}, {180.1f, std::numeric_limits::max()}}; + // clang-format off + {0.0f, 25.1f}, + {25.1f, 50.1f}, + {50.1f, 90.1f}, + {90.1f, 180.1f}, + {180.1f, std::numeric_limits::max()} + // clang-format on + }; static float calculate_index(float value, const float array[NUM_LEVELS][2]) { int grid_index = get_grid_index(value, array); @@ -42,7 +61,10 @@ class CAQICalculator : public AbstractAQICalculator { static int get_grid_index(float value, const float array[NUM_LEVELS][2]) { for (int i = 0; i < NUM_LEVELS; i++) { - if (value >= array[i][0] && value <= array[i][1]) { + const bool in_range = + (value >= array[i][0]) && ((i == NUM_LEVELS - 1) ? (value <= array[i][1]) // last bucket inclusive + : (value < array[i][1])); // others exclusive on hi + if (in_range) { return i; } } From 87ac263264b02033638abbed3d661ff33ff7bffd Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Mon, 9 Feb 2026 18:32:52 -0600 Subject: [PATCH 12/15] [dsmr] Batch UART reads to reduce per-loop overhead (#13826) --- esphome/components/dsmr/dsmr.cpp | 243 +++++++++++++++++-------------- esphome/components/dsmr/dsmr.h | 1 + 2 files changed, 137 insertions(+), 107 deletions(-) diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp index c78d37bf5ef..20fbd20cd63 100644 --- a/esphome/components/dsmr/dsmr.cpp +++ b/esphome/components/dsmr/dsmr.cpp @@ -40,9 +40,7 @@ bool Dsmr::ready_to_request_data_() { this->start_requesting_data_(); } if (!this->requesting_data_) { - while (this->available()) { - this->read(); - } + this->drain_rx_buffer_(); } } return this->requesting_data_; @@ -115,138 +113,169 @@ void Dsmr::stop_requesting_data_() { } else { ESP_LOGV(TAG, "Stop reading data from P1 port"); } - while (this->available()) { - this->read(); - } + this->drain_rx_buffer_(); this->requesting_data_ = false; } } +void Dsmr::drain_rx_buffer_() { + uint8_t buf[64]; + int avail; + while ((avail = this->available()) > 0) { + if (!this->read_array(buf, std::min(static_cast(avail), sizeof(buf)))) { + break; + } + } +} + void Dsmr::reset_telegram_() { this->header_found_ = false; this->footer_found_ = false; this->bytes_read_ = 0; this->crypt_bytes_read_ = 0; this->crypt_telegram_len_ = 0; - this->last_read_time_ = 0; } void Dsmr::receive_telegram_() { while (this->available_within_timeout_()) { - const char c = this->read(); + // Read all available bytes in batches to reduce UART call overhead. + uint8_t buf[64]; + int avail = this->available(); + while (avail > 0) { + size_t to_read = std::min(static_cast(avail), sizeof(buf)); + if (!this->read_array(buf, to_read)) + return; + avail -= to_read; - // Find a new telegram header, i.e. forward slash. - if (c == '/') { - ESP_LOGV(TAG, "Header of telegram found"); - this->reset_telegram_(); - this->header_found_ = true; - } - if (!this->header_found_) - continue; + for (size_t i = 0; i < to_read; i++) { + const char c = static_cast(buf[i]); - // Check for buffer overflow. - if (this->bytes_read_ >= this->max_telegram_len_) { - this->reset_telegram_(); - ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_); - return; - } + // Find a new telegram header, i.e. forward slash. + if (c == '/') { + ESP_LOGV(TAG, "Header of telegram found"); + this->reset_telegram_(); + this->header_found_ = true; + } + if (!this->header_found_) + continue; - // Some v2.2 or v3 meters will send a new value which starts with '(' - // in a new line, while the value belongs to the previous ObisId. For - // proper parsing, remove these new line characters. - if (c == '(') { - while (true) { - auto previous_char = this->telegram_[this->bytes_read_ - 1]; - if (previous_char == '\n' || previous_char == '\r') { - this->bytes_read_--; - } else { - break; + // Check for buffer overflow. + if (this->bytes_read_ >= this->max_telegram_len_) { + this->reset_telegram_(); + ESP_LOGE(TAG, "Error: telegram larger than buffer (%d bytes)", this->max_telegram_len_); + return; + } + + // Some v2.2 or v3 meters will send a new value which starts with '(' + // in a new line, while the value belongs to the previous ObisId. For + // proper parsing, remove these new line characters. + if (c == '(') { + while (true) { + auto previous_char = this->telegram_[this->bytes_read_ - 1]; + if (previous_char == '\n' || previous_char == '\r') { + this->bytes_read_--; + } else { + break; + } + } + } + + // Store the byte in the buffer. + this->telegram_[this->bytes_read_] = c; + this->bytes_read_++; + + // Check for a footer, i.e. exclamation mark, followed by a hex checksum. + if (c == '!') { + ESP_LOGV(TAG, "Footer of telegram found"); + this->footer_found_ = true; + continue; + } + // Check for the end of the hex checksum, i.e. a newline. + if (this->footer_found_ && c == '\n') { + // Parse the telegram and publish sensor values. + this->parse_telegram(); + this->reset_telegram_(); + return; } } } - - // Store the byte in the buffer. - this->telegram_[this->bytes_read_] = c; - this->bytes_read_++; - - // Check for a footer, i.e. exclamation mark, followed by a hex checksum. - if (c == '!') { - ESP_LOGV(TAG, "Footer of telegram found"); - this->footer_found_ = true; - continue; - } - // Check for the end of the hex checksum, i.e. a newline. - if (this->footer_found_ && c == '\n') { - // Parse the telegram and publish sensor values. - this->parse_telegram(); - this->reset_telegram_(); - return; - } } } void Dsmr::receive_encrypted_telegram_() { while (this->available_within_timeout_()) { - const char c = this->read(); + // Read all available bytes in batches to reduce UART call overhead. + uint8_t buf[64]; + int avail = this->available(); + while (avail > 0) { + size_t to_read = std::min(static_cast(avail), sizeof(buf)); + if (!this->read_array(buf, to_read)) + return; + avail -= to_read; - // Find a new telegram start byte. - if (!this->header_found_) { - if ((uint8_t) c != 0xDB) { - continue; + for (size_t i = 0; i < to_read; i++) { + const char c = static_cast(buf[i]); + + // Find a new telegram start byte. + if (!this->header_found_) { + if ((uint8_t) c != 0xDB) { + continue; + } + ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found"); + this->reset_telegram_(); + this->header_found_ = true; + } + + // Check for buffer overflow. + if (this->crypt_bytes_read_ >= this->max_telegram_len_) { + this->reset_telegram_(); + ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_); + return; + } + + // Store the byte in the buffer. + this->crypt_telegram_[this->crypt_bytes_read_] = c; + this->crypt_bytes_read_++; + + // Read the length of the incoming encrypted telegram. + if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) { + // Complete header + data bytes + this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]); + ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_); + } + + // Check for the end of the encrypted telegram. + if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) { + continue; + } + ESP_LOGV(TAG, "End of encrypted telegram found"); + + // Decrypt the encrypted telegram. + GCM *gcmaes128{new GCM()}; + gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize()); + // the iv is 8 bytes of the system title + 4 bytes frame counter + // system title is at byte 2 and frame counter at byte 15 + for (int i = 10; i < 14; i++) + this->crypt_telegram_[i] = this->crypt_telegram_[i + 4]; + constexpr uint16_t iv_size{12}; + gcmaes128->setIV(&this->crypt_telegram_[2], iv_size); + gcmaes128->decrypt(reinterpret_cast(this->telegram_), + // the ciphertext start at byte 18 + &this->crypt_telegram_[18], + // cipher size + this->crypt_bytes_read_ - 17); + delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory) + + this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_); + ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_); + ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_); + + // Parse the decrypted telegram and publish sensor values. + this->parse_telegram(); + this->reset_telegram_(); + return; } - ESP_LOGV(TAG, "Start byte 0xDB of encrypted telegram found"); - this->reset_telegram_(); - this->header_found_ = true; } - - // Check for buffer overflow. - if (this->crypt_bytes_read_ >= this->max_telegram_len_) { - this->reset_telegram_(); - ESP_LOGE(TAG, "Error: encrypted telegram larger than buffer (%d bytes)", this->max_telegram_len_); - return; - } - - // Store the byte in the buffer. - this->crypt_telegram_[this->crypt_bytes_read_] = c; - this->crypt_bytes_read_++; - - // Read the length of the incoming encrypted telegram. - if (this->crypt_telegram_len_ == 0 && this->crypt_bytes_read_ > 20) { - // Complete header + data bytes - this->crypt_telegram_len_ = 13 + (this->crypt_telegram_[11] << 8 | this->crypt_telegram_[12]); - ESP_LOGV(TAG, "Encrypted telegram length: %d bytes", this->crypt_telegram_len_); - } - - // Check for the end of the encrypted telegram. - if (this->crypt_telegram_len_ == 0 || this->crypt_bytes_read_ != this->crypt_telegram_len_) { - continue; - } - ESP_LOGV(TAG, "End of encrypted telegram found"); - - // Decrypt the encrypted telegram. - GCM *gcmaes128{new GCM()}; - gcmaes128->setKey(this->decryption_key_.data(), gcmaes128->keySize()); - // the iv is 8 bytes of the system title + 4 bytes frame counter - // system title is at byte 2 and frame counter at byte 15 - for (int i = 10; i < 14; i++) - this->crypt_telegram_[i] = this->crypt_telegram_[i + 4]; - constexpr uint16_t iv_size{12}; - gcmaes128->setIV(&this->crypt_telegram_[2], iv_size); - gcmaes128->decrypt(reinterpret_cast(this->telegram_), - // the ciphertext start at byte 18 - &this->crypt_telegram_[18], - // cipher size - this->crypt_bytes_read_ - 17); - delete gcmaes128; // NOLINT(cppcoreguidelines-owning-memory) - - this->bytes_read_ = strnlen(this->telegram_, this->max_telegram_len_); - ESP_LOGV(TAG, "Decrypted telegram size: %d bytes", this->bytes_read_); - ESP_LOGVV(TAG, "Decrypted telegram: %s", this->telegram_); - - // Parse the decrypted telegram and publish sensor values. - this->parse_telegram(); - this->reset_telegram_(); - return; } } diff --git a/esphome/components/dsmr/dsmr.h b/esphome/components/dsmr/dsmr.h index b7e05a22b3e..fafcf62b876 100644 --- a/esphome/components/dsmr/dsmr.h +++ b/esphome/components/dsmr/dsmr.h @@ -85,6 +85,7 @@ class Dsmr : public Component, public uart::UARTDevice { void receive_telegram_(); void receive_encrypted_telegram_(); void reset_telegram_(); + void drain_rx_buffer_(); /// Wait for UART data to become available within the read timeout. /// From dcbb0204794d672fcfc66f4174bb4ca0a1e9dc9f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Mon, 9 Feb 2026 20:02:41 -0500 Subject: [PATCH 13/15] [uart] Fix available() return type to size_t across components (#13898) Co-authored-by: Claude Opus 4.6 --- esphome/components/cse7766/cse7766.cpp | 6 +++--- esphome/components/dfplayer/dfplayer.cpp | 4 ++-- esphome/components/dsmr/dsmr.cpp | 12 ++++++------ esphome/components/ld2410/ld2410.cpp | 4 ++-- esphome/components/ld2412/ld2412.cpp | 4 ++-- esphome/components/ld2450/ld2450.cpp | 4 ++-- esphome/components/modbus/modbus.cpp | 4 ++-- esphome/components/nextion/nextion.cpp | 4 ++-- esphome/components/pipsolar/pipsolar.cpp | 8 ++++---- esphome/components/pylontech/pylontech.cpp | 4 ++-- esphome/components/rd03d/rd03d.cpp | 4 ++-- esphome/components/rf_bridge/rf_bridge.cpp | 4 ++-- esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp | 4 ++-- esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp | 4 ++-- esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp | 4 ++-- esphome/components/tuya/tuya.cpp | 4 ++-- 16 files changed, 39 insertions(+), 39 deletions(-) diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 45abd3ca3df..7ffdf757a0f 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -16,8 +16,8 @@ void CSE7766Component::loop() { } // Early return prevents updating last_transmission_ when no data is available. - int avail = this->available(); - if (avail <= 0) { + size_t avail = this->available(); + if (avail == 0) { return; } @@ -27,7 +27,7 @@ void CSE7766Component::loop() { // At 4800 baud (~480 bytes/sec) with ~122 Hz loop rate, typically ~4 bytes per call. uint8_t buf[CSE7766_RAW_DATA_SIZE]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/dfplayer/dfplayer.cpp b/esphome/components/dfplayer/dfplayer.cpp index 48c06be5581..79f8fd03c3e 100644 --- a/esphome/components/dfplayer/dfplayer.cpp +++ b/esphome/components/dfplayer/dfplayer.cpp @@ -133,10 +133,10 @@ void DFPlayer::send_cmd_(uint8_t cmd, uint16_t argument) { void DFPlayer::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/dsmr/dsmr.cpp b/esphome/components/dsmr/dsmr.cpp index 20fbd20cd63..baf7f593147 100644 --- a/esphome/components/dsmr/dsmr.cpp +++ b/esphome/components/dsmr/dsmr.cpp @@ -120,9 +120,9 @@ void Dsmr::stop_requesting_data_() { void Dsmr::drain_rx_buffer_() { uint8_t buf[64]; - int avail; + size_t avail; while ((avail = this->available()) > 0) { - if (!this->read_array(buf, std::min(static_cast(avail), sizeof(buf)))) { + if (!this->read_array(buf, std::min(avail, sizeof(buf)))) { break; } } @@ -140,9 +140,9 @@ void Dsmr::receive_telegram_() { while (this->available_within_timeout_()) { // Read all available bytes in batches to reduce UART call overhead. uint8_t buf[64]; - int avail = this->available(); + size_t avail = this->available(); while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) return; avail -= to_read; @@ -206,9 +206,9 @@ void Dsmr::receive_encrypted_telegram_() { while (this->available_within_timeout_()) { // Read all available bytes in batches to reduce UART call overhead. uint8_t buf[64]; - int avail = this->available(); + size_t avail = this->available(); while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) return; avail -= to_read; diff --git a/esphome/components/ld2410/ld2410.cpp b/esphome/components/ld2410/ld2410.cpp index b57b1d99788..95a04f768ae 100644 --- a/esphome/components/ld2410/ld2410.cpp +++ b/esphome/components/ld2410/ld2410.cpp @@ -276,10 +276,10 @@ void LD2410Component::restart_and_read_all_info() { void LD2410Component::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[MAX_LINE_LENGTH]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/ld2412/ld2412.cpp b/esphome/components/ld2412/ld2412.cpp index f8ceee78ebb..95e19e0d5fe 100644 --- a/esphome/components/ld2412/ld2412.cpp +++ b/esphome/components/ld2412/ld2412.cpp @@ -311,10 +311,10 @@ void LD2412Component::restart_and_read_all_info() { void LD2412Component::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[MAX_LINE_LENGTH]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index 38ba0d7f96b..b04b509a161 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -277,10 +277,10 @@ void LD2450Component::dump_config() { void LD2450Component::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[MAX_LINE_LENGTH]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/modbus/modbus.cpp b/esphome/components/modbus/modbus.cpp index c1f5635028c..d40343db33d 100644 --- a/esphome/components/modbus/modbus.cpp +++ b/esphome/components/modbus/modbus.cpp @@ -20,10 +20,10 @@ void Modbus::loop() { const uint32_t now = App.get_loop_component_start_time(); // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index 56bbc840fba..9f1ce47837c 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -398,10 +398,10 @@ bool Nextion::remove_from_q_(bool report_empty) { void Nextion::process_serial_() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index d7b37f61300..e6831ad19e0 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -14,9 +14,9 @@ void Pipsolar::setup() { void Pipsolar::empty_uart_buffer_() { uint8_t buf[64]; - int avail; + size_t avail; while ((avail = this->available()) > 0) { - if (!this->read_array(buf, std::min(static_cast(avail), sizeof(buf)))) { + if (!this->read_array(buf, std::min(avail, sizeof(buf)))) { break; } } @@ -97,10 +97,10 @@ void Pipsolar::loop() { } if (this->state_ == STATE_COMMAND || this->state_ == STATE_POLL) { - int avail = this->available(); + size_t avail = this->available(); while (avail > 0) { uint8_t buf[64]; - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/pylontech/pylontech.cpp b/esphome/components/pylontech/pylontech.cpp index d7242532564..7eb89d5b321 100644 --- a/esphome/components/pylontech/pylontech.cpp +++ b/esphome/components/pylontech/pylontech.cpp @@ -56,14 +56,14 @@ void PylontechComponent::setup() { void PylontechComponent::update() { this->write_str("pwr\n"); } void PylontechComponent::loop() { - int avail = this->available(); + size_t avail = this->available(); if (avail > 0) { // pylontech sends a lot of data very suddenly // we need to quickly put it all into our own buffer, otherwise the uart's buffer will overflow int recv = 0; uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/rd03d/rd03d.cpp b/esphome/components/rd03d/rd03d.cpp index e4dbdf41cb6..d47347fcfa6 100644 --- a/esphome/components/rd03d/rd03d.cpp +++ b/esphome/components/rd03d/rd03d.cpp @@ -82,10 +82,10 @@ void RD03DComponent::dump_config() { void RD03DComponent::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/rf_bridge/rf_bridge.cpp b/esphome/components/rf_bridge/rf_bridge.cpp index e33c13aafe8..d8c148145ce 100644 --- a/esphome/components/rf_bridge/rf_bridge.cpp +++ b/esphome/components/rf_bridge/rf_bridge.cpp @@ -136,10 +136,10 @@ void RFBridgeComponent::loop() { this->last_bridge_byte_ = now; } - int avail = this->available(); + size_t avail = this->available(); while (avail > 0) { uint8_t buf[64]; - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp index 3f2103b4013..99d519b434d 100644 --- a/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp +++ b/esphome/components/seeed_mr24hpc1/seeed_mr24hpc1.cpp @@ -107,10 +107,10 @@ void MR24HPC1Component::update_() { // main loop void MR24HPC1Component::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp index d95e13241d3..12f188fe03c 100644 --- a/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp +++ b/esphome/components/seeed_mr60bha2/seeed_mr60bha2.cpp @@ -31,10 +31,10 @@ void MR60BHA2Component::dump_config() { // main loop void MR60BHA2Component::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp index 441ee2b5c27..5d571618d33 100644 --- a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp @@ -50,10 +50,10 @@ void MR60FDA2Component::setup() { // main loop void MR60FDA2Component::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } diff --git a/esphome/components/tuya/tuya.cpp b/esphome/components/tuya/tuya.cpp index 9ee4c09b860..a1acbf2f56c 100644 --- a/esphome/components/tuya/tuya.cpp +++ b/esphome/components/tuya/tuya.cpp @@ -32,10 +32,10 @@ void Tuya::setup() { void Tuya::loop() { // Read all available bytes in batches to reduce UART call overhead. - int avail = this->available(); + size_t avail = this->available(); uint8_t buf[64]; while (avail > 0) { - size_t to_read = std::min(static_cast(avail), sizeof(buf)); + size_t to_read = std::min(avail, sizeof(buf)); if (!this->read_array(buf, to_read)) { break; } From b97a728cf1e8b35c0f9e60ca9c5f7a50fdd5c224 Mon Sep 17 00:00:00 2001 From: Cody Cutrer Date: Mon, 9 Feb 2026 20:40:44 -0700 Subject: [PATCH 14/15] [ld2450] add on_data callback (#13601) Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- esphome/components/ld2450/__init__.py | 13 ++++++++++++- esphome/components/ld2450/ld2450.cpp | 6 ++++++ esphome/components/ld2450/ld2450.h | 12 ++++++++++++ tests/components/ld2450/common.yaml | 3 +++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/esphome/components/ld2450/__init__.py b/esphome/components/ld2450/__init__.py index bd6d697c90d..5854a5794c4 100644 --- a/esphome/components/ld2450/__init__.py +++ b/esphome/components/ld2450/__init__.py @@ -1,7 +1,8 @@ +from esphome import automation import esphome.codegen as cg from esphome.components import uart import esphome.config_validation as cv -from esphome.const import CONF_ID, CONF_THROTTLE +from esphome.const import CONF_ID, CONF_ON_DATA, CONF_THROTTLE, CONF_TRIGGER_ID AUTO_LOAD = ["ld24xx"] DEPENDENCIES = ["uart"] @@ -11,6 +12,8 @@ MULTI_CONF = True ld2450_ns = cg.esphome_ns.namespace("ld2450") LD2450Component = ld2450_ns.class_("LD2450Component", cg.Component, uart.UARTDevice) +LD2450DataTrigger = ld2450_ns.class_("LD2450DataTrigger", automation.Trigger.template()) + CONF_LD2450_ID = "ld2450_id" CONFIG_SCHEMA = cv.All( @@ -20,6 +23,11 @@ CONFIG_SCHEMA = cv.All( cv.Optional(CONF_THROTTLE): cv.invalid( f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead" ), + cv.Optional(CONF_ON_DATA): automation.validate_automation( + { + cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(LD2450DataTrigger), + } + ), } ) .extend(uart.UART_DEVICE_SCHEMA) @@ -45,3 +53,6 @@ async def to_code(config): var = cg.new_Pvariable(config[CONF_ID]) await cg.register_component(var, config) await uart.register_uart_device(var, config) + for conf in config.get(CONF_ON_DATA, []): + trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) + await automation.build_automation(trigger, [], conf) diff --git a/esphome/components/ld2450/ld2450.cpp b/esphome/components/ld2450/ld2450.cpp index b04b509a161..1ea5c18271a 100644 --- a/esphome/components/ld2450/ld2450.cpp +++ b/esphome/components/ld2450/ld2450.cpp @@ -413,6 +413,10 @@ void LD2450Component::restart_and_read_all_info() { this->set_timeout(1500, [this]() { this->read_all_info(); }); } +void LD2450Component::add_on_data_callback(std::function &&callback) { + this->data_callback_.add(std::move(callback)); +} + // Send command with values to LD2450 void LD2450Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) { ESP_LOGV(TAG, "Sending COMMAND %02X", command); @@ -613,6 +617,8 @@ void LD2450Component::handle_periodic_data_() { this->still_presence_millis_ = App.get_loop_component_start_time(); } #endif + + this->data_callback_.call(); } bool LD2450Component::handle_ack_data_() { diff --git a/esphome/components/ld2450/ld2450.h b/esphome/components/ld2450/ld2450.h index b94c3cac37c..fe69cd81d05 100644 --- a/esphome/components/ld2450/ld2450.h +++ b/esphome/components/ld2450/ld2450.h @@ -141,6 +141,9 @@ class LD2450Component : public Component, public uart::UARTDevice { int32_t zone2_x1, int32_t zone2_y1, int32_t zone2_x2, int32_t zone2_y2, int32_t zone3_x1, int32_t zone3_y1, int32_t zone3_x2, int32_t zone3_y2); + /// Add a callback that will be called after each successfully processed periodic data frame. + void add_on_data_callback(std::function &&callback); + protected: void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len); void set_config_mode_(bool enable); @@ -190,6 +193,15 @@ class LD2450Component : public Component, public uart::UARTDevice { #ifdef USE_TEXT_SENSOR std::array direction_text_sensors_{}; #endif + + LazyCallbackManager data_callback_; +}; + +class LD2450DataTrigger : public Trigger<> { + public: + explicit LD2450DataTrigger(LD2450Component *parent) { + parent->add_on_data_callback([this]() { this->trigger(); }); + } }; } // namespace esphome::ld2450 diff --git a/tests/components/ld2450/common.yaml b/tests/components/ld2450/common.yaml index cfa3c922fc9..617228ca34a 100644 --- a/tests/components/ld2450/common.yaml +++ b/tests/components/ld2450/common.yaml @@ -1,5 +1,8 @@ ld2450: - id: ld2450_radar + on_data: + then: + - logger.log: "LD2450 Radar Data Received" button: - platform: ld2450 From 5caed68cd9a9a36e9d3fb805bb2e464dcfe18e73 Mon Sep 17 00:00:00 2001 From: tronikos Date: Tue, 10 Feb 2026 03:36:56 -0800 Subject: [PATCH 15/15] [api] Deprecate WATER_HEATER_COMMAND_HAS_STATE (#13892) Co-authored-by: J. Nick Koston --- esphome/components/api/api.proto | 4 +++- esphome/components/api/api_connection.cpp | 6 +++++- esphome/components/api/api_pb2.h | 2 ++ esphome/components/api/api_pb2_dump.cpp | 4 ++++ 4 files changed, 14 insertions(+), 2 deletions(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index d25934c60b4..18dac6a2d17 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -1155,9 +1155,11 @@ enum WaterHeaterCommandHasField { WATER_HEATER_COMMAND_HAS_NONE = 0; WATER_HEATER_COMMAND_HAS_MODE = 1; WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE = 2; - WATER_HEATER_COMMAND_HAS_STATE = 4; + WATER_HEATER_COMMAND_HAS_STATE = 4 [deprecated=true]; WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8; WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16; + WATER_HEATER_COMMAND_HAS_ON_STATE = 32; + WATER_HEATER_COMMAND_HAS_AWAY_STATE = 64; } message WaterHeaterCommandRequest { diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index ddc24a7e2c0..c00f413e672 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1343,8 +1343,12 @@ void APIConnection::on_water_heater_command_request(const WaterHeaterCommandRequ call.set_target_temperature_low(msg.target_temperature_low); if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH) call.set_target_temperature_high(msg.target_temperature_high); - if (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE) { + if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE) || + (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) { call.set_away((msg.state & water_heater::WATER_HEATER_STATE_AWAY) != 0); + } + if ((msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_ON_STATE) || + (msg.has_fields & enums::WATER_HEATER_COMMAND_HAS_STATE)) { call.set_on((msg.state & water_heater::WATER_HEATER_STATE_ON) != 0); } call.perform(); diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 15819da1721..d001f869c50 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -147,6 +147,8 @@ enum WaterHeaterCommandHasField : uint32_t { WATER_HEATER_COMMAND_HAS_STATE = 4, WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW = 8, WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH = 16, + WATER_HEATER_COMMAND_HAS_ON_STATE = 32, + WATER_HEATER_COMMAND_HAS_AWAY_STATE = 64, }; #ifdef USE_NUMBER enum NumberMode : uint32_t { diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index f1e3bdcafe1..73690610ede 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -385,6 +385,10 @@ const char *proto_enum_to_string(enums::Water return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_LOW"; case enums::WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH: return "WATER_HEATER_COMMAND_HAS_TARGET_TEMPERATURE_HIGH"; + case enums::WATER_HEATER_COMMAND_HAS_ON_STATE: + return "WATER_HEATER_COMMAND_HAS_ON_STATE"; + case enums::WATER_HEATER_COMMAND_HAS_AWAY_STATE: + return "WATER_HEATER_COMMAND_HAS_AWAY_STATE"; default: return "UNKNOWN"; }