mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 00:51:49 +00:00 
			
		
		
		
	Compare commits
	
		
			26 Commits
		
	
	
		
			jesserockz
			...
			2025.7.5
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					68c0aa4d6d | ||
| 
						 | 
					d29cae9c3b | ||
| 
						 | 
					532e3e370f | ||
| 
						 | 
					da573a217d | ||
| 
						 | 
					a9b27d1966 | ||
| 
						 | 
					0aa3c9685e | ||
| 
						 | 
					d6b222c370 | ||
| 
						 | 
					573dad1736 | ||
| 
						 | 
					3a6cc0ea3d | ||
| 
						 | 
					2f9475a927 | ||
| 
						 | 
					8dce7b0905 | ||
| 
						 | 
					8b0ad3072f | ||
| 
						 | 
					93028a4d90 | ||
| 
						 | 
					c9793f3741 | ||
| 
						 | 
					2b5cceda58 | ||
| 
						 | 
					dc26ed9c46 | ||
| 
						 | 
					8674012406 | ||
| 
						 | 
					ae12deff87 | ||
| 
						 | 
					cb6acfe24b | ||
| 
						 | 
					fc8c5a7438 | ||
| 
						 | 
					f8777d3b66 | ||
| 
						 | 
					76e75f4cdc | ||
| 
						 | 
					896d7f8f76 | ||
| 
						 | 
					d92ee563f2 | ||
| 
						 | 
					d6ff790823 | ||
| 
						 | 
					7ac60c15dc | 
							
								
								
									
										2
									
								
								Doxyfile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Doxyfile
									
									
									
									
									
								
							@@ -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.5
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
 
 | 
			
		||||
