Compare commits

..

2 Commits

Author SHA1 Message Date
J. Nick Koston
44a4bf4952 Update esphome/components/sml/sml_parser.cpp
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
2026-01-14 15:28:27 -10:00
J. Nick Koston
fc4f1ab094 [sml] Use stack buffers instead of str_sprintf 2026-01-14 15:20:33 -10:00
48 changed files with 101 additions and 273 deletions

View File

@@ -9,44 +9,10 @@ ESPHOME_COMPONENT_PATTERN = re.compile(r"esphome::([a-zA-Z0-9_]+)::")
# Maps standard section names to their various platform-specific variants
# Note: Order matters! More specific patterns (.bss) must come before general ones (.dram)
# because ESP-IDF uses names like ".dram0.bss" which would match ".dram" otherwise
#
# Platform-specific sections:
# - ESP8266/ESP32: .iram*, .dram*
# - LibreTiny RTL87xx: .xip.code_* (flash), .ram.code_* (RAM)
# - LibreTiny BK7231: .itcm.code (fast RAM), .vectors (interrupt vectors)
# - LibreTiny LN882X: .flash_text, .flash_copy* (flash code)
SECTION_MAPPING = {
".text": frozenset(
[
".text",
".iram",
# LibreTiny RTL87xx XIP (eXecute In Place) flash code
".xip.code",
# LibreTiny RTL87xx RAM code
".ram.code_text",
# LibreTiny BK7231 fast RAM code and vectors
".itcm.code",
".vectors",
# LibreTiny LN882X flash code
".flash_text",
".flash_copy",
]
),
".rodata": frozenset(
[
".rodata",
# LibreTiny RTL87xx read-only data in RAM
".ram.code_rodata",
]
),
# .bss patterns - must be before .data to catch ".dram0.bss"
".bss": frozenset(
[
".bss",
# LibreTiny LN882X BSS
".bss_ram",
]
),
".text": frozenset([".text", ".iram"]),
".rodata": frozenset([".rodata"]),
".bss": frozenset([".bss"]), # Must be before .data to catch ".dram0.bss"
".data": frozenset([".data", ".dram"]),
}

View File

@@ -31,8 +31,7 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) {
this->last_update_ = millis();
if (state != this->current_state_) {
auto prev_state = this->current_state_;
ESP_LOGD(TAG, "'%s' >> %s (was %s)", this->get_name().c_str(),
LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)),
LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state)));
this->current_state_ = state;
// Single state callback - triggers check get_state() for specific states

View File

@@ -241,10 +241,8 @@ void APIServer::handle_disconnect(APIConnection *conn) {}
void APIServer::on_##entity_name##_update(entity_type *obj) { /* NOLINT(bugprone-macro-parentheses) */ \
if (obj->is_internal()) \
return; \
for (auto &c : this->clients_) { \
if (c->flags_.state_subscription) \
c->send_##entity_name##_state(obj); \
} \
for (auto &c : this->clients_) \
c->send_##entity_name##_state(obj); \
}
#ifdef USE_BINARY_SENSOR
@@ -323,10 +321,8 @@ API_DISPATCH_UPDATE(water_heater::WaterHeater, water_heater)
void APIServer::on_event(event::Event *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_) {
if (c->flags_.state_subscription)
c->send_event(obj);
}
for (auto &c : this->clients_)
c->send_event(obj);
}
#endif
@@ -335,10 +331,8 @@ void APIServer::on_event(event::Event *obj) {
void APIServer::on_update(update::UpdateEntity *obj) {
if (obj->is_internal())
return;
for (auto &c : this->clients_) {
if (c->flags_.state_subscription)
c->send_update_state(obj);
}
for (auto &c : this->clients_)
c->send_update_state(obj);
}
#endif

View File

@@ -44,7 +44,7 @@ bool BinarySensor::set_new_state(const optional<bool> &new_state) {
#if defined(USE_BINARY_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_binary_sensor_update(this);
#endif
ESP_LOGD(TAG, "'%s' >> %s", this->get_name().c_str(), ONOFFMAYBE(new_state));
ESP_LOGD(TAG, "'%s': %s", this->get_name().c_str(), ONOFFMAYBE(new_state));
return true;
}
return false;

View File

@@ -44,7 +44,7 @@ CONFIG_SCHEMA = cv.All(
cv.GenerateID(CONF_WEB_SERVER_BASE_ID): cv.use_id(
web_server_base.WebServerBase
),
cv.Optional(CONF_COMPRESSION, default="gzip"): cv.one_of("gzip", "br"),
cv.Optional(CONF_COMPRESSION, default="br"): cv.one_of("br", "gzip"),
}
).extend(cv.COMPONENT_SCHEMA),
cv.only_on(

View File

@@ -436,7 +436,7 @@ void Climate::save_state_() {
}
void Climate::publish_state() {
ESP_LOGD(TAG, "'%s' >>", this->name_.c_str());
ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str());
auto traits = this->get_traits();
ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode)));

