mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 00:51:49 +00:00 
			
		
		
		
	Compare commits
	
		
			221 Commits
		
	
	
		
			ci_impact_
			...
			display_wr
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					7504219a2d | ||
| 
						 | 
					6947df56d5 | ||
| 
						 | 
					29c97686fa | ||
| 
						 | 
					f3087abd55 | ||
| 
						 | 
					f55dfc7f5d | ||
| 
						 | 
					c5a9d30362 | ||
| 
						 | 
					fae06133d4 | ||
| 
						 | 
					fd64585f99 | ||
| 
						 | 
					077cce9848 | ||
| 
						 | 
					bd87e56bc7 | ||
| 
						 | 
					58235049e3 | ||
| 
						 | 
					29ed3c20af | ||
| 
						 | 
					08aae39ea4 | ||
| 
						 | 
					03fd114371 | ||
| 
						 | 
					918650f15a | ||
| 
						 | 
					287f65cbaf | ||
| 
						 | 
					f18c70a256 | ||
| 
						 | 
					6fb490f49b | ||
| 
						 | 
					66cf7c3a3b | ||
| 
						 | 
					f29021b5ef | ||
| 
						 | 
					7549ca4d39 | ||
| 
						 | 
					33e7a2101b | ||
| 
						 | 
					59a216bfcb | ||
| 
						 | 
					09d89000ad | ||
| 
						 | 
					b6c9ece0e6 | ||
| 
						 | 
					7169556722 | ||
| 
						 | 
					f6e4c0cb52 | ||
| 
						 | 
					f3634edc22 | ||
| 
						 | 
					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 | ||
| 
						 | 
					40823df7bc | ||
| 
						 | 
					5e1019a6fa | ||
| 
						 | 
					f3cdbd0a05 | ||
| 
						 | 
					ddf1b67e49 | ||
| 
						 | 
					b4d9fddd07 | ||
| 
						 | 
					25f03074ab | ||
| 
						 | 
					590f6ff70b | ||
| 
						 | 
					a33ed5e47b | ||
| 
						 | 
					c11a9bb97f | ||
| 
						 | 
					acef2085d9 | ||
| 
						 | 
					865663ce5f | ||
| 
						 | 
					ae010fd6f1 | ||
| 
						 | 
					91a10d0e36 | ||
| 
						 | 
					d5c36eaf2a | 
@@ -1 +1 @@
 | 
			
		||||
d7693a1e996cacd4a3d1c9a16336799c2a8cc3db02e4e74084151ce964581248
 | 
			
		||||
3d46b63015d761c85ca9cb77ab79a389509e5776701fb22aed16e7b79d432c0c
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							@@ -62,7 +62,7 @@ jobs:
 | 
			
		||||
        run: git diff
 | 
			
		||||
      - if: failure()
 | 
			
		||||
        name: Archive artifacts
 | 
			
		||||
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
 | 
			
		||||
        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          name: generated-proto-files
 | 
			
		||||
          path: |
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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
 | 
			
		||||
							
								
								
									
										304
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										304
									
								
								.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,12 +170,17 @@ jobs:
 | 
			
		||||
    outputs:
 | 
			
		||||
      integration-tests: ${{ steps.determine.outputs.integration-tests }}
 | 
			
		||||
      clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
 | 
			
		||||
      clang-tidy-mode: ${{ steps.determine.outputs.clang-tidy-mode }}
 | 
			
		||||
      python-linters: ${{ steps.determine.outputs.python-linters }}
 | 
			
		||||
      changed-components: ${{ steps.determine.outputs.changed-components }}
 | 
			
		||||
      changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
 | 
			
		||||
      directly-changed-components-with-tests: ${{ steps.determine.outputs.directly-changed-components-with-tests }}
 | 
			
		||||
      component-test-count: ${{ steps.determine.outputs.component-test-count }}
 | 
			
		||||
      changed-cpp-file-count: ${{ steps.determine.outputs.changed-cpp-file-count }}
 | 
			
		||||
      memory_impact: ${{ steps.determine.outputs.memory-impact }}
 | 
			
		||||
      cpp-unit-tests-run-all: ${{ steps.determine.outputs.cpp-unit-tests-run-all }}
 | 
			
		||||
      cpp-unit-tests-components: ${{ steps.determine.outputs.cpp-unit-tests-components }}
 | 
			
		||||
      component-test-batches: ${{ steps.determine.outputs.component-test-batches }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
@@ -200,12 +205,17 @@ jobs:
 | 
			
		||||
          # Extract individual fields
 | 
			
		||||
          echo "integration-tests=$(echo "$output" | jq -r '.integration_tests')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "clang-tidy-mode=$(echo "$output" | jq -r '.clang_tidy_mode')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "directly-changed-components-with-tests=$(echo "$output" | jq -c '.directly_changed_components_with_tests')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "changed-cpp-file-count=$(echo "$output" | jq -r '.changed_cpp_file_count')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "memory-impact=$(echo "$output" | jq -c '.memory_impact')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "cpp-unit-tests-run-all=$(echo "$output" | jq -r '.cpp_unit_tests_run_all')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "cpp-unit-tests-components=$(echo "$output" | jq -c '.cpp_unit_tests_components')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "component-test-batches=$(echo "$output" | jq -c '.component_test_batches')" >> $GITHUB_OUTPUT
 | 
			
		||||
 | 
			
		||||
  integration-tests:
 | 
			
		||||
    name: Run integration tests
 | 
			
		||||
@@ -243,7 +253,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:
 | 
			
		||||
@@ -261,22 +298,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
 | 
			
		||||
@@ -357,45 +378,165 @@ jobs:
 | 
			
		||||
        # yamllint disable-line rule:line-length
 | 
			
		||||
        if: always()
 | 
			
		||||
 | 
			
		||||
  test-build-components-splitter:
 | 
			
		||||
    name: Split components for intelligent grouping (40 weighted per batch)
 | 
			
		||||
  clang-tidy-nosplit:
 | 
			
		||||
    name: Run script/clang-tidy for ESP32 Arduino
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
      - determine-jobs
 | 
			
		||||
    if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
 | 
			
		||||
    outputs:
 | 
			
		||||
      matrix: ${{ steps.split.outputs.components }}
 | 
			
		||||
    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: Split components intelligently based on bus configurations
 | 
			
		||||
        id: split
 | 
			
		||||
 | 
			
		||||
      - 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
 | 
			
		||||
 | 
			
		||||
          # Use intelligent splitter that groups components with same bus configs
 | 
			
		||||
          components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}'
 | 
			
		||||
 | 
			
		||||
          # Only isolate directly changed components when targeting dev branch
 | 
			
		||||
          # For beta/release branches, group everything for faster CI
 | 
			
		||||
          if [[ "${{ github.base_ref }}" == beta* ]] || [[ "${{ github.base_ref }}" == release* ]]; then
 | 
			
		||||
            directly_changed='[]'
 | 
			
		||||
            echo "Target branch: ${{ github.base_ref }} - grouping all components"
 | 
			
		||||
          if python script/clang_tidy_hash.py --check; then
 | 
			
		||||
            echo "full_scan=true" >> $GITHUB_OUTPUT
 | 
			
		||||
            echo "reason=hash_changed" >> $GITHUB_OUTPUT
 | 
			
		||||
          else
 | 
			
		||||
            directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}'
 | 
			
		||||
            echo "Target branch: ${{ github.base_ref }} - isolating directly changed components"
 | 
			
		||||
            echo "full_scan=false" >> $GITHUB_OUTPUT
 | 
			
		||||
            echo "reason=normal" >> $GITHUB_OUTPUT
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
          echo "Splitting components intelligently..."
 | 
			
		||||
          output=$(python3 script/split_components_for_ci.py --components "$components" --directly-changed "$directly_changed" --batch-size 40 --output github)
 | 
			
		||||
      - name: Run clang-tidy
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
 | 
			
		||||
            echo "Running FULL clang-tidy scan (hash changed)"
 | 
			
		||||
            script/clang-tidy --all-headers --fix --environment esp32-arduino-tidy
 | 
			
		||||
          else
 | 
			
		||||
            echo "Running clang-tidy on changed files only"
 | 
			
		||||
            script/clang-tidy --all-headers --fix --changed --environment esp32-arduino-tidy
 | 
			
		||||
          fi
 | 
			
		||||
        env:
 | 
			
		||||
          # Also cache libdeps, store them in a ~/.platformio subfolder
 | 
			
		||||
          PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
 | 
			
		||||
 | 
			
		||||
          echo "$output" >> $GITHUB_OUTPUT
 | 
			
		||||
      - name: Suggested changes
 | 
			
		||||
        run: script/ci-suggest-changes
 | 
			
		||||
        if: always()
 | 
			
		||||
 | 
			
		||||
  clang-tidy-split:
 | 
			
		||||
    name: ${{ matrix.name }}
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
      - determine-jobs
 | 
			
		||||
    if: needs.determine-jobs.outputs.clang-tidy-mode == 'split'
 | 
			
		||||
    env:
 | 
			
		||||
      GH_TOKEN: ${{ github.token }}
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      max-parallel: 2
 | 
			
		||||
      matrix:
 | 
			
		||||
        include:
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 Arduino 1/4
 | 
			
		||||
            options: --environment esp32-arduino-tidy --split-num 4 --split-at 1
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 Arduino 2/4
 | 
			
		||||
            options: --environment esp32-arduino-tidy --split-num 4 --split-at 2
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 Arduino 3/4
 | 
			
		||||
            options: --environment esp32-arduino-tidy --split-num 4 --split-at 3
 | 
			
		||||
          - id: clang-tidy
 | 
			
		||||
            name: Run script/clang-tidy for ESP32 Arduino 4/4
 | 
			
		||||
            options: --environment esp32-arduino-tidy --split-num 4 --split-at 4
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          # Need history for HEAD~1 to work for checking changed files
 | 
			
		||||
          fetch-depth: 2
 | 
			
		||||
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        if: github.ref == 'refs/heads/dev'
 | 
			
		||||
        uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
 | 
			
		||||
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        if: github.ref != 'refs/heads/dev'
 | 
			
		||||
        uses: actions/cache/restore@0057852bfaa89a56745cba8c7296529d2fc39830 # v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: platformio-tidyesp32-${{ hashFiles('platformio.ini') }}
 | 
			
		||||
 | 
			
		||||
      - name: Register problem matchers
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/gcc.json"
 | 
			
		||||
          echo "::add-matcher::.github/workflows/matchers/clang-tidy.json"
 | 
			
		||||
 | 
			
		||||
      - name: Check if full clang-tidy scan needed
 | 
			
		||||
        id: check_full_scan
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          if python script/clang_tidy_hash.py --check; then
 | 
			
		||||
            echo "full_scan=true" >> $GITHUB_OUTPUT
 | 
			
		||||
            echo "reason=hash_changed" >> $GITHUB_OUTPUT
 | 
			
		||||
          else
 | 
			
		||||
            echo "full_scan=false" >> $GITHUB_OUTPUT
 | 
			
		||||
            echo "reason=normal" >> $GITHUB_OUTPUT
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - name: Run clang-tidy
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          if [ "${{ steps.check_full_scan.outputs.full_scan }}" = "true" ]; then
 | 
			
		||||
            echo "Running FULL clang-tidy scan (hash changed)"
 | 
			
		||||
            script/clang-tidy --all-headers --fix ${{ matrix.options }}
 | 
			
		||||
          else
 | 
			
		||||
            echo "Running clang-tidy on changed files only"
 | 
			
		||||
            script/clang-tidy --all-headers --fix --changed ${{ matrix.options }}
 | 
			
		||||
          fi
 | 
			
		||||
        env:
 | 
			
		||||
          # Also cache libdeps, store them in a ~/.platformio subfolder
 | 
			
		||||
          PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps
 | 
			
		||||
 | 
			
		||||
      - name: Suggested changes
 | 
			
		||||
        run: script/ci-suggest-changes
 | 
			
		||||
        if: always()
 | 
			
		||||
 | 
			
		||||
  test-build-components-split:
 | 
			
		||||
    name: Test components batch (${{ matrix.components }})
 | 
			
		||||
