mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-31 06:28:43 +00:00 
			
		
		
		
	Compare commits
	
		
			240 Commits
		
	
	
		
			memory_api
			...
			memory_api
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
|   | 62569c9770 | ||
|   | 27859c8ccd | ||
|   | fae90194e7 | ||
|   | 5c99eabd1a | ||
|   | 1378e52838 | ||
|   | 868d01ae03 | ||
|   | c36b778158 | ||
|   | 1b5a942f61 | ||
|   | d7f55e9977 | ||
|   | f6e8fdcd91 | ||
|   | 1fd6f7bcd3 | ||
|   | 0a86254b84 | ||
|   | 6dd29f1917 | ||
|   | a073ec4e11 | ||
|   | d1bb5c4d79 | ||
|   | 60a303adb8 | ||
|   | 03ec52752b | ||
|   | 70ec33f418 | ||
|   | b4045b0963 | ||
|   | cd513b0672 | ||
|   | 5013b7be87 | ||
|   | 34d2056413 | ||
|   | 219a318ee3 | ||
|   | 13148f2c89 | ||
|   | dda7b52f94 | ||
|   | 56c6cc8c9f | ||
|   | af165539e6 | ||
|   | 1864cf6ad8 | ||
|   | 8c90ea860c | ||
|   | 46e4fe2896 | ||
|   | 4565dcc4d9 | ||
|   | 41bd8951dc | ||
|   | 952f6f5029 | ||
|   | f66f9c4eaf | ||
|   | b9d0e4061b | ||
|   | 39beaae20f | ||
|   | 6b2a85541d | ||
|   | 4d39e15920 | ||
|   | 42e6b4326f | ||
|   | 9161d3a758 | ||
|   | 4aa03ed0a2 | ||
|   | c3c1ae8e7f | ||
|   | 210320b8cc | ||
|   | 9753bd8b8a | ||
|   | 40a867a863 | ||
|   | d848cc33d7 | ||
|   | 1925cd0379 | ||
|   | 1905bbd898 | ||
|   | 59736f25e9 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | fd64585f99 | ||
|   | c544b8258f | ||
|   | 4926e90985 | ||
|   | 19e1427d92 | ||
|   | f7d3a8eab4 | ||
|   | 0b6648a823 | ||
|   | 2a73fd3fd6 | ||
|   | b28dc7218d | ||
|   | a0efa628d1 | ||
|   | 10b9ec32a8 | ||
|   | c191405b6d | ||
|   | 74a9445eff | ||
|   | b6b640cd33 | ||
|   | 774cdd33bc | ||
|   | 94207cb956 | ||
|   | 7546b61e01 | ||
|   | 394d50a328 | ||
|   | 04db4b821d | ||
|   | 61f9737557 | ||
|   | f86c74ff02 | ||
|   | 028d16d64e | ||
|   | bc32a0cc94 | ||
|   | 3552d29167 | ||
|   | 90a6771f4b | ||
|   | b28cee1f79 | ||
|   | 567672171a | ||
|   | be12da5690 | ||
|   | b147887b20 | ||
|   | f447aaed8d | ||
|   | 1a9aa23ae9 | ||
|   | fad0e55dcc | ||
|   | 52e330f323 | ||
|   | 6cab143db2 | ||
|   | 400e64906b | ||
|   | 627f86828c | ||
|   | 867ff200ce | ||
|   | 4913351540 | ||
|   | 1aee375c31 | ||
|   | 9f62df1456 | ||
|   | 9c9d6e61bb | ||
|   | a2e83d9018 | ||
|   | 6fa411d382 | ||
|   | c02d316866 | ||
|   | 16b9eecbcd | ||
|   | afdfeae7c3 | ||
|   | 54c536cbe2 | ||
|   | 7acc39abc8 | ||
|   | e7d617d89a | ||
|   | 849483eb3b | ||
|   | edc21fe41e | ||
|   | cf240aeee9 | ||
|   | d496676c84 | ||
|   | dcc7dbb9e1 | ||
|   | c0cab0974c | ||
|   | 7d2ebabec7 | ||
|   | 27cef4d250 | ||
|   | fb6efe93cd | ||
|   | ad5752f68e | ||
|   | 16f298896d | ||
|   | cf6e4c3e16 | ||
|   | 2e6dab89ff | ||
|   | 6dff2d6240 | ||
|   | b6d178b8c1 | ||
|   | fd8726b479 | ||
|   | f6aee64ec1 | ||
|   | 58a517afa6 | ||
|   | a02b90129d | ||
|   | d1adf79fc3 | ||
|   | 29887e1da5 | ||
|   | 5f4f6ced32 | ||
|   | cf99bab87b | ||
|   | c2902c9671 | ||
|   | 1c0a5a9765 | ||
|   | df014f0217 | ||
|   | 18783ff20b | ||
|   | 0db55ef2dd | ||
|   | 6f8842c170 | ||
|   | ea666bc18c | ||
|   | 721252d219 | ||
|   | 8f9f00df83 | ||
|   | bf1514e672 | ||
|   | ccfdd0cf06 | ||
|   | 10d6281edc | ||
|   | fa424514db | ||
|   | 9ed3f18893 | ||
|   | 789e435aac | ||
|   | d94c7b9c12 | ||
|   | 077cce9848 | ||
|   | a9b66ff943 | ||
|   | eaccc9305c | ||
|   | bd87e56bc7 | ||
|   | 58235049e3 | ||
|   | 29ed3c20af | ||
|   | 08aae39ea4 | ||
|   | 03fd114371 | ||
|   | 932e19d9a1 | ||
|   | 34f7ff42ae | ||
|   | 41abb8f9a5 | ||
|   | 22bf0ae505 | ||
|   | 6e259c2dbb | ||
|   | 80ed3a6f66 | ||
|   | 874f81e27b | ||
|   | 0ea74c2663 | ||
|   | 36e859be37 | ||
|   | 6f4296325a | ||
|   | b743786908 | ||
|   | 22b718a87d | ||
|   | af6581bfed | ||
|   | ec128914a3 | ||
|   | d2f1baa800 | ||
|   | 30e6d7a3c8 | ||
|   | 97f53765b5 | ||
|   | 29b544002c | ||
|   | fe1270e4c1 | ||
|   | 931f52cb7b | ||
|   | e1d854cf22 | ||
|   | 5478fa69e9 | ||
|   | 68d1a7e3ef | ||
|   | 922acda1a8 | ||
|   | a849ddd57d | ||
|   | f4d32c7def | ||
|   | 918650f15a | ||
|   | 287f65cbaf | ||
|   | b1dffcc921 | ||
|   | a8668d510f | ||
|   | 3636ab68f3 | ||
|   | d8da806bab | ||
|   | a21057a744 | ||
|   | d900b84e55 | ||
|   | 190fae51d8 | ||
|   | b30c4e716f | ||
|   | 658c50e0c6 | ||
|   | d6c23ac056 | ||
|   | f458ae9449 | ||
|   | 399b86255a | ||
|   | c38a558df8 | ||
|   | 299c937e67 | ||
|   | b6516c687d | ||
|   | f18c70a256 | ||
|   | 6fb490f49b | ||
|   | 83a4436b17 | ||
|   | 66cf7c3a3b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | f29021b5ef | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 7549ca4d39 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 33e7a2101b | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 59a216bfcb | ||
|   | 09d89000ad | ||
|   | b6c9ece0e6 | ||
|   | 6e1dace240 | ||
|   | 6e48f30147 | ||
|   | 90956f7417 | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 7169556722 | ||
|   | 5e6baba76c | ||
|   | 776198ec05 | ||
|   | a63b04fc0d | ||
|   | 7533da006e | ||
|   | 372c162e6b | ||
|   | b635689c29 | ||
|   | e4aec7f413 | ||
|   | bb99f68d33 | ||
|   | 47cbe74453 | ||
|   | cc815fd683 | ||
|   | 4cc41606d1 | ||
|   | 6cf0a38b86 | ||
|   | f6e4c0cb52 | ||
|   | 5e6ce6ee48 | ||
|   | f3634edc22 | ||
|   | c7904e845e | ||
|   | 44c2917f24 | ||
|   | a609343cb6 | ||
|   | 5528c3c765 | ||
|   | 0d805355f5 | ||
|   | 99f48ae51c | ||
|   | 25e4aafd71 | ||
|   | 4f2d54be4e | ||
| ![dependabot[bot]](/assets/img/avatar_default.png)  | 249cd7415b | ||
|   | 78d780105b | ||
|   | 466d4522bc | ||
|   | e462217500 | ||
|   | f1bce262ed | ||
|   | 7ed7e7ad26 | ||
|   | 5716b4bf2b | ||
|   | 2ecfe50a74 | ||
|   | 733001bf65 | ||
|   | 6d63e9869d | ||
|   | 0e1a79fc53 | ||
|   | c3606a9229 | ||
|   | 28ee05b1a3 | ||
|   | 5d170da762 | ||
|   | 60d949bf7b | ||
|   | 5426f8736b | 
							
								
								
									
										15
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										15
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							| @@ -416,7 +416,7 @@ jobs: | ||||
|             } | ||||
|  | ||||
|             // Generate review messages | ||||
|             function generateReviewMessages(finalLabels) { | ||||
|             function generateReviewMessages(finalLabels, originalLabelCount) { | ||||
|               const messages = []; | ||||
|               const prAuthor = context.payload.pull_request.user.login; | ||||
|  | ||||
| @@ -430,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).`; | ||||
|                 } | ||||
| @@ -466,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) | ||||
|               ); | ||||
| @@ -627,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']; | ||||
| @@ -635,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: | | ||||
|   | ||||
							
								
								
									
										62
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										62
									
								
								.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: | ||||
| @@ -180,6 +180,7 @@ jobs: | ||||
|       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 | ||||
| @@ -214,6 +215,7 @@ jobs: | ||||
|           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 | ||||
| @@ -458,7 +460,7 @@ jobs: | ||||
|       GH_TOKEN: ${{ github.token }} | ||||
|     strategy: | ||||
|       fail-fast: false | ||||
|       max-parallel: 1 | ||||
|       max-parallel: 2 | ||||
|       matrix: | ||||
|         include: | ||||
|           - id: clang-tidy | ||||
| @@ -536,59 +538,18 @@ jobs: | ||||
|         run: script/ci-suggest-changes | ||||
|         if: always() | ||||
|  | ||||
|   test-build-components-splitter: | ||||
|     name: Split components for intelligent grouping (40 weighted per batch) | ||||
|     runs-on: ubuntu-24.04 | ||||
|     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 }} | ||||
|     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: Split components intelligently based on bus configurations | ||||
|         id: split | ||||
|         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" | ||||
|           else | ||||
|             directly_changed='${{ needs.determine-jobs.outputs.directly-changed-components-with-tests }}' | ||||
|             echo "Target branch: ${{ github.base_ref }} - isolating directly changed components" | ||||
|           fi | ||||
|  | ||||
|           echo "Splitting components intelligently..." | ||||
|           output=$(python3 script/split_components_for_ci.py --components "$components" --directly-changed "$directly_changed" --batch-size 40 --output github) | ||||
|  | ||||
|           echo "$output" >> $GITHUB_OUTPUT | ||||
|  | ||||
|   test-build-components-split: | ||||
|     name: Test components batch (${{ matrix.components }}) | ||||
|     runs-on: ubuntu-24.04 | ||||
|     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: | | ||||
| @@ -849,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 | ||||
| @@ -913,7 +874,7 @@ jobs: | ||||
|             --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 | ||||
| @@ -943,13 +904,13 @@ jobs: | ||||
|           python-version: ${{ env.DEFAULT_PYTHON }} | ||||
|           cache-key: ${{ needs.common.outputs.cache-key }} | ||||
|       - name: Download target analysis JSON | ||||
|         uses: actions/download-artifact@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | ||||
|         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@634f93cb2916e3fdff6788551b99b062d0335ce0 # v5.0.0 | ||||
|         uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0 | ||||
|         with: | ||||
|           name: memory-analysis-pr | ||||
|           path: ./memory-analysis | ||||
| @@ -980,7 +941,6 @@ jobs: | ||||
|       - 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 | ||||
|   | ||||
| @@ -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 | ||||
|   | ||||
| @@ -207,14 +207,14 @@ 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) | ||||
| @@ -318,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): | ||||
|   | ||||
| @@ -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), | ||||
|   | ||||
| @@ -425,7 +425,7 @@ message ListEntitiesFanResponse { | ||||
|   bool disabled_by_default = 9; | ||||
|   string icon = 10 [(field_ifdef) = "USE_ENTITY_ICON"]; | ||||
|   EntityCategory entity_category = 11; | ||||
|   repeated string supported_preset_modes = 12 [(container_pointer) = "std::vector"]; | ||||
|   repeated string supported_preset_modes = 12 [(container_pointer_no_template) = "std::vector<const char *>"]; | ||||
|   uint32 device_id = 13 [(field_ifdef) = "USE_DEVICES"]; | ||||
| } | ||||
| // Deprecated in API version 1.6 - only used in deprecated fields | ||||
| @@ -1000,9 +1000,9 @@ message ListEntitiesClimateResponse { | ||||
|   bool supports_action = 12; // Deprecated: use feature_flags | ||||
|   repeated ClimateFanMode supported_fan_modes = 13 [(container_pointer_no_template) = "climate::ClimateFanModeMask"]; | ||||
|   repeated ClimateSwingMode supported_swing_modes = 14 [(container_pointer_no_template) = "climate::ClimateSwingModeMask"]; | ||||
|   repeated string supported_custom_fan_modes = 15 [(container_pointer) = "std::vector"]; | ||||
|   repeated string supported_custom_fan_modes = 15 [(container_pointer_no_template) = "std::vector<const char *>"]; | ||||
|   repeated ClimatePreset supported_presets = 16 [(container_pointer_no_template) = "climate::ClimatePresetMask"]; | ||||
|   repeated string supported_custom_presets = 17 [(container_pointer) = "std::vector"]; | ||||
|   repeated string supported_custom_presets = 17 [(container_pointer_no_template) = "std::vector<const char *>"]; | ||||
|   bool disabled_by_default = 18; | ||||
|   string icon = 19 [(field_ifdef) = "USE_ENTITY_ICON"]; | ||||
|   EntityCategory entity_category = 20; | ||||
|   | ||||
| @@ -423,7 +423,7 @@ uint16_t APIConnection::try_send_fan_info(EntityBase *entity, APIConnection *con | ||||
|   msg.supports_speed = traits.supports_speed(); | ||||
|   msg.supports_direction = traits.supports_direction(); | ||||
|   msg.supported_speed_count = traits.supported_speed_count(); | ||||
|   msg.supported_preset_modes = &traits.supported_preset_modes_for_api_(); | ||||
|   msg.supported_preset_modes = &traits.supported_preset_modes(); | ||||
|   return fill_and_encode_entity_info(fan, msg, ListEntitiesFanResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
| void APIConnection::fan_command(const FanCommandRequest &msg) { | ||||
| @@ -637,14 +637,14 @@ uint16_t APIConnection::try_send_climate_state(EntityBase *entity, APIConnection | ||||
|   } | ||||
|   if (traits.get_supports_fan_modes() && climate->fan_mode.has_value()) | ||||
|     resp.fan_mode = static_cast<enums::ClimateFanMode>(climate->fan_mode.value()); | ||||
|   if (!traits.get_supported_custom_fan_modes().empty() && climate->custom_fan_mode.has_value()) { | ||||
|     resp.set_custom_fan_mode(StringRef(climate->custom_fan_mode.value())); | ||||
|   if (!traits.get_supported_custom_fan_modes().empty() && climate->has_custom_fan_mode()) { | ||||
|     resp.set_custom_fan_mode(StringRef(climate->get_custom_fan_mode())); | ||||
|   } | ||||
|   if (traits.get_supports_presets() && climate->preset.has_value()) { | ||||
|     resp.preset = static_cast<enums::ClimatePreset>(climate->preset.value()); | ||||
|   } | ||||
|   if (!traits.get_supported_custom_presets().empty() && climate->custom_preset.has_value()) { | ||||
|     resp.set_custom_preset(StringRef(climate->custom_preset.value())); | ||||
|   if (!traits.get_supported_custom_presets().empty() && climate->has_custom_preset()) { | ||||
|     resp.set_custom_preset(StringRef(climate->get_custom_preset())); | ||||
|   } | ||||
|   if (traits.get_supports_swing_modes()) | ||||
|     resp.swing_mode = static_cast<enums::ClimateSwingMode>(climate->swing_mode); | ||||
| @@ -877,7 +877,7 @@ uint16_t APIConnection::try_send_select_state(EntityBase *entity, APIConnection | ||||
|                                               bool is_single) { | ||||
|   auto *select = static_cast<select::Select *>(entity); | ||||
|   SelectStateResponse resp; | ||||
|   resp.set_state(StringRef(select->state)); | ||||
|   resp.set_state(StringRef(select->current_option())); | ||||
|   resp.missing_state = !select->has_state(); | ||||
|   return fill_and_encode_entity_state(select, resp, SelectStateResponse::MESSAGE_TYPE, conn, remaining_size, is_single); | ||||
| } | ||||
|   | ||||
| @@ -355,8 +355,8 @@ void ListEntitiesFanResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   buffer.encode_string(10, this->icon_ref_); | ||||
| #endif | ||||
|   buffer.encode_uint32(11, static_cast<uint32_t>(this->entity_category)); | ||||
|   for (const auto &it : *this->supported_preset_modes) { | ||||
|     buffer.encode_string(12, it, true); | ||||
|   for (const char *it : *this->supported_preset_modes) { | ||||
|     buffer.encode_string(12, it, strlen(it), true); | ||||
|   } | ||||
| #ifdef USE_DEVICES | ||||
|   buffer.encode_uint32(13, this->device_id); | ||||
| @@ -376,8 +376,8 @@ void ListEntitiesFanResponse::calculate_size(ProtoSize &size) const { | ||||
| #endif | ||||
|   size.add_uint32(1, static_cast<uint32_t>(this->entity_category)); | ||||
|   if (!this->supported_preset_modes->empty()) { | ||||
|     for (const auto &it : *this->supported_preset_modes) { | ||||
|       size.add_length_force(1, it.size()); | ||||
|     for (const char *it : *this->supported_preset_modes) { | ||||
|       size.add_length_force(1, strlen(it)); | ||||
|     } | ||||
|   } | ||||
| #ifdef USE_DEVICES | ||||
| @@ -1179,14 +1179,14 @@ void ListEntitiesClimateResponse::encode(ProtoWriteBuffer buffer) const { | ||||
|   for (const auto &it : *this->supported_swing_modes) { | ||||
|     buffer.encode_uint32(14, static_cast<uint32_t>(it), true); | ||||
|   } | ||||
|   for (const auto &it : *this->supported_custom_fan_modes) { | ||||
|     buffer.encode_string(15, it, true); | ||||
|   for (const char *it : *this->supported_custom_fan_modes) { | ||||
|     buffer.encode_string(15, it, strlen(it), true); | ||||
|   } | ||||
|   for (const auto &it : *this->supported_presets) { | ||||
|     buffer.encode_uint32(16, static_cast<uint32_t>(it), true); | ||||
|   } | ||||
|   for (const auto &it : *this->supported_custom_presets) { | ||||
|     buffer.encode_string(17, it, true); | ||||
|   for (const char *it : *this->supported_custom_presets) { | ||||
|     buffer.encode_string(17, it, strlen(it), true); | ||||
|   } | ||||
|   buffer.encode_bool(18, this->disabled_by_default); | ||||
| #ifdef USE_ENTITY_ICON | ||||
| @@ -1229,8 +1229,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { | ||||
|     } | ||||
|   } | ||||
|   if (!this->supported_custom_fan_modes->empty()) { | ||||
|     for (const auto &it : *this->supported_custom_fan_modes) { | ||||
|       size.add_length_force(1, it.size()); | ||||
|     for (const char *it : *this->supported_custom_fan_modes) { | ||||
|       size.add_length_force(1, strlen(it)); | ||||
|     } | ||||
|   } | ||||
|   if (!this->supported_presets->empty()) { | ||||
| @@ -1239,8 +1239,8 @@ void ListEntitiesClimateResponse::calculate_size(ProtoSize &size) const { | ||||
|     } | ||||
|   } | ||||
|   if (!this->supported_custom_presets->empty()) { | ||||
|     for (const auto &it : *this->supported_custom_presets) { | ||||
|       size.add_length_force(2, it.size()); | ||||
|     for (const char *it : *this->supported_custom_presets) { | ||||
|       size.add_length_force(2, strlen(it)); | ||||
|     } | ||||
|   } | ||||
|   size.add_bool(2, this->disabled_by_default); | ||||
|   | ||||
| @@ -725,7 +725,7 @@ class ListEntitiesFanResponse final : public InfoResponseProtoMessage { | ||||
|   bool supports_speed{false}; | ||||
|   bool supports_direction{false}; | ||||
|   int32_t supported_speed_count{0}; | ||||
|   const std::vector<std::string> *supported_preset_modes{}; | ||||
|   const std::vector<const char *> *supported_preset_modes{}; | ||||
|   void encode(ProtoWriteBuffer buffer) const override; | ||||
|   void calculate_size(ProtoSize &size) const override; | ||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | ||||
| @@ -1384,9 +1384,9 @@ class ListEntitiesClimateResponse final : public InfoResponseProtoMessage { | ||||
|   bool supports_action{false}; | ||||
|   const climate::ClimateFanModeMask *supported_fan_modes{}; | ||||
|   const climate::ClimateSwingModeMask *supported_swing_modes{}; | ||||
|   const std::vector<std::string> *supported_custom_fan_modes{}; | ||||
|   const std::vector<const char *> *supported_custom_fan_modes{}; | ||||
|   const climate::ClimatePresetMask *supported_presets{}; | ||||
|   const std::vector<std::string> *supported_custom_presets{}; | ||||
|   const std::vector<const char *> *supported_custom_presets{}; | ||||
|   float visual_current_temperature_step{0.0f}; | ||||
|   bool supports_current_humidity{false}; | ||||
|   bool supports_target_humidity{false}; | ||||
|   | ||||
| @@ -7,7 +7,6 @@ | ||||
|  | ||||
| #include <cassert> | ||||
| #include <cstring> | ||||
| #include <type_traits> | ||||
| #include <vector> | ||||
|  | ||||
| #ifdef ESPHOME_LOG_HAS_VERY_VERBOSE | ||||
| @@ -160,6 +159,22 @@ class ProtoVarInt { | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|   void encode(std::vector<uint8_t> &out) { | ||||
|     uint64_t val = this->value_; | ||||
|     if (val <= 0x7F) { | ||||
|       out.push_back(val); | ||||
|       return; | ||||
|     } | ||||
|     while (val) { | ||||
|       uint8_t temp = val & 0x7F; | ||||
|       val >>= 7; | ||||
|       if (val) { | ||||
|         out.push_back(temp | 0x80); | ||||
|       } else { | ||||
|         out.push_back(temp); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|  protected: | ||||
|   uint64_t value_; | ||||
| @@ -218,86 +233,8 @@ class ProtoWriteBuffer { | ||||
|  public: | ||||
|   ProtoWriteBuffer(std::vector<uint8_t> *buffer) : buffer_(buffer) {} | ||||
|   void write(uint8_t value) { this->buffer_->push_back(value); } | ||||
|  | ||||
|   // Single implementation that all overloads delegate to | ||||
|   void encode_varint(uint64_t value) { | ||||
|     auto buffer = this->buffer_; | ||||
|     size_t start = buffer->size(); | ||||
|  | ||||
|     // Fast paths for common cases (1-3 bytes) | ||||
|     if (value < (1ULL << 7)) { | ||||
|       // 1 byte - very common for field IDs and small lengths | ||||
|       buffer->resize(start + 1); | ||||
|       buffer->data()[start] = static_cast<uint8_t>(value); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     uint8_t *p; | ||||
|     if (value < (1ULL << 14)) { | ||||
|       // 2 bytes - common for medium field IDs and lengths | ||||
|       buffer->resize(start + 2); | ||||
|       p = buffer->data() + start; | ||||
|       p[0] = (value & 0x7F) | 0x80; | ||||
|       p[1] = (value >> 7) & 0x7F; | ||||
|       return; | ||||
|     } | ||||
|     if (value < (1ULL << 21)) { | ||||
|       // 3 bytes - rare | ||||
|       buffer->resize(start + 3); | ||||
|       p = buffer->data() + start; | ||||
|       p[0] = (value & 0x7F) | 0x80; | ||||
|       p[1] = ((value >> 7) & 0x7F) | 0x80; | ||||
|       p[2] = (value >> 14) & 0x7F; | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     // Rare case: 4-10 byte values - calculate size from bit position | ||||
|     // Value is guaranteed >= (1ULL << 21), so CLZ is safe (non-zero) | ||||
|     uint32_t size; | ||||
| #if defined(__GNUC__) || defined(__clang__) | ||||
|     // Use compiler intrinsic for efficient bit position lookup | ||||
|     size = (64 - __builtin_clzll(value) + 6) / 7; | ||||
| #else | ||||
|     // Fallback for compilers without __builtin_clzll | ||||
|     if (value < (1ULL << 28)) { | ||||
|       size = 4; | ||||
|     } else if (value < (1ULL << 35)) { | ||||
|       size = 5; | ||||
|     } else if (value < (1ULL << 42)) { | ||||
|       size = 6; | ||||
|     } else if (value < (1ULL << 49)) { | ||||
|       size = 7; | ||||
|     } else if (value < (1ULL << 56)) { | ||||
|       size = 8; | ||||
|     } else if (value < (1ULL << 63)) { | ||||
|       size = 9; | ||||
|     } else { | ||||
|       size = 10; | ||||
|     } | ||||
| #endif | ||||
|  | ||||
|     buffer->resize(start + size); | ||||
|     p = buffer->data() + start; | ||||
|     size_t bytes = 0; | ||||
|     while (value) { | ||||
|       uint8_t temp = value & 0x7F; | ||||
|       value >>= 7; | ||||
|       p[bytes++] = value ? temp | 0x80 : temp; | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   // Common case: uint32_t values (field IDs, lengths, most integers) | ||||
|   void encode_varint(uint32_t value) { this->encode_varint(static_cast<uint64_t>(value)); } | ||||
|  | ||||
|   // size_t overload (only enabled if size_t is distinct from uint32_t and uint64_t) | ||||
|   template<typename T> | ||||
|   void encode_varint(T value) requires(std::is_same_v<T, size_t> && !std::is_same_v<size_t, uint32_t> && | ||||
|                                        !std::is_same_v<size_t, uint64_t>) { | ||||
|     this->encode_varint(static_cast<uint64_t>(value)); | ||||
|   } | ||||
|  | ||||
|   // Rare case: ProtoVarInt wrapper | ||||
|   void encode_varint(ProtoVarInt value) { this->encode_varint(value.as_uint64()); } | ||||
|   void encode_varint_raw(ProtoVarInt value) { value.encode(*this->buffer_); } | ||||
|   void encode_varint_raw(uint32_t value) { this->encode_varint_raw(ProtoVarInt(value)); } | ||||
|   /** | ||||
|    * Encode a field key (tag/wire type combination). | ||||
|    * | ||||
| @@ -312,14 +249,14 @@ class ProtoWriteBuffer { | ||||
|    */ | ||||
|   void encode_field_raw(uint32_t field_id, uint32_t type) { | ||||
|     uint32_t val = (field_id << 3) | (type & WIRE_TYPE_MASK); | ||||
|     this->encode_varint(val); | ||||
|     this->encode_varint_raw(val); | ||||
|   } | ||||
|   void encode_string(uint32_t field_id, const char *string, size_t len, bool force = false) { | ||||
|     if (len == 0 && !force) | ||||
|       return; | ||||
|  | ||||
|     this->encode_field_raw(field_id, 2);  // type 2: Length-delimited string | ||||
|     this->encode_varint(len); | ||||
|     this->encode_varint_raw(len); | ||||
|  | ||||
|     // Using resize + memcpy instead of insert provides significant performance improvement: | ||||
|     // ~10-11x faster for 16-32 byte strings, ~3x faster for 64-byte strings | ||||
| @@ -341,13 +278,13 @@ class ProtoWriteBuffer { | ||||
|     if (value == 0 && !force) | ||||
|       return; | ||||
|     this->encode_field_raw(field_id, 0);  // type 0: Varint - uint32 | ||||
|     this->encode_varint(value); | ||||
|     this->encode_varint_raw(value); | ||||
|   } | ||||
|   void encode_uint64(uint32_t field_id, uint64_t value, bool force = false) { | ||||
|     if (value == 0 && !force) | ||||
|       return; | ||||
|     this->encode_field_raw(field_id, 0);  // type 0: Varint - uint64 | ||||
|     this->encode_varint(value); | ||||
|     this->encode_varint_raw(ProtoVarInt(value)); | ||||
|   } | ||||
|   void encode_bool(uint32_t field_id, bool value, bool force = false) { | ||||
|     if (!value && !force) | ||||
|   | ||||
| @@ -48,7 +48,7 @@ void BedJetClimate::dump_config() { | ||||
|     ESP_LOGCONFIG(TAG, "   - %s", LOG_STR_ARG(climate_fan_mode_to_string(mode))); | ||||
|   } | ||||
|   for (const auto &mode : traits.get_supported_custom_fan_modes()) { | ||||
|     ESP_LOGCONFIG(TAG, "   - %s (c)", mode.c_str()); | ||||
|     ESP_LOGCONFIG(TAG, "   - %s (c)", mode); | ||||
|   } | ||||
|  | ||||
|   ESP_LOGCONFIG(TAG, "  Supported presets:"); | ||||
| @@ -56,7 +56,7 @@ void BedJetClimate::dump_config() { | ||||
|     ESP_LOGCONFIG(TAG, "   - %s", LOG_STR_ARG(climate_preset_to_string(preset))); | ||||
|   } | ||||
|   for (const auto &preset : traits.get_supported_custom_presets()) { | ||||
|     ESP_LOGCONFIG(TAG, "   - %s (c)", preset.c_str()); | ||||
|     ESP_LOGCONFIG(TAG, "   - %s (c)", preset); | ||||
|   } | ||||
| } | ||||
|  | ||||
| @@ -79,7 +79,7 @@ void BedJetClimate::reset_state_() { | ||||
|   this->target_temperature = NAN; | ||||
|   this->current_temperature = NAN; | ||||
|   this->preset.reset(); | ||||
|   this->custom_preset.reset(); | ||||
|   this->clear_custom_preset_(); | ||||
|   this->publish_state(); | ||||
| } | ||||
|  | ||||
| @@ -120,7 +120,7 @@ void BedJetClimate::control(const ClimateCall &call) { | ||||
|     if (button_result) { | ||||
|       this->mode = mode; | ||||
|       // We're using (custom) preset for Turbo, EXT HT, & M1-3 presets, so changing climate mode will clear those | ||||
|       this->custom_preset.reset(); | ||||
|       this->clear_custom_preset_(); | ||||
|       this->preset.reset(); | ||||
|     } | ||||
|   } | ||||
| @@ -144,8 +144,7 @@ void BedJetClimate::control(const ClimateCall &call) { | ||||
|  | ||||
|       if (result) { | ||||
|         this->mode = CLIMATE_MODE_HEAT; | ||||
|         this->preset = CLIMATE_PRESET_BOOST; | ||||
|         this->custom_preset.reset(); | ||||
|         this->set_preset_(CLIMATE_PRESET_BOOST); | ||||
|       } | ||||
|     } else if (preset == CLIMATE_PRESET_NONE && this->preset.has_value()) { | ||||
|       if (this->mode == CLIMATE_MODE_HEAT && this->preset == CLIMATE_PRESET_BOOST) { | ||||
| @@ -153,7 +152,7 @@ void BedJetClimate::control(const ClimateCall &call) { | ||||
|         result = this->parent_->send_button(heat_button(this->heating_mode_)); | ||||
|         if (result) { | ||||
|           this->preset.reset(); | ||||
|           this->custom_preset.reset(); | ||||
|           this->clear_custom_preset_(); | ||||
|         } | ||||
|       } else { | ||||
|         ESP_LOGD(TAG, "Ignoring preset '%s' call; with current mode '%s' and preset '%s'", | ||||
| @@ -184,8 +183,7 @@ void BedJetClimate::control(const ClimateCall &call) { | ||||
|     } | ||||
|  | ||||
|     if (result) { | ||||
|       this->custom_preset = preset; | ||||
|       this->preset.reset(); | ||||
|       this->set_custom_preset_(preset.c_str()); | ||||
|     } | ||||
|   } | ||||
|  | ||||
| @@ -207,8 +205,7 @@ void BedJetClimate::control(const ClimateCall &call) { | ||||
|     } | ||||
|  | ||||
|     if (result) { | ||||
|       this->fan_mode = fan_mode; | ||||
|       this->custom_fan_mode.reset(); | ||||
|       this->set_fan_mode_(fan_mode); | ||||
|     } | ||||
|   } else if (call.get_custom_fan_mode().has_value()) { | ||||
|     auto fan_mode = *call.get_custom_fan_mode(); | ||||
| @@ -218,8 +215,7 @@ void BedJetClimate::control(const ClimateCall &call) { | ||||
|                fan_index); | ||||
|       bool result = this->parent_->set_fan_index(fan_index); | ||||
|       if (result) { | ||||
|         this->custom_fan_mode = fan_mode; | ||||
|         this->fan_mode.reset(); | ||||
|         this->set_custom_fan_mode_(fan_mode.c_str()); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
| @@ -245,7 +241,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { | ||||
|  | ||||
|   const auto *fan_mode_name = bedjet_fan_step_to_fan_mode(data->fan_step); | ||||
|   if (fan_mode_name != nullptr) { | ||||
|     this->custom_fan_mode = *fan_mode_name; | ||||
|     this->set_custom_fan_mode_(fan_mode_name->c_str()); | ||||
|   } | ||||
|  | ||||
|   // TODO: Get biorhythm data to determine which preset (M1-3) is running, if any. | ||||
| @@ -255,7 +251,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { | ||||
|       this->mode = CLIMATE_MODE_OFF; | ||||
|       this->action = CLIMATE_ACTION_IDLE; | ||||
|       this->fan_mode = CLIMATE_FAN_OFF; | ||||
|       this->custom_preset.reset(); | ||||
|       this->clear_custom_preset_(); | ||||
|       this->preset.reset(); | ||||
|       break; | ||||
|  | ||||
| @@ -266,7 +262,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { | ||||
|       if (this->heating_mode_ == HEAT_MODE_EXTENDED) { | ||||
|         this->set_custom_preset_("LTD HT"); | ||||
|       } else { | ||||
|         this->custom_preset.reset(); | ||||
|         this->clear_custom_preset_(); | ||||
|       } | ||||
|       break; | ||||
|  | ||||
| @@ -275,7 +271,7 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { | ||||
|       this->action = CLIMATE_ACTION_HEATING; | ||||
|       this->preset.reset(); | ||||
|       if (this->heating_mode_ == HEAT_MODE_EXTENDED) { | ||||
|         this->custom_preset.reset(); | ||||
|         this->clear_custom_preset_(); | ||||
|       } else { | ||||
|         this->set_custom_preset_("EXT HT"); | ||||
|       } | ||||
| @@ -284,20 +280,19 @@ void BedJetClimate::on_status(const BedjetStatusPacket *data) { | ||||
|     case MODE_COOL: | ||||
|       this->mode = CLIMATE_MODE_FAN_ONLY; | ||||
|       this->action = CLIMATE_ACTION_COOLING; | ||||
|       this->custom_preset.reset(); | ||||
|       this->clear_custom_preset_(); | ||||
|       this->preset.reset(); | ||||
|       break; | ||||
|  | ||||
|     case MODE_DRY: | ||||
|       this->mode = CLIMATE_MODE_DRY; | ||||
|       this->action = CLIMATE_ACTION_DRYING; | ||||
|       this->custom_preset.reset(); | ||||
|       this->clear_custom_preset_(); | ||||
|       this->preset.reset(); | ||||
|       break; | ||||
|  | ||||
|     case MODE_TURBO: | ||||
|       this->preset = CLIMATE_PRESET_BOOST; | ||||
|       this->custom_preset.reset(); | ||||
|       this->set_preset_(CLIMATE_PRESET_BOOST); | ||||
|       this->mode = CLIMATE_MODE_HEAT; | ||||
|       this->action = CLIMATE_ACTION_HEATING; | ||||
|       break; | ||||
|   | ||||
| @@ -50,21 +50,13 @@ class BedJetClimate : public climate::Climate, public BedJetClient, public Polli | ||||
|         // Climate doesn't have a "TURBO" mode, but we can use the BOOST preset instead. | ||||
|         climate::CLIMATE_PRESET_BOOST, | ||||
|     }); | ||||
|     // String literals are stored in rodata and valid for program lifetime | ||||
|     traits.set_supported_custom_presets({ | ||||
|         // We could fetch biodata from bedjet and set these names that way. | ||||
|         // But then we have to invert the lookup in order to send the right preset. | ||||
|         // For now, we can leave them as M1-3 to match the remote buttons. | ||||
|         // EXT HT added to match remote button. | ||||
|         "EXT HT", | ||||
|         this->heating_mode_ == HEAT_MODE_EXTENDED ? "LTD HT" : "EXT HT", | ||||
|         "M1", | ||||
|         "M2", | ||||
|         "M3", | ||||
|     }); | ||||
|     if (this->heating_mode_ == HEAT_MODE_EXTENDED) { | ||||
|       traits.add_supported_custom_preset("LTD HT"); | ||||
|     } else { | ||||
|       traits.add_supported_custom_preset("EXT HT"); | ||||
|     } | ||||
|     traits.set_visual_min_temperature(19.0); | ||||
|     traits.set_visual_max_temperature(43.0); | ||||
|     traits.set_visual_temperature_step(1.0); | ||||
|   | ||||
| @@ -548,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") | ||||
|  | ||||
|   | ||||
| @@ -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") | ||||
|  | ||||
|   | ||||
| @@ -50,21 +50,21 @@ void ClimateCall::perform() { | ||||
|     const LogString *mode_s = climate_mode_to_string(*this->mode_); | ||||
|     ESP_LOGD(TAG, "  Mode: %s", LOG_STR_ARG(mode_s)); | ||||
|   } | ||||
|   if (this->custom_fan_mode_.has_value()) { | ||||
|   if (this->custom_fan_mode_ != nullptr) { | ||||
|     this->fan_mode_.reset(); | ||||
|     ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_.value().c_str()); | ||||
|     ESP_LOGD(TAG, " Custom Fan: %s", this->custom_fan_mode_); | ||||
|   } | ||||
|   if (this->fan_mode_.has_value()) { | ||||
|     this->custom_fan_mode_.reset(); | ||||
|     this->custom_fan_mode_ = nullptr; | ||||
|     const LogString *fan_mode_s = climate_fan_mode_to_string(*this->fan_mode_); | ||||
|     ESP_LOGD(TAG, "  Fan: %s", LOG_STR_ARG(fan_mode_s)); | ||||
|   } | ||||
|   if (this->custom_preset_.has_value()) { | ||||
|   if (this->custom_preset_ != nullptr) { | ||||
|     this->preset_.reset(); | ||||
|     ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_.value().c_str()); | ||||
|     ESP_LOGD(TAG, " Custom Preset: %s", this->custom_preset_); | ||||
|   } | ||||
|   if (this->preset_.has_value()) { | ||||
|     this->custom_preset_.reset(); | ||||
|     this->custom_preset_ = nullptr; | ||||
|     const LogString *preset_s = climate_preset_to_string(*this->preset_); | ||||
|     ESP_LOGD(TAG, "  Preset: %s", LOG_STR_ARG(preset_s)); | ||||
|   } | ||||
| @@ -96,11 +96,10 @@ void ClimateCall::validate_() { | ||||
|       this->mode_.reset(); | ||||
|     } | ||||
|   } | ||||
|   if (this->custom_fan_mode_.has_value()) { | ||||
|     auto custom_fan_mode = *this->custom_fan_mode_; | ||||
|     if (!traits.supports_custom_fan_mode(custom_fan_mode)) { | ||||
|       ESP_LOGW(TAG, "  Fan Mode %s not supported", custom_fan_mode.c_str()); | ||||
|       this->custom_fan_mode_.reset(); | ||||
|   if (this->custom_fan_mode_ != nullptr) { | ||||
|     if (!traits.supports_custom_fan_mode(this->custom_fan_mode_)) { | ||||
|       ESP_LOGW(TAG, "  Fan Mode %s not supported", this->custom_fan_mode_); | ||||
|       this->custom_fan_mode_ = nullptr; | ||||
|     } | ||||
|   } else if (this->fan_mode_.has_value()) { | ||||
|     auto fan_mode = *this->fan_mode_; | ||||
| @@ -109,11 +108,10 @@ void ClimateCall::validate_() { | ||||
|       this->fan_mode_.reset(); | ||||
|     } | ||||
|   } | ||||
|   if (this->custom_preset_.has_value()) { | ||||
|     auto custom_preset = *this->custom_preset_; | ||||
|     if (!traits.supports_custom_preset(custom_preset)) { | ||||
|       ESP_LOGW(TAG, "  Preset %s not supported", custom_preset.c_str()); | ||||
|       this->custom_preset_.reset(); | ||||
|   if (this->custom_preset_ != nullptr) { | ||||
|     if (!traits.supports_custom_preset(this->custom_preset_)) { | ||||
|       ESP_LOGW(TAG, "  Preset %s not supported", this->custom_preset_); | ||||
|       this->custom_preset_ = nullptr; | ||||
|     } | ||||
|   } else if (this->preset_.has_value()) { | ||||
|     auto preset = *this->preset_; | ||||
| @@ -186,26 +184,29 @@ ClimateCall &ClimateCall::set_mode(const std::string &mode) { | ||||
|  | ||||
| ClimateCall &ClimateCall::set_fan_mode(ClimateFanMode fan_mode) { | ||||
|   this->fan_mode_ = fan_mode; | ||||
|   this->custom_fan_mode_.reset(); | ||||
|   this->custom_fan_mode_ = nullptr; | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { | ||||
| ClimateCall &ClimateCall::set_fan_mode(const char *custom_fan_mode) { | ||||
|   // Check if it's a standard enum mode first | ||||
|   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 (str_equals_case_insensitive(custom_fan_mode, mode_entry.str)) { | ||||
|       return this->set_fan_mode(static_cast<ClimateFanMode>(mode_entry.value)); | ||||
|     } | ||||
|   } | ||||
|   if (this->parent_->get_traits().supports_custom_fan_mode(fan_mode)) { | ||||
|     this->custom_fan_mode_ = fan_mode; | ||||
|   // Find the matching pointer from parent climate device | ||||
|   if (const char *mode_ptr = this->parent_->find_custom_fan_mode_(custom_fan_mode)) { | ||||
|     this->custom_fan_mode_ = mode_ptr; | ||||
|     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; | ||||
|   } | ||||
|   ESP_LOGW(TAG, "'%s' - Unrecognized fan mode %s", this->parent_->get_name().c_str(), custom_fan_mode); | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_fan_mode(const std::string &fan_mode) { return this->set_fan_mode(fan_mode.c_str()); } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) { | ||||
|   if (fan_mode.has_value()) { | ||||
|     this->set_fan_mode(fan_mode.value()); | ||||
| @@ -215,26 +216,29 @@ ClimateCall &ClimateCall::set_fan_mode(optional<std::string> fan_mode) { | ||||
|  | ||||
| ClimateCall &ClimateCall::set_preset(ClimatePreset preset) { | ||||
|   this->preset_ = preset; | ||||
|   this->custom_preset_.reset(); | ||||
|   this->custom_preset_ = nullptr; | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_preset(const std::string &preset) { | ||||
| ClimateCall &ClimateCall::set_preset(const char *custom_preset) { | ||||
|   // Check if it's a standard enum preset first | ||||
|   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 (str_equals_case_insensitive(custom_preset, preset_entry.str)) { | ||||
|       return this->set_preset(static_cast<ClimatePreset>(preset_entry.value)); | ||||
|     } | ||||
|   } | ||||
|   if (this->parent_->get_traits().supports_custom_preset(preset)) { | ||||
|     this->custom_preset_ = preset; | ||||
|   // Find the matching pointer from parent climate device | ||||
|   if (const char *preset_ptr = this->parent_->find_custom_preset_(custom_preset)) { | ||||
|     this->custom_preset_ = preset_ptr; | ||||
|     this->preset_.reset(); | ||||
|   } else { | ||||
|     ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), preset.c_str()); | ||||
|     return *this; | ||||
|   } | ||||
|   ESP_LOGW(TAG, "'%s' - Unrecognized preset %s", this->parent_->get_name().c_str(), custom_preset); | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_preset(const std::string &preset) { return this->set_preset(preset.c_str()); } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_preset(optional<std::string> preset) { | ||||
|   if (preset.has_value()) { | ||||
|     this->set_preset(preset.value()); | ||||
| @@ -287,8 +291,14 @@ const optional<ClimateMode> &ClimateCall::get_mode() const { return this->mode_; | ||||
| const optional<ClimateFanMode> &ClimateCall::get_fan_mode() const { return this->fan_mode_; } | ||||
| const optional<ClimateSwingMode> &ClimateCall::get_swing_mode() const { return this->swing_mode_; } | ||||
| const optional<ClimatePreset> &ClimateCall::get_preset() const { return this->preset_; } | ||||
| const optional<std::string> &ClimateCall::get_custom_fan_mode() const { return this->custom_fan_mode_; } | ||||
| const optional<std::string> &ClimateCall::get_custom_preset() const { return this->custom_preset_; } | ||||
|  | ||||
| optional<std::string> ClimateCall::get_custom_fan_mode() const { | ||||
|   return this->custom_fan_mode_ != nullptr ? std::string(this->custom_fan_mode_) : optional<std::string>{}; | ||||
| } | ||||
|  | ||||
| optional<std::string> ClimateCall::get_custom_preset() const { | ||||
|   return this->custom_preset_ != nullptr ? std::string(this->custom_preset_) : optional<std::string>{}; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_target_temperature_high(optional<float> target_temperature_high) { | ||||
|   this->target_temperature_high_ = target_temperature_high; | ||||
| @@ -317,13 +327,13 @@ ClimateCall &ClimateCall::set_mode(optional<ClimateMode> mode) { | ||||
|  | ||||
| ClimateCall &ClimateCall::set_fan_mode(optional<ClimateFanMode> fan_mode) { | ||||
|   this->fan_mode_ = fan_mode; | ||||
|   this->custom_fan_mode_.reset(); | ||||
|   this->custom_fan_mode_ = nullptr; | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| ClimateCall &ClimateCall::set_preset(optional<ClimatePreset> preset) { | ||||
|   this->preset_ = preset; | ||||
|   this->custom_preset_.reset(); | ||||
|   this->custom_preset_ = nullptr; | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| @@ -382,13 +392,13 @@ void Climate::save_state_() { | ||||
|     state.uses_custom_fan_mode = false; | ||||
|     state.fan_mode = this->fan_mode.value(); | ||||
|   } | ||||
|   if (!traits.get_supported_custom_fan_modes().empty() && custom_fan_mode.has_value()) { | ||||
|   if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) { | ||||
|     state.uses_custom_fan_mode = true; | ||||
|     const auto &supported = traits.get_supported_custom_fan_modes(); | ||||
|     // std::vector maintains insertion order | ||||
|     size_t i = 0; | ||||
|     for (const auto &mode : supported) { | ||||
|       if (mode == custom_fan_mode) { | ||||
|     for (const char *mode : supported) { | ||||
|       if (strcmp(mode, this->custom_fan_mode_) == 0) { | ||||
|         state.custom_fan_mode = i; | ||||
|         break; | ||||
|       } | ||||
| @@ -399,13 +409,13 @@ void Climate::save_state_() { | ||||
|     state.uses_custom_preset = false; | ||||
|     state.preset = this->preset.value(); | ||||
|   } | ||||
|   if (!traits.get_supported_custom_presets().empty() && custom_preset.has_value()) { | ||||
|   if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) { | ||||
|     state.uses_custom_preset = true; | ||||
|     const auto &supported = traits.get_supported_custom_presets(); | ||||
|     // std::vector maintains insertion order | ||||
|     size_t i = 0; | ||||
|     for (const auto &preset : supported) { | ||||
|       if (preset == custom_preset) { | ||||
|     for (const char *preset : supported) { | ||||
|       if (strcmp(preset, this->custom_preset_) == 0) { | ||||
|         state.custom_preset = i; | ||||
|         break; | ||||
|       } | ||||
| @@ -430,14 +440,14 @@ void Climate::publish_state() { | ||||
|   if (traits.get_supports_fan_modes() && this->fan_mode.has_value()) { | ||||
|     ESP_LOGD(TAG, "  Fan Mode: %s", LOG_STR_ARG(climate_fan_mode_to_string(this->fan_mode.value()))); | ||||
|   } | ||||
|   if (!traits.get_supported_custom_fan_modes().empty() && this->custom_fan_mode.has_value()) { | ||||
|     ESP_LOGD(TAG, "  Custom Fan Mode: %s", this->custom_fan_mode.value().c_str()); | ||||
|   if (!traits.get_supported_custom_fan_modes().empty() && this->has_custom_fan_mode()) { | ||||
|     ESP_LOGD(TAG, "  Custom Fan Mode: %s", this->custom_fan_mode_); | ||||
|   } | ||||
|   if (traits.get_supports_presets() && this->preset.has_value()) { | ||||
|     ESP_LOGD(TAG, "  Preset: %s", LOG_STR_ARG(climate_preset_to_string(this->preset.value()))); | ||||
|   } | ||||
|   if (!traits.get_supported_custom_presets().empty() && this->custom_preset.has_value()) { | ||||
|     ESP_LOGD(TAG, "  Custom Preset: %s", this->custom_preset.value().c_str()); | ||||
|   if (!traits.get_supported_custom_presets().empty() && this->has_custom_preset()) { | ||||
|     ESP_LOGD(TAG, "  Custom Preset: %s", this->custom_preset_); | ||||
|   } | ||||
|   if (traits.get_supports_swing_modes()) { | ||||
|     ESP_LOGD(TAG, "  Swing Mode: %s", LOG_STR_ARG(climate_swing_mode_to_string(this->swing_mode))); | ||||
| @@ -527,7 +537,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { | ||||
|   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); | ||||
|       call.custom_fan_mode_ = traits.get_supported_custom_fan_modes()[this->custom_fan_mode]; | ||||
|     } | ||||
|   } else if (traits.supports_fan_mode(this->fan_mode)) { | ||||
|     call.set_fan_mode(this->fan_mode); | ||||
| @@ -535,7 +545,7 @@ ClimateCall ClimateDeviceRestoreState::to_call(Climate *climate) { | ||||
|   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); | ||||
|       call.custom_preset_ = traits.get_supported_custom_presets()[this->custom_preset]; | ||||
|     } | ||||
|   } else if (traits.supports_preset(this->preset)) { | ||||
|     call.set_preset(this->preset); | ||||
| @@ -562,20 +572,20 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { | ||||
|   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); | ||||
|       climate->custom_fan_mode_ = traits.get_supported_custom_fan_modes()[this->custom_fan_mode]; | ||||
|     } | ||||
|   } else if (traits.supports_fan_mode(this->fan_mode)) { | ||||
|     climate->fan_mode = this->fan_mode; | ||||
|     climate->custom_fan_mode.reset(); | ||||
|     climate->clear_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); | ||||
|       climate->custom_preset_ = traits.get_supported_custom_presets()[this->custom_preset]; | ||||
|     } | ||||
|   } else if (traits.supports_preset(this->preset)) { | ||||
|     climate->preset = this->preset; | ||||
|     climate->custom_preset.reset(); | ||||
|     climate->clear_custom_preset_(); | ||||
|   } | ||||
|   if (traits.supports_swing_mode(this->swing_mode)) { | ||||
|     climate->swing_mode = this->swing_mode; | ||||
| @@ -583,28 +593,107 @@ void ClimateDeviceRestoreState::apply(Climate *climate) { | ||||
|   climate->publish_state(); | ||||
| } | ||||
|  | ||||
| template<typename T1, typename T2> bool set_alternative(optional<T1> &dst, optional<T2> &alt, const T1 &src) { | ||||
|   bool is_changed = alt.has_value(); | ||||
|   alt.reset(); | ||||
|   if (is_changed || dst != src) { | ||||
|     dst = src; | ||||
|     is_changed = true; | ||||
| /** Template helper for setting primary modes (fan_mode, preset) with mutual exclusion. | ||||
|  * | ||||
|  * Climate devices have mutually exclusive mode pairs: | ||||
|  *   - fan_mode (enum) vs custom_fan_mode_ (const char*) | ||||
|  *   - preset (enum) vs custom_preset_ (const char*) | ||||
|  * | ||||
|  * Only one mode in each pair can be active at a time. This helper ensures setting a primary | ||||
|  * mode automatically clears its corresponding custom mode. | ||||
|  * | ||||
|  * Example state transitions: | ||||
|  *   Before: custom_fan_mode_="Turbo", fan_mode=nullopt | ||||
|  *   Call:   set_fan_mode_(CLIMATE_FAN_HIGH) | ||||
|  *   After:  custom_fan_mode_=nullptr,   fan_mode=CLIMATE_FAN_HIGH | ||||
|  * | ||||
|  * @param primary The primary mode optional (fan_mode or preset) | ||||
|  * @param custom_ptr Reference to the custom mode pointer (custom_fan_mode_ or custom_preset_) | ||||
|  * @param value The new primary mode value to set | ||||
|  * @return true if state changed, false if already set to this value | ||||
|  */ | ||||
| template<typename T> bool set_primary_mode(optional<T> &primary, const char *&custom_ptr, T value) { | ||||
|   // Clear the custom mode (mutual exclusion) | ||||
|   bool changed = custom_ptr != nullptr; | ||||
|   custom_ptr = nullptr; | ||||
|   // Set the primary mode | ||||
|   if (changed || !primary.has_value() || primary.value() != value) { | ||||
|     primary = value; | ||||
|     return true; | ||||
|   } | ||||
|   return is_changed; | ||||
|   return false; | ||||
| } | ||||
|  | ||||
| /** Template helper for setting custom modes (custom_fan_mode_, custom_preset_) with mutual exclusion. | ||||
|  * | ||||
|  * This helper ensures setting a custom mode automatically clears its corresponding primary mode. | ||||
|  * It also validates that the custom mode exists in the device's supported modes (lifetime safety). | ||||
|  * | ||||
|  * Example state transitions: | ||||
|  *   Before: fan_mode=CLIMATE_FAN_HIGH, custom_fan_mode_=nullptr | ||||
|  *   Call:   set_custom_fan_mode_("Turbo") | ||||
|  *   After:  fan_mode=nullopt,          custom_fan_mode_="Turbo" (pointer from traits) | ||||
|  * | ||||
|  * Lifetime Safety: | ||||
|  *   - found_ptr must come from traits.find_custom_*_mode_() | ||||
|  *   - Only pointers found in traits are stored, ensuring they remain valid | ||||
|  *   - Prevents dangling pointers from temporary strings | ||||
|  * | ||||
|  * @param custom_ptr Reference to the custom mode pointer to set | ||||
|  * @param primary The primary mode optional to clear | ||||
|  * @param found_ptr The validated pointer from traits (nullptr if not found) | ||||
|  * @param has_custom Whether a custom mode is currently active | ||||
|  * @return true if state changed, false otherwise | ||||
|  */ | ||||
| template<typename T> | ||||
| bool set_custom_mode(const char *&custom_ptr, optional<T> &primary, const char *found_ptr, bool has_custom) { | ||||
|   if (found_ptr != nullptr) { | ||||
|     // Clear the primary mode (mutual exclusion) | ||||
|     bool changed = primary.has_value(); | ||||
|     primary.reset(); | ||||
|     // Set the custom mode (pointer is validated by caller from traits) | ||||
|     if (changed || custom_ptr != found_ptr) { | ||||
|       custom_ptr = found_ptr; | ||||
|       return true; | ||||
|     } | ||||
|     return false; | ||||
|   } | ||||
|   // Mode not found in supported modes, clear it if currently set | ||||
|   if (has_custom) { | ||||
|     custom_ptr = nullptr; | ||||
|     return true; | ||||
|   } | ||||
|   return false; | ||||
| } | ||||
|  | ||||
| bool Climate::set_fan_mode_(ClimateFanMode mode) { | ||||
|   return set_alternative(this->fan_mode, this->custom_fan_mode, mode); | ||||
|   return set_primary_mode(this->fan_mode, this->custom_fan_mode_, mode); | ||||
| } | ||||
|  | ||||
| bool Climate::set_custom_fan_mode_(const std::string &mode) { | ||||
|   return set_alternative(this->custom_fan_mode, this->fan_mode, mode); | ||||
| bool Climate::set_custom_fan_mode_(const char *mode) { | ||||
|   auto traits = this->get_traits(); | ||||
|   return set_custom_mode<ClimateFanMode>(this->custom_fan_mode_, this->fan_mode, traits.find_custom_fan_mode_(mode), | ||||
|                                          this->has_custom_fan_mode()); | ||||
| } | ||||
|  | ||||
| bool Climate::set_preset_(ClimatePreset preset) { return set_alternative(this->preset, this->custom_preset, preset); } | ||||
| void Climate::clear_custom_fan_mode_() { this->custom_fan_mode_ = nullptr; } | ||||
|  | ||||
| bool Climate::set_custom_preset_(const std::string &preset) { | ||||
|   return set_alternative(this->custom_preset, this->preset, preset); | ||||
| bool Climate::set_preset_(ClimatePreset preset) { return set_primary_mode(this->preset, this->custom_preset_, preset); } | ||||
|  | ||||
| bool Climate::set_custom_preset_(const char *preset) { | ||||
|   auto traits = this->get_traits(); | ||||
|   return set_custom_mode<ClimatePreset>(this->custom_preset_, this->preset, traits.find_custom_preset_(preset), | ||||
|                                         this->has_custom_preset()); | ||||
| } | ||||
|  | ||||
| void Climate::clear_custom_preset_() { this->custom_preset_ = nullptr; } | ||||
|  | ||||
| const char *Climate::find_custom_fan_mode_(const char *custom_fan_mode) { | ||||
|   return this->get_traits().find_custom_fan_mode_(custom_fan_mode); | ||||
| } | ||||
|  | ||||
| const char *Climate::find_custom_preset_(const char *custom_preset) { | ||||
|   return this->get_traits().find_custom_preset_(custom_preset); | ||||
| } | ||||
|  | ||||
| void Climate::dump_traits_(const char *tag) { | ||||
| @@ -656,8 +745,8 @@ void Climate::dump_traits_(const char *tag) { | ||||
|   } | ||||
|   if (!traits.get_supported_custom_fan_modes().empty()) { | ||||
|     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()); | ||||
|     for (const char *s : traits.get_supported_custom_fan_modes()) | ||||
|       ESP_LOGCONFIG(tag, "  - %s", s); | ||||
|   } | ||||
|   if (!traits.get_supported_presets().empty()) { | ||||
|     ESP_LOGCONFIG(tag, "  Supported presets:"); | ||||
| @@ -666,8 +755,8 @@ void Climate::dump_traits_(const char *tag) { | ||||
|   } | ||||
|   if (!traits.get_supported_custom_presets().empty()) { | ||||
|     ESP_LOGCONFIG(tag, "  Supported custom presets:"); | ||||
|     for (const std::string &s : traits.get_supported_custom_presets()) | ||||
|       ESP_LOGCONFIG(tag, "  - %s", s.c_str()); | ||||
|     for (const char *s : traits.get_supported_custom_presets()) | ||||
|       ESP_LOGCONFIG(tag, "  - %s", s); | ||||
|   } | ||||
|   if (!traits.get_supported_swing_modes().empty()) { | ||||
|     ESP_LOGCONFIG(tag, "  Supported swing modes:"); | ||||
|   | ||||
| @@ -77,6 +77,8 @@ class ClimateCall { | ||||
|   ClimateCall &set_fan_mode(const std::string &fan_mode); | ||||
|   /// Set the fan mode of the climate device based on a string. | ||||
|   ClimateCall &set_fan_mode(optional<std::string> fan_mode); | ||||
|   /// Set the custom fan mode of the climate device. | ||||
|   ClimateCall &set_fan_mode(const char *custom_fan_mode); | ||||
|   /// Set the swing mode of the climate device. | ||||
|   ClimateCall &set_swing_mode(ClimateSwingMode swing_mode); | ||||
|   /// Set the swing mode of the climate device. | ||||
| @@ -91,6 +93,8 @@ class ClimateCall { | ||||
|   ClimateCall &set_preset(const std::string &preset); | ||||
|   /// Set the preset of the climate device based on a string. | ||||
|   ClimateCall &set_preset(optional<std::string> preset); | ||||
|   /// Set the custom preset of the climate device. | ||||
|   ClimateCall &set_preset(const char *custom_preset); | ||||
|  | ||||
|   void perform(); | ||||
|  | ||||
| @@ -103,8 +107,8 @@ class ClimateCall { | ||||
|   const optional<ClimateFanMode> &get_fan_mode() const; | ||||
|   const optional<ClimateSwingMode> &get_swing_mode() const; | ||||
|   const optional<ClimatePreset> &get_preset() const; | ||||
|   const optional<std::string> &get_custom_fan_mode() const; | ||||
|   const optional<std::string> &get_custom_preset() const; | ||||
|   optional<std::string> get_custom_fan_mode() const; | ||||
|   optional<std::string> get_custom_preset() const; | ||||
|  | ||||
|  protected: | ||||
|   void validate_(); | ||||
| @@ -118,8 +122,8 @@ class ClimateCall { | ||||
|   optional<ClimateFanMode> fan_mode_; | ||||
|   optional<ClimateSwingMode> swing_mode_; | ||||
|   optional<ClimatePreset> preset_; | ||||
|   optional<std::string> custom_fan_mode_; | ||||
|   optional<std::string> custom_preset_; | ||||
|   const char *custom_fan_mode_{nullptr}; | ||||
|   const char *custom_preset_{nullptr}; | ||||
| }; | ||||
|  | ||||
| /// Struct used to save the state of the climate device in restore memory. | ||||
| @@ -212,6 +216,12 @@ 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); | ||||
|  | ||||
|   /// Check if a custom fan mode is currently active. | ||||
|   bool has_custom_fan_mode() const { return this->custom_fan_mode_ != nullptr; } | ||||
|  | ||||
|   /// Check if a custom preset is currently active. | ||||
|   bool has_custom_preset() const { return this->custom_preset_ != nullptr; } | ||||
|  | ||||
|   /// The current temperature of the climate device, as reported from the integration. | ||||
|   float current_temperature{NAN}; | ||||
|  | ||||
| @@ -238,12 +248,6 @@ class Climate : public EntityBase { | ||||
|   /// 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}; | ||||
|  | ||||
| @@ -253,20 +257,37 @@ class Climate : public EntityBase { | ||||
|   /// The active swing mode of the climate device. | ||||
|   ClimateSwingMode swing_mode{CLIMATE_SWING_OFF}; | ||||
|  | ||||
|   /// Get the active custom fan mode (read-only access). | ||||
|   const char *get_custom_fan_mode() const { return this->custom_fan_mode_; } | ||||
|  | ||||
|   /// Get the active custom preset (read-only access). | ||||
|   const char *get_custom_preset() const { return this->custom_preset_; } | ||||
|  | ||||
|  protected: | ||||
|   friend ClimateCall; | ||||
|   friend struct ClimateDeviceRestoreState; | ||||
|  | ||||
|   /// Set fan mode. Reset custom fan mode. Return true if fan mode has been changed. | ||||
|   bool set_fan_mode_(ClimateFanMode mode); | ||||
|  | ||||
|   /// Set custom fan mode. Reset primary fan mode. Return true if fan mode has been changed. | ||||
|   bool set_custom_fan_mode_(const std::string &mode); | ||||
|   bool set_custom_fan_mode_(const char *mode); | ||||
|   /// Clear custom fan mode. | ||||
|   void clear_custom_fan_mode_(); | ||||
|  | ||||
|   /// Set preset. Reset custom preset. Return true if preset has been changed. | ||||
|   bool set_preset_(ClimatePreset preset); | ||||
|  | ||||
|   /// Set custom preset. Reset primary preset. Return true if preset has been changed. | ||||
|   bool set_custom_preset_(const std::string &preset); | ||||
|   bool set_custom_preset_(const char *preset); | ||||
|   /// Clear custom preset. | ||||
|   void clear_custom_preset_(); | ||||
|  | ||||
|   /// Find and return the matching custom fan mode pointer from traits, or nullptr if not found. | ||||
|   const char *find_custom_fan_mode_(const char *custom_fan_mode); | ||||
|  | ||||
|   /// Find and return the matching custom preset pointer from traits, or nullptr if not found. | ||||
|   const char *find_custom_preset_(const char *custom_preset); | ||||
|  | ||||
|   /** Get the default traits of this climate device. | ||||
|    * | ||||
| @@ -303,6 +324,21 @@ class Climate : public EntityBase { | ||||
|   optional<float> visual_current_temperature_step_override_{}; | ||||
|   optional<float> visual_min_humidity_override_{}; | ||||
|   optional<float> visual_max_humidity_override_{}; | ||||
|  | ||||
|  private: | ||||
|   /** The active custom fan mode (private - enforces use of safe setters). | ||||
|    * | ||||
|    * Points to an entry in traits.supported_custom_fan_modes_ or nullptr. | ||||
|    * Use get_custom_fan_mode() to read, set_custom_fan_mode_() to modify. | ||||
|    */ | ||||
|   const char *custom_fan_mode_{nullptr}; | ||||
|  | ||||
|   /** The active custom preset (private - enforces use of safe setters). | ||||
|    * | ||||
|    * Points to an entry in traits.supported_custom_presets_ or nullptr. | ||||
|    * Use get_custom_preset() to read, set_custom_preset_() to modify. | ||||
|    */ | ||||
|   const char *custom_preset_{nullptr}; | ||||
| }; | ||||
|  | ||||
| }  // namespace climate | ||||
|   | ||||
| @@ -1,5 +1,6 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <cstring> | ||||
| #include <vector> | ||||
| #include "climate_mode.h" | ||||
| #include "esphome/core/finite_set_mask.h" | ||||
| @@ -18,16 +19,25 @@ 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) | ||||
| // Lightweight linear search for small vectors (1-20 items) of const char* pointers | ||||
| // 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) | ||||
| inline bool vector_contains(const std::vector<const char *> &vec, const char *value) { | ||||
|   for (const char *item : vec) { | ||||
|     if (strcmp(item, value) == 0) | ||||
|       return true; | ||||
|   } | ||||
|   return false; | ||||
| } | ||||
|  | ||||
| // Find and return matching pointer from vector, or nullptr if not found | ||||
| inline const char *vector_find(const std::vector<const char *> &vec, const char *value) { | ||||
|   for (const char *item : vec) { | ||||
|     if (strcmp(item, value) == 0) | ||||
|       return item; | ||||
|   } | ||||
|   return nullptr; | ||||
| } | ||||
|  | ||||
| /** This class contains all static data for climate devices. | ||||
|  * | ||||
|  * All climate devices must support these features: | ||||
| @@ -55,7 +65,11 @@ template<typename T> inline bool vector_contains(const std::vector<T> &vec, cons | ||||
|  *  - temperature step - the step with which to increase/decrease target temperature. | ||||
|  *     This also affects with how many decimal places the temperature is shown | ||||
|  */ | ||||
| class Climate;  // Forward declaration | ||||
|  | ||||
| class ClimateTraits { | ||||
|   friend class Climate;  // Allow Climate to access protected find methods | ||||
|  | ||||
|  public: | ||||
|   /// Get/set feature flags (see ClimateFeatures enum in climate_mode.h) | ||||
|   uint32_t get_feature_flags() const { return this->feature_flags_; } | ||||
| @@ -128,47 +142,61 @@ class ClimateTraits { | ||||
|  | ||||
|   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_.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 ClimateFanModeMask &get_supported_fan_modes() const { return this->supported_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); | ||||
|   void set_supported_custom_fan_modes(std::initializer_list<const char *> modes) { | ||||
|     this->supported_custom_fan_modes_ = modes; | ||||
|   } | ||||
|   void set_supported_custom_fan_modes(std::initializer_list<std::string> modes) { | ||||
|   void set_supported_custom_fan_modes(const std::vector<const char *> &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 { | ||||
|  | ||||
|   // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages | ||||
|   void set_supported_custom_fan_modes(const std::vector<std::string> &modes) = delete; | ||||
|   void set_supported_custom_fan_modes(std::initializer_list<std::string> modes) = delete; | ||||
|  | ||||
|   const std::vector<const char *> &get_supported_custom_fan_modes() const { return this->supported_custom_fan_modes_; } | ||||
|   bool supports_custom_fan_mode(const char *custom_fan_mode) const { | ||||
|     return vector_contains(this->supported_custom_fan_modes_, custom_fan_mode); | ||||
|   } | ||||
|   bool supports_custom_fan_mode(const std::string &custom_fan_mode) const { | ||||
|     return this->supports_custom_fan_mode(custom_fan_mode.c_str()); | ||||
|   } | ||||
|  | ||||
|   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_.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 ClimatePresetMask &get_supported_presets() const { return this->supported_presets_; } | ||||
|  | ||||
|   void set_supported_custom_presets(std::vector<std::string> supported_custom_presets) { | ||||
|     this->supported_custom_presets_ = std::move(supported_custom_presets); | ||||
|   void set_supported_custom_presets(std::initializer_list<const char *> presets) { | ||||
|     this->supported_custom_presets_ = presets; | ||||
|   } | ||||
|   void set_supported_custom_presets(std::initializer_list<std::string> presets) { | ||||
|   void set_supported_custom_presets(const std::vector<const char *> &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 { | ||||
|  | ||||
|   // Deleted overloads to catch incorrect std::string usage at compile time with clear error messages | ||||
|   void set_supported_custom_presets(const std::vector<std::string> &presets) = delete; | ||||
|   void set_supported_custom_presets(std::initializer_list<std::string> presets) = delete; | ||||
|  | ||||
|   const std::vector<const char *> &get_supported_custom_presets() const { return this->supported_custom_presets_; } | ||||
|   bool supports_custom_preset(const char *custom_preset) const { | ||||
|     return vector_contains(this->supported_custom_presets_, custom_preset); | ||||
|   } | ||||
|   bool supports_custom_preset(const std::string &custom_preset) const { | ||||
|     return this->supports_custom_preset(custom_preset.c_str()); | ||||
|   } | ||||
|  | ||||
|   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); } | ||||
| @@ -227,6 +255,18 @@ class ClimateTraits { | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   /// Find and return the matching custom fan mode pointer from supported modes, or nullptr if not found | ||||
|   /// This is protected as it's an implementation detail - use Climate::find_custom_fan_mode_() instead | ||||
|   const char *find_custom_fan_mode_(const char *custom_fan_mode) const { | ||||
|     return vector_find(this->supported_custom_fan_modes_, custom_fan_mode); | ||||
|   } | ||||
|  | ||||
|   /// Find and return the matching custom preset pointer from supported presets, or nullptr if not found | ||||
|   /// This is protected as it's an implementation detail - use Climate::find_custom_preset_() instead | ||||
|   const char *find_custom_preset_(const char *custom_preset) const { | ||||
|     return vector_find(this->supported_custom_presets_, custom_preset); | ||||
|   } | ||||
|  | ||||
|   uint32_t feature_flags_{0}; | ||||
|   float visual_min_temperature_{10}; | ||||
|   float visual_max_temperature_{30}; | ||||
| @@ -239,8 +279,17 @@ class ClimateTraits { | ||||
|   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_; | ||||
|  | ||||
|   /** Custom mode storage using const char* pointers to eliminate std::string overhead. | ||||
|    * | ||||
|    * Pointers must remain valid for the ClimateTraits lifetime. Safe patterns: | ||||
|    *  - String literals: set_supported_custom_fan_modes({"Turbo", "Silent"}) | ||||
|    *  - Static const data: static const char* MODE = "Eco"; | ||||
|    * | ||||
|    * Climate class setters validate pointers are from these vectors before storing. | ||||
|    */ | ||||
|   std::vector<const char *> supported_custom_fan_modes_; | ||||
|   std::vector<const char *> 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) | ||||
|   | ||||
| @@ -7,19 +7,19 @@ namespace copy { | ||||
| static const char *const TAG = "copy.select"; | ||||
|  | ||||
| void CopySelect::setup() { | ||||
|   source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(value); }); | ||||
|   source_->add_on_state_callback([this](const std::string &value, size_t index) { this->publish_state(index); }); | ||||
|  | ||||
|   traits.set_options(source_->traits.get_options()); | ||||
|  | ||||
|   if (source_->has_state()) | ||||
|     this->publish_state(source_->state); | ||||
|     this->publish_state(source_->active_index().value()); | ||||
| } | ||||
|  | ||||
| void CopySelect::dump_config() { LOG_SELECT("", "Copy Select", this); } | ||||
|  | ||||
| void CopySelect::control(const std::string &value) { | ||||
| void CopySelect::control(size_t index) { | ||||
|   auto call = source_->make_call(); | ||||
|   call.set_option(value); | ||||
|   call.set_index(index); | ||||
|   call.perform(); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -13,7 +13,7 @@ class CopySelect : public select::Select, public Component { | ||||
|   void dump_config() override; | ||||
|  | ||||
|  protected: | ||||
|   void control(const std::string &value) override; | ||||
|   void control(size_t index) override; | ||||
|  | ||||
|   select::Select *source_; | ||||
| }; | ||||
|   | ||||
| @@ -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") | ||||
|  | ||||
|   | ||||
| @@ -28,16 +28,16 @@ class DemoClimate : public climate::Climate, public Component { | ||||
|         this->mode = climate::CLIMATE_MODE_AUTO; | ||||
|         this->action = climate::CLIMATE_ACTION_COOLING; | ||||
|         this->fan_mode = climate::CLIMATE_FAN_HIGH; | ||||
|         this->custom_preset = {"My Preset"}; | ||||
|         this->set_custom_preset_("My Preset"); | ||||
|         break; | ||||
|       case DemoClimateType::TYPE_3: | ||||
|         this->current_temperature = 21.5; | ||||
|         this->target_temperature_low = 21.0; | ||||
|         this->target_temperature_high = 22.5; | ||||
|         this->mode = climate::CLIMATE_MODE_HEAT_COOL; | ||||
|         this->custom_fan_mode = {"Auto Low"}; | ||||
|         this->set_custom_fan_mode_("Auto Low"); | ||||
|         this->swing_mode = climate::CLIMATE_SWING_HORIZONTAL; | ||||
|         this->preset = climate::CLIMATE_PRESET_AWAY; | ||||
|         this->set_preset_(climate::CLIMATE_PRESET_AWAY); | ||||
|         break; | ||||
|     } | ||||
|     this->publish_state(); | ||||
| @@ -58,23 +58,19 @@ class DemoClimate : public climate::Climate, public Component { | ||||
|       this->target_temperature_high = *call.get_target_temperature_high(); | ||||
|     } | ||||
|     if (call.get_fan_mode().has_value()) { | ||||
|       this->fan_mode = *call.get_fan_mode(); | ||||
|       this->custom_fan_mode.reset(); | ||||
|       this->set_fan_mode_(*call.get_fan_mode()); | ||||
|     } | ||||
|     if (call.get_swing_mode().has_value()) { | ||||
|       this->swing_mode = *call.get_swing_mode(); | ||||
|     } | ||||
|     if (call.get_custom_fan_mode().has_value()) { | ||||
|       this->custom_fan_mode = *call.get_custom_fan_mode(); | ||||
|       this->fan_mode.reset(); | ||||
|       this->set_custom_fan_mode_(call.get_custom_fan_mode()->c_str()); | ||||
|     } | ||||
|     if (call.get_preset().has_value()) { | ||||
|       this->preset = *call.get_preset(); | ||||
|       this->custom_preset.reset(); | ||||
|       this->set_preset_(*call.get_preset()); | ||||
|     } | ||||
|     if (call.get_custom_preset().has_value()) { | ||||
|       this->custom_preset = *call.get_custom_preset(); | ||||
|       this->preset.reset(); | ||||
|       this->set_custom_preset_(call.get_custom_preset()->c_str()); | ||||
|     } | ||||
|     this->publish_state(); | ||||
|   } | ||||
|   | ||||
| @@ -42,7 +42,7 @@ std::string MenuItemSelect::get_value_text() const { | ||||
|     result = this->value_getter_.value()(this); | ||||
|   } else { | ||||
|     if (this->select_var_ != nullptr) { | ||||
|       result = this->select_var_->state; | ||||
|       result = this->select_var_->current_option(); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   | ||||
| @@ -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(), 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(), 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 | ||||
|   | ||||
| @@ -31,6 +31,26 @@ namespace esphome::esp32_ble { | ||||
|  | ||||
| static const char *const TAG = "esp32_ble"; | ||||
|  | ||||
| // GAP event groups for deduplication across gap_event_handler and dispatch_gap_event_ | ||||
| #define GAP_SCAN_COMPLETE_EVENTS \ | ||||
|   case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: \ | ||||
|   case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: \ | ||||
|   case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT | ||||
|  | ||||
| #define GAP_ADV_COMPLETE_EVENTS \ | ||||
|   case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: \ | ||||
|   case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: \ | ||||
|   case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: \ | ||||
|   case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: \ | ||||
|   case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT | ||||
|  | ||||
| #define GAP_SECURITY_EVENTS \ | ||||
|   case ESP_GAP_BLE_AUTH_CMPL_EVT: \ | ||||
|   case ESP_GAP_BLE_SEC_REQ_EVT: \ | ||||
|   case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: \ | ||||
|   case ESP_GAP_BLE_PASSKEY_REQ_EVT: \ | ||||
|   case ESP_GAP_BLE_NC_REQ_EVT | ||||
|  | ||||
| void ESP32BLE::setup() { | ||||
|   global_ble = this; | ||||
|   if (!ble_pre_setup_()) { | ||||
| @@ -418,60 +438,48 @@ void ESP32BLE::loop() { | ||||
|             break; | ||||
|  | ||||
|           // Scan complete events | ||||
|           case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: | ||||
|           case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: | ||||
|           case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: | ||||
|             // All three scan complete events have the same structure with just status | ||||
|             // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe | ||||
|             // This is verified at compile-time by static_assert checks in ble_event.h | ||||
|             // The struct already contains our copy of the status (copied in BLEEvent constructor) | ||||
|             ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); | ||||
| #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT | ||||
|             for (auto *gap_handler : this->gap_event_handlers_) { | ||||
|               gap_handler->gap_event_handler( | ||||
|                   gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete)); | ||||
|             } | ||||
| #endif | ||||
|             break; | ||||
|  | ||||
|           GAP_SCAN_COMPLETE_EVENTS: | ||||
|           // Advertising complete events | ||||
|           case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: | ||||
|           case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: | ||||
|           case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: | ||||
|           case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: | ||||
|           case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: | ||||
|             // All advertising complete events have the same structure with just status | ||||
|             ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); | ||||
| #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT | ||||
|             for (auto *gap_handler : this->gap_event_handlers_) { | ||||
|               gap_handler->gap_event_handler( | ||||
|                   gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.adv_complete)); | ||||
|             } | ||||
| #endif | ||||
|             break; | ||||
|  | ||||
|           GAP_ADV_COMPLETE_EVENTS: | ||||
|           // RSSI complete event | ||||
|           case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: | ||||
|             ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); | ||||
| #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT | ||||
|             for (auto *gap_handler : this->gap_event_handlers_) { | ||||
|               gap_handler->gap_event_handler( | ||||
|                   gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.read_rssi_complete)); | ||||
|             } | ||||
| #endif | ||||
|             break; | ||||
|  | ||||
|           // Security events | ||||
|           case ESP_GAP_BLE_AUTH_CMPL_EVT: | ||||
|           case ESP_GAP_BLE_SEC_REQ_EVT: | ||||
|           case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: | ||||
|           case ESP_GAP_BLE_PASSKEY_REQ_EVT: | ||||
|           case ESP_GAP_BLE_NC_REQ_EVT: | ||||
|           GAP_SECURITY_EVENTS: | ||||
|             ESP_LOGV(TAG, "gap_event_handler - %d", gap_event); | ||||
| #ifdef ESPHOME_ESP32_BLE_GAP_EVENT_HANDLER_COUNT | ||||
|             for (auto *gap_handler : this->gap_event_handlers_) { | ||||
|               gap_handler->gap_event_handler( | ||||
|                   gap_event, reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.security)); | ||||
|             { | ||||
|               esp_ble_gap_cb_param_t *param; | ||||
|               // clang-format off | ||||
|               switch (gap_event) { | ||||
|                 // All three scan complete events have the same structure with just status | ||||
|                 // The scan_complete struct matches ESP-IDF's layout exactly, so this reinterpret_cast is safe | ||||
|                 // This is verified at compile-time by static_assert checks in ble_event.h | ||||
|                 // The struct already contains our copy of the status (copied in BLEEvent constructor) | ||||
|                 GAP_SCAN_COMPLETE_EVENTS: | ||||
|                   param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.scan_complete); | ||||
|                   break; | ||||
|  | ||||
|                 // All advertising complete events have the same structure with just status | ||||
|                 GAP_ADV_COMPLETE_EVENTS: | ||||
|                   param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.adv_complete); | ||||
|                   break; | ||||
|  | ||||
|                 case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: | ||||
|                   param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.read_rssi_complete); | ||||
|                   break; | ||||
|  | ||||
|                 GAP_SECURITY_EVENTS: | ||||
|                   param = reinterpret_cast<esp_ble_gap_cb_param_t *>(&ble_event->event_.gap.security); | ||||
|                   break; | ||||
|  | ||||
|                 default: | ||||
|                   break; | ||||
|               } | ||||
|               // clang-format on | ||||
|               // Dispatch to all registered handlers | ||||
|               for (auto *gap_handler : this->gap_event_handlers_) { | ||||
|                 gap_handler->gap_event_handler(gap_event, param); | ||||
|               } | ||||
|             } | ||||
| #endif | ||||
|             break; | ||||
| @@ -551,23 +559,13 @@ void ESP32BLE::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_pa | ||||
|     // Queue GAP events that components need to handle | ||||
|     // Scanning events - used by esp32_ble_tracker | ||||
|     case ESP_GAP_BLE_SCAN_RESULT_EVT: | ||||
|     case ESP_GAP_BLE_SCAN_PARAM_SET_COMPLETE_EVT: | ||||
|     case ESP_GAP_BLE_SCAN_START_COMPLETE_EVT: | ||||
|     case ESP_GAP_BLE_SCAN_STOP_COMPLETE_EVT: | ||||
|     GAP_SCAN_COMPLETE_EVENTS: | ||||
|     // Advertising events - used by esp32_ble_beacon and esp32_ble server | ||||
|     case ESP_GAP_BLE_ADV_DATA_SET_COMPLETE_EVT: | ||||
|     case ESP_GAP_BLE_SCAN_RSP_DATA_SET_COMPLETE_EVT: | ||||
|     case ESP_GAP_BLE_ADV_DATA_RAW_SET_COMPLETE_EVT: | ||||
|     case ESP_GAP_BLE_ADV_START_COMPLETE_EVT: | ||||
|     case ESP_GAP_BLE_ADV_STOP_COMPLETE_EVT: | ||||
|     GAP_ADV_COMPLETE_EVENTS: | ||||
|     // Connection events - used by ble_client | ||||
|     case ESP_GAP_BLE_READ_RSSI_COMPLETE_EVT: | ||||
|     // Security events - used by ble_client and bluetooth_proxy | ||||
|     case ESP_GAP_BLE_AUTH_CMPL_EVT: | ||||
|     case ESP_GAP_BLE_SEC_REQ_EVT: | ||||
|     case ESP_GAP_BLE_PASSKEY_NOTIF_EVT: | ||||
|     case ESP_GAP_BLE_PASSKEY_REQ_EVT: | ||||
|     case ESP_GAP_BLE_NC_REQ_EVT: | ||||
|     GAP_SECURITY_EVENTS: | ||||
|       enqueue_ble_event(event, param); | ||||
|       return; | ||||
|  | ||||
|   | ||||
| @@ -281,19 +281,15 @@ void ESPHomeOTAComponent::handle_data_() { | ||||
| #endif | ||||
|  | ||||
|   // Acknowledge auth OK - 1 byte | ||||
|   buf[0] = ota::OTA_RESPONSE_AUTH_OK; | ||||
|   this->writeall_(buf, 1); | ||||
|   this->write_byte_(ota::OTA_RESPONSE_AUTH_OK); | ||||
|  | ||||
|   // Read size, 4 bytes MSB first | ||||
|   if (!this->readall_(buf, 4)) { | ||||
|     this->log_read_error_(LOG_STR("size")); | ||||
|     goto error;  // NOLINT(cppcoreguidelines-avoid-goto) | ||||
|   } | ||||
|   ota_size = 0; | ||||
|   for (uint8_t i = 0; i < 4; i++) { | ||||
|     ota_size <<= 8; | ||||
|     ota_size |= buf[i]; | ||||
|   } | ||||
|   ota_size = (static_cast<size_t>(buf[0]) << 24) | (static_cast<size_t>(buf[1]) << 16) | | ||||
|              (static_cast<size_t>(buf[2]) << 8) | buf[3]; | ||||
|   ESP_LOGV(TAG, "Size is %u bytes", ota_size); | ||||
|  | ||||
|   // Now that we've passed authentication and are actually | ||||
| @@ -313,8 +309,7 @@ void ESPHomeOTAComponent::handle_data_() { | ||||
|   update_started = true; | ||||
|  | ||||
|   // Acknowledge prepare OK - 1 byte | ||||
|   buf[0] = ota::OTA_RESPONSE_UPDATE_PREPARE_OK; | ||||
|   this->writeall_(buf, 1); | ||||
|   this->write_byte_(ota::OTA_RESPONSE_UPDATE_PREPARE_OK); | ||||
|  | ||||
|   // Read binary MD5, 32 bytes | ||||
|   if (!this->readall_(buf, 32)) { | ||||
| @@ -326,8 +321,7 @@ void ESPHomeOTAComponent::handle_data_() { | ||||
|   this->backend_->set_update_md5(sbuf); | ||||
|  | ||||
|   // Acknowledge MD5 OK - 1 byte | ||||
|   buf[0] = ota::OTA_RESPONSE_BIN_MD5_OK; | ||||
|   this->writeall_(buf, 1); | ||||
|   this->write_byte_(ota::OTA_RESPONSE_BIN_MD5_OK); | ||||
|  | ||||
|   while (total < ota_size) { | ||||
|     // TODO: timeout check | ||||
| @@ -354,8 +348,7 @@ void ESPHomeOTAComponent::handle_data_() { | ||||
|     total += read; | ||||
| #if USE_OTA_VERSION == 2 | ||||
|     while (size_acknowledged + OTA_BLOCK_SIZE <= total || (total == ota_size && size_acknowledged < ota_size)) { | ||||
|       buf[0] = ota::OTA_RESPONSE_CHUNK_OK; | ||||
|       this->writeall_(buf, 1); | ||||
|       this->write_byte_(ota::OTA_RESPONSE_CHUNK_OK); | ||||
|       size_acknowledged += OTA_BLOCK_SIZE; | ||||
|     } | ||||
| #endif | ||||
| @@ -374,8 +367,7 @@ void ESPHomeOTAComponent::handle_data_() { | ||||
|   } | ||||
|  | ||||
|   // Acknowledge receive OK - 1 byte | ||||
|   buf[0] = ota::OTA_RESPONSE_RECEIVE_OK; | ||||
|   this->writeall_(buf, 1); | ||||
|   this->write_byte_(ota::OTA_RESPONSE_RECEIVE_OK); | ||||
|  | ||||
|   error_code = this->backend_->end(); | ||||
|   if (error_code != ota::OTA_RESPONSE_OK) { | ||||
| @@ -384,8 +376,7 @@ void ESPHomeOTAComponent::handle_data_() { | ||||
|   } | ||||
|  | ||||
|   // Acknowledge Update end OK - 1 byte | ||||
|   buf[0] = ota::OTA_RESPONSE_UPDATE_END_OK; | ||||
|   this->writeall_(buf, 1); | ||||
|   this->write_byte_(ota::OTA_RESPONSE_UPDATE_END_OK); | ||||
|  | ||||
|   // Read ACK | ||||
|   if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) { | ||||
| @@ -404,8 +395,7 @@ void ESPHomeOTAComponent::handle_data_() { | ||||
|   App.safe_reboot(); | ||||
|  | ||||
| error: | ||||
|   buf[0] = static_cast<uint8_t>(error_code); | ||||
|   this->writeall_(buf, 1); | ||||
|   this->write_byte_(static_cast<uint8_t>(error_code)); | ||||
|   this->cleanup_connection_(); | ||||
|  | ||||
|   if (this->backend_ != nullptr && update_started) { | ||||
|   | ||||
| @@ -53,6 +53,7 @@ class ESPHomeOTAComponent : public ota::OTAComponent { | ||||
| #endif  // USE_OTA_PASSWORD | ||||
|   bool readall_(uint8_t *buf, size_t len); | ||||
|   bool writeall_(const uint8_t *buf, size_t len); | ||||
|   inline bool write_byte_(uint8_t byte) { return this->writeall_(&byte, 1); } | ||||
|  | ||||
|   bool try_read_(size_t to_read, const LogString *desc); | ||||
|   bool try_write_(size_t to_write, const LogString *desc); | ||||
|   | ||||
| @@ -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") | ||||
|  | ||||
|   | ||||
| @@ -189,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), | ||||
|   | ||||
| @@ -53,7 +53,7 @@ void FanCall::validate_() { | ||||
|     const auto &preset_modes = traits.supported_preset_modes(); | ||||
|     bool found = false; | ||||
|     for (const auto &mode : preset_modes) { | ||||
|       if (mode == this->preset_mode_) { | ||||
|       if (strcmp(mode, this->preset_mode_.c_str()) == 0) { | ||||
|         found = true; | ||||
|         break; | ||||
|       } | ||||
| @@ -99,11 +99,12 @@ FanCall FanRestoreState::to_call(Fan &fan) { | ||||
|   call.set_speed(this->speed); | ||||
|   call.set_direction(this->direction); | ||||
|  | ||||
|   if (fan.get_traits().supports_preset_modes()) { | ||||
|   auto traits = fan.get_traits(); | ||||
|   if (traits.supports_preset_modes()) { | ||||
|     // Use stored preset index to get preset name | ||||
|     const auto &preset_modes = fan.get_traits().supported_preset_modes(); | ||||
|     const auto &preset_modes = traits.supported_preset_modes(); | ||||
|     if (this->preset_mode < preset_modes.size()) { | ||||
|       call.set_preset_mode(*std::next(preset_modes.begin(), this->preset_mode)); | ||||
|       call.set_preset_mode(preset_modes[this->preset_mode]); | ||||
|     } | ||||
|   } | ||||
|   return call; | ||||
| @@ -114,11 +115,12 @@ void FanRestoreState::apply(Fan &fan) { | ||||
|   fan.speed = this->speed; | ||||
|   fan.direction = this->direction; | ||||
|  | ||||
|   if (fan.get_traits().supports_preset_modes()) { | ||||
|   auto traits = fan.get_traits(); | ||||
|   if (traits.supports_preset_modes()) { | ||||
|     // Use stored preset index to get preset name | ||||
|     const auto &preset_modes = fan.get_traits().supported_preset_modes(); | ||||
|     const auto &preset_modes = traits.supported_preset_modes(); | ||||
|     if (this->preset_mode < preset_modes.size()) { | ||||
|       fan.preset_mode = *std::next(preset_modes.begin(), this->preset_mode); | ||||
|       fan.preset_mode = preset_modes[this->preset_mode]; | ||||
|     } | ||||
|   } | ||||
|   fan.publish_state(); | ||||
| @@ -189,18 +191,20 @@ void Fan::save_state_() { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   auto traits = this->get_traits(); | ||||
|  | ||||
|   FanRestoreState state{}; | ||||
|   state.state = this->state; | ||||
|   state.oscillating = this->oscillating; | ||||
|   state.speed = this->speed; | ||||
|   state.direction = this->direction; | ||||
|  | ||||
|   if (this->get_traits().supports_preset_modes() && !this->preset_mode.empty()) { | ||||
|     const auto &preset_modes = this->get_traits().supported_preset_modes(); | ||||
|   if (traits.supports_preset_modes() && !this->preset_mode.empty()) { | ||||
|     const auto &preset_modes = traits.supported_preset_modes(); | ||||
|     // Store index of current preset mode | ||||
|     size_t i = 0; | ||||
|     for (const auto &mode : preset_modes) { | ||||
|       if (mode == this->preset_mode) { | ||||
|       if (strcmp(mode, this->preset_mode.c_str()) == 0) { | ||||
|         state.preset_mode = i; | ||||
|         break; | ||||
|       } | ||||
| @@ -228,8 +232,8 @@ void Fan::dump_traits_(const char *tag, const char *prefix) { | ||||
|   } | ||||
|   if (traits.supports_preset_modes()) { | ||||
|     ESP_LOGCONFIG(tag, "%s  Supported presets:", prefix); | ||||
|     for (const std::string &s : traits.supported_preset_modes()) | ||||
|       ESP_LOGCONFIG(tag, "%s    - %s", prefix, s.c_str()); | ||||
|     for (const char *s : traits.supported_preset_modes()) | ||||
|       ESP_LOGCONFIG(tag, "%s    - %s", prefix, s); | ||||
|   } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -60,8 +60,6 @@ class FanCall { | ||||
|     this->speed_ = speed; | ||||
|     return *this; | ||||
|   } | ||||
|   ESPDEPRECATED("set_speed() with string argument is deprecated, use integer argument instead.", "2021.9") | ||||
|   FanCall &set_speed(const char *legacy_speed); | ||||
|   optional<int> get_speed() const { return this->speed_; } | ||||
|   FanCall &set_direction(FanDirection direction) { | ||||
|     this->direction_ = direction; | ||||
|   | ||||
| @@ -1,15 +1,10 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <vector> | ||||
| #include <initializer_list> | ||||
|  | ||||
| namespace esphome { | ||||
|  | ||||
| #ifdef USE_API | ||||
| namespace api { | ||||
| class APIConnection; | ||||
| }  // namespace api | ||||
| #endif | ||||
|  | ||||
| namespace fan { | ||||
|  | ||||
| class FanTraits { | ||||
| @@ -35,27 +30,22 @@ class FanTraits { | ||||
|   /// Set whether this fan supports changing direction | ||||
|   void set_direction(bool direction) { this->direction_ = direction; } | ||||
|   /// Return the preset modes supported by the fan. | ||||
|   const std::vector<std::string> &supported_preset_modes() const { return this->preset_modes_; } | ||||
|   /// Set the preset modes supported by the fan. | ||||
|   void set_supported_preset_modes(const std::vector<std::string> &preset_modes) { this->preset_modes_ = preset_modes; } | ||||
|   const std::vector<const char *> &supported_preset_modes() const { return this->preset_modes_; } | ||||
|   /// Set the preset modes supported by the fan (from initializer list). | ||||
|   void set_supported_preset_modes(std::initializer_list<const char *> preset_modes) { | ||||
|     this->preset_modes_ = preset_modes; | ||||
|   } | ||||
|   /// Set the preset modes supported by the fan (from vector). | ||||
|   void set_supported_preset_modes(const std::vector<const char *> &preset_modes) { this->preset_modes_ = preset_modes; } | ||||
|   /// Return if preset modes are supported | ||||
|   bool supports_preset_modes() const { return !this->preset_modes_.empty(); } | ||||
|  | ||||
|  protected: | ||||
| #ifdef USE_API | ||||
|   // The API connection is a friend class to access internal methods | ||||
|   friend class api::APIConnection; | ||||
|   // This method returns a reference to the internal preset modes. | ||||
|   // It is used by the API to avoid copying data when encoding messages. | ||||
|   // Warning: Do not use this method outside of the API connection code. | ||||
|   // It returns a reference to internal data that can be invalidated. | ||||
|   const std::vector<std::string> &supported_preset_modes_for_api_() const { return this->preset_modes_; } | ||||
| #endif | ||||
|   bool oscillation_{false}; | ||||
|   bool speed_{false}; | ||||
|   bool direction_{false}; | ||||
|   int speed_count_{}; | ||||
|   std::vector<std::string> preset_modes_{}; | ||||
|   std::vector<const char *> preset_modes_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace fan | ||||
|   | ||||
| @@ -94,6 +94,8 @@ async def to_code(config): | ||||
|         ) | ||||
|         use_interrupt = False | ||||
|  | ||||
|     cg.add(var.set_use_interrupt(use_interrupt)) | ||||
|     if use_interrupt: | ||||
|         cg.add(var.set_interrupt_type(config[CONF_INTERRUPT_TYPE])) | ||||
|     else: | ||||
|         # Only generate call when disabling interrupts (default is true) | ||||
|         cg.add(var.set_use_interrupt(use_interrupt)) | ||||
|   | ||||
| @@ -1,7 +1,5 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <set> | ||||
|  | ||||
| #include "esphome/core/automation.h" | ||||
| #include "esphome/components/output/binary_output.h" | ||||
| #include "esphome/components/output/float_output.h" | ||||
| @@ -22,7 +20,7 @@ class HBridgeFan : public Component, public fan::Fan { | ||||
|   void set_pin_a(output::FloatOutput *pin_a) { pin_a_ = pin_a; } | ||||
|   void set_pin_b(output::FloatOutput *pin_b) { pin_b_ = pin_b; } | ||||
|   void set_enable_pin(output::FloatOutput *enable) { enable_ = enable; } | ||||
|   void set_preset_modes(const std::vector<std::string> &presets) { preset_modes_ = presets; } | ||||
|   void set_preset_modes(std::initializer_list<const char *> presets) { preset_modes_ = presets; } | ||||
|  | ||||
|   void setup() override; | ||||
|   void dump_config() override; | ||||
| @@ -38,7 +36,7 @@ class HBridgeFan : public Component, public fan::Fan { | ||||
|   int speed_count_{}; | ||||
|   DecayMode decay_mode_{DECAY_MODE_SLOW}; | ||||
|   fan::FanTraits traits_; | ||||
|   std::vector<std::string> preset_modes_{}; | ||||
|   std::vector<const char *> preset_modes_{}; | ||||
|  | ||||
|   void control(const fan::FanCall &call) override; | ||||
|   void write_state_(); | ||||
|   | ||||
| @@ -12,7 +12,6 @@ from esphome.const import ( | ||||
|     CONF_ON_ERROR, | ||||
|     CONF_ON_RESPONSE, | ||||
|     CONF_TIMEOUT, | ||||
|     CONF_TRIGGER_ID, | ||||
|     CONF_URL, | ||||
|     CONF_WATCHDOG_TIMEOUT, | ||||
|     PLATFORM_HOST, | ||||
| @@ -216,16 +215,8 @@ HTTP_REQUEST_ACTION_SCHEMA = cv.Schema( | ||||
|             f"{CONF_VERIFY_SSL} has moved to the base component configuration." | ||||
|         ), | ||||
|         cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean, | ||||
|         cv.Optional(CONF_ON_RESPONSE): automation.validate_automation( | ||||
|             {cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(HttpRequestResponseTrigger)} | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_ERROR): automation.validate_automation( | ||||
|             { | ||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id( | ||||
|                     automation.Trigger.template() | ||||
|                 ) | ||||
|             } | ||||
|         ), | ||||
|         cv.Optional(CONF_ON_RESPONSE): automation.validate_automation(single=True), | ||||
|         cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True), | ||||
|         cv.Optional(CONF_MAX_RESPONSE_BUFFER_SIZE, default="1kB"): cv.validate_bytes, | ||||
|     } | ||||
| ) | ||||
| @@ -280,7 +271,12 @@ async def http_request_action_to_code(config, action_id, template_arg, args): | ||||
|     template_ = await cg.templatable(config[CONF_URL], args, cg.std_string) | ||||
|     cg.add(var.set_url(template_)) | ||||
|     cg.add(var.set_method(config[CONF_METHOD])) | ||||
|     cg.add(var.set_capture_response(config[CONF_CAPTURE_RESPONSE])) | ||||
|  | ||||
|     capture_response = config[CONF_CAPTURE_RESPONSE] | ||||
|     if capture_response: | ||||
|         cg.add(var.set_capture_response(capture_response)) | ||||
|         cg.add_define("USE_HTTP_REQUEST_RESPONSE") | ||||
|  | ||||
|     cg.add(var.set_max_response_buffer_size(config[CONF_MAX_RESPONSE_BUFFER_SIZE])) | ||||
|  | ||||
|     if CONF_BODY in config: | ||||
| @@ -303,21 +299,26 @@ async def http_request_action_to_code(config, action_id, template_arg, args): | ||||
|     for value in config.get(CONF_COLLECT_HEADERS, []): | ||||
|         cg.add(var.add_collect_header(value)) | ||||
|  | ||||
|     for conf in config.get(CONF_ON_RESPONSE, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) | ||||
|         cg.add(var.register_response_trigger(trigger)) | ||||
|         await automation.build_automation( | ||||
|             trigger, | ||||
|             [ | ||||
|                 (cg.std_shared_ptr.template(HttpContainer), "response"), | ||||
|                 (cg.std_string_ref, "body"), | ||||
|             ], | ||||
|             conf, | ||||
|         ) | ||||
|     for conf in config.get(CONF_ON_ERROR, []): | ||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID]) | ||||
|         cg.add(var.register_error_trigger(trigger)) | ||||
|         await automation.build_automation(trigger, [], conf) | ||||
|     if response_conf := config.get(CONF_ON_RESPONSE): | ||||
|         if capture_response: | ||||
|             await automation.build_automation( | ||||
|                 var.get_success_trigger_with_response(), | ||||
|                 [ | ||||
|                     (cg.std_shared_ptr.template(HttpContainer), "response"), | ||||
|                     (cg.std_string_ref, "body"), | ||||
|                     *args, | ||||
|                 ], | ||||
|                 response_conf, | ||||
|             ) | ||||
|         else: | ||||
|             await automation.build_automation( | ||||
|                 var.get_success_trigger(), | ||||
|                 [(cg.std_shared_ptr.template(HttpContainer), "response"), *args], | ||||
|                 response_conf, | ||||
|             ) | ||||
|  | ||||
|     if error_conf := config.get(CONF_ON_ERROR): | ||||
|         await automation.build_automation(var.get_error_trigger(), args, error_conf) | ||||
|  | ||||
|     return var | ||||
|  | ||||
|   | ||||
| @@ -183,7 +183,9 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> { | ||||
|   TEMPLATABLE_VALUE(std::string, url) | ||||
|   TEMPLATABLE_VALUE(const char *, method) | ||||
|   TEMPLATABLE_VALUE(std::string, body) | ||||
| #ifdef USE_HTTP_REQUEST_RESPONSE | ||||
|   TEMPLATABLE_VALUE(bool, capture_response) | ||||
| #endif | ||||
|  | ||||
|   void add_request_header(const char *key, TemplatableValue<const char *, Ts...> value) { | ||||
|     this->request_headers_.insert({key, value}); | ||||
| @@ -195,9 +197,14 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> { | ||||
|  | ||||
|   void set_json(std::function<void(Ts..., JsonObject)> json_func) { this->json_func_ = json_func; } | ||||
|  | ||||
|   void register_response_trigger(HttpRequestResponseTrigger *trigger) { this->response_triggers_.push_back(trigger); } | ||||
| #ifdef USE_HTTP_REQUEST_RESPONSE | ||||
|   Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...> *get_success_trigger_with_response() const { | ||||
|     return this->success_trigger_with_response_; | ||||
|   } | ||||
| #endif | ||||
|   Trigger<std::shared_ptr<HttpContainer>, Ts...> *get_success_trigger() const { return this->success_trigger_; } | ||||
|  | ||||
|   void register_error_trigger(Trigger<> *trigger) { this->error_triggers_.push_back(trigger); } | ||||
|   Trigger<Ts...> *get_error_trigger() const { return this->error_trigger_; } | ||||
|  | ||||
|   void set_max_response_buffer_size(size_t max_response_buffer_size) { | ||||
|     this->max_response_buffer_size_ = max_response_buffer_size; | ||||
| @@ -228,17 +235,20 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> { | ||||
|     auto container = this->parent_->start(this->url_.value(x...), this->method_.value(x...), body, request_headers, | ||||
|                                           this->collect_headers_); | ||||
|  | ||||
|     auto captured_args = std::make_tuple(x...); | ||||
|  | ||||
|     if (container == nullptr) { | ||||
|       for (auto *trigger : this->error_triggers_) | ||||
|         trigger->trigger(); | ||||
|       std::apply([this](Ts... captured_args_inner) { this->error_trigger_->trigger(captured_args_inner...); }, | ||||
|                  captured_args); | ||||
|       return; | ||||
|     } | ||||
|  | ||||
|     size_t content_length = container->content_length; | ||||
|     size_t max_length = std::min(content_length, this->max_response_buffer_size_); | ||||
|  | ||||
|     std::string response_body; | ||||
| #ifdef USE_HTTP_REQUEST_RESPONSE | ||||
|     if (this->capture_response_.value(x...)) { | ||||
|       std::string response_body; | ||||
|       RAMAllocator<uint8_t> allocator; | ||||
|       uint8_t *buf = allocator.allocate(max_length); | ||||
|       if (buf != nullptr) { | ||||
| @@ -253,19 +263,17 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> { | ||||
|         response_body.assign((char *) buf, read_index); | ||||
|         allocator.deallocate(buf, max_length); | ||||
|       } | ||||
|     } | ||||
|  | ||||
|     if (this->response_triggers_.size() == 1) { | ||||
|       // if there is only one trigger, no need to copy the response body | ||||
|       this->response_triggers_[0]->process(container, response_body); | ||||
|     } else { | ||||
|       for (auto *trigger : this->response_triggers_) { | ||||
|         // with multiple triggers, pass a copy of the response body to each | ||||
|         // one so that modifications made in one trigger are not visible to | ||||
|         // the others | ||||
|         auto response_body_copy = std::string(response_body); | ||||
|         trigger->process(container, response_body_copy); | ||||
|       } | ||||
|       std::apply( | ||||
|           [this, &container, &response_body](Ts... captured_args_inner) { | ||||
|             this->success_trigger_with_response_->trigger(container, response_body, captured_args_inner...); | ||||
|           }, | ||||
|           captured_args); | ||||
|     } else | ||||
| #endif | ||||
|     { | ||||
|       std::apply([this, &container]( | ||||
|                      Ts... captured_args_inner) { this->success_trigger_->trigger(container, captured_args_inner...); }, | ||||
|                  captured_args); | ||||
|     } | ||||
|     container->end(); | ||||
|   } | ||||
| @@ -283,8 +291,13 @@ template<typename... Ts> class HttpRequestSendAction : public Action<Ts...> { | ||||
|   std::set<std::string> collect_headers_{"content-type", "content-length"}; | ||||
|   std::map<const char *, TemplatableValue<std::string, Ts...>> json_{}; | ||||
|   std::function<void(Ts..., JsonObject)> json_func_{nullptr}; | ||||
|   std::vector<HttpRequestResponseTrigger *> response_triggers_{}; | ||||
|   std::vector<Trigger<> *> error_triggers_{}; | ||||
| #ifdef USE_HTTP_REQUEST_RESPONSE | ||||
|   Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...> *success_trigger_with_response_ = | ||||
|       new Trigger<std::shared_ptr<HttpContainer>, std::string &, Ts...>(); | ||||
| #endif | ||||
|   Trigger<std::shared_ptr<HttpContainer>, Ts...> *success_trigger_ = | ||||
|       new Trigger<std::shared_ptr<HttpContainer>, Ts...>(); | ||||
|   Trigger<Ts...> *error_trigger_ = new Trigger<Ts...>(); | ||||
|  | ||||
|   size_t max_response_buffer_size_{SIZE_MAX}; | ||||
| }; | ||||
|   | ||||
| @@ -125,7 +125,7 @@ lv_img_dsc_t *Image::get_lv_img_dsc() { | ||||
|  | ||||
|       case IMAGE_TYPE_RGB: | ||||
| #if LV_COLOR_DEPTH == 32 | ||||
|         switch (this->transparent_) { | ||||
|         switch (this->transparency_) { | ||||
|           case TRANSPARENCY_ALPHA_CHANNEL: | ||||
|             this->dsc_.header.cf = LV_IMG_CF_TRUE_COLOR_ALPHA; | ||||
|             break; | ||||
| @@ -156,7 +156,8 @@ lv_img_dsc_t *Image::get_lv_img_dsc() { | ||||
|             break; | ||||
|         } | ||||
| #else | ||||
|         this->dsc_.header.cf = this->transparent_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565; | ||||
|         this->dsc_.header.cf = | ||||
|             this->transparency_ == TRANSPARENCY_ALPHA_CHANNEL ? LV_IMG_CF_RGB565A8 : LV_IMG_CF_RGB565; | ||||
| #endif | ||||
|         break; | ||||
|     } | ||||
|   | ||||
| @@ -121,9 +121,9 @@ constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = { | ||||
| }; | ||||
|  | ||||
| // Helper functions for lookups | ||||
| template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { | ||||
| template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const char *str) { | ||||
|   for (const auto &entry : arr) { | ||||
|     if (str == entry.str) | ||||
|     if (strcmp(str, entry.str) == 0) | ||||
|       return entry.value; | ||||
|   } | ||||
|   return 0xFF;  // Not found | ||||
| @@ -441,7 +441,7 @@ bool LD2410Component::handle_ack_data_() { | ||||
|       ESP_LOGV(TAG, "Baud rate change"); | ||||
| #ifdef USE_SELECT | ||||
|       if (this->baud_rate_select_ != nullptr) { | ||||
|         ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); | ||||
|         ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option()); | ||||
|       } | ||||
| #endif | ||||
|       break; | ||||
| @@ -626,14 +626,14 @@ void LD2410Component::set_bluetooth(bool enable) { | ||||
|   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); | ||||
| } | ||||
|  | ||||
| void LD2410Component::set_distance_resolution(const std::string &state) { | ||||
| void LD2410Component::set_distance_resolution(const char *state) { | ||||
|   this->set_config_mode_(true); | ||||
|   const uint8_t cmd_value[2] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00}; | ||||
|   this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value)); | ||||
|   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); | ||||
| } | ||||
|  | ||||
| void LD2410Component::set_baud_rate(const std::string &state) { | ||||
| void LD2410Component::set_baud_rate(const char *state) { | ||||
|   this->set_config_mode_(true); | ||||
|   const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; | ||||
|   this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); | ||||
| @@ -759,10 +759,10 @@ void LD2410Component::set_light_out_control() { | ||||
| #endif | ||||
| #ifdef USE_SELECT | ||||
|   if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) { | ||||
|     this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->state); | ||||
|     this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option()); | ||||
|   } | ||||
|   if (this->out_pin_level_select_ != nullptr && this->out_pin_level_select_->has_state()) { | ||||
|     this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->state); | ||||
|     this->out_pin_level_ = find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option()); | ||||
|   } | ||||
| #endif | ||||
|   this->set_config_mode_(true); | ||||
|   | ||||
| @@ -98,8 +98,8 @@ class LD2410Component : public Component, public uart::UARTDevice { | ||||
|   void read_all_info(); | ||||
|   void restart_and_read_all_info(); | ||||
|   void set_bluetooth(bool enable); | ||||
|   void set_distance_resolution(const std::string &state); | ||||
|   void set_baud_rate(const std::string &state); | ||||
|   void set_distance_resolution(const char *state); | ||||
|   void set_baud_rate(const char *state); | ||||
|   void factory_reset(); | ||||
|  | ||||
|  protected: | ||||
|   | ||||
| @@ -3,9 +3,9 @@ | ||||
| namespace esphome { | ||||
| namespace ld2410 { | ||||
|  | ||||
| void BaudRateSelect::control(const std::string &value) { | ||||
|   this->publish_state(value); | ||||
|   this->parent_->set_baud_rate(state); | ||||
| void BaudRateSelect::control(size_t index) { | ||||
|   this->publish_state(index); | ||||
|   this->parent_->set_baud_rate(this->option_at(index)); | ||||
| } | ||||
|  | ||||
| }  // namespace ld2410 | ||||
|   | ||||
| @@ -11,7 +11,7 @@ class BaudRateSelect : public select::Select, public Parented<LD2410Component> { | ||||
|   BaudRateSelect() = default; | ||||
|  | ||||
|  protected: | ||||
|   void control(const std::string &value) override; | ||||
|   void control(size_t index) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace ld2410 | ||||
|   | ||||
| @@ -3,9 +3,9 @@ | ||||
| namespace esphome { | ||||
| namespace ld2410 { | ||||
|  | ||||
| void DistanceResolutionSelect::control(const std::string &value) { | ||||
|   this->publish_state(value); | ||||
|   this->parent_->set_distance_resolution(state); | ||||
| void DistanceResolutionSelect::control(size_t index) { | ||||
|   this->publish_state(index); | ||||
|   this->parent_->set_distance_resolution(this->option_at(index)); | ||||
| } | ||||
|  | ||||
| }  // namespace ld2410 | ||||
|   | ||||
| @@ -11,7 +11,7 @@ class DistanceResolutionSelect : public select::Select, public Parented<LD2410Co | ||||
|   DistanceResolutionSelect() = default; | ||||
|  | ||||
|  protected: | ||||
|   void control(const std::string &value) override; | ||||
|   void control(size_t index) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace ld2410 | ||||
|   | ||||
| @@ -3,8 +3,8 @@ | ||||
| namespace esphome { | ||||
| namespace ld2410 { | ||||
|  | ||||
| void LightOutControlSelect::control(const std::string &value) { | ||||
|   this->publish_state(value); | ||||
| void LightOutControlSelect::control(size_t index) { | ||||
|   this->publish_state(index); | ||||
|   this->parent_->set_light_out_control(); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -11,7 +11,7 @@ class LightOutControlSelect : public select::Select, public Parented<LD2410Compo | ||||
|   LightOutControlSelect() = default; | ||||
|  | ||||
|  protected: | ||||
|   void control(const std::string &value) override; | ||||
|   void control(size_t index) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace ld2410 | ||||
|   | ||||
| @@ -132,9 +132,9 @@ constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = { | ||||
| }; | ||||
|  | ||||
| // Helper functions for lookups | ||||
| template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) { | ||||
| template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const char *str) { | ||||
|   for (const auto &entry : arr) { | ||||
|     if (str == entry.str) { | ||||
|     if (strcmp(str, entry.str) == 0) { | ||||
|       return entry.value; | ||||
|     } | ||||
|   } | ||||
| @@ -485,7 +485,7 @@ bool LD2412Component::handle_ack_data_() { | ||||
|       ESP_LOGV(TAG, "Baud rate change"); | ||||
| #ifdef USE_SELECT | ||||
|       if (this->baud_rate_select_ != nullptr) { | ||||
|         ESP_LOGW(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); | ||||
|         ESP_LOGW(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option()); | ||||
|       } | ||||
| #endif | ||||
|       break; | ||||
| @@ -699,14 +699,14 @@ void LD2412Component::set_bluetooth(bool enable) { | ||||
|   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); | ||||
| } | ||||
|  | ||||
| void LD2412Component::set_distance_resolution(const std::string &state) { | ||||
| void LD2412Component::set_distance_resolution(const char *state) { | ||||
|   this->set_config_mode_(true); | ||||
|   const uint8_t cmd_value[6] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00, 0x00, 0x00, 0x00, 0x00}; | ||||
|   this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value)); | ||||
|   this->set_timeout(200, [this]() { this->restart_and_read_all_info(); }); | ||||
| } | ||||
|  | ||||
| void LD2412Component::set_baud_rate(const std::string &state) { | ||||
| void LD2412Component::set_baud_rate(const char *state) { | ||||
|   this->set_config_mode_(true); | ||||
|   const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; | ||||
|   this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); | ||||
| @@ -783,7 +783,7 @@ void LD2412Component::set_basic_config() { | ||||
|       1,    TOTAL_GATES, DEFAULT_PRESENCE_TIMEOUT, 0, | ||||
| #endif | ||||
| #ifdef USE_SELECT | ||||
|       find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->state), | ||||
|       find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->current_option()), | ||||
| #else | ||||
|       0x01,  // Default value if not using select | ||||
| #endif | ||||
| @@ -837,7 +837,7 @@ void LD2412Component::set_light_out_control() { | ||||
| #endif | ||||
| #ifdef USE_SELECT | ||||
|   if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) { | ||||
|     this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->state); | ||||
|     this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->current_option()); | ||||
|   } | ||||
| #endif | ||||
|   uint8_t value[2] = {this->light_function_, this->light_threshold_}; | ||||
|   | ||||
| @@ -99,8 +99,8 @@ class LD2412Component : public Component, public uart::UARTDevice { | ||||
|   void read_all_info(); | ||||
|   void restart_and_read_all_info(); | ||||
|   void set_bluetooth(bool enable); | ||||
|   void set_distance_resolution(const std::string &state); | ||||
|   void set_baud_rate(const std::string &state); | ||||
|   void set_distance_resolution(const char *state); | ||||
|   void set_baud_rate(const char *state); | ||||
|   void factory_reset(); | ||||
|   void start_dynamic_background_correction(); | ||||
|  | ||||
|   | ||||
| @@ -3,9 +3,9 @@ | ||||
| namespace esphome { | ||||
| namespace ld2412 { | ||||
|  | ||||
| void BaudRateSelect::control(const std::string &value) { | ||||
|   this->publish_state(value); | ||||
|   this->parent_->set_baud_rate(state); | ||||
| void BaudRateSelect::control(size_t index) { | ||||
|   this->publish_state(index); | ||||
|   this->parent_->set_baud_rate(this->option_at(index)); | ||||
| } | ||||
|  | ||||
| }  // namespace ld2412 | ||||
|   | ||||
| @@ -11,7 +11,7 @@ class BaudRateSelect : public select::Select, public Parented<LD2412Component> { | ||||
|   BaudRateSelect() = default; | ||||
|  | ||||
|  protected: | ||||
|   void control(const std::string &value) override; | ||||
|   void control(size_t index) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace ld2412 | ||||
|   | ||||
| @@ -3,9 +3,9 @@ | ||||
| namespace esphome { | ||||
| namespace ld2412 { | ||||
|  | ||||
| void DistanceResolutionSelect::control(const std::string &value) { | ||||
|   this->publish_state(value); | ||||
|   this->parent_->set_distance_resolution(state); | ||||
| void DistanceResolutionSelect::control(size_t index) { | ||||
|   this->publish_state(index); | ||||
|   this->parent_->set_distance_resolution(this->option_at(index)); | ||||
| } | ||||
|  | ||||
| }  // namespace ld2412 | ||||
|   | ||||
| @@ -11,7 +11,7 @@ class DistanceResolutionSelect : public select::Select, public Parented<LD2412Co | ||||
|   DistanceResolutionSelect() = default; | ||||
|  | ||||
|  protected: | ||||
|   void control(const std::string &value) override; | ||||
|   void control(size_t index) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace ld2412 | ||||
|   | ||||
| @@ -3,8 +3,8 @@ | ||||
| namespace esphome { | ||||
| namespace ld2412 { | ||||
|  | ||||
| void LightOutControlSelect::control(const std::string &value) { | ||||
|   this->publish_state(value); | ||||
| void LightOutControlSelect::control(size_t index) { | ||||
|   this->publish_state(index); | ||||
|   this->parent_->set_light_out_control(); | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -11,7 +11,7 @@ class LightOutControlSelect : public select::Select, public Parented<LD2412Compo | ||||
|   LightOutControlSelect() = default; | ||||
|  | ||||
|  protected: | ||||
|   void control(const std::string &value) override; | ||||
|   void control(size_t index) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace ld2412 | ||||
|   | ||||
| @@ -131,8 +131,8 @@ static const uint8_t CMD_FRAME_STATUS = 7; | ||||
| static const uint8_t CMD_ERROR_WORD = 8; | ||||
| static const uint8_t ENERGY_SENSOR_START = 9; | ||||
| static const uint8_t CALIBRATE_REPORT_INTERVAL = 4; | ||||
| static const std::string OP_NORMAL_MODE_STRING = "Normal"; | ||||
| static const std::string OP_SIMPLE_MODE_STRING = "Simple"; | ||||
| static const char *const OP_NORMAL_MODE_STRING = "Normal"; | ||||
| static const char *const OP_SIMPLE_MODE_STRING = "Simple"; | ||||
|  | ||||
| // Memory-efficient lookup tables | ||||
| struct StringToUint8 { | ||||
| @@ -379,7 +379,7 @@ void LD2420Component::report_gate_data() { | ||||
|   ESP_LOGI(TAG, "Total samples: %d", this->total_sample_number_counter); | ||||
| } | ||||
|  | ||||
| void LD2420Component::set_operating_mode(const std::string &state) { | ||||
| void LD2420Component::set_operating_mode(const char *state) { | ||||
|   // If unsupported firmware ignore mode select | ||||
|   if (ld2420::get_firmware_int(firmware_ver_) >= CALIBRATE_VERSION_MIN) { | ||||
|     this->current_operating_mode = find_uint8(OP_MODE_BY_STR, state); | ||||
|   | ||||
| @@ -107,7 +107,7 @@ class LD2420Component : public Component, public uart::UARTDevice { | ||||
|   int send_cmd_from_array(CmdFrameT cmd_frame); | ||||
|   void report_gate_data(); | ||||
|   void handle_cmd_error(uint8_t error); | ||||
|   void set_operating_mode(const std::string &state); | ||||
|   void set_operating_mode(const char *state); | ||||
|   void auto_calibrate_sensitivity(); | ||||
|   void update_radar_data(uint16_t const *gate_energy, uint8_t sample_number); | ||||
|   uint8_t set_config_mode(bool enable); | ||||
|   | ||||
| @@ -7,9 +7,9 @@ namespace ld2420 { | ||||
|  | ||||
| static const char *const TAG = "ld2420.select"; | ||||
|  | ||||
| void LD2420Select::control(const std::string &value) { | ||||
|   this->publish_state(value); | ||||
|   this->parent_->set_operating_mode(value); | ||||
| void LD2420Select::control(size_t index) { | ||||
|   this->publish_state(index); | ||||
|   this->parent_->set_operating_mode(this->option_at(index)); | ||||
| } | ||||
|  | ||||
| }  // namespace ld2420 | ||||
|   | ||||
| @@ -11,7 +11,7 @@ class LD2420Select : public Component, public select::Select, public Parented<LD | ||||
|   LD2420Select() = default; | ||||
|  | ||||
|  protected: | ||||
|   void control(const std::string &value) override; | ||||
|   void control(size_t index) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace ld2420 | ||||
|   | ||||
| @@ -380,7 +380,7 @@ void LD2450Component::read_all_info() { | ||||
|   this->set_config_mode_(false); | ||||
| #ifdef USE_SELECT | ||||
|   const auto baud_rate = std::to_string(this->parent_->get_baud_rate()); | ||||
|   if (this->baud_rate_select_ != nullptr && this->baud_rate_select_->state != baud_rate) { | ||||
|   if (this->baud_rate_select_ != nullptr && strcmp(this->baud_rate_select_->current_option(), baud_rate.c_str()) != 0) { | ||||
|     this->baud_rate_select_->publish_state(baud_rate); | ||||
|   } | ||||
|   this->publish_zone_type(); | ||||
| @@ -635,7 +635,7 @@ bool LD2450Component::handle_ack_data_() { | ||||
|       ESP_LOGV(TAG, "Baud rate change"); | ||||
| #ifdef USE_SELECT | ||||
|       if (this->baud_rate_select_ != nullptr) { | ||||
|         ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str()); | ||||
|         ESP_LOGE(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->current_option()); | ||||
|       } | ||||
| #endif | ||||
|       break; | ||||
| @@ -716,7 +716,7 @@ bool LD2450Component::handle_ack_data_() { | ||||
|       this->publish_zone_type(); | ||||
| #ifdef USE_SELECT | ||||
|       if (this->zone_type_select_ != nullptr) { | ||||
|         ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->state.c_str()); | ||||
|         ESP_LOGV(TAG, "Change zone type to: %s", this->zone_type_select_->current_option()); | ||||
|       } | ||||
| #endif | ||||
|       if (this->buffer_data_[10] == 0x00) { | ||||
| @@ -790,7 +790,7 @@ void LD2450Component::set_bluetooth(bool enable) { | ||||
| } | ||||
|  | ||||
| // Set Baud rate | ||||
| void LD2450Component::set_baud_rate(const std::string &state) { | ||||
| void LD2450Component::set_baud_rate(const char *state) { | ||||
|   this->set_config_mode_(true); | ||||
|   const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00}; | ||||
|   this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value)); | ||||
| @@ -798,8 +798,8 @@ void LD2450Component::set_baud_rate(const std::string &state) { | ||||
| } | ||||
|  | ||||
| // Set Zone Type - one of: Disabled, Detection, Filter | ||||
| void LD2450Component::set_zone_type(const std::string &state) { | ||||
|   ESP_LOGV(TAG, "Set zone type: %s", state.c_str()); | ||||
| void LD2450Component::set_zone_type(const char *state) { | ||||
|   ESP_LOGV(TAG, "Set zone type: %s", state); | ||||
|   uint8_t zone_type = find_uint8(ZONE_TYPE_BY_STR, state); | ||||
|   this->zone_type_ = zone_type; | ||||
|   this->send_set_zone_command_(); | ||||
|   | ||||
| @@ -115,8 +115,8 @@ class LD2450Component : public Component, public uart::UARTDevice { | ||||
|   void restart_and_read_all_info(); | ||||
|   void set_bluetooth(bool enable); | ||||
|   void set_multi_target(bool enable); | ||||
|   void set_baud_rate(const std::string &state); | ||||
|   void set_zone_type(const std::string &state); | ||||
|   void set_baud_rate(const char *state); | ||||
|   void set_zone_type(const char *state); | ||||
|   void publish_zone_type(); | ||||
|   void factory_reset(); | ||||
| #ifdef USE_TEXT_SENSOR | ||||
|   | ||||
| @@ -3,9 +3,9 @@ | ||||
| namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| void BaudRateSelect::control(const std::string &value) { | ||||
|   this->publish_state(value); | ||||
|   this->parent_->set_baud_rate(state); | ||||
| void BaudRateSelect::control(size_t index) { | ||||
|   this->publish_state(index); | ||||
|   this->parent_->set_baud_rate(this->option_at(index)); | ||||
| } | ||||
|  | ||||
| }  // namespace ld2450 | ||||
|   | ||||
| @@ -11,7 +11,7 @@ class BaudRateSelect : public select::Select, public Parented<LD2450Component> { | ||||
|   BaudRateSelect() = default; | ||||
|  | ||||
|  protected: | ||||
|   void control(const std::string &value) override; | ||||
|   void control(size_t index) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace ld2450 | ||||
|   | ||||
| @@ -3,9 +3,9 @@ | ||||
| namespace esphome { | ||||
| namespace ld2450 { | ||||
|  | ||||
| void ZoneTypeSelect::control(const std::string &value) { | ||||
|   this->publish_state(value); | ||||
|   this->parent_->set_zone_type(state); | ||||
| void ZoneTypeSelect::control(size_t index) { | ||||
|   this->publish_state(index); | ||||
|   this->parent_->set_zone_type(this->option_at(index)); | ||||
| } | ||||
|  | ||||
| }  // namespace ld2450 | ||||
|   | ||||
| @@ -11,7 +11,7 @@ class ZoneTypeSelect : public select::Select, public Parented<LD2450Component> { | ||||
|   ZoneTypeSelect() = default; | ||||
|  | ||||
|  protected: | ||||
|   void control(const std::string &value) override; | ||||
|   void control(size_t index) override; | ||||
| }; | ||||
|  | ||||
| }  // namespace ld2450 | ||||
|   | ||||
| @@ -91,11 +91,6 @@ def lock_schema( | ||||
|     return _LOCK_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| LOCK_SCHEMA = lock_schema() | ||||
| LOCK_SCHEMA.add_extra(cv.deprecated_schema_constant("lock")) | ||||
|  | ||||
|  | ||||
| async def _setup_lock_core(var, config): | ||||
|     await setup_entity(var, config, "lock") | ||||
|  | ||||
|   | ||||
| @@ -173,14 +173,34 @@ def uart_selection(value): | ||||
|     raise NotImplementedError | ||||
|  | ||||
|  | ||||
| def validate_local_no_higher_than_global(value): | ||||
|     global_level = LOG_LEVEL_SEVERITY.index(value[CONF_LEVEL]) | ||||
|     for tag, level in value.get(CONF_LOGS, {}).items(): | ||||
|         if LOG_LEVEL_SEVERITY.index(level) > global_level: | ||||
|             raise cv.Invalid( | ||||
|                 f"The configured log level for {tag} ({level}) must be no more severe than the global log level {value[CONF_LEVEL]}." | ||||
| def validate_local_no_higher_than_global(config): | ||||
|     global_level = config[CONF_LEVEL] | ||||
|     global_level_index = LOG_LEVEL_SEVERITY.index(global_level) | ||||
|     errs = [] | ||||
|     for tag, level in config.get(CONF_LOGS, {}).items(): | ||||
|         if LOG_LEVEL_SEVERITY.index(level) > global_level_index: | ||||
|             errs.append( | ||||
|                 cv.Invalid( | ||||
|                     f"The configured log level for {tag} ({level}) must not be less severe than the global log level ({global_level})", | ||||
|                     [CONF_LOGS, tag], | ||||
|                 ) | ||||
|             ) | ||||
|     return value | ||||
|     if errs: | ||||
|         raise cv.MultipleInvalid(errs) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| def validate_initial_no_higher_than_global(config): | ||||
|     if initial_level := config.get(CONF_INITIAL_LEVEL): | ||||
|         global_level = config[CONF_LEVEL] | ||||
|         if LOG_LEVEL_SEVERITY.index(initial_level) > LOG_LEVEL_SEVERITY.index( | ||||
|             global_level | ||||
|         ): | ||||
|             raise cv.Invalid( | ||||
|                 f"The initial log level ({initial_level}) must not be less severe than the global log level ({global_level})", | ||||
|                 [CONF_INITIAL_LEVEL], | ||||
|             ) | ||||
|     return config | ||||
|  | ||||
|  | ||||
| Logger = logger_ns.class_("Logger", cg.Component) | ||||
| @@ -263,6 +283,7 @@ CONFIG_SCHEMA = cv.All( | ||||
|         } | ||||
|     ).extend(cv.COMPONENT_SCHEMA), | ||||
|     validate_local_no_higher_than_global, | ||||
|     validate_initial_no_higher_than_global, | ||||
| ) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -3,10 +3,10 @@ | ||||
| namespace esphome::logger { | ||||
|  | ||||
| void LoggerLevelSelect::publish_state(int level) { | ||||
|   const auto &option = this->at(level_to_index(level)); | ||||
|   if (!option) | ||||
|   auto index = level_to_index(level); | ||||
|   if (!this->has_index(index)) | ||||
|     return; | ||||
|   Select::publish_state(option.value()); | ||||
|   Select::publish_state(index); | ||||
| } | ||||
|  | ||||
| void LoggerLevelSelect::setup() { | ||||
| @@ -14,11 +14,6 @@ void LoggerLevelSelect::setup() { | ||||
|   this->publish_state(this->parent_->get_log_level()); | ||||
| } | ||||
|  | ||||
| void LoggerLevelSelect::control(const std::string &value) { | ||||
|   const auto index = this->index_of(value); | ||||
|   if (!index) | ||||
|     return; | ||||
|   this->parent_->set_log_level(index_to_level(index.value())); | ||||
| } | ||||
| void LoggerLevelSelect::control(size_t index) { this->parent_->set_log_level(index_to_level(index)); } | ||||
|  | ||||
| }  // namespace esphome::logger | ||||
|   | ||||
| @@ -9,7 +9,7 @@ class LoggerLevelSelect : public Component, public select::Select, public Parent | ||||
|  public: | ||||
|   void publish_state(int level); | ||||
|   void setup() override; | ||||
|   void control(const std::string &value) override; | ||||
|   void control(size_t index) override; | ||||
|  | ||||
|  protected: | ||||
|   // Convert log level to option index (skip CONFIG at level 4) | ||||
|   | ||||
| @@ -5,6 +5,7 @@ Constants already defined in esphome.const are not duplicated here and must be i | ||||
| """ | ||||
|  | ||||
| import logging | ||||
| from typing import TYPE_CHECKING, Any | ||||
|  | ||||
| from esphome import codegen as cg, config_validation as cv | ||||
| from esphome.const import CONF_ITEMS | ||||
| @@ -12,6 +13,7 @@ from esphome.core import ID, Lambda | ||||
| from esphome.cpp_generator import LambdaExpression, MockObj | ||||
| from esphome.cpp_types import uint32 | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||
| from esphome.types import Expression, SafeExpType | ||||
|  | ||||
| from .helpers import requires_component | ||||
|  | ||||
| @@ -42,7 +44,13 @@ def static_cast(type, value): | ||||
| def call_lambda(lamb: LambdaExpression): | ||||
|     expr = lamb.content.strip() | ||||
|     if expr.startswith("return") and expr.endswith(";"): | ||||
|         return expr[6:][:-1].strip() | ||||
|         return expr[6:-1].strip() | ||||
|     # If lambda has parameters, call it with those parameter names | ||||
|     # Parameter names come from hardcoded component code (like "x", "it", "event") | ||||
|     # not from user input, so they're safe to use directly | ||||
|     if lamb.parameters and lamb.parameters.parameters: | ||||
|         param_names = ", ".join(str(param.id) for param in lamb.parameters.parameters) | ||||
|         return f"{lamb}({param_names})" | ||||
|     return f"{lamb}()" | ||||
|  | ||||
|  | ||||
| @@ -65,10 +73,20 @@ class LValidator: | ||||
|             return cv.returning_lambda(value) | ||||
|         return self.validator(value) | ||||
|  | ||||
|     async def process(self, value, args=()): | ||||
|     async def process( | ||||
|         self, value: Any, args: list[tuple[SafeExpType, str]] | None = None | ||||
|     ) -> Expression: | ||||
|         if value is None: | ||||
|             return None | ||||
|         if isinstance(value, Lambda): | ||||
|             # Local import to avoid circular import | ||||
|             from .lvcode import CodeContext, LambdaContext | ||||
|  | ||||
|             if TYPE_CHECKING: | ||||
|                 # CodeContext does not have get_automation_parameters | ||||
|                 # so we need to assert the type here | ||||
|                 assert isinstance(CodeContext.code_context, LambdaContext) | ||||
|             args = args or CodeContext.code_context.get_automation_parameters() | ||||
|             return cg.RawExpression( | ||||
|                 call_lambda( | ||||
|                     await cg.process_lambda(value, args, return_type=self.rtype) | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| from typing import TYPE_CHECKING, Any | ||||
|  | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import image | ||||
| from esphome.components.color import CONF_HEX, ColorStruct, from_rgbw | ||||
| @@ -17,6 +19,7 @@ from esphome.cpp_generator import MockObj | ||||
| from esphome.cpp_types import ESPTime, int32, uint32 | ||||
| from esphome.helpers import cpp_string_escape | ||||
| from esphome.schema_extractors import SCHEMA_EXTRACT, schema_extractor | ||||
| from esphome.types import Expression, SafeExpType | ||||
|  | ||||
| from . import types as ty | ||||
| from .defines import ( | ||||
| @@ -388,11 +391,23 @@ class TextValidator(LValidator): | ||||
|             return value | ||||
|         return super().__call__(value) | ||||
|  | ||||
|     async def process(self, value, args=()): | ||||
|     async def process( | ||||
|         self, value: Any, args: list[tuple[SafeExpType, str]] | None = None | ||||
|     ) -> Expression: | ||||
|         # Local import to avoid circular import at module level | ||||
|  | ||||
|         from .lvcode import CodeContext, LambdaContext | ||||
|  | ||||
|         if TYPE_CHECKING: | ||||
|             # CodeContext does not have get_automation_parameters | ||||
|             # so we need to assert the type here | ||||
|             assert isinstance(CodeContext.code_context, LambdaContext) | ||||
|         args = args or CodeContext.code_context.get_automation_parameters() | ||||
|  | ||||
|         if isinstance(value, dict): | ||||
|             if format_str := value.get(CONF_FORMAT): | ||||
|                 args = [str(x) for x in value[CONF_ARGS]] | ||||
|                 arg_expr = cg.RawExpression(",".join(args)) | ||||
|                 str_args = [str(x) for x in value[CONF_ARGS]] | ||||
|                 arg_expr = cg.RawExpression(",".join(str_args)) | ||||
|                 format_str = cpp_string_escape(format_str) | ||||
|                 return literal(f"str_sprintf({format_str}, {arg_expr}).c_str()") | ||||
|             if time_format := value.get(CONF_TIME_FORMAT): | ||||
|   | ||||
| @@ -164,6 +164,9 @@ class LambdaContext(CodeContext): | ||||
|             code_text.append(text) | ||||
|         return code_text | ||||
|  | ||||
|     def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]: | ||||
|         return self.parameters | ||||
|  | ||||
|     async def __aenter__(self): | ||||
|         await super().__aenter__() | ||||
|         add_line_marks(self.where) | ||||
| @@ -178,9 +181,8 @@ class LvContext(LambdaContext): | ||||
|  | ||||
|     added_lambda_count = 0 | ||||
|  | ||||
|     def __init__(self, args=None): | ||||
|         self.args = args or LVGL_COMP_ARG | ||||
|         super().__init__(parameters=self.args) | ||||
|     def __init__(self): | ||||
|         super().__init__(parameters=LVGL_COMP_ARG) | ||||
|  | ||||
|     async def __aexit__(self, exc_type, exc_val, exc_tb): | ||||
|         await super().__aexit__(exc_type, exc_val, exc_tb) | ||||
| @@ -189,6 +191,11 @@ class LvContext(LambdaContext): | ||||
|         cg.add(expression) | ||||
|         return expression | ||||
|  | ||||
|     def get_automation_parameters(self) -> list[tuple[SafeExpType, str]]: | ||||
|         # When generating automations, we don't want the `lv_component` parameter to be passed | ||||
|         # to the lambda. | ||||
|         return [] | ||||
|  | ||||
|     def __call__(self, *args): | ||||
|         return self.add(*args) | ||||
|  | ||||
|   | ||||
| @@ -5,7 +5,6 @@ from ..defines import CONF_WIDGET | ||||
| from ..lvcode import ( | ||||
|     API_EVENT, | ||||
|     EVENT_ARG, | ||||
|     LVGL_COMP_ARG, | ||||
|     UPDATE_EVENT, | ||||
|     LambdaContext, | ||||
|     LvContext, | ||||
| @@ -30,7 +29,7 @@ async def to_code(config): | ||||
|     await wait_for_widgets() | ||||
|     async with LambdaContext(EVENT_ARG) as lamb: | ||||
|         lv_add(sensor.publish_state(widget.get_value())) | ||||
|     async with LvContext(LVGL_COMP_ARG): | ||||
|     async with LvContext(): | ||||
|         lv_add( | ||||
|             lvgl_static.add_event_cb( | ||||
|                 widget.obj, | ||||
|   | ||||
| @@ -33,7 +33,7 @@ from ..lv_validation import ( | ||||
|     pixels, | ||||
|     size, | ||||
| ) | ||||
| from ..lvcode import LocalVariable, lv, lv_assign | ||||
| from ..lvcode import LocalVariable, lv, lv_assign, lv_expr | ||||
| from ..schemas import STYLE_PROPS, STYLE_REMAP, TEXT_SCHEMA, point_schema | ||||
| from ..types import LvType, ObjUpdateAction, WidgetType | ||||
| from . import Widget, get_widgets | ||||
| @@ -70,15 +70,18 @@ class CanvasType(WidgetType): | ||||
|         width = config[CONF_WIDTH] | ||||
|         height = config[CONF_HEIGHT] | ||||
|         use_alpha = "_ALPHA" if config[CONF_TRANSPARENT] else "" | ||||
|         lv.canvas_set_buffer( | ||||
|             w.obj, | ||||
|             lv.custom_mem_alloc( | ||||
|                 literal(f"LV_CANVAS_BUF_SIZE_TRUE_COLOR{use_alpha}({width}, {height})") | ||||
|             ), | ||||
|             width, | ||||
|             height, | ||||
|             literal(f"LV_IMG_CF_TRUE_COLOR{use_alpha}"), | ||||
|         buf_size = literal( | ||||
|             f"LV_CANVAS_BUF_SIZE_TRUE_COLOR{use_alpha}({width}, {height})" | ||||
|         ) | ||||
|         with LocalVariable("buf", cg.void, lv_expr.custom_mem_alloc(buf_size)) as buf: | ||||
|             cg.add(cg.RawExpression(f"memset({buf}, 0, {buf_size});")) | ||||
|             lv.canvas_set_buffer( | ||||
|                 w.obj, | ||||
|                 buf, | ||||
|                 width, | ||||
|                 height, | ||||
|                 literal(f"LV_IMG_CF_TRUE_COLOR{use_alpha}"), | ||||
|             ) | ||||
|  | ||||
|  | ||||
| canvas_spec = CanvasType() | ||||
|   | ||||
| @@ -192,10 +192,6 @@ def media_player_schema( | ||||
|     return _MEDIA_PLAYER_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| MEDIA_PLAYER_SCHEMA = media_player_schema(MediaPlayer) | ||||
| MEDIA_PLAYER_SCHEMA.add_extra(cv.deprecated_schema_constant("media_player")) | ||||
|  | ||||
| MEDIA_PLAYER_ACTION_SCHEMA = automation.maybe_simple_id( | ||||
|     cv.Schema( | ||||
|         { | ||||
|   | ||||
| @@ -8,9 +8,9 @@ namespace midea { | ||||
| namespace ac { | ||||
|  | ||||
| const char *const Constants::TAG = "midea"; | ||||
| const std::string Constants::FREEZE_PROTECTION = "freeze protection"; | ||||
| const std::string Constants::SILENT = "silent"; | ||||
| const std::string Constants::TURBO = "turbo"; | ||||
| const char *const Constants::FREEZE_PROTECTION = "freeze protection"; | ||||
| const char *const Constants::SILENT = "silent"; | ||||
| const char *const Constants::TURBO = "turbo"; | ||||
|  | ||||
| ClimateMode Converters::to_climate_mode(MideaMode mode) { | ||||
|   switch (mode) { | ||||
| @@ -108,7 +108,7 @@ bool Converters::is_custom_midea_fan_mode(MideaFanMode mode) { | ||||
|   } | ||||
| } | ||||
|  | ||||
| const std::string &Converters::to_custom_climate_fan_mode(MideaFanMode mode) { | ||||
| const char *Converters::to_custom_climate_fan_mode(MideaFanMode mode) { | ||||
|   switch (mode) { | ||||
|     case MideaFanMode::FAN_SILENT: | ||||
|       return Constants::SILENT; | ||||
| @@ -151,7 +151,7 @@ ClimatePreset Converters::to_climate_preset(MideaPreset preset) { | ||||
|  | ||||
| bool Converters::is_custom_midea_preset(MideaPreset preset) { return preset == MideaPreset::PRESET_FREEZE_PROTECTION; } | ||||
|  | ||||
| const std::string &Converters::to_custom_climate_preset(MideaPreset preset) { return Constants::FREEZE_PROTECTION; } | ||||
| const char *Converters::to_custom_climate_preset(MideaPreset preset) { return Constants::FREEZE_PROTECTION; } | ||||
|  | ||||
| MideaPreset Converters::to_midea_preset(const std::string &preset) { return MideaPreset::PRESET_FREEZE_PROTECTION; } | ||||
|  | ||||
| @@ -169,7 +169,7 @@ void Converters::to_climate_traits(ClimateTraits &traits, const dudanov::midea:: | ||||
|   if (capabilities.supportEcoPreset()) | ||||
|     traits.add_supported_preset(ClimatePreset::CLIMATE_PRESET_ECO); | ||||
|   if (capabilities.supportFrostProtectionPreset()) | ||||
|     traits.add_supported_custom_preset(Constants::FREEZE_PROTECTION); | ||||
|     traits.set_supported_custom_presets({Constants::FREEZE_PROTECTION}); | ||||
| } | ||||
|  | ||||
| }  // namespace ac | ||||
|   | ||||
| @@ -20,9 +20,9 @@ using MideaPreset = dudanov::midea::ac::Preset; | ||||
| class Constants { | ||||
|  public: | ||||
|   static const char *const TAG; | ||||
|   static const std::string FREEZE_PROTECTION; | ||||
|   static const std::string SILENT; | ||||
|   static const std::string TURBO; | ||||
|   static const char *const FREEZE_PROTECTION; | ||||
|   static const char *const SILENT; | ||||
|   static const char *const TURBO; | ||||
| }; | ||||
|  | ||||
| class Converters { | ||||
| @@ -35,12 +35,12 @@ class Converters { | ||||
|   static MideaPreset to_midea_preset(const std::string &preset); | ||||
|   static bool is_custom_midea_preset(MideaPreset preset); | ||||
|   static ClimatePreset to_climate_preset(MideaPreset preset); | ||||
|   static const std::string &to_custom_climate_preset(MideaPreset preset); | ||||
|   static const char *to_custom_climate_preset(MideaPreset preset); | ||||
|   static MideaFanMode to_midea_fan_mode(ClimateFanMode fan_mode); | ||||
|   static MideaFanMode to_midea_fan_mode(const std::string &fan_mode); | ||||
|   static bool is_custom_midea_fan_mode(MideaFanMode fan_mode); | ||||
|   static ClimateFanMode to_climate_fan_mode(MideaFanMode fan_mode); | ||||
|   static const std::string &to_custom_climate_fan_mode(MideaFanMode fan_mode); | ||||
|   static const char *to_custom_climate_fan_mode(MideaFanMode fan_mode); | ||||
|   static void to_climate_traits(ClimateTraits &traits, const dudanov::midea::ac::Capabilities &capabilities); | ||||
| }; | ||||
|  | ||||
|   | ||||
| @@ -84,8 +84,10 @@ ClimateTraits AirConditioner::traits() { | ||||
|   traits.set_supported_modes(this->supported_modes_); | ||||
|   traits.set_supported_swing_modes(this->supported_swing_modes_); | ||||
|   traits.set_supported_presets(this->supported_presets_); | ||||
|   traits.set_supported_custom_presets(this->supported_custom_presets_); | ||||
|   traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_); | ||||
|   if (!this->supported_custom_presets_.empty()) | ||||
|     traits.set_supported_custom_presets(this->supported_custom_presets_); | ||||
|   if (!this->supported_custom_fan_modes_.empty()) | ||||
|     traits.set_supported_custom_fan_modes(this->supported_custom_fan_modes_); | ||||
|   /* + MINIMAL SET OF CAPABILITIES */ | ||||
|   traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_AUTO); | ||||
|   traits.add_supported_fan_mode(ClimateFanMode::CLIMATE_FAN_LOW); | ||||
|   | ||||
| @@ -46,8 +46,8 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>, | ||||
|   void set_supported_modes(ClimateModeMask modes) { this->supported_modes_ = modes; } | ||||
|   void set_supported_swing_modes(ClimateSwingModeMask modes) { this->supported_swing_modes_ = modes; } | ||||
|   void set_supported_presets(ClimatePresetMask presets) { this->supported_presets_ = presets; } | ||||
|   void set_custom_presets(const std::vector<std::string> &presets) { this->supported_custom_presets_ = presets; } | ||||
|   void set_custom_fan_modes(const std::vector<std::string> &modes) { this->supported_custom_fan_modes_ = modes; } | ||||
|   void set_custom_presets(std::initializer_list<const char *> presets) { this->supported_custom_presets_ = presets; } | ||||
|   void set_custom_fan_modes(std::initializer_list<const char *> modes) { this->supported_custom_fan_modes_ = modes; } | ||||
|  | ||||
|  protected: | ||||
|   void control(const ClimateCall &call) override; | ||||
| @@ -55,8 +55,8 @@ class AirConditioner : public ApplianceBase<dudanov::midea::ac::AirConditioner>, | ||||
|   ClimateModeMask supported_modes_{}; | ||||
|   ClimateSwingModeMask supported_swing_modes_{}; | ||||
|   ClimatePresetMask supported_presets_{}; | ||||
|   std::vector<std::string> supported_custom_presets_{}; | ||||
|   std::vector<std::string> supported_custom_fan_modes_{}; | ||||
|   std::vector<const char *> supported_custom_presets_{}; | ||||
|   std::vector<const char *> supported_custom_fan_modes_{}; | ||||
|   Sensor *outdoor_sensor_{nullptr}; | ||||
|   Sensor *humidity_sensor_{nullptr}; | ||||
|   Sensor *power_sensor_{nullptr}; | ||||
|   | ||||
| @@ -384,6 +384,18 @@ class DriverChip: | ||||
|             transform[CONF_TRANSFORM] = True | ||||
|         return transform | ||||
|  | ||||
|     def swap_xy_schema(self): | ||||
|         uses_swap = self.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED | ||||
|  | ||||
|         def validator(value): | ||||
|             if value: | ||||
|                 raise cv.Invalid("Axis swapping not supported by this model") | ||||
|             return cv.boolean(value) | ||||
|  | ||||
|         if uses_swap: | ||||
|             return {cv.Required(CONF_SWAP_XY): cv.boolean} | ||||
|         return {cv.Optional(CONF_SWAP_XY, default=False): validator} | ||||
|  | ||||
|     def add_madctl(self, sequence: list, config: dict): | ||||
|         # Add the MADCTL command to the sequence based on the configuration. | ||||
|         use_flip = config.get(CONF_USE_AXIS_FLIPS) | ||||
|   | ||||
| @@ -46,6 +46,7 @@ from esphome.const import ( | ||||
|     CONF_DATA_RATE, | ||||
|     CONF_DC_PIN, | ||||
|     CONF_DIMENSIONS, | ||||
|     CONF_DISABLED, | ||||
|     CONF_ENABLE_PIN, | ||||
|     CONF_GREEN, | ||||
|     CONF_HSYNC_PIN, | ||||
| @@ -117,16 +118,16 @@ def data_pin_set(length): | ||||
|  | ||||
| def model_schema(config): | ||||
|     model = MODELS[config[CONF_MODEL].upper()] | ||||
|     if transforms := model.transforms: | ||||
|         transform = cv.Schema({cv.Required(x): cv.boolean for x in transforms}) | ||||
|         for x in (CONF_SWAP_XY, CONF_MIRROR_X, CONF_MIRROR_Y): | ||||
|             if x not in transforms: | ||||
|                 transform = transform.extend( | ||||
|                     {cv.Optional(x): cv.invalid(f"{x} not supported by this model")} | ||||
|                 ) | ||||
|     else: | ||||
|         transform = cv.invalid("This model does not support transforms") | ||||
|  | ||||
|     transform = cv.Any( | ||||
|         cv.Schema( | ||||
|             { | ||||
|                 cv.Required(CONF_MIRROR_X): cv.boolean, | ||||
|                 cv.Required(CONF_MIRROR_Y): cv.boolean, | ||||
|                 **model.swap_xy_schema(), | ||||
|             } | ||||
|         ), | ||||
|         cv.one_of(CONF_DISABLED, lower=True), | ||||
|     ) | ||||
|     # RPI model does not use an init sequence, indicates with empty list | ||||
|     if model.initsequence is None: | ||||
|         # Custom model requires an init sequence | ||||
| @@ -135,12 +136,16 @@ def model_schema(config): | ||||
|     else: | ||||
|         iseqconf = cv.Optional(CONF_INIT_SEQUENCE) | ||||
|         uses_spi = CONF_INIT_SEQUENCE in config or len(model.initsequence) != 0 | ||||
|     swap_xy = config.get(CONF_TRANSFORM, {}).get(CONF_SWAP_XY, False) | ||||
|  | ||||
|     # Dimensions are optional if the model has a default width and the swap_xy transform is not overridden | ||||
|     cv_dimensions = ( | ||||
|         cv.Optional if model.get_default(CONF_WIDTH) and not swap_xy else cv.Required | ||||
|     # Dimensions are optional if the model has a default width and the x-y transform is not overridden | ||||
|     transform_config = config.get(CONF_TRANSFORM, {}) | ||||
|     is_swapped = ( | ||||
|         isinstance(transform_config, dict) | ||||
|         and transform_config.get(CONF_SWAP_XY, False) is True | ||||
|     ) | ||||
|     cv_dimensions = ( | ||||
|         cv.Optional if model.get_default(CONF_WIDTH) and not is_swapped else cv.Required | ||||
|     ) | ||||
|  | ||||
|     pixel_modes = (PIXEL_MODE_16BIT, PIXEL_MODE_18BIT, "16", "18") | ||||
|     schema = display.FULL_DISPLAY_SCHEMA.extend( | ||||
|         { | ||||
| @@ -157,7 +162,7 @@ def model_schema(config): | ||||
|             model.option(CONF_PIXEL_MODE, PIXEL_MODE_16BIT): cv.one_of( | ||||
|                 *pixel_modes, lower=True | ||||
|             ), | ||||
|             model.option(CONF_TRANSFORM, cv.UNDEFINED): transform, | ||||
|             cv.Optional(CONF_TRANSFORM): transform, | ||||
|             cv.Required(CONF_MODEL): cv.one_of(model.name, upper=True), | ||||
|             model.option(CONF_INVERT_COLORS, False): cv.boolean, | ||||
|             model.option(CONF_USE_AXIS_FLIPS, True): cv.boolean, | ||||
| @@ -270,7 +275,6 @@ async def to_code(config): | ||||
|     cg.add(var.set_vsync_front_porch(config[CONF_VSYNC_FRONT_PORCH])) | ||||
|     cg.add(var.set_pclk_inverted(config[CONF_PCLK_INVERTED])) | ||||
|     cg.add(var.set_pclk_frequency(config[CONF_PCLK_FREQUENCY])) | ||||
|     index = 0 | ||||
|     dpins = [] | ||||
|     if CONF_RED in config[CONF_DATA_PINS]: | ||||
|         red_pins = config[CONF_DATA_PINS][CONF_RED] | ||||
|   | ||||
| @@ -131,19 +131,6 @@ def denominator(config): | ||||
|         ) from StopIteration | ||||
|  | ||||
|  | ||||
| def swap_xy_schema(model): | ||||
|     uses_swap = model.get_default(CONF_SWAP_XY, None) != cv.UNDEFINED | ||||
|  | ||||
|     def validator(value): | ||||
|         if value: | ||||
|             raise cv.Invalid("Axis swapping not supported by this model") | ||||
|         return cv.boolean(value) | ||||
|  | ||||
|     if uses_swap: | ||||
|         return {cv.Required(CONF_SWAP_XY): cv.boolean} | ||||
|     return {cv.Optional(CONF_SWAP_XY, default=False): validator} | ||||
|  | ||||
|  | ||||
| def model_schema(config): | ||||
|     model = MODELS[config[CONF_MODEL]] | ||||
|     bus_mode = config[CONF_BUS_MODE] | ||||
| @@ -152,7 +139,7 @@ def model_schema(config): | ||||
|             { | ||||
|                 cv.Required(CONF_MIRROR_X): cv.boolean, | ||||
|                 cv.Required(CONF_MIRROR_Y): cv.boolean, | ||||
|                 **swap_xy_schema(model), | ||||
|                 **model.swap_xy_schema(), | ||||
|             } | ||||
|         ), | ||||
|         cv.one_of(CONF_DISABLED, lower=True), | ||||
|   | ||||
| @@ -21,7 +21,8 @@ void MQTTSelectComponent::setup() { | ||||
|     call.set_option(state); | ||||
|     call.perform(); | ||||
|   }); | ||||
|   this->select_->add_on_state_callback([this](const std::string &state, size_t index) { this->publish_state(state); }); | ||||
|   this->select_->add_on_state_callback( | ||||
|       [this](const std::string &state, size_t index) { this->publish_state(this->select_->option_at(index)); }); | ||||
| } | ||||
|  | ||||
| void MQTTSelectComponent::dump_config() { | ||||
| @@ -44,7 +45,7 @@ void MQTTSelectComponent::send_discovery(JsonObject root, mqtt::SendDiscoveryCon | ||||
| } | ||||
| bool MQTTSelectComponent::send_initial_state() { | ||||
|   if (this->select_->has_state()) { | ||||
|     return this->publish_state(this->select_->state); | ||||
|     return this->publish_state(this->select_->current_option()); | ||||
|   } else { | ||||
|     return true; | ||||
|   } | ||||
|   | ||||
| @@ -238,11 +238,6 @@ def number_schema( | ||||
|     return _NUMBER_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| NUMBER_SCHEMA = number_schema(Number) | ||||
| NUMBER_SCHEMA.add_extra(cv.deprecated_schema_constant("number")) | ||||
|  | ||||
|  | ||||
| async def setup_number_core_( | ||||
|     var, config, *, min_value: float, max_value: float, step: float | ||||
| ): | ||||
|   | ||||
| @@ -102,7 +102,7 @@ CONFIG_SCHEMA = cv.Any( | ||||
|             str: PACKAGE_SCHEMA, | ||||
|         } | ||||
|     ), | ||||
|     cv.ensure_list(PACKAGE_SCHEMA), | ||||
|     [PACKAGE_SCHEMA], | ||||
| ) | ||||
|  | ||||
|  | ||||
|   | ||||
| @@ -1,3 +1,5 @@ | ||||
| import logging | ||||
|  | ||||
| from esphome import automation, pins | ||||
| import esphome.codegen as cg | ||||
| from esphome.components import esp32, esp32_rmt, remote_base | ||||
| @@ -18,9 +20,12 @@ from esphome.const import ( | ||||
| ) | ||||
| from esphome.core import CORE | ||||
|  | ||||
| _LOGGER = logging.getLogger(__name__) | ||||
|  | ||||
| AUTO_LOAD = ["remote_base"] | ||||
|  | ||||
| CONF_EOT_LEVEL = "eot_level" | ||||
| CONF_NON_BLOCKING = "non_blocking" | ||||
| CONF_ON_TRANSMIT = "on_transmit" | ||||
| CONF_ON_COMPLETE = "on_complete" | ||||
| CONF_TRANSMITTER_ID = remote_base.CONF_TRANSMITTER_ID | ||||
| @@ -65,11 +70,25 @@ CONFIG_SCHEMA = cv.Schema( | ||||
|             esp32_c6=48, | ||||
|             esp32_h2=48, | ||||
|         ): cv.All(cv.only_on_esp32, cv.int_range(min=2)), | ||||
|         cv.Optional(CONF_NON_BLOCKING): cv.All(cv.only_on_esp32, cv.boolean), | ||||
|         cv.Optional(CONF_ON_TRANSMIT): automation.validate_automation(single=True), | ||||
|         cv.Optional(CONF_ON_COMPLETE): automation.validate_automation(single=True), | ||||
|     } | ||||
| ).extend(cv.COMPONENT_SCHEMA) | ||||
|  | ||||
|  | ||||
| def _validate_non_blocking(config): | ||||
|     if CORE.is_esp32 and CONF_NON_BLOCKING not in config: | ||||
|         _LOGGER.warning( | ||||
|             "'non_blocking' is not set for 'remote_transmitter' and will default to 'true'.\n" | ||||
|             "The default behavior changed in 2025.11.0; previously blocking mode was used.\n" | ||||
|             "To silence this warning, explicitly set 'non_blocking: true' (or 'false')." | ||||
|         ) | ||||
|         config[CONF_NON_BLOCKING] = True | ||||
|  | ||||
|  | ||||
| FINAL_VALIDATE_SCHEMA = _validate_non_blocking | ||||
|  | ||||
| DIGITAL_WRITE_ACTION_SCHEMA = cv.maybe_simple_value( | ||||
|     { | ||||
|         cv.GenerateID(CONF_TRANSMITTER_ID): cv.use_id(RemoteTransmitterComponent), | ||||
| @@ -95,6 +114,7 @@ async def to_code(config): | ||||
|     if CORE.is_esp32: | ||||
|         var = cg.new_Pvariable(config[CONF_ID], pin) | ||||
|         cg.add(var.set_rmt_symbols(config[CONF_RMT_SYMBOLS])) | ||||
|         cg.add(var.set_non_blocking(config[CONF_NON_BLOCKING])) | ||||
|         if CONF_CLOCK_RESOLUTION in config: | ||||
|             cg.add(var.set_clock_resolution(config[CONF_CLOCK_RESOLUTION])) | ||||
|         if CONF_USE_DMA in config: | ||||
|   | ||||
| @@ -54,6 +54,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, | ||||
| #if defined(USE_ESP32) | ||||
|   void set_with_dma(bool with_dma) { this->with_dma_ = with_dma; } | ||||
|   void set_eot_level(bool eot_level) { this->eot_level_ = eot_level; } | ||||
|   void set_non_blocking(bool non_blocking) { this->non_blocking_ = non_blocking; } | ||||
| #endif | ||||
|  | ||||
|   Trigger<> *get_transmit_trigger() const { return this->transmit_trigger_; }; | ||||
| @@ -74,6 +75,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, | ||||
|  | ||||
| #ifdef USE_ESP32 | ||||
|   void configure_rmt_(); | ||||
|   void wait_for_rmt_(); | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
|   RemoteTransmitterComponentStore store_{}; | ||||
| @@ -90,6 +92,7 @@ class RemoteTransmitterComponent : public remote_base::RemoteTransmitterBase, | ||||
|   esp_err_t error_code_{ESP_OK}; | ||||
|   std::string error_string_{""}; | ||||
|   bool inverted_{false}; | ||||
|   bool non_blocking_{false}; | ||||
| #endif | ||||
|   uint8_t carrier_duty_percent_; | ||||
|  | ||||
|   | ||||
| @@ -196,12 +196,29 @@ void RemoteTransmitterComponent::configure_rmt_() { | ||||
|   } | ||||
| } | ||||
|  | ||||
| void RemoteTransmitterComponent::wait_for_rmt_() { | ||||
|   esp_err_t error = rmt_tx_wait_all_done(this->channel_, -1); | ||||
|   if (error != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error)); | ||||
|     this->status_set_warning(); | ||||
|   } | ||||
|  | ||||
|   this->complete_trigger_->trigger(); | ||||
| } | ||||
|  | ||||
| #if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 1) | ||||
| void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { | ||||
|   uint64_t total_duration = 0; | ||||
|  | ||||
|   if (this->is_failed()) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   // if the timeout was cancelled, block until the tx is complete | ||||
|   if (this->non_blocking_ && this->cancel_timeout("complete")) { | ||||
|     this->wait_for_rmt_(); | ||||
|   } | ||||
|  | ||||
|   if (this->current_carrier_frequency_ != this->temp_.get_carrier_frequency()) { | ||||
|     this->current_carrier_frequency_ = this->temp_.get_carrier_frequency(); | ||||
|     this->configure_rmt_(); | ||||
| @@ -212,6 +229,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen | ||||
|  | ||||
|   // encode any delay at the start of the buffer to simplify the encoder callback | ||||
|   // this will be skipped the first time around | ||||
|   total_duration += send_wait * (send_times - 1); | ||||
|   send_wait = this->from_microseconds_(static_cast<uint32_t>(send_wait)); | ||||
|   while (send_wait > 0) { | ||||
|     int32_t duration = std::min(send_wait, uint32_t(RMT_SYMBOL_DURATION_MAX)); | ||||
| @@ -229,6 +247,7 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen | ||||
|     if (!level) { | ||||
|       value = -value; | ||||
|     } | ||||
|     total_duration += value * send_times; | ||||
|     value = this->from_microseconds_(static_cast<uint32_t>(value)); | ||||
|     while (value > 0) { | ||||
|       int32_t duration = std::min(value, int32_t(RMT_SYMBOL_DURATION_MAX)); | ||||
| @@ -260,13 +279,12 @@ void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t sen | ||||
|   } else { | ||||
|     this->status_clear_warning(); | ||||
|   } | ||||
|   error = rmt_tx_wait_all_done(this->channel_, -1); | ||||
|   if (error != ESP_OK) { | ||||
|     ESP_LOGW(TAG, "rmt_tx_wait_all_done failed: %s", esp_err_to_name(error)); | ||||
|     this->status_set_warning(); | ||||
|   } | ||||
|  | ||||
|   this->complete_trigger_->trigger(); | ||||
|   if (this->non_blocking_) { | ||||
|     this->set_timeout("complete", total_duration / 1000, [this]() { this->wait_for_rmt_(); }); | ||||
|   } else { | ||||
|     this->wait_for_rmt_(); | ||||
|   } | ||||
| } | ||||
| #else | ||||
| void RemoteTransmitterComponent::send_internal(uint32_t send_times, uint32_t send_wait) { | ||||
|   | ||||
| @@ -435,12 +435,12 @@ void MR24HPC1Component::r24_frame_parse_open_underlying_information_(uint8_t *da | ||||
|   } else if ((this->existence_boundary_select_ != nullptr) && | ||||
|              ((data[FRAME_COMMAND_WORD_INDEX] == 0x0a) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8a))) { | ||||
|     if (this->existence_boundary_select_->has_index(data[FRAME_DATA_INDEX] - 1)) { | ||||
|       this->existence_boundary_select_->publish_state(S_BOUNDARY_STR[data[FRAME_DATA_INDEX] - 1]); | ||||
|       this->existence_boundary_select_->publish_state(data[FRAME_DATA_INDEX] - 1); | ||||
|     } | ||||
|   } else if ((this->motion_boundary_select_ != nullptr) && | ||||
|              ((data[FRAME_COMMAND_WORD_INDEX] == 0x0b) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8b))) { | ||||
|     if (this->motion_boundary_select_->has_index(data[FRAME_DATA_INDEX] - 1)) { | ||||
|       this->motion_boundary_select_->publish_state(S_BOUNDARY_STR[data[FRAME_DATA_INDEX] - 1]); | ||||
|       this->motion_boundary_select_->publish_state(data[FRAME_DATA_INDEX] - 1); | ||||
|     } | ||||
|   } else if ((this->motion_trigger_number_ != nullptr) && | ||||
|              ((data[FRAME_COMMAND_WORD_INDEX] == 0x0c) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8c))) { | ||||
| @@ -515,7 +515,7 @@ void MR24HPC1Component::r24_frame_parse_work_status_(uint8_t *data) { | ||||
|     ESP_LOGD(TAG, "Reply: get radar init status 0x%02X", data[FRAME_DATA_INDEX]); | ||||
|   } else if (data[FRAME_COMMAND_WORD_INDEX] == 0x07) { | ||||
|     if ((this->scene_mode_select_ != nullptr) && (this->scene_mode_select_->has_index(data[FRAME_DATA_INDEX]))) { | ||||
|       this->scene_mode_select_->publish_state(S_SCENE_STR[data[FRAME_DATA_INDEX]]); | ||||
|       this->scene_mode_select_->publish_state(data[FRAME_DATA_INDEX]); | ||||
|     } else { | ||||
|       ESP_LOGD(TAG, "Select has index offset %d Error", data[FRAME_DATA_INDEX]); | ||||
|     } | ||||
| @@ -538,7 +538,7 @@ void MR24HPC1Component::r24_frame_parse_work_status_(uint8_t *data) { | ||||
|     ESP_LOGD(TAG, "Reply: get radar init status 0x%02X", data[FRAME_DATA_INDEX]); | ||||
|   } else if (data[FRAME_COMMAND_WORD_INDEX] == 0x87) { | ||||
|     if ((this->scene_mode_select_ != nullptr) && (this->scene_mode_select_->has_index(data[FRAME_DATA_INDEX]))) { | ||||
|       this->scene_mode_select_->publish_state(S_SCENE_STR[data[FRAME_DATA_INDEX]]); | ||||
|       this->scene_mode_select_->publish_state(data[FRAME_DATA_INDEX]); | ||||
|     } else { | ||||
|       ESP_LOGD(TAG, "Select has index offset %d Error", data[FRAME_DATA_INDEX]); | ||||
|     } | ||||
| @@ -581,7 +581,7 @@ void MR24HPC1Component::r24_frame_parse_human_information_(uint8_t *data) { | ||||
|              ((data[FRAME_COMMAND_WORD_INDEX] == 0x0A) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8A))) { | ||||
|     // none:0x00  1s:0x01 30s:0x02 1min:0x03 2min:0x04 5min:0x05 10min:0x06 30min:0x07 1hour:0x08 | ||||
|     if (data[FRAME_DATA_INDEX] < 9) { | ||||
|       this->unman_time_select_->publish_state(S_UNMANNED_TIME_STR[data[FRAME_DATA_INDEX]]); | ||||
|       this->unman_time_select_->publish_state(data[FRAME_DATA_INDEX]); | ||||
|     } | ||||
|   } else if ((this->keep_away_text_sensor_ != nullptr) && | ||||
|              ((data[FRAME_COMMAND_WORD_INDEX] == 0x0B) || (data[FRAME_COMMAND_WORD_INDEX] == 0x8B))) { | ||||
|   | ||||
| @@ -292,7 +292,7 @@ void MR60FDA2Component::process_frame_() { | ||||
|  | ||||
|         install_height_float = bit_cast<float>(current_install_height_int); | ||||
|         uint32_t select_index = find_nearest_index(install_height_float, INSTALL_HEIGHT, 7); | ||||
|         this->install_height_select_->publish_state(this->install_height_select_->at(select_index).value()); | ||||
|         this->install_height_select_->publish_state(select_index); | ||||
|       } | ||||
|  | ||||
|       if (this->height_threshold_select_ != nullptr) { | ||||
| @@ -301,7 +301,7 @@ void MR60FDA2Component::process_frame_() { | ||||
|  | ||||
|         height_threshold_float = bit_cast<float>(current_height_threshold_int); | ||||
|         size_t select_index = find_nearest_index(height_threshold_float, HEIGHT_THRESHOLD, 7); | ||||
|         this->height_threshold_select_->publish_state(this->height_threshold_select_->at(select_index).value()); | ||||
|         this->height_threshold_select_->publish_state(select_index); | ||||
|       } | ||||
|  | ||||
|       if (this->sensitivity_select_ != nullptr) { | ||||
| @@ -309,7 +309,7 @@ void MR60FDA2Component::process_frame_() { | ||||
|             encode_uint32(current_data_buf_[11], current_data_buf_[10], current_data_buf_[9], current_data_buf_[8]); | ||||
|  | ||||
|         uint32_t select_index = find_nearest_index(current_sensitivity, SENSITIVITY, 3); | ||||
|         this->sensitivity_select_->publish_state(this->sensitivity_select_->at(select_index).value()); | ||||
|         this->sensitivity_select_->publish_state(select_index); | ||||
|       } | ||||
|  | ||||
|       ESP_LOGD(TAG, "Mounting height: %.2f, Height threshold: %.2f, Sensitivity: %" PRIu32, install_height_float, | ||||
|   | ||||
| @@ -86,11 +86,6 @@ def select_schema( | ||||
|     return _SELECT_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| SELECT_SCHEMA = select_schema(Select) | ||||
| SELECT_SCHEMA.add_extra(cv.deprecated_schema_constant("select")) | ||||
|  | ||||
|  | ||||
| async def setup_select_core_(var, config, *, options: list[str]): | ||||
|     await setup_entity(var, config, "select") | ||||
|  | ||||
|   | ||||
| @@ -7,24 +7,43 @@ namespace select { | ||||
|  | ||||
| static const char *const TAG = "select"; | ||||
|  | ||||
| void Select::publish_state(const std::string &state) { | ||||
| void Select::publish_state(const std::string &state) { this->publish_state(state.c_str()); } | ||||
|  | ||||
| void Select::publish_state(const char *state) { | ||||
|   auto index = this->index_of(state); | ||||
|   const auto *name = this->get_name().c_str(); | ||||
|   if (index.has_value()) { | ||||
|     this->set_has_state(true); | ||||
|     this->state = state; | ||||
|     ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", name, state.c_str(), index.value()); | ||||
|     this->state_callback_.call(state, index.value()); | ||||
|     this->publish_state(index.value()); | ||||
|   } else { | ||||
|     ESP_LOGE(TAG, "'%s': invalid state for publish_state(): %s", name, state.c_str()); | ||||
|     ESP_LOGE(TAG, "'%s': Invalid option %s", this->get_name().c_str(), state); | ||||
|   } | ||||
| } | ||||
|  | ||||
| void Select::publish_state(size_t index) { | ||||
|   if (!this->has_index(index)) { | ||||
|     ESP_LOGE(TAG, "'%s': Invalid index %zu", this->get_name().c_str(), index); | ||||
|     return; | ||||
|   } | ||||
|   const char *option = this->option_at(index); | ||||
|   this->set_has_state(true); | ||||
|   this->active_index_ = index; | ||||
| #pragma GCC diagnostic push | ||||
| #pragma GCC diagnostic ignored "-Wdeprecated-declarations" | ||||
|   this->state = option;  // Update deprecated member for backward compatibility | ||||
| #pragma GCC diagnostic pop | ||||
|   ESP_LOGD(TAG, "'%s': Sending state %s (index %zu)", this->get_name().c_str(), option, index); | ||||
|   // Callback signature requires std::string, create temporary for compatibility | ||||
|   this->state_callback_.call(std::string(option), index); | ||||
| } | ||||
|  | ||||
| const char *Select::current_option() const { return this->option_at(this->active_index_); } | ||||
|  | ||||
| void Select::add_on_state_callback(std::function<void(std::string, size_t)> &&callback) { | ||||
|   this->state_callback_.add(std::move(callback)); | ||||
| } | ||||
|  | ||||
| bool Select::has_option(const std::string &option) const { return this->index_of(option).has_value(); } | ||||
| bool Select::has_option(const std::string &option) const { return this->index_of(option.c_str()).has_value(); } | ||||
|  | ||||
| bool Select::has_option(const char *option) const { return this->index_of(option).has_value(); } | ||||
|  | ||||
| bool Select::has_index(size_t index) const { return index < this->size(); } | ||||
|  | ||||
| @@ -33,10 +52,12 @@ size_t Select::size() const { | ||||
|   return options.size(); | ||||
| } | ||||
|  | ||||
| optional<size_t> Select::index_of(const std::string &option) const { | ||||
| optional<size_t> Select::index_of(const std::string &option) const { return this->index_of(option.c_str()); } | ||||
|  | ||||
| optional<size_t> Select::index_of(const char *option) const { | ||||
|   const auto &options = traits.get_options(); | ||||
|   for (size_t i = 0; i < options.size(); i++) { | ||||
|     if (strcmp(options[i], option.c_str()) == 0) { | ||||
|     if (strcmp(options[i], option) == 0) { | ||||
|       return i; | ||||
|     } | ||||
|   } | ||||
| @@ -45,19 +66,17 @@ optional<size_t> Select::index_of(const std::string &option) const { | ||||
|  | ||||
| optional<size_t> Select::active_index() const { | ||||
|   if (this->has_state()) { | ||||
|     return this->index_of(this->state); | ||||
|   } else { | ||||
|     return {}; | ||||
|     return this->active_index_; | ||||
|   } | ||||
|   return {}; | ||||
| } | ||||
|  | ||||
| optional<std::string> Select::at(size_t index) const { | ||||
|   if (this->has_index(index)) { | ||||
|     const auto &options = traits.get_options(); | ||||
|     return std::string(options.at(index)); | ||||
|   } else { | ||||
|     return {}; | ||||
|   } | ||||
|   return {}; | ||||
| } | ||||
|  | ||||
| const char *Select::option_at(size_t index) const { return traits.get_options().at(index); } | ||||
|   | ||||
| @@ -30,16 +30,31 @@ namespace select { | ||||
|  */ | ||||
| class Select : public EntityBase { | ||||
|  public: | ||||
|   std::string state; | ||||
|   SelectTraits traits; | ||||
|  | ||||
| #pragma GCC diagnostic push | ||||
| #pragma GCC diagnostic ignored "-Wdeprecated-declarations" | ||||
|   /// @deprecated Use current_option() instead. This member will be removed in ESPHome 2026.5.0. | ||||
|   __attribute__((deprecated("Use current_option() instead of .state. Will be removed in 2026.5.0"))) | ||||
|   std::string state{}; | ||||
|  | ||||
|   Select() = default; | ||||
|   ~Select() = default; | ||||
| #pragma GCC diagnostic pop | ||||
|  | ||||
|   void publish_state(const std::string &state); | ||||
|   void publish_state(const char *state); | ||||
|   void publish_state(size_t index); | ||||
|  | ||||
|   /// Return the currently selected option (as const char* from flash). | ||||
|   const char *current_option() const; | ||||
|  | ||||
|   /// Instantiate a SelectCall object to modify this select component's state. | ||||
|   SelectCall make_call() { return SelectCall(this); } | ||||
|  | ||||
|   /// Return whether this select component contains the provided option. | ||||
|   bool has_option(const std::string &option) const; | ||||
|   bool has_option(const char *option) const; | ||||
|  | ||||
|   /// Return whether this select component contains the provided index offset. | ||||
|   bool has_index(size_t index) const; | ||||
| @@ -49,6 +64,7 @@ class Select : public EntityBase { | ||||
|  | ||||
|   /// Find the (optional) index offset of the provided option value. | ||||
|   optional<size_t> index_of(const std::string &option) const; | ||||
|   optional<size_t> index_of(const char *option) const; | ||||
|  | ||||
|   /// Return the (optional) index offset of the currently active option. | ||||
|   optional<size_t> active_index() const; | ||||
| @@ -61,16 +77,42 @@ class Select : public EntityBase { | ||||
|  | ||||
|   void add_on_state_callback(std::function<void(std::string, size_t)> &&callback); | ||||
|  | ||||
|   /** Set the value of the select by index, this is an optional virtual method. | ||||
|    * | ||||
|    * This method is called by the SelectCall when the index is already known. | ||||
|    * Default implementation converts to string and calls control(). | ||||
|    * Override this to work directly with indices and avoid string conversions. | ||||
|    * | ||||
|    * @param index The index as validated by the SelectCall. | ||||
|    */ | ||||
|   virtual void control(size_t index) { this->control(this->option_at(index)); } | ||||
|  | ||||
|  protected: | ||||
|   friend class SelectCall; | ||||
|  | ||||
|   /** Set the value of the select, this is a virtual method that each select integration must implement. | ||||
|   size_t active_index_{0}; | ||||
|  | ||||
|   /** Set the value of the select, this is a virtual method that each select integration can implement. | ||||
|    * | ||||
|    * This method is called by the SelectCall. | ||||
|    * This method is called by control(size_t) when not overridden, or directly by external code. | ||||
|    * Integrations can either: | ||||
|    * 1. Override this method to handle string-based control (traditional approach) | ||||
|    * 2. Override control(size_t) instead to work with indices directly (recommended) | ||||
|    * | ||||
|    * Default implementation converts to index and calls control(size_t). | ||||
|    * | ||||
|    * Delegation chain: | ||||
|    * - SelectCall::perform() → control(size_t) → [if not overridden] → control(string) | ||||
|    * - External code → control(string) → publish_state(string) → publish_state(size_t) | ||||
|    * | ||||
|    * @param value The value as validated by the SelectCall. | ||||
|    */ | ||||
|   virtual void control(const std::string &value) = 0; | ||||
|   virtual void control(const std::string &value) { | ||||
|     auto index = this->index_of(value); | ||||
|     if (index.has_value()) { | ||||
|       this->control(index.value()); | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   CallbackManager<void(std::string, size_t)> state_callback_; | ||||
| }; | ||||
|   | ||||
| @@ -7,19 +7,21 @@ namespace select { | ||||
|  | ||||
| static const char *const TAG = "select"; | ||||
|  | ||||
| SelectCall &SelectCall::set_option(const std::string &option) { | ||||
|   return with_operation(SELECT_OP_SET).with_option(option); | ||||
| SelectCall &SelectCall::set_option(const std::string &option) { return this->with_option(option); } | ||||
|  | ||||
| SelectCall &SelectCall::set_option(const char *option) { return this->with_option(option); } | ||||
|  | ||||
| SelectCall &SelectCall::set_index(size_t index) { return this->with_index(index); } | ||||
|  | ||||
| SelectCall &SelectCall::select_next(bool cycle) { return this->with_operation(SELECT_OP_NEXT).with_cycle(cycle); } | ||||
|  | ||||
| SelectCall &SelectCall::select_previous(bool cycle) { | ||||
|   return this->with_operation(SELECT_OP_PREVIOUS).with_cycle(cycle); | ||||
| } | ||||
|  | ||||
| SelectCall &SelectCall::set_index(size_t index) { return with_operation(SELECT_OP_SET_INDEX).with_index(index); } | ||||
| SelectCall &SelectCall::select_first() { return this->with_operation(SELECT_OP_FIRST); } | ||||
|  | ||||
| SelectCall &SelectCall::select_next(bool cycle) { return with_operation(SELECT_OP_NEXT).with_cycle(cycle); } | ||||
|  | ||||
| SelectCall &SelectCall::select_previous(bool cycle) { return with_operation(SELECT_OP_PREVIOUS).with_cycle(cycle); } | ||||
|  | ||||
| SelectCall &SelectCall::select_first() { return with_operation(SELECT_OP_FIRST); } | ||||
|  | ||||
| SelectCall &SelectCall::select_last() { return with_operation(SELECT_OP_LAST); } | ||||
| SelectCall &SelectCall::select_last() { return this->with_operation(SELECT_OP_LAST); } | ||||
|  | ||||
| SelectCall &SelectCall::with_operation(SelectOperation operation) { | ||||
|   this->operation_ = operation; | ||||
| @@ -31,89 +33,96 @@ SelectCall &SelectCall::with_cycle(bool cycle) { | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| SelectCall &SelectCall::with_option(const std::string &option) { | ||||
|   this->option_ = option; | ||||
| SelectCall &SelectCall::with_option(const std::string &option) { return this->with_option(option.c_str()); } | ||||
|  | ||||
| SelectCall &SelectCall::with_option(const char *option) { | ||||
|   this->operation_ = SELECT_OP_SET; | ||||
|   // Find the option index - this validates the option exists | ||||
|   this->index_ = this->parent_->index_of(option); | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| SelectCall &SelectCall::with_index(size_t index) { | ||||
|   this->index_ = index; | ||||
|   this->operation_ = SELECT_OP_SET; | ||||
|   if (index >= this->parent_->size()) { | ||||
|     ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", this->parent_->get_name().c_str(), index); | ||||
|     this->index_ = {};  // Store nullopt for invalid index | ||||
|   } else { | ||||
|     this->index_ = index; | ||||
|   } | ||||
|   return *this; | ||||
| } | ||||
|  | ||||
| optional<size_t> SelectCall::calculate_target_index_(const char *name) { | ||||
|   const auto &options = this->parent_->traits.get_options(); | ||||
|   if (options.empty()) { | ||||
|     ESP_LOGW(TAG, "'%s' - Select has no options", name); | ||||
|     return {}; | ||||
|   } | ||||
|  | ||||
|   if (this->operation_ == SELECT_OP_FIRST) { | ||||
|     return 0; | ||||
|   } | ||||
|  | ||||
|   if (this->operation_ == SELECT_OP_LAST) { | ||||
|     return options.size() - 1; | ||||
|   } | ||||
|  | ||||
|   if (this->operation_ == SELECT_OP_SET) { | ||||
|     ESP_LOGD(TAG, "'%s' - Setting", name); | ||||
|     if (!this->index_.has_value()) { | ||||
|       ESP_LOGW(TAG, "'%s' - No option set", name); | ||||
|       return {}; | ||||
|     } | ||||
|     return this->index_.value(); | ||||
|   } | ||||
|  | ||||
|   // SELECT_OP_NEXT or SELECT_OP_PREVIOUS | ||||
|   ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, | ||||
|            this->operation_ == SELECT_OP_NEXT ? LOG_STR_LITERAL("next") : LOG_STR_LITERAL("previous"), | ||||
|            this->cycle_ ? LOG_STR_LITERAL("") : LOG_STR_LITERAL("out")); | ||||
|  | ||||
|   const auto size = options.size(); | ||||
|   if (!this->parent_->has_state()) { | ||||
|     return this->operation_ == SELECT_OP_NEXT ? 0 : size - 1; | ||||
|   } | ||||
|  | ||||
|   // Use cached active_index_ instead of index_of() lookup | ||||
|   const auto active_index = this->parent_->active_index_; | ||||
|   if (this->cycle_) { | ||||
|     return (size + active_index + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size; | ||||
|   } | ||||
|  | ||||
|   if (this->operation_ == SELECT_OP_PREVIOUS && active_index > 0) { | ||||
|     return active_index - 1; | ||||
|   } | ||||
|  | ||||
|   if (this->operation_ == SELECT_OP_NEXT && active_index < size - 1) { | ||||
|     return active_index + 1; | ||||
|   } | ||||
|  | ||||
|   return {};  // Can't navigate further without cycling | ||||
| } | ||||
|  | ||||
| void SelectCall::perform() { | ||||
|   auto *parent = this->parent_; | ||||
|   const auto *name = parent->get_name().c_str(); | ||||
|   const auto &traits = parent->traits; | ||||
|   const auto &options = traits.get_options(); | ||||
|  | ||||
|   if (this->operation_ == SELECT_OP_NONE) { | ||||
|     ESP_LOGW(TAG, "'%s' - SelectCall performed without selecting an operation", name); | ||||
|     return; | ||||
|   } | ||||
|   if (options.empty()) { | ||||
|     ESP_LOGW(TAG, "'%s' - Cannot perform SelectCall, select has no options", name); | ||||
|  | ||||
|   // Calculate target index (with_index() and with_option() already validate bounds/existence) | ||||
|   auto target_index = this->calculate_target_index_(name); | ||||
|   if (!target_index.has_value()) { | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   std::string target_value; | ||||
|  | ||||
|   if (this->operation_ == SELECT_OP_SET) { | ||||
|     ESP_LOGD(TAG, "'%s' - Setting", name); | ||||
|     if (!this->option_.has_value()) { | ||||
|       ESP_LOGW(TAG, "'%s' - No option value set for SelectCall", name); | ||||
|       return; | ||||
|     } | ||||
|     target_value = this->option_.value(); | ||||
|   } else if (this->operation_ == SELECT_OP_SET_INDEX) { | ||||
|     if (!this->index_.has_value()) { | ||||
|       ESP_LOGW(TAG, "'%s' - No index value set for SelectCall", name); | ||||
|       return; | ||||
|     } | ||||
|     if (this->index_.value() >= options.size()) { | ||||
|       ESP_LOGW(TAG, "'%s' - Index value %zu out of bounds", name, this->index_.value()); | ||||
|       return; | ||||
|     } | ||||
|     target_value = options[this->index_.value()]; | ||||
|   } else if (this->operation_ == SELECT_OP_FIRST) { | ||||
|     target_value = options.front(); | ||||
|   } else if (this->operation_ == SELECT_OP_LAST) { | ||||
|     target_value = options.back(); | ||||
|   } else if (this->operation_ == SELECT_OP_NEXT || this->operation_ == SELECT_OP_PREVIOUS) { | ||||
|     auto cycle = this->cycle_; | ||||
|     ESP_LOGD(TAG, "'%s' - Selecting %s, with%s cycling", name, this->operation_ == SELECT_OP_NEXT ? "next" : "previous", | ||||
|              cycle ? "" : "out"); | ||||
|     if (!parent->has_state()) { | ||||
|       target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back(); | ||||
|     } else { | ||||
|       auto index = parent->index_of(parent->state); | ||||
|       if (index.has_value()) { | ||||
|         auto size = options.size(); | ||||
|         if (cycle) { | ||||
|           auto use_index = (size + index.value() + (this->operation_ == SELECT_OP_NEXT ? +1 : -1)) % size; | ||||
|           target_value = options[use_index]; | ||||
|         } else { | ||||
|           if (this->operation_ == SELECT_OP_PREVIOUS && index.value() > 0) { | ||||
|             target_value = options[index.value() - 1]; | ||||
|           } else if (this->operation_ == SELECT_OP_NEXT && index.value() < options.size() - 1) { | ||||
|             target_value = options[index.value() + 1]; | ||||
|           } else { | ||||
|             return; | ||||
|           } | ||||
|         } | ||||
|       } else { | ||||
|         target_value = this->operation_ == SELECT_OP_NEXT ? options.front() : options.back(); | ||||
|       } | ||||
|     } | ||||
|   } | ||||
|  | ||||
|   if (!parent->has_option(target_value)) { | ||||
|     ESP_LOGW(TAG, "'%s' - Option %s is not a valid option", name, target_value.c_str()); | ||||
|     return; | ||||
|   } | ||||
|  | ||||
|   ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, target_value.c_str()); | ||||
|   parent->control(target_value); | ||||
|   auto idx = target_index.value(); | ||||
|   // All operations use indices, call control() by index to avoid string conversion | ||||
|   ESP_LOGD(TAG, "'%s' - Set selected option to: %s", name, parent->option_at(idx)); | ||||
|   parent->control(idx); | ||||
| } | ||||
|  | ||||
| }  // namespace select | ||||
|   | ||||
| @@ -10,7 +10,6 @@ class Select; | ||||
| enum SelectOperation { | ||||
|   SELECT_OP_NONE, | ||||
|   SELECT_OP_SET, | ||||
|   SELECT_OP_SET_INDEX, | ||||
|   SELECT_OP_NEXT, | ||||
|   SELECT_OP_PREVIOUS, | ||||
|   SELECT_OP_FIRST, | ||||
| @@ -23,6 +22,7 @@ class SelectCall { | ||||
|   void perform(); | ||||
|  | ||||
|   SelectCall &set_option(const std::string &option); | ||||
|   SelectCall &set_option(const char *option); | ||||
|   SelectCall &set_index(size_t index); | ||||
|  | ||||
|   SelectCall &select_next(bool cycle); | ||||
| @@ -33,11 +33,13 @@ class SelectCall { | ||||
|   SelectCall &with_operation(SelectOperation operation); | ||||
|   SelectCall &with_cycle(bool cycle); | ||||
|   SelectCall &with_option(const std::string &option); | ||||
|   SelectCall &with_option(const char *option); | ||||
|   SelectCall &with_index(size_t index); | ||||
|  | ||||
|  protected: | ||||
|   __attribute__((always_inline)) inline optional<size_t> calculate_target_index_(const char *name); | ||||
|  | ||||
|   Select *const parent_; | ||||
|   optional<std::string> option_; | ||||
|   optional<size_t> index_; | ||||
|   SelectOperation operation_{SELECT_OP_NONE}; | ||||
|   bool cycle_; | ||||
|   | ||||
| @@ -369,11 +369,6 @@ def sensor_schema( | ||||
|     return _SENSOR_SCHEMA.extend(schema) | ||||
|  | ||||
|  | ||||
| # Remove before 2025.11.0 | ||||
| SENSOR_SCHEMA = sensor_schema() | ||||
| SENSOR_SCHEMA.add_extra(cv.deprecated_schema_constant("sensor")) | ||||
|  | ||||
|  | ||||
| @FILTER_REGISTRY.register("offset", OffsetFilter, cv.templatable(cv.float_)) | ||||
| async def offset_filter_to_code(config, filter_id): | ||||
|     template_ = await cg.templatable(config, [], float) | ||||
|   | ||||
| @@ -1,7 +1,5 @@ | ||||
| #pragma once | ||||
|  | ||||
| #include <set> | ||||
|  | ||||
| #include "esphome/core/component.h" | ||||
| #include "esphome/components/output/binary_output.h" | ||||
| #include "esphome/components/output/float_output.h" | ||||
| @@ -18,7 +16,7 @@ class SpeedFan : public Component, public fan::Fan { | ||||
|   void set_output(output::FloatOutput *output) { this->output_ = output; } | ||||
|   void set_oscillating(output::BinaryOutput *oscillating) { this->oscillating_ = oscillating; } | ||||
|   void set_direction(output::BinaryOutput *direction) { this->direction_ = direction; } | ||||
|   void set_preset_modes(const std::vector<std::string> &presets) { this->preset_modes_ = presets; } | ||||
|   void set_preset_modes(std::initializer_list<const char *> presets) { this->preset_modes_ = presets; } | ||||
|   fan::FanTraits get_traits() override { return this->traits_; } | ||||
|  | ||||
|  protected: | ||||
| @@ -30,7 +28,7 @@ class SpeedFan : public Component, public fan::Fan { | ||||
|   output::BinaryOutput *direction_{nullptr}; | ||||
|   int speed_count_{}; | ||||
|   fan::FanTraits traits_; | ||||
|   std::vector<std::string> preset_modes_{}; | ||||
|   std::vector<const char *> preset_modes_{}; | ||||
| }; | ||||
|  | ||||
| }  // namespace speed | ||||
|   | ||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user