mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 00:51:49 +00:00 
			
		
		
		
	Compare commits
	
		
			2 Commits
		
	
	
		
			fan_no_dou
			...
			jesserockz
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					6bf78e2e82 | ||
| 
						 | 
					817ee70db0 | 
@@ -1 +1 @@
 | 
			
		||||
3d46b63015d761c85ca9cb77ab79a389509e5776701fb22aed16e7b79d432c0c
 | 
			
		||||
d7693a1e996cacd4a3d1c9a16336799c2a8cc3db02e4e74084151ce964581248
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
[run]
 | 
			
		||||
omit =
 | 
			
		||||
    esphome/components/*
 | 
			
		||||
    esphome/analyze_memory/*
 | 
			
		||||
    tests/integration/*
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										22
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							@@ -53,7 +53,6 @@ jobs:
 | 
			
		||||
              'new-target-platform',
 | 
			
		||||
              'merging-to-release',
 | 
			
		||||
              'merging-to-beta',
 | 
			
		||||
              'chained-pr',
 | 
			
		||||
              'core',
 | 
			
		||||
              'small-pr',
 | 
			
		||||
              'dashboard',
 | 
			
		||||
@@ -141,8 +140,6 @@ jobs:
 | 
			
		||||
                labels.add('merging-to-release');
 | 
			
		||||
              } else if (baseRef === 'beta') {
 | 
			
		||||
                labels.add('merging-to-beta');
 | 
			
		||||
              } else if (baseRef !== 'dev') {
 | 
			
		||||
                labels.add('chained-pr');
 | 
			
		||||
              }
 | 
			
		||||
 | 
			
		||||
              return labels;
 | 
			
		||||
@@ -416,7 +413,7 @@ jobs:
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Generate review messages
 | 
			
		||||
            function generateReviewMessages(finalLabels, originalLabelCount) {
 | 
			
		||||
            function generateReviewMessages(finalLabels) {
 | 
			
		||||
              const messages = [];
 | 
			
		||||
              const prAuthor = context.payload.pull_request.user.login;
 | 
			
		||||
 | 
			
		||||
@@ -430,15 +427,15 @@ jobs:
 | 
			
		||||
                  .reduce((sum, file) => sum + (file.deletions || 0), 0);
 | 
			
		||||
                const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
 | 
			
		||||
 | 
			
		||||
                const tooManyLabels = originalLabelCount > MAX_LABELS;
 | 
			
		||||
                const tooManyLabels = finalLabels.length > MAX_LABELS;
 | 
			
		||||
                const tooManyChanges = nonTestChanges > TOO_BIG_THRESHOLD;
 | 
			
		||||
 | 
			
		||||
                let message = `${TOO_BIG_MARKER}\n### 📦 Pull Request Size\n\n`;
 | 
			
		||||
 | 
			
		||||
                if (tooManyLabels && tooManyChanges) {
 | 
			
		||||
                  message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`;
 | 
			
		||||
                  message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${finalLabels.length} different components/areas.`;
 | 
			
		||||
                } else if (tooManyLabels) {
 | 
			
		||||
                  message += `This PR affects ${originalLabelCount} different components/areas.`;
 | 
			
		||||
                  message += `This PR affects ${finalLabels.length} different components/areas.`;
 | 
			
		||||
                } else {
 | 
			
		||||
                  message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`;
 | 
			
		||||
                }
 | 
			
		||||
@@ -466,8 +463,8 @@ jobs:
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Handle reviews
 | 
			
		||||
            async function handleReviews(finalLabels, originalLabelCount) {
 | 
			
		||||
              const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount);
 | 
			
		||||
            async function handleReviews(finalLabels) {
 | 
			
		||||
              const reviewMessages = generateReviewMessages(finalLabels);
 | 
			
		||||
              const hasReviewableLabels = finalLabels.some(label =>
 | 
			
		||||
                ['too-big', 'needs-codeowners'].includes(label)
 | 
			
		||||
              );
 | 
			
		||||
@@ -531,8 +528,8 @@ jobs:
 | 
			
		||||
            const apiData = await fetchApiData();
 | 
			
		||||
            const baseRef = context.payload.pull_request.base.ref;
 | 
			
		||||
 | 
			
		||||
            // Early exit for release and beta branches only
 | 
			
		||||
            if (baseRef === 'release' || baseRef === 'beta') {
 | 
			
		||||
            // Early exit for non-dev branches
 | 
			
		||||
            if (baseRef !== 'dev') {
 | 
			
		||||
              const branchLabels = await detectMergeBranch();
 | 
			
		||||
              const finalLabels = Array.from(branchLabels);
 | 
			
		||||
 | 
			
		||||
@@ -627,7 +624,6 @@ jobs:
 | 
			
		||||
 | 
			
		||||
            // Handle too many labels (only for non-mega PRs)
 | 
			
		||||
            const tooManyLabels = finalLabels.length > MAX_LABELS;
 | 
			
		||||
            const originalLabelCount = finalLabels.length;
 | 
			
		||||
 | 
			
		||||
            if (tooManyLabels && !isMegaPR && !finalLabels.includes('too-big')) {
 | 
			
		||||
              finalLabels = ['too-big'];
 | 
			
		||||
@@ -636,7 +632,7 @@ jobs:
 | 
			
		||||
            console.log('Computed labels:', finalLabels.join(', '));
 | 
			
		||||
 | 
			
		||||
            // Handle reviews
 | 
			
		||||
            await handleReviews(finalLabels, originalLabelCount);
 | 
			
		||||
            await handleReviews(finalLabels);
 | 
			
		||||
 | 
			
		||||
            // Apply labels
 | 
			
		||||
            if (finalLabels.length > 0) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							@@ -62,7 +62,7 @@ jobs:
 | 
			
		||||
        run: git diff
 | 
			
		||||
      - if: failure()
 | 
			
		||||
        name: Archive artifacts
 | 
			
		||||
        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
 | 
			
		||||
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
 | 
			
		||||
        with:
 | 
			
		||||
          name: generated-proto-files
 | 
			
		||||
          path: |
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										111
									
								
								.github/workflows/ci-memory-impact-comment.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										111
									
								
								.github/workflows/ci-memory-impact-comment.yml
									
									
									
									
										vendored
									
									
								
							@@ -1,111 +0,0 @@
 | 
			
		||||
---
 | 
			
		||||
name: Memory Impact Comment (Forks)
 | 
			
		||||
 | 
			
		||||
on:
 | 
			
		||||
  workflow_run:
 | 
			
		||||
    workflows: ["CI"]
 | 
			
		||||
    types: [completed]
 | 
			
		||||
 | 
			
		||||
permissions:
 | 
			
		||||
  contents: read
 | 
			
		||||
  pull-requests: write
 | 
			
		||||
  actions: read
 | 
			
		||||
 | 
			
		||||
jobs:
 | 
			
		||||
  memory-impact-comment:
 | 
			
		||||
    name: Post memory impact comment (fork PRs only)
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    # Only run for PRs from forks that had successful CI runs
 | 
			
		||||
    if: >
 | 
			
		||||
      github.event.workflow_run.event == 'pull_request' &&
 | 
			
		||||
      github.event.workflow_run.conclusion == 'success' &&
 | 
			
		||||
      github.event.workflow_run.head_repository.full_name != github.repository
 | 
			
		||||
    env:
 | 
			
		||||
      GH_TOKEN: ${{ github.token }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Get PR details
 | 
			
		||||
        id: pr
 | 
			
		||||
        run: |
 | 
			
		||||
          # Get PR details by searching for PR with matching head SHA
 | 
			
		||||
          # The workflow_run.pull_requests field is often empty for forks
 | 
			
		||||
          # Use paginate to handle repos with many open PRs
 | 
			
		||||
          head_sha="${{ github.event.workflow_run.head_sha }}"
 | 
			
		||||
          pr_data=$(gh api --paginate "/repos/${{ github.repository }}/pulls" \
 | 
			
		||||
            --jq ".[] | select(.head.sha == \"$head_sha\") | {number: .number, base_ref: .base.ref}" \
 | 
			
		||||
            | head -n 1)
 | 
			
		||||
 | 
			
		||||
          if [ -z "$pr_data" ]; then
 | 
			
		||||
            echo "No PR found for SHA $head_sha, skipping"
 | 
			
		||||
            echo "skip=true" >> "$GITHUB_OUTPUT"
 | 
			
		||||
            exit 0
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
          pr_number=$(echo "$pr_data" | jq -r '.number')
 | 
			
		||||
          base_ref=$(echo "$pr_data" | jq -r '.base_ref')
 | 
			
		||||
 | 
			
		||||
          echo "pr_number=$pr_number" >> "$GITHUB_OUTPUT"
 | 
			
		||||
          echo "base_ref=$base_ref" >> "$GITHUB_OUTPUT"
 | 
			
		||||
          echo "Found PR #$pr_number targeting base branch: $base_ref"
 | 
			
		||||
 | 
			
		||||
      - name: Check out code from base repository
 | 
			
		||||
        if: steps.pr.outputs.skip != 'true'
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          # Always check out from the base repository (esphome/esphome), never from forks
 | 
			
		||||
          # Use the PR's target branch to ensure we run trusted code from the main repo
 | 
			
		||||
          repository: ${{ github.repository }}
 | 
			
		||||
          ref: ${{ steps.pr.outputs.base_ref }}
 | 
			
		||||
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        if: steps.pr.outputs.skip != 'true'
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: "3.11"
 | 
			
		||||
          cache-key: ${{ hashFiles('.cache-key') }}
 | 
			
		||||
 | 
			
		||||
      - name: Download memory analysis artifacts
 | 
			
		||||
        if: steps.pr.outputs.skip != 'true'
 | 
			
		||||
        run: |
 | 
			
		||||
          run_id="${{ github.event.workflow_run.id }}"
 | 
			
		||||
          echo "Downloading artifacts from workflow run $run_id"
 | 
			
		||||
 | 
			
		||||
          mkdir -p memory-analysis
 | 
			
		||||
 | 
			
		||||
          # Download target analysis artifact
 | 
			
		||||
          if gh run download --name "memory-analysis-target" --dir memory-analysis --repo "${{ github.repository }}" "$run_id"; then
 | 
			
		||||
            echo "Downloaded memory-analysis-target artifact."
 | 
			
		||||
          else
 | 
			
		||||
            echo "No memory-analysis-target artifact found."
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
          # Download PR analysis artifact
 | 
			
		||||
          if gh run download --name "memory-analysis-pr" --dir memory-analysis --repo "${{ github.repository }}" "$run_id"; then
 | 
			
		||||
            echo "Downloaded memory-analysis-pr artifact."
 | 
			
		||||
          else
 | 
			
		||||
            echo "No memory-analysis-pr artifact found."
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - name: Check if artifacts exist
 | 
			
		||||
        id: check
 | 
			
		||||
        if: steps.pr.outputs.skip != 'true'
 | 
			
		||||
        run: |
 | 
			
		||||
          if [ -f ./memory-analysis/memory-analysis-target.json ] && [ -f ./memory-analysis/memory-analysis-pr.json ]; then
 | 
			
		||||
            echo "found=true" >> "$GITHUB_OUTPUT"
 | 
			
		||||
          else
 | 
			
		||||
            echo "found=false" >> "$GITHUB_OUTPUT"
 | 
			
		||||
            echo "Memory analysis artifacts not found, skipping comment"
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - name: Post or update PR comment
 | 
			
		||||
        if: steps.pr.outputs.skip != 'true' && steps.check.outputs.found == 'true'
 | 
			
		||||
        env:
 | 
			
		||||
          PR_NUMBER: ${{ steps.pr.outputs.pr_number }}
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          # Pass PR number and JSON file paths directly to Python script
 | 
			
		||||
          # Let Python parse the JSON to avoid shell injection risks
 | 
			
		||||
          # The script will validate and sanitize all inputs
 | 
			
		||||
          python script/ci_memory_impact_comment.py \
 | 
			
		||||
            --pr-number "$PR_NUMBER" \
 | 
			
		||||
            --target-json ./memory-analysis/memory-analysis-target.json \
 | 
			
		||||
            --pr-json ./memory-analysis/memory-analysis-pr.json
 | 
			
		||||
							
								
								
									
										501
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										501
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@@ -114,7 +114,7 @@ jobs:
 | 
			
		||||
      matrix:
 | 
			
		||||
        python-version:
 | 
			
		||||
          - "3.11"
 | 
			
		||||
          - "3.13"
 | 
			
		||||
          - "3.14"
 | 
			
		||||
        os:
 | 
			
		||||
          - ubuntu-latest
 | 
			
		||||
          - macOS-latest
 | 
			
		||||
@@ -123,9 +123,9 @@ jobs:
 | 
			
		||||
          # Minimize CI resource usage
 | 
			
		||||
          # by only running the Python version
 | 
			
		||||
          # version used for docker images on Windows and macOS
 | 
			
		||||
          - python-version: "3.13"
 | 
			
		||||
          - python-version: "3.14"
 | 
			
		||||
            os: windows-latest
 | 
			
		||||
          - python-version: "3.13"
 | 
			
		||||
          - python-version: "3.14"
 | 
			
		||||
            os: macOS-latest
 | 
			
		||||
    runs-on: ${{ matrix.os }}
 | 
			
		||||
    needs:
 | 
			
		||||
@@ -170,17 +170,11 @@ jobs:
 | 
			
		||||
    outputs:
 | 
			
		||||
      integration-tests: ${{ steps.determine.outputs.integration-tests }}
 | 
			
		||||
      clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
 | 
			
		||||
      clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
 | 
			
		||||
      python-linters: ${{ steps.determine.outputs.python-linters }}
 | 
			
		||||
      changed-components: ${{ steps.determine.outputs.changed-components }}
 | 
			
		||||
      changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
 | 
			
		||||
      directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
 | 
			
		||||
      component-test-count: ${{ steps.determine.outputs.component-test-count }}
 | 
			
		||||
      changed-cpp-file-count: ${{ steps.determine.outputs.changed-cpp-file-count }}
 | 
			
		||||
      memory_impact: ${{ steps.determine.outputs.memory-impact }}
 | 
			
		||||
      cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }}
 | 
			
		||||
      cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }}
 | 
			
		||||
      component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
@@ -205,17 +199,11 @@ jobs:
 | 
			
		||||
          # Extract individual fields
 | 
			
		||||
          echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "changed-cpp-file-count=$(echo "$output" | jq -r '.changed_cpp_file_count')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
 | 
			
		||||
 | 
			
		||||
  integration-tests:
 | 
			
		||||
    name: Run integration tests
 | 
			
		||||
@@ -253,34 +241,7 @@ jobs:
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          pytest -vv --no-cov --tb=native -n auto tests/integration/
 | 
			
		||||
 | 
			
		||||
  cpp-unit-tests:
 | 
			
		||||
    name: Run C++ unit tests
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
      - determine-jobs
 | 
			
		||||
    if: github.event_name == 'pull_request' && (needs.determine-jobs.outputs.cpp-unit-tests-run-all == 'true' || needs.determine-jobs.outputs.cpp-unit-tests-components != '[]')
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
 | 
			
		||||
      - name: Run cpp_unit_test.py
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          if [ "${{ needs.determine-jobs.outputs.cpp-unit-tests-run-all }}" = "true" ]; then
 | 
			
		||||
            script/cpp_unit_test.py --all
 | 
			
		||||
          else
 | 
			
		||||
            ARGS=$(echo '${{ needs.determine-jobs.outputs.cpp-unit-tests-components }}' | jq -r '.[] | @sh' | xargs)
 | 
			
		||||
            script/cpp_unit_test.py $ARGS
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
  clang-tidy-single:
 | 
			
		||||
  clang-tidy:
 | 
			
		||||
    name: ${{ matrix.name }}
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    needs:
 | 
			
		||||
@@ -298,6 +259,22 @@ jobs:
 | 
			
		||||
            name: Run script/clang-tidy for ESP8266
 | 
			
		||||
            options: --environment esp8266-arduino-tidy --grep USE_ESP8266
 | 
			
		||||
            pio_cache_key: tidyesp8266
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 Arduino 1/4
 | 
			
		||||
            options: --environment esp32-arduino-tidy --split-num 4 --split-at 1
 | 
			
		||||
            pio_cache_key: tidyesp32
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 Arduino 2/4
 | 
			
		||||
            options: --environment esp32-arduino-tidy --split-num 4 --split-at 2
 | 
			
		||||
            pio_cache_key: tidyesp32
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 Arduino 3/4
 | 
			
		||||
            options: --environment esp32-arduino-tidy --split-num 4 --split-at 3
 | 
			
		||||
            pio_cache_key: tidyesp32
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 Arduino 4/4
 | 
			
		||||
            options: --environment esp32-arduino-tidy --split-num 4 --split-at 4
 | 
			
		||||
            pio_cache_key: tidyesp32
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 IDF
 | 
			
		||||
            options: --environment esp32-idf-tidy --grep USE_ESP_IDF
 | 
			
		||||
@@ -378,165 +355,45 @@ jobs:
 | 
			
		||||
        # yamllint disable-line rule:line-length
 | 
			
		||||
        if: always()
 | 
			
		||||
 | 
			
		||||
  clang-tidy-nosplit:
 | 
			
		||||
    name: Run script/clang-tidy for ESP32 Arduino
 | 
			
		||||
  test-build-components-splitter:
 | 
			
		||||
    name: Split components for intelligent grouping (40 weighted per batch)
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
      - determine-jobs
 | 
			
		||||
    if: needs.determine-jobs.outputs.clang-tidy-mode == 'nosplit'
 | 
			
		||||
    env:
 | 
			
		||||
      GH_TOKEN: ${{ github.token }}
 | 
			
		||||
    if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
 | 
			
		||||
    outputs:
 | 
			
		||||
      matrix: ${{ steps.split.outputs.components }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          # Need history for HEAD~1 to work for checking changed files
 | 
			
		||||
          fetch-depth: 2
 | 
			
		||||
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        if: github.ref == 'refs/heads/dev'
 | 
			
		||||
        uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
 | 
			
		||||
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        if: github.ref != 'refs/heads/dev'
 | 
			
		||||
        uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
 | 
			
		||||
 | 
			
		||||
      - name: Register problem matchers
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/gcc.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
 | 
			
		||||
 | 
			
		||||
      - name: Check if full clang-tidy scan needed
 | 
			
		||||
        id: check_full_scan
 | 
			
		||||
      - name: Split components intelligently based on bus configurations
 | 
			
		||||
        id: split
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          if python script/clang_tidy_hash.py --check; then
 | 
			
		||||
            echo "full_scan=true" >> $GITHUB_OUTPUT
 | 
			
		||||
            echo "reason=hash_changed" >> $GITHUB_OUTPUT
 | 
			
		||||
 | 
			
		||||
          # Use intelligent splitter that groups components with same bus configs
 | 
			
		||||
          components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}'
 | 
			
		||||
 | 
			
		||||
          # Only isolate directly changed components when targeting dev branch
 | 
			
		||||
          # For beta/release branches, group everything for faster CI
 | 
			
		||||
          if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
 | 
			
		||||
            directly_changed='[]'
 | 
			
		||||
            echo "Target branch: ${{ github.base_ref }} - grouping all components"
 | 
			
		||||
          else
 | 
			
		||||
            echo "full_scan=false" >> $GITHUB_OUTPUT
 | 
			
		||||
            echo "reason=normal" >> $GITHUB_OUTPUT
 | 
			
		||||
            directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}'
 | 
			
		||||
            echo "Target branch: ${{ github.base_ref }} - isolating directly changed components"
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - name: Run clang-tidy
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
 | 
			
		||||
            echo "Running FULL clang-tidy scan (hash changed)"
 | 
			
		||||
            script/clang-tidy --all-headers --fix --environment esp32-arduino-tidy
 | 
			
		||||
          else
 | 
			
		||||
            echo "Running clang-tidy on changed files only"
 | 
			
		||||
            script/clang-tidy --all-headers --fix --changed --environment esp32-arduino-tidy
 | 
			
		||||
          fi
 | 
			
		||||
        env:
 | 
			
		||||
          # Also cache libdeps, store them in a ~/.platformio subfolder
 | 
			
		||||
          PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
 | 
			
		||||
          echo "Splitting components intelligently..."
 | 
			
		||||
          output=$(python3 script/split_components_for_ci.py --components "$components" --directly-changed "$directly_changed" --batch-size 40 --output github)
 | 
			
		||||
 | 
			
		||||
      - name: Suggested changes
 | 
			
		||||
        run: script/ci-suggest-changes
 | 
			
		||||
        if: always()
 | 
			
		||||
 | 
			
		||||
  clang-tidy-split:
 | 
			
		||||
    name: ${{ matrix.name }}
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
      - determine-jobs
 | 
			
		||||
    if: needs.determine-jobs.outputs.clang-tidy-mode == 'split'
 | 
			
		||||
    env:
 | 
			
		||||
      GH_TOKEN: ${{ github.token }}
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      max-parallel: 2
 | 
			
		||||
      matrix:
 | 
			
		||||
        include:
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 Arduino 1/4
 | 
			
		||||
            options: --environment esp32-arduino-tidy --split-num 4 --split-at 1
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 Arduino 2/4
 | 
			
		||||
            options: --environment esp32-arduino-tidy --split-num 4 --split-at 2
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 Arduino 3/4
 | 
			
		||||
            options: --environment esp32-arduino-tidy --split-num 4 --split-at 3
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 Arduino 4/4
 | 
			
		||||
            options: --environment esp32-arduino-tidy --split-num 4 --split-at 4
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          # Need history for HEAD~1 to work for checking changed files
 | 
			
		||||
          fetch-depth: 2
 | 
			
		||||
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        if: github.ref == 'refs/heads/dev'
 | 
			
		||||
        uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
 | 
			
		||||
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        if: github.ref != 'refs/heads/dev'
 | 
			
		||||
        uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
 | 
			
		||||
 | 
			
		||||
      - name: Register problem matchers
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/gcc.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
 | 
			
		||||
 | 
			
		||||
      - name: Check if full clang-tidy scan needed
 | 
			
		||||
        id: check_full_scan
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          if python script/clang_tidy_hash.py --check; then
 | 
			
		||||
            echo "full_scan=true" >> $GITHUB_OUTPUT
 | 
			
		||||
            echo "reason=hash_changed" >> $GITHUB_OUTPUT
 | 
			
		||||
          else
 | 
			
		||||
            echo "full_scan=false" >> $GITHUB_OUTPUT
 | 
			
		||||
            echo "reason=normal" >> $GITHUB_OUTPUT
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - name: Run clang-tidy
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
 | 
			
		||||
            echo "Running FULL clang-tidy scan (hash changed)"
 | 
			
		||||
            script/clang-tidy --all-headers --fix ${{ matrix.options }}
 | 
			
		||||
          else
 | 
			
		||||
            echo "Running clang-tidy on changed files only"
 | 
			
		||||
            script/clang-tidy --all-headers --fix --changed ${{ matrix.options }}
 | 
			
		||||
          fi
 | 
			
		||||
        env:
 | 
			
		||||
          # Also cache libdeps, store them in a ~/.platformio subfolder
 | 
			
		||||
          PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
 | 
			
		||||
 | 
			
		||||
      - name: Suggested changes
 | 
			
		||||
        run: script/ci-suggest-changes
 | 
			
		||||
        if: always()
 | 
			
		||||
          echo "$output" >> $GITHUB_OUTPUT
 | 
			
		||||
 | 
			
		||||
  test-build-components-split:
 | 
			
		||||
    name: Test components batch (${{ matrix.components }})
 | 
			
		||||
@@ -544,12 +401,13 @@ jobs:
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
      - determine-jobs
 | 
			
		||||
      - test-build-components-splitter
 | 
			
		||||
    if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
 | 
			
		||||
      matrix:
 | 
			
		||||
        components: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }}
 | 
			
		||||
        components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Show disk space
 | 
			
		||||
        run: |
 | 
			
		||||
@@ -663,271 +521,6 @@ jobs:
 | 
			
		||||
      - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
 | 
			
		||||
        if: always()
 | 
			
		||||
 | 
			
		||||
  memory-impact-target-branch:
 | 
			
		||||
    name: Build target branch for memory impact
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
      - determine-jobs
 | 
			
		||||
    if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true'
 | 
			
		||||
    outputs:
 | 
			
		||||
      ram_usage: ${{ steps.extract.outputs.ram_usage }}
 | 
			
		||||
      flash_usage: ${{ steps.extract.outputs.flash_usage }}
 | 
			
		||||
      cache_hit: ${{ steps.cache-memory-analysis.outputs.cache-hit }}
 | 
			
		||||
      skip: ${{ steps.check-script.outputs.skip }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out target branch
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          ref: ${{ github.base_ref }}
 | 
			
		||||
 | 
			
		||||
      # Check if memory impact extraction script exists on target branch
 | 
			
		||||
      # If not, skip the analysis (this handles older branches that don't have the feature)
 | 
			
		||||
      - name: Check for memory impact script
 | 
			
		||||
        id: check-script
 | 
			
		||||
        run: |
 | 
			
		||||
          if [ -f "script/ci_memory_impact_extract.py" ]; then
 | 
			
		||||
            echo "skip=false" >> $GITHUB_OUTPUT
 | 
			
		||||
          else
 | 
			
		||||
            echo "skip=true" >> $GITHUB_OUTPUT
 | 
			
		||||
            echo "::warning::ci_memory_impact_extract.py not found on target branch, skipping memory impact analysis"
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      # All remaining steps only run if script exists
 | 
			
		||||
      - name: Generate cache key
 | 
			
		||||
        id: cache-key
 | 
			
		||||
        if: steps.check-script.outputs.skip != 'true'
 | 
			
		||||
        run: |
 | 
			
		||||
          # Get the commit SHA of the target branch
 | 
			
		||||
          target_sha=$(git rev-parse HEAD)
 | 
			
		||||
 | 
			
		||||
          # Hash the build infrastructure files (all files that affect build/analysis)
 | 
			
		||||
          infra_hash=$(cat \
 | 
			
		||||
            script/test_build_components.py \
 | 
			
		||||
            script/ci_memory_impact_extract.py \
 | 
			
		||||
            script/analyze_component_buses.py \
 | 
			
		||||
            script/merge_component_configs.py \
 | 
			
		||||
            script/ci_helpers.py \
 | 
			
		||||
            .github/workflows/ci.yml \
 | 
			
		||||
            | sha256sum | cut -d' ' -f1)
 | 
			
		||||
 | 
			
		||||
          # Get platform and components from job inputs
 | 
			
		||||
          platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}"
 | 
			
		||||
          components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}'
 | 
			
		||||
          components_hash=$(echo "$components" | sha256sum | cut -d' ' -f1)
 | 
			
		||||
 | 
			
		||||
          # Combine into cache key
 | 
			
		||||
          cache_key="memory-analysis-target-${target_sha}-${infra_hash}-${platform}-${components_hash}"
 | 
			
		||||
          echo "cache-key=${cache_key}" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "Cache key: ${cache_key}"
 | 
			
		||||
 | 
			
		||||
      - name: Restore cached memory analysis
 | 
			
		||||
        id: cache-memory-analysis
 | 
			
		||||
        if: steps.check-script.outputs.skip != 'true'
 | 
			
		||||
        uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: memory-analysis-target.json
 | 
			
		||||
          key: ${{ steps.cache-key.outputs.cache-key }}
 | 
			
		||||
 | 
			
		||||
      - name: Cache status
 | 
			
		||||
        if: steps.check-script.outputs.skip != 'true'
 | 
			
		||||
        run: |
 | 
			
		||||
          if [ "${{ steps.cache-memory-analysis.outputs.cache-hit }}" == "true" ]; then
 | 
			
		||||
            echo "✓ Cache hit! Using cached memory analysis results."
 | 
			
		||||
            echo "  Skipping build step to save time."
 | 
			
		||||
          else
 | 
			
		||||
            echo "✗ Cache miss. Will build and analyze memory usage."
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
 | 
			
		||||
        uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
 | 
			
		||||
 | 
			
		||||
      - name: Build, compile, and analyze memory
 | 
			
		||||
        if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true'
 | 
			
		||||
        id: build
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}'
 | 
			
		||||
          platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}"
 | 
			
		||||
 | 
			
		||||
          echo "Building with test_build_components.py for $platform with components:"
 | 
			
		||||
          echo "$components" | jq -r '.[]' | sed 's/^/  - /'
 | 
			
		||||
 | 
			
		||||
          # Use test_build_components.py which handles grouping automatically
 | 
			
		||||
          # Pass components as comma-separated list
 | 
			
		||||
          component_list=$(echo "$components" | jq -r 'join(",")')
 | 
			
		||||
 | 
			
		||||
          echo "Compiling with test_build_components.py..."
 | 
			
		||||
 | 
			
		||||
          # Run build and extract memory with auto-detection of build directory for detailed analysis
 | 
			
		||||
          # Use tee to show output in CI while also piping to extraction script
 | 
			
		||||
          python script/test_build_components.py \
 | 
			
		||||
            -e compile \
 | 
			
		||||
            -c "$component_list" \
 | 
			
		||||
            -t "$platform" 2>&1 | \
 | 
			
		||||
            tee /dev/stderr | \
 | 
			
		||||
            python script/ci_memory_impact_extract.py \
 | 
			
		||||
              --output-env \
 | 
			
		||||
              --output-json memory-analysis-target.json
 | 
			
		||||
 | 
			
		||||
          # Add metadata to JSON before caching
 | 
			
		||||
          python script/ci_add_metadata_to_json.py \
 | 
			
		||||
            --json-file memory-analysis-target.json \
 | 
			
		||||
            --components "$components" \
 | 
			
		||||
            --platform "$platform"
 | 
			
		||||
 | 
			
		||||
      - name: Save memory analysis to cache
 | 
			
		||||
        if: steps.check-script.outputs.skip != 'true' && steps.cache-memory-analysis.outputs.cache-hit != 'true' && steps.build.outcome == 'success'
 | 
			
		||||
        uses: actions/cache/save@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: memory-analysis-target.json
 | 
			
		||||
          key: ${{ steps.cache-key.outputs.cache-key }}
 | 
			
		||||
 | 
			
		||||
      - name: Extract memory usage for outputs
 | 
			
		||||
        id: extract
 | 
			
		||||
        if: steps.check-script.outputs.skip != 'true'
 | 
			
		||||
        run: |
 | 
			
		||||
          if [ -f memory-analysis-target.json ]; then
 | 
			
		||||
            ram=$(jq -r '.ram_bytes' memory-analysis-target.json)
 | 
			
		||||
            flash=$(jq -r '.flash_bytes' memory-analysis-target.json)
 | 
			
		||||
            echo "ram_usage=${ram}" >> $GITHUB_OUTPUT
 | 
			
		||||
            echo "flash_usage=${flash}" >> $GITHUB_OUTPUT
 | 
			
		||||
            echo "RAM: ${ram} bytes, Flash: ${flash} bytes"
 | 
			
		||||
          else
 | 
			
		||||
            echo "Error: memory-analysis-target.json not found"
 | 
			
		||||
            exit 1
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - name: Upload memory analysis JSON
 | 
			
		||||
        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          name: memory-analysis-target
 | 
			
		||||
          path: memory-analysis-target.json
 | 
			
		||||
          if-no-files-found: warn
 | 
			
		||||
          retention-days: 1
 | 
			
		||||
 | 
			
		||||
  memory-impact-pr-branch:
 | 
			
		||||
    name: Build PR branch for memory impact
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
      - determine-jobs
 | 
			
		||||
    if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true'
 | 
			
		||||
    outputs:
 | 
			
		||||
      ram_usage: ${{ steps.extract.outputs.ram_usage }}
 | 
			
		||||
      flash_usage: ${{ steps.extract.outputs.flash_usage }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out PR branch
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: platformio-memory-${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}-${{ hashFiles('platformio.ini') }}
 | 
			
		||||
      - name: Build, compile, and analyze memory
 | 
			
		||||
        id: extract
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          components='${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}'
 | 
			
		||||
          platform="${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}"
 | 
			
		||||
 | 
			
		||||
          echo "Building with test_build_components.py for $platform with components:"
 | 
			
		||||
          echo "$components" | jq -r '.[]' | sed 's/^/  - /'
 | 
			
		||||
 | 
			
		||||
          # Use test_build_components.py which handles grouping automatically
 | 
			
		||||
          # Pass components as comma-separated list
 | 
			
		||||
          component_list=$(echo "$components" | jq -r 'join(",")')
 | 
			
		||||
 | 
			
		||||
          echo "Compiling with test_build_components.py..."
 | 
			
		||||
 | 
			
		||||
          # Run build and extract memory with auto-detection of build directory for detailed analysis
 | 
			
		||||
          # Use tee to show output in CI while also piping to extraction script
 | 
			
		||||
          python script/test_build_components.py \
 | 
			
		||||
            -e compile \
 | 
			
		||||
            -c "$component_list" \
 | 
			
		||||
            -t "$platform" 2>&1 | \
 | 
			
		||||
            tee /dev/stderr | \
 | 
			
		||||
            python script/ci_memory_impact_extract.py \
 | 
			
		||||
              --output-env \
 | 
			
		||||
              --output-json memory-analysis-pr.json
 | 
			
		||||
 | 
			
		||||
          # Add metadata to JSON (components and platform are in shell variables above)
 | 
			
		||||
          python script/ci_add_metadata_to_json.py \
 | 
			
		||||
            --json-file memory-analysis-pr.json \
 | 
			
		||||
            --components "$components" \
 | 
			
		||||
            --platform "$platform"
 | 
			
		||||
 | 
			
		||||
      - name: Upload memory analysis JSON
 | 
			
		||||
        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          name: memory-analysis-pr
 | 
			
		||||
          path: memory-analysis-pr.json
 | 
			
		||||
          if-no-files-found: warn
 | 
			
		||||
          retention-days: 1
 | 
			
		||||
 | 
			
		||||
  memory-impact-comment:
 | 
			
		||||
    name: Comment memory impact
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
      - determine-jobs
 | 
			
		||||
      - memory-impact-target-branch
 | 
			
		||||
      - memory-impact-pr-branch
 | 
			
		||||
    if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' && needs.memory-impact-target-branch.outputs.skip != 'true'
 | 
			
		||||
    permissions:
 | 
			
		||||
      contents: read
 | 
			
		||||
      pull-requests: write
 | 
			
		||||
    env:
 | 
			
		||||
      GH_TOKEN: ${{ github.token }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: Download target analysis JSON
 | 
			
		||||
        uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          name: memory-analysis-target
 | 
			
		||||
          path: ./memory-analysis
 | 
			
		||||
        continue-on-error: true
 | 
			
		||||
      - name: Download PR analysis JSON
 | 
			
		||||
        uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          name: memory-analysis-pr
 | 
			
		||||
          path: ./memory-analysis
 | 
			
		||||
        continue-on-error: true
 | 
			
		||||
      - name: Post or update PR comment
 | 
			
		||||
        env:
 | 
			
		||||
          PR_NUMBER: ${{ github.event.pull_request.number }}
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
 | 
			
		||||
          # Pass JSON file paths directly to Python script
 | 
			
		||||
          # All data is extracted from JSON files for security
 | 
			
		||||
          python script/ci_memory_impact_comment.py \
 | 
			
		||||
            --pr-number "$PR_NUMBER" \
 | 
			
		||||
            --target-json ./memory-analysis/memory-analysis-target.json \
 | 
			
		||||
            --pr-json ./memory-analysis/memory-analysis-pr.json
 | 
			
		||||
 | 
			
		||||
  ci-status:
 | 
			
		||||
    name: CI Status
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
@@ -937,15 +530,11 @@ jobs:
 | 
			
		||||
      - pylint
 | 
			
		||||
      - pytest
 | 
			
		||||
      - integration-tests
 | 
			
		||||
      - clang-tidy-single
 | 
			
		||||
      - clang-tidy-nosplit
 | 
			
		||||
      - clang-tidy-split
 | 
			
		||||
      - clang-tidy
 | 
			
		||||
      - determine-jobs
 | 
			
		||||
      - test-build-components-splitter
 | 
			
		||||
      - test-build-components-split
 | 
			
		||||
      - pre-commit-ci-lite
 | 
			
		||||
      - memory-impact-target-branch
 | 
			
		||||
      - memory-impact-pr-branch
 | 
			
		||||
      - memory-impact-comment
 | 
			
		||||
    if: always()
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Success
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
 | 
			
		||||
        uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
 | 
			
		||||
        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@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
 | 
			
		||||
        uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
 | 
			
		||||
        with:
 | 
			
		||||
          category: "/language:${{matrix.language}}"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@@ -138,7 +138,7 @@ jobs:
 | 
			
		||||
      #     version: ${{ needs.init.outputs.tag }}
 | 
			
		||||
 | 
			
		||||
      - name: Upload digests
 | 
			
		||||
        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
 | 
			
		||||
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
 | 
			
		||||
        with:
 | 
			
		||||
          name: digests-${{ matrix.platform.arch }}
 | 
			
		||||
          path: /tmp/digests
 | 
			
		||||
@@ -171,7 +171,7 @@ jobs:
 | 
			
		||||
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
 | 
			
		||||
      - name: Download digests
 | 
			
		||||
        uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
 | 
			
		||||
        uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          pattern: digests-*
 | 
			
		||||
          path: /tmp/digests
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								.github/workflows/status-check-labels.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/status-check-labels.yml
									
									
									
									
										vendored
									
									
								
							@@ -14,7 +14,6 @@ jobs:
 | 
			
		||||
        label:
 | 
			
		||||
          - needs-docs
 | 
			
		||||
          - merge-after-release
 | 
			
		||||
          - chained-pr
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check for ${{ matrix.label }} label
 | 
			
		||||
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ ci:
 | 
			
		||||
repos:
 | 
			
		||||
  - repo: https://github.com/astral-sh/ruff-pre-commit
 | 
			
		||||
    # Ruff version.
 | 
			
		||||
    rev: v0.14.3
 | 
			
		||||
    rev: v0.14.1
 | 
			
		||||
    hooks:
 | 
			
		||||
      # Run the linter.
 | 
			
		||||
      - id: ruff
 | 
			
		||||
 
 | 
			
		||||
@@ -161,7 +161,6 @@ esphome/components/esp32_rmt_led_strip/* @jesserockz
 | 
			
		||||
esphome/components/esp8266/* @esphome/core
 | 
			
		||||
esphome/components/esp_ldo/* @clydebarrow
 | 
			
		||||
esphome/components/espnow/* @jesserockz
 | 
			
		||||
esphome/components/espnow/packet_transport/* @EasilyBoredEngineer
 | 
			
		||||
esphome/components/ethernet_info/* @gtjadsonsantos
 | 
			
		||||
esphome/components/event/* @nohat
 | 
			
		||||
esphome/components/exposure_notifications/* @OttoWinter
 | 
			
		||||
@@ -201,7 +200,6 @@ esphome/components/havells_solar/* @sourabhjaiswal
 | 
			
		||||
esphome/components/hbridge/fan/* @WeekendWarrior
 | 
			
		||||
esphome/components/hbridge/light/* @DotNetDann
 | 
			
		||||
esphome/components/hbridge/switch/* @dwmw2
 | 
			
		||||
esphome/components/hdc2010/* @optimusprimespace @ssieb
 | 
			
		||||
esphome/components/he60r/* @clydebarrow
 | 
			
		||||
esphome/components/heatpumpir/* @rob-deutsch
 | 
			
		||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
 | 
			
		||||
 
 | 
			
		||||
@@ -62,40 +62,6 @@ from esphome.util import (
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
# Special non-component keys that appear in configs
 | 
			
		||||
_NON_COMPONENT_KEYS = frozenset(
 | 
			
		||||
    {
 | 
			
		||||
        CONF_ESPHOME,
 | 
			
		||||
        "substitutions",
 | 
			
		||||
        "packages",
 | 
			
		||||
        "globals",
 | 
			
		||||
        "external_components",
 | 
			
		||||
        "<<",
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def detect_external_components(config: ConfigType) -> set[str]:
 | 
			
		||||
    """Detect external/custom components in the configuration.
 | 
			
		||||
 | 
			
		||||
    External components are those that appear in the config but are not
 | 
			
		||||
    part of ESPHome's built-in components and are not special config keys.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        config: The ESPHome configuration dictionary
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        A set of external component names
 | 
			
		||||
    """
 | 
			
		||||
    from esphome.analyze_memory.helpers import get_esphome_components
 | 
			
		||||
 | 
			
		||||
    builtin_components = get_esphome_components()
 | 
			
		||||
    return {
 | 
			
		||||
        key
 | 
			
		||||
        for key in config
 | 
			
		||||
        if key not in builtin_components and key not in _NON_COMPONENT_KEYS
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ArgsProtocol(Protocol):
 | 
			
		||||
    device: list[str] | None
 | 
			
		||||
@@ -207,21 +173,19 @@ def choose_upload_log_host(
 | 
			
		||||
                    if has_mqtt_logging():
 | 
			
		||||
                        resolved.append("MQTT")
 | 
			
		||||
 | 
			
		||||
                    if has_api() and has_non_ip_address() and has_resolvable_address():
 | 
			
		||||
                    if has_api() and has_non_ip_address():
 | 
			
		||||
                        resolved.extend(_resolve_with_cache(CORE.address, purpose))
 | 
			
		||||
 | 
			
		||||
                elif purpose == Purpose.UPLOADING:
 | 
			
		||||
                    if has_ota() and has_mqtt_ip_lookup():
 | 
			
		||||
                        resolved.append("MQTTIP")
 | 
			
		||||
 | 
			
		||||
                    if has_ota() and has_non_ip_address() and has_resolvable_address():
 | 
			
		||||
                    if has_ota() and has_non_ip_address():
 | 
			
		||||
                        resolved.extend(_resolve_with_cache(CORE.address, purpose))
 | 
			
		||||
            else:
 | 
			
		||||
                resolved.append(device)
 | 
			
		||||
        if not resolved:
 | 
			
		||||
            raise EsphomeError(
 | 
			
		||||
                f"All specified devices {defaults} could not be resolved. Is the device connected to the network?"
 | 
			
		||||
            )
 | 
			
		||||
            _LOGGER.error("All specified devices: %s could not be resolved.", defaults)
 | 
			
		||||
        return resolved
 | 
			
		||||
 | 
			
		||||
    # No devices specified, show interactive chooser
 | 
			
		||||
@@ -318,17 +282,7 @@ def has_resolvable_address() -> bool:
 | 
			
		||||
    """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
 | 
			
		||||
    if CORE.address is None:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    if has_ip_address():
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    if has_mdns():
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    # .local mDNS hostnames are only resolvable if mDNS is enabled
 | 
			
		||||
    return not CORE.address.endswith(".local")
 | 
			
		||||
    return CORE.address is not None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str):
 | 
			
		||||
@@ -512,9 +466,7 @@ def write_cpp_file() -> int:
 | 
			
		||||
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
 | 
			
		||||
    from esphome import platformio_api
 | 
			
		||||
 | 
			
		||||
    # NOTE: "Build path:" format is parsed by script/ci_memory_impact_extract.py
 | 
			
		||||
    # If you change this format, update the regex in that script as well
 | 
			
		||||
    _LOGGER.info("Compiling app... Build path: %s", CORE.build_path)
 | 
			
		||||
    _LOGGER.info("Compiling app...")
 | 
			
		||||
    rc = platformio_api.run_compile(config, CORE.verbose)
 | 
			
		||||
    if rc != 0:
 | 
			
		||||
        return rc
 | 
			
		||||
@@ -936,54 +888,6 @@ def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
 | 
			
		||||
    return 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_analyze_memory(args: ArgsProtocol, config: ConfigType) -> int:
 | 
			
		||||
    """Analyze memory usage by component.
 | 
			
		||||
 | 
			
		||||
    This command compiles the configuration and performs memory analysis.
 | 
			
		||||
    Compilation is fast if sources haven't changed (just relinking).
 | 
			
		||||
    """
 | 
			
		||||
    from esphome import platformio_api
 | 
			
		||||
    from esphome.analyze_memory.cli import MemoryAnalyzerCLI
 | 
			
		||||
 | 
			
		||||
    # Always compile to ensure fresh data (fast if no changes - just relinks)
 | 
			
		||||
    exit_code = write_cpp(config)
 | 
			
		||||
    if exit_code != 0:
 | 
			
		||||
        return exit_code
 | 
			
		||||
    exit_code = compile_program(args, config)
 | 
			
		||||
    if exit_code != 0:
 | 
			
		||||
        return exit_code
 | 
			
		||||
    _LOGGER.info("Successfully compiled program.")
 | 
			
		||||
 | 
			
		||||
    # Get idedata for analysis
 | 
			
		||||
    idedata = platformio_api.get_idedata(config)
 | 
			
		||||
    if idedata is None:
 | 
			
		||||
        _LOGGER.error("Failed to get IDE data for memory analysis")
 | 
			
		||||
        return 1
 | 
			
		||||
 | 
			
		||||
    firmware_elf = Path(idedata.firmware_elf_path)
 | 
			
		||||
 | 
			
		||||
    # Extract external components from config
 | 
			
		||||
    external_components = detect_external_components(config)
 | 
			
		||||
    _LOGGER.debug("Detected external components: %s", external_components)
 | 
			
		||||
 | 
			
		||||
    # Perform memory analysis
 | 
			
		||||
    _LOGGER.info("Analyzing memory usage...")
 | 
			
		||||
    analyzer = MemoryAnalyzerCLI(
 | 
			
		||||
        str(firmware_elf),
 | 
			
		||||
        idedata.objdump_path,
 | 
			
		||||
        idedata.readelf_path,
 | 
			
		||||
        external_components,
 | 
			
		||||
    )
 | 
			
		||||
    analyzer.analyze()
 | 
			
		||||
 | 
			
		||||
    # Generate and display report
 | 
			
		||||
    report = analyzer.generate_report()
 | 
			
		||||
    print()
 | 
			
		||||
    print(report)
 | 
			
		||||
 | 
			
		||||
    return 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
    new_name = args.name
 | 
			
		||||
    for c in new_name:
 | 
			
		||||
@@ -1099,7 +1003,6 @@ POST_CONFIG_ACTIONS = {
 | 
			
		||||
    "idedata": command_idedata,
 | 
			
		||||
    "rename": command_rename,
 | 
			
		||||
    "discover": command_discover,
 | 
			
		||||
    "analyze-memory": command_analyze_memory,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SIMPLE_CONFIG_ACTIONS = [
 | 
			
		||||
@@ -1385,14 +1288,6 @@ def parse_args(argv):
 | 
			
		||||
    )
 | 
			
		||||
    parser_rename.add_argument("name", help="The new name for the device.", type=str)
 | 
			
		||||
 | 
			
		||||
    parser_analyze_memory = subparsers.add_parser(
 | 
			
		||||
        "analyze-memory",
 | 
			
		||||
        help="Analyze memory usage by component.",
 | 
			
		||||
    )
 | 
			
		||||
    parser_analyze_memory.add_argument(
 | 
			
		||||
        "configuration", help="Your YAML configuration file(s).", nargs="+"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # Keep backward compatibility with the old command line format of
 | 
			
		||||
    # esphome <config> <command>.
 | 
			
		||||
    #
 | 
			
		||||
 
 | 
			
		||||
@@ -1,502 +0,0 @@
 | 
			
		||||
"""Memory usage analyzer for ESPHome compiled binaries."""
 | 
			
		||||
 | 
			
		||||
from collections import defaultdict
 | 
			
		||||
from dataclasses import dataclass, field
 | 
			
		||||
import logging
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
import re
 | 
			
		||||
import subprocess
 | 
			
		||||
from typing import TYPE_CHECKING
 | 
			
		||||
 | 
			
		||||
from .const import (
 | 
			
		||||
    CORE_SUBCATEGORY_PATTERNS,
 | 
			
		||||
    DEMANGLED_PATTERNS,
 | 
			
		||||
    ESPHOME_COMPONENT_PATTERN,
 | 
			
		||||
    SECTION_TO_ATTR,
 | 
			
		||||
    SYMBOL_PATTERNS,
 | 
			
		||||
)
 | 
			
		||||
from .helpers import (
 | 
			
		||||
    get_component_class_patterns,
 | 
			
		||||
    get_esphome_components,
 | 
			
		||||
    map_section_name,
 | 
			
		||||
    parse_symbol_line,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
if TYPE_CHECKING:
 | 
			
		||||
    from esphome.platformio_api import IDEData
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
# GCC global constructor/destructor prefix annotations
 | 
			
		||||
_GCC_PREFIX_ANNOTATIONS = {
 | 
			
		||||
    "_GLOBAL__sub_I_": "global constructor for",
 | 
			
		||||
    "_GLOBAL__sub_D_": "global destructor for",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# GCC optimization suffix pattern (e.g., $isra$0, $part$1, $constprop$2)
 | 
			
		||||
_GCC_OPTIMIZATION_SUFFIX_PATTERN = re.compile(r"(\$(?:isra|part|constprop)\$\d+)")
 | 
			
		||||
 | 
			
		||||
# C++ runtime patterns for categorization
 | 
			
		||||
_CPP_RUNTIME_PATTERNS = frozenset(["vtable", "typeinfo", "thunk"])
 | 
			
		||||
 | 
			
		||||
# libc printf/scanf family base names (used to detect variants like _printf_r, vfprintf, etc.)
 | 
			
		||||
_LIBC_PRINTF_SCANF_FAMILY = frozenset(["printf", "fprintf", "sprintf", "scanf"])
 | 
			
		||||
 | 
			
		||||
# Regex pattern for parsing readelf section headers
 | 
			
		||||
# Format: [ #] name type addr off size
 | 
			
		||||
_READELF_SECTION_PATTERN = re.compile(
 | 
			
		||||
    r"\s*\[\s*\d+\]\s+([\.\w]+)\s+\w+\s+[\da-fA-F]+\s+[\da-fA-F]+\s+([\da-fA-F]+)"
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
# Component category prefixes
 | 
			
		||||
_COMPONENT_PREFIX_ESPHOME = "[esphome]"
 | 
			
		||||
_COMPONENT_PREFIX_EXTERNAL = "[external]"
 | 
			
		||||
_COMPONENT_CORE = f"{_COMPONENT_PREFIX_ESPHOME}core"
 | 
			
		||||
_COMPONENT_API = f"{_COMPONENT_PREFIX_ESPHOME}api"
 | 
			
		||||
 | 
			
		||||
# C++ namespace prefixes
 | 
			
		||||
_NAMESPACE_ESPHOME = "esphome::"
 | 
			
		||||
_NAMESPACE_STD = "std::"
 | 
			
		||||
 | 
			
		||||
# Type alias for symbol information: (symbol_name, size, component)
 | 
			
		||||
SymbolInfoType = tuple[str, int, str]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class MemorySection:
 | 
			
		||||
    """Represents a memory section with its symbols."""
 | 
			
		||||
 | 
			
		||||
    name: str
 | 
			
		||||
    symbols: list[SymbolInfoType] = field(default_factory=list)
 | 
			
		||||
    total_size: int = 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@dataclass
 | 
			
		||||
class ComponentMemory:
 | 
			
		||||
    """Tracks memory usage for a component."""
 | 
			
		||||
 | 
			
		||||
    name: str
 | 
			
		||||
    text_size: int = 0  # Code in flash
 | 
			
		||||
    rodata_size: int = 0  # Read-only data in flash
 | 
			
		||||
    data_size: int = 0  # Initialized data (flash + ram)
 | 
			
		||||
    bss_size: int = 0  # Uninitialized data (ram only)
 | 
			
		||||
    symbol_count: int = 0
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def flash_total(self) -> int:
 | 
			
		||||
        """Total flash usage (text + rodata + data)."""
 | 
			
		||||
        return self.text_size + self.rodata_size + self.data_size
 | 
			
		||||
 | 
			
		||||
    @property
 | 
			
		||||
    def ram_total(self) -> int:
 | 
			
		||||
        """Total RAM usage (data + bss)."""
 | 
			
		||||
        return self.data_size + self.bss_size
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MemoryAnalyzer:
 | 
			
		||||
    """Analyzes memory usage from ELF files."""
 | 
			
		||||
 | 
			
		||||
    def __init__(
 | 
			
		||||
        self,
 | 
			
		||||
        elf_path: str,
 | 
			
		||||
        objdump_path: str | None = None,
 | 
			
		||||
        readelf_path: str | None = None,
 | 
			
		||||
        external_components: set[str] | None = None,
 | 
			
		||||
        idedata: "IDEData | None" = None,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        """Initialize memory analyzer.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            elf_path: Path to ELF file to analyze
 | 
			
		||||
            objdump_path: Path to objdump binary (auto-detected from idedata if not provided)
 | 
			
		||||
            readelf_path: Path to readelf binary (auto-detected from idedata if not provided)
 | 
			
		||||
            external_components: Set of external component names
 | 
			
		||||
            idedata: Optional PlatformIO IDEData object to auto-detect toolchain paths
 | 
			
		||||
        """
 | 
			
		||||
        self.elf_path = Path(elf_path)
 | 
			
		||||
        if not self.elf_path.exists():
 | 
			
		||||
            raise FileNotFoundError(f"ELF file not found: {elf_path}")
 | 
			
		||||
 | 
			
		||||
        # Auto-detect toolchain paths from idedata if not provided
 | 
			
		||||
        if idedata is not None and (objdump_path is None or readelf_path is None):
 | 
			
		||||
            objdump_path = objdump_path or idedata.objdump_path
 | 
			
		||||
            readelf_path = readelf_path or idedata.readelf_path
 | 
			
		||||
            _LOGGER.debug("Using toolchain paths from PlatformIO idedata")
 | 
			
		||||
 | 
			
		||||
        self.objdump_path = objdump_path or "objdump"
 | 
			
		||||
        self.readelf_path = readelf_path or "readelf"
 | 
			
		||||
        self.external_components = external_components or set()
 | 
			
		||||
 | 
			
		||||
        self.sections: dict[str, MemorySection] = {}
 | 
			
		||||
        self.components: dict[str, ComponentMemory] = defaultdict(
 | 
			
		||||
            lambda: ComponentMemory("")
 | 
			
		||||
        )
 | 
			
		||||
        self._demangle_cache: dict[str, str] = {}
 | 
			
		||||
        self._uncategorized_symbols: list[tuple[str, str, int]] = []
 | 
			
		||||
        self._esphome_core_symbols: list[
 | 
			
		||||
            tuple[str, str, int]
 | 
			
		||||
        ] = []  # Track core symbols
 | 
			
		||||
        self._component_symbols: dict[str, list[tuple[str, str, int]]] = defaultdict(
 | 
			
		||||
            list
 | 
			
		||||
        )  # Track symbols for all components
 | 
			
		||||
 | 
			
		||||
    def analyze(self) -> dict[str, ComponentMemory]:
 | 
			
		||||
        """Analyze the ELF file and return component memory usage."""
 | 
			
		||||
        self._parse_sections()
 | 
			
		||||
        self._parse_symbols()
 | 
			
		||||
        self._categorize_symbols()
 | 
			
		||||
        return dict(self.components)
 | 
			
		||||
 | 
			
		||||
    def _parse_sections(self) -> None:
 | 
			
		||||
        """Parse section headers from ELF file."""
 | 
			
		||||
        result = subprocess.run(
 | 
			
		||||
            [self.readelf_path, "-S", str(self.elf_path)],
 | 
			
		||||
            capture_output=True,
 | 
			
		||||
            text=True,
 | 
			
		||||
            check=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Parse section headers
 | 
			
		||||
        for line in result.stdout.splitlines():
 | 
			
		||||
            # Look for section entries
 | 
			
		||||
            if not (match := _READELF_SECTION_PATTERN.match(line)):
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            section_name = match.group(1)
 | 
			
		||||
            size_hex = match.group(2)
 | 
			
		||||
            size = int(size_hex, 16)
 | 
			
		||||
 | 
			
		||||
            # Map to standard section name
 | 
			
		||||
            mapped_section = map_section_name(section_name)
 | 
			
		||||
            if not mapped_section:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            if mapped_section not in self.sections:
 | 
			
		||||
                self.sections[mapped_section] = MemorySection(mapped_section)
 | 
			
		||||
            self.sections[mapped_section].total_size += size
 | 
			
		||||
 | 
			
		||||
    def _parse_symbols(self) -> None:
 | 
			
		||||
        """Parse symbols from ELF file."""
 | 
			
		||||
        result = subprocess.run(
 | 
			
		||||
            [self.objdump_path, "-t", str(self.elf_path)],
 | 
			
		||||
            capture_output=True,
 | 
			
		||||
            text=True,
 | 
			
		||||
            check=True,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Track seen addresses to avoid duplicates
 | 
			
		||||
        seen_addresses: set[str] = set()
 | 
			
		||||
 | 
			
		||||
        for line in result.stdout.splitlines():
 | 
			
		||||
            if not (symbol_info := parse_symbol_line(line)):
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            section, name, size, address = symbol_info
 | 
			
		||||
 | 
			
		||||
            # Skip duplicate symbols at the same address (e.g., C1/C2 constructors)
 | 
			
		||||
            if address in seen_addresses or section not in self.sections:
 | 
			
		||||
                continue
 | 
			
		||||
 | 
			
		||||
            self.sections[section].symbols.append((name, size, ""))
 | 
			
		||||
            seen_addresses.add(address)
 | 
			
		||||
 | 
			
		||||
    def _categorize_symbols(self) -> None:
 | 
			
		||||
        """Categorize symbols by component."""
 | 
			
		||||
        # First, collect all unique symbol names for batch demangling
 | 
			
		||||
        all_symbols = {
 | 
			
		||||
            symbol_name
 | 
			
		||||
            for section in self.sections.values()
 | 
			
		||||
            for symbol_name, _, _ in section.symbols
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        # Batch demangle all symbols at once
 | 
			
		||||
        self._batch_demangle_symbols(list(all_symbols))
 | 
			
		||||
 | 
			
		||||
        # Now categorize with cached demangled names
 | 
			
		||||
        for section_name, section in self.sections.items():
 | 
			
		||||
            for symbol_name, size, _ in section.symbols:
 | 
			
		||||
                component = self._identify_component(symbol_name)
 | 
			
		||||
 | 
			
		||||
                if component not in self.components:
 | 
			
		||||
                    self.components[component] = ComponentMemory(component)
 | 
			
		||||
 | 
			
		||||
                comp_mem = self.components[component]
 | 
			
		||||
                comp_mem.symbol_count += 1
 | 
			
		||||
 | 
			
		||||
                # Update the appropriate size attribute based on section
 | 
			
		||||
                if attr_name := SECTION_TO_ATTR.get(section_name):
 | 
			
		||||
                    setattr(comp_mem, attr_name, getattr(comp_mem, attr_name) + size)
 | 
			
		||||
 | 
			
		||||
                # Track uncategorized symbols
 | 
			
		||||
                if component == "other" and size > 0:
 | 
			
		||||
                    demangled = self._demangle_symbol(symbol_name)
 | 
			
		||||
                    self._uncategorized_symbols.append((symbol_name, demangled, size))
 | 
			
		||||
 | 
			
		||||
                # Track ESPHome core symbols for detailed analysis
 | 
			
		||||
                if component == _COMPONENT_CORE and size > 0:
 | 
			
		||||
                    demangled = self._demangle_symbol(symbol_name)
 | 
			
		||||
                    self._esphome_core_symbols.append((symbol_name, demangled, size))
 | 
			
		||||
 | 
			
		||||
                # Track all component symbols for detailed analysis
 | 
			
		||||
                if size > 0:
 | 
			
		||||
                    demangled = self._demangle_symbol(symbol_name)
 | 
			
		||||
                    self._component_symbols[component].append(
 | 
			
		||||
                        (symbol_name, demangled, size)
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
    def _identify_component(self, symbol_name: str) -> str:
 | 
			
		||||
        """Identify which component a symbol belongs to."""
 | 
			
		||||
        # Demangle C++ names if needed
 | 
			
		||||
        demangled = self._demangle_symbol(symbol_name)
 | 
			
		||||
 | 
			
		||||
        # Check for special component classes first (before namespace pattern)
 | 
			
		||||
        # This handles cases like esphome::ESPHomeOTAComponent which should map to ota
 | 
			
		||||
        if _NAMESPACE_ESPHOME in demangled:
 | 
			
		||||
            # Check for special component classes that include component name in the class
 | 
			
		||||
            # For example: esphome::ESPHomeOTAComponent -> ota component
 | 
			
		||||
            for component_name in get_esphome_components():
 | 
			
		||||
                patterns = get_component_class_patterns(component_name)
 | 
			
		||||
                if any(pattern in demangled for pattern in patterns):
 | 
			
		||||
                    return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}"
 | 
			
		||||
 | 
			
		||||
        # Check for ESPHome component namespaces
 | 
			
		||||
        match = ESPHOME_COMPONENT_PATTERN.search(demangled)
 | 
			
		||||
        if match:
 | 
			
		||||
            component_name = match.group(1)
 | 
			
		||||
            # Strip trailing underscore if present (e.g., switch_ -> switch)
 | 
			
		||||
            component_name = component_name.rstrip("_")
 | 
			
		||||
 | 
			
		||||
            # Check if this is an actual component in the components directory
 | 
			
		||||
            if component_name in get_esphome_components():
 | 
			
		||||
                return f"{_COMPONENT_PREFIX_ESPHOME}{component_name}"
 | 
			
		||||
            # Check if this is a known external component from the config
 | 
			
		||||
            if component_name in self.external_components:
 | 
			
		||||
                return f"{_COMPONENT_PREFIX_EXTERNAL}{component_name}"
 | 
			
		||||
            # Everything else in esphome:: namespace is core
 | 
			
		||||
            return _COMPONENT_CORE
 | 
			
		||||
 | 
			
		||||
        # Check for esphome core namespace (no component namespace)
 | 
			
		||||
        if _NAMESPACE_ESPHOME in demangled:
 | 
			
		||||
            # If no component match found, it's core
 | 
			
		||||
            return _COMPONENT_CORE
 | 
			
		||||
 | 
			
		||||
        # Check against symbol patterns
 | 
			
		||||
        for component, patterns in SYMBOL_PATTERNS.items():
 | 
			
		||||
            if any(pattern in symbol_name for pattern in patterns):
 | 
			
		||||
                return component
 | 
			
		||||
 | 
			
		||||
        # Check against demangled patterns
 | 
			
		||||
        for component, patterns in DEMANGLED_PATTERNS.items():
 | 
			
		||||
            if any(pattern in demangled for pattern in patterns):
 | 
			
		||||
                return component
 | 
			
		||||
 | 
			
		||||
        # Special cases that need more complex logic
 | 
			
		||||
 | 
			
		||||
        # Check if spi_flash vs spi_driver
 | 
			
		||||
        if "spi_" in symbol_name or "SPI" in symbol_name:
 | 
			
		||||
            return "spi_flash" if "spi_flash" in symbol_name else "spi_driver"
 | 
			
		||||
 | 
			
		||||
        # libc special printf variants
 | 
			
		||||
        if (
 | 
			
		||||
            symbol_name.startswith("_")
 | 
			
		||||
            and symbol_name[1:].replace("_r", "").replace("v", "").replace("s", "")
 | 
			
		||||
            in _LIBC_PRINTF_SCANF_FAMILY
 | 
			
		||||
        ):
 | 
			
		||||
            return "libc"
 | 
			
		||||
 | 
			
		||||
        # Track uncategorized symbols for analysis
 | 
			
		||||
        return "other"
 | 
			
		||||
 | 
			
		||||
    def _batch_demangle_symbols(self, symbols: list[str]) -> None:
 | 
			
		||||
        """Batch demangle C++ symbol names for efficiency."""
 | 
			
		||||
        if not symbols:
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # Try to find the appropriate c++filt for the platform
 | 
			
		||||
        cppfilt_cmd = "c++filt"
 | 
			
		||||
 | 
			
		||||
        _LOGGER.info("Demangling %d symbols", len(symbols))
 | 
			
		||||
        _LOGGER.debug("objdump_path = %s", self.objdump_path)
 | 
			
		||||
 | 
			
		||||
        # Check if we have a toolchain-specific c++filt
 | 
			
		||||
        if self.objdump_path and self.objdump_path != "objdump":
 | 
			
		||||
            # Replace objdump with c++filt in the path
 | 
			
		||||
            potential_cppfilt = self.objdump_path.replace("objdump", "c++filt")
 | 
			
		||||
            _LOGGER.info("Checking for toolchain c++filt at: %s", potential_cppfilt)
 | 
			
		||||
            if Path(potential_cppfilt).exists():
 | 
			
		||||
                cppfilt_cmd = potential_cppfilt
 | 
			
		||||
                _LOGGER.info("✓ Using toolchain c++filt: %s", cppfilt_cmd)
 | 
			
		||||
            else:
 | 
			
		||||
                _LOGGER.info(
 | 
			
		||||
                    "✗ Toolchain c++filt not found at %s, using system c++filt",
 | 
			
		||||
                    potential_cppfilt,
 | 
			
		||||
                )
 | 
			
		||||
        else:
 | 
			
		||||
            _LOGGER.info("✗ Using system c++filt (objdump_path=%s)", self.objdump_path)
 | 
			
		||||
 | 
			
		||||
        # Strip GCC optimization suffixes and prefixes before demangling
 | 
			
		||||
        # Suffixes like $isra$0, $part$0, $constprop$0 confuse c++filt
 | 
			
		||||
        # Prefixes like _GLOBAL__sub_I_ need to be removed and tracked
 | 
			
		||||
        symbols_stripped: list[str] = []
 | 
			
		||||
        symbols_prefixes: list[str] = []  # Track removed prefixes
 | 
			
		||||
        for symbol in symbols:
 | 
			
		||||
            # Remove GCC optimization markers
 | 
			
		||||
            stripped = _GCC_OPTIMIZATION_SUFFIX_PATTERN.sub("", symbol)
 | 
			
		||||
 | 
			
		||||
            # Handle GCC global constructor/initializer prefixes
 | 
			
		||||
            # _GLOBAL__sub_I_<mangled> -> extract <mangled> for demangling
 | 
			
		||||
            prefix = ""
 | 
			
		||||
            for gcc_prefix in _GCC_PREFIX_ANNOTATIONS:
 | 
			
		||||
                if stripped.startswith(gcc_prefix):
 | 
			
		||||
                    prefix = gcc_prefix
 | 
			
		||||
                    stripped = stripped[len(prefix) :]
 | 
			
		||||
                    break
 | 
			
		||||
 | 
			
		||||
            symbols_stripped.append(stripped)
 | 
			
		||||
            symbols_prefixes.append(prefix)
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            # Send all symbols to c++filt at once
 | 
			
		||||
            result = subprocess.run(
 | 
			
		||||
                [cppfilt_cmd],
 | 
			
		||||
                input="\n".join(symbols_stripped),
 | 
			
		||||
                capture_output=True,
 | 
			
		||||
                text=True,
 | 
			
		||||
                check=False,
 | 
			
		||||
            )
 | 
			
		||||
        except (subprocess.SubprocessError, OSError, UnicodeDecodeError) as e:
 | 
			
		||||
            # On error, cache originals
 | 
			
		||||
            _LOGGER.warning("Failed to batch demangle symbols: %s", e)
 | 
			
		||||
            for symbol in symbols:
 | 
			
		||||
                self._demangle_cache[symbol] = symbol
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        if result.returncode != 0:
 | 
			
		||||
            _LOGGER.warning(
 | 
			
		||||
                "c++filt exited with code %d: %s",
 | 
			
		||||
                result.returncode,
 | 
			
		||||
                result.stderr[:200] if result.stderr else "(no error output)",
 | 
			
		||||
            )
 | 
			
		||||
            # Cache originals on failure
 | 
			
		||||
            for symbol in symbols:
 | 
			
		||||
                self._demangle_cache[symbol] = symbol
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        # Process demangled output
 | 
			
		||||
        self._process_demangled_output(
 | 
			
		||||
            symbols, symbols_stripped, symbols_prefixes, result.stdout, cppfilt_cmd
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    def _process_demangled_output(
 | 
			
		||||
        self,
 | 
			
		||||
        symbols: list[str],
 | 
			
		||||
        symbols_stripped: list[str],
 | 
			
		||||
        symbols_prefixes: list[str],
 | 
			
		||||
        demangled_output: str,
 | 
			
		||||
        cppfilt_cmd: str,
 | 
			
		||||
    ) -> None:
 | 
			
		||||
        """Process demangled symbol output and populate cache.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            symbols: Original symbol names
 | 
			
		||||
            symbols_stripped: Stripped symbol names sent to c++filt
 | 
			
		||||
            symbols_prefixes: Removed prefixes to restore
 | 
			
		||||
            demangled_output: Output from c++filt
 | 
			
		||||
            cppfilt_cmd: Path to c++filt command (for logging)
 | 
			
		||||
        """
 | 
			
		||||
        demangled_lines = demangled_output.strip().split("\n")
 | 
			
		||||
        failed_count = 0
 | 
			
		||||
 | 
			
		||||
        for original, stripped, prefix, demangled in zip(
 | 
			
		||||
            symbols, symbols_stripped, symbols_prefixes, demangled_lines
 | 
			
		||||
        ):
 | 
			
		||||
            # Add back any prefix that was removed
 | 
			
		||||
            demangled = self._restore_symbol_prefix(prefix, stripped, demangled)
 | 
			
		||||
 | 
			
		||||
            # If we stripped a suffix, add it back to the demangled name for clarity
 | 
			
		||||
            if original != stripped and not prefix:
 | 
			
		||||
                demangled = self._restore_symbol_suffix(original, demangled)
 | 
			
		||||
 | 
			
		||||
            self._demangle_cache[original] = demangled
 | 
			
		||||
 | 
			
		||||
            # Log symbols that failed to demangle (stayed the same as stripped version)
 | 
			
		||||
            if stripped == demangled and stripped.startswith("_Z"):
 | 
			
		||||
                failed_count += 1
 | 
			
		||||
                if failed_count <= 5:  # Only log first 5 failures
 | 
			
		||||
                    _LOGGER.warning("Failed to demangle: %s", original)
 | 
			
		||||
 | 
			
		||||
        if failed_count == 0:
 | 
			
		||||
            _LOGGER.info("Successfully demangled all %d symbols", len(symbols))
 | 
			
		||||
            return
 | 
			
		||||
 | 
			
		||||
        _LOGGER.warning(
 | 
			
		||||
            "Failed to demangle %d/%d symbols using %s",
 | 
			
		||||
            failed_count,
 | 
			
		||||
            len(symbols),
 | 
			
		||||
            cppfilt_cmd,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _restore_symbol_prefix(prefix: str, stripped: str, demangled: str) -> str:
 | 
			
		||||
        """Restore prefix that was removed before demangling.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            prefix: Prefix that was removed (e.g., "_GLOBAL__sub_I_")
 | 
			
		||||
            stripped: Stripped symbol name
 | 
			
		||||
            demangled: Demangled symbol name
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            Demangled name with prefix restored/annotated
 | 
			
		||||
        """
 | 
			
		||||
        if not prefix:
 | 
			
		||||
            return demangled
 | 
			
		||||
 | 
			
		||||
        # Successfully demangled - add descriptive prefix
 | 
			
		||||
        if demangled != stripped and (
 | 
			
		||||
            annotation := _GCC_PREFIX_ANNOTATIONS.get(prefix)
 | 
			
		||||
        ):
 | 
			
		||||
            return f"[{annotation}: {demangled}]"
 | 
			
		||||
 | 
			
		||||
        # Failed to demangle - restore original prefix
 | 
			
		||||
        return prefix + demangled
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _restore_symbol_suffix(original: str, demangled: str) -> str:
 | 
			
		||||
        """Restore GCC optimization suffix that was removed before demangling.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            original: Original symbol name with suffix
 | 
			
		||||
            demangled: Demangled symbol name without suffix
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            Demangled name with suffix annotation
 | 
			
		||||
        """
 | 
			
		||||
        if suffix_match := _GCC_OPTIMIZATION_SUFFIX_PATTERN.search(original):
 | 
			
		||||
            return f"{demangled} [{suffix_match.group(1)}]"
 | 
			
		||||
        return demangled
 | 
			
		||||
 | 
			
		||||
    def _demangle_symbol(self, symbol: str) -> str:
 | 
			
		||||
        """Get demangled C++ symbol name from cache."""
 | 
			
		||||
        return self._demangle_cache.get(symbol, symbol)
 | 
			
		||||
 | 
			
		||||
    def _categorize_esphome_core_symbol(self, demangled: str) -> str:
 | 
			
		||||
        """Categorize ESPHome core symbols into subcategories."""
 | 
			
		||||
        # Special patterns that need to be checked separately
 | 
			
		||||
        if any(pattern in demangled for pattern in _CPP_RUNTIME_PATTERNS):
 | 
			
		||||
            return "C++ Runtime (vtables/RTTI)"
 | 
			
		||||
 | 
			
		||||
        if demangled.startswith(_NAMESPACE_STD):
 | 
			
		||||
            return "C++ STL"
 | 
			
		||||
 | 
			
		||||
        # Check against patterns from const.py
 | 
			
		||||
        for category, patterns in CORE_SUBCATEGORY_PATTERNS.items():
 | 
			
		||||
            if any(pattern in demangled for pattern in patterns):
 | 
			
		||||
                return category
 | 
			
		||||
 | 
			
		||||
        return "Other Core"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    from .cli import main
 | 
			
		||||
 | 
			
		||||
    main()
 | 
			
		||||
@@ -1,6 +0,0 @@
 | 
			
		||||
"""Main entry point for running the memory analyzer as a module."""
 | 
			
		||||
 | 
			
		||||
from .cli import main
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    main()
 | 
			
		||||
@@ -1,421 +0,0 @@
 | 
			
		||||
"""CLI interface for memory analysis with report generation."""
 | 
			
		||||
 | 
			
		||||
from collections import defaultdict
 | 
			
		||||
import sys
 | 
			
		||||
 | 
			
		||||
from . import (
 | 
			
		||||
    _COMPONENT_API,
 | 
			
		||||
    _COMPONENT_CORE,
 | 
			
		||||
    _COMPONENT_PREFIX_ESPHOME,
 | 
			
		||||
    _COMPONENT_PREFIX_EXTERNAL,
 | 
			
		||||
    MemoryAnalyzer,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class MemoryAnalyzerCLI(MemoryAnalyzer):
 | 
			
		||||
    """Memory analyzer with CLI-specific report generation."""
 | 
			
		||||
 | 
			
		||||
    # Column width constants
 | 
			
		||||
    COL_COMPONENT: int = 29
 | 
			
		||||
    COL_FLASH_TEXT: int = 14
 | 
			
		||||
    COL_FLASH_DATA: int = 14
 | 
			
		||||
    COL_RAM_DATA: int = 12
 | 
			
		||||
    COL_RAM_BSS: int = 12
 | 
			
		||||
    COL_TOTAL_FLASH: int = 15
 | 
			
		||||
    COL_TOTAL_RAM: int = 12
 | 
			
		||||
    COL_SEPARATOR: int = 3  # " | "
 | 
			
		||||
 | 
			
		||||
    # Core analysis column widths
 | 
			
		||||
    COL_CORE_SUBCATEGORY: int = 30
 | 
			
		||||
    COL_CORE_SIZE: int = 12
 | 
			
		||||
    COL_CORE_COUNT: int = 6
 | 
			
		||||
    COL_CORE_PERCENT: int = 10
 | 
			
		||||
 | 
			
		||||
    # Calculate table width once at class level
 | 
			
		||||
    TABLE_WIDTH: int = (
 | 
			
		||||
        COL_COMPONENT
 | 
			
		||||
        + COL_SEPARATOR
 | 
			
		||||
        + COL_FLASH_TEXT
 | 
			
		||||
        + COL_SEPARATOR
 | 
			
		||||
        + COL_FLASH_DATA
 | 
			
		||||
        + COL_SEPARATOR
 | 
			
		||||
        + COL_RAM_DATA
 | 
			
		||||
        + COL_SEPARATOR
 | 
			
		||||
        + COL_RAM_BSS
 | 
			
		||||
        + COL_SEPARATOR
 | 
			
		||||
        + COL_TOTAL_FLASH
 | 
			
		||||
        + COL_SEPARATOR
 | 
			
		||||
        + COL_TOTAL_RAM
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    @staticmethod
 | 
			
		||||
    def _make_separator_line(*widths: int) -> str:
 | 
			
		||||
        """Create a separator line with given column widths.
 | 
			
		||||
 | 
			
		||||
        Args:
 | 
			
		||||
            widths: Column widths to create separators for
 | 
			
		||||
 | 
			
		||||
        Returns:
 | 
			
		||||
            Separator line like "----+---------+-----"
 | 
			
		||||
        """
 | 
			
		||||
        return "-+-".join("-" * width for width in widths)
 | 
			
		||||
 | 
			
		||||
    # Pre-computed separator lines
 | 
			
		||||
    MAIN_TABLE_SEPARATOR: str = _make_separator_line(
 | 
			
		||||
        COL_COMPONENT,
 | 
			
		||||
        COL_FLASH_TEXT,
 | 
			
		||||
        COL_FLASH_DATA,
 | 
			
		||||
        COL_RAM_DATA,
 | 
			
		||||
        COL_RAM_BSS,
 | 
			
		||||
        COL_TOTAL_FLASH,
 | 
			
		||||
        COL_TOTAL_RAM,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    CORE_TABLE_SEPARATOR: str = _make_separator_line(
 | 
			
		||||
        COL_CORE_SUBCATEGORY,
 | 
			
		||||
        COL_CORE_SIZE,
 | 
			
		||||
        COL_CORE_COUNT,
 | 
			
		||||
        COL_CORE_PERCENT,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def generate_report(self, detailed: bool = False) -> str:
 | 
			
		||||
        """Generate a formatted memory report."""
 | 
			
		||||
        components = sorted(
 | 
			
		||||
            self.components.items(), key=lambda x: x[1].flash_total, reverse=True
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Calculate totals
 | 
			
		||||
        total_flash = sum(c.flash_total for _, c in components)
 | 
			
		||||
        total_ram = sum(c.ram_total for _, c in components)
 | 
			
		||||
 | 
			
		||||
        # Build report
 | 
			
		||||
        lines: list[str] = []
 | 
			
		||||
 | 
			
		||||
        lines.append("=" * self.TABLE_WIDTH)
 | 
			
		||||
        lines.append("Component Memory Analysis".center(self.TABLE_WIDTH))
 | 
			
		||||
        lines.append("=" * self.TABLE_WIDTH)
 | 
			
		||||
        lines.append("")
 | 
			
		||||
 | 
			
		||||
        # Main table - fixed column widths
 | 
			
		||||
        lines.append(
 | 
			
		||||
            f"{'Component':<{self.COL_COMPONENT}} | {'Flash (text)':>{self.COL_FLASH_TEXT}} | {'Flash (data)':>{self.COL_FLASH_DATA}} | {'RAM (data)':>{self.COL_RAM_DATA}} | {'RAM (bss)':>{self.COL_RAM_BSS}} | {'Total Flash':>{self.COL_TOTAL_FLASH}} | {'Total RAM':>{self.COL_TOTAL_RAM}}"
 | 
			
		||||
        )
 | 
			
		||||
        lines.append(self.MAIN_TABLE_SEPARATOR)
 | 
			
		||||
 | 
			
		||||
        for name, mem in components:
 | 
			
		||||
            if mem.flash_total > 0 or mem.ram_total > 0:
 | 
			
		||||
                flash_rodata = mem.rodata_size + mem.data_size
 | 
			
		||||
                lines.append(
 | 
			
		||||
                    f"{name:<{self.COL_COMPONENT}} | {mem.text_size:>{self.COL_FLASH_TEXT - 2},} B | {flash_rodata:>{self.COL_FLASH_DATA - 2},} B | "
 | 
			
		||||
                    f"{mem.data_size:>{self.COL_RAM_DATA - 2},} B | {mem.bss_size:>{self.COL_RAM_BSS - 2},} B | "
 | 
			
		||||
                    f"{mem.flash_total:>{self.COL_TOTAL_FLASH - 2},} B | {mem.ram_total:>{self.COL_TOTAL_RAM - 2},} B"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        lines.append(self.MAIN_TABLE_SEPARATOR)
 | 
			
		||||
        lines.append(
 | 
			
		||||
            f"{'TOTAL':<{self.COL_COMPONENT}} | {' ':>{self.COL_FLASH_TEXT}} | {' ':>{self.COL_FLASH_DATA}} | "
 | 
			
		||||
            f"{' ':>{self.COL_RAM_DATA}} | {' ':>{self.COL_RAM_BSS}} | "
 | 
			
		||||
            f"{total_flash:>{self.COL_TOTAL_FLASH - 2},} B | {total_ram:>{self.COL_TOTAL_RAM - 2},} B"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Top consumers
 | 
			
		||||
        lines.append("")
 | 
			
		||||
        lines.append("Top Flash Consumers:")
 | 
			
		||||
        for i, (name, mem) in enumerate(components[:25]):
 | 
			
		||||
            if mem.flash_total > 0:
 | 
			
		||||
                percentage = (
 | 
			
		||||
                    (mem.flash_total / total_flash * 100) if total_flash > 0 else 0
 | 
			
		||||
                )
 | 
			
		||||
                lines.append(
 | 
			
		||||
                    f"{i + 1}. {name} ({mem.flash_total:,} B) - {percentage:.1f}% of analyzed flash"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        lines.append("")
 | 
			
		||||
        lines.append("Top RAM Consumers:")
 | 
			
		||||
        ram_components = sorted(components, key=lambda x: x[1].ram_total, reverse=True)
 | 
			
		||||
        for i, (name, mem) in enumerate(ram_components[:25]):
 | 
			
		||||
            if mem.ram_total > 0:
 | 
			
		||||
                percentage = (mem.ram_total / total_ram * 100) if total_ram > 0 else 0
 | 
			
		||||
                lines.append(
 | 
			
		||||
                    f"{i + 1}. {name} ({mem.ram_total:,} B) - {percentage:.1f}% of analyzed RAM"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
        lines.append("")
 | 
			
		||||
        lines.append(
 | 
			
		||||
            "Note: This analysis covers symbols in the ELF file. Some runtime allocations may not be included."
 | 
			
		||||
        )
 | 
			
		||||
        lines.append("=" * self.TABLE_WIDTH)
 | 
			
		||||
 | 
			
		||||
        # Add ESPHome core detailed analysis if there are core symbols
 | 
			
		||||
        if self._esphome_core_symbols:
 | 
			
		||||
            lines.append("")
 | 
			
		||||
            lines.append("=" * self.TABLE_WIDTH)
 | 
			
		||||
            lines.append(
 | 
			
		||||
                f"{_COMPONENT_CORE} Detailed Analysis".center(self.TABLE_WIDTH)
 | 
			
		||||
            )
 | 
			
		||||
            lines.append("=" * self.TABLE_WIDTH)
 | 
			
		||||
            lines.append("")
 | 
			
		||||
 | 
			
		||||
            # Group core symbols by subcategory
 | 
			
		||||
            core_subcategories: dict[str, list[tuple[str, str, int]]] = defaultdict(
 | 
			
		||||
                list
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            for symbol, demangled, size in self._esphome_core_symbols:
 | 
			
		||||
                # Categorize based on demangled name patterns
 | 
			
		||||
                subcategory = self._categorize_esphome_core_symbol(demangled)
 | 
			
		||||
                core_subcategories[subcategory].append((symbol, demangled, size))
 | 
			
		||||
 | 
			
		||||
            # Sort subcategories by total size
 | 
			
		||||
            sorted_subcategories = sorted(
 | 
			
		||||
                [
 | 
			
		||||
                    (name, symbols, sum(s[2] for s in symbols))
 | 
			
		||||
                    for name, symbols in core_subcategories.items()
 | 
			
		||||
                ],
 | 
			
		||||
                key=lambda x: x[2],
 | 
			
		||||
                reverse=True,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            lines.append(
 | 
			
		||||
                f"{'Subcategory':<{self.COL_CORE_SUBCATEGORY}} | {'Size':>{self.COL_CORE_SIZE}} | "
 | 
			
		||||
                f"{'Count':>{self.COL_CORE_COUNT}} | {'% of Core':>{self.COL_CORE_PERCENT}}"
 | 
			
		||||
            )
 | 
			
		||||
            lines.append(self.CORE_TABLE_SEPARATOR)
 | 
			
		||||
 | 
			
		||||
            core_total = sum(size for _, _, size in self._esphome_core_symbols)
 | 
			
		||||
 | 
			
		||||
            for subcategory, symbols, total_size in sorted_subcategories:
 | 
			
		||||
                percentage = (total_size / core_total * 100) if core_total > 0 else 0
 | 
			
		||||
                lines.append(
 | 
			
		||||
                    f"{subcategory:<{self.COL_CORE_SUBCATEGORY}} | {total_size:>{self.COL_CORE_SIZE - 2},} B | "
 | 
			
		||||
                    f"{len(symbols):>{self.COL_CORE_COUNT}} | {percentage:>{self.COL_CORE_PERCENT - 1}.1f}%"
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            # Top 15 largest core symbols
 | 
			
		||||
            lines.append("")
 | 
			
		||||
            lines.append(f"Top 15 Largest {_COMPONENT_CORE} Symbols:")
 | 
			
		||||
            sorted_core_symbols = sorted(
 | 
			
		||||
                self._esphome_core_symbols, key=lambda x: x[2], reverse=True
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            for i, (symbol, demangled, size) in enumerate(sorted_core_symbols[:15]):
 | 
			
		||||
                lines.append(f"{i + 1}. {demangled} ({size:,} B)")
 | 
			
		||||
 | 
			
		||||
            lines.append("=" * self.TABLE_WIDTH)
 | 
			
		||||
 | 
			
		||||
        # Add detailed analysis for top ESPHome and external components
 | 
			
		||||
        esphome_components = [
 | 
			
		||||
            (name, mem)
 | 
			
		||||
            for name, mem in components
 | 
			
		||||
            if name.startswith(_COMPONENT_PREFIX_ESPHOME) and name != _COMPONENT_CORE
 | 
			
		||||
        ]
 | 
			
		||||
        external_components = [
 | 
			
		||||
            (name, mem)
 | 
			
		||||
            for name, mem in components
 | 
			
		||||
            if name.startswith(_COMPONENT_PREFIX_EXTERNAL)
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        top_esphome_components = sorted(
 | 
			
		||||
            esphome_components, key=lambda x: x[1].flash_total, reverse=True
 | 
			
		||||
        )[:30]
 | 
			
		||||
 | 
			
		||||
        # Include all external components (they're usually important)
 | 
			
		||||
        top_external_components = sorted(
 | 
			
		||||
            external_components, key=lambda x: x[1].flash_total, reverse=True
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Check if API component exists and ensure it's included
 | 
			
		||||
        api_component = None
 | 
			
		||||
        for name, mem in components:
 | 
			
		||||
            if name == _COMPONENT_API:
 | 
			
		||||
                api_component = (name, mem)
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
        # Also include wifi_stack and other important system components if they exist
 | 
			
		||||
        system_components_to_include = [
 | 
			
		||||
            # Empty list - we've finished debugging symbol categorization
 | 
			
		||||
            # Add component names here if you need to debug their symbols
 | 
			
		||||
        ]
 | 
			
		||||
        system_components = [
 | 
			
		||||
            (name, mem)
 | 
			
		||||
            for name, mem in components
 | 
			
		||||
            if name in system_components_to_include
 | 
			
		||||
        ]
 | 
			
		||||
 | 
			
		||||
        # Combine all components to analyze: top ESPHome + all external + API if not already included + system components
 | 
			
		||||
        components_to_analyze = (
 | 
			
		||||
            list(top_esphome_components)
 | 
			
		||||
            + list(top_external_components)
 | 
			
		||||
            + system_components
 | 
			
		||||
        )
 | 
			
		||||
        if api_component and api_component not in components_to_analyze:
 | 
			
		||||
            components_to_analyze.append(api_component)
 | 
			
		||||
 | 
			
		||||
        if components_to_analyze:
 | 
			
		||||
            for comp_name, comp_mem in components_to_analyze:
 | 
			
		||||
                if not (comp_symbols := self._component_symbols.get(comp_name, [])):
 | 
			
		||||
                    continue
 | 
			
		||||
                lines.append("")
 | 
			
		||||
                lines.append("=" * self.TABLE_WIDTH)
 | 
			
		||||
                lines.append(f"{comp_name} Detailed Analysis".center(self.TABLE_WIDTH))
 | 
			
		||||
                lines.append("=" * self.TABLE_WIDTH)
 | 
			
		||||
                lines.append("")
 | 
			
		||||
 | 
			
		||||
                # Sort symbols by size
 | 
			
		||||
                sorted_symbols = sorted(comp_symbols, key=lambda x: x[2], reverse=True)
 | 
			
		||||
 | 
			
		||||
                lines.append(f"Total symbols: {len(sorted_symbols)}")
 | 
			
		||||
                lines.append(f"Total size: {comp_mem.flash_total:,} B")
 | 
			
		||||
                lines.append("")
 | 
			
		||||
 | 
			
		||||
                # Show all symbols > 100 bytes for better visibility
 | 
			
		||||
                large_symbols = [
 | 
			
		||||
                    (sym, dem, size) for sym, dem, size in sorted_symbols if size > 100
 | 
			
		||||
                ]
 | 
			
		||||
 | 
			
		||||
                lines.append(
 | 
			
		||||
                    f"{comp_name} Symbols > 100 B ({len(large_symbols)} symbols):"
 | 
			
		||||
                )
 | 
			
		||||
                for i, (symbol, demangled, size) in enumerate(large_symbols):
 | 
			
		||||
                    lines.append(f"{i + 1}. {demangled} ({size:,} B)")
 | 
			
		||||
 | 
			
		||||
                lines.append("=" * self.TABLE_WIDTH)
 | 
			
		||||
 | 
			
		||||
        return "\n".join(lines)
 | 
			
		||||
 | 
			
		||||
    def dump_uncategorized_symbols(self, output_file: str | None = None) -> None:
 | 
			
		||||
        """Dump uncategorized symbols for analysis."""
 | 
			
		||||
        # Sort by size descending
 | 
			
		||||
        sorted_symbols = sorted(
 | 
			
		||||
            self._uncategorized_symbols, key=lambda x: x[2], reverse=True
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        lines = ["Uncategorized Symbols Analysis", "=" * 80]
 | 
			
		||||
        lines.append(f"Total uncategorized symbols: {len(sorted_symbols)}")
 | 
			
		||||
        lines.append(
 | 
			
		||||
            f"Total uncategorized size: {sum(s[2] for s in sorted_symbols):,} bytes"
 | 
			
		||||
        )
 | 
			
		||||
        lines.append("")
 | 
			
		||||
        lines.append(f"{'Size':>10} | {'Symbol':<60} | Demangled")
 | 
			
		||||
        lines.append("-" * 10 + "-+-" + "-" * 60 + "-+-" + "-" * 40)
 | 
			
		||||
 | 
			
		||||
        for symbol, demangled, size in sorted_symbols[:100]:  # Top 100
 | 
			
		||||
            demangled_display = (
 | 
			
		||||
                demangled[:100] if symbol != demangled else "[not demangled]"
 | 
			
		||||
            )
 | 
			
		||||
            lines.append(f"{size:>10,} | {symbol[:60]:<60} | {demangled_display}")
 | 
			
		||||
 | 
			
		||||
        if len(sorted_symbols) > 100:
 | 
			
		||||
            lines.append(f"\n... and {len(sorted_symbols) - 100} more symbols")
 | 
			
		||||
 | 
			
		||||
        content = "\n".join(lines)
 | 
			
		||||
 | 
			
		||||
        if output_file:
 | 
			
		||||
            with open(output_file, "w", encoding="utf-8") as f:
 | 
			
		||||
                f.write(content)
 | 
			
		||||
        else:
 | 
			
		||||
            print(content)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def analyze_elf(
 | 
			
		||||
    elf_path: str,
 | 
			
		||||
    objdump_path: str | None = None,
 | 
			
		||||
    readelf_path: str | None = None,
 | 
			
		||||
    detailed: bool = False,
 | 
			
		||||
    external_components: set[str] | None = None,
 | 
			
		||||
) -> str:
 | 
			
		||||
    """Analyze an ELF file and return a memory report."""
 | 
			
		||||
    analyzer = MemoryAnalyzerCLI(
 | 
			
		||||
        elf_path, objdump_path, readelf_path, external_components
 | 
			
		||||
    )
 | 
			
		||||
    analyzer.analyze()
 | 
			
		||||
    return analyzer.generate_report(detailed)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def main():
 | 
			
		||||
    """CLI entrypoint for memory analysis."""
 | 
			
		||||
    if len(sys.argv) < 2:
 | 
			
		||||
        print("Usage: python -m esphome.analyze_memory <build_directory>")
 | 
			
		||||
        print("\nAnalyze memory usage from an ESPHome build directory.")
 | 
			
		||||
        print("The build directory should contain firmware.elf and idedata will be")
 | 
			
		||||
        print("loaded from ~/.esphome/.internal/idedata/<device>.json")
 | 
			
		||||
        print("\nExamples:")
 | 
			
		||||
        print("  python -m esphome.analyze_memory ~/.esphome/build/my-device")
 | 
			
		||||
        print("  python -m esphome.analyze_memory .esphome/build/my-device")
 | 
			
		||||
        print("  python -m esphome.analyze_memory my-device  # Short form")
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
 | 
			
		||||
    build_dir = sys.argv[1]
 | 
			
		||||
 | 
			
		||||
    # Load build directory
 | 
			
		||||
    import json
 | 
			
		||||
    from pathlib import Path
 | 
			
		||||
 | 
			
		||||
    from esphome.platformio_api import IDEData
 | 
			
		||||
 | 
			
		||||
    build_path = Path(build_dir)
 | 
			
		||||
 | 
			
		||||
    # If no path separator in name, assume it's a device name
 | 
			
		||||
    if "/" not in build_dir and not build_path.is_dir():
 | 
			
		||||
        # Try current directory first
 | 
			
		||||
        cwd_path = Path.cwd() / ".esphome" / "build" / build_dir
 | 
			
		||||
        if cwd_path.is_dir():
 | 
			
		||||
            build_path = cwd_path
 | 
			
		||||
            print(f"Using build directory: {build_path}", file=sys.stderr)
 | 
			
		||||
        else:
 | 
			
		||||
            # Fall back to home directory
 | 
			
		||||
            build_path = Path.home() / ".esphome" / "build" / build_dir
 | 
			
		||||
            print(f"Using build directory: {build_path}", file=sys.stderr)
 | 
			
		||||
 | 
			
		||||
    if not build_path.is_dir():
 | 
			
		||||
        print(f"Error: {build_path} is not a directory", file=sys.stderr)
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
 | 
			
		||||
    # Find firmware.elf
 | 
			
		||||
    elf_file = None
 | 
			
		||||
    for elf_candidate in [
 | 
			
		||||
        build_path / "firmware.elf",
 | 
			
		||||
        build_path / ".pioenvs" / build_path.name / "firmware.elf",
 | 
			
		||||
    ]:
 | 
			
		||||
        if elf_candidate.exists():
 | 
			
		||||
            elf_file = str(elf_candidate)
 | 
			
		||||
            break
 | 
			
		||||
 | 
			
		||||
    if not elf_file:
 | 
			
		||||
        print(f"Error: firmware.elf not found in {build_dir}", file=sys.stderr)
 | 
			
		||||
        sys.exit(1)
 | 
			
		||||
 | 
			
		||||
    # Find idedata.json - check current directory first, then home
 | 
			
		||||
    device_name = build_path.name
 | 
			
		||||
    idedata_candidates = [
 | 
			
		||||
        Path.cwd() / ".esphome" / "idedata" / f"{device_name}.json",
 | 
			
		||||
        Path.home() / ".esphome" / "idedata" / f"{device_name}.json",
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
    idedata = None
 | 
			
		||||
    for idedata_path in idedata_candidates:
 | 
			
		||||
        if not idedata_path.exists():
 | 
			
		||||
            continue
 | 
			
		||||
        try:
 | 
			
		||||
            with open(idedata_path, encoding="utf-8") as f:
 | 
			
		||||
                raw_data = json.load(f)
 | 
			
		||||
            idedata = IDEData(raw_data)
 | 
			
		||||
            print(f"Loaded idedata from: {idedata_path}", file=sys.stderr)
 | 
			
		||||
            break
 | 
			
		||||
        except (json.JSONDecodeError, OSError) as e:
 | 
			
		||||
            print(f"Warning: Failed to load idedata: {e}", file=sys.stderr)
 | 
			
		||||
 | 
			
		||||
    if not idedata:
 | 
			
		||||
        print(
 | 
			
		||||
            f"Warning: idedata not found (searched {idedata_candidates[0]} and {idedata_candidates[1]})",
 | 
			
		||||
            file=sys.stderr,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    analyzer = MemoryAnalyzerCLI(elf_file, idedata=idedata)
 | 
			
		||||
    analyzer.analyze()
 | 
			
		||||
    report = analyzer.generate_report()
 | 
			
		||||
    print(report)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    main()
 | 
			
		||||
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							@@ -1,121 +0,0 @@
 | 
			
		||||
"""Helper functions for memory analysis."""
 | 
			
		||||
 | 
			
		||||
from functools import cache
 | 
			
		||||
from pathlib import Path
 | 
			
		||||
 | 
			
		||||
from .const import SECTION_MAPPING
 | 
			
		||||
 | 
			
		||||
# Import namespace constant from parent module
 | 
			
		||||
# Note: This would create a circular import if done at module level,
 | 
			
		||||
# so we'll define it locally here as well
 | 
			
		||||
_NAMESPACE_ESPHOME = "esphome::"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Get the list of actual ESPHome components by scanning the components directory
 | 
			
		||||
@cache
 | 
			
		||||
def get_esphome_components():
 | 
			
		||||
    """Get set of actual ESPHome components from the components directory."""
 | 
			
		||||
    # Find the components directory relative to this file
 | 
			
		||||
    # Go up two levels from analyze_memory/helpers.py to esphome/
 | 
			
		||||
    current_dir = Path(__file__).parent.parent
 | 
			
		||||
    components_dir = current_dir / "components"
 | 
			
		||||
 | 
			
		||||
    if not components_dir.exists() or not components_dir.is_dir():
 | 
			
		||||
        return frozenset()
 | 
			
		||||
 | 
			
		||||
    return frozenset(
 | 
			
		||||
        item.name
 | 
			
		||||
        for item in components_dir.iterdir()
 | 
			
		||||
        if item.is_dir()
 | 
			
		||||
        and not item.name.startswith(".")
 | 
			
		||||
        and not item.name.startswith("__")
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@cache
 | 
			
		||||
def get_component_class_patterns(component_name: str) -> list[str]:
 | 
			
		||||
    """Generate component class name patterns for symbol matching.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        component_name: The component name (e.g., "ota", "wifi", "api")
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        List of pattern strings to match against demangled symbols
 | 
			
		||||
    """
 | 
			
		||||
    component_upper = component_name.upper()
 | 
			
		||||
    component_camel = component_name.replace("_", "").title()
 | 
			
		||||
    return [
 | 
			
		||||
        f"{_NAMESPACE_ESPHOME}{component_upper}Component",  # e.g., esphome::OTAComponent
 | 
			
		||||
        f"{_NAMESPACE_ESPHOME}ESPHome{component_upper}Component",  # e.g., esphome::ESPHomeOTAComponent
 | 
			
		||||
        f"{_NAMESPACE_ESPHOME}{component_camel}Component",  # e.g., esphome::OtaComponent
 | 
			
		||||
        f"{_NAMESPACE_ESPHOME}ESPHome{component_camel}Component",  # e.g., esphome::ESPHomeOtaComponent
 | 
			
		||||
    ]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def map_section_name(raw_section: str) -> str | None:
 | 
			
		||||
    """Map raw section name to standard section.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        raw_section: Raw section name from ELF file (e.g., ".iram0.text", ".rodata.str1.1")
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        Standard section name (".text", ".rodata", ".data", ".bss") or None
 | 
			
		||||
    """
 | 
			
		||||
    for standard_section, patterns in SECTION_MAPPING.items():
 | 
			
		||||
        if any(pattern in raw_section for pattern in patterns):
 | 
			
		||||
            return standard_section
 | 
			
		||||
    return None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def parse_symbol_line(line: str) -> tuple[str, str, int, str] | None:
 | 
			
		||||
    """Parse a single symbol line from objdump output.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        line: Line from objdump -t output
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        Tuple of (section, name, size, address) or None if not a valid symbol.
 | 
			
		||||
        Format: address l/g w/d F/O section size name
 | 
			
		||||
        Example: 40084870 l     F .iram0.text    00000000 _xt_user_exc
 | 
			
		||||
    """
 | 
			
		||||
    parts = line.split()
 | 
			
		||||
    if len(parts) < 5:
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        # Validate and extract address
 | 
			
		||||
        address = parts[0]
 | 
			
		||||
        int(address, 16)
 | 
			
		||||
    except ValueError:
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    # Look for F (function) or O (object) flag
 | 
			
		||||
    if "F" not in parts and "O" not in parts:
 | 
			
		||||
        return None
 | 
			
		||||
 | 
			
		||||
    # Find section, size, and name
 | 
			
		||||
    for i, part in enumerate(parts):
 | 
			
		||||
        if not part.startswith("."):
 | 
			
		||||
            continue
 | 
			
		||||
 | 
			
		||||
        section = map_section_name(part)
 | 
			
		||||
        if not section:
 | 
			
		||||
            break
 | 
			
		||||
 | 
			
		||||
        # Need at least size field after section
 | 
			
		||||
        if i + 1 >= len(parts):
 | 
			
		||||
            break
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            size = int(parts[i + 1], 16)
 | 
			
		||||
        except ValueError:
 | 
			
		||||
            break
 | 
			
		||||
 | 
			
		||||
        # Need symbol name and non-zero size
 | 
			
		||||
        if i + 2 >= len(parts) or size == 0:
 | 
			
		||||
            break
 | 
			
		||||
 | 
			
		||||
        name = " ".join(parts[i + 2 :])
 | 
			
		||||
        return (section, name, size, address)
 | 
			
		||||
 | 
			
		||||
    return None
 | 
			
		||||
@@ -15,13 +15,8 @@ from esphome.const import (
 | 
			
		||||
    CONF_TYPE_ID,
 | 
			
		||||
    CONF_UPDATE_INTERVAL,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import ID, Lambda
 | 
			
		||||
from esphome.cpp_generator import (
 | 
			
		||||
    LambdaExpression,
 | 
			
		||||
    MockObj,
 | 
			
		||||
    MockObjClass,
 | 
			
		||||
    TemplateArgsType,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import ID
 | 
			
		||||
from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType
 | 
			
		||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
from esphome.util import Registry
 | 
			
		||||
@@ -92,7 +87,6 @@ def validate_potentially_or_condition(value):
 | 
			
		||||
 | 
			
		||||
DelayAction = cg.esphome_ns.class_("DelayAction", Action, cg.Component)
 | 
			
		||||
LambdaAction = cg.esphome_ns.class_("LambdaAction", Action)
 | 
			
		||||
StatelessLambdaAction = cg.esphome_ns.class_("StatelessLambdaAction", Action)
 | 
			
		||||
IfAction = cg.esphome_ns.class_("IfAction", Action)
 | 
			
		||||
WhileAction = cg.esphome_ns.class_("WhileAction", Action)
 | 
			
		||||
RepeatAction = cg.esphome_ns.class_("RepeatAction", Action)
 | 
			
		||||
@@ -103,40 +97,9 @@ ResumeComponentAction = cg.esphome_ns.class_("ResumeComponentAction", Action)
 | 
			
		||||
Automation = cg.esphome_ns.class_("Automation")
 | 
			
		||||
 | 
			
		||||
LambdaCondition = cg.esphome_ns.class_("LambdaCondition", Condition)
 | 
			
		||||
StatelessLambdaCondition = cg.esphome_ns.class_("StatelessLambdaCondition", Condition)
 | 
			
		||||
ForCondition = cg.esphome_ns.class_("ForCondition", Condition, cg.Component)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def new_lambda_pvariable(
 | 
			
		||||
    id_obj: ID,
 | 
			
		||||
    lambda_expr: LambdaExpression,
 | 
			
		||||
    stateless_class: MockObjClass,
 | 
			
		||||
    template_arg: cg.TemplateArguments | None = None,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    """Create Pvariable for lambda, using stateless class if applicable.
 | 
			
		||||
 | 
			
		||||
    Combines ID selection and Pvariable creation in one call. For stateless
 | 
			
		||||
    lambdas (empty capture), uses function pointer instead of std::function.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        id_obj: The ID object (action_id, condition_id, or filter_id)
 | 
			
		||||
        lambda_expr: The lambda expression object
 | 
			
		||||
        stateless_class: The stateless class to use for stateless lambdas
 | 
			
		||||
        template_arg: Optional template arguments (for actions/conditions)
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        The created Pvariable
 | 
			
		||||
    """
 | 
			
		||||
    # For stateless lambdas, use function pointer instead of std::function
 | 
			
		||||
    if lambda_expr.capture == "":
 | 
			
		||||
        id_obj = id_obj.copy()
 | 
			
		||||
        id_obj.type = stateless_class
 | 
			
		||||
 | 
			
		||||
    if template_arg is not None:
 | 
			
		||||
        return cg.new_Pvariable(id_obj, template_arg, lambda_expr)
 | 
			
		||||
    return cg.new_Pvariable(id_obj, lambda_expr)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_automation(extra_schema=None, extra_validators=None, single=False):
 | 
			
		||||
    if extra_schema is None:
 | 
			
		||||
        extra_schema = {}
 | 
			
		||||
@@ -182,7 +145,7 @@ def validate_automation(extra_schema=None, extra_validators=None, single=False):
 | 
			
		||||
            value = cv.Schema([extra_validators])(value)
 | 
			
		||||
        if single:
 | 
			
		||||
            if len(value) != 1:
 | 
			
		||||
                raise cv.Invalid("This trigger allows only a single automation")
 | 
			
		||||
                raise cv.Invalid("Cannot have more than 1 automation for templates")
 | 
			
		||||
            return value[0]
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
@@ -277,9 +240,7 @@ async def lambda_condition_to_code(
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    lambda_ = await cg.process_lambda(config, args, return_type=bool)
 | 
			
		||||
    return new_lambda_pvariable(
 | 
			
		||||
        condition_id, lambda_, StatelessLambdaCondition, template_arg
 | 
			
		||||
    )
 | 
			
		||||
    return cg.new_Pvariable(condition_id, template_arg, lambda_)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_condition(
 | 
			
		||||
@@ -310,30 +271,6 @@ async def for_condition_to_code(
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_condition(
 | 
			
		||||
    "component.is_idle",
 | 
			
		||||
    LambdaCondition,
 | 
			
		||||
    maybe_simple_id(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_ID): cv.use_id(cg.Component),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
async def component_is_idle_condition_to_code(
 | 
			
		||||
    config: ConfigType,
 | 
			
		||||
    condition_id: ID,
 | 
			
		||||
    template_arg: cg.TemplateArguments,
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    comp = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    lambda_ = await cg.process_lambda(
 | 
			
		||||
        Lambda(f"return {comp}->is_idle();"), args, return_type=bool
 | 
			
		||||
    )
 | 
			
		||||
    return new_lambda_pvariable(
 | 
			
		||||
        condition_id, lambda_, StatelessLambdaCondition, template_arg
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_action(
 | 
			
		||||
    "delay", DelayAction, cv.templatable(cv.positive_time_period_milliseconds)
 | 
			
		||||
)
 | 
			
		||||
@@ -469,7 +406,7 @@ async def lambda_action_to_code(
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    lambda_ = await cg.process_lambda(config, args, return_type=cg.void)
 | 
			
		||||
    return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg)
 | 
			
		||||
    return cg.new_Pvariable(action_id, template_arg, lambda_)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_action(
 | 
			
		||||
 
 | 
			
		||||
@@ -62,7 +62,6 @@ from esphome.cpp_types import (  # noqa: F401
 | 
			
		||||
    EntityBase,
 | 
			
		||||
    EntityCategory,
 | 
			
		||||
    ESPTime,
 | 
			
		||||
    FixedVector,
 | 
			
		||||
    GPIOPin,
 | 
			
		||||
    InternalGPIOPin,
 | 
			
		||||
    JsonObject,
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ static const char *const TAG = "adalight_light_effect";
 | 
			
		||||
static const uint32_t ADALIGHT_ACK_INTERVAL = 1000;
 | 
			
		||||
static const uint32_t ADALIGHT_RECEIVE_TIMEOUT = 1000;
 | 
			
		||||
 | 
			
		||||
AdalightLightEffect::AdalightLightEffect(const char *name) : AddressableLightEffect(name) {}
 | 
			
		||||
AdalightLightEffect::AdalightLightEffect(const std::string &name) : AddressableLightEffect(name) {}
 | 
			
		||||
 | 
			
		||||
void AdalightLightEffect::start() {
 | 
			
		||||
  AddressableLightEffect::start();
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ namespace adalight {
 | 
			
		||||
 | 
			
		||||
class AdalightLightEffect : public light::AddressableLightEffect, public uart::UARTDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  AdalightLightEffect(const char *name);
 | 
			
		||||
  AdalightLightEffect(const std::string &name);
 | 
			
		||||
 | 
			
		||||
  void start() override;
 | 
			
		||||
  void stop() override;
 | 
			
		||||
 
 | 
			
		||||
@@ -172,6 +172,12 @@ def alarm_control_panel_schema(
 | 
			
		||||
    return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Remove before 2025.11.0
 | 
			
		||||
ALARM_CONTROL_PANEL_SCHEMA = alarm_control_panel_schema(AlarmControlPanel)
 | 
			
		||||
ALARM_CONTROL_PANEL_SCHEMA.add_extra(
 | 
			
		||||
    cv.deprecated_schema_constant("alarm_control_panel")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.use_id(AlarmControlPanel),
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  climate::ClimateTraits traits() override {
 | 
			
		||||
    auto traits = climate::ClimateTraits();
 | 
			
		||||
    traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
 | 
			
		||||
    traits.set_supports_current_temperature(true);
 | 
			
		||||
    traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::ClimateMode::CLIMATE_MODE_HEAT});
 | 
			
		||||
    traits.set_visual_min_temperature(25.0);
 | 
			
		||||
    traits.set_visual_max_temperature(100.0);
 | 
			
		||||
 
 | 
			
		||||
@@ -71,12 +71,10 @@ SERVICE_ARG_NATIVE_TYPES = {
 | 
			
		||||
    "int": cg.int32,
 | 
			
		||||
    "float": float,
 | 
			
		||||
    "string": cg.std_string,
 | 
			
		||||
    "bool[]": cg.FixedVector.template(bool).operator("const").operator("ref"),
 | 
			
		||||
    "int[]": cg.FixedVector.template(cg.int32).operator("const").operator("ref"),
 | 
			
		||||
    "float[]": cg.FixedVector.template(float).operator("const").operator("ref"),
 | 
			
		||||
    "string[]": cg.FixedVector.template(cg.std_string)
 | 
			
		||||
    .operator("const")
 | 
			
		||||
    .operator("ref"),
 | 
			
		||||
    "bool[]": cg.std_vector.template(bool),
 | 
			
		||||
    "int[]": cg.std_vector.template(cg.int32),
 | 
			
		||||
    "float[]": cg.std_vector.template(float),
 | 
			
		||||
    "string[]": cg.std_vector.template(cg.std_string),
 | 
			
		||||
}
 | 
			
		||||
CONF_ENCRYPTION = "encryption"
 | 
			
		||||
CONF_BATCH_DELAY = "batch_delay"
 | 
			
		||||
@@ -157,17 +155,6 @@ def _validate_api_config(config: ConfigType) -> ConfigType:
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _consume_api_sockets(config: ConfigType) -> ConfigType:
 | 
			
		||||
    """Register socket needs for API component."""
 | 
			
		||||
    from esphome.components import socket
 | 
			
		||||
 | 
			
		||||
    # API needs 1 listening socket + typically 3 concurrent client connections
 | 
			
		||||
    # (not max_connections, which is the upper limit rarely reached)
 | 
			
		||||
    sockets_needed = 1 + 3
 | 
			
		||||
    socket.consume_sockets(sockets_needed, "api")(config)
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
@@ -235,7 +222,6 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
 | 
			
		||||
    _validate_api_config,
 | 
			
		||||
    _consume_api_sockets,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -260,10 +246,6 @@ async def to_code(config):
 | 
			
		||||
    if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
 | 
			
		||||
        cg.add_define("USE_API_SERVICES")
 | 
			
		||||
 | 
			
		||||
    # Set USE_API_CUSTOM_SERVICES if external components need dynamic service registration
 | 
			
		||||
    if config[CONF_CUSTOM_SERVICES]:
 | 
			
		||||
        cg.add_define("USE_API_CUSTOM_SERVICES")
 | 
			
		||||
 | 
			
		||||
    if config[CONF_HOMEASSISTANT_SERVICES]:
 | 
			
		||||
        cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
 | 
			
		||||
 | 
			
		||||
@@ -271,8 +253,6 @@ async def to_code(config):
 | 
			
		||||
        cg.add_define("USE_API_HOMEASSISTANT_STATES")
 | 
			
		||||
 | 
			
		||||
    if actions := config.get(CONF_ACTIONS, []):
 | 
			
		||||
        # Collect all triggers first, then register all at once with initializer_list
 | 
			
		||||
        triggers: list[cg.Pvariable] = []
 | 
			
		||||
        for conf in actions:
 | 
			
		||||
            template_args = []
 | 
			
		||||
            func_args = []
 | 
			
		||||
@@ -286,10 +266,8 @@ async def to_code(config):
 | 
			
		||||
            trigger = cg.new_Pvariable(
 | 
			
		||||
                conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names
 | 
			
		||||
            )
 | 
			
		||||
            triggers.append(trigger)
 | 
			
		||||
            cg.add(var.register_user_service(trigger))
 | 
			
		||||
            await automation.build_automation(trigger, func_args, conf)
 | 
			
		||||
        # Register all services at once - single allocation, no reallocations
 | 
			
		||||
        cg.add(var.initialize_user_services(triggers))
 | 
			
		||||
 | 
			
		||||
    if CONF_ON_CLIENT_CONNECTED in config:
 | 
			
		||||
        cg.add_define("USE_API_CLIENT_CONNECTED_TRIGGER")
 | 
			
		||||
 
 | 
			
		||||
@@ -425,7 +425,7 @@ message ListEntitiesFanResponse {
 | 
			
		||||
  bool disabled_by_default = 9;
 | 
			
		||||
  string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"];
 | 
			
		||||
  EntityCategory entity_category = 11;
 | 
			
		||||
  repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "std::vector<const char *>"];
 | 
			
		||||
  repeated string supported_preset_modes = 12 [(container_pointer) = "std::set"];
 | 
			
		||||
  uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"];
 | 
			
		||||
}
 | 
			
		||||
// Deprecated in API version 1.6 - only used in deprecated fields
 | 
			
		||||
@@ -506,7 +506,7 @@ message ListEntitiesLightResponse {
 | 
			
		||||
  string name = 3;
 | 
			
		||||
  reserved 4; // Deprecated: was string unique_id
 | 
			
		||||
 | 
			
		||||
  repeated ColorMode supported_color_modes = 12 [(container_pointer_no_template) = "light::ColorModeMask"];
 | 
			
		||||
  repeated ColorMode supported_color_modes = 12 [(container_pointer) = "std::set<light::ColorMode>"];
 | 
			
		||||
  // next four supports_* are for legacy clients, newer clients should use color modes
 | 
			
		||||
  // Deprecated in API version 1.6
 | 
			
		||||
  bool legacy_supports_brightness = 5 [deprecated=true];
 | 
			
		||||
@@ -989,7 +989,7 @@ message ListEntitiesClimateResponse {
 | 
			
		||||
 | 
			
		||||
  bool supports_current_temperature = 5; // Deprecated: use feature_flags
 | 
			
		||||
  bool supports_two_point_target_temperature = 6; // Deprecated: use feature_flags
 | 
			
		||||
  repeated ClimateMode supported_modes = 7 [(container_pointer_no_template) = "climate::ClimateModeMask"];
 | 
			
		||||
  repeated ClimateMode supported_modes = 7 [(container_pointer) = "std::set<climate::ClimateMode>"];
 | 
			
		||||
  float visual_min_temperature = 8;
 | 
			
		||||
  float visual_max_temperature = 9;
 | 
			
		||||
  float visual_target_temperature_step = 10;
 | 
			
		||||
@@ -998,11 +998,11 @@ message ListEntitiesClimateResponse {
 | 
			
		||||
  // Deprecated in API version 1.5
 | 
			
		||||
  bool legacy_supports_away = 11 [deprecated=true];
 | 
			
		||||
  bool supports_action = 12; // Deprecated: use feature_flags
 | 
			
		||||
  repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"];
 | 
			
		||||
  repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"];
 | 
			
		||||
  repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::vector"];
 | 
			
		||||
  repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"];
 | 
			
		||||
  repeated string supported_custom_presets = 17 [(container_pointer) = "std::vector"];
 | 
			
		||||
  repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer) = "std::set<climate::ClimateFanMode>"];
 | 
			
		||||
  repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer) = "std::set<climate::ClimateSwingMode>"];
 | 
			
		||||
  repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::set"];
 | 
			
		||||
  repeated ClimatePreset supported_presets = 16 [(container_pointer) = "std::set<climate::ClimatePreset>"];
 | 
			
		||||
  repeated string supported_custom_presets = 17 [(container_pointer) = "std::set"];
 | 
			
		||||
  bool disabled_by_default = 18;
 | 
			
		||||
  string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
 | 
			
		||||
  EntityCategory entity_category = 20;
 | 
			
		||||
@@ -1143,7 +1143,7 @@ message ListEntitiesSelectResponse {
 | 
			
		||||
  reserved 4; // Deprecated: was string unique_id
 | 
			
		||||
 | 
			
		||||
  string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
 | 
			
		||||
  repeated string options = 6 [(container_pointer_no_template) = "FixedVector<const char *>"];
 | 
			
		||||
  repeated string options = 6 [(container_pointer) = "std::vector"];
 | 
			
		||||
  bool disabled_by_default = 7;
 | 
			
		||||
  EntityCategory entity_category = 8;
 | 
			
		||||
  uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
 | 
			
		||||
 
 | 
			
		||||
@@ -410,8 +410,8 @@ uint16_t APIConnection::try_send_fan_state(EntityBase *entity, APIConnection *co
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.supports_direction())
 | 
			
		||||
    msg.direction = static_cast<enums::FanDirection>(fan->direction);
 | 
			
		||||
  if (traits.supports_preset_modes() && fan->has_preset_mode())
 | 
			
		||||
    msg.set_preset_mode(StringRef(fan->get_preset_mode()));
 | 
			
		||||
  if (traits.supports_preset_modes())
 | 
			
		||||
    msg.set_preset_mode(StringRef(fan->preset_mode));
 | 
			
		||||
  return fill_and_encode_entity_state(fan, msg, FanStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 | 
			
		||||
}
 | 
			
		||||
uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *conn, uint32_t remaining_size,
 | 
			
		||||
@@ -423,7 +423,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con
 | 
			
		||||
  msg.supports_speed = traits.supports_speed();
 | 
			
		||||
  msg.supports_direction = traits.supports_direction();
 | 
			
		||||
  msg.supported_speed_count = traits.supported_speed_count();
 | 
			
		||||
  msg.supported_preset_modes = &traits.supported_preset_modes();
 | 
			
		||||
  msg.supported_preset_modes = &traits.supported_preset_modes_for_api_();
 | 
			
		||||
  return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single);
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::fan_command(const FanCommandRequest &msg) {
 | 
			
		||||
@@ -453,6 +453,7 @@ uint16_t APIConnection::try_send_light_state(EntityBase *entity, APIConnection *
 | 
			
		||||
                                             bool is_single) {
 | 
			
		||||
  auto *light = static_cast<light::LightState *>(entity);
 | 
			
		||||
  LightStateResponse resp;
 | 
			
		||||
  auto traits = light->get_traits();
 | 
			
		||||
  auto values = light->remote_values;
 | 
			
		||||
  auto color_mode = values.get_color_mode();
 | 
			
		||||
  resp.state = values.is_on();
 | 
			
		||||
@@ -476,8 +477,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
 | 
			
		||||
  auto *light = static_cast<light::LightState *>(entity);
 | 
			
		||||
  ListEntitiesLightResponse msg;
 | 
			
		||||
  auto traits = light->get_traits();
 | 
			
		||||
  // Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values
 | 
			
		||||
  msg.supported_color_modes = &traits.get_supported_color_modes();
 | 
			
		||||
  msg.supported_color_modes = &traits.get_supported_color_modes_for_api_();
 | 
			
		||||
  if (traits.supports_color_capability(light::ColorCapability::COLOR_TEMPERATURE) ||
 | 
			
		||||
      traits.supports_color_capability(light::ColorCapability::COLD_WARM_WHITE)) {
 | 
			
		||||
    msg.min_mireds = traits.get_min_mireds();
 | 
			
		||||
@@ -486,7 +486,7 @@ uint16_t APIConnection::try_send_light_info(EntityBase *entity, APIConnection *c
 | 
			
		||||
  if (light->supports_effects()) {
 | 
			
		||||
    msg.effects.emplace_back("None");
 | 
			
		||||
    for (auto *effect : light->get_effects()) {
 | 
			
		||||
      msg.effects.emplace_back(effect->get_name());
 | 
			
		||||
      msg.effects.push_back(effect->get_name());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return fill_and_encode_entity_info(light, msg, ListEntitiesLightResponse::MESSAGE_TYPE, conn, remaining_size,
 | 
			
		||||
@@ -669,18 +669,18 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
 | 
			
		||||
  msg.supports_action = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
 | 
			
		||||
  // Current feature flags and other supported parameters
 | 
			
		||||
  msg.feature_flags = traits.get_feature_flags();
 | 
			
		||||
  msg.supported_modes = &traits.get_supported_modes();
 | 
			
		||||
  msg.supported_modes = &traits.get_supported_modes_for_api_();
 | 
			
		||||
  msg.visual_min_temperature = traits.get_visual_min_temperature();
 | 
			
		||||
  msg.visual_max_temperature = traits.get_visual_max_temperature();
 | 
			
		||||
  msg.visual_target_temperature_step = traits.get_visual_target_temperature_step();
 | 
			
		||||
  msg.visual_current_temperature_step = traits.get_visual_current_temperature_step();
 | 
			
		||||
  msg.visual_min_humidity = traits.get_visual_min_humidity();
 | 
			
		||||
  msg.visual_max_humidity = traits.get_visual_max_humidity();
 | 
			
		||||
  msg.supported_fan_modes = &traits.get_supported_fan_modes();
 | 
			
		||||
  msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes();
 | 
			
		||||
  msg.supported_presets = &traits.get_supported_presets();
 | 
			
		||||
  msg.supported_custom_presets = &traits.get_supported_custom_presets();
 | 
			
		||||
  msg.supported_swing_modes = &traits.get_supported_swing_modes();
 | 
			
		||||
  msg.supported_fan_modes = &traits.get_supported_fan_modes_for_api_();
 | 
			
		||||
  msg.supported_custom_fan_modes = &traits.get_supported_custom_fan_modes_for_api_();
 | 
			
		||||
  msg.supported_presets = &traits.get_supported_presets_for_api_();
 | 
			
		||||
  msg.supported_custom_presets = &traits.get_supported_custom_presets_for_api_();
 | 
			
		||||
  msg.supported_swing_modes = &traits.get_supported_swing_modes_for_api_();
 | 
			
		||||
  return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size,
 | 
			
		||||
                                     is_single);
 | 
			
		||||
}
 | 
			
		||||
@@ -1082,8 +1082,13 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) {
 | 
			
		||||
    homeassistant::global_homeassistant_time->set_epoch_time(value.epoch_seconds);
 | 
			
		||||
#ifdef USE_TIME_TIMEZONE
 | 
			
		||||
    if (value.timezone_len > 0) {
 | 
			
		||||
      homeassistant::global_homeassistant_time->set_timezone(reinterpret_cast<const char *>(value.timezone),
 | 
			
		||||
                                                             value.timezone_len);
 | 
			
		||||
      const std::string ¤t_tz = homeassistant::global_homeassistant_time->get_timezone();
 | 
			
		||||
      // Compare without allocating a string
 | 
			
		||||
      if (current_tz.length() != value.timezone_len ||
 | 
			
		||||
          memcmp(current_tz.c_str(), value.timezone, value.timezone_len) != 0) {
 | 
			
		||||
        homeassistant::global_homeassistant_time->set_timezone(
 | 
			
		||||
            std::string(reinterpret_cast<const char *>(value.timezone), value.timezone_len));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
@@ -1572,13 +1577,7 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption
 | 
			
		||||
  resp.success = false;
 | 
			
		||||
 | 
			
		||||
  psk_t psk{};
 | 
			
		||||
  if (msg.key.empty()) {
 | 
			
		||||
    if (this->parent_->clear_noise_psk(true)) {
 | 
			
		||||
      resp.success = true;
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "Failed to clear encryption key");
 | 
			
		||||
    }
 | 
			
		||||
  } else if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) {
 | 
			
		||||
  if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) {
 | 
			
		||||
    ESP_LOGW(TAG, "Invalid encryption key length");
 | 
			
		||||
  } else if (!this->parent_->save_noise_psk(psk, true)) {
 | 
			
		||||
    ESP_LOGW(TAG, "Failed to save encryption key");
 | 
			
		||||
 
 | 
			
		||||
@@ -434,7 +434,8 @@ APIError APINoiseFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer, st
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint8_t *buffer_data = buffer.get_buffer()->data();
 | 
			
		||||
  std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
 | 
			
		||||
  uint8_t *buffer_data = raw_buffer->data();  // Cache buffer pointer
 | 
			
		||||
 | 
			
		||||
  this->reusable_iovs_.clear();
 | 
			
		||||
  this->reusable_iovs_.reserve(packets.size());
 | 
			
		||||
 
 | 
			
		||||
@@ -230,7 +230,8 @@ APIError APIPlaintextFrameHelper::write_protobuf_packets(ProtoWriteBuffer buffer
 | 
			
		||||
    return APIError::OK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint8_t *buffer_data = buffer.get_buffer()->data();
 | 
			
		||||
  std::vector<uint8_t> *raw_buffer = buffer.get_buffer();
 | 
			
		||||
  uint8_t *buffer_data = raw_buffer->data();  // Cache buffer pointer
 | 
			
		||||
 | 
			
		||||
  this->reusable_iovs_.clear();
 | 
			
		||||
  this->reusable_iovs_.reserve(packets.size());
 | 
			
		||||
 
 | 
			
		||||
@@ -70,14 +70,4 @@ extend google.protobuf.FieldOptions {
 | 
			
		||||
    // init(size) before adding elements. This eliminates std::vector template overhead
 | 
			
		||||
    // and is ideal when the exact size is known before populating the array.
 | 
			
		||||
    optional bool fixed_vector = 50013 [default=false];
 | 
			
		||||
 | 
			
		||||
    // container_pointer_no_template: Use a non-template container type for repeated fields
 | 
			
		||||
    // Similar to container_pointer, but for containers that don't take template parameters.
 | 
			
		||||
    // The container type is used as-is without appending element type.
 | 
			
		||||
    // The container must have:
 | 
			
		||||
    // - begin() and end() methods returning iterators
 | 
			
		||||
    // - empty() method
 | 
			
		||||
    // Example: [(container_pointer_no_template) = "light::ColorModeMask"]
 | 
			
		||||
    //   generates: const light::ColorModeMask *supported_color_modes{};
 | 
			
		||||
    optional string container_pointer_no_template = 50014;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -355,8 +355,8 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  buffer.encode_string(10, this->icon_ref_);
 | 
			
		||||
#endif
 | 
			
		||||
  buffer.encode_uint32(11, static_cast<uint32_t>(this->entity_category));
 | 
			
		||||
  for (const char *it : *this->supported_preset_modes) {
 | 
			
		||||
    buffer.encode_string(12, it, strlen(it), true);
 | 
			
		||||
  for (const auto &it : *this->supported_preset_modes) {
 | 
			
		||||
    buffer.encode_string(12, it, true);
 | 
			
		||||
  }
 | 
			
		||||
#ifdef USE_DEVICES
 | 
			
		||||
  buffer.encode_uint32(13, this->device_id);
 | 
			
		||||
@@ -376,8 +376,8 @@ void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const {
 | 
			
		||||
#endif
 | 
			
		||||
  size.add_uint32(1, static_cast<uint32_t>(this->entity_category));
 | 
			
		||||
  if (!this->supported_preset_modes->empty()) {
 | 
			
		||||
    for (const char *it : *this->supported_preset_modes) {
 | 
			
		||||
      size.add_length_force(1, strlen(it));
 | 
			
		||||
    for (const auto &it : *this->supported_preset_modes) {
 | 
			
		||||
      size.add_length_force(1, it.size());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
#ifdef USE_DEVICES
 | 
			
		||||
@@ -1475,8 +1475,8 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
#ifdef USE_ENTITY_ICON
 | 
			
		||||
  buffer.encode_string(5, this->icon_ref_);
 | 
			
		||||
#endif
 | 
			
		||||
  for (const char *it : *this->options) {
 | 
			
		||||
    buffer.encode_string(6, it, strlen(it), true);
 | 
			
		||||
  for (const auto &it : *this->options) {
 | 
			
		||||
    buffer.encode_string(6, it, true);
 | 
			
		||||
  }
 | 
			
		||||
  buffer.encode_bool(7, this->disabled_by_default);
 | 
			
		||||
  buffer.encode_uint32(8, static_cast<uint32_t>(this->entity_category));
 | 
			
		||||
@@ -1492,8 +1492,8 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const {
 | 
			
		||||
  size.add_length(1, this->icon_ref_.size());
 | 
			
		||||
#endif
 | 
			
		||||
  if (!this->options->empty()) {
 | 
			
		||||
    for (const char *it : *this->options) {
 | 
			
		||||
      size.add_length_force(1, strlen(it));
 | 
			
		||||
    for (const auto &it : *this->options) {
 | 
			
		||||
      size.add_length_force(1, it.size());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  size.add_bool(1, this->disabled_by_default);
 | 
			
		||||
 
 | 
			
		||||
@@ -725,7 +725,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage {
 | 
			
		||||
  bool supports_speed{false};
 | 
			
		||||
  bool supports_direction{false};
 | 
			
		||||
  int32_t supported_speed_count{0};
 | 
			
		||||
  const std::vector<const char *> *supported_preset_modes{};
 | 
			
		||||
  const std::set<std::string> *supported_preset_modes{};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
  void calculate_size(ProtoSize &size) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
@@ -790,7 +790,7 @@ class ListEntitiesLightResponse final : public InfoResponseProtoMessage {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "list_entities_light_response"; }
 | 
			
		||||
#endif
 | 
			
		||||
  const light::ColorModeMask *supported_color_modes{};
 | 
			
		||||
  const std::set<light::ColorMode> *supported_color_modes{};
 | 
			
		||||
  float min_mireds{0.0f};
 | 
			
		||||
  float max_mireds{0.0f};
 | 
			
		||||
  std::vector<std::string> effects{};
 | 
			
		||||
@@ -1377,16 +1377,16 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage {
 | 
			
		||||
#endif
 | 
			
		||||
  bool supports_current_temperature{false};
 | 
			
		||||
  bool supports_two_point_target_temperature{false};
 | 
			
		||||
  const climate::ClimateModeMask *supported_modes{};
 | 
			
		||||
  const std::set<climate::ClimateMode> *supported_modes{};
 | 
			
		||||
  float visual_min_temperature{0.0f};
 | 
			
		||||
  float visual_max_temperature{0.0f};
 | 
			
		||||
  float visual_target_temperature_step{0.0f};
 | 
			
		||||
  bool supports_action{false};
 | 
			
		||||
  const climate::ClimateFanModeMask *supported_fan_modes{};
 | 
			
		||||
  const climate::ClimateSwingModeMask *supported_swing_modes{};
 | 
			
		||||
  const std::vector<std::string> *supported_custom_fan_modes{};
 | 
			
		||||
  const climate::ClimatePresetMask *supported_presets{};
 | 
			
		||||
  const std::vector<std::string> *supported_custom_presets{};
 | 
			
		||||
  const std::set<climate::ClimateFanMode> *supported_fan_modes{};
 | 
			
		||||
  const std::set<climate::ClimateSwingMode> *supported_swing_modes{};
 | 
			
		||||
  const std::set<std::string> *supported_custom_fan_modes{};
 | 
			
		||||
  const std::set<climate::ClimatePreset> *supported_presets{};
 | 
			
		||||
  const std::set<std::string> *supported_custom_presets{};
 | 
			
		||||
  float visual_current_temperature_step{0.0f};
 | 
			
		||||
  bool supports_current_humidity{false};
 | 
			
		||||
  bool supports_target_humidity{false};
 | 
			
		||||
@@ -1534,7 +1534,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "list_entities_select_response"; }
 | 
			
		||||
#endif
 | 
			
		||||
  const FixedVector<const char *> *options{};
 | 
			
		||||
  const std::vector<std::string> *options{};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
  void calculate_size(ProtoSize &size) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
 
 | 
			
		||||
@@ -88,12 +88,6 @@ static void dump_field(std::string &out, const char *field_name, StringRef value
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) {
 | 
			
		||||
  append_field_prefix(out, field_name, indent);
 | 
			
		||||
  out.append("'").append(value).append("'");
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
template<typename T> static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) {
 | 
			
		||||
  append_field_prefix(out, field_name, indent);
 | 
			
		||||
  out.append(proto_enum_to_string<T>(value));
 | 
			
		||||
 
 | 
			
		||||
@@ -468,31 +468,6 @@ uint16_t APIServer::get_port() const { return this->port_; }
 | 
			
		||||
void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeout_ = reboot_timeout; }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
bool APIServer::update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg,
 | 
			
		||||
                                  const LogString *fail_log_msg, const psk_t &active_psk, bool make_active) {
 | 
			
		||||
  if (!this->noise_pref_.save(&new_psk)) {
 | 
			
		||||
    ESP_LOGW(TAG, "%s", LOG_STR_ARG(fail_log_msg));
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  // ensure it's written immediately
 | 
			
		||||
  if (!global_preferences->sync()) {
 | 
			
		||||
    ESP_LOGW(TAG, "Failed to sync preferences");
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGD(TAG, "%s", LOG_STR_ARG(save_log_msg));
 | 
			
		||||
  if (make_active) {
 | 
			
		||||
    this->set_timeout(100, [this, active_psk]() {
 | 
			
		||||
      ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
 | 
			
		||||
      this->set_noise_psk(active_psk);
 | 
			
		||||
      for (auto &c : this->clients_) {
 | 
			
		||||
        DisconnectRequest req;
 | 
			
		||||
        c->send_message(req, DisconnectRequest::MESSAGE_TYPE);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
 | 
			
		||||
#ifdef USE_API_NOISE_PSK_FROM_YAML
 | 
			
		||||
  // When PSK is set from YAML, this function should never be called
 | 
			
		||||
@@ -507,21 +482,27 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  SavedNoisePsk new_saved_psk{psk};
 | 
			
		||||
  return this->update_noise_psk_(new_saved_psk, LOG_STR("Noise PSK saved"), LOG_STR("Failed to save Noise PSK"), psk,
 | 
			
		||||
                                 make_active);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
bool APIServer::clear_noise_psk(bool make_active) {
 | 
			
		||||
#ifdef USE_API_NOISE_PSK_FROM_YAML
 | 
			
		||||
  // When PSK is set from YAML, this function should never be called
 | 
			
		||||
  // but if it is, reject the change
 | 
			
		||||
  ESP_LOGW(TAG, "Key set in YAML");
 | 
			
		||||
  return false;
 | 
			
		||||
#else
 | 
			
		||||
  SavedNoisePsk empty_psk{};
 | 
			
		||||
  psk_t empty{};
 | 
			
		||||
  return this->update_noise_psk_(empty_psk, LOG_STR("Noise PSK cleared"), LOG_STR("Failed to clear Noise PSK"), empty,
 | 
			
		||||
                                 make_active);
 | 
			
		||||
  if (!this->noise_pref_.save(&new_saved_psk)) {
 | 
			
		||||
    ESP_LOGW(TAG, "Failed to save Noise PSK");
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  // ensure it's written immediately
 | 
			
		||||
  if (!global_preferences->sync()) {
 | 
			
		||||
    ESP_LOGW(TAG, "Failed to sync preferences");
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGD(TAG, "Noise PSK saved");
 | 
			
		||||
  if (make_active) {
 | 
			
		||||
    this->set_timeout(100, [this, psk]() {
 | 
			
		||||
      ESP_LOGW(TAG, "Disconnecting all clients to reset PSK");
 | 
			
		||||
      this->set_noise_psk(psk);
 | 
			
		||||
      for (auto &c : this->clients_) {
 | 
			
		||||
        DisconnectRequest req;
 | 
			
		||||
        c->send_message(req, DisconnectRequest::MESSAGE_TYPE);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -53,7 +53,6 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  bool save_noise_psk(psk_t psk, bool make_active = true);
 | 
			
		||||
  bool clear_noise_psk(bool make_active = true);
 | 
			
		||||
  void set_noise_psk(psk_t psk) { noise_ctx_->set_psk(psk); }
 | 
			
		||||
  std::shared_ptr<APINoiseContext> get_noise_ctx() { return noise_ctx_; }
 | 
			
		||||
#endif  // USE_API_NOISE
 | 
			
		||||
@@ -125,14 +124,8 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
#ifdef USE_API_SERVICES
 | 
			
		||||
  void initialize_user_services(std::initializer_list<UserServiceDescriptor *> services) {
 | 
			
		||||
    this->user_services_.assign(services);
 | 
			
		||||
  }
 | 
			
		||||
#ifdef USE_API_CUSTOM_SERVICES
 | 
			
		||||
  // Only compile push_back method when custom_services: true (external components)
 | 
			
		||||
  void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
 | 
			
		||||
#endif
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_HOMEASSISTANT_TIME
 | 
			
		||||
  void request_time();
 | 
			
		||||
#endif
 | 
			
		||||
@@ -181,10 +174,6 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void schedule_reboot_timeout_();
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
  bool update_noise_psk_(const SavedNoisePsk &new_psk, const LogString *save_log_msg, const LogString *fail_log_msg,
 | 
			
		||||
                         const psk_t &active_psk, bool make_active);
 | 
			
		||||
#endif  // USE_API_NOISE
 | 
			
		||||
  // Pointers and pointer-like types first (4 bytes each)
 | 
			
		||||
  std::unique_ptr<socket::Socket> socket_ = nullptr;
 | 
			
		||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
 | 
			
		||||
 
 | 
			
		||||
@@ -53,14 +53,8 @@ class CustomAPIDevice {
 | 
			
		||||
  template<typename T, typename... Ts>
 | 
			
		||||
  void register_service(void (T::*callback)(Ts...), const std::string &name,
 | 
			
		||||
                        const std::array<std::string, sizeof...(Ts)> &arg_names) {
 | 
			
		||||
#ifdef USE_API_CUSTOM_SERVICES
 | 
			
		||||
    auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback);  // NOLINT
 | 
			
		||||
    global_api_server->register_user_service(service);
 | 
			
		||||
#else
 | 
			
		||||
    static_assert(
 | 
			
		||||
        sizeof(T) == 0,
 | 
			
		||||
        "register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
#else
 | 
			
		||||
  template<typename T, typename... Ts>
 | 
			
		||||
@@ -92,14 +86,8 @@ class CustomAPIDevice {
 | 
			
		||||
   */
 | 
			
		||||
#ifdef USE_API_SERVICES
 | 
			
		||||
  template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
 | 
			
		||||
#ifdef USE_API_CUSTOM_SERVICES
 | 
			
		||||
    auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback);  // NOLINT
 | 
			
		||||
    global_api_server->register_user_service(service);
 | 
			
		||||
#else
 | 
			
		||||
    static_assert(
 | 
			
		||||
        sizeof(T) == 0,
 | 
			
		||||
        "register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
#else
 | 
			
		||||
  template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,58 +11,23 @@ template<> int32_t get_execute_arg_value<int32_t>(const ExecuteServiceArgument &
 | 
			
		||||
}
 | 
			
		||||
template<> float get_execute_arg_value<float>(const ExecuteServiceArgument &arg) { return arg.float_; }
 | 
			
		||||
template<> std::string get_execute_arg_value<std::string>(const ExecuteServiceArgument &arg) { return arg.string_; }
 | 
			
		||||
 | 
			
		||||
// Legacy std::vector versions for external components using custom_api_device.h - optimized with reserve
 | 
			
		||||
template<> std::vector<bool> get_execute_arg_value<std::vector<bool>>(const ExecuteServiceArgument &arg) {
 | 
			
		||||
  std::vector<bool> result;
 | 
			
		||||
  result.reserve(arg.bool_array.size());
 | 
			
		||||
  result.insert(result.end(), arg.bool_array.begin(), arg.bool_array.end());
 | 
			
		||||
  return result;
 | 
			
		||||
  return std::vector<bool>(arg.bool_array.begin(), arg.bool_array.end());
 | 
			
		||||
}
 | 
			
		||||
template<> std::vector<int32_t> get_execute_arg_value<std::vector<int32_t>>(const ExecuteServiceArgument &arg) {
 | 
			
		||||
  std::vector<int32_t> result;
 | 
			
		||||
  result.reserve(arg.int_array.size());
 | 
			
		||||
  result.insert(result.end(), arg.int_array.begin(), arg.int_array.end());
 | 
			
		||||
  return result;
 | 
			
		||||
  return std::vector<int32_t>(arg.int_array.begin(), arg.int_array.end());
 | 
			
		||||
}
 | 
			
		||||
template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) {
 | 
			
		||||
  std::vector<float> result;
 | 
			
		||||
  result.reserve(arg.float_array.size());
 | 
			
		||||
  result.insert(result.end(), arg.float_array.begin(), arg.float_array.end());
 | 
			
		||||
  return result;
 | 
			
		||||
  return std::vector<float>(arg.float_array.begin(), arg.float_array.end());
 | 
			
		||||
}
 | 
			
		||||
template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) {
 | 
			
		||||
  std::vector<std::string> result;
 | 
			
		||||
  result.reserve(arg.string_array.size());
 | 
			
		||||
  result.insert(result.end(), arg.string_array.begin(), arg.string_array.end());
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// New FixedVector const reference versions for YAML-generated services - zero-copy
 | 
			
		||||
template<>
 | 
			
		||||
const FixedVector<bool> &get_execute_arg_value<const FixedVector<bool> &>(const ExecuteServiceArgument &arg) {
 | 
			
		||||
  return arg.bool_array;
 | 
			
		||||
}
 | 
			
		||||
template<>
 | 
			
		||||
const FixedVector<int32_t> &get_execute_arg_value<const FixedVector<int32_t> &>(const ExecuteServiceArgument &arg) {
 | 
			
		||||
  return arg.int_array;
 | 
			
		||||
}
 | 
			
		||||
template<>
 | 
			
		||||
const FixedVector<float> &get_execute_arg_value<const FixedVector<float> &>(const ExecuteServiceArgument &arg) {
 | 
			
		||||
  return arg.float_array;
 | 
			
		||||
}
 | 
			
		||||
template<>
 | 
			
		||||
const FixedVector<std::string> &get_execute_arg_value<const FixedVector<std::string> &>(
 | 
			
		||||
    const ExecuteServiceArgument &arg) {
 | 
			
		||||
  return arg.string_array;
 | 
			
		||||
  return std::vector<std::string>(arg.string_array.begin(), arg.string_array.end());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
template<> enums::ServiceArgType to_service_arg_type<bool>() { return enums::SERVICE_ARG_TYPE_BOOL; }
 | 
			
		||||
template<> enums::ServiceArgType to_service_arg_type<int32_t>() { return enums::SERVICE_ARG_TYPE_INT; }
 | 
			
		||||
template<> enums::ServiceArgType to_service_arg_type<float>() { return enums::SERVICE_ARG_TYPE_FLOAT; }
 | 
			
		||||
template<> enums::ServiceArgType to_service_arg_type<std::string>() { return enums::SERVICE_ARG_TYPE_STRING; }
 | 
			
		||||
 | 
			
		||||
// Legacy std::vector versions for external components using custom_api_device.h
 | 
			
		||||
template<> enums::ServiceArgType to_service_arg_type<std::vector<bool>>() { return enums::SERVICE_ARG_TYPE_BOOL_ARRAY; }
 | 
			
		||||
template<> enums::ServiceArgType to_service_arg_type<std::vector<int32_t>>() {
 | 
			
		||||
  return enums::SERVICE_ARG_TYPE_INT_ARRAY;
 | 
			
		||||
@@ -74,18 +39,4 @@ template<> enums::ServiceArgType to_service_arg_type<std::vector<std::string>>()
 | 
			
		||||
  return enums::SERVICE_ARG_TYPE_STRING_ARRAY;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// New FixedVector const reference versions for YAML-generated services
 | 
			
		||||
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<bool> &>() {
 | 
			
		||||
  return enums::SERVICE_ARG_TYPE_BOOL_ARRAY;
 | 
			
		||||
}
 | 
			
		||||
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<int32_t> &>() {
 | 
			
		||||
  return enums::SERVICE_ARG_TYPE_INT_ARRAY;
 | 
			
		||||
}
 | 
			
		||||
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<float> &>() {
 | 
			
		||||
  return enums::SERVICE_ARG_TYPE_FLOAT_ARRAY;
 | 
			
		||||
}
 | 
			
		||||
template<> enums::ServiceArgType to_service_arg_type<const FixedVector<std::string> &>() {
 | 
			
		||||
  return enums::SERVICE_ARG_TYPE_STRING_ARRAY;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::api
 | 
			
		||||
 
 | 
			
		||||
@@ -6,9 +6,6 @@ namespace bang_bang {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "bang_bang.climate";
 | 
			
		||||
 | 
			
		||||
BangBangClimate::BangBangClimate()
 | 
			
		||||
    : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {}
 | 
			
		||||
 | 
			
		||||
void BangBangClimate::setup() {
 | 
			
		||||
  this->sensor_->add_on_state_callback([this](float state) {
 | 
			
		||||
    this->current_temperature = state;
 | 
			
		||||
@@ -34,63 +31,53 @@ void BangBangClimate::setup() {
 | 
			
		||||
    restore->to_call(this).perform();
 | 
			
		||||
  } else {
 | 
			
		||||
    // restore from defaults, change_away handles those for us
 | 
			
		||||
    if (this->supports_cool_ && this->supports_heat_) {
 | 
			
		||||
    if (supports_cool_ && supports_heat_) {
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
    } else if (this->supports_cool_) {
 | 
			
		||||
    } else if (supports_cool_) {
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_COOL;
 | 
			
		||||
    } else if (this->supports_heat_) {
 | 
			
		||||
    } else if (supports_heat_) {
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_HEAT;
 | 
			
		||||
    }
 | 
			
		||||
    this->change_away_(false);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BangBangClimate::control(const climate::ClimateCall &call) {
 | 
			
		||||
  if (call.get_mode().has_value()) {
 | 
			
		||||
  if (call.get_mode().has_value())
 | 
			
		||||
    this->mode = *call.get_mode();
 | 
			
		||||
  }
 | 
			
		||||
  if (call.get_target_temperature_low().has_value()) {
 | 
			
		||||
  if (call.get_target_temperature_low().has_value())
 | 
			
		||||
    this->target_temperature_low = *call.get_target_temperature_low();
 | 
			
		||||
  }
 | 
			
		||||
  if (call.get_target_temperature_high().has_value()) {
 | 
			
		||||
  if (call.get_target_temperature_high().has_value())
 | 
			
		||||
    this->target_temperature_high = *call.get_target_temperature_high();
 | 
			
		||||
  }
 | 
			
		||||
  if (call.get_preset().has_value()) {
 | 
			
		||||
  if (call.get_preset().has_value())
 | 
			
		||||
    this->change_away_(*call.get_preset() == climate::CLIMATE_PRESET_AWAY);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->compute_state_();
 | 
			
		||||
  this->publish_state();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
climate::ClimateTraits BangBangClimate::traits() {
 | 
			
		||||
  auto traits = climate::ClimateTraits();
 | 
			
		||||
  traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE |
 | 
			
		||||
                           climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION);
 | 
			
		||||
  if (this->humidity_sensor_ != nullptr) {
 | 
			
		||||
    traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
 | 
			
		||||
  }
 | 
			
		||||
  traits.set_supports_current_temperature(true);
 | 
			
		||||
  if (this->humidity_sensor_ != nullptr)
 | 
			
		||||
    traits.set_supports_current_humidity(true);
 | 
			
		||||
  traits.set_supported_modes({
 | 
			
		||||
      climate::CLIMATE_MODE_OFF,
 | 
			
		||||
  });
 | 
			
		||||
  if (this->supports_cool_) {
 | 
			
		||||
  if (supports_cool_)
 | 
			
		||||
    traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->supports_heat_) {
 | 
			
		||||
  if (supports_heat_)
 | 
			
		||||
    traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->supports_cool_ && this->supports_heat_) {
 | 
			
		||||
  if (supports_cool_ && supports_heat_)
 | 
			
		||||
    traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->supports_away_) {
 | 
			
		||||
  traits.set_supports_two_point_target_temperature(true);
 | 
			
		||||
  if (supports_away_) {
 | 
			
		||||
    traits.set_supported_presets({
 | 
			
		||||
        climate::CLIMATE_PRESET_HOME,
 | 
			
		||||
        climate::CLIMATE_PRESET_AWAY,
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
  traits.set_supports_action(true);
 | 
			
		||||
  return traits;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BangBangClimate::compute_state_() {
 | 
			
		||||
  if (this->mode == climate::CLIMATE_MODE_OFF) {
 | 
			
		||||
    this->switch_to_action_(climate::CLIMATE_ACTION_OFF);
 | 
			
		||||
@@ -135,7 +122,6 @@ void BangBangClimate::compute_state_() {
 | 
			
		||||
 | 
			
		||||
  this->switch_to_action_(target_action);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
 | 
			
		||||
  if (action == this->action) {
 | 
			
		||||
    // already in target mode
 | 
			
		||||
@@ -180,7 +166,6 @@ void BangBangClimate::switch_to_action_(climate::ClimateAction action) {
 | 
			
		||||
  this->prev_trigger_ = trig;
 | 
			
		||||
  this->publish_state();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BangBangClimate::change_away_(bool away) {
 | 
			
		||||
  if (!away) {
 | 
			
		||||
    this->target_temperature_low = this->normal_config_.default_temperature_low;
 | 
			
		||||
@@ -191,26 +176,22 @@ void BangBangClimate::change_away_(bool away) {
 | 
			
		||||
  }
 | 
			
		||||
  this->preset = away ? climate::CLIMATE_PRESET_AWAY : climate::CLIMATE_PRESET_HOME;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BangBangClimate::set_normal_config(const BangBangClimateTargetTempConfig &normal_config) {
 | 
			
		||||
  this->normal_config_ = normal_config;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BangBangClimate::set_away_config(const BangBangClimateTargetTempConfig &away_config) {
 | 
			
		||||
  this->supports_away_ = true;
 | 
			
		||||
  this->away_config_ = away_config;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
BangBangClimate::BangBangClimate()
 | 
			
		||||
    : idle_trigger_(new Trigger<>()), cool_trigger_(new Trigger<>()), heat_trigger_(new Trigger<>()) {}
 | 
			
		||||
void BangBangClimate::set_sensor(sensor::Sensor *sensor) { this->sensor_ = sensor; }
 | 
			
		||||
void BangBangClimate::set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
 | 
			
		||||
 | 
			
		||||
Trigger<> *BangBangClimate::get_idle_trigger() const { return this->idle_trigger_; }
 | 
			
		||||
Trigger<> *BangBangClimate::get_cool_trigger() const { return this->cool_trigger_; }
 | 
			
		||||
Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; }
 | 
			
		||||
 | 
			
		||||
void BangBangClimate::set_supports_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
 | 
			
		||||
Trigger<> *BangBangClimate::get_heat_trigger() const { return this->heat_trigger_; }
 | 
			
		||||
void BangBangClimate::set_supports_heat(bool supports_heat) { this->supports_heat_ = supports_heat; }
 | 
			
		||||
 | 
			
		||||
void BangBangClimate::dump_config() {
 | 
			
		||||
  LOG_CLIMATE("", "Bang Bang Climate", this);
 | 
			
		||||
  ESP_LOGCONFIG(TAG,
 | 
			
		||||
 
 | 
			
		||||
@@ -25,15 +25,14 @@ class BangBangClimate : public climate::Climate, public Component {
 | 
			
		||||
 | 
			
		||||
  void set_sensor(sensor::Sensor *sensor);
 | 
			
		||||
  void set_humidity_sensor(sensor::Sensor *humidity_sensor);
 | 
			
		||||
  Trigger<> *get_idle_trigger() const;
 | 
			
		||||
  Trigger<> *get_cool_trigger() const;
 | 
			
		||||
  void set_supports_cool(bool supports_cool);
 | 
			
		||||
  Trigger<> *get_heat_trigger() const;
 | 
			
		||||
  void set_supports_heat(bool supports_heat);
 | 
			
		||||
  void set_normal_config(const BangBangClimateTargetTempConfig &normal_config);
 | 
			
		||||
  void set_away_config(const BangBangClimateTargetTempConfig &away_config);
 | 
			
		||||
 | 
			
		||||
  Trigger<> *get_idle_trigger() const;
 | 
			
		||||
  Trigger<> *get_cool_trigger() const;
 | 
			
		||||
  Trigger<> *get_heat_trigger() const;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  /// Override control to change settings of the climate device.
 | 
			
		||||
  void control(const climate::ClimateCall &call) override;
 | 
			
		||||
@@ -57,10 +56,16 @@ class BangBangClimate : public climate::Climate, public Component {
 | 
			
		||||
   *
 | 
			
		||||
   * In idle mode, the controller is assumed to have both heating and cooling disabled.
 | 
			
		||||
   */
 | 
			
		||||
  Trigger<> *idle_trigger_{nullptr};
 | 
			
		||||
  Trigger<> *idle_trigger_;
 | 
			
		||||
  /** The trigger to call when the controller should switch to cooling mode.
 | 
			
		||||
   */
 | 
			
		||||
  Trigger<> *cool_trigger_{nullptr};
 | 
			
		||||
  Trigger<> *cool_trigger_;
 | 
			
		||||
  /** Whether the controller supports cooling.
 | 
			
		||||
   *
 | 
			
		||||
   * A false value for this attribute means that the controller has no cooling action
 | 
			
		||||
   * (for example a thermostat, where only heating and not-heating is possible).
 | 
			
		||||
   */
 | 
			
		||||
  bool supports_cool_{false};
 | 
			
		||||
  /** The trigger to call when the controller should switch to heating mode.
 | 
			
		||||
   *
 | 
			
		||||
   * A null value for this attribute means that the controller has no heating action
 | 
			
		||||
@@ -68,23 +73,15 @@ class BangBangClimate : public climate::Climate, public Component {
 | 
			
		||||
   * (blinds open) is possible.
 | 
			
		||||
   */
 | 
			
		||||
  Trigger<> *heat_trigger_{nullptr};
 | 
			
		||||
  bool supports_heat_{false};
 | 
			
		||||
  /** A reference to the trigger that was previously active.
 | 
			
		||||
   *
 | 
			
		||||
   * This is so that the previous trigger can be stopped before enabling a new one.
 | 
			
		||||
   */
 | 
			
		||||
  Trigger<> *prev_trigger_{nullptr};
 | 
			
		||||
 | 
			
		||||
  /** Whether the controller supports cooling/heating
 | 
			
		||||
   *
 | 
			
		||||
   * A false value for this attribute means that the controller has no respective action
 | 
			
		||||
   * (for example a thermostat, where only heating and not-heating is possible).
 | 
			
		||||
   */
 | 
			
		||||
  bool supports_cool_{false};
 | 
			
		||||
  bool supports_heat_{false};
 | 
			
		||||
 | 
			
		||||
  bool supports_away_{false};
 | 
			
		||||
 | 
			
		||||
  BangBangClimateTargetTempConfig normal_config_{};
 | 
			
		||||
  bool supports_away_{false};
 | 
			
		||||
  BangBangClimateTargetTempConfig away_config_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -99,8 +99,9 @@ enum BedjetCommand : uint8_t {
 | 
			
		||||
 | 
			
		||||
static const uint8_t BEDJET_FAN_SPEED_COUNT = 20;
 | 
			
		||||
 | 
			
		||||
static constexpr const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
 | 
			
		||||
static const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
 | 
			
		||||
static const std::string BEDJET_FAN_STEP_NAME_STRINGS[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
 | 
			
		||||
static const std::set<std::string> BEDJET_FAN_STEP_NAMES_SET BEDJET_FAN_STEP_NAMES_;
 | 
			
		||||
 | 
			
		||||
}  // namespace bedjet
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,8 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
 | 
			
		||||
 | 
			
		||||
  climate::ClimateTraits traits() override {
 | 
			
		||||
    auto traits = climate::ClimateTraits();
 | 
			
		||||
    traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION | climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
 | 
			
		||||
    traits.set_supports_action(true);
 | 
			
		||||
    traits.set_supports_current_temperature(true);
 | 
			
		||||
    traits.set_supported_modes({
 | 
			
		||||
        climate::CLIMATE_MODE_OFF,
 | 
			
		||||
        climate::CLIMATE_MODE_HEAT,
 | 
			
		||||
@@ -43,7 +44,7 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
 | 
			
		||||
    });
 | 
			
		||||
 | 
			
		||||
    // It would be better if we had a slider for the fan modes.
 | 
			
		||||
    traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES);
 | 
			
		||||
    traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES_SET);
 | 
			
		||||
    traits.set_supported_presets({
 | 
			
		||||
        // If we support NONE, then have to decide what happens if the user switches to it (turn off?)
 | 
			
		||||
        // climate::CLIMATE_PRESET_NONE,
 | 
			
		||||
 
 | 
			
		||||
@@ -155,7 +155,6 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon
 | 
			
		||||
InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
 | 
			
		||||
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component)
 | 
			
		||||
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
 | 
			
		||||
StatelessLambdaFilter = binary_sensor_ns.class_("StatelessLambdaFilter", Filter)
 | 
			
		||||
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
 | 
			
		||||
 | 
			
		||||
_LOGGER = getLogger(__name__)
 | 
			
		||||
@@ -265,31 +264,20 @@ async def delayed_off_filter_to_code(config, filter_id):
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
async def autorepeat_filter_to_code(config, filter_id):
 | 
			
		||||
    timings = []
 | 
			
		||||
    if len(config) > 0:
 | 
			
		||||
        timings = [
 | 
			
		||||
            cg.StructInitializer(
 | 
			
		||||
                cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"),
 | 
			
		||||
                ("delay", conf[CONF_DELAY]),
 | 
			
		||||
                ("time_off", conf[CONF_TIME_OFF]),
 | 
			
		||||
                ("time_on", conf[CONF_TIME_ON]),
 | 
			
		||||
            )
 | 
			
		||||
        timings.extend(
 | 
			
		||||
            (conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON])
 | 
			
		||||
            for conf in config
 | 
			
		||||
        ]
 | 
			
		||||
        )
 | 
			
		||||
    else:
 | 
			
		||||
        timings = [
 | 
			
		||||
            cg.StructInitializer(
 | 
			
		||||
                cg.MockObj("AutorepeatFilterTiming", "esphome::binary_sensor::"),
 | 
			
		||||
                ("delay", cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds),
 | 
			
		||||
                (
 | 
			
		||||
                    "time_off",
 | 
			
		||||
                    cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds,
 | 
			
		||||
                ),
 | 
			
		||||
                (
 | 
			
		||||
                    "time_on",
 | 
			
		||||
                    cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds,
 | 
			
		||||
                ),
 | 
			
		||||
        timings.append(
 | 
			
		||||
            (
 | 
			
		||||
                cv.time_period_str_unit(DEFAULT_DELAY).total_milliseconds,
 | 
			
		||||
                cv.time_period_str_unit(DEFAULT_TIME_OFF).total_milliseconds,
 | 
			
		||||
                cv.time_period_str_unit(DEFAULT_TIME_ON).total_milliseconds,
 | 
			
		||||
            )
 | 
			
		||||
        ]
 | 
			
		||||
        )
 | 
			
		||||
    var = cg.new_Pvariable(filter_id, timings)
 | 
			
		||||
    await cg.register_component(var, {})
 | 
			
		||||
    return var
 | 
			
		||||
@@ -300,7 +288,7 @@ async def lambda_filter_to_code(config, filter_id):
 | 
			
		||||
    lambda_ = await cg.process_lambda(
 | 
			
		||||
        config, [(bool, "x")], return_type=cg.optional.template(bool)
 | 
			
		||||
    )
 | 
			
		||||
    return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter)
 | 
			
		||||
    return cg.new_Pvariable(filter_id, lambda_)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_filter(
 | 
			
		||||
@@ -548,6 +536,11 @@ def binary_sensor_schema(
 | 
			
		||||
    return _BINARY_SENSOR_SCHEMA.extend(schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Remove before 2025.11.0
 | 
			
		||||
BINARY_SENSOR_SCHEMA = binary_sensor_schema()
 | 
			
		||||
BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def setup_binary_sensor_core_(var, config):
 | 
			
		||||
    await setup_entity(var, config, "binary_sensor")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,11 @@
 | 
			
		||||
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/components/binary_sensor/binary_sensor.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -92,8 +92,8 @@ class DoubleClickTrigger : public Trigger<> {
 | 
			
		||||
 | 
			
		||||
class MultiClickTrigger : public Trigger<>, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit MultiClickTrigger(BinarySensor *parent, std::initializer_list<MultiClickTriggerEvent> timing)
 | 
			
		||||
      : parent_(parent), timing_(timing) {}
 | 
			
		||||
  explicit MultiClickTrigger(BinarySensor *parent, std::vector<MultiClickTriggerEvent> timing)
 | 
			
		||||
      : parent_(parent), timing_(std::move(timing)) {}
 | 
			
		||||
 | 
			
		||||
  void setup() override {
 | 
			
		||||
    this->last_state_ = this->parent_->get_state_default(false);
 | 
			
		||||
@@ -115,7 +115,7 @@ class MultiClickTrigger : public Trigger<>, public Component {
 | 
			
		||||
  void trigger_();
 | 
			
		||||
 | 
			
		||||
  BinarySensor *parent_;
 | 
			
		||||
  FixedVector<MultiClickTriggerEvent> timing_;
 | 
			
		||||
  std::vector<MultiClickTriggerEvent> timing_;
 | 
			
		||||
  uint32_t invalid_cooldown_{1000};
 | 
			
		||||
  optional<size_t> at_index_{};
 | 
			
		||||
  bool last_state_{false};
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,7 @@ void BinarySensor::add_filter(Filter *filter) {
 | 
			
		||||
    last_filter->next_ = filter;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void BinarySensor::add_filters(std::initializer_list<Filter *> filters) {
 | 
			
		||||
void BinarySensor::add_filters(const std::vector<Filter *> &filters) {
 | 
			
		||||
  for (Filter *filter : filters) {
 | 
			
		||||
    this->add_filter(filter);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,7 @@
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/components/binary_sensor/filter.h"
 | 
			
		||||
 | 
			
		||||
#include <initializer_list>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
@@ -48,7 +48,7 @@ class BinarySensor : public StatefulEntityBase<bool>, public EntityBase_DeviceCl
 | 
			
		||||
  void publish_initial_state(bool new_state);
 | 
			
		||||
 | 
			
		||||
  void add_filter(Filter *filter);
 | 
			
		||||
  void add_filters(std::initializer_list<Filter *> filters);
 | 
			
		||||
  void add_filters(const std::vector<Filter *> &filters);
 | 
			
		||||
 | 
			
		||||
  // ========== INTERNAL METHODS ==========
 | 
			
		||||
  // (In most use cases you won't need these)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
#include "filter.h"
 | 
			
		||||
 | 
			
		||||
#include "binary_sensor.h"
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
@@ -67,7 +68,7 @@ float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARD
 | 
			
		||||
 | 
			
		||||
optional<bool> InvertFilter::new_value(bool value) { return !value; }
 | 
			
		||||
 | 
			
		||||
AutorepeatFilter::AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings) : timings_(timings) {}
 | 
			
		||||
AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {}
 | 
			
		||||
 | 
			
		||||
optional<bool> AutorepeatFilter::new_value(bool value) {
 | 
			
		||||
  if (value) {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,8 @@
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
namespace binary_sensor {
 | 
			
		||||
@@ -80,6 +82,11 @@ class InvertFilter : public Filter {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct AutorepeatFilterTiming {
 | 
			
		||||
  AutorepeatFilterTiming(uint32_t delay, uint32_t off, uint32_t on) {
 | 
			
		||||
    this->delay = delay;
 | 
			
		||||
    this->time_off = off;
 | 
			
		||||
    this->time_on = on;
 | 
			
		||||
  }
 | 
			
		||||
  uint32_t delay;
 | 
			
		||||
  uint32_t time_off;
 | 
			
		||||
  uint32_t time_on;
 | 
			
		||||
@@ -87,7 +94,7 @@ struct AutorepeatFilterTiming {
 | 
			
		||||
 | 
			
		||||
class AutorepeatFilter : public Filter, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings);
 | 
			
		||||
  explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings);
 | 
			
		||||
 | 
			
		||||
  optional<bool> new_value(bool value) override;
 | 
			
		||||
 | 
			
		||||
@@ -97,7 +104,7 @@ class AutorepeatFilter : public Filter, public Component {
 | 
			
		||||
  void next_timing_();
 | 
			
		||||
  void next_value_(bool val);
 | 
			
		||||
 | 
			
		||||
  FixedVector<AutorepeatFilterTiming> timings_;
 | 
			
		||||
  std::vector<AutorepeatFilterTiming> timings_;
 | 
			
		||||
  uint8_t active_timing_{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -111,21 +118,6 @@ class LambdaFilter : public Filter {
 | 
			
		||||
  std::function<optional<bool>(bool)> f_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/** Optimized lambda filter for stateless lambdas (no capture).
 | 
			
		||||
 *
 | 
			
		||||
 * Uses function pointer instead of std::function to reduce memory overhead.
 | 
			
		||||
 * Memory: 4 bytes (function pointer on 32-bit) vs 32 bytes (std::function).
 | 
			
		||||
 */
 | 
			
		||||
class StatelessLambdaFilter : public Filter {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit StatelessLambdaFilter(optional<bool> (*f)(bool)) : f_(f) {}
 | 
			
		||||
 | 
			
		||||
  optional<bool> new_value(bool value) override { return this->f_(value); }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  optional<bool> (*f_)(bool);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class SettleFilter : public Filter, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  optional<bool> new_value(bool value) override;
 | 
			
		||||
 
 | 
			
		||||
@@ -96,11 +96,8 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
 | 
			
		||||
  BLEClientWriteAction(BLEClient *ble_client) {
 | 
			
		||||
    ble_client->register_ble_node(this);
 | 
			
		||||
    ble_client_ = ble_client;
 | 
			
		||||
    this->construct_simple_value_();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ~BLEClientWriteAction() { this->destroy_simple_value_(); }
 | 
			
		||||
 | 
			
		||||
  void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
 | 
			
		||||
  void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
 | 
			
		||||
  void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
 | 
			
		||||
@@ -109,18 +106,14 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
 | 
			
		||||
  void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
 | 
			
		||||
  void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
 | 
			
		||||
 | 
			
		||||
  void set_value_template(std::vector<uint8_t> (*func)(Ts...)) {
 | 
			
		||||
    this->destroy_simple_value_();
 | 
			
		||||
    this->value_.template_func = func;
 | 
			
		||||
    this->has_simple_value_ = false;
 | 
			
		||||
  void set_value_template(std::function<std::vector<uint8_t>(Ts...)> func) {
 | 
			
		||||
    this->value_template_ = std::move(func);
 | 
			
		||||
    has_simple_value_ = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_value_simple(const std::vector<uint8_t> &value) {
 | 
			
		||||
    if (!this->has_simple_value_) {
 | 
			
		||||
      this->construct_simple_value_();
 | 
			
		||||
    }
 | 
			
		||||
    this->value_.simple = value;
 | 
			
		||||
    this->has_simple_value_ = true;
 | 
			
		||||
    this->value_simple_ = value;
 | 
			
		||||
    has_simple_value_ = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override {}
 | 
			
		||||
@@ -128,7 +121,7 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
 | 
			
		||||
  void play_complex(Ts... x) override {
 | 
			
		||||
    this->num_running_++;
 | 
			
		||||
    this->var_ = std::make_tuple(x...);
 | 
			
		||||
    auto value = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(x...);
 | 
			
		||||
    auto value = this->has_simple_value_ ? this->value_simple_ : this->value_template_(x...);
 | 
			
		||||
    // on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
 | 
			
		||||
    if (!write(value))
 | 
			
		||||
      this->play_next_(x...);
 | 
			
		||||
@@ -201,22 +194,10 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  void construct_simple_value_() { new (&this->value_.simple) std::vector<uint8_t>(); }
 | 
			
		||||
 | 
			
		||||
  void destroy_simple_value_() {
 | 
			
		||||
    if (this->has_simple_value_) {
 | 
			
		||||
      this->value_.simple.~vector();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  BLEClient *ble_client_;
 | 
			
		||||
  bool has_simple_value_ = true;
 | 
			
		||||
  union Value {
 | 
			
		||||
    std::vector<uint8_t> simple;
 | 
			
		||||
    std::vector<uint8_t> (*template_func)(Ts...);
 | 
			
		||||
    Value() {}   // trivial constructor
 | 
			
		||||
    ~Value() {}  // trivial destructor - we manage lifetime via discriminator
 | 
			
		||||
  } value_;
 | 
			
		||||
  std::vector<uint8_t> value_simple_;
 | 
			
		||||
  std::function<std::vector<uint8_t>(Ts...)> value_template_{};
 | 
			
		||||
  espbt::ESPBTUUID service_uuid_;
 | 
			
		||||
  espbt::ESPBTUUID char_uuid_;
 | 
			
		||||
  std::tuple<Ts...> var_{};
 | 
			
		||||
@@ -232,9 +213,9 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...
 | 
			
		||||
  void play(Ts... x) override {
 | 
			
		||||
    uint32_t passkey;
 | 
			
		||||
    if (has_simple_value_) {
 | 
			
		||||
      passkey = this->value_.simple;
 | 
			
		||||
      passkey = this->value_simple_;
 | 
			
		||||
    } else {
 | 
			
		||||
      passkey = this->value_.template_func(x...);
 | 
			
		||||
      passkey = this->value_template_(x...);
 | 
			
		||||
    }
 | 
			
		||||
    if (passkey > 999999)
 | 
			
		||||
      return;
 | 
			
		||||
@@ -243,23 +224,21 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...
 | 
			
		||||
    esp_ble_passkey_reply(remote_bda, true, passkey);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_value_template(uint32_t (*func)(Ts...)) {
 | 
			
		||||
    this->value_.template_func = func;
 | 
			
		||||
    this->has_simple_value_ = false;
 | 
			
		||||
  void set_value_template(std::function<uint32_t(Ts...)> func) {
 | 
			
		||||
    this->value_template_ = std::move(func);
 | 
			
		||||
    has_simple_value_ = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_value_simple(const uint32_t &value) {
 | 
			
		||||
    this->value_.simple = value;
 | 
			
		||||
    this->has_simple_value_ = true;
 | 
			
		||||
    this->value_simple_ = value;
 | 
			
		||||
    has_simple_value_ = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  BLEClient *parent_{nullptr};
 | 
			
		||||
  bool has_simple_value_ = true;
 | 
			
		||||
  union {
 | 
			
		||||
    uint32_t simple;
 | 
			
		||||
    uint32_t (*template_func)(Ts...);
 | 
			
		||||
  } value_{.simple = 0};
 | 
			
		||||
  uint32_t value_simple_{0};
 | 
			
		||||
  std::function<uint32_t(Ts...)> value_template_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Action<Ts...> {
 | 
			
		||||
@@ -270,29 +249,27 @@ template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Ac
 | 
			
		||||
    esp_bd_addr_t remote_bda;
 | 
			
		||||
    memcpy(remote_bda, parent_->get_remote_bda(), sizeof(esp_bd_addr_t));
 | 
			
		||||
    if (has_simple_value_) {
 | 
			
		||||
      esp_ble_confirm_reply(remote_bda, this->value_.simple);
 | 
			
		||||
      esp_ble_confirm_reply(remote_bda, this->value_simple_);
 | 
			
		||||
    } else {
 | 
			
		||||
      esp_ble_confirm_reply(remote_bda, this->value_.template_func(x...));
 | 
			
		||||
      esp_ble_confirm_reply(remote_bda, this->value_template_(x...));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_value_template(bool (*func)(Ts...)) {
 | 
			
		||||
    this->value_.template_func = func;
 | 
			
		||||
    this->has_simple_value_ = false;
 | 
			
		||||
  void set_value_template(std::function<bool(Ts...)> func) {
 | 
			
		||||
    this->value_template_ = std::move(func);
 | 
			
		||||
    has_simple_value_ = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_value_simple(const bool &value) {
 | 
			
		||||
    this->value_.simple = value;
 | 
			
		||||
    this->has_simple_value_ = true;
 | 
			
		||||
    this->value_simple_ = value;
 | 
			
		||||
    has_simple_value_ = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  BLEClient *parent_{nullptr};
 | 
			
		||||
  bool has_simple_value_ = true;
 | 
			
		||||
  union {
 | 
			
		||||
    bool simple;
 | 
			
		||||
    bool (*template_func)(Ts...);
 | 
			
		||||
  } value_{.simple = false};
 | 
			
		||||
  bool value_simple_{false};
 | 
			
		||||
  std::function<bool(Ts...)> value_template_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...> {
 | 
			
		||||
 
 | 
			
		||||
@@ -77,9 +77,6 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
        // For non-notify characteristics, trigger an immediate read after service discovery
 | 
			
		||||
        // to avoid peripherals disconnecting due to inactivity
 | 
			
		||||
        this->update();
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
@@ -120,9 +117,9 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float BLESensor::parse_data_(uint8_t *value, uint16_t value_len) {
 | 
			
		||||
  if (this->has_data_to_value_) {
 | 
			
		||||
  if (this->data_to_value_func_.has_value()) {
 | 
			
		||||
    std::vector<uint8_t> data(value, value + value_len);
 | 
			
		||||
    return this->data_to_value_func_(data);
 | 
			
		||||
    return (*this->data_to_value_func_)(data);
 | 
			
		||||
  } else {
 | 
			
		||||
    return value[0];
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -15,6 +15,8 @@ namespace ble_client {
 | 
			
		||||
 | 
			
		||||
namespace espbt = esphome::esp32_ble_tracker;
 | 
			
		||||
 | 
			
		||||
using data_to_value_t = std::function<float(std::vector<uint8_t>)>;
 | 
			
		||||
 | 
			
		||||
class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClientNode {
 | 
			
		||||
 public:
 | 
			
		||||
  void loop() override;
 | 
			
		||||
@@ -31,17 +33,13 @@ class BLESensor : public sensor::Sensor, public PollingComponent, public BLEClie
 | 
			
		||||
  void set_descr_uuid16(uint16_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
 | 
			
		||||
  void set_descr_uuid32(uint32_t uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
 | 
			
		||||
  void set_descr_uuid128(uint8_t *uuid) { this->descr_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
 | 
			
		||||
  void set_data_to_value(float (*lambda)(const std::vector<uint8_t> &)) {
 | 
			
		||||
    this->data_to_value_func_ = lambda;
 | 
			
		||||
    this->has_data_to_value_ = true;
 | 
			
		||||
  }
 | 
			
		||||
  void set_data_to_value(data_to_value_t &&lambda) { this->data_to_value_func_ = lambda; }
 | 
			
		||||
  void set_enable_notify(bool notify) { this->notify_ = notify; }
 | 
			
		||||
  uint16_t handle;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  float parse_data_(uint8_t *value, uint16_t value_len);
 | 
			
		||||
  bool has_data_to_value_{false};
 | 
			
		||||
  float (*data_to_value_func_)(const std::vector<uint8_t> &){};
 | 
			
		||||
  optional<data_to_value_t> data_to_value_func_{};
 | 
			
		||||
  bool notify_;
 | 
			
		||||
  espbt::ESPBTUUID service_uuid_;
 | 
			
		||||
  espbt::ESPBTUUID char_uuid_;
 | 
			
		||||
 
 | 
			
		||||
@@ -79,9 +79,6 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
        // For non-notify characteristics, trigger an immediate read after service discovery
 | 
			
		||||
        // to avoid peripherals disconnecting due to inactivity
 | 
			
		||||
        this->update();
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -155,12 +155,16 @@ esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_par
 | 
			
		||||
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
 | 
			
		||||
  for (uint8_t i = 0; i < this->connection_count_; i++) {
 | 
			
		||||
    auto *connection = this->connections_[i];
 | 
			
		||||
    uint64_t conn_addr = connection->get_address();
 | 
			
		||||
 | 
			
		||||
    if (conn_addr == address)
 | 
			
		||||
    if (connection->get_address() == address)
 | 
			
		||||
      return connection;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
    if (reserve && conn_addr == 0) {
 | 
			
		||||
  if (!reserve)
 | 
			
		||||
    return nullptr;
 | 
			
		||||
 | 
			
		||||
  for (uint8_t i = 0; i < this->connection_count_; i++) {
 | 
			
		||||
    auto *connection = this->connections_[i];
 | 
			
		||||
    if (connection->get_address() == 0) {
 | 
			
		||||
      connection->send_service_ = INIT_SENDING_SERVICES;
 | 
			
		||||
      connection->set_address(address);
 | 
			
		||||
      // All connections must start at INIT
 | 
			
		||||
@@ -171,6 +175,7 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
 | 
			
		||||
      return connection;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return nullptr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -84,6 +84,11 @@ def button_schema(
 | 
			
		||||
    return _BUTTON_SCHEMA.extend(schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Remove before 2025.11.0
 | 
			
		||||
BUTTON_SCHEMA = button_schema(Button)
 | 
			
		||||
BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def setup_button_core_(var, config):
 | 
			
		||||
    await setup_entity(var, config, "button")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -270,6 +270,11 @@ def climate_schema(
 | 
			
		||||
    return _CLIMATE_SCHEMA.extend(schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Remove before 2025.11.0
 | 
			
		||||
CLIMATE_SCHEMA = climate_schema(Climate)
 | 
			
		||||
CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def setup_climate_core_(var, config):
 | 
			
		||||
    await setup_entity(var, config, "climate")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,42 +6,6 @@ namespace climate {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "climate";
 | 
			
		||||
 | 
			
		||||
// Memory-efficient lookup tables
 | 
			
		||||
struct StringToUint8 {
 | 
			
		||||
  const char *str;
 | 
			
		||||
  const uint8_t value;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
constexpr StringToUint8 CLIMATE_MODES_BY_STR[] = {
 | 
			
		||||
    {"OFF", CLIMATE_MODE_OFF},
 | 
			
		||||
    {"AUTO", CLIMATE_MODE_AUTO},
 | 
			
		||||
    {"COOL", CLIMATE_MODE_COOL},
 | 
			
		||||
    {"HEAT", CLIMATE_MODE_HEAT},
 | 
			
		||||
    {"FAN_ONLY", CLIMATE_MODE_FAN_ONLY},
 | 
			
		||||
    {"DRY", CLIMATE_MODE_DRY},
 | 
			
		||||
    {"HEAT_COOL", CLIMATE_MODE_HEAT_COOL},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
constexpr StringToUint8 CLIMATE_FAN_MODES_BY_STR[] = {
 | 
			
		||||
    {"ON", CLIMATE_FAN_ON},         {"OFF", CLIMATE_FAN_OFF},       {"AUTO", CLIMATE_FAN_AUTO},
 | 
			
		||||
    {"LOW", CLIMATE_FAN_LOW},       {"MEDIUM", CLIMATE_FAN_MEDIUM}, {"HIGH", CLIMATE_FAN_HIGH},
 | 
			
		||||
    {"MIDDLE", CLIMATE_FAN_MIDDLE}, {"FOCUS", CLIMATE_FAN_FOCUS},   {"DIFFUSE", CLIMATE_FAN_DIFFUSE},
 | 
			
		||||
    {"QUIET", CLIMATE_FAN_QUIET},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
constexpr StringToUint8 CLIMATE_PRESETS_BY_STR[] = {
 | 
			
		||||
    {"ECO", CLIMATE_PRESET_ECO},           {"AWAY", CLIMATE_PRESET_AWAY}, {"BOOST", CLIMATE_PRESET_BOOST},
 | 
			
		||||
    {"COMFORT", CLIMATE_PRESET_COMFORT},   {"HOME", CLIMATE_PRESET_HOME}, {"SLEEP", CLIMATE_PRESET_SLEEP},
 | 
			
		||||
    {"ACTIVITY", CLIMATE_PRESET_ACTIVITY}, {"NONE", CLIMATE_PRESET_NONE},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
constexpr StringToUint8 CLIMATE_SWING_MODES_BY_STR[] = {
 | 
			
		||||
    {"OFF", CLIMATE_SWING_OFF},
 | 
			
		||||
    {"BOTH", CLIMATE_SWING_BOTH},
 | 
			
		||||
    {"VERTICAL", CLIMATE_SWING_VERTICAL},
 | 
			
		||||
    {"HORIZONTAL", CLIMATE_SWING_HORIZONTAL},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
void ClimateCall::perform() {
 | 
			
		||||
  this->parent_->control_callback_.call(*this);
 | 
			
		||||
  ESP_LOGD(TAG, "'%s' - Setting", this->parent_->get_name().c_str());
 | 
			
		||||
@@ -86,46 +50,47 @@ void ClimateCall::perform() {
 | 
			
		||||
  }
 | 
			
		||||
  this->parent_->control(*this);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ClimateCall::validate_() {
 | 
			
		||||
  auto traits = this->parent_->get_traits();
 | 
			
		||||
  if (this->mode_.has_value()) {
 | 
			
		||||
    auto mode = *this->mode_;
 | 
			
		||||
    if (!traits.supports_mode(mode)) {
 | 
			
		||||
      ESP_LOGW(TAG, "  Mode %s not supported", LOG_STR_ARG(climate_mode_to_string(mode)));
 | 
			
		||||
      ESP_LOGW(TAG, "  Mode %s is not supported by this device!", LOG_STR_ARG(climate_mode_to_string(mode)));
 | 
			
		||||
      this->mode_.reset();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (this->custom_fan_mode_.has_value()) {
 | 
			
		||||
    auto custom_fan_mode = *this->custom_fan_mode_;
 | 
			
		||||
    if (!traits.supports_custom_fan_mode(custom_fan_mode)) {
 | 
			
		||||
      ESP_LOGW(TAG, "  Fan Mode %s not supported", custom_fan_mode.c_str());
 | 
			
		||||
      ESP_LOGW(TAG, "  Fan Mode %s is not supported by this device!", custom_fan_mode.c_str());
 | 
			
		||||
      this->custom_fan_mode_.reset();
 | 
			
		||||
    }
 | 
			
		||||
  } else if (this->fan_mode_.has_value()) {
 | 
			
		||||
    auto fan_mode = *this->fan_mode_;
 | 
			
		||||
    if (!traits.supports_fan_mode(fan_mode)) {
 | 
			
		||||
      ESP_LOGW(TAG, "  Fan Mode %s not supported", LOG_STR_ARG(climate_fan_mode_to_string(fan_mode)));
 | 
			
		||||
      ESP_LOGW(TAG, "  Fan Mode %s is not supported by this device!",
 | 
			
		||||
               LOG_STR_ARG(climate_fan_mode_to_string(fan_mode)));
 | 
			
		||||
      this->fan_mode_.reset();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (this->custom_preset_.has_value()) {
 | 
			
		||||
    auto custom_preset = *this->custom_preset_;
 | 
			
		||||
    if (!traits.supports_custom_preset(custom_preset)) {
 | 
			
		||||
      ESP_LOGW(TAG, "  Preset %s not supported", custom_preset.c_str());
 | 
			
		||||
      ESP_LOGW(TAG, "  Preset %s is not supported by this device!", custom_preset.c_str());
 | 
			
		||||
      this->custom_preset_.reset();
 | 
			
		||||
    }
 | 
			
		||||
  } else if (this->preset_.has_value()) {
 | 
			
		||||
    auto preset = *this->preset_;
 | 
			
		||||
    if (!traits.supports_preset(preset)) {
 | 
			
		||||
      ESP_LOGW(TAG, "  Preset %s not supported", LOG_STR_ARG(climate_preset_to_string(preset)));
 | 
			
		||||
      ESP_LOGW(TAG, "  Preset %s is not supported by this device!", LOG_STR_ARG(climate_preset_to_string(preset)));
 | 
			
		||||
      this->preset_.reset();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (this->swing_mode_.has_value()) {
 | 
			
		||||
    auto swing_mode = *this->swing_mode_;
 | 
			
		||||
    if (!traits.supports_swing_mode(swing_mode)) {
 | 
			
		||||
      ESP_LOGW(TAG, "  Swing Mode %s not supported", LOG_STR_ARG(climate_swing_mode_to_string(swing_mode)));
 | 
			
		||||
      ESP_LOGW(TAG, "  Swing Mode %s is not supported by this device!",
 | 
			
		||||
               LOG_STR_ARG(climate_swing_mode_to_string(swing_mode)));
 | 
			
		||||
      this->swing_mode_.reset();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -134,127 +99,159 @@ void ClimateCall::validate_() {
 | 
			
		||||
    if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
 | 
			
		||||
                                 CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
 | 
			
		||||
      ESP_LOGW(TAG, "  Cannot set target temperature for climate device "
 | 
			
		||||
                    "with two-point target temperature");
 | 
			
		||||
                    "with two-point target temperature!");
 | 
			
		||||
      this->target_temperature_.reset();
 | 
			
		||||
    } else if (std::isnan(target)) {
 | 
			
		||||
      ESP_LOGW(TAG, "  Target temperature must not be NAN");
 | 
			
		||||
      ESP_LOGW(TAG, "  Target temperature must not be NAN!");
 | 
			
		||||
      this->target_temperature_.reset();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (this->target_temperature_low_.has_value() || this->target_temperature_high_.has_value()) {
 | 
			
		||||
    if (!traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
 | 
			
		||||
                                  CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
 | 
			
		||||
      ESP_LOGW(TAG, "  Cannot set low/high target temperature");
 | 
			
		||||
      ESP_LOGW(TAG, "  Cannot set low/high target temperature for this device!");
 | 
			
		||||
      this->target_temperature_low_.reset();
 | 
			
		||||
      this->target_temperature_high_.reset();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (this->target_temperature_low_.has_value() && std::isnan(*this->target_temperature_low_)) {
 | 
			
		||||
    ESP_LOGW(TAG, "  Target temperature low must not be NAN");
 | 
			
		||||
    ESP_LOGW(TAG, "  Target temperature low must not be NAN!");
 | 
			
		||||
    this->target_temperature_low_.reset();
 | 
			
		||||
  }
 | 
			
		||||
  if (this->target_temperature_high_.has_value() && std::isnan(*this->target_temperature_high_)) {
 | 
			
		||||
    ESP_LOGW(TAG, "  Target temperature high must not be NAN");
 | 
			
		||||
    ESP_LOGW(TAG, "  Target temperature low must not be NAN!");
 | 
			
		||||
    this->target_temperature_high_.reset();
 | 
			
		||||
  }
 | 
			
		||||
  if (this->target_temperature_low_.has_value() && this->target_temperature_high_.has_value()) {
 | 
			
		||||
    float low = *this->target_temperature_low_;
 | 
			
		||||
    float high = *this->target_temperature_high_;
 | 
			
		||||
    if (low > high) {
 | 
			
		||||
      ESP_LOGW(TAG, "  Target temperature low %.2f must be less than target temperature high %.2f", low, high);
 | 
			
		||||
      ESP_LOGW(TAG, "  Target temperature low %.2f must be smaller than target temperature high %.2f!", low, high);
 | 
			
		||||
      this->target_temperature_low_.reset();
 | 
			
		||||
      this->target_temperature_high_.reset();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_mode(ClimateMode mode) {
 | 
			
		||||
  this->mode_ = mode;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_mode(const std::string &mode) {
 | 
			
		||||
  for (const auto &mode_entry : CLIMATE_MODES_BY_STR) {
 | 
			
		||||
    if (str_equals_case_insensitive(mode, mode_entry.str)) {
 | 
			
		||||
      this->set_mode(static_cast<ClimateMode>(mode_entry.value));
 | 
			
		||||
      return *this;
 | 
			
		||||
    }
 | 
			
		||||
  if (str_equals_case_insensitive(mode, "OFF")) {
 | 
			
		||||
    this->set_mode(CLIMATE_MODE_OFF);
 | 
			
		||||
  } else if (str_equals_case_insensitive(mode, "AUTO")) {
 | 
			
		||||
    this->set_mode(CLIMATE_MODE_AUTO);
 | 
			
		||||
  } else if (str_equals_case_insensitive(mode, "COOL")) {
 | 
			
		||||
    this->set_mode(CLIMATE_MODE_COOL);
 | 
			
		||||
  } else if (str_equals_case_insensitive(mode, "HEAT")) {
 | 
			
		||||
    this->set_mode(CLIMATE_MODE_HEAT);
 | 
			
		||||
  } else if (str_equals_case_insensitive(mode, "FAN_ONLY")) {
 | 
			
		||||
    this->set_mode(CLIMATE_MODE_FAN_ONLY);
 | 
			
		||||
  } else if (str_equals_case_insensitive(mode, "DRY")) {
 | 
			
		||||
    this->set_mode(CLIMATE_MODE_DRY);
 | 
			
		||||
  } else if (str_equals_case_insensitive(mode, "HEAT_COOL")) {
 | 
			
		||||
    this->set_mode(CLIMATE_MODE_HEAT_COOL);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str());
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGW(TAG, "'%s' - Unrecognized mode %s", this->parent_->get_name().c_str(), mode.c_str());
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) {
 | 
			
		||||
  this->fan_mode_ = fan_mode;
 | 
			
		||||
  this->custom_fan_mode_.reset();
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) {
 | 
			
		||||
  for (const auto &mode_entry : CLIMATE_FAN_MODES_BY_STR) {
 | 
			
		||||
    if (str_equals_case_insensitive(fan_mode, mode_entry.str)) {
 | 
			
		||||
      this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value));
 | 
			
		||||
      return *this;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) {
 | 
			
		||||
    this->custom_fan_mode_ = fan_mode;
 | 
			
		||||
    this->fan_mode_.reset();
 | 
			
		||||
  if (str_equals_case_insensitive(fan_mode, "ON")) {
 | 
			
		||||
    this->set_fan_mode(CLIMATE_FAN_ON);
 | 
			
		||||
  } else if (str_equals_case_insensitive(fan_mode, "OFF")) {
 | 
			
		||||
    this->set_fan_mode(CLIMATE_FAN_OFF);
 | 
			
		||||
  } else if (str_equals_case_insensitive(fan_mode, "AUTO")) {
 | 
			
		||||
    this->set_fan_mode(CLIMATE_FAN_AUTO);
 | 
			
		||||
  } else if (str_equals_case_insensitive(fan_mode, "LOW")) {
 | 
			
		||||
    this->set_fan_mode(CLIMATE_FAN_LOW);
 | 
			
		||||
  } else if (str_equals_case_insensitive(fan_mode, "MEDIUM")) {
 | 
			
		||||
    this->set_fan_mode(CLIMATE_FAN_MEDIUM);
 | 
			
		||||
  } else if (str_equals_case_insensitive(fan_mode, "HIGH")) {
 | 
			
		||||
    this->set_fan_mode(CLIMATE_FAN_HIGH);
 | 
			
		||||
  } else if (str_equals_case_insensitive(fan_mode, "MIDDLE")) {
 | 
			
		||||
    this->set_fan_mode(CLIMATE_FAN_MIDDLE);
 | 
			
		||||
  } else if (str_equals_case_insensitive(fan_mode, "FOCUS")) {
 | 
			
		||||
    this->set_fan_mode(CLIMATE_FAN_FOCUS);
 | 
			
		||||
  } else if (str_equals_case_insensitive(fan_mode, "DIFFUSE")) {
 | 
			
		||||
    this->set_fan_mode(CLIMATE_FAN_DIFFUSE);
 | 
			
		||||
  } else if (str_equals_case_insensitive(fan_mode, "QUIET")) {
 | 
			
		||||
    this->set_fan_mode(CLIMATE_FAN_QUIET);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
 | 
			
		||||
    if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) {
 | 
			
		||||
      this->custom_fan_mode_ = fan_mode;
 | 
			
		||||
      this->fan_mode_.reset();
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), fan_mode.c_str());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) {
 | 
			
		||||
  if (fan_mode.has_value()) {
 | 
			
		||||
    this->set_fan_mode(fan_mode.value());
 | 
			
		||||
  }
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_preset(ClimatePreset preset) {
 | 
			
		||||
  this->preset_ = preset;
 | 
			
		||||
  this->custom_preset_.reset();
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_preset(const std::string &preset) {
 | 
			
		||||
  for (const auto &preset_entry : CLIMATE_PRESETS_BY_STR) {
 | 
			
		||||
    if (str_equals_case_insensitive(preset, preset_entry.str)) {
 | 
			
		||||
      this->set_preset(static_cast<ClimatePreset>(preset_entry.value));
 | 
			
		||||
      return *this;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (this->parent_->get_traits().supports_custom_preset(preset)) {
 | 
			
		||||
    this->custom_preset_ = preset;
 | 
			
		||||
    this->preset_.reset();
 | 
			
		||||
  if (str_equals_case_insensitive(preset, "ECO")) {
 | 
			
		||||
    this->set_preset(CLIMATE_PRESET_ECO);
 | 
			
		||||
  } else if (str_equals_case_insensitive(preset, "AWAY")) {
 | 
			
		||||
    this->set_preset(CLIMATE_PRESET_AWAY);
 | 
			
		||||
  } else if (str_equals_case_insensitive(preset, "BOOST")) {
 | 
			
		||||
    this->set_preset(CLIMATE_PRESET_BOOST);
 | 
			
		||||
  } else if (str_equals_case_insensitive(preset, "COMFORT")) {
 | 
			
		||||
    this->set_preset(CLIMATE_PRESET_COMFORT);
 | 
			
		||||
  } else if (str_equals_case_insensitive(preset, "HOME")) {
 | 
			
		||||
    this->set_preset(CLIMATE_PRESET_HOME);
 | 
			
		||||
  } else if (str_equals_case_insensitive(preset, "SLEEP")) {
 | 
			
		||||
    this->set_preset(CLIMATE_PRESET_SLEEP);
 | 
			
		||||
  } else if (str_equals_case_insensitive(preset, "ACTIVITY")) {
 | 
			
		||||
    this->set_preset(CLIMATE_PRESET_ACTIVITY);
 | 
			
		||||
  } else if (str_equals_case_insensitive(preset, "NONE")) {
 | 
			
		||||
    this->set_preset(CLIMATE_PRESET_NONE);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
 | 
			
		||||
    if (this->parent_->get_traits().supports_custom_preset(preset)) {
 | 
			
		||||
      this->custom_preset_ = preset;
 | 
			
		||||
      this->preset_.reset();
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str());
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_preset(optional<std::string> preset) {
 | 
			
		||||
  if (preset.has_value()) {
 | 
			
		||||
    this->set_preset(preset.value());
 | 
			
		||||
  }
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_swing_mode(ClimateSwingMode swing_mode) {
 | 
			
		||||
  this->swing_mode_ = swing_mode;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_swing_mode(const std::string &swing_mode) {
 | 
			
		||||
  for (const auto &mode_entry : CLIMATE_SWING_MODES_BY_STR) {
 | 
			
		||||
    if (str_equals_case_insensitive(swing_mode, mode_entry.str)) {
 | 
			
		||||
      this->set_swing_mode(static_cast<ClimateSwingMode>(mode_entry.value));
 | 
			
		||||
      return *this;
 | 
			
		||||
    }
 | 
			
		||||
  if (str_equals_case_insensitive(swing_mode, "OFF")) {
 | 
			
		||||
    this->set_swing_mode(CLIMATE_SWING_OFF);
 | 
			
		||||
  } else if (str_equals_case_insensitive(swing_mode, "BOTH")) {
 | 
			
		||||
    this->set_swing_mode(CLIMATE_SWING_BOTH);
 | 
			
		||||
  } else if (str_equals_case_insensitive(swing_mode, "VERTICAL")) {
 | 
			
		||||
    this->set_swing_mode(CLIMATE_SWING_VERTICAL);
 | 
			
		||||
  } else if (str_equals_case_insensitive(swing_mode, "HORIZONTAL")) {
 | 
			
		||||
    this->set_swing_mode(CLIMATE_SWING_HORIZONTAL);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str());
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str());
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -262,71 +259,59 @@ ClimateCall &ClimateCall::set_target_temperature(float target_temperature) {
 | 
			
		||||
  this->target_temperature_ = target_temperature;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_target_temperature_low(float target_temperature_low) {
 | 
			
		||||
  this->target_temperature_low_ = target_temperature_low;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_target_temperature_high(float target_temperature_high) {
 | 
			
		||||
  this->target_temperature_high_ = target_temperature_high;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_target_humidity(float target_humidity) {
 | 
			
		||||
  this->target_humidity_ = target_humidity;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_; }
 | 
			
		||||
const optional<float> &ClimateCall::get_target_temperature() const { return this->target_temperature_; }
 | 
			
		||||
const optional<float> &ClimateCall::get_target_temperature_low() const { return this->target_temperature_low_; }
 | 
			
		||||
const optional<float> &ClimateCall::get_target_temperature_high() const { return this->target_temperature_high_; }
 | 
			
		||||
const optional<float> &ClimateCall::get_target_humidity() const { return this->target_humidity_; }
 | 
			
		||||
 | 
			
		||||
const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_; }
 | 
			
		||||
const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; }
 | 
			
		||||
const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; }
 | 
			
		||||
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
 | 
			
		||||
const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; }
 | 
			
		||||
const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; }
 | 
			
		||||
const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; }
 | 
			
		||||
 | 
			
		||||
const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; }
 | 
			
		||||
ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) {
 | 
			
		||||
  this->target_temperature_high_ = target_temperature_high;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_target_temperature_low(optional<float> target_temperature_low) {
 | 
			
		||||
  this->target_temperature_low_ = target_temperature_low;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_target_temperature(optional<float> target_temperature) {
 | 
			
		||||
  this->target_temperature_ = target_temperature;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_target_humidity(optional<float> target_humidity) {
 | 
			
		||||
  this->target_humidity_ = target_humidity;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) {
 | 
			
		||||
  this->mode_ = mode;
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) {
 | 
			
		||||
  this->fan_mode_ = fan_mode;
 | 
			
		||||
  this->custom_fan_mode_.reset();
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) {
 | 
			
		||||
  this->preset_ = preset;
 | 
			
		||||
  this->custom_preset_.reset();
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ClimateCall &ClimateCall::set_swing_mode(optional<ClimateSwingMode> swing_mode) {
 | 
			
		||||
  this->swing_mode_ = swing_mode;
 | 
			
		||||
  return *this;
 | 
			
		||||
@@ -351,7 +336,6 @@ optional<ClimateDeviceRestoreState> Climate::restore_state_() {
 | 
			
		||||
    return {};
 | 
			
		||||
  return recovered;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Climate::save_state_() {
 | 
			
		||||
#if (defined(USE_ESP_IDF) || (defined(USE_ESP8266) && USE_ARDUINO_VERSION_CODE >= VERSION_CODE(3, 0, 0))) && \
 | 
			
		||||
    !defined(CLANG_TIDY)
 | 
			
		||||
@@ -385,14 +369,12 @@ void Climate::save_state_() {
 | 
			
		||||
  if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) {
 | 
			
		||||
    state.uses_custom_fan_mode = true;
 | 
			
		||||
    const auto &supported = traits.get_supported_custom_fan_modes();
 | 
			
		||||
    // std::vector maintains insertion order
 | 
			
		||||
    size_t i = 0;
 | 
			
		||||
    for (const auto &mode : supported) {
 | 
			
		||||
      if (mode == custom_fan_mode) {
 | 
			
		||||
    std::vector<std::string> vec{supported.begin(), supported.end()};
 | 
			
		||||
    for (size_t i = 0; i < vec.size(); i++) {
 | 
			
		||||
      if (vec[i] == custom_fan_mode) {
 | 
			
		||||
        state.custom_fan_mode = i;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      i++;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.get_supports_presets() && preset.has_value()) {
 | 
			
		||||
@@ -402,14 +384,12 @@ void Climate::save_state_() {
 | 
			
		||||
  if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) {
 | 
			
		||||
    state.uses_custom_preset = true;
 | 
			
		||||
    const auto &supported = traits.get_supported_custom_presets();
 | 
			
		||||
    // std::vector maintains insertion order
 | 
			
		||||
    size_t i = 0;
 | 
			
		||||
    for (const auto &preset : supported) {
 | 
			
		||||
      if (preset == custom_preset) {
 | 
			
		||||
    std::vector<std::string> vec{supported.begin(), supported.end()};
 | 
			
		||||
    for (size_t i = 0; i < vec.size(); i++) {
 | 
			
		||||
      if (vec[i] == custom_preset) {
 | 
			
		||||
        state.custom_preset = i;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      i++;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.get_supports_swing_modes()) {
 | 
			
		||||
@@ -418,7 +398,6 @@ void Climate::save_state_() {
 | 
			
		||||
 | 
			
		||||
  this->rtc_.save(&state);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Climate::publish_state() {
 | 
			
		||||
  ESP_LOGD(TAG, "'%s' - Sending state:", this->name_.c_str());
 | 
			
		||||
  auto traits = this->get_traits();
 | 
			
		||||
@@ -490,20 +469,16 @@ ClimateTraits Climate::get_traits() {
 | 
			
		||||
void Climate::set_visual_min_temperature_override(float visual_min_temperature_override) {
 | 
			
		||||
  this->visual_min_temperature_override_ = visual_min_temperature_override;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Climate::set_visual_max_temperature_override(float visual_max_temperature_override) {
 | 
			
		||||
  this->visual_max_temperature_override_ = visual_max_temperature_override;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Climate::set_visual_temperature_step_override(float target, float current) {
 | 
			
		||||
  this->visual_target_temperature_step_override_ = target;
 | 
			
		||||
  this->visual_current_temperature_step_override_ = current;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Climate::set_visual_min_humidity_override(float visual_min_humidity_override) {
 | 
			
		||||
  this->visual_min_humidity_override_ = visual_min_humidity_override;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Climate::set_visual_max_humidity_override(float visual_max_humidity_override) {
 | 
			
		||||
  this->visual_max_humidity_override_ = visual_max_humidity_override;
 | 
			
		||||
}
 | 
			
		||||
@@ -524,28 +499,17 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
 | 
			
		||||
    call.set_target_humidity(this->target_humidity);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->uses_custom_fan_mode) {
 | 
			
		||||
    if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
 | 
			
		||||
      call.fan_mode_.reset();
 | 
			
		||||
      call.custom_fan_mode_ = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode);
 | 
			
		||||
    }
 | 
			
		||||
  } else if (traits.supports_fan_mode(this->fan_mode)) {
 | 
			
		||||
  if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) {
 | 
			
		||||
    call.set_fan_mode(this->fan_mode);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->uses_custom_preset) {
 | 
			
		||||
    if (this->custom_preset < traits.get_supported_custom_presets().size()) {
 | 
			
		||||
      call.preset_.reset();
 | 
			
		||||
      call.custom_preset_ = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset);
 | 
			
		||||
    }
 | 
			
		||||
  } else if (traits.supports_preset(this->preset)) {
 | 
			
		||||
  if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
 | 
			
		||||
    call.set_preset(this->preset);
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.supports_swing_mode(this->swing_mode)) {
 | 
			
		||||
  if (traits.get_supports_swing_modes()) {
 | 
			
		||||
    call.set_swing_mode(this->swing_mode);
 | 
			
		||||
  }
 | 
			
		||||
  return call;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ClimateDeviceRestoreState::apply(Climate *climate) {
 | 
			
		||||
  auto traits = climate->get_traits();
 | 
			
		||||
  climate->mode = this->mode;
 | 
			
		||||
@@ -559,25 +523,29 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
 | 
			
		||||
    climate->target_humidity = this->target_humidity;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->uses_custom_fan_mode) {
 | 
			
		||||
    if (this->custom_fan_mode < traits.get_supported_custom_fan_modes().size()) {
 | 
			
		||||
      climate->fan_mode.reset();
 | 
			
		||||
      climate->custom_fan_mode = *std::next(traits.get_supported_custom_fan_modes().cbegin(), this->custom_fan_mode);
 | 
			
		||||
    }
 | 
			
		||||
  } else if (traits.supports_fan_mode(this->fan_mode)) {
 | 
			
		||||
  if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) {
 | 
			
		||||
    climate->fan_mode = this->fan_mode;
 | 
			
		||||
    climate->custom_fan_mode.reset();
 | 
			
		||||
  }
 | 
			
		||||
  if (this->uses_custom_preset) {
 | 
			
		||||
    if (this->custom_preset < traits.get_supported_custom_presets().size()) {
 | 
			
		||||
      climate->preset.reset();
 | 
			
		||||
      climate->custom_preset = *std::next(traits.get_supported_custom_presets().cbegin(), this->custom_preset);
 | 
			
		||||
  if (!traits.get_supported_custom_fan_modes().empty() && this->uses_custom_fan_mode) {
 | 
			
		||||
    // std::set has consistent order (lexicographic for strings), so this is ok
 | 
			
		||||
    const auto &modes = traits.get_supported_custom_fan_modes();
 | 
			
		||||
    std::vector<std::string> modes_vec{modes.begin(), modes.end()};
 | 
			
		||||
    if (custom_fan_mode < modes_vec.size()) {
 | 
			
		||||
      climate->custom_fan_mode = modes_vec[this->custom_fan_mode];
 | 
			
		||||
    }
 | 
			
		||||
  } else if (traits.supports_preset(this->preset)) {
 | 
			
		||||
    climate->preset = this->preset;
 | 
			
		||||
    climate->custom_preset.reset();
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.supports_swing_mode(this->swing_mode)) {
 | 
			
		||||
  if (traits.get_supports_presets() && !this->uses_custom_preset) {
 | 
			
		||||
    climate->preset = this->preset;
 | 
			
		||||
  }
 | 
			
		||||
  if (!traits.get_supported_custom_presets().empty() && uses_custom_preset) {
 | 
			
		||||
    // std::set has consistent order (lexicographic for strings), so this is ok
 | 
			
		||||
    const auto &presets = traits.get_supported_custom_presets();
 | 
			
		||||
    std::vector<std::string> presets_vec{presets.begin(), presets.end()};
 | 
			
		||||
    if (custom_preset < presets_vec.size()) {
 | 
			
		||||
      climate->custom_preset = presets_vec[this->custom_preset];
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.get_supports_swing_modes()) {
 | 
			
		||||
    climate->swing_mode = this->swing_mode;
 | 
			
		||||
  }
 | 
			
		||||
  climate->publish_state();
 | 
			
		||||
@@ -611,68 +579,68 @@ void Climate::dump_traits_(const char *tag) {
 | 
			
		||||
  auto traits = this->get_traits();
 | 
			
		||||
  ESP_LOGCONFIG(tag, "ClimateTraits:");
 | 
			
		||||
  ESP_LOGCONFIG(tag,
 | 
			
		||||
                "  Visual settings:\n"
 | 
			
		||||
                "  - Min temperature: %.1f\n"
 | 
			
		||||
                "  - Max temperature: %.1f\n"
 | 
			
		||||
                "  - Temperature step:\n"
 | 
			
		||||
                "      Target: %.1f",
 | 
			
		||||
                "  [x] Visual settings:\n"
 | 
			
		||||
                "      - Min temperature: %.1f\n"
 | 
			
		||||
                "      - Max temperature: %.1f\n"
 | 
			
		||||
                "      - Temperature step:\n"
 | 
			
		||||
                "          Target: %.1f",
 | 
			
		||||
                traits.get_visual_min_temperature(), traits.get_visual_max_temperature(),
 | 
			
		||||
                traits.get_visual_target_temperature_step());
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "      Current: %.1f", traits.get_visual_current_temperature_step());
 | 
			
		||||
    ESP_LOGCONFIG(tag, "          Current: %.1f", traits.get_visual_current_temperature_step());
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY |
 | 
			
		||||
                               climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
 | 
			
		||||
    ESP_LOGCONFIG(tag,
 | 
			
		||||
                  "  - Min humidity: %.0f\n"
 | 
			
		||||
                  "  - Max humidity: %.0f",
 | 
			
		||||
                  "      - Min humidity: %.0f\n"
 | 
			
		||||
                  "      - Max humidity: %.0f",
 | 
			
		||||
                  traits.get_visual_min_humidity(), traits.get_visual_max_humidity());
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE |
 | 
			
		||||
                               CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE)) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supports two-point target temperature");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supports two-point target temperature");
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supports current temperature");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supports current temperature");
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supports target humidity");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supports target humidity");
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supports current humidity");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supports current humidity");
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supports action");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supports action");
 | 
			
		||||
  }
 | 
			
		||||
  if (!traits.get_supported_modes().empty()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supported modes:");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supported modes:");
 | 
			
		||||
    for (ClimateMode m : traits.get_supported_modes())
 | 
			
		||||
      ESP_LOGCONFIG(tag, "  - %s", LOG_STR_ARG(climate_mode_to_string(m)));
 | 
			
		||||
      ESP_LOGCONFIG(tag, "      - %s", LOG_STR_ARG(climate_mode_to_string(m)));
 | 
			
		||||
  }
 | 
			
		||||
  if (!traits.get_supported_fan_modes().empty()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supported fan modes:");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supported fan modes:");
 | 
			
		||||
    for (ClimateFanMode m : traits.get_supported_fan_modes())
 | 
			
		||||
      ESP_LOGCONFIG(tag, "  - %s", LOG_STR_ARG(climate_fan_mode_to_string(m)));
 | 
			
		||||
      ESP_LOGCONFIG(tag, "      - %s", LOG_STR_ARG(climate_fan_mode_to_string(m)));
 | 
			
		||||
  }
 | 
			
		||||
  if (!traits.get_supported_custom_fan_modes().empty()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supported custom fan modes:");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supported custom fan modes:");
 | 
			
		||||
    for (const std::string &s : traits.get_supported_custom_fan_modes())
 | 
			
		||||
      ESP_LOGCONFIG(tag, "  - %s", s.c_str());
 | 
			
		||||
      ESP_LOGCONFIG(tag, "      - %s", s.c_str());
 | 
			
		||||
  }
 | 
			
		||||
  if (!traits.get_supported_presets().empty()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supported presets:");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supported presets:");
 | 
			
		||||
    for (ClimatePreset p : traits.get_supported_presets())
 | 
			
		||||
      ESP_LOGCONFIG(tag, "  - %s", LOG_STR_ARG(climate_preset_to_string(p)));
 | 
			
		||||
      ESP_LOGCONFIG(tag, "      - %s", LOG_STR_ARG(climate_preset_to_string(p)));
 | 
			
		||||
  }
 | 
			
		||||
  if (!traits.get_supported_custom_presets().empty()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supported custom presets:");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supported custom presets:");
 | 
			
		||||
    for (const std::string &s : traits.get_supported_custom_presets())
 | 
			
		||||
      ESP_LOGCONFIG(tag, "  - %s", s.c_str());
 | 
			
		||||
      ESP_LOGCONFIG(tag, "      - %s", s.c_str());
 | 
			
		||||
  }
 | 
			
		||||
  if (!traits.get_supported_swing_modes().empty()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supported swing modes:");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supported swing modes:");
 | 
			
		||||
    for (ClimateSwingMode m : traits.get_supported_swing_modes())
 | 
			
		||||
      ESP_LOGCONFIG(tag, "  - %s", LOG_STR_ARG(climate_swing_mode_to_string(m)));
 | 
			
		||||
      ESP_LOGCONFIG(tag, "      - %s", LOG_STR_ARG(climate_swing_mode_to_string(m)));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,6 @@ class Climate;
 | 
			
		||||
class ClimateCall {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit ClimateCall(Climate *parent) : parent_(parent) {}
 | 
			
		||||
  friend struct ClimateDeviceRestoreState;
 | 
			
		||||
 | 
			
		||||
  /// Set the mode of the climate device.
 | 
			
		||||
  ClimateCall &set_mode(ClimateMode mode);
 | 
			
		||||
@@ -94,31 +93,30 @@ class ClimateCall {
 | 
			
		||||
 | 
			
		||||
  void perform();
 | 
			
		||||
 | 
			
		||||
  const optional<ClimateMode> &get_mode() const;
 | 
			
		||||
  const optional<float> &get_target_temperature() const;
 | 
			
		||||
  const optional<float> &get_target_temperature_low() const;
 | 
			
		||||
  const optional<float> &get_target_temperature_high() const;
 | 
			
		||||
  const optional<float> &get_target_humidity() const;
 | 
			
		||||
 | 
			
		||||
  const optional<ClimateMode> &get_mode() const;
 | 
			
		||||
  const optional<ClimateFanMode> &get_fan_mode() const;
 | 
			
		||||
  const optional<ClimateSwingMode> &get_swing_mode() const;
 | 
			
		||||
  const optional<ClimatePreset> &get_preset() const;
 | 
			
		||||
  const optional<std::string> &get_custom_fan_mode() const;
 | 
			
		||||
  const optional<ClimatePreset> &get_preset() const;
 | 
			
		||||
  const optional<std::string> &get_custom_preset() const;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void validate_();
 | 
			
		||||
 | 
			
		||||
  Climate *const parent_;
 | 
			
		||||
  optional<ClimateMode> mode_;
 | 
			
		||||
  optional<float> target_temperature_;
 | 
			
		||||
  optional<float> target_temperature_low_;
 | 
			
		||||
  optional<float> target_temperature_high_;
 | 
			
		||||
  optional<float> target_humidity_;
 | 
			
		||||
  optional<ClimateMode> mode_;
 | 
			
		||||
  optional<ClimateFanMode> fan_mode_;
 | 
			
		||||
  optional<ClimateSwingMode> swing_mode_;
 | 
			
		||||
  optional<ClimatePreset> preset_;
 | 
			
		||||
  optional<std::string> custom_fan_mode_;
 | 
			
		||||
  optional<ClimatePreset> preset_;
 | 
			
		||||
  optional<std::string> custom_preset_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -171,6 +169,47 @@ class Climate : public EntityBase {
 | 
			
		||||
 public:
 | 
			
		||||
  Climate() {}
 | 
			
		||||
 | 
			
		||||
  /// The active mode of the climate device.
 | 
			
		||||
  ClimateMode mode{CLIMATE_MODE_OFF};
 | 
			
		||||
 | 
			
		||||
  /// The active state of the climate device.
 | 
			
		||||
  ClimateAction action{CLIMATE_ACTION_OFF};
 | 
			
		||||
 | 
			
		||||
  /// The current temperature of the climate device, as reported from the integration.
 | 
			
		||||
  float current_temperature{NAN};
 | 
			
		||||
 | 
			
		||||
  /// The current humidity of the climate device, as reported from the integration.
 | 
			
		||||
  float current_humidity{NAN};
 | 
			
		||||
 | 
			
		||||
  union {
 | 
			
		||||
    /// The target temperature of the climate device.
 | 
			
		||||
    float target_temperature;
 | 
			
		||||
    struct {
 | 
			
		||||
      /// The minimum target temperature of the climate device, for climate devices with split target temperature.
 | 
			
		||||
      float target_temperature_low{NAN};
 | 
			
		||||
      /// The maximum target temperature of the climate device, for climate devices with split target temperature.
 | 
			
		||||
      float target_temperature_high{NAN};
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /// The target humidity of the climate device.
 | 
			
		||||
  float target_humidity;
 | 
			
		||||
 | 
			
		||||
  /// The active fan mode of the climate device.
 | 
			
		||||
  optional<ClimateFanMode> fan_mode;
 | 
			
		||||
 | 
			
		||||
  /// The active swing mode of the climate device.
 | 
			
		||||
  ClimateSwingMode swing_mode;
 | 
			
		||||
 | 
			
		||||
  /// The active custom fan mode of the climate device.
 | 
			
		||||
  optional<std::string> custom_fan_mode;
 | 
			
		||||
 | 
			
		||||
  /// The active preset of the climate device.
 | 
			
		||||
  optional<ClimatePreset> preset;
 | 
			
		||||
 | 
			
		||||
  /// The active custom preset mode of the climate device.
 | 
			
		||||
  optional<std::string> custom_preset;
 | 
			
		||||
 | 
			
		||||
  /** Add a callback for the climate device state, each time the state of the climate device is updated
 | 
			
		||||
   * (using publish_state), this callback will be called.
 | 
			
		||||
   *
 | 
			
		||||
@@ -212,47 +251,6 @@ class Climate : public EntityBase {
 | 
			
		||||
  void set_visual_min_humidity_override(float visual_min_humidity_override);
 | 
			
		||||
  void set_visual_max_humidity_override(float visual_max_humidity_override);
 | 
			
		||||
 | 
			
		||||
  /// The current temperature of the climate device, as reported from the integration.
 | 
			
		||||
  float current_temperature{NAN};
 | 
			
		||||
 | 
			
		||||
  /// The current humidity of the climate device, as reported from the integration.
 | 
			
		||||
  float current_humidity{NAN};
 | 
			
		||||
 | 
			
		||||
  union {
 | 
			
		||||
    /// The target temperature of the climate device.
 | 
			
		||||
    float target_temperature;
 | 
			
		||||
    struct {
 | 
			
		||||
      /// The minimum target temperature of the climate device, for climate devices with split target temperature.
 | 
			
		||||
      float target_temperature_low{NAN};
 | 
			
		||||
      /// The maximum target temperature of the climate device, for climate devices with split target temperature.
 | 
			
		||||
      float target_temperature_high{NAN};
 | 
			
		||||
    };
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  /// The target humidity of the climate device.
 | 
			
		||||
  float target_humidity;
 | 
			
		||||
 | 
			
		||||
  /// The active fan mode of the climate device.
 | 
			
		||||
  optional<ClimateFanMode> fan_mode;
 | 
			
		||||
 | 
			
		||||
  /// The active preset of the climate device.
 | 
			
		||||
  optional<ClimatePreset> preset;
 | 
			
		||||
 | 
			
		||||
  /// The active custom fan mode of the climate device.
 | 
			
		||||
  optional<std::string> custom_fan_mode;
 | 
			
		||||
 | 
			
		||||
  /// The active custom preset mode of the climate device.
 | 
			
		||||
  optional<std::string> custom_preset;
 | 
			
		||||
 | 
			
		||||
  /// The active mode of the climate device.
 | 
			
		||||
  ClimateMode mode{CLIMATE_MODE_OFF};
 | 
			
		||||
 | 
			
		||||
  /// The active state of the climate device.
 | 
			
		||||
  ClimateAction action{CLIMATE_ACTION_OFF};
 | 
			
		||||
 | 
			
		||||
  /// The active swing mode of the climate device.
 | 
			
		||||
  ClimateSwingMode swing_mode{CLIMATE_SWING_OFF};
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  friend ClimateCall;
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ namespace esphome {
 | 
			
		||||
namespace climate {
 | 
			
		||||
 | 
			
		||||
/// Enum for all modes a climate device can be in.
 | 
			
		||||
/// NOTE: If adding values, update ClimateModeMask in climate_traits.h to use the new last value
 | 
			
		||||
enum ClimateMode : uint8_t {
 | 
			
		||||
  /// The climate device is off
 | 
			
		||||
  CLIMATE_MODE_OFF = 0,
 | 
			
		||||
@@ -25,7 +24,7 @@ enum ClimateMode : uint8_t {
 | 
			
		||||
   * For example, the target temperature can be adjusted based on a schedule, or learned behavior.
 | 
			
		||||
   * The target temperature can't be adjusted when in this mode.
 | 
			
		||||
   */
 | 
			
		||||
  CLIMATE_MODE_AUTO = 6  // Update ClimateModeMask in climate_traits.h if adding values after this
 | 
			
		||||
  CLIMATE_MODE_AUTO = 6
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Enum for the current action of the climate device. Values match those of ClimateMode.
 | 
			
		||||
@@ -44,7 +43,6 @@ enum ClimateAction : uint8_t {
 | 
			
		||||
  CLIMATE_ACTION_FAN = 6,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// NOTE: If adding values, update ClimateFanModeMask in climate_traits.h to use the new last value
 | 
			
		||||
enum ClimateFanMode : uint8_t {
 | 
			
		||||
  /// The fan mode is set to On
 | 
			
		||||
  CLIMATE_FAN_ON = 0,
 | 
			
		||||
@@ -65,11 +63,10 @@ enum ClimateFanMode : uint8_t {
 | 
			
		||||
  /// The fan mode is set to Diffuse
 | 
			
		||||
  CLIMATE_FAN_DIFFUSE = 8,
 | 
			
		||||
  /// The fan mode is set to Quiet
 | 
			
		||||
  CLIMATE_FAN_QUIET = 9,  // Update ClimateFanModeMask in climate_traits.h if adding values after this
 | 
			
		||||
  CLIMATE_FAN_QUIET = 9,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Enum for all modes a climate swing can be in
 | 
			
		||||
/// NOTE: If adding values, update ClimateSwingModeMask in climate_traits.h to use the new last value
 | 
			
		||||
enum ClimateSwingMode : uint8_t {
 | 
			
		||||
  /// The swing mode is set to Off
 | 
			
		||||
  CLIMATE_SWING_OFF = 0,
 | 
			
		||||
@@ -78,11 +75,10 @@ enum ClimateSwingMode : uint8_t {
 | 
			
		||||
  /// The fan mode is set to Vertical
 | 
			
		||||
  CLIMATE_SWING_VERTICAL = 2,
 | 
			
		||||
  /// The fan mode is set to Horizontal
 | 
			
		||||
  CLIMATE_SWING_HORIZONTAL = 3,  // Update ClimateSwingModeMask in climate_traits.h if adding values after this
 | 
			
		||||
  CLIMATE_SWING_HORIZONTAL = 3,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Enum for all preset modes
 | 
			
		||||
/// NOTE: If adding values, update ClimatePresetMask in climate_traits.h to use the new last value
 | 
			
		||||
enum ClimatePreset : uint8_t {
 | 
			
		||||
  /// No preset is active
 | 
			
		||||
  CLIMATE_PRESET_NONE = 0,
 | 
			
		||||
@@ -99,7 +95,7 @@ enum ClimatePreset : uint8_t {
 | 
			
		||||
  /// Device is prepared for sleep
 | 
			
		||||
  CLIMATE_PRESET_SLEEP = 6,
 | 
			
		||||
  /// Device is reacting to activity (e.g., movement sensors)
 | 
			
		||||
  CLIMATE_PRESET_ACTIVITY = 7,  // Update ClimatePresetMask in climate_traits.h if adding values after this
 | 
			
		||||
  CLIMATE_PRESET_ACTIVITY = 7,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum ClimateFeature : uint32_t {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,33 +1,19 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include "climate_mode.h"
 | 
			
		||||
#include "esphome/core/finite_set_mask.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "climate_mode.h"
 | 
			
		||||
#include <set>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API
 | 
			
		||||
namespace api {
 | 
			
		||||
class APIConnection;
 | 
			
		||||
}  // namespace api
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
namespace climate {
 | 
			
		||||
 | 
			
		||||
// Type aliases for climate enum bitmasks
 | 
			
		||||
// These replace std::set<EnumType> to eliminate red-black tree overhead
 | 
			
		||||
// For contiguous enums starting at 0, DefaultBitPolicy provides 1:1 mapping (enum value = bit position)
 | 
			
		||||
// Bitmask size is automatically calculated from the last enum value
 | 
			
		||||
using ClimateModeMask = FiniteSetMask<ClimateMode, DefaultBitPolicy<ClimateMode, CLIMATE_MODE_AUTO + 1>>;
 | 
			
		||||
using ClimateFanModeMask = FiniteSetMask<ClimateFanMode, DefaultBitPolicy<ClimateFanMode, CLIMATE_FAN_QUIET + 1>>;
 | 
			
		||||
using ClimateSwingModeMask =
 | 
			
		||||
    FiniteSetMask<ClimateSwingMode, DefaultBitPolicy<ClimateSwingMode, CLIMATE_SWING_HORIZONTAL + 1>>;
 | 
			
		||||
using ClimatePresetMask = FiniteSetMask<ClimatePreset, DefaultBitPolicy<ClimatePreset, CLIMATE_PRESET_ACTIVITY + 1>>;
 | 
			
		||||
 | 
			
		||||
// Lightweight linear search for small vectors (1-20 items)
 | 
			
		||||
// Avoids std::find template overhead
 | 
			
		||||
template<typename T> inline bool vector_contains(const std::vector<T> &vec, const T &value) {
 | 
			
		||||
  for (const auto &item : vec) {
 | 
			
		||||
    if (item == value)
 | 
			
		||||
      return true;
 | 
			
		||||
  }
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** This class contains all static data for climate devices.
 | 
			
		||||
 *
 | 
			
		||||
 * All climate devices must support these features:
 | 
			
		||||
@@ -121,60 +107,90 @@ class ClimateTraits {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; }
 | 
			
		||||
  void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
 | 
			
		||||
  void add_supported_mode(ClimateMode mode) { this->supported_modes_.insert(mode); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_auto_mode(bool supports_auto_mode) { set_mode_support_(CLIMATE_MODE_AUTO, supports_auto_mode); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_cool_mode(bool supports_cool_mode) { set_mode_support_(CLIMATE_MODE_COOL, supports_cool_mode); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_heat_mode(bool supports_heat_mode) { set_mode_support_(CLIMATE_MODE_HEAT, supports_heat_mode); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_heat_cool_mode(bool supported) { set_mode_support_(CLIMATE_MODE_HEAT_COOL, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_only_mode(bool supports_fan_only_mode) {
 | 
			
		||||
    set_mode_support_(CLIMATE_MODE_FAN_ONLY, supports_fan_only_mode);
 | 
			
		||||
  }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_dry_mode(bool supports_dry_mode) { set_mode_support_(CLIMATE_MODE_DRY, supports_dry_mode); }
 | 
			
		||||
  bool supports_mode(ClimateMode mode) const { return this->supported_modes_.count(mode); }
 | 
			
		||||
  const ClimateModeMask &get_supported_modes() const { return this->supported_modes_; }
 | 
			
		||||
  const std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; }
 | 
			
		||||
 | 
			
		||||
  void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = modes; }
 | 
			
		||||
  void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); }
 | 
			
		||||
  void add_supported_fan_mode(ClimateFanMode mode) { this->supported_fan_modes_.insert(mode); }
 | 
			
		||||
  void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.push_back(mode); }
 | 
			
		||||
  void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.insert(mode); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_mode_on(bool supported) { set_fan_mode_support_(CLIMATE_FAN_ON, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_mode_off(bool supported) { set_fan_mode_support_(CLIMATE_FAN_OFF, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_mode_auto(bool supported) { set_fan_mode_support_(CLIMATE_FAN_AUTO, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_mode_low(bool supported) { set_fan_mode_support_(CLIMATE_FAN_LOW, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_mode_medium(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MEDIUM, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_mode_high(bool supported) { set_fan_mode_support_(CLIMATE_FAN_HIGH, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_mode_middle(bool supported) { set_fan_mode_support_(CLIMATE_FAN_MIDDLE, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_mode_focus(bool supported) { set_fan_mode_support_(CLIMATE_FAN_FOCUS, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_fan_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_fan_mode_diffuse(bool supported) { set_fan_mode_support_(CLIMATE_FAN_DIFFUSE, supported); }
 | 
			
		||||
  bool supports_fan_mode(ClimateFanMode fan_mode) const { return this->supported_fan_modes_.count(fan_mode); }
 | 
			
		||||
  bool get_supports_fan_modes() const {
 | 
			
		||||
    return !this->supported_fan_modes_.empty() || !this->supported_custom_fan_modes_.empty();
 | 
			
		||||
  }
 | 
			
		||||
  const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; }
 | 
			
		||||
  const std::set<ClimateFanMode> &get_supported_fan_modes() const { return this->supported_fan_modes_; }
 | 
			
		||||
 | 
			
		||||
  void set_supported_custom_fan_modes(std::vector<std::string> supported_custom_fan_modes) {
 | 
			
		||||
  void set_supported_custom_fan_modes(std::set<std::string> supported_custom_fan_modes) {
 | 
			
		||||
    this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes);
 | 
			
		||||
  }
 | 
			
		||||
  void set_supported_custom_fan_modes(std::initializer_list<std::string> modes) {
 | 
			
		||||
    this->supported_custom_fan_modes_ = modes;
 | 
			
		||||
  }
 | 
			
		||||
  template<size_t N> void set_supported_custom_fan_modes(const char *const (&modes)[N]) {
 | 
			
		||||
    this->supported_custom_fan_modes_.assign(modes, modes + N);
 | 
			
		||||
  }
 | 
			
		||||
  const std::vector<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
 | 
			
		||||
  const std::set<std::string> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; }
 | 
			
		||||
  bool supports_custom_fan_mode(const std::string &custom_fan_mode) const {
 | 
			
		||||
    return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode);
 | 
			
		||||
    return this->supported_custom_fan_modes_.count(custom_fan_mode);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; }
 | 
			
		||||
  void set_supported_presets(std::set<ClimatePreset> presets) { this->supported_presets_ = std::move(presets); }
 | 
			
		||||
  void add_supported_preset(ClimatePreset preset) { this->supported_presets_.insert(preset); }
 | 
			
		||||
  void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.push_back(preset); }
 | 
			
		||||
  void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.insert(preset); }
 | 
			
		||||
  bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); }
 | 
			
		||||
  bool get_supports_presets() const { return !this->supported_presets_.empty(); }
 | 
			
		||||
  const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; }
 | 
			
		||||
  const std::set<climate::ClimatePreset> &get_supported_presets() const { return this->supported_presets_; }
 | 
			
		||||
 | 
			
		||||
  void set_supported_custom_presets(std::vector<std::string> supported_custom_presets) {
 | 
			
		||||
  void set_supported_custom_presets(std::set<std::string> supported_custom_presets) {
 | 
			
		||||
    this->supported_custom_presets_ = std::move(supported_custom_presets);
 | 
			
		||||
  }
 | 
			
		||||
  void set_supported_custom_presets(std::initializer_list<std::string> presets) {
 | 
			
		||||
    this->supported_custom_presets_ = presets;
 | 
			
		||||
  }
 | 
			
		||||
  template<size_t N> void set_supported_custom_presets(const char *const (&presets)[N]) {
 | 
			
		||||
    this->supported_custom_presets_.assign(presets, presets + N);
 | 
			
		||||
  }
 | 
			
		||||
  const std::vector<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
 | 
			
		||||
  const std::set<std::string> &get_supported_custom_presets() const { return this->supported_custom_presets_; }
 | 
			
		||||
  bool supports_custom_preset(const std::string &custom_preset) const {
 | 
			
		||||
    return vector_contains(this->supported_custom_presets_, custom_preset);
 | 
			
		||||
    return this->supported_custom_presets_.count(custom_preset);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; }
 | 
			
		||||
  void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); }
 | 
			
		||||
  void add_supported_swing_mode(ClimateSwingMode mode) { this->supported_swing_modes_.insert(mode); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_swing_mode_off(bool supported) { set_swing_mode_support_(CLIMATE_SWING_OFF, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_swing_mode_both(bool supported) { set_swing_mode_support_(CLIMATE_SWING_BOTH, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_swing_mode_vertical(bool supported) { set_swing_mode_support_(CLIMATE_SWING_VERTICAL, supported); }
 | 
			
		||||
  ESPDEPRECATED("This method is deprecated, use set_supported_swing_modes() instead", "v1.20")
 | 
			
		||||
  void set_supports_swing_mode_horizontal(bool supported) {
 | 
			
		||||
    set_swing_mode_support_(CLIMATE_SWING_HORIZONTAL, supported);
 | 
			
		||||
  }
 | 
			
		||||
  bool supports_swing_mode(ClimateSwingMode swing_mode) const { return this->supported_swing_modes_.count(swing_mode); }
 | 
			
		||||
  bool get_supports_swing_modes() const { return !this->supported_swing_modes_.empty(); }
 | 
			
		||||
  const ClimateSwingModeMask &get_supported_swing_modes() const { return this->supported_swing_modes_; }
 | 
			
		||||
  const std::set<ClimateSwingMode> &get_supported_swing_modes() const { return this->supported_swing_modes_; }
 | 
			
		||||
 | 
			
		||||
  float get_visual_min_temperature() const { return this->visual_min_temperature_; }
 | 
			
		||||
  void set_visual_min_temperature(float visual_min_temperature) {
 | 
			
		||||
@@ -205,6 +221,23 @@ class ClimateTraits {
 | 
			
		||||
  void set_visual_max_humidity(float visual_max_humidity) { this->visual_max_humidity_ = visual_max_humidity; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
#ifdef USE_API
 | 
			
		||||
  // The API connection is a friend class to access internal methods
 | 
			
		||||
  friend class api::APIConnection;
 | 
			
		||||
  // These methods return references to internal data structures.
 | 
			
		||||
  // They are used by the API to avoid copying data when encoding messages.
 | 
			
		||||
  // Warning: Do not use these methods outside of the API connection code.
 | 
			
		||||
  // They return references to internal data that can be invalidated.
 | 
			
		||||
  const std::set<ClimateMode> &get_supported_modes_for_api_() const { return this->supported_modes_; }
 | 
			
		||||
  const std::set<ClimateFanMode> &get_supported_fan_modes_for_api_() const { return this->supported_fan_modes_; }
 | 
			
		||||
  const std::set<std::string> &get_supported_custom_fan_modes_for_api_() const {
 | 
			
		||||
    return this->supported_custom_fan_modes_;
 | 
			
		||||
  }
 | 
			
		||||
  const std::set<climate::ClimatePreset> &get_supported_presets_for_api_() const { return this->supported_presets_; }
 | 
			
		||||
  const std::set<std::string> &get_supported_custom_presets_for_api_() const { return this->supported_custom_presets_; }
 | 
			
		||||
  const std::set<ClimateSwingMode> &get_supported_swing_modes_for_api_() const { return this->supported_swing_modes_; }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  void set_mode_support_(climate::ClimateMode mode, bool supported) {
 | 
			
		||||
    if (supported) {
 | 
			
		||||
      this->supported_modes_.insert(mode);
 | 
			
		||||
@@ -235,12 +268,12 @@ class ClimateTraits {
 | 
			
		||||
  float visual_min_humidity_{30};
 | 
			
		||||
  float visual_max_humidity_{99};
 | 
			
		||||
 | 
			
		||||
  climate::ClimateModeMask supported_modes_{climate::CLIMATE_MODE_OFF};
 | 
			
		||||
  climate::ClimateFanModeMask supported_fan_modes_;
 | 
			
		||||
  climate::ClimateSwingModeMask supported_swing_modes_;
 | 
			
		||||
  climate::ClimatePresetMask supported_presets_;
 | 
			
		||||
  std::vector<std::string> supported_custom_fan_modes_;
 | 
			
		||||
  std::vector<std::string> supported_custom_presets_;
 | 
			
		||||
  std::set<climate::ClimateMode> supported_modes_ = {climate::CLIMATE_MODE_OFF};
 | 
			
		||||
  std::set<climate::ClimateFanMode> supported_fan_modes_;
 | 
			
		||||
  std::set<climate::ClimateSwingMode> supported_swing_modes_;
 | 
			
		||||
  std::set<climate::ClimatePreset> supported_presets_;
 | 
			
		||||
  std::set<std::string> supported_custom_fan_modes_;
 | 
			
		||||
  std::set<std::string> supported_custom_presets_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace climate
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,10 @@
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from esphome import core
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import climate, remote_base, sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
 | 
			
		||||
from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
 | 
			
		||||
from esphome.cpp_generator import MockObjClass
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
@@ -51,6 +52,26 @@ def climate_ir_with_receiver_schema(
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Remove before 2025.11.0
 | 
			
		||||
def deprecated_schema_constant(config):
 | 
			
		||||
    type: str = "unknown"
 | 
			
		||||
    if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID):
 | 
			
		||||
        type = str(id.type).split("::", maxsplit=1)[0]
 | 
			
		||||
    _LOGGER.warning(
 | 
			
		||||
        "Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. "
 | 
			
		||||
        "Please use `climate_ir.climate_ir_with_receiver_schema(...)` instead. "
 | 
			
		||||
        "If you are seeing this, report an issue to the external_component author and ask them to update it. "
 | 
			
		||||
        "https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. "
 | 
			
		||||
        "Component using this schema: %s",
 | 
			
		||||
        type,
 | 
			
		||||
    )
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CLIMATE_IR_WITH_RECEIVER_SCHEMA = climate_ir_with_receiver_schema(ClimateIR)
 | 
			
		||||
CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def register_climate_ir(var, config):
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await remote_base.register_transmittable(var, config)
 | 
			
		||||
 
 | 
			
		||||
@@ -8,10 +8,7 @@ static const char *const TAG = "climate_ir";
 | 
			
		||||
 | 
			
		||||
climate::ClimateTraits ClimateIR::traits() {
 | 
			
		||||
  auto traits = climate::ClimateTraits();
 | 
			
		||||
  if (this->sensor_ != nullptr) {
 | 
			
		||||
    traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  traits.set_supports_current_temperature(this->sensor_ != nullptr);
 | 
			
		||||
  traits.set_supported_modes({climate::CLIMATE_MODE_OFF, climate::CLIMATE_MODE_HEAT_COOL});
 | 
			
		||||
  if (this->supports_cool_)
 | 
			
		||||
    traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
 | 
			
		||||
@@ -22,6 +19,7 @@ climate::ClimateTraits ClimateIR::traits() {
 | 
			
		||||
  if (this->supports_fan_only_)
 | 
			
		||||
    traits.add_supported_mode(climate::CLIMATE_MODE_FAN_ONLY);
 | 
			
		||||
 | 
			
		||||
  traits.set_supports_two_point_target_temperature(false);
 | 
			
		||||
  traits.set_visual_min_temperature(this->minimum_temperature_);
 | 
			
		||||
  traits.set_visual_max_temperature(this->maximum_temperature_);
 | 
			
		||||
  traits.set_visual_temperature_step(this->temperature_step_);
 | 
			
		||||
 
 | 
			
		||||
@@ -24,18 +24,16 @@ class ClimateIR : public Component,
 | 
			
		||||
                  public remote_base::RemoteTransmittable {
 | 
			
		||||
 public:
 | 
			
		||||
  ClimateIR(float minimum_temperature, float maximum_temperature, float temperature_step = 1.0f,
 | 
			
		||||
            bool supports_dry = false, bool supports_fan_only = false,
 | 
			
		||||
            climate::ClimateFanModeMask fan_modes = climate::ClimateFanModeMask(),
 | 
			
		||||
            climate::ClimateSwingModeMask swing_modes = climate::ClimateSwingModeMask(),
 | 
			
		||||
            climate::ClimatePresetMask presets = climate::ClimatePresetMask()) {
 | 
			
		||||
            bool supports_dry = false, bool supports_fan_only = false, std::set<climate::ClimateFanMode> fan_modes = {},
 | 
			
		||||
            std::set<climate::ClimateSwingMode> swing_modes = {}, std::set<climate::ClimatePreset> presets = {}) {
 | 
			
		||||
    this->minimum_temperature_ = minimum_temperature;
 | 
			
		||||
    this->maximum_temperature_ = maximum_temperature;
 | 
			
		||||
    this->temperature_step_ = temperature_step;
 | 
			
		||||
    this->supports_dry_ = supports_dry;
 | 
			
		||||
    this->supports_fan_only_ = supports_fan_only;
 | 
			
		||||
    this->fan_modes_ = fan_modes;
 | 
			
		||||
    this->swing_modes_ = swing_modes;
 | 
			
		||||
    this->presets_ = presets;
 | 
			
		||||
    this->fan_modes_ = std::move(fan_modes);
 | 
			
		||||
    this->swing_modes_ = std::move(swing_modes);
 | 
			
		||||
    this->presets_ = std::move(presets);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void setup() override;
 | 
			
		||||
@@ -62,9 +60,9 @@ class ClimateIR : public Component,
 | 
			
		||||
  bool supports_heat_{true};
 | 
			
		||||
  bool supports_dry_{false};
 | 
			
		||||
  bool supports_fan_only_{false};
 | 
			
		||||
  climate::ClimateFanModeMask fan_modes_{};
 | 
			
		||||
  climate::ClimateSwingModeMask swing_modes_{};
 | 
			
		||||
  climate::ClimatePresetMask presets_{};
 | 
			
		||||
  std::set<climate::ClimateFanMode> fan_modes_ = {};
 | 
			
		||||
  std::set<climate::ClimateSwingMode> swing_modes_ = {};
 | 
			
		||||
  std::set<climate::ClimatePreset> presets_ = {};
 | 
			
		||||
 | 
			
		||||
  sensor::Sensor *sensor_{nullptr};
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@ void CopyFan::setup() {
 | 
			
		||||
    this->oscillating = source_->oscillating;
 | 
			
		||||
    this->speed = source_->speed;
 | 
			
		||||
    this->direction = source_->direction;
 | 
			
		||||
    this->set_preset_mode_(source_->get_preset_mode());
 | 
			
		||||
    this->preset_mode = source_->preset_mode;
 | 
			
		||||
    this->publish_state();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
@@ -20,7 +20,7 @@ void CopyFan::setup() {
 | 
			
		||||
  this->oscillating = source_->oscillating;
 | 
			
		||||
  this->speed = source_->speed;
 | 
			
		||||
  this->direction = source_->direction;
 | 
			
		||||
  this->set_preset_mode_(source_->get_preset_mode());
 | 
			
		||||
  this->preset_mode = source_->preset_mode;
 | 
			
		||||
  this->publish_state();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -49,7 +49,7 @@ void CopyFan::control(const fan::FanCall &call) {
 | 
			
		||||
    call2.set_speed(*call.get_speed());
 | 
			
		||||
  if (call.get_direction().has_value())
 | 
			
		||||
    call2.set_direction(*call.get_direction());
 | 
			
		||||
  if (call.has_preset_mode())
 | 
			
		||||
  if (!call.get_preset_mode().empty())
 | 
			
		||||
    call2.set_preset_mode(call.get_preset_mode());
 | 
			
		||||
  call2.perform();
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -151,6 +151,11 @@ def cover_schema(
 | 
			
		||||
    return _COVER_SCHEMA.extend(schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Remove before 2025.11.0
 | 
			
		||||
COVER_SCHEMA = cover_schema(Cover)
 | 
			
		||||
COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def setup_cover_core_(var, config):
 | 
			
		||||
    await setup_entity(var, config, "cover")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
#include "cover.h"
 | 
			
		||||
#include <strings.h>
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include <strings.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace cover {
 | 
			
		||||
@@ -144,7 +144,21 @@ CoverCall &CoverCall::set_stop(bool stop) {
 | 
			
		||||
bool CoverCall::get_stop() const { return this->stop_; }
 | 
			
		||||
 | 
			
		||||
CoverCall Cover::make_call() { return {this}; }
 | 
			
		||||
 | 
			
		||||
void Cover::open() {
 | 
			
		||||
  auto call = this->make_call();
 | 
			
		||||
  call.set_command_open();
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
void Cover::close() {
 | 
			
		||||
  auto call = this->make_call();
 | 
			
		||||
  call.set_command_close();
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
void Cover::stop() {
 | 
			
		||||
  auto call = this->make_call();
 | 
			
		||||
  call.set_command_stop();
 | 
			
		||||
  call.perform();
 | 
			
		||||
}
 | 
			
		||||
void Cover::add_on_state_callback(std::function<void()> &&f) { this->state_callback_.add(std::move(f)); }
 | 
			
		||||
void Cover::publish_state(bool save) {
 | 
			
		||||
  this->position = clamp(this->position, 0.0f, 1.0f);
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
#include "esphome/core/entity_base.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/preferences.h"
 | 
			
		||||
 | 
			
		||||
#include "cover_traits.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -126,6 +125,25 @@ class Cover : public EntityBase, public EntityBase_DeviceClass {
 | 
			
		||||
 | 
			
		||||
  /// Construct a new cover call used to control the cover.
 | 
			
		||||
  CoverCall make_call();
 | 
			
		||||
  /** Open the cover.
 | 
			
		||||
   *
 | 
			
		||||
   * This is a legacy method and may be removed later, please use `.make_call()` instead.
 | 
			
		||||
   */
 | 
			
		||||
  ESPDEPRECATED("open() is deprecated, use make_call().set_command_open().perform() instead.", "2021.9")
 | 
			
		||||
  void open();
 | 
			
		||||
  /** Close the cover.
 | 
			
		||||
   *
 | 
			
		||||
   * This is a legacy method and may be removed later, please use `.make_call()` instead.
 | 
			
		||||
   */
 | 
			
		||||
  ESPDEPRECATED("close() is deprecated, use make_call().set_command_close().perform() instead.", "2021.9")
 | 
			
		||||
  void close();
 | 
			
		||||
  /** Stop the cover.
 | 
			
		||||
   *
 | 
			
		||||
   * This is a legacy method and may be removed later, please use `.make_call()` instead.
 | 
			
		||||
   * As per solution from issue #2885 the call should include perform()
 | 
			
		||||
   */
 | 
			
		||||
  ESPDEPRECATED("stop() is deprecated, use make_call().set_command_stop().perform() instead.", "2021.9")
 | 
			
		||||
  void stop();
 | 
			
		||||
 | 
			
		||||
  void add_on_state_callback(std::function<void()> &&f);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -241,7 +241,9 @@ uint8_t DaikinArcClimate::humidity_() {
 | 
			
		||||
 | 
			
		||||
climate::ClimateTraits DaikinArcClimate::traits() {
 | 
			
		||||
  climate::ClimateTraits traits = climate_ir::ClimateIR::traits();
 | 
			
		||||
  traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY);
 | 
			
		||||
  traits.set_supports_current_temperature(true);
 | 
			
		||||
  traits.set_supports_current_humidity(false);
 | 
			
		||||
  traits.set_supports_target_humidity(true);
 | 
			
		||||
  traits.set_visual_min_humidity(38);
 | 
			
		||||
  traits.set_visual_max_humidity(52);
 | 
			
		||||
  return traits;
 | 
			
		||||
 
 | 
			
		||||
@@ -82,14 +82,16 @@ class DemoClimate : public climate::Climate, public Component {
 | 
			
		||||
    climate::ClimateTraits traits{};
 | 
			
		||||
    switch (type_) {
 | 
			
		||||
      case DemoClimateType::TYPE_1:
 | 
			
		||||
        traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION);
 | 
			
		||||
        traits.set_supports_current_temperature(true);
 | 
			
		||||
        traits.set_supported_modes({
 | 
			
		||||
            climate::CLIMATE_MODE_OFF,
 | 
			
		||||
            climate::CLIMATE_MODE_HEAT,
 | 
			
		||||
        });
 | 
			
		||||
        traits.set_supports_action(true);
 | 
			
		||||
        traits.set_visual_temperature_step(0.5);
 | 
			
		||||
        break;
 | 
			
		||||
      case DemoClimateType::TYPE_2:
 | 
			
		||||
        traits.set_supports_current_temperature(false);
 | 
			
		||||
        traits.set_supported_modes({
 | 
			
		||||
            climate::CLIMATE_MODE_OFF,
 | 
			
		||||
            climate::CLIMATE_MODE_HEAT,
 | 
			
		||||
@@ -98,7 +100,7 @@ class DemoClimate : public climate::Climate, public Component {
 | 
			
		||||
            climate::CLIMATE_MODE_DRY,
 | 
			
		||||
            climate::CLIMATE_MODE_FAN_ONLY,
 | 
			
		||||
        });
 | 
			
		||||
        traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
 | 
			
		||||
        traits.set_supports_action(true);
 | 
			
		||||
        traits.set_supported_fan_modes({
 | 
			
		||||
            climate::CLIMATE_FAN_ON,
 | 
			
		||||
            climate::CLIMATE_FAN_OFF,
 | 
			
		||||
@@ -121,8 +123,8 @@ class DemoClimate : public climate::Climate, public Component {
 | 
			
		||||
        traits.set_supported_custom_presets({"My Preset"});
 | 
			
		||||
        break;
 | 
			
		||||
      case DemoClimateType::TYPE_3:
 | 
			
		||||
        traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE |
 | 
			
		||||
                                 climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE);
 | 
			
		||||
        traits.set_supports_current_temperature(true);
 | 
			
		||||
        traits.set_supports_two_point_target_temperature(true);
 | 
			
		||||
        traits.set_supported_modes({
 | 
			
		||||
            climate::CLIMATE_MODE_OFF,
 | 
			
		||||
            climate::CLIMATE_MODE_COOL,
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,6 @@
 | 
			
		||||
#include "e131_addressable_light_effect.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace e131 {
 | 
			
		||||
 | 
			
		||||
@@ -78,14 +76,14 @@ void E131Component::loop() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
 | 
			
		||||
  if (std::find(light_effects_.begin(), light_effects_.end(), light_effect) != light_effects_.end()) {
 | 
			
		||||
  if (light_effects_.count(light_effect)) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
 | 
			
		||||
           light_effect->get_last_universe());
 | 
			
		||||
  ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name().c_str(),
 | 
			
		||||
           light_effect->get_first_universe(), light_effect->get_last_universe());
 | 
			
		||||
 | 
			
		||||
  light_effects_.push_back(light_effect);
 | 
			
		||||
  light_effects_.insert(light_effect);
 | 
			
		||||
 | 
			
		||||
  for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
 | 
			
		||||
    join_(universe);
 | 
			
		||||
@@ -93,17 +91,14 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void E131Component::remove_effect(E131AddressableLightEffect *light_effect) {
 | 
			
		||||
  auto it = std::find(light_effects_.begin(), light_effects_.end(), light_effect);
 | 
			
		||||
  if (it == light_effects_.end()) {
 | 
			
		||||
  if (!light_effects_.count(light_effect)) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
 | 
			
		||||
           light_effect->get_last_universe());
 | 
			
		||||
  ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name().c_str(),
 | 
			
		||||
           light_effect->get_first_universe(), light_effect->get_last_universe());
 | 
			
		||||
 | 
			
		||||
  // Swap with last element and pop for O(1) removal (order doesn't matter)
 | 
			
		||||
  *it = light_effects_.back();
 | 
			
		||||
  light_effects_.pop_back();
 | 
			
		||||
  light_effects_.erase(light_effect);
 | 
			
		||||
 | 
			
		||||
  for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
 | 
			
		||||
    leave_(universe);
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
#include <map>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <set>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -46,8 +47,9 @@ class E131Component : public esphome::Component {
 | 
			
		||||
 | 
			
		||||
  E131ListenMethod listen_method_{E131_MULTICAST};
 | 
			
		||||
  std::unique_ptr<socket::Socket> socket_;
 | 
			
		||||
  std::vector<E131AddressableLightEffect *> light_effects_;
 | 
			
		||||
  std::set<E131AddressableLightEffect *> light_effects_;
 | 
			
		||||
  std::map<int, int> universe_consumers_;
 | 
			
		||||
  std::map<int, E131Packet> universe_packets_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace e131
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ namespace e131 {
 | 
			
		||||
static const char *const TAG = "e131_addressable_light_effect";
 | 
			
		||||
static const int MAX_DATA_SIZE = (sizeof(E131Packet::values) - 1);
 | 
			
		||||
 | 
			
		||||
E131AddressableLightEffect::E131AddressableLightEffect(const char *name) : AddressableLightEffect(name) {}
 | 
			
		||||
E131AddressableLightEffect::E131AddressableLightEffect(const std::string &name) : AddressableLightEffect(name) {}
 | 
			
		||||
 | 
			
		||||
int E131AddressableLightEffect::get_data_per_universe() const { return get_lights_per_universe() * channels_; }
 | 
			
		||||
 | 
			
		||||
@@ -58,8 +58,8 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet
 | 
			
		||||
      std::min(it->size(), std::min(output_offset + get_lights_per_universe(), output_offset + packet.count - 1));
 | 
			
		||||
  auto *input_data = packet.values + 1;
 | 
			
		||||
 | 
			
		||||
  ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name(), universe, output_offset,
 | 
			
		||||
           output_end);
 | 
			
		||||
  ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name().c_str(), universe,
 | 
			
		||||
           output_offset, output_end);
 | 
			
		||||
 | 
			
		||||
  switch (channels_) {
 | 
			
		||||
    case E131_MONO:
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ enum E131LightChannels { E131_MONO = 1, E131_RGB = 3, E131_RGBW = 4 };
 | 
			
		||||
 | 
			
		||||
class E131AddressableLightEffect : public light::AddressableLightEffect {
 | 
			
		||||
 public:
 | 
			
		||||
  E131AddressableLightEffect(const char *name);
 | 
			
		||||
  E131AddressableLightEffect(const std::string &name);
 | 
			
		||||
 | 
			
		||||
  void start() override;
 | 
			
		||||
  void stop() override;
 | 
			
		||||
 
 | 
			
		||||
@@ -103,7 +103,7 @@ bool EPaperBase::is_idle_() {
 | 
			
		||||
  if (this->busy_pin_ == nullptr) {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  return this->busy_pin_->digital_read();
 | 
			
		||||
  return !this->busy_pin_->digital_read();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperBase::reset() {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,3 @@
 | 
			
		||||
import contextlib
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
import itertools
 | 
			
		||||
import logging
 | 
			
		||||
@@ -103,10 +102,6 @@ COMPILER_OPTIMIZATIONS = {
 | 
			
		||||
    "SIZE": "CONFIG_COMPILER_OPTIMIZATION_SIZE",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Socket limit configuration for ESP-IDF
 | 
			
		||||
# ESP-IDF CONFIG_LWIP_MAX_SOCKETS has range 1-253, default 10
 | 
			
		||||
DEFAULT_MAX_SOCKETS = 10  # ESP-IDF default
 | 
			
		||||
 | 
			
		||||
ARDUINO_ALLOWED_VARIANTS = [
 | 
			
		||||
    VARIANT_ESP32,
 | 
			
		||||
    VARIANT_ESP32C3,
 | 
			
		||||
@@ -304,13 +299,9 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
 | 
			
		||||
def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
 | 
			
		||||
    # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to
 | 
			
		||||
    # a PIO platformio/framework-espidf value
 | 
			
		||||
    if ver == cv.Version(5, 4, 3) or ver >= cv.Version(5, 5, 1):
 | 
			
		||||
        ext = "tar.xz"
 | 
			
		||||
    else:
 | 
			
		||||
        ext = "zip"
 | 
			
		||||
    if release:
 | 
			
		||||
        return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.{ext}"
 | 
			
		||||
    return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.{ext}"
 | 
			
		||||
        return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip"
 | 
			
		||||
    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:
 | 
			
		||||
@@ -359,7 +350,6 @@ ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
 | 
			
		||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
 | 
			
		||||
    cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"),
 | 
			
		||||
    cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"),
 | 
			
		||||
    cv.Version(5, 4, 3): cv.Version(55, 3, 32),
 | 
			
		||||
    cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"),
 | 
			
		||||
    cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"),
 | 
			
		||||
    cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"),
 | 
			
		||||
@@ -555,32 +545,6 @@ CONF_ENABLE_LWIP_BRIDGE_INTERFACE = "enable_lwip_bridge_interface"
 | 
			
		||||
CONF_ENABLE_LWIP_TCPIP_CORE_LOCKING = "enable_lwip_tcpip_core_locking"
 | 
			
		||||
CONF_ENABLE_LWIP_CHECK_THREAD_SAFETY = "enable_lwip_check_thread_safety"
 | 
			
		||||
CONF_DISABLE_LIBC_LOCKS_IN_IRAM = "disable_libc_locks_in_iram"
 | 
			
		||||
CONF_DISABLE_VFS_SUPPORT_TERMIOS = "disable_vfs_support_termios"
 | 
			
		||||
CONF_DISABLE_VFS_SUPPORT_SELECT = "disable_vfs_support_select"
 | 
			
		||||
CONF_DISABLE_VFS_SUPPORT_DIR = "disable_vfs_support_dir"
 | 
			
		||||
 | 
			
		||||
# VFS requirement tracking
 | 
			
		||||
# Components that need VFS features can call require_vfs_select() or require_vfs_dir()
 | 
			
		||||
KEY_VFS_SELECT_REQUIRED = "vfs_select_required"
 | 
			
		||||
KEY_VFS_DIR_REQUIRED = "vfs_dir_required"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def require_vfs_select() -> None:
 | 
			
		||||
    """Mark that VFS select support is required by a component.
 | 
			
		||||
 | 
			
		||||
    Call this from components that use esp_vfs_eventfd or other VFS select features.
 | 
			
		||||
    This prevents CONFIG_VFS_SUPPORT_SELECT from being disabled.
 | 
			
		||||
    """
 | 
			
		||||
    CORE.data[KEY_VFS_SELECT_REQUIRED] = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def require_vfs_dir() -> None:
 | 
			
		||||
    """Mark that VFS directory support is required by a component.
 | 
			
		||||
 | 
			
		||||
    Call this from components that use directory functions (opendir, readdir, mkdir, etc.).
 | 
			
		||||
    This prevents CONFIG_VFS_SUPPORT_DIR from being disabled.
 | 
			
		||||
    """
 | 
			
		||||
    CORE.data[KEY_VFS_DIR_REQUIRED] = True
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _validate_idf_component(config: ConfigType) -> ConfigType:
 | 
			
		||||
@@ -646,13 +610,6 @@ FRAMEWORK_SCHEMA = cv.All(
 | 
			
		||||
                    cv.Optional(
 | 
			
		||||
                        CONF_DISABLE_LIBC_LOCKS_IN_IRAM, default=True
 | 
			
		||||
                    ): cv.boolean,
 | 
			
		||||
                    cv.Optional(
 | 
			
		||||
                        CONF_DISABLE_VFS_SUPPORT_TERMIOS, default=True
 | 
			
		||||
                    ): cv.boolean,
 | 
			
		||||
                    cv.Optional(
 | 
			
		||||
                        CONF_DISABLE_VFS_SUPPORT_SELECT, default=True
 | 
			
		||||
                    ): cv.boolean,
 | 
			
		||||
                    cv.Optional(CONF_DISABLE_VFS_SUPPORT_DIR, default=True): cv.boolean,
 | 
			
		||||
                    cv.Optional(CONF_EXECUTE_FROM_PSRAM): cv.boolean,
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
@@ -789,72 +746,6 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = cv.Schema(final_validate)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _configure_lwip_max_sockets(conf: dict) -> None:
 | 
			
		||||
    """Calculate and set CONFIG_LWIP_MAX_SOCKETS based on component needs.
 | 
			
		||||
 | 
			
		||||
    Socket component tracks consumer needs via consume_sockets() called during config validation.
 | 
			
		||||
    This function runs in to_code() after all components have registered their socket needs.
 | 
			
		||||
    User-provided sdkconfig_options take precedence.
 | 
			
		||||
    """
 | 
			
		||||
    from esphome.components.socket import KEY_SOCKET_CONSUMERS
 | 
			
		||||
 | 
			
		||||
    # Check if user manually specified CONFIG_LWIP_MAX_SOCKETS
 | 
			
		||||
    user_max_sockets = conf.get(CONF_SDKCONFIG_OPTIONS, {}).get(
 | 
			
		||||
        "CONFIG_LWIP_MAX_SOCKETS"
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    socket_consumers: dict[str, int] = CORE.data.get(KEY_SOCKET_CONSUMERS, {})
 | 
			
		||||
    total_sockets = sum(socket_consumers.values())
 | 
			
		||||
 | 
			
		||||
    # Early return if no sockets registered and no user override
 | 
			
		||||
    if total_sockets == 0 and user_max_sockets is None:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    components_list = ", ".join(
 | 
			
		||||
        f"{name}={count}" for name, count in sorted(socket_consumers.items())
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # User specified their own value - respect it but warn if insufficient
 | 
			
		||||
    if user_max_sockets is not None:
 | 
			
		||||
        _LOGGER.info(
 | 
			
		||||
            "Using user-provided CONFIG_LWIP_MAX_SOCKETS: %s",
 | 
			
		||||
            user_max_sockets,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Warn if user's value is less than what components need
 | 
			
		||||
        if total_sockets > 0:
 | 
			
		||||
            user_sockets_int = 0
 | 
			
		||||
            with contextlib.suppress(ValueError, TypeError):
 | 
			
		||||
                user_sockets_int = int(user_max_sockets)
 | 
			
		||||
 | 
			
		||||
            if user_sockets_int < total_sockets:
 | 
			
		||||
                _LOGGER.warning(
 | 
			
		||||
                    "CONFIG_LWIP_MAX_SOCKETS is set to %d but your configuration "
 | 
			
		||||
                    "needs %d sockets (registered: %s). You may experience socket "
 | 
			
		||||
                    "exhaustion errors. Consider increasing to at least %d.",
 | 
			
		||||
                    user_sockets_int,
 | 
			
		||||
                    total_sockets,
 | 
			
		||||
                    components_list,
 | 
			
		||||
                    total_sockets,
 | 
			
		||||
                )
 | 
			
		||||
        # User's value already added via sdkconfig_options processing
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # Auto-calculate based on component needs
 | 
			
		||||
    # Use at least the ESP-IDF default (10), or the total needed by components
 | 
			
		||||
    max_sockets = max(DEFAULT_MAX_SOCKETS, total_sockets)
 | 
			
		||||
 | 
			
		||||
    log_level = logging.INFO if max_sockets > DEFAULT_MAX_SOCKETS else logging.DEBUG
 | 
			
		||||
    _LOGGER.log(
 | 
			
		||||
        log_level,
 | 
			
		||||
        "Setting CONFIG_LWIP_MAX_SOCKETS to %d (registered: %s)",
 | 
			
		||||
        max_sockets,
 | 
			
		||||
        components_list,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_LWIP_MAX_SOCKETS", max_sockets)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    cg.add_platformio_option("board", config[CONF_BOARD])
 | 
			
		||||
    cg.add_platformio_option("board_upload.flash_size", config[CONF_FLASH_SIZE])
 | 
			
		||||
@@ -882,27 +773,12 @@ async def to_code(config):
 | 
			
		||||
    for clean_var in ("IDF_PATH", "IDF_TOOLS_PATH"):
 | 
			
		||||
        os.environ.pop(clean_var, None)
 | 
			
		||||
 | 
			
		||||
    # Set the location of the IDF component manager cache
 | 
			
		||||
    os.environ["IDF_COMPONENT_CACHE_PATH"] = str(
 | 
			
		||||
        CORE.relative_internal_path(".espressif")
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    add_extra_script(
 | 
			
		||||
        "post",
 | 
			
		||||
        "post_build.py",
 | 
			
		||||
        Path(__file__).parent / "post_build.py.script",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # In testing mode, add IRAM fix script to allow linking grouped component tests
 | 
			
		||||
    # Similar to ESP8266's approach but for ESP-IDF
 | 
			
		||||
    if CORE.testing_mode:
 | 
			
		||||
        cg.add_build_flag("-DESPHOME_TESTING_MODE")
 | 
			
		||||
        add_extra_script(
 | 
			
		||||
            "pre",
 | 
			
		||||
            "iram_fix.py",
 | 
			
		||||
            Path(__file__).parent / "iram_fix.py.script",
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if conf[CONF_TYPE] == FRAMEWORK_ESP_IDF:
 | 
			
		||||
        cg.add_platformio_option("framework", "espidf")
 | 
			
		||||
        cg.add_build_flag("-DUSE_ESP_IDF")
 | 
			
		||||
@@ -929,7 +805,6 @@ async def to_code(config):
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_AUTOSTART_ARDUINO", True)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_MBEDTLS_PSK_MODES", True)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_MBEDTLS_CERTIFICATE_BUNDLE", True)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_ESP_PHY_REDUCE_TX_POWER", True)
 | 
			
		||||
 | 
			
		||||
    cg.add_build_flag("-Wno-nonnull-compare")
 | 
			
		||||
 | 
			
		||||
@@ -980,9 +855,6 @@ async def to_code(config):
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_LWIP_DNS_SUPPORT_MDNS_QUERIES", False)
 | 
			
		||||
    if not advanced.get(CONF_ENABLE_LWIP_BRIDGE_INTERFACE, False):
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_LWIP_BRIDGEIF_MAX_PORTS", 0)
 | 
			
		||||
 | 
			
		||||
    _configure_lwip_max_sockets(conf)
 | 
			
		||||
 | 
			
		||||
    if advanced.get(CONF_EXECUTE_FROM_PSRAM, False):
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_SPIRAM_FETCH_INSTRUCTIONS", True)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_SPIRAM_RODATA", True)
 | 
			
		||||
@@ -1005,43 +877,6 @@ async def to_code(config):
 | 
			
		||||
    if advanced.get(CONF_DISABLE_LIBC_LOCKS_IN_IRAM, True):
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_LIBC_LOCKS_PLACE_IN_IRAM", False)
 | 
			
		||||
 | 
			
		||||
    # Disable VFS support for termios (terminal I/O functions)
 | 
			
		||||
    # ESPHome doesn't use termios functions on ESP32 (only used in host UART driver).
 | 
			
		||||
    # Saves approximately 1.8KB of flash when disabled (default).
 | 
			
		||||
    add_idf_sdkconfig_option(
 | 
			
		||||
        "CONFIG_VFS_SUPPORT_TERMIOS",
 | 
			
		||||
        not advanced.get(CONF_DISABLE_VFS_SUPPORT_TERMIOS, True),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # Disable VFS support for select() with file descriptors
 | 
			
		||||
    # ESPHome only uses select() with sockets via lwip_select(), which still works.
 | 
			
		||||
    # VFS select is only needed for UART/eventfd file descriptors.
 | 
			
		||||
    # Components that need it (e.g., openthread) call require_vfs_select().
 | 
			
		||||
    # Saves approximately 2.7KB of flash when disabled (default).
 | 
			
		||||
    if CORE.data.get(KEY_VFS_SELECT_REQUIRED, False):
 | 
			
		||||
        # Component requires VFS select - force enable regardless of user setting
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_SELECT", True)
 | 
			
		||||
    else:
 | 
			
		||||
        # No component needs it - allow user to control (default: disabled)
 | 
			
		||||
        add_idf_sdkconfig_option(
 | 
			
		||||
            "CONFIG_VFS_SUPPORT_SELECT",
 | 
			
		||||
            not advanced.get(CONF_DISABLE_VFS_SUPPORT_SELECT, True),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Disable VFS support for directory functions (opendir, readdir, mkdir, etc.)
 | 
			
		||||
    # ESPHome doesn't use directory functions on ESP32.
 | 
			
		||||
    # Components that need it (e.g., storage components) call require_vfs_dir().
 | 
			
		||||
    # Saves approximately 0.5KB+ of flash when disabled (default).
 | 
			
		||||
    if CORE.data.get(KEY_VFS_DIR_REQUIRED, False):
 | 
			
		||||
        # Component requires VFS directory support - force enable regardless of user setting
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_VFS_SUPPORT_DIR", True)
 | 
			
		||||
    else:
 | 
			
		||||
        # No component needs it - allow user to control (default: disabled)
 | 
			
		||||
        add_idf_sdkconfig_option(
 | 
			
		||||
            "CONFIG_VFS_SUPPORT_DIR",
 | 
			
		||||
            not advanced.get(CONF_DISABLE_VFS_SUPPORT_DIR, True),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    cg.add_platformio_option("board_build.partitions", "partitions.csv")
 | 
			
		||||
    if CONF_PARTITIONS in config:
 | 
			
		||||
        add_extra_build_file(
 | 
			
		||||
 
 | 
			
		||||
@@ -6,7 +6,6 @@
 | 
			
		||||
#include <freertos/FreeRTOS.h>
 | 
			
		||||
#include <freertos/task.h>
 | 
			
		||||
#include <esp_idf_version.h>
 | 
			
		||||
#include <esp_ota_ops.h>
 | 
			
		||||
#include <esp_task_wdt.h>
 | 
			
		||||
#include <esp_timer.h>
 | 
			
		||||
#include <soc/rtc.h>
 | 
			
		||||
@@ -53,16 +52,6 @@ void arch_init() {
 | 
			
		||||
  disableCore1WDT();
 | 
			
		||||
#endif
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // If the bootloader was compiled with CONFIG_BOOTLOADER_APP_ROLLBACK_ENABLE the current
 | 
			
		||||
  // partition will get rolled back unless it is marked as valid.
 | 
			
		||||
  esp_ota_img_states_t state;
 | 
			
		||||
  const esp_partition_t *running = esp_ota_get_running_partition();
 | 
			
		||||
  if (esp_ota_get_state_partition(running, &state) == ESP_OK) {
 | 
			
		||||
    if (state == ESP_OTA_IMG_PENDING_VERIFY) {
 | 
			
		||||
      esp_ota_mark_app_valid_cancel_rollback();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void IRAM_ATTR HOT arch_feed_wdt() { esp_task_wdt_reset(); }
 | 
			
		||||
 | 
			
		||||
@@ -96,11 +85,7 @@ void loop_task(void *pv_params) {
 | 
			
		||||
 | 
			
		||||
extern "C" void app_main() {
 | 
			
		||||
  esp32::setup_preferences();
 | 
			
		||||
#if CONFIG_FREERTOS_UNICORE
 | 
			
		||||
  xTaskCreate(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle);
 | 
			
		||||
#else
 | 
			
		||||
  xTaskCreatePinnedToCore(loop_task, "loopTask", 8192, nullptr, 1, &loop_task_handle, 1);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#endif  // USE_ESP_IDF
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -40,13 +40,13 @@ class ESP32InternalGPIOPin : public InternalGPIOPin {
 | 
			
		||||
  // - 3 bytes for members below
 | 
			
		||||
  // - 1 byte padding for alignment
 | 
			
		||||
  // - 4 bytes for vtable pointer
 | 
			
		||||
  uint8_t pin_;          // GPIO pin number (0-255, actual max ~54 on ESP32)
 | 
			
		||||
  gpio::Flags flags_{};  // GPIO flags (1 byte)
 | 
			
		||||
  uint8_t pin_;        // GPIO pin number (0-255, actual max ~54 on ESP32)
 | 
			
		||||
  gpio::Flags flags_;  // GPIO flags (1 byte)
 | 
			
		||||
  struct PinFlags {
 | 
			
		||||
    uint8_t inverted : 1;        // Invert pin logic (1 bit)
 | 
			
		||||
    uint8_t drive_strength : 2;  // Drive strength 0-3 (2 bits)
 | 
			
		||||
    uint8_t reserved : 5;        // Reserved for future use (5 bits)
 | 
			
		||||
  } pin_flags_{};                // Total: 1 byte
 | 
			
		||||
  } pin_flags_;                  // Total: 1 byte
 | 
			
		||||
  // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
  static bool isr_service_installed;
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -223,10 +223,7 @@ async def esp32_pin_to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    num = config[CONF_NUMBER]
 | 
			
		||||
    cg.add(var.set_pin(getattr(gpio_num_t, f"GPIO_NUM_{num}")))
 | 
			
		||||
    # Only set if true to avoid bloating setup() function
 | 
			
		||||
    # (inverted bit in pin_flags_ bitfield is zero-initialized to false)
 | 
			
		||||
    if config[CONF_INVERTED]:
 | 
			
		||||
        cg.add(var.set_inverted(True))
 | 
			
		||||
    cg.add(var.set_inverted(config[CONF_INVERTED]))
 | 
			
		||||
    if CONF_DRIVE_STRENGTH in config:
 | 
			
		||||
        cg.add(var.set_drive_strength(config[CONF_DRIVE_STRENGTH]))
 | 
			
		||||
    cg.add(var.set_flags(pins.gpio_flags_expr(config[CONF_MODE])))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,71 +0,0 @@
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
# pylint: disable=E0602
 | 
			
		||||
Import("env")  # noqa
 | 
			
		||||
 | 
			
		||||
# IRAM size for testing mode (2MB - large enough to accommodate grouped tests)
 | 
			
		||||
TESTING_IRAM_SIZE = 0x200000
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def patch_idf_linker_script(source, target, env):
 | 
			
		||||
    """Patch ESP-IDF linker script to increase IRAM size for testing mode."""
 | 
			
		||||
    # Check if we're in testing mode by looking for the define
 | 
			
		||||
    build_flags = env.get("BUILD_FLAGS", [])
 | 
			
		||||
    testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
 | 
			
		||||
 | 
			
		||||
    if not testing_mode:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # For ESP-IDF, the linker scripts are generated in the build directory
 | 
			
		||||
    build_dir = env.subst("$BUILD_DIR")
 | 
			
		||||
 | 
			
		||||
    # The memory.ld file is directly in the build directory
 | 
			
		||||
    memory_ld = os.path.join(build_dir, "memory.ld")
 | 
			
		||||
 | 
			
		||||
    if not os.path.exists(memory_ld):
 | 
			
		||||
        print(f"ESPHome: Warning - could not find linker script at {memory_ld}")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    try:
 | 
			
		||||
        with open(memory_ld, "r") as f:
 | 
			
		||||
            content = f.read()
 | 
			
		||||
    except OSError as e:
 | 
			
		||||
        print(f"ESPHome: Error reading linker script: {e}")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # Check if this file contains iram0_0_seg
 | 
			
		||||
    if 'iram0_0_seg' not in content:
 | 
			
		||||
        print(f"ESPHome: Warning - iram0_0_seg not found in {memory_ld}")
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # Look for iram0_0_seg definition and increase its length
 | 
			
		||||
    # ESP-IDF format can be:
 | 
			
		||||
    #   iram0_0_seg (RX) : org = 0x40080000, len = 0x20000 + 0x0
 | 
			
		||||
    # or more complex with nested parentheses:
 | 
			
		||||
    #   iram0_0_seg (RX) : org = (0x40370000 + 0x4000), len = (((0x403CB700 - (0x40378000 - 0x3FC88000)) - 0x3FC88000) + 0x8000 - 0x4000)
 | 
			
		||||
    # We want to change len to TESTING_IRAM_SIZE for testing
 | 
			
		||||
 | 
			
		||||
    # Use a more robust approach: find the line and manually parse it
 | 
			
		||||
    lines = content.split('\n')
 | 
			
		||||
    for i, line in enumerate(lines):
 | 
			
		||||
        if 'iram0_0_seg' in line and 'len' in line:
 | 
			
		||||
            # Find the position of "len = " and replace everything after it until the end of the statement
 | 
			
		||||
            match = re.search(r'(iram0_0_seg\s*\([^)]*\)\s*:\s*org\s*=\s*(?:\([^)]+\)|0x[0-9a-fA-F]+)\s*,\s*len\s*=\s*)(.+?)(\s*)$', line)
 | 
			
		||||
            if match:
 | 
			
		||||
                lines[i] = f"{match.group(1)}{TESTING_IRAM_SIZE:#x}{match.group(3)}"
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
    updated = '\n'.join(lines)
 | 
			
		||||
 | 
			
		||||
    if updated != content:
 | 
			
		||||
        with open(memory_ld, "w") as f:
 | 
			
		||||
            f.write(updated)
 | 
			
		||||
        print(f"ESPHome: Patched IRAM size to {TESTING_IRAM_SIZE:#x} in {memory_ld} for testing mode")
 | 
			
		||||
    else:
 | 
			
		||||
        print(f"ESPHome: Warning - could not patch iram0_0_seg in {memory_ld}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Hook into the build process before linking
 | 
			
		||||
# For ESP-IDF, we need to run this after the linker scripts are generated
 | 
			
		||||
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_idf_linker_script)
 | 
			
		||||
@@ -7,7 +7,6 @@ from typing import Any
 | 
			
		||||
 | 
			
		||||
from esphome import automation
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import socket
 | 
			
		||||
from esphome.components.esp32 import add_idf_sdkconfig_option, const, get_esp32_variant
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
@@ -482,14 +481,6 @@ async def to_code(config):
 | 
			
		||||
        cg.add(var.set_name(name))
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    # BLE uses 1 UDP socket for event notification to wake up main loop from select()
 | 
			
		||||
    # This enables low-latency (~12μs) BLE event processing instead of waiting for
 | 
			
		||||
    # select() timeout (0-16ms). The socket is created in ble_setup_() and used to
 | 
			
		||||
    # wake lwip_select() when BLE events arrive from the BLE thread.
 | 
			
		||||
    # Note: Called during config generation, socket is created at runtime. In practice,
 | 
			
		||||
    # always used since esp32_ble only runs on ESP32 which always has USE_SOCKET_SELECT_SUPPORT.
 | 
			
		||||
    socket.consume_sockets(1, "esp32_ble")(config)
 | 
			
		||||
 | 
			
		||||
    # Define max connections for use in C++ code (e.g., ble_server.h)
 | 
			
		||||
    max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
 | 
			
		||||
    cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections)
 | 
			
		||||
 
 | 
			
		||||
@@ -27,34 +27,10 @@ extern "C" {
 | 
			
		||||
#include <esp32-hal-bt.h>
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
#include <lwip/sockets.h>
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
namespace esphome::esp32_ble {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "esp32_ble";
 | 
			
		||||
 | 
			
		||||
// GAP event groups for deduplication across gap_event_handler and dispatch_gap_event_
 | 
			
		||||
#define GAP_SCAN_COMPLETE_EVENTS \
 | 
			
		||||
  case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: \
 | 
			
		||||
  case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: \
 | 
			
		||||
  case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT
 | 
			
		||||
 | 
			
		||||
#define GAP_ADV_COMPLETE_EVENTS \
 | 
			
		||||
  case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: \
 | 
			
		||||
  case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: \
 | 
			
		||||
  case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: \
 | 
			
		||||
  case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: \
 | 
			
		||||
  case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT
 | 
			
		||||
 | 
			
		||||
#define GAP_SECURITY_EVENTS \
 | 
			
		||||
  case ESP_GAP_BLE_AUTH_CMPL_EVT: \
 | 
			
		||||
  case ESP_GAP_BLE_SEC_REQ_EVT: \
 | 
			
		||||
  case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: \
 | 
			
		||||
  case ESP_GAP_BLE_PASSKEY_REQ_EVT: \
 | 
			
		||||
  case ESP_GAP_BLE_NC_REQ_EVT
 | 
			
		||||
 | 
			
		||||
void ESP32BLE::setup() {
 | 
			
		||||
  global_ble = this;
 | 
			
		||||
  if (!ble_pre_setup_()) {
 | 
			
		||||
@@ -297,21 +273,10 @@ bool ESP32BLE::ble_setup_() {
 | 
			
		||||
  // BLE takes some time to be fully set up, 200ms should be more than enough
 | 
			
		||||
  delay(200);  // NOLINT
 | 
			
		||||
 | 
			
		||||
  // Set up notification socket to wake main loop for BLE events
 | 
			
		||||
  // This enables low-latency (~12μs) event processing instead of waiting for select() timeout
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
  this->setup_event_notification_();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ESP32BLE::ble_dismantle_() {
 | 
			
		||||
  // Clean up notification socket first before dismantling BLE stack
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
  this->cleanup_event_notification_();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  esp_err_t err = esp_bluedroid_disable();
 | 
			
		||||
  if (err != ESP_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "esp_bluedroid_disable failed: %d", err);
 | 
			
		||||
@@ -409,12 +374,6 @@ void ESP32BLE::loop() {
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
  // Drain any notification socket events first
 | 
			
		||||
  // This clears the socket so it doesn't stay "ready" in subsequent select() calls
 | 
			
		||||
  this->drain_event_notifications_();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  BLEEvent *ble_event = this->ble_events_.pop();
 | 
			
		||||
  while (ble_event != nullptr) {
 | 
			
		||||
    switch (ble_event->type_) {
 | 
			
		||||
@@ -455,48 +414,60 @@ void ESP32BLE::loop() {
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
          // Scan complete events
 | 
			
		||||
          GAP_SCAN_COMPLETE_EVENTS:
 | 
			
		||||
          // Advertising complete events
 | 
			
		||||
          GAP_ADV_COMPLETE_EVENTS:
 | 
			
		||||
          // RSSI complete event
 | 
			
		||||
          case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
 | 
			
		||||
          // Security events
 | 
			
		||||
          GAP_SECURITY_EVENTS:
 | 
			
		||||
          case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
 | 
			
		||||
          case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
 | 
			
		||||
          case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
 | 
			
		||||
            // All three scan complete events have the same structure with just status
 | 
			
		||||
            // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe
 | 
			
		||||
            // This is verified at compile-time by static_assert checks in ble_event.h
 | 
			
		||||
            // The struct already contains our copy of the status (copied in BLEEvent constructor)
 | 
			
		||||
            ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
 | 
			
		||||
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
 | 
			
		||||
            {
 | 
			
		||||
              esp_ble_gap_cb_param_t *param;
 | 
			
		||||
              // clang-format off
 | 
			
		||||
              switch (gap_event) {
 | 
			
		||||
                // All three scan complete events have the same structure with just status
 | 
			
		||||
                // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe
 | 
			
		||||
                // This is verified at compile-time by static_assert checks in ble_event.h
 | 
			
		||||
                // The struct already contains our copy of the status (copied in BLEEvent constructor)
 | 
			
		||||
                GAP_SCAN_COMPLETE_EVENTS:
 | 
			
		||||
                  param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete);
 | 
			
		||||
                  break;
 | 
			
		||||
            for (auto *gap_handler : this->gap_event_handlers_) {
 | 
			
		||||
              gap_handler->gap_event_handler(
 | 
			
		||||
                  gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete));
 | 
			
		||||
            }
 | 
			
		||||
#endif
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
                // All advertising complete events have the same structure with just status
 | 
			
		||||
                GAP_ADV_COMPLETE_EVENTS:
 | 
			
		||||
                  param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.adv_complete);
 | 
			
		||||
                  break;
 | 
			
		||||
          // Advertising complete events
 | 
			
		||||
          case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
 | 
			
		||||
          case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
 | 
			
		||||
          case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
 | 
			
		||||
          case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
 | 
			
		||||
          case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
 | 
			
		||||
            // All advertising complete events have the same structure with just status
 | 
			
		||||
            ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
 | 
			
		||||
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
 | 
			
		||||
            for (auto *gap_handler : this->gap_event_handlers_) {
 | 
			
		||||
              gap_handler->gap_event_handler(
 | 
			
		||||
                  gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.adv_complete));
 | 
			
		||||
            }
 | 
			
		||||
#endif
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
                case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
 | 
			
		||||
                  param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.read_rssi_complete);
 | 
			
		||||
                  break;
 | 
			
		||||
          // RSSI complete event
 | 
			
		||||
          case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
 | 
			
		||||
            ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
 | 
			
		||||
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
 | 
			
		||||
            for (auto *gap_handler : this->gap_event_handlers_) {
 | 
			
		||||
              gap_handler->gap_event_handler(
 | 
			
		||||
                  gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.read_rssi_complete));
 | 
			
		||||
            }
 | 
			
		||||
#endif
 | 
			
		||||
            break;
 | 
			
		||||
 | 
			
		||||
                GAP_SECURITY_EVENTS:
 | 
			
		||||
                  param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.security);
 | 
			
		||||
                  break;
 | 
			
		||||
 | 
			
		||||
                default:
 | 
			
		||||
                  break;
 | 
			
		||||
              }
 | 
			
		||||
              // clang-format on
 | 
			
		||||
              // Dispatch to all registered handlers
 | 
			
		||||
              for (auto *gap_handler : this->gap_event_handlers_) {
 | 
			
		||||
                gap_handler->gap_event_handler(gap_event, param);
 | 
			
		||||
              }
 | 
			
		||||
          // Security events
 | 
			
		||||
          case ESP_GAP_BLE_AUTH_CMPL_EVT:
 | 
			
		||||
          case ESP_GAP_BLE_SEC_REQ_EVT:
 | 
			
		||||
          case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
 | 
			
		||||
          case ESP_GAP_BLE_PASSKEY_REQ_EVT:
 | 
			
		||||
          case ESP_GAP_BLE_NC_REQ_EVT:
 | 
			
		||||
            ESP_LOGV(TAG, "gap_event_handler - %d", gap_event);
 | 
			
		||||
#ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT
 | 
			
		||||
            for (auto *gap_handler : this->gap_event_handlers_) {
 | 
			
		||||
              gap_handler->gap_event_handler(
 | 
			
		||||
                  gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.security));
 | 
			
		||||
            }
 | 
			
		||||
#endif
 | 
			
		||||
            break;
 | 
			
		||||
@@ -576,13 +547,23 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
 | 
			
		||||
    // Queue GAP events that components need to handle
 | 
			
		||||
    // Scanning events - used by esp32_ble_tracker
 | 
			
		||||
    case ESP_GAP_BLE_SCAN_RESULT_EVT:
 | 
			
		||||
    GAP_SCAN_COMPLETE_EVENTS:
 | 
			
		||||
    case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT:
 | 
			
		||||
    case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT:
 | 
			
		||||
    case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT:
 | 
			
		||||
    // Advertising events - used by esp32_ble_beacon and esp32_ble server
 | 
			
		||||
    GAP_ADV_COMPLETE_EVENTS:
 | 
			
		||||
    case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT:
 | 
			
		||||
    case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT:
 | 
			
		||||
    case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT:
 | 
			
		||||
    case ESP_GAP_BLE_ADV_START_COMPLETE_EVT:
 | 
			
		||||
    case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT:
 | 
			
		||||
    // Connection events - used by ble_client
 | 
			
		||||
    case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT:
 | 
			
		||||
    // Security events - used by ble_client and bluetooth_proxy
 | 
			
		||||
    GAP_SECURITY_EVENTS:
 | 
			
		||||
    case ESP_GAP_BLE_AUTH_CMPL_EVT:
 | 
			
		||||
    case ESP_GAP_BLE_SEC_REQ_EVT:
 | 
			
		||||
    case ESP_GAP_BLE_PASSKEY_NOTIF_EVT:
 | 
			
		||||
    case ESP_GAP_BLE_PASSKEY_REQ_EVT:
 | 
			
		||||
    case ESP_GAP_BLE_NC_REQ_EVT:
 | 
			
		||||
      enqueue_ble_event(event, param);
 | 
			
		||||
      return;
 | 
			
		||||
 | 
			
		||||
@@ -603,10 +584,6 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa
 | 
			
		||||
void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
 | 
			
		||||
                                   esp_ble_gatts_cb_param_t *param) {
 | 
			
		||||
  enqueue_ble_event(event, gatts_if, param);
 | 
			
		||||
  // Wake up main loop to process GATT event immediately
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
  global_ble->notify_main_loop_();
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
@@ -614,10 +591,6 @@ void ESP32BLE::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gat
 | 
			
		||||
void ESP32BLE::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                                   esp_ble_gattc_cb_param_t *param) {
 | 
			
		||||
  enqueue_ble_event(event, gattc_if, param);
 | 
			
		||||
  // Wake up main loop to process GATT event immediately
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
  global_ble->notify_main_loop_();
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
@@ -657,89 +630,6 @@ void ESP32BLE::dump_config() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
void ESP32BLE::setup_event_notification_() {
 | 
			
		||||
  // Create UDP socket for event notifications
 | 
			
		||||
  this->notify_fd_ = lwip_socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP);
 | 
			
		||||
  if (this->notify_fd_ < 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Event socket create failed: %d", errno);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Bind to loopback with auto-assigned port
 | 
			
		||||
  struct sockaddr_in addr = {};
 | 
			
		||||
  addr.sin_family = AF_INET;
 | 
			
		||||
  addr.sin_addr.s_addr = lwip_htonl(INADDR_LOOPBACK);
 | 
			
		||||
  addr.sin_port = 0;  // Auto-assign port
 | 
			
		||||
 | 
			
		||||
  if (lwip_bind(this->notify_fd_, (struct sockaddr *) &addr, sizeof(addr)) < 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Event socket bind failed: %d", errno);
 | 
			
		||||
    lwip_close(this->notify_fd_);
 | 
			
		||||
    this->notify_fd_ = -1;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Get the assigned address and connect to it
 | 
			
		||||
  // Connecting a UDP socket allows using send() instead of sendto() for better performance
 | 
			
		||||
  struct sockaddr_in notify_addr;
 | 
			
		||||
  socklen_t len = sizeof(notify_addr);
 | 
			
		||||
  if (lwip_getsockname(this->notify_fd_, (struct sockaddr *) ¬ify_addr, &len) < 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Event socket address failed: %d", errno);
 | 
			
		||||
    lwip_close(this->notify_fd_);
 | 
			
		||||
    this->notify_fd_ = -1;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Connect to self (loopback) - allows using send() instead of sendto()
 | 
			
		||||
  // After connect(), no need to store notify_addr - the socket remembers it
 | 
			
		||||
  if (lwip_connect(this->notify_fd_, (struct sockaddr *) ¬ify_addr, sizeof(notify_addr)) < 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Event socket connect failed: %d", errno);
 | 
			
		||||
    lwip_close(this->notify_fd_);
 | 
			
		||||
    this->notify_fd_ = -1;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Set non-blocking mode
 | 
			
		||||
  int flags = lwip_fcntl(this->notify_fd_, F_GETFL, 0);
 | 
			
		||||
  lwip_fcntl(this->notify_fd_, F_SETFL, flags | O_NONBLOCK);
 | 
			
		||||
 | 
			
		||||
  // Register with application's select() loop
 | 
			
		||||
  if (!App.register_socket_fd(this->notify_fd_)) {
 | 
			
		||||
    ESP_LOGW(TAG, "Event socket register failed");
 | 
			
		||||
    lwip_close(this->notify_fd_);
 | 
			
		||||
    this->notify_fd_ = -1;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "Event socket ready");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32BLE::cleanup_event_notification_() {
 | 
			
		||||
  if (this->notify_fd_ >= 0) {
 | 
			
		||||
    App.unregister_socket_fd(this->notify_fd_);
 | 
			
		||||
    lwip_close(this->notify_fd_);
 | 
			
		||||
    this->notify_fd_ = -1;
 | 
			
		||||
    ESP_LOGD(TAG, "Event socket closed");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32BLE::drain_event_notifications_() {
 | 
			
		||||
  // Called from main loop to drain any pending notifications
 | 
			
		||||
  // Must check is_socket_ready() to avoid blocking on empty socket
 | 
			
		||||
  if (this->notify_fd_ >= 0 && App.is_socket_ready(this->notify_fd_)) {
 | 
			
		||||
    char buffer[BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE];
 | 
			
		||||
    // Drain all pending notifications with non-blocking reads
 | 
			
		||||
    // Multiple BLE events may have triggered multiple writes, so drain until EWOULDBLOCK
 | 
			
		||||
    // We control both ends of this loopback socket (always write 1 byte per event),
 | 
			
		||||
    // so no error checking needed - any errors indicate catastrophic system failure
 | 
			
		||||
    while (lwip_recvfrom(this->notify_fd_, buffer, sizeof(buffer), 0, nullptr, nullptr) > 0) {
 | 
			
		||||
      // Just draining, no action needed - actual BLE events are already queued
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#endif  // USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
 | 
			
		||||
uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) {
 | 
			
		||||
  uint64_t u = 0;
 | 
			
		||||
  u |= uint64_t(address[0] & 0xFF) << 40;
 | 
			
		||||
 
 | 
			
		||||
@@ -25,10 +25,6 @@
 | 
			
		||||
#include <esp_gattc_api.h>
 | 
			
		||||
#include <esp_gatts_api.h>
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
#include <lwip/sockets.h>
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
namespace esphome::esp32_ble {
 | 
			
		||||
 | 
			
		||||
// Maximum size of the BLE event queue
 | 
			
		||||
@@ -166,13 +162,6 @@ class ESP32BLE : public Component {
 | 
			
		||||
  void advertising_init_();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
  void setup_event_notification_();    // Create notification socket
 | 
			
		||||
  void cleanup_event_notification_();  // Close and unregister socket
 | 
			
		||||
  inline void notify_main_loop_();     // Wake up select() from BLE thread (hot path - inlined)
 | 
			
		||||
  void drain_event_notifications_();   // Read pending notifications in main loop
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  template<typename... Args> friend void enqueue_ble_event(Args... args);
 | 
			
		||||
 | 
			
		||||
@@ -207,13 +196,6 @@ class ESP32BLE : public Component {
 | 
			
		||||
  esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};  // 4 bytes (enum)
 | 
			
		||||
  uint32_t advertising_cycle_time_{};         // 4 bytes
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
  // Event notification socket for waking up main loop from BLE thread
 | 
			
		||||
  // Uses connected UDP loopback socket to wake lwip_select() with ~12μs latency vs 0-16ms timeout
 | 
			
		||||
  // Socket is connected during setup, allowing use of send() instead of sendto() for efficiency
 | 
			
		||||
  int notify_fd_{-1};  // 4 bytes (file descriptor)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // 2-byte aligned members
 | 
			
		||||
  uint16_t appearance_{0};  // 2 bytes
 | 
			
		||||
 | 
			
		||||
@@ -225,29 +207,6 @@ class ESP32BLE : public Component {
 | 
			
		||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
extern ESP32BLE *global_ble;
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
// Inline implementations for hot-path functions
 | 
			
		||||
// These are called from BLE thread (notify) and main loop (drain) on every event
 | 
			
		||||
 | 
			
		||||
// Small buffer for draining notification bytes (1 byte sent per BLE event)
 | 
			
		||||
// Size allows draining multiple notifications per recvfrom() without wasting stack
 | 
			
		||||
static constexpr size_t BLE_EVENT_NOTIFY_DRAIN_BUFFER_SIZE = 16;
 | 
			
		||||
 | 
			
		||||
inline void ESP32BLE::notify_main_loop_() {
 | 
			
		||||
  // Called from BLE thread context when events are queued
 | 
			
		||||
  // Wakes up lwip_select() in main loop by writing to connected loopback socket
 | 
			
		||||
  if (this->notify_fd_ >= 0) {
 | 
			
		||||
    const char dummy = 1;
 | 
			
		||||
    // Non-blocking send - if it fails (unlikely), select() will wake on timeout anyway
 | 
			
		||||
    // No error checking needed: we control both ends of this loopback socket, and the
 | 
			
		||||
    // BLE event is already queued. Notification is best-effort to reduce latency.
 | 
			
		||||
    // This is safe to call from BLE thread - send() is thread-safe in lwip
 | 
			
		||||
    // Socket is already connected to loopback address, so send() is faster than sendto()
 | 
			
		||||
    lwip_send(this->notify_fd_, &dummy, 1, 0);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif  // USE_SOCKET_SELECT_SUPPORT
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class BLEEnabledCondition : public Condition<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  bool check(Ts... x) override { return global_ble->is_active(); }
 | 
			
		||||
 
 | 
			
		||||
@@ -61,7 +61,12 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
 | 
			
		||||
      this->address_str_ = "";
 | 
			
		||||
    } else {
 | 
			
		||||
      char buf[18];
 | 
			
		||||
      format_mac_addr_upper(this->remote_bda_, buf);
 | 
			
		||||
      uint8_t mac[6] = {
 | 
			
		||||
          (uint8_t) ((this->address_ >> 40) & 0xff), (uint8_t) ((this->address_ >> 32) & 0xff),
 | 
			
		||||
          (uint8_t) ((this->address_ >> 24) & 0xff), (uint8_t) ((this->address_ >> 16) & 0xff),
 | 
			
		||||
          (uint8_t) ((this->address_ >> 8) & 0xff),  (uint8_t) ((this->address_ >> 0) & 0xff),
 | 
			
		||||
      };
 | 
			
		||||
      format_mac_addr_upper(mac, buf);
 | 
			
		||||
      this->address_str_ = buf;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -461,9 +461,7 @@ async def parse_value(value_config, args):
 | 
			
		||||
    if isinstance(value, str):
 | 
			
		||||
        value = list(value.encode(value_config[CONF_STRING_ENCODING]))
 | 
			
		||||
    if isinstance(value, list):
 | 
			
		||||
        # Generate initializer list {1, 2, 3} instead of std::vector<uint8_t>({1, 2, 3})
 | 
			
		||||
        # This calls the set_value(std::initializer_list<uint8_t>) overload
 | 
			
		||||
        return cg.ArrayInitializer(*value)
 | 
			
		||||
        return cg.std_vector.template(cg.uint8)(value)
 | 
			
		||||
    val = cg.RawExpression(f"{value_config[CONF_TYPE]}({cg.safe_exp(value)})")
 | 
			
		||||
    return ByteBuffer_ns.wrap(val, value_config[CONF_ENDIANNESS])
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -35,18 +35,13 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties)
 | 
			
		||||
 | 
			
		||||
void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
 | 
			
		||||
 | 
			
		||||
void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) {
 | 
			
		||||
void BLECharacteristic::set_value(const std::vector<uint8_t> &buffer) {
 | 
			
		||||
  xSemaphoreTake(this->set_value_lock_, 0L);
 | 
			
		||||
  this->value_ = std::move(buffer);
 | 
			
		||||
  this->value_ = buffer;
 | 
			
		||||
  xSemaphoreGive(this->set_value_lock_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLECharacteristic::set_value(std::initializer_list<uint8_t> data) {
 | 
			
		||||
  this->set_value(std::vector<uint8_t>(data));  // Delegate to move overload
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLECharacteristic::set_value(const std::string &buffer) {
 | 
			
		||||
  this->set_value(std::vector<uint8_t>(buffer.begin(), buffer.end()));  // Delegate to move overload
 | 
			
		||||
  this->set_value(std::vector<uint8_t>(buffer.begin(), buffer.end()));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLECharacteristic::notify() {
 | 
			
		||||
 
 | 
			
		||||
@@ -33,8 +33,7 @@ class BLECharacteristic {
 | 
			
		||||
  ~BLECharacteristic();
 | 
			
		||||
 | 
			
		||||
  void set_value(ByteBuffer buffer);
 | 
			
		||||
  void set_value(std::vector<uint8_t> &&buffer);
 | 
			
		||||
  void set_value(std::initializer_list<uint8_t> data);
 | 
			
		||||
  void set_value(const std::vector<uint8_t> &buffer);
 | 
			
		||||
  void set_value(const std::string &buffer);
 | 
			
		||||
 | 
			
		||||
  void set_broadcast_property(bool value);
 | 
			
		||||
 
 | 
			
		||||
@@ -46,17 +46,15 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) {
 | 
			
		||||
  this->state_ = CREATING;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEDescriptor::set_value(std::vector<uint8_t> &&buffer) { this->set_value_impl_(buffer.data(), buffer.size()); }
 | 
			
		||||
void BLEDescriptor::set_value(std::vector<uint8_t> buffer) {
 | 
			
		||||
  size_t length = buffer.size();
 | 
			
		||||
 | 
			
		||||
void BLEDescriptor::set_value(std::initializer_list<uint8_t> data) { this->set_value_impl_(data.begin(), data.size()); }
 | 
			
		||||
 | 
			
		||||
void BLEDescriptor::set_value_impl_(const uint8_t *data, size_t length) {
 | 
			
		||||
  if (length > this->value_.attr_max_len) {
 | 
			
		||||
    ESP_LOGE(TAG, "Size %d too large, must be no bigger than %d", length, this->value_.attr_max_len);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->value_.attr_len = length;
 | 
			
		||||
  memcpy(this->value_.attr_value, data, length);
 | 
			
		||||
  memcpy(this->value_.attr_value, buffer.data(), length);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
 | 
			
		||||
 
 | 
			
		||||
@@ -27,8 +27,7 @@ class BLEDescriptor {
 | 
			
		||||
  void do_create(BLECharacteristic *characteristic);
 | 
			
		||||
  ESPBTUUID get_uuid() const { return this->uuid_; }
 | 
			
		||||
 | 
			
		||||
  void set_value(std::vector<uint8_t> &&buffer);
 | 
			
		||||
  void set_value(std::initializer_list<uint8_t> data);
 | 
			
		||||
  void set_value(std::vector<uint8_t> buffer);
 | 
			
		||||
  void set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
 | 
			
		||||
 | 
			
		||||
  void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if, esp_ble_gatts_cb_param_t *param);
 | 
			
		||||
@@ -43,8 +42,6 @@ class BLEDescriptor {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void set_value_impl_(const uint8_t *data, size_t length);
 | 
			
		||||
 | 
			
		||||
  BLECharacteristic *characteristic_{nullptr};
 | 
			
		||||
  ESPBTUUID uuid_;
 | 
			
		||||
  uint16_t handle_{0xFFFF};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID, CONF_MODE, CONF_PORT
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@ayufan"]
 | 
			
		||||
AUTO_LOAD = ["camera"]
 | 
			
		||||
@@ -14,27 +13,13 @@ Mode = esp32_camera_web_server_ns.enum("Mode")
 | 
			
		||||
 | 
			
		||||
MODES = {"STREAM": Mode.STREAM, "SNAPSHOT": Mode.SNAPSHOT}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _consume_camera_web_server_sockets(config: ConfigType) -> ConfigType:
 | 
			
		||||
    """Register socket needs for camera web server."""
 | 
			
		||||
    from esphome.components import socket
 | 
			
		||||
 | 
			
		||||
    # Each camera web server instance needs 1 listening socket + 2 client connections
 | 
			
		||||
    sockets_needed = 3
 | 
			
		||||
    socket.consume_sockets(sockets_needed, "esp32_camera_web_server")(config)
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(CameraWebServer),
 | 
			
		||||
            cv.Required(CONF_PORT): cv.port,
 | 
			
		||||
            cv.Required(CONF_MODE): cv.enum(MODES, upper=True),
 | 
			
		||||
        },
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    _consume_camera_web_server_sockets,
 | 
			
		||||
)
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(CameraWebServer),
 | 
			
		||||
        cv.Required(CONF_PORT): cv.port,
 | 
			
		||||
        cv.Required(CONF_MODE): cv.enum(MODES, upper=True),
 | 
			
		||||
    },
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
 
 | 
			
		||||
@@ -95,7 +95,7 @@ async def to_code(config):
 | 
			
		||||
    if framework_ver >= cv.Version(5, 5, 0):
 | 
			
		||||
        esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="1.1.5")
 | 
			
		||||
        esp32.add_idf_component(name="espressif/eppp_link", ref="1.1.3")
 | 
			
		||||
        esp32.add_idf_component(name="espressif/esp_hosted", ref="2.6.1")
 | 
			
		||||
        esp32.add_idf_component(name="espressif/esp_hosted", ref="2.5.11")
 | 
			
		||||
    else:
 | 
			
		||||
        esp32.add_idf_component(name="espressif/esp_wifi_remote", ref="0.13.0")
 | 
			
		||||
        esp32.add_idf_component(name="espressif/eppp_link", ref="0.2.0")
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,11 @@
 | 
			
		||||
from esphome import automation
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import binary_sensor, esp32_ble, improv_base, output
 | 
			
		||||
from esphome.components import binary_sensor, esp32_ble, output
 | 
			
		||||
from esphome.components.esp32_ble import BTLoggers
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID, CONF_ON_STATE, CONF_TRIGGER_ID
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["esp32_ble_server", "improv_base"]
 | 
			
		||||
AUTO_LOAD = ["esp32_ble_server"]
 | 
			
		||||
CODEOWNERS = ["@jesserockz"]
 | 
			
		||||
DEPENDENCIES = ["wifi", "esp32"]
 | 
			
		||||
 | 
			
		||||
@@ -20,7 +20,6 @@ CONF_ON_STOP = "on_stop"
 | 
			
		||||
CONF_STATUS_INDICATOR = "status_indicator"
 | 
			
		||||
CONF_WIFI_TIMEOUT = "wifi_timeout"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
improv_ns = cg.esphome_ns.namespace("improv")
 | 
			
		||||
Error = improv_ns.enum("Error")
 | 
			
		||||
State = improv_ns.enum("State")
 | 
			
		||||
@@ -44,63 +43,55 @@ ESP32ImprovStoppedTrigger = esp32_improv_ns.class_(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(ESP32ImprovComponent),
 | 
			
		||||
            cv.Required(CONF_AUTHORIZER): cv.Any(
 | 
			
		||||
                cv.none, cv.use_id(binary_sensor.BinarySensor)
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput),
 | 
			
		||||
            cv.Optional(
 | 
			
		||||
                CONF_IDENTIFY_DURATION, default="10s"
 | 
			
		||||
            ): cv.positive_time_period_milliseconds,
 | 
			
		||||
            cv.Optional(
 | 
			
		||||
                CONF_AUTHORIZED_DURATION, default="1min"
 | 
			
		||||
            ): cv.positive_time_period_milliseconds,
 | 
			
		||||
            cv.Optional(
 | 
			
		||||
                CONF_WIFI_TIMEOUT, default="1min"
 | 
			
		||||
            ): cv.positive_time_period_milliseconds,
 | 
			
		||||
            cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                        ESP32ImprovProvisionedTrigger
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                        ESP32ImprovProvisioningTrigger
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ON_START): automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                        ESP32ImprovStartTrigger
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ON_STATE): automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                        ESP32ImprovStateTrigger
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ON_STOP): automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                        ESP32ImprovStoppedTrigger
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(improv_base.IMPROV_SCHEMA)
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(ESP32ImprovComponent),
 | 
			
		||||
        cv.Required(CONF_AUTHORIZER): cv.Any(
 | 
			
		||||
            cv.none, cv.use_id(binary_sensor.BinarySensor)
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_STATUS_INDICATOR): cv.use_id(output.BinaryOutput),
 | 
			
		||||
        cv.Optional(
 | 
			
		||||
            CONF_IDENTIFY_DURATION, default="10s"
 | 
			
		||||
        ): cv.positive_time_period_milliseconds,
 | 
			
		||||
        cv.Optional(
 | 
			
		||||
            CONF_AUTHORIZED_DURATION, default="1min"
 | 
			
		||||
        ): cv.positive_time_period_milliseconds,
 | 
			
		||||
        cv.Optional(
 | 
			
		||||
            CONF_WIFI_TIMEOUT, default="1min"
 | 
			
		||||
        ): cv.positive_time_period_milliseconds,
 | 
			
		||||
        cv.Optional(CONF_ON_PROVISIONED): automation.validate_automation(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                    ESP32ImprovProvisionedTrigger
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ON_PROVISIONING): automation.validate_automation(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                    ESP32ImprovProvisioningTrigger
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ON_START): automation.validate_automation(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStartTrigger),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ON_STATE): automation.validate_automation(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESP32ImprovStateTrigger),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ON_STOP): automation.validate_automation(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                    ESP32ImprovStoppedTrigger
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
@@ -111,8 +102,7 @@ async def to_code(config):
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    cg.add_define("USE_IMPROV")
 | 
			
		||||
 | 
			
		||||
    await improv_base.setup_improv_core(var, config, "esp32_improv")
 | 
			
		||||
    cg.add_library("improv/Improv", "1.2.4")
 | 
			
		||||
 | 
			
		||||
    cg.add(var.set_identify_duration(config[CONF_IDENTIFY_DURATION]))
 | 
			
		||||
    cg.add(var.set_authorized_duration(config[CONF_AUTHORIZED_DURATION]))
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,10 @@
 | 
			
		||||
#include "esp32_improv_component.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/bytebuffer/bytebuffer.h"
 | 
			
		||||
#include "esphome/components/esp32_ble/ble.h"
 | 
			
		||||
#include "esphome/components/esp32_ble_server/ble_2902.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/components/bytebuffer/bytebuffer.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
@@ -270,8 +270,8 @@ void ESP32ImprovComponent::set_error_(improv::Error error) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &&response) {
 | 
			
		||||
  this->rpc_response_->set_value(std::move(response));
 | 
			
		||||
void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &response) {
 | 
			
		||||
  this->rpc_response_->set_value(ByteBuffer::wrap(response));
 | 
			
		||||
  if (this->state_ != improv::STATE_STOPPED)
 | 
			
		||||
    this->rpc_response_->notify();
 | 
			
		||||
}
 | 
			
		||||
@@ -384,33 +384,18 @@ void ESP32ImprovComponent::check_wifi_connection_() {
 | 
			
		||||
    this->connecting_sta_ = {};
 | 
			
		||||
    this->cancel_timeout("wifi-connect-timeout");
 | 
			
		||||
 | 
			
		||||
    // Build URL list with minimal allocations
 | 
			
		||||
    // Maximum 3 URLs: custom next_url + ESPHOME_MY_LINK + webserver URL
 | 
			
		||||
    std::string url_strings[3];
 | 
			
		||||
    size_t url_count = 0;
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_IMPROV_NEXT_URL
 | 
			
		||||
    // Add next_url if configured (should be first per Improv BLE spec)
 | 
			
		||||
    std::string next_url = this->get_formatted_next_url_();
 | 
			
		||||
    if (!next_url.empty()) {
 | 
			
		||||
      url_strings[url_count++] = std::move(next_url);
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    // Add default URLs for backward compatibility
 | 
			
		||||
    url_strings[url_count++] = ESPHOME_MY_LINK;
 | 
			
		||||
    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()) {
 | 
			
		||||
        char url_buffer[64];
 | 
			
		||||
        snprintf(url_buffer, sizeof(url_buffer), "http://%s:%d", ip.str().c_str(), USE_WEBSERVER_PORT);
 | 
			
		||||
        url_strings[url_count++] = url_buffer;
 | 
			
		||||
        std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
 | 
			
		||||
        urls.push_back(webserver_url);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
    this->send_response_(improv::build_rpc_response(improv::WIFI_SETTINGS,
 | 
			
		||||
                                                    std::vector<std::string>(url_strings, url_strings + url_count)));
 | 
			
		||||
    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");
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/esp32_ble_server/ble_characteristic.h"
 | 
			
		||||
#include "esphome/components/esp32_ble_server/ble_server.h"
 | 
			
		||||
#include "esphome/components/improv_base/improv_base.h"
 | 
			
		||||
#include "esphome/components/wifi/wifi_component.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_IMPROV_STATE_CALLBACK
 | 
			
		||||
@@ -33,7 +32,7 @@ namespace esp32_improv {
 | 
			
		||||
 | 
			
		||||
using namespace esp32_ble_server;
 | 
			
		||||
 | 
			
		||||
class ESP32ImprovComponent : public Component, public improv_base::ImprovBase {
 | 
			
		||||
class ESP32ImprovComponent : public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  ESP32ImprovComponent();
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
@@ -109,7 +108,7 @@ class ESP32ImprovComponent : public Component, public improv_base::ImprovBase {
 | 
			
		||||
  void set_state_(improv::State state, bool update_advertising = true);
 | 
			
		||||
  void set_error_(improv::Error error);
 | 
			
		||||
  improv::State get_initial_state_() const;
 | 
			
		||||
  void send_response_(std::vector<uint8_t> &&response);
 | 
			
		||||
  void send_response_(std::vector<uint8_t> &response);
 | 
			
		||||
  void process_incoming_data_();
 | 
			
		||||
  void on_wifi_connect_timeout_();
 | 
			
		||||
  void check_wifi_connection_();
 | 
			
		||||
 
 | 
			
		||||
@@ -190,9 +190,7 @@ async def to_code(config):
 | 
			
		||||
    cg.add_define("ESPHOME_VARIANT", "ESP8266")
 | 
			
		||||
    cg.add_define(ThreadModel.SINGLE)
 | 
			
		||||
 | 
			
		||||
    cg.add_platformio_option(
 | 
			
		||||
        "extra_scripts", ["pre:testing_mode.py", "post:post_build.py"]
 | 
			
		||||
    )
 | 
			
		||||
    cg.add_platformio_option("extra_scripts", ["pre:iram_fix.py", "post:post_build.py"])
 | 
			
		||||
 | 
			
		||||
    conf = config[CONF_FRAMEWORK]
 | 
			
		||||
    cg.add_platformio_option("framework", "arduino")
 | 
			
		||||
@@ -232,9 +230,9 @@ async def to_code(config):
 | 
			
		||||
    # For cases where nullptrs can be handled, use nothrow: `new (std::nothrow) T;`
 | 
			
		||||
    cg.add_build_flag("-DNEW_OOM_ABORT")
 | 
			
		||||
 | 
			
		||||
    # In testing mode, fake larger memory to allow linking grouped component tests
 | 
			
		||||
    # Real ESP8266 hardware only has 32KB IRAM and ~80KB RAM, but for CI testing
 | 
			
		||||
    # we pretend it has much larger memory to test that components compile together
 | 
			
		||||
    # In testing mode, fake a larger IRAM to allow linking grouped component tests
 | 
			
		||||
    # Real ESP8266 hardware only has 32KB IRAM, but for CI testing we pretend it has 2MB
 | 
			
		||||
    # This is done via a pre-build script that generates a custom linker script
 | 
			
		||||
    if CORE.testing_mode:
 | 
			
		||||
        cg.add_build_flag("-DESPHOME_TESTING_MODE")
 | 
			
		||||
 | 
			
		||||
@@ -273,8 +271,8 @@ def copy_files():
 | 
			
		||||
        post_build_file,
 | 
			
		||||
        CORE.relative_build_path("post_build.py"),
 | 
			
		||||
    )
 | 
			
		||||
    testing_mode_file = dir / "testing_mode.py.script"
 | 
			
		||||
    iram_fix_file = dir / "iram_fix.py.script"
 | 
			
		||||
    copy_file_if_changed(
 | 
			
		||||
        testing_mode_file,
 | 
			
		||||
        CORE.relative_build_path("testing_mode.py"),
 | 
			
		||||
        iram_fix_file,
 | 
			
		||||
        CORE.relative_build_path("iram_fix.py"),
 | 
			
		||||
    )
 | 
			
		||||
 
 | 
			
		||||
@@ -29,8 +29,8 @@ class ESP8266GPIOPin : public InternalGPIOPin {
 | 
			
		||||
  void attach_interrupt(void (*func)(void *), void *arg, gpio::InterruptType type) const override;
 | 
			
		||||
 | 
			
		||||
  uint8_t pin_;
 | 
			
		||||
  bool inverted_{};
 | 
			
		||||
  gpio::Flags flags_{};
 | 
			
		||||
  bool inverted_;
 | 
			
		||||
  gpio::Flags flags_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esp8266
 | 
			
		||||
 
 | 
			
		||||
@@ -165,10 +165,7 @@ async def esp8266_pin_to_code(config):
 | 
			
		||||
    num = config[CONF_NUMBER]
 | 
			
		||||
    mode = config[CONF_MODE]
 | 
			
		||||
    cg.add(var.set_pin(num))
 | 
			
		||||
    # Only set if true to avoid bloating setup() function
 | 
			
		||||
    # (inverted bit in pin_flags_ bitfield is zero-initialized to false)
 | 
			
		||||
    if config[CONF_INVERTED]:
 | 
			
		||||
        cg.add(var.set_inverted(True))
 | 
			
		||||
    cg.add(var.set_inverted(config[CONF_INVERTED]))
 | 
			
		||||
    cg.add(var.set_flags(pins.gpio_flags_expr(mode)))
 | 
			
		||||
    if num < 16:
 | 
			
		||||
        initial_state: PinInitialState = CORE.data[KEY_ESP8266][KEY_PIN_INITIAL_STATES][
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										44
									
								
								esphome/components/esp8266/iram_fix.py.script
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										44
									
								
								esphome/components/esp8266/iram_fix.py.script
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,44 @@
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
# pylint: disable=E0602
 | 
			
		||||
Import("env")  # noqa
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def patch_linker_script_after_preprocess(source, target, env):
 | 
			
		||||
    """Patch the local linker script after PlatformIO preprocesses it."""
 | 
			
		||||
    # Check if we're in testing mode by looking for the define
 | 
			
		||||
    build_flags = env.get("BUILD_FLAGS", [])
 | 
			
		||||
    testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
 | 
			
		||||
 | 
			
		||||
    if not testing_mode:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # Get the local linker script path
 | 
			
		||||
    build_dir = env.subst("$BUILD_DIR")
 | 
			
		||||
    local_ld = os.path.join(build_dir, "ld", "local.eagle.app.v6.common.ld")
 | 
			
		||||
 | 
			
		||||
    if not os.path.exists(local_ld):
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # Read the linker script
 | 
			
		||||
    with open(local_ld, "r") as f:
 | 
			
		||||
        content = f.read()
 | 
			
		||||
 | 
			
		||||
    # Replace IRAM size from 0x8000 (32KB) to 0x200000 (2MB)
 | 
			
		||||
    # The line looks like: iram1_0_seg : org = 0x40100000, len = 0x8000
 | 
			
		||||
    updated = re.sub(
 | 
			
		||||
        r"(iram1_0_seg\s*:\s*org\s*=\s*0x40100000\s*,\s*len\s*=\s*)0x8000",
 | 
			
		||||
        r"\g<1>0x200000",
 | 
			
		||||
        content,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if updated != content:
 | 
			
		||||
        with open(local_ld, "w") as f:
 | 
			
		||||
            f.write(updated)
 | 
			
		||||
        print("ESPHome: Patched IRAM size to 2MB for testing mode")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Hook into the build process right before linking
 | 
			
		||||
# This runs after PlatformIO has already preprocessed the linker scripts
 | 
			
		||||
env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_linker_script_after_preprocess)
 | 
			
		||||
@@ -1,166 +0,0 @@
 | 
			
		||||
import os
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
# pylint: disable=E0602
 | 
			
		||||
Import("env")  # noqa
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Memory sizes for testing mode (allow larger builds for CI component grouping)
 | 
			
		||||
TESTING_IRAM_SIZE = "0x200000"  # 2MB
 | 
			
		||||
TESTING_DRAM_SIZE = "0x200000"  # 2MB
 | 
			
		||||
TESTING_FLASH_SIZE = "0x2000000"  # 32MB
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def patch_segment_size(content, segment_name, new_size, label):
 | 
			
		||||
    """Patch a memory segment's length in linker script.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        content: Linker script content
 | 
			
		||||
        segment_name: Name of the segment (e.g., 'iram1_0_seg')
 | 
			
		||||
        new_size: New size as hex string (e.g., '0x200000')
 | 
			
		||||
        label: Human-readable label for logging (e.g., 'IRAM')
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        Tuple of (patched_content, was_patched)
 | 
			
		||||
    """
 | 
			
		||||
    # Match: segment_name : org = 0x..., len = 0x...
 | 
			
		||||
    pattern = rf"({segment_name}\s*:\s*org\s*=\s*0x[0-9a-fA-F]+\s*,\s*len\s*=\s*)0x[0-9a-fA-F]+"
 | 
			
		||||
    new_content = re.sub(pattern, rf"\g<1>{new_size}", content)
 | 
			
		||||
    return new_content, new_content != content
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def apply_memory_patches(content):
 | 
			
		||||
    """Apply IRAM, DRAM, and Flash patches to linker script content.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        content: Linker script content as string
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        Patched content as string
 | 
			
		||||
    """
 | 
			
		||||
    patches_applied = []
 | 
			
		||||
 | 
			
		||||
    # Patch IRAM (for larger code in IRAM)
 | 
			
		||||
    content, patched = patch_segment_size(content, "iram1_0_seg", TESTING_IRAM_SIZE, "IRAM")
 | 
			
		||||
    if patched:
 | 
			
		||||
        patches_applied.append("IRAM")
 | 
			
		||||
 | 
			
		||||
    # Patch DRAM (for larger BSS/data sections)
 | 
			
		||||
    content, patched = patch_segment_size(content, "dram0_0_seg", TESTING_DRAM_SIZE, "DRAM")
 | 
			
		||||
    if patched:
 | 
			
		||||
        patches_applied.append("DRAM")
 | 
			
		||||
 | 
			
		||||
    # Patch Flash (for larger code sections)
 | 
			
		||||
    content, patched = patch_segment_size(content, "irom0_0_seg", TESTING_FLASH_SIZE, "Flash")
 | 
			
		||||
    if patched:
 | 
			
		||||
        patches_applied.append("Flash")
 | 
			
		||||
 | 
			
		||||
    if patches_applied:
 | 
			
		||||
        iram_mb = int(TESTING_IRAM_SIZE, 16) // (1024 * 1024)
 | 
			
		||||
        dram_mb = int(TESTING_DRAM_SIZE, 16) // (1024 * 1024)
 | 
			
		||||
        flash_mb = int(TESTING_FLASH_SIZE, 16) // (1024 * 1024)
 | 
			
		||||
        print(f"  Patched memory segments: {', '.join(patches_applied)} (IRAM/DRAM: {iram_mb}MB, Flash: {flash_mb}MB)")
 | 
			
		||||
 | 
			
		||||
    return content
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def patch_linker_script_file(filepath, description):
 | 
			
		||||
    """Patch a linker script file in the build directory with enlarged memory segments.
 | 
			
		||||
 | 
			
		||||
    This function modifies linker scripts in the build directory only (never SDK files).
 | 
			
		||||
    It patches IRAM, DRAM, and Flash segments to allow larger builds in testing mode.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        filepath: Path to the linker script file in the build directory
 | 
			
		||||
        description: Human-readable description for logging
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        True if the file was patched, False if already patched or not found
 | 
			
		||||
    """
 | 
			
		||||
    if not os.path.exists(filepath):
 | 
			
		||||
        print(f"ESPHome: {description} not found at {filepath}")
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    print(f"ESPHome: Patching {description}...")
 | 
			
		||||
    with open(filepath, "r") as f:
 | 
			
		||||
        content = f.read()
 | 
			
		||||
 | 
			
		||||
    patched_content = apply_memory_patches(content)
 | 
			
		||||
 | 
			
		||||
    if patched_content != content:
 | 
			
		||||
        with open(filepath, "w") as f:
 | 
			
		||||
            f.write(patched_content)
 | 
			
		||||
        print(f"ESPHome: Successfully patched {description}")
 | 
			
		||||
        return True
 | 
			
		||||
    else:
 | 
			
		||||
        print(f"ESPHome: {description} already patched or no changes needed")
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def patch_local_linker_script(source, target, env):
 | 
			
		||||
    """Patch the local.eagle.app.v6.common.ld in build directory.
 | 
			
		||||
 | 
			
		||||
    This patches the preprocessed linker script that PlatformIO creates in the build
 | 
			
		||||
    directory, enlarging IRAM, DRAM, and Flash segments for testing mode.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        source: SCons source nodes
 | 
			
		||||
        target: SCons target nodes
 | 
			
		||||
        env: SCons environment
 | 
			
		||||
    """
 | 
			
		||||
    # Check if we're in testing mode
 | 
			
		||||
    build_flags = env.get("BUILD_FLAGS", [])
 | 
			
		||||
    testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
 | 
			
		||||
 | 
			
		||||
    if not testing_mode:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    # Patch the local linker script if it exists
 | 
			
		||||
    build_dir = env.subst("$BUILD_DIR")
 | 
			
		||||
    ld_dir = os.path.join(build_dir, "ld")
 | 
			
		||||
    if os.path.exists(ld_dir):
 | 
			
		||||
        local_ld = os.path.join(ld_dir, "local.eagle.app.v6.common.ld")
 | 
			
		||||
        if os.path.exists(local_ld):
 | 
			
		||||
            patch_linker_script_file(local_ld, "local.eagle.app.v6.common.ld")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Check if we're in testing mode
 | 
			
		||||
build_flags = env.get("BUILD_FLAGS", [])
 | 
			
		||||
testing_mode = any("-DESPHOME_TESTING_MODE" in flag for flag in build_flags)
 | 
			
		||||
 | 
			
		||||
if testing_mode:
 | 
			
		||||
    # Create a custom linker script in the build directory with patched memory limits
 | 
			
		||||
    # This allows larger IRAM/DRAM/Flash for CI component grouping tests
 | 
			
		||||
    build_dir = env.subst("$BUILD_DIR")
 | 
			
		||||
    ldscript = env.GetProjectOption("board_build.ldscript", "")
 | 
			
		||||
    assert ldscript, "No linker script configured in board_build.ldscript"
 | 
			
		||||
 | 
			
		||||
    framework_dir = env.PioPlatform().get_package_dir("framework-arduinoespressif8266")
 | 
			
		||||
    assert framework_dir is not None, "Could not find framework-arduinoespressif8266 package"
 | 
			
		||||
 | 
			
		||||
    # Read the original SDK linker script (read-only, SDK is never modified)
 | 
			
		||||
    sdk_ld = os.path.join(framework_dir, "tools", "sdk", "ld", ldscript)
 | 
			
		||||
    # Create a custom version in the build directory (isolated, temporary)
 | 
			
		||||
    custom_ld = os.path.join(build_dir, f"testing_{ldscript}")
 | 
			
		||||
 | 
			
		||||
    if os.path.exists(sdk_ld) and not os.path.exists(custom_ld):
 | 
			
		||||
        # Read the SDK linker script
 | 
			
		||||
        with open(sdk_ld, "r") as f:
 | 
			
		||||
            content = f.read()
 | 
			
		||||
 | 
			
		||||
        # Apply memory patches (IRAM: 2MB, DRAM: 2MB, Flash: 32MB)
 | 
			
		||||
        patched_content = apply_memory_patches(content)
 | 
			
		||||
 | 
			
		||||
        # Write the patched linker script to the build directory
 | 
			
		||||
        with open(custom_ld, "w") as f:
 | 
			
		||||
            f.write(patched_content)
 | 
			
		||||
 | 
			
		||||
        print(f"ESPHome: Created custom linker script: {custom_ld}")
 | 
			
		||||
 | 
			
		||||
    # Tell the linker to use our custom script from the build directory
 | 
			
		||||
    assert os.path.exists(custom_ld), f"Custom linker script not found: {custom_ld}"
 | 
			
		||||
    env.Replace(LDSCRIPT_PATH=custom_ld)
 | 
			
		||||
    print(f"ESPHome: Using custom linker script with patched memory limits")
 | 
			
		||||
 | 
			
		||||
    # Also patch local.eagle.app.v6.common.ld after PlatformIO creates it
 | 
			
		||||
    env.AddPreAction("$BUILD_DIR/${PROGNAME}.elf", patch_local_linker_script)
 | 
			
		||||
@@ -103,16 +103,7 @@ def ota_esphome_final_validate(config):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _consume_ota_sockets(config: ConfigType) -> ConfigType:
 | 
			
		||||
    """Register socket needs for OTA component."""
 | 
			
		||||
    from esphome.components import socket
 | 
			
		||||
 | 
			
		||||
    # OTA needs 1 listening socket (client connections are temporary during updates)
 | 
			
		||||
    socket.consume_sockets(1, "ota")(config)
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent),
 | 
			
		||||
@@ -139,8 +130,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(BASE_OTA_SCHEMA)
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    _consume_ota_sockets,
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
 | 
			
		||||
 
 | 
			
		||||
@@ -281,15 +281,19 @@ void ESPHomeOTAComponent::handle_data_() {
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Acknowledge auth OK - 1 byte
 | 
			
		||||
  this->write_byte_(ota::OTA_RESPONSE_AUTH_OK);
 | 
			
		||||
  buf[0] = ota::OTA_RESPONSE_AUTH_OK;
 | 
			
		||||
  this->writeall_(buf, 1);
 | 
			
		||||
 | 
			
		||||
  // Read size, 4 bytes MSB first
 | 
			
		||||
  if (!this->readall_(buf, 4)) {
 | 
			
		||||
    this->log_read_error_(LOG_STR("size"));
 | 
			
		||||
    goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
 | 
			
		||||
  }
 | 
			
		||||
  ota_size = (static_cast<size_t>(buf[0]) << 24) | (static_cast<size_t>(buf[1]) << 16) |
 | 
			
		||||
             (static_cast<size_t>(buf[2]) << 8) | buf[3];
 | 
			
		||||
  ota_size = 0;
 | 
			
		||||
  for (uint8_t i = 0; i < 4; i++) {
 | 
			
		||||
    ota_size <<= 8;
 | 
			
		||||
    ota_size |= buf[i];
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGV(TAG, "Size is %u bytes", ota_size);
 | 
			
		||||
 | 
			
		||||
  // Now that we've passed authentication and are actually
 | 
			
		||||
@@ -309,7 +313,8 @@ void ESPHomeOTAComponent::handle_data_() {
 | 
			
		||||
  update_started = true;
 | 
			
		||||
 | 
			
		||||
  // Acknowledge prepare OK - 1 byte
 | 
			
		||||
  this->write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK);
 | 
			
		||||
  buf[0] = ota::OTA_RESPONSE_UPDATE_PREPARE_OK;
 | 
			
		||||
  this->writeall_(buf, 1);
 | 
			
		||||
 | 
			
		||||
  // Read binary MD5, 32 bytes
 | 
			
		||||
  if (!this->readall_(buf, 32)) {
 | 
			
		||||
@@ -321,7 +326,8 @@ void ESPHomeOTAComponent::handle_data_() {
 | 
			
		||||
  this->backend_->set_update_md5(sbuf);
 | 
			
		||||
 | 
			
		||||
  // Acknowledge MD5 OK - 1 byte
 | 
			
		||||
  this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK);
 | 
			
		||||
  buf[0] = ota::OTA_RESPONSE_BIN_MD5_OK;
 | 
			
		||||
  this->writeall_(buf, 1);
 | 
			
		||||
 | 
			
		||||
  while (total < ota_size) {
 | 
			
		||||
    // TODO: timeout check
 | 
			
		||||
@@ -348,7 +354,8 @@ void ESPHomeOTAComponent::handle_data_() {
 | 
			
		||||
    total += read;
 | 
			
		||||
#if USE_OTA_VERSION == 2
 | 
			
		||||
    while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) {
 | 
			
		||||
      this->write_byte_(ota::OTA_RESPONSE_CHUNK_OK);
 | 
			
		||||
      buf[0] = ota::OTA_RESPONSE_CHUNK_OK;
 | 
			
		||||
      this->writeall_(buf, 1);
 | 
			
		||||
      size_acknowledged += OTA_BLOCK_SIZE;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
@@ -367,7 +374,8 @@ void ESPHomeOTAComponent::handle_data_() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Acknowledge receive OK - 1 byte
 | 
			
		||||
  this->write_byte_(ota::OTA_RESPONSE_RECEIVE_OK);
 | 
			
		||||
  buf[0] = ota::OTA_RESPONSE_RECEIVE_OK;
 | 
			
		||||
  this->writeall_(buf, 1);
 | 
			
		||||
 | 
			
		||||
  error_code = this->backend_->end();
 | 
			
		||||
  if (error_code != ota::OTA_RESPONSE_OK) {
 | 
			
		||||
@@ -376,7 +384,8 @@ void ESPHomeOTAComponent::handle_data_() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Acknowledge Update end OK - 1 byte
 | 
			
		||||
  this->write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK);
 | 
			
		||||
  buf[0] = ota::OTA_RESPONSE_UPDATE_END_OK;
 | 
			
		||||
  this->writeall_(buf, 1);
 | 
			
		||||
 | 
			
		||||
  // Read ACK
 | 
			
		||||
  if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
 | 
			
		||||
@@ -395,7 +404,8 @@ void ESPHomeOTAComponent::handle_data_() {
 | 
			
		||||
  App.safe_reboot();
 | 
			
		||||
 | 
			
		||||
error:
 | 
			
		||||
  this->write_byte_(static_cast<uint8_t>(error_code));
 | 
			
		||||
  buf[0] = static_cast<uint8_t>(error_code);
 | 
			
		||||
  this->writeall_(buf, 1);
 | 
			
		||||
  this->cleanup_connection_();
 | 
			
		||||
 | 
			
		||||
  if (this->backend_ != nullptr && update_started) {
 | 
			
		||||
 
 | 
			
		||||
@@ -53,7 +53,6 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
 | 
			
		||||
#endif  // USE_OTA_PASSWORD
 | 
			
		||||
  bool readall_(uint8_t *buf, size_t len);
 | 
			
		||||
  bool writeall_(const uint8_t *buf, size_t len);
 | 
			
		||||
  inline bool write_byte_(uint8_t byte) { return this->writeall_(&byte, 1); }
 | 
			
		||||
 | 
			
		||||
  bool try_read_(size_t to_read, const LogString *desc);
 | 
			
		||||
  bool try_write_(size_t to_write, const LogString *desc);
 | 
			
		||||
 
 | 
			
		||||
@@ -14,13 +14,13 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente
 | 
			
		||||
  TEMPLATABLE_VALUE(std::vector<uint8_t>, data);
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
  void add_on_sent(const std::initializer_list<Action<Ts...> *> &actions) {
 | 
			
		||||
  void add_on_sent(const std::vector<Action<Ts...> *> &actions) {
 | 
			
		||||
    this->sent_.add_actions(actions);
 | 
			
		||||
    if (this->flags_.wait_for_sent) {
 | 
			
		||||
      this->sent_.add_action(new LambdaAction<Ts...>([this](Ts... x) { this->play_next_(x...); }));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  void add_on_error(const std::initializer_list<Action<Ts...> *> &actions) {
 | 
			
		||||
  void add_on_error(const std::vector<Action<Ts...> *> &actions) {
 | 
			
		||||
    this->error_.add_actions(actions);
 | 
			
		||||
    if (this->flags_.wait_for_sent) {
 | 
			
		||||
      this->error_.add_action(new LambdaAction<Ts...>([this](Ts... x) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,39 +0,0 @@
 | 
			
		||||
"""ESP-NOW transport platform for packet_transport component."""
 | 
			
		||||
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components.packet_transport import (
 | 
			
		||||
    PacketTransport,
 | 
			
		||||
    new_packet_transport,
 | 
			
		||||
    transport_schema,
 | 
			
		||||
)
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.core import HexInt
 | 
			
		||||
from esphome.cpp_types import PollingComponent
 | 
			
		||||
 | 
			
		||||
from .. import ESPNowComponent, espnow_ns
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@EasilyBoredEngineer"]
 | 
			
		||||
DEPENDENCIES = ["espnow"]
 | 
			
		||||
 | 
			
		||||
ESPNowTransport = espnow_ns.class_("ESPNowTransport", PacketTransport, PollingComponent)
 | 
			
		||||
 | 
			
		||||
CONF_ESPNOW_ID = "espnow_id"
 | 
			
		||||
CONF_PEER_ADDRESS = "peer_address"
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = transport_schema(ESPNowTransport).extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(CONF_ESPNOW_ID): cv.use_id(ESPNowComponent),
 | 
			
		||||
        cv.Optional(CONF_PEER_ADDRESS, default="FF:FF:FF:FF:FF:FF"): cv.mac_address,
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    """Set up the ESP-NOW transport component."""
 | 
			
		||||
    var, _ = await new_packet_transport(config)
 | 
			
		||||
 | 
			
		||||
    await cg.register_parented(var, config[CONF_ESPNOW_ID])
 | 
			
		||||
 | 
			
		||||
    # Set peer address - convert MAC to parts array like ESP-NOW does
 | 
			
		||||
    mac = config[CONF_PEER_ADDRESS]
 | 
			
		||||
    cg.add(var.set_peer_address([HexInt(x) for x in mac.parts]))
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user