@@ -657,10 +657,16 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
  bool send_message_smart_(EntityBase *entity, MessageCreatorPtr creator, uint8_t message_type,
 | 
			
		||||
                           uint8_t estimated_size) {
 | 
			
		||||
    // Try to send immediately if:
 | 
			
		||||
    // 1. We should try to send immediately (should_try_send_immediately = true)
 | 
			
		||||
    // 2. Batch delay is 0 (user has opted in to immediate sending)
 | 
			
		||||
    // 3. Buffer has space available
 | 
			
		||||
    if (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0 &&
 | 
			
		||||
    // 1. It's an UpdateStateResponse (always send immediately to handle cases where
 | 
			
		||||
    //    the main loop is blocked, e.g., during OTA updates)
 | 
			
		||||
    // 2. OR: We should try to send immediately (should_try_send_immediately = true)
 | 
			
		||||
    //        AND Batch delay is 0 (user has opted in to immediate sending)
 | 
			
		||||
    // 3. AND: Buffer has space available
 | 
			
		||||
    if ((
 | 
			
		||||
#ifdef USE_UPDATE
 | 
			
		||||
            message_type == UpdateStateResponse::MESSAGE_TYPE ||
 | 
			
		||||
#endif
 | 
			
		||||
            (this->flags_.should_try_send_immediately && this->get_batch_delay_ms_() == 0)) &&
 | 
			
		||||
        this->helper_->can_write_without_blocking()) {
 | 
			
		||||
      // Now actually encode and send
 | 
			
		||||
      if (creator(entity, this, MAX_BATCH_PACKET_SIZE, true) &&
 | 
			
		||||
 
 | 
			
		||||
@@ -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(
 | 
			
		||||
 
 | 
			
		||||
@@ -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,8 +46,12 @@ void ESP32TouchComponent::setup() {
 | 
			
		||||
 | 
			
		||||
  // Configure each touch pad
 | 
			
		||||
  for (auto *child : this->children_) {
 | 
			
		||||
    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
 | 
			
		||||
  esp_err_t err = touch_pad_isr_register(touch_isr_handler, this);
 | 
			
		||||
@@ -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 aren’t 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)
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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),
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,7 @@ from freetype import (
 | 
			
		||||
    FT_LOAD_RENDER,
 | 
			
		||||
    FT_LOAD_TARGET_MONO,
 | 
			
		||||
    Face,
 | 
			
		||||
    FT_Exception,
 | 
			
		||||
    ft_pixel_mode_mono,
 | 
			
		||||
)
 | 
			
		||||
import requests
 | 
			
		||||
@@ -94,7 +95,14 @@ class FontCache(MutableMapping):
 | 
			
		||||
        return self.store[self._keytransform(item)]
 | 
			
		||||
 | 
			
		||||
    def __setitem__(self, key, value):
 | 
			
		||||
        self.store[self._keytransform(key)] = Face(str(value))
 | 
			
		||||
        transformed = self._keytransform(key)
 | 
			
		||||
        try:
 | 
			
		||||
            self.store[transformed] = Face(str(value))
 | 
			
		||||
        except FT_Exception as exc:
 | 
			
		||||
            file = transformed.split(":", 1)
 | 
			
		||||
            raise cv.Invalid(
 | 
			
		||||
                f"{file[0].capitalize()} {file[1]} is not a valid font file"
 | 
			
		||||
            ) from exc
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FONT_CACHE = FontCache()
 | 
			
		||||
 
 | 
			
		||||
@@ -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]))
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,8 @@ namespace gt911 {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "gt911.touchscreen";
 | 
			
		||||
 | 
			
		||||
static const uint8_t PRIMARY_ADDRESS = 0x5D;    // default I2C address for GT911
 | 
			
		||||
static const uint8_t SECONDARY_ADDRESS = 0x14;  // secondary I2C address for GT911
 | 
			
		||||
static const uint8_t GET_TOUCH_STATE[2] = {0x81, 0x4E};
 | 
			
		||||
static const uint8_t CLEAR_TOUCH_STATE[3] = {0x81, 0x4E, 0x00};
 | 
			
		||||
static const uint8_t GET_TOUCHES[2] = {0x81, 0x4F};
 | 
			
		||||
@@ -18,8 +20,7 @@ static const size_t MAX_BUTTONS = 4;  // max number of buttons scanned
 | 
			
		||||
 | 
			
		||||
#define ERROR_CHECK(err) \
 | 
			
		||||
  if ((err) != i2c::ERROR_OK) { \
 | 
			
		||||
    ESP_LOGE(TAG, "Failed to communicate!"); \
 | 
			
		||||
    this->status_set_warning(); \
 | 
			
		||||
    this->status_set_warning("Communication failure"); \
 | 
			
		||||
    return; \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -30,31 +31,31 @@ void GT911Touchscreen::setup() {
 | 
			
		||||
    this->reset_pin_->setup();
 | 
			
		||||
    this->reset_pin_->digital_write(false);
 | 
			
		||||
    if (this->interrupt_pin_ != nullptr) {
 | 
			
		||||
      // The interrupt pin is used as an input during reset to select the I2C address.
 | 
			
		||||
      // temporarily set the interrupt pin to output to control address selection
 | 
			
		||||
      this->interrupt_pin_->pin_mode(gpio::FLAG_OUTPUT);
 | 
			
		||||
      this->interrupt_pin_->setup();
 | 
			
		||||
      this->interrupt_pin_->digital_write(false);
 | 
			
		||||
    }
 | 
			
		||||
    delay(2);
 | 
			
		||||
    this->reset_pin_->digital_write(true);
 | 
			
		||||
    delay(50);  // NOLINT
 | 
			
		||||
    if (this->interrupt_pin_ != nullptr) {
 | 
			
		||||
      this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT);
 | 
			
		||||
      this->interrupt_pin_->setup();
 | 
			
		||||
  }
 | 
			
		||||
  if (this->interrupt_pin_ != nullptr) {
 | 
			
		||||
    // set pre-configured input mode
 | 
			
		||||
    this->interrupt_pin_->setup();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // check the configuration of the int line.
 | 
			
		||||
  uint8_t data[4];
 | 
			
		||||
  err = this->write(GET_SWITCHES, 2);
 | 
			
		||||
  err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
 | 
			
		||||
  if (err != i2c::ERROR_OK && this->address_ == PRIMARY_ADDRESS) {
 | 
			
		||||
    this->address_ = SECONDARY_ADDRESS;
 | 
			
		||||
    err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
 | 
			
		||||
  }
 | 
			
		||||
  if (err == i2c::ERROR_OK) {
 | 
			
		||||
    err = this->read(data, 1);
 | 
			
		||||
    if (err == i2c::ERROR_OK) {
 | 
			
		||||
      ESP_LOGD(TAG, "Read from switches: 0x%02X", data[0]);
 | 
			
		||||
      ESP_LOGD(TAG, "Read from switches at address 0x%02X: 0x%02X", this->address_, data[0]);
 | 
			
		||||
      if (this->interrupt_pin_ != nullptr) {
 | 
			
		||||
        // datasheet says NOT to use pullup/down on the int line.
 | 
			
		||||
        this->interrupt_pin_->pin_mode(gpio::FLAG_INPUT);
 | 
			
		||||
        this->interrupt_pin_->setup();
 | 
			
		||||
        this->attach_interrupt_(this->interrupt_pin_,
 | 
			
		||||
                                (data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE);
 | 
			
		||||
      }
 | 
			
		||||
@@ -63,7 +64,7 @@ void GT911Touchscreen::setup() {
 | 
			
		||||
  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->write(GET_MAX_VALUES, 2);
 | 
			
		||||
      err = this->write(GET_MAX_VALUES, sizeof(GET_MAX_VALUES));
 | 
			
		||||
      if (err == i2c::ERROR_OK) {
 | 
			
		||||
        err = this->read(data, sizeof(data));
 | 
			
		||||
        if (err == i2c::ERROR_OK) {
 | 
			
		||||
@@ -75,15 +76,12 @@ void GT911Touchscreen::setup() {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (err != i2c::ERROR_OK) {
 | 
			
		||||
      ESP_LOGE(TAG, "Failed to read calibration values from touchscreen!");
 | 
			
		||||
      this->mark_failed();
 | 
			
		||||
      this->mark_failed("Failed to read calibration");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (err != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "Failed to communicate!");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
    this->mark_failed("Failed to communicate");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "GT911 Touchscreen setup complete");
 | 
			
		||||
@@ -94,7 +92,7 @@ void GT911Touchscreen::update_touches() {
 | 
			
		||||
  uint8_t touch_state = 0;
 | 
			
		||||
  uint8_t data[MAX_TOUCHES + 1][8];  // 8 bytes each for each point, plus extra space for the key byte
 | 
			
		||||
 | 
			
		||||
  err = this->write(GET_TOUCH_STATE, sizeof(GET_TOUCH_STATE), false);
 | 
			
		||||
  err = this->write(GET_TOUCH_STATE, sizeof(GET_TOUCH_STATE));
 | 
			
		||||
  ERROR_CHECK(err);
 | 
			
		||||
  err = this->read(&touch_state, 1);
 | 
			
		||||
  ERROR_CHECK(err);
 | 
			
		||||
@@ -106,7 +104,7 @@ void GT911Touchscreen::update_touches() {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES), false);
 | 
			
		||||
  err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES));
 | 
			
		||||
  ERROR_CHECK(err);
 | 
			
		||||
  // num_of_touches is guaranteed to be 0..5. Also read the key data
 | 
			
		||||
  err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1);
 | 
			
		||||
@@ -132,6 +130,7 @@ void GT911Touchscreen::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "GT911 Touchscreen:");
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
  LOG_PIN("  Interrupt Pin: ", this->interrupt_pin_);
 | 
			
		||||
  LOG_PIN("  Reset Pin: ", this->reset_pin_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace gt911
 | 
			
		||||
 
 | 
			
		||||
@@ -24,9 +24,6 @@ static const uint32_t READ_DURATION_MS = 16;
 | 
			
		||||
static const size_t TASK_STACK_SIZE = 4096;
 | 
			
		||||
static const ssize_t TASK_PRIORITY = 23;
 | 
			
		||||
 | 
			
		||||
// Use an exponential moving average to correct a DC offset with weight factor 1/1000
 | 
			
		||||
static const int32_t DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR = 1000;
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "i2s_audio.microphone";
 | 
			
		||||
 | 
			
		||||
enum MicrophoneEventGroupBits : uint32_t {
 | 
			
		||||
@@ -382,26 +379,57 @@ void I2SAudioMicrophone::mic_task(void *params) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void I2SAudioMicrophone::fix_dc_offset_(std::vector<uint8_t> &data) {
 | 
			
		||||
  /**
 | 
			
		||||
   * From https://www.musicdsp.org/en/latest/Filters/135-dc-filter.html:
 | 
			
		||||
   *
 | 
			
		||||
   *     y(n) = x(n) - x(n-1) + R * y(n-1)
 | 
			
		||||
   *     R = 1 - (pi * 2 * frequency / samplerate)
 | 
			
		||||
   *
 | 
			
		||||
   * From https://en.wikipedia.org/wiki/Hearing_range:
 | 
			
		||||
   *     The human range is commonly given as 20Hz up.
 | 
			
		||||
   *
 | 
			
		||||
   * From https://en.wikipedia.org/wiki/High-resolution_audio:
 | 
			
		||||
   *     A reasonable upper bound for sample rate seems to be 96kHz.
 | 
			
		||||
   *
 | 
			
		||||
   * Calculate R value for 20Hz on a 96kHz sample rate:
 | 
			
		||||
   *     R = 1 - (pi * 2 * 20 / 96000)
 | 
			
		||||
   *     R = 0.9986910031
 | 
			
		||||
   *
 | 
			
		||||
   * Transform floating point to bit-shifting approximation:
 | 
			
		||||
   *     output = input - prev_input + R * prev_output
 | 
			
		||||
   *     output = input - prev_input + (prev_output - (prev_output >> S))
 | 
			
		||||
   *
 | 
			
		||||
   * Approximate bit-shift value S from R:
 | 
			
		||||
   *     R = 1 - (1 >> S)
 | 
			
		||||
   *     R = 1 - (1 / 2^S)
 | 
			
		||||
   *     R = 1 - 2^-S
 | 
			
		||||
   *     0.9986910031 = 1 - 2^-S
 | 
			
		||||
   *     S = 9.57732 ~= 10
 | 
			
		||||
   *
 | 
			
		||||
   * Actual R from S:
 | 
			
		||||
   *     R = 1 - 2^-10 = 0.9990234375
 | 
			
		||||
   *
 | 
			
		||||
   * Confirm this has effect outside human hearing on 96000kHz sample:
 | 
			
		||||
   *     0.9990234375 = 1 - (pi * 2 * f / 96000)
 | 
			
		||||
   *     f = 14.9208Hz
 | 
			
		||||
   *
 | 
			
		||||
   * Confirm this has effect outside human hearing on PDM 16kHz sample:
 | 
			
		||||
   *     0.9990234375 = 1 - (pi * 2 * f / 16000)
 | 
			
		||||
   *     f = 2.4868Hz
 | 
			
		||||
   *
 | 
			
		||||
   */
 | 
			
		||||
  const uint8_t dc_filter_shift = 10;
 | 
			
		||||
  const size_t bytes_per_sample = this->audio_stream_info_.samples_to_bytes(1);
 | 
			
		||||
  const uint32_t total_samples = this->audio_stream_info_.bytes_to_samples(data.size());
 | 
			
		||||
 | 
			
		||||
  if (total_samples == 0) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int64_t offset_accumulator = 0;
 | 
			
		||||
  for (uint32_t sample_index = 0; sample_index < total_samples; ++sample_index) {
 | 
			
		||||
    const uint32_t byte_index = sample_index * bytes_per_sample;
 | 
			
		||||
    int32_t sample = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample);
 | 
			
		||||
    offset_accumulator += sample;
 | 
			
		||||
    sample -= this->dc_offset_;
 | 
			
		||||
    audio::pack_q31_as_audio_sample(sample, &data[byte_index], bytes_per_sample);
 | 
			
		||||
    int32_t input = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample);
 | 
			
		||||
    int32_t output = input - this->dc_offset_prev_input_ +
 | 
			
		||||
                     (this->dc_offset_prev_output_ - (this->dc_offset_prev_output_ >> dc_filter_shift));
 | 
			
		||||
    this->dc_offset_prev_input_ = input;
 | 
			
		||||
    this->dc_offset_prev_output_ = output;
 | 
			
		||||
    audio::pack_q31_as_audio_sample(output, &data[byte_index], bytes_per_sample);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const int32_t new_offset = offset_accumulator / total_samples;
 | 
			
		||||
  this->dc_offset_ = new_offset / DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR +
 | 
			
		||||
                     (DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR - 1) * this->dc_offset_ /
 | 
			
		||||
                         DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait) {
 | 
			
		||||
 
 | 
			
		||||
@@ -82,7 +82,8 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
 | 
			
		||||
 | 
			
		||||
  bool correct_dc_offset_;
 | 
			
		||||
  bool locked_driver_{false};
 | 
			
		||||
  int32_t dc_offset_{0};
 | 
			
		||||
  int32_t dc_offset_prev_input_{0};
 | 
			
		||||
  int32_t dc_offset_prev_output_{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace i2s_audio
 | 
			
		||||
 
 | 
			
		||||
@@ -477,10 +477,11 @@ void LD2450Component::handle_periodic_data_() {
 | 
			
		||||
    // X
 | 
			
		||||
    start = TARGET_X + index * 8;
 | 
			
		||||
    is_moving = false;
 | 
			
		||||
    sensor::Sensor *sx = this->move_x_sensors_[index];
 | 
			
		||||
    if (sx != nullptr) {
 | 
			
		||||
    // tx is used for further calculations, so always needs to be populated
 | 
			
		||||
    val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
 | 
			
		||||
    tx = val;
 | 
			
		||||
    sensor::Sensor *sx = this->move_x_sensors_[index];
 | 
			
		||||
    if (sx != nullptr) {
 | 
			
		||||
      if (this->cached_target_data_[index].x != val) {
 | 
			
		||||
        sx->publish_state(val);
 | 
			
		||||
        this->cached_target_data_[index].x = val;
 | 
			
		||||
@@ -488,10 +489,11 @@ void LD2450Component::handle_periodic_data_() {
 | 
			
		||||
    }
 | 
			
		||||
    // Y
 | 
			
		||||
    start = TARGET_Y + index * 8;
 | 
			
		||||
    sensor::Sensor *sy = this->move_y_sensors_[index];
 | 
			
		||||
    if (sy != nullptr) {
 | 
			
		||||
    // ty is used for further calculations, so always needs to be populated
 | 
			
		||||
    val = ld2450::decode_coordinate(this->buffer_data_[start], this->buffer_data_[start + 1]);
 | 
			
		||||
    ty = val;
 | 
			
		||||
    sensor::Sensor *sy = this->move_y_sensors_[index];
 | 
			
		||||
    if (sy != nullptr) {
 | 
			
		||||
      if (this->cached_target_data_[index].y != val) {
 | 
			
		||||
        sy->publish_state(val);
 | 
			
		||||
        this->cached_target_data_[index].y = val;
 | 
			
		||||
 
 | 
			
		||||
@@ -400,6 +400,7 @@ CONF_LOGGER_LOG = "logger.log"
 | 
			
		||||
LOGGER_LOG_ACTION_SCHEMA = cv.All(
 | 
			
		||||
    cv.maybe_simple_value(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(CONF_LOGGER_ID): cv.use_id(Logger),
 | 
			
		||||
            cv.Required(CONF_FORMAT): cv.string,
 | 
			
		||||
            cv.Optional(CONF_ARGS, default=list): cv.ensure_list(cv.lambda_),
 | 
			
		||||
            cv.Optional(CONF_LEVEL, default="DEBUG"): cv.one_of(
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,7 @@ from ..defines import (
 | 
			
		||||
    TILE_DIRECTIONS,
 | 
			
		||||
    literal,
 | 
			
		||||
)
 | 
			
		||||
from ..lv_validation import animated, lv_int
 | 
			
		||||
from ..lv_validation import animated, lv_int, lv_pct
 | 
			
		||||
from ..lvcode import lv, lv_assign, lv_expr, lv_obj, lv_Pvariable
 | 
			
		||||
from ..schemas import container_schema
 | 
			
		||||
from ..types import LV_EVENT, LvType, ObjUpdateAction, lv_obj_t, lv_obj_t_ptr
 | 
			
		||||
@@ -41,8 +41,8 @@ TILEVIEW_SCHEMA = cv.Schema(
 | 
			
		||||
            container_schema(
 | 
			
		||||
                obj_spec,
 | 
			
		||||
                {
 | 
			
		||||
                    cv.Required(CONF_ROW): lv_int,
 | 
			
		||||
                    cv.Required(CONF_COLUMN): lv_int,
 | 
			
		||||
                    cv.Required(CONF_ROW): cv.positive_int,
 | 
			
		||||
                    cv.Required(CONF_COLUMN): cv.positive_int,
 | 
			
		||||
                    cv.GenerateID(): cv.declare_id(lv_tile_t),
 | 
			
		||||
                    cv.Optional(CONF_DIR, default="ALL"): TILE_DIRECTIONS.several_of,
 | 
			
		||||
                },
 | 
			
		||||
@@ -63,21 +63,29 @@ class TileviewType(WidgetType):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    async def to_code(self, w: Widget, config: dict):
 | 
			
		||||
        for tile_conf in config.get(CONF_TILES, ()):
 | 
			
		||||
        tiles = config[CONF_TILES]
 | 
			
		||||
        for tile_conf in tiles:
 | 
			
		||||
            w_id = tile_conf[CONF_ID]
 | 
			
		||||
            tile_obj = lv_Pvariable(lv_obj_t, w_id)
 | 
			
		||||
            tile = Widget.create(w_id, tile_obj, tile_spec, tile_conf)
 | 
			
		||||
            dirs = tile_conf[CONF_DIR]
 | 
			
		||||
            if isinstance(dirs, list):
 | 
			
		||||
                dirs = "|".join(dirs)
 | 
			
		||||
            row_pos = tile_conf[CONF_ROW]
 | 
			
		||||
            col_pos = tile_conf[CONF_COLUMN]
 | 
			
		||||
            lv_assign(
 | 
			
		||||
                tile_obj,
 | 
			
		||||
                lv_expr.tileview_add_tile(
 | 
			
		||||
                    w.obj, tile_conf[CONF_COLUMN], tile_conf[CONF_ROW], literal(dirs)
 | 
			
		||||
                ),
 | 
			
		||||
                lv_expr.tileview_add_tile(w.obj, col_pos, row_pos, literal(dirs)),
 | 
			
		||||
            )
 | 
			
		||||
            # Bugfix for LVGL 8.x
 | 
			
		||||
            lv_obj.set_pos(tile_obj, lv_pct(col_pos * 100), lv_pct(row_pos * 100))
 | 
			
		||||
            await set_obj_properties(tile, tile_conf)
 | 
			
		||||
            await add_widgets(tile, tile_conf)
 | 
			
		||||
        if tiles:
 | 
			
		||||
            # Set the first tile as active
 | 
			
		||||
            lv_obj.set_tile_id(
 | 
			
		||||
                w.obj, tiles[0][CONF_COLUMN], tiles[0][CONF_ROW], literal("LV_ANIM_OFF")
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
tileview_spec = TileviewType()
 | 
			
		||||
 
 | 
			
		||||
@@ -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),
 | 
			
		||||
 
 | 
			
		||||
@@ -60,6 +60,20 @@ RemoteReceiverComponent = remote_receiver_ns.class_(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_config(config):
 | 
			
		||||
    if CORE.is_esp32:
 | 
			
		||||
        variant = esp32.get_esp32_variant()
 | 
			
		||||
        if variant in (esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S2):
 | 
			
		||||
            max_idle = 65535
 | 
			
		||||
        else:
 | 
			
		||||
            max_idle = 32767
 | 
			
		||||
        if CONF_CLOCK_RESOLUTION in config:
 | 
			
		||||
            max_idle = int(max_idle * 1000000 / config[CONF_CLOCK_RESOLUTION])
 | 
			
		||||
        if config[CONF_IDLE].total_microseconds > max_idle:
 | 
			
		||||
            raise cv.Invalid(f"config 'idle' exceeds the maximum value of {max_idle}us")
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_tolerance(value):
 | 
			
		||||
    if isinstance(value, dict):
 | 
			
		||||
        return TOLERANCE_SCHEMA(value)
 | 
			
		||||
@@ -136,7 +150,9 @@ CONFIG_SCHEMA = remote_base.validate_triggers(
 | 
			
		||||
                cv.boolean,
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
    .add_extra(validate_config)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -86,10 +86,9 @@ void RemoteReceiverComponent::setup() {
 | 
			
		||||
 | 
			
		||||
  uint32_t event_size = sizeof(rmt_rx_done_event_data_t);
 | 
			
		||||
  uint32_t max_filter_ns = 255u * 1000 / (RMT_CLK_FREQ / 1000000);
 | 
			
		||||
  uint32_t max_idle_ns = 65535u * 1000;
 | 
			
		||||
  memset(&this->store_.config, 0, sizeof(this->store_.config));
 | 
			
		||||
  this->store_.config.signal_range_min_ns = std::min(this->filter_us_ * 1000, max_filter_ns);
 | 
			
		||||
  this->store_.config.signal_range_max_ns = std::min(this->idle_us_ * 1000, max_idle_ns);
 | 
			
		||||
  this->store_.config.signal_range_max_ns = this->idle_us_ * 1000;
 | 
			
		||||
  this->store_.filter_symbols = this->filter_symbols_;
 | 
			
		||||
  this->store_.receive_size = this->receive_symbols_ * sizeof(rmt_symbol_word_t);
 | 
			
		||||
  this->store_.buffer_size = std::max((event_size + this->store_.receive_size) * 2, this->buffer_size_);
 | 
			
		||||
 
 | 
			
		||||
@@ -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);
 | 
			
		||||
 
 | 
			
		||||
@@ -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)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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:
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ from enum import Enum
 | 
			
		||||
 | 
			
		||||
from esphome.enum import StrEnum
 | 
			
		||||
 | 
			
		||||
__version__ = "2025.7.2"
 | 
			
		||||
__version__ = "2025.7.5"
 | 
			
		||||
 | 
			
		||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 | 
			
		||||
VALID_SUBSTITUTIONS_CHARACTERS = (
 | 
			
		||||
 
 | 
			
		||||
@@ -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
 | 
			
		||||
 
 | 
			
		||||
@@ -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_();
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -67,7 +67,10 @@ To bit_cast(const From &src) {
 | 
			
		||||
  return dst;
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
using std::lerp;
 | 
			
		||||
 | 
			
		||||
// clang-format off
 | 
			
		||||
inline float lerp(float completion, float start, float end) = delete;  // Please use std::lerp. Notice that it has different order on arguments!
 | 
			
		||||
// clang-format on
 | 
			
		||||
 | 
			
		||||
// std::byteswap from C++23
 | 
			
		||||
template<typename T> constexpr T byteswap(T n) {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ set -e
 | 
			
		||||
cd "$(dirname "$0")/.."
 | 
			
		||||
if [ ! -n "$VIRTUAL_ENV" ]; then
 | 
			
		||||
  if [ -x "$(command -v uv)" ]; then
 | 
			
		||||
    uv venv venv
 | 
			
		||||
    uv venv --seed venv
 | 
			
		||||
  else
 | 
			
		||||
    python3 -m venv venv
 | 
			
		||||
  fi
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,8 @@ esphome:
 | 
			
		||||
esp32:
 | 
			
		||||
  board: esp32dev
 | 
			
		||||
 | 
			
		||||
logger:
 | 
			
		||||
 | 
			
		||||
text:
 | 
			
		||||
  - platform: template
 | 
			
		||||
    name: "test 1 text"
 | 
			
		||||
 
 | 
			
		||||
@@ -738,7 +738,7 @@ lvgl:
 | 
			
		||||
                    id: bar_id
 | 
			
		||||
                    value: !lambda return (int)((float)rand() / RAND_MAX * 100);
 | 
			
		||||
                    start_value: !lambda return (int)((float)rand() / RAND_MAX * 100);
 | 
			
		||||
                    mode: symmetrical
 | 
			
		||||
                    mode: range
 | 
			
		||||
                - logger.log:
 | 
			
		||||
                    format: "bar value %f"
 | 
			
		||||
                    args: [x]
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user