From 0aa3c9685e4e9454f0d2ffdde6214bb5a5d41dce Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Tue, 29 Jul 2025 06:43:22 +1000 Subject: [PATCH 1/5] [lvgl] Bugfix for tileview (#9938) --- esphome/components/lvgl/widgets/tileview.py | 22 ++++++++++++++------- tests/components/lvgl/lvgl-package.yaml | 2 +- 2 files changed, 16 insertions(+), 8 deletions(-) diff --git a/esphome/components/lvgl/widgets/tileview.py b/esphome/components/lvgl/widgets/tileview.py index 3865d404e2..5e3a95f017 100644 --- a/esphome/components/lvgl/widgets/tileview.py +++ b/esphome/components/lvgl/widgets/tileview.py @@ -15,7 +15,7 @@ from ..defines import ( TILE_DIRECTIONS, literal, ) -from ..lv_validation import animated, lv_int +from ..lv_validation import animated, lv_int, lv_pct from ..lvcode import lv, lv_assign, lv_expr, lv_obj, lv_Pvariable from ..schemas import container_schema from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr @@ -41,8 +41,8 @@ TILEVIEW_SCHEMA = cv.Schema( container_schema( obj_spec, { - cv.Required(CONF_ROW): lv_int, - cv.Required(CONF_COLUMN): lv_int, + cv.Required(CONF_ROW): cv.positive_int, + cv.Required(CONF_COLUMN): cv.positive_int, cv.GenerateID(): cv.declare_id(lv_tile_t), cv.Optional(CONF_DIR, default="ALL"): TILE_DIRECTIONS.several_of, }, @@ -63,21 +63,29 @@ class TileviewType(WidgetType): ) async def to_code(self, w: Widget, config: dict): - for tile_conf in config.get(CONF_TILES, ()): + tiles = config[CONF_TILES] + for tile_conf in tiles: w_id = tile_conf[CONF_ID] tile_obj = lv_Pvariable(lv_obj_t, w_id) tile = Widget.create(w_id, tile_obj, tile_spec, tile_conf) dirs = tile_conf[CONF_DIR] if isinstance(dirs, list): dirs = "|".join(dirs) + row_pos = tile_conf[CONF_ROW] + col_pos = tile_conf[CONF_COLUMN] lv_assign( tile_obj, - lv_expr.tileview_add_tile( - w.obj, tile_conf[CONF_COLUMN], tile_conf[CONF_ROW], literal(dirs) - ), + lv_expr.tileview_add_tile(w.obj, col_pos, row_pos, literal(dirs)), ) + # Bugfix for LVGL 8.x + lv_obj.set_pos(tile_obj, lv_pct(col_pos * 100), lv_pct(row_pos * 100)) await set_obj_properties(tile, tile_conf) await add_widgets(tile, tile_conf) + if tiles: + # Set the first tile as active + lv_obj.set_tile_id( + w.obj, tiles[0][CONF_COLUMN], tiles[0][CONF_ROW], literal("LV_ANIM_OFF") + ) tileview_spec = TileviewType() diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 46341c266d..853466c9cc 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -738,7 +738,7 @@ lvgl: id: bar_id value: !lambda return (int)((float)rand() / RAND_MAX * 100); start_value: !lambda return (int)((float)rand() / RAND_MAX * 100); - mode: symmetrical + mode: range - logger.log: format: "bar value %f" args: [x] From a9b27d1966015b4eba1e1b9525cf839d37d9420f Mon Sep 17 00:00:00 2001 From: "J. Nick Koston" Date: Sun, 3 Aug 2025 14:23:45 -1000 Subject: [PATCH 2/5] [api] Fix OTA progress updates not being sent when main loop is blocked (#10049) --- esphome/components/api/api_connection.h | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/esphome/components/api/api_connection.h b/esphome/components/api/api_connection.h index 0051a143de..391f4f088f 100644 --- a/esphome/components/api/api_connection.h +++ b/esphome/components/api/api_connection.h @@ -657,10 +657,16 @@ class APIConnection : public APIServerConnection { bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type, uint8_t estimated_size) { // Try to send immediately if: - // 1. We should try to send immediately (should_try_send_immediately = true) - // 2. Batch delay is 0 (user has opted in to immediate sending) - // 3. Buffer has space available - if (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0 && + // 1. It's an UpdateStateResponse (always send immediately to handle cases where + // the main loop is blocked, e.g., during OTA updates) + // 2. OR: We should try to send immediately (should_try_send_immediately = true) + // AND Batch delay is 0 (user has opted in to immediate sending) + // 3. AND: Buffer has space available + if (( +#ifdef USE_UPDATE + message_type == UpdateStateResponse::MESSAGE_TYPE || +#endif + (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) && this->helper_->can_write_without_blocking()) { // Now actually encode and send if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) && From da573a217dab381749fff96619bd37c2287aab23 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Mon, 4 Aug 2025 14:55:54 +1000 Subject: [PATCH 3/5] [font] Catch file load exception (#10058) Co-authored-by: clydeps --- esphome/components/font/__init__.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/esphome/components/font/__init__.py b/esphome/components/font/__init__.py index 7d9a35647e..4ecc76c561 100644 --- a/esphome/components/font/__init__.py +++ b/esphome/components/font/__init__.py @@ -15,6 +15,7 @@ from freetype import ( FT_LOAD_RENDER, FT_LOAD_TARGET_MONO, Face, + FT_Exception, ft_pixel_mode_mono, ) import requests @@ -94,7 +95,14 @@ class FontCache(MutableMapping): return self.store[self._keytransform(item)] def __setitem__(self, key, value): - self.store[self._keytransform(key)] = Face(str(value)) + transformed = self._keytransform(key) + try: + self.store[transformed] = Face(str(value)) + except FT_Exception as exc: + file = transformed.split(":", 1) + raise cv.Invalid( + f"{file[0].capitalize()} {file[1]} is not a valid font file" + ) from exc FONT_CACHE = FontCache() From 532e3e370f7d8b39f0065ed5dd875456a1c203a1 Mon Sep 17 00:00:00 2001 From: Chris Beswick Date: Mon, 4 Aug 2025 15:43:44 +0100 Subject: [PATCH 4/5] [i2s_audio] Use high-pass filter for dc offset correction (#10005) --- .../microphone/i2s_audio_microphone.cpp | 64 +++++++++++++------ .../microphone/i2s_audio_microphone.h | 3 +- 2 files changed, 48 insertions(+), 19 deletions(-) diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp index 0477e0682d..f442c74c9f 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.cpp @@ -24,9 +24,6 @@ static const uint32_t READ_DURATION_MS = 16; static const size_t TASK_STACK_SIZE = 4096; static const ssize_t TASK_PRIORITY = 23; -// Use an exponential moving average to correct a DC offset with weight factor 1/1000 -static const int32_t DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR = 1000; - static const char *const TAG = "i2s_audio.microphone"; enum MicrophoneEventGroupBits : uint32_t { @@ -382,26 +379,57 @@ void I2SAudioMicrophone::mic_task(void *params) { } void I2SAudioMicrophone::fix_dc_offset_(std::vector &data) { + /** + * From https://www.musicdsp.org/en/latest/Filters/135-dc-filter.html: + * + * y(n) = x(n) - x(n-1) + R * y(n-1) + * R = 1 - (pi * 2 * frequency / samplerate) + * + * From https://en.wikipedia.org/wiki/Hearing_range: + * The human range is commonly given as 20Hz up. + * + * From https://en.wikipedia.org/wiki/High-resolution_audio: + * A reasonable upper bound for sample rate seems to be 96kHz. + * + * Calculate R value for 20Hz on a 96kHz sample rate: + * R = 1 - (pi * 2 * 20 / 96000) + * R = 0.9986910031 + * + * Transform floating point to bit-shifting approximation: + * output = input - prev_input + R * prev_output + * output = input - prev_input + (prev_output - (prev_output >> S)) + * + * Approximate bit-shift value S from R: + * R = 1 - (1 >> S) + * R = 1 - (1 / 2^S) + * R = 1 - 2^-S + * 0.9986910031 = 1 - 2^-S + * S = 9.57732 ~= 10 + * + * Actual R from S: + * R = 1 - 2^-10 = 0.9990234375 + * + * Confirm this has effect outside human hearing on 96000kHz sample: + * 0.9990234375 = 1 - (pi * 2 * f / 96000) + * f = 14.9208Hz + * + * Confirm this has effect outside human hearing on PDM 16kHz sample: + * 0.9990234375 = 1 - (pi * 2 * f / 16000) + * f = 2.4868Hz + * + */ + const uint8_t dc_filter_shift = 10; const size_t bytes_per_sample = this->audio_stream_info_.samples_to_bytes(1); const uint32_t total_samples = this->audio_stream_info_.bytes_to_samples(data.size()); - - if (total_samples == 0) { - return; - } - - int64_t offset_accumulator = 0; for (uint32_t sample_index = 0; sample_index < total_samples; ++sample_index) { const uint32_t byte_index = sample_index * bytes_per_sample; - int32_t sample = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample); - offset_accumulator += sample; - sample -= this->dc_offset_; - audio::pack_q31_as_audio_sample(sample, &data[byte_index], bytes_per_sample); + int32_t input = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample); + int32_t output = input - this->dc_offset_prev_input_ + + (this->dc_offset_prev_output_ - (this->dc_offset_prev_output_ >> dc_filter_shift)); + this->dc_offset_prev_input_ = input; + this->dc_offset_prev_output_ = output; + audio::pack_q31_as_audio_sample(output, &data[byte_index], bytes_per_sample); } - - const int32_t new_offset = offset_accumulator / total_samples; - this->dc_offset_ = new_offset / DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR + - (DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR - 1) * this->dc_offset_ / - DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR; } size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait) { diff --git a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h index 5f66f2e962..acfd535188 100644 --- a/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h +++ b/esphome/components/i2s_audio/microphone/i2s_audio_microphone.h @@ -82,7 +82,8 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub bool correct_dc_offset_; bool locked_driver_{false}; - int32_t dc_offset_{0}; + int32_t dc_offset_prev_input_{0}; + int32_t dc_offset_prev_output_{0}; }; } // namespace i2s_audio From d29cae9c3b79a14d23b4fae1a82d2fdb93c79285 Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Tue, 5 Aug 2025 13:21:00 +1200 Subject: [PATCH 5/5] Bump version to 2025.7.5 --- Doxyfile | 2 +- esphome/const.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Doxyfile b/Doxyfile index afd3b582e5..63f48f1238 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.7.4 +PROJECT_NUMBER = 2025.7.5 # 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/const.py b/esphome/const.py index 6ed0119411..ca6504d0c9 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -4,7 +4,7 @@ from enum import Enum from esphome.enum import StrEnum -__version__ = "2025.7.4" +__version__ = "2025.7.5" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = (