mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 09:01:49 +00:00 
			
		
		
		
	Compare commits
	
		
			48 Commits
		
	
	
		
			wifi_fixed
			...
			2025.10.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					9a29dec6d9 | ||
| 
						 | 
					63b113d823 | ||
| 
						 | 
					0381644605 | ||
| 
						 | 
					48a557b005 | ||
| 
						 | 
					780ece73ff | ||
| 
						 | 
					d7fcf8d57b | ||
| 
						 | 
					82a3ca575f | ||
| 
						 | 
					5913da5a89 | ||
| 
						 | 
					8c13105ce1 | ||
| 
						 | 
					c3fd07f8bc | ||
| 
						 | 
					d02ed41eb4 | ||
| 
						 | 
					07504c8208 | ||
| 
						 | 
					b666b8e261 | ||
| 
						 | 
					8627b56e36 | ||
| 
						 | 
					69df07ddcf | ||
| 
						 | 
					13cfa30c67 | ||
| 
						 | 
					da1959ab5d | ||
| 
						 | 
					2b42903e9c | ||
| 
						 | 
					742c9cbb53 | ||
| 
						 | 
					e4bc465a3d | ||
| 
						 | 
					5cec0941f8 | ||
| 
						 | 
					72a7aeb430 | ||
| 
						 | 
					53e6b28092 | ||
| 
						 | 
					7f3c7bb5c6 | ||
| 
						 | 
					c02c0b2a96 | ||
| 
						 | 
					5f5092e29f | ||
| 
						 | 
					2864bf1674 | ||
| 
						 | 
					132e949927 | ||
| 
						 | 
					8fa44e471d | ||
| 
						 | 
					ccedcfb600 | ||
| 
						 | 
					8b0ec0afe3 | ||
| 
						 | 
					dca29ed89b | ||
| 
						 | 
					728726e29e | ||
| 
						 | 
					79f4ca20b8 | ||
| 
						 | 
					3eca72e0b8 | ||
| 
						 | 
					22c0f55cef | ||
| 
						 | 
					fd8ecc9608 | ||
| 
						 | 
					ac96a59d58 | ||
| 
						 | 
					dceed992d8 | ||
| 
						 | 
					b0c66c1c09 | ||
| 
						 | 
					8f04a5b944 | ||
| 
						 | 
					e6c21df30b | ||
| 
						 | 
					842cb9033a | ||
| 
						 | 
					a2cb415dfa | ||
| 
						 | 
					1fac193535 | ||
| 
						 | 
					34632f78cf | ||
| 
						 | 
					b93c60e85a | ||
| 
						 | 
					60dc055509 | 
							
								
								
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@@ -433,7 +433,7 @@ jobs:
 | 
			
		||||
    if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      max-parallel: 5
 | 
			
		||||
      max-parallel: ${{ (github.base_ref == 'beta' || github.base_ref == 'release') && 8 || 4 }}
 | 
			
		||||
      matrix:
 | 
			
		||||
        components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }}
 | 
			
		||||
    steps:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
								
							@@ -58,7 +58,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      # Initializes the CodeQL tools for scanning.
 | 
			
		||||
      - name: Initialize CodeQL
 | 
			
		||||
        uses: github/codeql-action/init@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
 | 
			
		||||
        uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
 | 
			
		||||
        with:
 | 
			
		||||
          languages: ${{ matrix.language }}
 | 
			
		||||
          build-mode: ${{ matrix.build-mode }}
 | 
			
		||||
@@ -86,6 +86,6 @@ jobs:
 | 
			
		||||
          exit 1
 | 
			
		||||
 | 
			
		||||
      - name: Perform CodeQL Analysis
 | 
			
		||||
        uses: github/codeql-action/analyze@f443b600d91635bebf5b0d9ebc620189c0d6fba5 # v4.30.8
 | 
			
		||||
        uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
 | 
			
		||||
        with:
 | 
			
		||||
          category: "/language:${{matrix.language}}"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							@@ -23,7 +23,7 @@ jobs:
 | 
			
		||||
        with:
 | 
			
		||||
          debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
 | 
			
		||||
          remove-stale-when-updated: true
 | 
			
		||||
          operations-per-run: 400
 | 
			
		||||
          operations-per-run: 150
 | 
			
		||||
 | 
			
		||||
          # The 90 day stale policy for PRs
 | 
			
		||||
          # - PRs
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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.11.0-dev
 | 
			
		||||
PROJECT_NUMBER         = 2025.10.0
 | 
			
		||||
 | 
			
		||||
# 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
 | 
			
		||||
 
 | 
			
		||||
@@ -268,8 +268,10 @@ def has_ip_address() -> bool:
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def has_resolvable_address() -> bool:
 | 
			
		||||
    """Check if CORE.address is resolvable (via mDNS or is an IP address)."""
 | 
			
		||||
    return has_mdns() or has_ip_address()
 | 
			
		||||
    """Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address)."""
 | 
			
		||||
    # Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable
 | 
			
		||||
    # The resolve_ip_address() function in helpers.py handles all types via AsyncResolver
 | 
			
		||||
    return CORE.address is not None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str):
 | 
			
		||||
@@ -578,11 +580,12 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
 | 
			
		||||
    if has_api():
 | 
			
		||||
        addresses_to_use: list[str] | None = None
 | 
			
		||||
 | 
			
		||||
        if port_type == "NETWORK" and (has_mdns() or is_ip_address(port)):
 | 
			
		||||
        if port_type == "NETWORK":
 | 
			
		||||
            # Network addresses (IPs, mDNS names, or regular DNS hostnames) can be used
 | 
			
		||||
            # The resolve_ip_address() function in helpers.py handles all types
 | 
			
		||||
            addresses_to_use = devices
 | 
			
		||||
        elif port_type in ("NETWORK", "MQTT", "MQTTIP") and has_mqtt_ip_lookup():
 | 
			
		||||
            # Only use MQTT IP lookup if the first condition didn't match
 | 
			
		||||
            # (for MQTT/MQTTIP types, or for NETWORK when mdns/ip check fails)
 | 
			
		||||
        elif port_type in ("MQTT", "MQTTIP") and has_mqtt_ip_lookup():
 | 
			
		||||
            # Use MQTT IP lookup for MQTT/MQTTIP types
 | 
			
		||||
            addresses_to_use = mqtt_get_ip(
 | 
			
		||||
                config, args.username, args.password, args.client_id
 | 
			
		||||
            )
 | 
			
		||||
 
 | 
			
		||||
@@ -304,17 +304,6 @@ def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
 | 
			
		||||
    return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _is_framework_url(source: str) -> str:
 | 
			
		||||
    # platformio accepts many URL schemes for framework repositories and archives including http, https, git, file, and symlink
 | 
			
		||||
    import urllib.parse
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        parsed = urllib.parse.urlparse(source)
 | 
			
		||||
    except ValueError:
 | 
			
		||||
        return False
 | 
			
		||||
    return bool(parsed.scheme)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# NOTE: Keep this in mind when updating the recommended version:
 | 
			
		||||
#  * New framework historically have had some regressions, especially for WiFi.
 | 
			
		||||
#    The new version needs to be thoroughly validated before changing the
 | 
			
		||||
@@ -398,7 +387,7 @@ def _check_versions(value):
 | 
			
		||||
        value[CONF_SOURCE] = value.get(
 | 
			
		||||
            CONF_SOURCE, _format_framework_arduino_version(version)
 | 
			
		||||
        )
 | 
			
		||||
        if _is_framework_url(value[CONF_SOURCE]):
 | 
			
		||||
        if value[CONF_SOURCE].startswith("http"):
 | 
			
		||||
            value[CONF_SOURCE] = (
 | 
			
		||||
                f"pioarduino/framework-arduinoespressif32@{value[CONF_SOURCE]}"
 | 
			
		||||
            )
 | 
			
		||||
@@ -411,7 +400,7 @@ def _check_versions(value):
 | 
			
		||||
            CONF_SOURCE,
 | 
			
		||||
            _format_framework_espidf_version(version, value.get(CONF_RELEASE, None)),
 | 
			
		||||
        )
 | 
			
		||||
        if _is_framework_url(value[CONF_SOURCE]):
 | 
			
		||||
        if value[CONF_SOURCE].startswith("http"):
 | 
			
		||||
            value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}"
 | 
			
		||||
 | 
			
		||||
    if CONF_PLATFORM_VERSION not in value:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from esphome import automation
 | 
			
		||||
@@ -53,19 +52,9 @@ class BLEFeatures(StrEnum):
 | 
			
		||||
    ESP_BT_DEVICE = "ESP_BT_DEVICE"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Dataclass for registration counts
 | 
			
		||||
@dataclass
 | 
			
		||||
class RegistrationCounts:
 | 
			
		||||
    listeners: int = 0
 | 
			
		||||
    clients: int = 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Set to track which features are needed by components
 | 
			
		||||
_required_features: set[BLEFeatures] = set()
 | 
			
		||||
 | 
			
		||||
# Track registration counts for StaticVector sizing
 | 
			
		||||
