mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 00:51:49 +00:00 
			
		
		
		
	Compare commits
	
		
			166 Commits
		
	
	
		
			cond_compi
			...
			2025.10.0b
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					13cfa30c67 | ||
| 
						 | 
					da1959ab5d | ||
| 
						 | 
					2b42903e9c | ||
| 
						 | 
					742c9cbb53 | ||
| 
						 | 
					e4bc465a3d | ||
| 
						 | 
					5cec0941f8 | ||
| 
						 | 
					72a7aeb430 | ||
| 
						 | 
					53e6b28092 | ||
| 
						 | 
					7f3c7bb5c6 | ||
| 
						 | 
					c02c0b2a96 | ||
| 
						 | 
					5f5092e29f | ||
| 
						 | 
					2864bf1674 | ||
| 
						 | 
					132e949927 | ||
| 
						 | 
					8fa44e471d | ||
| 
						 | 
					ccedcfb600 | ||
| 
						 | 
					8b0ec0afe3 | ||
| 
						 | 
					dca29ed89b | ||
| 
						 | 
					728726e29e | ||
| 
						 | 
					79f4ca20b8 | ||
| 
						 | 
					3eca72e0b8 | ||
| 
						 | 
					22c0f55cef | ||
| 
						 | 
					fd8ecc9608 | ||
| 
						 | 
					ac96a59d58 | ||
| 
						 | 
					dceed992d8 | ||
| 
						 | 
					b0c66c1c09 | ||
| 
						 | 
					8f04a5b944 | ||
| 
						 | 
					e6c21df30b | ||
| 
						 | 
					842cb9033a | ||
| 
						 | 
					a2cb415dfa | ||
| 
						 | 
					1fac193535 | ||
| 
						 | 
					34632f78cf | ||
| 
						 | 
					b93c60e85a | ||
| 
						 | 
					60dc055509 | ||
| 
						 | 
					9ad462d8c6 | ||
| 
						 | 
					f1af9d978c | ||
| 
						 | 
					785df05631 | ||
| 
						 | 
					93266ad08f | ||
| 
						 | 
					2fac813f18 | ||
| 
						 | 
					a62c7a03dd | ||
| 
						 | 
					ec63247ae0 | ||
| 
						 | 
					0fe6e7169c | ||
| 
						 | 
					a0f4de1bfb | ||
| 
						 | 
					a541549d23 | ||
| 
						 | 
					b74715fe14 | ||
| 
						 | 
					5aff20a624 | ||
| 
						 | 
					7682b4e9a3 | ||
| 
						 | 
					6eabf709c6 | ||
| 
						 | 
					6209d4b493 | ||
| 
						 | 
					f10c361454 | ||
| 
						 | 
					27456c1370 | ||
| 
						 | 
					1aeefbe547 | ||
| 
						 | 
					3f3bce7ef4 | ||
| 
						 | 
					0acc58d5a1 | ||
| 
						 | 
					0b4ef0fea2 | ||
| 
						 | 
					a067bdb769 | ||
| 
						 | 
					301e7a7ac5 | ||
| 
						 | 
					ac566b7fd6 | ||
| 
						 | 
					fddb8b35f2 | ||
| 
						 | 
					27e1095cd7 | ||
| 
						 | 
					fa4541a4f3 | ||
| 
						 | 
					24dcc1843e | ||
| 
						 | 
					f670d775ac | ||
| 
						 | 
					59a31adac2 | ||
| 
						 | 
					a3c0acc7c9 | ||
| 
						 | 
					ad2c5b96a9 | ||
| 
						 | 
					9adc3bd943 | ||
| 
						 | 
					ad296a7d74 | ||
| 
						 | 
					fdd422c42a | ||
| 
						 | 
					3d82301c3d | ||
| 
						 | 
					2fa49be17d | ||
| 
						 | 
					75867842ea | ||
| 
						 | 
					cba85c0925 | ||
| 
						 | 
					42d1269aaf | ||
| 
						 | 
					f4df17673b | ||
| 
						 | 
					e340397b41 | ||
| 
						 | 
					abeadc7830 | ||
| 
						 | 
					8d4b347e5c | ||
| 
						 | 
					a7f556c25f | ||
| 
						 | 
					3f4250fcd7 | ||
| 
						 | 
					b532e04ae4 | ||
| 
						 | 
					697cab45dd | ||
| 
						 | 
					a88182c8e3 | ||
| 
						 | 
					8cfb6578d1 | ||
| 
						 | 
					eb16d322cd | ||
| 
						 | 
					22e06ba063 | ||
| 
						 | 
					7147479f90 | ||
| 
						 | 
					e55df1babc | ||
| 
						 | 
					4c8fc5f4e6 | ||
| 
						 | 
					646508006c | ||
| 
						 | 
					9384f0683b | ||
| 
						 | 
					5e7f5bf890 | ||
| 
						 | 
					2a8796437d | ||
| 
						 | 
					1635767aa2 | ||
| 
						 | 
					192856e8d1 | ||
| 
						 | 
					71be5a5f65 | ||
| 
						 | 
					f86b83cda5 | ||
| 
						 | 
					74c055745f | ||
| 
						 | 
					3edcdc7d80 | ||
| 
						 | 
					94fea68e3e | ||
| 
						 | 
					6880f9fc5c | ||
| 
						 | 
					26ebac8cb8 | ||
| 
						 | 
					5cf0046601 | ||
| 
						 | 
					c68017ddb4 | ||
| 
						 | 
					cfd241ff29 | ||
| 
						 | 
					f757a19e82 | ||
| 
						 | 
					e8854e0659 | ||
| 
						 | 
					a3622d878d | ||
| 
						 | 
					da2089c8be | ||
| 
						 | 
					118663f9e2 | ||
| 
						 | 
					4a99987bfe | ||
| 
						 | 
					d164c06f01 | ||
| 
						 | 
					972987acdf | ||
| 
						 | 
					eea2b6b81b | ||
| 
						 | 
					f62e06104e | ||
| 
						 | 
					f26e71bae6 | ||
| 
						 | 
					c6e4a7911c | ||
| 
						 | 
					e2c5eeef97 | ||
| 
						 | 
					7ea51b1865 | ||
| 
						 | 
					aa1afbd152 | ||
| 
						 | 
					20d9ae699c | ||
| 
						 | 
					c0fb0ae06f | ||
| 
						 | 
					9b6d62cd69 | ||
| 
						 | 
					5932a4bd0e | ||
| 
						 | 
					84c3cf5f17 | ||
| 
						 | 
					120a445abf | ||
| 
						 | 
					41c073a451 | ||
| 
						 | 
					0fd71ca211 | ||
| 
						 | 
					19439199cc | ||
| 
						 | 
					39d5cbc74a | ||
| 
						 | 
					722c5a94f2 | ||
| 
						 | 
					7b48fc292f | ||
| 
						 | 
					6c7d92e726 | ||
| 
						 | 
					b1859c50bd | ||
| 
						 | 
					3f9924eac2 | ||
| 
						 | 
					874db20b7d | ||
| 
						 | 
					2eea674c04 | ||
| 
						 | 
					0137954f2b | ||
| 
						 | 
					0a40a30e4a | ||
| 
						 | 
					d43b844e06 | ||
| 
						 | 
					2596b6096f | ||
| 
						 | 
					6f8e82aeb6 | ||
| 
						 | 
					ca0e738799 | ||
| 
						 | 
					14a23101f2 | ||
| 
						 | 
					2b389bb8f2 | ||
| 
						 | 
					89c3340ef6 | ||
| 
						 | 
					ba0532cda7 | ||
| 
						 | 
					5419b8bddb | ||
| 
						 | 
					624868bb05 | ||
| 
						 | 
					f2aa5a754c | ||
| 
						 | 
					638c6cc14e | ||
| 
						 | 
					8137d7600a | ||
| 
						 | 
					08afc3030a | ||
| 
						 | 
					1deb79a24b | ||
| 
						 | 
					de21c61b6a | ||
| 
						 | 
					db1aa82350 | ||
| 
						 | 
					fe4799b300 | ||
| 
						 | 
					93e18e850e | ||
| 
						 | 
					5cef75dbe1 | ||
| 
						 | 
					4194a940ae | ||
| 
						 | 
					59c0ffb98b | ||
| 
						 | 
					29658b79bc | ||
| 
						 | 
					158a59aa83 | ||
| 
						 | 
					c95180504a | ||
| 
						 | 
					848ba6b717 | ||
| 
						 | 
					922f4b6352 | ||
| 
						 | 
					fd3c05b42e | 