View File

@@ -153,7 +153,7 @@ void Cover::publish_state(bool save) {
this->position = clamp(this->position, 0.0f, 1.0f);
this->tilt = clamp(this->tilt, 0.0f, 1.0f);
ESP_LOGD(TAG, "'%s' >>", this->name_.c_str());
ESP_LOGD(TAG, "'%s' - Publishing:", this->name_.c_str());
auto traits = this->get_traits();
if (traits.get_supports_position()) {
ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f);

View File

@@ -30,7 +30,7 @@ void DateEntity::publish_state() {
return;
}
this->set_has_state(true);
ESP_LOGD(TAG, "'%s' >> %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_);
ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_);
this->state_callback_.call();
#if defined(USE_DATETIME_DATE) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_date_update(this);

View File

@@ -45,8 +45,8 @@ void DateTimeEntity::publish_state() {
return;
}
this->set_has_state(true);
ESP_LOGD(TAG, "'%s' >> %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_, this->month_,
this->day_, this->hour_, this->minute_, this->second_);
ESP_LOGD(TAG, "'%s': Sending datetime %04u-%02u-%02u %02d:%02d:%02d", this->get_name().c_str(), this->year_,
this->month_, this->day_, this->hour_, this->minute_, this->second_);
this->state_callback_.call();
#if defined(USE_DATETIME_DATETIME) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_datetime_update(this);

View File

@@ -26,7 +26,8 @@ void TimeEntity::publish_state() {
return;
}
this->set_has_state(true);
ESP_LOGD(TAG, "'%s' >> %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, this->second_);
ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_,
this->second_);
this->state_callback_.call();
#if defined(USE_DATETIME_TIME) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_time_update(this);

View File

@@ -17,11 +17,7 @@ from esphome.const import (
UNIT_PERCENT,
)
from . import ( # noqa: F401 pylint: disable=unused-import
CONF_DEBUG_ID,
FILTER_SOURCE_FILES,
DebugComponent,
)
from . import CONF_DEBUG_ID, DebugComponent
DEPENDENCIES = ["debug"]

View File

@@ -8,11 +8,7 @@ from esphome.const import (
ICON_RESTART,
)
from . import ( # noqa: F401 pylint: disable=unused-import
CONF_DEBUG_ID,
FILTER_SOURCE_FILES,
DebugComponent,
)
from . import CONF_DEBUG_ID, DebugComponent
DEPENDENCIES = ["debug"]

View File

@@ -294,7 +294,8 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() {
}
// Stream firmware to coprocessor while computing SHA256
sha256::SHA256 hasher;
// Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+)
alignas(32) sha256::SHA256 hasher;
hasher.init();
uint8_t buffer[CHUNK_SIZE];
@@ -351,7 +352,8 @@ bool Esp32HostedUpdate::write_embedded_firmware_to_coprocessor_() {
}
// Verify SHA256 before writing
sha256::SHA256 hasher;
// Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+)
alignas(32) sha256::SHA256 hasher;
hasher.init();
hasher.add(this->firmware_data_, this->firmware_size_);
hasher.calculate();

View File

@@ -563,9 +563,11 @@ bool ESPHomeOTAComponent::handle_auth_send_() {
// [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce
// [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash
// CRITICAL ESP32-S2/S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
// (no passing to other functions). All hash operations must happen in this function.
sha256::SHA256 hasher;
// NOTE: On ESP32-S3 with IDF 5.5.x, the SHA256 context must be properly aligned for
// hardware SHA acceleration DMA operations.
alignas(32) sha256::SHA256 hasher;
const size_t hex_size = hasher.get_size() * 2;
const size_t nonce_len = hasher.get_size() / 4;
@@ -637,9 +639,11 @@ bool ESPHomeOTAComponent::handle_auth_read_() {
const char *cnonce = nonce + hex_size;
const char *response = cnonce + hex_size;
// CRITICAL ESP32-S2/S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame
// (no passing to other functions). All hash operations must happen in this function.
sha256::SHA256 hasher;
// NOTE: On ESP32-S3 with IDF 5.5.x, the SHA256 context must be properly aligned for
// hardware SHA acceleration DMA operations.
alignas(32) sha256::SHA256 hasher;
hasher.init();
hasher.add(this->password_.c_str(), this->password_.length());

View File

@@ -22,7 +22,7 @@ void Event::trigger(const std::string &event_type) {
return;
}
this->last_event_type_ = found;
ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->last_event_type_);
ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), this->last_event_type_);
this->event_callback_.call(event_type);
#if defined(USE_EVENT) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_event(this);

