From a9cceebb33612054866465254456336fd4d76ba5 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:48:50 -0500 Subject: [PATCH 01/26] [pid][nextion][pn532_i2c][pipsolar] Fix copy-paste and logic bugs (#14551) Co-authored-by: Claude Opus 4.6 --- esphome/components/nextion/nextion_component.cpp | 2 +- esphome/components/pid/pid_autotuner.cpp | 4 ++-- esphome/components/pipsolar/pipsolar.cpp | 1 + esphome/components/pn532_i2c/pn532_i2c.cpp | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/esphome/components/nextion/nextion_component.cpp b/esphome/components/nextion/nextion_component.cpp index 324ad87372..30c8b80524 100644 --- a/esphome/components/nextion/nextion_component.cpp +++ b/esphome/components/nextion/nextion_component.cpp @@ -88,7 +88,7 @@ void NextionComponent::update_component_settings(bool force_update) { this->send_state_to_nextion(); } - if (this->component_flags_.bco_needs_update || (force_update && this->component_flags_.bco2_is_set)) { + if (this->component_flags_.bco_needs_update || (force_update && this->component_flags_.bco_is_set)) { this->nextion_->set_component_background_color(this->variable_name_.c_str(), this->bco_); this->component_flags_.bco_needs_update = false; } diff --git a/esphome/components/pid/pid_autotuner.cpp b/esphome/components/pid/pid_autotuner.cpp index d1d9c200cf..e1ddd1d7c6 100644 --- a/esphome/components/pid/pid_autotuner.cpp +++ b/esphome/components/pid/pid_autotuner.cpp @@ -97,7 +97,7 @@ PIDAutotuner::PIDAutotuneResult PIDAutotuner::update(float setpoint, float proce } bool zc_symmetrical = this->frequency_detector_.is_increase_decrease_symmetrical(); - bool amplitude_convergent = this->frequency_detector_.is_increase_decrease_symmetrical(); + bool amplitude_convergent = this->amplitude_detector_.is_amplitude_convergent(); if (!zc_symmetrical || !amplitude_convergent) { // The frequency/amplitude is not fully accurate yet, try to wait // until the fault clears, or terminate after a while anyway @@ -362,7 +362,7 @@ bool PIDAutotuner::OscillationAmplitudeDetector::is_amplitude_convergent() const for (auto v : this->phase_mins) global_min = std::min(global_min, v); for (auto v : this->phase_maxs) - global_max = std::min(global_max, v); + global_max = std::max(global_max, v); float global_amplitude = (global_max - global_min) / 2.0f; float mean_amplitude = this->get_mean_oscillation_amplitude(); return (mean_amplitude - global_amplitude) / (global_amplitude) < 0.05f; diff --git a/esphome/components/pipsolar/pipsolar.cpp b/esphome/components/pipsolar/pipsolar.cpp index 9c5caec775..eb6d3931e0 100644 --- a/esphome/components/pipsolar/pipsolar.cpp +++ b/esphome/components/pipsolar/pipsolar.cpp @@ -647,6 +647,7 @@ void Pipsolar::handle_qpiws_(const char *message) { case 34: this->publish_binary_sensor_(enabled, this->warning_high_ac_input_during_bus_soft_start_); value_warnings_present |= enabled.value_or(false); + break; case 35: this->publish_binary_sensor_(enabled, this->warning_battery_equalization_); value_warnings_present |= enabled.value_or(false); diff --git a/esphome/components/pn532_i2c/pn532_i2c.cpp b/esphome/components/pn532_i2c/pn532_i2c.cpp index b306222a21..41f0f079aa 100644 --- a/esphome/components/pn532_i2c/pn532_i2c.cpp +++ b/esphome/components/pn532_i2c/pn532_i2c.cpp @@ -49,7 +49,7 @@ bool PN532I2C::read_response(uint8_t command, std::vector &data) { return false; } - if (data[1] != 0x00 && data[2] != 0x00 && data[3] != 0xFF) { + if (data[1] != 0x00 || data[2] != 0x00 || data[3] != 0xFF) { // invalid packet ESP_LOGV(TAG, "read data invalid preamble!"); return false; @@ -95,7 +95,7 @@ uint8_t PN532I2C::read_response_length_() { return 0; } - if (data[1] != 0x00 && data[2] != 0x00 && data[3] != 0xFF) { + if (data[1] != 0x00 || data[2] != 0x00 || data[3] != 0xFF) { // invalid packet ESP_LOGV(TAG, "read data invalid preamble!"); return 0; From 8f3db96291c06708a23f5171515cd26ec84e33a1 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:50:26 -0500 Subject: [PATCH 02/26] [esp32_ble_server][weikai][ade7880] Fix copy-paste bugs (#14552) Co-authored-by: Claude Opus 4.6 --- esphome/components/ade7880/ade7880.cpp | 6 +++--- esphome/components/ade7880/ade7880_registers.h | 3 +++ esphome/components/esp32_ble_server/ble_characteristic.cpp | 4 ++-- esphome/components/weikai/weikai.cpp | 4 +++- 4 files changed, 11 insertions(+), 6 deletions(-) diff --git a/esphome/components/ade7880/ade7880.cpp b/esphome/components/ade7880/ade7880.cpp index f6a15190cd..8fb3e55b91 100644 --- a/esphome/components/ade7880/ade7880.cpp +++ b/esphome/components/ade7880/ade7880.cpp @@ -121,7 +121,7 @@ void ADE7880::update() { this->update_sensor_from_s32_register16_(chan->forward_active_energy, AFWATTHR, [&chan](float val) { return chan->forward_active_energy_total += val / 14400.0f; }); - this->update_sensor_from_s32_register16_(chan->reverse_active_energy, AFWATTHR, [&chan](float val) { + this->update_sensor_from_s32_register16_(chan->reverse_active_energy, ARWATTHR, [&chan](float val) { return chan->reverse_active_energy_total += val / 14400.0f; }); } @@ -137,7 +137,7 @@ void ADE7880::update() { this->update_sensor_from_s32_register16_(chan->forward_active_energy, BFWATTHR, [&chan](float val) { return chan->forward_active_energy_total += val / 14400.0f; }); - this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BFWATTHR, [&chan](float val) { + this->update_sensor_from_s32_register16_(chan->reverse_active_energy, BRWATTHR, [&chan](float val) { return chan->reverse_active_energy_total += val / 14400.0f; }); } @@ -153,7 +153,7 @@ void ADE7880::update() { this->update_sensor_from_s32_register16_(chan->forward_active_energy, CFWATTHR, [&chan](float val) { return chan->forward_active_energy_total += val / 14400.0f; }); - this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CFWATTHR, [&chan](float val) { + this->update_sensor_from_s32_register16_(chan->reverse_active_energy, CRWATTHR, [&chan](float val) { return chan->reverse_active_energy_total += val / 14400.0f; }); } diff --git a/esphome/components/ade7880/ade7880_registers.h b/esphome/components/ade7880/ade7880_registers.h index 8b5b68abb0..9fd8ca3bf5 100644 --- a/esphome/components/ade7880/ade7880_registers.h +++ b/esphome/components/ade7880/ade7880_registers.h @@ -85,6 +85,9 @@ constexpr uint16_t CWATTHR = 0xE402; constexpr uint16_t AFWATTHR = 0xE403; constexpr uint16_t BFWATTHR = 0xE404; constexpr uint16_t CFWATTHR = 0xE405; +constexpr uint16_t ARWATTHR = 0xE406; +constexpr uint16_t BRWATTHR = 0xE407; +constexpr uint16_t CRWATTHR = 0xE408; constexpr uint16_t AFVARHR = 0xE409; constexpr uint16_t BFVARHR = 0xE40A; constexpr uint16_t CFVARHR = 0xE40B; diff --git a/esphome/components/esp32_ble_server/ble_characteristic.cpp b/esphome/components/esp32_ble_server/ble_characteristic.cpp index d4ccefd9b2..1806354712 100644 --- a/esphome/components/esp32_ble_server/ble_characteristic.cpp +++ b/esphome/components/esp32_ble_server/ble_characteristic.cpp @@ -310,8 +310,8 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt (*this->on_write_callback_)(this->value_, param->exec_write.conn_id); } } - esp_err_t err = - esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr); + esp_err_t err = esp_ble_gatts_send_response(gatts_if, param->exec_write.conn_id, param->exec_write.trans_id, + ESP_GATT_OK, nullptr); if (err != ESP_OK) { ESP_LOGE(TAG, "esp_ble_gatts_send_response failed: %d", err); } diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp index 1d835daf1e..3f5d6c787c 100644 --- a/esphome/components/weikai/weikai.cpp +++ b/esphome/components/weikai/weikai.cpp @@ -445,6 +445,7 @@ void WeikaiChannel::flush() { } size_t WeikaiChannel::xfer_fifo_to_buffer_() { + size_t total = 0; size_t to_transfer; size_t free; while ((to_transfer = this->rx_in_fifo_()) && (free = this->receive_buffer_.free())) { @@ -458,9 +459,10 @@ size_t WeikaiChannel::xfer_fifo_to_buffer_() { this->reg(0).read_fifo(data, to_transfer); for (size_t i = 0; i < to_transfer; i++) this->receive_buffer_.push(data[i]); + total += to_transfer; } } // while work to do - return to_transfer; + return total; } /// From 0469612d0774ef26acef975446e4c187dde876f8 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:02:17 -0500 Subject: [PATCH 03/26] [multiple] Fix assorted medium-severity bugs (#14555) Co-authored-by: Claude Opus 4.6 --- esphome/components/bytebuffer/bytebuffer.h | 2 +- esphome/components/cap1188/cap1188.cpp | 2 +- esphome/components/hte501/hte501.cpp | 2 +- .../components/ina2xx_base/ina2xx_base.cpp | 6 +---- esphome/components/inkplate/inkplate.cpp | 10 ++++----- esphome/components/msa3xx/msa3xx.cpp | 6 +---- esphome/components/nfc/ndef_record_text.cpp | 5 +++++ esphome/components/nfc/nfc.cpp | 22 +++++++++++++------ esphome/components/nfc/nfc.h | 2 +- .../components/template/text/template_text.h | 6 ++++- 10 files changed, 36 insertions(+), 27 deletions(-) diff --git a/esphome/components/bytebuffer/bytebuffer.h b/esphome/components/bytebuffer/bytebuffer.h index 030484ce32..3c68094dbc 100644 --- a/esphome/components/bytebuffer/bytebuffer.h +++ b/esphome/components/bytebuffer/bytebuffer.h @@ -263,7 +263,7 @@ class ByteBuffer { void put_uint8(uint8_t value, size_t offset) { this->data_[offset] = value; } void put_uint16(uint16_t value, size_t offset) { this->put(value, offset); } - void put_uint24(uint32_t value, size_t offset) { this->put(value, offset); } + void put_uint24(uint32_t value, size_t offset) { this->put_uint32_(value, offset, 3); } void put_uint32(uint32_t value, size_t offset) { this->put(value, offset); } void put_uint64(uint64_t value, size_t offset) { this->put(value, offset); } // Signed versions of the put functions diff --git a/esphome/components/cap1188/cap1188.cpp b/esphome/components/cap1188/cap1188.cpp index 9e8c87d147..64bdc620cd 100644 --- a/esphome/components/cap1188/cap1188.cpp +++ b/esphome/components/cap1188/cap1188.cpp @@ -92,7 +92,7 @@ void CAP1188Component::loop() { this->read_register(CAP1188_MAIN, &data, 1); data = data & ~CAP1188_MAIN_INT; - this->write_register(CAP1188_MAIN, &data, 2); + this->write_register(CAP1188_MAIN, &data, 1); } for (auto *channel : this->channels_) { diff --git a/esphome/components/hte501/hte501.cpp b/esphome/components/hte501/hte501.cpp index 972e72c170..ef9ef1fabf 100644 --- a/esphome/components/hte501/hte501.cpp +++ b/esphome/components/hte501/hte501.cpp @@ -49,7 +49,7 @@ void HTE501Component::update() { this->set_timeout(50, [this]() { uint8_t i2c_response[6]; this->read(i2c_response, 6); - if (i2c_response[2] != crc8(i2c_response, 2, 0xFF, 0x31, true) && + if (i2c_response[2] != crc8(i2c_response, 2, 0xFF, 0x31, true) || i2c_response[5] != crc8(i2c_response + 3, 2, 0xFF, 0x31, true)) { this->error_code_ = CRC_CHECK_FAILED; this->status_set_warning(); diff --git a/esphome/components/ina2xx_base/ina2xx_base.cpp b/esphome/components/ina2xx_base/ina2xx_base.cpp index 8a20192c1e..9f510eef74 100644 --- a/esphome/components/ina2xx_base/ina2xx_base.cpp +++ b/esphome/components/ina2xx_base/ina2xx_base.cpp @@ -599,11 +599,7 @@ bool INA2XX::read_unsigned_16_(uint8_t reg, uint16_t &out) { } int64_t INA2XX::two_complement_(uint64_t value, uint8_t bits) { - if (value > (1ULL << (bits - 1))) { - return (int64_t) (value - (1ULL << bits)); - } else { - return (int64_t) value; - } + return (int64_t) (value << (64 - bits)) >> (64 - bits); } } // namespace ina2xx_base } // namespace esphome diff --git a/esphome/components/inkplate/inkplate.cpp b/esphome/components/inkplate/inkplate.cpp index df9c2b29c7..7551c6fc77 100644 --- a/esphome/components/inkplate/inkplate.cpp +++ b/esphome/components/inkplate/inkplate.cpp @@ -407,7 +407,7 @@ void Inkplate::display1b_() { break; } - uint32_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t clock = (1UL << this->cl_pin_->get_pin()); uint32_t data_mask = this->get_data_pin_mask_(); ESP_LOGV(TAG, "Display1b start loops (%ums)", millis() - start_time); @@ -575,7 +575,7 @@ void Inkplate::display3b_() { break; } - uint32_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t clock = (1UL << this->cl_pin_->get_pin()); uint32_t data_mask = this->get_data_pin_mask_(); uint32_t pos; uint32_t data; @@ -646,7 +646,7 @@ bool Inkplate::partial_update_() { int rep = (this->model_ == INKPLATE_6_V2) ? 6 : 5; eink_on_(); - uint32_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t clock = (1UL << this->cl_pin_->get_pin()); uint32_t data_mask = this->get_data_pin_mask_(); for (int k = 0; k < rep; k++) { vscan_start_(); @@ -704,7 +704,7 @@ void Inkplate::vscan_start_() { } void Inkplate::hscan_start_(uint32_t d) { - uint8_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t clock = (1UL << this->cl_pin_->get_pin()); this->sph_pin_->digital_write(false); GPIO.out_w1ts = d | clock; GPIO.out_w1tc = this->get_data_pin_mask_() | clock; @@ -751,7 +751,7 @@ void Inkplate::clean_fast_(uint8_t c, uint8_t rep) { uint32_t send = ((data & 0b00000011) << 4) | (((data & 0b00001100) >> 2) << 18) | (((data & 0b00010000) >> 4) << 23) | (((data & 0b11100000) >> 5) << 25); - uint32_t clock = (1 << this->cl_pin_->get_pin()); + uint32_t clock = (1UL << this->cl_pin_->get_pin()); for (int k = 0; k < rep; k++) { vscan_start_(); diff --git a/esphome/components/msa3xx/msa3xx.cpp b/esphome/components/msa3xx/msa3xx.cpp index e46bfed193..6d6b21e6af 100644 --- a/esphome/components/msa3xx/msa3xx.cpp +++ b/esphome/components/msa3xx/msa3xx.cpp @@ -364,11 +364,7 @@ void MSA3xxComponent::setup_offset_(float offset_x, float offset_y, float offset } int64_t MSA3xxComponent::twos_complement_(uint64_t value, uint8_t bits) { - if (value > (1ULL << (bits - 1))) { - return (int64_t) (value - (1ULL << bits)); - } else { - return (int64_t) value; - } + return (int64_t) (value << (64 - bits)) >> (64 - bits); } void binary_event_debounce(bool state, bool old_state, uint32_t now, uint32_t &last_ms, Trigger<> &trigger, diff --git a/esphome/components/nfc/ndef_record_text.cpp b/esphome/components/nfc/ndef_record_text.cpp index 80b0108b46..8a9a2cb014 100644 --- a/esphome/components/nfc/ndef_record_text.cpp +++ b/esphome/components/nfc/ndef_record_text.cpp @@ -14,6 +14,11 @@ NdefRecordText::NdefRecordText(const std::vector &payload) { uint8_t language_code_length = payload[0] & 0b00111111; // Todo, make use of encoding bit? + if (1 + language_code_length > payload.size()) { + ESP_LOGE(TAG, "Record payload too short for language code"); + return; + } + this->language_code_ = std::string(payload.begin() + 1, payload.begin() + 1 + language_code_length); this->text_ = std::string(payload.begin() + 1 + language_code_length, payload.end()); diff --git a/esphome/components/nfc/nfc.cpp b/esphome/components/nfc/nfc.cpp index 8567b0969a..55543cd292 100644 --- a/esphome/components/nfc/nfc.cpp +++ b/esphome/components/nfc/nfc.cpp @@ -35,7 +35,7 @@ uint8_t guess_tag_type(uint8_t uid_length) { } } -uint8_t get_mifare_classic_ndef_start_index(std::vector &data) { +int8_t get_mifare_classic_ndef_start_index(std::vector &data) { for (uint8_t i = 0; i < MIFARE_CLASSIC_BLOCK_SIZE; i++) { if (data[i] == 0x00) { // Do nothing, skip @@ -49,17 +49,25 @@ uint8_t get_mifare_classic_ndef_start_index(std::vector &data) { } bool decode_mifare_classic_tlv(std::vector &data, uint32_t &message_length, uint8_t &message_start_index) { + if (data.size() < MIFARE_CLASSIC_BLOCK_SIZE) { + ESP_LOGE(TAG, "Error, data too short for NDEF detection."); + return false; + } auto i = get_mifare_classic_ndef_start_index(data); - if (data[i] != 0x03) { + if (i < 0 || data[i] != 0x03) { ESP_LOGE(TAG, "Error, Can't decode message length."); return false; } - if (data[i + 1] == 0xFF) { - message_length = ((0xFF & data[i + 2]) << 8) | (0xFF & data[i + 3]); - message_start_index = i + MIFARE_CLASSIC_LONG_TLV_SIZE; + uint8_t idx = static_cast(i); + if (idx + 4 <= data.size() && data[idx + 1] == 0xFF) { + message_length = ((0xFF & data[idx + 2]) << 8) | (0xFF & data[idx + 3]); + message_start_index = idx + MIFARE_CLASSIC_LONG_TLV_SIZE; + } else if (idx + 2 <= data.size()) { + message_length = data[idx + 1]; + message_start_index = idx + MIFARE_CLASSIC_SHORT_TLV_SIZE; } else { - message_length = data[i + 1]; - message_start_index = i + MIFARE_CLASSIC_SHORT_TLV_SIZE; + ESP_LOGE(TAG, "Error, TLV data too short."); + return false; } return true; } diff --git a/esphome/components/nfc/nfc.h b/esphome/components/nfc/nfc.h index cdaea82af6..8ca5cb7ea4 100644 --- a/esphome/components/nfc/nfc.h +++ b/esphome/components/nfc/nfc.h @@ -72,7 +72,7 @@ ESPDEPRECATED("Use format_bytes_to() with stack buffer instead. Removed in 2026. std::string format_bytes(std::span bytes); uint8_t guess_tag_type(uint8_t uid_length); -uint8_t get_mifare_classic_ndef_start_index(std::vector &data); +int8_t get_mifare_classic_ndef_start_index(std::vector &data); bool decode_mifare_classic_tlv(std::vector &data, uint32_t &message_length, uint8_t &message_start_index); uint32_t get_mifare_classic_buffer_size(uint32_t message_length); diff --git a/esphome/components/template/text/template_text.h b/esphome/components/template/text/template_text.h index 88c6afdf2c..7f176db09e 100644 --- a/esphome/components/template/text/template_text.h +++ b/esphome/components/template/text/template_text.h @@ -52,7 +52,11 @@ template class TextSaver : public TemplateTextSaverBase { bool hasdata = this->pref_.load(&temp); if (hasdata) { - value.assign(temp + 1, (size_t) temp[0]); + size_t len = static_cast(temp[0]); + if (len > SZ) { + len = SZ; + } + value.assign(temp + 1, len); } this->prev_.assign(value); From 4f4b2bfdecc1ccc06f344d4e0c6dbad88537a3a6 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:14:35 -0500 Subject: [PATCH 04/26] [bmp581_base][bl0906] Fix 24-bit sign extension bugs (#14558) Co-authored-by: Claude Opus 4.6 --- esphome/components/bl0906/bl0906.cpp | 4 +++- esphome/components/bmp581_base/bmp581_base.cpp | 4 ++-- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/esphome/components/bl0906/bl0906.cpp b/esphome/components/bl0906/bl0906.cpp index c1cd48a1ac..7b643bba98 100644 --- a/esphome/components/bl0906/bl0906.cpp +++ b/esphome/components/bl0906/bl0906.cpp @@ -10,7 +10,9 @@ static const char *const TAG = "bl0906"; constexpr uint32_t to_uint32_t(ube24_t input) { return input.h << 16 | input.m << 8 | input.l; } -constexpr int32_t to_int32_t(sbe24_t input) { return input.h << 16 | input.m << 8 | input.l; } +constexpr int32_t to_int32_t(sbe24_t input) { + return static_cast(encode_uint32((uint8_t) input.h, input.m, input.l, 0)) >> 8; +} // The SUM byte is (Addr+Data_L+Data_M+Data_H)&0xFF negated; constexpr uint8_t bl0906_checksum(const uint8_t address, const DataPacket *data) { diff --git a/esphome/components/bmp581_base/bmp581_base.cpp b/esphome/components/bmp581_base/bmp581_base.cpp index c4a96ebc39..89a92de31d 100644 --- a/esphome/components/bmp581_base/bmp581_base.cpp +++ b/esphome/components/bmp581_base/bmp581_base.cpp @@ -429,7 +429,7 @@ bool BMP581Component::read_temperature_(float &temperature) { } // temperature MSB is in data[2], LSB is in data[1], XLSB in data[0] - int32_t raw_temp = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; + int32_t raw_temp = static_cast(encode_uint32(data[2], data[1], data[0], 0)) >> 8; temperature = (float) (raw_temp / 65536.0); // convert measurement to degrees Celsius (page 22 of datasheet) return true; @@ -458,7 +458,7 @@ bool BMP581Component::read_temperature_and_pressure_(float &temperature, float & } // temperature MSB is in data[2], LSB is in data[1], XLSB in data[0] - int32_t raw_temp = (int32_t) data[2] << 16 | (int32_t) data[1] << 8 | (int32_t) data[0]; + int32_t raw_temp = static_cast(encode_uint32(data[2], data[1], data[0], 0)) >> 8; temperature = (float) (raw_temp / 65536.0); // convert measurement to degrees Celsius (page 22 of datasheet) // pressure MSB is in data[5], LSB is in data[4], XLSB in data[3] From 2c83c6a79f356d3f0f6910fc573e47c53e695c5e Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 15:47:56 -0500 Subject: [PATCH 05/26] [shelly_dimmer][lvgl][seeed_mr60fda2][packet_transport] Fix buffer bounds checks (#14534) Co-authored-by: Claude Opus 4.6 --- esphome/components/lvgl/lvgl_esphome.cpp | 3 ++ .../packet_transport/packet_transport.cpp | 2 + .../seeed_mr60fda2/seeed_mr60fda2.cpp | 44 ++++++++----------- .../shelly_dimmer/shelly_dimmer.cpp | 4 +- 4 files changed, 25 insertions(+), 28 deletions(-) diff --git a/esphome/components/lvgl/lvgl_esphome.cpp b/esphome/components/lvgl/lvgl_esphome.cpp index 3e447e9169..66cb25b864 100644 --- a/esphome/components/lvgl/lvgl_esphome.cpp +++ b/esphome/components/lvgl/lvgl_esphome.cpp @@ -422,6 +422,9 @@ void LvglComponent::write_random_() { auto row = random_uint32() % this->disp_drv_.ver_res; row = row / this->draw_rounding * this->draw_rounding; auto size = ((random_uint32() % 32) / this->draw_rounding + 2) * this->draw_rounding - 1; + // clamp size so the square fits within the draw buffer + if ((size + 1) * (size + 1) > this->draw_buf_.size) + size = static_cast(sqrtf(this->draw_buf_.size)) - 1; lv_area_t area; area.x1 = col; area.y1 = row; diff --git a/esphome/components/packet_transport/packet_transport.cpp b/esphome/components/packet_transport/packet_transport.cpp index 6f1286b469..964037a02c 100644 --- a/esphome/components/packet_transport/packet_transport.cpp +++ b/esphome/components/packet_transport/packet_transport.cpp @@ -137,6 +137,8 @@ class PacketDecoder { return DECODE_EMPTY; if (this->buffer_[this->position_] != key) return DECODE_UNMATCHED; + if (this->position_ + 1 + sizeof(T) > this->len_) + return DECODE_ERROR; this->position_++; T value = 0; for (size_t i = 0; i != sizeof(T); ++i) { diff --git a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp index 5d571618d3..c6527a948e 100644 --- a/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp +++ b/esphome/components/seeed_mr60fda2/seeed_mr60fda2.cpp @@ -149,28 +149,25 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { switch (this->current_frame_locate_) { case LOCATE_FRAME_HEADER: // starting buffer if (buffer == FRAME_HEADER_BUFFER) { - this->current_frame_len_ = 1; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_len_ = 0; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; } break; case LOCATE_ID_FRAME1: this->current_frame_id_ = buffer << 8; - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; break; case LOCATE_ID_FRAME2: this->current_frame_id_ += buffer; - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; break; case LOCATE_LENGTH_FRAME_H: this->current_data_frame_len_ = buffer << 8; - if (this->current_data_frame_len_ == 0x00) { - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + if (this->current_data_frame_len_ == 0) { + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; } else { this->current_frame_locate_ = LOCATE_FRAME_HEADER; @@ -181,15 +178,13 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { if (this->current_data_frame_len_ > DATA_BUF_MAX_SIZE) { this->current_frame_locate_ = LOCATE_FRAME_HEADER; } else { - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; } break; case LOCATE_TYPE_FRAME1: this->current_frame_type_ = buffer << 8; - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; break; case LOCATE_TYPE_FRAME2: @@ -198,8 +193,7 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { (this->current_frame_type_ == PEOPLE_EXIST_TYPE_BUFFER) || (this->current_frame_type_ == RESULT_INSTALL_HEIGHT) || (this->current_frame_type_ == RESULT_PARAMETERS) || (this->current_frame_type_ == RESULT_HEIGHT_THRESHOLD) || (this->current_frame_type_ == RESULT_SENSITIVITY)) { - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; } else { this->current_frame_locate_ = LOCATE_FRAME_HEADER; @@ -207,8 +201,7 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { break; case LOCATE_HEAD_CKSUM_FRAME: if (validate_checksum(this->current_frame_buf_, this->current_frame_len_, buffer)) { - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; } else { ESP_LOGD(TAG, "HEAD_CKSUM_FRAME ERROR: 0x%02x", buffer); @@ -223,21 +216,20 @@ void MR60FDA2Component::split_frame_(uint8_t buffer) { } break; case LOCATE_DATA_FRAME: - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; - this->current_data_buf_[this->current_frame_len_ - LEN_TO_DATA_FRAME] = buffer; - if (this->current_frame_len_ - LEN_TO_HEAD_CKSUM == this->current_data_frame_len_) { - this->current_frame_locate_++; - } - if (this->current_frame_len_ > FRAME_BUF_MAX_SIZE) { + if (this->current_frame_len_ >= FRAME_BUF_MAX_SIZE) { ESP_LOGD(TAG, "PRACTICE_DATA_FRAME_LEN ERROR: %d", this->current_frame_len_ - LEN_TO_HEAD_CKSUM); this->current_frame_locate_ = LOCATE_FRAME_HEADER; + break; + } + this->current_data_buf_[this->current_frame_len_ - LEN_TO_DATA_FRAME + 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; + if (this->current_frame_len_ - LEN_TO_HEAD_CKSUM == this->current_data_frame_len_) { + this->current_frame_locate_++; } break; case LOCATE_DATA_CKSUM_FRAME: if (validate_checksum(this->current_data_buf_, this->current_data_frame_len_, buffer)) { - this->current_frame_len_++; - this->current_frame_buf_[this->current_frame_len_ - 1] = buffer; + this->current_frame_buf_[this->current_frame_len_++] = buffer; this->current_frame_locate_++; this->process_frame_(); } else { diff --git a/esphome/components/shelly_dimmer/shelly_dimmer.cpp b/esphome/components/shelly_dimmer/shelly_dimmer.cpp index bdb33d31af..88fcbcbfe1 100644 --- a/esphome/components/shelly_dimmer/shelly_dimmer.cpp +++ b/esphome/components/shelly_dimmer/shelly_dimmer.cpp @@ -188,8 +188,8 @@ bool ShellyDimmer::upgrade_firmware_() { break; } - std::memcpy(buffer, p, BUFFER_SIZE); - p += BUFFER_SIZE; + std::memcpy(buffer, p, len); + p += len; if (stm32_write_memory(stm32, addr, buffer, len) != STM32_ERR_OK) { ESP_LOGW(TAG, "Failed to write to STM32 flash memory"); From 587bf68091caffac14f4d7ed2714fac28848354c Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:03:30 -0500 Subject: [PATCH 06/26] [ltr501][pvvx_mithermometer][smt100] Convert static locals to instance members (#14569) Co-authored-by: Claude Opus 4.6 Co-authored-by: J. Nick Koston --- esphome/components/ltr501/ltr501.cpp | 21 +++++++++---------- esphome/components/ltr501/ltr501.h | 3 +++ .../pvvx_mithermometer/pvvx_mithermometer.cpp | 7 +++---- .../pvvx_mithermometer/pvvx_mithermometer.h | 2 ++ esphome/components/smt100/smt100.cpp | 16 +++++++------- esphome/components/smt100/smt100.h | 3 +++ 6 files changed, 28 insertions(+), 24 deletions(-) diff --git a/esphome/components/ltr501/ltr501.cpp b/esphome/components/ltr501/ltr501.cpp index 04de91e362..4c9006be1d 100644 --- a/esphome/components/ltr501/ltr501.cpp +++ b/esphome/components/ltr501/ltr501.cpp @@ -146,7 +146,6 @@ void LTRAlsPs501Component::update() { void LTRAlsPs501Component::loop() { ErrorCode err = i2c::ERROR_OK; - static uint8_t tries{0}; switch (this->state_) { case State::DELAYED_SETUP: @@ -175,20 +174,20 @@ void LTRAlsPs501Component::loop() { case State::WAITING_FOR_DATA: if (this->is_als_data_ready_(this->als_readings_) == LtrDataAvail::LTR_DATA_OK) { - tries = 0; + this->tries_ = 0; ESP_LOGV(TAG, "Reading sensor data assuming gain = %.0fx, time = %d ms", get_gain_coeff(this->als_readings_.gain), get_itime_ms(this->als_readings_.integration_time)); this->read_sensor_data_(this->als_readings_); this->apply_lux_calculation_(this->als_readings_); this->state_ = State::DATA_COLLECTED; - } else if (tries >= MAX_TRIES) { + } else if (this->tries_ >= MAX_TRIES) { ESP_LOGW(TAG, "Can't get data after several tries. Aborting."); - tries = 0; + this->tries_ = 0; this->status_set_warning(); this->state_ = State::IDLE; return; } else { - tries++; + this->tries_++; } break; @@ -230,21 +229,21 @@ void LTRAlsPs501Component::loop() { } void LTRAlsPs501Component::check_and_trigger_ps_() { - static uint32_t last_high_trigger_time{0}; - static uint32_t last_low_trigger_time{0}; uint16_t ps_data = this->read_ps_data_(); uint32_t now = millis(); if (ps_data != this->ps_readings_) { this->ps_readings_ = ps_data; // Higher values - object is closer to sensor - if (ps_data > this->ps_threshold_high_ && now - last_high_trigger_time >= this->ps_cooldown_time_s_ * 1000) { - last_high_trigger_time = now; + if (ps_data > this->ps_threshold_high_ && + now - this->last_ps_high_trigger_time_ >= this->ps_cooldown_time_s_ * 1000) { + this->last_ps_high_trigger_time_ = now; ESP_LOGD(TAG, "Proximity high threshold triggered. Value = %d, Trigger level = %d", ps_data, this->ps_threshold_high_); this->on_ps_high_trigger_callback_.call(); - } else if (ps_data < this->ps_threshold_low_ && now - last_low_trigger_time >= this->ps_cooldown_time_s_ * 1000) { - last_low_trigger_time = now; + } else if (ps_data < this->ps_threshold_low_ && + now - this->last_ps_low_trigger_time_ >= this->ps_cooldown_time_s_ * 1000) { + this->last_ps_low_trigger_time_ = now; ESP_LOGD(TAG, "Proximity low threshold triggered. Value = %d, Trigger level = %d", ps_data, this->ps_threshold_low_); this->on_ps_low_trigger_callback_.call(); diff --git a/esphome/components/ltr501/ltr501.h b/esphome/components/ltr501/ltr501.h index 02c025da30..d9a53c9bd4 100644 --- a/esphome/components/ltr501/ltr501.h +++ b/esphome/components/ltr501/ltr501.h @@ -74,6 +74,7 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { READY_TO_PUBLISH, KEEP_PUBLISHING } state_{State::NOT_INITIALIZED}; + uint8_t tries_{0}; LtrType ltr_type_{LtrType::LTR_TYPE_ALS_ONLY}; @@ -130,6 +131,8 @@ class LTRAlsPs501Component : public PollingComponent, public i2c::I2CDevice { PsGain501 ps_gain_{PsGain501::PS_GAIN_1}; uint16_t ps_threshold_high_{0xffff}; uint16_t ps_threshold_low_{0x0000}; + uint32_t last_ps_high_trigger_time_{0}; + uint32_t last_ps_low_trigger_time_{0}; // // Sensors for publishing data diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp index 239a1e74fe..35badf48bb 100644 --- a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.cpp @@ -66,12 +66,11 @@ optional PVVXMiThermometer::parse_header_(const esp32_ble_tracker:: return {}; } - static uint8_t last_frame_count = 0; - if (last_frame_count == raw[13]) { - ESP_LOGVV(TAG, "parse_header(): duplicate data packet received (%hhu).", last_frame_count); + if (this->last_frame_count_ == raw[13]) { + ESP_LOGVV(TAG, "parse_header(): duplicate data packet received (%hhu).", this->last_frame_count_); return {}; } - last_frame_count = raw[13]; + this->last_frame_count_ = raw[13]; return result; } diff --git a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h index c15e1e7e22..09b5e91a16 100644 --- a/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h +++ b/esphome/components/pvvx_mithermometer/pvvx_mithermometer.h @@ -39,6 +39,8 @@ class PVVXMiThermometer : public Component, public esp32_ble_tracker::ESPBTDevic sensor::Sensor *battery_voltage_{nullptr}; sensor::Sensor *signal_strength_{nullptr}; + uint8_t last_frame_count_{0}; + optional parse_header_(const esp32_ble_tracker::ServiceData &service_data); bool parse_message_(const std::vector &message, ParseResult &result); bool report_results_(const optional &result, const char *address); diff --git a/esphome/components/smt100/smt100.cpp b/esphome/components/smt100/smt100.cpp index 1bcb964264..105cc06edb 100644 --- a/esphome/components/smt100/smt100.cpp +++ b/esphome/components/smt100/smt100.cpp @@ -12,10 +12,9 @@ void SMT100Component::update() { } void SMT100Component::loop() { - static char buffer[MAX_LINE_LENGTH]; while (this->available() != 0) { - if (readline_(read(), buffer, MAX_LINE_LENGTH) > 0) { - int counts = (int) strtol((strtok(buffer, ",")), nullptr, 10); + if (this->readline_(this->read(), this->readline_buffer_, MAX_LINE_LENGTH) > 0) { + int counts = (int) strtol((strtok(this->readline_buffer_, ",")), nullptr, 10); float permittivity = (float) strtod((strtok(nullptr, ",")), nullptr); float moisture = (float) strtod((strtok(nullptr, ",")), nullptr); float temperature = (float) strtod((strtok(nullptr, ",")), nullptr); @@ -56,7 +55,6 @@ void SMT100Component::dump_config() { } int SMT100Component::readline_(int readch, char *buffer, int len) { - static int pos = 0; int rpos; if (readch > 0) { @@ -64,13 +62,13 @@ int SMT100Component::readline_(int readch, char *buffer, int len) { case '\n': // Ignore new-lines break; case '\r': // Return on CR - rpos = pos; - pos = 0; // Reset position index ready for next time + rpos = this->readline_pos_; + this->readline_pos_ = 0; // Reset position index ready for next time return rpos; default: - if (pos < len - 1) { - buffer[pos++] = readch; - buffer[pos] = 0; + if (this->readline_pos_ < len - 1) { + buffer[this->readline_pos_++] = readch; + buffer[this->readline_pos_] = 0; } } } diff --git a/esphome/components/smt100/smt100.h b/esphome/components/smt100/smt100.h index df8803e1c6..cb01b1ed55 100644 --- a/esphome/components/smt100/smt100.h +++ b/esphome/components/smt100/smt100.h @@ -28,6 +28,9 @@ class SMT100Component : public PollingComponent, public uart::UARTDevice { protected: int readline_(int readch, char *buffer, int len); + char readline_buffer_[MAX_LINE_LENGTH]{}; + int readline_pos_{0}; + sensor::Sensor *counts_sensor_{nullptr}; sensor::Sensor *permittivity_sensor_{nullptr}; sensor::Sensor *moisture_sensor_{nullptr}; From 5777908da712d64dde2f760d68aaa707b606dc1f Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:03:53 -0500 Subject: [PATCH 07/26] [iaqcore][scd30][sen21231][beken_spi_led_strip] Fix uninitialized variables and missing error checks (#14568) Co-authored-by: Claude Opus 4.6 --- esphome/components/beken_spi_led_strip/led_strip.cpp | 2 +- esphome/components/iaqcore/iaqcore.cpp | 10 ++++++---- esphome/components/scd30/scd30.cpp | 2 +- esphome/components/sen21231/sen21231.cpp | 7 ++++++- 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/esphome/components/beken_spi_led_strip/led_strip.cpp b/esphome/components/beken_spi_led_strip/led_strip.cpp index 67b8472257..f425f3ca5c 100644 --- a/esphome/components/beken_spi_led_strip/led_strip.cpp +++ b/esphome/components/beken_spi_led_strip/led_strip.cpp @@ -78,7 +78,7 @@ static void spi_set_clock(uint32_t max_hz) { int source_clk = 0; int spi_clk = 0; int div = 0; - uint32_t param; + uint32_t param = PWD_SPI_CLK_BIT; if (max_hz > 4333000) { if (max_hz > 30000000) { spi_clk = 30000000; diff --git a/esphome/components/iaqcore/iaqcore.cpp b/esphome/components/iaqcore/iaqcore.cpp index 274f9086b6..c414eb8f60 100644 --- a/esphome/components/iaqcore/iaqcore.cpp +++ b/esphome/components/iaqcore/iaqcore.cpp @@ -10,11 +10,13 @@ static const char *const TAG = "iaqcore"; enum IAQCoreErrorCode : uint8_t { ERROR_OK = 0, ERROR_RUNIN = 0x10, ERROR_BUSY = 0x01, ERROR_ERROR = 0x80 }; +static constexpr size_t SENSOR_DATA_LENGTH = 9; + struct SensorData { - uint16_t co2; - IAQCoreErrorCode status; int32_t resistance; + uint16_t co2; uint16_t tvoc; + IAQCoreErrorCode status; SensorData(const uint8_t *buffer) { this->co2 = encode_uint16(buffer[0], buffer[1]); @@ -33,9 +35,9 @@ void IAQCore::setup() { } void IAQCore::update() { - uint8_t buffer[sizeof(SensorData)]; + uint8_t buffer[SENSOR_DATA_LENGTH]; - if (this->read_register(0xB5, buffer, sizeof(buffer)) != i2c::ERROR_OK) { + if (this->read_register(0xB5, buffer, SENSOR_DATA_LENGTH) != i2c::ERROR_OK) { ESP_LOGD(TAG, "Read failed"); this->status_set_warning(); this->publish_nans_(); diff --git a/esphome/components/scd30/scd30.cpp b/esphome/components/scd30/scd30.cpp index 3c2c06fd68..e61d0142be 100644 --- a/esphome/components/scd30/scd30.cpp +++ b/esphome/components/scd30/scd30.cpp @@ -222,7 +222,7 @@ bool SCD30Component::force_recalibration_with_reference(uint16_t co2_reference) } uint16_t SCD30Component::get_forced_calibration_reference() { - uint16_t forced_calibration_reference; + uint16_t forced_calibration_reference = 0; // Get current CO2 calibration if (!this->get_register(SCD30_CMD_FORCED_CALIBRATION, forced_calibration_reference)) { ESP_LOGE(TAG, "Unable to read forced calibration reference."); diff --git a/esphome/components/sen21231/sen21231.cpp b/esphome/components/sen21231/sen21231.cpp index 67001c3f14..8c9f3d7134 100644 --- a/esphome/components/sen21231/sen21231.cpp +++ b/esphome/components/sen21231/sen21231.cpp @@ -20,7 +20,12 @@ void Sen21231Sensor::dump_config() { void Sen21231Sensor::read_data_() { person_sensor_results_t results; - this->read_bytes(PERSON_SENSOR_I2C_ADDRESS, (uint8_t *) &results, sizeof(results)); + if (!this->read_bytes(PERSON_SENSOR_I2C_ADDRESS, (uint8_t *) &results, sizeof(results))) { + ESP_LOGW(TAG, "Failed to read data from SEN21231"); + this->status_set_warning(); + return; + } + this->status_clear_warning(); ESP_LOGD(TAG, "SEN21231: %d faces detected", results.num_faces); this->publish_state(results.num_faces); if (results.num_faces == 1) { From de7572bd3e7ebb9308744efb6f7faf88c480629d Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 16:04:12 -0500 Subject: [PATCH 08/26] [lightwaverf] Fix ISR safety issues (#14563) Co-authored-by: Claude Opus 4.6 --- esphome/components/lightwaverf/LwRx.cpp | 14 +++++++++++--- esphome/components/lightwaverf/LwRx.h | 4 ++-- esphome/components/lightwaverf/LwTx.cpp | 3 ++- esphome/components/lightwaverf/LwTx.h | 4 ++-- 4 files changed, 17 insertions(+), 8 deletions(-) diff --git a/esphome/components/lightwaverf/LwRx.cpp b/esphome/components/lightwaverf/LwRx.cpp index 2b1ad5e870..9710457850 100644 --- a/esphome/components/lightwaverf/LwRx.cpp +++ b/esphome/components/lightwaverf/LwRx.cpp @@ -8,6 +8,7 @@ #include "LwRx.h" #include +#include "esphome/core/helpers.h" namespace esphome { namespace lightwaverf { @@ -185,13 +186,20 @@ bool LwRx::lwrx_getmessage(uint8_t *buf, uint8_t len) { bool ret = true; int16_t j = 0; // int if (this->rx_msgcomplete && len <= RX_MSGLEN) { + // Copy message under interrupt lock to prevent ISR overwriting rx_msg mid-read + uint8_t msg_copy[RX_MSGLEN]; + { + InterruptLock lock; + memcpy(msg_copy, this->rx_msg, RX_MSGLEN); + this->rx_msgcomplete = false; + } for (uint8_t i = 0; ret && i < RX_MSGLEN; i++) { if (this->rx_translate || (len != RX_MSGLEN)) { - j = this->rx_find_nibble_(this->rx_msg[i]); + j = this->rx_find_nibble_(msg_copy[i]); if (j < 0) ret = false; } else { - j = this->rx_msg[i]; + j = msg_copy[i]; } switch (len) { case 4: @@ -199,6 +207,7 @@ bool LwRx::lwrx_getmessage(uint8_t *buf, uint8_t len) { buf[2] = j; if (i == 2) buf[3] = j; + [[fallthrough]]; case 2: if (i == 3) buf[0] = j; @@ -212,7 +221,6 @@ bool LwRx::lwrx_getmessage(uint8_t *buf, uint8_t len) { break; } } - this->rx_msgcomplete = false; } else { ret = false; } diff --git a/esphome/components/lightwaverf/LwRx.h b/esphome/components/lightwaverf/LwRx.h index 7200f9a51c..8b34de9fbb 100644 --- a/esphome/components/lightwaverf/LwRx.h +++ b/esphome/components/lightwaverf/LwRx.h @@ -105,8 +105,8 @@ class LwRx { uint32_t rx_prev; // time of previous interrupt in microseconds - bool rx_msgcomplete = false; // set high when message available - bool rx_translate = true; // Set false to get raw data + volatile bool rx_msgcomplete = false; // set high when message available + bool rx_translate = true; // Set false to get raw data uint8_t rx_state = 0; diff --git a/esphome/components/lightwaverf/LwTx.cpp b/esphome/components/lightwaverf/LwTx.cpp index b69b93b978..8852935bfd 100644 --- a/esphome/components/lightwaverf/LwTx.cpp +++ b/esphome/components/lightwaverf/LwTx.cpp @@ -192,7 +192,8 @@ void LwTx::lwtx_set_gap_multiplier(uint8_t gap_multiplier) { this->tx_gap_multip void LwTx::lw_timer_start() { { InterruptLock lock; - static LwTx *arg = this; // NOLINT + static LwTx *arg; + arg = this; timer1_attachInterrupt([] { isr_t_xtimer(arg); }); timer1_enable(TIM_DIV16, TIM_EDGE, TIM_LOOP); timer1_write(this->espPeriod); diff --git a/esphome/components/lightwaverf/LwTx.h b/esphome/components/lightwaverf/LwTx.h index fe7b942a3a..9192426440 100644 --- a/esphome/components/lightwaverf/LwTx.h +++ b/esphome/components/lightwaverf/LwTx.h @@ -62,8 +62,8 @@ class LwTx { uint8_t tx_repeats = 12; // Number of repeats of message sent uint8_t txon = 1; uint8_t txoff = 0; - bool tx_msg_active = false; // set true to activate message sending - bool tx_translate = true; // Set false to send raw data + volatile bool tx_msg_active = false; // set true to activate message sending + bool tx_translate = true; // Set false to send raw data uint8_t tx_buf[TX_MSGLEN]; // the message buffer during reception uint8_t tx_repeat = 0; // counter for repeats From c26c5935b6c9bb1b7a521efa7188a0c3dad74e66 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:12:19 -1000 Subject: [PATCH 09/26] Bump github/codeql-action from 4.32.5 to 4.32.6 (#14566) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/codeql.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index 4bd018b5c9..1a0c54da6d 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -58,7 +58,7 @@ jobs: # Initializes the CodeQL tools for scanning. - name: Initialize CodeQL - uses: github/codeql-action/init@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5 + uses: github/codeql-action/init@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 with: languages: ${{ matrix.language }} build-mode: ${{ matrix.build-mode }} @@ -86,6 +86,6 @@ jobs: exit 1 - name: Perform CodeQL Analysis - uses: github/codeql-action/analyze@c793b717bc78562f491db7b0e93a3a178b099162 # v4.32.5 + uses: github/codeql-action/analyze@0d579ffd059c29b07949a3cce3983f0780820c98 # v4.32.6 with: category: "/language:${{matrix.language}}" From 086c1bb505dc718d35e9c91b6fbea5bb54705735 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 12:12:44 -1000 Subject: [PATCH 10/26] Bump docker/build-push-action from 6.19.2 to 7.0.0 in /.github/actions/build-image (#14567) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/build-image/action.yaml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/actions/build-image/action.yaml b/.github/actions/build-image/action.yaml index 38e93c4f17..a895226030 100644 --- a/.github/actions/build-image/action.yaml +++ b/.github/actions/build-image/action.yaml @@ -47,7 +47,7 @@ runs: - name: Build and push to ghcr by digest id: build-ghcr - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false @@ -73,7 +73,7 @@ runs: - name: Build and push to dockerhub by digest id: build-dockerhub - uses: docker/build-push-action@10e90e3645eae34f1e60eeb005ba3a3d33f178e8 # v6.19.2 + uses: docker/build-push-action@d08e5c354a6adb9ed34480a06d141179aa583294 # v7.0.0 env: DOCKER_BUILD_SUMMARY: false DOCKER_BUILD_RECORD_UPLOAD: false From 035f98569326bafd7da350e4a15c2739c424e987 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Fri, 6 Mar 2026 22:16:36 +0000 Subject: [PATCH 11/26] Bump ruff from 0.15.4 to 0.15.5 (#14565) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> Co-authored-by: J. Nick Koston --- .pre-commit-config.yaml | 2 +- requirements_test.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d70dd9d0e1..b036da6ef1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -11,7 +11,7 @@ ci: repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. - rev: v0.15.3 + rev: v0.15.5 hooks: # Run the linter. - id: ruff diff --git a/requirements_test.txt b/requirements_test.txt index 6b2617b656..93a20896aa 100644 --- a/requirements_test.txt +++ b/requirements_test.txt @@ -1,6 +1,6 @@ pylint==4.0.5 flake8==7.3.0 # also change in .pre-commit-config.yaml when updating -ruff==0.15.4 # also change in .pre-commit-config.yaml when updating +ruff==0.15.5 # also change in .pre-commit-config.yaml when updating pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating pre-commit From d8deb2255d3a982bb3a755efa9291a2a59014c69 Mon Sep 17 00:00:00 2001 From: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> Date: Fri, 6 Mar 2026 17:18:09 -0500 Subject: [PATCH 12/26] [mipi_rgb] Fix byte order and dirty bounds in fill() (#14537) Co-authored-by: Claude Opus 4.6 --- esphome/components/mipi_rgb/mipi_rgb.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/esphome/components/mipi_rgb/mipi_rgb.cpp b/esphome/components/mipi_rgb/mipi_rgb.cpp index 7ff6868c15..ae7c795846 100644 --- a/esphome/components/mipi_rgb/mipi_rgb.cpp +++ b/esphome/components/mipi_rgb/mipi_rgb.cpp @@ -288,9 +288,7 @@ void MipiRgb::draw_pixel_at(int x, int y, Color color) { if (!this->check_buffer_()) return; size_t pos = (y * this->width_) + x; - uint8_t hi_byte = static_cast(color.r & 0xF8) | (color.g >> 5); - uint8_t lo_byte = static_cast((color.g & 0x1C) << 3) | (color.b >> 3); - uint16_t new_color = hi_byte | (lo_byte << 8); // big endian + uint16_t new_color = convert_big_endian(display::ColorUtil::color_to_565(color)); if (this->buffer_[pos] == new_color) return; this->buffer_[pos] = new_color; @@ -315,10 +313,12 @@ void MipiRgb::fill(Color color) { } auto *ptr_16 = reinterpret_cast(this->buffer_); - uint8_t hi_byte = static_cast(color.r & 0xF8) | (color.g >> 5); - uint8_t lo_byte = static_cast((color.g & 0x1C) << 3) | (color.b >> 3); - uint16_t new_color = lo_byte | (hi_byte << 8); // little endian + uint16_t new_color = convert_big_endian(display::ColorUtil::color_to_565(color)); std::fill_n(ptr_16, this->width_ * this->height_, new_color); + this->x_low_ = 0; + this->y_low_ = 0; + this->x_high_ = this->width_ - 1; + this->y_high_ = this->height_ - 1; } int MipiRgb::get_width() { From f53ee70caadb75ef3ffe20f2a471d15ba06e34a7 Mon Sep 17 00:00:00 2001 From: AndreKR Date: Sat, 7 Mar 2026 01:29:20 +0100 Subject: [PATCH 13/26] [http_request] Make TLS buffer configurable on ESP8266 (#14009) Co-authored-by: Jonathan Swoboda <154711427+swoboda1337@users.noreply.github.com> --- esphome/components/http_request/__init__.py | 10 ++++++++++ .../components/http_request/http_request_arduino.cpp | 8 +++----- esphome/components/http_request/http_request_arduino.h | 10 ++++++++++ tests/components/http_request/test.esp8266-ard.yaml | 8 +++++--- 4 files changed, 28 insertions(+), 8 deletions(-) diff --git a/esphome/components/http_request/__init__.py b/esphome/components/http_request/__init__.py index 2d6ecae0bc..81337ebdf6 100644 --- a/esphome/components/http_request/__init__.py +++ b/esphome/components/http_request/__init__.py @@ -50,6 +50,8 @@ CONF_FOLLOW_REDIRECTS = "follow_redirects" CONF_REDIRECT_LIMIT = "redirect_limit" CONF_BUFFER_SIZE_RX = "buffer_size_rx" CONF_BUFFER_SIZE_TX = "buffer_size_tx" +CONF_TLS_BUFFER_SIZE_RX = "tls_buffer_size_rx" +CONF_TLS_BUFFER_SIZE_TX = "tls_buffer_size_tx" CONF_CA_CERTIFICATE_PATH = "ca_certificate_path" CONF_MAX_RESPONSE_BUFFER_SIZE = "max_response_buffer_size" @@ -124,6 +126,12 @@ CONFIG_SCHEMA = cv.All( cv.SplitDefault(CONF_BUFFER_SIZE_TX, esp32=512): cv.All( cv.uint16_t, cv.only_on_esp32 ), + cv.SplitDefault(CONF_TLS_BUFFER_SIZE_RX, esp8266=512): cv.All( + cv.uint16_t, cv.only_on_esp8266 + ), + cv.SplitDefault(CONF_TLS_BUFFER_SIZE_TX, esp8266=512): cv.All( + cv.uint16_t, cv.only_on_esp8266 + ), cv.Optional(CONF_CA_CERTIFICATE_PATH): cv.All( cv.file_, cv.Any(cv.only_on(PLATFORM_HOST), cv.only_on_esp32), @@ -150,6 +158,8 @@ async def to_code(config): if CORE.is_esp8266 and not config[CONF_ESP8266_DISABLE_SSL_SUPPORT]: cg.add_define("USE_HTTP_REQUEST_ESP8266_HTTPS") + cg.add(var.set_tls_buffer_size_rx(config[CONF_TLS_BUFFER_SIZE_RX])) + cg.add(var.set_tls_buffer_size_tx(config[CONF_TLS_BUFFER_SIZE_TX])) if timeout_ms := config.get(CONF_WATCHDOG_TIMEOUT): cg.add(var.set_watchdog_timeout(timeout_ms)) diff --git a/esphome/components/http_request/http_request_arduino.cpp b/esphome/components/http_request/http_request_arduino.cpp index 56b51e8b5a..f0dd649285 100644 --- a/esphome/components/http_request/http_request_arduino.cpp +++ b/esphome/components/http_request/http_request_arduino.cpp @@ -18,8 +18,6 @@ namespace esphome::http_request { static const char *const TAG = "http_request.arduino"; #ifdef USE_ESP8266 -static constexpr int RX_BUFFER_SIZE = 512; -static constexpr int TX_BUFFER_SIZE = 512; // ESP8266 Arduino core (WiFiClientSecureBearSSL.cpp) returns -1000 on OOM static constexpr int ESP8266_SSL_ERR_OOM = -1000; #endif @@ -58,7 +56,7 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur ESP_LOGV(TAG, "ESP8266 HTTPS connection with WiFiClientSecure"); stream_ptr = std::make_unique(); WiFiClientSecure *secure_client = static_cast(stream_ptr.get()); - secure_client->setBufferSizes(RX_BUFFER_SIZE, TX_BUFFER_SIZE); + secure_client->setBufferSizes(this->tls_buffer_size_rx_, this->tls_buffer_size_tx_); secure_client->setInsecure(); } else { stream_ptr = std::make_unique(); @@ -138,8 +136,8 @@ std::shared_ptr HttpRequestArduino::perform(const std::string &ur } ESP_LOGW(TAG, "SSL failure: %s (Code: %d)", LOG_STR_ARG(error_msg), last_error); if (last_error == ESP8266_SSL_ERR_OOM) { - ESP_LOGW(TAG, "Heap free: %u bytes, configured buffer sizes: %u bytes", ESP.getFreeHeap(), - static_cast(RX_BUFFER_SIZE + TX_BUFFER_SIZE)); + ESP_LOGW(TAG, "Configured TLS buffer sizes: %u/%u bytes, check max free heap block using the debug component", + (unsigned int) this->tls_buffer_size_rx_, (unsigned int) this->tls_buffer_size_tx_); } } else { ESP_LOGW(TAG, "Connection failure with no error code"); diff --git a/esphome/components/http_request/http_request_arduino.h b/esphome/components/http_request/http_request_arduino.h index d5ce5c0ff3..b009d45b1c 100644 --- a/esphome/components/http_request/http_request_arduino.h +++ b/esphome/components/http_request/http_request_arduino.h @@ -47,10 +47,20 @@ class HttpContainerArduino : public HttpContainer { }; class HttpRequestArduino : public HttpRequestComponent { + public: +#ifdef USE_ESP8266 + void set_tls_buffer_size_rx(uint16_t size) { this->tls_buffer_size_rx_ = size; } + void set_tls_buffer_size_tx(uint16_t size) { this->tls_buffer_size_tx_ = size; } +#endif + protected: std::shared_ptr perform(const std::string &url, const std::string &method, const std::string &body, const std::vector
&request_headers, const std::vector &lower_case_collect_headers) override; +#ifdef USE_ESP8266 + uint16_t tls_buffer_size_rx_{512}; + uint16_t tls_buffer_size_tx_{512}; +#endif }; } // namespace esphome::http_request diff --git a/tests/components/http_request/test.esp8266-ard.yaml b/tests/components/http_request/test.esp8266-ard.yaml index c1937b5a10..dd2d0df62b 100644 --- a/tests/components/http_request/test.esp8266-ard.yaml +++ b/tests/components/http_request/test.esp8266-ard.yaml @@ -1,4 +1,6 @@ -substitutions: - verify_ssl: "false" - <<: !include common.yaml + +http_request: + verify_ssl: false + tls_buffer_size_rx: 16384 + tls_buffer_size_tx: 512 From df11e2765ed016dabcdc211b7321289fa7cbeccf Mon Sep 17 00:00:00 2001 From: Ricardo Sanz Date: Sat, 7 Mar 2026 02:00:52 +0100 Subject: [PATCH 14/26] [climate][haier][template][core] Relocate CONF_CURRENT_TEMPERATURE to general const file (#14503) --- esphome/components/climate/__init__.py | 2 +- esphome/components/haier/climate.py | 8 ++------ esphome/components/template/water_heater/__init__.py | 2 +- esphome/const.py | 1 + 4 files changed, 5 insertions(+), 8 deletions(-) diff --git a/esphome/components/climate/__init__.py b/esphome/components/climate/__init__.py index 1f449ad2a4..f5b91c502c 100644 --- a/esphome/components/climate/__init__.py +++ b/esphome/components/climate/__init__.py @@ -8,6 +8,7 @@ from esphome.const import ( CONF_AWAY_COMMAND_TOPIC, CONF_AWAY_STATE_TOPIC, CONF_CURRENT_HUMIDITY_STATE_TOPIC, + CONF_CURRENT_TEMPERATURE, CONF_CURRENT_TEMPERATURE_STATE_TOPIC, CONF_CUSTOM_FAN_MODE, CONF_CUSTOM_PRESET, @@ -112,7 +113,6 @@ CLIMATE_SWING_MODES = { validate_climate_swing_mode = cv.enum(CLIMATE_SWING_MODES, upper=True) -CONF_CURRENT_TEMPERATURE = "current_temperature" CONF_MIN_HUMIDITY = "min_humidity" CONF_MAX_HUMIDITY = "max_humidity" CONF_TARGET_HUMIDITY = "target_humidity" diff --git a/esphome/components/haier/climate.py b/esphome/components/haier/climate.py index 8c3649058f..6c208f6caa 100644 --- a/esphome/components/haier/climate.py +++ b/esphome/components/haier/climate.py @@ -3,15 +3,11 @@ import logging from esphome import automation import esphome.codegen as cg from esphome.components import climate, logger, uart -from esphome.components.climate import ( - CONF_CURRENT_TEMPERATURE, - ClimateMode, - ClimatePreset, - ClimateSwingMode, -) +from esphome.components.climate import ClimateMode, ClimatePreset, ClimateSwingMode import esphome.config_validation as cv from esphome.const import ( CONF_BEEPER, + CONF_CURRENT_TEMPERATURE, CONF_DISPLAY, CONF_ID, CONF_LEVEL, diff --git a/esphome/components/template/water_heater/__init__.py b/esphome/components/template/water_heater/__init__.py index 71f98c826a..cb5f2dbe56 100644 --- a/esphome/components/template/water_heater/__init__.py +++ b/esphome/components/template/water_heater/__init__.py @@ -4,6 +4,7 @@ from esphome.components import water_heater import esphome.config_validation as cv from esphome.const import ( CONF_AWAY, + CONF_CURRENT_TEMPERATURE, CONF_ID, CONF_MODE, CONF_OPTIMISTIC, @@ -18,7 +19,6 @@ from esphome.types import ConfigType from .. import template_ns -CONF_CURRENT_TEMPERATURE = "current_temperature" CONF_IS_ON = "is_on" TemplateWaterHeater = template_ns.class_( diff --git a/esphome/const.py b/esphome/const.py index 060e962573..88e3c33fbc 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -274,6 +274,7 @@ CONF_CURRENT = "current" CONF_CURRENT_HUMIDITY_STATE_TOPIC = "current_humidity_state_topic" CONF_CURRENT_OPERATION = "current_operation" CONF_CURRENT_RESISTOR = "current_resistor" +CONF_CURRENT_TEMPERATURE = "current_temperature" CONF_CURRENT_TEMPERATURE_STATE_TOPIC = "current_temperature_state_topic" CONF_CUSTOM = "custom" CONF_CUSTOM_FAN_MODE = "custom_fan_mode" From 9b489c9eba639b8dea29dec7815c32c22cda28a1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Sat, 7 Mar 2026 03:52:51 +0000 Subject: [PATCH 15/26] Bump aioesphomeapi from 44.2.0 to 44.3.1 (#14580) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requirements.txt b/requirements.txt index f111e05a9d..8e875eba62 100644 --- a/requirements.txt +++ b/requirements.txt @@ -12,7 +12,7 @@ platformio==6.1.19 esptool==5.2.0 click==8.3.1 esphome-dashboard==20260210.0 -aioesphomeapi==44.2.0 +aioesphomeapi==44.3.1 zeroconf==0.148.0 puremagic==1.30 ruamel.yaml==0.19.1 # dashboard_import From 05ae69b766e9745892e97c8ac40d7017d5fecb3b Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Fri, 6 Mar 2026 19:00:37 -1000 Subject: [PATCH 16/26] [api] Sync api.proto from aioesphomeapi (#14579) --- esphome/components/api/api.proto | 132 ++++++++++++++ esphome/components/api/api_pb2.cpp | 161 +++++++++++++++++ esphome/components/api/api_pb2.h | 200 ++++++++++++++++++++- esphome/components/api/api_pb2_dump.cpp | 117 ++++++++++++ esphome/components/api/api_pb2_service.cpp | 66 +++++++ esphome/components/api/api_pb2_service.h | 21 +++ 6 files changed, 696 insertions(+), 1 deletion(-) diff --git a/esphome/components/api/api.proto b/esphome/components/api/api.proto index 802e3e3ae2..618fd1b83c 100644 --- a/esphome/components/api/api.proto +++ b/esphome/components/api/api.proto @@ -58,6 +58,7 @@ service APIConnection { rpc subscribe_bluetooth_connections_free(SubscribeBluetoothConnectionsFreeRequest) returns (BluetoothConnectionsFreeResponse) {} rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {} rpc bluetooth_scanner_set_mode(BluetoothScannerSetModeRequest) returns (void) {} + rpc bluetooth_set_connection_params(BluetoothSetConnectionParamsRequest) returns (BluetoothSetConnectionParamsResponse) {} rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {} rpc voice_assistant_get_configuration(VoiceAssistantConfigurationRequest) returns (VoiceAssistantConfigurationResponse) {} @@ -69,6 +70,12 @@ service APIConnection { rpc zwave_proxy_request(ZWaveProxyRequest) returns (void) {} rpc infrared_rf_transmit_raw_timings(InfraredRFTransmitRawTimingsRequest) returns (void) {} + + rpc serial_proxy_configure(SerialProxyConfigureRequest) returns (void) {} + rpc serial_proxy_write(SerialProxyWriteRequest) returns (void) {} + rpc serial_proxy_set_modem_pins(SerialProxySetModemPinsRequest) returns (void) {} + rpc serial_proxy_get_modem_pins(SerialProxyGetModemPinsRequest) returns (void) {} + rpc serial_proxy_request(SerialProxyRequest) returns (void) {} } @@ -198,6 +205,17 @@ message DeviceInfo { uint32 area_id = 3; } +enum SerialProxyPortType { + SERIAL_PROXY_PORT_TYPE_TTL = 0; + SERIAL_PROXY_PORT_TYPE_RS232 = 1; + SERIAL_PROXY_PORT_TYPE_RS485 = 2; +} + +message SerialProxyInfo { + string name = 1; // Human-readable port name + SerialProxyPortType port_type = 2; // Port type (RS232, RS485) +} + message DeviceInfoResponse { option (id) = 10; option (source) = SOURCE_SERVER; @@ -260,6 +278,9 @@ message DeviceInfoResponse { // Indicates if Z-Wave proxy support is available and features supported uint32 zwave_proxy_feature_flags = 23 [(field_ifdef) = "USE_ZWAVE_PROXY"]; uint32 zwave_home_id = 24 [(field_ifdef) = "USE_ZWAVE_PROXY"]; + + // Serial proxy instance metadata + repeated SerialProxyInfo serial_proxies = 25 [(field_ifdef) = "USE_SERIAL_PROXY", (fixed_array_size_define) = "SERIAL_PROXY_COUNT"]; } message ListEntitiesRequest { @@ -2517,3 +2538,114 @@ message InfraredRFReceiveEvent { fixed32 key = 2; // Key identifying the receiver instance repeated sint32 timings = 3 [packed = true, (container_pointer_no_template) = "std::vector"]; // Raw timings in microseconds (zigzag-encoded): alternating mark/space periods } + +// ==================== SERIAL PROXY ==================== + +enum SerialProxyParity { + SERIAL_PROXY_PARITY_NONE = 0; + SERIAL_PROXY_PARITY_EVEN = 1; + SERIAL_PROXY_PARITY_ODD = 2; +} + +// Configure UART parameters for a serial proxy instance +message SerialProxyConfigureRequest { + option (id) = 138; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + uint32 baudrate = 2; // Baud rate in bits per second + bool flow_control = 3; // Enable hardware flow control + SerialProxyParity parity = 4; // Parity setting + uint32 stop_bits = 5; // Number of stop bits (1 or 2) + uint32 data_size = 6; // Number of data bits (5-8) +} + +// Data received from a serial device, forwarded to clients +message SerialProxyDataReceived { + option (id) = 139; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SERIAL_PROXY"; + option (no_delay) = true; + + uint32 instance = 1; // Instance index (0-based) + bytes data = 2; // Raw data received from the serial device +} + +// Write data to a serial device +message SerialProxyWriteRequest { + option (id) = 140; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + option (no_delay) = true; + + uint32 instance = 1; // Instance index (0-based) + bytes data = 2; // Raw data to write to the serial device +} + +// Set modem control pin states (RTS and DTR) +message SerialProxySetModemPinsRequest { + option (id) = 141; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags +} + +// Request current modem control pin states +message SerialProxyGetModemPinsRequest { + option (id) = 142; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) +} + +// Response with current modem control pin states +message SerialProxyGetModemPinsResponse { + option (id) = 143; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + uint32 line_states = 2; // Bitmask of SerialProxyLineStateFlags +} + +enum SerialProxyRequestType { + SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0; // Subscribe to receive data from this serial proxy instance + SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1; // Unsubscribe from this serial proxy instance + SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2; // Flush the serial port (block until all TX data is sent) +} + +// Generic request message for simple serial proxy operations +message SerialProxyRequest { + option (id) = 144; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_SERIAL_PROXY"; + + uint32 instance = 1; // Instance index (0-based) + SerialProxyRequestType type = 2; // Request type +} + +// ==================== BLUETOOTH CONNECTION PARAMS ==================== +message BluetoothSetConnectionParamsRequest { + option (id) = 145; + option (source) = SOURCE_CLIENT; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + uint32 min_interval = 2; // units of 1.25ms + uint32 max_interval = 3; // units of 1.25ms + uint32 latency = 4; + uint32 timeout = 5; // units of 10ms +} + +message BluetoothSetConnectionParamsResponse { + option (id) = 146; + option (source) = SOURCE_SERVER; + option (ifdef) = "USE_BLUETOOTH_PROXY"; + + uint64 address = 1; + int32 error = 2; +} diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index d8703aa416..b1176de539 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -71,6 +71,18 @@ uint32_t DeviceInfo::calculate_size() const { return size; } #endif +#ifdef USE_SERIAL_PROXY +void SerialProxyInfo::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_string(1, this->name); + buffer.encode_uint32(2, static_cast(this->port_type)); +} +uint32_t SerialProxyInfo::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_length(1, this->name.size()); + size += ProtoSize::calc_uint32(1, static_cast(this->port_type)); + return size; +} +#endif void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(2, this->name); buffer.encode_string(3, this->mac_address); @@ -125,6 +137,11 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { #ifdef USE_ZWAVE_PROXY buffer.encode_uint32(24, this->zwave_home_id); #endif +#ifdef USE_SERIAL_PROXY + for (const auto &it : this->serial_proxies) { + buffer.encode_message(25, it); + } +#endif } uint32_t DeviceInfoResponse::calculate_size() const { uint32_t size = 0; @@ -180,6 +197,11 @@ uint32_t DeviceInfoResponse::calculate_size() const { #endif #ifdef USE_ZWAVE_PROXY size += ProtoSize::calc_uint32(2, this->zwave_home_id); +#endif +#ifdef USE_SERIAL_PROXY + for (const auto &it : this->serial_proxies) { + size += ProtoSize::calc_message_force(2, it.calculate_size()); + } #endif return size; } @@ -3714,5 +3736,144 @@ uint32_t InfraredRFReceiveEvent::calculate_size() const { return size; } #endif +#ifdef USE_SERIAL_PROXY +bool SerialProxyConfigureRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->instance = value.as_uint32(); + break; + case 2: + this->baudrate = value.as_uint32(); + break; + case 3: + this->flow_control = value.as_bool(); + break; + case 4: + this->parity = static_cast(value.as_uint32()); + break; + case 5: + this->stop_bits = value.as_uint32(); + break; + case 6: + this->data_size = value.as_uint32(); + break; + default: + return false; + } + return true; +} +void SerialProxyDataReceived::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_uint32(1, this->instance); + buffer.encode_bytes(2, this->data_ptr_, this->data_len_); +} +uint32_t SerialProxyDataReceived::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->instance); + size += ProtoSize::calc_length(1, this->data_len_); + return size; +} +bool SerialProxyWriteRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->instance = value.as_uint32(); + break; + default: + return false; + } + return true; +} +bool SerialProxyWriteRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { + switch (field_id) { + case 2: { + this->data = value.data(); + this->data_len = value.size(); + break; + } + default: + return false; + } + return true; +} +bool SerialProxySetModemPinsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->instance = value.as_uint32(); + break; + case 2: + this->line_states = value.as_uint32(); + break; + default: + return false; + } + return true; +} +bool SerialProxyGetModemPinsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->instance = value.as_uint32(); + break; + default: + return false; + } + return true; +} +void SerialProxyGetModemPinsResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_uint32(1, this->instance); + buffer.encode_uint32(2, this->line_states); +} +uint32_t SerialProxyGetModemPinsResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint32(1, this->instance); + size += ProtoSize::calc_uint32(1, this->line_states); + return size; +} +bool SerialProxyRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->instance = value.as_uint32(); + break; + case 2: + this->type = static_cast(value.as_uint32()); + break; + default: + return false; + } + return true; +} +#endif +#ifdef USE_BLUETOOTH_PROXY +bool BluetoothSetConnectionParamsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { + switch (field_id) { + case 1: + this->address = value.as_uint64(); + break; + case 2: + this->min_interval = value.as_uint32(); + break; + case 3: + this->max_interval = value.as_uint32(); + break; + case 4: + this->latency = value.as_uint32(); + break; + case 5: + this->timeout = value.as_uint32(); + break; + default: + return false; + } + return true; +} +void BluetoothSetConnectionParamsResponse::encode(ProtoWriteBuffer &buffer) const { + buffer.encode_uint64(1, this->address); + buffer.encode_int32(2, this->error); +} +uint32_t BluetoothSetConnectionParamsResponse::calculate_size() const { + uint32_t size = 0; + size += ProtoSize::calc_uint64(1, this->address); + size += ProtoSize::calc_int32(1, this->error); + return size; +} +#endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2.h b/esphome/components/api/api_pb2.h index 89cb1158f3..a6167dc810 100644 --- a/esphome/components/api/api_pb2.h +++ b/esphome/components/api/api_pb2.h @@ -11,6 +11,11 @@ namespace esphome::api { namespace enums { +enum SerialProxyPortType : uint32_t { + SERIAL_PROXY_PORT_TYPE_TTL = 0, + SERIAL_PROXY_PORT_TYPE_RS232 = 1, + SERIAL_PROXY_PORT_TYPE_RS485 = 2, +}; enum EntityCategory : uint32_t { ENTITY_CATEGORY_NONE = 0, ENTITY_CATEGORY_CONFIG = 1, @@ -317,6 +322,18 @@ enum ZWaveProxyRequestType : uint32_t { ZWAVE_PROXY_REQUEST_TYPE_HOME_ID_CHANGE = 2, }; #endif +#ifdef USE_SERIAL_PROXY +enum SerialProxyParity : uint32_t { + SERIAL_PROXY_PARITY_NONE = 0, + SERIAL_PROXY_PARITY_EVEN = 1, + SERIAL_PROXY_PARITY_ODD = 2, +}; +enum SerialProxyRequestType : uint32_t { + SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE = 0, + SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE = 1, + SERIAL_PROXY_REQUEST_TYPE_FLUSH = 2, +}; +#endif } // namespace enums @@ -477,10 +494,24 @@ class DeviceInfo final : public ProtoMessage { protected: }; #endif +#ifdef USE_SERIAL_PROXY +class SerialProxyInfo final : public ProtoMessage { + public: + StringRef name{}; + enums::SerialProxyPortType port_type{}; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +#endif class DeviceInfoResponse final : public ProtoMessage { public: static constexpr uint8_t MESSAGE_TYPE = 10; - static constexpr uint8_t ESTIMATED_SIZE = 255; + static constexpr uint16_t ESTIMATED_SIZE = 309; #ifdef HAS_PROTO_MESSAGE_DUMP const char *message_name() const override { return "device_info_response"; } #endif @@ -532,6 +563,9 @@ class DeviceInfoResponse final : public ProtoMessage { #endif #ifdef USE_ZWAVE_PROXY uint32_t zwave_home_id{0}; +#endif +#ifdef USE_SERIAL_PROXY + std::array serial_proxies{}; #endif void encode(ProtoWriteBuffer &buffer) const; uint32_t calculate_size() const; @@ -3061,5 +3095,169 @@ class InfraredRFReceiveEvent final : public ProtoMessage { protected: }; #endif +#ifdef USE_SERIAL_PROXY +class SerialProxyConfigureRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 138; + static constexpr uint8_t ESTIMATED_SIZE = 20; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_configure_request"; } +#endif + uint32_t instance{0}; + uint32_t baudrate{0}; + bool flow_control{false}; + enums::SerialProxyParity parity{}; + uint32_t stop_bits{0}; + uint32_t data_size{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SerialProxyDataReceived final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 139; + static constexpr uint8_t ESTIMATED_SIZE = 23; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_data_received"; } +#endif + uint32_t instance{0}; + const uint8_t *data_ptr_{nullptr}; + size_t data_len_{0}; + void set_data(const uint8_t *data, size_t len) { + this->data_ptr_ = data; + this->data_len_ = len; + } + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +class SerialProxyWriteRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 140; + static constexpr uint8_t ESTIMATED_SIZE = 23; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_write_request"; } +#endif + uint32_t instance{0}; + const uint8_t *data{nullptr}; + uint16_t data_len{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SerialProxySetModemPinsRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 141; + static constexpr uint8_t ESTIMATED_SIZE = 8; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_set_modem_pins_request"; } +#endif + uint32_t instance{0}; + uint32_t line_states{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SerialProxyGetModemPinsRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 142; + static constexpr uint8_t ESTIMATED_SIZE = 4; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_get_modem_pins_request"; } +#endif + uint32_t instance{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class SerialProxyGetModemPinsResponse final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 143; + static constexpr uint8_t ESTIMATED_SIZE = 8; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_get_modem_pins_response"; } +#endif + uint32_t instance{0}; + uint32_t line_states{0}; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +class SerialProxyRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 144; + static constexpr uint8_t ESTIMATED_SIZE = 6; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "serial_proxy_request"; } +#endif + uint32_t instance{0}; + enums::SerialProxyRequestType type{}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +#endif +#ifdef USE_BLUETOOTH_PROXY +class BluetoothSetConnectionParamsRequest final : public ProtoDecodableMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 145; + static constexpr uint8_t ESTIMATED_SIZE = 20; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "bluetooth_set_connection_params_request"; } +#endif + uint64_t address{0}; + uint32_t min_interval{0}; + uint32_t max_interval{0}; + uint32_t latency{0}; + uint32_t timeout{0}; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: + bool decode_varint(uint32_t field_id, ProtoVarInt value) override; +}; +class BluetoothSetConnectionParamsResponse final : public ProtoMessage { + public: + static constexpr uint8_t MESSAGE_TYPE = 146; + static constexpr uint8_t ESTIMATED_SIZE = 8; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *message_name() const override { return "bluetooth_set_connection_params_response"; } +#endif + uint64_t address{0}; + int32_t error{0}; + void encode(ProtoWriteBuffer &buffer) const; + uint32_t calculate_size() const; +#ifdef HAS_PROTO_MESSAGE_DUMP + const char *dump_to(DumpBuffer &out) const override; +#endif + + protected: +}; +#endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2_dump.cpp b/esphome/components/api/api_pb2_dump.cpp index 4eec42e936..086b1bdc2f 100644 --- a/esphome/components/api/api_pb2_dump.cpp +++ b/esphome/components/api/api_pb2_dump.cpp @@ -100,6 +100,18 @@ static void dump_bytes_field(DumpBuffer &out, const char *field_name, const uint out.append(hex_buf).append("\n"); } +template<> const char *proto_enum_to_string(enums::SerialProxyPortType value) { + switch (value) { + case enums::SERIAL_PROXY_PORT_TYPE_TTL: + return "SERIAL_PROXY_PORT_TYPE_TTL"; + case enums::SERIAL_PROXY_PORT_TYPE_RS232: + return "SERIAL_PROXY_PORT_TYPE_RS232"; + case enums::SERIAL_PROXY_PORT_TYPE_RS485: + return "SERIAL_PROXY_PORT_TYPE_RS485"; + default: + return "UNKNOWN"; + } +} template<> const char *proto_enum_to_string(enums::EntityCategory value) { switch (value) { case enums::ENTITY_CATEGORY_NONE: @@ -752,6 +764,32 @@ template<> const char *proto_enum_to_string(enums: } } #endif +#ifdef USE_SERIAL_PROXY +template<> const char *proto_enum_to_string(enums::SerialProxyParity value) { + switch (value) { + case enums::SERIAL_PROXY_PARITY_NONE: + return "SERIAL_PROXY_PARITY_NONE"; + case enums::SERIAL_PROXY_PARITY_EVEN: + return "SERIAL_PROXY_PARITY_EVEN"; + case enums::SERIAL_PROXY_PARITY_ODD: + return "SERIAL_PROXY_PARITY_ODD"; + default: + return "UNKNOWN"; + } +} +template<> const char *proto_enum_to_string(enums::SerialProxyRequestType value) { + switch (value) { + case enums::SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE: + return "SERIAL_PROXY_REQUEST_TYPE_SUBSCRIBE"; + case enums::SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE: + return "SERIAL_PROXY_REQUEST_TYPE_UNSUBSCRIBE"; + case enums::SERIAL_PROXY_REQUEST_TYPE_FLUSH: + return "SERIAL_PROXY_REQUEST_TYPE_FLUSH"; + default: + return "UNKNOWN"; + } +} +#endif const char *HelloRequest::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "HelloRequest"); @@ -801,6 +839,14 @@ const char *DeviceInfo::dump_to(DumpBuffer &out) const { return out.c_str(); } #endif +#ifdef USE_SERIAL_PROXY +const char *SerialProxyInfo::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyInfo"); + dump_field(out, "name", this->name); + dump_field(out, "port_type", static_cast(this->port_type)); + return out.c_str(); +} +#endif const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const { MessageDumpHelper helper(out, "DeviceInfoResponse"); dump_field(out, "name", this->name); @@ -861,6 +907,13 @@ const char *DeviceInfoResponse::dump_to(DumpBuffer &out) const { #endif #ifdef USE_ZWAVE_PROXY dump_field(out, "zwave_home_id", this->zwave_home_id); +#endif +#ifdef USE_SERIAL_PROXY + for (const auto &it : this->serial_proxies) { + out.append(" serial_proxies: "); + it.dump_to(out); + out.append("\n"); + } #endif return out.c_str(); } @@ -2510,6 +2563,70 @@ const char *InfraredRFReceiveEvent::dump_to(DumpBuffer &out) const { return out.c_str(); } #endif +#ifdef USE_SERIAL_PROXY +const char *SerialProxyConfigureRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyConfigureRequest"); + dump_field(out, "instance", this->instance); + dump_field(out, "baudrate", this->baudrate); + dump_field(out, "flow_control", this->flow_control); + dump_field(out, "parity", static_cast(this->parity)); + dump_field(out, "stop_bits", this->stop_bits); + dump_field(out, "data_size", this->data_size); + return out.c_str(); +} +const char *SerialProxyDataReceived::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyDataReceived"); + dump_field(out, "instance", this->instance); + dump_bytes_field(out, "data", this->data_ptr_, this->data_len_); + return out.c_str(); +} +const char *SerialProxyWriteRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyWriteRequest"); + dump_field(out, "instance", this->instance); + dump_bytes_field(out, "data", this->data, this->data_len); + return out.c_str(); +} +const char *SerialProxySetModemPinsRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxySetModemPinsRequest"); + dump_field(out, "instance", this->instance); + dump_field(out, "line_states", this->line_states); + return out.c_str(); +} +const char *SerialProxyGetModemPinsRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyGetModemPinsRequest"); + dump_field(out, "instance", this->instance); + return out.c_str(); +} +const char *SerialProxyGetModemPinsResponse::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyGetModemPinsResponse"); + dump_field(out, "instance", this->instance); + dump_field(out, "line_states", this->line_states); + return out.c_str(); +} +const char *SerialProxyRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "SerialProxyRequest"); + dump_field(out, "instance", this->instance); + dump_field(out, "type", static_cast(this->type)); + return out.c_str(); +} +#endif +#ifdef USE_BLUETOOTH_PROXY +const char *BluetoothSetConnectionParamsRequest::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "BluetoothSetConnectionParamsRequest"); + dump_field(out, "address", this->address); + dump_field(out, "min_interval", this->min_interval); + dump_field(out, "max_interval", this->max_interval); + dump_field(out, "latency", this->latency); + dump_field(out, "timeout", this->timeout); + return out.c_str(); +} +const char *BluetoothSetConnectionParamsResponse::dump_to(DumpBuffer &out) const { + MessageDumpHelper helper(out, "BluetoothSetConnectionParamsResponse"); + dump_field(out, "address", this->address); + dump_field(out, "error", this->error); + return out.c_str(); +} +#endif } // namespace esphome::api diff --git a/esphome/components/api/api_pb2_service.cpp b/esphome/components/api/api_pb2_service.cpp index f9151ae3b4..f2f7fa5238 100644 --- a/esphome/components/api/api_pb2_service.cpp +++ b/esphome/components/api/api_pb2_service.cpp @@ -634,6 +634,72 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, this->on_infrared_rf_transmit_raw_timings_request(msg); break; } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxyConfigureRequest::MESSAGE_TYPE: { + SerialProxyConfigureRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_configure_request"), msg); +#endif + this->on_serial_proxy_configure_request(msg); + break; + } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxyWriteRequest::MESSAGE_TYPE: { + SerialProxyWriteRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_write_request"), msg); +#endif + this->on_serial_proxy_write_request(msg); + break; + } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxySetModemPinsRequest::MESSAGE_TYPE: { + SerialProxySetModemPinsRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_set_modem_pins_request"), msg); +#endif + this->on_serial_proxy_set_modem_pins_request(msg); + break; + } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxyGetModemPinsRequest::MESSAGE_TYPE: { + SerialProxyGetModemPinsRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_get_modem_pins_request"), msg); +#endif + this->on_serial_proxy_get_modem_pins_request(msg); + break; + } +#endif +#ifdef USE_SERIAL_PROXY + case SerialProxyRequest::MESSAGE_TYPE: { + SerialProxyRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_serial_proxy_request"), msg); +#endif + this->on_serial_proxy_request(msg); + break; + } +#endif +#ifdef USE_BLUETOOTH_PROXY + case BluetoothSetConnectionParamsRequest::MESSAGE_TYPE: { + BluetoothSetConnectionParamsRequest msg; + msg.decode(msg_data, msg_size); +#ifdef HAS_PROTO_MESSAGE_DUMP + this->log_receive_message_(LOG_STR("on_bluetooth_set_connection_params_request"), msg); +#endif + this->on_bluetooth_set_connection_params_request(msg); + break; + } #endif default: break; diff --git a/esphome/components/api/api_pb2_service.h b/esphome/components/api/api_pb2_service.h index e70b97196b..a031d2d969 100644 --- a/esphome/components/api/api_pb2_service.h +++ b/esphome/components/api/api_pb2_service.h @@ -216,6 +216,27 @@ class APIServerConnectionBase : public ProtoService { virtual void on_infrared_rf_transmit_raw_timings_request(const InfraredRFTransmitRawTimingsRequest &value){}; #endif +#ifdef USE_SERIAL_PROXY + virtual void on_serial_proxy_configure_request(const SerialProxyConfigureRequest &value){}; +#endif + +#ifdef USE_SERIAL_PROXY + virtual void on_serial_proxy_write_request(const SerialProxyWriteRequest &value){}; +#endif +#ifdef USE_SERIAL_PROXY + virtual void on_serial_proxy_set_modem_pins_request(const SerialProxySetModemPinsRequest &value){}; +#endif +#ifdef USE_SERIAL_PROXY + virtual void on_serial_proxy_get_modem_pins_request(const SerialProxyGetModemPinsRequest &value){}; +#endif + +#ifdef USE_SERIAL_PROXY + virtual void on_serial_proxy_request(const SerialProxyRequest &value){}; +#endif +#ifdef USE_BLUETOOTH_PROXY + virtual void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &value){}; +#endif + protected: void read_message(uint32_t msg_size, uint32_t msg_type, const uint8_t *msg_data) override; }; From cbebb811965d63f115ea4fba5c8fd7f6da393b29 Mon Sep 17 00:00:00 2001 From: rwrozelle Date: Sat, 7 Mar 2026 08:12:27 -0800 Subject: [PATCH 17/26] [openthread] move esp functions into correct file (#14588) --- esphome/components/openthread/openthread.cpp | 9 ++------- esphome/components/openthread/openthread.h | 2 ++ esphome/components/openthread/openthread_esp.cpp | 5 +++++ 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/esphome/components/openthread/openthread.cpp b/esphome/components/openthread/openthread.cpp index fb81481299..1596b6e990 100644 --- a/esphome/components/openthread/openthread.cpp +++ b/esphome/components/openthread/openthread.cpp @@ -1,7 +1,6 @@ #include "esphome/core/defines.h" #ifdef USE_OPENTHREAD #include "openthread.h" -#include "esp_openthread.h" #include @@ -51,7 +50,7 @@ void OpenThreadComponent::on_state_changed_(otChangedFlags flags, void *context) auto *self = static_cast(context); // This runs on the OpenThread task thread with the OT lock held, // so we can safely call otThreadGetDeviceRole directly. - otInstance *instance = esp_openthread_get_instance(); + otInstance *instance = self->get_openthread_instance_(); otDeviceRole role = otThreadGetDeviceRole(instance); self->connected_ = role >= OT_DEVICE_ROLE_CHILD; } @@ -233,16 +232,12 @@ bool OpenThreadComponent::teardown() { otSrpClientClearHostAndServices(instance); otSrpClientBuffersFreeAllServices(instance); global_openthread_component = nullptr; -#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0) ESP_LOGD(TAG, "Exit main loop "); - int error = esp_openthread_mainloop_exit(); + int error = this->openthread_stop_(); if (error != ESP_OK) { ESP_LOGW(TAG, "Failed attempt to stop main loop %d", error); this->teardown_complete_ = true; } -#else - this->teardown_complete_ = true; -#endif } return this->teardown_complete_; } diff --git a/esphome/components/openthread/openthread.h b/esphome/components/openthread/openthread.h index c87f4fa7c1..75d8fe11fd 100644 --- a/esphome/components/openthread/openthread.h +++ b/esphome/components/openthread/openthread.h @@ -44,6 +44,8 @@ class OpenThreadComponent : public Component { protected: std::optional get_omr_address_(InstanceLock &lock); static void on_state_changed_(otChangedFlags flags, void *context); + otInstance *get_openthread_instance_(); + int openthread_stop_(); std::function factory_reset_external_callback_; #if CONFIG_OPENTHREAD_MTD uint32_t poll_period_{0}; diff --git a/esphome/components/openthread/openthread_esp.cpp b/esphome/components/openthread/openthread_esp.cpp index cdc7a404b2..9cc9223b52 100644 --- a/esphome/components/openthread/openthread_esp.cpp +++ b/esphome/components/openthread/openthread_esp.cpp @@ -190,6 +190,8 @@ void OpenThreadComponent::ot_main() { vTaskDelete(NULL); } +int OpenThreadComponent::openthread_stop_() { return esp_openthread_mainloop_exit(); } + network::IPAddresses OpenThreadComponent::get_ip_addresses() { network::IPAddresses addresses; struct esp_ip6_addr if_ip6s[CONFIG_LWIP_IPV6_NUM_ADDRESSES]; @@ -204,6 +206,9 @@ network::IPAddresses OpenThreadComponent::get_ip_addresses() { return addresses; } +// not thread safe, only use in read-only use cases +otInstance *OpenThreadComponent::get_openthread_instance_() { return esp_openthread_get_instance(); } + std::optional InstanceLock::try_acquire(int delay) { if (esp_openthread_lock_acquire(delay)) { return InstanceLock(); From 0e106d843c730eb15e083ebd0147124dadc99a5a Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Sat, 7 Mar 2026 17:18:21 +0100 Subject: [PATCH 18/26] [nrf52][zephyr] support for multi on rate callbacks (#14557) --- esphome/components/nrf52/__init__.py | 3 +++ esphome/components/nrf52/dfu.cpp | 35 ++++++++------------------- esphome/components/nrf52/dfu.h | 1 - esphome/components/zephyr/__init__.py | 20 +++++++++++---- esphome/components/zephyr/cdc_acm.cpp | 30 +++++++++++++++++++++++ esphome/components/zephyr/cdc_acm.h | 27 +++++++++++++++++++++ esphome/components/zephyr/const.py | 2 ++ 7 files changed, 87 insertions(+), 31 deletions(-) create mode 100644 esphome/components/zephyr/cdc_acm.cpp create mode 100644 esphome/components/zephyr/cdc_acm.h diff --git a/esphome/components/nrf52/__init__.py b/esphome/components/nrf52/__init__.py index a12d1db1ab..0a9fb5939a 100644 --- a/esphome/components/nrf52/__init__.py +++ b/esphome/components/nrf52/__init__.py @@ -20,8 +20,10 @@ from esphome.components.zephyr import ( ) from esphome.components.zephyr.const import ( BOOTLOADER_MCUBOOT, + CONF_CDC_ACM, KEY_BOOTLOADER, KEY_ZEPHYR, + CdcAcm, ) import esphome.config_validation as cv from esphome.const import ( @@ -159,6 +161,7 @@ CONFIG_SCHEMA = cv.All( cv.Required(CONF_VERSION): cv.string_strict, } ), + cv.GenerateID(CONF_CDC_ACM): cv.declare_id(CdcAcm), } ), set_framework, diff --git a/esphome/components/nrf52/dfu.cpp b/esphome/components/nrf52/dfu.cpp index 9e49373467..c2017248d2 100644 --- a/esphome/components/nrf52/dfu.cpp +++ b/esphome/components/nrf52/dfu.cpp @@ -2,42 +2,27 @@ #ifdef USE_NRF52_DFU -#include -#include -#include #include "esphome/core/log.h" +#include "esphome/components/zephyr/cdc_acm.h" namespace esphome { namespace nrf52 { static const char *const TAG = "dfu"; -volatile bool goto_dfu = false; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) - static const uint32_t DFU_DBL_RESET_MAGIC = 0x5A1AD5; // SALADS -#define DEVICE_AND_COMMA(node_id) DEVICE_DT_GET(node_id), - -static void cdc_dte_rate_callback(const struct device * /*unused*/, uint32_t rate) { - if (rate == 1200) { - goto_dfu = true; - } -} void DeviceFirmwareUpdate::setup() { this->reset_pin_->setup(); - const struct device *cdc_dev[] = {DT_FOREACH_STATUS_OKAY(zephyr_cdc_acm_uart, DEVICE_AND_COMMA)}; - for (auto &idx : cdc_dev) { - cdc_acm_dte_rate_callback_set(idx, cdc_dte_rate_callback); - } -} - -void DeviceFirmwareUpdate::loop() { - if (goto_dfu) { - goto_dfu = false; - volatile uint32_t *dbl_reset_mem = (volatile uint32_t *) 0x20007F7C; - (*dbl_reset_mem) = DFU_DBL_RESET_MAGIC; - this->reset_pin_->digital_write(true); - } +#if defined(CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT) + zephyr::global_cdc_acm->add_on_rate_callback([this](const device *, uint32_t rate) { + if (rate == 1200) { + volatile uint32_t *dbl_reset_mem = (volatile uint32_t *) 0x20007F7C; + (*dbl_reset_mem) = DFU_DBL_RESET_MAGIC; + this->reset_pin_->digital_write(true); + } + }); +#endif } void DeviceFirmwareUpdate::dump_config() { diff --git a/esphome/components/nrf52/dfu.h b/esphome/components/nrf52/dfu.h index 979a4567cf..71060e43c1 100644 --- a/esphome/components/nrf52/dfu.h +++ b/esphome/components/nrf52/dfu.h @@ -10,7 +10,6 @@ namespace nrf52 { class DeviceFirmwareUpdate : public Component { public: void setup() override; - void loop() override; void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; } void dump_config() override; diff --git a/esphome/components/zephyr/__init__.py b/esphome/components/zephyr/__init__.py index 4cc71bddca..b8a091feb9 100644 --- a/esphome/components/zephyr/__init__.py +++ b/esphome/components/zephyr/__init__.py @@ -5,11 +5,13 @@ from typing import TypedDict import esphome.codegen as cg import esphome.config_validation as cv from esphome.const import CONF_BOARD, KEY_CORE, KEY_FRAMEWORK_VERSION -from esphome.core import CORE +from esphome.core import CORE, CoroPriority, coroutine_with_priority from esphome.helpers import copy_file_if_changed, write_file_if_changed +from esphome.types import ConfigType from .const import ( BOOTLOADER_MCUBOOT, + CONF_CDC_ACM, KEY_BOARD, KEY_BOOTLOADER, KEY_EXTRA_BUILD_FILES, @@ -54,7 +56,7 @@ class ZephyrData(TypedDict): user: dict[str, list[str]] -def zephyr_set_core_data(config): +def zephyr_set_core_data(config: ConfigType) -> None: CORE.data[KEY_ZEPHYR] = ZephyrData( board=config[CONF_BOARD], bootloader=config[KEY_BOOTLOADER], @@ -64,7 +66,6 @@ def zephyr_set_core_data(config): pm_static=[], user={}, ) - return config def zephyr_data() -> ZephyrData: @@ -110,7 +111,7 @@ def add_extra_script(stage: str, filename: str, path: Path) -> None: cg.add_platformio_option("extra_scripts", [key]) -def zephyr_to_code(config): +def zephyr_to_code(config: ConfigType) -> None: cg.add_build_flag("-DUSE_ZEPHYR") cg.add_define("USE_NATIVE_64BIT_TIME") cg.set_cpp_standard("gnu++20") @@ -132,6 +133,15 @@ def zephyr_to_code(config): Path(__file__).parent / "pre_build.py.script", ) + CORE.add_job(_cdc_acm_to_code, config) + + +@coroutine_with_priority(CoroPriority.FINAL) +async def _cdc_acm_to_code(config: ConfigType) -> None: + if "CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT" in zephyr_data()[KEY_PRJ_CONF]: + var = cg.new_Pvariable(config[CONF_CDC_ACM]) + await cg.register_component(var, {}) + def zephyr_setup_preferences(): cg.add(zephyr_ns.setup_preferences()) @@ -151,7 +161,7 @@ def _format_prj_conf_val(value: PrjConfValueType) -> str: raise ValueError -def zephyr_add_cdc_acm(config, id): +def zephyr_add_cdc_acm(config: ConfigType, id: int) -> None: framework_ver: cv.Version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] if CORE.is_nrf52 and framework_ver >= cv.Version(3, 2, 0): zephyr_add_prj_conf("CONFIG_USB_DEVICE_STACK_NEXT", False) diff --git a/esphome/components/zephyr/cdc_acm.cpp b/esphome/components/zephyr/cdc_acm.cpp new file mode 100644 index 0000000000..04ee9a0bef --- /dev/null +++ b/esphome/components/zephyr/cdc_acm.cpp @@ -0,0 +1,30 @@ +#if defined(CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT) +#include "cdc_acm.h" +#include +#include + +#define DEVICE_AND_COMMA(node_id) DEVICE_DT_GET(node_id), + +namespace esphome::zephyr { + +CdcAcm::CdcAcm() { global_cdc_acm = this; } + +void CdcAcm::setup() { +#if DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) + const struct device *cdc_dev[] = {DT_FOREACH_STATUS_OKAY(zephyr_cdc_acm_uart, DEVICE_AND_COMMA)}; + for (auto &idx : cdc_dev) { + // only one global callback can be registered + cdc_acm_dte_rate_callback_set(idx, CdcAcm::cdc_dte_rate_callback_); + } +#endif // DT_HAS_COMPAT_STATUS_OKAY(zephyr_cdc_acm_uart) +} + +void CdcAcm::cdc_dte_rate_callback_(const struct device *device, uint32_t rate) { + global_cdc_acm->defer([device, rate]() { global_cdc_acm->rate_callbacks_.call(device, rate); }); +} + +CdcAcm *global_cdc_acm; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome::zephyr + +#endif diff --git a/esphome/components/zephyr/cdc_acm.h b/esphome/components/zephyr/cdc_acm.h new file mode 100644 index 0000000000..2e9da85a11 --- /dev/null +++ b/esphome/components/zephyr/cdc_acm.h @@ -0,0 +1,27 @@ +#pragma once +#if defined(CONFIG_CDC_ACM_DTE_RATE_CALLBACK_SUPPORT) + +#include "esphome/core/component.h" +#include "esphome/core/helpers.h" +#include + +namespace esphome::zephyr { + +class CdcAcm : public Component { + public: + CdcAcm(); + void setup() override; + void add_on_rate_callback(std::function &&callback) { + this->rate_callbacks_.add(std::move(callback)); + } + + protected: + static void cdc_dte_rate_callback_(const device *device, uint32_t rate); + CallbackManager rate_callbacks_; +}; + +extern CdcAcm *global_cdc_acm; // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) + +} // namespace esphome::zephyr + +#endif diff --git a/esphome/components/zephyr/const.py b/esphome/components/zephyr/const.py index 06a4fc42bc..f67b058ed7 100644 --- a/esphome/components/zephyr/const.py +++ b/esphome/components/zephyr/const.py @@ -14,3 +14,5 @@ KEY_BOARD: Final = "board" KEY_USER: Final = "user" zephyr_ns = cg.esphome_ns.namespace("zephyr") +CdcAcm = zephyr_ns.class_("CdcAcm", cg.Component) +CONF_CDC_ACM = "cdc_acm" From 8b62c35ea7831d2c0e24f836e096b7b4c1cb1ca0 Mon Sep 17 00:00:00 2001 From: Simon Redman Date: Sat, 7 Mar 2026 11:41:37 -0500 Subject: [PATCH 19/26] [uart] Add error message when initializing UART with unsupported configuration (#13229) --- esphome/components/uart/uart_component_libretiny.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/uart/uart_component_libretiny.cpp b/esphome/components/uart/uart_component_libretiny.cpp index cb4465068d..83d2acb332 100644 --- a/esphome/components/uart/uart_component_libretiny.cpp +++ b/esphome/components/uart/uart_component_libretiny.cpp @@ -110,7 +110,7 @@ void LibreTinyUARTComponent::setup() { #if LT_HW_UART2 ESP_LOGE(TAG, " TX=%u, RX=%u", PIN_SERIAL2_TX, PIN_SERIAL2_RX); #endif - this->mark_failed(); + this->mark_failed(LOG_STR("SoftwareSerial is not implemented for this chip.")); return; #endif } From 15ffbb0b05da71f6ef008c0a38ffd185d0490edc Mon Sep 17 00:00:00 2001 From: puddly <32534428+puddly@users.noreply.github.com> Date: Sat, 7 Mar 2026 11:51:02 -0500 Subject: [PATCH 20/26] [uart] Fully enable raw mode with host serial (#14573) --- esphome/components/uart/uart_component_host.cpp | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/esphome/components/uart/uart_component_host.cpp b/esphome/components/uart/uart_component_host.cpp index 0e5ef3c6bd..9dce25c500 100644 --- a/esphome/components/uart/uart_component_host.cpp +++ b/esphome/components/uart/uart_component_host.cpp @@ -124,17 +124,10 @@ void HostUartComponent::setup() { fcntl(this->file_descriptor_, F_SETFL, 0); struct termios options; tcgetattr(this->file_descriptor_, &options); + cfmakeraw(&options); options.c_cflag &= ~CRTSCTS; options.c_cflag |= CREAD | CLOCAL; - options.c_lflag &= ~ICANON; - options.c_lflag &= ~ECHO; - options.c_lflag &= ~ECHOE; - options.c_lflag &= ~ECHONL; - options.c_lflag &= ~ISIG; - options.c_iflag &= ~(IXON | IXOFF | IXANY); - options.c_iflag &= ~(IGNBRK | BRKINT | PARMRK | ISTRIP | INLCR | IGNCR | ICRNL); - options.c_oflag &= ~OPOST; - options.c_oflag &= ~ONLCR; + options.c_iflag &= ~(IXOFF | IXANY); // Set data bits options.c_cflag &= ~CSIZE; // Mask the character size bits switch (this->data_bits_) { From abc870006cce42296a2f581478f79fbe69df2391 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Mar 2026 07:25:13 -1000 Subject: [PATCH 21/26] [captive_portal] Enable support for RP2040 (#14505) --- esphome/components/captive_portal/__init__.py | 9 ++++----- tests/components/captive_portal/test.rp2040-ard.yaml | 1 + 2 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 tests/components/captive_portal/test.rp2040-ard.yaml diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 6c190814c0..cd877fc879 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -13,6 +13,7 @@ from esphome.const import ( PLATFORM_ESP32, PLATFORM_ESP8266, PLATFORM_LN882X, + PLATFORM_RP2040, PLATFORM_RTL87XX, PlatformFramework, ) @@ -53,6 +54,7 @@ CONFIG_SCHEMA = cv.All( PLATFORM_ESP8266, PLATFORM_BK72XX, PLATFORM_LN882X, + PLATFORM_RP2040, PLATFORM_RTL87XX, ] ), @@ -103,11 +105,8 @@ async def to_code(config): if config[CONF_COMPRESSION] == "gzip": cg.add_define("USE_CAPTIVE_PORTAL_GZIP") - if CORE.using_arduino: - if CORE.is_esp8266: - cg.add_library("DNSServer", None) - if CORE.is_libretiny: - cg.add_library("DNSServer", None) + if CORE.using_arduino and (CORE.is_esp8266 or CORE.is_libretiny or CORE.is_rp2040): + cg.add_library("DNSServer", None) # Only compile the ESP-IDF DNS server when using ESP-IDF framework diff --git a/tests/components/captive_portal/test.rp2040-ard.yaml b/tests/components/captive_portal/test.rp2040-ard.yaml new file mode 100644 index 0000000000..dade44d145 --- /dev/null +++ b/tests/components/captive_portal/test.rp2040-ard.yaml @@ -0,0 +1 @@ +<<: !include common.yaml From f57fa4cc8d66a99f1e6bef885e2f0c8f96280c04 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Mar 2026 07:25:33 -1000 Subject: [PATCH 22/26] [bluetooth_proxy] Add BLE connection parameters API (#14577) --- esphome/components/api/api_connection.cpp | 3 ++ esphome/components/api/api_connection.h | 1 + .../bluetooth_proxy/bluetooth_connection.h | 4 +++ .../bluetooth_proxy/bluetooth_proxy.cpp | 29 +++++++++++++++++++ .../bluetooth_proxy/bluetooth_proxy.h | 3 ++ .../esp32_ble_client/ble_client_base.cpp | 5 ++-- .../esp32_ble_client/ble_client_base.h | 4 +-- 7 files changed, 45 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 8721072e49..bd3de02895 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1188,6 +1188,9 @@ void APIConnection::on_bluetooth_scanner_set_mode_request(const BluetoothScanner bluetooth_proxy::global_bluetooth_proxy->bluetooth_scanner_set_mode( msg.mode == enums::BluetoothScannerMode::BLUETOOTH_SCANNER_MODE_ACTIVE); } +void APIConnection::on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg) { + bluetooth_proxy::global_bluetooth_proxy->bluetooth_set_connection_params(msg); +} #endif #ifdef USE_VOICE_ASSISTANT diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 54b6db6800..b075bc83ab 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -148,6 +148,7 @@ class APIConnection final : public APIServerConnectionBase { void on_bluetooth_gatt_notify_request(const BluetoothGATTNotifyRequest &msg) override; void on_subscribe_bluetooth_connections_free_request() override; void on_bluetooth_scanner_set_mode_request(const BluetoothScannerSetModeRequest &msg) override; + void on_bluetooth_set_connection_params_request(const BluetoothSetConnectionParamsRequest &msg) override; #endif #ifdef USE_HOMEASSISTANT_TIME diff --git a/esphome/components/bluetooth_proxy/bluetooth_connection.h b/esphome/components/bluetooth_proxy/bluetooth_connection.h index 60bbc93e8b..b50ea2d6a2 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_connection.h +++ b/esphome/components/bluetooth_proxy/bluetooth_connection.h @@ -24,6 +24,10 @@ class BluetoothConnection final : public esp32_ble_client::BLEClientBase { esp_err_t notify_characteristic(uint16_t handle, bool enable); + esp_err_t update_connection_params(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout) { + return this->update_conn_params_(min_interval, max_interval, latency, timeout, "custom"); + } + void set_address(uint64_t address) override; protected: diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp index 21da4ead14..87206996b2 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.cpp @@ -3,7 +3,9 @@ #include "esphome/core/log.h" #include "esphome/core/macros.h" #include "esphome/core/application.h" +#include #include +#include #ifdef USE_ESP32 @@ -361,6 +363,33 @@ void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest } } +void BluetoothProxy::bluetooth_set_connection_params(const api::BluetoothSetConnectionParamsRequest &msg) { + if (this->api_connection_ == nullptr) + return; + + auto *connection = this->get_connection_(msg.address, false); + api::BluetoothSetConnectionParamsResponse resp; + resp.address = msg.address; + + if (connection == nullptr || !connection->connected()) { + ESP_LOGW(TAG, "[%d] [%s] Cannot set connection params, not connected", + connection ? static_cast(connection->connection_index_) : -1, + connection ? connection->address_str() : "unknown"); + resp.error = ESP_GATT_NOT_CONNECTED; + this->api_connection_->send_message(resp); + return; + } + + // Protobuf fields are uint32_t to future-proof the API if BLE ever supports wider values; + // clamp to uint16_t since the current BLE spec defines these as 16-bit. + constexpr uint32_t max_val = std::numeric_limits::max(); + resp.error = connection->update_connection_params(static_cast(std::min(msg.min_interval, max_val)), + static_cast(std::min(msg.max_interval, max_val)), + static_cast(std::min(msg.latency, max_val)), + static_cast(std::min(msg.timeout, max_val))); + this->api_connection_->send_message(resp); +} + void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags) { if (this->api_connection_ != nullptr) { ESP_LOGE(TAG, "Only one API subscription is allowed at a time"); diff --git a/esphome/components/bluetooth_proxy/bluetooth_proxy.h b/esphome/components/bluetooth_proxy/bluetooth_proxy.h index 85461755aa..f1b723e719 100644 --- a/esphome/components/bluetooth_proxy/bluetooth_proxy.h +++ b/esphome/components/bluetooth_proxy/bluetooth_proxy.h @@ -46,6 +46,7 @@ enum BluetoothProxyFeature : uint32_t { FEATURE_CACHE_CLEARING = 1 << 4, FEATURE_RAW_ADVERTISEMENTS = 1 << 5, FEATURE_STATE_AND_MODE = 1 << 6, + FEATURE_CONNECTION_PARAMS_SETTING = 1 << 7, }; enum BluetoothProxySubscriptionFlag : uint32_t { @@ -82,6 +83,7 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, void bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg); void bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg); void bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg); + void bluetooth_set_connection_params(const api::BluetoothSetConnectionParamsRequest &msg); void subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags); void unsubscribe_api_connection(api::APIConnection *api_connection); @@ -130,6 +132,7 @@ class BluetoothProxy final : public esp32_ble_tracker::ESPBTDeviceListener, flags |= BluetoothProxyFeature::FEATURE_REMOTE_CACHING; flags |= BluetoothProxyFeature::FEATURE_PAIRING; flags |= BluetoothProxyFeature::FEATURE_CACHE_CLEARING; + flags |= BluetoothProxyFeature::FEATURE_CONNECTION_PARAMS_SETTING; } return flags; diff --git a/esphome/components/esp32_ble_client/ble_client_base.cpp b/esphome/components/esp32_ble_client/ble_client_base.cpp index e6a85c784a..2f17334c77 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.cpp +++ b/esphome/components/esp32_ble_client/ble_client_base.cpp @@ -236,8 +236,8 @@ void BLEClientBase::log_warning_(const char *message) { ESP_LOGW(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_, message); } -void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, - uint16_t timeout, const char *param_type) { +esp_err_t BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, + uint16_t timeout, const char *param_type) { esp_ble_conn_update_params_t conn_params = {{0}}; memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t)); conn_params.min_int = min_interval; @@ -249,6 +249,7 @@ void BLEClientBase::update_conn_params_(uint16_t min_interval, uint16_t max_inte if (err != ESP_OK) { this->log_gattc_warning_("esp_ble_gap_update_conn_params", err); } + return err; } void BLEClientBase::set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, diff --git a/esphome/components/esp32_ble_client/ble_client_base.h b/esphome/components/esp32_ble_client/ble_client_base.h index c2336b2349..af4f1b3029 100644 --- a/esphome/components/esp32_ble_client/ble_client_base.h +++ b/esphome/components/esp32_ble_client/ble_client_base.h @@ -129,8 +129,8 @@ class BLEClientBase : public espbt::ESPBTClient, public Component { void log_event_(const char *name); void log_gattc_lifecycle_event_(const char *name); void log_gattc_data_event_(const char *name); - void update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, - const char *param_type); + esp_err_t update_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, + const char *param_type); void set_conn_params_(uint16_t min_interval, uint16_t max_interval, uint16_t latency, uint16_t timeout, const char *param_type); void log_gattc_warning_(const char *operation, esp_gatt_status_t status); From 45f20d9c06119e3c02aa8e8be131297c032da4e1 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Mar 2026 07:26:01 -1000 Subject: [PATCH 23/26] [core] Merge set_name + set_entity_strings into configure_entity_ (#14444) Co-authored-by: Claude Opus 4.6 --- esphome/core/entity_base.cpp | 15 ++- esphome/core/entity_base.h | 30 ++--- esphome/core/entity_helpers.py | 17 ++- .../binary_sensor/test_binary_sensor.py | 2 +- tests/component_tests/button/test_button.py | 2 +- tests/component_tests/sensor/test_sensor.py | 15 ++- tests/component_tests/text/test_text.py | 2 +- .../text_sensor/test_text_sensor.py | 25 ++++- tests/unit_tests/core/test_entity_helpers.py | 103 +++++++----------- 9 files changed, 112 insertions(+), 99 deletions(-) diff --git a/esphome/core/entity_base.cpp b/esphome/core/entity_base.cpp index 5c4e1c4445..3274640eb3 100644 --- a/esphome/core/entity_base.cpp +++ b/esphome/core/entity_base.cpp @@ -10,8 +10,8 @@ static const char *const TAG = "entity_base"; // Entity Name const StringRef &EntityBase::get_name() const { return this->name_; } -void EntityBase::set_name(const char *name) { this->set_name(name, 0); } -void EntityBase::set_name(const char *name, uint32_t object_id_hash) { + +void EntityBase::configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_strings_packed) { this->name_ = StringRef(name); if (this->name_.empty()) { #ifdef USE_DEVICES @@ -44,6 +44,17 @@ void EntityBase::set_name(const char *name, uint32_t object_id_hash) { this->calc_object_id_(); } } + // Unpack entity string table indices. + // Packed: [23..16] icon | [15..8] UoM | [7..0] device_class (each 8 bits) +#ifdef USE_ENTITY_DEVICE_CLASS + this->device_class_idx_ = entity_strings_packed & 0xFF; +#endif +#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT + this->uom_idx_ = (entity_strings_packed >> 8) & 0xFF; +#endif +#ifdef USE_ENTITY_ICON + this->icon_idx_ = (entity_strings_packed >> 16) & 0xFF; +#endif } // Weak default lookup functions — overridden by generated code in main.cpp diff --git a/esphome/core/entity_base.h b/esphome/core/entity_base.h index 20eb68b67a..accd532b0d 100644 --- a/esphome/core/entity_base.h +++ b/esphome/core/entity_base.h @@ -12,6 +12,10 @@ #include "device.h" #endif +// Forward declarations for friend access from codegen-generated setup() +void setup(); // NOLINT(readability-redundant-declaration) - may be declared in Arduino.h +void original_setup(); // NOLINT(readability-redundant-declaration) - used by cpp unit tests + namespace esphome { // Extern lookup functions for entity string tables. @@ -54,12 +58,8 @@ enum EntityCategory : uint8_t { // The generic Entity base class that provides an interface common to all Entities. class EntityBase { public: - // Get/set the name of this Entity + // Get the name of this Entity const StringRef &get_name() const; - void set_name(const char *name); - /// Set name with pre-computed object_id hash (avoids runtime hash calculation) - /// Use hash=0 for dynamic names that need runtime calculation - void set_name(const char *name, uint32_t object_id_hash); // Get whether this Entity has its own name or it should use the device friendly_name. bool has_own_name() const { return this->flags_.has_own_name; } @@ -104,20 +104,6 @@ class EntityBase { this->flags_.entity_category = static_cast(entity_category); } - // Set entity string table indices — one call per entity from codegen. - // Packed: [23..16] icon | [15..8] UoM | [7..0] device_class (each 8 bits) - void set_entity_strings([[maybe_unused]] uint32_t packed) { -#ifdef USE_ENTITY_DEVICE_CLASS - this->device_class_idx_ = packed & 0xFF; -#endif -#ifdef USE_ENTITY_UNIT_OF_MEASUREMENT - this->uom_idx_ = (packed >> 8) & 0xFF; -#endif -#ifdef USE_ENTITY_ICON - this->icon_idx_ = (packed >> 16) & 0xFF; -#endif - } - // Get this entity's device class into a stack buffer. // On non-ESP8266: returns pointer to PROGMEM string directly (buffer unused). // On ESP8266: copies from PROGMEM to buffer, returns buffer pointer. @@ -239,6 +225,12 @@ class EntityBase { } protected: + friend void ::setup(); + friend void ::original_setup(); + + /// Combined entity setup from codegen: set name, object_id hash, and entity string indices. + void configure_entity_(const char *name, uint32_t object_id_hash, uint32_t entity_strings_packed); + /// Non-template helper for make_entity_preference() to avoid code bloat. /// When preference hash algorithm changes, migration logic goes here. ESPPreferenceObject make_entity_preference_(size_t size, uint32_t version); diff --git a/esphome/core/entity_helpers.py b/esphome/core/entity_helpers.py index a46d2466fd..4fa109fb0e 100644 --- a/esphome/core/entity_helpers.py +++ b/esphome/core/entity_helpers.py @@ -31,8 +31,10 @@ DOMAIN = "entity_string_pool" _KEY_DC_IDX = "_entity_dc_idx" _KEY_UOM_IDX = "_entity_uom_idx" _KEY_ICON_IDX = "_entity_icon_idx" +_KEY_ENTITY_NAME = "_entity_name" +_KEY_OBJECT_ID_HASH = "_entity_object_id_hash" -# Bit layout for set_entity_strings(packed) — must match C++ setter in entity_base.h: +# Bit layout for entity_strings_packed in configure_entity_() — must match C++ in entity_base.h: # [23..16] icon (8 bits) | [15..8] UoM (8 bits) | [7..0] device_class (8 bits) _DC_SHIFT = 0 _UOM_SHIFT = 8 @@ -219,17 +221,18 @@ def setup_unit_of_measurement(config: ConfigType) -> None: def finalize_entity_strings(var: MockObj, config: ConfigType) -> None: - """Emit a single set_entity_strings() call with all packed indices. + """Emit a single configure_entity_() call with name, hash, and packed string indices. Call this at the end of each component's setup function, after setup_entity() and any register_device_class/register_unit_of_measurement calls. """ + entity_name = config[_KEY_ENTITY_NAME] + object_id_hash = config[_KEY_OBJECT_ID_HASH] dc_idx = config.get(_KEY_DC_IDX, 0) uom_idx = config.get(_KEY_UOM_IDX, 0) icon_idx = config.get(_KEY_ICON_IDX, 0) packed = (dc_idx << _DC_SHIFT) | (uom_idx << _UOM_SHIFT) | (icon_idx << _ICON_SHIFT) - if packed != 0: - add(var.set_entity_strings(packed)) + add(var.configure_entity_(entity_name, object_id_hash, packed)) def get_base_entity_object_id( @@ -331,13 +334,15 @@ async def _setup_entity_impl(var: MockObj, config: ConfigType, platform: str) -> device: MockObj = await get_variable(device_id_obj) add(var.set_device(device)) - # Set the entity name with pre-computed object_id hash + # Pre-compute entity name and object_id hash for configure_entity_() + # which is emitted later by finalize_entity_strings(). # For named entities: pre-compute hash from entity name # For empty-name entities: pass 0, C++ calculates hash at runtime from # device name, friendly_name, or app name (bug-for-bug compatibility) entity_name = config[CONF_NAME] object_id_hash = fnv1_hash_object_id(entity_name) if entity_name else 0 - add(var.set_name(entity_name, object_id_hash)) + config[_KEY_ENTITY_NAME] = entity_name + config[_KEY_OBJECT_ID_HASH] = object_id_hash # Only set disabled_by_default if True (default is False) if config[CONF_DISABLED_BY_DEFAULT]: add(var.set_disabled_by_default(True)) diff --git a/tests/component_tests/binary_sensor/test_binary_sensor.py b/tests/component_tests/binary_sensor/test_binary_sensor.py index ce4e64681f..fbc2f37d9a 100644 --- a/tests/component_tests/binary_sensor/test_binary_sensor.py +++ b/tests/component_tests/binary_sensor/test_binary_sensor.py @@ -29,7 +29,7 @@ def test_binary_sensor_sets_mandatory_fields(generate_main): ) # Then - assert 'bs_1->set_name("test bs1",' in main_cpp + assert 'bs_1->configure_entity_("test bs1",' in main_cpp assert "bs_1->set_pin(" in main_cpp diff --git a/tests/component_tests/button/test_button.py b/tests/component_tests/button/test_button.py index 797b6fb1a4..9f94d61c8c 100644 --- a/tests/component_tests/button/test_button.py +++ b/tests/component_tests/button/test_button.py @@ -26,7 +26,7 @@ def test_button_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/button/test_button.yaml") # Then - assert 'wol_1->set_name("wol_test_1",' in main_cpp + assert 'wol_1->configure_entity_("wol_test_1",' in main_cpp assert "wol_2->set_macaddr(18, 52, 86, 120, 144, 171);" in main_cpp diff --git a/tests/component_tests/sensor/test_sensor.py b/tests/component_tests/sensor/test_sensor.py index 221e7edf2c..d9ab3a022c 100644 --- a/tests/component_tests/sensor/test_sensor.py +++ b/tests/component_tests/sensor/test_sensor.py @@ -1,5 +1,15 @@ """Tests for the sensor component.""" +import re + + +def _extract_packed_value(main_cpp, var_name): + """Extract the third (packed) argument from a configure_entity_ call.""" + pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)" + match = re.search(pattern, main_cpp) + assert match, f"configure_entity_ call not found for {var_name}" + return int(match.group(1)) + def test_sensor_device_class_set(generate_main): """ @@ -10,5 +20,6 @@ def test_sensor_device_class_set(generate_main): # When main_cpp = generate_main("tests/component_tests/sensor/test_sensor.yaml") - # Then - assert "s_1->set_entity_strings(" in main_cpp + # Then: device_class: voltage means packed value must be non-zero + packed = _extract_packed_value(main_cpp, "s_1") + assert packed != 0 diff --git a/tests/component_tests/text/test_text.py b/tests/component_tests/text/test_text.py index 16f5f980a5..3ceaa9b8f8 100644 --- a/tests/component_tests/text/test_text.py +++ b/tests/component_tests/text/test_text.py @@ -25,7 +25,7 @@ def test_text_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/text/test_text.yaml") # Then - assert 'it_1->set_name("test 1 text",' in main_cpp + assert 'it_1->configure_entity_("test 1 text",' in main_cpp def test_text_config_value_internal_set(generate_main): diff --git a/tests/component_tests/text_sensor/test_text_sensor.py b/tests/component_tests/text_sensor/test_text_sensor.py index 4aaebe04d1..f30b820e94 100644 --- a/tests/component_tests/text_sensor/test_text_sensor.py +++ b/tests/component_tests/text_sensor/test_text_sensor.py @@ -1,5 +1,15 @@ """Tests for the text sensor component.""" +import re + + +def _extract_packed_value(main_cpp, var_name): + """Extract the third (packed) argument from a configure_entity_ call.""" + pattern = rf"{re.escape(var_name)}->configure_entity_\([^,]+,\s*\w+,\s*(\d+)\)" + match = re.search(pattern, main_cpp) + assert match, f"configure_entity_ call not found for {var_name}" + return int(match.group(1)) + def test_text_sensor_is_setup(generate_main): """ @@ -25,9 +35,9 @@ def test_text_sensor_sets_mandatory_fields(generate_main): main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") # Then - assert 'ts_1->set_name("Template Text Sensor 1",' in main_cpp - assert 'ts_2->set_name("Template Text Sensor 2",' in main_cpp - assert 'ts_3->set_name("Template Text Sensor 3",' in main_cpp + assert 'ts_1->configure_entity_("Template Text Sensor 1",' in main_cpp + assert 'ts_2->configure_entity_("Template Text Sensor 2",' in main_cpp + assert 'ts_3->configure_entity_("Template Text Sensor 3",' in main_cpp def test_text_sensor_config_value_internal_set(generate_main): @@ -53,6 +63,9 @@ def test_text_sensor_device_class_set(generate_main): # When main_cpp = generate_main("tests/component_tests/text_sensor/test_text_sensor.yaml") - # Then - assert "ts_2->set_entity_strings(" in main_cpp - assert "ts_3->set_entity_strings(" in main_cpp + # Then: ts_2 has device_class: timestamp, ts_3 has device_class: date + # so their packed values must be non-zero + packed_ts_2 = _extract_packed_value(main_cpp, "ts_2") + assert packed_ts_2 != 0 + packed_ts_3 = _extract_packed_value(main_cpp, "ts_3") + assert packed_ts_3 != 0 diff --git a/tests/unit_tests/core/test_entity_helpers.py b/tests/unit_tests/core/test_entity_helpers.py index 1392a1d043..3f6faaee54 100644 --- a/tests/unit_tests/core/test_entity_helpers.py +++ b/tests/unit_tests/core/test_entity_helpers.py @@ -32,9 +32,11 @@ from esphome.helpers import sanitize, snake_case from .common import load_config_from_fixture -# Pre-compiled regex pattern for extracting names from set_name calls -# Matches: .set_name("name", hash) or .set_name("name") -SET_NAME_PATTERN = re.compile(r'\.set_name\(["\']([^"\']*)["\']') +# Pre-compiled regex pattern for extracting names from configure_entity_/set_name calls +# Matches: .configure_entity_("name", ...) or .set_name("name", ...) +ENTITY_NAME_PATTERN = re.compile( + r'\.(?:configure_entity_|set_name)\(["\']([^"\']*)["\']' +) FIXTURES_DIR = Path(__file__).parent.parent / "fixtures" / "core" / "entity_helpers" @@ -276,15 +278,23 @@ def setup_test_environment() -> Generator[list[str], None, None]: entity_helpers.add = original_add -def extract_object_id_from_expressions(expressions: list[str]) -> str | None: - """Extract the object ID that would be computed from set_name calls. +def extract_object_id_from_config(config: dict[str, Any]) -> str | None: + """Extract the object ID from config keys set by _setup_entity_impl.""" + name = config.get("_entity_name") + if name is None: + return None + if name: + return sanitize(snake_case(name)) + # Empty name - fall back to friendly_name or device name + if CORE.friendly_name: + return sanitize(snake_case(CORE.friendly_name)) + return sanitize(snake_case(CORE.name)) if CORE.name else None - Since object_id is now computed from the name (via snake_case + sanitize), - we extract the name from set_name() calls and compute the expected object_id. - For empty names, we fall back to CORE.friendly_name or CORE.name. - """ + +def extract_object_id_from_expressions(expressions: list[str]) -> str | None: + """Extract the object ID from configure_entity_() calls in generated expressions.""" for expr in expressions: - if match := SET_NAME_PATTERN.search(expr): + if match := ENTITY_NAME_PATTERN.search(expr): name = match.group(1) if name: return sanitize(snake_case(name)) @@ -299,8 +309,6 @@ def extract_object_id_from_expressions(expressions: list[str]) -> str | None: async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> None: """Test setup_entity with unique names.""" - added_expressions = setup_test_environment - # Create mock entities var1 = MockObj("sensor1") var2 = MockObj("sensor2") @@ -312,13 +320,10 @@ async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> } await _setup_entity_impl(var1, config1, "sensor") - # Get object ID from first entity - object_id1 = extract_object_id_from_expressions(added_expressions) + # Get object ID from first entity (stored in config, emitted later by finalize) + object_id1 = extract_object_id_from_config(config1) assert object_id1 == "temperature" - # Clear for next entity - added_expressions.clear() - # Set up second entity with different name config2 = { CONF_NAME: "Humidity", @@ -327,7 +332,7 @@ async def test_setup_entity_no_duplicates(setup_test_environment: list[str]) -> await _setup_entity_impl(var2, config2, "sensor") # Get object ID from second entity - object_id2 = extract_object_id_from_expressions(added_expressions) + object_id2 = extract_object_id_from_config(config2) assert object_id2 == "humidity" @@ -337,8 +342,6 @@ async def test_setup_entity_different_platforms( ) -> None: """Test that same name on different platforms doesn't conflict.""" - added_expressions = setup_test_environment - # Create mock entities sensor = MockObj("sensor1") binary_sensor = MockObj("binary_sensor1") @@ -356,15 +359,11 @@ async def test_setup_entity_different_platforms( (text_sensor, "text_sensor"), ] - object_ids: list[str] = [] for var, platform in platforms: - added_expressions.clear() await _setup_entity_impl(var, config, platform) - object_id = extract_object_id_from_expressions(added_expressions) - object_ids.append(object_id) - # All should get base object ID without suffix - assert all(obj_id == "status" for obj_id in object_ids) + # All should get the same object ID (name stored in config, not platform-specific) + assert extract_object_id_from_config(config) == "status" @pytest.fixture @@ -389,7 +388,6 @@ async def test_setup_entity_with_devices( setup_test_environment: list[str], mock_get_variable: dict[ID, MockObj] ) -> None: """Test that same name on different devices doesn't conflict.""" - added_expressions = setup_test_environment # Create mock devices device1_id = ID("device1", type="Device") @@ -418,24 +416,18 @@ async def test_setup_entity_with_devices( } # Get object IDs - object_ids: list[str] = [] for var, config in [(sensor1, config1), (sensor2, config2)]: - added_expressions.clear() await _setup_entity_impl(var, config, "sensor") - object_id = extract_object_id_from_expressions(added_expressions) - object_ids.append(object_id) # Both should get base object ID without suffix (different devices) - assert object_ids[0] == "temperature" - assert object_ids[1] == "temperature" + assert extract_object_id_from_config(config1) == "temperature" + assert extract_object_id_from_config(config2) == "temperature" @pytest.mark.asyncio async def test_setup_entity_empty_name(setup_test_environment: list[str]) -> None: """Test setup_entity with empty entity name.""" - added_expressions = setup_test_environment - var = MockObj("sensor1") config = { @@ -445,7 +437,7 @@ async def test_setup_entity_empty_name(setup_test_environment: list[str]) -> Non await _setup_entity_impl(var, config, "sensor") - object_id = extract_object_id_from_expressions(added_expressions) + object_id = extract_object_id_from_config(config) # Should use friendly name assert object_id == "test_device" @@ -456,8 +448,6 @@ async def test_setup_entity_special_characters( ) -> None: """Test setup_entity with names containing special characters.""" - added_expressions = setup_test_environment - var = MockObj("sensor1") config = { @@ -466,7 +456,7 @@ async def test_setup_entity_special_characters( } await _setup_entity_impl(var, config, "sensor") - object_id = extract_object_id_from_expressions(added_expressions) + object_id = extract_object_id_from_config(config) # Special characters should be sanitized assert object_id == "temperature_sensor_" @@ -476,8 +466,6 @@ async def test_setup_entity_special_characters( async def test_setup_entity_with_icon(setup_test_environment: list[str]) -> None: """Test setup_entity sets icon correctly.""" - setup_test_environment # noqa: F841 - fixture initializes CORE state - var = MockObj("sensor1") config = { @@ -800,10 +788,9 @@ async def test_setup_entity_empty_name_with_device( # Check that set_device was called assert any("sensor1.set_device" in expr for expr in added_expressions) - # For empty-name entities, Python passes 0 - C++ calculates hash at runtime - assert any('set_name("", 0)' in expr for expr in added_expressions), ( - f"Expected set_name with hash 0, got {added_expressions}" - ) + # For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime + assert config.get("_entity_name") == "" + assert config.get("_entity_object_id_hash") == 0 @pytest.mark.asyncio @@ -815,7 +802,6 @@ async def test_setup_entity_empty_name_with_mac_suffix( For empty-name entities, Python passes 0 and C++ calculates the hash at runtime from friendly_name (bug-for-bug compatibility). """ - added_expressions = setup_test_environment # Set up CORE.config with name_add_mac_suffix enabled CORE.config = {"name_add_mac_suffix": True} @@ -831,10 +817,9 @@ async def test_setup_entity_empty_name_with_mac_suffix( await _setup_entity_impl(var, config, "sensor") - # For empty-name entities, Python passes 0 - C++ calculates hash at runtime - assert any('set_name("", 0)' in expr for expr in added_expressions), ( - f"Expected set_name with hash 0, got {added_expressions}" - ) + # For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime + assert config.get("_entity_name") == "" + assert config.get("_entity_object_id_hash") == 0 @pytest.mark.asyncio @@ -847,7 +832,6 @@ async def test_setup_entity_empty_name_with_mac_suffix_no_friendly_name( at runtime. In this case C++ will hash the empty friendly_name (bug-for-bug compatibility). """ - added_expressions = setup_test_environment # Set up CORE.config with name_add_mac_suffix enabled CORE.config = {"name_add_mac_suffix": True} @@ -863,10 +847,9 @@ async def test_setup_entity_empty_name_with_mac_suffix_no_friendly_name( await _setup_entity_impl(var, config, "sensor") - # For empty-name entities, Python passes 0 - C++ calculates hash at runtime - assert any('set_name("", 0)' in expr for expr in added_expressions), ( - f"Expected set_name with hash 0, got {added_expressions}" - ) + # For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime + assert config.get("_entity_name") == "" + assert config.get("_entity_object_id_hash") == 0 @pytest.mark.asyncio @@ -878,7 +861,6 @@ async def test_setup_entity_empty_name_no_mac_suffix_no_friendly_name( For empty-name entities, Python passes 0 and C++ calculates the hash at runtime from the device name. """ - added_expressions = setup_test_environment # No MAC suffix (either not set or False) CORE.config = {} @@ -896,10 +878,9 @@ async def test_setup_entity_empty_name_no_mac_suffix_no_friendly_name( await _setup_entity_impl(var, config, "sensor") - # For empty-name entities, Python passes 0 - C++ calculates hash at runtime - assert any('set_name("", 0)' in expr for expr in added_expressions), ( - f"Expected set_name with hash 0, got {added_expressions}" - ) + # For empty-name entities, Python stores hash 0 - C++ calculates hash at runtime + assert config.get("_entity_name") == "" + assert config.get("_entity_object_id_hash") == 0 def test_register_string_overflow() -> None: @@ -976,7 +957,7 @@ async def test_setup_entity_direct_call(setup_test_environment: list[str]) -> No # Direct call mode: await setup_entity(var, config, "camera") await setup_entity(var, config, "camera") - # Should have called set_name + # Should have emitted configure_entity_ object_id = extract_object_id_from_expressions(added_expressions) assert object_id == "my_camera" From 77f2c371b2b20b00d6e064bd5cbc618b2f1e22c8 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Mar 2026 07:26:34 -1000 Subject: [PATCH 24/26] [api] Single-pass protobuf encode for BLE proxy advertisements (#14575) --- esphome/components/api/api_pb2.cpp | 28 ++++++------ esphome/components/api/proto.cpp | 71 +++++++++++++++++++++++++++++ esphome/components/api/proto.h | 44 ++++++++---------- script/api_protobuf/api_protobuf.py | 17 +++---- 4 files changed, 111 insertions(+), 49 deletions(-) diff --git a/esphome/components/api/api_pb2.cpp b/esphome/components/api/api_pb2.cpp index b1176de539..1944aac6e8 100644 --- a/esphome/components/api/api_pb2.cpp +++ b/esphome/components/api/api_pb2.cpp @@ -120,16 +120,16 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer &buffer) const { #endif #ifdef USE_DEVICES for (const auto &it : this->devices) { - buffer.encode_message(20, it); + buffer.encode_sub_message(20, it); } #endif #ifdef USE_AREAS for (const auto &it : this->areas) { - buffer.encode_message(21, it); + buffer.encode_sub_message(21, it); } #endif #ifdef USE_AREAS - buffer.encode_message(22, this->area, false); + buffer.encode_optional_sub_message(22, this->area); #endif #ifdef USE_ZWAVE_PROXY buffer.encode_uint32(23, this->zwave_proxy_feature_flags); @@ -920,13 +920,13 @@ uint32_t HomeassistantServiceMap::calculate_size() const { void HomeassistantActionRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->service); for (auto &it : this->data) { - buffer.encode_message(2, it); + buffer.encode_sub_message(2, it); } for (auto &it : this->data_template) { - buffer.encode_message(3, it); + buffer.encode_sub_message(3, it); } for (auto &it : this->variables) { - buffer.encode_message(4, it); + buffer.encode_sub_message(4, it); } buffer.encode_bool(5, this->is_event); #ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES @@ -1126,7 +1126,7 @@ void ListEntitiesServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_string(1, this->name); buffer.encode_fixed32(2, this->key); for (auto &it : this->args) { - buffer.encode_message(3, it); + buffer.encode_sub_message(3, it); } buffer.encode_uint32(4, static_cast(this->supports_response)); } @@ -2133,7 +2133,7 @@ void ListEntitiesMediaPlayerResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(7, static_cast(this->entity_category)); buffer.encode_bool(8, this->supports_pause); for (auto &it : this->supported_formats) { - buffer.encode_message(9, it); + buffer.encode_sub_message(9, it); } #ifdef USE_DEVICES buffer.encode_uint32(10, this->device_id); @@ -2264,7 +2264,7 @@ uint32_t BluetoothLERawAdvertisement::calculate_size() const { } void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer &buffer) const { for (uint16_t i = 0; i < this->advertisements_len; i++) { - buffer.encode_message(1, this->advertisements[i]); + buffer.encode_sub_message(1, this->advertisements[i]); } } uint32_t BluetoothLERawAdvertisementsResponse::calculate_size() const { @@ -2343,7 +2343,7 @@ void BluetoothGATTCharacteristic::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint32(2, this->handle); buffer.encode_uint32(3, this->properties); for (auto &it : this->descriptors) { - buffer.encode_message(4, it); + buffer.encode_sub_message(4, it); } buffer.encode_uint32(5, this->short_uuid); } @@ -2370,7 +2370,7 @@ void BluetoothGATTService::encode(ProtoWriteBuffer &buffer) const { } buffer.encode_uint32(2, this->handle); for (auto &it : this->characteristics) { - buffer.encode_message(3, it); + buffer.encode_sub_message(3, it); } buffer.encode_uint32(4, this->short_uuid); } @@ -2392,7 +2392,7 @@ uint32_t BluetoothGATTService::calculate_size() const { void BluetoothGATTGetServicesResponse::encode(ProtoWriteBuffer &buffer) const { buffer.encode_uint64(1, this->address); for (auto &it : this->services) { - buffer.encode_message(2, it); + buffer.encode_sub_message(2, it); } } uint32_t BluetoothGATTGetServicesResponse::calculate_size() const { @@ -2673,7 +2673,7 @@ void VoiceAssistantRequest::encode(ProtoWriteBuffer &buffer) const { buffer.encode_bool(1, this->start); buffer.encode_string(2, this->conversation_id); buffer.encode_uint32(3, this->flags); - buffer.encode_message(4, this->audio_settings, false); + buffer.encode_optional_sub_message(4, this->audio_settings); buffer.encode_string(5, this->wake_word_phrase); } uint32_t VoiceAssistantRequest::calculate_size() const { @@ -2906,7 +2906,7 @@ bool VoiceAssistantConfigurationRequest::decode_length(uint32_t field_id, ProtoL } void VoiceAssistantConfigurationResponse::encode(ProtoWriteBuffer &buffer) const { for (auto &it : this->available_wake_words) { - buffer.encode_message(1, it); + buffer.encode_sub_message(1, it); } for (const auto &it : *this->active_wake_words) { buffer.encode_string(2, it, true); diff --git a/esphome/components/api/proto.cpp b/esphome/components/api/proto.cpp index a252907fd7..1ca6b702ad 100644 --- a/esphome/components/api/proto.cpp +++ b/esphome/components/api/proto.cpp @@ -1,5 +1,6 @@ #include "proto.h" #include +#include #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -87,6 +88,76 @@ uint32_t ProtoDecodableMessage::count_repeated_field(const uint8_t *buffer, size return count; } +// Single-pass encode for repeated submessage elements (non-template core). +// Writes field tag, reserves 1 byte for length varint, encodes the submessage body, +// then backpatches the actual length. For the common case (body < 128 bytes), this is +// just a single byte write with no memmove — all current repeated submessage types +// (BLE advertisements at ~47B, GATT descriptors at ~24B, service args, etc.) take +// this fast path. +// +// The memmove fallback for body >= 128 bytes exists only for correctness (e.g., a GATT +// characteristic with many descriptors). It is safe because calculate_size() already +// reserved space for the full multi-byte varint — the shift fills that reserved space: +// +// calculate_size() allocates per element: tag + varint_size(body) + body_size +// +// After encode, before memmove (1 byte reserved, body written): +// [tag][__][body ..... body][??] +// ^ ^-- unused byte (v2 space from calculate_size) +// len_pos +// +// After memmove(body_start+1, body_start, body_size): +// [tag][__][__][body ..... body] +// ^ ^-- body shifted forward, fills v2 space exactly +// len_pos +// +// After writing 2-byte varint at len_pos: +// [tag][v1][v2][body ..... body] +// ^-- pos_ = element end, within buffer +void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const void *value, + void (*encode_fn)(const void *, ProtoWriteBuffer &)) { + this->encode_field_raw(field_id, 2); + // Reserve 1 byte for length varint (optimistic: submessage < 128 bytes) + uint8_t *len_pos = this->pos_; + this->debug_check_bounds_(1); + this->pos_++; + uint8_t *body_start = this->pos_; + encode_fn(value, *this); + uint32_t body_size = static_cast(this->pos_ - body_start); + if (body_size < 128) [[likely]] { + // Common case: 1-byte varint, just backpatch + *len_pos = static_cast(body_size); + return; + } + // Compute extra bytes needed for varint beyond the 1 already reserved + uint8_t extra = ProtoSize::varint(body_size) - 1; + // Shift body forward to make room for the extra varint bytes + this->debug_check_bounds_(extra); + std::memmove(body_start + extra, body_start, body_size); + uint8_t *end = this->pos_ + extra; + // Write the full varint at len_pos + this->pos_ = len_pos; + this->encode_varint_raw(body_size); + this->pos_ = end; +} + +// Non-template core for encode_optional_sub_message. +void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value, + void (*encode_fn)(const void *, ProtoWriteBuffer &)) { + if (nested_size == 0) + return; + this->encode_field_raw(field_id, 2); + this->encode_varint_raw(nested_size); +#ifdef ESPHOME_DEBUG_API + uint8_t *start = this->pos_; + encode_fn(value, *this); + if (static_cast(this->pos_ - start) != nested_size) + this->debug_check_encode_size_(field_id, nested_size, this->pos_ - start); +#else + encode_fn(value, *this); +#endif +} + #ifdef ESPHOME_DEBUG_API void ProtoWriteBuffer::debug_check_bounds_(size_t bytes, const char *caller) { if (this->pos_ + bytes > this->buffer_->data() + this->buffer_->size()) { diff --git a/esphome/components/api/proto.h b/esphome/components/api/proto.h index 702208d9de..bbdd11b29d 100644 --- a/esphome/components/api/proto.h +++ b/esphome/components/api/proto.h @@ -185,7 +185,7 @@ class ProtoVarInt { #endif }; -// Forward declarations for decode_to_message, encode_message and encode_packed_sint32 +// Forward declarations for decode_to_message and related encoding helpers class ProtoDecodableMessage; class ProtoMessage; class ProtoSize; @@ -363,12 +363,18 @@ class ProtoWriteBuffer { } /// Encode a packed repeated sint32 field (zero-copy from vector) void encode_packed_sint32(uint32_t field_id, const std::vector &values); - /// Encode a nested message field (force=true for repeated, false for singular) - /// Templated so concrete message type is preserved for direct encode/calculate_size calls. - template void encode_message(uint32_t field_id, const T &value, bool force = true); - // Non-template core for encode_message — all buffer work happens here - void encode_message(uint32_t field_id, uint32_t msg_length_bytes, const void *value, - void (*encode_fn)(const void *, ProtoWriteBuffer &), bool force); + /// Single-pass encode for repeated submessage elements. + /// Thin template wrapper; all buffer work is in the non-template core. + template void encode_sub_message(uint32_t field_id, const T &value); + /// Encode an optional singular submessage field — skips if empty. + /// Thin template wrapper; all buffer work is in the non-template core. + template void encode_optional_sub_message(uint32_t field_id, const T &value); + + // Non-template core for encode_sub_message — backpatch approach. + void encode_sub_message(uint32_t field_id, const void *value, void (*encode_fn)(const void *, ProtoWriteBuffer &)); + // Non-template core for encode_optional_sub_message. + void encode_optional_sub_message(uint32_t field_id, uint32_t nested_size, const void *value, + void (*encode_fn)(const void *, ProtoWriteBuffer &)); std::vector *get_buffer() const { return buffer_; } protected: @@ -690,26 +696,14 @@ template void proto_encode_msg(const void *msg, ProtoWriteBuffer &bu static_cast(msg)->encode(buf); } -// Implementation of encode_message - must be after ProtoMessage is defined -template inline void ProtoWriteBuffer::encode_message(uint32_t field_id, const T &value, bool force) { - this->encode_message(field_id, value.calculate_size(), &value, &proto_encode_msg, force); +// Thin template wrapper; delegates to non-template core in proto.cpp. +template inline void ProtoWriteBuffer::encode_sub_message(uint32_t field_id, const T &value) { + this->encode_sub_message(field_id, &value, &proto_encode_msg); } -// Non-template core for encode_message -inline void ProtoWriteBuffer::encode_message(uint32_t field_id, uint32_t msg_length_bytes, const void *value, - void (*encode_fn)(const void *, ProtoWriteBuffer &), bool force) { - if (msg_length_bytes == 0 && !force) - return; - this->encode_field_raw(field_id, 2); - this->encode_varint_raw(msg_length_bytes); -#ifdef ESPHOME_DEBUG_API - uint8_t *start = this->pos_; - encode_fn(value, *this); - if (static_cast(this->pos_ - start) != msg_length_bytes) - this->debug_check_encode_size_(field_id, msg_length_bytes, this->pos_ - start); -#else - encode_fn(value, *this); -#endif +// Thin template wrapper; delegates to non-template core. +template inline void ProtoWriteBuffer::encode_optional_sub_message(uint32_t field_id, const T &value) { + this->encode_optional_sub_message(field_id, value.calculate_size(), &value, &proto_encode_msg); } // Implementation of decode_to_message - must be after ProtoDecodableMessage is defined diff --git a/script/api_protobuf/api_protobuf.py b/script/api_protobuf/api_protobuf.py index 85352689e6..7ae7063a41 100755 --- a/script/api_protobuf/api_protobuf.py +++ b/script/api_protobuf/api_protobuf.py @@ -690,15 +690,12 @@ class MessageType(TypeInfo): @property def encode_func(self) -> str: - return "encode_message" + return "encode_optional_sub_message" @property def encode_content(self) -> str: - # Singular message fields pass force=false (skip empty messages) - # The default for encode_nested_message is force=true (for repeated fields) - return ( - f"buffer.{self.encode_func}({self.number}, this->{self.field_name}, false);" - ) + # Singular message fields skip encoding when empty + return f"buffer.{self.encode_func}({self.number}, this->{self.field_name});" @property def decode_length(self) -> str: @@ -1322,9 +1319,9 @@ class FixedArrayRepeatedType(TypeInfo): """Helper to generate encode statement for a single element.""" if isinstance(self._ti, EnumType): return f"buffer.{self._ti.encode_func}({self.number}, static_cast({element}), true);" - # MessageType.encode_message doesn't have a force parameter + # Repeated message elements use encode_sub_message (force=true is default) if isinstance(self._ti, MessageType): - return f"buffer.{self._ti.encode_func}({self.number}, {element});" + return f"buffer.encode_sub_message({self.number}, {element});" return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" @property @@ -1650,9 +1647,9 @@ class RepeatedTypeInfo(TypeInfo): """Helper to generate encode call for a single element.""" if isinstance(self._ti, EnumType): return f"buffer.{self._ti.encode_func}({self.number}, static_cast({element}), true);" - # MessageType.encode_message doesn't have a force parameter + # Repeated message elements use encode_sub_message (force=true is default) if isinstance(self._ti, MessageType): - return f"buffer.{self._ti.encode_func}({self.number}, {element});" + return f"buffer.encode_sub_message({self.number}, {element});" return f"buffer.{self._ti.encode_func}({self.number}, {element}, true);" @property From e7b8ec18f17b23709719c63e1d05a6b9ecfb0054 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Mar 2026 07:26:50 -1000 Subject: [PATCH 25/26] [api] Inline APIServer::is_connected() for common no-arg path (#14574) --- esphome/components/api/api_server.cpp | 6 +----- esphome/components/api/api_server.h | 8 ++++++-- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index 06816fe3e0..17d69405ad 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -582,11 +582,7 @@ void APIServer::request_time() { } #endif -bool APIServer::is_connected(bool state_subscription_only) const { - if (!state_subscription_only) { - return !this->clients_.empty(); - } - +bool APIServer::is_connected_with_state_subscription() const { for (const auto &client : this->clients_) { if (client->flags_.state_subscription) { return true; diff --git a/esphome/components/api/api_server.h b/esphome/components/api/api_server.h index e6c10d1595..e5f371d8a1 100644 --- a/esphome/components/api/api_server.h +++ b/esphome/components/api/api_server.h @@ -185,7 +185,8 @@ class APIServer : public Component, void send_infrared_rf_receive_event(uint32_t device_id, uint32_t key, const std::vector *timings); #endif - bool is_connected(bool state_subscription_only = false) const; + bool is_connected() const { return !this->clients_.empty(); } + bool is_connected_with_state_subscription() const; #ifdef USE_API_HOMEASSISTANT_STATES struct HomeAssistantStateSubscription { @@ -323,7 +324,10 @@ template class APIConnectedCondition : public Condition { TEMPLATABLE_VALUE(bool, state_subscription_only) public: bool check(const Ts &...x) override { - return global_api_server->is_connected(this->state_subscription_only_.value(x...)); + if (this->state_subscription_only_.value(x...)) { + return global_api_server->is_connected_with_state_subscription(); + } + return global_api_server->is_connected(); } }; From a0cd35c5fc2b8b1e9ab0db69fd70b90c95e8dbad Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 7 Mar 2026 07:27:08 -1000 Subject: [PATCH 26/26] [core] Inline status_clear_warning/error fast path (#14571) --- esphome/core/component.cpp | 8 ++------ esphome/core/component.h | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 8 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index a9ff3ec1eb..2879d4b5ab 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -422,15 +422,11 @@ void Component::status_set_error(const LogString *message) { store_component_error_message(this, LOG_STR_ARG(message), true); } } -void Component::status_clear_warning() { - if ((this->component_state_ & STATUS_LED_WARNING) == 0) - return; +void Component::status_clear_warning_slow_path_() { this->component_state_ &= ~STATUS_LED_WARNING; ESP_LOGW(TAG, "%s cleared Warning flag", LOG_STR_ARG(this->get_component_log_str())); } -void Component::status_clear_error() { - if ((this->component_state_ & STATUS_LED_ERROR) == 0) - return; +void Component::status_clear_error_slow_path_() { this->component_state_ &= ~STATUS_LED_ERROR; ESP_LOGE(TAG, "%s cleared Error flag", LOG_STR_ARG(this->get_component_log_str())); } diff --git a/esphome/core/component.h b/esphome/core/component.h index 59222dc4f4..7266f57e15 100644 --- a/esphome/core/component.h +++ b/esphome/core/component.h @@ -251,9 +251,17 @@ class Component { void status_set_error(const char *message); void status_set_error(const LogString *message); - void status_clear_warning(); + void status_clear_warning() { + if ((this->component_state_ & STATUS_LED_WARNING) == 0) + return; + this->status_clear_warning_slow_path_(); + } - void status_clear_error(); + void status_clear_error() { + if ((this->component_state_ & STATUS_LED_ERROR) == 0) + return; + this->status_clear_error_slow_path_(); + } /** Set warning status flag and automatically clear it after a timeout. * @@ -505,6 +513,9 @@ class Component { bool cancel_defer(const char *name); // NOLINT bool cancel_defer(uint32_t id); // NOLINT + void status_clear_warning_slow_path_(); + void status_clear_error_slow_path_(); + // Ordered for optimal packing on 32-bit systems const LogString *component_source_{nullptr}; uint16_t warn_if_blocking_over_{WARN_IF_BLOCKING_OVER_MS}; ///< Warn if blocked for this many ms (max 65.5s)