_registration_counts = RegistrationCounts()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def register_ble_features(features: set[BLEFeatures]) -> None:
 | 
			
		||||
    """Register BLE features that a component needs.
 | 
			
		||||
@@ -268,14 +257,12 @@ async def to_code(config):
 | 
			
		||||
        register_ble_features({BLEFeatures.ESP_BT_DEVICE})
 | 
			
		||||
 | 
			
		||||
    for conf in config.get(CONF_ON_BLE_ADVERTISE, []):
 | 
			
		||||
        _registration_counts.listeners += 1
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        if CONF_MAC_ADDRESS in conf:
 | 
			
		||||
            addr_list = [it.as_hex for it in conf[CONF_MAC_ADDRESS]]
 | 
			
		||||
            cg.add(trigger.set_addresses(addr_list))
 | 
			
		||||
        await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf)
 | 
			
		||||
    for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []):
 | 
			
		||||
        _registration_counts.listeners += 1
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        if len(conf[CONF_SERVICE_UUID]) == len(bt_uuid16_format):
 | 
			
		||||
            cg.add(trigger.set_service_uuid16(as_hex(conf[CONF_SERVICE_UUID])))
 | 
			
		||||
@@ -288,7 +275,6 @@ async def to_code(config):
 | 
			
		||||
            cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
 | 
			
		||||
        await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
 | 
			
		||||
    for conf in config.get(CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE, []):
 | 
			
		||||
        _registration_counts.listeners += 1
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        if len(conf[CONF_MANUFACTURER_ID]) == len(bt_uuid16_format):
 | 
			
		||||
            cg.add(trigger.set_manufacturer_uuid16(as_hex(conf[CONF_MANUFACTURER_ID])))
 | 
			
		||||
@@ -301,7 +287,6 @@ async def to_code(config):
 | 
			
		||||
            cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex))
 | 
			
		||||
        await automation.build_automation(trigger, [(adv_data_t_const_ref, "x")], conf)
 | 
			
		||||
    for conf in config.get(CONF_ON_SCAN_END, []):
 | 
			
		||||
        _registration_counts.listeners += 1
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await automation.build_automation(trigger, [], conf)
 | 
			
		||||
 | 
			
		||||
@@ -335,17 +320,6 @@ async def _add_ble_features():
 | 
			
		||||
        cg.add_define("USE_ESP32_BLE_DEVICE")
 | 
			
		||||
        cg.add_define("USE_ESP32_BLE_UUID")
 | 
			
		||||
 | 
			
		||||
    # Add defines for StaticVector sizing based on registration counts
 | 
			
		||||
    # Only define if count > 0 to avoid allocating unnecessary memory
 | 
			
		||||
    if _registration_counts.listeners > 0:
 | 
			
		||||
        cg.add_define(
 | 
			
		||||
            "ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT", _registration_counts.listeners
 | 
			
		||||
        )
 | 
			
		||||
    if _registration_counts.clients > 0:
 | 
			
		||||
        cg.add_define(
 | 
			
		||||
            "ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT", _registration_counts.clients
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
ESP32_BLE_START_SCAN_ACTION_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
@@ -395,7 +369,6 @@ async def register_ble_device(
 | 
			
		||||
    var: cg.SafeExpType, config: ConfigType
 | 
			
		||||
) -> cg.SafeExpType:
 | 
			
		||||
    register_ble_features({BLEFeatures.ESP_BT_DEVICE})
 | 
			
		||||
    _registration_counts.listeners += 1
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
 | 
			
		||||
    cg.add(paren.register_listener(var))
 | 
			
		||||
    return var
 | 
			
		||||
@@ -403,7 +376,6 @@ async def register_ble_device(
 | 
			
		||||
 | 
			
		||||
async def register_client(var: cg.SafeExpType, config: ConfigType) -> cg.SafeExpType:
 | 
			
		||||
    register_ble_features({BLEFeatures.ESP_BT_DEVICE})
 | 
			
		||||
    _registration_counts.clients += 1
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
 | 
			
		||||
    cg.add(paren.register_client(var))
 | 
			
		||||
    return var
 | 
			
		||||
@@ -417,7 +389,6 @@ async def register_raw_ble_device(
 | 
			
		||||
    This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
 | 
			
		||||
    will not be compiled in if this is the only registration method used.
 | 
			
		||||
    """
 | 
			
		||||
    _registration_counts.listeners += 1
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
 | 
			
		||||
    cg.add(paren.register_listener(var))
 | 
			
		||||
    return var
 | 
			
		||||
