mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-03 00:21:56 +00:00 
			
		
		
		
	Compare commits
	
		
			193 Commits
		
	
	
		
			jesserockz
			...
			proto_vect
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					ed4c2cc8be | ||
| 
						 | 
					f96c4ad721 | ||
| 
						 | 
					5b5388b3ff | ||
| 
						 | 
					1e3195676b | ||
| 
						 | 
					ea14f374e7 | ||
| 
						 | 
					81fdc14d7f | ||
| 
						 | 
					c96b8aaf2d | ||
| 
						 | 
					82c0f889ed | ||
| 
						 | 
					61bb4ef4f0 | ||
| 
						 | 
					aa98ccf3fd | ||
| 
						 | 
					b754a3c1b3 | ||
| 
						 | 
					d781fc7210 | ||
| 
						 | 
					aed34e78c8 | ||
| 
						 | 
					36ffe21bfd | ||
| 
						 | 
					a609343cb6 | ||
| 
						 | 
					5528c3c765 | ||
| 
						 | 
					0d805355f5 | ||
| 
						 | 
					99f48ae51c | ||
| 
						 | 
					25e4aafd71 | ||
| 
						 | 
					4f2d54be4e | ||
| 
						 | 
					249cd7415b | ||
| 
						 | 
					78d780105b | ||
| 
						 | 
					466d4522bc | ||
| 
						 | 
					e462217500 | ||
| 
						 | 
					f1bce262ed | ||
| 
						 | 
					7ed7e7ad26 | ||
| 
						 | 
					08b8454555 | ||
| 
						 | 
					0119e17f04 | ||
| 
						 | 
					c3f40de844 | ||
| 
						 | 
					7dd829cfca | ||
| 
						 | 
					da19673f51 | ||
| 
						 | 
					f5e32d03d0 | ||
| 
						 | 
					f3b69383fd | ||
| 
						 | 
					aba72809d3 | ||
| 
						 | 
					85205a28d2 | ||
| 
						 | 
					285e006637 | ||
| 
						 | 
					5647f36900 | ||
| 
						 | 
					1e9309ffff | ||
| 
						 | 
					ce8a6a6c43 | ||
| 
						 | 
					dfb4b31bf9 | ||
| 
						 | 
					31b1b50ad9 | ||
| 
						 | 
					3377080272 | ||
| 
						 | 
					d65ad69338 | ||
| 
						 | 
					dfa69173ea | ||
| 
						 | 
					f44615cc8d | ||
| 
						 | 
					bda4769bd3 | ||
| 
						 | 
					14b057f54e | ||
| 
						 | 
					e26b5874d7 | ||
| 
						 | 
					00f22e5c36 | ||
| 
						 | 
					51e080c2d3 | ||
| 
						 | 
					3c18558003 | ||
| 
						 | 
					7394cbf773 | ||
| 
						 | 
					1577a46efd | ||
| 
						 | 
					e212ed024d | ||
| 
						 | 
					5fdd90c71a | ||
| 
						 | 
					6929bdb415 | ||
| 
						 | 
					2c85ba037e | ||
| 
						 | 
					2440bbdceb | ||
| 
						 | 
					3ac8eb7696 | ||
| 
						 | 
					6a478b9070 | ||
| 
						 | 
					a32a1d11fb | ||
| 
						 | 
					daeb8ef88c | ||
| 
						 | 
					febee437d6 | ||
| 
						 | 
					de2f475dbd | ||
| 
						 | 
					fa3ec6f732 | ||
| 
						 | 
					e490aec6b4 | ||
| 
						 | 
					8da8095a6a | ||
| 
						 | 
					ab14c0cd72 | ||
| 
						 | 
					917deac7cb | ||
| 
						 | 
					3d21adecd3 | ||
| 
						 | 
					5b023f9369 | ||
| 
						 | 
					6c2ce5cacf | ||
| 
						 | 
					d23e25f099 | ||
| 
						 | 
					9b78098eec | ||
| 
						 | 
					7e5b82c5f3 | ||
| 
						 | 
					2864e989bd | ||
| 
						 | 
					6efe346cc5 | ||
| 
						 | 
					f2f6c597ef | ||
| 
						 | 
					b91b12d77a | ||
| 
						 | 
					7f567bdfbe | ||
| 
						 | 
					f2de8df556 | ||
| 
						 | 
					1c67a61945 | ||
| 
						 | 
					77141d3e83 | ||
| 
						 | 
					f592f79bce | ||
| 
						 | 
					6edbb94529 | ||
| 
						 | 
					d37eb59fd7 | ||
| 
						 | 
					e2b3617df3 | ||
| 
						 | 
					e1c851cab8 | ||
| 
						 | 
					146b067d62 | ||
| 
						 | 
					5b15827009 | ||
| 
						 | 
					0de79ba291 | ||
| 
						 | 
					e3aaf6a144 | ||
| 
						 | 
					78ffeb30fb | ||
| 
						 | 
					2c1927fd12 | ||
| 
						 | 
					c6ae1a5909 | ||
| 
						 | 
					9c712744be | ||
| 
						 | 
					ae50a09b4e | ||
| 
						 | 
					1ea80594c6 | ||
| 
						 | 
					8500323d39 | ||
| 
						 | 
					6f7db2f5f7 | ||
| 
						 | 
					9922c65912 | ||
| 
						 | 
					f2469077d9 | ||
| 
						 | 
					742eca92d8 | ||
| 
						 | 
					548913b471 | ||
| 
						 | 
					a05c5ea240 | ||
| 
						 | 
					8e8a2bde95 | ||
| 
						 | 
					80265a6bd2 | ||
| 
						 | 
					87e9a7a1bd | ||
| 
						 | 
					3aedfe8be3 | ||
| 
						 | 
					7f2cc47ed6 | ||
| 
						 | 
					a5542e0d2b | ||
| 
						 | 
					66afe4a9be | ||
| 
						 | 
					0ae9009e41 | ||
| 
						 | 
					0b2f5fcd7e | ||
| 
						 | 
					7a2887e2ed | ||
| 
						 | 
					cd2d3f061d | ||
| 
						 | 
					73f5d01c2d | ||
| 
						 | 
					0938609f7a | ||
| 
						 | 
					77203f0cb4 | ||
| 
						 | 
					040130e357 | ||
| 
						 | 
					85959e3004 | ||
| 
						 | 
					a809a13729 | ||
| 
						 | 
					3b6ff615e8 | ||
| 
						 | 
					05216db5f0 | ||
| 
						 | 
					9f668b0c4b | ||
| 
						 | 
					6a239f4d1c | ||
| 
						 | 
					ffb0e854b6 | ||
| 
						 | 
					6fbd0e3385 | ||
| 
						 | 
					426511e78d | ||
| 
						 | 
					97d91fee85 | ||
| 
						 | 
					0f4b54aa82 | ||
| 
						 | 
					1706a69fad | ||
| 
						 | 
					e23d66a8cf | ||
| 
						 | 
					46101fd830 | ||
| 
						 | 
					e988905c2f | ||
| 
						 | 
					abb57f08f5 | ||
| 
						 | 
					ca2fe994a1 | ||
| 
						 | 
					03def13917 | ||
| 
						 | 
					63f100a8ca | ||
| 
						 | 
					ea4e5fd7bd | ||
| 
						 | 
					12e9c5e60e | ||
| 
						 | 
					3d82c5baf7 | ||
| 
						 | 
					6f5e36ffc3 | ||
| 
						 | 
					118b1d8593 | ||
| 
						 | 
					319ba4a504 | ||
| 
						 | 
					ae8336c268 | ||
| 
						 | 
					1b38518c63 | ||
| 
						 | 
					c00977df54 | ||
| 
						 | 
					255b5a3abd | ||
| 
						 | 
					dd732dd155 | ||
| 
						 | 
					22fec4329f | ||
| 
						 | 
					8f1c4634ec | ||
| 
						 | 
					c15f1a9be8 | ||
| 
						 | 
					11b53096a6 | ||
| 
						 | 
					6a18367949 | ||
| 
						 | 
					a59b1494d8 | ||
| 
						 | 
					e6ce5c58d1 | ||
| 
						 | 
					ebc0f5f7c9 | ||
| 
						 | 
					0f87e7508b | ||
| 
						 | 
					862bbb7fe1 | ||
| 
						 | 
					020cea80b2 | ||
| 
						 | 
					9c146a7070 | ||
| 
						 | 
					afbd3f77af | ||
| 
						 | 
					1e1fefbd0a | ||
| 
						 | 
					1a2057df30 | ||
| 
						 | 
					87ca8784ef | ||
| 
						 | 
					a186c1062f | ||
| 
						 | 
					ea38237f29 | ||
| 
						 | 
					6aff1394ad | ||
| 
						 | 
					0e34d1b64d | ||
| 
						 | 
					1483cee0fb | ||
| 
						 | 
					8c1bd2fd85 | ||
| 
						 | 
					ea609dc0f6 | ||
| 
						 | 
					913095f6be | ||
| 
						 | 
					bb24ad4a30 | ||
| 
						 | 
					0d612fecfc | ||
| 
						 | 
					9c235b4140 | ||
| 
						 | 
					70cb1793f3 | ||
| 
						 | 
					3bdd351d49 | ||
| 
						 | 
					b0ea3f57de | ||
| 
						 | 
					c9312d5c27 | ||
| 
						 | 
					33fea90c19 | ||
| 
						 | 
					25f3b6a959 | ||
| 
						 | 
					e993312640 | ||
| 
						 | 
					85babe85e4 | ||
| 
						 | 
					0266c897c9 | ||
| 
						 | 
					bda7676e3a | ||
| 
						 | 
					57e98ec3fc | ||
| 
						 | 
					09b2ad071b | ||
| 
						 | 
					fdecda3d65 | ||
| 
						 | 
					a0922bc8b0 | ||
| 
						 | 
					f25af18655 | ||
| 
						 | 
					5db07c2d70 | 
@@ -1 +1 @@
 | 
			
		||||
d7693a1e996cacd4a3d1c9a16336799c2a8cc3db02e4e74084151ce964581248
 | 
			
		||||
3d46b63015d761c85ca9cb77ab79a389509e5776701fb22aed16e7b79d432c0c
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,5 @@
 | 
			
		||||
