1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-20 08:46:01 +00:00

Compare commits

...

45 Commits

Author SHA1 Message Date
Keith Burzinski
584c5bd5be Merge pull request #8489 from esphome/bump-2025.3.3
2025.3.3
2025-03-31 17:07:02 -05:00
Keith Burzinski
79c8a55459 Bump version to 2025.3.3 2025-03-31 12:48:16 -05:00
Kevin Ahrendt
36d6fe29f2 [speaker] Bugfixes: two pause state issues (#8488) 2025-03-31 12:48:16 -05:00
Clyde Stubbs
e1868ddecb [lvgl] Implement switch restore (#8481) 2025-03-31 12:48:16 -05:00
Kevin Ahrendt
6151644b96 [speaker] Bugfix: Media player always unpauses when receiving a stop command (#8474) 2025-03-31 12:48:15 -05:00
J. Nick Koston
a4914eb5b7 Bump ESP mdns to 1.8.2 (#8482) 2025-03-31 12:48:15 -05:00
Clyde Stubbs
57a57f0d6a [display] Don't assume glyph x_offset is zero. (#8473) 2025-03-31 12:48:15 -05:00
Keith Burzinski
573088aadb Merge pull request #8469 from esphome/bump-2025.3.2
2025.3.2
2025-03-25 18:06:42 -05:00
Keith Burzinski
031b1c8bd0 Bump version to 2025.3.2 2025-03-25 15:22:11 -05:00
Keith Burzinski
f95b2ba898 [ld2450] Fix bluetooth state not reported correctly (#8458) 2025-03-25 15:22:11 -05:00
Kevin Ahrendt
ea4b573f9a [speaker] Bugfix: Fix rapidly adding items to playlist (#8466)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-03-25 15:22:11 -05:00
Kevin Ahrendt
8fcbd57f2f [media_player] Don't reset enqueue command (#8465) 2025-03-25 15:22:11 -05:00
Samuel Sieb
f131186e6b fix 1bpp rendering (#8463) 2025-03-25 15:22:11 -05:00
Clyde Stubbs
20c7778524 [font] More robust handling of fixed font sizes. (#8443)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-03-25 15:22:11 -05:00
Clyde Stubbs
2d8e86324b [gt911][cst226][ektf2232] Swap x and y calibration values (#8450)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-03-25 15:22:10 -05:00
Keith Burzinski
8ea4d8402f Merge pull request #8451 from esphome/bump-2025.3.1
2025.3.1
2025-03-22 23:45:18 -05:00
Keith Burzinski
2c53408cfc Bump version to 2025.3.1 2025-03-22 23:14:32 -05:00
Clyde Stubbs
33dce6e522 [lvgl] Ensure non-zero screen dimensions during init (#8444) 2025-03-22 23:14:32 -05:00
Clyde Stubbs
e213932b7c [lvgl] Set correct buffer size (#8442) 2025-03-22 23:14:32 -05:00
Clyde Stubbs
42fb0e2809 [ft63x6] Get correct dimensions from display (#8417) 2025-03-22 23:14:31 -05:00
Keith Burzinski
c4de9e87e4 Merge pull request #8438 from esphome/bump-2025.3.0
2025.3.0
2025-03-19 23:37:13 -05:00
Keith Burzinski
918924d697 Bump version to 2025.3.0 2025-03-19 20:54:32 -05:00
Keith Burzinski
e2c16b4baa Merge pull request #8436 from esphome/bump-2025.3.0b5
2025.3.0b5
2025-03-19 16:01:39 -05:00
Keith Burzinski
10a9162f48 Bump version to 2025.3.0b5 2025-03-19 14:36:04 -05:00
Kevin Ahrendt
fbc884772c [audio] Bugfix: fix flac decoding glitches by using esp-audio-libs v1.1.3 (#8431) 2025-03-19 14:36:03 -05:00
Keith Burzinski
54e3153f27 Merge pull request #8428 from esphome/bump-2025.3.0b4
2025.3.0b4
2025-03-18 15:21:15 -05:00
Keith Burzinski
c2e0a01106 Bump version to 2025.3.0b4 2025-03-18 14:43:26 -05:00
Clyde Stubbs
d2c2439b97 [core] Handle mis-typed platform name more cleanly (#8424) 2025-03-18 14:43:25 -05:00
Keith Burzinski
a8d33dd26a [docker] Bump libfreetype (#8426) 2025-03-18 14:43:25 -05:00
Keith Burzinski
da41a9204e [docker] Bump curl, git, openssh-client, libopenjp2-7, nginx-light (#8419) 2025-03-18 14:43:25 -05:00
Keith Burzinski
5c6368b6b8 Merge pull request #8415 from esphome/bump-2025.3.0b3
2025.3.0b3
2025-03-16 01:53:55 -05:00
Keith Burzinski
9bd7060f6b Bump version to 2025.3.0b3 2025-03-16 01:23:06 -05:00
Mikkel Jeppesen
fb1d178abc Added getters for graphs ymin and ymax (#8112)
Co-authored-by: guillempages <guillempages@users.noreply.github.com>
2025-03-16 01:23:06 -05:00
Clyde Stubbs
90c96a0a0f [font] Fix issues with bitmap fonts (#8407) 2025-03-16 01:23:05 -05:00
Keith Burzinski
1bdf0fdc57 Merge pull request #8400 from esphome/bump-2025.3.0b2
2025.3.0b2
2025-03-13 01:31:17 -05:00
Keith Burzinski
4d95ff2ae0 Bump version to 2025.3.0b2 2025-03-12 23:23:27 -05:00
dependabot[bot]
f36d400058 Bump tornado from 6.4 to 6.4.2 (#8398)
Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-12 23:23:26 -05:00
J. Nick Koston
c63cf9d151 Bump cryptography to 44.0.2 (#8399) 2025-03-12 23:23:26 -05:00
J. Nick Koston
0a02c1461e Rework pyproject.toml to make it parseable by dependabot (#8397) 2025-03-12 23:23:26 -05:00
J. Nick Koston
b3a69c6c05 Bump aioesphomeapi to 29.6.0 (#8396) 2025-03-12 23:23:26 -05:00
Kevin Ahrendt
dd113f2972 [api] add voice assistant announce to the api (#8395) 2025-03-12 23:23:26 -05:00
Kevin Ahrendt
3c5a0091ee [core] add reallocation support to RAMAllocator (#8390) 2025-03-12 23:23:26 -05:00
Kevin Ahrendt
bf65b73569 [speaker, resampler, mixer] Make volume and mute getters virtual (#8391) 2025-03-12 23:23:26 -05:00
Kevin Ahrendt
a2b123a29a [audio, mixer] Memory and CPU performance improvements (#8387) 2025-03-12 23:23:25 -05:00
J. Nick Koston
3575f52cdf Bump mdns library to 1.8.0 (#8378)
Co-authored-by: Keith Burzinski <kbx81x@gmail.com>
2025-03-12 23:23:25 -05:00
39 changed files with 3614 additions and 251 deletions

View File

@@ -33,9 +33,9 @@ RUN \
python3-venv=3.11.2-1+b1 \
python3-wheel=0.38.4-2 \
iputils-ping=3:20221126-1+deb12u1 \
git=1:2.39.5-0+deb12u1 \
curl=7.88.1-10+deb12u8 \
openssh-client=1:9.2p1-2+deb12u4 \
git=1:2.39.5-0+deb12u2 \
curl=7.88.1-10+deb12u12 \
openssh-client=1:9.2p1-2+deb12u5 \
python3-cffi=1.15.1-5 \
libcairo2=1.16.0-7 \
libmagic1=1:5.44-3 \
@@ -76,7 +76,7 @@ BUILD_DEPS="
python3-dev=3.11.2-1+b1
zlib1g-dev=1:1.2.13.dfsg-1
libjpeg-dev=1:2.1.5-2
libfreetype-dev=2.12.1+dfsg-5+deb12u3
libfreetype-dev=2.12.1+dfsg-5+deb12u4
libssl-dev=3.0.15-1~deb12u1
libffi-dev=3.4.4-1
cargo=0.66.0+ds1-1
@@ -84,7 +84,7 @@ BUILD_DEPS="
"
LIB_DEPS="
libtiff6=4.5.0-6+deb12u1
libopenjp2-7=2.5.0-2
libopenjp2-7=2.5.0-2+deb12u1
"
if [ "$TARGETARCH$TARGETVARIANT" = "arm64" ]
then
@@ -160,7 +160,7 @@ RUN \
apt-get update \
# Use pinned versions so that we get updates with build caching
&& apt-get install -y --no-install-recommends \
nginx-light=1.22.1-9 \
nginx-light=1.22.1-9+deb12u1 \
&& rm -rf \
/tmp/* \
/var/{cache,log}/* \

View File

@@ -1567,6 +1567,8 @@ message VoiceAssistantAnnounceRequest {
string media_id = 1;
string text = 2;
string preannounce_media_id = 3;
bool start_conversation = 4;
}
message VoiceAssistantAnnounceFinished {

View File

@@ -7094,6 +7094,16 @@ void VoiceAssistantTimerEventResponse::dump_to(std::string &out) const {
out.append("}");
}
#endif
bool VoiceAssistantAnnounceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
switch (field_id) {
case 4: {
this->start_conversation = value.as_bool();
return true;
}
default:
return false;
}
}
bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
switch (field_id) {
case 1: {
@@ -7104,6 +7114,10 @@ bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLength
this->text = value.as_string();
return true;
}
case 3: {
this->preannounce_media_id = value.as_string();
return true;
}
default:
return false;
}
@@ -7111,6 +7125,8 @@ bool VoiceAssistantAnnounceRequest::decode_length(uint32_t field_id, ProtoLength
void VoiceAssistantAnnounceRequest::encode(ProtoWriteBuffer buffer) const {
buffer.encode_string(1, this->media_id);
buffer.encode_string(2, this->text);
buffer.encode_string(3, this->preannounce_media_id);
buffer.encode_bool(4, this->start_conversation);
}
#ifdef HAS_PROTO_MESSAGE_DUMP
void VoiceAssistantAnnounceRequest::dump_to(std::string &out) const {
@@ -7123,6 +7139,14 @@ void VoiceAssistantAnnounceRequest::dump_to(std::string &out) const {
out.append(" text: ");
out.append("'").append(this->text).append("'");
out.append("\n");
out.append(" preannounce_media_id: ");
out.append("'").append(this->preannounce_media_id).append("'");
out.append("\n");
out.append(" start_conversation: ");
out.append(YESNO(this->start_conversation));
out.append("\n");
out.append("}");
}
#endif

View File

@@ -1832,6 +1832,8 @@ class VoiceAssistantAnnounceRequest : public ProtoMessage {
public:
std::string media_id{};
std::string text{};
std::string preannounce_media_id{};
bool start_conversation{false};
void encode(ProtoWriteBuffer buffer) const override;
#ifdef HAS_PROTO_MESSAGE_DUMP
void dump_to(std::string &out) const override;
@@ -1839,6 +1841,7 @@ class VoiceAssistantAnnounceRequest : public ProtoMessage {
protected:
bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
};
class VoiceAssistantAnnounceFinished : public ProtoMessage {
public:

View File

@@ -118,4 +118,4 @@ def final_validate_audio_schema(
async def to_code(config):
cg.add_library("esphome/esp-audio-libs", "1.1.1")
cg.add_library("esphome/esp-audio-libs", "1.1.3")

View File

@@ -66,19 +66,30 @@ esp_err_t AudioDecoder::start(AudioFileType audio_file_type) {
case AudioFileType::FLAC:
this->flac_decoder_ = make_unique<esp_audio_libs::flac::FLACDecoder>();
this->free_buffer_required_ =
this->output_transfer_buffer_->capacity(); // We'll revise this after reading the header
this->output_transfer_buffer_->capacity(); // Adjusted and reallocated after reading the header
break;
#endif
#ifdef USE_AUDIO_MP3_SUPPORT
case AudioFileType::MP3:
this->mp3_decoder_ = esp_audio_libs::helix_decoder::MP3InitDecoder();
// MP3 always has 1152 samples per chunk
this->free_buffer_required_ = 1152 * sizeof(int16_t) * 2; // samples * size per sample * channels
// Always reallocate the output transfer buffer to the smallest necessary size
this->output_transfer_buffer_->reallocate(this->free_buffer_required_);
break;
#endif
case AudioFileType::WAV:
this->wav_decoder_ = make_unique<esp_audio_libs::wav_decoder::WAVDecoder>();
this->wav_decoder_->reset();
// Processing WAVs doesn't actually require a specific amount of buffer size, as it is already in PCM format.
// Thus, we don't reallocate to a minimum size.
this->free_buffer_required_ = 1024;
if (this->output_transfer_buffer_->capacity() < this->free_buffer_required_) {
this->output_transfer_buffer_->reallocate(this->free_buffer_required_);
}
break;
case AudioFileType::NONE:
default:
@@ -116,10 +127,18 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
uint32_t decoding_start = millis();
bool first_loop_iteration = true;
size_t bytes_processed = 0;
size_t bytes_available_before_processing = 0;
while (state == FileDecoderState::MORE_TO_PROCESS) {
// Transfer decoded out
if (!this->pause_output_) {
size_t bytes_written = this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
// Never shift the data in the output transfer buffer to avoid unnecessary, slow data moves
size_t bytes_written =
this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
if (this->audio_stream_info_.has_value()) {
this->accumulated_frames_written_ += this->audio_stream_info_.value().bytes_to_frames(bytes_written);
this->playback_ms_ +=
@@ -138,12 +157,24 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
// Decode more audio
size_t bytes_read = this->input_transfer_buffer_->transfer_data_from_source(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
// Only shift data on the first loop iteration to avoid unnecessary, slow moves
size_t bytes_read = this->input_transfer_buffer_->transfer_data_from_source(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS),
first_loop_iteration);
if ((this->potentially_failed_count_ > 0) && (bytes_read == 0)) {
if (!first_loop_iteration && (this->input_transfer_buffer_->available() < bytes_processed)) {
// Less data is available than what was processed in last iteration, so don't attempt to decode.
// This attempts to avoid the decoder from consistently trying to decode an incomplete frame. The transfer buffer
// will shift the remaining data to the start and copy more from the source the next time the decode function is
// called
break;
}
bytes_available_before_processing = this->input_transfer_buffer_->available();
if ((this->potentially_failed_count_ > 10) && (bytes_read == 0)) {
// Failed to decode in last attempt and there is no new data
if (this->input_transfer_buffer_->free() == 0) {
if ((this->input_transfer_buffer_->free() == 0) && first_loop_iteration) {
// The input buffer is full. Since it previously failed on the exact same data, we can never recover
state = FileDecoderState::FAILED;
} else {
@@ -175,6 +206,9 @@ AudioDecoderState AudioDecoder::decode(bool stop_gracefully) {
}
}
first_loop_iteration = false;
bytes_processed = bytes_available_before_processing - this->input_transfer_buffer_->available();
if (state == FileDecoderState::POTENTIALLY_FAILED) {
++this->potentially_failed_count_;
} else if (state == FileDecoderState::END_OF_FILE) {
@@ -207,13 +241,11 @@ FileDecoderState AudioDecoder::decode_flac_() {
size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed);
// Reallocate the output transfer buffer to the smallest necessary size
this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes();
if (this->output_transfer_buffer_->capacity() < this->free_buffer_required_) {
// Output buffer is not big enough
if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
// Couldn't reallocate output buffer
return FileDecoderState::FAILED;
}
if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
// Couldn't reallocate output buffer
return FileDecoderState::FAILED;
}
this->audio_stream_info_ =

View File

@@ -259,14 +259,14 @@ AudioReaderState AudioReader::file_read_() {
}
AudioReaderState AudioReader::http_read_() {
this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
if (esp_http_client_is_complete_data_received(this->client_)) {
if (this->output_transfer_buffer_->available() == 0) {
this->cleanup_connection_();
return AudioReaderState::FINISHED;
}
} else {
} else if (this->output_transfer_buffer_->free() > 0) {
size_t bytes_to_read = this->output_transfer_buffer_->free();
int received_len =
esp_http_client_read(this->client_, (char *) this->output_transfer_buffer_->get_buffer_end(), bytes_to_read);

View File

@@ -93,8 +93,9 @@ AudioResamplerState AudioResampler::resample(bool stop_gracefully, int32_t *ms_d
}
if (!this->pause_output_) {
// Move audio data to the sink
this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS));
// Move audio data to the sink without shifting the data in the output transfer buffer to avoid unnecessary, slow
// data moves
this->output_transfer_buffer_->transfer_data_to_sink(pdMS_TO_TICKS(READ_WRITE_TIMEOUT_MS), false);
} else {
// If paused, block to avoid wasting CPU resources
delay(READ_WRITE_TIMEOUT_MS);
@@ -115,6 +116,7 @@ AudioResamplerState AudioResampler::resample(bool stop_gracefully, int32_t *ms_d
if ((this->input_stream_info_.get_sample_rate() != this->output_stream_info_.get_sample_rate()) ||
(this->input_stream_info_.get_bits_per_sample() != this->output_stream_info_.get_bits_per_sample())) {
// Adjust gain by -3 dB to avoid clipping due to the resampling process
esp_audio_libs::resampler::ResamplerResults results =
this->resampler_->resample(this->input_transfer_buffer_->get_buffer_start(),
this->output_transfer_buffer_->get_buffer_end(), frames_available, frames_free, -3);

View File

@@ -33,12 +33,17 @@ size_t AudioTransferBuffer::free() const {
if (this->buffer_size_ == 0) {
return 0;
}
return this->buffer_size_ - (this->buffer_length_ - (this->data_start_ - this->buffer_));
return this->buffer_size_ - (this->buffer_length_ + (this->data_start_ - this->buffer_));
}
void AudioTransferBuffer::decrease_buffer_length(size_t bytes) {
this->buffer_length_ -= bytes;
this->data_start_ += bytes;
if (this->buffer_length_ > 0) {
this->data_start_ += bytes;
} else {
// All the data in the buffer has been consumed, reset the start pointer
this->data_start_ = this->buffer_;
}
}
void AudioTransferBuffer::increase_buffer_length(size_t bytes) { this->buffer_length_ += bytes; }
@@ -71,7 +76,7 @@ bool AudioTransferBuffer::has_buffered_data() const {
bool AudioTransferBuffer::reallocate(size_t new_buffer_size) {
if (this->buffer_length_ > 0) {
// Already has data in the buffer, fail
// Buffer currently has data, so reallocation is impossible
return false;
}
this->deallocate_buffer_();
@@ -106,12 +111,14 @@ void AudioTransferBuffer::deallocate_buffer_() {
this->buffer_length_ = 0;
}
size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_wait) {
// Shift data in buffer to start
if (this->buffer_length_ > 0) {
memmove(this->buffer_, this->data_start_, this->buffer_length_);
size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_wait, bool pre_shift) {
if (pre_shift) {
// Shift data in buffer to start
if (this->buffer_length_ > 0) {
memmove(this->buffer_, this->data_start_, this->buffer_length_);
}
this->data_start_ = this->buffer_;
}
this->data_start_ = this->buffer_;
size_t bytes_to_read = this->free();
size_t bytes_read = 0;
@@ -125,7 +132,7 @@ size_t AudioSourceTransferBuffer::transfer_data_from_source(TickType_t ticks_to_
return bytes_read;
}
size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait) {
size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait, bool post_shift) {
size_t bytes_written = 0;
if (this->available()) {
#ifdef USE_SPEAKER
@@ -139,11 +146,14 @@ size_t AudioSinkTransferBuffer::transfer_data_to_sink(TickType_t ticks_to_wait)
}
this->decrease_buffer_length(bytes_written);
}
if (post_shift) {
// Shift unwritten data to the start of the buffer
memmove(this->buffer_, this->data_start_, this->buffer_length_);
this->data_start_ = this->buffer_;
}
return bytes_written;
}

View File

@@ -60,6 +60,7 @@ class AudioTransferBuffer {
protected:
/// @brief Allocates the transfer buffer in external memory, if available.
/// @param buffer_size The number of bytes to allocate
/// @return True is successful, false otherwise.
bool allocate_buffer_(size_t buffer_size);
@@ -89,8 +90,10 @@ class AudioSinkTransferBuffer : public AudioTransferBuffer {
/// @brief Writes any available data in the transfer buffer to the sink.
/// @param ticks_to_wait FreeRTOS ticks to block while waiting for the sink to have enough space
/// @param post_shift If true, all remaining data is moved to the start of the buffer after transferring to the sink.
/// Defaults to true.
/// @return Number of bytes written
size_t transfer_data_to_sink(TickType_t ticks_to_wait);
size_t transfer_data_to_sink(TickType_t ticks_to_wait, bool post_shift = true);
/// @brief Adds a ring buffer as the transfer buffer's sink.
/// @param ring_buffer weak_ptr to the allocated ring buffer
@@ -125,8 +128,10 @@ class AudioSourceTransferBuffer : public AudioTransferBuffer {
/// @brief Reads any available data from the sink into the transfer buffer.
/// @param ticks_to_wait FreeRTOS ticks to block while waiting for the source to have enough data
/// @param pre_shift If true, any unwritten data is moved to the start of the buffer before transferring from the
/// source. Defaults to true.
/// @return Number of bytes read
size_t transfer_data_from_source(TickType_t ticks_to_wait);
size_t transfer_data_from_source(TickType_t ticks_to_wait, bool pre_shift = true);
/// @brief Adds a ring buffer as the transfer buffer's source.
/// @param ring_buffer weak_ptr to the allocated ring buffer

View File

@@ -72,6 +72,8 @@ void CST226Touchscreen::continue_setup_() {
if (this->read16_(0xD1F8, buffer, 4)) {
this->x_raw_max_ = buffer[0] + (buffer[1] << 8);
this->y_raw_max_ = buffer[2] + (buffer[3] << 8);
if (this->swap_x_y_)
std::swap(this->x_raw_max_, this->y_raw_max_);
} else {
this->x_raw_max_ = this->display_->get_native_width();
this->y_raw_max_ = this->display_->get_native_height();

View File

@@ -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:

View File

@@ -34,26 +34,29 @@ void EKTF2232Touchscreen::setup() {
// Get touch resolution
uint8_t received[4];
if (this->x_raw_max_ == this->x_raw_min_) {
this->write(GET_X_RES, 4);
if (this->read(received, 4)) {
ESP_LOGE(TAG, "Failed to read X resolution!");
if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) {
auto err = this->write(GET_X_RES, 4);
if (err == i2c::ERROR_OK) {
err = this->read(received, 4);
if (err == i2c::ERROR_OK) {
this->x_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4);
err = this->write(GET_Y_RES, 4);
if (err == i2c::ERROR_OK) {
err = this->read(received, 4);
if (err == i2c::ERROR_OK) {
this->y_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4);
}
}
}
}
if (err != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Failed to read calibration values!");
this->interrupt_pin_->detach_interrupt();
this->mark_failed();
return;
}
this->x_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4);
}
if (this->y_raw_max_ == this->y_raw_min_) {
this->write(GET_Y_RES, 4);
if (this->read(received, 4)) {
ESP_LOGE(TAG, "Failed to read Y resolution!");
this->interrupt_pin_->detach_interrupt();
this->mark_failed();
return;
}
this->y_raw_max_ = ((received[2])) | ((received[3] & 0xf0) << 4);
if (this->swap_x_y_)
std::swap(this->x_raw_max_, this->y_raw_max_);
}
this->set_power_state(true);
}

View File

@@ -7,7 +7,15 @@ from pathlib import Path
import re
import esphome_glyphsets as glyphsets
from freetype import Face, ft_pixel_mode_grays, ft_pixel_mode_mono
# pylint: disable=no-name-in-module
from freetype import (
FT_LOAD_NO_BITMAP,
FT_LOAD_RENDER,
FT_LOAD_TARGET_MONO,
Face,
ft_pixel_mode_mono,
)
import requests
from esphome import external_files
@@ -146,6 +154,13 @@ def check_missing_glyphs(file, codepoints, warning: bool = False):
raise cv.Invalid(message)
def pt_to_px(pt):
"""
Convert a point size to pixels, rounding up to the nearest pixel
"""
return (pt + 63) // 64
def validate_font_config(config):
"""
Check for duplicate codepoints, then check that all requested codepoints actually
@@ -172,42 +187,43 @@ def validate_font_config(config):
)
# Make setpoints and glyphspoints disjoint
setpoints.difference_update(glyphspoints)
if fileconf[CONF_TYPE] == TYPE_LOCAL_BITMAP:
if any(x >= 256 for x in setpoints.copy().union(glyphspoints)):
raise cv.Invalid("Codepoints in bitmap fonts must be in the range 0-255")
else:
# for TT fonts, check that glyphs are actually present
# Check extras against their own font, exclude from parent font codepoints
for extra in config[CONF_EXTRAS]:
points = {ord(x) for x in flatten(extra[CONF_GLYPHS])}
glyphspoints.difference_update(points)
setpoints.difference_update(points)
check_missing_glyphs(extra[CONF_FILE], points)
# check that glyphs are actually present
# Check extras against their own font, exclude from parent font codepoints
for extra in config[CONF_EXTRAS]:
points = {ord(x) for x in flatten(extra[CONF_GLYPHS])}
glyphspoints.difference_update(points)
setpoints.difference_update(points)
check_missing_glyphs(extra[CONF_FILE], points)
# A named glyph that can't be provided is an error
# A named glyph that can't be provided is an error
check_missing_glyphs(fileconf, glyphspoints)
# A missing glyph from a set is a warning.
if not config[CONF_IGNORE_MISSING_GLYPHS]:
check_missing_glyphs(fileconf, setpoints, warning=True)
check_missing_glyphs(fileconf, glyphspoints)
# A missing glyph from a set is a warning.
if not config[CONF_IGNORE_MISSING_GLYPHS]:
check_missing_glyphs(fileconf, setpoints, warning=True)
# Populate the default after the above checks so that use of the default doesn't trigger errors
font = FONT_CACHE[fileconf]
if not config[CONF_GLYPHS] and not config[CONF_GLYPHSETS]:
if fileconf[CONF_TYPE] == TYPE_LOCAL_BITMAP:
config[CONF_GLYPHS] = [DEFAULT_GLYPHS]
else:
# set a default glyphset, intersected with what the font actually offers
font = FONT_CACHE[fileconf]
config[CONF_GLYPHS] = [
chr(x)
for x in glyphsets.unicodes_per_glyphset(DEFAULT_GLYPHSET)
if font.get_char_index(x) != 0
]
# set a default glyphset, intersected with what the font actually offers
config[CONF_GLYPHS] = [
chr(x)
for x in glyphsets.unicodes_per_glyphset(DEFAULT_GLYPHSET)
if font.get_char_index(x) != 0
]
if config[CONF_FILE][CONF_TYPE] == TYPE_LOCAL_BITMAP:
if CONF_SIZE in config:
if not font.is_scalable:
sizes = [pt_to_px(x.size) for x in font.available_sizes]
if not sizes:
raise cv.Invalid(
"Size is not a valid option for bitmap fonts, which are inherently fixed size"
f"Font {FontCache.get_name(fileconf)} has no available sizes"
)
if CONF_SIZE not in config:
config[CONF_SIZE] = sizes[0]
elif config[CONF_SIZE] not in sizes:
sizes = ", ".join(str(x) for x in sizes)
raise cv.Invalid(
f"Font {FontCache.get_name(fileconf)} only has size{'s' if len(sizes) != 1 else ''} {sizes} available"
)
elif CONF_SIZE not in config:
config[CONF_SIZE] = 20
@@ -215,14 +231,7 @@ def validate_font_config(config):
return config
FONT_EXTENSIONS = (".ttf", ".woff", ".otf")
BITMAP_EXTENSIONS = (".bdf", ".pcf")
def validate_bitmap_file(value):
if not any(map(value.lower().endswith, BITMAP_EXTENSIONS)):
raise cv.Invalid(f"Only {', '.join(BITMAP_EXTENSIONS)} files are supported.")
return CORE.relative_config_path(cv.file_(value))
FONT_EXTENSIONS = (".ttf", ".woff", ".otf", "bdf", ".pcf")
def validate_truetype_file(value):
@@ -246,7 +255,6 @@ def add_local_file(value):
TYPE_LOCAL = "local"
TYPE_LOCAL_BITMAP = "local_bitmap"
TYPE_GFONTS = "gfonts"
TYPE_WEB = "web"
LOCAL_SCHEMA = cv.All(
@@ -258,15 +266,6 @@ LOCAL_SCHEMA = cv.All(
add_local_file,
)
LOCAL_BITMAP_SCHEMA = cv.All(
cv.Schema(
{
cv.Required(CONF_PATH): validate_bitmap_file,
}
),
add_local_file,
)
FULLPATH_SCHEMA = cv.maybe_simple_value(
{cv.Required(CONF_PATH): cv.string}, key=CONF_PATH
)
@@ -403,15 +402,6 @@ def validate_file_shorthand(value):
}
)
extension = Path(value).suffix
if extension in BITMAP_EXTENSIONS:
return font_file_schema(
{
CONF_TYPE: TYPE_LOCAL_BITMAP,
CONF_PATH: value,
}
)
return font_file_schema(
{
CONF_TYPE: TYPE_LOCAL,
@@ -424,7 +414,6 @@ TYPED_FILE_SCHEMA = cv.typed_schema(
{
TYPE_LOCAL: LOCAL_SCHEMA,
TYPE_GFONTS: GFONTS_SCHEMA,
TYPE_LOCAL_BITMAP: LOCAL_BITMAP_SCHEMA,
TYPE_WEB: WEB_FONT_SCHEMA,
}
)
@@ -520,15 +509,23 @@ async def to_code(config):
glyph_args = {}
data = []
bpp = config[CONF_BPP]
mode = ft_pixel_mode_grays
scale = 256 // (1 << bpp)
size = config[CONF_SIZE]
# create the data array for all glyphs
for codepoint in codepoints:
font = point_font_map[codepoint]
if not font.has_fixed_sizes:
font.set_pixel_sizes(config[CONF_SIZE], 0)
font.load_char(codepoint)
font.glyph.render(mode)
if not font.is_scalable:
sizes = [pt_to_px(x.size) for x in font.available_sizes]
if size in sizes:
font.select_size(sizes.index(size))
else:
font.set_pixel_sizes(size, 0)
flags = FT_LOAD_RENDER
if bpp != 1:
flags |= FT_LOAD_NO_BITMAP
else:
flags |= FT_LOAD_TARGET_MONO
font.load_char(codepoint, flags)
width = font.glyph.bitmap.width
height = font.glyph.bitmap.rows
buffer = font.glyph.bitmap.buffer
@@ -550,17 +547,17 @@ async def to_code(config):
if pixel & (1 << (bpp - bit_num - 1)):
glyph_data[pos // 8] |= 0x80 >> (pos % 8)
pos += 1
ascender = font.size.ascender // 64
ascender = pt_to_px(font.size.ascender)
if ascender == 0:
if font.has_fixed_sizes:
ascender = font.available_sizes[0].height
if not font.is_scalable:
ascender = size
else:
_LOGGER.error(
"Unable to determine ascender of font %s", config[CONF_FILE]
)
glyph_args[codepoint] = GlyphInfo(
len(data),
font.glyph.metrics.horiAdvance // 64,
pt_to_px(font.glyph.metrics.horiAdvance),
font.glyph.bitmap_left,
ascender - font.glyph.bitmap_top,
width,
@@ -599,11 +596,11 @@ async def to_code(config):
glyphs = cg.static_const_array(config[CONF_RAW_GLYPH_ID], glyph_initializer)
font_height = base_font.size.height // 64
ascender = base_font.size.ascender // 64
font_height = pt_to_px(base_font.size.height)
ascender = pt_to_px(base_font.size.ascender)
if font_height == 0:
if base_font.has_fixed_sizes:
font_height = base_font.available_sizes[0].height
if not base_font.is_scalable:
font_height = size
ascender = font_height
else:
_LOGGER.error("Unable to determine height of font %s", config[CONF_FILE])

View File

@@ -42,10 +42,10 @@ void FT63X6Touchscreen::setup() {
// Get touch resolution
if (this->x_raw_max_ == this->x_raw_min_) {
this->x_raw_max_ = 320;
this->x_raw_max_ = this->display_->get_native_width();
}
if (this->y_raw_max_ == this->y_raw_min_) {
this->y_raw_max_ = 480;
this->y_raw_max_ = this->display_->get_native_height();
}
uint8_t chip_id = this->read_byte_(FT63X6_ADDR_CHIP_ID);
if (chip_id != 0) {
@@ -71,6 +71,8 @@ void FT63X6Touchscreen::dump_config() {
LOG_I2C_DEVICE(this);
LOG_PIN(" Interrupt Pin: ", this->interrupt_pin_);
LOG_PIN(" Reset Pin: ", this->reset_pin_);
ESP_LOGCONFIG(TAG, " X Calibration: [%d, %d]", this->x_raw_min_, this->x_raw_max_);
ESP_LOGCONFIG(TAG, " Y Calibration: [%d, %d]", this->y_raw_min_, this->y_raw_max_);
LOG_UPDATE_INTERVAL(this);
}

View File

@@ -132,6 +132,10 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo
yrange = ymax - ymin;
}
// Store graph limts
this->graph_limit_max_ = ymax;
this->graph_limit_min_ = ymin;
/// Draw grid
if (!std::isnan(this->gridspacing_y_)) {
for (int y = yn; y <= ym; y++) {

View File

@@ -161,11 +161,15 @@ class Graph : public Component {
uint32_t get_duration() { return duration_; }
uint32_t get_width() { return width_; }
uint32_t get_height() { return height_; }
float get_graph_limit_min() { return graph_limit_min_; }
float get_graph_limit_max() { return graph_limit_max_; }
protected:
uint32_t duration_; /// in seconds
uint32_t width_; /// in pixels
uint32_t height_; /// in pixels
float graph_limit_min_{NAN};
float graph_limit_max_{NAN};
float min_value_{NAN};
float max_value_{NAN};
float min_range_{1.0};

View File

@@ -60,20 +60,25 @@ void GT911Touchscreen::setup() {
}
}
}
if (err == i2c::ERROR_OK) {
err = this->write(GET_MAX_VALUES, 2);
if (this->x_raw_max_ == 0 || this->y_raw_max_ == 0) {
// no calibration? Attempt to read the max values from the touchscreen.
if (err == i2c::ERROR_OK) {
err = this->read(data, sizeof(data));
err = this->write(GET_MAX_VALUES, 2);
if (err == i2c::ERROR_OK) {
if (this->x_raw_max_ == this->x_raw_min_) {
err = this->read(data, sizeof(data));
if (err == i2c::ERROR_OK) {
this->x_raw_max_ = encode_uint16(data[1], data[0]);
}
if (this->y_raw_max_ == this->y_raw_min_) {
this->y_raw_max_ = encode_uint16(data[3], data[2]);
if (this->swap_x_y_)
std::swap(this->x_raw_max_, this->y_raw_max_);
}
esph_log_d(TAG, "calibration max_x/max_y %d/%d", this->x_raw_max_, this->y_raw_max_);
}
}
if (err != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Failed to read calibration values from touchscreen!");
this->mark_failed();
return;
}
}
if (err != i2c::ERROR_OK) {
ESP_LOGE(TAG, "Failed to communicate!");

View File

@@ -15,6 +15,7 @@ namespace esphome {
namespace ld2450 {
static const char *const TAG = "ld2450";
static const char *const NO_MAC("08:05:04:03:02:01");
static const char *const UNKNOWN_MAC("unknown");
// LD2450 UART Serial Commands
@@ -614,12 +615,12 @@ bool LD2450Component::handle_ack_data_(uint8_t *buffer, uint8_t len) {
ESP_LOGV(TAG, "MAC address: %s", this->mac_.c_str());
#ifdef USE_TEXT_SENSOR
if (this->mac_text_sensor_ != nullptr) {
this->mac_text_sensor_->publish_state(this->mac_);
this->mac_text_sensor_->publish_state(this->mac_ == NO_MAC ? UNKNOWN_MAC : this->mac_);
}
#endif
#ifdef USE_SWITCH
if (this->bluetooth_switch_ != nullptr) {
this->bluetooth_switch_->publish_state(this->mac_ != UNKNOWN_MAC);
this->bluetooth_switch_->publish_state(this->mac_ != NO_MAC);
}
#endif
break;

View File

@@ -423,8 +423,6 @@ LvglComponent::LvglComponent(std::vector<display::Display *> displays, float buf
this->disp_drv_.full_refresh = this->full_refresh_;
this->disp_drv_.flush_cb = static_flush_cb;
this->disp_drv_.rounder_cb = rounder_cb;
this->disp_drv_.hor_res = 0;
this->disp_drv_.ver_res = 0;
this->disp_ = lv_disp_drv_register(&this->disp_drv_);
}
@@ -441,14 +439,14 @@ void LvglComponent::setup() {
this->status_set_error("Memory allocation failure");
return;
}
lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buf_bytes);
lv_disp_draw_buf_init(&this->draw_buf_, buffer, nullptr, buffer_pixels);
this->disp_drv_.hor_res = width;
this->disp_drv_.ver_res = height;
// this->setup_driver_(display->get_width(), display->get_height());
lv_disp_drv_update(this->disp_, &this->disp_drv_);
this->rotation = display->get_rotation();
if (this->rotation != display::DISPLAY_ROTATION_0_DEGREES) {
this->rotate_buf_ = static_cast<lv_color_t *>(lv_custom_mem_alloc(this->draw_buf_.size)); // NOLINT
this->rotate_buf_ = static_cast<lv_color_t *>(lv_custom_mem_alloc(buf_bytes)); // NOLINT
if (this->rotate_buf_ == nullptr) {
this->mark_failed();
this->status_set_error("Memory allocation failure");

View File

@@ -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,

View File

@@ -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<void(bool)> 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<void(bool)> 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<void(bool)> state_lambda_{};
optional<bool> initial_state_{};
};
} // namespace lvgl

View File

@@ -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.5.1",
ref="mdns-v1.8.2",
path="components/mdns",
)

View File

@@ -56,7 +56,8 @@ const char *media_player_command_to_string(MediaPlayerCommand command) {
void MediaPlayerCall::validate_() {
if (this->media_url_.has_value()) {
if (this->command_.has_value()) {
if (this->command_.has_value() && this->command_.value() != MEDIA_PLAYER_COMMAND_ENQUEUE) {
// Don't remove an enqueue command
ESP_LOGW(TAG, "MediaPlayerCall: Setting both command and media_url is not needed.");
this->command_.reset();
}

View File

@@ -177,11 +177,15 @@ void SourceSpeaker::set_mute_state(bool mute_state) {
this->parent_->get_output_speaker()->set_mute_state(mute_state);
}
bool SourceSpeaker::get_mute_state() { return this->parent_->get_output_speaker()->get_mute_state(); }
void SourceSpeaker::set_volume(float volume) {
this->volume_ = volume;
this->parent_->get_output_speaker()->set_volume(volume);
}
float SourceSpeaker::get_volume() { return this->parent_->get_output_speaker()->get_volume(); }
size_t SourceSpeaker::process_data_from_source(TickType_t ticks_to_wait) {
if (!this->transfer_buffer_.use_count()) {
return 0;
@@ -490,7 +494,8 @@ void MixerSpeaker::audio_mixer_task(void *params) {
break;
}
output_transfer_buffer->transfer_data_to_sink(pdMS_TO_TICKS(TASK_DELAY_MS));
// Never shift the data in the output transfer buffer to avoid unnecessary, slow data moves
output_transfer_buffer->transfer_data_to_sink(pdMS_TO_TICKS(TASK_DELAY_MS), false);
const uint32_t output_frames_free =
this_mixer->audio_stream_info_.value().bytes_to_frames(output_transfer_buffer->free());

View File

@@ -53,9 +53,11 @@ class SourceSpeaker : public speaker::Speaker, public Component {
/// @brief Mute state changes are passed to the parent's output speaker
void set_mute_state(bool mute_state) override;
bool get_mute_state() override;
/// @brief Volume state changes are passed to the parent's output speaker
void set_volume(float volume) override;
float get_volume() override;
void set_pause_state(bool pause_state) override { this->pause_state_ = pause_state; }
bool get_pause_state() const override { return this->pause_state_; }

View File

@@ -34,9 +34,11 @@ class ResamplerSpeaker : public Component, public speaker::Speaker {
/// @brief Mute state changes are passed to the parent's output speaker
void set_mute_state(bool mute_state) override;
bool get_mute_state() override { return this->output_speaker_->get_mute_state(); }
/// @brief Volume state changes are passed to the parent's output speaker
void set_volume(float volume) override;
float get_volume() override { return this->output_speaker_->get_volume(); }
void set_output_speaker(speaker::Speaker *speaker) { this->output_speaker_ = speaker; }
void set_task_stack_in_psram(bool task_stack_in_psram) { this->task_stack_in_psram_ = task_stack_in_psram; }

View File

@@ -100,7 +100,7 @@ void SpeakerMediaPlayer::setup() {
if (!this->single_pipeline_()) {
this->media_pipeline_ = make_unique<AudioPipeline>(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");
@@ -138,77 +138,64 @@ void SpeakerMediaPlayer::watch_media_commands_() {
}
MediaCallCommand media_command;
esp_err_t err = ESP_OK;
if (xQueueReceive(this->media_control_command_queue_, &media_command, 0) == pdTRUE) {
bool new_url = media_command.new_url.has_value() && media_command.new_url.value();
bool new_file = media_command.new_file.has_value() && media_command.new_file.value();
bool enqueue = media_command.enqueue.has_value() && media_command.enqueue.value();
if (new_url || new_file) {
bool enqueue = media_command.enqueue.has_value() && media_command.enqueue.value();
if (media_command.url.has_value() || media_command.file.has_value()) {
PlaylistItem playlist_item;
if (media_command.url.has_value()) {
playlist_item.url = *media_command.url.value();
delete media_command.url.value();
}
if (media_command.file.has_value()) {
playlist_item.file = media_command.file.value();
}
if (this->single_pipeline_() || (media_command.announce.has_value() && media_command.announce.value())) {
// Announcement playlist/pipeline
if (!enqueue) {
// Clear the queue and ensure the loaded next item doesn't start playing
// Ensure the loaded next item doesn't start playing, clear the queue, start the file, and unpause
this->cancel_timeout("next_ann");
this->announcement_playlist_.clear();
}
PlaylistItem playlist_item;
if (new_url) {
playlist_item.url = this->announcement_url_;
if (!enqueue) {
// Not adding to the queue, so directly start playback and internally unpause the pipeline
this->announcement_pipeline_->start_url(playlist_item.url.value());
this->announcement_pipeline_->set_pause_state(false);
}
} else {
playlist_item.file = this->announcement_file_;
if (!enqueue) {
// Not adding to the queue, so directly start playback and internally unpause the pipeline
if (media_command.file.has_value()) {
this->announcement_pipeline_->start_file(playlist_item.file.value());
this->announcement_pipeline_->set_pause_state(false);
} else if (media_command.url.has_value()) {
this->announcement_pipeline_->start_url(playlist_item.url.value());
}
this->announcement_pipeline_->set_pause_state(false);
}
this->announcement_playlist_.push_back(playlist_item);
} else {
// Media playlist/pipeline
if (!enqueue) {
// Clear the queue and ensure the loaded next item doesn't start playing
// 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();
}
this->is_paused_ = false;
PlaylistItem playlist_item;
if (new_url) {
playlist_item.url = this->media_url_;
if (!enqueue) {
// Not adding to the queue, so directly start playback and internally unpause the pipeline
this->media_pipeline_->start_url(playlist_item.url.value());
this->media_pipeline_->set_pause_state(false);
}
} else {
playlist_item.file = this->media_file_;
if (!enqueue) {
// Not adding to the queue, so directly start playback and internally unpause the pipeline
this->media_pipeline_->start_file(playlist_item.file.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_playlist_.push_back(playlist_item);
}
if (err != ESP_OK) {
ESP_LOGE(TAG, "Error starting the audio pipeline: %s", esp_err_to_name(err));
this->status_set_error();
} else {
this->status_clear_error();
}
return; // Don't process the new file play command further
}
@@ -232,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) {
@@ -361,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 {
@@ -401,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_); });
}
@@ -429,12 +435,10 @@ void SpeakerMediaPlayer::play_file(audio::AudioFile *media_file, bool announceme
MediaCallCommand media_command;
media_command.new_file = true;
media_command.file = media_file;
if (this->single_pipeline_() || announcement) {
this->announcement_file_ = media_file;
media_command.announce = true;
} else {
this->media_file_ = media_file;
media_command.announce = false;
}
media_command.enqueue = enqueue;
@@ -456,14 +460,8 @@ void SpeakerMediaPlayer::control(const media_player::MediaPlayerCall &call) {
}
if (call.get_media_url().has_value()) {
std::string new_uri = call.get_media_url().value();
media_command.new_url = true;
if (this->single_pipeline_() || (call.get_announcement().has_value() && call.get_announcement().value())) {
this->announcement_url_ = new_uri;
} else {
this->media_url_ = new_uri;
}
media_command.url = new std::string(
call.get_media_url().value()); // Must be manually deleted after receiving media_command from a queue
if (call.get_command().has_value()) {
if (call.get_command().value() == media_player::MEDIA_PLAYER_COMMAND_ENQUEUE) {

View File

@@ -24,8 +24,8 @@ struct MediaCallCommand {
optional<media_player::MediaPlayerCommand> command;
optional<float> volume;
optional<bool> announce;
optional<bool> new_url;
optional<bool> new_file;
optional<std::string *> url; // Must be manually deleted after receiving this struct from a queue
optional<audio::AudioFile *> file;
optional<bool> enqueue;
};
@@ -109,15 +109,11 @@ class SpeakerMediaPlayer : public Component, public media_player::MediaPlayer {
optional<media_player::MediaPlayerSupportedFormat> media_format_;
AudioPipelineState media_pipeline_state_{AudioPipelineState::STOPPED};
std::string media_url_{}; // only modified by control function
audio::AudioFile *media_file_{}; // only modified by play_file function
bool media_repeat_one_{false};
uint32_t media_playlist_delay_ms_{0};
optional<media_player::MediaPlayerSupportedFormat> announcement_format_;
AudioPipelineState announcement_pipeline_state_{AudioPipelineState::STOPPED};
std::string announcement_url_{}; // only modified by control function
audio::AudioFile *announcement_file_{}; // only modified by play_file function
bool announcement_repeat_one_{false};
uint32_t announcement_playlist_delay_ms_{0};

View File

@@ -76,7 +76,7 @@ class Speaker {
}
#endif
};
float get_volume() { return this->volume_; }
virtual float get_volume() { return this->volume_; }
virtual void set_mute_state(bool mute_state) {
this->mute_state_ = mute_state;
@@ -90,7 +90,7 @@ class Speaker {
}
#endif
}
bool get_mute_state() { return this->mute_state_; }
virtual 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; }

View File

@@ -1,6 +1,6 @@
"""Constants used by esphome."""
__version__ = "2025.3.0b1"
__version__ = "2025.3.3"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@@ -189,10 +189,11 @@ def _is_target_platform(name):
from esphome.loader import get_component
try:
if get_component(name, True).is_target_platform:
return True
return get_component(name, True).is_target_platform
except KeyError:
pass
except ImportError:
pass
return False

View File

@@ -719,6 +719,25 @@ template<class T> class RAMAllocator {
return ptr;
}
T *reallocate(T *p, size_t n) { return this->reallocate(p, n, sizeof(T)); }
T *reallocate(T *p, size_t n, size_t manual_size) {
size_t size = n * sizeof(T);
T *ptr = nullptr;
#ifdef USE_ESP32
if (this->flags_ & Flags::ALLOC_EXTERNAL) {
ptr = static_cast<T *>(heap_caps_realloc(p, size, MALLOC_CAP_SPIRAM | MALLOC_CAP_8BIT));
}
if (ptr == nullptr && this->flags_ & Flags::ALLOC_INTERNAL) {
ptr = static_cast<T *>(heap_caps_realloc(p, size, MALLOC_CAP_INTERNAL | MALLOC_CAP_8BIT));
}
#else
// Ignore ALLOC_EXTERNAL/ALLOC_INTERNAL flags if external allocation is not supported
ptr = static_cast<T *>(realloc(p, size)); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc)
#endif
return ptr;
}
void deallocate(T *p, size_t n) {
free(p); // NOLINT(cppcoreguidelines-owning-memory,cppcoreguidelines-no-malloc)
}

View File

@@ -7,7 +7,7 @@ dependencies:
version: v2.0.9
mdns:
git: https://github.com/espressif/esp-protocols.git
version: mdns-v1.5.1
version: mdns-v1.8.2
path: components/mdns
rules:
- if: "idf_version >=5.0"

View File

@@ -128,7 +128,7 @@ lib_deps =
DNSServer ; captive_portal (Arduino built-in)
esphome/ESP32-audioI2S@2.0.7 ; i2s_audio
droscy/esp_wireguard@0.4.2 ; wireguard
esphome/esp-audio-libs@1.1.1 ; audio
esphome/esp-audio-libs@1.1.3 ; audio
build_flags =
${common:arduino.build_flags}
@@ -149,7 +149,7 @@ lib_deps =
${common:idf.lib_deps}
droscy/esp_wireguard@0.4.2 ; wireguard
kahrendt/ESPMicroSpeechFeatures@1.1.0 ; micro_wake_word
esphome/esp-audio-libs@1.1.1 ; audio
esphome/esp-audio-libs@1.1.3 ; audio
build_flags =
${common:idf.build_flags}
-Wno-nonnull-compare

View File

@@ -43,11 +43,13 @@ include-package-data = true
[tool.setuptools.dynamic]
dependencies = {file = ["requirements.txt"]}
optional-dependencies.dev = { file = ["requirements_dev.txt"] }
optional-dependencies.test = { file = ["requirements_test.txt"] }
optional-dependencies.displays = { file = ["requirements_optional.txt"] }
version = {attr = "esphome.const.__version__"}
[tool.setuptools.dynamic.optional-dependencies]
dev = { file = ["requirements_dev.txt"] }
test = { file = ["requirements_test.txt"] }
displays = { file = ["requirements_optional.txt"] }
[tool.setuptools.packages.find]
include = ["esphome*"]

View File

@@ -1,11 +1,11 @@
async_timeout==4.0.3; python_version <= "3.10"
cryptography==43.0.0
cryptography==44.0.2
voluptuous==0.14.2
PyYAML==6.0.2
paho-mqtt==1.6.1
colorama==0.4.6
icmplib==3.0.4
tornado==6.4
tornado==6.4.2
tzlocal==5.2 # from time
tzdata>=2021.1 # from time
pyserial==3.5
@@ -13,7 +13,7 @@ platformio==6.1.16 # When updating platformio, also update Dockerfile
esptool==4.8.1
click==8.1.7
esphome-dashboard==20250212.0
aioesphomeapi==29.5.1
aioesphomeapi==29.6.0
zeroconf==0.146.1
puremagic==1.27
ruamel.yaml==0.18.6 # dashboard_import

File diff suppressed because it is too large Load Diff

View File

@@ -43,6 +43,9 @@ font:
id: default_font
- file: $component_dir/x11.pcf
id: pcf_font
- file: $component_dir/Tamzen5x9b.bdf
id: bdf_font
size: 7
i2c:
scl: ${i2c_scl}