From fdebf041967549866f438431284c0690bec3d6da Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 23 Oct 2024 13:25:31 -0400 Subject: [PATCH 1/9] [voice_assistant] Bugfix: Fix crash on start (#7662) --- .../voice_assistant/voice_assistant.cpp | 54 +++++++++++-------- .../voice_assistant/voice_assistant.h | 6 +-- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 0b53e74ba3..6f164f69d3 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -433,16 +433,18 @@ void VoiceAssistant::loop() { #ifdef USE_SPEAKER void VoiceAssistant::write_speaker_() { - if (this->speaker_buffer_size_ > 0) { - size_t write_chunk = std::min(this->speaker_buffer_size_, 4 * 1024); - size_t written = this->speaker_->play(this->speaker_buffer_, write_chunk); - if (written > 0) { - memmove(this->speaker_buffer_, this->speaker_buffer_ + written, this->speaker_buffer_size_ - written); - this->speaker_buffer_size_ -= written; - this->speaker_buffer_index_ -= written; - this->set_timeout("speaker-timeout", 5000, [this]() { this->speaker_->stop(); }); - } else { - ESP_LOGV(TAG, "Speaker buffer full, trying again next loop"); + if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) { + if (this->speaker_buffer_size_ > 0) { + size_t write_chunk = std::min(this->speaker_buffer_size_, 4 * 1024); + size_t written = this->speaker_->play(this->speaker_buffer_, write_chunk); + if (written > 0) { + memmove(this->speaker_buffer_, this->speaker_buffer_ + written, this->speaker_buffer_size_ - written); + this->speaker_buffer_size_ -= written; + this->speaker_buffer_index_ -= written; + this->set_timeout("speaker-timeout", 5000, [this]() { this->speaker_->stop(); }); + } else { + ESP_LOGV(TAG, "Speaker buffer full, trying again next loop"); + } } } } @@ -772,16 +774,20 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } case api::enums::VOICE_ASSISTANT_TTS_STREAM_START: { #ifdef USE_SPEAKER - this->wait_for_stream_end_ = true; - ESP_LOGD(TAG, "TTS stream start"); - this->defer([this] { this->tts_stream_start_trigger_->trigger(); }); + if (this->speaker_ != nullptr) { + this->wait_for_stream_end_ = true; + ESP_LOGD(TAG, "TTS stream start"); + this->defer([this] { this->tts_stream_start_trigger_->trigger(); }); + } #endif break; } case api::enums::VOICE_ASSISTANT_TTS_STREAM_END: { #ifdef USE_SPEAKER - this->stream_ended_ = true; - ESP_LOGD(TAG, "TTS stream end"); + if (this->speaker_ != nullptr) { + this->stream_ended_ = true; + ESP_LOGD(TAG, "TTS stream end"); + } #endif break; } @@ -802,14 +808,16 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { void VoiceAssistant::on_audio(const api::VoiceAssistantAudio &msg) { #ifdef USE_SPEAKER // We should never get to this function if there is no speaker anyway - if (this->speaker_buffer_index_ + msg.data.length() < SPEAKER_BUFFER_SIZE) { - memcpy(this->speaker_buffer_ + this->speaker_buffer_index_, msg.data.data(), msg.data.length()); - this->speaker_buffer_index_ += msg.data.length(); - this->speaker_buffer_size_ += msg.data.length(); - this->speaker_bytes_received_ += msg.data.length(); - ESP_LOGV(TAG, "Received audio: %u bytes from API", msg.data.length()); - } else { - ESP_LOGE(TAG, "Cannot receive audio, buffer is full"); + if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) { + if (this->speaker_buffer_index_ + msg.data.length() < SPEAKER_BUFFER_SIZE) { + memcpy(this->speaker_buffer_ + this->speaker_buffer_index_, msg.data.data(), msg.data.length()); + this->speaker_buffer_index_ += msg.data.length(); + this->speaker_buffer_size_ += msg.data.length(); + this->speaker_bytes_received_ += msg.data.length(); + ESP_LOGV(TAG, "Received audio: %u bytes from API", msg.data.length()); + } else { + ESP_LOGE(TAG, "Cannot receive audio, buffer is full"); + } } #endif } diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 870e2acdaa..0016d3157c 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -250,7 +250,7 @@ class VoiceAssistant : public Component { #ifdef USE_SPEAKER void write_speaker_(); speaker::Speaker *speaker_{nullptr}; - uint8_t *speaker_buffer_; + uint8_t *speaker_buffer_{nullptr}; size_t speaker_buffer_index_{0}; size_t speaker_buffer_size_{0}; size_t speaker_bytes_received_{0}; @@ -282,8 +282,8 @@ class VoiceAssistant : public Component { float volume_multiplier_; uint32_t conversation_timeout_; - uint8_t *send_buffer_; - int16_t *input_buffer_; + uint8_t *send_buffer_{nullptr}; + int16_t *input_buffer_{nullptr}; bool continuous_{false}; bool silence_detection_; From 833565feb985e91c9512774a363ea2e5ca79d267 Mon Sep 17 00:00:00 2001 From: Kyle Cascade Date: Mon, 21 Oct 2024 21:11:23 -0700 Subject: [PATCH 2/9] Humanized the missing MQTT log topic error message (#7634) --- esphome/mqtt.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/esphome/mqtt.py b/esphome/mqtt.py index c1c45799cc..d55fb0202d 100644 --- a/esphome/mqtt.py +++ b/esphome/mqtt.py @@ -209,6 +209,12 @@ def show_logs(config, topic=None, username=None, password=None, client_id=None): elif CONF_MQTT in config: conf = config[CONF_MQTT] if CONF_LOG_TOPIC in conf: + if config[CONF_MQTT][CONF_LOG_TOPIC] is None: + _LOGGER.error("MQTT log topic set to null, can't start MQTT logs") + return 1 + if CONF_TOPIC not in config[CONF_MQTT][CONF_LOG_TOPIC]: + _LOGGER.error("MQTT log topic not available, can't start MQTT logs") + return 1 topic = config[CONF_MQTT][CONF_LOG_TOPIC][CONF_TOPIC] elif CONF_TOPIC_PREFIX in config[CONF_MQTT]: topic = f"{config[CONF_MQTT][CONF_TOPIC_PREFIX]}/debug" From 8d90d256bf2518c46676a0a7dfe37ef3dd1868c8 Mon Sep 17 00:00:00 2001 From: Clyde Stubbs <2366188+clydebarrow@users.noreply.github.com> Date: Wed, 23 Oct 2024 10:16:55 +1100 Subject: [PATCH 3/9] [lvgl] Some properties were not templatable (Bugfix) (#7655) --- esphome/components/lvgl/lv_validation.py | 4 ++++ esphome/components/lvgl/schemas.py | 14 +++++++------- tests/components/lvgl/lvgl-package.yaml | 7 +++++++ 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/esphome/components/lvgl/lv_validation.py b/esphome/components/lvgl/lv_validation.py index fd840cc417..a58fbc33e0 100644 --- a/esphome/components/lvgl/lv_validation.py +++ b/esphome/components/lvgl/lv_validation.py @@ -267,6 +267,9 @@ def angle(value): return int(cv.float_range(0.0, 360.0)(cv.angle(value)) * 10) +lv_angle = LValidator(angle, uint32) + + @schema_extractor("one_of") def size_validator(value): """A size in one axis - one of "size_content", a number (pixels) or a percentage""" @@ -403,6 +406,7 @@ class TextValidator(LValidator): lv_text = TextValidator() lv_float = LValidator(cv.float_, cg.float_) lv_int = LValidator(cv.int_, cg.int_) +lv_positive_int = LValidator(cv.positive_int, cg.int_) lv_brightness = LValidator(cv.percentage, cg.float_, retmapper=lambda x: int(x * 255)) diff --git a/esphome/components/lvgl/schemas.py b/esphome/components/lvgl/schemas.py index 780057623a..0d5a609a2d 100644 --- a/esphome/components/lvgl/schemas.py +++ b/esphome/components/lvgl/schemas.py @@ -91,7 +91,7 @@ STYLE_PROPS = { "arc_opa": lvalid.opacity, "arc_color": lvalid.lv_color, "arc_rounded": lvalid.lv_bool, - "arc_width": cv.positive_int, + "arc_width": lvalid.lv_positive_int, "anim_time": lvalid.lv_milliseconds, "bg_color": lvalid.lv_color, "bg_grad": lv_gradient, @@ -111,7 +111,7 @@ STYLE_PROPS = { "border_side": df.LvConstant( "LV_BORDER_SIDE_", "NONE", "TOP", "BOTTOM", "LEFT", "RIGHT", "INTERNAL" ).several_of, - "border_width": cv.positive_int, + "border_width": lvalid.lv_positive_int, "clip_corner": lvalid.lv_bool, "color_filter_opa": lvalid.opacity, "height": lvalid.size, @@ -134,11 +134,11 @@ STYLE_PROPS = { "pad_right": lvalid.pixels, "pad_top": lvalid.pixels, "shadow_color": lvalid.lv_color, - "shadow_ofs_x": cv.int_, - "shadow_ofs_y": cv.int_, + "shadow_ofs_x": lvalid.lv_int, + "shadow_ofs_y": lvalid.lv_int, "shadow_opa": lvalid.opacity, - "shadow_spread": cv.int_, - "shadow_width": cv.positive_int, + "shadow_spread": lvalid.lv_int, + "shadow_width": lvalid.lv_positive_int, "text_align": df.LvConstant( "LV_TEXT_ALIGN_", "LEFT", "CENTER", "RIGHT", "AUTO" ).one_of, @@ -150,7 +150,7 @@ STYLE_PROPS = { "text_letter_space": cv.positive_int, "text_line_space": cv.positive_int, "text_opa": lvalid.opacity, - "transform_angle": lvalid.angle, + "transform_angle": lvalid.lv_angle, "transform_height": lvalid.pixels_or_percent, "transform_pivot_x": lvalid.pixels_or_percent, "transform_pivot_y": lvalid.pixels_or_percent, diff --git a/tests/components/lvgl/lvgl-package.yaml b/tests/components/lvgl/lvgl-package.yaml index 1770c1bfbc..2e05cf10d3 100644 --- a/tests/components/lvgl/lvgl-package.yaml +++ b/tests/components/lvgl/lvgl-package.yaml @@ -299,6 +299,13 @@ lvgl: id: button_button width: 20% height: 10% + transform_angle: !lambda return 180*100; + arc_width: !lambda return 4; + border_width: !lambda return 6; + shadow_ofs_x: !lambda return 6; + shadow_ofs_y: !lambda return 6; + shadow_spread: !lambda return 6; + shadow_width: !lambda return 6; pressed: bg_color: light_blue checkable: true From 156ad773c91edca8b14a8518363287e027ef8147 Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 23 Oct 2024 13:25:31 -0400 Subject: [PATCH 4/9] [voice_assistant] Bugfix: Fix crash on start (#7662) --- .../voice_assistant/voice_assistant.cpp | 54 +++++++++++-------- .../voice_assistant/voice_assistant.h | 6 +-- 2 files changed, 34 insertions(+), 26 deletions(-) diff --git a/esphome/components/voice_assistant/voice_assistant.cpp b/esphome/components/voice_assistant/voice_assistant.cpp index 0b53e74ba3..6f164f69d3 100644 --- a/esphome/components/voice_assistant/voice_assistant.cpp +++ b/esphome/components/voice_assistant/voice_assistant.cpp @@ -433,16 +433,18 @@ void VoiceAssistant::loop() { #ifdef USE_SPEAKER void VoiceAssistant::write_speaker_() { - if (this->speaker_buffer_size_ > 0) { - size_t write_chunk = std::min(this->speaker_buffer_size_, 4 * 1024); - size_t written = this->speaker_->play(this->speaker_buffer_, write_chunk); - if (written > 0) { - memmove(this->speaker_buffer_, this->speaker_buffer_ + written, this->speaker_buffer_size_ - written); - this->speaker_buffer_size_ -= written; - this->speaker_buffer_index_ -= written; - this->set_timeout("speaker-timeout", 5000, [this]() { this->speaker_->stop(); }); - } else { - ESP_LOGV(TAG, "Speaker buffer full, trying again next loop"); + if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) { + if (this->speaker_buffer_size_ > 0) { + size_t write_chunk = std::min(this->speaker_buffer_size_, 4 * 1024); + size_t written = this->speaker_->play(this->speaker_buffer_, write_chunk); + if (written > 0) { + memmove(this->speaker_buffer_, this->speaker_buffer_ + written, this->speaker_buffer_size_ - written); + this->speaker_buffer_size_ -= written; + this->speaker_buffer_index_ -= written; + this->set_timeout("speaker-timeout", 5000, [this]() { this->speaker_->stop(); }); + } else { + ESP_LOGV(TAG, "Speaker buffer full, trying again next loop"); + } } } } @@ -772,16 +774,20 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { } case api::enums::VOICE_ASSISTANT_TTS_STREAM_START: { #ifdef USE_SPEAKER - this->wait_for_stream_end_ = true; - ESP_LOGD(TAG, "TTS stream start"); - this->defer([this] { this->tts_stream_start_trigger_->trigger(); }); + if (this->speaker_ != nullptr) { + this->wait_for_stream_end_ = true; + ESP_LOGD(TAG, "TTS stream start"); + this->defer([this] { this->tts_stream_start_trigger_->trigger(); }); + } #endif break; } case api::enums::VOICE_ASSISTANT_TTS_STREAM_END: { #ifdef USE_SPEAKER - this->stream_ended_ = true; - ESP_LOGD(TAG, "TTS stream end"); + if (this->speaker_ != nullptr) { + this->stream_ended_ = true; + ESP_LOGD(TAG, "TTS stream end"); + } #endif break; } @@ -802,14 +808,16 @@ void VoiceAssistant::on_event(const api::VoiceAssistantEventResponse &msg) { void VoiceAssistant::on_audio(const api::VoiceAssistantAudio &msg) { #ifdef USE_SPEAKER // We should never get to this function if there is no speaker anyway - if (this->speaker_buffer_index_ + msg.data.length() < SPEAKER_BUFFER_SIZE) { - memcpy(this->speaker_buffer_ + this->speaker_buffer_index_, msg.data.data(), msg.data.length()); - this->speaker_buffer_index_ += msg.data.length(); - this->speaker_buffer_size_ += msg.data.length(); - this->speaker_bytes_received_ += msg.data.length(); - ESP_LOGV(TAG, "Received audio: %u bytes from API", msg.data.length()); - } else { - ESP_LOGE(TAG, "Cannot receive audio, buffer is full"); + if ((this->speaker_ != nullptr) && (this->speaker_buffer_ != nullptr)) { + if (this->speaker_buffer_index_ + msg.data.length() < SPEAKER_BUFFER_SIZE) { + memcpy(this->speaker_buffer_ + this->speaker_buffer_index_, msg.data.data(), msg.data.length()); + this->speaker_buffer_index_ += msg.data.length(); + this->speaker_buffer_size_ += msg.data.length(); + this->speaker_bytes_received_ += msg.data.length(); + ESP_LOGV(TAG, "Received audio: %u bytes from API", msg.data.length()); + } else { + ESP_LOGE(TAG, "Cannot receive audio, buffer is full"); + } } #endif } diff --git a/esphome/components/voice_assistant/voice_assistant.h b/esphome/components/voice_assistant/voice_assistant.h index 870e2acdaa..0016d3157c 100644 --- a/esphome/components/voice_assistant/voice_assistant.h +++ b/esphome/components/voice_assistant/voice_assistant.h @@ -250,7 +250,7 @@ class VoiceAssistant : public Component { #ifdef USE_SPEAKER void write_speaker_(); speaker::Speaker *speaker_{nullptr}; - uint8_t *speaker_buffer_; + uint8_t *speaker_buffer_{nullptr}; size_t speaker_buffer_index_{0}; size_t speaker_buffer_size_{0}; size_t speaker_bytes_received_{0}; @@ -282,8 +282,8 @@ class VoiceAssistant : public Component { float volume_multiplier_; uint32_t conversation_timeout_; - uint8_t *send_buffer_; - int16_t *input_buffer_; + uint8_t *send_buffer_{nullptr}; + int16_t *input_buffer_{nullptr}; bool continuous_{false}; bool silence_detection_; From 127acfde6482ebb7e44894af0c17660263166fce Mon Sep 17 00:00:00 2001 From: Jesse Hills <3060199+jesserockz@users.noreply.github.com> Date: Thu, 24 Oct 2024 07:15:40 +1300 Subject: [PATCH 5/9] Bump version to 2024.10.2 --- esphome/const.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/esphome/const.py b/esphome/const.py index 5fa457b25a..032a4c79a0 100644 --- a/esphome/const.py +++ b/esphome/const.py @@ -1,6 +1,6 @@ """Constants used by esphome.""" -__version__ = "2024.10.1" +__version__ = "2024.10.2" ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_" VALID_SUBSTITUTIONS_CHARACTERS = ( From 4289e00ad0ee3f466a60d9044c97157a8070a047 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:06:45 +1300 Subject: [PATCH 6/9] Bump actions/cache from 4.1.1 to 4.1.2 in /.github/actions/restore-python (#7659) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/actions/restore-python/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/restore-python/action.yml b/.github/actions/restore-python/action.yml index 1f5812691e..c6978f68c5 100644 --- a/.github/actions/restore-python/action.yml +++ b/.github/actions/restore-python/action.yml @@ -22,7 +22,7 @@ runs: python-version: ${{ inputs.python-version }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache/restore@v4.1.1 + uses: actions/cache/restore@v4.1.2 with: path: venv # yamllint disable-line rule:line-length From 2feffddc550c0241aede210bb273b8e0001084f2 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Thu, 24 Oct 2024 08:06:53 +1300 Subject: [PATCH 7/9] Bump actions/cache from 4.1.1 to 4.1.2 (#7660) Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .github/workflows/ci.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 178b914a1c..0d2f1c877d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -46,7 +46,7 @@ jobs: python-version: ${{ env.DEFAULT_PYTHON }} - name: Restore Python virtual environment id: cache-venv - uses: actions/cache@v4.1.1 + uses: actions/cache@v4.1.2 with: path: venv # yamllint disable-line rule:line-length @@ -302,14 +302,14 @@ jobs: - name: Cache platformio if: github.ref == 'refs/heads/dev' - uses: actions/cache@v4.1.1 + uses: actions/cache@v4.1.2 with: path: ~/.platformio key: platformio-${{ matrix.pio_cache_key }} - name: Cache platformio if: github.ref != 'refs/heads/dev' - uses: actions/cache/restore@v4.1.1 + uses: actions/cache/restore@v4.1.2 with: path: ~/.platformio key: platformio-${{ matrix.pio_cache_key }} From bff0e81ed3863ee960d1612fd64bec78fe34c03b Mon Sep 17 00:00:00 2001 From: Kevin Ahrendt Date: Wed, 23 Oct 2024 16:37:38 -0400 Subject: [PATCH 8/9] [speaker, i2s_audio] Support audio_dac component, mute actions, and improved logging (#7664) Co-authored-by: Jesse Hills <3060199+jesserockz@users.noreply.github.com> --- CODEOWNERS | 4 +- .../components/i2s_audio/speaker/__init__.py | 2 +- .../i2s_audio/speaker/i2s_audio_speaker.cpp | 72 +++++++++++++++---- .../i2s_audio/speaker/i2s_audio_speaker.h | 14 ++-- esphome/components/speaker/__init__.py | 31 ++++++-- esphome/components/speaker/automation.h | 20 ++++++ esphome/components/speaker/speaker.h | 42 ++++++++++- tests/components/speaker/test.esp32-ard.yaml | 2 + .../components/speaker/test.esp32-c3-ard.yaml | 2 + .../components/speaker/test.esp32-c3-idf.yaml | 2 + tests/components/speaker/test.esp32-idf.yaml | 15 +++- 11 files changed, 177 insertions(+), 29 deletions(-) diff --git a/CODEOWNERS b/CODEOWNERS index 616b18293d..f96e43d5b7 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -202,7 +202,7 @@ esphome/components/i2c_device/* @gabest11 esphome/components/i2s_audio/* @jesserockz esphome/components/i2s_audio/media_player/* @jesserockz esphome/components/i2s_audio/microphone/* @jesserockz -esphome/components/i2s_audio/speaker/* @jesserockz +esphome/components/i2s_audio/speaker/* @jesserockz @kahrendt esphome/components/iaqcore/* @yozik04 esphome/components/ili9xxx/* @clydebarrow @nielsnl68 esphome/components/improv_base/* @esphome/core @@ -377,7 +377,7 @@ esphome/components/smt100/* @piechade esphome/components/sn74hc165/* @jesserockz esphome/components/socket/* @esphome/core esphome/components/sonoff_d1/* @anatoly-savchenkov -esphome/components/speaker/* @jesserockz +esphome/components/speaker/* @jesserockz @kahrendt esphome/components/spi/* @clydebarrow @esphome/core esphome/components/spi_device/* @clydebarrow esphome/components/spi_led_strip/* @clydebarrow diff --git a/esphome/components/i2s_audio/speaker/__init__.py b/esphome/components/i2s_audio/speaker/__init__.py index 9fdaced64c..dd43d6cb39 100644 --- a/esphome/components/i2s_audio/speaker/__init__.py +++ b/esphome/components/i2s_audio/speaker/__init__.py @@ -17,7 +17,7 @@ from .. import ( ) AUTO_LOAD = ["audio"] -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@kahrendt"] DEPENDENCIES = ["i2s_audio"] I2SAudioSpeaker = i2s_audio_ns.class_( diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp index 4fc489d0a3..cf6c3bbbba 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.cpp @@ -32,6 +32,7 @@ enum SpeakerEventGroupBits : uint32_t { STATE_RUNNING = (1 << 11), STATE_STOPPING = (1 << 12), STATE_STOPPED = (1 << 13), + ERR_INVALID_FORMAT = (1 << 14), ERR_TASK_FAILED_TO_START = (1 << 15), ERR_ESP_INVALID_STATE = (1 << 16), ERR_ESP_INVALID_ARG = (1 << 17), @@ -104,16 +105,6 @@ void I2SAudioSpeaker::setup() { void I2SAudioSpeaker::loop() { uint32_t event_group_bits = xEventGroupGetBits(this->event_group_); - if (event_group_bits & SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START) { - this->status_set_error("Failed to start speaker task"); - } - - if (event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS) { - uint32_t error_bits = event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS; - ESP_LOGW(TAG, "Error writing to I2S: %s", esp_err_to_name(err_bit_to_esp_err(error_bits))); - this->status_set_warning(); - } - if (event_group_bits & SpeakerEventGroupBits::STATE_STARTING) { ESP_LOGD(TAG, "Starting Speaker"); this->state_ = speaker::STATE_STARTING; @@ -139,12 +130,64 @@ void I2SAudioSpeaker::loop() { this->speaker_task_handle_ = nullptr; } } + + if (event_group_bits & SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START) { + this->status_set_error("Failed to start speaker task"); + xEventGroupClearBits(this->event_group_, SpeakerEventGroupBits::ERR_TASK_FAILED_TO_START); + } + + if (event_group_bits & SpeakerEventGroupBits::ERR_INVALID_FORMAT) { + this->status_set_error("Failed to adjust I2S bus to match the incoming audio"); + ESP_LOGE(TAG, + "Incompatible audio format: sample rate = %" PRIu32 ", channels = %" PRIu8 ", bits per sample = %" PRIu8, + this->audio_stream_info_.sample_rate, this->audio_stream_info_.channels, + this->audio_stream_info_.bits_per_sample); + } + + if (event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS) { + uint32_t error_bits = event_group_bits & SpeakerEventGroupBits::ALL_ERR_ESP_BITS; + ESP_LOGW(TAG, "Error writing to I2S: %s", esp_err_to_name(err_bit_to_esp_err(error_bits))); + this->status_set_warning(); + } } void I2SAudioSpeaker::set_volume(float volume) { this->volume_ = volume; - ssize_t decibel_index = remap(volume, 0.0f, 1.0f, 0, Q15_VOLUME_SCALING_FACTORS.size() - 1); - this->q15_volume_factor_ = Q15_VOLUME_SCALING_FACTORS[decibel_index]; +#ifdef USE_AUDIO_DAC + if (this->audio_dac_ != nullptr) { + if (volume > 0.0) { + this->audio_dac_->set_mute_off(); + } + this->audio_dac_->set_volume(volume); + } else +#endif + { + // Fallback to software volume control by using a Q15 fixed point scaling factor + ssize_t decibel_index = remap(volume, 0.0f, 1.0f, 0, Q15_VOLUME_SCALING_FACTORS.size() - 1); + this->q15_volume_factor_ = Q15_VOLUME_SCALING_FACTORS[decibel_index]; + } +} + +void I2SAudioSpeaker::set_mute_state(bool mute_state) { + this->mute_state_ = mute_state; +#ifdef USE_AUDIO_DAC + if (this->audio_dac_) { + if (mute_state) { + this->audio_dac_->set_mute_on(); + } else { + this->audio_dac_->set_mute_off(); + } + } else +#endif + { + if (mute_state) { + // Fallback to software volume control and scale by 0 + this->q15_volume_factor_ = 0; + } else { + // Revert to previous volume when unmuting + this->set_volume(this->volume_); + } + } } size_t I2SAudioSpeaker::play(const uint8_t *data, size_t length, TickType_t ticks_to_wait) { @@ -275,6 +318,9 @@ void I2SAudioSpeaker::speaker_task(void *params) { i2s_zero_dma_buffer(this_speaker->parent_->get_port()); } } + } else { + // Couldn't configure the I2S port to be compatible with the incoming audio + xEventGroupSetBits(this_speaker->event_group_, SpeakerEventGroupBits::ERR_INVALID_FORMAT); } i2s_zero_dma_buffer(this_speaker->parent_->get_port()); @@ -288,7 +334,7 @@ void I2SAudioSpeaker::speaker_task(void *params) { } void I2SAudioSpeaker::start() { - if (this->is_failed()) + if (this->is_failed() || this->status_has_error()) return; if ((this->state_ == speaker::STATE_STARTING) || (this->state_ == speaker::STATE_RUNNING)) return; diff --git a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h index 245f97d1e7..3c512d4d4d 100644 --- a/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h +++ b/esphome/components/i2s_audio/speaker/i2s_audio_speaker.h @@ -49,11 +49,17 @@ class I2SAudioSpeaker : public I2SAudioOut, public speaker::Speaker, public Comp bool has_buffered_data() const override; - /// @brief Sets the volume of the speaker. It is implemented as a software volume control. - /// Overrides the default setter to convert the floating point volume to a Q15 fixed-point factor. - /// @param volume + /// @brief Sets the volume of the speaker. Uses the speaker's configured audio dac component. If unavailble, it is + /// implemented as a software volume control. Overrides the default setter to convert the floating point volume to a + /// Q15 fixed-point factor. + /// @param volume between 0.0 and 1.0 void set_volume(float volume) override; - float get_volume() override { return this->volume_; } + + /// @brief Mutes or unmute the speaker. Uses the speaker's configured audio dac component. If unavailble, it is + /// implemented as a software volume control. Overrides the default setter to convert the floating point volume to a + /// Q15 fixed-point factor. + /// @param mute_state true for muting, false for unmuting + void set_mute_state(bool mute_state) override; protected: /// @brief Function for the FreeRTOS task handling audio output. diff --git a/esphome/components/speaker/__init__.py b/esphome/components/speaker/__init__.py index 1bbc0b02ef..7a668dc2f3 100644 --- a/esphome/components/speaker/__init__.py +++ b/esphome/components/speaker/__init__.py @@ -1,15 +1,18 @@ from esphome import automation from esphome.automation import maybe_simple_id import esphome.codegen as cg +from esphome.components import audio_dac import esphome.config_validation as cv from esphome.const import CONF_DATA, CONF_ID, CONF_VOLUME from esphome.core import CORE from esphome.coroutine import coroutine_with_priority -CODEOWNERS = ["@jesserockz"] +CODEOWNERS = ["@jesserockz", "@kahrendt"] IS_PLATFORM_COMPONENT = True +CONF_AUDIO_DAC = "audio_dac" + speaker_ns = cg.esphome_ns.namespace("speaker") Speaker = speaker_ns.class_("Speaker") @@ -26,6 +29,12 @@ FinishAction = speaker_ns.class_( VolumeSetAction = speaker_ns.class_( "VolumeSetAction", automation.Action, cg.Parented.template(Speaker) ) +MuteOnAction = speaker_ns.class_( + "MuteOnAction", automation.Action, cg.Parented.template(Speaker) +) +MuteOffAction = speaker_ns.class_( + "MuteOffAction", automation.Action, cg.Parented.template(Speaker) +) IsPlayingCondition = speaker_ns.class_("IsPlayingCondition", automation.Condition) @@ -33,7 +42,9 @@ IsStoppedCondition = speaker_ns.class_("IsStoppedCondition", automation.Conditio async def setup_speaker_core_(var, config): - pass + if audio_dac_config := config.get(CONF_AUDIO_DAC): + aud_dac = await cg.get_variable(audio_dac_config) + cg.add(var.set_audio_dac(aud_dac)) async def register_speaker(var, config): @@ -42,8 +53,11 @@ async def register_speaker(var, config): await setup_speaker_core_(var, config) -SPEAKER_SCHEMA = cv.Schema({}) - +SPEAKER_SCHEMA = cv.Schema( + { + cv.Optional(CONF_AUDIO_DAC): cv.use_id(audio_dac.AudioDac), + } +) SPEAKER_AUTOMATION_SCHEMA = maybe_simple_id({cv.GenerateID(): cv.use_id(Speaker)}) @@ -113,6 +127,15 @@ async def speaker_volume_set_action(config, action_id, template_arg, args): return var +@automation.register_action( + "speaker.mute_off", MuteOffAction, SPEAKER_AUTOMATION_SCHEMA +) +@automation.register_action("speaker.mute_on", MuteOnAction, SPEAKER_AUTOMATION_SCHEMA) +async def speaker_mute_action_to_code(config, action_id, template_arg, args): + paren = await cg.get_variable(config[CONF_ID]) + return cg.new_Pvariable(action_id, template_arg, paren) + + @coroutine_with_priority(100.0) async def to_code(config): cg.add_global(speaker_ns.using) diff --git a/esphome/components/speaker/automation.h b/esphome/components/speaker/automation.h index 9efda011f2..c083796eea 100644 --- a/esphome/components/speaker/automation.h +++ b/esphome/components/speaker/automation.h @@ -39,6 +39,26 @@ template class VolumeSetAction : public Action, public Pa void play(Ts... x) override { this->parent_->set_volume(this->volume_.value(x...)); } }; +template class MuteOnAction : public Action { + public: + explicit MuteOnAction(Speaker *speaker) : speaker_(speaker) {} + + void play(Ts... x) override { this->speaker_->set_mute_state(true); } + + protected: + Speaker *speaker_; +}; + +template class MuteOffAction : public Action { + public: + explicit MuteOffAction(Speaker *speaker) : speaker_(speaker) {} + + void play(Ts... x) override { this->speaker_->set_mute_state(false); } + + protected: + Speaker *speaker_; +}; + template class StopAction : public Action, public Parented { public: void play(Ts... x) override { this->parent_->stop(); } diff --git a/esphome/components/speaker/speaker.h b/esphome/components/speaker/speaker.h index 9390e4edb7..96843e2d5a 100644 --- a/esphome/components/speaker/speaker.h +++ b/esphome/components/speaker/speaker.h @@ -8,7 +8,12 @@ #include #endif +#include "esphome/core/defines.h" + #include "esphome/components/audio/audio.h" +#ifdef USE_AUDIO_DAC +#include "esphome/components/audio_dac/audio_dac.h" +#endif namespace esphome { namespace speaker { @@ -56,9 +61,35 @@ class Speaker { bool is_running() const { return this->state_ == STATE_RUNNING; } bool is_stopped() const { return this->state_ == STATE_STOPPED; } - // Volume control must be implemented by each speaker component, otherwise it will have no effect. - virtual void set_volume(float volume) { this->volume_ = volume; }; - virtual float get_volume() { return this->volume_; } + // Volume control is handled by a configured audio dac component. Individual speaker components can + // override and implement in software if an audio dac isn't available. + virtual void set_volume(float volume) { + this->volume_ = volume; +#ifdef USE_AUDIO_DAC + if (this->audio_dac_ != nullptr) { + this->audio_dac_->set_volume(volume); + } +#endif + }; + float get_volume() { return this->volume_; } + + virtual void set_mute_state(bool mute_state) { + this->mute_state_ = mute_state; +#ifdef USE_AUDIO_DAC + if (this->audio_dac_) { + if (mute_state) { + this->audio_dac_->set_mute_on(); + } else { + this->audio_dac_->set_mute_off(); + } + } +#endif + } + bool get_mute_state() { return this->mute_state_; } + +#ifdef USE_AUDIO_DAC + void set_audio_dac(audio_dac::AudioDac *audio_dac) { this->audio_dac_ = audio_dac; } +#endif void set_audio_stream_info(const audio::AudioStreamInfo &audio_stream_info) { this->audio_stream_info_ = audio_stream_info; @@ -68,6 +99,11 @@ class Speaker { State state_{STATE_STOPPED}; audio::AudioStreamInfo audio_stream_info_; float volume_{1.0f}; + bool mute_state_{false}; + +#ifdef USE_AUDIO_DAC + audio_dac::AudioDac *audio_dac_{nullptr}; +#endif }; } // namespace speaker diff --git a/tests/components/speaker/test.esp32-ard.yaml b/tests/components/speaker/test.esp32-ard.yaml index 9a24d00f68..396b4d95ea 100644 --- a/tests/components/speaker/test.esp32-ard.yaml +++ b/tests/components/speaker/test.esp32-ard.yaml @@ -1,6 +1,8 @@ esphome: on_boot: then: + - speaker.mute_on: + - speaker.mute_off: - if: condition: speaker.is_stopped then: diff --git a/tests/components/speaker/test.esp32-c3-ard.yaml b/tests/components/speaker/test.esp32-c3-ard.yaml index f28014337c..636aeba766 100644 --- a/tests/components/speaker/test.esp32-c3-ard.yaml +++ b/tests/components/speaker/test.esp32-c3-ard.yaml @@ -1,6 +1,8 @@ esphome: on_boot: then: + - speaker.mute_on: + - speaker.mute_off: - if: condition: speaker.is_stopped then: diff --git a/tests/components/speaker/test.esp32-c3-idf.yaml b/tests/components/speaker/test.esp32-c3-idf.yaml index f28014337c..636aeba766 100644 --- a/tests/components/speaker/test.esp32-c3-idf.yaml +++ b/tests/components/speaker/test.esp32-c3-idf.yaml @@ -1,6 +1,8 @@ esphome: on_boot: then: + - speaker.mute_on: + - speaker.mute_off: - if: condition: speaker.is_stopped then: diff --git a/tests/components/speaker/test.esp32-idf.yaml b/tests/components/speaker/test.esp32-idf.yaml index 9a24d00f68..b69440b133 100644 --- a/tests/components/speaker/test.esp32-idf.yaml +++ b/tests/components/speaker/test.esp32-idf.yaml @@ -1,6 +1,8 @@ esphome: on_boot: then: + - speaker.mute_on: + - speaker.mute_off: - if: condition: speaker.is_stopped then: @@ -17,8 +19,17 @@ i2s_audio: i2s_bclk_pin: 17 i2s_mclk_pin: 15 +i2c: + scl: 12 + sda: 10 + +audio_dac: + - platform: aic3204 + id: internal_dac + speaker: - platform: i2s_audio - id: speaker_id + id: speaker_with_audio_dac_id + audio_dac: internal_dac dac_type: external - i2s_dout_pin: 13 + i2s_dout_pin: 14 From 9acc21e81a393a409236f29aeaf195cedb1ea472 Mon Sep 17 00:00:00 2001 From: tomaszduda23 Date: Wed, 23 Oct 2024 23:04:59 +0200 Subject: [PATCH 9/9] unified way how all platforms handle copy_files (#7614) Co-authored-by: Tomasz Duda --- esphome/components/rp2040/__init__.py | 9 ++++++--- esphome/writer.py | 25 +++++++------------------ 2 files changed, 13 insertions(+), 21 deletions(-) diff --git a/esphome/components/rp2040/__init__.py b/esphome/components/rp2040/__init__.py index f59962477f..d612631a4c 100644 --- a/esphome/components/rp2040/__init__.py +++ b/esphome/components/rp2040/__init__.py @@ -17,7 +17,7 @@ from esphome.const import ( PLATFORM_RP2040, ) from esphome.core import CORE, EsphomeError, coroutine_with_priority -from esphome.helpers import copy_file_if_changed, mkdir_p, write_file +from esphome.helpers import copy_file_if_changed, mkdir_p, write_file, read_file from .const import KEY_BOARD, KEY_PIO_FILES, KEY_RP2040, rp2040_ns @@ -230,11 +230,14 @@ def generate_pio_files() -> bool: # Called by writer.py -def copy_files() -> bool: +def copy_files(): dir = os.path.dirname(__file__) post_build_file = os.path.join(dir, "post_build.py.script") copy_file_if_changed( post_build_file, CORE.relative_build_path("post_build.py"), ) - return generate_pio_files() + if generate_pio_files(): + path = CORE.relative_src_path("esphome.h") + content = read_file(path).rstrip("\n") + write_file(path, content + '\n#include "pio_includes.h"\n') diff --git a/esphome/writer.py b/esphome/writer.py index 79ee72996c..90446ae4b1 100644 --- a/esphome/writer.py +++ b/esphome/writer.py @@ -1,3 +1,4 @@ +import importlib import logging import os from pathlib import Path @@ -299,25 +300,13 @@ def copy_src_tree(): CORE.relative_src_path("esphome", "core", "version.h"), generate_version_h() ) - if CORE.is_esp32: - from esphome.components.esp32 import copy_files - + platform = "esphome.components." + CORE.target_platform + try: + module = importlib.import_module(platform) + copy_files = getattr(module, "copy_files") copy_files() - - elif CORE.is_esp8266: - from esphome.components.esp8266 import copy_files - - copy_files() - - elif CORE.is_rp2040: - from esphome.components.rp2040 import copy_files - - (pio) = copy_files() - if pio: - write_file_if_changed( - CORE.relative_src_path("esphome.h"), - ESPHOME_H_FORMAT.format(include_s + '\n#include "pio_includes.h"'), - ) + except AttributeError: + pass def generate_defines_h():