@@ -403,13 +544,12 @@ jobs:
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
      - determine-jobs
 | 
			
		||||
      - test-build-components-splitter
 | 
			
		||||
    if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) > 0
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      max-parallel: ${{ (startsWith(github.base_ref, 'beta') || startsWith(github.base_ref, 'release')) && 8 || 4 }}
 | 
			
		||||
      matrix:
 | 
			
		||||
        components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }}
 | 
			
		||||
        components: ${{ fromJson(needs.determine-jobs.outputs.component-test-batches) }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Show disk space
 | 
			
		||||
        run: |
 | 
			
		||||
@@ -641,6 +781,12 @@ jobs:
 | 
			
		||||
              --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
 | 
			
		||||
@@ -664,7 +810,7 @@ jobs:
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
      - name: Upload memory analysis JSON
 | 
			
		||||
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
 | 
			
		||||
        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          name: memory-analysis-target
 | 
			
		||||
          path: memory-analysis-target.json
 | 
			
		||||
@@ -720,8 +866,15 @@ jobs:
 | 
			
		||||
            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
 | 
			
		||||
        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          name: memory-analysis-pr
 | 
			
		||||
          path: memory-analysis-pr.json
 | 
			
		||||
@@ -736,10 +889,12 @@ jobs:
 | 
			
		||||
      - determine-jobs
 | 
			
		||||
      - memory-impact-target-branch
 | 
			
		||||
      - memory-impact-pr-branch
 | 
			
		||||
    if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.memory_impact).should_run == 'true' && needs.memory-impact-target-branch.outputs.skip != 'true'
 | 
			
		||||
    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
 | 
			
		||||
@@ -749,65 +904,29 @@ jobs:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: Download target analysis JSON
 | 
			
		||||
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
 | 
			
		||||
        uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          name: memory-analysis-target
 | 
			
		||||
          path: ./memory-analysis
 | 
			
		||||
        continue-on-error: true
 | 
			
		||||
      - name: Download PR analysis JSON
 | 
			
		||||
        uses: actions/download-artifact@d3f86a106a0bac45b974a628896c90dbdf5c8093 # v4.3.0
 | 
			
		||||
        uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          name: memory-analysis-pr
 | 
			
		||||
          path: ./memory-analysis
 | 
			
		||||
        continue-on-error: true
 | 
			
		||||
      - name: Post or update PR comment
 | 
			
		||||
        env:
 | 
			
		||||
          GH_TOKEN: ${{ github.token }}
 | 
			
		||||
          COMPONENTS: ${{ toJSON(fromJSON(needs.determine-jobs.outputs.memory_impact).components) }}
 | 
			
		||||
          PLATFORM: ${{ fromJSON(needs.determine-jobs.outputs.memory_impact).platform }}
 | 
			
		||||
          TARGET_RAM: ${{ needs.memory-impact-target-branch.outputs.ram_usage }}
 | 
			
		||||
          TARGET_FLASH: ${{ needs.memory-impact-target-branch.outputs.flash_usage }}
 | 
			
		||||
          PR_RAM: ${{ needs.memory-impact-pr-branch.outputs.ram_usage }}
 | 
			
		||||
          PR_FLASH: ${{ needs.memory-impact-pr-branch.outputs.flash_usage }}
 | 
			
		||||
          TARGET_CACHE_HIT: ${{ needs.memory-impact-target-branch.outputs.cache_hit }}
 | 
			
		||||
          PR_NUMBER: ${{ github.event.pull_request.number }}
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
 | 
			
		||||
          # Check if analysis JSON files exist
 | 
			
		||||
          target_json_arg=""
 | 
			
		||||
          pr_json_arg=""
 | 
			
		||||
 | 
			
		||||
          if [ -f ./memory-analysis/memory-analysis-target.json ]; then
 | 
			
		||||
            echo "Found target analysis JSON"
 | 
			
		||||
            target_json_arg="--target-json ./memory-analysis/memory-analysis-target.json"
 | 
			
		||||
          else
 | 
			
		||||
            echo "No target analysis JSON found"
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
          if [ -f ./memory-analysis/memory-analysis-pr.json ]; then
 | 
			
		||||
            echo "Found PR analysis JSON"
 | 
			
		||||
            pr_json_arg="--pr-json ./memory-analysis/memory-analysis-pr.json"
 | 
			
		||||
          else
 | 
			
		||||
            echo "No PR analysis JSON found"
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
          # Add cache flag if target was cached
 | 
			
		||||
          cache_flag=""
 | 
			
		||||
          if [ "$TARGET_CACHE_HIT" == "true" ]; then
 | 
			
		||||
            cache_flag="--target-cache-hit"
 | 
			
		||||
          fi
 | 
			
		||||
 | 
			
		||||
          # 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 "${{ github.event.pull_request.number }}" \
 | 
			
		||||
            --components "$COMPONENTS" \
 | 
			
		||||
            --platform "$PLATFORM" \
 | 
			
		||||
            --target-ram "$TARGET_RAM" \
 | 
			
		||||
            --target-flash "$TARGET_FLASH" \
 | 
			
		||||
            --pr-ram "$PR_RAM" \
 | 
			
		||||
            --pr-flash "$PR_FLASH" \
 | 
			
		||||
            $target_json_arg \
 | 
			
		||||
            $pr_json_arg \
 | 
			
		||||
            $cache_flag
 | 
			
		||||
            --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
 | 
			
		||||
@@ -818,9 +937,10 @@ 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
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
								
							@@ -58,7 +58,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      # Initializes the CodeQL tools for scanning.
 | 
			
		||||
      - name: Initialize CodeQL
 | 
			
		||||
        uses: github/codeql-action/init@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
 | 
			
		||||
        uses: github/codeql-action/init@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
 | 
			
		||||
        with:
 | 
			
		||||
          languages: ${{ matrix.language }}
 | 
			
		||||
          build-mode: ${{ matrix.build-mode }}
 | 
			
		||||
@@ -86,6 +86,6 @@ jobs:
 | 
			
		||||
          exit 1
 | 
			
		||||
 | 
			
		||||
      - name: Perform CodeQL Analysis
 | 
			
		||||
        uses: github/codeql-action/analyze@16140ae1a102900babc80a33c44059580f687047 # v4.30.9
 | 
			
		||||
        uses: github/codeql-action/analyze@0499de31b99561a6d14a36a5f662c2a54f91beee # v4.31.2
 | 
			
		||||
        with:
 | 
			
		||||
          category: "/language:${{matrix.language}}"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@@ -138,7 +138,7 @@ jobs:
 | 
			
		||||
      #     version: ${{ needs.init.outputs.tag }}
 | 
			
		||||
 | 
			
		||||
      - name: Upload digests
 | 
			
		||||
        uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2
 | 
			
		||||
        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          name: digests-${{ matrix.platform.arch }}
 | 
			
		||||
          path: /tmp/digests
 | 
			
		||||
@@ -171,7 +171,7 @@ jobs:
 | 
			
		||||
      - uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
 | 
			
		||||
      - name: Download digests
 | 
			
		||||
        uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0
 | 
			
		||||
        uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          pattern: digests-*
 | 
			
		||||
          path: /tmp/digests
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								.github/workflows/status-check-labels.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/status-check-labels.yml
									
									
									
									
										vendored
									
									
								
							@@ -14,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
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ ci:
 | 
			
		||||
repos:
 | 
			
		||||
  - repo: https://github.com/astral-sh/ruff-pre-commit
 | 
			
		||||
    # Ruff version.
 | 
			
		||||
    rev: v0.14.1
 | 
			
		||||
    rev: v0.14.2
 | 
			
		||||
    hooks:
 | 
			
		||||
      # Run the linter.
 | 
			
		||||
      - id: ruff
 | 
			
		||||
 
 | 
			
		||||
