Merge pull request #9796 from esphome/bump-2025.7.3

2025.7.3
This commit is contained in:
Jesse Hills 2025-07-23 08:09:40 +12:00 committed by GitHub
commit 2b5cceda58
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 194 additions and 75 deletions

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 = 2025.7.2
PROJECT_NUMBER = 2025.7.3
# 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

View File

@ -1,7 +1,7 @@
import esphome.codegen as cg
from esphome.components import esp32, i2c
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET
from esphome.const import CONF_ID, CONF_SAMPLE_RATE, CONF_TEMPERATURE_OFFSET, Framework
CODEOWNERS = ["@trvrnrth"]
DEPENDENCIES = ["i2c"]
@ -56,7 +56,15 @@ CONFIG_SCHEMA = cv.All(
): cv.positive_time_period_minutes,
}
).extend(i2c.i2c_device_schema(0x76)),
cv.only_with_arduino,
cv.only_with_framework(
frameworks=Framework.ARDUINO,
suggestions={
Framework.ESP_IDF: (
"bme68x_bsec2_i2c",
"sensor/bme68x_bsec2",
)
},
),
cv.Any(
cv.only_on_esp8266,
cv.All(

View File

@ -16,6 +16,8 @@ namespace esp32_touch {
static const char *const TAG = "esp32_touch";
static const uint32_t SETUP_MODE_THRESHOLD = 0xFFFF;
void ESP32TouchComponent::setup() {
// Create queue for touch events
// Queue size calculation: children * 4 allows for burst scenarios where ISR
@ -44,7 +46,11 @@ void ESP32TouchComponent::setup() {
// Configure each touch pad
for (auto *child : this->children_) {
touch_pad_config(child->get_touch_pad(), child->get_threshold());
if (this->setup_mode_) {
touch_pad_config(child->get_touch_pad(), SETUP_MODE_THRESHOLD);
} else {
touch_pad_config(child->get_touch_pad(), child->get_threshold());
}
}
// Register ISR handler
@ -114,8 +120,8 @@ void ESP32TouchComponent::loop() {
child->publish_state(new_state);
// Original ESP32: ISR only fires when touched, release is detected by timeout
// Note: ESP32 v1 uses inverted logic - touched when value < threshold
ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " < threshold: %" PRIu32 ")",
child->get_name().c_str(), event.value, child->get_threshold());
ESP_LOGV(TAG, "Touch Pad '%s' state: %s (value: %" PRIu32 " < threshold: %" PRIu32 ")",
child->get_name().c_str(), ONOFF(new_state), event.value, child->get_threshold());
}
break; // Exit inner loop after processing matching pad
}
@ -188,11 +194,6 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
// as any pad remains touched. This allows us to detect both new touches and
// continued touches, but releases must be detected by timeout in the main loop.
// IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2!
// ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE
// Therefore: touched = (value < threshold)
// This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold)
// Process all configured pads to check their current state
// Note: ESP32 v1 doesn't tell us which specific pad triggered the interrupt,
// so we must scan all configured pads to find which ones were touched
@ -211,11 +212,16 @@ void IRAM_ATTR ESP32TouchComponent::touch_isr_handler(void *arg) {
}
// Skip pads that arent in the trigger mask
bool is_touched = (mask >> pad) & 1;
if (!is_touched) {
if (((mask >> pad) & 1) == 0) {
continue;
}
// IMPORTANT: ESP32 v1 touch detection logic - INVERTED compared to v2!
// ESP32 v1: Touch is detected when capacitance INCREASES, causing the measured value to DECREASE
// Therefore: touched = (value < threshold)
// This is opposite to ESP32-S2/S3 v2 where touched = (value > threshold)
bool is_touched = value < child->get_threshold();
// Always send the current state - the main loop will filter for changes
// We send both touched and untouched states because the ISR doesn't
// track previous state (to keep ISR fast and simple)

View File

@ -2,7 +2,13 @@ from esphome import pins
import esphome.codegen as cg
from esphome.components import fastled_base
import esphome.config_validation as cv
from esphome.const import CONF_CHIPSET, CONF_NUM_LEDS, CONF_PIN, CONF_RGB_ORDER
from esphome.const import (
CONF_CHIPSET,
CONF_NUM_LEDS,
CONF_PIN,
CONF_RGB_ORDER,
Framework,
)
AUTO_LOAD = ["fastled_base"]
@ -48,13 +54,22 @@ CONFIG_SCHEMA = cv.All(
cv.Required(CONF_PIN): pins.internal_gpio_output_pin_number,
}
),
_validate,
cv.only_with_framework(
frameworks=Framework.ARDUINO,
suggestions={
Framework.ESP_IDF: (
"esp32_rmt_led_strip",
"light/esp32_rmt_led_strip",
)
},
),
cv.require_framework_version(
esp8266_arduino=cv.Version(2, 7, 4),
esp32_arduino=cv.Version(99, 0, 0),
max_version=True,
extra_message="Please see note on documentation for FastLED",
),
_validate,
)

View File

@ -9,6 +9,7 @@ from esphome.const import (
CONF_DATA_RATE,
CONF_NUM_LEDS,
CONF_RGB_ORDER,
Framework,
)
AUTO_LOAD = ["fastled_base"]
@ -33,6 +34,15 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_DATA_RATE): cv.frequency,
}
),
cv.only_with_framework(
frameworks=Framework.ARDUINO,
suggestions={
Framework.ESP_IDF: (
"spi_led_strip",
"light/spi_led_strip",
)
},
),
cv.require_framework_version(
esp8266_arduino=cv.Version(2, 7, 4),
esp32_arduino=cv.Version(99, 0, 0),

View File

@ -4,7 +4,13 @@ from esphome import pins
import esphome.codegen as cg
from esphome.components import binary_sensor
import esphome.config_validation as cv
from esphome.const import CONF_ID, CONF_NAME, CONF_NUMBER, CONF_PIN
from esphome.const import (
CONF_ALLOW_OTHER_USES,
CONF_ID,
CONF_NAME,
CONF_NUMBER,
CONF_PIN,
)
from esphome.core import CORE
from .. import gpio_ns
@ -76,6 +82,18 @@ async def to_code(config):
)
use_interrupt = False
# Check if pin is shared with other components (allow_other_uses)
# When a pin is shared, interrupts can interfere with other components
# (e.g., duty_cycle sensor) that need to monitor the pin's state changes
if use_interrupt and config[CONF_PIN].get(CONF_ALLOW_OTHER_USES, False):
_LOGGER.info(
"GPIO binary_sensor '%s': Disabling interrupts because pin %s is shared with other components. "
"The sensor will use polling mode for compatibility with other pin uses.",
config.get(CONF_NAME, config[CONF_ID]),
config[CONF_PIN][CONF_NUMBER],
)
use_interrupt = False
cg.add(var.set_use_interrupt(use_interrupt))
if use_interrupt:
cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE]))