[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,6 +53,7 @@ jobs:
 | 
			
		||||
              'new-target-platform',
 | 
			
		||||
              'merging-to-release',
 | 
			
		||||
              'merging-to-beta',
 | 
			
		||||
              'chained-pr',
 | 
			
		||||
              'core',
 | 
			
		||||
              'small-pr',
 | 
			
		||||
              'dashboard',
 | 
			
		||||
@@ -140,6 +141,8 @@ 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;
 | 
			
		||||
@@ -413,7 +416,7 @@ jobs:
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Generate review messages
 | 
			
		||||
            function generateReviewMessages(finalLabels) {
 | 
			
		||||
            function generateReviewMessages(finalLabels, originalLabelCount) {
 | 
			
		||||
              const messages = [];
 | 
			
		||||
              const prAuthor = context.payload.pull_request.user.login;
 | 
			
		||||
 | 
			
		||||
@@ -427,15 +430,15 @@ jobs:
 | 
			
		||||
                  .reduce((sum, file) => sum + (file.deletions || 0), 0);
 | 
			
		||||
                const nonTestChanges = (totalAdditions - testAdditions) - (totalDeletions - testDeletions);
 | 
			
		||||
 | 
			
		||||
                const tooManyLabels = finalLabels.length > MAX_LABELS;
 | 
			
		||||
                const tooManyLabels = originalLabelCount > 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 ${finalLabels.length} different components/areas.`;
 | 
			
		||||
                  message += `This PR is too large with ${nonTestChanges} line changes (excluding tests) and affects ${originalLabelCount} different components/areas.`;
 | 
			
		||||
                } else if (tooManyLabels) {
 | 
			
		||||
                  message += `This PR affects ${finalLabels.length} different components/areas.`;
 | 
			
		||||
                  message += `This PR affects ${originalLabelCount} different components/areas.`;
 | 
			
		||||
                } else {
 | 
			
		||||
                  message += `This PR is too large with ${nonTestChanges} line changes (excluding tests).`;
 | 
			
		||||
                }
 | 
			
		||||
@@ -463,8 +466,8 @@ jobs:
 | 
			
		||||
            }
 | 
			
		||||
 | 
			
		||||
            // Handle reviews
 | 
			
		||||
            async function handleReviews(finalLabels) {
 | 
			
		||||
              const reviewMessages = generateReviewMessages(finalLabels);
 | 
			
		||||
            async function handleReviews(finalLabels, originalLabelCount) {
 | 
			
		||||
              const reviewMessages = generateReviewMessages(finalLabels, originalLabelCount);
 | 
			
		||||
              const hasReviewableLabels = finalLabels.some(label =>
 | 
			
		||||
                ['too-big', 'needs-codeowners'].includes(label)
 | 
			
		||||
              );
 | 
			
		||||
@@ -528,8 +531,8 @@ jobs:
 | 
			
		||||
            const apiData = await fetchApiData();
 | 
			
		||||
            const baseRef = context.payload.pull_request.base.ref;
 | 
			
		||||
 | 
			
		||||
            // Early exit for non-dev branches
 | 
			
		||||
            if (baseRef !== 'dev') {
 | 
			
		||||
            // Early exit for release and beta branches only
 | 
			
		||||
            if (baseRef === 'release' || baseRef === 'beta') {
 | 
			
		||||
              const branchLabels = await detectMergeBranch();
 | 
			
		||||
              const finalLabels = Array.from(branchLabels);
 | 
			
		||||
 | 
			
		||||
@@ -624,6 +627,7 @@ 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'];
 | 
			
		||||
@@ -632,7 +636,7 @@ jobs:
 | 
			
		||||
            console.log('Computed labels:', finalLabels.join(', '));
 | 
			
		||||
 | 
			
		||||
            // Handle reviews
 | 
			
		||||
            await handleReviews(finalLabels);
 | 
			
		||||
            await handleReviews(finalLabels, originalLabelCount);
 | 
			
		||||
 | 
			
		||||
            // Apply labels
 | 
			
		||||
            if (finalLabels.length > 0) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										111
									
								
								.github/workflows/ci-memory-impact-comment.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								.github/workflows/ci-memory-impact-comment.yml
									
									
									
									
										vendored
									
									
										Normal file
									
								
							@@ -0,0 +1,111 @@
 | 
			
		||||
---
 | 
			
		||||
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
 | 
			
		||||
							
								
								
									
										493
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										493
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@@ -114,7 +114,7 @@ jobs:
 | 
			
		||||
      matrix:
 | 
			
		||||
        python-version:
 | 
			
		||||
          - "3.11"
 | 
			
		||||
          - "3.14"
 | 
			
		||||
          - "3.13"
 | 
			
		||||
        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.14"
 | 
			
		||||
          - python-version: "3.13"
 | 
			
		||||
            os: windows-latest
 | 
			
		||||
          - python-version: "3.14"
 | 
			
		||||
          - python-version: "3.13"
 | 
			
		||||
            os: macOS-latest
 | 
			
		||||
    runs-on: ${{ matrix.os }}
 | 
			
		||||
    needs:
 | 
			
		||||
@@ -170,11 +170,16 @@ 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 }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
@@ -199,11 +204,16 @@ 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
 | 
			
		||||
 | 
			
		||||
  integration-tests:
 | 
			
		||||
    name: Run integration tests
 | 
			
		||||
@@ -241,7 +251,34 @@ jobs:
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          pytest -vv --no-cov --tb=native -n auto tests/integration/
 | 
			
		||||
 | 
			
		||||
  clang-tidy:
 | 
			
		||||
  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:
 | 
			
		||||
    name: ${{ matrix.name }}
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    needs:
 | 
			
		||||
@@ -259,22 +296,6 @@ 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
 | 
			
		||||
@@ -355,6 +376,166 @@ jobs:
 | 
			
		||||
        # yamllint disable-line rule:line-length
 | 
			
		||||
        if: always()
 | 
			
		||||
 | 
			
		||||
  clang-tidy-nosplit:
 | 
			
		||||
    name: Run script/clang-tidy for ESP32 Arduino
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
      - determine-jobs
 | 
			
		||||
    if: needs.determine-jobs.outputs.clang-tidy-mode == 'nosplit'
 | 
			
		||||
    env:
 | 
			
		||||
      GH_TOKEN: ${{ github.token }}
 | 
			
		||||
    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 --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
 | 
			
		||||
 | 
			
		||||
      - 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: 1
 | 
			
		||||
      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()
 | 
			
		||||
 | 
			
		||||
  test-build-components-splitter:
 | 
			
		||||
    name: Split components for intelligent grouping (40 weighted per batch)
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
@@ -521,6 +702,271 @@ 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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
 | 
			
		||||
        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@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
 | 
			
		||||
        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@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          name: memory-analysis-target
 | 
			
		||||
          path: ./memory-analysis
 | 
			
		||||
        continue-on-error: true
 | 
			
		||||
      - name: Download PR analysis JSON
 | 
			
		||||
        uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.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
 | 
			
		||||
@@ -530,11 +976,16 @@ jobs:
 | 
			
		||||
      - pylint
 | 
			
		||||
      - pytest
 | 
			
		||||
      - integration-tests
 | 
			
		||||
      - clang-tidy
 | 
			
		||||
      - clang-tidy-single
 | 
			
		||||
      - clang-tidy-nosplit
 | 
			
		||||
      - clang-tidy-split
 | 
			
		||||
      - 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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								.github/workflows/status-check-labels.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/status-check-labels.yml
									
									
									
									
										vendored
									
									
								
							@@ -14,6 +14,7 @@ jobs:
 | 
			
		||||
        label:
 | 
			
		||||
          - needs-docs
 | 
			
		||||
          - merge-after-release
 | 
			
		||||
          - chained-pr
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check for ${{ matrix.label }} label
 | 
			
		||||
        uses: actions/github-script@ed597411d8f924073f98dfc5c65a23a2325f34cd # v8.0.0
 | 
			
		||||
 
 | 
			
		||||
@@ -161,6 +161,7 @@ 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
 | 
			
		||||
@@ -200,6 +201,7 @@ 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,6 +62,40 @@ 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
 | 
			
		||||
@@ -185,7 +219,9 @@ def choose_upload_log_host(
 | 
			
		||||
            else:
 | 
			
		||||
                resolved.append(device)
 | 
			
		||||
        if not resolved:
 | 
			
		||||
            _LOGGER.error("All specified devices: %s could not be resolved.", defaults)
 | 
			
		||||
            raise EsphomeError(
 | 
			
		||||
                f"All specified devices {defaults} could not be resolved. Is the device connected to the network?"
 | 
			
		||||
            )
 | 
			
		||||
        return resolved
 | 
			
		||||
 | 
			
		||||
    # No devices specified, show interactive chooser
 | 
			
		||||
@@ -466,7 +502,9 @@ def write_cpp_file() -> int:
 | 
			
		||||
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
 | 
			
		||||
    from esphome import platformio_api
 | 
			
		||||
 | 
			
		||||
    _LOGGER.info("Compiling app...")
 | 
			
		||||
    # 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)
 | 
			
		||||
    rc = platformio_api.run_compile(config, CORE.verbose)
 | 
			
		||||
    if rc != 0:
 | 
			
		||||
        return rc
 | 
			
		||||
@@ -888,6 +926,54 @@ 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:
 | 
			
		||||
@@ -1003,6 +1089,7 @@ POST_CONFIG_ACTIONS = {
 | 
			
		||||
    "idedata": command_idedata,
 | 
			
		||||
    "rename": command_rename,
 | 
			
		||||
    "discover": command_discover,
 | 
			
		||||
    "analyze-memory": command_analyze_memory,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SIMPLE_CONFIG_ACTIONS = [
 | 
			
		||||
@@ -1288,6 +1375,14 @@ 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>.
 | 
			
		||||
    #
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										502
									
								
								esphome/analyze_memory/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										502
									
								
								esphome/analyze_memory/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,502 @@
 | 
			
		||||
"""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()
 | 
			
		||||
							
								
								
									
										6
									
								
								esphome/analyze_memory/__main__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										6
									
								
								esphome/analyze_memory/__main__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,6 @@
 | 
			
		||||
"""Main entry point for running the memory analyzer as a module."""
 | 
			
		||||
 | 
			
		||||
from .cli import main
 | 
			
		||||
 | 
			
		||||
if __name__ == "__main__":
 | 
			
		||||
    main()
 | 
			
		||||
							
								
								
									
										421
									
								
								esphome/analyze_memory/cli.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										421
									
								
								esphome/analyze_memory/cli.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,421 @@
 | 
			
		||||
"""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()
 | 
			
		||||
							
								
								
									
										1052
									
								
								esphome/analyze_memory/const.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1052
									
								
								esphome/analyze_memory/const.py
									
									
									
									
									
										Normal file
									
								
							
										
											
												File diff suppressed because it is too large
												Load Diff
											
										
									
								
							
							
								
								
									
										121
									
								
								esphome/analyze_memory/helpers.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										121
									
								
								esphome/analyze_memory/helpers.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,121 @@
 | 
			
		||||
"""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
 | 
			
		||||
@@ -16,7 +16,12 @@ from esphome.const import (
 | 
			
		||||
    CONF_UPDATE_INTERVAL,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import ID
 | 
			
		||||
from esphome.cpp_generator import MockObj, MockObjClass, TemplateArgsType
 | 
			
		||||
from esphome.cpp_generator import (
 | 
			
		||||
    LambdaExpression,
 | 
			
		||||
    MockObj,
 | 
			
		||||
    MockObjClass,
 | 
			
		||||
    TemplateArgsType,
 | 
			
		||||
)
 | 
			
		||||
from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
from esphome.util import Registry
 | 
			
		||||
@@ -87,6 +92,7 @@ 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)
 | 
			
		||||
@@ -97,9 +103,40 @@ 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 = {}
 | 
			
		||||
@@ -240,7 +277,9 @@ async def lambda_condition_to_code(
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    lambda_ = await cg.process_lambda(config, args, return_type=bool)
 | 
			
		||||
    return cg.new_Pvariable(condition_id, template_arg, lambda_)
 | 
			
		||||
    return new_lambda_pvariable(
 | 
			
		||||
        condition_id, lambda_, StatelessLambdaCondition, template_arg
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_condition(
 | 
			
		||||
@@ -406,7 +445,7 @@ async def lambda_action_to_code(
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
) -> MockObj:
 | 
			
		||||
    lambda_ = await cg.process_lambda(config, args, return_type=cg.void)
 | 
			
		||||
    return cg.new_Pvariable(action_id, template_arg, lambda_)
 | 
			
		||||
    return new_lambda_pvariable(action_id, lambda_, StatelessLambdaAction, template_arg)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_action(
 | 
			
		||||
 
 | 
			
		||||
@@ -62,6 +62,7 @@ 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 std::string &name) : AddressableLightEffect(name) {}
 | 
			
		||||
AdalightLightEffect::AdalightLightEffect(const char *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 std::string &name);
 | 
			
		||||
  AdalightLightEffect(const char *name);
 | 
			
		||||
 | 
			
		||||
  void start() override;
 | 
			
		||||
  void stop() override;
 | 
			
		||||
 
 | 
			
		||||
@@ -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.set_supports_current_temperature(true);
 | 
			
		||||
    traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
 | 
			
		||||
    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,10 +71,12 @@ SERVICE_ARG_NATIVE_TYPES = {
 | 
			
		||||
    "int": cg.int32,
 | 
			
		||||
    "float": float,
 | 
			
		||||
    "string": cg.std_string,
 | 
			
		||||
    "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),
 | 
			
		||||
    "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"),
 | 
			
		||||
}
 | 
			
		||||
CONF_ENCRYPTION = "encryption"
 | 
			
		||||
CONF_BATCH_DELAY = "batch_delay"
 | 
			
		||||
@@ -155,6 +157,17 @@ 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(
 | 
			
		||||
        {
 | 
			
		||||
@@ -222,6 +235,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
 | 
			
		||||
    _validate_api_config,
 | 
			
		||||
    _consume_api_sockets,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -246,6 +260,10 @@ 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")
 | 
			
		||||
 | 
			
		||||
@@ -253,6 +271,8 @@ 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 = []
 | 
			
		||||
@@ -266,8 +286,10 @@ async def to_code(config):
 | 
			
		||||
            trigger = cg.new_Pvariable(
 | 
			
		||||
                conf[CONF_TRIGGER_ID], templ, conf[CONF_ACTION], service_arg_names
 | 
			
		||||
            )
 | 
			
		||||
            cg.add(var.register_user_service(trigger))
 | 
			
		||||
            triggers.append(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")
 | 
			
		||||
 
 | 
			
		||||
@@ -506,7 +506,7 @@ message ListEntitiesLightResponse {
 | 
			
		||||
  string name = 3;
 | 
			
		||||
  reserved 4; // Deprecated: was string unique_id
 | 
			
		||||
 | 
			
		||||
  repeated ColorMode supported_color_modes = 12 [(container_pointer) = "std::set<light::ColorMode>"];
 | 
			
		||||
  repeated ColorMode supported_color_modes = 12 [(container_pointer_no_template) = "light::ColorModeMask"];
 | 
			
		||||
  // 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) = "std::set<climate::ClimateMode>"];
 | 
			
		||||
  repeated ClimateMode supported_modes = 7 [(container_pointer_no_template) = "climate::ClimateModeMask"];
 | 
			
		||||
  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) = "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"];
 | 
			
		||||
  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"];
 | 
			
		||||
  bool disabled_by_default = 18;
 | 
			
		||||
  string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"];
 | 
			
		||||
  EntityCategory entity_category = 20;
 | 
			
		||||
 
 | 
			
		||||
@@ -453,7 +453,6 @@ 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();
 | 
			
		||||
@@ -477,7 +476,8 @@ 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();
 | 
			
		||||
  msg.supported_color_modes = &traits.get_supported_color_modes_for_api_();
 | 
			
		||||
  // Pass pointer to ColorModeMask so the iterator can encode actual ColorMode enum values
 | 
			
		||||
  msg.supported_color_modes = &traits.get_supported_color_modes();
 | 
			
		||||
  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.push_back(effect->get_name());
 | 
			
		||||
      msg.effects.emplace_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_for_api_();
 | 
			
		||||
  msg.supported_modes = &traits.get_supported_modes();
 | 
			
		||||
  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_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_();
 | 
			
		||||
  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();
 | 
			
		||||
  return fill_and_encode_entity_info(climate, msg, ListEntitiesClimateResponse::MESSAGE_TYPE, conn, remaining_size,
 | 
			
		||||
                                     is_single);
 | 
			
		||||
}
 | 
			
		||||
@@ -1082,13 +1082,8 @@ 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) {
 | 
			
		||||
      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));
 | 
			
		||||
      }
 | 
			
		||||
      homeassistant::global_homeassistant_time->set_timezone(reinterpret_cast<const char *>(value.timezone),
 | 
			
		||||
                                                             value.timezone_len);
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
@@ -1577,7 +1572,13 @@ bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryption
 | 
			
		||||
  resp.success = false;
 | 
			
		||||
 | 
			
		||||
  psk_t psk{};
 | 
			
		||||
  if (base64_decode(msg.key, psk.data(), msg.key.size()) != psk.size()) {
 | 
			
		||||
  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()) {
 | 
			
		||||
    ESP_LOGW(TAG, "Invalid encryption key length");
 | 
			
		||||
  } else if (!this->parent_->save_noise_psk(psk, true)) {
 | 
			
		||||
    ESP_LOGW(TAG, "Failed to save encryption key");
 | 
			
		||||
 
 | 
			
		||||
@@ -70,4 +70,14 @@ 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;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -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 std::set<light::ColorMode> *supported_color_modes{};
 | 
			
		||||
  const light::ColorModeMask *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 std::set<climate::ClimateMode> *supported_modes{};
 | 
			
		||||
  const climate::ClimateModeMask *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 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{};
 | 
			
		||||
  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{};
 | 
			
		||||
  float visual_current_temperature_step{0.0f};
 | 
			
		||||
  bool supports_current_humidity{false};
 | 
			
		||||
  bool supports_target_humidity{false};
 | 
			
		||||
 
 | 
			
		||||
@@ -468,6 +468,31 @@ 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
 | 
			
		||||
@@ -482,27 +507,21 @@ bool APIServer::save_noise_psk(psk_t psk, bool make_active) {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  SavedNoisePsk new_saved_psk{psk};
 | 
			
		||||
  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;
 | 
			
		||||
  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);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -53,6 +53,7 @@ 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
 | 
			
		||||
@@ -124,8 +125,14 @@ 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
 | 
			
		||||
@@ -174,6 +181,10 @@ 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,8 +53,14 @@ 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>
 | 
			
		||||
@@ -86,8 +92,14 @@ 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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -3,10 +3,12 @@
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/macros.h"
 | 
			
		||||
#include "esphome/core/string_ref.h"
 | 
			
		||||
 | 
			
		||||
#include <cassert>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
#include <type_traits>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
#ifdef ESPHOME_LOG_HAS_VERY_VERBOSE
 | 
			
		||||
@@ -159,22 +161,6 @@ class ProtoVarInt {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  void encode(std::vector<uint8_t> &out) {
 | 
			
		||||
    uint64_t val = this->value_;
 | 
			
		||||
    if (val <= 0x7F) {
 | 
			
		||||
      out.push_back(val);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    while (val) {
 | 
			
		||||
      uint8_t temp = val & 0x7F;
 | 
			
		||||
      val >>= 7;
 | 
			
		||||
      if (val) {
 | 
			
		||||
        out.push_back(temp | 0x80);
 | 
			
		||||
      } else {
 | 
			
		||||
        out.push_back(temp);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  uint64_t value_;
 | 
			
		||||
@@ -233,8 +219,87 @@ class ProtoWriteBuffer {
 | 
			
		||||
 public:
 | 
			
		||||
  ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {}
 | 
			
		||||
  void write(uint8_t value) { this->buffer_->push_back(value); }
 | 
			
		||||
  void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); }
 | 
			
		||||
  void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); }
 | 
			
		||||
 | 
			
		||||
  // Single implementation that all overloads delegate to
 | 
			
		||||
  // Mark noinline to prevent code bloat from inlining into every caller
 | 
			
		||||
  __attribute__((noinline)) void encode_varint(uint64_t value) {
 | 
			
		||||
    auto buffer = this->buffer_;
 | 
			
		||||
    size_t start = buffer->size();
 | 
			
		||||
 | 
			
		||||
    // Fast paths for common cases (1-3 bytes)
 | 
			
		||||
    if (ESPHOME_LIKELY(value < (1ULL << 7))) {
 | 
			
		||||
      // 1 byte - very common for field IDs and small lengths
 | 
			
		||||
      buffer->resize(start + 1);
 | 
			
		||||
      buffer->data()[start] = static_cast<uint8_t>(value);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    uint8_t *p;
 | 
			
		||||
    if (ESPHOME_LIKELY(value < (1ULL << 14))) {
 | 
			
		||||
      // 2 bytes - common for medium field IDs and lengths
 | 
			
		||||
      buffer->resize(start + 2);
 | 
			
		||||
      p = buffer->data() + start;
 | 
			
		||||
      p[0] = (value & 0x7F) | 0x80;
 | 
			
		||||
      p[1] = (value >> 7) & 0x7F;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    if (value < (1ULL << 21)) {
 | 
			
		||||
      // 3 bytes - rare
 | 
			
		||||
      buffer->resize(start + 3);
 | 
			
		||||
      p = buffer->data() + start;
 | 
			
		||||
      p[0] = (value & 0x7F) | 0x80;
 | 
			
		||||
      p[1] = ((value >> 7) & 0x7F) | 0x80;
 | 
			
		||||
      p[2] = (value >> 14) & 0x7F;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Rare case: 4-10 byte values - calculate size from bit position
 | 
			
		||||
    // Value is guaranteed >= (1ULL << 21), so CLZ is safe (non-zero)
 | 
			
		||||
    uint32_t size;
 | 
			
		||||
#if defined(__GNUC__) || defined(__clang__)
 | 
			
		||||
    // Use compiler intrinsic for efficient bit position lookup
 | 
			
		||||
    size = (64 - __builtin_clzll(value) + 6) / 7;
 | 
			
		||||
#else
 | 
			
		||||
    // Fallback for compilers without __builtin_clzll
 | 
			
		||||
    if (value < (1ULL << 28)) {
 | 
			
		||||
      size = 4;
 | 
			
		||||
    } else if (value < (1ULL << 35)) {
 | 
			
		||||
      size = 5;
 | 
			
		||||
    } else if (value < (1ULL << 42)) {
 | 
			
		||||
      size = 6;
 | 
			
		||||
    } else if (value < (1ULL << 49)) {
 | 
			
		||||
      size = 7;
 | 
			
		||||
    } else if (value < (1ULL << 56)) {
 | 
			
		||||
      size = 8;
 | 
			
		||||
    } else if (value < (1ULL << 63)) {
 | 
			
		||||
      size = 9;
 | 
			
		||||
    } else {
 | 
			
		||||
      size = 10;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    buffer->resize(start + size);
 | 
			
		||||
    p = buffer->data() + start;
 | 
			
		||||
    size_t bytes = 0;
 | 
			
		||||
    while (value) {
 | 
			
		||||
      uint8_t temp = value & 0x7F;
 | 
			
		||||
      value >>= 7;
 | 
			
		||||
      p[bytes++] = value ? temp | 0x80 : temp;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Common case: uint32_t values (field IDs, lengths, most integers)
 | 
			
		||||
  void encode_varint(uint32_t value) { this->encode_varint(static_cast<uint64_t>(value)); }
 | 
			
		||||
 | 
			
		||||
  // size_t overload (only enabled if size_t is distinct from uint32_t and uint64_t)
 | 
			
		||||
  template<typename T>
 | 
			
		||||
  void encode_varint(T value) requires(std::is_same_v<T, size_t> && !std::is_same_v<size_t, uint32_t> &&
 | 
			
		||||
                                       !std::is_same_v<size_t, uint64_t>) {
 | 
			
		||||
    this->encode_varint(static_cast<uint64_t>(value));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Rare case: ProtoVarInt wrapper
 | 
			
		||||
  void encode_varint(ProtoVarInt value) { this->encode_varint(value.as_uint64()); }
 | 
			
		||||
  /**
 | 
			
		||||
   * Encode a field key (tag/wire type combination).
 | 
			
		||||
   *
 | 
			
		||||
@@ -249,14 +314,14 @@ class ProtoWriteBuffer {
 | 
			
		||||
   */
 | 
			
		||||
  void encode_field_raw(uint32_t field_id, uint32_t type) {
 | 
			
		||||
    uint32_t val = (field_id << 3) | (type & WIRE_TYPE_MASK);
 | 
			
		||||
    this->encode_varint_raw(val);
 | 
			
		||||
    this->encode_varint(val);
 | 
			
		||||
  }
 | 
			
		||||
  void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) {
 | 
			
		||||
    if (len == 0 && !force)
 | 
			
		||||
      return;
 | 
			
		||||
 | 
			
		||||
    this->encode_field_raw(field_id, 2);  // type 2: Length-delimited string
 | 
			
		||||
    this->encode_varint_raw(len);
 | 
			
		||||
    this->encode_varint(len);
 | 
			
		||||
 | 
			
		||||
    // Using resize + memcpy instead of insert provides significant performance improvement:
 | 
			
		||||
    // ~10-11x faster for 16-32 byte strings, ~3x faster for 64-byte strings
 | 
			
		||||
@@ -278,13 +343,13 @@ class ProtoWriteBuffer {
 | 
			
		||||
    if (value == 0 && !force)
 | 
			
		||||
      return;
 | 
			
		||||
    this->encode_field_raw(field_id, 0);  // type 0: Varint - uint32
 | 
			
		||||
    this->encode_varint_raw(value);
 | 
			
		||||
    this->encode_varint(value);
 | 
			
		||||
  }
 | 
			
		||||
  void encode_uint64(uint32_t field_id, uint64_t value, bool force = false) {
 | 
			
		||||
    if (value == 0 && !force)
 | 
			
		||||
      return;
 | 
			
		||||
    this->encode_field_raw(field_id, 0);  // type 0: Varint - uint64
 | 
			
		||||
    this->encode_varint_raw(ProtoVarInt(value));
 | 
			
		||||
    this->encode_varint(value);
 | 
			
		||||
  }
 | 
			
		||||
  void encode_bool(uint32_t field_id, bool value, bool force = false) {
 | 
			
		||||
    if (!value && !force)
 | 
			
		||||
 
 | 
			
		||||
@@ -11,23 +11,58 @@ 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) {
 | 
			
		||||
  return std::vector<bool>(arg.bool_array.begin(), arg.bool_array.end());
 | 
			
		||||
  std::vector<bool> result;
 | 
			
		||||
  result.reserve(arg.bool_array.size());
 | 
			
		||||
  result.insert(result.end(), arg.bool_array.begin(), arg.bool_array.end());
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
template<> std::vector<int32_t> get_execute_arg_value<std::vector<int32_t>>(const ExecuteServiceArgument &arg) {
 | 
			
		||||
  return std::vector<int32_t>(arg.int_array.begin(), arg.int_array.end());
 | 
			
		||||
  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;
 | 
			
		||||
}
 | 
			
		||||
template<> std::vector<float> get_execute_arg_value<std::vector<float>>(const ExecuteServiceArgument &arg) {
 | 
			
		||||
  return std::vector<float>(arg.float_array.begin(), arg.float_array.end());
 | 
			
		||||
  std::vector<float> result;
 | 
			
		||||
  result.reserve(arg.float_array.size());
 | 
			
		||||
  result.insert(result.end(), arg.float_array.begin(), arg.float_array.end());
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
template<> std::vector<std::string> get_execute_arg_value<std::vector<std::string>>(const ExecuteServiceArgument &arg) {
 | 
			
		||||
  return std::vector<std::string>(arg.string_array.begin(), arg.string_array.end());
 | 
			
		||||
  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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
@@ -39,4 +74,18 @@ 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,6 +6,9 @@ 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;
 | 
			
		||||
@@ -31,53 +34,63 @@ void BangBangClimate::setup() {
 | 
			
		||||
    restore->to_call(this).perform();
 | 
			
		||||
  } else {
 | 
			
		||||
    // restore from defaults, change_away handles those for us
 | 
			
		||||
    if (supports_cool_ && supports_heat_) {
 | 
			
		||||
    if (this->supports_cool_ && this->supports_heat_) {
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_HEAT_COOL;
 | 
			
		||||
    } else if (supports_cool_) {
 | 
			
		||||
    } else if (this->supports_cool_) {
 | 
			
		||||
      this->mode = climate::CLIMATE_MODE_COOL;
 | 
			
		||||
    } else if (supports_heat_) {
 | 
			
		||||
    } else if (this->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.set_supports_current_temperature(true);
 | 
			
		||||
  if (this->humidity_sensor_ != nullptr)
 | 
			
		||||
    traits.set_supports_current_humidity(true);
 | 
			
		||||
  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_supported_modes({
 | 
			
		||||
      climate::CLIMATE_MODE_OFF,
 | 
			
		||||
  });
 | 
			
		||||
  if (supports_cool_)
 | 
			
		||||
  if (this->supports_cool_) {
 | 
			
		||||
    traits.add_supported_mode(climate::CLIMATE_MODE_COOL);
 | 
			
		||||
  if (supports_heat_)
 | 
			
		||||
  }
 | 
			
		||||
  if (this->supports_heat_) {
 | 
			
		||||
    traits.add_supported_mode(climate::CLIMATE_MODE_HEAT);
 | 
			
		||||
  if (supports_cool_ && supports_heat_)
 | 
			
		||||
  }
 | 
			
		||||
  if (this->supports_cool_ && this->supports_heat_) {
 | 
			
		||||
    traits.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);
 | 
			
		||||
  traits.set_supports_two_point_target_temperature(true);
 | 
			
		||||
  if (supports_away_) {
 | 
			
		||||
  }
 | 
			
		||||
  if (this->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);
 | 
			
		||||
@@ -122,6 +135,7 @@ 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
 | 
			
		||||
@@ -166,6 +180,7 @@ 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;
 | 
			
		||||
@@ -176,22 +191,26 @@ 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_; }
 | 
			
		||||
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_cool(bool supports_cool) { this->supports_cool_ = supports_cool; }
 | 
			
		||||
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,14 +25,15 @@ 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;
 | 
			
		||||
@@ -56,16 +57,10 @@ class BangBangClimate : public climate::Climate, public Component {
 | 
			
		||||
   *
 | 
			
		||||
   * In idle mode, the controller is assumed to have both heating and cooling disabled.
 | 
			
		||||
   */
 | 
			
		||||
  Trigger<> *idle_trigger_;
 | 
			
		||||
  Trigger<> *idle_trigger_{nullptr};
 | 
			
		||||
  /** The trigger to call when the controller should switch to cooling mode.
 | 
			
		||||
   */
 | 
			
		||||
  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};
 | 
			
		||||
  Trigger<> *cool_trigger_{nullptr};
 | 
			
		||||
  /** 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
 | 
			
		||||
@@ -73,15 +68,23 @@ 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};
 | 
			
		||||
 | 
			
		||||
  BangBangClimateTargetTempConfig normal_config_{};
 | 
			
		||||
  /** 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_{};
 | 
			
		||||
  BangBangClimateTargetTempConfig away_config_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -99,9 +99,8 @@ enum BedjetCommand : uint8_t {
 | 
			
		||||
 | 
			
		||||
static const uint8_t BEDJET_FAN_SPEED_COUNT = 20;
 | 
			
		||||
 | 
			
		||||
static const char *const BEDJET_FAN_STEP_NAMES[BEDJET_FAN_SPEED_COUNT] = BEDJET_FAN_STEP_NAMES_;
 | 
			
		||||
static constexpr 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,8 +33,7 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli
 | 
			
		||||
 | 
			
		||||
  climate::ClimateTraits traits() override {
 | 
			
		||||
    auto traits = climate::ClimateTraits();
 | 
			
		||||
    traits.set_supports_action(true);
 | 
			
		||||
    traits.set_supports_current_temperature(true);
 | 
			
		||||
    traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION | climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
 | 
			
		||||
    traits.set_supported_modes({
 | 
			
		||||
        climate::CLIMATE_MODE_OFF,
 | 
			
		||||
        climate::CLIMATE_MODE_HEAT,
 | 
			
		||||
@@ -44,7 +43,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_SET);
 | 
			
		||||
    traits.set_supported_custom_fan_modes(BEDJET_FAN_STEP_NAMES);
 | 
			
		||||
    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,6 +155,7 @@ 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__)
 | 
			
		||||
@@ -264,20 +265,31 @@ 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.extend(
 | 
			
		||||
            (conf[CONF_DELAY], conf[CONF_TIME_OFF], conf[CONF_TIME_ON])
 | 
			
		||||
            for conf in config
 | 
			
		||||
        )
 | 
			
		||||
    else:
 | 
			
		||||
        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,
 | 
			
		||||
        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]),
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
            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,
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
        ]
 | 
			
		||||
    var = cg.new_Pvariable(filter_id, timings)
 | 
			
		||||
    await cg.register_component(var, {})
 | 
			
		||||
    return var
 | 
			
		||||
@@ -288,7 +300,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 cg.new_Pvariable(filter_id, lambda_)
 | 
			
		||||
    return automation.new_lambda_pvariable(filter_id, lambda_, StatelessLambdaFilter)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_filter(
 | 
			
		||||
 
 | 
			
		||||
@@ -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::vector<MultiClickTriggerEvent> timing)
 | 
			
		||||
      : parent_(parent), timing_(std::move(timing)) {}
 | 
			
		||||
  explicit MultiClickTrigger(BinarySensor *parent, std::initializer_list<MultiClickTriggerEvent> timing)
 | 
			
		||||
      : parent_(parent), timing_(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_;
 | 
			
		||||
  std::vector<MultiClickTriggerEvent> timing_;
 | 
			
		||||
  FixedVector<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(const std::vector<Filter *> &filters) {
 | 
			
		||||
void BinarySensor::add_filters(std::initializer_list<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 <vector>
 | 
			
		||||
#include <initializer_list>
 | 
			
		||||
 | 
			
		||||
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(const std::vector<Filter *> &filters);
 | 
			
		||||
  void add_filters(std::initializer_list<Filter *> filters);
 | 
			
		||||
 | 
			
		||||
  // ========== INTERNAL METHODS ==========
 | 
			
		||||
  // (In most use cases you won't need these)
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
#include "filter.h"
 | 
			
		||||
 | 
			
		||||
#include "binary_sensor.h"
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
@@ -68,7 +67,7 @@ float DelayedOffFilter::get_setup_priority() const { return setup_priority::HARD
 | 
			
		||||
 | 
			
		||||
optional<bool> InvertFilter::new_value(bool value) { return !value; }
 | 
			
		||||
 | 
			
		||||
AutorepeatFilter::AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings) : timings_(std::move(timings)) {}
 | 
			
		||||
AutorepeatFilter::AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings) : timings_(timings) {}
 | 
			
		||||
 | 
			
		||||
optional<bool> AutorepeatFilter::new_value(bool value) {
 | 
			
		||||
  if (value) {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,8 +4,6 @@
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
namespace binary_sensor {
 | 
			
		||||
@@ -82,11 +80,6 @@ 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;
 | 
			
		||||
@@ -94,7 +87,7 @@ struct AutorepeatFilterTiming {
 | 
			
		||||
 | 
			
		||||
class AutorepeatFilter : public Filter, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit AutorepeatFilter(std::vector<AutorepeatFilterTiming> timings);
 | 
			
		||||
  explicit AutorepeatFilter(std::initializer_list<AutorepeatFilterTiming> timings);
 | 
			
		||||
 | 
			
		||||
  optional<bool> new_value(bool value) override;
 | 
			
		||||
 | 
			
		||||
@@ -104,7 +97,7 @@ class AutorepeatFilter : public Filter, public Component {
 | 
			
		||||
  void next_timing_();
 | 
			
		||||
  void next_value_(bool val);
 | 
			
		||||
 | 
			
		||||
  std::vector<AutorepeatFilterTiming> timings_;
 | 
			
		||||
  FixedVector<AutorepeatFilterTiming> timings_;
 | 
			
		||||
  uint8_t active_timing_{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -118,6 +111,21 @@ 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,8 +96,11 @@ 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); }
 | 
			
		||||
@@ -106,14 +109,18 @@ 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::function<std::vector<uint8_t>(Ts...)> func) {
 | 
			
		||||
    this->value_template_ = std::move(func);
 | 
			
		||||
    has_simple_value_ = false;
 | 
			
		||||
  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_simple(const std::vector<uint8_t> &value) {
 | 
			
		||||
    this->value_simple_ = value;
 | 
			
		||||
    has_simple_value_ = true;
 | 
			
		||||
    if (!this->has_simple_value_) {
 | 
			
		||||
      this->construct_simple_value_();
 | 
			
		||||
    }
 | 
			
		||||
    this->value_.simple = value;
 | 
			
		||||
    this->has_simple_value_ = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override {}
 | 
			
		||||
@@ -121,7 +128,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_(x...);
 | 
			
		||||
    auto value = this->has_simple_value_ ? this->value_.simple : this->value_.template_func(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...);
 | 
			
		||||
@@ -194,10 +201,22 @@ 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;
 | 
			
		||||
  std::vector<uint8_t> value_simple_;
 | 
			
		||||
  std::function<std::vector<uint8_t>(Ts...)> value_template_{};
 | 
			
		||||
  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_;
 | 
			
		||||
  espbt::ESPBTUUID service_uuid_;
 | 
			
		||||
  espbt::ESPBTUUID char_uuid_;
 | 
			
		||||
  std::tuple<Ts...> var_{};
 | 
			
		||||
@@ -213,9 +232,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_(x...);
 | 
			
		||||
      passkey = this->value_.template_func(x...);
 | 
			
		||||
    }
 | 
			
		||||
    if (passkey > 999999)
 | 
			
		||||
      return;
 | 
			
		||||
@@ -224,21 +243,23 @@ template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...
 | 
			
		||||
    esp_ble_passkey_reply(remote_bda, true, passkey);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_value_template(std::function<uint32_t(Ts...)> func) {
 | 
			
		||||
    this->value_template_ = std::move(func);
 | 
			
		||||
    has_simple_value_ = false;
 | 
			
		||||
  void set_value_template(uint32_t (*func)(Ts...)) {
 | 
			
		||||
    this->value_.template_func = func;
 | 
			
		||||
    this->has_simple_value_ = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_value_simple(const uint32_t &value) {
 | 
			
		||||
    this->value_simple_ = value;
 | 
			
		||||
    has_simple_value_ = true;
 | 
			
		||||
    this->value_.simple = value;
 | 
			
		||||
    this->has_simple_value_ = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  BLEClient *parent_{nullptr};
 | 
			
		||||
  bool has_simple_value_ = true;
 | 
			
		||||
  uint32_t value_simple_{0};
 | 
			
		||||
  std::function<uint32_t(Ts...)> value_template_{};
 | 
			
		||||
  union {
 | 
			
		||||
    uint32_t simple;
 | 
			
		||||
    uint32_t (*template_func)(Ts...);
 | 
			
		||||
  } value_{.simple = 0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class BLEClientNumericComparisonReplyAction : public Action<Ts...> {
 | 
			
		||||
@@ -249,27 +270,29 @@ 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_(x...));
 | 
			
		||||
      esp_ble_confirm_reply(remote_bda, this->value_.template_func(x...));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_value_template(std::function<bool(Ts...)> func) {
 | 
			
		||||
    this->value_template_ = std::move(func);
 | 
			
		||||
    has_simple_value_ = false;
 | 
			
		||||
  void set_value_template(bool (*func)(Ts...)) {
 | 
			
		||||
    this->value_.template_func = func;
 | 
			
		||||
    this->has_simple_value_ = false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_value_simple(const bool &value) {
 | 
			
		||||
    this->value_simple_ = value;
 | 
			
		||||
    has_simple_value_ = true;
 | 
			
		||||
    this->value_.simple = value;
 | 
			
		||||
    this->has_simple_value_ = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  BLEClient *parent_{nullptr};
 | 
			
		||||
  bool has_simple_value_ = true;
 | 
			
		||||
  bool value_simple_{false};
 | 
			
		||||
  std::function<bool(Ts...)> value_template_{};
 | 
			
		||||
  union {
 | 
			
		||||
    bool simple;
 | 
			
		||||
    bool (*template_func)(Ts...);
 | 
			
		||||
  } value_{.simple = false};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...> {
 | 
			
		||||
 
 | 
			
		||||
@@ -117,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->data_to_value_func_.has_value()) {
 | 
			
		||||
  if (this->has_data_to_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,8 +15,6 @@ 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;
 | 
			
		||||
@@ -33,13 +31,17 @@ 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(data_to_value_t &&lambda) { this->data_to_value_func_ = lambda; }
 | 
			
		||||
  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_enable_notify(bool notify) { this->notify_ = notify; }
 | 
			
		||||
  uint16_t handle;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  float parse_data_(uint8_t *value, uint16_t value_len);
 | 
			
		||||
  optional<data_to_value_t> data_to_value_func_{};
 | 
			
		||||
  bool has_data_to_value_{false};
 | 
			
		||||
  float (*data_to_value_func_)(const std::vector<uint8_t> &){};
 | 
			
		||||
  bool notify_;
 | 
			
		||||
  espbt::ESPBTUUID service_uuid_;
 | 
			
		||||
  espbt::ESPBTUUID char_uuid_;
 | 
			
		||||
 
 | 
			
		||||
@@ -155,16 +155,12 @@ 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];
 | 
			
		||||
    if (connection->get_address() == address)
 | 
			
		||||
    uint64_t conn_addr = connection->get_address();
 | 
			
		||||
 | 
			
		||||
    if (conn_addr == address)
 | 
			
		||||
      return connection;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!reserve)
 | 
			
		||||
    return nullptr;
 | 
			
		||||
 | 
			
		||||
  for (uint8_t i = 0; i < this->connection_count_; i++) {
 | 
			
		||||
    auto *connection = this->connections_[i];
 | 
			
		||||
    if (connection->get_address() == 0) {
 | 
			
		||||
    if (reserve && conn_addr == 0) {
 | 
			
		||||
      connection->send_service_ = INIT_SENDING_SERVICES;
 | 
			
		||||
      connection->set_address(address);
 | 
			
		||||
      // All connections must start at INIT
 | 
			
		||||
@@ -175,7 +171,6 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
 | 
			
		||||
      return connection;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return nullptr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,42 @@ 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());
 | 
			
		||||
@@ -50,47 +86,46 @@ 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 is not supported by this device!", LOG_STR_ARG(climate_mode_to_string(mode)));
 | 
			
		||||
      ESP_LOGW(TAG, "  Mode %s not supported", 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 is not supported by this device!", custom_fan_mode.c_str());
 | 
			
		||||
      ESP_LOGW(TAG, "  Fan Mode %s not supported", 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 is not supported by this device!",
 | 
			
		||||
               LOG_STR_ARG(climate_fan_mode_to_string(fan_mode)));
 | 
			
		||||
      ESP_LOGW(TAG, "  Fan Mode %s not supported", 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 is not supported by this device!", custom_preset.c_str());
 | 
			
		||||
      ESP_LOGW(TAG, "  Preset %s not supported", 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 is not supported by this device!", LOG_STR_ARG(climate_preset_to_string(preset)));
 | 
			
		||||
      ESP_LOGW(TAG, "  Preset %s not supported", 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 is not supported by this device!",
 | 
			
		||||
               LOG_STR_ARG(climate_swing_mode_to_string(swing_mode)));
 | 
			
		||||
      ESP_LOGW(TAG, "  Swing Mode %s not supported", LOG_STR_ARG(climate_swing_mode_to_string(swing_mode)));
 | 
			
		||||
      this->swing_mode_.reset();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -99,159 +134,127 @@ 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 for this device!");
 | 
			
		||||
      ESP_LOGW(TAG, "  Cannot set low/high target temperature");
 | 
			
		||||
      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 low must not be NAN!");
 | 
			
		||||
    ESP_LOGW(TAG, "  Target temperature high 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 smaller than target temperature high %.2f!", low, high);
 | 
			
		||||
      ESP_LOGW(TAG, "  Target temperature low %.2f must be less 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) {
 | 
			
		||||
  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());
 | 
			
		||||
  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;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  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) {
 | 
			
		||||
  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 {
 | 
			
		||||
    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());
 | 
			
		||||
  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();
 | 
			
		||||
  } 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) {
 | 
			
		||||
  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 {
 | 
			
		||||
    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());
 | 
			
		||||
  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();
 | 
			
		||||
  } 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) {
 | 
			
		||||
  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());
 | 
			
		||||
  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;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGW(TAG, "'%s' - Unrecognized swing mode %s", this->parent_->get_name().c_str(), swing_mode.c_str());
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -259,59 +262,71 @@ 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<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_; }
 | 
			
		||||
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<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; }
 | 
			
		||||
 | 
			
		||||
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;
 | 
			
		||||
@@ -336,6 +351,7 @@ 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)
 | 
			
		||||
@@ -369,12 +385,14 @@ 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<std::string> vec{supported.begin(), supported.end()};
 | 
			
		||||
    for (size_t i = 0; i < vec.size(); i++) {
 | 
			
		||||
      if (vec[i] == custom_fan_mode) {
 | 
			
		||||
    // std::vector maintains insertion order
 | 
			
		||||
    size_t i = 0;
 | 
			
		||||
    for (const auto &mode : supported) {
 | 
			
		||||
      if (mode == custom_fan_mode) {
 | 
			
		||||
        state.custom_fan_mode = i;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      i++;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.get_supports_presets() && preset.has_value()) {
 | 
			
		||||
@@ -384,12 +402,14 @@ 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<std::string> vec{supported.begin(), supported.end()};
 | 
			
		||||
    for (size_t i = 0; i < vec.size(); i++) {
 | 
			
		||||
      if (vec[i] == custom_preset) {
 | 
			
		||||
    // std::vector maintains insertion order
 | 
			
		||||
    size_t i = 0;
 | 
			
		||||
    for (const auto &preset : supported) {
 | 
			
		||||
      if (preset == custom_preset) {
 | 
			
		||||
        state.custom_preset = i;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      i++;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.get_supports_swing_modes()) {
 | 
			
		||||
@@ -398,6 +418,7 @@ 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();
 | 
			
		||||
@@ -469,16 +490,20 @@ 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;
 | 
			
		||||
}
 | 
			
		||||
@@ -499,17 +524,28 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) {
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
 | 
			
		||||
    call.set_target_humidity(this->target_humidity);
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.get_supports_fan_modes() || !traits.get_supported_custom_fan_modes().empty()) {
 | 
			
		||||
  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)) {
 | 
			
		||||
    call.set_fan_mode(this->fan_mode);
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.get_supports_presets() || !traits.get_supported_custom_presets().empty()) {
 | 
			
		||||
  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)) {
 | 
			
		||||
    call.set_preset(this->preset);
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.get_supports_swing_modes()) {
 | 
			
		||||
  if (traits.supports_swing_mode(this->swing_mode)) {
 | 
			
		||||
    call.set_swing_mode(this->swing_mode);
 | 
			
		||||
  }
 | 
			
		||||
  return call;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ClimateDeviceRestoreState::apply(Climate *climate) {
 | 
			
		||||
  auto traits = climate->get_traits();
 | 
			
		||||
  climate->mode = this->mode;
 | 
			
		||||
@@ -523,29 +559,25 @@ void ClimateDeviceRestoreState::apply(Climate *climate) {
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
 | 
			
		||||
    climate->target_humidity = this->target_humidity;
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.get_supports_fan_modes() && !this->uses_custom_fan_mode) {
 | 
			
		||||
  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)) {
 | 
			
		||||
    climate->fan_mode = this->fan_mode;
 | 
			
		||||
    climate->custom_fan_mode.reset();
 | 
			
		||||
  }
 | 
			
		||||
  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];
 | 
			
		||||
  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_supports_presets() && !this->uses_custom_preset) {
 | 
			
		||||
  } else if (traits.supports_preset(this->preset)) {
 | 
			
		||||
    climate->preset = this->preset;
 | 
			
		||||
    climate->custom_preset.reset();
 | 
			
		||||
  }
 | 
			
		||||
  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()) {
 | 
			
		||||
  if (traits.supports_swing_mode(this->swing_mode)) {
 | 
			
		||||
    climate->swing_mode = this->swing_mode;
 | 
			
		||||
  }
 | 
			
		||||
  climate->publish_state();
 | 
			
		||||
@@ -579,68 +611,68 @@ void Climate::dump_traits_(const char *tag) {
 | 
			
		||||
  auto traits = this->get_traits();
 | 
			
		||||
  ESP_LOGCONFIG(tag, "ClimateTraits:");
 | 
			
		||||
  ESP_LOGCONFIG(tag,
 | 
			
		||||
                "  [x] Visual settings:\n"
 | 
			
		||||
                "      - Min temperature: %.1f\n"
 | 
			
		||||
                "      - Max temperature: %.1f\n"
 | 
			
		||||
                "      - Temperature step:\n"
 | 
			
		||||
                "          Target: %.1f",
 | 
			
		||||
                "  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, "  [x] Supports two-point target temperature");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supports two-point target temperature");
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE)) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supports current temperature");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supports current temperature");
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY)) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supports target humidity");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supports target humidity");
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY)) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supports current humidity");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supports current humidity");
 | 
			
		||||
  }
 | 
			
		||||
  if (traits.has_feature_flags(climate::CLIMATE_SUPPORTS_ACTION)) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supports action");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  Supports action");
 | 
			
		||||
  }
 | 
			
		||||
  if (!traits.get_supported_modes().empty()) {
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  [x] Supported modes:");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  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, "  [x] Supported fan modes:");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  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, "  [x] Supported custom fan modes:");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  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, "  [x] Supported presets:");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  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, "  [x] Supported custom presets:");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  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, "  [x] Supported swing modes:");
 | 
			
		||||
    ESP_LOGCONFIG(tag, "  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,6 +33,7 @@ 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);
 | 
			
		||||
@@ -93,30 +94,31 @@ 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<std::string> &get_custom_fan_mode() const;
 | 
			
		||||
  const optional<ClimatePreset> &get_preset() const;
 | 
			
		||||
  const optional<std::string> &get_custom_fan_mode() 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<std::string> custom_fan_mode_;
 | 
			
		||||
  optional<ClimatePreset> preset_;
 | 
			
		||||
  optional<std::string> custom_fan_mode_;
 | 
			
		||||
  optional<std::string> custom_preset_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -169,47 +171,6 @@ 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.
 | 
			
		||||
   *
 | 
			
		||||
@@ -251,6 +212,47 @@ 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,6 +7,7 @@ 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,
 | 
			
		||||
@@ -24,7 +25,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
 | 
			
		||||
  CLIMATE_MODE_AUTO = 6  // Update ClimateModeMask in climate_traits.h if adding values after this
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// Enum for the current action of the climate device. Values match those of ClimateMode.
 | 
			
		||||
@@ -43,6 +44,7 @@ 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,
 | 
			
		||||
@@ -63,10 +65,11 @@ 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,
 | 
			
		||||
  CLIMATE_FAN_QUIET = 9,  // Update ClimateFanModeMask in climate_traits.h if adding values after this
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// 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,
 | 
			
		||||
@@ -75,10 +78,11 @@ 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,
 | 
			
		||||
  CLIMATE_SWING_HORIZONTAL = 3,  // Update ClimateSwingModeMask in climate_traits.h if adding values after this
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// 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,
 | 
			
		||||
@@ -95,7 +99,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,
 | 
			
		||||
  CLIMATE_PRESET_ACTIVITY = 7,  // Update ClimatePresetMask in climate_traits.h if adding values after this
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum ClimateFeature : uint32_t {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,33 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include "climate_mode.h"
 | 
			
		||||
#include <set>
 | 
			
		||||
#include "esphome/core/finite_set_mask.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
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:
 | 
			
		||||
@@ -107,90 +121,60 @@ class ClimateTraits {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_supported_modes(std::set<ClimateMode> modes) { this->supported_modes_ = std::move(modes); }
 | 
			
		||||
  void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = 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 std::set<ClimateMode> &get_supported_modes() const { return this->supported_modes_; }
 | 
			
		||||
  const ClimateModeMask &get_supported_modes() const { return this->supported_modes_; }
 | 
			
		||||
 | 
			
		||||
  void set_supported_fan_modes(std::set<ClimateFanMode> modes) { this->supported_fan_modes_ = std::move(modes); }
 | 
			
		||||
  void set_supported_fan_modes(ClimateFanModeMask modes) { this->supported_fan_modes_ = 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_.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); }
 | 
			
		||||
  void add_supported_custom_fan_mode(const std::string &mode) { this->supported_custom_fan_modes_.push_back(mode); }
 | 
			
		||||
  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 std::set<ClimateFanMode> &get_supported_fan_modes() const { return this->supported_fan_modes_; }
 | 
			
		||||
  const ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_fan_modes_; }
 | 
			
		||||
 | 
			
		||||
  void set_supported_custom_fan_modes(std::set<std::string> supported_custom_fan_modes) {
 | 
			
		||||
  void set_supported_custom_fan_modes(std::vector<std::string> supported_custom_fan_modes) {
 | 
			
		||||
    this->supported_custom_fan_modes_ = std::move(supported_custom_fan_modes);
 | 
			
		||||
  }
 | 
			
		||||
  const std::set<std::string> &get_supported_custom_fan_modes() const { return this->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_; }
 | 
			
		||||
  bool supports_custom_fan_mode(const std::string &custom_fan_mode) const {
 | 
			
		||||
    return this->supported_custom_fan_modes_.count(custom_fan_mode);
 | 
			
		||||
    return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_supported_presets(std::set<ClimatePreset> presets) { this->supported_presets_ = std::move(presets); }
 | 
			
		||||
  void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = 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_.insert(preset); }
 | 
			
		||||
  void add_supported_custom_preset(const std::string &preset) { this->supported_custom_presets_.push_back(preset); }
 | 
			
		||||
  bool supports_preset(ClimatePreset preset) const { return this->supported_presets_.count(preset); }
 | 
			
		||||
  bool get_supports_presets() const { return !this->supported_presets_.empty(); }
 | 
			
		||||
  const std::set<climate::ClimatePreset> &get_supported_presets() const { return this->supported_presets_; }
 | 
			
		||||
  const ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; }
 | 
			
		||||
 | 
			
		||||
  void set_supported_custom_presets(std::set<std::string> supported_custom_presets) {
 | 
			
		||||
  void set_supported_custom_presets(std::vector<std::string> supported_custom_presets) {
 | 
			
		||||
    this->supported_custom_presets_ = std::move(supported_custom_presets);
 | 
			
		||||
  }
 | 
			
		||||
  const std::set<std::string> &get_supported_custom_presets() const { return this->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_; }
 | 
			
		||||
  bool supports_custom_preset(const std::string &custom_preset) const {
 | 
			
		||||
    return this->supported_custom_presets_.count(custom_preset);
 | 
			
		||||
    return vector_contains(this->supported_custom_presets_, custom_preset);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_supported_swing_modes(std::set<ClimateSwingMode> modes) { this->supported_swing_modes_ = std::move(modes); }
 | 
			
		||||
  void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = 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 std::set<ClimateSwingMode> &get_supported_swing_modes() const { return this->supported_swing_modes_; }
 | 
			
		||||
  const ClimateSwingModeMask &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) {
 | 
			
		||||
@@ -221,23 +205,6 @@ 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);
 | 
			
		||||
@@ -268,12 +235,12 @@ class ClimateTraits {
 | 
			
		||||
  float visual_min_humidity_{30};
 | 
			
		||||
  float visual_max_humidity_{99};
 | 
			
		||||
 | 
			
		||||
  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_;
 | 
			
		||||
  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_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace climate
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,10 @@ static const char *const TAG = "climate_ir";
 | 
			
		||||
 | 
			
		||||
climate::ClimateTraits ClimateIR::traits() {
 | 
			
		||||
  auto traits = climate::ClimateTraits();
 | 
			
		||||
  traits.set_supports_current_temperature(this->sensor_ != nullptr);
 | 
			
		||||
  if (this->sensor_ != nullptr) {
 | 
			
		||||
    traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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);
 | 
			
		||||
@@ -19,7 +22,6 @@ 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,16 +24,18 @@ 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, std::set<climate::ClimateFanMode> fan_modes = {},
 | 
			
		||||
            std::set<climate::ClimateSwingMode> swing_modes = {}, std::set<climate::ClimatePreset> presets = {}) {
 | 
			
		||||
            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()) {
 | 
			
		||||
    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_ = std::move(fan_modes);
 | 
			
		||||
    this->swing_modes_ = std::move(swing_modes);
 | 
			
		||||
    this->presets_ = std::move(presets);
 | 
			
		||||
    this->fan_modes_ = fan_modes;
 | 
			
		||||
    this->swing_modes_ = swing_modes;
 | 
			
		||||
    this->presets_ = presets;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void setup() override;
 | 
			
		||||
@@ -60,9 +62,9 @@ class ClimateIR : public Component,
 | 
			
		||||
  bool supports_heat_{true};
 | 
			
		||||
  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_ = {};
 | 
			
		||||
  climate::ClimateFanModeMask fan_modes_{};
 | 
			
		||||
  climate::ClimateSwingModeMask swing_modes_{};
 | 
			
		||||
  climate::ClimatePresetMask presets_{};
 | 
			
		||||
 | 
			
		||||
  sensor::Sensor *sensor_{nullptr};
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
#include "cover.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include <strings.h>
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace cover {
 | 
			
		||||
@@ -144,21 +144,7 @@ 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,6 +4,7 @@
 | 
			
		||||
#include "esphome/core/entity_base.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/preferences.h"
 | 
			
		||||
 | 
			
		||||
#include "cover_traits.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -125,25 +126,6 @@ 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,9 +241,7 @@ uint8_t DaikinArcClimate::humidity_() {
 | 
			
		||||
 | 
			
		||||
climate::ClimateTraits DaikinArcClimate::traits() {
 | 
			
		||||
  climate::ClimateTraits traits = climate_ir::ClimateIR::traits();
 | 
			
		||||
  traits.set_supports_current_temperature(true);
 | 
			
		||||
  traits.set_supports_current_humidity(false);
 | 
			
		||||
  traits.set_supports_target_humidity(true);
 | 
			
		||||
  traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY);
 | 
			
		||||
  traits.set_visual_min_humidity(38);
 | 
			
		||||
  traits.set_visual_max_humidity(52);
 | 
			
		||||
  return traits;
 | 
			
		||||
 
 | 
			
		||||
@@ -82,16 +82,14 @@ class DemoClimate : public climate::Climate, public Component {
 | 
			
		||||
    climate::ClimateTraits traits{};
 | 
			
		||||
    switch (type_) {
 | 
			
		||||
      case DemoClimateType::TYPE_1:
 | 
			
		||||
        traits.set_supports_current_temperature(true);
 | 
			
		||||
        traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE | climate::CLIMATE_SUPPORTS_ACTION);
 | 
			
		||||
        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,
 | 
			
		||||
@@ -100,7 +98,7 @@ class DemoClimate : public climate::Climate, public Component {
 | 
			
		||||
            climate::CLIMATE_MODE_DRY,
 | 
			
		||||
            climate::CLIMATE_MODE_FAN_ONLY,
 | 
			
		||||
        });
 | 
			
		||||
        traits.set_supports_action(true);
 | 
			
		||||
        traits.add_feature_flags(climate::CLIMATE_SUPPORTS_ACTION);
 | 
			
		||||
        traits.set_supported_fan_modes({
 | 
			
		||||
            climate::CLIMATE_FAN_ON,
 | 
			
		||||
            climate::CLIMATE_FAN_OFF,
 | 
			
		||||
@@ -123,8 +121,8 @@ class DemoClimate : public climate::Climate, public Component {
 | 
			
		||||
        traits.set_supported_custom_presets({"My Preset"});
 | 
			
		||||
        break;
 | 
			
		||||
      case DemoClimateType::TYPE_3:
 | 
			
		||||
        traits.set_supports_current_temperature(true);
 | 
			
		||||
        traits.set_supports_two_point_target_temperature(true);
 | 
			
		||||
        traits.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE |
 | 
			
		||||
                                 climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE);
 | 
			
		||||
        traits.set_supported_modes({
 | 
			
		||||
            climate::CLIMATE_MODE_OFF,
 | 
			
		||||
            climate::CLIMATE_MODE_COOL,
 | 
			
		||||
 
 | 
			
		||||
@@ -80,8 +80,8 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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());
 | 
			
		||||
  ESP_LOGD(TAG, "Registering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
 | 
			
		||||
           light_effect->get_last_universe());
 | 
			
		||||
 | 
			
		||||
  light_effects_.insert(light_effect);
 | 
			
		||||
 | 
			
		||||
@@ -95,8 +95,8 @@ void E131Component::remove_effect(E131AddressableLightEffect *light_effect) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  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());
 | 
			
		||||
  ESP_LOGD(TAG, "Unregistering '%s' for universes %d-%d.", light_effect->get_name(), light_effect->get_first_universe(),
 | 
			
		||||
           light_effect->get_last_universe());
 | 
			
		||||
 | 
			
		||||
  light_effects_.erase(light_effect);
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 std::string &name) : AddressableLightEffect(name) {}
 | 
			
		||||
E131AddressableLightEffect::E131AddressableLightEffect(const char *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().c_str(), universe,
 | 
			
		||||
           output_offset, output_end);
 | 
			
		||||
  ESP_LOGV(TAG, "Applying data for '%s' on %d universe, for %" PRId32 "-%d.", get_name(), 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 std::string &name);
 | 
			
		||||
  E131AddressableLightEffect(const char *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,3 +1,4 @@
 | 
			
		||||
import contextlib
 | 
			
		||||
from dataclasses import dataclass
 | 
			
		||||
import itertools
 | 
			
		||||
import logging
 | 
			
		||||
@@ -102,6 +103,10 @@ 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,
 | 
			
		||||
@@ -299,9 +304,13 @@ 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)}.zip"
 | 
			
		||||
    return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip"
 | 
			
		||||
        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}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _is_framework_url(source: str) -> str:
 | 
			
		||||
@@ -350,6 +359,7 @@ 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"),
 | 
			
		||||
@@ -545,6 +555,32 @@ 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:
 | 
			
		||||
@@ -610,6 +646,13 @@ 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,
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
@@ -746,6 +789,72 @@ 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])
 | 
			
		||||
@@ -773,12 +882,27 @@ 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")
 | 
			
		||||
@@ -805,6 +929,7 @@ 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")
 | 
			
		||||
 | 
			
		||||
@@ -855,6 +980,9 @@ 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)
 | 
			
		||||
@@ -877,6 +1005,43 @@ 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,6 +6,7 @@
 | 
			
		||||
#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>
 | 
			
		||||
@@ -52,6 +53,16 @@ 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(); }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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,7 +223,10 @@ 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}")))
 | 
			
		||||
    cg.add(var.set_inverted(config[CONF_INVERTED]))
 | 
			
		||||
    # 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))
 | 
			
		||||
    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])))
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										71
									
								
								esphome/components/esp32/iram_fix.py.script
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										71
									
								
								esphome/components/esp32/iram_fix.py.script
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,71 @@
 | 
			
		||||
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)
 | 
			
		||||
@@ -61,12 +61,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
 | 
			
		||||
      this->address_str_ = "";
 | 
			
		||||
    } else {
 | 
			
		||||
      char buf[18];
 | 
			
		||||
      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);
 | 
			
		||||
      format_mac_addr_upper(this->remote_bda_, buf);
 | 
			
		||||
      this->address_str_ = buf;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -461,7 +461,9 @@ async def parse_value(value_config, args):
 | 
			
		||||
    if isinstance(value, str):
 | 
			
		||||
        value = list(value.encode(value_config[CONF_STRING_ENCODING]))
 | 
			
		||||
    if isinstance(value, list):
 | 
			
		||||
        return cg.std_vector.template(cg.uint8)(value)
 | 
			
		||||
        # 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)
 | 
			
		||||
    val = cg.RawExpression(f"{value_config[CONF_TYPE]}({cg.safe_exp(value)})")
 | 
			
		||||
    return ByteBuffer_ns.wrap(val, value_config[CONF_ENDIANNESS])
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -35,13 +35,18 @@ BLECharacteristic::BLECharacteristic(const ESPBTUUID uuid, uint32_t properties)
 | 
			
		||||
 | 
			
		||||
void BLECharacteristic::set_value(ByteBuffer buffer) { this->set_value(buffer.get_data()); }
 | 
			
		||||
 | 
			
		||||
void BLECharacteristic::set_value(const std::vector<uint8_t> &buffer) {
 | 
			
		||||
void BLECharacteristic::set_value(std::vector<uint8_t> &&buffer) {
 | 
			
		||||
  xSemaphoreTake(this->set_value_lock_, 0L);
 | 
			
		||||
  this->value_ = buffer;
 | 
			
		||||
  this->value_ = std::move(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()));
 | 
			
		||||
  this->set_value(std::vector<uint8_t>(buffer.begin(), buffer.end()));  // Delegate to move overload
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLECharacteristic::notify() {
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,8 @@ class BLECharacteristic {
 | 
			
		||||
  ~BLECharacteristic();
 | 
			
		||||
 | 
			
		||||
  void set_value(ByteBuffer buffer);
 | 
			
		||||
  void set_value(const std::vector<uint8_t> &buffer);
 | 
			
		||||
  void set_value(std::vector<uint8_t> &&buffer);
 | 
			
		||||
  void set_value(std::initializer_list<uint8_t> data);
 | 
			
		||||
  void set_value(const std::string &buffer);
 | 
			
		||||
 | 
			
		||||
  void set_broadcast_property(bool value);
 | 
			
		||||
 
 | 
			
		||||
@@ -46,15 +46,17 @@ void BLEDescriptor::do_create(BLECharacteristic *characteristic) {
 | 
			
		||||
  this->state_ = CREATING;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEDescriptor::set_value(std::vector<uint8_t> buffer) {
 | 
			
		||||
  size_t length = buffer.size();
 | 
			
		||||
void BLEDescriptor::set_value(std::vector<uint8_t> &&buffer) { this->set_value_impl_(buffer.data(), 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, buffer.data(), length);
 | 
			
		||||
  memcpy(this->value_.attr_value, data, length);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
 | 
			
		||||
 
 | 
			
		||||
@@ -27,7 +27,8 @@ 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::vector<uint8_t> &&buffer);
 | 
			
		||||
  void set_value(std::initializer_list<uint8_t> data);
 | 
			
		||||
  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);
 | 
			
		||||
@@ -42,6 +43,8 @@ class BLEDescriptor {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void set_value_impl_(const uint8_t *data, size_t length);
 | 
			
		||||
 | 
			
		||||
  BLECharacteristic *characteristic_{nullptr};
 | 
			
		||||
  ESPBTUUID uuid_;
 | 
			
		||||
  uint16_t handle_{0xFFFF};
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,7 @@
 | 
			
		||||
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"]
 | 
			
		||||
@@ -13,13 +14,27 @@ Mode = esp32_camera_web_server_ns.enum("Mode")
 | 
			
		||||
 | 
			
		||||
MODES = {"STREAM": Mode.STREAM, "SNAPSHOT": Mode.SNAPSHOT}
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
 | 
			
		||||
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,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
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.5.11")
 | 
			
		||||
        esp32.add_idf_component(name="espressif/esp_hosted", ref="2.6.1")
 | 
			
		||||
    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, output
 | 
			
		||||
from esphome.components import binary_sensor, esp32_ble, improv_base, 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"]
 | 
			
		||||
AUTO_LOAD = ["esp32_ble_server", "improv_base"]
 | 
			
		||||
CODEOWNERS = ["@jesserockz"]
 | 
			
		||||
DEPENDENCIES = ["wifi", "esp32"]
 | 
			
		||||
 | 
			
		||||
@@ -20,6 +20,7 @@ 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")
 | 
			
		||||
@@ -43,55 +44,63 @@ 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(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(improv_base.IMPROV_SCHEMA)
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
@@ -102,7 +111,8 @@ async def to_code(config):
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    cg.add_define("USE_IMPROV")
 | 
			
		||||
    cg.add_library("improv/Improv", "1.2.4")
 | 
			
		||||
 | 
			
		||||
    await improv_base.setup_improv_core(var, config, "esp32_improv")
 | 
			
		||||
 | 
			
		||||
    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(ByteBuffer::wrap(response));
 | 
			
		||||
void ESP32ImprovComponent::send_response_(std::vector<uint8_t> &&response) {
 | 
			
		||||
  this->rpc_response_->set_value(std::move(response));
 | 
			
		||||
  if (this->state_ != improv::STATE_STOPPED)
 | 
			
		||||
    this->rpc_response_->notify();
 | 
			
		||||
}
 | 
			
		||||
@@ -384,18 +384,33 @@ void ESP32ImprovComponent::check_wifi_connection_() {
 | 
			
		||||
    this->connecting_sta_ = {};
 | 
			
		||||
    this->cancel_timeout("wifi-connect-timeout");
 | 
			
		||||
 | 
			
		||||
    std::vector<std::string> urls = {ESPHOME_MY_LINK};
 | 
			
		||||
    // 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;
 | 
			
		||||
#ifdef USE_WEBSERVER
 | 
			
		||||
    for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
 | 
			
		||||
      if (ip.is_ip4()) {
 | 
			
		||||
        std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
 | 
			
		||||
        urls.push_back(webserver_url);
 | 
			
		||||
        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;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
    std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls);
 | 
			
		||||
    this->send_response_(data);
 | 
			
		||||
    this->send_response_(improv::build_rpc_response(improv::WIFI_SETTINGS,
 | 
			
		||||
                                                    std::vector<std::string>(url_strings, url_strings + url_count)));
 | 
			
		||||
  } else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) {
 | 
			
		||||
    ESP_LOGD(TAG, "WiFi provisioned externally");
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -7,6 +7,7 @@
 | 
			
		||||
 | 
			
		||||
#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
 | 
			
		||||
@@ -32,7 +33,7 @@ namespace esp32_improv {
 | 
			
		||||
 | 
			
		||||
using namespace esp32_ble_server;
 | 
			
		||||
 | 
			
		||||
class ESP32ImprovComponent : public Component {
 | 
			
		||||
class ESP32ImprovComponent : public Component, public improv_base::ImprovBase {
 | 
			
		||||
 public:
 | 
			
		||||
  ESP32ImprovComponent();
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
@@ -108,7 +109,7 @@ class ESP32ImprovComponent : public Component {
 | 
			
		||||
  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,7 +190,9 @@ async def to_code(config):
 | 
			
		||||
    cg.add_define("ESPHOME_VARIANT", "ESP8266")
 | 
			
		||||
    cg.add_define(ThreadModel.SINGLE)
 | 
			
		||||
 | 
			
		||||
    cg.add_platformio_option("extra_scripts", ["pre:iram_fix.py", "post:post_build.py"])
 | 
			
		||||
    cg.add_platformio_option(
 | 
			
		||||
        "extra_scripts", ["pre:testing_mode.py", "post:post_build.py"]
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    conf = config[CONF_FRAMEWORK]
 | 
			
		||||
    cg.add_platformio_option("framework", "arduino")
 | 
			
		||||
@@ -230,9 +232,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 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
 | 
			
		||||
    # 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
 | 
			
		||||
    if CORE.testing_mode:
 | 
			
		||||
        cg.add_build_flag("-DESPHOME_TESTING_MODE")
 | 
			
		||||
 | 
			
		||||
@@ -271,8 +273,8 @@ def copy_files():
 | 
			
		||||
        post_build_file,
 | 
			
		||||
        CORE.relative_build_path("post_build.py"),
 | 
			
		||||
    )
 | 
			
		||||
    iram_fix_file = dir / "iram_fix.py.script"
 | 
			
		||||
    testing_mode_file = dir / "testing_mode.py.script"
 | 
			
		||||
    copy_file_if_changed(
 | 
			
		||||
        iram_fix_file,
 | 
			
		||||
        CORE.relative_build_path("iram_fix.py"),
 | 
			
		||||
        testing_mode_file,
 | 
			
		||||
        CORE.relative_build_path("testing_mode.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,7 +165,10 @@ async def esp8266_pin_to_code(config):
 | 
			
		||||
    num = config[CONF_NUMBER]
 | 
			
		||||
    mode = config[CONF_MODE]
 | 
			
		||||
    cg.add(var.set_pin(num))
 | 
			
		||||
    cg.add(var.set_inverted(config[CONF_INVERTED]))
 | 
			
		||||
    # 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_flags(pins.gpio_flags_expr(mode)))
 | 
			
		||||
    if num < 16:
 | 
			
		||||
        initial_state: PinInitialState = CORE.data[KEY_ESP8266][KEY_PIN_INITIAL_STATES][
 | 
			
		||||
 
 | 
			
		||||
@@ -1,44 +0,0 @@
 | 
			
		||||
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)
 | 
			
		||||
							
								
								
									
										166
									
								
								esphome/components/esp8266/testing_mode.py.script
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										166
									
								
								esphome/components/esp8266/testing_mode.py.script
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,166 @@
 | 
			
		||||
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,7 +103,16 @@ def ota_esphome_final_validate(config):
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
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(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(ESPHomeOTAComponent),
 | 
			
		||||
@@ -130,7 +139,8 @@ CONFIG_SCHEMA = (
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(BASE_OTA_SCHEMA)
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    _consume_ota_sockets,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = ota_esphome_final_validate
 | 
			
		||||
 
 | 
			
		||||
@@ -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::vector<Action<Ts...> *> &actions) {
 | 
			
		||||
  void add_on_sent(const std::initializer_list<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::vector<Action<Ts...> *> &actions) {
 | 
			
		||||
  void add_on_error(const std::initializer_list<Action<Ts...> *> &actions) {
 | 
			
		||||
    this->error_.add_actions(actions);
 | 
			
		||||
    if (this->flags_.wait_for_sent) {
 | 
			
		||||
      this->error_.add_action(new LambdaAction<Ts...>([this](Ts... x) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										39
									
								
								esphome/components/espnow/packet_transport/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								esphome/components/espnow/packet_transport/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
"""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]))
 | 
			
		||||
@@ -0,0 +1,97 @@
 | 
			
		||||
#include "espnow_transport.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace espnow {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "espnow.transport";
 | 
			
		||||
 | 
			
		||||
bool ESPNowTransport::should_send() { return this->parent_ != nullptr && !this->parent_->is_failed(); }
 | 
			
		||||
 | 
			
		||||
void ESPNowTransport::setup() {
 | 
			
		||||
  packet_transport::PacketTransport::setup();
 | 
			
		||||
 | 
			
		||||
  if (this->parent_ == nullptr) {
 | 
			
		||||
    ESP_LOGE(TAG, "ESPNow component not set");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "Registering ESP-NOW handlers");
 | 
			
		||||
  ESP_LOGI(TAG, "Peer address: %02X:%02X:%02X:%02X:%02X:%02X", this->peer_address_[0], this->peer_address_[1],
 | 
			
		||||
           this->peer_address_[2], this->peer_address_[3], this->peer_address_[4], this->peer_address_[5]);
 | 
			
		||||
 | 
			
		||||
  // Register received handler
 | 
			
		||||
  this->parent_->register_received_handler(static_cast<ESPNowReceivedPacketHandler *>(this));
 | 
			
		||||
 | 
			
		||||
  // Register broadcasted handler
 | 
			
		||||
  this->parent_->register_broadcasted_handler(static_cast<ESPNowBroadcastedHandler *>(this));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPNowTransport::update() {
 | 
			
		||||
  packet_transport::PacketTransport::update();
 | 
			
		||||
  this->updated_ = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPNowTransport::send_packet(const std::vector<uint8_t> &buf) const {
 | 
			
		||||
  if (this->parent_ == nullptr) {
 | 
			
		||||
    ESP_LOGE(TAG, "ESPNow component not set");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (buf.empty()) {
 | 
			
		||||
    ESP_LOGW(TAG, "Attempted to send empty packet");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (buf.size() > ESP_NOW_MAX_DATA_LEN) {
 | 
			
		||||
    ESP_LOGE(TAG, "Packet too large: %zu bytes (max %d)", buf.size(), ESP_NOW_MAX_DATA_LEN);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Send to configured peer address
 | 
			
		||||
  this->parent_->send(this->peer_address_.data(), buf.data(), buf.size(), [](esp_err_t err) {
 | 
			
		||||
    if (err != ESP_OK) {
 | 
			
		||||
      ESP_LOGW(TAG, "Send failed: %d", err);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ESPNowTransport::on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
 | 
			
		||||
  ESP_LOGV(TAG, "Received packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0],
 | 
			
		||||
           info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
 | 
			
		||||
 | 
			
		||||
  if (data == nullptr || size == 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Received empty or null packet");
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->packet_buffer_.resize(size);
 | 
			
		||||
  memcpy(this->packet_buffer_.data(), data, size);
 | 
			
		||||
  this->process_(this->packet_buffer_);
 | 
			
		||||
  return false;  // Allow other handlers to run
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ESPNowTransport::on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) {
 | 
			
		||||
  ESP_LOGV(TAG, "Received broadcast packet of size %u from %02X:%02X:%02X:%02X:%02X:%02X", size, info.src_addr[0],
 | 
			
		||||
           info.src_addr[1], info.src_addr[2], info.src_addr[3], info.src_addr[4], info.src_addr[5]);
 | 
			
		||||
 | 
			
		||||
  if (data == nullptr || size == 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Received empty or null broadcast packet");
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->packet_buffer_.resize(size);
 | 
			
		||||
  memcpy(this->packet_buffer_.data(), data, size);
 | 
			
		||||
  this->process_(this->packet_buffer_);
 | 
			
		||||
  return false;  // Allow other handlers to run
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace espnow
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
@@ -0,0 +1,44 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "../espnow_component.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/packet_transport/packet_transport.h"
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace espnow {
 | 
			
		||||
 | 
			
		||||
class ESPNowTransport : public packet_transport::PacketTransport,
 | 
			
		||||
                        public Parented<ESPNowComponent>,
 | 
			
		||||
                        public ESPNowReceivedPacketHandler,
 | 
			
		||||
                        public ESPNowBroadcastedHandler {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::AFTER_WIFI; }
 | 
			
		||||
 | 
			
		||||
  void set_peer_address(peer_address_t address) {
 | 
			
		||||
    memcpy(this->peer_address_.data(), address.data(), ESP_NOW_ETH_ALEN);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // ESPNow handler interface
 | 
			
		||||
  bool on_received(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
 | 
			
		||||
  bool on_broadcasted(const ESPNowRecvInfo &info, const uint8_t *data, uint8_t size) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void send_packet(const std::vector<uint8_t> &buf) const override;
 | 
			
		||||
  size_t get_max_packet_size() override { return ESP_NOW_MAX_DATA_LEN; }
 | 
			
		||||
  bool should_send() override;
 | 
			
		||||
 | 
			
		||||
  peer_address_t peer_address_{{0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF}};
 | 
			
		||||
  std::vector<uint8_t> packet_buffer_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace espnow
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
@@ -14,7 +14,7 @@ from esphome.components.esp32.const import (
 | 
			
		||||
    VARIANT_ESP32S2,
 | 
			
		||||
    VARIANT_ESP32S3,
 | 
			
		||||
)
 | 
			
		||||
from esphome.components.network import IPAddress
 | 
			
		||||
from esphome.components.network import ip_address_literal
 | 
			
		||||
from esphome.components.spi import CONF_INTERFACE_INDEX, get_spi_interface
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
@@ -32,6 +32,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_MISO_PIN,
 | 
			
		||||
    CONF_MODE,
 | 
			
		||||
    CONF_MOSI_PIN,
 | 
			
		||||
    CONF_NUMBER,
 | 
			
		||||
    CONF_PAGE_ID,
 | 
			
		||||
    CONF_PIN,
 | 
			
		||||
    CONF_POLLING_INTERVAL,
 | 
			
		||||
@@ -52,12 +53,36 @@ from esphome.core import (
 | 
			
		||||
    coroutine_with_priority,
 | 
			
		||||
)
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
 | 
			
		||||
CONFLICTS_WITH = ["wifi"]
 | 
			
		||||
DEPENDENCIES = ["esp32"]
 | 
			
		||||
AUTO_LOAD = ["network"]
 | 
			
		||||
LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
# RMII pins that are hardcoded on ESP32 classic and cannot be changed
 | 
			
		||||
# These pins are used by the internal Ethernet MAC when using RMII PHYs
 | 
			
		||||
ESP32_RMII_FIXED_PINS = {
 | 
			
		||||
    19: "EMAC_TXD0",
 | 
			
		||||
    21: "EMAC_TX_EN",
 | 
			
		||||
    22: "EMAC_TXD1",
 | 
			
		||||
    25: "EMAC_RXD0",
 | 
			
		||||
    26: "EMAC_RXD1",
 | 
			
		||||
    27: "EMAC_RX_CRS_DV",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# RMII default pins for ESP32-P4
 | 
			
		||||
# These are the default pins used by ESP-IDF and are configurable in principle,
 | 
			
		||||
# but ESPHome's ethernet component currently has no way to change them
 | 
			
		||||
ESP32P4_RMII_DEFAULT_PINS = {
 | 
			
		||||
    34: "EMAC_TXD0",
 | 
			
		||||
    35: "EMAC_TXD1",
 | 
			
		||||
    28: "EMAC_RX_CRS_DV",
 | 
			
		||||
    29: "EMAC_RXD0",
 | 
			
		||||
    30: "EMAC_RXD1",
 | 
			
		||||
    49: "EMAC_TX_EN",
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ethernet_ns = cg.esphome_ns.namespace("ethernet")
 | 
			
		||||
PHYRegister = ethernet_ns.struct("PHYRegister")
 | 
			
		||||
CONF_PHY_ADDR = "phy_addr"
 | 
			
		||||
@@ -273,7 +298,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _final_validate(config):
 | 
			
		||||
def _final_validate_spi(config):
 | 
			
		||||
    if config[CONF_TYPE] not in SPI_ETHERNET_TYPES:
 | 
			
		||||
        return
 | 
			
		||||
    if spi_configs := fv.full_config.get().get(CONF_SPI):
 | 
			
		||||
@@ -292,17 +317,14 @@ def _final_validate(config):
 | 
			
		||||
                    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = _final_validate
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def manual_ip(config):
 | 
			
		||||
    return cg.StructInitializer(
 | 
			
		||||
        ManualIP,
 | 
			
		||||
        ("static_ip", IPAddress(str(config[CONF_STATIC_IP]))),
 | 
			
		||||
        ("gateway", IPAddress(str(config[CONF_GATEWAY]))),
 | 
			
		||||
        ("subnet", IPAddress(str(config[CONF_SUBNET]))),
 | 
			
		||||
        ("dns1", IPAddress(str(config[CONF_DNS1]))),
 | 
			
		||||
        ("dns2", IPAddress(str(config[CONF_DNS2]))),
 | 
			
		||||
        ("static_ip", ip_address_literal(config[CONF_STATIC_IP])),
 | 
			
		||||
        ("gateway", ip_address_literal(config[CONF_GATEWAY])),
 | 
			
		||||
        ("subnet", ip_address_literal(config[CONF_SUBNET])),
 | 
			
		||||
        ("dns1", ip_address_literal(config[CONF_DNS1])),
 | 
			
		||||
        ("dns2", ip_address_literal(config[CONF_DNS2])),
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -383,3 +405,57 @@ async def to_code(config):
 | 
			
		||||
 | 
			
		||||
    if CORE.using_arduino:
 | 
			
		||||
        cg.add_library("WiFi", None)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _final_validate_rmii_pins(config: ConfigType) -> None:
 | 
			
		||||
    """Validate that RMII pins are not used by other components."""
 | 
			
		||||
    # Only validate for RMII-based PHYs on ESP32/ESP32P4
 | 
			
		||||
    if config[CONF_TYPE] in SPI_ETHERNET_TYPES or config[CONF_TYPE] == "OPENETH":
 | 
			
		||||
        return  # SPI and OPENETH don't use RMII
 | 
			
		||||
 | 
			
		||||
    variant = get_esp32_variant()
 | 
			
		||||
    if variant == VARIANT_ESP32:
 | 
			
		||||
        rmii_pins = ESP32_RMII_FIXED_PINS
 | 
			
		||||
        is_configurable = False
 | 
			
		||||
    elif variant == VARIANT_ESP32P4:
 | 
			
		||||
        rmii_pins = ESP32P4_RMII_DEFAULT_PINS
 | 
			
		||||
        is_configurable = True
 | 
			
		||||
    else:
 | 
			
		||||
        return  # No RMII validation needed for other variants
 | 
			
		||||
 | 
			
		||||
    # Check all used pins against RMII reserved pins
 | 
			
		||||
    for pin_list in pins.PIN_SCHEMA_REGISTRY.pins_used.values():
 | 
			
		||||
        for pin_path, _, pin_config in pin_list:
 | 
			
		||||
            pin_num = pin_config.get(CONF_NUMBER)
 | 
			
		||||
            if pin_num not in rmii_pins:
 | 
			
		||||
                continue
 | 
			
		||||
            # Found a conflict - show helpful error message
 | 
			
		||||
            pin_function = rmii_pins[pin_num]
 | 
			
		||||
            component_path = ".".join(str(p) for p in pin_path)
 | 
			
		||||
            if is_configurable:
 | 
			
		||||
                error_msg = (
 | 
			
		||||
                    f"GPIO{pin_num} is used by Ethernet RMII "
 | 
			
		||||
                    f"({pin_function}) with the current default "
 | 
			
		||||
                    f"configuration. This conflicts with '{component_path}'. "
 | 
			
		||||
                    f"Please choose a different GPIO pin for "
 | 
			
		||||
                    f"'{component_path}'."
 | 
			
		||||
                )
 | 
			
		||||
            else:
 | 
			
		||||
                error_msg = (
 | 
			
		||||
                    f"GPIO{pin_num} is reserved for Ethernet RMII "
 | 
			
		||||
                    f"({pin_function}) and cannot be used. This pin is "
 | 
			
		||||
                    f"hardcoded by ESP-IDF and cannot be changed when using "
 | 
			
		||||
                    f"RMII Ethernet PHYs. Please choose a different GPIO pin "
 | 
			
		||||
                    f"for '{component_path}'."
 | 
			
		||||
                )
 | 
			
		||||
            raise cv.Invalid(error_msg, path=pin_path)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _final_validate(config: ConfigType) -> ConfigType:
 | 
			
		||||
    """Final validation for Ethernet component."""
 | 
			
		||||
    _final_validate_spi(config)
 | 
			
		||||
    _final_validate_rmii_pins(config)
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = _final_validate
 | 
			
		||||
 
 | 
			
		||||
@@ -8,12 +8,19 @@ namespace event {
 | 
			
		||||
static const char *const TAG = "event";
 | 
			
		||||
 | 
			
		||||
void Event::trigger(const std::string &event_type) {
 | 
			
		||||
  auto found = types_.find(event_type);
 | 
			
		||||
  if (found == types_.end()) {
 | 
			
		||||
  // Linear search - faster than std::set for small datasets (1-5 items typical)
 | 
			
		||||
  const std::string *found = nullptr;
 | 
			
		||||
  for (const auto &type : this->types_) {
 | 
			
		||||
    if (type == event_type) {
 | 
			
		||||
      found = &type;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (found == nullptr) {
 | 
			
		||||
    ESP_LOGE(TAG, "'%s': invalid event type for trigger(): %s", this->get_name().c_str(), event_type.c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  last_event_type = &(*found);
 | 
			
		||||
  last_event_type = found;
 | 
			
		||||
  ESP_LOGD(TAG, "'%s' Triggered event '%s'", this->get_name().c_str(), last_event_type->c_str());
 | 
			
		||||
  this->event_callback_.call(event_type);
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,5 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <set>
 | 
			
		||||
#include <string>
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
@@ -26,13 +25,13 @@ class Event : public EntityBase, public EntityBase_DeviceClass {
 | 
			
		||||
  const std::string *last_event_type;
 | 
			
		||||
 | 
			
		||||
  void trigger(const std::string &event_type);
 | 
			
		||||
  void set_event_types(const std::set<std::string> &event_types) { this->types_ = event_types; }
 | 
			
		||||
  std::set<std::string> get_event_types() const { return this->types_; }
 | 
			
		||||
  void set_event_types(const std::initializer_list<std::string> &event_types) { this->types_ = event_types; }
 | 
			
		||||
  const FixedVector<std::string> &get_event_types() const { return this->types_; }
 | 
			
		||||
  void add_on_event_callback(std::function<void(const std::string &event_type)> &&callback);
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  CallbackManager<void(const std::string &event_type)> event_callback_;
 | 
			
		||||
  std::set<std::string> types_;
 | 
			
		||||
  FixedVector<std::string> types_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace event
 | 
			
		||||
 
 | 
			
		||||
@@ -38,7 +38,6 @@ IS_PLATFORM_COMPONENT = True
 | 
			
		||||
 | 
			
		||||
fan_ns = cg.esphome_ns.namespace("fan")
 | 
			
		||||
Fan = fan_ns.class_("Fan", cg.EntityBase)
 | 
			
		||||
FanState = fan_ns.class_("Fan", Fan, cg.Component)
 | 
			
		||||
 | 
			
		||||
FanDirection = fan_ns.enum("FanDirection", is_class=True)
 | 
			
		||||
FAN_DIRECTION_ENUM = {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,8 +1,8 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "fan_state.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "fan.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace fan {
 | 
			
		||||
 
 | 
			
		||||
@@ -60,8 +60,6 @@ class FanCall {
 | 
			
		||||
    this->speed_ = speed;
 | 
			
		||||
    return *this;
 | 
			
		||||
  }
 | 
			
		||||
  ESPDEPRECATED("set_speed() with string argument is deprecated, use integer argument instead.", "2021.9")
 | 
			
		||||
  FanCall &set_speed(const char *legacy_speed);
 | 
			
		||||
  optional<int> get_speed() const { return this->speed_; }
 | 
			
		||||
  FanCall &set_direction(FanDirection direction) {
 | 
			
		||||
    this->direction_ = direction;
 | 
			
		||||
 
 | 
			
		||||
@@ -1,16 +0,0 @@
 | 
			
		||||
#include "fan_state.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace fan {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "fan";
 | 
			
		||||
 | 
			
		||||
void FanState::setup() {
 | 
			
		||||
  auto restore = this->restore_state_();
 | 
			
		||||
  if (restore)
 | 
			
		||||
    restore->to_call(*this).perform();
 | 
			
		||||
}
 | 
			
		||||
float FanState::get_setup_priority() const { return setup_priority::DATA - 1.0f; }
 | 
			
		||||
 | 
			
		||||
}  // namespace fan
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,34 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "fan.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace fan {
 | 
			
		||||
 | 
			
		||||
enum ESPDEPRECATED("LegacyFanDirection members are deprecated, use FanDirection instead.",
 | 
			
		||||
                   "2022.2") LegacyFanDirection {
 | 
			
		||||
  FAN_DIRECTION_FORWARD = 0,
 | 
			
		||||
  FAN_DIRECTION_REVERSE = 1
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ESPDEPRECATED("FanState is deprecated, use Fan instead.", "2022.2") FanState : public Fan, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  FanState() = default;
 | 
			
		||||
 | 
			
		||||
  /// Get the traits of this fan.
 | 
			
		||||
  FanTraits get_traits() override { return this->traits_; }
 | 
			
		||||
  /// Set the traits of this fan (i.e. what features it supports).
 | 
			
		||||
  void set_traits(const FanTraits &traits) { this->traits_ = traits; }
 | 
			
		||||
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void control(const FanCall &call) override { this->publish_state(); }
 | 
			
		||||
 | 
			
		||||
  FanTraits traits_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace fan
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -67,7 +67,7 @@ void GPIOSwitch::write_state(bool state) {
 | 
			
		||||
  this->pin_->digital_write(state);
 | 
			
		||||
  this->publish_state(state);
 | 
			
		||||
}
 | 
			
		||||
void GPIOSwitch::set_interlock(const std::vector<Switch *> &interlock) { this->interlock_ = interlock; }
 | 
			
		||||
void GPIOSwitch::set_interlock(const std::initializer_list<Switch *> &interlock) { this->interlock_ = interlock; }
 | 
			
		||||
 | 
			
		||||
}  // namespace gpio
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,9 @@
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/components/switch/switch.h"
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace gpio {
 | 
			
		||||
 | 
			
		||||
@@ -19,14 +18,14 @@ class GPIOSwitch : public switch_::Switch, public Component {
 | 
			
		||||
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void set_interlock(const std::vector<Switch *> &interlock);
 | 
			
		||||
  void set_interlock(const std::initializer_list<Switch *> &interlock);
 | 
			
		||||
  void set_interlock_wait_time(uint32_t interlock_wait_time) { interlock_wait_time_ = interlock_wait_time; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void write_state(bool state) override;
 | 
			
		||||
 | 
			
		||||
  GPIOPin *pin_;
 | 
			
		||||
  std::vector<Switch *> interlock_;
 | 
			
		||||
  FixedVector<Switch *> interlock_;
 | 
			
		||||
  uint32_t interlock_wait_time_{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -65,7 +65,7 @@ HaierClimateBase::HaierClimateBase()
 | 
			
		||||
      {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, climate::CLIMATE_FAN_HIGH});
 | 
			
		||||
  this->traits_.set_supported_swing_modes({climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_BOTH,
 | 
			
		||||
                                           climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL});
 | 
			
		||||
  this->traits_.set_supports_current_temperature(true);
 | 
			
		||||
  this->traits_.add_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
HaierClimateBase::~HaierClimateBase() {}
 | 
			
		||||
@@ -171,7 +171,7 @@ void HaierClimateBase::toggle_power() {
 | 
			
		||||
      PendingAction({ActionRequest::TOGGLE_POWER, esphome::optional<haier_protocol::HaierMessage>()});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::set_supported_swing_modes(const std::set<climate::ClimateSwingMode> &modes) {
 | 
			
		||||
void HaierClimateBase::set_supported_swing_modes(climate::ClimateSwingModeMask modes) {
 | 
			
		||||
  this->traits_.set_supported_swing_modes(modes);
 | 
			
		||||
  if (!modes.empty())
 | 
			
		||||
    this->traits_.add_supported_swing_mode(climate::CLIMATE_SWING_OFF);
 | 
			
		||||
@@ -179,13 +179,13 @@ void HaierClimateBase::set_supported_swing_modes(const std::set<climate::Climate
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::set_answer_timeout(uint32_t timeout) { this->haier_protocol_.set_answer_timeout(timeout); }
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::set_supported_modes(const std::set<climate::ClimateMode> &modes) {
 | 
			
		||||
void HaierClimateBase::set_supported_modes(climate::ClimateModeMask modes) {
 | 
			
		||||
  this->traits_.set_supported_modes(modes);
 | 
			
		||||
  this->traits_.add_supported_mode(climate::CLIMATE_MODE_OFF);        // Always available
 | 
			
		||||
  this->traits_.add_supported_mode(climate::CLIMATE_MODE_HEAT_COOL);  // Always available
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HaierClimateBase::set_supported_presets(const std::set<climate::ClimatePreset> &presets) {
 | 
			
		||||
void HaierClimateBase::set_supported_presets(climate::ClimatePresetMask presets) {
 | 
			
		||||
  this->traits_.set_supported_presets(presets);
 | 
			
		||||
  if (!presets.empty())
 | 
			
		||||
    this->traits_.add_supported_preset(climate::CLIMATE_PRESET_NONE);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include <set>
 | 
			
		||||
#include "esphome/components/climate/climate.h"
 | 
			
		||||
#include "esphome/components/uart/uart.h"
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
@@ -60,9 +59,9 @@ class HaierClimateBase : public esphome::Component,
 | 
			
		||||
  void send_power_off_command();
 | 
			
		||||
  void toggle_power();
 | 
			
		||||
  void reset_protocol() { this->reset_protocol_request_ = true; };
 | 
			
		||||
  void set_supported_modes(const std::set<esphome::climate::ClimateMode> &modes);
 | 
			
		||||
  void set_supported_swing_modes(const std::set<esphome::climate::ClimateSwingMode> &modes);
 | 
			
		||||
  void set_supported_presets(const std::set<esphome::climate::ClimatePreset> &presets);
 | 
			
		||||
  void set_supported_modes(esphome::climate::ClimateModeMask modes);
 | 
			
		||||
  void set_supported_swing_modes(esphome::climate::ClimateSwingModeMask modes);
 | 
			
		||||
  void set_supported_presets(esphome::climate::ClimatePresetMask presets);
 | 
			
		||||
  bool valid_connection() const { return this->protocol_phase_ >= ProtocolPhases::IDLE; };
 | 
			
		||||
  size_t available() noexcept override { return esphome::uart::UARTDevice::available(); };
 | 
			
		||||
  size_t read_array(uint8_t *data, size_t len) noexcept override {
 | 
			
		||||
 
 | 
			
		||||
@@ -1033,9 +1033,9 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
 | 
			
		||||
  {
 | 
			
		||||
    // Swing mode
 | 
			
		||||
    ClimateSwingMode old_swing_mode = this->swing_mode;
 | 
			
		||||
    const std::set<ClimateSwingMode> &swing_modes = traits_.get_supported_swing_modes();
 | 
			
		||||
    bool vertical_swing_supported = swing_modes.find(CLIMATE_SWING_VERTICAL) != swing_modes.end();
 | 
			
		||||
    bool horizontal_swing_supported = swing_modes.find(CLIMATE_SWING_HORIZONTAL) != swing_modes.end();
 | 
			
		||||
    const auto &swing_modes = traits_.get_supported_swing_modes();
 | 
			
		||||
    bool vertical_swing_supported = swing_modes.count(CLIMATE_SWING_VERTICAL);
 | 
			
		||||
    bool horizontal_swing_supported = swing_modes.count(CLIMATE_SWING_HORIZONTAL);
 | 
			
		||||
    if (horizontal_swing_supported &&
 | 
			
		||||
        (packet.control.horizontal_swing_mode == (uint8_t) hon_protocol::HorizontalSwingMode::AUTO)) {
 | 
			
		||||
      if (vertical_swing_supported &&
 | 
			
		||||
@@ -1218,13 +1218,13 @@ void HonClimate::fill_control_messages_queue_() {
 | 
			
		||||
                                                (uint8_t) hon_protocol::DataParameters::QUIET_MODE,
 | 
			
		||||
                                            quiet_mode_buf, 2);
 | 
			
		||||
    }
 | 
			
		||||
    if ((fast_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_BOOST) != presets.end()))) {
 | 
			
		||||
    if ((fast_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_BOOST)) {
 | 
			
		||||
      this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
 | 
			
		||||
                                            (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
 | 
			
		||||
                                                (uint8_t) hon_protocol::DataParameters::FAST_MODE,
 | 
			
		||||
                                            fast_mode_buf, 2);
 | 
			
		||||
    }
 | 
			
		||||
    if ((away_mode_buf[1] != 0xFF) && ((presets.find(climate::ClimatePreset::CLIMATE_PRESET_AWAY) != presets.end()))) {
 | 
			
		||||
    if ((away_mode_buf[1] != 0xFF) && presets.count(climate::ClimatePreset::CLIMATE_PRESET_AWAY)) {
 | 
			
		||||
      this->control_messages_queue_.emplace(haier_protocol::FrameType::CONTROL,
 | 
			
		||||
                                            (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
 | 
			
		||||
                                                (uint8_t) hon_protocol::DataParameters::TEN_DEGREE,
 | 
			
		||||
 
 | 
			
		||||
@@ -16,7 +16,8 @@ void HDC1080Component::setup() {
 | 
			
		||||
 | 
			
		||||
  // if configuration fails - there is a problem
 | 
			
		||||
  if (this->write_register(HDC1080_CMD_CONFIGURATION, config, 2) != i2c::ERROR_OK) {
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    ESP_LOGW(TAG, "Failed to configure HDC1080");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								esphome/components/hdc2010/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/hdc2010/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
CODEOWNERS = ["@optimusprimespace", "@ssieb"]
 | 
			
		||||
							
								
								
									
										111
									
								
								esphome/components/hdc2010/hdc2010.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								esphome/components/hdc2010/hdc2010.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,111 @@
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "hdc2010.h"
 | 
			
		||||
// https://github.com/vigsterkr/homebridge-hdc2010/blob/main/src/hdc2010.js
 | 
			
		||||
// https://github.com/lime-labs/HDC2080-Arduino/blob/master/src/HDC2080.cpp
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace hdc2010 {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "hdc2010";
 | 
			
		||||
 | 
			
		||||
static const uint8_t HDC2010_ADDRESS = 0x40;  // 0b1000000 or 0b1000001 from datasheet
 | 
			
		||||
static const uint8_t HDC2010_CMD_CONFIGURATION_MEASUREMENT = 0x8F;
 | 
			
		||||
static const uint8_t HDC2010_CMD_START_MEASUREMENT = 0xF9;
 | 
			
		||||
static const uint8_t HDC2010_CMD_TEMPERATURE_LOW = 0x00;
 | 
			
		||||
static const uint8_t HDC2010_CMD_TEMPERATURE_HIGH = 0x01;
 | 
			
		||||
static const uint8_t HDC2010_CMD_HUMIDITY_LOW = 0x02;
 | 
			
		||||
static const uint8_t HDC2010_CMD_HUMIDITY_HIGH = 0x03;
 | 
			
		||||
static const uint8_t CONFIG = 0x0E;
 | 
			
		||||
static const uint8_t MEASUREMENT_CONFIG = 0x0F;
 | 
			
		||||
 | 
			
		||||
void HDC2010Component::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Running setup");
 | 
			
		||||
 | 
			
		||||
  const uint8_t data[2] = {
 | 
			
		||||
      0b00000000,  // resolution 14bit for both humidity and temperature
 | 
			
		||||
      0b00000000   // reserved
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  if (!this->write_bytes(HDC2010_CMD_CONFIGURATION_MEASUREMENT, data, 2)) {
 | 
			
		||||
    ESP_LOGW(TAG, "Initial config instruction error");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Set measurement mode to temperature and humidity
 | 
			
		||||
  uint8_t config_contents;
 | 
			
		||||
  this->read_register(MEASUREMENT_CONFIG, &config_contents, 1);
 | 
			
		||||
  config_contents = (config_contents & 0xF9);  // Always set to TEMP_AND_HUMID mode
 | 
			
		||||
  this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1);
 | 
			
		||||
 | 
			
		||||
  // Set rate to manual
 | 
			
		||||
  this->read_register(CONFIG, &config_contents, 1);
 | 
			
		||||
  config_contents &= 0x8F;
 | 
			
		||||
  this->write_bytes(CONFIG, &config_contents, 1);
 | 
			
		||||
 | 
			
		||||
  // Set temperature resolution to 14bit
 | 
			
		||||
  this->read_register(CONFIG, &config_contents, 1);
 | 
			
		||||
  config_contents &= 0x3F;
 | 
			
		||||
  this->write_bytes(CONFIG, &config_contents, 1);
 | 
			
		||||
 | 
			
		||||
  // Set humidity resolution to 14bit
 | 
			
		||||
  this->read_register(CONFIG, &config_contents, 1);
 | 
			
		||||
  config_contents &= 0xCF;
 | 
			
		||||
  this->write_bytes(CONFIG, &config_contents, 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HDC2010Component::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "HDC2010:");
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
  if (this->is_failed()) {
 | 
			
		||||
    ESP_LOGE(TAG, ESP_LOG_MSG_COMM_FAIL);
 | 
			
		||||
  }
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
  LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Humidity", this->humidity_sensor_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HDC2010Component::update() {
 | 
			
		||||
  // Trigger measurement
 | 
			
		||||
  uint8_t config_contents;
 | 
			
		||||
  this->read_register(CONFIG, &config_contents, 1);
 | 
			
		||||
  config_contents |= 0x01;
 | 
			
		||||
  this->write_bytes(MEASUREMENT_CONFIG, &config_contents, 1);
 | 
			
		||||
 | 
			
		||||
  // 1ms delay after triggering the sample
 | 
			
		||||
  set_timeout(1, [this]() {
 | 
			
		||||
    if (this->temperature_sensor_ != nullptr) {
 | 
			
		||||
      float temp = this->read_temp();
 | 
			
		||||
      this->temperature_sensor_->publish_state(temp);
 | 
			
		||||
      ESP_LOGD(TAG, "Temp=%.1f°C", temp);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this->humidity_sensor_ != nullptr) {
 | 
			
		||||
      float humidity = this->read_humidity();
 | 
			
		||||
      this->humidity_sensor_->publish_state(humidity);
 | 
			
		||||
      ESP_LOGD(TAG, "Humidity=%.1f%%", humidity);
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float HDC2010Component::read_temp() {
 | 
			
		||||
  uint8_t byte[2];
 | 
			
		||||
 | 
			
		||||
  this->read_register(HDC2010_CMD_TEMPERATURE_LOW, &byte[0], 1);
 | 
			
		||||
  this->read_register(HDC2010_CMD_TEMPERATURE_HIGH, &byte[1], 1);
 | 
			
		||||
 | 
			
		||||
  uint16_t temp = encode_uint16(byte[1], byte[0]);
 | 
			
		||||
  return (float) temp * 0.0025177f - 40.0f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float HDC2010Component::read_humidity() {
 | 
			
		||||
  uint8_t byte[2];
 | 
			
		||||
 | 
			
		||||
  this->read_register(HDC2010_CMD_HUMIDITY_LOW, &byte[0], 1);
 | 
			
		||||
  this->read_register(HDC2010_CMD_HUMIDITY_HIGH, &byte[1], 1);
 | 
			
		||||
 | 
			
		||||
  uint16_t humidity = encode_uint16(byte[1], byte[0]);
 | 
			
		||||
  return (float) humidity * 0.001525879f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace hdc2010
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										32
									
								
								esphome/components/hdc2010/hdc2010.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										32
									
								
								esphome/components/hdc2010/hdc2010.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,32 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/i2c/i2c.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace hdc2010 {
 | 
			
		||||
 | 
			
		||||
class HDC2010Component : public PollingComponent, public i2c::I2CDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  void set_temperature_sensor(sensor::Sensor *temperature) { this->temperature_sensor_ = temperature; }
 | 
			
		||||
 | 
			
		||||
  void set_humidity_sensor(sensor::Sensor *humidity) { this->humidity_sensor_ = humidity; }
 | 
			
		||||
 | 
			
		||||
  /// Setup the sensor and check for connection.
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  /// Retrieve the latest sensor values. This operation takes approximately 16ms.
 | 
			
		||||
  void update() override;
 | 
			
		||||
 | 
			
		||||
  float read_temp();
 | 
			
		||||
 | 
			
		||||
  float read_humidity();
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  sensor::Sensor *temperature_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *humidity_sensor_{nullptr};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace hdc2010
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										56
									
								
								esphome/components/hdc2010/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										56
									
								
								esphome/components/hdc2010/sensor.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,56 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import i2c, sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_HUMIDITY,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
 | 
			
		||||
hdc2010_ns = cg.esphome_ns.namespace("hdc2010")
 | 
			
		||||
HDC2010Component = hdc2010_ns.class_(
 | 
			
		||||
    "HDC2010Component", cg.PollingComponent, i2c.I2CDevice
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(HDC2010Component),
 | 
			
		||||
            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_CELSIUS,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
                device_class=DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
    .extend(i2c.i2c_device_schema(0x40))
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await i2c.register_i2c_device(var, config)
 | 
			
		||||
 | 
			
		||||
    if temperature_config := config.get(CONF_TEMPERATURE):
 | 
			
		||||
        sens = await sensor.new_sensor(temperature_config)
 | 
			
		||||
        cg.add(var.set_temperature_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if humidity_config := config.get(CONF_HUMIDITY):
 | 
			
		||||
        sens = await sensor.new_sensor(humidity_config)
 | 
			
		||||
        cg.add(var.set_humidity_sensor(sens))
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user