Compare commits

...

29 Commits

Author SHA1 Message Date
J. Nick Koston
83c68e246d [lock] Extract set_state_ helper to reduce code duplication 2026-01-18 19:49:20 -10:00
J. Nick Koston
d8a28f6fba [scheduler] Replace resize() with erase() to save ~ 436 bytes flash (#13214) 2026-01-18 18:54:30 -10:00
J. Nick Koston
e80a940222 [gdk101] Use stack buffer to eliminate heap allocation for firmware version (#13224) 2026-01-18 18:52:49 -10:00
J. Nick Koston
e99dbe05f7 [toshiba] Replace to_string with stack buffer in debug logging (#13296) 2026-01-18 18:52:34 -10:00
J. Nick Koston
f453a8d9a1 [dfrobot_sen0395] Reduce heap allocations in command building (#13219) 2026-01-18 18:44:56 -10:00
J. Nick Koston
126190d26a [ezo] Replace str_sprintf with stack-based formatting (#13218)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 18:44:41 -10:00
J. Nick Koston
e40201a98d [cse7766] Use stack buffer for verbose debug logging (#13217) 2026-01-18 18:44:27 -10:00
J. Nick Koston
8142f5db44 [zephyr] Avoid heap allocation in preferences key formatting (#13215) 2026-01-18 18:43:50 -10:00
J. Nick Koston
98ccab87a7 [tormatic] Use stack buffers instead of str_sprintf in debug methods (#13225) 2026-01-18 18:43:36 -10:00
J. Nick Koston
b9e72a8774 [daikin_arc] Fix undefined behavior in sprintf calls (#13279)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 18:43:19 -10:00
J. Nick Koston
d9fc625c6a [web_server] Simplify datetime formatting with buf_append_printf (#13281) 2026-01-18 18:43:05 -10:00
J. Nick Koston
dfbf79d6d6 [homeassistant] Use buf_append_printf for ESP8266 flash optimization (#13284) 2026-01-18 18:42:19 -10:00
J. Nick Koston
ea0fac96cb [core][mqtt] Add str_sanitize_to(), soft-deprecate str_sanitize() (#13233) 2026-01-18 18:42:04 -10:00
J. Nick Koston
3182222d60 [esp32_hosted] Use stack buffer instead of str_sprintf for version string (#13226) 2026-01-18 18:41:47 -10:00
J. Nick Koston
d8849b16f2 [gpio] Use buf_append_printf in dump_summary for ESP8266 flash optimization (#13283) 2026-01-18 18:41:34 -10:00
J. Nick Koston
635983f163 [uptime] Use buf_append_printf for ESP8266 flash optimization (#13282) 2026-01-18 18:41:19 -10:00
J. Nick Koston
6cbe672004 [tuya] Use buf_append_printf for ESP8266 flash optimization (#13287) 2026-01-18 18:41:07 -10:00
J. Nick Koston
226867b05c [esp8266] Use direct SDK calls instead of Arduino ESP class wrappers (#13353) 2026-01-18 18:40:53 -10:00
J. Nick Koston
67871a1683 [ccs811] Use buf_append_printf for buffer safety and ESP8266 flash optimization (#13300) 2026-01-18 18:40:14 -10:00
J. Nick Koston
f60c03e350 [syslog] Use buf_append_printf for ESP8266 flash optimization (#13286) 2026-01-18 18:39:53 -10:00
J. Nick Koston
eb66429144 [sml] Use stack buffers instead of str_sprintf (#13222)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-18 18:39:23 -10:00
J. Nick Koston
0f3bac5dd6 [nextion] Replace to_string with stack buffer and fix unsafe sprintf (#13295) 2026-01-18 18:37:29 -10:00
J. Nick Koston
5b92d0b89e [wiegand] Replace heap-allocating to_string with stack buffers (#13294) 2026-01-18 18:37:14 -10:00
J. Nick Koston
052b05df56 [tuya] Replace unsafe sprintf with snprintf in light color formatting (#13292) 2026-01-18 18:37:02 -10:00
J. Nick Koston
7b0db659d1 [rc522_spi] Replace unsafe sprintf with buf_append_printf (#13291) 2026-01-18 18:36:46 -10:00
J. Nick Koston
2f7270cf8f [uart] Replace unsafe sprintf with buf_append_printf in debugger (#13288) 2026-01-18 18:36:32 -10:00
J. Nick Koston
b44727aee6 [socket] Eliminate heap allocations in set_sockaddr() (#13228) 2026-01-18 18:29:31 -10:00
J. Nick Koston
1a55254258 [status] Convert to PollingComponent to reduce CPU usage (#13342) 2026-01-18 18:28:24 -10:00
J. Nick Koston
baf2b0e3c9 [api] Fix truncation of Home Assistant attributes longer than 255 characters (#13348) 2026-01-18 18:23:11 -10:00
59 changed files with 364 additions and 212 deletions

View File

@@ -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<char *>(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

View File

@@ -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);
}

View File

@@ -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;

View File

@@ -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
}

View File

@@ -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<char[]> buf(new char[bytes_count * 3 + 1]);
buf[0] = '\0';
size_t buf_size = bytes_count * 3 + 1;
std::unique_ptr<char[]> 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<size_t>(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<size_t>(data.size())) {
ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf);

View File

@@ -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) {

View File

@@ -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";
}

View File

@@ -6,7 +6,11 @@
#include "esphome/core/helpers.h"
#include "preferences.h"
#include <Arduino.h>
#include <Esp.h>
#include <core_esp8266_features.h>
extern "C" {
#include <user_interface.h>
}
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() {

View File

@@ -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() {

View File

@@ -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<EzoCommand> 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

View File

@@ -92,7 +92,7 @@ class EZOSensor : public sensor::Sensor, public PollingComponent, public i2c::I2
std::deque<std::unique_ptr<EzoCommand>> 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);

View File

@@ -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;

View File

@@ -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);

View File

@@ -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;

View File

@@ -11,22 +11,6 @@
namespace esphome {
namespace i2c {
/// @brief Helper class for efficient buffer allocation - uses stack for small sizes, heap for large
template<size_t STACK_SIZE> class SmallBufferWithHeapFallback {
public:
uint8_t *get(size_t size) {
if (size <= STACK_SIZE) {
return this->stack_buffer_;
}
this->heap_buffer_ = std::unique_ptr<uint8_t[]>(new uint8_t[size]);
return this->heap_buffer_.get();
}
private:
uint8_t stack_buffer_[STACK_SIZE];
std::unique_ptr<uint8_t[]> 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++) {

View File

@@ -28,16 +28,14 @@ const LogString *lock_state_to_string(LockState state) {
Lock::Lock() : state(LOCK_STATE_NONE) {}
LockCall Lock::make_call() { return LockCall(this); }
void Lock::lock() {
void Lock::set_state_(LockState state) {
auto call = this->make_call();
call.set_state(LOCK_STATE_LOCKED);
this->control(call);
}
void Lock::unlock() {
auto call = this->make_call();
call.set_state(LOCK_STATE_UNLOCKED);
call.set_state(state);
this->control(call);
}
void Lock::lock() { this->set_state_(LOCK_STATE_LOCKED); }
void Lock::unlock() { this->set_state_(LOCK_STATE_UNLOCKED); }
void Lock::open() {
if (traits.get_supports_open()) {
ESP_LOGD(TAG, "'%s' Opening.", this->get_name().c_str());

View File

@@ -156,6 +156,9 @@ class Lock : public EntityBase {
protected:
friend LockCall;
/// Helper for lock/unlock convenience methods
void set_state_(LockState state);
/** Perform the open latch action with hardware. This method is optional to implement
* when creating a new lock.
*

View File

@@ -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

View File

@@ -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

View File

@@ -17,7 +17,7 @@ template<uint8_t N> void MCP23XXXGPIOPin<N>::digital_write(bool value) {
this->parent_->digital_write(this->pin_, value != this->inverted_);
}
template<uint8_t N> size_t MCP23XXXGPIOPin<N>::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>;

View File

@@ -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

View File

@@ -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;
}

View File

@@ -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);

View File

@@ -1,6 +1,7 @@
#include "nextion.h"
#include <cinttypes>
#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();

View File

@@ -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();

View File

@@ -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");

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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
}

View File

@@ -104,7 +104,10 @@ std::vector<ObisInfo> 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<unsigned int>(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

View File

@@ -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

View File

@@ -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

View File

@@ -107,9 +107,9 @@ std::unique_ptr<Socket> 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<sockaddr_in *>(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);
}

View File

@@ -87,7 +87,17 @@ std::unique_ptr<Socket> socket_loop_monitored(int domain, int type, int protocol
std::unique_ptr<Socket> 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);

View File

@@ -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):

View File

@@ -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

View File

@@ -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

View File

@@ -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

View File

@@ -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<size_t>(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<size_t>(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) {

View File

@@ -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

View File

@@ -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));

View File

@@ -1,5 +1,6 @@
#include "toshiba.h"
#include "esphome/components/remote_base/toshiba_ac_protocol.h"
#include "esphome/core/helpers.h"
#include <vector>
@@ -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<int>(this->mode),
this->fan_mode.has_value() ? std::to_string(static_cast<int>(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<int>(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<int>(this->mode), fan_mode_str,
static_cast<int>(this->swing_mode), this->target_temperature);
#endif
}
void ToshibaClimate::transmit_state() {

View File

@@ -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;
}

View File

@@ -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;

View File

@@ -107,7 +107,7 @@ void UARTDebug::log_hex(UARTDirection direction, std::vector<uint8_t> 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<uint8_t> 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<uint8_t> 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<uint8_t> 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());

View File

@@ -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() {

View File

@@ -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);

View File

@@ -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());
}
///////////////////////////////////////////////////////////////////////////////

View File

@@ -1,4 +1,5 @@
#include "wiegand.h"
#include <cinttypes>
#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<uint32_t>((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<uint32_t>((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<uint64_t>((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);

View File

@@ -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); }

View File

@@ -5,6 +5,8 @@
#include "esphome/core/preferences.h"
#include "esphome/core/log.h"
#include <zephyr/settings/settings.h>
#include <cinttypes>
#include <cstring>
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<uint8_t> 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<ZephyrPreferences *>(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;
}

View File

@@ -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, ...) {

View File

@@ -366,6 +366,35 @@ template<typename T> 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<size_t STACK_SIZE> 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<size_t N> 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.

View File

@@ -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;
}

View File

@@ -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"

View File

@@ -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

View File

@@ -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)