@@ -431,7 +402,6 @@ async def register_raw_client(
 | 
			
		||||
    This does NOT register the ESP_BT_DEVICE feature, meaning ESPBTDevice
 | 
			
		||||
    will not be compiled in if this is the only registration method used.
 | 
			
		||||
    """
 | 
			
		||||
    _registration_counts.clients += 1
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ESP32_BLE_ID])
 | 
			
		||||
    cg.add(paren.register_client(var))
 | 
			
		||||
    return var
 | 
			
		||||
 
 | 
			
		||||
@@ -74,11 +74,9 @@ void ESP32BLETracker::setup() {
 | 
			
		||||
      [this](ota::OTAState state, float progress, uint8_t error, ota::OTAComponent *comp) {
 | 
			
		||||
        if (state == ota::OTA_STARTED) {
 | 
			
		||||
          this->stop_scan();
 | 
			
		||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
 | 
			
		||||
          for (auto *client : this->clients_) {
 | 
			
		||||
            client->disconnect();
 | 
			
		||||
          }
 | 
			
		||||
#endif
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
#endif
 | 
			
		||||
@@ -208,10 +206,8 @@ void ESP32BLETracker::start_scan_(bool first) {
 | 
			
		||||
  this->set_scanner_state_(ScannerState::STARTING);
 | 
			
		||||
  ESP_LOGD(TAG, "Starting scan, set scanner state to STARTING.");
 | 
			
		||||
  if (!first) {
 | 
			
		||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
 | 
			
		||||
    for (auto *listener : this->listeners_)
 | 
			
		||||
      listener->on_scan_end();
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
  this->already_discovered_.clear();
 | 
			
		||||
@@ -240,25 +236,20 @@ void ESP32BLETracker::start_scan_(bool first) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32BLETracker::register_client(ESPBTClient *client) {
 | 
			
		||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
 | 
			
		||||
  client->app_id = ++this->app_id_;
 | 
			
		||||
  this->clients_.push_back(client);
 | 
			
		||||
  this->recalculate_advertisement_parser_types();
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32BLETracker::register_listener(ESPBTDeviceListener *listener) {
 | 
			
		||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
 | 
			
		||||
  listener->set_parent(this);
 | 
			
		||||
  this->listeners_.push_back(listener);
 | 
			
		||||
  this->recalculate_advertisement_parser_types();
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32BLETracker::recalculate_advertisement_parser_types() {
 | 
			
		||||
  this->raw_advertisements_ = false;
 | 
			
		||||
  this->parse_advertisements_ = false;
 | 
			
		||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
 | 
			
		||||
  for (auto *listener : this->listeners_) {
 | 
			
		||||
    if (listener->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) {
 | 
			
		||||
      this->parse_advertisements_ = true;
 | 
			
		||||
@@ -266,8 +257,6 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() {
 | 
			
		||||
      this->raw_advertisements_ = true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
 | 
			
		||||
  for (auto *client : this->clients_) {
 | 
			
		||||
    if (client->get_advertisement_parser_type() == AdvertisementParserType::PARSED_ADVERTISEMENTS) {
 | 
			
		||||
      this->parse_advertisements_ = true;
 | 
			
		||||
@@ -275,7 +264,6 @@ void ESP32BLETracker::recalculate_advertisement_parser_types() {
 | 
			
		||||
      this->raw_advertisements_ = true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
 | 
			
		||||
@@ -295,11 +283,9 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
  // Forward all events to clients (scan results are handled separately via gap_scan_event_handler)
 | 
			
		||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
 | 
			
		||||
  for (auto *client : this->clients_) {
 | 
			
		||||
    client->gap_event_handler(event, param);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
 | 
			
		||||
@@ -362,11 +348,9 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_
 | 
			
		||||
 | 
			
		||||
void ESP32BLETracker::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                                          esp_ble_gattc_cb_param_t *param) {
 | 
			
		||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
 | 
			
		||||
  for (auto *client : this->clients_) {
 | 
			
		||||
    client->gattc_event_handler(event, gattc_if, param);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32BLETracker::set_scanner_state_(ScannerState state) {
 | 
			
		||||
@@ -720,16 +704,12 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
 | 
			
		||||
void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
 | 
			
		||||
  // Process raw advertisements
 | 
			
		||||
  if (this->raw_advertisements_) {
 | 
			
		||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
 | 
			
		||||
    for (auto *listener : this->listeners_) {
 | 
			
		||||
      listener->parse_devices(&scan_result, 1);
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
 | 
			
		||||
    for (auto *client : this->clients_) {
 | 
			
		||||
      client->parse_devices(&scan_result, 1);
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Process parsed advertisements
 | 
			
		||||
@@ -739,20 +719,16 @@ void ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
 | 
			
		||||
    device.parse_scan_rst(scan_result);
 | 
			
		||||
 | 
			
		||||
    bool found = false;
 | 
			
		||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
 | 
			
		||||
    for (auto *listener : this->listeners_) {
 | 
			
		||||
      if (listener->parse_device(device))
 | 
			
		||||
        found = true;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
 | 
			
		||||
    for (auto *client : this->clients_) {
 | 
			
		||||
      if (client->parse_device(device)) {
 | 
			
		||||
        found = true;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    if (!found && !this->scan_continuous_) {
 | 
			
		||||
      this->print_bt_device_info(device);
 | 
			
		||||
@@ -769,10 +745,8 @@ void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {
 | 
			
		||||
  // Reset timeout state machine instead of cancelling scheduler timeout
 | 
			
		||||
  this->scan_timeout_state_ = ScanTimeoutState::INACTIVE;
 | 
			
		||||
 | 
			
		||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
 | 
			
		||||
  for (auto *listener : this->listeners_)
 | 
			
		||||
    listener->on_scan_end();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  this->set_scanner_state_(ScannerState::IDLE);
 | 
			
		||||
}
 | 
			
		||||
@@ -796,7 +770,6 @@ void ESP32BLETracker::handle_scanner_failure_() {
 | 
			
		||||
 | 
			
		||||
void ESP32BLETracker::try_promote_discovered_clients_() {
 | 
			
		||||
  // Only promote the first discovered client to avoid multiple simultaneous connections
 | 
			
		||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
 | 
			
		||||
  for (auto *client : this->clients_) {
 | 
			
		||||
    if (client->state() != ClientState::DISCOVERED) {
 | 
			
		||||
      continue;
 | 
			
		||||
@@ -818,7 +791,6 @@ void ESP32BLETracker::try_promote_discovered_clients_() {
 | 
			
		||||
    client->connect();
 | 
			
		||||
    break;
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const char *ESP32BLETracker::scanner_state_to_string_(ScannerState state) const {
 | 
			
		||||
 
 | 
			
		||||
@@ -302,7 +302,6 @@ class ESP32BLETracker : public Component,
 | 
			
		||||
  /// Count clients in each state
 | 
			
		||||
  ClientStateCounts count_client_states_() const {
 | 
			
		||||
    ClientStateCounts counts;
 | 
			
		||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
 | 
			
		||||
    for (auto *client : this->clients_) {
 | 
			
		||||
      switch (client->state()) {
 | 
			
		||||
        case ClientState::DISCONNECTING:
 | 
			
		||||
@@ -318,17 +317,12 @@ class ESP32BLETracker : public Component,
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
    return counts;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Group 1: Large objects (12+ bytes) - vectors and callback manager
 | 
			
		||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT
 | 
			
		||||
  StaticVector<ESPBTDeviceListener *, ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT> listeners_;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT
 | 
			
		||||
  StaticVector<ESPBTClient *, ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT> clients_;
 | 
			
		||||
#endif
 | 
			
		||||
  std::vector<ESPBTDeviceListener *> listeners_;
 | 
			
		||||
  std::vector<ESPBTClient *> clients_;
 | 
			
		||||
  CallbackManager<void(ScannerState)> scanner_state_callbacks_;
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
  /// Vector of addresses that have already been printed in print_bt_device_info
 | 
			
		||||
 
 | 
			
		||||
@@ -143,6 +143,7 @@ void ESP32ImprovComponent::loop() {
 | 
			
		||||
#else
 | 
			
		||||
      this->set_state_(improv::STATE_AUTHORIZED);
 | 
			
		||||
#endif
 | 
			
		||||
      this->check_wifi_connection_();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case improv::STATE_AUTHORIZED: {
 | 
			
		||||
@@ -156,31 +157,12 @@ void ESP32ImprovComponent::loop() {
 | 
			
		||||
      if (!this->check_identify_()) {
 | 
			
		||||
        this->set_status_indicator_state_((now % 1000) < 500);
 | 
			
		||||
      }
 | 
			
		||||
      this->check_wifi_connection_();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case improv::STATE_PROVISIONING: {
 | 
			
		||||
      this->set_status_indicator_state_((now % 200) < 100);
 | 
			
		||||
      if (wifi::global_wifi_component->is_connected()) {
 | 
			
		||||
        wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
 | 
			
		||||
                                                   this->connecting_sta_.get_password());
 | 
			
		||||
        this->connecting_sta_ = {};
 | 
			
		||||
        this->cancel_timeout("wifi-connect-timeout");
 | 
			
		||||
        this->set_state_(improv::STATE_PROVISIONED);
 | 
			
		||||
 | 
			
		||||
        std::vector<std::string> urls = {ESPHOME_MY_LINK};
 | 
			
		||||
#ifdef USE_WEBSERVER
 | 
			
		||||
        for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
 | 
			
		||||
          if (ip.is_ip4()) {
 | 
			
		||||
            std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
 | 
			
		||||
            urls.push_back(webserver_url);
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
#endif
 | 
			
		||||
        std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls);
 | 
			
		||||
        this->send_response_(data);
 | 
			
		||||
        this->stop();
 | 
			
		||||
      }
 | 
			
		||||
      this->check_wifi_connection_();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case improv::STATE_PROVISIONED: {
 | 
			
		||||
@@ -392,6 +374,36 @@ void ESP32ImprovComponent::on_wifi_connect_timeout_() {
 | 
			
		||||
  wifi::global_wifi_component->clear_sta();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32ImprovComponent::check_wifi_connection_() {
 | 
			
		||||
  if (!wifi::global_wifi_component->is_connected()) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->state_ == improv::STATE_PROVISIONING) {
 | 
			
		||||
    wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), this->connecting_sta_.get_password());
 | 
			
		||||
    this->connecting_sta_ = {};
 | 
			
		||||
    this->cancel_timeout("wifi-connect-timeout");
 | 
			
		||||
 | 
			
		||||
    std::vector<std::string> urls = {ESPHOME_MY_LINK};
 | 
			
		||||
#ifdef USE_WEBSERVER
 | 
			
		||||
    for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
 | 
			
		||||
      if (ip.is_ip4()) {
 | 
			
		||||
        std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
 | 
			
		||||
        urls.push_back(webserver_url);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
    std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls);
 | 
			
		||||
    this->send_response_(data);
 | 
			
		||||
  } else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) {
 | 
			
		||||
    ESP_LOGD(TAG, "WiFi provisioned externally");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->set_state_(improv::STATE_PROVISIONED);
 | 
			
		||||
  this->stop();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32ImprovComponent::advertise_service_data_() {
 | 
			
		||||
  uint8_t service_data[IMPROV_SERVICE_DATA_SIZE] = {};
 | 
			
		||||
  service_data[0] = IMPROV_PROTOCOL_ID_1;  // PR
 | 
			
		||||
 
 | 
			
		||||
@@ -111,6 +111,7 @@ class ESP32ImprovComponent : public Component {
 | 
			
		||||
  void send_response_(std::vector<uint8_t> &response);
 | 
			
		||||
  void process_incoming_data_();
 | 
			
		||||
  void on_wifi_connect_timeout_();
 | 
			
		||||
  void check_wifi_connection_();
 | 
			
		||||
  bool check_identify_();
 | 
			
		||||
  void advertise_service_data_();
 | 
			
		||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ namespace esphome {
 | 
			
		||||
static const char *const TAG = "esphome.ota";
 | 
			
		||||
static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
 | 
			
		||||
static constexpr size_t OTA_BUFFER_SIZE = 1024;                  // buffer size for OTA data transfer
 | 
			
		||||
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 10000;  // milliseconds for initial handshake
 | 
			
		||||
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000;  // milliseconds for initial handshake
 | 
			
		||||
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000;       // milliseconds for data transfer
 | 
			
		||||
 | 
			
		||||
#ifdef USE_OTA_PASSWORD
 | 
			
		||||
 
 | 
			
		||||
@@ -56,50 +56,41 @@ DriverChip(
 | 
			
		||||
    "WAVESHARE-P4-86-PANEL",
 | 
			
		||||
    height=720,
 | 
			
		||||
    width=720,
 | 
			
		||||
    hsync_back_porch=80,
 | 
			
		||||
    hsync_back_porch=50,
 | 
			
		||||
    hsync_pulse_width=20,
 | 
			
		||||
    hsync_front_porch=80,
 | 
			
		||||
    vsync_back_porch=12,
 | 
			
		||||
    hsync_front_porch=50,
 | 
			
		||||
    vsync_back_porch=20,
 | 
			
		||||
    vsync_pulse_width=4,
 | 
			
		||||
    vsync_front_porch=30,
 | 
			
		||||
    pclk_frequency="46MHz",
 | 
			
		||||
    lane_bit_rate="1Gbps",
 | 
			
		||||
    vsync_front_porch=20,
 | 
			
		||||
    pclk_frequency="38MHz",
 | 
			
		||||
    lane_bit_rate="480Mbps",
 | 
			
		||||
    swap_xy=cv.UNDEFINED,
 | 
			
		||||
    color_order="RGB",
 | 
			
		||||
    reset_pin=27,
 | 
			
		||||
    initsequence=[
 | 
			
		||||
        (0xB9, 0xF1, 0x12, 0x83),
 | 
			
		||||
        (
 | 
			
		||||
            0xBA, 0x31, 0x81, 0x05, 0xF9, 0x0E, 0x0E, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x25, 0x00,
 | 
			
		||||
            0x90, 0x0A, 0x00, 0x00, 0x01, 0x4F, 0x01, 0x00, 0x00, 0x37,
 | 
			
		||||
        ),
 | 
			
		||||
        (0xB8, 0x25, 0x22, 0xF0, 0x63),
 | 
			
		||||
        (0xBF, 0x02, 0x11, 0x00),
 | 
			
		||||
        (0xB1, 0x00, 0x00, 0x00, 0xDA, 0x80),
 | 
			
		||||
        (0xB2, 0x3C, 0x12, 0x30),
 | 
			
		||||
        (0xB3, 0x10, 0x10, 0x28, 0x28, 0x03, 0xFF, 0x00, 0x00, 0x00, 0x00),
 | 
			
		||||
        (0xC0, 0x73, 0x73, 0x50, 0x50, 0x00, 0x00, 0x12, 0x70, 0x00),
 | 
			
		||||
        (0xBC, 0x46), (0xCC, 0x0B), (0xB4, 0x80), (0xB2, 0x3C, 0x12, 0x30),
 | 
			
		||||
        (0xE3, 0x07, 0x07, 0x0B, 0x0B, 0x03, 0x0B, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xC0, 0x10,),
 | 
			
		||||
        (0xC1, 0x36, 0x00, 0x32, 0x32, 0x77, 0xF1, 0xCC, 0xCC, 0x77, 0x77, 0x33, 0x33),
 | 
			
		||||
        (0xB4, 0x80),
 | 
			
		||||
        (0xB5, 0x0A, 0x0A),
 | 
			
		||||
        (0xB6, 0xB2, 0xB2),
 | 
			
		||||
        (
 | 
			
		||||
            0xE9, 0xC8, 0x10, 0x0A, 0x10, 0x0F, 0xA1, 0x80, 0x12, 0x31, 0x23, 0x47, 0x86, 0xA1, 0x80,
 | 
			
		||||
            0x47, 0x08, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0D, 0x00, 0x00, 0x00, 0x48,
 | 
			
		||||
            0x02, 0x8B, 0xAF, 0x46, 0x02, 0x88, 0x88, 0x88, 0x88, 0x88, 0x48, 0x13, 0x8B, 0xAF, 0x57,
 | 
			
		||||
            0x13, 0x88, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
 | 
			
		||||
            0x00, 0x00, 0x00, 0x00,
 | 
			
		||||
        ),
 | 
			
		||||
        (
 | 
			
		||||
            0xEA, 0x96, 0x12, 0x01, 0x01, 0x01, 0x78, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00, 0x4F, 0x31,
 | 
			
		||||
            0x8B, 0xA8, 0x31, 0x75, 0x88, 0x88, 0x88, 0x88, 0x88, 0x4F, 0x20, 0x8B, 0xA8, 0x20, 0x64,
 | 
			
		||||
            0x88, 0x88, 0x88, 0x88, 0x88, 0x23, 0x00, 0x00, 0x01, 0x02, 0x00, 0x00, 0x00, 0x00, 0x00,
 | 
			
		||||
            0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0xA1, 0x80, 0x00, 0x00,
 | 
			
		||||
            0x00, 0x00,
 | 
			
		||||
        ),
 | 
			
		||||
        (
 | 
			
		||||
            0xE0, 0x00, 0x0A, 0x0F, 0x29, 0x3B, 0x3F, 0x42, 0x39, 0x06, 0x0D, 0x10, 0x13, 0x15, 0x14,
 | 
			
		||||
            0x15, 0x10, 0x17, 0x00, 0x0A, 0x0F, 0x29, 0x3B, 0x3F, 0x42, 0x39, 0x06, 0x0D, 0x10, 0x13,
 | 
			
		||||
            0x15, 0x14, 0x15, 0x10, 0x17,
 | 
			
		||||
        ),
 | 
			
		||||
        (0xB6, 0x97, 0x97),
 | 
			
		||||
        (0xB8, 0x26, 0x22, 0xF0, 0x13),
 | 
			
		||||
        (0xBA, 0x31, 0x81, 0x0F, 0xF9, 0x0E, 0x06, 0x20, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x44, 0x25, 0x00, 0x90, 0x0A, 0x00, 0x00, 0x01, 0x4F, 0x01, 0x00, 0x00, 0x37),
 | 
			
		||||
        (0xBC, 0x47),
 | 
			
		||||
        (0xBF, 0x02, 0x11, 0x00),
 | 
			
		||||
        (0xC0, 0x73, 0x73, 0x50, 0x50, 0x00, 0x00, 0x12, 0x70, 0x00),
 | 
			
		||||
        (0xC1, 0x25, 0x00, 0x32, 0x32, 0x77, 0xE4, 0xFF, 0xFF, 0xCC, 0xCC, 0x77, 0x77),
 | 
			
		||||
        (0xC6, 0x82, 0x00, 0xBF, 0xFF, 0x00, 0xFF),
 | 
			
		||||
        (0xC7, 0xB8, 0x00, 0x0A, 0x10, 0x01, 0x09),
 | 
			
		||||
        (0xC8, 0x10, 0x40, 0x1E, 0x02),
 | 
			
		||||
        (0xCC, 0x0B),
 | 
			
		||||
        (0xE0, 0x00, 0x0B, 0x10, 0x2C, 0x3D, 0x3F, 0x42, 0x3A, 0x07, 0x0D, 0x0F, 0x13, 0x15, 0x13, 0x14, 0x0F, 0x16, 0x00, 0x0B, 0x10, 0x2C, 0x3D, 0x3F, 0x42, 0x3A, 0x07, 0x0D, 0x0F, 0x13, 0x15, 0x13, 0x14, 0x0F, 0x16),
 | 
			
		||||
        (0xE3, 0x07, 0x07, 0x0B, 0x0B, 0x0B, 0x0B, 0x00, 0x00, 0x00, 0x00, 0xFF, 0x00, 0xC0, 0x10),
 | 
			
		||||
        (0xE9, 0xC8, 0x10, 0x0A, 0x00, 0x00, 0x80, 0x81, 0x12, 0x31, 0x23, 0x4F, 0x86, 0xA0, 0x00, 0x47, 0x08, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0C, 0x00, 0x00, 0x00, 0x98, 0x02, 0x8B, 0xAF, 0x46, 0x02, 0x88, 0x88, 0x88, 0x88, 0x88, 0x98, 0x13, 0x8B, 0xAF, 0x57, 0x13, 0x88, 0x88, 0x88, 0x88, 0x88, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00),
 | 
			
		||||
        (0xEA, 0x97, 0x0C, 0x09, 0x09, 0x09, 0x78, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x9F, 0x31, 0x8B, 0xA8, 0x31, 0x75, 0x88, 0x88, 0x88, 0x88, 0x88, 0x9F, 0x20, 0x8B, 0xA8, 0x20, 0x64, 0x88, 0x88, 0x88, 0x88, 0x88, 0x23, 0x00, 0x00, 0x02, 0x71, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x40, 0x80, 0x81, 0x00, 0x00, 0x00, 0x00),
 | 
			
		||||
        (0xEF, 0xFF, 0xFF, 0x01),
 | 
			
		||||
        (0x11, 0x00),
 | 
			
		||||
        (0x29, 0x00),
 | 
			
		||||
    ],
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -63,6 +63,8 @@ SPIRAM_SPEEDS = {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def supported() -> bool:
 | 
			
		||||
    if not CORE.is_esp32:
 | 
			
		||||
        return False
 | 
			
		||||
    variant = get_esp32_variant()
 | 
			
		||||
    return variant in SPIRAM_MODES
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,7 @@ from pathlib import Path
 | 
			
		||||
 | 
			
		||||
from esphome import automation, external_files
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import audio, esp32, media_player, speaker
 | 
			
		||||
from esphome.components import audio, esp32, media_player, psram, speaker
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_BUFFER_SIZE,
 | 
			
		||||
@@ -26,10 +26,21 @@ from esphome.const import (
 | 
			
		||||
from esphome.core import CORE, HexInt
 | 
			
		||||
from esphome.core.entity_helpers import inherit_property_from
 | 
			
		||||
from esphome.external_files import download_content
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["audio", "psram"]
 | 
			
		||||
 | 
			
		||||
def AUTO_LOAD(config: ConfigType) -> list[str]:
 | 
			
		||||
    load = ["audio"]
 | 
			
		||||
    if (
 | 
			
		||||
        not config
 | 
			
		||||
        or config.get(CONF_TASK_STACK_IN_PSRAM)
 | 
			
		||||
        or config.get(CONF_CODEC_SUPPORT_ENABLED)
 | 
			
		||||
    ):
 | 
			
		||||
        return load + ["psram"]
 | 
			
		||||
    return load
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@kahrendt", "@synesthesiam"]
 | 
			
		||||
DOMAIN = "media_player"
 | 
			
		||||
@@ -279,7 +290,9 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
            cv.Optional(CONF_BUFFER_SIZE, default=1000000): cv.int_range(
 | 
			
		||||
                min=4000, max=4000000
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_CODEC_SUPPORT_ENABLED, default=True): cv.boolean,
 | 
			
		||||
            cv.Optional(
 | 
			
		||||
                CONF_CODEC_SUPPORT_ENABLED, default=psram.supported()
 | 
			
		||||
            ): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_FILES): cv.ensure_list(MEDIA_FILE_TYPE_SCHEMA),
 | 
			
		||||
            cv.Optional(CONF_TASK_STACK_IN_PSRAM, default=False): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_VOLUME_INCREMENT, default=0.05): cv.percentage,
 | 
			
		||||
 
 | 
			
		||||
@@ -9,6 +9,7 @@ from esphome.components.esp32 import (
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_DEVICES, CONF_ID
 | 
			
		||||
from esphome.cpp_types import Component
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["bytebuffer"]
 | 
			
		||||
CODEOWNERS = ["@clydebarrow"]
 | 
			
		||||
@@ -20,6 +21,7 @@ USBClient = usb_host_ns.class_("USBClient", Component)
 | 
			
		||||
CONF_VID = "vid"
 | 
			
		||||
CONF_PID = "pid"
 | 
			
		||||
CONF_ENABLE_HUBS = "enable_hubs"
 | 
			
		||||
CONF_MAX_TRANSFER_REQUESTS = "max_transfer_requests"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def usb_device_schema(cls=USBClient, vid: int = None, pid: [int] = None) -> cv.Schema:
 | 
			
		||||
@@ -44,6 +46,9 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(USBHost),
 | 
			
		||||
            cv.Optional(CONF_ENABLE_HUBS, default=False): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_MAX_TRANSFER_REQUESTS, default=16): cv.int_range(
 | 
			
		||||
                min=1, max=32
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_DEVICES): cv.ensure_list(usb_device_schema()),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
@@ -58,10 +63,14 @@ async def register_usb_client(config):
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
async def to_code(config: ConfigType) -> None:
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_USB_HOST_CONTROL_TRANSFER_MAX_SIZE", 1024)
 | 
			
		||||
    if config.get(CONF_ENABLE_HUBS):
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_USB_HOST_HUBS_SUPPORTED", True)
 | 
			
		||||
 | 
			
		||||
    max_requests = config[CONF_MAX_TRANSFER_REQUESTS]
 | 
			
		||||
    cg.add_define("USB_HOST_MAX_REQUESTS", max_requests)
 | 
			
		||||
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    for device in config.get(CONF_DEVICES) or ():
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
 | 
			
		||||
// Should not be needed, but it's required to pass CI clang-tidy checks
 | 
			
		||||
#if defined(USE_ESP32_VARIANT_ESP32S2) || defined(USE_ESP32_VARIANT_ESP32S3) || defined(USE_ESP32_VARIANT_ESP32P4)
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include "usb/usb_host.h"
 | 
			
		||||
@@ -16,23 +17,25 @@ namespace usb_host {
 | 
			
		||||
 | 
			
		||||
// THREADING MODEL:
 | 
			
		||||
// This component uses a dedicated USB task for event processing to prevent data loss.
 | 
			
		||||
// - USB Task (high priority): Handles USB events, executes transfer callbacks
 | 
			
		||||
// - Main Loop Task: Initiates transfers, processes completion events
 | 
			
		||||
// - USB Task (high priority): Handles USB events, executes transfer callbacks, releases transfer slots
 | 
			
		||||
// - Main Loop Task: Initiates transfers, processes device connect/disconnect events
 | 
			
		||||
//
 | 
			
		||||
// Thread-safe communication:
 | 
			
		||||
// - Lock-free queues for USB task -> main loop events (SPSC pattern)
 | 
			
		||||
// - Lock-free TransferRequest pool using atomic bitmask (MCSP pattern)
 | 
			
		||||
// - Lock-free TransferRequest pool using atomic bitmask (MCMP pattern - multi-consumer, multi-producer)
 | 
			
		||||
//
 | 
			
		||||
// TransferRequest pool access pattern:
 | 
			
		||||
// - get_trq_() [allocate]: Called from BOTH USB task and main loop threads
 | 
			
		||||
//   * USB task: via USB UART input callbacks that restart transfers immediately
 | 
			
		||||
//   * Main loop: for output transfers and flow-controlled input restarts
 | 
			
		||||
// - release_trq() [deallocate]: Called from main loop thread only
 | 
			
		||||
// - release_trq() [deallocate]: Called from BOTH USB task and main loop threads
 | 
			
		||||
//   * USB task: immediately after transfer callback completes (critical for preventing slot exhaustion)
 | 
			
		||||
//   * Main loop: when transfer submission fails
 | 
			
		||||
//
 | 
			
		||||
// The multi-threaded allocation is intentional for performance:
 | 
			
		||||
// - USB task can immediately restart input transfers without context switching
 | 
			
		||||
// The multi-threaded allocation/deallocation is intentional for performance:
 | 
			
		||||
// - USB task can immediately restart input transfers and release slots without context switching
 | 
			
		||||
// - Main loop controls backpressure by deciding when to restart after consuming data
 | 
			
		||||
// The atomic bitmask ensures thread-safe allocation without mutex blocking.
 | 
			
		||||
// The atomic bitmask ensures thread-safe allocation/deallocation without mutex blocking.
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "usb_host";
 | 
			
		||||
 | 
			
		||||
@@ -52,8 +55,17 @@ static const uint8_t USB_DIR_IN = 1 << 7;
 | 
			
		||||
static const uint8_t USB_DIR_OUT = 0;
 | 
			
		||||
static const size_t SETUP_PACKET_SIZE = 8;
 | 
			
		||||
 | 
			
		||||
static const size_t MAX_REQUESTS = 16;  // maximum number of outstanding requests possible.
 | 
			
		||||
static_assert(MAX_REQUESTS <= 16, "MAX_REQUESTS must be <= 16 to fit in uint16_t bitmask");
 | 
			
		||||
static const size_t MAX_REQUESTS = USB_HOST_MAX_REQUESTS;  // maximum number of outstanding requests possible.
 | 
			
		||||
static_assert(MAX_REQUESTS >= 1 && MAX_REQUESTS <= 32, "MAX_REQUESTS must be between 1 and 32");
 | 
			
		||||
 | 
			
		||||
// Select appropriate bitmask type for tracking allocation of TransferRequest slots.
 | 
			
		||||
// The bitmask must have at least as many bits as MAX_REQUESTS, so:
 | 
			
		||||
// - Use uint16_t for up to 16 requests (MAX_REQUESTS <= 16)
 | 
			
		||||
// - Use uint32_t for 17-32 requests (MAX_REQUESTS > 16)
 | 
			
		||||
// This is tied to the static_assert above, which enforces MAX_REQUESTS is between 1 and 32.
 | 
			
		||||
// If MAX_REQUESTS is increased above 32, this logic and the static_assert must be updated.
 | 
			
		||||
using trq_bitmask_t = std::conditional<(MAX_REQUESTS <= 16), uint16_t, uint32_t>::type;
 | 
			
		||||
 | 
			
		||||
static constexpr size_t USB_EVENT_QUEUE_SIZE = 32;   // Size of event queue between USB task and main loop
 | 
			
		||||
static constexpr size_t USB_TASK_STACK_SIZE = 4096;  // Stack size for USB task (same as ESP-IDF USB examples)
 | 
			
		||||
static constexpr UBaseType_t USB_TASK_PRIORITY = 5;  // Higher priority than main loop (tskIDLE_PRIORITY + 5)
 | 
			
		||||
@@ -83,8 +95,6 @@ struct TransferRequest {
 | 
			
		||||
enum EventType : uint8_t {
 | 
			
		||||
  EVENT_DEVICE_NEW,
 | 
			
		||||
  EVENT_DEVICE_GONE,
 | 
			
		||||
  EVENT_TRANSFER_COMPLETE,
 | 
			
		||||
  EVENT_CONTROL_COMPLETE,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct UsbEvent {
 | 
			
		||||
@@ -96,9 +106,6 @@ struct UsbEvent {
 | 
			
		||||
    struct {
 | 
			
		||||
      usb_device_handle_t handle;
 | 
			
		||||
    } device_gone;
 | 
			
		||||
    struct {
 | 
			
		||||
      TransferRequest *trq;
 | 
			
		||||
    } transfer;
 | 
			
		||||
  } data;
 | 
			
		||||
 | 
			
		||||
  // Required for EventPool - no cleanup needed for POD types
 | 
			
		||||
@@ -163,10 +170,9 @@ class USBClient : public Component {
 | 
			
		||||
  uint16_t pid_{};
 | 
			
		||||
  // Lock-free pool management using atomic bitmask (no dynamic allocation)
 | 
			
		||||
  // Bit i = 1: requests_[i] is in use, Bit i = 0: requests_[i] is available
 | 
			
		||||
  // Supports multiple concurrent consumers (both threads can allocate)
 | 
			
		||||
  // Single producer for deallocation (main loop only)
 | 
			
		||||
  // Limited to 16 slots by uint16_t size (enforced by static_assert)
 | 
			
		||||
  std::atomic<uint16_t> trq_in_use_;
 | 
			
		||||
  // Supports multiple concurrent consumers and producers (both threads can allocate/deallocate)
 | 
			
		||||
  // Bitmask type automatically selected: uint16_t for <= 16 slots, uint32_t for 17-32 slots
 | 
			
		||||
  std::atomic<trq_bitmask_t> trq_in_use_;
 | 
			
		||||
  TransferRequest requests_[MAX_REQUESTS]{};
 | 
			
		||||
};
 | 
			
		||||
class USBHost : public Component {
 | 
			
		||||
 
 | 
			
		||||
@@ -228,12 +228,6 @@ void USBClient::loop() {
 | 
			
		||||
      case EVENT_DEVICE_GONE:
 | 
			
		||||
        this->on_removed(event->data.device_gone.handle);
 | 
			
		||||
        break;
 | 
			
		||||
      case EVENT_TRANSFER_COMPLETE:
 | 
			
		||||
      case EVENT_CONTROL_COMPLETE: {
 | 
			
		||||
        auto *trq = event->data.transfer.trq;
 | 
			
		||||
        this->release_trq(trq);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Return event to pool for reuse
 | 
			
		||||
    this->event_pool.release(event);
 | 
			
		||||
@@ -313,25 +307,6 @@ void USBClient::on_removed(usb_device_handle_t handle) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Helper to queue transfer cleanup to main loop
 | 
			
		||||
static void queue_transfer_cleanup(TransferRequest *trq, EventType type) {
 | 
			
		||||
  auto *client = trq->client;
 | 
			
		||||
 | 
			
		||||
  // Allocate event from pool
 | 
			
		||||
  UsbEvent *event = client->event_pool.allocate();
 | 
			
		||||
  if (event == nullptr) {
 | 
			
		||||
    // No events available - increment counter for periodic logging
 | 
			
		||||
    client->event_queue.increment_dropped_count();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  event->type = type;
 | 
			
		||||
  event->data.transfer.trq = trq;
 | 
			
		||||
 | 
			
		||||
  // Push to lock-free queue (always succeeds since pool size == queue size)
 | 
			
		||||
  client->event_queue.push(event);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CALLBACK CONTEXT: USB task (called from usb_host_client_handle_events in USB task)
 | 
			
		||||
static void control_callback(const usb_transfer_t *xfer) {
 | 
			
		||||
  auto *trq = static_cast<TransferRequest *>(xfer->context);
 | 
			
		||||
@@ -346,8 +321,9 @@ static void control_callback(const usb_transfer_t *xfer) {
 | 
			
		||||
    trq->callback(trq->status);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Queue cleanup to main loop
 | 
			
		||||
  queue_transfer_cleanup(trq, EVENT_CONTROL_COMPLETE);
 | 
			
		||||
  // Release transfer slot immediately in USB task
 | 
			
		||||
  // The release_trq() uses thread-safe atomic operations
 | 
			
		||||
  trq->client->release_trq(trq);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// THREAD CONTEXT: Called from both USB task and main loop threads (multi-consumer)
 | 
			
		||||
@@ -358,20 +334,20 @@ static void control_callback(const usb_transfer_t *xfer) {
 | 
			
		||||
// This multi-threaded access is intentional for performance - USB task can
 | 
			
		||||
// immediately restart transfers without waiting for main loop scheduling.
 | 
			
		||||
TransferRequest *USBClient::get_trq_() {
 | 
			
		||||
  uint16_t mask = this->trq_in_use_.load(std::memory_order_relaxed);
 | 
			
		||||
  trq_bitmask_t mask = this->trq_in_use_.load(std::memory_order_relaxed);
 | 
			
		||||
 | 
			
		||||
  // Find first available slot (bit = 0) and try to claim it atomically
 | 
			
		||||
  // We use a while loop to allow retrying the same slot after CAS failure
 | 
			
		||||
  size_t i = 0;
 | 
			
		||||
  while (i != MAX_REQUESTS) {
 | 
			
		||||
    if (mask & (1U << i)) {
 | 
			
		||||
    if (mask & (static_cast<trq_bitmask_t>(1) << i)) {
 | 
			
		||||
      // Slot is in use, move to next slot
 | 
			
		||||
      i++;
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Slot i appears available, try to claim it atomically
 | 
			
		||||
    uint16_t desired = mask | (1U << i);  // Set bit i to mark as in-use
 | 
			
		||||
    trq_bitmask_t desired = mask | (static_cast<trq_bitmask_t>(1) << i);  // Set bit i to mark as in-use
 | 
			
		||||
 | 
			
		||||
    if (this->trq_in_use_.compare_exchange_weak(mask, desired, std::memory_order_acquire, std::memory_order_relaxed)) {
 | 
			
		||||
      // Successfully claimed slot i - prepare the TransferRequest
 | 
			
		||||
@@ -386,7 +362,7 @@ TransferRequest *USBClient::get_trq_() {
 | 
			
		||||
    i = 0;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGE(TAG, "All %d transfer slots in use", MAX_REQUESTS);
 | 
			
		||||
  ESP_LOGE(TAG, "All %zu transfer slots in use", MAX_REQUESTS);
 | 
			
		||||
  return nullptr;
 | 
			
		||||
}
 | 
			
		||||
void USBClient::disconnect() {
 | 
			
		||||
@@ -452,8 +428,11 @@ static void transfer_callback(usb_transfer_t *xfer) {
 | 
			
		||||
    trq->callback(trq->status);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Queue cleanup to main loop
 | 
			
		||||
  queue_transfer_cleanup(trq, EVENT_TRANSFER_COMPLETE);
 | 
			
		||||
  // Release transfer slot AFTER callback completes to prevent slot exhaustion
 | 
			
		||||
  // This is critical for high-throughput transfers (e.g., USB UART at 115200 baud)
 | 
			
		||||
  // The callback has finished accessing xfer->data_buffer, so it's safe to release
 | 
			
		||||
  // The release_trq() uses thread-safe atomic operations
 | 
			
		||||
  trq->client->release_trq(trq);
 | 
			
		||||
}
 | 
			
		||||
/**
 | 
			
		||||
 * Performs a transfer input operation.
 | 
			
		||||
@@ -521,12 +500,12 @@ void USBClient::dump_config() {
 | 
			
		||||
                "  Product id %04X",
 | 
			
		||||
                this->vid_, this->pid_);
 | 
			
		||||
}
 | 
			
		||||
// THREAD CONTEXT: Only called from main loop thread (single producer for deallocation)
 | 
			
		||||
// - Via event processing when handling EVENT_TRANSFER_COMPLETE/EVENT_CONTROL_COMPLETE
 | 
			
		||||
// - Directly when transfer submission fails
 | 
			
		||||
// THREAD CONTEXT: Called from both USB task and main loop threads
 | 
			
		||||
// - USB task: Immediately after transfer callback completes
 | 
			
		||||
// - Main loop: When transfer submission fails
 | 
			
		||||
//
 | 
			
		||||
// THREAD SAFETY: Lock-free using atomic AND to clear bit
 | 
			
		||||
// Single-producer pattern makes this simpler than allocation
 | 
			
		||||
// Thread-safe atomic operation allows multi-threaded deallocation
 | 
			
		||||
void USBClient::release_trq(TransferRequest *trq) {
 | 
			
		||||
  if (trq == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
@@ -540,8 +519,8 @@ void USBClient::release_trq(TransferRequest *trq) {
 | 
			
		||||
 | 
			
		||||
  // Atomically clear bit i to mark slot as available
 | 
			
		||||
  // fetch_and with inverted bitmask clears the bit atomically
 | 
			
		||||
  uint16_t bit = 1U << index;
 | 
			
		||||
  this->trq_in_use_.fetch_and(static_cast<uint16_t>(~bit), std::memory_order_release);
 | 
			
		||||
  trq_bitmask_t bit = static_cast<trq_bitmask_t>(1) << index;
 | 
			
		||||
  this->trq_in_use_.fetch_and(static_cast<trq_bitmask_t>(~bit), std::memory_order_release);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace usb_host
 | 
			
		||||
 
 | 
			
		||||
@@ -552,7 +552,7 @@ void WiFiComponent::start_scanning() {
 | 
			
		||||
// Using insertion sort instead of std::stable_sort saves flash memory
 | 
			
		||||
// by avoiding template instantiations (std::rotate, std::stable_sort, lambdas)
 | 
			
		||||
// IMPORTANT: This sort is stable (preserves relative order of equal elements)
 | 
			
		||||
static void insertion_sort_scan_results(FixedVector<WiFiScanResult> &results) {
 | 
			
		||||
static void insertion_sort_scan_results(std::vector<WiFiScanResult> &results) {
 | 
			
		||||
  const size_t size = results.size();
 | 
			
		||||
  for (size_t i = 1; i < size; i++) {
 | 
			
		||||
    // Make a copy to avoid issues with move semantics during comparison
 | 
			
		||||
@@ -576,8 +576,9 @@ __attribute__((noinline)) static void log_scan_result(const WiFiScanResult &res)
 | 
			
		||||
  format_mac_addr_upper(bssid.data(), bssid_s);
 | 
			
		||||
 | 
			
		||||
  if (res.get_matches()) {
 | 
			
		||||
    ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(), res.get_is_hidden() ? "(HIDDEN) " : "",
 | 
			
		||||
             bssid_s, LOG_STR_ARG(get_signal_bars(res.get_rssi())));
 | 
			
		||||
    ESP_LOGI(TAG, "- '%s' %s" LOG_SECRET("(%s) ") "%s", res.get_ssid().c_str(),
 | 
			
		||||
             res.get_is_hidden() ? LOG_STR_LITERAL("(HIDDEN) ") : LOG_STR_LITERAL(""), bssid_s,
 | 
			
		||||
             LOG_STR_ARG(get_signal_bars(res.get_rssi())));
 | 
			
		||||
    ESP_LOGD(TAG,
 | 
			
		||||
             "    Channel: %u\n"
 | 
			
		||||
             "    RSSI: %d dB",
 | 
			
		||||
 
 | 
			
		||||
@@ -278,7 +278,7 @@ class WiFiComponent : public Component {
 | 
			
		||||
  std::string get_use_address() const;
 | 
			
		||||
  void set_use_address(const std::string &use_address);
 | 
			
		||||
 | 
			
		||||
  const FixedVector<WiFiScanResult> &get_scan_result() const { return scan_result_; }
 | 
			
		||||
  const std::vector<WiFiScanResult> &get_scan_result() const { return scan_result_; }
 | 
			
		||||
 | 
			
		||||
  network::IPAddress wifi_soft_ap_ip();
 | 
			
		||||
 | 
			
		||||
@@ -385,7 +385,7 @@ class WiFiComponent : public Component {
 | 
			
		||||
  std::string use_address_;
 | 
			
		||||
  std::vector<WiFiAP> sta_;
 | 
			
		||||
  std::vector<WiFiSTAPriority> sta_priorities_;
 | 
			
		||||
  FixedVector<WiFiScanResult> scan_result_;
 | 
			
		||||
  std::vector<WiFiScanResult> scan_result_;
 | 
			
		||||
  WiFiAP selected_ap_;
 | 
			
		||||
  WiFiAP ap_;
 | 
			
		||||
  optional<float> output_power_;
 | 
			
		||||
 
 | 
			
		||||
@@ -696,15 +696,7 @@ void WiFiComponent::wifi_scan_done_callback_(void *arg, STATUS status) {
 | 
			
		||||
    this->retry_connect();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Count the number of results first
 | 
			
		||||
  auto *head = reinterpret_cast<bss_info *>(arg);
 | 
			
		||||
  size_t count = 0;
 | 
			
		||||
  for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
 | 
			
		||||
    count++;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->scan_result_.init(count);
 | 
			
		||||
  for (bss_info *it = head; it != nullptr; it = STAILQ_NEXT(it, next)) {
 | 
			
		||||
    WiFiScanResult res({it->bssid[0], it->bssid[1], it->bssid[2], it->bssid[3], it->bssid[4], it->bssid[5]},
 | 
			
		||||
                       std::string(reinterpret_cast<char *>(it->ssid), it->ssid_len), it->channel, it->rssi,
 | 
			
		||||
 
 | 
			
		||||
@@ -763,9 +763,8 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
 | 
			
		||||
    const auto &it = data->data.sta_scan_done;
 | 
			
		||||
    ESP_LOGV(TAG, "Scan done: status=%" PRIu32 " number=%u scan_id=%u", it.status, it.number, it.scan_id);
 | 
			
		||||
 | 
			
		||||
    this->scan_done_ = true;
 | 
			
		||||
    scan_result_.clear();
 | 
			
		||||
 | 
			
		||||
    this->scan_done_ = true;
 | 
			
		||||
    if (it.status != 0) {
 | 
			
		||||
      // scan error
 | 
			
		||||
      return;
 | 
			
		||||
@@ -785,7 +784,7 @@ void WiFiComponent::wifi_process_event_(IDFWiFiEvent *data) {
 | 
			
		||||
    }
 | 
			
		||||
    records.resize(number);
 | 
			
		||||
 | 
			
		||||
    scan_result_.init(number);
 | 
			
		||||
    scan_result_.reserve(number);
 | 
			
		||||
    for (int i = 0; i < number; i++) {
 | 
			
		||||
      auto &record = records[i];
 | 
			
		||||
      bssid_t bssid;
 | 
			
		||||
 
 | 
			
		||||
@@ -411,7 +411,7 @@ void WiFiComponent::wifi_scan_done_callback_() {
 | 
			
		||||
  if (num < 0)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  this->scan_result_.init(static_cast<unsigned int>(num));
 | 
			
		||||
  this->scan_result_.reserve(static_cast<unsigned int>(num));
 | 
			
		||||
  for (int i = 0; i < num; i++) {
 | 
			
		||||
    String ssid = WiFi.SSID(i);
 | 
			
		||||
    wifi_auth_mode_t authmode = WiFi.encryptionType(i);
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@ from enum import Enum
 | 
			
		||||
 | 
			
		||||
from esphome.enum import StrEnum
 | 
			
		||||
 | 
			
		||||
__version__ = "2025.11.0-dev"
 | 
			
		||||
__version__ = "2025.10.0"
 | 
			
		||||
 | 
			
		||||
ALLOWED_NAME_CHARS = "abcdefghijklmnopqrstuvwxyz0123456789-_"
 | 
			
		||||
VALID_SUBSTITUTIONS_CHARACTERS = (
 | 
			
		||||
 
 | 
			
		||||
@@ -340,8 +340,8 @@ void Application::calculate_looping_components_() {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Initialize FixedVector with exact size - no reallocation possible
 | 
			
		||||
  this->looping_components_.init(total_looping);
 | 
			
		||||
  // Pre-reserve vector to avoid reallocations
 | 
			
		||||
  this->looping_components_.reserve(total_looping);
 | 
			
		||||
 | 
			
		||||
  // Add all components with loop override that aren't already LOOP_DONE
 | 
			
		||||
  // Some components (like logger) may call disable_loop() during initialization
 | 
			
		||||
 
 | 
			
		||||
@@ -472,7 +472,7 @@ class Application {
 | 
			
		||||
  // - When a component is enabled, it's swapped with the first inactive component
 | 
			
		||||
  //   and active_end_ is incremented
 | 
			
		||||
  // - This eliminates branch mispredictions from flag checking in the hot loop
 | 
			
		||||
  FixedVector<Component *> looping_components_{};
 | 
			
		||||
  std::vector<Component *> looping_components_{};
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
  std::vector<int> socket_fds_;  // Vector of all monitored socket file descriptors
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -175,8 +175,6 @@
 | 
			
		||||
#define USE_ESP32_BLE_SERVER_DESCRIPTOR_ON_WRITE
 | 
			
		||||
#define USE_ESP32_BLE_SERVER_ON_CONNECT
 | 
			
		||||
#define USE_ESP32_BLE_SERVER_ON_DISCONNECT
 | 
			
		||||
#define ESPHOME_ESP32_BLE_TRACKER_LISTENER_COUNT 1
 | 
			
		||||
#define ESPHOME_ESP32_BLE_TRACKER_CLIENT_COUNT 1
 | 
			
		||||
#define USE_ESP32_CAMERA_JPEG_ENCODER
 | 
			
		||||
#define USE_I2C
 | 
			
		||||
#define USE_IMPROV
 | 
			
		||||
@@ -193,6 +191,7 @@
 | 
			
		||||
#define USE_WEBSERVER_PORT 80  // NOLINT
 | 
			
		||||
#define USE_WEBSERVER_SORTING
 | 
			
		||||
#define USE_WIFI_11KV_SUPPORT
 | 
			
		||||
#define USB_HOST_MAX_REQUESTS 16
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
#define USE_ARDUINO_VERSION_CODE VERSION_CODE(3, 2, 1)
 | 
			
		||||
 
 | 
			
		||||
@@ -159,80 +159,6 @@ template<typename T, size_t N> class StaticVector {
 | 
			
		||||
  const_reverse_iterator rend() const { return const_reverse_iterator(begin()); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Fixed-capacity vector - allocates once at runtime, never reallocates
 | 
			
		||||
/// This avoids std::vector template overhead (_M_realloc_insert, _M_default_append)
 | 
			
		||||
/// when size is known at initialization but not at compile time
 | 
			
		||||
template<typename T> class FixedVector {
 | 
			
		||||
 private:
 | 
			
		||||
  T *data_{nullptr};
 | 
			
		||||
  size_t size_{0};
 | 
			
		||||
  size_t capacity_{0};
 | 
			
		||||
 | 
			
		||||
  // Helper to destroy elements and free memory
 | 
			
		||||
  void cleanup_() {
 | 
			
		||||
    if (data_ != nullptr) {
 | 
			
		||||
      // Manually destroy all elements
 | 
			
		||||
      for (size_t i = 0; i < size_; i++) {
 | 
			
		||||
        data_[i].~T();
 | 
			
		||||
      }
 | 
			
		||||
      // Free raw memory
 | 
			
		||||
      ::operator delete(data_);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
  FixedVector() = default;
 | 
			
		||||
 | 
			
		||||
  ~FixedVector() { cleanup_(); }
 | 
			
		||||
 | 
			
		||||
  // Disable copy to avoid accidental copies
 | 
			
		||||
  FixedVector(const FixedVector &) = delete;
 | 
			
		||||
  FixedVector &operator=(const FixedVector &) = delete;
 | 
			
		||||
 | 
			
		||||
  // Allocate capacity - can be called multiple times to reinit
 | 
			
		||||
  void init(size_t n) {
 | 
			
		||||
    cleanup_();
 | 
			
		||||
    data_ = nullptr;
 | 
			
		||||
    capacity_ = 0;
 | 
			
		||||
    size_ = 0;
 | 
			
		||||
    if (n > 0) {
 | 
			
		||||
      // Allocate raw memory without calling constructors
 | 
			
		||||
      data_ = static_cast<T *>(::operator new(n * sizeof(T)));
 | 
			
		||||
      capacity_ = n;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Clear the vector (reset size to 0, keep capacity)
 | 
			
		||||
  void clear() { size_ = 0; }
 | 
			
		||||
 | 
			
		||||
  // Check if vector is empty
 | 
			
		||||
  bool empty() const { return size_ == 0; }
 | 
			
		||||
 | 
			
		||||
  /// Add element without bounds checking
 | 
			
		||||
  /// Caller must ensure sufficient capacity was allocated via init()
 | 
			
		||||
  /// Silently ignores pushes beyond capacity (no exception or assertion)
 | 
			
		||||
  void push_back(const T &value) {
 | 
			
		||||
    if (size_ < capacity_) {
 | 
			
		||||
      // Use placement new to construct the object in pre-allocated memory
 | 
			
		||||
      new (&data_[size_]) T(value);
 | 
			
		||||
      size_++;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  size_t size() const { return size_; }
 | 
			
		||||
 | 
			
		||||
  /// Access element without bounds checking (matches std::vector behavior)
 | 
			
		||||
  /// Caller must ensure index is valid (i < size())
 | 
			
		||||
  T &operator[](size_t i) { return data_[i]; }
 | 
			
		||||
  const T &operator[](size_t i) const { return data_[i]; }
 | 
			
		||||
 | 
			
		||||
  // Iterator support for range-based for loops
 | 
			
		||||
  T *begin() { return data_; }
 | 
			
		||||
  T *end() { return data_ + size_; }
 | 
			
		||||
  const T *begin() const { return data_; }
 | 
			
		||||
  const T *end() const { return data_ + size_; }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
///@}
 | 
			
		||||
 | 
			
		||||
/// @name Mathematics
 | 
			
		||||
 
 | 
			
		||||
@@ -410,7 +410,7 @@ def run_ota_impl_(
 | 
			
		||||
        af, socktype, _, _, sa = r
 | 
			
		||||
        _LOGGER.info("Connecting to %s port %s...", sa[0], sa[1])
 | 
			
		||||
        sock = socket.socket(af, socktype)
 | 
			
		||||
        sock.settimeout(10.0)
 | 
			
		||||
        sock.settimeout(20.0)
 | 
			
		||||
        try:
 | 
			
		||||
            sock.connect(sa)
 | 
			
		||||
        except OSError as err:
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,8 @@ from esphome.const import (
 | 
			
		||||
from esphome.core import CORE, EsphomeError
 | 
			
		||||
from esphome.helpers import (
 | 
			
		||||
    copy_file_if_changed,
 | 
			
		||||
    get_str_env,
 | 
			
		||||
    is_ha_addon,
 | 
			
		||||
    read_file,
 | 
			
		||||
    walk_files,
 | 
			
		||||
    write_file_if_changed,
 | 
			
		||||
@@ -338,16 +340,21 @@ def clean_build():
 | 
			
		||||
def clean_all(configuration: list[str]):
 | 
			
		||||
    import shutil
 | 
			
		||||
 | 
			
		||||
    # Clean entire build dir
 | 
			
		||||
    for dir in configuration:
 | 
			
		||||
        build_dir = Path(dir) / ".esphome"
 | 
			
		||||
        if build_dir.is_dir():
 | 
			
		||||
            _LOGGER.info("Cleaning %s", build_dir)
 | 
			
		||||
            # Don't remove storage as it will cause the dashboard to regenerate all configs
 | 
			
		||||
            for item in build_dir.iterdir():
 | 
			
		||||
                if item.is_file():
 | 
			
		||||
    data_dirs = [Path(dir) / ".esphome" for dir in configuration]
 | 
			
		||||
    if is_ha_addon():
 | 
			
		||||
        data_dirs.append(Path("/data"))
 | 
			
		||||
    if "ESPHOME_DATA_DIR" in os.environ:
 | 
			
		||||
        data_dirs.append(Path(get_str_env("ESPHOME_DATA_DIR", None)))
 | 
			
		||||
 | 
			
		||||
    # Clean build dir
 | 
			
		||||
    for dir in data_dirs:
 | 
			
		||||
        if dir.is_dir():
 | 
			
		||||
            _LOGGER.info("Cleaning %s", dir)
 | 
			
		||||
            # Don't remove storage or .json files which are needed by the dashboard
 | 
			
		||||
            for item in dir.iterdir():
 | 
			
		||||
                if item.is_file() and not item.name.endswith(".json"):
 | 
			
		||||
                    item.unlink()
 | 
			
		||||
                elif item.name != "storage" and item.is_dir():
 | 
			
		||||
                elif item.is_dir() and item.name != "storage":
 | 
			
		||||
                    shutil.rmtree(item)
 | 
			
		||||
 | 
			
		||||
    # Clean PlatformIO project files
 | 
			
		||||
 
 | 
			
		||||
@@ -1,3 +1,4 @@
 | 
			
		||||
[build]
 | 
			
		||||
  command = "script/build-api-docs"
 | 
			
		||||
  publish = "api-docs"
 | 
			
		||||
  environment = { PYTHON_VERSION = "3.13" }
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,8 @@ pyserial==3.5
 | 
			
		||||
platformio==6.1.18  # When updating platformio, also update /docker/Dockerfile
 | 
			
		||||
esptool==5.1.0
 | 
			
		||||
click==8.1.7
 | 
			
		||||
esphome-dashboard==20251009.0
 | 
			
		||||
aioesphomeapi==41.13.0
 | 
			
		||||
esphome-dashboard==20251013.0
 | 
			
		||||
aioesphomeapi==41.16.1
 | 
			
		||||
zeroconf==0.148.0
 | 
			
		||||
puremagic==1.30
 | 
			
		||||
ruamel.yaml==0.18.15 # dashboard_import
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
pylint==3.3.9
 | 
			
		||||
flake8==7.3.0  # also change in .pre-commit-config.yaml when updating
 | 
			
		||||
ruff==0.14.0  # also change in .pre-commit-config.yaml when updating
 | 
			
		||||
pyupgrade==3.21.0  # also change in .pre-commit-config.yaml when updating
 | 
			
		||||
pyupgrade==3.20.0  # also change in .pre-commit-config.yaml when updating
 | 
			
		||||
pre-commit
 | 
			
		||||
 | 
			
		||||
# Unit tests
 | 
			
		||||
 
 | 
			
		||||
@@ -40,9 +40,7 @@ display:
 | 
			
		||||
        - number: 17
 | 
			
		||||
      blue:
 | 
			
		||||
        - number: 47
 | 
			
		||||
          allow_other_uses: true
 | 
			
		||||
        - number: 41
 | 
			
		||||
          allow_other_uses: true
 | 
			
		||||
        - number: 1
 | 
			
		||||
        - number: 0
 | 
			
		||||
          ignore_strapping_warning: true
 | 
			
		||||
        - number: 42
 | 
			
		||||
@@ -53,7 +51,7 @@ display:
 | 
			
		||||
      number: 45
 | 
			
		||||
      ignore_strapping_warning: true
 | 
			
		||||
    hsync_pin:
 | 
			
		||||
      number: 40
 | 
			
		||||
      number: 38
 | 
			
		||||
    vsync_pin:
 | 
			
		||||
      number: 48
 | 
			
		||||
    data_rate: 1000000.0
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
substitutions:
 | 
			
		||||
  tx_pin: GPIO4
 | 
			
		||||
  rx_pin: GPIO5
 | 
			
		||||
  flow_control_pin: GPIO13
 | 
			
		||||
 | 
			
		||||
packages:
 | 
			
		||||
  modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
substitutions:
 | 
			
		||||
  tx_pin: GPIO4
 | 
			
		||||
  rx_pin: GPIO5
 | 
			
		||||
  flow_control_pin: GPIO13
 | 
			
		||||
 | 
			
		||||
packages:
 | 
			
		||||
  modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
substitutions:
 | 
			
		||||
  tx_pin: GPIO4
 | 
			
		||||
  rx_pin: GPIO5
 | 
			
		||||
  flow_control_pin: GPIO13
 | 
			
		||||
 | 
			
		||||
packages:
 | 
			
		||||
  modbus: !include ../../test_build_components/common/modbus/esp32-idf.yaml
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
usb_host:
 | 
			
		||||
  max_transfer_requests: 32  # Test uint32_t bitmask path (17-32 requests)
 | 
			
		||||
  devices:
 | 
			
		||||
    - id: device_1
 | 
			
		||||
      vid: 0x1234
 | 
			
		||||
 
 | 
			
		||||
@@ -493,7 +493,7 @@ def test_run_ota_impl_successful(
 | 
			
		||||
    assert result_host == "192.168.1.100"
 | 
			
		||||
 | 
			
		||||
    # Verify socket was configured correctly
 | 
			
		||||
    mock_socket.settimeout.assert_called_with(10.0)
 | 
			
		||||
    mock_socket.settimeout.assert_called_with(20.0)
 | 
			
		||||
    mock_socket.connect.assert_called_once_with(("192.168.1.100", 3232))
 | 
			
		||||
    mock_socket.close.assert_called_once()
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1203,6 +1203,31 @@ def test_show_logs_api(
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@patch("esphome.components.api.client.run_logs")
 | 
			
		||||
def test_show_logs_api_with_fqdn_mdns_disabled(
 | 
			
		||||
    mock_run_logs: Mock,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test show_logs with API using FQDN when mDNS is disabled."""
 | 
			
		||||
    setup_core(
 | 
			
		||||
        config={
 | 
			
		||||
            "logger": {},
 | 
			
		||||
            CONF_API: {},
 | 
			
		||||
            CONF_MDNS: {CONF_DISABLED: True},
 | 
			
		||||
        },
 | 
			
		||||
        platform=PLATFORM_ESP32,
 | 
			
		||||
    )
 | 
			
		||||
    mock_run_logs.return_value = 0
 | 
			
		||||
 | 
			
		||||
    args = MockArgs()
 | 
			
		||||
    devices = ["device.example.com"]
 | 
			
		||||
 | 
			
		||||
    result = show_logs(CORE.config, args, devices)
 | 
			
		||||
 | 
			
		||||
    assert result == 0
 | 
			
		||||
    # Should use the FQDN directly, not try MQTT lookup
 | 
			
		||||
    mock_run_logs.assert_called_once_with(CORE.config, ["device.example.com"])
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@patch("esphome.components.api.client.run_logs")
 | 
			
		||||
def test_show_logs_api_with_mqtt_fallback(
 | 
			
		||||
    mock_run_logs: Mock,
 | 
			
		||||
@@ -1222,7 +1247,7 @@ def test_show_logs_api_with_mqtt_fallback(
 | 
			
		||||
    mock_mqtt_get_ip.return_value = ["192.168.1.200"]
 | 
			
		||||
 | 
			
		||||
    args = MockArgs(username="user", password="pass", client_id="client")
 | 
			
		||||
    devices = ["device.local"]
 | 
			
		||||
    devices = ["MQTTIP"]
 | 
			
		||||
 | 
			
		||||
    result = show_logs(CORE.config, args, devices)
 | 
			
		||||
 | 
			
		||||
@@ -1487,27 +1512,31 @@ def test_mqtt_get_ip() -> None:
 | 
			
		||||
def test_has_resolvable_address() -> None:
 | 
			
		||||
    """Test has_resolvable_address function."""
 | 
			
		||||
 | 
			
		||||
    # Test with mDNS enabled and hostname address
 | 
			
		||||
    # Test with mDNS enabled and .local hostname address
 | 
			
		||||
    setup_core(config={}, address="esphome-device.local")
 | 
			
		||||
    assert has_resolvable_address() is True
 | 
			
		||||
 | 
			
		||||
    # Test with mDNS disabled and hostname address
 | 
			
		||||
    # Test with mDNS disabled and .local hostname address (still resolvable via DNS)
 | 
			
		||||
    setup_core(
 | 
			
		||||
        config={CONF_MDNS: {CONF_DISABLED: True}}, address="esphome-device.local"
 | 
			
		||||
    )
 | 
			
		||||
    assert has_resolvable_address() is False
 | 
			
		||||
    assert has_resolvable_address() is True
 | 
			
		||||
 | 
			
		||||
    # Test with IP address (mDNS doesn't matter)
 | 
			
		||||
    # Test with mDNS disabled and regular DNS hostname (resolvable)
 | 
			
		||||
    setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address="device.example.com")
 | 
			
		||||
    assert has_resolvable_address() is True
 | 
			
		||||
 | 
			
		||||
    # Test with IP address (always resolvable, mDNS doesn't matter)
 | 
			
		||||
    setup_core(config={}, address="192.168.1.100")
 | 
			
		||||
    assert has_resolvable_address() is True
 | 
			
		||||
 | 
			
		||||
    # Test with IP address and mDNS disabled
 | 
			
		||||
    # Test with IP address and mDNS disabled (still resolvable)
 | 
			
		||||
    setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address="192.168.1.100")
 | 
			
		||||
    assert has_resolvable_address() is True
 | 
			
		||||
 | 
			
		||||
    # Test with no address but mDNS enabled (can still resolve mDNS names)
 | 
			
		||||
    # Test with no address
 | 
			
		||||
    setup_core(config={}, address=None)
 | 
			
		||||
    assert has_resolvable_address() is True
 | 
			
		||||
    assert has_resolvable_address() is False
 | 
			
		||||
 | 
			
		||||
    # Test with no address and mDNS disabled
 | 
			
		||||
    setup_core(config={CONF_MDNS: {CONF_DISABLED: True}}, address=None)
 | 
			
		||||
 
 | 
			
		||||
@@ -985,3 +985,49 @@ def test_clean_all_removes_non_storage_directories(
 | 
			
		||||
    # Verify logging mentions cleaning
 | 
			
		||||
    assert "Cleaning" in caplog.text
 | 
			
		||||
    assert str(build_dir) in caplog.text
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@patch("esphome.writer.CORE")
 | 
			
		||||
def test_clean_all_preserves_json_files(
 | 
			
		||||
    mock_core: MagicMock,
 | 
			
		||||
    tmp_path: Path,
 | 
			
		||||
    caplog: pytest.LogCaptureFixture,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Test clean_all preserves .json files."""
 | 
			
		||||
    # Create build directory with various files
 | 
			
		||||
    config_dir = tmp_path / "config"
 | 
			
		||||
    config_dir.mkdir()
 | 
			
		||||
 | 
			
		||||
    build_dir = config_dir / ".esphome"
 | 
			
		||||
    build_dir.mkdir()
 | 
			
		||||
 | 
			
		||||
    # Create .json files (should be preserved)
 | 
			
		||||
    (build_dir / "config.json").write_text('{"config": "data"}')
 | 
			
		||||
    (build_dir / "metadata.json").write_text('{"metadata": "info"}')
 | 
			
		||||
 | 
			
		||||
    # Create non-.json files (should be removed)
 | 
			
		||||
    (build_dir / "dummy.txt").write_text("x")
 | 
			
		||||
    (build_dir / "other.log").write_text("log content")
 | 
			
		||||
 | 
			
		||||
    # Call clean_all
 | 
			
		||||
    from esphome.writer import clean_all
 | 
			
		||||
 | 
			
		||||
    with caplog.at_level("INFO"):
 | 
			
		||||
        clean_all([str(config_dir)])
 | 
			
		||||
 | 
			
		||||
    # Verify .esphome directory still exists
 | 
			
		||||
    assert build_dir.exists()
 | 
			
		||||
 | 
			
		||||
    # Verify .json files are preserved
 | 
			
		||||
    assert (build_dir / "config.json").exists()
 | 
			
		||||
    assert (build_dir / "config.json").read_text() == '{"config": "data"}'
 | 
			
		||||
    assert (build_dir / "metadata.json").exists()
 | 
			
		||||
    assert (build_dir / "metadata.json").read_text() == '{"metadata": "info"}'
 | 
			
		||||
 | 
			
		||||
    # Verify non-.json files were removed
 | 
			
		||||
    assert not (build_dir / "dummy.txt").exists()
 | 
			
		||||
    assert not (build_dir / "other.log").exists()
 | 
			
		||||
 | 
			
		||||
    # Verify logging mentions cleaning
 | 
			
		||||
    assert "Cleaning" in caplog.text
 | 
			
		||||
    assert str(build_dir) in caplog.text
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user