@@ -70,6 +70,7 @@ esphome/components/bl0939/* @ziceva
 | 
			
		||||
esphome/components/bl0940/* @dan-s-github @tobias-
 | 
			
		||||
esphome/components/bl0942/* @dbuezas @dwmw2
 | 
			
		||||
esphome/components/ble_client/* @buxtronix @clydebarrow
 | 
			
		||||
esphome/components/ble_nus/* @tomaszduda23
 | 
			
		||||
esphome/components/bluetooth_proxy/* @bdraco @jesserockz
 | 
			
		||||
esphome/components/bme280_base/* @esphome/core
 | 
			
		||||
esphome/components/bme280_spi/* @apbodrov
 | 
			
		||||
@@ -160,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
 | 
			
		||||
@@ -199,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
 | 
			
		||||
@@ -173,19 +207,21 @@ def choose_upload_log_host(
 | 
			
		||||
                    if has_mqtt_logging():
 | 
			
		||||
                        resolved.append("MQTT")
 | 
			
		||||
 | 
			
		||||
                    if has_api() and has_non_ip_address():
 | 
			
		||||
                    if has_api() and has_non_ip_address() and has_resolvable_address():
 | 
			
		||||
                        resolved.extend(_resolve_with_cache(CORE.address, purpose))
 | 
			
		||||
 | 
			
		||||
                elif purpose == Purpose.UPLOADING:
 | 
			
		||||
                    if has_ota() and has_mqtt_ip_lookup():
 | 
			
		||||
                        resolved.append("MQTTIP")
 | 
			
		||||
 | 
			
		||||
                    if has_ota() and has_non_ip_address():
 | 
			
		||||
                    if has_ota() and has_non_ip_address() and has_resolvable_address():
 | 
			
		||||
                        resolved.extend(_resolve_with_cache(CORE.address, purpose))
 | 
			
		||||
            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
 | 
			
		||||
@@ -282,7 +318,17 @@ def has_resolvable_address() -> bool:
 | 
			
		||||
    """Check if CORE.address is resolvable (via mDNS, DNS, or is an IP address)."""
 | 
			
		||||
    # Any address (IP, mDNS hostname, or regular DNS hostname) is resolvable
 | 
			
		||||
    # The resolve_ip_address() function in helpers.py handles all types via AsyncResolver
 | 
			
		||||
    return CORE.address is not None
 | 
			
		||||
    if CORE.address is None:
 | 
			
		||||
        return False
 | 
			
		||||
 | 
			
		||||
    if has_ip_address():
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    if has_mdns():
 | 
			
		||||
        return True
 | 
			
		||||
 | 
			
		||||
    # .local mDNS hostnames are only resolvable if mDNS is enabled
 | 
			
		||||
    return not CORE.address.endswith(".local")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def mqtt_get_ip(config: ConfigType, username: str, password: str, client_id: str):
 | 
			
		||||
@@ -890,6 +936,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:
 | 
			
		||||
@@ -1005,6 +1099,7 @@ POST_CONFIG_ACTIONS = {
 | 
			
		||||
    "idedata": command_idedata,
 | 
			
		||||
    "rename": command_rename,
 | 
			
		||||
    "discover": command_discover,
 | 
			
		||||
    "analyze-memory": command_analyze_memory,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SIMPLE_CONFIG_ACTIONS = [
 | 
			
		||||
@@ -1290,6 +1385,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>.
 | 
			
		||||
    #
 | 
			
		||||
 
 | 
			
		||||
@@ -231,9 +231,22 @@ class MemoryAnalyzerCLI(MemoryAnalyzer):
 | 
			
		||||
                api_component = (name, mem)
 | 
			
		||||
                break
 | 
			
		||||
 | 
			
		||||
        # Combine all components to analyze: top ESPHome + all external + API if not already included
 | 
			
		||||
        components_to_analyze = list(top_esphome_components) + list(
 | 
			
		||||
            top_external_components
 | 
			
		||||
        # 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)
 | 
			
		||||
 
 | 
			
		||||
@@ -127,40 +127,39 @@ SYMBOL_PATTERNS = {
 | 
			
		||||
        "tryget_socket_unconn",
 | 
			
		||||
        "cs_create_ctrl_sock",
 | 
			
		||||
        "netbuf_alloc",
 | 
			
		||||
        "tcp_",  # TCP protocol functions
 | 
			
		||||
        "udp_",  # UDP protocol functions
 | 
			
		||||
        "lwip_",  # LwIP stack functions
 | 
			
		||||
        "eagle_lwip",  # ESP-specific LwIP functions
 | 
			
		||||
        "new_linkoutput",  # Link output function
 | 
			
		||||
        "acd_",  # Address Conflict Detection (ACD)
 | 
			
		||||
        "eth_",  # Ethernet functions
 | 
			
		||||
        "mac_enable_bb",  # MAC baseband enable
 | 
			
		||||
        "reassemble_and_dispatch",  # Packet reassembly
 | 
			
		||||
    ],
 | 
			
		||||
    # dhcp must come before libc to avoid "dhcp_select" matching "select" pattern
 | 
			
		||||
    "dhcp": ["dhcp", "handle_dhcp"],
 | 
			
		||||
    "ipv6_stack": ["nd6_", "ip6_", "mld6_", "icmp6_", "icmp6_input"],
 | 
			
		||||
    "wifi_stack": [
 | 
			
		||||
        "ieee80211",
 | 
			
		||||
        "hostap",
 | 
			
		||||
        "sta_",
 | 
			
		||||
        "ap_",
 | 
			
		||||
        "scan_",
 | 
			
		||||
        "wifi_",
 | 
			
		||||
        "wpa_",
 | 
			
		||||
        "wps_",
 | 
			
		||||
        "esp_wifi",
 | 
			
		||||
        "cnx_",
 | 
			
		||||
        "wpa3_",
 | 
			
		||||
        "sae_",
 | 
			
		||||
        "wDev_",
 | 
			
		||||
        "ic_",
 | 
			
		||||
        "mac_",
 | 
			
		||||
        "esf_buf",
 | 
			
		||||
        "gWpaSm",
 | 
			
		||||
        "sm_WPA",
 | 
			
		||||
        "eapol_",
 | 
			
		||||
        "owe_",
 | 
			
		||||
        "wifiLowLevelInit",
 | 
			
		||||
        "s_do_mapping",
 | 
			
		||||
        "gScanStruct",
 | 
			
		||||
        "ppSearchTxframe",
 | 
			
		||||
        "ppMapWaitTxq",
 | 
			
		||||
        "ppFillAMPDUBar",
 | 
			
		||||
        "ppCheckTxConnTrafficIdle",
 | 
			
		||||
        "ppCalTkipMic",
 | 
			
		||||
    # Order matters! More specific categories must come before general ones.
 | 
			
		||||
    # mdns must come before bluetooth to avoid "_mdns_disable_pcb" matching "ble_" pattern
 | 
			
		||||
    "mdns_lib": ["mdns"],
 | 
			
		||||
    # memory_mgmt must come before wifi_stack to catch mmu_hal_* symbols
 | 
			
		||||
    "memory_mgmt": [
 | 
			
		||||
        "mem_",
 | 
			
		||||
        "memory_",
 | 
			
		||||
        "tlsf_",
 | 
			
		||||
        "memp_",
 | 
			
		||||
        "pbuf_",
 | 
			
		||||
        "pbuf_alloc",
 | 
			
		||||
        "pbuf_copy_partial_pbuf",
 | 
			
		||||
        "esp_mmu_map",
 | 
			
		||||
        "mmu_hal_",
 | 
			
		||||
        "s_do_mapping",  # Memory mapping function, not WiFi
 | 
			
		||||
        "hash_map_",  # Hash map data structure
 | 
			
		||||
        "umm_assimilate",  # UMM malloc assimilation
 | 
			
		||||
    ],
 | 
			
		||||
    "bluetooth": ["bt_", "ble_", "l2c_", "gatt_", "gap_", "hci_", "BT_init"],
 | 
			
		||||
    "wifi_bt_coex": ["coex"],
 | 
			
		||||
    # Bluetooth categories must come BEFORE wifi_stack to avoid misclassification
 | 
			
		||||
    # Many BLE symbols contain patterns like "ble_" that would otherwise match wifi patterns
 | 
			
		||||
    "bluetooth_rom": ["r_ble", "r_lld", "r_llc", "r_llm"],
 | 
			
		||||
    "bluedroid_bt": [
 | 
			
		||||
        "bluedroid",
 | 
			
		||||
@@ -207,6 +206,61 @@ SYMBOL_PATTERNS = {
 | 
			
		||||
        "copy_extra_byte_in_db",
 | 
			
		||||
        "parse_read_local_supported_commands_response",
 | 
			
		||||
    ],
 | 
			
		||||
    "bluetooth": [
 | 
			
		||||
        "bt_",
 | 
			
		||||
        "_ble_",  # More specific than "ble_" to avoid matching "able_", "enable_", "disable_"
 | 
			
		||||
        "l2c_",
 | 
			
		||||
        "l2ble_",  # L2CAP for BLE
 | 
			
		||||
        "gatt_",
 | 
			
		||||
        "gap_",
 | 
			
		||||
        "hci_",
 | 
			
		||||
        "btsnd_hcic_",  # Bluetooth HCI command send functions
 | 
			
		||||
        "BT_init",
 | 
			
		||||
        "BT_tx_",  # Bluetooth transmit functions
 | 
			
		||||
        "esp_ble_",  # Catch esp_ble_* functions
 | 
			
		||||
    ],
 | 
			
		||||
    "bluetooth_ll": [
 | 
			
		||||
        "llm_",  # Link layer manager
 | 
			
		||||
        "llc_",  # Link layer control
 | 
			
		||||
        "lld_",  # Link layer driver
 | 
			
		||||
        "ld_acl_",  # Link layer ACL (Asynchronous Connection-Oriented)
 | 
			
		||||
        "llcp_",  # Link layer control protocol
 | 
			
		||||
        "lmp_",  # Link manager protocol
 | 
			
		||||
    ],
 | 
			
		||||
    "wifi_bt_coex": ["coex"],
 | 
			
		||||
    "wifi_stack": [
 | 
			
		||||
        "ieee80211",
 | 
			
		||||
        "hostap",
 | 
			
		||||
        "sta_",
 | 
			
		||||
        "wifi_ap_",  # More specific than "ap_" to avoid matching "cap_", "map_"
 | 
			
		||||
        "wifi_scan_",  # More specific than "scan_" to avoid matching "_scan_" in other contexts
 | 
			
		||||
        "wifi_",
 | 
			
		||||
        "wpa_",
 | 
			
		||||
        "wps_",
 | 
			
		||||
        "esp_wifi",
 | 
			
		||||
        "cnx_",
 | 
			
		||||
        "wpa3_",
 | 
			
		||||
        "sae_",
 | 
			
		||||
        "wDev_",
 | 
			
		||||
        "ic_mac_",  # More specific than "mac_" to avoid matching emac_
 | 
			
		||||
        "esf_buf",
 | 
			
		||||
        "gWpaSm",
 | 
			
		||||
        "sm_WPA",
 | 
			
		||||
        "eapol_",
 | 
			
		||||
        "owe_",
 | 
			
		||||
        "wifiLowLevelInit",
 | 
			
		||||
        # Removed "s_do_mapping" - this is memory management, not WiFi
 | 
			
		||||
        "gScanStruct",
 | 
			
		||||
        "ppSearchTxframe",
 | 
			
		||||
        "ppMapWaitTxq",
 | 
			
		||||
        "ppFillAMPDUBar",
 | 
			
		||||
        "ppCheckTxConnTrafficIdle",
 | 
			
		||||
        "ppCalTkipMic",
 | 
			
		||||
        "phy_force_wifi",
 | 
			
		||||
        "phy_unforce_wifi",
 | 
			
		||||
        "write_wifi_chan",
 | 
			
		||||
        "wifi_track_pll",
 | 
			
		||||
    ],
 | 
			
		||||
    "crypto_math": [
 | 
			
		||||
        "ecp_",
 | 
			
		||||
        "bignum_",
 | 
			
		||||
@@ -231,13 +285,36 @@ SYMBOL_PATTERNS = {
 | 
			
		||||
        "p_256_init_curve",
 | 
			
		||||
        "shift_sub_rows",
 | 
			
		||||
        "rshift",
 | 
			
		||||
        "rijndaelEncrypt",  # AES Rijndael encryption
 | 
			
		||||
    ],
 | 
			
		||||
    # System and Arduino core functions must come before libc
 | 
			
		||||
    "esp_system": [
 | 
			
		||||
        "system_",  # ESP system functions
 | 
			
		||||
        "postmortem_",  # Postmortem reporting
 | 
			
		||||
    ],
 | 
			
		||||
    "arduino_core": [
 | 
			
		||||
        "pinMode",
 | 
			
		||||
        "resetPins",
 | 
			
		||||
        "millis",
 | 
			
		||||
        "micros",
 | 
			
		||||
        "delay(",  # More specific - Arduino delay function with parenthesis
 | 
			
		||||
        "delayMicroseconds",
 | 
			
		||||
        "digitalWrite",
 | 
			
		||||
        "digitalRead",
 | 
			
		||||
    ],
 | 
			
		||||
    "sntp": ["sntp_", "sntp_recv"],
 | 
			
		||||
    "scheduler": [
 | 
			
		||||
        "run_scheduled_",
 | 
			
		||||
        "compute_scheduled_",
 | 
			
		||||
        "event_TaskQueue",
 | 
			
		||||
    ],
 | 
			
		||||
    "hw_crypto": ["esp_aes", "esp_sha", "esp_rsa", "esp_bignum", "esp_mpi"],
 | 
			
		||||
    "libc": [
 | 
			
		||||
        "printf",
 | 
			
		||||
        "scanf",
 | 
			
		||||
        "malloc",
 | 
			
		||||
        "free",
 | 
			
		||||
        "_free",  # More specific than "free" to match _free, __free_r, etc. but not arbitrary "free" substring
 | 
			
		||||
        "umm_free",  # UMM malloc free function
 | 
			
		||||
        "memcpy",
 | 
			
		||||
        "memset",
 | 
			
		||||
        "strcpy",
 | 
			
		||||
@@ -259,7 +336,7 @@ SYMBOL_PATTERNS = {
 | 
			
		||||
        "_setenv_r",
 | 
			
		||||
        "_tzset_unlocked_r",
 | 
			
		||||
        "__tzcalc_limits",
 | 
			
		||||
        "select",
 | 
			
		||||
        "_select",  # More specific than "select" to avoid matching "dhcp_select", etc.
 | 
			
		||||
        "scalbnf",
 | 
			
		||||
        "strtof",
 | 
			
		||||
        "strtof_l",
 | 
			
		||||
@@ -316,8 +393,24 @@ SYMBOL_PATTERNS = {
 | 
			
		||||
        "CSWTCH$",
 | 
			
		||||
        "dst$",
 | 
			
		||||
        "sulp",
 | 
			
		||||
        "_strtol_l",  # String to long with locale
 | 
			
		||||
        "__cvt",  # Convert
 | 
			
		||||
        "__utoa",  # Unsigned to ASCII
 | 
			
		||||
        "__global_locale",  # Global locale
 | 
			
		||||
        "_ctype_",  # Character type
 | 
			
		||||
        "impure_data",  # Impure data
 | 
			
		||||
    ],
 | 
			
		||||
    "string_ops": [
 | 
			
		||||
        "strcmp",
 | 
			
		||||
        "strncmp",
 | 
			
		||||
        "strchr",
 | 
			
		||||
        "strstr",
 | 
			
		||||
        "strtok",
 | 
			
		||||
        "strdup",
 | 
			
		||||
        "strncasecmp_P",  # String compare (case insensitive, from program memory)
 | 
			
		||||
        "strnlen_P",  # String length (from program memory)
 | 
			
		||||
        "strncat_P",  # String concatenate (from program memory)
 | 
			
		||||
    ],
 | 
			
		||||
    "string_ops": ["strcmp", "strncmp", "strchr", "strstr", "strtok", "strdup"],
 | 
			
		||||
    "memory_alloc": ["malloc", "calloc", "realloc", "free", "_sbrk"],
 | 
			
		||||
    "file_io": [
 | 
			
		||||
        "fread",
 | 
			
		||||
@@ -338,10 +431,26 @@ SYMBOL_PATTERNS = {
 | 
			
		||||
        "vsscanf",
 | 
			
		||||
    ],
 | 
			
		||||
    "cpp_anonymous": ["_GLOBAL__N_", "n$"],
 | 
			
		||||
    "cpp_runtime": ["__cxx", "_ZN", "_ZL", "_ZSt", "__gxx_personality", "_Z16"],
 | 
			
		||||
    "exception_handling": ["__cxa_", "_Unwind_", "__gcc_personality", "uw_frame_state"],
 | 
			
		||||
    # Plain C patterns only - C++ symbols will be categorized via DEMANGLED_PATTERNS
 | 
			
		||||
    "nvs": ["nvs_"],  # Plain C NVS functions
 | 
			
		||||
    "ota": ["ota_", "OTA", "esp_ota", "app_desc"],
 | 
			
		||||
    # cpp_runtime: Removed _ZN, _ZL to let DEMANGLED_PATTERNS categorize C++ symbols properly
 | 
			
		||||
    # Only keep patterns that are truly runtime-specific and not categorizable by namespace
 | 
			
		||||
    "cpp_runtime": ["__cxx", "_ZSt", "__gxx_personality", "_Z16"],
 | 
			
		||||
    "exception_handling": [
 | 
			
		||||
        "__cxa_",
 | 
			
		||||
        "_Unwind_",
 | 
			
		||||
        "__gcc_personality",
 | 
			
		||||
        "uw_frame_state",
 | 
			
		||||
        "search_object",  # Search for exception handling object
 | 
			
		||||
        "get_cie_encoding",  # Get CIE encoding
 | 
			
		||||
        "add_fdes",  # Add frame description entries
 | 
			
		||||
        "fde_unencoded_compare",  # Compare FDEs
 | 
			
		||||
        "fde_mixed_encoding_compare",  # Compare mixed encoding FDEs
 | 
			
		||||
        "frame_downheap",  # Frame heap operations
 | 
			
		||||
        "frame_heapsort",  # Frame heap sorting
 | 
			
		||||
    ],
 | 
			
		||||
    "static_init": ["_GLOBAL__sub_I_"],
 | 
			
		||||
    "mdns_lib": ["mdns"],
 | 
			
		||||
    "phy_radio": [
 | 
			
		||||
        "phy_",
 | 
			
		||||
        "rf_",
 | 
			
		||||
@@ -394,10 +503,47 @@ SYMBOL_PATTERNS = {
 | 
			
		||||
        "txcal_debuge_mode",
 | 
			
		||||
        "ant_wifitx_cfg",
 | 
			
		||||
        "reg_init_begin",
 | 
			
		||||
        "tx_cap_init",  # TX capacitance init
 | 
			
		||||
        "ram_set_txcap",  # RAM TX capacitance setting
 | 
			
		||||
        "tx_atten_",  # TX attenuation
 | 
			
		||||
        "txiq_",  # TX I/Q calibration
 | 
			
		||||
        "ram_cal_",  # RAM calibration
 | 
			
		||||
        "ram_rxiq_",  # RAM RX I/Q
 | 
			
		||||
        "readvdd33",  # Read VDD33
 | 
			
		||||
        "test_tout",  # Test timeout
 | 
			
		||||
        "tsen_meas",  # Temperature sensor measurement
 | 
			
		||||
        "bbpll_cal",  # Baseband PLL calibration
 | 
			
		||||
        "set_cal_",  # Set calibration
 | 
			
		||||
        "set_rfanagain_",  # Set RF analog gain
 | 
			
		||||
        "set_txdc_",  # Set TX DC
 | 
			
		||||
        "get_vdd33_",  # Get VDD33
 | 
			
		||||
        "gen_rx_gain_table",  # Generate RX gain table
 | 
			
		||||
        "ram_ana_inf_gating_en",  # RAM analog interface gating enable
 | 
			
		||||
        "tx_cont_en",  # TX continuous enable
 | 
			
		||||
        "tx_delay_cfg",  # TX delay configuration
 | 
			
		||||
        "tx_gain_table_set",  # TX gain table set
 | 
			
		||||
        "check_and_reset_hw_deadlock",  # Hardware deadlock check
 | 
			
		||||
        "s_config",  # System/hardware config
 | 
			
		||||
        "chan14_mic_cfg",  # Channel 14 MIC config
 | 
			
		||||
    ],
 | 
			
		||||
    "wifi_phy_pp": [
 | 
			
		||||
        "pp_",
 | 
			
		||||
        "ppT",
 | 
			
		||||
        "ppR",
 | 
			
		||||
        "ppP",
 | 
			
		||||
        "ppInstall",
 | 
			
		||||
        "ppCalTxAMPDULength",
 | 
			
		||||
        "ppCheckTx",  # Packet processor TX check
 | 
			
		||||
        "ppCal",  # Packet processor calibration
 | 
			
		||||
        "HdlAllBuffedEb",  # Handle buffered EB
 | 
			
		||||
    ],
 | 
			
		||||
    "wifi_phy_pp": ["pp_", "ppT", "ppR", "ppP", "ppInstall", "ppCalTxAMPDULength"],
 | 
			
		||||
    "wifi_lmac": ["lmac"],
 | 
			
		||||
    "wifi_device": ["wdev", "wDev_"],
 | 
			
		||||
    "wifi_device": [
 | 
			
		||||
        "wdev",
 | 
			
		||||
        "wDev_",
 | 
			
		||||
        "ic_set_sta",  # Set station mode
 | 
			
		||||
        "ic_set_vif",  # Set virtual interface
 | 
			
		||||
    ],
 | 
			
		||||
    "power_mgmt": [
 | 
			
		||||
        "pm_",
 | 
			
		||||
        "sleep",
 | 
			
		||||
@@ -406,15 +552,7 @@ SYMBOL_PATTERNS = {
 | 
			
		||||
        "deep_sleep",
 | 
			
		||||
        "power_down",
 | 
			
		||||
        "g_pm",
 | 
			
		||||
    ],
 | 
			
		||||
    "memory_mgmt": [
 | 
			
		||||
        "mem_",
 | 
			
		||||
        "memory_",
 | 
			
		||||
        "tlsf_",
 | 
			
		||||
        "memp_",
 | 
			
		||||
        "pbuf_",
 | 
			
		||||
        "pbuf_alloc",
 | 
			
		||||
        "pbuf_copy_partial_pbuf",
 | 
			
		||||
        "pmc",  # Power Management Controller
 | 
			
		||||
    ],
 | 
			
		||||
    "hal_layer": ["hal_"],
 | 
			
		||||
    "clock_mgmt": [
 | 
			
		||||
@@ -439,7 +577,6 @@ SYMBOL_PATTERNS = {
 | 
			
		||||
    "error_handling": ["panic", "abort", "assert", "error_", "fault"],
 | 
			
		||||
    "authentication": ["auth"],
 | 
			
		||||
    "ppp_protocol": ["ppp", "ipcp_", "lcp_", "chap_", "LcpEchoCheck"],
 | 
			
		||||
    "dhcp": ["dhcp", "handle_dhcp"],
 | 
			
		||||
    "ethernet_phy": [
 | 
			
		||||
        "emac_",
 | 
			
		||||
        "eth_phy_",
 | 
			
		||||
@@ -618,7 +755,15 @@ SYMBOL_PATTERNS = {
 | 
			
		||||
        "ampdu_dispatch_upto",
 | 
			
		||||
    ],
 | 
			
		||||
    "ieee802_11": ["ieee802_11_", "ieee802_11_parse_elems"],
 | 
			
		||||
    "rate_control": ["rssi_margin", "rcGetSched", "get_rate_fcc_index"],
 | 
			
		||||
    "rate_control": [
 | 
			
		||||
        "rssi_margin",
 | 
			
		||||
        "rcGetSched",
 | 
			
		||||
        "get_rate_fcc_index",
 | 
			
		||||
        "rcGetRate",  # Get rate
 | 
			
		||||
        "rc_get_",  # Rate control getters
 | 
			
		||||
        "rc_set_",  # Rate control setters
 | 
			
		||||
        "rc_enable_",  # Rate control enable functions
 | 
			
		||||
    ],
 | 
			
		||||
    "nan": ["nan_dp_", "nan_dp_post_tx", "nan_dp_delete_peer"],
 | 
			
		||||
    "channel_mgmt": ["chm_init", "chm_set_current_channel"],
 | 
			
		||||
    "trace": ["trc_init", "trc_onAmpduOp"],
 | 
			
		||||
@@ -799,31 +944,18 @@ SYMBOL_PATTERNS = {
 | 
			
		||||
        "supports_interlaced_inquiry_scan",
 | 
			
		||||
        "supports_reading_remote_extended_features",
 | 
			
		||||
    ],
 | 
			
		||||
    "bluetooth_ll": [
 | 
			
		||||
        "lld_pdu_",
 | 
			
		||||
        "ld_acl_",
 | 
			
		||||
        "lld_stop_ind_handler",
 | 
			
		||||
        "lld_evt_winsize_change",
 | 
			
		||||
        "config_lld_evt_funcs_reset",
 | 
			
		||||
        "config_lld_funcs_reset",
 | 
			
		||||
        "config_llm_funcs_reset",
 | 
			
		||||
        "llm_set_long_adv_data",
 | 
			
		||||
        "lld_retry_tx_prog",
 | 
			
		||||
        "llc_link_sup_to_ind_handler",
 | 
			
		||||
        "config_llc_funcs_reset",
 | 
			
		||||
        "lld_evt_rxwin_compute",
 | 
			
		||||
        "config_btdm_funcs_reset",
 | 
			
		||||
        "config_ea_funcs_reset",
 | 
			
		||||
        "llc_defalut_state_tab_reset",
 | 
			
		||||
        "config_rwip_funcs_reset",
 | 
			
		||||
        "ke_lmp_rx_flooding_detect",
 | 
			
		||||
    ],
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# Demangled patterns: patterns found in demangled C++ names
 | 
			
		||||
DEMANGLED_PATTERNS = {
 | 
			
		||||
    "gpio_driver": ["GPIO"],
 | 
			
		||||
    "uart_driver": ["UART"],
 | 
			
		||||
    # mdns_lib must come before network_stack to avoid "udp" matching "_udpReadBuffer" in MDNSResponder
 | 
			
		||||
    "mdns_lib": [
 | 
			
		||||
        "MDNSResponder",
 | 
			
		||||
        "MDNSImplementation",
 | 
			
		||||
        "MDNS",
 | 
			
		||||
    ],
 | 
			
		||||
    "network_stack": [
 | 
			
		||||
        "lwip",
 | 
			
		||||
        "tcp",
 | 
			
		||||
@@ -836,6 +968,24 @@ DEMANGLED_PATTERNS = {
 | 
			
		||||
        "ethernet",
 | 
			
		||||
        "ppp",
 | 
			
		||||
        "slip",
 | 
			
		||||
        "UdpContext",  # UDP context class
 | 
			
		||||
        "DhcpServer",  # DHCP server class
 | 
			
		||||
    ],
 | 
			
		||||
    "arduino_core": [
 | 
			
		||||
        "String::",  # Arduino String class
 | 
			
		||||
        "Print::",  # Arduino Print class
 | 
			
		||||
        "HardwareSerial::",  # Serial class
 | 
			
		||||
        "IPAddress::",  # IP address class
 | 
			
		||||
        "EspClass::",  # ESP class
 | 
			
		||||
        "experimental::_SPI",  # Experimental SPI
 | 
			
		||||
    ],
 | 
			
		||||
    "ota": [
 | 
			
		||||
        "UpdaterClass",
 | 
			
		||||
        "Updater::",
 | 
			
		||||
    ],
 | 
			
		||||
    "wifi": [
 | 
			
		||||
        "ESP8266WiFi",
 | 
			
		||||
        "WiFi::",
 | 
			
		||||
    ],
 | 
			
		||||
    "wifi_stack": ["NetworkInterface"],
 | 
			
		||||
    "nimble_bt": [
 | 
			
		||||
@@ -854,7 +1004,6 @@ DEMANGLED_PATTERNS = {
 | 
			
		||||
    "rtti": ["__type_info", "__class_type_info"],
 | 
			
		||||
    "web_server_lib": ["AsyncWebServer", "AsyncWebHandler", "WebServer"],
 | 
			
		||||
    "async_tcp": ["AsyncClient", "AsyncServer"],
 | 
			
		||||
    "mdns_lib": ["mdns"],
 | 
			
		||||
    "json_lib": [
 | 
			
		||||
        "ArduinoJson",
 | 
			
		||||
        "JsonDocument",
 | 
			
		||||
 
 | 
			
		||||
@@ -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;
 | 
			
		||||
 
 | 
			
		||||
@@ -172,12 +172,6 @@ def alarm_control_panel_schema(
 | 
			
		||||
    return _ALARM_CONTROL_PANEL_SCHEMA.extend(schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Remove before 2025.11.0
 | 
			
		||||
ALARM_CONTROL_PANEL_SCHEMA = alarm_control_panel_schema(AlarmControlPanel)
 | 
			
		||||
ALARM_CONTROL_PANEL_SCHEMA.add_extra(
 | 
			
		||||
    cv.deprecated_schema_constant("alarm_control_panel")
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.use_id(AlarmControlPanel),
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ class Anova : public climate::Climate, public esphome::ble_client::BLEClientNode
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  climate::ClimateTraits traits() override {
 | 
			
		||||
    auto traits = climate::ClimateTraits();
 | 
			
		||||
    traits.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;
 | 
			
		||||
@@ -1143,7 +1143,7 @@ message ListEntitiesSelectResponse {
 | 
			
		||||
  reserved 4; // Deprecated: was string unique_id
 | 
			
		||||
 | 
			
		||||
  string icon = 5 [(field_ifdef) = "USE_ENTITY_ICON"];
 | 
			
		||||
  repeated string options = 6 [(container_pointer) = "std::vector"];
 | 
			
		||||
  repeated string options = 6 [(container_pointer_no_template) = "FixedVector<const char *>"];
 | 
			
		||||
  bool disabled_by_default = 7;
 | 
			
		||||
  EntityCategory entity_category = 8;
 | 
			
		||||
  uint32 device_id = 9 [(field_ifdef) = "USE_DEVICES"];
 | 
			
		||||
 
 | 
			
		||||
@@ -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,
 | 
			
		||||
@@ -661,25 +661,26 @@ uint16_t APIConnection::try_send_climate_info(EntityBase *entity, APIConnection
 | 
			
		||||
  ListEntitiesClimateResponse msg;
 | 
			
		||||
  auto traits = climate->get_traits();
 | 
			
		||||
  // Flags set for backward compatibility, deprecated in 2025.11.0
 | 
			
		||||
  msg.supports_current_temperature = traits.get_supports_current_temperature();
 | 
			
		||||
  msg.supports_current_humidity = traits.get_supports_current_humidity();
 | 
			
		||||
  msg.supports_two_point_target_temperature = traits.get_supports_two_point_target_temperature();
 | 
			
		||||
  msg.supports_target_humidity = traits.get_supports_target_humidity();
 | 
			
		||||
  msg.supports_action = traits.get_supports_action();
 | 
			
		||||
  msg.supports_current_temperature = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_TEMPERATURE);
 | 
			
		||||
  msg.supports_current_humidity = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_CURRENT_HUMIDITY);
 | 
			
		||||
  msg.supports_two_point_target_temperature = traits.has_feature_flags(
 | 
			
		||||
      climate::CLIMATE_SUPPORTS_TWO_POINT_TARGET_TEMPERATURE | climate::CLIMATE_REQUIRES_TWO_POINT_TARGET_TEMPERATURE);
 | 
			
		||||
  msg.supports_target_humidity = traits.has_feature_flags(climate::CLIMATE_SUPPORTS_TARGET_HUMIDITY);
 | 
			
		||||
  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);
 | 
			
		||||
}
 | 
			
		||||
@@ -1081,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
 | 
			
		||||
  }
 | 
			
		||||
@@ -1576,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;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -1475,8 +1475,8 @@ void ListEntitiesSelectResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
#ifdef USE_ENTITY_ICON
 | 
			
		||||
  buffer.encode_string(5, this->icon_ref_);
 | 
			
		||||
#endif
 | 
			
		||||
  for (const auto &it : *this->options) {
 | 
			
		||||
    buffer.encode_string(6, it, true);
 | 
			
		||||
  for (const char *it : *this->options) {
 | 
			
		||||
    buffer.encode_string(6, it, strlen(it), true);
 | 
			
		||||
  }
 | 
			
		||||
  buffer.encode_bool(7, this->disabled_by_default);
 | 
			
		||||
  buffer.encode_uint32(8, static_cast<uint32_t>(this->entity_category));
 | 
			
		||||
@@ -1492,8 +1492,8 @@ void ListEntitiesSelectResponse::calculate_size(ProtoSize &size) const {
 | 
			
		||||
  size.add_length(1, this->icon_ref_.size());
 | 
			
		||||
#endif
 | 
			
		||||
  if (!this->options->empty()) {
 | 
			
		||||
    for (const auto &it : *this->options) {
 | 
			
		||||
      size.add_length_force(1, it.size());
 | 
			
		||||
    for (const char *it : *this->options) {
 | 
			
		||||
      size.add_length_force(1, strlen(it));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  size.add_bool(1, this->disabled_by_default);
 | 
			
		||||
 
 | 
			
		||||
@@ -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};
 | 
			
		||||
@@ -1534,7 +1534,7 @@ class ListEntitiesSelectResponse final : public InfoResponseProtoMessage {
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "list_entities_select_response"; }
 | 
			
		||||
#endif
 | 
			
		||||
  const std::vector<std::string> *options{};
 | 
			
		||||
  const FixedVector<const char *> *options{};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
  void calculate_size(ProtoSize &size) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
 
 | 
			
		||||
@@ -88,6 +88,12 @@ static void dump_field(std::string &out, const char *field_name, StringRef value
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static void dump_field(std::string &out, const char *field_name, const char *value, int indent = 2) {
 | 
			
		||||
  append_field_prefix(out, field_name, indent);
 | 
			
		||||
  out.append("'").append(value).append("'");
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
template<typename T> static void dump_field(std::string &out, const char *field_name, T value, int indent = 2) {
 | 
			
		||||
  append_field_prefix(out, field_name, indent);
 | 
			
		||||
  out.append(proto_enum_to_string<T>(value));
 | 
			
		||||
 
 | 
			
		||||
@@ -468,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) {
 | 
			
		||||
 
 | 
			
		||||
@@ -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(
 | 
			
		||||
@@ -536,11 +548,6 @@ def binary_sensor_schema(
 | 
			
		||||
    return _BINARY_SENSOR_SCHEMA.extend(schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Remove before 2025.11.0
 | 
			
		||||
BINARY_SENSOR_SCHEMA = binary_sensor_schema()
 | 
			
		||||
BINARY_SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("binary_sensor"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def setup_binary_sensor_core_(var, config):
 | 
			
		||||
    await setup_entity(var, config, "binary_sensor")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,11 @@
 | 
			
		||||
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/components/binary_sensor/binary_sensor.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -92,8 +92,8 @@ class DoubleClickTrigger : public Trigger<> {
 | 
			
		||||
 | 
			
		||||
class MultiClickTrigger : public Trigger<>, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit MultiClickTrigger(BinarySensor *parent, std::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_;
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										29
									
								
								esphome/components/ble_nus/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								esphome/components/ble_nus/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,29 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components.zephyr import zephyr_add_prj_conf
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID, CONF_LOGS, CONF_TYPE
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["zephyr_ble_server"]
 | 
			
		||||
CODEOWNERS = ["@tomaszduda23"]
 | 
			
		||||
 | 
			
		||||
ble_nus_ns = cg.esphome_ns.namespace("ble_nus")
 | 
			
		||||
BLENUS = ble_nus_ns.class_("BLENUS", cg.Component)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(BLENUS),
 | 
			
		||||
            cv.Optional(CONF_TYPE, default=CONF_LOGS): cv.one_of(
 | 
			
		||||
                *[CONF_LOGS], lower=True
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    cv.only_with_framework("zephyr"),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    zephyr_add_prj_conf("BT_NUS", True)
 | 
			
		||||
    cg.add(var.set_expose_log(config[CONF_TYPE] == CONF_LOGS))
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
							
								
								
									
										157
									
								
								esphome/components/ble_nus/ble_nus.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										157
									
								
								esphome/components/ble_nus/ble_nus.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,157 @@
 | 
			
		||||
#ifdef USE_ZEPHYR
 | 
			
		||||
#include "ble_nus.h"
 | 
			
		||||
#include <zephyr/kernel.h>
 | 
			
		||||
#include <bluetooth/services/nus.h>
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#ifdef USE_LOGGER
 | 
			
		||||
#include "esphome/components/logger/logger.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#endif
 | 
			
		||||
#include <zephyr/sys/ring_buffer.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome::ble_nus {
 | 
			
		||||
 | 
			
		||||
constexpr size_t BLE_TX_BUF_SIZE = 2048;
 | 
			
		||||
 | 
			
		||||
// NOLINTBEGIN(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
BLENUS *global_ble_nus;
 | 
			
		||||
RING_BUF_DECLARE(global_ble_tx_ring_buf, BLE_TX_BUF_SIZE);
 | 
			
		||||
// NOLINTEND(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "ble_nus";
 | 
			
		||||
 | 
			
		||||
size_t BLENUS::write_array(const uint8_t *data, size_t len) {
 | 
			
		||||
  if (atomic_get(&this->tx_status_) == TX_DISABLED) {
 | 
			
		||||
    return 0;
 | 
			
		||||
  }
 | 
			
		||||
  return ring_buf_put(&global_ble_tx_ring_buf, data, len);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLENUS::connected(bt_conn *conn, uint8_t err) {
 | 
			
		||||
  if (err == 0) {
 | 
			
		||||
    global_ble_nus->conn_.store(bt_conn_ref(conn));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLENUS::disconnected(bt_conn *conn, uint8_t reason) {
 | 
			
		||||
  if (global_ble_nus->conn_) {
 | 
			
		||||
    bt_conn_unref(global_ble_nus->conn_.load());
 | 
			
		||||
    // Connection array is global static.
 | 
			
		||||
    // Reference can be kept even if disconnected.
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLENUS::tx_callback(bt_conn *conn) {
 | 
			
		||||
  atomic_cas(&global_ble_nus->tx_status_, TX_BUSY, TX_ENABLED);
 | 
			
		||||
  ESP_LOGVV(TAG, "Sent operation completed");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLENUS::send_enabled_callback(bt_nus_send_status status) {
 | 
			
		||||
  switch (status) {
 | 
			
		||||
    case BT_NUS_SEND_STATUS_ENABLED:
 | 
			
		||||
      atomic_set(&global_ble_nus->tx_status_, TX_ENABLED);
 | 
			
		||||
#ifdef USE_LOGGER
 | 
			
		||||
      if (global_ble_nus->expose_log_) {
 | 
			
		||||
        App.schedule_dump_config();
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      ESP_LOGD(TAG, "NUS notification has been enabled");
 | 
			
		||||
      break;
 | 
			
		||||
    case BT_NUS_SEND_STATUS_DISABLED:
 | 
			
		||||
      atomic_set(&global_ble_nus->tx_status_, TX_DISABLED);
 | 
			
		||||
      ESP_LOGD(TAG, "NUS notification has been disabled");
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLENUS::rx_callback(bt_conn *conn, const uint8_t *const data, uint16_t len) {
 | 
			
		||||
  ESP_LOGD(TAG, "Received %d bytes.", len);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLENUS::setup() {
 | 
			
		||||
  bt_nus_cb callbacks = {
 | 
			
		||||
      .received = rx_callback,
 | 
			
		||||
      .sent = tx_callback,
 | 
			
		||||
      .send_enabled = send_enabled_callback,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  bt_nus_init(&callbacks);
 | 
			
		||||
 | 
			
		||||
  static bt_conn_cb conn_callbacks = {
 | 
			
		||||
      .connected = BLENUS::connected,
 | 
			
		||||
      .disconnected = BLENUS::disconnected,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  bt_conn_cb_register(&conn_callbacks);
 | 
			
		||||
 | 
			
		||||
  global_ble_nus = this;
 | 
			
		||||
#ifdef USE_LOGGER
 | 
			
		||||
  if (logger::global_logger != nullptr && this->expose_log_) {
 | 
			
		||||
    logger::global_logger->add_on_log_callback(
 | 
			
		||||
        [this](int level, const char *tag, const char *message, size_t message_len) {
 | 
			
		||||
          this->write_array(reinterpret_cast<const uint8_t *>(message), message_len);
 | 
			
		||||
          const char c = '\n';
 | 
			
		||||
          this->write_array(reinterpret_cast<const uint8_t *>(&c), 1);
 | 
			
		||||
        });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLENUS::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "ble nus:");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  log: %s", YESNO(this->expose_log_));
 | 
			
		||||
  uint32_t mtu = 0;
 | 
			
		||||
  bt_conn *conn = this->conn_.load();
 | 
			
		||||
  if (conn) {
 | 
			
		||||
    mtu = bt_nus_get_mtu(conn);
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  MTU: %u", mtu);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLENUS::loop() {
 | 
			
		||||
  if (ring_buf_is_empty(&global_ble_tx_ring_buf)) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!atomic_cas(&this->tx_status_, TX_ENABLED, TX_BUSY)) {
 | 
			
		||||
    if (atomic_get(&this->tx_status_) == TX_DISABLED) {
 | 
			
		||||
      ring_buf_reset(&global_ble_tx_ring_buf);
 | 
			
		||||
    }
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bt_conn *conn = this->conn_.load();
 | 
			
		||||
  if (conn) {
 | 
			
		||||
    conn = bt_conn_ref(conn);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (nullptr == conn) {
 | 
			
		||||
    atomic_cas(&this->tx_status_, TX_BUSY, TX_ENABLED);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint32_t req_len = bt_nus_get_mtu(conn);
 | 
			
		||||
 | 
			
		||||
  uint8_t *buf;
 | 
			
		||||
  uint32_t size = ring_buf_get_claim(&global_ble_tx_ring_buf, &buf, req_len);
 | 
			
		||||
 | 
			
		||||
  int err, err2;
 | 
			
		||||
 | 
			
		||||
  err = bt_nus_send(conn, buf, size);
 | 
			
		||||
  err2 = ring_buf_get_finish(&global_ble_tx_ring_buf, size);
 | 
			
		||||
  if (err2) {
 | 
			
		||||
    // It should no happen.
 | 
			
		||||
    ESP_LOGE(TAG, "Size %u exceeds valid bytes in the ring buffer (%d error)", size, err2);
 | 
			
		||||
  }
 | 
			
		||||
  if (err == 0) {
 | 
			
		||||
    ESP_LOGVV(TAG, "Sent %d bytes", size);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGE(TAG, "Failed to send %d bytes (%d error)", size, err);
 | 
			
		||||
    atomic_cas(&this->tx_status_, TX_BUSY, TX_ENABLED);
 | 
			
		||||
  }
 | 
			
		||||
  bt_conn_unref(conn);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::ble_nus
 | 
			
		||||
#endif
 | 
			
		||||
							
								
								
									
										37
									
								
								esphome/components/ble_nus/ble_nus.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								esphome/components/ble_nus/ble_nus.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,37 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#ifdef USE_ZEPHYR
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include <shell/shell_bt_nus.h>
 | 
			
		||||
#include <atomic>
 | 
			
		||||
 | 
			
		||||
namespace esphome::ble_nus {
 | 
			
		||||
 | 
			
		||||
class BLENUS : public Component {
 | 
			
		||||
  enum TxStatus {
 | 
			
		||||
    TX_DISABLED,
 | 
			
		||||
    TX_ENABLED,
 | 
			
		||||
    TX_BUSY,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  size_t write_array(const uint8_t *data, size_t len);
 | 
			
		||||
  void set_expose_log(bool expose_log) { this->expose_log_ = expose_log; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  static void send_enabled_callback(bt_nus_send_status status);
 | 
			
		||||
  static void tx_callback(bt_conn *conn);
 | 
			
		||||
  static void rx_callback(bt_conn *conn, const uint8_t *data, uint16_t len);
 | 
			
		||||
  static void connected(bt_conn *conn, uint8_t err);
 | 
			
		||||
  static void disconnected(bt_conn *conn, uint8_t reason);
 | 
			
		||||
 | 
			
		||||
  std::atomic<bt_conn *> conn_ = nullptr;
 | 
			
		||||
  bool expose_log_ = false;
 | 
			
		||||
  atomic_t tx_status_ = ATOMIC_INIT(TX_DISABLED);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::ble_nus
 | 
			
		||||
#endif
 | 
			
		||||
@@ -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;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -41,7 +41,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(BME680BSECComponent),
 | 
			
		||||
            cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature,
 | 
			
		||||
            cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature_delta,
 | 
			
		||||
            cv.Optional(CONF_IAQ_MODE, default="STATIC"): cv.enum(
 | 
			
		||||
                IAQ_MODE_OPTIONS, upper=True
 | 
			
		||||
            ),
 | 
			
		||||
 
 | 
			
		||||
@@ -139,7 +139,7 @@ CONFIG_SCHEMA_BASE = (
 | 
			
		||||
            cv.Optional(CONF_SUPPLY_VOLTAGE, default="3.3V"): cv.enum(
 | 
			
		||||
                VOLTAGE_OPTIONS, upper=True
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature,
 | 
			
		||||
            cv.Optional(CONF_TEMPERATURE_OFFSET, default=0): cv.temperature_delta,
 | 
			
		||||
            cv.Optional(
 | 
			
		||||
                CONF_STATE_SAVE_INTERVAL, default="6hours"
 | 
			
		||||
            ): cv.positive_time_period_minutes,
 | 
			
		||||
 
 | 
			
		||||
@@ -84,11 +84,6 @@ def button_schema(
 | 
			
		||||
    return _BUTTON_SCHEMA.extend(schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Remove before 2025.11.0
 | 
			
		||||
BUTTON_SCHEMA = button_schema(Button)
 | 
			
		||||
BUTTON_SCHEMA.add_extra(cv.deprecated_schema_constant("button"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def setup_button_core_(var, config):
 | 
			
		||||
    await setup_entity(var, config, "button")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -270,11 +270,6 @@ def climate_schema(
 | 
			
		||||
    return _CLIMATE_SCHEMA.extend(schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Remove before 2025.11.0
 | 
			
		||||
CLIMATE_SCHEMA = climate_schema(Climate)
 | 
			
		||||
CLIMATE_SCHEMA.add_extra(cv.deprecated_schema_constant("climate"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def setup_climate_core_(var, config):
 | 
			
		||||
    await setup_entity(var, config, "climate")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -6,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
 | 
			
		||||
 
 | 
			
		||||
@@ -1,10 +1,9 @@
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
from esphome import core
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import climate, remote_base, sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID, CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
 | 
			
		||||
from esphome.const import CONF_SENSOR, CONF_SUPPORTS_COOL, CONF_SUPPORTS_HEAT
 | 
			
		||||
from esphome.cpp_generator import MockObjClass
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
@@ -52,26 +51,6 @@ def climate_ir_with_receiver_schema(
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Remove before 2025.11.0
 | 
			
		||||
def deprecated_schema_constant(config):
 | 
			
		||||
    type: str = "unknown"
 | 
			
		||||
    if (id := config.get(CONF_ID)) is not None and isinstance(id, core.ID):
 | 
			
		||||
        type = str(id.type).split("::", maxsplit=1)[0]
 | 
			
		||||
    _LOGGER.warning(
 | 
			
		||||
        "Using `climate_ir.CLIMATE_IR_WITH_RECEIVER_SCHEMA` is deprecated and will be removed in ESPHome 2025.11.0. "
 | 
			
		||||
        "Please use `climate_ir.climate_ir_with_receiver_schema(...)` instead. "
 | 
			
		||||
        "If you are seeing this, report an issue to the external_component author and ask them to update it. "
 | 
			
		||||
        "https://developers.esphome.io/blog/2025/05/14/_schema-deprecations/. "
 | 
			
		||||
        "Component using this schema: %s",
 | 
			
		||||
        type,
 | 
			
		||||
    )
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CLIMATE_IR_WITH_RECEIVER_SCHEMA = climate_ir_with_receiver_schema(ClimateIR)
 | 
			
		||||
CLIMATE_IR_WITH_RECEIVER_SCHEMA.add_extra(deprecated_schema_constant)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def register_climate_ir(var, config):
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await remote_base.register_transmittable(var, config)
 | 
			
		||||
 
 | 
			
		||||
@@ -8,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};
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -151,11 +151,6 @@ def cover_schema(
 | 
			
		||||
    return _COVER_SCHEMA.extend(schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Remove before 2025.11.0
 | 
			
		||||
COVER_SCHEMA = cover_schema(Cover)
 | 
			
		||||
COVER_SCHEMA.add_extra(cv.deprecated_schema_constant("cover"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def setup_cover_core_(var, config):
 | 
			
		||||
    await setup_entity(var, config, "cover")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,6 +1,6 @@
 | 
			
		||||
#include "cover.h"
 | 
			
		||||
#include "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,
 | 
			
		||||
 
 | 
			
		||||
@@ -176,7 +176,117 @@ class Display;
 | 
			
		||||
class DisplayPage;
 | 
			
		||||
class DisplayOnPageChangeTrigger;
 | 
			
		||||
 | 
			
		||||
using display_writer_t = std::function<void(Display &)>;
 | 
			
		||||
/** Optimized display writer that uses function pointers for stateless lambdas.
 | 
			
		||||
 *
 | 
			
		||||
 * Similar to TemplatableValue but specialized for display writer callbacks.
 | 
			
		||||
 * Saves ~8 bytes per stateless lambda on 32-bit platforms (16 bytes std::function → ~8 bytes discriminator+pointer).
 | 
			
		||||
 *
 | 
			
		||||
 * Supports both:
 | 
			
		||||
 * - Stateless lambdas (from YAML) → function pointer (4 bytes)
 | 
			
		||||
 * - Stateful lambdas/std::function (from C++ code) → std::function* (heap allocated)
 | 
			
		||||
 *
 | 
			
		||||
 * @tparam T The display type (e.g., Display, Nextion, GPIOLCDDisplay)
 | 
			
		||||
 */
 | 
			
		||||
