1
0
mirror of https://github.com/esphome/esphome.git synced 2025-11-14 05:45:48 +00:00

Compare commits

...

15 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
16 changed files with 3400 additions and 137 deletions

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
@@ -204,7 +212,7 @@ def validate_font_config(config):
if font.get_char_index(x) != 0
]
if font.has_fixed_sizes:
if not font.is_scalable:
sizes = [pt_to_px(x.size) for x in font.available_sizes]
if not sizes:
raise cv.Invalid(
@@ -501,17 +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]
format = font.get_format().decode("utf-8")
if format != "PCF":
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)
font.load_char(codepoint)
font.glyph.render(mode)
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
@@ -535,7 +549,7 @@ async def to_code(config):
pos += 1
ascender = pt_to_px(font.size.ascender)
if ascender == 0:
if font.has_fixed_sizes:
if not font.is_scalable:
ascender = size
else:
_LOGGER.error(
@@ -585,7 +599,7 @@ async def to_code(config):
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:
if not base_font.is_scalable:
font_height = size
ascender = font_height
else:

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

@@ -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.8.0",
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

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

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

View File

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

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}