diff --git a/Doxyfile b/Doxyfile index d22d87bf8e..e5c3c1802d 100644 --- a/Doxyfile +++ b/Doxyfile @@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome # could be handy for archiving the generated documentation or if some version # control system is used. -PROJECT_NUMBER = 2025.5.0 +PROJECT_NUMBER = 2025.5.1 # 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/api/api_connection.cpp b/esphome/components/api/api_connection.cpp index 27dd44ae86..b4646a2d7d 100644 --- a/esphome/components/api/api_connection.cpp +++ b/esphome/components/api/api_connection.cpp @@ -4,11 +4,11 @@ #include #include #include "esphome/components/network/util.h" +#include "esphome/core/application.h" #include "esphome/core/entity_base.h" #include "esphome/core/hal.h" #include "esphome/core/log.h" #include "esphome/core/version.h" -#include "esphome/core/application.h" #ifdef USE_DEEP_SLEEP #include "esphome/components/deep_sleep/deep_sleep_component.h" @@ -153,7 +153,11 @@ void APIConnection::loop() { } else { this->last_traffic_ = App.get_loop_component_start_time(); // read a packet - this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); + if (buffer.data_len > 0) { + this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]); + } else { + this->read_message(0, buffer.type, nullptr); + } if (this->remove_) return; } diff --git a/esphome/components/esp32/core.cpp b/esphome/components/esp32/core.cpp index c90d68d00e..562bcba3c2 100644 --- a/esphome/components/esp32/core.cpp +++ b/esphome/components/esp32/core.cpp @@ -15,8 +15,9 @@ #ifdef USE_ARDUINO #include #else +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) #include - +#endif void setup(); void loop(); #endif @@ -63,7 +64,13 @@ uint32_t arch_get_cpu_cycle_count() { return cpu_hal_get_cycle_count(); } uint32_t arch_get_cpu_freq_hz() { uint32_t freq = 0; #ifdef USE_ESP_IDF +#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 1, 0) esp_clk_tree_src_get_freq_hz(SOC_MOD_CLK_CPU, ESP_CLK_TREE_SRC_FREQ_PRECISION_CACHED, &freq); +#else + rtc_cpu_freq_config_t config; + rtc_clk_cpu_freq_get_config(&config); + freq = config.freq_mhz * 1000000U; +#endif #elif defined(USE_ARDUINO) freq = ESP.getCpuFreqMHz() * 1000000; #endif diff --git a/esphome/components/logger/__init__.py b/esphome/components/logger/__init__.py index 4698c1d9f1..4136480629 100644 --- a/esphome/components/logger/__init__.py +++ b/esphome/components/logger/__init__.py @@ -24,6 +24,7 @@ from esphome.const import ( CONF_HARDWARE_UART, CONF_ID, CONF_LEVEL, + CONF_LOGGER, CONF_LOGS, CONF_ON_MESSAGE, CONF_TAG, @@ -247,6 +248,7 @@ CONFIG_SCHEMA = cv.All( async def to_code(config): baud_rate = config[CONF_BAUD_RATE] level = config[CONF_LEVEL] + CORE.data.setdefault(CONF_LOGGER, {})[CONF_LEVEL] = level initial_level = LOG_LEVELS[config.get(CONF_INITIAL_LEVEL, level)] log = cg.new_Pvariable( config[CONF_ID], diff --git a/esphome/components/logger/select/__init__.py b/esphome/components/logger/select/__init__.py index b1fc881537..2e83599eb4 100644 --- a/esphome/components/logger/select/__init__.py +++ b/esphome/components/logger/select/__init__.py @@ -5,7 +5,7 @@ from esphome.const import CONF_LEVEL, CONF_LOGGER, ENTITY_CATEGORY_CONFIG, ICON_ from esphome.core import CORE from esphome.cpp_helpers import register_component, register_parented -from .. import CONF_LOGGER_ID, LOG_LEVEL_SEVERITY, Logger, logger_ns +from .. import CONF_LOGGER_ID, LOG_LEVELS, Logger, logger_ns CODEOWNERS = ["@clydebarrow"] @@ -21,9 +21,10 @@ CONFIG_SCHEMA = select.select_schema( async def to_code(config): - levels = LOG_LEVEL_SEVERITY - index = levels.index(CORE.config[CONF_LOGGER][CONF_LEVEL]) + parent = await cg.get_variable(config[CONF_LOGGER_ID]) + levels = list(LOG_LEVELS) + index = levels.index(CORE.data[CONF_LOGGER][CONF_LEVEL]) levels = levels[: index + 1] var = await select.new_select(config, options=levels) - await register_parented(var, config[CONF_LOGGER_ID]) + await register_parented(var, parent) await register_component(var, config) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index d0dde01421..b51b6b8a85 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -36,29 +36,43 @@ from .types import ( # this will be populated later, in __init__.py to avoid circular imports. WIDGET_TYPES: dict = {} +TIME_TEXT_SCHEMA = cv.Schema( + { + cv.Required(CONF_TIME_FORMAT): cv.string, + cv.GenerateID(CONF_TIME): cv.templatable(cv.use_id(RealTimeClock)), + } +) + +PRINTF_TEXT_SCHEMA = cv.All( + cv.Schema( + { + cv.Required(CONF_FORMAT): cv.string, + cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_), + }, + ), + validate_printf, +) + + +def _validate_text(value): + """ + Do some sanity checking of the format to get better error messages + than using cv.Any + """ + if value is None: + raise cv.Invalid("No text specified") + if isinstance(value, dict): + if CONF_TIME_FORMAT in value: + return TIME_TEXT_SCHEMA(value) + return PRINTF_TEXT_SCHEMA(value) + + return cv.templatable(cv.string)(value) + + # A schema for text properties TEXT_SCHEMA = cv.Schema( { - cv.Optional(CONF_TEXT): cv.Any( - cv.All( - cv.Schema( - { - cv.Required(CONF_FORMAT): cv.string, - cv.Optional(CONF_ARGS, default=list): cv.ensure_list( - cv.lambda_ - ), - }, - ), - validate_printf, - ), - cv.Schema( - { - cv.Required(CONF_TIME_FORMAT): cv.string, - cv.GenerateID(CONF_TIME): cv.templatable(cv.use_id(RealTimeClock)), - } - ), - cv.templatable(cv.string), - ) + cv.Optional(CONF_TEXT): _validate_text, } ) diff --git a/esphome/components/micro_wake_word/streaming_model.cpp b/esphome/components/micro_wake_word/streaming_model.cpp index ce3d8c2e4c..38b88557e6 100644 --- a/esphome/components/micro_wake_word/streaming_model.cpp +++ b/esphome/components/micro_wake_word/streaming_model.cpp @@ -147,7 +147,11 @@ bool StreamingModel::perform_streaming_inference(const int8_t features[PREPROCES this->recent_streaming_probabilities_[this->last_n_index_] = output->data.uint8[0]; // probability; this->unprocessed_probability_status_ = true; } - this->ignore_windows_ = std::min(this->ignore_windows_ + 1, 0); + if (this->recent_streaming_probabilities_[this->last_n_index_] < this->probability_cutoff_) { + // Only increment ignore windows if less than the probability cutoff; this forces the model to "cool-off" from a + // previous detection and calling ``reset_probabilities`` so it avoids duplicate detections + this->ignore_windows_ = std::min(this->ignore_windows_ + 1, 0); + } } return true; } diff --git a/esphome/components/online_image/__init__.py b/esphome/components/online_image/__init__.py index 6b69bc240b..55b9037176 100644 --- a/esphome/components/online_image/__init__.py +++ b/esphome/components/online_image/__init__.py @@ -75,7 +75,7 @@ class PNGFormat(Format): def actions(self): cg.add_define("USE_ONLINE_IMAGE_PNG_SUPPORT") - cg.add_library("pngle", "1.0.2") + cg.add_library("pngle", "1.1.0") IMAGE_FORMATS = { diff --git a/esphome/components/online_image/png_image.cpp b/esphome/components/online_image/png_image.cpp index fc5fb554bf..2038d09ed0 100644 --- a/esphome/components/online_image/png_image.cpp +++ b/esphome/components/online_image/png_image.cpp @@ -34,12 +34,32 @@ static void init_callback(pngle_t *pngle, uint32_t w, uint32_t h) { * @param h The height of the rectangle to draw. * @param rgba The color to paint the rectangle in. */ -static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, uint8_t rgba[4]) { +static void draw_callback(pngle_t *pngle, uint32_t x, uint32_t y, uint32_t w, uint32_t h, const uint8_t rgba[4]) { PngDecoder *decoder = (PngDecoder *) pngle_get_user_data(pngle); Color color(rgba[0], rgba[1], rgba[2], rgba[3]); decoder->draw(x, y, w, h, color); } +PngDecoder::PngDecoder(OnlineImage *image) : ImageDecoder(image) { + { + pngle_t *pngle = this->allocator_.allocate(1, PNGLE_T_SIZE); + if (!pngle) { + ESP_LOGE(TAG, "Failed to allocate memory for PNGLE engine!"); + return; + } + memset(pngle, 0, PNGLE_T_SIZE); + pngle_reset(pngle); + this->pngle_ = pngle; + } +} + +PngDecoder::~PngDecoder() { + if (this->pngle_) { + pngle_reset(this->pngle_); + this->allocator_.deallocate(this->pngle_, PNGLE_T_SIZE); + } +} + int PngDecoder::prepare(size_t download_size) { ImageDecoder::prepare(download_size); if (!this->pngle_) { diff --git a/esphome/components/online_image/png_image.h b/esphome/components/online_image/png_image.h index 39f445c588..46519f8ef4 100644 --- a/esphome/components/online_image/png_image.h +++ b/esphome/components/online_image/png_image.h @@ -1,7 +1,8 @@ #pragma once -#include "image_decoder.h" #include "esphome/core/defines.h" +#include "esphome/core/helpers.h" +#include "image_decoder.h" #ifdef USE_ONLINE_IMAGE_PNG_SUPPORT #include @@ -18,13 +19,14 @@ class PngDecoder : public ImageDecoder { * * @param display The image to decode the stream into. */ - PngDecoder(OnlineImage *image) : ImageDecoder(image), pngle_(pngle_new()) {} - ~PngDecoder() override { pngle_destroy(this->pngle_); } + PngDecoder(OnlineImage *image); + ~PngDecoder() override; int prepare(size_t download_size) override; int HOT decode(uint8_t *buffer, size_t size) override; protected: + RAMAllocator allocator_; pngle_t *pngle_; }; diff --git a/esphome/components/speaker/media_player/audio_pipeline.cpp b/esphome/components/speaker/media_player/audio_pipeline.cpp index 60f562cc2c..ac122b6e0c 100644 --- a/esphome/components/speaker/media_player/audio_pipeline.cpp +++ b/esphome/components/speaker/media_player/audio_pipeline.cpp @@ -174,6 +174,16 @@ AudioPipelineState AudioPipeline::process_state() { } } + if ((event_bits & EventGroupBits::READER_MESSAGE_ERROR)) { + xEventGroupClearBits(this->event_group_, EventGroupBits::READER_MESSAGE_ERROR); + return AudioPipelineState::ERROR_READING; + } + + if ((event_bits & EventGroupBits::DECODER_MESSAGE_ERROR)) { + xEventGroupClearBits(this->event_group_, EventGroupBits::DECODER_MESSAGE_ERROR); + return AudioPipelineState::ERROR_DECODING; + } + if ((event_bits & EventGroupBits::READER_MESSAGE_FINISHED) && (!(event_bits & EventGroupBits::READER_MESSAGE_LOADED_MEDIA_TYPE) && (event_bits & EventGroupBits::DECODER_MESSAGE_FINISHED))) { @@ -203,16 +213,6 @@ AudioPipelineState AudioPipeline::process_state() { return AudioPipelineState::STOPPED; } - if ((event_bits & EventGroupBits::READER_MESSAGE_ERROR)) { - xEventGroupClearBits(this->event_group_, EventGroupBits::READER_MESSAGE_ERROR); - return AudioPipelineState::ERROR_READING; - } - - if ((event_bits & EventGroupBits::DECODER_MESSAGE_ERROR)) { - xEventGroupClearBits(this->event_group_, EventGroupBits::DECODER_MESSAGE_ERROR); - return AudioPipelineState::ERROR_DECODING; - } - if (this->pause_state_) { return AudioPipelineState::PAUSED; } diff --git a/esphome/components/tuya/select/__init__.py b/esphome/components/tuya/select/__init__.py index e5b2e36ce7..9f2b6f1e12 100644 --- a/esphome/components/tuya/select/__init__.py +++ b/esphome/components/tuya/select/__init__.py @@ -54,8 +54,8 @@ async def to_code(config): cg.add(var.set_select_mappings(list(options_map.keys()))) parent = await cg.get_variable(config[CONF_TUYA_ID]) cg.add(var.set_tuya_parent(parent)) - if enum_datapoint := config.get(CONF_ENUM_DATAPOINT, None) is not None: + if (enum_datapoint := config.get(CONF_ENUM_DATAPOINT, None)) is not None: cg.add(var.set_select_id(enum_datapoint, False)) - if int_datapoint := config.get(CONF_INT_DATAPOINT, None) is not None: + if (int_datapoint := config.get(CONF_INT_DATAPOINT, None)) is not None: cg.add(var.set_select_id(int_datapoint, True)) cg.add(var.set_optimistic(config[CONF_OPTIMISTIC])) diff --git a/esphome/const.py b/esphome/const.py index 7a61c93428..2fc30beaaa 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2025.5.0" +__version__ = "2025.5.1" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( diff --git a/esphome/core/helpers.h b/esphome/core/helpers.h index 7866eaa9bd..4212aeca98 100644 --- a/esphome/core/helpers.h +++ b/esphome/core/helpers.h @@ -1,6 +1,7 @@ #pragma once #include +#include #include #include #include diff --git a/esphome/core/string_ref.h b/esphome/core/string_ref.h index f3dc3a38b0..c4320107e3 100644 --- a/esphome/core/string_ref.h +++ b/esphome/core/string_ref.h @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include diff --git a/esphome/dashboard/web_server.py b/esphome/dashboard/web_server.py index 6196e01760..ddef2e7e2b 100644 --- a/esphome/dashboard/web_server.py +++ b/esphome/dashboard/web_server.py @@ -601,10 +601,12 @@ class DownloadListRequestHandler(BaseHandler): loop = asyncio.get_running_loop() try: downloads_json = await loop.run_in_executor(None, self._get, configuration) - except vol.Invalid: + except vol.Invalid as exc: + _LOGGER.exception("Error while fetching downloads", exc_info=exc) self.send_error(404) return if downloads_json is None: + _LOGGER.error("Configuration %s not found", configuration) self.send_error(404) return self.set_status(200) @@ -618,14 +620,17 @@ class DownloadListRequestHandler(BaseHandler): if storage_json is None: return None - config = yaml_util.load_yaml(settings.rel_path(configuration)) + try: + config = yaml_util.load_yaml(settings.rel_path(configuration)) - if const.CONF_EXTERNAL_COMPONENTS in config: - from esphome.components.external_components import ( - do_external_components_pass, - ) + if const.CONF_EXTERNAL_COMPONENTS in config: + from esphome.components.external_components import ( + do_external_components_pass, + ) - do_external_components_pass(config) + do_external_components_pass(config) + except vol.Invalid: + _LOGGER.info("Could not parse `external_components`, skipping") from esphome.components.esp32 import VARIANTS as ESP32_VARIANTS diff --git a/platformio.ini b/platformio.ini index 292188c6fa..23ed89c262 100644 --- a/platformio.ini +++ b/platformio.ini @@ -40,7 +40,7 @@ lib_deps = wjtje/qr-code-generator-library@1.7.0 ; qr_code functionpointer/arduino-MLX90393@1.0.2 ; mlx90393 pavlodn/HaierProtocol@0.9.31 ; haier - kikuchan98/pngle@1.0.2 ; online_image + kikuchan98/pngle@1.1.0 ; online_image ; Using the repository directly, otherwise ESP-IDF can't use the library https://github.com/bitbank2/JPEGDEC.git#ca1e0f2 ; online_image ; This is using the repository until a new release is published to PlatformIO