@@ -186,6 +186,11 @@ This document provides essential context for AI models interacting with this pro
 | 
			
		||||
        └── components/[component]/ # Component-specific tests
 | 
			
		||||
        ```
 | 
			
		||||
        Run them using `script/test_build_components`. Use `-c <component>` to test specific components and `-t <target>` for specific platforms.
 | 
			
		||||
    *   **Testing All Components Together:** To verify that all components can be tested together without ID conflicts or configuration issues, use:
 | 
			
		||||
        ```bash
 | 
			
		||||
        ./script/test_component_grouping.py -e config --all
 | 
			
		||||
        ```
 | 
			
		||||
        This tests all components in a single build to catch conflicts that might not appear when testing components individually. Use `-e config` for fast configuration validation, or `-e compile` for full compilation testing.
 | 
			
		||||
*   **Debugging and Troubleshooting:**
 | 
			
		||||
    *   **Debug Tools:**
 | 
			
		||||
        - `esphome config <file>.yaml` to validate configuration.
 | 
			
		||||
 
 | 
			
		||||
@@ -1 +1 @@
 | 
			
		||||
4368db58e8f884aff245996b1e8b644cc0796c0bb2fa706d5740d40b823d3ac9
 | 
			
		||||
049d60eed541730efaa4c0dc5d337b4287bf29b6daa350b5dfc1f23915f1c52f
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								.github/workflows/ci-clang-tidy-hash.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.github/workflows/ci-clang-tidy-hash.yml
									
									
									
									
										vendored
									
									
								
							@@ -6,6 +6,7 @@ on:
 | 
			
		||||
      - ".clang-tidy"
 | 
			
		||||
      - "platformio.ini"
 | 
			
		||||
      - "requirements_dev.txt"
 | 
			
		||||
      - "sdkconfig.defaults"
 | 
			
		||||
      - ".clang-tidy.hash"
 | 
			
		||||
      - "script/clang_tidy_hash.py"
 | 
			
		||||
      - ".github/workflows/ci-clang-tidy-hash.yml"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										102
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										102
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@@ -177,6 +177,7 @@ jobs:
 | 
			
		||||
      clang-tidy: ${{ steps.determine.outputs.clang-tidy }}
 | 
			
		||||
      python-linters: ${{ steps.determine.outputs.python-linters }}
 | 
			
		||||
      changed-components: ${{ steps.determine.outputs.changed-components }}
 | 
			
		||||
      changed-components-with-tests: ${{ steps.determine.outputs.changed-components-with-tests }}
 | 
			
		||||
      component-test-count: ${{ steps.determine.outputs.component-test-count }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
@@ -204,6 +205,7 @@ jobs:
 | 
			
		||||
          echo "clang-tidy=$(echo "$output" | jq -r '.clang_tidy')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "python-linters=$(echo "$output" | jq -r '.python_linters')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "changed-components=$(echo "$output" | jq -c '.changed_components')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "changed-components-with-tests=$(echo "$output" | jq -c '.changed_components_with_tests')" >> $GITHUB_OUTPUT
 | 
			
		||||
          echo "component-test-count=$(echo "$output" | jq -r '.component_test_count')" >> $GITHUB_OUTPUT
 | 
			
		||||
 | 
			
		||||
  integration-tests:
 | 
			
		||||
@@ -367,12 +369,13 @@ jobs:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      max-parallel: 2
 | 
			
		||||
      matrix:
 | 
			
		||||
        file: ${{ fromJson(needs.determine-jobs.outputs.changed-components) }}
 | 
			
		||||
        file: ${{ fromJson(needs.determine-jobs.outputs.changed-components-with-tests) }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: |
 | 
			
		||||
          sudo apt-get update
 | 
			
		||||
          sudo apt-get install libsdl2-dev
 | 
			
		||||
      - name: Cache apt packages
 | 
			
		||||
        uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3
 | 
			
		||||
        with:
 | 
			
		||||
          packages: libsdl2-dev
 | 
			
		||||
          version: 1.0
 | 
			
		||||
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
@@ -381,17 +384,17 @@ jobs:
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: test_build_components -e config -c ${{ matrix.file }}
 | 
			
		||||
      - name: Validate config for ${{ matrix.file }}
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          ./script/test_build_components -e config -c ${{ matrix.file }}
 | 
			
		||||
      - name: test_build_components -e compile -c ${{ matrix.file }}
 | 
			
		||||
          python3 script/test_build_components.py -e config -c ${{ matrix.file }}
 | 
			
		||||
      - name: Compile config for ${{ matrix.file }}
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          ./script/test_build_components -e compile -c ${{ matrix.file }}
 | 
			
		||||
          python3 script/test_build_components.py -e compile -c ${{ matrix.file }}
 | 
			
		||||
 | 
			
		||||
  test-build-components-splitter:
 | 
			
		||||
    name: Split components for testing into 20 groups maximum
 | 
			
		||||
    name: Split components for intelligent grouping (40 weighted per batch)
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
@@ -402,14 +405,26 @@ jobs:
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
      - name: Split components into 20 groups
 | 
			
		||||
      - 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: |
 | 
			
		||||
          components=$(echo '${{ needs.determine-jobs.outputs.changed-components }}' | jq -c '.[]' | shuf | jq -s -c '[_nwise(20) | join(" ")]')
 | 
			
		||||
          echo "components=$components" >> $GITHUB_OUTPUT
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
 | 
			
		||||
          # Use intelligent splitter that groups components with same bus configs
 | 
			
		||||
          components='${{ needs.determine-jobs.outputs.changed-components-with-tests }}'
 | 
			
		||||
 | 
			
		||||
          echo "Splitting components intelligently..."
 | 
			
		||||
          output=$(python3 script/split_components_for_ci.py --components "$components" --batch-size 40 --output github)
 | 
			
		||||
 | 
			
		||||
          echo "$output" >> $GITHUB_OUTPUT
 | 
			
		||||
 | 
			
		||||
  test-build-components-split:
 | 
			
		||||
    name: Test split components
 | 
			
		||||
    name: Test components batch (${{ matrix.components }})
 | 
			
		||||
    runs-on: ubuntu-24.04
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
@@ -418,17 +433,23 @@ jobs:
 | 
			
		||||
    if: github.event_name == 'pull_request' && fromJSON(needs.determine-jobs.outputs.component-test-count) >= 100
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      max-parallel: 4
 | 
			
		||||
      max-parallel: ${{ (github.base_ref == 'beta' || github.base_ref == 'release') && 8 || 4 }}
 | 
			
		||||
      matrix:
 | 
			
		||||
        components: ${{ fromJson(needs.test-build-components-splitter.outputs.matrix) }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Show disk space
 | 
			
		||||
        run: |
 | 
			
		||||
          echo "Available disk space:"
 | 
			
		||||
          df -h
 | 
			
		||||
 | 
			
		||||
      - name: List components
 | 
			
		||||
        run: echo ${{ matrix.components }}
 | 
			
		||||
 | 
			
		||||
      - name: Install dependencies
 | 
			
		||||
        run: |
 | 
			
		||||
          sudo apt-get update
 | 
			
		||||
          sudo apt-get install libsdl2-dev
 | 
			
		||||
      - name: Cache apt packages
 | 
			
		||||
        uses: awalsh128/cache-apt-pkgs-action@acb598e5ddbc6f68a970c5da0688d2f3a9f04d05 # v1.5.3
 | 
			
		||||
        with:
 | 
			
		||||
          packages: libsdl2-dev
 | 
			
		||||
          version: 1.0
 | 
			
		||||
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
 | 
			
		||||
@@ -437,20 +458,37 @@ jobs:
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: Validate config
 | 
			
		||||
      - name: Validate and compile components with intelligent grouping
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          for component in ${{ matrix.components }}; do
 | 
			
		||||
            ./script/test_build_components -e config -c $component
 | 
			
		||||
          done
 | 
			
		||||
      - name: Compile config
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          mkdir build_cache
 | 
			
		||||
          export PLATFORMIO_BUILD_CACHE_DIR=$PWD/build_cache
 | 
			
		||||
          for component in ${{ matrix.components }}; do
 | 
			
		||||
            ./script/test_build_components -e compile -c $component
 | 
			
		||||
          done
 | 
			
		||||
          # Use /mnt for build files (70GB available vs ~29GB on /)
 | 
			
		||||
          # Bind mount PlatformIO directory to /mnt (tools, packages, build cache all go there)
 | 
			
		||||
          sudo mkdir -p /mnt/platformio
 | 
			
		||||
          sudo chown $USER:$USER /mnt/platformio
 | 
			
		||||
          mkdir -p ~/.platformio
 | 
			
		||||
          sudo mount --bind /mnt/platformio ~/.platformio
 | 
			
		||||
 | 
			
		||||
          # Bind mount test build directory to /mnt
 | 
			
		||||
          sudo mkdir -p /mnt/test_build_components_build
 | 
			
		||||
          sudo chown $USER:$USER /mnt/test_build_components_build
 | 
			
		||||
          mkdir -p tests/test_build_components/build
 | 
			
		||||
          sudo mount --bind /mnt/test_build_components_build tests/test_build_components/build
 | 
			
		||||
 | 
			
		||||
          # Convert space-separated components to comma-separated for Python script
 | 
			
		||||
          components_csv=$(echo "${{ matrix.components }}" | tr ' ' ',')
 | 
			
		||||
 | 
			
		||||
          echo "Testing components: $components_csv"
 | 
			
		||||
          echo ""
 | 
			
		||||
 | 
			
		||||
          # Run config validation with grouping
 | 
			
		||||
          python3 script/test_build_components.py -e config -c "$components_csv" -f
 | 
			
		||||
 | 
			
		||||
          echo ""
 | 
			
		||||
          echo "Config validation passed! Starting compilation..."
 | 
			
		||||
          echo ""
 | 
			
		||||
 | 
			
		||||
          # Run compilation with grouping
 | 
			
		||||
          python3 script/test_build_components.py -e compile -c "$components_csv" -f
 | 
			
		||||
 | 
			
		||||
  pre-commit-ci-lite:
 | 
			
		||||
    name: pre-commit.ci lite
 | 
			
		||||
@@ -466,7 +504,7 @@ jobs:
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - uses: pre-commit/action@2c7b3805fd2a0fd8c1884dcaebf91fc102a13ecd # v3.0.1
 | 
			
		||||
      - uses: esphome/action@43cd1109c09c544d97196f7730ee5b2e0cc6d81e # v3.0.1 fork with pinned actions/cache
 | 
			
		||||
        env:
 | 
			
		||||
          SKIP: pylint,clang-tidy-hash
 | 
			
		||||
      - uses: pre-commit-ci/lite-action@5d6cc0eb514c891a40562a58a8e71576c5c7fb43 # v1.1.0
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										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@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
 | 
			
		||||
        uses: github/codeql-action/init@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
 | 
			
		||||
        with:
 | 
			
		||||
          languages: ${{ matrix.language }}
 | 
			
		||||
          build-mode: ${{ matrix.build-mode }}
 | 
			
		||||
@@ -86,6 +86,6 @@ jobs:
 | 
			
		||||
          exit 1
 | 
			
		||||
 | 
			
		||||
      - name: Perform CodeQL Analysis
 | 
			
		||||
        uses: github/codeql-action/analyze@3599b3baa15b485a2e49ef411a7a4bb2452e7f93 # v3.30.5
 | 
			
		||||
        uses: github/codeql-action/analyze@e296a935590eb16afc0c0108289f68c87e2a89a5 # v4.30.7
 | 
			
		||||
        with:
 | 
			
		||||
          category: "/language:${{matrix.language}}"
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							@@ -19,7 +19,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Stale
 | 
			
		||||
        uses: actions/stale@3a9db7e6a41a89f618792c92c0e97cc736e1b13f # v10.0.0
 | 
			
		||||
        uses: actions/stale@5f858e3efba33a5ca4407a664cc011ad407f2008 # v10.1.0
 | 
			
		||||
        with:
 | 
			
		||||
          debug-only: ${{ github.ref != 'refs/heads/dev' }} # Dry-run when not run on dev branch
 | 
			
		||||
          remove-stale-when-updated: true
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ ci:
 | 
			
		||||
repos:
 | 
			
		||||
  - repo: https://github.com/astral-sh/ruff-pre-commit
 | 
			
		||||
    # Ruff version.
 | 
			
		||||
    rev: v0.13.2
 | 
			
		||||
    rev: v0.14.0
 | 
			
		||||
    hooks:
 | 
			
		||||
      # Run the linter.
 | 
			
		||||
      - id: ruff
 | 
			
		||||
 
 | 
			
		||||
@@ -139,6 +139,7 @@ esphome/components/ens160_base/* @latonita @vincentscode
 | 
			
		||||
esphome/components/ens160_i2c/* @latonita
 | 
			
		||||
esphome/components/ens160_spi/* @latonita
 | 
			
		||||
esphome/components/ens210/* @itn3rd77
 | 
			
		||||
esphome/components/epaper_spi/* @esphome/core
 | 
			
		||||
esphome/components/es7210/* @kahrendt
 | 
			
		||||
esphome/components/es7243e/* @kbx81
 | 
			
		||||
esphome/components/es8156/* @kbx81
 | 
			
		||||
@@ -160,7 +161,6 @@ esphome/components/esp_ldo/* @clydebarrow
 | 
			
		||||
esphome/components/espnow/* @jesserockz
 | 
			
		||||
esphome/components/ethernet_info/* @gtjadsonsantos
 | 
			
		||||
esphome/components/event/* @nohat
 | 
			
		||||
esphome/components/event_emitter/* @Rapsssito
 | 
			
		||||
esphome/components/exposure_notifications/* @OttoWinter
 | 
			
		||||
esphome/components/ezo/* @ssieb
 | 
			
		||||
esphome/components/ezo_pmp/* @carlos-sarmiento
 | 
			
		||||
@@ -257,6 +257,7 @@ esphome/components/libretiny_pwm/* @kuba2k2
 | 
			
		||||
esphome/components/light/* @esphome/core
 | 
			
		||||
esphome/components/lightwaverf/* @max246
 | 
			
		||||
esphome/components/lilygo_t5_47/touchscreen/* @jesserockz
 | 
			
		||||
esphome/components/lm75b/* @beormund
 | 
			
		||||
esphome/components/ln882x/* @lamauny
 | 
			
		||||
esphome/components/lock/* @esphome/core
 | 
			
		||||
esphome/components/logger/* @esphome/core
 | 
			
		||||
@@ -429,6 +430,7 @@ esphome/components/speaker/media_player/* @kahrendt @synesthesiam
 | 
			
		||||
esphome/components/spi/* @clydebarrow @esphome/core
 | 
			
		||||
esphome/components/spi_device/* @clydebarrow
 | 
			
		||||
esphome/components/spi_led_strip/* @clydebarrow
 | 
			
		||||
esphome/components/split_buffer/* @jesserockz
 | 
			
		||||
esphome/components/sprinkler/* @kbx81
 | 
			
		||||
esphome/components/sps30/* @martgras
 | 
			
		||||
esphome/components/ssd1322_base/* @kbx81
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								Doxyfile
									
									
									
									
									
								
							
							
						
						
									
										2
									
								
								Doxyfile
									
									
									
									
									
								
							@@ -48,7 +48,7 @@ PROJECT_NAME           = ESPHome
 | 
			
		||||
# could be handy for archiving the generated documentation or if some version
 | 
			
		||||
# control system is used.
 | 
			
		||||
 | 
			
		||||
PROJECT_NUMBER         = 2025.10.0-dev
 | 
			
		||||
PROJECT_NUMBER         = 2025.10.0b2
 | 
			
		||||
 | 
			
		||||
# Using the PROJECT_BRIEF tag one can provide an optional one line description
 | 
			
		||||
# for a project that appears at the top of each page and should give viewer a
 | 
			
		||||
 
 | 
			
		||||
@@ -14,9 +14,11 @@ from typing import Protocol
 | 
			
		||||
 | 
			
		||||
import argcomplete
 | 
			
		||||
 | 
			
		||||
# Note: Do not import modules from esphome.components here, as this would
 | 
			
		||||
# cause them to be loaded before external components are processed, resulting
 | 
			
		||||
# in the built-in version being used instead of the external component one.
 | 
			
		||||
from esphome import const, writer, yaml_util
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components.mqtt import CONF_DISCOVER_IP
 | 
			
		||||
from esphome.config import iter_component_configs, read_config, strip_default_ids
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    ALLOWED_NAME_CHARS,
 | 
			
		||||
@@ -240,6 +242,8 @@ def has_ota() -> bool:
 | 
			
		||||
 | 
			
		||||
def has_mqtt_ip_lookup() -> bool:
 | 
			
		||||
    """Check if MQTT is available and IP lookup is supported."""
 | 
			
		||||
    from esphome.components.mqtt import CONF_DISCOVER_IP
 | 
			
		||||
 | 
			
		||||
    if CONF_MQTT not in CORE.config:
 | 
			
		||||
        return False
 | 
			
		||||
    # Default Enabled
 | 
			
		||||
@@ -998,6 +1002,12 @@ def parse_args(argv):
 | 
			
		||||
        action="append",
 | 
			
		||||
        default=[],
 | 
			
		||||
    )
 | 
			
		||||
    options_parser.add_argument(
 | 
			
		||||
        "--testing-mode",
 | 
			
		||||
        help="Enable testing mode (disables validation checks for grouped component testing)",
 | 
			
		||||
        action="store_true",
 | 
			
		||||
        default=False,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    parser = argparse.ArgumentParser(
 | 
			
		||||
        description=f"ESPHome {const.__version__}", parents=[options_parser]
 | 
			
		||||
@@ -1256,6 +1266,7 @@ def run_esphome(argv):
 | 
			
		||||
 | 
			
		||||
    args = parse_args(argv)
 | 
			
		||||
    CORE.dashboard = args.dashboard
 | 
			
		||||
    CORE.testing_mode = args.testing_mode
 | 
			
		||||
 | 
			
		||||
    # Create address cache from command-line arguments
 | 
			
		||||
    CORE.address_cache = AddressCache.from_cli_args(
 | 
			
		||||
 
 | 
			
		||||
@@ -26,12 +26,12 @@ uint32_t Animation::get_animation_frame_count() const { return this->animation_f
 | 
			
		||||
int Animation::get_current_frame() const { return this->current_frame_; }
 | 
			
		||||
void Animation::next_frame() {
 | 
			
		||||
  this->current_frame_++;
 | 
			
		||||
  if (loop_count_ && this->current_frame_ == loop_end_frame_ &&
 | 
			
		||||
  if (loop_count_ && static_cast<uint32_t>(this->current_frame_) == loop_end_frame_ &&
 | 
			
		||||
      (this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) {
 | 
			
		||||
    this->current_frame_ = loop_start_frame_;
 | 
			
		||||
    this->loop_current_iteration_++;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->current_frame_ >= animation_frame_count_) {
 | 
			
		||||
  if (static_cast<uint32_t>(this->current_frame_) >= animation_frame_count_) {
 | 
			
		||||
    this->loop_current_iteration_ = 1;
 | 
			
		||||
    this->current_frame_ = 0;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -9,37 +9,59 @@ import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ACTION,
 | 
			
		||||
    CONF_ACTIONS,
 | 
			
		||||
    CONF_CAPTURE_RESPONSE,
 | 
			
		||||
    CONF_DATA,
 | 
			
		||||
    CONF_DATA_TEMPLATE,
 | 
			
		||||
    CONF_EVENT,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_KEY,
 | 
			
		||||
    CONF_MAX_CONNECTIONS,
 | 
			
		||||
    CONF_ON_CLIENT_CONNECTED,
 | 
			
		||||
    CONF_ON_CLIENT_DISCONNECTED,
 | 
			
		||||
    CONF_ON_ERROR,
 | 
			
		||||
    CONF_ON_SUCCESS,
 | 
			
		||||
    CONF_PASSWORD,
 | 
			
		||||
    CONF_PORT,
 | 
			
		||||
    CONF_REBOOT_TIMEOUT,
 | 
			
		||||
    CONF_RESPONSE_TEMPLATE,
 | 
			
		||||
    CONF_SERVICE,
 | 
			
		||||
    CONF_SERVICES,
 | 
			
		||||
    CONF_TAG,
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
    CONF_VARIABLES,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.core import CORE, ID, CoroPriority, coroutine_with_priority
 | 
			
		||||
from esphome.cpp_generator import TemplateArgsType
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
DOMAIN = "api"
 | 
			
		||||
DEPENDENCIES = ["network"]
 | 
			
		||||
AUTO_LOAD = ["socket"]
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def AUTO_LOAD(config: ConfigType) -> list[str]:
 | 
			
		||||
    """Conditionally auto-load json only when capture_response is used."""
 | 
			
		||||
    base = ["socket"]
 | 
			
		||||
 | 
			
		||||
    # Check if any homeassistant.action/homeassistant.service has capture_response: true
 | 
			
		||||
    # This flag is set during config validation in _validate_response_config
 | 
			
		||||
    if not config or CORE.data.get(DOMAIN, {}).get(CONF_CAPTURE_RESPONSE, False):
 | 
			
		||||
        return base + ["json"]
 | 
			
		||||
 | 
			
		||||
    return base
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
api_ns = cg.esphome_ns.namespace("api")
 | 
			
		||||
APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)
 | 
			
		||||
HomeAssistantServiceCallAction = api_ns.class_(
 | 
			
		||||
    "HomeAssistantServiceCallAction", automation.Action
 | 
			
		||||
)
 | 
			
		||||
ActionResponse = api_ns.class_("ActionResponse")
 | 
			
		||||
HomeAssistantActionResponseTrigger = api_ns.class_(
 | 
			
		||||
    "HomeAssistantActionResponseTrigger", automation.Trigger
 | 
			
		||||
)
 | 
			
		||||
APIConnectedCondition = api_ns.class_("APIConnectedCondition", Condition)
 | 
			
		||||
 | 
			
		||||
UserServiceTrigger = api_ns.class_("UserServiceTrigger", automation.Trigger)
 | 
			
		||||
@@ -60,7 +82,7 @@ CONF_CUSTOM_SERVICES = "custom_services"
 | 
			
		||||
CONF_HOMEASSISTANT_SERVICES = "homeassistant_services"
 | 
			
		||||
CONF_HOMEASSISTANT_STATES = "homeassistant_states"
 | 
			
		||||
CONF_LISTEN_BACKLOG = "listen_backlog"
 | 
			
		||||
CONF_MAX_CONNECTIONS = "max_connections"
 | 
			
		||||
CONF_MAX_SEND_QUEUE = "max_send_queue"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_encryption_key(value):
 | 
			
		||||
@@ -183,6 +205,19 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
                host=8,  # Abundant resources
 | 
			
		||||
                ln882x=8,  # Moderate RAM
 | 
			
		||||
            ): cv.int_range(min=1, max=20),
 | 
			
		||||
            # Maximum queued send buffers per connection before dropping connection
 | 
			
		||||
            # Each buffer uses ~8-12 bytes overhead plus actual message size
 | 
			
		||||
            # Platform defaults based on available RAM and typical message rates:
 | 
			
		||||
            cv.SplitDefault(
 | 
			
		||||
                CONF_MAX_SEND_QUEUE,
 | 
			
		||||
                esp8266=5,  # Limited RAM, need to fail fast
 | 
			
		||||
                esp32=8,  # More RAM, can buffer more
 | 
			
		||||
                rp2040=5,  # Limited RAM
 | 
			
		||||
                bk72xx=8,  # Moderate RAM
 | 
			
		||||
                rtl87xx=8,  # Moderate RAM
 | 
			
		||||
                host=16,  # Abundant resources
 | 
			
		||||
                ln882x=8,  # Moderate RAM
 | 
			
		||||
            ): cv.int_range(min=1, max=64),
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    cv.rename_key(CONF_SERVICES, CONF_ACTIONS),
 | 
			
		||||
@@ -205,6 +240,7 @@ async def to_code(config):
 | 
			
		||||
        cg.add(var.set_listen_backlog(config[CONF_LISTEN_BACKLOG]))
 | 
			
		||||
    if CONF_MAX_CONNECTIONS in config:
 | 
			
		||||
        cg.add(var.set_max_connections(config[CONF_MAX_CONNECTIONS]))
 | 
			
		||||
    cg.add_define("API_MAX_SEND_QUEUE", config[CONF_MAX_SEND_QUEUE])
 | 
			
		||||
 | 
			
		||||
    # Set USE_API_SERVICES if any services are enabled
 | 
			
		||||
    if config.get(CONF_ACTIONS) or config[CONF_CUSTOM_SERVICES]:
 | 
			
		||||
@@ -273,6 +309,29 @@ async def to_code(config):
 | 
			
		||||
KEY_VALUE_SCHEMA = cv.Schema({cv.string: cv.templatable(cv.string_strict)})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _validate_response_config(config: ConfigType) -> ConfigType:
 | 
			
		||||
    # Validate dependencies:
 | 
			
		||||
    # - response_template requires capture_response: true
 | 
			
		||||
    # - capture_response: true requires on_success
 | 
			
		||||
    if CONF_RESPONSE_TEMPLATE in config and not config[CONF_CAPTURE_RESPONSE]:
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            f"`{CONF_RESPONSE_TEMPLATE}` requires `{CONF_CAPTURE_RESPONSE}: true` to be set.",
 | 
			
		||||
            path=[CONF_RESPONSE_TEMPLATE],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if config[CONF_CAPTURE_RESPONSE] and CONF_ON_SUCCESS not in config:
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            f"`{CONF_CAPTURE_RESPONSE}: true` requires `{CONF_ON_SUCCESS}` to be set.",
 | 
			
		||||
            path=[CONF_CAPTURE_RESPONSE],
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    # Track if any action uses capture_response for AUTO_LOAD
 | 
			
		||||
    if config[CONF_CAPTURE_RESPONSE]:
 | 
			
		||||
        CORE.data.setdefault(DOMAIN, {})[CONF_CAPTURE_RESPONSE] = True
 | 
			
		||||
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
@@ -288,10 +347,15 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
 | 
			
		||||
            cv.Optional(CONF_VARIABLES, default={}): cv.Schema(
 | 
			
		||||
                {cv.string: cv.returning_lambda}
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_RESPONSE_TEMPLATE): cv.templatable(cv.string),
 | 
			
		||||
            cv.Optional(CONF_CAPTURE_RESPONSE, default=False): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_ON_SUCCESS): automation.validate_automation(single=True),
 | 
			
		||||
            cv.Optional(CONF_ON_ERROR): automation.validate_automation(single=True),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
    cv.has_exactly_one_key(CONF_SERVICE, CONF_ACTION),
 | 
			
		||||
    cv.rename_key(CONF_SERVICE, CONF_ACTION),
 | 
			
		||||
    _validate_response_config,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -305,7 +369,12 @@ HOMEASSISTANT_ACTION_ACTION_SCHEMA = cv.All(
 | 
			
		||||
    HomeAssistantServiceCallAction,
 | 
			
		||||
    HOMEASSISTANT_ACTION_ACTION_SCHEMA,
 | 
			
		||||
)
 | 
			
		||||
async def homeassistant_service_to_code(config, action_id, template_arg, args):
 | 
			
		||||
async def homeassistant_service_to_code(
 | 
			
		||||
    config: ConfigType,
 | 
			
		||||
    action_id: ID,
 | 
			
		||||
    template_arg: cg.TemplateArguments,
 | 
			
		||||
    args: TemplateArgsType,
 | 
			
		||||
):
 | 
			
		||||
    cg.add_define("USE_API_HOMEASSISTANT_SERVICES")
 | 
			
		||||
    serv = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, serv, False)
 | 
			
		||||
@@ -320,6 +389,40 @@ async def homeassistant_service_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    for key, value in config[CONF_VARIABLES].items():
 | 
			
		||||
        templ = await cg.templatable(value, args, None)
 | 
			
		||||
        cg.add(var.add_variable(key, templ))
 | 
			
		||||
 | 
			
		||||
    if on_error := config.get(CONF_ON_ERROR):
 | 
			
		||||
        cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
 | 
			
		||||
        cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_ERRORS")
 | 
			
		||||
        cg.add(var.set_wants_status())
 | 
			
		||||
        await automation.build_automation(
 | 
			
		||||
            var.get_error_trigger(),
 | 
			
		||||
            [(cg.std_string, "error"), *args],
 | 
			
		||||
            on_error,
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if on_success := config.get(CONF_ON_SUCCESS):
 | 
			
		||||
        cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES")
 | 
			
		||||
        cg.add(var.set_wants_status())
 | 
			
		||||
        if config[CONF_CAPTURE_RESPONSE]:
 | 
			
		||||
            cg.add(var.set_wants_response())
 | 
			
		||||
            cg.add_define("USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON")
 | 
			
		||||
            await automation.build_automation(
 | 
			
		||||
                var.get_success_trigger_with_response(),
 | 
			
		||||
                [(cg.JsonObjectConst, "response"), *args],
 | 
			
		||||
                on_success,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
            if response_template := config.get(CONF_RESPONSE_TEMPLATE):
 | 
			
		||||
                templ = await cg.templatable(response_template, args, cg.std_string)
 | 
			
		||||
                cg.add(var.set_response_template(templ))
 | 
			
		||||
 | 
			
		||||
        else:
 | 
			
		||||
            await automation.build_automation(
 | 
			
		||||
                var.get_success_trigger(),
 | 
			
		||||
                args,
 | 
			
		||||
                on_success,
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -780,6 +780,22 @@ message HomeassistantActionRequest {
 | 
			
		||||
  repeated HomeassistantServiceMap data_template = 3;
 | 
			
		||||
  repeated HomeassistantServiceMap variables = 4;
 | 
			
		||||
  bool is_event = 5;
 | 
			
		||||
  uint32 call_id = 6 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES"];
 | 
			
		||||
  bool wants_response = 7 [(field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
 | 
			
		||||
  string response_template = 8 [(no_zero_copy) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Message sent by Home Assistant to ESPHome with service call response data
 | 
			
		||||
message HomeassistantActionResponse {
 | 
			
		||||
  option (id) = 130;
 | 
			
		||||
  option (source) = SOURCE_CLIENT;
 | 
			
		||||
  option (no_delay) = true;
 | 
			
		||||
  option (ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES";
 | 
			
		||||
 | 
			
		||||
  uint32 call_id = 1; // Matches the call_id from HomeassistantActionRequest
 | 
			
		||||
  bool success = 2; // Whether the service call succeeded
 | 
			
		||||
  string error_message = 3; // Error message if success = false
 | 
			
		||||
  bytes response_data = 4 [(pointer_to_buffer) = true, (field_ifdef) = "USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON"];
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// ==================== IMPORT HOME ASSISTANT STATES ====================
 | 
			
		||||
 
 | 
			
		||||
@@ -8,9 +8,9 @@
 | 
			
		||||
#endif
 | 
			
		||||
#include <cerrno>
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <limits>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include "esphome/components/network/util.h"
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/entity_base.h"
 | 
			
		||||
@@ -116,8 +116,7 @@ void APIConnection::start() {
 | 
			
		||||
 | 
			
		||||
  APIError err = this->helper_->init();
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    this->log_warning_(LOG_STR("Helper init failed"), err);
 | 
			
		||||
    this->fatal_error_with_log_(LOG_STR("Helper init failed"), err);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->client_info_.peername = helper_->getpeername();
 | 
			
		||||
@@ -147,8 +146,7 @@ void APIConnection::loop() {
 | 
			
		||||
 | 
			
		||||
  APIError err = this->helper_->loop();
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    this->log_socket_operation_failed_(err);
 | 
			
		||||
    this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -163,17 +161,13 @@ void APIConnection::loop() {
 | 
			
		||||
        // No more data available
 | 
			
		||||
        break;
 | 
			
		||||
      } else if (err != APIError::OK) {
 | 
			
		||||
        on_fatal_error();
 | 
			
		||||
        this->log_warning_(LOG_STR("Reading failed"), err);
 | 
			
		||||
        this->fatal_error_with_log_(LOG_STR("Reading failed"), err);
 | 
			
		||||
        return;
 | 
			
		||||
      } else {
 | 
			
		||||
        this->last_traffic_ = now;
 | 
			
		||||
        // read a packet
 | 
			
		||||
        if (buffer.data_len > 0) {
 | 
			
		||||
          this->read_message(buffer.data_len, buffer.type, &buffer.container[buffer.data_offset]);
 | 
			
		||||
        } else {
 | 
			
		||||
          this->read_message(0, buffer.type, nullptr);
 | 
			
		||||
        }
 | 
			
		||||
        this->read_message(buffer.data_len, buffer.type,
 | 
			
		||||
                           buffer.data_len > 0 ? &buffer.container[buffer.data_offset] : nullptr);
 | 
			
		||||
        if (this->flags_.remove)
 | 
			
		||||
          return;
 | 
			
		||||
      }
 | 
			
		||||
@@ -205,7 +199,8 @@ void APIConnection::loop() {
 | 
			
		||||
    // Disconnect if not responded within 2.5*keepalive
 | 
			
		||||
    if (now - this->last_traffic_ > KEEPALIVE_DISCONNECT_TIMEOUT) {
 | 
			
		||||
      on_fatal_error();
 | 
			
		||||
      ESP_LOGW(TAG, "%s is unresponsive; disconnecting", this->get_client_combined_info().c_str());
 | 
			
		||||
      ESP_LOGW(TAG, "%s (%s) is unresponsive; disconnecting", this->client_info_.name.c_str(),
 | 
			
		||||
               this->client_info_.peername.c_str());
 | 
			
		||||
    }
 | 
			
		||||
  } else if (now - this->last_traffic_ > KEEPALIVE_TIMEOUT_MS && !this->flags_.remove) {
 | 
			
		||||
    // Only send ping if we're not disconnecting
 | 
			
		||||
@@ -255,7 +250,7 @@ bool APIConnection::send_disconnect_response(const DisconnectRequest &msg) {
 | 
			
		||||
  // remote initiated disconnect_client
 | 
			
		||||
  // don't close yet, we still need to send the disconnect response
 | 
			
		||||
  // close will happen on next loop
 | 
			
		||||
  ESP_LOGD(TAG, "%s disconnected", this->get_client_combined_info().c_str());
 | 
			
		||||
  ESP_LOGD(TAG, "%s (%s) disconnected", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
 | 
			
		||||
  this->flags_.next_close = true;
 | 
			
		||||
  DisconnectResponse resp;
 | 
			
		||||
  return this->send_message(resp, DisconnectResponse::MESSAGE_TYPE);
 | 
			
		||||
@@ -1385,7 +1380,7 @@ void APIConnection::complete_authentication_() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->flags_.connection_state = static_cast<uint8_t>(ConnectionState::AUTHENTICATED);
 | 
			
		||||
  ESP_LOGD(TAG, "%s connected", this->get_client_combined_info().c_str());
 | 
			
		||||
  ESP_LOGD(TAG, "%s (%s) connected", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
 | 
			
		||||
#ifdef USE_API_CLIENT_CONNECTED_TRIGGER
 | 
			
		||||
  this->parent_->get_client_connected_trigger()->trigger(this->client_info_.name, this->client_info_.peername);
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1394,6 +1389,11 @@ void APIConnection::complete_authentication_() {
 | 
			
		||||
    this->send_time_request();
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ZWAVE_PROXY
 | 
			
		||||
  if (zwave_proxy::global_zwave_proxy != nullptr) {
 | 
			
		||||
    zwave_proxy::global_zwave_proxy->api_connection_authenticated(this);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool APIConnection::send_hello_response(const HelloRequest &msg) {
 | 
			
		||||
@@ -1549,6 +1549,20 @@ void APIConnection::execute_service(const ExecuteServiceRequest &msg) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
void APIConnection::on_homeassistant_action_response(const HomeassistantActionResponse &msg) {
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  if (msg.response_data_len > 0) {
 | 
			
		||||
    this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message, msg.response_data,
 | 
			
		||||
                                          msg.response_data_len);
 | 
			
		||||
  } else
 | 
			
		||||
#endif
 | 
			
		||||
  {
 | 
			
		||||
    this->parent_->handle_action_response(msg.call_id, msg.success, msg.error_message);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_NOISE
 | 
			
		||||
bool APIConnection::send_noise_encryption_set_key_response(const NoiseEncryptionSetKeyRequest &msg) {
 | 
			
		||||
  NoiseEncryptionSetKeyResponse resp;
 | 
			
		||||
@@ -1579,8 +1593,7 @@ bool APIConnection::try_to_clear_buffer(bool log_out_of_space) {
 | 
			
		||||
  delay(0);
 | 
			
		||||
  APIError err = this->helper_->loop();
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    this->log_socket_operation_failed_(err);
 | 
			
		||||
    this->fatal_error_with_log_(LOG_STR("Socket operation failed"), err);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->helper_->can_write_without_blocking())
 | 
			
		||||
@@ -1599,8 +1612,7 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
 | 
			
		||||
  if (err == APIError::WOULD_BLOCK)
 | 
			
		||||
    return false;
 | 
			
		||||
  if (err != APIError::OK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    this->log_warning_(LOG_STR("Packet write failed"), err);
 | 
			
		||||
    this->fatal_error_with_log_(LOG_STR("Packet write failed"), err);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  // Do not set last_traffic_ on send
 | 
			
		||||
@@ -1609,12 +1621,12 @@ bool APIConnection::send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) {
 | 
			
		||||
#ifdef USE_API_PASSWORD
 | 
			
		||||
void APIConnection::on_unauthenticated_access() {
 | 
			
		||||
  this->on_fatal_error();
 | 
			
		||||
  ESP_LOGD(TAG, "%s access without authentication", this->get_client_combined_info().c_str());
 | 
			
		||||
  ESP_LOGD(TAG, "%s (%s) no authentication", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
void APIConnection::on_no_setup_connection() {
 | 
			
		||||
  this->on_fatal_error();
 | 
			
		||||
  ESP_LOGD(TAG, "%s access without full connection", this->get_client_combined_info().c_str());
 | 
			
		||||
  ESP_LOGD(TAG, "%s (%s) no connection setup", this->client_info_.name.c_str(), this->client_info_.peername.c_str());
 | 
			
		||||
}
 | 
			
		||||
void APIConnection::on_fatal_error() {
 | 
			
		||||
  this->helper_->close();
 | 
			
		||||
@@ -1786,8 +1798,7 @@ void APIConnection::process_batch_() {
 | 
			
		||||
  APIError err = this->helper_->write_protobuf_packets(ProtoWriteBuffer{&shared_buf},
 | 
			
		||||
                                                       std::span<const PacketInfo>(packet_info, packet_count));
 | 
			
		||||
  if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
 | 
			
		||||
    on_fatal_error();
 | 
			
		||||
    this->log_warning_(LOG_STR("Batch write failed"), err);
 | 
			
		||||
    this->fatal_error_with_log_(LOG_STR("Batch write failed"), err);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
@@ -1866,12 +1877,8 @@ void APIConnection::process_state_subscriptions_() {
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
 | 
			
		||||
void APIConnection::log_warning_(const LogString *message, APIError err) {
 | 
			
		||||
  ESP_LOGW(TAG, "%s: %s %s errno=%d", this->get_client_combined_info().c_str(), LOG_STR_ARG(message),
 | 
			
		||||
           LOG_STR_ARG(api_error_to_logstr(err)), errno);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void APIConnection::log_socket_operation_failed_(APIError err) {
 | 
			
		||||
  this->log_warning_(LOG_STR("Socket operation failed"), err);
 | 
			
		||||
  ESP_LOGW(TAG, "%s (%s): %s %s errno=%d", this->client_info_.name.c_str(), this->client_info_.peername.c_str(),
 | 
			
		||||
           LOG_STR_ARG(message), LOG_STR_ARG(api_error_to_logstr(err)), errno);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::api
 | 
			
		||||
 
 | 
			
		||||
@@ -19,14 +19,6 @@ namespace esphome::api {
 | 
			
		||||
struct ClientInfo {
 | 
			
		||||
  std::string name;      // Client name from Hello message
 | 
			
		||||
  std::string peername;  // IP:port from socket
 | 
			
		||||
 | 
			
		||||
  std::string get_combined_info() const {
 | 
			
		||||
    if (name == peername) {
 | 
			
		||||
      // Before Hello message, both are the same
 | 
			
		||||
      return name;
 | 
			
		||||
    }
 | 
			
		||||
    return name + " (" + peername + ")";
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Keepalive timeout in milliseconds
 | 
			
		||||
@@ -137,7 +129,10 @@ class APIConnection final : public APIServerConnection {
 | 
			
		||||
      return;
 | 
			
		||||
    this->send_message(call, HomeassistantActionRequest::MESSAGE_TYPE);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  void on_homeassistant_action_response(const HomeassistantActionResponse &msg) override;
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
#ifdef USE_BLUETOOTH_PROXY
 | 
			
		||||
  void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override;
 | 
			
		||||
  void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override;
 | 
			
		||||
@@ -278,7 +273,8 @@ class APIConnection final : public APIServerConnection {
 | 
			
		||||
  bool try_to_clear_buffer(bool log_out_of_space);
 | 
			
		||||
  bool send_buffer(ProtoWriteBuffer buffer, uint8_t message_type) override;
 | 
			
		||||
 | 
			
		||||
  std::string get_client_combined_info() const { return this->client_info_.get_combined_info(); }
 | 
			
		||||
  const std::string &get_name() const { return this->client_info_.name; }
 | 
			
		||||
  const std::string &get_peername() const { return this->client_info_.peername; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  // Helper function to handle authentication completion
 | 
			
		||||
@@ -739,8 +735,11 @@ class APIConnection final : public APIServerConnection {
 | 
			
		||||
 | 
			
		||||
  // Helper function to log API errors with errno
 | 
			
		||||
  void log_warning_(const LogString *message, APIError err);
 | 
			
		||||
  // Specific helper for duplicated error message
 | 
			
		||||
  void log_socket_operation_failed_(APIError err);
 | 
			
		||||
  // Helper to handle fatal errors with logging
 | 
			
		||||
  inline void fatal_error_with_log_(const LogString *message, APIError err) {
 | 
			
		||||
    this->on_fatal_error();
 | 
			
		||||
    this->log_warning_(message, err);
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::api
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,8 @@ namespace esphome::api {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "api.frame_helper";
 | 
			
		||||
 | 
			
		||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__)
 | 
			
		||||
#define HELPER_LOG(msg, ...) \
 | 
			
		||||
  ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
 | 
			
		||||
 | 
			
		||||
#ifdef HELPER_LOG_PACKETS
 | 
			
		||||
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
 | 
			
		||||
@@ -80,7 +81,7 @@ const LogString *api_error_to_logstr(APIError err) {
 | 
			
		||||
 | 
			
		||||
// Default implementation for loop - handles sending buffered data
 | 
			
		||||
APIError APIFrameHelper::loop() {
 | 
			
		||||
  if (!this->tx_buf_.empty()) {
 | 
			
		||||
  if (this->tx_buf_count_ > 0) {
 | 
			
		||||
    APIError err = try_send_tx_buf_();
 | 
			
		||||
    if (err != APIError::OK && err != APIError::WOULD_BLOCK) {
 | 
			
		||||
      return err;
 | 
			
		||||
@@ -102,9 +103,20 @@ APIError APIFrameHelper::handle_socket_write_error_() {
 | 
			
		||||
// Helper method to buffer data from IOVs
 | 
			
		||||
void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt, uint16_t total_write_len,
 | 
			
		||||
                                           uint16_t offset) {
 | 
			
		||||
  SendBuffer buffer;
 | 
			
		||||
  buffer.size = total_write_len - offset;
 | 
			
		||||
  buffer.data = std::make_unique<uint8_t[]>(buffer.size);
 | 
			
		||||
  // Check if queue is full
 | 
			
		||||
  if (this->tx_buf_count_ >= API_MAX_SEND_QUEUE) {
 | 
			
		||||
    HELPER_LOG("Send queue full (%u buffers), dropping connection", this->tx_buf_count_);
 | 
			
		||||
    this->state_ = State::FAILED;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint16_t buffer_size = total_write_len - offset;
 | 
			
		||||
  auto &buffer = this->tx_buf_[this->tx_buf_tail_];
 | 
			
		||||
  buffer = std::make_unique<SendBuffer>(SendBuffer{
 | 
			
		||||
      .data = std::make_unique<uint8_t[]>(buffer_size),
 | 
			
		||||
      .size = buffer_size,
 | 
			
		||||
      .offset = 0,
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  uint16_t to_skip = offset;
 | 
			
		||||
  uint16_t write_pos = 0;
 | 
			
		||||
@@ -117,12 +129,15 @@ void APIFrameHelper::buffer_data_from_iov_(const struct iovec *iov, int iovcnt,
 | 
			
		||||
      // Include this segment (partially or fully)
 | 
			
		||||
      const uint8_t *src = reinterpret_cast<uint8_t *>(iov[i].iov_base) + to_skip;
 | 
			
		||||
      uint16_t len = static_cast<uint16_t>(iov[i].iov_len) - to_skip;
 | 
			
		||||
      std::memcpy(buffer.data.get() + write_pos, src, len);
 | 
			
		||||
      std::memcpy(buffer->data.get() + write_pos, src, len);
 | 
			
		||||
      write_pos += len;
 | 
			
		||||
      to_skip = 0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  this->tx_buf_.push_back(std::move(buffer));
 | 
			
		||||
 | 
			
		||||
  // Update circular buffer tracking
 | 
			
		||||
  this->tx_buf_tail_ = (this->tx_buf_tail_ + 1) % API_MAX_SEND_QUEUE;
 | 
			
		||||
  this->tx_buf_count_++;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// This method writes data to socket or buffers it
 | 
			
		||||
@@ -140,7 +155,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Try to send any existing buffered data first if there is any
 | 
			
		||||
  if (!this->tx_buf_.empty()) {
 | 
			
		||||
  if (this->tx_buf_count_ > 0) {
 | 
			
		||||
    APIError send_result = try_send_tx_buf_();
 | 
			
		||||
    // If real error occurred (not just WOULD_BLOCK), return it
 | 
			
		||||
    if (send_result != APIError::OK && send_result != APIError::WOULD_BLOCK) {
 | 
			
		||||
@@ -149,7 +164,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_
 | 
			
		||||
 | 
			
		||||
    // If there is still data in the buffer, we can't send, buffer
 | 
			
		||||
    // the new data and return
 | 
			
		||||
    if (!this->tx_buf_.empty()) {
 | 
			
		||||
    if (this->tx_buf_count_ > 0) {
 | 
			
		||||
      this->buffer_data_from_iov_(iov, iovcnt, total_write_len, 0);
 | 
			
		||||
      return APIError::OK;  // Success, data buffered
 | 
			
		||||
    }
 | 
			
		||||
@@ -177,32 +192,31 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Common implementation for trying to send buffered data
 | 
			
		||||
// IMPORTANT: Caller MUST ensure tx_buf_ is not empty before calling this method
 | 
			
		||||
// IMPORTANT: Caller MUST ensure tx_buf_count_ > 0 before calling this method
 | 
			
		||||
APIError APIFrameHelper::try_send_tx_buf_() {
 | 
			
		||||
  // Try to send from tx_buf - we assume it's not empty as it's the caller's responsibility to check
 | 
			
		||||
  bool tx_buf_empty = false;
 | 
			
		||||
  while (!tx_buf_empty) {
 | 
			
		||||
  while (this->tx_buf_count_ > 0) {
 | 
			
		||||
    // Get the first buffer in the queue
 | 
			
		||||
    SendBuffer &front_buffer = this->tx_buf_.front();
 | 
			
		||||
    SendBuffer *front_buffer = this->tx_buf_[this->tx_buf_head_].get();
 | 
			
		||||
 | 
			
		||||
    // Try to send the remaining data in this buffer
 | 
			
		||||
    ssize_t sent = this->socket_->write(front_buffer.current_data(), front_buffer.remaining());
 | 
			
		||||
    ssize_t sent = this->socket_->write(front_buffer->current_data(), front_buffer->remaining());
 | 
			
		||||
 | 
			
		||||
    if (sent == -1) {
 | 
			
		||||
      return this->handle_socket_write_error_();
 | 
			
		||||
    } else if (sent == 0) {
 | 
			
		||||
      // Nothing sent but not an error
 | 
			
		||||
      return APIError::WOULD_BLOCK;
 | 
			
		||||
    } else if (static_cast<uint16_t>(sent) < front_buffer.remaining()) {
 | 
			
		||||
    } else if (static_cast<uint16_t>(sent) < front_buffer->remaining()) {
 | 
			
		||||
      // Partially sent, update offset
 | 
			
		||||
      // Cast to ensure no overflow issues with uint16_t
 | 
			
		||||
      front_buffer.offset += static_cast<uint16_t>(sent);
 | 
			
		||||
      front_buffer->offset += static_cast<uint16_t>(sent);
 | 
			
		||||
      return APIError::WOULD_BLOCK;  // Stop processing more buffers if we couldn't send a complete buffer
 | 
			
		||||
    } else {
 | 
			
		||||
      // Buffer completely sent, remove it from the queue
 | 
			
		||||
      this->tx_buf_.pop_front();
 | 
			
		||||
      // Update empty status for the loop condition
 | 
			
		||||
      tx_buf_empty = this->tx_buf_.empty();
 | 
			
		||||
      this->tx_buf_[this->tx_buf_head_].reset();
 | 
			
		||||
      this->tx_buf_head_ = (this->tx_buf_head_ + 1) % API_MAX_SEND_QUEUE;
 | 
			
		||||
      this->tx_buf_count_--;
 | 
			
		||||
      // Continue loop to try sending the next buffer
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,8 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include <array>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <deque>
 | 
			
		||||
#include <limits>
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <span>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
@@ -17,6 +18,17 @@ namespace esphome::api {
 | 
			
		||||
// uncomment to log raw packets
 | 
			
		||||
//#define HELPER_LOG_PACKETS
 | 
			
		||||
 | 
			
		||||
// Maximum message size limits to prevent OOM on constrained devices
 | 
			
		||||
// Handshake messages are limited to a small size for security
 | 
			
		||||
static constexpr uint16_t MAX_HANDSHAKE_SIZE = 128;
 | 
			
		||||
 | 
			
		||||
// Data message limits vary by platform based on available memory
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
static constexpr uint16_t MAX_MESSAGE_SIZE = 8192;  // 8 KiB for ESP8266
 | 
			
		||||
#else
 | 
			
		||||
static constexpr uint16_t MAX_MESSAGE_SIZE = 32768;  // 32 KiB for ESP32 and other platforms
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
// Forward declaration
 | 
			
		||||
struct ClientInfo;
 | 
			
		||||
 | 
			
		||||
@@ -79,7 +91,7 @@ class APIFrameHelper {
 | 
			
		||||
  virtual APIError init() = 0;
 | 
			
		||||
  virtual APIError loop();
 | 
			
		||||
  virtual APIError read_packet(ReadPacketBuffer *buffer) = 0;
 | 
			
		||||
  bool can_write_without_blocking() { return state_ == State::DATA && tx_buf_.empty(); }
 | 
			
		||||
  bool can_write_without_blocking() { return this->state_ == State::DATA && this->tx_buf_count_ == 0; }
 | 
			
		||||
  std::string getpeername() { return socket_->getpeername(); }
 | 
			
		||||
  int getpeername(struct sockaddr *addr, socklen_t *addrlen) { return socket_->getpeername(addr, addrlen); }
 | 
			
		||||
  APIError close() {
 | 
			
		||||
@@ -161,7 +173,7 @@ class APIFrameHelper {
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  // Containers (size varies, but typically 12+ bytes on 32-bit)
 | 
			
		||||
  std::deque<SendBuffer> tx_buf_;
 | 
			
		||||
  std::array<std::unique_ptr<SendBuffer>, API_MAX_SEND_QUEUE> tx_buf_;
 | 
			
		||||
  std::vector<struct iovec> reusable_iovs_;
 | 
			
		||||
  std::vector<uint8_t> rx_buf_;
 | 
			
		||||
 | 
			
		||||
@@ -174,7 +186,10 @@ class APIFrameHelper {
 | 
			
		||||
  State state_{State::INITIALIZE};
 | 
			
		||||
  uint8_t frame_header_padding_{0};
 | 
			
		||||
  uint8_t frame_footer_size_{0};
 | 
			
		||||
  // 5 bytes total, 3 bytes padding
 | 
			
		||||
  uint8_t tx_buf_head_{0};
 | 
			
		||||
  uint8_t tx_buf_tail_{0};
 | 
			
		||||
  uint8_t tx_buf_count_{0};
 | 
			
		||||
  // 8 bytes total, 0 bytes padding
 | 
			
		||||
 | 
			
		||||
  // Common initialization for both plaintext and noise protocols
 | 
			
		||||
  APIError init_common_();
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,8 @@ static const char *const PROLOGUE_INIT = "NoiseAPIInit";
 | 
			
		||||
#endif
 | 
			
		||||
static constexpr size_t PROLOGUE_INIT_LEN = 12;  // strlen("NoiseAPIInit")
 | 
			
		||||
 | 
			
		||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__)
 | 
			
		||||
#define HELPER_LOG(msg, ...) \
 | 
			
		||||
  ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
 | 
			
		||||
 | 
			
		||||
#ifdef HELPER_LOG_PACKETS
 | 
			
		||||
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
 | 
			
		||||
@@ -131,26 +132,16 @@ APIError APINoiseFrameHelper::loop() {
 | 
			
		||||
  return APIFrameHelper::loop();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
 | 
			
		||||
/** Read a packet into the rx_buf_.
 | 
			
		||||
 *
 | 
			
		||||
 * @param frame: The struct to hold the frame information in.
 | 
			
		||||
 *   msg_start: points to the start of the payload - this pointer is only valid until the next
 | 
			
		||||
 *     try_receive_raw_ call
 | 
			
		||||
 *
 | 
			
		||||
 * @return 0 if a full packet is in rx_buf_
 | 
			
		||||
 * @return -1 if error, check errno.
 | 
			
		||||
 * @return APIError::OK if a full packet is in rx_buf_
 | 
			
		||||
 *
 | 
			
		||||
 * errno EWOULDBLOCK: Packet could not be read without blocking. Try again later.
 | 
			
		||||
 * errno ENOMEM: Not enough memory for reading packet.
 | 
			
		||||
 * errno API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
 | 
			
		||||
 * errno API_ERROR_HANDSHAKE_PACKET_LEN: Packet too big for this phase.
 | 
			
		||||
 */
 | 
			
		||||
APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
 | 
			
		||||
  if (frame == nullptr) {
 | 
			
		||||
    HELPER_LOG("Bad argument for try_read_frame_");
 | 
			
		||||
    return APIError::BAD_ARG;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
APIError APINoiseFrameHelper::try_read_frame_() {
 | 
			
		||||
  // read header
 | 
			
		||||
  if (rx_header_buf_len_ < 3) {
 | 
			
		||||
    // no header information yet
 | 
			
		||||
@@ -177,16 +168,17 @@ APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
 | 
			
		||||
  // read body
 | 
			
		||||
  uint16_t msg_size = (((uint16_t) rx_header_buf_[1]) << 8) | rx_header_buf_[2];
 | 
			
		||||
 | 
			
		||||
  if (state_ != State::DATA && msg_size > 128) {
 | 
			
		||||
    // for handshake message only permit up to 128 bytes
 | 
			
		||||
  // Check against size limits to prevent OOM: MAX_HANDSHAKE_SIZE for handshake, MAX_MESSAGE_SIZE for data
 | 
			
		||||
  uint16_t limit = (state_ == State::DATA) ? MAX_MESSAGE_SIZE : MAX_HANDSHAKE_SIZE;
 | 
			
		||||
  if (msg_size > limit) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Bad packet len for handshake: %d", msg_size);
 | 
			
		||||
    return APIError::BAD_HANDSHAKE_PACKET_LEN;
 | 
			
		||||
    HELPER_LOG("Bad packet: message size %u exceeds maximum %u", msg_size, limit);
 | 
			
		||||
    return (state_ == State::DATA) ? APIError::BAD_DATA_PACKET : APIError::BAD_HANDSHAKE_PACKET_LEN;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // reserve space for body
 | 
			
		||||
  if (rx_buf_.size() != msg_size) {
 | 
			
		||||
    rx_buf_.resize(msg_size);
 | 
			
		||||
  // Reserve space for body
 | 
			
		||||
  if (this->rx_buf_.size() != msg_size) {
 | 
			
		||||
    this->rx_buf_.resize(msg_size);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (rx_buf_len_ < msg_size) {
 | 
			
		||||
@@ -204,12 +196,12 @@ APIError APINoiseFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  LOG_PACKET_RECEIVED(rx_buf_);
 | 
			
		||||
  *frame = std::move(rx_buf_);
 | 
			
		||||
  // consume msg
 | 
			
		||||
  rx_buf_ = {};
 | 
			
		||||
  rx_buf_len_ = 0;
 | 
			
		||||
  rx_header_buf_len_ = 0;
 | 
			
		||||
  LOG_PACKET_RECEIVED(this->rx_buf_);
 | 
			
		||||
 | 
			
		||||
  // Clear state for next frame (rx_buf_ still contains data for caller)
 | 
			
		||||
  this->rx_buf_len_ = 0;
 | 
			
		||||
  this->rx_header_buf_len_ = 0;
 | 
			
		||||
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -231,18 +223,17 @@ APIError APINoiseFrameHelper::state_action_() {
 | 
			
		||||
  }
 | 
			
		||||
  if (state_ == State::CLIENT_HELLO) {
 | 
			
		||||
    // waiting for client hello
 | 
			
		||||
    std::vector<uint8_t> frame;
 | 
			
		||||
    aerr = try_read_frame_(&frame);
 | 
			
		||||
    aerr = this->try_read_frame_();
 | 
			
		||||
    if (aerr != APIError::OK) {
 | 
			
		||||
      return handle_handshake_frame_error_(aerr);
 | 
			
		||||
    }
 | 
			
		||||
    // ignore contents, may be used in future for flags
 | 
			
		||||
    // Resize for: existing prologue + 2 size bytes + frame data
 | 
			
		||||
    size_t old_size = prologue_.size();
 | 
			
		||||
    prologue_.resize(old_size + 2 + frame.size());
 | 
			
		||||
    prologue_[old_size] = (uint8_t) (frame.size() >> 8);
 | 
			
		||||
    prologue_[old_size + 1] = (uint8_t) frame.size();
 | 
			
		||||
    std::memcpy(prologue_.data() + old_size + 2, frame.data(), frame.size());
 | 
			
		||||
    size_t old_size = this->prologue_.size();
 | 
			
		||||
    this->prologue_.resize(old_size + 2 + this->rx_buf_.size());
 | 
			
		||||
    this->prologue_[old_size] = (uint8_t) (this->rx_buf_.size() >> 8);
 | 
			
		||||
    this->prologue_[old_size + 1] = (uint8_t) this->rx_buf_.size();
 | 
			
		||||
    std::memcpy(this->prologue_.data() + old_size + 2, this->rx_buf_.data(), this->rx_buf_.size());
 | 
			
		||||
 | 
			
		||||
    state_ = State::SERVER_HELLO;
 | 
			
		||||
  }
 | 
			
		||||
@@ -284,24 +275,23 @@ APIError APINoiseFrameHelper::state_action_() {
 | 
			
		||||
    int action = noise_handshakestate_get_action(handshake_);
 | 
			
		||||
    if (action == NOISE_ACTION_READ_MESSAGE) {
 | 
			
		||||
      // waiting for handshake msg
 | 
			
		||||
      std::vector<uint8_t> frame;
 | 
			
		||||
      aerr = try_read_frame_(&frame);
 | 
			
		||||
      aerr = this->try_read_frame_();
 | 
			
		||||
      if (aerr != APIError::OK) {
 | 
			
		||||
        return handle_handshake_frame_error_(aerr);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (frame.empty()) {
 | 
			
		||||
      if (this->rx_buf_.empty()) {
 | 
			
		||||
        send_explicit_handshake_reject_(LOG_STR("Empty handshake message"));
 | 
			
		||||
        return APIError::BAD_HANDSHAKE_ERROR_BYTE;
 | 
			
		||||
      } else if (frame[0] != 0x00) {
 | 
			
		||||
        HELPER_LOG("Bad handshake error byte: %u", frame[0]);
 | 
			
		||||
      } else if (this->rx_buf_[0] != 0x00) {
 | 
			
		||||
        HELPER_LOG("Bad handshake error byte: %u", this->rx_buf_[0]);
 | 
			
		||||
        send_explicit_handshake_reject_(LOG_STR("Bad handshake error byte"));
 | 
			
		||||
        return APIError::BAD_HANDSHAKE_ERROR_BYTE;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      NoiseBuffer mbuf;
 | 
			
		||||
      noise_buffer_init(mbuf);
 | 
			
		||||
      noise_buffer_set_input(mbuf, frame.data() + 1, frame.size() - 1);
 | 
			
		||||
      noise_buffer_set_input(mbuf, this->rx_buf_.data() + 1, this->rx_buf_.size() - 1);
 | 
			
		||||
      err = noise_handshakestate_read_message(handshake_, &mbuf, nullptr);
 | 
			
		||||
      if (err != 0) {
 | 
			
		||||
        // Special handling for MAC failure
 | 
			
		||||
@@ -378,35 +368,33 @@ void APINoiseFrameHelper::send_explicit_handshake_reject_(const LogString *reaso
 | 
			
		||||
  state_ = orig_state;
 | 
			
		||||
}
 | 
			
		||||
APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  int err;
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
  aerr = state_action_();
 | 
			
		||||
  APIError aerr = this->state_action_();
 | 
			
		||||
  if (aerr != APIError::OK) {
 | 
			
		||||
    return aerr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (state_ != State::DATA) {
 | 
			
		||||
  if (this->state_ != State::DATA) {
 | 
			
		||||
    return APIError::WOULD_BLOCK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> frame;
 | 
			
		||||
  aerr = try_read_frame_(&frame);
 | 
			
		||||
  aerr = this->try_read_frame_();
 | 
			
		||||
  if (aerr != APIError::OK)
 | 
			
		||||
    return aerr;
 | 
			
		||||
 | 
			
		||||
  NoiseBuffer mbuf;
 | 
			
		||||
  noise_buffer_init(mbuf);
 | 
			
		||||
  noise_buffer_set_inout(mbuf, frame.data(), frame.size(), frame.size());
 | 
			
		||||
  err = noise_cipherstate_decrypt(recv_cipher_, &mbuf);
 | 
			
		||||
  noise_buffer_set_inout(mbuf, this->rx_buf_.data(), this->rx_buf_.size(), this->rx_buf_.size());
 | 
			
		||||
  int err = noise_cipherstate_decrypt(this->recv_cipher_, &mbuf);
 | 
			
		||||
  APIError decrypt_err =
 | 
			
		||||
      handle_noise_error_(err, LOG_STR("noise_cipherstate_decrypt"), APIError::CIPHERSTATE_DECRYPT_FAILED);
 | 
			
		||||
  if (decrypt_err != APIError::OK)
 | 
			
		||||
  if (decrypt_err != APIError::OK) {
 | 
			
		||||
    return decrypt_err;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint16_t msg_size = mbuf.size;
 | 
			
		||||
  uint8_t *msg_data = frame.data();
 | 
			
		||||
  uint8_t *msg_data = this->rx_buf_.data();
 | 
			
		||||
  if (msg_size < 4) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    this->state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Bad data packet: size %d too short", msg_size);
 | 
			
		||||
    return APIError::BAD_DATA_PACKET;
 | 
			
		||||
  }
 | 
			
		||||
@@ -414,12 +402,12 @@ APIError APINoiseFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  uint16_t type = (((uint16_t) msg_data[0]) << 8) | msg_data[1];
 | 
			
		||||
  uint16_t data_len = (((uint16_t) msg_data[2]) << 8) | msg_data[3];
 | 
			
		||||
  if (data_len > msg_size - 4) {
 | 
			
		||||
    state_ = State::FAILED;
 | 
			
		||||
    this->state_ = State::FAILED;
 | 
			
		||||
    HELPER_LOG("Bad data packet: data_len %u greater than msg_size %u", data_len, msg_size);
 | 
			
		||||
    return APIError::BAD_DATA_PACKET;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  buffer->container = std::move(frame);
 | 
			
		||||
  buffer->container = std::move(this->rx_buf_);
 | 
			
		||||
  buffer->data_offset = 4;
 | 
			
		||||
  buffer->data_len = data_len;
 | 
			
		||||
  buffer->type = type;
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ class APINoiseFrameHelper final : public APIFrameHelper {
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  APIError state_action_();
 | 
			
		||||
  APIError try_read_frame_(std::vector<uint8_t> *frame);
 | 
			
		||||
  APIError try_read_frame_();
 | 
			
		||||
  APIError write_frame_(const uint8_t *data, uint16_t len);
 | 
			
		||||
  APIError init_handshake_();
 | 
			
		||||
  APIError check_handshake_finished_();
 | 
			
		||||
 
 | 
			
		||||
@@ -18,7 +18,8 @@ namespace esphome::api {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "api.plaintext";
 | 
			
		||||
 | 
			
		||||
#define HELPER_LOG(msg, ...) ESP_LOGVV(TAG, "%s: " msg, this->client_info_->get_combined_info().c_str(), ##__VA_ARGS__)
 | 
			
		||||
#define HELPER_LOG(msg, ...) \
 | 
			
		||||
  ESP_LOGVV(TAG, "%s (%s): " msg, this->client_info_->name.c_str(), this->client_info_->peername.c_str(), ##__VA_ARGS__)
 | 
			
		||||
 | 
			
		||||
#ifdef HELPER_LOG_PACKETS
 | 
			
		||||
#define LOG_PACKET_RECEIVED(buffer) ESP_LOGVV(TAG, "Received frame: %s", format_hex_pretty(buffer).c_str())
 | 
			
		||||
@@ -46,21 +47,13 @@ APIError APIPlaintextFrameHelper::loop() {
 | 
			
		||||
  return APIFrameHelper::loop();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/** Read a packet into the rx_buf_. If successful, stores frame data in the frame parameter
 | 
			
		||||
 *
 | 
			
		||||
 * @param frame: The struct to hold the frame information in.
 | 
			
		||||
 *   msg: store the parsed frame in that struct
 | 
			
		||||
/** Read a packet into the rx_buf_.
 | 
			
		||||
 *
 | 
			
		||||
 * @return See APIError
 | 
			
		||||
 *
 | 
			
		||||
 * error API_ERROR_BAD_INDICATOR: Bad indicator byte at start of frame.
 | 
			
		||||
 */
 | 
			
		||||
APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
 | 
			
		||||
  if (frame == nullptr) {
 | 
			
		||||
    HELPER_LOG("Bad argument for try_read_frame_");
 | 
			
		||||
    return APIError::BAD_ARG;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
APIError APIPlaintextFrameHelper::try_read_frame_() {
 | 
			
		||||
  // read header
 | 
			
		||||
  while (!rx_header_parsed_) {
 | 
			
		||||
    // Now that we know when the socket is ready, we can read up to 3 bytes
 | 
			
		||||
@@ -122,10 +115,10 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (msg_size_varint->as_uint32() > std::numeric_limits<uint16_t>::max()) {
 | 
			
		||||
    if (msg_size_varint->as_uint32() > MAX_MESSAGE_SIZE) {
 | 
			
		||||
      state_ = State::FAILED;
 | 
			
		||||
      HELPER_LOG("Bad packet: message size %" PRIu32 " exceeds maximum %u", msg_size_varint->as_uint32(),
 | 
			
		||||
                 std::numeric_limits<uint16_t>::max());
 | 
			
		||||
                 MAX_MESSAGE_SIZE);
 | 
			
		||||
      return APIError::BAD_DATA_PACKET;
 | 
			
		||||
    }
 | 
			
		||||
    rx_header_parsed_len_ = msg_size_varint->as_uint16();
 | 
			
		||||
@@ -149,9 +142,9 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
 | 
			
		||||
  }
 | 
			
		||||
  // header reading done
 | 
			
		||||
 | 
			
		||||
  // reserve space for body
 | 
			
		||||
  if (rx_buf_.size() != rx_header_parsed_len_) {
 | 
			
		||||
    rx_buf_.resize(rx_header_parsed_len_);
 | 
			
		||||
  // Reserve space for body
 | 
			
		||||
  if (this->rx_buf_.size() != this->rx_header_parsed_len_) {
 | 
			
		||||
    this->rx_buf_.resize(this->rx_header_parsed_len_);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (rx_buf_len_ < rx_header_parsed_len_) {
 | 
			
		||||
@@ -169,24 +162,22 @@ APIError APIPlaintextFrameHelper::try_read_frame_(std::vector<uint8_t> *frame) {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  LOG_PACKET_RECEIVED(rx_buf_);
 | 
			
		||||
  *frame = std::move(rx_buf_);
 | 
			
		||||
  // consume msg
 | 
			
		||||
  rx_buf_ = {};
 | 
			
		||||
  rx_buf_len_ = 0;
 | 
			
		||||
  rx_header_buf_pos_ = 0;
 | 
			
		||||
  rx_header_parsed_ = false;
 | 
			
		||||
  LOG_PACKET_RECEIVED(this->rx_buf_);
 | 
			
		||||
 | 
			
		||||
  // Clear state for next frame (rx_buf_ still contains data for caller)
 | 
			
		||||
  this->rx_buf_len_ = 0;
 | 
			
		||||
  this->rx_header_buf_pos_ = 0;
 | 
			
		||||
  this->rx_header_parsed_ = false;
 | 
			
		||||
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  APIError aerr;
 | 
			
		||||
 | 
			
		||||
  if (state_ != State::DATA) {
 | 
			
		||||
APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
  if (this->state_ != State::DATA) {
 | 
			
		||||
    return APIError::WOULD_BLOCK;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> frame;
 | 
			
		||||
  aerr = try_read_frame_(&frame);
 | 
			
		||||
  APIError aerr = this->try_read_frame_();
 | 
			
		||||
  if (aerr != APIError::OK) {
 | 
			
		||||
    if (aerr == APIError::BAD_INDICATOR) {
 | 
			
		||||
      // Make sure to tell the remote that we don't
 | 
			
		||||
@@ -219,10 +210,10 @@ APIError APIPlaintextFrameHelper::read_packet(ReadPacketBuffer *buffer) {
 | 
			
		||||
    return aerr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  buffer->container = std::move(frame);
 | 
			
		||||
  buffer->container = std::move(this->rx_buf_);
 | 
			
		||||
  buffer->data_offset = 0;
 | 
			
		||||
  buffer->data_len = rx_header_parsed_len_;
 | 
			
		||||
  buffer->type = rx_header_parsed_type_;
 | 
			
		||||
  buffer->data_len = this->rx_header_parsed_len_;
 | 
			
		||||
  buffer->type = this->rx_header_parsed_type_;
 | 
			
		||||
  return APIError::OK;
 | 
			
		||||
}
 | 
			
		||||
APIError APIPlaintextFrameHelper::write_protobuf_packet(uint8_t type, ProtoWriteBuffer buffer) {
 | 
			
		||||
 
 | 
			
		||||
@@ -24,7 +24,7 @@ class APIPlaintextFrameHelper final : public APIFrameHelper {
 | 
			
		||||
  APIError write_protobuf_packets(ProtoWriteBuffer buffer, std::span<const PacketInfo> packets) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  APIError try_read_frame_(std::vector<uint8_t> *frame);
 | 
			
		||||
  APIError try_read_frame_();
 | 
			
		||||
 | 
			
		||||
  // Group 2-byte aligned types
 | 
			
		||||
  uint16_t rx_header_parsed_type_ = 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -884,6 +884,15 @@ void HomeassistantActionRequest::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
    buffer.encode_message(4, it, true);
 | 
			
		||||
  }
 | 
			
		||||
  buffer.encode_bool(5, this->is_event);
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  buffer.encode_uint32(6, this->call_id);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  buffer.encode_bool(7, this->wants_response);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  buffer.encode_string(8, this->response_template);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
 | 
			
		||||
  size.add_length(1, this->service_ref_.size());
 | 
			
		||||
@@ -891,6 +900,48 @@ void HomeassistantActionRequest::calculate_size(ProtoSize &size) const {
 | 
			
		||||
  size.add_repeated_message(1, this->data_template);
 | 
			
		||||
  size.add_repeated_message(1, this->variables);
 | 
			
		||||
  size.add_bool(1, this->is_event);
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  size.add_uint32(1, this->call_id);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  size.add_bool(1, this->wants_response);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  size.add_length(1, this->response_template.size());
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
bool HomeassistantActionResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 1:
 | 
			
		||||
      this->call_id = value.as_uint32();
 | 
			
		||||
      break;
 | 
			
		||||
    case 2:
 | 
			
		||||
      this->success = value.as_bool();
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
bool HomeassistantActionResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
    case 3:
 | 
			
		||||
      this->error_message = value.as_string();
 | 
			
		||||
      break;
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
    case 4: {
 | 
			
		||||
      // Use raw data directly to avoid allocation
 | 
			
		||||
      this->response_data = value.data();
 | 
			
		||||
      this->response_data_len = value.size();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
    default:
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
 
 | 
			
		||||
@@ -1104,7 +1104,7 @@ class HomeassistantServiceMap final : public ProtoMessage {
 | 
			
		||||
class HomeassistantActionRequest final : public ProtoMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  static constexpr uint8_t MESSAGE_TYPE = 35;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 113;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 128;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "homeassistant_action_request"; }
 | 
			
		||||
#endif
 | 
			
		||||
@@ -1114,6 +1114,15 @@ class HomeassistantActionRequest final : public ProtoMessage {
 | 
			
		||||
  std::vector<HomeassistantServiceMap> data_template{};
 | 
			
		||||
  std::vector<HomeassistantServiceMap> variables{};
 | 
			
		||||
  bool is_event{false};
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  uint32_t call_id{0};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  bool wants_response{false};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  std::string response_template{};
 | 
			
		||||
#endif
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
  void calculate_size(ProtoSize &size) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
@@ -1123,6 +1132,30 @@ class HomeassistantActionRequest final : public ProtoMessage {
 | 
			
		||||
 protected:
 | 
			
		||||
};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
class HomeassistantActionResponse final : public ProtoDecodableMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  static constexpr uint8_t MESSAGE_TYPE = 130;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 34;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "homeassistant_action_response"; }
 | 
			
		||||
#endif
 | 
			
		||||
  uint32_t call_id{0};
 | 
			
		||||
  bool success{false};
 | 
			
		||||
  std::string error_message{};
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  const uint8_t *response_data{nullptr};
 | 
			
		||||
  uint16_t response_data_len{0};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  void dump_to(std::string &out) const override;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override;
 | 
			
		||||
  bool decode_varint(uint32_t field_id, ProtoVarInt value) override;
 | 
			
		||||
};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
class SubscribeHomeAssistantStatesRequest final : public ProtoMessage {
 | 
			
		||||
 public:
 | 
			
		||||
 
 | 
			
		||||
@@ -1122,6 +1122,28 @@ void HomeassistantActionRequest::dump_to(std::string &out) const {
 | 
			
		||||
    out.append("\n");
 | 
			
		||||
  }
 | 
			
		||||
  dump_field(out, "is_event", this->is_event);
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  dump_field(out, "call_id", this->call_id);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  dump_field(out, "wants_response", this->wants_response);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  dump_field(out, "response_template", this->response_template);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
void HomeassistantActionResponse::dump_to(std::string &out) const {
 | 
			
		||||
  MessageDumpHelper helper(out, "HomeassistantActionResponse");
 | 
			
		||||
  dump_field(out, "call_id", this->call_id);
 | 
			
		||||
  dump_field(out, "success", this->success);
 | 
			
		||||
  dump_field(out, "error_message", this->error_message);
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  out.append("  response_data: ");
 | 
			
		||||
  out.append(format_hex_pretty(this->response_data, this->response_data_len));
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
 
 | 
			
		||||
@@ -610,6 +610,17 @@ void APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type,
 | 
			
		||||
      this->on_z_wave_proxy_request(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
    case HomeassistantActionResponse::MESSAGE_TYPE: {
 | 
			
		||||
      HomeassistantActionResponse msg;
 | 
			
		||||
      msg.decode(msg_data, msg_size);
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
      ESP_LOGVV(TAG, "on_homeassistant_action_response: %s", msg.dump().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
      this->on_homeassistant_action_response(msg);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
 
 | 
			
		||||
@@ -66,6 +66,9 @@ class APIServerConnectionBase : public ProtoService {
 | 
			
		||||
  virtual void on_subscribe_homeassistant_services_request(const SubscribeHomeassistantServicesRequest &value){};
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  virtual void on_homeassistant_action_response(const HomeassistantActionResponse &value){};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
  virtual void on_subscribe_home_assistant_states_request(const SubscribeHomeAssistantStatesRequest &value){};
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -9,12 +9,16 @@
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/util.h"
 | 
			
		||||
#include "esphome/core/version.h"
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
#include "homeassistant_service.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_LOGGER
 | 
			
		||||
#include "esphome/components/logger/logger.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include <algorithm>
 | 
			
		||||
#include <utility>
 | 
			
		||||
 | 
			
		||||
namespace esphome::api {
 | 
			
		||||
 | 
			
		||||
@@ -177,7 +181,8 @@ void APIServer::loop() {
 | 
			
		||||
    // Network is down - disconnect all clients
 | 
			
		||||
    for (auto &client : this->clients_) {
 | 
			
		||||
      client->on_fatal_error();
 | 
			
		||||
      ESP_LOGW(TAG, "%s: Network down; disconnect", client->get_client_combined_info().c_str());
 | 
			
		||||
      ESP_LOGW(TAG, "%s (%s): Network down; disconnect", client->client_info_.name.c_str(),
 | 
			
		||||
               client->client_info_.peername.c_str());
 | 
			
		||||
    }
 | 
			
		||||
    // Continue to process and clean up the clients below
 | 
			
		||||
  }
 | 
			
		||||
@@ -399,7 +404,38 @@ void APIServer::send_homeassistant_action(const HomeassistantActionRequest &call
 | 
			
		||||
    client->send_homeassistant_action(call);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
void APIServer::register_action_response_callback(uint32_t call_id, ActionResponseCallback callback) {
 | 
			
		||||
  this->action_response_callbacks_.push_back({call_id, std::move(callback)});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message) {
 | 
			
		||||
  for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
 | 
			
		||||
    if (it->call_id == call_id) {
 | 
			
		||||
      auto callback = std::move(it->callback);
 | 
			
		||||
      this->action_response_callbacks_.erase(it);
 | 
			
		||||
      ActionResponse response(success, error_message);
 | 
			
		||||
      callback(response);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
void APIServer::handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
 | 
			
		||||
                                       const uint8_t *response_data, size_t response_data_len) {
 | 
			
		||||
  for (auto it = this->action_response_callbacks_.begin(); it != this->action_response_callbacks_.end(); ++it) {
 | 
			
		||||
    if (it->call_id == call_id) {
 | 
			
		||||
      auto callback = std::move(it->callback);
 | 
			
		||||
      this->action_response_callbacks_.erase(it);
 | 
			
		||||
      ActionResponse response(success, error_message, response_data, response_data_len);
 | 
			
		||||
      callback(response);
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute,
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@
 | 
			
		||||
#include "user_services.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include <map>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome::api {
 | 
			
		||||
@@ -111,7 +112,17 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
  void send_homeassistant_action(const HomeassistantActionRequest &call);
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  // Action response handling
 | 
			
		||||
  using ActionResponseCallback = std::function<void(const class ActionResponse &)>;
 | 
			
		||||
  void register_action_response_callback(uint32_t call_id, ActionResponseCallback callback);
 | 
			
		||||
  void handle_action_response(uint32_t call_id, bool success, const std::string &error_message);
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  void handle_action_response(uint32_t call_id, bool success, const std::string &error_message,
 | 
			
		||||
                              const uint8_t *response_data, size_t response_data_len);
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
#ifdef USE_API_SERVICES
 | 
			
		||||
  void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); }
 | 
			
		||||
#endif
 | 
			
		||||
@@ -187,6 +198,13 @@ class APIServer : public Component, public Controller {
 | 
			
		||||
#ifdef USE_API_SERVICES
 | 
			
		||||
  std::vector<UserServiceDescriptor *> user_services_;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  struct PendingActionResponse {
 | 
			
		||||
    uint32_t call_id;
 | 
			
		||||
    ActionResponseCallback callback;
 | 
			
		||||
  };
 | 
			
		||||
  std::vector<PendingActionResponse> action_response_callbacks_;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Group smaller types together
 | 
			
		||||
  uint16_t port_{6053};
 | 
			
		||||
 
 | 
			
		||||
@@ -3,8 +3,13 @@
 | 
			
		||||
#include "api_server.h"
 | 
			
		||||
#ifdef USE_API
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <utility>
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include "api_pb2.h"
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
#include "esphome/components/json/json_util.h"
 | 
			
		||||
#endif
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
@@ -44,9 +49,47 @@ template<typename... Ts> class TemplatableKeyValuePair {
 | 
			
		||||
  TemplatableStringValue<Ts...> value;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
// Represents the response data from a Home Assistant action
 | 
			
		||||
class ActionResponse {
 | 
			
		||||
 public:
 | 
			
		||||
  ActionResponse(bool success, std::string error_message = "")
 | 
			
		||||
      : success_(success), error_message_(std::move(error_message)) {}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  ActionResponse(bool success, std::string error_message, const uint8_t *data, size_t data_len)
 | 
			
		||||
      : success_(success), error_message_(std::move(error_message)) {
 | 
			
		||||
    if (data == nullptr || data_len == 0)
 | 
			
		||||
      return;
 | 
			
		||||
    this->json_document_ = json::parse_json(data, data_len);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  bool is_success() const { return this->success_; }
 | 
			
		||||
  const std::string &get_error_message() const { return this->error_message_; }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  // Get data as parsed JSON object (const version returns read-only view)
 | 
			
		||||
  JsonObjectConst get_json() const { return this->json_document_.as<JsonObjectConst>(); }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool success_;
 | 
			
		||||
  std::string error_message_;
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  JsonDocument json_document_;
 | 
			
		||||
#endif
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Callback type for action responses
 | 
			
		||||
template<typename... Ts> using ActionResponseCallback = std::function<void(const ActionResponse &, Ts...)>;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent), is_event_(is_event) {}
 | 
			
		||||
  explicit HomeAssistantServiceCallAction(APIServer *parent, bool is_event) : parent_(parent) {
 | 
			
		||||
    this->flags_.is_event = is_event;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  template<typename T> void set_service(T service) { this->service_ = service; }
 | 
			
		||||
 | 
			
		||||
@@ -61,11 +104,29 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
 | 
			
		||||
    this->variables_.emplace_back(std::move(key), value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
  template<typename T> void set_response_template(T response_template) {
 | 
			
		||||
    this->response_template_ = response_template;
 | 
			
		||||
    this->flags_.has_response_template = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_wants_status() { this->flags_.wants_status = true; }
 | 
			
		||||
  void set_wants_response() { this->flags_.wants_response = true; }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  Trigger<JsonObjectConst, Ts...> *get_success_trigger_with_response() const {
 | 
			
		||||
    return this->success_trigger_with_response_;
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
  Trigger<Ts...> *get_success_trigger() const { return this->success_trigger_; }
 | 
			
		||||
  Trigger<std::string, Ts...> *get_error_trigger() const { return this->error_trigger_; }
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override {
 | 
			
		||||
    HomeassistantActionRequest resp;
 | 
			
		||||
    std::string service_value = this->service_.value(x...);
 | 
			
		||||
    resp.set_service(StringRef(service_value));
 | 
			
		||||
    resp.is_event = this->is_event_;
 | 
			
		||||
    resp.is_event = this->flags_.is_event;
 | 
			
		||||
    for (auto &it : this->data_) {
 | 
			
		||||
      resp.data.emplace_back();
 | 
			
		||||
      auto &kv = resp.data.back();
 | 
			
		||||
@@ -84,18 +145,74 @@ template<typename... Ts> class HomeAssistantServiceCallAction : public Action<Ts
 | 
			
		||||
      kv.set_key(StringRef(it.key));
 | 
			
		||||
      kv.value = it.value.value(x...);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
    if (this->flags_.wants_status) {
 | 
			
		||||
      // Generate a unique call ID for this service call
 | 
			
		||||
      static uint32_t call_id_counter = 1;
 | 
			
		||||
      uint32_t call_id = call_id_counter++;
 | 
			
		||||
      resp.call_id = call_id;
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
      if (this->flags_.wants_response) {
 | 
			
		||||
        resp.wants_response = true;
 | 
			
		||||
        // Set response template if provided
 | 
			
		||||
        if (this->flags_.has_response_template) {
 | 
			
		||||
          std::string response_template_value = this->response_template_.value(x...);
 | 
			
		||||
          resp.response_template = response_template_value;
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
      auto captured_args = std::make_tuple(x...);
 | 
			
		||||
      this->parent_->register_action_response_callback(call_id, [this, captured_args](const ActionResponse &response) {
 | 
			
		||||
        std::apply(
 | 
			
		||||
            [this, &response](auto &&...args) {
 | 
			
		||||
              if (response.is_success()) {
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
                if (this->flags_.wants_response) {
 | 
			
		||||
                  this->success_trigger_with_response_->trigger(response.get_json(), args...);
 | 
			
		||||
                } else
 | 
			
		||||
#endif
 | 
			
		||||
                {
 | 
			
		||||
                  this->success_trigger_->trigger(args...);
 | 
			
		||||
                }
 | 
			
		||||
              } else {
 | 
			
		||||
                this->error_trigger_->trigger(response.get_error_message(), args...);
 | 
			
		||||
              }
 | 
			
		||||
            },
 | 
			
		||||
            captured_args);
 | 
			
		||||
      });
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    this->parent_->send_homeassistant_action(resp);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  APIServer *parent_;
 | 
			
		||||
  bool is_event_;
 | 
			
		||||
  TemplatableStringValue<Ts...> service_{};
 | 
			
		||||
  std::vector<TemplatableKeyValuePair<Ts...>> data_;
 | 
			
		||||
  std::vector<TemplatableKeyValuePair<Ts...>> data_template_;
 | 
			
		||||
  std::vector<TemplatableKeyValuePair<Ts...>> variables_;
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  TemplatableStringValue<Ts...> response_template_{""};
 | 
			
		||||
  Trigger<JsonObjectConst, Ts...> *success_trigger_with_response_ = new Trigger<JsonObjectConst, Ts...>();
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES_JSON
 | 
			
		||||
  Trigger<Ts...> *success_trigger_ = new Trigger<Ts...>();
 | 
			
		||||
  Trigger<std::string, Ts...> *error_trigger_ = new Trigger<std::string, Ts...>();
 | 
			
		||||
#endif  // USE_API_HOMEASSISTANT_ACTION_RESPONSES
 | 
			
		||||
 | 
			
		||||
  struct Flags {
 | 
			
		||||
    uint8_t is_event : 1;
 | 
			
		||||
    uint8_t wants_status : 1;
 | 
			
		||||
    uint8_t wants_response : 1;
 | 
			
		||||
    uint8_t has_response_template : 1;
 | 
			
		||||
    uint8_t reserved : 5;
 | 
			
		||||
  } flags_{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::api
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
 | 
			
		||||
    msg.set_name(StringRef(this->name_));
 | 
			
		||||
    msg.key = this->key_;
 | 
			
		||||
    std::array<enums::ServiceArgType, sizeof...(Ts)> arg_types = {to_service_arg_type<Ts>()...};
 | 
			
		||||
    for (int i = 0; i < sizeof...(Ts); i++) {
 | 
			
		||||
    for (size_t i = 0; i < sizeof...(Ts); i++) {
 | 
			
		||||
      msg.args.emplace_back();
 | 
			
		||||
      auto &arg = msg.args.back();
 | 
			
		||||
      arg.type = arg_types[i];
 | 
			
		||||
@@ -55,7 +55,7 @@ template<typename... Ts> class UserServiceBase : public UserServiceDescriptor {
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  virtual void execute(Ts... x) = 0;
 | 
			
		||||
  template<int... S> void execute_(std::vector<ExecuteServiceArgument> args, seq<S...> type) {
 | 
			
		||||
  template<int... S> void execute_(const std::vector<ExecuteServiceArgument> &args, seq<S...> type) {
 | 
			
		||||
    this->execute((get_execute_arg_value<Ts>(args[S]))...);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -165,4 +165,4 @@ def final_validate_audio_schema(
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    cg.add_library("esphome/esp-audio-libs", "1.1.4")
 | 
			
		||||
    cg.add_library("esphome/esp-audio-libs", "2.0.1")
 | 
			
		||||
 
 | 
			
		||||
@@ -57,7 +57,7 @@ const char *audio_file_type_to_string(AudioFileType file_type) {
 | 
			
		||||
void scale_audio_samples(const int16_t *audio_samples, int16_t *output_buffer, int16_t scale_factor,
 | 
			
		||||
                         size_t samples_to_scale) {
 | 
			
		||||
  // Note the assembly dsps_mulc function has audio glitches if the input and output buffers are the same.
 | 
			
		||||
  for (int i = 0; i < samples_to_scale; i++) {
 | 
			
		||||
  for (size_t i = 0; i < samples_to_scale; i++) {
 | 
			
		||||
    int32_t acc = (int32_t) audio_samples[i] * (int32_t) scale_factor;
 | 
			
		||||
    output_buffer[i] = (int16_t) (acc >> 15);
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -229,18 +229,18 @@ FileDecoderState AudioDecoder::decode_flac_() {
 | 
			
		||||
    auto result = this->flac_decoder_->read_header(this->input_transfer_buffer_->get_buffer_start(),
 | 
			
		||||
                                                   this->input_transfer_buffer_->available());
 | 
			
		||||
 | 
			
		||||
    if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
 | 
			
		||||
      return FileDecoderState::POTENTIALLY_FAILED;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (result != esp_audio_libs::flac::FLAC_DECODER_SUCCESS) {
 | 
			
		||||
      // Couldn't read FLAC header
 | 
			
		||||
    if (result > esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
 | 
			
		||||
      // Serrious error reading FLAC header, there is no recovery
 | 
			
		||||
      return FileDecoderState::FAILED;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    size_t bytes_consumed = this->flac_decoder_->get_bytes_index();
 | 
			
		||||
    this->input_transfer_buffer_->decrease_buffer_length(bytes_consumed);
 | 
			
		||||
 | 
			
		||||
    if (result == esp_audio_libs::flac::FLAC_DECODER_HEADER_OUT_OF_DATA) {
 | 
			
		||||
      return FileDecoderState::MORE_TO_PROCESS;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Reallocate the output transfer buffer to the smallest necessary size
 | 
			
		||||
    this->free_buffer_required_ = flac_decoder_->get_output_buffer_size_bytes();
 | 
			
		||||
    if (!this->output_transfer_buffer_->reallocate(this->free_buffer_required_)) {
 | 
			
		||||
@@ -256,9 +256,9 @@ FileDecoderState AudioDecoder::decode_flac_() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint32_t output_samples = 0;
 | 
			
		||||
  auto result = this->flac_decoder_->decode_frame(
 | 
			
		||||
      this->input_transfer_buffer_->get_buffer_start(), this->input_transfer_buffer_->available(),
 | 
			
		||||
      reinterpret_cast<int16_t *>(this->output_transfer_buffer_->get_buffer_end()), &output_samples);
 | 
			
		||||
  auto result = this->flac_decoder_->decode_frame(this->input_transfer_buffer_->get_buffer_start(),
 | 
			
		||||
                                                  this->input_transfer_buffer_->available(),
 | 
			
		||||
                                                  this->output_transfer_buffer_->get_buffer_end(), &output_samples);
 | 
			
		||||
 | 
			
		||||
  if (result == esp_audio_libs::flac::FLAC_DECODER_ERROR_OUT_OF_DATA) {
 | 
			
		||||
    // Not an issue, just needs more data that we'll get next time.
 | 
			
		||||
 
 | 
			
		||||
@@ -97,10 +97,10 @@ void BL0906::handle_actions_() {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  ActionCallbackFuncPtr ptr_func = nullptr;
 | 
			
		||||
  for (int i = 0; i < this->action_queue_.size(); i++) {
 | 
			
		||||
  for (size_t i = 0; i < this->action_queue_.size(); i++) {
 | 
			
		||||
    ptr_func = this->action_queue_[i];
 | 
			
		||||
    if (ptr_func) {
 | 
			
		||||
      ESP_LOGI(TAG, "HandleActionCallback[%d]", i);
 | 
			
		||||
      ESP_LOGI(TAG, "HandleActionCallback[%zu]", i);
 | 
			
		||||
      (this->*ptr_func)();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -51,7 +51,7 @@ void BL0942::loop() {
 | 
			
		||||
  if (!avail) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (avail < sizeof(buffer)) {
 | 
			
		||||
  if (static_cast<size_t>(avail) < sizeof(buffer)) {
 | 
			
		||||
    if (!this->rx_start_) {
 | 
			
		||||
      this->rx_start_ = millis();
 | 
			
		||||
    } else if (millis() > this->rx_start_ + PKT_TIMEOUT_MS) {
 | 
			
		||||
@@ -148,7 +148,7 @@ void BL0942::setup() {
 | 
			
		||||
 | 
			
		||||
  this->write_reg_(BL0942_REG_USR_WRPROT, 0);
 | 
			
		||||
 | 
			
		||||
  if (this->read_reg_(BL0942_REG_MODE) != mode)
 | 
			
		||||
  if (static_cast<uint32_t>(this->read_reg_(BL0942_REG_MODE)) != mode)
 | 
			
		||||
    this->status_set_warning(LOG_STR("BL0942 setup failed!"));
 | 
			
		||||
 | 
			
		||||
  this->flush();
 | 
			
		||||
 
 | 
			
		||||
@@ -116,7 +116,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
    .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA),
 | 
			
		||||
    esp32_ble_tracker.consume_connection_slots(1, "ble_client"),
 | 
			
		||||
    esp32_ble.consume_connection_slots(1, "ble_client"),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONF_BLE_CLIENT_ID = "ble_client_id"
 | 
			
		||||
 
 | 
			
		||||
@@ -42,9 +42,7 @@ def validate_connections(config):
 | 
			
		||||
            )
 | 
			
		||||
    elif config[CONF_ACTIVE]:
 | 
			
		||||
        connection_slots: int = config[CONF_CONNECTION_SLOTS]
 | 
			
		||||
        esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")(
 | 
			
		||||
            config
 | 
			
		||||
        )
 | 
			
		||||
        esp32_ble.consume_connection_slots(connection_slots, "bluetooth_proxy")(config)
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            **config,
 | 
			
		||||
@@ -65,11 +63,11 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
                    default=DEFAULT_CONNECTION_SLOTS,
 | 
			
		||||
                ): cv.All(
 | 
			
		||||
                    cv.positive_int,
 | 
			
		||||
                    cv.Range(min=1, max=esp32_ble_tracker.IDF_MAX_CONNECTIONS),
 | 
			
		||||
                    cv.Range(min=1, max=esp32_ble.IDF_MAX_CONNECTIONS),
 | 
			
		||||
                ),
 | 
			
		||||
                cv.Optional(CONF_CONNECTIONS): cv.All(
 | 
			
		||||
                    cv.ensure_list(CONNECTION_SCHEMA),
 | 
			
		||||
                    cv.Length(min=1, max=esp32_ble_tracker.IDF_MAX_CONNECTIONS),
 | 
			
		||||
                    cv.Length(min=1, max=esp32_ble.IDF_MAX_CONNECTIONS),
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -105,9 +105,9 @@ class Canbus : public Component {
 | 
			
		||||
  CallbackManager<void(uint32_t can_id, bool extended_id, bool rtr, const std::vector<uint8_t> &data)>
 | 
			
		||||
      callback_manager_{};
 | 
			
		||||
 | 
			
		||||
  virtual bool setup_internal();
 | 
			
		||||
  virtual Error send_message(struct CanFrame *frame);
 | 
			
		||||
  virtual Error read_message(struct CanFrame *frame);
 | 
			
		||||
  virtual bool setup_internal() = 0;
 | 
			
		||||
  virtual Error send_message(struct CanFrame *frame) = 0;
 | 
			
		||||
  virtual Error read_message(struct CanFrame *frame) = 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class CanbusSendAction : public Action<Ts...>, public Parented<Canbus> {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,14 +11,14 @@ namespace captive_portal {
 | 
			
		||||
static const char *const TAG = "captive_portal";
 | 
			
		||||
 | 
			
		||||
void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
 | 
			
		||||
  AsyncResponseStream *stream = request->beginResponseStream(F("application/json"));
 | 
			
		||||
  stream->addHeader(F("cache-control"), F("public, max-age=0, must-revalidate"));
 | 
			
		||||
  AsyncResponseStream *stream = request->beginResponseStream(ESPHOME_F("application/json"));
 | 
			
		||||
  stream->addHeader(ESPHOME_F("cache-control"), ESPHOME_F("public, max-age=0, must-revalidate"));
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
  stream->print(F("{\"mac\":\""));
 | 
			
		||||
  stream->print(ESPHOME_F("{\"mac\":\""));
 | 
			
		||||
  stream->print(get_mac_address_pretty().c_str());
 | 
			
		||||
  stream->print(F("\",\"name\":\""));
 | 
			
		||||
  stream->print(ESPHOME_F("\",\"name\":\""));
 | 
			
		||||
  stream->print(App.get_name().c_str());
 | 
			
		||||
  stream->print(F("\",\"aps\":[{}"));
 | 
			
		||||
  stream->print(ESPHOME_F("\",\"aps\":[{}"));
 | 
			
		||||
#else
 | 
			
		||||
  stream->printf(R"({"mac":"%s","name":"%s","aps":[{})", get_mac_address_pretty().c_str(), App.get_name().c_str());
 | 
			
		||||
#endif
 | 
			
		||||
@@ -29,19 +29,19 @@ void CaptivePortal::handle_config(AsyncWebServerRequest *request) {
 | 
			
		||||
 | 
			
		||||
      // Assumes no " in ssid, possible unicode isses?
 | 
			
		||||
#ifdef USE_ESP8266
 | 
			
		||||
    stream->print(F(",{\"ssid\":\""));
 | 
			
		||||
    stream->print(ESPHOME_F(",{\"ssid\":\""));
 | 
			
		||||
    stream->print(scan.get_ssid().c_str());
 | 
			
		||||
    stream->print(F("\",\"rssi\":"));
 | 
			
		||||
    stream->print(ESPHOME_F("\",\"rssi\":"));
 | 
			
		||||
    stream->print(scan.get_rssi());
 | 
			
		||||
    stream->print(F(",\"lock\":"));
 | 
			
		||||
    stream->print(ESPHOME_F(",\"lock\":"));
 | 
			
		||||
    stream->print(scan.get_with_auth());
 | 
			
		||||
    stream->print(F("}"));
 | 
			
		||||
    stream->print(ESPHOME_F("}"));
 | 
			
		||||
#else
 | 
			
		||||
    stream->printf(R"(,{"ssid":"%s","rssi":%d,"lock":%d})", scan.get_ssid().c_str(), scan.get_rssi(),
 | 
			
		||||
                   scan.get_with_auth());
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
  stream->print(F("]}"));
 | 
			
		||||
  stream->print(ESPHOME_F("]}"));
 | 
			
		||||
  request->send(stream);
 | 
			
		||||
}
 | 
			
		||||
void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
 | 
			
		||||
@@ -52,7 +52,7 @@ void CaptivePortal::handle_wifisave(AsyncWebServerRequest *request) {
 | 
			
		||||
  ESP_LOGI(TAG, "  Password=" LOG_SECRET("'%s'"), psk.c_str());
 | 
			
		||||
  wifi::global_wifi_component->save_wifi_sta(ssid, psk);
 | 
			
		||||
  wifi::global_wifi_component->start_scanning();
 | 
			
		||||
  request->redirect(F("/?save"));
 | 
			
		||||
  request->redirect(ESPHOME_F("/?save"));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void CaptivePortal::setup() {
 | 
			
		||||
@@ -75,7 +75,7 @@ void CaptivePortal::start() {
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
  this->dns_server_ = make_unique<DNSServer>();
 | 
			
		||||
  this->dns_server_->setErrorReplyCode(DNSReplyCode::NoError);
 | 
			
		||||
  this->dns_server_->start(53, F("*"), ip);
 | 
			
		||||
  this->dns_server_->start(53, ESPHOME_F("*"), ip);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  this->initialized_ = true;
 | 
			
		||||
@@ -88,10 +88,10 @@ void CaptivePortal::start() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
 | 
			
		||||
  if (req->url() == F("/config.json")) {
 | 
			
		||||
  if (req->url() == ESPHOME_F("/config.json")) {
 | 
			
		||||
    this->handle_config(req);
 | 
			
		||||
    return;
 | 
			
		||||
  } else if (req->url() == F("/wifisave")) {
 | 
			
		||||
  } else if (req->url() == ESPHOME_F("/wifisave")) {
 | 
			
		||||
    this->handle_wifisave(req);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
@@ -100,11 +100,11 @@ void CaptivePortal::handleRequest(AsyncWebServerRequest *req) {
 | 
			
		||||
  // This includes OS captive portal detection endpoints which will trigger
 | 
			
		||||
  // the captive portal when they don't receive their expected responses
 | 
			
		||||
#ifndef USE_ESP8266
 | 
			
		||||
  auto *response = req->beginResponse(200, F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
 | 
			
		||||
  auto *response = req->beginResponse(200, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
 | 
			
		||||
#else
 | 
			
		||||
  auto *response = req->beginResponse_P(200, F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
 | 
			
		||||
  auto *response = req->beginResponse_P(200, ESPHOME_F("text/html"), INDEX_GZ, sizeof(INDEX_GZ));
 | 
			
		||||
#endif
 | 
			
		||||
  response->addHeader(F("Content-Encoding"), F("gzip"));
 | 
			
		||||
  response->addHeader(ESPHOME_F("Content-Encoding"), ESPHOME_F("gzip"));
 | 
			
		||||
  req->send(response);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ static const uint8_t C_M1106_CMD_SET_CO2_CALIB_RESPONSE[4] = {0x16, 0x01, 0x03,
 | 
			
		||||
 | 
			
		||||
uint8_t cm1106_checksum(const uint8_t *response, size_t len) {
 | 
			
		||||
  uint8_t crc = 0;
 | 
			
		||||
  for (int i = 0; i < len - 1; i++) {
 | 
			
		||||
  for (size_t i = 0; i < len - 1; i++) {
 | 
			
		||||
    crc -= response[i];
 | 
			
		||||
  }
 | 
			
		||||
  return crc;
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ void CopyLock::setup() {
 | 
			
		||||
 | 
			
		||||
  traits.set_assumed_state(source_->traits.get_assumed_state());
 | 
			
		||||
  traits.set_requires_code(source_->traits.get_requires_code());
 | 
			
		||||
  traits.set_supported_states(source_->traits.get_supported_states());
 | 
			
		||||
  traits.set_supported_states_mask(source_->traits.get_supported_states_mask());
 | 
			
		||||
  traits.set_supports_open(source_->traits.get_supports_open());
 | 
			
		||||
 | 
			
		||||
  this->publish_state(source_->state);
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ void DaikinArcClimate::transmit_query_() {
 | 
			
		||||
  uint8_t remote_header[8] = {0x11, 0xDA, 0x27, 0x00, 0x84, 0x87, 0x20, 0x00};
 | 
			
		||||
 | 
			
		||||
  // Calculate checksum
 | 
			
		||||
  for (int i = 0; i < sizeof(remote_header) - 1; i++) {
 | 
			
		||||
  for (size_t i = 0; i < sizeof(remote_header) - 1; i++) {
 | 
			
		||||
    remote_header[sizeof(remote_header) - 1] += remote_header[i];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -102,7 +102,7 @@ void DaikinArcClimate::transmit_state() {
 | 
			
		||||
  remote_state[9] = fan_speed & 0xff;
 | 
			
		||||
 | 
			
		||||
  // Calculate checksum
 | 
			
		||||
  for (int i = 0; i < sizeof(remote_header) - 1; i++) {
 | 
			
		||||
  for (size_t i = 0; i < sizeof(remote_header) - 1; i++) {
 | 
			
		||||
    remote_header[sizeof(remote_header) - 1] += remote_header[i];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -350,7 +350,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
 | 
			
		||||
  bool valid_daikin_frame = false;
 | 
			
		||||
  if (data.expect_item(DAIKIN_HEADER_MARK, DAIKIN_HEADER_SPACE)) {
 | 
			
		||||
    valid_daikin_frame = true;
 | 
			
		||||
    int bytes_count = data.size() / 2 / 8;
 | 
			
		||||
    size_t bytes_count = data.size() / 2 / 8;
 | 
			
		||||
    std::unique_ptr<char[]> buf(new char[bytes_count * 3 + 1]);
 | 
			
		||||
    buf[0] = '\0';
 | 
			
		||||
    for (size_t i = 0; i < bytes_count; i++) {
 | 
			
		||||
@@ -370,7 +370,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
 | 
			
		||||
  if (!valid_daikin_frame) {
 | 
			
		||||
    char sbuf[16 * 10 + 1];
 | 
			
		||||
    sbuf[0] = '\0';
 | 
			
		||||
    for (size_t j = 0; j < data.size(); j++) {
 | 
			
		||||
    for (size_t j = 0; j < static_cast<size_t>(data.size()); j++) {
 | 
			
		||||
      if ((j - 2) % 16 == 0) {
 | 
			
		||||
        if (j > 0) {
 | 
			
		||||
          ESP_LOGD(TAG, "DATA %04x: %s", (j - 16 > 0xffff ? 0 : j - 16), sbuf);
 | 
			
		||||
@@ -380,19 +380,26 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
 | 
			
		||||
      char type_ch = ' ';
 | 
			
		||||
      // debug_tolerance = 25%
 | 
			
		||||
 | 
			
		||||
      if (DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_MARK))
 | 
			
		||||
      if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_MARK)) <= data[j] &&
 | 
			
		||||
          data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_MARK)))
 | 
			
		||||
        type_ch = 'P';
 | 
			
		||||
      if (DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_SPACE))
 | 
			
		||||
      if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ARC_PRE_SPACE)) <= -data[j] &&
 | 
			
		||||
          -data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ARC_PRE_SPACE)))
 | 
			
		||||
        type_ch = 'a';
 | 
			
		||||
      if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_MARK))
 | 
			
		||||
      if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_HEADER_MARK)) <= data[j] &&
 | 
			
		||||
          data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_HEADER_MARK)))
 | 
			
		||||
        type_ch = 'H';
 | 
			
		||||
      if (DAIKIN_DBG_LOWER(DAIKIN_HEADER_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_HEADER_SPACE))
 | 
			
		||||
      if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_HEADER_SPACE)) <= -data[j] &&
 | 
			
		||||
          -data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_HEADER_SPACE)))
 | 
			
		||||
        type_ch = 'h';
 | 
			
		||||
      if (DAIKIN_DBG_LOWER(DAIKIN_BIT_MARK) <= data[j] && data[j] <= DAIKIN_DBG_UPPER(DAIKIN_BIT_MARK))
 | 
			
		||||
      if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_BIT_MARK)) <= data[j] &&
 | 
			
		||||
          data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_BIT_MARK)))
 | 
			
		||||
        type_ch = 'B';
 | 
			
		||||
      if (DAIKIN_DBG_LOWER(DAIKIN_ONE_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ONE_SPACE))
 | 
			
		||||
      if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ONE_SPACE)) <= -data[j] &&
 | 
			
		||||
          -data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ONE_SPACE)))
 | 
			
		||||
        type_ch = '1';
 | 
			
		||||
      if (DAIKIN_DBG_LOWER(DAIKIN_ZERO_SPACE) <= -data[j] && -data[j] <= DAIKIN_DBG_UPPER(DAIKIN_ZERO_SPACE))
 | 
			
		||||
      if (static_cast<int32_t>(DAIKIN_DBG_LOWER(DAIKIN_ZERO_SPACE)) <= -data[j] &&
 | 
			
		||||
          -data[j] <= static_cast<int32_t>(DAIKIN_DBG_UPPER(DAIKIN_ZERO_SPACE)))
 | 
			
		||||
        type_ch = '0';
 | 
			
		||||
 | 
			
		||||
      if (abs(data[j]) > 100000) {
 | 
			
		||||
@@ -400,7 +407,7 @@ bool DaikinArcClimate::on_receive(remote_base::RemoteReceiveData data) {
 | 
			
		||||
      } else {
 | 
			
		||||
        sprintf(sbuf, "%s%-5d[%c] ", sbuf, (int) (round(data[j] / 10.) * 10), type_ch);
 | 
			
		||||
      }
 | 
			
		||||
      if (j == data.size() - 1) {
 | 
			
		||||
      if (j + 1 == static_cast<size_t>(data.size())) {
 | 
			
		||||
        ESP_LOGD(TAG, "DATA %04x: %s", (j - 8 > 0xffff ? 0 : j - 8), sbuf);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ namespace dashboard_import {
 | 
			
		||||
 | 
			
		||||
static std::string g_package_import_url;  // NOLINT
 | 
			
		||||
 | 
			
		||||
std::string get_package_import_url() { return g_package_import_url; }
 | 
			
		||||
const std::string &get_package_import_url() { return g_package_import_url; }
 | 
			
		||||
void set_package_import_url(std::string url) { g_package_import_url = std::move(url); }
 | 
			
		||||
 | 
			
		||||
}  // namespace dashboard_import
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace dashboard_import {
 | 
			
		||||
 | 
			
		||||
std::string get_package_import_url();
 | 
			
		||||
const std::string &get_package_import_url();
 | 
			
		||||
void set_package_import_url(std::string url);
 | 
			
		||||
 | 
			
		||||
}  // namespace dashboard_import
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										1
									
								
								esphome/components/epaper_spi/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/epaper_spi/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
							
								
								
									
										80
									
								
								esphome/components/epaper_spi/display.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										80
									
								
								esphome/components/epaper_spi/display.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,80 @@
 | 
			
		||||
from esphome import core, pins
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import display, spi
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_BUSY_PIN,
 | 
			
		||||
    CONF_DC_PIN,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_LAMBDA,
 | 
			
		||||
    CONF_MODEL,
 | 
			
		||||
    CONF_PAGES,
 | 
			
		||||
    CONF_RESET_DURATION,
 | 
			
		||||
    CONF_RESET_PIN,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["split_buffer"]
 | 
			
		||||
DEPENDENCIES = ["spi"]
 | 
			
		||||
 | 
			
		||||
epaper_spi_ns = cg.esphome_ns.namespace("epaper_spi")
 | 
			
		||||
EPaperBase = epaper_spi_ns.class_(
 | 
			
		||||
    "EPaperBase", cg.PollingComponent, spi.SPIDevice, display.DisplayBuffer
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
EPaperSpectraE6 = epaper_spi_ns.class_("EPaperSpectraE6", EPaperBase)
 | 
			
		||||
EPaper7p3InSpectraE6 = epaper_spi_ns.class_("EPaper7p3InSpectraE6", EPaperSpectraE6)
 | 
			
		||||
 | 
			
		||||
MODELS = {
 | 
			
		||||
    "7.3in-spectra-e6": EPaper7p3InSpectraE6,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    display.FULL_DISPLAY_SCHEMA.extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(EPaperBase),
 | 
			
		||||
            cv.Required(CONF_DC_PIN): pins.gpio_output_pin_schema,
 | 
			
		||||
            cv.Required(CONF_MODEL): cv.one_of(*MODELS, lower=True, space="-"),
 | 
			
		||||
            cv.Optional(CONF_RESET_PIN): pins.gpio_output_pin_schema,
 | 
			
		||||
            cv.Optional(CONF_BUSY_PIN): pins.gpio_input_pin_schema,
 | 
			
		||||
            cv.Optional(CONF_RESET_DURATION): cv.All(
 | 
			
		||||
                cv.positive_time_period_milliseconds,
 | 
			
		||||
                cv.Range(max=core.TimePeriod(milliseconds=500)),
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
    .extend(spi.spi_device_schema()),
 | 
			
		||||
    cv.has_at_most_one_key(CONF_PAGES, CONF_LAMBDA),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = spi.final_validate_device_schema(
 | 
			
		||||
    "epaper_spi", require_miso=False, require_mosi=True
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    model = MODELS[config[CONF_MODEL]]
 | 
			
		||||
 | 
			
		||||
    rhs = model.new()
 | 
			
		||||
    var = cg.Pvariable(config[CONF_ID], rhs, model)
 | 
			
		||||
 | 
			
		||||
    await display.register_display(var, config)
 | 
			
		||||
    await spi.register_spi_device(var, config)
 | 
			
		||||
 | 
			
		||||
    dc = await cg.gpio_pin_expression(config[CONF_DC_PIN])
 | 
			
		||||
    cg.add(var.set_dc_pin(dc))
 | 
			
		||||
 | 
			
		||||
    if CONF_LAMBDA in config:
 | 
			
		||||
        lambda_ = await cg.process_lambda(
 | 
			
		||||
            config[CONF_LAMBDA], [(display.DisplayRef, "it")], return_type=cg.void
 | 
			
		||||
        )
 | 
			
		||||
        cg.add(var.set_writer(lambda_))
 | 
			
		||||
    if CONF_RESET_PIN in config:
 | 
			
		||||
        reset = await cg.gpio_pin_expression(config[CONF_RESET_PIN])
 | 
			
		||||
        cg.add(var.set_reset_pin(reset))
 | 
			
		||||
    if CONF_BUSY_PIN in config:
 | 
			
		||||
        busy = await cg.gpio_pin_expression(config[CONF_BUSY_PIN])
 | 
			
		||||
        cg.add(var.set_busy_pin(busy))
 | 
			
		||||
    if CONF_RESET_DURATION in config:
 | 
			
		||||
        cg.add(var.set_reset_duration(config[CONF_RESET_DURATION]))
 | 
			
		||||
							
								
								
									
										227
									
								
								esphome/components/epaper_spi/epaper_spi.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										227
									
								
								esphome/components/epaper_spi/epaper_spi.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,227 @@
 | 
			
		||||
#include "epaper_spi.h"
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome::epaper_spi {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "epaper_spi";
 | 
			
		||||
 | 
			
		||||
static const LogString *epaper_state_to_string(EPaperState state) {
 | 
			
		||||
  switch (state) {
 | 
			
		||||
    case EPaperState::IDLE:
 | 
			
		||||
      return LOG_STR("IDLE");
 | 
			
		||||
    case EPaperState::UPDATE:
 | 
			
		||||
      return LOG_STR("UPDATE");
 | 
			
		||||
    case EPaperState::RESET:
 | 
			
		||||
      return LOG_STR("RESET");
 | 
			
		||||
    case EPaperState::INITIALISE:
 | 
			
		||||
      return LOG_STR("INITIALISE");
 | 
			
		||||
    case EPaperState::TRANSFER_DATA:
 | 
			
		||||
      return LOG_STR("TRANSFER_DATA");
 | 
			
		||||
    case EPaperState::POWER_ON:
 | 
			
		||||
      return LOG_STR("POWER_ON");
 | 
			
		||||
    case EPaperState::REFRESH_SCREEN:
 | 
			
		||||
      return LOG_STR("REFRESH_SCREEN");
 | 
			
		||||
    case EPaperState::POWER_OFF:
 | 
			
		||||
      return LOG_STR("POWER_OFF");
 | 
			
		||||
    case EPaperState::DEEP_SLEEP:
 | 
			
		||||
      return LOG_STR("DEEP_SLEEP");
 | 
			
		||||
    default:
 | 
			
		||||
      return LOG_STR("UNKNOWN");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperBase::setup() {
 | 
			
		||||
  if (!this->init_buffer_(this->get_buffer_length())) {
 | 
			
		||||
    this->mark_failed("Failed to initialise buffer");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->setup_pins_();
 | 
			
		||||
  this->spi_setup();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool EPaperBase::init_buffer_(size_t buffer_length) {
 | 
			
		||||
  if (!this->buffer_.init(buffer_length)) {
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
  this->clear();
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperBase::setup_pins_() {
 | 
			
		||||
  this->dc_pin_->setup();  // OUTPUT
 | 
			
		||||
  this->dc_pin_->digital_write(false);
 | 
			
		||||
 | 
			
		||||
  if (this->reset_pin_ != nullptr) {
 | 
			
		||||
    this->reset_pin_->setup();  // OUTPUT
 | 
			
		||||
    this->reset_pin_->digital_write(true);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->busy_pin_ != nullptr) {
 | 
			
		||||
    this->busy_pin_->setup();  // INPUT
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float EPaperBase::get_setup_priority() const { return setup_priority::PROCESSOR; }
 | 
			
		||||
 | 
			
		||||
void EPaperBase::command(uint8_t value) {
 | 
			
		||||
  this->start_command_();
 | 
			
		||||
  this->write_byte(value);
 | 
			
		||||
  this->end_command_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperBase::data(uint8_t value) {
 | 
			
		||||
  this->start_data_();
 | 
			
		||||
  this->write_byte(value);
 | 
			
		||||
  this->end_data_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// write a command followed by zero or more bytes of data.
 | 
			
		||||
// The command is the first byte, length is the length of data only in the second byte, followed by the data.
 | 
			
		||||
// [COMMAND, LENGTH, DATA...]
 | 
			
		||||
void EPaperBase::cmd_data(const uint8_t *data) {
 | 
			
		||||
  const uint8_t command = data[0];
 | 
			
		||||
  const uint8_t length = data[1];
 | 
			
		||||
  const uint8_t *ptr = data + 2;
 | 
			
		||||
 | 
			
		||||
  ESP_LOGVV(TAG, "Command: 0x%02X, Length: %d, Data: %s", command, length,
 | 
			
		||||
            format_hex_pretty(ptr, length, '.', false).c_str());
 | 
			
		||||
 | 
			
		||||
  this->dc_pin_->digital_write(false);
 | 
			
		||||
  this->enable();
 | 
			
		||||
  this->write_byte(command);
 | 
			
		||||
  if (length > 0) {
 | 
			
		||||
    this->dc_pin_->digital_write(true);
 | 
			
		||||
    this->write_array(ptr, length);
 | 
			
		||||
  }
 | 
			
		||||
  this->disable();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool EPaperBase::is_idle_() {
 | 
			
		||||
  if (this->busy_pin_ == nullptr) {
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  return !this->busy_pin_->digital_read();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperBase::reset() {
 | 
			
		||||
  if (this->reset_pin_ != nullptr) {
 | 
			
		||||
    this->reset_pin_->digital_write(false);
 | 
			
		||||
    this->disable_loop();
 | 
			
		||||
    this->set_timeout(this->reset_duration_, [this] {
 | 
			
		||||
      this->reset_pin_->digital_write(true);
 | 
			
		||||
      this->set_timeout(20, [this] { this->enable_loop(); });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperBase::update() {
 | 
			
		||||
  if (!this->state_queue_.empty()) {
 | 
			
		||||
    ESP_LOGE(TAG, "Display update already in progress - %s",
 | 
			
		||||
             LOG_STR_ARG(epaper_state_to_string(this->state_queue_.front())));
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->state_queue_.push(EPaperState::UPDATE);
 | 
			
		||||
  this->state_queue_.push(EPaperState::RESET);
 | 
			
		||||
  this->state_queue_.push(EPaperState::INITIALISE);
 | 
			
		||||
  this->state_queue_.push(EPaperState::TRANSFER_DATA);
 | 
			
		||||
  this->state_queue_.push(EPaperState::POWER_ON);
 | 
			
		||||
  this->state_queue_.push(EPaperState::REFRESH_SCREEN);
 | 
			
		||||
  this->state_queue_.push(EPaperState::POWER_OFF);
 | 
			
		||||
  this->state_queue_.push(EPaperState::DEEP_SLEEP);
 | 
			
		||||
  this->state_queue_.push(EPaperState::IDLE);
 | 
			
		||||
 | 
			
		||||
  this->enable_loop();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperBase::loop() {
 | 
			
		||||
  if (this->waiting_for_idle_) {
 | 
			
		||||
    if (this->is_idle_()) {
 | 
			
		||||
      this->waiting_for_idle_ = false;
 | 
			
		||||
    } else {
 | 
			
		||||
      if (App.get_loop_component_start_time() - this->waiting_for_idle_last_print_ >= 1000) {
 | 
			
		||||
        ESP_LOGV(TAG, "Waiting for idle");
 | 
			
		||||
        this->waiting_for_idle_last_print_ = App.get_loop_component_start_time();
 | 
			
		||||
      }
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto state = this->state_queue_.front();
 | 
			
		||||
 | 
			
		||||
  switch (state) {
 | 
			
		||||
    case EPaperState::IDLE:
 | 
			
		||||
      this->disable_loop();
 | 
			
		||||
      break;
 | 
			
		||||
    case EPaperState::UPDATE:
 | 
			
		||||
      this->do_update_();  // Calls ESPHome (current page) lambda
 | 
			
		||||
      break;
 | 
			
		||||
    case EPaperState::RESET:
 | 
			
		||||
      this->reset();
 | 
			
		||||
      break;
 | 
			
		||||
    case EPaperState::INITIALISE:
 | 
			
		||||
      this->initialise_();
 | 
			
		||||
      break;
 | 
			
		||||
    case EPaperState::TRANSFER_DATA:
 | 
			
		||||
      if (!this->transfer_data()) {
 | 
			
		||||
        return;  // Not done yet, come back next loop
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case EPaperState::POWER_ON:
 | 
			
		||||
      this->power_on();
 | 
			
		||||
      break;
 | 
			
		||||
    case EPaperState::REFRESH_SCREEN:
 | 
			
		||||
      this->refresh_screen();
 | 
			
		||||
      break;
 | 
			
		||||
    case EPaperState::POWER_OFF:
 | 
			
		||||
      this->power_off();
 | 
			
		||||
      break;
 | 
			
		||||
    case EPaperState::DEEP_SLEEP:
 | 
			
		||||
      this->deep_sleep();
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
  this->state_queue_.pop();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperBase::start_command_() {
 | 
			
		||||
  this->dc_pin_->digital_write(false);
 | 
			
		||||
  this->enable();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperBase::end_command_() { this->disable(); }
 | 
			
		||||
 | 
			
		||||
void EPaperBase::start_data_() {
 | 
			
		||||
  this->dc_pin_->digital_write(true);
 | 
			
		||||
  this->enable();
 | 
			
		||||
}
 | 
			
		||||
void EPaperBase::end_data_() { this->disable(); }
 | 
			
		||||
 | 
			
		||||
void EPaperBase::on_safe_shutdown() { this->deep_sleep(); }
 | 
			
		||||
 | 
			
		||||
void EPaperBase::initialise_() {
 | 
			
		||||
  size_t index = 0;
 | 
			
		||||
  const auto &sequence = this->init_sequence_;
 | 
			
		||||
  const size_t sequence_size = this->init_sequence_length_;
 | 
			
		||||
  while (index != sequence_size) {
 | 
			
		||||
    if (sequence_size - index < 2) {
 | 
			
		||||
      this->mark_failed("Malformed init sequence");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    const auto *ptr = sequence + index;
 | 
			
		||||
    const uint8_t length = ptr[1];
 | 
			
		||||
    if (sequence_size - index < length + 2) {
 | 
			
		||||
      this->mark_failed("Malformed init sequence");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this->cmd_data(ptr);
 | 
			
		||||
    index += length + 2;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->power_on();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::epaper_spi
 | 
			
		||||
							
								
								
									
										93
									
								
								esphome/components/epaper_spi/epaper_spi.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								esphome/components/epaper_spi/epaper_spi.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,93 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/display/display_buffer.h"
 | 
			
		||||
#include "esphome/components/spi/spi.h"
 | 
			
		||||
#include "esphome/components/split_buffer/split_buffer.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
 | 
			
		||||
#include <queue>
 | 
			
		||||
 | 
			
		||||
namespace esphome::epaper_spi {
 | 
			
		||||
 | 
			
		||||
enum class EPaperState : uint8_t {
 | 
			
		||||
  IDLE,
 | 
			
		||||
  UPDATE,
 | 
			
		||||
  RESET,
 | 
			
		||||
  INITIALISE,
 | 
			
		||||
  TRANSFER_DATA,
 | 
			
		||||
  POWER_ON,
 | 
			
		||||
  REFRESH_SCREEN,
 | 
			
		||||
  POWER_OFF,
 | 
			
		||||
  DEEP_SLEEP,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
static const uint8_t MAX_TRANSFER_TIME = 10;  // Transfer in 10ms blocks to allow the loop to run
 | 
			
		||||
 | 
			
		||||
class EPaperBase : public display::DisplayBuffer,
 | 
			
		||||
                   public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW, spi::CLOCK_PHASE_LEADING,
 | 
			
		||||
                                         spi::DATA_RATE_2MHZ> {
 | 
			
		||||
 public:
 | 
			
		||||
  EPaperBase(const uint8_t *init_sequence, const size_t init_sequence_length)
 | 
			
		||||
      : init_sequence_length_(init_sequence_length), init_sequence_(init_sequence) {}
 | 
			
		||||
  void set_dc_pin(GPIOPin *dc_pin) { dc_pin_ = dc_pin; }
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
  void set_reset_pin(GPIOPin *reset) { this->reset_pin_ = reset; }
 | 
			
		||||
  void set_busy_pin(GPIOPin *busy) { this->busy_pin_ = busy; }
 | 
			
		||||
  void set_reset_duration(uint32_t reset_duration) { this->reset_duration_ = reset_duration; }
 | 
			
		||||
 | 
			
		||||
  void command(uint8_t value);
 | 
			
		||||
  void data(uint8_t value);
 | 
			
		||||
  void cmd_data(const uint8_t *data);
 | 
			
		||||
 | 
			
		||||
  void update() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
 | 
			
		||||
  void setup() override;
 | 
			
		||||
 | 
			
		||||
  void on_safe_shutdown() override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool is_idle_();
 | 
			
		||||
  void setup_pins_();
 | 
			
		||||
  virtual void reset();
 | 
			
		||||
  void initialise_();
 | 
			
		||||
  bool init_buffer_(size_t buffer_length);
 | 
			
		||||
 | 
			
		||||
  virtual int get_width_controller() { return this->get_width_internal(); };
 | 
			
		||||
  virtual void deep_sleep() = 0;
 | 
			
		||||
  /**
 | 
			
		||||
   * Send data to the device via SPI
 | 
			
		||||
   * @return true if done, false if should be called next loop
 | 
			
		||||
   */
 | 
			
		||||
  virtual bool transfer_data() = 0;
 | 
			
		||||
  virtual void refresh_screen() = 0;
 | 
			
		||||
 | 
			
		||||
  virtual void power_on() = 0;
 | 
			
		||||
  virtual void power_off() = 0;
 | 
			
		||||
  virtual uint32_t get_buffer_length() = 0;
 | 
			
		||||
 | 
			
		||||
  void start_command_();
 | 
			
		||||
  void end_command_();
 | 
			
		||||
  void start_data_();
 | 
			
		||||
  void end_data_();
 | 
			
		||||
 | 
			
		||||
  const size_t init_sequence_length_{0};
 | 
			
		||||
 | 
			
		||||
  size_t current_data_index_{0};
 | 
			
		||||
  uint32_t reset_duration_{200};
 | 
			
		||||
  uint32_t waiting_for_idle_last_print_{0};
 | 
			
		||||
 | 
			
		||||
  GPIOPin *dc_pin_;
 | 
			
		||||
  GPIOPin *busy_pin_{nullptr};
 | 
			
		||||
  GPIOPin *reset_pin_{nullptr};
 | 
			
		||||
 | 
			
		||||
  const uint8_t *init_sequence_{nullptr};
 | 
			
		||||
 | 
			
		||||
  bool waiting_for_idle_{false};
 | 
			
		||||
 | 
			
		||||
  split_buffer::SplitBuffer buffer_;
 | 
			
		||||
 | 
			
		||||
  std::queue<EPaperState> state_queue_{{EPaperState::IDLE}};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::epaper_spi
 | 
			
		||||
@@ -0,0 +1,42 @@
 | 
			
		||||
#include "epaper_spi_model_7p3in_spectra_e6.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome::epaper_spi {
 | 
			
		||||
 | 
			
		||||
static constexpr const char *const TAG = "epaper_spi.7.3in-spectra-e6";
 | 
			
		||||
 | 
			
		||||
void EPaper7p3InSpectraE6::power_on() {
 | 
			
		||||
  ESP_LOGI(TAG, "Power on");
 | 
			
		||||
  this->command(0x04);
 | 
			
		||||
  this->waiting_for_idle_ = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaper7p3InSpectraE6::power_off() {
 | 
			
		||||
  ESP_LOGI(TAG, "Power off");
 | 
			
		||||
  this->command(0x02);
 | 
			
		||||
  this->data(0x00);
 | 
			
		||||
  this->waiting_for_idle_ = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaper7p3InSpectraE6::refresh_screen() {
 | 
			
		||||
  ESP_LOGI(TAG, "Refresh");
 | 
			
		||||
  this->command(0x12);
 | 
			
		||||
  this->data(0x00);
 | 
			
		||||
  this->waiting_for_idle_ = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaper7p3InSpectraE6::deep_sleep() {
 | 
			
		||||
  ESP_LOGI(TAG, "Deep sleep");
 | 
			
		||||
  this->command(0x07);
 | 
			
		||||
  this->data(0xA5);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaper7p3InSpectraE6::dump_config() {
 | 
			
		||||
  LOG_DISPLAY("", "E-Paper SPI", this);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Model: 7.3in Spectra E6");
 | 
			
		||||
  LOG_PIN("  Reset Pin: ", this->reset_pin_);
 | 
			
		||||
  LOG_PIN("  DC Pin: ", this->dc_pin_);
 | 
			
		||||
  LOG_PIN("  Busy Pin: ", this->busy_pin_);
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::epaper_spi
 | 
			
		||||
@@ -0,0 +1,45 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "epaper_spi_spectra_e6.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome::epaper_spi {
 | 
			
		||||
 | 
			
		||||
class EPaper7p3InSpectraE6 : public EPaperSpectraE6 {
 | 
			
		||||
  static constexpr const uint16_t WIDTH = 800;
 | 
			
		||||
  static constexpr const uint16_t HEIGHT = 480;
 | 
			
		||||
  // clang-format off
 | 
			
		||||
 | 
			
		||||
  // Command, data length, data
 | 
			
		||||
  static constexpr uint8_t INIT_SEQUENCE[] = {
 | 
			
		||||
    0xAA, 6, 0x49, 0x55, 0x20, 0x08, 0x09, 0x18,
 | 
			
		||||
    0x01, 1, 0x3F,
 | 
			
		||||
    0x00, 2, 0x5F, 0x69,
 | 
			
		||||
    0x03, 4, 0x00, 0x54, 0x00, 0x44,
 | 
			
		||||
    0x05, 4, 0x40, 0x1F, 0x1F, 0x2C,
 | 
			
		||||
    0x06, 4, 0x6F, 0x1F, 0x17, 0x49,
 | 
			
		||||
    0x08, 4, 0x6F, 0x1F, 0x1F, 0x22,
 | 
			
		||||
    0x30, 1, 0x03,
 | 
			
		||||
    0x50, 1, 0x3F,
 | 
			
		||||
    0x60, 2, 0x02, 0x00,
 | 
			
		||||
    0x61, 4, WIDTH / 256, WIDTH % 256, HEIGHT / 256, HEIGHT % 256,
 | 
			
		||||
    0x84, 1, 0x01,
 | 
			
		||||
    0xE3, 1, 0x2F,
 | 
			
		||||
  };
 | 
			
		||||
  // clang-format on
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
  EPaper7p3InSpectraE6() : EPaperSpectraE6(INIT_SEQUENCE, sizeof(INIT_SEQUENCE)) {}
 | 
			
		||||
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  int get_width_internal() override { return WIDTH; };
 | 
			
		||||
  int get_height_internal() override { return HEIGHT; };
 | 
			
		||||
 | 
			
		||||
  void refresh_screen() override;
 | 
			
		||||
  void power_on() override;
 | 
			
		||||
  void power_off() override;
 | 
			
		||||
  void deep_sleep() override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::epaper_spi
 | 
			
		||||
							
								
								
									
										135
									
								
								esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								esphome/components/epaper_spi/epaper_spi_spectra_e6.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,135 @@
 | 
			
		||||
#include "epaper_spi_spectra_e6.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome::epaper_spi {
 | 
			
		||||
 | 
			
		||||
static constexpr const char *const TAG = "epaper_spi.6c";
 | 
			
		||||
 | 
			
		||||
static inline uint8_t color_to_hex(Color color) {
 | 
			
		||||
  if (color.red > 127) {
 | 
			
		||||
    if (color.green > 170) {
 | 
			
		||||
      if (color.blue > 127) {
 | 
			
		||||
        return 0x1;  // White
 | 
			
		||||
      } else {
 | 
			
		||||
        return 0x2;  // Yellow
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      return 0x3;  // Red (or Magenta)
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    if (color.green > 127) {
 | 
			
		||||
      if (color.blue > 127) {
 | 
			
		||||
        return 0x5;  // Cyan -> Blue
 | 
			
		||||
      } else {
 | 
			
		||||
        return 0x6;  // Green
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      if (color.blue > 127) {
 | 
			
		||||
        return 0x5;  // Blue
 | 
			
		||||
      } else {
 | 
			
		||||
        return 0x0;  // Black
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperSpectraE6::fill(Color color) {
 | 
			
		||||
  uint8_t pixel_color;
 | 
			
		||||
  if (color.is_on()) {
 | 
			
		||||
    pixel_color = color_to_hex(color);
 | 
			
		||||
  } else {
 | 
			
		||||
    pixel_color = 0x1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // We store 8 bitset<3> in 3 bytes
 | 
			
		||||
  // | byte 1 | byte 2 | byte 3 |
 | 
			
		||||
  // |aaabbbaa|abbbaaab|bbaaabbb|
 | 
			
		||||
  uint8_t byte_1 = pixel_color << 5 | pixel_color << 2 | pixel_color >> 1;
 | 
			
		||||
  uint8_t byte_2 = pixel_color << 7 | pixel_color << 4 | pixel_color << 1 | pixel_color >> 2;
 | 
			
		||||
  uint8_t byte_3 = pixel_color << 6 | pixel_color << 3 | pixel_color << 0;
 | 
			
		||||
 | 
			
		||||
  const size_t buffer_length = this->get_buffer_length();
 | 
			
		||||
  for (size_t i = 0; i < buffer_length; i += 3) {
 | 
			
		||||
    this->buffer_[i + 0] = byte_1;
 | 
			
		||||
    this->buffer_[i + 1] = byte_2;
 | 
			
		||||
    this->buffer_[i + 2] = byte_3;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint32_t EPaperSpectraE6::get_buffer_length() {
 | 
			
		||||
  // 6 colors buffer, 1 pixel = 3 bits, we will store 8 pixels in 24 bits = 3 bytes
 | 
			
		||||
  return this->get_width_controller() * this->get_height_internal() / 8u * 3u;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HOT EPaperSpectraE6::draw_absolute_pixel_internal(int x, int y, Color color) {
 | 
			
		||||
  if (x >= this->get_width_internal() || y >= this->get_height_internal() || x < 0 || y < 0)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  uint8_t pixel_bits = color_to_hex(color);
 | 
			
		||||
  uint32_t pixel_position = x + y * this->get_width_controller();
 | 
			
		||||
  uint32_t first_bit_position = pixel_position * 3;
 | 
			
		||||
  uint32_t byte_position = first_bit_position / 8u;
 | 
			
		||||
  uint32_t byte_subposition = first_bit_position % 8u;
 | 
			
		||||
 | 
			
		||||
  if (byte_subposition <= 5) {
 | 
			
		||||
    this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 << (5 - byte_subposition)))) |
 | 
			
		||||
                                   (pixel_bits << (5 - byte_subposition));
 | 
			
		||||
  } else {
 | 
			
		||||
    this->buffer_[byte_position] = (this->buffer_[byte_position] & (0xFF ^ (0b111 >> (byte_subposition - 5)))) |
 | 
			
		||||
                                   (pixel_bits >> (byte_subposition - 5));
 | 
			
		||||
 | 
			
		||||
    this->buffer_[byte_position + 1] =
 | 
			
		||||
        (this->buffer_[byte_position + 1] & (0xFF ^ (0xFF & (0b111 << (13 - byte_subposition))))) |
 | 
			
		||||
        (pixel_bits << (13 - byte_subposition));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool HOT EPaperSpectraE6::transfer_data() {
 | 
			
		||||
  const uint32_t start_time = App.get_loop_component_start_time();
 | 
			
		||||
  if (this->current_data_index_ == 0) {
 | 
			
		||||
    ESP_LOGV(TAG, "Sending data");
 | 
			
		||||
    this->command(0x10);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint8_t bytes_to_send[4]{0};
 | 
			
		||||
  const size_t buffer_length = this->get_buffer_length();
 | 
			
		||||
  for (size_t i = this->current_data_index_; i < buffer_length; i += 3) {
 | 
			
		||||
    const uint32_t triplet = encode_uint24(this->buffer_[i + 0], this->buffer_[i + 1], this->buffer_[i + 2]);
 | 
			
		||||
    // 8 pixels are stored in 3 bytes
 | 
			
		||||
    // |aaabbbaa|abbbaaab|bbaaabbb|
 | 
			
		||||
    // | byte 1 | byte 2 | byte 3 |
 | 
			
		||||
    bytes_to_send[0] = ((triplet >> 17) & 0b01110000) | ((triplet >> 18) & 0b00000111);
 | 
			
		||||
    bytes_to_send[1] = ((triplet >> 11) & 0b01110000) | ((triplet >> 12) & 0b00000111);
 | 
			
		||||
    bytes_to_send[2] = ((triplet >> 5) & 0b01110000) | ((triplet >> 6) & 0b00000111);
 | 
			
		||||
    bytes_to_send[3] = ((triplet << 1) & 0b01110000) | ((triplet << 0) & 0b00000111);
 | 
			
		||||
 | 
			
		||||
    this->start_data_();
 | 
			
		||||
    this->write_array(bytes_to_send, sizeof(bytes_to_send));
 | 
			
		||||
    this->end_data_();
 | 
			
		||||
 | 
			
		||||
    if (millis() - start_time > MAX_TRANSFER_TIME) {
 | 
			
		||||
      // Let the main loop run and come back next loop
 | 
			
		||||
      this->current_data_index_ = i + 3;
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  // Finished the entire dataset
 | 
			
		||||
  this->current_data_index_ = 0;
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void EPaperSpectraE6::reset() {
 | 
			
		||||
  if (this->reset_pin_ != nullptr) {
 | 
			
		||||
    this->disable_loop();
 | 
			
		||||
    this->reset_pin_->digital_write(true);
 | 
			
		||||
    this->set_timeout(20, [this] {
 | 
			
		||||
      this->reset_pin_->digital_write(false);
 | 
			
		||||
      delay(2);
 | 
			
		||||
      this->reset_pin_->digital_write(true);
 | 
			
		||||
      this->set_timeout(20, [this] { this->enable_loop(); });
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::epaper_spi
 | 
			
		||||
							
								
								
									
										23
									
								
								esphome/components/epaper_spi/epaper_spi_spectra_e6.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								esphome/components/epaper_spi/epaper_spi_spectra_e6.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,23 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "epaper_spi.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome::epaper_spi {
 | 
			
		||||
 | 
			
		||||
class EPaperSpectraE6 : public EPaperBase {
 | 
			
		||||
 public:
 | 
			
		||||
  EPaperSpectraE6(const uint8_t *init_sequence, const size_t init_sequence_length)
 | 
			
		||||
      : EPaperBase(init_sequence, init_sequence_length) {}
 | 
			
		||||
 | 
			
		||||
  display::DisplayType get_display_type() override { return display::DisplayType::DISPLAY_TYPE_COLOR; }
 | 
			
		||||
  void fill(Color color) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void draw_absolute_pixel_internal(int x, int y, Color color) override;
 | 
			
		||||
  uint32_t get_buffer_length() override;
 | 
			
		||||
 | 
			
		||||
  bool transfer_data() override;
 | 
			
		||||
  void reset() override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::epaper_spi
 | 
			
		||||
@@ -97,12 +97,12 @@ bool ES7210::set_mic_gain(float mic_gain) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ES7210::configure_sample_rate_() {
 | 
			
		||||
  int mclk_fre = this->sample_rate_ * MCLK_DIV_FRE;
 | 
			
		||||
  uint32_t mclk_fre = this->sample_rate_ * MCLK_DIV_FRE;
 | 
			
		||||
  int coeff = -1;
 | 
			
		||||
 | 
			
		||||
  for (int i = 0; i < (sizeof(ES7210_COEFFICIENTS) / sizeof(ES7210_COEFFICIENTS[0])); ++i) {
 | 
			
		||||
  for (size_t i = 0; i < (sizeof(ES7210_COEFFICIENTS) / sizeof(ES7210_COEFFICIENTS[0])); ++i) {
 | 
			
		||||
    if (ES7210_COEFFICIENTS[i].lrclk == this->sample_rate_ && ES7210_COEFFICIENTS[i].mclk == mclk_fre)
 | 
			
		||||
      coeff = i;
 | 
			
		||||
      coeff = static_cast<int>(i);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (coeff >= 0) {
 | 
			
		||||
 
 | 
			
		||||
@@ -296,14 +296,9 @@ def _format_framework_arduino_version(ver: cv.Version) -> str:
 | 
			
		||||
    return f"pioarduino/framework-arduinoespressif32@https://github.com/espressif/arduino-esp32/releases/download/{str(ver)}/esp32-{str(ver)}.zip"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _format_framework_espidf_version(
 | 
			
		||||
    ver: cv.Version, release: str, for_platformio: bool
 | 
			
		||||
) -> str:
 | 
			
		||||
    # format the given arduino (https://github.com/espressif/esp-idf/releases) version to
 | 
			
		||||
def _format_framework_espidf_version(ver: cv.Version, release: str) -> str:
 | 
			
		||||
    # format the given espidf (https://github.com/pioarduino/esp-idf/releases) version to
 | 
			
		||||
    # a PIO platformio/framework-espidf value
 | 
			
		||||
    # List of package versions: https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
 | 
			
		||||
    if for_platformio:
 | 
			
		||||
        return f"platformio/framework-espidf@~3.{ver.major}{ver.minor:02d}{ver.patch:02d}.0"
 | 
			
		||||
    if release:
 | 
			
		||||
        return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}.{release}/esp-idf-v{str(ver)}.zip"
 | 
			
		||||
    return f"pioarduino/framework-espidf@https://github.com/pioarduino/esp-idf/releases/download/v{str(ver)}/esp-idf-v{str(ver)}.zip"
 | 
			
		||||
@@ -317,157 +312,115 @@ def _format_framework_espidf_version(
 | 
			
		||||
 | 
			
		||||
# The default/recommended arduino framework version
 | 
			
		||||
#  - https://github.com/espressif/arduino-esp32/releases
 | 
			
		||||
RECOMMENDED_ARDUINO_FRAMEWORK_VERSION = cv.Version(3, 2, 1)
 | 
			
		||||
# The platform-espressif32 version to use for arduino frameworks
 | 
			
		||||
#  - https://github.com/pioarduino/platform-espressif32/releases
 | 
			
		||||
ARDUINO_PLATFORM_VERSION = cv.Version(54, 3, 21, "2")
 | 
			
		||||
ARDUINO_FRAMEWORK_VERSION_LOOKUP = {
 | 
			
		||||
    "recommended": cv.Version(3, 2, 1),
 | 
			
		||||
    "latest": cv.Version(3, 3, 2),
 | 
			
		||||
    "dev": cv.Version(3, 3, 2),
 | 
			
		||||
}
 | 
			
		||||
ARDUINO_PLATFORM_VERSION_LOOKUP = {
 | 
			
		||||
    cv.Version(3, 3, 2): cv.Version(55, 3, 31, "1"),
 | 
			
		||||
    cv.Version(3, 3, 1): cv.Version(55, 3, 31, "1"),
 | 
			
		||||
    cv.Version(3, 3, 0): cv.Version(55, 3, 30, "2"),
 | 
			
		||||
    cv.Version(3, 2, 1): cv.Version(54, 3, 21, "2"),
 | 
			
		||||
    cv.Version(3, 2, 0): cv.Version(54, 3, 20),
 | 
			
		||||
    cv.Version(3, 1, 3): cv.Version(53, 3, 13),
 | 
			
		||||
    cv.Version(3, 1, 2): cv.Version(53, 3, 12),
 | 
			
		||||
    cv.Version(3, 1, 1): cv.Version(53, 3, 11),
 | 
			
		||||
    cv.Version(3, 1, 0): cv.Version(53, 3, 10),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# The default/recommended esp-idf framework version
 | 
			
		||||
#  - https://github.com/espressif/esp-idf/releases
 | 
			
		||||
#  - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
 | 
			
		||||
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(5, 4, 2)
 | 
			
		||||
# The platformio/espressif32 version to use for esp-idf frameworks
 | 
			
		||||
#  - https://github.com/platformio/platform-espressif32/releases
 | 
			
		||||
#  - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
 | 
			
		||||
ESP_IDF_PLATFORM_VERSION = cv.Version(54, 3, 21, "2")
 | 
			
		||||
ESP_IDF_FRAMEWORK_VERSION_LOOKUP = {
 | 
			
		||||
    "recommended": cv.Version(5, 4, 2),
 | 
			
		||||
    "latest": cv.Version(5, 5, 1),
 | 
			
		||||
    "dev": cv.Version(5, 5, 1),
 | 
			
		||||
}
 | 
			
		||||
ESP_IDF_PLATFORM_VERSION_LOOKUP = {
 | 
			
		||||
    cv.Version(5, 5, 1): cv.Version(55, 3, 31, "1"),
 | 
			
		||||
    cv.Version(5, 5, 0): cv.Version(55, 3, 31, "1"),
 | 
			
		||||
    cv.Version(5, 4, 2): cv.Version(54, 3, 21, "2"),
 | 
			
		||||
    cv.Version(5, 4, 1): cv.Version(54, 3, 21, "2"),
 | 
			
		||||
    cv.Version(5, 4, 0): cv.Version(54, 3, 21, "2"),
 | 
			
		||||
    cv.Version(5, 3, 2): cv.Version(53, 3, 13),
 | 
			
		||||
    cv.Version(5, 3, 1): cv.Version(53, 3, 13),
 | 
			
		||||
    cv.Version(5, 3, 0): cv.Version(53, 3, 13),
 | 
			
		||||
    cv.Version(5, 1, 6): cv.Version(51, 3, 7),
 | 
			
		||||
    cv.Version(5, 1, 5): cv.Version(51, 3, 7),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
# List based on https://registry.platformio.org/tools/platformio/framework-espidf/versions
 | 
			
		||||
SUPPORTED_PLATFORMIO_ESP_IDF_5X = [
 | 
			
		||||
    cv.Version(5, 3, 1),
 | 
			
		||||
    cv.Version(5, 3, 0),
 | 
			
		||||
    cv.Version(5, 2, 2),
 | 
			
		||||
    cv.Version(5, 2, 1),
 | 
			
		||||
    cv.Version(5, 1, 2),
 | 
			
		||||
    cv.Version(5, 1, 1),
 | 
			
		||||
    cv.Version(5, 1, 0),
 | 
			
		||||
    cv.Version(5, 0, 2),
 | 
			
		||||
    cv.Version(5, 0, 1),
 | 
			
		||||
    cv.Version(5, 0, 0),
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
# pioarduino versions that don't require a release number
 | 
			
		||||
# List based on https://github.com/pioarduino/esp-idf/releases
 | 
			
		||||
SUPPORTED_PIOARDUINO_ESP_IDF_5X = [
 | 
			
		||||
    cv.Version(5, 5, 1),
 | 
			
		||||
    cv.Version(5, 5, 0),
 | 
			
		||||
    cv.Version(5, 4, 2),
 | 
			
		||||
    cv.Version(5, 4, 1),
 | 
			
		||||
    cv.Version(5, 4, 0),
 | 
			
		||||
    cv.Version(5, 3, 3),
 | 
			
		||||
    cv.Version(5, 3, 2),
 | 
			
		||||
    cv.Version(5, 3, 1),
 | 
			
		||||
    cv.Version(5, 3, 0),
 | 
			
		||||
    cv.Version(5, 1, 5),
 | 
			
		||||
    cv.Version(5, 1, 6),
 | 
			
		||||
]
 | 
			
		||||
# The platform-espressif32 version
 | 
			
		||||
#  - https://github.com/pioarduino/platform-espressif32/releases
 | 
			
		||||
PLATFORM_VERSION_LOOKUP = {
 | 
			
		||||
    "recommended": cv.Version(54, 3, 21, "2"),
 | 
			
		||||
    "latest": cv.Version(55, 3, 31, "1"),
 | 
			
		||||
    "dev": cv.Version(55, 3, 31, "1"),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _check_versions(value):
 | 
			
		||||
    value = value.copy()
 | 
			
		||||
    if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
 | 
			
		||||
        lookups = {
 | 
			
		||||
            "dev": (
 | 
			
		||||
                cv.Version(3, 2, 1),
 | 
			
		||||
                "https://github.com/espressif/arduino-esp32.git",
 | 
			
		||||
            ),
 | 
			
		||||
            "latest": (cv.Version(3, 2, 1), None),
 | 
			
		||||
            "recommended": (RECOMMENDED_ARDUINO_FRAMEWORK_VERSION, None),
 | 
			
		||||
        }
 | 
			
		||||
 | 
			
		||||
        if value[CONF_VERSION] in lookups:
 | 
			
		||||
            if CONF_SOURCE in value:
 | 
			
		||||
                raise cv.Invalid(
 | 
			
		||||
                    "Framework version needs to be explicitly specified when custom source is used."
 | 
			
		||||
                )
 | 
			
		||||
 | 
			
		||||
            version, source = lookups[value[CONF_VERSION]]
 | 
			
		||||
        else:
 | 
			
		||||
            version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
 | 
			
		||||
            source = value.get(CONF_SOURCE, None)
 | 
			
		||||
 | 
			
		||||
        value[CONF_VERSION] = str(version)
 | 
			
		||||
        value[CONF_SOURCE] = source or _format_framework_arduino_version(version)
 | 
			
		||||
 | 
			
		||||
        value[CONF_PLATFORM_VERSION] = value.get(
 | 
			
		||||
            CONF_PLATFORM_VERSION,
 | 
			
		||||
            _parse_platform_version(str(ARDUINO_PLATFORM_VERSION)),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        if value[CONF_SOURCE].startswith("http"):
 | 
			
		||||
            # prefix is necessary or platformio will complain with a cryptic error
 | 
			
		||||
            value[CONF_SOURCE] = f"framework-arduinoespressif32@{value[CONF_SOURCE]}"
 | 
			
		||||
 | 
			
		||||
        if version != RECOMMENDED_ARDUINO_FRAMEWORK_VERSION:
 | 
			
		||||
            _LOGGER.warning(
 | 
			
		||||
                "The selected Arduino framework version is not the recommended one. "
 | 
			
		||||
                "If there are connectivity or build issues please remove the manual version."
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
    lookups = {
 | 
			
		||||
        "dev": (cv.Version(5, 4, 2), "https://github.com/espressif/esp-idf.git"),
 | 
			
		||||
        "latest": (cv.Version(5, 2, 2), None),
 | 
			
		||||
        "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if value[CONF_VERSION] in lookups:
 | 
			
		||||
        if CONF_SOURCE in value:
 | 
			
		||||
    if value[CONF_VERSION] in PLATFORM_VERSION_LOOKUP:
 | 
			
		||||
        if CONF_SOURCE in value or CONF_PLATFORM_VERSION in value:
 | 
			
		||||
            raise cv.Invalid(
 | 
			
		||||
                "Framework version needs to be explicitly specified when custom source is used."
 | 
			
		||||
                "Version needs to be explicitly set when a custom source or platform_version is used."
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        version, source = lookups[value[CONF_VERSION]]
 | 
			
		||||
        platform_lookup = PLATFORM_VERSION_LOOKUP[value[CONF_VERSION]]
 | 
			
		||||
        value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup))
 | 
			
		||||
 | 
			
		||||
        if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
 | 
			
		||||
            version = ARDUINO_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]]
 | 
			
		||||
        else:
 | 
			
		||||
            version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP[value[CONF_VERSION]]
 | 
			
		||||
    else:
 | 
			
		||||
        version = cv.Version.parse(cv.version_number(value[CONF_VERSION]))
 | 
			
		||||
        source = value.get(CONF_SOURCE, None)
 | 
			
		||||
 | 
			
		||||
    if version < cv.Version(5, 0, 0):
 | 
			
		||||
        raise cv.Invalid("Only ESP-IDF 5.0+ is supported.")
 | 
			
		||||
 | 
			
		||||
    # flag this for later *before* we set value[CONF_PLATFORM_VERSION] below
 | 
			
		||||
    has_platform_ver = CONF_PLATFORM_VERSION in value
 | 
			
		||||
 | 
			
		||||
    value[CONF_PLATFORM_VERSION] = value.get(
 | 
			
		||||
        CONF_PLATFORM_VERSION, _parse_platform_version(str(ESP_IDF_PLATFORM_VERSION))
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        is_platformio := _platform_is_platformio(value[CONF_PLATFORM_VERSION])
 | 
			
		||||
    ) and version not in SUPPORTED_PLATFORMIO_ESP_IDF_5X:
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            f"ESP-IDF {str(version)} not supported by platformio/espressif32"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        version in SUPPORTED_PLATFORMIO_ESP_IDF_5X
 | 
			
		||||
        and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X
 | 
			
		||||
    ) and not has_platform_ver:
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            f"ESP-IDF {value[CONF_VERSION]} may be supported by platformio/espressif32; please specify '{CONF_PLATFORM_VERSION}'"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if (
 | 
			
		||||
        not is_platformio
 | 
			
		||||
        and CONF_RELEASE not in value
 | 
			
		||||
        and version not in SUPPORTED_PIOARDUINO_ESP_IDF_5X
 | 
			
		||||
    ):
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            f"ESP-IDF {value[CONF_VERSION]} is not available with pioarduino; you may need to specify '{CONF_RELEASE}'"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    value[CONF_VERSION] = str(version)
 | 
			
		||||
    value[CONF_SOURCE] = source or _format_framework_espidf_version(
 | 
			
		||||
        version, value.get(CONF_RELEASE, None), is_platformio
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    if value[CONF_SOURCE].startswith("http"):
 | 
			
		||||
        # prefix is necessary or platformio will complain with a cryptic error
 | 
			
		||||
        value[CONF_SOURCE] = f"framework-espidf@{value[CONF_SOURCE]}"
 | 
			
		||||
    if value[CONF_TYPE] == FRAMEWORK_ARDUINO:
 | 
			
		||||
        if version < cv.Version(3, 0, 0):
 | 
			
		||||
            raise cv.Invalid("Only Arduino 3.0+ is supported.")
 | 
			
		||||
        recommended_version = ARDUINO_FRAMEWORK_VERSION_LOOKUP["recommended"]
 | 
			
		||||
        platform_lookup = ARDUINO_PLATFORM_VERSION_LOOKUP.get(version)
 | 
			
		||||
        value[CONF_SOURCE] = value.get(
 | 
			
		||||
            CONF_SOURCE, _format_framework_arduino_version(version)
 | 
			
		||||
        )
 | 
			
		||||
        if value[CONF_SOURCE].startswith("http"):
 | 
			
		||||
            value[CONF_SOURCE] = (
 | 
			
		||||
                f"pioarduino/framework-arduinoespressif32@{value[CONF_SOURCE]}"
 | 
			
		||||
            )
 | 
			
		||||
    else:
 | 
			
		||||
        if version < cv.Version(5, 0, 0):
 | 
			
		||||
            raise cv.Invalid("Only ESP-IDF 5.0+ is supported.")
 | 
			
		||||
        recommended_version = ESP_IDF_FRAMEWORK_VERSION_LOOKUP["recommended"]
 | 
			
		||||
        platform_lookup = ESP_IDF_PLATFORM_VERSION_LOOKUP.get(version)
 | 
			
		||||
        value[CONF_SOURCE] = value.get(
 | 
			
		||||
            CONF_SOURCE,
 | 
			
		||||
            _format_framework_espidf_version(version, value.get(CONF_RELEASE, None)),
 | 
			
		||||
        )
 | 
			
		||||
        if value[CONF_SOURCE].startswith("http"):
 | 
			
		||||
            value[CONF_SOURCE] = f"pioarduino/framework-espidf@{value[CONF_SOURCE]}"
 | 
			
		||||
 | 
			
		||||
    if version != RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION:
 | 
			
		||||
    if CONF_PLATFORM_VERSION not in value:
 | 
			
		||||
        if platform_lookup is None:
 | 
			
		||||
            raise cv.Invalid(
 | 
			
		||||
                "Framework version not recognized; please specify platform_version"
 | 
			
		||||
            )
 | 
			
		||||
        value[CONF_PLATFORM_VERSION] = _parse_platform_version(str(platform_lookup))
 | 
			
		||||
 | 
			
		||||
    if version != recommended_version:
 | 
			
		||||
        _LOGGER.warning(
 | 
			
		||||
            "The selected ESP-IDF framework version is not the recommended one. "
 | 
			
		||||
            "The selected framework version is not the recommended one. "
 | 
			
		||||
            "If there are connectivity or build issues please remove the manual version."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    if value[CONF_PLATFORM_VERSION] != _parse_platform_version(
 | 
			
		||||
        str(PLATFORM_VERSION_LOOKUP["recommended"])
 | 
			
		||||
    ):
 | 
			
		||||
        _LOGGER.warning(
 | 
			
		||||
            "The selected platform version is not the recommended one. "
 | 
			
		||||
            "If there are connectivity or build issues please remove the manual version."
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
@@ -477,26 +430,14 @@ def _check_versions(value):
 | 
			
		||||
def _parse_platform_version(value):
 | 
			
		||||
    try:
 | 
			
		||||
        ver = cv.Version.parse(cv.version_number(value))
 | 
			
		||||
        if ver.major >= 50:  # a pioarduino version
 | 
			
		||||
            release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}"
 | 
			
		||||
            if ver.extra:
 | 
			
		||||
                release += f"-{ver.extra}"
 | 
			
		||||
            return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip"
 | 
			
		||||
        # if platform version is a valid version constraint, prefix the default package
 | 
			
		||||
        cv.platformio_version_constraint(value)
 | 
			
		||||
        return f"platformio/espressif32@{value}"
 | 
			
		||||
        release = f"{ver.major}.{ver.minor:02d}.{ver.patch:02d}"
 | 
			
		||||
        if ver.extra:
 | 
			
		||||
            release += f"-{ver.extra}"
 | 
			
		||||
        return f"https://github.com/pioarduino/platform-espressif32/releases/download/{release}/platform-espressif32.zip"
 | 
			
		||||
    except cv.Invalid:
 | 
			
		||||
        return value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _platform_is_platformio(value):
 | 
			
		||||
    try:
 | 
			
		||||
        ver = cv.Version.parse(cv.version_number(value))
 | 
			
		||||
        return ver.major < 50
 | 
			
		||||
    except cv.Invalid:
 | 
			
		||||
        return "platformio" in value
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _detect_variant(value):
 | 
			
		||||
    board = value.get(CONF_BOARD)
 | 
			
		||||
    variant = value.get(CONF_VARIANT)
 | 
			
		||||
@@ -705,6 +646,7 @@ def _show_framework_migration_message(name: str, variant: str) -> None:
 | 
			
		||||
        + "Why change? ESP-IDF offers:\n"
 | 
			
		||||
        + color(AnsiFore.GREEN, "  ✨ Up to 40% smaller binaries\n")
 | 
			
		||||
        + color(AnsiFore.GREEN, "  🚀 Better performance and optimization\n")
 | 
			
		||||
        + color(AnsiFore.GREEN, "  ⚡ 2-3x faster compile times\n")
 | 
			
		||||
        + color(AnsiFore.GREEN, "  📦 Custom-built firmware for your exact needs\n")
 | 
			
		||||
        + color(
 | 
			
		||||
            AnsiFore.GREEN,
 | 
			
		||||
@@ -712,7 +654,6 @@ def _show_framework_migration_message(name: str, variant: str) -> None:
 | 
			
		||||
        )
 | 
			
		||||
        + "\n"
 | 
			
		||||
        + "Trade-offs:\n"
 | 
			
		||||
        + color(AnsiFore.YELLOW, "  ⏱️  Compile times are ~25% longer\n")
 | 
			
		||||
        + color(AnsiFore.YELLOW, "  🔄 Some components need migration\n")
 | 
			
		||||
        + "\n"
 | 
			
		||||
        + "What should I do?\n"
 | 
			
		||||
@@ -808,6 +749,8 @@ async def to_code(config):
 | 
			
		||||
 | 
			
		||||
    conf = config[CONF_FRAMEWORK]
 | 
			
		||||
    cg.add_platformio_option("platform", conf[CONF_PLATFORM_VERSION])
 | 
			
		||||
    if CONF_SOURCE in conf:
 | 
			
		||||
        cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
 | 
			
		||||
 | 
			
		||||
    if conf[CONF_ADVANCED][CONF_IGNORE_EFUSE_CUSTOM_MAC]:
 | 
			
		||||
        cg.add_define("USE_ESP32_IGNORE_EFUSE_CUSTOM_MAC")
 | 
			
		||||
@@ -850,8 +793,6 @@ async def to_code(config):
 | 
			
		||||
 | 
			
		||||
    cg.add_build_flag("-Wno-nonnull-compare")
 | 
			
		||||
 | 
			
		||||
    cg.add_platformio_option("platform_packages", [conf[CONF_SOURCE]])
 | 
			
		||||
 | 
			
		||||
    add_idf_sdkconfig_option(f"CONFIG_IDF_TARGET_{variant}", True)
 | 
			
		||||
    add_idf_sdkconfig_option(
 | 
			
		||||
        f"CONFIG_ESPTOOLPY_FLASHSIZE_{config[CONF_FLASH_SIZE]}", True
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,8 @@
 | 
			
		||||
from collections.abc import Callable, MutableMapping
 | 
			
		||||
from enum import Enum
 | 
			
		||||
import logging
 | 
			
		||||
import re
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
from esphome import automation
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
@@ -9,16 +12,19 @@ from esphome.const import (
 | 
			
		||||
    CONF_ENABLE_ON_BOOT,
 | 
			
		||||
    CONF_ESPHOME,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_MAX_CONNECTIONS,
 | 
			
		||||
    CONF_NAME,
 | 
			
		||||
    CONF_NAME_ADD_MAC_SUFFIX,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import TimePeriod
 | 
			
		||||
from esphome.core import CORE, TimePeriod
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["esp32"]
 | 
			
		||||
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
 | 
			
		||||
DOMAIN = "esp32_ble"
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BTLoggers(Enum):
 | 
			
		||||
    """Bluetooth logger categories available in ESP-IDF.
 | 
			
		||||
@@ -127,6 +133,28 @@ CONF_DISABLE_BT_LOGS = "disable_bt_logs"
 | 
			
		||||
CONF_CONNECTION_TIMEOUT = "connection_timeout"
 | 
			
		||||
CONF_MAX_NOTIFICATIONS = "max_notifications"
 | 
			
		||||
 | 
			
		||||
# BLE connection limits
 | 
			
		||||
# ESP-IDF CONFIG_BT_ACL_CONNECTIONS has range 1-9, default 4
 | 
			
		||||
# Total instances: 10 (ADV + SCAN + connections)
 | 
			
		||||
# - ADV only: up to 9 connections
 | 
			
		||||
# - SCAN only: up to 9 connections
 | 
			
		||||
# - ADV + SCAN: up to 8 connections
 | 
			
		||||
DEFAULT_MAX_CONNECTIONS = 3
 | 
			
		||||
IDF_MAX_CONNECTIONS = 9
 | 
			
		||||
 | 
			
		||||
# Connection slot tracking keys
 | 
			
		||||
KEY_ESP32_BLE = "esp32_ble"
 | 
			
		||||
KEY_USED_CONNECTION_SLOTS = "used_connection_slots"
 | 
			
		||||
 | 
			
		||||
# Export for use by other components (bluetooth_proxy, etc.)
 | 
			
		||||
__all__ = [
 | 
			
		||||
    "DEFAULT_MAX_CONNECTIONS",
 | 
			
		||||
    "IDF_MAX_CONNECTIONS",
 | 
			
		||||
    "KEY_ESP32_BLE",
 | 
			
		||||
    "KEY_USED_CONNECTION_SLOTS",
 | 
			
		||||
    "consume_connection_slots",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2]
 | 
			
		||||
 | 
			
		||||
esp32_ble_ns = cg.esphome_ns.namespace("esp32_ble")
 | 
			
		||||
@@ -183,6 +211,9 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
            cv.positive_int,
 | 
			
		||||
            cv.Range(min=1, max=64),
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All(
 | 
			
		||||
            cv.positive_int, cv.Range(min=1, max=IDF_MAX_CONNECTIONS)
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
@@ -230,6 +261,60 @@ def validate_variant(_):
 | 
			
		||||
        raise cv.Invalid(f"{variant} does not support Bluetooth")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def consume_connection_slots(
 | 
			
		||||
    value: int, consumer: str
 | 
			
		||||
) -> Callable[[MutableMapping], MutableMapping]:
 | 
			
		||||
    """Reserve BLE connection slots for a component.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        value: Number of connection slots to reserve
 | 
			
		||||
        consumer: Name of the component consuming the slots
 | 
			
		||||
 | 
			
		||||
    Returns:
 | 
			
		||||
        A validator function that records the slot usage
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def _consume_connection_slots(config: MutableMapping) -> MutableMapping:
 | 
			
		||||
        data: dict[str, Any] = CORE.data.setdefault(KEY_ESP32_BLE, {})
 | 
			
		||||
        slots: list[str] = data.setdefault(KEY_USED_CONNECTION_SLOTS, [])
 | 
			
		||||
        slots.extend([consumer] * value)
 | 
			
		||||
        return config
 | 
			
		||||
 | 
			
		||||
    return _consume_connection_slots
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_connection_slots(max_connections: int) -> None:
 | 
			
		||||
    """Validate that BLE connection slots don't exceed the configured maximum."""
 | 
			
		||||
    # Skip validation in testing mode to allow component grouping
 | 
			
		||||
    if CORE.testing_mode:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    ble_data = CORE.data.get(KEY_ESP32_BLE, {})
 | 
			
		||||
    used_slots = ble_data.get(KEY_USED_CONNECTION_SLOTS, [])
 | 
			
		||||
    num_used = len(used_slots)
 | 
			
		||||
 | 
			
		||||
    if num_used <= max_connections:
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    slot_users = ", ".join(used_slots)
 | 
			
		||||
 | 
			
		||||
    if num_used > IDF_MAX_CONNECTIONS:
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            f"BLE components require {num_used} connection slots but maximum is {IDF_MAX_CONNECTIONS}. "
 | 
			
		||||
            f"Reduce the number of BLE clients. Components: {slot_users}"
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    _LOGGER.warning(
 | 
			
		||||
        "BLE components require %d connection slot(s) but only %d configured. "
 | 
			
		||||
        "Please set 'max_connections: %d' in the 'esp32_ble' component. "
 | 
			
		||||
        "Components: %s",
 | 
			
		||||
        num_used,
 | 
			
		||||
        max_connections,
 | 
			
		||||
        num_used,
 | 
			
		||||
        slot_users,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def final_validation(config):
 | 
			
		||||
    validate_variant(config)
 | 
			
		||||
    if (name := config.get(CONF_NAME)) is not None:
 | 
			
		||||
@@ -245,16 +330,44 @@ def final_validation(config):
 | 
			
		||||
    # Set GATT Client/Server sdkconfig options based on which components are loaded
 | 
			
		||||
    full_config = fv.full_config.get()
 | 
			
		||||
 | 
			
		||||
    # Validate connection slots usage
 | 
			
		||||
    max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
 | 
			
		||||
    validate_connection_slots(max_connections)
 | 
			
		||||
 | 
			
		||||
    # Check if BLE Server is needed
 | 
			
		||||
    has_ble_server = "esp32_ble_server" in full_config
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server)
 | 
			
		||||
 | 
			
		||||
    # Check if BLE Client is needed (via esp32_ble_tracker or esp32_ble_client)
 | 
			
		||||
    has_ble_client = (
 | 
			
		||||
        "esp32_ble_tracker" in full_config or "esp32_ble_client" in full_config
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # ESP-IDF BLE stack requires GATT Server to be enabled when GATT Client is enabled
 | 
			
		||||
    # This is an internal dependency in the Bluedroid stack (tested ESP-IDF 5.4.2-5.5.1)
 | 
			
		||||
    # See: https://github.com/espressif/esp-idf/issues/17724
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_BT_GATTS_ENABLE", has_ble_server or has_ble_client)
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_BT_GATTC_ENABLE", has_ble_client)
 | 
			
		||||
 | 
			
		||||
    # Handle max_connections: check for deprecated location in esp32_ble_tracker
 | 
			
		||||
    max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
 | 
			
		||||
 | 
			
		||||
    # Use value from tracker if esp32_ble doesn't have it explicitly set (backward compat)
 | 
			
		||||
    if "esp32_ble_tracker" in full_config:
 | 
			
		||||
        tracker_config = full_config["esp32_ble_tracker"]
 | 
			
		||||
        if "max_connections" in tracker_config and CONF_MAX_CONNECTIONS not in config:
 | 
			
		||||
            max_connections = tracker_config["max_connections"]
 | 
			
		||||
 | 
			
		||||
    # Set CONFIG_BT_ACL_CONNECTIONS to the maximum connections needed + 1 for ADV/SCAN
 | 
			
		||||
    # This is the Bluedroid host stack total instance limit (range 1-9, default 4)
 | 
			
		||||
    # Total instances = ADV/SCAN (1) + connection slots (max_connections)
 | 
			
		||||
    # Shared between client (tracker/ble_client) and server
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", max_connections + 1)
 | 
			
		||||
 | 
			
		||||
    # Set controller-specific max connections for ESP32 (classic)
 | 
			
		||||
    # CONFIG_BTDM_CTRL_BLE_MAX_CONN is ESP32-specific controller limit (just connections, not ADV/SCAN)
 | 
			
		||||
    # For newer chips (C3/S3/etc), different configs are used automatically
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_BTDM_CTRL_BLE_MAX_CONN", max_connections)
 | 
			
		||||
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -270,6 +383,10 @@ async def to_code(config):
 | 
			
		||||
        cg.add(var.set_name(name))
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
    # Define max connections for use in C++ code (e.g., ble_server.h)
 | 
			
		||||
    max_connections = config.get(CONF_MAX_CONNECTIONS, DEFAULT_MAX_CONNECTIONS)
 | 
			
		||||
    cg.add_define("USE_ESP32_BLE_MAX_CONNECTIONS", max_connections)
 | 
			
		||||
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -213,15 +213,17 @@ bool ESP32BLE::ble_setup_() {
 | 
			
		||||
  if (this->name_.has_value()) {
 | 
			
		||||
    name = this->name_.value();
 | 
			
		||||
    if (App.is_name_add_mac_suffix_enabled()) {
 | 
			
		||||
      name += "-" + get_mac_address().substr(6);
 | 
			
		||||
      name += "-";
 | 
			
		||||
      name += get_mac_address().substr(6);
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    name = App.get_name();
 | 
			
		||||
    if (name.length() > 20) {
 | 
			
		||||
      if (App.is_name_add_mac_suffix_enabled()) {
 | 
			
		||||
        name.erase(name.begin() + 13, name.end() - 7);  // Remove characters between 13 and the mac address
 | 
			
		||||
        // Keep first 13 chars and last 7 chars (MAC suffix), remove middle
 | 
			
		||||
        name.erase(13, name.length() - 20);
 | 
			
		||||
      } else {
 | 
			
		||||
        name = name.substr(0, 20);
 | 
			
		||||
        name.resize(20);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -152,7 +152,7 @@ void BLEAdvertising::loop() {
 | 
			
		||||
  if (now - this->last_advertisement_time_ > this->advertising_cycle_time_) {
 | 
			
		||||
    this->stop();
 | 
			
		||||
    this->current_adv_index_ += 1;
 | 
			
		||||
    if (this->current_adv_index_ >= this->raw_advertisements_callbacks_.size()) {
 | 
			
		||||
    if (static_cast<size_t>(this->current_adv_index_) >= this->raw_advertisements_callbacks_.size()) {
 | 
			
		||||
      this->current_adv_index_ = -1;
 | 
			
		||||
    }
 | 
			
		||||
    this->start();
 | 
			
		||||
 
 | 
			
		||||
@@ -42,32 +42,18 @@ ESPBTUUID ESPBTUUID::from_raw_reversed(const uint8_t *data) {
 | 
			
		||||
ESPBTUUID ESPBTUUID::from_raw(const std::string &data) {
 | 
			
		||||
  ESPBTUUID ret;
 | 
			
		||||
  if (data.length() == 4) {
 | 
			
		||||
    ret.uuid_.len = ESP_UUID_LEN_16;
 | 
			
		||||
    ret.uuid_.uuid.uuid16 = 0;
 | 
			
		||||
    for (uint i = 0; i < data.length(); i += 2) {
 | 
			
		||||
      uint8_t msb = data.c_str()[i];
 | 
			
		||||
      uint8_t lsb = data.c_str()[i + 1];
 | 
			
		||||
      uint8_t lsb_shift = i <= 2 ? (2 - i) * 4 : 0;
 | 
			
		||||
 | 
			
		||||
      if (msb > '9')
 | 
			
		||||
        msb -= 7;
 | 
			
		||||
      if (lsb > '9')
 | 
			
		||||
        lsb -= 7;
 | 
			
		||||
      ret.uuid_.uuid.uuid16 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << lsb_shift;
 | 
			
		||||
    // 16-bit UUID as 4-character hex string
 | 
			
		||||
    auto parsed = parse_hex<uint16_t>(data);
 | 
			
		||||
    if (parsed.has_value()) {
 | 
			
		||||
      ret.uuid_.len = ESP_UUID_LEN_16;
 | 
			
		||||
      ret.uuid_.uuid.uuid16 = parsed.value();
 | 
			
		||||
    }
 | 
			
		||||
  } else if (data.length() == 8) {
 | 
			
		||||
    ret.uuid_.len = ESP_UUID_LEN_32;
 | 
			
		||||
    ret.uuid_.uuid.uuid32 = 0;
 | 
			
		||||
    for (uint i = 0; i < data.length(); i += 2) {
 | 
			
		||||
      uint8_t msb = data.c_str()[i];
 | 
			
		||||
      uint8_t lsb = data.c_str()[i + 1];
 | 
			
		||||
      uint8_t lsb_shift = i <= 6 ? (6 - i) * 4 : 0;
 | 
			
		||||
 | 
			
		||||
      if (msb > '9')
 | 
			
		||||
        msb -= 7;
 | 
			
		||||
      if (lsb > '9')
 | 
			
		||||
        lsb -= 7;
 | 
			
		||||
      ret.uuid_.uuid.uuid32 += (((msb & 0x0F) << 4) | (lsb & 0x0F)) << lsb_shift;
 | 
			
		||||
    // 32-bit UUID as 8-character hex string
 | 
			
		||||
    auto parsed = parse_hex<uint32_t>(data);
 | 
			
		||||
    if (parsed.has_value()) {
 | 
			
		||||
      ret.uuid_.len = ESP_UUID_LEN_32;
 | 
			
		||||
      ret.uuid_.uuid.uuid32 = parsed.value();
 | 
			
		||||
    }
 | 
			
		||||
  } else if (data.length() == 16) {  // how we can have 16 byte length string reprezenting 128 bit uuid??? needs to be
 | 
			
		||||
                                     // investigated (lack of time)
 | 
			
		||||
@@ -145,28 +131,16 @@ bool ESPBTUUID::operator==(const ESPBTUUID &uuid) const {
 | 
			
		||||
  if (this->uuid_.len == uuid.uuid_.len) {
 | 
			
		||||
    switch (this->uuid_.len) {
 | 
			
		||||
      case ESP_UUID_LEN_16:
 | 
			
		||||
        if (uuid.uuid_.uuid.uuid16 == this->uuid_.uuid.uuid16) {
 | 
			
		||||
          return true;
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
        return this->uuid_.uuid.uuid16 == uuid.uuid_.uuid.uuid16;
 | 
			
		||||
      case ESP_UUID_LEN_32:
 | 
			
		||||
        if (uuid.uuid_.uuid.uuid32 == this->uuid_.uuid.uuid32) {
 | 
			
		||||
          return true;
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
        return this->uuid_.uuid.uuid32 == uuid.uuid_.uuid.uuid32;
 | 
			
		||||
      case ESP_UUID_LEN_128:
 | 
			
		||||
        for (uint8_t i = 0; i < ESP_UUID_LEN_128; i++) {
 | 
			
		||||
          if (uuid.uuid_.uuid.uuid128[i] != this->uuid_.uuid.uuid128[i]) {
 | 
			
		||||
            return false;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        return true;
 | 
			
		||||
        break;
 | 
			
		||||
        return memcmp(this->uuid_.uuid.uuid128, uuid.uuid_.uuid.uuid128, ESP_UUID_LEN_128) == 0;
 | 
			
		||||
      default:
 | 
			
		||||
        return false;
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    return this->as_128bit() == uuid.as_128bit();
 | 
			
		||||
  }
 | 
			
		||||
  return false;
 | 
			
		||||
  return this->as_128bit() == uuid.as_128bit();
 | 
			
		||||
}
 | 
			
		||||
esp_bt_uuid_t ESPBTUUID::get_uuid() const { return this->uuid_; }
 | 
			
		||||
std::string ESPBTUUID::to_string() const {
 | 
			
		||||
 
 | 
			
		||||
@@ -14,10 +14,6 @@
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
#include <esp32-hal-bt.h>
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble_beacon {
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,7 +26,7 @@ from esphome.const import (
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
from esphome.schema_extractors import SCHEMA_EXTRACT
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["esp32_ble", "bytebuffer", "event_emitter"]
 | 
			
		||||
AUTO_LOAD = ["esp32_ble", "bytebuffer"]
 | 
			
		||||
CODEOWNERS = ["@jesserockz", "@clydebarrow", "@Rapsssito"]
 | 
			
		||||
DEPENDENCIES = ["esp32"]
 | 
			
		||||
DOMAIN = "esp32_ble_server"
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,11 @@ void BLECharacteristic::notify() {
 | 
			
		||||
      this->service_->get_server()->get_connected_client_count() == 0)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  for (auto &client : this->service_->get_server()->get_clients()) {
 | 
			
		||||
  const uint16_t *clients = this->service_->get_server()->get_clients();
 | 
			
		||||
  uint8_t client_count = this->service_->get_server()->get_client_count();
 | 
			
		||||
 | 
			
		||||
  for (uint8_t i = 0; i < client_count; i++) {
 | 
			
		||||
    uint16_t client = clients[i];
 | 
			
		||||
    size_t length = this->value_.size();
 | 
			
		||||
    // Find the client in the list of clients to notify
 | 
			
		||||
    auto *entry = this->find_client_in_notify_list_(client);
 | 
			
		||||
@@ -73,7 +77,7 @@ void BLECharacteristic::notify() {
 | 
			
		||||
void BLECharacteristic::add_descriptor(BLEDescriptor *descriptor) {
 | 
			
		||||
  // If the descriptor is the CCCD descriptor, listen to its write event to know if the client wants to be notified
 | 
			
		||||
  if (descriptor->get_uuid() == ESPBTUUID::from_uint16(ESP_GATT_UUID_CHAR_CLIENT_CONFIG)) {
 | 
			
		||||
    descriptor->on(BLEDescriptorEvt::VectorEvt::ON_WRITE, [this](const std::vector<uint8_t> &value, uint16_t conn_id) {
 | 
			
		||||
    descriptor->on_write([this](std::span<const uint8_t> value, uint16_t conn_id) {
 | 
			
		||||
      if (value.size() != 2)
 | 
			
		||||
        return;
 | 
			
		||||
      uint16_t cccd = encode_uint16(value[1], value[0]);
 | 
			
		||||
@@ -121,69 +125,49 @@ bool BLECharacteristic::is_created() {
 | 
			
		||||
  if (this->state_ != CREATING_DEPENDENTS)
 | 
			
		||||
    return false;
 | 
			
		||||
 | 
			
		||||
  bool created = true;
 | 
			
		||||
  for (auto *descriptor : this->descriptors_) {
 | 
			
		||||
    created &= descriptor->is_created();
 | 
			
		||||
    if (!descriptor->is_created())
 | 
			
		||||
      return false;
 | 
			
		||||
  }
 | 
			
		||||
  if (created)
 | 
			
		||||
    this->state_ = CREATED;
 | 
			
		||||
  return this->state_ == CREATED;
 | 
			
		||||
  // All descriptors are created if we reach here
 | 
			
		||||
  this->state_ = CREATED;
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool BLECharacteristic::is_failed() {
 | 
			
		||||
  if (this->state_ == FAILED)
 | 
			
		||||
    return true;
 | 
			
		||||
 | 
			
		||||
  bool failed = false;
 | 
			
		||||
  for (auto *descriptor : this->descriptors_) {
 | 
			
		||||
    failed |= descriptor->is_failed();
 | 
			
		||||
    if (descriptor->is_failed()) {
 | 
			
		||||
      this->state_ = FAILED;
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLECharacteristic::set_property_bit_(esp_gatt_char_prop_t bit, bool value) {
 | 
			
		||||
  if (value) {
 | 
			
		||||
    this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | bit);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~bit);
 | 
			
		||||
  }
 | 
			
		||||
  if (failed)
 | 
			
		||||
    this->state_ = FAILED;
 | 
			
		||||
  return this->state_ == FAILED;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLECharacteristic::set_broadcast_property(bool value) {
 | 
			
		||||
  if (value) {
 | 
			
		||||
    this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_BROADCAST);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_BROADCAST);
 | 
			
		||||
  }
 | 
			
		||||
  this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_BROADCAST, value);
 | 
			
		||||
}
 | 
			
		||||
void BLECharacteristic::set_indicate_property(bool value) {
 | 
			
		||||
  if (value) {
 | 
			
		||||
    this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_INDICATE);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_INDICATE);
 | 
			
		||||
  }
 | 
			
		||||
  this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_INDICATE, value);
 | 
			
		||||
}
 | 
			
		||||
void BLECharacteristic::set_notify_property(bool value) {
 | 
			
		||||
  if (value) {
 | 
			
		||||
    this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_NOTIFY);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_NOTIFY);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void BLECharacteristic::set_read_property(bool value) {
 | 
			
		||||
  if (value) {
 | 
			
		||||
    this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_READ);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_READ);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void BLECharacteristic::set_write_property(bool value) {
 | 
			
		||||
  if (value) {
 | 
			
		||||
    this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE);
 | 
			
		||||
  }
 | 
			
		||||
  this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_NOTIFY, value);
 | 
			
		||||
}
 | 
			
		||||
void BLECharacteristic::set_read_property(bool value) { this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_READ, value); }
 | 
			
		||||
void BLECharacteristic::set_write_property(bool value) { this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_WRITE, value); }
 | 
			
		||||
void BLECharacteristic::set_write_no_response_property(bool value) {
 | 
			
		||||
  if (value) {
 | 
			
		||||
    this->properties_ = (esp_gatt_char_prop_t) (this->properties_ | ESP_GATT_CHAR_PROP_BIT_WRITE_NR);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->properties_ = (esp_gatt_char_prop_t) (this->properties_ & ~ESP_GATT_CHAR_PROP_BIT_WRITE_NR);
 | 
			
		||||
  }
 | 
			
		||||
  this->set_property_bit_(ESP_GATT_CHAR_PROP_BIT_WRITE_NR, value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
 | 
			
		||||
@@ -208,8 +192,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
 | 
			
		||||
      if (!param->read.need_rsp)
 | 
			
		||||
        break;  // For some reason you can request a read but not want a response
 | 
			
		||||
 | 
			
		||||
      this->EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t>::emit_(BLECharacteristicEvt::EmptyEvt::ON_READ,
 | 
			
		||||
                                                                          param->read.conn_id);
 | 
			
		||||
      if (this->on_read_callback_) {
 | 
			
		||||
        (*this->on_read_callback_)(param->read.conn_id);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      uint16_t max_offset = 22;
 | 
			
		||||
 | 
			
		||||
@@ -277,8 +262,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (!param->write.is_prep) {
 | 
			
		||||
        this->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::emit_(
 | 
			
		||||
            BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->write.conn_id);
 | 
			
		||||
        if (this->on_write_callback_) {
 | 
			
		||||
          (*this->on_write_callback_)(this->value_, param->write.conn_id);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      break;
 | 
			
		||||
@@ -289,8 +275,9 @@ void BLECharacteristic::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt
 | 
			
		||||
        break;
 | 
			
		||||
      this->write_event_ = false;
 | 
			
		||||
      if (param->exec_write.exec_write_flag == ESP_GATT_PREP_WRITE_EXEC) {
 | 
			
		||||
        this->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::emit_(
 | 
			
		||||
            BLECharacteristicEvt::VectorEvt::ON_WRITE, this->value_, param->exec_write.conn_id);
 | 
			
		||||
        if (this->on_write_callback_) {
 | 
			
		||||
          (*this->on_write_callback_)(this->value_, param->exec_write.conn_id);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      esp_err_t err =
 | 
			
		||||
          esp_ble_gatts_send_response(gatts_if, param->write.conn_id, param->write.trans_id, ESP_GATT_OK, nullptr);
 | 
			
		||||
 
 | 
			
		||||
@@ -2,10 +2,12 @@
 | 
			
		||||
 | 
			
		||||
#include "ble_descriptor.h"
 | 
			
		||||
#include "esphome/components/esp32_ble/ble_uuid.h"
 | 
			
		||||
#include "esphome/components/event_emitter/event_emitter.h"
 | 
			
		||||
#include "esphome/components/bytebuffer/bytebuffer.h"
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <span>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <memory>
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
@@ -22,22 +24,10 @@ namespace esp32_ble_server {
 | 
			
		||||
 | 
			
		||||
using namespace esp32_ble;
 | 
			
		||||
using namespace bytebuffer;
 | 
			
		||||
using namespace event_emitter;
 | 
			
		||||
 | 
			
		||||
class BLEService;
 | 
			
		||||
 | 
			
		||||
namespace BLECharacteristicEvt {
 | 
			
		||||
enum VectorEvt {
 | 
			
		||||
  ON_WRITE,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum EmptyEvt {
 | 
			
		||||
  ON_READ,
 | 
			
		||||
};
 | 
			
		||||
}  // namespace BLECharacteristicEvt
 | 
			
		||||
 | 
			
		||||
class BLECharacteristic : public EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>,
 | 
			
		||||
                          public EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t> {
 | 
			
		||||
class BLECharacteristic {
 | 
			
		||||
 public:
 | 
			
		||||
  BLECharacteristic(ESPBTUUID uuid, uint32_t properties);
 | 
			
		||||
  ~BLECharacteristic();
 | 
			
		||||
@@ -76,6 +66,15 @@ class BLECharacteristic : public EventEmitter<BLECharacteristicEvt::VectorEvt, s
 | 
			
		||||
  bool is_created();
 | 
			
		||||
  bool is_failed();
 | 
			
		||||
 | 
			
		||||
  // Direct callback registration - only allocates when callback is set
 | 
			
		||||
  void on_write(std::function<void(std::span<const uint8_t>, uint16_t)> &&callback) {
 | 
			
		||||
    this->on_write_callback_ =
 | 
			
		||||
        std::make_unique<std::function<void(std::span<const uint8_t>, uint16_t)>>(std::move(callback));
 | 
			
		||||
  }
 | 
			
		||||
  void on_read(std::function<void(uint16_t)> &&callback) {
 | 
			
		||||
    this->on_read_callback_ = std::make_unique<std::function<void(uint16_t)>>(std::move(callback));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool write_event_{false};
 | 
			
		||||
  BLEService *service_{};
 | 
			
		||||
@@ -98,6 +97,11 @@ class BLECharacteristic : public EventEmitter<BLECharacteristicEvt::VectorEvt, s
 | 
			
		||||
  void remove_client_from_notify_list_(uint16_t conn_id);
 | 
			
		||||
  ClientNotificationEntry *find_client_in_notify_list_(uint16_t conn_id);
 | 
			
		||||
 | 
			
		||||
  void set_property_bit_(esp_gatt_char_prop_t bit, bool value);
 | 
			
		||||
 | 
			
		||||
  std::unique_ptr<std::function<void(std::span<const uint8_t>, uint16_t)>> on_write_callback_;
 | 
			
		||||
  std::unique_ptr<std::function<void(uint16_t)>> on_read_callback_;
 | 
			
		||||
 | 
			
		||||
  esp_gatt_perm_t permissions_ = ESP_GATT_PERM_READ | ESP_GATT_PERM_WRITE;
 | 
			
		||||
 | 
			
		||||
  enum State : uint8_t {
 | 
			
		||||
 
 | 
			
		||||
@@ -74,9 +74,10 @@ void BLEDescriptor::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_
 | 
			
		||||
        break;
 | 
			
		||||
      this->value_.attr_len = param->write.len;
 | 
			
		||||
      memcpy(this->value_.attr_value, param->write.value, param->write.len);
 | 
			
		||||
      this->emit_(BLEDescriptorEvt::VectorEvt::ON_WRITE,
 | 
			
		||||
                  std::vector<uint8_t>(param->write.value, param->write.value + param->write.len),
 | 
			
		||||
                  param->write.conn_id);
 | 
			
		||||
      if (this->on_write_callback_) {
 | 
			
		||||
        (*this->on_write_callback_)(std::span<const uint8_t>(param->write.value, param->write.len),
 | 
			
		||||
                                    param->write.conn_id);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,30 +1,26 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/esp32_ble/ble_uuid.h"
 | 
			
		||||
#include "esphome/components/event_emitter/event_emitter.h"
 | 
			
		||||
#include "esphome/components/bytebuffer/bytebuffer.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include <esp_gatt_defs.h>
 | 
			
		||||
#include <esp_gatts_api.h>
 | 
			
		||||
#include <span>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <memory>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble_server {
 | 
			
		||||
 | 
			
		||||
using namespace esp32_ble;
 | 
			
		||||
using namespace bytebuffer;
 | 
			
		||||
using namespace event_emitter;
 | 
			
		||||
 | 
			
		||||
class BLECharacteristic;
 | 
			
		||||
 | 
			
		||||
namespace BLEDescriptorEvt {
 | 
			
		||||
enum VectorEvt {
 | 
			
		||||
  ON_WRITE,
 | 
			
		||||
};
 | 
			
		||||
}  // namespace BLEDescriptorEvt
 | 
			
		||||
 | 
			
		||||
class BLEDescriptor : public EventEmitter<BLEDescriptorEvt::VectorEvt, std::vector<uint8_t>, uint16_t> {
 | 
			
		||||
// Base class for BLE descriptors
 | 
			
		||||
class BLEDescriptor {
 | 
			
		||||
 public:
 | 
			
		||||
  BLEDescriptor(ESPBTUUID uuid, uint16_t max_len = 100, bool read = true, bool write = true);
 | 
			
		||||
  virtual ~BLEDescriptor();
 | 
			
		||||
@@ -39,6 +35,12 @@ class BLEDescriptor : public EventEmitter<BLEDescriptorEvt::VectorEvt, std::vect
 | 
			
		||||
  bool is_created() { return this->state_ == CREATED; }
 | 
			
		||||
  bool is_failed() { return this->state_ == FAILED; }
 | 
			
		||||
 | 
			
		||||
  // Direct callback registration - only allocates when callback is set
 | 
			
		||||
  void on_write(std::function<void(std::span<const uint8_t>, uint16_t)> &&callback) {
 | 
			
		||||
    this->on_write_callback_ =
 | 
			
		||||
        std::make_unique<std::function<void(std::span<const uint8_t>, uint16_t)>>(std::move(callback));
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  BLECharacteristic *characteristic_{nullptr};
 | 
			
		||||
  ESPBTUUID uuid_;
 | 
			
		||||
@@ -46,6 +48,8 @@ class BLEDescriptor : public EventEmitter<BLEDescriptorEvt::VectorEvt, std::vect
 | 
			
		||||
 | 
			
		||||
  esp_attr_value_t value_{};
 | 
			
		||||
 | 
			
		||||
  std::unique_ptr<std::function<void(std::span<const uint8_t>, uint16_t)>> on_write_callback_;
 | 
			
		||||
 | 
			
		||||
  esp_gatt_perm_t permissions_{};
 | 
			
		||||
 | 
			
		||||
  enum State : uint8_t {
 | 
			
		||||
 
 | 
			
		||||
@@ -147,20 +147,28 @@ BLEService *BLEServer::get_service(ESPBTUUID uuid, uint8_t inst_id) {
 | 
			
		||||
  return nullptr;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEServer::dispatch_callbacks_(CallbackType type, uint16_t conn_id) {
 | 
			
		||||
  for (auto &entry : this->callbacks_) {
 | 
			
		||||
    if (entry.type == type) {
 | 
			
		||||
      entry.callback(conn_id);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
 | 
			
		||||
                                    esp_ble_gatts_cb_param_t *param) {
 | 
			
		||||
  switch (event) {
 | 
			
		||||
    case ESP_GATTS_CONNECT_EVT: {
 | 
			
		||||
      ESP_LOGD(TAG, "BLE Client connected");
 | 
			
		||||
      this->add_client_(param->connect.conn_id);
 | 
			
		||||
      this->emit_(BLEServerEvt::EmptyEvt::ON_CONNECT, param->connect.conn_id);
 | 
			
		||||
      this->dispatch_callbacks_(CallbackType::ON_CONNECT, param->connect.conn_id);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTS_DISCONNECT_EVT: {
 | 
			
		||||
      ESP_LOGD(TAG, "BLE Client disconnected");
 | 
			
		||||
      this->remove_client_(param->disconnect.conn_id);
 | 
			
		||||
      this->parent_->advertising_start();
 | 
			
		||||
      this->emit_(BLEServerEvt::EmptyEvt::ON_DISCONNECT, param->disconnect.conn_id);
 | 
			
		||||
      this->dispatch_callbacks_(CallbackType::ON_DISCONNECT, param->disconnect.conn_id);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTS_REG_EVT: {
 | 
			
		||||
@@ -177,9 +185,38 @@ void BLEServer::gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t ga
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int8_t BLEServer::find_client_index_(uint16_t conn_id) const {
 | 
			
		||||
  for (uint8_t i = 0; i < this->client_count_; i++) {
 | 
			
		||||
    if (this->clients_[i] == conn_id)
 | 
			
		||||
      return i;
 | 
			
		||||
  }
 | 
			
		||||
  return -1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEServer::add_client_(uint16_t conn_id) {
 | 
			
		||||
  // Check if already in list
 | 
			
		||||
  if (this->find_client_index_(conn_id) >= 0)
 | 
			
		||||
    return;
 | 
			
		||||
  // Add if there's space
 | 
			
		||||
  if (this->client_count_ < USE_ESP32_BLE_MAX_CONNECTIONS) {
 | 
			
		||||
    this->clients_[this->client_count_++] = conn_id;
 | 
			
		||||
  } else {
 | 
			
		||||
    // This should never happen since max clients is known at compile time
 | 
			
		||||
    ESP_LOGE(TAG, "Client array full");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEServer::remove_client_(uint16_t conn_id) {
 | 
			
		||||
  int8_t index = this->find_client_index_(conn_id);
 | 
			
		||||
  if (index >= 0) {
 | 
			
		||||
    // Replace with last element and decrement count (client order not preserved)
 | 
			
		||||
    this->clients_[index] = this->clients_[--this->client_count_];
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEServer::ble_before_disabled_event_handler() {
 | 
			
		||||
  // Delete all clients
 | 
			
		||||
  this->clients_.clear();
 | 
			
		||||
  this->client_count_ = 0;
 | 
			
		||||
  // Delete all services
 | 
			
		||||
  for (auto &entry : this->services_) {
 | 
			
		||||
    entry.service->do_delete();
 | 
			
		||||
 
 | 
			
		||||
@@ -12,7 +12,7 @@
 | 
			
		||||
#include <memory>
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <unordered_map>
 | 
			
		||||
#include <unordered_set>
 | 
			
		||||
#include <functional>
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
@@ -24,18 +24,7 @@ namespace esp32_ble_server {
 | 
			
		||||
using namespace esp32_ble;
 | 
			
		||||
using namespace bytebuffer;
 | 
			
		||||
 | 
			
		||||
namespace BLEServerEvt {
 | 
			
		||||
enum EmptyEvt {
 | 
			
		||||
  ON_CONNECT,
 | 
			
		||||
  ON_DISCONNECT,
 | 
			
		||||
};
 | 
			
		||||
}  // namespace BLEServerEvt
 | 
			
		||||
 | 
			
		||||
class BLEServer : public Component,
 | 
			
		||||
                  public GATTsEventHandler,
 | 
			
		||||
                  public BLEStatusEventHandler,
 | 
			
		||||
                  public Parented<ESP32BLE>,
 | 
			
		||||
                  public EventEmitter<BLEServerEvt::EmptyEvt, uint16_t> {
 | 
			
		||||
class BLEServer : public Component, public GATTsEventHandler, public BLEStatusEventHandler, public Parented<ESP32BLE> {
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
@@ -57,15 +46,34 @@ class BLEServer : public Component,
 | 
			
		||||
  void set_device_information_service(BLEService *service) { this->device_information_service_ = service; }
 | 
			
		||||
 | 
			
		||||
  esp_gatt_if_t get_gatts_if() { return this->gatts_if_; }
 | 
			
		||||
  uint32_t get_connected_client_count() { return this->clients_.size(); }
 | 
			
		||||
  const std::unordered_set<uint16_t> &get_clients() { return this->clients_; }
 | 
			
		||||
  uint32_t get_connected_client_count() { return this->client_count_; }
 | 
			
		||||
  const uint16_t *get_clients() const { return this->clients_; }
 | 
			
		||||
  uint8_t get_client_count() const { return this->client_count_; }
 | 
			
		||||
 | 
			
		||||
  void gatts_event_handler(esp_gatts_cb_event_t event, esp_gatt_if_t gatts_if,
 | 
			
		||||
                           esp_ble_gatts_cb_param_t *param) override;
 | 
			
		||||
 | 
			
		||||
  void ble_before_disabled_event_handler() override;
 | 
			
		||||
 | 
			
		||||
  // Direct callback registration - supports multiple callbacks
 | 
			
		||||
  void on_connect(std::function<void(uint16_t)> &&callback) {
 | 
			
		||||
    this->callbacks_.push_back({CallbackType::ON_CONNECT, std::move(callback)});
 | 
			
		||||
  }
 | 
			
		||||
  void on_disconnect(std::function<void(uint16_t)> &&callback) {
 | 
			
		||||
    this->callbacks_.push_back({CallbackType::ON_DISCONNECT, std::move(callback)});
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  enum class CallbackType : uint8_t {
 | 
			
		||||
    ON_CONNECT,
 | 
			
		||||
    ON_DISCONNECT,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  struct CallbackEntry {
 | 
			
		||||
    CallbackType type;
 | 
			
		||||
    std::function<void(uint16_t)> callback;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  struct ServiceEntry {
 | 
			
		||||
    ESPBTUUID uuid;
 | 
			
		||||
    uint8_t inst_id;
 | 
			
		||||
@@ -74,14 +82,19 @@ class BLEServer : public Component,
 | 
			
		||||
 | 
			
		||||
  void restart_advertising_();
 | 
			
		||||
 | 
			
		||||
  void add_client_(uint16_t conn_id) { this->clients_.insert(conn_id); }
 | 
			
		||||
  void remove_client_(uint16_t conn_id) { this->clients_.erase(conn_id); }
 | 
			
		||||
  int8_t find_client_index_(uint16_t conn_id) const;
 | 
			
		||||
  void add_client_(uint16_t conn_id);
 | 
			
		||||
  void remove_client_(uint16_t conn_id);
 | 
			
		||||
  void dispatch_callbacks_(CallbackType type, uint16_t conn_id);
 | 
			
		||||
 | 
			
		||||
  std::vector<CallbackEntry> callbacks_;
 | 
			
		||||
 | 
			
		||||
  std::vector<uint8_t> manufacturer_data_{};
 | 
			
		||||
  esp_gatt_if_t gatts_if_{0};
 | 
			
		||||
  bool registered_{false};
 | 
			
		||||
 | 
			
		||||
  std::unordered_set<uint16_t> clients_;
 | 
			
		||||
  uint16_t clients_[USE_ESP32_BLE_MAX_CONNECTIONS]{};
 | 
			
		||||
  uint8_t client_count_{0};
 | 
			
		||||
  std::vector<ServiceEntry> services_{};
 | 
			
		||||
  std::vector<BLEService *> services_to_start_{};
 | 
			
		||||
  BLEService *device_information_service_{};
 | 
			
		||||
 
 | 
			
		||||
@@ -14,9 +14,10 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
 | 
			
		||||
    BLECharacteristic *characteristic) {
 | 
			
		||||
  Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger =  // NOLINT(cppcoreguidelines-owning-memory)
 | 
			
		||||
      new Trigger<std::vector<uint8_t>, uint16_t>();
 | 
			
		||||
  characteristic->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::on(
 | 
			
		||||
      BLECharacteristicEvt::VectorEvt::ON_WRITE,
 | 
			
		||||
      [on_write_trigger](const std::vector<uint8_t> &data, uint16_t id) { on_write_trigger->trigger(data, id); });
 | 
			
		||||
  characteristic->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
 | 
			
		||||
    // Convert span to vector for trigger
 | 
			
		||||
    on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
 | 
			
		||||
  });
 | 
			
		||||
  return on_write_trigger;
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -25,9 +26,10 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_characteristic_on_w
 | 
			
		||||
Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write_trigger(BLEDescriptor *descriptor) {
 | 
			
		||||
  Trigger<std::vector<uint8_t>, uint16_t> *on_write_trigger =  // NOLINT(cppcoreguidelines-owning-memory)
 | 
			
		||||
      new Trigger<std::vector<uint8_t>, uint16_t>();
 | 
			
		||||
  descriptor->on(
 | 
			
		||||
      BLEDescriptorEvt::VectorEvt::ON_WRITE,
 | 
			
		||||
      [on_write_trigger](const std::vector<uint8_t> &data, uint16_t id) { on_write_trigger->trigger(data, id); });
 | 
			
		||||
  descriptor->on_write([on_write_trigger](std::span<const uint8_t> data, uint16_t id) {
 | 
			
		||||
    // Convert span to vector for trigger
 | 
			
		||||
    on_write_trigger->trigger(std::vector<uint8_t>(data.begin(), data.end()), id);
 | 
			
		||||
  });
 | 
			
		||||
  return on_write_trigger;
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -35,8 +37,7 @@ Trigger<std::vector<uint8_t>, uint16_t> *BLETriggers::create_descriptor_on_write
 | 
			
		||||
#ifdef USE_ESP32_BLE_SERVER_ON_CONNECT
 | 
			
		||||
Trigger<uint16_t> *BLETriggers::create_server_on_connect_trigger(BLEServer *server) {
 | 
			
		||||
  Trigger<uint16_t> *on_connect_trigger = new Trigger<uint16_t>();  // NOLINT(cppcoreguidelines-owning-memory)
 | 
			
		||||
  server->on(BLEServerEvt::EmptyEvt::ON_CONNECT,
 | 
			
		||||
             [on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); });
 | 
			
		||||
  server->on_connect([on_connect_trigger](uint16_t conn_id) { on_connect_trigger->trigger(conn_id); });
 | 
			
		||||
  return on_connect_trigger;
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
@@ -44,38 +45,22 @@ Trigger<uint16_t> *BLETriggers::create_server_on_connect_trigger(BLEServer *serv
 | 
			
		||||
#ifdef USE_ESP32_BLE_SERVER_ON_DISCONNECT
 | 
			
		||||
Trigger<uint16_t> *BLETriggers::create_server_on_disconnect_trigger(BLEServer *server) {
 | 
			
		||||
  Trigger<uint16_t> *on_disconnect_trigger = new Trigger<uint16_t>();  // NOLINT(cppcoreguidelines-owning-memory)
 | 
			
		||||
  server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT,
 | 
			
		||||
             [on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); });
 | 
			
		||||
  server->on_disconnect([on_disconnect_trigger](uint16_t conn_id) { on_disconnect_trigger->trigger(conn_id); });
 | 
			
		||||
  return on_disconnect_trigger;
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION
 | 
			
		||||
void BLECharacteristicSetValueActionManager::set_listener(BLECharacteristic *characteristic,
 | 
			
		||||
                                                          EventEmitterListenerID listener_id,
 | 
			
		||||
                                                          const std::function<void()> &pre_notify_listener) {
 | 
			
		||||
  // Find and remove existing listener for this characteristic
 | 
			
		||||
  auto *existing = this->find_listener_(characteristic);
 | 
			
		||||
  if (existing != nullptr) {
 | 
			
		||||
    // Remove the previous listener
 | 
			
		||||
    characteristic->EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t>::off(BLECharacteristicEvt::EmptyEvt::ON_READ,
 | 
			
		||||
                                                                                existing->listener_id);
 | 
			
		||||
    // Remove the pre-notify listener
 | 
			
		||||
    this->off(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, existing->pre_notify_listener_id);
 | 
			
		||||
    // Remove from vector
 | 
			
		||||
    this->remove_listener_(characteristic);
 | 
			
		||||
  }
 | 
			
		||||
  // Create a new listener for the pre-notify event
 | 
			
		||||
  EventEmitterListenerID pre_notify_listener_id =
 | 
			
		||||
      this->on(BLECharacteristicSetValueActionEvt::PRE_NOTIFY,
 | 
			
		||||
               [pre_notify_listener, characteristic](const BLECharacteristic *evt_characteristic) {
 | 
			
		||||
                 // Only call the pre-notify listener if the characteristic is the one we are interested in
 | 
			
		||||
                 if (characteristic == evt_characteristic) {
 | 
			
		||||
                   pre_notify_listener();
 | 
			
		||||
                 }
 | 
			
		||||
               });
 | 
			
		||||
  // Save the entry to the vector
 | 
			
		||||
  this->listeners_.push_back({characteristic, listener_id, pre_notify_listener_id});
 | 
			
		||||
  this->listeners_.push_back({characteristic, pre_notify_listener});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
BLECharacteristicSetValueActionManager::ListenerEntry *BLECharacteristicSetValueActionManager::find_listener_(
 | 
			
		||||
 
 | 
			
		||||
@@ -4,7 +4,6 @@
 | 
			
		||||
#include "ble_characteristic.h"
 | 
			
		||||
#include "ble_descriptor.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/event_emitter/event_emitter.h"
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
@@ -18,10 +17,6 @@ namespace esp32_ble_server {
 | 
			
		||||
namespace esp32_ble_server_automations {
 | 
			
		||||
 | 
			
		||||
using namespace esp32_ble;
 | 
			
		||||
using namespace event_emitter;
 | 
			
		||||
 | 
			
		||||
// Invalid listener ID constant - 0 is used as sentinel value in EventEmitter
 | 
			
		||||
static constexpr EventEmitterListenerID INVALID_LISTENER_ID = 0;
 | 
			
		||||
 | 
			
		||||
class BLETriggers {
 | 
			
		||||
 public:
 | 
			
		||||
@@ -41,38 +36,29 @@ class BLETriggers {
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_BLE_SERVER_SET_VALUE_ACTION
 | 
			
		||||
enum BLECharacteristicSetValueActionEvt {
 | 
			
		||||
  PRE_NOTIFY,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Class to make sure only one BLECharacteristicSetValueAction is active at a time for each characteristic
 | 
			
		||||
class BLECharacteristicSetValueActionManager
 | 
			
		||||
    : public EventEmitter<BLECharacteristicSetValueActionEvt, BLECharacteristic *> {
 | 
			
		||||
class BLECharacteristicSetValueActionManager {
 | 
			
		||||
 public:
 | 
			
		||||
  // Singleton pattern
 | 
			
		||||
  static BLECharacteristicSetValueActionManager *get_instance() {
 | 
			
		||||
    static BLECharacteristicSetValueActionManager instance;
 | 
			
		||||
    return &instance;
 | 
			
		||||
  }
 | 
			
		||||
  void set_listener(BLECharacteristic *characteristic, EventEmitterListenerID listener_id,
 | 
			
		||||
                    const std::function<void()> &pre_notify_listener);
 | 
			
		||||
  EventEmitterListenerID get_listener(BLECharacteristic *characteristic) {
 | 
			
		||||
  void set_listener(BLECharacteristic *characteristic, const std::function<void()> &pre_notify_listener);
 | 
			
		||||
  bool has_listener(BLECharacteristic *characteristic) { return this->find_listener_(characteristic) != nullptr; }
 | 
			
		||||
  void emit_pre_notify(BLECharacteristic *characteristic) {
 | 
			
		||||
    for (const auto &entry : this->listeners_) {
 | 
			
		||||
      if (entry.characteristic == characteristic) {
 | 
			
		||||
        return entry.listener_id;
 | 
			
		||||
        entry.pre_notify_listener();
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return INVALID_LISTENER_ID;
 | 
			
		||||
  }
 | 
			
		||||
  void emit_pre_notify(BLECharacteristic *characteristic) {
 | 
			
		||||
    this->emit_(BLECharacteristicSetValueActionEvt::PRE_NOTIFY, characteristic);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  struct ListenerEntry {
 | 
			
		||||
    BLECharacteristic *characteristic;
 | 
			
		||||
    EventEmitterListenerID listener_id;
 | 
			
		||||
    EventEmitterListenerID pre_notify_listener_id;
 | 
			
		||||
    std::function<void()> pre_notify_listener;
 | 
			
		||||
  };
 | 
			
		||||
  std::vector<ListenerEntry> listeners_;
 | 
			
		||||
 | 
			
		||||
@@ -87,24 +73,22 @@ template<typename... Ts> class BLECharacteristicSetValueAction : public Action<T
 | 
			
		||||
  void set_buffer(ByteBuffer buffer) { this->set_buffer(buffer.get_data()); }
 | 
			
		||||
  void play(Ts... x) override {
 | 
			
		||||
    // If the listener is already set, do nothing
 | 
			
		||||
    if (BLECharacteristicSetValueActionManager::get_instance()->get_listener(this->parent_) == this->listener_id_)
 | 
			
		||||
    if (BLECharacteristicSetValueActionManager::get_instance()->has_listener(this->parent_))
 | 
			
		||||
      return;
 | 
			
		||||
    // Set initial value
 | 
			
		||||
    this->parent_->set_value(this->buffer_.value(x...));
 | 
			
		||||
    // Set the listener for read events
 | 
			
		||||
    this->listener_id_ = this->parent_->EventEmitter<BLECharacteristicEvt::EmptyEvt, uint16_t>::on(
 | 
			
		||||
        BLECharacteristicEvt::EmptyEvt::ON_READ, [this, x...](uint16_t id) {
 | 
			
		||||
          // Set the value of the characteristic every time it is read
 | 
			
		||||
          this->parent_->set_value(this->buffer_.value(x...));
 | 
			
		||||
        });
 | 
			
		||||
    this->parent_->on_read([this, x...](uint16_t id) {
 | 
			
		||||
      // Set the value of the characteristic every time it is read
 | 
			
		||||
      this->parent_->set_value(this->buffer_.value(x...));
 | 
			
		||||
    });
 | 
			
		||||
    // Set the listener in the global manager so only one BLECharacteristicSetValueAction is set for each characteristic
 | 
			
		||||
    BLECharacteristicSetValueActionManager::get_instance()->set_listener(
 | 
			
		||||
        this->parent_, this->listener_id_, [this, x...]() { this->parent_->set_value(this->buffer_.value(x...)); });
 | 
			
		||||
        this->parent_, [this, x...]() { this->parent_->set_value(this->buffer_.value(x...)); });
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  BLECharacteristic *parent_;
 | 
			
		||||
  EventEmitterListenerID listener_id_;
 | 
			
		||||
};
 | 
			
		||||
#endif  // USE_ESP32_BLE_SERVER_SET_VALUE_ACTION
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,14 +1,13 @@
 | 
			
		||||
from __future__ import annotations
 | 
			
		||||
 | 
			
		||||
from collections.abc import Callable, MutableMapping
 | 
			
		||||
import logging
 | 
			
		||||
from typing import Any
 | 
			
		||||
 | 
			
		||||
from esphome import automation
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import esp32_ble
 | 
			
		||||
from esphome.components.esp32 import add_idf_sdkconfig_option
 | 
			
		||||
from esphome.components.esp32_ble import (
 | 
			
		||||
    IDF_MAX_CONNECTIONS,
 | 
			
		||||
    BTLoggers,
 | 
			
		||||
    bt_uuid,
 | 
			
		||||
    bt_uuid16_format,
 | 
			
		||||
@@ -24,6 +23,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_INTERVAL,
 | 
			
		||||
    CONF_MAC_ADDRESS,
 | 
			
		||||
    CONF_MANUFACTURER_ID,
 | 
			
		||||
    CONF_MAX_CONNECTIONS,
 | 
			
		||||
    CONF_ON_BLE_ADVERTISE,
 | 
			
		||||
    CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
 | 
			
		||||
    CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
 | 
			
		||||
@@ -38,19 +38,12 @@ AUTO_LOAD = ["esp32_ble"]
 | 
			
		||||
DEPENDENCIES = ["esp32"]
 | 
			
		||||
CODEOWNERS = ["@bdraco"]
 | 
			
		||||
 | 
			
		||||
KEY_ESP32_BLE_TRACKER = "esp32_ble_tracker"
 | 
			
		||||
KEY_USED_CONNECTION_SLOTS = "used_connection_slots"
 | 
			
		||||
 | 
			
		||||
CONF_MAX_CONNECTIONS = "max_connections"
 | 
			
		||||
CONF_ESP32_BLE_ID = "esp32_ble_id"
 | 
			
		||||
CONF_SCAN_PARAMETERS = "scan_parameters"
 | 
			
		||||
CONF_WINDOW = "window"
 | 
			
		||||
CONF_ON_SCAN_END = "on_scan_end"
 | 
			
		||||
CONF_SOFTWARE_COEXISTENCE = "software_coexistence"
 | 
			
		||||
 | 
			
		||||
DEFAULT_MAX_CONNECTIONS = 3
 | 
			
		||||
IDF_MAX_CONNECTIONS = 9
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -128,6 +121,15 @@ def validate_scan_parameters(config):
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_max_connections_deprecated(config: ConfigType) -> ConfigType:
 | 
			
		||||
    if CONF_MAX_CONNECTIONS in config:
 | 
			
		||||
        _LOGGER.warning(
 | 
			
		||||
            "The 'max_connections' option in 'esp32_ble_tracker' is deprecated. "
 | 
			
		||||
            "Please move it to the 'esp32_ble' component instead."
 | 
			
		||||
        )
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def as_hex(value):
 | 
			
		||||
    return cg.RawExpression(f"0x{value}ULL")
 | 
			
		||||
 | 
			
		||||
@@ -150,24 +152,12 @@ def as_reversed_hex_array(value):
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def consume_connection_slots(
 | 
			
		||||
    value: int, consumer: str
 | 
			
		||||
) -> Callable[[MutableMapping], MutableMapping]:
 | 
			
		||||
    def _consume_connection_slots(config: MutableMapping) -> MutableMapping:
 | 
			
		||||
        data: dict[str, Any] = CORE.data.setdefault(KEY_ESP32_BLE_TRACKER, {})
 | 
			
		||||
        slots: list[str] = data.setdefault(KEY_USED_CONNECTION_SLOTS, [])
 | 
			
		||||
        slots.extend([consumer] * value)
 | 
			
		||||
        return config
 | 
			
		||||
 | 
			
		||||
    return _consume_connection_slots
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(ESP32BLETracker),
 | 
			
		||||
            cv.GenerateID(esp32_ble.CONF_BLE_ID): cv.use_id(esp32_ble.ESP32BLE),
 | 
			
		||||
            cv.Optional(CONF_MAX_CONNECTIONS, default=DEFAULT_MAX_CONNECTIONS): cv.All(
 | 
			
		||||
            cv.Optional(CONF_MAX_CONNECTIONS): cv.All(
 | 
			
		||||
                cv.positive_int, cv.Range(min=0, max=IDF_MAX_CONNECTIONS)
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_SCAN_PARAMETERS, default={}): cv.All(
 | 
			
		||||
@@ -224,48 +214,11 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
            cv.OnlyWith(CONF_SOFTWARE_COEXISTENCE, "wifi", default=True): bool,
 | 
			
		||||
        }
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    validate_max_connections_deprecated,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_remaining_connections(config):
 | 
			
		||||
    data: dict[str, Any] = CORE.data.get(KEY_ESP32_BLE_TRACKER, {})
 | 
			
		||||
    slots: list[str] = data.get(KEY_USED_CONNECTION_SLOTS, [])
 | 
			
		||||
    used_slots = len(slots)
 | 
			
		||||
    if used_slots <= config[CONF_MAX_CONNECTIONS]:
 | 
			
		||||
        return config
 | 
			
		||||
    slot_users = ", ".join(slots)
 | 
			
		||||
 | 
			
		||||
    if used_slots < IDF_MAX_CONNECTIONS:
 | 
			
		||||
        _LOGGER.warning(
 | 
			
		||||
            "esp32_ble_tracker exceeded `%s`: components attempted to consume %d "
 | 
			
		||||
            "connection slot(s) out of available configured maximum %d connection "
 | 
			
		||||
            "slot(s); The system automatically increased `%s` to %d to match the "
 | 
			
		||||
            "number of used connection slot(s) by components: %s.",
 | 
			
		||||
            CONF_MAX_CONNECTIONS,
 | 
			
		||||
            used_slots,
 | 
			
		||||
            config[CONF_MAX_CONNECTIONS],
 | 
			
		||||
            CONF_MAX_CONNECTIONS,
 | 
			
		||||
            used_slots,
 | 
			
		||||
            slot_users,
 | 
			
		||||
        )
 | 
			
		||||
        config[CONF_MAX_CONNECTIONS] = used_slots
 | 
			
		||||
        return config
 | 
			
		||||
 | 
			
		||||
    msg = (
 | 
			
		||||
        f"esp32_ble_tracker exceeded `{CONF_MAX_CONNECTIONS}`: "
 | 
			
		||||
        f"components attempted to consume {used_slots} connection slot(s) "
 | 
			
		||||
        f"out of available configured maximum {config[CONF_MAX_CONNECTIONS]} "
 | 
			
		||||
        f"connection slot(s); Decrease the number of BLE clients ({slot_users})"
 | 
			
		||||
    )
 | 
			
		||||
    if config[CONF_MAX_CONNECTIONS] < IDF_MAX_CONNECTIONS:
 | 
			
		||||
        msg += f" or increase {CONF_MAX_CONNECTIONS}` to {used_slots}"
 | 
			
		||||
    msg += f" to stay under the {IDF_MAX_CONNECTIONS} connection slot(s) limit."
 | 
			
		||||
    raise cv.Invalid(msg)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = cv.All(
 | 
			
		||||
    validate_remaining_connections, esp32_ble.validate_variant
 | 
			
		||||
)
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = esp32_ble.validate_variant
 | 
			
		||||
 | 
			
		||||