View File

@@ -201,7 +201,7 @@ void Fan::publish_state() {
auto traits = this->get_traits();
ESP_LOGD(TAG,
"'%s' >>\n"
"'%s' - Sending state:\n"
" State: %s",
this->name_.c_str(), ONOFF(this->state));
if (traits.supports_speed()) {

View File

@@ -665,10 +665,15 @@ async def write_image(config, all_frames=False):
if is_svg_file(path):
import resvg_py
resize = resize or (None, None)
image_data = resvg_py.svg_to_bytes(
svg_path=str(path), width=resize[0], height=resize[1], dpi=100
)
if resize:
width, height = resize
# resvg-py allows rendering by width/height directly
image_data = resvg_py.svg_to_bytes(
svg_path=str(path), width=int(width), height=int(height)
)
else:
# Default size
image_data = resvg_py.svg_to_bytes(svg_path=str(path))
# Convert bytes to Pillow Image
image = Image.open(io.BytesIO(image_data))

View File

@@ -52,7 +52,7 @@ void Lock::publish_state(LockState state) {
this->state = state;
this->rtc_.save(&this->state);
ESP_LOGD(TAG, "'%s' >> %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state)));
ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state)));
this->state_callback_.call();
#if defined(USE_LOCK) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_lock_update(this);

View File

@@ -413,7 +413,6 @@ class TextValidator(LValidator):
str_args = [str(x) for x in value[CONF_ARGS]]
arg_expr = cg.RawExpression(",".join(str_args))
format_str = cpp_string_escape(format_str)
# str_sprintf justified: user-defined format, can't optimize without permanent RAM cost
sprintf_str = f"str_sprintf({format_str}, {arg_expr}).c_str()"
if nanval := value.get(CONF_IF_NAN):
nanval = cpp_string_escape(nanval)

View File

@@ -65,10 +65,7 @@ std::string lv_event_code_name_for(uint8_t event_code) {
if (event_code < sizeof(EVENT_NAMES) / sizeof(EVENT_NAMES[0])) {
return EVENT_NAMES[event_code];
}
// max 4 bytes: "%u" with uint8_t (max 255, 3 digits) + null
char buf[4];
snprintf(buf, sizeof(buf), "%u", event_code);
return buf;
return str_sprintf("%2d", event_code);
}
static void rounder_cb(lv_disp_drv_t *disp_drv, lv_area_t *area) {

View File

@@ -11,12 +11,7 @@ from esphome.const import (
)
from esphome.core import CORE, TimePeriod
from . import ( # noqa: F401 pylint: disable=unused-import
FILTER_SOURCE_FILES,
Nextion,
nextion_ns,
nextion_ref,
)
from . import Nextion, nextion_ns, nextion_ref
from .base_component import (
CONF_AUTO_WAKE_ON_TOUCH,
CONF_COMMAND_SPACING,

View File

@@ -31,7 +31,7 @@ void log_number(const char *tag, const char *prefix, const char *type, Number *o
void Number::publish_state(float state) {
this->set_has_state(true);
this->state = state;
ESP_LOGD(TAG, "'%s' >> %.2f", this->get_name().c_str(), state);
ESP_LOGD(TAG, "'%s': Sending state %f", this->get_name().c_str(), state);
this->state_callback_.call(state);
#if defined(USE_NUMBER) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_number_update(this);

View File

@@ -27,16 +27,7 @@ void QrCode::set_ecc(qrcodegen_Ecc ecc) {
void QrCode::generate_qr_code() {
ESP_LOGV(TAG, "Generating QR code");
#ifdef USE_ESP32
// ESP32 has 8KB stack, safe to allocate ~4KB buffer on stack
uint8_t tempbuffer[qrcodegen_BUFFER_LEN_MAX];
#else
// Other platforms (ESP8266: 4KB, RP2040: 2KB, LibreTiny: ~4KB) have smaller stacks
// Allocate buffer on heap to avoid stack overflow
auto tempbuffer_owner = std::make_unique<uint8_t[]>(qrcodegen_BUFFER_LEN_MAX);
uint8_t *tempbuffer = tempbuffer_owner.get();
#endif
if (!qrcodegen_encodeText(this->value_.c_str(), tempbuffer, this->qr_, this->ecc_, qrcodegen_VERSION_MIN,
qrcodegen_VERSION_MAX, qrcodegen_Mask_AUTO, true)) {

View File

@@ -1,7 +1,5 @@
from esphome.components import binary_sensor, remote_base
from . import FILTER_SOURCE_FILES # noqa: F401 pylint: disable=unused-import
DEPENDENCIES = ["remote_receiver"]
CONFIG_SCHEMA = remote_base.validate_binary_sensor

View File

@@ -9,7 +9,7 @@
#include <cinttypes>
#include <cstdio>
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
#ifdef USE_OTA_ROLLBACK
#include <esp_ota_ops.h>
#endif
@@ -26,17 +26,6 @@ void SafeModeComponent::dump_config() {
this->safe_mode_boot_is_good_after_ / 1000, // because milliseconds
this->safe_mode_num_attempts_,
this->safe_mode_enable_time_ / 1000); // because milliseconds
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
const char *state_str;
if (this->ota_state_ == ESP_OTA_IMG_NEW) {
state_str = "not supported";
} else if (this->ota_state_ == ESP_OTA_IMG_PENDING_VERIFY) {
state_str = "supported";
} else {
state_str = "support unknown";
}
ESP_LOGCONFIG(TAG, " Bootloader rollback: %s", state_str);
#endif
if (this->safe_mode_rtc_value_ > 1 && this->safe_mode_rtc_value_ != SafeModeComponent::ENTER_SAFE_MODE_MAGIC) {
auto remaining_restarts = this->safe_mode_num_attempts_ - this->safe_mode_rtc_value_;
@@ -47,7 +36,7 @@ void SafeModeComponent::dump_config() {
}
}
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
#ifdef USE_OTA_ROLLBACK
const esp_partition_t *last_invalid = esp_ota_get_last_invalid_partition();
if (last_invalid != nullptr) {
ESP_LOGW(TAG,
@@ -66,7 +55,7 @@ void SafeModeComponent::loop() {
ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter");
this->clean_rtc();
this->boot_successful_ = true;
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
#ifdef USE_OTA_ROLLBACK
// Mark OTA partition as valid to prevent rollback
esp_ota_mark_app_valid_cancel_rollback();
#endif
@@ -101,12 +90,6 @@ bool SafeModeComponent::should_enter_safe_mode(uint8_t num_attempts, uint32_t en
this->safe_mode_num_attempts_ = num_attempts;
this->rtc_ = global_preferences->make_preference<uint32_t>(233825507UL, false);
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
// Check partition state to detect if bootloader supports rollback
const esp_partition_t *running = esp_ota_get_running_partition();
esp_ota_get_state_partition(running, &this->ota_state_);
#endif
uint32_t rtc_val = this->read_rtc_();
this->safe_mode_rtc_value_ = rtc_val;

View File

@@ -5,10 +5,6 @@
#include "esphome/core/helpers.h"
#include "esphome/core/preferences.h"
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
#include <esp_ota_ops.h>
#endif
namespace esphome::safe_mode {
/// SafeModeComponent provides a safe way to recover from repeated boot failures
@@ -46,9 +42,6 @@ class SafeModeComponent : public Component {
// Group 1-byte members together to minimize padding
bool boot_successful_{false}; ///< set to true after boot is considered successful
uint8_t safe_mode_num_attempts_{0};
#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK)
esp_ota_img_states_t ota_state_{ESP_OTA_IMG_UNDEFINED};
#endif
// Larger objects at the end
ESPPreferenceObject rtc_;
#ifdef USE_SAFE_MODE_CALLBACK

View File

@@ -31,7 +31,7 @@ void Select::publish_state(size_t index) {
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
this->state = option; // Update deprecated member for backward compatibility
#pragma GCC diagnostic pop
ESP_LOGD(TAG, "'%s' >> %s (%zu)", this->get_name().c_str(), option, index);
ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", this->get_name().c_str(), option, index);
this->state_callback_.call(index);
#if defined(USE_SELECT) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_select_update(this);

View File

@@ -126,8 +126,8 @@ float Sensor::get_raw_state() const { return this->raw_state; }
void Sensor::internal_send_state_to_frontend(float state) {
this->set_has_state(true);
this->state = state;
ESP_LOGD(TAG, "'%s' >> %.*f %s", this->get_name().c_str(), std::max(0, (int) this->get_accuracy_decimals()), state,
this->get_unit_of_measurement_ref().c_str());
ESP_LOGD(TAG, "'%s': Sending state %.5f %s with %d decimals of accuracy", this->get_name().c_str(), state,
this->get_unit_of_measurement_ref().c_str(), this->get_accuracy_decimals());
this->callback_.call(state);
#if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_sensor_update(this);

View File

@@ -10,24 +10,26 @@ namespace esphome::sha256 {
#if defined(USE_ESP32) || defined(USE_LIBRETINY)
// CRITICAL ESP32 HARDWARE SHA ACCELERATION REQUIREMENTS (IDF 5.5.x):
// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS (IDF 5.5.x):
//
// ESP32 variants (except original ESP32) use DMA-based hardware SHA acceleration that requires
// 32-byte aligned digest buffers. This is handled automatically via HashBase::digest_ which has
// alignas(32) on these platforms. Two additional constraints apply:
// The ESP32-S3 uses hardware DMA for SHA acceleration. The mbedtls_sha256_context structure contains
// internal state that the DMA engine references. This imposes three critical constraints:
//
// 1. NO VARIABLE LENGTH ARRAYS (VLAs): VLAs corrupt the stack layout, causing the DMA engine to
// 1. ALIGNMENT: The SHA256 object MUST be declared with `alignas(32)` for proper DMA alignment.
// Without this, the DMA engine may crash with an abort in sha_hal_read_digest().
//
// 2. NO VARIABLE LENGTH ARRAYS (VLAs): VLAs corrupt the stack layout, causing the DMA engine to
// write to incorrect memory locations. This results in null pointer dereferences and crashes.
// ALWAYS use fixed-size arrays (e.g., char buf[65], not char buf[size+1]).
//
// 2. SAME STACK FRAME ONLY: The SHA256 object must be created and used entirely within the same
// 3. SAME STACK FRAME ONLY: The SHA256 object must be created and used entirely within the same
// function. NEVER pass the SHA256 object or HashBase pointer to another function. When the stack
// frame changes (function call/return), the DMA references become invalid and will produce
// truncated hash output (20 bytes instead of 32) or corrupt memory.
//
// CORRECT USAGE:
// void my_function() {
// sha256::SHA256 hasher;
// alignas(32) sha256::SHA256 hasher; // Created locally with proper alignment
// hasher.init();
// hasher.add(data, len); // Any size, no chunking needed
// hasher.calculate();
@@ -35,9 +37,9 @@ namespace esphome::sha256 {
// // hasher destroyed when function returns
// }
//
// INCORRECT USAGE (WILL FAIL):
// INCORRECT USAGE (WILL FAIL ON ESP32-S3):
// void my_function() {
// sha256::SHA256 hasher;
// sha256::SHA256 hasher; // WRONG: Missing alignas(32)
// helper(&hasher); // WRONG: Passed to different stack frame
// }
// void helper(HashBase *h) {

View File

@@ -24,14 +24,13 @@ namespace esphome::sha256 {
/// SHA256 hash implementation.
///
/// CRITICAL for ESP32 variants (except original) with IDF 5.5.x hardware SHA acceleration:
/// 1. The object MUST stay in the same stack frame (no passing to other functions)
/// 2. NO Variable Length Arrays (VLAs) in the same function
///
/// Note: Alignment is handled automatically via the HashBase::digest_ member.
/// CRITICAL for ESP32-S3 with IDF 5.5.x hardware SHA acceleration:
/// 1. SHA256 objects MUST be declared with `alignas(32)` for proper DMA alignment
/// 2. The object MUST stay in the same stack frame (no passing to other functions)
/// 3. NO Variable Length Arrays (VLAs) in the same function
///
/// Example usage:
/// sha256::SHA256 hasher;
/// alignas(32) sha256::SHA256 hasher;
/// hasher.init();
/// hasher.add(data, len);
/// hasher.calculate();

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

@@ -332,7 +332,6 @@ Sprinkler::Sprinkler(const std::string &name) {
// The `name` is needed to set timers up, hence non-default constructor
// replaces `set_name()` method previously existed
this->name_ = name;
this->timer_.init(2);
this->timer_.push_back({this->name_ + "sm", false, 0, 0, std::bind(&Sprinkler::sm_timer_callback_, this)});
this->timer_.push_back({this->name_ + "vs", false, 0, 0, std::bind(&Sprinkler::valve_selection_callback_, this)});
}
@@ -1575,8 +1574,7 @@ const LogString *Sprinkler::state_as_str_(SprinklerState state) {
void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) {
if (this->timer_duration_(timer_index) > 0) {
// FixedVector ensures timer_ can't be resized, so .c_str() pointers remain valid
this->set_timeout(this->timer_[timer_index].name.c_str(), this->timer_duration_(timer_index),
this->set_timeout(this->timer_[timer_index].name, this->timer_duration_(timer_index),
this->timer_cbf_(timer_index));
this->timer_[timer_index].start_time = millis();
this->timer_[timer_index].active = true;
@@ -1587,7 +1585,7 @@ void Sprinkler::start_timer_(const SprinklerTimerIndex timer_index) {
bool Sprinkler::cancel_timer_(const SprinklerTimerIndex timer_index) {
this->timer_[timer_index].active = false;
return this->cancel_timeout(this->timer_[timer_index].name.c_str());
return this->cancel_timeout(this->timer_[timer_index].name);
}
bool Sprinkler::timer_active_(const SprinklerTimerIndex timer_index) { return this->timer_[timer_index].active; }

View File

@@ -3,7 +3,6 @@
#include "esphome/core/automation.h"
#include "esphome/core/component.h"
#include "esphome/core/hal.h"
#include "esphome/core/helpers.h"
#include "esphome/components/number/number.h"
#include "esphome/components/switch/switch.h"
@@ -554,8 +553,8 @@ class Sprinkler : public Component {
/// Sprinkler valve operator objects
std::vector<SprinklerValveOperator> valve_op_{2};
/// Valve control timers - FixedVector enforces that this can never grow beyond init() size
FixedVector<SprinklerTimer> timer_;
/// Valve control timers
std::vector<SprinklerTimer> timer_{};
/// Other Sprinkler instances we should be aware of (used to check if pumps are in use)
std::vector<Sprinkler *> other_controllers_;

View File

@@ -62,7 +62,7 @@ void Switch::publish_state(bool state) {
if (restore_mode & RESTORE_MODE_PERSISTENT_MASK)
this->rtc_.save(&this->state);
ESP_LOGD(TAG, "'%s' >> %s", this->name_.c_str(), ONOFF(this->state));
ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), ONOFF(this->state));
this->state_callback_.call(this->state);
#if defined(USE_SWITCH) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_switch_update(this);

View File

@@ -20,9 +20,9 @@ void Text::publish_state(const char *state, size_t len) {
this->state.assign(state, len);
}
if (this->traits.get_mode() == TEXT_MODE_PASSWORD) {
ESP_LOGD(TAG, "'%s' >> " LOG_SECRET("'%s'"), this->get_name().c_str(), this->state.c_str());
ESP_LOGD(TAG, "'%s': Sending state " LOG_SECRET("'%s'"), this->get_name().c_str(), this->state.c_str());
} else {
ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->state.c_str());
ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), this->state.c_str());
}
this->state_callback_.call(this->state);
#if defined(USE_TEXT) && defined(USE_CONTROLLER_REGISTRY)

View File

@@ -116,7 +116,7 @@ void TextSensor::internal_send_state_to_frontend(const char *state, size_t len)
void TextSensor::notify_frontend_() {
this->set_has_state(true);
ESP_LOGD(TAG, "'%s' >> '%s'", this->name_.c_str(), this->state.c_str());
ESP_LOGD(TAG, "'%s': Sending state '%s'", this->name_.c_str(), this->state.c_str());
this->callback_.call(this->state);
#if defined(USE_TEXT_SENSOR) && defined(USE_CONTROLLER_REGISTRY)
ControllerRegistry::notify_text_sensor_update(this);

View File

@@ -10,7 +10,7 @@ static const char *const TAG = "update";
void UpdateEntity::publish_state() {
ESP_LOGD(TAG,
"'%s' >>\n"
"'%s' - Publishing:\n"
" Current Version: %s",
this->name_.c_str(), this->update_info_.current_version.c_str());

View File

@@ -133,7 +133,7 @@ void Valve::add_on_state_callback(std::function<void()> &&f) { this->state_callb
void Valve::publish_state(bool save) {
this->position = clamp(this->position, 0.0f, 1.0f);
ESP_LOGD(TAG, "'%s' >>", this->name_.c_str());
ESP_LOGD(TAG, "'%s' - Publishing:", this->name_.c_str());
auto traits = this->get_traits();
if (traits.get_supports_position()) {
ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f);

View File

@@ -153,7 +153,7 @@ void WaterHeater::setup() {
void WaterHeater::publish_state() {
auto traits = this->get_traits();
ESP_LOGD(TAG,
"'%s' >>\n"
"'%s' - Sending state:\n"
" Mode: %s",
this->name_.c_str(), LOG_STR_ARG(water_heater_mode_to_string(this->mode_)));
if (!std::isnan(this->current_temperature_)) {

View File

@@ -203,7 +203,7 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_OTA): cv.boolean,
cv.Optional(CONF_LOG, default=True): cv.boolean,
cv.Optional(CONF_LOCAL): cv.boolean,
cv.Optional(CONF_COMPRESSION, default="gzip"): cv.one_of("gzip", "br"),
cv.Optional(CONF_COMPRESSION, default="br"): cv.one_of("br", "gzip"),
cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group),
}
).extend(cv.COMPONENT_SCHEMA),

View File

@@ -753,6 +753,9 @@ void WebServer::handle_button_request(AsyncWebServerRequest *request, const UrlM
}
request->send(404);
}
std::string WebServer::button_state_json_generator(WebServer *web_server, void *source) {
return web_server->button_json_((button::Button *) (source), DETAIL_STATE);
}
std::string WebServer::button_all_json_generator(WebServer *web_server, void *source) {
return web_server->button_json_((button::Button *) (source), DETAIL_ALL);
}

View File

@@ -295,7 +295,7 @@ class WebServer : public Controller,
/// Handle a button request under '/button/<id>/press'.
void handle_button_request(AsyncWebServerRequest *request, const UrlMatch &match);
// Buttons are stateless, so there is no button_state_json_generator
static std::string button_state_json_generator(WebServer *web_server, void *source);
static std::string button_all_json_generator(WebServer *web_server, void *source);
#endif

View File

@@ -44,15 +44,7 @@ class HashBase {
virtual size_t get_size() const = 0;
protected:
// ESP32 variants with DMA-based hardware SHA (all except original ESP32) require 32-byte aligned buffers.
// Original ESP32 uses a different hardware SHA implementation without DMA alignment requirements.
// Other platforms (ESP8266, RP2040, LibreTiny) use software SHA and don't need alignment.
// Storage sized for max(MD5=16, SHA256=32) bytes
#if defined(USE_ESP32) && !defined(USE_ESP32_VARIANT_ESP32)
alignas(32) uint8_t digest_[32];
#else
uint8_t digest_[32];
#endif
uint8_t digest_[32]; // Storage sized for max(MD5=16, SHA256=32) bytes
};
} // namespace esphome

View File

@@ -1,6 +1,6 @@
pylint==4.0.4
flake8==7.3.0 # also change in .pre-commit-config.yaml when updating
ruff==0.14.12 # also change in .pre-commit-config.yaml when updating
ruff==0.14.11 # also change in .pre-commit-config.yaml when updating
pyupgrade==3.21.2 # also change in .pre-commit-config.yaml when updating
pre-commit

View File

@@ -90,8 +90,6 @@ class Platform(StrEnum):
ESP32_S2_IDF = "esp32-s2-idf"
ESP32_S3_IDF = "esp32-s3-idf"
BK72XX_ARD = "bk72xx-ard" # LibreTiny BK7231N
RTL87XX_ARD = "rtl87xx-ard" # LibreTiny RTL8720x
LN882X_ARD = "ln882x-ard" # LibreTiny LN882x
RP2040_ARD = "rp2040-ard" # Raspberry Pi Pico
@@ -124,8 +122,8 @@ PLATFORM_SPECIFIC_COMPONENTS = frozenset(
# fastest build times, most sensitive to code size changes
# 3. ESP32 IDF - Primary ESP32 platform, most representative of modern ESPHome
# 4-6. Other ESP32 variants - Less commonly used but still supported
# 7-9. LibreTiny platforms (BK72XX, RTL87XX, LN882X) - good for detecting LibreTiny-specific changes
# 10. RP2040 - Raspberry Pi Pico platform
# 7. BK72XX - LibreTiny platform (good for detecting LibreTiny-specific changes)
# 8. RP2040 - Raspberry Pi Pico platform
MEMORY_IMPACT_PLATFORM_PREFERENCE = [
Platform.ESP32_C6_IDF, # ESP32-C6 IDF (newest, supports Thread/Zigbee)
Platform.ESP8266_ARD, # ESP8266 Arduino (most memory constrained, fastest builds)
@@ -134,8 +132,6 @@ MEMORY_IMPACT_PLATFORM_PREFERENCE = [
Platform.ESP32_S2_IDF, # ESP32-S2 IDF
Platform.ESP32_S3_IDF, # ESP32-S3 IDF
Platform.BK72XX_ARD, # LibreTiny BK7231N
Platform.RTL87XX_ARD, # LibreTiny RTL8720x
Platform.LN882X_ARD, # LibreTiny LN882x
Platform.RP2040_ARD, # Raspberry Pi Pico
]
@@ -415,8 +411,6 @@ def _detect_platform_hint_from_filename(filename: str) -> Platform | None:
- wifi_component_esp8266.cpp, *_esp8266.h -> ESP8266_ARD
- *_esp32*.cpp -> ESP32 IDF (generic)
- *_libretiny.cpp, *_bk72*.* -> BK72XX (LibreTiny)
- *_rtl87*.* -> RTL87XX (LibreTiny Realtek)
- *_ln882*.* -> LN882X (LibreTiny Lightning)
- *_pico.cpp, *_rp2040.* -> RP2040_ARD
Args:
@@ -450,12 +444,7 @@ def _detect_platform_hint_from_filename(filename: str) -> Platform | None:
if "esp32" in filename_lower:
return Platform.ESP32_IDF
# LibreTiny platforms (check specific variants before generic libretiny)
# Check specific variants first to handle paths like libretiny/wifi_rtl87xx.cpp
if "rtl87" in filename_lower:
return Platform.RTL87XX_ARD
if "ln882" in filename_lower:
return Platform.LN882X_ARD
# LibreTiny (via 'libretiny' pattern or BK72xx-specific files)
if "libretiny" in filename_lower or "bk72" in filename_lower:
return Platform.BK72XX_ARD

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="10mm" height="10mm" viewBox="0 0 100 100">
<rect x="0" y="0" width="100" height="100" fill="#00FF00"/>
<circle cx="50" cy="50" r="30" fill="#0000FF"/>
</svg>

Before

Width:  |  Height:  |  Size: 248 B

View File

@@ -5,21 +5,17 @@ from __future__ import annotations
from collections.abc import Callable
from pathlib import Path
from typing import Any
from unittest.mock import MagicMock, patch
import pytest
from esphome import config_validation as cv
from esphome.components.image import (
CONF_INVERT_ALPHA,
CONF_OPAQUE,
CONF_TRANSPARENCY,
CONFIG_SCHEMA,
get_all_image_metadata,
get_image_metadata,
write_image,
)
from esphome.const import CONF_DITHER, CONF_FILE, CONF_ID, CONF_RAW_DATA_ID, CONF_TYPE
from esphome.const import CONF_ID, CONF_RAW_DATA_ID, CONF_TYPE
from esphome.core import CORE
@@ -354,52 +350,3 @@ def test_get_all_image_metadata_empty() -> None:
"get_all_image_metadata should always return a dict"
)
# Length could be 0 or more depending on what's in CORE at test time
@pytest.fixture
def mock_progmem_array():
"""Mock progmem_array to avoid needing a proper ID object in tests."""
with patch("esphome.components.image.cg.progmem_array") as mock_progmem:
mock_progmem.return_value = MagicMock()
yield mock_progmem
@pytest.mark.asyncio
async def test_svg_with_mm_dimensions_succeeds(
component_config_path: Callable[[str], Path],
mock_progmem_array: MagicMock,
) -> None:
"""Test that SVG files with dimensions in mm are successfully processed."""
# Create a config for write_image without CONF_RESIZE
config = {
CONF_FILE: component_config_path("mm_dimensions.svg"),
CONF_TYPE: "BINARY",
CONF_TRANSPARENCY: CONF_OPAQUE,
CONF_DITHER: "NONE",
CONF_INVERT_ALPHA: False,
CONF_RAW_DATA_ID: "test_raw_data_id",
}
# This should succeed without raising an error
result = await write_image(config)
# Verify that write_image returns the expected tuple
assert isinstance(result, tuple), "write_image should return a tuple"
assert len(result) == 6, "write_image should return 6 values"
prog_arr, width, height, image_type, trans_value, frame_count = result
# Verify the dimensions are positive integers
# At 100 DPI, 10mm = ~39 pixels (10mm * 100dpi / 25.4mm_per_inch)
assert isinstance(width, int), "Width should be an integer"
assert isinstance(height, int), "Height should be an integer"
assert width > 0, "Width should be positive"
assert height > 0, "Height should be positive"
assert frame_count == 1, "Single image should have frame_count of 1"
# Verify we got reasonable dimensions from the mm-based SVG
assert 30 < width < 50, (
f"Width should be around 39 pixels for 10mm at 100dpi, got {width}"
)
assert 30 < height < 50, (
f"Height should be around 39 pixels for 10mm at 100dpi, got {height}"
)

View File

@@ -1472,24 +1472,6 @@ def test_detect_memory_impact_config_runs_at_component_limit(tmp_path: Path) ->
determine_jobs.Platform.BK72XX_ARD,
),
("esphome/components/ble/ble_bk72xx.cpp", determine_jobs.Platform.BK72XX_ARD),
# RTL87xx (LibreTiny Realtek) detection
(
"tests/components/logger/test.rtl87xx-ard.yaml",
determine_jobs.Platform.RTL87XX_ARD,
),
(
"esphome/components/libretiny/wifi_rtl87xx.cpp",
determine_jobs.Platform.RTL87XX_ARD,
),
# LN882x (LibreTiny Lightning) detection
(
"tests/components/logger/test.ln882x-ard.yaml",
determine_jobs.Platform.LN882X_ARD,
),
(
"esphome/components/libretiny/wifi_ln882x.cpp",
determine_jobs.Platform.LN882X_ARD,
),
# RP2040 / Raspberry Pi Pico detection
("esphome/components/gpio/gpio_rp2040.cpp", determine_jobs.Platform.RP2040_ARD),
("esphome/components/wifi/wifi_rp2040.cpp", determine_jobs.Platform.RP2040_ARD),
@@ -1519,10 +1501,6 @@ def test_detect_memory_impact_config_runs_at_component_limit(tmp_path: Path) ->
"esp32_in_name",
"libretiny",
"bk72xx",
"rtl87xx_test_yaml",
"rtl87xx_wifi",
"ln882x_test_yaml",
"ln882x_wifi",
"rp2040_gpio",
"rp2040_wifi",
"pico_i2c",