diff --git a/esphome/components/display/display.cpp b/esphome/components/display/display.cpp index b12a81e050..a13464ce1b 100644 --- a/esphome/components/display/display.cpp +++ b/esphome/components/display/display.cpp @@ -555,10 +555,10 @@ void Display::get_text_bounds(int x, int y, const char *text, BaseFont *font, Te switch (x_align) { case TextAlign::RIGHT: - *x1 = x - *width; + *x1 = x - *width - x_offset; break; case TextAlign::CENTER_HORIZONTAL: - *x1 = x - (*width) / 2; + *x1 = x - (*width + x_offset) / 2; break; case TextAlign::LEFT: default: diff --git a/esphome/components/lvgl/switch/__init__.py b/esphome/components/lvgl/switch/__init__.py index 4e1e7f72e0..6d10a70d85 100644 --- a/esphome/components/lvgl/switch/__init__.py +++ b/esphome/components/lvgl/switch/__init__.py @@ -1,7 +1,9 @@ import esphome.codegen as cg -from esphome.components.switch import Switch, new_switch, switch_schema +from esphome.components.switch import Switch, register_switch, switch_schema import esphome.config_validation as cv +from esphome.const import CONF_ID from esphome.cpp_generator import MockObj +from esphome.cpp_types import Component from ..defines import CONF_WIDGET, literal from ..lvcode import ( @@ -18,7 +20,7 @@ from ..lvcode import ( from ..types import LV_EVENT, LV_STATE, lv_pseudo_button_t, lvgl_ns from ..widgets import get_widgets, wait_for_widgets -LVGLSwitch = lvgl_ns.class_("LVGLSwitch", Switch) +LVGLSwitch = lvgl_ns.class_("LVGLSwitch", Switch, Component) CONFIG_SCHEMA = switch_schema(LVGLSwitch).extend( { cv.Required(CONF_WIDGET): cv.use_id(lv_pseudo_button_t), @@ -27,21 +29,24 @@ CONFIG_SCHEMA = switch_schema(LVGLSwitch).extend( async def to_code(config): - switch = await new_switch(config) widget = await get_widgets(config, CONF_WIDGET) widget = widget[0] await wait_for_widgets() - async with LambdaContext(EVENT_ARG) as checked_ctx: - checked_ctx.add(switch.publish_state(widget.get_value())) + switch_id = MockObj(config[CONF_ID], "->") + v = literal("v") async with LambdaContext([(cg.bool_, "v")]) as control: - with LvConditional(MockObj("v")) as cond: + with LvConditional(v) as cond: widget.add_state(LV_STATE.CHECKED) cond.else_() widget.clear_state(LV_STATE.CHECKED) lv.event_send(widget.obj, API_EVENT, cg.nullptr) - control.add(switch.publish_state(literal("v"))) + control.add(switch_id.publish_state(v)) + switch = cg.new_Pvariable(config[CONF_ID], await control.get_lambda()) + await cg.register_component(switch, config) + await register_switch(switch, config) + async with LambdaContext(EVENT_ARG) as checked_ctx: + checked_ctx.add(switch.publish_state(widget.get_value())) async with LvContext() as ctx: - lv_add(switch.set_control_lambda(await control.get_lambda())) ctx.add( lvgl_static.add_event_cb( widget.obj, diff --git a/esphome/components/lvgl/switch/lvgl_switch.h b/esphome/components/lvgl/switch/lvgl_switch.h index af839b8892..485459691c 100644 --- a/esphome/components/lvgl/switch/lvgl_switch.h +++ b/esphome/components/lvgl/switch/lvgl_switch.h @@ -10,26 +10,15 @@ namespace esphome { namespace lvgl { -class LVGLSwitch : public switch_::Switch { +class LVGLSwitch : public switch_::Switch, public Component { public: - void set_control_lambda(std::function state_lambda) { - this->state_lambda_ = std::move(state_lambda); - if (this->initial_state_.has_value()) { - this->state_lambda_(this->initial_state_.value()); - this->initial_state_.reset(); - } - } + LVGLSwitch(std::function state_lambda) : state_lambda_(std::move(state_lambda)) {} + + void setup() override { this->write_state(this->get_initial_state_with_restore_mode().value_or(false)); } protected: - void write_state(bool value) override { - if (this->state_lambda_ != nullptr) { - this->state_lambda_(value); - } else { - this->initial_state_ = value; - } - } + void write_state(bool value) override { this->state_lambda_(value); } std::function state_lambda_{}; - optional initial_state_{}; }; } // namespace lvgl diff --git a/esphome/components/mdns/__init__.py b/esphome/components/mdns/__init__.py index 9a198280dd..e8902d5222 100644 --- a/esphome/components/mdns/__init__.py +++ b/esphome/components/mdns/__init__.py @@ -91,7 +91,7 @@ async def to_code(config): add_idf_component( name="mdns", repo="https://github.com/espressif/esp-protocols.git", - ref="mdns-v1.8.0", + ref="mdns-v1.8.2", path="components/mdns", ) diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index 80ddfb2f04..e143920010 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -100,7 +100,7 @@ void SpeakerMediaPlayer::setup() { if (!this->single_pipeline_()) { this->media_pipeline_ = make_unique(this->media_speaker_, this->buffer_size_, - this->task_stack_in_psram_, "ann", MEDIA_PIPELINE_TASK_PRIORITY); + this->task_stack_in_psram_, "med", MEDIA_PIPELINE_TASK_PRIORITY); if (this->media_pipeline_ == nullptr) { ESP_LOGE(TAG, "Failed to create media pipeline"); @@ -170,12 +170,28 @@ void SpeakerMediaPlayer::watch_media_commands_() { // Ensure the loaded next item doesn't start playing, clear the queue, start the file, and unpause this->cancel_timeout("next_media"); this->media_playlist_.clear(); - if (media_command.file.has_value()) { - this->media_pipeline_->start_file(playlist_item.file.value()); - } else if (media_command.url.has_value()) { - this->media_pipeline_->start_url(playlist_item.url.value()); + if (this->is_paused_) { + // If paused, stop the media pipeline and unpause it after confirming its stopped. This avoids playing a + // short segment of the paused file before starting the new one. + this->media_pipeline_->stop(); + this->set_retry("unpause_med", 50, 3, [this](const uint8_t remaining_attempts) { + if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) { + this->media_pipeline_->set_pause_state(false); + this->is_paused_ = false; + return RetryResult::DONE; + } + return RetryResult::RETRY; + }); + } else { + // Not paused, just directly start the file + if (media_command.file.has_value()) { + this->media_pipeline_->start_file(playlist_item.file.value()); + } else if (media_command.url.has_value()) { + this->media_pipeline_->start_url(playlist_item.url.value()); + } + this->media_pipeline_->set_pause_state(false); + this->is_paused_ = false; } - this->media_pipeline_->set_pause_state(false); } this->media_playlist_.push_back(playlist_item); } @@ -203,19 +219,37 @@ void SpeakerMediaPlayer::watch_media_commands_() { this->is_paused_ = true; break; case media_player::MEDIA_PLAYER_COMMAND_STOP: + // Pipelines do not stop immediately after calling the stop command, so confirm its stopped before unpausing. + // This avoids an audible short segment playing after receiving the stop command in a paused state. if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) { if (this->announcement_pipeline_ != nullptr) { this->cancel_timeout("next_ann"); this->announcement_playlist_.clear(); this->announcement_pipeline_->stop(); + this->set_retry("unpause_ann", 50, 3, [this](const uint8_t remaining_attempts) { + if (this->announcement_pipeline_state_ == AudioPipelineState::STOPPED) { + this->announcement_pipeline_->set_pause_state(false); + return RetryResult::DONE; + } + return RetryResult::RETRY; + }); } } else { if (this->media_pipeline_ != nullptr) { this->cancel_timeout("next_media"); this->media_playlist_.clear(); this->media_pipeline_->stop(); + this->set_retry("unpause_med", 50, 3, [this](const uint8_t remaining_attempts) { + if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) { + this->media_pipeline_->set_pause_state(false); + this->is_paused_ = false; + return RetryResult::DONE; + } + return RetryResult::RETRY; + }); } } + break; case media_player::MEDIA_PLAYER_COMMAND_TOGGLE: if (this->media_pipeline_ != nullptr) { @@ -332,11 +366,11 @@ void SpeakerMediaPlayer::loop() { } if (timeout_ms > 0) { - // Pause pipeline internally to facilitiate delay between items + // Pause pipeline internally to facilitate the delay between items this->announcement_pipeline_->set_pause_state(true); - // Internally unpause the pipeline after the delay between playlist items - this->set_timeout("next_ann", timeout_ms, - [this]() { this->announcement_pipeline_->set_pause_state(this->is_paused_); }); + // Internally unpause the pipeline after the delay between playlist items. Announcements do not follow the + // media player's pause state. + this->set_timeout("next_ann", timeout_ms, [this]() { this->announcement_pipeline_->set_pause_state(false); }); } } } else { @@ -372,9 +406,10 @@ void SpeakerMediaPlayer::loop() { } if (timeout_ms > 0) { - // Pause pipeline internally to facilitiate delay between items + // Pause pipeline internally to facilitate the delay between items this->media_pipeline_->set_pause_state(true); - // Internally unpause the pipeline after the delay between playlist items + // Internally unpause the pipeline after the delay between playlist items, if the media player state is + // not paused. this->set_timeout("next_media", timeout_ms, [this]() { this->media_pipeline_->set_pause_state(this->is_paused_); }); } diff --git a/esphome/const.py b/esphome/const.py index 2851578029..dab9327a3a 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2025.3.2" +__version__ = "2025.3.3" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( diff --git a/esphome/idf_component.yml b/esphome/idf_component.yml index 32ba414ba3..c273cae07e 100644 --- a/esphome/idf_component.yml +++ b/esphome/idf_component.yml @@ -7,7 +7,7 @@ dependencies: version: v2.0.9 mdns: git: https://github.com/espressif/esp-protocols.git - version: mdns-v1.8.0 + version: mdns-v1.8.2 path: components/mdns rules: - if: "idf_version >=5.0"