diff --git a/Doxyfile b/Doxyfile index e98eac6aa5..aa6a2f169e 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2026.1.0b1 +PROJECT_NUMBER = 2026.1.0b2 # Using the PROJECT_BRIEF tag one can provide an optional one line description # for a project that appears at the top of each page and should give viewer a diff --git a/esphome/components/alarm_control_panel/alarm_control_panel.cpp b/esphome/components/alarm_control_panel/alarm_control_panel.cpp index 89c0908a74..248b5065ad 100644 --- a/esphome/components/alarm_control_panel/alarm_control_panel.cpp +++ b/esphome/components/alarm_control_panel/alarm_control_panel.cpp @@ -31,7 +31,8 @@ void AlarmControlPanel::publish_state(AlarmControlPanelState state) { this->last_update_ = millis(); if (state != this->current_state_) { auto prev_state = this->current_state_; - ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)), + ESP_LOGD(TAG, "'%s' >> %s (was %s)", this->get_name().c_str(), + 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 diff --git a/esphome/components/api/api_server.cpp b/esphome/components/api/api_server.cpp index a4eeb4dd5e..a63d33f73b 100644 --- a/esphome/components/api/api_server.cpp +++ b/esphome/components/api/api_server.cpp @@ -241,8 +241,10 @@ 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_) \ - c->send_##entity_name##_state(obj); \ + for (auto &c : this->clients_) { \ + if (c->flags_.state_subscription) \ + c->send_##entity_name##_state(obj); \ + } \ } #ifdef USE_BINARY_SENSOR @@ -321,8 +323,10 @@ 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_) - c->send_event(obj); + for (auto &c : this->clients_) { + if (c->flags_.state_subscription) + c->send_event(obj); + } } #endif @@ -331,8 +335,10 @@ void APIServer::on_event(event::Event *obj) { void APIServer::on_update(update::UpdateEntity *obj) { if (obj->is_internal()) return; - for (auto &c : this->clients_) - c->send_update_state(obj); + for (auto &c : this->clients_) { + if (c->flags_.state_subscription) + c->send_update_state(obj); + } } #endif diff --git a/esphome/components/binary_sensor/binary_sensor.cpp b/esphome/components/binary_sensor/binary_sensor.cpp index 86b7350aa8..4fe2a019e0 100644 --- a/esphome/components/binary_sensor/binary_sensor.cpp +++ b/esphome/components/binary_sensor/binary_sensor.cpp @@ -44,7 +44,7 @@ bool BinarySensor::set_new_state(const optional &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; diff --git a/esphome/components/captive_portal/__init__.py b/esphome/components/captive_portal/__init__.py index 4b30dc5d16..049618219e 100644 --- a/esphome/components/captive_portal/__init__.py +++ b/esphome/components/captive_portal/__init__.py @@ -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="br"): cv.one_of("br", "gzip"), + cv.Optional(CONF_COMPRESSION, default="gzip"): cv.one_of("gzip", "br"), } ).extend(cv.COMPONENT_SCHEMA), cv.only_on( diff --git a/esphome/components/climate/climate.cpp b/esphome/components/climate/climate.cpp index 7611d33cbf..816bd5dfcb 100644 --- a/esphome/components/climate/climate.cpp +++ b/esphome/components/climate/climate.cpp @@ -436,7 +436,7 @@ void Climate::save_state_() { } void Climate::publish_state() { - ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str()); + ESP_LOGD(TAG, "'%s' >>", this->name_.c_str()); auto traits = this->get_traits(); ESP_LOGD(TAG, " Mode: %s", LOG_STR_ARG(climate_mode_to_string(this->mode))); diff --git a/esphome/components/cover/cover.cpp b/esphome/components/cover/cover.cpp index feac9823b9..97b8c2213e 100644 --- a/esphome/components/cover/cover.cpp +++ b/esphome/components/cover/cover.cpp @@ -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' - Publishing:", this->name_.c_str()); + ESP_LOGD(TAG, "'%s' >>", this->name_.c_str()); auto traits = this->get_traits(); if (traits.get_supports_position()) { ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f); diff --git a/esphome/components/datetime/date_entity.cpp b/esphome/components/datetime/date_entity.cpp index c061bc81f7..c5ea051914 100644 --- a/esphome/components/datetime/date_entity.cpp +++ b/esphome/components/datetime/date_entity.cpp @@ -30,7 +30,7 @@ void DateEntity::publish_state() { return; } this->set_has_state(true); - ESP_LOGD(TAG, "'%s': Sending date %d-%d-%d", this->get_name().c_str(), this->year_, this->month_, this->day_); + ESP_LOGD(TAG, "'%s' >> %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); diff --git a/esphome/components/datetime/datetime_entity.cpp b/esphome/components/datetime/datetime_entity.cpp index 694f9c5721..fd3901fcfc 100644 --- a/esphome/components/datetime/datetime_entity.cpp +++ b/esphome/components/datetime/datetime_entity.cpp @@ -45,8 +45,8 @@ void DateTimeEntity::publish_state() { return; } this->set_has_state(true); - 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_); + 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_); this->state_callback_.call(); #if defined(USE_DATETIME_DATETIME) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_datetime_update(this); diff --git a/esphome/components/datetime/time_entity.cpp b/esphome/components/datetime/time_entity.cpp index 0e71c95238..d0b8875ed1 100644 --- a/esphome/components/datetime/time_entity.cpp +++ b/esphome/components/datetime/time_entity.cpp @@ -26,8 +26,7 @@ void TimeEntity::publish_state() { return; } this->set_has_state(true); - ESP_LOGD(TAG, "'%s': Sending time %02d:%02d:%02d", this->get_name().c_str(), this->hour_, this->minute_, - this->second_); + ESP_LOGD(TAG, "'%s' >> %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); diff --git a/esphome/components/debug/sensor.py b/esphome/components/debug/sensor.py index 4484f15935..6a8e2cd828 100644 --- a/esphome/components/debug/sensor.py +++ b/esphome/components/debug/sensor.py @@ -17,7 +17,11 @@ from esphome.const import ( UNIT_PERCENT, ) -from . import CONF_DEBUG_ID, DebugComponent +from . import ( # noqa: F401 pylint: disable=unused-import + CONF_DEBUG_ID, + FILTER_SOURCE_FILES, + DebugComponent, +) DEPENDENCIES = ["debug"] diff --git a/esphome/components/debug/text_sensor.py b/esphome/components/debug/text_sensor.py index 96ef231850..c69b8d9461 100644 --- a/esphome/components/debug/text_sensor.py +++ b/esphome/components/debug/text_sensor.py @@ -8,7 +8,11 @@ from esphome.const import ( ICON_RESTART, ) -from . import CONF_DEBUG_ID, DebugComponent +from . import ( # noqa: F401 pylint: disable=unused-import + CONF_DEBUG_ID, + FILTER_SOURCE_FILES, + DebugComponent, +) DEPENDENCIES = ["debug"] diff --git a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp index 9f8ae3277e..d69a438578 100644 --- a/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp +++ b/esphome/components/esp32_hosted/update/esp32_hosted_update.cpp @@ -294,8 +294,7 @@ bool Esp32HostedUpdate::stream_firmware_to_coprocessor_() { } // Stream firmware to coprocessor while computing SHA256 - // Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+) - alignas(32) sha256::SHA256 hasher; + sha256::SHA256 hasher; hasher.init(); uint8_t buffer[CHUNK_SIZE]; @@ -352,8 +351,7 @@ bool Esp32HostedUpdate::write_embedded_firmware_to_coprocessor_() { } // Verify SHA256 before writing - // Hardware SHA acceleration requires 32-byte alignment on some chips (ESP32-S3 with IDF 5.5.x+) - alignas(32) sha256::SHA256 hasher; + sha256::SHA256 hasher; hasher.init(); hasher.add(this->firmware_data_, this->firmware_size_); hasher.calculate(); diff --git a/esphome/components/esphome/ota/ota_esphome.cpp b/esphome/components/esphome/ota/ota_esphome.cpp index b2ae185687..df2ea98f2c 100644 --- a/esphome/components/esphome/ota/ota_esphome.cpp +++ b/esphome/components/esphome/ota/ota_esphome.cpp @@ -563,11 +563,9 @@ 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-S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame + // CRITICAL ESP32-S2/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. - // 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; + sha256::SHA256 hasher; const size_t hex_size = hasher.get_size() * 2; const size_t nonce_len = hasher.get_size() / 4; @@ -639,11 +637,9 @@ bool ESPHomeOTAComponent::handle_auth_read_() { const char *cnonce = nonce + hex_size; const char *response = cnonce + hex_size; - // CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION: Hash object must stay in same stack frame + // CRITICAL ESP32-S2/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. - // 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; + sha256::SHA256 hasher; hasher.init(); hasher.add(this->password_.c_str(), this->password_.length()); diff --git a/esphome/components/event/event.cpp b/esphome/components/event/event.cpp index 4c74a11388..8015f2255a 100644 --- a/esphome/components/event/event.cpp +++ b/esphome/components/event/event.cpp @@ -22,7 +22,7 @@ void Event::trigger(const std::string &event_type) { return; } this->last_event_type_ = found; - ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), this->last_event_type_); + ESP_LOGD(TAG, "'%s' >> '%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); diff --git a/esphome/components/fan/fan.cpp b/esphome/components/fan/fan.cpp index 2e48d84eb9..02fde730eb 100644 --- a/esphome/components/fan/fan.cpp +++ b/esphome/components/fan/fan.cpp @@ -201,7 +201,7 @@ void Fan::publish_state() { auto traits = this->get_traits(); ESP_LOGD(TAG, - "'%s' - Sending state:\n" + "'%s' >>\n" " State: %s", this->name_.c_str(), ONOFF(this->state)); if (traits.supports_speed()) { diff --git a/esphome/components/image/__init__.py b/esphome/components/image/__init__.py index 3f8d909824..6ff75d7709 100644 --- a/esphome/components/image/__init__.py +++ b/esphome/components/image/__init__.py @@ -665,15 +665,10 @@ async def write_image(config, all_frames=False): if is_svg_file(path): import resvg_py - 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)) + resize = resize or (None, None) + image_data = resvg_py.svg_to_bytes( + svg_path=str(path), width=resize[0], height=resize[1], dpi=100 + ) # Convert bytes to Pillow Image image = Image.open(io.BytesIO(image_data)) diff --git a/esphome/components/lock/lock.cpp b/esphome/components/lock/lock.cpp index 018f5113e3..aca6ec10f3 100644 --- a/esphome/components/lock/lock.cpp +++ b/esphome/components/lock/lock.cpp @@ -52,7 +52,7 @@ void Lock::publish_state(LockState state) { this->state = state; this->rtc_.save(&this->state); - ESP_LOGD(TAG, "'%s': Sending state %s", this->name_.c_str(), LOG_STR_ARG(lock_state_to_string(state))); + ESP_LOGD(TAG, "'%s' >> %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); diff --git a/esphome/components/nextion/display.py b/esphome/components/nextion/display.py index b95df55a61..0b4ba3a171 100644 --- a/esphome/components/nextion/display.py +++ b/esphome/components/nextion/display.py @@ -11,7 +11,12 @@ from esphome.const import ( ) from esphome.core import CORE, TimePeriod -from . import Nextion, nextion_ns, nextion_ref +from . import ( # noqa: F401 pylint: disable=unused-import + FILTER_SOURCE_FILES, + Nextion, + nextion_ns, + nextion_ref, +) from .base_component import ( CONF_AUTO_WAKE_ON_TOUCH, CONF_COMMAND_SPACING, diff --git a/esphome/components/number/number.cpp b/esphome/components/number/number.cpp index 992100ead0..b0af604189 100644 --- a/esphome/components/number/number.cpp +++ b/esphome/components/number/number.cpp @@ -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': Sending state %f", this->get_name().c_str(), state); + ESP_LOGD(TAG, "'%s' >> %.2f", this->get_name().c_str(), state); this->state_callback_.call(state); #if defined(USE_NUMBER) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_number_update(this); diff --git a/esphome/components/qr_code/qr_code.cpp b/esphome/components/qr_code/qr_code.cpp index c2db741e17..0322c8a141 100644 --- a/esphome/components/qr_code/qr_code.cpp +++ b/esphome/components/qr_code/qr_code.cpp @@ -27,7 +27,16 @@ 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(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)) { diff --git a/esphome/components/remote_receiver/binary_sensor.py b/esphome/components/remote_receiver/binary_sensor.py index 218b40d6cc..fe3e2af950 100644 --- a/esphome/components/remote_receiver/binary_sensor.py +++ b/esphome/components/remote_receiver/binary_sensor.py @@ -1,5 +1,7 @@ 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 diff --git a/esphome/components/safe_mode/safe_mode.cpp b/esphome/components/safe_mode/safe_mode.cpp index ef6ebea247..f32511531a 100644 --- a/esphome/components/safe_mode/safe_mode.cpp +++ b/esphome/components/safe_mode/safe_mode.cpp @@ -9,7 +9,7 @@ #include #include -#ifdef USE_OTA_ROLLBACK +#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) #include #endif @@ -26,6 +26,17 @@ 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_; @@ -36,7 +47,7 @@ void SafeModeComponent::dump_config() { } } -#ifdef USE_OTA_ROLLBACK +#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) const esp_partition_t *last_invalid = esp_ota_get_last_invalid_partition(); if (last_invalid != nullptr) { ESP_LOGW(TAG, @@ -55,7 +66,7 @@ void SafeModeComponent::loop() { ESP_LOGI(TAG, "Boot seems successful; resetting boot loop counter"); this->clean_rtc(); this->boot_successful_ = true; -#ifdef USE_OTA_ROLLBACK +#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) // Mark OTA partition as valid to prevent rollback esp_ota_mark_app_valid_cancel_rollback(); #endif @@ -90,6 +101,12 @@ 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(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; diff --git a/esphome/components/safe_mode/safe_mode.h b/esphome/components/safe_mode/safe_mode.h index 4aefd11458..d6f669f39f 100644 --- a/esphome/components/safe_mode/safe_mode.h +++ b/esphome/components/safe_mode/safe_mode.h @@ -5,6 +5,10 @@ #include "esphome/core/helpers.h" #include "esphome/core/preferences.h" +#if defined(USE_ESP32) && defined(USE_OTA_ROLLBACK) +#include +#endif + namespace esphome::safe_mode { /// SafeModeComponent provides a safe way to recover from repeated boot failures @@ -42,6 +46,9 @@ 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 diff --git a/esphome/components/select/select.cpp b/esphome/components/select/select.cpp index 3d70e94d47..91e27b30de 100644 --- a/esphome/components/select/select.cpp +++ b/esphome/components/select/select.cpp @@ -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': Sending state %s (index %zu)", this->get_name().c_str(), option, index); + ESP_LOGD(TAG, "'%s' >> %s (%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); diff --git a/esphome/components/sensor/sensor.cpp b/esphome/components/sensor/sensor.cpp index 64678f8d0c..9fdb7bbafd 100644 --- a/esphome/components/sensor/sensor.cpp +++ b/esphome/components/sensor/sensor.cpp @@ -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': 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()); + 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()); this->callback_.call(state); #if defined(USE_SENSOR) && defined(USE_CONTROLLER_REGISTRY) ControllerRegistry::notify_sensor_update(this); diff --git a/esphome/components/sha256/sha256.cpp b/esphome/components/sha256/sha256.cpp index 48559d7c73..23995e6534 100644 --- a/esphome/components/sha256/sha256.cpp +++ b/esphome/components/sha256/sha256.cpp @@ -10,26 +10,24 @@ namespace esphome::sha256 { #if defined(USE_ESP32) || defined(USE_LIBRETINY) -// CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS (IDF 5.5.x): +// CRITICAL ESP32 HARDWARE SHA ACCELERATION REQUIREMENTS (IDF 5.5.x): // -// 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: +// 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: // -// 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 +// 1. 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]). // -// 3. SAME STACK FRAME ONLY: The SHA256 object must be created and used entirely within the same +// 2. 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() { -// alignas(32) sha256::SHA256 hasher; // Created locally with proper alignment +// sha256::SHA256 hasher; // hasher.init(); // hasher.add(data, len); // Any size, no chunking needed // hasher.calculate(); @@ -37,9 +35,9 @@ namespace esphome::sha256 { // // hasher destroyed when function returns // } // -// INCORRECT USAGE (WILL FAIL ON ESP32-S3): +// INCORRECT USAGE (WILL FAIL): // void my_function() { -// sha256::SHA256 hasher; // WRONG: Missing alignas(32) +// sha256::SHA256 hasher; // helper(&hasher); // WRONG: Passed to different stack frame // } // void helper(HashBase *h) { diff --git a/esphome/components/sha256/sha256.h b/esphome/components/sha256/sha256.h index 17d80636f1..bafb359485 100644 --- a/esphome/components/sha256/sha256.h +++ b/esphome/components/sha256/sha256.h @@ -24,13 +24,14 @@ namespace esphome::sha256 { /// SHA256 hash implementation. /// -/// 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 +/// 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. /// /// Example usage: -/// alignas(32) sha256::SHA256 hasher; +/// sha256::SHA256 hasher; /// hasher.init(); /// hasher.add(data, len); /// hasher.calculate(); diff --git a/esphome/components/switch/switch.cpp b/esphome/components/switch/switch.cpp index 3c3a437ff3..069533fa78 100644 --- a/esphome/components/switch/switch.cpp +++ b/esphome/components/switch/switch.cpp @@ -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': Sending state %s", this->name_.c_str(), ONOFF(this->state)); + ESP_LOGD(TAG, "'%s' >> %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); diff --git a/esphome/components/text/text.cpp b/esphome/components/text/text.cpp index c2ade56f69..e3f74b685b 100644 --- a/esphome/components/text/text.cpp +++ b/esphome/components/text/text.cpp @@ -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': Sending state " LOG_SECRET("'%s'"), this->get_name().c_str(), this->state.c_str()); + ESP_LOGD(TAG, "'%s' >> " LOG_SECRET("'%s'"), this->get_name().c_str(), this->state.c_str()); } else { - ESP_LOGD(TAG, "'%s': Sending state %s", this->get_name().c_str(), this->state.c_str()); + ESP_LOGD(TAG, "'%s' >> '%s'", this->get_name().c_str(), this->state.c_str()); } this->state_callback_.call(this->state); #if defined(USE_TEXT) && defined(USE_CONTROLLER_REGISTRY) diff --git a/esphome/components/text_sensor/text_sensor.cpp b/esphome/components/text_sensor/text_sensor.cpp index 66301564a4..86e2387dc7 100644 --- a/esphome/components/text_sensor/text_sensor.cpp +++ b/esphome/components/text_sensor/text_sensor.cpp @@ -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': Sending state '%s'", this->name_.c_str(), this->state.c_str()); + ESP_LOGD(TAG, "'%s' >> '%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); diff --git a/esphome/components/update/update_entity.cpp b/esphome/components/update/update_entity.cpp index 6d13341a8a..515e4c2c18 100644 --- a/esphome/components/update/update_entity.cpp +++ b/esphome/components/update/update_entity.cpp @@ -10,7 +10,7 @@ static const char *const TAG = "update"; void UpdateEntity::publish_state() { ESP_LOGD(TAG, - "'%s' - Publishing:\n" + "'%s' >>\n" " Current Version: %s", this->name_.c_str(), this->update_info_.current_version.c_str()); diff --git a/esphome/components/valve/valve.cpp b/esphome/components/valve/valve.cpp index fed113afc2..a9086747ce 100644 --- a/esphome/components/valve/valve.cpp +++ b/esphome/components/valve/valve.cpp @@ -133,7 +133,7 @@ void Valve::add_on_state_callback(std::function &&f) { this->state_callb void Valve::publish_state(bool save) { this->position = clamp(this->position, 0.0f, 1.0f); - ESP_LOGD(TAG, "'%s' - Publishing:", this->name_.c_str()); + ESP_LOGD(TAG, "'%s' >>", this->name_.c_str()); auto traits = this->get_traits(); if (traits.get_supports_position()) { ESP_LOGD(TAG, " Position: %.0f%%", this->position * 100.0f); diff --git a/esphome/components/water_heater/water_heater.cpp b/esphome/components/water_heater/water_heater.cpp index d092203d06..7b947057e1 100644 --- a/esphome/components/water_heater/water_heater.cpp +++ b/esphome/components/water_heater/water_heater.cpp @@ -153,7 +153,7 @@ void WaterHeater::setup() { void WaterHeater::publish_state() { auto traits = this->get_traits(); ESP_LOGD(TAG, - "'%s' - Sending state:\n" + "'%s' >>\n" " Mode: %s", this->name_.c_str(), LOG_STR_ARG(water_heater_mode_to_string(this->mode_))); if (!std::isnan(this->current_temperature_)) { diff --git a/esphome/components/web_server/__init__.py b/esphome/components/web_server/__init__.py index 16ac9d054c..3f1e094afc 100644 --- a/esphome/components/web_server/__init__.py +++ b/esphome/components/web_server/__init__.py @@ -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="br"): cv.one_of("br", "gzip"), + cv.Optional(CONF_COMPRESSION, default="gzip"): cv.one_of("gzip", "br"), cv.Optional(CONF_SORTING_GROUPS): cv.ensure_list(sorting_group), } ).extend(cv.COMPONENT_SCHEMA), diff --git a/esphome/const.py b/esphome/const.py index 95ccfb9dee..e99fbb8283 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2026.1.0b1" +__version__ = "2026.1.0b2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( diff --git a/esphome/core/hash_base.h b/esphome/core/hash_base.h index 0c1c2dce33..606cd3080c 100644 --- a/esphome/core/hash_base.h +++ b/esphome/core/hash_base.h @@ -44,7 +44,15 @@ class HashBase { virtual size_t get_size() const = 0; protected: - uint8_t digest_[32]; // Storage sized for max(MD5=16, SHA256=32) bytes +// 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 }; } // namespace esphome diff --git a/tests/component_tests/image/config/mm_dimensions.svg b/tests/component_tests/image/config/mm_dimensions.svg new file mode 100644 index 0000000000..bb64433a4d --- /dev/null +++ b/tests/component_tests/image/config/mm_dimensions.svg @@ -0,0 +1,5 @@ + + + + + diff --git a/tests/component_tests/image/test_init.py b/tests/component_tests/image/test_init.py index 930bbac8d1..c9481a0e1d 100644 --- a/tests/component_tests/image/test_init.py +++ b/tests/component_tests/image/test_init.py @@ -5,17 +5,21 @@ 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_ID, CONF_RAW_DATA_ID, CONF_TYPE +from esphome.const import CONF_DITHER, CONF_FILE, CONF_ID, CONF_RAW_DATA_ID, CONF_TYPE from esphome.core import CORE @@ -350,3 +354,52 @@ 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}" + )