diff --git a/esphome/components/speaker/media_player/__init__.py b/esphome/components/speaker/media_player/__init__.py index b302bd9b23..42ca762858 100644 --- a/esphome/components/speaker/media_player/__init__.py +++ b/esphome/components/speaker/media_player/__init__.py @@ -15,6 +15,8 @@ from esphome.const import ( CONF_FORMAT, CONF_ID, CONF_NUM_CHANNELS, + CONF_ON_TURN_OFF, + CONF_ON_TURN_ON, CONF_PATH, CONF_RAW_DATA_ID, CONF_SAMPLE_RATE, @@ -401,6 +403,9 @@ FINAL_VALIDATE_SCHEMA = cv.All( async def to_code(config): + if CONF_ON_TURN_OFF in config or CONF_ON_TURN_ON in config: + cg.add_define("USE_SPEAKER_MEDIA_PLAYER_ON_OFF", True) + var = await media_player.new_media_player(config) await cg.register_component(var, config) diff --git a/esphome/components/speaker/media_player/speaker_media_player.cpp b/esphome/components/speaker/media_player/speaker_media_player.cpp index fdf6bf66cd..3f5cb2fda6 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.cpp +++ b/esphome/components/speaker/media_player/speaker_media_player.cpp @@ -51,7 +51,11 @@ static const UBaseType_t ANNOUNCEMENT_PIPELINE_TASK_PRIORITY = 1; static const char *const TAG = "speaker_media_player"; void SpeakerMediaPlayer::setup() { +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + state = media_player::MEDIA_PLAYER_STATE_OFF; +#else state = media_player::MEDIA_PLAYER_STATE_IDLE; +#endif this->media_control_command_queue_ = xQueueCreate(MEDIA_CONTROLS_QUEUE_LENGTH, sizeof(MediaCallCommand)); @@ -128,6 +132,12 @@ void SpeakerMediaPlayer::watch_media_commands_() { bool enqueue = media_command.enqueue.has_value() && media_command.enqueue.value(); if (media_command.url.has_value() || media_command.file.has_value()) { +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + if (this->state == media_player::MEDIA_PLAYER_STATE_OFF) { + this->state = media_player::MEDIA_PLAYER_STATE_ON; + publish_state(); + } +#endif PlaylistItem playlist_item; if (media_command.url.has_value()) { playlist_item.url = *media_command.url.value(); @@ -184,6 +194,12 @@ void SpeakerMediaPlayer::watch_media_commands_() { if (media_command.command.has_value()) { switch (media_command.command.value()) { case media_player::MEDIA_PLAYER_COMMAND_PLAY: +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + if (this->state == media_player::MEDIA_PLAYER_STATE_OFF) { + this->state = media_player::MEDIA_PLAYER_STATE_ON; + publish_state(); + } +#endif if ((this->media_pipeline_ != nullptr) && (this->is_paused_)) { this->media_pipeline_->set_pause_state(false); } @@ -195,10 +211,26 @@ void SpeakerMediaPlayer::watch_media_commands_() { } this->is_paused_ = true; break; +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + case media_player::MEDIA_PLAYER_COMMAND_TURN_ON: + if (this->state == media_player::MEDIA_PLAYER_STATE_OFF) { + this->state = media_player::MEDIA_PLAYER_STATE_ON; + this->publish_state(); + } + break; + case media_player::MEDIA_PLAYER_COMMAND_TURN_OFF: + this->is_turn_off_ = true; + // Intentional Fall-through +#endif 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. +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value()) || + (this->is_turn_off_ && this->announcement_pipeline_state_ != AudioPipelineState::STOPPED)) { +#else if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) { +#endif if (this->announcement_pipeline_ != nullptr) { this->cancel_timeout("next_ann"); this->announcement_playlist_.clear(); @@ -366,7 +398,13 @@ void SpeakerMediaPlayer::loop() { } } else { if (this->is_paused_) { +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + if (this->state != media_player::MEDIA_PLAYER_STATE_OFF) { + this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; + } +#else this->state = media_player::MEDIA_PLAYER_STATE_PAUSED; +#endif } else if (this->media_pipeline_state_ == AudioPipelineState::PLAYING) { this->state = media_player::MEDIA_PLAYER_STATE_PLAYING; } else if (this->media_pipeline_state_ == AudioPipelineState::STOPPED) { @@ -399,7 +437,13 @@ void SpeakerMediaPlayer::loop() { } } } else { +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + if (this->state != media_player::MEDIA_PLAYER_STATE_OFF) { + this->state = media_player::MEDIA_PLAYER_STATE_IDLE; + } +#else this->state = media_player::MEDIA_PLAYER_STATE_IDLE; +#endif } } } @@ -409,6 +453,20 @@ void SpeakerMediaPlayer::loop() { this->publish_state(); ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state)); } +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + if (this->is_turn_off_ && (this->state == media_player::MEDIA_PLAYER_STATE_PAUSED || + this->state == media_player::MEDIA_PLAYER_STATE_IDLE)) { + this->is_turn_off_ = false; + if (this->state == media_player::MEDIA_PLAYER_STATE_PAUSED) { + this->state = media_player::MEDIA_PLAYER_STATE_IDLE; + this->publish_state(); + ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state)); + } + this->state = media_player::MEDIA_PLAYER_STATE_OFF; + this->publish_state(); + ESP_LOGD(TAG, "State changed to %s", media_player::media_player_state_to_string(this->state)); + } +#endif } void SpeakerMediaPlayer::play_file(audio::AudioFile *media_file, bool announcement, bool enqueue) { @@ -481,6 +539,9 @@ media_player::MediaPlayerTraits SpeakerMediaPlayer::get_traits() { if (!this->single_pipeline_()) { traits.set_supports_pause(true); } +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + traits.set_supports_turn_off_on(true); +#endif if (this->announcement_format_.has_value()) { traits.get_supported_formats().push_back(this->announcement_format_.value()); diff --git a/esphome/components/speaker/media_player/speaker_media_player.h b/esphome/components/speaker/media_player/speaker_media_player.h index 6796fc9c00..3fa6f47b84 100644 --- a/esphome/components/speaker/media_player/speaker_media_player.h +++ b/esphome/components/speaker/media_player/speaker_media_player.h @@ -144,6 +144,9 @@ class SpeakerMediaPlayer : public Component, bool is_paused_{false}; bool is_muted_{false}; +#ifdef USE_SPEAKER_MEDIA_PLAYER_ON_OFF + bool is_turn_off_{false}; +#endif uint8_t unpause_media_remaining_{0}; uint8_t unpause_announcement_remaining_{0}; diff --git a/esphome/core/defines.h b/esphome/core/defines.h index 7fbc5a0b53..8d778edf2a 100644 --- a/esphome/core/defines.h +++ b/esphome/core/defines.h @@ -233,6 +233,7 @@ #define USE_LWIP_FAST_SELECT #define USE_WAKE_LOOP_THREADSAFE #define USE_SPEAKER +#define USE_SPEAKER_MEDIA_PLAYER_ON_OFF #define USE_SPI #define USE_VOICE_ASSISTANT #define USE_WEBSERVER diff --git a/tests/components/speaker/common-media_player_off_on.yaml b/tests/components/speaker/common-media_player_off_on.yaml new file mode 100644 index 0000000000..a5bea62c84 --- /dev/null +++ b/tests/components/speaker/common-media_player_off_on.yaml @@ -0,0 +1,18 @@ +<<: !include common.yaml + +media_player: + - platform: speaker + id: speaker_media_player_id + announcement_pipeline: + speaker: speaker_id + buffer_size: 1000000 + volume_increment: 0.02 + volume_max: 0.95 + volume_min: 0.0 + task_stack_in_psram: true + on_turn_on: + then: + - logger.log: "Turn On Media Player" + on_turn_off: + then: + - logger.log: "Turn Off Media Player" diff --git a/tests/components/speaker/media_player_off_on.esp32-idf.yaml b/tests/components/speaker/media_player_off_on.esp32-idf.yaml new file mode 100644 index 0000000000..2d5eefff19 --- /dev/null +++ b/tests/components/speaker/media_player_off_on.esp32-idf.yaml @@ -0,0 +1,9 @@ +substitutions: + scl_pin: GPIO16 + sda_pin: GPIO17 + i2s_bclk_pin: GPIO27 + i2s_lrclk_pin: GPIO26 + i2s_mclk_pin: GPIO25 + i2s_dout_pin: GPIO23 + +<<: !include common-media_player_off_on.yaml