ESP_BLE_DEVICE_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
@@ -345,10 +298,8 @@ async def to_code(config):
 | 
			
		||||
    # Match arduino CONFIG_BTU_TASK_STACK_SIZE
 | 
			
		||||
    # https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_BT_BTU_TASK_STACK_SIZE", 8192)
 | 
			
		||||
    add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9)
 | 
			
		||||
    add_idf_sdkconfig_option(
 | 
			
		||||
        "CONFIG_BTDM_CTRL_BLE_MAX_CONN", config[CONF_MAX_CONNECTIONS]
 | 
			
		||||
    )
 | 
			
		||||
    # Note: CONFIG_BT_ACL_CONNECTIONS and CONFIG_BTDM_CTRL_BLE_MAX_CONN are now
 | 
			
		||||
    # configured in esp32_ble component based on max_connections setting
 | 
			
		||||
 | 
			
		||||
    cg.add_define("USE_OTA_STATE_CALLBACK")  # To be notified when an OTA update starts
 | 
			
		||||
    cg.add_define("USE_ESP32_BLE_CLIENT")
 | 
			
		||||
 
 | 
			
		||||
@@ -25,10 +25,6 @@
 | 
			
		||||
#include <esp_coexist.h>
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
#include <esp32-hal-bt.h>
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#define MBEDTLS_AES_ALT
 | 
			
		||||