View File

@ -15,6 +15,7 @@ from esphome.const import (
CONF_PIN,
CONF_TYPE,
CONF_VARIANT,
Framework,
)
from esphome.core import CORE
@ -162,7 +163,15 @@ def _validate_method(value):
CONFIG_SCHEMA = cv.All(
cv.only_with_arduino,
cv.only_with_framework(
frameworks=Framework.ARDUINO,
suggestions={
Framework.ESP_IDF: (
"esp32_rmt_led_strip",
"light/esp32_rmt_led_strip",
)
},
),
cv.require_framework_version(
esp8266_arduino=cv.Version(2, 4, 0),
esp32_arduino=cv.Version(0, 0, 0),

View File

@ -48,6 +48,9 @@ void Sdl::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *
}
void Sdl::draw_pixel_at(int x, int y, Color color) {
if (!this->get_clipping().inside(x, y))
return;
SDL_Rect rect{x, y, 1, 1};
auto data = (display::ColorUtil::color_to_565(color, display::COLOR_ORDER_RGB));
SDL_UpdateTexture(this->texture_, &rect, &data, 2);

View File

@ -1,7 +1,7 @@
import esphome.codegen as cg
from esphome.components import fan
import esphome.config_validation as cv
from esphome.const import CONF_OUTPUT_ID, CONF_SPEED_COUNT, CONF_SWITCH_DATAPOINT
from esphome.const import CONF_ID, CONF_SPEED_COUNT, CONF_SWITCH_DATAPOINT
from .. import CONF_TUYA_ID, Tuya, tuya_ns
@ -14,9 +14,9 @@ CONF_DIRECTION_DATAPOINT = "direction_datapoint"
TuyaFan = tuya_ns.class_("TuyaFan", cg.Component, fan.Fan)
CONFIG_SCHEMA = cv.All(
fan.FAN_SCHEMA.extend(
fan.fan_schema(TuyaFan)
.extend(
{
cv.GenerateID(CONF_OUTPUT_ID): cv.declare_id(TuyaFan),
cv.GenerateID(CONF_TUYA_ID): cv.use_id(Tuya),
cv.Optional(CONF_OSCILLATION_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_SPEED_DATAPOINT): cv.uint8_t,
@ -24,7 +24,8 @@ CONFIG_SCHEMA = cv.All(
cv.Optional(CONF_DIRECTION_DATAPOINT): cv.uint8_t,
cv.Optional(CONF_SPEED_COUNT, default=3): cv.int_range(min=1, max=256),
}
).extend(cv.COMPONENT_SCHEMA),
)
.extend(cv.COMPONENT_SCHEMA),
cv.has_at_least_one_key(CONF_SPEED_DATAPOINT, CONF_SWITCH_DATAPOINT),
)
@ -32,7 +33,7 @@ CONFIG_SCHEMA = cv.All(
async def to_code(config):
parent = await cg.get_variable(config[CONF_TUYA_ID])
var = cg.new_Pvariable(config[CONF_OUTPUT_ID], parent, config[CONF_SPEED_COUNT])
var = cg.new_Pvariable(config[CONF_ID], parent, config[CONF_SPEED_COUNT])
await cg.register_component(var, config)
await fan.register_fan(var, config)

View File

@ -76,7 +76,7 @@ void OTARequestHandler::report_ota_progress_(AsyncWebServerRequest *request) {
percentage = (this->ota_read_length_ * 100.0f) / request->contentLength();
ESP_LOGD(TAG, "OTA in progress: %0.1f%%", percentage);
} else {
ESP_LOGD(TAG, "OTA in progress: %u bytes read", this->ota_read_length_);
ESP_LOGD(TAG, "OTA in progress: %" PRIu32 " bytes read", this->ota_read_length_);
}
#ifdef USE_OTA_STATE_CALLBACK
// Report progress - use call_deferred since we're in web server task
@ -171,7 +171,7 @@ void OTARequestHandler::handleUpload(AsyncWebServerRequest *request, const Strin
// Finalize
if (final) {
ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%u, contentLength=%zu", index, len,
ESP_LOGD(TAG, "OTA final chunk: index=%zu, len=%zu, total_read=%" PRIu32 ", contentLength=%zu", index, len,
this->ota_read_length_, request->contentLength());
// For Arduino framework, the Update library tracks expected size from firmware header

View File

@ -73,6 +73,7 @@ from esphome.const import (
TYPE_GIT,
TYPE_LOCAL,
VALID_SUBSTITUTIONS_CHARACTERS,
Framework,
__version__ as ESPHOME_VERSION,
)
from esphome.core import (
@ -282,6 +283,38 @@ class FinalExternalInvalid(Invalid):
"""Represents an invalid value in the final validation phase where the path should not be prepended."""
@dataclass(frozen=True, order=True)
class Version:
major: int
minor: int
patch: int
extra: str = ""
def __str__(self):
return f"{self.major}.{self.minor}.{self.patch}"
@classmethod
def parse(cls, value: str) -> Version:
match = re.match(r"^(\d+).(\d+).(\d+)-?(\w*)$", value)
if match is None:
raise ValueError(f"Not a valid version number {value}")
major = int(match[1])
minor = int(match[2])
patch = int(match[3])
extra = match[4] or ""
return Version(major=major, minor=minor, patch=patch, extra=extra)
@property
def is_beta(self) -> bool:
"""Check if this version is a beta version."""
return self.extra.startswith("b")
@property
def is_dev(self) -> bool:
"""Check if this version is a development version."""
return self.extra.startswith("dev")
def check_not_templatable(value):
if isinstance(value, Lambda):
raise Invalid("This option is not templatable!")
@ -619,16 +652,35 @@ def only_on(platforms):
return validator_
def only_with_framework(frameworks):
def only_with_framework(
frameworks: Framework | str | list[Framework | str], suggestions=None
):
"""Validate that this option can only be specified on the given frameworks."""
if not isinstance(frameworks, list):
frameworks = [frameworks]
frameworks = [Framework(framework) for framework in frameworks]
if suggestions is None:
suggestions = {}
version = Version.parse(ESPHOME_VERSION)
if version.is_beta:
docs_format = "https://beta.esphome.io/components/{path}"
elif version.is_dev:
docs_format = "https://next.esphome.io/components/{path}"
else:
docs_format = "https://esphome.io/components/{path}"
def validator_(obj):
if CORE.target_framework not in frameworks:
raise Invalid(
f"This feature is only available with frameworks {frameworks}"
)
err_str = f"This feature is only available with framework(s) {', '.join([framework.value for framework in frameworks])}"
if suggestion := suggestions.get(CORE.target_framework, None):
(component, docs_path) = suggestion
err_str += f"\nPlease use '{component}'"
if docs_path:
err_str += f": {docs_format.format(path=docs_path)}"
raise Invalid(err_str)
return obj
return validator_
@ -637,8 +689,8 @@ def only_with_framework(frameworks):
only_on_esp32 = only_on(PLATFORM_ESP32)
only_on_esp8266 = only_on(PLATFORM_ESP8266)
only_on_rp2040 = only_on(PLATFORM_RP2040)
only_with_arduino = only_with_framework("arduino")
only_with_esp_idf = only_with_framework("esp-idf")
only_with_arduino = only_with_framework(Framework.ARDUINO)
only_with_esp_idf = only_with_framework(Framework.ESP_IDF)
# Adapted from:
@ -1965,26 +2017,6 @@ def source_refresh(value: str):
return positive_time_period_seconds(value)
@dataclass(frozen=True, order=True)
class Version:
major: int
minor: int
patch: int
def __str__(self):
return f"{self.major}.{self.minor}.{self.patch}"
@classmethod
def parse(cls, value: str) -> Version:
match = re.match(r"^(\d+).(\d+).(\d+)-?\w*$", value)
if match is None:
raise ValueError(f"Not a valid version number {value}")
major = int(match[1])
minor = int(match[2])
patch = int(match[3])
return Version(major=major, minor=minor, patch=patch)
def version_number(value):
value = string_strict(value)
try:

View File

@ -4,7 +4,7 @@ from enum import Enum
from esphome.enum import StrEnum
__version__ = "2025.7.2"
__version__ = "2025.7.3"
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
VALID_SUBSTITUTIONS_CHARACTERS = (

View File

@ -68,8 +68,11 @@ void Application::setup() {
do {
uint8_t new_app_state = STATUS_LED_WARNING;
this->scheduler.call();
this->feed_wdt();
uint32_t now = millis();
// Process pending loop enables to handle GPIO interrupts during setup
this->before_loop_tasks_(now);
for (uint32_t j = 0; j <= i; j++) {
// Update loop_component_start_time_ right before calling each component
this->loop_component_start_time_ = millis();
@ -78,6 +81,8 @@ void Application::setup() {
this->app_state_ |= new_app_state;
this->feed_wdt();
}
this->after_loop_tasks_();
this->app_state_ = new_app_state;
yield();
} while (!component->can_proceed());
@ -94,30 +99,10 @@ void Application::setup() {
void Application::loop() {
uint8_t new_app_state = 0;
this->scheduler.call();
// Get the initial loop time at the start
uint32_t last_op_end_time = millis();
// Feed WDT with time
this->feed_wdt(last_op_end_time);
// Process any pending enable_loop requests from ISRs
// This must be done before marking in_loop_ = true to avoid race conditions
if (this->has_pending_enable_loop_requests_) {
// Clear flag BEFORE processing to avoid race condition
// If ISR sets it during processing, we'll catch it next loop iteration
// This is safe because:
// 1. Each component has its own pending_enable_loop_ flag that we check
// 2. If we can't process a component (wrong state), enable_pending_loops_()
// will set this flag back to true
// 3. Any new ISR requests during processing will set the flag again
this->has_pending_enable_loop_requests_ = false;
this->enable_pending_loops_();
}
// Mark that we're in the loop for safe reentrant modifications
this->in_loop_ = true;
this->before_loop_tasks_(last_op_end_time);
for (this->current_loop_index_ = 0; this->current_loop_index_ < this->looping_components_active_end_;
this->current_loop_index_++) {
@ -138,7 +123,7 @@ void Application::loop() {
this->feed_wdt(last_op_end_time);
}
this->in_loop_ = false;
this->after_loop_tasks_();
this->app_state_ = new_app_state;
// Use the last component's end time instead of calling millis() again
@ -400,6 +385,36 @@ void Application::enable_pending_loops_() {
}
}
void Application::before_loop_tasks_(uint32_t loop_start_time) {
// Process scheduled tasks
this->scheduler.call();
// Feed the watchdog timer
this->feed_wdt(loop_start_time);
// Process any pending enable_loop requests from ISRs
// This must be done before marking in_loop_ = true to avoid race conditions
if (this->has_pending_enable_loop_requests_) {
// Clear flag BEFORE processing to avoid race condition
// If ISR sets it during processing, we'll catch it next loop iteration
// This is safe because:
// 1. Each component has its own pending_enable_loop_ flag that we check
// 2. If we can't process a component (wrong state), enable_pending_loops_()
// will set this flag back to true
// 3. Any new ISR requests during processing will set the flag again
this->has_pending_enable_loop_requests_ = false;
this->enable_pending_loops_();
}
// Mark that we're in the loop for safe reentrant modifications
this->in_loop_ = true;
}
void Application::after_loop_tasks_() {
// Clear the in_loop_ flag to indicate we're done processing components
this->in_loop_ = false;
}
#ifdef USE_SOCKET_SELECT_SUPPORT
bool Application::register_socket_fd(int fd) {
// WARNING: This function is NOT thread-safe and must only be called from the main loop

View File

@ -504,6 +504,8 @@ class Application {
void enable_component_loop_(Component *component);
void enable_pending_loops_();
void activate_looping_component_(uint16_t index);
void before_loop_tasks_(uint32_t loop_start_time);
void after_loop_tasks_();
void feed_wdt_arch_();