mirror of
				https://github.com/esphome/esphome.git
				synced 2025-11-03 00:21:56 +00:00 
			
		
		
		
	Compare commits
	
		
			238 Commits
		
	
	
		
			2023.12.0b
			...
			2024.2.0
		
	
	| Author | SHA1 | Date | |
|---|---|---|---|
| 
						 | 
					2d22a2d1c2 | ||
| 
						 | 
					c92968da8a | ||
| 
						 | 
					86580d07cb | ||
| 
						 | 
					03ea71034f | ||
| 
						 | 
					7bf676abfa | ||
| 
						 | 
					fb16e6b027 | ||
| 
						 | 
					4eb04afa62 | ||
| 
						 | 
					841a831c63 | ||
| 
						 | 
					ae4af2966a | ||
| 
						 | 
					6ced54ea8e | ||
| 
						 | 
					e0e3489335 | ||
| 
						 | 
					cc1813f5b9 | ||
| 
						 | 
					6eb3c65445 | ||
| 
						 | 
					29ec40db5f | ||
| 
						 | 
					61a45dcebe | ||
| 
						 | 
					7aa2c494c8 | ||
| 
						 | 
					373569d86d | ||
| 
						 | 
					fb94778c04 | ||
| 
						 | 
					6935b02d3f | ||
| 
						 | 
					0e769d77ff | ||
| 
						 | 
					082778d117 | ||
| 
						 | 
					e521662342 | ||
| 
						 | 
					061d5b4979 | ||
| 
						 | 
					71b3a14a29 | ||
| 
						 | 
					3eaf59cc5a | ||
| 
						 | 
					a91937dca5 | ||
| 
						 | 
					558588ee8a | ||
| 
						 | 
					f3ef05f5c3 | ||
| 
						 | 
					0ede4a3095 | ||
| 
						 | 
					fe789c8beb | ||
| 
						 | 
					05da0fb4cf | ||
| 
						 | 
					cfe16c92ee | ||
| 
						 | 
					9dbbc80c74 | ||
| 
						 | 
					164b42f5aa | ||
| 
						 | 
					5e9741f51c | ||
| 
						 | 
					b28821d846 | ||
| 
						 | 
					0fa0904bc5 | ||
| 
						 | 
					92798751c2 | ||
| 
						 | 
					23a9a704f3 | ||
| 
						 | 
					f2caf13d39 | ||
| 
						 | 
					25ab6f0297 | ||
| 
						 | 
					23071e932a | ||
| 
						 | 
					4812997429 | ||
| 
						 | 
					ec3162282c | ||
| 
						 | 
					f3997d0f77 | ||
| 
						 | 
					4e5534850c | ||
| 
						 | 
					354314dbf3 | ||
| 
						 | 
					2cda6462f3 | ||
| 
						 | 
					a6f864a4a3 | ||
| 
						 | 
					c35a21773e | ||
| 
						 | 
					1821ddd996 | ||
| 
						 | 
					aee702f84f | ||
| 
						 | 
					d5fe5b0899 | ||
| 
						 | 
					bd7fe1227c | ||
| 
						 | 
					0cbc06a9b9 | ||
| 
						 | 
					2f09624c07 | ||
| 
						 | 
					6a8da17ea3 | ||
| 
						 | 
					ed771abc8a | ||
| 
						 | 
					6561746f97 | ||
| 
						 | 
					1fef769496 | ||
| 
						 | 
					2283b3b443 | ||
| 
						 | 
					8267b3274c | ||
| 
						 | 
					6a6a70f1e5 | ||
| 
						 | 
					ea9de45d16 | ||
| 
						 | 
					045836c3fe | ||
| 
						 | 
					45c0d10eb0 | ||
| 
						 | 
					e2f2feafdd | ||
| 
						 | 
					c6f528583b | ||
| 
						 | 
					c9c8d39778 | ||
| 
						 | 
					39bd829236 | ||
| 
						 | 
					e731a2ffaa | ||
| 
						 | 
					b606e976e1 | ||
| 
						 | 
					0cd232cdf5 | ||
| 
						 | 
					7dced7f55d | ||
| 
						 | 
					36de644065 | ||
| 
						 | 
					95292dbba1 | ||
| 
						 | 
					86c9546362 | ||
| 
						 | 
					f37a812e59 | ||
| 
						 | 
					596943b683 | ||
| 
						 | 
					3854203037 | ||
| 
						 | 
					21337ffc67 | ||
| 
						 | 
					ea03058ace | ||
| 
						 | 
					e35cab018a | ||
| 
						 | 
					26acbbedbf | ||
| 
						 | 
					249cd67588 | ||
| 
						 | 
					72ab1700e7 | ||
| 
						 | 
					87cab92af6 | ||
| 
						 | 
					412c999f14 | ||
| 
						 | 
					8cd1798674 | ||
| 
						 | 
					e39099137d | ||
| 
						 | 
					83baa24022 | ||
| 
						 | 
					78a6509fb1 | ||
| 
						 | 
					8b2d76e8ce | ||
| 
						 | 
					dd2dca4d08 | ||
| 
						 | 
					534c14e313 | ||
| 
						 | 
					3fec8f9b53 | ||
| 
						 | 
					b8b6462844 | ||
| 
						 | 
					59e7c52341 | ||
| 
						 | 
					ff7de4c971 | ||
| 
						 | 
					978a676c7c | ||
| 
						 | 
					33051906bd | ||
| 
						 | 
					da56d333dc | ||
| 
						 | 
					48a4e6bae9 | ||
| 
						 | 
					5220c9edf8 | ||
| 
						 | 
					f567b5d28b | ||
| 
						 | 
					8e83c7dd19 | ||
| 
						 | 
					d551a2eba2 | ||
| 
						 | 
					ed2ab9e962 | ||
| 
						 | 
					aa04a3caaf | ||
| 
						 | 
					343a8c063e | ||
| 
						 | 
					4cc17dac0d | ||
| 
						 | 
					d616025fed | ||
| 
						 | 
					082d9fcf0e | ||
| 
						 | 
					4b783c0372 | ||
| 
						 | 
					fcd549e5b6 | ||
| 
						 | 
					aa8a533da6 | ||
| 
						 | 
					97be209aec | ||
| 
						 | 
					6dfdcff66c | ||
| 
						 | 
					87301a2e76 | ||
| 
						 | 
					2be19c4e45 | ||
| 
						 | 
					d9def0cb3a | ||
| 
						 | 
					65e6f9cba9 | ||
| 
						 | 
					79d00ec913 | ||
| 
						 | 
					869cdf122d | ||
| 
						 | 
					2bb5343d27 | ||
| 
						 | 
					e3d146ee44 | ||
| 
						 | 
					6061699eff | ||
| 
						 | 
					886d1a2d00 | ||
| 
						 | 
					9bdb9dc1a3 | ||
| 
						 | 
					696bfe6a87 | ||
| 
						 | 
					14bffaf8a7 | ||
| 
						 | 
					4202fe65b5 | ||
| 
						 | 
					fdd54d74a3 | ||
| 
						 | 
					a2e152ad12 | ||
| 
						 | 
					ae52164d9c | ||
| 
						 | 
					773cd0f414 | ||
| 
						 | 
					2a43e55452 | ||
| 
						 | 
					5ebb68f4ff | ||
| 
						 | 
					d3567f9ac6 | ||
| 
						 | 
					21ec42f495 | ||
| 
						 | 
					d4d49e38fc | ||
| 
						 | 
					3be97868fc | ||
| 
						 | 
					41dc73d228 | ||
| 
						 | 
					6ceefe08ab | ||
| 
						 | 
					21e5806a73 | ||
| 
						 | 
					4fd79fee2c | ||
| 
						 | 
					4c8c4a2579 | ||
| 
						 | 
					b68420b2cc | ||
| 
						 | 
					7bce999bba | ||
| 
						 | 
					dc0cc0b431 | ||
| 
						 | 
					0990d0812e | ||
| 
						 | 
					93ac765425 | ||
| 
						 | 
					46fc37b691 | ||
| 
						 | 
					de6fc6b1dd | ||
| 
						 | 
					fe15d993f9 | ||
| 
						 | 
					6583026e14 | ||
| 
						 | 
					8e674990b0 | ||
| 
						 | 
					a97fc4f758 | ||
| 
						 | 
					35388cf2a2 | ||
| 
						 | 
					417e37d291 | ||
| 
						 | 
					7dc35a1029 | ||
| 
						 | 
					9202a30dc7 | ||
| 
						 | 
					45f9f3d972 | ||
| 
						 | 
					46310ff223 | ||
| 
						 | 
					f5c99d1647 | ||
| 
						 | 
					46c4c61b40 | ||
| 
						 | 
					46255ad4df | ||
| 
						 | 
					d2d0058386 | ||
| 
						 | 
					676ae6b26e | ||
| 
						 | 
					bd6fa29f77 | ||
| 
						 | 
					4fb7e945f8 | ||
| 
						 | 
					9b72a3a584 | ||
| 
						 | 
					3de5b26d77 | ||
| 
						 | 
					19e5a4a81a | ||
| 
						 | 
					8e13c3e1b0 | ||
| 
						 | 
					4f8e3211bf | ||
| 
						 | 
					872519f7f6 | ||
| 
						 | 
					2a69a49061 | ||
| 
						 | 
					1a8e7854c7 | ||
| 
						 | 
					70fdc3c3f8 | ||
| 
						 | 
					059e4cee58 | ||
| 
						 | 
					513a02ce11 | ||
| 
						 | 
					31448a4fcd | ||
| 
						 | 
					0a779a9299 | ||
| 
						 | 
					6d3c7f035d | ||
| 
						 | 
					00ab17cb8e | ||
| 
						 | 
					2ee01e22cd | ||
| 
						 | 
					442820deaf | ||
| 
						 | 
					5e2df0b6a2 | ||
| 
						 | 
					74281b93c4 | ||
| 
						 | 
					222bb9b495 | ||
| 
						 | 
					d73ad39aed | ||
| 
						 | 
					f096f107e2 | ||
| 
						 | 
					223e6e8f13 | ||
| 
						 | 
					04b3547992 | ||
| 
						 | 
					a784f1e691 | ||
| 
						 | 
					c92715e403 | ||
| 
						 | 
					c305f61020 | ||
| 
						 | 
					784dff7574 | ||
| 
						 | 
					2a1d16f17b | ||
| 
						 | 
					937a9c96ce | ||
| 
						 | 
					b5932940ee | ||
| 
						 | 
					c6a37da9da | ||
| 
						 | 
					3c2383e261 | ||
| 
						 | 
					991880d53f | ||
| 
						 | 
					26277e4ba2 | ||
| 
						 | 
					cfa5e5c5a9 | ||
| 
						 | 
					8675955614 | ||
| 
						 | 
					23ceddafed | ||
| 
						 | 
					84174aeb80 | ||
| 
						 | 
					d582cfa30a | ||
| 
						 | 
					16798bbfb4 | ||
| 
						 | 
					efda2033f7 | ||
| 
						 | 
					cd06dc77ee | ||
| 
						 | 
					52b9668170 | ||
| 
						 | 
					0a117eb562 | ||
| 
						 | 
					3ea5054cf2 | ||
| 
						 | 
					8961e8ab32 | ||
| 
						 | 
					d99598bba6 | ||
| 
						 | 
					bf258230cd | ||
| 
						 | 
					89c6f3d45d | ||
| 
						 | 
					0f4d7dadb3 | ||
| 
						 | 
					323f8c9bdb | ||
| 
						 | 
					2060d1ac89 | ||
| 
						 | 
					1d37edb63c | ||
| 
						 | 
					29fb2a5360 | ||
| 
						 | 
					8653972cb8 | ||
| 
						 | 
					8a23b7e0c8 | ||
| 
						 | 
					003d8b0cf5 | ||
| 
						 | 
					94904f44f9 | ||
| 
						 | 
					ea4e618f2a | ||
| 
						 | 
					836a3db163 | ||
| 
						 | 
					300343ae24 | ||
| 
						 | 
					0a188ad9d2 | ||
| 
						 | 
					a3cc551856 | ||
| 
						 | 
					8c37066ed9 | ||
| 
						 | 
					777cdb1c21 | ||
| 
						 | 
					9f27eadaee | 
							
								
								
									
										13
									
								
								.github/actions/restore-python/action.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										13
									
								
								.github/actions/restore-python/action.yml
									
									
									
									
										vendored
									
									
								
							@@ -28,11 +28,20 @@ runs:
 | 
			
		||||
        # yamllint disable-line rule:line-length
 | 
			
		||||
        key: ${{ runner.os }}-${{ steps.python.outputs.python-version }}-venv-${{ inputs.cache-key }}
 | 
			
		||||
    - name: Create Python virtual environment
 | 
			
		||||
      if: steps.cache-venv.outputs.cache-hit != 'true'
 | 
			
		||||
      if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os != 'Windows'
 | 
			
		||||
      shell: bash
 | 
			
		||||
      run: |
 | 
			
		||||
        python -m venv venv
 | 
			
		||||
        . venv/bin/activate
 | 
			
		||||
        source venv/bin/activate
 | 
			
		||||
        python --version
 | 
			
		||||
        pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
 | 
			
		||||
        pip install -e .
 | 
			
		||||
    - name: Create Python virtual environment
 | 
			
		||||
      if: steps.cache-venv.outputs.cache-hit != 'true' && runner.os == 'Windows'
 | 
			
		||||
      shell: bash
 | 
			
		||||
      run: |
 | 
			
		||||
        python -m venv venv
 | 
			
		||||
        ./venv/Scripts/activate
 | 
			
		||||
        python --version
 | 
			
		||||
        pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt
 | 
			
		||||
        pip install -e .
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										105
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										105
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							@@ -45,7 +45,7 @@ jobs:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
      - name: Restore Python virtual environment
 | 
			
		||||
        id: cache-venv
 | 
			
		||||
        uses: actions/cache@v3.3.2
 | 
			
		||||
        uses: actions/cache@v4.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: venv
 | 
			
		||||
          # yamllint disable-line rule:line-length
 | 
			
		||||
@@ -166,7 +166,35 @@ jobs:
 | 
			
		||||
 | 
			
		||||
  pytest:
 | 
			
		||||
    name: Run pytest
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      matrix:
 | 
			
		||||
        python-version:
 | 
			
		||||
          - "3.9"
 | 
			
		||||
          - "3.10"
 | 
			
		||||
          - "3.11"
 | 
			
		||||
          - "3.12"
 | 
			
		||||
        os:
 | 
			
		||||
          - ubuntu-latest
 | 
			
		||||
          - macOS-latest
 | 
			
		||||
          - windows-latest
 | 
			
		||||
        exclude:
 | 
			
		||||
          # Minimize CI resource usage
 | 
			
		||||
          # by only running the Python version
 | 
			
		||||
          # version used for docker images on Windows and macOS
 | 
			
		||||
          - python-version: "3.12"
 | 
			
		||||
            os: windows-latest
 | 
			
		||||
          - python-version: "3.10"
 | 
			
		||||
            os: windows-latest
 | 
			
		||||
          - python-version: "3.9"
 | 
			
		||||
            os: windows-latest
 | 
			
		||||
          - python-version: "3.12"
 | 
			
		||||
            os: macOS-latest
 | 
			
		||||
          - python-version: "3.10"
 | 
			
		||||
            os: macOS-latest
 | 
			
		||||
          - python-version: "3.9"
 | 
			
		||||
            os: macOS-latest
 | 
			
		||||
    runs-on: ${{ matrix.os }}
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
    steps:
 | 
			
		||||
@@ -175,14 +203,24 @@ jobs:
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          python-version: ${{ matrix.python-version }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: Register matcher
 | 
			
		||||
        run: echo "::add-matcher::.github/workflows/matchers/pytest.json"
 | 
			
		||||
      - name: Run pytest
 | 
			
		||||
        if: matrix.os == 'windows-latest'
 | 
			
		||||
        run: |
 | 
			
		||||
          ./venv/Scripts/activate
 | 
			
		||||
          pytest -vv --cov-report=xml --tb=native tests
 | 
			
		||||
      - name: Run pytest
 | 
			
		||||
        if: matrix.os == 'ubuntu-latest' || matrix.os == 'macOS-latest'
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          pytest -vv --tb=native tests
 | 
			
		||||
          pytest -vv --cov-report=xml --tb=native tests
 | 
			
		||||
      - name: Upload coverage to Codecov
 | 
			
		||||
        uses: codecov/codecov-action@v3
 | 
			
		||||
        with:
 | 
			
		||||
          token: ${{ secrets.CODECOV_TOKEN }}
 | 
			
		||||
 | 
			
		||||
  clang-format:
 | 
			
		||||
    name: Check clang-format
 | 
			
		||||
@@ -327,7 +365,7 @@ jobs:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: Cache platformio
 | 
			
		||||
        uses: actions/cache@v3.3.2
 | 
			
		||||
        uses: actions/cache@v4.0.0
 | 
			
		||||
        with:
 | 
			
		||||
          path: ~/.platformio
 | 
			
		||||
          # yamllint disable-line rule:line-length
 | 
			
		||||
@@ -354,6 +392,62 @@ jobs:
 | 
			
		||||
        # yamllint disable-line rule:line-length
 | 
			
		||||
        if: always()
 | 
			
		||||
 | 
			
		||||
  list-components:
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
    outputs:
 | 
			
		||||
      matrix: ${{ steps.set-matrix.outputs.matrix }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.1
 | 
			
		||||
        with:
 | 
			
		||||
          # Fetch enough history so `git merge-base refs/remotes/origin/dev HEAD` works.
 | 
			
		||||
          fetch-depth: 500
 | 
			
		||||
      - name: Fetch dev branch
 | 
			
		||||
        run: |
 | 
			
		||||
          git -c protocol.version=2 fetch --no-tags --prune --no-recurse-submodules --depth=1 origin +refs/heads/dev*:refs/remotes/origin/dev* +refs/tags/dev*:refs/tags/dev*
 | 
			
		||||
          git merge-base refs/remotes/origin/dev HEAD
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: Find changed components
 | 
			
		||||
        id: set-matrix
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          echo "matrix=$(script/list-components.py --changed | jq -R -s -c 'split("\n")[:-1]')" >> $GITHUB_OUTPUT
 | 
			
		||||
 | 
			
		||||
  test-build-components:
 | 
			
		||||
    name: Component test ${{ matrix.file }}
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
    needs:
 | 
			
		||||
      - common
 | 
			
		||||
      - list-components
 | 
			
		||||
    if: ${{ needs.list-components.outputs.matrix != '[]' && needs.list-components.outputs.matrix != '' }}
 | 
			
		||||
    strategy:
 | 
			
		||||
      fail-fast: false
 | 
			
		||||
      max-parallel: 2
 | 
			
		||||
      matrix:
 | 
			
		||||
        file: ${{ fromJson(needs.list-components.outputs.matrix) }}
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Check out code from GitHub
 | 
			
		||||
        uses: actions/checkout@v4.1.1
 | 
			
		||||
      - name: Restore Python
 | 
			
		||||
        uses: ./.github/actions/restore-python
 | 
			
		||||
        with:
 | 
			
		||||
          python-version: ${{ env.DEFAULT_PYTHON }}
 | 
			
		||||
          cache-key: ${{ needs.common.outputs.cache-key }}
 | 
			
		||||
      - name: test_build_components -e config -c ${{ matrix.file }}
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          ./script/test_build_components -e config -c ${{ matrix.file }}
 | 
			
		||||
      - name: test_build_components -e compile -c ${{ matrix.file }}
 | 
			
		||||
        run: |
 | 
			
		||||
          . venv/bin/activate
 | 
			
		||||
          ./script/test_build_components -e compile -c ${{ matrix.file }}
 | 
			
		||||
 | 
			
		||||
  ci-status:
 | 
			
		||||
    name: CI Status
 | 
			
		||||
    runs-on: ubuntu-latest
 | 
			
		||||
@@ -368,6 +462,7 @@ jobs:
 | 
			
		||||
      - pyupgrade
 | 
			
		||||
      - compile-tests
 | 
			
		||||
      - clang-tidy
 | 
			
		||||
      - test-build-components
 | 
			
		||||
    if: always()
 | 
			
		||||
    steps:
 | 
			
		||||
      - name: Success
 | 
			
		||||
 
 | 
			
		||||
@@ -3,7 +3,7 @@
 | 
			
		||||
# See https://pre-commit.com/hooks.html for more hooks
 | 
			
		||||
repos:
 | 
			
		||||
  - repo: https://github.com/psf/black-pre-commit-mirror
 | 
			
		||||
    rev: 23.12.0
 | 
			
		||||
    rev: 23.12.1
 | 
			
		||||
    hooks:
 | 
			
		||||
      - id: black
 | 
			
		||||
        args:
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										21
									
								
								CODEOWNERS
									
									
									
									
									
								
							
							
						
						
									
										21
									
								
								CODEOWNERS
									
									
									
									
									
								
							@@ -25,7 +25,7 @@ esphome/components/airthings_ble/* @jeromelaban
 | 
			
		||||
esphome/components/airthings_wave_base/* @jeromelaban @kpfleming @ncareau
 | 
			
		||||
esphome/components/airthings_wave_mini/* @ncareau
 | 
			
		||||
esphome/components/airthings_wave_plus/* @jeromelaban
 | 
			
		||||
esphome/components/alarm_control_panel/* @grahambrown11
 | 
			
		||||
esphome/components/alarm_control_panel/* @grahambrown11 @hwstar
 | 
			
		||||
esphome/components/alpha3/* @jan-hofmeier
 | 
			
		||||
esphome/components/am43/* @buxtronix
 | 
			
		||||
esphome/components/am43/cover/* @buxtronix
 | 
			
		||||
@@ -34,6 +34,8 @@ esphome/components/analog_threshold/* @ianchi
 | 
			
		||||
esphome/components/animation/* @syndlex
 | 
			
		||||
esphome/components/anova/* @buxtronix
 | 
			
		||||
esphome/components/api/* @OttoWinter
 | 
			
		||||
esphome/components/as5600/* @ammmze
 | 
			
		||||
esphome/components/as5600/sensor/* @ammmze
 | 
			
		||||
esphome/components/as7341/* @mrgnr
 | 
			
		||||
esphome/components/async_tcp/* @OttoWinter
 | 
			
		||||
esphome/components/atc_mithermometer/* @ahpohl
 | 
			
		||||
@@ -50,8 +52,10 @@ esphome/components/bk72xx/* @kuba2k2
 | 
			
		||||
esphome/components/bl0939/* @ziceva
 | 
			
		||||
esphome/components/bl0940/* @tobias-
 | 
			
		||||
esphome/components/bl0942/* @dbuezas
 | 
			
		||||
esphome/components/ble_client/* @buxtronix
 | 
			
		||||
esphome/components/ble_client/* @buxtronix @clydebarrow
 | 
			
		||||
esphome/components/bluetooth_proxy/* @jesserockz
 | 
			
		||||
esphome/components/bme280_base/* @esphome/core
 | 
			
		||||
esphome/components/bme280_spi/* @apbodrov
 | 
			
		||||
esphome/components/bme680_bsec/* @trvrnrth
 | 
			
		||||
esphome/components/bmi160/* @flaviut
 | 
			
		||||
esphome/components/bmp3xx/* @martgras
 | 
			
		||||
@@ -67,6 +71,7 @@ esphome/components/cd74hc4067/* @asoehlke
 | 
			
		||||
esphome/components/climate/* @esphome/core
 | 
			
		||||
esphome/components/climate_ir/* @glmnet
 | 
			
		||||
esphome/components/color_temperature/* @jesserockz
 | 
			
		||||
esphome/components/combination/* @Cat-Ion @kahrendt
 | 
			
		||||
esphome/components/coolix/* @glmnet
 | 
			
		||||
esphome/components/copy/* @OttoWinter
 | 
			
		||||
esphome/components/cover/* @esphome/core
 | 
			
		||||
@@ -133,6 +138,7 @@ esphome/components/heatpumpir/* @rob-deutsch
 | 
			
		||||
esphome/components/hitachi_ac424/* @sourabhjaiswal
 | 
			
		||||
esphome/components/hm3301/* @freekode
 | 
			
		||||
esphome/components/homeassistant/* @OttoWinter
 | 
			
		||||
esphome/components/honeywell_hih_i2c/* @Benichou34
 | 
			
		||||
esphome/components/honeywellabp/* @RubyBailey
 | 
			
		||||
esphome/components/honeywellabp2_i2c/* @jpfaff
 | 
			
		||||
esphome/components/host/* @esphome/core
 | 
			
		||||
@@ -156,7 +162,6 @@ esphome/components/integration/* @OttoWinter
 | 
			
		||||
esphome/components/internal_temperature/* @Mat931
 | 
			
		||||
esphome/components/interval/* @esphome/core
 | 
			
		||||
esphome/components/json/* @OttoWinter
 | 
			
		||||
esphome/components/kalman_combinator/* @Cat-Ion
 | 
			
		||||
esphome/components/key_collector/* @ssieb
 | 
			
		||||
esphome/components/key_provider/* @ssieb
 | 
			
		||||
esphome/components/kuntze/* @ssieb
 | 
			
		||||
@@ -194,6 +199,7 @@ esphome/components/mcp9808/* @k7hpn
 | 
			
		||||
esphome/components/md5/* @esphome/core
 | 
			
		||||
esphome/components/mdns/* @esphome/core
 | 
			
		||||
esphome/components/media_player/* @jesserockz
 | 
			
		||||
esphome/components/micro_wake_word/* @jesserockz @kahrendt
 | 
			
		||||
esphome/components/micronova/* @jorre05
 | 
			
		||||
esphome/components/microphone/* @jesserockz
 | 
			
		||||
esphome/components/mics_4514/* @jesserockz
 | 
			
		||||
@@ -223,7 +229,7 @@ esphome/components/nextion/binary_sensor/* @senexcrenshaw
 | 
			
		||||
esphome/components/nextion/sensor/* @senexcrenshaw
 | 
			
		||||
esphome/components/nextion/switch/* @senexcrenshaw
 | 
			
		||||
esphome/components/nextion/text_sensor/* @senexcrenshaw
 | 
			
		||||
esphome/components/nfc/* @jesserockz
 | 
			
		||||
esphome/components/nfc/* @jesserockz @kbx81
 | 
			
		||||
esphome/components/noblex/* @AGalfra
 | 
			
		||||
esphome/components/number/* @esphome/core
 | 
			
		||||
esphome/components/ota/* @esphome/core
 | 
			
		||||
@@ -314,6 +320,9 @@ esphome/components/ssd1331_base/* @kbx81
 | 
			
		||||
esphome/components/ssd1331_spi/* @kbx81
 | 
			
		||||
esphome/components/ssd1351_base/* @kbx81
 | 
			
		||||
esphome/components/ssd1351_spi/* @kbx81
 | 
			
		||||
esphome/components/st7567_base/* @latonita
 | 
			
		||||
esphome/components/st7567_i2c/* @latonita
 | 
			
		||||
esphome/components/st7567_spi/* @latonita
 | 
			
		||||
esphome/components/st7735/* @SenexCrenshaw
 | 
			
		||||
esphome/components/st7789v/* @kbx81
 | 
			
		||||
esphome/components/st7920/* @marsjan155
 | 
			
		||||
@@ -325,7 +334,7 @@ esphome/components/tca9548a/* @andreashergert1984
 | 
			
		||||
esphome/components/tcl112/* @glmnet
 | 
			
		||||
esphome/components/tee501/* @Stock-M
 | 
			
		||||
esphome/components/teleinfo/* @0hax
 | 
			
		||||
esphome/components/template/alarm_control_panel/* @grahambrown11
 | 
			
		||||
esphome/components/template/alarm_control_panel/* @grahambrown11 @hwstar
 | 
			
		||||
esphome/components/text/* @mauritskorse
 | 
			
		||||
esphome/components/thermostat/* @kbx81
 | 
			
		||||
esphome/components/time/* @OttoWinter
 | 
			
		||||
@@ -355,9 +364,11 @@ esphome/components/ufire_ec/* @pvizeli
 | 
			
		||||
esphome/components/ufire_ise/* @pvizeli
 | 
			
		||||
esphome/components/ultrasonic/* @OttoWinter
 | 
			
		||||
esphome/components/vbus/* @ssieb
 | 
			
		||||
esphome/components/veml3235/* @kbx81
 | 
			
		||||
esphome/components/version/* @esphome/core
 | 
			
		||||
esphome/components/voice_assistant/* @jesserockz
 | 
			
		||||
esphome/components/wake_on_lan/* @willwill2will54
 | 
			
		||||
esphome/components/waveshare_epaper/* @clydebarrow
 | 
			
		||||
esphome/components/web_server_base/* @OttoWinter
 | 
			
		||||
esphome/components/web_server_idf/* @dentra
 | 
			
		||||
esphome/components/whirlpool/* @glmnet
 | 
			
		||||
 
 | 
			
		||||
@@ -34,8 +34,8 @@ RUN \
 | 
			
		||||
        python3-wheel=0.38.4-2 \
 | 
			
		||||
        iputils-ping=3:20221126-1 \
 | 
			
		||||
        git=1:2.39.2-1.1 \
 | 
			
		||||
        curl=7.88.1-10+deb12u4 \
 | 
			
		||||
        openssh-client=1:9.2p1-2+deb12u1 \
 | 
			
		||||
        curl=7.88.1-10+deb12u5 \
 | 
			
		||||
        openssh-client=1:9.2p1-2+deb12u2 \
 | 
			
		||||
        python3-cffi=1.15.1-5 \
 | 
			
		||||
        libcairo2=1.16.0-7 \
 | 
			
		||||
        libmagic1=1:5.44-3 \
 | 
			
		||||
@@ -50,7 +50,7 @@ RUN \
 | 
			
		||||
          libssl-dev=3.0.11-1~deb12u2 \
 | 
			
		||||
          libffi-dev=3.4.4-1 \
 | 
			
		||||
          libopenjp2-7=2.5.0-2 \
 | 
			
		||||
          libtiff6=4.5.0-6 \
 | 
			
		||||
          libtiff6=4.5.0-6+deb12u1 \
 | 
			
		||||
          cargo=0.66.0+ds1-1 \
 | 
			
		||||
          pkg-config=1.8.1-1 \
 | 
			
		||||
          gcc-arm-linux-gnueabihf=4:12.2.0-3; \
 | 
			
		||||
@@ -81,7 +81,7 @@ RUN \
 | 
			
		||||
    fi; \
 | 
			
		||||
    pip3 install \
 | 
			
		||||
    --break-system-packages --no-cache-dir \
 | 
			
		||||
    platformio==6.1.11 \
 | 
			
		||||
    platformio==6.1.13 \
 | 
			
		||||
    # Change some platformio settings
 | 
			
		||||
    && platformio settings set enable_telemetry No \
 | 
			
		||||
    && platformio settings set check_platformio_interval 1000000 \
 | 
			
		||||
 
 | 
			
		||||
@@ -139,6 +139,9 @@ ESP32_VARIANT_ADC2_PIN_TO_CHANNEL = {
 | 
			
		||||
    VARIANT_ESP32C3: {
 | 
			
		||||
        5: adc2_channel_t.ADC2_CHANNEL_0,
 | 
			
		||||
    },
 | 
			
		||||
    VARIANT_ESP32C2: {},
 | 
			
		||||
    VARIANT_ESP32C6: {},
 | 
			
		||||
    VARIANT_ESP32H2: {},
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -11,7 +11,7 @@ from esphome.const import (
 | 
			
		||||
)
 | 
			
		||||
from esphome.cpp_helpers import setup_entity
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@grahambrown11"]
 | 
			
		||||
CODEOWNERS = ["@grahambrown11", "@hwstar"]
 | 
			
		||||
IS_PLATFORM_COMPONENT = True
 | 
			
		||||
 | 
			
		||||
CONF_ON_TRIGGERED = "on_triggered"
 | 
			
		||||
@@ -22,6 +22,8 @@ CONF_ON_ARMED_HOME = "on_armed_home"
 | 
			
		||||
CONF_ON_ARMED_NIGHT = "on_armed_night"
 | 
			
		||||
CONF_ON_ARMED_AWAY = "on_armed_away"
 | 
			
		||||
CONF_ON_DISARMED = "on_disarmed"
 | 
			
		||||
CONF_ON_CHIME = "on_chime"
 | 
			
		||||
CONF_ON_READY = "on_ready"
 | 
			
		||||
 | 
			
		||||
alarm_control_panel_ns = cg.esphome_ns.namespace("alarm_control_panel")
 | 
			
		||||
AlarmControlPanel = alarm_control_panel_ns.class_("AlarmControlPanel", cg.EntityBase)
 | 
			
		||||
@@ -53,12 +55,22 @@ ArmedAwayTrigger = alarm_control_panel_ns.class_(
 | 
			
		||||
DisarmedTrigger = alarm_control_panel_ns.class_(
 | 
			
		||||
    "DisarmedTrigger", automation.Trigger.template()
 | 
			
		||||
)
 | 
			
		||||
ChimeTrigger = alarm_control_panel_ns.class_(
 | 
			
		||||
    "ChimeTrigger", automation.Trigger.template()
 | 
			
		||||
)
 | 
			
		||||
ReadyTrigger = alarm_control_panel_ns.class_(
 | 
			
		||||
    "ReadyTrigger", automation.Trigger.template()
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action)
 | 
			
		||||
ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", automation.Action)
 | 
			
		||||
ArmNightAction = alarm_control_panel_ns.class_("ArmNightAction", automation.Action)
 | 
			
		||||
DisarmAction = alarm_control_panel_ns.class_("DisarmAction", automation.Action)
 | 
			
		||||
PendingAction = alarm_control_panel_ns.class_("PendingAction", automation.Action)
 | 
			
		||||
TriggeredAction = alarm_control_panel_ns.class_("TriggeredAction", automation.Action)
 | 
			
		||||
ChimeAction = alarm_control_panel_ns.class_("ChimeAction", automation.Action)
 | 
			
		||||
ReadyAction = alarm_control_panel_ns.class_("ReadyAction", automation.Action)
 | 
			
		||||
 | 
			
		||||
AlarmControlPanelCondition = alarm_control_panel_ns.class_(
 | 
			
		||||
    "AlarmControlPanelCondition", automation.Condition
 | 
			
		||||
)
 | 
			
		||||
@@ -111,6 +123,16 @@ ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend(
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ON_CHIME): automation.validate_automation(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ChimeTrigger),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ON_READY): automation.validate_automation(
 | 
			
		||||
            {
 | 
			
		||||
                cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ReadyTrigger),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -157,6 +179,12 @@ async def setup_alarm_control_panel_core_(var, config):
 | 
			
		||||
    for conf in config.get(CONF_ON_CLEARED, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await automation.build_automation(trigger, [], conf)
 | 
			
		||||
    for conf in config.get(CONF_ON_CHIME, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await automation.build_automation(trigger, [], conf)
 | 
			
		||||
    for conf in config.get(CONF_ON_READY, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await automation.build_automation(trigger, [], conf)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def register_alarm_control_panel(var, config):
 | 
			
		||||
@@ -232,6 +260,29 @@ async def alarm_action_trigger_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "alarm_control_panel.chime", ChimeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
async def alarm_action_chime_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, paren)
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "alarm_control_panel.ready", ReadyAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
@automation.register_condition(
 | 
			
		||||
    "alarm_control_panel.ready",
 | 
			
		||||
    AlarmControlPanelCondition,
 | 
			
		||||
    ALARM_CONTROL_PANEL_CONDITION_SCHEMA,
 | 
			
		||||
)
 | 
			
		||||
async def alarm_action_ready_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    paren = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, paren)
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_condition(
 | 
			
		||||
    "alarm_control_panel.is_armed",
 | 
			
		||||
    AlarmControlPanelCondition,
 | 
			
		||||
 
 | 
			
		||||
@@ -96,6 +96,14 @@ void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback
 | 
			
		||||
  this->cleared_callback_.add(std::move(callback));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AlarmControlPanel::add_on_chime_callback(std::function<void()> &&callback) {
 | 
			
		||||
  this->chime_callback_.add(std::move(callback));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AlarmControlPanel::add_on_ready_callback(std::function<void()> &&callback) {
 | 
			
		||||
  this->ready_callback_.add(std::move(callback));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AlarmControlPanel::arm_away(optional<std::string> code) {
 | 
			
		||||
  auto call = this->make_call();
 | 
			
		||||
  call.arm_away();
 | 
			
		||||
 
 | 
			
		||||
@@ -89,6 +89,18 @@ class AlarmControlPanel : public EntityBase {
 | 
			
		||||
   */
 | 
			
		||||
  void add_on_cleared_callback(std::function<void()> &&callback);
 | 
			
		||||
 | 
			
		||||
  /** Add a callback for when a chime zone goes from closed to open
 | 
			
		||||
   *
 | 
			
		||||
   * @param callback The callback function
 | 
			
		||||
   */
 | 
			
		||||
  void add_on_chime_callback(std::function<void()> &&callback);
 | 
			
		||||
 | 
			
		||||
  /** Add a callback for when a ready state changes
 | 
			
		||||
   *
 | 
			
		||||
   * @param callback The callback function
 | 
			
		||||
   */
 | 
			
		||||
  void add_on_ready_callback(std::function<void()> &&callback);
 | 
			
		||||
 | 
			
		||||
  /** A numeric representation of the supported features as per HomeAssistant
 | 
			
		||||
   *
 | 
			
		||||
   */
 | 
			
		||||
@@ -178,6 +190,10 @@ class AlarmControlPanel : public EntityBase {
 | 
			
		||||
  CallbackManager<void()> disarmed_callback_{};
 | 
			
		||||
  // clear callback
 | 
			
		||||
  CallbackManager<void()> cleared_callback_{};
 | 
			
		||||
  // chime callback
 | 
			
		||||
  CallbackManager<void()> chime_callback_{};
 | 
			
		||||
  // ready callback
 | 
			
		||||
  CallbackManager<void()> ready_callback_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace alarm_control_panel
 | 
			
		||||
 
 | 
			
		||||
@@ -69,6 +69,20 @@ class ClearedTrigger : public Trigger<> {
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ChimeTrigger : public Trigger<> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit ChimeTrigger(AlarmControlPanel *alarm_control_panel) {
 | 
			
		||||
    alarm_control_panel->add_on_chime_callback([this]() { this->trigger(); });
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class ReadyTrigger : public Trigger<> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit ReadyTrigger(AlarmControlPanel *alarm_control_panel) {
 | 
			
		||||
    alarm_control_panel->add_on_ready_callback([this]() { this->trigger(); });
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class ArmAwayAction : public Action<Ts...> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit ArmAwayAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {}
 | 
			
		||||
 
 | 
			
		||||
@@ -118,7 +118,9 @@ void APIConnection::loop() {
 | 
			
		||||
  this->list_entities_iterator_.advance();
 | 
			
		||||
  this->initial_state_iterator_.advance();
 | 
			
		||||
 | 
			
		||||
  const uint32_t keepalive = 60000;
 | 
			
		||||
  static uint32_t keepalive = 60000;
 | 
			
		||||
  static uint8_t max_ping_retries = 60;
 | 
			
		||||
  static uint16_t ping_retry_interval = 1000;
 | 
			
		||||
  const uint32_t now = millis();
 | 
			
		||||
  if (this->sent_ping_) {
 | 
			
		||||
    // Disconnect if not responded within 2.5*keepalive
 | 
			
		||||
@@ -126,10 +128,24 @@ void APIConnection::loop() {
 | 
			
		||||
      on_fatal_error();
 | 
			
		||||
      ESP_LOGW(TAG, "%s didn't respond to ping request in time. Disconnecting...", this->client_combined_info_.c_str());
 | 
			
		||||
    }
 | 
			
		||||
  } else if (now - this->last_traffic_ > keepalive) {
 | 
			
		||||
  } else if (now - this->last_traffic_ > keepalive && now > this->next_ping_retry_) {
 | 
			
		||||
    ESP_LOGVV(TAG, "Sending keepalive PING...");
 | 
			
		||||
    this->sent_ping_ = true;
 | 
			
		||||
    this->send_ping_request(PingRequest());
 | 
			
		||||
    this->sent_ping_ = this->send_ping_request(PingRequest());
 | 
			
		||||
    if (!this->sent_ping_) {
 | 
			
		||||
      this->next_ping_retry_ = now + ping_retry_interval;
 | 
			
		||||
      this->ping_retries_++;
 | 
			
		||||
      if (this->ping_retries_ >= max_ping_retries) {
 | 
			
		||||
        on_fatal_error();
 | 
			
		||||
        ESP_LOGE(TAG, "%s: Sending keepalive failed %d time(s). Disconnecting...", this->client_combined_info_.c_str(),
 | 
			
		||||
                 this->ping_retries_);
 | 
			
		||||
      } else if (this->ping_retries_ >= 10) {
 | 
			
		||||
        ESP_LOGW(TAG, "%s: Sending keepalive failed %d time(s), will retry in %d ms",
 | 
			
		||||
                 this->client_combined_info_.c_str(), this->ping_retries_, ping_retry_interval);
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGD(TAG, "%s: Sending keepalive failed %d time(s), will retry in %d ms",
 | 
			
		||||
                 this->client_combined_info_.c_str(), this->ping_retries_, ping_retry_interval);
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#ifdef USE_ESP32_CAMERA
 | 
			
		||||
 
 | 
			
		||||
@@ -140,6 +140,7 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
  void on_disconnect_response(const DisconnectResponse &value) override;
 | 
			
		||||
  void on_ping_response(const PingResponse &value) override {
 | 
			
		||||
    // we initiated ping
 | 
			
		||||
    this->ping_retries_ = 0;
 | 
			
		||||
    this->sent_ping_ = false;
 | 
			
		||||
  }
 | 
			
		||||
  void on_home_assistant_state_response(const HomeAssistantStateResponse &msg) override;
 | 
			
		||||
@@ -217,6 +218,8 @@ class APIConnection : public APIServerConnection {
 | 
			
		||||
  bool state_subscription_{false};
 | 
			
		||||
  int log_subscription_{ESPHOME_LOG_LEVEL_NONE};
 | 
			
		||||
  uint32_t last_traffic_;
 | 
			
		||||
  uint32_t next_ping_retry_{0};
 | 
			
		||||
  uint8_t ping_retries_{0};
 | 
			
		||||
  bool sent_ping_{false};
 | 
			
		||||
  bool service_call_subscription_{false};
 | 
			
		||||
  bool next_close_ = false;
 | 
			
		||||
 
 | 
			
		||||
@@ -3848,6 +3848,7 @@ void ListEntitiesClimateResponse::dump_to(std::string &out) const {
 | 
			
		||||
  sprintf(buffer, "%g", this->visual_max_humidity);
 | 
			
		||||
  out.append(buffer);
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
  out.append("}");
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
bool ClimateStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) {
 | 
			
		||||
@@ -4015,6 +4016,7 @@ void ClimateStateResponse::dump_to(std::string &out) const {
 | 
			
		||||
  sprintf(buffer, "%g", this->target_humidity);
 | 
			
		||||
  out.append(buffer);
 | 
			
		||||
  out.append("\n");
 | 
			
		||||
  out.append("}");
 | 
			
		||||
}
 | 
			
		||||
#endif
 | 
			
		||||
bool ClimateCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) {
 | 
			
		||||
 
 | 
			
		||||
@@ -319,7 +319,7 @@ void APIServer::set_reboot_timeout(uint32_t reboot_timeout) { this->reboot_timeo
 | 
			
		||||
#ifdef USE_HOMEASSISTANT_TIME
 | 
			
		||||
void APIServer::request_time() {
 | 
			
		||||
  for (auto &client : this->clients_) {
 | 
			
		||||
    if (!client->remove_ && client->connection_state_ == APIConnection::ConnectionState::CONNECTED)
 | 
			
		||||
    if (!client->remove_ && client->is_authenticated())
 | 
			
		||||
      client->send_time_request();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										228
									
								
								esphome/components/as5600/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										228
									
								
								esphome/components/as5600/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,228 @@
 | 
			
		||||
from esphome import pins
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import i2c
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_DIR_PIN,
 | 
			
		||||
    CONF_DIRECTION,
 | 
			
		||||
    CONF_HYSTERESIS,
 | 
			
		||||
    CONF_RANGE,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@ammmze"]
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
MULTI_CONF = True
 | 
			
		||||
 | 
			
		||||
as5600_ns = cg.esphome_ns.namespace("as5600")
 | 
			
		||||
AS5600Component = as5600_ns.class_("AS5600Component", cg.Component, i2c.I2CDevice)
 | 
			
		||||
 | 
			
		||||
DIRECTION = {
 | 
			
		||||
    "CLOCKWISE": 0,
 | 
			
		||||
    "COUNTERCLOCKWISE": 1,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
POWER_MODE = {
 | 
			
		||||
    "NOMINAL": 0,
 | 
			
		||||
    "LOW1": 1,
 | 
			
		||||
    "LOW2": 2,
 | 
			
		||||
    "LOW3": 3,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
HYSTERESIS = {
 | 
			
		||||
    "NONE": 0,
 | 
			
		||||
    "LSB1": 1,
 | 
			
		||||
    "LSB2": 2,
 | 
			
		||||
    "LSB3": 3,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SLOW_FILTER = {
 | 
			
		||||
    "16X": 0,
 | 
			
		||||
    "8X": 1,
 | 
			
		||||
    "4X": 2,
 | 
			
		||||
    "2X": 3,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
FAST_FILTER = {
 | 
			
		||||
    "NONE": 0,
 | 
			
		||||
    "LSB6": 1,
 | 
			
		||||
    "LSB7": 2,
 | 
			
		||||
    "LSB9": 3,
 | 
			
		||||
    "LSB18": 4,
 | 
			
		||||
    "LSB21": 5,
 | 
			
		||||
    "LSB24": 6,
 | 
			
		||||
    "LSB10": 7,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
CONF_ANGLE = "angle"
 | 
			
		||||
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"
 | 
			
		||||
CONF_END_POSITION = "end_position"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
RESOLUTION = 4096
 | 
			
		||||
MAX_POSITION = RESOLUTION - 1
 | 
			
		||||
ANGLE_TO_POSITION = RESOLUTION / 360
 | 
			
		||||
POSITION_TO_ANGLE = 360 / RESOLUTION
 | 
			
		||||
# validate min range of 18deg (per datasheet) ... though i seem to get valid values down to a range of 192steps (16.875deg)
 | 
			
		||||
MIN_RANGE = round(18 * ANGLE_TO_POSITION)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def angle(min=-360, max=360):
 | 
			
		||||
    return cv.All(
 | 
			
		||||
        cv.float_with_unit("angle", "(°|deg)"), cv.float_range(min=min, max=max)
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def angle_to_position(value, min=-360, max=360):
 | 
			
		||||
    try:
 | 
			
		||||
        value = angle(min=min, max=max)(value)
 | 
			
		||||
        return (RESOLUTION + round(value * ANGLE_TO_POSITION)) % RESOLUTION
 | 
			
		||||
    except cv.Invalid as e:
 | 
			
		||||
        raise cv.Invalid(f"When using angle, {e.error_message}")
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def percent_to_position(value):
 | 
			
		||||
    value = cv.possibly_negative_percentage(value)
 | 
			
		||||
    return (RESOLUTION + round(value * RESOLUTION)) % RESOLUTION
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def position(min=-MAX_POSITION, max=MAX_POSITION):
 | 
			
		||||
    """Validate that the config option is a position.
 | 
			
		||||
    Accepts integers, degrees, or percentage (of 360 degrees).
 | 
			
		||||
    """
 | 
			
		||||
 | 
			
		||||
    def validator(value):
 | 
			
		||||
        if isinstance(value, str) and value.endswith("%"):
 | 
			
		||||
            value = percent_to_position(value)
 | 
			
		||||
 | 
			
		||||
        if isinstance(value, str) and (value.endswith("°") or value.endswith("deg")):
 | 
			
		||||
            return angle_to_position(
 | 
			
		||||
                value,
 | 
			
		||||
                min=round(min * POSITION_TO_ANGLE),
 | 
			
		||||
                max=round(max * POSITION_TO_ANGLE),
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
        return cv.int_range(min=min, max=max)(value)
 | 
			
		||||
 | 
			
		||||
    return validator
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def position_range():
 | 
			
		||||
    """Validate that value given is a valid range for the device.
 | 
			
		||||
    A valid range is one of the following:
 | 
			
		||||
    - a value of 0 (meaning full range)
 | 
			
		||||
    - 18 thru 360 degrees
 | 
			
		||||
    - negative 360 thru negative 18 degrees (notes: these are normalized to their positive values, accepting negatives is for convenience)
 | 
			
		||||
    """
 | 
			
		||||
    zero_validator = position(min=0, max=0)
 | 
			
		||||
    negative_validator = cv.Any(
 | 
			
		||||
        position(min=-MAX_POSITION, max=-MIN_RANGE),
 | 
			
		||||
        zero_validator,
 | 
			
		||||
    )
 | 
			
		||||
    positive_validator = cv.Any(
 | 
			
		||||
        position(min=MIN_RANGE, max=MAX_POSITION),
 | 
			
		||||
        zero_validator,
 | 
			
		||||
    )
 | 
			
		||||
 | 
			
		||||
    def validator(value):
 | 
			
		||||
        is_negative_str = isinstance(value, str) and value.startswith("-")
 | 
			
		||||
        is_negative_num = isinstance(value, (float, int)) and value < 0
 | 
			
		||||
        if is_negative_str or is_negative_num:
 | 
			
		||||
            return negative_validator(value)
 | 
			
		||||
        return positive_validator(value)
 | 
			
		||||
 | 
			
		||||
    return validator
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def has_valid_range_config():
 | 
			
		||||
    """Validate that that the config start + end position results in a valid
 | 
			
		||||
    positional range, which must be >= 18degrees
 | 
			
		||||
    """
 | 
			
		||||
    range_validator = position_range()
 | 
			
		||||
 | 
			
		||||
    def validator(config):
 | 
			
		||||
        # if we don't have an end position, then there is nothing to do
 | 
			
		||||
        if CONF_END_POSITION not in config:
 | 
			
		||||
            return config
 | 
			
		||||
 | 
			
		||||
        # determine the range by taking the difference from the end and start
 | 
			
		||||
        range = config[CONF_END_POSITION] - config[CONF_START_POSITION]
 | 
			
		||||
 | 
			
		||||
        # but need to account for start position being greater than end position
 | 
			
		||||
        # where the range rolls back around the 0 position
 | 
			
		||||
        if config[CONF_END_POSITION] < config[CONF_START_POSITION]:
 | 
			
		||||
            range = RESOLUTION + config[CONF_END_POSITION] - config[CONF_START_POSITION]
 | 
			
		||||
 | 
			
		||||
        try:
 | 
			
		||||
            range_validator(range)
 | 
			
		||||
            return config
 | 
			
		||||
        except cv.Invalid as e:
 | 
			
		||||
            raise cv.Invalid(
 | 
			
		||||
                f"The range between start and end position is invalid. It was was {range} but {e.error_message}"
 | 
			
		||||
            )
 | 
			
		||||
 | 
			
		||||
    return validator
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.All(
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(AS5600Component),
 | 
			
		||||
            cv.Optional(CONF_DIR_PIN): pins.gpio_input_pin_schema,
 | 
			
		||||
            cv.Optional(CONF_DIRECTION, default="CLOCKWISE"): cv.enum(
 | 
			
		||||
                DIRECTION, upper=True
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_WATCHDOG, default=False): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_POWER_MODE, default="NOMINAL"): cv.enum(
 | 
			
		||||
                POWER_MODE, upper=True, space=""
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_HYSTERESIS, default="NONE"): cv.enum(
 | 
			
		||||
                HYSTERESIS, upper=True, space=""
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_SLOW_FILTER, default="16X"): cv.enum(
 | 
			
		||||
                SLOW_FILTER, upper=True, space=""
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_FAST_FILTER, default="NONE"): cv.enum(
 | 
			
		||||
                FAST_FILTER, upper=True, space=""
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_START_POSITION, default=0): position(),
 | 
			
		||||
            cv.Optional(CONF_END_POSITION): position(),
 | 
			
		||||
            cv.Optional(CONF_RANGE): position_range(),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
    .extend(i2c.i2c_device_schema(0x36)),
 | 
			
		||||
    # ensure end_position and range are mutually exclusive
 | 
			
		||||
    cv.has_at_most_one_key(CONF_END_POSITION, CONF_RANGE),
 | 
			
		||||
    has_valid_range_config(),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await i2c.register_i2c_device(var, config)
 | 
			
		||||
 | 
			
		||||
    cg.add(var.set_direction(config[CONF_DIRECTION]))
 | 
			
		||||
    cg.add(var.set_watchdog(config[CONF_WATCHDOG]))
 | 
			
		||||
    cg.add(var.set_power_mode(config[CONF_POWER_MODE]))
 | 
			
		||||
    cg.add(var.set_hysteresis(config[CONF_HYSTERESIS]))
 | 
			
		||||
    cg.add(var.set_slow_filter(config[CONF_SLOW_FILTER]))
 | 
			
		||||
    cg.add(var.set_fast_filter(config[CONF_FAST_FILTER]))
 | 
			
		||||
    cg.add(var.set_start_position(config[CONF_START_POSITION]))
 | 
			
		||||
 | 
			
		||||
    if dir_pin_config := config.get(CONF_DIR_PIN):
 | 
			
		||||
        pin = await cg.gpio_pin_expression(dir_pin_config)
 | 
			
		||||
        cg.add(var.set_dir_pin(pin))
 | 
			
		||||
 | 
			
		||||
    if (end_position_config := config.get(CONF_END_POSITION, None)) is not None:
 | 
			
		||||
        cg.add(var.set_end_position(end_position_config))
 | 
			
		||||
 | 
			
		||||
    if (range_config := config.get(CONF_RANGE, None)) is not None:
 | 
			
		||||
        cg.add(var.set_range(range_config))
 | 
			
		||||
							
								
								
									
										138
									
								
								esphome/components/as5600/as5600.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										138
									
								
								esphome/components/as5600/as5600.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,138 @@
 | 
			
		||||
#include "as5600.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace as5600 {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "as5600";
 | 
			
		||||
 | 
			
		||||
// Configuration registers
 | 
			
		||||
static const uint8_t REGISTER_ZMCO = 0x00;  // 8 bytes  / R
 | 
			
		||||
static const uint8_t REGISTER_ZPOS = 0x01;  // 16 bytes / RW
 | 
			
		||||
static const uint8_t REGISTER_MPOS = 0x03;  // 16 bytes / RW
 | 
			
		||||
static const uint8_t REGISTER_MANG = 0x05;  // 16 bytes / RW
 | 
			
		||||
static const uint8_t REGISTER_CONF = 0x07;  // 16 bytes / RW
 | 
			
		||||
 | 
			
		||||
// Output registers
 | 
			
		||||
static const uint8_t REGISTER_ANGLE_RAW = 0x0C;  // 16 bytes / R
 | 
			
		||||
static const uint8_t REGISTER_ANGLE = 0x0E;      // 16 bytes / R
 | 
			
		||||
 | 
			
		||||
// Status registers
 | 
			
		||||
static const uint8_t REGISTER_STATUS = 0x0B;     // 8 bytes  / R
 | 
			
		||||
static const uint8_t REGISTER_AGC = 0x1A;        // 8 bytes  / R
 | 
			
		||||
static const uint8_t REGISTER_MAGNITUDE = 0x1B;  // 16 bytes / R
 | 
			
		||||
 | 
			
		||||
void AS5600Component::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up AS5600...");
 | 
			
		||||
 | 
			
		||||
  if (!this->read_byte(REGISTER_STATUS).has_value()) {
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // configuration direction pin, if given
 | 
			
		||||
  // the dir pin on the chip should be low for clockwise
 | 
			
		||||
  // and high for counterclockwise. If the pin is left floating
 | 
			
		||||
  // the reported positions will be erratic.
 | 
			
		||||
  if (this->dir_pin_ != nullptr) {
 | 
			
		||||
    this->dir_pin_->pin_mode(gpio::FLAG_OUTPUT);
 | 
			
		||||
    this->dir_pin_->digital_write(this->direction_ == 1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // build config register
 | 
			
		||||
  // take the value, shift it left, and add mask to it to ensure we
 | 
			
		||||
  // are only changing the bits appropriate for that setting in the
 | 
			
		||||
  // off chance we somehow have bad value in there and it makes for
 | 
			
		||||
  // a nice visual for the bit positions.
 | 
			
		||||
  uint16_t config = 0;
 | 
			
		||||
  // clang-format off
 | 
			
		||||
  config |= (this->watchdog_      << 13) & 0b0010000000000000;
 | 
			
		||||
  config |= (this->fast_filter_   << 10) & 0b0001110000000000;
 | 
			
		||||
  config |= (this->slow_filter_   <<  8) & 0b0000001100000000;
 | 
			
		||||
  config |= (this->pwm_frequency_ <<  6) & 0b0000000011000000;
 | 
			
		||||
  config |= (this->output_mode_   <<  4) & 0b0000000000110000;
 | 
			
		||||
  config |= (this->hysteresis_    <<  2) & 0b0000000000001100;
 | 
			
		||||
  config |= (this->power_mode_    <<  0) & 0b0000000000000011;
 | 
			
		||||
  // clang-format on
 | 
			
		||||
 | 
			
		||||
  // write config to config register
 | 
			
		||||
  if (!this->write_byte_16(REGISTER_CONF, config)) {
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // configure the start position
 | 
			
		||||
  this->write_byte_16(REGISTER_ZPOS, this->start_position_);
 | 
			
		||||
 | 
			
		||||
  // configure either end position or max angle
 | 
			
		||||
  if (this->end_mode_ == END_MODE_POSITION) {
 | 
			
		||||
    this->write_byte_16(REGISTER_MPOS, this->end_position_);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->write_byte_16(REGISTER_MANG, this->end_position_);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // calculate the raw max from end position or start + range
 | 
			
		||||
  this->raw_max_ = this->end_mode_ == END_MODE_POSITION ? this->end_position_ & 4095
 | 
			
		||||
                                                        : (this->start_position_ + this->end_position_) & 4095;
 | 
			
		||||
 | 
			
		||||
  // calculate allowed range of motion by taking the start from the end
 | 
			
		||||
  // but only if the end is greater than the start. If the start is greater
 | 
			
		||||
  // than the end position, then that means we take the start all the way to
 | 
			
		||||
  // reset point (i.e. 0 deg raw) and then we that with the end position
 | 
			
		||||
  uint16_t range = this->raw_max_ > this->start_position_ ? this->raw_max_ - this->start_position_
 | 
			
		||||
                                                          : (4095 - this->start_position_) + this->raw_max_;
 | 
			
		||||
 | 
			
		||||
  // range scale is ratio of actual allowed range to the full range
 | 
			
		||||
  this->range_scale_ = range / 4095.0f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AS5600Component::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "AS5600:");
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
 | 
			
		||||
  if (this->is_failed()) {
 | 
			
		||||
    ESP_LOGE(TAG, "Communication with AS5600 failed!");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Watchdog: %d", this->watchdog_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Fast Filter: %d", this->fast_filter_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Slow Filter: %d", this->slow_filter_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Hysteresis: %d", this->hysteresis_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Start Position: %d", this->start_position_);
 | 
			
		||||
  if (this->end_mode_ == END_MODE_POSITION) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  End Position: %d", this->end_position_);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "  Range: %d", this->end_position_);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool AS5600Component::in_range(uint16_t raw_position) {
 | 
			
		||||
  return this->raw_max_ > this->start_position_
 | 
			
		||||
             ? raw_position >= this->start_position_ && raw_position <= this->raw_max_
 | 
			
		||||
             : raw_position >= this->start_position_ || raw_position <= this->raw_max_;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
AS5600MagnetStatus AS5600Component::read_magnet_status() {
 | 
			
		||||
  uint8_t status = this->reg(REGISTER_STATUS).get() >> 3 & 0b000111;
 | 
			
		||||
  return static_cast<AS5600MagnetStatus>(status);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
optional<uint16_t> AS5600Component::read_position() {
 | 
			
		||||
  uint16_t pos = 0;
 | 
			
		||||
  if (!this->read_byte_16(REGISTER_ANGLE, &pos)) {
 | 
			
		||||
    return {};
 | 
			
		||||
  }
 | 
			
		||||
  return pos;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
optional<uint16_t> AS5600Component::read_raw_position() {
 | 
			
		||||
  uint16_t pos = 0;
 | 
			
		||||
  if (!this->read_byte_16(REGISTER_ANGLE_RAW, &pos)) {
 | 
			
		||||
    return {};
 | 
			
		||||
  }
 | 
			
		||||
  return pos;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace as5600
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										105
									
								
								esphome/components/as5600/as5600.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								esphome/components/as5600/as5600.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,105 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/preferences.h"
 | 
			
		||||
#include "esphome/components/i2c/i2c.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace as5600 {
 | 
			
		||||
 | 
			
		||||
static const uint16_t POSITION_COUNT = 4096;
 | 
			
		||||
static const float RAW_TO_DEGREES = 360.0 / POSITION_COUNT;
 | 
			
		||||
static const float DEGREES_TO_RAW = POSITION_COUNT / 360.0;
 | 
			
		||||
 | 
			
		||||
enum EndPositionMode : uint8_t {
 | 
			
		||||
  // In this mode, the end position is calculated by taking the start position
 | 
			
		||||
  // and adding the range/positions. For example, you could say start at 90deg,
 | 
			
		||||
  // and have a range of 180deg and effectively the sensor will report values
 | 
			
		||||
  // from the physical 90deg thru 270deg.
 | 
			
		||||
  END_MODE_RANGE,
 | 
			
		||||
  // In this mode, the end position is explicitly set, and changing the start
 | 
			
		||||
  // position will NOT change the end position.
 | 
			
		||||
  END_MODE_POSITION,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum OutRangeMode : uint8_t {
 | 
			
		||||
  // In this mode, the AS5600 chip itself actually reports these values, but
 | 
			
		||||
  // effectively it splits the out-of-range values in half, and when positioned
 | 
			
		||||
  // over the half closest to the min/start position, it will report 0 and when
 | 
			
		||||
  // positioned over the half closes to the max/end position, it will report the
 | 
			
		||||
  // max/end value.
 | 
			
		||||
  OUT_RANGE_MODE_MIN_MAX,
 | 
			
		||||
  // In this mode, when the magnet is positioned outside the configured
 | 
			
		||||
  // range, the sensor will report NAN, which translates to "Unknown"
 | 
			
		||||
  // in Home Assistant.
 | 
			
		||||
  OUT_RANGE_MODE_NAN,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
enum AS5600MagnetStatus : uint8_t {
 | 
			
		||||
  MAGNET_GONE = 2,    // 0b010 / magnet not detected
 | 
			
		||||
  MAGNET_OK = 4,      // 0b100 / magnet just right
 | 
			
		||||
  MAGNET_STRONG = 5,  // 0b101 / magnet too strong
 | 
			
		||||
  MAGNET_WEAK = 6,    // 0b110 / magnet too weak
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class AS5600Component : public Component, public i2c::I2CDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  /// Set up the internal sensor array.
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  /// HARDWARE_LATE setup priority
 | 
			
		||||
  float get_setup_priority() const override { return setup_priority::DATA; }
 | 
			
		||||
 | 
			
		||||
  // configuration setters
 | 
			
		||||
  void set_dir_pin(InternalGPIOPin *pin) { this->dir_pin_ = pin; }
 | 
			
		||||
  void set_direction(uint8_t direction) { this->direction_ = direction; }
 | 
			
		||||
  void set_fast_filter(uint8_t fast_filter) { this->fast_filter_ = fast_filter; }
 | 
			
		||||
  void set_hysteresis(uint8_t hysteresis) { this->hysteresis_ = hysteresis; }
 | 
			
		||||
  void set_power_mode(uint8_t power_mode) { this->power_mode_ = power_mode; }
 | 
			
		||||
  void set_slow_filter(uint8_t slow_filter) { this->slow_filter_ = slow_filter; }
 | 
			
		||||
  void set_watchdog(bool watchdog) { this->watchdog_ = watchdog; }
 | 
			
		||||
  bool get_watchdog() { return this->watchdog_; }
 | 
			
		||||
  void set_start_position(uint16_t start_position) { this->start_position_ = start_position % POSITION_COUNT; }
 | 
			
		||||
  void set_end_position(uint16_t end_position) {
 | 
			
		||||
    this->end_position_ = end_position % POSITION_COUNT;
 | 
			
		||||
    this->end_mode_ = END_MODE_POSITION;
 | 
			
		||||
  }
 | 
			
		||||
  void set_range(uint16_t range) {
 | 
			
		||||
    this->end_position_ = range % POSITION_COUNT;
 | 
			
		||||
    this->end_mode_ = END_MODE_RANGE;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Gets the scale value for the configured range.
 | 
			
		||||
  // For example, if configured to start at 0deg and end at 180deg, the
 | 
			
		||||
  // range is 50% of the native/raw range, so the range scale would be 0.5.
 | 
			
		||||
  // If configured to use the full 360deg, the range scale would be 1.0.
 | 
			
		||||
  float get_range_scale() { return this->range_scale_; }
 | 
			
		||||
 | 
			
		||||
  // Indicates whether the given *raw* position is within the configured range
 | 
			
		||||
  bool in_range(uint16_t raw_position);
 | 
			
		||||
 | 
			
		||||
  AS5600MagnetStatus read_magnet_status();
 | 
			
		||||
  optional<uint16_t> read_position();
 | 
			
		||||
  optional<uint16_t> read_raw_position();
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  InternalGPIOPin *dir_pin_{nullptr};
 | 
			
		||||
  uint8_t direction_;
 | 
			
		||||
  uint8_t fast_filter_;
 | 
			
		||||
  uint8_t hysteresis_;
 | 
			
		||||
  uint8_t power_mode_;
 | 
			
		||||
  uint8_t slow_filter_;
 | 
			
		||||
  uint8_t pwm_frequency_{0};
 | 
			
		||||
  uint8_t output_mode_{0};
 | 
			
		||||
  bool watchdog_;
 | 
			
		||||
  uint16_t start_position_;
 | 
			
		||||
  uint16_t end_position_{0};
 | 
			
		||||
  uint16_t raw_max_;
 | 
			
		||||
  EndPositionMode end_mode_{END_MODE_RANGE};
 | 
			
		||||
  float range_scale_{1.0};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace as5600
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										119
									
								
								esphome/components/as5600/sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										119
									
								
								esphome/components/as5600/sensor/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,119 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    ICON_MAGNET,
 | 
			
		||||
    ICON_ROTATE_RIGHT,
 | 
			
		||||
    CONF_GAIN,
 | 
			
		||||
    ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
    CONF_MAGNITUDE,
 | 
			
		||||
    CONF_STATUS,
 | 
			
		||||
    CONF_POSITION,
 | 
			
		||||
)
 | 
			
		||||
from .. import as5600_ns, AS5600Component
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@ammmze"]
 | 
			
		||||
DEPENDENCIES = ["as5600"]
 | 
			
		||||
 | 
			
		||||
AS5600Sensor = as5600_ns.class_("AS5600Sensor", sensor.Sensor, cg.PollingComponent)
 | 
			
		||||
 | 
			
		||||
CONF_ANGLE = "angle"
 | 
			
		||||
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"
 | 
			
		||||
CONF_BURN_COUNT = "burn_count"
 | 
			
		||||
CONF_START_POSITION = "start_position"
 | 
			
		||||
CONF_END_POSITION = "end_position"
 | 
			
		||||
CONF_OUT_OF_RANGE_MODE = "out_of_range_mode"
 | 
			
		||||
 | 
			
		||||
OutOfRangeMode = as5600_ns.enum("OutRangeMode")
 | 
			
		||||
OUT_OF_RANGE_MODES = {
 | 
			
		||||
    "MIN_MAX": OutOfRangeMode.OUT_RANGE_MODE_MIN_MAX,
 | 
			
		||||
    "NAN": OutOfRangeMode.OUT_RANGE_MODE_NAN,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONF_AS5600_ID = "as5600_id"
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    sensor.sensor_schema(
 | 
			
		||||
        AS5600Sensor,
 | 
			
		||||
        accuracy_decimals=0,
 | 
			
		||||
        icon=ICON_ROTATE_RIGHT,
 | 
			
		||||
        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    )
 | 
			
		||||
    .extend(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(CONF_AS5600_ID): cv.use_id(AS5600Component),
 | 
			
		||||
            cv.Optional(CONF_OUT_OF_RANGE_MODE): cv.enum(
 | 
			
		||||
                OUT_OF_RANGE_MODES, upper=True, space="_"
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_RAW_POSITION): sensor.sensor_schema(
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                icon=ICON_ROTATE_RIGHT,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_GAIN): sensor.sensor_schema(
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_MAGNITUDE): sensor.sensor_schema(
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                icon=ICON_MAGNET,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_STATUS): sensor.sensor_schema(
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                icon=ICON_MAGNET,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
                entity_category=ENTITY_CATEGORY_DIAGNOSTIC,
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_parented(var, config[CONF_AS5600_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await sensor.register_sensor(var, config)
 | 
			
		||||
 | 
			
		||||
    if out_of_range_mode_config := config.get(CONF_OUT_OF_RANGE_MODE):
 | 
			
		||||
        cg.add(var.set_out_of_range_mode(out_of_range_mode_config))
 | 
			
		||||
 | 
			
		||||
    if angle_config := config.get(CONF_ANGLE):
 | 
			
		||||
        sens = await sensor.new_sensor(angle_config)
 | 
			
		||||
        cg.add(var.set_angle_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if raw_angle_config := config.get(CONF_RAW_ANGLE):
 | 
			
		||||
        sens = await sensor.new_sensor(raw_angle_config)
 | 
			
		||||
        cg.add(var.set_raw_angle_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if position_config := config.get(CONF_POSITION):
 | 
			
		||||
        sens = await sensor.new_sensor(position_config)
 | 
			
		||||
        cg.add(var.set_position_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if raw_position_config := config.get(CONF_RAW_POSITION):
 | 
			
		||||
        sens = await sensor.new_sensor(raw_position_config)
 | 
			
		||||
        cg.add(var.set_raw_position_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if gain_config := config.get(CONF_GAIN):
 | 
			
		||||
        sens = await sensor.new_sensor(gain_config)
 | 
			
		||||
        cg.add(var.set_gain_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if magnitude_config := config.get(CONF_MAGNITUDE):
 | 
			
		||||
        sens = await sensor.new_sensor(magnitude_config)
 | 
			
		||||
        cg.add(var.set_magnitude_sensor(sens))
 | 
			
		||||
 | 
			
		||||
    if status_config := config.get(CONF_STATUS):
 | 
			
		||||
        sens = await sensor.new_sensor(status_config)
 | 
			
		||||
        cg.add(var.set_status_sensor(sens))
 | 
			
		||||
							
								
								
									
										98
									
								
								esphome/components/as5600/sensor/as5600_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										98
									
								
								esphome/components/as5600/sensor/as5600_sensor.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,98 @@
 | 
			
		||||
#include "as5600_sensor.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace as5600 {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "as5600.sensor";
 | 
			
		||||
 | 
			
		||||
// Configuration registers
 | 
			
		||||
static const uint8_t REGISTER_ZMCO = 0x00;  // 8 bytes  / R
 | 
			
		||||
static const uint8_t REGISTER_ZPOS = 0x01;  // 16 bytes / RW
 | 
			
		||||
static const uint8_t REGISTER_MPOS = 0x03;  // 16 bytes / RW
 | 
			
		||||
static const uint8_t REGISTER_MANG = 0x05;  // 16 bytes / RW
 | 
			
		||||
static const uint8_t REGISTER_CONF = 0x07;  // 16 bytes / RW
 | 
			
		||||
 | 
			
		||||
// Output registers
 | 
			
		||||
static const uint8_t REGISTER_ANGLE_RAW = 0x0C;  // 16 bytes / R
 | 
			
		||||
static const uint8_t REGISTER_ANGLE = 0x0E;      // 16 bytes / R
 | 
			
		||||
 | 
			
		||||
// Status registers
 | 
			
		||||
static const uint8_t REGISTER_STATUS = 0x0B;     // 8 bytes  / R
 | 
			
		||||
static const uint8_t REGISTER_AGC = 0x1A;        // 8 bytes  / R
 | 
			
		||||
static const uint8_t REGISTER_MAGNITUDE = 0x1B;  // 16 bytes / R
 | 
			
		||||
 | 
			
		||||
float AS5600Sensor::get_setup_priority() const { return setup_priority::DATA; }
 | 
			
		||||
 | 
			
		||||
void AS5600Sensor::dump_config() {
 | 
			
		||||
  LOG_SENSOR("", "AS5600 Sensor", this);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Out of Range Mode: %u", this->out_of_range_mode_);
 | 
			
		||||
  if (this->angle_sensor_ != nullptr) {
 | 
			
		||||
    LOG_SENSOR("  ", "Angle Sensor", this->angle_sensor_);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->raw_angle_sensor_ != nullptr) {
 | 
			
		||||
    LOG_SENSOR("  ", "Raw Angle Sensor", this->raw_angle_sensor_);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->position_sensor_ != nullptr) {
 | 
			
		||||
    LOG_SENSOR("  ", "Position Sensor", this->position_sensor_);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->raw_position_sensor_ != nullptr) {
 | 
			
		||||
    LOG_SENSOR("  ", "Raw Position Sensor", this->raw_position_sensor_);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->gain_sensor_ != nullptr) {
 | 
			
		||||
    LOG_SENSOR("  ", "Gain Sensor", this->gain_sensor_);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->magnitude_sensor_ != nullptr) {
 | 
			
		||||
    LOG_SENSOR("  ", "Magnitude Sensor", this->magnitude_sensor_);
 | 
			
		||||
  }
 | 
			
		||||
  if (this->status_sensor_ != nullptr) {
 | 
			
		||||
    LOG_SENSOR("  ", "Status Sensor", this->status_sensor_);
 | 
			
		||||
  }
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void AS5600Sensor::update() {
 | 
			
		||||
  if (this->gain_sensor_ != nullptr) {
 | 
			
		||||
    this->gain_sensor_->publish_state(this->parent_->reg(REGISTER_AGC).get());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->magnitude_sensor_ != nullptr) {
 | 
			
		||||
    uint16_t value = 0;
 | 
			
		||||
    this->parent_->read_byte_16(REGISTER_MAGNITUDE, &value);
 | 
			
		||||
    this->magnitude_sensor_->publish_state(value);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // 2 = magnet not detected
 | 
			
		||||
  // 4 = magnet just right
 | 
			
		||||
  // 5 = magnet too strong
 | 
			
		||||
  // 6 = magnet too weak
 | 
			
		||||
  if (this->status_sensor_ != nullptr) {
 | 
			
		||||
    this->status_sensor_->publish_state(this->parent_->read_magnet_status());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto pos = this->parent_->read_position();
 | 
			
		||||
  if (!pos.has_value()) {
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto raw = this->parent_->read_raw_position();
 | 
			
		||||
  if (!raw.has_value()) {
 | 
			
		||||
    this->status_set_warning();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->out_of_range_mode_ == OUT_RANGE_MODE_NAN) {
 | 
			
		||||
    this->publish_state(this->parent_->in_range(raw.value()) ? pos.value() : NAN);
 | 
			
		||||
  } else {
 | 
			
		||||
    this->publish_state(pos.value());
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->raw_position_sensor_ != nullptr) {
 | 
			
		||||
    this->raw_position_sensor_->publish_state(raw.value());
 | 
			
		||||
  }
 | 
			
		||||
  this->status_clear_warning();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace as5600
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										43
									
								
								esphome/components/as5600/sensor/as5600_sensor.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										43
									
								
								esphome/components/as5600/sensor/as5600_sensor.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,43 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/preferences.h"
 | 
			
		||||
#include "esphome/components/i2c/i2c.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/as5600/as5600.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace as5600 {
 | 
			
		||||
 | 
			
		||||
class AS5600Sensor : public PollingComponent, public Parented<AS5600Component>, public sensor::Sensor {
 | 
			
		||||
 public:
 | 
			
		||||
  void update() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
 | 
			
		||||
  void set_angle_sensor(sensor::Sensor *angle_sensor) { this->angle_sensor_ = angle_sensor; }
 | 
			
		||||
  void set_raw_angle_sensor(sensor::Sensor *raw_angle_sensor) { this->raw_angle_sensor_ = raw_angle_sensor; }
 | 
			
		||||
  void set_position_sensor(sensor::Sensor *position_sensor) { this->position_sensor_ = position_sensor; }
 | 
			
		||||
  void set_raw_position_sensor(sensor::Sensor *raw_position_sensor) {
 | 
			
		||||
    this->raw_position_sensor_ = raw_position_sensor;
 | 
			
		||||
  }
 | 
			
		||||
  void set_gain_sensor(sensor::Sensor *gain_sensor) { this->gain_sensor_ = gain_sensor; }
 | 
			
		||||
  void set_magnitude_sensor(sensor::Sensor *magnitude_sensor) { this->magnitude_sensor_ = magnitude_sensor; }
 | 
			
		||||
  void set_status_sensor(sensor::Sensor *status_sensor) { this->status_sensor_ = status_sensor; }
 | 
			
		||||
  void set_out_of_range_mode(OutRangeMode oor_mode) { this->out_of_range_mode_ = oor_mode; }
 | 
			
		||||
  OutRangeMode get_out_of_range_mode() { return this->out_of_range_mode_; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  sensor::Sensor *angle_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *raw_angle_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *position_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *raw_position_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *gain_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *magnitude_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *status_sensor_{nullptr};
 | 
			
		||||
  OutRangeMode out_of_range_mode_{OUT_RANGE_MODE_MIN_MAX};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace as5600
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -242,7 +242,7 @@ void BedJetHub::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
 | 
			
		||||
        this->set_notify_(true);
 | 
			
		||||
 | 
			
		||||
#ifdef USE_TIME
 | 
			
		||||
        if (this->time_id_.has_value()) {
 | 
			
		||||
        if (this->time_id_ != nullptr) {
 | 
			
		||||
          this->send_local_time();
 | 
			
		||||
        }
 | 
			
		||||
#endif
 | 
			
		||||
@@ -441,9 +441,8 @@ uint8_t BedJetHub::write_notify_config_descriptor_(bool enable) {
 | 
			
		||||
 | 
			
		||||
#ifdef USE_TIME
 | 
			
		||||
void BedJetHub::send_local_time() {
 | 
			
		||||
  if (this->time_id_.has_value()) {
 | 
			
		||||
    auto *time_id = *this->time_id_;
 | 
			
		||||
    ESPTime now = time_id->now();
 | 
			
		||||
  if (this->time_id_ != nullptr) {
 | 
			
		||||
    ESPTime now = this->time_id_->now();
 | 
			
		||||
    if (now.is_valid()) {
 | 
			
		||||
      this->set_clock(now.hour, now.minute);
 | 
			
		||||
      ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute);
 | 
			
		||||
@@ -454,10 +453,9 @@ void BedJetHub::send_local_time() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BedJetHub::setup_time_() {
 | 
			
		||||
  if (this->time_id_.has_value()) {
 | 
			
		||||
  if (this->time_id_ != nullptr) {
 | 
			
		||||
    this->send_local_time();
 | 
			
		||||
    auto *time_id = *this->time_id_;
 | 
			
		||||
    time_id->add_on_time_sync_callback([this] { this->send_local_time(); });
 | 
			
		||||
    this->time_id_->add_on_time_sync_callback([this] { this->send_local_time(); });
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGI(TAG, "`time_id` is not configured: will not sync BedJet clock.");
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -141,7 +141,7 @@ class BedJetHub : public esphome::ble_client::BLEClientNode, public PollingCompo
 | 
			
		||||
#ifdef USE_TIME
 | 
			
		||||
  /** Initializes time sync callbacks to support syncing current time to the BedJet. */
 | 
			
		||||
  void setup_time_();
 | 
			
		||||
  optional<time::RealTimeClock *> time_id_{};
 | 
			
		||||
  time::RealTimeClock *time_id_{nullptr};
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  uint32_t timeout_{DEFAULT_STATUS_TIMEOUT};
 | 
			
		||||
 
 | 
			
		||||
@@ -141,6 +141,7 @@ DelayedOffFilter = binary_sensor_ns.class_("DelayedOffFilter", Filter, cg.Compon
 | 
			
		||||
InvertFilter = binary_sensor_ns.class_("InvertFilter", Filter)
 | 
			
		||||
AutorepeatFilter = binary_sensor_ns.class_("AutorepeatFilter", Filter, cg.Component)
 | 
			
		||||
LambdaFilter = binary_sensor_ns.class_("LambdaFilter", Filter)
 | 
			
		||||
SettleFilter = binary_sensor_ns.class_("SettleFilter", Filter, cg.Component)
 | 
			
		||||
 | 
			
		||||
FILTER_REGISTRY = Registry()
 | 
			
		||||
validate_filters = cv.validate_registry("filter", FILTER_REGISTRY)
 | 
			
		||||
@@ -259,6 +260,19 @@ async def lambda_filter_to_code(config, filter_id):
 | 
			
		||||
    return cg.new_Pvariable(filter_id, lambda_)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@register_filter(
 | 
			
		||||
    "settle",
 | 
			
		||||
    SettleFilter,
 | 
			
		||||
    cv.templatable(cv.positive_time_period_milliseconds),
 | 
			
		||||
)
 | 
			
		||||
async def settle_filter_to_code(config, filter_id):
 | 
			
		||||
    var = cg.new_Pvariable(filter_id)
 | 
			
		||||
    await cg.register_component(var, {})
 | 
			
		||||
    template_ = await cg.templatable(config, [], cg.uint32)
 | 
			
		||||
    cg.add(var.set_delay(template_))
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
MULTI_CLICK_TIMING_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Optional(CONF_STATE): cv.boolean,
 | 
			
		||||
 
 | 
			
		||||
@@ -111,6 +111,23 @@ LambdaFilter::LambdaFilter(std::function<optional<bool>(bool)> f) : f_(std::move
 | 
			
		||||
 | 
			
		||||
optional<bool> LambdaFilter::new_value(bool value, bool is_initial) { return this->f_(value); }
 | 
			
		||||
 | 
			
		||||
optional<bool> SettleFilter::new_value(bool value, bool is_initial) {
 | 
			
		||||
  if (!this->steady_) {
 | 
			
		||||
    this->set_timeout("SETTLE", this->delay_.value(), [this, value, is_initial]() {
 | 
			
		||||
      this->steady_ = true;
 | 
			
		||||
      this->output(value, is_initial);
 | 
			
		||||
    });
 | 
			
		||||
    return {};
 | 
			
		||||
  } else {
 | 
			
		||||
    this->steady_ = false;
 | 
			
		||||
    this->output(value, is_initial);
 | 
			
		||||
    this->set_timeout("SETTLE", this->delay_.value(), [this]() { this->steady_ = true; });
 | 
			
		||||
    return value;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float SettleFilter::get_setup_priority() const { return setup_priority::HARDWARE; }
 | 
			
		||||
 | 
			
		||||
}  // namespace binary_sensor
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -108,6 +108,19 @@ class LambdaFilter : public Filter {
 | 
			
		||||
  std::function<optional<bool>(bool)> f_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class SettleFilter : public Filter, public Component {
 | 
			
		||||
 public:
 | 
			
		||||
  optional<bool> new_value(bool value, bool is_initial) override;
 | 
			
		||||
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
 | 
			
		||||
  template<typename T> void set_delay(T delay) { this->delay_ = delay; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  TemplatableValue<uint32_t> delay_{};
 | 
			
		||||
  bool steady_{true};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace binary_sensor
 | 
			
		||||
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ from esphome.const import (
 | 
			
		||||
    UNIT_KILOWATT_HOURS,
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
    UNIT_WATT,
 | 
			
		||||
    STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["uart"]
 | 
			
		||||
@@ -54,6 +55,7 @@ CONFIG_SCHEMA = (
 | 
			
		||||
                unit_of_measurement=UNIT_KILOWATT_HOURS,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
                state_class=STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_INTERNAL_TEMPERATURE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_CELSIUS,
 | 
			
		||||
 
 | 
			
		||||
@@ -19,6 +19,7 @@ from esphome.const import (
 | 
			
		||||
    UNIT_VOLT,
 | 
			
		||||
    UNIT_WATT,
 | 
			
		||||
    UNIT_HERTZ,
 | 
			
		||||
    STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["uart"]
 | 
			
		||||
@@ -52,6 +53,7 @@ CONFIG_SCHEMA = (
 | 
			
		||||
                unit_of_measurement=UNIT_KILOWATT_HOURS,
 | 
			
		||||
                accuracy_decimals=0,
 | 
			
		||||
                device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
                state_class=STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_FREQUENCY): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_HERTZ,
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,6 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.automation import maybe_simple_id
 | 
			
		||||
from esphome.components import esp32_ble_tracker, esp32_ble_client
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_CHARACTERISTIC_UUID,
 | 
			
		||||
@@ -15,7 +16,7 @@ from esphome.const import (
 | 
			
		||||
from esphome import automation
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["esp32_ble_client"]
 | 
			
		||||
CODEOWNERS = ["@buxtronix"]
 | 
			
		||||
CODEOWNERS = ["@buxtronix", "@clydebarrow"]
 | 
			
		||||
DEPENDENCIES = ["esp32_ble_tracker"]
 | 
			
		||||
 | 
			
		||||
ble_client_ns = cg.esphome_ns.namespace("ble_client")
 | 
			
		||||
@@ -43,6 +44,10 @@ BLEClientNumericComparisonRequestTrigger = ble_client_ns.class_(
 | 
			
		||||
 | 
			
		||||
# Actions
 | 
			
		||||
BLEWriteAction = ble_client_ns.class_("BLEClientWriteAction", automation.Action)
 | 
			
		||||
BLEConnectAction = ble_client_ns.class_("BLEClientConnectAction", automation.Action)
 | 
			
		||||
BLEDisconnectAction = ble_client_ns.class_(
 | 
			
		||||
    "BLEClientDisconnectAction", automation.Action
 | 
			
		||||
)
 | 
			
		||||
BLEPasskeyReplyAction = ble_client_ns.class_(
 | 
			
		||||
    "BLEClientPasskeyReplyAction", automation.Action
 | 
			
		||||
)
 | 
			
		||||
@@ -58,6 +63,7 @@ CONF_ACCEPT = "accept"
 | 
			
		||||
CONF_ON_PASSKEY_REQUEST = "on_passkey_request"
 | 
			
		||||
CONF_ON_PASSKEY_NOTIFICATION = "on_passkey_notification"
 | 
			
		||||
CONF_ON_NUMERIC_COMPARISON_REQUEST = "on_numeric_comparison_request"
 | 
			
		||||
CONF_AUTO_CONNECT = "auto_connect"
 | 
			
		||||
 | 
			
		||||
# Espressif platformio framework is built with MAX_BLE_CONN to 3, so
 | 
			
		||||
# enforce this in yaml checks.
 | 
			
		||||
@@ -69,6 +75,7 @@ CONFIG_SCHEMA = (
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(BLEClient),
 | 
			
		||||
            cv.Required(CONF_MAC_ADDRESS): cv.mac_address,
 | 
			
		||||
            cv.Optional(CONF_NAME): cv.string,
 | 
			
		||||
            cv.Optional(CONF_AUTO_CONNECT, default=True): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_ON_CONNECT): automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
@@ -135,6 +142,12 @@ BLE_WRITE_ACTION_SCHEMA = cv.Schema(
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
BLE_CONNECT_ACTION_SCHEMA = maybe_simple_id(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
BLE_NUMERIC_COMPARISON_REPLY_ACTION_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(CONF_ID): cv.use_id(BLEClient),
 | 
			
		||||
@@ -157,6 +170,24 @@ BLE_REMOVE_BOND_ACTION_SCHEMA = cv.Schema(
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "ble_client.disconnect", BLEDisconnectAction, BLE_CONNECT_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
async def ble_disconnect_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    parent = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, parent)
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "ble_client.connect", BLEConnectAction, BLE_CONNECT_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
async def ble_connect_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    parent = await cg.get_variable(config[CONF_ID])
 | 
			
		||||
    var = cg.new_Pvariable(action_id, template_arg, parent)
 | 
			
		||||
    return var
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
@automation.register_action(
 | 
			
		||||
    "ble_client.ble_write", BLEWriteAction, BLE_WRITE_ACTION_SCHEMA
 | 
			
		||||
)
 | 
			
		||||
@@ -261,6 +292,7 @@ async def to_code(config):
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await esp32_ble_tracker.register_client(var, config)
 | 
			
		||||
    cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex))
 | 
			
		||||
    cg.add(var.set_auto_connect(config[CONF_AUTO_CONNECT]))
 | 
			
		||||
    for conf in config.get(CONF_ON_CONNECT, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await automation.build_automation(trigger, [], conf)
 | 
			
		||||
 
 | 
			
		||||
@@ -2,76 +2,10 @@
 | 
			
		||||
 | 
			
		||||
#include "automation.h"
 | 
			
		||||
 | 
			
		||||
#include <esp_bt_defs.h>
 | 
			
		||||
#include <esp_gap_ble_api.h>
 | 
			
		||||
#include <esp_gattc_api.h>
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ble_client {
 | 
			
		||||
static const char *const TAG = "ble_client.automation";
 | 
			
		||||
 | 
			
		||||
void BLEWriterClientNode::write(const std::vector<uint8_t> &value) {
 | 
			
		||||
  if (this->node_state != espbt::ClientState::ESTABLISHED) {
 | 
			
		||||
    ESP_LOGW(TAG, "Cannot write to BLE characteristic - not connected");
 | 
			
		||||
    return;
 | 
			
		||||
  } else if (this->ble_char_handle_ == 0) {
 | 
			
		||||
    ESP_LOGW(TAG, "Cannot write to BLE characteristic - characteristic not found");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  esp_gatt_write_type_t write_type;
 | 
			
		||||
  if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) {
 | 
			
		||||
    write_type = ESP_GATT_WRITE_TYPE_RSP;
 | 
			
		||||
    ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP");
 | 
			
		||||
  } else if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) {
 | 
			
		||||
    write_type = ESP_GATT_WRITE_TYPE_NO_RSP;
 | 
			
		||||
    ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP");
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGE(TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_string().c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
  ESP_LOGVV(TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str());
 | 
			
		||||
  esp_err_t err =
 | 
			
		||||
      esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->ble_char_handle_,
 | 
			
		||||
                               value.size(), const_cast<uint8_t *>(value.data()), write_type, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
  if (err != ESP_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "Error writing to characteristic: %s!", esp_err_to_name(err));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEWriterClientNode::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                                              esp_ble_gattc_cb_param_t *param) {
 | 
			
		||||
  switch (event) {
 | 
			
		||||
    case ESP_GATTC_REG_EVT:
 | 
			
		||||
      break;
 | 
			
		||||
    case ESP_GATTC_OPEN_EVT:
 | 
			
		||||
      this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
      ESP_LOGD(TAG, "Connection established with %s", ble_client_->address_str().c_str());
 | 
			
		||||
      break;
 | 
			
		||||
    case ESP_GATTC_SEARCH_CMPL_EVT: {
 | 
			
		||||
      auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_);
 | 
			
		||||
      if (chr == nullptr) {
 | 
			
		||||
        ESP_LOGW("ble_write_action", "Characteristic %s was not found in service %s",
 | 
			
		||||
                 this->char_uuid_.to_string().c_str(), this->service_uuid_.to_string().c_str());
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      this->ble_char_handle_ = chr->handle;
 | 
			
		||||
      this->char_props_ = chr->properties;
 | 
			
		||||
      this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
      ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(),
 | 
			
		||||
               ble_client_->address_str().c_str());
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_DISCONNECT_EVT:
 | 
			
		||||
      this->node_state = espbt::ClientState::IDLE;
 | 
			
		||||
      this->ble_char_handle_ = 0;
 | 
			
		||||
      ESP_LOGD(TAG, "Disconnected from %s", ble_client_->address_str().c_str());
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
const char *const Automation::TAG = "ble_client.automation";
 | 
			
		||||
 | 
			
		||||
}  // namespace ble_client
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -7,9 +7,19 @@
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/components/ble_client/ble_client.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ble_client {
 | 
			
		||||
 | 
			
		||||
// placeholder class for static TAG .
 | 
			
		||||
class Automation {
 | 
			
		||||
 public:
 | 
			
		||||
  // could be made inline with C++17
 | 
			
		||||
  static const char *const TAG;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// implement on_connect automation.
 | 
			
		||||
class BLEClientConnectTrigger : public Trigger<>, public BLEClientNode {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit BLEClientConnectTrigger(BLEClient *parent) { parent->register_ble_node(this); }
 | 
			
		||||
@@ -23,17 +33,28 @@ class BLEClientConnectTrigger : public Trigger<>, public BLEClientNode {
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// on_disconnect automation
 | 
			
		||||
class BLEClientDisconnectTrigger : public Trigger<>, public BLEClientNode {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit BLEClientDisconnectTrigger(BLEClient *parent) { parent->register_ble_node(this); }
 | 
			
		||||
  void loop() override {}
 | 
			
		||||
  void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                           esp_ble_gattc_cb_param_t *param) override {
 | 
			
		||||
    if (event == ESP_GATTC_DISCONNECT_EVT &&
 | 
			
		||||
        memcmp(param->disconnect.remote_bda, this->parent_->get_remote_bda(), 6) == 0)
 | 
			
		||||
      this->trigger();
 | 
			
		||||
    if (event == ESP_GATTC_SEARCH_CMPL_EVT)
 | 
			
		||||
      this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
    // test for CLOSE and not DISCONNECT - DISCONNECT can occur even if no virtual connection (OPEN event) occurred.
 | 
			
		||||
    // So this will not trigger unless a complete open has previously succeeded.
 | 
			
		||||
    switch (event) {
 | 
			
		||||
      case ESP_GATTC_SEARCH_CMPL_EVT: {
 | 
			
		||||
        this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      case ESP_GATTC_CLOSE_EVT: {
 | 
			
		||||
        this->trigger();
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      default: {
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -42,10 +63,8 @@ class BLEClientPasskeyRequestTrigger : public Trigger<>, public BLEClientNode {
 | 
			
		||||
  explicit BLEClientPasskeyRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); }
 | 
			
		||||
  void loop() override {}
 | 
			
		||||
  void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override {
 | 
			
		||||
    if (event == ESP_GAP_BLE_PASSKEY_REQ_EVT &&
 | 
			
		||||
        memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) {
 | 
			
		||||
    if (event == ESP_GAP_BLE_PASSKEY_REQ_EVT && this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr))
 | 
			
		||||
      this->trigger();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
@@ -54,10 +73,8 @@ class BLEClientPasskeyNotificationTrigger : public Trigger<uint32_t>, public BLE
 | 
			
		||||
  explicit BLEClientPasskeyNotificationTrigger(BLEClient *parent) { parent->register_ble_node(this); }
 | 
			
		||||
  void loop() override {}
 | 
			
		||||
  void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override {
 | 
			
		||||
    if (event == ESP_GAP_BLE_PASSKEY_NOTIF_EVT &&
 | 
			
		||||
        memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) {
 | 
			
		||||
      uint32_t passkey = param->ble_security.key_notif.passkey;
 | 
			
		||||
      this->trigger(passkey);
 | 
			
		||||
    if (event == ESP_GAP_BLE_PASSKEY_NOTIF_EVT && this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr)) {
 | 
			
		||||
      this->trigger(param->ble_security.key_notif.passkey);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
@@ -67,24 +84,20 @@ class BLEClientNumericComparisonRequestTrigger : public Trigger<uint32_t>, publi
 | 
			
		||||
  explicit BLEClientNumericComparisonRequestTrigger(BLEClient *parent) { parent->register_ble_node(this); }
 | 
			
		||||
  void loop() override {}
 | 
			
		||||
  void gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) override {
 | 
			
		||||
    if (event == ESP_GAP_BLE_NC_REQ_EVT &&
 | 
			
		||||
        memcmp(param->ble_security.auth_cmpl.bd_addr, this->parent_->get_remote_bda(), 6) == 0) {
 | 
			
		||||
      uint32_t passkey = param->ble_security.key_notif.passkey;
 | 
			
		||||
      this->trigger(passkey);
 | 
			
		||||
    if (event == ESP_GAP_BLE_NC_REQ_EVT && this->parent_->check_addr(param->ble_security.auth_cmpl.bd_addr)) {
 | 
			
		||||
      this->trigger(param->ble_security.key_notif.passkey);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class BLEWriterClientNode : public BLEClientNode {
 | 
			
		||||
// implement the ble_client.ble_write action.
 | 
			
		||||
template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, public BLEClientNode {
 | 
			
		||||
 public:
 | 
			
		||||
  BLEWriterClientNode(BLEClient *ble_client) {
 | 
			
		||||
  BLEClientWriteAction(BLEClient *ble_client) {
 | 
			
		||||
    ble_client->register_ble_node(this);
 | 
			
		||||
    ble_client_ = ble_client;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Attempts to write the contents of value to char_uuid_.
 | 
			
		||||
  void write(const std::vector<uint8_t> &value);
 | 
			
		||||
 | 
			
		||||
  void set_service_uuid16(uint16_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint16(uuid); }
 | 
			
		||||
  void set_service_uuid32(uint32_t uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
 | 
			
		||||
  void set_service_uuid128(uint8_t *uuid) { this->service_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
 | 
			
		||||
@@ -93,29 +106,6 @@ class BLEWriterClientNode : public BLEClientNode {
 | 
			
		||||
  void set_char_uuid32(uint32_t uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_uint32(uuid); }
 | 
			
		||||
  void set_char_uuid128(uint8_t *uuid) { this->char_uuid_ = espbt::ESPBTUUID::from_raw(uuid); }
 | 
			
		||||
 | 
			
		||||
  void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                           esp_ble_gattc_cb_param_t *param) override;
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  BLEClient *ble_client_;
 | 
			
		||||
  int ble_char_handle_ = 0;
 | 
			
		||||
  esp_gatt_char_prop_t char_props_;
 | 
			
		||||
  espbt::ESPBTUUID service_uuid_;
 | 
			
		||||
  espbt::ESPBTUUID char_uuid_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, public BLEWriterClientNode {
 | 
			
		||||
 public:
 | 
			
		||||
  BLEClientWriteAction(BLEClient *ble_client) : BLEWriterClientNode(ble_client) {}
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override {
 | 
			
		||||
    if (has_simple_value_) {
 | 
			
		||||
      return write(this->value_simple_);
 | 
			
		||||
    } else {
 | 
			
		||||
      return write(this->value_template_(x...));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void set_value_template(std::function<std::vector<uint8_t>(Ts...)> func) {
 | 
			
		||||
    this->value_template_ = std::move(func);
 | 
			
		||||
    has_simple_value_ = false;
 | 
			
		||||
@@ -126,10 +116,94 @@ template<typename... Ts> class BLEClientWriteAction : public Action<Ts...>, publ
 | 
			
		||||
    has_simple_value_ = true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void play(Ts... x) override {}
 | 
			
		||||
 | 
			
		||||
  void play_complex(Ts... x) override {
 | 
			
		||||
    this->num_running_++;
 | 
			
		||||
    this->var_ = std::make_tuple(x...);
 | 
			
		||||
    auto value = this->has_simple_value_ ? this->value_simple_ : this->value_template_(x...);
 | 
			
		||||
    // on write failure, continue the automation chain rather than stopping so that e.g. disconnect can work.
 | 
			
		||||
    if (!write(value))
 | 
			
		||||
      this->play_next_(x...);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * Note about logging: the esph_log_X macros are used here because the CI checks complain about use of the ESP LOG
 | 
			
		||||
   * macros in header files (Can't even write it in a comment!)
 | 
			
		||||
   * Not sure why, because they seem to work just fine.
 | 
			
		||||
   * The problem is that the implementation of a templated class can't be placed in a .cpp file when using C++ less than
 | 
			
		||||
   * 17, so the methods have to be here.  The esph_log_X macros are equivalent in function, but don't trigger the CI
 | 
			
		||||
   * errors.
 | 
			
		||||
   */
 | 
			
		||||
  // initiate the write. Return true if all went well, will be followed by a WRITE_CHAR event.
 | 
			
		||||
  bool write(const std::vector<uint8_t> &value) {
 | 
			
		||||
    if (this->node_state != espbt::ClientState::ESTABLISHED) {
 | 
			
		||||
      esph_log_w(Automation::TAG, "Cannot write to BLE characteristic - not connected");
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    esph_log_vv(Automation::TAG, "Will write %d bytes: %s", value.size(), format_hex_pretty(value).c_str());
 | 
			
		||||
    esp_err_t err = esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(),
 | 
			
		||||
                                             this->char_handle_, value.size(), const_cast<uint8_t *>(value.data()),
 | 
			
		||||
                                             this->write_type_, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
    if (err != ESP_OK) {
 | 
			
		||||
      esph_log_e(Automation::TAG, "Error writing to characteristic: %s!", esp_err_to_name(err));
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
    return true;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                           esp_ble_gattc_cb_param_t *param) override {
 | 
			
		||||
    switch (event) {
 | 
			
		||||
      case ESP_GATTC_WRITE_CHAR_EVT:
 | 
			
		||||
        // upstream code checked the MAC address, verify the characteristic.
 | 
			
		||||
        if (param->write.handle == this->char_handle_)
 | 
			
		||||
          this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); });
 | 
			
		||||
        break;
 | 
			
		||||
      case ESP_GATTC_DISCONNECT_EVT:
 | 
			
		||||
        if (this->num_running_ != 0)
 | 
			
		||||
          this->stop_complex();
 | 
			
		||||
        break;
 | 
			
		||||
      case ESP_GATTC_SEARCH_CMPL_EVT: {
 | 
			
		||||
        auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_);
 | 
			
		||||
        if (chr == nullptr) {
 | 
			
		||||
          esph_log_w("ble_write_action", "Characteristic %s was not found in service %s",
 | 
			
		||||
                     this->char_uuid_.to_string().c_str(), this->service_uuid_.to_string().c_str());
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
        this->char_handle_ = chr->handle;
 | 
			
		||||
        this->char_props_ = chr->properties;
 | 
			
		||||
        if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) {
 | 
			
		||||
          this->write_type_ = ESP_GATT_WRITE_TYPE_RSP;
 | 
			
		||||
          esph_log_d(Automation::TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP");
 | 
			
		||||
        } else if (this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) {
 | 
			
		||||
          this->write_type_ = ESP_GATT_WRITE_TYPE_NO_RSP;
 | 
			
		||||
          esph_log_d(Automation::TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP");
 | 
			
		||||
        } else {
 | 
			
		||||
          esph_log_e(Automation::TAG, "Characteristic %s does not allow writing", this->char_uuid_.to_string().c_str());
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
        this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
        esph_log_d(Automation::TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(),
 | 
			
		||||
                   ble_client_->address_str().c_str());
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      default:
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  BLEClient *ble_client_;
 | 
			
		||||
  bool has_simple_value_ = true;
 | 
			
		||||
  std::vector<uint8_t> value_simple_;
 | 
			
		||||
  std::function<std::vector<uint8_t>(Ts...)> value_template_{};
 | 
			
		||||
  espbt::ESPBTUUID service_uuid_;
 | 
			
		||||
  espbt::ESPBTUUID char_uuid_;
 | 
			
		||||
  std::tuple<Ts...> var_{};
 | 
			
		||||
  uint16_t char_handle_{};
 | 
			
		||||
  esp_gatt_char_prop_t char_props_{};
 | 
			
		||||
  esp_gatt_write_type_t write_type_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class BLEClientPasskeyReplyAction : public Action<Ts...> {
 | 
			
		||||
@@ -212,6 +286,92 @@ template<typename... Ts> class BLEClientRemoveBondAction : public Action<Ts...>
 | 
			
		||||
  BLEClient *parent_{nullptr};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class BLEClientConnectAction : public Action<Ts...>, public BLEClientNode {
 | 
			
		||||
 public:
 | 
			
		||||
  BLEClientConnectAction(BLEClient *ble_client) {
 | 
			
		||||
    ble_client->register_ble_node(this);
 | 
			
		||||
    ble_client_ = ble_client;
 | 
			
		||||
  }
 | 
			
		||||
  void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                           esp_ble_gattc_cb_param_t *param) override {
 | 
			
		||||
    if (this->num_running_ == 0)
 | 
			
		||||
      return;
 | 
			
		||||
    switch (event) {
 | 
			
		||||
      case ESP_GATTC_SEARCH_CMPL_EVT:
 | 
			
		||||
        this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
        this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); });
 | 
			
		||||
        break;
 | 
			
		||||
      // if the connection is closed, terminate the automation chain.
 | 
			
		||||
      case ESP_GATTC_DISCONNECT_EVT:
 | 
			
		||||
        this->stop_complex();
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // not used since we override play_complex_
 | 
			
		||||
  void play(Ts... x) override {}
 | 
			
		||||
 | 
			
		||||
  void play_complex(Ts... x) override {
 | 
			
		||||
    // it makes no sense to have multiple instances of this running at the same time.
 | 
			
		||||
    // this would occur only if the same automation was re-triggered while still
 | 
			
		||||
    // running. So just cancel the second chain if this is detected.
 | 
			
		||||
    if (this->num_running_ != 0) {
 | 
			
		||||
      this->stop_complex();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    this->num_running_++;
 | 
			
		||||
    if (this->node_state == espbt::ClientState::ESTABLISHED) {
 | 
			
		||||
      this->play_next_(x...);
 | 
			
		||||
    } else {
 | 
			
		||||
      this->var_ = std::make_tuple(x...);
 | 
			
		||||
      this->ble_client_->connect();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  BLEClient *ble_client_;
 | 
			
		||||
  std::tuple<Ts...> var_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
template<typename... Ts> class BLEClientDisconnectAction : public Action<Ts...>, public BLEClientNode {
 | 
			
		||||
 public:
 | 
			
		||||
  BLEClientDisconnectAction(BLEClient *ble_client) {
 | 
			
		||||
    ble_client->register_ble_node(this);
 | 
			
		||||
    ble_client_ = ble_client;
 | 
			
		||||
  }
 | 
			
		||||
  void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                           esp_ble_gattc_cb_param_t *param) override {
 | 
			
		||||
    if (this->num_running_ == 0)
 | 
			
		||||
      return;
 | 
			
		||||
    switch (event) {
 | 
			
		||||
      case ESP_GATTC_CLOSE_EVT:
 | 
			
		||||
      case ESP_GATTC_DISCONNECT_EVT:
 | 
			
		||||
        this->parent()->run_later([this]() { this->play_next_tuple_(this->var_); });
 | 
			
		||||
        break;
 | 
			
		||||
      default:
 | 
			
		||||
        break;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // not used since we override play_complex_
 | 
			
		||||
  void play(Ts... x) override {}
 | 
			
		||||
 | 
			
		||||
  void play_complex(Ts... x) override {
 | 
			
		||||
    this->num_running_++;
 | 
			
		||||
    if (this->node_state == espbt::ClientState::IDLE) {
 | 
			
		||||
      this->play_next_(x...);
 | 
			
		||||
    } else {
 | 
			
		||||
      this->var_ = std::make_tuple(x...);
 | 
			
		||||
      this->ble_client_->disconnect();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  BLEClient *ble_client_;
 | 
			
		||||
  std::tuple<Ts...> var_{};
 | 
			
		||||
};
 | 
			
		||||
}  // namespace ble_client
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,7 @@ void BLEClient::loop() {
 | 
			
		||||
void BLEClient::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "BLE Client:");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Address: %s", this->address_str().c_str());
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Auto-Connect: %s", TRUEFALSE(this->auto_connect_));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool BLEClient::parse_device(const espbt::ESPBTDevice &device) {
 | 
			
		||||
@@ -37,31 +38,24 @@ bool BLEClient::parse_device(const espbt::ESPBTDevice &device) {
 | 
			
		||||
void BLEClient::set_enabled(bool enabled) {
 | 
			
		||||
  if (enabled == this->enabled)
 | 
			
		||||
    return;
 | 
			
		||||
  if (!enabled && this->state() != espbt::ClientState::IDLE) {
 | 
			
		||||
    ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str());
 | 
			
		||||
    auto ret = esp_ble_gattc_close(this->gattc_if_, this->conn_id_);
 | 
			
		||||
    if (ret) {
 | 
			
		||||
      ESP_LOGW(TAG, "esp_ble_gattc_close error, address=%s status=%d", this->address_str().c_str(), ret);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  this->enabled = enabled;
 | 
			
		||||
  if (!enabled) {
 | 
			
		||||
    ESP_LOGI(TAG, "[%s] Disabling BLE client.", this->address_str().c_str());
 | 
			
		||||
    this->disconnect();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool BLEClient::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t esp_gattc_if,
 | 
			
		||||
                                    esp_ble_gattc_cb_param_t *param) {
 | 
			
		||||
  bool all_established = this->all_nodes_established_();
 | 
			
		||||
 | 
			
		||||
  if (!BLEClientBase::gattc_event_handler(event, esp_gattc_if, param))
 | 
			
		||||
    return false;
 | 
			
		||||
 | 
			
		||||
  for (auto *node : this->nodes_)
 | 
			
		||||
    node->gattc_event_handler(event, esp_gattc_if, param);
 | 
			
		||||
 | 
			
		||||
  // Delete characteristics after clients have used them to save RAM.
 | 
			
		||||
  if (!all_established && this->all_nodes_established_()) {
 | 
			
		||||
    for (auto &svc : this->services_)
 | 
			
		||||
      delete svc;  // NOLINT(cppcoreguidelines-owning-memory)
 | 
			
		||||
    this->services_.clear();
 | 
			
		||||
  if (!this->services_.empty() && this->all_nodes_established_()) {
 | 
			
		||||
    this->release_services();
 | 
			
		||||
    ESP_LOGD(TAG, "All clients established, services released");
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -19,26 +19,36 @@ void BLEBinaryOutput::dump_config() {
 | 
			
		||||
void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                                          esp_ble_gattc_cb_param_t *param) {
 | 
			
		||||
  switch (event) {
 | 
			
		||||
    case ESP_GATTC_OPEN_EVT:
 | 
			
		||||
      this->client_state_ = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] Connected successfully!", this->char_uuid_.to_string().c_str());
 | 
			
		||||
      break;
 | 
			
		||||
    case ESP_GATTC_DISCONNECT_EVT:
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] Disconnected", this->char_uuid_.to_string().c_str());
 | 
			
		||||
      this->client_state_ = espbt::ClientState::IDLE;
 | 
			
		||||
      break;
 | 
			
		||||
    case ESP_GATTC_WRITE_CHAR_EVT: {
 | 
			
		||||
      if (param->write.status == 0) {
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
    case ESP_GATTC_SEARCH_CMPL_EVT: {
 | 
			
		||||
      auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_);
 | 
			
		||||
      if (chr == nullptr) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%s] Characteristic not found.", this->char_uuid_.to_string().c_str());
 | 
			
		||||
        ESP_LOGW(TAG, "Characteristic %s was not found in service %s", this->char_uuid_.to_string().c_str(),
 | 
			
		||||
                 this->service_uuid_.to_string().c_str());
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      if (param->write.handle == chr->handle) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%s] Write error, status=%d", this->char_uuid_.to_string().c_str(), param->write.status);
 | 
			
		||||
      this->char_handle_ = chr->handle;
 | 
			
		||||
      this->char_props_ = chr->properties;
 | 
			
		||||
      if (this->require_response_ && this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE) {
 | 
			
		||||
        this->write_type_ = ESP_GATT_WRITE_TYPE_RSP;
 | 
			
		||||
        ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_RSP");
 | 
			
		||||
      } else if (!this->require_response_ && this->char_props_ & ESP_GATT_CHAR_PROP_BIT_WRITE_NR) {
 | 
			
		||||
        this->write_type_ = ESP_GATT_WRITE_TYPE_NO_RSP;
 | 
			
		||||
        ESP_LOGD(TAG, "Write type: ESP_GATT_WRITE_TYPE_NO_RSP");
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGE(TAG, "Characteristic %s does not allow writing with%s response", this->char_uuid_.to_string().c_str(),
 | 
			
		||||
                 this->require_response_ ? "" : "out");
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
      ESP_LOGD(TAG, "Found characteristic %s on device %s", this->char_uuid_.to_string().c_str(),
 | 
			
		||||
               this->parent()->address_str().c_str());
 | 
			
		||||
      this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_WRITE_CHAR_EVT: {
 | 
			
		||||
      if (param->write.handle == this->char_handle_) {
 | 
			
		||||
        if (param->write.status != 0)
 | 
			
		||||
          ESP_LOGW(TAG, "[%s] Write error, status=%d", this->char_uuid_.to_string().c_str(), param->write.status);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
@@ -48,26 +58,18 @@ void BLEBinaryOutput::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_i
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEBinaryOutput::write_state(bool state) {
 | 
			
		||||
  if (this->client_state_ != espbt::ClientState::ESTABLISHED) {
 | 
			
		||||
  if (this->node_state != espbt::ClientState::ESTABLISHED) {
 | 
			
		||||
    ESP_LOGW(TAG, "[%s] Not connected to BLE client.  State update can not be written.",
 | 
			
		||||
             this->char_uuid_.to_string().c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->char_uuid_);
 | 
			
		||||
  if (chr == nullptr) {
 | 
			
		||||
    ESP_LOGW(TAG, "[%s] Characteristic not found.  State update can not be written.",
 | 
			
		||||
             this->char_uuid_.to_string().c_str());
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint8_t state_as_uint = (uint8_t) state;
 | 
			
		||||
  ESP_LOGV(TAG, "[%s] Write State: %d", this->char_uuid_.to_string().c_str(), state_as_uint);
 | 
			
		||||
  if (this->require_response_) {
 | 
			
		||||
    chr->write_value(&state_as_uint, sizeof(state_as_uint), ESP_GATT_WRITE_TYPE_RSP);
 | 
			
		||||
  } else {
 | 
			
		||||
    chr->write_value(&state_as_uint, sizeof(state_as_uint), ESP_GATT_WRITE_TYPE_NO_RSP);
 | 
			
		||||
  }
 | 
			
		||||
  esp_err_t err =
 | 
			
		||||
      esp_ble_gattc_write_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->char_handle_,
 | 
			
		||||
                               sizeof(state_as_uint), &state_as_uint, this->write_type_, ESP_GATT_AUTH_REQ_NONE);
 | 
			
		||||
  if (err != ESP_GATT_OK)
 | 
			
		||||
    ESP_LOGW(TAG, "[%s] Write error, err=%d", this->char_uuid_.to_string().c_str(), err);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace ble_client
 | 
			
		||||
 
 | 
			
		||||
@@ -32,7 +32,9 @@ class BLEBinaryOutput : public output::BinaryOutput, public BLEClientNode, publi
 | 
			
		||||
  bool require_response_;
 | 
			
		||||
  espbt::ESPBTUUID service_uuid_;
 | 
			
		||||
  espbt::ESPBTUUID char_uuid_;
 | 
			
		||||
  espbt::ClientState client_state_;
 | 
			
		||||
  uint16_t char_handle_{};
 | 
			
		||||
  esp_gatt_char_prop_t char_props_{};
 | 
			
		||||
  esp_gatt_write_type_t write_type_{};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace ble_client
 | 
			
		||||
 
 | 
			
		||||
@@ -14,15 +14,17 @@ class BLESensorNotifyTrigger : public Trigger<float>, public BLESensor {
 | 
			
		||||
  void gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                           esp_ble_gattc_cb_param_t *param) override {
 | 
			
		||||
    switch (event) {
 | 
			
		||||
      case ESP_GATTC_SEARCH_CMPL_EVT: {
 | 
			
		||||
        this->sensor_->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
      case ESP_GATTC_NOTIFY_EVT: {
 | 
			
		||||
        if (param->notify.handle == this->sensor_->handle)
 | 
			
		||||
          this->trigger(this->sensor_->parent()->parse_char_value(param->notify.value, param->notify.value_len));
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      case ESP_GATTC_NOTIFY_EVT: {
 | 
			
		||||
        if (param->notify.conn_id != this->sensor_->parent()->get_conn_id() ||
 | 
			
		||||
            param->notify.handle != this->sensor_->handle)
 | 
			
		||||
          break;
 | 
			
		||||
        this->trigger(this->sensor_->parent()->parse_char_value(param->notify.value, param->notify.value_len));
 | 
			
		||||
      case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
 | 
			
		||||
        // confirms notifications are being listened for. While enabling of notifications may still be in
 | 
			
		||||
        // progress by the parent, we assume it will happen.
 | 
			
		||||
        if (param->reg_for_notify.status == ESP_GATT_OK && param->reg_for_notify.handle == this->sensor_->handle)
 | 
			
		||||
          this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      default:
 | 
			
		||||
        break;
 | 
			
		||||
 
 | 
			
		||||
@@ -22,26 +22,19 @@ void BLEClientRSSISensor::dump_config() {
 | 
			
		||||
void BLEClientRSSISensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                                              esp_ble_gattc_cb_param_t *param) {
 | 
			
		||||
  switch (event) {
 | 
			
		||||
    case ESP_GATTC_OPEN_EVT: {
 | 
			
		||||
      if (param->open.status == ESP_GATT_OK) {
 | 
			
		||||
        ESP_LOGI(TAG, "[%s] Connected successfully!", this->get_name().c_str());
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_DISCONNECT_EVT: {
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str());
 | 
			
		||||
    case ESP_GATTC_CLOSE_EVT: {
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      this->publish_state(NAN);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_SEARCH_CMPL_EVT:
 | 
			
		||||
    case ESP_GATTC_SEARCH_CMPL_EVT: {
 | 
			
		||||
      this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
      if (this->should_update_) {
 | 
			
		||||
        this->should_update_ = false;
 | 
			
		||||
        this->get_rssi_();
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_DISCONNECT_EVT: {
 | 
			
		||||
    case ESP_GATTC_CLOSE_EVT: {
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str());
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      this->publish_state(NAN);
 | 
			
		||||
@@ -74,8 +74,6 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_READ_CHAR_EVT: {
 | 
			
		||||
      if (param->read.conn_id != this->parent()->get_conn_id())
 | 
			
		||||
        break;
 | 
			
		||||
      if (param->read.status != ESP_GATT_OK) {
 | 
			
		||||
        ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
 | 
			
		||||
        break;
 | 
			
		||||
@@ -87,15 +85,23 @@ void BLESensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t ga
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_NOTIFY_EVT: {
 | 
			
		||||
      if (param->notify.conn_id != this->parent()->get_conn_id() || param->notify.handle != this->handle)
 | 
			
		||||
        break;
 | 
			
		||||
      ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
 | 
			
		||||
      ESP_LOGD(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
 | 
			
		||||
               param->notify.handle, param->notify.value[0]);
 | 
			
		||||
      if (param->notify.handle != this->handle)
 | 
			
		||||
        break;
 | 
			
		||||
      this->publish_state(this->parse_data_(param->notify.value, param->notify.value_len));
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
 | 
			
		||||
      this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
      if (param->reg_for_notify.handle == this->handle) {
 | 
			
		||||
        if (param->reg_for_notify.status != ESP_GATT_OK) {
 | 
			
		||||
          ESP_LOGW(TAG, "Error registering for notifications at handle %d, status=%d", param->reg_for_notify.handle,
 | 
			
		||||
                   param->reg_for_notify.status);
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
        this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
        ESP_LOGD(TAG, "Register for notify on %s complete", this->char_uuid_.to_string().c_str());
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
 
 | 
			
		||||
@@ -17,14 +17,11 @@ void BLEClientSwitch::write_state(bool state) {
 | 
			
		||||
void BLEClientSwitch::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
                                          esp_ble_gattc_cb_param_t *param) {
 | 
			
		||||
  switch (event) {
 | 
			
		||||
    case ESP_GATTC_REG_EVT:
 | 
			
		||||
    case ESP_GATTC_CLOSE_EVT:
 | 
			
		||||
      this->publish_state(this->parent_->enabled);
 | 
			
		||||
      break;
 | 
			
		||||
    case ESP_GATTC_OPEN_EVT:
 | 
			
		||||
    case ESP_GATTC_SEARCH_CMPL_EVT:
 | 
			
		||||
      this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
      break;
 | 
			
		||||
    case ESP_GATTC_DISCONNECT_EVT:
 | 
			
		||||
      this->node_state = espbt::ClientState::IDLE;
 | 
			
		||||
      this->publish_state(this->parent_->enabled);
 | 
			
		||||
      break;
 | 
			
		||||
    default:
 | 
			
		||||
 
 | 
			
		||||
@@ -36,8 +36,7 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_DISCONNECT_EVT: {
 | 
			
		||||
      ESP_LOGW(TAG, "[%s] Disconnected!", this->get_name().c_str());
 | 
			
		||||
    case ESP_GATTC_CLOSE_EVT: {
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      this->publish_state(EMPTY);
 | 
			
		||||
      break;
 | 
			
		||||
@@ -77,20 +76,18 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_READ_CHAR_EVT: {
 | 
			
		||||
      if (param->read.conn_id != this->parent()->get_conn_id())
 | 
			
		||||
        break;
 | 
			
		||||
      if (param->read.status != ESP_GATT_OK) {
 | 
			
		||||
        ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      if (param->read.handle == this->handle) {
 | 
			
		||||
        if (param->read.status != ESP_GATT_OK) {
 | 
			
		||||
          ESP_LOGW(TAG, "Error reading char at handle %d, status=%d", param->read.handle, param->read.status);
 | 
			
		||||
          break;
 | 
			
		||||
        }
 | 
			
		||||
        this->status_clear_warning();
 | 
			
		||||
        this->publish_state(this->parse_data(param->read.value, param->read.value_len));
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_NOTIFY_EVT: {
 | 
			
		||||
      if (param->notify.conn_id != this->parent()->get_conn_id() || param->notify.handle != this->handle)
 | 
			
		||||
      if (param->notify.handle != this->handle)
 | 
			
		||||
        break;
 | 
			
		||||
      ESP_LOGV(TAG, "[%s] ESP_GATTC_NOTIFY_EVT: handle=0x%x, value=0x%x", this->get_name().c_str(),
 | 
			
		||||
               param->notify.handle, param->notify.value[0]);
 | 
			
		||||
@@ -98,7 +95,8 @@ void BLETextSensor::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
 | 
			
		||||
      this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
      if (param->reg_for_notify.status == ESP_GATT_OK && param->reg_for_notify.handle == this->handle)
 | 
			
		||||
        this->node_state = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    default:
 | 
			
		||||
 
 | 
			
		||||
@@ -1,116 +0,0 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import i2c, sensor
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_HUMIDITY,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_IIR_FILTER,
 | 
			
		||||
    CONF_OVERSAMPLING,
 | 
			
		||||
    CONF_PRESSURE,
 | 
			
		||||
    CONF_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
    DEVICE_CLASS_PRESSURE,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    UNIT_HECTOPASCAL,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
 | 
			
		||||
bme280_ns = cg.esphome_ns.namespace("bme280")
 | 
			
		||||
BME280Oversampling = bme280_ns.enum("BME280Oversampling")
 | 
			
		||||
OVERSAMPLING_OPTIONS = {
 | 
			
		||||
    "NONE": BME280Oversampling.BME280_OVERSAMPLING_NONE,
 | 
			
		||||
    "1X": BME280Oversampling.BME280_OVERSAMPLING_1X,
 | 
			
		||||
    "2X": BME280Oversampling.BME280_OVERSAMPLING_2X,
 | 
			
		||||
    "4X": BME280Oversampling.BME280_OVERSAMPLING_4X,
 | 
			
		||||
    "8X": BME280Oversampling.BME280_OVERSAMPLING_8X,
 | 
			
		||||
    "16X": BME280Oversampling.BME280_OVERSAMPLING_16X,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
BME280IIRFilter = bme280_ns.enum("BME280IIRFilter")
 | 
			
		||||
IIR_FILTER_OPTIONS = {
 | 
			
		||||
    "OFF": BME280IIRFilter.BME280_IIR_FILTER_OFF,
 | 
			
		||||
    "2X": BME280IIRFilter.BME280_IIR_FILTER_2X,
 | 
			
		||||
    "4X": BME280IIRFilter.BME280_IIR_FILTER_4X,
 | 
			
		||||
    "8X": BME280IIRFilter.BME280_IIR_FILTER_8X,
 | 
			
		||||
    "16X": BME280IIRFilter.BME280_IIR_FILTER_16X,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
BME280Component = bme280_ns.class_(
 | 
			
		||||
    "BME280Component", cg.PollingComponent, i2c.I2CDevice
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(BME280Component),
 | 
			
		||||
            cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_CELSIUS,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
                device_class=DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ).extend(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
 | 
			
		||||
                        OVERSAMPLING_OPTIONS, upper=True
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_HECTOPASCAL,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
                device_class=DEVICE_CLASS_PRESSURE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ).extend(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
 | 
			
		||||
                        OVERSAMPLING_OPTIONS, upper=True
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
                device_class=DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ).extend(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
 | 
			
		||||
                        OVERSAMPLING_OPTIONS, upper=True
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
 | 
			
		||||
                IIR_FILTER_OPTIONS, upper=True
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
    .extend(i2c.i2c_device_schema(0x77))
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await i2c.register_i2c_device(var, config)
 | 
			
		||||
 | 
			
		||||
    if temperature_config := config.get(CONF_TEMPERATURE):
 | 
			
		||||
        sens = await sensor.new_sensor(temperature_config)
 | 
			
		||||
        cg.add(var.set_temperature_sensor(sens))
 | 
			
		||||
        cg.add(var.set_temperature_oversampling(temperature_config[CONF_OVERSAMPLING]))
 | 
			
		||||
 | 
			
		||||
    if pressure_config := config.get(CONF_PRESSURE):
 | 
			
		||||
        sens = await sensor.new_sensor(pressure_config)
 | 
			
		||||
        cg.add(var.set_pressure_sensor(sens))
 | 
			
		||||
        cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING]))
 | 
			
		||||
 | 
			
		||||
    if humidity_config := config.get(CONF_HUMIDITY):
 | 
			
		||||
        sens = await sensor.new_sensor(humidity_config)
 | 
			
		||||
        cg.add(var.set_humidity_sensor(sens))
 | 
			
		||||
        cg.add(var.set_humidity_oversampling(humidity_config[CONF_OVERSAMPLING]))
 | 
			
		||||
 | 
			
		||||
    cg.add(var.set_iir_filter(config[CONF_IIR_FILTER]))
 | 
			
		||||
							
								
								
									
										1
									
								
								esphome/components/bme280_base/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/bme280_base/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
CODEOWNERS = ["@esphome/core"]
 | 
			
		||||
@@ -1,9 +1,14 @@
 | 
			
		||||
#include "bme280.h"
 | 
			
		||||
#include <cmath>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
 | 
			
		||||
#include "bme280_base.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include <esphome/components/sensor/sensor.h>
 | 
			
		||||
#include <esphome/core/component.h>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace bme280 {
 | 
			
		||||
namespace bme280_base {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "bme280.sensor";
 | 
			
		||||
 | 
			
		||||
@@ -46,7 +51,24 @@ static const uint8_t BME280_STATUS_IM_UPDATE = 0b01;
 | 
			
		||||
 | 
			
		||||
inline uint16_t combine_bytes(uint8_t msb, uint8_t lsb) { return ((msb & 0xFF) << 8) | (lsb & 0xFF); }
 | 
			
		||||
 | 
			
		||||
static const char *oversampling_to_str(BME280Oversampling oversampling) {
 | 
			
		||||
const char *iir_filter_to_str(BME280IIRFilter filter) {  // NOLINT
 | 
			
		||||
  switch (filter) {
 | 
			
		||||
    case BME280_IIR_FILTER_OFF:
 | 
			
		||||
      return "OFF";
 | 
			
		||||
    case BME280_IIR_FILTER_2X:
 | 
			
		||||
      return "2x";
 | 
			
		||||
    case BME280_IIR_FILTER_4X:
 | 
			
		||||
      return "4x";
 | 
			
		||||
    case BME280_IIR_FILTER_8X:
 | 
			
		||||
      return "8x";
 | 
			
		||||
    case BME280_IIR_FILTER_16X:
 | 
			
		||||
      return "16x";
 | 
			
		||||
    default:
 | 
			
		||||
      return "UNKNOWN";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
const char *oversampling_to_str(BME280Oversampling oversampling) {  // NOLINT
 | 
			
		||||
  switch (oversampling) {
 | 
			
		||||
    case BME280_OVERSAMPLING_NONE:
 | 
			
		||||
      return "None";
 | 
			
		||||
@@ -65,23 +87,6 @@ static const char *oversampling_to_str(BME280Oversampling oversampling) {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
static const char *iir_filter_to_str(BME280IIRFilter filter) {
 | 
			
		||||
  switch (filter) {
 | 
			
		||||
    case BME280_IIR_FILTER_OFF:
 | 
			
		||||
      return "OFF";
 | 
			
		||||
    case BME280_IIR_FILTER_2X:
 | 
			
		||||
      return "2x";
 | 
			
		||||
    case BME280_IIR_FILTER_4X:
 | 
			
		||||
      return "4x";
 | 
			
		||||
    case BME280_IIR_FILTER_8X:
 | 
			
		||||
      return "8x";
 | 
			
		||||
    case BME280_IIR_FILTER_16X:
 | 
			
		||||
      return "16x";
 | 
			
		||||
    default:
 | 
			
		||||
      return "UNKNOWN";
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BME280Component::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up BME280...");
 | 
			
		||||
  uint8_t chip_id = 0;
 | 
			
		||||
@@ -112,7 +117,7 @@ void BME280Component::setup() {
 | 
			
		||||
  // Wait until the NVM data has finished loading.
 | 
			
		||||
  uint8_t status;
 | 
			
		||||
  uint8_t retry = 5;
 | 
			
		||||
  do {
 | 
			
		||||
  do {  // NOLINT
 | 
			
		||||
    delay(2);
 | 
			
		||||
    if (!this->read_byte(BME280_REGISTER_STATUS, &status)) {
 | 
			
		||||
      ESP_LOGW(TAG, "Error reading status register.");
 | 
			
		||||
@@ -175,7 +180,6 @@ void BME280Component::setup() {
 | 
			
		||||
}
 | 
			
		||||
void BME280Component::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "BME280:");
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
  switch (this->error_code_) {
 | 
			
		||||
    case COMMUNICATION_FAILED:
 | 
			
		||||
      ESP_LOGE(TAG, "Communication with BME280 failed!");
 | 
			
		||||
@@ -226,14 +230,14 @@ void BME280Component::update() {
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    int32_t t_fine = 0;
 | 
			
		||||
    float temperature = this->read_temperature_(data, &t_fine);
 | 
			
		||||
    float const temperature = this->read_temperature_(data, &t_fine);
 | 
			
		||||
    if (std::isnan(temperature)) {
 | 
			
		||||
      ESP_LOGW(TAG, "Invalid temperature, cannot read pressure & humidity values.");
 | 
			
		||||
      this->status_set_warning();
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
    float pressure = this->read_pressure_(data, t_fine);
 | 
			
		||||
    float humidity = this->read_humidity_(data, t_fine);
 | 
			
		||||
    float const pressure = this->read_pressure_(data, t_fine);
 | 
			
		||||
    float const humidity = this->read_humidity_(data, t_fine);
 | 
			
		||||
 | 
			
		||||
    ESP_LOGV(TAG, "Got temperature=%.1f°C pressure=%.1fhPa humidity=%.1f%%", temperature, pressure, humidity);
 | 
			
		||||
    if (this->temperature_sensor_ != nullptr)
 | 
			
		||||
@@ -257,12 +261,12 @@ float BME280Component::read_temperature_(const uint8_t *data, int32_t *t_fine) {
 | 
			
		||||
  const int32_t t2 = this->calibration_.t2;
 | 
			
		||||
  const int32_t t3 = this->calibration_.t3;
 | 
			
		||||
 | 
			
		||||
  int32_t var1 = (((adc >> 3) - (t1 << 1)) * t2) >> 11;
 | 
			
		||||
  int32_t var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14;
 | 
			
		||||
  int32_t const var1 = (((adc >> 3) - (t1 << 1)) * t2) >> 11;
 | 
			
		||||
  int32_t const var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14;
 | 
			
		||||
  *t_fine = var1 + var2;
 | 
			
		||||
 | 
			
		||||
  float temperature = (*t_fine * 5 + 128) >> 8;
 | 
			
		||||
  return temperature / 100.0f;
 | 
			
		||||
  float const temperature = (*t_fine * 5 + 128);
 | 
			
		||||
  return temperature / 25600.0f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float BME280Component::read_pressure_(const uint8_t *data, int32_t t_fine) {
 | 
			
		||||
@@ -303,11 +307,11 @@ float BME280Component::read_pressure_(const uint8_t *data, int32_t t_fine) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float BME280Component::read_humidity_(const uint8_t *data, int32_t t_fine) {
 | 
			
		||||
  uint16_t raw_adc = ((data[6] & 0xFF) << 8) | (data[7] & 0xFF);
 | 
			
		||||
  uint16_t const raw_adc = ((data[6] & 0xFF) << 8) | (data[7] & 0xFF);
 | 
			
		||||
  if (raw_adc == 0x8000)
 | 
			
		||||
    return NAN;
 | 
			
		||||
 | 
			
		||||
  int32_t adc = raw_adc;
 | 
			
		||||
  int32_t const adc = raw_adc;
 | 
			
		||||
 | 
			
		||||
  const int32_t h1 = this->calibration_.h1;
 | 
			
		||||
  const int32_t h2 = this->calibration_.h2;
 | 
			
		||||
@@ -325,7 +329,7 @@ float BME280Component::read_humidity_(const uint8_t *data, int32_t t_fine) {
 | 
			
		||||
 | 
			
		||||
  v_x1_u32r = v_x1_u32r < 0 ? 0 : v_x1_u32r;
 | 
			
		||||
  v_x1_u32r = v_x1_u32r > 419430400 ? 419430400 : v_x1_u32r;
 | 
			
		||||
  float h = v_x1_u32r >> 12;
 | 
			
		||||
  float const h = v_x1_u32r >> 12;
 | 
			
		||||
 | 
			
		||||
  return h / 1024.0f;
 | 
			
		||||
}
 | 
			
		||||
@@ -351,5 +355,5 @@ uint16_t BME280Component::read_u16_le_(uint8_t a_register) {
 | 
			
		||||
}
 | 
			
		||||
int16_t BME280Component::read_s16_le_(uint8_t a_register) { return this->read_u16_le_(a_register); }
 | 
			
		||||
 | 
			
		||||
}  // namespace bme280
 | 
			
		||||
}  // namespace bme280_base
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
@@ -2,10 +2,9 @@
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/i2c/i2c.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace bme280 {
 | 
			
		||||
namespace bme280_base {
 | 
			
		||||
 | 
			
		||||
/// Internal struct storing the calibration values of an BME280.
 | 
			
		||||
struct BME280CalibrationData {
 | 
			
		||||
@@ -57,8 +56,8 @@ enum BME280IIRFilter {
 | 
			
		||||
  BME280_IIR_FILTER_16X = 0b100,
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// This class implements support for the BME280 Temperature+Pressure+Humidity i2c sensor.
 | 
			
		||||
class BME280Component : public PollingComponent, public i2c::I2CDevice {
 | 
			
		||||
/// This class implements support for the BME280 Temperature+Pressure+Humidity sensor.
 | 
			
		||||
class BME280Component : public PollingComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  void set_temperature_sensor(sensor::Sensor *temperature_sensor) { temperature_sensor_ = temperature_sensor; }
 | 
			
		||||
  void set_pressure_sensor(sensor::Sensor *pressure_sensor) { pressure_sensor_ = pressure_sensor; }
 | 
			
		||||
@@ -91,6 +90,11 @@ class BME280Component : public PollingComponent, public i2c::I2CDevice {
 | 
			
		||||
  uint16_t read_u16_le_(uint8_t a_register);
 | 
			
		||||
  int16_t read_s16_le_(uint8_t a_register);
 | 
			
		||||
 | 
			
		||||
  virtual bool read_byte(uint8_t a_register, uint8_t *data) = 0;
 | 
			
		||||
  virtual bool write_byte(uint8_t a_register, uint8_t data) = 0;
 | 
			
		||||
  virtual bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) = 0;
 | 
			
		||||
  virtual bool read_byte_16(uint8_t a_register, uint16_t *data) = 0;
 | 
			
		||||
 | 
			
		||||
  BME280CalibrationData calibration_;
 | 
			
		||||
  BME280Oversampling temperature_oversampling_{BME280_OVERSAMPLING_16X};
 | 
			
		||||
  BME280Oversampling pressure_oversampling_{BME280_OVERSAMPLING_16X};
 | 
			
		||||
@@ -106,5 +110,5 @@ class BME280Component : public PollingComponent, public i2c::I2CDevice {
 | 
			
		||||
  } error_code_{NONE};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace bme280
 | 
			
		||||
}  // namespace bme280_base
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										106
									
								
								esphome/components/bme280_base/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										106
									
								
								esphome/components/bme280_base/sensor.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,106 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_HUMIDITY,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_IIR_FILTER,
 | 
			
		||||
    CONF_OVERSAMPLING,
 | 
			
		||||
    CONF_PRESSURE,
 | 
			
		||||
    CONF_TEMPERATURE,
 | 
			
		||||
    DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
    DEVICE_CLASS_PRESSURE,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
    STATE_CLASS_MEASUREMENT,
 | 
			
		||||
    UNIT_CELSIUS,
 | 
			
		||||
    UNIT_HECTOPASCAL,
 | 
			
		||||
    UNIT_PERCENT,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
bme280_ns = cg.esphome_ns.namespace("bme280_base")
 | 
			
		||||
BME280Oversampling = bme280_ns.enum("BME280Oversampling")
 | 
			
		||||
OVERSAMPLING_OPTIONS = {
 | 
			
		||||
    "NONE": BME280Oversampling.BME280_OVERSAMPLING_NONE,
 | 
			
		||||
    "1X": BME280Oversampling.BME280_OVERSAMPLING_1X,
 | 
			
		||||
    "2X": BME280Oversampling.BME280_OVERSAMPLING_2X,
 | 
			
		||||
    "4X": BME280Oversampling.BME280_OVERSAMPLING_4X,
 | 
			
		||||
    "8X": BME280Oversampling.BME280_OVERSAMPLING_8X,
 | 
			
		||||
    "16X": BME280Oversampling.BME280_OVERSAMPLING_16X,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
BME280IIRFilter = bme280_ns.enum("BME280IIRFilter")
 | 
			
		||||
IIR_FILTER_OPTIONS = {
 | 
			
		||||
    "OFF": BME280IIRFilter.BME280_IIR_FILTER_OFF,
 | 
			
		||||
    "2X": BME280IIRFilter.BME280_IIR_FILTER_2X,
 | 
			
		||||
    "4X": BME280IIRFilter.BME280_IIR_FILTER_4X,
 | 
			
		||||
    "8X": BME280IIRFilter.BME280_IIR_FILTER_8X,
 | 
			
		||||
    "16X": BME280IIRFilter.BME280_IIR_FILTER_16X,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA_BASE = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema(
 | 
			
		||||
            unit_of_measurement=UNIT_CELSIUS,
 | 
			
		||||
            accuracy_decimals=1,
 | 
			
		||||
            device_class=DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
            state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        ).extend(
 | 
			
		||||
            {
 | 
			
		||||
                cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
 | 
			
		||||
                    OVERSAMPLING_OPTIONS, upper=True
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_PRESSURE): sensor.sensor_schema(
 | 
			
		||||
            unit_of_measurement=UNIT_HECTOPASCAL,
 | 
			
		||||
            accuracy_decimals=1,
 | 
			
		||||
            device_class=DEVICE_CLASS_PRESSURE,
 | 
			
		||||
            state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        ).extend(
 | 
			
		||||
            {
 | 
			
		||||
                cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
 | 
			
		||||
                    OVERSAMPLING_OPTIONS, upper=True
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_HUMIDITY): sensor.sensor_schema(
 | 
			
		||||
            unit_of_measurement=UNIT_PERCENT,
 | 
			
		||||
            accuracy_decimals=1,
 | 
			
		||||
            device_class=DEVICE_CLASS_HUMIDITY,
 | 
			
		||||
            state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        ).extend(
 | 
			
		||||
            {
 | 
			
		||||
                cv.Optional(CONF_OVERSAMPLING, default="16X"): cv.enum(
 | 
			
		||||
                    OVERSAMPLING_OPTIONS, upper=True
 | 
			
		||||
                ),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_IIR_FILTER, default="OFF"): cv.enum(
 | 
			
		||||
            IIR_FILTER_OPTIONS, upper=True
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
).extend(cv.polling_component_schema("60s"))
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config, func=None):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    if func is not None:
 | 
			
		||||
        await func(var, config)
 | 
			
		||||
 | 
			
		||||
    if temperature_config := config.get(CONF_TEMPERATURE):
 | 
			
		||||
        sens = await sensor.new_sensor(temperature_config)
 | 
			
		||||
        cg.add(var.set_temperature_sensor(sens))
 | 
			
		||||
        cg.add(var.set_temperature_oversampling(temperature_config[CONF_OVERSAMPLING]))
 | 
			
		||||
 | 
			
		||||
    if pressure_config := config.get(CONF_PRESSURE):
 | 
			
		||||
        sens = await sensor.new_sensor(pressure_config)
 | 
			
		||||
        cg.add(var.set_pressure_sensor(sens))
 | 
			
		||||
        cg.add(var.set_pressure_oversampling(pressure_config[CONF_OVERSAMPLING]))
 | 
			
		||||
 | 
			
		||||
    if humidity_config := config.get(CONF_HUMIDITY):
 | 
			
		||||
        sens = await sensor.new_sensor(humidity_config)
 | 
			
		||||
        cg.add(var.set_humidity_sensor(sens))
 | 
			
		||||
        cg.add(var.set_humidity_oversampling(humidity_config[CONF_OVERSAMPLING]))
 | 
			
		||||
 | 
			
		||||
    cg.add(var.set_iir_filter(config[CONF_IIR_FILTER]))
 | 
			
		||||
							
								
								
									
										30
									
								
								esphome/components/bme280_i2c/bme280_i2c.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										30
									
								
								esphome/components/bme280_i2c/bme280_i2c.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,30 @@
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
 | 
			
		||||
#include "bme280_i2c.h"
 | 
			
		||||
#include "esphome/components/i2c/i2c.h"
 | 
			
		||||
#include "../bme280_base/bme280_base.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace bme280_i2c {
 | 
			
		||||
 | 
			
		||||
bool BME280I2CComponent::read_byte(uint8_t a_register, uint8_t *data) {
 | 
			
		||||
  return I2CDevice::read_byte(a_register, data);
 | 
			
		||||
};
 | 
			
		||||
bool BME280I2CComponent::write_byte(uint8_t a_register, uint8_t data) {
 | 
			
		||||
  return I2CDevice::write_byte(a_register, data);
 | 
			
		||||
};
 | 
			
		||||
bool BME280I2CComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) {
 | 
			
		||||
  return I2CDevice::read_bytes(a_register, data, len);
 | 
			
		||||
};
 | 
			
		||||
bool BME280I2CComponent::read_byte_16(uint8_t a_register, uint16_t *data) {
 | 
			
		||||
  return I2CDevice::read_byte_16(a_register, data);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
void BME280I2CComponent::dump_config() {
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
  BME280Component::dump_config();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace bme280_i2c
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										20
									
								
								esphome/components/bme280_i2c/bme280_i2c.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								esphome/components/bme280_i2c/bme280_i2c.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/bme280_base/bme280_base.h"
 | 
			
		||||
#include "esphome/components/i2c/i2c.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace bme280_i2c {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "bme280_i2c.sensor";
 | 
			
		||||
 | 
			
		||||
class BME280I2CComponent : public esphome::bme280_base::BME280Component, public i2c::I2CDevice {
 | 
			
		||||
  bool read_byte(uint8_t a_register, uint8_t *data) override;
 | 
			
		||||
  bool write_byte(uint8_t a_register, uint8_t data) override;
 | 
			
		||||
  bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
 | 
			
		||||
  bool read_byte_16(uint8_t a_register, uint16_t *data) override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace bme280_i2c
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										19
									
								
								esphome/components/bme280_i2c/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										19
									
								
								esphome/components/bme280_i2c/sensor.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,19 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import i2c
 | 
			
		||||
from ..bme280_base.sensor import to_code as to_code_base, cv, CONFIG_SCHEMA_BASE
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["i2c"]
 | 
			
		||||
AUTO_LOAD = ["bme280_base"]
 | 
			
		||||
 | 
			
		||||
bme280_ns = cg.esphome_ns.namespace("bme280_i2c")
 | 
			
		||||
BME280I2CComponent = bme280_ns.class_(
 | 
			
		||||
    "BME280I2CComponent", cg.PollingComponent, i2c.I2CDevice
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(
 | 
			
		||||
    i2c.i2c_device_schema(default_address=0x77)
 | 
			
		||||
).extend({cv.GenerateID(): cv.declare_id(BME280I2CComponent)})
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    await to_code_base(config, func=i2c.register_i2c_device)
 | 
			
		||||
							
								
								
									
										1
									
								
								esphome/components/bme280_spi/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								esphome/components/bme280_spi/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1 @@
 | 
			
		||||
CODEOWNERS = ["@apbodrov"]
 | 
			
		||||
							
								
								
									
										66
									
								
								esphome/components/bme280_spi/bme280_spi.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								esphome/components/bme280_spi/bme280_spi.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,66 @@
 | 
			
		||||
#include <cstdint>
 | 
			
		||||
#include <cstddef>
 | 
			
		||||
 | 
			
		||||
#include "bme280_spi.h"
 | 
			
		||||
#include <esphome/components/bme280_base/bme280_base.h>
 | 
			
		||||
 | 
			
		||||
int set_bit(uint8_t num, int position) {
 | 
			
		||||
  int mask = 1 << position;
 | 
			
		||||
  return num | mask;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
int clear_bit(uint8_t num, int position) {
 | 
			
		||||
  int mask = 1 << position;
 | 
			
		||||
  return num & ~mask;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace bme280_spi {
 | 
			
		||||
 | 
			
		||||
void BME280SPIComponent::setup() {
 | 
			
		||||
  this->spi_setup();
 | 
			
		||||
  BME280Component::setup();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// In SPI mode, only 7 bits of the register addresses are used; the MSB of register address is not used
 | 
			
		||||
// and replaced by a read/write bit (RW = ‘0’ for write and RW = ‘1’ for read).
 | 
			
		||||
// Example: address 0xF7 is accessed by using SPI register address 0x77. For write access, the byte
 | 
			
		||||
// 0x77 is transferred, for read access, the byte 0xF7 is transferred.
 | 
			
		||||
// https://www.bosch-sensortec.com/media/boschsensortec/downloads/datasheets/bst-bme280-ds002.pdf
 | 
			
		||||
 | 
			
		||||
bool BME280SPIComponent::read_byte(uint8_t a_register, uint8_t *data) {
 | 
			
		||||
  this->enable();
 | 
			
		||||
  // cause: *data = this->delegate_->transfer(tmp) doesnt work
 | 
			
		||||
  this->delegate_->transfer(set_bit(a_register, 7));
 | 
			
		||||
  *data = this->delegate_->transfer(0);
 | 
			
		||||
  this->disable();
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool BME280SPIComponent::write_byte(uint8_t a_register, uint8_t data) {
 | 
			
		||||
  this->enable();
 | 
			
		||||
  this->delegate_->transfer(clear_bit(a_register, 7));
 | 
			
		||||
  this->delegate_->transfer(data);
 | 
			
		||||
  this->disable();
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool BME280SPIComponent::read_bytes(uint8_t a_register, uint8_t *data, size_t len) {
 | 
			
		||||
  this->enable();
 | 
			
		||||
  this->delegate_->transfer(set_bit(a_register, 7));
 | 
			
		||||
  this->delegate_->read_array(data, len);
 | 
			
		||||
  this->disable();
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool BME280SPIComponent::read_byte_16(uint8_t a_register, uint16_t *data) {
 | 
			
		||||
  this->enable();
 | 
			
		||||
  this->delegate_->transfer(set_bit(a_register, 7));
 | 
			
		||||
  ((uint8_t *) data)[1] = this->delegate_->transfer(0);
 | 
			
		||||
  ((uint8_t *) data)[0] = this->delegate_->transfer(0);
 | 
			
		||||
  this->disable();
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace bme280_spi
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										20
									
								
								esphome/components/bme280_spi/bme280_spi.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								esphome/components/bme280_spi/bme280_spi.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,20 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/components/bme280_base/bme280_base.h"
 | 
			
		||||
#include "esphome/components/spi/spi.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace bme280_spi {
 | 
			
		||||
 | 
			
		||||
class BME280SPIComponent : public esphome::bme280_base::BME280Component,
 | 
			
		||||
                           public spi::SPIDevice<spi::BIT_ORDER_MSB_FIRST, spi::CLOCK_POLARITY_LOW,
 | 
			
		||||
                                                 spi::CLOCK_PHASE_LEADING, spi::DATA_RATE_200KHZ> {
 | 
			
		||||
  void setup() override;
 | 
			
		||||
  bool read_byte(uint8_t a_register, uint8_t *data) override;
 | 
			
		||||
  bool write_byte(uint8_t a_register, uint8_t data) override;
 | 
			
		||||
  bool read_bytes(uint8_t a_register, uint8_t *data, size_t len) override;
 | 
			
		||||
  bool read_byte_16(uint8_t a_register, uint16_t *data) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace bme280_spi
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										24
									
								
								esphome/components/bme280_spi/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										24
									
								
								esphome/components/bme280_spi/sensor.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,24 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
from esphome.components import spi
 | 
			
		||||
from esphome.components.bme280_base.sensor import (
 | 
			
		||||
    to_code as to_code_base,
 | 
			
		||||
    cv,
 | 
			
		||||
    CONFIG_SCHEMA_BASE,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
DEPENDENCIES = ["spi"]
 | 
			
		||||
AUTO_LOAD = ["bme280_base"]
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
bme280_spi_ns = cg.esphome_ns.namespace("bme280_spi")
 | 
			
		||||
BME280SPIComponent = bme280_spi_ns.class_(
 | 
			
		||||
    "BME280SPIComponent", cg.PollingComponent, spi.SPIDevice
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = CONFIG_SCHEMA_BASE.extend(spi.spi_device_schema()).extend(
 | 
			
		||||
    {cv.GenerateID(): cv.declare_id(BME280SPIComponent)}
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    await to_code_base(config, func=spi.register_spi_device)
 | 
			
		||||
@@ -200,8 +200,8 @@ float BMP280Component::read_temperature_(int32_t *t_fine) {
 | 
			
		||||
  int32_t var2 = (((((adc >> 4) - t1) * ((adc >> 4) - t1)) >> 12) * t3) >> 14;
 | 
			
		||||
  *t_fine = var1 + var2;
 | 
			
		||||
 | 
			
		||||
  float temperature = (*t_fine * 5 + 128) >> 8;
 | 
			
		||||
  return temperature / 100.0f;
 | 
			
		||||
  float temperature = (*t_fine * 5 + 128);
 | 
			
		||||
  return temperature / 25600.0f;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float BMP280Component::read_pressure_(int32_t t_fine) {
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										0
									
								
								esphome/components/combination/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								esphome/components/combination/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
								
								
									
										262
									
								
								esphome/components/combination/combination.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										262
									
								
								esphome/components/combination/combination.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,262 @@
 | 
			
		||||
#include "combination.h"
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include "esphome/core/hal.h"
 | 
			
		||||
 | 
			
		||||
#include <cmath>
 | 
			
		||||
#include <functional>
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace combination {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "combination";
 | 
			
		||||
 | 
			
		||||
void CombinationComponent::log_config_(const LogString *combo_type) {
 | 
			
		||||
  LOG_SENSOR("", "Combination Sensor:", this);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Combination Type: %s", LOG_STR_ARG(combo_type));
 | 
			
		||||
  this->log_source_sensors();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void CombinationNoParameterComponent::add_source(Sensor *sensor) { this->sensors_.emplace_back(sensor); }
 | 
			
		||||
 | 
			
		||||
void CombinationOneParameterComponent::add_source(Sensor *sensor, std::function<float(float)> const &stddev) {
 | 
			
		||||
  this->sensor_pairs_.emplace_back(sensor, stddev);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void CombinationOneParameterComponent::add_source(Sensor *sensor, float stddev) {
 | 
			
		||||
  this->add_source(sensor, std::function<float(float)>{[stddev](float x) -> float { return stddev; }});
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void CombinationNoParameterComponent::log_source_sensors() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Source Sensors:");
 | 
			
		||||
  for (const auto &sensor : this->sensors_) {
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "    - %s", sensor->get_name().c_str());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void CombinationOneParameterComponent::log_source_sensors() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Source Sensors:");
 | 
			
		||||
  for (const auto &sensor : this->sensor_pairs_) {
 | 
			
		||||
    auto &entity = *sensor.first;
 | 
			
		||||
    ESP_LOGCONFIG(TAG, "    - %s", entity.get_name().c_str());
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void CombinationNoParameterComponent::setup() {
 | 
			
		||||
  for (const auto &sensor : this->sensors_) {
 | 
			
		||||
    // All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result
 | 
			
		||||
    // repeatedly in the same loop if multiple source senors update.
 | 
			
		||||
    sensor->add_on_state_callback(
 | 
			
		||||
        [this](float value) -> void { this->defer("update", [this, value]() { this->handle_new_value(value); }); });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void KalmanCombinationComponent::dump_config() {
 | 
			
		||||
  this->log_config_(LOG_STR("kalman"));
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Update variance: %f per ms", this->update_variance_value_);
 | 
			
		||||
 | 
			
		||||
  if (this->std_dev_sensor_ != nullptr) {
 | 
			
		||||
    LOG_SENSOR("  ", "Standard Deviation Sensor:", this->std_dev_sensor_);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void KalmanCombinationComponent::setup() {
 | 
			
		||||
  for (const auto &sensor : this->sensor_pairs_) {
 | 
			
		||||
    const auto stddev = sensor.second;
 | 
			
		||||
    sensor.first->add_on_state_callback([this, stddev](float x) -> void { this->correct_(x, stddev(x)); });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void KalmanCombinationComponent::update_variance_() {
 | 
			
		||||
  uint32_t now = millis();
 | 
			
		||||
 | 
			
		||||
  // Variance increases by update_variance_ each millisecond
 | 
			
		||||
  auto dt = now - this->last_update_;
 | 
			
		||||
  auto dv = this->update_variance_value_ * dt;
 | 
			
		||||
  this->variance_ += dv;
 | 
			
		||||
  this->last_update_ = now;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void KalmanCombinationComponent::correct_(float value, float stddev) {
 | 
			
		||||
  if (std::isnan(value) || std::isinf(stddev)) {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (std::isnan(this->state_) || std::isinf(this->variance_)) {
 | 
			
		||||
    this->state_ = value;
 | 
			
		||||
    this->variance_ = stddev * stddev;
 | 
			
		||||
    if (this->std_dev_sensor_ != nullptr) {
 | 
			
		||||
      this->std_dev_sensor_->publish_state(stddev);
 | 
			
		||||
    }
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->update_variance_();
 | 
			
		||||
 | 
			
		||||
  // Combine two gaussian distributions mu1+-var1, mu2+-var2 to a new one around mu
 | 
			
		||||
  // Use the value with the smaller variance as mu1 to prevent precision errors
 | 
			
		||||
  const bool this_first = this->variance_ < (stddev * stddev);
 | 
			
		||||
  const float mu1 = this_first ? this->state_ : value;
 | 
			
		||||
  const float mu2 = this_first ? value : this->state_;
 | 
			
		||||
 | 
			
		||||
  const float var1 = this_first ? this->variance_ : stddev * stddev;
 | 
			
		||||
  const float var2 = this_first ? stddev * stddev : this->variance_;
 | 
			
		||||
 | 
			
		||||
  const float mu = mu1 + var1 * (mu2 - mu1) / (var1 + var2);
 | 
			
		||||
  const float var = var1 - (var1 * var1) / (var1 + var2);
 | 
			
		||||
 | 
			
		||||
  // Update and publish state
 | 
			
		||||
  this->state_ = mu;
 | 
			
		||||
  this->variance_ = var;
 | 
			
		||||
 | 
			
		||||
  this->publish_state(mu);
 | 
			
		||||
  if (this->std_dev_sensor_ != nullptr) {
 | 
			
		||||
    this->std_dev_sensor_->publish_state(std::sqrt(var));
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LinearCombinationComponent::setup() {
 | 
			
		||||
  for (const auto &sensor : this->sensor_pairs_) {
 | 
			
		||||
    // All sensor updates are deferred until the next loop. This avoids publishing the combined sensor's result
 | 
			
		||||
    // repeatedly in the same loop if multiple source senors update.
 | 
			
		||||
    sensor.first->add_on_state_callback(
 | 
			
		||||
        [this](float value) -> void { this->defer("update", [this, value]() { this->handle_new_value(value); }); });
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void LinearCombinationComponent::handle_new_value(float value) {
 | 
			
		||||
  // Multiplies each sensor state by a configured coeffecient and then sums
 | 
			
		||||
 | 
			
		||||
  if (!std::isfinite(value))
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  float sum = 0.0;
 | 
			
		||||
 | 
			
		||||
  for (const auto &sensor : this->sensor_pairs_) {
 | 
			
		||||
    const float sensor_state = sensor.first->state;
 | 
			
		||||
    if (std::isfinite(sensor_state)) {
 | 
			
		||||
      sum += sensor_state * sensor.second(sensor_state);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->publish_state(sum);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
void MaximumCombinationComponent::handle_new_value(float value) {
 | 
			
		||||
  if (!std::isfinite(value))
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  float max_value = (-1) * std::numeric_limits<float>::infinity();  // note x = max(x, -infinity)
 | 
			
		||||
 | 
			
		||||
  for (const auto &sensor : this->sensors_) {
 | 
			
		||||
    if (std::isfinite(sensor->state)) {
 | 
			
		||||
      max_value = std::max(max_value, sensor->state);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->publish_state(max_value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void MeanCombinationComponent::handle_new_value(float value) {
 | 
			
		||||
  if (!std::isfinite(value))
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  float sum = 0.0;
 | 
			
		||||
  size_t count = 0.0;
 | 
			
		||||
 | 
			
		||||
  for (const auto &sensor : this->sensors_) {
 | 
			
		||||
    if (std::isfinite(sensor->state)) {
 | 
			
		||||
      ++count;
 | 
			
		||||
      sum += sensor->state;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  float mean = sum / count;
 | 
			
		||||
 | 
			
		||||
  this->publish_state(mean);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void MedianCombinationComponent::handle_new_value(float value) {
 | 
			
		||||
  // Sorts sensor states in ascending order and determines the middle value
 | 
			
		||||
 | 
			
		||||
  if (!std::isfinite(value))
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  std::vector<float> sensor_states;
 | 
			
		||||
  for (const auto &sensor : this->sensors_) {
 | 
			
		||||
    if (std::isfinite(sensor->state)) {
 | 
			
		||||
      sensor_states.push_back(sensor->state);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  sort(sensor_states.begin(), sensor_states.end());
 | 
			
		||||
  size_t sensor_states_size = sensor_states.size();
 | 
			
		||||
 | 
			
		||||
  float median = NAN;
 | 
			
		||||
 | 
			
		||||
  if (sensor_states_size) {
 | 
			
		||||
    if (sensor_states_size % 2) {
 | 
			
		||||
      // Odd number of measurements, use middle measurement
 | 
			
		||||
      median = sensor_states[sensor_states_size / 2];
 | 
			
		||||
    } else {
 | 
			
		||||
      // Even number of measurements, use the average of the two middle measurements
 | 
			
		||||
      median = (sensor_states[sensor_states_size / 2] + sensor_states[sensor_states_size / 2 - 1]) / 2.0;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->publish_state(median);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void MinimumCombinationComponent::handle_new_value(float value) {
 | 
			
		||||
  if (!std::isfinite(value))
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  float min_value = std::numeric_limits<float>::infinity();  // note x = min(x, infinity)
 | 
			
		||||
 | 
			
		||||
  for (const auto &sensor : this->sensors_) {
 | 
			
		||||
    if (std::isfinite(sensor->state)) {
 | 
			
		||||
      min_value = std::min(min_value, sensor->state);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->publish_state(min_value);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void MostRecentCombinationComponent::handle_new_value(float value) { this->publish_state(value); }
 | 
			
		||||
 | 
			
		||||
void RangeCombinationComponent::handle_new_value(float value) {
 | 
			
		||||
  // Sorts sensor states then takes difference between largest and smallest states
 | 
			
		||||
 | 
			
		||||
  if (!std::isfinite(value))
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  std::vector<float> sensor_states;
 | 
			
		||||
  for (const auto &sensor : this->sensors_) {
 | 
			
		||||
    if (std::isfinite(sensor->state)) {
 | 
			
		||||
      sensor_states.push_back(sensor->state);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  sort(sensor_states.begin(), sensor_states.end());
 | 
			
		||||
 | 
			
		||||
  float range = sensor_states.back() - sensor_states.front();
 | 
			
		||||
  this->publish_state(range);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void SumCombinationComponent::handle_new_value(float value) {
 | 
			
		||||
  if (!std::isfinite(value))
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  float sum = 0.0;
 | 
			
		||||
  for (const auto &sensor : this->sensors_) {
 | 
			
		||||
    if (std::isfinite(sensor->state)) {
 | 
			
		||||
      sum += sensor->state;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->publish_state(sum);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace combination
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										141
									
								
								esphome/components/combination/combination.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								esphome/components/combination/combination.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,141 @@
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
 | 
			
		||||
#include <vector>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace combination {
 | 
			
		||||
 | 
			
		||||
class CombinationComponent : public Component, public sensor::Sensor {
 | 
			
		||||
 public:
 | 
			
		||||
  float get_setup_priority() const override { return esphome::setup_priority::DATA; }
 | 
			
		||||
 | 
			
		||||
  /// @brief Logs all source sensor's names
 | 
			
		||||
  virtual void log_source_sensors() = 0;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  /// @brief Logs the sensor for use in dump_config
 | 
			
		||||
  /// @param combo_type Name of the combination operation
 | 
			
		||||
  void log_config_(const LogString *combo_type);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
/// @brief Base class for operations that do not require an extra parameter to compute the combination
 | 
			
		||||
class CombinationNoParameterComponent : public CombinationComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  /// @brief Adds a callback to each source sensor
 | 
			
		||||
  void setup() override;
 | 
			
		||||
 | 
			
		||||
  void add_source(Sensor *sensor);
 | 
			
		||||
 | 
			
		||||
  /// @brief Computes the combination
 | 
			
		||||
  /// @param value Newest sensor measurement
 | 
			
		||||
  virtual void handle_new_value(float value) = 0;
 | 
			
		||||
 | 
			
		||||
  /// @brief Logs all source sensor's names in sensors_
 | 
			
		||||
  void log_source_sensors() override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  std::vector<Sensor *> sensors_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
// Base class for opertions that require one parameter to compute the combination
 | 
			
		||||
class CombinationOneParameterComponent : public CombinationComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  void add_source(Sensor *sensor, std::function<float(float)> const &stddev);
 | 
			
		||||
  void add_source(Sensor *sensor, float stddev);
 | 
			
		||||
 | 
			
		||||
  /// @brief Logs all source sensor's names in sensor_pairs_
 | 
			
		||||
  void log_source_sensors() override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  std::vector<std::pair<Sensor *, std::function<float(float)>>> sensor_pairs_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class KalmanCombinationComponent : public CombinationOneParameterComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  void setup() override;
 | 
			
		||||
 | 
			
		||||
  void set_process_std_dev(float process_std_dev) {
 | 
			
		||||
    this->update_variance_value_ = process_std_dev * process_std_dev * 0.001f;
 | 
			
		||||
  }
 | 
			
		||||
  void set_std_dev_sensor(Sensor *sensor) { this->std_dev_sensor_ = sensor; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void update_variance_();
 | 
			
		||||
  void correct_(float value, float stddev);
 | 
			
		||||
 | 
			
		||||
  // Optional sensor for publishing the current error
 | 
			
		||||
  sensor::Sensor *std_dev_sensor_{nullptr};
 | 
			
		||||
 | 
			
		||||
  // Tick of the last update
 | 
			
		||||
  uint32_t last_update_{0};
 | 
			
		||||
  // Change of the variance, per ms
 | 
			
		||||
  float update_variance_value_{0.f};
 | 
			
		||||
 | 
			
		||||
  // Best guess for the state and its variance
 | 
			
		||||
  float state_{NAN};
 | 
			
		||||
  float variance_{INFINITY};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class LinearCombinationComponent : public CombinationOneParameterComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  void dump_config() override { this->log_config_(LOG_STR("linear")); }
 | 
			
		||||
  void setup() override;
 | 
			
		||||
 | 
			
		||||
  void handle_new_value(float value);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class MaximumCombinationComponent : public CombinationNoParameterComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  void dump_config() override { this->log_config_(LOG_STR("max")); }
 | 
			
		||||
 | 
			
		||||
  void handle_new_value(float value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class MeanCombinationComponent : public CombinationNoParameterComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  void dump_config() override { this->log_config_(LOG_STR("mean")); }
 | 
			
		||||
 | 
			
		||||
  void handle_new_value(float value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class MedianCombinationComponent : public CombinationNoParameterComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  void dump_config() override { this->log_config_(LOG_STR("median")); }
 | 
			
		||||
 | 
			
		||||
  void handle_new_value(float value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class MinimumCombinationComponent : public CombinationNoParameterComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  void dump_config() override { this->log_config_(LOG_STR("min")); }
 | 
			
		||||
 | 
			
		||||
  void handle_new_value(float value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class MostRecentCombinationComponent : public CombinationNoParameterComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  void dump_config() override { this->log_config_(LOG_STR("most_recently_updated")); }
 | 
			
		||||
 | 
			
		||||
  void handle_new_value(float value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class RangeCombinationComponent : public CombinationNoParameterComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  void dump_config() override { this->log_config_(LOG_STR("range")); }
 | 
			
		||||
 | 
			
		||||
  void handle_new_value(float value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class SumCombinationComponent : public CombinationNoParameterComponent {
 | 
			
		||||
 public:
 | 
			
		||||
  void dump_config() override { this->log_config_(LOG_STR("sum")); }
 | 
			
		||||
 | 
			
		||||
  void handle_new_value(float value) override;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace combination
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										176
									
								
								esphome/components/combination/sensor.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										176
									
								
								esphome/components/combination/sensor.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,176 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome.components import sensor
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ACCURACY_DECIMALS,
 | 
			
		||||
    CONF_DEVICE_CLASS,
 | 
			
		||||
    CONF_ENTITY_CATEGORY,
 | 
			
		||||
    CONF_ICON,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_RANGE,
 | 
			
		||||
    CONF_SOURCE,
 | 
			
		||||
    CONF_SUM,
 | 
			
		||||
    CONF_TYPE,
 | 
			
		||||
    CONF_UNIT_OF_MEASUREMENT,
 | 
			
		||||
)
 | 
			
		||||
from esphome.core.entity_helpers import inherit_property_from
 | 
			
		||||
 | 
			
		||||
CODEOWNERS = ["@Cat-Ion", "@kahrendt"]
 | 
			
		||||
 | 
			
		||||
combination_ns = cg.esphome_ns.namespace("combination")
 | 
			
		||||
 | 
			
		||||
KalmanCombinationComponent = combination_ns.class_(
 | 
			
		||||
    "KalmanCombinationComponent", cg.Component, sensor.Sensor
 | 
			
		||||
)
 | 
			
		||||
LinearCombinationComponent = combination_ns.class_(
 | 
			
		||||
    "LinearCombinationComponent", cg.Component, sensor.Sensor
 | 
			
		||||
)
 | 
			
		||||
MaximumCombinationComponent = combination_ns.class_(
 | 
			
		||||
    "MaximumCombinationComponent", cg.Component, sensor.Sensor
 | 
			
		||||
)
 | 
			
		||||
MeanCombinationComponent = combination_ns.class_(
 | 
			
		||||
    "MeanCombinationComponent", cg.Component, sensor.Sensor
 | 
			
		||||
)
 | 
			
		||||
MedianCombinationComponent = combination_ns.class_(
 | 
			
		||||
    "MedianCombinationComponent", cg.Component, sensor.Sensor
 | 
			
		||||
)
 | 
			
		||||
MinimumCombinationComponent = combination_ns.class_(
 | 
			
		||||
    "MinimumCombinationComponent", cg.Component, sensor.Sensor
 | 
			
		||||
)
 | 
			
		||||
MostRecentCombinationComponent = combination_ns.class_(
 | 
			
		||||
    "MostRecentCombinationComponent", cg.Component, sensor.Sensor
 | 
			
		||||
)
 | 
			
		||||
RangeCombinationComponent = combination_ns.class_(
 | 
			
		||||
    "RangeCombinationComponent", cg.Component, sensor.Sensor
 | 
			
		||||
)
 | 
			
		||||
SumCombinationComponent = combination_ns.class_(
 | 
			
		||||
    "SumCombinationComponent", cg.Component, sensor.Sensor
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONF_COEFFECIENT = "coeffecient"
 | 
			
		||||
CONF_ERROR = "error"
 | 
			
		||||
CONF_KALMAN = "kalman"
 | 
			
		||||
CONF_LINEAR = "linear"
 | 
			
		||||
CONF_MAX = "max"
 | 
			
		||||
CONF_MEAN = "mean"
 | 
			
		||||
CONF_MEDIAN = "median"
 | 
			
		||||
CONF_MIN = "min"
 | 
			
		||||
CONF_MOST_RECENTLY_UPDATED = "most_recently_updated"
 | 
			
		||||
CONF_PROCESS_STD_DEV = "process_std_dev"
 | 
			
		||||
CONF_SOURCES = "sources"
 | 
			
		||||
CONF_STD_DEV = "std_dev"
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
KALMAN_SOURCE_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor),
 | 
			
		||||
        cv.Required(CONF_ERROR): cv.templatable(cv.positive_float),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
LINEAR_SOURCE_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor),
 | 
			
		||||
        cv.Required(CONF_COEFFECIENT): cv.templatable(cv.float_),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
SENSOR_ONLY_SOURCE_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.Required(CONF_SOURCE): cv.use_id(sensor.Sensor),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = cv.typed_schema(
 | 
			
		||||
    {
 | 
			
		||||
        CONF_KALMAN: sensor.sensor_schema(KalmanCombinationComponent)
 | 
			
		||||
        .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
        .extend(
 | 
			
		||||
            {
 | 
			
		||||
                cv.Required(CONF_PROCESS_STD_DEV): cv.positive_float,
 | 
			
		||||
                cv.Required(CONF_SOURCES): cv.ensure_list(KALMAN_SOURCE_SCHEMA),
 | 
			
		||||
                cv.Optional(CONF_STD_DEV): sensor.sensor_schema(),
 | 
			
		||||
            }
 | 
			
		||||
        ),
 | 
			
		||||
        CONF_LINEAR: sensor.sensor_schema(LinearCombinationComponent)
 | 
			
		||||
        .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
        .extend({cv.Required(CONF_SOURCES): cv.ensure_list(LINEAR_SOURCE_SCHEMA)}),
 | 
			
		||||
        CONF_MAX: sensor.sensor_schema(MaximumCombinationComponent)
 | 
			
		||||
        .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
        .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}),
 | 
			
		||||
        CONF_MEAN: sensor.sensor_schema(MeanCombinationComponent)
 | 
			
		||||
        .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
        .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}),
 | 
			
		||||
        CONF_MEDIAN: sensor.sensor_schema(MedianCombinationComponent)
 | 
			
		||||
        .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
        .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}),
 | 
			
		||||
        CONF_MIN: sensor.sensor_schema(MinimumCombinationComponent)
 | 
			
		||||
        .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
        .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}),
 | 
			
		||||
        CONF_MOST_RECENTLY_UPDATED: sensor.sensor_schema(MostRecentCombinationComponent)
 | 
			
		||||
        .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
        .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}),
 | 
			
		||||
        CONF_RANGE: sensor.sensor_schema(RangeCombinationComponent)
 | 
			
		||||
        .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
        .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}),
 | 
			
		||||
        CONF_SUM: sensor.sensor_schema(SumCombinationComponent)
 | 
			
		||||
        .extend(cv.COMPONENT_SCHEMA)
 | 
			
		||||
        .extend({cv.Required(CONF_SOURCES): cv.ensure_list(SENSOR_ONLY_SOURCE_SCHEMA)}),
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
# Inherit some sensor values from the first source, for both the state and the error value
 | 
			
		||||
# CONF_STATE_CLASS could also be inherited, but might lead to unexpected behaviour with "total_increasing"
 | 
			
		||||
properties_to_inherit = [
 | 
			
		||||
    CONF_ACCURACY_DECIMALS,
 | 
			
		||||
    CONF_DEVICE_CLASS,
 | 
			
		||||
    CONF_ENTITY_CATEGORY,
 | 
			
		||||
    CONF_ICON,
 | 
			
		||||
    CONF_UNIT_OF_MEASUREMENT,
 | 
			
		||||
]
 | 
			
		||||
inherit_schema_for_state = [
 | 
			
		||||
    inherit_property_from(property, [CONF_SOURCES, 0, CONF_SOURCE])
 | 
			
		||||
    for property in properties_to_inherit
 | 
			
		||||
]
 | 
			
		||||
inherit_schema_for_std_dev = [
 | 
			
		||||
    inherit_property_from([CONF_STD_DEV, property], [CONF_SOURCES, 0, CONF_SOURCE])
 | 
			
		||||
    for property in properties_to_inherit
 | 
			
		||||
]
 | 
			
		||||
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = cv.All(
 | 
			
		||||
    *inherit_schema_for_state,
 | 
			
		||||
    *inherit_schema_for_std_dev,
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
async def to_code(config):
 | 
			
		||||
    var = cg.new_Pvariable(config[CONF_ID])
 | 
			
		||||
    await cg.register_component(var, config)
 | 
			
		||||
    await sensor.register_sensor(var, config)
 | 
			
		||||
 | 
			
		||||
    if proces_std_dev := config.get(CONF_PROCESS_STD_DEV):
 | 
			
		||||
        cg.add(var.set_process_std_dev(proces_std_dev))
 | 
			
		||||
 | 
			
		||||
    for source_conf in config[CONF_SOURCES]:
 | 
			
		||||
        source = await cg.get_variable(source_conf[CONF_SOURCE])
 | 
			
		||||
        if config[CONF_TYPE] == CONF_KALMAN:
 | 
			
		||||
            error = await cg.templatable(
 | 
			
		||||
                source_conf[CONF_ERROR],
 | 
			
		||||
                [(float, "x")],
 | 
			
		||||
                cg.float_,
 | 
			
		||||
            )
 | 
			
		||||
            cg.add(var.add_source(source, error))
 | 
			
		||||
        elif config[CONF_TYPE] == CONF_LINEAR:
 | 
			
		||||
            coeffecient = await cg.templatable(
 | 
			
		||||
                source_conf[CONF_COEFFECIENT],
 | 
			
		||||
                [(float, "x")],
 | 
			
		||||
                cg.float_,
 | 
			
		||||
            )
 | 
			
		||||
            cg.add(var.add_source(source, coeffecient))
 | 
			
		||||
        else:
 | 
			
		||||
            cg.add(var.add_source(source))
 | 
			
		||||
 | 
			
		||||
    if CONF_STD_DEV in config:
 | 
			
		||||
        sens = await sensor.new_sensor(config[CONF_STD_DEV])
 | 
			
		||||
        cg.add(var.set_std_dev_sensor(sens))
 | 
			
		||||
@@ -1,6 +1,8 @@
 | 
			
		||||
#include "cse7766.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
#include <cinttypes>
 | 
			
		||||
#include <iomanip>
 | 
			
		||||
#include <sstream>
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace cse7766 {
 | 
			
		||||
@@ -68,20 +70,26 @@ bool CSE7766Component::check_byte_() {
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
void CSE7766Component::parse_data_() {
 | 
			
		||||
  ESP_LOGVV(TAG, "CSE7766 Data: ");
 | 
			
		||||
  for (uint8_t i = 0; i < 23; i++) {
 | 
			
		||||
    ESP_LOGVV(TAG, "  %u: 0b" BYTE_TO_BINARY_PATTERN " (0x%02X)", i + 1, BYTE_TO_BINARY(this->raw_data_[i]),
 | 
			
		||||
              this->raw_data_[i]);
 | 
			
		||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
 | 
			
		||||
  {
 | 
			
		||||
    std::stringstream ss;
 | 
			
		||||
    ss << "Raw data:" << std::hex << std::uppercase << std::setfill('0');
 | 
			
		||||
    for (uint8_t i = 0; i < 23; i++) {
 | 
			
		||||
      ss << ' ' << std::setw(2) << static_cast<unsigned>(this->raw_data_[i]);
 | 
			
		||||
    }
 | 
			
		||||
    ESP_LOGVV(TAG, "%s", ss.str().c_str());
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
 | 
			
		||||
  // Parse header
 | 
			
		||||
  uint8_t header1 = this->raw_data_[0];
 | 
			
		||||
 | 
			
		||||
  if (header1 == 0xAA) {
 | 
			
		||||
    ESP_LOGE(TAG, "CSE7766 not calibrated!");
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool power_cycle_exceeds_range = false;
 | 
			
		||||
 | 
			
		||||
  if ((header1 & 0xF0) == 0xF0) {
 | 
			
		||||
    if (header1 & 0xD) {
 | 
			
		||||
      ESP_LOGE(TAG, "CSE7766 reports abnormal external circuit or chip damage: (0x%02X)", header1);
 | 
			
		||||
@@ -94,99 +102,106 @@ void CSE7766Component::parse_data_() {
 | 
			
		||||
      if (header1 & (1 << 0)) {
 | 
			
		||||
        ESP_LOGE(TAG, "  Coefficient storage area is abnormal.");
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      // Datasheet: voltage or current cycle exceeding range means invalid values
 | 
			
		||||
      return;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    power_cycle_exceeds_range = header1 & (1 << 1);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint32_t voltage_calib = this->get_24_bit_uint_(2);
 | 
			
		||||
  // Parse data frame
 | 
			
		||||
  uint32_t voltage_coeff = this->get_24_bit_uint_(2);
 | 
			
		||||
  uint32_t voltage_cycle = this->get_24_bit_uint_(5);
 | 
			
		||||
  uint32_t current_calib = this->get_24_bit_uint_(8);
 | 
			
		||||
  uint32_t current_coeff = this->get_24_bit_uint_(8);
 | 
			
		||||
  uint32_t current_cycle = this->get_24_bit_uint_(11);
 | 
			
		||||
  uint32_t power_calib = this->get_24_bit_uint_(14);
 | 
			
		||||
  uint32_t power_coeff = this->get_24_bit_uint_(14);
 | 
			
		||||
  uint32_t power_cycle = this->get_24_bit_uint_(17);
 | 
			
		||||
 | 
			
		||||
  uint8_t adj = this->raw_data_[20];
 | 
			
		||||
  uint32_t cf_pulses = (this->raw_data_[21] << 8) + this->raw_data_[22];
 | 
			
		||||
 | 
			
		||||
  bool have_power = adj & 0x10;
 | 
			
		||||
  bool have_current = adj & 0x20;
 | 
			
		||||
  bool have_voltage = adj & 0x40;
 | 
			
		||||
 | 
			
		||||
  float voltage = 0.0f;
 | 
			
		||||
  if (have_voltage) {
 | 
			
		||||
    // voltage cycle of serial port outputted is a complete cycle;
 | 
			
		||||
    this->voltage_acc_ += voltage_calib / float(voltage_cycle);
 | 
			
		||||
    this->voltage_counts_ += 1;
 | 
			
		||||
    voltage = voltage_coeff / float(voltage_cycle);
 | 
			
		||||
    if (this->voltage_sensor_ != nullptr) {
 | 
			
		||||
      this->voltage_sensor_->publish_state(voltage);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  bool have_power = adj & 0x10;
 | 
			
		||||
  float power = 0.0f;
 | 
			
		||||
 | 
			
		||||
  if (have_power) {
 | 
			
		||||
    // power cycle of serial port outputted is a complete cycle;
 | 
			
		||||
    // According to the user manual, power cycle exceeding range means the measured power is 0
 | 
			
		||||
    if (!power_cycle_exceeds_range) {
 | 
			
		||||
      power = power_calib / float(power_cycle);
 | 
			
		||||
  float energy = 0.0f;
 | 
			
		||||
  if (power_cycle_exceeds_range) {
 | 
			
		||||
    // Datasheet: power cycle exceeding range means active power is 0
 | 
			
		||||
    if (this->power_sensor_ != nullptr) {
 | 
			
		||||
      this->power_sensor_->publish_state(0.0f);
 | 
			
		||||
    }
 | 
			
		||||
  } else if (have_power) {
 | 
			
		||||
    power = power_coeff / float(power_cycle);
 | 
			
		||||
    if (this->power_sensor_ != nullptr) {
 | 
			
		||||
      this->power_sensor_->publish_state(power);
 | 
			
		||||
    }
 | 
			
		||||
    this->power_acc_ += power;
 | 
			
		||||
    this->power_counts_ += 1;
 | 
			
		||||
 | 
			
		||||
    uint32_t difference;
 | 
			
		||||
    // Add CF pulses to the total energy only if we have Power coefficient to multiply by
 | 
			
		||||
 | 
			
		||||
    if (this->cf_pulses_last_ == 0) {
 | 
			
		||||
      this->cf_pulses_last_ = cf_pulses;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    uint32_t cf_diff;
 | 
			
		||||
    if (cf_pulses < this->cf_pulses_last_) {
 | 
			
		||||
      difference = cf_pulses + (0x10000 - this->cf_pulses_last_);
 | 
			
		||||
      cf_diff = cf_pulses + (0x10000 - this->cf_pulses_last_);
 | 
			
		||||
    } else {
 | 
			
		||||
      difference = cf_pulses - this->cf_pulses_last_;
 | 
			
		||||
      cf_diff = cf_pulses - this->cf_pulses_last_;
 | 
			
		||||
    }
 | 
			
		||||
    this->cf_pulses_last_ = cf_pulses;
 | 
			
		||||
    this->energy_total_ += difference * float(power_calib) / 1000000.0f / 3600.0f;
 | 
			
		||||
    this->energy_total_counts_ += 1;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (adj & 0x20) {
 | 
			
		||||
    // indicates current cycle of serial port outputted is a complete cycle;
 | 
			
		||||
    float current = 0.0f;
 | 
			
		||||
    if (have_voltage && !have_power) {
 | 
			
		||||
      // Testing has shown that when we have voltage and current but not power, that means the power is 0.
 | 
			
		||||
      // We report a power of 0, which in turn means we should report a current of 0.
 | 
			
		||||
      this->power_counts_ += 1;
 | 
			
		||||
    } else if (power != 0.0f) {
 | 
			
		||||
      current = current_calib / float(current_cycle);
 | 
			
		||||
    }
 | 
			
		||||
    this->current_acc_ += current;
 | 
			
		||||
    this->current_counts_ += 1;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void CSE7766Component::update() {
 | 
			
		||||
  const auto publish_state = [](const char *name, sensor::Sensor *sensor, float &acc, uint32_t &counts) {
 | 
			
		||||
    if (counts != 0) {
 | 
			
		||||
      const auto avg = acc / counts;
 | 
			
		||||
 | 
			
		||||
      ESP_LOGV(TAG, "Got %s_acc=%.2f %s_counts=%" PRIu32 " %s=%.1f", name, acc, name, counts, name, avg);
 | 
			
		||||
 | 
			
		||||
      if (sensor != nullptr) {
 | 
			
		||||
        sensor->publish_state(avg);
 | 
			
		||||
      }
 | 
			
		||||
 | 
			
		||||
      acc = 0.0f;
 | 
			
		||||
      counts = 0;
 | 
			
		||||
    }
 | 
			
		||||
  };
 | 
			
		||||
 | 
			
		||||
  publish_state("voltage", this->voltage_sensor_, this->voltage_acc_, this->voltage_counts_);
 | 
			
		||||
  publish_state("current", this->current_sensor_, this->current_acc_, this->current_counts_);
 | 
			
		||||
  publish_state("power", this->power_sensor_, this->power_acc_, this->power_counts_);
 | 
			
		||||
 | 
			
		||||
  if (this->energy_total_counts_ != 0) {
 | 
			
		||||
    ESP_LOGV(TAG, "Got energy_total=%.2f energy_total_counts=%" PRIu32, this->energy_total_,
 | 
			
		||||
             this->energy_total_counts_);
 | 
			
		||||
 | 
			
		||||
    if (this->energy_sensor_ != nullptr) {
 | 
			
		||||
    energy = cf_diff * float(power_coeff) / 1000000.0f / 3600.0f;
 | 
			
		||||
    this->energy_total_ += energy;
 | 
			
		||||
    if (this->energy_sensor_ != nullptr)
 | 
			
		||||
      this->energy_sensor_->publish_state(this->energy_total_);
 | 
			
		||||
    }
 | 
			
		||||
    this->energy_total_counts_ = 0;
 | 
			
		||||
  } else if ((this->energy_sensor_ != nullptr) && !this->energy_sensor_->has_state()) {
 | 
			
		||||
    this->energy_sensor_->publish_state(0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  float current = 0.0f;
 | 
			
		||||
  float calculated_current = 0.0f;
 | 
			
		||||
  if (have_current) {
 | 
			
		||||
    // Assumption: if we don't have power measurement, then current is likely below 50mA
 | 
			
		||||
    if (have_power && voltage > 1.0f) {
 | 
			
		||||
      calculated_current = power / voltage;
 | 
			
		||||
    }
 | 
			
		||||
    // Datasheet: minimum measured current is 50mA
 | 
			
		||||
    if (calculated_current > 0.05f) {
 | 
			
		||||
      current = current_coeff / float(current_cycle);
 | 
			
		||||
    }
 | 
			
		||||
    if (this->current_sensor_ != nullptr) {
 | 
			
		||||
      this->current_sensor_->publish_state(current);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
#if ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_VERY_VERBOSE
 | 
			
		||||
  {
 | 
			
		||||
    std::stringstream ss;
 | 
			
		||||
    ss << "Parsed:";
 | 
			
		||||
    if (have_voltage) {
 | 
			
		||||
      ss << " V=" << voltage << "V";
 | 
			
		||||
    }
 | 
			
		||||
    if (have_current) {
 | 
			
		||||
      ss << " I=" << current * 1000.0f << "mA (~" << calculated_current * 1000.0f << "mA)";
 | 
			
		||||
    }
 | 
			
		||||
    if (have_power) {
 | 
			
		||||
      ss << " P=" << power << "W";
 | 
			
		||||
    }
 | 
			
		||||
    if (energy != 0.0f) {
 | 
			
		||||
      ss << " E=" << energy << "kWh (" << cf_pulses << ")";
 | 
			
		||||
    }
 | 
			
		||||
    ESP_LOGVV(TAG, "%s", ss.str().c_str());
 | 
			
		||||
  }
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint32_t CSE7766Component::get_24_bit_uint_(uint8_t start_index) {
 | 
			
		||||
@@ -196,7 +211,6 @@ uint32_t CSE7766Component::get_24_bit_uint_(uint8_t start_index) {
 | 
			
		||||
 | 
			
		||||
void CSE7766Component::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "CSE7766:");
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
  LOG_SENSOR("  ", "Voltage", this->voltage_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Current", this->current_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Power", this->power_sensor_);
 | 
			
		||||
 
 | 
			
		||||
@@ -7,7 +7,7 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace cse7766 {
 | 
			
		||||
 | 
			
		||||
class CSE7766Component : public PollingComponent, public uart::UARTDevice {
 | 
			
		||||
class CSE7766Component : public Component, public uart::UARTDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  void set_voltage_sensor(sensor::Sensor *voltage_sensor) { voltage_sensor_ = voltage_sensor; }
 | 
			
		||||
  void set_current_sensor(sensor::Sensor *current_sensor) { current_sensor_ = current_sensor; }
 | 
			
		||||
@@ -16,7 +16,6 @@ class CSE7766Component : public PollingComponent, public uart::UARTDevice {
 | 
			
		||||
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
@@ -31,16 +30,8 @@ class CSE7766Component : public PollingComponent, public uart::UARTDevice {
 | 
			
		||||
  sensor::Sensor *current_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *power_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *energy_sensor_{nullptr};
 | 
			
		||||
  float voltage_acc_{0.0f};
 | 
			
		||||
  float current_acc_{0.0f};
 | 
			
		||||
  float power_acc_{0.0f};
 | 
			
		||||
  float energy_total_{0.0f};
 | 
			
		||||
  uint32_t cf_pulses_last_{0};
 | 
			
		||||
  uint32_t voltage_counts_{0};
 | 
			
		||||
  uint32_t current_counts_{0};
 | 
			
		||||
  uint32_t power_counts_{0};
 | 
			
		||||
  // Setting this to 1 means it will always publish 0 once at startup
 | 
			
		||||
  uint32_t energy_total_counts_{1};
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace cse7766
 | 
			
		||||
 
 | 
			
		||||
@@ -22,43 +22,37 @@ from esphome.const import (
 | 
			
		||||
DEPENDENCIES = ["uart"]
 | 
			
		||||
 | 
			
		||||
cse7766_ns = cg.esphome_ns.namespace("cse7766")
 | 
			
		||||
CSE7766Component = cse7766_ns.class_(
 | 
			
		||||
    "CSE7766Component", cg.PollingComponent, uart.UARTDevice
 | 
			
		||||
)
 | 
			
		||||
CSE7766Component = cse7766_ns.class_("CSE7766Component", cg.Component, uart.UARTDevice)
 | 
			
		||||
 | 
			
		||||
CONFIG_SCHEMA = (
 | 
			
		||||
    cv.Schema(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(): cv.declare_id(CSE7766Component),
 | 
			
		||||
            cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
                device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_CURRENT): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_AMPERE,
 | 
			
		||||
                accuracy_decimals=2,
 | 
			
		||||
                device_class=DEVICE_CLASS_CURRENT,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_POWER): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_WATT,
 | 
			
		||||
                accuracy_decimals=1,
 | 
			
		||||
                device_class=DEVICE_CLASS_POWER,
 | 
			
		||||
                state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ENERGY): sensor.sensor_schema(
 | 
			
		||||
                unit_of_measurement=UNIT_WATT_HOURS,
 | 
			
		||||
                accuracy_decimals=3,
 | 
			
		||||
                device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
                state_class=STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
            ),
 | 
			
		||||
        }
 | 
			
		||||
    )
 | 
			
		||||
    .extend(cv.polling_component_schema("60s"))
 | 
			
		||||
    .extend(uart.UART_DEVICE_SCHEMA)
 | 
			
		||||
)
 | 
			
		||||
CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
        cv.GenerateID(): cv.declare_id(CSE7766Component),
 | 
			
		||||
        cv.Optional(CONF_VOLTAGE): sensor.sensor_schema(
 | 
			
		||||
            unit_of_measurement=UNIT_VOLT,
 | 
			
		||||
            accuracy_decimals=1,
 | 
			
		||||
            device_class=DEVICE_CLASS_VOLTAGE,
 | 
			
		||||
            state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_CURRENT): sensor.sensor_schema(
 | 
			
		||||
            unit_of_measurement=UNIT_AMPERE,
 | 
			
		||||
            accuracy_decimals=2,
 | 
			
		||||
            device_class=DEVICE_CLASS_CURRENT,
 | 
			
		||||
            state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_POWER): sensor.sensor_schema(
 | 
			
		||||
            unit_of_measurement=UNIT_WATT,
 | 
			
		||||
            accuracy_decimals=1,
 | 
			
		||||
            device_class=DEVICE_CLASS_POWER,
 | 
			
		||||
            state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_ENERGY): sensor.sensor_schema(
 | 
			
		||||
            unit_of_measurement=UNIT_WATT_HOURS,
 | 
			
		||||
            accuracy_decimals=3,
 | 
			
		||||
            device_class=DEVICE_CLASS_ENERGY,
 | 
			
		||||
            state_class=STATE_CLASS_TOTAL_INCREASING,
 | 
			
		||||
        ),
 | 
			
		||||
    }
 | 
			
		||||
).extend(uart.UART_DEVICE_SCHEMA)
 | 
			
		||||
FINAL_VALIDATE_SCHEMA = uart.final_validate_device_schema(
 | 
			
		||||
    "cse7766", baud_rate=4800, require_rx=True
 | 
			
		||||
)
 | 
			
		||||
 
 | 
			
		||||
@@ -168,10 +168,6 @@ bool IRAM_ATTR DallasTemperatureSensor::read_scratch_pad() {
 | 
			
		||||
    if (!wire->reset()) {
 | 
			
		||||
      return false;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  {
 | 
			
		||||
    InterruptLock lock;
 | 
			
		||||
 | 
			
		||||
    wire->select(this->address_);
 | 
			
		||||
    wire->write8(DALLAS_COMMAND_READ_SCRATCH_PAD);
 | 
			
		||||
 
 | 
			
		||||
@@ -1,7 +1,6 @@
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome import automation
 | 
			
		||||
from esphome import core
 | 
			
		||||
from esphome.automation import maybe_simple_id
 | 
			
		||||
from esphome.const import CONF_ID
 | 
			
		||||
from esphome.components import uart
 | 
			
		||||
@@ -101,7 +100,7 @@ def range_segment_list(input):
 | 
			
		||||
 | 
			
		||||
    largest_distance = -1
 | 
			
		||||
    for distance in input:
 | 
			
		||||
        if isinstance(distance, core.Lambda):
 | 
			
		||||
        if isinstance(distance, cv.Lambda):
 | 
			
		||||
            continue
 | 
			
		||||
        m = cv.distance(distance)
 | 
			
		||||
        if m > 9:
 | 
			
		||||
@@ -128,14 +127,14 @@ MMWAVE_SETTINGS_SCHEMA = cv.Schema(
 | 
			
		||||
        cv.Optional(CONF_OUTPUT_LATENCY): {
 | 
			
		||||
            cv.Required(CONF_DELAY_AFTER_DETECT): cv.templatable(
 | 
			
		||||
                cv.All(
 | 
			
		||||
                    cv.positive_time_period,
 | 
			
		||||
                    cv.Range(max=core.TimePeriod(seconds=1638.375)),
 | 
			
		||||
                    cv.positive_time_period_milliseconds,
 | 
			
		||||
                    cv.Range(max=cv.TimePeriod(seconds=1638.375)),
 | 
			
		||||
                )
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Required(CONF_DELAY_AFTER_DISAPPEAR): cv.templatable(
 | 
			
		||||
                cv.All(
 | 
			
		||||
                    cv.positive_time_period,
 | 
			
		||||
                    cv.Range(max=core.TimePeriod(seconds=1638.375)),
 | 
			
		||||
                    cv.positive_time_period_milliseconds,
 | 
			
		||||
                    cv.Range(max=cv.TimePeriod(seconds=1638.375)),
 | 
			
		||||
                )
 | 
			
		||||
            ),
 | 
			
		||||
        },
 | 
			
		||||
 
 | 
			
		||||
@@ -50,7 +50,7 @@ class DfrobotSen0395SettingsAction : public Action<Ts...>, public Parented<Dfrob
 | 
			
		||||
      float detect = this->delay_after_detect_.value(x...);
 | 
			
		||||
      float disappear = this->delay_after_disappear_.value(x...);
 | 
			
		||||
      if (detect >= 0 && disappear >= 0) {
 | 
			
		||||
        this->parent_->enqueue(make_unique<OutputLatencyCommand>(detect, disappear));
 | 
			
		||||
        this->parent_->enqueue(make_unique<SetLatencyCommand>(detect, disappear));
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
    if (this->start_after_power_on_.has_value()) {
 | 
			
		||||
 
 | 
			
		||||
@@ -1,5 +1,7 @@
 | 
			
		||||
#include "commands.h"
 | 
			
		||||
 | 
			
		||||
#include <cmath>
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
#include "dfrobot_sen0395.h"
 | 
			
		||||
@@ -194,32 +196,22 @@ uint8_t DetRangeCfgCommand::on_message(std::string &message) {
 | 
			
		||||
  return 0;  // Command not done yet.
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
OutputLatencyCommand::OutputLatencyCommand(float delay_after_detection, float delay_after_disappear) {
 | 
			
		||||
  delay_after_detection = round(delay_after_detection / 0.025) * 0.025;
 | 
			
		||||
  delay_after_disappear = round(delay_after_disappear / 0.025) * 0.025;
 | 
			
		||||
  if (delay_after_detection < 0)
 | 
			
		||||
    delay_after_detection = 0;
 | 
			
		||||
  if (delay_after_detection > 1638.375)
 | 
			
		||||
    delay_after_detection = 1638.375;
 | 
			
		||||
  if (delay_after_disappear < 0)
 | 
			
		||||
    delay_after_disappear = 0;
 | 
			
		||||
  if (delay_after_disappear > 1638.375)
 | 
			
		||||
    delay_after_disappear = 1638.375;
 | 
			
		||||
 | 
			
		||||
  this->delay_after_detection_ = delay_after_detection;
 | 
			
		||||
  this->delay_after_disappear_ = delay_after_disappear;
 | 
			
		||||
 | 
			
		||||
  this->cmd_ = str_sprintf("outputLatency -1 %.0f %.0f", delay_after_detection / 0.025, delay_after_disappear / 0.025);
 | 
			
		||||
SetLatencyCommand::SetLatencyCommand(float delay_after_detection, float delay_after_disappear) {
 | 
			
		||||
  delay_after_detection = std::round(delay_after_detection / 0.025f) * 0.025f;
 | 
			
		||||
  delay_after_disappear = std::round(delay_after_disappear / 0.025f) * 0.025f;
 | 
			
		||||
  this->delay_after_detection_ = clamp(delay_after_detection, 0.0f, 1638.375f);
 | 
			
		||||
  this->delay_after_disappear_ = clamp(delay_after_disappear, 0.0f, 1638.375f);
 | 
			
		||||
  this->cmd_ = str_sprintf("setLatency %.03f %.03f", this->delay_after_detection_, this->delay_after_disappear_);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
uint8_t OutputLatencyCommand::on_message(std::string &message) {
 | 
			
		||||
uint8_t SetLatencyCommand::on_message(std::string &message) {
 | 
			
		||||
  if (message == "sensor is not stopped") {
 | 
			
		||||
    ESP_LOGE(TAG, "Cannot configure output latency. Sensor is not stopped!");
 | 
			
		||||
    return 1;  // Command done
 | 
			
		||||
  } else if (message == "Done") {
 | 
			
		||||
    ESP_LOGI(TAG, "Updated output latency config:");
 | 
			
		||||
    ESP_LOGI(TAG, "Signal that someone was detected is delayed by %.02fs.", this->delay_after_detection_);
 | 
			
		||||
    ESP_LOGI(TAG, "Signal that nobody is detected anymore is delayed by %.02fs.", this->delay_after_disappear_);
 | 
			
		||||
    ESP_LOGI(TAG, "Signal that someone was detected is delayed by %.03f s.", this->delay_after_detection_);
 | 
			
		||||
    ESP_LOGI(TAG, "Signal that nobody is detected anymore is delayed by %.03f s.", this->delay_after_disappear_);
 | 
			
		||||
    ESP_LOGD(TAG, "Used command: %s", this->cmd_.c_str());
 | 
			
		||||
    return 1;  // Command done
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -62,9 +62,9 @@ class DetRangeCfgCommand : public Command {
 | 
			
		||||
  // TODO: Set min max values in component, so they can be published as sensor.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class OutputLatencyCommand : public Command {
 | 
			
		||||
class SetLatencyCommand : public Command {
 | 
			
		||||
 public:
 | 
			
		||||
  OutputLatencyCommand(float delay_after_detection, float delay_after_disappear);
 | 
			
		||||
  SetLatencyCommand(float delay_after_detection, float delay_after_disappear);
 | 
			
		||||
  uint8_t on_message(std::string &message) override;
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
 
 | 
			
		||||
@@ -91,7 +91,7 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
 | 
			
		||||
    delayMicroseconds(40);
 | 
			
		||||
  } else if (this->model_ == DHT_MODEL_DHT22_TYPE2) {
 | 
			
		||||
    delayMicroseconds(2000);
 | 
			
		||||
  } else if (this->model_ == DHT_MODEL_AM2302) {
 | 
			
		||||
  } else if (this->model_ == DHT_MODEL_AM2120 || this->model_ == DHT_MODEL_AM2302) {
 | 
			
		||||
    delayMicroseconds(1000);
 | 
			
		||||
  } else {
 | 
			
		||||
    delayMicroseconds(800);
 | 
			
		||||
@@ -217,8 +217,12 @@ bool HOT IRAM_ATTR DHT::read_sensor_(float *temperature, float *humidity, bool r
 | 
			
		||||
    uint16_t raw_humidity = (uint16_t(data[0] & 0xFF) << 8) | (data[1] & 0xFF);
 | 
			
		||||
    uint16_t raw_temperature = (uint16_t(data[2] & 0xFF) << 8) | (data[3] & 0xFF);
 | 
			
		||||
 | 
			
		||||
    if (this->model_ != DHT_MODEL_DHT22_TYPE2 && (raw_temperature & 0x8000) != 0)
 | 
			
		||||
      raw_temperature = ~(raw_temperature & 0x7FFF);
 | 
			
		||||
    if (raw_temperature & 0x8000) {
 | 
			
		||||
      if (!(raw_temperature & 0x4000))
 | 
			
		||||
        raw_temperature = ~(raw_temperature & 0x7FFF);
 | 
			
		||||
    } else if (raw_temperature & 0x800) {
 | 
			
		||||
      raw_temperature |= 0xf000;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    if (raw_temperature == 1 && raw_humidity == 10) {
 | 
			
		||||
      if (report_errors) {
 | 
			
		||||
 
 | 
			
		||||
@@ -11,6 +11,7 @@ enum DHTModel {
 | 
			
		||||
  DHT_MODEL_AUTO_DETECT = 0,
 | 
			
		||||
  DHT_MODEL_DHT11,
 | 
			
		||||
  DHT_MODEL_DHT22,
 | 
			
		||||
  DHT_MODEL_AM2120,
 | 
			
		||||
  DHT_MODEL_AM2302,
 | 
			
		||||
  DHT_MODEL_RHT03,
 | 
			
		||||
  DHT_MODEL_SI7021,
 | 
			
		||||
@@ -27,6 +28,7 @@ class DHT : public PollingComponent {
 | 
			
		||||
   *  - DHT_MODEL_AUTO_DETECT (default)
 | 
			
		||||
   *  - DHT_MODEL_DHT11
 | 
			
		||||
   *  - DHT_MODEL_DHT22
 | 
			
		||||
   *  - DHT_MODEL_AM2120
 | 
			
		||||
   *  - DHT_MODEL_AM2302
 | 
			
		||||
   *  - DHT_MODEL_RHT03
 | 
			
		||||
   *  - DHT_MODEL_SI7021
 | 
			
		||||
 
 | 
			
		||||
@@ -23,6 +23,7 @@ DHT_MODELS = {
 | 
			
		||||
    "AUTO_DETECT": DHTModel.DHT_MODEL_AUTO_DETECT,
 | 
			
		||||
    "DHT11": DHTModel.DHT_MODEL_DHT11,
 | 
			
		||||
    "DHT22": DHTModel.DHT_MODEL_DHT22,
 | 
			
		||||
    "AM2120": DHTModel.DHT_MODEL_AM2120,
 | 
			
		||||
    "AM2302": DHTModel.DHT_MODEL_AM2302,
 | 
			
		||||
    "RHT03": DHTModel.DHT_MODEL_RHT03,
 | 
			
		||||
    "SI7021": DHTModel.DHT_MODEL_SI7021,
 | 
			
		||||
 
 | 
			
		||||
@@ -18,8 +18,8 @@ from esphome.core import coroutine_with_priority
 | 
			
		||||
IS_PLATFORM_COMPONENT = True
 | 
			
		||||
 | 
			
		||||
display_ns = cg.esphome_ns.namespace("display")
 | 
			
		||||
Display = display_ns.class_("Display")
 | 
			
		||||
DisplayBuffer = display_ns.class_("DisplayBuffer")
 | 
			
		||||
Display = display_ns.class_("Display", cg.PollingComponent)
 | 
			
		||||
DisplayBuffer = display_ns.class_("DisplayBuffer", Display)
 | 
			
		||||
DisplayPage = display_ns.class_("DisplayPage")
 | 
			
		||||
DisplayPagePtr = DisplayPage.operator("ptr")
 | 
			
		||||
DisplayRef = Display.operator("ref")
 | 
			
		||||
@@ -145,7 +145,7 @@ async def display_page_show_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    DisplayPageShowNextAction,
 | 
			
		||||
    maybe_simple_id(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_ID): cv.templatable(cv.use_id(DisplayBuffer)),
 | 
			
		||||
            cv.Required(CONF_ID): cv.templatable(cv.use_id(Display)),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
@@ -159,7 +159,7 @@ async def display_page_show_next_to_code(config, action_id, template_arg, args):
 | 
			
		||||
    DisplayPageShowPrevAction,
 | 
			
		||||
    maybe_simple_id(
 | 
			
		||||
        {
 | 
			
		||||
            cv.Required(CONF_ID): cv.templatable(cv.use_id(DisplayBuffer)),
 | 
			
		||||
            cv.Required(CONF_ID): cv.templatable(cv.use_id(Display)),
 | 
			
		||||
        }
 | 
			
		||||
    ),
 | 
			
		||||
)
 | 
			
		||||
@@ -173,7 +173,7 @@ async def display_page_show_previous_to_code(config, action_id, template_arg, ar
 | 
			
		||||
    DisplayIsDisplayingPageCondition,
 | 
			
		||||
    cv.maybe_simple_value(
 | 
			
		||||
        {
 | 
			
		||||
            cv.GenerateID(CONF_ID): cv.use_id(DisplayBuffer),
 | 
			
		||||
            cv.GenerateID(CONF_ID): cv.use_id(Display),
 | 
			
		||||
            cv.Required(CONF_PAGE_ID): cv.use_id(DisplayPage),
 | 
			
		||||
        },
 | 
			
		||||
        key=CONF_PAGE_ID,
 | 
			
		||||
 
 | 
			
		||||
@@ -35,6 +35,41 @@ void HOT Display::line(int x1, int y1, int x2, int y2, Color color) {
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Display::draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
 | 
			
		||||
                             ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad) {
 | 
			
		||||
  size_t line_stride = x_offset + w + x_pad;  // length of each source line in pixels
 | 
			
		||||
  uint32_t color_value;
 | 
			
		||||
  for (int y = 0; y != h; y++) {
 | 
			
		||||
    size_t source_idx = (y_offset + y) * line_stride + x_offset;
 | 
			
		||||
    size_t source_idx_mod;
 | 
			
		||||
    for (int x = 0; x != w; x++, source_idx++) {
 | 
			
		||||
      switch (bitness) {
 | 
			
		||||
        default:
 | 
			
		||||
          color_value = ptr[source_idx];
 | 
			
		||||
          break;
 | 
			
		||||
        case COLOR_BITNESS_565:
 | 
			
		||||
          source_idx_mod = source_idx * 2;
 | 
			
		||||
          if (big_endian) {
 | 
			
		||||
            color_value = (ptr[source_idx_mod] << 8) + ptr[source_idx_mod + 1];
 | 
			
		||||
          } else {
 | 
			
		||||
            color_value = ptr[source_idx_mod] + (ptr[source_idx_mod + 1] << 8);
 | 
			
		||||
          }
 | 
			
		||||
          break;
 | 
			
		||||
        case COLOR_BITNESS_888:
 | 
			
		||||
          source_idx_mod = source_idx * 3;
 | 
			
		||||
          if (big_endian) {
 | 
			
		||||
            color_value = (ptr[source_idx_mod + 0] << 16) + (ptr[source_idx_mod + 1] << 8) + ptr[source_idx_mod + 2];
 | 
			
		||||
          } else {
 | 
			
		||||
            color_value = ptr[source_idx_mod + 0] + (ptr[source_idx_mod + 1] << 8) + (ptr[source_idx_mod + 2] << 16);
 | 
			
		||||
          }
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
      this->draw_pixel_at(x + x_start, y + y_start, ColorUtil::to_color(color_value, order, bitness));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HOT Display::horizontal_line(int x, int y, int width, Color color) {
 | 
			
		||||
  // Future: Could be made more efficient by manipulating buffer directly in certain rotations.
 | 
			
		||||
  for (int i = x; i < x + width; i++)
 | 
			
		||||
@@ -106,6 +141,122 @@ void Display::filled_circle(int center_x, int center_y, int radius, Color color)
 | 
			
		||||
    }
 | 
			
		||||
  } while (dx <= 0);
 | 
			
		||||
}
 | 
			
		||||
void HOT Display::triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) {
 | 
			
		||||
  this->line(x1, y1, x2, y2, color);
 | 
			
		||||
  this->line(x1, y1, x3, y3, color);
 | 
			
		||||
  this->line(x2, y2, x3, y3, color);
 | 
			
		||||
}
 | 
			
		||||
void Display::sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3) {
 | 
			
		||||
  if (*y1 > *y2) {
 | 
			
		||||
    int x_temp = *x1, y_temp = *y1;
 | 
			
		||||
    *x1 = *x2, *y1 = *y2;
 | 
			
		||||
    *x2 = x_temp, *y2 = y_temp;
 | 
			
		||||
  }
 | 
			
		||||
  if (*y1 > *y3) {
 | 
			
		||||
    int x_temp = *x1, y_temp = *y1;
 | 
			
		||||
    *x1 = *x3, *y1 = *y3;
 | 
			
		||||
    *x3 = x_temp, *y3 = y_temp;
 | 
			
		||||
  }
 | 
			
		||||
  if (*y2 > *y3) {
 | 
			
		||||
    int x_temp = *x2, y_temp = *y2;
 | 
			
		||||
    *x2 = *x3, *y2 = *y3;
 | 
			
		||||
    *x3 = x_temp, *y3 = y_temp;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void Display::filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int y3, Color color) {
 | 
			
		||||
  // y2 must be equal to y3 (same horizontal line)
 | 
			
		||||
 | 
			
		||||
  // Initialize Bresenham's algorithm for side 1
 | 
			
		||||
  int s1_current_x = x1;
 | 
			
		||||
  int s1_current_y = y1;
 | 
			
		||||
  bool s1_axis_swap = false;
 | 
			
		||||
  int s1_dx = abs(x2 - x1);
 | 
			
		||||
  int s1_dy = abs(y2 - y1);
 | 
			
		||||
  int s1_sign_x = ((x2 - x1) >= 0) ? 1 : -1;
 | 
			
		||||
  int s1_sign_y = ((y2 - y1) >= 0) ? 1 : -1;
 | 
			
		||||
  if (s1_dy > s1_dx) {  // swap values
 | 
			
		||||
    int tmp = s1_dx;
 | 
			
		||||
    s1_dx = s1_dy;
 | 
			
		||||
    s1_dy = tmp;
 | 
			
		||||
    s1_axis_swap = true;
 | 
			
		||||
  }
 | 
			
		||||
  int s1_error = 2 * s1_dy - s1_dx;
 | 
			
		||||
 | 
			
		||||
  // Initialize Bresenham's algorithm for side 2
 | 
			
		||||
  int s2_current_x = x1;
 | 
			
		||||
  int s2_current_y = y1;
 | 
			
		||||
  bool s2_axis_swap = false;
 | 
			
		||||
  int s2_dx = abs(x3 - x1);
 | 
			
		||||
  int s2_dy = abs(y3 - y1);
 | 
			
		||||
  int s2_sign_x = ((x3 - x1) >= 0) ? 1 : -1;
 | 
			
		||||
  int s2_sign_y = ((y3 - y1) >= 0) ? 1 : -1;
 | 
			
		||||
  if (s2_dy > s2_dx) {  // swap values
 | 
			
		||||
    int tmp = s2_dx;
 | 
			
		||||
    s2_dx = s2_dy;
 | 
			
		||||
    s2_dy = tmp;
 | 
			
		||||
    s2_axis_swap = true;
 | 
			
		||||
  }
 | 
			
		||||
  int s2_error = 2 * s2_dy - s2_dx;
 | 
			
		||||
 | 
			
		||||
  // Iterate on side 1 and allow side 2 to be processed to match the advance of the y-axis.
 | 
			
		||||
  for (int i = 0; i <= s1_dx; i++) {
 | 
			
		||||
    if (s1_current_x <= s2_current_x) {
 | 
			
		||||
      this->horizontal_line(s1_current_x, s1_current_y, s2_current_x - s1_current_x + 1, color);
 | 
			
		||||
    } else {
 | 
			
		||||
      this->horizontal_line(s2_current_x, s2_current_y, s1_current_x - s2_current_x + 1, color);
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    // Bresenham's #1
 | 
			
		||||
    // Side 1 s1_current_x and s1_current_y calculation
 | 
			
		||||
    while (s1_error >= 0) {
 | 
			
		||||
      if (s1_axis_swap) {
 | 
			
		||||
        s1_current_x += s1_sign_x;
 | 
			
		||||
      } else {
 | 
			
		||||
        s1_current_y += s1_sign_y;
 | 
			
		||||
      }
 | 
			
		||||
      s1_error = s1_error - 2 * s1_dx;
 | 
			
		||||
    }
 | 
			
		||||
    if (s1_axis_swap) {
 | 
			
		||||
      s1_current_y += s1_sign_y;
 | 
			
		||||
    } else {
 | 
			
		||||
      s1_current_x += s1_sign_x;
 | 
			
		||||
    }
 | 
			
		||||
    s1_error = s1_error + 2 * s1_dy;
 | 
			
		||||
 | 
			
		||||
    // Bresenham's #2
 | 
			
		||||
    // Side 2 s2_current_x and s2_current_y calculation
 | 
			
		||||
    while (s2_current_y != s1_current_y) {
 | 
			
		||||
      while (s2_error >= 0) {
 | 
			
		||||
        if (s2_axis_swap) {
 | 
			
		||||
          s2_current_x += s2_sign_x;
 | 
			
		||||
        } else {
 | 
			
		||||
          s2_current_y += s2_sign_y;
 | 
			
		||||
        }
 | 
			
		||||
        s2_error = s2_error - 2 * s2_dx;
 | 
			
		||||
      }
 | 
			
		||||
      if (s2_axis_swap) {
 | 
			
		||||
        s2_current_y += s2_sign_y;
 | 
			
		||||
      } else {
 | 
			
		||||
        s2_current_x += s2_sign_x;
 | 
			
		||||
      }
 | 
			
		||||
      s2_error = s2_error + 2 * s2_dy;
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
void Display::filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color) {
 | 
			
		||||
  // Sort the three points by y-coordinate ascending, so [x1,y1] is the topmost point
 | 
			
		||||
  this->sort_triangle_points_by_y_(&x1, &y1, &x2, &y2, &x3, &y3);
 | 
			
		||||
 | 
			
		||||
  if (y2 == y3) {  // Check for special case of a bottom-flat triangle
 | 
			
		||||
    this->filled_flat_side_triangle_(x1, y1, x2, y2, x3, y3, color);
 | 
			
		||||
  } else if (y1 == y2) {  // Check for special case of a top-flat triangle
 | 
			
		||||
    this->filled_flat_side_triangle_(x3, y3, x1, y1, x2, y2, color);
 | 
			
		||||
  } else {  // General case: split the no-flat-side triangle in a top-flat triangle and bottom-flat triangle
 | 
			
		||||
    int x_temp = (int) (x1 + ((float) (y2 - y1) / (float) (y3 - y1)) * (x3 - x1)), y_temp = y2;
 | 
			
		||||
    this->filled_flat_side_triangle_(x1, y1, x2, y2, x_temp, y_temp, color);
 | 
			
		||||
    this->filled_flat_side_triangle_(x3, y3, x2, y2, x_temp, y_temp, color);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void Display::print(int x, int y, BaseFont *font, Color color, TextAlign align, const char *text) {
 | 
			
		||||
  int x_start, y_start;
 | 
			
		||||
 
 | 
			
		||||
@@ -8,6 +8,7 @@
 | 
			
		||||
#include "esphome/core/color.h"
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "esphome/core/time.h"
 | 
			
		||||
#include "display_color_utils.h"
 | 
			
		||||
 | 
			
		||||
#ifdef USE_GRAPH
 | 
			
		||||
#include "esphome/components/graph/graph.h"
 | 
			
		||||
@@ -185,6 +186,34 @@ class Display : public PollingComponent {
 | 
			
		||||
  /// Set a single pixel at the specified coordinates to the given color.
 | 
			
		||||
  virtual void draw_pixel_at(int x, int y, Color color) = 0;
 | 
			
		||||
 | 
			
		||||
  /** Given an array of pixels encoded in the nominated format, draw these into the display's buffer.
 | 
			
		||||
   * The naive implementation here will work in all cases, but can be overridden by sub-classes
 | 
			
		||||
   * in order to optimise the procedure.
 | 
			
		||||
   * The parameters describe a rectangular block of pixels, potentially within a larger buffer.
 | 
			
		||||
   *
 | 
			
		||||
   * \param x_start The starting destination x position
 | 
			
		||||
   * \param y_start The starting destination y position
 | 
			
		||||
   * \param w the width of the pixel block
 | 
			
		||||
   * \param h the height of the pixel block
 | 
			
		||||
   * \param ptr A pointer to the start of the data to be copied
 | 
			
		||||
   * \param order The ordering of the colors
 | 
			
		||||
   * \param bitness Defines the number of bits and their format for each pixel
 | 
			
		||||
   * \param big_endian True if 16 bit values are stored big-endian
 | 
			
		||||
   * \param x_offset The initial x-offset into the source buffer.
 | 
			
		||||
   * \param y_offset The initial y-offset into the source buffer.
 | 
			
		||||
   * \param x_pad How many pixels are in each line after the end of the pixels to be copied.
 | 
			
		||||
   *
 | 
			
		||||
   * The length of each source buffer line (stride) will be x_offset + w + x_pad.
 | 
			
		||||
   */
 | 
			
		||||
  virtual void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
 | 
			
		||||
                              ColorBitness bitness, bool big_endian, int x_offset, int y_offset, int x_pad);
 | 
			
		||||
 | 
			
		||||
  /// Convenience overload for base case where the pixels are packed into the buffer with no gaps (e.g. suits LVGL.)
 | 
			
		||||
  void draw_pixels_at(int x_start, int y_start, int w, int h, const uint8_t *ptr, ColorOrder order,
 | 
			
		||||
                      ColorBitness bitness, bool big_endian) {
 | 
			
		||||
    this->draw_pixels_at(x_start, y_start, w, h, ptr, order, bitness, big_endian, 0, 0, 0);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  /// Draw a straight line from the point [x1,y1] to [x2,y2] with the given color.
 | 
			
		||||
  void line(int x1, int y1, int x2, int y2, Color color = COLOR_ON);
 | 
			
		||||
 | 
			
		||||
@@ -207,6 +236,12 @@ class Display : public PollingComponent {
 | 
			
		||||
  /// Fill a circle centered around [center_x,center_y] with the radius radius with the given color.
 | 
			
		||||
  void filled_circle(int center_x, int center_y, int radius, Color color = COLOR_ON);
 | 
			
		||||
 | 
			
		||||
  /// Draw the outline of a triangle contained between the points [x1,y1], [x2,y2] and [x3,y3] with the given color.
 | 
			
		||||
  void triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color = COLOR_ON);
 | 
			
		||||
 | 
			
		||||
  /// Fill a triangle contained between the points [x1,y1], [x2,y2] and [x3,y3] with the given color.
 | 
			
		||||
  void filled_triangle(int x1, int y1, int x2, int y2, int x3, int y3, Color color = COLOR_ON);
 | 
			
		||||
 | 
			
		||||
  /** Print `text` with the anchor point at [x,y] with `font`.
 | 
			
		||||
   *
 | 
			
		||||
   * @param x The x coordinate of the text alignment anchor point.
 | 
			
		||||
@@ -503,6 +538,15 @@ class Display : public PollingComponent {
 | 
			
		||||
  void do_update_();
 | 
			
		||||
  void clear_clipping_();
 | 
			
		||||
 | 
			
		||||
  /**
 | 
			
		||||
   * This method fills a triangle using only integer variables by using a
 | 
			
		||||
   * modified bresenham algorithm.
 | 
			
		||||
   * It is mandatory that [x2,y2] and [x3,y3] lie on the same horizontal line,
 | 
			
		||||
   * so y2 must be equal to y3.
 | 
			
		||||
   */
 | 
			
		||||
  void filled_flat_side_triangle_(int x1, int y1, int x2, int y2, int x3, int y3, Color color);
 | 
			
		||||
  void sort_triangle_points_by_y_(int *x1, int *y1, int *x2, int *y2, int *x3, int *y3);
 | 
			
		||||
 | 
			
		||||
  DisplayRotation rotation_{DISPLAY_ROTATION_0_DEGREES};
 | 
			
		||||
  optional<display_writer_t> writer_{};
 | 
			
		||||
  DisplayPage *page_{nullptr};
 | 
			
		||||
 
 | 
			
		||||
@@ -226,7 +226,7 @@ ARDUINO_PLATFORM_VERSION = cv.Version(5, 4, 0)
 | 
			
		||||
# The default/recommended esp-idf framework version
 | 
			
		||||
#  - https://github.com/espressif/esp-idf/releases
 | 
			
		||||
#  - https://api.registry.platformio.org/v3/packages/platformio/tool/framework-espidf
 | 
			
		||||
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 5)
 | 
			
		||||
RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION = cv.Version(4, 4, 6)
 | 
			
		||||
# The platformio/espressif32 version to use for esp-idf frameworks
 | 
			
		||||
#  - https://github.com/platformio/platform-espressif32/releases
 | 
			
		||||
#  - https://api.registry.platformio.org/v3/packages/platformio/platform/espressif32
 | 
			
		||||
@@ -271,8 +271,8 @@ def _arduino_check_versions(value):
 | 
			
		||||
def _esp_idf_check_versions(value):
 | 
			
		||||
    value = value.copy()
 | 
			
		||||
    lookups = {
 | 
			
		||||
        "dev": (cv.Version(5, 1, 0), "https://github.com/espressif/esp-idf.git"),
 | 
			
		||||
        "latest": (cv.Version(5, 1, 0), None),
 | 
			
		||||
        "dev": (cv.Version(5, 1, 2), "https://github.com/espressif/esp-idf.git"),
 | 
			
		||||
        "latest": (cv.Version(5, 1, 2), None),
 | 
			
		||||
        "recommended": (RECOMMENDED_ESP_IDF_FRAMEWORK_VERSION, None),
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -42,6 +42,34 @@ ESP32_BASE_PINS = {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
ESP32_BOARD_PINS = {
 | 
			
		||||
    "adafruit_feather_esp32_v2": {
 | 
			
		||||
        "A0": 26,
 | 
			
		||||
        "A1": 25,
 | 
			
		||||
        "A2": 34,
 | 
			
		||||
        "A3": 39,
 | 
			
		||||
        "A4": 36,
 | 
			
		||||
        "A5": 4,
 | 
			
		||||
        "SCK": 5,
 | 
			
		||||
        "MOSI": 19,
 | 
			
		||||
        "MISO": 21,
 | 
			
		||||
        "RX": 7,
 | 
			
		||||
        "TX": 8,
 | 
			
		||||
        "D37": 37,
 | 
			
		||||
        "LED": 13,
 | 
			
		||||
        "LED_BUILTIN": 13,
 | 
			
		||||
        "D12": 12,
 | 
			
		||||
        "D27": 27,
 | 
			
		||||
        "D33": 33,
 | 
			
		||||
        "D15": 15,
 | 
			
		||||
        "D32": 32,
 | 
			
		||||
        "D14": 14,
 | 
			
		||||
        "SCL": 20,
 | 
			
		||||
        "SDA": 22,
 | 
			
		||||
        "BUTTON": 38,
 | 
			
		||||
        "NEOPIXEL": 0,
 | 
			
		||||
        "PIN_NEOPIXEL": 0,
 | 
			
		||||
        "NEOPIXEL_POWER": 2,
 | 
			
		||||
    },
 | 
			
		||||
    "adafruit_feather_esp32s2_tft": {
 | 
			
		||||
        "BUTTON": 0,
 | 
			
		||||
        "A0": 18,
 | 
			
		||||
@@ -133,6 +161,10 @@ ESP32_BOARD_PINS = {
 | 
			
		||||
        "BUTTON": 0,
 | 
			
		||||
        "SWITCH": 0,
 | 
			
		||||
    },
 | 
			
		||||
    "airm2m_core_esp32c3": {
 | 
			
		||||
        "LED1_BUILTIN": 12,
 | 
			
		||||
        "LED2_BUILTIN": 13,
 | 
			
		||||
    },
 | 
			
		||||
    "alksesp32": {
 | 
			
		||||
        "A0": 32,
 | 
			
		||||
        "A1": 33,
 | 
			
		||||
 
 | 
			
		||||
@@ -45,21 +45,19 @@ void BLEClientBase::loop() {
 | 
			
		||||
float BLEClientBase::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; }
 | 
			
		||||
 | 
			
		||||
bool BLEClientBase::parse_device(const espbt::ESPBTDevice &device) {
 | 
			
		||||
  if (!this->auto_connect_)
 | 
			
		||||
    return false;
 | 
			
		||||
  if (this->address_ == 0 || device.address_uint64() != this->address_)
 | 
			
		||||
    return false;
 | 
			
		||||
  if (this->state_ != espbt::ClientState::IDLE && this->state_ != espbt::ClientState::SEARCHING)
 | 
			
		||||
    return false;
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "[%d] [%s] Found device", this->connection_index_, this->address_str_.c_str());
 | 
			
		||||
  this->set_state(espbt::ClientState::DISCOVERED);
 | 
			
		||||
  this->log_event_("Found device");
 | 
			
		||||
  if (ESPHOME_LOG_LEVEL >= ESPHOME_LOG_LEVEL_DEBUG)
 | 
			
		||||
    esp32_ble_tracker::global_esp32_ble_tracker->print_bt_device_info(device);
 | 
			
		||||
 | 
			
		||||
  auto addr = device.address_uint64();
 | 
			
		||||
  this->remote_bda_[0] = (addr >> 40) & 0xFF;
 | 
			
		||||
  this->remote_bda_[1] = (addr >> 32) & 0xFF;
 | 
			
		||||
  this->remote_bda_[2] = (addr >> 24) & 0xFF;
 | 
			
		||||
  this->remote_bda_[3] = (addr >> 16) & 0xFF;
 | 
			
		||||
  this->remote_bda_[4] = (addr >> 8) & 0xFF;
 | 
			
		||||
  this->remote_bda_[5] = (addr >> 0) & 0xFF;
 | 
			
		||||
  this->set_state(espbt::ClientState::DISCOVERED);
 | 
			
		||||
  this->set_address(device.address_uint64());
 | 
			
		||||
  this->remote_addr_type_ = device.get_address_type();
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
@@ -108,6 +106,10 @@ void BLEClientBase::release_services() {
 | 
			
		||||
#endif
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEClientBase::log_event_(const char *name) {
 | 
			
		||||
  ESP_LOGD(TAG, "[%d] [%s] %s", this->connection_index_, this->address_str_.c_str(), name);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
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)
 | 
			
		||||
@@ -131,51 +133,73 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_OPEN_EVT: {
 | 
			
		||||
      ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_OPEN_EVT", this->connection_index_, this->address_str_.c_str());
 | 
			
		||||
      if (!this->check_addr(param->open.remote_bda))
 | 
			
		||||
        return false;
 | 
			
		||||
      this->log_event_("ESP_GATTC_OPEN_EVT");
 | 
			
		||||
      this->conn_id_ = param->open.conn_id;
 | 
			
		||||
      this->service_count_ = 0;
 | 
			
		||||
      if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) {
 | 
			
		||||
        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;
 | 
			
		||||
        return false;
 | 
			
		||||
      }
 | 
			
		||||
      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);
 | 
			
		||||
      if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) {
 | 
			
		||||
        ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str());
 | 
			
		||||
        this->set_state(espbt::ClientState::CONNECTED);
 | 
			
		||||
        // only set our state, subclients might have more stuff to do yet.
 | 
			
		||||
        this->state_ = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      esp_ble_gattc_search_service(esp_gattc_if, param->cfg_mtu.conn_id, nullptr);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_CFG_MTU_EVT: {
 | 
			
		||||
      if (param->cfg_mtu.status != ESP_GATT_OK) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_,
 | 
			
		||||
                 this->address_str_.c_str(), param->cfg_mtu.mtu, param->cfg_mtu.status);
 | 
			
		||||
        this->set_state(espbt::ClientState::IDLE);
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      ESP_LOGV(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
               param->cfg_mtu.status, param->cfg_mtu.mtu);
 | 
			
		||||
      this->mtu_ = param->cfg_mtu.mtu;
 | 
			
		||||
    case ESP_GATTC_CONNECT_EVT: {
 | 
			
		||||
      if (!this->check_addr(param->connect.remote_bda))
 | 
			
		||||
        return false;
 | 
			
		||||
      this->log_event_("ESP_GATTC_CONNECT_EVT");
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_DISCONNECT_EVT: {
 | 
			
		||||
      if (memcmp(param->disconnect.remote_bda, this->remote_bda_, 6) != 0)
 | 
			
		||||
      if (!this->check_addr(param->disconnect.remote_bda))
 | 
			
		||||
        return false;
 | 
			
		||||
      ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->connection_index_,
 | 
			
		||||
      ESP_LOGD(TAG, "[%d] [%s] ESP_GATTC_DISCONNECT_EVT, reason %d", this->connection_index_,
 | 
			
		||||
               this->address_str_.c_str(), param->disconnect.reason);
 | 
			
		||||
      this->release_services();
 | 
			
		||||
      this->set_state(espbt::ClientState::IDLE);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    case ESP_GATTC_CFG_MTU_EVT: {
 | 
			
		||||
      if (this->conn_id_ != param->cfg_mtu.conn_id)
 | 
			
		||||
        return false;
 | 
			
		||||
      if (param->cfg_mtu.status != ESP_GATT_OK) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] cfg_mtu failed, mtu %d, status %d", this->connection_index_,
 | 
			
		||||
                 this->address_str_.c_str(), param->cfg_mtu.mtu, param->cfg_mtu.status);
 | 
			
		||||
        // No state change required here - disconnect event will follow if needed.
 | 
			
		||||
        break;
 | 
			
		||||
      }
 | 
			
		||||
      ESP_LOGD(TAG, "[%d] [%s] cfg_mtu status %d, mtu %d", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
               param->cfg_mtu.status, param->cfg_mtu.mtu);
 | 
			
		||||
      this->mtu_ = param->cfg_mtu.mtu;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_CLOSE_EVT: {
 | 
			
		||||
      if (this->conn_id_ != param->close.conn_id)
 | 
			
		||||
        return false;
 | 
			
		||||
      this->log_event_("ESP_GATTC_CLOSE_EVT");
 | 
			
		||||
      this->release_services();
 | 
			
		||||
      this->set_state(espbt::ClientState::IDLE);
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_SEARCH_RES_EVT: {
 | 
			
		||||
      if (this->conn_id_ != param->search_res.conn_id)
 | 
			
		||||
        return false;
 | 
			
		||||
      this->service_count_++;
 | 
			
		||||
      if (this->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) {
 | 
			
		||||
        // V3 clients don't need services initialized since
 | 
			
		||||
@@ -191,7 +215,9 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_SEARCH_CMPL_EVT: {
 | 
			
		||||
      ESP_LOGV(TAG, "[%d] [%s] ESP_GATTC_SEARCH_CMPL_EVT", this->connection_index_, this->address_str_.c_str());
 | 
			
		||||
      if (this->conn_id_ != param->search_cmpl.conn_id)
 | 
			
		||||
        return false;
 | 
			
		||||
      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());
 | 
			
		||||
@@ -199,11 +225,41 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
                 this->address_str_.c_str(), svc->start_handle, svc->end_handle);
 | 
			
		||||
      }
 | 
			
		||||
      ESP_LOGI(TAG, "[%d] [%s] Connected", this->connection_index_, this->address_str_.c_str());
 | 
			
		||||
      this->set_state(espbt::ClientState::CONNECTED);
 | 
			
		||||
      this->state_ = espbt::ClientState::ESTABLISHED;
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_READ_DESCR_EVT: {
 | 
			
		||||
      if (this->conn_id_ != param->write.conn_id)
 | 
			
		||||
        return false;
 | 
			
		||||
      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_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_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_event_("ESP_GATTC_READ_CHAR_EVT");
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_NOTIFY_EVT: {
 | 
			
		||||
      if (this->conn_id_ != param->notify.conn_id)
 | 
			
		||||
        return false;
 | 
			
		||||
      this->log_event_("ESP_GATTC_NOTIFY_EVT");
 | 
			
		||||
      break;
 | 
			
		||||
    }
 | 
			
		||||
    case ESP_GATTC_REG_FOR_NOTIFY_EVT: {
 | 
			
		||||
      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
 | 
			
		||||
@@ -212,9 +268,8 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
      }
 | 
			
		||||
      esp_gattc_descr_elem_t desc_result;
 | 
			
		||||
      uint16_t count = 1;
 | 
			
		||||
      esp_gatt_status_t descr_status =
 | 
			
		||||
          esp_ble_gattc_get_descr_by_char_handle(this->gattc_if_, this->connection_index_, param->reg_for_notify.handle,
 | 
			
		||||
                                                 NOTIFY_DESC_UUID, &desc_result, &count);
 | 
			
		||||
      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) {
 | 
			
		||||
        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);
 | 
			
		||||
@@ -222,7 +277,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
      }
 | 
			
		||||
      esp_gattc_char_elem_t char_result;
 | 
			
		||||
      esp_gatt_status_t char_status =
 | 
			
		||||
          esp_ble_gattc_get_all_char(this->gattc_if_, this->connection_index_, param->reg_for_notify.handle,
 | 
			
		||||
          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) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_get_all_char error, status=%d", this->connection_index_,
 | 
			
		||||
@@ -238,6 +293,7 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
      esp_err_t status =
 | 
			
		||||
          esp_ble_gattc_write_char_descr(this->gattc_if_, this->conn_id_, desc_result.handle, sizeof(notify_en),
 | 
			
		||||
                                         (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) {
 | 
			
		||||
        ESP_LOGW(TAG, "[%d] [%s] esp_ble_gattc_write_char_descr error, status=%d", this->connection_index_,
 | 
			
		||||
                 this->address_str_.c_str(), status);
 | 
			
		||||
@@ -246,24 +302,31 @@ bool BLEClientBase::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_
 | 
			
		||||
    }
 | 
			
		||||
 | 
			
		||||
    default:
 | 
			
		||||
      // ideally would check all other events for matching conn_id
 | 
			
		||||
      ESP_LOGD(TAG, "[%d] [%s] Event %d", this->connection_index_, this->address_str_.c_str(), event);
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
  return true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// clients can't call defer() directly since it's protected.
 | 
			
		||||
void BLEClientBase::run_later(std::function<void()> &&f) {  // NOLINT
 | 
			
		||||
  this->defer(std::move(f));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_cb_param_t *param) {
 | 
			
		||||
  switch (event) {
 | 
			
		||||
    // This event is sent by the server when it requests security
 | 
			
		||||
    case ESP_GAP_BLE_SEC_REQ_EVT:
 | 
			
		||||
      if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0)
 | 
			
		||||
        break;
 | 
			
		||||
      if (!this->check_addr(param->ble_security.auth_cmpl.bd_addr))
 | 
			
		||||
        return;
 | 
			
		||||
      ESP_LOGV(TAG, "[%d] [%s] ESP_GAP_BLE_SEC_REQ_EVT %x", this->connection_index_, this->address_str_.c_str(), event);
 | 
			
		||||
      esp_ble_gap_security_rsp(param->ble_security.ble_req.bd_addr, true);
 | 
			
		||||
      break;
 | 
			
		||||
    // This event is sent once authentication has completed
 | 
			
		||||
    case ESP_GAP_BLE_AUTH_CMPL_EVT:
 | 
			
		||||
      if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0)
 | 
			
		||||
        break;
 | 
			
		||||
      if (!this->check_addr(param->ble_security.auth_cmpl.bd_addr))
 | 
			
		||||
        return;
 | 
			
		||||
      esp_bd_addr_t bd_addr;
 | 
			
		||||
      memcpy(bd_addr, param->ble_security.auth_cmpl.bd_addr, sizeof(esp_bd_addr_t));
 | 
			
		||||
      ESP_LOGI(TAG, "[%d] [%s] auth complete. remote BD_ADDR: %s", this->connection_index_, this->address_str_.c_str(),
 | 
			
		||||
@@ -273,11 +336,12 @@ void BLEClientBase::gap_event_handler(esp_gap_ble_cb_event_t event, esp_ble_gap_
 | 
			
		||||
                 param->ble_security.auth_cmpl.fail_reason);
 | 
			
		||||
      } else {
 | 
			
		||||
        this->paired_ = true;
 | 
			
		||||
        ESP_LOGV(TAG, "[%d] [%s] auth success. address type = %d auth mode = %d", this->connection_index_,
 | 
			
		||||
        ESP_LOGD(TAG, "[%d] [%s] auth success. address type = %d auth mode = %d", this->connection_index_,
 | 
			
		||||
                 this->address_str_.c_str(), param->ble_security.auth_cmpl.addr_type,
 | 
			
		||||
                 param->ble_security.auth_cmpl.auth_mode);
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
 | 
			
		||||
    // There are other events we'll want to implement at some point to support things like pass key
 | 
			
		||||
    // https://github.com/espressif/esp-idf/blob/cba69dd088344ed9d26739f04736ae7a37541b3a/examples/bluetooth/bluedroid/ble/gatt_security_client/tutorial/Gatt_Security_Client_Example_Walkthrough.md
 | 
			
		||||
    default:
 | 
			
		||||
 
 | 
			
		||||
@@ -27,6 +27,7 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
 | 
			
		||||
  void run_later(std::function<void()> &&f);  // NOLINT
 | 
			
		||||
  bool parse_device(const espbt::ESPBTDevice &device) override;
 | 
			
		||||
  void on_scan_end() override {}
 | 
			
		||||
  bool gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if,
 | 
			
		||||
@@ -39,10 +40,17 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
 | 
			
		||||
 | 
			
		||||
  bool connected() { return this->state_ == espbt::ClientState::ESTABLISHED; }
 | 
			
		||||
 | 
			
		||||
  void set_auto_connect(bool auto_connect) { this->auto_connect_ = auto_connect; }
 | 
			
		||||
 | 
			
		||||
  void set_address(uint64_t address) {
 | 
			
		||||
    this->address_ = address;
 | 
			
		||||
    this->remote_bda_[0] = (address >> 40) & 0xFF;
 | 
			
		||||
    this->remote_bda_[1] = (address >> 32) & 0xFF;
 | 
			
		||||
    this->remote_bda_[2] = (address >> 24) & 0xFF;
 | 
			
		||||
    this->remote_bda_[3] = (address >> 16) & 0xFF;
 | 
			
		||||
    this->remote_bda_[4] = (address >> 8) & 0xFF;
 | 
			
		||||
    this->remote_bda_[5] = (address >> 0) & 0xFF;
 | 
			
		||||
    if (address == 0) {
 | 
			
		||||
      memset(this->remote_bda_, 0, sizeof(this->remote_bda_));
 | 
			
		||||
      this->address_str_ = "";
 | 
			
		||||
    } else {
 | 
			
		||||
      this->address_str_ =
 | 
			
		||||
@@ -79,20 +87,24 @@ class BLEClientBase : public espbt::ESPBTClient, public Component {
 | 
			
		||||
 | 
			
		||||
  virtual void set_connection_type(espbt::ConnectionType ct) { this->connection_type_ = ct; }
 | 
			
		||||
 | 
			
		||||
  bool check_addr(esp_bd_addr_t &addr) { return memcmp(addr, this->remote_bda_, sizeof(esp_bd_addr_t)) == 0; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  int gattc_if_;
 | 
			
		||||
  esp_bd_addr_t remote_bda_;
 | 
			
		||||
  esp_ble_addr_type_t remote_addr_type_;
 | 
			
		||||
  esp_ble_addr_type_t remote_addr_type_{BLE_ADDR_TYPE_PUBLIC};
 | 
			
		||||
  uint16_t conn_id_{0xFFFF};
 | 
			
		||||
  uint64_t address_{0};
 | 
			
		||||
  bool auto_connect_{false};
 | 
			
		||||
  std::string address_str_{};
 | 
			
		||||
  uint8_t connection_index_;
 | 
			
		||||
  int16_t service_count_{0};
 | 
			
		||||
  uint16_t mtu_{23};
 | 
			
		||||
  bool paired_{false};
 | 
			
		||||
  espbt::ConnectionType connection_type_{espbt::ConnectionType::V1};
 | 
			
		||||
 | 
			
		||||
  std::vector<BLEService *> services_;
 | 
			
		||||
 | 
			
		||||
  void log_event_(const char *name);
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace esp32_ble_client
 | 
			
		||||
 
 | 
			
		||||
@@ -1,23 +1,26 @@
 | 
			
		||||
import re
 | 
			
		||||
 | 
			
		||||
import esphome.codegen as cg
 | 
			
		||||
import esphome.config_validation as cv
 | 
			
		||||
from esphome import automation
 | 
			
		||||
from esphome.components import esp32_ble
 | 
			
		||||
from esphome.components.esp32 import add_idf_sdkconfig_option
 | 
			
		||||
from esphome.const import (
 | 
			
		||||
    CONF_ACTIVE,
 | 
			
		||||
    CONF_DURATION,
 | 
			
		||||
    CONF_ID,
 | 
			
		||||
    CONF_INTERVAL,
 | 
			
		||||
    CONF_DURATION,
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
    CONF_MAC_ADDRESS,
 | 
			
		||||
    CONF_SERVICE_UUID,
 | 
			
		||||
    CONF_MANUFACTURER_ID,
 | 
			
		||||
    CONF_ON_BLE_ADVERTISE,
 | 
			
		||||
    CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
 | 
			
		||||
    CONF_ON_BLE_MANUFACTURER_DATA_ADVERTISE,
 | 
			
		||||
    CONF_ON_BLE_SERVICE_DATA_ADVERTISE,
 | 
			
		||||
    CONF_SERVICE_UUID,
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
    KEY_CORE,
 | 
			
		||||
    KEY_FRAMEWORK_VERSION,
 | 
			
		||||
)
 | 
			
		||||
from esphome.components import esp32_ble
 | 
			
		||||
from esphome.core import CORE
 | 
			
		||||
from esphome.components.esp32 import add_idf_sdkconfig_option
 | 
			
		||||
 | 
			
		||||
AUTO_LOAD = ["esp32_ble"]
 | 
			
		||||
DEPENDENCIES = ["esp32"]
 | 
			
		||||
@@ -263,7 +266,10 @@ async def to_code(config):
 | 
			
		||||
        # https://github.com/espressif/esp-idf/issues/2503
 | 
			
		||||
        # Match arduino CONFIG_BTU_TASK_STACK_SIZE
 | 
			
		||||
        # https://github.com/espressif/arduino-esp32/blob/fd72cf46ad6fc1a6de99c1d83ba8eba17d80a4ee/tools/sdk/esp32/sdkconfig#L1866
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192)
 | 
			
		||||
        if CORE.data[KEY_CORE][KEY_FRAMEWORK_VERSION] >= cv.Version(4, 4, 6):
 | 
			
		||||
            add_idf_sdkconfig_option("CONFIG_BT_BTU_TASK_STACK_SIZE", 8192)
 | 
			
		||||
        else:
 | 
			
		||||
            add_idf_sdkconfig_option("CONFIG_BTU_TASK_STACK_SIZE", 8192)
 | 
			
		||||
        add_idf_sdkconfig_option("CONFIG_BT_ACL_CONNECTIONS", 9)
 | 
			
		||||
 | 
			
		||||
    cg.add_define("USE_OTA_STATE_CALLBACK")  # To be notified when an OTA update starts
 | 
			
		||||
 
 | 
			
		||||
@@ -13,6 +13,8 @@ namespace esp32_rmt_led_strip {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "esp32_rmt_led_strip";
 | 
			
		||||
 | 
			
		||||
static const uint32_t RMT_CLK_FREQ = 80000000;
 | 
			
		||||
 | 
			
		||||
static const uint8_t RMT_CLK_DIV = 2;
 | 
			
		||||
 | 
			
		||||
void ESP32RMTLEDStripLightOutput::setup() {
 | 
			
		||||
@@ -65,7 +67,7 @@ void ESP32RMTLEDStripLightOutput::setup() {
 | 
			
		||||
 | 
			
		||||
void ESP32RMTLEDStripLightOutput::set_led_params(uint32_t bit0_high, uint32_t bit0_low, uint32_t bit1_high,
 | 
			
		||||
                                                 uint32_t bit1_low) {
 | 
			
		||||
  float ratio = (float) APB_CLK_FREQ / RMT_CLK_DIV / 1e09f;
 | 
			
		||||
  float ratio = (float) RMT_CLK_FREQ / RMT_CLK_DIV / 1e09f;
 | 
			
		||||
 | 
			
		||||
  // 0-bit
 | 
			
		||||
  this->bit0_.duration0 = (uint32_t) (ratio * bit0_high);
 | 
			
		||||
@@ -158,11 +160,13 @@ light::ESPColorView ESP32RMTLEDStripLightOutput::get_view_internal(int32_t index
 | 
			
		||||
      b = 0;
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
  uint8_t multiplier = this->is_rgbw_ ? 4 : 3;
 | 
			
		||||
  return {this->buf_ + (index * multiplier) + r,
 | 
			
		||||
          this->buf_ + (index * multiplier) + g,
 | 
			
		||||
          this->buf_ + (index * multiplier) + b,
 | 
			
		||||
          this->is_rgbw_ ? this->buf_ + (index * multiplier) + 3 : nullptr,
 | 
			
		||||
  uint8_t multiplier = this->is_rgbw_ || this->is_wrgb_ ? 4 : 3;
 | 
			
		||||
  uint8_t white = this->is_wrgb_ ? 0 : 3;
 | 
			
		||||
 | 
			
		||||
  return {this->buf_ + (index * multiplier) + r + this->is_wrgb_,
 | 
			
		||||
          this->buf_ + (index * multiplier) + g + this->is_wrgb_,
 | 
			
		||||
          this->buf_ + (index * multiplier) + b + this->is_wrgb_,
 | 
			
		||||
          this->is_rgbw_ || this->is_wrgb_ ? this->buf_ + (index * multiplier) + white : nullptr,
 | 
			
		||||
          &this->effect_data_[index],
 | 
			
		||||
          &this->correction_};
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -33,7 +33,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
 | 
			
		||||
  int32_t size() const override { return this->num_leds_; }
 | 
			
		||||
  light::LightTraits get_traits() override {
 | 
			
		||||
    auto traits = light::LightTraits();
 | 
			
		||||
    if (this->is_rgbw_) {
 | 
			
		||||
    if (this->is_rgbw_ || this->is_wrgb_) {
 | 
			
		||||
      traits.set_supported_color_modes({light::ColorMode::RGB_WHITE, light::ColorMode::WHITE});
 | 
			
		||||
    } else {
 | 
			
		||||
      traits.set_supported_color_modes({light::ColorMode::RGB});
 | 
			
		||||
@@ -44,6 +44,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
 | 
			
		||||
  void set_pin(uint8_t pin) { this->pin_ = pin; }
 | 
			
		||||
  void set_num_leds(uint16_t num_leds) { this->num_leds_ = num_leds; }
 | 
			
		||||
  void set_is_rgbw(bool is_rgbw) { this->is_rgbw_ = is_rgbw; }
 | 
			
		||||
  void set_is_wrgb(bool is_wrgb) { this->is_wrgb_ = is_wrgb; }
 | 
			
		||||
 | 
			
		||||
  /// Set a maximum refresh rate in µs as some lights do not like being updated too often.
 | 
			
		||||
  void set_max_refresh_rate(uint32_t interval_us) { this->max_refresh_rate_ = interval_us; }
 | 
			
		||||
@@ -72,6 +73,7 @@ class ESP32RMTLEDStripLightOutput : public light::AddressableLight {
 | 
			
		||||
  uint8_t pin_;
 | 
			
		||||
  uint16_t num_leds_;
 | 
			
		||||
  bool is_rgbw_;
 | 
			
		||||
  bool is_wrgb_;
 | 
			
		||||
 | 
			
		||||
  rmt_item32_t bit0_, bit1_;
 | 
			
		||||
  RGBOrder rgb_order_;
 | 
			
		||||
 
 | 
			
		||||
@@ -52,6 +52,7 @@ CHIPSETS = {
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
CONF_IS_RGBW = "is_rgbw"
 | 
			
		||||
CONF_IS_WRGB = "is_wrgb"
 | 
			
		||||
CONF_BIT0_HIGH = "bit0_high"
 | 
			
		||||
CONF_BIT0_LOW = "bit0_low"
 | 
			
		||||
CONF_BIT1_HIGH = "bit1_high"
 | 
			
		||||
@@ -90,6 +91,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
            cv.Optional(CONF_MAX_REFRESH_RATE): cv.positive_time_period_microseconds,
 | 
			
		||||
            cv.Optional(CONF_CHIPSET): cv.one_of(*CHIPSETS, upper=True),
 | 
			
		||||
            cv.Optional(CONF_IS_RGBW, default=False): cv.boolean,
 | 
			
		||||
            cv.Optional(CONF_IS_WRGB, default=False): cv.boolean,
 | 
			
		||||
            cv.Inclusive(
 | 
			
		||||
                CONF_BIT0_HIGH,
 | 
			
		||||
                "custom",
 | 
			
		||||
@@ -145,6 +147,7 @@ async def to_code(config):
 | 
			
		||||
 | 
			
		||||
    cg.add(var.set_rgb_order(config[CONF_RGB_ORDER]))
 | 
			
		||||
    cg.add(var.set_is_rgbw(config[CONF_IS_RGBW]))
 | 
			
		||||
    cg.add(var.set_is_wrgb(config[CONF_IS_WRGB]))
 | 
			
		||||
 | 
			
		||||
    cg.add(
 | 
			
		||||
        var.set_rmt_channel(
 | 
			
		||||
 
 | 
			
		||||
@@ -13,8 +13,11 @@ from esphome.const import (
 | 
			
		||||
    CONF_ON_ENROLLMENT_DONE,
 | 
			
		||||
    CONF_ON_ENROLLMENT_FAILED,
 | 
			
		||||
    CONF_ON_ENROLLMENT_SCAN,
 | 
			
		||||
    CONF_ON_FINGER_SCAN_START,
 | 
			
		||||
    CONF_ON_FINGER_SCAN_MATCHED,
 | 
			
		||||
    CONF_ON_FINGER_SCAN_UNMATCHED,
 | 
			
		||||
    CONF_ON_FINGER_SCAN_MISPLACED,
 | 
			
		||||
    CONF_ON_FINGER_SCAN_INVALID,
 | 
			
		||||
    CONF_PASSWORD,
 | 
			
		||||
    CONF_SENSING_PIN,
 | 
			
		||||
    CONF_SPEED,
 | 
			
		||||
@@ -34,6 +37,10 @@ FingerprintGrowComponent = fingerprint_grow_ns.class_(
 | 
			
		||||
    "FingerprintGrowComponent", cg.PollingComponent, uart.UARTDevice
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
FingerScanStartTrigger = fingerprint_grow_ns.class_(
 | 
			
		||||
    "FingerScanStartTrigger", automation.Trigger.template()
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
FingerScanMatchedTrigger = fingerprint_grow_ns.class_(
 | 
			
		||||
    "FingerScanMatchedTrigger", automation.Trigger.template(cg.uint16, cg.uint16)
 | 
			
		||||
)
 | 
			
		||||
@@ -42,6 +49,14 @@ FingerScanUnmatchedTrigger = fingerprint_grow_ns.class_(
 | 
			
		||||
    "FingerScanUnmatchedTrigger", automation.Trigger.template()
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
FingerScanMisplacedTrigger = fingerprint_grow_ns.class_(
 | 
			
		||||
    "FingerScanMisplacedTrigger", automation.Trigger.template()
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
FingerScanInvalidTrigger = fingerprint_grow_ns.class_(
 | 
			
		||||
    "FingerScanInvalidTrigger", automation.Trigger.template()
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
EnrollmentScanTrigger = fingerprint_grow_ns.class_(
 | 
			
		||||
    "EnrollmentScanTrigger", automation.Trigger.template(cg.uint8, cg.uint16)
 | 
			
		||||
)
 | 
			
		||||
@@ -94,6 +109,13 @@ CONFIG_SCHEMA = (
 | 
			
		||||
            cv.Optional(CONF_SENSING_PIN): pins.gpio_input_pin_schema,
 | 
			
		||||
            cv.Optional(CONF_PASSWORD): cv.uint32_t,
 | 
			
		||||
            cv.Optional(CONF_NEW_PASSWORD): cv.uint32_t,
 | 
			
		||||
            cv.Optional(CONF_ON_FINGER_SCAN_START): automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                        FingerScanStartTrigger
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ON_FINGER_SCAN_MATCHED): automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
@@ -108,6 +130,20 @@ CONFIG_SCHEMA = (
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ON_FINGER_SCAN_MISPLACED): automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                        FingerScanMisplacedTrigger
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ON_FINGER_SCAN_INVALID): automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                        FingerScanInvalidTrigger
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
            cv.Optional(CONF_ON_ENROLLMENT_SCAN): automation.validate_automation(
 | 
			
		||||
                {
 | 
			
		||||
                    cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
@@ -152,6 +188,10 @@ async def to_code(config):
 | 
			
		||||
        sensing_pin = await cg.gpio_pin_expression(config[CONF_SENSING_PIN])
 | 
			
		||||
        cg.add(var.set_sensing_pin(sensing_pin))
 | 
			
		||||
 | 
			
		||||
    for conf in config.get(CONF_ON_FINGER_SCAN_START, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await automation.build_automation(trigger, [], conf)
 | 
			
		||||
 | 
			
		||||
    for conf in config.get(CONF_ON_FINGER_SCAN_MATCHED, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await automation.build_automation(
 | 
			
		||||
@@ -162,6 +202,14 @@ async def to_code(config):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await automation.build_automation(trigger, [], conf)
 | 
			
		||||
 | 
			
		||||
    for conf in config.get(CONF_ON_FINGER_SCAN_MISPLACED, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await automation.build_automation(trigger, [], conf)
 | 
			
		||||
 | 
			
		||||
    for conf in config.get(CONF_ON_FINGER_SCAN_INVALID, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await automation.build_automation(trigger, [], conf)
 | 
			
		||||
 | 
			
		||||
    for conf in config.get(CONF_ON_ENROLLMENT_SCAN, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await automation.build_automation(
 | 
			
		||||
 
 | 
			
		||||
@@ -15,16 +15,18 @@ void FingerprintGrowComponent::update() {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->sensing_pin_ != nullptr) {
 | 
			
		||||
  if (this->has_sensing_pin_) {
 | 
			
		||||
    if (this->sensing_pin_->digital_read()) {
 | 
			
		||||
      ESP_LOGV(TAG, "No touch sensing");
 | 
			
		||||
      this->waiting_removal_ = false;
 | 
			
		||||
      return;
 | 
			
		||||
    } else if (!this->waiting_removal_) {
 | 
			
		||||
      this->finger_scan_start_callback_.call();
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->waiting_removal_) {
 | 
			
		||||
    if (this->scan_image_(1) == NO_FINGER) {
 | 
			
		||||
    if ((!this->has_sensing_pin_) && (this->scan_image_(1) == NO_FINGER)) {
 | 
			
		||||
      ESP_LOGD(TAG, "Finger removed");
 | 
			
		||||
      this->waiting_removal_ = false;
 | 
			
		||||
    }
 | 
			
		||||
@@ -51,6 +53,7 @@ void FingerprintGrowComponent::update() {
 | 
			
		||||
 | 
			
		||||
void FingerprintGrowComponent::setup() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "Setting up Grow Fingerprint Reader...");
 | 
			
		||||
  this->has_sensing_pin_ = (this->sensing_pin_ != nullptr);
 | 
			
		||||
  if (this->check_password_()) {
 | 
			
		||||
    if (this->new_password_ != -1) {
 | 
			
		||||
      if (this->set_password_())
 | 
			
		||||
@@ -91,7 +94,7 @@ void FingerprintGrowComponent::finish_enrollment(uint8_t result) {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FingerprintGrowComponent::scan_and_match_() {
 | 
			
		||||
  if (this->sensing_pin_ != nullptr) {
 | 
			
		||||
  if (this->has_sensing_pin_) {
 | 
			
		||||
    ESP_LOGD(TAG, "Scan and match");
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGV(TAG, "Scan and match");
 | 
			
		||||
@@ -122,43 +125,52 @@ void FingerprintGrowComponent::scan_and_match_() {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint8_t FingerprintGrowComponent::scan_image_(uint8_t buffer) {
 | 
			
		||||
  if (this->sensing_pin_ != nullptr) {
 | 
			
		||||
  if (this->has_sensing_pin_) {
 | 
			
		||||
    ESP_LOGD(TAG, "Getting image %d", buffer);
 | 
			
		||||
  } else {
 | 
			
		||||
    ESP_LOGV(TAG, "Getting image %d", buffer);
 | 
			
		||||
  }
 | 
			
		||||
  this->data_ = {GET_IMAGE};
 | 
			
		||||
  switch (this->send_command_()) {
 | 
			
		||||
  uint8_t send_result = this->send_command_();
 | 
			
		||||
  switch (send_result) {
 | 
			
		||||
    case OK:
 | 
			
		||||
      break;
 | 
			
		||||
    case NO_FINGER:
 | 
			
		||||
      if (this->sensing_pin_ != nullptr) {
 | 
			
		||||
        ESP_LOGD(TAG, "No finger");
 | 
			
		||||
      if (this->has_sensing_pin_) {
 | 
			
		||||
        this->waiting_removal_ = true;
 | 
			
		||||
        ESP_LOGD(TAG, "Finger Misplaced");
 | 
			
		||||
        this->finger_scan_misplaced_callback_.call();
 | 
			
		||||
      } else {
 | 
			
		||||
        ESP_LOGV(TAG, "No finger");
 | 
			
		||||
      }
 | 
			
		||||
      return this->data_[0];
 | 
			
		||||
      return send_result;
 | 
			
		||||
    case IMAGE_FAIL:
 | 
			
		||||
      ESP_LOGE(TAG, "Imaging error");
 | 
			
		||||
      this->finger_scan_invalid_callback_.call();
 | 
			
		||||
      return send_result;
 | 
			
		||||
    default:
 | 
			
		||||
      return this->data_[0];
 | 
			
		||||
      ESP_LOGD(TAG, "Unknown Scan Error: %d", send_result);
 | 
			
		||||
      return send_result;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "Processing image %d", buffer);
 | 
			
		||||
  this->data_ = {IMAGE_2_TZ, buffer};
 | 
			
		||||
  switch (this->send_command_()) {
 | 
			
		||||
  send_result = this->send_command_();
 | 
			
		||||
  switch (send_result) {
 | 
			
		||||
    case OK:
 | 
			
		||||
      ESP_LOGI(TAG, "Processed image %d", buffer);
 | 
			
		||||
      break;
 | 
			
		||||
    case IMAGE_MESS:
 | 
			
		||||
      ESP_LOGE(TAG, "Image too messy");
 | 
			
		||||
      this->finger_scan_invalid_callback_.call();
 | 
			
		||||
      break;
 | 
			
		||||
    case FEATURE_FAIL:
 | 
			
		||||
    case INVALID_IMAGE:
 | 
			
		||||
      ESP_LOGE(TAG, "Could not find fingerprint features");
 | 
			
		||||
      this->finger_scan_invalid_callback_.call();
 | 
			
		||||
      break;
 | 
			
		||||
  }
 | 
			
		||||
  return this->data_[0];
 | 
			
		||||
  return send_result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint8_t FingerprintGrowComponent::save_fingerprint_() {
 | 
			
		||||
@@ -221,10 +233,11 @@ bool FingerprintGrowComponent::get_parameters_() {
 | 
			
		||||
  ESP_LOGD(TAG, "Getting parameters");
 | 
			
		||||
  this->data_ = {READ_SYS_PARAM};
 | 
			
		||||
  if (this->send_command_() == OK) {
 | 
			
		||||
    ESP_LOGD(TAG, "Got parameters");
 | 
			
		||||
    if (this->status_sensor_ != nullptr) {
 | 
			
		||||
    ESP_LOGD(TAG, "Got parameters");        // Bear in mind data_[0] is the transfer status,
 | 
			
		||||
    if (this->status_sensor_ != nullptr) {  // the parameters table start at data_[1]
 | 
			
		||||
      this->status_sensor_->publish_state(((uint16_t) this->data_[1] << 8) | this->data_[2]);
 | 
			
		||||
    }
 | 
			
		||||
    this->system_identifier_code_ = ((uint16_t) this->data_[3] << 8) | this->data_[4];
 | 
			
		||||
    this->capacity_ = ((uint16_t) this->data_[5] << 8) | this->data_[6];
 | 
			
		||||
    if (this->capacity_sensor_ != nullptr) {
 | 
			
		||||
      this->capacity_sensor_->publish_state(this->capacity_);
 | 
			
		||||
@@ -323,6 +336,8 @@ void FingerprintGrowComponent::aura_led_control(uint8_t state, uint8_t speed, ui
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint8_t FingerprintGrowComponent::send_command_() {
 | 
			
		||||
  while (this->available())
 | 
			
		||||
    this->read();
 | 
			
		||||
  this->write((uint8_t) (START_CODE >> 8));
 | 
			
		||||
  this->write((uint8_t) (START_CODE & 0xFF));
 | 
			
		||||
  this->write(this->address_[0]);
 | 
			
		||||
@@ -426,13 +441,22 @@ uint8_t FingerprintGrowComponent::send_command_() {
 | 
			
		||||
 | 
			
		||||
void FingerprintGrowComponent::dump_config() {
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "GROW_FINGERPRINT_READER:");
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  System Identifier Code: 0x%.4X", this->system_identifier_code_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "  Touch Sensing Pin: %s",
 | 
			
		||||
                this->has_sensing_pin_ ? this->sensing_pin_->dump_summary().c_str() : "None");
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
  LOG_SENSOR("  ", "Fingerprint Count", this->fingerprint_count_sensor_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "    Current Value: %d", (uint16_t) this->fingerprint_count_sensor_->get_state());
 | 
			
		||||
  LOG_SENSOR("  ", "Status", this->status_sensor_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "    Current Value: %d", (uint8_t) this->status_sensor_->get_state());
 | 
			
		||||
  LOG_SENSOR("  ", "Capacity", this->capacity_sensor_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "    Current Value: %d", (uint16_t) this->capacity_sensor_->get_state());
 | 
			
		||||
  LOG_SENSOR("  ", "Security Level", this->security_level_sensor_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "    Current Value: %d", (uint8_t) this->security_level_sensor_->get_state());
 | 
			
		||||
  LOG_SENSOR("  ", "Last Finger ID", this->last_finger_id_sensor_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "    Current Value: %d", (uint32_t) this->last_finger_id_sensor_->get_state());
 | 
			
		||||
  LOG_SENSOR("  ", "Last Confidence", this->last_confidence_sensor_);
 | 
			
		||||
  ESP_LOGCONFIG(TAG, "    Current Value: %d", (uint32_t) this->last_confidence_sensor_->get_state());
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace fingerprint_grow
 | 
			
		||||
 
 | 
			
		||||
@@ -118,12 +118,21 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic
 | 
			
		||||
  void set_enrolling_binary_sensor(binary_sensor::BinarySensor *enrolling_binary_sensor) {
 | 
			
		||||
    this->enrolling_binary_sensor_ = enrolling_binary_sensor;
 | 
			
		||||
  }
 | 
			
		||||
  void add_on_finger_scan_start_callback(std::function<void()> callback) {
 | 
			
		||||
    this->finger_scan_start_callback_.add(std::move(callback));
 | 
			
		||||
  }
 | 
			
		||||
  void add_on_finger_scan_matched_callback(std::function<void(uint16_t, uint16_t)> callback) {
 | 
			
		||||
    this->finger_scan_matched_callback_.add(std::move(callback));
 | 
			
		||||
  }
 | 
			
		||||
  void add_on_finger_scan_unmatched_callback(std::function<void()> callback) {
 | 
			
		||||
    this->finger_scan_unmatched_callback_.add(std::move(callback));
 | 
			
		||||
  }
 | 
			
		||||
  void add_on_finger_scan_misplaced_callback(std::function<void()> callback) {
 | 
			
		||||
    this->finger_scan_misplaced_callback_.add(std::move(callback));
 | 
			
		||||
  }
 | 
			
		||||
  void add_on_finger_scan_invalid_callback(std::function<void()> callback) {
 | 
			
		||||
    this->finger_scan_invalid_callback_.add(std::move(callback));
 | 
			
		||||
  }
 | 
			
		||||
  void add_on_enrollment_scan_callback(std::function<void(uint8_t, uint16_t)> callback) {
 | 
			
		||||
    this->enrollment_scan_callback_.add(std::move(callback));
 | 
			
		||||
  }
 | 
			
		||||
@@ -163,8 +172,10 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic
 | 
			
		||||
  uint16_t enrollment_slot_ = ENROLLMENT_SLOT_UNUSED;
 | 
			
		||||
  uint8_t enrollment_buffers_ = 5;
 | 
			
		||||
  bool waiting_removal_ = false;
 | 
			
		||||
  bool has_sensing_pin_ = false;
 | 
			
		||||
  uint32_t last_aura_led_control_ = 0;
 | 
			
		||||
  uint16_t last_aura_led_duration_ = 0;
 | 
			
		||||
  uint16_t system_identifier_code_ = 0;
 | 
			
		||||
  sensor::Sensor *fingerprint_count_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *status_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *capacity_sensor_{nullptr};
 | 
			
		||||
@@ -172,13 +183,23 @@ class FingerprintGrowComponent : public PollingComponent, public uart::UARTDevic
 | 
			
		||||
  sensor::Sensor *last_finger_id_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *last_confidence_sensor_{nullptr};
 | 
			
		||||
  binary_sensor::BinarySensor *enrolling_binary_sensor_{nullptr};
 | 
			
		||||
  CallbackManager<void()> finger_scan_invalid_callback_;
 | 
			
		||||
  CallbackManager<void()> finger_scan_start_callback_;
 | 
			
		||||
  CallbackManager<void(uint16_t, uint16_t)> finger_scan_matched_callback_;
 | 
			
		||||
  CallbackManager<void()> finger_scan_unmatched_callback_;
 | 
			
		||||
  CallbackManager<void()> finger_scan_misplaced_callback_;
 | 
			
		||||
  CallbackManager<void(uint8_t, uint16_t)> enrollment_scan_callback_;
 | 
			
		||||
  CallbackManager<void(uint16_t)> enrollment_done_callback_;
 | 
			
		||||
  CallbackManager<void(uint16_t)> enrollment_failed_callback_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class FingerScanStartTrigger : public Trigger<> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit FingerScanStartTrigger(FingerprintGrowComponent *parent) {
 | 
			
		||||
    parent->add_on_finger_scan_start_callback([this]() { this->trigger(); });
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class FingerScanMatchedTrigger : public Trigger<uint16_t, uint16_t> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit FingerScanMatchedTrigger(FingerprintGrowComponent *parent) {
 | 
			
		||||
@@ -194,6 +215,20 @@ class FingerScanUnmatchedTrigger : public Trigger<> {
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class FingerScanMisplacedTrigger : public Trigger<> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit FingerScanMisplacedTrigger(FingerprintGrowComponent *parent) {
 | 
			
		||||
    parent->add_on_finger_scan_misplaced_callback([this]() { this->trigger(); });
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class FingerScanInvalidTrigger : public Trigger<> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit FingerScanInvalidTrigger(FingerprintGrowComponent *parent) {
 | 
			
		||||
    parent->add_on_finger_scan_invalid_callback([this]() { this->trigger(); });
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class EnrollmentScanTrigger : public Trigger<uint8_t, uint16_t> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit EnrollmentScanTrigger(FingerprintGrowComponent *parent) {
 | 
			
		||||
 
 | 
			
		||||
@@ -67,13 +67,13 @@ def validate_pillow_installed(value):
 | 
			
		||||
    except ImportError as err:
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            "Please install the pillow python package to use this feature. "
 | 
			
		||||
            '(pip install "pillow==10.1.0")'
 | 
			
		||||
            '(pip install "pillow==10.2.0")'
 | 
			
		||||
        ) from err
 | 
			
		||||
 | 
			
		||||
    if version.parse(PIL.__version__) != version.parse("10.1.0"):
 | 
			
		||||
    if version.parse(PIL.__version__) != version.parse("10.2.0"):
 | 
			
		||||
        raise cv.Invalid(
 | 
			
		||||
            "Please update your pillow installation to 10.1.0. "
 | 
			
		||||
            '(pip install "pillow==10.1.0")'
 | 
			
		||||
            "Please update your pillow installation to 10.2.0. "
 | 
			
		||||
            '(pip install "pillow==10.2.0")'
 | 
			
		||||
        )
 | 
			
		||||
 | 
			
		||||
    return value
 | 
			
		||||
@@ -235,7 +235,7 @@ FILE_SCHEMA = cv.Schema(_file_schema)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
DEFAULT_GLYPHS = (
 | 
			
		||||
    ' !"%()+=,-.:/0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
 | 
			
		||||
    ' !"%()+=,-.:/?0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz°'
 | 
			
		||||
)
 | 
			
		||||
CONF_RAW_GLYPH_ID = "raw_glyph_id"
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -13,14 +13,14 @@
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace ft63x6 {
 | 
			
		||||
 | 
			
		||||
static const uint8_t FT63X6_ADDR_TOUCH_COUNT = 0x02;
 | 
			
		||||
 | 
			
		||||
static const uint8_t FT63X6_ADDR_TOUCH1_ID = 0x05;
 | 
			
		||||
static const uint8_t FT63X6_ADDR_TOUCH1_STATE = 0x03;
 | 
			
		||||
static const uint8_t FT63X6_ADDR_TOUCH1_X = 0x03;
 | 
			
		||||
static const uint8_t FT63X6_ADDR_TOUCH1_ID = 0x05;
 | 
			
		||||
static const uint8_t FT63X6_ADDR_TOUCH1_Y = 0x05;
 | 
			
		||||
 | 
			
		||||
static const uint8_t FT63X6_ADDR_TOUCH2_ID = 0x0B;
 | 
			
		||||
static const uint8_t FT63X6_ADDR_TOUCH2_STATE = 0x09;
 | 
			
		||||
static const uint8_t FT63X6_ADDR_TOUCH2_X = 0x09;
 | 
			
		||||
static const uint8_t FT63X6_ADDR_TOUCH2_ID = 0x0B;
 | 
			
		||||
static const uint8_t FT63X6_ADDR_TOUCH2_Y = 0x0B;
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "FT63X6Touchscreen";
 | 
			
		||||
@@ -40,26 +40,11 @@ void FT63X6Touchscreen::setup() {
 | 
			
		||||
  this->hard_reset_();
 | 
			
		||||
 | 
			
		||||
  // Get touch resolution
 | 
			
		||||
  this->x_raw_max_ = 320;
 | 
			
		||||
  this->y_raw_max_ = 480;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void FT63X6Touchscreen::update_touches() {
 | 
			
		||||
  int touch_count = this->read_touch_count_();
 | 
			
		||||
  if (touch_count == 0) {
 | 
			
		||||
    return;
 | 
			
		||||
  if (this->x_raw_max_ == this->x_raw_min_) {
 | 
			
		||||
    this->x_raw_max_ = 320;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  uint8_t touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH1_ID);  // id1 = 0 or 1
 | 
			
		||||
  int16_t x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_X);
 | 
			
		||||
  int16_t y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH1_Y);
 | 
			
		||||
  this->add_raw_touch_position_(touch_id, x, y);
 | 
			
		||||
 | 
			
		||||
  if (touch_count >= 2) {
 | 
			
		||||
    touch_id = this->read_touch_id_(FT63X6_ADDR_TOUCH2_ID);  // id2 = 0 or 1(~id1 & 0x01)
 | 
			
		||||
    x = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_X);
 | 
			
		||||
    y = this->read_touch_coordinate_(FT63X6_ADDR_TOUCH2_Y);
 | 
			
		||||
    this->add_raw_touch_position_(touch_id, x, y);
 | 
			
		||||
  if (this->y_raw_max_ == this->y_raw_min_) {
 | 
			
		||||
    this->y_raw_max_ = 480;
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -76,23 +61,31 @@ void FT63X6Touchscreen::dump_config() {
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
  LOG_PIN("  Interrupt Pin: ", this->interrupt_pin_);
 | 
			
		||||
  LOG_PIN("  Reset Pin: ", this->reset_pin_);
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
uint8_t FT63X6Touchscreen::read_touch_count_() { return this->read_byte_(FT63X6_ADDR_TOUCH_COUNT); }
 | 
			
		||||
void FT63X6Touchscreen::update_touches() {
 | 
			
		||||
  uint8_t data[15];
 | 
			
		||||
  uint16_t touch_id, x, y;
 | 
			
		||||
 | 
			
		||||
// Touch functions
 | 
			
		||||
uint16_t FT63X6Touchscreen::read_touch_coordinate_(uint8_t coordinate) {
 | 
			
		||||
  uint8_t read_buf[2];
 | 
			
		||||
  read_buf[0] = this->read_byte_(coordinate);
 | 
			
		||||
  read_buf[1] = this->read_byte_(coordinate + 1);
 | 
			
		||||
  return ((read_buf[0] & 0x0f) << 8) | read_buf[1];
 | 
			
		||||
}
 | 
			
		||||
uint8_t FT63X6Touchscreen::read_touch_id_(uint8_t id_address) { return this->read_byte_(id_address) >> 4; }
 | 
			
		||||
  if (!this->read_bytes(0x00, (uint8_t *) data, 15)) {
 | 
			
		||||
    ESP_LOGE(TAG, "Failed to read touch data");
 | 
			
		||||
    this->skip_update_ = true;
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
uint8_t FT63X6Touchscreen::read_byte_(uint8_t addr) {
 | 
			
		||||
  uint8_t byte = 0;
 | 
			
		||||
  this->read_byte(addr, &byte);
 | 
			
		||||
  return byte;
 | 
			
		||||
  if (((data[FT63X6_ADDR_TOUCH1_STATE] >> 6) & 0x01) == 0) {
 | 
			
		||||
    touch_id = data[FT63X6_ADDR_TOUCH1_ID] >> 4;  // id1 = 0 or 1
 | 
			
		||||
    x = encode_uint16(data[FT63X6_ADDR_TOUCH1_X] & 0x0F, data[FT63X6_ADDR_TOUCH1_X + 1]);
 | 
			
		||||
    y = encode_uint16(data[FT63X6_ADDR_TOUCH1_Y] & 0x0F, data[FT63X6_ADDR_TOUCH1_Y + 1]);
 | 
			
		||||
    this->add_raw_touch_position_(touch_id, x, y);
 | 
			
		||||
  }
 | 
			
		||||
  if (((data[FT63X6_ADDR_TOUCH2_STATE] >> 6) & 0x01) == 0) {
 | 
			
		||||
    touch_id = data[FT63X6_ADDR_TOUCH2_ID] >> 4;  // id1 = 0 or 1
 | 
			
		||||
    x = encode_uint16(data[FT63X6_ADDR_TOUCH2_X] & 0x0F, data[FT63X6_ADDR_TOUCH2_X + 1]);
 | 
			
		||||
    y = encode_uint16(data[FT63X6_ADDR_TOUCH2_Y] & 0x0F, data[FT63X6_ADDR_TOUCH2_Y + 1]);
 | 
			
		||||
    this->add_raw_touch_position_(touch_id, x, y);
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
}  // namespace ft63x6
 | 
			
		||||
 
 | 
			
		||||
@@ -61,6 +61,7 @@ VALUE_POSITION_TYPE = {
 | 
			
		||||
    "BELOW": ValuePositionType.VALUE_POSITION_TYPE_BELOW,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
CONF_CONTINUOUS = "continuous"
 | 
			
		||||
 | 
			
		||||
GRAPH_TRACE_SCHEMA = cv.Schema(
 | 
			
		||||
    {
 | 
			
		||||
@@ -70,6 +71,7 @@ GRAPH_TRACE_SCHEMA = cv.Schema(
 | 
			
		||||
        cv.Optional(CONF_LINE_THICKNESS): cv.positive_int,
 | 
			
		||||
        cv.Optional(CONF_LINE_TYPE): cv.enum(LINE_TYPE, upper=True),
 | 
			
		||||
        cv.Optional(CONF_COLOR): cv.use_id(color.ColorStruct),
 | 
			
		||||
        cv.Optional(CONF_CONTINUOUS): cv.boolean,
 | 
			
		||||
    }
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
@@ -186,6 +188,8 @@ async def to_code(config):
 | 
			
		||||
        if CONF_COLOR in trace:
 | 
			
		||||
            c = await cg.get_variable(trace[CONF_COLOR])
 | 
			
		||||
            cg.add(tr.set_line_color(c))
 | 
			
		||||
        if CONF_CONTINUOUS in trace:
 | 
			
		||||
            cg.add(tr.set_continuous(trace[CONF_CONTINUOUS]))
 | 
			
		||||
        cg.add(var.add_trace(tr))
 | 
			
		||||
    # Add legend
 | 
			
		||||
    if CONF_LEGEND in config:
 | 
			
		||||
 
 | 
			
		||||
@@ -165,17 +165,42 @@ void Graph::draw(Display *buff, uint16_t x_offset, uint16_t y_offset, Color colo
 | 
			
		||||
  for (auto *trace : traces_) {
 | 
			
		||||
    Color c = trace->get_line_color();
 | 
			
		||||
    uint16_t thick = trace->get_line_thickness();
 | 
			
		||||
    bool continuous = trace->get_continuous();
 | 
			
		||||
    bool has_prev = false;
 | 
			
		||||
    bool prev_b = false;
 | 
			
		||||
    int16_t prev_y = 0;
 | 
			
		||||
    for (uint32_t i = 0; i < this->width_; i++) {
 | 
			
		||||
      float v = (trace->get_tracedata()->get_value(i) - ymin) / yrange;
 | 
			
		||||
      if (!std::isnan(v) && (thick > 0)) {
 | 
			
		||||
        int16_t x = this->width_ - 1 - i;
 | 
			
		||||
        uint8_t b = (i % (thick * LineType::PATTERN_LENGTH)) / thick;
 | 
			
		||||
        if (((uint8_t) trace->get_line_type() & (1 << b)) == (1 << b)) {
 | 
			
		||||
          int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2;
 | 
			
		||||
          for (uint16_t t = 0; t < thick; t++) {
 | 
			
		||||
            buff->draw_pixel_at(x_offset + x, y_offset + y + t, c);
 | 
			
		||||
        int16_t x = this->width_ - 1 - i + x_offset;
 | 
			
		||||
        uint8_t bit = 1 << ((i % (thick * LineType::PATTERN_LENGTH)) / thick);
 | 
			
		||||
        bool b = (trace->get_line_type() & bit) == bit;
 | 
			
		||||
        if (b) {
 | 
			
		||||
          int16_t y = (int16_t) roundf((this->height_ - 1) * (1.0 - v)) - thick / 2 + y_offset;
 | 
			
		||||
          if (!continuous || !has_prev || !prev_b || (abs(y - prev_y) <= thick)) {
 | 
			
		||||
            for (uint16_t t = 0; t < thick; t++) {
 | 
			
		||||
              buff->draw_pixel_at(x, y + t, c);
 | 
			
		||||
            }
 | 
			
		||||
          } else {
 | 
			
		||||
            int16_t mid_y = (y + prev_y + thick) / 2;
 | 
			
		||||
            if (y > prev_y) {
 | 
			
		||||
              for (uint16_t t = prev_y + thick; t <= mid_y; t++)
 | 
			
		||||
                buff->draw_pixel_at(x + 1, t, c);
 | 
			
		||||
              for (uint16_t t = mid_y + 1; t < y + thick; t++)
 | 
			
		||||
                buff->draw_pixel_at(x, t, c);
 | 
			
		||||
            } else {
 | 
			
		||||
              for (uint16_t t = prev_y - 1; t >= mid_y; t--)
 | 
			
		||||
                buff->draw_pixel_at(x + 1, t, c);
 | 
			
		||||
              for (uint16_t t = mid_y - 1; t >= y; t--)
 | 
			
		||||
                buff->draw_pixel_at(x, t, c);
 | 
			
		||||
            }
 | 
			
		||||
          }
 | 
			
		||||
          prev_y = y;
 | 
			
		||||
        }
 | 
			
		||||
        prev_b = b;
 | 
			
		||||
        has_prev = true;
 | 
			
		||||
      } else {
 | 
			
		||||
        has_prev = false;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
 
 | 
			
		||||
@@ -116,6 +116,8 @@ class GraphTrace {
 | 
			
		||||
  void set_line_type(enum LineType val) { this->line_type_ = val; }
 | 
			
		||||
  Color get_line_color() { return this->line_color_; }
 | 
			
		||||
  void set_line_color(Color val) { this->line_color_ = val; }
 | 
			
		||||
  bool get_continuous() { return this->continuous_; }
 | 
			
		||||
  void set_continuous(bool continuous) { this->continuous_ = continuous; }
 | 
			
		||||
  std::string get_name() { return name_; }
 | 
			
		||||
  const HistoryData *get_tracedata() { return &data_; }
 | 
			
		||||
 | 
			
		||||
@@ -125,6 +127,7 @@ class GraphTrace {
 | 
			
		||||
  uint8_t line_thickness_{3};
 | 
			
		||||
  enum LineType line_type_ { LINE_TYPE_SOLID };
 | 
			
		||||
  Color line_color_{COLOR_ON};
 | 
			
		||||
  bool continuous_{false};
 | 
			
		||||
  HistoryData data_;
 | 
			
		||||
 | 
			
		||||
  friend Graph;
 | 
			
		||||
 
 | 
			
		||||
@@ -14,6 +14,7 @@ static const uint8_t GET_TOUCHES[2] = {0x81, 0x4F};
 | 
			
		||||
static const uint8_t GET_SWITCHES[2] = {0x80, 0x4D};
 | 
			
		||||
static const uint8_t GET_MAX_VALUES[2] = {0x80, 0x48};
 | 
			
		||||
static const size_t MAX_TOUCHES = 5;  // max number of possible touches reported
 | 
			
		||||
static const size_t MAX_BUTTONS = 4;  // max number of buttons scanned
 | 
			
		||||
 | 
			
		||||
#define ERROR_CHECK(err) \
 | 
			
		||||
  if ((err) != i2c::ERROR_OK) { \
 | 
			
		||||
@@ -79,9 +80,6 @@ void GT911Touchscreen::update_touches() {
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (num_of_touches == 0)
 | 
			
		||||
    return;
 | 
			
		||||
 | 
			
		||||
  err = this->write(GET_TOUCHES, sizeof(GET_TOUCHES), false);
 | 
			
		||||
  ERROR_CHECK(err);
 | 
			
		||||
  // num_of_touches is guaranteed to be 0..5. Also read the key data
 | 
			
		||||
@@ -94,10 +92,13 @@ void GT911Touchscreen::update_touches() {
 | 
			
		||||
    uint16_t y = encode_uint16(data[i][4], data[i][3]);
 | 
			
		||||
    this->add_raw_touch_position_(id, x, y);
 | 
			
		||||
  }
 | 
			
		||||
  auto keys = data[num_of_touches][0];
 | 
			
		||||
  for (size_t i = 0; i != 4; i++) {
 | 
			
		||||
    for (auto *listener : this->button_listeners_)
 | 
			
		||||
      listener->update_button(i, (keys & (1 << i)) != 0);
 | 
			
		||||
  auto keys = data[num_of_touches][0] & ((1 << MAX_BUTTONS) - 1);
 | 
			
		||||
  if (keys != this->button_state_) {
 | 
			
		||||
    this->button_state_ = keys;
 | 
			
		||||
    for (size_t i = 0; i != MAX_BUTTONS; i++) {
 | 
			
		||||
      for (auto *listener : this->button_listeners_)
 | 
			
		||||
        listener->update_button(i, (keys & (1 << i)) != 0);
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
 
 | 
			
		||||
@@ -26,6 +26,7 @@ class GT911Touchscreen : public touchscreen::Touchscreen, public i2c::I2CDevice
 | 
			
		||||
 | 
			
		||||
  InternalGPIOPin *interrupt_pin_{};
 | 
			
		||||
  std::vector<GT911ButtonListener *> button_listeners_;
 | 
			
		||||
  uint8_t button_state_{0xFF};  // last button state. Initial FF guarantees first update.
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace gt911
 | 
			
		||||
 
 | 
			
		||||
@@ -18,6 +18,7 @@ from esphome.const import (
 | 
			
		||||
    CONF_SUPPORTED_SWING_MODES,
 | 
			
		||||
    CONF_TARGET_TEMPERATURE,
 | 
			
		||||
    CONF_TEMPERATURE_STEP,
 | 
			
		||||
    CONF_TRIGGER_ID,
 | 
			
		||||
    CONF_VISUAL,
 | 
			
		||||
    CONF_WIFI,
 | 
			
		||||
    DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
@@ -49,6 +50,8 @@ CONF_CONTROL_METHOD = "control_method"
 | 
			
		||||
CONF_CONTROL_PACKET_SIZE = "control_packet_size"
 | 
			
		||||
CONF_DISPLAY = "display"
 | 
			
		||||
CONF_HORIZONTAL_AIRFLOW = "horizontal_airflow"
 | 
			
		||||
CONF_ON_ALARM_START = "on_alarm_start"
 | 
			
		||||
CONF_ON_ALARM_END = "on_alarm_end"
 | 
			
		||||
CONF_OUTDOOR_TEMPERATURE = "outdoor_temperature"
 | 
			
		||||
CONF_VERTICAL_AIRFLOW = "vertical_airflow"
 | 
			
		||||
CONF_WIFI_SIGNAL = "wifi_signal"
 | 
			
		||||
@@ -85,8 +88,8 @@ AIRFLOW_HORIZONTAL_DIRECTION_OPTIONS = {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SUPPORTED_SWING_MODES_OPTIONS = {
 | 
			
		||||
    "OFF": ClimateSwingMode.CLIMATE_SWING_OFF,  # always available
 | 
			
		||||
    "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL,  # always available
 | 
			
		||||
    "OFF": ClimateSwingMode.CLIMATE_SWING_OFF,
 | 
			
		||||
    "VERTICAL": ClimateSwingMode.CLIMATE_SWING_VERTICAL,
 | 
			
		||||
    "HORIZONTAL": ClimateSwingMode.CLIMATE_SWING_HORIZONTAL,
 | 
			
		||||
    "BOTH": ClimateSwingMode.CLIMATE_SWING_BOTH,
 | 
			
		||||
}
 | 
			
		||||
@@ -101,13 +104,15 @@ SUPPORTED_CLIMATE_MODES_OPTIONS = {
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS = {
 | 
			
		||||
    "AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
 | 
			
		||||
    "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
 | 
			
		||||
    "COMFORT": ClimatePreset.CLIMATE_PRESET_COMFORT,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS = {
 | 
			
		||||
    "ECO": ClimatePreset.CLIMATE_PRESET_ECO,
 | 
			
		||||
    "AWAY": ClimatePreset.CLIMATE_PRESET_AWAY,
 | 
			
		||||
    "BOOST": ClimatePreset.CLIMATE_PRESET_BOOST,
 | 
			
		||||
    "ECO": ClimatePreset.CLIMATE_PRESET_ECO,
 | 
			
		||||
    "SLEEP": ClimatePreset.CLIMATE_PRESET_SLEEP,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -118,6 +123,16 @@ SUPPORTED_HON_CONTROL_METHODS = {
 | 
			
		||||
    "SET_SINGLE_PARAMETER": HonControlMethod.SET_SINGLE_PARAMETER,
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
HaierAlarmStartTrigger = haier_ns.class_(
 | 
			
		||||
    "HaierAlarmStartTrigger",
 | 
			
		||||
    automation.Trigger.template(cg.uint8, cg.const_char_ptr),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
HaierAlarmEndTrigger = haier_ns.class_(
 | 
			
		||||
    "HaierAlarmEndTrigger",
 | 
			
		||||
    automation.Trigger.template(cg.uint8, cg.const_char_ptr),
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
 | 
			
		||||
def validate_visual(config):
 | 
			
		||||
    if CONF_VISUAL in config:
 | 
			
		||||
@@ -200,9 +215,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
                    ): cv.boolean,
 | 
			
		||||
                    cv.Optional(
 | 
			
		||||
                        CONF_SUPPORTED_PRESETS,
 | 
			
		||||
                        default=list(
 | 
			
		||||
                            SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS.keys()
 | 
			
		||||
                        ),
 | 
			
		||||
                        default=list(["BOOST", "COMFORT"]),  # No AWAY by default
 | 
			
		||||
                    ): cv.ensure_list(
 | 
			
		||||
                        cv.enum(SUPPORTED_CLIMATE_PRESETS_SMARTAIR2_OPTIONS, upper=True)
 | 
			
		||||
                    ),
 | 
			
		||||
@@ -222,7 +235,7 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
                    ): cv.int_range(min=PROTOCOL_CONTROL_PACKET_SIZE, max=50),
 | 
			
		||||
                    cv.Optional(
 | 
			
		||||
                        CONF_SUPPORTED_PRESETS,
 | 
			
		||||
                        default=list(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS.keys()),
 | 
			
		||||
                        default=list(["BOOST", "ECO", "SLEEP"]),  # No AWAY by default
 | 
			
		||||
                    ): cv.ensure_list(
 | 
			
		||||
                        cv.enum(SUPPORTED_CLIMATE_PRESETS_HON_OPTIONS, upper=True)
 | 
			
		||||
                    ),
 | 
			
		||||
@@ -233,6 +246,20 @@ CONFIG_SCHEMA = cv.All(
 | 
			
		||||
                        device_class=DEVICE_CLASS_TEMPERATURE,
 | 
			
		||||
                        state_class=STATE_CLASS_MEASUREMENT,
 | 
			
		||||
                    ),
 | 
			
		||||
                    cv.Optional(CONF_ON_ALARM_START): automation.validate_automation(
 | 
			
		||||
                        {
 | 
			
		||||
                            cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                                HaierAlarmStartTrigger
 | 
			
		||||
                            ),
 | 
			
		||||
                        }
 | 
			
		||||
                    ),
 | 
			
		||||
                    cv.Optional(CONF_ON_ALARM_END): automation.validate_automation(
 | 
			
		||||
                        {
 | 
			
		||||
                            cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(
 | 
			
		||||
                                HaierAlarmEndTrigger
 | 
			
		||||
                            ),
 | 
			
		||||
                        }
 | 
			
		||||
                    ),
 | 
			
		||||
                }
 | 
			
		||||
            ),
 | 
			
		||||
        },
 | 
			
		||||
@@ -457,5 +484,15 @@ async def to_code(config):
 | 
			
		||||
                config[CONF_CONTROL_PACKET_SIZE] - PROTOCOL_CONTROL_PACKET_SIZE
 | 
			
		||||
            )
 | 
			
		||||
        )
 | 
			
		||||
    for conf in config.get(CONF_ON_ALARM_START, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await automation.build_automation(
 | 
			
		||||
            trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf
 | 
			
		||||
        )
 | 
			
		||||
    for conf in config.get(CONF_ON_ALARM_END, []):
 | 
			
		||||
        trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var)
 | 
			
		||||
        await automation.build_automation(
 | 
			
		||||
            trigger, [(cg.uint8, "code"), (cg.const_char_ptr, "message")], conf
 | 
			
		||||
        )
 | 
			
		||||
    # https://github.com/paveldn/HaierProtocol
 | 
			
		||||
    cg.add_library("pavlodn/HaierProtocol", "0.9.24")
 | 
			
		||||
    cg.add_library("pavlodn/HaierProtocol", "0.9.25")
 | 
			
		||||
 
 | 
			
		||||
@@ -25,13 +25,14 @@ const char *HaierClimateBase::phase_to_string_(ProtocolPhases phase) {
 | 
			
		||||
      "SENDING_INIT_1",
 | 
			
		||||
      "SENDING_INIT_2",
 | 
			
		||||
      "SENDING_FIRST_STATUS_REQUEST",
 | 
			
		||||
      "SENDING_ALARM_STATUS_REQUEST",
 | 
			
		||||
      "SENDING_FIRST_ALARM_STATUS_REQUEST",
 | 
			
		||||
      "IDLE",
 | 
			
		||||
      "SENDING_STATUS_REQUEST",
 | 
			
		||||
      "SENDING_UPDATE_SIGNAL_REQUEST",
 | 
			
		||||
      "SENDING_SIGNAL_LEVEL",
 | 
			
		||||
      "SENDING_CONTROL",
 | 
			
		||||
      "SENDING_ACTION_COMMAND",
 | 
			
		||||
      "SENDING_ALARM_STATUS_REQUEST",
 | 
			
		||||
      "UNKNOWN"  // Should be the last!
 | 
			
		||||
  };
 | 
			
		||||
  static_assert(
 | 
			
		||||
 
 | 
			
		||||
@@ -64,7 +64,7 @@ class HaierClimateBase : public esphome::Component,
 | 
			
		||||
    SENDING_INIT_1 = 0,
 | 
			
		||||
    SENDING_INIT_2,
 | 
			
		||||
    SENDING_FIRST_STATUS_REQUEST,
 | 
			
		||||
    SENDING_ALARM_STATUS_REQUEST,
 | 
			
		||||
    SENDING_FIRST_ALARM_STATUS_REQUEST,
 | 
			
		||||
    // FUNCTIONAL STATE
 | 
			
		||||
    IDLE,
 | 
			
		||||
    SENDING_STATUS_REQUEST,
 | 
			
		||||
@@ -72,6 +72,7 @@ class HaierClimateBase : public esphome::Component,
 | 
			
		||||
    SENDING_SIGNAL_LEVEL,
 | 
			
		||||
    SENDING_CONTROL,
 | 
			
		||||
    SENDING_ACTION_COMMAND,
 | 
			
		||||
    SENDING_ALARM_STATUS_REQUEST,
 | 
			
		||||
    NUM_PROTOCOL_PHASES
 | 
			
		||||
  };
 | 
			
		||||
  const char *phase_to_string_(ProtocolPhases phase);
 | 
			
		||||
 
 | 
			
		||||
@@ -16,6 +16,7 @@ constexpr size_t SIGNAL_LEVEL_UPDATE_INTERVAL_MS = 10000;
 | 
			
		||||
constexpr int PROTOCOL_OUTDOOR_TEMPERATURE_OFFSET = -64;
 | 
			
		||||
constexpr uint8_t CONTROL_MESSAGE_RETRIES = 5;
 | 
			
		||||
constexpr std::chrono::milliseconds CONTROL_MESSAGE_RETRIES_INTERVAL = std::chrono::milliseconds(500);
 | 
			
		||||
constexpr size_t ALARM_STATUS_REQUEST_INTERVAL_MS = 600000;
 | 
			
		||||
 | 
			
		||||
hon_protocol::VerticalSwingMode get_vertical_swing_mode(AirflowVerticalDirection direction) {
 | 
			
		||||
  switch (direction) {
 | 
			
		||||
@@ -110,6 +111,14 @@ void HonClimate::start_steri_cleaning() {
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HonClimate::add_alarm_start_callback(std::function<void(uint8_t, const char *)> &&callback) {
 | 
			
		||||
  this->alarm_start_callback_.add(std::move(callback));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HonClimate::add_alarm_end_callback(std::function<void(uint8_t, const char *)> &&callback) {
 | 
			
		||||
  this->alarm_end_callback_.add(std::move(callback));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError HonClimate::get_device_version_answer_handler_(haier_protocol::FrameType request_type,
 | 
			
		||||
                                                                            haier_protocol::FrameType message_type,
 | 
			
		||||
                                                                            const uint8_t *data, size_t data_size) {
 | 
			
		||||
@@ -194,7 +203,7 @@ haier_protocol::HandlerError HonClimate::status_handler_(haier_protocol::FrameTy
 | 
			
		||||
      switch (this->protocol_phase_) {
 | 
			
		||||
        case ProtocolPhases::SENDING_FIRST_STATUS_REQUEST:
 | 
			
		||||
          ESP_LOGI(TAG, "First HVAC status received");
 | 
			
		||||
          this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST);
 | 
			
		||||
          this->set_phase(ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST);
 | 
			
		||||
          break;
 | 
			
		||||
        case ProtocolPhases::SENDING_ACTION_COMMAND:
 | 
			
		||||
          // Do nothing, phase will be changed in process_phase
 | 
			
		||||
@@ -251,12 +260,15 @@ haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(haier_
 | 
			
		||||
      this->set_phase(ProtocolPhases::IDLE);
 | 
			
		||||
      return haier_protocol::HandlerError::UNSUPPORTED_MESSAGE;
 | 
			
		||||
    }
 | 
			
		||||
    if (this->protocol_phase_ != ProtocolPhases::SENDING_ALARM_STATUS_REQUEST) {
 | 
			
		||||
    if ((this->protocol_phase_ != ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST) &&
 | 
			
		||||
        (this->protocol_phase_ != ProtocolPhases::SENDING_ALARM_STATUS_REQUEST)) {
 | 
			
		||||
      // Don't expect this answer now
 | 
			
		||||
      this->set_phase(ProtocolPhases::IDLE);
 | 
			
		||||
      return haier_protocol::HandlerError::UNEXPECTED_MESSAGE;
 | 
			
		||||
    }
 | 
			
		||||
    memcpy(this->active_alarms_, data + 2, 8);
 | 
			
		||||
    if (data_size < sizeof(active_alarms_) + 2)
 | 
			
		||||
      return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
 | 
			
		||||
    this->process_alarm_message_(data, data_size, this->protocol_phase_ >= ProtocolPhases::IDLE);
 | 
			
		||||
    this->set_phase(ProtocolPhases::IDLE);
 | 
			
		||||
    return haier_protocol::HandlerError::HANDLER_OK;
 | 
			
		||||
  } else {
 | 
			
		||||
@@ -265,6 +277,19 @@ haier_protocol::HandlerError HonClimate::get_alarm_status_answer_handler_(haier_
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError HonClimate::alarm_status_message_handler_(haier_protocol::FrameType type,
 | 
			
		||||
                                                                       const uint8_t *buffer, size_t size) {
 | 
			
		||||
  haier_protocol::HandlerError result = haier_protocol::HandlerError::HANDLER_OK;
 | 
			
		||||
  if (size < sizeof(this->active_alarms_) + 2) {
 | 
			
		||||
    // Log error but confirm anyway to avoid to many messages
 | 
			
		||||
    result = haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
 | 
			
		||||
  }
 | 
			
		||||
  this->process_alarm_message_(buffer, size, true);
 | 
			
		||||
  this->haier_protocol_.send_answer(haier_protocol::HaierMessage(haier_protocol::FrameType::CONFIRM));
 | 
			
		||||
  this->last_alarm_request_ = std::chrono::steady_clock::now();
 | 
			
		||||
  return result;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HonClimate::set_handlers() {
 | 
			
		||||
  // Set handlers
 | 
			
		||||
  this->haier_protocol_.set_answer_handler(
 | 
			
		||||
@@ -291,6 +316,10 @@ void HonClimate::set_handlers() {
 | 
			
		||||
      haier_protocol::FrameType::REPORT_NETWORK_STATUS,
 | 
			
		||||
      std::bind(&HonClimate::report_network_status_answer_handler_, this, std::placeholders::_1, std::placeholders::_2,
 | 
			
		||||
                std::placeholders::_3, std::placeholders::_4));
 | 
			
		||||
  this->haier_protocol_.set_message_handler(
 | 
			
		||||
      haier_protocol::FrameType::ALARM_STATUS,
 | 
			
		||||
      std::bind(&HonClimate::alarm_status_message_handler_, this, std::placeholders::_1, std::placeholders::_2,
 | 
			
		||||
                std::placeholders::_3));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HonClimate::dump_config() {
 | 
			
		||||
@@ -363,10 +392,12 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
 | 
			
		||||
      this->set_phase(ProtocolPhases::IDLE);
 | 
			
		||||
      break;
 | 
			
		||||
#endif
 | 
			
		||||
    case ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST:
 | 
			
		||||
    case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
 | 
			
		||||
      if (this->can_send_message() && this->is_message_interval_exceeded_(now)) {
 | 
			
		||||
        static const haier_protocol::HaierMessage ALARM_STATUS_REQUEST(haier_protocol::FrameType::GET_ALARM_STATUS);
 | 
			
		||||
        this->send_message_(ALARM_STATUS_REQUEST, this->use_crc_);
 | 
			
		||||
        this->last_alarm_request_ = now;
 | 
			
		||||
      }
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::SENDING_CONTROL:
 | 
			
		||||
@@ -417,12 +448,16 @@ void HonClimate::process_phase(std::chrono::steady_clock::time_point now) {
 | 
			
		||||
      if (this->forced_request_status_ || this->is_status_request_interval_exceeded_(now)) {
 | 
			
		||||
        this->set_phase(ProtocolPhases::SENDING_STATUS_REQUEST);
 | 
			
		||||
        this->forced_request_status_ = false;
 | 
			
		||||
      } else if (std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_alarm_request_).count() >
 | 
			
		||||
                 ALARM_STATUS_REQUEST_INTERVAL_MS) {
 | 
			
		||||
        this->set_phase(ProtocolPhases::SENDING_ALARM_STATUS_REQUEST);
 | 
			
		||||
      }
 | 
			
		||||
#ifdef USE_WIFI
 | 
			
		||||
      else if (this->send_wifi_signal_ &&
 | 
			
		||||
               (std::chrono::duration_cast<std::chrono::milliseconds>(now - this->last_signal_request_).count() >
 | 
			
		||||
                SIGNAL_LEVEL_UPDATE_INTERVAL_MS))
 | 
			
		||||
                SIGNAL_LEVEL_UPDATE_INTERVAL_MS)) {
 | 
			
		||||
        this->set_phase(ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST);
 | 
			
		||||
      }
 | 
			
		||||
#endif
 | 
			
		||||
    } break;
 | 
			
		||||
    default:
 | 
			
		||||
@@ -452,6 +487,7 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
 | 
			
		||||
  uint8_t control_out_buffer[sizeof(hon_protocol::HaierPacketControl)];
 | 
			
		||||
  memcpy(control_out_buffer, this->last_status_message_.get(), sizeof(hon_protocol::HaierPacketControl));
 | 
			
		||||
  hon_protocol::HaierPacketControl *out_data = (hon_protocol::HaierPacketControl *) control_out_buffer;
 | 
			
		||||
  control_out_buffer[4] = 0;  // This byte should be cleared before setting values
 | 
			
		||||
  bool has_hvac_settings = false;
 | 
			
		||||
  if (this->current_hvac_settings_.valid) {
 | 
			
		||||
    has_hvac_settings = true;
 | 
			
		||||
@@ -552,31 +588,41 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
 | 
			
		||||
          out_data->quiet_mode = 0;
 | 
			
		||||
          out_data->fast_mode = 0;
 | 
			
		||||
          out_data->sleep_mode = 0;
 | 
			
		||||
          out_data->ten_degree = 0;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_PRESET_ECO:
 | 
			
		||||
          // Eco is not supported in Fan only mode
 | 
			
		||||
          out_data->quiet_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
 | 
			
		||||
          out_data->fast_mode = 0;
 | 
			
		||||
          out_data->sleep_mode = 0;
 | 
			
		||||
          out_data->ten_degree = 0;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_PRESET_BOOST:
 | 
			
		||||
          out_data->quiet_mode = 0;
 | 
			
		||||
          // Boost is not supported in Fan only mode
 | 
			
		||||
          out_data->fast_mode = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 1 : 0;
 | 
			
		||||
          out_data->sleep_mode = 0;
 | 
			
		||||
          out_data->ten_degree = 0;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_PRESET_AWAY:
 | 
			
		||||
          out_data->quiet_mode = 0;
 | 
			
		||||
          out_data->fast_mode = 0;
 | 
			
		||||
          out_data->sleep_mode = 0;
 | 
			
		||||
          // 10 degrees allowed only in heat mode
 | 
			
		||||
          out_data->ten_degree = (this->mode == CLIMATE_MODE_HEAT) ? 1 : 0;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_PRESET_SLEEP:
 | 
			
		||||
          out_data->quiet_mode = 0;
 | 
			
		||||
          out_data->fast_mode = 0;
 | 
			
		||||
          out_data->sleep_mode = 1;
 | 
			
		||||
          out_data->ten_degree = 0;
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          ESP_LOGE("Control", "Unsupported preset");
 | 
			
		||||
          out_data->quiet_mode = 0;
 | 
			
		||||
          out_data->fast_mode = 0;
 | 
			
		||||
          out_data->sleep_mode = 0;
 | 
			
		||||
          out_data->ten_degree = 0;
 | 
			
		||||
          break;
 | 
			
		||||
      }
 | 
			
		||||
    }
 | 
			
		||||
@@ -595,6 +641,50 @@ haier_protocol::HaierMessage HonClimate::get_control_message() {
 | 
			
		||||
                                      control_out_buffer, sizeof(hon_protocol::HaierPacketControl));
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HonClimate::process_alarm_message_(const uint8_t *packet, uint8_t size, bool check_new) {
 | 
			
		||||
  constexpr size_t active_alarms_size = sizeof(this->active_alarms_);
 | 
			
		||||
  if (size >= active_alarms_size + 2) {
 | 
			
		||||
    if (check_new) {
 | 
			
		||||
      size_t alarm_code = 0;
 | 
			
		||||
      for (int i = active_alarms_size - 1; i >= 0; i--) {
 | 
			
		||||
        if (packet[2 + i] != active_alarms_[i]) {
 | 
			
		||||
          uint8_t alarm_bit = 1;
 | 
			
		||||
          for (int b = 0; b < 8; b++) {
 | 
			
		||||
            if ((packet[2 + i] & alarm_bit) != (this->active_alarms_[i] & alarm_bit)) {
 | 
			
		||||
              bool alarm_status = (packet[2 + i] & alarm_bit) != 0;
 | 
			
		||||
              int log_level = alarm_status ? ESPHOME_LOG_LEVEL_WARN : ESPHOME_LOG_LEVEL_INFO;
 | 
			
		||||
              const char *alarm_message = alarm_code < esphome::haier::hon_protocol::HON_ALARM_COUNT
 | 
			
		||||
                                              ? esphome::haier::hon_protocol::HON_ALARM_MESSAGES[alarm_code].c_str()
 | 
			
		||||
                                              : "Unknown";
 | 
			
		||||
              esp_log_printf_(log_level, TAG, __LINE__, "Alarm %s (%d): %s", alarm_status ? "activated" : "deactivated",
 | 
			
		||||
                              alarm_code, alarm_message);
 | 
			
		||||
              if (alarm_status) {
 | 
			
		||||
                this->alarm_start_callback_.call(alarm_code, alarm_message);
 | 
			
		||||
                this->active_alarm_count_ += 1.0f;
 | 
			
		||||
              } else {
 | 
			
		||||
                this->alarm_end_callback_.call(alarm_code, alarm_message);
 | 
			
		||||
                this->active_alarm_count_ -= 1.0f;
 | 
			
		||||
              }
 | 
			
		||||
            }
 | 
			
		||||
            alarm_bit <<= 1;
 | 
			
		||||
            alarm_code++;
 | 
			
		||||
          }
 | 
			
		||||
          active_alarms_[i] = packet[2 + i];
 | 
			
		||||
        } else
 | 
			
		||||
          alarm_code += 8;
 | 
			
		||||
      }
 | 
			
		||||
    } else {
 | 
			
		||||
      float alarm_count = 0.0f;
 | 
			
		||||
      static uint8_t nibble_bits_count[] = {0, 1, 1, 2, 1, 2, 2, 3, 1, 2, 2, 3, 2, 3, 3, 4};
 | 
			
		||||
      for (size_t i = 0; i < sizeof(this->active_alarms_); i++) {
 | 
			
		||||
        alarm_count += (float) (nibble_bits_count[packet[2 + i] & 0x0F] + nibble_bits_count[packet[2 + i] >> 4]);
 | 
			
		||||
      }
 | 
			
		||||
      this->active_alarm_count_ = alarm_count;
 | 
			
		||||
      memcpy(this->active_alarms_, packet + 2, sizeof(this->active_alarms_));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *packet_buffer, uint8_t size) {
 | 
			
		||||
  if (size < hon_protocol::HAIER_STATUS_FRAME_SIZE + this->extra_control_packet_bytes_)
 | 
			
		||||
    return haier_protocol::HandlerError::WRONG_MESSAGE_STRUCTURE;
 | 
			
		||||
@@ -626,6 +716,8 @@ haier_protocol::HandlerError HonClimate::process_status_message_(const uint8_t *
 | 
			
		||||
      this->preset = CLIMATE_PRESET_BOOST;
 | 
			
		||||
    } else if (packet.control.sleep_mode != 0) {
 | 
			
		||||
      this->preset = CLIMATE_PRESET_SLEEP;
 | 
			
		||||
    } else if (packet.control.ten_degree != 0) {
 | 
			
		||||
      this->preset = CLIMATE_PRESET_AWAY;
 | 
			
		||||
    } else {
 | 
			
		||||
      this->preset = CLIMATE_PRESET_NONE;
 | 
			
		||||
    }
 | 
			
		||||
@@ -882,25 +974,35 @@ void HonClimate::fill_control_messages_queue_() {
 | 
			
		||||
  // CLimate preset
 | 
			
		||||
  {
 | 
			
		||||
    uint8_t fast_mode_buf[] = {0x00, 0xFF};
 | 
			
		||||
    uint8_t away_mode_buf[] = {0x00, 0xFF};
 | 
			
		||||
    if (!new_power) {
 | 
			
		||||
      // If AC is off - no presets allowed
 | 
			
		||||
      quiet_mode_buf[1] = 0x00;
 | 
			
		||||
      fast_mode_buf[1] = 0x00;
 | 
			
		||||
      away_mode_buf[1] = 0x00;
 | 
			
		||||
    } else if (climate_control.preset.has_value()) {
 | 
			
		||||
      switch (climate_control.preset.value()) {
 | 
			
		||||
        case CLIMATE_PRESET_NONE:
 | 
			
		||||
          quiet_mode_buf[1] = 0x00;
 | 
			
		||||
          fast_mode_buf[1] = 0x00;
 | 
			
		||||
          away_mode_buf[1] = 0x00;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_PRESET_ECO:
 | 
			
		||||
          // Eco is not supported in Fan only mode
 | 
			
		||||
          quiet_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00;
 | 
			
		||||
          fast_mode_buf[1] = 0x00;
 | 
			
		||||
          away_mode_buf[1] = 0x00;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_PRESET_BOOST:
 | 
			
		||||
          quiet_mode_buf[1] = 0x00;
 | 
			
		||||
          // Boost is not supported in Fan only mode
 | 
			
		||||
          fast_mode_buf[1] = (this->mode != CLIMATE_MODE_FAN_ONLY) ? 0x01 : 0x00;
 | 
			
		||||
          away_mode_buf[1] = 0x00;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_PRESET_AWAY:
 | 
			
		||||
          quiet_mode_buf[1] = 0x00;
 | 
			
		||||
          fast_mode_buf[1] = 0x00;
 | 
			
		||||
          away_mode_buf[1] = (this->mode == CLIMATE_MODE_HEAT) ? 0x01 : 0x00;
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          ESP_LOGE("Control", "Unsupported preset");
 | 
			
		||||
@@ -921,6 +1023,13 @@ void HonClimate::fill_control_messages_queue_() {
 | 
			
		||||
                                           (uint8_t) hon_protocol::DataParameters::FAST_MODE,
 | 
			
		||||
                                       fast_mode_buf, 2));
 | 
			
		||||
    }
 | 
			
		||||
    if (away_mode_buf[1] != 0xFF) {
 | 
			
		||||
      this->control_messages_queue_.push(
 | 
			
		||||
          haier_protocol::HaierMessage(haier_protocol::FrameType::CONTROL,
 | 
			
		||||
                                       (uint16_t) hon_protocol::SubcommandsControl::SET_SINGLE_PARAMETER +
 | 
			
		||||
                                           (uint8_t) hon_protocol::DataParameters::TEN_DEGREE,
 | 
			
		||||
                                       away_mode_buf, 2));
 | 
			
		||||
    }
 | 
			
		||||
  }
 | 
			
		||||
  // Target temperature
 | 
			
		||||
  if (climate_control.target_temperature.has_value()) {
 | 
			
		||||
 
 | 
			
		||||
@@ -2,6 +2,7 @@
 | 
			
		||||
 | 
			
		||||
#include <chrono>
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/core/automation.h"
 | 
			
		||||
#include "haier_base.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
@@ -52,6 +53,9 @@ class HonClimate : public HaierClimateBase {
 | 
			
		||||
  void start_steri_cleaning();
 | 
			
		||||
  void set_extra_control_packet_bytes_size(size_t size) { this->extra_control_packet_bytes_ = size; };
 | 
			
		||||
  void set_control_method(HonControlMethod method) { this->control_method_ = method; };
 | 
			
		||||
  void add_alarm_start_callback(std::function<void(uint8_t, const char *)> &&callback);
 | 
			
		||||
  void add_alarm_end_callback(std::function<void(uint8_t, const char *)> &&callback);
 | 
			
		||||
  float get_active_alarm_count() const { return this->active_alarm_count_; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  void set_handlers() override;
 | 
			
		||||
@@ -77,8 +81,11 @@ class HonClimate : public HaierClimateBase {
 | 
			
		||||
  haier_protocol::HandlerError get_alarm_status_answer_handler_(haier_protocol::FrameType request_type,
 | 
			
		||||
                                                                haier_protocol::FrameType message_type,
 | 
			
		||||
                                                                const uint8_t *data, size_t data_size);
 | 
			
		||||
  haier_protocol::HandlerError alarm_status_message_handler_(haier_protocol::FrameType type, const uint8_t *buffer,
 | 
			
		||||
                                                             size_t size);
 | 
			
		||||
  // Helper functions
 | 
			
		||||
  haier_protocol::HandlerError process_status_message_(const uint8_t *packet, uint8_t size);
 | 
			
		||||
  void process_alarm_message_(const uint8_t *packet, uint8_t size, bool check_new);
 | 
			
		||||
  void fill_control_messages_queue_();
 | 
			
		||||
  void clear_control_messages_queue_();
 | 
			
		||||
 | 
			
		||||
@@ -101,6 +108,26 @@ class HonClimate : public HaierClimateBase {
 | 
			
		||||
  HonControlMethod control_method_;
 | 
			
		||||
  esphome::sensor::Sensor *outdoor_sensor_;
 | 
			
		||||
  std::queue<haier_protocol::HaierMessage> control_messages_queue_;
 | 
			
		||||
  CallbackManager<void(uint8_t, const char *)> alarm_start_callback_{};
 | 
			
		||||
  CallbackManager<void(uint8_t, const char *)> alarm_end_callback_{};
 | 
			
		||||
  float active_alarm_count_{NAN};
 | 
			
		||||
  std::chrono::steady_clock::time_point last_alarm_request_;
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class HaierAlarmStartTrigger : public Trigger<uint8_t, const char *> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit HaierAlarmStartTrigger(HonClimate *parent) {
 | 
			
		||||
    parent->add_alarm_start_callback(
 | 
			
		||||
        [this](uint8_t alarm_code, const char *alarm_message) { this->trigger(alarm_code, alarm_message); });
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
class HaierAlarmEndTrigger : public Trigger<uint8_t, const char *> {
 | 
			
		||||
 public:
 | 
			
		||||
  explicit HaierAlarmEndTrigger(HonClimate *parent) {
 | 
			
		||||
    parent->add_alarm_end_callback(
 | 
			
		||||
        [this](uint8_t alarm_code, const char *alarm_message) { this->trigger(alarm_code, alarm_message); });
 | 
			
		||||
  }
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace haier
 | 
			
		||||
 
 | 
			
		||||
@@ -163,6 +163,62 @@ enum class SubcommandsControl : uint16_t {
 | 
			
		||||
                                  // content: all values like in status packet)
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
const std::string HON_ALARM_MESSAGES[] = {
 | 
			
		||||
    "Outdoor module failure",
 | 
			
		||||
    "Outdoor defrost sensor failure",
 | 
			
		||||
    "Outdoor compressor exhaust sensor failure",
 | 
			
		||||
    "Outdoor EEPROM abnormality",
 | 
			
		||||
    "Indoor coil sensor failure",
 | 
			
		||||
    "Indoor-outdoor communication failure",
 | 
			
		||||
    "Power supply overvoltage protection",
 | 
			
		||||
    "Communication failure between panel and indoor unit",
 | 
			
		||||
    "Outdoor compressor overheat protection",
 | 
			
		||||
    "Outdoor environmental sensor abnormality",
 | 
			
		||||
    "Full water protection",
 | 
			
		||||
    "Indoor EEPROM failure",
 | 
			
		||||
    "Outdoor out air sensor failure",
 | 
			
		||||
    "CBD and module communication failure",
 | 
			
		||||
    "Indoor DC fan failure",
 | 
			
		||||
    "Outdoor DC fan failure",
 | 
			
		||||
    "Door switch failure",
 | 
			
		||||
    "Dust filter needs cleaning reminder",
 | 
			
		||||
    "Water shortage protection",
 | 
			
		||||
    "Humidity sensor failure",
 | 
			
		||||
    "Indoor temperature sensor failure",
 | 
			
		||||
    "Manipulator limit failure",
 | 
			
		||||
    "Indoor PM2.5 sensor failure",
 | 
			
		||||
    "Outdoor PM2.5 sensor failure",
 | 
			
		||||
    "Indoor heating overload/high load alarm",
 | 
			
		||||
    "Outdoor AC current protection",
 | 
			
		||||
    "Outdoor compressor operation abnormality",
 | 
			
		||||
    "Outdoor DC current protection",
 | 
			
		||||
    "Outdoor no-load failure",
 | 
			
		||||
    "CT current abnormality",
 | 
			
		||||
    "Indoor cooling freeze protection",
 | 
			
		||||
    "High and low pressure protection",
 | 
			
		||||
    "Compressor out air temperature is too high",
 | 
			
		||||
    "Outdoor evaporator sensor failure",
 | 
			
		||||
    "Outdoor cooling overload",
 | 
			
		||||
    "Water pump drainage failure",
 | 
			
		||||
    "Three-phase power supply failure",
 | 
			
		||||
    "Four-way valve failure",
 | 
			
		||||
    "External alarm/scraper flow switch failure",
 | 
			
		||||
    "Temperature cutoff protection alarm",
 | 
			
		||||
    "Different mode operation failure",
 | 
			
		||||
    "Electronic expansion valve failure",
 | 
			
		||||
    "Dual heat source sensor Tw failure",
 | 
			
		||||
    "Communication failure with the wired controller",
 | 
			
		||||
    "Indoor unit address duplication failure",
 | 
			
		||||
    "50Hz zero crossing failure",
 | 
			
		||||
    "Outdoor unit failure",
 | 
			
		||||
    "Formaldehyde sensor failure",
 | 
			
		||||
    "VOC sensor failure",
 | 
			
		||||
    "CO2 sensor failure",
 | 
			
		||||
    "Firewall failure",
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
constexpr size_t HON_ALARM_COUNT = sizeof(HON_ALARM_MESSAGES) / sizeof(HON_ALARM_MESSAGES[0]);
 | 
			
		||||
 | 
			
		||||
}  // namespace hon_protocol
 | 
			
		||||
}  // namespace haier
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
 
 | 
			
		||||
@@ -95,7 +95,7 @@ haier_protocol::HandlerError Smartair2Climate::messages_timeout_handler_with_cyc
 | 
			
		||||
  ESP_LOGI(TAG, "Answer timeout for command %02X, phase %s", (uint8_t) message_type,
 | 
			
		||||
           phase_to_string_(this->protocol_phase_));
 | 
			
		||||
  ProtocolPhases new_phase = (ProtocolPhases) ((int) this->protocol_phase_ + 1);
 | 
			
		||||
  if (new_phase >= ProtocolPhases::SENDING_ALARM_STATUS_REQUEST)
 | 
			
		||||
  if (new_phase >= ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST)
 | 
			
		||||
    new_phase = ProtocolPhases::SENDING_INIT_1;
 | 
			
		||||
  this->set_phase(new_phase);
 | 
			
		||||
  return haier_protocol::HandlerError::HANDLER_OK;
 | 
			
		||||
@@ -170,9 +170,12 @@ void Smartair2Climate::process_phase(std::chrono::steady_clock::time_point now)
 | 
			
		||||
    case ProtocolPhases::SENDING_UPDATE_SIGNAL_REQUEST:
 | 
			
		||||
      this->set_phase(ProtocolPhases::SENDING_SIGNAL_LEVEL);
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
 | 
			
		||||
    case ProtocolPhases::SENDING_FIRST_ALARM_STATUS_REQUEST:
 | 
			
		||||
      this->set_phase(ProtocolPhases::SENDING_INIT_1);
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::SENDING_ALARM_STATUS_REQUEST:
 | 
			
		||||
      this->set_phase(ProtocolPhases::IDLE);
 | 
			
		||||
      break;
 | 
			
		||||
    case ProtocolPhases::SENDING_CONTROL:
 | 
			
		||||
      if (this->can_send_message() && this->is_control_message_interval_exceeded_(now)) {
 | 
			
		||||
        ESP_LOGI(TAG, "Sending control packet");
 | 
			
		||||
@@ -343,19 +346,29 @@ haier_protocol::HaierMessage Smartair2Climate::get_control_message() {
 | 
			
		||||
    } else if (climate_control.preset.has_value()) {
 | 
			
		||||
      switch (climate_control.preset.value()) {
 | 
			
		||||
        case CLIMATE_PRESET_NONE:
 | 
			
		||||
          out_data->ten_degree = 0;
 | 
			
		||||
          out_data->turbo_mode = 0;
 | 
			
		||||
          out_data->quiet_mode = 0;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_PRESET_BOOST:
 | 
			
		||||
          out_data->ten_degree = 0;
 | 
			
		||||
          out_data->turbo_mode = 1;
 | 
			
		||||
          out_data->quiet_mode = 0;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_PRESET_COMFORT:
 | 
			
		||||
          out_data->ten_degree = 0;
 | 
			
		||||
          out_data->turbo_mode = 0;
 | 
			
		||||
          out_data->quiet_mode = 1;
 | 
			
		||||
          break;
 | 
			
		||||
        case CLIMATE_PRESET_AWAY:
 | 
			
		||||
          // Only allowed in heat mode
 | 
			
		||||
          out_data->ten_degree = (this->mode == CLIMATE_MODE_HEAT) ? 1 : 0;
 | 
			
		||||
          out_data->turbo_mode = 0;
 | 
			
		||||
          out_data->quiet_mode = 0;
 | 
			
		||||
          break;
 | 
			
		||||
        default:
 | 
			
		||||
          ESP_LOGE("Control", "Unsupported preset");
 | 
			
		||||
          out_data->ten_degree = 0;
 | 
			
		||||
          out_data->turbo_mode = 0;
 | 
			
		||||
          out_data->quiet_mode = 0;
 | 
			
		||||
          break;
 | 
			
		||||
@@ -381,6 +394,8 @@ haier_protocol::HandlerError Smartair2Climate::process_status_message_(const uin
 | 
			
		||||
      this->preset = CLIMATE_PRESET_BOOST;
 | 
			
		||||
    } else if (packet.control.quiet_mode != 0) {
 | 
			
		||||
      this->preset = CLIMATE_PRESET_COMFORT;
 | 
			
		||||
    } else if (packet.control.ten_degree != 0) {
 | 
			
		||||
      this->preset = CLIMATE_PRESET_AWAY;
 | 
			
		||||
    } else {
 | 
			
		||||
      this->preset = CLIMATE_PRESET_NONE;
 | 
			
		||||
    }
 | 
			
		||||
 
 | 
			
		||||
@@ -96,7 +96,7 @@ void HLW8012Component::update() {
 | 
			
		||||
    this->energy_sensor_->publish_state(energy);
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  if (this->change_mode_at_++ == this->change_mode_every_) {
 | 
			
		||||
  if (this->change_mode_every_ != 0 && this->change_mode_at_++ == this->change_mode_every_) {
 | 
			
		||||
    this->current_mode_ = !this->current_mode_;
 | 
			
		||||
    ESP_LOGV(TAG, "Changing mode to %s mode", this->current_mode_ ? "CURRENT" : "VOLTAGE");
 | 
			
		||||
    this->change_mode_at_ = 0;
 | 
			
		||||
 
 | 
			
		||||
@@ -79,8 +79,9 @@ CONFIG_SCHEMA = cv.Schema(
 | 
			
		||||
        cv.Optional(CONF_CURRENT_RESISTOR, default=0.001): cv.resistance,
 | 
			
		||||
        cv.Optional(CONF_VOLTAGE_DIVIDER, default=2351): cv.positive_float,
 | 
			
		||||
        cv.Optional(CONF_MODEL, default="HLW8012"): cv.enum(MODELS, upper=True),
 | 
			
		||||
        cv.Optional(CONF_CHANGE_MODE_EVERY, default=8): cv.All(
 | 
			
		||||
            cv.uint32_t, cv.Range(min=1)
 | 
			
		||||
        cv.Optional(CONF_CHANGE_MODE_EVERY, default=8): cv.Any(
 | 
			
		||||
            "never",
 | 
			
		||||
            cv.All(cv.uint32_t, cv.Range(min=1)),
 | 
			
		||||
        ),
 | 
			
		||||
        cv.Optional(CONF_INITIAL_MODE, default=CONF_VOLTAGE): cv.one_of(
 | 
			
		||||
            *INITIAL_MODES, lower=True
 | 
			
		||||
@@ -114,6 +115,10 @@ async def to_code(config):
 | 
			
		||||
        cg.add(var.set_energy_sensor(sens))
 | 
			
		||||
    cg.add(var.set_current_resistor(config[CONF_CURRENT_RESISTOR]))
 | 
			
		||||
    cg.add(var.set_voltage_divider(config[CONF_VOLTAGE_DIVIDER]))
 | 
			
		||||
    cg.add(var.set_change_mode_every(config[CONF_CHANGE_MODE_EVERY]))
 | 
			
		||||
    cg.add(var.set_initial_mode(INITIAL_MODES[config[CONF_INITIAL_MODE]]))
 | 
			
		||||
    cg.add(var.set_sensor_model(config[CONF_MODEL]))
 | 
			
		||||
 | 
			
		||||
    interval = config[CONF_CHANGE_MODE_EVERY]
 | 
			
		||||
    if interval == "never":
 | 
			
		||||
        interval = 0
 | 
			
		||||
    cg.add(var.set_change_mode_every(interval))
 | 
			
		||||
 
 | 
			
		||||
							
								
								
									
										2
									
								
								esphome/components/honeywell_hih_i2c/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										2
									
								
								esphome/components/honeywell_hih_i2c/__init__.py
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,2 @@
 | 
			
		||||
"""Support for Honeywell HumidIcon HIH"""
 | 
			
		||||
CODEOWNERS = ["@Benichou34"]
 | 
			
		||||
							
								
								
									
										97
									
								
								esphome/components/honeywell_hih_i2c/honeywell_hih.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										97
									
								
								esphome/components/honeywell_hih_i2c/honeywell_hih.cpp
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,97 @@
 | 
			
		||||
// Honeywell HumidIcon I2C Sensors
 | 
			
		||||
// https://prod-edam.honeywell.com/content/dam/honeywell-edam/sps/siot/en-us/products/sensors/humidity-with-temperature-sensors/common/documents/sps-siot-i2c-comms-humidicon-tn-009061-2-en-ciid-142171.pdf
 | 
			
		||||
//
 | 
			
		||||
 | 
			
		||||
#include "honeywell_hih.h"
 | 
			
		||||
#include "esphome/core/log.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace honeywell_hih_i2c {
 | 
			
		||||
 | 
			
		||||
static const char *const TAG = "honeywell_hih.i2c";
 | 
			
		||||
 | 
			
		||||
static const uint8_t REQUEST_CMD[1] = {0x00};  // Measurement Request Format
 | 
			
		||||
static const uint16_t MAX_COUNT = 0x3FFE;      // 2^14 - 2
 | 
			
		||||
 | 
			
		||||
void HoneywellHIComponent::read_sensor_data_() {
 | 
			
		||||
  uint8_t data[4];
 | 
			
		||||
 | 
			
		||||
  if (this->read(data, sizeof(data)) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "Communication with Honeywell HIH failed!");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  const uint16_t raw_humidity = (static_cast<uint16_t>(data[0] & 0x3F) << 8) | data[1];
 | 
			
		||||
  float humidity = (static_cast<float>(raw_humidity) / MAX_COUNT) * 100;
 | 
			
		||||
 | 
			
		||||
  const uint16_t raw_temperature = (static_cast<uint16_t>(data[2]) << 6) | (data[3] >> 2);
 | 
			
		||||
  float temperature = (static_cast<float>(raw_temperature) / MAX_COUNT) * 165 - 40;
 | 
			
		||||
 | 
			
		||||
  ESP_LOGD(TAG, "Got temperature=%.2f°C humidity=%.2f%%", temperature, humidity);
 | 
			
		||||
  if (this->temperature_sensor_ != nullptr)
 | 
			
		||||
    this->temperature_sensor_->publish_state(temperature);
 | 
			
		||||
  if (this->humidity_sensor_ != nullptr)
 | 
			
		||||
    this->humidity_sensor_->publish_state(humidity);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HoneywellHIComponent::start_measurement_() {
 | 
			
		||||
  if (this->write(REQUEST_CMD, sizeof(REQUEST_CMD)) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "Communication with Honeywell HIH failed!");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  this->measurement_running_ = true;
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
bool HoneywellHIComponent::is_measurement_ready_() {
 | 
			
		||||
  uint8_t data[1];
 | 
			
		||||
 | 
			
		||||
  if (this->read(data, sizeof(data)) != i2c::ERROR_OK) {
 | 
			
		||||
    ESP_LOGE(TAG, "Communication with Honeywell HIH failed!");
 | 
			
		||||
    this->mark_failed();
 | 
			
		||||
    return false;
 | 
			
		||||
  }
 | 
			
		||||
 | 
			
		||||
  // Check status bits
 | 
			
		||||
  return ((data[0] & 0xC0) == 0x00);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HoneywellHIComponent::measurement_timeout_() {
 | 
			
		||||
  ESP_LOGE(TAG, "Honeywell HIH Timeout!");
 | 
			
		||||
  this->measurement_running_ = false;
 | 
			
		||||
  this->mark_failed();
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HoneywellHIComponent::update() {
 | 
			
		||||
  ESP_LOGV(TAG, "Update Honeywell HIH Sensor");
 | 
			
		||||
 | 
			
		||||
  this->start_measurement_();
 | 
			
		||||
  // The measurement cycle duration is typically 36.65 ms for temperature and humidity readings.
 | 
			
		||||
  this->set_timeout("meas_timeout", 100, [this] { this->measurement_timeout_(); });
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HoneywellHIComponent::loop() {
 | 
			
		||||
  if (this->measurement_running_ && this->is_measurement_ready_()) {
 | 
			
		||||
    this->measurement_running_ = false;
 | 
			
		||||
    this->cancel_timeout("meas_timeout");
 | 
			
		||||
    this->read_sensor_data_();
 | 
			
		||||
  }
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
void HoneywellHIComponent::dump_config() {
 | 
			
		||||
  ESP_LOGD(TAG, "Honeywell HIH:");
 | 
			
		||||
  LOG_I2C_DEVICE(this);
 | 
			
		||||
  if (this->is_failed()) {
 | 
			
		||||
    ESP_LOGE(TAG, "Communication with Honeywell HIH failed!");
 | 
			
		||||
  }
 | 
			
		||||
  LOG_SENSOR("  ", "Temperature", this->temperature_sensor_);
 | 
			
		||||
  LOG_SENSOR("  ", "Humidity", this->humidity_sensor_);
 | 
			
		||||
  LOG_UPDATE_INTERVAL(this);
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
float HoneywellHIComponent::get_setup_priority() const { return setup_priority::DATA; }
 | 
			
		||||
 | 
			
		||||
}  // namespace honeywell_hih_i2c
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
							
								
								
									
										34
									
								
								esphome/components/honeywell_hih_i2c/honeywell_hih.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								esphome/components/honeywell_hih_i2c/honeywell_hih.h
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,34 @@
 | 
			
		||||
// Honeywell HumidIcon I2C Sensors
 | 
			
		||||
#pragma once
 | 
			
		||||
 | 
			
		||||
#include "esphome/core/component.h"
 | 
			
		||||
#include "esphome/components/sensor/sensor.h"
 | 
			
		||||
#include "esphome/components/i2c/i2c.h"
 | 
			
		||||
 | 
			
		||||
namespace esphome {
 | 
			
		||||
namespace honeywell_hih_i2c {
 | 
			
		||||
 | 
			
		||||
class HoneywellHIComponent : public PollingComponent, public i2c::I2CDevice {
 | 
			
		||||
 public:
 | 
			
		||||
  void dump_config() override;
 | 
			
		||||
  float get_setup_priority() const override;
 | 
			
		||||
  void loop() override;
 | 
			
		||||
  void update() override;
 | 
			
		||||
 | 
			
		||||
  void set_temperature_sensor(sensor::Sensor *temperature_sensor) { this->temperature_sensor_ = temperature_sensor; }
 | 
			
		||||
  void set_humidity_sensor(sensor::Sensor *humidity_sensor) { this->humidity_sensor_ = humidity_sensor; }
 | 
			
		||||
 | 
			
		||||
 protected:
 | 
			
		||||
  bool measurement_running_{false};
 | 
			
		||||
  sensor::Sensor *temperature_sensor_{nullptr};
 | 
			
		||||
  sensor::Sensor *humidity_sensor_{nullptr};
 | 
			
		||||
 | 
			
		||||
 private:
 | 
			
		||||
  void read_sensor_data_();
 | 
			
		||||
  void start_measurement_();
 | 
			
		||||
  bool is_measurement_ready_();
 | 
			
		||||
  void measurement_timeout_();
 | 
			
		||||
};
 | 
			
		||||
 | 
			
		||||
}  // namespace honeywell_hih_i2c
 | 
			
		||||
}  // namespace esphome
 | 
			
		||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user