Merge pull request #16488 from esphome/bump-2026.5.0b2
CI for docker images / Build docker containers (docker, ubuntu-24.04) (push) Has been cancelled
CI for docker images / Build docker containers (docker, ubuntu-24.04-arm) (push) Has been cancelled
CI for docker images / Build docker containers (ha-addon, ubuntu-24.04) (push) Has been cancelled
CI for docker images / Build docker containers (ha-addon, ubuntu-24.04-arm) (push) Has been cancelled
CI / Create common environment (push) Has been cancelled
CI / Check pylint (push) Has been cancelled
CI / Run script/ci-custom (push) Has been cancelled
CI / Check import esphome.__main__ time (push) Has been cancelled
CI / Test downstream esphome/device-builder (push) Has been cancelled
CI / Run pytest (macOS-latest, 3.11) (push) Has been cancelled
CI / Run pytest (macOS-latest, 3.14) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.11) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.13) (push) Has been cancelled
CI / Run pytest (ubuntu-latest, 3.14) (push) Has been cancelled
CI / Run pytest (windows-latest, 3.11) (push) Has been cancelled
CI / Run pytest (windows-latest, 3.14) (push) Has been cancelled
CI / Determine which jobs to run (push) Has been cancelled
CI / Run integration tests (${{ matrix.bucket.name }}) (push) Has been cancelled
CI / Run C++ unit tests (push) Has been cancelled
CI / Run CodSpeed benchmarks (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 IDF (push) Has been cancelled
CI / Run script/clang-tidy for ESP8266 (push) Has been cancelled
CI / Run script/clang-tidy for ZEPHYR (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 1/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 2/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 3/4 (push) Has been cancelled
CI / Run script/clang-tidy for ESP32 Arduino 4/4 (push) Has been cancelled
CI / Test components batch (${{ matrix.components }}) (push) Has been cancelled
CI / Test components with native ESP-IDF (push) Has been cancelled
CI / pre-commit.ci lite (push) Has been cancelled
CI / Build target branch for memory impact (push) Has been cancelled
CI / Build PR branch for memory impact (push) Has been cancelled
CI / Comment memory impact (push) Has been cancelled
CI / CI Status (push) Has been cancelled

2026.5.0b2
This commit is contained in:
Jesse Hills
2026-05-18 15:28:53 +12:00
committed by GitHub
62 changed files with 483 additions and 167 deletions
+1 -1
View File
@@ -48,7 +48,7 @@ PROJECT_NAME = ESPHome
# could be handy for archiving the generated documentation or if some version
# control system is used.
PROJECT_NUMBER = 2026.5.0b1
PROJECT_NUMBER = 2026.5.0b2
# Using the PROJECT_BRIEF tag one can provide an optional one line description
# for a project that appears at the top of each page and should give viewer a
+7 -3
View File
@@ -13,12 +13,16 @@ RUN git config --system --add safe.directory "*" \
&& git config --system advice.detachedHead false
# Install build tools for Python packages that require compilation
# (e.g., ruamel.yaml.clibz used by ESP-IDF's idf-component-manager)
# (e.g., ruamel.yaml.clib used by ESP-IDF's idf-component-manager).
# Also install libusb-1.0 at runtime so the ESP-IDF tools installer can
# validate openocd-esp32 (it dynamically links libusb-1.0.so.0); without
# it idf_tools.py rejects the openocd install with exit 127 and aborts
# the whole framework setup.
RUN if command -v apk > /dev/null; then \
apk add --no-cache build-base; \
apk add --no-cache build-base libusb; \
else \
apt-get update \
&& apt-get install -y --no-install-recommends build-essential \
&& apt-get install -y --no-install-recommends build-essential libusb-1.0-0 \
&& rm -rf /var/lib/apt/lists/*; \
fi
+8
View File
@@ -50,6 +50,7 @@ from esphome.const import (
CONF_TOPIC,
CONF_USERNAME,
CONF_WEB_SERVER,
CONF_WIFI,
ENV_NOGITIGNORE,
KEY_CORE,
KEY_TARGET_PLATFORM,
@@ -733,6 +734,13 @@ def write_cpp_file() -> int:
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
# Keep this gate here, NOT in config validation: device-builder needs
# `esphome config` to keep succeeding with placeholders so onboarding can run.
if CONF_WIFI in config:
from esphome.components.wifi import check_placeholder_credentials
check_placeholder_credentials(config)
# NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py
# If you change this format, update the regex in that script as well
_LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
+8 -2
View File
@@ -3,7 +3,8 @@
import json
from pathlib import Path
from esphome.components.esp32 import get_esp32_variant
from esphome.components.esp32 import get_esp32_variant, idf_version
import esphome.config_validation as cv
from esphome.core import CORE
from esphome.helpers import mkdir_p, write_file_if_changed
from esphome.writer import update_storage_json
@@ -61,6 +62,11 @@ def get_project_cmakelists(minimal: bool = False) -> str:
variant = get_esp32_variant()
idf_target = variant.lower().replace("-", "")
# esp_idf_size 2.x (bundled with IDF >=6.0) made NG the default and
# removed the --ng flag; on 1.x (IDF 5.5) --ng is required to get
# --format=raw because the legacy mode doesn't support it.
size_ng_flag = "--ng" if idf_version() < cv.Version(6, 0, 0) else ""
# Project-wide compile options: -D defines and -W warning flags (skip
# -Wl, linker flags — those go on the src component via
# target_link_options below). Emitted via idf_build_set_property so the
@@ -146,7 +152,7 @@ project({CORE.name})
# Emit raw JSON size data for ESPHome to read post-build.
add_custom_command(
TARGET ${{CMAKE_PROJECT_NAME}}.elf POST_BUILD
COMMAND ${{PYTHON}} -m esp_idf_size --ng --format=raw
COMMAND ${{PYTHON}} -m esp_idf_size {size_ng_flag} --format=raw
-o ${{CMAKE_BINARY_DIR}}/esp_idf_size.json
${{CMAKE_PROJECT_NAME}}.map
WORKING_DIRECTORY ${{CMAKE_BINARY_DIR}}
+1 -1
View File
@@ -395,7 +395,7 @@ async def to_code(config):
)
if data.mp3_support:
cg.add_define("USE_AUDIO_MP3_SUPPORT")
add_idf_component(name="esphome/micro-mp3", ref="0.2.0")
add_idf_component(name="esphome/micro-mp3", ref="0.2.1")
_emit_memory_pair(
data.mp3.buffer_memory,
"CONFIG_MP3_DECODER_PREFER_PSRAM",
@@ -161,7 +161,7 @@ void BME680BSECComponent::dump_config() {
" IAQ Mode: %s\n"
" Supply Voltage: %sV\n"
" Sample Rate: %s\n"
" State Save Interval: %ims",
" State Save Interval: %" PRIu32 "ms",
this->temperature_offset_, this->iaq_mode_ == IAQ_MODE_STATIC ? "Static" : "Mobile",
this->supply_voltage_ == SUPPLY_VOLTAGE_3V3 ? "3.3" : "1.8",
BME680_BSEC_SAMPLE_RATE_LOG(this->sample_rate_), this->state_save_interval_ms_);
@@ -461,7 +461,7 @@ int8_t BME680BSECComponent::write_bytes_wrapper(uint8_t devid, uint8_t a_registe
}
void BME680BSECComponent::delay_ms(uint32_t period) {
ESP_LOGV(TAG, "Delaying for %ums", period);
ESP_LOGV(TAG, "Delaying for %" PRIu32 "ms", period);
delay(period);
}
+16 -3
View File
@@ -1767,9 +1767,11 @@ async def to_code(config):
else:
cg.add_build_flag("-Wno-error=format")
cg.add_build_flag("-Wno-error=maybe-uninitialized")
cg.add_build_flag("-Wno-error=missing-field-initializers")
cg.add_build_flag("-Wno-error=overloaded-virtual")
cg.add_build_flag("-Wno-error=reorder")
cg.add_build_flag("-Wno-error=volatile")
# -Wno- (not -Wno-error=): suppress entirely, too noisy on C++ aggregates
cg.add_build_flag("-Wno-missing-field-initializers")
cg.set_cpp_standard("gnu++20")
cg.add_build_flag("-DUSE_ESP32")
@@ -2464,8 +2466,14 @@ def _write_sdkconfig():
)
want_opts = CORE.data[KEY_ESP32][KEY_SDKCONFIG_OPTIONS]
# Include the resolved framework version as a Kconfig comment so a
# version switch that happens to leave the option set unchanged still
# bumps this file's content -- which is what has_outdated_files()
# uses to decide whether to reconfigure.
framework_version = CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION]
contents = (
"\n".join(
f"# ESPHOME_IDF_VERSION={framework_version}\n"
+ "\n".join(
f"{name}={_format_sdkconfig_val(value)}"
for name, value in sorted(want_opts.items())
)
@@ -2509,7 +2517,12 @@ def _write_idf_component_yml():
stubs_dir = CORE.relative_build_path("component_stubs")
stubs_dir.mkdir(exist_ok=True)
for component_name in components_to_stub:
# Sort so the dict insertion order (and thus the generated
# src/idf_component.yml) is deterministic across runs; otherwise
# the manifest content shuffles every build, write_file_if_changed
# always writes, and ninja keeps triggering CMake re-runs on
# otherwise-cached rebuilds.
for component_name in sorted(components_to_stub):
# Create stub directory with minimal CMakeLists.txt
stub_path = stubs_dir / _idf_component_stub_name(component_name)
stub_path.mkdir(exist_ok=True)
+1 -1
View File
@@ -249,7 +249,7 @@ async def to_code(config):
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.5.1")
esp32.add_idf_component(name="espressif/wifi_remote_over_eppp", ref="0.3.2")
esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.5")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.6")
esp32.add_idf_component(name="espressif/esp_hosted", ref="2.12.7")
else:
esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
@@ -92,7 +92,7 @@ void Esp32HostedUpdate::setup() {
if (esp_hosted_get_coprocessor_fwversion(&ver_info) == ESP_OK) {
// 16 bytes: "255.255.255" (11 chars) + null + safety margin
char buf[16];
snprintf(buf, sizeof(buf), "%d.%d.%d", ver_info.major1, ver_info.minor1, ver_info.patch1);
snprintf(buf, sizeof(buf), "%" PRIu32 ".%" PRIu32 ".%" PRIu32, ver_info.major1, ver_info.minor1, ver_info.patch1);
this->update_info_.current_version = buf;
} else {
this->update_info_.current_version = "unknown";
@@ -120,8 +120,8 @@ void Esp32HostedUpdate::setup() {
this->state_ = update::UPDATE_STATE_NO_UPDATE;
}
} else {
ESP_LOGW(TAG, "Invalid app description magic word: 0x%08x (expected 0x%08x)", app_desc->magic_word,
ESP_APP_DESC_MAGIC_WORD);
ESP_LOGW(TAG, "Invalid app description magic word: 0x%08" PRIx32 " (expected 0x%08" PRIx32 ")",
app_desc->magic_word, static_cast<uint32_t>(ESP_APP_DESC_MAGIC_WORD));
this->state_ = update::UPDATE_STATE_NO_UPDATE;
}
} else {
@@ -108,8 +108,8 @@ void ESPHomeOTAComponent::dump_config() {
ESP_LOGCONFIG(TAG,
" Partition access allowed\n"
" Running app:\n"
" Partition address: 0x%X\n"
" Used size: %zu bytes (0x%X)",
" Partition address: 0x%" PRIX32 "\n"
" Used size: %zu bytes (0x%zX)",
this->running_app_offset_, this->running_app_size_, this->running_app_size_);
#ifdef USE_ESP32
@@ -378,7 +378,7 @@ void ESPHomeOTAComponent::handle_data_() {
}
ota_size = (static_cast<size_t>(buf[0]) << 24) | (static_cast<size_t>(buf[1]) << 16) |
(static_cast<size_t>(buf[2]) << 8) | buf[3];
ESP_LOGV(TAG, "Size is %u bytes", ota_size);
ESP_LOGV(TAG, "Size is %zu bytes", ota_size);
#ifndef USE_OTA_PARTITIONS
if (ota_type != ota::OTA_TYPE_UPDATE_APP) {
@@ -749,7 +749,7 @@ bool ESPHomeOTAComponent::handle_auth_send_() {
this->auth_buf_[0] = this->auth_type_;
hasher.get_hex(buf);
ESP_LOGV(TAG, "Auth: Nonce is %.*s", hex_size, buf);
ESP_LOGV(TAG, "Auth: Nonce is %.*s", (int) hex_size, buf);
}
// Try to write auth_type + nonce
@@ -809,13 +809,13 @@ bool ESPHomeOTAComponent::handle_auth_read_() {
hasher.add(nonce, hex_size * 2); // Add both nonce and cnonce (contiguous in buffer)
hasher.calculate();
ESP_LOGV(TAG, "Auth: CNonce is %.*s", hex_size, cnonce);
ESP_LOGV(TAG, "Auth: CNonce is %.*s", (int) hex_size, cnonce);
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
char computed_hash[SHA256_HEX_SIZE + 1]; // Buffer for hex-encoded hash (max expected length + null terminator)
hasher.get_hex(computed_hash);
ESP_LOGV(TAG, "Auth: Result is %.*s", hex_size, computed_hash);
ESP_LOGV(TAG, "Auth: Result is %.*s", (int) hex_size, computed_hash);
#endif
ESP_LOGV(TAG, "Auth: Response is %.*s", hex_size, response);
ESP_LOGV(TAG, "Auth: Response is %.*s", (int) hex_size, response);
// Compare response
bool matches = hasher.equals_hex(response);
@@ -19,7 +19,7 @@ void FastLEDLightOutput::dump_config() {
ESP_LOGCONFIG(TAG,
"FastLED light:\n"
" Num LEDs: %u\n"
" Max refresh rate: %u",
" Max refresh rate: %" PRIu32,
this->num_leds_, this->max_refresh_rate_.value_or(0));
}
void FastLEDLightOutput::write_state(light::LightState *state) {
@@ -206,6 +206,7 @@ uint8_t FingerprintGrowComponent::save_fingerprint_() {
break;
case ENROLL_MISMATCH:
ESP_LOGE(TAG, "Scans do not match");
[[fallthrough]];
default:
return this->data_[0];
}
@@ -15,6 +15,16 @@ void FT5x06Touchscreen::setup() {
this->attach_interrupt_(this->interrupt_pin_, gpio::INTERRUPT_FALLING_EDGE);
}
// reading the chip registers to get max x/y does not seem to work.
if (this->display_ != nullptr) {
if (this->x_raw_max_ == this->x_raw_min_) {
this->x_raw_max_ = this->display_->get_native_width();
}
if (this->y_raw_max_ == this->y_raw_min_) {
this->y_raw_max_ = this->display_->get_native_height();
}
}
// wait 200ms after reset.
this->set_timeout(200, [this] { this->continue_setup_(); });
}
@@ -39,15 +49,6 @@ void FT5x06Touchscreen::continue_setup_() {
this->mark_failed();
return;
}
// reading the chip registers to get max x/y does not seem to work.
if (this->display_ != nullptr) {
if (this->x_raw_max_ == this->x_raw_min_) {
this->x_raw_max_ = this->display_->get_native_width();
}
if (this->y_raw_max_ == this->y_raw_min_) {
this->y_raw_max_ = this->display_->get_native_height();
}
}
}
void FT5x06Touchscreen::update_touches() {
@@ -71,7 +72,7 @@ void FT5x06Touchscreen::update_touches() {
uint16_t x = encode_uint16(data[i][0] & 0x0F, data[i][1]);
uint16_t y = encode_uint16(data[i][2] & 0xF, data[i][3]);
ESP_LOGD(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y);
ESP_LOGV(TAG, "Read %X status, id: %d, pos %d/%d", status, id, x, y);
if (status == 0 || status == 2) {
this->add_raw_touch_position_(id, x, y);
}
+1 -1
View File
@@ -22,7 +22,7 @@ static constexpr uint8_t MEAS_CONF_HUM = 0x04; // Bits 2:1 = 10: humidity only
void HDC2080Component::setup() {
const uint8_t data = 0x00; // automatic measurement mode disabled, heater off
if (this->write_register(REG_RESET_DRDY_INT_CONF, &data, 1) != i2c::ERROR_OK) {
this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
this->mark_failed(LOG_STR(ESP_LOG_MSG_COMM_FAIL));
return;
}
}
-1
View File
@@ -125,7 +125,6 @@ async def to_code(config):
cg.add(var.set_vertical_default(config[CONF_VERTICAL_DEFAULT]))
cg.add(var.set_max_temperature(config[CONF_MAX_TEMPERATURE]))
cg.add(var.set_min_temperature(config[CONF_MIN_TEMPERATURE]))
cg.add_build_flag("-Wno-error=overloaded-virtual")
cg.add_library("tonia/HeatpumpIR", "1.0.41")
if CORE.is_libretiny or CORE.is_esp32:
+1 -1
View File
@@ -319,7 +319,7 @@ void Inkplate::fill(Color color) {
memset(this->partial_buffer_, fill, this->get_buffer_length_());
}
ESP_LOGV(TAG, "Fill finished (%ums)", millis() - start_time);
ESP_LOGV(TAG, "Fill finished (%" PRIu32 "ms)", millis() - start_time);
}
void Inkplate::display() {
+7 -7
View File
@@ -506,13 +506,13 @@ async def _late_logger_init(config: ConfigType) -> None:
def validate_printf(value):
# https://stackoverflow.com/questions/30011379/how-can-i-parse-a-c-format-string-in-python
cfmt = r"""
( # start of capture group 1
% # literal "%"
(?:[-+0 #]{0,5}) # optional flags
(?:\d+|\*)? # width
(?:\.(?:\d+|\*))? # precision
(?:h|l|ll|w|I|I32|I64)? # size
[cCdiouxXeEfgGaAnpsSZ] # type
( # start of capture group 1
% # literal "%"
(?:[-+0 #]{0,5}) # optional flags
(?:\d+|\*)? # width
(?:\.(?:\d+|\*))? # precision
(?:hh|h|ll|l|j|z|t|L|w|I|I32|I64)? # size
[cCdiouxXeEfgGaAnpsSZ] # type
)
""" # noqa
matches = re.findall(cfmt, value[CONF_FORMAT], flags=re.VERBOSE)
+5
View File
@@ -55,6 +55,7 @@ from .automation import layers_to_code, lvgl_update
from .defines import (
CONF_ALIGN_TO_LAMBDA_ID,
LOGGER,
add_lv_use,
get_focused_widgets,
get_lv_images_used,
get_refreshed_widgets,
@@ -71,6 +72,7 @@ from .keypads import KEYPADS_CONFIG, keypads_to_code
from .lv_validation import lv_bool
from .lvcode import LvContext, LvglComponent, lv_event_t_ptr, lvgl_static
from .schemas import (
BASE_PROPS,
DISP_BG_SCHEMA,
FULL_STYLE_SCHEMA,
STYLE_REMAP,
@@ -100,6 +102,7 @@ from .widgets import (
get_screen_active,
set_obj_properties,
)
from .widgets.img import CONF_IMAGE
# Import only what we actually use directly in this file
from .widgets.msgbox import MSGBOX_SCHEMA, msgboxes_to_code
@@ -433,6 +436,8 @@ async def to_code(configs):
# This must be done after all widgets are created
styles_used = df.get_styles_used()
if any(BASE_PROPS.get(x) is lvalid.lv_image for x in styles_used):
add_lv_use(CONF_IMAGE)
for use in df.get_lv_uses():
df.add_define(f"LV_USE_{use.upper()}")
cg.add_define(f"USE_LVGL_{use.upper()}")
+14 -14
View File
@@ -9,13 +9,13 @@ CONF_IF_NAN = "if_nan"
# noqa
f_regex = re.compile(
r"""
( # start of capture group 1
% # literal "%"
[-+0 #]{0,5} # optional flags
(?:\d+|\*)? # width
(?:\.(?:\d+|\*))? # precision
(?:h|l|ll|w|I|I32|I64)? # size
f # type
( # start of capture group 1
% # literal "%"
[-+0 #]{0,5} # optional flags
(?:\d+|\*)? # width
(?:\.(?:\d+|\*))? # precision
(?:hh|h|ll|l|j|z|t|L|w|I|I32|I64)? # size
f # type
)
""",
flags=re.VERBOSE,
@@ -23,13 +23,13 @@ f_regex = re.compile(
# noqa
c_regex = re.compile(
r"""
( # start of capture group 1
% # literal "%"
[-+0 #]{0,5} # optional flags
(?:\d+|\*)? # width
(?:\.(?:\d+|\*))? # precision
(?:h|l|ll|w|I|I32|I64)? # size
[cCdiouxXeEfgGaAnpsSZ] # type
( # start of capture group 1
% # literal "%"
[-+0 #]{0,5} # optional flags
(?:\d+|\*)? # width
(?:\.(?:\d+|\*))? # precision
(?:hh|h|ll|l|j|z|t|L|w|I|I32|I64)? # size
[cCdiouxXeEfgGaAnpsSZ] # type
)
""",
flags=re.VERBOSE,
+6 -4
View File
@@ -74,11 +74,11 @@ inline void lv_style_set_text_font(lv_style_t *style, const font::Font *font) {
lv_style_set_text_font(style, font->get_lv_font());
}
#endif
#if defined(USE_LVGL_IMAGE) && defined(USE_IMAGE)
#if LV_USE_IMAGE
#ifdef USE_IMAGE
#ifdef USE_LVGL_IMAGE
// Shortcut / overload, so that the source of an image widget can easily be updated from within a lambda.
inline void lv_image_set_src(lv_obj_t *obj, image::Image *image) { ::lv_image_set_src(obj, image->get_lv_image_dsc()); }
#endif // LV_USE_IMAGE
inline void lv_obj_set_style_bitmap_mask_src(lv_obj_t *obj, image::Image *image, lv_style_selector_t selector) {
::lv_obj_set_style_bitmap_mask_src(obj, image->get_lv_image_dsc(), selector);
@@ -93,7 +93,8 @@ inline void lv_style_set_bg_image_src(lv_style_t *style, image::Image *image) {
inline void lv_style_set_bitmap_mask_src(lv_style_t *style, image::Image *image) {
::lv_style_set_bitmap_mask_src(style, image->get_lv_image_dsc());
}
#endif // USE_LVGL_IMAGE
#endif
#ifdef USE_LVGL_ANIMIMG
inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images) {
auto *dsc = static_cast<std::vector<lv_image_dsc_t *> *>(lv_obj_get_user_data(img));
@@ -109,6 +110,7 @@ inline void lv_animimg_set_src(lv_obj_t *img, std::vector<image::Image *> images
lv_animimg_set_src(img, (const void **) dsc->data(), dsc->size());
}
#endif // USE_LVGL_ANIMIMG
#endif // USE_IMAGE
#ifdef USE_LVGL_METER
int16_t lv_get_needle_angle_for_value(lv_obj_t *obj, int32_t value);
+2
View File
@@ -9,6 +9,7 @@ from .defines import (
CONF_THEME,
LValidator,
add_lv_use,
get_styles_used,
get_theme_widget_map,
literal,
)
@@ -25,6 +26,7 @@ def has_style_props(config) -> bool:
async def style_set(svar, style):
for prop, validator in ALL_STYLES.items():
if (value := style.get(prop)) is not None:
get_styles_used().add(prop)
if isinstance(validator, LValidator):
value = await validator.process(value)
if isinstance(value, list):
+2 -2
View File
@@ -130,8 +130,8 @@ ClimateTraits AirConditioner::traits() {
void AirConditioner::dump_config() {
ESP_LOGCONFIG(Constants::TAG,
"MideaDongle:\n"
" [x] Period: %dms\n"
" [x] Response timeout: %dms\n"
" [x] Period: %" PRIu32 "ms\n"
" [x] Response timeout: %" PRIu32 "ms\n"
" [x] Request attempts: %d",
this->base_.getPeriod(), this->base_.getTimeout(), this->base_.getNumAttempts());
#ifdef USE_REMOTE_TRANSMITTER
@@ -210,7 +210,7 @@ OTAResponseTypes IDFOTABackend::update_partition_table() {
ESP_LOGE(TAG, "Cannot resolve running app partition at address 0x%" PRIX32, running_app_offset);
return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE;
}
ESP_LOGD(TAG, "Copying running app from 0x%X to 0x%X (size: 0x%X)", running_app_part->address,
ESP_LOGD(TAG, "Copying running app from 0x%" PRIX32 " to 0x%" PRIX32 " (size: 0x%zX)", running_app_part->address,
plan.copy_dest_part->address, running_app_size);
err = esp_partition_copy(plan.copy_dest_part, 0, running_app_part, 0, running_app_size);
if (err != ESP_OK) {
@@ -261,7 +261,7 @@ OTAResponseTypes IDFOTABackend::update_partition_table() {
ESP_LOGE(TAG, "Selected app partition not found after partition table update");
return OTA_RESPONSE_ERROR_PARTITION_TABLE_UPDATE;
}
ESP_LOGD(TAG, "Setting next boot partition to 0x%X", new_boot_partition->address);
ESP_LOGD(TAG, "Setting next boot partition to 0x%" PRIX32, new_boot_partition->address);
err = esp_ota_set_boot_partition(new_boot_partition);
if (err != ESP_OK) {
ESP_LOGE(TAG, "esp_ota_set_boot_partition failed (err=0x%X)", err);
@@ -150,7 +150,7 @@ void IRAM_ATTR PulseMeterSensor::edge_intr(PulseMeterSensor *sensor) {
edge_state.last_sent_edge_us_ = now;
state.last_detected_edge_us_ = now;
state.last_rising_edge_us_ = now;
state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
state.count_ += 1;
}
// This ISR is bound to rising edges, so the pin is high
@@ -173,7 +173,7 @@ void IRAM_ATTR PulseMeterSensor::pulse_intr(PulseMeterSensor *sensor) {
} else if (length && !pulse_state.latched_ && sensor->last_pin_val_) { // Long enough high edge
pulse_state.latched_ = true;
state.last_detected_edge_us_ = pulse_state.last_intr_;
state.count_++; // NOLINT(clang-diagnostic-deprecated-volatile)
state.count_ += 1;
}
// Due to order of operations this includes
@@ -78,10 +78,10 @@ void RemoteReceiverComponent::setup() {
void RemoteReceiverComponent::dump_config() {
ESP_LOGCONFIG(TAG,
"Remote Receiver:\n"
" Buffer Size: %u\n"
" Tolerance: %u%s\n"
" Filter out pulses shorter than: %u us\n"
" Signal is done after %u us of no changes",
" Buffer Size: %" PRIu32 "\n"
" Tolerance: %" PRIu32 "%s\n"
" Filter out pulses shorter than: %" PRIu32 " us\n"
" Signal is done after %" PRIu32 " us of no changes",
this->buffer_size_, this->tolerance_,
(this->tolerance_mode_ == remote_base::TOLERANCE_MODE_TIME) ? " us" : "%", this->filter_us_,
this->idle_us_);
+2 -2
View File
@@ -153,7 +153,7 @@ bool SendspinHub::save_last_server_hash(uint32_t hash) {
LastPlayedServerPref pref{.server_id_hash = hash};
bool ok = this->last_played_server_pref_.save(&pref);
if (ok) {
ESP_LOGD(TAG, "Persisted last played server hash: 0x%08X", hash);
ESP_LOGD(TAG, "Persisted last played server hash: 0x%08" PRIX32, hash);
} else {
ESP_LOGW(TAG, "Failed to persist last played server hash");
}
@@ -164,7 +164,7 @@ bool SendspinHub::save_last_server_hash(uint32_t hash) {
std::optional<uint32_t> SendspinHub::load_last_server_hash() {
LastPlayedServerPref pref{};
if (this->last_played_server_pref_.load(&pref)) {
ESP_LOGI(TAG, "Loaded last played server hash: 0x%08X", pref.server_id_hash);
ESP_LOGI(TAG, "Loaded last played server hash: 0x%08" PRIX32, pref.server_id_hash);
return pref.server_id_hash;
}
return std::nullopt;
+1 -1
View File
@@ -126,7 +126,7 @@ void Sim800LComponent::parse_cmd_(std::string message) {
break;
}
// Else fall thru ...
[[fallthrough]];
}
case STATE_CHECK_SMS:
send_cmd_("AT+CMGL=\"ALL\"");
+30 -28
View File
@@ -11,7 +11,7 @@ namespace esphome::sound_level {
static const char *const TAG = "sound_level";
static const uint32_t AUDIO_BUFFER_DURATION_MS = 30;
static const uint32_t MAX_FILL_DURATION_MS = 30;
static const uint32_t RING_BUFFER_DURATION_MS = 120;
// Square INT16_MIN since INT16_MIN^2 > INT16_MAX^2
@@ -30,8 +30,7 @@ void SoundLevelComponent::dump_config() {
void SoundLevelComponent::setup() {
this->microphone_source_->add_data_callback([this](const std::vector<uint8_t> &data) {
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = this->ring_buffer_.lock();
if (this->ring_buffer_.use_count() == 2) {
// ``audio_buffer_`` and ``temp_ring_buffer`` share ownership of a ring buffer, so its safe/useful to write
if (temp_ring_buffer != nullptr) {
temp_ring_buffer->write((void *) data.data(), data.size());
}
});
@@ -81,10 +80,11 @@ void SoundLevelComponent::loop() {
return;
}
// Copy data from ring buffer into the transfer buffer - don't block to avoid slowing the main loop
this->audio_buffer_->transfer_data_from_source(0);
// Expose a chunk of the ring buffer's internal storage - don't block to avoid slowing the main loop.
// pre_shift is ignored by RingBufferAudioSource (no intermediate transfer buffer to compact).
this->audio_source_->fill(0, false);
if (this->audio_buffer_->available() == 0) {
if (this->audio_source_->available() == 0) {
// No new audio available for processing
return;
}
@@ -92,11 +92,11 @@ void SoundLevelComponent::loop() {
const uint32_t samples_in_window =
this->microphone_source_->get_audio_stream_info().ms_to_samples(this->measurement_duration_ms_);
const uint32_t samples_available_to_process =
this->microphone_source_->get_audio_stream_info().bytes_to_samples(this->audio_buffer_->available());
this->microphone_source_->get_audio_stream_info().bytes_to_samples(this->audio_source_->available());
const uint32_t samples_to_process = std::min(samples_in_window - this->sample_count_, samples_available_to_process);
// MicrophoneSource always provides int16 samples due to Python codegen settings
const int16_t *audio_data = reinterpret_cast<const int16_t *>(this->audio_buffer_->get_buffer_start());
const int16_t *audio_data = reinterpret_cast<const int16_t *>(this->audio_source_->data());
// Process all the new audio samples
for (uint32_t i = 0; i < samples_to_process; ++i) {
@@ -115,9 +115,8 @@ void SoundLevelComponent::loop() {
++this->sample_count_;
}
// Remove the processed samples from ``audio_buffer_``
this->audio_buffer_->decrease_buffer_length(
this->microphone_source_->get_audio_stream_info().samples_to_bytes(samples_to_process));
// Remove the processed samples from ``audio_source_``
this->audio_source_->consume(this->microphone_source_->get_audio_stream_info().samples_to_bytes(samples_to_process));
if (this->sample_count_ == samples_in_window) {
// Processed enough samples for the measurement window, compute and publish the sensor values
@@ -158,36 +157,39 @@ void SoundLevelComponent::stop() {
}
bool SoundLevelComponent::start_() {
if (this->audio_buffer_ != nullptr) {
if (this->audio_source_ != nullptr) {
return true;
}
// Allocate a transfer buffer
this->audio_buffer_ = audio::AudioSourceTransferBuffer::create(
this->microphone_source_->get_audio_stream_info().ms_to_bytes(AUDIO_BUFFER_DURATION_MS));
if (this->audio_buffer_ == nullptr) {
this->status_momentary_error("transfer_buffer", 15000);
const auto &stream_info = this->microphone_source_->get_audio_stream_info();
const size_t bytes_per_frame = stream_info.frames_to_bytes(1);
// Allocate a ring buffer for the microphone callback to write into. Round the size down to a multiple
// of bytes_per_frame so the wrap boundary stays frame-aligned and avoids unnecessary single-frame splices.
this->ring_buffer_.reset(); // Reset pointer to any previous ring buffer allocation
const size_t ring_buffer_size =
(stream_info.ms_to_bytes(RING_BUFFER_DURATION_MS) / bytes_per_frame) * bytes_per_frame;
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(ring_buffer_size);
if (temp_ring_buffer == nullptr) {
this->status_momentary_error("ring_buffer", 15000);
return false;
}
// Allocates a new ring buffer, adds it as a source for the transfer buffer, and points ring_buffer_ to it
this->ring_buffer_.reset(); // Reset pointer to any previous ring buffer allocation
std::shared_ptr<ring_buffer::RingBuffer> temp_ring_buffer = ring_buffer::RingBuffer::create(
this->microphone_source_->get_audio_stream_info().ms_to_bytes(RING_BUFFER_DURATION_MS));
if (temp_ring_buffer.use_count() == 0) {
this->status_momentary_error("ring_buffer", 15000);
this->stop_();
// Zero-copy source that reads directly from the ring buffer's internal storage. Frame-aligned reads
// ensure multi-channel frames are never split across the ring buffer's wrap boundary.
this->audio_source_ = audio::RingBufferAudioSource::create(
temp_ring_buffer, stream_info.ms_to_bytes(MAX_FILL_DURATION_MS), static_cast<uint8_t>(bytes_per_frame));
if (this->audio_source_ == nullptr) {
this->status_momentary_error("audio_source", 15000);
return false;
} else {
this->ring_buffer_ = temp_ring_buffer;
this->audio_buffer_->set_source(temp_ring_buffer);
}
this->ring_buffer_ = temp_ring_buffer;
this->status_clear_error();
return true;
}
void SoundLevelComponent::stop_() { this->audio_buffer_.reset(); }
void SoundLevelComponent::stop_() { this->audio_source_.reset(); }
} // namespace esphome::sound_level
+5 -4
View File
@@ -36,11 +36,12 @@ class SoundLevelComponent : public Component {
void stop();
protected:
/// @brief Internal start command that, if necessary, allocates ``audio_buffer_`` and a ring buffer which
/// ``audio_buffer_`` owns and ``ring_buffer_`` points to. Returns true if allocations were successful.
/// @brief Internal start command that, if necessary, allocates a ring buffer and a zero-copy
/// ``RingBufferAudioSource`` that reads directly from it. ``ring_buffer_`` weakly references the
/// ring buffer owned by ``audio_source_``. Returns true if allocations were successful.
bool start_();
/// @brief Internal stop command the deallocates ``audio_buffer_`` (which automatically deallocates its ring buffer)
/// @brief Internal stop command that deallocates ``audio_source_`` (which releases its ring buffer)
void stop_();
microphone::MicrophoneSource *microphone_source_{nullptr};
@@ -48,7 +49,7 @@ class SoundLevelComponent : public Component {
sensor::Sensor *peak_sensor_{nullptr};
sensor::Sensor *rms_sensor_{nullptr};
std::unique_ptr<audio::AudioSourceTransferBuffer> audio_buffer_;
std::unique_ptr<audio::RingBufferAudioSource> audio_source_;
std::weak_ptr<ring_buffer::RingBuffer> ring_buffer_;
int32_t squared_peak_{0};
+32 -1
View File
@@ -1,3 +1,4 @@
from esphome import final_validate as fv
import esphome.codegen as cg
from esphome.components import esp32
from esphome.components.esp32 import (
@@ -8,7 +9,7 @@ from esphome.components.esp32 import (
add_idf_sdkconfig_option,
)
import esphome.config_validation as cv
from esphome.const import CONF_ID
from esphome.const import CONF_HARDWARE_UART, CONF_ID
CODEOWNERS = ["@kbx81"]
CONFLICTS_WITH = ["usb_host"]
@@ -20,6 +21,13 @@ CONF_USB_PRODUCT_STR = "usb_product_str"
CONF_USB_SERIAL_STR = "usb_serial_str"
CONF_USB_VENDOR_ID = "usb_vendor_id"
# Components that provide a USB device class (CDC, HID, MSC, ...) on top of
# tinyusb. Configuring `tinyusb:` without any of these triggers a 5s hang in
# esp_tinyusb's driver install (descriptors_set fails with no class and no
# user-provided full_speed_config), which trips the task watchdog before
# loop() ever runs.
_USB_CLASS_COMPONENTS = ("usb_cdc_acm",)
tinyusb_ns = cg.esphome_ns.namespace("tinyusb")
TinyUSB = tinyusb_ns.class_("TinyUSB", cg.Component)
@@ -41,6 +49,29 @@ CONFIG_SCHEMA = cv.All(
)
def _final_validate(config):
full_config = fv.full_config.get()
if not any(name in full_config for name in _USB_CLASS_COMPONENTS):
raise cv.Invalid(
"The 'tinyusb' component requires at least one USB class component"
)
# tinyusb owns the USB OTG peripheral. The logger's USB_CDC backend routes
# the ROM console through that same peripheral, so the two cannot coexist.
# (USB_SERIAL_JTAG is a separate peripheral and is fine alongside tinyusb.)
logger_config = full_config.get("logger")
if logger_config and logger_config.get(CONF_HARDWARE_UART) == "USB_CDC":
raise cv.Invalid(
"'tinyusb' cannot be used with 'logger.hardware_uart: USB_CDC' "
"because both share the USB OTG peripheral. Set "
"'logger.hardware_uart' to a hardware UART (e.g. UART0), or to "
"USB_SERIAL_JTAG on variants that support it (ESP32-S3, ESP32-P4)"
)
return config
FINAL_VALIDATE_SCHEMA = _final_validate
async def to_code(config):
var = cg.new_Pvariable(config[CONF_ID])
await cg.register_component(var, config)
@@ -26,6 +26,21 @@ void TinyUSB::setup() {
.string_count = SIZE,
};
// Defense-in-depth: esp_tinyusb's tinyusb_descriptors_set() fails with
// ESP_ERR_INVALID_ARG when no configuration descriptor is provided and
// no class that has a built-in default (CDC/MSC/NCM) is compiled in. In
// that case the internal task exits without notifying us, and
// tinyusb_driver_install() blocks 5s on the notify-take -- long enough
// to trip the task watchdog. Bail early so the rest of the device can
// still boot.
#if !(CFG_TUD_CDC > 0 || CFG_TUD_MSC > 0 || CFG_TUD_NCM > 0)
if (this->tusb_cfg_.descriptor.full_speed_config == nullptr) {
ESP_LOGE(TAG, "No USB class configured");
this->mark_failed();
return;
}
#endif
esp_err_t result = tinyusb_driver_install(&this->tusb_cfg_);
if (result != ESP_OK) {
ESP_LOGE(TAG, "tinyusb_driver_install failed: %s", esp_err_to_name(result));
@@ -72,7 +72,7 @@ void TotalDailyEnergy::schedule_midnight_reset_() {
timeout_seconds = seconds_until_midnight + 1;
}
ESP_LOGD(TAG, "Scheduling midnight check in %us", timeout_seconds);
ESP_LOGD(TAG, "Scheduling midnight check in %" PRIu32 "s", timeout_seconds);
this->set_timeout(TIMEOUT_ID_MIDNIGHT, timeout_seconds * MILLIS_PER_SECOND,
[this]() { this->schedule_midnight_reset_(); });
}
+2
View File
@@ -684,8 +684,10 @@ void Tuya::set_numeric_datapoint_value_(uint8_t datapoint_id, TuyaDatapointType
case 4:
data.push_back(value >> 24);
data.push_back(value >> 16);
[[fallthrough]];
case 2:
data.push_back(value >> 8);
[[fallthrough]];
case 1:
data.push_back(value >> 0);
break;
+3 -3
View File
@@ -135,7 +135,7 @@ void Tx20Component::decode_and_publish_() {
}
if (tx20_se == tx20_sb) {
tx20_wind_direction = tx20_se;
if (tx20_wind_direction >= 0 && tx20_wind_direction < 16) {
if (tx20_wind_direction < 16) {
wind_cardinal_direction_ = DIRECTIONS[tx20_wind_direction];
}
ESP_LOGV(TAG, "WindDirection %d", tx20_wind_direction);
@@ -164,7 +164,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) {
}
arg->buffer[arg->buffer_index] = 1;
arg->start_time = now;
arg->buffer_index++; // NOLINT(clang-diagnostic-deprecated-volatile)
arg->buffer_index += 1;
return;
}
const uint32_t delay = now - arg->start_time;
@@ -195,7 +195,7 @@ void IRAM_ATTR Tx20ComponentStore::gpio_intr(Tx20ComponentStore *arg) {
}
arg->spent_time += delay;
arg->start_time = now;
arg->buffer_index++; // NOLINT(clang-diagnostic-deprecated-volatile)
arg->buffer_index += 1;
}
void IRAM_ATTR Tx20ComponentStore::reset() {
tx20_available = false;
+1 -1
View File
@@ -135,7 +135,7 @@ class USBUartChannel : public uart::UARTComponent, public Parented<USBUartCompon
// Computed as ceil(buffer_size / 64) + 1 in Python codegen; defaults to 5 (256 / 64 + 1).
static constexpr uint8_t USB_OUTPUT_CHUNK_COUNT = USB_UART_OUTPUT_CHUNK_COUNT;
USBUartChannel(uint8_t index, uint16_t buffer_size) : index_(index), input_buffer_(RingBuffer(buffer_size)) {}
USBUartChannel(uint8_t index, uint16_t buffer_size) : input_buffer_(RingBuffer(buffer_size)), index_(index) {}
void write_array(const uint8_t *data, size_t len) override;
bool peek_byte(uint8_t *data) override;
bool read_array(uint8_t *data, size_t len) override;
+1 -1
View File
@@ -611,7 +611,7 @@ static void set_json_icon_state_value(JsonObject &root, EntityBase *obj, const c
}
// Helper to get request detail parameter
static JsonDetail get_request_detail(AsyncWebServerRequest *request) {
[[maybe_unused]] static JsonDetail get_request_detail(AsyncWebServerRequest *request) {
return request->arg(ESPHOME_F("detail")) == "all" ? DETAIL_ALL : DETAIL_STATE;
}
@@ -66,7 +66,7 @@ namespace {
* - HTTPD_SOCK_ERR_TIMEOUT if the send buffer is full (EAGAIN/EWOULDBLOCK).
* - HTTPD_SOCK_ERR_FAIL for other errors.
*/
int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) {
[[maybe_unused]] int nonblocking_send(httpd_handle_t hd, int sockfd, const char *buf, size_t buf_len, int flags) {
if (buf == nullptr) {
return HTTPD_SOCK_ERR_INVALID;
}
+2 -2
View File
@@ -11,7 +11,7 @@ static const char *const KEYS = "0123456789*#";
void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) {
if (arg->d0.digital_read())
return;
arg->count++; // NOLINT(clang-diagnostic-deprecated-volatile)
arg->count += 1;
arg->value <<= 1;
arg->last_bit_time = millis();
arg->done = false;
@@ -20,7 +20,7 @@ void IRAM_ATTR HOT WiegandStore::d0_gpio_intr(WiegandStore *arg) {
void IRAM_ATTR HOT WiegandStore::d1_gpio_intr(WiegandStore *arg) {
if (arg->d1.digital_read())
return;
arg->count++; // NOLINT(clang-diagnostic-deprecated-volatile)
arg->count += 1;
arg->value = (arg->value << 1) | 1;
arg->last_bit_time = millis();
arg->done = false;
+51 -1
View File
@@ -54,10 +54,18 @@ from esphome.const import (
CONF_TTLS_PHASE_2,
CONF_USE_ADDRESS,
CONF_USERNAME,
CONF_WIFI,
PLACEHOLDER_WIFI_SSID,
Platform,
PlatformFramework,
)
from esphome.core import CORE, CoroPriority, HexInt, coroutine_with_priority
from esphome.core import (
CORE,
CoroPriority,
EsphomeError,
HexInt,
coroutine_with_priority,
)
import esphome.final_validate as fv
from esphome.types import ConfigType
@@ -903,3 +911,45 @@ FILTER_SOURCE_FILES = filter_source_files_from_platform(
"wifi_component_pico_w.cpp": {PlatformFramework.RP2040_ARDUINO},
}
)
def _placeholder_wifi_credentials(config: ConfigType) -> list[str]:
"""Return human-readable locations where the dashboard's placeholder wifi
values still appear. Empty list means no placeholders were found.
"""
placeholders: list[str] = []
wifi_conf = config.get(CONF_WIFI)
if not wifi_conf:
return placeholders
for idx, network in enumerate(wifi_conf.get(CONF_NETWORKS, [])):
ssid = network.get(CONF_SSID)
if isinstance(ssid, str) and ssid == PLACEHOLDER_WIFI_SSID:
placeholders.append(f"wifi.networks[{idx}].ssid")
ap_conf = wifi_conf.get(CONF_AP)
if ap_conf:
ap_ssid = ap_conf.get(CONF_SSID)
if isinstance(ap_ssid, str) and ap_ssid == PLACEHOLDER_WIFI_SSID:
placeholders.append("wifi.ap.ssid")
return placeholders
def check_placeholder_credentials(config: ConfigType) -> None:
"""Raise EsphomeError if any wifi credential is the dashboard placeholder.
Call only at compile time. NEVER from CONFIG_SCHEMA, FINAL_VALIDATE_SCHEMA,
or any path reached by `esphome config`; device-builder relies on
validation passing with the placeholders still in place.
"""
locations = _placeholder_wifi_credentials(config)
if not locations:
return
formatted = ", ".join(locations)
raise EsphomeError(
f"wifi configuration still contains the dashboard placeholder value "
f"'{PLACEHOLDER_WIFI_SSID}' at: {formatted}. "
f"Open secrets.yaml and replace 'wifi_ssid' (and 'wifi_password') "
f"with your real wifi credentials before flashing."
)
+10 -1
View File
@@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2026.5.0b1"
__version__ = "2026.5.0b2"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (
@@ -1415,3 +1415,12 @@ ENTITY_CATEGORY_DIAGNOSTIC = "diagnostic"
# The corresponding constant exists in c++
# when update_interval is set to never, it becomes SCHEDULER_DONT_RUN milliseconds
SCHEDULER_DONT_RUN = 4294967295
# Sentinel values written by the esphome-device-builder dashboard into
# secrets.yaml on first boot so that !secret wifi_ssid / !secret wifi_password
# references resolve cleanly through validation before the user has finished
# the onboarding wizard. Compilation refuses if these reach the binary so that
# a user who dismisses onboarding can't accidentally flash a device that will
# never associate with their wifi.
PLACEHOLDER_WIFI_SSID = "REPLACE_WITH_YOUR_WIFI_NETWORK"
PLACEHOLDER_WIFI_PASSWORD = "REPLACE_WITH_YOUR_WIFI_PASSWORD" # noqa: S105
+1 -1
View File
@@ -69,7 +69,7 @@ ESPHOME_IDF_DEFAULT_FEATURES = _str_to_lst_of_str(
ESPHOME_IDF_FRAMEWORK_MIRRORS = _str_to_lst_of_str(
os.environ.get(
"ESPHOME_IDF_FRAMEWORK_MIRRORS",
"https://github.com/espressif/esp-idf/releases/download/v{VERSION}/esp-idf-v{VERSION}.zip;https://github.com/espressif/esp-idf/releases/download/v{MAJOR}.{MINOR}/esp-idf-v{MAJOR}.{MINOR}.zip",
"https://github.com/esphome-libs/esp-idf/releases/download/v{VERSION}/esp-idf-v{VERSION}.tar.xz;https://github.com/esphome-libs/esp-idf/releases/download/v{MAJOR}.{MINOR}/esp-idf-v{MAJOR}.{MINOR}.tar.xz",
)
)
+30 -23
View File
@@ -191,17 +191,38 @@ def run_reconfigure() -> int:
def has_outdated_files():
"""Check if the build configuration is stale.
Returns True if required build files are missing or if configuration inputs
are newer than the generated CMake/Ninja build artifacts.
Returns True if required build files are missing or if ESPHome's
resolved build inputs are newer than CMakeCache.txt:
- ``sdkconfig.<name>.esphomeinternal`` -- the canonical "what state
did ESPHome resolve the YAML to" snapshot. Any change in build
flags, enabled components, framework version, or target ends up
rewriting it (we embed a ``# ESPHOME_IDF_VERSION=`` comment line
for the version case where the option set would otherwise be
identical).
- ``src/idf_component.yml`` -- the project manifest. Managed
component additions/removals (e.g. via ``add_idf_component``) can
happen without any sdkconfig impact, and ``_write_idf_component_yml``
already deletes ``dependencies.lock`` on a change but that signal
gets lost as soon as the lock is missing.
We deliberately don't watch:
- The top-level/src ``CMakeLists.txt`` -- ESPHome owns those, and
ninja already tracks them as configure-time deps. Including them
causes a perpetual reconfigure loop because CMake doesn't restamp
``CMakeCache.txt`` when only ``idf_build_set_property`` values
change between configures.
- ``$IDF_PATH`` and CMake's ``build/config/`` -- both have mtime
semantics that fire after the wrong configure (or not at all in
common cases like in-place IDF version replacement). The sdkconfig
and manifest hashes subsume the meaningful signal.
"""
cmakecache_txt_path = CORE.relative_build_path("build/CMakeCache.txt")
cmakelists_txt_build_path = CORE.relative_build_path("CMakeLists.txt")
cmakelists_txt_src_path = CORE.relative_src_path("CMakeLists.txt")
build_config_path = CORE.relative_build_path("build/config")
sdkconfig_internal_path = CORE.relative_build_path(
f"sdkconfig.{CORE.name}.esphomeinternal"
)
idf_component_yml_path = CORE.relative_build_path("src/idf_component.yml")
dependency_lock_path = CORE.relative_build_path("dependencies.lock")
build_ninja_path = CORE.relative_build_path("build/build.ninja")
@@ -219,14 +240,8 @@ def has_outdated_files():
cmakecache_txt_mtime = os.path.getmtime(cmakecache_txt_path)
return any(
os.path.getmtime(f) > cmakecache_txt_mtime
for f in [
_get_idf_path(),
cmakelists_txt_build_path,
cmakelists_txt_src_path,
sdkconfig_internal_path,
build_config_path,
]
if f and os.path.exists(f)
for f in [sdkconfig_internal_path, idf_component_yml_path]
if f.exists()
)
@@ -302,21 +317,13 @@ def run_compile(config, verbose: bool) -> int:
return rc
_LOGGER.info("Regenerating CMakeLists.txt with discovered components...")
write_project(minimal=False)
# The post-discovery rewrite leaves CMakeLists newer than
# CMakeCache.txt. CMake won't re-touch CMakeCache.txt on a
# configure that only changes idf_build_set_property values
# (those aren't cache variables), so has_outdated_files() would
# return True on every subsequent build, perpetually retriggering
# the two-pass. Touch CMakeCache.txt now so its mtime stays past
# the rewritten CMakeLists.
cmakecache = CORE.relative_build_path("build/CMakeCache.txt")
if cmakecache.is_file():
os.utime(cmakecache)
if CORE.testing_mode:
# Reconfigure again so cmake is up to date with the full
# component list before the build's idf.py invocation runs --
# idf.py build would otherwise re-run cmake and regenerate
# memory.ld, wiping the DRAM/IRAM patches applied below.
# Outside testing mode ninja's own configure-time dep on
# CMakeLists.txt handles the re-run as part of the build step.
rc = run_reconfigure()
if rc != 0:
_LOGGER.error("Reconfigure with discovered components failed")
+2 -2
View File
@@ -10,7 +10,7 @@ dependencies:
esphome/micro-flac:
version: 0.2.0
esphome/micro-mp3:
version: 0.2.0
version: 0.2.1
esphome/micro-opus:
version: 0.4.1
esphome/micro-wav:
@@ -36,7 +36,7 @@ dependencies:
rules:
- if: "target in [esp32h2, esp32p4]"
espressif/esp_hosted:
version: 2.12.6
version: 2.12.7
rules:
- if: "target in [esp32h2, esp32p4]"
zorxx/multipart-parser:
+12 -1
View File
@@ -273,10 +273,21 @@ class StorageJSON:
"""
CORE.name = self.name
CORE.build_path = self.build_path
target_platform = self.core_platform or self.target_platform.lower()
CORE.data[KEY_CORE] = {
KEY_TARGET_PLATFORM: self.core_platform or self.target_platform.lower(),
KEY_TARGET_PLATFORM: target_platform,
KEY_TARGET_FRAMEWORK: self.framework,
}
# The compile pipeline populates CORE.data[KEY_ESP32] when esp32's
# validator runs; on the cache fast path that validator is skipped,
# so populate the variant upload_using_esptool reads via
# esp32.get_esp32_variant(). target_platform on disk is the variant
# (e.g. "ESP32S3"); core_platform is the family (e.g. "esp32").
if target_platform == const.PLATFORM_ESP32:
from esphome.components.esp32.const import KEY_ESP32
from esphome.const import KEY_VARIANT
CORE.data[KEY_ESP32] = {KEY_VARIANT: self.target_platform}
def __eq__(self, o) -> bool:
return isinstance(o, StorageJSON) and self.as_dict() == o.as_dict()
+2 -2
View File
@@ -12,8 +12,8 @@ platformio==6.1.19
esptool==5.2.0
click==8.3.3
esphome-dashboard==20260425.0
aioesphomeapi==45.0.0
zeroconf==0.148.0
aioesphomeapi==45.0.3
zeroconf==0.149.3
puremagic==1.30
ruamel.yaml==0.19.1 # dashboard_import
ruamel.yaml.clib==0.2.15 # dashboard_import
@@ -29,12 +29,12 @@ esp32_ble_tracker:
- service_uuid: ABCD
then:
- lambda: !lambda |-
ESP_LOGD("main", "Length of service data is %i", x.size());
ESP_LOGD("main", "Length of service data is %zu", x.size());
on_ble_manufacturer_data_advertise:
- manufacturer_id: ABCD
then:
- lambda: !lambda |-
ESP_LOGD("main", "Length of manufacturer data is %i", x.size());
ESP_LOGD("main", "Length of manufacturer data is %zu", x.size());
on_scan_end:
- then:
- lambda: |-
+1 -1
View File
@@ -123,7 +123,7 @@ select:
- lambda: |-
id(uart_bus).flush();
uint32_t new_baud_rate = stoi(x);
ESP_LOGD("change_baud_rate", "Changing baud rate from %i to %i",id(uart_bus).get_baud_rate(), new_baud_rate);
ESP_LOGD("change_baud_rate", "Changing baud rate from %" PRIu32 " to %" PRIu32, id(uart_bus).get_baud_rate(), new_baud_rate);
if (id(uart_bus).get_baud_rate() != new_baud_rate) {
id(uart_bus).set_baud_rate(new_baud_rate);
#if defined(USE_ESP8266) || defined(USE_ESP32)
+3 -3
View File
@@ -660,13 +660,13 @@ lvgl:
on_release:
logger.log:
format: Button released at %d/%d
args: [point.x, point.y]
args: ['(int) point.x', '(int) point.y']
on_long_press_repeat:
logger.log: Button clicked
on_pressing:
logger.log:
format: Button pressing at %d/%d
args: [point.x, point.y]
args: ['(int) point.x', '(int) point.y']
on_press_lost:
logger.log: Button press lost
on_single_click:
@@ -944,7 +944,7 @@ lvgl:
on_release:
logger.log:
format: Slider released at %d/%d with value %.0f
args: [point.x, point.y, x]
args: ['(int) point.x', '(int) point.y', x]
- button:
styles: spin_button
id: spin_up
+1 -1
View File
@@ -21,7 +21,7 @@ modbus_server:
read_lambda: |-
return 31;
write_lambda: |-
printf("address=%d, value=%d", x);
printf("address=%d, value=%" PRId32 "\n", (int) address, x);
return true;
- id: modbus_server4
modbus_id: mod_bus2
+1 -1
View File
@@ -64,7 +64,7 @@ mqtt:
topic: some/topic
payload: Good-bye
- lambda: |-
ESP_LOGD("MQTT", "Disconnect reason %d", reason);
ESP_LOGD("MQTT", "Disconnect reason %d", (int) reason);
publish_nan_as_none: false
binary_sensor:

Some files were not shown because too many files have changed in this diff Show More