#include <aes_alt.h>
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -67,8 +67,16 @@ static bool get_bitrate(canbus::CanSpeed bitrate, twai_timing_config_t *t_config
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ESP32Can::setup_internal() {
 | 
			
		||||
  static int next_twai_ctrl_num = 0;
 | 
			
		||||
  if (static_cast<unsigned>(next_twai_ctrl_num) >= SOC_TWAI_CONTROLLER_NUM) {
 | 
			
		||||
    ESP_LOGW(TAG, "Maximum number of esp32_can components created already");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  twai_general_config_t g_config =
 | 
			
		||||
      TWAI_GENERAL_CONFIG_DEFAULT((gpio_num_t) this->tx_, (gpio_num_t) this->rx_, TWAI_MODE_NORMAL);
 | 
			
		||||
  g_config.controller_id = next_twai_ctrl_num++;
 | 
			
		||||
  if (this->tx_queue_len_.has_value()) {
 | 
			
		||||
    g_config.tx_queue_len = this->tx_queue_len_.value();
 | 
			
		||||
  }
 | 
			
		||||
@@ -86,14 +94,14 @@ bool ESP32Can::setup_internal() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Install TWAI driver
 | 
			
		||||
  if (twai_driver_install(&g_config, &t_config, &f_config) != ESP_OK) {
 | 
			
		||||
  if (twai_driver_install_v2(&g_config, &t_config, &f_config, &(this->twai_handle_)) != ESP_OK) {
 | 
			
		||||
    // Failed to install driver
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Start TWAI driver
 | 
			
		||||
  if (twai_start() != ESP_OK) {
 | 
			
		||||
  if (twai_start_v2(this->twai_handle_) != ESP_OK) {
 | 
			
		||||
    // Failed to start driver
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return false;
 | 
			
		||||
@@ -102,6 +110,11 @@ bool ESP32Can::setup_internal() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) {
 | 
			
		||||
  if (this->twai_handle_ == nullptr) {
 | 
			
		||||
    // not setup yet or setup failed
 | 
			
		||||
    return canbus::ERROR_FAIL;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (frame->can_data_length_code > canbus::CAN_MAX_DATA_LENGTH) {
 | 
			
		||||
    return canbus::ERROR_FAILTX;
 | 
			
		||||
  }
 | 
			
		||||
@@ -124,7 +137,7 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) {
 | 
			
		||||
    memcpy(message.data, frame->data, frame->can_data_length_code);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (twai_transmit(&message, this->tx_enqueue_timeout_ticks_) == ESP_OK) {
 | 
			
		||||
  if (twai_transmit_v2(this->twai_handle_, &message, this->tx_enqueue_timeout_ticks_) == ESP_OK) {
 | 
			
		||||
    return canbus::ERROR_OK;
 | 
			
		||||
  } else {
 | 
			
		||||
    return canbus::ERROR_ALLTXBUSY;
 | 
			
		||||
@@ -132,9 +145,14 @@ canbus::Error ESP32Can::send_message(struct canbus::CanFrame *frame) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
canbus::Error ESP32Can::read_message(struct canbus::CanFrame *frame) {
 | 
			
		||||
  if (this->twai_handle_ == nullptr) {
 | 
			
		||||
    // not setup yet or setup failed
 | 
			
		||||
    return canbus::ERROR_FAIL;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  twai_message_t message;
 | 
			
		||||
 | 
			
		||||
  if (twai_receive(&message, 0) != ESP_OK) {
 | 
			
		||||
  if (twai_receive_v2(this->twai_handle_, &message, 0) != ESP_OK) {
 | 
			
		||||
    return canbus::ERROR_NOMSG;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,8 @@
 | 
			
		||||
#include "esphome/components/canbus/canbus.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
 | 
			
		||||
#include <driver/twai.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_can {
 | 
			
		||||
 | 
			
		||||
@@ -29,6 +31,7 @@ class ESP32Can : public canbus::Canbus {
 | 
			
		||||
  TickType_t tx_enqueue_timeout_ticks_{};
 | 
			
		||||
  optional<uint32_t> tx_queue_len_{};
 | 
			
		||||
  optional<uint32_t> rx_queue_len_{};
 | 
			
		||||
  twai_handle_t twai_handle_{nullptr};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_can
 | 
			
		||||
 
 | 
			
		||||
@@ -38,8 +38,7 @@ void ESP32ImprovComponent::setup() {
 | 
			
		||||
    });
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
  global_ble_server->on(BLEServerEvt::EmptyEvt::ON_DISCONNECT,
 | 
			
		||||
                        [this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); });
 | 
			
		||||
  global_ble_server->on_disconnect([this](uint16_t conn_id) { this->set_error_(improv::ERROR_NONE); });
 | 
			
		||||
 | 
			
		||||
  // Start with loop disabled - will be enabled by start() when needed
 | 
			
		||||
  this->disable_loop();
 | 
			
		||||
@@ -57,12 +56,11 @@ void ESP32ImprovComponent::setup_characteristics() {
 | 
			
		||||
  this->error_->add_descriptor(error_descriptor);
 | 
			
		||||
 | 
			
		||||
  this->rpc_ = this->service_->create_characteristic(improv::RPC_COMMAND_UUID, BLECharacteristic::PROPERTY_WRITE);
 | 
			
		||||
  this->rpc_->EventEmitter<BLECharacteristicEvt::VectorEvt, std::vector<uint8_t>, uint16_t>::on(
 | 
			
		||||
      BLECharacteristicEvt::VectorEvt::ON_WRITE, [this](const std::vector<uint8_t> &data, uint16_t id) {
 | 
			
		||||
        if (!data.empty()) {
 | 
			
		||||
          this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end());
 | 
			
		||||
        }
 | 
			
		||||
      });
 | 
			
		||||
  this->rpc_->on_write([this](std::span<const uint8_t> data, uint16_t id) {
 | 
			
		||||
    if (!data.empty()) {
 | 
			
		||||
      this->incoming_data_.insert(this->incoming_data_.end(), data.begin(), data.end());
 | 
			
		||||
    }
 | 
			
		||||
  });
 | 
			
		||||
  BLEDescriptor *rpc_descriptor = new BLE2902();
 | 
			
		||||
  this->rpc_->add_descriptor(rpc_descriptor);
 | 
			
		||||
 | 
			
		||||
@@ -145,6 +143,7 @@ void ESP32ImprovComponent::loop() {
 | 
			
		||||
#else
 | 
			
		||||
      this->set_state_(improv::STATE_AUTHORIZED);
 | 
			
		||||
#endif
 | 
			
		||||
      this->check_wifi_connection_();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case improv::STATE_AUTHORIZED: {
 | 
			
		||||
@@ -158,31 +157,12 @@ void ESP32ImprovComponent::loop() {
 | 
			
		||||
      if (!this->check_identify_()) {
 | 
			
		||||
        this->set_status_indicator_state_((now % 1000) < 500);
 | 
			
		||||
      }
 | 
			
		||||
      this->check_wifi_connection_();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case improv::STATE_PROVISIONING: {
 | 
			
		||||
      this->set_status_indicator_state_((now % 200) < 100);
 | 
			
		||||
      if (wifi::global_wifi_component->is_connected()) {
 | 
			
		||||
        wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(),
 | 
			
		||||
                                                   this->connecting_sta_.get_password());
 | 
			
		||||
        this->connecting_sta_ = {};
 | 
			
		||||
        this->cancel_timeout("wifi-connect-timeout");
 | 
			
		||||
        this->set_state_(improv::STATE_PROVISIONED);
 | 
			
		||||
 | 
			
		||||
        std::vector<std::string> urls = {ESPHOME_MY_LINK};
 | 
			
		||||
#ifdef USE_WEBSERVER
 | 
			
		||||
        for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
 | 
			
		||||
          if (ip.is_ip4()) {
 | 
			
		||||
            std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
 | 
			
		||||
            urls.push_back(webserver_url);
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
#endif
 | 
			
		||||
        std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls);
 | 
			
		||||
        this->send_response_(data);
 | 
			
		||||
        this->stop();
 | 
			
		||||
      }
 | 
			
		||||
      this->check_wifi_connection_();
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case improv::STATE_PROVISIONED: {
 | 
			
		||||
@@ -394,6 +374,36 @@ void ESP32ImprovComponent::on_wifi_connect_timeout_() {
 | 
			
		||||
  wifi::global_wifi_component->clear_sta();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32ImprovComponent::check_wifi_connection_() {
 | 
			
		||||
  if (!wifi::global_wifi_component->is_connected()) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->state_ == improv::STATE_PROVISIONING) {
 | 
			
		||||
    wifi::global_wifi_component->save_wifi_sta(this->connecting_sta_.get_ssid(), this->connecting_sta_.get_password());
 | 
			
		||||
    this->connecting_sta_ = {};
 | 
			
		||||
    this->cancel_timeout("wifi-connect-timeout");
 | 
			
		||||
 | 
			
		||||
    std::vector<std::string> urls = {ESPHOME_MY_LINK};
 | 
			
		||||
#ifdef USE_WEBSERVER
 | 
			
		||||
    for (auto &ip : wifi::global_wifi_component->wifi_sta_ip_addresses()) {
 | 
			
		||||
      if (ip.is_ip4()) {
 | 
			
		||||
        std::string webserver_url = "http://" + ip.str() + ":" + to_string(USE_WEBSERVER_PORT);
 | 
			
		||||
        urls.push_back(webserver_url);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
    std::vector<uint8_t> data = improv::build_rpc_response(improv::WIFI_SETTINGS, urls);
 | 
			
		||||
    this->send_response_(data);
 | 
			
		||||
  } else if (this->is_active() && this->state_ != improv::STATE_PROVISIONED) {
 | 
			
		||||
    ESP_LOGD(TAG, "WiFi provisioned externally");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->set_state_(improv::STATE_PROVISIONED);
 | 
			
		||||
  this->stop();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32ImprovComponent::advertise_service_data_() {
 | 
			
		||||
  uint8_t service_data[IMPROV_SERVICE_DATA_SIZE] = {};
 | 
			
		||||
  service_data[0] = IMPROV_PROTOCOL_ID_1;  // PR
 | 
			
		||||
 
 | 
			
		||||
@@ -111,6 +111,7 @@ class ESP32ImprovComponent : public Component {
 | 
			
		||||
  void send_response_(std::vector<uint8_t> &response);
 | 
			
		||||
  void process_incoming_data_();
 | 
			
		||||
  void on_wifi_connect_timeout_();
 | 
			
		||||
  void check_wifi_connection_();
 | 
			
		||||
  bool check_identify_();
 | 
			
		||||
  void advertise_service_data_();
 | 
			
		||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG
 | 
			
		||||
 
 | 
			
		||||
@@ -35,7 +35,7 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size
 | 
			
		||||
    if (symbols_free < RMT_SYMBOLS_PER_BYTE) {
 | 
			
		||||
      return 0;
 | 
			
		||||
    }
 | 
			
		||||
    for (int32_t i = 0; i < RMT_SYMBOLS_PER_BYTE; i++) {
 | 
			
		||||
    for (size_t i = 0; i < RMT_SYMBOLS_PER_BYTE; i++) {
 | 
			
		||||
      if (bytes[index] & (1 << (7 - i))) {
 | 
			
		||||
        symbols[i] = params->bit1;
 | 
			
		||||
      } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ namespace esphome {
 | 
			
		||||
static const char *const TAG = "esphome.ota";
 | 
			
		||||
static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
 | 
			
		||||
static constexpr size_t OTA_BUFFER_SIZE = 1024;                  // buffer size for OTA data transfer
 | 
			
		||||
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 10000;  // milliseconds for initial handshake
 | 
			
		||||
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 20000;  // milliseconds for initial handshake
 | 
			
		||||
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000;       // milliseconds for data transfer
 | 
			
		||||
 | 
			
		||||
#ifdef USE_OTA_PASSWORD
 | 
			
		||||
@@ -614,24 +614,67 @@ bool ESPHomeOTAComponent::handle_auth_send_() {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Generate nonce with appropriate hasher
 | 
			
		||||
    bool success = false;
 | 
			
		||||
    // Generate nonce - hasher must be created and used in same stack frame
 | 
			
		||||
    // CRITICAL ESP32-S3 HARDWARE SHA ACCELERATION REQUIREMENTS:
 | 
			
		||||
    // 1. Hash objects must NEVER be passed to another function (different stack frame)
 | 
			
		||||
    // 2. NO Variable Length Arrays (VLAs) - they corrupt the stack with hardware DMA
 | 
			
		||||
    // 3. All hash operations (init/add/calculate) must happen in the SAME function where object is created
 | 
			
		||||
    // Violating these causes truncated hash output (20 bytes instead of 32) or memory corruption.
 | 
			
		||||
    //
 | 
			
		||||
    // Buffer layout after AUTH_READ completes:
 | 
			
		||||
    //   [0]: auth_type (1 byte)
 | 
			
		||||
    //   [1...hex_size]: nonce (hex_size bytes) - our random nonce sent in AUTH_SEND
 | 
			
		||||
    //   [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce
 | 
			
		||||
    //   [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash
 | 
			
		||||
 | 
			
		||||
    // Declare both hash objects in same stack frame, use pointer to select.
 | 
			
		||||
    // NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3
 | 
			
		||||
    // hardware SHA acceleration - the object must exist in this stack frame for all operations.
 | 
			
		||||
    // Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope.
 | 
			
		||||
#ifdef USE_OTA_SHA256
 | 
			
		||||
    sha256::SHA256 sha_hasher;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_OTA_MD5
 | 
			
		||||
    md5::MD5Digest md5_hasher;
 | 
			
		||||
#endif
 | 
			
		||||
    HashBase *hasher = nullptr;
 | 
			
		||||
 | 
			
		||||
#ifdef USE_OTA_SHA256
 | 
			
		||||
    if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) {
 | 
			
		||||
      sha256::SHA256 sha_hasher;
 | 
			
		||||
      success = this->prepare_auth_nonce_(&sha_hasher);
 | 
			
		||||
      hasher = &sha_hasher;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_OTA_MD5
 | 
			
		||||
    if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) {
 | 
			
		||||
      md5::MD5Digest md5_hasher;
 | 
			
		||||
      success = this->prepare_auth_nonce_(&md5_hasher);
 | 
			
		||||
      hasher = &md5_hasher;
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
    if (!success) {
 | 
			
		||||
    const size_t hex_size = hasher->get_size() * 2;
 | 
			
		||||
    const size_t nonce_len = hasher->get_size() / 4;
 | 
			
		||||
    const size_t auth_buf_size = 1 + 3 * hex_size;
 | 
			
		||||
    this->auth_buf_ = std::make_unique<uint8_t[]>(auth_buf_size);
 | 
			
		||||
    this->auth_buf_pos_ = 0;
 | 
			
		||||
 | 
			
		||||
    char *buf = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
 | 
			
		||||
    if (!random_bytes(reinterpret_cast<uint8_t *>(buf), nonce_len)) {
 | 
			
		||||
      this->log_auth_warning_(LOG_STR("Random failed"));
 | 
			
		||||
      this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN);
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    hasher->init();
 | 
			
		||||
    hasher->add(buf, nonce_len);
 | 
			
		||||
    hasher->calculate();
 | 
			
		||||
    this->auth_buf_[0] = this->auth_type_;
 | 
			
		||||
    hasher->get_hex(buf);
 | 
			
		||||
 | 
			
		||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
 | 
			
		||||
    char log_buf[65];  // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too
 | 
			
		||||
    memcpy(log_buf, buf, hex_size);
 | 
			
		||||
    log_buf[hex_size] = '\0';
 | 
			
		||||
    ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf);
 | 
			
		||||
#endif
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Try to write auth_type + nonce
 | 
			
		||||
@@ -678,89 +721,41 @@ bool ESPHomeOTAComponent::handle_auth_read_() {
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // We have all the data, verify it
 | 
			
		||||
  bool matches = false;
 | 
			
		||||
  const char *nonce = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
 | 
			
		||||
  const char *cnonce = nonce + hex_size;
 | 
			
		||||
  const char *response = cnonce + hex_size;
 | 
			
		||||
 | 
			
		||||
  // CRITICAL ESP32-S3: Hash objects must stay in same stack frame (no passing to other functions).
 | 
			
		||||
  // Declare both hash objects in same stack frame, use pointer to select.
 | 
			
		||||
  // NOTE: Both objects are declared here even though only one is used. This is REQUIRED for ESP32-S3
 | 
			
		||||
  // hardware SHA acceleration - the object must exist in this stack frame for all operations.
 | 
			
		||||
  // Do NOT try to "optimize" by creating the object inside the if block, as it would go out of scope.
 | 
			
		||||
#ifdef USE_OTA_SHA256
 | 
			
		||||
  sha256::SHA256 sha_hasher;
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_OTA_MD5
 | 
			
		||||
  md5::MD5Digest md5_hasher;
 | 
			
		||||
#endif
 | 
			
		||||
  HashBase *hasher = nullptr;
 | 
			
		||||
 | 
			
		||||
#ifdef USE_OTA_SHA256
 | 
			
		||||
  if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_SHA256_AUTH) {
 | 
			
		||||
    sha256::SHA256 sha_hasher;
 | 
			
		||||
    matches = this->verify_hash_auth_(&sha_hasher, hex_size);
 | 
			
		||||
    hasher = &sha_hasher;
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_OTA_MD5
 | 
			
		||||
  if (this->auth_type_ == ota::OTA_RESPONSE_REQUEST_AUTH) {
 | 
			
		||||
    md5::MD5Digest md5_hasher;
 | 
			
		||||
    matches = this->verify_hash_auth_(&md5_hasher, hex_size);
 | 
			
		||||
    hasher = &md5_hasher;
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  if (!matches) {
 | 
			
		||||
    this->log_auth_warning_(LOG_STR("Password mismatch"));
 | 
			
		||||
    this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Authentication successful - clean up auth state
 | 
			
		||||
  this->cleanup_auth_();
 | 
			
		||||
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ESPHomeOTAComponent::prepare_auth_nonce_(HashBase *hasher) {
 | 
			
		||||
  // Calculate required buffer size using the hasher
 | 
			
		||||
  const size_t hex_size = hasher->get_size() * 2;
 | 
			
		||||
  const size_t nonce_len = hasher->get_size() / 4;
 | 
			
		||||
 | 
			
		||||
  // Buffer layout after AUTH_READ completes:
 | 
			
		||||
  //   [0]: auth_type (1 byte)
 | 
			
		||||
  //   [1...hex_size]: nonce (hex_size bytes) - our random nonce sent in AUTH_SEND
 | 
			
		||||
  //   [1+hex_size...1+2*hex_size-1]: cnonce (hex_size bytes) - client's nonce
 | 
			
		||||
  //   [1+2*hex_size...1+3*hex_size-1]: response (hex_size bytes) - client's hash
 | 
			
		||||
  // Total: 1 + 3*hex_size
 | 
			
		||||
  const size_t auth_buf_size = 1 + 3 * hex_size;
 | 
			
		||||
  this->auth_buf_ = std::make_unique<uint8_t[]>(auth_buf_size);
 | 
			
		||||
  this->auth_buf_pos_ = 0;
 | 
			
		||||
 | 
			
		||||
  // Generate nonce
 | 
			
		||||
  char *buf = reinterpret_cast<char *>(this->auth_buf_.get() + 1);
 | 
			
		||||
  if (!random_bytes(reinterpret_cast<uint8_t *>(buf), nonce_len)) {
 | 
			
		||||
    this->log_auth_warning_(LOG_STR("Random failed"));
 | 
			
		||||
    this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_UNKNOWN);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  hasher->init();
 | 
			
		||||
  hasher->add(buf, nonce_len);
 | 
			
		||||
  hasher->calculate();
 | 
			
		||||
 | 
			
		||||
  // Prepare buffer: auth_type (1 byte) + nonce (hex_size bytes)
 | 
			
		||||
  this->auth_buf_[0] = this->auth_type_;
 | 
			
		||||
  hasher->get_hex(buf);
 | 
			
		||||
 | 
			
		||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
 | 
			
		||||
  char log_buf[hex_size + 1];
 | 
			
		||||
  // Log nonce for debugging
 | 
			
		||||
  memcpy(log_buf, buf, hex_size);
 | 
			
		||||
  log_buf[hex_size] = '\0';
 | 
			
		||||
  ESP_LOGV(TAG, "Auth: Nonce is %s", log_buf);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ESPHomeOTAComponent::verify_hash_auth_(HashBase *hasher, size_t hex_size) {
 | 
			
		||||
  // Get pointers to the data in the buffer (see prepare_auth_nonce_ for buffer layout)
 | 
			
		||||
  const char *nonce = reinterpret_cast<char *>(this->auth_buf_.get() + 1);  // Skip auth_type byte
 | 
			
		||||
  const char *cnonce = nonce + hex_size;                                    // CNonce immediately follows nonce
 | 
			
		||||
  const char *response = cnonce + hex_size;                                 // Response immediately follows cnonce
 | 
			
		||||
 | 
			
		||||
  // Calculate expected hash: password + nonce + cnonce
 | 
			
		||||
  hasher->init();
 | 
			
		||||
  hasher->add(this->password_.c_str(), this->password_.length());
 | 
			
		||||
  hasher->add(nonce, hex_size * 2);  // Add both nonce and cnonce (contiguous in buffer)
 | 
			
		||||
  hasher->calculate();
 | 
			
		||||
 | 
			
		||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERBOSE
 | 
			
		||||
  char log_buf[hex_size + 1];
 | 
			
		||||
  char log_buf[65];  // Fixed size for SHA256 hex (64) + null, works for MD5 (32) too
 | 
			
		||||
  // Log CNonce
 | 
			
		||||
  memcpy(log_buf, cnonce, hex_size);
 | 
			
		||||
  log_buf[hex_size] = '\0';
 | 
			
		||||
@@ -778,7 +773,18 @@ bool ESPHomeOTAComponent::verify_hash_auth_(HashBase *hasher, size_t hex_size) {
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Compare response
 | 
			
		||||
  return hasher->equals_hex(response);
 | 
			
		||||
  bool matches = hasher->equals_hex(response);
 | 
			
		||||
 | 
			
		||||
  if (!matches) {
 | 
			
		||||
    this->log_auth_warning_(LOG_STR("Password mismatch"));
 | 
			
		||||
    this->send_error_and_cleanup_(ota::OTA_RESPONSE_ERROR_AUTH_INVALID);
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Authentication successful - clean up auth state
 | 
			
		||||
  this->cleanup_auth_();
 | 
			
		||||
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
size_t ESPHomeOTAComponent::get_auth_hex_size_() const {
 | 
			
		||||
 
 | 
			
		||||
@@ -47,8 +47,6 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
 | 
			
		||||
  bool handle_auth_send_();
 | 
			
		||||
  bool handle_auth_read_();
 | 
			
		||||
  bool select_auth_type_();
 | 
			
		||||
  bool prepare_auth_nonce_(HashBase *hasher);
 | 
			
		||||
  bool verify_hash_auth_(HashBase *hasher, size_t hex_size);
 | 
			
		||||
  size_t get_auth_hex_size_() const;
 | 
			
		||||
  void cleanup_auth_();
 | 
			
		||||
  void log_auth_warning_(const LogString *msg);
 | 
			
		||||
 
 | 
			
		||||
@@ -41,17 +41,20 @@ static const char *const TAG = "ethernet";
 | 
			
		||||
 | 
			
		||||
EthernetComponent *global_eth_component;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 | 
			
		||||
void EthernetComponent::log_error_and_mark_failed_(esp_err_t err, const char *message) {
 | 
			
		||||
  ESP_LOGE(TAG, "%s: (%d) %s", message, err, esp_err_to_name(err));
 | 
			
		||||
  this->mark_failed();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#define ESPHL_ERROR_CHECK(err, message) \
 | 
			
		||||
  if ((err) != ESP_OK) { \
 | 
			
		||||
    ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \
 | 
			
		||||
    this->mark_failed(); \
 | 
			
		||||
    this->log_error_and_mark_failed_(err, message); \
 | 
			
		||||
    return; \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#define ESPHL_ERROR_CHECK_RET(err, message, ret) \
 | 
			
		||||
  if ((err) != ESP_OK) { \
 | 
			
		||||
    ESP_LOGE(TAG, message ": (%d) %s", err, esp_err_to_name(err)); \
 | 
			
		||||
    this->mark_failed(); \
 | 
			
		||||
    this->log_error_and_mark_failed_(err, message); \
 | 
			
		||||
    return ret; \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -106,6 +106,7 @@ class EthernetComponent : public Component {
 | 
			
		||||
  void start_connect_();
 | 
			
		||||
  void finish_connect_();
 | 
			
		||||
  void dump_connect_params_();
 | 
			
		||||
  void log_error_and_mark_failed_(esp_err_t err, const char *message);
 | 
			
		||||
#ifdef USE_ETHERNET_KSZ8081
 | 
			
		||||
  /// @brief Set `RMII Reference Clock Select` bit for KSZ8081.
 | 
			
		||||
  void ksz8081_set_clock_reference_(esp_eth_mac_t *mac);
 | 
			
		||||
@@ -162,7 +163,7 @@ class EthernetComponent : public Component {
 | 
			
		||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
extern EthernetComponent *global_eth_component;
 | 
			
		||||
 | 
			
		||||
#if defined(USE_ARDUINO) || ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2)
 | 
			
		||||
#if ESP_IDF_VERSION < ESP_IDF_VERSION_VAL(5, 4, 2)
 | 
			
		||||
extern "C" esp_eth_phy_t *esp_eth_phy_new_jl1101(const eth_phy_config_t *config);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +0,0 @@
 | 
			
		||||
CODEOWNERS = ["@Rapsssito"]
 | 
			
		||||
 | 
			
		||||
# Allows event_emitter to be configured in yaml, to allow use of the C++ api.
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = {}
 | 
			
		||||
@@ -1,117 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include <vector>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <limits>
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace event_emitter {
 | 
			
		||||
 | 
			
		||||
using EventEmitterListenerID = uint32_t;
 | 
			
		||||
static constexpr EventEmitterListenerID INVALID_LISTENER_ID = 0;
 | 
			
		||||
 | 
			
		||||
// EventEmitter class that can emit events with a specific name (it is highly recommended to use an enum class for this)
 | 
			
		||||
// and a list of arguments. Supports multiple listeners for each event.
 | 
			
		||||
template<typename EvtType, typename... Args> class EventEmitter {
 | 
			
		||||
 public:
 | 
			
		||||
  EventEmitterListenerID on(EvtType event, std::function<void(Args...)> listener) {
 | 
			
		||||
    EventEmitterListenerID listener_id = this->get_next_id_();
 | 
			
		||||
 | 
			
		||||
    // Find or create event entry
 | 
			
		||||
    EventEntry *entry = this->find_or_create_event_(event);
 | 
			
		||||
    entry->listeners.push_back({listener_id, listener});
 | 
			
		||||
 | 
			
		||||
    return listener_id;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void off(EvtType event, EventEmitterListenerID id) {
 | 
			
		||||
    EventEntry *entry = this->find_event_(event);
 | 
			
		||||
    if (entry == nullptr)
 | 
			
		||||
      return;
 | 
			
		||||
 | 
			
		||||
    // Remove listener with given id
 | 
			
		||||
    for (auto it = entry->listeners.begin(); it != entry->listeners.end(); ++it) {
 | 
			
		||||
      if (it->id == id) {
 | 
			
		||||
        // Swap with last and pop for efficient removal
 | 
			
		||||
        *it = entry->listeners.back();
 | 
			
		||||
        entry->listeners.pop_back();
 | 
			
		||||
 | 
			
		||||
        // Remove event entry if no more listeners
 | 
			
		||||
        if (entry->listeners.empty()) {
 | 
			
		||||
          this->remove_event_(event);
 | 
			
		||||
        }
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void emit_(EvtType event, Args... args) {
 | 
			
		||||
    EventEntry *entry = this->find_event_(event);
 | 
			
		||||
    if (entry == nullptr)
 | 
			
		||||
      return;
 | 
			
		||||
 | 
			
		||||
    // Call all listeners for this event
 | 
			
		||||
    for (const auto &listener : entry->listeners) {
 | 
			
		||||
      listener.callback(args...);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  struct Listener {
 | 
			
		||||
    EventEmitterListenerID id;
 | 
			
		||||
    std::function<void(Args...)> callback;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  struct EventEntry {
 | 
			
		||||
    EvtType event;
 | 
			
		||||
    std::vector<Listener> listeners;
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  EventEmitterListenerID get_next_id_() {
 | 
			
		||||
    // Simple incrementing ID, wrapping around at max
 | 
			
		||||
    EventEmitterListenerID next_id = (this->current_id_ + 1);
 | 
			
		||||
    if (next_id == INVALID_LISTENER_ID) {
 | 
			
		||||
      next_id = 1;
 | 
			
		||||
    }
 | 
			
		||||
    this->current_id_ = next_id;
 | 
			
		||||
    return this->current_id_;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  EventEntry *find_event_(EvtType event) {
 | 
			
		||||
    for (auto &entry : this->events_) {
 | 
			
		||||
      if (entry.event == event) {
 | 
			
		||||
        return &entry;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return nullptr;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  EventEntry *find_or_create_event_(EvtType event) {
 | 
			
		||||
    EventEntry *entry = this->find_event_(event);
 | 
			
		||||
    if (entry != nullptr)
 | 
			
		||||
      return entry;
 | 
			
		||||
 | 
			
		||||
    // Create new event entry
 | 
			
		||||
    this->events_.push_back({event, {}});
 | 
			
		||||
    return &this->events_.back();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void remove_event_(EvtType event) {
 | 
			
		||||
    for (auto it = this->events_.begin(); it != this->events_.end(); ++it) {
 | 
			
		||||
      if (it->event == event) {
 | 
			
		||||
        // Swap with last and pop
 | 
			
		||||
        *it = this->events_.back();
 | 
			
		||||
        this->events_.pop_back();
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  std::vector<EventEntry> events_;
 | 
			
		||||
  EventEmitterListenerID current_id_ = 0;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace event_emitter
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -80,7 +80,7 @@ void FingerprintGrowComponent::setup() {
 | 
			
		||||
  delay(20);  // This delay guarantees the sensor will in fact be powered power.
 | 
			
		||||
 | 
			
		||||
  if (this->check_password_()) {
 | 
			
		||||
    if (this->new_password_ != -1) {
 | 
			
		||||
    if (this->new_password_ != std::numeric_limits<uint32_t>::max()) {
 | 
			
		||||
      if (this->set_password_())
 | 
			
		||||
        return;
 | 
			
		||||
    } else {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@
 | 
			
		||||
#include "esphome/components/binary_sensor/binary_sensor.h"
 | 
			
		||||
#include "esphome/components/uart/uart.h"
 | 
			
		||||
 | 
			
		||||
#include <limits>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -177,7 +178,7 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic
 | 
			
		||||
  uint8_t address_[4] = {0xFF, 0xFF, 0xFF, 0xFF};
 | 
			
		||||
  uint16_t capacity_ = 64;
 | 
			
		||||
  uint32_t password_ = 0x0;
 | 
			
		||||
  uint32_t new_password_ = -1;
 | 
			
		||||
  uint32_t new_password_ = std::numeric_limits<uint32_t>::max();
 | 
			
		||||
  GPIOPin *sensing_pin_{nullptr};
 | 
			
		||||
  GPIOPin *sensor_power_pin_{nullptr};
 | 
			
		||||
  uint8_t enrollment_image_ = 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -179,7 +179,7 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo
 | 
			
		||||
        if (b) {
 | 
			
		||||
          int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset;
 | 
			
		||||
          auto draw_pixel_at = [&buff, c, y_offset, this](int16_t x, int16_t y) {
 | 
			
		||||
            if (y >= y_offset && y < y_offset + this->height_)
 | 
			
		||||
            if (y >= y_offset && static_cast<uint32_t>(y) < y_offset + this->height_)
 | 
			
		||||
              buff->draw_pixel_at(x, y, c);
 | 
			
		||||
          };
 | 
			
		||||
          if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) {
 | 
			
		||||
 
 | 
			
		||||
@@ -116,7 +116,7 @@ void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const
 | 
			
		||||
  int number_items_fit_to_screen = 0;
 | 
			
		||||
  const int max_item_index = this->displayed_item_->items_size() - 1;
 | 
			
		||||
 | 
			
		||||
  for (size_t i = 0; i <= max_item_index; i++) {
 | 
			
		||||
  for (size_t i = 0; max_item_index >= 0 && i <= static_cast<size_t>(max_item_index); i++) {
 | 
			
		||||
    const auto *item = this->displayed_item_->get_item(i);
 | 
			
		||||
    const bool selected = i == this->cursor_index_;
 | 
			
		||||
    const display::Rect item_dimensions = this->measure_item(display, item, bounds, selected);
 | 
			
		||||
@@ -174,7 +174,8 @@ void GraphicalDisplayMenu::draw_menu_internal_(display::Display *display, const
 | 
			
		||||
 | 
			
		||||
  display->filled_rectangle(bounds->x, bounds->y, max_width, total_height, this->background_color_);
 | 
			
		||||
  auto y_offset = bounds->y;
 | 
			
		||||
  for (size_t i = first_item_index; i <= last_item_index; i++) {
 | 
			
		||||
  for (size_t i = static_cast<size_t>(first_item_index);
 | 
			
		||||
       last_item_index >= 0 && i <= static_cast<size_t>(last_item_index); i++) {
 | 
			
		||||
    const auto *item = this->displayed_item_->get_item(i);
 | 
			
		||||
    const bool selected = i == this->cursor_index_;
 | 
			
		||||
    display::Rect dimensions = menu_dimensions[i];
 | 
			
		||||
 
 | 
			
		||||
@@ -213,7 +213,7 @@ haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameTy
 | 
			
		||||
               this->real_control_packet_size_);
 | 
			
		||||
        this->status_message_callback_.call((const char *) data, data_size);
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGW(TAG, "Status packet too small: %d (should be >= %d)", data_size, this->real_control_packet_size_);
 | 
			
		||||
        ESP_LOGW(TAG, "Status packet too small: %zu (should be >= %zu)", data_size, this->real_control_packet_size_);
 | 
			
		||||
      }
 | 
			
		||||
      switch (this->protocol_phase_) {
 | 
			
		||||
        case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
 | 
			
		||||
@@ -827,7 +827,7 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
 | 
			
		||||
  size_t expected_size =
 | 
			
		||||
      2 + this->status_message_header_size_ + this->real_control_packet_size_ + this->real_sensors_packet_size_;
 | 
			
		||||
  if (size < expected_size) {
 | 
			
		||||
    ESP_LOGW(TAG, "Unexpected message size %d (expexted >= %d)", size, expected_size);
 | 
			
		||||
    ESP_LOGW(TAG, "Unexpected message size %u (expexted >= %zu)", size, expected_size);
 | 
			
		||||
    return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
 | 
			
		||||
  }
 | 
			
		||||
  uint16_t subtype = (((uint16_t) packet_buffer[0]) << 8) + packet_buffer[1];
 | 
			
		||||
 
 | 
			
		||||
@@ -178,7 +178,7 @@ class HonClimate : public HaierClimateBase {
 | 
			
		||||
  int extra_control_packet_bytes_{0};
 | 
			
		||||
  int extra_sensors_packet_bytes_{4};
 | 
			
		||||
  int status_message_header_size_{0};
 | 
			
		||||
  int real_control_packet_size_{sizeof(hon_protocol::HaierPacketControl)};
 | 
			
		||||
  size_t real_control_packet_size_{sizeof(hon_protocol::HaierPacketControl)};
 | 
			
		||||
  int real_sensors_packet_size_{sizeof(hon_protocol::HaierPacketSensors) + 4};
 | 
			
		||||
  HonControlMethod control_method_;
 | 
			
		||||
  std::queue<haier_protocol::HaierMessage> control_messages_queue_;
 | 
			
		||||
 
 | 
			
		||||
@@ -7,24 +7,20 @@ namespace hdc1080 {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "hdc1080";
 | 
			
		||||
 | 
			
		||||
static const uint8_t HDC1080_ADDRESS = 0x40;  // 0b1000000 from datasheet
 | 
			
		||||
static const uint8_t HDC1080_CMD_CONFIGURATION = 0x02;
 | 
			
		||||
static const uint8_t HDC1080_CMD_TEMPERATURE = 0x00;
 | 
			
		||||
static const uint8_t HDC1080_CMD_HUMIDITY = 0x01;
 | 
			
		||||
 | 
			
		||||
void HDC1080Component::setup() {
 | 
			
		||||
  const uint8_t data[2] = {
 | 
			
		||||
      0b00000000,  // resolution 14bit for both humidity and temperature
 | 
			
		||||
      0b00000000   // reserved
 | 
			
		||||
  };
 | 
			
		||||
  const uint8_t config[2] = {0x00, 0x00};  // resolution 14bit for both humidity and temperature
 | 
			
		||||
 | 
			
		||||
  if (!this->write_bytes(HDC1080_CMD_CONFIGURATION, data, 2)) {
 | 
			
		||||
    // as instruction is same as powerup defaults (for now), interpret as warning if this fails
 | 
			
		||||
    ESP_LOGW(TAG, "HDC1080 initial config instruction error");
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
  // if configuration fails - there is a problem
 | 
			
		||||
  if (this->write_register(HDC1080_CMD_CONFIGURATION, config, 2) != i2c::ERROR_OK) {
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HDC1080Component::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "HDC1080:");
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
@@ -35,39 +31,51 @@ void HDC1080Component::dump_config() {
 | 
			
		||||
  LOG_SENSOR("  ", "Temperature", this->temperature_);
 | 
			
		||||
  LOG_SENSOR("  ", "Humidity", this->humidity_);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HDC1080Component::update() {
 | 
			
		||||
  uint16_t raw_temp;
 | 
			
		||||
  // regardless of what sensor/s are defined in yaml configuration
 | 
			
		||||
  // the hdc1080 setup configuration used, requires both temperature and humidity to be read
 | 
			
		||||
 | 
			
		||||
  this->status_clear_warning();
 | 
			
		||||
 | 
			
		||||
  if (this->write(&HDC1080_CMD_TEMPERATURE, 1) != i2c::ERROR_OK) {
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  delay(20);
 | 
			
		||||
  if (this->read(reinterpret_cast<uint8_t *>(&raw_temp), 2) != i2c::ERROR_OK) {
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  raw_temp = i2c::i2ctohs(raw_temp);
 | 
			
		||||
  float temp = raw_temp * 0.0025177f - 40.0f;  // raw * 2^-16 * 165 - 40
 | 
			
		||||
  this->temperature_->publish_state(temp);
 | 
			
		||||
 | 
			
		||||
  uint16_t raw_humidity;
 | 
			
		||||
  if (this->write(&HDC1080_CMD_HUMIDITY, 1) != i2c::ERROR_OK) {
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  delay(20);
 | 
			
		||||
  if (this->read(reinterpret_cast<uint8_t *>(&raw_humidity), 2) != i2c::ERROR_OK) {
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  raw_humidity = i2c::i2ctohs(raw_humidity);
 | 
			
		||||
  float humidity = raw_humidity * 0.001525879f;  // raw * 2^-16 * 100
 | 
			
		||||
  this->humidity_->publish_state(humidity);
 | 
			
		||||
  this->set_timeout(20, [this]() {
 | 
			
		||||
    uint16_t raw_temperature;
 | 
			
		||||
    if (this->read(reinterpret_cast<uint8_t *>(&raw_temperature), 2) != i2c::ERROR_OK) {
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "Got temperature=%.1f°C humidity=%.1f%%", temp, humidity);
 | 
			
		||||
  this->status_clear_warning();
 | 
			
		||||
    if (this->temperature_ != nullptr) {
 | 
			
		||||
      raw_temperature = i2c::i2ctohs(raw_temperature);
 | 
			
		||||
      float temperature = raw_temperature * 0.0025177f - 40.0f;  // raw * 2^-16 * 165 - 40
 | 
			
		||||
      this->temperature_->publish_state(temperature);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this->write(&HDC1080_CMD_HUMIDITY, 1) != i2c::ERROR_OK) {
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    this->set_timeout(20, [this]() {
 | 
			
		||||
      uint16_t raw_humidity;
 | 
			
		||||
      if (this->read(reinterpret_cast<uint8_t *>(&raw_humidity), 2) != i2c::ERROR_OK) {
 | 
			
		||||
        this->status_set_warning();
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      if (this->humidity_ != nullptr) {
 | 
			
		||||
        raw_humidity = i2c::i2ctohs(raw_humidity);
 | 
			
		||||
        float humidity = raw_humidity * 0.001525879f;  // raw * 2^-16 * 100
 | 
			
		||||
        this->humidity_->publish_state(humidity);
 | 
			
		||||
      }
 | 
			
		||||
    });
 | 
			
		||||
  });
 | 
			
		||||
}
 | 
			
		||||
float HDC1080Component::get_setup_priority() const { return setup_priority::DATA; }
 | 
			
		||||
 | 
			
		||||
}  // namespace hdc1080
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -12,13 +12,11 @@ class HDC1080Component : public PollingComponent, public i2c::I2CDevice {
 | 
			
		||||
  void set_temperature(sensor::Sensor *temperature) { temperature_ = temperature; }
 | 
			
		||||
  void set_humidity(sensor::Sensor *humidity) { humidity_ = humidity; }
 | 
			
		||||
 | 
			
		||||
  /// Setup the sensor and check for connection.
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  /// Retrieve the latest sensor values. This operation takes approximately 16ms.
 | 
			
		||||
  void update() override;
 | 
			
		||||
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::DATA; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  sensor::Sensor *temperature_{nullptr};
 | 
			
		||||
 
 | 
			
		||||
@@ -5,6 +5,7 @@ from esphome.components.const import CONF_REQUEST_HEADERS
 | 
			
		||||
from esphome.config_helpers import filter_source_files_from_platform
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_CAPTURE_RESPONSE,
 | 
			
		||||
    CONF_ESP8266_DISABLE_SSL_SUPPORT,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_METHOD,
 | 
			
		||||
@@ -57,7 +58,6 @@ CONF_HEADERS = "headers"
 | 
			
		||||
CONF_COLLECT_HEADERS = "collect_headers"
 | 
			
		||||
CONF_BODY = "body"
 | 
			
		||||
CONF_JSON = "json"
 | 
			
		||||
CONF_CAPTURE_RESPONSE = "capture_response"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_url(value):
 | 
			
		||||
 
 | 
			
		||||
@@ -377,7 +377,7 @@ void I2SAudioSpeaker::speaker_task(void *params) {
 | 
			
		||||
            this_speaker->current_stream_info_.get_bits_per_sample() <= 16) {
 | 
			
		||||
          size_t len = bytes_read / sizeof(int16_t);
 | 
			
		||||
          int16_t *tmp_buf = (int16_t *) new_data;
 | 
			
		||||
          for (int i = 0; i < len; i += 2) {
 | 
			
		||||
          for (size_t i = 0; i < len; i += 2) {
 | 
			
		||||
            int16_t tmp = tmp_buf[i];
 | 
			
		||||
            tmp_buf[i] = tmp_buf[i + 1];
 | 
			
		||||
            tmp_buf[i + 1] = tmp;
 | 
			
		||||
 
 | 
			
		||||
@@ -325,7 +325,7 @@ void ILI9XXXDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, cons
 | 
			
		||||
      // we could deal here with a non-zero y_offset, but if x_offset is zero, y_offset probably will be so don't bother
 | 
			
		||||
      this->write_array(ptr, w * h * 2);
 | 
			
		||||
    } else {
 | 
			
		||||
      for (size_t y = 0; y != h; y++) {
 | 
			
		||||
      for (size_t y = 0; y != static_cast<size_t>(h); y++) {
 | 
			
		||||
        this->write_array(ptr + (y + y_offset) * stride + x_offset, w * 2);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
@@ -349,7 +349,7 @@ void ILI9XXXDisplay::draw_pixels_at(int x_start, int y_start, int w, int h, cons
 | 
			
		||||
        App.feed_wdt();
 | 
			
		||||
      }
 | 
			
		||||
      // end of line? Skip to the next.
 | 
			
		||||
      if (++pixel == w) {
 | 
			
		||||
      if (++pixel == static_cast<size_t>(w)) {
 | 
			
		||||
        pixel = 0;
 | 
			
		||||
        ptr += (x_pad + x_offset) * 2;
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,13 @@ namespace json {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "json";
 | 
			
		||||
 | 
			
		||||
#ifdef USE_PSRAM
 | 
			
		||||
// Global allocator that outlives all JsonDocuments returned by parse_json()
 | 
			
		||||
// This prevents dangling pointer issues when JsonDocuments are returned from functions
 | 
			
		||||
// NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) - Must be mutable for ArduinoJson::Allocator
 | 
			
		||||
static SpiRamAllocator global_json_allocator;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
std::string build_json(const json_build_t &f) {
 | 
			
		||||
  // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
 | 
			
		||||
  JsonBuilder builder;
 | 
			
		||||
@@ -19,18 +26,21 @@ std::string build_json(const json_build_t &f) {
 | 
			
		||||
 | 
			
		||||
bool parse_json(const std::string &data, const json_parse_t &f) {
 | 
			
		||||
  // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
 | 
			
		||||
  JsonDocument doc = parse_json(data);
 | 
			
		||||
  JsonDocument doc = parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size());
 | 
			
		||||
  if (doc.overflowed() || doc.isNull())
 | 
			
		||||
    return false;
 | 
			
		||||
  return f(doc.as<JsonObject>());
 | 
			
		||||
  // NOLINTEND(clang-analyzer-cplusplus.NewDeleteLeaks)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
JsonDocument parse_json(const std::string &data) {
 | 
			
		||||
JsonDocument parse_json(const uint8_t *data, size_t len) {
 | 
			
		||||
  // NOLINTBEGIN(clang-analyzer-cplusplus.NewDeleteLeaks) false positive with ArduinoJson
 | 
			
		||||
  if (data == nullptr || len == 0) {
 | 
			
		||||
    ESP_LOGE(TAG, "No data to parse");
 | 
			
		||||
    return JsonObject();  // return unbound object
 | 
			
		||||
  }
 | 
			
		||||
#ifdef USE_PSRAM
 | 
			
		||||
  auto doc_allocator = SpiRamAllocator();
 | 
			
		||||
  JsonDocument json_document(&doc_allocator);
 | 
			
		||||
  JsonDocument json_document(&global_json_allocator);
 | 
			
		||||
#else
 | 
			
		||||
  JsonDocument json_document;
 | 
			
		||||
#endif
 | 
			
		||||
@@ -38,7 +48,7 @@ JsonDocument parse_json(const std::string &data) {
 | 
			
		||||
    ESP_LOGE(TAG, "Could not allocate memory for JSON document!");
 | 
			
		||||
    return JsonObject();  // return unbound object
 | 
			
		||||
  }
 | 
			
		||||
  DeserializationError err = deserializeJson(json_document, data);
 | 
			
		||||
  DeserializationError err = deserializeJson(json_document, data, len);
 | 
			
		||||
 | 
			
		||||
  if (err == DeserializationError::Ok) {
 | 
			
		||||
    return json_document;
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
#define ARDUINOJSON_ENABLE_STD_STRING 1  // NOLINT
 | 
			
		||||
@@ -49,8 +50,13 @@ std::string build_json(const json_build_t &f);
 | 
			
		||||
 | 
			
		||||
/// Parse a JSON string and run the provided json parse function if it's valid.
 | 
			
		||||
bool parse_json(const std::string &data, const json_parse_t &f);
 | 
			
		||||
 | 
			
		||||
/// Parse a JSON string and return the root JsonDocument (or an unbound object on error)
 | 
			
		||||
JsonDocument parse_json(const std::string &data);
 | 
			
		||||
JsonDocument parse_json(const uint8_t *data, size_t len);
 | 
			
		||||
/// Parse a JSON string and return the root JsonDocument (or an unbound object on error)
 | 
			
		||||
inline JsonDocument parse_json(const std::string &data) {
 | 
			
		||||
  return parse_json(reinterpret_cast<const uint8_t *>(data.c_str()), data.size());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/// Builder class for creating JSON documents without lambdas
 | 
			
		||||
class JsonBuilder {
 | 
			
		||||
 
 | 
			
		||||
@@ -22,7 +22,7 @@ void KamstrupKMPComponent::dump_config() {
 | 
			
		||||
  LOG_SENSOR("  ", "Flow", this->flow_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Volume", this->volume_sensor_);
 | 
			
		||||
 | 
			
		||||
  for (int i = 0; i < this->custom_sensors_.size(); i++) {
 | 
			
		||||
  for (size_t i = 0; i < this->custom_sensors_.size(); i++) {
 | 
			
		||||
    LOG_SENSOR("  ", "Custom Sensor", this->custom_sensors_[i]);
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "    Command: 0x%04X", this->custom_commands_[i]);
 | 
			
		||||
  }
 | 
			
		||||
@@ -268,7 +268,7 @@ void KamstrupKMPComponent::set_sensor_value_(uint16_t command, float value, uint
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Custom sensors
 | 
			
		||||
  for (int i = 0; i < this->custom_commands_.size(); i++) {
 | 
			
		||||
  for (size_t i = 0; i < this->custom_commands_.size(); i++) {
 | 
			
		||||
    if (command == this->custom_commands_[i]) {
 | 
			
		||||
      this->custom_sensors_[i]->publish_state(value);
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -13,8 +13,8 @@ class KeyCollector : public Component {
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void set_provider(key_provider::KeyProvider *provider);
 | 
			
		||||
  void set_min_length(int min_length) { this->min_length_ = min_length; };
 | 
			
		||||
  void set_max_length(int max_length) { this->max_length_ = max_length; };
 | 
			
		||||
  void set_min_length(uint32_t min_length) { this->min_length_ = min_length; };
 | 
			
		||||
  void set_max_length(uint32_t max_length) { this->max_length_ = max_length; };
 | 
			
		||||
  void set_start_keys(std::string start_keys) { this->start_keys_ = std::move(start_keys); };
 | 
			
		||||
  void set_end_keys(std::string end_keys) { this->end_keys_ = std::move(end_keys); };
 | 
			
		||||
  void set_end_key_required(bool end_key_required) { this->end_key_required_ = end_key_required; };
 | 
			
		||||
@@ -33,8 +33,8 @@ class KeyCollector : public Component {
 | 
			
		||||
 protected:
 | 
			
		||||
  void key_pressed_(uint8_t key);
 | 
			
		||||
 | 
			
		||||
  int min_length_{0};
 | 
			
		||||
  int max_length_{0};
 | 
			
		||||
  uint32_t min_length_{0};
 | 
			
		||||
  uint32_t max_length_{0};
 | 
			
		||||
  std::string start_keys_;
 | 
			
		||||
  std::string end_keys_;
 | 
			
		||||
  bool end_key_required_{false};
 | 
			
		||||
 
 | 
			
		||||
@@ -10,11 +10,15 @@ namespace light {
 | 
			
		||||
static const char *const TAG = "light";
 | 
			
		||||
 | 
			
		||||
// Helper functions to reduce code size for logging
 | 
			
		||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
 | 
			
		||||
static void log_validation_warning(const char *name, const LogString *param_name, float val, float min, float max) {
 | 
			
		||||
  ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), val, min, max);
 | 
			
		||||
static void clamp_and_log_if_invalid(const char *name, float &value, const LogString *param_name, float min = 0.0f,
 | 
			
		||||
                                     float max = 1.0f) {
 | 
			
		||||
  if (value < min || value > max) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s': %s value %.2f is out of range [%.1f - %.1f]", name, LOG_STR_ARG(param_name), value, min, max);
 | 
			
		||||
    value = clamp(value, min, max);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_WARN
 | 
			
		||||
static void log_feature_not_supported(const char *name, const LogString *feature) {
 | 
			
		||||
  ESP_LOGW(TAG, "'%s': %s not supported", name, LOG_STR_ARG(feature));
 | 
			
		||||
}
 | 
			
		||||
@@ -27,7 +31,6 @@ static void log_invalid_parameter(const char *name, const LogString *message) {
 | 
			
		||||
  ESP_LOGW(TAG, "'%s': %s", name, LOG_STR_ARG(message));
 | 
			
		||||
}
 | 
			
		||||
#else
 | 
			
		||||
#define log_validation_warning(name, param_name, val, min, max)
 | 
			
		||||
#define log_feature_not_supported(name, feature)
 | 
			
		||||
#define log_color_mode_not_supported(name, feature)
 | 
			
		||||
#define log_invalid_parameter(name, message)
 | 
			
		||||
@@ -44,7 +47,7 @@ static void log_invalid_parameter(const char *name, const LogString *message) {
 | 
			
		||||
  } \
 | 
			
		||||
  LightCall &LightCall::set_##name(type name) { \
 | 
			
		||||
    this->name##_ = name; \
 | 
			
		||||
    this->set_flag_(flag, true); \
 | 
			
		||||
    this->set_flag_(flag); \
 | 
			
		||||
    return *this; \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -181,6 +184,16 @@ void LightCall::perform() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LightCall::log_and_clear_unsupported_(FieldFlags flag, const LogString *feature, bool use_color_mode_log) {
 | 
			
		||||
  auto *name = this->parent_->get_name().c_str();
 | 
			
		||||
  if (use_color_mode_log) {
 | 
			
		||||
    log_color_mode_not_supported(name, feature);
 | 
			
		||||
  } else {
 | 
			
		||||
    log_feature_not_supported(name, feature);
 | 
			
		||||
  }
 | 
			
		||||
  this->clear_flag_(flag);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
LightColorValues LightCall::validate_() {
 | 
			
		||||
  auto *name = this->parent_->get_name().c_str();
 | 
			
		||||
  auto traits = this->parent_->get_traits();
 | 
			
		||||
@@ -188,141 +201,108 @@ LightColorValues LightCall::validate_() {
 | 
			
		||||
  // Color mode check
 | 
			
		||||
  if (this->has_color_mode() && !traits.supports_color_mode(this->color_mode_)) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s' does not support color mode %s", name, LOG_STR_ARG(color_mode_to_human(this->color_mode_)));
 | 
			
		||||
    this->set_flag_(FLAG_HAS_COLOR_MODE, false);
 | 
			
		||||
    this->clear_flag_(FLAG_HAS_COLOR_MODE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Ensure there is always a color mode set
 | 
			
		||||
  if (!this->has_color_mode()) {
 | 
			
		||||
    this->color_mode_ = this->compute_color_mode_();
 | 
			
		||||
    this->set_flag_(FLAG_HAS_COLOR_MODE, true);
 | 
			
		||||
    this->set_flag_(FLAG_HAS_COLOR_MODE);
 | 
			
		||||
  }
 | 
			
		||||
  auto color_mode = this->color_mode_;
 | 
			
		||||
 | 
			
		||||
  // Transform calls that use non-native parameters for the current mode.
 | 
			
		||||
  this->transform_parameters_();
 | 
			
		||||
 | 
			
		||||
  // Brightness exists check
 | 
			
		||||
  if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS)) {
 | 
			
		||||
    log_feature_not_supported(name, LOG_STR("brightness"));
 | 
			
		||||
    this->set_flag_(FLAG_HAS_BRIGHTNESS, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Transition length possible check
 | 
			
		||||
  if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS)) {
 | 
			
		||||
    log_feature_not_supported(name, LOG_STR("transitions"));
 | 
			
		||||
    this->set_flag_(FLAG_HAS_TRANSITION, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Color brightness exists check
 | 
			
		||||
  if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB)) {
 | 
			
		||||
    log_color_mode_not_supported(name, LOG_STR("RGB brightness"));
 | 
			
		||||
    this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // RGB exists check
 | 
			
		||||
  if ((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) ||
 | 
			
		||||
      (this->has_blue() && this->blue_ > 0.0f)) {
 | 
			
		||||
    if (!(color_mode & ColorCapability::RGB)) {
 | 
			
		||||
      log_color_mode_not_supported(name, LOG_STR("RGB color"));
 | 
			
		||||
      this->set_flag_(FLAG_HAS_RED, false);
 | 
			
		||||
      this->set_flag_(FLAG_HAS_GREEN, false);
 | 
			
		||||
      this->set_flag_(FLAG_HAS_BLUE, false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // White value exists check
 | 
			
		||||
  if (this->has_white() && this->white_ > 0.0f &&
 | 
			
		||||
      !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE)) {
 | 
			
		||||
    log_color_mode_not_supported(name, LOG_STR("white value"));
 | 
			
		||||
    this->set_flag_(FLAG_HAS_WHITE, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Color temperature exists check
 | 
			
		||||
  if (this->has_color_temperature() &&
 | 
			
		||||
      !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE)) {
 | 
			
		||||
    log_color_mode_not_supported(name, LOG_STR("color temperature"));
 | 
			
		||||
    this->set_flag_(FLAG_HAS_COLOR_TEMPERATURE, false);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Cold/warm white value exists check
 | 
			
		||||
  if ((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) {
 | 
			
		||||
    if (!(color_mode & ColorCapability::COLD_WARM_WHITE)) {
 | 
			
		||||
      log_color_mode_not_supported(name, LOG_STR("cold/warm white value"));
 | 
			
		||||
      this->set_flag_(FLAG_HAS_COLD_WHITE, false);
 | 
			
		||||
      this->set_flag_(FLAG_HAS_WARM_WHITE, false);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#define VALIDATE_RANGE_(name_, upper_name, min, max) \
 | 
			
		||||
  if (this->has_##name_()) { \
 | 
			
		||||
    auto val = this->name_##_; \
 | 
			
		||||
    if (val < (min) || val > (max)) { \
 | 
			
		||||
      log_validation_warning(name, LOG_STR(upper_name), val, (min), (max)); \
 | 
			
		||||
      this->name_##_ = clamp(val, (min), (max)); \
 | 
			
		||||
    } \
 | 
			
		||||
  }
 | 
			
		||||
#define VALIDATE_RANGE(name, upper_name) VALIDATE_RANGE_(name, upper_name, 0.0f, 1.0f)
 | 
			
		||||
 | 
			
		||||
  // Range checks
 | 
			
		||||
  VALIDATE_RANGE(brightness, "Brightness")
 | 
			
		||||
  VALIDATE_RANGE(color_brightness, "Color brightness")
 | 
			
		||||
  VALIDATE_RANGE(red, "Red")
 | 
			
		||||
  VALIDATE_RANGE(green, "Green")
 | 
			
		||||
  VALIDATE_RANGE(blue, "Blue")
 | 
			
		||||
  VALIDATE_RANGE(white, "White")
 | 
			
		||||
  VALIDATE_RANGE(cold_white, "Cold white")
 | 
			
		||||
  VALIDATE_RANGE(warm_white, "Warm white")
 | 
			
		||||
  VALIDATE_RANGE_(color_temperature, "Color temperature", traits.get_min_mireds(), traits.get_max_mireds())
 | 
			
		||||
 | 
			
		||||
  // Business logic adjustments before validation
 | 
			
		||||
  // Flag whether an explicit turn off was requested, in which case we'll also stop the effect.
 | 
			
		||||
  bool explicit_turn_off_request = this->has_state() && !this->state_;
 | 
			
		||||
 | 
			
		||||
  // Turn off when brightness is set to zero, and reset brightness (so that it has nonzero brightness when turned on).
 | 
			
		||||
  if (this->has_brightness() && this->brightness_ == 0.0f) {
 | 
			
		||||
    this->state_ = false;
 | 
			
		||||
    this->set_flag_(FLAG_HAS_STATE, true);
 | 
			
		||||
    this->set_flag_(FLAG_HAS_STATE);
 | 
			
		||||
    this->brightness_ = 1.0f;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Set color brightness to 100% if currently zero and a color is set.
 | 
			
		||||
  if (this->has_red() || this->has_green() || this->has_blue()) {
 | 
			
		||||
    if (!this->has_color_brightness() && this->parent_->remote_values.get_color_brightness() == 0.0f) {
 | 
			
		||||
      this->color_brightness_ = 1.0f;
 | 
			
		||||
      this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS, true);
 | 
			
		||||
    }
 | 
			
		||||
  if ((this->has_red() || this->has_green() || this->has_blue()) && !this->has_color_brightness() &&
 | 
			
		||||
      this->parent_->remote_values.get_color_brightness() == 0.0f) {
 | 
			
		||||
    this->color_brightness_ = 1.0f;
 | 
			
		||||
    this->set_flag_(FLAG_HAS_COLOR_BRIGHTNESS);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Create color values for the light with this call applied.
 | 
			
		||||
  // Capability validation
 | 
			
		||||
  if (this->has_brightness() && this->brightness_ > 0.0f && !(color_mode & ColorCapability::BRIGHTNESS))
 | 
			
		||||
    this->log_and_clear_unsupported_(FLAG_HAS_BRIGHTNESS, LOG_STR("brightness"), false);
 | 
			
		||||
 | 
			
		||||
  // Transition length possible check
 | 
			
		||||
  if (this->has_transition_() && this->transition_length_ != 0 && !(color_mode & ColorCapability::BRIGHTNESS))
 | 
			
		||||
    this->log_and_clear_unsupported_(FLAG_HAS_TRANSITION, LOG_STR("transitions"), false);
 | 
			
		||||
 | 
			
		||||
  if (this->has_color_brightness() && this->color_brightness_ > 0.0f && !(color_mode & ColorCapability::RGB))
 | 
			
		||||
    this->log_and_clear_unsupported_(FLAG_HAS_COLOR_BRIGHTNESS, LOG_STR("RGB brightness"), true);
 | 
			
		||||
 | 
			
		||||
  // RGB exists check
 | 
			
		||||
  if (((this->has_red() && this->red_ > 0.0f) || (this->has_green() && this->green_ > 0.0f) ||
 | 
			
		||||
       (this->has_blue() && this->blue_ > 0.0f)) &&
 | 
			
		||||
      !(color_mode & ColorCapability::RGB)) {
 | 
			
		||||
    log_color_mode_not_supported(name, LOG_STR("RGB color"));
 | 
			
		||||
    this->clear_flag_(FLAG_HAS_RED);
 | 
			
		||||
    this->clear_flag_(FLAG_HAS_GREEN);
 | 
			
		||||
    this->clear_flag_(FLAG_HAS_BLUE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // White value exists check
 | 
			
		||||
  if (this->has_white() && this->white_ > 0.0f &&
 | 
			
		||||
      !(color_mode & ColorCapability::WHITE || color_mode & ColorCapability::COLD_WARM_WHITE))
 | 
			
		||||
    this->log_and_clear_unsupported_(FLAG_HAS_WHITE, LOG_STR("white value"), true);
 | 
			
		||||
 | 
			
		||||
  // Color temperature exists check
 | 
			
		||||
  if (this->has_color_temperature() &&
 | 
			
		||||
      !(color_mode & ColorCapability::COLOR_TEMPERATURE || color_mode & ColorCapability::COLD_WARM_WHITE))
 | 
			
		||||
    this->log_and_clear_unsupported_(FLAG_HAS_COLOR_TEMPERATURE, LOG_STR("color temperature"), true);
 | 
			
		||||
 | 
			
		||||
  // Cold/warm white value exists check
 | 
			
		||||
  if (((this->has_cold_white() && this->cold_white_ > 0.0f) || (this->has_warm_white() && this->warm_white_ > 0.0f)) &&
 | 
			
		||||
      !(color_mode & ColorCapability::COLD_WARM_WHITE)) {
 | 
			
		||||
    log_color_mode_not_supported(name, LOG_STR("cold/warm white value"));
 | 
			
		||||
    this->clear_flag_(FLAG_HAS_COLD_WHITE);
 | 
			
		||||
    this->clear_flag_(FLAG_HAS_WARM_WHITE);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Create color values and validate+apply ranges in one step to eliminate duplicate checks
 | 
			
		||||
  auto v = this->parent_->remote_values;
 | 
			
		||||
  if (this->has_color_mode())
 | 
			
		||||
    v.set_color_mode(this->color_mode_);
 | 
			
		||||
  if (this->has_state())
 | 
			
		||||
    v.set_state(this->state_);
 | 
			
		||||
  if (this->has_brightness())
 | 
			
		||||
    v.set_brightness(this->brightness_);
 | 
			
		||||
  if (this->has_color_brightness())
 | 
			
		||||
    v.set_color_brightness(this->color_brightness_);
 | 
			
		||||
  if (this->has_red())
 | 
			
		||||
    v.set_red(this->red_);
 | 
			
		||||
  if (this->has_green())
 | 
			
		||||
    v.set_green(this->green_);
 | 
			
		||||
  if (this->has_blue())
 | 
			
		||||
    v.set_blue(this->blue_);
 | 
			
		||||
  if (this->has_white())
 | 
			
		||||
    v.set_white(this->white_);
 | 
			
		||||
  if (this->has_color_temperature())
 | 
			
		||||
    v.set_color_temperature(this->color_temperature_);
 | 
			
		||||
  if (this->has_cold_white())
 | 
			
		||||
    v.set_cold_white(this->cold_white_);
 | 
			
		||||
  if (this->has_warm_white())
 | 
			
		||||
    v.set_warm_white(this->warm_white_);
 | 
			
		||||
 | 
			
		||||
#define VALIDATE_AND_APPLY(field, setter, name_str, ...) \
 | 
			
		||||
  if (this->has_##field()) { \
 | 
			
		||||
    clamp_and_log_if_invalid(name, this->field##_, LOG_STR(name_str), ##__VA_ARGS__); \
 | 
			
		||||
    v.setter(this->field##_); \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  VALIDATE_AND_APPLY(brightness, set_brightness, "Brightness")
 | 
			
		||||
  VALIDATE_AND_APPLY(color_brightness, set_color_brightness, "Color brightness")
 | 
			
		||||
  VALIDATE_AND_APPLY(red, set_red, "Red")
 | 
			
		||||
  VALIDATE_AND_APPLY(green, set_green, "Green")
 | 
			
		||||
  VALIDATE_AND_APPLY(blue, set_blue, "Blue")
 | 
			
		||||
  VALIDATE_AND_APPLY(white, set_white, "White")
 | 
			
		||||
  VALIDATE_AND_APPLY(cold_white, set_cold_white, "Cold white")
 | 
			
		||||
  VALIDATE_AND_APPLY(warm_white, set_warm_white, "Warm white")
 | 
			
		||||
  VALIDATE_AND_APPLY(color_temperature, set_color_temperature, "Color temperature", traits.get_min_mireds(),
 | 
			
		||||
                     traits.get_max_mireds())
 | 
			
		||||
 | 
			
		||||
#undef VALIDATE_AND_APPLY
 | 
			
		||||
 | 
			
		||||
  v.normalize_color();
 | 
			
		||||
 | 
			
		||||
  // Flash length check
 | 
			
		||||
  if (this->has_flash_() && this->flash_length_ == 0) {
 | 
			
		||||
    log_invalid_parameter(name, LOG_STR("flash length must be greater than zero"));
 | 
			
		||||
    this->set_flag_(FLAG_HAS_FLASH, false);
 | 
			
		||||
    log_invalid_parameter(name, LOG_STR("flash length must be >0"));
 | 
			
		||||
    this->clear_flag_(FLAG_HAS_FLASH);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // validate transition length/flash length/effect not used at the same time
 | 
			
		||||
@@ -330,42 +310,40 @@ LightColorValues LightCall::validate_() {
 | 
			
		||||
 | 
			
		||||
  // If effect is already active, remove effect start
 | 
			
		||||
  if (this->has_effect_() && this->effect_ == this->parent_->active_effect_index_) {
 | 
			
		||||
    this->set_flag_(FLAG_HAS_EFFECT, false);
 | 
			
		||||
    this->clear_flag_(FLAG_HAS_EFFECT);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // validate effect index
 | 
			
		||||
  if (this->has_effect_() && this->effect_ > this->parent_->effects_.size()) {
 | 
			
		||||
    ESP_LOGW(TAG, "'%s': invalid effect index %" PRIu32, name, this->effect_);
 | 
			
		||||
    this->set_flag_(FLAG_HAS_EFFECT, false);
 | 
			
		||||
    this->clear_flag_(FLAG_HAS_EFFECT);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->has_effect_() && (this->has_transition_() || this->has_flash_())) {
 | 
			
		||||
    log_invalid_parameter(name, LOG_STR("effect cannot be used with transition/flash"));
 | 
			
		||||
    this->set_flag_(FLAG_HAS_TRANSITION, false);
 | 
			
		||||
    this->set_flag_(FLAG_HAS_FLASH, false);
 | 
			
		||||
    this->clear_flag_(FLAG_HAS_TRANSITION);
 | 
			
		||||
    this->clear_flag_(FLAG_HAS_FLASH);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->has_flash_() && this->has_transition_()) {
 | 
			
		||||
    log_invalid_parameter(name, LOG_STR("flash cannot be used with transition"));
 | 
			
		||||
    this->set_flag_(FLAG_HAS_TRANSITION, false);
 | 
			
		||||
    this->clear_flag_(FLAG_HAS_TRANSITION);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (!this->has_transition_() && !this->has_flash_() && (!this->has_effect_() || this->effect_ == 0) &&
 | 
			
		||||
      supports_transition) {
 | 
			
		||||
    // nothing specified and light supports transitions, set default transition length
 | 
			
		||||
    this->transition_length_ = this->parent_->default_transition_length_;
 | 
			
		||||
    this->set_flag_(FLAG_HAS_TRANSITION, true);
 | 
			
		||||
    this->set_flag_(FLAG_HAS_TRANSITION);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->has_transition_() && this->transition_length_ == 0) {
 | 
			
		||||
    // 0 transition is interpreted as no transition (instant change)
 | 
			
		||||
    this->set_flag_(FLAG_HAS_TRANSITION, false);
 | 
			
		||||
    this->clear_flag_(FLAG_HAS_TRANSITION);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->has_transition_() && !supports_transition) {
 | 
			
		||||
    log_feature_not_supported(name, LOG_STR("transitions"));
 | 
			
		||||
    this->set_flag_(FLAG_HAS_TRANSITION, false);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->has_transition_() && !supports_transition)
 | 
			
		||||
    this->log_and_clear_unsupported_(FLAG_HAS_TRANSITION, LOG_STR("transitions"), false);
 | 
			
		||||
 | 
			
		||||
  // If not a flash and turning the light off, then disable the light
 | 
			
		||||
  // Do not use light color values directly, so that effects can set 0% brightness
 | 
			
		||||
@@ -374,17 +352,17 @@ LightColorValues LightCall::validate_() {
 | 
			
		||||
  if (!this->has_flash_() && !target_state) {
 | 
			
		||||
    if (this->has_effect_()) {
 | 
			
		||||
      log_invalid_parameter(name, LOG_STR("cannot start effect when turning off"));
 | 
			
		||||
      this->set_flag_(FLAG_HAS_EFFECT, false);
 | 
			
		||||
      this->clear_flag_(FLAG_HAS_EFFECT);
 | 
			
		||||
    } else if (this->parent_->active_effect_index_ != 0 && explicit_turn_off_request) {
 | 
			
		||||
      // Auto turn off effect
 | 
			
		||||
      this->effect_ = 0;
 | 
			
		||||
      this->set_flag_(FLAG_HAS_EFFECT, true);
 | 
			
		||||
      this->set_flag_(FLAG_HAS_EFFECT);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Disable saving for flashes
 | 
			
		||||
  if (this->has_flash_())
 | 
			
		||||
    this->set_flag_(FLAG_SAVE, false);
 | 
			
		||||
    this->clear_flag_(FLAG_SAVE);
 | 
			
		||||
 | 
			
		||||
  return v;
 | 
			
		||||
}
 | 
			
		||||
@@ -418,12 +396,12 @@ void LightCall::transform_parameters_() {
 | 
			
		||||
      const float gamma = this->parent_->get_gamma_correct();
 | 
			
		||||
      this->cold_white_ = gamma_uncorrect(cw_fraction / max_cw_ww, gamma);
 | 
			
		||||
      this->warm_white_ = gamma_uncorrect(ww_fraction / max_cw_ww, gamma);
 | 
			
		||||
      this->set_flag_(FLAG_HAS_COLD_WHITE, true);
 | 
			
		||||
      this->set_flag_(FLAG_HAS_WARM_WHITE, true);
 | 
			
		||||
      this->set_flag_(FLAG_HAS_COLD_WHITE);
 | 
			
		||||
      this->set_flag_(FLAG_HAS_WARM_WHITE);
 | 
			
		||||
    }
 | 
			
		||||
    if (this->has_white()) {
 | 
			
		||||
      this->brightness_ = this->white_;
 | 
			
		||||
      this->set_flag_(FLAG_HAS_BRIGHTNESS, true);
 | 
			
		||||
      this->set_flag_(FLAG_HAS_BRIGHTNESS);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -630,7 +608,7 @@ LightCall &LightCall::set_effect(optional<std::string> effect) {
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_effect(uint32_t effect_number) {
 | 
			
		||||
  this->effect_ = effect_number;
 | 
			
		||||
  this->set_flag_(FLAG_HAS_EFFECT, true);
 | 
			
		||||
  this->set_flag_(FLAG_HAS_EFFECT);
 | 
			
		||||
  return *this;
 | 
			
		||||
}
 | 
			
		||||
LightCall &LightCall::set_effect(optional<uint32_t> effect_number) {
 | 
			
		||||
 
 | 
			
		||||
@@ -4,6 +4,10 @@
 | 
			
		||||
#include <set>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
// Forward declaration
 | 
			
		||||
struct LogString;
 | 
			
		||||
 | 
			
		||||
namespace light {
 | 
			
		||||
 | 
			
		||||
class LightState;
 | 
			
		||||
@@ -207,14 +211,14 @@ class LightCall {
 | 
			
		||||
    FLAG_SAVE = 1 << 15,
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; }
 | 
			
		||||
  bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; }
 | 
			
		||||
  bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; }
 | 
			
		||||
  bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; }
 | 
			
		||||
  bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; }
 | 
			
		||||
  inline bool has_transition_() { return (this->flags_ & FLAG_HAS_TRANSITION) != 0; }
 | 
			
		||||
  inline bool has_flash_() { return (this->flags_ & FLAG_HAS_FLASH) != 0; }
 | 
			
		||||
  inline bool has_effect_() { return (this->flags_ & FLAG_HAS_EFFECT) != 0; }
 | 
			
		||||
  inline bool get_publish_() { return (this->flags_ & FLAG_PUBLISH) != 0; }
 | 
			
		||||
  inline bool get_save_() { return (this->flags_ & FLAG_SAVE) != 0; }
 | 
			
		||||
 | 
			
		||||
  // Helper to set flag
 | 
			
		||||
  void set_flag_(FieldFlags flag, bool value) {
 | 
			
		||||
  // Helper to set flag - defaults to true for common case
 | 
			
		||||
  void set_flag_(FieldFlags flag, bool value = true) {
 | 
			
		||||
    if (value) {
 | 
			
		||||
      this->flags_ |= flag;
 | 
			
		||||
    } else {
 | 
			
		||||
@@ -222,6 +226,12 @@ class LightCall {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Helper to clear flag - reduces code size for common case
 | 
			
		||||
  void clear_flag_(FieldFlags flag) { this->flags_ &= ~flag; }
 | 
			
		||||
 | 
			
		||||
  // Helper to log unsupported feature and clear flag - reduces code duplication
 | 
			
		||||
  void log_and_clear_unsupported_(FieldFlags flag, const LogString *feature, bool use_color_mode_log);
 | 
			
		||||
 | 
			
		||||
  LightState *parent_;
 | 
			
		||||
 | 
			
		||||
  // Light state values - use flags_ to check if a value has been set.
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								esphome/components/lm75b/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/lm75b/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										39
									
								
								esphome/components/lm75b/lm75b.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										39
									
								
								esphome/components/lm75b/lm75b.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,39 @@
 | 
			
		||||
#include "lm75b.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace lm75b {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "lm75b";
 | 
			
		||||
 | 
			
		||||
void LM75BComponent::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "LM75B:");
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
  if (this->is_failed()) {
 | 
			
		||||
    ESP_LOGE(TAG, "Setting up LM75B failed!");
 | 
			
		||||
  }
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
  LOG_SENSOR("  ", "Temperature", this);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LM75BComponent::update() {
 | 
			
		||||
  // Create a temporary buffer
 | 
			
		||||
  uint8_t buff[2];
 | 
			
		||||
  if (this->read_register(LM75B_REG_TEMPERATURE, buff, 2) != i2c::ERROR_OK) {
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  // Obtain combined 16-bit value
 | 
			
		||||
  int16_t raw_temperature = (buff[0] << 8) | buff[1];
 | 
			
		||||
  // Read the 11-bit raw temperature value
 | 
			
		||||
  raw_temperature >>= 5;
 | 
			
		||||
  // Publish the temperature in °C
 | 
			
		||||
  this->publish_state(raw_temperature * 0.125);
 | 
			
		||||
  if (this->status_has_warning()) {
 | 
			
		||||
    this->status_clear_warning();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace lm75b
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user