template<typename T> class DisplayWriter {
 | 
			
		||||
 public:
 | 
			
		||||
  DisplayWriter() : type_(NONE) {}
 | 
			
		||||
 | 
			
		||||
  // For stateless lambdas (convertible to function pointer): use function pointer (4 bytes)
 | 
			
		||||
  template<typename F>
 | 
			
		||||
  DisplayWriter(F f) requires std::invocable<F, T &> && std::convertible_to<F, void (*)(T &)>
 | 
			
		||||
      : type_(STATELESS_LAMBDA) {
 | 
			
		||||
    this->stateless_f_ = f;  // Implicit conversion to function pointer
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // For stateful lambdas and std::function (not convertible to function pointer): use std::function* (heap allocated)
 | 
			
		||||
  // This handles backwards compatibility with external components
 | 
			
		||||
  template<typename F>
 | 
			
		||||
  DisplayWriter(F f) requires std::invocable<F, T &> &&(!std::convertible_to<F, void (*)(T &)>) : type_(LAMBDA) {
 | 
			
		||||
    this->f_ = new std::function<void(T &)>(std::move(f));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Copy constructor
 | 
			
		||||
  DisplayWriter(const DisplayWriter &other) : type_(other.type_) {
 | 
			
		||||
    if (type_ == LAMBDA) {
 | 
			
		||||
      this->f_ = new std::function<void(T &)>(*other.f_);
 | 
			
		||||
    } else if (type_ == STATELESS_LAMBDA) {
 | 
			
		||||
      this->stateless_f_ = other.stateless_f_;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Move constructor
 | 
			
		||||
  DisplayWriter(DisplayWriter &&other) noexcept : type_(other.type_) {
 | 
			
		||||
    if (type_ == LAMBDA) {
 | 
			
		||||
      this->f_ = other.f_;
 | 
			
		||||
      other.f_ = nullptr;
 | 
			
		||||
    } else if (type_ == STATELESS_LAMBDA) {
 | 
			
		||||
      this->stateless_f_ = other.stateless_f_;
 | 
			
		||||
    }
 | 
			
		||||
    other.type_ = NONE;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Assignment operators
 | 
			
		||||
  DisplayWriter &operator=(const DisplayWriter &other) {
 | 
			
		||||
    if (this != &other) {
 | 
			
		||||
      this->~DisplayWriter();
 | 
			
		||||
      new (this) DisplayWriter(other);
 | 
			
		||||
    }
 | 
			
		||||
    return *this;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  DisplayWriter &operator=(DisplayWriter &&other) noexcept {
 | 
			
		||||
    if (this != &other) {
 | 
			
		||||
      this->~DisplayWriter();
 | 
			
		||||
      new (this) DisplayWriter(std::move(other));
 | 
			
		||||
    }
 | 
			
		||||
    return *this;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ~DisplayWriter() {
 | 
			
		||||
    if (type_ == LAMBDA) {
 | 
			
		||||
      delete this->f_;
 | 
			
		||||
    }
 | 
			
		||||
    // STATELESS_LAMBDA/NONE: no cleanup needed (function pointer or empty)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool has_value() const { return this->type_ != NONE; }
 | 
			
		||||
 | 
			
		||||
  void call(T &display) const {
 | 
			
		||||
    switch (this->type_) {
 | 
			
		||||
      case STATELESS_LAMBDA:
 | 
			
		||||
        this->stateless_f_(display);  // Direct function pointer call
 | 
			
		||||
        break;
 | 
			
		||||
      case LAMBDA:
 | 
			
		||||
        (*this->f_)(display);  // std::function call
 | 
			
		||||
        break;
 | 
			
		||||
      case NONE:
 | 
			
		||||
      default:
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Operator() for convenience
 | 
			
		||||
  void operator()(T &display) const { this->call(display); }
 | 
			
		||||
 | 
			
		||||
  // Operator* for backwards compatibility with (*writer_)(*this) pattern
 | 
			
		||||
  DisplayWriter &operator*() { return *this; }
 | 
			
		||||
  const DisplayWriter &operator*() const { return *this; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  enum : uint8_t {
 | 
			
		||||
    NONE,
 | 
			
		||||
    LAMBDA,
 | 
			
		||||
    STATELESS_LAMBDA,
 | 
			
		||||
  } type_;
 | 
			
		||||
 | 
			
		||||
  union {
 | 
			
		||||
    std::function<void(T &)> *f_;
 | 
			
		||||
    void (*stateless_f_)(T &);
 | 
			
		||||
  };
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Type alias for Display writer - uses optimized DisplayWriter instead of std::function
 | 
			
		||||
using display_writer_t = DisplayWriter<Display>;
 | 
			
		||||
 | 
			
		||||
#define LOG_DISPLAY(prefix, type, obj) \
 | 
			
		||||
  if ((obj) != nullptr) { \
 | 
			
		||||
@@ -678,7 +788,7 @@ class Display : public PollingComponent {
 | 
			
		||||
  void sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3);
 | 
			
		||||
 | 
			
		||||
  DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES};
 | 
			
		||||
  optional<display_writer_t> writer_{};
 | 
			
		||||
  display_writer_t writer_{};
 | 
			
		||||
  DisplayPage *page_{nullptr};
 | 
			
		||||
  DisplayPage *previous_page_{nullptr};
 | 
			
		||||
  std::vector<DisplayOnPageChangeTrigger *> on_page_change_triggers_;
 | 
			
		||||
 
 | 
			
		||||
@@ -3,6 +3,8 @@
 | 
			
		||||
#include "e131_addressable_light_effect.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace e131 {
 | 
			
		||||
 | 
			
		||||
@@ -76,14 +78,14 @@ void E131Component::loop() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
 | 
			
		||||
  if (light_effects_.count(light_effect)) {
 | 
			
		||||
  if (std::find(light_effects_.begin(), light_effects_.end(), light_effect) != light_effects_.end()) {
 | 
			
		||||
    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);
 | 
			
		||||
  light_effects_.push_back(light_effect);
 | 
			
		||||
 | 
			
		||||
  for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
 | 
			
		||||
    join_(universe);
 | 
			
		||||
@@ -91,14 +93,17 @@ void E131Component::add_effect(E131AddressableLightEffect *light_effect) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void E131Component::remove_effect(E131AddressableLightEffect *light_effect) {
 | 
			
		||||
  if (!light_effects_.count(light_effect)) {
 | 
			
		||||
  auto it = std::find(light_effects_.begin(), light_effects_.end(), light_effect);
 | 
			
		||||
  if (it == light_effects_.end()) {
 | 
			
		||||
    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);
 | 
			
		||||
  // Swap with last element and pop for O(1) removal (order doesn't matter)
 | 
			
		||||
  *it = light_effects_.back();
 | 
			
		||||
  light_effects_.pop_back();
 | 
			
		||||
 | 
			
		||||
  for (auto universe = light_effect->get_first_universe(); universe <= light_effect->get_last_universe(); ++universe) {
 | 
			
		||||
    leave_(universe);
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
#include <map>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <set>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -47,9 +46,8 @@ class E131Component : public esphome::Component {
 | 
			
		||||
 | 
			
		||||
  E131ListenMethod listen_method_{E131_MULTICAST};
 | 
			
		||||
  std::unique_ptr<socket::Socket> socket_;
 | 
			
		||||
  std::set<E131AddressableLightEffect *> light_effects_;
 | 
			
		||||
  std::vector<E131AddressableLightEffect *> light_effects_;
 | 
			
		||||
  std::map<int, int> universe_consumers_;
 | 
			
		||||
  std::map<int, E131Packet> universe_packets_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace e131
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,7 @@ namespace e131 {
 | 
			
		||||
static const char *const TAG = "e131_addressable_light_effect";
 | 
			
		||||
static const int MAX_DATA_SIZE = (sizeof(E131Packet::values) - 1);
 | 
			
		||||
 | 
			
		||||
E131AddressableLightEffect::E131AddressableLightEffect(const 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
 | 
			
		||||
 
 | 
			
		||||
@@ -85,11 +85,6 @@ def event_schema(
 | 
			
		||||
    return _EVENT_SCHEMA.extend(schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Remove before 2025.11.0
 | 
			
		||||
EVENT_SCHEMA = event_schema()
 | 
			
		||||
EVENT_SCHEMA.add_extra(cv.deprecated_schema_constant("event"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def setup_event_core_(var, config, *, event_types: list[str]):
 | 
			
		||||
    await setup_entity(var, config, "event")
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -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 = {
 | 
			
		||||
@@ -190,10 +189,6 @@ def fan_schema(
 | 
			
		||||
    return _FAN_SCHEMA.extend(schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Remove before 2025.11.0
 | 
			
		||||
FAN_SCHEMA = fan_schema(Fan)
 | 
			
		||||
FAN_SCHEMA.add_extra(cv.deprecated_schema_constant("fan"))
 | 
			
		||||
 | 
			
		||||
_PRESET_MODES_SCHEMA = cv.All(
 | 
			
		||||
    cv.ensure_list(cv.string_strict),
 | 
			
		||||
    cv.Length(min=1),
 | 
			
		||||
 
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user