mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-04 00:51:49 +00:00 
			
		
		
		
	Compare commits
	
		
			3 Commits
		
	
	
		
			jesserockz
			...
			copilot/fi
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					484087f780 | ||
| 
						 | 
					d91eed1411 | ||
| 
						 | 
					47714a1745 | 
							
								
								
									
										2
									
								
								.github/actions/restore-python/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/actions/restore-python/action.yml
									
									
									
									
										vendored
									
									
								
							@@ -22,7 +22,7 @@ runs:
 | 
			
		||||
        python-version: ${{ inputs.python-version }}
 | 
			
		||||
    - name: Restore Python virtual environment
 | 
			
		||||
      id: cache-venv
 | 
			
		||||
      uses: actions/cache/restore@v4.2.4
 | 
			
		||||
      uses: actions/cache/restore@v4.2.3
 | 
			
		||||
      with:
 | 
			
		||||
        path: venv
 | 
			
		||||
        # yamllint disable-line rule:line-length
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/auto-label-pr.yml
									
									
									
									
										vendored
									
									
								
							@@ -22,7 +22,7 @@ jobs:
 | 
			
		||||
    if: github.event.action != 'labeled' || github.event.sender.type != 'Bot'
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
 | 
			
		||||
      - name: Generate a token
 | 
			
		||||
        id: generate-token
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-api-proto.yml
									
									
									
									
										vendored
									
									
								
							@@ -21,7 +21,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        with:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/ci-clang-tidy-hash.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-clang-tidy-hash.yml
									
									
									
									
										vendored
									
									
								
							@@ -20,7 +20,7 @@ jobs:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/ci-docker.yml
									
									
									
									
										vendored
									
									
								
							@@ -43,7 +43,7 @@ jobs:
 | 
			
		||||
          - "docker"
 | 
			
		||||
          # - "lint"
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5.0.0
 | 
			
		||||
      - uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        with:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										32
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										32
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@@ -36,7 +36,7 @@ jobs:
 | 
			
		||||
      cache-key: ${{ steps.cache-key.outputs.key }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Generate cache-key
 | 
			
		||||
        id: cache-key
 | 
			
		||||
        run: echo key="${{ hashFiles('requirements.txt', 'requirements_test.txt', '.pre-commit-config.yaml') }}" >> $GITHUB_OUTPUT
 | 
			
		||||
@@ -47,7 +47,7 @@ jobs:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
      - name: Restore Python virtual environment
 | 
			
		||||
        id: cache-venv
 | 
			
		||||
        uses: actions/cache@v4.2.4
 | 
			
		||||
        uses: actions/cache@v4.2.3
 | 
			
		||||
        with:
 | 
			
		||||
          path: venv
 | 
			
		||||
          # yamllint disable-line rule:line-length
 | 
			
		||||
@@ -70,7 +70,7 @@ jobs:
 | 
			
		||||
    if: needs.determine-jobs.outputs.python-linters == 'true'
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -91,7 +91,7 @@ jobs:
 | 
			
		||||
      - common
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -136,7 +136,7 @@ jobs:
 | 
			
		||||
      - common
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        id: restore-python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
@@ -161,7 +161,7 @@ jobs:
 | 
			
		||||
          token: ${{ secrets.CODECOV_TOKEN }}
 | 
			
		||||
      - name: Save Python virtual environment cache
 | 
			
		||||
        if: github.ref == 'refs/heads/dev'
 | 
			
		||||
        uses: actions/cache/save@v4.2.4
 | 
			
		||||
        uses: actions/cache/save@v4.2.3
 | 
			
		||||
        with:
 | 
			
		||||
          path: venv
 | 
			
		||||
          key: ${{ runner.os }}-${{ steps.restore-python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
 | 
			
		||||
@@ -179,7 +179,7 @@ jobs:
 | 
			
		||||
      component-test-count: ${{ steps.determine.outputs.component-test-count }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        with:
 | 
			
		||||
          # Fetch enough history to find the merge base
 | 
			
		||||
          fetch-depth: 2
 | 
			
		||||
@@ -214,7 +214,7 @@ jobs:
 | 
			
		||||
    if: needs.determine-jobs.outputs.integration-tests == 'true'
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Set up Python 3.13
 | 
			
		||||
        id: python
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
@@ -222,7 +222,7 @@ jobs:
 | 
			
		||||
          python-version: "3.13"
 | 
			
		||||
      - name: Restore Python virtual environment
 | 
			
		||||
        id: cache-venv
 | 
			
		||||
        uses: actions/cache@v4.2.4
 | 
			
		||||
        uses: actions/cache@v4.2.3
 | 
			
		||||
        with:
 | 
			
		||||
          path: venv
 | 
			
		||||
          key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ needs.common.outputs.cache-key }}
 | 
			
		||||
@@ -287,7 +287,7 @@ jobs:
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        with:
 | 
			
		||||
          # Need history for HEAD~1 to work for checking changed files
 | 
			
		||||
          fetch-depth: 2
 | 
			
		||||
@@ -300,14 +300,14 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        if: github.ref == 'refs/heads/dev'
 | 
			
		||||
        uses: actions/cache@v4.2.4
 | 
			
		||||
        uses: actions/cache@v4.2.3
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
 | 
			
		||||
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        if: github.ref != 'refs/heads/dev'
 | 
			
		||||
        uses: actions/cache/restore@v4.2.4
 | 
			
		||||
        uses: actions/cache/restore@v4.2.3
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }}
 | 
			
		||||
@@ -374,7 +374,7 @@ jobs:
 | 
			
		||||
          sudo apt-get install libsdl2-dev
 | 
			
		||||
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -400,7 +400,7 @@ jobs:
 | 
			
		||||
      matrix: ${{ steps.split.outputs.components }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Split components into 20 groups
 | 
			
		||||
        id: split
 | 
			
		||||
        run: |
 | 
			
		||||
@@ -430,7 +430,7 @@ jobs:
 | 
			
		||||
          sudo apt-get install libsdl2-dev
 | 
			
		||||
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
@@ -459,7 +459,7 @@ jobs:
 | 
			
		||||
    if: github.event_name == 'pull_request' && github.base_ref != 'beta' && github.base_ref != 'release'
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/codeql.yml
									
									
									
									
										vendored
									
									
								
							@@ -54,7 +54,7 @@ jobs:
 | 
			
		||||
            # your codebase is analyzed, see https://docs.github.com/en/code-security/code-scanning/creating-an-advanced-setup-for-code-scanning/codeql-code-scanning-for-compiled-languages
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout repository
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@v4
 | 
			
		||||
 | 
			
		||||
      # Initializes the CodeQL tools for scanning.
 | 
			
		||||
      - name: Initialize CodeQL
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										18
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							@@ -20,7 +20,7 @@ jobs:
 | 
			
		||||
      branch_build: ${{ steps.tag.outputs.branch_build }}
 | 
			
		||||
      deploy_env: ${{ steps.tag.outputs.deploy_env }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5.0.0
 | 
			
		||||
      - uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Get tag
 | 
			
		||||
        id: tag
 | 
			
		||||
        # yamllint disable rule:line-length
 | 
			
		||||
@@ -60,7 +60,7 @@ jobs:
 | 
			
		||||
      contents: read
 | 
			
		||||
      id-token: write
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5.0.0
 | 
			
		||||
      - uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        with:
 | 
			
		||||
@@ -92,7 +92,7 @@ jobs:
 | 
			
		||||
            os: "ubuntu-24.04-arm"
 | 
			
		||||
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5.0.0
 | 
			
		||||
      - uses: actions/checkout@v4.2.2
 | 
			
		||||
      - name: Set up Python
 | 
			
		||||
        uses: actions/setup-python@v5.6.0
 | 
			
		||||
        with:
 | 
			
		||||
@@ -102,12 +102,12 @@ jobs:
 | 
			
		||||
        uses: docker/setup-buildx-action@v3.11.1
 | 
			
		||||
 | 
			
		||||
      - name: Log in to docker hub
 | 
			
		||||
        uses: docker/login-action@v3.5.0
 | 
			
		||||
        uses: docker/login-action@v3.4.0
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ secrets.DOCKER_USER }}
 | 
			
		||||
          password: ${{ secrets.DOCKER_PASSWORD }}
 | 
			
		||||
      - name: Log in to the GitHub container registry
 | 
			
		||||
        uses: docker/login-action@v3.5.0
 | 
			
		||||
        uses: docker/login-action@v3.4.0
 | 
			
		||||
        with:
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.actor }}
 | 
			
		||||
@@ -168,10 +168,10 @@ jobs:
 | 
			
		||||
          - ghcr
 | 
			
		||||
          - dockerhub
 | 
			
		||||
    steps:
 | 
			
		||||
      - uses: actions/checkout@v5.0.0
 | 
			
		||||
      - uses: actions/checkout@v4.2.2
 | 
			
		||||
 | 
			
		||||
      - name: Download digests
 | 
			
		||||
        uses: actions/download-artifact@v5.0.0
 | 
			
		||||
        uses: actions/download-artifact@v4.3.0
 | 
			
		||||
        with:
 | 
			
		||||
          pattern: digests-*
 | 
			
		||||
          path: /tmp/digests
 | 
			
		||||
@@ -182,13 +182,13 @@ jobs:
 | 
			
		||||
 | 
			
		||||
      - name: Log in to docker hub
 | 
			
		||||
        if: matrix.registry == 'dockerhub'
 | 
			
		||||
        uses: docker/login-action@v3.5.0
 | 
			
		||||
        uses: docker/login-action@v3.4.0
 | 
			
		||||
        with:
 | 
			
		||||
          username: ${{ secrets.DOCKER_USER }}
 | 
			
		||||
          password: ${{ secrets.DOCKER_PASSWORD }}
 | 
			
		||||
      - name: Log in to the GitHub container registry
 | 
			
		||||
        if: matrix.registry == 'ghcr'
 | 
			
		||||
        uses: docker/login-action@v3.5.0
 | 
			
		||||
        uses: docker/login-action@v3.4.0
 | 
			
		||||
        with:
 | 
			
		||||
          registry: ghcr.io
 | 
			
		||||
          username: ${{ github.actor }}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										4
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							@@ -13,10 +13,10 @@ jobs:
 | 
			
		||||
    if: github.repository == 'esphome/esphome'
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Checkout
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
 | 
			
		||||
      - name: Checkout Home Assistant
 | 
			
		||||
        uses: actions/checkout@v5.0.0
 | 
			
		||||
        uses: actions/checkout@v4.2.2
 | 
			
		||||
        with:
 | 
			
		||||
          repository: home-assistant/core
 | 
			
		||||
          path: lib/home-assistant
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ ci:
 | 
			
		||||
repos:
 | 
			
		||||
  - repo: https://github.com/astral-sh/ruff-pre-commit
 | 
			
		||||
    # Ruff version.
 | 
			
		||||
    rev: v0.12.8
 | 
			
		||||
    rev: v0.12.7
 | 
			
		||||
    hooks:
 | 
			
		||||
      # Run the linter.
 | 
			
		||||
      - id: ruff
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										22
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										22
									
								
								CODEOWNERS
									
									
									
									
									
								
							@@ -40,11 +40,11 @@ esphome/components/analog_threshold/* @ianchi
 | 
			
		||||
esphome/components/animation/* @syndlex
 | 
			
		||||
esphome/components/anova/* @buxtronix
 | 
			
		||||
esphome/components/apds9306/* @aodrenah
 | 
			
		||||
esphome/components/api/* @esphome/core
 | 
			
		||||
esphome/components/api/* @OttoWinter
 | 
			
		||||
esphome/components/as5600/* @ammmze
 | 
			
		||||
esphome/components/as5600/sensor/* @ammmze
 | 
			
		||||
esphome/components/as7341/* @mrgnr
 | 
			
		||||
esphome/components/async_tcp/* @esphome/core
 | 
			
		||||
esphome/components/async_tcp/* @OttoWinter
 | 
			
		||||
esphome/components/at581x/* @X-Ryl669
 | 
			
		||||
esphome/components/atc_mithermometer/* @ahpohl
 | 
			
		||||
esphome/components/atm90e26/* @danieltwagner
 | 
			
		||||
@@ -69,7 +69,7 @@ esphome/components/bl0939/* @ziceva
 | 
			
		||||
esphome/components/bl0940/* @tobias-
 | 
			
		||||
esphome/components/bl0942/* @dbuezas @dwmw2
 | 
			
		||||
esphome/components/ble_client/* @buxtronix @clydebarrow
 | 
			
		||||
esphome/components/bluetooth_proxy/* @bdraco @jesserockz
 | 
			
		||||
esphome/components/bluetooth_proxy/* @jesserockz
 | 
			
		||||
esphome/components/bme280_base/* @esphome/core
 | 
			
		||||
esphome/components/bme280_spi/* @apbodrov
 | 
			
		||||
esphome/components/bme680_bsec/* @trvrnrth
 | 
			
		||||
@@ -91,7 +91,7 @@ esphome/components/bytebuffer/* @clydebarrow
 | 
			
		||||
esphome/components/camera/* @DT-art1 @bdraco
 | 
			
		||||
esphome/components/canbus/* @danielschramm @mvturnho
 | 
			
		||||
esphome/components/cap1188/* @mreditor97
 | 
			
		||||
esphome/components/captive_portal/* @esphome/core
 | 
			
		||||
esphome/components/captive_portal/* @OttoWinter
 | 
			
		||||
esphome/components/ccs811/* @habbie
 | 
			
		||||
esphome/components/cd74hc4067/* @asoehlke
 | 
			
		||||
esphome/components/ch422g/* @clydebarrow @jesterret
 | 
			
		||||
@@ -118,7 +118,7 @@ esphome/components/dallas_temp/* @ssieb
 | 
			
		||||
esphome/components/daly_bms/* @s1lvi0
 | 
			
		||||
esphome/components/dashboard_import/* @esphome/core
 | 
			
		||||
esphome/components/datetime/* @jesserockz @rfdarter
 | 
			
		||||
esphome/components/debug/* @esphome/core
 | 
			
		||||
esphome/components/debug/* @OttoWinter
 | 
			
		||||
esphome/components/delonghi/* @grob6000
 | 
			
		||||
esphome/components/dfplayer/* @glmnet
 | 
			
		||||
esphome/components/dfrobot_sen0395/* @niklasweber
 | 
			
		||||
@@ -144,10 +144,9 @@ esphome/components/es8156/* @kbx81
 | 
			
		||||
esphome/components/es8311/* @kahrendt @kroimon
 | 
			
		||||
esphome/components/es8388/* @P4uLT
 | 
			
		||||
esphome/components/esp32/* @esphome/core
 | 
			
		||||
esphome/components/esp32_ble/* @Rapsssito @bdraco @jesserockz
 | 
			
		||||
esphome/components/esp32_ble_client/* @bdraco @jesserockz
 | 
			
		||||
esphome/components/esp32_ble/* @Rapsssito @jesserockz
 | 
			
		||||
esphome/components/esp32_ble_client/* @jesserockz
 | 
			
		||||
esphome/components/esp32_ble_server/* @Rapsssito @clydebarrow @jesserockz
 | 
			
		||||
esphome/components/esp32_ble_tracker/* @bdraco
 | 
			
		||||
esphome/components/esp32_camera_web_server/* @ayufan
 | 
			
		||||
esphome/components/esp32_can/* @Sympatron
 | 
			
		||||
esphome/components/esp32_hosted/* @swoboda1337
 | 
			
		||||
@@ -238,7 +237,7 @@ esphome/components/integration/* @OttoWinter
 | 
			
		||||
esphome/components/internal_temperature/* @Mat931
 | 
			
		||||
esphome/components/interval/* @esphome/core
 | 
			
		||||
esphome/components/jsn_sr04t/* @Mafus1
 | 
			
		||||
esphome/components/json/* @esphome/core
 | 
			
		||||
esphome/components/json/* @OttoWinter
 | 
			
		||||
esphome/components/kamstrup_kmp/* @cfeenstra1024
 | 
			
		||||
esphome/components/key_collector/* @ssieb
 | 
			
		||||
esphome/components/key_provider/* @ssieb
 | 
			
		||||
@@ -246,7 +245,6 @@ esphome/components/kuntze/* @ssieb
 | 
			
		||||
esphome/components/lc709203f/* @ilikecake
 | 
			
		||||
esphome/components/lcd_menu/* @numo68
 | 
			
		||||
esphome/components/ld2410/* @regevbr @sebcaps
 | 
			
		||||
esphome/components/ld2412/* @Rihan9
 | 
			
		||||
esphome/components/ld2420/* @descipher
 | 
			
		||||
esphome/components/ld2450/* @hareeshmu
 | 
			
		||||
esphome/components/ld24xx/* @kbx81
 | 
			
		||||
@@ -469,7 +467,7 @@ esphome/components/template/event/* @nohat
 | 
			
		||||
esphome/components/template/fan/* @ssieb
 | 
			
		||||
esphome/components/text/* @mauritskorse
 | 
			
		||||
esphome/components/thermostat/* @kbx81
 | 
			
		||||
esphome/components/time/* @esphome/core
 | 
			
		||||
esphome/components/time/* @OttoWinter
 | 
			
		||||
esphome/components/tlc5947/* @rnauber
 | 
			
		||||
esphome/components/tlc5971/* @IJIJI
 | 
			
		||||
esphome/components/tm1621/* @Philippe12
 | 
			
		||||
@@ -513,7 +511,7 @@ esphome/components/wake_on_lan/* @clydebarrow @willwill2will54
 | 
			
		||||
esphome/components/watchdog/* @oarcher
 | 
			
		||||
esphome/components/waveshare_epaper/* @clydebarrow
 | 
			
		||||
esphome/components/web_server/ota/* @esphome/core
 | 
			
		||||
esphome/components/web_server_base/* @esphome/core
 | 
			
		||||
esphome/components/web_server_base/* @OttoWinter
 | 
			
		||||
esphome/components/web_server_idf/* @dentra
 | 
			
		||||
esphome/components/weikai/* @DrCoolZic
 | 
			
		||||
esphome/components/weikai_i2c/* @DrCoolZic
 | 
			
		||||
 
 | 
			
		||||
@@ -90,7 +90,7 @@ def main():
 | 
			
		||||
    def run_command(*cmd, ignore_error: bool = False):
 | 
			
		||||
        print(f"$ {shlex.join(list(cmd))}")
 | 
			
		||||
        if not args.dry_run:
 | 
			
		||||
            rc = subprocess.call(list(cmd), close_fds=False)
 | 
			
		||||
            rc = subprocess.call(list(cmd))
 | 
			
		||||
            if rc != 0 and not ignore_error:
 | 
			
		||||
                print("Command failed")
 | 
			
		||||
                sys.exit(1)
 | 
			
		||||
 
 | 
			
		||||
@@ -9,7 +9,6 @@ import os
 | 
			
		||||
import re
 | 
			
		||||
import sys
 | 
			
		||||
import time
 | 
			
		||||
from typing import Protocol
 | 
			
		||||
 | 
			
		||||
import argcomplete
 | 
			
		||||
 | 
			
		||||
@@ -45,7 +44,6 @@ from esphome.const import (
 | 
			
		||||
from esphome.core import CORE, EsphomeError, coroutine
 | 
			
		||||
from esphome.helpers import get_bool_env, indent, is_ip_address
 | 
			
		||||
from esphome.log import AnsiFore, color, setup_log
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
from esphome.util import (
 | 
			
		||||
    get_serial_ports,
 | 
			
		||||
    list_yaml_files,
 | 
			
		||||
@@ -57,23 +55,6 @@ from esphome.util import (
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class ArgsProtocol(Protocol):
 | 
			
		||||
    device: list[str] | None
 | 
			
		||||
    reset: bool
 | 
			
		||||
    username: str | None
 | 
			
		||||
    password: str | None
 | 
			
		||||
    client_id: str | None
 | 
			
		||||
    topic: str | None
 | 
			
		||||
    file: str | None
 | 
			
		||||
    no_logs: bool
 | 
			
		||||
    only_generate: bool
 | 
			
		||||
    show_secrets: bool
 | 
			
		||||
    dashboard: bool
 | 
			
		||||
    configuration: str
 | 
			
		||||
    name: str
 | 
			
		||||
    upload_speed: str | None
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def choose_prompt(options, purpose: str = None):
 | 
			
		||||
    if not options:
 | 
			
		||||
        raise EsphomeError(
 | 
			
		||||
@@ -107,54 +88,30 @@ def choose_prompt(options, purpose: str = None):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def choose_upload_log_host(
 | 
			
		||||
    default: list[str] | str | None,
 | 
			
		||||
    check_default: str | None,
 | 
			
		||||
    show_ota: bool,
 | 
			
		||||
    show_mqtt: bool,
 | 
			
		||||
    show_api: bool,
 | 
			
		||||
    purpose: str | None = None,
 | 
			
		||||
) -> list[str]:
 | 
			
		||||
    # Convert to list for uniform handling
 | 
			
		||||
    defaults = [default] if isinstance(default, str) else default or []
 | 
			
		||||
 | 
			
		||||
    # If devices specified, resolve them
 | 
			
		||||
    if defaults:
 | 
			
		||||
        resolved: list[str] = []
 | 
			
		||||
        for device in defaults:
 | 
			
		||||
            if device == "SERIAL":
 | 
			
		||||
                serial_ports = get_serial_ports()
 | 
			
		||||
                if not serial_ports:
 | 
			
		||||
                    _LOGGER.warning("No serial ports found, skipping SERIAL device")
 | 
			
		||||
                    continue
 | 
			
		||||
                options = [
 | 
			
		||||
                    (f"{port.path} ({port.description})", port.path)
 | 
			
		||||
                    for port in serial_ports
 | 
			
		||||
                ]
 | 
			
		||||
                resolved.append(choose_prompt(options, purpose=purpose))
 | 
			
		||||
            elif device == "OTA":
 | 
			
		||||
                if (show_ota and "ota" in CORE.config) or (
 | 
			
		||||
                    show_api and "api" in CORE.config
 | 
			
		||||
                ):
 | 
			
		||||
                    resolved.append(CORE.address)
 | 
			
		||||
                elif show_mqtt and has_mqtt_logging():
 | 
			
		||||
                    resolved.append("MQTT")
 | 
			
		||||
            else:
 | 
			
		||||
                resolved.append(device)
 | 
			
		||||
        return resolved
 | 
			
		||||
 | 
			
		||||
    # No devices specified, show interactive chooser
 | 
			
		||||
    default, check_default, show_ota, show_mqtt, show_api, purpose: str = None
 | 
			
		||||
):
 | 
			
		||||
    options = [
 | 
			
		||||
        (f"{port.path} ({port.description})", port.path) for port in get_serial_ports()
 | 
			
		||||
    ]
 | 
			
		||||
    if default == "SERIAL":
 | 
			
		||||
        return choose_prompt(options, purpose=purpose)
 | 
			
		||||
    if (show_ota and "ota" in CORE.config) or (show_api and "api" in CORE.config):
 | 
			
		||||
        options.append((f"Over The Air ({CORE.address})", CORE.address))
 | 
			
		||||
    if show_mqtt and has_mqtt_logging():
 | 
			
		||||
        mqtt_config = CORE.config[CONF_MQTT]
 | 
			
		||||
        if default == "OTA":
 | 
			
		||||
            return CORE.address
 | 
			
		||||
    if (
 | 
			
		||||
        show_mqtt
 | 
			
		||||
        and (mqtt_config := CORE.config.get(CONF_MQTT))
 | 
			
		||||
        and mqtt_logging_enabled(mqtt_config)
 | 
			
		||||
    ):
 | 
			
		||||
        options.append((f"MQTT ({mqtt_config[CONF_BROKER]})", "MQTT"))
 | 
			
		||||
 | 
			
		||||
        if default == "OTA":
 | 
			
		||||
            return "MQTT"
 | 
			
		||||
    if default is not None:
 | 
			
		||||
        return default
 | 
			
		||||
    if check_default is not None and check_default in [opt[1] for opt in options]:
 | 
			
		||||
        return [check_default]
 | 
			
		||||
    return [choose_prompt(options, purpose=purpose)]
 | 
			
		||||
        return check_default
 | 
			
		||||
    return choose_prompt(options, purpose=purpose)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def mqtt_logging_enabled(mqtt_config):
 | 
			
		||||
@@ -166,14 +123,7 @@ def mqtt_logging_enabled(mqtt_config):
 | 
			
		||||
    return log_topic.get(CONF_LEVEL, None) != "NONE"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def has_mqtt_logging() -> bool:
 | 
			
		||||
    """Check if MQTT logging is available."""
 | 
			
		||||
    return (mqtt_config := CORE.config.get(CONF_MQTT)) and mqtt_logging_enabled(
 | 
			
		||||
        mqtt_config
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def get_port_type(port: str) -> str:
 | 
			
		||||
def get_port_type(port):
 | 
			
		||||
    if port.startswith("/") or port.startswith("COM"):
 | 
			
		||||
        return "SERIAL"
 | 
			
		||||
    if port == "MQTT":
 | 
			
		||||
@@ -181,7 +131,7 @@ def get_port_type(port: str) -> str:
 | 
			
		||||
    return "NETWORK"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_miniterm(config: ConfigType, port: str, args) -> int:
 | 
			
		||||
def run_miniterm(config, port, args):
 | 
			
		||||
    from aioesphomeapi import LogParser
 | 
			
		||||
    import serial
 | 
			
		||||
 | 
			
		||||
@@ -258,7 +208,7 @@ def wrap_to_code(name, comp):
 | 
			
		||||
    return wrapped
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def write_cpp(config: ConfigType) -> int:
 | 
			
		||||
def write_cpp(config):
 | 
			
		||||
    if not get_bool_env(ENV_NOGITIGNORE):
 | 
			
		||||
        writer.write_gitignore()
 | 
			
		||||
 | 
			
		||||
@@ -266,7 +216,7 @@ def write_cpp(config: ConfigType) -> int:
 | 
			
		||||
    return write_cpp_file()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def generate_cpp_contents(config: ConfigType) -> None:
 | 
			
		||||
def generate_cpp_contents(config):
 | 
			
		||||
    _LOGGER.info("Generating C++ source...")
 | 
			
		||||
 | 
			
		||||
    for name, component, conf in iter_component_configs(CORE.config):
 | 
			
		||||
@@ -277,7 +227,7 @@ def generate_cpp_contents(config: ConfigType) -> None:
 | 
			
		||||
    CORE.flush_tasks()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def write_cpp_file() -> int:
 | 
			
		||||
def write_cpp_file():
 | 
			
		||||
    code_s = indent(CORE.cpp_main_section)
 | 
			
		||||
    writer.write_cpp(code_s)
 | 
			
		||||
 | 
			
		||||
@@ -288,7 +238,7 @@ def write_cpp_file() -> int:
 | 
			
		||||
    return 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
 | 
			
		||||
def compile_program(args, config):
 | 
			
		||||
    from esphome import platformio_api
 | 
			
		||||
 | 
			
		||||
    _LOGGER.info("Compiling app...")
 | 
			
		||||
@@ -299,9 +249,7 @@ def compile_program(args: ArgsProtocol, config: ConfigType) -> int:
 | 
			
		||||
    return 0 if idedata is not None else 1
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upload_using_esptool(
 | 
			
		||||
    config: ConfigType, port: str, file: str, speed: int
 | 
			
		||||
) -> str | int:
 | 
			
		||||
def upload_using_esptool(config, port, file, speed):
 | 
			
		||||
    from esphome import platformio_api
 | 
			
		||||
 | 
			
		||||
    first_baudrate = speed or config[CONF_ESPHOME][CONF_PLATFORMIO_OPTIONS].get(
 | 
			
		||||
@@ -366,7 +314,7 @@ def upload_using_esptool(
 | 
			
		||||
    return run_esptool(115200)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upload_using_platformio(config: ConfigType, port: str):
 | 
			
		||||
def upload_using_platformio(config, port):
 | 
			
		||||
    from esphome import platformio_api
 | 
			
		||||
 | 
			
		||||
    upload_args = ["-t", "upload", "-t", "nobuild"]
 | 
			
		||||
@@ -375,7 +323,7 @@ def upload_using_platformio(config: ConfigType, port: str):
 | 
			
		||||
    return platformio_api.run_platformio_cli_run(config, CORE.verbose, *upload_args)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def check_permissions(port: str):
 | 
			
		||||
def check_permissions(port):
 | 
			
		||||
    if os.name == "posix" and get_port_type(port) == "SERIAL":
 | 
			
		||||
        # Check if we can open selected serial port
 | 
			
		||||
        if not os.access(port, os.F_OK):
 | 
			
		||||
@@ -393,7 +341,7 @@ def check_permissions(port: str):
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | str:
 | 
			
		||||
def upload_program(config, args, host):
 | 
			
		||||
    try:
 | 
			
		||||
        module = importlib.import_module("esphome.components." + CORE.target_platform)
 | 
			
		||||
        if getattr(module, "upload_program")(config, args, host):
 | 
			
		||||
@@ -408,7 +356,7 @@ def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | s
 | 
			
		||||
            return upload_using_esptool(config, host, file, args.upload_speed)
 | 
			
		||||
 | 
			
		||||
        if CORE.target_platform in (PLATFORM_RP2040):
 | 
			
		||||
            return upload_using_platformio(config, host)
 | 
			
		||||
            return upload_using_platformio(config, args.device)
 | 
			
		||||
 | 
			
		||||
        if CORE.is_libretiny:
 | 
			
		||||
            return upload_using_platformio(config, host)
 | 
			
		||||
@@ -431,12 +379,9 @@ def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | s
 | 
			
		||||
    remote_port = int(ota_conf[CONF_PORT])
 | 
			
		||||
    password = ota_conf.get(CONF_PASSWORD, "")
 | 
			
		||||
 | 
			
		||||
    # Check if we should use MQTT for address resolution
 | 
			
		||||
    # This happens when no device was specified, or the current host is "MQTT"/"OTA"
 | 
			
		||||
    devices: list[str] = args.device or []
 | 
			
		||||
    if (
 | 
			
		||||
        CONF_MQTT in config  # pylint: disable=too-many-boolean-expressions
 | 
			
		||||
        and (not devices or host in ("MQTT", "OTA"))
 | 
			
		||||
        and (not args.device or args.device in ("MQTT", "OTA"))
 | 
			
		||||
        and (
 | 
			
		||||
            ((config[CONF_MDNS][CONF_DISABLED]) and not is_ip_address(CORE.address))
 | 
			
		||||
            or get_port_type(host) == "MQTT"
 | 
			
		||||
@@ -454,28 +399,23 @@ def upload_program(config: ConfigType, args: ArgsProtocol, host: str) -> int | s
 | 
			
		||||
    return espota2.run_ota(host, remote_port, password, CORE.firmware_bin)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int | None:
 | 
			
		||||
def show_logs(config, args, port):
 | 
			
		||||
    if "logger" not in config:
 | 
			
		||||
        raise EsphomeError("Logger is not configured!")
 | 
			
		||||
 | 
			
		||||
    port = devices[0]
 | 
			
		||||
 | 
			
		||||
    if get_port_type(port) == "SERIAL":
 | 
			
		||||
        check_permissions(port)
 | 
			
		||||
        return run_miniterm(config, port, args)
 | 
			
		||||
    if get_port_type(port) == "NETWORK" and "api" in config:
 | 
			
		||||
        addresses_to_use = devices
 | 
			
		||||
        if config[CONF_MDNS][CONF_DISABLED] and CONF_MQTT in config:
 | 
			
		||||
            from esphome import mqtt
 | 
			
		||||
 | 
			
		||||
            mqtt_address = mqtt.get_esphome_device_ip(
 | 
			
		||||
            port = mqtt.get_esphome_device_ip(
 | 
			
		||||
                config, args.username, args.password, args.client_id
 | 
			
		||||
            )[0]
 | 
			
		||||
            addresses_to_use = [mqtt_address]
 | 
			
		||||
 | 
			
		||||
        from esphome.components.api.client import run_logs
 | 
			
		||||
 | 
			
		||||
        return run_logs(config, addresses_to_use)
 | 
			
		||||
        return run_logs(config, port)
 | 
			
		||||
    if get_port_type(port) == "MQTT" and "mqtt" in config:
 | 
			
		||||
        from esphome import mqtt
 | 
			
		||||
 | 
			
		||||
@@ -486,7 +426,7 @@ def show_logs(config: ConfigType, args: ArgsProtocol, devices: list[str]) -> int
 | 
			
		||||
    raise EsphomeError("No remote or local logging method configured (api/mqtt/logger)")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def clean_mqtt(config: ConfigType, args: ArgsProtocol) -> int | None:
 | 
			
		||||
def clean_mqtt(config, args):
 | 
			
		||||
    from esphome import mqtt
 | 
			
		||||
 | 
			
		||||
    return mqtt.clear_topic(
 | 
			
		||||
@@ -494,13 +434,13 @@ def clean_mqtt(config: ConfigType, args: ArgsProtocol) -> int | None:
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_wizard(args: ArgsProtocol) -> int | None:
 | 
			
		||||
def command_wizard(args):
 | 
			
		||||
    from esphome import wizard
 | 
			
		||||
 | 
			
		||||
    return wizard.wizard(args.configuration)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
def command_config(args, config):
 | 
			
		||||
    if not CORE.verbose:
 | 
			
		||||
        config = strip_default_ids(config)
 | 
			
		||||
    output = yaml_util.dump(config, args.show_secrets)
 | 
			
		||||
@@ -515,7 +455,7 @@ def command_config(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
    return 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_vscode(args: ArgsProtocol) -> int | None:
 | 
			
		||||
def command_vscode(args):
 | 
			
		||||
    from esphome import vscode
 | 
			
		||||
 | 
			
		||||
    logging.disable(logging.INFO)
 | 
			
		||||
@@ -523,7 +463,7 @@ def command_vscode(args: ArgsProtocol) -> int | None:
 | 
			
		||||
    vscode.read_config(args)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
def command_compile(args, config):
 | 
			
		||||
    exit_code = write_cpp(config)
 | 
			
		||||
    if exit_code != 0:
 | 
			
		||||
        return exit_code
 | 
			
		||||
@@ -537,9 +477,8 @@ def command_compile(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
    return 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
    # Get devices, resolving special identifiers like OTA
 | 
			
		||||
    devices = choose_upload_log_host(
 | 
			
		||||
def command_upload(args, config):
 | 
			
		||||
    port = choose_upload_log_host(
 | 
			
		||||
        default=args.device,
 | 
			
		||||
        check_default=None,
 | 
			
		||||
        show_ota=True,
 | 
			
		||||
@@ -547,22 +486,14 @@ def command_upload(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
        show_api=False,
 | 
			
		||||
        purpose="uploading",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # Try each device until one succeeds
 | 
			
		||||
    exit_code = 1
 | 
			
		||||
    for device in devices:
 | 
			
		||||
        _LOGGER.info("Uploading to %s", device)
 | 
			
		||||
        exit_code = upload_program(config, args, device)
 | 
			
		||||
        if exit_code == 0:
 | 
			
		||||
            _LOGGER.info("Successfully uploaded program.")
 | 
			
		||||
            return 0
 | 
			
		||||
        if len(devices) > 1:
 | 
			
		||||
            _LOGGER.warning("Failed to upload to %s", device)
 | 
			
		||||
 | 
			
		||||
    return exit_code
 | 
			
		||||
    exit_code = upload_program(config, args, port)
 | 
			
		||||
    if exit_code != 0:
 | 
			
		||||
        return exit_code
 | 
			
		||||
    _LOGGER.info("Successfully uploaded program.")
 | 
			
		||||
    return 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_discover(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
def command_discover(args, config):
 | 
			
		||||
    if "mqtt" in config:
 | 
			
		||||
        from esphome import mqtt
 | 
			
		||||
 | 
			
		||||
@@ -571,9 +502,8 @@ def command_discover(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
    raise EsphomeError("No discover method configured (mqtt)")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
    # Get devices, resolving special identifiers like OTA
 | 
			
		||||
    devices = choose_upload_log_host(
 | 
			
		||||
def command_logs(args, config):
 | 
			
		||||
    port = choose_upload_log_host(
 | 
			
		||||
        default=args.device,
 | 
			
		||||
        check_default=None,
 | 
			
		||||
        show_ota=False,
 | 
			
		||||
@@ -581,10 +511,10 @@ def command_logs(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
        show_api=True,
 | 
			
		||||
        purpose="logging",
 | 
			
		||||
    )
 | 
			
		||||
    return show_logs(config, args, devices)
 | 
			
		||||
    return show_logs(config, args, port)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
def command_run(args, config):
 | 
			
		||||
    exit_code = write_cpp(config)
 | 
			
		||||
    if exit_code != 0:
 | 
			
		||||
        return exit_code
 | 
			
		||||
@@ -601,8 +531,7 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
        program_path = idedata.raw["prog_path"]
 | 
			
		||||
        return run_external_process(program_path)
 | 
			
		||||
 | 
			
		||||
    # Get devices, resolving special identifiers like OTA
 | 
			
		||||
    devices = choose_upload_log_host(
 | 
			
		||||
    port = choose_upload_log_host(
 | 
			
		||||
        default=args.device,
 | 
			
		||||
        check_default=None,
 | 
			
		||||
        show_ota=True,
 | 
			
		||||
@@ -610,53 +539,39 @@ def command_run(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
        show_api=True,
 | 
			
		||||
        purpose="uploading",
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    # Try each device for upload until one succeeds
 | 
			
		||||
    successful_device: str | None = None
 | 
			
		||||
    for device in devices:
 | 
			
		||||
        _LOGGER.info("Uploading to %s", device)
 | 
			
		||||
        exit_code = upload_program(config, args, device)
 | 
			
		||||
        if exit_code == 0:
 | 
			
		||||
            _LOGGER.info("Successfully uploaded program.")
 | 
			
		||||
            successful_device = device
 | 
			
		||||
            break
 | 
			
		||||
        if len(devices) > 1:
 | 
			
		||||
            _LOGGER.warning("Failed to upload to %s", device)
 | 
			
		||||
 | 
			
		||||
    if successful_device is None:
 | 
			
		||||
    exit_code = upload_program(config, args, port)
 | 
			
		||||
    if exit_code != 0:
 | 
			
		||||
        return exit_code
 | 
			
		||||
 | 
			
		||||
    _LOGGER.info("Successfully uploaded program.")
 | 
			
		||||
    if args.no_logs:
 | 
			
		||||
        return 0
 | 
			
		||||
 | 
			
		||||
    # For logs, prefer the device we successfully uploaded to
 | 
			
		||||
    devices = choose_upload_log_host(
 | 
			
		||||
        default=successful_device,
 | 
			
		||||
        check_default=successful_device,
 | 
			
		||||
    port = choose_upload_log_host(
 | 
			
		||||
        default=args.device,
 | 
			
		||||
        check_default=port,
 | 
			
		||||
        show_ota=False,
 | 
			
		||||
        show_mqtt=True,
 | 
			
		||||
        show_api=True,
 | 
			
		||||
        purpose="logging",
 | 
			
		||||
    )
 | 
			
		||||
    return show_logs(config, args, devices)
 | 
			
		||||
    return show_logs(config, args, port)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_clean_mqtt(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
def command_clean_mqtt(args, config):
 | 
			
		||||
    return clean_mqtt(config, args)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_mqtt_fingerprint(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
def command_mqtt_fingerprint(args, config):
 | 
			
		||||
    from esphome import mqtt
 | 
			
		||||
 | 
			
		||||
    return mqtt.get_fingerprint(config)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_version(args: ArgsProtocol) -> int | None:
 | 
			
		||||
def command_version(args):
 | 
			
		||||
    safe_print(f"Version: {const.__version__}")
 | 
			
		||||
    return 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
def command_clean(args, config):
 | 
			
		||||
    try:
 | 
			
		||||
        writer.clean_build()
 | 
			
		||||
    except OSError as err:
 | 
			
		||||
@@ -666,13 +581,13 @@ def command_clean(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
    return 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_dashboard(args: ArgsProtocol) -> int | None:
 | 
			
		||||
def command_dashboard(args):
 | 
			
		||||
    from esphome.dashboard import dashboard
 | 
			
		||||
 | 
			
		||||
    return dashboard.start_dashboard(args)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_update_all(args: ArgsProtocol) -> int | None:
 | 
			
		||||
def command_update_all(args):
 | 
			
		||||
    import click
 | 
			
		||||
 | 
			
		||||
    success = {}
 | 
			
		||||
@@ -719,7 +634,7 @@ def command_update_all(args: ArgsProtocol) -> int | None:
 | 
			
		||||
    return failed
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
 | 
			
		||||
def command_idedata(args, config):
 | 
			
		||||
    import json
 | 
			
		||||
 | 
			
		||||
    from esphome import platformio_api
 | 
			
		||||
@@ -735,7 +650,7 @@ def command_idedata(args: ArgsProtocol, config: ConfigType) -> int:
 | 
			
		||||
    return 0
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def command_rename(args: ArgsProtocol, config: ConfigType) -> int | None:
 | 
			
		||||
def command_rename(args, config):
 | 
			
		||||
    for c in args.name:
 | 
			
		||||
        if c not in ALLOWED_NAME_CHARS:
 | 
			
		||||
            print(
 | 
			
		||||
@@ -852,12 +767,6 @@ POST_CONFIG_ACTIONS = {
 | 
			
		||||
    "discover": command_discover,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SIMPLE_CONFIG_ACTIONS = [
 | 
			
		||||
    "clean",
 | 
			
		||||
    "clean-mqtt",
 | 
			
		||||
    "config",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def parse_args(argv):
 | 
			
		||||
    options_parser = argparse.ArgumentParser(add_help=False)
 | 
			
		||||
@@ -945,8 +854,7 @@ def parse_args(argv):
 | 
			
		||||
    )
 | 
			
		||||
    parser_upload.add_argument(
 | 
			
		||||
        "--device",
 | 
			
		||||
        action="append",
 | 
			
		||||
        help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
 | 
			
		||||
        help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
 | 
			
		||||
    )
 | 
			
		||||
    parser_upload.add_argument(
 | 
			
		||||
        "--upload_speed",
 | 
			
		||||
@@ -968,8 +876,7 @@ def parse_args(argv):
 | 
			
		||||
    )
 | 
			
		||||
    parser_logs.add_argument(
 | 
			
		||||
        "--device",
 | 
			
		||||
        action="append",
 | 
			
		||||
        help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
 | 
			
		||||
        help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
 | 
			
		||||
    )
 | 
			
		||||
    parser_logs.add_argument(
 | 
			
		||||
        "--reset",
 | 
			
		||||
@@ -998,8 +905,7 @@ def parse_args(argv):
 | 
			
		||||
    )
 | 
			
		||||
    parser_run.add_argument(
 | 
			
		||||
        "--device",
 | 
			
		||||
        action="append",
 | 
			
		||||
        help="Manually specify the serial port/address to use, for example /dev/ttyUSB0. Can be specified multiple times for fallback addresses.",
 | 
			
		||||
        help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.",
 | 
			
		||||
    )
 | 
			
		||||
    parser_run.add_argument(
 | 
			
		||||
        "--upload_speed",
 | 
			
		||||
@@ -1126,13 +1032,6 @@ def parse_args(argv):
 | 
			
		||||
    arguments = argv[1:]
 | 
			
		||||
 | 
			
		||||
    argcomplete.autocomplete(parser)
 | 
			
		||||
 | 
			
		||||
    if len(arguments) > 0 and arguments[0] in SIMPLE_CONFIG_ACTIONS:
 | 
			
		||||
        args, unknown_args = parser.parse_known_args(arguments)
 | 
			
		||||
        if unknown_args:
 | 
			
		||||
            _LOGGER.warning("Ignored unrecognized arguments: %s", unknown_args)
 | 
			
		||||
        return args
 | 
			
		||||
 | 
			
		||||
    return parser.parse_args(arguments)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -5,7 +5,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_EQUATION,
 | 
			
		||||
    CONF_HUMIDITY,
 | 
			
		||||
    CONF_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_ABSOLUTE_HUMIDITY,
 | 
			
		||||
    ICON_WATER,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_GRAMS_PER_CUBIC_METER,
 | 
			
		||||
)
 | 
			
		||||
@@ -27,8 +27,8 @@ EQUATION = {
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    sensor.sensor_schema(
 | 
			
		||||
        unit_of_measurement=UNIT_GRAMS_PER_CUBIC_METER,
 | 
			
		||||
        icon=ICON_WATER,
 | 
			
		||||
        accuracy_decimals=2,
 | 
			
		||||
        device_class=DEVICE_CLASS_ABSOLUTE_HUMIDITY,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    )
 | 
			
		||||
    .extend(
 | 
			
		||||
 
 | 
			
		||||
@@ -36,7 +36,6 @@ from esphome.const import (
 | 
			
		||||
    UNIT_WATT,
 | 
			
		||||
    UNIT_WATT_HOURS,
 | 
			
		||||
)
 | 
			
		||||
from esphome.types import ConfigType
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
 | 
			
		||||
@@ -52,20 +51,6 @@ CONF_POWER_GAIN = "power_gain"
 | 
			
		||||
 | 
			
		||||
CONF_NEUTRAL = "neutral"
 | 
			
		||||
 | 
			
		||||
# Tuple of power channel phases
 | 
			
		||||
POWER_PHASES = (CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C)
 | 
			
		||||
 | 
			
		||||
# Tuple of sensor types that can be configured for power channels
 | 
			
		||||
POWER_SENSOR_TYPES = (
 | 
			
		||||
    CONF_CURRENT,
 | 
			
		||||
    CONF_VOLTAGE,
 | 
			
		||||
    CONF_ACTIVE_POWER,
 | 
			
		||||
    CONF_APPARENT_POWER,
 | 
			
		||||
    CONF_POWER_FACTOR,
 | 
			
		||||
    CONF_FORWARD_ACTIVE_ENERGY,
 | 
			
		||||
    CONF_REVERSE_ACTIVE_ENERGY,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
NEUTRAL_CHANNEL_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(NeutralChannel),
 | 
			
		||||
@@ -165,64 +150,7 @@ POWER_CHANNEL_SCHEMA = cv.Schema(
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def prefix_sensor_name(
 | 
			
		||||
    sensor_conf: ConfigType,
 | 
			
		||||
    channel_name: str,
 | 
			
		||||
    channel_config: ConfigType,
 | 
			
		||||
    sensor_type: str,
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Helper to prefix sensor name with channel name.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        sensor_conf: The sensor configuration (dict or string)
 | 
			
		||||
        channel_name: The channel name to prefix with
 | 
			
		||||
        channel_config: The channel configuration to update
 | 
			
		||||
        sensor_type: The sensor type key in the channel config
 | 
			
		||||
    """
 | 
			
		||||
    if isinstance(sensor_conf, dict) and CONF_NAME in sensor_conf:
 | 
			
		||||
        sensor_name = sensor_conf[CONF_NAME]
 | 
			
		||||
        if sensor_name and not sensor_name.startswith(channel_name):
 | 
			
		||||
            sensor_conf[CONF_NAME] = f"{channel_name} {sensor_name}"
 | 
			
		||||
    elif isinstance(sensor_conf, str):
 | 
			
		||||
        # Simple value case - convert to dict with prefixed name
 | 
			
		||||
        channel_config[sensor_type] = {CONF_NAME: f"{channel_name} {sensor_conf}"}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def process_channel_sensors(
 | 
			
		||||
    config: ConfigType, channel_key: str, sensor_types: tuple
 | 
			
		||||
) -> None:
 | 
			
		||||
    """Process sensors for a channel and prefix their names.
 | 
			
		||||
 | 
			
		||||
    Args:
 | 
			
		||||
        config: The main configuration
 | 
			
		||||
        channel_key: The channel key (e.g., CONF_PHASE_A, CONF_NEUTRAL)
 | 
			
		||||
        sensor_types: Tuple of sensor types to process for this channel
 | 
			
		||||
    """
 | 
			
		||||
    if not (channel_config := config.get(channel_key)) or not (
 | 
			
		||||
        channel_name := channel_config.get(CONF_NAME)
 | 
			
		||||
    ):
 | 
			
		||||
        return
 | 
			
		||||
 | 
			
		||||
    for sensor_type in sensor_types:
 | 
			
		||||
        if sensor_conf := channel_config.get(sensor_type):
 | 
			
		||||
            prefix_sensor_name(sensor_conf, channel_name, channel_config, sensor_type)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def preprocess_channels(config: ConfigType) -> ConfigType:
 | 
			
		||||
    """Preprocess channel configurations to add channel name prefix to sensor names."""
 | 
			
		||||
    # Process power channels
 | 
			
		||||
    for channel in POWER_PHASES:
 | 
			
		||||
        process_channel_sensors(config, channel, POWER_SENSOR_TYPES)
 | 
			
		||||
 | 
			
		||||
    # Process neutral channel
 | 
			
		||||
    process_channel_sensors(config, CONF_NEUTRAL, (CONF_CURRENT,))
 | 
			
		||||
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    preprocess_channels,
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(ADE7880),
 | 
			
		||||
@@ -239,7 +167,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
    .extend(i2c.i2c_device_schema(0x38)),
 | 
			
		||||
    .extend(i2c.i2c_device_schema(0x38))
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -260,7 +188,15 @@ async def neutral_channel(config):
 | 
			
		||||
async def power_channel(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
 | 
			
		||||
    for sensor_type in POWER_SENSOR_TYPES:
 | 
			
		||||
    for sensor_type in [
 | 
			
		||||
        CONF_CURRENT,
 | 
			
		||||
        CONF_VOLTAGE,
 | 
			
		||||
        CONF_ACTIVE_POWER,
 | 
			
		||||
        CONF_APPARENT_POWER,
 | 
			
		||||
        CONF_POWER_FACTOR,
 | 
			
		||||
        CONF_FORWARD_ACTIVE_ENERGY,
 | 
			
		||||
        CONF_REVERSE_ACTIVE_ENERGY,
 | 
			
		||||
    ]:
 | 
			
		||||
        if conf := config.get(sensor_type):
 | 
			
		||||
            sens = await sensor.new_sensor(conf)
 | 
			
		||||
            cg.add(getattr(var, f"set_{sensor_type}")(sens))
 | 
			
		||||
@@ -280,6 +216,44 @@ async def power_channel(config):
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def final_validate(config):
 | 
			
		||||
    for channel in [CONF_PHASE_A, CONF_PHASE_B, CONF_PHASE_C]:
 | 
			
		||||
        if channel := config.get(channel):
 | 
			
		||||
            channel_name = channel.get(CONF_NAME)
 | 
			
		||||
 | 
			
		||||
            for sensor_type in [
 | 
			
		||||
                CONF_CURRENT,
 | 
			
		||||
                CONF_VOLTAGE,
 | 
			
		||||
                CONF_ACTIVE_POWER,
 | 
			
		||||
                CONF_APPARENT_POWER,
 | 
			
		||||
                CONF_POWER_FACTOR,
 | 
			
		||||
                CONF_FORWARD_ACTIVE_ENERGY,
 | 
			
		||||
                CONF_REVERSE_ACTIVE_ENERGY,
 | 
			
		||||
            ]:
 | 
			
		||||
                if conf := channel.get(sensor_type):
 | 
			
		||||
                    sensor_name = conf.get(CONF_NAME)
 | 
			
		||||
                    if (
 | 
			
		||||
                        sensor_name
 | 
			
		||||
                        and channel_name
 | 
			
		||||
                        and not sensor_name.startswith(channel_name)
 | 
			
		||||
                    ):
 | 
			
		||||
                        conf[CONF_NAME] = f"{channel_name} {sensor_name}"
 | 
			
		||||
 | 
			
		||||
    if channel := config.get(CONF_NEUTRAL):
 | 
			
		||||
        channel_name = channel.get(CONF_NAME)
 | 
			
		||||
        if conf := channel.get(CONF_CURRENT):
 | 
			
		||||
            sensor_name = conf.get(CONF_NAME)
 | 
			
		||||
            if (
 | 
			
		||||
                sensor_name
 | 
			
		||||
                and channel_name
 | 
			
		||||
                and not sensor_name.startswith(channel_name)
 | 
			
		||||
            ):
 | 
			
		||||
                conf[CONF_NAME] = f"{channel_name} {sensor_name}"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = final_validate
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 
 | 
			
		||||
@@ -29,7 +29,7 @@ from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
DOMAIN = "api"
 | 
			
		||||
DEPENDENCIES = ["network"]
 | 
			
		||||
AUTO_LOAD = ["socket"]
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
CODEOWNERS = ["@OttoWinter"]
 | 
			
		||||
 | 
			
		||||
api_ns = cg.esphome_ns.namespace("api")
 | 
			
		||||
APIServer = api_ns.class_("APIServer", cg.Component, cg.Controller)
 | 
			
		||||
 
 | 
			
		||||
@@ -1438,11 +1438,11 @@ message BluetoothLERawAdvertisementsResponse {
 | 
			
		||||
  option (ifdef) = "USE_BLUETOOTH_PROXY";
 | 
			
		||||
  option (no_delay) = true;
 | 
			
		||||
 | 
			
		||||
  repeated BluetoothLERawAdvertisement advertisements = 1 [(fixed_array_with_length_define) = "BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE"];
 | 
			
		||||
  repeated BluetoothLERawAdvertisement advertisements = 1;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
enum BluetoothDeviceRequestType {
 | 
			
		||||
  BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0 [deprecated = true]; // V1 removed, use V3 variants
 | 
			
		||||
  BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0;
 | 
			
		||||
  BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1;
 | 
			
		||||
  BLUETOOTH_DEVICE_REQUEST_TYPE_PAIR = 2;
 | 
			
		||||
  BLUETOOTH_DEVICE_REQUEST_TYPE_UNPAIR = 3;
 | 
			
		||||
 
 | 
			
		||||
@@ -156,9 +156,7 @@ APIError APIFrameHelper::write_raw_(const struct iovec *iov, int iovcnt, uint16_
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Try to send directly if no buffered data
 | 
			
		||||
  // Optimize for single iovec case (common for plaintext API)
 | 
			
		||||
  ssize_t sent =
 | 
			
		||||
      (iovcnt == 1) ? this->socket_->write(iov[0].iov_base, iov[0].iov_len) : this->socket_->writev(iov, iovcnt);
 | 
			
		||||
  ssize_t sent = this->socket_->writev(iov, iovcnt);
 | 
			
		||||
 | 
			
		||||
  if (sent == -1) {
 | 
			
		||||
    APIError err = this->handle_socket_write_error_();
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,6 @@ extend google.protobuf.FieldOptions {
 | 
			
		||||
    optional bool no_zero_copy = 50008 [default=false];
 | 
			
		||||
    optional bool fixed_array_skip_zero = 50009 [default=false];
 | 
			
		||||
    optional string fixed_array_size_define = 50010;
 | 
			
		||||
    optional string fixed_array_with_length_define = 50011;
 | 
			
		||||
 | 
			
		||||
    // container_pointer: Zero-copy optimization for repeated fields.
 | 
			
		||||
    //
 | 
			
		||||
 
 | 
			
		||||
@@ -1843,14 +1843,12 @@ void BluetoothLERawAdvertisement::calculate_size(ProtoSize &size) const {
 | 
			
		||||
  size.add_length(1, this->data_len);
 | 
			
		||||
}
 | 
			
		||||
void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const {
 | 
			
		||||
  for (uint16_t i = 0; i < this->advertisements_len; i++) {
 | 
			
		||||
    buffer.encode_message(1, this->advertisements[i], true);
 | 
			
		||||
  for (auto &it : this->advertisements) {
 | 
			
		||||
    buffer.encode_message(1, it, true);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void BluetoothLERawAdvertisementsResponse::calculate_size(ProtoSize &size) const {
 | 
			
		||||
  for (uint16_t i = 0; i < this->advertisements_len; i++) {
 | 
			
		||||
    size.add_message_object_force(1, this->advertisements[i]);
 | 
			
		||||
  }
 | 
			
		||||
  size.add_repeated_message(1, this->advertisements);
 | 
			
		||||
}
 | 
			
		||||
bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
 | 
			
		||||
  switch (field_id) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1788,12 +1788,11 @@ class BluetoothLERawAdvertisement : public ProtoMessage {
 | 
			
		||||
class BluetoothLERawAdvertisementsResponse : public ProtoMessage {
 | 
			
		||||
 public:
 | 
			
		||||
  static constexpr uint8_t MESSAGE_TYPE = 93;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 136;
 | 
			
		||||
  static constexpr uint8_t ESTIMATED_SIZE = 34;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
  const char *message_name() const override { return "bluetooth_le_raw_advertisements_response"; }
 | 
			
		||||
#endif
 | 
			
		||||
  std::array<BluetoothLERawAdvertisement, BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE> advertisements{};
 | 
			
		||||
  uint16_t advertisements_len{0};
 | 
			
		||||
  std::vector<BluetoothLERawAdvertisement> advertisements{};
 | 
			
		||||
  void encode(ProtoWriteBuffer buffer) const override;
 | 
			
		||||
  void calculate_size(ProtoSize &size) const override;
 | 
			
		||||
#ifdef HAS_PROTO_MESSAGE_DUMP
 | 
			
		||||
 
 | 
			
		||||
@@ -1534,9 +1534,9 @@ void BluetoothLERawAdvertisement::dump_to(std::string &out) const {
 | 
			
		||||
}
 | 
			
		||||
void BluetoothLERawAdvertisementsResponse::dump_to(std::string &out) const {
 | 
			
		||||
  MessageDumpHelper helper(out, "BluetoothLERawAdvertisementsResponse");
 | 
			
		||||
  for (uint16_t i = 0; i < this->advertisements_len; i++) {
 | 
			
		||||
  for (const auto &it : this->advertisements) {
 | 
			
		||||
    out.append("  advertisements: ");
 | 
			
		||||
    this->advertisements[i].dump_to(out);
 | 
			
		||||
    it.dump_to(out);
 | 
			
		||||
    out.append("\n");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -30,7 +30,7 @@ if TYPE_CHECKING:
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
 | 
			
		||||
async def async_run_logs(config: dict[str, Any], address: str) -> None:
 | 
			
		||||
    """Run the logs command in the event loop."""
 | 
			
		||||
    conf = config["api"]
 | 
			
		||||
    name = config["esphome"]["name"]
 | 
			
		||||
@@ -39,21 +39,13 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
 | 
			
		||||
    noise_psk: str | None = None
 | 
			
		||||
    if (encryption := conf.get(CONF_ENCRYPTION)) and (key := encryption.get(CONF_KEY)):
 | 
			
		||||
        noise_psk = key
 | 
			
		||||
 | 
			
		||||
    if len(addresses) == 1:
 | 
			
		||||
        _LOGGER.info("Starting log output from %s using esphome API", addresses[0])
 | 
			
		||||
    else:
 | 
			
		||||
        _LOGGER.info(
 | 
			
		||||
            "Starting log output from %s using esphome API", " or ".join(addresses)
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    _LOGGER.info("Starting log output from %s using esphome API", address)
 | 
			
		||||
    cli = APIClient(
 | 
			
		||||
        addresses[0],  # Primary address for compatibility
 | 
			
		||||
        address,
 | 
			
		||||
        port,
 | 
			
		||||
        password,
 | 
			
		||||
        client_info=f"ESPHome Logs {__version__}",
 | 
			
		||||
        noise_psk=noise_psk,
 | 
			
		||||
        addresses=addresses,  # Pass all addresses for automatic retry
 | 
			
		||||
    )
 | 
			
		||||
    dashboard = CORE.dashboard
 | 
			
		||||
 | 
			
		||||
@@ -74,7 +66,7 @@ async def async_run_logs(config: dict[str, Any], addresses: list[str]) -> None:
 | 
			
		||||
        await stop()
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def run_logs(config: dict[str, Any], addresses: list[str]) -> None:
 | 
			
		||||
def run_logs(config: dict[str, Any], address: str) -> None:
 | 
			
		||||
    """Run the logs command."""
 | 
			
		||||
    with contextlib.suppress(KeyboardInterrupt):
 | 
			
		||||
        asyncio.run(async_run_logs(config, addresses))
 | 
			
		||||
        asyncio.run(async_run_logs(config, address))
 | 
			
		||||
 
 | 
			
		||||
@@ -56,14 +56,6 @@ class CustomAPIDevice {
 | 
			
		||||
    auto *service = new CustomAPIDeviceService<T, Ts...>(name, arg_names, (T *) this, callback);  // NOLINT
 | 
			
		||||
    global_api_server->register_user_service(service);
 | 
			
		||||
  }
 | 
			
		||||
#else
 | 
			
		||||
  template<typename T, typename... Ts>
 | 
			
		||||
  void register_service(void (T::*callback)(Ts...), const std::string &name,
 | 
			
		||||
                        const std::array<std::string, sizeof...(Ts)> &arg_names) {
 | 
			
		||||
    static_assert(
 | 
			
		||||
        sizeof(T) == 0,
 | 
			
		||||
        "register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  /** Register a custom native API service that will show up in Home Assistant.
 | 
			
		||||
@@ -89,12 +81,6 @@ class CustomAPIDevice {
 | 
			
		||||
    auto *service = new CustomAPIDeviceService<T>(name, {}, (T *) this, callback);  // NOLINT
 | 
			
		||||
    global_api_server->register_user_service(service);
 | 
			
		||||
  }
 | 
			
		||||
#else
 | 
			
		||||
  template<typename T> void register_service(void (T::*callback)(), const std::string &name) {
 | 
			
		||||
    static_assert(
 | 
			
		||||
        sizeof(T) == 0,
 | 
			
		||||
        "register_service() requires 'custom_services: true' in the 'api:' section of your YAML configuration");
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_STATES
 | 
			
		||||
@@ -149,22 +135,6 @@ class CustomAPIDevice {
 | 
			
		||||
    auto f = std::bind(callback, (T *) this, entity_id, std::placeholders::_1);
 | 
			
		||||
    global_api_server->subscribe_home_assistant_state(entity_id, optional<std::string>(attribute), f);
 | 
			
		||||
  }
 | 
			
		||||
#else
 | 
			
		||||
  template<typename T>
 | 
			
		||||
  void subscribe_homeassistant_state(void (T::*callback)(std::string), const std::string &entity_id,
 | 
			
		||||
                                     const std::string &attribute = "") {
 | 
			
		||||
    static_assert(sizeof(T) == 0,
 | 
			
		||||
                  "subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
 | 
			
		||||
                  "of your YAML configuration");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  template<typename T>
 | 
			
		||||
  void subscribe_homeassistant_state(void (T::*callback)(std::string, std::string), const std::string &entity_id,
 | 
			
		||||
                                     const std::string &attribute = "") {
 | 
			
		||||
    static_assert(sizeof(T) == 0,
 | 
			
		||||
                  "subscribe_homeassistant_state() requires 'homeassistant_states: true' in the 'api:' section "
 | 
			
		||||
                  "of your YAML configuration");
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_API_HOMEASSISTANT_SERVICES
 | 
			
		||||
@@ -252,28 +222,6 @@ class CustomAPIDevice {
 | 
			
		||||
    }
 | 
			
		||||
    global_api_server->send_homeassistant_service_call(resp);
 | 
			
		||||
  }
 | 
			
		||||
#else
 | 
			
		||||
  template<typename T = void> void call_homeassistant_service(const std::string &service_name) {
 | 
			
		||||
    static_assert(sizeof(T) == 0, "call_homeassistant_service() requires 'homeassistant_services: true' in the 'api:' "
 | 
			
		||||
                                  "section of your YAML configuration");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  template<typename T = void>
 | 
			
		||||
  void call_homeassistant_service(const std::string &service_name, const std::map<std::string, std::string> &data) {
 | 
			
		||||
    static_assert(sizeof(T) == 0, "call_homeassistant_service() requires 'homeassistant_services: true' in the 'api:' "
 | 
			
		||||
                                  "section of your YAML configuration");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  template<typename T = void> void fire_homeassistant_event(const std::string &event_name) {
 | 
			
		||||
    static_assert(sizeof(T) == 0, "fire_homeassistant_event() requires 'homeassistant_services: true' in the 'api:' "
 | 
			
		||||
                                  "section of your YAML configuration");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  template<typename T = void>
 | 
			
		||||
  void fire_homeassistant_event(const std::string &service_name, const std::map<std::string, std::string> &data) {
 | 
			
		||||
    static_assert(sizeof(T) == 0, "fire_homeassistant_event() requires 'homeassistant_services: true' in the 'api:' "
 | 
			
		||||
                                  "section of your YAML configuration");
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,23 +15,6 @@
 | 
			
		||||
 | 
			
		||||
namespace esphome::api {
 | 
			
		||||
 | 
			
		||||
// Helper functions for ZigZag encoding/decoding
 | 
			
		||||
inline constexpr uint32_t encode_zigzag32(int32_t value) {
 | 
			
		||||
  return (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
inline constexpr uint64_t encode_zigzag64(int64_t value) {
 | 
			
		||||
  return (static_cast<uint64_t>(value) << 1) ^ (static_cast<uint64_t>(value >> 63));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
inline constexpr int32_t decode_zigzag32(uint32_t value) {
 | 
			
		||||
  return (value & 1) ? static_cast<int32_t>(~(value >> 1)) : static_cast<int32_t>(value >> 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
inline constexpr int64_t decode_zigzag64(uint64_t value) {
 | 
			
		||||
  return (value & 1) ? static_cast<int64_t>(~(value >> 1)) : static_cast<int64_t>(value >> 1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
 * StringRef Ownership Model for API Protocol Messages
 | 
			
		||||
 * ===================================================
 | 
			
		||||
@@ -104,25 +87,33 @@ class ProtoVarInt {
 | 
			
		||||
    return {};  // Incomplete or invalid varint
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  constexpr uint16_t as_uint16() const { return this->value_; }
 | 
			
		||||
  constexpr uint32_t as_uint32() const { return this->value_; }
 | 
			
		||||
  constexpr uint64_t as_uint64() const { return this->value_; }
 | 
			
		||||
  constexpr bool as_bool() const { return this->value_; }
 | 
			
		||||
  constexpr int32_t as_int32() const {
 | 
			
		||||
  uint16_t as_uint16() const { return this->value_; }
 | 
			
		||||
  uint32_t as_uint32() const { return this->value_; }
 | 
			
		||||
  uint64_t as_uint64() const { return this->value_; }
 | 
			
		||||
  bool as_bool() const { return this->value_; }
 | 
			
		||||
  int32_t as_int32() const {
 | 
			
		||||
    // Not ZigZag encoded
 | 
			
		||||
    return static_cast<int32_t>(this->as_int64());
 | 
			
		||||
  }
 | 
			
		||||
  constexpr int64_t as_int64() const {
 | 
			
		||||
  int64_t as_int64() const {
 | 
			
		||||
    // Not ZigZag encoded
 | 
			
		||||
    return static_cast<int64_t>(this->value_);
 | 
			
		||||
  }
 | 
			
		||||
  constexpr int32_t as_sint32() const {
 | 
			
		||||
  int32_t as_sint32() const {
 | 
			
		||||
    // with ZigZag encoding
 | 
			
		||||
    return decode_zigzag32(static_cast<uint32_t>(this->value_));
 | 
			
		||||
    if (this->value_ & 1) {
 | 
			
		||||
      return static_cast<int32_t>(~(this->value_ >> 1));
 | 
			
		||||
    } else {
 | 
			
		||||
      return static_cast<int32_t>(this->value_ >> 1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  constexpr int64_t as_sint64() const {
 | 
			
		||||
  int64_t as_sint64() const {
 | 
			
		||||
    // with ZigZag encoding
 | 
			
		||||
    return decode_zigzag64(this->value_);
 | 
			
		||||
    if (this->value_ & 1) {
 | 
			
		||||
      return static_cast<int64_t>(~(this->value_ >> 1));
 | 
			
		||||
    } else {
 | 
			
		||||
      return static_cast<int64_t>(this->value_ >> 1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  /**
 | 
			
		||||
   * Encode the varint value to a pre-allocated buffer without bounds checking.
 | 
			
		||||
@@ -318,10 +309,22 @@ class ProtoWriteBuffer {
 | 
			
		||||
    this->encode_uint64(field_id, static_cast<uint64_t>(value), force);
 | 
			
		||||
  }
 | 
			
		||||
  void encode_sint32(uint32_t field_id, int32_t value, bool force = false) {
 | 
			
		||||
    this->encode_uint32(field_id, encode_zigzag32(value), force);
 | 
			
		||||
    uint32_t uvalue;
 | 
			
		||||
    if (value < 0) {
 | 
			
		||||
      uvalue = ~(value << 1);
 | 
			
		||||
    } else {
 | 
			
		||||
      uvalue = value << 1;
 | 
			
		||||
    }
 | 
			
		||||
    this->encode_uint32(field_id, uvalue, force);
 | 
			
		||||
  }
 | 
			
		||||
  void encode_sint64(uint32_t field_id, int64_t value, bool force = false) {
 | 
			
		||||
    this->encode_uint64(field_id, encode_zigzag64(value), force);
 | 
			
		||||
    uint64_t uvalue;
 | 
			
		||||
    if (value < 0) {
 | 
			
		||||
      uvalue = ~(value << 1);
 | 
			
		||||
    } else {
 | 
			
		||||
      uvalue = value << 1;
 | 
			
		||||
    }
 | 
			
		||||
    this->encode_uint64(field_id, uvalue, force);
 | 
			
		||||
  }
 | 
			
		||||
  void encode_message(uint32_t field_id, const ProtoMessage &value, bool force = false);
 | 
			
		||||
  std::vector<uint8_t> *get_buffer() const { return buffer_; }
 | 
			
		||||
@@ -392,7 +395,7 @@ class ProtoSize {
 | 
			
		||||
   * @param value The uint32_t value to calculate size for
 | 
			
		||||
   * @return The number of bytes needed to encode the value
 | 
			
		||||
   */
 | 
			
		||||
  static constexpr uint32_t varint(uint32_t value) {
 | 
			
		||||
  static inline uint32_t varint(uint32_t value) {
 | 
			
		||||
    // Optimized varint size calculation using leading zeros
 | 
			
		||||
    // Each 7 bits requires one byte in the varint encoding
 | 
			
		||||
    if (value < 128)
 | 
			
		||||
@@ -416,7 +419,7 @@ class ProtoSize {
 | 
			
		||||
   * @param value The uint64_t value to calculate size for
 | 
			
		||||
   * @return The number of bytes needed to encode the value
 | 
			
		||||
   */
 | 
			
		||||
  static constexpr uint32_t varint(uint64_t value) {
 | 
			
		||||
  static inline uint32_t varint(uint64_t value) {
 | 
			
		||||
    // Handle common case of values fitting in uint32_t (vast majority of use cases)
 | 
			
		||||
    if (value <= UINT32_MAX) {
 | 
			
		||||
      return varint(static_cast<uint32_t>(value));
 | 
			
		||||
@@ -447,7 +450,7 @@ class ProtoSize {
 | 
			
		||||
   * @param value The int32_t value to calculate size for
 | 
			
		||||
   * @return The number of bytes needed to encode the value
 | 
			
		||||
   */
 | 
			
		||||
  static constexpr uint32_t varint(int32_t value) {
 | 
			
		||||
  static inline uint32_t varint(int32_t value) {
 | 
			
		||||
    // Negative values are sign-extended to 64 bits in protocol buffers,
 | 
			
		||||
    // which always results in a 10-byte varint for negative int32
 | 
			
		||||
    if (value < 0) {
 | 
			
		||||
@@ -463,7 +466,7 @@ class ProtoSize {
 | 
			
		||||
   * @param value The int64_t value to calculate size for
 | 
			
		||||
   * @return The number of bytes needed to encode the value
 | 
			
		||||
   */
 | 
			
		||||
  static constexpr uint32_t varint(int64_t value) {
 | 
			
		||||
  static inline uint32_t varint(int64_t value) {
 | 
			
		||||
    // For int64_t, we convert to uint64_t and calculate the size
 | 
			
		||||
    // This works because the bit pattern determines the encoding size,
 | 
			
		||||
    // and we've handled negative int32 values as a special case above
 | 
			
		||||
@@ -477,7 +480,7 @@ class ProtoSize {
 | 
			
		||||
   * @param type The wire type value (from the WireType enum in the protobuf spec)
 | 
			
		||||
   * @return The number of bytes needed to encode the field ID and wire type
 | 
			
		||||
   */
 | 
			
		||||
  static constexpr uint32_t field(uint32_t field_id, uint32_t type) {
 | 
			
		||||
  static inline uint32_t field(uint32_t field_id, uint32_t type) {
 | 
			
		||||
    uint32_t tag = (field_id << 3) | (type & 0b111);
 | 
			
		||||
    return varint(tag);
 | 
			
		||||
  }
 | 
			
		||||
@@ -604,8 +607,9 @@ class ProtoSize {
 | 
			
		||||
   */
 | 
			
		||||
  inline void add_sint32_force(uint32_t field_id_size, int32_t value) {
 | 
			
		||||
    // Always calculate size when force is true
 | 
			
		||||
    // ZigZag encoding for sint32
 | 
			
		||||
    total_size_ += field_id_size + varint(encode_zigzag32(value));
 | 
			
		||||
    // ZigZag encoding for sint32: (n << 1) ^ (n >> 31)
 | 
			
		||||
    uint32_t zigzag = (static_cast<uint32_t>(value) << 1) ^ (static_cast<uint32_t>(value >> 31));
 | 
			
		||||
    total_size_ += field_id_size + varint(zigzag);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,6 @@ from esphome.const import (
 | 
			
		||||
    CONF_DIRECTION,
 | 
			
		||||
    CONF_HYSTERESIS,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_POWER_MODE,
 | 
			
		||||
    CONF_RANGE,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -58,6 +57,7 @@ FAST_FILTER = {
 | 
			
		||||
CONF_RAW_ANGLE = "raw_angle"
 | 
			
		||||
CONF_RAW_POSITION = "raw_position"
 | 
			
		||||
CONF_WATCHDOG = "watchdog"
 | 
			
		||||
CONF_POWER_MODE = "power_mode"
 | 
			
		||||
CONF_SLOW_FILTER = "slow_filter"
 | 
			
		||||
CONF_FAST_FILTER = "fast_filter"
 | 
			
		||||
CONF_START_POSITION = "start_position"
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,7 @@ AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingCompone
 | 
			
		||||
CONF_RAW_ANGLE = "raw_angle"
 | 
			
		||||
CONF_RAW_POSITION = "raw_position"
 | 
			
		||||
CONF_WATCHDOG = "watchdog"
 | 
			
		||||
CONF_POWER_MODE = "power_mode"
 | 
			
		||||
CONF_SLOW_FILTER = "slow_filter"
 | 
			
		||||
CONF_FAST_FILTER = "fast_filter"
 | 
			
		||||
CONF_PWM_FREQUENCY = "pwm_frequency"
 | 
			
		||||
 
 | 
			
		||||
@@ -10,7 +10,7 @@ from esphome.const import (
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
CODEOWNERS = ["@OttoWinter"]
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema({}),
 | 
			
		||||
 
 | 
			
		||||
@@ -110,8 +110,6 @@ void ATM90E32Component::update() {
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::setup() {
 | 
			
		||||
  this->spi_setup();
 | 
			
		||||
  this->cs_summary_ = this->cs_->dump_summary();
 | 
			
		||||
  const char *cs = this->cs_summary_.c_str();
 | 
			
		||||
 | 
			
		||||
  uint16_t mmode0 = 0x87;  // 3P4W 50Hz
 | 
			
		||||
  uint16_t high_thresh = 0;
 | 
			
		||||
@@ -132,9 +130,9 @@ void ATM90E32Component::setup() {
 | 
			
		||||
    mmode0 |= 0 << 1;  // sets 1st bit to 0, phase b is not counted into the all-phase sum energy/power (P/Q/S)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A, false);  // Perform soft reset
 | 
			
		||||
  delay(6);                                                    // Wait for the minimum 5ms + 1ms
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA);       // enable register config access
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_SOFTRESET, 0x789A);    // Perform soft reset
 | 
			
		||||
  delay(6);                                               // Wait for the minimum 5ms + 1ms
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x55AA);  // enable register config access
 | 
			
		||||
  if (!this->validate_spi_read_(0x55AA, "setup()")) {
 | 
			
		||||
    ESP_LOGW(TAG, "Could not initialize ATM90E32 IC, check SPI settings");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
@@ -158,17 +156,16 @@ void ATM90E32Component::setup() {
 | 
			
		||||
 | 
			
		||||
  if (this->enable_offset_calibration_) {
 | 
			
		||||
    // Initialize flash storage for offset calibrations
 | 
			
		||||
    uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_summary_);
 | 
			
		||||
    uint32_t o_hash = fnv1_hash(std::string("_offset_calibration_") + this->cs_->dump_summary());
 | 
			
		||||
    this->offset_pref_ = global_preferences->make_preference<OffsetCalibration[3]>(o_hash, true);
 | 
			
		||||
    this->restore_offset_calibrations_();
 | 
			
		||||
 | 
			
		||||
    // Initialize flash storage for power offset calibrations
 | 
			
		||||
    uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_summary_);
 | 
			
		||||
    uint32_t po_hash = fnv1_hash(std::string("_power_offset_calibration_") + this->cs_->dump_summary());
 | 
			
		||||
    this->power_offset_pref_ = global_preferences->make_preference<PowerOffsetCalibration[3]>(po_hash, true);
 | 
			
		||||
    this->restore_power_offset_calibrations_();
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] Power & Voltage/Current offset calibration is disabled. Using config file values.",
 | 
			
		||||
             cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION] Power & Voltage/Current offset calibration is disabled. Using config file values.");
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; ++phase) {
 | 
			
		||||
      this->write16_(this->voltage_offset_registers[phase],
 | 
			
		||||
                     static_cast<uint16_t>(this->offset_phase_[phase].voltage_offset_));
 | 
			
		||||
@@ -183,18 +180,21 @@ void ATM90E32Component::setup() {
 | 
			
		||||
 | 
			
		||||
  if (this->enable_gain_calibration_) {
 | 
			
		||||
    // Initialize flash storage for gain calibration
 | 
			
		||||
    uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_summary_);
 | 
			
		||||
    uint32_t g_hash = fnv1_hash(std::string("_gain_calibration_") + this->cs_->dump_summary());
 | 
			
		||||
    this->gain_calibration_pref_ = global_preferences->make_preference<GainCalibration[3]>(g_hash, true);
 | 
			
		||||
    this->restore_gain_calibrations_();
 | 
			
		||||
 | 
			
		||||
    if (!this->using_saved_calibrations_) {
 | 
			
		||||
    if (this->using_saved_calibrations_) {
 | 
			
		||||
      ESP_LOGI(TAG, "[CALIBRATION] Successfully restored gain calibration from memory.");
 | 
			
		||||
    } else {
 | 
			
		||||
      for (uint8_t phase = 0; phase < 3; ++phase) {
 | 
			
		||||
        this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_);
 | 
			
		||||
        this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration is disabled. Using config file values.", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION] Gain calibration is disabled. Using config file values.");
 | 
			
		||||
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; ++phase) {
 | 
			
		||||
      this->write16_(voltage_gain_registers[phase], this->phase_[phase].voltage_gain_);
 | 
			
		||||
      this->write16_(current_gain_registers[phase], this->phase_[phase].ct_gain_);
 | 
			
		||||
@@ -213,122 +213,6 @@ void ATM90E32Component::setup() {
 | 
			
		||||
  this->write16_(ATM90E32_REGISTER_CFGREGACCEN, 0x0000);  // end configuration
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::log_calibration_status_() {
 | 
			
		||||
  const char *cs = this->cs_summary_.c_str();
 | 
			
		||||
 | 
			
		||||
  bool offset_mismatch = false;
 | 
			
		||||
  bool power_mismatch = false;
 | 
			
		||||
  bool gain_mismatch = false;
 | 
			
		||||
 | 
			
		||||
  for (uint8_t phase = 0; phase < 3; ++phase) {
 | 
			
		||||
    offset_mismatch |= this->offset_calibration_mismatch_[phase];
 | 
			
		||||
    power_mismatch |= this->power_offset_calibration_mismatch_[phase];
 | 
			
		||||
    gain_mismatch |= this->gain_calibration_mismatch_[phase];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (offset_mismatch) {
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION][%s] ", cs);
 | 
			
		||||
    ESP_LOGW(TAG,
 | 
			
		||||
             "[CALIBRATION][%s] ===================== Offset mismatch: using flash values =====================", cs);
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------",
 | 
			
		||||
             cs);
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION][%s] | Phase |   offset_voltage   |   offset_current   |", cs);
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION][%s] |       |  config  |  flash  |  config  |  flash  |", cs);
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------",
 | 
			
		||||
             cs);
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; ++phase) {
 | 
			
		||||
      ESP_LOGW(TAG, "[CALIBRATION][%s] |   %c   |  %6d  | %6d  |  %6d  | %6d  |", cs, 'A' + phase,
 | 
			
		||||
               this->config_offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].voltage_offset_,
 | 
			
		||||
               this->config_offset_phase_[phase].current_offset_, this->offset_phase_[phase].current_offset_);
 | 
			
		||||
    }
 | 
			
		||||
    ESP_LOGW(TAG,
 | 
			
		||||
             "[CALIBRATION][%s] ===============================================================================", cs);
 | 
			
		||||
  }
 | 
			
		||||
  if (power_mismatch) {
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION][%s] ", cs);
 | 
			
		||||
    ESP_LOGW(TAG,
 | 
			
		||||
             "[CALIBRATION][%s] ================= Power offset mismatch: using flash values =================", cs);
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------",
 | 
			
		||||
             cs);
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION][%s] | Phase | offset_active_power|offset_reactive_power|", cs);
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION][%s] |       |  config  |  flash  |  config  |  flash  |", cs);
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------",
 | 
			
		||||
             cs);
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; ++phase) {
 | 
			
		||||
      ESP_LOGW(TAG, "[CALIBRATION][%s] |   %c   |  %6d  | %6d  |  %6d  | %6d  |", cs, 'A' + phase,
 | 
			
		||||
               this->config_power_offset_phase_[phase].active_power_offset,
 | 
			
		||||
               this->power_offset_phase_[phase].active_power_offset,
 | 
			
		||||
               this->config_power_offset_phase_[phase].reactive_power_offset,
 | 
			
		||||
               this->power_offset_phase_[phase].reactive_power_offset);
 | 
			
		||||
    }
 | 
			
		||||
    ESP_LOGW(TAG,
 | 
			
		||||
             "[CALIBRATION][%s] ===============================================================================", cs);
 | 
			
		||||
  }
 | 
			
		||||
  if (gain_mismatch) {
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION][%s] ", cs);
 | 
			
		||||
    ESP_LOGW(TAG,
 | 
			
		||||
             "[CALIBRATION][%s] ====================== Gain mismatch: using flash values =====================", cs);
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------",
 | 
			
		||||
             cs);
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION][%s] | Phase |    voltage_gain    |    current_gain    |", cs);
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION][%s] |       |  config  |  flash  |  config  |  flash  |", cs);
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------------------",
 | 
			
		||||
             cs);
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; ++phase) {
 | 
			
		||||
      ESP_LOGW(TAG, "[CALIBRATION][%s] |   %c   |  %6u  | %6u  |  %6u  | %6u  |", cs, 'A' + phase,
 | 
			
		||||
               this->config_gain_phase_[phase].voltage_gain, this->gain_phase_[phase].voltage_gain,
 | 
			
		||||
               this->config_gain_phase_[phase].current_gain, this->gain_phase_[phase].current_gain);
 | 
			
		||||
    }
 | 
			
		||||
    ESP_LOGW(TAG,
 | 
			
		||||
             "[CALIBRATION][%s] ===============================================================================", cs);
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->enable_offset_calibration_) {
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] Power & Voltage/Current offset calibration is disabled. Using config file values.",
 | 
			
		||||
             cs);
 | 
			
		||||
  } else if (this->restored_offset_calibration_ && !offset_mismatch) {
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] ============== Restored offset calibration from memory ==============", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      ESP_LOGI(TAG, "[CALIBRATION][%s] |   %c   |     %6d      |     %6d      |", cs, 'A' + phase,
 | 
			
		||||
               this->offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].current_offset_);
 | 
			
		||||
    }
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\\n", cs);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->restored_power_offset_calibration_ && !power_mismatch) {
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] ============ Restored power offset calibration from memory ============", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      ESP_LOGI(TAG, "[CALIBRATION][%s] |   %c   |       %6d        |        %6d        |", cs, 'A' + phase,
 | 
			
		||||
               this->power_offset_phase_[phase].active_power_offset,
 | 
			
		||||
               this->power_offset_phase_[phase].reactive_power_offset);
 | 
			
		||||
    }
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->enable_gain_calibration_) {
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration is disabled. Using config file values.", cs);
 | 
			
		||||
  } else if (this->restored_gain_calibration_ && !gain_mismatch) {
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] ============ Restoring saved gain calibrations to registers ============", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      ESP_LOGI(TAG, "[CALIBRATION][%s] |   %c   |    %6u    |    %6u    |", cs, 'A' + phase,
 | 
			
		||||
               this->gain_phase_[phase].voltage_gain, this->gain_phase_[phase].current_gain);
 | 
			
		||||
    }
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\\n", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration loaded and verified successfully.\n", cs);
 | 
			
		||||
  }
 | 
			
		||||
  this->calibration_message_printed_ = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG("", "ATM90E32:");
 | 
			
		||||
  LOG_PIN("  CS Pin: ", this->cs_);
 | 
			
		||||
@@ -371,10 +255,6 @@ void ATM90E32Component::dump_config() {
 | 
			
		||||
  LOG_SENSOR("  ", "Peak Current C", this->phase_[PHASEC].peak_current_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Frequency", this->freq_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Chip Temp", this->chip_temperature_sensor_);
 | 
			
		||||
  if (this->restored_offset_calibration_ || this->restored_power_offset_calibration_ ||
 | 
			
		||||
      this->restored_gain_calibration_ || !this->enable_offset_calibration_ || !this->enable_gain_calibration_) {
 | 
			
		||||
    this->log_calibration_status_();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ATM90E32Component::get_setup_priority() const { return setup_priority::IO; }
 | 
			
		||||
@@ -382,35 +262,26 @@ float ATM90E32Component::get_setup_priority() const { return setup_priority::IO;
 | 
			
		||||
// R/C registers can conly be cleared after the LastSPIData register is updated (register 78H)
 | 
			
		||||
// Peakdetect period: 05H. Bit 15:8 are PeakDet_period in ms. 7:0 are Sag_period
 | 
			
		||||
// Default is 143FH (20ms, 63ms)
 | 
			
		||||
uint16_t ATM90E32Component::read16_transaction_(uint16_t a_register) {
 | 
			
		||||
uint16_t ATM90E32Component::read16_(uint16_t a_register) {
 | 
			
		||||
  uint8_t addrh = (1 << 7) | ((a_register >> 8) & 0x03);
 | 
			
		||||
  uint8_t addrl = (a_register & 0xFF);
 | 
			
		||||
  uint8_t data[4] = {addrh, addrl, 0x00, 0x00};
 | 
			
		||||
  this->transfer_array(data, 4);
 | 
			
		||||
  uint16_t output = encode_uint16(data[2], data[3]);
 | 
			
		||||
  uint8_t data[2];
 | 
			
		||||
  uint16_t output;
 | 
			
		||||
  this->enable();
 | 
			
		||||
  delay_microseconds_safe(1);  // min delay between CS low and first SCK is 200ns - 1ms is plenty
 | 
			
		||||
  this->write_byte(addrh);
 | 
			
		||||
  this->write_byte(addrl);
 | 
			
		||||
  this->read_array(data, 2);
 | 
			
		||||
  this->disable();
 | 
			
		||||
 | 
			
		||||
  output = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF);
 | 
			
		||||
  ESP_LOGVV(TAG, "read16_ 0x%04" PRIX16 " output 0x%04" PRIX16, a_register, output);
 | 
			
		||||
  return output;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint16_t ATM90E32Component::read16_(uint16_t a_register) {
 | 
			
		||||
  this->enable();
 | 
			
		||||
  delay_microseconds_safe(1);  // min delay between CS low and first SCK is 200ns - 1us is plenty
 | 
			
		||||
  uint16_t output = this->read16_transaction_(a_register);
 | 
			
		||||
  delay_microseconds_safe(1);  // allow the last clock to propagate before releasing CS
 | 
			
		||||
  this->disable();
 | 
			
		||||
  delay_microseconds_safe(1);  // meet minimum CS high time before next transaction
 | 
			
		||||
  return output;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int ATM90E32Component::read32_(uint16_t addr_h, uint16_t addr_l) {
 | 
			
		||||
  this->enable();
 | 
			
		||||
  delay_microseconds_safe(1);
 | 
			
		||||
  const uint16_t val_h = this->read16_transaction_(addr_h);
 | 
			
		||||
  delay_microseconds_safe(1);
 | 
			
		||||
  const uint16_t val_l = this->read16_transaction_(addr_l);
 | 
			
		||||
  delay_microseconds_safe(1);
 | 
			
		||||
  this->disable();
 | 
			
		||||
  delay_microseconds_safe(1);
 | 
			
		||||
  const uint16_t val_h = this->read16_(addr_h);
 | 
			
		||||
  const uint16_t val_l = this->read16_(addr_l);
 | 
			
		||||
  const int32_t val = (val_h << 16) | val_l;
 | 
			
		||||
 | 
			
		||||
  ESP_LOGVV(TAG,
 | 
			
		||||
@@ -421,19 +292,13 @@ int ATM90E32Component::read32_(uint16_t addr_h, uint16_t addr_l) {
 | 
			
		||||
  return val;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::write16_(uint16_t a_register, uint16_t val, bool validate) {
 | 
			
		||||
void ATM90E32Component::write16_(uint16_t a_register, uint16_t val) {
 | 
			
		||||
  ESP_LOGVV(TAG, "write16_ 0x%04" PRIX16 " val 0x%04" PRIX16, a_register, val);
 | 
			
		||||
  uint8_t addrh = ((a_register >> 8) & 0x03);
 | 
			
		||||
  uint8_t addrl = (a_register & 0xFF);
 | 
			
		||||
  uint8_t data[4] = {addrh, addrl, uint8_t((val >> 8) & 0xFF), uint8_t(val & 0xFF)};
 | 
			
		||||
  this->enable();
 | 
			
		||||
  delay_microseconds_safe(1);  // ensure CS setup time
 | 
			
		||||
  this->write_array(data, 4);
 | 
			
		||||
  delay_microseconds_safe(1);  // allow clock to settle before raising CS
 | 
			
		||||
  this->write_byte16(a_register);
 | 
			
		||||
  this->write_byte16(val);
 | 
			
		||||
  this->disable();
 | 
			
		||||
  delay_microseconds_safe(1);  // ensure minimum CS high time
 | 
			
		||||
  if (validate)
 | 
			
		||||
    this->validate_spi_read_(val, "write16()");
 | 
			
		||||
  this->validate_spi_read_(val, "write16()");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ATM90E32Component::get_local_phase_voltage_(uint8_t phase) { return this->phase_[phase].voltage_; }
 | 
			
		||||
@@ -576,10 +441,8 @@ float ATM90E32Component::get_chip_temperature_() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::run_gain_calibrations() {
 | 
			
		||||
  const char *cs = this->cs_summary_.c_str();
 | 
			
		||||
  if (!this->enable_gain_calibration_) {
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION][%s] Gain calibration is disabled! Enable it first with enable_gain_calibration: true",
 | 
			
		||||
             cs);
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION] Gain calibration is disabled! Enable it first with enable_gain_calibration: true");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -591,14 +454,12 @@ void ATM90E32Component::run_gain_calibrations() {
 | 
			
		||||
  float ref_currents[3] = {this->get_reference_current(0), this->get_reference_current(1),
 | 
			
		||||
                           this->get_reference_current(2)};
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] ========================= Gain Calibration  =========================", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
 | 
			
		||||
  ESP_LOGI(
 | 
			
		||||
      TAG,
 | 
			
		||||
      "[CALIBRATION][%s] | Phase | V_meas (V) | I_meas (A) | V_ref | I_ref  | V_gain (old→new) | I_gain (old→new) |",
 | 
			
		||||
      cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION] ");
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION] ========================= Gain Calibration  =========================");
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------");
 | 
			
		||||
  ESP_LOGI(TAG,
 | 
			
		||||
           "[CALIBRATION] | Phase | V_meas (V) | I_meas (A) | V_ref | I_ref  | V_gain (old→new) | I_gain (old→new) |");
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION] ---------------------------------------------------------------------");
 | 
			
		||||
 | 
			
		||||
  for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
    float measured_voltage = this->get_phase_voltage_avg_(phase);
 | 
			
		||||
@@ -615,22 +476,22 @@ void ATM90E32Component::run_gain_calibrations() {
 | 
			
		||||
 | 
			
		||||
    // Voltage calibration
 | 
			
		||||
    if (ref_voltage <= 0.0f) {
 | 
			
		||||
      ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: reference voltage is 0.", cs,
 | 
			
		||||
      ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: reference voltage is 0.",
 | 
			
		||||
               phase_labels[phase]);
 | 
			
		||||
    } else if (measured_voltage == 0.0f) {
 | 
			
		||||
      ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping voltage calibration: measured voltage is 0.", cs,
 | 
			
		||||
      ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping voltage calibration: measured voltage is 0.",
 | 
			
		||||
               phase_labels[phase]);
 | 
			
		||||
    } else {
 | 
			
		||||
      uint32_t new_voltage_gain = static_cast<uint16_t>((ref_voltage / measured_voltage) * current_voltage_gain);
 | 
			
		||||
      if (new_voltage_gain == 0) {
 | 
			
		||||
        ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Voltage gain would be 0. Check reference and measured voltage.", cs,
 | 
			
		||||
        ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Voltage gain would be 0. Check reference and measured voltage.",
 | 
			
		||||
                 phase_labels[phase]);
 | 
			
		||||
      } else {
 | 
			
		||||
        if (new_voltage_gain >= 65535) {
 | 
			
		||||
          ESP_LOGW(TAG,
 | 
			
		||||
                   "[CALIBRATION][%s] Phase %s - Voltage gain exceeds 65535. You may need a higher output voltage "
 | 
			
		||||
                   "transformer.",
 | 
			
		||||
                   cs, phase_labels[phase]);
 | 
			
		||||
          ESP_LOGW(
 | 
			
		||||
              TAG,
 | 
			
		||||
              "[CALIBRATION] Phase %s - Voltage gain exceeds 65535. You may need a higher output voltage transformer.",
 | 
			
		||||
              phase_labels[phase]);
 | 
			
		||||
          new_voltage_gain = 65535;
 | 
			
		||||
        }
 | 
			
		||||
        this->gain_phase_[phase].voltage_gain = static_cast<uint16_t>(new_voltage_gain);
 | 
			
		||||
@@ -640,20 +501,20 @@ void ATM90E32Component::run_gain_calibrations() {
 | 
			
		||||
 | 
			
		||||
    // Current calibration
 | 
			
		||||
    if (ref_current == 0.0f) {
 | 
			
		||||
      ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: reference current is 0.", cs,
 | 
			
		||||
      ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: reference current is 0.",
 | 
			
		||||
               phase_labels[phase]);
 | 
			
		||||
    } else if (measured_current == 0.0f) {
 | 
			
		||||
      ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Skipping current calibration: measured current is 0.", cs,
 | 
			
		||||
      ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Skipping current calibration: measured current is 0.",
 | 
			
		||||
               phase_labels[phase]);
 | 
			
		||||
    } else {
 | 
			
		||||
      uint32_t new_current_gain = static_cast<uint16_t>((ref_current / measured_current) * current_current_gain);
 | 
			
		||||
      if (new_current_gain == 0) {
 | 
			
		||||
        ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain would be 0. Check reference and measured current.", cs,
 | 
			
		||||
        ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain would be 0. Check reference and measured current.",
 | 
			
		||||
                 phase_labels[phase]);
 | 
			
		||||
      } else {
 | 
			
		||||
        if (new_current_gain >= 65535) {
 | 
			
		||||
          ESP_LOGW(TAG, "[CALIBRATION][%s] Phase %s - Current gain exceeds 65535. You may need to turn up pga gain.",
 | 
			
		||||
                   cs, phase_labels[phase]);
 | 
			
		||||
          ESP_LOGW(TAG, "[CALIBRATION] Phase %s - Current gain exceeds 65535. You may need to turn up pga gain.",
 | 
			
		||||
                   phase_labels[phase]);
 | 
			
		||||
          new_current_gain = 65535;
 | 
			
		||||
        }
 | 
			
		||||
        this->gain_phase_[phase].current_gain = static_cast<uint16_t>(new_current_gain);
 | 
			
		||||
@@ -662,13 +523,13 @@ void ATM90E32Component::run_gain_calibrations() {
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Final row output
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] |   %c   |  %9.2f |  %9.4f | %5.2f | %6.4f |  %5u → %-5u  |  %5u → %-5u  |", cs,
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION] |   %c   |  %9.2f |  %9.4f | %5.2f | %6.4f |  %5u → %-5u  |  %5u → %-5u  |",
 | 
			
		||||
             'A' + phase, measured_voltage, measured_current, ref_voltage, ref_current, current_voltage_gain,
 | 
			
		||||
             did_voltage ? this->gain_phase_[phase].voltage_gain : current_voltage_gain, current_current_gain,
 | 
			
		||||
             did_current ? this->gain_phase_[phase].current_gain : current_current_gain);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION] =====================================================================\n");
 | 
			
		||||
 | 
			
		||||
  this->save_gain_calibration_to_memory_();
 | 
			
		||||
  this->write_gains_to_registers_();
 | 
			
		||||
@@ -676,108 +537,54 @@ void ATM90E32Component::run_gain_calibrations() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::save_gain_calibration_to_memory_() {
 | 
			
		||||
  const char *cs = this->cs_summary_.c_str();
 | 
			
		||||
  bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
 | 
			
		||||
  global_preferences->sync();
 | 
			
		||||
  if (success) {
 | 
			
		||||
    this->using_saved_calibrations_ = true;
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] Gain calibration saved to memory.", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION] Gain calibration saved to memory.");
 | 
			
		||||
  } else {
 | 
			
		||||
    this->using_saved_calibrations_ = false;
 | 
			
		||||
    ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save gain calibration to memory!", cs);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::save_offset_calibration_to_memory_() {
 | 
			
		||||
  const char *cs = this->cs_summary_.c_str();
 | 
			
		||||
  bool success = this->offset_pref_.save(&this->offset_phase_);
 | 
			
		||||
  global_preferences->sync();
 | 
			
		||||
  if (success) {
 | 
			
		||||
    this->using_saved_calibrations_ = true;
 | 
			
		||||
    this->restored_offset_calibration_ = true;
 | 
			
		||||
    for (bool &phase : this->offset_calibration_mismatch_)
 | 
			
		||||
      phase = false;
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] Offset calibration saved to memory.", cs);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->using_saved_calibrations_ = false;
 | 
			
		||||
    ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save offset calibration to memory!", cs);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::save_power_offset_calibration_to_memory_() {
 | 
			
		||||
  const char *cs = this->cs_summary_.c_str();
 | 
			
		||||
  bool success = this->power_offset_pref_.save(&this->power_offset_phase_);
 | 
			
		||||
  global_preferences->sync();
 | 
			
		||||
  if (success) {
 | 
			
		||||
    this->using_saved_calibrations_ = true;
 | 
			
		||||
    this->restored_power_offset_calibration_ = true;
 | 
			
		||||
    for (bool &phase : this->power_offset_calibration_mismatch_)
 | 
			
		||||
      phase = false;
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] Power offset calibration saved to memory.", cs);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->using_saved_calibrations_ = false;
 | 
			
		||||
    ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to save power offset calibration to memory!", cs);
 | 
			
		||||
    ESP_LOGE(TAG, "[CALIBRATION] Failed to save gain calibration to memory!");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::run_offset_calibrations() {
 | 
			
		||||
  const char *cs = this->cs_summary_.c_str();
 | 
			
		||||
  if (!this->enable_offset_calibration_) {
 | 
			
		||||
    ESP_LOGW(TAG,
 | 
			
		||||
             "[CALIBRATION][%s] Offset calibration is disabled! Enable it first with enable_offset_calibration: true",
 | 
			
		||||
             cs);
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION] Offset calibration is disabled! Enable it first with enable_offset_calibration: true");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] ======================== Offset Calibration ========================", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] ------------------------------------------------------------------", cs);
 | 
			
		||||
 | 
			
		||||
  for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
    int16_t voltage_offset = calibrate_offset(phase, true);
 | 
			
		||||
    int16_t current_offset = calibrate_offset(phase, false);
 | 
			
		||||
 | 
			
		||||
    this->write_offsets_to_registers_(phase, voltage_offset, current_offset);
 | 
			
		||||
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] |   %c   |     %6d      |     %6d      |", cs, 'A' + phase, voltage_offset,
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage: %d, offset_current: %d", 'A' + phase, voltage_offset,
 | 
			
		||||
             current_offset);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] ==================================================================\n", cs);
 | 
			
		||||
 | 
			
		||||
  this->save_offset_calibration_to_memory_();
 | 
			
		||||
  this->offset_pref_.save(&this->offset_phase_);  // Save to flash
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::run_power_offset_calibrations() {
 | 
			
		||||
  const char *cs = this->cs_summary_.c_str();
 | 
			
		||||
  if (!this->enable_offset_calibration_) {
 | 
			
		||||
    ESP_LOGW(
 | 
			
		||||
        TAG,
 | 
			
		||||
        "[CALIBRATION][%s] Offset power calibration is disabled! Enable it first with enable_offset_calibration: true",
 | 
			
		||||
        cs);
 | 
			
		||||
        "[CALIBRATION] Offset power calibration is disabled! Enable it first with enable_offset_calibration: true");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] ", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] ===================== Power Offset Calibration =====================", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
 | 
			
		||||
 | 
			
		||||
  for (uint8_t phase = 0; phase < 3; ++phase) {
 | 
			
		||||
    int16_t active_offset = calibrate_power_offset(phase, false);
 | 
			
		||||
    int16_t reactive_offset = calibrate_power_offset(phase, true);
 | 
			
		||||
 | 
			
		||||
    this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset);
 | 
			
		||||
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] |   %c   |       %6d        |        %6d        |", cs, 'A' + phase, active_offset,
 | 
			
		||||
             reactive_offset);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase,
 | 
			
		||||
             active_offset, reactive_offset);
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
 | 
			
		||||
 | 
			
		||||
  this->save_power_offset_calibration_to_memory_();
 | 
			
		||||
  this->power_offset_pref_.save(&this->power_offset_phase_);  // Save to flash
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::write_gains_to_registers_() {
 | 
			
		||||
@@ -824,276 +631,102 @@ void ATM90E32Component::write_power_offsets_to_registers_(uint8_t phase, int16_t
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::restore_gain_calibrations_() {
 | 
			
		||||
  const char *cs = this->cs_summary_.c_str();
 | 
			
		||||
  for (uint8_t i = 0; i < 3; ++i) {
 | 
			
		||||
    this->config_gain_phase_[i].voltage_gain = this->phase_[i].voltage_gain_;
 | 
			
		||||
    this->config_gain_phase_[i].current_gain = this->phase_[i].ct_gain_;
 | 
			
		||||
    this->gain_phase_[i] = this->config_gain_phase_[i];
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->gain_calibration_pref_.load(&this->gain_phase_)) {
 | 
			
		||||
    bool all_zero = true;
 | 
			
		||||
    bool same_as_config = true;
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; ++phase) {
 | 
			
		||||
      const auto &cfg = this->config_gain_phase_[phase];
 | 
			
		||||
      const auto &saved = this->gain_phase_[phase];
 | 
			
		||||
      if (saved.voltage_gain != 0 || saved.current_gain != 0)
 | 
			
		||||
        all_zero = false;
 | 
			
		||||
      if (saved.voltage_gain != cfg.voltage_gain || saved.current_gain != cfg.current_gain)
 | 
			
		||||
        same_as_config = false;
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION] Restoring saved gain calibrations to registers:");
 | 
			
		||||
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      uint16_t v_gain = this->gain_phase_[phase].voltage_gain;
 | 
			
		||||
      uint16_t i_gain = this->gain_phase_[phase].current_gain;
 | 
			
		||||
      ESP_LOGI(TAG, "[CALIBRATION]   Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase, v_gain, i_gain);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!all_zero && !same_as_config) {
 | 
			
		||||
      for (uint8_t phase = 0; phase < 3; ++phase) {
 | 
			
		||||
        bool mismatch = false;
 | 
			
		||||
        if (this->has_config_voltage_gain_[phase] &&
 | 
			
		||||
            this->gain_phase_[phase].voltage_gain != this->config_gain_phase_[phase].voltage_gain)
 | 
			
		||||
          mismatch = true;
 | 
			
		||||
        if (this->has_config_current_gain_[phase] &&
 | 
			
		||||
            this->gain_phase_[phase].current_gain != this->config_gain_phase_[phase].current_gain)
 | 
			
		||||
          mismatch = true;
 | 
			
		||||
        if (mismatch)
 | 
			
		||||
          this->gain_calibration_mismatch_[phase] = true;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this->write_gains_to_registers_();
 | 
			
		||||
 | 
			
		||||
      if (this->verify_gain_writes_()) {
 | 
			
		||||
        this->using_saved_calibrations_ = true;
 | 
			
		||||
        this->restored_gain_calibration_ = true;
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
    this->write_gains_to_registers_();
 | 
			
		||||
 | 
			
		||||
    if (this->verify_gain_writes_()) {
 | 
			
		||||
      this->using_saved_calibrations_ = true;
 | 
			
		||||
      ESP_LOGI(TAG, "[CALIBRATION] Gain calibration loaded and verified successfully.");
 | 
			
		||||
    } else {
 | 
			
		||||
      this->using_saved_calibrations_ = false;
 | 
			
		||||
      ESP_LOGE(TAG, "[CALIBRATION][%s] Gain verification failed! Calibration may not be applied correctly.", cs);
 | 
			
		||||
      ESP_LOGE(TAG, "[CALIBRATION] Gain verification failed! Calibration may not be applied correctly.");
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    this->using_saved_calibrations_ = false;
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION] No stored gain calibrations found. Using config file values.");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->using_saved_calibrations_ = false;
 | 
			
		||||
  for (uint8_t i = 0; i < 3; ++i)
 | 
			
		||||
    this->gain_phase_[i] = this->config_gain_phase_[i];
 | 
			
		||||
  this->write_gains_to_registers_();
 | 
			
		||||
 | 
			
		||||
  ESP_LOGW(TAG, "[CALIBRATION][%s] No stored gain calibrations found. Using config file values.", cs);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::restore_offset_calibrations_() {
 | 
			
		||||
  const char *cs = this->cs_summary_.c_str();
 | 
			
		||||
  for (uint8_t i = 0; i < 3; ++i)
 | 
			
		||||
    this->config_offset_phase_[i] = this->offset_phase_[i];
 | 
			
		||||
  if (this->offset_pref_.load(&this->offset_phase_)) {
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION] Successfully restored offset calibration from memory.");
 | 
			
		||||
 | 
			
		||||
  bool have_data = this->offset_pref_.load(&this->offset_phase_);
 | 
			
		||||
  bool all_zero = true;
 | 
			
		||||
  if (have_data) {
 | 
			
		||||
    for (auto &phase : this->offset_phase_) {
 | 
			
		||||
      if (phase.voltage_offset_ != 0 || phase.current_offset_ != 0) {
 | 
			
		||||
        all_zero = false;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (have_data && !all_zero) {
 | 
			
		||||
    this->restored_offset_calibration_ = true;
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      auto &offset = this->offset_phase_[phase];
 | 
			
		||||
      bool mismatch = false;
 | 
			
		||||
      if (this->has_config_voltage_offset_[phase] &&
 | 
			
		||||
          offset.voltage_offset_ != this->config_offset_phase_[phase].voltage_offset_)
 | 
			
		||||
        mismatch = true;
 | 
			
		||||
      if (this->has_config_current_offset_[phase] &&
 | 
			
		||||
          offset.current_offset_ != this->config_offset_phase_[phase].current_offset_)
 | 
			
		||||
        mismatch = true;
 | 
			
		||||
      if (mismatch)
 | 
			
		||||
        this->offset_calibration_mismatch_[phase] = true;
 | 
			
		||||
      write_offsets_to_registers_(phase, offset.voltage_offset_, offset.current_offset_);
 | 
			
		||||
      ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_voltage:: %d, offset_current: %d", 'A' + phase,
 | 
			
		||||
               offset.voltage_offset_, offset.current_offset_);
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++)
 | 
			
		||||
      this->offset_phase_[phase] = this->config_offset_phase_[phase];
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION][%s] No stored offset calibrations found. Using default values.", cs);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
    write_offsets_to_registers_(phase, this->offset_phase_[phase].voltage_offset_,
 | 
			
		||||
                                this->offset_phase_[phase].current_offset_);
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION] No stored offset calibrations found. Using default values.");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::restore_power_offset_calibrations_() {
 | 
			
		||||
  const char *cs = this->cs_summary_.c_str();
 | 
			
		||||
  for (uint8_t i = 0; i < 3; ++i)
 | 
			
		||||
    this->config_power_offset_phase_[i] = this->power_offset_phase_[i];
 | 
			
		||||
  if (this->power_offset_pref_.load(&this->power_offset_phase_)) {
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION] Successfully restored power offset calibration from memory.");
 | 
			
		||||
 | 
			
		||||
  bool have_data = this->power_offset_pref_.load(&this->power_offset_phase_);
 | 
			
		||||
  bool all_zero = true;
 | 
			
		||||
  if (have_data) {
 | 
			
		||||
    for (auto &phase : this->power_offset_phase_) {
 | 
			
		||||
      if (phase.active_power_offset != 0 || phase.reactive_power_offset != 0) {
 | 
			
		||||
        all_zero = false;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (have_data && !all_zero) {
 | 
			
		||||
    this->restored_power_offset_calibration_ = true;
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; ++phase) {
 | 
			
		||||
      auto &offset = this->power_offset_phase_[phase];
 | 
			
		||||
      bool mismatch = false;
 | 
			
		||||
      if (this->has_config_active_power_offset_[phase] &&
 | 
			
		||||
          offset.active_power_offset != this->config_power_offset_phase_[phase].active_power_offset)
 | 
			
		||||
        mismatch = true;
 | 
			
		||||
      if (this->has_config_reactive_power_offset_[phase] &&
 | 
			
		||||
          offset.reactive_power_offset != this->config_power_offset_phase_[phase].reactive_power_offset)
 | 
			
		||||
        mismatch = true;
 | 
			
		||||
      if (mismatch)
 | 
			
		||||
        this->power_offset_calibration_mismatch_[phase] = true;
 | 
			
		||||
      write_power_offsets_to_registers_(phase, offset.active_power_offset, offset.reactive_power_offset);
 | 
			
		||||
      ESP_LOGI(TAG, "[CALIBRATION] Phase %c - offset_active_power: %d, offset_reactive_power: %d", 'A' + phase,
 | 
			
		||||
               offset.active_power_offset, offset.reactive_power_offset);
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; ++phase)
 | 
			
		||||
      this->power_offset_phase_[phase] = this->config_power_offset_phase_[phase];
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION][%s] No stored power offsets found. Using default values.", cs);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  for (uint8_t phase = 0; phase < 3; ++phase) {
 | 
			
		||||
    write_power_offsets_to_registers_(phase, this->power_offset_phase_[phase].active_power_offset,
 | 
			
		||||
                                      this->power_offset_phase_[phase].reactive_power_offset);
 | 
			
		||||
    ESP_LOGW(TAG, "[CALIBRATION] No stored power offsets found. Using default values.");
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::clear_gain_calibrations() {
 | 
			
		||||
  const char *cs = this->cs_summary_.c_str();
 | 
			
		||||
  if (!this->using_saved_calibrations_) {
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] No stored gain calibrations to clear. Current values:", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs);
 | 
			
		||||
    for (int phase = 0; phase < 3; phase++) {
 | 
			
		||||
      ESP_LOGI(TAG, "[CALIBRATION][%s] |   %c   |    %6u    |    %6u    |", cs, 'A' + phase,
 | 
			
		||||
               this->gain_phase_[phase].voltage_gain, this->gain_phase_[phase].current_gain);
 | 
			
		||||
    }
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] ==========================================================\n", cs);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] Clearing stored gain calibrations and restoring config-defined values", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | voltage_gain | current_gain |", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] ----------------------------------------------------------", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION] Clearing stored gain calibrations and restoring config-defined values");
 | 
			
		||||
 | 
			
		||||
  for (int phase = 0; phase < 3; phase++) {
 | 
			
		||||
    uint16_t voltage_gain = this->phase_[phase].voltage_gain_;
 | 
			
		||||
    uint16_t current_gain = this->phase_[phase].ct_gain_;
 | 
			
		||||
 | 
			
		||||
    this->config_gain_phase_[phase].voltage_gain = voltage_gain;
 | 
			
		||||
    this->config_gain_phase_[phase].current_gain = current_gain;
 | 
			
		||||
    this->gain_phase_[phase].voltage_gain = voltage_gain;
 | 
			
		||||
    this->gain_phase_[phase].current_gain = current_gain;
 | 
			
		||||
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] |   %c   |    %6u    |    %6u    |", cs, 'A' + phase, voltage_gain, current_gain);
 | 
			
		||||
    gain_phase_[phase].voltage_gain = this->phase_[phase].voltage_gain_;
 | 
			
		||||
    gain_phase_[phase].current_gain = this->phase_[phase].ct_gain_;
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] ==========================================================\n", cs);
 | 
			
		||||
 | 
			
		||||
  GainCalibration zero_gains[3]{{0, 0}, {0, 0}, {0, 0}};
 | 
			
		||||
  bool success = this->gain_calibration_pref_.save(&zero_gains);
 | 
			
		||||
  global_preferences->sync();
 | 
			
		||||
 | 
			
		||||
  bool success = this->gain_calibration_pref_.save(&this->gain_phase_);
 | 
			
		||||
  this->using_saved_calibrations_ = false;
 | 
			
		||||
  this->restored_gain_calibration_ = false;
 | 
			
		||||
  for (bool &phase : this->gain_calibration_mismatch_)
 | 
			
		||||
    phase = false;
 | 
			
		||||
 | 
			
		||||
  if (!success) {
 | 
			
		||||
    ESP_LOGE(TAG, "[CALIBRATION][%s] Failed to clear gain calibrations!", cs);
 | 
			
		||||
  if (success) {
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION] Gain calibrations cleared. Config values restored:");
 | 
			
		||||
    for (int phase = 0; phase < 3; phase++) {
 | 
			
		||||
      ESP_LOGI(TAG, "[CALIBRATION]   Phase %c - Voltage Gain: %u, Current Gain: %u", 'A' + phase,
 | 
			
		||||
               gain_phase_[phase].voltage_gain, gain_phase_[phase].current_gain);
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGE(TAG, "[CALIBRATION] Failed to clear gain calibrations!");
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->write_gains_to_registers_();  // Apply them to the chip immediately
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::clear_offset_calibrations() {
 | 
			
		||||
  const char *cs = this->cs_summary_.c_str();
 | 
			
		||||
  if (!this->restored_offset_calibration_) {
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] No stored offset calibrations to clear. Current values:", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      ESP_LOGI(TAG, "[CALIBRATION][%s] |   %c   |     %6d      |     %6d      |", cs, 'A' + phase,
 | 
			
		||||
               this->offset_phase_[phase].voltage_offset_, this->offset_phase_[phase].current_offset_);
 | 
			
		||||
    }
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\n", cs);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] Clearing stored offset calibrations and restoring config-defined values", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_voltage | offset_current |", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] --------------------------------------------------------------", cs);
 | 
			
		||||
 | 
			
		||||
  for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
    int16_t voltage_offset =
 | 
			
		||||
        this->has_config_voltage_offset_[phase] ? this->config_offset_phase_[phase].voltage_offset_ : 0;
 | 
			
		||||
    int16_t current_offset =
 | 
			
		||||
        this->has_config_current_offset_[phase] ? this->config_offset_phase_[phase].current_offset_ : 0;
 | 
			
		||||
    this->write_offsets_to_registers_(phase, voltage_offset, current_offset);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] |   %c   |     %6d      |     %6d      |", cs, 'A' + phase, voltage_offset,
 | 
			
		||||
             current_offset);
 | 
			
		||||
    this->write_offsets_to_registers_(phase, 0, 0);
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] ==============================================================\n", cs);
 | 
			
		||||
 | 
			
		||||
  OffsetCalibration zero_offsets[3]{{0, 0}, {0, 0}, {0, 0}};
 | 
			
		||||
  this->offset_pref_.save(&zero_offsets);  // Clear stored values in flash
 | 
			
		||||
  global_preferences->sync();
 | 
			
		||||
  this->offset_pref_.save(&this->offset_phase_);  // Save cleared values to flash memory
 | 
			
		||||
 | 
			
		||||
  this->restored_offset_calibration_ = false;
 | 
			
		||||
  for (bool &phase : this->offset_calibration_mismatch_)
 | 
			
		||||
    phase = false;
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] Offsets cleared.", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION] Offsets cleared.");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ATM90E32Component::clear_power_offset_calibrations() {
 | 
			
		||||
  const char *cs = this->cs_summary_.c_str();
 | 
			
		||||
  if (!this->restored_power_offset_calibration_) {
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] No stored power offsets to clear. Current values:", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
 | 
			
		||||
    for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
      ESP_LOGI(TAG, "[CALIBRATION][%s] |   %c   |       %6d        |        %6d        |", cs, 'A' + phase,
 | 
			
		||||
               this->power_offset_phase_[phase].active_power_offset,
 | 
			
		||||
               this->power_offset_phase_[phase].reactive_power_offset);
 | 
			
		||||
    }
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] Clearing stored power offsets and restoring config-defined values", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] | Phase | offset_active_power | offset_reactive_power |", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] ---------------------------------------------------------------------", cs);
 | 
			
		||||
 | 
			
		||||
  for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
    int16_t active_offset =
 | 
			
		||||
        this->has_config_active_power_offset_[phase] ? this->config_power_offset_phase_[phase].active_power_offset : 0;
 | 
			
		||||
    int16_t reactive_offset = this->has_config_reactive_power_offset_[phase]
 | 
			
		||||
                                  ? this->config_power_offset_phase_[phase].reactive_power_offset
 | 
			
		||||
                                  : 0;
 | 
			
		||||
    this->write_power_offsets_to_registers_(phase, active_offset, reactive_offset);
 | 
			
		||||
    ESP_LOGI(TAG, "[CALIBRATION][%s] |   %c   |       %6d        |        %6d        |", cs, 'A' + phase, active_offset,
 | 
			
		||||
             reactive_offset);
 | 
			
		||||
    this->write_power_offsets_to_registers_(phase, 0, 0);
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] =====================================================================\n", cs);
 | 
			
		||||
 | 
			
		||||
  PowerOffsetCalibration zero_power_offsets[3]{{0, 0}, {0, 0}, {0, 0}};
 | 
			
		||||
  this->power_offset_pref_.save(&zero_power_offsets);
 | 
			
		||||
  global_preferences->sync();
 | 
			
		||||
  this->power_offset_pref_.save(&this->power_offset_phase_);
 | 
			
		||||
 | 
			
		||||
  this->restored_power_offset_calibration_ = false;
 | 
			
		||||
  for (bool &phase : this->power_offset_calibration_mismatch_)
 | 
			
		||||
    phase = false;
 | 
			
		||||
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION][%s] Power offsets cleared.", cs);
 | 
			
		||||
  ESP_LOGI(TAG, "[CALIBRATION] Power offsets cleared.");
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int16_t ATM90E32Component::calibrate_offset(uint8_t phase, bool voltage) {
 | 
			
		||||
@@ -1114,21 +747,20 @@ int16_t ATM90E32Component::calibrate_offset(uint8_t phase, bool voltage) {
 | 
			
		||||
 | 
			
		||||
int16_t ATM90E32Component::calibrate_power_offset(uint8_t phase, bool reactive) {
 | 
			
		||||
  const uint8_t num_reads = 5;
 | 
			
		||||
  int64_t total_value = 0;
 | 
			
		||||
  uint64_t total_value = 0;
 | 
			
		||||
 | 
			
		||||
  for (uint8_t i = 0; i < num_reads; ++i) {
 | 
			
		||||
    int32_t reading = reactive ? this->read32_(ATM90E32_REGISTER_QMEAN + phase, ATM90E32_REGISTER_QMEANLSB + phase)
 | 
			
		||||
                               : this->read32_(ATM90E32_REGISTER_PMEAN + phase, ATM90E32_REGISTER_PMEANLSB + phase);
 | 
			
		||||
    uint32_t reading = reactive ? this->read32_(ATM90E32_REGISTER_QMEAN + phase, ATM90E32_REGISTER_QMEANLSB + phase)
 | 
			
		||||
                                : this->read32_(ATM90E32_REGISTER_PMEAN + phase, ATM90E32_REGISTER_PMEANLSB + phase);
 | 
			
		||||
    total_value += reading;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int32_t average_value = total_value / num_reads;
 | 
			
		||||
  int32_t power_offset = -average_value;
 | 
			
		||||
  const uint32_t average_value = total_value / num_reads;
 | 
			
		||||
  const uint32_t power_offset = ~average_value + 1;
 | 
			
		||||
  return static_cast<int16_t>(power_offset);  // Takes the lower 16 bits
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ATM90E32Component::verify_gain_writes_() {
 | 
			
		||||
  const char *cs = this->cs_summary_.c_str();
 | 
			
		||||
  bool success = true;
 | 
			
		||||
  for (uint8_t phase = 0; phase < 3; phase++) {
 | 
			
		||||
    uint16_t read_voltage = this->read16_(voltage_gain_registers[phase]);
 | 
			
		||||
@@ -1136,7 +768,7 @@ bool ATM90E32Component::verify_gain_writes_() {
 | 
			
		||||
 | 
			
		||||
    if (read_voltage != this->gain_phase_[phase].voltage_gain ||
 | 
			
		||||
        read_current != this->gain_phase_[phase].current_gain) {
 | 
			
		||||
      ESP_LOGE(TAG, "[CALIBRATION][%s] Mismatch detected for Phase %s!", cs, phase_labels[phase]);
 | 
			
		||||
      ESP_LOGE(TAG, "[CALIBRATION] Mismatch detected for Phase %s!", phase_labels[phase]);
 | 
			
		||||
      success = false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -1159,16 +791,16 @@ void ATM90E32Component::check_phase_status() {
 | 
			
		||||
      status += "Phase Loss; ";
 | 
			
		||||
 | 
			
		||||
    auto *sensor = this->phase_status_text_sensor_[phase];
 | 
			
		||||
    if (sensor == nullptr)
 | 
			
		||||
      continue;
 | 
			
		||||
 | 
			
		||||
    const char *phase_name = sensor ? sensor->get_name().c_str() : "Unknown Phase";
 | 
			
		||||
    if (!status.empty()) {
 | 
			
		||||
      status.pop_back();  // remove space
 | 
			
		||||
      status.pop_back();  // remove semicolon
 | 
			
		||||
      ESP_LOGW(TAG, "%s: %s", sensor->get_name().c_str(), status.c_str());
 | 
			
		||||
      sensor->publish_state(status);
 | 
			
		||||
      ESP_LOGW(TAG, "%s: %s", phase_name, status.c_str());
 | 
			
		||||
      if (sensor != nullptr)
 | 
			
		||||
        sensor->publish_state(status);
 | 
			
		||||
    } else {
 | 
			
		||||
      sensor->publish_state("Okay");
 | 
			
		||||
      if (sensor != nullptr)
 | 
			
		||||
        sensor->publish_state("Okay");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
@@ -1185,12 +817,9 @@ void ATM90E32Component::check_freq_status() {
 | 
			
		||||
  } else {
 | 
			
		||||
    freq_status = "Normal";
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGW(TAG, "Frequency status: %s", freq_status.c_str());
 | 
			
		||||
 | 
			
		||||
  if (this->freq_status_text_sensor_ != nullptr) {
 | 
			
		||||
    if (freq_status == "Normal") {
 | 
			
		||||
      ESP_LOGD(TAG, "Frequency status: %s", freq_status.c_str());
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "Frequency status: %s", freq_status.c_str());
 | 
			
		||||
    }
 | 
			
		||||
    this->freq_status_text_sensor_->publish_state(freq_status);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -61,29 +61,15 @@ class ATM90E32Component : public PollingComponent,
 | 
			
		||||
    this->phase_[phase].harmonic_active_power_sensor_ = obj;
 | 
			
		||||
  }
 | 
			
		||||
  void set_peak_current_sensor(int phase, sensor::Sensor *obj) { this->phase_[phase].peak_current_sensor_ = obj; }
 | 
			
		||||
  void set_volt_gain(int phase, uint16_t gain) {
 | 
			
		||||
    this->phase_[phase].voltage_gain_ = gain;
 | 
			
		||||
    this->has_config_voltage_gain_[phase] = true;
 | 
			
		||||
  }
 | 
			
		||||
  void set_ct_gain(int phase, uint16_t gain) {
 | 
			
		||||
    this->phase_[phase].ct_gain_ = gain;
 | 
			
		||||
    this->has_config_current_gain_[phase] = true;
 | 
			
		||||
  }
 | 
			
		||||
  void set_voltage_offset(uint8_t phase, int16_t offset) {
 | 
			
		||||
    this->offset_phase_[phase].voltage_offset_ = offset;
 | 
			
		||||
    this->has_config_voltage_offset_[phase] = true;
 | 
			
		||||
  }
 | 
			
		||||
  void set_current_offset(uint8_t phase, int16_t offset) {
 | 
			
		||||
    this->offset_phase_[phase].current_offset_ = offset;
 | 
			
		||||
    this->has_config_current_offset_[phase] = true;
 | 
			
		||||
  }
 | 
			
		||||
  void set_volt_gain(int phase, uint16_t gain) { this->phase_[phase].voltage_gain_ = gain; }
 | 
			
		||||
  void set_ct_gain(int phase, uint16_t gain) { this->phase_[phase].ct_gain_ = gain; }
 | 
			
		||||
  void set_voltage_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].voltage_offset_ = offset; }
 | 
			
		||||
  void set_current_offset(uint8_t phase, int16_t offset) { this->offset_phase_[phase].current_offset_ = offset; }
 | 
			
		||||
  void set_active_power_offset(uint8_t phase, int16_t offset) {
 | 
			
		||||
    this->power_offset_phase_[phase].active_power_offset = offset;
 | 
			
		||||
    this->has_config_active_power_offset_[phase] = true;
 | 
			
		||||
  }
 | 
			
		||||
  void set_reactive_power_offset(uint8_t phase, int16_t offset) {
 | 
			
		||||
    this->power_offset_phase_[phase].reactive_power_offset = offset;
 | 
			
		||||
    this->has_config_reactive_power_offset_[phase] = true;
 | 
			
		||||
  }
 | 
			
		||||
  void set_freq_sensor(sensor::Sensor *freq_sensor) { freq_sensor_ = freq_sensor; }
 | 
			
		||||
  void set_peak_current_signed(bool flag) { peak_current_signed_ = flag; }
 | 
			
		||||
@@ -140,9 +126,8 @@ class ATM90E32Component : public PollingComponent,
 | 
			
		||||
  number::Number *ref_currents_[3]{nullptr, nullptr, nullptr};
 | 
			
		||||
#endif
 | 
			
		||||
  uint16_t read16_(uint16_t a_register);
 | 
			
		||||
  uint16_t read16_transaction_(uint16_t a_register);
 | 
			
		||||
  int read32_(uint16_t addr_h, uint16_t addr_l);
 | 
			
		||||
  void write16_(uint16_t a_register, uint16_t val, bool validate = true);
 | 
			
		||||
  void write16_(uint16_t a_register, uint16_t val);
 | 
			
		||||
  float get_local_phase_voltage_(uint8_t phase);
 | 
			
		||||
  float get_local_phase_current_(uint8_t phase);
 | 
			
		||||
  float get_local_phase_active_power_(uint8_t phase);
 | 
			
		||||
@@ -174,15 +159,12 @@ class ATM90E32Component : public PollingComponent,
 | 
			
		||||
  void restore_offset_calibrations_();
 | 
			
		||||
  void restore_power_offset_calibrations_();
 | 
			
		||||
  void restore_gain_calibrations_();
 | 
			
		||||
  void save_offset_calibration_to_memory_();
 | 
			
		||||
  void save_gain_calibration_to_memory_();
 | 
			
		||||
  void save_power_offset_calibration_to_memory_();
 | 
			
		||||
  void write_offsets_to_registers_(uint8_t phase, int16_t voltage_offset, int16_t current_offset);
 | 
			
		||||
  void write_power_offsets_to_registers_(uint8_t phase, int16_t p_offset, int16_t q_offset);
 | 
			
		||||
  void write_gains_to_registers_();
 | 
			
		||||
  bool verify_gain_writes_();
 | 
			
		||||
  bool validate_spi_read_(uint16_t expected, const char *context = nullptr);
 | 
			
		||||
  void log_calibration_status_();
 | 
			
		||||
 | 
			
		||||
  struct ATM90E32Phase {
 | 
			
		||||
    uint16_t voltage_gain_{0};
 | 
			
		||||
@@ -222,33 +204,19 @@ class ATM90E32Component : public PollingComponent,
 | 
			
		||||
    int16_t current_offset_{0};
 | 
			
		||||
  } offset_phase_[3];
 | 
			
		||||
 | 
			
		||||
  OffsetCalibration config_offset_phase_[3];
 | 
			
		||||
 | 
			
		||||
  struct PowerOffsetCalibration {
 | 
			
		||||
    int16_t active_power_offset{0};
 | 
			
		||||
    int16_t reactive_power_offset{0};
 | 
			
		||||
  } power_offset_phase_[3];
 | 
			
		||||
 | 
			
		||||
  PowerOffsetCalibration config_power_offset_phase_[3];
 | 
			
		||||
 | 
			
		||||
  struct GainCalibration {
 | 
			
		||||
    uint16_t voltage_gain{1};
 | 
			
		||||
    uint16_t current_gain{1};
 | 
			
		||||
  } gain_phase_[3];
 | 
			
		||||
 | 
			
		||||
  GainCalibration config_gain_phase_[3];
 | 
			
		||||
 | 
			
		||||
  bool has_config_voltage_offset_[3]{false, false, false};
 | 
			
		||||
  bool has_config_current_offset_[3]{false, false, false};
 | 
			
		||||
  bool has_config_active_power_offset_[3]{false, false, false};
 | 
			
		||||
  bool has_config_reactive_power_offset_[3]{false, false, false};
 | 
			
		||||
  bool has_config_voltage_gain_[3]{false, false, false};
 | 
			
		||||
  bool has_config_current_gain_[3]{false, false, false};
 | 
			
		||||
 | 
			
		||||
  ESPPreferenceObject offset_pref_;
 | 
			
		||||
  ESPPreferenceObject power_offset_pref_;
 | 
			
		||||
  ESPPreferenceObject gain_calibration_pref_;
 | 
			
		||||
  std::string cs_summary_;
 | 
			
		||||
 | 
			
		||||
  sensor::Sensor *freq_sensor_{nullptr};
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
@@ -263,13 +231,6 @@ class ATM90E32Component : public PollingComponent,
 | 
			
		||||
  bool peak_current_signed_{false};
 | 
			
		||||
  bool enable_offset_calibration_{false};
 | 
			
		||||
  bool enable_gain_calibration_{false};
 | 
			
		||||
  bool restored_offset_calibration_{false};
 | 
			
		||||
  bool restored_power_offset_calibration_{false};
 | 
			
		||||
  bool restored_gain_calibration_{false};
 | 
			
		||||
  bool calibration_message_printed_{false};
 | 
			
		||||
  bool offset_calibration_mismatch_[3]{false, false, false};
 | 
			
		||||
  bool power_offset_calibration_mismatch_[3]{false, false, false};
 | 
			
		||||
  bool gain_calibration_mismatch_[3]{false, false, false};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace atm90e32
 | 
			
		||||
 
 | 
			
		||||
@@ -1,19 +1,13 @@
 | 
			
		||||
import logging
 | 
			
		||||
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import esp32_ble, esp32_ble_client, esp32_ble_tracker
 | 
			
		||||
from esphome.components.esp32 import add_idf_sdkconfig_option
 | 
			
		||||
from esphome.components.esp32_ble import BTLoggers
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ACTIVE, CONF_ID
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
from esphome.log import AnsiFore, color
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["esp32_ble_client", "esp32_ble_tracker"]
 | 
			
		||||
DEPENDENCIES = ["api", "esp32"]
 | 
			
		||||
CODEOWNERS = ["@jesserockz", "@bdraco"]
 | 
			
		||||
 | 
			
		||||
_LOGGER = logging.getLogger(__name__)
 | 
			
		||||
CODEOWNERS = ["@jesserockz"]
 | 
			
		||||
 | 
			
		||||
CONF_CONNECTION_SLOTS = "connection_slots"
 | 
			
		||||
CONF_CACHE_SERVICES = "cache_services"
 | 
			
		||||
@@ -47,27 +41,6 @@ def validate_connections(config):
 | 
			
		||||
        esp32_ble_tracker.consume_connection_slots(connection_slots, "bluetooth_proxy")(
 | 
			
		||||
            config
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        # Warn about connection slot waste when using Arduino framework
 | 
			
		||||
        if CORE.using_arduino and connection_slots:
 | 
			
		||||
            _LOGGER.warning(
 | 
			
		||||
                "Bluetooth Proxy with active connections on Arduino framework has suboptimal performance.\n"
 | 
			
		||||
                "If BLE connections fail, they can waste connection slots for 10 seconds because\n"
 | 
			
		||||
                "Arduino doesn't allow configuring the BLE connection timeout (fixed at 30s).\n"
 | 
			
		||||
                "ESP-IDF framework allows setting it to 20s to match client timeouts.\n"
 | 
			
		||||
                "\n"
 | 
			
		||||
                "To switch to ESP-IDF, add this to your YAML:\n"
 | 
			
		||||
                "  esp32:\n"
 | 
			
		||||
                "    framework:\n"
 | 
			
		||||
                "      type: esp-idf\n"
 | 
			
		||||
                "\n"
 | 
			
		||||
                "For detailed migration instructions, see:\n"
 | 
			
		||||
                "%s",
 | 
			
		||||
                color(
 | 
			
		||||
                    AnsiFore.BLUE, "https://esphome.io/guides/esp32_arduino_to_idf.html"
 | 
			
		||||
                ),
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return {
 | 
			
		||||
            **config,
 | 
			
		||||
            CONF_CONNECTIONS: [CONNECTION_SCHEMA({}) for _ in range(connection_slots)],
 | 
			
		||||
@@ -118,12 +91,6 @@ async def to_code(config):
 | 
			
		||||
    connection_count = len(config.get(CONF_CONNECTIONS, []))
 | 
			
		||||
    cg.add_define("BLUETOOTH_PROXY_MAX_CONNECTIONS", connection_count)
 | 
			
		||||
 | 
			
		||||
    # Define batch size for BLE advertisements
 | 
			
		||||
    # Each advertisement is up to 80 bytes when packaged (including protocol overhead)
 | 
			
		||||
    # 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload
 | 
			
		||||
    # This achieves ~97% WiFi MTU utilization while staying under the limit
 | 
			
		||||
    cg.add_define("BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE", 16)
 | 
			
		||||
 | 
			
		||||
    for connection_conf in config.get(CONF_CONNECTIONS, []):
 | 
			
		||||
        connection_var = cg.new_Pvariable(connection_conf[CONF_ID])
 | 
			
		||||
        await cg.register_component(connection_var, connection_conf)
 | 
			
		||||
 
 | 
			
		||||
@@ -80,11 +80,9 @@ void BluetoothConnection::dump_config() {
 | 
			
		||||
 | 
			
		||||
void BluetoothConnection::update_allocated_slot_(uint64_t find_value, uint64_t set_value) {
 | 
			
		||||
  auto &allocated = this->proxy_->connections_free_response_.allocated;
 | 
			
		||||
  for (auto &slot : allocated) {
 | 
			
		||||
    if (slot == find_value) {
 | 
			
		||||
      slot = set_value;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  auto *it = std::find(allocated.begin(), allocated.end(), find_value);
 | 
			
		||||
  if (it != allocated.end()) {
 | 
			
		||||
    *it = set_value;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -107,24 +105,13 @@ void BluetoothConnection::set_address(uint64_t address) {
 | 
			
		||||
void BluetoothConnection::loop() {
 | 
			
		||||
  BLEClientBase::loop();
 | 
			
		||||
 | 
			
		||||
  // Early return if no active connection
 | 
			
		||||
  if (this->address_ == 0) {
 | 
			
		||||
  // Early return if no active connection or not in service discovery phase
 | 
			
		||||
  if (this->address_ == 0 || this->send_service_ < 0 || this->send_service_ > this->service_count_) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Handle service discovery if in valid range
 | 
			
		||||
  if (this->send_service_ >= 0 && this->send_service_ <= this->service_count_) {
 | 
			
		||||
    this->send_service_for_discovery_();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Check if we should disable the loop
 | 
			
		||||
  // - For V3_WITH_CACHE: Services are never sent, disable after INIT state
 | 
			
		||||
  // - For other connections: Disable only after service discovery is complete
 | 
			
		||||
  //   (send_service_ == DONE_SENDING_SERVICES, which is only set after services are sent)
 | 
			
		||||
  if (this->state_ != espbt::ClientState::INIT && (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
 | 
			
		||||
                                                   this->send_service_ == DONE_SENDING_SERVICES)) {
 | 
			
		||||
    this->disable_loop();
 | 
			
		||||
  }
 | 
			
		||||
  // Handle service discovery
 | 
			
		||||
  this->send_service_for_discovery_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothConnection::reset_connection_(esp_err_t reason) {
 | 
			
		||||
@@ -138,7 +125,7 @@ void BluetoothConnection::reset_connection_(esp_err_t reason) {
 | 
			
		||||
  // to detect incomplete service discovery rather than relying on us to
 | 
			
		||||
  // tell them about a partial list.
 | 
			
		||||
  this->set_address(0);
 | 
			
		||||
  this->send_service_ = INIT_SENDING_SERVICES;
 | 
			
		||||
  this->send_service_ = DONE_SENDING_SERVICES;
 | 
			
		||||
  this->proxy_->send_connections_free();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -198,7 +185,8 @@ void BluetoothConnection::send_service_for_discovery_() {
 | 
			
		||||
                                     service_result.start_handle, service_result.end_handle, 0, &total_char_count);
 | 
			
		||||
 | 
			
		||||
    if (char_count_status != ESP_GATT_OK) {
 | 
			
		||||
      this->log_connection_error_("esp_ble_gattc_get_attr_count", char_count_status);
 | 
			
		||||
      ESP_LOGE(TAG, "[%d] [%s] Error getting characteristic count, status=%d", this->connection_index_,
 | 
			
		||||
               this->address_str().c_str(), char_count_status);
 | 
			
		||||
      this->send_service_ = DONE_SENDING_SERVICES;
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
@@ -232,7 +220,8 @@ void BluetoothConnection::send_service_for_discovery_() {
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
        if (char_status != ESP_GATT_OK) {
 | 
			
		||||
          this->log_connection_error_("esp_ble_gattc_get_all_char", char_status);
 | 
			
		||||
          ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_,
 | 
			
		||||
                   this->address_str().c_str(), char_status);
 | 
			
		||||
          this->send_service_ = DONE_SENDING_SERVICES;
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
@@ -255,7 +244,8 @@ void BluetoothConnection::send_service_for_discovery_() {
 | 
			
		||||
            this->gattc_if_, this->conn_id_, ESP_GATT_DB_DESCRIPTOR, 0, 0, char_result.char_handle, &total_desc_count);
 | 
			
		||||
 | 
			
		||||
        if (desc_count_status != ESP_GATT_OK) {
 | 
			
		||||
          this->log_connection_error_("esp_ble_gattc_get_attr_count", desc_count_status);
 | 
			
		||||
          ESP_LOGE(TAG, "[%d] [%s] Error getting descriptor count for char handle %d, status=%d",
 | 
			
		||||
                   this->connection_index_, this->address_str().c_str(), char_result.char_handle, desc_count_status);
 | 
			
		||||
          this->send_service_ = DONE_SENDING_SERVICES;
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
@@ -276,7 +266,8 @@ void BluetoothConnection::send_service_for_discovery_() {
 | 
			
		||||
            break;
 | 
			
		||||
          }
 | 
			
		||||
          if (desc_status != ESP_GATT_OK) {
 | 
			
		||||
            this->log_connection_error_("esp_ble_gattc_get_all_descr", desc_status);
 | 
			
		||||
            ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_get_all_descr error, status=%d", this->connection_index_,
 | 
			
		||||
                     this->address_str().c_str(), desc_status);
 | 
			
		||||
            this->send_service_ = DONE_SENDING_SERVICES;
 | 
			
		||||
            return;
 | 
			
		||||
          }
 | 
			
		||||
@@ -330,33 +321,6 @@ void BluetoothConnection::send_service_for_discovery_() {
 | 
			
		||||
  api_conn->send_message(resp, api::BluetoothGATTGetServicesResponse::MESSAGE_TYPE);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothConnection::log_connection_error_(const char *operation, esp_gatt_status_t status) {
 | 
			
		||||
  ESP_LOGE(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str().c_str(), operation,
 | 
			
		||||
           status);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothConnection::log_connection_warning_(const char *operation, esp_err_t err) {
 | 
			
		||||
  ESP_LOGW(TAG, "[%d] [%s] %s failed, err=%d", this->connection_index_, this->address_str().c_str(), operation, err);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothConnection::log_gatt_not_connected_(const char *action, const char *type) {
 | 
			
		||||
  ESP_LOGW(TAG, "[%d] [%s] Cannot %s GATT %s, not connected.", this->connection_index_, this->address_str().c_str(),
 | 
			
		||||
           action, type);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothConnection::log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status) {
 | 
			
		||||
  ESP_LOGW(TAG, "[%d] [%s] Error %s for handle 0x%2X, status=%d", this->connection_index_, this->address_str().c_str(),
 | 
			
		||||
           operation, handle, status);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
esp_err_t BluetoothConnection::check_and_log_error_(const char *operation, esp_err_t err) {
 | 
			
		||||
  if (err != ESP_OK) {
 | 
			
		||||
    this->log_connection_warning_(operation, err);
 | 
			
		||||
    return err;
 | 
			
		||||
  }
 | 
			
		||||
  return ESP_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                                              esp_ble_gattc_cb_param_t *param) {
 | 
			
		||||
  if (!BLEClientBase::gattc_event_handler(event, gattc_if, param))
 | 
			
		||||
@@ -397,7 +361,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
 | 
			
		||||
    case ESP_GATTC_READ_DESCR_EVT:
 | 
			
		||||
    case ESP_GATTC_READ_CHAR_EVT: {
 | 
			
		||||
      if (param->read.status != ESP_GATT_OK) {
 | 
			
		||||
        this->log_gatt_operation_error_("reading char/descriptor", param->read.handle, param->read.status);
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] Error reading char/descriptor at handle 0x%2X, status=%d", this->connection_index_,
 | 
			
		||||
                 this->address_str_.c_str(), param->read.handle, param->read.status);
 | 
			
		||||
        this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
@@ -411,7 +376,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
 | 
			
		||||
    case ESP_GATTC_WRITE_CHAR_EVT:
 | 
			
		||||
    case ESP_GATTC_WRITE_DESCR_EVT: {
 | 
			
		||||
      if (param->write.status != ESP_GATT_OK) {
 | 
			
		||||
        this->log_gatt_operation_error_("writing char/descriptor", param->write.handle, param->write.status);
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] Error writing char/descriptor at handle 0x%2X, status=%d", this->connection_index_,
 | 
			
		||||
                 this->address_str_.c_str(), param->write.handle, param->write.status);
 | 
			
		||||
        this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
@@ -423,8 +389,9 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: {
 | 
			
		||||
      if (param->unreg_for_notify.status != ESP_GATT_OK) {
 | 
			
		||||
        this->log_gatt_operation_error_("unregistering notifications", param->unreg_for_notify.handle,
 | 
			
		||||
                                        param->unreg_for_notify.status);
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] Error unregistering notifications for handle 0x%2X, status=%d",
 | 
			
		||||
                 this->connection_index_, this->address_str_.c_str(), param->unreg_for_notify.handle,
 | 
			
		||||
                 param->unreg_for_notify.status);
 | 
			
		||||
        this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
@@ -436,8 +403,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
 | 
			
		||||
      if (param->reg_for_notify.status != ESP_GATT_OK) {
 | 
			
		||||
        this->log_gatt_operation_error_("registering notifications", param->reg_for_notify.handle,
 | 
			
		||||
                                        param->reg_for_notify.status);
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] Error registering notifications for handle 0x%2X, status=%d", this->connection_index_,
 | 
			
		||||
                 this->address_str_.c_str(), param->reg_for_notify.handle, param->reg_for_notify.status);
 | 
			
		||||
        this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
@@ -483,7 +450,8 @@ void BluetoothConnection::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl
 | 
			
		||||
 | 
			
		||||
esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
 | 
			
		||||
  if (!this->connected()) {
 | 
			
		||||
    this->log_gatt_not_connected_("read", "characteristic");
 | 
			
		||||
    ESP_LOGW(TAG, "[%d] [%s] Cannot read GATT characteristic, not connected.", this->connection_index_,
 | 
			
		||||
             this->address_str_.c_str());
 | 
			
		||||
    return ESP_GATT_NOT_CONNECTED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -491,12 +459,18 @@ esp_err_t BluetoothConnection::read_characteristic(uint16_t handle) {
 | 
			
		||||
           handle);
 | 
			
		||||
 | 
			
		||||
  esp_err_t err = esp_ble_gattc_read_char(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
  return this->check_and_log_error_("esp_ble_gattc_read_char", err);
 | 
			
		||||
  if (err != ERR_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_read_char error, err=%d", this->connection_index_,
 | 
			
		||||
             this->address_str_.c_str(), err);
 | 
			
		||||
    return err;
 | 
			
		||||
  }
 | 
			
		||||
  return ESP_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::string &data, bool response) {
 | 
			
		||||
  if (!this->connected()) {
 | 
			
		||||
    this->log_gatt_not_connected_("write", "characteristic");
 | 
			
		||||
    ESP_LOGW(TAG, "[%d] [%s] Cannot write GATT characteristic, not connected.", this->connection_index_,
 | 
			
		||||
             this->address_str_.c_str());
 | 
			
		||||
    return ESP_GATT_NOT_CONNECTED;
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGV(TAG, "[%d] [%s] Writing GATT characteristic handle %d", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
@@ -505,24 +479,36 @@ esp_err_t BluetoothConnection::write_characteristic(uint16_t handle, const std::
 | 
			
		||||
  esp_err_t err =
 | 
			
		||||
      esp_ble_gattc_write_char(this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(),
 | 
			
		||||
                               response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
  return this->check_and_log_error_("esp_ble_gattc_write_char", err);
 | 
			
		||||
  if (err != ERR_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char error, err=%d", this->connection_index_,
 | 
			
		||||
             this->address_str_.c_str(), err);
 | 
			
		||||
    return err;
 | 
			
		||||
  }
 | 
			
		||||
  return ESP_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
esp_err_t BluetoothConnection::read_descriptor(uint16_t handle) {
 | 
			
		||||
  if (!this->connected()) {
 | 
			
		||||
    this->log_gatt_not_connected_("read", "descriptor");
 | 
			
		||||
    ESP_LOGW(TAG, "[%d] [%s] Cannot read GATT descriptor, not connected.", this->connection_index_,
 | 
			
		||||
             this->address_str_.c_str());
 | 
			
		||||
    return ESP_GATT_NOT_CONNECTED;
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGV(TAG, "[%d] [%s] Reading GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
           handle);
 | 
			
		||||
 | 
			
		||||
  esp_err_t err = esp_ble_gattc_read_char_descr(this->gattc_if_, this->conn_id_, handle, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
  return this->check_and_log_error_("esp_ble_gattc_read_char_descr", err);
 | 
			
		||||
  if (err != ERR_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_read_char_descr error, err=%d", this->connection_index_,
 | 
			
		||||
             this->address_str_.c_str(), err);
 | 
			
		||||
    return err;
 | 
			
		||||
  }
 | 
			
		||||
  return ESP_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::string &data, bool response) {
 | 
			
		||||
  if (!this->connected()) {
 | 
			
		||||
    this->log_gatt_not_connected_("write", "descriptor");
 | 
			
		||||
    ESP_LOGW(TAG, "[%d] [%s] Cannot write GATT descriptor, not connected.", this->connection_index_,
 | 
			
		||||
             this->address_str_.c_str());
 | 
			
		||||
    return ESP_GATT_NOT_CONNECTED;
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGV(TAG, "[%d] [%s] Writing GATT descriptor handle %d", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
@@ -531,12 +517,18 @@ esp_err_t BluetoothConnection::write_descriptor(uint16_t handle, const std::stri
 | 
			
		||||
  esp_err_t err = esp_ble_gattc_write_char_descr(
 | 
			
		||||
      this->gattc_if_, this->conn_id_, handle, data.size(), (uint8_t *) data.data(),
 | 
			
		||||
      response ? ESP_GATT_WRITE_TYPE_RSP : ESP_GATT_WRITE_TYPE_NO_RSP, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
  return this->check_and_log_error_("esp_ble_gattc_write_char_descr", err);
 | 
			
		||||
  if (err != ERR_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, err=%d", this->connection_index_,
 | 
			
		||||
             this->address_str_.c_str(), err);
 | 
			
		||||
    return err;
 | 
			
		||||
  }
 | 
			
		||||
  return ESP_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enable) {
 | 
			
		||||
  if (!this->connected()) {
 | 
			
		||||
    this->log_gatt_not_connected_("notify", "characteristic");
 | 
			
		||||
    ESP_LOGW(TAG, "[%d] [%s] Cannot notify GATT characteristic, not connected.", this->connection_index_,
 | 
			
		||||
             this->address_str_.c_str());
 | 
			
		||||
    return ESP_GATT_NOT_CONNECTED;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -544,13 +536,22 @@ esp_err_t BluetoothConnection::notify_characteristic(uint16_t handle, bool enabl
 | 
			
		||||
    ESP_LOGV(TAG, "[%d] [%s] Registering for GATT characteristic notifications handle %d", this->connection_index_,
 | 
			
		||||
             this->address_str_.c_str(), handle);
 | 
			
		||||
    esp_err_t err = esp_ble_gattc_register_for_notify(this->gattc_if_, this->remote_bda_, handle);
 | 
			
		||||
    return this->check_and_log_error_("esp_ble_gattc_register_for_notify", err);
 | 
			
		||||
    if (err != ESP_OK) {
 | 
			
		||||
      ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_register_for_notify failed, err=%d", this->connection_index_,
 | 
			
		||||
               this->address_str_.c_str(), err);
 | 
			
		||||
      return err;
 | 
			
		||||
    }
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_,
 | 
			
		||||
             this->address_str_.c_str(), handle);
 | 
			
		||||
    esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle);
 | 
			
		||||
    if (err != ESP_OK) {
 | 
			
		||||
      ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_unregister_for_notify failed, err=%d", this->connection_index_,
 | 
			
		||||
               this->address_str_.c_str(), err);
 | 
			
		||||
      return err;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGV(TAG, "[%d] [%s] Unregistering for GATT characteristic notifications handle %d", this->connection_index_,
 | 
			
		||||
           this->address_str_.c_str(), handle);
 | 
			
		||||
  esp_err_t err = esp_ble_gattc_unregister_for_notify(this->gattc_if_, this->remote_bda_, handle);
 | 
			
		||||
  return this->check_and_log_error_("esp_ble_gattc_unregister_for_notify", err);
 | 
			
		||||
  return ESP_OK;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
esp32_ble_tracker::AdvertisementParserType BluetoothConnection::get_advertisement_parser_type() {
 | 
			
		||||
 
 | 
			
		||||
@@ -33,18 +33,13 @@ class BluetoothConnection : public esp32_ble_client::BLEClientBase {
 | 
			
		||||
  void send_service_for_discovery_();
 | 
			
		||||
  void reset_connection_(esp_err_t reason);
 | 
			
		||||
  void update_allocated_slot_(uint64_t find_value, uint64_t set_value);
 | 
			
		||||
  void log_connection_error_(const char *operation, esp_gatt_status_t status);
 | 
			
		||||
  void log_connection_warning_(const char *operation, esp_err_t err);
 | 
			
		||||
  void log_gatt_not_connected_(const char *action, const char *type);
 | 
			
		||||
  void log_gatt_operation_error_(const char *operation, uint16_t handle, esp_gatt_status_t status);
 | 
			
		||||
  esp_err_t check_and_log_error_(const char *operation, esp_err_t err);
 | 
			
		||||
 | 
			
		||||
  // Memory optimized layout for 32-bit systems
 | 
			
		||||
  // Group 1: Pointers (4 bytes each, naturally aligned)
 | 
			
		||||
  BluetoothProxy *proxy_;
 | 
			
		||||
 | 
			
		||||
  // Group 2: 2-byte types
 | 
			
		||||
  int16_t send_service_{-3};  // -3 = INIT_SENDING_SERVICES, -2 = DONE_SENDING_SERVICES, >=0 = service index
 | 
			
		||||
  int16_t send_service_{-2};  // Needs to handle negative values and service count
 | 
			
		||||
 | 
			
		||||
  // Group 3: 1-byte types
 | 
			
		||||
  bool seen_mtu_or_services_{false};
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,12 @@ namespace esphome::bluetooth_proxy {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "bluetooth_proxy";
 | 
			
		||||
 | 
			
		||||
// BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE is defined during code generation
 | 
			
		||||
// It sets the batch size for BLE advertisements to maximize WiFi efficiency
 | 
			
		||||
// Batch size for BLE advertisements to maximize WiFi efficiency
 | 
			
		||||
// Each advertisement is up to 80 bytes when packaged (including protocol overhead)
 | 
			
		||||
// Most advertisements are 20-30 bytes, allowing even more to fit per packet
 | 
			
		||||
// 16 advertisements × 80 bytes (worst case) = 1280 bytes out of ~1320 bytes usable payload
 | 
			
		||||
// This achieves ~97% WiFi MTU utilization while staying under the limit
 | 
			
		||||
static constexpr size_t FLUSH_BATCH_SIZE = 16;
 | 
			
		||||
 | 
			
		||||
// Verify BLE advertisement data array size matches the BLE specification (31 bytes adv + 31 bytes scan response)
 | 
			
		||||
static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62,
 | 
			
		||||
@@ -21,8 +25,18 @@ static_assert(sizeof(((api::BluetoothLERawAdvertisement *) nullptr)->data) == 62
 | 
			
		||||
BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; }
 | 
			
		||||
 | 
			
		||||
void BluetoothProxy::setup() {
 | 
			
		||||
  this->connections_free_response_.limit = BLUETOOTH_PROXY_MAX_CONNECTIONS;
 | 
			
		||||
  this->connections_free_response_.free = BLUETOOTH_PROXY_MAX_CONNECTIONS;
 | 
			
		||||
  // Pre-allocate response object
 | 
			
		||||
  this->response_ = std::make_unique<api::BluetoothLERawAdvertisementsResponse>();
 | 
			
		||||
 | 
			
		||||
  // Reserve capacity but start with size 0
 | 
			
		||||
  // Reserve 50% since we'll grow naturally and flush at FLUSH_BATCH_SIZE
 | 
			
		||||
  this->response_->advertisements.reserve(FLUSH_BATCH_SIZE / 2);
 | 
			
		||||
 | 
			
		||||
  // Don't pre-allocate pool - let it grow only if needed in busy environments
 | 
			
		||||
  // Many devices in quiet areas will never need the overflow pool
 | 
			
		||||
 | 
			
		||||
  this->connections_free_response_.limit = this->connections_.size();
 | 
			
		||||
  this->connections_free_response_.free = this->connections_.size();
 | 
			
		||||
 | 
			
		||||
  this->parent_->add_scanner_state_callback([this](esp32_ble_tracker::ScannerState state) {
 | 
			
		||||
    if (this->api_connection_ != nullptr) {
 | 
			
		||||
@@ -39,26 +53,6 @@ void BluetoothProxy::send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerSta
 | 
			
		||||
  this->api_connection_->send_message(resp, api::BluetoothScannerStateResponse::MESSAGE_TYPE);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothProxy::log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state) {
 | 
			
		||||
  ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, state: %s", connection->get_connection_index(),
 | 
			
		||||
           connection->address_str().c_str(), espbt::client_state_to_string(state));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothProxy::log_connection_info_(BluetoothConnection *connection, const char *message) {
 | 
			
		||||
  ESP_LOGI(TAG, "[%d] [%s] Connecting %s", connection->get_connection_index(), connection->address_str().c_str(),
 | 
			
		||||
           message);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothProxy::log_not_connected_gatt_(const char *action, const char *type) {
 | 
			
		||||
  ESP_LOGW(TAG, "Cannot %s GATT %s, not connected", action, type);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothProxy::handle_gatt_not_connected_(uint64_t address, uint16_t handle, const char *action,
 | 
			
		||||
                                                const char *type) {
 | 
			
		||||
  this->log_not_connected_gatt_(action, type);
 | 
			
		||||
  this->send_gatt_error(address, handle, ESP_GATT_NOT_CONNECTED);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) {
 | 
			
		||||
  // This method should never be called since bluetooth_proxy always uses raw advertisements
 | 
			
		||||
@@ -71,27 +65,39 @@ bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results,
 | 
			
		||||
  if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr)
 | 
			
		||||
    return false;
 | 
			
		||||
 | 
			
		||||
  auto &advertisements = this->response_.advertisements;
 | 
			
		||||
  auto &advertisements = this->response_->advertisements;
 | 
			
		||||
 | 
			
		||||
  for (size_t i = 0; i < count; i++) {
 | 
			
		||||
    auto &result = scan_results[i];
 | 
			
		||||
    uint8_t length = result.adv_data_len + result.scan_rsp_len;
 | 
			
		||||
 | 
			
		||||
    // Check if we need to expand the vector
 | 
			
		||||
    if (this->advertisement_count_ >= advertisements.size()) {
 | 
			
		||||
      if (this->advertisement_pool_.empty()) {
 | 
			
		||||
        // No room in pool, need to allocate
 | 
			
		||||
        advertisements.emplace_back();
 | 
			
		||||
      } else {
 | 
			
		||||
        // Pull from pool
 | 
			
		||||
        advertisements.push_back(std::move(this->advertisement_pool_.back()));
 | 
			
		||||
        this->advertisement_pool_.pop_back();
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Fill in the data directly at current position
 | 
			
		||||
    auto &adv = advertisements[this->response_.advertisements_len];
 | 
			
		||||
    auto &adv = advertisements[this->advertisement_count_];
 | 
			
		||||
    adv.address = esp32_ble::ble_addr_to_uint64(result.bda);
 | 
			
		||||
    adv.rssi = result.rssi;
 | 
			
		||||
    adv.address_type = result.ble_addr_type;
 | 
			
		||||
    adv.data_len = length;
 | 
			
		||||
    std::memcpy(adv.data, result.ble_adv, length);
 | 
			
		||||
 | 
			
		||||
    this->response_.advertisements_len++;
 | 
			
		||||
    this->advertisement_count_++;
 | 
			
		||||
 | 
			
		||||
    ESP_LOGV(TAG, "Queuing raw packet from %02X:%02X:%02X:%02X:%02X:%02X, length %d. RSSI: %d dB", result.bda[0],
 | 
			
		||||
             result.bda[1], result.bda[2], result.bda[3], result.bda[4], result.bda[5], length, result.rssi);
 | 
			
		||||
 | 
			
		||||
    // Flush if we have reached BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE
 | 
			
		||||
    if (this->response_.advertisements_len >= BLUETOOTH_PROXY_ADVERTISEMENT_BATCH_SIZE) {
 | 
			
		||||
    // Flush if we have reached FLUSH_BATCH_SIZE
 | 
			
		||||
    if (this->advertisement_count_ >= FLUSH_BATCH_SIZE) {
 | 
			
		||||
      this->flush_pending_advertisements();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
@@ -100,31 +106,40 @@ bool BluetoothProxy::parse_devices(const esp32_ble::BLEScanResult *scan_results,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothProxy::flush_pending_advertisements() {
 | 
			
		||||
  if (this->response_.advertisements_len == 0 || !api::global_api_server->is_connected() ||
 | 
			
		||||
      this->api_connection_ == nullptr)
 | 
			
		||||
  if (this->advertisement_count_ == 0 || !api::global_api_server->is_connected() || this->api_connection_ == nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  auto &advertisements = this->response_->advertisements;
 | 
			
		||||
 | 
			
		||||
  // Return any items beyond advertisement_count_ to the pool
 | 
			
		||||
  if (advertisements.size() > this->advertisement_count_) {
 | 
			
		||||
    // Move unused items back to pool
 | 
			
		||||
    this->advertisement_pool_.insert(this->advertisement_pool_.end(),
 | 
			
		||||
                                     std::make_move_iterator(advertisements.begin() + this->advertisement_count_),
 | 
			
		||||
                                     std::make_move_iterator(advertisements.end()));
 | 
			
		||||
 | 
			
		||||
    // Resize to actual count
 | 
			
		||||
    advertisements.resize(this->advertisement_count_);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Send the message
 | 
			
		||||
  this->api_connection_->send_message(this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE);
 | 
			
		||||
  this->api_connection_->send_message(*this->response_, api::BluetoothLERawAdvertisementsResponse::MESSAGE_TYPE);
 | 
			
		||||
 | 
			
		||||
  ESP_LOGV(TAG, "Sent batch of %u BLE advertisements", this->response_.advertisements_len);
 | 
			
		||||
 | 
			
		||||
  // Reset the length for the next batch
 | 
			
		||||
  this->response_.advertisements_len = 0;
 | 
			
		||||
  // Reset count - existing items will be overwritten in next batch
 | 
			
		||||
  this->advertisement_count_ = 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothProxy::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Bluetooth Proxy:");
 | 
			
		||||
  ESP_LOGCONFIG(TAG,
 | 
			
		||||
                "Bluetooth Proxy:\n"
 | 
			
		||||
                "  Active: %s\n"
 | 
			
		||||
                "  Connections: %d",
 | 
			
		||||
                YESNO(this->active_), this->connection_count_);
 | 
			
		||||
                YESNO(this->active_), this->connections_.size());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothProxy::loop() {
 | 
			
		||||
  if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) {
 | 
			
		||||
    for (uint8_t i = 0; i < this->connection_count_; i++) {
 | 
			
		||||
      auto *connection = this->connections_[i];
 | 
			
		||||
    for (auto *connection : this->connections_) {
 | 
			
		||||
      if (connection->get_address() != 0 && !connection->disconnect_pending()) {
 | 
			
		||||
        connection->disconnect();
 | 
			
		||||
      }
 | 
			
		||||
@@ -147,8 +162,7 @@ esp32_ble_tracker::AdvertisementParserType BluetoothProxy::get_advertisement_par
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool reserve) {
 | 
			
		||||
  for (uint8_t i = 0; i < this->connection_count_; i++) {
 | 
			
		||||
    auto *connection = this->connections_[i];
 | 
			
		||||
  for (auto *connection : this->connections_) {
 | 
			
		||||
    if (connection->get_address() == address)
 | 
			
		||||
      return connection;
 | 
			
		||||
  }
 | 
			
		||||
@@ -156,10 +170,9 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
 | 
			
		||||
  if (!reserve)
 | 
			
		||||
    return nullptr;
 | 
			
		||||
 | 
			
		||||
  for (uint8_t i = 0; i < this->connection_count_; i++) {
 | 
			
		||||
    auto *connection = this->connections_[i];
 | 
			
		||||
  for (auto *connection : this->connections_) {
 | 
			
		||||
    if (connection->get_address() == 0) {
 | 
			
		||||
      connection->send_service_ = INIT_SENDING_SERVICES;
 | 
			
		||||
      connection->send_service_ = DONE_SENDING_SERVICES;
 | 
			
		||||
      connection->set_address(address);
 | 
			
		||||
      // All connections must start at INIT
 | 
			
		||||
      // We only set the state if we allocate the connection
 | 
			
		||||
@@ -176,7 +189,8 @@ BluetoothConnection *BluetoothProxy::get_connection_(uint64_t address, bool rese
 | 
			
		||||
void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest &msg) {
 | 
			
		||||
  switch (msg.request_type) {
 | 
			
		||||
    case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE:
 | 
			
		||||
    case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE: {
 | 
			
		||||
    case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE:
 | 
			
		||||
    case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: {
 | 
			
		||||
      auto *connection = this->get_connection_(msg.address, true);
 | 
			
		||||
      if (connection == nullptr) {
 | 
			
		||||
        ESP_LOGW(TAG, "No free connections available");
 | 
			
		||||
@@ -185,10 +199,23 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
 | 
			
		||||
      }
 | 
			
		||||
      if (connection->state() == espbt::ClientState::CONNECTED ||
 | 
			
		||||
          connection->state() == espbt::ClientState::ESTABLISHED) {
 | 
			
		||||
        this->log_connection_request_ignored_(connection, connection->state());
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] Connection already established", connection->get_connection_index(),
 | 
			
		||||
                 connection->address_str().c_str());
 | 
			
		||||
        this->send_device_connection(msg.address, true);
 | 
			
		||||
        this->send_connections_free();
 | 
			
		||||
        return;
 | 
			
		||||
      } else if (connection->state() == espbt::ClientState::SEARCHING) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already searching for device",
 | 
			
		||||
                 connection->get_connection_index(), connection->address_str().c_str());
 | 
			
		||||
        return;
 | 
			
		||||
      } else if (connection->state() == espbt::ClientState::DISCOVERED) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, device already discovered",
 | 
			
		||||
                 connection->get_connection_index(), connection->address_str().c_str());
 | 
			
		||||
        return;
 | 
			
		||||
      } else if (connection->state() == espbt::ClientState::READY_TO_CONNECT) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, waiting in line to connect",
 | 
			
		||||
                 connection->get_connection_index(), connection->address_str().c_str());
 | 
			
		||||
        return;
 | 
			
		||||
      } else if (connection->state() == espbt::ClientState::CONNECTING) {
 | 
			
		||||
        if (connection->disconnect_pending()) {
 | 
			
		||||
          ESP_LOGW(TAG, "[%d] [%s] Connection request while pending disconnect, cancelling pending disconnect",
 | 
			
		||||
@@ -196,18 +223,29 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
 | 
			
		||||
          connection->cancel_pending_disconnect();
 | 
			
		||||
          return;
 | 
			
		||||
        }
 | 
			
		||||
        this->log_connection_request_ignored_(connection, connection->state());
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already connecting", connection->get_connection_index(),
 | 
			
		||||
                 connection->address_str().c_str());
 | 
			
		||||
        return;
 | 
			
		||||
      } else if (connection->state() == espbt::ClientState::DISCONNECTING) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, device is disconnecting",
 | 
			
		||||
                 connection->get_connection_index(), connection->address_str().c_str());
 | 
			
		||||
        return;
 | 
			
		||||
      } else if (connection->state() != espbt::ClientState::INIT) {
 | 
			
		||||
        this->log_connection_request_ignored_(connection, connection->state());
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] Connection already in progress", connection->get_connection_index(),
 | 
			
		||||
                 connection->address_str().c_str());
 | 
			
		||||
        return;
 | 
			
		||||
      }
 | 
			
		||||
      if (msg.request_type == api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITH_CACHE) {
 | 
			
		||||
        connection->set_connection_type(espbt::ConnectionType::V3_WITH_CACHE);
 | 
			
		||||
        this->log_connection_info_(connection, "v3 with cache");
 | 
			
		||||
      } else {  // BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE
 | 
			
		||||
        ESP_LOGI(TAG, "[%d] [%s] Connecting v3 with cache", connection->get_connection_index(),
 | 
			
		||||
                 connection->address_str().c_str());
 | 
			
		||||
      } else if (msg.request_type == api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT_V3_WITHOUT_CACHE) {
 | 
			
		||||
        connection->set_connection_type(espbt::ConnectionType::V3_WITHOUT_CACHE);
 | 
			
		||||
        this->log_connection_info_(connection, "v3 without cache");
 | 
			
		||||
        ESP_LOGI(TAG, "[%d] [%s] Connecting v3 without cache", connection->get_connection_index(),
 | 
			
		||||
                 connection->address_str().c_str());
 | 
			
		||||
      } else {
 | 
			
		||||
        connection->set_connection_type(espbt::ConnectionType::V1);
 | 
			
		||||
        ESP_LOGI(TAG, "[%d] [%s] Connecting v1", connection->get_connection_index(), connection->address_str().c_str());
 | 
			
		||||
      }
 | 
			
		||||
      if (msg.has_address_type) {
 | 
			
		||||
        uint64_to_bd_addr(msg.address, connection->remote_bda_);
 | 
			
		||||
@@ -269,18 +307,14 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest
 | 
			
		||||
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT: {
 | 
			
		||||
      ESP_LOGE(TAG, "V1 connections removed");
 | 
			
		||||
      this->send_device_connection(msg.address, false);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &msg) {
 | 
			
		||||
  auto *connection = this->get_connection_(msg.address, false);
 | 
			
		||||
  if (connection == nullptr) {
 | 
			
		||||
    this->handle_gatt_not_connected_(msg.address, msg.handle, "read", "characteristic");
 | 
			
		||||
    ESP_LOGW(TAG, "Cannot read GATT characteristic, not connected");
 | 
			
		||||
    this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -293,7 +327,8 @@ void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &ms
 | 
			
		||||
void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &msg) {
 | 
			
		||||
  auto *connection = this->get_connection_(msg.address, false);
 | 
			
		||||
  if (connection == nullptr) {
 | 
			
		||||
    this->handle_gatt_not_connected_(msg.address, msg.handle, "write", "characteristic");
 | 
			
		||||
    ESP_LOGW(TAG, "Cannot write GATT characteristic, not connected");
 | 
			
		||||
    this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -306,7 +341,8 @@ void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest &
 | 
			
		||||
void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTReadDescriptorRequest &msg) {
 | 
			
		||||
  auto *connection = this->get_connection_(msg.address, false);
 | 
			
		||||
  if (connection == nullptr) {
 | 
			
		||||
    this->handle_gatt_not_connected_(msg.address, msg.handle, "read", "descriptor");
 | 
			
		||||
    ESP_LOGW(TAG, "Cannot read GATT descriptor, not connected");
 | 
			
		||||
    this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -319,7 +355,8 @@ void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTRead
 | 
			
		||||
void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWriteDescriptorRequest &msg) {
 | 
			
		||||
  auto *connection = this->get_connection_(msg.address, false);
 | 
			
		||||
  if (connection == nullptr) {
 | 
			
		||||
    this->handle_gatt_not_connected_(msg.address, msg.handle, "write", "descriptor");
 | 
			
		||||
    ESP_LOGW(TAG, "Cannot write GATT descriptor, not connected");
 | 
			
		||||
    this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -332,7 +369,8 @@ void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWri
 | 
			
		||||
void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetServicesRequest &msg) {
 | 
			
		||||
  auto *connection = this->get_connection_(msg.address, false);
 | 
			
		||||
  if (connection == nullptr || !connection->connected()) {
 | 
			
		||||
    this->handle_gatt_not_connected_(msg.address, 0, "get", "services");
 | 
			
		||||
    ESP_LOGW(TAG, "Cannot get GATT services, not connected");
 | 
			
		||||
    this->send_gatt_error(msg.address, 0, ESP_GATT_NOT_CONNECTED);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (!connection->service_count_) {
 | 
			
		||||
@@ -340,14 +378,16 @@ void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetSer
 | 
			
		||||
    this->send_gatt_services_done(msg.address);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (connection->send_service_ == INIT_SENDING_SERVICES)  // Start sending services if not started yet
 | 
			
		||||
  if (connection->send_service_ ==
 | 
			
		||||
      DONE_SENDING_SERVICES)  // Only start sending services if we're not already sending them
 | 
			
		||||
    connection->send_service_ = 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest &msg) {
 | 
			
		||||
  auto *connection = this->get_connection_(msg.address, false);
 | 
			
		||||
  if (connection == nullptr) {
 | 
			
		||||
    this->handle_gatt_not_connected_(msg.address, msg.handle, "notify", "characteristic");
 | 
			
		||||
    ESP_LOGW(TAG, "Cannot notify GATT characteristic, not connected");
 | 
			
		||||
    this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED);
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,6 @@
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include <array>
 | 
			
		||||
#include <map>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
@@ -23,7 +22,6 @@ namespace esphome::bluetooth_proxy {
 | 
			
		||||
 | 
			
		||||
static const esp_err_t ESP_GATT_NOT_CONNECTED = -1;
 | 
			
		||||
static const int DONE_SENDING_SERVICES = -2;
 | 
			
		||||
static const int INIT_SENDING_SERVICES = -3;
 | 
			
		||||
 | 
			
		||||
using namespace esp32_ble_client;
 | 
			
		||||
 | 
			
		||||
@@ -65,10 +63,8 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
 | 
			
		||||
  esp32_ble_tracker::AdvertisementParserType get_advertisement_parser_type() override;
 | 
			
		||||
 | 
			
		||||
  void register_connection(BluetoothConnection *connection) {
 | 
			
		||||
    if (this->connection_count_ < BLUETOOTH_PROXY_MAX_CONNECTIONS) {
 | 
			
		||||
      this->connections_[this->connection_count_++] = connection;
 | 
			
		||||
      connection->proxy_ = this;
 | 
			
		||||
    }
 | 
			
		||||
    this->connections_.push_back(connection);
 | 
			
		||||
    connection->proxy_ = this;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void bluetooth_device_request(const api::BluetoothDeviceRequest &msg);
 | 
			
		||||
@@ -137,20 +133,17 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
 | 
			
		||||
  void send_bluetooth_scanner_state_(esp32_ble_tracker::ScannerState state);
 | 
			
		||||
 | 
			
		||||
  BluetoothConnection *get_connection_(uint64_t address, bool reserve);
 | 
			
		||||
  void log_connection_request_ignored_(BluetoothConnection *connection, espbt::ClientState state);
 | 
			
		||||
  void log_connection_info_(BluetoothConnection *connection, const char *message);
 | 
			
		||||
  void log_not_connected_gatt_(const char *action, const char *type);
 | 
			
		||||
  void handle_gatt_not_connected_(uint64_t address, uint16_t handle, const char *action, const char *type);
 | 
			
		||||
 | 
			
		||||
  // Memory optimized layout for 32-bit systems
 | 
			
		||||
  // Group 1: Pointers (4 bytes each, naturally aligned)
 | 
			
		||||
  api::APIConnection *api_connection_{nullptr};
 | 
			
		||||
 | 
			
		||||
  // Group 2: Fixed-size array of connection pointers
 | 
			
		||||
  std::array<BluetoothConnection *, BLUETOOTH_PROXY_MAX_CONNECTIONS> connections_{};
 | 
			
		||||
  // Group 2: Container types (typically 12 bytes on 32-bit)
 | 
			
		||||
  std::vector<BluetoothConnection *> connections_{};
 | 
			
		||||
 | 
			
		||||
  // BLE advertisement batching
 | 
			
		||||
  api::BluetoothLERawAdvertisementsResponse response_;
 | 
			
		||||
  std::vector<api::BluetoothLERawAdvertisement> advertisement_pool_;
 | 
			
		||||
  std::unique_ptr<api::BluetoothLERawAdvertisementsResponse> response_;
 | 
			
		||||
 | 
			
		||||
  // Group 3: 4-byte types
 | 
			
		||||
  uint32_t last_advertisement_flush_time_{0};
 | 
			
		||||
@@ -160,7 +153,7 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com
 | 
			
		||||
 | 
			
		||||
  // Group 4: 1-byte types grouped together
 | 
			
		||||
  bool active_;
 | 
			
		||||
  uint8_t connection_count_{0};
 | 
			
		||||
  uint8_t advertisement_count_{0};
 | 
			
		||||
  // 2 bytes used, 2 bytes padding
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -28,7 +28,7 @@ const float BME680_GAS_LOOKUP_TABLE_1[16] PROGMEM = {0.0, 0.0, 0.0,  0.0,  0.0,
 | 
			
		||||
const float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0,  0.0, 0.0, 0.0, 0.1, 0.7, 0.0, -0.8,
 | 
			
		||||
                                                     -0.1, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0};
 | 
			
		||||
 | 
			
		||||
[[maybe_unused]] static const char *oversampling_to_str(BME680Oversampling oversampling) {
 | 
			
		||||
static const char *oversampling_to_str(BME680Oversampling oversampling) {
 | 
			
		||||
  switch (oversampling) {
 | 
			
		||||
    case BME680_OVERSAMPLING_NONE:
 | 
			
		||||
      return "None";
 | 
			
		||||
@@ -47,7 +47,7 @@ const float BME680_GAS_LOOKUP_TABLE_2[16] PROGMEM = {0.0,  0.0, 0.0, 0.0, 0.1, 0
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
[[maybe_unused]] static const char *iir_filter_to_str(BME680IIRFilter filter) {
 | 
			
		||||
static const char *iir_filter_to_str(BME680IIRFilter filter) {
 | 
			
		||||
  switch (filter) {
 | 
			
		||||
    case BME680_IIR_FILTER_OFF:
 | 
			
		||||
      return "OFF";
 | 
			
		||||
 
 | 
			
		||||
@@ -14,7 +14,7 @@ from esphome.core import CORE, coroutine_with_priority
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["web_server_base", "ota.web_server"]
 | 
			
		||||
DEPENDENCIES = ["wifi"]
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
CODEOWNERS = ["@OttoWinter"]
 | 
			
		||||
 | 
			
		||||
captive_portal_ns = cg.esphome_ns.namespace("captive_portal")
 | 
			
		||||
CaptivePortal = captive_portal_ns.class_("CaptivePortal", cg.Component)
 | 
			
		||||
 
 | 
			
		||||
@@ -99,39 +99,43 @@ const optional<float> &CoverCall::get_tilt() const { return this->tilt_; }
 | 
			
		||||
const optional<bool> &CoverCall::get_toggle() const { return this->toggle_; }
 | 
			
		||||
void CoverCall::validate_() {
 | 
			
		||||
  auto traits = this->parent_->get_traits();
 | 
			
		||||
  const char *name = this->parent_->get_name().c_str();
 | 
			
		||||
 | 
			
		||||
  if (this->position_.has_value()) {
 | 
			
		||||
    auto pos = *this->position_;
 | 
			
		||||
    if (!traits.get_supports_position() && pos != COVER_OPEN && pos != COVER_CLOSED) {
 | 
			
		||||
      ESP_LOGW(TAG, "'%s': position unsupported", name);
 | 
			
		||||
      ESP_LOGW(TAG, "'%s' - This cover device does not support setting position!", this->parent_->get_name().c_str());
 | 
			
		||||
      this->position_.reset();
 | 
			
		||||
    } else if (pos < 0.0f || pos > 1.0f) {
 | 
			
		||||
      ESP_LOGW(TAG, "'%s': position %.2f out of range", name, pos);
 | 
			
		||||
      ESP_LOGW(TAG, "'%s' - Position %.2f is out of range [0.0 - 1.0]", this->parent_->get_name().c_str(), pos);
 | 
			
		||||
      this->position_ = clamp(pos, 0.0f, 1.0f);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (this->tilt_.has_value()) {
 | 
			
		||||
    auto tilt = *this->tilt_;
 | 
			
		||||
    if (!traits.get_supports_tilt()) {
 | 
			
		||||
      ESP_LOGW(TAG, "'%s': tilt unsupported", name);
 | 
			
		||||
      ESP_LOGW(TAG, "'%s' - This cover device does not support tilt!", this->parent_->get_name().c_str());
 | 
			
		||||
      this->tilt_.reset();
 | 
			
		||||
    } else if (tilt < 0.0f || tilt > 1.0f) {
 | 
			
		||||
      ESP_LOGW(TAG, "'%s': tilt %.2f out of range", name, tilt);
 | 
			
		||||
      ESP_LOGW(TAG, "'%s' - Tilt %.2f is out of range [0.0 - 1.0]", this->parent_->get_name().c_str(), tilt);
 | 
			
		||||
      this->tilt_ = clamp(tilt, 0.0f, 1.0f);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (this->toggle_.has_value()) {
 | 
			
		||||
    if (!traits.get_supports_toggle()) {
 | 
			
		||||
      ESP_LOGW(TAG, "'%s': toggle unsupported", name);
 | 
			
		||||
      ESP_LOGW(TAG, "'%s' - This cover device does not support toggle!", this->parent_->get_name().c_str());
 | 
			
		||||
      this->toggle_.reset();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (this->stop_) {
 | 
			
		||||
    if (this->position_.has_value() || this->tilt_.has_value() || this->toggle_.has_value()) {
 | 
			
		||||
      ESP_LOGW(TAG, "'%s': cannot position/tilt/toggle when stopping", name);
 | 
			
		||||
    if (this->position_.has_value()) {
 | 
			
		||||
      ESP_LOGW(TAG, "Cannot set position when stopping a cover!");
 | 
			
		||||
      this->position_.reset();
 | 
			
		||||
    }
 | 
			
		||||
    if (this->tilt_.has_value()) {
 | 
			
		||||
      ESP_LOGW(TAG, "Cannot set tilt when stopping a cover!");
 | 
			
		||||
      this->tilt_.reset();
 | 
			
		||||
    }
 | 
			
		||||
    if (this->toggle_.has_value()) {
 | 
			
		||||
      ESP_LOGW(TAG, "Cannot set toggle when stopping a cover!");
 | 
			
		||||
      this->toggle_.reset();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -13,7 +13,7 @@ from esphome.const import (
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
CODEOWNERS = ["@OttoWinter"]
 | 
			
		||||
DEPENDENCIES = ["logger"]
 | 
			
		||||
 | 
			
		||||
CONF_DEBUG_ID = "debug_id"
 | 
			
		||||
@@ -48,15 +48,6 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    if CORE.using_zephyr:
 | 
			
		||||
        zephyr_add_prj_conf("HWINFO", True)
 | 
			
		||||
        # gdb thread support
 | 
			
		||||
        zephyr_add_prj_conf("DEBUG_THREAD_INFO", True)
 | 
			
		||||
        # RTT
 | 
			
		||||
        zephyr_add_prj_conf("USE_SEGGER_RTT", True)
 | 
			
		||||
        zephyr_add_prj_conf("RTT_CONSOLE", True)
 | 
			
		||||
        zephyr_add_prj_conf("LOG", True)
 | 
			
		||||
        zephyr_add_prj_conf("LOG_BLOCK_IN_THREAD", True)
 | 
			
		||||
        zephyr_add_prj_conf("LOG_BUFFER_SIZE", 4096)
 | 
			
		||||
        zephyr_add_prj_conf("SEGGER_RTT_MODE_BLOCK_IF_FIFO_FULL", True)
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,4 @@
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
#include "driver/gpio.h"
 | 
			
		||||
#include "deep_sleep_component.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
@@ -75,20 +74,11 @@ void DeepSleepComponent::deep_sleep_() {
 | 
			
		||||
  if (this->sleep_duration_.has_value())
 | 
			
		||||
    esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
 | 
			
		||||
  if (this->wakeup_pin_ != nullptr) {
 | 
			
		||||
    const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin());
 | 
			
		||||
    if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLUP) {
 | 
			
		||||
      gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLUP_ONLY);
 | 
			
		||||
    } else if (this->wakeup_pin_->get_flags() & gpio::FLAG_PULLDOWN) {
 | 
			
		||||
      gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLDOWN_ONLY);
 | 
			
		||||
    }
 | 
			
		||||
    gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT);
 | 
			
		||||
    gpio_hold_en(gpio_pin);
 | 
			
		||||
    gpio_deep_sleep_hold_en();
 | 
			
		||||
    bool level = !this->wakeup_pin_->is_inverted();
 | 
			
		||||
    if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
 | 
			
		||||
      level = !level;
 | 
			
		||||
    }
 | 
			
		||||
    esp_sleep_enable_ext0_wakeup(gpio_pin, level);
 | 
			
		||||
    esp_sleep_enable_ext0_wakeup(gpio_num_t(this->wakeup_pin_->get_pin()), level);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->ext1_wakeup_.has_value()) {
 | 
			
		||||
    esp_sleep_enable_ext1_wakeup(this->ext1_wakeup_->mask, this->ext1_wakeup_->wakeup_mode);
 | 
			
		||||
@@ -112,15 +102,6 @@ void DeepSleepComponent::deep_sleep_() {
 | 
			
		||||
  if (this->sleep_duration_.has_value())
 | 
			
		||||
    esp_sleep_enable_timer_wakeup(*this->sleep_duration_);
 | 
			
		||||
  if (this->wakeup_pin_ != nullptr) {
 | 
			
		||||
    const auto gpio_pin = gpio_num_t(this->wakeup_pin_->get_pin());
 | 
			
		||||
    if (this->wakeup_pin_->get_flags() && gpio::FLAG_PULLUP) {
 | 
			
		||||
      gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLUP_ONLY);
 | 
			
		||||
    } else if (this->wakeup_pin_->get_flags() && gpio::FLAG_PULLDOWN) {
 | 
			
		||||
      gpio_sleep_set_pull_mode(gpio_pin, GPIO_PULLDOWN_ONLY);
 | 
			
		||||
    }
 | 
			
		||||
    gpio_sleep_set_direction(gpio_pin, GPIO_MODE_INPUT);
 | 
			
		||||
    gpio_hold_en(gpio_pin);
 | 
			
		||||
    gpio_deep_sleep_hold_en();
 | 
			
		||||
    bool level = !this->wakeup_pin_->is_inverted();
 | 
			
		||||
    if (this->wakeup_pin_mode_ == WAKEUP_PIN_MODE_INVERT_WAKEUP && this->wakeup_pin_->digital_read()) {
 | 
			
		||||
      level = !level;
 | 
			
		||||
 
 | 
			
		||||
@@ -23,7 +23,7 @@ void DelonghiClimate::transmit_state() {
 | 
			
		||||
 | 
			
		||||
  data->mark(DELONGHI_HEADER_MARK);
 | 
			
		||||
  data->space(DELONGHI_HEADER_SPACE);
 | 
			
		||||
  for (uint8_t b : remote_state) {
 | 
			
		||||
  for (unsigned char b : remote_state) {
 | 
			
		||||
    for (uint8_t mask = 1; mask > 0; mask <<= 1) {  // iterate through bit mask
 | 
			
		||||
      data->mark(DELONGHI_BIT_MARK);
 | 
			
		||||
      bool bit = b & mask;
 | 
			
		||||
 
 | 
			
		||||
@@ -12,8 +12,6 @@ from esphome.const import (
 | 
			
		||||
    CONF_ROTATION,
 | 
			
		||||
    CONF_TO,
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
    CONF_UPDATE_INTERVAL,
 | 
			
		||||
    SCHEDULER_DONT_RUN,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core import coroutine_with_priority
 | 
			
		||||
 | 
			
		||||
@@ -69,18 +67,6 @@ BASIC_DISPLAY_SCHEMA = cv.Schema(
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.polling_component_schema("1s"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _validate_test_card(config):
 | 
			
		||||
    if (
 | 
			
		||||
        config.get(CONF_SHOW_TEST_CARD, False)
 | 
			
		||||
        and config.get(CONF_UPDATE_INTERVAL, False) == SCHEDULER_DONT_RUN
 | 
			
		||||
    ):
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            f"`{CONF_SHOW_TEST_CARD}: True` cannot be used with `{CONF_UPDATE_INTERVAL}: never` because this combination will not show a test_card."
 | 
			
		||||
        )
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Optional(CONF_ROTATION): validate_rotation,
 | 
			
		||||
@@ -108,7 +94,6 @@ FULL_DISPLAY_SCHEMA = BASIC_DISPLAY_SCHEMA.extend(
 | 
			
		||||
        cv.Optional(CONF_SHOW_TEST_CARD): cv.boolean,
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
FULL_DISPLAY_SCHEMA.add_extra(_validate_test_card)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def setup_display_core_(var, config):
 | 
			
		||||
@@ -215,6 +200,7 @@ async def display_is_displaying_page_to_code(config, condition_id, template_arg,
 | 
			
		||||
    page = await cg.get_variable(config[CONF_PAGE_ID])
 | 
			
		||||
    var = cg.new_Pvariable(condition_id, template_arg, paren)
 | 
			
		||||
    cg.add(var.set_page(page))
 | 
			
		||||
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -15,7 +15,6 @@ from esphome.const import (
 | 
			
		||||
    CONF_FRAMEWORK,
 | 
			
		||||
    CONF_IGNORE_EFUSE_CUSTOM_MAC,
 | 
			
		||||
    CONF_IGNORE_EFUSE_MAC_CRC,
 | 
			
		||||
    CONF_LOG_LEVEL,
 | 
			
		||||
    CONF_NAME,
 | 
			
		||||
    CONF_PATH,
 | 
			
		||||
    CONF_PLATFORM_VERSION,
 | 
			
		||||
@@ -80,15 +79,6 @@ CONF_ENABLE_LWIP_ASSERT = "enable_lwip_assert"
 | 
			
		||||
CONF_EXECUTE_FROM_PSRAM = "execute_from_psram"
 | 
			
		||||
CONF_RELEASE = "release"
 | 
			
		||||
 | 
			
		||||
LOG_LEVELS_IDF = [
 | 
			
		||||
    "NONE",
 | 
			
		||||
    "ERROR",
 | 
			
		||||
    "WARN",
 | 
			
		||||
    "INFO",
 | 
			
		||||
    "DEBUG",
 | 
			
		||||
    "VERBOSE",
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
ASSERTION_LEVELS = {
 | 
			
		||||
    "DISABLE": "CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_DISABLE",
 | 
			
		||||
    "ENABLE": "CONFIG_COMPILER_OPTIMIZATION_ASSERTIONS_ENABLE",
 | 
			
		||||
@@ -633,9 +623,6 @@ ESP_IDF_FRAMEWORK_SCHEMA = cv.All(
 | 
			
		||||
            cv.Optional(CONF_SDKCONFIG_OPTIONS, default={}): {
 | 
			
		||||
                cv.string_strict: cv.string_strict
 | 
			
		||||
            },
 | 
			
		||||
            cv.Optional(CONF_LOG_LEVEL, default="ERROR"): cv.one_of(
 | 
			
		||||
                *LOG_LEVELS_IDF, upper=True
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ADVANCED, default={}): cv.Schema(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.Optional(CONF_ASSERTION_LEVEL): cv.one_of(
 | 
			
		||||
@@ -950,10 +937,6 @@ async def to_code(config):
 | 
			
		||||
            ),
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        add_idf_sdkconfig_option(
 | 
			
		||||
            f"CONFIG_LOG_DEFAULT_LEVEL_{conf[CONF_LOG_LEVEL]}", True
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
        for name, value in conf[CONF_SDKCONFIG_OPTIONS].items():
 | 
			
		||||
            add_idf_sdkconfig_option(name, RawSdkconfigValue(value))
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,8 +11,7 @@ from esphome.core.config import CONF_NAME_ADD_MAC_SUFFIX
 | 
			
		||||
import esphome.final_validate as fv
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["esp32"]
 | 
			
		||||
CODEOWNERS = ["@jesserockz", "@Rapsssito", "@bdraco"]
 | 
			
		||||
DOMAIN = "esp32_ble"
 | 
			
		||||
CODEOWNERS = ["@jesserockz", "@Rapsssito"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
class BTLoggers(Enum):
 | 
			
		||||
@@ -116,11 +115,9 @@ def register_bt_logger(*loggers: BTLoggers) -> None:
 | 
			
		||||
 | 
			
		||||
CONF_BLE_ID = "ble_id"
 | 
			
		||||
CONF_IO_CAPABILITY = "io_capability"
 | 
			
		||||
CONF_ADVERTISING = "advertising"
 | 
			
		||||
CONF_ADVERTISING_CYCLE_TIME = "advertising_cycle_time"
 | 
			
		||||
CONF_DISABLE_BT_LOGS = "disable_bt_logs"
 | 
			
		||||
CONF_CONNECTION_TIMEOUT = "connection_timeout"
 | 
			
		||||
CONF_MAX_NOTIFICATIONS = "max_notifications"
 | 
			
		||||
 | 
			
		||||
NO_BLUETOOTH_VARIANTS = [const.VARIANT_ESP32S2]
 | 
			
		||||
 | 
			
		||||
@@ -165,7 +162,6 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
            IO_CAPABILITY, lower=True
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ENABLE_ON_BOOT, default=True): cv.boolean,
 | 
			
		||||
        cv.Optional(CONF_ADVERTISING, default=False): cv.boolean,
 | 
			
		||||
        cv.Optional(
 | 
			
		||||
            CONF_ADVERTISING_CYCLE_TIME, default="10s"
 | 
			
		||||
        ): cv.positive_time_period_milliseconds,
 | 
			
		||||
@@ -177,11 +173,6 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
            cv.positive_time_period_seconds,
 | 
			
		||||
            cv.Range(min=TimePeriod(seconds=10), max=TimePeriod(seconds=180)),
 | 
			
		||||
        ),
 | 
			
		||||
        cv.SplitDefault(CONF_MAX_NOTIFICATIONS, esp32_idf=12): cv.All(
 | 
			
		||||
            cv.only_with_esp_idf,
 | 
			
		||||
            cv.positive_int,
 | 
			
		||||
            cv.Range(min=1, max=64),
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
 | 
			
		||||
@@ -281,20 +272,8 @@ async def to_code(config):
 | 
			
		||||
                "CONFIG_BT_BLE_ESTAB_LINK_CONN_TOUT", timeout_seconds
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        # Set the maximum number of notification registrations
 | 
			
		||||
        # This controls how many BLE characteristics can have notifications enabled
 | 
			
		||||
        # across all connections for a single GATT client interface
 | 
			
		||||
        # https://github.com/esphome/issues/issues/6808
 | 
			
		||||
        if CONF_MAX_NOTIFICATIONS in config:
 | 
			
		||||
            add_idf_sdkconfig_option(
 | 
			
		||||
                "CONFIG_BT_GATTC_NOTIF_REG_MAX", config[CONF_MAX_NOTIFICATIONS]
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    cg.add_define("USE_ESP32_BLE")
 | 
			
		||||
 | 
			
		||||
    if config[CONF_ADVERTISING]:
 | 
			
		||||
        cg.add_define("USE_ESP32_BLE_ADVERTISING")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_condition("ble.enabled", BLEEnabledCondition, cv.Schema({}))
 | 
			
		||||
async def ble_enabled_to_code(config, condition_id, template_arg, args):
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,7 @@
 | 
			
		||||
#include "ble.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#include "ble.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
@@ -53,7 +53,6 @@ void ESP32BLE::disable() {
 | 
			
		||||
 | 
			
		||||
bool ESP32BLE::is_active() { return this->state_ == BLE_COMPONENT_STATE_ACTIVE; }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_BLE_ADVERTISING
 | 
			
		||||
void ESP32BLE::advertising_start() {
 | 
			
		||||
  this->advertising_init_();
 | 
			
		||||
  if (!this->is_active())
 | 
			
		||||
@@ -89,7 +88,6 @@ void ESP32BLE::advertising_remove_service_uuid(ESPBTUUID uuid) {
 | 
			
		||||
  this->advertising_->remove_service_uuid(uuid);
 | 
			
		||||
  this->advertising_start();
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
bool ESP32BLE::ble_pre_setup_() {
 | 
			
		||||
  esp_err_t err = nvs_flash_init();
 | 
			
		||||
@@ -100,7 +98,6 @@ bool ESP32BLE::ble_pre_setup_() {
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_BLE_ADVERTISING
 | 
			
		||||
void ESP32BLE::advertising_init_() {
 | 
			
		||||
  if (this->advertising_ != nullptr)
 | 
			
		||||
    return;
 | 
			
		||||
@@ -110,7 +107,6 @@ void ESP32BLE::advertising_init_() {
 | 
			
		||||
  this->advertising_->set_min_preferred_interval(0x06);
 | 
			
		||||
  this->advertising_->set_appearance(this->appearance_);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
bool ESP32BLE::ble_setup_() {
 | 
			
		||||
  esp_err_t err;
 | 
			
		||||
@@ -398,11 +394,9 @@ void ESP32BLE::loop() {
 | 
			
		||||
    this->ble_event_pool_.release(ble_event);
 | 
			
		||||
    ble_event = this->ble_events_.pop();
 | 
			
		||||
  }
 | 
			
		||||
#ifdef USE_ESP32_BLE_ADVERTISING
 | 
			
		||||
  if (this->advertising_ != nullptr) {
 | 
			
		||||
    this->advertising_->loop();
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Log dropped events periodically
 | 
			
		||||
  uint16_t dropped = this->ble_events_.get_and_reset_dropped_count();
 | 
			
		||||
 
 | 
			
		||||
@@ -1,17 +1,14 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/defines.h"  // Must be included before conditional includes
 | 
			
		||||
 | 
			
		||||
#include "ble_advertising.h"
 | 
			
		||||
#include "ble_uuid.h"
 | 
			
		||||
#include "ble_scan_result.h"
 | 
			
		||||
#ifdef USE_ESP32_BLE_ADVERTISING
 | 
			
		||||
#include "ble_advertising.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include <functional>
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
#include "ble_event.h"
 | 
			
		||||
@@ -26,14 +23,21 @@
 | 
			
		||||
 | 
			
		||||
namespace esphome::esp32_ble {
 | 
			
		||||
 | 
			
		||||
// Maximum size of the BLE event queue
 | 
			
		||||
// Increased to absorb the ring buffer capacity from esp32_ble_tracker
 | 
			
		||||
// Maximum number of BLE scan results to buffer
 | 
			
		||||
// Sized to handle bursts of advertisements while allowing for processing delays
 | 
			
		||||
// With 16 advertisements per batch and some safety margin:
 | 
			
		||||
// - Without PSRAM: 24 entries (1.5× batch size)
 | 
			
		||||
// - With PSRAM: 36 entries (2.25× batch size)
 | 
			
		||||
// The reduced structure size (~80 bytes vs ~400 bytes) allows for larger buffers
 | 
			
		||||
#ifdef USE_PSRAM
 | 
			
		||||
static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 100;  // 64 + 36 (ring buffer size with PSRAM)
 | 
			
		||||
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 36;
 | 
			
		||||
#else
 | 
			
		||||
static constexpr uint8_t MAX_BLE_QUEUE_SIZE = 88;  // 64 + 24 (ring buffer size without PSRAM)
 | 
			
		||||
static constexpr uint8_t SCAN_RESULT_BUFFER_SIZE = 24;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
// Maximum size of the BLE event queue - must be power of 2 for lock-free queue
 | 
			
		||||
static constexpr size_t MAX_BLE_QUEUE_SIZE = 64;
 | 
			
		||||
 | 
			
		||||
uint64_t ble_addr_to_uint64(const esp_bd_addr_t address);
 | 
			
		||||
 | 
			
		||||
// NOLINTNEXTLINE(modernize-use-using)
 | 
			
		||||
@@ -109,7 +113,6 @@ class ESP32BLE : public Component {
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
  void set_name(const std::string &name) { this->name_ = name; }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_BLE_ADVERTISING
 | 
			
		||||
  void advertising_start();
 | 
			
		||||
  void advertising_set_service_data(const std::vector<uint8_t> &data);
 | 
			
		||||
  void advertising_set_manufacturer_data(const std::vector<uint8_t> &data);
 | 
			
		||||
@@ -117,7 +120,6 @@ class ESP32BLE : public Component {
 | 
			
		||||
  void advertising_add_service_uuid(ESPBTUUID uuid);
 | 
			
		||||
  void advertising_remove_service_uuid(ESPBTUUID uuid);
 | 
			
		||||
  void advertising_register_raw_advertisement_callback(std::function<void(bool)> &&callback);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  void register_gap_event_handler(GAPEventHandler *handler) { this->gap_event_handlers_.push_back(handler); }
 | 
			
		||||
  void register_gap_scan_event_handler(GAPScanEventHandler *handler) {
 | 
			
		||||
@@ -138,9 +140,7 @@ class ESP32BLE : public Component {
 | 
			
		||||
  bool ble_setup_();
 | 
			
		||||
  bool ble_dismantle_();
 | 
			
		||||
  bool ble_pre_setup_();
 | 
			
		||||
#ifdef USE_ESP32_BLE_ADVERTISING
 | 
			
		||||
  void advertising_init_();
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  template<typename... Args> friend void enqueue_ble_event(Args... args);
 | 
			
		||||
@@ -160,9 +160,7 @@ class ESP32BLE : public Component {
 | 
			
		||||
  optional<std::string> name_;
 | 
			
		||||
 | 
			
		||||
  // 4-byte aligned members
 | 
			
		||||
#ifdef USE_ESP32_BLE_ADVERTISING
 | 
			
		||||
  BLEAdvertising *advertising_{};  // 4 bytes (pointer)
 | 
			
		||||
#endif
 | 
			
		||||
  BLEAdvertising *advertising_{};             // 4 bytes (pointer)
 | 
			
		||||
  esp_ble_io_cap_t io_cap_{ESP_IO_CAP_NONE};  // 4 bytes (enum)
 | 
			
		||||
  uint32_t advertising_cycle_time_{};         // 4 bytes
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
#include "ble_advertising.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
#ifdef USE_ESP32_BLE_ADVERTISING
 | 
			
		||||
 | 
			
		||||
#include <cstdio>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
@@ -162,5 +161,4 @@ void BLEAdvertising::register_raw_advertisement_callback(std::function<void(bool
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::esp32_ble
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32_BLE_ADVERTISING
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,10 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
 | 
			
		||||
#include <array>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
#ifdef USE_ESP32_BLE_ADVERTISING
 | 
			
		||||
 | 
			
		||||
#include <esp_bt.h>
 | 
			
		||||
#include <esp_gap_ble_api.h>
 | 
			
		||||
@@ -59,5 +56,4 @@ class BLEAdvertising {
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::esp32_ble
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32_BLE_ADVERTISING
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -82,8 +82,6 @@ async def to_code(config):
 | 
			
		||||
    cg.add(var.set_measured_power(config[CONF_MEASURED_POWER]))
 | 
			
		||||
    cg.add(var.set_tx_power(config[CONF_TX_POWER]))
 | 
			
		||||
 | 
			
		||||
    cg.add_define("USE_ESP32_BLE_ADVERTISING")
 | 
			
		||||
 | 
			
		||||
    if CORE.using_esp_idf:
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_BT_BLE_42_FEATURES_SUPPORTED", True)
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import esphome.codegen as cg
 | 
			
		||||
from esphome.components import esp32_ble_tracker
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["esp32_ble_tracker"]
 | 
			
		||||
CODEOWNERS = ["@jesserockz", "@bdraco"]
 | 
			
		||||
CODEOWNERS = ["@jesserockz"]
 | 
			
		||||
DEPENDENCIES = ["esp32"]
 | 
			
		||||
 | 
			
		||||
esp32_ble_client_ns = cg.esphome_ns.namespace("esp32_ble_client")
 | 
			
		||||
 
 | 
			
		||||
@@ -5,9 +5,9 @@
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
 | 
			
		||||
namespace esphome::esp32_ble_client {
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble_client {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "esp32_ble_client";
 | 
			
		||||
 | 
			
		||||
@@ -93,7 +93,7 @@ esp_err_t BLECharacteristic::write_value(uint8_t *new_val, int16_t new_val_size)
 | 
			
		||||
  return write_value(new_val, new_val_size, ESP_GATT_WRITE_TYPE_NO_RSP);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::esp32_ble_client
 | 
			
		||||
}  // namespace esp32_ble_client
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32_BLE_DEVICE
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,6 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
 | 
			
		||||
 | 
			
		||||
@@ -11,7 +8,8 @@
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome::esp32_ble_client {
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble_client {
 | 
			
		||||
 | 
			
		||||
namespace espbt = esphome::esp32_ble_tracker;
 | 
			
		||||
 | 
			
		||||
@@ -35,7 +33,7 @@ class BLECharacteristic {
 | 
			
		||||
  BLEService *service;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::esp32_ble_client
 | 
			
		||||
}  // namespace esp32_ble_client
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32_BLE_DEVICE
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
 
 | 
			
		||||
@@ -8,7 +8,8 @@
 | 
			
		||||
#include <esp_gap_ble_api.h>
 | 
			
		||||
#include <esp_gatt_defs.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome::esp32_ble_client {
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble_client {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "esp32_ble_client";
 | 
			
		||||
 | 
			
		||||
@@ -78,7 +79,40 @@ void BLEClientBase::dump_config() {
 | 
			
		||||
                "  Address: %s\n"
 | 
			
		||||
                "  Auto-Connect: %s",
 | 
			
		||||
                this->address_str().c_str(), TRUEFALSE(this->auto_connect_));
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  State: %s", espbt::client_state_to_string(this->state()));
 | 
			
		||||
  std::string state_name;
 | 
			
		||||
  switch (this->state()) {
 | 
			
		||||
    case espbt::ClientState::INIT:
 | 
			
		||||
      state_name = "INIT";
 | 
			
		||||
      break;
 | 
			
		||||
    case espbt::ClientState::DISCONNECTING:
 | 
			
		||||
      state_name = "DISCONNECTING";
 | 
			
		||||
      break;
 | 
			
		||||
    case espbt::ClientState::IDLE:
 | 
			
		||||
      state_name = "IDLE";
 | 
			
		||||
      break;
 | 
			
		||||
    case espbt::ClientState::SEARCHING:
 | 
			
		||||
      state_name = "SEARCHING";
 | 
			
		||||
      break;
 | 
			
		||||
    case espbt::ClientState::DISCOVERED:
 | 
			
		||||
      state_name = "DISCOVERED";
 | 
			
		||||
      break;
 | 
			
		||||
    case espbt::ClientState::READY_TO_CONNECT:
 | 
			
		||||
      state_name = "READY_TO_CONNECT";
 | 
			
		||||
      break;
 | 
			
		||||
    case espbt::ClientState::CONNECTING:
 | 
			
		||||
      state_name = "CONNECTING";
 | 
			
		||||
      break;
 | 
			
		||||
    case espbt::ClientState::CONNECTED:
 | 
			
		||||
      state_name = "CONNECTED";
 | 
			
		||||
      break;
 | 
			
		||||
    case espbt::ClientState::ESTABLISHED:
 | 
			
		||||
      state_name = "ESTABLISHED";
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      state_name = "UNKNOWN_STATE";
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  State: %s", state_name.c_str());
 | 
			
		||||
  if (this->status_ == ESP_GATT_NO_RESOURCES) {
 | 
			
		||||
    ESP_LOGE(TAG, "  Failed due to no resources. Try to reduce number of BLE clients in config.");
 | 
			
		||||
  } else if (this->status_ != ESP_GATT_OK) {
 | 
			
		||||
@@ -107,55 +141,60 @@ bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
void BLEClientBase::connect() {
 | 
			
		||||
  ESP_LOGI(TAG, "[%d] [%s] 0x%02x Connecting", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
  ESP_LOGI(TAG, "[%d] [%s] 0x%02x Attempting BLE connection", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
           this->remote_addr_type_);
 | 
			
		||||
  this->paired_ = false;
 | 
			
		||||
 | 
			
		||||
  // Set preferred connection parameters before connecting
 | 
			
		||||
  // Use FAST for all V3 connections (better latency and reliability)
 | 
			
		||||
  // Use MEDIUM for V1/legacy connections (balanced performance)
 | 
			
		||||
  uint16_t min_interval, max_interval, timeout;
 | 
			
		||||
  const char *param_type;
 | 
			
		||||
 | 
			
		||||
  if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
 | 
			
		||||
      this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
 | 
			
		||||
    min_interval = FAST_MIN_CONN_INTERVAL;
 | 
			
		||||
    max_interval = FAST_MAX_CONN_INTERVAL;
 | 
			
		||||
    timeout = FAST_CONN_TIMEOUT;
 | 
			
		||||
    param_type = "fast";
 | 
			
		||||
  } else {
 | 
			
		||||
    min_interval = MEDIUM_MIN_CONN_INTERVAL;
 | 
			
		||||
    max_interval = MEDIUM_MAX_CONN_INTERVAL;
 | 
			
		||||
    timeout = MEDIUM_CONN_TIMEOUT;
 | 
			
		||||
    param_type = "medium";
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto param_ret = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval,
 | 
			
		||||
                                                      0,  // latency: 0
 | 
			
		||||
                                                      timeout);
 | 
			
		||||
  if (param_ret != ESP_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "[%d] [%s] esp_ble_gap_set_prefer_conn_params failed: %d", this->connection_index_,
 | 
			
		||||
             this->address_str_.c_str(), param_ret);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->log_connection_params_(param_type);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Now open the connection
 | 
			
		||||
  auto ret = esp_ble_gattc_open(this->gattc_if_, this->remote_bda_, this->remote_addr_type_, true);
 | 
			
		||||
  if (ret) {
 | 
			
		||||
    this->log_gattc_warning_("esp_ble_gattc_open", ret);
 | 
			
		||||
    ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_open error, status=%d", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
             ret);
 | 
			
		||||
    this->set_state(espbt::ClientState::IDLE);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->set_state(espbt::ClientState::CONNECTING);
 | 
			
		||||
 | 
			
		||||
    // Always set connection parameters to ensure stable operation
 | 
			
		||||
    // Use FAST for all V3 connections (better latency and reliability)
 | 
			
		||||
    // Use MEDIUM for V1/legacy connections (balanced performance)
 | 
			
		||||
    uint16_t min_interval, max_interval, timeout;
 | 
			
		||||
    const char *param_type;
 | 
			
		||||
 | 
			
		||||
    if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
 | 
			
		||||
        this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
 | 
			
		||||
      min_interval = FAST_MIN_CONN_INTERVAL;
 | 
			
		||||
      max_interval = FAST_MAX_CONN_INTERVAL;
 | 
			
		||||
      timeout = FAST_CONN_TIMEOUT;
 | 
			
		||||
      param_type = "fast";
 | 
			
		||||
    } else {
 | 
			
		||||
      min_interval = MEDIUM_MIN_CONN_INTERVAL;
 | 
			
		||||
      max_interval = MEDIUM_MAX_CONN_INTERVAL;
 | 
			
		||||
      timeout = MEDIUM_CONN_TIMEOUT;
 | 
			
		||||
      param_type = "medium";
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    auto param_ret = esp_ble_gap_set_prefer_conn_params(this->remote_bda_, min_interval, max_interval,
 | 
			
		||||
                                                        0,  // latency: 0
 | 
			
		||||
                                                        timeout);
 | 
			
		||||
    if (param_ret != ESP_OK) {
 | 
			
		||||
      ESP_LOGW(TAG, "[%d] [%s] esp_ble_gap_set_prefer_conn_params failed: %d", this->connection_index_,
 | 
			
		||||
               this->address_str_.c_str(), param_ret);
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGD(TAG, "[%d] [%s] Set %s conn params", this->connection_index_, this->address_str_.c_str(), param_type);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
esp_err_t BLEClientBase::pair() { return esp_ble_set_encryption(this->remote_bda_, ESP_BLE_SEC_ENCRYPT); }
 | 
			
		||||
 | 
			
		||||
void BLEClientBase::disconnect() {
 | 
			
		||||
  if (this->state_ == espbt::ClientState::IDLE || this->state_ == espbt::ClientState::DISCONNECTING) {
 | 
			
		||||
    ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already %s", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
             espbt::client_state_to_string(this->state_));
 | 
			
		||||
  if (this->state_ == espbt::ClientState::IDLE) {
 | 
			
		||||
    ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already idle.", this->connection_index_,
 | 
			
		||||
             this->address_str_.c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->state_ == espbt::ClientState::DISCONNECTING) {
 | 
			
		||||
    ESP_LOGI(TAG, "[%d] [%s] Disconnect requested, but already disconnecting.", this->connection_index_,
 | 
			
		||||
             this->address_str_.c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->state_ == espbt::ClientState::CONNECTING || this->conn_id_ == UNSET_CONN_ID) {
 | 
			
		||||
@@ -190,7 +229,8 @@ void BLEClientBase::unconditional_disconnect() {
 | 
			
		||||
    // In the future we might consider App.reboot() here since
 | 
			
		||||
    // the BLE stack is in an indeterminate state.
 | 
			
		||||
    //
 | 
			
		||||
    this->log_gattc_warning_("esp_ble_gattc_close", err);
 | 
			
		||||
    ESP_LOGE(TAG, "[%d] [%s] esp_ble_gattc_close error, err=%d", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
             err);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->state_ == espbt::ClientState::SEARCHING || this->state_ == espbt::ClientState::READY_TO_CONNECT ||
 | 
			
		||||
@@ -203,11 +243,9 @@ void BLEClientBase::unconditional_disconnect() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEClientBase::release_services() {
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
  for (auto &svc : this->services_)
 | 
			
		||||
    delete svc;  // NOLINT(cppcoreguidelines-owning-memory)
 | 
			
		||||
  this->services_.clear();
 | 
			
		||||
#endif
 | 
			
		||||
#ifndef CONFIG_BT_GATTC_CACHE_NVS_FLASH
 | 
			
		||||
  esp_ble_gattc_cache_clean(this->remote_bda_);
 | 
			
		||||
#endif
 | 
			
		||||
@@ -217,36 +255,6 @@ void BLEClientBase::log_event_(const char *name) {
 | 
			
		||||
  ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEClientBase::log_gattc_event_(const char *name) {
 | 
			
		||||
  ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_%s_EVT", this->connection_index_, this->address_str_.c_str(), name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEClientBase::log_gattc_warning_(const char *operation, esp_gatt_status_t status) {
 | 
			
		||||
  ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_.c_str(), operation,
 | 
			
		||||
           status);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEClientBase::log_gattc_warning_(const char *operation, esp_err_t err) {
 | 
			
		||||
  ESP_LOGW(TAG, "[%d] [%s] %s error, status=%d", this->connection_index_, this->address_str_.c_str(), operation, err);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEClientBase::log_connection_params_(const char *param_type) {
 | 
			
		||||
  ESP_LOGD(TAG, "[%d] [%s] %s conn params", this->connection_index_, this->address_str_.c_str(), param_type);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEClientBase::restore_medium_conn_params_() {
 | 
			
		||||
  // Restore to medium connection parameters after initial connection phase
 | 
			
		||||
  // This balances performance with bandwidth usage for normal operation
 | 
			
		||||
  esp_ble_conn_update_params_t conn_params = {{0}};
 | 
			
		||||
  memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
 | 
			
		||||
  conn_params.min_int = MEDIUM_MIN_CONN_INTERVAL;
 | 
			
		||||
  conn_params.max_int = MEDIUM_MAX_CONN_INTERVAL;
 | 
			
		||||
  conn_params.latency = 0;
 | 
			
		||||
  conn_params.timeout = MEDIUM_CONN_TIMEOUT;
 | 
			
		||||
  this->log_connection_params_("medium");
 | 
			
		||||
  esp_ble_gap_update_conn_params(&conn_params);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
 | 
			
		||||
                                        esp_ble_gattc_cb_param_t *param) {
 | 
			
		||||
  if (event == ESP_GATTC_REG_EVT && this->app_id != param->reg.app_id)
 | 
			
		||||
@@ -274,18 +282,30 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
    case ESP_GATTC_OPEN_EVT: {
 | 
			
		||||
      if (!this->check_addr(param->open.remote_bda))
 | 
			
		||||
        return false;
 | 
			
		||||
      this->log_gattc_event_("OPEN");
 | 
			
		||||
      // conn_id was already set in ESP_GATTC_CONNECT_EVT
 | 
			
		||||
      this->log_event_("ESP_GATTC_OPEN_EVT");
 | 
			
		||||
      this->conn_id_ = param->open.conn_id;
 | 
			
		||||
      this->service_count_ = 0;
 | 
			
		||||
      if (this->state_ != espbt::ClientState::CONNECTING) {
 | 
			
		||||
        // This should not happen but lets log it in case it does
 | 
			
		||||
        // because it means we have a bad assumption about how the
 | 
			
		||||
        // ESP BT stack works.
 | 
			
		||||
        ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while in %s state, status=%d", this->connection_index_,
 | 
			
		||||
                 this->address_str_.c_str(), espbt::client_state_to_string(this->state_), param->open.status);
 | 
			
		||||
        if (this->state_ == espbt::ClientState::CONNECTED) {
 | 
			
		||||
          ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while already connected, status=%d", this->connection_index_,
 | 
			
		||||
                   this->address_str_.c_str(), param->open.status);
 | 
			
		||||
        } else if (this->state_ == espbt::ClientState::ESTABLISHED) {
 | 
			
		||||
          ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while already established, status=%d",
 | 
			
		||||
                   this->connection_index_, this->address_str_.c_str(), param->open.status);
 | 
			
		||||
        } else if (this->state_ == espbt::ClientState::DISCONNECTING) {
 | 
			
		||||
          ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while disconnecting, status=%d", this->connection_index_,
 | 
			
		||||
                   this->address_str_.c_str(), param->open.status);
 | 
			
		||||
        } else {
 | 
			
		||||
          ESP_LOGE(TAG, "[%d] [%s] Got ESP_GATTC_OPEN_EVT while not in connecting state, status=%d",
 | 
			
		||||
                   this->connection_index_, this->address_str_.c_str(), param->open.status);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
      if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
 | 
			
		||||
        this->log_gattc_warning_("Connection open", param->open.status);
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] Connection failed, status=%d", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
                 param->open.status);
 | 
			
		||||
        this->set_state(espbt::ClientState::IDLE);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
@@ -297,12 +317,15 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
        this->conn_id_ = UNSET_CONN_ID;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      // MTU negotiation already started in ESP_GATTC_CONNECT_EVT
 | 
			
		||||
      auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->open.conn_id);
 | 
			
		||||
      if (ret) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_send_mtu_req failed, status=%x", this->connection_index_,
 | 
			
		||||
                 this->address_str_.c_str(), ret);
 | 
			
		||||
      }
 | 
			
		||||
      this->set_state(espbt::ClientState::CONNECTED);
 | 
			
		||||
      ESP_LOGI(TAG, "[%d] [%s] Connection open", this->connection_index_, this->address_str_.c_str());
 | 
			
		||||
      if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
 | 
			
		||||
        // Restore to medium connection parameters for cached connections too
 | 
			
		||||
        this->restore_medium_conn_params_();
 | 
			
		||||
        ESP_LOGI(TAG, "[%d] [%s] Using cached services", this->connection_index_, this->address_str_.c_str());
 | 
			
		||||
        // only set our state, subclients might have more stuff to do yet.
 | 
			
		||||
        this->state_ = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
        break;
 | 
			
		||||
@@ -314,16 +337,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
    case ESP_GATTC_CONNECT_EVT: {
 | 
			
		||||
      if (!this->check_addr(param->connect.remote_bda))
 | 
			
		||||
        return false;
 | 
			
		||||
      this->log_gattc_event_("CONNECT");
 | 
			
		||||
      this->conn_id_ = param->connect.conn_id;
 | 
			
		||||
      // Start MTU negotiation immediately as recommended by ESP-IDF examples
 | 
			
		||||
      // (gatt_client, ble_throughput) which call esp_ble_gattc_send_mtu_req in
 | 
			
		||||
      // ESP_GATTC_CONNECT_EVT instead of waiting for ESP_GATTC_OPEN_EVT.
 | 
			
		||||
      // This saves ~3ms in the connection process.
 | 
			
		||||
      auto ret = esp_ble_gattc_send_mtu_req(this->gattc_if_, param->connect.conn_id);
 | 
			
		||||
      if (ret) {
 | 
			
		||||
        this->log_gattc_warning_("esp_ble_gattc_send_mtu_req", ret);
 | 
			
		||||
      }
 | 
			
		||||
      this->log_event_("ESP_GATTC_CONNECT_EVT");
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_DISCONNECT_EVT: {
 | 
			
		||||
@@ -360,7 +374,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
    case ESP_GATTC_CLOSE_EVT: {
 | 
			
		||||
      if (this->conn_id_ != param->close.conn_id)
 | 
			
		||||
        return false;
 | 
			
		||||
      this->log_gattc_event_("CLOSE");
 | 
			
		||||
      this->log_event_("ESP_GATTC_CLOSE_EVT");
 | 
			
		||||
      this->release_services();
 | 
			
		||||
      this->set_state(espbt::ClientState::IDLE);
 | 
			
		||||
      this->conn_id_ = UNSET_CONN_ID;
 | 
			
		||||
@@ -372,74 +386,79 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
      this->service_count_++;
 | 
			
		||||
      if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
 | 
			
		||||
        // V3 clients don't need services initialized since
 | 
			
		||||
        // as they use the ESP APIs to get services.
 | 
			
		||||
        // they only request by handle after receiving the services.
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
      BLEService *ble_service = new BLEService();  // NOLINT(cppcoreguidelines-owning-memory)
 | 
			
		||||
      ble_service->uuid = espbt::ESPBTUUID::from_uuid(param->search_res.srvc_id.uuid);
 | 
			
		||||
      ble_service->start_handle = param->search_res.start_handle;
 | 
			
		||||
      ble_service->end_handle = param->search_res.end_handle;
 | 
			
		||||
      ble_service->client = this;
 | 
			
		||||
      this->services_.push_back(ble_service);
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_SEARCH_CMPL_EVT: {
 | 
			
		||||
      if (this->conn_id_ != param->search_cmpl.conn_id)
 | 
			
		||||
        return false;
 | 
			
		||||
      this->log_gattc_event_("SEARCH_CMPL");
 | 
			
		||||
      this->log_event_("ESP_GATTC_SEARCH_CMPL_EVT");
 | 
			
		||||
      for (auto &svc : this->services_) {
 | 
			
		||||
        ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
                 svc->uuid.to_string().c_str());
 | 
			
		||||
        ESP_LOGV(TAG, "[%d] [%s]  start_handle: 0x%x  end_handle: 0x%x", this->connection_index_,
 | 
			
		||||
                 this->address_str_.c_str(), svc->start_handle, svc->end_handle);
 | 
			
		||||
      }
 | 
			
		||||
      ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_.c_str());
 | 
			
		||||
 | 
			
		||||
      // For V3 connections, restore to medium connection parameters after service discovery
 | 
			
		||||
      // This balances performance with bandwidth usage after the critical discovery phase
 | 
			
		||||
      if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE ||
 | 
			
		||||
          this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
 | 
			
		||||
        this->restore_medium_conn_params_();
 | 
			
		||||
      } else {
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
        for (auto &svc : this->services_) {
 | 
			
		||||
          ESP_LOGV(TAG, "[%d] [%s] Service UUID: %s", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
                   svc->uuid.to_string().c_str());
 | 
			
		||||
          ESP_LOGV(TAG, "[%d] [%s]  start_handle: 0x%x  end_handle: 0x%x", this->connection_index_,
 | 
			
		||||
                   this->address_str_.c_str(), svc->start_handle, svc->end_handle);
 | 
			
		||||
        }
 | 
			
		||||
#endif
 | 
			
		||||
        esp_ble_conn_update_params_t conn_params = {{0}};
 | 
			
		||||
        memcpy(conn_params.bda, this->remote_bda_, sizeof(esp_bd_addr_t));
 | 
			
		||||
        conn_params.min_int = MEDIUM_MIN_CONN_INTERVAL;
 | 
			
		||||
        conn_params.max_int = MEDIUM_MAX_CONN_INTERVAL;
 | 
			
		||||
        conn_params.latency = 0;
 | 
			
		||||
        conn_params.timeout = MEDIUM_CONN_TIMEOUT;
 | 
			
		||||
        ESP_LOGD(TAG, "[%d] [%s] Restored medium conn params after service discovery", this->connection_index_,
 | 
			
		||||
                 this->address_str_.c_str());
 | 
			
		||||
        esp_ble_gap_update_conn_params(&conn_params);
 | 
			
		||||
      }
 | 
			
		||||
      ESP_LOGI(TAG, "[%d] [%s] Service discovery complete", this->connection_index_, this->address_str_.c_str());
 | 
			
		||||
 | 
			
		||||
      this->state_ = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_READ_DESCR_EVT: {
 | 
			
		||||
      if (this->conn_id_ != param->write.conn_id)
 | 
			
		||||
        return false;
 | 
			
		||||
      this->log_gattc_event_("READ_DESCR");
 | 
			
		||||
      this->log_event_("ESP_GATTC_READ_DESCR_EVT");
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_WRITE_DESCR_EVT: {
 | 
			
		||||
      if (this->conn_id_ != param->write.conn_id)
 | 
			
		||||
        return false;
 | 
			
		||||
      this->log_gattc_event_("WRITE_DESCR");
 | 
			
		||||
      this->log_event_("ESP_GATTC_WRITE_DESCR_EVT");
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_WRITE_CHAR_EVT: {
 | 
			
		||||
      if (this->conn_id_ != param->write.conn_id)
 | 
			
		||||
        return false;
 | 
			
		||||
      this->log_gattc_event_("WRITE_CHAR");
 | 
			
		||||
      this->log_event_("ESP_GATTC_WRITE_CHAR_EVT");
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_READ_CHAR_EVT: {
 | 
			
		||||
      if (this->conn_id_ != param->read.conn_id)
 | 
			
		||||
        return false;
 | 
			
		||||
      this->log_gattc_event_("READ_CHAR");
 | 
			
		||||
      this->log_event_("ESP_GATTC_READ_CHAR_EVT");
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_NOTIFY_EVT: {
 | 
			
		||||
      if (this->conn_id_ != param->notify.conn_id)
 | 
			
		||||
        return false;
 | 
			
		||||
      this->log_gattc_event_("NOTIFY");
 | 
			
		||||
      this->log_event_("ESP_GATTC_NOTIFY_EVT");
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
 | 
			
		||||
      this->log_gattc_event_("REG_FOR_NOTIFY");
 | 
			
		||||
      this->log_event_("ESP_GATTC_REG_FOR_NOTIFY_EVT");
 | 
			
		||||
      if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE ||
 | 
			
		||||
          this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
 | 
			
		||||
        // Client is responsible for flipping the descriptor value
 | 
			
		||||
@@ -451,7 +470,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
      esp_gatt_status_t descr_status = esp_ble_gattc_get_descr_by_char_handle(
 | 
			
		||||
          this->gattc_if_, this->conn_id_, param->reg_for_notify.handle, NOTIFY_DESC_UUID, &desc_result, &count);
 | 
			
		||||
      if (descr_status != ESP_GATT_OK) {
 | 
			
		||||
        this->log_gattc_warning_("esp_ble_gattc_get_descr_by_char_handle", descr_status);
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_descr_by_char_handle error, status=%d", this->connection_index_,
 | 
			
		||||
                 this->address_str_.c_str(), descr_status);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      esp_gattc_char_elem_t char_result;
 | 
			
		||||
@@ -459,7 +479,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
          esp_ble_gattc_get_all_char(this->gattc_if_, this->conn_id_, param->reg_for_notify.handle,
 | 
			
		||||
                                     param->reg_for_notify.handle, &char_result, &count, 0);
 | 
			
		||||
      if (char_status != ESP_GATT_OK) {
 | 
			
		||||
        this->log_gattc_warning_("esp_ble_gattc_get_all_char", char_status);
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_,
 | 
			
		||||
                 this->address_str_.c_str(), char_status);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
@@ -473,7 +494,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
                                         (uint8_t *) ¬ify_en, ESP_GATT_WRITE_TYPE_RSP, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
      ESP_LOGD(TAG, "Wrote notify descriptor %d, properties=%d", notify_en, char_result.properties);
 | 
			
		||||
      if (status) {
 | 
			
		||||
        this->log_gattc_warning_("esp_ble_gattc_write_char_descr", status);
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, status=%d", this->connection_index_,
 | 
			
		||||
                 this->address_str_.c_str(), status);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
@@ -581,7 +603,6 @@ float BLEClientBase::parse_char_value(uint8_t *value, uint16_t length) {
 | 
			
		||||
  return NAN;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
BLEService *BLEClientBase::get_service(espbt::ESPBTUUID uuid) {
 | 
			
		||||
  for (auto *svc : this->services_) {
 | 
			
		||||
    if (svc->uuid == uuid)
 | 
			
		||||
@@ -658,8 +679,8 @@ BLEDescriptor *BLEClientBase::get_descriptor(uint16_t handle) {
 | 
			
		||||
  }
 | 
			
		||||
  return nullptr;
 | 
			
		||||
}
 | 
			
		||||
#endif  // USE_ESP32_BLE_DEVICE
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::esp32_ble_client
 | 
			
		||||
}  // namespace esp32_ble_client
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
 
 | 
			
		||||
@@ -5,9 +5,7 @@
 | 
			
		||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
#include "ble_service.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include <array>
 | 
			
		||||
#include <string>
 | 
			
		||||
@@ -18,7 +16,8 @@
 | 
			
		||||
#include <esp_gatt_common_api.h>
 | 
			
		||||
#include <esp_gattc_api.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome::esp32_ble_client {
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble_client {
 | 
			
		||||
 | 
			
		||||
namespace espbt = esphome::esp32_ble_tracker;
 | 
			
		||||
 | 
			
		||||
@@ -67,9 +66,8 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
 | 
			
		||||
                       (uint8_t) (this->address_ >> 0) & 0xff);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  const std::string &address_str() const { return this->address_str_; }
 | 
			
		||||
  std::string address_str() const { return this->address_str_; }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
  BLEService *get_service(espbt::ESPBTUUID uuid);
 | 
			
		||||
  BLEService *get_service(uint16_t uuid);
 | 
			
		||||
  BLECharacteristic *get_characteristic(espbt::ESPBTUUID service, espbt::ESPBTUUID chr);
 | 
			
		||||
@@ -80,7 +78,6 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
 | 
			
		||||
  BLEDescriptor *get_descriptor(uint16_t handle);
 | 
			
		||||
  // Get the configuration descriptor for the given characteristic handle.
 | 
			
		||||
  BLEDescriptor *get_config_descriptor(uint16_t handle);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  float parse_char_value(uint8_t *value, uint16_t length);
 | 
			
		||||
 | 
			
		||||
@@ -107,9 +104,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
 | 
			
		||||
 | 
			
		||||
  // Group 2: Container types (grouped for memory optimization)
 | 
			
		||||
  std::string address_str_{};
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
  std::vector<BLEService *> services_;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Group 3: 4-byte types
 | 
			
		||||
  int gattc_if_;
 | 
			
		||||
@@ -132,13 +127,9 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
 | 
			
		||||
  // 6 bytes used, 2 bytes padding
 | 
			
		||||
 | 
			
		||||
  void log_event_(const char *name);
 | 
			
		||||
  void log_gattc_event_(const char *name);
 | 
			
		||||
  void restore_medium_conn_params_();
 | 
			
		||||
  void log_gattc_warning_(const char *operation, esp_gatt_status_t status);
 | 
			
		||||
  void log_gattc_warning_(const char *operation, esp_err_t err);
 | 
			
		||||
  void log_connection_params_(const char *param_type);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::esp32_ble_client
 | 
			
		||||
}  // namespace esp32_ble_client
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,11 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome::esp32_ble_client {
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble_client {
 | 
			
		||||
 | 
			
		||||
namespace espbt = esphome::esp32_ble_tracker;
 | 
			
		||||
 | 
			
		||||
@@ -21,7 +19,7 @@ class BLEDescriptor {
 | 
			
		||||
  BLECharacteristic *characteristic;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::esp32_ble_client
 | 
			
		||||
}  // namespace esp32_ble_client
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32_BLE_DEVICE
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
 
 | 
			
		||||
@@ -4,9 +4,9 @@
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
 | 
			
		||||
namespace esphome::esp32_ble_client {
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble_client {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "esp32_ble_client";
 | 
			
		||||
 | 
			
		||||
@@ -71,7 +71,7 @@ void BLEService::parse_characteristics() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::esp32_ble_client
 | 
			
		||||
}  // namespace esp32_ble_client
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32_BLE_DEVICE
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,6 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h"
 | 
			
		||||
 | 
			
		||||
@@ -11,7 +8,8 @@
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome::esp32_ble_client {
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_ble_client {
 | 
			
		||||
 | 
			
		||||
namespace espbt = esphome::esp32_ble_tracker;
 | 
			
		||||
 | 
			
		||||
@@ -32,7 +30,7 @@ class BLEService {
 | 
			
		||||
  BLECharacteristic *get_characteristic(uint16_t uuid);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::esp32_ble_client
 | 
			
		||||
}  // namespace esp32_ble_client
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32_BLE_DEVICE
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
 
 | 
			
		||||
@@ -571,7 +571,6 @@ async def to_code(config):
 | 
			
		||||
            config[CONF_ON_DISCONNECT],
 | 
			
		||||
        )
 | 
			
		||||
    cg.add_define("USE_ESP32_BLE_SERVER")
 | 
			
		||||
    cg.add_define("USE_ESP32_BLE_ADVERTISING")
 | 
			
		||||
    if CORE.using_esp_idf:
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_BT_ENABLED", True)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -36,7 +36,6 @@ from esphome.types import ConfigType
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["esp32_ble"]
 | 
			
		||||
DEPENDENCIES = ["esp32"]
 | 
			
		||||
CODEOWNERS = ["@bdraco"]
 | 
			
		||||
 | 
			
		||||
KEY_ESP32_BLE_TRACKER = "esp32_ble_tracker"
 | 
			
		||||
KEY_USED_CONNECTION_SLOTS = "used_connection_slots"
 | 
			
		||||
@@ -355,6 +354,11 @@ async def to_code(config):
 | 
			
		||||
        add_idf_sdkconfig_option(
 | 
			
		||||
            "CONFIG_BTDM_CTRL_BLE_MAX_CONN", config[CONF_MAX_CONNECTIONS]
 | 
			
		||||
        )
 | 
			
		||||
        # CONFIG_BT_GATTC_NOTIF_REG_MAX controls the number of
 | 
			
		||||
        # max notifications in 5.x, setting CONFIG_BT_ACL_CONNECTIONS
 | 
			
		||||
        # is enough in 4.x
 | 
			
		||||
        # https://github.com/esphome/issues/issues/6808
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_BT_GATTC_NOTIF_REG_MAX", 9)
 | 
			
		||||
 | 
			
		||||
    cg.add_define("USE_OTA_STATE_CALLBACK")  # To be notified when an OTA update starts
 | 
			
		||||
    cg.add_define("USE_ESP32_BLE_CLIENT")
 | 
			
		||||
 
 | 
			
		||||
@@ -41,31 +41,6 @@ static const char *const TAG = "esp32_ble_tracker";
 | 
			
		||||
 | 
			
		||||
ESP32BLETracker *global_esp32_ble_tracker = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables)
 | 
			
		||||
 | 
			
		||||
const char *client_state_to_string(ClientState state) {
 | 
			
		||||
  switch (state) {
 | 
			
		||||
    case ClientState::INIT:
 | 
			
		||||
      return "INIT";
 | 
			
		||||
    case ClientState::DISCONNECTING:
 | 
			
		||||
      return "DISCONNECTING";
 | 
			
		||||
    case ClientState::IDLE:
 | 
			
		||||
      return "IDLE";
 | 
			
		||||
    case ClientState::SEARCHING:
 | 
			
		||||
      return "SEARCHING";
 | 
			
		||||
    case ClientState::DISCOVERED:
 | 
			
		||||
      return "DISCOVERED";
 | 
			
		||||
    case ClientState::READY_TO_CONNECT:
 | 
			
		||||
      return "READY_TO_CONNECT";
 | 
			
		||||
    case ClientState::CONNECTING:
 | 
			
		||||
      return "CONNECTING";
 | 
			
		||||
    case ClientState::CONNECTED:
 | 
			
		||||
      return "CONNECTED";
 | 
			
		||||
    case ClientState::ESTABLISHED:
 | 
			
		||||
      return "ESTABLISHED";
 | 
			
		||||
    default:
 | 
			
		||||
      return "UNKNOWN";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float ESP32BLETracker::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
 | 
			
		||||
 | 
			
		||||
void ESP32BLETracker::setup() {
 | 
			
		||||
@@ -74,6 +49,13 @@ void ESP32BLETracker::setup() {
 | 
			
		||||
    ESP_LOGE(TAG, "BLE Tracker was marked failed by ESP32BLE");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  RAMAllocator<BLEScanResult> allocator;
 | 
			
		||||
  this->scan_ring_buffer_ = allocator.allocate(SCAN_RESULT_BUFFER_SIZE);
 | 
			
		||||
 | 
			
		||||
  if (this->scan_ring_buffer_ == nullptr) {
 | 
			
		||||
    ESP_LOGE(TAG, "Could not allocate ring buffer for BLE Tracker!");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  global_esp32_ble_tracker = this;
 | 
			
		||||
 | 
			
		||||
@@ -101,49 +83,124 @@ void ESP32BLETracker::loop() {
 | 
			
		||||
      this->start_scan();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Check for scan timeout - moved here from scheduler to avoid false reboots
 | 
			
		||||
  // when the loop is blocked
 | 
			
		||||
  if (this->scanner_state_ == ScannerState::RUNNING) {
 | 
			
		||||
    switch (this->scan_timeout_state_) {
 | 
			
		||||
      case ScanTimeoutState::MONITORING: {
 | 
			
		||||
        uint32_t now = App.get_loop_component_start_time();
 | 
			
		||||
        uint32_t timeout_ms = this->scan_duration_ * 2000;
 | 
			
		||||
        // Robust time comparison that handles rollover correctly
 | 
			
		||||
        // This works because unsigned arithmetic wraps around predictably
 | 
			
		||||
        if ((now - this->scan_start_time_) > timeout_ms) {
 | 
			
		||||
          // First time we've seen the timeout exceeded - wait one more loop iteration
 | 
			
		||||
          // This ensures all components have had a chance to process pending events
 | 
			
		||||
          // This is because esp32_ble may not have run yet and called
 | 
			
		||||
          // gap_scan_event_handler yet when the loop unblocks
 | 
			
		||||
          ESP_LOGW(TAG, "Scan timeout exceeded");
 | 
			
		||||
          this->scan_timeout_state_ = ScanTimeoutState::EXCEEDED_WAIT;
 | 
			
		||||
        }
 | 
			
		||||
  int connecting = 0;
 | 
			
		||||
  int discovered = 0;
 | 
			
		||||
  int searching = 0;
 | 
			
		||||
  int disconnecting = 0;
 | 
			
		||||
  for (auto *client : this->clients_) {
 | 
			
		||||
    switch (client->state()) {
 | 
			
		||||
      case ClientState::DISCONNECTING:
 | 
			
		||||
        disconnecting++;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      case ScanTimeoutState::EXCEEDED_WAIT:
 | 
			
		||||
        // We've waited at least one full loop iteration, and scan is still running
 | 
			
		||||
        ESP_LOGE(TAG, "Scan never terminated, rebooting");
 | 
			
		||||
        App.reboot();
 | 
			
		||||
      case ClientState::DISCOVERED:
 | 
			
		||||
        discovered++;
 | 
			
		||||
        break;
 | 
			
		||||
 | 
			
		||||
      case ScanTimeoutState::INACTIVE:
 | 
			
		||||
        // This case should be unreachable - scanner and timeout states are always synchronized
 | 
			
		||||
      case ClientState::SEARCHING:
 | 
			
		||||
        searching++;
 | 
			
		||||
        break;
 | 
			
		||||
      case ClientState::CONNECTING:
 | 
			
		||||
      case ClientState::READY_TO_CONNECT:
 | 
			
		||||
        connecting++;
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ClientStateCounts counts = this->count_client_states_();
 | 
			
		||||
  if (counts != this->client_state_counts_) {
 | 
			
		||||
    this->client_state_counts_ = counts;
 | 
			
		||||
    ESP_LOGD(TAG, "connecting: %d, discovered: %d, searching: %d, disconnecting: %d",
 | 
			
		||||
             this->client_state_counts_.connecting, this->client_state_counts_.discovered,
 | 
			
		||||
             this->client_state_counts_.searching, this->client_state_counts_.disconnecting);
 | 
			
		||||
  if (connecting != connecting_ || discovered != discovered_ || searching != searching_ ||
 | 
			
		||||
      disconnecting != disconnecting_) {
 | 
			
		||||
    connecting_ = connecting;
 | 
			
		||||
    discovered_ = discovered;
 | 
			
		||||
    searching_ = searching;
 | 
			
		||||
    disconnecting_ = disconnecting;
 | 
			
		||||
    ESP_LOGD(TAG, "connecting: %d, discovered: %d, searching: %d, disconnecting: %d", connecting_, discovered_,
 | 
			
		||||
             searching_, disconnecting_);
 | 
			
		||||
  }
 | 
			
		||||
  bool promote_to_connecting = discovered && !searching && !connecting;
 | 
			
		||||
 | 
			
		||||
  // Process scan results from lock-free SPSC ring buffer
 | 
			
		||||
  // Consumer side: This runs in the main loop thread
 | 
			
		||||
  if (this->scanner_state_ == ScannerState::RUNNING) {
 | 
			
		||||
    // Load our own index with relaxed ordering (we're the only writer)
 | 
			
		||||
    uint8_t read_idx = this->ring_read_index_.load(std::memory_order_relaxed);
 | 
			
		||||
 | 
			
		||||
    // Load producer's index with acquire to see their latest writes
 | 
			
		||||
    uint8_t write_idx = this->ring_write_index_.load(std::memory_order_acquire);
 | 
			
		||||
 | 
			
		||||
    while (read_idx != write_idx) {
 | 
			
		||||
      // Calculate how many contiguous results we can process in one batch
 | 
			
		||||
      // If write > read: process all results from read to write
 | 
			
		||||
      // If write <= read (wraparound): process from read to end of buffer first
 | 
			
		||||
      size_t batch_size = (write_idx > read_idx) ? (write_idx - read_idx) : (SCAN_RESULT_BUFFER_SIZE - read_idx);
 | 
			
		||||
 | 
			
		||||
      // Process the batch for raw advertisements
 | 
			
		||||
      if (this->raw_advertisements_) {
 | 
			
		||||
        for (auto *listener : this->listeners_) {
 | 
			
		||||
          listener->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size);
 | 
			
		||||
        }
 | 
			
		||||
        for (auto *client : this->clients_) {
 | 
			
		||||
          client->parse_devices(&this->scan_ring_buffer_[read_idx], batch_size);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Process individual results for parsed advertisements
 | 
			
		||||
      if (this->parse_advertisements_) {
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
        for (size_t i = 0; i < batch_size; i++) {
 | 
			
		||||
          BLEScanResult &scan_result = this->scan_ring_buffer_[read_idx + i];
 | 
			
		||||
          ESPBTDevice device;
 | 
			
		||||
          device.parse_scan_rst(scan_result);
 | 
			
		||||
 | 
			
		||||
          bool found = false;
 | 
			
		||||
          for (auto *listener : this->listeners_) {
 | 
			
		||||
            if (listener->parse_device(device))
 | 
			
		||||
              found = true;
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          for (auto *client : this->clients_) {
 | 
			
		||||
            if (client->parse_device(device)) {
 | 
			
		||||
              found = true;
 | 
			
		||||
              if (!connecting && client->state() == ClientState::DISCOVERED) {
 | 
			
		||||
                promote_to_connecting = true;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
 | 
			
		||||
          if (!found && !this->scan_continuous_) {
 | 
			
		||||
            this->print_bt_device_info(device);
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
#endif  // USE_ESP32_BLE_DEVICE
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Update read index for entire batch
 | 
			
		||||
      read_idx = (read_idx + batch_size) % SCAN_RESULT_BUFFER_SIZE;
 | 
			
		||||
 | 
			
		||||
      // Store with release to ensure reads complete before index update
 | 
			
		||||
      this->ring_read_index_.store(read_idx, std::memory_order_release);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Log dropped results periodically
 | 
			
		||||
    size_t dropped = this->scan_results_dropped_.exchange(0, std::memory_order_relaxed);
 | 
			
		||||
    if (dropped > 0) {
 | 
			
		||||
      ESP_LOGW(TAG, "Dropped %zu BLE scan results due to buffer overflow", dropped);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (this->scanner_state_ == ScannerState::FAILED ||
 | 
			
		||||
      (this->scan_set_param_failed_ && this->scanner_state_ == ScannerState::RUNNING)) {
 | 
			
		||||
    this->handle_scanner_failure_();
 | 
			
		||||
    this->stop_scan_();
 | 
			
		||||
    if (this->scan_start_fail_count_ == std::numeric_limits<uint8_t>::max()) {
 | 
			
		||||
      ESP_LOGE(TAG, "Scan could not restart after %d attempts, rebooting to restore stack (IDF)",
 | 
			
		||||
               std::numeric_limits<uint8_t>::max());
 | 
			
		||||
      App.reboot();
 | 
			
		||||
    }
 | 
			
		||||
    if (this->scan_start_failed_) {
 | 
			
		||||
      ESP_LOGE(TAG, "Scan start failed: %d", this->scan_start_failed_);
 | 
			
		||||
      this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS;
 | 
			
		||||
    }
 | 
			
		||||
    if (this->scan_set_param_failed_) {
 | 
			
		||||
      ESP_LOGE(TAG, "Scan set param failed: %d", this->scan_set_param_failed_);
 | 
			
		||||
      this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  /*
 | 
			
		||||
 | 
			
		||||
@@ -158,12 +215,13 @@ void ESP32BLETracker::loop() {
 | 
			
		||||
    https://github.com/espressif/esp-idf/issues/6688
 | 
			
		||||
 | 
			
		||||
  */
 | 
			
		||||
  bool promote_to_connecting = counts.discovered && !counts.searching && !counts.connecting;
 | 
			
		||||
 | 
			
		||||
  if (this->scanner_state_ == ScannerState::IDLE && !counts.connecting && !counts.disconnecting &&
 | 
			
		||||
      !promote_to_connecting) {
 | 
			
		||||
  if (this->scanner_state_ == ScannerState::IDLE && !connecting && !disconnecting && !promote_to_connecting) {
 | 
			
		||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
 | 
			
		||||
    this->update_coex_preference_(false);
 | 
			
		||||
    if (this->coex_prefer_ble_) {
 | 
			
		||||
      this->coex_prefer_ble_ = false;
 | 
			
		||||
      ESP_LOGD(TAG, "Setting coexistence preference to balanced.");
 | 
			
		||||
      esp_coex_preference_set(ESP_COEX_PREFER_BALANCE);  // Reset to default
 | 
			
		||||
    }
 | 
			
		||||
#endif
 | 
			
		||||
    if (this->scan_continuous_) {
 | 
			
		||||
      this->start_scan_(false);  // first = false
 | 
			
		||||
@@ -171,13 +229,31 @@ void ESP32BLETracker::loop() {
 | 
			
		||||
  }
 | 
			
		||||
  // If there is a discovered client and no connecting
 | 
			
		||||
  // clients and no clients using the scanner to search for
 | 
			
		||||
  // devices, then promote the discovered client to ready to connect.
 | 
			
		||||
  // We check both RUNNING and IDLE states because:
 | 
			
		||||
  // - RUNNING: gap_scan_event_handler initiates stop_scan_() but promotion can happen immediately
 | 
			
		||||
  // - IDLE: Scanner has already stopped (naturally or by gap_scan_event_handler)
 | 
			
		||||
  // devices, then stop scanning and promote the discovered
 | 
			
		||||
  // client to ready to connect.
 | 
			
		||||
  if (promote_to_connecting &&
 | 
			
		||||
      (this->scanner_state_ == ScannerState::RUNNING || this->scanner_state_ == ScannerState::IDLE)) {
 | 
			
		||||
    this->try_promote_discovered_clients_();
 | 
			
		||||
    for (auto *client : this->clients_) {
 | 
			
		||||
      if (client->state() == ClientState::DISCOVERED) {
 | 
			
		||||
        if (this->scanner_state_ == ScannerState::RUNNING) {
 | 
			
		||||
          ESP_LOGD(TAG, "Stopping scan to make connection");
 | 
			
		||||
          this->stop_scan_();
 | 
			
		||||
        } else if (this->scanner_state_ == ScannerState::IDLE) {
 | 
			
		||||
          ESP_LOGD(TAG, "Promoting client to connect");
 | 
			
		||||
          // We only want to promote one client at a time.
 | 
			
		||||
          // once the scanner is fully stopped.
 | 
			
		||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
 | 
			
		||||
          ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection.");
 | 
			
		||||
          if (!this->coex_prefer_ble_) {
 | 
			
		||||
            this->coex_prefer_ble_ = true;
 | 
			
		||||
            esp_coex_preference_set(ESP_COEX_PREFER_BT);  // Prioritize Bluetooth
 | 
			
		||||
          }
 | 
			
		||||
#endif
 | 
			
		||||
          client->set_state(ClientState::READY_TO_CONNECT);
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -193,11 +269,16 @@ void ESP32BLETracker::ble_before_disabled_event_handler() { this->stop_scan_();
 | 
			
		||||
 | 
			
		||||
void ESP32BLETracker::stop_scan_() {
 | 
			
		||||
  if (this->scanner_state_ != ScannerState::RUNNING && this->scanner_state_ != ScannerState::FAILED) {
 | 
			
		||||
    ESP_LOGE(TAG, "Cannot stop scan: %s", this->scanner_state_to_string_(this->scanner_state_));
 | 
			
		||||
    if (this->scanner_state_ == ScannerState::IDLE) {
 | 
			
		||||
      ESP_LOGE(TAG, "Scan is already stopped while trying to stop.");
 | 
			
		||||
    } else if (this->scanner_state_ == ScannerState::STARTING) {
 | 
			
		||||
      ESP_LOGE(TAG, "Scan is starting while trying to stop.");
 | 
			
		||||
    } else if (this->scanner_state_ == ScannerState::STOPPING) {
 | 
			
		||||
      ESP_LOGE(TAG, "Scan is already stopping while trying to stop.");
 | 
			
		||||
    }
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  // Reset timeout state machine when stopping scan
 | 
			
		||||
  this->scan_timeout_state_ = ScanTimeoutState::INACTIVE;
 | 
			
		||||
  this->cancel_timeout("scan");
 | 
			
		||||
  this->set_scanner_state_(ScannerState::STOPPING);
 | 
			
		||||
  esp_err_t err = esp_ble_gap_stop_scanning();
 | 
			
		||||
  if (err != ESP_OK) {
 | 
			
		||||
@@ -212,7 +293,15 @@ void ESP32BLETracker::start_scan_(bool first) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->scanner_state_ != ScannerState::IDLE) {
 | 
			
		||||
    this->log_unexpected_state_("start scan", ScannerState::IDLE);
 | 
			
		||||
    if (this->scanner_state_ == ScannerState::STARTING) {
 | 
			
		||||
      ESP_LOGE(TAG, "Cannot start scan while already starting.");
 | 
			
		||||
    } else if (this->scanner_state_ == ScannerState::RUNNING) {
 | 
			
		||||
      ESP_LOGE(TAG, "Cannot start scan while already running.");
 | 
			
		||||
    } else if (this->scanner_state_ == ScannerState::STOPPING) {
 | 
			
		||||
      ESP_LOGE(TAG, "Cannot start scan while already stopping.");
 | 
			
		||||
    } else if (this->scanner_state_ == ScannerState::FAILED) {
 | 
			
		||||
      ESP_LOGE(TAG, "Cannot start scan while already failed.");
 | 
			
		||||
    }
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  this->set_scanner_state_(ScannerState::STARTING);
 | 
			
		||||
@@ -221,19 +310,18 @@ void ESP32BLETracker::start_scan_(bool first) {
 | 
			
		||||
    for (auto *listener : this->listeners_)
 | 
			
		||||
      listener->on_scan_end();
 | 
			
		||||
  }
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
  this->already_discovered_.clear();
 | 
			
		||||
#endif
 | 
			
		||||
  this->scan_params_.scan_type = this->scan_active_ ? BLE_SCAN_TYPE_ACTIVE : BLE_SCAN_TYPE_PASSIVE;
 | 
			
		||||
  this->scan_params_.own_addr_type = BLE_ADDR_TYPE_PUBLIC;
 | 
			
		||||
  this->scan_params_.scan_filter_policy = BLE_SCAN_FILTER_ALLOW_ALL;
 | 
			
		||||
  this->scan_params_.scan_interval = this->scan_interval_;
 | 
			
		||||
  this->scan_params_.scan_window = this->scan_window_;
 | 
			
		||||
 | 
			
		||||
  // Start timeout monitoring in loop() instead of using scheduler
 | 
			
		||||
  // This prevents false reboots when the loop is blocked
 | 
			
		||||
  this->scan_start_time_ = App.get_loop_component_start_time();
 | 
			
		||||
  this->scan_timeout_state_ = ScanTimeoutState::MONITORING;
 | 
			
		||||
  // Start timeout before scan is started. Otherwise scan never starts if any error.
 | 
			
		||||
  this->set_timeout("scan", this->scan_duration_ * 2000, []() {
 | 
			
		||||
    ESP_LOGE(TAG, "Scan never terminated, rebooting to restore stack (IDF)");
 | 
			
		||||
    App.reboot();
 | 
			
		||||
  });
 | 
			
		||||
 | 
			
		||||
  esp_err_t err = esp_ble_gap_set_scan_params(&this->scan_params_);
 | 
			
		||||
  if (err != ESP_OK) {
 | 
			
		||||
@@ -302,23 +390,44 @@ void ESP32BLETracker::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_ga
 | 
			
		||||
 | 
			
		||||
void ESP32BLETracker::gap_scan_event_handler(const BLEScanResult &scan_result) {
 | 
			
		||||
  // Note: This handler is called from the main loop context via esp32_ble's event queue.
 | 
			
		||||
  // We process advertisements immediately instead of buffering them.
 | 
			
		||||
  // However, we still use a lock-free ring buffer to batch results efficiently.
 | 
			
		||||
  ESP_LOGV(TAG, "gap_scan_result - event %d", scan_result.search_evt);
 | 
			
		||||
 | 
			
		||||
  if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_RES_EVT) {
 | 
			
		||||
    // Process the scan result immediately
 | 
			
		||||
    bool found_discovered_client = this->process_scan_result_(scan_result);
 | 
			
		||||
    // Ring buffer write (Producer side)
 | 
			
		||||
    // Even though we're in the main loop, the ring buffer design allows efficient batching
 | 
			
		||||
    // IMPORTANT: Only this thread writes to ring_write_index_
 | 
			
		||||
 | 
			
		||||
    // If we found a discovered client that needs promotion, stop scanning
 | 
			
		||||
    // This replaces the promote_to_connecting logic from loop()
 | 
			
		||||
    if (found_discovered_client && this->scanner_state_ == ScannerState::RUNNING) {
 | 
			
		||||
      ESP_LOGD(TAG, "Found discovered client, stopping scan for connection");
 | 
			
		||||
      this->stop_scan_();
 | 
			
		||||
    // Load our own index with relaxed ordering (we're the only writer)
 | 
			
		||||
    uint8_t write_idx = this->ring_write_index_.load(std::memory_order_relaxed);
 | 
			
		||||
    uint8_t next_write_idx = (write_idx + 1) % SCAN_RESULT_BUFFER_SIZE;
 | 
			
		||||
 | 
			
		||||
    // Load consumer's index with acquire to see their latest updates
 | 
			
		||||
    uint8_t read_idx = this->ring_read_index_.load(std::memory_order_acquire);
 | 
			
		||||
 | 
			
		||||
    // Check if buffer is full
 | 
			
		||||
    if (next_write_idx != read_idx) {
 | 
			
		||||
      // Write to ring buffer
 | 
			
		||||
      this->scan_ring_buffer_[write_idx] = scan_result;
 | 
			
		||||
 | 
			
		||||
      // Store with release to ensure the write is visible before index update
 | 
			
		||||
      this->ring_write_index_.store(next_write_idx, std::memory_order_release);
 | 
			
		||||
    } else {
 | 
			
		||||
      // Buffer full, track dropped results
 | 
			
		||||
      this->scan_results_dropped_.fetch_add(1, std::memory_order_relaxed);
 | 
			
		||||
    }
 | 
			
		||||
  } else if (scan_result.search_evt == ESP_GAP_SEARCH_INQ_CMPL_EVT) {
 | 
			
		||||
    // Scan finished on its own
 | 
			
		||||
    if (this->scanner_state_ != ScannerState::RUNNING) {
 | 
			
		||||
      this->log_unexpected_state_("scan complete", ScannerState::RUNNING);
 | 
			
		||||
      if (this->scanner_state_ == ScannerState::STOPPING) {
 | 
			
		||||
        ESP_LOGE(TAG, "Scan was not running when scan completed.");
 | 
			
		||||
      } else if (this->scanner_state_ == ScannerState::STARTING) {
 | 
			
		||||
        ESP_LOGE(TAG, "Scan was not started when scan completed.");
 | 
			
		||||
      } else if (this->scanner_state_ == ScannerState::FAILED) {
 | 
			
		||||
        ESP_LOGE(TAG, "Scan was in failed state when scan completed.");
 | 
			
		||||
      } else if (this->scanner_state_ == ScannerState::IDLE) {
 | 
			
		||||
        ESP_LOGE(TAG, "Scan was idle when scan completed.");
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    // Scan completed naturally, perform cleanup and transition to IDLE
 | 
			
		||||
    this->cleanup_scan_state_(false);
 | 
			
		||||
@@ -340,7 +449,15 @@ void ESP32BLETracker::gap_scan_start_complete_(const esp_ble_gap_cb_param_t::ble
 | 
			
		||||
  ESP_LOGV(TAG, "gap_scan_start_complete - status %d", param.status);
 | 
			
		||||
  this->scan_start_failed_ = param.status;
 | 
			
		||||
  if (this->scanner_state_ != ScannerState::STARTING) {
 | 
			
		||||
    this->log_unexpected_state_("start complete", ScannerState::STARTING);
 | 
			
		||||
    if (this->scanner_state_ == ScannerState::RUNNING) {
 | 
			
		||||
      ESP_LOGE(TAG, "Scan was already running when start complete.");
 | 
			
		||||
    } else if (this->scanner_state_ == ScannerState::STOPPING) {
 | 
			
		||||
      ESP_LOGE(TAG, "Scan was stopping when start complete.");
 | 
			
		||||
    } else if (this->scanner_state_ == ScannerState::FAILED) {
 | 
			
		||||
      ESP_LOGE(TAG, "Scan was in failed state when start complete.");
 | 
			
		||||
    } else if (this->scanner_state_ == ScannerState::IDLE) {
 | 
			
		||||
      ESP_LOGE(TAG, "Scan was idle when start complete.");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  if (param.status == ESP_BT_STATUS_SUCCESS) {
 | 
			
		||||
    this->scan_start_fail_count_ = 0;
 | 
			
		||||
@@ -358,7 +475,15 @@ void ESP32BLETracker::gap_scan_stop_complete_(const esp_ble_gap_cb_param_t::ble_
 | 
			
		||||
  // This allows us to safely transition to IDLE state and perform cleanup without race conditions
 | 
			
		||||
  ESP_LOGV(TAG, "gap_scan_stop_complete - status %d", param.status);
 | 
			
		||||
  if (this->scanner_state_ != ScannerState::STOPPING) {
 | 
			
		||||
    this->log_unexpected_state_("stop complete", ScannerState::STOPPING);
 | 
			
		||||
    if (this->scanner_state_ == ScannerState::RUNNING) {
 | 
			
		||||
      ESP_LOGE(TAG, "Scan was not running when stop complete.");
 | 
			
		||||
    } else if (this->scanner_state_ == ScannerState::STARTING) {
 | 
			
		||||
      ESP_LOGE(TAG, "Scan was not started when stop complete.");
 | 
			
		||||
    } else if (this->scanner_state_ == ScannerState::FAILED) {
 | 
			
		||||
      ESP_LOGE(TAG, "Scan was in failed state when stop complete.");
 | 
			
		||||
    } else if (this->scanner_state_ == ScannerState::IDLE) {
 | 
			
		||||
      ESP_LOGE(TAG, "Scan was idle when stop complete.");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Perform cleanup and transition to IDLE
 | 
			
		||||
@@ -639,10 +764,25 @@ void ESP32BLETracker::dump_config() {
 | 
			
		||||
                "  Continuous Scanning: %s",
 | 
			
		||||
                this->scan_duration_, this->scan_interval_ * 0.625f, this->scan_window_ * 0.625f,
 | 
			
		||||
                this->scan_active_ ? "ACTIVE" : "PASSIVE", YESNO(this->scan_continuous_));
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Scanner State: %s", this->scanner_state_to_string_(this->scanner_state_));
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Connecting: %d, discovered: %d, searching: %d, disconnecting: %d",
 | 
			
		||||
                this->client_state_counts_.connecting, this->client_state_counts_.discovered,
 | 
			
		||||
                this->client_state_counts_.searching, this->client_state_counts_.disconnecting);
 | 
			
		||||
  switch (this->scanner_state_) {
 | 
			
		||||
    case ScannerState::IDLE:
 | 
			
		||||
      ESP_LOGCONFIG(TAG, "  Scanner State: IDLE");
 | 
			
		||||
      break;
 | 
			
		||||
    case ScannerState::STARTING:
 | 
			
		||||
      ESP_LOGCONFIG(TAG, "  Scanner State: STARTING");
 | 
			
		||||
      break;
 | 
			
		||||
    case ScannerState::RUNNING:
 | 
			
		||||
      ESP_LOGCONFIG(TAG, "  Scanner State: RUNNING");
 | 
			
		||||
      break;
 | 
			
		||||
    case ScannerState::STOPPING:
 | 
			
		||||
      ESP_LOGCONFIG(TAG, "  Scanner State: STOPPING");
 | 
			
		||||
      break;
 | 
			
		||||
    case ScannerState::FAILED:
 | 
			
		||||
      ESP_LOGCONFIG(TAG, "  Scanner State: FAILED");
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Connecting: %d, discovered: %d, searching: %d, disconnecting: %d", connecting_, discovered_,
 | 
			
		||||
                searching_, disconnecting_);
 | 
			
		||||
  if (this->scan_start_fail_count_) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Scan Start Fail Count: %d", this->scan_start_fail_count_);
 | 
			
		||||
  }
 | 
			
		||||
@@ -719,73 +859,12 @@ bool ESPBTDevice::resolve_irk(const uint8_t *irk) const {
 | 
			
		||||
  return ecb_ciphertext[15] == (addr64 & 0xff) && ecb_ciphertext[14] == ((addr64 >> 8) & 0xff) &&
 | 
			
		||||
         ecb_ciphertext[13] == ((addr64 >> 16) & 0xff);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool ESP32BLETracker::has_connecting_clients_() const {
 | 
			
		||||
  for (auto *client : this->clients_) {
 | 
			
		||||
    auto state = client->state();
 | 
			
		||||
    if (state == ClientState::CONNECTING || state == ClientState::READY_TO_CONNECT) {
 | 
			
		||||
      return true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return false;
 | 
			
		||||
}
 | 
			
		||||
#endif  // USE_ESP32_BLE_DEVICE
 | 
			
		||||
 | 
			
		||||
bool ESP32BLETracker::process_scan_result_(const BLEScanResult &scan_result) {
 | 
			
		||||
  bool found_discovered_client = false;
 | 
			
		||||
 | 
			
		||||
  // Process raw advertisements
 | 
			
		||||
  if (this->raw_advertisements_) {
 | 
			
		||||
    for (auto *listener : this->listeners_) {
 | 
			
		||||
      listener->parse_devices(&scan_result, 1);
 | 
			
		||||
    }
 | 
			
		||||
    for (auto *client : this->clients_) {
 | 
			
		||||
      client->parse_devices(&scan_result, 1);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Process parsed advertisements
 | 
			
		||||
  if (this->parse_advertisements_) {
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
    ESPBTDevice device;
 | 
			
		||||
    device.parse_scan_rst(scan_result);
 | 
			
		||||
 | 
			
		||||
    bool found = false;
 | 
			
		||||
    for (auto *listener : this->listeners_) {
 | 
			
		||||
      if (listener->parse_device(device))
 | 
			
		||||
        found = true;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    for (auto *client : this->clients_) {
 | 
			
		||||
      if (client->parse_device(device)) {
 | 
			
		||||
        found = true;
 | 
			
		||||
        // Check if this client is discovered and needs promotion
 | 
			
		||||
        if (client->state() == ClientState::DISCOVERED) {
 | 
			
		||||
          // Only check for connecting clients if we found a discovered client
 | 
			
		||||
          // This matches the original logic: !connecting && client->state() == DISCOVERED
 | 
			
		||||
          if (!this->has_connecting_clients_()) {
 | 
			
		||||
            found_discovered_client = true;
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (!found && !this->scan_continuous_) {
 | 
			
		||||
      this->print_bt_device_info(device);
 | 
			
		||||
    }
 | 
			
		||||
#endif  // USE_ESP32_BLE_DEVICE
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return found_discovered_client;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {
 | 
			
		||||
  ESP_LOGD(TAG, "Scan %scomplete, set scanner state to IDLE.", is_stop_complete ? "stop " : "");
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
  this->already_discovered_.clear();
 | 
			
		||||
#endif
 | 
			
		||||
  // Reset timeout state machine instead of cancelling scheduler timeout
 | 
			
		||||
  this->scan_timeout_state_ = ScanTimeoutState::INACTIVE;
 | 
			
		||||
  this->cancel_timeout("scan");
 | 
			
		||||
 | 
			
		||||
  for (auto *listener : this->listeners_)
 | 
			
		||||
    listener->on_scan_end();
 | 
			
		||||
@@ -793,84 +872,6 @@ void ESP32BLETracker::cleanup_scan_state_(bool is_stop_complete) {
 | 
			
		||||
  this->set_scanner_state_(ScannerState::IDLE);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32BLETracker::handle_scanner_failure_() {
 | 
			
		||||
  this->stop_scan_();
 | 
			
		||||
  if (this->scan_start_fail_count_ == std::numeric_limits<uint8_t>::max()) {
 | 
			
		||||
    ESP_LOGE(TAG, "Scan could not restart after %d attempts, rebooting to restore stack (IDF)",
 | 
			
		||||
             std::numeric_limits<uint8_t>::max());
 | 
			
		||||
    App.reboot();
 | 
			
		||||
  }
 | 
			
		||||
  if (this->scan_start_failed_) {
 | 
			
		||||
    ESP_LOGE(TAG, "Scan start failed: %d", this->scan_start_failed_);
 | 
			
		||||
    this->scan_start_failed_ = ESP_BT_STATUS_SUCCESS;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->scan_set_param_failed_) {
 | 
			
		||||
    ESP_LOGE(TAG, "Scan set param failed: %d", this->scan_set_param_failed_);
 | 
			
		||||
    this->scan_set_param_failed_ = ESP_BT_STATUS_SUCCESS;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32BLETracker::try_promote_discovered_clients_() {
 | 
			
		||||
  // Only promote the first discovered client to avoid multiple simultaneous connections
 | 
			
		||||
  for (auto *client : this->clients_) {
 | 
			
		||||
    if (client->state() != ClientState::DISCOVERED) {
 | 
			
		||||
      continue;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (this->scanner_state_ == ScannerState::RUNNING) {
 | 
			
		||||
      ESP_LOGD(TAG, "Stopping scan to make connection");
 | 
			
		||||
      this->stop_scan_();
 | 
			
		||||
      // Don't wait for scan stop complete - promote immediately.
 | 
			
		||||
      // This is safe because ESP-IDF processes BLE commands sequentially through its internal mailbox queue.
 | 
			
		||||
      // This guarantees that the stop scan command will be fully processed before any subsequent connect command,
 | 
			
		||||
      // preventing race conditions or overlapping operations.
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ESP_LOGD(TAG, "Promoting client to connect");
 | 
			
		||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
 | 
			
		||||
    this->update_coex_preference_(true);
 | 
			
		||||
#endif
 | 
			
		||||
    client->set_state(ClientState::READY_TO_CONNECT);
 | 
			
		||||
    break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const char *ESP32BLETracker::scanner_state_to_string_(ScannerState state) const {
 | 
			
		||||
  switch (state) {
 | 
			
		||||
    case ScannerState::IDLE:
 | 
			
		||||
      return "IDLE";
 | 
			
		||||
    case ScannerState::STARTING:
 | 
			
		||||
      return "STARTING";
 | 
			
		||||
    case ScannerState::RUNNING:
 | 
			
		||||
      return "RUNNING";
 | 
			
		||||
    case ScannerState::STOPPING:
 | 
			
		||||
      return "STOPPING";
 | 
			
		||||
    case ScannerState::FAILED:
 | 
			
		||||
      return "FAILED";
 | 
			
		||||
    default:
 | 
			
		||||
      return "UNKNOWN";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32BLETracker::log_unexpected_state_(const char *operation, ScannerState expected_state) const {
 | 
			
		||||
  ESP_LOGE(TAG, "Unexpected state: %s on %s, expected: %s", this->scanner_state_to_string_(this->scanner_state_),
 | 
			
		||||
           operation, this->scanner_state_to_string_(expected_state));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
 | 
			
		||||
void ESP32BLETracker::update_coex_preference_(bool force_ble) {
 | 
			
		||||
  if (force_ble && !this->coex_prefer_ble_) {
 | 
			
		||||
    ESP_LOGD(TAG, "Setting coexistence to Bluetooth to make connection.");
 | 
			
		||||
    this->coex_prefer_ble_ = true;
 | 
			
		||||
    esp_coex_preference_set(ESP_COEX_PREFER_BT);  // Prioritize Bluetooth
 | 
			
		||||
  } else if (!force_ble && this->coex_prefer_ble_) {
 | 
			
		||||
    ESP_LOGD(TAG, "Setting coexistence preference to balanced.");
 | 
			
		||||
    this->coex_prefer_ble_ = false;
 | 
			
		||||
    esp_coex_preference_set(ESP_COEX_PREFER_BALANCE);  // Reset to default
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::esp32_ble_tracker
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,7 @@
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
#include <array>
 | 
			
		||||
#include <atomic>
 | 
			
		||||
#include <string>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
@@ -20,7 +21,6 @@
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/esp32_ble/ble.h"
 | 
			
		||||
#include "esphome/components/esp32_ble/ble_uuid.h"
 | 
			
		||||
#include "esphome/components/esp32_ble/ble_scan_result.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome::esp32_ble_tracker {
 | 
			
		||||
 | 
			
		||||
@@ -136,20 +136,6 @@ class ESPBTDeviceListener {
 | 
			
		||||
  ESP32BLETracker *parent_{nullptr};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct ClientStateCounts {
 | 
			
		||||
  uint8_t connecting = 0;
 | 
			
		||||
  uint8_t discovered = 0;
 | 
			
		||||
  uint8_t searching = 0;
 | 
			
		||||
  uint8_t disconnecting = 0;
 | 
			
		||||
 | 
			
		||||
  bool operator==(const ClientStateCounts &other) const {
 | 
			
		||||
    return connecting == other.connecting && discovered == other.discovered && searching == other.searching &&
 | 
			
		||||
           disconnecting == other.disconnecting;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool operator!=(const ClientStateCounts &other) const { return !(*this == other); }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum class ClientState : uint8_t {
 | 
			
		||||
  // Connection is allocated
 | 
			
		||||
  INIT,
 | 
			
		||||
@@ -184,9 +170,6 @@ enum class ScannerState {
 | 
			
		||||
  STOPPING,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Helper function to convert ClientState to string
 | 
			
		||||
const char *client_state_to_string(ClientState state);
 | 
			
		||||
 | 
			
		||||
enum class ConnectionType : uint8_t {
 | 
			
		||||
  // The default connection type, we hold all the services in ram
 | 
			
		||||
  // for the duration of the connection.
 | 
			
		||||
@@ -289,92 +272,47 @@ class ESP32BLETracker : public Component,
 | 
			
		||||
  void set_scanner_state_(ScannerState state);
 | 
			
		||||
  /// Common cleanup logic when transitioning scanner to IDLE state
 | 
			
		||||
  void cleanup_scan_state_(bool is_stop_complete);
 | 
			
		||||
  /// Process a single scan result immediately
 | 
			
		||||
  /// Returns true if a discovered client needs promotion to READY_TO_CONNECT
 | 
			
		||||
  bool process_scan_result_(const BLEScanResult &scan_result);
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
  /// Check if any clients are in connecting or ready to connect state
 | 
			
		||||
  bool has_connecting_clients_() const;
 | 
			
		||||
#endif
 | 
			
		||||
  /// Handle scanner failure states
 | 
			
		||||
  void handle_scanner_failure_();
 | 
			
		||||
  /// Try to promote discovered clients to ready to connect
 | 
			
		||||
  void try_promote_discovered_clients_();
 | 
			
		||||
  /// Convert scanner state enum to string for logging
 | 
			
		||||
  const char *scanner_state_to_string_(ScannerState state) const;
 | 
			
		||||
  /// Log an unexpected scanner state
 | 
			
		||||
  void log_unexpected_state_(const char *operation, ScannerState expected_state) const;
 | 
			
		||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
 | 
			
		||||
  /// Update BLE coexistence preference
 | 
			
		||||
  void update_coex_preference_(bool force_ble);
 | 
			
		||||
#endif
 | 
			
		||||
  /// Count clients in each state
 | 
			
		||||
  ClientStateCounts count_client_states_() const {
 | 
			
		||||
    ClientStateCounts counts;
 | 
			
		||||
    for (auto *client : this->clients_) {
 | 
			
		||||
      switch (client->state()) {
 | 
			
		||||
        case ClientState::DISCONNECTING:
 | 
			
		||||
          counts.disconnecting++;
 | 
			
		||||
          break;
 | 
			
		||||
        case ClientState::DISCOVERED:
 | 
			
		||||
          counts.discovered++;
 | 
			
		||||
          break;
 | 
			
		||||
        case ClientState::SEARCHING:
 | 
			
		||||
          counts.searching++;
 | 
			
		||||
          break;
 | 
			
		||||
        case ClientState::CONNECTING:
 | 
			
		||||
        case ClientState::READY_TO_CONNECT:
 | 
			
		||||
          counts.connecting++;
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    return counts;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Group 1: Large objects (12+ bytes) - vectors and callback manager
 | 
			
		||||
  std::vector<ESPBTDeviceListener *> listeners_;
 | 
			
		||||
  std::vector<ESPBTClient *> clients_;
 | 
			
		||||
  CallbackManager<void(ScannerState)> scanner_state_callbacks_;
 | 
			
		||||
#ifdef USE_ESP32_BLE_DEVICE
 | 
			
		||||
  uint8_t app_id_{0};
 | 
			
		||||
 | 
			
		||||
  /// Vector of addresses that have already been printed in print_bt_device_info
 | 
			
		||||
  std::vector<uint64_t> already_discovered_;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Group 2: Structs (aligned to 4 bytes)
 | 
			
		||||
  std::vector<ESPBTDeviceListener *> listeners_;
 | 
			
		||||
  /// Client parameters.
 | 
			
		||||
  std::vector<ESPBTClient *> clients_;
 | 
			
		||||
  /// A structure holding the ESP BLE scan parameters.
 | 
			
		||||
  esp_ble_scan_params_t scan_params_;
 | 
			
		||||
  ClientStateCounts client_state_counts_;
 | 
			
		||||
 | 
			
		||||
  // Group 3: 4-byte types
 | 
			
		||||
  /// The interval in seconds to perform scans.
 | 
			
		||||
  uint32_t scan_duration_;
 | 
			
		||||
  uint32_t scan_interval_;
 | 
			
		||||
  uint32_t scan_window_;
 | 
			
		||||
  esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
 | 
			
		||||
  esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS};
 | 
			
		||||
 | 
			
		||||
  // Group 4: 1-byte types (enums, uint8_t, bool)
 | 
			
		||||
  uint8_t app_id_{0};
 | 
			
		||||
  uint8_t scan_start_fail_count_{0};
 | 
			
		||||
  ScannerState scanner_state_{ScannerState::IDLE};
 | 
			
		||||
  bool scan_continuous_;
 | 
			
		||||
  bool scan_active_;
 | 
			
		||||
  ScannerState scanner_state_{ScannerState::IDLE};
 | 
			
		||||
  CallbackManager<void(ScannerState)> scanner_state_callbacks_;
 | 
			
		||||
  bool ble_was_disabled_{true};
 | 
			
		||||
  bool raw_advertisements_{false};
 | 
			
		||||
  bool parse_advertisements_{false};
 | 
			
		||||
 | 
			
		||||
  // Lock-free Single-Producer Single-Consumer (SPSC) ring buffer for scan results
 | 
			
		||||
  // Producer: ESP-IDF Bluetooth stack callback (gap_scan_event_handler)
 | 
			
		||||
  // Consumer: ESPHome main loop (loop() method)
 | 
			
		||||
  // This design ensures zero blocking in the BT callback and prevents scan result loss
 | 
			
		||||
  BLEScanResult *scan_ring_buffer_;
 | 
			
		||||
  std::atomic<uint8_t> ring_write_index_{0};       // Written only by BT callback (producer)
 | 
			
		||||
  std::atomic<uint8_t> ring_read_index_{0};        // Written only by main loop (consumer)
 | 
			
		||||
  std::atomic<uint16_t> scan_results_dropped_{0};  // Tracks buffer overflow events
 | 
			
		||||
 | 
			
		||||
  esp_bt_status_t scan_start_failed_{ESP_BT_STATUS_SUCCESS};
 | 
			
		||||
  esp_bt_status_t scan_set_param_failed_{ESP_BT_STATUS_SUCCESS};
 | 
			
		||||
  int connecting_{0};
 | 
			
		||||
  int discovered_{0};
 | 
			
		||||
  int searching_{0};
 | 
			
		||||
  int disconnecting_{0};
 | 
			
		||||
#ifdef USE_ESP32_BLE_SOFTWARE_COEXISTENCE
 | 
			
		||||
  bool coex_prefer_ble_{false};
 | 
			
		||||
#endif
 | 
			
		||||
  // Scan timeout state machine
 | 
			
		||||
  enum class ScanTimeoutState : uint8_t {
 | 
			
		||||
    INACTIVE,       // No timeout monitoring
 | 
			
		||||
    MONITORING,     // Actively monitoring for timeout
 | 
			
		||||
    EXCEEDED_WAIT,  // Timeout exceeded, waiting one loop before reboot
 | 
			
		||||
  };
 | 
			
		||||
  uint32_t scan_start_time_{0};
 | 
			
		||||
  ScanTimeoutState scan_timeout_state_{ScanTimeoutState::INACTIVE};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// NOLINTNEXTLINE
 | 
			
		||||
 
 | 
			
		||||
@@ -345,7 +345,7 @@ async def to_code(config):
 | 
			
		||||
    cg.add_define("USE_CAMERA")
 | 
			
		||||
 | 
			
		||||
    if CORE.using_esp_idf:
 | 
			
		||||
        add_idf_component(name="espressif/esp32-camera", ref="2.1.1")
 | 
			
		||||
        add_idf_component(name="espressif/esp32-camera", ref="2.1.0")
 | 
			
		||||
 | 
			
		||||
    for conf in config.get(CONF_ON_STREAM_START, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,11 @@
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#if defined(USE_ESP32_VARIANT_ESP32) || defined(USE_ESP32_VARIANT_ESP32S2)
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
#include <esp32-hal-dac.h>
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_dac {
 | 
			
		||||
@@ -19,12 +23,18 @@ void ESP32DAC::setup() {
 | 
			
		||||
  this->pin_->setup();
 | 
			
		||||
  this->turn_off();
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
  const dac_channel_t channel = this->pin_->get_pin() == DAC0_PIN ? DAC_CHAN_0 : DAC_CHAN_1;
 | 
			
		||||
  const dac_oneshot_config_t oneshot_cfg{channel};
 | 
			
		||||
  dac_oneshot_new_channel(&oneshot_cfg, &this->dac_handle_);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32DAC::on_safe_shutdown() { dac_oneshot_del_channel(this->dac_handle_); }
 | 
			
		||||
void ESP32DAC::on_safe_shutdown() {
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
  dac_oneshot_del_channel(this->dac_handle_);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESP32DAC::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "ESP32 DAC:");
 | 
			
		||||
@@ -38,10 +48,15 @@ void ESP32DAC::write_state(float state) {
 | 
			
		||||
 | 
			
		||||
  state = state * 255;
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
  dac_oneshot_output_voltage(this->dac_handle_, state);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_ARDUINO
 | 
			
		||||
  dacWrite(this->pin_->get_pin(), state);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_dac
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32_VARIANT_ESP32 || USE_ESP32_VARIANT_ESP32S2
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -1,13 +1,15 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/output/float_output.h"
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/components/output/float_output.h"
 | 
			
		||||
 | 
			
		||||
#if defined(USE_ESP32_VARIANT_ESP32) || defined(USE_ESP32_VARIANT_ESP32S2)
 | 
			
		||||
#ifdef USE_ESP32
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
#include <driver/dac_oneshot.h>
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace esp32_dac {
 | 
			
		||||
@@ -27,10 +29,12 @@ class ESP32DAC : public output::FloatOutput, public Component {
 | 
			
		||||
  void write_state(float state) override;
 | 
			
		||||
 | 
			
		||||
  InternalGPIOPin *pin_;
 | 
			
		||||
#ifdef USE_ESP_IDF
 | 
			
		||||
  dac_oneshot_handle_t dac_handle_;
 | 
			
		||||
#endif
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_dac
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
#endif  // USE_ESP32_VARIANT_ESP32 || USE_ESP32_VARIANT_ESP32S2
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,9 @@ static size_t IRAM_ATTR HOT encoder_callback(const void *data, size_t size, size
 | 
			
		||||
        symbols[i] = params->bit0;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if ((index + 1) >= size && params->reset.duration0 == 0 && params->reset.duration1 == 0) {
 | 
			
		||||
      *done = true;
 | 
			
		||||
    }
 | 
			
		||||
    return RMT_SYMBOLS_PER_BYTE;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -107,7 +110,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
 | 
			
		||||
  memset(&encoder, 0, sizeof(encoder));
 | 
			
		||||
  encoder.callback = encoder_callback;
 | 
			
		||||
  encoder.arg = &this->params_;
 | 
			
		||||
  encoder.min_chunk_size = RMT_SYMBOLS_PER_BYTE;
 | 
			
		||||
  encoder.min_chunk_size = 8;
 | 
			
		||||
  if (rmt_new_simple_encoder(&encoder, &this->encoder_) != ESP_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "Encoder creation failed");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
 
 | 
			
		||||
@@ -171,8 +171,8 @@ class ESP32TouchComponent : public Component {
 | 
			
		||||
  // based on the filter configuration
 | 
			
		||||
  uint32_t read_touch_value(touch_pad_t pad) const;
 | 
			
		||||
 | 
			
		||||
  // Helper to update touch state with a known state and value
 | 
			
		||||
  void update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched, uint32_t value);
 | 
			
		||||
  // Helper to update touch state with a known state
 | 
			
		||||
  void update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched);
 | 
			
		||||
 | 
			
		||||
  // Helper to read touch value and update state for a given child
 | 
			
		||||
  bool check_and_update_touch_state_(ESP32TouchBinarySensor *child);
 | 
			
		||||
@@ -234,13 +234,9 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor {
 | 
			
		||||
  touch_pad_t get_touch_pad() const { return this->touch_pad_; }
 | 
			
		||||
  uint32_t get_threshold() const { return this->threshold_; }
 | 
			
		||||
  void set_threshold(uint32_t threshold) { this->threshold_ = threshold; }
 | 
			
		||||
 | 
			
		||||
  /// Get the raw touch measurement value.
 | 
			
		||||
  /// @note Although this method may appear unused within the component, it is a public API
 | 
			
		||||
  /// used by lambdas in user configurations for custom touch value processing.
 | 
			
		||||
  /// @return The current raw touch sensor reading
 | 
			
		||||
#ifdef USE_ESP32_VARIANT_ESP32
 | 
			
		||||
  uint32_t get_value() const { return this->value_; }
 | 
			
		||||
 | 
			
		||||
#endif
 | 
			
		||||
  uint32_t get_wakeup_threshold() const { return this->wakeup_threshold_; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
@@ -249,8 +245,9 @@ class ESP32TouchBinarySensor : public binary_sensor::BinarySensor {
 | 
			
		||||
  touch_pad_t touch_pad_{TOUCH_PAD_MAX};
 | 
			
		||||
  uint32_t threshold_{0};
 | 
			
		||||
  uint32_t benchmark_{};
 | 
			
		||||
  /// Stores the last raw touch measurement value.
 | 
			
		||||
#ifdef USE_ESP32_VARIANT_ESP32
 | 
			
		||||
  uint32_t value_{0};
 | 
			
		||||
#endif
 | 
			
		||||
  bool last_state_{false};
 | 
			
		||||
  const uint32_t wakeup_threshold_{0};
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -100,8 +100,6 @@ void ESP32TouchComponent::process_setup_mode_logging_(uint32_t now) {
 | 
			
		||||
#else
 | 
			
		||||
      // Read the value being used for touch detection
 | 
			
		||||
      uint32_t value = this->read_touch_value(child->get_touch_pad());
 | 
			
		||||
      // Store the value for get_value() access in lambdas
 | 
			
		||||
      child->value_ = value;
 | 
			
		||||
      ESP_LOGD(TAG, "Touch Pad '%s' (T%d): %d", child->get_name().c_str(), child->get_touch_pad(), value);
 | 
			
		||||
#endif
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -10,11 +10,8 @@ namespace esp32_touch {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "esp32_touch";
 | 
			
		||||
 | 
			
		||||
// Helper to update touch state with a known state and value
 | 
			
		||||
void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched, uint32_t value) {
 | 
			
		||||
  // Store the value for get_value() access in lambdas
 | 
			
		||||
  child->value_ = value;
 | 
			
		||||
 | 
			
		||||
// Helper to update touch state with a known state
 | 
			
		||||
void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, bool is_touched) {
 | 
			
		||||
  // Always update timer when touched
 | 
			
		||||
  if (is_touched) {
 | 
			
		||||
    child->last_touch_time_ = App.get_loop_component_start_time();
 | 
			
		||||
@@ -24,8 +21,9 @@ void ESP32TouchComponent::update_touch_state_(ESP32TouchBinarySensor *child, boo
 | 
			
		||||
    child->last_state_ = is_touched;
 | 
			
		||||
    child->publish_state(is_touched);
 | 
			
		||||
    if (is_touched) {
 | 
			
		||||
      // ESP32-S2/S3 v2: touched when value > threshold
 | 
			
		||||
      ESP_LOGV(TAG, "Touch Pad '%s' state: ON (value: %" PRIu32 " > threshold: %" PRIu32 ")", child->get_name().c_str(),
 | 
			
		||||
               value, child->threshold_ + child->benchmark_);
 | 
			
		||||
               this->read_touch_value(child->touch_pad_), child->threshold_ + child->benchmark_);
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGV(TAG, "Touch Pad '%s' state: OFF", child->get_name().c_str());
 | 
			
		||||
    }
 | 
			
		||||
@@ -43,7 +41,7 @@ bool ESP32TouchComponent::check_and_update_touch_state_(ESP32TouchBinarySensor *
 | 
			
		||||
           child->get_name().c_str(), child->touch_pad_, value, child->threshold_, child->benchmark_);
 | 
			
		||||
  bool is_touched = value > child->benchmark_ + child->threshold_;
 | 
			
		||||
 | 
			
		||||
  this->update_touch_state_(child, is_touched, value);
 | 
			
		||||
  this->update_touch_state_(child, is_touched);
 | 
			
		||||
  return is_touched;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -298,9 +296,7 @@ void ESP32TouchComponent::loop() {
 | 
			
		||||
          this->check_and_update_touch_state_(child);
 | 
			
		||||
        } else if (event.intr_mask & TOUCH_PAD_INTR_MASK_ACTIVE) {
 | 
			
		||||
          // We only get ACTIVE interrupts now, releases are detected by timeout
 | 
			
		||||
          // Read the current value
 | 
			
		||||
          uint32_t value = this->read_touch_value(child->touch_pad_);
 | 
			
		||||
          this->update_touch_state_(child, true, value);  // Always touched for ACTIVE interrupts
 | 
			
		||||
          this->update_touch_state_(child, true);  // Always touched for ACTIVE interrupts
 | 
			
		||||
        }
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
 
 | 
			
		||||
@@ -19,9 +19,7 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "esphome.ota";
 | 
			
		||||
static constexpr uint16_t OTA_BLOCK_SIZE = 8192;
 | 
			
		||||
static constexpr uint32_t OTA_SOCKET_TIMEOUT_HANDSHAKE = 10000;  // milliseconds for initial handshake
 | 
			
		||||
static constexpr uint32_t OTA_SOCKET_TIMEOUT_DATA = 90000;       // milliseconds for data transfer
 | 
			
		||||
static constexpr u_int16_t OTA_BLOCK_SIZE = 8192;
 | 
			
		||||
 | 
			
		||||
void ESPHomeOTAComponent::setup() {
 | 
			
		||||
#ifdef USE_OTA_STATE_CALLBACK
 | 
			
		||||
@@ -30,19 +28,19 @@ void ESPHomeOTAComponent::setup() {
 | 
			
		||||
 | 
			
		||||
  this->server_ = socket::socket_ip_loop_monitored(SOCK_STREAM, 0);  // monitored for incoming connections
 | 
			
		||||
  if (this->server_ == nullptr) {
 | 
			
		||||
    this->log_socket_error_("creation");
 | 
			
		||||
    ESP_LOGW(TAG, "Could not create socket");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  int enable = 1;
 | 
			
		||||
  int err = this->server_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int));
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    this->log_socket_error_("reuseaddr");
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err);
 | 
			
		||||
    // we can still continue
 | 
			
		||||
  }
 | 
			
		||||
  err = this->server_->setblocking(false);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    this->log_socket_error_("non-blocking");
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
@@ -51,21 +49,21 @@ void ESPHomeOTAComponent::setup() {
 | 
			
		||||
 | 
			
		||||
  socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), this->port_);
 | 
			
		||||
  if (sl == 0) {
 | 
			
		||||
    this->log_socket_error_("set sockaddr");
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  err = this->server_->bind((struct sockaddr *) &server, sizeof(server));
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    this->log_socket_error_("bind");
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  err = this->server_->listen(4);
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    this->log_socket_error_("listen");
 | 
			
		||||
    ESP_LOGW(TAG, "Socket unable to listen: errno %d", errno);
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
@@ -85,93 +83,17 @@ void ESPHomeOTAComponent::dump_config() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPHomeOTAComponent::loop() {
 | 
			
		||||
  // Skip handle_handshake_() call if no client connected and no incoming connections
 | 
			
		||||
  // Skip handle_() call if no client connected and no incoming connections
 | 
			
		||||
  // This optimization reduces idle loop overhead when OTA is not active
 | 
			
		||||
  // Note: No need to check server_ for null as the component is marked failed in setup()
 | 
			
		||||
  // if server_ creation fails
 | 
			
		||||
  // Note: No need to check server_ for null as the component is marked failed in setup() if server_ creation fails
 | 
			
		||||
  if (this->client_ != nullptr || this->server_->ready()) {
 | 
			
		||||
    this->handle_handshake_();
 | 
			
		||||
    this->handle_();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static const uint8_t FEATURE_SUPPORTS_COMPRESSION = 0x01;
 | 
			
		||||
 | 
			
		||||
void ESPHomeOTAComponent::handle_handshake_() {
 | 
			
		||||
  /// Handle the initial OTA handshake.
 | 
			
		||||
  ///
 | 
			
		||||
  /// This method is non-blocking and will return immediately if no data is available.
 | 
			
		||||
  /// It waits for the first magic byte (0x6C) before proceeding to handle_data_().
 | 
			
		||||
  /// A 10-second timeout is enforced from initial connection.
 | 
			
		||||
 | 
			
		||||
  if (this->client_ == nullptr) {
 | 
			
		||||
    // We already checked server_->ready() in loop(), so we can accept directly
 | 
			
		||||
    struct sockaddr_storage source_addr;
 | 
			
		||||
    socklen_t addr_len = sizeof(source_addr);
 | 
			
		||||
    int enable = 1;
 | 
			
		||||
 | 
			
		||||
    this->client_ = this->server_->accept_loop_monitored((struct sockaddr *) &source_addr, &addr_len);
 | 
			
		||||
    if (this->client_ == nullptr)
 | 
			
		||||
      return;
 | 
			
		||||
    int err = this->client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
 | 
			
		||||
    if (err != 0) {
 | 
			
		||||
      this->log_socket_error_("nodelay");
 | 
			
		||||
      this->cleanup_connection_();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    err = this->client_->setblocking(false);
 | 
			
		||||
    if (err != 0) {
 | 
			
		||||
      this->log_socket_error_("non-blocking");
 | 
			
		||||
      this->cleanup_connection_();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this->log_start_("handshake");
 | 
			
		||||
    this->client_connect_time_ = App.get_loop_component_start_time();
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Check for handshake timeout
 | 
			
		||||
  uint32_t now = App.get_loop_component_start_time();
 | 
			
		||||
  if (now - this->client_connect_time_ > OTA_SOCKET_TIMEOUT_HANDSHAKE) {
 | 
			
		||||
    ESP_LOGW(TAG, "Handshake timeout");
 | 
			
		||||
    this->cleanup_connection_();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Try to read first byte of magic bytes
 | 
			
		||||
  uint8_t first_byte;
 | 
			
		||||
  ssize_t read = this->client_->read(&first_byte, 1);
 | 
			
		||||
 | 
			
		||||
  if (read == -1 && (errno == EAGAIN || errno == EWOULDBLOCK)) {
 | 
			
		||||
    return;  // No data yet, try again next loop
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (read <= 0) {
 | 
			
		||||
    // Error or connection closed
 | 
			
		||||
    if (read == -1) {
 | 
			
		||||
      this->log_socket_error_("reading first byte");
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGW(TAG, "Remote closed during handshake");
 | 
			
		||||
    }
 | 
			
		||||
    this->cleanup_connection_();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Got first byte, check if it's the magic byte
 | 
			
		||||
  if (first_byte != 0x6C) {
 | 
			
		||||
    ESP_LOGW(TAG, "Invalid initial byte: 0x%02X", first_byte);
 | 
			
		||||
    this->cleanup_connection_();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // First byte is valid, continue with data handling
 | 
			
		||||
  this->handle_data_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPHomeOTAComponent::handle_data_() {
 | 
			
		||||
  /// Handle the OTA data transfer and update process.
 | 
			
		||||
  ///
 | 
			
		||||
  /// This method is blocking and will not return until the OTA update completes,
 | 
			
		||||
  /// fails, or times out. It handles authentication, receives the firmware data,
 | 
			
		||||
  /// writes it to flash, and reboots on success.
 | 
			
		||||
void ESPHomeOTAComponent::handle_() {
 | 
			
		||||
  ota::OTAResponseTypes error_code = ota::OTA_RESPONSE_ERROR_UNKNOWN;
 | 
			
		||||
  bool update_started = false;
 | 
			
		||||
  size_t total = 0;
 | 
			
		||||
@@ -186,14 +108,38 @@ void ESPHomeOTAComponent::handle_data_() {
 | 
			
		||||
  size_t size_acknowledged = 0;
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Read remaining 4 bytes of magic (we already read the first byte 0x6C in handle_handshake_)
 | 
			
		||||
  if (!this->readall_(buf, 4)) {
 | 
			
		||||
    this->log_read_error_("magic bytes");
 | 
			
		||||
  if (this->client_ == nullptr) {
 | 
			
		||||
    // We already checked server_->ready() in loop(), so we can accept directly
 | 
			
		||||
    struct sockaddr_storage source_addr;
 | 
			
		||||
    socklen_t addr_len = sizeof(source_addr);
 | 
			
		||||
    this->client_ = this->server_->accept((struct sockaddr *) &source_addr, &addr_len);
 | 
			
		||||
    if (this->client_ == nullptr)
 | 
			
		||||
      return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int enable = 1;
 | 
			
		||||
  int err = this->client_->setsockopt(IPPROTO_TCP, TCP_NODELAY, &enable, sizeof(int));
 | 
			
		||||
  if (err != 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Socket could not enable TCP nodelay, errno %d", errno);
 | 
			
		||||
    this->client_->close();
 | 
			
		||||
    this->client_ = nullptr;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "Starting update from %s", this->client_->getpeername().c_str());
 | 
			
		||||
  this->status_set_warning();
 | 
			
		||||
#ifdef USE_OTA_STATE_CALLBACK
 | 
			
		||||
  this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  if (!this->readall_(buf, 5)) {
 | 
			
		||||
    ESP_LOGW(TAG, "Reading magic bytes failed");
 | 
			
		||||
    goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
 | 
			
		||||
  }
 | 
			
		||||
  // Check remaining magic bytes: 0x26, 0xF7, 0x5C, 0x45
 | 
			
		||||
  if (buf[0] != 0x26 || buf[1] != 0xF7 || buf[2] != 0x5C || buf[3] != 0x45) {
 | 
			
		||||
    ESP_LOGW(TAG, "Magic bytes mismatch! 0x6C-0x%02X-0x%02X-0x%02X-0x%02X", buf[0], buf[1], buf[2], buf[3]);
 | 
			
		||||
  // 0x6C, 0x26, 0xF7, 0x5C, 0x45
 | 
			
		||||
  if (buf[0] != 0x6C || buf[1] != 0x26 || buf[2] != 0xF7 || buf[3] != 0x5C || buf[4] != 0x45) {
 | 
			
		||||
    ESP_LOGW(TAG, "Magic bytes do not match! 0x%02X-0x%02X-0x%02X-0x%02X-0x%02X", buf[0], buf[1], buf[2], buf[3],
 | 
			
		||||
             buf[4]);
 | 
			
		||||
    error_code = ota::OTA_RESPONSE_ERROR_MAGIC;
 | 
			
		||||
    goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
 | 
			
		||||
  }
 | 
			
		||||
@@ -207,7 +153,7 @@ void ESPHomeOTAComponent::handle_data_() {
 | 
			
		||||
 | 
			
		||||
  // Read features - 1 byte
 | 
			
		||||
  if (!this->readall_(buf, 1)) {
 | 
			
		||||
    this->log_read_error_("features");
 | 
			
		||||
    ESP_LOGW(TAG, "Reading features failed");
 | 
			
		||||
    goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
 | 
			
		||||
  }
 | 
			
		||||
  ota_features = buf[0];  // NOLINT
 | 
			
		||||
@@ -286,7 +232,7 @@ void ESPHomeOTAComponent::handle_data_() {
 | 
			
		||||
 | 
			
		||||
  // Read size, 4 bytes MSB first
 | 
			
		||||
  if (!this->readall_(buf, 4)) {
 | 
			
		||||
    this->log_read_error_("size");
 | 
			
		||||
    ESP_LOGW(TAG, "Reading size failed");
 | 
			
		||||
    goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
 | 
			
		||||
  }
 | 
			
		||||
  ota_size = 0;
 | 
			
		||||
@@ -296,17 +242,6 @@ void ESPHomeOTAComponent::handle_data_() {
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGV(TAG, "Size is %u bytes", ota_size);
 | 
			
		||||
 | 
			
		||||
  // Now that we've passed authentication and are actually
 | 
			
		||||
  // starting the update, set the warning status and notify
 | 
			
		||||
  // listeners. This ensures that port scanners do not
 | 
			
		||||
  // accidentally trigger the update process.
 | 
			
		||||
  this->log_start_("update");
 | 
			
		||||
  this->status_set_warning();
 | 
			
		||||
#ifdef USE_OTA_STATE_CALLBACK
 | 
			
		||||
  this->state_callback_.call(ota::OTA_STARTED, 0.0f, 0);
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // This will block for a few seconds as it locks flash
 | 
			
		||||
  error_code = backend->begin(ota_size);
 | 
			
		||||
  if (error_code != ota::OTA_RESPONSE_OK)
 | 
			
		||||
    goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
 | 
			
		||||
@@ -318,7 +253,7 @@ void ESPHomeOTAComponent::handle_data_() {
 | 
			
		||||
 | 
			
		||||
  // Read binary MD5, 32 bytes
 | 
			
		||||
  if (!this->readall_(buf, 32)) {
 | 
			
		||||
    this->log_read_error_("MD5 checksum");
 | 
			
		||||
    ESP_LOGW(TAG, "Reading binary MD5 checksum failed");
 | 
			
		||||
    goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
 | 
			
		||||
  }
 | 
			
		||||
  sbuf[32] = '\0';
 | 
			
		||||
@@ -335,22 +270,23 @@ void ESPHomeOTAComponent::handle_data_() {
 | 
			
		||||
    ssize_t read = this->client_->read(buf, requested);
 | 
			
		||||
    if (read == -1) {
 | 
			
		||||
      if (errno == EAGAIN || errno == EWOULDBLOCK) {
 | 
			
		||||
        this->yield_and_feed_watchdog_();
 | 
			
		||||
        App.feed_wdt();
 | 
			
		||||
        delay(1);
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      ESP_LOGW(TAG, "Read error, errno %d", errno);
 | 
			
		||||
      ESP_LOGW(TAG, "Error receiving data for update, errno %d", errno);
 | 
			
		||||
      goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
 | 
			
		||||
    } else if (read == 0) {
 | 
			
		||||
      // $ man recv
 | 
			
		||||
      // "When  a  stream socket peer has performed an orderly shutdown, the return value will
 | 
			
		||||
      // be 0 (the traditional "end-of-file" return)."
 | 
			
		||||
      ESP_LOGW(TAG, "Remote closed connection");
 | 
			
		||||
      ESP_LOGW(TAG, "Remote end closed connection");
 | 
			
		||||
      goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    error_code = backend->write(buf, read);
 | 
			
		||||
    if (error_code != ota::OTA_RESPONSE_OK) {
 | 
			
		||||
      ESP_LOGW(TAG, "Flash write error, code: %d", error_code);
 | 
			
		||||
      ESP_LOGW(TAG, "Error writing binary data to flash!, error_code: %d", error_code);
 | 
			
		||||
      goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
 | 
			
		||||
    }
 | 
			
		||||
    total += read;
 | 
			
		||||
@@ -371,7 +307,8 @@ void ESPHomeOTAComponent::handle_data_() {
 | 
			
		||||
      this->state_callback_.call(ota::OTA_IN_PROGRESS, percentage, 0);
 | 
			
		||||
#endif
 | 
			
		||||
      // feed watchdog and give other tasks a chance to run
 | 
			
		||||
      this->yield_and_feed_watchdog_();
 | 
			
		||||
      App.feed_wdt();
 | 
			
		||||
      yield();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -381,7 +318,7 @@ void ESPHomeOTAComponent::handle_data_() {
 | 
			
		||||
 | 
			
		||||
  error_code = backend->end();
 | 
			
		||||
  if (error_code != ota::OTA_RESPONSE_OK) {
 | 
			
		||||
    ESP_LOGW(TAG, "Error ending update! code: %d", error_code);
 | 
			
		||||
    ESP_LOGW(TAG, "Error ending update! error_code: %d", error_code);
 | 
			
		||||
    goto error;  // NOLINT(cppcoreguidelines-avoid-goto)
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -391,11 +328,12 @@ void ESPHomeOTAComponent::handle_data_() {
 | 
			
		||||
 | 
			
		||||
  // Read ACK
 | 
			
		||||
  if (!this->readall_(buf, 1) || buf[0] != ota::OTA_RESPONSE_OK) {
 | 
			
		||||
    this->log_read_error_("ack");
 | 
			
		||||
    ESP_LOGW(TAG, "Reading back acknowledgement failed");
 | 
			
		||||
    // do not go to error, this is not fatal
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->cleanup_connection_();
 | 
			
		||||
  this->client_->close();
 | 
			
		||||
  this->client_ = nullptr;
 | 
			
		||||
  delay(10);
 | 
			
		||||
  ESP_LOGI(TAG, "Update complete");
 | 
			
		||||
  this->status_clear_warning();
 | 
			
		||||
@@ -408,7 +346,8 @@ void ESPHomeOTAComponent::handle_data_() {
 | 
			
		||||
error:
 | 
			
		||||
  buf[0] = static_cast<uint8_t>(error_code);
 | 
			
		||||
  this->writeall_(buf, 1);
 | 
			
		||||
  this->cleanup_connection_();
 | 
			
		||||
  this->client_->close();
 | 
			
		||||
  this->client_ = nullptr;
 | 
			
		||||
 | 
			
		||||
  if (backend != nullptr && update_started) {
 | 
			
		||||
    backend->abort();
 | 
			
		||||
@@ -425,24 +364,28 @@ bool ESPHomeOTAComponent::readall_(uint8_t *buf, size_t len) {
 | 
			
		||||
  uint32_t at = 0;
 | 
			
		||||
  while (len - at > 0) {
 | 
			
		||||
    uint32_t now = millis();
 | 
			
		||||
    if (now - start > OTA_SOCKET_TIMEOUT_DATA) {
 | 
			
		||||
      ESP_LOGW(TAG, "Timeout reading %d bytes", len);
 | 
			
		||||
    if (now - start > 1000) {
 | 
			
		||||
      ESP_LOGW(TAG, "Timed out reading %d bytes of data", len);
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ssize_t read = this->client_->read(buf + at, len - at);
 | 
			
		||||
    if (read == -1) {
 | 
			
		||||
      if (errno != EAGAIN && errno != EWOULDBLOCK) {
 | 
			
		||||
        ESP_LOGW(TAG, "Error reading %d bytes, errno %d", len, errno);
 | 
			
		||||
        return false;
 | 
			
		||||
      if (errno == EAGAIN || errno == EWOULDBLOCK) {
 | 
			
		||||
        App.feed_wdt();
 | 
			
		||||
        delay(1);
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      ESP_LOGW(TAG, "Failed to read %d bytes of data, errno %d", len, errno);
 | 
			
		||||
      return false;
 | 
			
		||||
    } else if (read == 0) {
 | 
			
		||||
      ESP_LOGW(TAG, "Remote closed connection");
 | 
			
		||||
      return false;
 | 
			
		||||
    } else {
 | 
			
		||||
      at += read;
 | 
			
		||||
    }
 | 
			
		||||
    this->yield_and_feed_watchdog_();
 | 
			
		||||
    App.feed_wdt();
 | 
			
		||||
    delay(1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return true;
 | 
			
		||||
@@ -452,21 +395,25 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) {
 | 
			
		||||
  uint32_t at = 0;
 | 
			
		||||
  while (len - at > 0) {
 | 
			
		||||
    uint32_t now = millis();
 | 
			
		||||
    if (now - start > OTA_SOCKET_TIMEOUT_DATA) {
 | 
			
		||||
      ESP_LOGW(TAG, "Timeout writing %d bytes", len);
 | 
			
		||||
    if (now - start > 1000) {
 | 
			
		||||
      ESP_LOGW(TAG, "Timed out writing %d bytes of data", len);
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    ssize_t written = this->client_->write(buf + at, len - at);
 | 
			
		||||
    if (written == -1) {
 | 
			
		||||
      if (errno != EAGAIN && errno != EWOULDBLOCK) {
 | 
			
		||||
        ESP_LOGW(TAG, "Error writing %d bytes, errno %d", len, errno);
 | 
			
		||||
        return false;
 | 
			
		||||
      if (errno == EAGAIN || errno == EWOULDBLOCK) {
 | 
			
		||||
        App.feed_wdt();
 | 
			
		||||
        delay(1);
 | 
			
		||||
        continue;
 | 
			
		||||
      }
 | 
			
		||||
      ESP_LOGW(TAG, "Failed to write %d bytes of data, errno %d", len, errno);
 | 
			
		||||
      return false;
 | 
			
		||||
    } else {
 | 
			
		||||
      at += written;
 | 
			
		||||
    }
 | 
			
		||||
    this->yield_and_feed_watchdog_();
 | 
			
		||||
    App.feed_wdt();
 | 
			
		||||
    delay(1);
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
@@ -474,25 +421,5 @@ bool ESPHomeOTAComponent::writeall_(const uint8_t *buf, size_t len) {
 | 
			
		||||
float ESPHomeOTAComponent::get_setup_priority() const { return setup_priority::AFTER_WIFI; }
 | 
			
		||||
uint16_t ESPHomeOTAComponent::get_port() const { return this->port_; }
 | 
			
		||||
void ESPHomeOTAComponent::set_port(uint16_t port) { this->port_ = port; }
 | 
			
		||||
 | 
			
		||||
void ESPHomeOTAComponent::log_socket_error_(const char *msg) { ESP_LOGW(TAG, "Socket %s: errno %d", msg, errno); }
 | 
			
		||||
 | 
			
		||||
void ESPHomeOTAComponent::log_read_error_(const char *what) { ESP_LOGW(TAG, "Read %s failed", what); }
 | 
			
		||||
 | 
			
		||||
void ESPHomeOTAComponent::log_start_(const char *phase) {
 | 
			
		||||
  ESP_LOGD(TAG, "Starting %s from %s", phase, this->client_->getpeername().c_str());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPHomeOTAComponent::cleanup_connection_() {
 | 
			
		||||
  this->client_->close();
 | 
			
		||||
  this->client_ = nullptr;
 | 
			
		||||
  this->client_connect_time_ = 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPHomeOTAComponent::yield_and_feed_watchdog_() {
 | 
			
		||||
  App.feed_wdt();
 | 
			
		||||
  delay(1);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
#endif
 | 
			
		||||
 
 | 
			
		||||
@@ -27,22 +27,15 @@ class ESPHomeOTAComponent : public ota::OTAComponent {
 | 
			
		||||
  uint16_t get_port() const;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void handle_handshake_();
 | 
			
		||||
  void handle_data_();
 | 
			
		||||
  void handle_();
 | 
			
		||||
  bool readall_(uint8_t *buf, size_t len);
 | 
			
		||||
  bool writeall_(const uint8_t *buf, size_t len);
 | 
			
		||||
  void log_socket_error_(const char *msg);
 | 
			
		||||
  void log_read_error_(const char *what);
 | 
			
		||||
  void log_start_(const char *phase);
 | 
			
		||||
  void cleanup_connection_();
 | 
			
		||||
  void yield_and_feed_watchdog_();
 | 
			
		||||
 | 
			
		||||
#ifdef USE_OTA_PASSWORD
 | 
			
		||||
  std::string password_;
 | 
			
		||||
#endif  // USE_OTA_PASSWORD
 | 
			
		||||
 | 
			
		||||
  uint16_t port_;
 | 
			
		||||
  uint32_t client_connect_time_{0};
 | 
			
		||||
 | 
			
		||||
  std::unique_ptr<socket::Socket> server_;
 | 
			
		||||
  std::unique_ptr<socket::Socket> client_;
 | 
			
		||||
 
 | 
			
		||||
@@ -65,6 +65,15 @@ CONF_WAIT_FOR_SENT = "wait_for_sent"
 | 
			
		||||
MAX_ESPNOW_PACKET_SIZE = 250  # Maximum size of the payload in bytes
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def _validate_unknown_peer(config):
 | 
			
		||||
    if config[CONF_AUTO_ADD_PEER] and config.get(CONF_ON_UNKNOWN_PEER):
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            f"'{CONF_ON_UNKNOWN_PEER}' cannot be used when '{CONF_AUTO_ADD_PEER}' is enabled.",
 | 
			
		||||
            path=[CONF_ON_UNKNOWN_PEER],
 | 
			
		||||
        )
 | 
			
		||||
    return config
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
@@ -94,6 +103,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
        },
 | 
			
		||||
    ).extend(cv.COMPONENT_SCHEMA),
 | 
			
		||||
    cv.only_on_esp32,
 | 
			
		||||
    _validate_unknown_peer,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@@ -114,6 +124,7 @@ async def _trigger_to_code(config):
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    print(config)
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -40,20 +40,20 @@ template<typename... Ts> class SendAction : public Action<Ts...>, public Parente
 | 
			
		||||
    this->num_running_++;
 | 
			
		||||
    send_callback_t send_callback = [this, x...](esp_err_t status) {
 | 
			
		||||
      if (status == ESP_OK) {
 | 
			
		||||
        if (!this->sent_.empty()) {
 | 
			
		||||
          this->sent_.play(x...);
 | 
			
		||||
        } else if (this->flags_.wait_for_sent) {
 | 
			
		||||
        if (this->sent_.empty() && this->flags_.wait_for_sent) {
 | 
			
		||||
          this->play_next_(x...);
 | 
			
		||||
        } else if (!this->sent_.empty()) {
 | 
			
		||||
          this->sent_.play(x...);
 | 
			
		||||
        }
 | 
			
		||||
      } else {
 | 
			
		||||
        if (!this->error_.empty()) {
 | 
			
		||||
          this->error_.play(x...);
 | 
			
		||||
        } else if (this->flags_.wait_for_sent) {
 | 
			
		||||
        if (this->error_.empty() && this->flags_.wait_for_sent) {
 | 
			
		||||
          if (this->flags_.continue_on_error) {
 | 
			
		||||
            this->play_next_(x...);
 | 
			
		||||
          } else {
 | 
			
		||||
            this->stop_complex();
 | 
			
		||||
          }
 | 
			
		||||
        } else if (!this->error_.empty()) {
 | 
			
		||||
          this->error_.play(x...);
 | 
			
		||||
        }
 | 
			
		||||
      }
 | 
			
		||||
    };
 | 
			
		||||
 
 | 
			
		||||
@@ -154,7 +154,7 @@ void ESPNowComponent::setup() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void ESPNowComponent::enable() {
 | 
			
		||||
  if (this->state_ == ESPNOW_STATE_ENABLED)
 | 
			
		||||
  if (this->state_ != ESPNOW_STATE_ENABLED)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "Enabling");
 | 
			
		||||
@@ -178,7 +178,11 @@ void ESPNowComponent::enable_() {
 | 
			
		||||
 | 
			
		||||
    this->apply_wifi_channel();
 | 
			
		||||
  }
 | 
			
		||||
  this->get_wifi_channel();
 | 
			
		||||
#ifdef USE_WIFI
 | 
			
		||||
  else {
 | 
			
		||||
    this->wifi_channel_ = wifi::global_wifi_component->get_wifi_channel();
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  esp_err_t err = esp_now_init();
 | 
			
		||||
  if (err != ESP_OK) {
 | 
			
		||||
@@ -211,7 +215,6 @@ void ESPNowComponent::enable_() {
 | 
			
		||||
  for (auto peer : this->peers_) {
 | 
			
		||||
    this->add_peer(peer.address);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->state_ = ESPNOW_STATE_ENABLED;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -225,6 +228,10 @@ void ESPNowComponent::disable() {
 | 
			
		||||
  esp_now_unregister_recv_cb();
 | 
			
		||||
  esp_now_unregister_send_cb();
 | 
			
		||||
 | 
			
		||||
  for (auto peer : this->peers_) {
 | 
			
		||||
    this->del_peer(peer.address);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  esp_err_t err = esp_now_deinit();
 | 
			
		||||
  if (err != ESP_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "esp_now_deinit failed! 0x%x", err);
 | 
			
		||||
@@ -260,6 +267,7 @@ void ESPNowComponent::loop() {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Process received packets
 | 
			
		||||
  ESPNowPacket *packet = this->receive_packet_queue_.pop();
 | 
			
		||||
  while (packet != nullptr) {
 | 
			
		||||
@@ -267,15 +275,13 @@ void ESPNowComponent::loop() {
 | 
			
		||||
      case ESPNowPacket::RECEIVED: {
 | 
			
		||||
        const ESPNowRecvInfo info = packet->get_receive_info();
 | 
			
		||||
        if (!esp_now_is_peer_exist(info.src_addr)) {
 | 
			
		||||
          bool handled = false;
 | 
			
		||||
          for (auto *handler : this->unknown_peer_handlers_) {
 | 
			
		||||
            if (handler->on_unknown_peer(info, packet->packet_.receive.data, packet->packet_.receive.size)) {
 | 
			
		||||
              handled = true;
 | 
			
		||||
              break;  // If a handler returns true, stop processing further handlers
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          if (!handled && this->auto_add_peer_) {
 | 
			
		||||
          if (this->auto_add_peer_) {
 | 
			
		||||
            this->add_peer(info.src_addr);
 | 
			
		||||
          } else {
 | 
			
		||||
            for (auto *handler : this->unknown_peer_handlers_) {
 | 
			
		||||
              if (handler->on_unknown_peer(info, packet->packet_.receive.data, packet->packet_.receive.size))
 | 
			
		||||
                break;  // If a handler returns true, stop processing further handlers
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
        }
 | 
			
		||||
        // Intentionally left as if instead of else in case the peer is added above
 | 
			
		||||
@@ -337,12 +343,6 @@ void ESPNowComponent::loop() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint8_t ESPNowComponent::get_wifi_channel() {
 | 
			
		||||
  wifi_second_chan_t dummy;
 | 
			
		||||
  esp_wifi_get_channel(&this->wifi_channel_, &dummy);
 | 
			
		||||
  return this->wifi_channel_;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
esp_err_t ESPNowComponent::send(const uint8_t *peer_address, const uint8_t *payload, size_t size,
 | 
			
		||||
                                const send_callback_t &callback) {
 | 
			
		||||
  if (this->state_ != ESPNOW_STATE_ENABLED) {
 | 
			
		||||
 
 | 
			
		||||
@@ -110,7 +110,6 @@ class ESPNowComponent : public Component {
 | 
			
		||||
 | 
			
		||||
  void set_wifi_channel(uint8_t channel) { this->wifi_channel_ = channel; }
 | 
			
		||||
  void apply_wifi_channel();
 | 
			
		||||
  uint8_t get_wifi_channel();
 | 
			
		||||
 | 
			
		||||
  void set_auto_add_peer(bool value) { this->auto_add_peer_ = value; }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -49,7 +49,7 @@ class ESPNowPacket {
 | 
			
		||||
#if ESP_IDF_VERSION >= ESP_IDF_VERSION_VAL(5, 5, 0)
 | 
			
		||||
  // Constructor for sent data
 | 
			
		||||
  ESPNowPacket(const esp_now_send_info_t *info, esp_now_send_status_t status) {
 | 
			
		||||
    this->init_sent_data_(info->src_addr, status);
 | 
			
		||||
    this->init_sent_data(info->src_addr, status);
 | 
			
		||||
  }
 | 
			
		||||
#else
 | 
			
		||||
  // Constructor for sent data
 | 
			
		||||
 
 | 
			
		||||
@@ -2,11 +2,10 @@
 | 
			
		||||
 | 
			
		||||
#include <array>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <cstring>
 | 
			
		||||
#include <limits>
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome::gpio_expander {
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace gpio_expander {
 | 
			
		||||
 | 
			
		||||
/// @brief A class to cache the read state of a GPIO expander.
 | 
			
		||||
///        This class caches reads between GPIO Pins which are on the same bank.
 | 
			
		||||
@@ -18,22 +17,12 @@ namespace esphome::gpio_expander {
 | 
			
		||||
///           N - Number of pins
 | 
			
		||||
template<typename T, T N> class CachedGpioExpander {
 | 
			
		||||
 public:
 | 
			
		||||
  /// @brief Read the state of the given pin. This will invalidate the cache for the given pin number.
 | 
			
		||||
  /// @param pin Pin number to read
 | 
			
		||||
  /// @return Pin state
 | 
			
		||||
  bool digital_read(T pin) {
 | 
			
		||||
    const uint8_t bank = pin / BANK_SIZE;
 | 
			
		||||
    const T pin_mask = (1 << (pin % BANK_SIZE));
 | 
			
		||||
    // Check if specific pin cache is valid
 | 
			
		||||
    if (this->read_cache_valid_[bank] & pin_mask) {
 | 
			
		||||
      // Invalidate pin
 | 
			
		||||
      this->read_cache_valid_[bank] &= ~pin_mask;
 | 
			
		||||
    } else {
 | 
			
		||||
      // Read whole bank from hardware
 | 
			
		||||
    uint8_t bank = pin / (sizeof(T) * BITS_PER_BYTE);
 | 
			
		||||
    if (this->read_cache_invalidated_[bank]) {
 | 
			
		||||
      this->read_cache_invalidated_[bank] = false;
 | 
			
		||||
      if (!this->digital_read_hw(pin))
 | 
			
		||||
        return false;
 | 
			
		||||
      // Mark bank cache as valid except the pin that is being returned now
 | 
			
		||||
      this->read_cache_valid_[bank] = std::numeric_limits<T>::max() & ~pin_mask;
 | 
			
		||||
    }
 | 
			
		||||
    return this->digital_read_cache(pin);
 | 
			
		||||
  }
 | 
			
		||||
@@ -47,16 +36,18 @@ template<typename T, T N> class CachedGpioExpander {
 | 
			
		||||
  virtual bool digital_read_cache(T pin) = 0;
 | 
			
		||||
  /// @brief Call component low level function to write GPIO state to device
 | 
			
		||||
  virtual void digital_write_hw(T pin, bool value) = 0;
 | 
			
		||||
  const uint8_t cache_byte_size_ = N / (sizeof(T) * BITS_PER_BYTE);
 | 
			
		||||
 | 
			
		||||
  /// @brief Invalidate cache. This function should be called in component loop().
 | 
			
		||||
  void reset_pin_cache_() { memset(this->read_cache_valid_, 0x00, CACHE_SIZE_BYTES); }
 | 
			
		||||
  void reset_pin_cache_() {
 | 
			
		||||
    for (T i = 0; i < this->cache_byte_size_; i++) {
 | 
			
		||||
      this->read_cache_invalidated_[i] = true;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  static constexpr uint8_t BITS_PER_BYTE = 8;
 | 
			
		||||
  static constexpr uint8_t BANK_SIZE = sizeof(T) * BITS_PER_BYTE;
 | 
			
		||||
  static constexpr size_t BANKS = N / BANK_SIZE;
 | 
			
		||||
  static constexpr size_t CACHE_SIZE_BYTES = BANKS * sizeof(T);
 | 
			
		||||
 | 
			
		||||
  T read_cache_valid_[BANKS]{0};
 | 
			
		||||
  static const uint8_t BITS_PER_BYTE = 8;
 | 
			
		||||
  std::array<bool, N / (sizeof(T) * BITS_PER_BYTE)> read_cache_invalidated_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome::gpio_expander
 | 
			
		||||
}  // namespace gpio_expander
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -20,11 +20,12 @@ static const size_t MAX_BUTTONS = 4;  // max number of buttons scanned
 | 
			
		||||
 | 
			
		||||
#define ERROR_CHECK(err) \
 | 
			
		||||
  if ((err) != i2c::ERROR_OK) { \
 | 
			
		||||
    this->status_set_warning(ESP_LOG_MSG_COMM_FAIL); \
 | 
			
		||||
    this->status_set_warning("Communication failure"); \
 | 
			
		||||
    return; \
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
void GT911Touchscreen::setup() {
 | 
			
		||||
  i2c::ErrorCode err;
 | 
			
		||||
  if (this->reset_pin_ != nullptr) {
 | 
			
		||||
    this->reset_pin_->setup();
 | 
			
		||||
    this->reset_pin_->digital_write(false);
 | 
			
		||||
@@ -34,14 +35,9 @@ void GT911Touchscreen::setup() {
 | 
			
		||||
      this->interrupt_pin_->digital_write(false);
 | 
			
		||||
    }
 | 
			
		||||
    delay(2);
 | 
			
		||||
    this->reset_pin_->digital_write(true);  // wait 50ms after reset
 | 
			
		||||
    this->set_timeout(50, [this] { this->setup_internal_(); });
 | 
			
		||||
    return;
 | 
			
		||||
    this->reset_pin_->digital_write(true);
 | 
			
		||||
    delay(50);  // NOLINT
 | 
			
		||||
  }
 | 
			
		||||
  this->setup_internal_();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void GT911Touchscreen::setup_internal_() {
 | 
			
		||||
  if (this->interrupt_pin_ != nullptr) {
 | 
			
		||||
    // set pre-configured input mode
 | 
			
		||||
    this->interrupt_pin_->setup();
 | 
			
		||||
@@ -49,7 +45,7 @@ void GT911Touchscreen::setup_internal_() {
 | 
			
		||||
 | 
			
		||||
  // check the configuration of the int line.
 | 
			
		||||
  uint8_t data[4];
 | 
			
		||||
  i2c::ErrorCode err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
 | 
			
		||||
  err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
 | 
			
		||||
  if (err != i2c::ERROR_OK && this->address_ == PRIMARY_ADDRESS) {
 | 
			
		||||
    this->address_ = SECONDARY_ADDRESS;
 | 
			
		||||
    err = this->write(GET_SWITCHES, sizeof(GET_SWITCHES));
 | 
			
		||||
@@ -57,7 +53,7 @@ void GT911Touchscreen::setup_internal_() {
 | 
			
		||||
  if (err == i2c::ERROR_OK) {
 | 
			
		||||
    err = this->read(data, 1);
 | 
			
		||||
    if (err == i2c::ERROR_OK) {
 | 
			
		||||
      ESP_LOGD(TAG, "Switches ADDR: 0x%02X DATA: 0x%02X", this->address_, data[0]);
 | 
			
		||||
      ESP_LOGD(TAG, "Read from switches at address 0x%02X: 0x%02X", this->address_, data[0]);
 | 
			
		||||
      if (this->interrupt_pin_ != nullptr) {
 | 
			
		||||
        this->attach_interrupt_(this->interrupt_pin_,
 | 
			
		||||
                                (data[0] & 1) ? gpio::INTERRUPT_FALLING_EDGE : gpio::INTERRUPT_RISING_EDGE);
 | 
			
		||||
@@ -79,24 +75,16 @@ void GT911Touchscreen::setup_internal_() {
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (err != i2c::ERROR_OK) {
 | 
			
		||||
      this->mark_failed("Calibration error");
 | 
			
		||||
      this->mark_failed("Failed to read calibration");
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (err != i2c::ERROR_OK) {
 | 
			
		||||
    this->mark_failed(ESP_LOG_MSG_COMM_FAIL);
 | 
			
		||||
    return;
 | 
			
		||||
    this->mark_failed("Failed to communicate");
 | 
			
		||||
  }
 | 
			
		||||
  this->setup_done_ = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void GT911Touchscreen::update_touches() {
 | 
			
		||||
  this->skip_update_ = true;  // skip send touch events by default, set to false after successful error checks
 | 
			
		||||
  if (!this->setup_done_) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  i2c::ErrorCode err;
 | 
			
		||||
  uint8_t touch_state = 0;
 | 
			
		||||
  uint8_t data[MAX_TOUCHES + 1][8];  // 8 bytes each for each point, plus extra space for the key byte
 | 
			
		||||
@@ -109,6 +97,7 @@ void GT911Touchscreen::update_touches() {
 | 
			
		||||
  uint8_t num_of_touches = touch_state & 0x07;
 | 
			
		||||
 | 
			
		||||
  if ((touch_state & 0x80) == 0 || num_of_touches > MAX_TOUCHES) {
 | 
			
		||||
    this->skip_update_ = true;  // skip send touch events, touchscreen is not ready yet.
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
@@ -118,7 +107,6 @@ void GT911Touchscreen::update_touches() {
 | 
			
		||||
  err = this->read(data[0], sizeof(data[0]) * num_of_touches + 1);
 | 
			
		||||
  ERROR_CHECK(err);
 | 
			
		||||
 | 
			
		||||
  this->skip_update_ = false;  // All error checks passed, send touch events
 | 
			
		||||
  for (uint8_t i = 0; i != num_of_touches; i++) {
 | 
			
		||||
    uint16_t id = data[i][0];
 | 
			
		||||
    uint16_t x = encode_uint16(data[i][2], data[i][1]);
 | 
			
		||||
 
 | 
			
		||||
@@ -15,20 +15,8 @@ class GT911ButtonListener {
 | 
			
		||||
 | 
			
		||||
class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  /// @brief Initialize the GT911 touchscreen.
 | 
			
		||||
  ///
 | 
			
		||||
  /// If @ref reset_pin_ is set, the touchscreen will be hardware reset,
 | 
			
		||||
  /// and the rest of the setup will be scheduled to run 50ms later using @ref set_timeout()
 | 
			
		||||
  /// to allow the device to stabilize after reset.
 | 
			
		||||
  ///
 | 
			
		||||
  /// If @ref interrupt_pin_ is set, it will be temporarily configured during reset
 | 
			
		||||
  /// to control I2C address selection.
 | 
			
		||||
  ///
 | 
			
		||||
  /// After the timeout, or immediately if no reset is performed, @ref setup_internal_()
 | 
			
		||||
  /// is called to complete the initialization.
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  bool can_proceed() override { return this->setup_done_; }
 | 
			
		||||
 | 
			
		||||
  void set_interrupt_pin(InternalGPIOPin *pin) { this->interrupt_pin_ = pin; }
 | 
			
		||||
  void set_reset_pin(GPIOPin *pin) { this->reset_pin_ = pin; }
 | 
			
		||||
@@ -37,20 +25,8 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
 | 
			
		||||
 protected:
 | 
			
		||||
  void update_touches() override;
 | 
			
		||||
 | 
			
		||||
  /// @brief Perform the internal setup routine for the GT911 touchscreen.
 | 
			
		||||
  ///
 | 
			
		||||
  /// This function checks the I2C address, configures the interrupt pin (if available),
 | 
			
		||||
  /// reads the touchscreen mode from the controller, and attempts to read calibration
 | 
			
		||||
  /// data (maximum X and Y values) if not already set.
 | 
			
		||||
  ///
 | 
			
		||||
  /// On success, sets @ref setup_done_ to true.
 | 
			
		||||
  /// On failure, calls @ref mark_failed() with an appropriate error message.
 | 
			
		||||
  void setup_internal_();
 | 
			
		||||
  /// @brief True if the touchscreen setup has completed successfully.
 | 
			
		||||
  bool setup_done_{false};
 | 
			
		||||
 | 
			
		||||
  InternalGPIOPin *interrupt_pin_{nullptr};
 | 
			
		||||
  GPIOPin *reset_pin_{nullptr};
 | 
			
		||||
  InternalGPIOPin *interrupt_pin_{};
 | 
			
		||||
  GPIOPin *reset_pin_{};
 | 
			
		||||
  std::vector<GT911ButtonListener *> button_listeners_;
 | 
			
		||||
  uint8_t button_state_{0xFF};  // last button state. Initial FF guarantees first update.
 | 
			
		||||
};
 | 
			
		||||
 
 | 
			
		||||
@@ -24,6 +24,9 @@ static const uint32_t READ_DURATION_MS = 16;
 | 
			
		||||
static const size_t TASK_STACK_SIZE = 4096;
 | 
			
		||||
static const ssize_t TASK_PRIORITY = 23;
 | 
			
		||||
 | 
			
		||||
// Use an exponential moving average to correct a DC offset with weight factor 1/1000
 | 
			
		||||
static const int32_t DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR = 1000;
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "i2s_audio.microphone";
 | 
			
		||||
 | 
			
		||||
enum MicrophoneEventGroupBits : uint32_t {
 | 
			
		||||
@@ -378,57 +381,26 @@ void I2SAudioMicrophone::mic_task(void *params) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void I2SAudioMicrophone::fix_dc_offset_(std::vector<uint8_t> &data) {
 | 
			
		||||
  /**
 | 
			
		||||
   * From https://www.musicdsp.org/en/latest/Filters/135-dc-filter.html:
 | 
			
		||||
   *
 | 
			
		||||
   *     y(n) = x(n) - x(n-1) + R * y(n-1)
 | 
			
		||||
   *     R = 1 - (pi * 2 * frequency / samplerate)
 | 
			
		||||
   *
 | 
			
		||||
   * From https://en.wikipedia.org/wiki/Hearing_range:
 | 
			
		||||
   *     The human range is commonly given as 20Hz up.
 | 
			
		||||
   *
 | 
			
		||||
   * From https://en.wikipedia.org/wiki/High-resolution_audio:
 | 
			
		||||
   *     A reasonable upper bound for sample rate seems to be 96kHz.
 | 
			
		||||
   *
 | 
			
		||||
   * Calculate R value for 20Hz on a 96kHz sample rate:
 | 
			
		||||
   *     R = 1 - (pi * 2 * 20 / 96000)
 | 
			
		||||
   *     R = 0.9986910031
 | 
			
		||||
   *
 | 
			
		||||
   * Transform floating point to bit-shifting approximation:
 | 
			
		||||
   *     output = input - prev_input + R * prev_output
 | 
			
		||||
   *     output = input - prev_input + (prev_output - (prev_output >> S))
 | 
			
		||||
   *
 | 
			
		||||
   * Approximate bit-shift value S from R:
 | 
			
		||||
   *     R = 1 - (1 >> S)
 | 
			
		||||
   *     R = 1 - (1 / 2^S)
 | 
			
		||||
   *     R = 1 - 2^-S
 | 
			
		||||
   *     0.9986910031 = 1 - 2^-S
 | 
			
		||||
   *     S = 9.57732 ~= 10
 | 
			
		||||
   *
 | 
			
		||||
   * Actual R from S:
 | 
			
		||||
   *     R = 1 - 2^-10 = 0.9990234375
 | 
			
		||||
   *
 | 
			
		||||
   * Confirm this has effect outside human hearing on 96000kHz sample:
 | 
			
		||||
   *     0.9990234375 = 1 - (pi * 2 * f / 96000)
 | 
			
		||||
   *     f = 14.9208Hz
 | 
			
		||||
   *
 | 
			
		||||
   * Confirm this has effect outside human hearing on PDM 16kHz sample:
 | 
			
		||||
   *     0.9990234375 = 1 - (pi * 2 * f / 16000)
 | 
			
		||||
   *     f = 2.4868Hz
 | 
			
		||||
   *
 | 
			
		||||
   */
 | 
			
		||||
  const uint8_t dc_filter_shift = 10;
 | 
			
		||||
  const size_t bytes_per_sample = this->audio_stream_info_.samples_to_bytes(1);
 | 
			
		||||
  const uint32_t total_samples = this->audio_stream_info_.bytes_to_samples(data.size());
 | 
			
		||||
 | 
			
		||||
  if (total_samples == 0) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  int64_t offset_accumulator = 0;
 | 
			
		||||
  for (uint32_t sample_index = 0; sample_index < total_samples; ++sample_index) {
 | 
			
		||||
    const uint32_t byte_index = sample_index * bytes_per_sample;
 | 
			
		||||
    int32_t input = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample);
 | 
			
		||||
    int32_t output = input - this->dc_offset_prev_input_ +
 | 
			
		||||
                     (this->dc_offset_prev_output_ - (this->dc_offset_prev_output_ >> dc_filter_shift));
 | 
			
		||||
    this->dc_offset_prev_input_ = input;
 | 
			
		||||
    this->dc_offset_prev_output_ = output;
 | 
			
		||||
    audio::pack_q31_as_audio_sample(output, &data[byte_index], bytes_per_sample);
 | 
			
		||||
    int32_t sample = audio::unpack_audio_sample_to_q31(&data[byte_index], bytes_per_sample);
 | 
			
		||||
    offset_accumulator += sample;
 | 
			
		||||
    sample -= this->dc_offset_;
 | 
			
		||||
    audio::pack_q31_as_audio_sample(sample, &data[byte_index], bytes_per_sample);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const int32_t new_offset = offset_accumulator / total_samples;
 | 
			
		||||
  this->dc_offset_ = new_offset / DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR +
 | 
			
		||||
                     (DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR - 1) * this->dc_offset_ /
 | 
			
		||||
                         DC_OFFSET_MOVING_AVERAGE_COEFFICIENT_DENOMINATOR;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
size_t I2SAudioMicrophone::read_(uint8_t *buf, size_t len, TickType_t ticks_to_wait) {
 | 
			
		||||
 
 | 
			
		||||
@@ -82,8 +82,7 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub
 | 
			
		||||
 | 
			
		||||
  bool correct_dc_offset_;
 | 
			
		||||
  bool locked_driver_{false};
 | 
			
		||||
  int32_t dc_offset_prev_input_{0};
 | 
			
		||||
  int32_t dc_offset_prev_output_{0};
 | 
			
		||||
  int32_t dc_offset_{0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace i2s_audio
 | 
			
		||||
 
 | 
			
		||||
@@ -2,7 +2,7 @@ import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.core import coroutine_with_priority
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
CODEOWNERS = ["@OttoWinter"]
 | 
			
		||||
json_ns = cg.esphome_ns.namespace("json")
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
 
 | 
			
		||||
@@ -14,16 +14,18 @@ ld2410_ns = cg.esphome_ns.namespace("ld2410")
 | 
			
		||||
LD2410Component = ld2410_ns.class_("LD2410Component", cg.Component, uart.UARTDevice)
 | 
			
		||||
 | 
			
		||||
CONF_LD2410_ID = "ld2410_id"
 | 
			
		||||
 | 
			
		||||
CONF_MAX_MOVE_DISTANCE = "max_move_distance"
 | 
			
		||||
CONF_MAX_STILL_DISTANCE = "max_still_distance"
 | 
			
		||||
CONF_MOVE_THRESHOLDS = [f"g{x}_move_threshold" for x in range(9)]
 | 
			
		||||
CONF_STILL_THRESHOLDS = [f"g{x}_still_threshold" for x in range(9)]
 | 
			
		||||
CONF_MOVE_THRESHOLDS = [f"g{x}_move_threshold" for x in range(9)]
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(LD2410Component),
 | 
			
		||||
        cv.Optional(CONF_THROTTLE): cv.invalid(
 | 
			
		||||
            f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead"
 | 
			
		||||
        cv.Optional(CONF_THROTTLE, default="1000ms"): cv.All(
 | 
			
		||||
            cv.positive_time_period_milliseconds,
 | 
			
		||||
            cv.Range(min=cv.TimePeriod(milliseconds=1)),
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_MAX_MOVE_DISTANCE): cv.invalid(
 | 
			
		||||
            f"The '{CONF_MAX_MOVE_DISTANCE}' option has been moved to the '{CONF_MAX_MOVE_DISTANCE}'"
 | 
			
		||||
@@ -73,6 +75,7 @@ async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await uart.register_uart_device(var, config)
 | 
			
		||||
    cg.add(var.set_throttle(config[CONF_THROTTLE]))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CALIBRATION_ACTION_SCHEMA = maybe_simple_id(
 | 
			
		||||
 
 | 
			
		||||
@@ -22,23 +22,19 @@ CONFIG_SCHEMA = {
 | 
			
		||||
    cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
 | 
			
		||||
    cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema(
 | 
			
		||||
        device_class=DEVICE_CLASS_OCCUPANCY,
 | 
			
		||||
        filters=[{"settle": cv.TimePeriod(milliseconds=1000)}],
 | 
			
		||||
        icon=ICON_ACCOUNT,
 | 
			
		||||
    ),
 | 
			
		||||
    cv.Optional(CONF_HAS_MOVING_TARGET): binary_sensor.binary_sensor_schema(
 | 
			
		||||
        device_class=DEVICE_CLASS_MOTION,
 | 
			
		||||
        filters=[{"settle": cv.TimePeriod(milliseconds=1000)}],
 | 
			
		||||
        icon=ICON_MOTION_SENSOR,
 | 
			
		||||
    ),
 | 
			
		||||
    cv.Optional(CONF_HAS_STILL_TARGET): binary_sensor.binary_sensor_schema(
 | 
			
		||||
        device_class=DEVICE_CLASS_OCCUPANCY,
 | 
			
		||||
        filters=[{"settle": cv.TimePeriod(milliseconds=1000)}],
 | 
			
		||||
        icon=ICON_MOTION_SENSOR,
 | 
			
		||||
    ),
 | 
			
		||||
    cv.Optional(CONF_OUT_PIN_PRESENCE_STATUS): binary_sensor.binary_sensor_schema(
 | 
			
		||||
        device_class=DEVICE_CLASS_PRESENCE,
 | 
			
		||||
        entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
        filters=[{"settle": cv.TimePeriod(milliseconds=1000)}],
 | 
			
		||||
        icon=ICON_ACCOUNT,
 | 
			
		||||
    ),
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -188,8 +188,9 @@ void LD2410Component::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG,
 | 
			
		||||
                "LD2410:\n"
 | 
			
		||||
                "  Firmware version: %s\n"
 | 
			
		||||
                "  MAC address: %s",
 | 
			
		||||
                version.c_str(), mac_str.c_str());
 | 
			
		||||
                "  MAC address: %s\n"
 | 
			
		||||
                "  Throttle: %u ms",
 | 
			
		||||
                version.c_str(), mac_str.c_str(), this->throttle_);
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Binary Sensors:");
 | 
			
		||||
  LOG_BINARY_SENSOR("  ", "Target", this->target_binary_sensor_);
 | 
			
		||||
@@ -305,6 +306,11 @@ void LD2410Component::send_command_(uint8_t command, const uint8_t *command_valu
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2410Component::handle_periodic_data_() {
 | 
			
		||||
  // Reduce data update rate to reduce home assistant database growth
 | 
			
		||||
  // Check this first to prevent unnecessary processing done in later checks/parsing
 | 
			
		||||
  if (App.get_loop_component_start_time() - this->last_periodic_millis_ < this->throttle_) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  // 4 frame header bytes + 2 length bytes + 1 data end byte + 1 crc byte + 4 frame footer bytes
 | 
			
		||||
  // data header=0xAA, data footer=0x55, crc=0x00
 | 
			
		||||
  if (this->buffer_pos_ < 12 || !ld2410::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) ||
 | 
			
		||||
@@ -312,6 +318,9 @@ void LD2410Component::handle_periodic_data_() {
 | 
			
		||||
      this->buffer_data_[this->buffer_pos_ - 5] != CHECK) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  // Save the timestamp after validating the frame so, if invalid, we'll take the next frame immediately
 | 
			
		||||
  this->last_periodic_millis_ = App.get_loop_component_start_time();
 | 
			
		||||
 | 
			
		||||
  /*
 | 
			
		||||
    Data Type: 7th
 | 
			
		||||
    0x01: Engineering mode
 | 
			
		||||
 
 | 
			
		||||
@@ -93,6 +93,7 @@ class LD2410Component : public Component, public uart::UARTDevice {
 | 
			
		||||
  void set_gate_move_sensor(uint8_t gate, sensor::Sensor *s);
 | 
			
		||||
  void set_gate_still_sensor(uint8_t gate, sensor::Sensor *s);
 | 
			
		||||
#endif
 | 
			
		||||
  void set_throttle(uint16_t value) { this->throttle_ = value; };
 | 
			
		||||
  void set_bluetooth_password(const std::string &password);
 | 
			
		||||
  void set_engineering_mode(bool enable);
 | 
			
		||||
  void read_all_info();
 | 
			
		||||
@@ -115,6 +116,8 @@ class LD2410Component : public Component, public uart::UARTDevice {
 | 
			
		||||
  void query_light_control_();
 | 
			
		||||
  void restart_();
 | 
			
		||||
 | 
			
		||||
  uint32_t last_periodic_millis_ = 0;
 | 
			
		||||
  uint16_t throttle_ = 0;
 | 
			
		||||
  uint8_t light_function_ = 0;
 | 
			
		||||
  uint8_t light_threshold_ = 0;
 | 
			
		||||
  uint8_t out_pin_level_ = 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -18,50 +18,42 @@ from esphome.const import (
 | 
			
		||||
from . import CONF_LD2410_ID, LD2410Component
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["ld2410"]
 | 
			
		||||
 | 
			
		||||
CONF_STILL_DISTANCE = "still_distance"
 | 
			
		||||
CONF_MOVING_ENERGY = "moving_energy"
 | 
			
		||||
CONF_STILL_ENERGY = "still_energy"
 | 
			
		||||
CONF_DETECTION_DISTANCE = "detection_distance"
 | 
			
		||||
CONF_MOVE_ENERGY = "move_energy"
 | 
			
		||||
CONF_MOVING_ENERGY = "moving_energy"
 | 
			
		||||
CONF_STILL_DISTANCE = "still_distance"
 | 
			
		||||
CONF_STILL_ENERGY = "still_energy"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(CONF_LD2410_ID): cv.use_id(LD2410Component),
 | 
			
		||||
        cv.Optional(CONF_MOVING_DISTANCE): sensor.sensor_schema(
 | 
			
		||||
            device_class=DEVICE_CLASS_DISTANCE,
 | 
			
		||||
            filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
 | 
			
		||||
            icon=ICON_SIGNAL,
 | 
			
		||||
            unit_of_measurement=UNIT_CENTIMETER,
 | 
			
		||||
            icon=ICON_SIGNAL,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_STILL_DISTANCE): sensor.sensor_schema(
 | 
			
		||||
            device_class=DEVICE_CLASS_DISTANCE,
 | 
			
		||||
            filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
 | 
			
		||||
            icon=ICON_SIGNAL,
 | 
			
		||||
            unit_of_measurement=UNIT_CENTIMETER,
 | 
			
		||||
            icon=ICON_SIGNAL,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_MOVING_ENERGY): sensor.sensor_schema(
 | 
			
		||||
            filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
 | 
			
		||||
            icon=ICON_MOTION_SENSOR,
 | 
			
		||||
            unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
            icon=ICON_MOTION_SENSOR,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
 | 
			
		||||
            filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
 | 
			
		||||
            icon=ICON_FLASH,
 | 
			
		||||
            unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
            icon=ICON_FLASH,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_LIGHT): sensor.sensor_schema(
 | 
			
		||||
            device_class=DEVICE_CLASS_ILLUMINANCE,
 | 
			
		||||
            entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
            filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
 | 
			
		||||
            icon=ICON_LIGHTBULB,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_DETECTION_DISTANCE): sensor.sensor_schema(
 | 
			
		||||
            device_class=DEVICE_CLASS_DISTANCE,
 | 
			
		||||
            filters=[{"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}],
 | 
			
		||||
            icon=ICON_SIGNAL,
 | 
			
		||||
            unit_of_measurement=UNIT_CENTIMETER,
 | 
			
		||||
            icon=ICON_SIGNAL,
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
@@ -71,20 +63,14 @@ CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
 | 
			
		||||
        cv.Optional(f"g{x}"): cv.Schema(
 | 
			
		||||
            {
 | 
			
		||||
                cv.Optional(CONF_MOVE_ENERGY): sensor.sensor_schema(
 | 
			
		||||
                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
                    filters=[
 | 
			
		||||
                        {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}
 | 
			
		||||
                    ],
 | 
			
		||||
                    icon=ICON_MOTION_SENSOR,
 | 
			
		||||
                    unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
                    icon=ICON_MOTION_SENSOR,
 | 
			
		||||
                ),
 | 
			
		||||
                cv.Optional(CONF_STILL_ENERGY): sensor.sensor_schema(
 | 
			
		||||
                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
                    filters=[
 | 
			
		||||
                        {"throttle_with_priority": cv.TimePeriod(milliseconds=1000)}
 | 
			
		||||
                    ],
 | 
			
		||||
                    icon=ICON_FLASH,
 | 
			
		||||
                    unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
                    entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
                    icon=ICON_FLASH,
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
 
 | 
			
		||||
@@ -1,46 +0,0 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import uart
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import CONF_ID, CONF_THROTTLE
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["ld24xx"]
 | 
			
		||||
CODEOWNERS = ["@Rihan9"]
 | 
			
		||||
DEPENDENCIES = ["uart"]
 | 
			
		||||
MULTI_CONF = True
 | 
			
		||||
 | 
			
		||||
LD2412_ns = cg.esphome_ns.namespace("ld2412")
 | 
			
		||||
LD2412Component = LD2412_ns.class_("LD2412Component", cg.Component, uart.UARTDevice)
 | 
			
		||||
 | 
			
		||||
CONF_LD2412_ID = "ld2412_id"
 | 
			
		||||
 | 
			
		||||
CONF_MAX_MOVE_DISTANCE = "max_move_distance"
 | 
			
		||||
CONF_MAX_STILL_DISTANCE = "max_still_distance"
 | 
			
		||||
CONF_MOVE_THRESHOLDS = [f"g{x}_move_threshold" for x in range(9)]
 | 
			
		||||
CONF_STILL_THRESHOLDS = [f"g{x}_still_threshold" for x in range(9)]
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(LD2412Component),
 | 
			
		||||
            cv.Optional(CONF_THROTTLE): cv.invalid(
 | 
			
		||||
                f"{CONF_THROTTLE} has been removed; use per-sensor filters, instead"
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(uart.UART_DEVICE_SCHEMA)
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
 | 
			
		||||
    "ld2412",
 | 
			
		||||
    require_tx=True,
 | 
			
		||||
    require_rx=True,
 | 
			
		||||
    parity="NONE",
 | 
			
		||||
    stop_bits=1,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await uart.register_uart_device(var, config)
 | 
			
		||||
@@ -1,70 +0,0 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import binary_sensor
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_HAS_MOVING_TARGET,
 | 
			
		||||
    CONF_HAS_STILL_TARGET,
 | 
			
		||||
    CONF_HAS_TARGET,
 | 
			
		||||
    DEVICE_CLASS_MOTION,
 | 
			
		||||
    DEVICE_CLASS_OCCUPANCY,
 | 
			
		||||
    DEVICE_CLASS_RUNNING,
 | 
			
		||||
    ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
    ICON_ACCOUNT,
 | 
			
		||||
    ICON_MOTION_SENSOR,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from . import CONF_LD2412_ID, LD2412Component
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["ld2412"]
 | 
			
		||||
 | 
			
		||||
CONF_DYNAMIC_BACKGROUND_CORRECTION_STATUS = "dynamic_background_correction_status"
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = {
 | 
			
		||||
    cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
 | 
			
		||||
    cv.Optional(
 | 
			
		||||
        CONF_DYNAMIC_BACKGROUND_CORRECTION_STATUS
 | 
			
		||||
    ): binary_sensor.binary_sensor_schema(
 | 
			
		||||
        device_class=DEVICE_CLASS_RUNNING,
 | 
			
		||||
        entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
        icon=ICON_ACCOUNT,
 | 
			
		||||
    ),
 | 
			
		||||
    cv.Optional(CONF_HAS_TARGET): binary_sensor.binary_sensor_schema(
 | 
			
		||||
        device_class=DEVICE_CLASS_OCCUPANCY,
 | 
			
		||||
        filters=[{"settle": cv.TimePeriod(milliseconds=1000)}],
 | 
			
		||||
        icon=ICON_ACCOUNT,
 | 
			
		||||
    ),
 | 
			
		||||
    cv.Optional(CONF_HAS_MOVING_TARGET): binary_sensor.binary_sensor_schema(
 | 
			
		||||
        device_class=DEVICE_CLASS_MOTION,
 | 
			
		||||
        filters=[{"settle": cv.TimePeriod(milliseconds=1000)}],
 | 
			
		||||
        icon=ICON_MOTION_SENSOR,
 | 
			
		||||
    ),
 | 
			
		||||
    cv.Optional(CONF_HAS_STILL_TARGET): binary_sensor.binary_sensor_schema(
 | 
			
		||||
        device_class=DEVICE_CLASS_OCCUPANCY,
 | 
			
		||||
        filters=[{"settle": cv.TimePeriod(milliseconds=1000)}],
 | 
			
		||||
        icon=ICON_MOTION_SENSOR,
 | 
			
		||||
    ),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    LD2412_component = await cg.get_variable(config[CONF_LD2412_ID])
 | 
			
		||||
    if dynamic_background_correction_status_config := config.get(
 | 
			
		||||
        CONF_DYNAMIC_BACKGROUND_CORRECTION_STATUS
 | 
			
		||||
    ):
 | 
			
		||||
        sens = await binary_sensor.new_binary_sensor(
 | 
			
		||||
            dynamic_background_correction_status_config
 | 
			
		||||
        )
 | 
			
		||||
        cg.add(
 | 
			
		||||
            LD2412_component.set_dynamic_background_correction_status_binary_sensor(
 | 
			
		||||
                sens
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    if has_target_config := config.get(CONF_HAS_TARGET):
 | 
			
		||||
        sens = await binary_sensor.new_binary_sensor(has_target_config)
 | 
			
		||||
        cg.add(LD2412_component.set_target_binary_sensor(sens))
 | 
			
		||||
    if has_moving_target_config := config.get(CONF_HAS_MOVING_TARGET):
 | 
			
		||||
        sens = await binary_sensor.new_binary_sensor(has_moving_target_config)
 | 
			
		||||
        cg.add(LD2412_component.set_moving_target_binary_sensor(sens))
 | 
			
		||||
    if has_still_target_config := config.get(CONF_HAS_STILL_TARGET):
 | 
			
		||||
        sens = await binary_sensor.new_binary_sensor(has_still_target_config)
 | 
			
		||||
        cg.add(LD2412_component.set_still_target_binary_sensor(sens))
 | 
			
		||||
@@ -1,74 +0,0 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import button
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_FACTORY_RESET,
 | 
			
		||||
    CONF_RESTART,
 | 
			
		||||
    DEVICE_CLASS_RESTART,
 | 
			
		||||
    ENTITY_CATEGORY_CONFIG,
 | 
			
		||||
    ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
    ICON_DATABASE,
 | 
			
		||||
    ICON_PULSE,
 | 
			
		||||
    ICON_RESTART,
 | 
			
		||||
    ICON_RESTART_ALERT,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component
 | 
			
		||||
 | 
			
		||||
FactoryResetButton = LD2412_ns.class_("FactoryResetButton", button.Button)
 | 
			
		||||
QueryButton = LD2412_ns.class_("QueryButton", button.Button)
 | 
			
		||||
RestartButton = LD2412_ns.class_("RestartButton", button.Button)
 | 
			
		||||
StartDynamicBackgroundCorrectionButton = LD2412_ns.class_(
 | 
			
		||||
    "StartDynamicBackgroundCorrectionButton", button.Button
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONF_QUERY_PARAMS = "query_params"
 | 
			
		||||
CONF_START_DYNAMIC_BACKGROUND_CORRECTION = "start_dynamic_background_correction"
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = {
 | 
			
		||||
    cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
 | 
			
		||||
    cv.Optional(CONF_FACTORY_RESET): button.button_schema(
 | 
			
		||||
        FactoryResetButton,
 | 
			
		||||
        device_class=DEVICE_CLASS_RESTART,
 | 
			
		||||
        entity_category=ENTITY_CATEGORY_CONFIG,
 | 
			
		||||
        icon=ICON_RESTART_ALERT,
 | 
			
		||||
    ),
 | 
			
		||||
    cv.Optional(CONF_QUERY_PARAMS): button.button_schema(
 | 
			
		||||
        QueryButton,
 | 
			
		||||
        entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
        icon=ICON_DATABASE,
 | 
			
		||||
    ),
 | 
			
		||||
    cv.Optional(CONF_RESTART): button.button_schema(
 | 
			
		||||
        RestartButton,
 | 
			
		||||
        device_class=DEVICE_CLASS_RESTART,
 | 
			
		||||
        entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
        icon=ICON_RESTART,
 | 
			
		||||
    ),
 | 
			
		||||
    cv.Optional(CONF_START_DYNAMIC_BACKGROUND_CORRECTION): button.button_schema(
 | 
			
		||||
        StartDynamicBackgroundCorrectionButton,
 | 
			
		||||
        entity_category=ENTITY_CATEGORY_CONFIG,
 | 
			
		||||
        icon=ICON_PULSE,
 | 
			
		||||
    ),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    LD2412_component = await cg.get_variable(config[CONF_LD2412_ID])
 | 
			
		||||
    if factory_reset_config := config.get(CONF_FACTORY_RESET):
 | 
			
		||||
        b = await button.new_button(factory_reset_config)
 | 
			
		||||
        await cg.register_parented(b, config[CONF_LD2412_ID])
 | 
			
		||||
        cg.add(LD2412_component.set_factory_reset_button(b))
 | 
			
		||||
    if query_params_config := config.get(CONF_QUERY_PARAMS):
 | 
			
		||||
        b = await button.new_button(query_params_config)
 | 
			
		||||
        await cg.register_parented(b, config[CONF_LD2412_ID])
 | 
			
		||||
        cg.add(LD2412_component.set_query_button(b))
 | 
			
		||||
    if restart_config := config.get(CONF_RESTART):
 | 
			
		||||
        b = await button.new_button(restart_config)
 | 
			
		||||
        await cg.register_parented(b, config[CONF_LD2412_ID])
 | 
			
		||||
        cg.add(LD2412_component.set_restart_button(b))
 | 
			
		||||
    if start_dynamic_background_correction_config := config.get(
 | 
			
		||||
        CONF_START_DYNAMIC_BACKGROUND_CORRECTION
 | 
			
		||||
    ):
 | 
			
		||||
        b = await button.new_button(start_dynamic_background_correction_config)
 | 
			
		||||
        await cg.register_parented(b, config[CONF_LD2412_ID])
 | 
			
		||||
        cg.add(LD2412_component.set_start_dynamic_background_correction_button(b))
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
#include "factory_reset_button.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ld2412 {
 | 
			
		||||
 | 
			
		||||
void FactoryResetButton::press_action() { this->parent_->factory_reset(); }
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2412
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/button/button.h"
 | 
			
		||||
#include "../ld2412.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ld2412 {
 | 
			
		||||
 | 
			
		||||
class FactoryResetButton : public button::Button, public Parented<LD2412Component> {
 | 
			
		||||
 public:
 | 
			
		||||
  FactoryResetButton() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void press_action() override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2412
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
#include "query_button.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ld2412 {
 | 
			
		||||
 | 
			
		||||
void QueryButton::press_action() { this->parent_->read_all_info(); }
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2412
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/button/button.h"
 | 
			
		||||
#include "../ld2412.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ld2412 {
 | 
			
		||||
 | 
			
		||||
class QueryButton : public button::Button, public Parented<LD2412Component> {
 | 
			
		||||
 public:
 | 
			
		||||
  QueryButton() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void press_action() override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2412
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,9 +0,0 @@
 | 
			
		||||
#include "restart_button.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ld2412 {
 | 
			
		||||
 | 
			
		||||
void RestartButton::press_action() { this->parent_->restart_and_read_all_info(); }
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2412
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/button/button.h"
 | 
			
		||||
#include "../ld2412.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ld2412 {
 | 
			
		||||
 | 
			
		||||
class RestartButton : public button::Button, public Parented<LD2412Component> {
 | 
			
		||||
 public:
 | 
			
		||||
  RestartButton() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void press_action() override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2412
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,11 +0,0 @@
 | 
			
		||||
#include "start_dynamic_background_correction_button.h"
 | 
			
		||||
 | 
			
		||||
#include "restart_button.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ld2412 {
 | 
			
		||||
 | 
			
		||||
void StartDynamicBackgroundCorrectionButton::press_action() { this->parent_->start_dynamic_background_correction(); }
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2412
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,18 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/button/button.h"
 | 
			
		||||
#include "../ld2412.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ld2412 {
 | 
			
		||||
 | 
			
		||||
class StartDynamicBackgroundCorrectionButton : public button::Button, public Parented<LD2412Component> {
 | 
			
		||||
 public:
 | 
			
		||||
  StartDynamicBackgroundCorrectionButton() = default;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void press_action() override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2412
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,861 +0,0 @@
 | 
			
		||||
#include "ld2412.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
#include "esphome/components/number/number.h"
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SENSOR
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/application.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ld2412 {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "ld2412";
 | 
			
		||||
static const char *const UNKNOWN_MAC = "unknown";
 | 
			
		||||
static const char *const VERSION_FMT = "%u.%02X.%02X%02X%02X%02X";
 | 
			
		||||
 | 
			
		||||
enum BaudRate : uint8_t {
 | 
			
		||||
  BAUD_RATE_9600 = 1,
 | 
			
		||||
  BAUD_RATE_19200 = 2,
 | 
			
		||||
  BAUD_RATE_38400 = 3,
 | 
			
		||||
  BAUD_RATE_57600 = 4,
 | 
			
		||||
  BAUD_RATE_115200 = 5,
 | 
			
		||||
  BAUD_RATE_230400 = 6,
 | 
			
		||||
  BAUD_RATE_256000 = 7,
 | 
			
		||||
  BAUD_RATE_460800 = 8,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum DistanceResolution : uint8_t {
 | 
			
		||||
  DISTANCE_RESOLUTION_0_2 = 0x03,
 | 
			
		||||
  DISTANCE_RESOLUTION_0_5 = 0x01,
 | 
			
		||||
  DISTANCE_RESOLUTION_0_75 = 0x00,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum LightFunction : uint8_t {
 | 
			
		||||
  LIGHT_FUNCTION_OFF = 0x00,
 | 
			
		||||
  LIGHT_FUNCTION_BELOW = 0x01,
 | 
			
		||||
  LIGHT_FUNCTION_ABOVE = 0x02,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum OutPinLevel : uint8_t {
 | 
			
		||||
  OUT_PIN_LEVEL_LOW = 0x01,
 | 
			
		||||
  OUT_PIN_LEVEL_HIGH = 0x00,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/*
 | 
			
		||||
Data Type: 6th byte
 | 
			
		||||
Target states: 9th byte
 | 
			
		||||
    Moving target distance: 10~11th bytes
 | 
			
		||||
    Moving target energy: 12th byte
 | 
			
		||||
    Still target distance: 13~14th bytes
 | 
			
		||||
    Still target energy: 15th byte
 | 
			
		||||
    Detect distance: 16~17th bytes
 | 
			
		||||
*/
 | 
			
		||||
enum PeriodicData : uint8_t {
 | 
			
		||||
  DATA_TYPES = 6,
 | 
			
		||||
  TARGET_STATES = 8,
 | 
			
		||||
  MOVING_TARGET_LOW = 9,
 | 
			
		||||
  MOVING_TARGET_HIGH = 10,
 | 
			
		||||
  MOVING_ENERGY = 11,
 | 
			
		||||
  STILL_TARGET_LOW = 12,
 | 
			
		||||
  STILL_TARGET_HIGH = 13,
 | 
			
		||||
  STILL_ENERGY = 14,
 | 
			
		||||
  MOVING_SENSOR_START = 17,
 | 
			
		||||
  STILL_SENSOR_START = 31,
 | 
			
		||||
  LIGHT_SENSOR = 45,
 | 
			
		||||
  OUT_PIN_SENSOR = 38,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum PeriodicDataValue : uint8_t {
 | 
			
		||||
  HEADER = 0XAA,
 | 
			
		||||
  FOOTER = 0x55,
 | 
			
		||||
  CHECK = 0x00,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum AckData : uint8_t {
 | 
			
		||||
  COMMAND = 6,
 | 
			
		||||
  COMMAND_STATUS = 7,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Memory-efficient lookup tables
 | 
			
		||||
struct StringToUint8 {
 | 
			
		||||
  const char *str;
 | 
			
		||||
  const uint8_t value;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
struct Uint8ToString {
 | 
			
		||||
  const uint8_t value;
 | 
			
		||||
  const char *str;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
constexpr StringToUint8 BAUD_RATES_BY_STR[] = {
 | 
			
		||||
    {"9600", BAUD_RATE_9600},     {"19200", BAUD_RATE_19200},   {"38400", BAUD_RATE_38400},
 | 
			
		||||
    {"57600", BAUD_RATE_57600},   {"115200", BAUD_RATE_115200}, {"230400", BAUD_RATE_230400},
 | 
			
		||||
    {"256000", BAUD_RATE_256000}, {"460800", BAUD_RATE_460800},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
constexpr StringToUint8 DISTANCE_RESOLUTIONS_BY_STR[] = {
 | 
			
		||||
    {"0.2m", DISTANCE_RESOLUTION_0_2},
 | 
			
		||||
    {"0.5m", DISTANCE_RESOLUTION_0_5},
 | 
			
		||||
    {"0.75m", DISTANCE_RESOLUTION_0_75},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
constexpr Uint8ToString DISTANCE_RESOLUTIONS_BY_UINT[] = {
 | 
			
		||||
    {DISTANCE_RESOLUTION_0_2, "0.2m"},
 | 
			
		||||
    {DISTANCE_RESOLUTION_0_5, "0.5m"},
 | 
			
		||||
    {DISTANCE_RESOLUTION_0_75, "0.75m"},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
constexpr StringToUint8 LIGHT_FUNCTIONS_BY_STR[] = {
 | 
			
		||||
    {"off", LIGHT_FUNCTION_OFF},
 | 
			
		||||
    {"below", LIGHT_FUNCTION_BELOW},
 | 
			
		||||
    {"above", LIGHT_FUNCTION_ABOVE},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
constexpr Uint8ToString LIGHT_FUNCTIONS_BY_UINT[] = {
 | 
			
		||||
    {LIGHT_FUNCTION_OFF, "off"},
 | 
			
		||||
    {LIGHT_FUNCTION_BELOW, "below"},
 | 
			
		||||
    {LIGHT_FUNCTION_ABOVE, "above"},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
constexpr StringToUint8 OUT_PIN_LEVELS_BY_STR[] = {
 | 
			
		||||
    {"low", OUT_PIN_LEVEL_LOW},
 | 
			
		||||
    {"high", OUT_PIN_LEVEL_HIGH},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
constexpr Uint8ToString OUT_PIN_LEVELS_BY_UINT[] = {
 | 
			
		||||
    {OUT_PIN_LEVEL_LOW, "low"},
 | 
			
		||||
    {OUT_PIN_LEVEL_HIGH, "high"},
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Helper functions for lookups
 | 
			
		||||
template<size_t N> uint8_t find_uint8(const StringToUint8 (&arr)[N], const std::string &str) {
 | 
			
		||||
  for (const auto &entry : arr) {
 | 
			
		||||
    if (str == entry.str) {
 | 
			
		||||
      return entry.value;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return 0xFF;  // Not found
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
template<size_t N> const char *find_str(const Uint8ToString (&arr)[N], uint8_t value) {
 | 
			
		||||
  for (const auto &entry : arr) {
 | 
			
		||||
    if (value == entry.value) {
 | 
			
		||||
      return entry.str;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  return "";  // Not found
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static constexpr uint8_t DEFAULT_PRESENCE_TIMEOUT = 5;  // Default used when number component is not defined
 | 
			
		||||
// Commands
 | 
			
		||||
static constexpr uint8_t CMD_ENABLE_CONF = 0xFF;
 | 
			
		||||
static constexpr uint8_t CMD_DISABLE_CONF = 0xFE;
 | 
			
		||||
static constexpr uint8_t CMD_ENABLE_ENG = 0x62;
 | 
			
		||||
static constexpr uint8_t CMD_DISABLE_ENG = 0x63;
 | 
			
		||||
static constexpr uint8_t CMD_QUERY_BASIC_CONF = 0x12;
 | 
			
		||||
static constexpr uint8_t CMD_BASIC_CONF = 0x02;
 | 
			
		||||
static constexpr uint8_t CMD_QUERY_VERSION = 0xA0;
 | 
			
		||||
static constexpr uint8_t CMD_QUERY_DISTANCE_RESOLUTION = 0x11;
 | 
			
		||||
static constexpr uint8_t CMD_SET_DISTANCE_RESOLUTION = 0x01;
 | 
			
		||||
static constexpr uint8_t CMD_QUERY_LIGHT_CONTROL = 0x1C;
 | 
			
		||||
static constexpr uint8_t CMD_SET_LIGHT_CONTROL = 0x0C;
 | 
			
		||||
static constexpr uint8_t CMD_SET_BAUD_RATE = 0xA1;
 | 
			
		||||
static constexpr uint8_t CMD_QUERY_MAC_ADDRESS = 0xA5;
 | 
			
		||||
static constexpr uint8_t CMD_FACTORY_RESET = 0xA2;
 | 
			
		||||
static constexpr uint8_t CMD_RESTART = 0xA3;
 | 
			
		||||
static constexpr uint8_t CMD_BLUETOOTH = 0xA4;
 | 
			
		||||
static constexpr uint8_t CMD_DYNAMIC_BACKGROUND_CORRECTION = 0x0B;
 | 
			
		||||
static constexpr uint8_t CMD_QUERY_DYNAMIC_BACKGROUND_CORRECTION = 0x1B;
 | 
			
		||||
static constexpr uint8_t CMD_MOTION_GATE_SENS = 0x03;
 | 
			
		||||
static constexpr uint8_t CMD_QUERY_MOTION_GATE_SENS = 0x13;
 | 
			
		||||
static constexpr uint8_t CMD_STATIC_GATE_SENS = 0x04;
 | 
			
		||||
static constexpr uint8_t CMD_QUERY_STATIC_GATE_SENS = 0x14;
 | 
			
		||||
static constexpr uint8_t CMD_NONE = 0x00;
 | 
			
		||||
// Commands values
 | 
			
		||||
static constexpr uint8_t CMD_MAX_MOVE_VALUE = 0x00;
 | 
			
		||||
static constexpr uint8_t CMD_MAX_STILL_VALUE = 0x01;
 | 
			
		||||
static constexpr uint8_t CMD_DURATION_VALUE = 0x02;
 | 
			
		||||
// Bitmasks for target states
 | 
			
		||||
static constexpr uint8_t MOVE_BITMASK = 0x01;
 | 
			
		||||
static constexpr uint8_t STILL_BITMASK = 0x02;
 | 
			
		||||
// Header & Footer size
 | 
			
		||||
static constexpr uint8_t HEADER_FOOTER_SIZE = 4;
 | 
			
		||||
// Command Header & Footer
 | 
			
		||||
static constexpr uint8_t CMD_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xFD, 0xFC, 0xFB, 0xFA};
 | 
			
		||||
static constexpr uint8_t CMD_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0x04, 0x03, 0x02, 0x01};
 | 
			
		||||
// Data Header & Footer
 | 
			
		||||
static constexpr uint8_t DATA_FRAME_HEADER[HEADER_FOOTER_SIZE] = {0xF4, 0xF3, 0xF2, 0xF1};
 | 
			
		||||
static constexpr uint8_t DATA_FRAME_FOOTER[HEADER_FOOTER_SIZE] = {0xF8, 0xF7, 0xF6, 0xF5};
 | 
			
		||||
// MAC address the module uses when Bluetooth is disabled
 | 
			
		||||
static constexpr uint8_t NO_MAC[] = {0x08, 0x05, 0x04, 0x03, 0x02, 0x01};
 | 
			
		||||
 | 
			
		||||
static inline int two_byte_to_int(char firstbyte, char secondbyte) { return (int16_t) (secondbyte << 8) + firstbyte; }
 | 
			
		||||
 | 
			
		||||
static inline bool validate_header_footer(const uint8_t *header_footer, const uint8_t *buffer) {
 | 
			
		||||
  return std::memcmp(header_footer, buffer, HEADER_FOOTER_SIZE) == 0;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::dump_config() {
 | 
			
		||||
  std::string mac_str =
 | 
			
		||||
      mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
 | 
			
		||||
  std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
 | 
			
		||||
                                    this->version_[4], this->version_[3], this->version_[2]);
 | 
			
		||||
  ESP_LOGCONFIG(TAG,
 | 
			
		||||
                "LD2412:\n"
 | 
			
		||||
                "  Firmware version: %s\n"
 | 
			
		||||
                "  MAC address: %s",
 | 
			
		||||
                version.c_str(), mac_str.c_str());
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Binary Sensors:");
 | 
			
		||||
  LOG_BINARY_SENSOR("  ", "DynamicBackgroundCorrectionStatus",
 | 
			
		||||
                    this->dynamic_background_correction_status_binary_sensor_);
 | 
			
		||||
  LOG_BINARY_SENSOR("  ", "MovingTarget", this->moving_target_binary_sensor_);
 | 
			
		||||
  LOG_BINARY_SENSOR("  ", "StillTarget", this->still_target_binary_sensor_);
 | 
			
		||||
  LOG_BINARY_SENSOR("  ", "Target", this->target_binary_sensor_);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SENSOR
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Sensors:");
 | 
			
		||||
  LOG_SENSOR_WITH_DEDUP_SAFE("  ", "Light", this->light_sensor_);
 | 
			
		||||
  LOG_SENSOR_WITH_DEDUP_SAFE("  ", "DetectionDistance", this->detection_distance_sensor_);
 | 
			
		||||
  LOG_SENSOR_WITH_DEDUP_SAFE("  ", "MovingTargetDistance", this->moving_target_distance_sensor_);
 | 
			
		||||
  LOG_SENSOR_WITH_DEDUP_SAFE("  ", "MovingTargetEnergy", this->moving_target_energy_sensor_);
 | 
			
		||||
  LOG_SENSOR_WITH_DEDUP_SAFE("  ", "StillTargetDistance", this->still_target_distance_sensor_);
 | 
			
		||||
  LOG_SENSOR_WITH_DEDUP_SAFE("  ", "StillTargetEnergy", this->still_target_energy_sensor_);
 | 
			
		||||
  for (auto &s : this->gate_still_sensors_) {
 | 
			
		||||
    LOG_SENSOR_WITH_DEDUP_SAFE("  ", "GateStill", s);
 | 
			
		||||
  }
 | 
			
		||||
  for (auto &s : this->gate_move_sensors_) {
 | 
			
		||||
    LOG_SENSOR_WITH_DEDUP_SAFE("  ", "GateMove", s);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Text Sensors:");
 | 
			
		||||
  LOG_TEXT_SENSOR("  ", "MAC address", this->mac_text_sensor_);
 | 
			
		||||
  LOG_TEXT_SENSOR("  ", "Version", this->version_text_sensor_);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Numbers:");
 | 
			
		||||
  LOG_NUMBER("  ", "LightThreshold", this->light_threshold_number_);
 | 
			
		||||
  LOG_NUMBER("  ", "MaxDistanceGate", this->max_distance_gate_number_);
 | 
			
		||||
  LOG_NUMBER("  ", "MinDistanceGate", this->min_distance_gate_number_);
 | 
			
		||||
  LOG_NUMBER("  ", "Timeout", this->timeout_number_);
 | 
			
		||||
  for (number::Number *n : this->gate_move_threshold_numbers_) {
 | 
			
		||||
    LOG_NUMBER("  ", "Move Thresholds", n);
 | 
			
		||||
  }
 | 
			
		||||
  for (number::Number *n : this->gate_still_threshold_numbers_) {
 | 
			
		||||
    LOG_NUMBER("  ", "Still Thresholds", n);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Selects:");
 | 
			
		||||
  LOG_SELECT("  ", "BaudRate", this->baud_rate_select_);
 | 
			
		||||
  LOG_SELECT("  ", "DistanceResolution", this->distance_resolution_select_);
 | 
			
		||||
  LOG_SELECT("  ", "LightFunction", this->light_function_select_);
 | 
			
		||||
  LOG_SELECT("  ", "OutPinLevel", this->out_pin_level_select_);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Switches:");
 | 
			
		||||
  LOG_SWITCH("  ", "Bluetooth", this->bluetooth_switch_);
 | 
			
		||||
  LOG_SWITCH("  ", "EngineeringMode", this->engineering_mode_switch_);
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BUTTON
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Buttons:");
 | 
			
		||||
  LOG_BUTTON("  ", "FactoryReset", this->factory_reset_button_);
 | 
			
		||||
  LOG_BUTTON("  ", "Query", this->query_button_);
 | 
			
		||||
  LOG_BUTTON("  ", "Restart", this->restart_button_);
 | 
			
		||||
  LOG_BUTTON("  ", "StartDynamicBackgroundCorrection", this->start_dynamic_background_correction_button_);
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Running setup");
 | 
			
		||||
  this->read_all_info();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::read_all_info() {
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
  this->get_version_();
 | 
			
		||||
  delay(10);  // NOLINT
 | 
			
		||||
  this->get_mac_();
 | 
			
		||||
  delay(10);  // NOLINT
 | 
			
		||||
  this->get_distance_resolution_();
 | 
			
		||||
  delay(10);  // NOLINT
 | 
			
		||||
  this->query_parameters_();
 | 
			
		||||
  delay(10);  // NOLINT
 | 
			
		||||
  this->query_dynamic_background_correction_();
 | 
			
		||||
  delay(10);  // NOLINT
 | 
			
		||||
  this->query_light_control_();
 | 
			
		||||
  delay(10);  // NOLINT
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  this->get_gate_threshold();
 | 
			
		||||
  delay(10);  // NOLINT
 | 
			
		||||
#endif
 | 
			
		||||
  this->set_config_mode_(false);
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  const auto baud_rate = std::to_string(this->parent_->get_baud_rate());
 | 
			
		||||
  if (this->baud_rate_select_ != nullptr) {
 | 
			
		||||
    this->baud_rate_select_->publish_state(baud_rate);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::restart_and_read_all_info() {
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
  this->restart_();
 | 
			
		||||
  this->set_timeout(1000, [this]() { this->read_all_info(); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::loop() {
 | 
			
		||||
  while (this->available()) {
 | 
			
		||||
    this->readline_(this->read());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::send_command_(uint8_t command, const uint8_t *command_value, uint8_t command_value_len) {
 | 
			
		||||
  ESP_LOGV(TAG, "Sending COMMAND %02X", command);
 | 
			
		||||
  // frame header bytes
 | 
			
		||||
  this->write_array(CMD_FRAME_HEADER, HEADER_FOOTER_SIZE);
 | 
			
		||||
  // length bytes
 | 
			
		||||
  uint8_t len = 2;
 | 
			
		||||
  if (command_value != nullptr) {
 | 
			
		||||
    len += command_value_len;
 | 
			
		||||
  }
 | 
			
		||||
  // 2 length bytes (low, high) + 2 command bytes (low, high)
 | 
			
		||||
  uint8_t len_cmd[] = {len, 0x00, command, 0x00};
 | 
			
		||||
  this->write_array(len_cmd, sizeof(len_cmd));
 | 
			
		||||
 | 
			
		||||
  // command value bytes
 | 
			
		||||
  if (command_value != nullptr) {
 | 
			
		||||
    this->write_array(command_value, command_value_len);
 | 
			
		||||
  }
 | 
			
		||||
  // frame footer bytes
 | 
			
		||||
  this->write_array(CMD_FRAME_FOOTER, HEADER_FOOTER_SIZE);
 | 
			
		||||
 | 
			
		||||
  if (command != CMD_ENABLE_CONF && command != CMD_DISABLE_CONF) {
 | 
			
		||||
    delay(30);  // NOLINT
 | 
			
		||||
  }
 | 
			
		||||
  delay(20);  // NOLINT
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::handle_periodic_data_() {
 | 
			
		||||
  // 4 frame header bytes + 2 length bytes + 1 data end byte + 1 crc byte + 4 frame footer bytes
 | 
			
		||||
  // data header=0xAA, data footer=0x55, crc=0x00
 | 
			
		||||
  if (this->buffer_pos_ < 12 || !ld2412::validate_header_footer(DATA_FRAME_HEADER, this->buffer_data_) ||
 | 
			
		||||
      this->buffer_data_[7] != HEADER || this->buffer_data_[this->buffer_pos_ - 6] != FOOTER) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  /*
 | 
			
		||||
    Data Type: 7th
 | 
			
		||||
    0x01: Engineering mode
 | 
			
		||||
    0x02: Normal mode
 | 
			
		||||
  */
 | 
			
		||||
  bool engineering_mode = this->buffer_data_[DATA_TYPES] == 0x01;
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
  if (this->engineering_mode_switch_ != nullptr) {
 | 
			
		||||
    this->engineering_mode_switch_->publish_state(engineering_mode);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
  /*
 | 
			
		||||
    Target states: 9th
 | 
			
		||||
    0x00 = No target
 | 
			
		||||
    0x01 = Moving targets
 | 
			
		||||
    0x02 = Still targets
 | 
			
		||||
    0x03 = Moving+Still targets
 | 
			
		||||
  */
 | 
			
		||||
  char target_state = this->buffer_data_[TARGET_STATES];
 | 
			
		||||
  if (this->target_binary_sensor_ != nullptr) {
 | 
			
		||||
    this->target_binary_sensor_->publish_state(target_state != 0x00);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->moving_target_binary_sensor_ != nullptr) {
 | 
			
		||||
    this->moving_target_binary_sensor_->publish_state(target_state & MOVE_BITMASK);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->still_target_binary_sensor_ != nullptr) {
 | 
			
		||||
    this->still_target_binary_sensor_->publish_state(target_state & STILL_BITMASK);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
  /*
 | 
			
		||||
    Moving target distance: 10~11th bytes
 | 
			
		||||
    Moving target energy: 12th byte
 | 
			
		||||
    Still target distance: 13~14th bytes
 | 
			
		||||
    Still target energy: 15th byte
 | 
			
		||||
    Detect distance: 16~17th bytes
 | 
			
		||||
  */
 | 
			
		||||
#ifdef USE_SENSOR
 | 
			
		||||
  SAFE_PUBLISH_SENSOR(
 | 
			
		||||
      this->moving_target_distance_sensor_,
 | 
			
		||||
      ld2412::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH]))
 | 
			
		||||
  SAFE_PUBLISH_SENSOR(this->moving_target_energy_sensor_, this->buffer_data_[MOVING_ENERGY])
 | 
			
		||||
  SAFE_PUBLISH_SENSOR(
 | 
			
		||||
      this->still_target_distance_sensor_,
 | 
			
		||||
      ld2412::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH]))
 | 
			
		||||
  SAFE_PUBLISH_SENSOR(this->still_target_energy_sensor_, this->buffer_data_[STILL_ENERGY])
 | 
			
		||||
  if (this->detection_distance_sensor_ != nullptr) {
 | 
			
		||||
    int new_detect_distance = 0;
 | 
			
		||||
    if (target_state != 0x00 && (target_state & MOVE_BITMASK)) {
 | 
			
		||||
      new_detect_distance =
 | 
			
		||||
          ld2412::two_byte_to_int(this->buffer_data_[MOVING_TARGET_LOW], this->buffer_data_[MOVING_TARGET_HIGH]);
 | 
			
		||||
    } else if (target_state != 0x00) {
 | 
			
		||||
      new_detect_distance =
 | 
			
		||||
          ld2412::two_byte_to_int(this->buffer_data_[STILL_TARGET_LOW], this->buffer_data_[STILL_TARGET_HIGH]);
 | 
			
		||||
    }
 | 
			
		||||
    this->detection_distance_sensor_->publish_state_if_not_dup(new_detect_distance);
 | 
			
		||||
  }
 | 
			
		||||
  if (engineering_mode) {
 | 
			
		||||
    /*
 | 
			
		||||
      Moving distance range: 18th byte
 | 
			
		||||
      Still distance range: 19th byte
 | 
			
		||||
      Moving energy: 20~28th bytes
 | 
			
		||||
    */
 | 
			
		||||
    for (uint8_t i = 0; i < TOTAL_GATES; i++) {
 | 
			
		||||
      SAFE_PUBLISH_SENSOR(this->gate_move_sensors_[i], this->buffer_data_[MOVING_SENSOR_START + i])
 | 
			
		||||
    }
 | 
			
		||||
    /*
 | 
			
		||||
      Still energy: 29~37th bytes
 | 
			
		||||
    */
 | 
			
		||||
    for (uint8_t i = 0; i < TOTAL_GATES; i++) {
 | 
			
		||||
      SAFE_PUBLISH_SENSOR(this->gate_still_sensors_[i], this->buffer_data_[STILL_SENSOR_START + i])
 | 
			
		||||
    }
 | 
			
		||||
    /*
 | 
			
		||||
      Light sensor: 38th bytes
 | 
			
		||||
    */
 | 
			
		||||
    SAFE_PUBLISH_SENSOR(this->light_sensor_, this->buffer_data_[LIGHT_SENSOR])
 | 
			
		||||
  } else {
 | 
			
		||||
    for (auto &gate_move_sensor : this->gate_move_sensors_) {
 | 
			
		||||
      SAFE_PUBLISH_SENSOR_UNKNOWN(gate_move_sensor)
 | 
			
		||||
    }
 | 
			
		||||
    for (auto &gate_still_sensor : this->gate_still_sensors_) {
 | 
			
		||||
      SAFE_PUBLISH_SENSOR_UNKNOWN(gate_still_sensor)
 | 
			
		||||
    }
 | 
			
		||||
    SAFE_PUBLISH_SENSOR_UNKNOWN(this->light_sensor_)
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
  // the radar module won't tell us when it's done, so we just have to keep polling...
 | 
			
		||||
  if (this->dynamic_background_correction_active_) {
 | 
			
		||||
    this->set_config_mode_(true);
 | 
			
		||||
    this->query_dynamic_background_correction_();
 | 
			
		||||
    this->set_config_mode_(false);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
std::function<void(void)> set_number_value(number::Number *n, float value) {
 | 
			
		||||
  if (n != nullptr && (!n->has_state() || n->state != value)) {
 | 
			
		||||
    n->state = value;
 | 
			
		||||
    return [n, value]() { n->publish_state(value); };
 | 
			
		||||
  }
 | 
			
		||||
  return []() {};
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
bool LD2412Component::handle_ack_data_() {
 | 
			
		||||
  ESP_LOGV(TAG, "Handling ACK DATA for COMMAND %02X", this->buffer_data_[COMMAND]);
 | 
			
		||||
  if (this->buffer_pos_ < 10) {
 | 
			
		||||
    ESP_LOGW(TAG, "Invalid length");
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  if (!ld2412::validate_header_footer(CMD_FRAME_HEADER, this->buffer_data_)) {
 | 
			
		||||
    ESP_LOGW(TAG, "Invalid header: %s", format_hex_pretty(this->buffer_data_, HEADER_FOOTER_SIZE).c_str());
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->buffer_data_[COMMAND_STATUS] != 0x01) {
 | 
			
		||||
    ESP_LOGW(TAG, "Invalid status");
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->buffer_data_[8] || this->buffer_data_[9]) {
 | 
			
		||||
    ESP_LOGW(TAG, "Invalid command: %02X, %02X", this->buffer_data_[8], this->buffer_data_[9]);
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  switch (this->buffer_data_[COMMAND]) {
 | 
			
		||||
    case CMD_ENABLE_CONF:
 | 
			
		||||
      ESP_LOGV(TAG, "Enable conf");
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case CMD_DISABLE_CONF:
 | 
			
		||||
      ESP_LOGV(TAG, "Disabled conf");
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case CMD_SET_BAUD_RATE:
 | 
			
		||||
      ESP_LOGV(TAG, "Baud rate change");
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
      if (this->baud_rate_select_ != nullptr) {
 | 
			
		||||
        ESP_LOGW(TAG, "Change baud rate to %s and reinstall", this->baud_rate_select_->state.c_str());
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case CMD_QUERY_VERSION: {
 | 
			
		||||
      std::memcpy(this->version_, &this->buffer_data_[12], sizeof(this->version_));
 | 
			
		||||
      std::string version = str_sprintf(VERSION_FMT, this->version_[1], this->version_[0], this->version_[5],
 | 
			
		||||
                                        this->version_[4], this->version_[3], this->version_[2]);
 | 
			
		||||
      ESP_LOGV(TAG, "Firmware version: %s", version.c_str());
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
      if (this->version_text_sensor_ != nullptr) {
 | 
			
		||||
        this->version_text_sensor_->publish_state(version);
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case CMD_QUERY_DISTANCE_RESOLUTION: {
 | 
			
		||||
      const auto *distance_resolution = find_str(DISTANCE_RESOLUTIONS_BY_UINT, this->buffer_data_[10]);
 | 
			
		||||
      ESP_LOGV(TAG, "Distance resolution: %s", distance_resolution);
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
      if (this->distance_resolution_select_ != nullptr) {
 | 
			
		||||
        this->distance_resolution_select_->publish_state(distance_resolution);
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case CMD_QUERY_LIGHT_CONTROL: {
 | 
			
		||||
      this->light_function_ = this->buffer_data_[10];
 | 
			
		||||
      this->light_threshold_ = this->buffer_data_[11];
 | 
			
		||||
      const auto *light_function_str = find_str(LIGHT_FUNCTIONS_BY_UINT, this->light_function_);
 | 
			
		||||
      ESP_LOGV(TAG,
 | 
			
		||||
               "Light function: %s\n"
 | 
			
		||||
               "Light threshold: %u",
 | 
			
		||||
               light_function_str, this->light_threshold_);
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
      if (this->light_function_select_ != nullptr) {
 | 
			
		||||
        this->light_function_select_->publish_state(light_function_str);
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
      if (this->light_threshold_number_ != nullptr) {
 | 
			
		||||
        this->light_threshold_number_->publish_state(static_cast<float>(this->light_threshold_));
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case CMD_QUERY_MAC_ADDRESS: {
 | 
			
		||||
      if (this->buffer_pos_ < 20) {
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      this->bluetooth_on_ = std::memcmp(&this->buffer_data_[10], NO_MAC, sizeof(NO_MAC)) != 0;
 | 
			
		||||
      if (this->bluetooth_on_) {
 | 
			
		||||
        std::memcpy(this->mac_address_, &this->buffer_data_[10], sizeof(this->mac_address_));
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      std::string mac_str =
 | 
			
		||||
          mac_address_is_valid(this->mac_address_) ? format_mac_address_pretty(this->mac_address_) : UNKNOWN_MAC;
 | 
			
		||||
      ESP_LOGV(TAG, "MAC address: %s", mac_str.c_str());
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
      if (this->mac_text_sensor_ != nullptr) {
 | 
			
		||||
        this->mac_text_sensor_->publish_state(mac_str);
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
      if (this->bluetooth_switch_ != nullptr) {
 | 
			
		||||
        this->bluetooth_switch_->publish_state(this->bluetooth_on_);
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case CMD_SET_DISTANCE_RESOLUTION:
 | 
			
		||||
      ESP_LOGV(TAG, "Handled set distance resolution command");
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case CMD_QUERY_DYNAMIC_BACKGROUND_CORRECTION: {
 | 
			
		||||
      ESP_LOGV(TAG, "Handled query dynamic background correction");
 | 
			
		||||
      bool dynamic_background_correction_active = (this->buffer_data_[10] != 0x00);
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
      if (this->dynamic_background_correction_status_binary_sensor_ != nullptr) {
 | 
			
		||||
        this->dynamic_background_correction_status_binary_sensor_->publish_state(dynamic_background_correction_active);
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      this->dynamic_background_correction_active_ = dynamic_background_correction_active;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case CMD_BLUETOOTH:
 | 
			
		||||
      ESP_LOGV(TAG, "Handled bluetooth command");
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case CMD_SET_LIGHT_CONTROL:
 | 
			
		||||
      ESP_LOGV(TAG, "Handled set light control command");
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    case CMD_QUERY_MOTION_GATE_SENS: {
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
      std::vector<std::function<void(void)>> updates;
 | 
			
		||||
      updates.reserve(this->gate_still_threshold_numbers_.size());
 | 
			
		||||
      for (size_t i = 0; i < this->gate_still_threshold_numbers_.size(); i++) {
 | 
			
		||||
        updates.push_back(set_number_value(this->gate_move_threshold_numbers_[i], this->buffer_data_[10 + i]));
 | 
			
		||||
      }
 | 
			
		||||
      for (auto &update : updates) {
 | 
			
		||||
        update();
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case CMD_QUERY_STATIC_GATE_SENS: {
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
      std::vector<std::function<void(void)>> updates;
 | 
			
		||||
      updates.reserve(this->gate_still_threshold_numbers_.size());
 | 
			
		||||
      for (size_t i = 0; i < this->gate_still_threshold_numbers_.size(); i++) {
 | 
			
		||||
        updates.push_back(set_number_value(this->gate_still_threshold_numbers_[i], this->buffer_data_[10 + i]));
 | 
			
		||||
      }
 | 
			
		||||
      for (auto &update : updates) {
 | 
			
		||||
        update();
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case CMD_QUERY_BASIC_CONF:  // Query parameters response
 | 
			
		||||
    {
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
      /*
 | 
			
		||||
        Moving distance range: 9th byte
 | 
			
		||||
        Still distance range: 10th byte
 | 
			
		||||
      */
 | 
			
		||||
      std::vector<std::function<void(void)>> updates;
 | 
			
		||||
      updates.push_back(set_number_value(this->min_distance_gate_number_, this->buffer_data_[10]));
 | 
			
		||||
      updates.push_back(set_number_value(this->max_distance_gate_number_, this->buffer_data_[11] - 1));
 | 
			
		||||
      ESP_LOGV(TAG, "min_distance_gate_number_: %u, max_distance_gate_number_ %u", this->buffer_data_[10],
 | 
			
		||||
               this->buffer_data_[11]);
 | 
			
		||||
      /*
 | 
			
		||||
        None Duration: 11~12th bytes
 | 
			
		||||
      */
 | 
			
		||||
      updates.push_back(set_number_value(this->timeout_number_,
 | 
			
		||||
                                         ld2412::two_byte_to_int(this->buffer_data_[12], this->buffer_data_[13])));
 | 
			
		||||
      ESP_LOGV(TAG, "timeout_number_: %u", ld2412::two_byte_to_int(this->buffer_data_[12], this->buffer_data_[13]));
 | 
			
		||||
      /*
 | 
			
		||||
        Output pin configuration: 13th bytes
 | 
			
		||||
      */
 | 
			
		||||
      this->out_pin_level_ = this->buffer_data_[14];
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
      const auto *out_pin_level_str = find_str(OUT_PIN_LEVELS_BY_UINT, this->out_pin_level_);
 | 
			
		||||
      if (this->out_pin_level_select_ != nullptr) {
 | 
			
		||||
        this->out_pin_level_select_->publish_state(out_pin_level_str);
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
      for (auto &update : updates) {
 | 
			
		||||
        update();
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
    } break;
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::readline_(int readch) {
 | 
			
		||||
  if (readch < 0) {
 | 
			
		||||
    return;  // No data available
 | 
			
		||||
  }
 | 
			
		||||
  if (this->buffer_pos_ < HEADER_FOOTER_SIZE && readch != DATA_FRAME_HEADER[this->buffer_pos_] &&
 | 
			
		||||
      readch != CMD_FRAME_HEADER[this->buffer_pos_]) {
 | 
			
		||||
    this->buffer_pos_ = 0;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->buffer_pos_ < MAX_LINE_LENGTH - 1) {
 | 
			
		||||
    this->buffer_data_[this->buffer_pos_++] = readch;
 | 
			
		||||
    this->buffer_data_[this->buffer_pos_] = 0;
 | 
			
		||||
  } else {
 | 
			
		||||
    // We should never get here, but just in case...
 | 
			
		||||
    ESP_LOGW(TAG, "Max command length exceeded; ignoring");
 | 
			
		||||
    this->buffer_pos_ = 0;
 | 
			
		||||
  }
 | 
			
		||||
  if (this->buffer_pos_ < 4) {
 | 
			
		||||
    return;  // Not enough data to process yet
 | 
			
		||||
  }
 | 
			
		||||
  if (ld2412::validate_header_footer(DATA_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
 | 
			
		||||
    ESP_LOGV(TAG, "Handling Periodic Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str());
 | 
			
		||||
    this->handle_periodic_data_();
 | 
			
		||||
    this->buffer_pos_ = 0;  // Reset position index for next message
 | 
			
		||||
  } else if (ld2412::validate_header_footer(CMD_FRAME_FOOTER, &this->buffer_data_[this->buffer_pos_ - 4])) {
 | 
			
		||||
    ESP_LOGV(TAG, "Handling Ack Data: %s", format_hex_pretty(this->buffer_data_, this->buffer_pos_).c_str());
 | 
			
		||||
    if (this->handle_ack_data_()) {
 | 
			
		||||
      this->buffer_pos_ = 0;  // Reset position index for next message
 | 
			
		||||
    } else {
 | 
			
		||||
      ESP_LOGV(TAG, "Ack Data incomplete");
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::set_config_mode_(bool enable) {
 | 
			
		||||
  const uint8_t cmd = enable ? CMD_ENABLE_CONF : CMD_DISABLE_CONF;
 | 
			
		||||
  const uint8_t cmd_value[2] = {0x01, 0x00};
 | 
			
		||||
  this->send_command_(cmd, enable ? cmd_value : nullptr, sizeof(cmd_value));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::set_bluetooth(bool enable) {
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
  const uint8_t cmd_value[2] = {enable ? (uint8_t) 0x01 : (uint8_t) 0x00, 0x00};
 | 
			
		||||
  this->send_command_(CMD_BLUETOOTH, cmd_value, sizeof(cmd_value));
 | 
			
		||||
  this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::set_distance_resolution(const std::string &state) {
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
  const uint8_t cmd_value[6] = {find_uint8(DISTANCE_RESOLUTIONS_BY_STR, state), 0x00, 0x00, 0x00, 0x00, 0x00};
 | 
			
		||||
  this->send_command_(CMD_SET_DISTANCE_RESOLUTION, cmd_value, sizeof(cmd_value));
 | 
			
		||||
  this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::set_baud_rate(const std::string &state) {
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
  const uint8_t cmd_value[2] = {find_uint8(BAUD_RATES_BY_STR, state), 0x00};
 | 
			
		||||
  this->send_command_(CMD_SET_BAUD_RATE, cmd_value, sizeof(cmd_value));
 | 
			
		||||
  this->set_timeout(200, [this]() { this->restart_(); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::query_dynamic_background_correction_() {
 | 
			
		||||
  this->send_command_(CMD_QUERY_DYNAMIC_BACKGROUND_CORRECTION, nullptr, 0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::start_dynamic_background_correction() {
 | 
			
		||||
  if (this->dynamic_background_correction_active_) {
 | 
			
		||||
    return;  // Already in progress
 | 
			
		||||
  }
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
  if (this->dynamic_background_correction_status_binary_sensor_ != nullptr) {
 | 
			
		||||
    this->dynamic_background_correction_status_binary_sensor_->publish_state(true);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
  this->dynamic_background_correction_active_ = true;
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
  this->send_command_(CMD_DYNAMIC_BACKGROUND_CORRECTION, nullptr, 0);
 | 
			
		||||
  this->set_config_mode_(false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::set_engineering_mode(bool enable) {
 | 
			
		||||
  const uint8_t cmd = enable ? CMD_ENABLE_ENG : CMD_DISABLE_ENG;
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
  this->send_command_(cmd, nullptr, 0);
 | 
			
		||||
  this->set_config_mode_(false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::factory_reset() {
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
  this->send_command_(CMD_FACTORY_RESET, nullptr, 0);
 | 
			
		||||
  this->set_timeout(2000, [this]() { this->restart_and_read_all_info(); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::restart_() { this->send_command_(CMD_RESTART, nullptr, 0); }
 | 
			
		||||
 | 
			
		||||
void LD2412Component::query_parameters_() { this->send_command_(CMD_QUERY_BASIC_CONF, nullptr, 0); }
 | 
			
		||||
 | 
			
		||||
void LD2412Component::get_version_() { this->send_command_(CMD_QUERY_VERSION, nullptr, 0); }
 | 
			
		||||
 | 
			
		||||
void LD2412Component::get_mac_() {
 | 
			
		||||
  const uint8_t cmd_value[2] = {0x01, 0x00};
 | 
			
		||||
  this->send_command_(CMD_QUERY_MAC_ADDRESS, cmd_value, sizeof(cmd_value));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::get_distance_resolution_() { this->send_command_(CMD_QUERY_DISTANCE_RESOLUTION, nullptr, 0); }
 | 
			
		||||
 | 
			
		||||
void LD2412Component::query_light_control_() { this->send_command_(CMD_QUERY_LIGHT_CONTROL, nullptr, 0); }
 | 
			
		||||
 | 
			
		||||
void LD2412Component::set_basic_config() {
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  if (!this->min_distance_gate_number_->has_state() || !this->max_distance_gate_number_->has_state() ||
 | 
			
		||||
      !this->timeout_number_->has_state()) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  if (!this->out_pin_level_select_->has_state()) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  uint8_t value[5] = {
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
      lowbyte(static_cast<int>(this->min_distance_gate_number_->state)),
 | 
			
		||||
      lowbyte(static_cast<int>(this->max_distance_gate_number_->state) + 1),
 | 
			
		||||
      lowbyte(static_cast<int>(this->timeout_number_->state)),
 | 
			
		||||
      highbyte(static_cast<int>(this->timeout_number_->state)),
 | 
			
		||||
#else
 | 
			
		||||
      1,    TOTAL_GATES, DEFAULT_PRESENCE_TIMEOUT, 0,
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
      find_uint8(OUT_PIN_LEVELS_BY_STR, this->out_pin_level_select_->state),
 | 
			
		||||
#else
 | 
			
		||||
      0x01,  // Default value if not using select
 | 
			
		||||
#endif
 | 
			
		||||
  };
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
  this->send_command_(CMD_BASIC_CONF, value, sizeof(value));
 | 
			
		||||
  this->set_config_mode_(false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
void LD2412Component::set_gate_threshold() {
 | 
			
		||||
  if (this->gate_move_threshold_numbers_.empty() && this->gate_still_threshold_numbers_.empty()) {
 | 
			
		||||
    return;  // No gate threshold numbers set; nothing to do here
 | 
			
		||||
  }
 | 
			
		||||
  uint8_t value[TOTAL_GATES] = {0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0};
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
  if (!this->gate_move_threshold_numbers_.empty()) {
 | 
			
		||||
    for (size_t i = 0; i < this->gate_move_threshold_numbers_.size(); i++) {
 | 
			
		||||
      value[i] = lowbyte(static_cast<int>(this->gate_move_threshold_numbers_[i]->state));
 | 
			
		||||
    }
 | 
			
		||||
    this->send_command_(CMD_MOTION_GATE_SENS, value, sizeof(value));
 | 
			
		||||
  }
 | 
			
		||||
  if (!this->gate_still_threshold_numbers_.empty()) {
 | 
			
		||||
    for (size_t i = 0; i < this->gate_still_threshold_numbers_.size(); i++) {
 | 
			
		||||
      value[i] = lowbyte(static_cast<int>(this->gate_still_threshold_numbers_[i]->state));
 | 
			
		||||
    }
 | 
			
		||||
    this->send_command_(CMD_STATIC_GATE_SENS, value, sizeof(value));
 | 
			
		||||
  }
 | 
			
		||||
  this->set_config_mode_(false);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::get_gate_threshold() {
 | 
			
		||||
  this->send_command_(CMD_QUERY_MOTION_GATE_SENS, nullptr, 0);
 | 
			
		||||
  this->send_command_(CMD_QUERY_STATIC_GATE_SENS, nullptr, 0);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::set_gate_still_threshold_number(uint8_t gate, number::Number *n) {
 | 
			
		||||
  this->gate_still_threshold_numbers_[gate] = n;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LD2412Component::set_gate_move_threshold_number(uint8_t gate, number::Number *n) {
 | 
			
		||||
  this->gate_move_threshold_numbers_[gate] = n;
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
void LD2412Component::set_light_out_control() {
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  if (this->light_threshold_number_ != nullptr && this->light_threshold_number_->has_state()) {
 | 
			
		||||
    this->light_threshold_ = static_cast<uint8_t>(this->light_threshold_number_->state);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  if (this->light_function_select_ != nullptr && this->light_function_select_->has_state()) {
 | 
			
		||||
    this->light_function_ = find_uint8(LIGHT_FUNCTIONS_BY_STR, this->light_function_select_->state);
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
  uint8_t value[2] = {this->light_function_, this->light_threshold_};
 | 
			
		||||
  this->set_config_mode_(true);
 | 
			
		||||
  this->send_command_(CMD_SET_LIGHT_CONTROL, value, sizeof(value));
 | 
			
		||||
  this->query_light_control_();
 | 
			
		||||
  this->set_timeout(200, [this]() { this->restart_and_read_all_info(); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#ifdef USE_SENSOR
 | 
			
		||||
// These could leak memory, but they are only set once prior to 'setup()' and should never be used again.
 | 
			
		||||
void LD2412Component::set_gate_move_sensor(uint8_t gate, sensor::Sensor *s) {
 | 
			
		||||
  this->gate_move_sensors_[gate] = new SensorWithDedup<uint8_t>(s);
 | 
			
		||||
}
 | 
			
		||||
void LD2412Component::set_gate_still_sensor(uint8_t gate, sensor::Sensor *s) {
 | 
			
		||||
  this->gate_still_sensors_[gate] = new SensorWithDedup<uint8_t>(s);
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2412
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,141 +0,0 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
#include "esphome/core/defines.h"
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
#include "esphome/components/binary_sensor/binary_sensor.h"
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SENSOR
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
#include "esphome/components/number/number.h"
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
#include "esphome/components/switch/switch.h"
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BUTTON
 | 
			
		||||
#include "esphome/components/button/button.h"
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
#include "esphome/components/select/select.h"
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
#include "esphome/components/text_sensor/text_sensor.h"
 | 
			
		||||
#endif
 | 
			
		||||
#include "esphome/components/ld24xx/ld24xx.h"
 | 
			
		||||
#include "esphome/components/uart/uart.h"
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/core/helpers.h"
 | 
			
		||||
 | 
			
		||||
#include <array>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ld2412 {
 | 
			
		||||
 | 
			
		||||
using namespace ld24xx;
 | 
			
		||||
 | 
			
		||||
static constexpr uint8_t MAX_LINE_LENGTH = 54;  // Max characters for serial buffer
 | 
			
		||||
static constexpr uint8_t TOTAL_GATES = 14;      // Total number of gates supported by the LD2412
 | 
			
		||||
 | 
			
		||||
class LD2412Component : public Component, public uart::UARTDevice {
 | 
			
		||||
#ifdef USE_BINARY_SENSOR
 | 
			
		||||
  SUB_BINARY_SENSOR(dynamic_background_correction_status)
 | 
			
		||||
  SUB_BINARY_SENSOR(moving_target)
 | 
			
		||||
  SUB_BINARY_SENSOR(still_target)
 | 
			
		||||
  SUB_BINARY_SENSOR(target)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SENSOR
 | 
			
		||||
  SUB_SENSOR_WITH_DEDUP(light, uint8_t)
 | 
			
		||||
  SUB_SENSOR_WITH_DEDUP(detection_distance, int)
 | 
			
		||||
  SUB_SENSOR_WITH_DEDUP(moving_target_distance, int)
 | 
			
		||||
  SUB_SENSOR_WITH_DEDUP(moving_target_energy, uint8_t)
 | 
			
		||||
  SUB_SENSOR_WITH_DEDUP(still_target_distance, int)
 | 
			
		||||
  SUB_SENSOR_WITH_DEDUP(still_target_energy, uint8_t)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_TEXT_SENSOR
 | 
			
		||||
  SUB_TEXT_SENSOR(mac)
 | 
			
		||||
  SUB_TEXT_SENSOR(version)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  SUB_NUMBER(light_threshold)
 | 
			
		||||
  SUB_NUMBER(max_distance_gate)
 | 
			
		||||
  SUB_NUMBER(min_distance_gate)
 | 
			
		||||
  SUB_NUMBER(timeout)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SELECT
 | 
			
		||||
  SUB_SELECT(baud_rate)
 | 
			
		||||
  SUB_SELECT(distance_resolution)
 | 
			
		||||
  SUB_SELECT(light_function)
 | 
			
		||||
  SUB_SELECT(out_pin_level)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SWITCH
 | 
			
		||||
  SUB_SWITCH(bluetooth)
 | 
			
		||||
  SUB_SWITCH(engineering_mode)
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_BUTTON
 | 
			
		||||
  SUB_BUTTON(factory_reset)
 | 
			
		||||
  SUB_BUTTON(query)
 | 
			
		||||
  SUB_BUTTON(restart)
 | 
			
		||||
  SUB_BUTTON(start_dynamic_background_correction)
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
 public:
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  void set_light_out_control();
 | 
			
		||||
  void set_basic_config();
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  void set_gate_move_threshold_number(uint8_t gate, number::Number *n);
 | 
			
		||||
  void set_gate_still_threshold_number(uint8_t gate, number::Number *n);
 | 
			
		||||
  void set_gate_threshold();
 | 
			
		||||
  void get_gate_threshold();
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SENSOR
 | 
			
		||||
  void set_gate_move_sensor(uint8_t gate, sensor::Sensor *s);
 | 
			
		||||
  void set_gate_still_sensor(uint8_t gate, sensor::Sensor *s);
 | 
			
		||||
#endif
 | 
			
		||||
  void set_engineering_mode(bool enable);
 | 
			
		||||
  void read_all_info();
 | 
			
		||||
  void restart_and_read_all_info();
 | 
			
		||||
  void set_bluetooth(bool enable);
 | 
			
		||||
  void set_distance_resolution(const std::string &state);
 | 
			
		||||
  void set_baud_rate(const std::string &state);
 | 
			
		||||
  void factory_reset();
 | 
			
		||||
  void start_dynamic_background_correction();
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void send_command_(uint8_t command_str, const uint8_t *command_value, uint8_t command_value_len);
 | 
			
		||||
  void set_config_mode_(bool enable);
 | 
			
		||||
  void handle_periodic_data_();
 | 
			
		||||
  bool handle_ack_data_();
 | 
			
		||||
  void readline_(int readch);
 | 
			
		||||
  void query_parameters_();
 | 
			
		||||
  void get_version_();
 | 
			
		||||
  void get_mac_();
 | 
			
		||||
  void get_distance_resolution_();
 | 
			
		||||
  void query_light_control_();
 | 
			
		||||
  void restart_();
 | 
			
		||||
  void query_dynamic_background_correction_();
 | 
			
		||||
 | 
			
		||||
  uint8_t light_function_ = 0;
 | 
			
		||||
  uint8_t light_threshold_ = 0;
 | 
			
		||||
  uint8_t out_pin_level_ = 0;
 | 
			
		||||
  uint8_t buffer_pos_ = 0;  // where to resume processing/populating buffer
 | 
			
		||||
  uint8_t buffer_data_[MAX_LINE_LENGTH];
 | 
			
		||||
  uint8_t mac_address_[6] = {0, 0, 0, 0, 0, 0};
 | 
			
		||||
  uint8_t version_[6] = {0, 0, 0, 0, 0, 0};
 | 
			
		||||
  bool bluetooth_on_{false};
 | 
			
		||||
  bool dynamic_background_correction_active_{false};
 | 
			
		||||
#ifdef USE_NUMBER
 | 
			
		||||
  std::array<number::Number *, TOTAL_GATES> gate_move_threshold_numbers_{};
 | 
			
		||||
  std::array<number::Number *, TOTAL_GATES> gate_still_threshold_numbers_{};
 | 
			
		||||
#endif
 | 
			
		||||
#ifdef USE_SENSOR
 | 
			
		||||
  std::array<SensorWithDedup<uint8_t> *, TOTAL_GATES> gate_move_sensors_{};
 | 
			
		||||
  std::array<SensorWithDedup<uint8_t> *, TOTAL_GATES> gate_still_sensors_{};
 | 
			
		||||
#endif
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ld2412
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -1,126 +0,0 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import number
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_MOVE_THRESHOLD,
 | 
			
		||||
    CONF_STILL_THRESHOLD,
 | 
			
		||||
    CONF_TIMEOUT,
 | 
			
		||||
    DEVICE_CLASS_DISTANCE,
 | 
			
		||||
    DEVICE_CLASS_ILLUMINANCE,
 | 
			
		||||
    DEVICE_CLASS_SIGNAL_STRENGTH,
 | 
			
		||||
    ENTITY_CATEGORY_CONFIG,
 | 
			
		||||
    ICON_LIGHTBULB,
 | 
			
		||||
    ICON_MOTION_SENSOR,
 | 
			
		||||
    ICON_TIMELAPSE,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
    UNIT_SECOND,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
from .. import CONF_LD2412_ID, LD2412_ns, LD2412Component
 | 
			
		||||
 | 
			
		||||
GateThresholdNumber = LD2412_ns.class_("GateThresholdNumber", number.Number)
 | 
			
		||||
LightThresholdNumber = LD2412_ns.class_("LightThresholdNumber", number.Number)
 | 
			
		||||
MaxDistanceTimeoutNumber = LD2412_ns.class_("MaxDistanceTimeoutNumber", number.Number)
 | 
			
		||||
 | 
			
		||||
CONF_LIGHT_THRESHOLD = "light_threshold"
 | 
			
		||||
CONF_MAX_DISTANCE_GATE = "max_distance_gate"
 | 
			
		||||
CONF_MIN_DISTANCE_GATE = "min_distance_gate"
 | 
			
		||||
 | 
			
		||||
TIMEOUT_GROUP = "timeout"
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(CONF_LD2412_ID): cv.use_id(LD2412Component),
 | 
			
		||||
        cv.Optional(CONF_LIGHT_THRESHOLD): number.number_schema(
 | 
			
		||||
            LightThresholdNumber,
 | 
			
		||||
            device_class=DEVICE_CLASS_ILLUMINANCE,
 | 
			
		||||
            entity_category=ENTITY_CATEGORY_CONFIG,
 | 
			
		||||
            icon=ICON_LIGHTBULB,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_MAX_DISTANCE_GATE): number.number_schema(
 | 
			
		||||
            MaxDistanceTimeoutNumber,
 | 
			
		||||
            device_class=DEVICE_CLASS_DISTANCE,
 | 
			
		||||
            entity_category=ENTITY_CATEGORY_CONFIG,
 | 
			
		||||
            icon=ICON_MOTION_SENSOR,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_MIN_DISTANCE_GATE): number.number_schema(
 | 
			
		||||
            MaxDistanceTimeoutNumber,
 | 
			
		||||
            device_class=DEVICE_CLASS_DISTANCE,
 | 
			
		||||
            entity_category=ENTITY_CATEGORY_CONFIG,
 | 
			
		||||
            icon=ICON_MOTION_SENSOR,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_TIMEOUT): number.number_schema(
 | 
			
		||||
            MaxDistanceTimeoutNumber,
 | 
			
		||||
            entity_category=ENTITY_CATEGORY_CONFIG,
 | 
			
		||||
            icon=ICON_TIMELAPSE,
 | 
			
		||||
            unit_of_measurement=UNIT_SECOND,
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = CONFIG_SCHEMA.extend(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Optional(f"gate_{x}"): (
 | 
			
		||||
            {
 | 
			
		||||
                cv.Required(CONF_MOVE_THRESHOLD): number.number_schema(
 | 
			
		||||
                    GateThresholdNumber,
 | 
			
		||||
                    device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
 | 
			
		||||
                    entity_category=ENTITY_CATEGORY_CONFIG,
 | 
			
		||||
                    icon=ICON_MOTION_SENSOR,
 | 
			
		||||
                    unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
                ),
 | 
			
		||||
                cv.Required(CONF_STILL_THRESHOLD): number.number_schema(
 | 
			
		||||
                    GateThresholdNumber,
 | 
			
		||||
                    device_class=DEVICE_CLASS_SIGNAL_STRENGTH,
 | 
			
		||||
                    entity_category=ENTITY_CATEGORY_CONFIG,
 | 
			
		||||
                    icon=ICON_MOTION_SENSOR,
 | 
			
		||||
                    unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        )
 | 
			
		||||
        for x in range(14)
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    LD2412_component = await cg.get_variable(config[CONF_LD2412_ID])
 | 
			
		||||
    if light_threshold_config := config.get(CONF_LIGHT_THRESHOLD):
 | 
			
		||||
        n = await number.new_number(
 | 
			
		||||
            light_threshold_config, min_value=0, max_value=255, step=1
 | 
			
		||||
        )
 | 
			
		||||
        await cg.register_parented(n, config[CONF_LD2412_ID])
 | 
			
		||||
        cg.add(LD2412_component.set_light_threshold_number(n))
 | 
			
		||||
    if max_distance_gate_config := config.get(CONF_MAX_DISTANCE_GATE):
 | 
			
		||||
        n = await number.new_number(
 | 
			
		||||
            max_distance_gate_config, min_value=2, max_value=13, step=1
 | 
			
		||||
        )
 | 
			
		||||
        await cg.register_parented(n, config[CONF_LD2412_ID])
 | 
			
		||||
        cg.add(LD2412_component.set_max_distance_gate_number(n))
 | 
			
		||||
    if min_distance_gate_config := config.get(CONF_MIN_DISTANCE_GATE):
 | 
			
		||||
        n = await number.new_number(
 | 
			
		||||
            min_distance_gate_config, min_value=1, max_value=12, step=1
 | 
			
		||||
        )
 | 
			
		||||
        await cg.register_parented(n, config[CONF_LD2412_ID])
 | 
			
		||||
        cg.add(LD2412_component.set_min_distance_gate_number(n))
 | 
			
		||||
    for x in range(14):
 | 
			
		||||
        if gate_conf := config.get(f"gate_{x}"):
 | 
			
		||||
            move_config = gate_conf[CONF_MOVE_THRESHOLD]
 | 
			
		||||
            n = cg.new_Pvariable(move_config[CONF_ID], x)
 | 
			
		||||
            await number.register_number(
 | 
			
		||||
                n, move_config, min_value=0, max_value=100, step=1
 | 
			
		||||
            )
 | 
			
		||||
            await cg.register_parented(n, config[CONF_LD2412_ID])
 | 
			
		||||
            cg.add(LD2412_component.set_gate_move_threshold_number(x, n))
 | 
			
		||||
            still_config = gate_conf[CONF_STILL_THRESHOLD]
 | 
			
		||||
            n = cg.new_Pvariable(still_config[CONF_ID], x)
 | 
			
		||||
            await number.register_number(
 | 
			
		||||
                n, still_config, min_value=0, max_value=100, step=1
 | 
			
		||||
            )
 | 
			
		||||
            await cg.register_parented(n, config[CONF_LD2412_ID])
 | 
			
		||||
            cg.add(LD2412_component.set_gate_still_threshold_number(x, n))
 | 
			
		||||
    if timeout_config := config.get(CONF_TIMEOUT):
 | 
			
		||||
        n = await number.new_number(timeout_config, min_value=0, max_value=900, step=1)
 | 
			
		||||
        await cg.register_parented(n, config[CONF_LD2412_ID])
 | 
			
		||||
        cg.add(LD2412_component.set_timeout_number(n))
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user