diff --git a/esphome/components/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 0804985cc5..25512de4c7 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -1712,17 +1712,16 @@ void APIConnection::on_home_assistant_state_response(const HomeAssistantStateRes } // Create null-terminated state for callback (parse_number needs null-termination) - // HA state max length is 255, so 256 byte buffer covers all cases - char state_buf[256]; - size_t copy_len = msg.state.size(); - if (copy_len >= sizeof(state_buf)) { - copy_len = sizeof(state_buf) - 1; // Truncate to leave space for null terminator + // HA state max length is 255 characters, but attributes can be much longer + // Use stack buffer for common case (states), heap fallback for large attributes + size_t state_len = msg.state.size(); + SmallBufferWithHeapFallback<256> state_buf_alloc(state_len + 1); + char *state_buf = reinterpret_cast(state_buf_alloc.get()); + if (state_len > 0) { + memcpy(state_buf, msg.state.c_str(), state_len); } - if (copy_len > 0) { - memcpy(state_buf, msg.state.c_str(), copy_len); - } - state_buf[copy_len] = '\0'; - it.callback(StringRef(state_buf, copy_len)); + state_buf[state_len] = '\0'; + it.callback(StringRef(state_buf, state_len)); } } #endif diff --git a/esphome/components/ccs811/ccs811.cpp b/esphome/components/ccs811/ccs811.cpp index 84355f2793..9ff01b32b2 100644 --- a/esphome/components/ccs811/ccs811.cpp +++ b/esphome/components/ccs811/ccs811.cpp @@ -81,8 +81,8 @@ void CCS811Component::setup() { bootloader_version, application_version); if (this->version_ != nullptr) { char version[20]; // "15.15.15 (0xffff)" is 17 chars, plus NUL, plus wiggle room - sprintf(version, "%d.%d.%d (0x%02x)", (application_version >> 12 & 15), (application_version >> 8 & 15), - (application_version >> 4 & 15), application_version); + buf_append_printf(version, sizeof(version), 0, "%d.%d.%d (0x%02x)", (application_version >> 12 & 15), + (application_version >> 8 & 15), (application_version >> 4 & 15), application_version); ESP_LOGD(TAG, "publishing version state: %s", version); this->version_->publish_state(version); } diff --git a/esphome/components/ch422g/ch422g.cpp b/esphome/components/ch422g/ch422g.cpp index d031c31294..eef95b9ba2 100644 --- a/esphome/components/ch422g/ch422g.cpp +++ b/esphome/components/ch422g/ch422g.cpp @@ -133,7 +133,7 @@ bool CH422GGPIOPin::digital_read() { return this->parent_->digital_read(this->pi void CH422GGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value ^ this->inverted_); } size_t CH422GGPIOPin::dump_summary(char *buffer, size_t len) const { - return snprintf(buffer, len, "EXIO%u via CH422G", this->pin_); + return buf_append_printf(buffer, len, 0, "EXIO%u via CH422G", this->pin_); } void CH422GGPIOPin::set_flags(gpio::Flags flags) { flags_ = flags; diff --git a/esphome/components/cse7766/cse7766.cpp b/esphome/components/cse7766/cse7766.cpp index 71fe15f0ae..4432195365 100644 --- a/esphome/components/cse7766/cse7766.cpp +++ b/esphome/components/cse7766/cse7766.cpp @@ -207,20 +207,24 @@ void CSE7766Component::parse_data_() { #if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE { - std::string buf = "Parsed:"; + // Buffer: 7 + 15 + 33 + 15 + 25 = 95 chars max + null, rounded to 128 for safety margin. + // Float sizes with %.4f can be up to 11 chars for large values (e.g., 999999.9999). + char buf[128]; + size_t pos = buf_append_printf(buf, sizeof(buf), 0, "Parsed:"); if (have_voltage) { - buf += str_sprintf(" V=%fV", voltage); + pos = buf_append_printf(buf, sizeof(buf), pos, " V=%.4fV", voltage); } if (have_current) { - buf += str_sprintf(" I=%fmA (~%fmA)", current * 1000.0f, calculated_current * 1000.0f); + pos = buf_append_printf(buf, sizeof(buf), pos, " I=%.4fmA (~%.4fmA)", current * 1000.0f, + calculated_current * 1000.0f); } if (have_power) { - buf += str_sprintf(" P=%fW", power); + pos = buf_append_printf(buf, sizeof(buf), pos, " P=%.4fW", power); } if (energy != 0.0f) { - buf += str_sprintf(" E=%fkWh (%u)", energy, cf_pulses); + buf_append_printf(buf, sizeof(buf), pos, " E=%.4fkWh (%u)", energy, cf_pulses); } - ESP_LOGVV(TAG, "%s", buf.c_str()); + ESP_LOGVV(TAG, "%s", buf); } #endif } diff --git a/esphome/components/daikin_arc/daikin_arc.cpp b/esphome/components/daikin_arc/daikin_arc.cpp index f05342f482..4726310806 100644 --- a/esphome/components/daikin_arc/daikin_arc.cpp +++ b/esphome/components/daikin_arc/daikin_arc.cpp @@ -258,8 +258,9 @@ bool DaikinArcClimate::parse_state_frame_(const uint8_t frame[]) { } char buf[DAIKIN_STATE_FRAME_SIZE * 3 + 1] = {0}; + size_t pos = 0; for (size_t i = 0; i < DAIKIN_STATE_FRAME_SIZE; i++) { - sprintf(buf, "%s%02x ", buf, frame[i]); + pos = buf_append_printf(buf, sizeof(buf), pos, "%02x ", frame[i]); } ESP_LOGD(TAG, "FRAME %s", buf); @@ -349,8 +350,9 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) { valid_daikin_frame = true; size_t bytes_count = data.size() / 2 / 8; - std::unique_ptr buf(new char[bytes_count * 3 + 1]); - buf[0] = '\0'; + size_t buf_size = bytes_count * 3 + 1; + std::unique_ptr buf(new char[buf_size]()); // value-initialize (zero-fill) + size_t buf_pos = 0; for (size_t i = 0; i < bytes_count; i++) { uint8_t byte = 0; for (int8_t bit = 0; bit < 8; bit++) { @@ -361,19 +363,19 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { break; } } - sprintf(buf.get(), "%s%02x ", buf.get(), byte); + buf_pos = buf_append_printf(buf.get(), buf_size, buf_pos, "%02x ", byte); } ESP_LOGD(TAG, "WHOLE FRAME %s size: %d", buf.get(), data.size()); } if (!valid_daikin_frame) { - char sbuf[16 * 10 + 1]; - sbuf[0] = '\0'; + char sbuf[16 * 10 + 1] = {0}; + size_t sbuf_pos = 0; for (size_t j = 0; j < static_cast(data.size()); j++) { if ((j - 2) % 16 == 0) { if (j > 0) { ESP_LOGD(TAG, "DATA %04x: %s", (j - 16 > 0xffff ? 0 : j - 16), sbuf); } - sbuf[0] = '\0'; + sbuf_pos = 0; } char type_ch = ' '; // debug_tolerance = 25% @@ -401,9 +403,10 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) { type_ch = '0'; if (abs(data[j]) > 100000) { - sprintf(sbuf, "%s%-5d[%c] ", sbuf, data[j] > 0 ? 99999 : -99999, type_ch); + sbuf_pos = buf_append_printf(sbuf, sizeof(sbuf), sbuf_pos, "%-5d[%c] ", data[j] > 0 ? 99999 : -99999, type_ch); } else { - sprintf(sbuf, "%s%-5d[%c] ", sbuf, (int) (round(data[j] / 10.) * 10), type_ch); + sbuf_pos = + buf_append_printf(sbuf, sizeof(sbuf), sbuf_pos, "%-5d[%c] ", (int) (round(data[j] / 10.) * 10), type_ch); } if (j + 1 == static_cast(data.size())) { ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf); diff --git a/esphome/components/dfrobot_sen0395/commands.cpp b/esphome/components/dfrobot_sen0395/commands.cpp index 8bb6ddf942..2c44c6fba9 100644 --- a/esphome/components/dfrobot_sen0395/commands.cpp +++ b/esphome/components/dfrobot_sen0395/commands.cpp @@ -127,7 +127,9 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float this->min2_ = min2 = this->max2_ = max2 = this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = this->max4_ = max4 = -1; - this->cmd_ = str_sprintf("detRangeCfg -1 %.0f %.0f", min1 / 0.15, max1 / 0.15); + char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null + snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f", min1 / 0.15, max1 / 0.15); + this->cmd_ = buf; } else if (min3 < 0 || max3 < 0) { this->min1_ = min1 = round(min1 / 0.15) * 0.15; this->max1_ = max1 = round(max1 / 0.15) * 0.15; @@ -135,7 +137,10 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float this->max2_ = max2 = round(max2 / 0.15) * 0.15; this->min3_ = min3 = this->max3_ = max3 = this->min4_ = min4 = this->max4_ = max4 = -1; - this->cmd_ = str_sprintf("detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15); + char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null + snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15, + max2 / 0.15); + this->cmd_ = buf; } else if (min4 < 0 || max4 < 0) { this->min1_ = min1 = round(min1 / 0.15) * 0.15; this->max1_ = max1 = round(max1 / 0.15) * 0.15; @@ -145,9 +150,10 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float this->max3_ = max3 = round(max3 / 0.15) * 0.15; this->min4_ = min4 = this->max4_ = max4 = -1; - this->cmd_ = str_sprintf("detRangeCfg -1 " - "%.0f %.0f %.0f %.0f %.0f %.0f", - min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15); + char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null + snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, min2 / 0.15, + max2 / 0.15, min3 / 0.15, max3 / 0.15); + this->cmd_ = buf; } else { this->min1_ = min1 = round(min1 / 0.15) * 0.15; this->max1_ = max1 = round(max1 / 0.15) * 0.15; @@ -158,10 +164,10 @@ DetRangeCfgCommand::DetRangeCfgCommand(float min1, float max1, float min2, float this->min4_ = min4 = round(min4 / 0.15) * 0.15; this->max4_ = max4 = round(max4 / 0.15) * 0.15; - this->cmd_ = str_sprintf("detRangeCfg -1 " - "%.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f", - min1 / 0.15, max1 / 0.15, min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15, min4 / 0.15, - max4 / 0.15); + char buf[72]; // max 72: "detRangeCfg -1 "(15) + 8 * (float(5) + space(1)) + null + snprintf(buf, sizeof(buf), "detRangeCfg -1 %.0f %.0f %.0f %.0f %.0f %.0f %.0f %.0f", min1 / 0.15, max1 / 0.15, + min2 / 0.15, max2 / 0.15, min3 / 0.15, max3 / 0.15, min4 / 0.15, max4 / 0.15); + this->cmd_ = buf; } this->min1_ = min1; @@ -203,7 +209,10 @@ SetLatencyCommand::SetLatencyCommand(float delay_after_detection, float delay_af delay_after_disappear = std::round(delay_after_disappear / 0.025f) * 0.025f; this->delay_after_detection_ = clamp(delay_after_detection, 0.0f, 1638.375f); this->delay_after_disappear_ = clamp(delay_after_disappear, 0.0f, 1638.375f); - this->cmd_ = str_sprintf("setLatency %.03f %.03f", this->delay_after_detection_, this->delay_after_disappear_); + // max 32: "setLatency "(11) + float(8) + " "(1) + float(8) + null, rounded to 32 + char buf[32]; + snprintf(buf, sizeof(buf), "setLatency %.03f %.03f", this->delay_after_detection_, this->delay_after_disappear_); + this->cmd_ = buf; }; uint8_t SetLatencyCommand::on_message(std::string &message) { diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index d69a438578..a82ee48718 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -69,7 +69,10 @@ void Esp32HostedUpdate::setup() { // Get coprocessor version esp_hosted_coprocessor_fwver_t ver_info; if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) { - this->update_info_.current_version = str_sprintf("%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1); + // 16 bytes: "255.255.255" (11 chars) + null + safety margin + char buf[16]; + snprintf(buf, sizeof(buf), "%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1); + this->update_info_.current_version = buf; } else { this->update_info_.current_version = "unknown"; } diff --git a/esphome/components/esp8266/core.cpp b/esphome/components/esp8266/core.cpp index 200ca567c2..784b87916b 100644 --- a/esphome/components/esp8266/core.cpp +++ b/esphome/components/esp8266/core.cpp @@ -6,7 +6,11 @@ #include "esphome/core/helpers.h" #include "preferences.h" #include -#include +#include + +extern "C" { +#include +} namespace esphome { @@ -16,23 +20,19 @@ void IRAM_ATTR HOT delay(uint32_t ms) { ::delay(ms); } uint32_t IRAM_ATTR HOT micros() { return ::micros(); } void IRAM_ATTR HOT delayMicroseconds(uint32_t us) { delay_microseconds_safe(us); } void arch_restart() { - ESP.restart(); // NOLINT(readability-static-accessed-through-instance) + system_restart(); // restart() doesn't always end execution while (true) { // NOLINT(clang-diagnostic-unreachable-code) yield(); } } void arch_init() {} -void IRAM_ATTR HOT arch_feed_wdt() { - ESP.wdtFeed(); // NOLINT(readability-static-accessed-through-instance) -} +void IRAM_ATTR HOT arch_feed_wdt() { system_soft_wdt_feed(); } uint8_t progmem_read_byte(const uint8_t *addr) { return pgm_read_byte(addr); // NOLINT } -uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { - return ESP.getCycleCount(); // NOLINT(readability-static-accessed-through-instance) -} +uint32_t IRAM_ATTR HOT arch_get_cpu_cycle_count() { return esp_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { return F_CPU; } void force_link_symbols() { diff --git a/esphome/components/esp8266/gpio.cpp b/esphome/components/esp8266/gpio.cpp index 7a5ee08984..659233443e 100644 --- a/esphome/components/esp8266/gpio.cpp +++ b/esphome/components/esp8266/gpio.cpp @@ -99,7 +99,7 @@ void ESP8266GPIOPin::pin_mode(gpio::Flags flags) { } size_t ESP8266GPIOPin::dump_summary(char *buffer, size_t len) const { - return snprintf(buffer, len, "GPIO%u", this->pin_); + return buf_append_printf(buffer, len, 0, "GPIO%u", this->pin_); } bool ESP8266GPIOPin::digital_read() { diff --git a/esphome/components/ezo/ezo.cpp b/esphome/components/ezo/ezo.cpp index 2e92c58e29..e4036021df 100644 --- a/esphome/components/ezo/ezo.cpp +++ b/esphome/components/ezo/ezo.cpp @@ -160,7 +160,7 @@ void EZOSensor::loop() { this->commands_.pop_front(); } -void EZOSensor::add_command_(const std::string &command, EzoCommandType command_type, uint16_t delay_ms) { +void EZOSensor::add_command_(const char *command, EzoCommandType command_type, uint16_t delay_ms) { std::unique_ptr ezo_command(new EzoCommand); ezo_command->command = command; ezo_command->command_type = command_type; @@ -169,13 +169,17 @@ void EZOSensor::add_command_(const std::string &command, EzoCommandType command_ } void EZOSensor::set_calibration_point_(EzoCalibrationType type, float value) { - std::string payload = str_sprintf("Cal,%s,%0.2f", EZO_CALIBRATION_TYPE_STRINGS[type], value); + // max 21: "Cal,"(4) + type(4) + ","(1) + float(11) + null; use 24 for safety + char payload[24]; + snprintf(payload, sizeof(payload), "Cal,%s,%0.2f", EZO_CALIBRATION_TYPE_STRINGS[type], value); this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900); } void EZOSensor::set_address(uint8_t address) { if (address > 0 && address < 128) { - std::string payload = str_sprintf("I2C,%u", address); + // max 8: "I2C,"(4) + uint8(3) + null + char payload[8]; + snprintf(payload, sizeof(payload), "I2C,%u", address); this->new_address_ = address; this->add_command_(payload, EzoCommandType::EZO_I2C); } else { @@ -194,7 +198,9 @@ void EZOSensor::get_slope() { this->add_command_("Slope,?", EzoCommandType::EZO_ void EZOSensor::get_t() { this->add_command_("T,?", EzoCommandType::EZO_T); } void EZOSensor::set_t(float value) { - std::string payload = str_sprintf("T,%0.2f", value); + // max 14 bytes: "T,"(2) + float with "%0.2f" (up to 11 chars) + null(1); use 16 for alignment + char payload[16]; + snprintf(payload, sizeof(payload), "T,%0.2f", value); this->add_command_(payload, EzoCommandType::EZO_T); } @@ -215,7 +221,9 @@ void EZOSensor::set_calibration_point_high(float value) { } void EZOSensor::set_calibration_generic(float value) { - std::string payload = str_sprintf("Cal,%0.2f", value); + // exact 16 bytes: "Cal," (4) + float with "%0.2f" (up to 11 chars, e.g. "-9999999.99") + null (1) = 16 + char payload[16]; + snprintf(payload, sizeof(payload), "Cal,%0.2f", value); this->add_command_(payload, EzoCommandType::EZO_CALIBRATION, 900); } @@ -223,13 +231,11 @@ void EZOSensor::clear_calibration() { this->add_command_("Cal,clear", EzoCommand void EZOSensor::get_led_state() { this->add_command_("L,?", EzoCommandType::EZO_LED); } -void EZOSensor::set_led_state(bool on) { - std::string to_send = "L,"; - to_send += on ? "1" : "0"; - this->add_command_(to_send, EzoCommandType::EZO_LED); -} +void EZOSensor::set_led_state(bool on) { this->add_command_(on ? "L,1" : "L,0", EzoCommandType::EZO_LED); } -void EZOSensor::send_custom(const std::string &to_send) { this->add_command_(to_send, EzoCommandType::EZO_CUSTOM); } +void EZOSensor::send_custom(const std::string &to_send) { + this->add_command_(to_send.c_str(), EzoCommandType::EZO_CUSTOM); +} } // namespace ezo } // namespace esphome diff --git a/esphome/components/ezo/ezo.h b/esphome/components/ezo/ezo.h index 00dd98fc80..f1a2802cbd 100644 --- a/esphome/components/ezo/ezo.h +++ b/esphome/components/ezo/ezo.h @@ -92,7 +92,7 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2 std::deque> commands_; int new_address_; - void add_command_(const std::string &command, EzoCommandType command_type, uint16_t delay_ms = 300); + void add_command_(const char *command, EzoCommandType command_type, uint16_t delay_ms = 300); void set_calibration_point_(EzoCalibrationType type, float value); diff --git a/esphome/components/gdk101/gdk101.cpp b/esphome/components/gdk101/gdk101.cpp index 617e2138fb..ddf38f2f55 100644 --- a/esphome/components/gdk101/gdk101.cpp +++ b/esphome/components/gdk101/gdk101.cpp @@ -163,9 +163,10 @@ bool GDK101Component::read_fw_version_(uint8_t *data) { return false; } - const std::string fw_version_str = str_sprintf("%d.%d", data[0], data[1]); - - this->fw_version_text_sensor_->publish_state(fw_version_str); + // max 8: "255.255" (7 chars) + null + char buf[8]; + snprintf(buf, sizeof(buf), "%d.%d", data[0], data[1]); + this->fw_version_text_sensor_->publish_state(buf); } #endif // USE_TEXT_SENSOR return true; diff --git a/esphome/components/homeassistant/number/homeassistant_number.cpp b/esphome/components/homeassistant/number/homeassistant_number.cpp index 92ecd5ea39..00ea88ff16 100644 --- a/esphome/components/homeassistant/number/homeassistant_number.cpp +++ b/esphome/components/homeassistant/number/homeassistant_number.cpp @@ -97,7 +97,7 @@ void HomeassistantNumber::control(float value) { entity_value.key = VALUE_KEY; // Stack buffer - no heap allocation; %g produces shortest representation char value_buf[16]; - snprintf(value_buf, sizeof(value_buf), "%g", value); + buf_append_printf(value_buf, sizeof(value_buf), 0, "%g", value); entity_value.value = StringRef(value_buf); api::global_api_server->send_homeassistant_action(resp); diff --git a/esphome/components/i2c/i2c.cpp b/esphome/components/i2c/i2c.cpp index f8c7a1b40b..c1e7336ce4 100644 --- a/esphome/components/i2c/i2c.cpp +++ b/esphome/components/i2c/i2c.cpp @@ -42,8 +42,8 @@ ErrorCode I2CDevice::read_register16(uint16_t a_register, uint8_t *data, size_t } ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len) const { - SmallBufferWithHeapFallback<17> buffer_alloc; // Most I2C writes are <= 16 bytes - uint8_t *buffer = buffer_alloc.get(len + 1); + SmallBufferWithHeapFallback<17> buffer_alloc(len + 1); // Most I2C writes are <= 16 bytes + uint8_t *buffer = buffer_alloc.get(); buffer[0] = a_register; std::copy(data, data + len, buffer + 1); @@ -51,8 +51,8 @@ ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, siz } ErrorCode I2CDevice::write_register16(uint16_t a_register, const uint8_t *data, size_t len) const { - SmallBufferWithHeapFallback<18> buffer_alloc; // Most I2C writes are <= 16 bytes + 2 for register - uint8_t *buffer = buffer_alloc.get(len + 2); + SmallBufferWithHeapFallback<18> buffer_alloc(len + 2); // Most I2C writes are <= 16 bytes + 2 for register + uint8_t *buffer = buffer_alloc.get(); buffer[0] = a_register >> 8; buffer[1] = a_register; diff --git a/esphome/components/i2c/i2c_bus.h b/esphome/components/i2c/i2c_bus.h index 1acbe506a3..3de5d5ca7b 100644 --- a/esphome/components/i2c/i2c_bus.h +++ b/esphome/components/i2c/i2c_bus.h @@ -11,22 +11,6 @@ namespace esphome { namespace i2c { -/// @brief Helper class for efficient buffer allocation - uses stack for small sizes, heap for large -template class SmallBufferWithHeapFallback { - public: - uint8_t *get(size_t size) { - if (size <= STACK_SIZE) { - return this->stack_buffer_; - } - this->heap_buffer_ = std::unique_ptr(new uint8_t[size]); - return this->heap_buffer_.get(); - } - - private: - uint8_t stack_buffer_[STACK_SIZE]; - std::unique_ptr heap_buffer_; -}; - /// @brief Error codes returned by I2CBus and I2CDevice methods enum ErrorCode { NO_ERROR = 0, ///< No error found during execution of method @@ -92,8 +76,8 @@ class I2CBus { total_len += read_buffers[i].len; } - SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C reads are small - uint8_t *buffer = buffer_alloc.get(total_len); + SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C reads are small + uint8_t *buffer = buffer_alloc.get(); auto err = this->write_readv(address, nullptr, 0, buffer, total_len); if (err != ERROR_OK) @@ -116,8 +100,8 @@ class I2CBus { total_len += write_buffers[i].len; } - SmallBufferWithHeapFallback<128> buffer_alloc; // Most I2C writes are small - uint8_t *buffer = buffer_alloc.get(total_len); + SmallBufferWithHeapFallback<128> buffer_alloc(total_len); // Most I2C writes are small + uint8_t *buffer = buffer_alloc.get(); size_t pos = 0; for (size_t i = 0; i != count; i++) { diff --git a/esphome/components/max6956/max6956.cpp b/esphome/components/max6956/max6956.cpp index 13fe5a5323..6ba17f11d1 100644 --- a/esphome/components/max6956/max6956.cpp +++ b/esphome/components/max6956/max6956.cpp @@ -162,7 +162,7 @@ void MAX6956GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this- bool MAX6956GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void MAX6956GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } size_t MAX6956GPIOPin::dump_summary(char *buffer, size_t len) const { - return snprintf(buffer, len, "%u via Max6956", this->pin_); + return buf_append_printf(buffer, len, 0, "%u via Max6956", this->pin_); } } // namespace max6956 diff --git a/esphome/components/mcp23016/mcp23016.cpp b/esphome/components/mcp23016/mcp23016.cpp index 87c2668962..56b2ecf9f4 100644 --- a/esphome/components/mcp23016/mcp23016.cpp +++ b/esphome/components/mcp23016/mcp23016.cpp @@ -100,7 +100,7 @@ void MCP23016GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this bool MCP23016GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void MCP23016GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } size_t MCP23016GPIOPin::dump_summary(char *buffer, size_t len) const { - return snprintf(buffer, len, "%u via MCP23016", this->pin_); + return buf_append_printf(buffer, len, 0, "%u via MCP23016", this->pin_); } } // namespace mcp23016 diff --git a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp index 302f6b8280..535119fc5c 100644 --- a/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp +++ b/esphome/components/mcp23xxx_base/mcp23xxx_base.cpp @@ -17,7 +17,7 @@ template void MCP23XXXGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } template size_t MCP23XXXGPIOPin::dump_summary(char *buffer, size_t len) const { - return snprintf(buffer, len, "%u via MCP23XXX", this->pin_); + return buf_append_printf(buffer, len, 0, "%u via MCP23XXX", this->pin_); } template class MCP23XXXGPIOPin<8>; diff --git a/esphome/components/mpr121/mpr121.cpp b/esphome/components/mpr121/mpr121.cpp index 4b358e384c..cd9c81fe03 100644 --- a/esphome/components/mpr121/mpr121.cpp +++ b/esphome/components/mpr121/mpr121.cpp @@ -154,7 +154,7 @@ void MPR121GPIOPin::digital_write(bool value) { } size_t MPR121GPIOPin::dump_summary(char *buffer, size_t len) const { - return snprintf(buffer, len, "ELE%u on MPR121", this->pin_); + return buf_append_printf(buffer, len, 0, "ELE%u on MPR121", this->pin_); } } // namespace mpr121 diff --git a/esphome/components/mqtt/mqtt_client.cpp b/esphome/components/mqtt/mqtt_client.cpp index 0ab5b238b5..7d4050284f 100644 --- a/esphome/components/mqtt/mqtt_client.cpp +++ b/esphome/components/mqtt/mqtt_client.cpp @@ -635,7 +635,8 @@ void MQTTClientComponent::set_log_message_template(MQTTMessage &&message) { this const MQTTDiscoveryInfo &MQTTClientComponent::get_discovery_info() const { return this->discovery_info_; } void MQTTClientComponent::set_topic_prefix(const std::string &topic_prefix, const std::string &check_topic_prefix) { if (App.is_name_add_mac_suffix_enabled() && (topic_prefix == check_topic_prefix)) { - this->topic_prefix_ = str_sanitize(App.get_name()); + char buf[ESPHOME_DEVICE_NAME_MAX_LEN + 1]; + this->topic_prefix_ = str_sanitize_to(buf, App.get_name().c_str()); } else { this->topic_prefix_ = topic_prefix; } diff --git a/esphome/components/mqtt/mqtt_component.cpp b/esphome/components/mqtt/mqtt_component.cpp index 8e4b3437ab..3b9290259b 100644 --- a/esphome/components/mqtt/mqtt_component.cpp +++ b/esphome/components/mqtt/mqtt_component.cpp @@ -48,7 +48,8 @@ void MQTTComponent::set_subscribe_qos(uint8_t qos) { this->subscribe_qos_ = qos; void MQTTComponent::set_retain(bool retain) { this->retain_ = retain; } std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discovery_info) const { - std::string sanitized_name = str_sanitize(App.get_name()); + char sanitized_name[ESPHOME_DEVICE_NAME_MAX_LEN + 1]; + str_sanitize_to(sanitized_name, App.get_name().c_str()); const char *comp_type = this->component_type(); char object_id_buf[OBJECT_ID_MAX_LEN]; StringRef object_id = this->get_default_object_id_to_(object_id_buf); @@ -60,7 +61,7 @@ std::string MQTTComponent::get_discovery_topic_(const MQTTDiscoveryInfo &discove p = append_char(p, '/'); p = append_str(p, comp_type, strlen(comp_type)); p = append_char(p, '/'); - p = append_str(p, sanitized_name.data(), sanitized_name.size()); + p = append_str(p, sanitized_name, strlen(sanitized_name)); p = append_char(p, '/'); p = append_str(p, object_id.c_str(), object_id.size()); p = append_str(p, "/config", 7); diff --git a/esphome/components/nextion/nextion.cpp b/esphome/components/nextion/nextion.cpp index d77af510d7..354288e1a3 100644 --- a/esphome/components/nextion/nextion.cpp +++ b/esphome/components/nextion/nextion.cpp @@ -1,6 +1,7 @@ #include "nextion.h" #include #include "esphome/core/application.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" #include "esphome/core/util.h" @@ -1283,8 +1284,9 @@ void Nextion::check_pending_waveform_() { size_t buffer_to_send = component->get_wave_buffer_size() < 255 ? component->get_wave_buffer_size() : 255; // ADDT command can only send 255 - std::string command = "addt " + to_string(component->get_component_id()) + "," + - to_string(component->get_wave_channel_id()) + "," + to_string(buffer_to_send); + char command[24]; // "addt " + uint8 + "," + uint8 + "," + uint8 + null = max 17 chars + buf_append_printf(command, sizeof(command), 0, "addt %u,%u,%zu", component->get_component_id(), + component->get_wave_channel_id(), buffer_to_send); if (!this->send_command_(command)) { delete nb; // NOLINT(cppcoreguidelines-owning-memory) this->waveform_queue_.pop_front(); diff --git a/esphome/components/nextion/nextion_upload_arduino.cpp b/esphome/components/nextion/nextion_upload_arduino.cpp index d210bad004..220c75f9d3 100644 --- a/esphome/components/nextion/nextion_upload_arduino.cpp +++ b/esphome/components/nextion/nextion_upload_arduino.cpp @@ -34,7 +34,7 @@ int Nextion::upload_by_chunks_(HTTPClient &http_client, uint32_t &range_start) { } char range_header[32]; - sprintf(range_header, "bytes=%" PRIu32 "-%" PRIu32, range_start, range_end); + buf_append_printf(range_header, sizeof(range_header), 0, "bytes=%" PRIu32 "-%" PRIu32, range_start, range_end); ESP_LOGV(TAG, "Range: %s", range_header); http_client.addHeader("Range", range_header); int code = http_client.GET(); diff --git a/esphome/components/nextion/nextion_upload_esp32.cpp b/esphome/components/nextion/nextion_upload_esp32.cpp index 712fa8e78e..c4e6ff7182 100644 --- a/esphome/components/nextion/nextion_upload_esp32.cpp +++ b/esphome/components/nextion/nextion_upload_esp32.cpp @@ -36,7 +36,7 @@ int Nextion::upload_by_chunks_(esp_http_client_handle_t http_client, uint32_t &r } char range_header[32]; - sprintf(range_header, "bytes=%" PRIu32 "-%" PRIu32, range_start, range_end); + buf_append_printf(range_header, sizeof(range_header), 0, "bytes=%" PRIu32 "-%" PRIu32, range_start, range_end); ESP_LOGV(TAG, "Range: %s", range_header); esp_http_client_set_header(http_client, "Range", range_header); ESP_LOGV(TAG, "Open HTTP"); diff --git a/esphome/components/pca6416a/pca6416a.cpp b/esphome/components/pca6416a/pca6416a.cpp index 909bac5f05..f393af88ce 100644 --- a/esphome/components/pca6416a/pca6416a.cpp +++ b/esphome/components/pca6416a/pca6416a.cpp @@ -181,7 +181,7 @@ void PCA6416AGPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this bool PCA6416AGPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void PCA6416AGPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } size_t PCA6416AGPIOPin::dump_summary(char *buffer, size_t len) const { - return snprintf(buffer, len, "%u via PCA6416A", this->pin_); + return buf_append_printf(buffer, len, 0, "%u via PCA6416A", this->pin_); } } // namespace pca6416a diff --git a/esphome/components/pca9554/pca9554.cpp b/esphome/components/pca9554/pca9554.cpp index a6f9c2396c..c574ce6593 100644 --- a/esphome/components/pca9554/pca9554.cpp +++ b/esphome/components/pca9554/pca9554.cpp @@ -130,7 +130,7 @@ void PCA9554GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this- bool PCA9554GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void PCA9554GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } size_t PCA9554GPIOPin::dump_summary(char *buffer, size_t len) const { - return snprintf(buffer, len, "%u via PCA9554", this->pin_); + return buf_append_printf(buffer, len, 0, "%u via PCA9554", this->pin_); } } // namespace pca9554 diff --git a/esphome/components/pcf8574/pcf8574.cpp b/esphome/components/pcf8574/pcf8574.cpp index 8bdd312ab9..b7d3848f0e 100644 --- a/esphome/components/pcf8574/pcf8574.cpp +++ b/esphome/components/pcf8574/pcf8574.cpp @@ -107,7 +107,7 @@ void PCF8574GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this- bool PCF8574GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void PCF8574GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } size_t PCF8574GPIOPin::dump_summary(char *buffer, size_t len) const { - return snprintf(buffer, len, "%u via PCF8574", this->pin_); + return buf_append_printf(buffer, len, 0, "%u via PCF8574", this->pin_); } } // namespace pcf8574 diff --git a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp index f3a1f013d9..fdff11dedb 100644 --- a/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp +++ b/esphome/components/pi4ioe5v6408/pi4ioe5v6408.cpp @@ -165,7 +165,7 @@ void PI4IOE5V6408GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } size_t PI4IOE5V6408GPIOPin::dump_summary(char *buffer, size_t len) const { - return snprintf(buffer, len, "%u via PI4IOE5V6408", this->pin_); + return buf_append_printf(buffer, len, 0, "%u via PI4IOE5V6408", this->pin_); } } // namespace pi4ioe5v6408 diff --git a/esphome/components/rc522_spi/rc522_spi.cpp b/esphome/components/rc522_spi/rc522_spi.cpp index 23e92be65a..40da449814 100644 --- a/esphome/components/rc522_spi/rc522_spi.cpp +++ b/esphome/components/rc522_spi/rc522_spi.cpp @@ -1,4 +1,5 @@ #include "rc522_spi.h" +#include "esphome/core/helpers.h" #include "esphome/core/log.h" // Based on: @@ -70,7 +71,7 @@ void RC522Spi::pcd_read_register(PcdRegister reg, ///< The register to read fro index++; #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - sprintf(cstrb, " %x", values[0]); + buf_append_printf(cstrb, sizeof(cstrb), 0, " %x", values[0]); buf.append(cstrb); #endif } @@ -78,7 +79,7 @@ void RC522Spi::pcd_read_register(PcdRegister reg, ///< The register to read fro values[index] = transfer_byte(address); // Read value and tell that we want to read the same address again. #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - sprintf(cstrb, " %x", values[index]); + buf_append_printf(cstrb, sizeof(cstrb), 0, " %x", values[index]); buf.append(cstrb); #endif @@ -88,7 +89,7 @@ void RC522Spi::pcd_read_register(PcdRegister reg, ///< The register to read fro #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE buf = buf + " "; - sprintf(cstrb, "%x", values[index]); + buf_append_printf(cstrb, sizeof(cstrb), 0, "%x", values[index]); buf.append(cstrb); ESP_LOGVV(TAG, "read_register_array_(%x, %d, , %d) -> %s", reg, count, rx_align, buf.c_str()); @@ -127,7 +128,7 @@ void RC522Spi::pcd_write_register(PcdRegister reg, ///< The register to write t transfer_byte(values[index]); #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE - sprintf(cstrb, " %x", values[index]); + buf_append_printf(cstrb, sizeof(cstrb), 0, " %x", values[index]); buf.append(cstrb); #endif } diff --git a/esphome/components/sml/sml_parser.cpp b/esphome/components/sml/sml_parser.cpp index 85e5a2da03..16e37949dc 100644 --- a/esphome/components/sml/sml_parser.cpp +++ b/esphome/components/sml/sml_parser.cpp @@ -104,7 +104,10 @@ std::vector SmlFile::get_obis_info() { std::string bytes_repr(const BytesView &buffer) { std::string repr; for (auto const value : buffer) { - repr += str_sprintf("%02x", value & 0xff); + // max 3: 2 hex digits + null + char hex_buf[3]; + snprintf(hex_buf, sizeof(hex_buf), "%02x", static_cast(value)); + repr += hex_buf; } return repr; } @@ -146,7 +149,11 @@ ObisInfo::ObisInfo(const BytesView &server_id, const SmlNode &val_list_entry) : } std::string ObisInfo::code_repr() const { - return str_sprintf("%d-%d:%d.%d.%d", this->code[0], this->code[1], this->code[2], this->code[3], this->code[4]); + // max 20: "255-255:255.255.255" (19 chars) + null + char buf[20]; + snprintf(buf, sizeof(buf), "%d-%d:%d.%d.%d", this->code[0], this->code[1], this->code[2], this->code[3], + this->code[4]); + return buf; } } // namespace sml diff --git a/esphome/components/sn74hc165/sn74hc165.cpp b/esphome/components/sn74hc165/sn74hc165.cpp index 718e0b86ed..63b3f98521 100644 --- a/esphome/components/sn74hc165/sn74hc165.cpp +++ b/esphome/components/sn74hc165/sn74hc165.cpp @@ -65,7 +65,7 @@ float SN74HC165Component::get_setup_priority() const { return setup_priority::IO bool SN74HC165GPIOPin::digital_read() { return this->parent_->digital_read_(this->pin_) != this->inverted_; } size_t SN74HC165GPIOPin::dump_summary(char *buffer, size_t len) const { - return snprintf(buffer, len, "%u via SN74HC165", this->pin_); + return buf_append_printf(buffer, len, 0, "%u via SN74HC165", this->pin_); } } // namespace sn74hc165 diff --git a/esphome/components/sn74hc595/sn74hc595.cpp b/esphome/components/sn74hc595/sn74hc595.cpp index 6b5c5d9fc4..1bb8c7936d 100644 --- a/esphome/components/sn74hc595/sn74hc595.cpp +++ b/esphome/components/sn74hc595/sn74hc595.cpp @@ -94,7 +94,7 @@ void SN74HC595GPIOPin::digital_write(bool value) { this->parent_->digital_write_(this->pin_, value != this->inverted_); } size_t SN74HC595GPIOPin::dump_summary(char *buffer, size_t len) const { - return snprintf(buffer, len, "%u via SN74HC595", this->pin_); + return buf_append_printf(buffer, len, 0, "%u via SN74HC595", this->pin_); } } // namespace sn74hc595 diff --git a/esphome/components/socket/socket.cpp b/esphome/components/socket/socket.cpp index c92e33393b..bb94ecb675 100644 --- a/esphome/components/socket/socket.cpp +++ b/esphome/components/socket/socket.cpp @@ -107,9 +107,9 @@ std::unique_ptr socket_ip_loop_monitored(int type, int protocol) { #endif /* USE_NETWORK_IPV6 */ } -socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::string &ip_address, uint16_t port) { +socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_address, uint16_t port) { #if USE_NETWORK_IPV6 - if (ip_address.find(':') != std::string::npos) { + if (strchr(ip_address, ':') != nullptr) { if (addrlen < sizeof(sockaddr_in6)) { errno = EINVAL; return 0; @@ -121,14 +121,14 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::stri #ifdef USE_SOCKET_IMPL_BSD_SOCKETS // Use standard inet_pton for BSD sockets - if (inet_pton(AF_INET6, ip_address.c_str(), &server->sin6_addr) != 1) { + if (inet_pton(AF_INET6, ip_address, &server->sin6_addr) != 1) { errno = EINVAL; return 0; } #else // Use LWIP-specific functions ip6_addr_t ip6; - inet6_aton(ip_address.c_str(), &ip6); + inet6_aton(ip_address, &ip6); memcpy(server->sin6_addr.un.u32_addr, ip6.addr, sizeof(ip6.addr)); #endif return sizeof(sockaddr_in6); @@ -141,7 +141,7 @@ socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::stri auto *server = reinterpret_cast(addr); memset(server, 0, sizeof(sockaddr_in)); server->sin_family = AF_INET; - server->sin_addr.s_addr = inet_addr(ip_address.c_str()); + server->sin_addr.s_addr = inet_addr(ip_address); server->sin_port = htons(port); return sizeof(sockaddr_in); } diff --git a/esphome/components/socket/socket.h b/esphome/components/socket/socket.h index 9f9f61de85..d74804fdb0 100644 --- a/esphome/components/socket/socket.h +++ b/esphome/components/socket/socket.h @@ -87,7 +87,17 @@ std::unique_ptr socket_loop_monitored(int domain, int type, int protocol std::unique_ptr socket_ip_loop_monitored(int type, int protocol); /// Set a sockaddr to the specified address and port for the IP version used by socket_ip(). -socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::string &ip_address, uint16_t port); +/// @param addr Destination sockaddr structure +/// @param addrlen Size of the addr buffer +/// @param ip_address Null-terminated IP address string (IPv4 or IPv6) +/// @param port Port number in host byte order +/// @return Size of the sockaddr structure used, or 0 on error +socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const char *ip_address, uint16_t port); + +/// Convenience overload for std::string (backward compatible). +inline socklen_t set_sockaddr(struct sockaddr *addr, socklen_t addrlen, const std::string &ip_address, uint16_t port) { + return set_sockaddr(addr, addrlen, ip_address.c_str(), port); +} /// Set a sockaddr to the any address and specified port for the IP version used by socket_ip(). socklen_t set_sockaddr_any(struct sockaddr *addr, socklen_t addrlen, uint16_t port); diff --git a/esphome/components/status/binary_sensor.py b/esphome/components/status/binary_sensor.py index c1a4a52ce2..f0c7c87e17 100644 --- a/esphome/components/status/binary_sensor.py +++ b/esphome/components/status/binary_sensor.py @@ -7,14 +7,14 @@ DEPENDENCIES = ["network"] status_ns = cg.esphome_ns.namespace("status") StatusBinarySensor = status_ns.class_( - "StatusBinarySensor", binary_sensor.BinarySensor, cg.Component + "StatusBinarySensor", binary_sensor.BinarySensor, cg.PollingComponent ) CONFIG_SCHEMA = binary_sensor.binary_sensor_schema( StatusBinarySensor, device_class=DEVICE_CLASS_CONNECTIVITY, entity_category=ENTITY_CATEGORY_DIAGNOSTIC, -).extend(cv.COMPONENT_SCHEMA) +).extend(cv.polling_component_schema("1s")) async def to_code(config): diff --git a/esphome/components/status/status_binary_sensor.cpp b/esphome/components/status/status_binary_sensor.cpp index 1795a9c41b..2c95be8569 100644 --- a/esphome/components/status/status_binary_sensor.cpp +++ b/esphome/components/status/status_binary_sensor.cpp @@ -10,12 +10,11 @@ #include "esphome/components/api/api_server.h" #endif -namespace esphome { -namespace status { +namespace esphome::status { static const char *const TAG = "status"; -void StatusBinarySensor::loop() { +void StatusBinarySensor::update() { bool status = network::is_connected(); #ifdef USE_MQTT if (mqtt::global_mqtt_client != nullptr) { @@ -33,5 +32,4 @@ void StatusBinarySensor::loop() { void StatusBinarySensor::setup() { this->publish_initial_state(false); } void StatusBinarySensor::dump_config() { LOG_BINARY_SENSOR("", "Status Binary Sensor", this); } -} // namespace status -} // namespace esphome +} // namespace esphome::status diff --git a/esphome/components/status/status_binary_sensor.h b/esphome/components/status/status_binary_sensor.h index feda8b6328..7e8c31d741 100644 --- a/esphome/components/status/status_binary_sensor.h +++ b/esphome/components/status/status_binary_sensor.h @@ -3,12 +3,11 @@ #include "esphome/core/component.h" #include "esphome/components/binary_sensor/binary_sensor.h" -namespace esphome { -namespace status { +namespace esphome::status { -class StatusBinarySensor : public binary_sensor::BinarySensor, public Component { +class StatusBinarySensor : public binary_sensor::BinarySensor, public PollingComponent { public: - void loop() override; + void update() override; void setup() override; void dump_config() override; @@ -16,5 +15,4 @@ class StatusBinarySensor : public binary_sensor::BinarySensor, public Component bool is_status_binary_sensor() const override { return true; } }; -} // namespace status -} // namespace esphome +} // namespace esphome::status diff --git a/esphome/components/sx1509/sx1509_gpio_pin.cpp b/esphome/components/sx1509/sx1509_gpio_pin.cpp index 41a99eba4b..a7e5d0514d 100644 --- a/esphome/components/sx1509/sx1509_gpio_pin.cpp +++ b/esphome/components/sx1509/sx1509_gpio_pin.cpp @@ -13,7 +13,7 @@ void SX1509GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this-> bool SX1509GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void SX1509GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } size_t SX1509GPIOPin::dump_summary(char *buffer, size_t len) const { - return snprintf(buffer, len, "%u via sx1509", this->pin_); + return buf_append_printf(buffer, len, 0, "%u via sx1509", this->pin_); } } // namespace sx1509 diff --git a/esphome/components/syslog/esphome_syslog.cpp b/esphome/components/syslog/esphome_syslog.cpp index 83ad6b2720..376de54db4 100644 --- a/esphome/components/syslog/esphome_syslog.cpp +++ b/esphome/components/syslog/esphome_syslog.cpp @@ -47,29 +47,27 @@ void Syslog::log_(const int level, const char *tag, const char *message, size_t size_t remaining = sizeof(packet); // Write PRI - abort if this fails as packet would be malformed - int ret = snprintf(packet, remaining, "<%d>", pri); - if (ret <= 0 || static_cast(ret) >= remaining) { - return; + offset = buf_append_printf(packet, sizeof(packet), 0, "<%d>", pri); + if (offset == 0) { + return; // PRI always produces at least "<0>" (3 chars), so 0 means error } - offset = ret; - remaining -= ret; + remaining -= offset; // Write timestamp directly into packet (RFC 5424: use "-" if time not valid or strftime fails) auto now = this->time_->now(); size_t ts_written = now.is_valid() ? now.strftime(packet + offset, remaining, "%b %e %H:%M:%S") : 0; if (ts_written > 0) { offset += ts_written; - remaining -= ts_written; } else if (remaining > 0) { packet[offset++] = '-'; - remaining--; } // Write hostname, tag, and message - ret = snprintf(packet + offset, remaining, " %s %s: %.*s", App.get_name().c_str(), tag, (int) len, message); - if (ret > 0) { - // snprintf returns chars that would be written; clamp to actual buffer space - offset += std::min(static_cast(ret), remaining > 0 ? remaining - 1 : 0); + offset = buf_append_printf(packet, sizeof(packet), offset, " %s %s: %.*s", App.get_name().c_str(), tag, (int) len, + message); + // Clamp to exclude null terminator position if buffer was filled + if (offset >= sizeof(packet)) { + offset = sizeof(packet) - 1; } if (offset > 0) { diff --git a/esphome/components/tca9555/tca9555.cpp b/esphome/components/tca9555/tca9555.cpp index 376de6a370..79c5253898 100644 --- a/esphome/components/tca9555/tca9555.cpp +++ b/esphome/components/tca9555/tca9555.cpp @@ -139,7 +139,7 @@ void TCA9555GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this- bool TCA9555GPIOPin::digital_read() { return this->parent_->digital_read(this->pin_) != this->inverted_; } void TCA9555GPIOPin::digital_write(bool value) { this->parent_->digital_write(this->pin_, value != this->inverted_); } size_t TCA9555GPIOPin::dump_summary(char *buffer, size_t len) const { - return snprintf(buffer, len, "%u via TCA9555", this->pin_); + return buf_append_printf(buffer, len, 0, "%u via TCA9555", this->pin_); } } // namespace tca9555 diff --git a/esphome/components/tormatic/tormatic_protocol.h b/esphome/components/tormatic/tormatic_protocol.h index e26535e985..057713b884 100644 --- a/esphome/components/tormatic/tormatic_protocol.h +++ b/esphome/components/tormatic/tormatic_protocol.h @@ -55,6 +55,7 @@ enum MessageType : uint16_t { COMMAND = 0x0106, }; +// Max string length: 7 ("Unknown"/"Command"). Update print() buffer sizes if adding longer strings. inline const char *message_type_to_str(MessageType t) { switch (t) { case STATUS: @@ -83,7 +84,11 @@ struct MessageHeader { } std::string print() { - return str_sprintf("MessageHeader: seq %d, len %d, type %s", this->seq, this->len, message_type_to_str(this->type)); + // 64 bytes: "MessageHeader: seq " + uint16 + ", len " + uint32 + ", type " + type + safety margin + char buf[64]; + buf_append_printf(buf, sizeof(buf), 0, "MessageHeader: seq %d, len %d, type %s", this->seq, this->len, + message_type_to_str(this->type)); + return buf; } void byteswap() { @@ -131,6 +136,7 @@ inline CoverOperation gate_status_to_cover_operation(GateStatus s) { return COVER_OPERATION_IDLE; } +// Max string length: 11 ("Ventilating"). Update print() buffer sizes if adding longer strings. inline const char *gate_status_to_str(GateStatus s) { switch (s) { case PAUSED: @@ -170,7 +176,12 @@ struct StatusReply { GateStatus state; uint8_t trailer = 0x0; - std::string print() { return str_sprintf("StatusReply: state %s", gate_status_to_str(this->state)); } + std::string print() { + // 48 bytes: "StatusReply: state " (19) + state (11) + safety margin + char buf[48]; + buf_append_printf(buf, sizeof(buf), 0, "StatusReply: state %s", gate_status_to_str(this->state)); + return buf; + } void byteswap(){}; } __attribute__((packed)); @@ -202,7 +213,12 @@ struct CommandRequestReply { CommandRequestReply() = default; CommandRequestReply(GateStatus state) { this->state = state; } - std::string print() { return str_sprintf("CommandRequestReply: state %s", gate_status_to_str(this->state)); } + std::string print() { + // 56 bytes: "CommandRequestReply: state " (27) + state (11) + safety margin + char buf[56]; + buf_append_printf(buf, sizeof(buf), 0, "CommandRequestReply: state %s", gate_status_to_str(this->state)); + return buf; + } void byteswap() { this->type = convert_big_endian(this->type); } } __attribute__((packed)); diff --git a/esphome/components/toshiba/toshiba.cpp b/esphome/components/toshiba/toshiba.cpp index 5efa70d6b4..7b5e78af52 100644 --- a/esphome/components/toshiba/toshiba.cpp +++ b/esphome/components/toshiba/toshiba.cpp @@ -1,5 +1,6 @@ #include "toshiba.h" #include "esphome/components/remote_base/toshiba_ac_protocol.h" +#include "esphome/core/helpers.h" #include @@ -427,10 +428,17 @@ void ToshibaClimate::setup() { // Never send nan to HA if (std::isnan(this->target_temperature)) this->target_temperature = 24; +#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE // Log final state for debugging HA errors - ESP_LOGV(TAG, "Setup complete - Mode: %d, Fan: %s, Swing: %d, Temp: %.1f", static_cast(this->mode), - this->fan_mode.has_value() ? std::to_string(static_cast(this->fan_mode.value())).c_str() : "NONE", + const char *fan_mode_str = "NONE"; + char fan_mode_buf[4]; // max 3 digits for fan mode enum + null + if (this->fan_mode.has_value()) { + buf_append_printf(fan_mode_buf, sizeof(fan_mode_buf), 0, "%d", static_cast(this->fan_mode.value())); + fan_mode_str = fan_mode_buf; + } + ESP_LOGV(TAG, "Setup complete - Mode: %d, Fan: %s, Swing: %d, Temp: %.1f", static_cast(this->mode), fan_mode_str, static_cast(this->swing_mode), this->target_temperature); +#endif } void ToshibaClimate::transmit_state() { diff --git a/esphome/components/tuya/light/tuya_light.cpp b/esphome/components/tuya/light/tuya_light.cpp index c487f9f50b..097b3c1af8 100644 --- a/esphome/components/tuya/light/tuya_light.cpp +++ b/esphome/components/tuya/light/tuya_light.cpp @@ -191,7 +191,7 @@ void TuyaLight::write_state(light::LightState *state) { case TuyaColorType::RGB: { char buffer[7]; const char *format_str = this->color_type_lowercase_ ? "%02x%02x%02x" : "%02X%02X%02X"; - sprintf(buffer, format_str, int(red * 255), int(green * 255), int(blue * 255)); + snprintf(buffer, sizeof(buffer), format_str, int(red * 255), int(green * 255), int(blue * 255)); color_value = buffer; break; } @@ -201,7 +201,7 @@ void TuyaLight::write_state(light::LightState *state) { rgb_to_hsv(red, green, blue, hue, saturation, value); char buffer[13]; const char *format_str = this->color_type_lowercase_ ? "%04x%04x%04x" : "%04X%04X%04X"; - sprintf(buffer, format_str, hue, int(saturation * 1000), int(value * 1000)); + snprintf(buffer, sizeof(buffer), format_str, hue, int(saturation * 1000), int(value * 1000)); color_value = buffer; break; } @@ -211,8 +211,8 @@ void TuyaLight::write_state(light::LightState *state) { rgb_to_hsv(red, green, blue, hue, saturation, value); char buffer[15]; const char *format_str = this->color_type_lowercase_ ? "%02x%02x%02x%04x%02x%02x" : "%02X%02X%02X%04X%02X%02X"; - sprintf(buffer, format_str, int(red * 255), int(green * 255), int(blue * 255), hue, int(saturation * 255), - int(value * 255)); + snprintf(buffer, sizeof(buffer), format_str, int(red * 255), int(green * 255), int(blue * 255), hue, + int(saturation * 255), int(value * 255)); color_value = buffer; break; } diff --git a/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp index 36b6d630ae..b15fb6f85a 100644 --- a/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp +++ b/esphome/components/tuya/text_sensor/tuya_text_sensor.cpp @@ -24,7 +24,7 @@ void TuyaTextSensor::setup() { } case TuyaDatapointType::ENUM: { char buf[4]; // uint8_t max is 3 digits + null - snprintf(buf, sizeof(buf), "%u", datapoint.value_enum); + buf_append_printf(buf, sizeof(buf), 0, "%u", datapoint.value_enum); ESP_LOGD(TAG, "MCU reported text sensor %u is: %s", datapoint.id, buf); this->publish_state(buf); break; diff --git a/esphome/components/uart/uart_debugger.cpp b/esphome/components/uart/uart_debugger.cpp index b51a57d68e..5490154d01 100644 --- a/esphome/components/uart/uart_debugger.cpp +++ b/esphome/components/uart/uart_debugger.cpp @@ -107,7 +107,7 @@ void UARTDebug::log_hex(UARTDirection direction, std::vector bytes, uin if (i > 0) { res += separator; } - sprintf(buf, "%02X", bytes[i]); + buf_append_printf(buf, sizeof(buf), 0, "%02X", bytes[i]); res += buf; } ESP_LOGD(TAG, "%s", res.c_str()); @@ -147,7 +147,7 @@ void UARTDebug::log_string(UARTDirection direction, std::vector bytes) } else if (bytes[i] == 92) { res += "\\\\"; } else if (bytes[i] < 32 || bytes[i] > 127) { - sprintf(buf, "\\x%02X", bytes[i]); + buf_append_printf(buf, sizeof(buf), 0, "\\x%02X", bytes[i]); res += buf; } else { res += bytes[i]; @@ -166,11 +166,13 @@ void UARTDebug::log_int(UARTDirection direction, std::vector bytes, uin } else { res += ">>> "; } + char buf[4]; // max 3 digits for uint8_t (255) + null for (size_t i = 0; i < len; i++) { if (i > 0) { res += separator; } - res += to_string(bytes[i]); + buf_append_printf(buf, sizeof(buf), 0, "%u", bytes[i]); + res += buf; } ESP_LOGD(TAG, "%s", res.c_str()); delay(10); @@ -189,7 +191,7 @@ void UARTDebug::log_binary(UARTDirection direction, std::vector bytes, if (i > 0) { res += separator; } - sprintf(buf, "0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(bytes[i]), bytes[i]); + buf_append_printf(buf, sizeof(buf), 0, "0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", BYTE_TO_BINARY(bytes[i]), bytes[i]); res += buf; } ESP_LOGD(TAG, "%s", res.c_str()); diff --git a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp index b7b3273f39..acd3980a1a 100644 --- a/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp +++ b/esphome/components/uptime/text_sensor/uptime_text_sensor.cpp @@ -9,17 +9,12 @@ namespace uptime { static const char *const TAG = "uptime.sensor"; -// Clamp position to valid buffer range when snprintf indicates truncation -static size_t clamp_buffer_pos(size_t pos, size_t buf_size) { return pos < buf_size ? pos : buf_size - 1; } - static void append_unit(char *buf, size_t buf_size, size_t &pos, const char *separator, unsigned value, const char *label) { if (pos > 0) { - pos += snprintf(buf + pos, buf_size - pos, "%s", separator); - pos = clamp_buffer_pos(pos, buf_size); + pos = buf_append_printf(buf, buf_size, pos, "%s", separator); } - pos += snprintf(buf + pos, buf_size - pos, "%u%s", value, label); - pos = clamp_buffer_pos(pos, buf_size); + pos = buf_append_printf(buf, buf_size, pos, "%u%s", value, label); } void UptimeTextSensor::setup() { diff --git a/esphome/components/web_server/web_server.cpp b/esphome/components/web_server/web_server.cpp index 61b9ea5163..e538a35e8c 100644 --- a/esphome/components/web_server/web_server.cpp +++ b/esphome/components/web_server/web_server.cpp @@ -1188,11 +1188,7 @@ std::string WebServer::date_json_(datetime::DateEntity *obj, JsonDetail start_co // Format: YYYY-MM-DD (max 10 chars + null) char value[12]; -#ifdef USE_ESP8266 - snprintf_P(value, sizeof(value), PSTR("%d-%02d-%02d"), obj->year, obj->month, obj->day); -#else - snprintf(value, sizeof(value), "%d-%02d-%02d", obj->year, obj->month, obj->day); -#endif + buf_append_printf(value, sizeof(value), 0, "%d-%02d-%02d", obj->year, obj->month, obj->day); set_json_icon_state_value(root, obj, "date", value, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); @@ -1251,11 +1247,7 @@ std::string WebServer::time_json_(datetime::TimeEntity *obj, JsonDetail start_co // Format: HH:MM:SS (8 chars + null) char value[12]; -#ifdef USE_ESP8266 - snprintf_P(value, sizeof(value), PSTR("%02d:%02d:%02d"), obj->hour, obj->minute, obj->second); -#else - snprintf(value, sizeof(value), "%02d:%02d:%02d", obj->hour, obj->minute, obj->second); -#endif + buf_append_printf(value, sizeof(value), 0, "%02d:%02d:%02d", obj->hour, obj->minute, obj->second); set_json_icon_state_value(root, obj, "time", value, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); @@ -1314,13 +1306,8 @@ std::string WebServer::datetime_json_(datetime::DateTimeEntity *obj, JsonDetail // Format: YYYY-MM-DD HH:MM:SS (max 19 chars + null) char value[24]; -#ifdef USE_ESP8266 - snprintf_P(value, sizeof(value), PSTR("%d-%02d-%02d %02d:%02d:%02d"), obj->year, obj->month, obj->day, obj->hour, - obj->minute, obj->second); -#else - snprintf(value, sizeof(value), "%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, obj->minute, - obj->second); -#endif + buf_append_printf(value, sizeof(value), 0, "%d-%02d-%02d %02d:%02d:%02d", obj->year, obj->month, obj->day, obj->hour, + obj->minute, obj->second); set_json_icon_state_value(root, obj, "datetime", value, value, start_config); if (start_config == DETAIL_ALL) { this->add_sorting_info_(root, obj); diff --git a/esphome/components/weikai/weikai.cpp b/esphome/components/weikai/weikai.cpp index 197b516bb2..d0a8f8366b 100644 --- a/esphome/components/weikai/weikai.cpp +++ b/esphome/components/weikai/weikai.cpp @@ -246,7 +246,7 @@ void WeikaiGPIOPin::setup() { } size_t WeikaiGPIOPin::dump_summary(char *buffer, size_t len) const { - return snprintf(buffer, len, "%u via WeiKai %s", this->pin_, this->parent_->get_name()); + return buf_append_printf(buffer, len, 0, "%u via WeiKai %s", this->pin_, this->parent_->get_name()); } /////////////////////////////////////////////////////////////////////////////// diff --git a/esphome/components/wiegand/wiegand.cpp b/esphome/components/wiegand/wiegand.cpp index dd1443d10c..f3f578794a 100644 --- a/esphome/components/wiegand/wiegand.cpp +++ b/esphome/components/wiegand/wiegand.cpp @@ -1,4 +1,5 @@ #include "wiegand.h" +#include #include "esphome/core/helpers.h" #include "esphome/core/log.h" @@ -69,32 +70,35 @@ void Wiegand::loop() { for (auto *trigger : this->raw_triggers_) trigger->trigger(count, value); if (count == 26) { - std::string tag = to_string((value >> 1) & 0xffffff); - ESP_LOGD(TAG, "received 26-bit tag: %s", tag.c_str()); + char tag_buf[12]; // max 8 digits for 24-bit value + null + buf_append_printf(tag_buf, sizeof(tag_buf), 0, "%" PRIu32, static_cast((value >> 1) & 0xffffff)); + ESP_LOGD(TAG, "received 26-bit tag: %s", tag_buf); if (!check_eparity(value, 13, 13) || !check_oparity(value, 0, 13)) { ESP_LOGW(TAG, "invalid parity"); return; } for (auto *trigger : this->tag_triggers_) - trigger->trigger(tag); + trigger->trigger(tag_buf); } else if (count == 34) { - std::string tag = to_string((value >> 1) & 0xffffffff); - ESP_LOGD(TAG, "received 34-bit tag: %s", tag.c_str()); + char tag_buf[12]; // max 10 digits for 32-bit value + null + buf_append_printf(tag_buf, sizeof(tag_buf), 0, "%" PRIu32, static_cast((value >> 1) & 0xffffffff)); + ESP_LOGD(TAG, "received 34-bit tag: %s", tag_buf); if (!check_eparity(value, 17, 17) || !check_oparity(value, 0, 17)) { ESP_LOGW(TAG, "invalid parity"); return; } for (auto *trigger : this->tag_triggers_) - trigger->trigger(tag); + trigger->trigger(tag_buf); } else if (count == 37) { - std::string tag = to_string((value >> 1) & 0x7ffffffff); - ESP_LOGD(TAG, "received 37-bit tag: %s", tag.c_str()); + char tag_buf[12]; // max 11 digits for 35-bit value + null + buf_append_printf(tag_buf, sizeof(tag_buf), 0, "%" PRIu64, static_cast((value >> 1) & 0x7ffffffff)); + ESP_LOGD(TAG, "received 37-bit tag: %s", tag_buf); if (!check_eparity(value, 18, 19) || !check_oparity(value, 0, 19)) { ESP_LOGW(TAG, "invalid parity"); return; } for (auto *trigger : this->tag_triggers_) - trigger->trigger(tag); + trigger->trigger(tag_buf); } else if (count == 4) { for (auto *trigger : this->key_triggers_) trigger->trigger(value); diff --git a/esphome/components/xl9535/xl9535.cpp b/esphome/components/xl9535/xl9535.cpp index dd6c8188eb..cfcbeeeb8d 100644 --- a/esphome/components/xl9535/xl9535.cpp +++ b/esphome/components/xl9535/xl9535.cpp @@ -111,7 +111,7 @@ void XL9535Component::pin_mode(uint8_t pin, gpio::Flags mode) { void XL9535GPIOPin::setup() { this->pin_mode(this->flags_); } size_t XL9535GPIOPin::dump_summary(char *buffer, size_t len) const { - return snprintf(buffer, len, "%u via XL9535", this->pin_); + return buf_append_printf(buffer, len, 0, "%u via XL9535", this->pin_); } void XL9535GPIOPin::pin_mode(gpio::Flags flags) { this->parent_->pin_mode(this->pin_, flags); } diff --git a/esphome/components/zephyr/preferences.cpp b/esphome/components/zephyr/preferences.cpp index 08b361b8fb..311133a813 100644 --- a/esphome/components/zephyr/preferences.cpp +++ b/esphome/components/zephyr/preferences.cpp @@ -5,6 +5,8 @@ #include "esphome/core/preferences.h" #include "esphome/core/log.h" #include +#include +#include namespace esphome { namespace zephyr { @@ -13,6 +15,9 @@ static const char *const TAG = "zephyr.preferences"; #define ESPHOME_SETTINGS_KEY "esphome" +// Buffer size for key: "esphome/" (8) + max hex uint32 (8) + null terminator (1) = 17; use 20 for safety margin +static constexpr size_t KEY_BUFFER_SIZE = 20; + class ZephyrPreferenceBackend : public ESPPreferenceBackend { public: ZephyrPreferenceBackend(uint32_t type) { this->type_ = type; } @@ -27,7 +32,9 @@ class ZephyrPreferenceBackend : public ESPPreferenceBackend { bool load(uint8_t *data, size_t len) override { if (len != this->data.size()) { - ESP_LOGE(TAG, "size of setting key %s changed, from: %u, to: %u", get_key().c_str(), this->data.size(), len); + char key_buf[KEY_BUFFER_SIZE]; + this->format_key(key_buf, sizeof(key_buf)); + ESP_LOGE(TAG, "size of setting key %s changed, from: %u, to: %u", key_buf, this->data.size(), len); return false; } std::memcpy(data, this->data.data(), len); @@ -36,7 +43,7 @@ class ZephyrPreferenceBackend : public ESPPreferenceBackend { } uint32_t get_type() const { return this->type_; } - std::string get_key() const { return str_sprintf(ESPHOME_SETTINGS_KEY "/%" PRIx32, this->type_); } + void format_key(char *buf, size_t size) const { snprintf(buf, size, ESPHOME_SETTINGS_KEY "/%" PRIx32, this->type_); } std::vector data; @@ -85,7 +92,9 @@ class ZephyrPreferences : public ESPPreferences { } printf("type %u size %u\n", type, this->backends_.size()); auto *pref = new ZephyrPreferenceBackend(type); // NOLINT(cppcoreguidelines-owning-memory) - ESP_LOGD(TAG, "Add new setting %s.", pref->get_key().c_str()); + char key_buf[KEY_BUFFER_SIZE]; + pref->format_key(key_buf, sizeof(key_buf)); + ESP_LOGD(TAG, "Add new setting %s.", key_buf); this->backends_.push_back(pref); return ESPPreferenceObject(pref); } @@ -134,9 +143,10 @@ class ZephyrPreferences : public ESPPreferences { static int export_settings(int (*cb)(const char *name, const void *value, size_t val_len)) { for (auto *backend : static_cast(global_preferences)->backends_) { - auto name = backend->get_key(); - int err = cb(name.c_str(), backend->data.data(), backend->data.size()); - ESP_LOGD(TAG, "save in flash, name %s, len %u, err %d", name.c_str(), backend->data.size(), err); + char name[KEY_BUFFER_SIZE]; + backend->format_key(name, sizeof(name)); + int err = cb(name, backend->data.data(), backend->data.size()); + ESP_LOGD(TAG, "save in flash, name %s, len %u, err %d", name, backend->data.size(), err); } return 0; } diff --git a/esphome/core/helpers.cpp b/esphome/core/helpers.cpp index 44c20845e8..e7b901d71f 100644 --- a/esphome/core/helpers.cpp +++ b/esphome/core/helpers.cpp @@ -206,11 +206,22 @@ std::string str_snake_case(const std::string &str) { } return result; } -std::string str_sanitize(const std::string &str) { - std::string result = str; - for (char &c : result) { - c = to_sanitized_char(c); +char *str_sanitize_to(char *buffer, size_t buffer_size, const char *str) { + if (buffer_size == 0) { + return buffer; } + size_t i = 0; + while (*str && i < buffer_size - 1) { + buffer[i++] = to_sanitized_char(*str++); + } + buffer[i] = '\0'; + return buffer; +} + +std::string str_sanitize(const std::string &str) { + std::string result; + result.resize(str.size()); + str_sanitize_to(&result[0], str.size() + 1, str.c_str()); return result; } std::string str_snprintf(const char *fmt, size_t len, ...) { diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 8b581eb97a..bd3a4def05 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -366,6 +366,35 @@ template class FixedVector { const T *end() const { return data_ + size_; } }; +/// @brief Helper class for efficient buffer allocation - uses stack for small sizes, heap for large +/// This is useful when most operations need a small buffer but occasionally need larger ones. +/// The stack buffer avoids heap allocation in the common case, while heap fallback handles edge cases. +template class SmallBufferWithHeapFallback { + public: + explicit SmallBufferWithHeapFallback(size_t size) { + if (size <= STACK_SIZE) { + this->buffer_ = this->stack_buffer_; + } else { + this->heap_buffer_ = new uint8_t[size]; + this->buffer_ = this->heap_buffer_; + } + } + ~SmallBufferWithHeapFallback() { delete[] this->heap_buffer_; } + + // Delete copy and move operations to prevent double-delete + SmallBufferWithHeapFallback(const SmallBufferWithHeapFallback &) = delete; + SmallBufferWithHeapFallback &operator=(const SmallBufferWithHeapFallback &) = delete; + SmallBufferWithHeapFallback(SmallBufferWithHeapFallback &&) = delete; + SmallBufferWithHeapFallback &operator=(SmallBufferWithHeapFallback &&) = delete; + + uint8_t *get() { return this->buffer_; } + + private: + uint8_t stack_buffer_[STACK_SIZE]; + uint8_t *heap_buffer_{nullptr}; + uint8_t *buffer_; +}; + ///@} /// @name Mathematics @@ -580,7 +609,25 @@ std::string str_snake_case(const std::string &str); constexpr char to_sanitized_char(char c) { return (c == '-' || c == '_' || (c >= '0' && c <= '9') || (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z')) ? c : '_'; } + +/** Sanitize a string to buffer, keeping only alphanumerics, dashes, and underscores. + * + * @param buffer Output buffer to write to. + * @param buffer_size Size of the output buffer. + * @param str Input string to sanitize. + * @return Pointer to buffer. + * + * Buffer size needed: strlen(str) + 1. + */ +char *str_sanitize_to(char *buffer, size_t buffer_size, const char *str); + +/// Sanitize a string to buffer. Automatically deduces buffer size. +template inline char *str_sanitize_to(char (&buffer)[N], const char *str) { + return str_sanitize_to(buffer, N, str); +} + /// Sanitizes the input string by removing all characters but alphanumerics, dashes and underscores. +/// @warning Allocates heap memory. Use str_sanitize_to() with a stack buffer instead. std::string str_sanitize(const std::string &str); /// Calculate FNV-1 hash of a string while applying snake_case + sanitize transformations. diff --git a/esphome/core/scheduler.h b/esphome/core/scheduler.h index 8c2e349180..7de1023e6d 100644 --- a/esphome/core/scheduler.h +++ b/esphome/core/scheduler.h @@ -403,7 +403,9 @@ class Scheduler { for (size_t i = 0; i < remaining; i++) { this->defer_queue_[i] = std::move(this->defer_queue_[this->defer_queue_front_ + i]); } - this->defer_queue_.resize(remaining); + // Use erase() instead of resize() to avoid instantiating _M_default_append + // (saves ~156 bytes flash). Erasing from the end is O(1) - no shifting needed. + this->defer_queue_.erase(this->defer_queue_.begin() + remaining, this->defer_queue_.end()); } this->defer_queue_front_ = 0; } diff --git a/script/ci-custom.py b/script/ci-custom.py index e227ec873e..ee2e73872c 100755 --- a/script/ci-custom.py +++ b/script/ci-custom.py @@ -688,6 +688,7 @@ HEAP_ALLOCATING_HELPERS = { "format_mac_address_pretty": "format_mac_addr_upper() with a stack buffer", "get_mac_address": "get_mac_address_into_buffer() with a stack buffer", "get_mac_address_pretty": "get_mac_address_pretty_into_buffer() with a stack buffer", + "str_sanitize": "str_sanitize_to() with a stack buffer", "str_truncate": "removal (function is unused)", "str_upper_case": "removal (function is unused)", "str_snake_case": "removal (function is unused)", @@ -706,6 +707,7 @@ HEAP_ALLOCATING_HELPERS = { r"format_mac_address_pretty|" r"get_mac_address_pretty(?!_)|" r"get_mac_address(?!_)|" + r"str_sanitize(?!_)|" r"str_truncate|" r"str_upper_case|" r"str_snake_case" diff --git a/tests/integration/fixtures/api_homeassistant.yaml b/tests/integration/fixtures/api_homeassistant.yaml index 8fe23b9a19..2d77821ff3 100644 --- a/tests/integration/fixtures/api_homeassistant.yaml +++ b/tests/integration/fixtures/api_homeassistant.yaml @@ -108,6 +108,25 @@ text_sensor: format: "HA Empty state updated: %s" args: ['x.c_str()'] + # Test long attribute handling (>255 characters) + # HA states are limited to 255 chars, but attributes are not + - platform: homeassistant + name: "HA Long Attribute" + entity_id: sensor.long_data + attribute: long_value + id: ha_long_attribute + on_value: + then: + - logger.log: + format: "HA Long attribute received, length: %d" + args: ['x.size()'] + # Log the first 50 and last 50 chars to verify no truncation + - lambda: |- + if (x.size() >= 100) { + ESP_LOGI("test", "Long attribute first 50 chars: %.50s", x.c_str()); + ESP_LOGI("test", "Long attribute last 50 chars: %s", x.c_str() + x.size() - 50); + } + # Number component for testing HA number control number: - platform: template diff --git a/tests/integration/test_api_homeassistant.py b/tests/integration/test_api_homeassistant.py index 3fe0dfe045..b4adedf873 100644 --- a/tests/integration/test_api_homeassistant.py +++ b/tests/integration/test_api_homeassistant.py @@ -40,6 +40,7 @@ async def test_api_homeassistant( humidity_update_future = loop.create_future() motion_update_future = loop.create_future() weather_update_future = loop.create_future() + long_attr_future = loop.create_future() # Number future ha_number_future = loop.create_future() @@ -58,6 +59,7 @@ async def test_api_homeassistant( humidity_update_pattern = re.compile(r"HA Humidity state updated: ([\d.]+)") motion_update_pattern = re.compile(r"HA Motion state changed: (ON|OFF)") weather_update_pattern = re.compile(r"HA Weather condition updated: (\w+)") + long_attr_pattern = re.compile(r"HA Long attribute received, length: (\d+)") # Number pattern ha_number_pattern = re.compile(r"Setting HA number to: ([\d.]+)") @@ -143,8 +145,14 @@ async def test_api_homeassistant( elif not weather_update_future.done() and weather_update_pattern.search(line): weather_update_future.set_result(line) - # Check number pattern - elif not ha_number_future.done() and ha_number_pattern.search(line): + # Check long attribute pattern - separate if since it can come at different times + if not long_attr_future.done(): + match = long_attr_pattern.search(line) + if match: + long_attr_future.set_result(int(match.group(1))) + + # Check number pattern - separate if since it can come at different times + if not ha_number_future.done(): match = ha_number_pattern.search(line) if match: ha_number_future.set_result(match.group(1)) @@ -179,6 +187,14 @@ async def test_api_homeassistant( client.send_home_assistant_state("binary_sensor.external_motion", "", "ON") client.send_home_assistant_state("weather.home", "condition", "sunny") + # Send a long attribute (300 characters) to test that attributes aren't truncated + # HA states are limited to 255 chars, but attributes are NOT limited + # This tests the fix for the 256-byte buffer truncation bug + long_attr_value = "X" * 300 # 300 chars - enough to expose truncation bug + client.send_home_assistant_state( + "sensor.long_data", "long_value", long_attr_value + ) + # Test edge cases for zero-copy implementation safety # Empty entity_id should be silently ignored (no crash) client.send_home_assistant_state("", "", "should_be_ignored") @@ -225,6 +241,13 @@ async def test_api_homeassistant( number_value = await asyncio.wait_for(ha_number_future, timeout=5.0) assert number_value == "42.5", f"Unexpected number value: {number_value}" + # Long attribute test - verify 300 chars weren't truncated to 255 + long_attr_len = await asyncio.wait_for(long_attr_future, timeout=5.0) + assert long_attr_len == 300, ( + f"Long attribute was truncated! Expected 300 chars, got {long_attr_len}. " + "This indicates the 256-byte truncation bug." + ) + # Wait for completion await asyncio.wait_for(tests_complete_future, timeout=5.0)