From 097aac2183641c087f161b2b34648d26fbc497d2 Mon Sep 17 00:00:00 2001 From: Keith Burzinski Date: Fri, 11 Jul 2025 22:33:36 -0500 Subject: [PATCH 01/12] [ld2420] Memory optimization, code clean-up (#9426) --- .../binary_sensor/ld2420_binary_sensor.cpp | 4 +- .../ld2420/button/reconfig_buttons.cpp | 2 +- esphome/components/ld2420/ld2420.cpp | 123 ++++++++++-------- esphome/components/ld2420/ld2420.h | 20 ++- .../ld2420/number/gate_config_number.cpp | 2 +- .../ld2420/select/operating_mode_select.cpp | 2 +- .../ld2420/sensor/ld2420_sensor.cpp | 4 +- .../ld2420/text_sensor/text_sensor.cpp | 4 +- 8 files changed, 84 insertions(+), 77 deletions(-) diff --git a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp index c6ea0a348b..d8632e9c19 100644 --- a/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp +++ b/esphome/components/ld2420/binary_sensor/ld2420_binary_sensor.cpp @@ -5,10 +5,10 @@ namespace esphome { namespace ld2420 { -static const char *const TAG = "LD2420.binary_sensor"; +static const char *const TAG = "ld2420.binary_sensor"; void LD2420BinarySensor::dump_config() { - ESP_LOGCONFIG(TAG, "LD2420 BinarySensor:"); + ESP_LOGCONFIG(TAG, "Binary Sensor:"); LOG_BINARY_SENSOR(" ", "Presence", this->presence_bsensor_); } diff --git a/esphome/components/ld2420/button/reconfig_buttons.cpp b/esphome/components/ld2420/button/reconfig_buttons.cpp index 3537c1d64a..fb8ec2b5a6 100644 --- a/esphome/components/ld2420/button/reconfig_buttons.cpp +++ b/esphome/components/ld2420/button/reconfig_buttons.cpp @@ -2,7 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -static const char *const TAG = "LD2420.button"; +static const char *const TAG = "ld2420.button"; namespace esphome { namespace ld2420 { diff --git a/esphome/components/ld2420/ld2420.cpp b/esphome/components/ld2420/ld2420.cpp index 8a7d7de23b..0baff368c8 100644 --- a/esphome/components/ld2420/ld2420.cpp +++ b/esphome/components/ld2420/ld2420.cpp @@ -137,7 +137,7 @@ static const std::string OP_SIMPLE_MODE_STRING = "Simple"; // Memory-efficient lookup tables struct StringToUint8 { const char *str; - uint8_t value; + const uint8_t value; }; static constexpr StringToUint8 OP_MODE_BY_STR[] = { @@ -155,8 +155,9 @@ static constexpr const char *ERR_MESSAGE[] = { // Helper function for lookups template uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { for (const auto &entry : arr) { - if (str == entry.str) + if (str == entry.str) { return entry.value; + } } return 0xFF; // Not found } @@ -326,15 +327,8 @@ void LD2420Component::revert_config_action() { void LD2420Component::loop() { // If there is a active send command do not process it here, the send command call will handle it. - if (!this->get_cmd_active_()) { - if (!this->available()) - return; - static uint8_t buffer[2048]; - static uint8_t rx_data; - while (this->available()) { - rx_data = this->read(); - this->readline_(rx_data, buffer, sizeof(buffer)); - } + while (!this->cmd_active_ && this->available()) { + this->readline_(this->read(), this->buffer_data_, MAX_LINE_LENGTH); } } @@ -365,8 +359,9 @@ void LD2420Component::auto_calibrate_sensitivity() { // Store average and peak values this->gate_avg[gate] = sum / CALIBRATE_SAMPLES; - if (this->gate_peak[gate] < peak) + if (this->gate_peak[gate] < peak) { this->gate_peak[gate] = peak; + } uint32_t calculated_value = (static_cast(this->gate_peak[gate]) + (move_factor * static_cast(this->gate_peak[gate]))); @@ -403,8 +398,9 @@ void LD2420Component::set_operating_mode(const std::string &state) { } } else { // Set the current data back so we don't have new data that can be applied in error. - if (this->get_calibration_()) + if (this->get_calibration_()) { memcpy(&this->new_config, &this->current_config, sizeof(this->current_config)); + } this->set_calibration_(false); } } else { @@ -414,30 +410,32 @@ void LD2420Component::set_operating_mode(const std::string &state) { } void LD2420Component::readline_(int rx_data, uint8_t *buffer, int len) { - static int pos = 0; - - if (rx_data >= 0) { - if (pos < len - 1) { - buffer[pos++] = rx_data; - buffer[pos] = 0; - } else { - pos = 0; - } - if (pos >= 4) { - if (memcmp(&buffer[pos - 4], &CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)) == 0) { - this->set_cmd_active_(false); // Set command state to inactive after responce. - this->handle_ack_data_(buffer, pos); - pos = 0; - } else if ((buffer[pos - 2] == 0x0D && buffer[pos - 1] == 0x0A) && - (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) { - this->handle_simple_mode_(buffer, pos); - pos = 0; - } else if ((memcmp(&buffer[pos - 4], &ENERGY_FRAME_FOOTER, sizeof(ENERGY_FRAME_FOOTER)) == 0) && - (this->get_mode_() == CMD_SYSTEM_MODE_ENERGY)) { - this->handle_energy_mode_(buffer, pos); - pos = 0; - } - } + if (rx_data < 0) { + return; // No data available + } + if (this->buffer_pos_ < len - 1) { + buffer[this->buffer_pos_++] = rx_data; + buffer[this->buffer_pos_] = 0; + } else { + // We should never get here, but just in case... + ESP_LOGW(TAG, "Max command length exceeded; ignoring"); + this->buffer_pos_ = 0; + } + if (this->buffer_pos_ < 4) { + return; // Not enough data to process yet + } + if (memcmp(&buffer[this->buffer_pos_ - 4], &CMD_FRAME_FOOTER, sizeof(CMD_FRAME_FOOTER)) == 0) { + this->cmd_active_ = false; // Set command state to inactive after response + this->handle_ack_data_(buffer, this->buffer_pos_); + this->buffer_pos_ = 0; + } else if ((buffer[this->buffer_pos_ - 2] == 0x0D && buffer[this->buffer_pos_ - 1] == 0x0A) && + (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE)) { + this->handle_simple_mode_(buffer, this->buffer_pos_); + this->buffer_pos_ = 0; + } else if ((memcmp(&buffer[this->buffer_pos_ - 4], &ENERGY_FRAME_FOOTER, sizeof(ENERGY_FRAME_FOOTER)) == 0) && + (this->get_mode_() == CMD_SYSTEM_MODE_ENERGY)) { + this->handle_energy_mode_(buffer, this->buffer_pos_); + this->buffer_pos_ = 0; } } @@ -462,8 +460,9 @@ void LD2420Component::handle_energy_mode_(uint8_t *buffer, int len) { // Resonable refresh rate for home assistant database size health const int32_t current_millis = App.get_loop_component_start_time(); - if (current_millis - this->last_periodic_millis < REFRESH_RATE_MS) + if (current_millis - this->last_periodic_millis < REFRESH_RATE_MS) { return; + } this->last_periodic_millis = current_millis; for (auto &listener : this->listeners_) { listener->on_distance(this->get_distance_()); @@ -506,14 +505,16 @@ void LD2420Component::handle_simple_mode_(const uint8_t *inbuf, int len) { } } outbuf[index] = '\0'; - if (index > 1) + if (index > 1) { this->set_distance_(strtol(outbuf, &endptr, 10)); + } if (this->get_mode_() == CMD_SYSTEM_MODE_SIMPLE) { // Resonable refresh rate for home assistant database size health const int32_t current_millis = App.get_loop_component_start_time(); - if (current_millis - this->last_normal_periodic_millis < REFRESH_RATE_MS) + if (current_millis - this->last_normal_periodic_millis < REFRESH_RATE_MS) { return; + } this->last_normal_periodic_millis = current_millis; for (auto &listener : this->listeners_) listener->on_distance(this->get_distance_()); @@ -593,11 +594,12 @@ void LD2420Component::handle_ack_data_(uint8_t *buffer, int len) { int LD2420Component::send_cmd_from_array(CmdFrameT frame) { uint32_t start_millis = millis(); uint8_t error = 0; - uint8_t ack_buffer[64]; - uint8_t cmd_buffer[64]; + uint8_t ack_buffer[MAX_LINE_LENGTH]; + uint8_t cmd_buffer[MAX_LINE_LENGTH]; this->cmd_reply_.ack = false; - if (frame.command != CMD_RESTART) - this->set_cmd_active_(true); // Restart does not reply, thus no ack state required. + if (frame.command != CMD_RESTART) { + this->cmd_active_ = true; + } // Restart does not reply, thus no ack state required uint8_t retry = 3; while (retry) { frame.length = 0; @@ -619,9 +621,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) { memcpy(cmd_buffer + frame.length, &frame.footer, sizeof(frame.footer)); frame.length += sizeof(frame.footer); - for (uint16_t index = 0; index < frame.length; index++) { - this->write_byte(cmd_buffer[index]); - } + this->write_array(cmd_buffer, frame.length); error = 0; if (frame.command == CMD_RESTART) { @@ -630,7 +630,7 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) { while (!this->cmd_reply_.ack) { while (this->available()) { - this->readline_(read(), ack_buffer, sizeof(ack_buffer)); + this->readline_(this->read(), ack_buffer, sizeof(ack_buffer)); } delay_microseconds_safe(1450); // Wait on an Rx from the LD2420 for up to 3 1 second loops, otherwise it could trigger a WDT. @@ -641,10 +641,12 @@ int LD2420Component::send_cmd_from_array(CmdFrameT frame) { break; } } - if (this->cmd_reply_.ack) + if (this->cmd_reply_.ack) { retry = 0; - if (this->cmd_reply_.error > 0) + } + if (this->cmd_reply_.error > 0) { this->handle_cmd_error(error); + } } return error; } @@ -764,8 +766,9 @@ void LD2420Component::set_system_mode(uint16_t mode) { cmd_frame.data_length += sizeof(unknown_parm); cmd_frame.footer = CMD_FRAME_FOOTER; ESP_LOGV(TAG, "Sending write system mode command: %2X", cmd_frame.command); - if (this->send_cmd_from_array(cmd_frame) == 0) + if (this->send_cmd_from_array(cmd_frame) == 0) { this->set_mode_(mode); + } } void LD2420Component::get_firmware_version_() { @@ -840,18 +843,24 @@ void LD2420Component::set_gate_threshold(uint8_t gate) { #ifdef USE_NUMBER void LD2420Component::init_gate_config_numbers() { - if (this->gate_timeout_number_ != nullptr) + if (this->gate_timeout_number_ != nullptr) { this->gate_timeout_number_->publish_state(static_cast(this->current_config.timeout)); - if (this->gate_select_number_ != nullptr) + } + if (this->gate_select_number_ != nullptr) { this->gate_select_number_->publish_state(0); - if (this->min_gate_distance_number_ != nullptr) + } + if (this->min_gate_distance_number_ != nullptr) { this->min_gate_distance_number_->publish_state(static_cast(this->current_config.min_gate)); - if (this->max_gate_distance_number_ != nullptr) + } + if (this->max_gate_distance_number_ != nullptr) { this->max_gate_distance_number_->publish_state(static_cast(this->current_config.max_gate)); - if (this->gate_move_sensitivity_factor_number_ != nullptr) + } + if (this->gate_move_sensitivity_factor_number_ != nullptr) { this->gate_move_sensitivity_factor_number_->publish_state(this->gate_move_sensitivity_factor); - if (this->gate_still_sensitivity_factor_number_ != nullptr) + } + if (this->gate_still_sensitivity_factor_number_ != nullptr) { this->gate_still_sensitivity_factor_number_->publish_state(this->gate_still_sensitivity_factor); + } for (uint8_t gate = 0; gate < TOTAL_GATES; gate++) { if (this->gate_still_threshold_numbers_[gate] != nullptr) { this->gate_still_threshold_numbers_[gate]->publish_state( diff --git a/esphome/components/ld2420/ld2420.h b/esphome/components/ld2420/ld2420.h index d574a25c89..812c408cfd 100644 --- a/esphome/components/ld2420/ld2420.h +++ b/esphome/components/ld2420/ld2420.h @@ -20,8 +20,9 @@ namespace esphome { namespace ld2420 { -static const uint8_t TOTAL_GATES = 16; static const uint8_t CALIBRATE_SAMPLES = 64; +static const uint8_t MAX_LINE_LENGTH = 46; // Max characters for serial buffer +static const uint8_t TOTAL_GATES = 16; enum OpMode : uint8_t { OP_NORMAL_MODE = 1, @@ -118,10 +119,10 @@ class LD2420Component : public Component, public uart::UARTDevice { float gate_move_sensitivity_factor{0.5}; float gate_still_sensitivity_factor{0.5}; - int32_t last_periodic_millis = millis(); - int32_t report_periodic_millis = millis(); - int32_t monitor_periodic_millis = millis(); - int32_t last_normal_periodic_millis = millis(); + int32_t last_periodic_millis{0}; + int32_t report_periodic_millis{0}; + int32_t monitor_periodic_millis{0}; + int32_t last_normal_periodic_millis{0}; uint16_t radar_data[TOTAL_GATES][CALIBRATE_SAMPLES]; uint16_t gate_avg[TOTAL_GATES]; uint16_t gate_peak[TOTAL_GATES]; @@ -161,8 +162,6 @@ class LD2420Component : public Component, public uart::UARTDevice { void set_presence_(bool presence) { this->presence_ = presence; }; uint16_t get_distance_() { return this->distance_; }; void set_distance_(uint16_t distance) { this->distance_ = distance; }; - bool get_cmd_active_() { return this->cmd_active_; }; - void set_cmd_active_(bool active) { this->cmd_active_ = active; }; void handle_simple_mode_(const uint8_t *inbuf, int len); void handle_energy_mode_(uint8_t *buffer, int len); void handle_ack_data_(uint8_t *buffer, int len); @@ -181,12 +180,11 @@ class LD2420Component : public Component, public uart::UARTDevice { std::vector gate_move_threshold_numbers_ = std::vector(16); #endif - uint32_t max_distance_gate_; - uint32_t min_distance_gate_; + uint16_t distance_{0}; uint16_t system_mode_; uint16_t gate_energy_[TOTAL_GATES]; - uint16_t distance_{0}; - uint8_t config_checksum_{0}; + uint8_t buffer_pos_{0}; // where to resume processing/populating buffer + uint8_t buffer_data_[MAX_LINE_LENGTH]; char firmware_ver_[8]{"v0.0.0"}; bool cmd_active_{false}; bool presence_{false}; diff --git a/esphome/components/ld2420/number/gate_config_number.cpp b/esphome/components/ld2420/number/gate_config_number.cpp index e5eaafb46d..a373753770 100644 --- a/esphome/components/ld2420/number/gate_config_number.cpp +++ b/esphome/components/ld2420/number/gate_config_number.cpp @@ -2,7 +2,7 @@ #include "esphome/core/helpers.h" #include "esphome/core/log.h" -static const char *const TAG = "LD2420.number"; +static const char *const TAG = "ld2420.number"; namespace esphome { namespace ld2420 { diff --git a/esphome/components/ld2420/select/operating_mode_select.cpp b/esphome/components/ld2420/select/operating_mode_select.cpp index 1c59f443a5..2d576e7cc6 100644 --- a/esphome/components/ld2420/select/operating_mode_select.cpp +++ b/esphome/components/ld2420/select/operating_mode_select.cpp @@ -5,7 +5,7 @@ namespace esphome { namespace ld2420 { -static const char *const TAG = "LD2420.select"; +static const char *const TAG = "ld2420.select"; void LD2420Select::control(const std::string &value) { this->publish_state(value); diff --git a/esphome/components/ld2420/sensor/ld2420_sensor.cpp b/esphome/components/ld2420/sensor/ld2420_sensor.cpp index 97f0c594b7..723604f396 100644 --- a/esphome/components/ld2420/sensor/ld2420_sensor.cpp +++ b/esphome/components/ld2420/sensor/ld2420_sensor.cpp @@ -5,10 +5,10 @@ namespace esphome { namespace ld2420 { -static const char *const TAG = "LD2420.sensor"; +static const char *const TAG = "ld2420.sensor"; void LD2420Sensor::dump_config() { - ESP_LOGCONFIG(TAG, "LD2420 Sensor:"); + ESP_LOGCONFIG(TAG, "Sensor:"); LOG_SENSOR(" ", "Distance", this->distance_sensor_); } diff --git a/esphome/components/ld2420/text_sensor/text_sensor.cpp b/esphome/components/ld2420/text_sensor/text_sensor.cpp index 1dcdcf7d60..73af3b3660 100644 --- a/esphome/components/ld2420/text_sensor/text_sensor.cpp +++ b/esphome/components/ld2420/text_sensor/text_sensor.cpp @@ -5,10 +5,10 @@ namespace esphome { namespace ld2420 { -static const char *const TAG = "LD2420.text_sensor"; +static const char *const TAG = "ld2420.text_sensor"; void LD2420TextSensor::dump_config() { - ESP_LOGCONFIG(TAG, "LD2420 TextSensor:"); + ESP_LOGCONFIG(TAG, "Text Sensor:"); LOG_TEXT_SENSOR(" ", "Firmware", this->fw_version_text_sensor_); } From 9beb4e2cd418a64098a369536cefa086deb2f8c3 Mon Sep 17 00:00:00 2001 From: Peter Zich Date: Sat, 12 Jul 2025 18:22:07 -0700 Subject: [PATCH 02/12] (Maybe?) fix I2S speaker internal DAC mode (#9435) --- esphome/components/i2s_audio/speaker/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py index bb9f24bf0b..cb7b876a40 100644 --- a/esphome/components/i2s_audio/speaker/__init__.py +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -180,7 +180,7 @@ async def to_code(config): await speaker.register_speaker(var, config) if config[CONF_DAC_TYPE] == "internal": - cg.add(var.set_internal_dac_mode(config[CONF_CHANNEL])) + cg.add(var.set_internal_dac_mode(config[CONF_MODE])) else: cg.add(var.set_dout_pin(config[CONF_I2S_DOUT_PIN])) if use_legacy(): From 8f42bc6aaca742c2cb1dd729174ef84cbd0547f5 Mon Sep 17 00:00:00 2001 From: Peter Zich Date: Sat, 12 Jul 2025 22:43:32 -0700 Subject: [PATCH 03/12] [lvgl] Post-process size arguments in meter config (#9466) Co-authored-by: clydebarrow <2366188+clydebarrow@users.noreply.github.com> --- esphome/components/lvgl/widgets/meter.py | 33 ++++++++++++++---------- tests/components/lvgl/lvgl-package.yaml | 10 +++---- 2 files changed, 25 insertions(+), 18 deletions(-) diff --git a/esphome/components/lvgl/widgets/meter.py b/esphome/components/lvgl/widgets/meter.py index 840511da69..f836a1eca5 100644 --- a/esphome/components/lvgl/widgets/meter.py +++ b/esphome/components/lvgl/widgets/meter.py @@ -29,9 +29,9 @@ from ..defines import ( ) from ..helpers import add_lv_use, lvgl_components_required from ..lv_validation import ( - angle, get_end_value, get_start_value, + lv_angle, lv_bool, lv_color, lv_float, @@ -162,7 +162,7 @@ SCALE_SCHEMA = cv.Schema( cv.Optional(CONF_RANGE_FROM, default=0.0): cv.float_, cv.Optional(CONF_RANGE_TO, default=100.0): cv.float_, cv.Optional(CONF_ANGLE_RANGE, default=270): cv.int_range(0, 360), - cv.Optional(CONF_ROTATION): angle, + cv.Optional(CONF_ROTATION): lv_angle, cv.Optional(CONF_INDICATORS): cv.ensure_list(INDICATOR_SCHEMA), } ) @@ -187,7 +187,7 @@ class MeterType(WidgetType): for scale_conf in config.get(CONF_SCALES, ()): rotation = 90 + (360 - scale_conf[CONF_ANGLE_RANGE]) / 2 if CONF_ROTATION in scale_conf: - rotation = scale_conf[CONF_ROTATION] // 10 + rotation = await lv_angle.process(scale_conf[CONF_ROTATION]) with LocalVariable( "meter_var", "lv_meter_scale_t", lv_expr.meter_add_scale(var) ) as meter_var: @@ -205,21 +205,20 @@ class MeterType(WidgetType): var, meter_var, ticks[CONF_COUNT], - ticks[CONF_WIDTH], - ticks[CONF_LENGTH], + await size.process(ticks[CONF_WIDTH]), + await size.process(ticks[CONF_LENGTH]), color, ) if CONF_MAJOR in ticks: major = ticks[CONF_MAJOR] - color = await lv_color.process(major[CONF_COLOR]) lv.meter_set_scale_major_ticks( var, meter_var, major[CONF_STRIDE], - major[CONF_WIDTH], - major[CONF_LENGTH], - color, - major[CONF_LABEL_GAP], + await size.process(major[CONF_WIDTH]), + await size.process(major[CONF_LENGTH]), + await lv_color.process(major[CONF_COLOR]), + await size.process(major[CONF_LABEL_GAP]), ) for indicator in scale_conf.get(CONF_INDICATORS, ()): (t, v) = next(iter(indicator.items())) @@ -233,7 +232,11 @@ class MeterType(WidgetType): lv_assign( ivar, lv_expr.meter_add_needle_line( - var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD] + var, + meter_var, + await size.process(v[CONF_WIDTH]), + color, + await size.process(v[CONF_R_MOD]), ), ) if t == CONF_ARC: @@ -241,7 +244,11 @@ class MeterType(WidgetType): lv_assign( ivar, lv_expr.meter_add_arc( - var, meter_var, v[CONF_WIDTH], color, v[CONF_R_MOD] + var, + meter_var, + await size.process(v[CONF_WIDTH]), + color, + await size.process(v[CONF_R_MOD]), ), ) if t == CONF_TICK_STYLE: @@ -257,7 +264,7 @@ class MeterType(WidgetType): color_start, color_end, v[CONF_LOCAL], - v[CONF_WIDTH], + size.process(v[CONF_WIDTH]), ), ) if t == CONF_IMAGE: diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 2edc62b6a1..fbcd2a3fba 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -919,21 +919,21 @@ lvgl: text_color: 0xFFFFFF scales: - ticks: - width: 1 + width: !lambda return 1; count: 61 - length: 20 + length: 20% color: 0xFFFFFF range_from: 0 range_to: 60 angle_range: 360 - rotation: 270 + rotation: !lambda return 2700; indicators: - line: opa: 50% id: minute_hand color: 0xFF0000 - r_mod: -1 - width: 3 + r_mod: !lambda return -1; + width: !lambda return 3; - angle_range: 330 rotation: 300 From 77d1d0414d6903a2c77e32e6d30fb607b3bdc2a2 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sat, 12 Jul 2025 22:09:55 -1000 Subject: [PATCH 04/12] Automatically disable interrupts for ESP8266 GPIO16 binary sensors (#9467) --- .../components/gpio/binary_sensor/__init__.py | 27 +++++++- .../gpio/test_gpio_binary_sensor.py | 69 +++++++++++++++++++ .../gpio/test_gpio_binary_sensor.yaml | 11 +++ .../gpio/test_gpio_binary_sensor_esp8266.yaml | 20 ++++++ .../gpio/test_gpio_binary_sensor_polling.yaml | 12 ++++ 5 files changed, 136 insertions(+), 3 deletions(-) create mode 100644 tests/component_tests/gpio/test_gpio_binary_sensor.py create mode 100644 tests/component_tests/gpio/test_gpio_binary_sensor.yaml create mode 100644 tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml create mode 100644 tests/component_tests/gpio/test_gpio_binary_sensor_polling.yaml diff --git a/esphome/components/gpio/binary_sensor/__init__.py b/esphome/components/gpio/binary_sensor/__init__.py index 9f50fd779a..867a8efe49 100644 --- a/esphome/components/gpio/binary_sensor/__init__.py +++ b/esphome/components/gpio/binary_sensor/__init__.py @@ -1,11 +1,16 @@ +import logging + from esphome import pins import esphome.codegen as cg from esphome.components import binary_sensor import esphome.config_validation as cv -from esphome.const import CONF_PIN +from esphome.const import CONF_ID, CONF_NAME, CONF_NUMBER, CONF_PIN +from esphome.core import CORE from .. import gpio_ns +_LOGGER = logging.getLogger(__name__) + GPIOBinarySensor = gpio_ns.class_( "GPIOBinarySensor", binary_sensor.BinarySensor, cg.Component ) @@ -41,6 +46,22 @@ async def to_code(config): pin = await cg.gpio_pin_expression(config[CONF_PIN]) cg.add(var.set_pin(pin)) - cg.add(var.set_use_interrupt(config[CONF_USE_INTERRUPT])) - if config[CONF_USE_INTERRUPT]: + # Check for ESP8266 GPIO16 interrupt limitation + # GPIO16 on ESP8266 is a special pin that doesn't support interrupts through + # the Arduino attachInterrupt() function. This is the only known GPIO pin + # across all supported platforms that has this limitation, so we handle it + # here instead of in the platform-specific code. + use_interrupt = config[CONF_USE_INTERRUPT] + if use_interrupt and CORE.is_esp8266 and config[CONF_PIN][CONF_NUMBER] == 16: + _LOGGER.warning( + "GPIO binary_sensor '%s': GPIO16 on ESP8266 doesn't support interrupts. " + "Falling back to polling mode (same as in ESPHome <2025.7). " + "The sensor will work exactly as before, but other pins have better " + "performance with interrupts.", + config.get(CONF_NAME, config[CONF_ID]), + ) + use_interrupt = False + + cg.add(var.set_use_interrupt(use_interrupt)) + if use_interrupt: cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) diff --git a/tests/component_tests/gpio/test_gpio_binary_sensor.py b/tests/component_tests/gpio/test_gpio_binary_sensor.py new file mode 100644 index 0000000000..74fa2ab1c1 --- /dev/null +++ b/tests/component_tests/gpio/test_gpio_binary_sensor.py @@ -0,0 +1,69 @@ +"""Tests for the GPIO binary sensor component.""" + +from __future__ import annotations + +from collections.abc import Callable +from pathlib import Path + +import pytest + + +def test_gpio_binary_sensor_basic_setup( + generate_main: Callable[[str | Path], str], +) -> None: + """ + When the GPIO binary sensor is set in the yaml file, it should be registered in main + """ + main_cpp = generate_main("tests/component_tests/gpio/test_gpio_binary_sensor.yaml") + + assert "new gpio::GPIOBinarySensor();" in main_cpp + assert "App.register_binary_sensor" in main_cpp + assert "bs_gpio->set_use_interrupt(true);" in main_cpp + assert "bs_gpio->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp + + +def test_gpio_binary_sensor_esp8266_gpio16_disables_interrupt( + generate_main: Callable[[str | Path], str], + caplog: pytest.LogCaptureFixture, +) -> None: + """ + Test that ESP8266 GPIO16 automatically disables interrupt mode with a warning + """ + main_cpp = generate_main( + "tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml" + ) + + # Check that interrupt is disabled for GPIO16 + assert "bs_gpio16->set_use_interrupt(false);" in main_cpp + + # Check that the warning was logged + assert "GPIO16 on ESP8266 doesn't support interrupts" in caplog.text + assert "Falling back to polling mode" in caplog.text + + +def test_gpio_binary_sensor_esp8266_other_pins_use_interrupt( + generate_main: Callable[[str | Path], str], +) -> None: + """ + Test that ESP8266 pins other than GPIO16 still use interrupt mode + """ + main_cpp = generate_main( + "tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml" + ) + + # GPIO5 should still use interrupts + assert "bs_gpio5->set_use_interrupt(true);" in main_cpp + assert "bs_gpio5->set_interrupt_type(gpio::INTERRUPT_ANY_EDGE);" in main_cpp + + +def test_gpio_binary_sensor_explicit_polling_mode( + generate_main: Callable[[str | Path], str], +) -> None: + """ + Test that explicitly setting use_interrupt: false works + """ + main_cpp = generate_main( + "tests/component_tests/gpio/test_gpio_binary_sensor_polling.yaml" + ) + + assert "bs_polling->set_use_interrupt(false);" in main_cpp diff --git a/tests/component_tests/gpio/test_gpio_binary_sensor.yaml b/tests/component_tests/gpio/test_gpio_binary_sensor.yaml new file mode 100644 index 0000000000..e258fe0cb4 --- /dev/null +++ b/tests/component_tests/gpio/test_gpio_binary_sensor.yaml @@ -0,0 +1,11 @@ +esphome: + name: test + +esp32: + board: esp32dev + +binary_sensor: + - platform: gpio + pin: 5 + name: "Test GPIO Binary Sensor" + id: bs_gpio diff --git a/tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml b/tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml new file mode 100644 index 0000000000..aec26fe572 --- /dev/null +++ b/tests/component_tests/gpio/test_gpio_binary_sensor_esp8266.yaml @@ -0,0 +1,20 @@ +esphome: + name: test + +esp8266: + board: d1_mini + +binary_sensor: + - platform: gpio + pin: + number: 16 + mode: INPUT_PULLDOWN_16 + name: "GPIO16 Touch Sensor" + id: bs_gpio16 + + - platform: gpio + pin: + number: 5 + mode: INPUT_PULLUP + name: "GPIO5 Button" + id: bs_gpio5 diff --git a/tests/component_tests/gpio/test_gpio_binary_sensor_polling.yaml b/tests/component_tests/gpio/test_gpio_binary_sensor_polling.yaml new file mode 100644 index 0000000000..7c53e09265 --- /dev/null +++ b/tests/component_tests/gpio/test_gpio_binary_sensor_polling.yaml @@ -0,0 +1,12 @@ +esphome: + name: test + +esp32: + board: esp32dev + +binary_sensor: + - platform: gpio + pin: 5 + name: "Polling Mode Sensor" + id: bs_polling + use_interrupt: false From c13317f807f320d6d7989462ead61cdc17415131 Mon Sep 17 00:00:00 2001 From: Javier Peletier Date: Sun, 13 Jul 2025 23:58:52 +0200 Subject: [PATCH 05/12] [substitutions] Fix #7189 (#9469) --- esphome/components/substitutions/__init__.py | 7 +++++-- .../fixtures/substitutions/00-simple_var.approved.yaml | 2 ++ .../fixtures/substitutions/00-simple_var.input.yaml | 2 ++ .../fixtures/substitutions/03-closures.approved.yaml | 1 + .../fixtures/substitutions/closures_package.yaml | 1 + 5 files changed, 11 insertions(+), 2 deletions(-) diff --git a/esphome/components/substitutions/__init__.py b/esphome/components/substitutions/__init__.py index 5878af43b2..e07c2e3859 100644 --- a/esphome/components/substitutions/__init__.py +++ b/esphome/components/substitutions/__init__.py @@ -151,8 +151,11 @@ def _substitute_item(substitutions, item, path, jinja, ignore_missing): if sub is not None: item[k] = sub for old, new in replace_keys: - item[new] = merge_config(item.get(old), item.get(new)) - del item[old] + if str(new) == str(old): + item[new] = item[old] + else: + item[new] = merge_config(item.get(old), item.get(new)) + del item[old] elif isinstance(item, str): sub = _expand_substitutions(substitutions, item, path, jinja, ignore_missing) if isinstance(sub, JinjaStr) or sub != item: diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml index c031399c37..f5d2f8aa20 100644 --- a/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.approved.yaml @@ -17,3 +17,5 @@ test_list: - ${undefined_var} - $undefined_var - ${ undefined_var } + - key1: 1 + key2: 2 diff --git a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml index 88a4ffb991..5717433c7e 100644 --- a/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml +++ b/tests/unit_tests/fixtures/substitutions/00-simple_var.input.yaml @@ -19,3 +19,5 @@ test_list: - ${undefined_var} - $undefined_var - ${ undefined_var } + - key${var1}: 1 + key${var2}: 2 diff --git a/tests/unit_tests/fixtures/substitutions/03-closures.approved.yaml b/tests/unit_tests/fixtures/substitutions/03-closures.approved.yaml index c8f7d9976c..dad3be8aa7 100644 --- a/tests/unit_tests/fixtures/substitutions/03-closures.approved.yaml +++ b/tests/unit_tests/fixtures/substitutions/03-closures.approved.yaml @@ -6,6 +6,7 @@ package_result: root file - Double substitution also works; the value of var7 is 79, where A is a package var + - key79: Key should substitute to key79 local_results: - The value of B is 5 - 'You will see, however, that diff --git a/tests/unit_tests/fixtures/substitutions/closures_package.yaml b/tests/unit_tests/fixtures/substitutions/closures_package.yaml index e87908814d..4a0be24b93 100644 --- a/tests/unit_tests/fixtures/substitutions/closures_package.yaml +++ b/tests/unit_tests/fixtures/substitutions/closures_package.yaml @@ -1,3 +1,4 @@ package_result: - The value of A*B is ${A * B}, where A is a package var and B is a substitution in the root file - Double substitution also works; the value of var7 is ${var$A}, where A is a package var + - key${var7}: Key should substitute to key79 From 9207bf97f316c5ad93da2ffa9f267c8c182e40f0 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 14 Jul 2025 09:47:52 +1000 Subject: [PATCH 06/12] [esp_ldo] Component schema; default priority (#9479) --- esphome/components/esp_ldo/__init__.py | 18 ++++++++++-------- esphome/components/esp_ldo/esp_ldo.h | 3 +++ .../components/esp_ldo/test.esp32-p4-idf.yaml | 1 + 3 files changed, 14 insertions(+), 8 deletions(-) diff --git a/esphome/components/esp_ldo/__init__.py b/esphome/components/esp_ldo/__init__.py index ce24028083..38e684c537 100644 --- a/esphome/components/esp_ldo/__init__.py +++ b/esphome/components/esp_ldo/__init__.py @@ -20,14 +20,16 @@ adjusted_ids = set() CONFIG_SCHEMA = cv.All( cv.ensure_list( - { - cv.GenerateID(): cv.declare_id(EspLdo), - cv.Required(CONF_VOLTAGE): cv.All( - cv.voltage, cv.float_range(min=0.5, max=2.7) - ), - cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True), - cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean, - } + cv.COMPONENT_SCHEMA.extend( + { + cv.GenerateID(): cv.declare_id(EspLdo), + cv.Required(CONF_VOLTAGE): cv.All( + cv.voltage, cv.float_range(min=0.5, max=2.7) + ), + cv.Required(CONF_CHANNEL): cv.one_of(*CHANNELS, int=True), + cv.Optional(CONF_ADJUSTABLE, default=False): cv.boolean, + } + ) ), cv.only_with_esp_idf, only_on_variant(supported=[VARIANT_ESP32P4]), diff --git a/esphome/components/esp_ldo/esp_ldo.h b/esphome/components/esp_ldo/esp_ldo.h index fb5abf7a3a..bafa32db6b 100644 --- a/esphome/components/esp_ldo/esp_ldo.h +++ b/esphome/components/esp_ldo/esp_ldo.h @@ -17,6 +17,9 @@ class EspLdo : public Component { void set_adjustable(bool adjustable) { this->adjustable_ = adjustable; } void set_voltage(float voltage) { this->voltage_ = voltage; } void adjust_voltage(float voltage); + float get_setup_priority() const override { + return setup_priority::BUS; // LDO setup should be done early + } protected: int channel_; diff --git a/tests/components/esp_ldo/test.esp32-p4-idf.yaml b/tests/components/esp_ldo/test.esp32-p4-idf.yaml index 0e91aaf082..38bd6ac411 100644 --- a/tests/components/esp_ldo/test.esp32-p4-idf.yaml +++ b/tests/components/esp_ldo/test.esp32-p4-idf.yaml @@ -6,6 +6,7 @@ esp_ldo: - id: ldo_4 channel: 4 voltage: 2.0V + setup_priority: 900 esphome: on_boot: From e43efdaaecb6bf0fb6783b6da7fc20722d55c4ed Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Jul 2025 14:41:49 -1000 Subject: [PATCH 07/12] Follow logging best practices by removing redundant component prefix (#9481) --- esphome/core/component.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/esphome/core/component.cpp b/esphome/core/component.cpp index 9d863e56cd..b360e1d20b 100644 --- a/esphome/core/component.cpp +++ b/esphome/core/component.cpp @@ -138,7 +138,7 @@ void Component::call_dump_config() { } } } - ESP_LOGE(TAG, " Component %s is marked FAILED: %s", this->get_component_source(), error_msg); + ESP_LOGE(TAG, " %s is marked FAILED: %s", this->get_component_source(), error_msg); } } @@ -191,7 +191,7 @@ bool Component::should_warn_of_blocking(uint32_t blocking_time) { return false; } void Component::mark_failed() { - ESP_LOGE(TAG, "Component %s was marked as failed", this->get_component_source()); + ESP_LOGE(TAG, "%s was marked as failed", this->get_component_source()); this->component_state_ &= ~COMPONENT_STATE_MASK; this->component_state_ |= COMPONENT_STATE_FAILED; this->status_set_error(); @@ -229,7 +229,7 @@ void IRAM_ATTR HOT Component::enable_loop_soon_any_context() { } void Component::reset_to_construction_state() { if ((this->component_state_ & COMPONENT_STATE_MASK) == COMPONENT_STATE_FAILED) { - ESP_LOGI(TAG, "Component %s is being reset to construction state", this->get_component_source()); + ESP_LOGI(TAG, "%s is being reset to construction state", this->get_component_source()); this->component_state_ &= ~COMPONENT_STATE_MASK; this->component_state_ |= COMPONENT_STATE_CONSTRUCTION; // Clear error status when resetting @@ -275,14 +275,14 @@ void Component::status_set_warning(const char *message) { return; this->component_state_ |= STATUS_LED_WARNING; App.app_state_ |= STATUS_LED_WARNING; - ESP_LOGW(TAG, "Component %s set Warning flag: %s", this->get_component_source(), message); + ESP_LOGW(TAG, "%s set Warning flag: %s", this->get_component_source(), message); } void Component::status_set_error(const char *message) { if ((this->component_state_ & STATUS_LED_ERROR) != 0) return; this->component_state_ |= STATUS_LED_ERROR; App.app_state_ |= STATUS_LED_ERROR; - ESP_LOGE(TAG, "Component %s set Error flag: %s", this->get_component_source(), message); + ESP_LOGE(TAG, "%s set Error flag: %s", this->get_component_source(), message); if (strcmp(message, "unspecified") != 0) { // Lazy allocate the error messages vector if needed if (!component_error_messages) { @@ -303,13 +303,13 @@ void Component::status_clear_warning() { if ((this->component_state_ & STATUS_LED_WARNING) == 0) return; this->component_state_ &= ~STATUS_LED_WARNING; - ESP_LOGW(TAG, "Component %s cleared Warning flag", this->get_component_source()); + ESP_LOGW(TAG, "%s cleared Warning flag", this->get_component_source()); } void Component::status_clear_error() { if ((this->component_state_ & STATUS_LED_ERROR) == 0) return; this->component_state_ &= ~STATUS_LED_ERROR; - ESP_LOGE(TAG, "Component %s cleared Error flag", this->get_component_source()); + ESP_LOGE(TAG, "%s cleared Error flag", this->get_component_source()); } void Component::status_momentary_warning(const std::string &name, uint32_t length) { this->status_set_warning(); @@ -403,7 +403,7 @@ uint32_t WarnIfComponentBlockingGuard::finish() { } if (should_warn) { const char *src = component_ == nullptr ? "" : component_->get_component_source(); - ESP_LOGW(TAG, "Component %s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time); + ESP_LOGW(TAG, "%s took a long time for an operation (%" PRIu32 " ms)", src, blocking_time); ESP_LOGW(TAG, "Components should block for at most 30 ms"); } From 10ca7ed85b5bc90e0f653445f3dcfe88e1c4ccae Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Jul 2025 14:58:07 -1000 Subject: [PATCH 08/12] Fix dormant bug in RAMAllocator::reallocate() manual_size calculation (#9482) --- esphome/core/helpers.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 58f162ff9d..c3b404ae60 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -783,7 +783,7 @@ template class RAMAllocator { T *reallocate(T *p, size_t n) { return this->reallocate(p, n, sizeof(T)); } T *reallocate(T *p, size_t n, size_t manual_size) { - size_t size = n * sizeof(T); + size_t size = n * manual_size; T *ptr = nullptr; #ifdef USE_ESP32 if (this->flags_ & Flags::ALLOC_EXTERNAL) { From b4521e1d8c6dad3f8de33a72951609d610d228f2 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 14 Jul 2025 13:08:24 +1200 Subject: [PATCH 09/12] Bump version to 2025.7.0b3 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index 05731fa4ef..42af703a94 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.7.0b2 +PROJECT_NUMBER = 2025.7.0b3 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/const.py b/esphome/const.py index 0753ea32d4..de15050d0c 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.7.0b2" +__version__ = "2025.7.0b3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 740c0ef9d7efb0fefd953abbccb6baa7cf77c472 Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 13 Jul 2025 12:44:21 -1000 Subject: [PATCH 10/12] Fix pre-commit CI failures by skipping local hooks that require virtual environment (#9476) --- .pre-commit-config.yaml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 831473c325..cf00f9bd47 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,13 @@ --- # See https://pre-commit.com for more information # See https://pre-commit.com/hooks.html for more hooks + +ci: + autoupdate_commit_msg: 'pre-commit: autoupdate' + autoupdate_schedule: weekly + # Skip hooks that have issues in pre-commit CI environment + skip: [pylint, clang-tidy-hash, yamllint] + repos: - repo: https://github.com/astral-sh/ruff-pre-commit # Ruff version. From 4153380f99af004e7072f0e293610e4feb4b0304 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 14 Jul 2025 13:48:41 +1200 Subject: [PATCH 11/12] Remove hook that doesnt exist on beta --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index cf00f9bd47..837c71cf38 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -6,7 +6,7 @@ ci: autoupdate_commit_msg: 'pre-commit: autoupdate' autoupdate_schedule: weekly # Skip hooks that have issues in pre-commit CI environment - skip: [pylint, clang-tidy-hash, yamllint] + skip: [pylint, yamllint] repos: - repo: https://github.com/astral-sh/ruff-pre-commit From 90f0ebb22b5a8294b295a002748378ba05f16aaf Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Mon, 14 Jul 2025 14:12:26 +1200 Subject: [PATCH 12/12] Dont autofix PR --- .pre-commit-config.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 837c71cf38..83ea2dc823 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -5,6 +5,7 @@ ci: autoupdate_commit_msg: 'pre-commit: autoupdate' autoupdate_schedule: weekly + autofix_prs: false # Skip hooks that have issues in pre-commit CI environment skip: [pylint, yamllint]