mirror of
				https://github.com/esphome/esphome.git
				synced 2025-10-30 22:53:59 +00:00 
			
		
		
		
	| @@ -40,6 +40,7 @@ | |||||||
|         "yaml.customTags": [ |         "yaml.customTags": [ | ||||||
|           "!secret scalar", |           "!secret scalar", | ||||||
|           "!lambda scalar", |           "!lambda scalar", | ||||||
|  |           "!extend scalar", | ||||||
|           "!include_dir_named scalar", |           "!include_dir_named scalar", | ||||||
|           "!include_dir_list scalar", |           "!include_dir_list scalar", | ||||||
|           "!include_dir_merge_list scalar", |           "!include_dir_merge_list scalar", | ||||||
|   | |||||||
							
								
								
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										1
									
								
								.gitattributes
									
									
									
									
										vendored
									
									
								
							| @@ -1,2 +1,3 @@ | |||||||
| # Normalize line endings to LF in the repository | # Normalize line endings to LF in the repository | ||||||
| * text eol=lf | * text eol=lf | ||||||
|  | *.png binary | ||||||
|   | |||||||
							
								
								
									
										396
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										396
									
								
								.github/workflows/ci.yml
									
									
									
									
										vendored
									
									
								
							| @@ -12,60 +12,266 @@ on: | |||||||
| permissions: | permissions: | ||||||
|   contents: read |   contents: read | ||||||
|  |  | ||||||
|  | env: | ||||||
|  |   DEFAULT_PYTHON: "3.9" | ||||||
|  |   PYUPGRADE_TARGET: "--py39-plus" | ||||||
|  |   CLANG_FORMAT_VERSION: "13.0.1" | ||||||
|  |  | ||||||
| concurrency: | concurrency: | ||||||
|   # yamllint disable-line rule:line-length |   # yamllint disable-line rule:line-length | ||||||
|   group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} |   group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | ||||||
|   cancel-in-progress: true |   cancel-in-progress: true | ||||||
|  |  | ||||||
| jobs: | jobs: | ||||||
|   ci: |   common: | ||||||
|     name: ${{ matrix.name }} |     name: Create common environment | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - name: Check out code from GitHub | ||||||
|  |         uses: actions/checkout@v3.5.2 | ||||||
|  |       - name: Set up Python ${{ env.DEFAULT_PYTHON }} | ||||||
|  |         uses: actions/setup-python@v4.6.0 | ||||||
|  |         with: | ||||||
|  |           python-version: ${{ env.DEFAULT_PYTHON }} | ||||||
|  |       - name: Restore Python virtual environment | ||||||
|  |         id: cache-venv | ||||||
|  |         uses: actions/cache@v3.3.1 | ||||||
|  |         with: | ||||||
|  |           path: venv | ||||||
|  |           # yamllint disable-line rule:line-length | ||||||
|  |           key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} | ||||||
|  |       - name: Create Python virtual environment | ||||||
|  |         if: steps.cache-venv.outputs.cache-hit != 'true' | ||||||
|  |         run: | | ||||||
|  |           python -m venv venv | ||||||
|  |           . venv/bin/activate | ||||||
|  |           python --version | ||||||
|  |           pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt | ||||||
|  |           pip install -e . | ||||||
|  |  | ||||||
|  |   yamllint: | ||||||
|  |     name: yamllint | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     steps: | ||||||
|  |       - name: Check out code from GitHub | ||||||
|  |         uses: actions/checkout@v3.5.2 | ||||||
|  |       - name: Run yamllint | ||||||
|  |         uses: frenck/action-yamllint@v1.4.1 | ||||||
|  |  | ||||||
|  |   black: | ||||||
|  |     name: Check black | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     needs: | ||||||
|  |       - common | ||||||
|  |     steps: | ||||||
|  |       - name: Check out code from GitHub | ||||||
|  |         uses: actions/checkout@v3.5.2 | ||||||
|  |       - name: Restore Python virtual environment | ||||||
|  |         uses: actions/cache/restore@v3.3.1 | ||||||
|  |         with: | ||||||
|  |           path: venv | ||||||
|  |           # yamllint disable-line rule:line-length | ||||||
|  |           key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} | ||||||
|  |       - name: Run black | ||||||
|  |         run: | | ||||||
|  |           . venv/bin/activate | ||||||
|  |           black --verbose esphome tests | ||||||
|  |       - name: Suggested changes | ||||||
|  |         run: script/ci-suggest-changes | ||||||
|  |         if: always() | ||||||
|  |  | ||||||
|  |   flake8: | ||||||
|  |     name: Check flake8 | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     needs: | ||||||
|  |       - common | ||||||
|  |     steps: | ||||||
|  |       - name: Check out code from GitHub | ||||||
|  |         uses: actions/checkout@v3.5.2 | ||||||
|  |       - name: Restore Python virtual environment | ||||||
|  |         uses: actions/cache/restore@v3.3.1 | ||||||
|  |         with: | ||||||
|  |           path: venv | ||||||
|  |           # yamllint disable-line rule:line-length | ||||||
|  |           key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} | ||||||
|  |       - name: Run flake8 | ||||||
|  |         run: | | ||||||
|  |           . venv/bin/activate | ||||||
|  |           flake8 esphome | ||||||
|  |       - name: Suggested changes | ||||||
|  |         run: script/ci-suggest-changes | ||||||
|  |         if: always() | ||||||
|  |  | ||||||
|  |   pylint: | ||||||
|  |     name: Check pylint | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     needs: | ||||||
|  |       - common | ||||||
|  |     steps: | ||||||
|  |       - name: Check out code from GitHub | ||||||
|  |         uses: actions/checkout@v3.5.2 | ||||||
|  |       - name: Restore Python virtual environment | ||||||
|  |         uses: actions/cache/restore@v3.3.1 | ||||||
|  |         with: | ||||||
|  |           path: venv | ||||||
|  |           # yamllint disable-line rule:line-length | ||||||
|  |           key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} | ||||||
|  |       - name: Run pylint | ||||||
|  |         run: | | ||||||
|  |           . venv/bin/activate | ||||||
|  |           pylint -f parseable --persistent=n esphome | ||||||
|  |       - name: Suggested changes | ||||||
|  |         run: script/ci-suggest-changes | ||||||
|  |         if: always() | ||||||
|  |  | ||||||
|  |   pyupgrade: | ||||||
|  |     name: Check pyupgrade | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     needs: | ||||||
|  |       - common | ||||||
|  |     steps: | ||||||
|  |       - name: Check out code from GitHub | ||||||
|  |         uses: actions/checkout@v3.5.2 | ||||||
|  |       - name: Restore Python virtual environment | ||||||
|  |         uses: actions/cache/restore@v3.3.1 | ||||||
|  |         with: | ||||||
|  |           path: venv | ||||||
|  |           # yamllint disable-line rule:line-length | ||||||
|  |           key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} | ||||||
|  |       - name: Run pyupgrade | ||||||
|  |         run: | | ||||||
|  |           . venv/bin/activate | ||||||
|  |           pyupgrade ${{ env.PYUPGRADE_TARGET }} `find esphome -name "*.py" -type f` | ||||||
|  |       - name: Suggested changes | ||||||
|  |         run: script/ci-suggest-changes | ||||||
|  |         if: always() | ||||||
|  |  | ||||||
|  |   ci-custom: | ||||||
|  |     name: Run script/ci-custom | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     needs: | ||||||
|  |       - common | ||||||
|  |     steps: | ||||||
|  |       - name: Check out code from GitHub | ||||||
|  |         uses: actions/checkout@v3.5.2 | ||||||
|  |       - name: Restore Python virtual environment | ||||||
|  |         uses: actions/cache/restore@v3.3.1 | ||||||
|  |         with: | ||||||
|  |           path: venv | ||||||
|  |           # yamllint disable-line rule:line-length | ||||||
|  |           key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} | ||||||
|  |       - name: Register matcher | ||||||
|  |         run: echo "::add-matcher::.github/workflows/matchers/ci-custom.json" | ||||||
|  |       - name: Run script/ci-custom | ||||||
|  |         run: | | ||||||
|  |           . venv/bin/activate | ||||||
|  |           script/ci-custom.py | ||||||
|  |           script/build_codeowners.py --check | ||||||
|  |  | ||||||
|  |   pytest: | ||||||
|  |     name: Run pytest | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     needs: | ||||||
|  |       - common | ||||||
|  |     steps: | ||||||
|  |       - name: Check out code from GitHub | ||||||
|  |         uses: actions/checkout@v3.5.2 | ||||||
|  |       - name: Restore Python virtual environment | ||||||
|  |         uses: actions/cache/restore@v3.3.1 | ||||||
|  |         with: | ||||||
|  |           path: venv | ||||||
|  |           # yamllint disable-line rule:line-length | ||||||
|  |           key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} | ||||||
|  |       - name: Register matcher | ||||||
|  |         run: echo "::add-matcher::.github/workflows/matchers/pytest.json" | ||||||
|  |       - name: Run pytest | ||||||
|  |         run: | | ||||||
|  |           . venv/bin/activate | ||||||
|  |           pytest -vv --tb=native tests | ||||||
|  |  | ||||||
|  |   clang-format: | ||||||
|  |     name: Check clang-format | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     needs: | ||||||
|  |       - common | ||||||
|  |     steps: | ||||||
|  |       - name: Check out code from GitHub | ||||||
|  |         uses: actions/checkout@v3.5.2 | ||||||
|  |       - name: Restore Python virtual environment | ||||||
|  |         uses: actions/cache/restore@v3.3.1 | ||||||
|  |         with: | ||||||
|  |           path: venv | ||||||
|  |           # yamllint disable-line rule:line-length | ||||||
|  |           key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} | ||||||
|  |       - name: Install clang-format | ||||||
|  |         run: | | ||||||
|  |           . venv/bin/activate | ||||||
|  |           pip install clang-format==${{ env.CLANG_FORMAT_VERSION }} | ||||||
|  |       - name: Run clang-format | ||||||
|  |         run: | | ||||||
|  |           . venv/bin/activate | ||||||
|  |           script/clang-format -i | ||||||
|  |           git diff-index --quiet HEAD -- | ||||||
|  |       - name: Suggested changes | ||||||
|  |         run: script/ci-suggest-changes | ||||||
|  |         if: always() | ||||||
|  |  | ||||||
|  |   compile-tests: | ||||||
|  |     name: Run YAML test ${{ matrix.file }} | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     needs: | ||||||
|  |       - common | ||||||
|  |       - black | ||||||
|  |       - ci-custom | ||||||
|  |       - clang-format | ||||||
|  |       - flake8 | ||||||
|  |       - pylint | ||||||
|  |       - pytest | ||||||
|  |       - pyupgrade | ||||||
|  |       - yamllint | ||||||
|     strategy: |     strategy: | ||||||
|       fail-fast: false |       fail-fast: false | ||||||
|       max-parallel: 5 |       max-parallel: 2 | ||||||
|  |       matrix: | ||||||
|  |         file: [1, 2, 3, 3.1, 4, 5, 6, 7, 8] | ||||||
|  |     steps: | ||||||
|  |       - name: Check out code from GitHub | ||||||
|  |         uses: actions/checkout@v3.5.2 | ||||||
|  |       - name: Restore Python virtual environment | ||||||
|  |         uses: actions/cache/restore@v3.3.1 | ||||||
|  |         with: | ||||||
|  |           path: venv | ||||||
|  |           # yamllint disable-line rule:line-length | ||||||
|  |           key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} | ||||||
|  |       - name: Cache platformio | ||||||
|  |         uses: actions/cache@v3.3.1 | ||||||
|  |         with: | ||||||
|  |           path: ~/.platformio | ||||||
|  |           # yamllint disable-line rule:line-length | ||||||
|  |           key: platformio-test${{ matrix.file }}-${{ hashFiles('platformio.ini') }} | ||||||
|  |       - name: Run esphome compile tests/test${{ matrix.file }}.yaml | ||||||
|  |         run: | | ||||||
|  |           . venv/bin/activate | ||||||
|  |           esphome compile tests/test${{ matrix.file }}.yaml | ||||||
|  |  | ||||||
|  |   clang-tidy: | ||||||
|  |     name: ${{ matrix.name }} | ||||||
|  |     runs-on: ubuntu-latest | ||||||
|  |     needs: | ||||||
|  |       - common | ||||||
|  |       - black | ||||||
|  |       - ci-custom | ||||||
|  |       - clang-format | ||||||
|  |       - flake8 | ||||||
|  |       - pylint | ||||||
|  |       - pytest | ||||||
|  |       - pyupgrade | ||||||
|  |       - yamllint | ||||||
|  |     strategy: | ||||||
|  |       fail-fast: false | ||||||
|  |       max-parallel: 2 | ||||||
|       matrix: |       matrix: | ||||||
|         include: |         include: | ||||||
|           - id: ci-custom |  | ||||||
|             name: Run script/ci-custom |  | ||||||
|           - id: lint-python |  | ||||||
|             name: Run script/lint-python |  | ||||||
|           - id: test |  | ||||||
|             file: tests/test1.yaml |  | ||||||
|             name: Test tests/test1.yaml |  | ||||||
|             pio_cache_key: test1 |  | ||||||
|           - id: test |  | ||||||
|             file: tests/test2.yaml |  | ||||||
|             name: Test tests/test2.yaml |  | ||||||
|             pio_cache_key: test2 |  | ||||||
|           - id: test |  | ||||||
|             file: tests/test3.yaml |  | ||||||
|             name: Test tests/test3.yaml |  | ||||||
|             pio_cache_key: test3 |  | ||||||
|           - id: test |  | ||||||
|             file: tests/test3.1.yaml |  | ||||||
|             name: Test tests/test3.1.yaml |  | ||||||
|             pio_cache_key: test3.1 |  | ||||||
|           - id: test |  | ||||||
|             file: tests/test4.yaml |  | ||||||
|             name: Test tests/test4.yaml |  | ||||||
|             pio_cache_key: test4 |  | ||||||
|           - id: test |  | ||||||
|             file: tests/test5.yaml |  | ||||||
|             name: Test tests/test5.yaml |  | ||||||
|             pio_cache_key: test5 |  | ||||||
|           - id: test |  | ||||||
|             file: tests/test6.yaml |  | ||||||
|             name: Test tests/test6.yaml |  | ||||||
|             pio_cache_key: test6 |  | ||||||
|           - id: test |  | ||||||
|             file: tests/test7.yaml |  | ||||||
|             name: Test tests/test7.yaml |  | ||||||
|             pio_cache_key: test7 |  | ||||||
|           - id: pytest |  | ||||||
|             name: Run pytest |  | ||||||
|           - id: clang-format |  | ||||||
|             name: Run script/clang-format |  | ||||||
|           - id: clang-tidy |           - id: clang-tidy | ||||||
|             name: Run script/clang-tidy for ESP8266 |             name: Run script/clang-tidy for ESP8266 | ||||||
|             options: --environment esp8266-arduino-tidy --grep USE_ESP8266 |             options: --environment esp8266-arduino-tidy --grep USE_ESP8266 | ||||||
| @@ -90,119 +296,65 @@ jobs: | |||||||
|             name: Run script/clang-tidy for ESP32 IDF |             name: Run script/clang-tidy for ESP32 IDF | ||||||
|             options: --environment esp32-idf-tidy --grep USE_ESP_IDF |             options: --environment esp32-idf-tidy --grep USE_ESP_IDF | ||||||
|             pio_cache_key: tidyesp32-idf |             pio_cache_key: tidyesp32-idf | ||||||
|           - id: yamllint |  | ||||||
|             name: Run yamllint |  | ||||||
|  |  | ||||||
|     steps: |     steps: | ||||||
|       - uses: actions/checkout@v3 |       - name: Check out code from GitHub | ||||||
|       - name: Set up Python |         uses: actions/checkout@v3.5.2 | ||||||
|         uses: actions/setup-python@v4 |       - name: Restore Python virtual environment | ||||||
|         id: python |         uses: actions/cache/restore@v3.3.1 | ||||||
|         with: |         with: | ||||||
|           python-version: "3.9" |           path: venv | ||||||
|  |  | ||||||
|       - name: Cache virtualenv |  | ||||||
|         uses: actions/cache@v3 |  | ||||||
|         with: |  | ||||||
|           path: .venv |  | ||||||
|           # yamllint disable-line rule:line-length |           # yamllint disable-line rule:line-length | ||||||
|           key: venv-${{ steps.python.outputs.python-version }}-${{ hashFiles('requirements*.txt') }} |           key: ${{ runner.os }}-${{ env.DEFAULT_PYTHON }}-venv-${{ hashFiles('requirements.txt', 'requirements_optional.txt', 'requirements_test.txt') }} | ||||||
|           restore-keys: | |  | ||||||
|             venv-${{ steps.python.outputs.python-version }}- |  | ||||||
|  |  | ||||||
|       - name: Set up virtualenv |  | ||||||
|         # yamllint disable rule:line-length |  | ||||||
|         run: | |  | ||||||
|           python -m venv .venv |  | ||||||
|           source .venv/bin/activate |  | ||||||
|           pip install -U pip |  | ||||||
|           pip install -r requirements.txt -r requirements_optional.txt -r requirements_test.txt |  | ||||||
|           pip install -e . |  | ||||||
|           echo "$GITHUB_WORKSPACE/.venv/bin" >> $GITHUB_PATH |  | ||||||
|           echo "VIRTUAL_ENV=$GITHUB_WORKSPACE/.venv" >> $GITHUB_ENV |  | ||||||
|         # yamllint enable rule:line-length |  | ||||||
|  |  | ||||||
|         # Use per check platformio cache because checks use different parts |         # Use per check platformio cache because checks use different parts | ||||||
|       - name: Cache platformio |       - name: Cache platformio | ||||||
|         uses: actions/cache@v3 |         uses: actions/cache@v3.3.1 | ||||||
|         with: |         with: | ||||||
|           path: ~/.platformio |           path: ~/.platformio | ||||||
|           # yamllint disable-line rule:line-length |           # yamllint disable-line rule:line-length | ||||||
|           key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} |           key: platformio-${{ matrix.pio_cache_key }}-${{ hashFiles('platformio.ini') }} | ||||||
|         if: matrix.id == 'test' || matrix.id == 'clang-tidy' |  | ||||||
|  |  | ||||||
|       - name: Install clang tools |       - name: Install clang-tidy | ||||||
|         run: | |         run: sudo apt-get install clang-tidy-11 | ||||||
|           sudo apt-get install \ |  | ||||||
|               clang-format-13 \ |  | ||||||
|               clang-tidy-11 |  | ||||||
|         if: matrix.id == 'clang-tidy' || matrix.id == 'clang-format' |  | ||||||
|  |  | ||||||
|       - name: Register problem matchers |       - name: Register problem matchers | ||||||
|         run: | |         run: | | ||||||
|           echo "::add-matcher::.github/workflows/matchers/ci-custom.json" |  | ||||||
|           echo "::add-matcher::.github/workflows/matchers/lint-python.json" |  | ||||||
|           echo "::add-matcher::.github/workflows/matchers/python.json" |  | ||||||
|           echo "::add-matcher::.github/workflows/matchers/pytest.json" |  | ||||||
|           echo "::add-matcher::.github/workflows/matchers/gcc.json" |           echo "::add-matcher::.github/workflows/matchers/gcc.json" | ||||||
|           echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" |           echo "::add-matcher::.github/workflows/matchers/clang-tidy.json" | ||||||
|  |  | ||||||
|       - name: Lint Custom |  | ||||||
|         run: | |  | ||||||
|           script/ci-custom.py |  | ||||||
|           script/build_codeowners.py --check |  | ||||||
|         if: matrix.id == 'ci-custom' |  | ||||||
|  |  | ||||||
|       - name: Lint Python |  | ||||||
|         run: script/lint-python -a |  | ||||||
|         if: matrix.id == 'lint-python' |  | ||||||
|  |  | ||||||
|       - run: esphome compile ${{ matrix.file }} |  | ||||||
|         if: matrix.id == 'test' |  | ||||||
|         env: |  | ||||||
|           # Also cache libdeps, store them in a ~/.platformio subfolder |  | ||||||
|           PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps |  | ||||||
|  |  | ||||||
|       - name: Run pytest |  | ||||||
|         run: | |  | ||||||
|           pytest -vv --tb=native tests |  | ||||||
|         if: matrix.id == 'pytest' |  | ||||||
|  |  | ||||||
|       # Also run git-diff-index so that the step is marked as failed on |  | ||||||
|       # formatting errors, since clang-format doesn't do anything but |  | ||||||
|       # change files if -i is passed. |  | ||||||
|       - name: Run clang-format |  | ||||||
|         run: | |  | ||||||
|           script/clang-format -i |  | ||||||
|           git diff-index --quiet HEAD -- |  | ||||||
|         if: matrix.id == 'clang-format' |  | ||||||
|  |  | ||||||
|       - name: Run clang-tidy |       - name: Run clang-tidy | ||||||
|         run: | |         run: | | ||||||
|  |           . venv/bin/activate | ||||||
|           script/clang-tidy --all-headers --fix ${{ matrix.options }} |           script/clang-tidy --all-headers --fix ${{ matrix.options }} | ||||||
|         if: matrix.id == 'clang-tidy' |  | ||||||
|         env: |         env: | ||||||
|           # Also cache libdeps, store them in a ~/.platformio subfolder |           # Also cache libdeps, store them in a ~/.platformio subfolder | ||||||
|           PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps |           PLATFORMIO_LIBDEPS_DIR: ~/.platformio/libdeps | ||||||
|  |  | ||||||
|       - name: Run yamllint |  | ||||||
|         if: matrix.id == 'yamllint' |  | ||||||
|         uses: frenck/action-yamllint@v1.4.0 |  | ||||||
|  |  | ||||||
|       - name: Suggested changes |       - name: Suggested changes | ||||||
|         run: script/ci-suggest-changes |         run: script/ci-suggest-changes | ||||||
|         # yamllint disable-line rule:line-length |         # yamllint disable-line rule:line-length | ||||||
|         if: always() && (matrix.id == 'clang-tidy' || matrix.id == 'clang-format' || matrix.id == 'lint-python') |         if: always() | ||||||
|  |  | ||||||
|   ci-status: |   ci-status: | ||||||
|     name: CI Status |     name: CI Status | ||||||
|     runs-on: ubuntu-latest |     runs-on: ubuntu-latest | ||||||
|     needs: [ci] |     needs: | ||||||
|  |       - common | ||||||
|  |       - black | ||||||
|  |       - ci-custom | ||||||
|  |       - clang-format | ||||||
|  |       - flake8 | ||||||
|  |       - pylint | ||||||
|  |       - pytest | ||||||
|  |       - pyupgrade | ||||||
|  |       - yamllint | ||||||
|  |       - compile-tests | ||||||
|  |       - clang-tidy | ||||||
|     if: always() |     if: always() | ||||||
|     steps: |     steps: | ||||||
|       - name: Successful deploy |       - name: Success | ||||||
|         if: ${{ !(contains(needs.*.result, 'failure')) }} |         if: ${{ !(contains(needs.*.result, 'failure')) }} | ||||||
|         run: exit 0 |         run: exit 0 | ||||||
|       - name: Failing deploy |       - name: Failure | ||||||
|         if: ${{ contains(needs.*.result, 'failure') }} |         if: ${{ contains(needs.*.result, 'failure') }} | ||||||
|         run: exit 1 |         run: exit 1 | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/release.yml
									
									
									
									
										vendored
									
									
								
							| @@ -49,9 +49,11 @@ jobs: | |||||||
|         with: |         with: | ||||||
|           python-version: "3.x" |           python-version: "3.x" | ||||||
|       - name: Set up python environment |       - name: Set up python environment | ||||||
|  |         env: | ||||||
|  |           ESPHOME_NO_VENV: 1 | ||||||
|         run: | |         run: | | ||||||
|           script/setup |           script/setup | ||||||
|           pip install setuptools wheel twine |           pip install twine | ||||||
|       - name: Build |       - name: Build | ||||||
|         run: python setup.py sdist bdist_wheel |         run: python setup.py sdist bdist_wheel | ||||||
|       - name: Upload |       - name: Upload | ||||||
|   | |||||||
							
								
								
									
										2
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										2
									
								
								.github/workflows/stale.yml
									
									
									
									
										vendored
									
									
								
							| @@ -26,7 +26,7 @@ jobs: | |||||||
|           days-before-issue-close: -1 |           days-before-issue-close: -1 | ||||||
|           remove-stale-when-updated: true |           remove-stale-when-updated: true | ||||||
|           stale-pr-label: "stale" |           stale-pr-label: "stale" | ||||||
|           exempt-pr-labels: "no-stale" |           exempt-pr-labels: "not-stale" | ||||||
|           stale-pr-message: > |           stale-pr-message: > | ||||||
|             There hasn't been any activity on this pull request recently. This |             There hasn't been any activity on this pull request recently. This | ||||||
|             pull request has been automatically marked as stale because of that |             pull request has been automatically marked as stale because of that | ||||||
|   | |||||||
							
								
								
									
										4
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										4
									
								
								.github/workflows/sync-device-classes.yml
									
									
									
									
										vendored
									
									
								
							| @@ -53,8 +53,8 @@ jobs: | |||||||
|           commit-message: "Synchronise Device Classes from Home Assistant" |           commit-message: "Synchronise Device Classes from Home Assistant" | ||||||
|           committer: esphomebot <esphome@nabucasa.com> |           committer: esphomebot <esphome@nabucasa.com> | ||||||
|           author: esphomebot <esphome@nabucasa.com> |           author: esphomebot <esphome@nabucasa.com> | ||||||
|           branch: sync/device-classes/ |           branch: sync/device-classes | ||||||
|           branch-suffix: timestamp |  | ||||||
|           delete-branch: true |           delete-branch: true | ||||||
|           title: "Synchronise Device Classes from Home Assistant" |           title: "Synchronise Device Classes from Home Assistant" | ||||||
|           body: ${{ steps.pr-template-body.outputs.body }} |           body: ${{ steps.pr-template-body.outputs.body }} | ||||||
|  |           token: ${{ secrets.DEVICE_CLASS_SYNC_TOKEN }} | ||||||
|   | |||||||
| @@ -27,7 +27,7 @@ repos: | |||||||
|           - --branch=release |           - --branch=release | ||||||
|           - --branch=beta |           - --branch=beta | ||||||
|   - repo: https://github.com/asottile/pyupgrade |   - repo: https://github.com/asottile/pyupgrade | ||||||
|     rev: v3.3.2 |     rev: v3.4.0 | ||||||
|     hooks: |     hooks: | ||||||
|       - id: pyupgrade |       - id: pyupgrade | ||||||
|         args: [--py39-plus] |         args: [--py39-plus] | ||||||
|   | |||||||
							
								
								
									
										18
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							
							
						
						
									
										18
									
								
								.vscode/tasks.json
									
									
									
									
										vendored
									
									
								
							| @@ -36,6 +36,24 @@ | |||||||
|           ] |           ] | ||||||
|         } |         } | ||||||
|       ] |       ] | ||||||
|  |     }, | ||||||
|  |     { | ||||||
|  |       "label": "Generate proto files", | ||||||
|  |       "type": "shell", | ||||||
|  |       "command": "${command:python.interpreterPath}", | ||||||
|  |       "args": [ | ||||||
|  |         "./script/api_protobuf/api_protobuf.py" | ||||||
|  |       ], | ||||||
|  |       "group": { | ||||||
|  |         "kind": "build", | ||||||
|  |         "isDefault": true | ||||||
|  |       }, | ||||||
|  |       "presentation": { | ||||||
|  |         "reveal": "never", | ||||||
|  |         "close": true, | ||||||
|  |         "panel": "new" | ||||||
|  |       }, | ||||||
|  |       "problemMatcher": [] | ||||||
|     } |     } | ||||||
|   ] |   ] | ||||||
| } | } | ||||||
|   | |||||||
| @@ -17,8 +17,10 @@ esphome/components/adc/* @esphome/core | |||||||
| esphome/components/adc128s102/* @DeerMaximum | esphome/components/adc128s102/* @DeerMaximum | ||||||
| esphome/components/addressable_light/* @justfalter | esphome/components/addressable_light/* @justfalter | ||||||
| esphome/components/airthings_ble/* @jeromelaban | esphome/components/airthings_ble/* @jeromelaban | ||||||
|  | esphome/components/airthings_wave_base/* @jeromelaban @ncareau | ||||||
| esphome/components/airthings_wave_mini/* @ncareau | esphome/components/airthings_wave_mini/* @ncareau | ||||||
| esphome/components/airthings_wave_plus/* @jeromelaban | esphome/components/airthings_wave_plus/* @jeromelaban | ||||||
|  | esphome/components/alarm_control_panel/* @grahambrown11 | ||||||
| esphome/components/am43/* @buxtronix | esphome/components/am43/* @buxtronix | ||||||
| esphome/components/am43/cover/* @buxtronix | esphome/components/am43/cover/* @buxtronix | ||||||
| esphome/components/am43/sensor/* @buxtronix | esphome/components/am43/sensor/* @buxtronix | ||||||
| @@ -107,6 +109,7 @@ esphome/components/hbridge/fan/* @WeekendWarrior | |||||||
| esphome/components/hbridge/light/* @DotNetDann | esphome/components/hbridge/light/* @DotNetDann | ||||||
| esphome/components/heatpumpir/* @rob-deutsch | esphome/components/heatpumpir/* @rob-deutsch | ||||||
| esphome/components/hitachi_ac424/* @sourabhjaiswal | esphome/components/hitachi_ac424/* @sourabhjaiswal | ||||||
|  | esphome/components/hm3301/* @freekode | ||||||
| esphome/components/homeassistant/* @OttoWinter | esphome/components/homeassistant/* @OttoWinter | ||||||
| esphome/components/honeywellabp/* @RubyBailey | esphome/components/honeywellabp/* @RubyBailey | ||||||
| esphome/components/host/* @esphome/core | esphome/components/host/* @esphome/core | ||||||
| @@ -220,6 +223,7 @@ esphome/components/restart/* @esphome/core | |||||||
| esphome/components/rf_bridge/* @jesserockz | esphome/components/rf_bridge/* @jesserockz | ||||||
| esphome/components/rgbct/* @jesserockz | esphome/components/rgbct/* @jesserockz | ||||||
| esphome/components/rp2040/* @jesserockz | esphome/components/rp2040/* @jesserockz | ||||||
|  | esphome/components/rp2040_pio_led_strip/* @Papa-DMan | ||||||
| esphome/components/rp2040_pwm/* @jesserockz | esphome/components/rp2040_pwm/* @jesserockz | ||||||
| esphome/components/rtttl/* @glmnet | esphome/components/rtttl/* @glmnet | ||||||
| esphome/components/safe_mode/* @jsuanet @paulmonigatti | esphome/components/safe_mode/* @jsuanet @paulmonigatti | ||||||
| @@ -275,13 +279,16 @@ esphome/components/tca9548a/* @andreashergert1984 | |||||||
| esphome/components/tcl112/* @glmnet | esphome/components/tcl112/* @glmnet | ||||||
| esphome/components/tee501/* @Stock-M | esphome/components/tee501/* @Stock-M | ||||||
| esphome/components/teleinfo/* @0hax | esphome/components/teleinfo/* @0hax | ||||||
|  | esphome/components/template/alarm_control_panel/* @grahambrown11 | ||||||
| esphome/components/thermostat/* @kbx81 | esphome/components/thermostat/* @kbx81 | ||||||
| esphome/components/time/* @OttoWinter | esphome/components/time/* @OttoWinter | ||||||
| esphome/components/tlc5947/* @rnauber | esphome/components/tlc5947/* @rnauber | ||||||
| esphome/components/tm1621/* @Philippe12 | esphome/components/tm1621/* @Philippe12 | ||||||
| esphome/components/tm1637/* @glmnet | esphome/components/tm1637/* @glmnet | ||||||
| esphome/components/tm1638/* @skykingjwc | esphome/components/tm1638/* @skykingjwc | ||||||
|  | esphome/components/tm1651/* @freekode | ||||||
| esphome/components/tmp102/* @timsavage | esphome/components/tmp102/* @timsavage | ||||||
|  | esphome/components/tmp1075/* @sybrenstuvel | ||||||
| esphome/components/tmp117/* @Azimath | esphome/components/tmp117/* @Azimath | ||||||
| esphome/components/tof10120/* @wstrzalka | esphome/components/tof10120/* @wstrzalka | ||||||
| esphome/components/toshiba/* @kbx81 | esphome/components/toshiba/* @kbx81 | ||||||
|   | |||||||
| @@ -29,6 +29,8 @@ RUN \ | |||||||
|         git=1:2.30.2-1+deb11u2 \ |         git=1:2.30.2-1+deb11u2 \ | ||||||
|         curl=7.74.0-1.3+deb11u7 \ |         curl=7.74.0-1.3+deb11u7 \ | ||||||
|         openssh-client=1:8.4p1-5+deb11u1 \ |         openssh-client=1:8.4p1-5+deb11u1 \ | ||||||
|  |         libcairo2=1.16.0-5 \ | ||||||
|  |         python3-cffi=1.14.5-1 \ | ||||||
|     && rm -rf \ |     && rm -rf \ | ||||||
|         /tmp/* \ |         /tmp/* \ | ||||||
|         /var/{cache,log}/* \ |         /var/{cache,log}/* \ | ||||||
| @@ -52,7 +54,7 @@ RUN \ | |||||||
|     # Ubuntu python3-pip is missing wheel |     # Ubuntu python3-pip is missing wheel | ||||||
|     pip3 install --no-cache-dir \ |     pip3 install --no-cache-dir \ | ||||||
|         wheel==0.37.1 \ |         wheel==0.37.1 \ | ||||||
|         platformio==6.1.6 \ |         platformio==6.1.7 \ | ||||||
|     # Change some platformio settings |     # Change some platformio settings | ||||||
|     && platformio settings set enable_telemetry No \ |     && platformio settings set enable_telemetry No \ | ||||||
|     && platformio settings set check_platformio_interval 1000000 \ |     && platformio settings set check_platformio_interval 1000000 \ | ||||||
|   | |||||||
| @@ -18,6 +18,9 @@ from esphome.const import ( | |||||||
|     CONF_LOGGER, |     CONF_LOGGER, | ||||||
|     CONF_NAME, |     CONF_NAME, | ||||||
|     CONF_OTA, |     CONF_OTA, | ||||||
|  |     CONF_MQTT, | ||||||
|  |     CONF_MDNS, | ||||||
|  |     CONF_DISABLED, | ||||||
|     CONF_PASSWORD, |     CONF_PASSWORD, | ||||||
|     CONF_PORT, |     CONF_PORT, | ||||||
|     CONF_ESPHOME, |     CONF_ESPHOME, | ||||||
| @@ -42,7 +45,7 @@ from esphome.log import color, setup_log, Fore | |||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  |  | ||||||
| def choose_prompt(options): | def choose_prompt(options, purpose: str = None): | ||||||
|     if not options: |     if not options: | ||||||
|         raise EsphomeError( |         raise EsphomeError( | ||||||
|             "Found no valid options for upload/logging, please make sure relevant " |             "Found no valid options for upload/logging, please make sure relevant " | ||||||
| @@ -53,7 +56,9 @@ def choose_prompt(options): | |||||||
|     if len(options) == 1: |     if len(options) == 1: | ||||||
|         return options[0][1] |         return options[0][1] | ||||||
|  |  | ||||||
|     safe_print("Found multiple options, please choose one:") |     safe_print( | ||||||
|  |         f'Found multiple options{f" for {purpose}" if purpose else ""}, please choose one:' | ||||||
|  |     ) | ||||||
|     for i, (desc, _) in enumerate(options): |     for i, (desc, _) in enumerate(options): | ||||||
|         safe_print(f"  [{i+1}] {desc}") |         safe_print(f"  [{i+1}] {desc}") | ||||||
|  |  | ||||||
| @@ -72,7 +77,9 @@ def choose_prompt(options): | |||||||
|     return options[opt - 1][1] |     return options[opt - 1][1] | ||||||
|  |  | ||||||
|  |  | ||||||
| def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api): | def choose_upload_log_host( | ||||||
|  |     default, check_default, show_ota, show_mqtt, show_api, purpose: str = None | ||||||
|  | ): | ||||||
|     options = [] |     options = [] | ||||||
|     for port in get_serial_ports(): |     for port in get_serial_ports(): | ||||||
|         options.append((f"{port.path} ({port.description})", port.path)) |         options.append((f"{port.path} ({port.description})", port.path)) | ||||||
| @@ -80,7 +87,7 @@ def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api | |||||||
|         options.append((f"Over The Air ({CORE.address})", CORE.address)) |         options.append((f"Over The Air ({CORE.address})", CORE.address)) | ||||||
|         if default == "OTA": |         if default == "OTA": | ||||||
|             return CORE.address |             return CORE.address | ||||||
|     if show_mqtt and "mqtt" in CORE.config: |     if show_mqtt and CONF_MQTT in CORE.config: | ||||||
|         options.append((f"MQTT ({CORE.config['mqtt'][CONF_BROKER]})", "MQTT")) |         options.append((f"MQTT ({CORE.config['mqtt'][CONF_BROKER]})", "MQTT")) | ||||||
|         if default == "OTA": |         if default == "OTA": | ||||||
|             return "MQTT" |             return "MQTT" | ||||||
| @@ -88,7 +95,7 @@ def choose_upload_log_host(default, check_default, show_ota, show_mqtt, show_api | |||||||
|         return default |         return default | ||||||
|     if check_default is not None and check_default in [opt[1] for opt in options]: |     if check_default is not None and check_default in [opt[1] for opt in options]: | ||||||
|         return check_default |         return check_default | ||||||
|     return choose_prompt(options) |     return choose_prompt(options, purpose=purpose) | ||||||
|  |  | ||||||
|  |  | ||||||
| def get_port_type(port): | def get_port_type(port): | ||||||
| @@ -288,19 +295,30 @@ def upload_program(config, args, host): | |||||||
|  |  | ||||||
|         return 1  # Unknown target platform |         return 1  # Unknown target platform | ||||||
|  |  | ||||||
|     from esphome import espota2 |  | ||||||
|  |  | ||||||
|     if CONF_OTA not in config: |     if CONF_OTA not in config: | ||||||
|         raise EsphomeError( |         raise EsphomeError( | ||||||
|             "Cannot upload Over the Air as the config does not include the ota: " |             "Cannot upload Over the Air as the config does not include the ota: " | ||||||
|             "component" |             "component" | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     from esphome import espota2 | ||||||
|  |  | ||||||
|     ota_conf = config[CONF_OTA] |     ota_conf = config[CONF_OTA] | ||||||
|     remote_port = ota_conf[CONF_PORT] |     remote_port = ota_conf[CONF_PORT] | ||||||
|     password = ota_conf.get(CONF_PASSWORD, "") |     password = ota_conf.get(CONF_PASSWORD, "") | ||||||
|  |  | ||||||
|  |     if ( | ||||||
|  |         get_port_type(host) == "MQTT" or config[CONF_MDNS][CONF_DISABLED] | ||||||
|  |     ) and CONF_MQTT in config: | ||||||
|  |         from esphome import mqtt | ||||||
|  |  | ||||||
|  |         host = mqtt.get_esphome_device_ip( | ||||||
|  |             config, args.username, args.password, args.client_id | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     if getattr(args, "file", None) is not None: |     if getattr(args, "file", None) is not None: | ||||||
|         return espota2.run_ota(host, remote_port, password, args.file) |         return espota2.run_ota(host, remote_port, password, args.file) | ||||||
|  |  | ||||||
|     return espota2.run_ota(host, remote_port, password, CORE.firmware_bin) |     return espota2.run_ota(host, remote_port, password, CORE.firmware_bin) | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -310,6 +328,13 @@ def show_logs(config, args, port): | |||||||
|     if get_port_type(port) == "SERIAL": |     if get_port_type(port) == "SERIAL": | ||||||
|         return run_miniterm(config, port) |         return run_miniterm(config, port) | ||||||
|     if get_port_type(port) == "NETWORK" and "api" in config: |     if get_port_type(port) == "NETWORK" and "api" in config: | ||||||
|  |         if config[CONF_MDNS][CONF_DISABLED] and CONF_MQTT in config: | ||||||
|  |             from esphome import mqtt | ||||||
|  |  | ||||||
|  |             port = mqtt.get_esphome_device_ip( | ||||||
|  |                 config, args.username, args.password, args.client_id | ||||||
|  |             ) | ||||||
|  |  | ||||||
|         from esphome.components.api.client import run_logs |         from esphome.components.api.client import run_logs | ||||||
|  |  | ||||||
|         return run_logs(config, port) |         return run_logs(config, port) | ||||||
| @@ -374,6 +399,7 @@ def command_upload(args, config): | |||||||
|         show_ota=True, |         show_ota=True, | ||||||
|         show_mqtt=False, |         show_mqtt=False, | ||||||
|         show_api=False, |         show_api=False, | ||||||
|  |         purpose="uploading", | ||||||
|     ) |     ) | ||||||
|     exit_code = upload_program(config, args, port) |     exit_code = upload_program(config, args, port) | ||||||
|     if exit_code != 0: |     if exit_code != 0: | ||||||
| @@ -382,6 +408,15 @@ def command_upload(args, config): | |||||||
|     return 0 |     return 0 | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def command_discover(args, config): | ||||||
|  |     if "mqtt" in config: | ||||||
|  |         from esphome import mqtt | ||||||
|  |  | ||||||
|  |         return mqtt.show_discover(config, args.username, args.password, args.client_id) | ||||||
|  |  | ||||||
|  |     raise EsphomeError("No discover method configured (mqtt)") | ||||||
|  |  | ||||||
|  |  | ||||||
| def command_logs(args, config): | def command_logs(args, config): | ||||||
|     port = choose_upload_log_host( |     port = choose_upload_log_host( | ||||||
|         default=args.device, |         default=args.device, | ||||||
| @@ -389,6 +424,7 @@ def command_logs(args, config): | |||||||
|         show_ota=False, |         show_ota=False, | ||||||
|         show_mqtt=True, |         show_mqtt=True, | ||||||
|         show_api=True, |         show_api=True, | ||||||
|  |         purpose="logging", | ||||||
|     ) |     ) | ||||||
|     return show_logs(config, args, port) |     return show_logs(config, args, port) | ||||||
|  |  | ||||||
| @@ -407,6 +443,7 @@ def command_run(args, config): | |||||||
|         show_ota=True, |         show_ota=True, | ||||||
|         show_mqtt=False, |         show_mqtt=False, | ||||||
|         show_api=True, |         show_api=True, | ||||||
|  |         purpose="uploading", | ||||||
|     ) |     ) | ||||||
|     exit_code = upload_program(config, args, port) |     exit_code = upload_program(config, args, port) | ||||||
|     if exit_code != 0: |     if exit_code != 0: | ||||||
| @@ -420,6 +457,7 @@ def command_run(args, config): | |||||||
|         show_ota=False, |         show_ota=False, | ||||||
|         show_mqtt=True, |         show_mqtt=True, | ||||||
|         show_api=True, |         show_api=True, | ||||||
|  |         purpose="logging", | ||||||
|     ) |     ) | ||||||
|     return show_logs(config, args, port) |     return show_logs(config, args, port) | ||||||
|  |  | ||||||
| @@ -623,6 +661,7 @@ POST_CONFIG_ACTIONS = { | |||||||
|     "clean": command_clean, |     "clean": command_clean, | ||||||
|     "idedata": command_idedata, |     "idedata": command_idedata, | ||||||
|     "rename": command_rename, |     "rename": command_rename, | ||||||
|  |     "discover": command_discover, | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -711,6 +750,15 @@ def parse_args(argv): | |||||||
|         help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", |         help="Manually specify the serial port/address to use, for example /dev/ttyUSB0.", | ||||||
|     ) |     ) | ||||||
|  |  | ||||||
|  |     parser_discover = subparsers.add_parser( | ||||||
|  |         "discover", | ||||||
|  |         help="Validate the configuration and show all discovered devices.", | ||||||
|  |         parents=[mqtt_options], | ||||||
|  |     ) | ||||||
|  |     parser_discover.add_argument( | ||||||
|  |         "configuration", help="Your YAML configuration file.", nargs=1 | ||||||
|  |     ) | ||||||
|  |  | ||||||
|     parser_run = subparsers.add_parser( |     parser_run = subparsers.add_parser( | ||||||
|         "run", |         "run", | ||||||
|         help="Validate the configuration, create a binary, upload it, and start logs.", |         help="Validate the configuration, create a binary, upload it, and start logs.", | ||||||
|   | |||||||
							
								
								
									
										83
									
								
								esphome/components/airthings_wave_base/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										83
									
								
								esphome/components/airthings_wave_base/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,83 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome.components import sensor, ble_client | ||||||
|  |  | ||||||
|  | from esphome.const import ( | ||||||
|  |     DEVICE_CLASS_HUMIDITY, | ||||||
|  |     DEVICE_CLASS_TEMPERATURE, | ||||||
|  |     DEVICE_CLASS_PRESSURE, | ||||||
|  |     STATE_CLASS_MEASUREMENT, | ||||||
|  |     UNIT_PERCENT, | ||||||
|  |     UNIT_CELSIUS, | ||||||
|  |     UNIT_HECTOPASCAL, | ||||||
|  |     CONF_HUMIDITY, | ||||||
|  |     CONF_TVOC, | ||||||
|  |     CONF_PRESSURE, | ||||||
|  |     CONF_TEMPERATURE, | ||||||
|  |     DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, | ||||||
|  |     UNIT_PARTS_PER_BILLION, | ||||||
|  |     ICON_RADIATOR, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@ncareau", "@jeromelaban"] | ||||||
|  |  | ||||||
|  | DEPENDENCIES = ["ble_client"] | ||||||
|  |  | ||||||
|  | airthings_wave_base_ns = cg.esphome_ns.namespace("airthings_wave_base") | ||||||
|  | AirthingsWaveBase = airthings_wave_base_ns.class_( | ||||||
|  |     "AirthingsWaveBase", cg.PollingComponent, ble_client.BLEClientNode | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | BASE_SCHEMA = ( | ||||||
|  |     sensor.SENSOR_SCHEMA.extend( | ||||||
|  |         { | ||||||
|  |             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_PERCENT, | ||||||
|  |                 device_class=DEVICE_CLASS_HUMIDITY, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |                 accuracy_decimals=0, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_CELSIUS, | ||||||
|  |                 accuracy_decimals=2, | ||||||
|  |                 device_class=DEVICE_CLASS_TEMPERATURE, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |             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, | ||||||
|  |             ), | ||||||
|  |             cv.Optional(CONF_TVOC): sensor.sensor_schema( | ||||||
|  |                 unit_of_measurement=UNIT_PARTS_PER_BILLION, | ||||||
|  |                 icon=ICON_RADIATOR, | ||||||
|  |                 accuracy_decimals=0, | ||||||
|  |                 device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, | ||||||
|  |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|  |             ), | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  |     .extend(cv.polling_component_schema("5min")) | ||||||
|  |     .extend(ble_client.BLE_CLIENT_SCHEMA) | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def wave_base_to_code(var, config): | ||||||
|  |     await cg.register_component(var, config) | ||||||
|  |  | ||||||
|  |     await ble_client.register_ble_node(var, config) | ||||||
|  |  | ||||||
|  |     if CONF_HUMIDITY in config: | ||||||
|  |         sens = await sensor.new_sensor(config[CONF_HUMIDITY]) | ||||||
|  |         cg.add(var.set_humidity(sens)) | ||||||
|  |     if CONF_TEMPERATURE in config: | ||||||
|  |         sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) | ||||||
|  |         cg.add(var.set_temperature(sens)) | ||||||
|  |     if CONF_PRESSURE in config: | ||||||
|  |         sens = await sensor.new_sensor(config[CONF_PRESSURE]) | ||||||
|  |         cg.add(var.set_pressure(sens)) | ||||||
|  |     if CONF_TVOC in config: | ||||||
|  |         sens = await sensor.new_sensor(config[CONF_TVOC]) | ||||||
|  |         cg.add(var.set_tvoc(sens)) | ||||||
| @@ -0,0 +1,83 @@ | |||||||
|  | #include "airthings_wave_base.h" | ||||||
|  |  | ||||||
|  | #ifdef USE_ESP32 | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace airthings_wave_base { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "airthings_wave_base"; | ||||||
|  |  | ||||||
|  | void AirthingsWaveBase::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, "Connected successfully!"); | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     case ESP_GATTC_DISCONNECT_EVT: { | ||||||
|  |       ESP_LOGW(TAG, "Disconnected!"); | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     case ESP_GATTC_SEARCH_CMPL_EVT: { | ||||||
|  |       this->handle_ = 0; | ||||||
|  |       auto *chr = this->parent()->get_characteristic(this->service_uuid_, this->sensors_data_characteristic_uuid_); | ||||||
|  |       if (chr == nullptr) { | ||||||
|  |         ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", this->service_uuid_.to_string().c_str(), | ||||||
|  |                  this->sensors_data_characteristic_uuid_.to_string().c_str()); | ||||||
|  |         break; | ||||||
|  |       } | ||||||
|  |       this->handle_ = chr->handle; | ||||||
|  |       this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED; | ||||||
|  |  | ||||||
|  |       this->request_read_values_(); | ||||||
|  |       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_) { | ||||||
|  |         this->read_sensors(param->read.value, param->read.value_len); | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     default: | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool AirthingsWaveBase::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; } | ||||||
|  |  | ||||||
|  | void AirthingsWaveBase::update() { | ||||||
|  |   if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) { | ||||||
|  |     if (!this->parent()->enabled) { | ||||||
|  |       ESP_LOGW(TAG, "Reconnecting to device"); | ||||||
|  |       this->parent()->set_enabled(true); | ||||||
|  |       this->parent()->connect(); | ||||||
|  |     } else { | ||||||
|  |       ESP_LOGW(TAG, "Connection in progress"); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AirthingsWaveBase::request_read_values_() { | ||||||
|  |   auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_, | ||||||
|  |                                         ESP_GATT_AUTH_REQ_NONE); | ||||||
|  |   if (status) { | ||||||
|  |     ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace airthings_wave_base | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif  // USE_ESP32 | ||||||
							
								
								
									
										50
									
								
								esphome/components/airthings_wave_base/airthings_wave_base.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										50
									
								
								esphome/components/airthings_wave_base/airthings_wave_base.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,50 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #ifdef USE_ESP32 | ||||||
|  |  | ||||||
|  | #include <esp_gattc_api.h> | ||||||
|  | #include <algorithm> | ||||||
|  | #include <iterator> | ||||||
|  | #include "esphome/components/ble_client/ble_client.h" | ||||||
|  | #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" | ||||||
|  | #include "esphome/components/sensor/sensor.h" | ||||||
|  | #include "esphome/core/component.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace airthings_wave_base { | ||||||
|  |  | ||||||
|  | class AirthingsWaveBase : public PollingComponent, public ble_client::BLEClientNode { | ||||||
|  |  public: | ||||||
|  |   AirthingsWaveBase() = default; | ||||||
|  |  | ||||||
|  |   void update() 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; | ||||||
|  |  | ||||||
|  |   void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } | ||||||
|  |   void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } | ||||||
|  |   void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; } | ||||||
|  |   void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   bool is_valid_voc_value_(uint16_t voc); | ||||||
|  |  | ||||||
|  |   virtual void read_sensors(uint8_t *value, uint16_t value_len) = 0; | ||||||
|  |   void request_read_values_(); | ||||||
|  |  | ||||||
|  |   sensor::Sensor *temperature_sensor_{nullptr}; | ||||||
|  |   sensor::Sensor *humidity_sensor_{nullptr}; | ||||||
|  |   sensor::Sensor *pressure_sensor_{nullptr}; | ||||||
|  |   sensor::Sensor *tvoc_sensor_{nullptr}; | ||||||
|  |  | ||||||
|  |   uint16_t handle_; | ||||||
|  |   esp32_ble_tracker::ESPBTUUID service_uuid_; | ||||||
|  |   esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace airthings_wave_base | ||||||
|  | }  // namespace esphome | ||||||
|  |  | ||||||
|  | #endif  // USE_ESP32 | ||||||
| @@ -7,105 +7,47 @@ namespace airthings_wave_mini { | |||||||
|  |  | ||||||
| static const char *const TAG = "airthings_wave_mini"; | static const char *const TAG = "airthings_wave_mini"; | ||||||
|  |  | ||||||
| void AirthingsWaveMini::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | void AirthingsWaveMini::read_sensors(uint8_t *raw_value, uint16_t value_len) { | ||||||
|                                             esp_ble_gattc_cb_param_t *param) { |  | ||||||
|   switch (event) { |  | ||||||
|     case ESP_GATTC_OPEN_EVT: { |  | ||||||
|       if (param->open.status == ESP_GATT_OK) { |  | ||||||
|         ESP_LOGI(TAG, "Connected successfully!"); |  | ||||||
|       } |  | ||||||
|       break; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     case ESP_GATTC_DISCONNECT_EVT: { |  | ||||||
|       ESP_LOGW(TAG, "Disconnected!"); |  | ||||||
|       break; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     case ESP_GATTC_SEARCH_CMPL_EVT: { |  | ||||||
|       this->handle_ = 0; |  | ||||||
|       auto *chr = this->parent()->get_characteristic(service_uuid_, sensors_data_characteristic_uuid_); |  | ||||||
|       if (chr == nullptr) { |  | ||||||
|         ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid_.to_string().c_str(), |  | ||||||
|                  sensors_data_characteristic_uuid_.to_string().c_str()); |  | ||||||
|         break; |  | ||||||
|       } |  | ||||||
|       this->handle_ = chr->handle; |  | ||||||
|       this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED; |  | ||||||
|  |  | ||||||
|       request_read_values_(); |  | ||||||
|       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_) { |  | ||||||
|         read_sensors_(param->read.value, param->read.value_len); |  | ||||||
|       } |  | ||||||
|       break; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     default: |  | ||||||
|       break; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void AirthingsWaveMini::read_sensors_(uint8_t *raw_value, uint16_t value_len) { |  | ||||||
|   auto *value = (WaveMiniReadings *) raw_value; |   auto *value = (WaveMiniReadings *) raw_value; | ||||||
|  |  | ||||||
|   if (sizeof(WaveMiniReadings) <= value_len) { |   if (sizeof(WaveMiniReadings) <= value_len) { | ||||||
|  |     if (this->humidity_sensor_ != nullptr) { | ||||||
|       this->humidity_sensor_->publish_state(value->humidity / 100.0f); |       this->humidity_sensor_->publish_state(value->humidity / 100.0f); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (this->pressure_sensor_ != nullptr) { | ||||||
|       this->pressure_sensor_->publish_state(value->pressure / 50.0f); |       this->pressure_sensor_->publish_state(value->pressure / 50.0f); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     if (this->temperature_sensor_ != nullptr) { | ||||||
|       this->temperature_sensor_->publish_state(value->temperature / 100.0f - 273.15f); |       this->temperature_sensor_->publish_state(value->temperature / 100.0f - 273.15f); | ||||||
|     if (is_valid_voc_value_(value->voc)) { |     } | ||||||
|  |  | ||||||
|  |     if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) { | ||||||
|       this->tvoc_sensor_->publish_state(value->voc); |       this->tvoc_sensor_->publish_state(value->voc); | ||||||
|     } |     } | ||||||
|  |  | ||||||
|     // This instance must not stay connected |     // This instance must not stay connected | ||||||
|     // so other clients can connect to it (e.g. the |     // so other clients can connect to it (e.g. the | ||||||
|     // mobile app). |     // mobile app). | ||||||
|     parent()->set_enabled(false); |     this->parent()->set_enabled(false); | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| bool AirthingsWaveMini::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; } |  | ||||||
|  |  | ||||||
| void AirthingsWaveMini::update() { |  | ||||||
|   if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) { |  | ||||||
|     if (!parent()->enabled) { |  | ||||||
|       ESP_LOGW(TAG, "Reconnecting to device"); |  | ||||||
|       parent()->set_enabled(true); |  | ||||||
|       parent()->connect(); |  | ||||||
|     } else { |  | ||||||
|       ESP_LOGW(TAG, "Connection in progress"); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void AirthingsWaveMini::request_read_values_() { |  | ||||||
|   auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_, |  | ||||||
|                                         ESP_GATT_AUTH_REQ_NONE); |  | ||||||
|   if (status) { |  | ||||||
|     ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| void AirthingsWaveMini::dump_config() { | void AirthingsWaveMini::dump_config() { | ||||||
|  |   // these really don't belong here, but there doesn't seem to be a | ||||||
|  |   // practical way to have the base class use LOG_SENSOR and include | ||||||
|  |   // the TAG from this component | ||||||
|   LOG_SENSOR("  ", "Humidity", this->humidity_sensor_); |   LOG_SENSOR("  ", "Humidity", this->humidity_sensor_); | ||||||
|   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); |   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); | ||||||
|   LOG_SENSOR("  ", "Pressure", this->pressure_sensor_); |   LOG_SENSOR("  ", "Pressure", this->pressure_sensor_); | ||||||
|   LOG_SENSOR("  ", "TVOC", this->tvoc_sensor_); |   LOG_SENSOR("  ", "TVOC", this->tvoc_sensor_); | ||||||
| } | } | ||||||
|  |  | ||||||
| AirthingsWaveMini::AirthingsWaveMini() | AirthingsWaveMini::AirthingsWaveMini() { | ||||||
|     : PollingComponent(10000), |   this->service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID); | ||||||
|       service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)), |   this->sensors_data_characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID); | ||||||
|       sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {} | } | ||||||
|  |  | ||||||
| }  // namespace airthings_wave_mini | }  // namespace airthings_wave_mini | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -2,14 +2,7 @@ | |||||||
|  |  | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
|  |  | ||||||
| #include <esp_gattc_api.h> | #include "esphome/components/airthings_wave_base/airthings_wave_base.h" | ||||||
| #include <algorithm> |  | ||||||
| #include <iterator> |  | ||||||
| #include "esphome/components/ble_client/ble_client.h" |  | ||||||
| #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" |  | ||||||
| #include "esphome/components/sensor/sensor.h" |  | ||||||
| #include "esphome/core/component.h" |  | ||||||
| #include "esphome/core/log.h" |  | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace airthings_wave_mini { | namespace airthings_wave_mini { | ||||||
| @@ -17,35 +10,14 @@ namespace airthings_wave_mini { | |||||||
| static const char *const SERVICE_UUID = "b42e3882-ade7-11e4-89d3-123b93f75cba"; | static const char *const SERVICE_UUID = "b42e3882-ade7-11e4-89d3-123b93f75cba"; | ||||||
| static const char *const CHARACTERISTIC_UUID = "b42e3b98-ade7-11e4-89d3-123b93f75cba"; | static const char *const CHARACTERISTIC_UUID = "b42e3b98-ade7-11e4-89d3-123b93f75cba"; | ||||||
|  |  | ||||||
| class AirthingsWaveMini : public PollingComponent, public ble_client::BLEClientNode { | class AirthingsWaveMini : public airthings_wave_base::AirthingsWaveBase { | ||||||
|  public: |  public: | ||||||
|   AirthingsWaveMini(); |   AirthingsWaveMini(); | ||||||
|  |  | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   void update() 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; |  | ||||||
|  |  | ||||||
|   void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } |  | ||||||
|   void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } |  | ||||||
|   void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; } |  | ||||||
|   void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } |  | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   bool is_valid_voc_value_(uint16_t voc); |   void read_sensors(uint8_t *value, uint16_t value_len) override; | ||||||
|  |  | ||||||
|   void read_sensors_(uint8_t *value, uint16_t value_len); |  | ||||||
|   void request_read_values_(); |  | ||||||
|  |  | ||||||
|   sensor::Sensor *temperature_sensor_{nullptr}; |  | ||||||
|   sensor::Sensor *humidity_sensor_{nullptr}; |  | ||||||
|   sensor::Sensor *pressure_sensor_{nullptr}; |  | ||||||
|   sensor::Sensor *tvoc_sensor_{nullptr}; |  | ||||||
|  |  | ||||||
|   uint16_t handle_; |  | ||||||
|   esp32_ble_tracker::ESPBTUUID service_uuid_; |  | ||||||
|   esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_; |  | ||||||
|  |  | ||||||
|   struct WaveMiniReadings { |   struct WaveMiniReadings { | ||||||
|     uint16_t unused01; |     uint16_t unused01; | ||||||
|   | |||||||
| @@ -1,82 +1,28 @@ | |||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.components import sensor, ble_client | from esphome.components import airthings_wave_base | ||||||
|  |  | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     DEVICE_CLASS_HUMIDITY, |  | ||||||
|     DEVICE_CLASS_TEMPERATURE, |  | ||||||
|     DEVICE_CLASS_PRESSURE, |  | ||||||
|     STATE_CLASS_MEASUREMENT, |  | ||||||
|     UNIT_PERCENT, |  | ||||||
|     UNIT_CELSIUS, |  | ||||||
|     UNIT_HECTOPASCAL, |  | ||||||
|     CONF_ID, |     CONF_ID, | ||||||
|     CONF_HUMIDITY, |  | ||||||
|     CONF_TVOC, |  | ||||||
|     CONF_PRESSURE, |  | ||||||
|     CONF_TEMPERATURE, |  | ||||||
|     UNIT_PARTS_PER_BILLION, |  | ||||||
|     ICON_RADIATOR, |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| DEPENDENCIES = ["ble_client"] | DEPENDENCIES = airthings_wave_base.DEPENDENCIES | ||||||
|  |  | ||||||
|  | AUTO_LOAD = ["airthings_wave_base"] | ||||||
|  |  | ||||||
| airthings_wave_mini_ns = cg.esphome_ns.namespace("airthings_wave_mini") | airthings_wave_mini_ns = cg.esphome_ns.namespace("airthings_wave_mini") | ||||||
| AirthingsWaveMini = airthings_wave_mini_ns.class_( | AirthingsWaveMini = airthings_wave_mini_ns.class_( | ||||||
|     "AirthingsWaveMini", cg.PollingComponent, ble_client.BLEClientNode |     "AirthingsWaveMini", airthings_wave_base.AirthingsWaveBase | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.All( | CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend( | ||||||
|     cv.Schema( |  | ||||||
|     { |     { | ||||||
|         cv.GenerateID(): cv.declare_id(AirthingsWaveMini), |         cv.GenerateID(): cv.declare_id(AirthingsWaveMini), | ||||||
|             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( |  | ||||||
|                 unit_of_measurement=UNIT_PERCENT, |  | ||||||
|                 device_class=DEVICE_CLASS_HUMIDITY, |  | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |  | ||||||
|                 accuracy_decimals=2, |  | ||||||
|             ), |  | ||||||
|             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( |  | ||||||
|                 unit_of_measurement=UNIT_CELSIUS, |  | ||||||
|                 accuracy_decimals=2, |  | ||||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, |  | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |  | ||||||
|             ), |  | ||||||
|             cv.Optional(CONF_PRESSURE): sensor.sensor_schema( |  | ||||||
|                 unit_of_measurement=UNIT_HECTOPASCAL, |  | ||||||
|                 accuracy_decimals=2, |  | ||||||
|                 device_class=DEVICE_CLASS_PRESSURE, |  | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |  | ||||||
|             ), |  | ||||||
|             cv.Optional(CONF_TVOC): sensor.sensor_schema( |  | ||||||
|                 unit_of_measurement=UNIT_PARTS_PER_BILLION, |  | ||||||
|                 icon=ICON_RADIATOR, |  | ||||||
|                 accuracy_decimals=0, |  | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |  | ||||||
|             ), |  | ||||||
|     } |     } | ||||||
| ) | ) | ||||||
|     .extend(cv.polling_component_schema("5min")) |  | ||||||
|     .extend(ble_client.BLE_CLIENT_SCHEMA), |  | ||||||
| ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|     await cg.register_component(var, config) |     await airthings_wave_base.wave_base_to_code(var, config) | ||||||
|  |  | ||||||
|     await ble_client.register_ble_node(var, config) |  | ||||||
|  |  | ||||||
|     if CONF_HUMIDITY in config: |  | ||||||
|         sens = await sensor.new_sensor(config[CONF_HUMIDITY]) |  | ||||||
|         cg.add(var.set_humidity(sens)) |  | ||||||
|     if CONF_TEMPERATURE in config: |  | ||||||
|         sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) |  | ||||||
|         cg.add(var.set_temperature(sens)) |  | ||||||
|     if CONF_PRESSURE in config: |  | ||||||
|         sens = await sensor.new_sensor(config[CONF_PRESSURE]) |  | ||||||
|         cg.add(var.set_pressure(sens)) |  | ||||||
|     if CONF_TVOC in config: |  | ||||||
|         sens = await sensor.new_sensor(config[CONF_TVOC]) |  | ||||||
|         cg.add(var.set_tvoc(sens)) |  | ||||||
|   | |||||||
| @@ -7,55 +7,7 @@ namespace airthings_wave_plus { | |||||||
|  |  | ||||||
| static const char *const TAG = "airthings_wave_plus"; | static const char *const TAG = "airthings_wave_plus"; | ||||||
|  |  | ||||||
| void AirthingsWavePlus::gattc_event_handler(esp_gattc_cb_event_t event, esp_gatt_if_t gattc_if, | void AirthingsWavePlus::read_sensors(uint8_t *raw_value, uint16_t value_len) { | ||||||
|                                             esp_ble_gattc_cb_param_t *param) { |  | ||||||
|   switch (event) { |  | ||||||
|     case ESP_GATTC_OPEN_EVT: { |  | ||||||
|       if (param->open.status == ESP_GATT_OK) { |  | ||||||
|         ESP_LOGI(TAG, "Connected successfully!"); |  | ||||||
|       } |  | ||||||
|       break; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     case ESP_GATTC_DISCONNECT_EVT: { |  | ||||||
|       ESP_LOGW(TAG, "Disconnected!"); |  | ||||||
|       break; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     case ESP_GATTC_SEARCH_CMPL_EVT: { |  | ||||||
|       this->handle_ = 0; |  | ||||||
|       auto *chr = this->parent()->get_characteristic(service_uuid_, sensors_data_characteristic_uuid_); |  | ||||||
|       if (chr == nullptr) { |  | ||||||
|         ESP_LOGW(TAG, "No sensor characteristic found at service %s char %s", service_uuid_.to_string().c_str(), |  | ||||||
|                  sensors_data_characteristic_uuid_.to_string().c_str()); |  | ||||||
|         break; |  | ||||||
|       } |  | ||||||
|       this->handle_ = chr->handle; |  | ||||||
|       this->node_state = esp32_ble_tracker::ClientState::ESTABLISHED; |  | ||||||
|  |  | ||||||
|       request_read_values_(); |  | ||||||
|       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_) { |  | ||||||
|         read_sensors_(param->read.value, param->read.value_len); |  | ||||||
|       } |  | ||||||
|       break; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     default: |  | ||||||
|       break; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) { |  | ||||||
|   auto *value = (WavePlusReadings *) raw_value; |   auto *value = (WavePlusReadings *) raw_value; | ||||||
|  |  | ||||||
|   if (sizeof(WavePlusReadings) <= value_len) { |   if (sizeof(WavePlusReadings) <= value_len) { | ||||||
| @@ -64,26 +16,38 @@ void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) { | |||||||
|     if (value->version == 1) { |     if (value->version == 1) { | ||||||
|       ESP_LOGD(TAG, "ambient light = %d", value->ambientLight); |       ESP_LOGD(TAG, "ambient light = %d", value->ambientLight); | ||||||
|  |  | ||||||
|  |       if (this->humidity_sensor_ != nullptr) { | ||||||
|         this->humidity_sensor_->publish_state(value->humidity / 2.0f); |         this->humidity_sensor_->publish_state(value->humidity / 2.0f); | ||||||
|       if (is_valid_radon_value_(value->radon)) { |       } | ||||||
|  |  | ||||||
|  |       if ((this->radon_sensor_ != nullptr) && this->is_valid_radon_value_(value->radon)) { | ||||||
|         this->radon_sensor_->publish_state(value->radon); |         this->radon_sensor_->publish_state(value->radon); | ||||||
|       } |       } | ||||||
|       if (is_valid_radon_value_(value->radon_lt)) { |  | ||||||
|  |       if ((this->radon_long_term_sensor_ != nullptr) && this->is_valid_radon_value_(value->radon_lt)) { | ||||||
|         this->radon_long_term_sensor_->publish_state(value->radon_lt); |         this->radon_long_term_sensor_->publish_state(value->radon_lt); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       if (this->temperature_sensor_ != nullptr) { | ||||||
|         this->temperature_sensor_->publish_state(value->temperature / 100.0f); |         this->temperature_sensor_->publish_state(value->temperature / 100.0f); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (this->pressure_sensor_ != nullptr) { | ||||||
|         this->pressure_sensor_->publish_state(value->pressure / 50.0f); |         this->pressure_sensor_->publish_state(value->pressure / 50.0f); | ||||||
|       if (is_valid_co2_value_(value->co2)) { |       } | ||||||
|  |  | ||||||
|  |       if ((this->co2_sensor_ != nullptr) && this->is_valid_co2_value_(value->co2)) { | ||||||
|         this->co2_sensor_->publish_state(value->co2); |         this->co2_sensor_->publish_state(value->co2); | ||||||
|       } |       } | ||||||
|       if (is_valid_voc_value_(value->voc)) { |  | ||||||
|  |       if ((this->tvoc_sensor_ != nullptr) && this->is_valid_voc_value_(value->voc)) { | ||||||
|         this->tvoc_sensor_->publish_state(value->voc); |         this->tvoc_sensor_->publish_state(value->voc); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|       // This instance must not stay connected |       // This instance must not stay connected | ||||||
|       // so other clients can connect to it (e.g. the |       // so other clients can connect to it (e.g. the | ||||||
|       // mobile app). |       // mobile app). | ||||||
|       parent()->set_enabled(false); |       this->parent()->set_enabled(false); | ||||||
|     } else { |     } else { | ||||||
|       ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version); |       ESP_LOGE(TAG, "Invalid payload version (%d != 1, newer version or not a Wave Plus?)", value->version); | ||||||
|     } |     } | ||||||
| @@ -92,44 +56,26 @@ void AirthingsWavePlus::read_sensors_(uint8_t *raw_value, uint16_t value_len) { | |||||||
|  |  | ||||||
| bool AirthingsWavePlus::is_valid_radon_value_(uint16_t radon) { return 0 <= radon && radon <= 16383; } | bool AirthingsWavePlus::is_valid_radon_value_(uint16_t radon) { return 0 <= radon && radon <= 16383; } | ||||||
|  |  | ||||||
| bool AirthingsWavePlus::is_valid_voc_value_(uint16_t voc) { return 0 <= voc && voc <= 16383; } |  | ||||||
|  |  | ||||||
| bool AirthingsWavePlus::is_valid_co2_value_(uint16_t co2) { return 0 <= co2 && co2 <= 16383; } | bool AirthingsWavePlus::is_valid_co2_value_(uint16_t co2) { return 0 <= co2 && co2 <= 16383; } | ||||||
|  |  | ||||||
| void AirthingsWavePlus::update() { |  | ||||||
|   if (this->node_state != esp32_ble_tracker::ClientState::ESTABLISHED) { |  | ||||||
|     if (!parent()->enabled) { |  | ||||||
|       ESP_LOGW(TAG, "Reconnecting to device"); |  | ||||||
|       parent()->set_enabled(true); |  | ||||||
|       parent()->connect(); |  | ||||||
|     } else { |  | ||||||
|       ESP_LOGW(TAG, "Connection in progress"); |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void AirthingsWavePlus::request_read_values_() { |  | ||||||
|   auto status = esp_ble_gattc_read_char(this->parent()->get_gattc_if(), this->parent()->get_conn_id(), this->handle_, |  | ||||||
|                                         ESP_GATT_AUTH_REQ_NONE); |  | ||||||
|   if (status) { |  | ||||||
|     ESP_LOGW(TAG, "Error sending read request for sensor, status=%d", status); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void AirthingsWavePlus::dump_config() { | void AirthingsWavePlus::dump_config() { | ||||||
|  |   // these really don't belong here, but there doesn't seem to be a | ||||||
|  |   // practical way to have the base class use LOG_SENSOR and include | ||||||
|  |   // the TAG from this component | ||||||
|   LOG_SENSOR("  ", "Humidity", this->humidity_sensor_); |   LOG_SENSOR("  ", "Humidity", this->humidity_sensor_); | ||||||
|   LOG_SENSOR("  ", "Radon", this->radon_sensor_); |  | ||||||
|   LOG_SENSOR("  ", "Radon Long Term", this->radon_long_term_sensor_); |  | ||||||
|   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); |   LOG_SENSOR("  ", "Temperature", this->temperature_sensor_); | ||||||
|   LOG_SENSOR("  ", "Pressure", this->pressure_sensor_); |   LOG_SENSOR("  ", "Pressure", this->pressure_sensor_); | ||||||
|   LOG_SENSOR("  ", "CO2", this->co2_sensor_); |  | ||||||
|   LOG_SENSOR("  ", "TVOC", this->tvoc_sensor_); |   LOG_SENSOR("  ", "TVOC", this->tvoc_sensor_); | ||||||
|  |  | ||||||
|  |   LOG_SENSOR("  ", "Radon", this->radon_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "Radon Long Term", this->radon_long_term_sensor_); | ||||||
|  |   LOG_SENSOR("  ", "CO2", this->co2_sensor_); | ||||||
| } | } | ||||||
|  |  | ||||||
| AirthingsWavePlus::AirthingsWavePlus() | AirthingsWavePlus::AirthingsWavePlus() { | ||||||
|     : PollingComponent(10000), |   this->service_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID); | ||||||
|       service_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(SERVICE_UUID)), |   this->sensors_data_characteristic_uuid_ = esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID); | ||||||
|       sensors_data_characteristic_uuid_(esp32_ble_tracker::ESPBTUUID::from_raw(CHARACTERISTIC_UUID)) {} | } | ||||||
|  |  | ||||||
| }  // namespace airthings_wave_plus | }  // namespace airthings_wave_plus | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -2,14 +2,7 @@ | |||||||
|  |  | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
|  |  | ||||||
| #include <esp_gattc_api.h> | #include "esphome/components/airthings_wave_base/airthings_wave_base.h" | ||||||
| #include <algorithm> |  | ||||||
| #include <iterator> |  | ||||||
| #include "esphome/components/ble_client/ble_client.h" |  | ||||||
| #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" |  | ||||||
| #include "esphome/components/sensor/sensor.h" |  | ||||||
| #include "esphome/core/component.h" |  | ||||||
| #include "esphome/core/log.h" |  | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace airthings_wave_plus { | namespace airthings_wave_plus { | ||||||
| @@ -17,43 +10,25 @@ namespace airthings_wave_plus { | |||||||
| static const char *const SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba"; | static const char *const SERVICE_UUID = "b42e1c08-ade7-11e4-89d3-123b93f75cba"; | ||||||
| static const char *const CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba"; | static const char *const CHARACTERISTIC_UUID = "b42e2a68-ade7-11e4-89d3-123b93f75cba"; | ||||||
|  |  | ||||||
| class AirthingsWavePlus : public PollingComponent, public ble_client::BLEClientNode { | class AirthingsWavePlus : public airthings_wave_base::AirthingsWaveBase { | ||||||
|  public: |  public: | ||||||
|   AirthingsWavePlus(); |   AirthingsWavePlus(); | ||||||
|  |  | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   void update() 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; |  | ||||||
|  |  | ||||||
|   void set_temperature(sensor::Sensor *temperature) { temperature_sensor_ = temperature; } |  | ||||||
|   void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; } |   void set_radon(sensor::Sensor *radon) { radon_sensor_ = radon; } | ||||||
|   void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; } |   void set_radon_long_term(sensor::Sensor *radon_long_term) { radon_long_term_sensor_ = radon_long_term; } | ||||||
|   void set_humidity(sensor::Sensor *humidity) { humidity_sensor_ = humidity; } |  | ||||||
|   void set_pressure(sensor::Sensor *pressure) { pressure_sensor_ = pressure; } |  | ||||||
|   void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; } |   void set_co2(sensor::Sensor *co2) { co2_sensor_ = co2; } | ||||||
|   void set_tvoc(sensor::Sensor *tvoc) { tvoc_sensor_ = tvoc; } |  | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   bool is_valid_radon_value_(uint16_t radon); |   bool is_valid_radon_value_(uint16_t radon); | ||||||
|   bool is_valid_voc_value_(uint16_t voc); |  | ||||||
|   bool is_valid_co2_value_(uint16_t co2); |   bool is_valid_co2_value_(uint16_t co2); | ||||||
|  |  | ||||||
|   void read_sensors_(uint8_t *value, uint16_t value_len); |   void read_sensors(uint8_t *value, uint16_t value_len) override; | ||||||
|   void request_read_values_(); |  | ||||||
|  |  | ||||||
|   sensor::Sensor *temperature_sensor_{nullptr}; |  | ||||||
|   sensor::Sensor *radon_sensor_{nullptr}; |   sensor::Sensor *radon_sensor_{nullptr}; | ||||||
|   sensor::Sensor *radon_long_term_sensor_{nullptr}; |   sensor::Sensor *radon_long_term_sensor_{nullptr}; | ||||||
|   sensor::Sensor *humidity_sensor_{nullptr}; |  | ||||||
|   sensor::Sensor *pressure_sensor_{nullptr}; |  | ||||||
|   sensor::Sensor *co2_sensor_{nullptr}; |   sensor::Sensor *co2_sensor_{nullptr}; | ||||||
|   sensor::Sensor *tvoc_sensor_{nullptr}; |  | ||||||
|  |  | ||||||
|   uint16_t handle_; |  | ||||||
|   esp32_ble_tracker::ESPBTUUID service_uuid_; |  | ||||||
|   esp32_ble_tracker::ESPBTUUID sensors_data_characteristic_uuid_; |  | ||||||
|  |  | ||||||
|   struct WavePlusReadings { |   struct WavePlusReadings { | ||||||
|     uint8_t version; |     uint8_t version; | ||||||
|   | |||||||
| @@ -1,49 +1,32 @@ | |||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| from esphome.components import sensor, ble_client | from esphome.components import sensor, airthings_wave_base | ||||||
|  |  | ||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     DEVICE_CLASS_CARBON_DIOXIDE, |     DEVICE_CLASS_CARBON_DIOXIDE, | ||||||
|     DEVICE_CLASS_HUMIDITY, |  | ||||||
|     DEVICE_CLASS_TEMPERATURE, |  | ||||||
|     DEVICE_CLASS_PRESSURE, |  | ||||||
|     STATE_CLASS_MEASUREMENT, |     STATE_CLASS_MEASUREMENT, | ||||||
|     UNIT_PERCENT, |  | ||||||
|     UNIT_CELSIUS, |  | ||||||
|     UNIT_HECTOPASCAL, |  | ||||||
|     ICON_RADIOACTIVE, |     ICON_RADIOACTIVE, | ||||||
|     CONF_ID, |     CONF_ID, | ||||||
|     CONF_RADON, |     CONF_RADON, | ||||||
|     CONF_RADON_LONG_TERM, |     CONF_RADON_LONG_TERM, | ||||||
|     CONF_HUMIDITY, |  | ||||||
|     CONF_TVOC, |  | ||||||
|     CONF_CO2, |     CONF_CO2, | ||||||
|     CONF_PRESSURE, |  | ||||||
|     CONF_TEMPERATURE, |  | ||||||
|     UNIT_BECQUEREL_PER_CUBIC_METER, |     UNIT_BECQUEREL_PER_CUBIC_METER, | ||||||
|     UNIT_PARTS_PER_MILLION, |     UNIT_PARTS_PER_MILLION, | ||||||
|     UNIT_PARTS_PER_BILLION, |  | ||||||
|     ICON_RADIATOR, |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
| DEPENDENCIES = ["ble_client"] | DEPENDENCIES = airthings_wave_base.DEPENDENCIES | ||||||
|  |  | ||||||
|  | AUTO_LOAD = ["airthings_wave_base"] | ||||||
|  |  | ||||||
| airthings_wave_plus_ns = cg.esphome_ns.namespace("airthings_wave_plus") | airthings_wave_plus_ns = cg.esphome_ns.namespace("airthings_wave_plus") | ||||||
| AirthingsWavePlus = airthings_wave_plus_ns.class_( | AirthingsWavePlus = airthings_wave_plus_ns.class_( | ||||||
|     "AirthingsWavePlus", cg.PollingComponent, ble_client.BLEClientNode |     "AirthingsWavePlus", airthings_wave_base.AirthingsWaveBase | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.All( | CONFIG_SCHEMA = airthings_wave_base.BASE_SCHEMA.extend( | ||||||
|     cv.Schema( |  | ||||||
|     { |     { | ||||||
|         cv.GenerateID(): cv.declare_id(AirthingsWavePlus), |         cv.GenerateID(): cv.declare_id(AirthingsWavePlus), | ||||||
|             cv.Optional(CONF_HUMIDITY): sensor.sensor_schema( |  | ||||||
|                 unit_of_measurement=UNIT_PERCENT, |  | ||||||
|                 device_class=DEVICE_CLASS_HUMIDITY, |  | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |  | ||||||
|                 accuracy_decimals=0, |  | ||||||
|             ), |  | ||||||
|         cv.Optional(CONF_RADON): sensor.sensor_schema( |         cv.Optional(CONF_RADON): sensor.sensor_schema( | ||||||
|             unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, |             unit_of_measurement=UNIT_BECQUEREL_PER_CUBIC_METER, | ||||||
|             icon=ICON_RADIOACTIVE, |             icon=ICON_RADIOACTIVE, | ||||||
| @@ -56,61 +39,26 @@ CONFIG_SCHEMA = cv.All( | |||||||
|             accuracy_decimals=0, |             accuracy_decimals=0, | ||||||
|             state_class=STATE_CLASS_MEASUREMENT, |             state_class=STATE_CLASS_MEASUREMENT, | ||||||
|         ), |         ), | ||||||
|             cv.Optional(CONF_TEMPERATURE): sensor.sensor_schema( |  | ||||||
|                 unit_of_measurement=UNIT_CELSIUS, |  | ||||||
|                 accuracy_decimals=2, |  | ||||||
|                 device_class=DEVICE_CLASS_TEMPERATURE, |  | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |  | ||||||
|             ), |  | ||||||
|             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, |  | ||||||
|             ), |  | ||||||
|         cv.Optional(CONF_CO2): sensor.sensor_schema( |         cv.Optional(CONF_CO2): sensor.sensor_schema( | ||||||
|             unit_of_measurement=UNIT_PARTS_PER_MILLION, |             unit_of_measurement=UNIT_PARTS_PER_MILLION, | ||||||
|             accuracy_decimals=0, |             accuracy_decimals=0, | ||||||
|             device_class=DEVICE_CLASS_CARBON_DIOXIDE, |             device_class=DEVICE_CLASS_CARBON_DIOXIDE, | ||||||
|             state_class=STATE_CLASS_MEASUREMENT, |             state_class=STATE_CLASS_MEASUREMENT, | ||||||
|         ), |         ), | ||||||
|             cv.Optional(CONF_TVOC): sensor.sensor_schema( |  | ||||||
|                 unit_of_measurement=UNIT_PARTS_PER_BILLION, |  | ||||||
|                 icon=ICON_RADIATOR, |  | ||||||
|                 accuracy_decimals=0, |  | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |  | ||||||
|             ), |  | ||||||
|     } |     } | ||||||
| ) | ) | ||||||
|     .extend(cv.polling_component_schema("5min")) |  | ||||||
|     .extend(ble_client.BLE_CLIENT_SCHEMA), |  | ||||||
| ) |  | ||||||
|  |  | ||||||
|  |  | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     var = cg.new_Pvariable(config[CONF_ID]) |     var = cg.new_Pvariable(config[CONF_ID]) | ||||||
|     await cg.register_component(var, config) |     await airthings_wave_base.wave_base_to_code(var, config) | ||||||
|  |  | ||||||
|     await ble_client.register_ble_node(var, config) |  | ||||||
|  |  | ||||||
|     if CONF_HUMIDITY in config: |  | ||||||
|         sens = await sensor.new_sensor(config[CONF_HUMIDITY]) |  | ||||||
|         cg.add(var.set_humidity(sens)) |  | ||||||
|     if CONF_RADON in config: |     if CONF_RADON in config: | ||||||
|         sens = await sensor.new_sensor(config[CONF_RADON]) |         sens = await sensor.new_sensor(config[CONF_RADON]) | ||||||
|         cg.add(var.set_radon(sens)) |         cg.add(var.set_radon(sens)) | ||||||
|     if CONF_RADON_LONG_TERM in config: |     if CONF_RADON_LONG_TERM in config: | ||||||
|         sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM]) |         sens = await sensor.new_sensor(config[CONF_RADON_LONG_TERM]) | ||||||
|         cg.add(var.set_radon_long_term(sens)) |         cg.add(var.set_radon_long_term(sens)) | ||||||
|     if CONF_TEMPERATURE in config: |  | ||||||
|         sens = await sensor.new_sensor(config[CONF_TEMPERATURE]) |  | ||||||
|         cg.add(var.set_temperature(sens)) |  | ||||||
|     if CONF_PRESSURE in config: |  | ||||||
|         sens = await sensor.new_sensor(config[CONF_PRESSURE]) |  | ||||||
|         cg.add(var.set_pressure(sens)) |  | ||||||
|     if CONF_CO2 in config: |     if CONF_CO2 in config: | ||||||
|         sens = await sensor.new_sensor(config[CONF_CO2]) |         sens = await sensor.new_sensor(config[CONF_CO2]) | ||||||
|         cg.add(var.set_co2(sens)) |         cg.add(var.set_co2(sens)) | ||||||
|     if CONF_TVOC in config: |  | ||||||
|         sens = await sensor.new_sensor(config[CONF_TVOC]) |  | ||||||
|         cg.add(var.set_tvoc(sens)) |  | ||||||
|   | |||||||
							
								
								
									
										165
									
								
								esphome/components/alarm_control_panel/__init__.py
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								esphome/components/alarm_control_panel/__init__.py
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,165 @@ | |||||||
|  | import esphome.codegen as cg | ||||||
|  | import esphome.config_validation as cv | ||||||
|  | from esphome import automation | ||||||
|  | from esphome.automation import maybe_simple_id | ||||||
|  | from esphome.core import CORE, coroutine_with_priority | ||||||
|  | from esphome.const import ( | ||||||
|  |     CONF_ID, | ||||||
|  |     CONF_ON_STATE, | ||||||
|  |     CONF_TRIGGER_ID, | ||||||
|  |     CONF_CODE, | ||||||
|  | ) | ||||||
|  | from esphome.cpp_helpers import setup_entity | ||||||
|  |  | ||||||
|  | CODEOWNERS = ["@grahambrown11"] | ||||||
|  | IS_PLATFORM_COMPONENT = True | ||||||
|  |  | ||||||
|  | CONF_ON_TRIGGERED = "on_triggered" | ||||||
|  | CONF_ON_CLEARED = "on_cleared" | ||||||
|  |  | ||||||
|  | alarm_control_panel_ns = cg.esphome_ns.namespace("alarm_control_panel") | ||||||
|  | AlarmControlPanel = alarm_control_panel_ns.class_("AlarmControlPanel", cg.EntityBase) | ||||||
|  |  | ||||||
|  | StateTrigger = alarm_control_panel_ns.class_( | ||||||
|  |     "StateTrigger", automation.Trigger.template() | ||||||
|  | ) | ||||||
|  | TriggeredTrigger = alarm_control_panel_ns.class_( | ||||||
|  |     "TriggeredTrigger", automation.Trigger.template() | ||||||
|  | ) | ||||||
|  | ClearedTrigger = alarm_control_panel_ns.class_( | ||||||
|  |     "ClearedTrigger", automation.Trigger.template() | ||||||
|  | ) | ||||||
|  | ArmAwayAction = alarm_control_panel_ns.class_("ArmAwayAction", automation.Action) | ||||||
|  | ArmHomeAction = alarm_control_panel_ns.class_("ArmHomeAction", 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) | ||||||
|  | AlarmControlPanelCondition = alarm_control_panel_ns.class_( | ||||||
|  |     "AlarmControlPanelCondition", automation.Condition | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | ALARM_CONTROL_PANEL_SCHEMA = cv.ENTITY_BASE_SCHEMA.extend( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.declare_id(AlarmControlPanel), | ||||||
|  |         cv.Optional(CONF_ON_STATE): automation.validate_automation( | ||||||
|  |             { | ||||||
|  |                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(StateTrigger), | ||||||
|  |             } | ||||||
|  |         ), | ||||||
|  |         cv.Optional(CONF_ON_TRIGGERED): automation.validate_automation( | ||||||
|  |             { | ||||||
|  |                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(TriggeredTrigger), | ||||||
|  |             } | ||||||
|  |         ), | ||||||
|  |         cv.Optional(CONF_ON_CLEARED): automation.validate_automation( | ||||||
|  |             { | ||||||
|  |                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ClearedTrigger), | ||||||
|  |             } | ||||||
|  |         ), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | ALARM_CONTROL_PANEL_ACTION_SCHEMA = maybe_simple_id( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.use_id(AlarmControlPanel), | ||||||
|  |         cv.Optional(CONF_CODE): cv.templatable(cv.string), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | ALARM_CONTROL_PANEL_CONDITION_SCHEMA = maybe_simple_id( | ||||||
|  |     { | ||||||
|  |         cv.GenerateID(): cv.use_id(AlarmControlPanel), | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def setup_alarm_control_panel_core_(var, config): | ||||||
|  |     await setup_entity(var, config) | ||||||
|  |     for conf in config.get(CONF_ON_STATE, []): | ||||||
|  |         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||||
|  |         await automation.build_automation(trigger, [], conf) | ||||||
|  |     for conf in config.get(CONF_ON_TRIGGERED, []): | ||||||
|  |         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||||
|  |         await automation.build_automation(trigger, [], conf) | ||||||
|  |     for conf in config.get(CONF_ON_CLEARED, []): | ||||||
|  |         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||||
|  |         await automation.build_automation(trigger, [], conf) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | async def register_alarm_control_panel(var, config): | ||||||
|  |     if not CORE.has_id(config[CONF_ID]): | ||||||
|  |         var = cg.Pvariable(config[CONF_ID], var) | ||||||
|  |     cg.add(cg.App.register_alarm_control_panel(var)) | ||||||
|  |     await setup_alarm_control_panel_core_(var, config) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @automation.register_action( | ||||||
|  |     "alarm_control_panel.arm_away", ArmAwayAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA | ||||||
|  | ) | ||||||
|  | async def alarm_action_arm_away_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) | ||||||
|  |     if CONF_CODE in config: | ||||||
|  |         templatable_ = await cg.templatable(config[CONF_CODE], args, cg.std_string) | ||||||
|  |         cg.add(var.set_code(templatable_)) | ||||||
|  |     return var | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @automation.register_action( | ||||||
|  |     "alarm_control_panel.arm_home", ArmHomeAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA | ||||||
|  | ) | ||||||
|  | async def alarm_action_arm_home_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) | ||||||
|  |     if CONF_CODE in config: | ||||||
|  |         templatable_ = await cg.templatable(config[CONF_CODE], args, cg.std_string) | ||||||
|  |         cg.add(var.set_code(templatable_)) | ||||||
|  |     return var | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @automation.register_action( | ||||||
|  |     "alarm_control_panel.disarm", DisarmAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA | ||||||
|  | ) | ||||||
|  | async def alarm_action_disarm_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) | ||||||
|  |     if CONF_CODE in config: | ||||||
|  |         templatable_ = await cg.templatable(config[CONF_CODE], args, cg.std_string) | ||||||
|  |         cg.add(var.set_code(templatable_)) | ||||||
|  |     return var | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @automation.register_action( | ||||||
|  |     "alarm_control_panel.pending", PendingAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA | ||||||
|  | ) | ||||||
|  | async def alarm_action_pending_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.triggered", TriggeredAction, ALARM_CONTROL_PANEL_ACTION_SCHEMA | ||||||
|  | ) | ||||||
|  | async def alarm_action_trigger_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, | ||||||
|  |     ALARM_CONTROL_PANEL_CONDITION_SCHEMA, | ||||||
|  | ) | ||||||
|  | async def alarm_control_panel_is_armed_to_code( | ||||||
|  |     config, condition_id, template_arg, args | ||||||
|  | ): | ||||||
|  |     paren = await cg.get_variable(config[CONF_ID]) | ||||||
|  |     return cg.new_Pvariable(condition_id, template_arg, paren) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | @coroutine_with_priority(100.0) | ||||||
|  | async def to_code(config): | ||||||
|  |     cg.add_global(alarm_control_panel_ns.using) | ||||||
|  |     cg.add_define("USE_ALARM_CONTROL_PANEL") | ||||||
							
								
								
									
										111
									
								
								esphome/components/alarm_control_panel/alarm_control_panel.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										111
									
								
								esphome/components/alarm_control_panel/alarm_control_panel.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,111 @@ | |||||||
|  | #include <utility> | ||||||
|  |  | ||||||
|  | #include "alarm_control_panel.h" | ||||||
|  |  | ||||||
|  | #include "esphome/core/application.h" | ||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace alarm_control_panel { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "alarm_control_panel"; | ||||||
|  |  | ||||||
|  | AlarmControlPanelCall AlarmControlPanel::make_call() { return AlarmControlPanelCall(this); } | ||||||
|  |  | ||||||
|  | bool AlarmControlPanel::is_state_armed(AlarmControlPanelState state) { | ||||||
|  |   switch (state) { | ||||||
|  |     case ACP_STATE_ARMED_AWAY: | ||||||
|  |     case ACP_STATE_ARMED_HOME: | ||||||
|  |     case ACP_STATE_ARMED_NIGHT: | ||||||
|  |     case ACP_STATE_ARMED_VACATION: | ||||||
|  |     case ACP_STATE_ARMED_CUSTOM_BYPASS: | ||||||
|  |       return true; | ||||||
|  |     default: | ||||||
|  |       return false; | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | void AlarmControlPanel::publish_state(AlarmControlPanelState state) { | ||||||
|  |   this->last_update_ = millis(); | ||||||
|  |   if (state != this->current_state_) { | ||||||
|  |     auto prev_state = this->current_state_; | ||||||
|  |     ESP_LOGD(TAG, "Set state to: %s, previous: %s", LOG_STR_ARG(alarm_control_panel_state_to_string(state)), | ||||||
|  |              LOG_STR_ARG(alarm_control_panel_state_to_string(prev_state))); | ||||||
|  |     this->current_state_ = state; | ||||||
|  |     this->state_callback_.call(); | ||||||
|  |     if (state == ACP_STATE_TRIGGERED) { | ||||||
|  |       this->triggered_callback_.call(); | ||||||
|  |     } | ||||||
|  |     if (prev_state == ACP_STATE_TRIGGERED) { | ||||||
|  |       this->cleared_callback_.call(); | ||||||
|  |     } | ||||||
|  |     if (state == this->desired_state_) { | ||||||
|  |       // only store when in the desired state | ||||||
|  |       this->pref_.save(&state); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AlarmControlPanel::add_on_state_callback(std::function<void()> &&callback) { | ||||||
|  |   this->state_callback_.add(std::move(callback)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AlarmControlPanel::add_on_triggered_callback(std::function<void()> &&callback) { | ||||||
|  |   this->triggered_callback_.add(std::move(callback)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AlarmControlPanel::add_on_cleared_callback(std::function<void()> &&callback) { | ||||||
|  |   this->cleared_callback_.add(std::move(callback)); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AlarmControlPanel::arm_away(optional<std::string> code) { | ||||||
|  |   auto call = this->make_call(); | ||||||
|  |   call.arm_away(); | ||||||
|  |   if (code.has_value()) | ||||||
|  |     call.set_code(code.value()); | ||||||
|  |   call.perform(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AlarmControlPanel::arm_home(optional<std::string> code) { | ||||||
|  |   auto call = this->make_call(); | ||||||
|  |   call.arm_home(); | ||||||
|  |   if (code.has_value()) | ||||||
|  |     call.set_code(code.value()); | ||||||
|  |   call.perform(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AlarmControlPanel::arm_night(optional<std::string> code) { | ||||||
|  |   auto call = this->make_call(); | ||||||
|  |   call.arm_night(); | ||||||
|  |   if (code.has_value()) | ||||||
|  |     call.set_code(code.value()); | ||||||
|  |   call.perform(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AlarmControlPanel::arm_vacation(optional<std::string> code) { | ||||||
|  |   auto call = this->make_call(); | ||||||
|  |   call.arm_vacation(); | ||||||
|  |   if (code.has_value()) | ||||||
|  |     call.set_code(code.value()); | ||||||
|  |   call.perform(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AlarmControlPanel::arm_custom_bypass(optional<std::string> code) { | ||||||
|  |   auto call = this->make_call(); | ||||||
|  |   call.arm_custom_bypass(); | ||||||
|  |   if (code.has_value()) | ||||||
|  |     call.set_code(code.value()); | ||||||
|  |   call.perform(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AlarmControlPanel::disarm(optional<std::string> code) { | ||||||
|  |   auto call = this->make_call(); | ||||||
|  |   call.disarm(); | ||||||
|  |   if (code.has_value()) | ||||||
|  |     call.set_code(code.value()); | ||||||
|  |   call.perform(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace alarm_control_panel | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										136
									
								
								esphome/components/alarm_control_panel/alarm_control_panel.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										136
									
								
								esphome/components/alarm_control_panel/alarm_control_panel.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,136 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include <map> | ||||||
|  |  | ||||||
|  | #include "alarm_control_panel_call.h" | ||||||
|  | #include "alarm_control_panel_state.h" | ||||||
|  |  | ||||||
|  | #include "esphome/core/automation.h" | ||||||
|  | #include "esphome/core/entity_base.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace alarm_control_panel { | ||||||
|  |  | ||||||
|  | enum AlarmControlPanelFeature : uint8_t { | ||||||
|  |   // Matches Home Assistant values | ||||||
|  |   ACP_FEAT_ARM_HOME = 1 << 0, | ||||||
|  |   ACP_FEAT_ARM_AWAY = 1 << 1, | ||||||
|  |   ACP_FEAT_ARM_NIGHT = 1 << 2, | ||||||
|  |   ACP_FEAT_TRIGGER = 1 << 3, | ||||||
|  |   ACP_FEAT_ARM_CUSTOM_BYPASS = 1 << 4, | ||||||
|  |   ACP_FEAT_ARM_VACATION = 1 << 5, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class AlarmControlPanel : public EntityBase { | ||||||
|  |  public: | ||||||
|  |   /** Make a AlarmControlPanelCall | ||||||
|  |    * | ||||||
|  |    */ | ||||||
|  |   AlarmControlPanelCall make_call(); | ||||||
|  |  | ||||||
|  |   /** Set the state of the alarm_control_panel. | ||||||
|  |    * | ||||||
|  |    * @param state The AlarmControlPanelState. | ||||||
|  |    */ | ||||||
|  |   void publish_state(AlarmControlPanelState state); | ||||||
|  |  | ||||||
|  |   /** Add a callback for when the state of the alarm_control_panel changes | ||||||
|  |    * | ||||||
|  |    * @param callback The callback function | ||||||
|  |    */ | ||||||
|  |   void add_on_state_callback(std::function<void()> &&callback); | ||||||
|  |  | ||||||
|  |   /** Add a callback for when the state of the alarm_control_panel chanes to triggered | ||||||
|  |    * | ||||||
|  |    * @param callback The callback function | ||||||
|  |    */ | ||||||
|  |   void add_on_triggered_callback(std::function<void()> &&callback); | ||||||
|  |  | ||||||
|  |   /** Add a callback for when the state of the alarm_control_panel clears from triggered | ||||||
|  |    * | ||||||
|  |    * @param callback The callback function | ||||||
|  |    */ | ||||||
|  |   void add_on_cleared_callback(std::function<void()> &&callback); | ||||||
|  |  | ||||||
|  |   /** A numeric representation of the supported features as per HomeAssistant | ||||||
|  |    * | ||||||
|  |    */ | ||||||
|  |   virtual uint32_t get_supported_features() const = 0; | ||||||
|  |  | ||||||
|  |   /** Returns if the alarm_control_panel has a code | ||||||
|  |    * | ||||||
|  |    */ | ||||||
|  |   virtual bool get_requires_code() const = 0; | ||||||
|  |  | ||||||
|  |   /** Returns if the alarm_control_panel requires a code to arm | ||||||
|  |    * | ||||||
|  |    */ | ||||||
|  |   virtual bool get_requires_code_to_arm() const = 0; | ||||||
|  |  | ||||||
|  |   /** arm the alarm in away mode | ||||||
|  |    * | ||||||
|  |    * @param code The code | ||||||
|  |    */ | ||||||
|  |   void arm_away(optional<std::string> code = nullopt); | ||||||
|  |  | ||||||
|  |   /** arm the alarm in home mode | ||||||
|  |    * | ||||||
|  |    * @param code The code | ||||||
|  |    */ | ||||||
|  |   void arm_home(optional<std::string> code = nullopt); | ||||||
|  |  | ||||||
|  |   /** arm the alarm in night mode | ||||||
|  |    * | ||||||
|  |    * @param code The code | ||||||
|  |    */ | ||||||
|  |   void arm_night(optional<std::string> code = nullopt); | ||||||
|  |  | ||||||
|  |   /** arm the alarm in vacation mode | ||||||
|  |    * | ||||||
|  |    * @param code The code | ||||||
|  |    */ | ||||||
|  |   void arm_vacation(optional<std::string> code = nullopt); | ||||||
|  |  | ||||||
|  |   /** arm the alarm in custom bypass mode | ||||||
|  |    * | ||||||
|  |    * @param code The code | ||||||
|  |    */ | ||||||
|  |   void arm_custom_bypass(optional<std::string> code = nullopt); | ||||||
|  |  | ||||||
|  |   /** disarm the alarm | ||||||
|  |    * | ||||||
|  |    * @param code The code | ||||||
|  |    */ | ||||||
|  |   void disarm(optional<std::string> code = nullopt); | ||||||
|  |  | ||||||
|  |   /** Get the state | ||||||
|  |    * | ||||||
|  |    */ | ||||||
|  |   AlarmControlPanelState get_state() const { return this->current_state_; } | ||||||
|  |  | ||||||
|  |   // is the state one of the armed states | ||||||
|  |   bool is_state_armed(AlarmControlPanelState state); | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   friend AlarmControlPanelCall; | ||||||
|  |   // in order to store last panel state in flash | ||||||
|  |   ESPPreferenceObject pref_; | ||||||
|  |   // current state | ||||||
|  |   AlarmControlPanelState current_state_; | ||||||
|  |   // the desired (or previous) state | ||||||
|  |   AlarmControlPanelState desired_state_; | ||||||
|  |   // last time the state was updated | ||||||
|  |   uint32_t last_update_; | ||||||
|  |   // the call control function | ||||||
|  |   virtual void control(const AlarmControlPanelCall &call) = 0; | ||||||
|  |   // state callback | ||||||
|  |   CallbackManager<void()> state_callback_{}; | ||||||
|  |   // trigger callback | ||||||
|  |   CallbackManager<void()> triggered_callback_{}; | ||||||
|  |   // clear callback | ||||||
|  |   CallbackManager<void()> cleared_callback_{}; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace alarm_control_panel | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,99 @@ | |||||||
|  | #include "alarm_control_panel_call.h" | ||||||
|  |  | ||||||
|  | #include "alarm_control_panel.h" | ||||||
|  |  | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace alarm_control_panel { | ||||||
|  |  | ||||||
|  | static const char *const TAG = "alarm_control_panel"; | ||||||
|  |  | ||||||
|  | AlarmControlPanelCall::AlarmControlPanelCall(AlarmControlPanel *parent) : parent_(parent) {} | ||||||
|  |  | ||||||
|  | AlarmControlPanelCall &AlarmControlPanelCall::set_code(const std::string &code) { | ||||||
|  |   this->code_ = code; | ||||||
|  |   return *this; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | AlarmControlPanelCall &AlarmControlPanelCall::arm_away() { | ||||||
|  |   this->state_ = ACP_STATE_ARMED_AWAY; | ||||||
|  |   return *this; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | AlarmControlPanelCall &AlarmControlPanelCall::arm_home() { | ||||||
|  |   this->state_ = ACP_STATE_ARMED_HOME; | ||||||
|  |   return *this; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | AlarmControlPanelCall &AlarmControlPanelCall::arm_night() { | ||||||
|  |   this->state_ = ACP_STATE_ARMED_NIGHT; | ||||||
|  |   return *this; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | AlarmControlPanelCall &AlarmControlPanelCall::arm_vacation() { | ||||||
|  |   this->state_ = ACP_STATE_ARMED_VACATION; | ||||||
|  |   return *this; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | AlarmControlPanelCall &AlarmControlPanelCall::arm_custom_bypass() { | ||||||
|  |   this->state_ = ACP_STATE_ARMED_CUSTOM_BYPASS; | ||||||
|  |   return *this; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | AlarmControlPanelCall &AlarmControlPanelCall::disarm() { | ||||||
|  |   this->state_ = ACP_STATE_DISARMED; | ||||||
|  |   return *this; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | AlarmControlPanelCall &AlarmControlPanelCall::pending() { | ||||||
|  |   this->state_ = ACP_STATE_PENDING; | ||||||
|  |   return *this; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | AlarmControlPanelCall &AlarmControlPanelCall::triggered() { | ||||||
|  |   this->state_ = ACP_STATE_TRIGGERED; | ||||||
|  |   return *this; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | const optional<AlarmControlPanelState> &AlarmControlPanelCall::get_state() const { return this->state_; } | ||||||
|  | const optional<std::string> &AlarmControlPanelCall::get_code() const { return this->code_; } | ||||||
|  |  | ||||||
|  | void AlarmControlPanelCall::validate_() { | ||||||
|  |   if (this->state_.has_value()) { | ||||||
|  |     auto state = *this->state_; | ||||||
|  |     if (this->parent_->is_state_armed(state) && this->parent_->get_state() != ACP_STATE_DISARMED) { | ||||||
|  |       ESP_LOGW(TAG, "Cannot arm when not disarmed"); | ||||||
|  |       this->state_.reset(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (state == ACP_STATE_PENDING && this->parent_->get_state() == ACP_STATE_DISARMED) { | ||||||
|  |       ESP_LOGW(TAG, "Cannot trip alarm when disarmed"); | ||||||
|  |       this->state_.reset(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (state == ACP_STATE_DISARMED && | ||||||
|  |         !(this->parent_->is_state_armed(this->parent_->get_state()) || | ||||||
|  |           this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_ARMING || | ||||||
|  |           this->parent_->get_state() == ACP_STATE_TRIGGERED)) { | ||||||
|  |       ESP_LOGW(TAG, "Cannot disarm when not armed"); | ||||||
|  |       this->state_.reset(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |     if (state == ACP_STATE_ARMED_HOME && (this->parent_->get_supported_features() & ACP_FEAT_ARM_HOME) == 0) { | ||||||
|  |       ESP_LOGW(TAG, "Cannot arm home when not supported"); | ||||||
|  |       this->state_.reset(); | ||||||
|  |       return; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void AlarmControlPanelCall::perform() { | ||||||
|  |   this->validate_(); | ||||||
|  |   if (this->state_) { | ||||||
|  |     this->parent_->control(*this); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace alarm_control_panel | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,40 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include <string> | ||||||
|  |  | ||||||
|  | #include "alarm_control_panel_state.h" | ||||||
|  |  | ||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace alarm_control_panel { | ||||||
|  |  | ||||||
|  | class AlarmControlPanel; | ||||||
|  |  | ||||||
|  | class AlarmControlPanelCall { | ||||||
|  |  public: | ||||||
|  |   AlarmControlPanelCall(AlarmControlPanel *parent); | ||||||
|  |  | ||||||
|  |   AlarmControlPanelCall &set_code(const std::string &code); | ||||||
|  |   AlarmControlPanelCall &arm_away(); | ||||||
|  |   AlarmControlPanelCall &arm_home(); | ||||||
|  |   AlarmControlPanelCall &arm_night(); | ||||||
|  |   AlarmControlPanelCall &arm_vacation(); | ||||||
|  |   AlarmControlPanelCall &arm_custom_bypass(); | ||||||
|  |   AlarmControlPanelCall &disarm(); | ||||||
|  |   AlarmControlPanelCall &pending(); | ||||||
|  |   AlarmControlPanelCall &triggered(); | ||||||
|  |  | ||||||
|  |   void perform(); | ||||||
|  |   const optional<AlarmControlPanelState> &get_state() const; | ||||||
|  |   const optional<std::string> &get_code() const; | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   AlarmControlPanel *parent_; | ||||||
|  |   optional<std::string> code_{}; | ||||||
|  |   optional<AlarmControlPanelState> state_{}; | ||||||
|  |   void validate_(); | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace alarm_control_panel | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,34 @@ | |||||||
|  | #include "alarm_control_panel_state.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace alarm_control_panel { | ||||||
|  |  | ||||||
|  | const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state) { | ||||||
|  |   switch (state) { | ||||||
|  |     case ACP_STATE_DISARMED: | ||||||
|  |       return LOG_STR("DISARMED"); | ||||||
|  |     case ACP_STATE_ARMED_HOME: | ||||||
|  |       return LOG_STR("ARMED_HOME"); | ||||||
|  |     case ACP_STATE_ARMED_AWAY: | ||||||
|  |       return LOG_STR("ARMED_AWAY"); | ||||||
|  |     case ACP_STATE_ARMED_NIGHT: | ||||||
|  |       return LOG_STR("NIGHT"); | ||||||
|  |     case ACP_STATE_ARMED_VACATION: | ||||||
|  |       return LOG_STR("ARMED_VACATION"); | ||||||
|  |     case ACP_STATE_ARMED_CUSTOM_BYPASS: | ||||||
|  |       return LOG_STR("ARMED_CUSTOM_BYPASS"); | ||||||
|  |     case ACP_STATE_PENDING: | ||||||
|  |       return LOG_STR("PENDING"); | ||||||
|  |     case ACP_STATE_ARMING: | ||||||
|  |       return LOG_STR("ARMING"); | ||||||
|  |     case ACP_STATE_DISARMING: | ||||||
|  |       return LOG_STR("DISARMING"); | ||||||
|  |     case ACP_STATE_TRIGGERED: | ||||||
|  |       return LOG_STR("TRIGGERED"); | ||||||
|  |     default: | ||||||
|  |       return LOG_STR("UNKNOWN"); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace alarm_control_panel | ||||||
|  | }  // namespace esphome | ||||||
| @@ -0,0 +1,29 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include <cstdint> | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace alarm_control_panel { | ||||||
|  |  | ||||||
|  | enum AlarmControlPanelState : uint8_t { | ||||||
|  |   ACP_STATE_DISARMED = 0, | ||||||
|  |   ACP_STATE_ARMED_HOME = 1, | ||||||
|  |   ACP_STATE_ARMED_AWAY = 2, | ||||||
|  |   ACP_STATE_ARMED_NIGHT = 3, | ||||||
|  |   ACP_STATE_ARMED_VACATION = 4, | ||||||
|  |   ACP_STATE_ARMED_CUSTOM_BYPASS = 5, | ||||||
|  |   ACP_STATE_PENDING = 6, | ||||||
|  |   ACP_STATE_ARMING = 7, | ||||||
|  |   ACP_STATE_DISARMING = 8, | ||||||
|  |   ACP_STATE_TRIGGERED = 9 | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | /** Returns a string representation of the state. | ||||||
|  |  * | ||||||
|  |  * @param state The AlarmControlPanelState. | ||||||
|  |  */ | ||||||
|  | const LogString *alarm_control_panel_state_to_string(AlarmControlPanelState state); | ||||||
|  |  | ||||||
|  | }  // namespace alarm_control_panel | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										115
									
								
								esphome/components/alarm_control_panel/automation.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										115
									
								
								esphome/components/alarm_control_panel/automation.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,115 @@ | |||||||
|  |  | ||||||
|  | #pragma once | ||||||
|  | #include "esphome/core/automation.h" | ||||||
|  | #include "alarm_control_panel.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace alarm_control_panel { | ||||||
|  |  | ||||||
|  | class StateTrigger : public Trigger<> { | ||||||
|  |  public: | ||||||
|  |   explicit StateTrigger(AlarmControlPanel *alarm_control_panel) { | ||||||
|  |     alarm_control_panel->add_on_state_callback([this]() { this->trigger(); }); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class TriggeredTrigger : public Trigger<> { | ||||||
|  |  public: | ||||||
|  |   explicit TriggeredTrigger(AlarmControlPanel *alarm_control_panel) { | ||||||
|  |     alarm_control_panel->add_on_triggered_callback([this]() { this->trigger(); }); | ||||||
|  |   } | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class ClearedTrigger : public Trigger<> { | ||||||
|  |  public: | ||||||
|  |   explicit ClearedTrigger(AlarmControlPanel *alarm_control_panel) { | ||||||
|  |     alarm_control_panel->add_on_cleared_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) {} | ||||||
|  |  | ||||||
|  |   TEMPLATABLE_VALUE(std::string, code) | ||||||
|  |  | ||||||
|  |   void play(Ts... x) override { | ||||||
|  |     auto call = this->alarm_control_panel_->make_call(); | ||||||
|  |     auto code = this->code_.optional_value(x...); | ||||||
|  |     if (code.has_value()) { | ||||||
|  |       call.set_code(code.value()); | ||||||
|  |     } | ||||||
|  |     call.arm_away(); | ||||||
|  |     call.perform(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   AlarmControlPanel *alarm_control_panel_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class ArmHomeAction : public Action<Ts...> { | ||||||
|  |  public: | ||||||
|  |   explicit ArmHomeAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} | ||||||
|  |  | ||||||
|  |   TEMPLATABLE_VALUE(std::string, code) | ||||||
|  |  | ||||||
|  |   void play(Ts... x) override { | ||||||
|  |     auto call = this->alarm_control_panel_->make_call(); | ||||||
|  |     auto code = this->code_.optional_value(x...); | ||||||
|  |     if (code.has_value()) { | ||||||
|  |       call.set_code(code.value()); | ||||||
|  |     } | ||||||
|  |     call.arm_home(); | ||||||
|  |     call.perform(); | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   AlarmControlPanel *alarm_control_panel_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class DisarmAction : public Action<Ts...> { | ||||||
|  |  public: | ||||||
|  |   explicit DisarmAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} | ||||||
|  |  | ||||||
|  |   TEMPLATABLE_VALUE(std::string, code) | ||||||
|  |  | ||||||
|  |   void play(Ts... x) override { this->alarm_control_panel_->disarm(this->code_.optional_value(x...)); } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   AlarmControlPanel *alarm_control_panel_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class PendingAction : public Action<Ts...> { | ||||||
|  |  public: | ||||||
|  |   explicit PendingAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} | ||||||
|  |  | ||||||
|  |   void play(Ts... x) override { this->alarm_control_panel_->make_call().pending().perform(); } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   AlarmControlPanel *alarm_control_panel_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class TriggeredAction : public Action<Ts...> { | ||||||
|  |  public: | ||||||
|  |   explicit TriggeredAction(AlarmControlPanel *alarm_control_panel) : alarm_control_panel_(alarm_control_panel) {} | ||||||
|  |  | ||||||
|  |   void play(Ts... x) override { this->alarm_control_panel_->make_call().triggered().perform(); } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   AlarmControlPanel *alarm_control_panel_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | template<typename... Ts> class AlarmControlPanelCondition : public Condition<Ts...> { | ||||||
|  |  public: | ||||||
|  |   AlarmControlPanelCondition(AlarmControlPanel *parent) : parent_(parent) {} | ||||||
|  |   bool check(Ts... x) override { | ||||||
|  |     return this->parent_->is_state_armed(this->parent_->get_state()) || | ||||||
|  |            this->parent_->get_state() == ACP_STATE_PENDING || this->parent_->get_state() == ACP_STATE_TRIGGERED; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   AlarmControlPanel *parent_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace alarm_control_panel | ||||||
|  | }  // namespace esphome | ||||||
| @@ -3,9 +3,17 @@ import logging | |||||||
| from esphome import core | from esphome import core | ||||||
| from esphome.components import display, font | from esphome.components import display, font | ||||||
| import esphome.components.image as espImage | import esphome.components.image as espImage | ||||||
|  | from esphome.components.image import CONF_USE_TRANSPARENCY | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| import esphome.codegen as cg | import esphome.codegen as cg | ||||||
| from esphome.const import CONF_FILE, CONF_ID, CONF_RAW_DATA_ID, CONF_RESIZE, CONF_TYPE | from esphome.const import ( | ||||||
|  |     CONF_FILE, | ||||||
|  |     CONF_ID, | ||||||
|  |     CONF_RAW_DATA_ID, | ||||||
|  |     CONF_REPEAT, | ||||||
|  |     CONF_RESIZE, | ||||||
|  |     CONF_TYPE, | ||||||
|  | ) | ||||||
| from esphome.core import CORE, HexInt | from esphome.core import CORE, HexInt | ||||||
|  |  | ||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
| @@ -13,9 +21,34 @@ _LOGGER = logging.getLogger(__name__) | |||||||
| DEPENDENCIES = ["display"] | DEPENDENCIES = ["display"] | ||||||
| MULTI_CONF = True | MULTI_CONF = True | ||||||
|  |  | ||||||
|  | CONF_LOOP = "loop" | ||||||
|  | CONF_START_FRAME = "start_frame" | ||||||
|  | CONF_END_FRAME = "end_frame" | ||||||
|  |  | ||||||
| Animation_ = display.display_ns.class_("Animation", espImage.Image_) | Animation_ = display.display_ns.class_("Animation", espImage.Image_) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_cross_dependencies(config): | ||||||
|  |     """ | ||||||
|  |     Validate fields whose possible values depend on other fields. | ||||||
|  |     For example, validate that explicitly transparent image types | ||||||
|  |     have "use_transparency" set to True. | ||||||
|  |     Also set the default value for those kind of dependent fields. | ||||||
|  |     """ | ||||||
|  |     image_type = config[CONF_TYPE] | ||||||
|  |     is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"] | ||||||
|  |     # If the use_transparency option was not specified, set the default depending on the image type | ||||||
|  |     if CONF_USE_TRANSPARENCY not in config: | ||||||
|  |         config[CONF_USE_TRANSPARENCY] = is_transparent_type | ||||||
|  |  | ||||||
|  |     if is_transparent_type and not config[CONF_USE_TRANSPARENCY]: | ||||||
|  |         raise cv.Invalid(f"Image type {image_type} must always be transparent.") | ||||||
|  |  | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
| ANIMATION_SCHEMA = cv.Schema( | ANIMATION_SCHEMA = cv.Schema( | ||||||
|  |     cv.All( | ||||||
|         { |         { | ||||||
|             cv.Required(CONF_ID): cv.declare_id(Animation_), |             cv.Required(CONF_ID): cv.declare_id(Animation_), | ||||||
|             cv.Required(CONF_FILE): cv.file_, |             cv.Required(CONF_FILE): cv.file_, | ||||||
| @@ -23,8 +56,20 @@ ANIMATION_SCHEMA = cv.Schema( | |||||||
|             cv.Optional(CONF_TYPE, default="BINARY"): cv.enum( |             cv.Optional(CONF_TYPE, default="BINARY"): cv.enum( | ||||||
|                 espImage.IMAGE_TYPE, upper=True |                 espImage.IMAGE_TYPE, upper=True | ||||||
|             ), |             ), | ||||||
|         cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), |             # Not setting default here on purpose; the default depends on the image type, | ||||||
|  |             # and thus will be set in the "validate_cross_dependencies" validator. | ||||||
|  |             cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean, | ||||||
|  |             cv.Optional(CONF_LOOP): cv.All( | ||||||
|  |                 { | ||||||
|  |                     cv.Optional(CONF_START_FRAME, default=0): cv.positive_int, | ||||||
|  |                     cv.Optional(CONF_END_FRAME): cv.positive_int, | ||||||
|  |                     cv.Optional(CONF_REPEAT): cv.positive_int, | ||||||
|                 } |                 } | ||||||
|  |             ), | ||||||
|  |             cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), | ||||||
|  |         }, | ||||||
|  |         validate_cross_dependencies, | ||||||
|  |     ) | ||||||
| ) | ) | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA) | CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, ANIMATION_SCHEMA) | ||||||
| @@ -50,16 +95,19 @@ async def to_code(config): | |||||||
|     else: |     else: | ||||||
|         if width > 500 or height > 500: |         if width > 500 or height > 500: | ||||||
|             _LOGGER.warning( |             _LOGGER.warning( | ||||||
|                 "The image you requested is very big. Please consider using" |                 'The image "%s" you requested is very big. Please consider' | ||||||
|                 " the resize parameter." |                 " using the resize parameter.", | ||||||
|  |                 path, | ||||||
|             ) |             ) | ||||||
|  |  | ||||||
|  |     transparent = config[CONF_USE_TRANSPARENCY] | ||||||
|  |  | ||||||
|     if config[CONF_TYPE] == "GRAYSCALE": |     if config[CONF_TYPE] == "GRAYSCALE": | ||||||
|         data = [0 for _ in range(height * width * frames)] |         data = [0 for _ in range(height * width * frames)] | ||||||
|         pos = 0 |         pos = 0 | ||||||
|         for frameIndex in range(frames): |         for frameIndex in range(frames): | ||||||
|             image.seek(frameIndex) |             image.seek(frameIndex) | ||||||
|             frame = image.convert("L", dither=Image.NONE) |             frame = image.convert("LA", dither=Image.NONE) | ||||||
|             if CONF_RESIZE in config: |             if CONF_RESIZE in config: | ||||||
|                 frame = frame.resize([width, height]) |                 frame = frame.resize([width, height]) | ||||||
|             pixels = list(frame.getdata()) |             pixels = list(frame.getdata()) | ||||||
| @@ -67,16 +115,22 @@ async def to_code(config): | |||||||
|                 raise core.EsphomeError( |                 raise core.EsphomeError( | ||||||
|                     f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" |                     f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" | ||||||
|                 ) |                 ) | ||||||
|             for pix in pixels: |             for pix, a in pixels: | ||||||
|  |                 if transparent: | ||||||
|  |                     if pix == 1: | ||||||
|  |                         pix = 0 | ||||||
|  |                     if a < 0x80: | ||||||
|  |                         pix = 1 | ||||||
|  |  | ||||||
|                 data[pos] = pix |                 data[pos] = pix | ||||||
|                 pos += 1 |                 pos += 1 | ||||||
|  |  | ||||||
|     elif config[CONF_TYPE] == "RGB24": |     elif config[CONF_TYPE] == "RGBA": | ||||||
|         data = [0 for _ in range(height * width * 3 * frames)] |         data = [0 for _ in range(height * width * 4 * frames)] | ||||||
|         pos = 0 |         pos = 0 | ||||||
|         for frameIndex in range(frames): |         for frameIndex in range(frames): | ||||||
|             image.seek(frameIndex) |             image.seek(frameIndex) | ||||||
|             frame = image.convert("RGB") |             frame = image.convert("RGBA") | ||||||
|             if CONF_RESIZE in config: |             if CONF_RESIZE in config: | ||||||
|                 frame = frame.resize([width, height]) |                 frame = frame.resize([width, height]) | ||||||
|             pixels = list(frame.getdata()) |             pixels = list(frame.getdata()) | ||||||
| @@ -91,13 +145,15 @@ async def to_code(config): | |||||||
|                 pos += 1 |                 pos += 1 | ||||||
|                 data[pos] = pix[2] |                 data[pos] = pix[2] | ||||||
|                 pos += 1 |                 pos += 1 | ||||||
|  |                 data[pos] = pix[3] | ||||||
|  |                 pos += 1 | ||||||
|  |  | ||||||
|     elif config[CONF_TYPE] == "RGB565": |     elif config[CONF_TYPE] == "RGB24": | ||||||
|         data = [0 for _ in range(height * width * 2 * frames)] |         data = [0 for _ in range(height * width * 3 * frames)] | ||||||
|         pos = 0 |         pos = 0 | ||||||
|         for frameIndex in range(frames): |         for frameIndex in range(frames): | ||||||
|             image.seek(frameIndex) |             image.seek(frameIndex) | ||||||
|             frame = image.convert("RGB") |             frame = image.convert("RGBA") | ||||||
|             if CONF_RESIZE in config: |             if CONF_RESIZE in config: | ||||||
|                 frame = frame.resize([width, height]) |                 frame = frame.resize([width, height]) | ||||||
|             pixels = list(frame.getdata()) |             pixels = list(frame.getdata()) | ||||||
| @@ -105,14 +161,50 @@ async def to_code(config): | |||||||
|                 raise core.EsphomeError( |                 raise core.EsphomeError( | ||||||
|                     f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" |                     f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" | ||||||
|                 ) |                 ) | ||||||
|             for pix in pixels: |             for r, g, b, a in pixels: | ||||||
|                 R = pix[0] >> 3 |                 if transparent: | ||||||
|                 G = pix[1] >> 2 |                     if r == 0 and g == 0 and b == 1: | ||||||
|                 B = pix[2] >> 3 |                         b = 0 | ||||||
|  |                     if a < 0x80: | ||||||
|  |                         r = 0 | ||||||
|  |                         g = 0 | ||||||
|  |                         b = 1 | ||||||
|  |  | ||||||
|  |                 data[pos] = r | ||||||
|  |                 pos += 1 | ||||||
|  |                 data[pos] = g | ||||||
|  |                 pos += 1 | ||||||
|  |                 data[pos] = b | ||||||
|  |                 pos += 1 | ||||||
|  |  | ||||||
|  |     elif config[CONF_TYPE] in ["RGB565", "TRANSPARENT_IMAGE"]: | ||||||
|  |         data = [0 for _ in range(height * width * 2 * frames)] | ||||||
|  |         pos = 0 | ||||||
|  |         for frameIndex in range(frames): | ||||||
|  |             image.seek(frameIndex) | ||||||
|  |             frame = image.convert("RGBA") | ||||||
|  |             if CONF_RESIZE in config: | ||||||
|  |                 frame = frame.resize([width, height]) | ||||||
|  |             pixels = list(frame.getdata()) | ||||||
|  |             if len(pixels) != height * width: | ||||||
|  |                 raise core.EsphomeError( | ||||||
|  |                     f"Unexpected number of pixels in {path} frame {frameIndex}: ({len(pixels)} != {height*width})" | ||||||
|  |                 ) | ||||||
|  |             for r, g, b, a in pixels: | ||||||
|  |                 R = r >> 3 | ||||||
|  |                 G = g >> 2 | ||||||
|  |                 B = b >> 3 | ||||||
|                 rgb = (R << 11) | (G << 5) | B |                 rgb = (R << 11) | (G << 5) | B | ||||||
|  |  | ||||||
|  |                 if transparent: | ||||||
|  |                     if rgb == 0x0020: | ||||||
|  |                         rgb = 0 | ||||||
|  |                     if a < 0x80: | ||||||
|  |                         rgb = 0x0020 | ||||||
|  |  | ||||||
|                 data[pos] = rgb >> 8 |                 data[pos] = rgb >> 8 | ||||||
|                 pos += 1 |                 pos += 1 | ||||||
|                 data[pos] = rgb & 255 |                 data[pos] = rgb & 0xFF | ||||||
|                 pos += 1 |                 pos += 1 | ||||||
|  |  | ||||||
|     elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: |     elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: | ||||||
| @@ -120,19 +212,31 @@ async def to_code(config): | |||||||
|         data = [0 for _ in range((height * width8 // 8) * frames)] |         data = [0 for _ in range((height * width8 // 8) * frames)] | ||||||
|         for frameIndex in range(frames): |         for frameIndex in range(frames): | ||||||
|             image.seek(frameIndex) |             image.seek(frameIndex) | ||||||
|  |             if transparent: | ||||||
|  |                 alpha = image.split()[-1] | ||||||
|  |                 has_alpha = alpha.getextrema()[0] < 0xFF | ||||||
|             frame = image.convert("1", dither=Image.NONE) |             frame = image.convert("1", dither=Image.NONE) | ||||||
|             if CONF_RESIZE in config: |             if CONF_RESIZE in config: | ||||||
|                 frame = frame.resize([width, height]) |                 frame = frame.resize([width, height]) | ||||||
|             for y in range(height): |                 if transparent: | ||||||
|                 for x in range(width): |                     alpha = alpha.resize([width, height]) | ||||||
|                     if frame.getpixel((x, y)): |             for x, y in [(i, j) for i in range(width) for j in range(height)]: | ||||||
|  |                 if transparent and has_alpha: | ||||||
|  |                     if not alpha.getpixel((x, y)): | ||||||
|                         continue |                         continue | ||||||
|  |                 elif frame.getpixel((x, y)): | ||||||
|  |                     continue | ||||||
|  |  | ||||||
|                 pos = x + y * width8 + (height * width8 * frameIndex) |                 pos = x + y * width8 + (height * width8 * frameIndex) | ||||||
|                 data[pos // 8] |= 0x80 >> (pos % 8) |                 data[pos // 8] |= 0x80 >> (pos % 8) | ||||||
|  |     else: | ||||||
|  |         raise core.EsphomeError( | ||||||
|  |             f"Animation f{config[CONF_ID]} has not supported type {config[CONF_TYPE]}." | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     rhs = [HexInt(x) for x in data] |     rhs = [HexInt(x) for x in data] | ||||||
|     prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) |     prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) | ||||||
|     cg.new_Pvariable( |     var = cg.new_Pvariable( | ||||||
|         config[CONF_ID], |         config[CONF_ID], | ||||||
|         prog_arr, |         prog_arr, | ||||||
|         width, |         width, | ||||||
| @@ -140,3 +244,9 @@ async def to_code(config): | |||||||
|         frames, |         frames, | ||||||
|         espImage.IMAGE_TYPE[config[CONF_TYPE]], |         espImage.IMAGE_TYPE[config[CONF_TYPE]], | ||||||
|     ) |     ) | ||||||
|  |     cg.add(var.set_transparency(transparent)) | ||||||
|  |     if CONF_LOOP in config: | ||||||
|  |         start = config[CONF_LOOP][CONF_START_FRAME] | ||||||
|  |         end = config[CONF_LOOP].get(CONF_END_FRAME, frames) | ||||||
|  |         count = config[CONF_LOOP].get(CONF_REPEAT, -1) | ||||||
|  |         cg.add(var.set_loop(start, end, count)) | ||||||
|   | |||||||
| @@ -56,6 +56,8 @@ service APIConnection { | |||||||
|   rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {} |   rpc unsubscribe_bluetooth_le_advertisements(UnsubscribeBluetoothLEAdvertisementsRequest) returns (void) {} | ||||||
|  |  | ||||||
|   rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {} |   rpc subscribe_voice_assistant(SubscribeVoiceAssistantRequest) returns (void) {} | ||||||
|  |  | ||||||
|  |   rpc alarm_control_panel_command (AlarmControlPanelCommandRequest) returns (void) {} | ||||||
| } | } | ||||||
|  |  | ||||||
|  |  | ||||||
| @@ -206,7 +208,8 @@ message DeviceInfoResponse { | |||||||
|  |  | ||||||
|   uint32 webserver_port = 10; |   uint32 webserver_port = 10; | ||||||
|  |  | ||||||
|   uint32 bluetooth_proxy_version = 11; |   uint32 legacy_bluetooth_proxy_version = 11; | ||||||
|  |   uint32 bluetooth_proxy_feature_flags = 15; | ||||||
|  |  | ||||||
|   string manufacturer = 12; |   string manufacturer = 12; | ||||||
|  |  | ||||||
| @@ -1130,6 +1133,8 @@ message SubscribeBluetoothLEAdvertisementsRequest { | |||||||
|   option (id) = 66; |   option (id) = 66; | ||||||
|   option (source) = SOURCE_CLIENT; |   option (source) = SOURCE_CLIENT; | ||||||
|   option (ifdef) = "USE_BLUETOOTH_PROXY"; |   option (ifdef) = "USE_BLUETOOTH_PROXY"; | ||||||
|  |  | ||||||
|  |   uint32 flags = 1; | ||||||
| } | } | ||||||
|  |  | ||||||
| message BluetoothServiceData { | message BluetoothServiceData { | ||||||
| @@ -1154,6 +1159,23 @@ message BluetoothLEAdvertisementResponse { | |||||||
|   uint32 address_type = 7; |   uint32 address_type = 7; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | message BluetoothLERawAdvertisement { | ||||||
|  |   uint64 address = 1; | ||||||
|  |   sint32 rssi = 2; | ||||||
|  |   uint32 address_type = 3; | ||||||
|  |  | ||||||
|  |   bytes data = 4; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message BluetoothLERawAdvertisementsResponse { | ||||||
|  |   option (id) = 93; | ||||||
|  |   option (source) = SOURCE_SERVER; | ||||||
|  |   option (ifdef) = "USE_BLUETOOTH_PROXY"; | ||||||
|  |   option (no_delay) = true; | ||||||
|  |  | ||||||
|  |   repeated BluetoothLERawAdvertisement advertisements = 1; | ||||||
|  | } | ||||||
|  |  | ||||||
| enum BluetoothDeviceRequestType { | enum BluetoothDeviceRequestType { | ||||||
|   BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0; |   BLUETOOTH_DEVICE_REQUEST_TYPE_CONNECT = 0; | ||||||
|   BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1; |   BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT = 1; | ||||||
| @@ -1397,6 +1419,7 @@ message VoiceAssistantRequest { | |||||||
|   option (ifdef) = "USE_VOICE_ASSISTANT"; |   option (ifdef) = "USE_VOICE_ASSISTANT"; | ||||||
|  |  | ||||||
|   bool start = 1; |   bool start = 1; | ||||||
|  |   string conversation_id = 2; | ||||||
| } | } | ||||||
|  |  | ||||||
| message VoiceAssistantResponse { | message VoiceAssistantResponse { | ||||||
| @@ -1433,3 +1456,63 @@ message VoiceAssistantEventResponse { | |||||||
|   VoiceAssistantEvent event_type = 1; |   VoiceAssistantEvent event_type = 1; | ||||||
|   repeated VoiceAssistantEventData data = 2; |   repeated VoiceAssistantEventData data = 2; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | // ==================== ALARM CONTROL PANEL ==================== | ||||||
|  | enum AlarmControlPanelState { | ||||||
|  |   ALARM_STATE_DISARMED = 0; | ||||||
|  |   ALARM_STATE_ARMED_HOME = 1; | ||||||
|  |   ALARM_STATE_ARMED_AWAY = 2; | ||||||
|  |   ALARM_STATE_ARMED_NIGHT = 3; | ||||||
|  |   ALARM_STATE_ARMED_VACATION = 4; | ||||||
|  |   ALARM_STATE_ARMED_CUSTOM_BYPASS = 5; | ||||||
|  |   ALARM_STATE_PENDING = 6; | ||||||
|  |   ALARM_STATE_ARMING = 7; | ||||||
|  |   ALARM_STATE_DISARMING = 8; | ||||||
|  |   ALARM_STATE_TRIGGERED = 9; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | enum AlarmControlPanelStateCommand { | ||||||
|  |   ALARM_CONTROL_PANEL_DISARM = 0; | ||||||
|  |   ALARM_CONTROL_PANEL_ARM_AWAY = 1; | ||||||
|  |   ALARM_CONTROL_PANEL_ARM_HOME = 2; | ||||||
|  |   ALARM_CONTROL_PANEL_ARM_NIGHT = 3; | ||||||
|  |   ALARM_CONTROL_PANEL_ARM_VACATION = 4; | ||||||
|  |   ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS = 5; | ||||||
|  |   ALARM_CONTROL_PANEL_TRIGGER = 6; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message ListEntitiesAlarmControlPanelResponse { | ||||||
|  |   option (id) = 94; | ||||||
|  |   option (source) = SOURCE_SERVER; | ||||||
|  |   option (ifdef) = "USE_ALARM_CONTROL_PANEL"; | ||||||
|  |  | ||||||
|  |   string object_id = 1; | ||||||
|  |   fixed32 key = 2; | ||||||
|  |   string name = 3; | ||||||
|  |   string unique_id = 4; | ||||||
|  |   string icon = 5; | ||||||
|  |   bool disabled_by_default = 6; | ||||||
|  |   EntityCategory entity_category = 7; | ||||||
|  |   uint32 supported_features = 8; | ||||||
|  |   bool requires_code = 9; | ||||||
|  |   bool requires_code_to_arm = 10; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message AlarmControlPanelStateResponse { | ||||||
|  |   option (id) = 95; | ||||||
|  |   option (source) = SOURCE_SERVER; | ||||||
|  |   option (ifdef) = "USE_ALARM_CONTROL_PANEL"; | ||||||
|  |   option (no_delay) = true; | ||||||
|  |   fixed32 key = 1; | ||||||
|  |   AlarmControlPanelState state = 2; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | message AlarmControlPanelCommandRequest { | ||||||
|  |   option (id) = 96; | ||||||
|  |   option (source) = SOURCE_CLIENT; | ||||||
|  |   option (ifdef) = "USE_ALARM_CONTROL_PANEL"; | ||||||
|  |   option (no_delay) = true; | ||||||
|  |   fixed32 key = 1; | ||||||
|  |   AlarmControlPanelStateCommand command = 2; | ||||||
|  |   string code = 3; | ||||||
|  | } | ||||||
|   | |||||||
| @@ -51,6 +51,14 @@ void APIConnection::start() { | |||||||
|   helper_->set_log_info(client_info_); |   helper_->set_log_info(client_info_); | ||||||
| } | } | ||||||
|  |  | ||||||
|  | APIConnection::~APIConnection() { | ||||||
|  | #ifdef USE_BLUETOOTH_PROXY | ||||||
|  |   if (bluetooth_proxy::global_bluetooth_proxy->get_api_connection() == this) { | ||||||
|  |     bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); | ||||||
|  |   } | ||||||
|  | #endif | ||||||
|  | } | ||||||
|  |  | ||||||
| void APIConnection::loop() { | void APIConnection::loop() { | ||||||
|   if (this->remove_) |   if (this->remove_) | ||||||
|     return; |     return; | ||||||
| @@ -845,9 +853,13 @@ void APIConnection::on_get_time_response(const GetTimeResponse &value) { | |||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_BLUETOOTH_PROXY | #ifdef USE_BLUETOOTH_PROXY | ||||||
|  | void APIConnection::subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) { | ||||||
|  |   bluetooth_proxy::global_bluetooth_proxy->subscribe_api_connection(this, msg.flags); | ||||||
|  | } | ||||||
|  | void APIConnection::unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) { | ||||||
|  |   bluetooth_proxy::global_bluetooth_proxy->unsubscribe_api_connection(this); | ||||||
|  | } | ||||||
| bool APIConnection::send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg) { | bool APIConnection::send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg) { | ||||||
|   if (!this->bluetooth_le_advertisement_subscription_) |  | ||||||
|     return false; |  | ||||||
|   if (this->client_api_version_major_ < 1 || this->client_api_version_minor_ < 7) { |   if (this->client_api_version_major_ < 1 || this->client_api_version_minor_ < 7) { | ||||||
|     BluetoothLEAdvertisementResponse resp = msg; |     BluetoothLEAdvertisementResponse resp = msg; | ||||||
|     for (auto &service : resp.service_data) { |     for (auto &service : resp.service_data) { | ||||||
| @@ -895,11 +907,12 @@ BluetoothConnectionsFreeResponse APIConnection::subscribe_bluetooth_connections_ | |||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_VOICE_ASSISTANT | #ifdef USE_VOICE_ASSISTANT | ||||||
| bool APIConnection::request_voice_assistant(bool start) { | bool APIConnection::request_voice_assistant(bool start, const std::string &conversation_id) { | ||||||
|   if (!this->voice_assistant_subscription_) |   if (!this->voice_assistant_subscription_) | ||||||
|     return false; |     return false; | ||||||
|   VoiceAssistantRequest msg; |   VoiceAssistantRequest msg; | ||||||
|   msg.start = start; |   msg.start = start; | ||||||
|  |   msg.conversation_id = conversation_id; | ||||||
|   return this->send_voice_assistant_request(msg); |   return this->send_voice_assistant_request(msg); | ||||||
| } | } | ||||||
| void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) { | void APIConnection::on_voice_assistant_response(const VoiceAssistantResponse &msg) { | ||||||
| @@ -918,6 +931,64 @@ void APIConnection::on_voice_assistant_event_response(const VoiceAssistantEventR | |||||||
|  |  | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|  | bool APIConnection::send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { | ||||||
|  |   if (!this->state_subscription_) | ||||||
|  |     return false; | ||||||
|  |  | ||||||
|  |   AlarmControlPanelStateResponse resp{}; | ||||||
|  |   resp.key = a_alarm_control_panel->get_object_id_hash(); | ||||||
|  |   resp.state = static_cast<enums::AlarmControlPanelState>(a_alarm_control_panel->get_state()); | ||||||
|  |   return this->send_alarm_control_panel_state_response(resp); | ||||||
|  | } | ||||||
|  | bool APIConnection::send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { | ||||||
|  |   ListEntitiesAlarmControlPanelResponse msg; | ||||||
|  |   msg.key = a_alarm_control_panel->get_object_id_hash(); | ||||||
|  |   msg.object_id = a_alarm_control_panel->get_object_id(); | ||||||
|  |   msg.name = a_alarm_control_panel->get_name(); | ||||||
|  |   msg.unique_id = get_default_unique_id("alarm_control_panel", a_alarm_control_panel); | ||||||
|  |   msg.icon = a_alarm_control_panel->get_icon(); | ||||||
|  |   msg.disabled_by_default = a_alarm_control_panel->is_disabled_by_default(); | ||||||
|  |   msg.entity_category = static_cast<enums::EntityCategory>(a_alarm_control_panel->get_entity_category()); | ||||||
|  |   msg.supported_features = a_alarm_control_panel->get_supported_features(); | ||||||
|  |   msg.requires_code = a_alarm_control_panel->get_requires_code(); | ||||||
|  |   msg.requires_code_to_arm = a_alarm_control_panel->get_requires_code_to_arm(); | ||||||
|  |   return this->send_list_entities_alarm_control_panel_response(msg); | ||||||
|  | } | ||||||
|  | void APIConnection::alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) { | ||||||
|  |   alarm_control_panel::AlarmControlPanel *a_alarm_control_panel = App.get_alarm_control_panel_by_key(msg.key); | ||||||
|  |   if (a_alarm_control_panel == nullptr) | ||||||
|  |     return; | ||||||
|  |  | ||||||
|  |   auto call = a_alarm_control_panel->make_call(); | ||||||
|  |   switch (msg.command) { | ||||||
|  |     case enums::ALARM_CONTROL_PANEL_DISARM: | ||||||
|  |       call.disarm(); | ||||||
|  |       break; | ||||||
|  |     case enums::ALARM_CONTROL_PANEL_ARM_AWAY: | ||||||
|  |       call.arm_away(); | ||||||
|  |       break; | ||||||
|  |     case enums::ALARM_CONTROL_PANEL_ARM_HOME: | ||||||
|  |       call.arm_home(); | ||||||
|  |       break; | ||||||
|  |     case enums::ALARM_CONTROL_PANEL_ARM_NIGHT: | ||||||
|  |       call.arm_night(); | ||||||
|  |       break; | ||||||
|  |     case enums::ALARM_CONTROL_PANEL_ARM_VACATION: | ||||||
|  |       call.arm_vacation(); | ||||||
|  |       break; | ||||||
|  |     case enums::ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS: | ||||||
|  |       call.arm_custom_bypass(); | ||||||
|  |       break; | ||||||
|  |     case enums::ALARM_CONTROL_PANEL_TRIGGER: | ||||||
|  |       call.pending(); | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|  |   call.set_code(msg.code); | ||||||
|  |   call.perform(); | ||||||
|  | } | ||||||
|  | #endif | ||||||
|  |  | ||||||
| bool APIConnection::send_log_message(int level, const char *tag, const char *line) { | bool APIConnection::send_log_message(int level, const char *tag, const char *line) { | ||||||
|   if (this->log_subscription_ < level) |   if (this->log_subscription_ < level) | ||||||
|     return false; |     return false; | ||||||
| @@ -942,7 +1013,7 @@ HelloResponse APIConnection::hello(const HelloRequest &msg) { | |||||||
|  |  | ||||||
|   HelloResponse resp; |   HelloResponse resp; | ||||||
|   resp.api_version_major = 1; |   resp.api_version_major = 1; | ||||||
|   resp.api_version_minor = 8; |   resp.api_version_minor = 9; | ||||||
|   resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; |   resp.server_info = App.get_name() + " (esphome v" ESPHOME_VERSION ")"; | ||||||
|   resp.name = App.get_name(); |   resp.name = App.get_name(); | ||||||
|  |  | ||||||
| @@ -994,9 +1065,8 @@ DeviceInfoResponse APIConnection::device_info(const DeviceInfoRequest &msg) { | |||||||
|   resp.webserver_port = USE_WEBSERVER_PORT; |   resp.webserver_port = USE_WEBSERVER_PORT; | ||||||
| #endif | #endif | ||||||
| #ifdef USE_BLUETOOTH_PROXY | #ifdef USE_BLUETOOTH_PROXY | ||||||
|   resp.bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->has_active() |   resp.legacy_bluetooth_proxy_version = bluetooth_proxy::global_bluetooth_proxy->get_legacy_version(); | ||||||
|                                      ? bluetooth_proxy::ACTIVE_CONNECTIONS_VERSION |   resp.bluetooth_proxy_feature_flags = bluetooth_proxy::global_bluetooth_proxy->get_feature_flags(); | ||||||
|                                      : bluetooth_proxy::PASSIVE_ONLY_VERSION; |  | ||||||
| #endif | #endif | ||||||
| #ifdef USE_VOICE_ASSISTANT | #ifdef USE_VOICE_ASSISTANT | ||||||
|   resp.voice_assistant_version = voice_assistant::global_voice_assistant->get_version(); |   resp.voice_assistant_version = voice_assistant::global_voice_assistant->get_version(); | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ namespace api { | |||||||
| class APIConnection : public APIServerConnection { | class APIConnection : public APIServerConnection { | ||||||
|  public: |  public: | ||||||
|   APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent); |   APIConnection(std::unique_ptr<socket::Socket> socket, APIServer *parent); | ||||||
|   virtual ~APIConnection() = default; |   virtual ~APIConnection(); | ||||||
|  |  | ||||||
|   void start(); |   void start(); | ||||||
|   void loop(); |   void loop(); | ||||||
| @@ -98,12 +98,8 @@ class APIConnection : public APIServerConnection { | |||||||
|     this->send_homeassistant_service_response(call); |     this->send_homeassistant_service_response(call); | ||||||
|   } |   } | ||||||
| #ifdef USE_BLUETOOTH_PROXY | #ifdef USE_BLUETOOTH_PROXY | ||||||
|   void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override { |   void subscribe_bluetooth_le_advertisements(const SubscribeBluetoothLEAdvertisementsRequest &msg) override; | ||||||
|     this->bluetooth_le_advertisement_subscription_ = true; |   void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override; | ||||||
|   } |  | ||||||
|   void unsubscribe_bluetooth_le_advertisements(const UnsubscribeBluetoothLEAdvertisementsRequest &msg) override { |  | ||||||
|     this->bluetooth_le_advertisement_subscription_ = false; |  | ||||||
|   } |  | ||||||
|   bool send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg); |   bool send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &msg); | ||||||
|  |  | ||||||
|   void bluetooth_device_request(const BluetoothDeviceRequest &msg) override; |   void bluetooth_device_request(const BluetoothDeviceRequest &msg) override; | ||||||
| @@ -128,11 +124,17 @@ class APIConnection : public APIServerConnection { | |||||||
|   void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override { |   void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) override { | ||||||
|     this->voice_assistant_subscription_ = msg.subscribe; |     this->voice_assistant_subscription_ = msg.subscribe; | ||||||
|   } |   } | ||||||
|   bool request_voice_assistant(bool start); |   bool request_voice_assistant(bool start, const std::string &conversation_id); | ||||||
|   void on_voice_assistant_response(const VoiceAssistantResponse &msg) override; |   void on_voice_assistant_response(const VoiceAssistantResponse &msg) override; | ||||||
|   void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override; |   void on_voice_assistant_event_response(const VoiceAssistantEventResponse &msg) override; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|  |   bool send_alarm_control_panel_state(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); | ||||||
|  |   bool send_alarm_control_panel_info(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel); | ||||||
|  |   void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) override; | ||||||
|  | #endif | ||||||
|  |  | ||||||
|   void on_disconnect_response(const DisconnectResponse &value) override; |   void on_disconnect_response(const DisconnectResponse &value) override; | ||||||
|   void on_ping_response(const PingResponse &value) override { |   void on_ping_response(const PingResponse &value) override { | ||||||
|     // we initiated ping |     // we initiated ping | ||||||
| @@ -211,9 +213,6 @@ class APIConnection : public APIServerConnection { | |||||||
|   uint32_t last_traffic_; |   uint32_t last_traffic_; | ||||||
|   bool sent_ping_{false}; |   bool sent_ping_{false}; | ||||||
|   bool service_call_subscription_{false}; |   bool service_call_subscription_{false}; | ||||||
| #ifdef USE_BLUETOOTH_PROXY |  | ||||||
|   bool bluetooth_le_advertisement_subscription_{false}; |  | ||||||
| #endif |  | ||||||
| #ifdef USE_VOICE_ASSISTANT | #ifdef USE_VOICE_ASSISTANT | ||||||
|   bool voice_assistant_subscription_{false}; |   bool voice_assistant_subscription_{false}; | ||||||
| #endif | #endif | ||||||
|   | |||||||
| @@ -433,6 +433,57 @@ template<> const char *proto_enum_to_string<enums::VoiceAssistantEvent>(enums::V | |||||||
|   } |   } | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|  | template<> const char *proto_enum_to_string<enums::AlarmControlPanelState>(enums::AlarmControlPanelState value) { | ||||||
|  |   switch (value) { | ||||||
|  |     case enums::ALARM_STATE_DISARMED: | ||||||
|  |       return "ALARM_STATE_DISARMED"; | ||||||
|  |     case enums::ALARM_STATE_ARMED_HOME: | ||||||
|  |       return "ALARM_STATE_ARMED_HOME"; | ||||||
|  |     case enums::ALARM_STATE_ARMED_AWAY: | ||||||
|  |       return "ALARM_STATE_ARMED_AWAY"; | ||||||
|  |     case enums::ALARM_STATE_ARMED_NIGHT: | ||||||
|  |       return "ALARM_STATE_ARMED_NIGHT"; | ||||||
|  |     case enums::ALARM_STATE_ARMED_VACATION: | ||||||
|  |       return "ALARM_STATE_ARMED_VACATION"; | ||||||
|  |     case enums::ALARM_STATE_ARMED_CUSTOM_BYPASS: | ||||||
|  |       return "ALARM_STATE_ARMED_CUSTOM_BYPASS"; | ||||||
|  |     case enums::ALARM_STATE_PENDING: | ||||||
|  |       return "ALARM_STATE_PENDING"; | ||||||
|  |     case enums::ALARM_STATE_ARMING: | ||||||
|  |       return "ALARM_STATE_ARMING"; | ||||||
|  |     case enums::ALARM_STATE_DISARMING: | ||||||
|  |       return "ALARM_STATE_DISARMING"; | ||||||
|  |     case enums::ALARM_STATE_TRIGGERED: | ||||||
|  |       return "ALARM_STATE_TRIGGERED"; | ||||||
|  |     default: | ||||||
|  |       return "UNKNOWN"; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | #endif | ||||||
|  | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|  | template<> | ||||||
|  | const char *proto_enum_to_string<enums::AlarmControlPanelStateCommand>(enums::AlarmControlPanelStateCommand value) { | ||||||
|  |   switch (value) { | ||||||
|  |     case enums::ALARM_CONTROL_PANEL_DISARM: | ||||||
|  |       return "ALARM_CONTROL_PANEL_DISARM"; | ||||||
|  |     case enums::ALARM_CONTROL_PANEL_ARM_AWAY: | ||||||
|  |       return "ALARM_CONTROL_PANEL_ARM_AWAY"; | ||||||
|  |     case enums::ALARM_CONTROL_PANEL_ARM_HOME: | ||||||
|  |       return "ALARM_CONTROL_PANEL_ARM_HOME"; | ||||||
|  |     case enums::ALARM_CONTROL_PANEL_ARM_NIGHT: | ||||||
|  |       return "ALARM_CONTROL_PANEL_ARM_NIGHT"; | ||||||
|  |     case enums::ALARM_CONTROL_PANEL_ARM_VACATION: | ||||||
|  |       return "ALARM_CONTROL_PANEL_ARM_VACATION"; | ||||||
|  |     case enums::ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS: | ||||||
|  |       return "ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS"; | ||||||
|  |     case enums::ALARM_CONTROL_PANEL_TRIGGER: | ||||||
|  |       return "ALARM_CONTROL_PANEL_TRIGGER"; | ||||||
|  |     default: | ||||||
|  |       return "UNKNOWN"; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | #endif | ||||||
| bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | bool HelloRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||||
|   switch (field_id) { |   switch (field_id) { | ||||||
|     case 2: { |     case 2: { | ||||||
| @@ -617,7 +668,11 @@ bool DeviceInfoResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | |||||||
|       return true; |       return true; | ||||||
|     } |     } | ||||||
|     case 11: { |     case 11: { | ||||||
|       this->bluetooth_proxy_version = value.as_uint32(); |       this->legacy_bluetooth_proxy_version = value.as_uint32(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     case 15: { | ||||||
|  |       this->bluetooth_proxy_feature_flags = value.as_uint32(); | ||||||
|       return true; |       return true; | ||||||
|     } |     } | ||||||
|     case 14: { |     case 14: { | ||||||
| @@ -681,7 +736,8 @@ void DeviceInfoResponse::encode(ProtoWriteBuffer buffer) const { | |||||||
|   buffer.encode_string(8, this->project_name); |   buffer.encode_string(8, this->project_name); | ||||||
|   buffer.encode_string(9, this->project_version); |   buffer.encode_string(9, this->project_version); | ||||||
|   buffer.encode_uint32(10, this->webserver_port); |   buffer.encode_uint32(10, this->webserver_port); | ||||||
|   buffer.encode_uint32(11, this->bluetooth_proxy_version); |   buffer.encode_uint32(11, this->legacy_bluetooth_proxy_version); | ||||||
|  |   buffer.encode_uint32(15, this->bluetooth_proxy_feature_flags); | ||||||
|   buffer.encode_string(12, this->manufacturer); |   buffer.encode_string(12, this->manufacturer); | ||||||
|   buffer.encode_string(13, this->friendly_name); |   buffer.encode_string(13, this->friendly_name); | ||||||
|   buffer.encode_uint32(14, this->voice_assistant_version); |   buffer.encode_uint32(14, this->voice_assistant_version); | ||||||
| @@ -731,8 +787,13 @@ void DeviceInfoResponse::dump_to(std::string &out) const { | |||||||
|   out.append(buffer); |   out.append(buffer); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|  |  | ||||||
|   out.append("  bluetooth_proxy_version: "); |   out.append("  legacy_bluetooth_proxy_version: "); | ||||||
|   sprintf(buffer, "%u", this->bluetooth_proxy_version); |   sprintf(buffer, "%u", this->legacy_bluetooth_proxy_version); | ||||||
|  |   out.append(buffer); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  bluetooth_proxy_feature_flags: "); | ||||||
|  |   sprintf(buffer, "%u", this->bluetooth_proxy_feature_flags); | ||||||
|   out.append(buffer); |   out.append(buffer); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|  |  | ||||||
| @@ -5041,10 +5102,28 @@ void MediaPlayerCommandRequest::dump_to(std::string &out) const { | |||||||
|   out.append("}"); |   out.append("}"); | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
| void SubscribeBluetoothLEAdvertisementsRequest::encode(ProtoWriteBuffer buffer) const {} | bool SubscribeBluetoothLEAdvertisementsRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||||
|  |   switch (field_id) { | ||||||
|  |     case 1: { | ||||||
|  |       this->flags = value.as_uint32(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     default: | ||||||
|  |       return false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | void SubscribeBluetoothLEAdvertisementsRequest::encode(ProtoWriteBuffer buffer) const { | ||||||
|  |   buffer.encode_uint32(1, this->flags); | ||||||
|  | } | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
| void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const { | void SubscribeBluetoothLEAdvertisementsRequest::dump_to(std::string &out) const { | ||||||
|   out.append("SubscribeBluetoothLEAdvertisementsRequest {}"); |   __attribute__((unused)) char buffer[64]; | ||||||
|  |   out.append("SubscribeBluetoothLEAdvertisementsRequest {\n"); | ||||||
|  |   out.append("  flags: "); | ||||||
|  |   sprintf(buffer, "%u", this->flags); | ||||||
|  |   out.append(buffer); | ||||||
|  |   out.append("\n"); | ||||||
|  |   out.append("}"); | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
| bool BluetoothServiceData::decode_varint(uint32_t field_id, ProtoVarInt value) { | bool BluetoothServiceData::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||||
| @@ -5197,6 +5276,92 @@ void BluetoothLEAdvertisementResponse::dump_to(std::string &out) const { | |||||||
|   out.append("}"); |   out.append("}"); | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  | bool BluetoothLERawAdvertisement::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||||
|  |   switch (field_id) { | ||||||
|  |     case 1: { | ||||||
|  |       this->address = value.as_uint64(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     case 2: { | ||||||
|  |       this->rssi = value.as_sint32(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     case 3: { | ||||||
|  |       this->address_type = value.as_uint32(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     default: | ||||||
|  |       return false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | bool BluetoothLERawAdvertisement::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||||
|  |   switch (field_id) { | ||||||
|  |     case 4: { | ||||||
|  |       this->data = value.as_string(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     default: | ||||||
|  |       return false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | void BluetoothLERawAdvertisement::encode(ProtoWriteBuffer buffer) const { | ||||||
|  |   buffer.encode_uint64(1, this->address); | ||||||
|  |   buffer.encode_sint32(2, this->rssi); | ||||||
|  |   buffer.encode_uint32(3, this->address_type); | ||||||
|  |   buffer.encode_string(4, this->data); | ||||||
|  | } | ||||||
|  | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|  | void BluetoothLERawAdvertisement::dump_to(std::string &out) const { | ||||||
|  |   __attribute__((unused)) char buffer[64]; | ||||||
|  |   out.append("BluetoothLERawAdvertisement {\n"); | ||||||
|  |   out.append("  address: "); | ||||||
|  |   sprintf(buffer, "%llu", this->address); | ||||||
|  |   out.append(buffer); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  rssi: "); | ||||||
|  |   sprintf(buffer, "%d", this->rssi); | ||||||
|  |   out.append(buffer); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  address_type: "); | ||||||
|  |   sprintf(buffer, "%u", this->address_type); | ||||||
|  |   out.append(buffer); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  data: "); | ||||||
|  |   out.append("'").append(this->data).append("'"); | ||||||
|  |   out.append("\n"); | ||||||
|  |   out.append("}"); | ||||||
|  | } | ||||||
|  | #endif | ||||||
|  | bool BluetoothLERawAdvertisementsResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||||
|  |   switch (field_id) { | ||||||
|  |     case 1: { | ||||||
|  |       this->advertisements.push_back(value.as_message<BluetoothLERawAdvertisement>()); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     default: | ||||||
|  |       return false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | void BluetoothLERawAdvertisementsResponse::encode(ProtoWriteBuffer buffer) const { | ||||||
|  |   for (auto &it : this->advertisements) { | ||||||
|  |     buffer.encode_message<BluetoothLERawAdvertisement>(1, it, true); | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|  | void BluetoothLERawAdvertisementsResponse::dump_to(std::string &out) const { | ||||||
|  |   __attribute__((unused)) char buffer[64]; | ||||||
|  |   out.append("BluetoothLERawAdvertisementsResponse {\n"); | ||||||
|  |   for (const auto &it : this->advertisements) { | ||||||
|  |     out.append("  advertisements: "); | ||||||
|  |     it.dump_to(out); | ||||||
|  |     out.append("\n"); | ||||||
|  |   } | ||||||
|  |   out.append("}"); | ||||||
|  | } | ||||||
|  | #endif | ||||||
| bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | bool BluetoothDeviceRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||||
|   switch (field_id) { |   switch (field_id) { | ||||||
|     case 1: { |     case 1: { | ||||||
| @@ -6187,7 +6352,20 @@ bool VoiceAssistantRequest::decode_varint(uint32_t field_id, ProtoVarInt value) | |||||||
|       return false; |       return false; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { buffer.encode_bool(1, this->start); } | bool VoiceAssistantRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||||
|  |   switch (field_id) { | ||||||
|  |     case 2: { | ||||||
|  |       this->conversation_id = value.as_string(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     default: | ||||||
|  |       return false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | void VoiceAssistantRequest::encode(ProtoWriteBuffer buffer) const { | ||||||
|  |   buffer.encode_bool(1, this->start); | ||||||
|  |   buffer.encode_string(2, this->conversation_id); | ||||||
|  | } | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
| void VoiceAssistantRequest::dump_to(std::string &out) const { | void VoiceAssistantRequest::dump_to(std::string &out) const { | ||||||
|   __attribute__((unused)) char buffer[64]; |   __attribute__((unused)) char buffer[64]; | ||||||
| @@ -6195,6 +6373,10 @@ void VoiceAssistantRequest::dump_to(std::string &out) const { | |||||||
|   out.append("  start: "); |   out.append("  start: "); | ||||||
|   out.append(YESNO(this->start)); |   out.append(YESNO(this->start)); | ||||||
|   out.append("\n"); |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  conversation_id: "); | ||||||
|  |   out.append("'").append(this->conversation_id).append("'"); | ||||||
|  |   out.append("\n"); | ||||||
|   out.append("}"); |   out.append("}"); | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
| @@ -6305,6 +6487,217 @@ void VoiceAssistantEventResponse::dump_to(std::string &out) const { | |||||||
|   out.append("}"); |   out.append("}"); | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  | bool ListEntitiesAlarmControlPanelResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||||
|  |   switch (field_id) { | ||||||
|  |     case 6: { | ||||||
|  |       this->disabled_by_default = value.as_bool(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     case 7: { | ||||||
|  |       this->entity_category = value.as_enum<enums::EntityCategory>(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     case 8: { | ||||||
|  |       this->supported_features = value.as_uint32(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     case 9: { | ||||||
|  |       this->requires_code = value.as_bool(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     case 10: { | ||||||
|  |       this->requires_code_to_arm = value.as_bool(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     default: | ||||||
|  |       return false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | bool ListEntitiesAlarmControlPanelResponse::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||||
|  |   switch (field_id) { | ||||||
|  |     case 1: { | ||||||
|  |       this->object_id = value.as_string(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     case 3: { | ||||||
|  |       this->name = value.as_string(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     case 4: { | ||||||
|  |       this->unique_id = value.as_string(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     case 5: { | ||||||
|  |       this->icon = value.as_string(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     default: | ||||||
|  |       return false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | bool ListEntitiesAlarmControlPanelResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { | ||||||
|  |   switch (field_id) { | ||||||
|  |     case 2: { | ||||||
|  |       this->key = value.as_fixed32(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     default: | ||||||
|  |       return false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | void ListEntitiesAlarmControlPanelResponse::encode(ProtoWriteBuffer buffer) const { | ||||||
|  |   buffer.encode_string(1, this->object_id); | ||||||
|  |   buffer.encode_fixed32(2, this->key); | ||||||
|  |   buffer.encode_string(3, this->name); | ||||||
|  |   buffer.encode_string(4, this->unique_id); | ||||||
|  |   buffer.encode_string(5, this->icon); | ||||||
|  |   buffer.encode_bool(6, this->disabled_by_default); | ||||||
|  |   buffer.encode_enum<enums::EntityCategory>(7, this->entity_category); | ||||||
|  |   buffer.encode_uint32(8, this->supported_features); | ||||||
|  |   buffer.encode_bool(9, this->requires_code); | ||||||
|  |   buffer.encode_bool(10, this->requires_code_to_arm); | ||||||
|  | } | ||||||
|  | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|  | void ListEntitiesAlarmControlPanelResponse::dump_to(std::string &out) const { | ||||||
|  |   __attribute__((unused)) char buffer[64]; | ||||||
|  |   out.append("ListEntitiesAlarmControlPanelResponse {\n"); | ||||||
|  |   out.append("  object_id: "); | ||||||
|  |   out.append("'").append(this->object_id).append("'"); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  key: "); | ||||||
|  |   sprintf(buffer, "%u", this->key); | ||||||
|  |   out.append(buffer); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  name: "); | ||||||
|  |   out.append("'").append(this->name).append("'"); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  unique_id: "); | ||||||
|  |   out.append("'").append(this->unique_id).append("'"); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  icon: "); | ||||||
|  |   out.append("'").append(this->icon).append("'"); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  disabled_by_default: "); | ||||||
|  |   out.append(YESNO(this->disabled_by_default)); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  entity_category: "); | ||||||
|  |   out.append(proto_enum_to_string<enums::EntityCategory>(this->entity_category)); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  supported_features: "); | ||||||
|  |   sprintf(buffer, "%u", this->supported_features); | ||||||
|  |   out.append(buffer); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  requires_code: "); | ||||||
|  |   out.append(YESNO(this->requires_code)); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  requires_code_to_arm: "); | ||||||
|  |   out.append(YESNO(this->requires_code_to_arm)); | ||||||
|  |   out.append("\n"); | ||||||
|  |   out.append("}"); | ||||||
|  | } | ||||||
|  | #endif | ||||||
|  | bool AlarmControlPanelStateResponse::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||||
|  |   switch (field_id) { | ||||||
|  |     case 2: { | ||||||
|  |       this->state = value.as_enum<enums::AlarmControlPanelState>(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     default: | ||||||
|  |       return false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | bool AlarmControlPanelStateResponse::decode_32bit(uint32_t field_id, Proto32Bit value) { | ||||||
|  |   switch (field_id) { | ||||||
|  |     case 1: { | ||||||
|  |       this->key = value.as_fixed32(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     default: | ||||||
|  |       return false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | void AlarmControlPanelStateResponse::encode(ProtoWriteBuffer buffer) const { | ||||||
|  |   buffer.encode_fixed32(1, this->key); | ||||||
|  |   buffer.encode_enum<enums::AlarmControlPanelState>(2, this->state); | ||||||
|  | } | ||||||
|  | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|  | void AlarmControlPanelStateResponse::dump_to(std::string &out) const { | ||||||
|  |   __attribute__((unused)) char buffer[64]; | ||||||
|  |   out.append("AlarmControlPanelStateResponse {\n"); | ||||||
|  |   out.append("  key: "); | ||||||
|  |   sprintf(buffer, "%u", this->key); | ||||||
|  |   out.append(buffer); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  state: "); | ||||||
|  |   out.append(proto_enum_to_string<enums::AlarmControlPanelState>(this->state)); | ||||||
|  |   out.append("\n"); | ||||||
|  |   out.append("}"); | ||||||
|  | } | ||||||
|  | #endif | ||||||
|  | bool AlarmControlPanelCommandRequest::decode_varint(uint32_t field_id, ProtoVarInt value) { | ||||||
|  |   switch (field_id) { | ||||||
|  |     case 2: { | ||||||
|  |       this->command = value.as_enum<enums::AlarmControlPanelStateCommand>(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     default: | ||||||
|  |       return false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | bool AlarmControlPanelCommandRequest::decode_length(uint32_t field_id, ProtoLengthDelimited value) { | ||||||
|  |   switch (field_id) { | ||||||
|  |     case 3: { | ||||||
|  |       this->code = value.as_string(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     default: | ||||||
|  |       return false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | bool AlarmControlPanelCommandRequest::decode_32bit(uint32_t field_id, Proto32Bit value) { | ||||||
|  |   switch (field_id) { | ||||||
|  |     case 1: { | ||||||
|  |       this->key = value.as_fixed32(); | ||||||
|  |       return true; | ||||||
|  |     } | ||||||
|  |     default: | ||||||
|  |       return false; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | void AlarmControlPanelCommandRequest::encode(ProtoWriteBuffer buffer) const { | ||||||
|  |   buffer.encode_fixed32(1, this->key); | ||||||
|  |   buffer.encode_enum<enums::AlarmControlPanelStateCommand>(2, this->command); | ||||||
|  |   buffer.encode_string(3, this->code); | ||||||
|  | } | ||||||
|  | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|  | void AlarmControlPanelCommandRequest::dump_to(std::string &out) const { | ||||||
|  |   __attribute__((unused)) char buffer[64]; | ||||||
|  |   out.append("AlarmControlPanelCommandRequest {\n"); | ||||||
|  |   out.append("  key: "); | ||||||
|  |   sprintf(buffer, "%u", this->key); | ||||||
|  |   out.append(buffer); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  command: "); | ||||||
|  |   out.append(proto_enum_to_string<enums::AlarmControlPanelStateCommand>(this->command)); | ||||||
|  |   out.append("\n"); | ||||||
|  |  | ||||||
|  |   out.append("  code: "); | ||||||
|  |   out.append("'").append(this->code).append("'"); | ||||||
|  |   out.append("\n"); | ||||||
|  |   out.append("}"); | ||||||
|  | } | ||||||
|  | #endif | ||||||
|  |  | ||||||
| }  // namespace api | }  // namespace api | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -176,6 +176,27 @@ enum VoiceAssistantEvent : uint32_t { | |||||||
|   VOICE_ASSISTANT_TTS_START = 7, |   VOICE_ASSISTANT_TTS_START = 7, | ||||||
|   VOICE_ASSISTANT_TTS_END = 8, |   VOICE_ASSISTANT_TTS_END = 8, | ||||||
| }; | }; | ||||||
|  | enum AlarmControlPanelState : uint32_t { | ||||||
|  |   ALARM_STATE_DISARMED = 0, | ||||||
|  |   ALARM_STATE_ARMED_HOME = 1, | ||||||
|  |   ALARM_STATE_ARMED_AWAY = 2, | ||||||
|  |   ALARM_STATE_ARMED_NIGHT = 3, | ||||||
|  |   ALARM_STATE_ARMED_VACATION = 4, | ||||||
|  |   ALARM_STATE_ARMED_CUSTOM_BYPASS = 5, | ||||||
|  |   ALARM_STATE_PENDING = 6, | ||||||
|  |   ALARM_STATE_ARMING = 7, | ||||||
|  |   ALARM_STATE_DISARMING = 8, | ||||||
|  |   ALARM_STATE_TRIGGERED = 9, | ||||||
|  | }; | ||||||
|  | enum AlarmControlPanelStateCommand : uint32_t { | ||||||
|  |   ALARM_CONTROL_PANEL_DISARM = 0, | ||||||
|  |   ALARM_CONTROL_PANEL_ARM_AWAY = 1, | ||||||
|  |   ALARM_CONTROL_PANEL_ARM_HOME = 2, | ||||||
|  |   ALARM_CONTROL_PANEL_ARM_NIGHT = 3, | ||||||
|  |   ALARM_CONTROL_PANEL_ARM_VACATION = 4, | ||||||
|  |   ALARM_CONTROL_PANEL_ARM_CUSTOM_BYPASS = 5, | ||||||
|  |   ALARM_CONTROL_PANEL_TRIGGER = 6, | ||||||
|  | }; | ||||||
|  |  | ||||||
| }  // namespace enums | }  // namespace enums | ||||||
|  |  | ||||||
| @@ -287,7 +308,8 @@ class DeviceInfoResponse : public ProtoMessage { | |||||||
|   std::string project_name{}; |   std::string project_name{}; | ||||||
|   std::string project_version{}; |   std::string project_version{}; | ||||||
|   uint32_t webserver_port{0}; |   uint32_t webserver_port{0}; | ||||||
|   uint32_t bluetooth_proxy_version{0}; |   uint32_t legacy_bluetooth_proxy_version{0}; | ||||||
|  |   uint32_t bluetooth_proxy_feature_flags{0}; | ||||||
|   std::string manufacturer{}; |   std::string manufacturer{}; | ||||||
|   std::string friendly_name{}; |   std::string friendly_name{}; | ||||||
|   uint32_t voice_assistant_version{0}; |   uint32_t voice_assistant_version{0}; | ||||||
| @@ -1247,12 +1269,14 @@ class MediaPlayerCommandRequest : public ProtoMessage { | |||||||
| }; | }; | ||||||
| class SubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { | class SubscribeBluetoothLEAdvertisementsRequest : public ProtoMessage { | ||||||
|  public: |  public: | ||||||
|  |   uint32_t flags{0}; | ||||||
|   void encode(ProtoWriteBuffer buffer) const override; |   void encode(ProtoWriteBuffer buffer) const override; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   void dump_to(std::string &out) const override; |   void dump_to(std::string &out) const override; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|  |   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||||
| }; | }; | ||||||
| class BluetoothServiceData : public ProtoMessage { | class BluetoothServiceData : public ProtoMessage { | ||||||
|  public: |  public: | ||||||
| @@ -1286,6 +1310,32 @@ class BluetoothLEAdvertisementResponse : public ProtoMessage { | |||||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; |   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; |   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||||
| }; | }; | ||||||
|  | class BluetoothLERawAdvertisement : public ProtoMessage { | ||||||
|  |  public: | ||||||
|  |   uint64_t address{0}; | ||||||
|  |   int32_t rssi{0}; | ||||||
|  |   uint32_t address_type{0}; | ||||||
|  |   std::string data{}; | ||||||
|  |   void encode(ProtoWriteBuffer buffer) const override; | ||||||
|  | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|  |   void dump_to(std::string &out) const override; | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||||
|  |   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||||
|  | }; | ||||||
|  | class BluetoothLERawAdvertisementsResponse : public ProtoMessage { | ||||||
|  |  public: | ||||||
|  |   std::vector<BluetoothLERawAdvertisement> advertisements{}; | ||||||
|  |   void encode(ProtoWriteBuffer buffer) const override; | ||||||
|  | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|  |   void dump_to(std::string &out) const override; | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||||
|  | }; | ||||||
| class BluetoothDeviceRequest : public ProtoMessage { | class BluetoothDeviceRequest : public ProtoMessage { | ||||||
|  public: |  public: | ||||||
|   uint64_t address{0}; |   uint64_t address{0}; | ||||||
| @@ -1604,12 +1654,14 @@ class SubscribeVoiceAssistantRequest : public ProtoMessage { | |||||||
| class VoiceAssistantRequest : public ProtoMessage { | class VoiceAssistantRequest : public ProtoMessage { | ||||||
|  public: |  public: | ||||||
|   bool start{false}; |   bool start{false}; | ||||||
|  |   std::string conversation_id{}; | ||||||
|   void encode(ProtoWriteBuffer buffer) const override; |   void encode(ProtoWriteBuffer buffer) const override; | ||||||
| #ifdef HAS_PROTO_MESSAGE_DUMP | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|   void dump_to(std::string &out) const override; |   void dump_to(std::string &out) const override; | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|  |   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; |   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||||
| }; | }; | ||||||
| class VoiceAssistantResponse : public ProtoMessage { | class VoiceAssistantResponse : public ProtoMessage { | ||||||
| @@ -1649,6 +1701,56 @@ class VoiceAssistantEventResponse : public ProtoMessage { | |||||||
|   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; |   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||||
|   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; |   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||||
| }; | }; | ||||||
|  | class ListEntitiesAlarmControlPanelResponse : public ProtoMessage { | ||||||
|  |  public: | ||||||
|  |   std::string object_id{}; | ||||||
|  |   uint32_t key{0}; | ||||||
|  |   std::string name{}; | ||||||
|  |   std::string unique_id{}; | ||||||
|  |   std::string icon{}; | ||||||
|  |   bool disabled_by_default{false}; | ||||||
|  |   enums::EntityCategory entity_category{}; | ||||||
|  |   uint32_t supported_features{0}; | ||||||
|  |   bool requires_code{false}; | ||||||
|  |   bool requires_code_to_arm{false}; | ||||||
|  |   void encode(ProtoWriteBuffer buffer) const override; | ||||||
|  | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|  |   void dump_to(std::string &out) const override; | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||||
|  |   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||||
|  |   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||||
|  | }; | ||||||
|  | class AlarmControlPanelStateResponse : public ProtoMessage { | ||||||
|  |  public: | ||||||
|  |   uint32_t key{0}; | ||||||
|  |   enums::AlarmControlPanelState state{}; | ||||||
|  |   void encode(ProtoWriteBuffer buffer) const override; | ||||||
|  | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|  |   void dump_to(std::string &out) const override; | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||||
|  |   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||||
|  | }; | ||||||
|  | class AlarmControlPanelCommandRequest : public ProtoMessage { | ||||||
|  |  public: | ||||||
|  |   uint32_t key{0}; | ||||||
|  |   enums::AlarmControlPanelStateCommand command{}; | ||||||
|  |   std::string code{}; | ||||||
|  |   void encode(ProtoWriteBuffer buffer) const override; | ||||||
|  | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|  |   void dump_to(std::string &out) const override; | ||||||
|  | #endif | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   bool decode_32bit(uint32_t field_id, Proto32Bit value) override; | ||||||
|  |   bool decode_length(uint32_t field_id, ProtoLengthDelimited value) override; | ||||||
|  |   bool decode_varint(uint32_t field_id, ProtoVarInt value) override; | ||||||
|  | }; | ||||||
|  |  | ||||||
| }  // namespace api | }  // namespace api | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -339,6 +339,15 @@ bool APIServerConnectionBase::send_bluetooth_le_advertisement_response(const Blu | |||||||
| } | } | ||||||
| #endif | #endif | ||||||
| #ifdef USE_BLUETOOTH_PROXY | #ifdef USE_BLUETOOTH_PROXY | ||||||
|  | bool APIServerConnectionBase::send_bluetooth_le_raw_advertisements_response( | ||||||
|  |     const BluetoothLERawAdvertisementsResponse &msg) { | ||||||
|  | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|  |   ESP_LOGVV(TAG, "send_bluetooth_le_raw_advertisements_response: %s", msg.dump().c_str()); | ||||||
|  | #endif | ||||||
|  |   return this->send_message_<BluetoothLERawAdvertisementsResponse>(msg, 93); | ||||||
|  | } | ||||||
|  | #endif | ||||||
|  | #ifdef USE_BLUETOOTH_PROXY | ||||||
| #endif | #endif | ||||||
| #ifdef USE_BLUETOOTH_PROXY | #ifdef USE_BLUETOOTH_PROXY | ||||||
| bool APIServerConnectionBase::send_bluetooth_device_connection_response(const BluetoothDeviceConnectionResponse &msg) { | bool APIServerConnectionBase::send_bluetooth_device_connection_response(const BluetoothDeviceConnectionResponse &msg) { | ||||||
| @@ -467,6 +476,25 @@ bool APIServerConnectionBase::send_voice_assistant_request(const VoiceAssistantR | |||||||
| #endif | #endif | ||||||
| #ifdef USE_VOICE_ASSISTANT | #ifdef USE_VOICE_ASSISTANT | ||||||
| #endif | #endif | ||||||
|  | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|  | bool APIServerConnectionBase::send_list_entities_alarm_control_panel_response( | ||||||
|  |     const ListEntitiesAlarmControlPanelResponse &msg) { | ||||||
|  | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|  |   ESP_LOGVV(TAG, "send_list_entities_alarm_control_panel_response: %s", msg.dump().c_str()); | ||||||
|  | #endif | ||||||
|  |   return this->send_message_<ListEntitiesAlarmControlPanelResponse>(msg, 94); | ||||||
|  | } | ||||||
|  | #endif | ||||||
|  | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|  | bool APIServerConnectionBase::send_alarm_control_panel_state_response(const AlarmControlPanelStateResponse &msg) { | ||||||
|  | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|  |   ESP_LOGVV(TAG, "send_alarm_control_panel_state_response: %s", msg.dump().c_str()); | ||||||
|  | #endif | ||||||
|  |   return this->send_message_<AlarmControlPanelStateResponse>(msg, 95); | ||||||
|  | } | ||||||
|  | #endif | ||||||
|  | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|  | #endif | ||||||
| bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { | bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) { | ||||||
|   switch (msg_type) { |   switch (msg_type) { | ||||||
|     case 1: { |     case 1: { | ||||||
| @@ -874,6 +902,17 @@ bool APIServerConnectionBase::read_message(uint32_t msg_size, uint32_t msg_type, | |||||||
|       ESP_LOGVV(TAG, "on_voice_assistant_event_response: %s", msg.dump().c_str()); |       ESP_LOGVV(TAG, "on_voice_assistant_event_response: %s", msg.dump().c_str()); | ||||||
| #endif | #endif | ||||||
|       this->on_voice_assistant_event_response(msg); |       this->on_voice_assistant_event_response(msg); | ||||||
|  | #endif | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     case 96: { | ||||||
|  | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|  |       AlarmControlPanelCommandRequest msg; | ||||||
|  |       msg.decode(msg_data, msg_size); | ||||||
|  | #ifdef HAS_PROTO_MESSAGE_DUMP | ||||||
|  |       ESP_LOGVV(TAG, "on_alarm_control_panel_command_request: %s", msg.dump().c_str()); | ||||||
|  | #endif | ||||||
|  |       this->on_alarm_control_panel_command_request(msg); | ||||||
| #endif | #endif | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
| @@ -1286,6 +1325,19 @@ void APIServerConnection::on_subscribe_voice_assistant_request(const SubscribeVo | |||||||
|   this->subscribe_voice_assistant(msg); |   this->subscribe_voice_assistant(msg); | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|  | void APIServerConnection::on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) { | ||||||
|  |   if (!this->is_connection_setup()) { | ||||||
|  |     this->on_no_setup_connection(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   if (!this->is_authenticated()) { | ||||||
|  |     this->on_unauthenticated_access(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   this->alarm_control_panel_command(msg); | ||||||
|  | } | ||||||
|  | #endif | ||||||
|  |  | ||||||
| }  // namespace api | }  // namespace api | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -161,6 +161,9 @@ class APIServerConnectionBase : public ProtoService { | |||||||
| #ifdef USE_BLUETOOTH_PROXY | #ifdef USE_BLUETOOTH_PROXY | ||||||
|   bool send_bluetooth_le_advertisement_response(const BluetoothLEAdvertisementResponse &msg); |   bool send_bluetooth_le_advertisement_response(const BluetoothLEAdvertisementResponse &msg); | ||||||
| #endif | #endif | ||||||
|  | #ifdef USE_BLUETOOTH_PROXY | ||||||
|  |   bool send_bluetooth_le_raw_advertisements_response(const BluetoothLERawAdvertisementsResponse &msg); | ||||||
|  | #endif | ||||||
| #ifdef USE_BLUETOOTH_PROXY | #ifdef USE_BLUETOOTH_PROXY | ||||||
|   virtual void on_bluetooth_device_request(const BluetoothDeviceRequest &value){}; |   virtual void on_bluetooth_device_request(const BluetoothDeviceRequest &value){}; | ||||||
| #endif | #endif | ||||||
| @@ -236,6 +239,15 @@ class APIServerConnectionBase : public ProtoService { | |||||||
| #endif | #endif | ||||||
| #ifdef USE_VOICE_ASSISTANT | #ifdef USE_VOICE_ASSISTANT | ||||||
|   virtual void on_voice_assistant_event_response(const VoiceAssistantEventResponse &value){}; |   virtual void on_voice_assistant_event_response(const VoiceAssistantEventResponse &value){}; | ||||||
|  | #endif | ||||||
|  | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|  |   bool send_list_entities_alarm_control_panel_response(const ListEntitiesAlarmControlPanelResponse &msg); | ||||||
|  | #endif | ||||||
|  | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|  |   bool send_alarm_control_panel_state_response(const AlarmControlPanelStateResponse &msg); | ||||||
|  | #endif | ||||||
|  | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|  |   virtual void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &value){}; | ||||||
| #endif | #endif | ||||||
|  protected: |  protected: | ||||||
|   bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; |   bool read_message(uint32_t msg_size, uint32_t msg_type, uint8_t *msg_data) override; | ||||||
| @@ -321,6 +333,9 @@ class APIServerConnection : public APIServerConnectionBase { | |||||||
| #endif | #endif | ||||||
| #ifdef USE_VOICE_ASSISTANT | #ifdef USE_VOICE_ASSISTANT | ||||||
|   virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0; |   virtual void subscribe_voice_assistant(const SubscribeVoiceAssistantRequest &msg) = 0; | ||||||
|  | #endif | ||||||
|  | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|  |   virtual void alarm_control_panel_command(const AlarmControlPanelCommandRequest &msg) = 0; | ||||||
| #endif | #endif | ||||||
|  protected: |  protected: | ||||||
|   void on_hello_request(const HelloRequest &msg) override; |   void on_hello_request(const HelloRequest &msg) override; | ||||||
| @@ -402,6 +417,9 @@ class APIServerConnection : public APIServerConnectionBase { | |||||||
| #ifdef USE_VOICE_ASSISTANT | #ifdef USE_VOICE_ASSISTANT | ||||||
|   void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override; |   void on_subscribe_voice_assistant_request(const SubscribeVoiceAssistantRequest &msg) override; | ||||||
| #endif | #endif | ||||||
|  | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|  |   void on_alarm_control_panel_command_request(const AlarmControlPanelCommandRequest &msg) override; | ||||||
|  | #endif | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace api | }  // namespace api | ||||||
|   | |||||||
| @@ -291,112 +291,7 @@ void APIServer::send_homeassistant_service_call(const HomeassistantServiceRespon | |||||||
|     client->send_homeassistant_service_call(call); |     client->send_homeassistant_service_call(call); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| #ifdef USE_BLUETOOTH_PROXY |  | ||||||
| void APIServer::send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &call) { |  | ||||||
|   for (auto &client : this->clients_) { |  | ||||||
|     client->send_bluetooth_le_advertisement(call); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| void APIServer::send_bluetooth_device_connection(uint64_t address, bool connected, uint16_t mtu, esp_err_t error) { |  | ||||||
|   BluetoothDeviceConnectionResponse call; |  | ||||||
|   call.address = address; |  | ||||||
|   call.connected = connected; |  | ||||||
|   call.mtu = mtu; |  | ||||||
|   call.error = error; |  | ||||||
|  |  | ||||||
|   for (auto &client : this->clients_) { |  | ||||||
|     client->send_bluetooth_device_connection_response(call); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void APIServer::send_bluetooth_device_pairing(uint64_t address, bool paired, esp_err_t error) { |  | ||||||
|   BluetoothDevicePairingResponse call; |  | ||||||
|   call.address = address; |  | ||||||
|   call.paired = paired; |  | ||||||
|   call.error = error; |  | ||||||
|  |  | ||||||
|   for (auto &client : this->clients_) { |  | ||||||
|     client->send_bluetooth_device_pairing_response(call); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void APIServer::send_bluetooth_device_unpairing(uint64_t address, bool success, esp_err_t error) { |  | ||||||
|   BluetoothDeviceUnpairingResponse call; |  | ||||||
|   call.address = address; |  | ||||||
|   call.success = success; |  | ||||||
|   call.error = error; |  | ||||||
|  |  | ||||||
|   for (auto &client : this->clients_) { |  | ||||||
|     client->send_bluetooth_device_unpairing_response(call); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void APIServer::send_bluetooth_device_clear_cache(uint64_t address, bool success, esp_err_t error) { |  | ||||||
|   BluetoothDeviceClearCacheResponse call; |  | ||||||
|   call.address = address; |  | ||||||
|   call.success = success; |  | ||||||
|   call.error = error; |  | ||||||
|  |  | ||||||
|   for (auto &client : this->clients_) { |  | ||||||
|     client->send_bluetooth_device_clear_cache_response(call); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void APIServer::send_bluetooth_connections_free(uint8_t free, uint8_t limit) { |  | ||||||
|   BluetoothConnectionsFreeResponse call; |  | ||||||
|   call.free = free; |  | ||||||
|   call.limit = limit; |  | ||||||
|  |  | ||||||
|   for (auto &client : this->clients_) { |  | ||||||
|     client->send_bluetooth_connections_free_response(call); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void APIServer::send_bluetooth_gatt_read_response(const BluetoothGATTReadResponse &call) { |  | ||||||
|   for (auto &client : this->clients_) { |  | ||||||
|     client->send_bluetooth_gatt_read_response(call); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| void APIServer::send_bluetooth_gatt_write_response(const BluetoothGATTWriteResponse &call) { |  | ||||||
|   for (auto &client : this->clients_) { |  | ||||||
|     client->send_bluetooth_gatt_write_response(call); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| void APIServer::send_bluetooth_gatt_notify_data_response(const BluetoothGATTNotifyDataResponse &call) { |  | ||||||
|   for (auto &client : this->clients_) { |  | ||||||
|     client->send_bluetooth_gatt_notify_data_response(call); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| void APIServer::send_bluetooth_gatt_notify_response(const BluetoothGATTNotifyResponse &call) { |  | ||||||
|   for (auto &client : this->clients_) { |  | ||||||
|     client->send_bluetooth_gatt_notify_response(call); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| void APIServer::send_bluetooth_gatt_services(const BluetoothGATTGetServicesResponse &call) { |  | ||||||
|   for (auto &client : this->clients_) { |  | ||||||
|     client->send_bluetooth_gatt_get_services_response(call); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| void APIServer::send_bluetooth_gatt_services_done(uint64_t address) { |  | ||||||
|   BluetoothGATTGetServicesDoneResponse call; |  | ||||||
|   call.address = address; |  | ||||||
|  |  | ||||||
|   for (auto &client : this->clients_) { |  | ||||||
|     client->send_bluetooth_gatt_get_services_done_response(call); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| void APIServer::send_bluetooth_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) { |  | ||||||
|   BluetoothGATTErrorResponse call; |  | ||||||
|   call.address = address; |  | ||||||
|   call.handle = handle; |  | ||||||
|   call.error = error; |  | ||||||
|  |  | ||||||
|   for (auto &client : this->clients_) { |  | ||||||
|     client->send_bluetooth_gatt_error_response(call); |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| #endif |  | ||||||
| APIServer::APIServer() { global_api_server = this; } | APIServer::APIServer() { global_api_server = this; } | ||||||
| void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute, | void APIServer::subscribe_home_assistant_state(std::string entity_id, optional<std::string> attribute, | ||||||
|                                                std::function<void(std::string)> f) { |                                                std::function<void(std::string)> f) { | ||||||
| @@ -428,20 +323,29 @@ void APIServer::on_shutdown() { | |||||||
| } | } | ||||||
|  |  | ||||||
| #ifdef USE_VOICE_ASSISTANT | #ifdef USE_VOICE_ASSISTANT | ||||||
| bool APIServer::start_voice_assistant() { | bool APIServer::start_voice_assistant(const std::string &conversation_id) { | ||||||
|   for (auto &c : this->clients_) { |   for (auto &c : this->clients_) { | ||||||
|     if (c->request_voice_assistant(true)) |     if (c->request_voice_assistant(true, conversation_id)) | ||||||
|       return true; |       return true; | ||||||
|   } |   } | ||||||
|   return false; |   return false; | ||||||
| } | } | ||||||
| void APIServer::stop_voice_assistant() { | void APIServer::stop_voice_assistant() { | ||||||
|   for (auto &c : this->clients_) { |   for (auto &c : this->clients_) { | ||||||
|     if (c->request_voice_assistant(false)) |     if (c->request_voice_assistant(false, "")) | ||||||
|       return; |       return; | ||||||
|   } |   } | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|  | void APIServer::on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) { | ||||||
|  |   if (obj->is_internal()) | ||||||
|  |     return; | ||||||
|  |   for (auto &c : this->clients_) | ||||||
|  |     c->send_alarm_control_panel_state(obj); | ||||||
|  | } | ||||||
|  | #endif | ||||||
|  |  | ||||||
| }  // namespace api | }  // namespace api | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -75,31 +75,20 @@ class APIServer : public Component, public Controller { | |||||||
|   void on_media_player_update(media_player::MediaPlayer *obj) override; |   void on_media_player_update(media_player::MediaPlayer *obj) override; | ||||||
| #endif | #endif | ||||||
|   void send_homeassistant_service_call(const HomeassistantServiceResponse &call); |   void send_homeassistant_service_call(const HomeassistantServiceResponse &call); | ||||||
| #ifdef USE_BLUETOOTH_PROXY |  | ||||||
|   void send_bluetooth_le_advertisement(const BluetoothLEAdvertisementResponse &call); |  | ||||||
|   void send_bluetooth_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, esp_err_t error = ESP_OK); |  | ||||||
|   void send_bluetooth_device_pairing(uint64_t address, bool paired, esp_err_t error = ESP_OK); |  | ||||||
|   void send_bluetooth_device_unpairing(uint64_t address, bool success, esp_err_t error = ESP_OK); |  | ||||||
|   void send_bluetooth_device_clear_cache(uint64_t address, bool success, esp_err_t error = ESP_OK); |  | ||||||
|   void send_bluetooth_connections_free(uint8_t free, uint8_t limit); |  | ||||||
|   void send_bluetooth_gatt_read_response(const BluetoothGATTReadResponse &call); |  | ||||||
|   void send_bluetooth_gatt_write_response(const BluetoothGATTWriteResponse &call); |  | ||||||
|   void send_bluetooth_gatt_notify_data_response(const BluetoothGATTNotifyDataResponse &call); |  | ||||||
|   void send_bluetooth_gatt_notify_response(const BluetoothGATTNotifyResponse &call); |  | ||||||
|   void send_bluetooth_gatt_services(const BluetoothGATTGetServicesResponse &call); |  | ||||||
|   void send_bluetooth_gatt_services_done(uint64_t address); |  | ||||||
|   void send_bluetooth_gatt_error(uint64_t address, uint16_t handle, esp_err_t error); |  | ||||||
| #endif |  | ||||||
|   void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } |   void register_user_service(UserServiceDescriptor *descriptor) { this->user_services_.push_back(descriptor); } | ||||||
| #ifdef USE_HOMEASSISTANT_TIME | #ifdef USE_HOMEASSISTANT_TIME | ||||||
|   void request_time(); |   void request_time(); | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #ifdef USE_VOICE_ASSISTANT | #ifdef USE_VOICE_ASSISTANT | ||||||
|   bool start_voice_assistant(); |   bool start_voice_assistant(const std::string &conversation_id); | ||||||
|   void stop_voice_assistant(); |   void stop_voice_assistant(); | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|  |   void on_alarm_control_panel_update(alarm_control_panel::AlarmControlPanel *obj) override; | ||||||
|  | #endif | ||||||
|  |  | ||||||
|   bool is_connected() const; |   bool is_connected() const; | ||||||
|  |  | ||||||
|   struct HomeAssistantStateSubscription { |   struct HomeAssistantStateSubscription { | ||||||
|   | |||||||
| @@ -69,6 +69,11 @@ bool ListEntitiesIterator::on_media_player(media_player::MediaPlayer *media_play | |||||||
|   return this->client_->send_media_player_info(media_player); |   return this->client_->send_media_player_info(media_player); | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|  | bool ListEntitiesIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { | ||||||
|  |   return this->client_->send_alarm_control_panel_info(a_alarm_control_panel); | ||||||
|  | } | ||||||
|  | #endif | ||||||
|  |  | ||||||
| }  // namespace api | }  // namespace api | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -54,6 +54,9 @@ class ListEntitiesIterator : public ComponentIterator { | |||||||
| #endif | #endif | ||||||
| #ifdef USE_MEDIA_PLAYER | #ifdef USE_MEDIA_PLAYER | ||||||
|   bool on_media_player(media_player::MediaPlayer *media_player) override; |   bool on_media_player(media_player::MediaPlayer *media_player) override; | ||||||
|  | #endif | ||||||
|  | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|  |   bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; | ||||||
| #endif | #endif | ||||||
|   bool on_end() override; |   bool on_end() override; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -55,6 +55,11 @@ bool InitialStateIterator::on_media_player(media_player::MediaPlayer *media_play | |||||||
|   return this->client_->send_media_player_state(media_player); |   return this->client_->send_media_player_state(media_player); | ||||||
| } | } | ||||||
| #endif | #endif | ||||||
|  | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|  | bool InitialStateIterator::on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) { | ||||||
|  |   return this->client_->send_alarm_control_panel_state(a_alarm_control_panel); | ||||||
|  | } | ||||||
|  | #endif | ||||||
| InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} | InitialStateIterator::InitialStateIterator(APIConnection *client) : client_(client) {} | ||||||
|  |  | ||||||
| }  // namespace api | }  // namespace api | ||||||
|   | |||||||
| @@ -51,6 +51,9 @@ class InitialStateIterator : public ComponentIterator { | |||||||
| #endif | #endif | ||||||
| #ifdef USE_MEDIA_PLAYER | #ifdef USE_MEDIA_PLAYER | ||||||
|   bool on_media_player(media_player::MediaPlayer *media_player) override; |   bool on_media_player(media_player::MediaPlayer *media_player) override; | ||||||
|  | #endif | ||||||
|  | #ifdef USE_ALARM_CONTROL_PANEL | ||||||
|  |   bool on_alarm_control_panel(alarm_control_panel::AlarmControlPanel *a_alarm_control_panel) override; | ||||||
| #endif | #endif | ||||||
|  protected: |  protected: | ||||||
|   APIConnection *client_; |   APIConnection *client_; | ||||||
|   | |||||||
| @@ -442,7 +442,7 @@ uint8_t BedJetHub::write_notify_config_descriptor_(bool enable) { | |||||||
| void BedJetHub::send_local_time() { | void BedJetHub::send_local_time() { | ||||||
|   if (this->time_id_.has_value()) { |   if (this->time_id_.has_value()) { | ||||||
|     auto *time_id = *this->time_id_; |     auto *time_id = *this->time_id_; | ||||||
|     time::ESPTime now = time_id->now(); |     ESPTime now = time_id->now(); | ||||||
|     if (now.is_valid()) { |     if (now.is_valid()) { | ||||||
|       this->set_clock(now.hour, now.minute); |       this->set_clock(now.hour, now.minute); | ||||||
|       ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute); |       ESP_LOGD(TAG, "Using time component to set BedJet clock: %d:%02d", now.hour, now.minute); | ||||||
|   | |||||||
| @@ -13,6 +13,7 @@ | |||||||
|  |  | ||||||
| #ifdef USE_TIME | #ifdef USE_TIME | ||||||
| #include "esphome/components/time/real_time_clock.h" | #include "esphome/components/time/real_time_clock.h" | ||||||
|  | #include "esphome/core/time.h" | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| #include <esp_gattc_api.h> | #include <esp_gattc_api.h> | ||||||
|   | |||||||
| @@ -7,6 +7,7 @@ from esphome.const import ( | |||||||
|     CONF_IBEACON_MAJOR, |     CONF_IBEACON_MAJOR, | ||||||
|     CONF_IBEACON_MINOR, |     CONF_IBEACON_MINOR, | ||||||
|     CONF_IBEACON_UUID, |     CONF_IBEACON_UUID, | ||||||
|  |     CONF_MIN_RSSI, | ||||||
| ) | ) | ||||||
|  |  | ||||||
| DEPENDENCIES = ["esp32_ble_tracker"] | DEPENDENCIES = ["esp32_ble_tracker"] | ||||||
| @@ -37,6 +38,9 @@ CONFIG_SCHEMA = cv.All( | |||||||
|             cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t, |             cv.Optional(CONF_IBEACON_MAJOR): cv.uint16_t, | ||||||
|             cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t, |             cv.Optional(CONF_IBEACON_MINOR): cv.uint16_t, | ||||||
|             cv.Optional(CONF_IBEACON_UUID): cv.uuid, |             cv.Optional(CONF_IBEACON_UUID): cv.uuid, | ||||||
|  |             cv.Optional(CONF_MIN_RSSI): cv.All( | ||||||
|  |                 cv.decibel, cv.int_range(min=-90, max=-30) | ||||||
|  |             ), | ||||||
|         } |         } | ||||||
|     ) |     ) | ||||||
|     .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) |     .extend(esp32_ble_tracker.ESP_BLE_DEVICE_SCHEMA) | ||||||
| @@ -51,6 +55,9 @@ async def to_code(config): | |||||||
|     await cg.register_component(var, config) |     await cg.register_component(var, config) | ||||||
|     await esp32_ble_tracker.register_ble_device(var, config) |     await esp32_ble_tracker.register_ble_device(var, config) | ||||||
|  |  | ||||||
|  |     if CONF_MIN_RSSI in config: | ||||||
|  |         cg.add(var.set_minimum_rssi(config[CONF_MIN_RSSI])) | ||||||
|  |  | ||||||
|     if CONF_MAC_ADDRESS in config: |     if CONF_MAC_ADDRESS in config: | ||||||
|         cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) |         cg.add(var.set_address(config[CONF_MAC_ADDRESS].as_hex)) | ||||||
|  |  | ||||||
|   | |||||||
| @@ -41,12 +41,19 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, | |||||||
|     this->check_ibeacon_minor_ = true; |     this->check_ibeacon_minor_ = true; | ||||||
|     this->ibeacon_minor_ = minor; |     this->ibeacon_minor_ = minor; | ||||||
|   } |   } | ||||||
|  |   void set_minimum_rssi(int rssi) { | ||||||
|  |     this->check_minimum_rssi_ = true; | ||||||
|  |     this->minimum_rssi_ = rssi; | ||||||
|  |   } | ||||||
|   void on_scan_end() override { |   void on_scan_end() override { | ||||||
|     if (!this->found_) |     if (!this->found_) | ||||||
|       this->publish_state(false); |       this->publish_state(false); | ||||||
|     this->found_ = false; |     this->found_ = false; | ||||||
|   } |   } | ||||||
|   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { |   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override { | ||||||
|  |     if (this->check_minimum_rssi_ && this->minimum_rssi_ <= device.get_rssi()) { | ||||||
|  |       return false; | ||||||
|  |     } | ||||||
|     switch (this->match_by_) { |     switch (this->match_by_) { | ||||||
|       case MATCH_BY_MAC_ADDRESS: |       case MATCH_BY_MAC_ADDRESS: | ||||||
|         if (device.address_uint64() == this->address_) { |         if (device.address_uint64() == this->address_) { | ||||||
| @@ -96,17 +103,21 @@ class BLEPresenceDevice : public binary_sensor::BinarySensorInitiallyOff, | |||||||
|   enum MatchType { MATCH_BY_MAC_ADDRESS, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID }; |   enum MatchType { MATCH_BY_MAC_ADDRESS, MATCH_BY_SERVICE_UUID, MATCH_BY_IBEACON_UUID }; | ||||||
|   MatchType match_by_; |   MatchType match_by_; | ||||||
|  |  | ||||||
|   bool found_{false}; |  | ||||||
|  |  | ||||||
|   uint64_t address_; |   uint64_t address_; | ||||||
|  |  | ||||||
|   esp32_ble_tracker::ESPBTUUID uuid_; |   esp32_ble_tracker::ESPBTUUID uuid_; | ||||||
|  |  | ||||||
|   esp32_ble_tracker::ESPBTUUID ibeacon_uuid_; |   esp32_ble_tracker::ESPBTUUID ibeacon_uuid_; | ||||||
|   uint16_t ibeacon_major_; |   uint16_t ibeacon_major_{0}; | ||||||
|   bool check_ibeacon_major_; |   uint16_t ibeacon_minor_{0}; | ||||||
|   uint16_t ibeacon_minor_; |  | ||||||
|   bool check_ibeacon_minor_; |   int minimum_rssi_{0}; | ||||||
|  |  | ||||||
|  |   bool check_ibeacon_major_{false}; | ||||||
|  |   bool check_ibeacon_minor_{false}; | ||||||
|  |   bool check_minimum_rssi_{false}; | ||||||
|  |  | ||||||
|  |   bool found_{false}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| }  // namespace ble_presence | }  // namespace ble_presence | ||||||
|   | |||||||
| @@ -102,8 +102,9 @@ class BLERSSISensor : public sensor::Sensor, public esp32_ble_tracker::ESPBTDevi | |||||||
|  |  | ||||||
|   esp32_ble_tracker::ESPBTUUID ibeacon_uuid_; |   esp32_ble_tracker::ESPBTUUID ibeacon_uuid_; | ||||||
|   uint16_t ibeacon_major_; |   uint16_t ibeacon_major_; | ||||||
|   bool check_ibeacon_major_; |  | ||||||
|   uint16_t ibeacon_minor_; |   uint16_t ibeacon_minor_; | ||||||
|  |  | ||||||
|  |   bool check_ibeacon_major_; | ||||||
|   bool check_ibeacon_minor_; |   bool check_ibeacon_minor_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,6 +1,6 @@ | |||||||
| #include "bluetooth_connection.h" | #include "bluetooth_connection.h" | ||||||
|  |  | ||||||
| #include "esphome/components/api/api_server.h" | #include "esphome/components/api/api_pb2.h" | ||||||
| #include "esphome/core/helpers.h" | #include "esphome/core/helpers.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
| @@ -20,24 +20,21 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | |||||||
|  |  | ||||||
|   switch (event) { |   switch (event) { | ||||||
|     case ESP_GATTC_DISCONNECT_EVT: { |     case ESP_GATTC_DISCONNECT_EVT: { | ||||||
|       api::global_api_server->send_bluetooth_device_connection(this->address_, false, 0, param->disconnect.reason); |       this->proxy_->send_device_connection(this->address_, false, 0, param->disconnect.reason); | ||||||
|       this->set_address(0); |       this->set_address(0); | ||||||
|       api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(), |       this->proxy_->send_connections_free(); | ||||||
|                                                               this->proxy_->get_bluetooth_connections_limit()); |  | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     case ESP_GATTC_OPEN_EVT: { |     case ESP_GATTC_OPEN_EVT: { | ||||||
|       if (param->open.conn_id != this->conn_id_) |       if (param->open.conn_id != this->conn_id_) | ||||||
|         break; |         break; | ||||||
|       if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { |       if (param->open.status != ESP_GATT_OK && param->open.status != ESP_GATT_ALREADY_OPEN) { | ||||||
|         api::global_api_server->send_bluetooth_device_connection(this->address_, false, 0, param->open.status); |         this->proxy_->send_device_connection(this->address_, false, 0, param->open.status); | ||||||
|         this->set_address(0); |         this->set_address(0); | ||||||
|         api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(), |         this->proxy_->send_connections_free(); | ||||||
|                                                                 this->proxy_->get_bluetooth_connections_limit()); |  | ||||||
|       } else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { |       } else if (this->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE) { | ||||||
|         api::global_api_server->send_bluetooth_device_connection(this->address_, true, this->mtu_); |         this->proxy_->send_device_connection(this->address_, true, this->mtu_); | ||||||
|         api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(), |         this->proxy_->send_connections_free(); | ||||||
|                                                                 this->proxy_->get_bluetooth_connections_limit()); |  | ||||||
|       } |       } | ||||||
|       this->seen_mtu_or_services_ = false; |       this->seen_mtu_or_services_ = false; | ||||||
|       break; |       break; | ||||||
| @@ -52,9 +49,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | |||||||
|         this->seen_mtu_or_services_ = true; |         this->seen_mtu_or_services_ = true; | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|       api::global_api_server->send_bluetooth_device_connection(this->address_, true, this->mtu_); |       this->proxy_->send_device_connection(this->address_, true, this->mtu_); | ||||||
|       api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(), |       this->proxy_->send_connections_free(); | ||||||
|                                                               this->proxy_->get_bluetooth_connections_limit()); |  | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     case ESP_GATTC_SEARCH_CMPL_EVT: { |     case ESP_GATTC_SEARCH_CMPL_EVT: { | ||||||
| @@ -67,9 +63,8 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | |||||||
|         this->seen_mtu_or_services_ = true; |         this->seen_mtu_or_services_ = true; | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|       api::global_api_server->send_bluetooth_device_connection(this->address_, true, this->mtu_); |       this->proxy_->send_device_connection(this->address_, true, this->mtu_); | ||||||
|       api::global_api_server->send_bluetooth_connections_free(this->proxy_->get_bluetooth_connections_free(), |       this->proxy_->send_connections_free(); | ||||||
|                                                               this->proxy_->get_bluetooth_connections_limit()); |  | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     case ESP_GATTC_READ_DESCR_EVT: |     case ESP_GATTC_READ_DESCR_EVT: | ||||||
| @@ -79,7 +74,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | |||||||
|       if (param->read.status != ESP_GATT_OK) { |       if (param->read.status != ESP_GATT_OK) { | ||||||
|         ESP_LOGW(TAG, "[%d] [%s] Error reading char/descriptor at handle 0x%2X, status=%d", this->connection_index_, |         ESP_LOGW(TAG, "[%d] [%s] Error reading char/descriptor at handle 0x%2X, status=%d", this->connection_index_, | ||||||
|                  this->address_str_.c_str(), param->read.handle, param->read.status); |                  this->address_str_.c_str(), param->read.handle, param->read.status); | ||||||
|         api::global_api_server->send_bluetooth_gatt_error(this->address_, param->read.handle, param->read.status); |         this->proxy_->send_gatt_error(this->address_, param->read.handle, param->read.status); | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|       api::BluetoothGATTReadResponse resp; |       api::BluetoothGATTReadResponse resp; | ||||||
| @@ -89,7 +84,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | |||||||
|       for (uint16_t i = 0; i < param->read.value_len; i++) { |       for (uint16_t i = 0; i < param->read.value_len; i++) { | ||||||
|         resp.data.push_back(param->read.value[i]); |         resp.data.push_back(param->read.value[i]); | ||||||
|       } |       } | ||||||
|       api::global_api_server->send_bluetooth_gatt_read_response(resp); |       this->proxy_->get_api_connection()->send_bluetooth_gatt_read_response(resp); | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     case ESP_GATTC_WRITE_CHAR_EVT: |     case ESP_GATTC_WRITE_CHAR_EVT: | ||||||
| @@ -99,13 +94,13 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | |||||||
|       if (param->write.status != ESP_GATT_OK) { |       if (param->write.status != ESP_GATT_OK) { | ||||||
|         ESP_LOGW(TAG, "[%d] [%s] Error writing char/descriptor at handle 0x%2X, status=%d", this->connection_index_, |         ESP_LOGW(TAG, "[%d] [%s] Error writing char/descriptor at handle 0x%2X, status=%d", this->connection_index_, | ||||||
|                  this->address_str_.c_str(), param->write.handle, param->write.status); |                  this->address_str_.c_str(), param->write.handle, param->write.status); | ||||||
|         api::global_api_server->send_bluetooth_gatt_error(this->address_, param->write.handle, param->write.status); |         this->proxy_->send_gatt_error(this->address_, param->write.handle, param->write.status); | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|       api::BluetoothGATTWriteResponse resp; |       api::BluetoothGATTWriteResponse resp; | ||||||
|       resp.address = this->address_; |       resp.address = this->address_; | ||||||
|       resp.handle = param->write.handle; |       resp.handle = param->write.handle; | ||||||
|       api::global_api_server->send_bluetooth_gatt_write_response(resp); |       this->proxy_->get_api_connection()->send_bluetooth_gatt_write_response(resp); | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { |     case ESP_GATTC_UNREG_FOR_NOTIFY_EVT: { | ||||||
| @@ -113,28 +108,26 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | |||||||
|         ESP_LOGW(TAG, "[%d] [%s] Error unregistering notifications for handle 0x%2X, status=%d", |         ESP_LOGW(TAG, "[%d] [%s] Error unregistering notifications for handle 0x%2X, status=%d", | ||||||
|                  this->connection_index_, this->address_str_.c_str(), param->unreg_for_notify.handle, |                  this->connection_index_, this->address_str_.c_str(), param->unreg_for_notify.handle, | ||||||
|                  param->unreg_for_notify.status); |                  param->unreg_for_notify.status); | ||||||
|         api::global_api_server->send_bluetooth_gatt_error(this->address_, param->unreg_for_notify.handle, |         this->proxy_->send_gatt_error(this->address_, param->unreg_for_notify.handle, param->unreg_for_notify.status); | ||||||
|                                                           param->unreg_for_notify.status); |  | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|       api::BluetoothGATTNotifyResponse resp; |       api::BluetoothGATTNotifyResponse resp; | ||||||
|       resp.address = this->address_; |       resp.address = this->address_; | ||||||
|       resp.handle = param->unreg_for_notify.handle; |       resp.handle = param->unreg_for_notify.handle; | ||||||
|       api::global_api_server->send_bluetooth_gatt_notify_response(resp); |       this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_response(resp); | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     case ESP_GATTC_REG_FOR_NOTIFY_EVT: { |     case ESP_GATTC_REG_FOR_NOTIFY_EVT: { | ||||||
|       if (param->reg_for_notify.status != ESP_GATT_OK) { |       if (param->reg_for_notify.status != ESP_GATT_OK) { | ||||||
|         ESP_LOGW(TAG, "[%d] [%s] Error registering notifications for handle 0x%2X, status=%d", this->connection_index_, |         ESP_LOGW(TAG, "[%d] [%s] Error registering notifications for handle 0x%2X, status=%d", this->connection_index_, | ||||||
|                  this->address_str_.c_str(), param->reg_for_notify.handle, param->reg_for_notify.status); |                  this->address_str_.c_str(), param->reg_for_notify.handle, param->reg_for_notify.status); | ||||||
|         api::global_api_server->send_bluetooth_gatt_error(this->address_, param->reg_for_notify.handle, |         this->proxy_->send_gatt_error(this->address_, param->reg_for_notify.handle, param->reg_for_notify.status); | ||||||
|                                                           param->reg_for_notify.status); |  | ||||||
|         break; |         break; | ||||||
|       } |       } | ||||||
|       api::BluetoothGATTNotifyResponse resp; |       api::BluetoothGATTNotifyResponse resp; | ||||||
|       resp.address = this->address_; |       resp.address = this->address_; | ||||||
|       resp.handle = param->reg_for_notify.handle; |       resp.handle = param->reg_for_notify.handle; | ||||||
|       api::global_api_server->send_bluetooth_gatt_notify_response(resp); |       this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_response(resp); | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     case ESP_GATTC_NOTIFY_EVT: { |     case ESP_GATTC_NOTIFY_EVT: { | ||||||
| @@ -149,7 +142,7 @@ bool BluetoothConnection::gattc_event_handler(esp_gattc_cb_event_t event, esp_ga | |||||||
|       for (uint16_t i = 0; i < param->notify.value_len; i++) { |       for (uint16_t i = 0; i < param->notify.value_len; i++) { | ||||||
|         resp.data.push_back(param->notify.value[i]); |         resp.data.push_back(param->notify.value[i]); | ||||||
|       } |       } | ||||||
|       api::global_api_server->send_bluetooth_gatt_notify_data_response(resp); |       this->proxy_->get_api_connection()->send_bluetooth_gatt_notify_data_response(resp); | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     default: |     default: | ||||||
| @@ -166,10 +159,9 @@ void BluetoothConnection::gap_event_handler(esp_gap_ble_cb_event_t event, esp_bl | |||||||
|       if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0) |       if (memcmp(param->ble_security.auth_cmpl.bd_addr, this->remote_bda_, 6) != 0) | ||||||
|         break; |         break; | ||||||
|       if (param->ble_security.auth_cmpl.success) { |       if (param->ble_security.auth_cmpl.success) { | ||||||
|         api::global_api_server->send_bluetooth_device_pairing(this->address_, true); |         this->proxy_->send_device_pairing(this->address_, true); | ||||||
|       } else { |       } else { | ||||||
|         api::global_api_server->send_bluetooth_device_pairing(this->address_, false, |         this->proxy_->send_device_pairing(this->address_, false, param->ble_security.auth_cmpl.fail_reason); | ||||||
|                                                               param->ble_security.auth_cmpl.fail_reason); |  | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
|     default: |     default: | ||||||
|   | |||||||
| @@ -1,11 +1,10 @@ | |||||||
| #include "bluetooth_proxy.h" | #include "bluetooth_proxy.h" | ||||||
|  |  | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
|  | #include "esphome/core/macros.h" | ||||||
|  |  | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
|  |  | ||||||
| #include "esphome/components/api/api_server.h" |  | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace bluetooth_proxy { | namespace bluetooth_proxy { | ||||||
|  |  | ||||||
| @@ -27,15 +26,39 @@ std::vector<uint64_t> get_128bit_uuid_vec(esp_bt_uuid_t uuid_source) { | |||||||
| BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; } | BluetoothProxy::BluetoothProxy() { global_bluetooth_proxy = this; } | ||||||
|  |  | ||||||
| bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { | bool BluetoothProxy::parse_device(const esp32_ble_tracker::ESPBTDevice &device) { | ||||||
|   if (!api::global_api_server->is_connected()) |   if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || this->raw_advertisements_) | ||||||
|     return false; |     return false; | ||||||
|  |  | ||||||
|   ESP_LOGV(TAG, "Proxying packet from %s - %s. RSSI: %d dB", device.get_name().c_str(), device.address_str().c_str(), |   ESP_LOGV(TAG, "Proxying packet from %s - %s. RSSI: %d dB", device.get_name().c_str(), device.address_str().c_str(), | ||||||
|            device.get_rssi()); |            device.get_rssi()); | ||||||
|   this->send_api_packet_(device); |   this->send_api_packet_(device); | ||||||
|  |  | ||||||
|   return true; |   return true; | ||||||
| } | } | ||||||
|  |  | ||||||
|  | bool BluetoothProxy::parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { | ||||||
|  |   if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr || !this->raw_advertisements_) | ||||||
|  |     return false; | ||||||
|  |  | ||||||
|  |   api::BluetoothLERawAdvertisementsResponse resp; | ||||||
|  |   for (size_t i = 0; i < count; i++) { | ||||||
|  |     auto &result = advertisements[i]; | ||||||
|  |     api::BluetoothLERawAdvertisement adv; | ||||||
|  |     adv.address = esp32_ble::ble_addr_to_uint64(result.bda); | ||||||
|  |     adv.rssi = result.rssi; | ||||||
|  |     adv.address_type = result.ble_addr_type; | ||||||
|  |  | ||||||
|  |     uint8_t length = result.adv_data_len + result.scan_rsp_len; | ||||||
|  |     adv.data.reserve(length); | ||||||
|  |     for (uint16_t i = 0; i < length; i++) { | ||||||
|  |       adv.data.push_back(result.ble_adv[i]); | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     resp.advertisements.push_back(std::move(adv)); | ||||||
|  |   } | ||||||
|  |   ESP_LOGV(TAG, "Proxying %d packets", count); | ||||||
|  |   this->api_connection_->send_bluetooth_le_raw_advertisements_response(resp); | ||||||
|  |   return true; | ||||||
|  | } | ||||||
| void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { | void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device) { | ||||||
|   api::BluetoothLEAdvertisementResponse resp; |   api::BluetoothLEAdvertisementResponse resp; | ||||||
|   resp.address = device.address_uint64(); |   resp.address = device.address_uint64(); | ||||||
| @@ -58,7 +81,7 @@ void BluetoothProxy::send_api_packet_(const esp32_ble_tracker::ESPBTDevice &devi | |||||||
|     manufacturer_data.data.assign(data.data.begin(), data.data.end()); |     manufacturer_data.data.assign(data.data.begin(), data.data.end()); | ||||||
|     resp.manufacturer_data.push_back(std::move(manufacturer_data)); |     resp.manufacturer_data.push_back(std::move(manufacturer_data)); | ||||||
|   } |   } | ||||||
|   api::global_api_server->send_bluetooth_le_advertisement(resp); |   this->api_connection_->send_bluetooth_le_advertisement(resp); | ||||||
| } | } | ||||||
|  |  | ||||||
| void BluetoothProxy::dump_config() { | void BluetoothProxy::dump_config() { | ||||||
| @@ -81,7 +104,7 @@ int BluetoothProxy::get_bluetooth_connections_free() { | |||||||
| } | } | ||||||
|  |  | ||||||
| void BluetoothProxy::loop() { | void BluetoothProxy::loop() { | ||||||
|   if (!api::global_api_server->is_connected()) { |   if (!api::global_api_server->is_connected() || this->api_connection_ == nullptr) { | ||||||
|     for (auto *connection : this->connections_) { |     for (auto *connection : this->connections_) { | ||||||
|       if (connection->get_address() != 0) { |       if (connection->get_address() != 0) { | ||||||
|         connection->disconnect(); |         connection->disconnect(); | ||||||
| @@ -92,7 +115,7 @@ void BluetoothProxy::loop() { | |||||||
|   for (auto *connection : this->connections_) { |   for (auto *connection : this->connections_) { | ||||||
|     if (connection->send_service_ == connection->service_count_) { |     if (connection->send_service_ == connection->service_count_) { | ||||||
|       connection->send_service_ = DONE_SENDING_SERVICES; |       connection->send_service_ = DONE_SENDING_SERVICES; | ||||||
|       api::global_api_server->send_bluetooth_gatt_services_done(connection->get_address()); |       this->send_gatt_services_done(connection->get_address()); | ||||||
|       if (connection->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || |       if (connection->connection_type_ == espbt::ConnectionType::V3_WITH_CACHE || | ||||||
|           connection->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { |           connection->connection_type_ == espbt::ConnectionType::V3_WITHOUT_CACHE) { | ||||||
|         connection->release_services(); |         connection->release_services(); | ||||||
| @@ -170,7 +193,7 @@ void BluetoothProxy::loop() { | |||||||
|         service_resp.characteristics.push_back(std::move(characteristic_resp)); |         service_resp.characteristics.push_back(std::move(characteristic_resp)); | ||||||
|       } |       } | ||||||
|       resp.services.push_back(std::move(service_resp)); |       resp.services.push_back(std::move(service_resp)); | ||||||
|       api::global_api_server->send_bluetooth_gatt_services(resp); |       this->api_connection_->send_bluetooth_gatt_get_services_response(resp); | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| } | } | ||||||
| @@ -208,16 +231,15 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest | |||||||
|       auto *connection = this->get_connection_(msg.address, true); |       auto *connection = this->get_connection_(msg.address, true); | ||||||
|       if (connection == nullptr) { |       if (connection == nullptr) { | ||||||
|         ESP_LOGW(TAG, "No free connections available"); |         ESP_LOGW(TAG, "No free connections available"); | ||||||
|         api::global_api_server->send_bluetooth_device_connection(msg.address, false); |         this->send_device_connection(msg.address, false); | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|       if (connection->state() == espbt::ClientState::CONNECTED || |       if (connection->state() == espbt::ClientState::CONNECTED || | ||||||
|           connection->state() == espbt::ClientState::ESTABLISHED) { |           connection->state() == espbt::ClientState::ESTABLISHED) { | ||||||
|         ESP_LOGW(TAG, "[%d] [%s] Connection already established", connection->get_connection_index(), |         ESP_LOGW(TAG, "[%d] [%s] Connection already established", connection->get_connection_index(), | ||||||
|                  connection->address_str().c_str()); |                  connection->address_str().c_str()); | ||||||
|         api::global_api_server->send_bluetooth_device_connection(msg.address, true); |         this->send_device_connection(msg.address, true); | ||||||
|         api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), |         this->send_connections_free(); | ||||||
|                                                                 this->get_bluetooth_connections_limit()); |  | ||||||
|         return; |         return; | ||||||
|       } else if (connection->state() == espbt::ClientState::SEARCHING) { |       } else if (connection->state() == espbt::ClientState::SEARCHING) { | ||||||
|         ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already searching for device", |         ESP_LOGW(TAG, "[%d] [%s] Connection request ignored, already searching for device", | ||||||
| @@ -263,25 +285,22 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest | |||||||
|       } else { |       } else { | ||||||
|         connection->set_state(espbt::ClientState::SEARCHING); |         connection->set_state(espbt::ClientState::SEARCHING); | ||||||
|       } |       } | ||||||
|       api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), |       this->send_connections_free(); | ||||||
|                                                               this->get_bluetooth_connections_limit()); |  | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT: { |     case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_DISCONNECT: { | ||||||
|       auto *connection = this->get_connection_(msg.address, false); |       auto *connection = this->get_connection_(msg.address, false); | ||||||
|       if (connection == nullptr) { |       if (connection == nullptr) { | ||||||
|         api::global_api_server->send_bluetooth_device_connection(msg.address, false); |         this->send_device_connection(msg.address, false); | ||||||
|         api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), |         this->send_connections_free(); | ||||||
|                                                                 this->get_bluetooth_connections_limit()); |  | ||||||
|         return; |         return; | ||||||
|       } |       } | ||||||
|       if (connection->state() != espbt::ClientState::IDLE) { |       if (connection->state() != espbt::ClientState::IDLE) { | ||||||
|         connection->disconnect(); |         connection->disconnect(); | ||||||
|       } else { |       } else { | ||||||
|         connection->set_address(0); |         connection->set_address(0); | ||||||
|         api::global_api_server->send_bluetooth_device_connection(msg.address, false); |         this->send_device_connection(msg.address, false); | ||||||
|         api::global_api_server->send_bluetooth_connections_free(this->get_bluetooth_connections_free(), |         this->send_connections_free(); | ||||||
|                                                                 this->get_bluetooth_connections_limit()); |  | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
| @@ -291,10 +310,10 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest | |||||||
|         if (!connection->is_paired()) { |         if (!connection->is_paired()) { | ||||||
|           auto err = connection->pair(); |           auto err = connection->pair(); | ||||||
|           if (err != ESP_OK) { |           if (err != ESP_OK) { | ||||||
|             api::global_api_server->send_bluetooth_device_pairing(msg.address, false, err); |             this->send_device_pairing(msg.address, false, err); | ||||||
|           } |           } | ||||||
|         } else { |         } else { | ||||||
|           api::global_api_server->send_bluetooth_device_pairing(msg.address, true); |           this->send_device_pairing(msg.address, true); | ||||||
|         } |         } | ||||||
|       } |       } | ||||||
|       break; |       break; | ||||||
| @@ -303,14 +322,20 @@ void BluetoothProxy::bluetooth_device_request(const api::BluetoothDeviceRequest | |||||||
|       esp_bd_addr_t address; |       esp_bd_addr_t address; | ||||||
|       uint64_to_bd_addr(msg.address, address); |       uint64_to_bd_addr(msg.address, address); | ||||||
|       esp_err_t ret = esp_ble_remove_bond_device(address); |       esp_err_t ret = esp_ble_remove_bond_device(address); | ||||||
|       api::global_api_server->send_bluetooth_device_unpairing(msg.address, ret == ESP_OK, ret); |       this->send_device_pairing(msg.address, ret == ESP_OK, ret); | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|     case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE: { |     case api::enums::BLUETOOTH_DEVICE_REQUEST_TYPE_CLEAR_CACHE: { | ||||||
|       esp_bd_addr_t address; |       esp_bd_addr_t address; | ||||||
|       uint64_to_bd_addr(msg.address, address); |       uint64_to_bd_addr(msg.address, address); | ||||||
|       esp_err_t ret = esp_ble_gattc_cache_clean(address); |       esp_err_t ret = esp_ble_gattc_cache_clean(address); | ||||||
|       api::global_api_server->send_bluetooth_device_clear_cache(msg.address, ret == ESP_OK, ret); |       api::BluetoothDeviceClearCacheResponse call; | ||||||
|  |       call.address = msg.address; | ||||||
|  |       call.success = ret == ESP_OK; | ||||||
|  |       call.error = ret; | ||||||
|  |  | ||||||
|  |       this->api_connection_->send_bluetooth_device_clear_cache_response(call); | ||||||
|  |  | ||||||
|       break; |       break; | ||||||
|     } |     } | ||||||
|   } |   } | ||||||
| @@ -320,13 +345,13 @@ void BluetoothProxy::bluetooth_gatt_read(const api::BluetoothGATTReadRequest &ms | |||||||
|   auto *connection = this->get_connection_(msg.address, false); |   auto *connection = this->get_connection_(msg.address, false); | ||||||
|   if (connection == nullptr) { |   if (connection == nullptr) { | ||||||
|     ESP_LOGW(TAG, "Cannot read GATT characteristic, not connected"); |     ESP_LOGW(TAG, "Cannot read GATT characteristic, not connected"); | ||||||
|     api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); |     this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   auto err = connection->read_characteristic(msg.handle); |   auto err = connection->read_characteristic(msg.handle); | ||||||
|   if (err != ESP_OK) { |   if (err != ESP_OK) { | ||||||
|     api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); |     this->send_gatt_error(msg.address, msg.handle, err); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -334,13 +359,13 @@ void BluetoothProxy::bluetooth_gatt_write(const api::BluetoothGATTWriteRequest & | |||||||
|   auto *connection = this->get_connection_(msg.address, false); |   auto *connection = this->get_connection_(msg.address, false); | ||||||
|   if (connection == nullptr) { |   if (connection == nullptr) { | ||||||
|     ESP_LOGW(TAG, "Cannot write GATT characteristic, not connected"); |     ESP_LOGW(TAG, "Cannot write GATT characteristic, not connected"); | ||||||
|     api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); |     this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   auto err = connection->write_characteristic(msg.handle, msg.data, msg.response); |   auto err = connection->write_characteristic(msg.handle, msg.data, msg.response); | ||||||
|   if (err != ESP_OK) { |   if (err != ESP_OK) { | ||||||
|     api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); |     this->send_gatt_error(msg.address, msg.handle, err); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -348,13 +373,13 @@ void BluetoothProxy::bluetooth_gatt_read_descriptor(const api::BluetoothGATTRead | |||||||
|   auto *connection = this->get_connection_(msg.address, false); |   auto *connection = this->get_connection_(msg.address, false); | ||||||
|   if (connection == nullptr) { |   if (connection == nullptr) { | ||||||
|     ESP_LOGW(TAG, "Cannot read GATT descriptor, not connected"); |     ESP_LOGW(TAG, "Cannot read GATT descriptor, not connected"); | ||||||
|     api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); |     this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   auto err = connection->read_descriptor(msg.handle); |   auto err = connection->read_descriptor(msg.handle); | ||||||
|   if (err != ESP_OK) { |   if (err != ESP_OK) { | ||||||
|     api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); |     this->send_gatt_error(msg.address, msg.handle, err); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -362,13 +387,13 @@ void BluetoothProxy::bluetooth_gatt_write_descriptor(const api::BluetoothGATTWri | |||||||
|   auto *connection = this->get_connection_(msg.address, false); |   auto *connection = this->get_connection_(msg.address, false); | ||||||
|   if (connection == nullptr) { |   if (connection == nullptr) { | ||||||
|     ESP_LOGW(TAG, "Cannot write GATT descriptor, not connected"); |     ESP_LOGW(TAG, "Cannot write GATT descriptor, not connected"); | ||||||
|     api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); |     this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   auto err = connection->write_descriptor(msg.handle, msg.data, true); |   auto err = connection->write_descriptor(msg.handle, msg.data, true); | ||||||
|   if (err != ESP_OK) { |   if (err != ESP_OK) { | ||||||
|     api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); |     this->send_gatt_error(msg.address, msg.handle, err); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| @@ -376,12 +401,12 @@ void BluetoothProxy::bluetooth_gatt_send_services(const api::BluetoothGATTGetSer | |||||||
|   auto *connection = this->get_connection_(msg.address, false); |   auto *connection = this->get_connection_(msg.address, false); | ||||||
|   if (connection == nullptr || !connection->connected()) { |   if (connection == nullptr || !connection->connected()) { | ||||||
|     ESP_LOGW(TAG, "Cannot get GATT services, not connected"); |     ESP_LOGW(TAG, "Cannot get GATT services, not connected"); | ||||||
|     api::global_api_server->send_bluetooth_gatt_error(msg.address, 0, ESP_GATT_NOT_CONNECTED); |     this->send_gatt_error(msg.address, 0, ESP_GATT_NOT_CONNECTED); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (!connection->service_count_) { |   if (!connection->service_count_) { | ||||||
|     ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str().c_str()); |     ESP_LOGW(TAG, "[%d] [%s] No GATT services found", connection->connection_index_, connection->address_str().c_str()); | ||||||
|     api::global_api_server->send_bluetooth_gatt_services_done(msg.address); |     this->send_gatt_services_done(msg.address); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   if (connection->send_service_ == |   if (connection->send_service_ == | ||||||
| @@ -393,16 +418,89 @@ void BluetoothProxy::bluetooth_gatt_notify(const api::BluetoothGATTNotifyRequest | |||||||
|   auto *connection = this->get_connection_(msg.address, false); |   auto *connection = this->get_connection_(msg.address, false); | ||||||
|   if (connection == nullptr) { |   if (connection == nullptr) { | ||||||
|     ESP_LOGW(TAG, "Cannot notify GATT characteristic, not connected"); |     ESP_LOGW(TAG, "Cannot notify GATT characteristic, not connected"); | ||||||
|     api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); |     this->send_gatt_error(msg.address, msg.handle, ESP_GATT_NOT_CONNECTED); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   auto err = connection->notify_characteristic(msg.handle, msg.enable); |   auto err = connection->notify_characteristic(msg.handle, msg.enable); | ||||||
|   if (err != ESP_OK) { |   if (err != ESP_OK) { | ||||||
|     api::global_api_server->send_bluetooth_gatt_error(msg.address, msg.handle, err); |     this->send_gatt_error(msg.address, msg.handle, err); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | void BluetoothProxy::subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags) { | ||||||
|  |   if (this->api_connection_ != nullptr) { | ||||||
|  |     ESP_LOGE(TAG, "Only one API subscription is allowed at a time"); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   this->api_connection_ = api_connection; | ||||||
|  |   this->raw_advertisements_ = flags & BluetoothProxySubscriptionFlag::SUBSCRIPTION_RAW_ADVERTISEMENTS; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void BluetoothProxy::unsubscribe_api_connection(api::APIConnection *api_connection) { | ||||||
|  |   if (this->api_connection_ != api_connection) { | ||||||
|  |     ESP_LOGV(TAG, "API connection is not subscribed"); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   this->api_connection_ = nullptr; | ||||||
|  |   this->raw_advertisements_ = false; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void BluetoothProxy::send_device_connection(uint64_t address, bool connected, uint16_t mtu, esp_err_t error) { | ||||||
|  |   if (this->api_connection_ == nullptr) | ||||||
|  |     return; | ||||||
|  |   api::BluetoothDeviceConnectionResponse call; | ||||||
|  |   call.address = address; | ||||||
|  |   call.connected = connected; | ||||||
|  |   call.mtu = mtu; | ||||||
|  |   call.error = error; | ||||||
|  |   this->api_connection_->send_bluetooth_device_connection_response(call); | ||||||
|  | } | ||||||
|  | void BluetoothProxy::send_connections_free() { | ||||||
|  |   if (this->api_connection_ == nullptr) | ||||||
|  |     return; | ||||||
|  |   api::BluetoothConnectionsFreeResponse call; | ||||||
|  |   call.free = this->get_bluetooth_connections_free(); | ||||||
|  |   call.limit = this->get_bluetooth_connections_limit(); | ||||||
|  |   this->api_connection_->send_bluetooth_connections_free_response(call); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void BluetoothProxy::send_gatt_services_done(uint64_t address) { | ||||||
|  |   if (this->api_connection_ == nullptr) | ||||||
|  |     return; | ||||||
|  |   api::BluetoothGATTGetServicesDoneResponse call; | ||||||
|  |   call.address = address; | ||||||
|  |   this->api_connection_->send_bluetooth_gatt_get_services_done_response(call); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void BluetoothProxy::send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error) { | ||||||
|  |   if (this->api_connection_ == nullptr) | ||||||
|  |     return; | ||||||
|  |   api::BluetoothGATTErrorResponse call; | ||||||
|  |   call.address = address; | ||||||
|  |   call.handle = handle; | ||||||
|  |   call.error = error; | ||||||
|  |   this->api_connection_->send_bluetooth_gatt_error_response(call); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void BluetoothProxy::send_device_pairing(uint64_t address, bool paired, esp_err_t error) { | ||||||
|  |   api::BluetoothDevicePairingResponse call; | ||||||
|  |   call.address = address; | ||||||
|  |   call.paired = paired; | ||||||
|  |   call.error = error; | ||||||
|  |  | ||||||
|  |   this->api_connection_->send_bluetooth_device_pairing_response(call); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void BluetoothProxy::send_device_unpairing(uint64_t address, bool success, esp_err_t error) { | ||||||
|  |   api::BluetoothDeviceUnpairingResponse call; | ||||||
|  |   call.address = address; | ||||||
|  |   call.success = success; | ||||||
|  |   call.error = error; | ||||||
|  |  | ||||||
|  |   this->api_connection_->send_bluetooth_device_unpairing_response(call); | ||||||
|  | } | ||||||
|  |  | ||||||
| BluetoothProxy *global_bluetooth_proxy = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | BluetoothProxy *global_bluetooth_proxy = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||||
|  |  | ||||||
| }  // namespace bluetooth_proxy | }  // namespace bluetooth_proxy | ||||||
|   | |||||||
| @@ -5,6 +5,7 @@ | |||||||
| #include <map> | #include <map> | ||||||
| #include <vector> | #include <vector> | ||||||
|  |  | ||||||
|  | #include "esphome/components/api/api_connection.h" | ||||||
| #include "esphome/components/api/api_pb2.h" | #include "esphome/components/api/api_pb2.h" | ||||||
| #include "esphome/components/esp32_ble_client/ble_client_base.h" | #include "esphome/components/esp32_ble_client/ble_client_base.h" | ||||||
| #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" | #include "esphome/components/esp32_ble_tracker/esp32_ble_tracker.h" | ||||||
| @@ -21,10 +22,33 @@ static const esp_err_t ESP_GATT_NOT_CONNECTED = -1; | |||||||
|  |  | ||||||
| using namespace esp32_ble_client; | using namespace esp32_ble_client; | ||||||
|  |  | ||||||
|  | // Legacy versions: | ||||||
|  | // Version 1: Initial version without active connections | ||||||
|  | // Version 2: Support for active connections | ||||||
|  | // Version 3: New connection API | ||||||
|  | // Version 4: Pairing support | ||||||
|  | // Version 5: Cache clear support | ||||||
|  | static const uint32_t LEGACY_ACTIVE_CONNECTIONS_VERSION = 5; | ||||||
|  | static const uint32_t LEGACY_PASSIVE_ONLY_VERSION = 1; | ||||||
|  |  | ||||||
|  | enum BluetoothProxyFeature : uint32_t { | ||||||
|  |   FEATURE_PASSIVE_SCAN = 1 << 0, | ||||||
|  |   FEATURE_ACTIVE_CONNECTIONS = 1 << 1, | ||||||
|  |   FEATURE_REMOTE_CACHING = 1 << 2, | ||||||
|  |   FEATURE_PAIRING = 1 << 3, | ||||||
|  |   FEATURE_CACHE_CLEARING = 1 << 4, | ||||||
|  |   FEATURE_RAW_ADVERTISEMENTS = 1 << 5, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | enum BluetoothProxySubscriptionFlag : uint32_t { | ||||||
|  |   SUBSCRIPTION_RAW_ADVERTISEMENTS = 1 << 0, | ||||||
|  | }; | ||||||
|  |  | ||||||
| class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component { | class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Component { | ||||||
|  public: |  public: | ||||||
|   BluetoothProxy(); |   BluetoothProxy(); | ||||||
|   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; |   bool parse_device(const esp32_ble_tracker::ESPBTDevice &device) override; | ||||||
|  |   bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) override; | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
|   void loop() override; |   void loop() override; | ||||||
|  |  | ||||||
| @@ -44,6 +68,18 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com | |||||||
|   int get_bluetooth_connections_free(); |   int get_bluetooth_connections_free(); | ||||||
|   int get_bluetooth_connections_limit() { return this->connections_.size(); } |   int get_bluetooth_connections_limit() { return this->connections_.size(); } | ||||||
|  |  | ||||||
|  |   void subscribe_api_connection(api::APIConnection *api_connection, uint32_t flags); | ||||||
|  |   void unsubscribe_api_connection(api::APIConnection *api_connection); | ||||||
|  |   api::APIConnection *get_api_connection() { return this->api_connection_; } | ||||||
|  |  | ||||||
|  |   void send_device_connection(uint64_t address, bool connected, uint16_t mtu = 0, esp_err_t error = ESP_OK); | ||||||
|  |   void send_connections_free(); | ||||||
|  |   void send_gatt_services_done(uint64_t address); | ||||||
|  |   void send_gatt_error(uint64_t address, uint16_t handle, esp_err_t error); | ||||||
|  |   void send_device_pairing(uint64_t address, bool paired, esp_err_t error = ESP_OK); | ||||||
|  |   void send_device_unpairing(uint64_t address, bool success, esp_err_t error = ESP_OK); | ||||||
|  |   void send_device_clear_cache(uint64_t address, bool success, esp_err_t error = ESP_OK); | ||||||
|  |  | ||||||
|   static void uint64_to_bd_addr(uint64_t address, esp_bd_addr_t bd_addr) { |   static void uint64_to_bd_addr(uint64_t address, esp_bd_addr_t bd_addr) { | ||||||
|     bd_addr[0] = (address >> 40) & 0xff; |     bd_addr[0] = (address >> 40) & 0xff; | ||||||
|     bd_addr[1] = (address >> 32) & 0xff; |     bd_addr[1] = (address >> 32) & 0xff; | ||||||
| @@ -56,6 +92,27 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com | |||||||
|   void set_active(bool active) { this->active_ = active; } |   void set_active(bool active) { this->active_ = active; } | ||||||
|   bool has_active() { return this->active_; } |   bool has_active() { return this->active_; } | ||||||
|  |  | ||||||
|  |   uint32_t get_legacy_version() const { | ||||||
|  |     if (this->active_) { | ||||||
|  |       return LEGACY_ACTIVE_CONNECTIONS_VERSION; | ||||||
|  |     } | ||||||
|  |     return LEGACY_PASSIVE_ONLY_VERSION; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   uint32_t get_feature_flags() const { | ||||||
|  |     uint32_t flags = 0; | ||||||
|  |     flags |= BluetoothProxyFeature::FEATURE_PASSIVE_SCAN; | ||||||
|  |     flags |= BluetoothProxyFeature::FEATURE_RAW_ADVERTISEMENTS; | ||||||
|  |     if (this->active_) { | ||||||
|  |       flags |= BluetoothProxyFeature::FEATURE_ACTIVE_CONNECTIONS; | ||||||
|  |       flags |= BluetoothProxyFeature::FEATURE_REMOTE_CACHING; | ||||||
|  |       flags |= BluetoothProxyFeature::FEATURE_PAIRING; | ||||||
|  |       flags |= BluetoothProxyFeature::FEATURE_CACHE_CLEARING; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     return flags; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); |   void send_api_packet_(const esp32_ble_tracker::ESPBTDevice &device); | ||||||
|  |  | ||||||
| @@ -64,18 +121,12 @@ class BluetoothProxy : public esp32_ble_tracker::ESPBTDeviceListener, public Com | |||||||
|   bool active_; |   bool active_; | ||||||
|  |  | ||||||
|   std::vector<BluetoothConnection *> connections_{}; |   std::vector<BluetoothConnection *> connections_{}; | ||||||
|  |   api::APIConnection *api_connection_{nullptr}; | ||||||
|  |   bool raw_advertisements_{false}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| extern BluetoothProxy *global_bluetooth_proxy;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | extern BluetoothProxy *global_bluetooth_proxy;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||||
|  |  | ||||||
| // Version 1: Initial version without active connections |  | ||||||
| // Version 2: Support for active connections |  | ||||||
| // Version 3: New connection API |  | ||||||
| // Version 4: Pairing support |  | ||||||
| // Version 5: Cache clear support |  | ||||||
| static const uint32_t ACTIVE_CONNECTIONS_VERSION = 5; |  | ||||||
| static const uint32_t PASSIVE_ONLY_VERSION = 1; |  | ||||||
|  |  | ||||||
| }  // namespace bluetooth_proxy | }  // namespace bluetooth_proxy | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|  |  | ||||||
|   | |||||||
| @@ -94,3 +94,5 @@ async def to_code(config): | |||||||
|         sens = await sensor.new_sensor(conf) |         sens = await sensor.new_sensor(conf) | ||||||
|         cg.add(var.set_pressure_sensor(sens)) |         cg.add(var.set_pressure_sensor(sens)) | ||||||
|         cg.add(var.set_pressure_oversampling(conf[CONF_OVERSAMPLING])) |         cg.add(var.set_pressure_oversampling(conf[CONF_OVERSAMPLING])) | ||||||
|  |  | ||||||
|  |     cg.add(var.set_iir_filter(config[CONF_IIR_FILTER])) | ||||||
|   | |||||||
| @@ -6,102 +6,100 @@ namespace esphome { | |||||||
| namespace captive_portal { | namespace captive_portal { | ||||||
|  |  | ||||||
| const uint8_t INDEX_GZ[] PROGMEM = { | const uint8_t INDEX_GZ[] PROGMEM = { | ||||||
|     0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x0a, 0xdd, 0x58, 0x09, 0x6f, 0xdc, 0x36, 0x16, 0xfe, 0x2b, |     0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x03, 0xdd, 0x58, 0x5b, 0x8f, 0xdb, 0x36, 0x16, 0x7e, 0xef, | ||||||
|     0xac, 0x92, 0x74, 0x34, 0x8d, 0xc5, 0xd1, 0x31, 0x97, 0x35, 0xd2, 0x14, 0x89, 0x37, 0x45, 0x0b, 0x24, 0x69, 0x00, |     0xaf, 0xe0, 0x2a, 0x49, 0x2d, 0x37, 0x23, 0xea, 0x66, 0xf9, 0x2a, 0xa9, 0x48, 0xb2, 0x29, 0x5a, 0x20, 0x69, 0x03, | ||||||
|     0xbb, 0x5d, 0x14, 0x69, 0x00, 0x73, 0x24, 0x6a, 0xc4, 0x58, 0xa2, 0x54, 0x91, 0x9a, 0x23, 0x83, 0xd9, 0xdf, 0xde, |     0xcc, 0xb4, 0xfb, 0x10, 0x04, 0x18, 0x5a, 0xa2, 0x2c, 0x66, 0x24, 0x4a, 0x15, 0xe9, 0x5b, 0x0c, 0xef, 0x6f, 0xdf, | ||||||
|     0x47, 0x52, 0x73, 0x38, 0x6b, 0x2f, 0x90, 0x62, 0x8b, 0xa2, 0x4d, 0x6c, 0x9a, 0xc7, 0x3b, 0x3f, 0xf2, 0xf1, 0x3d, |     0x43, 0x52, 0xf6, 0x38, 0xb3, 0x99, 0x05, 0x52, 0xec, 0x62, 0xd1, 0x4e, 0x26, 0x1c, 0x92, 0x3a, 0xd7, 0x4f, 0x3c, | ||||||
|     0x2a, 0xfa, 0x2a, 0xad, 0x12, 0xb9, 0xad, 0x29, 0xca, 0x65, 0x59, 0xcc, 0x23, 0xd5, 0xa2, 0x82, 0xf0, 0x65, 0x4c, |     0x17, 0x2a, 0xfe, 0x5b, 0xde, 0x64, 0x72, 0xdf, 0x52, 0x54, 0xca, 0xba, 0x4a, 0x63, 0x35, 0xa2, 0x8a, 0xf0, 0x55, | ||||||
|     0x39, 0x8c, 0x28, 0x49, 0xe7, 0x51, 0x49, 0x25, 0x41, 0x49, 0x4e, 0x1a, 0x41, 0x65, 0xfc, 0xd3, 0xcd, 0x77, 0xce, |     0x42, 0x39, 0xac, 0x28, 0xc9, 0xd3, 0xb8, 0xa6, 0x92, 0xa0, 0xac, 0x24, 0x9d, 0xa0, 0x32, 0xf9, 0xf5, 0xe6, 0x07, | ||||||
|     0x14, 0x0d, 0xe6, 0x51, 0xc1, 0xf8, 0x1d, 0x6a, 0x68, 0x11, 0xb3, 0xa4, 0xe2, 0x28, 0x6f, 0x68, 0x16, 0xa7, 0x44, |     0x67, 0x8a, 0xdc, 0x34, 0xae, 0x18, 0xbf, 0x43, 0x1d, 0xad, 0x12, 0x96, 0x35, 0x1c, 0x95, 0x1d, 0x2d, 0x92, 0x9c, | ||||||
|     0x92, 0x90, 0x95, 0x64, 0x49, 0x15, 0x81, 0x66, 0xe3, 0xa4, 0xa4, 0xf1, 0x8a, 0xd1, 0x75, 0x5d, 0x35, 0x12, 0x01, |     0x48, 0x32, 0x67, 0x35, 0x59, 0x51, 0x45, 0xa0, 0xd9, 0x38, 0xa9, 0x69, 0xb2, 0x61, 0x74, 0xdb, 0x36, 0x9d, 0x44, | ||||||
|     0xa5, 0xa4, 0x5c, 0xc6, 0xd6, 0x9a, 0xa5, 0x32, 0x8f, 0x53, 0xba, 0x62, 0x09, 0x75, 0xf4, 0xe0, 0x82, 0x71, 0x26, |     0x40, 0x29, 0x29, 0x97, 0x89, 0xb5, 0x65, 0xb9, 0x2c, 0x93, 0x9c, 0x6e, 0x58, 0x46, 0x1d, 0xbd, 0xb8, 0x62, 0x9c, | ||||||
|     0x19, 0x29, 0x1c, 0x91, 0x90, 0x82, 0xc6, 0xde, 0x45, 0x2b, 0x68, 0xa3, 0x07, 0x64, 0x01, 0x63, 0x5e, 0x59, 0x20, |     0x49, 0x46, 0x2a, 0x47, 0x64, 0xa4, 0xa2, 0x89, 0x7f, 0xb5, 0x16, 0xb4, 0xd3, 0x0b, 0xb2, 0x84, 0x35, 0x6f, 0x2c, | ||||||
|     0x52, 0x24, 0x0d, 0xab, 0x25, 0x52, 0xf6, 0xc6, 0x65, 0x95, 0xb6, 0x05, 0x9d, 0x67, 0x2d, 0x4f, 0x24, 0x03, 0x0b, |     0x10, 0x29, 0xb2, 0x8e, 0xb5, 0x12, 0x29, 0x7b, 0x93, 0xba, 0xc9, 0xd7, 0x15, 0x4d, 0x5d, 0x97, 0x08, 0xb0, 0x4b, | ||||||
|     0x84, 0xcd, 0xfb, 0xbb, 0x82, 0x4a, 0x44, 0xe3, 0x37, 0x44, 0xe6, 0xb8, 0x24, 0x1b, 0xdb, 0x74, 0x18, 0xb7, 0xfd, |     0xb8, 0x8c, 0xe7, 0x74, 0x87, 0xa7, 0xb3, 0x68, 0x32, 0x9e, 0xe6, 0x13, 0xfc, 0x51, 0x7c, 0x03, 0x9e, 0xad, 0x6b, | ||||||
|     0x6f, 0x6c, 0xfe, 0xdc, 0x73, 0xdd, 0xfe, 0x85, 0x6e, 0xdc, 0xfe, 0x00, 0xfe, 0xce, 0x1a, 0x2a, 0xdb, 0x86, 0x23, |     0x50, 0x87, 0xab, 0x26, 0x23, 0x92, 0x35, 0x1c, 0x0b, 0x4a, 0xba, 0xac, 0x4c, 0x92, 0xc4, 0xfa, 0x5e, 0x90, 0x0d, | ||||||
|     0x62, 0xdf, 0x46, 0x35, 0x50, 0xa2, 0x34, 0xb6, 0x4a, 0xcf, 0xc7, 0xae, 0x3b, 0x45, 0xde, 0x25, 0xf6, 0x47, 0x8e, |     0xb5, 0xbe, 0xfd, 0xd6, 0x3e, 0x13, 0xad, 0xa8, 0x7c, 0x5d, 0x51, 0x35, 0x15, 0x2f, 0xf7, 0x37, 0x64, 0xf5, 0x33, | ||||||
|     0xe7, 0xe1, 0xc0, 0xf1, 0x46, 0xc9, 0xc4, 0x19, 0x21, 0x6f, 0x08, 0x8d, 0xef, 0xe3, 0x11, 0x72, 0x3f, 0x59, 0x28, |     0x58, 0x6e, 0x5b, 0x44, 0xb0, 0x9c, 0x5a, 0xc3, 0xf7, 0xde, 0x07, 0x2c, 0xe4, 0xbe, 0xa2, 0x38, 0x67, 0xa2, 0xad, | ||||||
|     0x63, 0x45, 0x11, 0x5b, 0xbc, 0xe2, 0xd4, 0x42, 0x42, 0x36, 0xd5, 0x1d, 0x8d, 0xad, 0xa4, 0x6d, 0x1a, 0xf0, 0xee, |     0xc8, 0x3e, 0xb1, 0x96, 0x20, 0xf5, 0xce, 0x1a, 0x2e, 0x8a, 0x35, 0xcf, 0x94, 0x70, 0x24, 0x6c, 0x3a, 0x3c, 0x54, | ||||||
|     0xaa, 0x2a, 0xaa, 0x06, 0xac, 0xfd, 0x95, 0xa3, 0x7b, 0xff, 0xbe, 0x58, 0x87, 0x6c, 0x08, 0x17, 0x59, 0xd5, 0x94, |     0x14, 0xcc, 0x4b, 0xde, 0x12, 0x59, 0xe2, 0x9a, 0xec, 0x6c, 0x33, 0x61, 0xdc, 0x0e, 0xbe, 0xb3, 0xe9, 0x73, 0xdf, | ||||||
|     0xb1, 0xa5, 0x41, 0xb1, 0x9f, 0xee, 0xe8, 0x1e, 0xa9, 0xa6, 0x7f, 0xb6, 0xe8, 0x54, 0x0d, 0x5b, 0x32, 0x1e, 0x5b, |     0xf3, 0x86, 0x57, 0x7a, 0xf0, 0x86, 0x2e, 0xfc, 0x5d, 0x74, 0x54, 0xae, 0x3b, 0x8e, 0x88, 0x7d, 0x1b, 0xb7, 0x40, | ||||||
|     0x9e, 0x8f, 0xbc, 0x29, 0xe8, 0xbd, 0xed, 0xef, 0x8f, 0xa0, 0x10, 0x05, 0x4a, 0xe7, 0x66, 0x65, 0xbf, 0xbf, 0x8d, |     0x89, 0xf2, 0xc4, 0xaa, 0xfd, 0x00, 0x7b, 0xde, 0x14, 0xf9, 0x33, 0x1c, 0x44, 0x8e, 0xef, 0xe3, 0xd0, 0xf1, 0xa3, | ||||||
|     0xc4, 0x6a, 0x89, 0x36, 0x65, 0xc1, 0x45, 0x6c, 0xe5, 0x52, 0xd6, 0xe1, 0x60, 0xb0, 0x5e, 0xaf, 0xf1, 0x3a, 0xc0, |     0x6c, 0xe2, 0x44, 0xc8, 0x1f, 0xc1, 0x10, 0x04, 0x38, 0x42, 0xde, 0x27, 0x0b, 0x15, 0xac, 0xaa, 0x12, 0x8b, 0x37, | ||||||
|     0x55, 0xb3, 0x1c, 0xf8, 0xae, 0xeb, 0x0e, 0x80, 0xc2, 0x42, 0x66, 0x7f, 0x2c, 0x7f, 0x68, 0xa1, 0x9c, 0xb2, 0x65, |     0x9c, 0x5a, 0x48, 0xc8, 0xae, 0xb9, 0xa3, 0x89, 0x95, 0xad, 0xbb, 0x0e, 0xec, 0x7f, 0xd5, 0x54, 0x4d, 0x07, 0x70, | ||||||
|     0x2e, 0x75, 0x7f, 0xfe, 0x74, 0xc7, 0xf7, 0x91, 0xa2, 0x98, 0xdf, 0x7e, 0x38, 0xd3, 0xd2, 0x9c, 0x69, 0xe1, 0xdf, |     0x7d, 0x83, 0x3e, 0xfb, 0xf9, 0x6a, 0x15, 0xb2, 0x23, 0x5c, 0x14, 0x4d, 0x57, 0x27, 0x96, 0x7e, 0x29, 0xf6, 0xd3, | ||||||
|     0x12, 0xdb, 0x3a, 0xb8, 0xda, 0x7b, 0xa3, 0x8c, 0x9a, 0x10, 0x1f, 0xf9, 0xc8, 0xd5, 0xff, 0x7d, 0x47, 0xf5, 0xbb, |     0x83, 0x3c, 0x22, 0x35, 0x0c, 0x2f, 0x1e, 0x3a, 0x4d, 0xc7, 0x56, 0x8c, 0x27, 0x96, 0x1f, 0x20, 0x7f, 0x0a, 0x6a, | ||||||
|     0x91, 0xf3, 0xd9, 0x08, 0x9d, 0x8d, 0xe0, 0xaf, 0x02, 0xd0, 0x2f, 0xc7, 0xce, 0xe5, 0x91, 0xdf, 0x53, 0xeb, 0x2b, |     0x6f, 0x87, 0xc7, 0x33, 0x26, 0x44, 0x61, 0xd2, 0x7b, 0xd9, 0xd8, 0xef, 0x6f, 0x63, 0xb1, 0x59, 0xa1, 0x5d, 0x5d, | ||||||
|     0xcf, 0x3d, 0x4d, 0x28, 0xa6, 0xef, 0xc7, 0xe7, 0x63, 0xc7, 0xff, 0x59, 0x11, 0x68, 0xf4, 0x8f, 0x5c, 0x8e, 0x9f, |     0x71, 0x91, 0x58, 0xa5, 0x94, 0xed, 0xdc, 0x75, 0xb7, 0xdb, 0x2d, 0xde, 0x86, 0xb8, 0xe9, 0x56, 0x6e, 0xe0, 0x79, | ||||||
|     0x7b, 0x3f, 0x8f, 0xc9, 0x08, 0x8d, 0xba, 0x99, 0x91, 0xa3, 0xfa, 0xc7, 0x91, 0xd6, 0x85, 0x46, 0x2b, 0x20, 0x2b, |     0x9e, 0x0b, 0x14, 0x16, 0x32, 0xe7, 0xc3, 0x0a, 0x46, 0x16, 0x2a, 0x29, 0x5b, 0x95, 0x52, 0xcf, 0xd3, 0xa7, 0x07, | ||||||
|     0x9d, 0xb1, 0x33, 0x22, 0x01, 0x0a, 0x3a, 0xab, 0xa0, 0x07, 0xd3, 0x63, 0xe0, 0x3e, 0x9b, 0x73, 0x82, 0x4f, 0xbd, |     0x7a, 0x8c, 0x15, 0x45, 0x7a, 0xfb, 0xe1, 0x42, 0x4b, 0x77, 0xa1, 0x85, 0x7e, 0x7f, 0x81, 0xe6, 0xe0, 0xad, 0x32, | ||||||
|     0xc1, 0xdc, 0xea, 0x87, 0x96, 0x75, 0x82, 0xa1, 0x3a, 0x87, 0x01, 0x7f, 0xac, 0xe0, 0xdc, 0x59, 0x56, 0x7f, 0x6f, |     0x6a, 0x42, 0x02, 0x14, 0x20, 0x4f, 0xff, 0x0b, 0x1c, 0x35, 0xef, 0x57, 0xce, 0x83, 0x15, 0xba, 0x58, 0xc1, 0x5f, | ||||||
|     0x7d, 0x2b, 0xc8, 0x8a, 0x5a, 0x71, 0x1c, 0x43, 0xa8, 0xb5, 0x25, 0x9c, 0x10, 0x5c, 0x54, 0x09, 0x51, 0x2c, 0x58, |     0xc0, 0x2f, 0xa8, 0xc7, 0xce, 0xec, 0xcc, 0xee, 0xab, 0xc7, 0x1b, 0xdf, 0xbb, 0xdf, 0x50, 0x3c, 0x3f, 0x8e, 0x2f, | ||||||
|     0x50, 0xd2, 0x24, 0xf9, 0xd7, 0x5f, 0xdb, 0xc7, 0xa5, 0x25, 0x95, 0xaf, 0x0a, 0xaa, 0xba, 0xe2, 0xe5, 0xf6, 0x86, |     0xd7, 0x4e, 0xf0, 0x9b, 0x22, 0x50, 0xd8, 0x9f, 0x99, 0x9c, 0xa0, 0xf4, 0x7f, 0x1b, 0x93, 0x08, 0x45, 0xfd, 0x4e, | ||||||
|     0x2c, 0xdf, 0x42, 0x00, 0xd9, 0x16, 0x11, 0x2c, 0xa5, 0x56, 0xff, 0xbd, 0xfb, 0x01, 0x0b, 0xb9, 0x2d, 0x28, 0x4e, |     0xe4, 0xa8, 0xf9, 0x79, 0xa5, 0x34, 0xa1, 0x68, 0x03, 0x54, 0xb5, 0x33, 0x76, 0x22, 0x12, 0xa2, 0xb0, 0x37, 0x09, | ||||||
|     0x99, 0xa8, 0x0b, 0xb2, 0x8d, 0xad, 0x05, 0xc8, 0xba, 0xb3, 0xfa, 0x17, 0x19, 0x95, 0x49, 0x6e, 0x5b, 0x03, 0x08, |     0x66, 0xb0, 0x3d, 0x06, 0xe6, 0x8b, 0x3d, 0x27, 0xfc, 0x34, 0x50, 0x30, 0xcf, 0x2d, 0xeb, 0x1e, 0x83, 0xe6, 0x12, | ||||||
|     0xb1, 0x8c, 0x2d, 0xf1, 0x47, 0x51, 0x71, 0xab, 0x8f, 0x65, 0x4e, 0xb9, 0x6d, 0x1f, 0x2c, 0x54, 0xf6, 0x71, 0xbd, |     0x03, 0xfc, 0xb1, 0x81, 0x33, 0x67, 0x59, 0x80, 0x11, 0x95, 0x59, 0x69, 0x5b, 0x2e, 0x44, 0x5e, 0xc1, 0x56, 0x10, | ||||||
|     0x64, 0x3f, 0xb4, 0x74, 0xb4, 0x41, 0x32, 0xa9, 0x42, 0x0e, 0xab, 0xe0, 0xbd, 0x38, 0xce, 0x2e, 0xaa, 0x74, 0xfb, |     0x15, 0x0d, 0xb7, 0x86, 0x58, 0x96, 0x94, 0xdb, 0x27, 0x56, 0xc5, 0x48, 0xf5, 0x13, 0xfb, 0xe1, 0x13, 0x39, 0x3c, | ||||||
|     0x88, 0x79, 0xb9, 0x67, 0x6c, 0x63, 0x9c, 0xd3, 0xe6, 0x86, 0x6e, 0xe0, 0xb8, 0xfc, 0x9b, 0x7d, 0xc7, 0xd0, 0x5b, |     0x9c, 0xe3, 0x43, 0x32, 0x09, 0x71, 0x28, 0xb1, 0x8a, 0xe8, 0xab, 0xf3, 0xee, 0xb2, 0xc9, 0xf7, 0x8f, 0x84, 0x4e, | ||||||
|     0x2a, 0xd7, 0x55, 0x73, 0x27, 0x42, 0x64, 0x3d, 0x37, 0xe2, 0x66, 0x26, 0x42, 0x39, 0x26, 0xb5, 0xc0, 0xa2, 0x80, |     0xe9, 0x9b, 0xb8, 0x61, 0x9c, 0xd3, 0xee, 0x86, 0xee, 0xe0, 0x1d, 0xfe, 0x83, 0xfd, 0xc0, 0xd0, 0xcf, 0x54, 0x6e, | ||||||
|     0xf0, 0xb7, 0xbd, 0x3e, 0xc4, 0x6a, 0x7d, 0xdf, 0x14, 0x83, 0xe2, 0x6d, 0x94, 0xb2, 0x15, 0x4a, 0x0a, 0x22, 0xe0, |     0x9b, 0xee, 0x4e, 0xcc, 0x91, 0xf5, 0xdc, 0x88, 0x5b, 0xa8, 0xa8, 0x61, 0x20, 0x9b, 0xb4, 0x02, 0x8b, 0x0a, 0x72, | ||||||
|     0xb8, 0x72, 0x23, 0xcb, 0x42, 0x87, 0xb8, 0xaa, 0x78, 0x02, 0xfc, 0x77, 0xb1, 0xf5, 0x00, 0x76, 0x2f, 0xb7, 0x3f, |     0x82, 0xed, 0x0f, 0x21, 0x7e, 0xda, 0x7b, 0x4b, 0xf8, 0xc9, 0xb9, 0xdb, 0x38, 0x67, 0x1b, 0x94, 0x55, 0x10, 0xf5, | ||||||
|     0xa4, 0x76, 0x4f, 0x00, 0x6a, 0xbd, 0x3e, 0x5e, 0x91, 0xa2, 0xa5, 0x28, 0x46, 0x32, 0x67, 0xe2, 0x64, 0xe2, 0xec, |     0x70, 0xfc, 0x8d, 0x28, 0x0b, 0xf5, 0x47, 0xbd, 0xe1, 0x19, 0x70, 0xdf, 0x25, 0xd6, 0x17, 0xa2, 0xfa, 0xe5, 0xfe, | ||||||
|     0x51, 0xb6, 0x5a, 0xdc, 0x01, 0x57, 0x06, 0xcb, 0xc2, 0xee, 0x5b, 0xc7, 0x38, 0x8e, 0x88, 0xb9, 0xe5, 0xac, 0x27, |     0xa7, 0xdc, 0x1e, 0x08, 0x88, 0xe7, 0xc1, 0x10, 0x6f, 0x48, 0xb5, 0xa6, 0x28, 0x41, 0xb2, 0x64, 0xe2, 0xde, 0xc0, | ||||||
|     0xd6, 0x67, 0x36, 0x39, 0x05, 0xcd, 0xa4, 0x75, 0x16, 0xf0, 0x4f, 0x77, 0x70, 0x1b, 0xe1, 0x06, 0xf4, 0xf7, 0xf7, |     0xc5, 0xa3, 0x6c, 0xad, 0xb8, 0x03, 0xae, 0x02, 0x1e, 0x0b, 0x7b, 0x68, 0x9d, 0x22, 0x2b, 0x26, 0x26, 0xef, 0x59, | ||||||
|     0xa7, 0xd9, 0x48, 0xd4, 0x84, 0x7f, 0xce, 0xaa, 0x6c, 0xd4, 0x81, 0x85, 0x55, 0x4f, 0x45, 0x17, 0x10, 0x9d, 0x74, |     0x4f, 0xac, 0x07, 0x16, 0x39, 0x15, 0x2d, 0xa4, 0x75, 0x1f, 0x81, 0x4f, 0x0f, 0xc2, 0xe6, 0xb8, 0x03, 0xed, 0xc3, | ||||||
|     0x0e, 0xc8, 0xb1, 0xff, 0x74, 0x07, 0x71, 0xa6, 0x8e, 0xce, 0xdd, 0x49, 0x68, 0x34, 0x00, 0x84, 0xe6, 0xb7, 0xfb, |     0xe3, 0x79, 0x33, 0x16, 0x2d, 0xe1, 0x0f, 0x19, 0x95, 0x81, 0xea, 0xa0, 0x43, 0xb2, 0x82, 0x99, 0x3a, 0xed, 0x40, | ||||||
|     0x7e, 0xff, 0xe4, 0xce, 0x6f, 0x2d, 0x6d, 0xb6, 0xd7, 0xb4, 0xa0, 0x89, 0xac, 0x1a, 0xdb, 0x7a, 0x02, 0x9a, 0xe0, |     0x74, 0x56, 0xe8, 0x92, 0xd3, 0xf4, 0xe9, 0xa1, 0x03, 0x89, 0x2a, 0x07, 0x9d, 0x25, 0xc6, 0x2e, 0x40, 0x93, 0xde, | ||||||
|     0x24, 0x68, 0xbf, 0xbf, 0xbf, 0x79, 0xf3, 0x3a, 0xae, 0x6c, 0xda, 0xbf, 0x78, 0x8c, 0x5a, 0xdd, 0xea, 0xef, 0xe1, |     0x1e, 0x87, 0xf7, 0x7e, 0xfc, 0xbe, 0xa6, 0xdd, 0xfe, 0x9a, 0x56, 0x34, 0x93, 0x4d, 0x67, 0x5b, 0x4f, 0x40, 0x0b, | ||||||
|     0x56, 0xff, 0x4f, 0xdc, 0x53, 0xf7, 0x7a, 0xef, 0x03, 0xb0, 0x1a, 0xaf, 0x4f, 0x97, 0xbb, 0xba, 0x00, 0x9e, 0xc3, |     0xbc, 0x7e, 0xed, 0xf0, 0x8f, 0x37, 0x6f, 0xdf, 0x24, 0x8d, 0xcd, 0x86, 0x57, 0x8f, 0x51, 0xab, 0x0c, 0xff, 0x1e, | ||||||
|     0x25, 0x72, 0x61, 0x3d, 0x17, 0xb6, 0x33, 0x1e, 0xf5, 0x41, 0x3d, 0xfc, 0x80, 0xe9, 0xfa, 0x7a, 0x86, 0x6b, 0x5a, |     0x32, 0xfc, 0x3f, 0x93, 0x81, 0xca, 0xf1, 0x83, 0x0f, 0xc0, 0xaa, 0xfd, 0xbd, 0xbd, 0x4f, 0xf4, 0x2a, 0x18, 0x9f, | ||||||
|     0x1d, 0xd1, 0xf9, 0x37, 0xbb, 0x45, 0xb5, 0x71, 0x04, 0xfb, 0xc4, 0xf8, 0x32, 0x64, 0x3c, 0xa7, 0x0d, 0x93, 0x7b, |     0x43, 0x40, 0x5f, 0x29, 0x0f, 0x9d, 0x71, 0x34, 0x3c, 0x82, 0x7e, 0xb0, 0x00, 0xec, 0xd6, 0xb9, 0x1a, 0x72, 0xb6, | ||||||
|     0x30, 0x17, 0x6e, 0xfa, 0xba, 0x95, 0xbb, 0x9a, 0xa4, 0xa9, 0x5a, 0x19, 0xd5, 0x9b, 0x59, 0x06, 0x79, 0x41, 0x51, |     0x4a, 0x9b, 0xe9, 0x77, 0x87, 0x65, 0xb3, 0x73, 0x04, 0xfb, 0xc4, 0xf8, 0x6a, 0xce, 0x78, 0x49, 0x3b, 0x26, 0x8f, | ||||||
|     0xd2, 0xd0, 0xa3, 0xe5, 0xde, 0xac, 0xeb, 0x2b, 0x28, 0xbc, 0x1c, 0x3d, 0xdb, 0xab, 0x83, 0xb7, 0x93, 0xb0, 0x65, |     0x60, 0x2e, 0xa4, 0xfd, 0x76, 0x2d, 0x0f, 0x2d, 0xc9, 0x73, 0xf5, 0x24, 0x6a, 0x77, 0x8b, 0x02, 0x8a, 0x84, 0xa2, | ||||||
|     0x0e, 0x29, 0xd8, 0x92, 0x87, 0x09, 0xd8, 0x4d, 0x1b, 0xc3, 0x94, 0x91, 0x92, 0x15, 0xdb, 0x50, 0xc0, 0x65, 0xe8, |     0xa4, 0x73, 0x9f, 0xd6, 0x47, 0xf3, 0x5c, 0xe7, 0x83, 0xf9, 0x2c, 0x7a, 0x76, 0x54, 0x07, 0xee, 0x20, 0xe1, 0x65, | ||||||
|     0x40, 0xc2, 0x60, 0xd9, 0x7e, 0xd1, 0x4a, 0x59, 0x71, 0xd0, 0xdd, 0xa4, 0xb4, 0x09, 0xdd, 0x99, 0xe9, 0x38, 0x0d, |     0x39, 0xa4, 0x62, 0x2b, 0x3e, 0xcf, 0xc0, 0x70, 0xda, 0x19, 0xa6, 0x82, 0xd4, 0xac, 0xda, 0xcf, 0x05, 0x64, 0x26, | ||||||
|     0x49, 0x59, 0x2b, 0x42, 0x1c, 0x34, 0xb4, 0x9c, 0x2d, 0x48, 0x72, 0xb7, 0x6c, 0xaa, 0x96, 0xa7, 0x4e, 0xa2, 0x6e, |     0x07, 0xaa, 0x07, 0x2b, 0x8e, 0xcb, 0xb5, 0x94, 0x0d, 0x07, 0xdd, 0x5d, 0x4e, 0xbb, 0xb9, 0xb7, 0x30, 0x13, 0xa7, | ||||||
|     0xeb, 0xf0, 0x89, 0x97, 0x91, 0x80, 0x26, 0xb3, 0x6e, 0x94, 0x65, 0xd9, 0x0c, 0x90, 0xa0, 0x8e, 0xb9, 0xfc, 0x42, |     0x23, 0x39, 0x5b, 0x8b, 0x39, 0x0e, 0x3b, 0x5a, 0x2f, 0x96, 0x24, 0xbb, 0x5b, 0x75, 0xcd, 0x9a, 0xe7, 0x4e, 0xa6, | ||||||
|     0x1f, 0x0f, 0x15, 0xdb, 0x99, 0x99, 0xd8, 0x57, 0x13, 0xc6, 0x46, 0x48, 0x25, 0xcf, 0x66, 0x07, 0x77, 0xdc, 0x19, |     0x32, 0xe7, 0xfc, 0x89, 0x5f, 0x90, 0x90, 0x66, 0x8b, 0x7e, 0x55, 0x14, 0xc5, 0x02, 0xa0, 0xa0, 0x8e, 0xc9, 0x44, | ||||||
|     0xa4, 0x01, 0x01, 0x42, 0x6a, 0x88, 0x7f, 0x30, 0x73, 0x5f, 0x12, 0xc6, 0xcf, 0xad, 0x57, 0x67, 0x65, 0xd6, 0x85, |     0xf3, 0x00, 0x8f, 0x14, 0xdb, 0x85, 0x99, 0x38, 0x50, 0x1b, 0xc6, 0x46, 0x48, 0xeb, 0xcf, 0x16, 0x27, 0x77, 0xbc, | ||||||
|     0x2f, 0xc0, 0xa2, 0xd5, 0xe8, 0x20, 0x9e, 0x41, 0xa2, 0x32, 0xb9, 0x30, 0xf4, 0xc7, 0x6e, 0xbd, 0xd9, 0xe3, 0xee, |     0x05, 0xa4, 0x64, 0x01, 0x42, 0x5a, 0x88, 0x47, 0x30, 0xf3, 0x58, 0x13, 0xc6, 0x2f, 0xad, 0x57, 0xc7, 0x64, 0xd1, | ||||||
|     0x8c, 0xec, 0x0e, 0xd4, 0x59, 0x41, 0x37, 0xb3, 0x8f, 0xad, 0x90, 0x2c, 0xdb, 0x3a, 0x5d, 0x2e, 0x0d, 0xe1, 0xbc, |     0x97, 0x14, 0x80, 0x45, 0xab, 0xd1, 0x85, 0x65, 0x01, 0x45, 0xc3, 0x14, 0xc6, 0x79, 0x30, 0xf6, 0xda, 0xdd, 0x11, | ||||||
|     0x40, 0x0e, 0x5d, 0x00, 0x29, 0xa5, 0x7c, 0xa6, 0x75, 0x38, 0x4c, 0xd2, 0x52, 0x74, 0x38, 0x1d, 0xc5, 0xe8, 0x53, |     0xf7, 0x07, 0xe4, 0x70, 0xa2, 0x2e, 0x2a, 0xba, 0x5b, 0x7c, 0x5c, 0x0b, 0xc9, 0x8a, 0xbd, 0xd3, 0x17, 0xd6, 0x39, | ||||||
|     0x7a, 0x5f, 0xd6, 0xff, 0xa2, 0x56, 0xc7, 0x71, 0x57, 0x92, 0x06, 0x72, 0x8b, 0xb3, 0xa8, 0x00, 0xd3, 0x32, 0x74, |     0x1c, 0x16, 0x28, 0xa8, 0x4b, 0x20, 0xa5, 0x94, 0x2f, 0xb4, 0x0e, 0x87, 0x49, 0x5a, 0x8b, 0x1e, 0xa7, 0xb3, 0x18, | ||||||
|     0x26, 0xb0, 0x57, 0xdd, 0x94, 0x12, 0x06, 0x9e, 0x83, 0x99, 0xfa, 0x6e, 0x3a, 0xe0, 0xed, 0xd5, 0x1b, 0x24, 0xaa, |     0x7d, 0x40, 0x3f, 0x97, 0xf5, 0x9f, 0xa8, 0xd5, 0x59, 0x3c, 0xd4, 0xa4, 0x83, 0x44, 0xef, 0x2c, 0x1b, 0xc0, 0xb4, | ||||||
|     0x82, 0xa5, 0x1d, 0x9d, 0x26, 0x41, 0xee, 0x11, 0x1e, 0x0f, 0xb6, 0x1b, 0xa9, 0xb9, 0x03, 0xd4, 0xc3, 0x6c, 0x4a, |     0x9e, 0x3b, 0x13, 0x78, 0x57, 0xfd, 0x96, 0x12, 0x06, 0x9e, 0x83, 0x99, 0xba, 0x5e, 0x9e, 0xf0, 0xf6, 0xdb, 0x1d, | ||||||
|     0x3c, 0xf7, 0x81, 0x1d, 0x49, 0xb3, 0xcc, 0x5f, 0x64, 0x47, 0xa4, 0x54, 0xaa, 0xdd, 0xb3, 0xee, 0x54, 0xf8, 0x43, |     0x12, 0x4d, 0xc5, 0xf2, 0x9e, 0x4e, 0x93, 0x20, 0xef, 0x0c, 0x8f, 0x0f, 0xaf, 0x1b, 0xa9, 0xbd, 0x13, 0xd4, 0xa3, | ||||||
|     0x10, 0x70, 0xd8, 0x1b, 0xe8, 0xef, 0x99, 0x8e, 0x8b, 0xdd, 0x99, 0x14, 0x7d, 0x52, 0xc3, 0xb6, 0x29, 0xec, 0x87, |     0x62, 0x4a, 0x7c, 0xef, 0x0b, 0x6f, 0x24, 0x2f, 0x8a, 0x60, 0x59, 0x9c, 0x91, 0x52, 0x65, 0xef, 0xc8, 0xfa, 0x53, | ||||||
|     0x4e, 0xee, 0xb3, 0xe0, 0xea, 0x94, 0x09, 0x7b, 0x8f, 0x67, 0xc2, 0x1e, 0x52, 0xb5, 0xcb, 0xcb, 0x6a, 0x13, 0xf7, |     0x11, 0x8c, 0x40, 0xc0, 0xe9, 0xdd, 0xc0, 0xfc, 0xc8, 0x74, 0x58, 0x1c, 0x2e, 0xa4, 0xe8, 0xa3, 0x3a, 0x5f, 0x77, | ||||||
|     0x74, 0x4e, 0x1a, 0xc2, 0x4f, 0xef, 0x59, 0xf0, 0x0a, 0xf8, 0xff, 0x2f, 0x29, 0xee, 0x0f, 0xa7, 0xb7, 0x2f, 0x48, |     0x95, 0x6d, 0x7d, 0xe1, 0xe8, 0x3e, 0x0b, 0x5f, 0xdd, 0x97, 0xa5, 0xc1, 0xe3, 0x65, 0x69, 0x80, 0x54, 0x23, 0xf3, | ||||||
|     0x6d, 0x5f, 0x98, 0xd5, 0x8c, 0x77, 0xca, 0x79, 0xe8, 0x41, 0xfa, 0x62, 0x58, 0xb0, 0xa5, 0xf7, 0x67, 0x40, 0xfb, |     0xb2, 0xd9, 0x25, 0x03, 0x5d, 0x20, 0x46, 0xf0, 0x3b, 0x78, 0x16, 0xbe, 0x06, 0xfe, 0xff, 0x4a, 0xbd, 0xf9, 0xc3, | ||||||
|     0xdf, 0x38, 0x06, 0x2f, 0xbc, 0x29, 0xbe, 0x44, 0xba, 0x31, 0x10, 0xe1, 0x60, 0x8a, 0x26, 0x57, 0x43, 0x3c, 0xf4, |     0xc5, 0xe6, 0x2b, 0x2a, 0xcd, 0x57, 0x56, 0x19, 0xe3, 0x9d, 0x72, 0x1e, 0x66, 0x50, 0x4e, 0x18, 0x16, 0x6c, 0xe5, | ||||||
|     0x90, 0xaa, 0x9a, 0xc6, 0x68, 0x82, 0xa7, 0x40, 0x30, 0xc6, 0xc1, 0x04, 0x26, 0x90, 0xef, 0xe1, 0xd1, 0x6b, 0x3f, |     0xff, 0x2f, 0xa0, 0xfd, 0x77, 0x1c, 0xc3, 0x17, 0xfe, 0x14, 0xcf, 0x90, 0x1e, 0x0c, 0x44, 0x38, 0x9c, 0xa2, 0xc9, | ||||||
|     0xc0, 0xe3, 0x11, 0x50, 0xf9, 0x2e, 0x0e, 0x7c, 0x64, 0x68, 0xc7, 0xd8, 0x07, 0x71, 0x8a, 0x24, 0x28, 0x01, 0xe8, |     0xab, 0x11, 0x1e, 0xf9, 0x48, 0xb5, 0x30, 0x63, 0x34, 0x81, 0x7e, 0x0f, 0xf9, 0x63, 0x1c, 0x4e, 0x60, 0x03, 0x05, | ||||||
|     0x24, 0xc0, 0xee, 0x04, 0xc4, 0x8d, 0xb1, 0x7b, 0x89, 0xa7, 0x63, 0x34, 0xc5, 0x13, 0x80, 0x0e, 0x0f, 0x47, 0x85, |     0x3e, 0x8e, 0xde, 0x04, 0x21, 0x1e, 0x47, 0x40, 0x15, 0x78, 0x38, 0x0c, 0x90, 0xa1, 0x1d, 0xe3, 0x00, 0xc4, 0x29, | ||||||
|     0x33, 0xc2, 0x1e, 0x4c, 0x07, 0x63, 0x32, 0xc5, 0xc3, 0x00, 0xe9, 0xc6, 0xc0, 0x31, 0x01, 0x11, 0x0e, 0x76, 0xbd, |     0x92, 0xb0, 0x06, 0xa0, 0xb3, 0x10, 0x7b, 0x13, 0x10, 0x37, 0xc6, 0xde, 0x0c, 0x4f, 0xc7, 0x68, 0x8a, 0x27, 0x00, | ||||||
|     0xd7, 0x01, 0xf6, 0x27, 0xa0, 0x77, 0x38, 0x7c, 0x01, 0x62, 0x2f, 0x87, 0xc8, 0xb4, 0x06, 0x5e, 0x50, 0x30, 0x7a, |     0x1d, 0x1e, 0x45, 0x95, 0x13, 0x61, 0x1f, 0xb6, 0xc3, 0x31, 0x99, 0xe2, 0x51, 0x88, 0xf4, 0x60, 0xe0, 0x98, 0x80, | ||||||
|     0x0c, 0x34, 0xff, 0x9f, 0x0b, 0x1a, 0x40, 0xe2, 0xa1, 0x00, 0x5f, 0x42, 0xec, 0x7a, 0x8a, 0xdf, 0xb4, 0x06, 0x37, |     0x08, 0x07, 0x7b, 0xfe, 0x9b, 0x10, 0x07, 0x13, 0xd0, 0x3b, 0x1a, 0xbd, 0x00, 0xb1, 0xb3, 0x11, 0x32, 0xa3, 0x81, | ||||||
|     0xcf, 0x43, 0xee, 0x1f, 0xc6, 0x2c, 0xf8, 0xe7, 0x62, 0xe6, 0x29, 0x04, 0xa0, 0x0b, 0xba, 0x41, 0x0e, 0xd2, 0x8d, |     0x17, 0x14, 0x44, 0x8f, 0x81, 0x16, 0xfc, 0x75, 0x41, 0x03, 0x48, 0x7c, 0x14, 0xe2, 0x19, 0xc4, 0xae, 0xaf, 0xf8, | ||||||
|     0xd1, 0x0d, 0xcc, 0xd3, 0xab, 0x4b, 0x34, 0x05, 0xae, 0xf1, 0x14, 0x5d, 0xa2, 0x91, 0x42, 0x17, 0xd8, 0x87, 0x86, |     0xcd, 0x68, 0x70, 0xf3, 0x7d, 0xe4, 0xfd, 0x61, 0xcc, 0xc2, 0xbf, 0x2e, 0x66, 0xbe, 0x42, 0x00, 0xa6, 0xa0, 0x1b, | ||||||
|     0xc9, 0x01, 0xa6, 0x2f, 0x84, 0x71, 0xf8, 0x37, 0x86, 0xf1, 0x31, 0x9f, 0xfe, 0xc6, 0x2e, 0xfd, 0x15, 0x57, 0x10, |     0xe4, 0x20, 0x3d, 0x18, 0xdd, 0xc0, 0x3c, 0x7d, 0x35, 0x43, 0x53, 0xe0, 0x1a, 0x4f, 0xd1, 0x0c, 0x45, 0x0a, 0x5d, | ||||||
|     0x94, 0x63, 0xba, 0x0c, 0x8b, 0x06, 0xe6, 0x15, 0xaf, 0xaa, 0x28, 0x78, 0x94, 0x43, 0x35, 0x02, 0xef, 0x7a, 0x0f, |     0x60, 0x1f, 0x19, 0x26, 0x07, 0x98, 0xbe, 0x12, 0xc6, 0xd1, 0x9f, 0x18, 0xc6, 0xc7, 0x7c, 0xfa, 0x13, 0xbb, 0xf4, | ||||||
|     0xb1, 0x34, 0xce, 0xbd, 0xf9, 0xbd, 0x2a, 0x1d, 0x28, 0xbd, 0x79, 0xa4, 0xd3, 0xf9, 0xfc, 0x26, 0xa7, 0xe8, 0xd5, |     0xff, 0x48, 0x41, 0xd0, 0x8e, 0xe9, 0x36, 0x2c, 0x76, 0xcd, 0x95, 0x5e, 0x75, 0x51, 0x70, 0x43, 0x87, 0x6e, 0x04, | ||||||
|     0xf5, 0x3b, 0x78, 0x08, 0x16, 0x05, 0xe2, 0xd5, 0x1a, 0xde, 0x9b, 0x5b, 0x24, 0x2b, 0xf5, 0x82, 0xe7, 0x50, 0x2a, |     0x2e, 0xf9, 0x3e, 0x62, 0x79, 0x52, 0xfa, 0xe9, 0x67, 0xdd, 0x39, 0x50, 0xfa, 0x69, 0xac, 0xcb, 0x79, 0x7a, 0x53, | ||||||
|     0xaa, 0x2e, 0x3c, 0x20, 0x50, 0x57, 0x2c, 0x60, 0x8c, 0xa3, 0x45, 0x33, 0x7f, 0x57, 0x50, 0x22, 0x28, 0x5a, 0xb2, |     0x52, 0xf4, 0xfa, 0xfa, 0x1d, 0xdc, 0xca, 0xaa, 0x0a, 0xf1, 0x66, 0x0b, 0x97, 0xbf, 0x3d, 0x92, 0x8d, 0xba, 0xce, | ||||||
|     0x15, 0x45, 0x4c, 0x42, 0x1d, 0x50, 0x52, 0x24, 0x99, 0x6a, 0x8e, 0x8c, 0x9a, 0xee, 0x6d, 0x25, 0x69, 0x88, 0xae, |     0x73, 0xe8, 0x15, 0xd5, 0x14, 0xee, 0x0d, 0xa8, 0x6f, 0x16, 0x30, 0xc6, 0xf1, 0xb2, 0x4b, 0xdf, 0x55, 0x94, 0x08, | ||||||
|     0xaa, 0x7a, 0xab, 0x85, 0x24, 0x39, 0xe1, 0x4b, 0x9a, 0x1e, 0x84, 0x29, 0xea, 0x6d, 0xd5, 0x36, 0xe8, 0x97, 0x17, |     0x8a, 0x56, 0x6c, 0x43, 0x11, 0x93, 0xd0, 0x07, 0xd4, 0x14, 0x49, 0xa6, 0x86, 0x33, 0xa3, 0xa6, 0x83, 0x9e, 0x56, | ||||||
|     0x6f, 0x5e, 0xab, 0x87, 0x36, 0x45, 0x4e, 0xa7, 0x6c, 0x23, 0xd1, 0x8f, 0x37, 0x2f, 0x50, 0x5b, 0xc3, 0xa6, 0x53, |     0x2b, 0x31, 0xdd, 0x30, 0x58, 0x02, 0x62, 0xd2, 0xbe, 0xed, 0x8d, 0xcb, 0xd0, 0x58, 0x75, 0x4d, 0xa5, 0x84, 0x8e, | ||||||
|     0x63, 0x5b, 0xb5, 0xa2, 0xcd, 0x1a, 0x2a, 0x4b, 0xaa, 0x48, 0x40, 0xb9, 0xa0, 0x52, 0x42, 0xa1, 0x21, 0x30, 0x94, |     0x41, 0x59, 0x15, 0xa6, 0xb1, 0xba, 0x76, 0x22, 0xa2, 0x2f, 0x06, 0x89, 0xbb, 0x65, 0x05, 0x53, 0x97, 0xf9, 0x34, | ||||||
|     0xce, 0xda, 0x13, 0x53, 0x75, 0x83, 0xbb, 0x20, 0x7e, 0xde, 0x95, 0xd7, 0x51, 0x1e, 0x18, 0xd7, 0xaf, 0x3b, 0x6a, |     0xd6, 0xad, 0xa2, 0x92, 0xa0, 0xba, 0x15, 0xf3, 0xe5, 0x41, 0xcf, 0x2a, 0xca, 0x57, 0x70, 0x9b, 0x84, 0x77, 0x01, | ||||||
|     0x70, 0x3d, 0x98, 0x47, 0xea, 0x39, 0x8d, 0x88, 0x7e, 0x84, 0xc4, 0x83, 0x35, 0xcb, 0x98, 0x7a, 0xb8, 0xcd, 0x23, |     0xcd, 0x43, 0x46, 0xcb, 0xa6, 0x82, 0xe6, 0x24, 0xb9, 0xbe, 0xfe, 0xe9, 0xef, 0xea, 0x33, 0x85, 0x32, 0xe1, 0xcc, | ||||||
|     0x5d, 0x8f, 0x2a, 0x09, 0xaa, 0x24, 0x32, 0x5f, 0x34, 0x74, 0xaf, 0xa0, 0x7c, 0x09, 0xaf, 0x64, 0xd8, 0x70, 0xa8, |     0x09, 0x7d, 0xbe, 0x61, 0x54, 0x93, 0x9e, 0x6f, 0x3c, 0x32, 0x1f, 0x1c, 0x5a, 0xe8, 0xd3, 0xc1, 0xbf, 0xfc, 0x33, | ||||||
|     0x50, 0x12, 0x9a, 0x57, 0x05, 0x54, 0x40, 0xf1, 0xf5, 0xf5, 0x0f, 0xff, 0x52, 0x9f, 0x3f, 0xc0, 0xcf, 0x13, 0x27, |     0x29, 0xef, 0x4e, 0x9b, 0xbd, 0x24, 0xfd, 0x5f, 0x37, 0x9d, 0x86, 0x49, 0xac, 0x97, 0x35, 0x93, 0xe9, 0x35, 0x18, | ||||||
|     0x3c, 0x29, 0x0c, 0xa3, 0xea, 0x74, 0x7c, 0xe3, 0xa1, 0xf9, 0x90, 0x51, 0xc3, 0x7b, 0x00, 0xfc, 0x4e, 0xef, 0x49, |     0x18, 0xbb, 0xe6, 0x01, 0x38, 0xa7, 0x1c, 0x30, 0xb4, 0x65, 0xcf, 0x03, 0x60, 0xff, 0x72, 0xf3, 0x02, 0xfd, 0xda, | ||||||
|     0x79, 0x77, 0x98, 0xec, 0x24, 0xe9, 0x5f, 0x5d, 0xd9, 0x1a, 0x26, 0xd1, 0x2e, 0x4a, 0x26, 0xe7, 0xd7, 0x60, 0x60, |     0xc2, 0x09, 0xa6, 0x06, 0x7b, 0xed, 0x65, 0x4d, 0x65, 0xd9, 0xe4, 0xc9, 0xbb, 0x5f, 0xae, 0x6f, 0xce, 0x1e, 0xaf, | ||||||
|     0x34, 0x30, 0x0b, 0xe0, 0x9c, 0x72, 0xc0, 0xd0, 0xe6, 0x1d, 0x0f, 0xec, 0xa8, 0x42, 0xec, 0x27, 0x8d, 0x98, 0xd9, |     0x35, 0x11, 0xa2, 0x3c, 0x33, 0x1f, 0x42, 0xd6, 0x95, 0x64, 0x2d, 0xe9, 0xa4, 0x16, 0xeb, 0xa8, 0x10, 0x38, 0x79, | ||||||
|     0x60, 0xed, 0x65, 0x49, 0x65, 0x5e, 0xa5, 0xf1, 0xbb, 0x1f, 0xaf, 0x6f, 0x8e, 0x1e, 0x77, 0xb0, 0x52, 0x9e, 0x98, |     0xa4, 0x9f, 0x17, 0xac, 0xa2, 0xc6, 0xa9, 0x9e, 0xd1, 0x4d, 0xd1, 0x97, 0x6c, 0x3c, 0xe9, 0x7e, 0x60, 0xa5, 0x6b, | ||||||
|     0x0f, 0x2c, 0x6d, 0x21, 0x59, 0x4d, 0x1a, 0xa9, 0xc5, 0x3a, 0x2a, 0xce, 0x0e, 0x1e, 0xe9, 0x75, 0xbd, 0x33, 0xda, |     0x4e, 0x89, 0x6b, 0x8e, 0x8c, 0xab, 0x3f, 0x13, 0xfd, 0x0b, 0x65, 0x37, 0xa3, 0x8e, 0x36, 0x12, 0x00, 0x00}; | ||||||
|     0xa9, 0x8e, 0x71, 0x30, 0x47, 0x0f, 0xd9, 0x78, 0xd0, 0xfd, 0x99, 0x95, 0x03, 0x73, 0x14, 0x07, 0xe6, 0x5c, 0x0e, |  | ||||||
|     0xf4, 0xe7, 0xa7, 0xdf, 0x01, 0xf1, 0x69, 0xfc, 0xac, 0x8e, 0x12, 0x00, 0x00}; |  | ||||||
|  |  | ||||||
| }  // namespace captive_portal | }  // namespace captive_portal | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -6,7 +6,7 @@ from esphome.const import ( | |||||||
|     ICON_RADIATOR, |     ICON_RADIATOR, | ||||||
|     ICON_RESTART, |     ICON_RESTART, | ||||||
|     DEVICE_CLASS_CARBON_DIOXIDE, |     DEVICE_CLASS_CARBON_DIOXIDE, | ||||||
|     DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, |     DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, | ||||||
|     STATE_CLASS_MEASUREMENT, |     STATE_CLASS_MEASUREMENT, | ||||||
|     UNIT_PARTS_PER_MILLION, |     UNIT_PARTS_PER_MILLION, | ||||||
|     UNIT_PARTS_PER_BILLION, |     UNIT_PARTS_PER_BILLION, | ||||||
| @@ -43,7 +43,7 @@ CONFIG_SCHEMA = ( | |||||||
|                 unit_of_measurement=UNIT_PARTS_PER_BILLION, |                 unit_of_measurement=UNIT_PARTS_PER_BILLION, | ||||||
|                 icon=ICON_RADIATOR, |                 icon=ICON_RADIATOR, | ||||||
|                 accuracy_decimals=0, |                 accuracy_decimals=0, | ||||||
|                 device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS, |                 device_class=DEVICE_CLASS_VOLATILE_ORGANIC_COMPOUNDS_PARTS, | ||||||
|                 state_class=STATE_CLASS_MEASUREMENT, |                 state_class=STATE_CLASS_MEASUREMENT, | ||||||
|             ), |             ), | ||||||
|             cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema( |             cv.Optional(CONF_VERSION): text_sensor.text_sensor_schema( | ||||||
|   | |||||||
| @@ -1,9 +1,9 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #include "esphome/core/component.h" |  | ||||||
| #include "esphome/core/helpers.h" |  | ||||||
| #include "esphome/core/automation.h" | #include "esphome/core/automation.h" | ||||||
|  | #include "esphome/core/component.h" | ||||||
| #include "esphome/core/hal.h" | #include "esphome/core/hal.h" | ||||||
|  | #include "esphome/core/helpers.h" | ||||||
|  |  | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
| #include <esp_sleep.h> | #include <esp_sleep.h> | ||||||
| @@ -11,6 +11,7 @@ | |||||||
|  |  | ||||||
| #ifdef USE_TIME | #ifdef USE_TIME | ||||||
| #include "esphome/components/time/real_time_clock.h" | #include "esphome/components/time/real_time_clock.h" | ||||||
|  | #include "esphome/core/time.h" | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| @@ -170,7 +171,7 @@ template<typename... Ts> class EnterDeepSleepAction : public Action<Ts...> { | |||||||
|       if (after_time) |       if (after_time) | ||||||
|         timestamp += 60 * 60 * 24; |         timestamp += 60 * 60 * 24; | ||||||
|  |  | ||||||
|       int32_t offset = time::ESPTime::timezone_offset(); |       int32_t offset = ESPTime::timezone_offset(); | ||||||
|       timestamp -= offset;  // Change timestamp to utc |       timestamp -= offset;  // Change timestamp to utc | ||||||
|       const uint32_t ms_left = (timestamp - timestamp_now) * 1000; |       const uint32_t ms_left = (timestamp - timestamp_now) * 1000; | ||||||
|       this->deep_sleep_->set_sleep_duration(ms_left); |       this->deep_sleep_->set_sleep_duration(ms_left); | ||||||
|   | |||||||
							
								
								
									
										69
									
								
								esphome/components/display/animation.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								esphome/components/display/animation.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,69 @@ | |||||||
|  | #include "animation.h" | ||||||
|  |  | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace display { | ||||||
|  |  | ||||||
|  | Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type) | ||||||
|  |     : Image(data_start, width, height, type), | ||||||
|  |       animation_data_start_(data_start), | ||||||
|  |       current_frame_(0), | ||||||
|  |       animation_frame_count_(animation_frame_count), | ||||||
|  |       loop_start_frame_(0), | ||||||
|  |       loop_end_frame_(animation_frame_count_), | ||||||
|  |       loop_count_(0), | ||||||
|  |       loop_current_iteration_(1) {} | ||||||
|  | void Animation::set_loop(uint32_t start_frame, uint32_t end_frame, int count) { | ||||||
|  |   loop_start_frame_ = std::min(start_frame, animation_frame_count_); | ||||||
|  |   loop_end_frame_ = std::min(end_frame, animation_frame_count_); | ||||||
|  |   loop_count_ = count; | ||||||
|  |   loop_current_iteration_ = 1; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | uint32_t Animation::get_animation_frame_count() const { return this->animation_frame_count_; } | ||||||
|  | int Animation::get_current_frame() const { return this->current_frame_; } | ||||||
|  | void Animation::next_frame() { | ||||||
|  |   this->current_frame_++; | ||||||
|  |   if (loop_count_ && this->current_frame_ == loop_end_frame_ && | ||||||
|  |       (this->loop_current_iteration_ < loop_count_ || loop_count_ < 0)) { | ||||||
|  |     this->current_frame_ = loop_start_frame_; | ||||||
|  |     this->loop_current_iteration_++; | ||||||
|  |   } | ||||||
|  |   if (this->current_frame_ >= animation_frame_count_) { | ||||||
|  |     this->loop_current_iteration_ = 1; | ||||||
|  |     this->current_frame_ = 0; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   this->update_data_start_(); | ||||||
|  | } | ||||||
|  | void Animation::prev_frame() { | ||||||
|  |   this->current_frame_--; | ||||||
|  |   if (this->current_frame_ < 0) { | ||||||
|  |     this->current_frame_ = this->animation_frame_count_ - 1; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   this->update_data_start_(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Animation::set_frame(int frame) { | ||||||
|  |   unsigned abs_frame = abs(frame); | ||||||
|  |  | ||||||
|  |   if (abs_frame < this->animation_frame_count_) { | ||||||
|  |     if (frame >= 0) { | ||||||
|  |       this->current_frame_ = frame; | ||||||
|  |     } else { | ||||||
|  |       this->current_frame_ = this->animation_frame_count_ - abs_frame; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   this->update_data_start_(); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | void Animation::update_data_start_() { | ||||||
|  |   const uint32_t image_size = image_type_to_width_stride(this->width_, this->type_) * this->height_; | ||||||
|  |   this->data_start_ = this->animation_data_start_ + image_size * this->current_frame_; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace display | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										37
									
								
								esphome/components/display/animation.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										37
									
								
								esphome/components/display/animation.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,37 @@ | |||||||
|  | #pragma once | ||||||
|  | #include "image.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace display { | ||||||
|  |  | ||||||
|  | class Animation : public Image { | ||||||
|  |  public: | ||||||
|  |   Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type); | ||||||
|  |  | ||||||
|  |   uint32_t get_animation_frame_count() const; | ||||||
|  |   int get_current_frame() const; | ||||||
|  |   void next_frame(); | ||||||
|  |   void prev_frame(); | ||||||
|  |  | ||||||
|  |   /** Selects a specific frame within the animation. | ||||||
|  |    * | ||||||
|  |    * @param frame If possitive, advance to the frame. If negative, recede to that frame from the end frame. | ||||||
|  |    */ | ||||||
|  |   void set_frame(int frame); | ||||||
|  |  | ||||||
|  |   void set_loop(uint32_t start_frame, uint32_t end_frame, int count); | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   void update_data_start_(); | ||||||
|  |  | ||||||
|  |   const uint8_t *animation_data_start_; | ||||||
|  |   int current_frame_; | ||||||
|  |   uint32_t animation_frame_count_; | ||||||
|  |   uint32_t loop_start_frame_; | ||||||
|  |   uint32_t loop_end_frame_; | ||||||
|  |   int loop_count_; | ||||||
|  |   int loop_current_iteration_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace display | ||||||
|  | }  // namespace esphome | ||||||
| @@ -3,16 +3,20 @@ | |||||||
| #include <utility> | #include <utility> | ||||||
| #include "esphome/core/application.h" | #include "esphome/core/application.h" | ||||||
| #include "esphome/core/color.h" | #include "esphome/core/color.h" | ||||||
| #include "esphome/core/log.h" |  | ||||||
| #include "esphome/core/hal.h" | #include "esphome/core/hal.h" | ||||||
| #include "esphome/core/helpers.h" | #include "esphome/core/helpers.h" | ||||||
|  | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
|  | #include "animation.h" | ||||||
|  | #include "image.h" | ||||||
|  | #include "font.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace display { | namespace display { | ||||||
|  |  | ||||||
| static const char *const TAG = "display"; | static const char *const TAG = "display"; | ||||||
|  |  | ||||||
| const Color COLOR_OFF(0, 0, 0, 0); | const Color COLOR_OFF(0, 0, 0, 255); | ||||||
| const Color COLOR_ON(255, 255, 255, 255); | const Color COLOR_ON(255, 255, 255, 255); | ||||||
|  |  | ||||||
| void Rect::expand(int16_t horizontal, int16_t vertical) { | void Rect::expand(int16_t horizontal, int16_t vertical) { | ||||||
| @@ -306,45 +310,39 @@ void DisplayBuffer::vprintf_(int x, int y, Font *font, Color color, TextAlign al | |||||||
|     this->print(x, y, font, color, align, buffer); |     this->print(x, y, font, color, align, buffer); | ||||||
| } | } | ||||||
|  |  | ||||||
| void DisplayBuffer::image(int x, int y, Image *image, Color color_on, Color color_off) { | void DisplayBuffer::image(int x, int y, BaseImage *image, Color color_on, Color color_off) { | ||||||
|   switch (image->get_type()) { |   this->image(x, y, image, ImageAlign::TOP_LEFT, color_on, color_off); | ||||||
|     case IMAGE_TYPE_BINARY: |  | ||||||
|       for (int img_x = 0; img_x < image->get_width(); img_x++) { |  | ||||||
|         for (int img_y = 0; img_y < image->get_height(); img_y++) { |  | ||||||
|           this->draw_pixel_at(x + img_x, y + img_y, image->get_pixel(img_x, img_y) ? color_on : color_off); |  | ||||||
|         } |  | ||||||
| } | } | ||||||
|  |  | ||||||
|  | void DisplayBuffer::image(int x, int y, BaseImage *image, ImageAlign align, Color color_on, Color color_off) { | ||||||
|  |   auto x_align = ImageAlign(int(align) & (int(ImageAlign::HORIZONTAL_ALIGNMENT))); | ||||||
|  |   auto y_align = ImageAlign(int(align) & (int(ImageAlign::VERTICAL_ALIGNMENT))); | ||||||
|  |  | ||||||
|  |   switch (x_align) { | ||||||
|  |     case ImageAlign::RIGHT: | ||||||
|  |       x -= image->get_width(); | ||||||
|       break; |       break; | ||||||
|     case IMAGE_TYPE_GRAYSCALE: |     case ImageAlign::CENTER_HORIZONTAL: | ||||||
|       for (int img_x = 0; img_x < image->get_width(); img_x++) { |       x -= image->get_width() / 2; | ||||||
|         for (int img_y = 0; img_y < image->get_height(); img_y++) { |  | ||||||
|           this->draw_pixel_at(x + img_x, y + img_y, image->get_grayscale_pixel(img_x, img_y)); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
|     case IMAGE_TYPE_RGB24: |     case ImageAlign::LEFT: | ||||||
|       for (int img_x = 0; img_x < image->get_width(); img_x++) { |     default: | ||||||
|         for (int img_y = 0; img_y < image->get_height(); img_y++) { |  | ||||||
|           this->draw_pixel_at(x + img_x, y + img_y, image->get_color_pixel(img_x, img_y)); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |  | ||||||
|     case IMAGE_TYPE_TRANSPARENT_BINARY: |  | ||||||
|       for (int img_x = 0; img_x < image->get_width(); img_x++) { |  | ||||||
|         for (int img_y = 0; img_y < image->get_height(); img_y++) { |  | ||||||
|           if (image->get_pixel(img_x, img_y)) |  | ||||||
|             this->draw_pixel_at(x + img_x, y + img_y, color_on); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |  | ||||||
|     case IMAGE_TYPE_RGB565: |  | ||||||
|       for (int img_x = 0; img_x < image->get_width(); img_x++) { |  | ||||||
|         for (int img_y = 0; img_y < image->get_height(); img_y++) { |  | ||||||
|           this->draw_pixel_at(x + img_x, y + img_y, image->get_rgb565_pixel(img_x, img_y)); |  | ||||||
|         } |  | ||||||
|       } |  | ||||||
|       break; |       break; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   switch (y_align) { | ||||||
|  |     case ImageAlign::BOTTOM: | ||||||
|  |       y -= image->get_height(); | ||||||
|  |       break; | ||||||
|  |     case ImageAlign::CENTER_VERTICAL: | ||||||
|  |       y -= image->get_height() / 2; | ||||||
|  |       break; | ||||||
|  |     case ImageAlign::TOP: | ||||||
|  |     default: | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   image->draw(x, y, this, color_on, color_off); | ||||||
| } | } | ||||||
|  |  | ||||||
| #ifdef USE_GRAPH | #ifdef USE_GRAPH | ||||||
| @@ -472,24 +470,21 @@ void DisplayOnPageChangeTrigger::process(DisplayPage *from, DisplayPage *to) { | |||||||
|   if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to)) |   if ((this->from_ == nullptr || this->from_ == from) && (this->to_ == nullptr || this->to_ == to)) | ||||||
|     this->trigger(from, to); |     this->trigger(from, to); | ||||||
| } | } | ||||||
| #ifdef USE_TIME | void DisplayBuffer::strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, ESPTime time) { | ||||||
| void DisplayBuffer::strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, |  | ||||||
|                              time::ESPTime time) { |  | ||||||
|   char buffer[64]; |   char buffer[64]; | ||||||
|   size_t ret = time.strftime(buffer, sizeof(buffer), format); |   size_t ret = time.strftime(buffer, sizeof(buffer), format); | ||||||
|   if (ret > 0) |   if (ret > 0) | ||||||
|     this->print(x, y, font, color, align, buffer); |     this->print(x, y, font, color, align, buffer); | ||||||
| } | } | ||||||
| void DisplayBuffer::strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time) { | void DisplayBuffer::strftime(int x, int y, Font *font, Color color, const char *format, ESPTime time) { | ||||||
|   this->strftime(x, y, font, color, TextAlign::TOP_LEFT, format, time); |   this->strftime(x, y, font, color, TextAlign::TOP_LEFT, format, time); | ||||||
| } | } | ||||||
| void DisplayBuffer::strftime(int x, int y, Font *font, TextAlign align, const char *format, time::ESPTime time) { | void DisplayBuffer::strftime(int x, int y, Font *font, TextAlign align, const char *format, ESPTime time) { | ||||||
|   this->strftime(x, y, font, COLOR_ON, align, format, time); |   this->strftime(x, y, font, COLOR_ON, align, format, time); | ||||||
| } | } | ||||||
| void DisplayBuffer::strftime(int x, int y, Font *font, const char *format, time::ESPTime time) { | void DisplayBuffer::strftime(int x, int y, Font *font, const char *format, ESPTime time) { | ||||||
|   this->strftime(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, time); |   this->strftime(x, y, font, COLOR_ON, TextAlign::TOP_LEFT, format, time); | ||||||
| } | } | ||||||
| #endif |  | ||||||
|  |  | ||||||
| void DisplayBuffer::start_clipping(Rect rect) { | void DisplayBuffer::start_clipping(Rect rect) { | ||||||
|   if (!this->clipping_rectangle_.empty()) { |   if (!this->clipping_rectangle_.empty()) { | ||||||
| @@ -526,217 +521,6 @@ Rect DisplayBuffer::get_clipping() { | |||||||
|     return this->clipping_rectangle_.back(); |     return this->clipping_rectangle_.back(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| bool Glyph::get_pixel(int x, int y) const { |  | ||||||
|   const int x_data = x - this->glyph_data_->offset_x; |  | ||||||
|   const int y_data = y - this->glyph_data_->offset_y; |  | ||||||
|   if (x_data < 0 || x_data >= this->glyph_data_->width || y_data < 0 || y_data >= this->glyph_data_->height) |  | ||||||
|     return false; |  | ||||||
|   const uint32_t width_8 = ((this->glyph_data_->width + 7u) / 8u) * 8u; |  | ||||||
|   const uint32_t pos = x_data + y_data * width_8; |  | ||||||
|   return progmem_read_byte(this->glyph_data_->data + (pos / 8u)) & (0x80 >> (pos % 8u)); |  | ||||||
| } |  | ||||||
| const char *Glyph::get_char() const { return this->glyph_data_->a_char; } |  | ||||||
| bool Glyph::compare_to(const char *str) const { |  | ||||||
|   // 1 -> this->char_ |  | ||||||
|   // 2 -> str |  | ||||||
|   for (uint32_t i = 0;; i++) { |  | ||||||
|     if (this->glyph_data_->a_char[i] == '\0') |  | ||||||
|       return true; |  | ||||||
|     if (str[i] == '\0') |  | ||||||
|       return false; |  | ||||||
|     if (this->glyph_data_->a_char[i] > str[i]) |  | ||||||
|       return false; |  | ||||||
|     if (this->glyph_data_->a_char[i] < str[i]) |  | ||||||
|       return true; |  | ||||||
|   } |  | ||||||
|   // this should not happen |  | ||||||
|   return false; |  | ||||||
| } |  | ||||||
| int Glyph::match_length(const char *str) const { |  | ||||||
|   for (uint32_t i = 0;; i++) { |  | ||||||
|     if (this->glyph_data_->a_char[i] == '\0') |  | ||||||
|       return i; |  | ||||||
|     if (str[i] != this->glyph_data_->a_char[i]) |  | ||||||
|       return 0; |  | ||||||
|   } |  | ||||||
|   // this should not happen |  | ||||||
|   return 0; |  | ||||||
| } |  | ||||||
| void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { |  | ||||||
|   *x1 = this->glyph_data_->offset_x; |  | ||||||
|   *y1 = this->glyph_data_->offset_y; |  | ||||||
|   *width = this->glyph_data_->width; |  | ||||||
|   *height = this->glyph_data_->height; |  | ||||||
| } |  | ||||||
| int Font::match_next_glyph(const char *str, int *match_length) { |  | ||||||
|   int lo = 0; |  | ||||||
|   int hi = this->glyphs_.size() - 1; |  | ||||||
|   while (lo != hi) { |  | ||||||
|     int mid = (lo + hi + 1) / 2; |  | ||||||
|     if (this->glyphs_[mid].compare_to(str)) { |  | ||||||
|       lo = mid; |  | ||||||
|     } else { |  | ||||||
|       hi = mid - 1; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
|   *match_length = this->glyphs_[lo].match_length(str); |  | ||||||
|   if (*match_length <= 0) |  | ||||||
|     return -1; |  | ||||||
|   return lo; |  | ||||||
| } |  | ||||||
| void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) { |  | ||||||
|   *baseline = this->baseline_; |  | ||||||
|   *height = this->height_; |  | ||||||
|   int i = 0; |  | ||||||
|   int min_x = 0; |  | ||||||
|   bool has_char = false; |  | ||||||
|   int x = 0; |  | ||||||
|   while (str[i] != '\0') { |  | ||||||
|     int match_length; |  | ||||||
|     int glyph_n = this->match_next_glyph(str + i, &match_length); |  | ||||||
|     if (glyph_n < 0) { |  | ||||||
|       // Unknown char, skip |  | ||||||
|       if (!this->get_glyphs().empty()) |  | ||||||
|         x += this->get_glyphs()[0].glyph_data_->width; |  | ||||||
|       i++; |  | ||||||
|       continue; |  | ||||||
|     } |  | ||||||
|  |  | ||||||
|     const Glyph &glyph = this->glyphs_[glyph_n]; |  | ||||||
|     if (!has_char) { |  | ||||||
|       min_x = glyph.glyph_data_->offset_x; |  | ||||||
|     } else { |  | ||||||
|       min_x = std::min(min_x, x + glyph.glyph_data_->offset_x); |  | ||||||
|     } |  | ||||||
|     x += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; |  | ||||||
|  |  | ||||||
|     i += match_length; |  | ||||||
|     has_char = true; |  | ||||||
|   } |  | ||||||
|   *x_offset = min_x; |  | ||||||
|   *width = x - min_x; |  | ||||||
| } |  | ||||||
| Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) { |  | ||||||
|   glyphs_.reserve(data_nr); |  | ||||||
|   for (int i = 0; i < data_nr; ++i) |  | ||||||
|     glyphs_.emplace_back(&data[i]); |  | ||||||
| } |  | ||||||
|  |  | ||||||
| bool Image::get_pixel(int x, int y) const { |  | ||||||
|   if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) |  | ||||||
|     return false; |  | ||||||
|   const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; |  | ||||||
|   const uint32_t pos = x + y * width_8; |  | ||||||
|   return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); |  | ||||||
| } |  | ||||||
| Color Image::get_color_pixel(int x, int y) const { |  | ||||||
|   if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) |  | ||||||
|     return Color::BLACK; |  | ||||||
|   const uint32_t pos = (x + y * this->width_) * 3; |  | ||||||
|   const uint32_t color32 = (progmem_read_byte(this->data_start_ + pos + 2) << 0) | |  | ||||||
|                            (progmem_read_byte(this->data_start_ + pos + 1) << 8) | |  | ||||||
|                            (progmem_read_byte(this->data_start_ + pos + 0) << 16); |  | ||||||
|   return Color(color32); |  | ||||||
| } |  | ||||||
| Color Image::get_rgb565_pixel(int x, int y) const { |  | ||||||
|   if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) |  | ||||||
|     return Color::BLACK; |  | ||||||
|   const uint32_t pos = (x + y * this->width_) * 2; |  | ||||||
|   uint16_t rgb565 = |  | ||||||
|       progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1); |  | ||||||
|   auto r = (rgb565 & 0xF800) >> 11; |  | ||||||
|   auto g = (rgb565 & 0x07E0) >> 5; |  | ||||||
|   auto b = rgb565 & 0x001F; |  | ||||||
|   return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)); |  | ||||||
| } |  | ||||||
| Color Image::get_grayscale_pixel(int x, int y) const { |  | ||||||
|   if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) |  | ||||||
|     return Color::BLACK; |  | ||||||
|   const uint32_t pos = (x + y * this->width_); |  | ||||||
|   const uint8_t gray = progmem_read_byte(this->data_start_ + pos); |  | ||||||
|   return Color(gray | gray << 8 | gray << 16 | gray << 24); |  | ||||||
| } |  | ||||||
| int Image::get_width() const { return this->width_; } |  | ||||||
| int Image::get_height() const { return this->height_; } |  | ||||||
| ImageType Image::get_type() const { return this->type_; } |  | ||||||
| Image::Image(const uint8_t *data_start, int width, int height, ImageType type) |  | ||||||
|     : width_(width), height_(height), type_(type), data_start_(data_start) {} |  | ||||||
| int Image::get_current_frame() const { return 0; } |  | ||||||
|  |  | ||||||
| bool Animation::get_pixel(int x, int y) const { |  | ||||||
|   if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) |  | ||||||
|     return false; |  | ||||||
|   const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; |  | ||||||
|   const uint32_t frame_index = this->height_ * width_8 * this->current_frame_; |  | ||||||
|   if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_)) |  | ||||||
|     return false; |  | ||||||
|   const uint32_t pos = x + y * width_8 + frame_index; |  | ||||||
|   return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); |  | ||||||
| } |  | ||||||
| Color Animation::get_color_pixel(int x, int y) const { |  | ||||||
|   if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) |  | ||||||
|     return Color::BLACK; |  | ||||||
|   const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; |  | ||||||
|   if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_)) |  | ||||||
|     return Color::BLACK; |  | ||||||
|   const uint32_t pos = (x + y * this->width_ + frame_index) * 3; |  | ||||||
|   const uint32_t color32 = (progmem_read_byte(this->data_start_ + pos + 2) << 0) | |  | ||||||
|                            (progmem_read_byte(this->data_start_ + pos + 1) << 8) | |  | ||||||
|                            (progmem_read_byte(this->data_start_ + pos + 0) << 16); |  | ||||||
|   return Color(color32); |  | ||||||
| } |  | ||||||
| Color Animation::get_rgb565_pixel(int x, int y) const { |  | ||||||
|   if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) |  | ||||||
|     return Color::BLACK; |  | ||||||
|   const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; |  | ||||||
|   if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_)) |  | ||||||
|     return Color::BLACK; |  | ||||||
|   const uint32_t pos = (x + y * this->width_ + frame_index) * 2; |  | ||||||
|   uint16_t rgb565 = |  | ||||||
|       progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1); |  | ||||||
|   auto r = (rgb565 & 0xF800) >> 11; |  | ||||||
|   auto g = (rgb565 & 0x07E0) >> 5; |  | ||||||
|   auto b = rgb565 & 0x001F; |  | ||||||
|   return Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)); |  | ||||||
| } |  | ||||||
| Color Animation::get_grayscale_pixel(int x, int y) const { |  | ||||||
|   if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) |  | ||||||
|     return Color::BLACK; |  | ||||||
|   const uint32_t frame_index = this->width_ * this->height_ * this->current_frame_; |  | ||||||
|   if (frame_index >= (uint32_t) (this->width_ * this->height_ * this->animation_frame_count_)) |  | ||||||
|     return Color::BLACK; |  | ||||||
|   const uint32_t pos = (x + y * this->width_ + frame_index); |  | ||||||
|   const uint8_t gray = progmem_read_byte(this->data_start_ + pos); |  | ||||||
|   return Color(gray | gray << 8 | gray << 16 | gray << 24); |  | ||||||
| } |  | ||||||
| Animation::Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type) |  | ||||||
|     : Image(data_start, width, height, type), current_frame_(0), animation_frame_count_(animation_frame_count) {} |  | ||||||
| int Animation::get_animation_frame_count() const { return this->animation_frame_count_; } |  | ||||||
| int Animation::get_current_frame() const { return this->current_frame_; } |  | ||||||
| void Animation::next_frame() { |  | ||||||
|   this->current_frame_++; |  | ||||||
|   if (this->current_frame_ >= animation_frame_count_) { |  | ||||||
|     this->current_frame_ = 0; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| void Animation::prev_frame() { |  | ||||||
|   this->current_frame_--; |  | ||||||
|   if (this->current_frame_ < 0) { |  | ||||||
|     this->current_frame_ = this->animation_frame_count_ - 1; |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| void Animation::set_frame(int frame) { |  | ||||||
|   unsigned abs_frame = abs(frame); |  | ||||||
|  |  | ||||||
|   if (abs_frame < this->animation_frame_count_) { |  | ||||||
|     if (frame >= 0) { |  | ||||||
|       this->current_frame_ = frame; |  | ||||||
|     } else { |  | ||||||
|       this->current_frame_ = this->animation_frame_count_ - abs_frame; |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
|  |  | ||||||
| DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} | DisplayPage::DisplayPage(display_writer_t writer) : writer_(std::move(writer)) {} | ||||||
| void DisplayPage::show() { this->parent_->show_page(this); } | void DisplayPage::show() { this->parent_->show_page(this); } | ||||||
|   | |||||||
| @@ -1,15 +1,12 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #include "esphome/core/component.h" |  | ||||||
| #include "esphome/core/defines.h" |  | ||||||
| #include "esphome/core/automation.h" |  | ||||||
| #include "display_color_utils.h" |  | ||||||
| #include <cstdarg> | #include <cstdarg> | ||||||
| #include <vector> | #include <vector> | ||||||
|  | #include "display_color_utils.h" | ||||||
| #ifdef USE_TIME | #include "esphome/core/automation.h" | ||||||
| #include "esphome/components/time/real_time_clock.h" | #include "esphome/core/component.h" | ||||||
| #endif | #include "esphome/core/defines.h" | ||||||
|  | #include "esphome/core/time.h" | ||||||
|  |  | ||||||
| #ifdef USE_GRAPH | #ifdef USE_GRAPH | ||||||
| #include "esphome/components/graph/graph.h" | #include "esphome/components/graph/graph.h" | ||||||
| @@ -19,6 +16,10 @@ | |||||||
| #include "esphome/components/qr_code/qr_code.h" | #include "esphome/components/qr_code/qr_code.h" | ||||||
| #endif | #endif | ||||||
|  |  | ||||||
|  | #include "animation.h" | ||||||
|  | #include "font.h" | ||||||
|  | #include "image.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace display { | namespace display { | ||||||
|  |  | ||||||
| @@ -73,17 +74,52 @@ enum class TextAlign { | |||||||
|   BOTTOM_RIGHT = BOTTOM | RIGHT, |   BOTTOM_RIGHT = BOTTOM | RIGHT, | ||||||
| }; | }; | ||||||
|  |  | ||||||
| /// Turn the pixel OFF. | /** ImageAlign is used to tell the display class how to position a image. By default | ||||||
| extern const Color COLOR_OFF; |  * the coordinates you enter for the image() functions take the upper left corner of the image | ||||||
| /// Turn the pixel ON. |  * as the "anchor" point. You can customize this behavior to, for example, make the coordinates | ||||||
| extern const Color COLOR_ON; |  * refer to the *center* of the image. | ||||||
|  |  * | ||||||
|  |  * All image alignments consist of an X and Y-coordinate alignment. For the alignment along the X-axis | ||||||
|  |  * these options are allowed: | ||||||
|  |  * | ||||||
|  |  * - LEFT (x-coordinate of anchor point is on left) | ||||||
|  |  * - CENTER_HORIZONTAL (x-coordinate of anchor point is in the horizontal center of the image) | ||||||
|  |  * - RIGHT (x-coordinate of anchor point is on right) | ||||||
|  |  * | ||||||
|  |  * For the Y-Axis alignment these options are allowed: | ||||||
|  |  * | ||||||
|  |  * - TOP (y-coordinate of anchor is on the top of the image) | ||||||
|  |  * - CENTER_VERTICAL (y-coordinate of anchor is in the vertical center of the image) | ||||||
|  |  * - BOTTOM (y-coordinate of anchor is on the bottom of the image) | ||||||
|  |  * | ||||||
|  |  * These options are then combined to create combined TextAlignment options like: | ||||||
|  |  * - TOP_LEFT (default) | ||||||
|  |  * - CENTER (anchor point is in the middle of the image bounds) | ||||||
|  |  * - ... | ||||||
|  |  */ | ||||||
|  | enum class ImageAlign { | ||||||
|  |   TOP = 0x00, | ||||||
|  |   CENTER_VERTICAL = 0x01, | ||||||
|  |   BOTTOM = 0x02, | ||||||
|  |  | ||||||
| enum ImageType { |   LEFT = 0x00, | ||||||
|   IMAGE_TYPE_BINARY = 0, |   CENTER_HORIZONTAL = 0x04, | ||||||
|   IMAGE_TYPE_GRAYSCALE = 1, |   RIGHT = 0x08, | ||||||
|   IMAGE_TYPE_RGB24 = 2, |  | ||||||
|   IMAGE_TYPE_TRANSPARENT_BINARY = 3, |   TOP_LEFT = TOP | LEFT, | ||||||
|   IMAGE_TYPE_RGB565 = 4, |   TOP_CENTER = TOP | CENTER_HORIZONTAL, | ||||||
|  |   TOP_RIGHT = TOP | RIGHT, | ||||||
|  |  | ||||||
|  |   CENTER_LEFT = CENTER_VERTICAL | LEFT, | ||||||
|  |   CENTER = CENTER_VERTICAL | CENTER_HORIZONTAL, | ||||||
|  |   CENTER_RIGHT = CENTER_VERTICAL | RIGHT, | ||||||
|  |  | ||||||
|  |   BOTTOM_LEFT = BOTTOM | LEFT, | ||||||
|  |   BOTTOM_CENTER = BOTTOM | CENTER_HORIZONTAL, | ||||||
|  |   BOTTOM_RIGHT = BOTTOM | RIGHT, | ||||||
|  |  | ||||||
|  |   HORIZONTAL_ALIGNMENT = LEFT | CENTER_HORIZONTAL | RIGHT, | ||||||
|  |   VERTICAL_ALIGNMENT = TOP | CENTER_VERTICAL | BOTTOM | ||||||
| }; | }; | ||||||
|  |  | ||||||
| enum DisplayType { | enum DisplayType { | ||||||
| @@ -126,8 +162,6 @@ class Rect { | |||||||
|   void info(const std::string &prefix = "rect info:"); |   void info(const std::string &prefix = "rect info:"); | ||||||
| }; | }; | ||||||
|  |  | ||||||
| class Font; |  | ||||||
| class Image; |  | ||||||
| class DisplayBuffer; | class DisplayBuffer; | ||||||
| class DisplayPage; | class DisplayPage; | ||||||
| class DisplayOnPageChangeTrigger; | class DisplayOnPageChangeTrigger; | ||||||
| @@ -263,7 +297,6 @@ class DisplayBuffer { | |||||||
|    */ |    */ | ||||||
|   void printf(int x, int y, Font *font, const char *format, ...) __attribute__((format(printf, 5, 6))); |   void printf(int x, int y, Font *font, const char *format, ...) __attribute__((format(printf, 5, 6))); | ||||||
|  |  | ||||||
| #ifdef USE_TIME |  | ||||||
|   /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. |   /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. | ||||||
|    * |    * | ||||||
|    * @param x The x coordinate of the text alignment anchor point. |    * @param x The x coordinate of the text alignment anchor point. | ||||||
| @@ -274,7 +307,7 @@ class DisplayBuffer { | |||||||
|    * @param format The strftime format to use. |    * @param format The strftime format to use. | ||||||
|    * @param time The time to format. |    * @param time The time to format. | ||||||
|    */ |    */ | ||||||
|   void strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, time::ESPTime time) |   void strftime(int x, int y, Font *font, Color color, TextAlign align, const char *format, ESPTime time) | ||||||
|       __attribute__((format(strftime, 7, 0))); |       __attribute__((format(strftime, 7, 0))); | ||||||
|  |  | ||||||
|   /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. |   /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. | ||||||
| @@ -286,7 +319,7 @@ class DisplayBuffer { | |||||||
|    * @param format The strftime format to use. |    * @param format The strftime format to use. | ||||||
|    * @param time The time to format. |    * @param time The time to format. | ||||||
|    */ |    */ | ||||||
|   void strftime(int x, int y, Font *font, Color color, const char *format, time::ESPTime time) |   void strftime(int x, int y, Font *font, Color color, const char *format, ESPTime time) | ||||||
|       __attribute__((format(strftime, 6, 0))); |       __attribute__((format(strftime, 6, 0))); | ||||||
|  |  | ||||||
|   /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. |   /** Evaluate the strftime-format `format` and print the result with the anchor point at [x,y] with `font`. | ||||||
| @@ -298,7 +331,7 @@ class DisplayBuffer { | |||||||
|    * @param format The strftime format to use. |    * @param format The strftime format to use. | ||||||
|    * @param time The time to format. |    * @param time The time to format. | ||||||
|    */ |    */ | ||||||
|   void strftime(int x, int y, Font *font, TextAlign align, const char *format, time::ESPTime time) |   void strftime(int x, int y, Font *font, TextAlign align, const char *format, ESPTime time) | ||||||
|       __attribute__((format(strftime, 6, 0))); |       __attribute__((format(strftime, 6, 0))); | ||||||
|  |  | ||||||
|   /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. |   /** Evaluate the strftime-format `format` and print the result with the top left at [x,y] with `font`. | ||||||
| @@ -309,19 +342,28 @@ class DisplayBuffer { | |||||||
|    * @param format The strftime format to use. |    * @param format The strftime format to use. | ||||||
|    * @param time The time to format. |    * @param time The time to format. | ||||||
|    */ |    */ | ||||||
|   void strftime(int x, int y, Font *font, const char *format, time::ESPTime time) |   void strftime(int x, int y, Font *font, const char *format, ESPTime time) __attribute__((format(strftime, 5, 0))); | ||||||
|       __attribute__((format(strftime, 5, 0))); |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
|   /** Draw the `image` with the top-left corner at [x,y] to the screen. |   /** Draw the `image` with the top-left corner at [x,y] to the screen. | ||||||
|    * |    * | ||||||
|    * @param x The x coordinate of the upper left corner. |    * @param x The x coordinate of the upper left corner. | ||||||
|    * @param y The y coordinate of the upper left corner. |    * @param y The y coordinate of the upper left corner. | ||||||
|    * @param image The image to draw |    * @param image The image to draw. | ||||||
|    * @param color_on The color to replace in binary images for the on bits. |    * @param color_on The color to replace in binary images for the on bits. | ||||||
|    * @param color_off The color to replace in binary images for the off bits. |    * @param color_off The color to replace in binary images for the off bits. | ||||||
|    */ |    */ | ||||||
|   void image(int x, int y, Image *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); |   void image(int x, int y, BaseImage *image, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); | ||||||
|  |  | ||||||
|  |   /** Draw the `image` at [x,y] to the screen. | ||||||
|  |    * | ||||||
|  |    * @param x The x coordinate of the upper left corner. | ||||||
|  |    * @param y The y coordinate of the upper left corner. | ||||||
|  |    * @param image The image to draw. | ||||||
|  |    * @param align The alignment of the image. | ||||||
|  |    * @param color_on The color to replace in binary images for the on bits. | ||||||
|  |    * @param color_off The color to replace in binary images for the off bits. | ||||||
|  |    */ | ||||||
|  |   void image(int x, int y, BaseImage *image, ImageAlign align, Color color_on = COLOR_ON, Color color_off = COLOR_OFF); | ||||||
|  |  | ||||||
| #ifdef USE_GRAPH | #ifdef USE_GRAPH | ||||||
|   /** Draw the `graph` with the top-left corner at [x,y] to the screen. |   /** Draw the `graph` with the top-left corner at [x,y] to the screen. | ||||||
| @@ -481,104 +523,6 @@ class DisplayPage { | |||||||
|   DisplayPage *next_{nullptr}; |   DisplayPage *next_{nullptr}; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| struct GlyphData { |  | ||||||
|   const char *a_char; |  | ||||||
|   const uint8_t *data; |  | ||||||
|   int offset_x; |  | ||||||
|   int offset_y; |  | ||||||
|   int width; |  | ||||||
|   int height; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| class Glyph { |  | ||||||
|  public: |  | ||||||
|   Glyph(const GlyphData *data) : glyph_data_(data) {} |  | ||||||
|  |  | ||||||
|   bool get_pixel(int x, int y) const; |  | ||||||
|  |  | ||||||
|   const char *get_char() const; |  | ||||||
|  |  | ||||||
|   bool compare_to(const char *str) const; |  | ||||||
|  |  | ||||||
|   int match_length(const char *str) const; |  | ||||||
|  |  | ||||||
|   void scan_area(int *x1, int *y1, int *width, int *height) const; |  | ||||||
|  |  | ||||||
|  protected: |  | ||||||
|   friend Font; |  | ||||||
|   friend DisplayBuffer; |  | ||||||
|  |  | ||||||
|   const GlyphData *glyph_data_; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| class Font { |  | ||||||
|  public: |  | ||||||
|   /** Construct the font with the given glyphs. |  | ||||||
|    * |  | ||||||
|    * @param glyphs A vector of glyphs, must be sorted lexicographically. |  | ||||||
|    * @param baseline The y-offset from the top of the text to the baseline. |  | ||||||
|    * @param bottom The y-offset from the top of the text to the bottom (i.e. height). |  | ||||||
|    */ |  | ||||||
|   Font(const GlyphData *data, int data_nr, int baseline, int height); |  | ||||||
|  |  | ||||||
|   int match_next_glyph(const char *str, int *match_length); |  | ||||||
|  |  | ||||||
|   void measure(const char *str, int *width, int *x_offset, int *baseline, int *height); |  | ||||||
|   inline int get_baseline() { return this->baseline_; } |  | ||||||
|   inline int get_height() { return this->height_; } |  | ||||||
|  |  | ||||||
|   const std::vector<Glyph, ExternalRAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; } |  | ||||||
|  |  | ||||||
|  protected: |  | ||||||
|   std::vector<Glyph, ExternalRAMAllocator<Glyph>> glyphs_; |  | ||||||
|   int baseline_; |  | ||||||
|   int height_; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| class Image { |  | ||||||
|  public: |  | ||||||
|   Image(const uint8_t *data_start, int width, int height, ImageType type); |  | ||||||
|   virtual bool get_pixel(int x, int y) const; |  | ||||||
|   virtual Color get_color_pixel(int x, int y) const; |  | ||||||
|   virtual Color get_rgb565_pixel(int x, int y) const; |  | ||||||
|   virtual Color get_grayscale_pixel(int x, int y) const; |  | ||||||
|   int get_width() const; |  | ||||||
|   int get_height() const; |  | ||||||
|   ImageType get_type() const; |  | ||||||
|  |  | ||||||
|   virtual int get_current_frame() const; |  | ||||||
|  |  | ||||||
|  protected: |  | ||||||
|   int width_; |  | ||||||
|   int height_; |  | ||||||
|   ImageType type_; |  | ||||||
|   const uint8_t *data_start_; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| class Animation : public Image { |  | ||||||
|  public: |  | ||||||
|   Animation(const uint8_t *data_start, int width, int height, uint32_t animation_frame_count, ImageType type); |  | ||||||
|   bool get_pixel(int x, int y) const override; |  | ||||||
|   Color get_color_pixel(int x, int y) const override; |  | ||||||
|   Color get_rgb565_pixel(int x, int y) const override; |  | ||||||
|   Color get_grayscale_pixel(int x, int y) const override; |  | ||||||
|  |  | ||||||
|   int get_animation_frame_count() const; |  | ||||||
|   int get_current_frame() const override; |  | ||||||
|   void next_frame(); |  | ||||||
|   void prev_frame(); |  | ||||||
|  |  | ||||||
|   /** Selects a specific frame within the animation. |  | ||||||
|    * |  | ||||||
|    * @param frame If possitive, advance to the frame. If negative, recede to that frame from the end frame. |  | ||||||
|    */ |  | ||||||
|   void set_frame(int frame); |  | ||||||
|  |  | ||||||
|  protected: |  | ||||||
|   int current_frame_; |  | ||||||
|   int animation_frame_count_; |  | ||||||
| }; |  | ||||||
|  |  | ||||||
| template<typename... Ts> class DisplayPageShowAction : public Action<Ts...> { | template<typename... Ts> class DisplayPageShowAction : public Action<Ts...> { | ||||||
|  public: |  public: | ||||||
|   TEMPLATABLE_VALUE(DisplayPage *, page) |   TEMPLATABLE_VALUE(DisplayPage *, page) | ||||||
|   | |||||||
							
								
								
									
										105
									
								
								esphome/components/display/font.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										105
									
								
								esphome/components/display/font.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,105 @@ | |||||||
|  | #include "font.h" | ||||||
|  |  | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace display { | ||||||
|  |  | ||||||
|  | bool Glyph::get_pixel(int x, int y) const { | ||||||
|  |   const int x_data = x - this->glyph_data_->offset_x; | ||||||
|  |   const int y_data = y - this->glyph_data_->offset_y; | ||||||
|  |   if (x_data < 0 || x_data >= this->glyph_data_->width || y_data < 0 || y_data >= this->glyph_data_->height) | ||||||
|  |     return false; | ||||||
|  |   const uint32_t width_8 = ((this->glyph_data_->width + 7u) / 8u) * 8u; | ||||||
|  |   const uint32_t pos = x_data + y_data * width_8; | ||||||
|  |   return progmem_read_byte(this->glyph_data_->data + (pos / 8u)) & (0x80 >> (pos % 8u)); | ||||||
|  | } | ||||||
|  | const char *Glyph::get_char() const { return this->glyph_data_->a_char; } | ||||||
|  | bool Glyph::compare_to(const char *str) const { | ||||||
|  |   // 1 -> this->char_ | ||||||
|  |   // 2 -> str | ||||||
|  |   for (uint32_t i = 0;; i++) { | ||||||
|  |     if (this->glyph_data_->a_char[i] == '\0') | ||||||
|  |       return true; | ||||||
|  |     if (str[i] == '\0') | ||||||
|  |       return false; | ||||||
|  |     if (this->glyph_data_->a_char[i] > str[i]) | ||||||
|  |       return false; | ||||||
|  |     if (this->glyph_data_->a_char[i] < str[i]) | ||||||
|  |       return true; | ||||||
|  |   } | ||||||
|  |   // this should not happen | ||||||
|  |   return false; | ||||||
|  | } | ||||||
|  | int Glyph::match_length(const char *str) const { | ||||||
|  |   for (uint32_t i = 0;; i++) { | ||||||
|  |     if (this->glyph_data_->a_char[i] == '\0') | ||||||
|  |       return i; | ||||||
|  |     if (str[i] != this->glyph_data_->a_char[i]) | ||||||
|  |       return 0; | ||||||
|  |   } | ||||||
|  |   // this should not happen | ||||||
|  |   return 0; | ||||||
|  | } | ||||||
|  | void Glyph::scan_area(int *x1, int *y1, int *width, int *height) const { | ||||||
|  |   *x1 = this->glyph_data_->offset_x; | ||||||
|  |   *y1 = this->glyph_data_->offset_y; | ||||||
|  |   *width = this->glyph_data_->width; | ||||||
|  |   *height = this->glyph_data_->height; | ||||||
|  | } | ||||||
|  | int Font::match_next_glyph(const char *str, int *match_length) { | ||||||
|  |   int lo = 0; | ||||||
|  |   int hi = this->glyphs_.size() - 1; | ||||||
|  |   while (lo != hi) { | ||||||
|  |     int mid = (lo + hi + 1) / 2; | ||||||
|  |     if (this->glyphs_[mid].compare_to(str)) { | ||||||
|  |       lo = mid; | ||||||
|  |     } else { | ||||||
|  |       hi = mid - 1; | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |   *match_length = this->glyphs_[lo].match_length(str); | ||||||
|  |   if (*match_length <= 0) | ||||||
|  |     return -1; | ||||||
|  |   return lo; | ||||||
|  | } | ||||||
|  | void Font::measure(const char *str, int *width, int *x_offset, int *baseline, int *height) { | ||||||
|  |   *baseline = this->baseline_; | ||||||
|  |   *height = this->height_; | ||||||
|  |   int i = 0; | ||||||
|  |   int min_x = 0; | ||||||
|  |   bool has_char = false; | ||||||
|  |   int x = 0; | ||||||
|  |   while (str[i] != '\0') { | ||||||
|  |     int match_length; | ||||||
|  |     int glyph_n = this->match_next_glyph(str + i, &match_length); | ||||||
|  |     if (glyph_n < 0) { | ||||||
|  |       // Unknown char, skip | ||||||
|  |       if (!this->get_glyphs().empty()) | ||||||
|  |         x += this->get_glyphs()[0].glyph_data_->width; | ||||||
|  |       i++; | ||||||
|  |       continue; | ||||||
|  |     } | ||||||
|  |  | ||||||
|  |     const Glyph &glyph = this->glyphs_[glyph_n]; | ||||||
|  |     if (!has_char) { | ||||||
|  |       min_x = glyph.glyph_data_->offset_x; | ||||||
|  |     } else { | ||||||
|  |       min_x = std::min(min_x, x + glyph.glyph_data_->offset_x); | ||||||
|  |     } | ||||||
|  |     x += glyph.glyph_data_->width + glyph.glyph_data_->offset_x; | ||||||
|  |  | ||||||
|  |     i += match_length; | ||||||
|  |     has_char = true; | ||||||
|  |   } | ||||||
|  |   *x_offset = min_x; | ||||||
|  |   *width = x - min_x; | ||||||
|  | } | ||||||
|  | Font::Font(const GlyphData *data, int data_nr, int baseline, int height) : baseline_(baseline), height_(height) { | ||||||
|  |   glyphs_.reserve(data_nr); | ||||||
|  |   for (int i = 0; i < data_nr; ++i) | ||||||
|  |     glyphs_.emplace_back(&data[i]); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | }  // namespace display | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										66
									
								
								esphome/components/display/font.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										66
									
								
								esphome/components/display/font.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,66 @@ | |||||||
|  | #pragma once | ||||||
|  |  | ||||||
|  | #include "esphome/core/datatypes.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace display { | ||||||
|  |  | ||||||
|  | class DisplayBuffer; | ||||||
|  | class Font; | ||||||
|  |  | ||||||
|  | struct GlyphData { | ||||||
|  |   const char *a_char; | ||||||
|  |   const uint8_t *data; | ||||||
|  |   int offset_x; | ||||||
|  |   int offset_y; | ||||||
|  |   int width; | ||||||
|  |   int height; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class Glyph { | ||||||
|  |  public: | ||||||
|  |   Glyph(const GlyphData *data) : glyph_data_(data) {} | ||||||
|  |  | ||||||
|  |   bool get_pixel(int x, int y) const; | ||||||
|  |  | ||||||
|  |   const char *get_char() const; | ||||||
|  |  | ||||||
|  |   bool compare_to(const char *str) const; | ||||||
|  |  | ||||||
|  |   int match_length(const char *str) const; | ||||||
|  |  | ||||||
|  |   void scan_area(int *x1, int *y1, int *width, int *height) const; | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   friend Font; | ||||||
|  |   friend DisplayBuffer; | ||||||
|  |  | ||||||
|  |   const GlyphData *glyph_data_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class Font { | ||||||
|  |  public: | ||||||
|  |   /** Construct the font with the given glyphs. | ||||||
|  |    * | ||||||
|  |    * @param glyphs A vector of glyphs, must be sorted lexicographically. | ||||||
|  |    * @param baseline The y-offset from the top of the text to the baseline. | ||||||
|  |    * @param bottom The y-offset from the top of the text to the bottom (i.e. height). | ||||||
|  |    */ | ||||||
|  |   Font(const GlyphData *data, int data_nr, int baseline, int height); | ||||||
|  |  | ||||||
|  |   int match_next_glyph(const char *str, int *match_length); | ||||||
|  |  | ||||||
|  |   void measure(const char *str, int *width, int *x_offset, int *baseline, int *height); | ||||||
|  |   inline int get_baseline() { return this->baseline_; } | ||||||
|  |   inline int get_height() { return this->height_; } | ||||||
|  |  | ||||||
|  |   const std::vector<Glyph, ExternalRAMAllocator<Glyph>> &get_glyphs() const { return glyphs_; } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   std::vector<Glyph, ExternalRAMAllocator<Glyph>> glyphs_; | ||||||
|  |   int baseline_; | ||||||
|  |   int height_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace display | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										135
									
								
								esphome/components/display/image.cpp
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								esphome/components/display/image.cpp
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,135 @@ | |||||||
|  | #include "image.h" | ||||||
|  |  | ||||||
|  | #include "esphome/core/hal.h" | ||||||
|  | #include "display_buffer.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace display { | ||||||
|  |  | ||||||
|  | void Image::draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) { | ||||||
|  |   switch (type_) { | ||||||
|  |     case IMAGE_TYPE_BINARY: { | ||||||
|  |       for (int img_x = 0; img_x < width_; img_x++) { | ||||||
|  |         for (int img_y = 0; img_y < height_; img_y++) { | ||||||
|  |           if (this->get_binary_pixel_(img_x, img_y)) { | ||||||
|  |             display->draw_pixel_at(x + img_x, y + img_y, color_on); | ||||||
|  |           } else if (!this->transparent_) { | ||||||
|  |             display->draw_pixel_at(x + img_x, y + img_y, color_off); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |     } | ||||||
|  |     case IMAGE_TYPE_GRAYSCALE: | ||||||
|  |       for (int img_x = 0; img_x < width_; img_x++) { | ||||||
|  |         for (int img_y = 0; img_y < height_; img_y++) { | ||||||
|  |           auto color = this->get_grayscale_pixel_(img_x, img_y); | ||||||
|  |           if (color.w >= 0x80) { | ||||||
|  |             display->draw_pixel_at(x + img_x, y + img_y, color); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |     case IMAGE_TYPE_RGB565: | ||||||
|  |       for (int img_x = 0; img_x < width_; img_x++) { | ||||||
|  |         for (int img_y = 0; img_y < height_; img_y++) { | ||||||
|  |           auto color = this->get_rgb565_pixel_(img_x, img_y); | ||||||
|  |           if (color.w >= 0x80) { | ||||||
|  |             display->draw_pixel_at(x + img_x, y + img_y, color); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |     case IMAGE_TYPE_RGB24: | ||||||
|  |       for (int img_x = 0; img_x < width_; img_x++) { | ||||||
|  |         for (int img_y = 0; img_y < height_; img_y++) { | ||||||
|  |           auto color = this->get_rgb24_pixel_(img_x, img_y); | ||||||
|  |           if (color.w >= 0x80) { | ||||||
|  |             display->draw_pixel_at(x + img_x, y + img_y, color); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |     case IMAGE_TYPE_RGBA: | ||||||
|  |       for (int img_x = 0; img_x < width_; img_x++) { | ||||||
|  |         for (int img_y = 0; img_y < height_; img_y++) { | ||||||
|  |           auto color = this->get_rgba_pixel_(img_x, img_y); | ||||||
|  |           if (color.w >= 0x80) { | ||||||
|  |             display->draw_pixel_at(x + img_x, y + img_y, color); | ||||||
|  |           } | ||||||
|  |         } | ||||||
|  |       } | ||||||
|  |       break; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | Color Image::get_pixel(int x, int y, Color color_on, Color color_off) const { | ||||||
|  |   if (x < 0 || x >= this->width_ || y < 0 || y >= this->height_) | ||||||
|  |     return color_off; | ||||||
|  |   switch (this->type_) { | ||||||
|  |     case IMAGE_TYPE_BINARY: | ||||||
|  |       return this->get_binary_pixel_(x, y) ? color_on : color_off; | ||||||
|  |     case IMAGE_TYPE_GRAYSCALE: | ||||||
|  |       return this->get_grayscale_pixel_(x, y); | ||||||
|  |     case IMAGE_TYPE_RGB565: | ||||||
|  |       return this->get_rgb565_pixel_(x, y); | ||||||
|  |     case IMAGE_TYPE_RGB24: | ||||||
|  |       return this->get_rgb24_pixel_(x, y); | ||||||
|  |     case IMAGE_TYPE_RGBA: | ||||||
|  |       return this->get_rgba_pixel_(x, y); | ||||||
|  |     default: | ||||||
|  |       return color_off; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  | bool Image::get_binary_pixel_(int x, int y) const { | ||||||
|  |   const uint32_t width_8 = ((this->width_ + 7u) / 8u) * 8u; | ||||||
|  |   const uint32_t pos = x + y * width_8; | ||||||
|  |   return progmem_read_byte(this->data_start_ + (pos / 8u)) & (0x80 >> (pos % 8u)); | ||||||
|  | } | ||||||
|  | Color Image::get_rgba_pixel_(int x, int y) const { | ||||||
|  |   const uint32_t pos = (x + y * this->width_) * 4; | ||||||
|  |   return Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), | ||||||
|  |                progmem_read_byte(this->data_start_ + pos + 2), progmem_read_byte(this->data_start_ + pos + 3)); | ||||||
|  | } | ||||||
|  | Color Image::get_rgb24_pixel_(int x, int y) const { | ||||||
|  |   const uint32_t pos = (x + y * this->width_) * 3; | ||||||
|  |   Color color = Color(progmem_read_byte(this->data_start_ + pos + 0), progmem_read_byte(this->data_start_ + pos + 1), | ||||||
|  |                       progmem_read_byte(this->data_start_ + pos + 2)); | ||||||
|  |   if (color.b == 1 && color.r == 0 && color.g == 0 && transparent_) { | ||||||
|  |     // (0, 0, 1) has been defined as transparent color for non-alpha images. | ||||||
|  |     // putting blue == 1 as a first condition for performance reasons (least likely value to short-cut the if) | ||||||
|  |     color.w = 0; | ||||||
|  |   } else { | ||||||
|  |     color.w = 0xFF; | ||||||
|  |   } | ||||||
|  |   return color; | ||||||
|  | } | ||||||
|  | Color Image::get_rgb565_pixel_(int x, int y) const { | ||||||
|  |   const uint32_t pos = (x + y * this->width_) * 2; | ||||||
|  |   uint16_t rgb565 = | ||||||
|  |       progmem_read_byte(this->data_start_ + pos + 0) << 8 | progmem_read_byte(this->data_start_ + pos + 1); | ||||||
|  |   auto r = (rgb565 & 0xF800) >> 11; | ||||||
|  |   auto g = (rgb565 & 0x07E0) >> 5; | ||||||
|  |   auto b = rgb565 & 0x001F; | ||||||
|  |   Color color = Color((r << 3) | (r >> 2), (g << 2) | (g >> 4), (b << 3) | (b >> 2)); | ||||||
|  |   if (rgb565 == 0x0020 && transparent_) { | ||||||
|  |     // darkest green has been defined as transparent color for transparent RGB565 images. | ||||||
|  |     color.w = 0; | ||||||
|  |   } else { | ||||||
|  |     color.w = 0xFF; | ||||||
|  |   } | ||||||
|  |   return color; | ||||||
|  | } | ||||||
|  | Color Image::get_grayscale_pixel_(int x, int y) const { | ||||||
|  |   const uint32_t pos = (x + y * this->width_); | ||||||
|  |   const uint8_t gray = progmem_read_byte(this->data_start_ + pos); | ||||||
|  |   uint8_t alpha = (gray == 1 && transparent_) ? 0 : 0xFF; | ||||||
|  |   return Color(gray, gray, gray, alpha); | ||||||
|  | } | ||||||
|  | int Image::get_width() const { return this->width_; } | ||||||
|  | int Image::get_height() const { return this->height_; } | ||||||
|  | ImageType Image::get_type() const { return this->type_; } | ||||||
|  | Image::Image(const uint8_t *data_start, int width, int height, ImageType type) | ||||||
|  |     : width_(width), height_(height), type_(type), data_start_(data_start) {} | ||||||
|  |  | ||||||
|  | }  // namespace display | ||||||
|  | }  // namespace esphome | ||||||
							
								
								
									
										75
									
								
								esphome/components/display/image.h
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										75
									
								
								esphome/components/display/image.h
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,75 @@ | |||||||
|  | #pragma once | ||||||
|  | #include "esphome/core/color.h" | ||||||
|  |  | ||||||
|  | namespace esphome { | ||||||
|  | namespace display { | ||||||
|  |  | ||||||
|  | enum ImageType { | ||||||
|  |   IMAGE_TYPE_BINARY = 0, | ||||||
|  |   IMAGE_TYPE_GRAYSCALE = 1, | ||||||
|  |   IMAGE_TYPE_RGB24 = 2, | ||||||
|  |   IMAGE_TYPE_RGB565 = 3, | ||||||
|  |   IMAGE_TYPE_RGBA = 4, | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | inline int image_type_to_bpp(ImageType type) { | ||||||
|  |   switch (type) { | ||||||
|  |     case IMAGE_TYPE_BINARY: | ||||||
|  |       return 1; | ||||||
|  |     case IMAGE_TYPE_GRAYSCALE: | ||||||
|  |       return 8; | ||||||
|  |     case IMAGE_TYPE_RGB565: | ||||||
|  |       return 16; | ||||||
|  |     case IMAGE_TYPE_RGB24: | ||||||
|  |       return 24; | ||||||
|  |     case IMAGE_TYPE_RGBA: | ||||||
|  |       return 32; | ||||||
|  |   } | ||||||
|  |   return 0; | ||||||
|  | } | ||||||
|  |  | ||||||
|  | inline int image_type_to_width_stride(int width, ImageType type) { return (width * image_type_to_bpp(type) + 7u) / 8u; } | ||||||
|  |  | ||||||
|  | /// Turn the pixel OFF. | ||||||
|  | extern const Color COLOR_OFF; | ||||||
|  | /// Turn the pixel ON. | ||||||
|  | extern const Color COLOR_ON; | ||||||
|  |  | ||||||
|  | class DisplayBuffer; | ||||||
|  |  | ||||||
|  | class BaseImage { | ||||||
|  |  public: | ||||||
|  |   virtual void draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) = 0; | ||||||
|  |   virtual int get_width() const = 0; | ||||||
|  |   virtual int get_height() const = 0; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class Image : public BaseImage { | ||||||
|  |  public: | ||||||
|  |   Image(const uint8_t *data_start, int width, int height, ImageType type); | ||||||
|  |   Color get_pixel(int x, int y, Color color_on = COLOR_ON, Color color_off = COLOR_OFF) const; | ||||||
|  |   int get_width() const override; | ||||||
|  |   int get_height() const override; | ||||||
|  |   ImageType get_type() const; | ||||||
|  |  | ||||||
|  |   void draw(int x, int y, DisplayBuffer *display, Color color_on, Color color_off) override; | ||||||
|  |  | ||||||
|  |   void set_transparency(bool transparent) { transparent_ = transparent; } | ||||||
|  |   bool has_transparency() const { return transparent_; } | ||||||
|  |  | ||||||
|  |  protected: | ||||||
|  |   bool get_binary_pixel_(int x, int y) const; | ||||||
|  |   Color get_rgb24_pixel_(int x, int y) const; | ||||||
|  |   Color get_rgba_pixel_(int x, int y) const; | ||||||
|  |   Color get_rgb565_pixel_(int x, int y) const; | ||||||
|  |   Color get_grayscale_pixel_(int x, int y) const; | ||||||
|  |  | ||||||
|  |   int width_; | ||||||
|  |   int height_; | ||||||
|  |   ImageType type_; | ||||||
|  |   const uint8_t *data_start_; | ||||||
|  |   bool transparent_; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | }  // namespace display | ||||||
|  | }  // namespace esphome | ||||||
| @@ -37,7 +37,7 @@ void DS1307Component::read_time() { | |||||||
|     ESP_LOGW(TAG, "RTC halted, not syncing to system clock."); |     ESP_LOGW(TAG, "RTC halted, not syncing to system clock."); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|   time::ESPTime rtc_time{.second = uint8_t(ds1307_.reg.second + 10 * ds1307_.reg.second_10), |   ESPTime rtc_time{.second = uint8_t(ds1307_.reg.second + 10 * ds1307_.reg.second_10), | ||||||
|                    .minute = uint8_t(ds1307_.reg.minute + 10u * ds1307_.reg.minute_10), |                    .minute = uint8_t(ds1307_.reg.minute + 10u * ds1307_.reg.minute_10), | ||||||
|                    .hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10), |                    .hour = uint8_t(ds1307_.reg.hour + 10u * ds1307_.reg.hour_10), | ||||||
|                    .day_of_week = uint8_t(ds1307_.reg.weekday), |                    .day_of_week = uint8_t(ds1307_.reg.weekday), | ||||||
|   | |||||||
| @@ -4,6 +4,7 @@ from esphome.components.light.types import AddressableLightEffect | |||||||
| from esphome.components.light.effects import register_addressable_effect | from esphome.components.light.effects import register_addressable_effect | ||||||
| from esphome.const import CONF_ID, CONF_NAME, CONF_METHOD, CONF_CHANNELS | from esphome.const import CONF_ID, CONF_NAME, CONF_METHOD, CONF_CHANNELS | ||||||
|  |  | ||||||
|  | AUTO_LOAD = ["socket"] | ||||||
| DEPENDENCIES = ["network"] | DEPENDENCIES = ["network"] | ||||||
|  |  | ||||||
| e131_ns = cg.esphome_ns.namespace("e131") | e131_ns = cg.esphome_ns.namespace("e131") | ||||||
| @@ -23,16 +24,11 @@ CHANNELS = { | |||||||
| CONF_UNIVERSE = "universe" | CONF_UNIVERSE = "universe" | ||||||
| CONF_E131_ID = "e131_id" | CONF_E131_ID = "e131_id" | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.All( | CONFIG_SCHEMA = cv.Schema( | ||||||
|     cv.Schema( |  | ||||||
|     { |     { | ||||||
|         cv.GenerateID(): cv.declare_id(E131Component), |         cv.GenerateID(): cv.declare_id(E131Component), | ||||||
|             cv.Optional(CONF_METHOD, default="MULTICAST"): cv.one_of( |         cv.Optional(CONF_METHOD, default="MULTICAST"): cv.one_of(*METHODS, upper=True), | ||||||
|                 *METHODS, upper=True |  | ||||||
|             ), |  | ||||||
|     } |     } | ||||||
|     ), |  | ||||||
|     cv.only_with_arduino, |  | ||||||
| ) | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|   | |||||||
| @@ -1,18 +1,7 @@ | |||||||
| #ifdef USE_ARDUINO |  | ||||||
|  |  | ||||||
| #include "e131.h" | #include "e131.h" | ||||||
| #include "e131_addressable_light_effect.h" | #include "e131_addressable_light_effect.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
| #ifdef USE_ESP32 |  | ||||||
| #include <WiFi.h> |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| #ifdef USE_ESP8266 |  | ||||||
| #include <ESP8266WiFi.h> |  | ||||||
| #include <WiFiUdp.h> |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace e131 { | namespace e131 { | ||||||
|  |  | ||||||
| @@ -22,17 +11,41 @@ static const int PORT = 5568; | |||||||
| E131Component::E131Component() {} | E131Component::E131Component() {} | ||||||
|  |  | ||||||
| E131Component::~E131Component() { | E131Component::~E131Component() { | ||||||
|   if (udp_) { |   if (this->socket_) { | ||||||
|     udp_->stop(); |     this->socket_->close(); | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
| void E131Component::setup() { | void E131Component::setup() { | ||||||
|   udp_ = make_unique<WiFiUDP>(); |   this->socket_ = socket::socket_ip(SOCK_DGRAM, IPPROTO_IP); | ||||||
|  |  | ||||||
|   if (!udp_->begin(PORT)) { |   int enable = 1; | ||||||
|     ESP_LOGE(TAG, "Cannot bind E131 to %d.", PORT); |   int err = this->socket_->setsockopt(SOL_SOCKET, SO_REUSEADDR, &enable, sizeof(int)); | ||||||
|     mark_failed(); |   if (err != 0) { | ||||||
|  |     ESP_LOGW(TAG, "Socket unable to set reuseaddr: errno %d", err); | ||||||
|  |     // we can still continue | ||||||
|  |   } | ||||||
|  |   err = this->socket_->setblocking(false); | ||||||
|  |   if (err != 0) { | ||||||
|  |     ESP_LOGW(TAG, "Socket unable to set nonblocking mode: errno %d", err); | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   struct sockaddr_storage server; | ||||||
|  |  | ||||||
|  |   socklen_t sl = socket::set_sockaddr_any((struct sockaddr *) &server, sizeof(server), PORT); | ||||||
|  |   if (sl == 0) { | ||||||
|  |     ESP_LOGW(TAG, "Socket unable to set sockaddr: errno %d", errno); | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   server.ss_family = AF_INET; | ||||||
|  |  | ||||||
|  |   err = this->socket_->bind((struct sockaddr *) &server, sizeof(server)); | ||||||
|  |   if (err != 0) { | ||||||
|  |     ESP_LOGW(TAG, "Socket unable to bind: errno %d", errno); | ||||||
|  |     this->mark_failed(); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
| @@ -43,24 +56,24 @@ void E131Component::loop() { | |||||||
|   std::vector<uint8_t> payload; |   std::vector<uint8_t> payload; | ||||||
|   E131Packet packet; |   E131Packet packet; | ||||||
|   int universe = 0; |   int universe = 0; | ||||||
|  |   uint8_t buf[1460]; | ||||||
|  |  | ||||||
|   while (uint16_t packet_size = udp_->parsePacket()) { |   ssize_t len = this->socket_->read(buf, sizeof(buf)); | ||||||
|     payload.resize(packet_size); |   if (len == -1) { | ||||||
|  |     return; | ||||||
|     if (!udp_->read(&payload[0], payload.size())) { |  | ||||||
|       continue; |  | ||||||
|   } |   } | ||||||
|  |   payload.resize(len); | ||||||
|  |   memmove(&payload[0], buf, len); | ||||||
|  |  | ||||||
|     if (!packet_(payload, universe, packet)) { |   if (!this->packet_(payload, universe, packet)) { | ||||||
|     ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size()); |     ESP_LOGV(TAG, "Invalid packet received of size %zu.", payload.size()); | ||||||
|       continue; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|     if (!process_(universe, packet)) { |   if (!this->process_(universe, packet)) { | ||||||
|     ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); |     ESP_LOGV(TAG, "Ignored packet for %d universe of size %d.", universe, packet.count); | ||||||
|   } |   } | ||||||
| } | } | ||||||
| } |  | ||||||
|  |  | ||||||
| void E131Component::add_effect(E131AddressableLightEffect *light_effect) { | void E131Component::add_effect(E131AddressableLightEffect *light_effect) { | ||||||
|   if (light_effects_.count(light_effect)) { |   if (light_effects_.count(light_effect)) { | ||||||
| @@ -106,5 +119,3 @@ bool E131Component::process_(int universe, const E131Packet &packet) { | |||||||
|  |  | ||||||
| }  // namespace e131 | }  // namespace e131 | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|  |  | ||||||
| #endif  // USE_ARDUINO |  | ||||||
|   | |||||||
| @@ -1,7 +1,6 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #ifdef USE_ARDUINO | #include "esphome/components/socket/socket.h" | ||||||
|  |  | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
|  |  | ||||||
| #include <map> | #include <map> | ||||||
| @@ -9,8 +8,6 @@ | |||||||
| #include <set> | #include <set> | ||||||
| #include <vector> | #include <vector> | ||||||
|  |  | ||||||
| class UDP; |  | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace e131 { | namespace e131 { | ||||||
|  |  | ||||||
| @@ -47,7 +44,7 @@ class E131Component : public esphome::Component { | |||||||
|   void leave_(int universe); |   void leave_(int universe); | ||||||
|  |  | ||||||
|   E131ListenMethod listen_method_{E131_MULTICAST}; |   E131ListenMethod listen_method_{E131_MULTICAST}; | ||||||
|   std::unique_ptr<UDP> udp_; |   std::unique_ptr<socket::Socket> socket_; | ||||||
|   std::set<E131AddressableLightEffect *> light_effects_; |   std::set<E131AddressableLightEffect *> light_effects_; | ||||||
|   std::map<int, int> universe_consumers_; |   std::map<int, int> universe_consumers_; | ||||||
|   std::map<int, E131Packet> universe_packets_; |   std::map<int, E131Packet> universe_packets_; | ||||||
| @@ -55,5 +52,3 @@ class E131Component : public esphome::Component { | |||||||
|  |  | ||||||
| }  // namespace e131 | }  // namespace e131 | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|  |  | ||||||
| #endif  // USE_ARDUINO |  | ||||||
|   | |||||||
| @@ -1,7 +1,5 @@ | |||||||
| #ifdef USE_ARDUINO |  | ||||||
|  |  | ||||||
| #include "e131.h" |  | ||||||
| #include "e131_addressable_light_effect.h" | #include "e131_addressable_light_effect.h" | ||||||
|  | #include "e131.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| @@ -92,5 +90,3 @@ bool E131AddressableLightEffect::process_(int universe, const E131Packet &packet | |||||||
|  |  | ||||||
| }  // namespace e131 | }  // namespace e131 | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|  |  | ||||||
| #endif  // USE_ARDUINO |  | ||||||
|   | |||||||
| @@ -1,7 +1,5 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #ifdef USE_ARDUINO |  | ||||||
|  |  | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/components/light/addressable_light_effect.h" | #include "esphome/components/light/addressable_light_effect.h" | ||||||
|  |  | ||||||
| @@ -44,5 +42,3 @@ class E131AddressableLightEffect : public light::AddressableLightEffect { | |||||||
|  |  | ||||||
| }  // namespace e131 | }  // namespace e131 | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|  |  | ||||||
| #endif  // USE_ARDUINO |  | ||||||
|   | |||||||
| @@ -1,15 +1,13 @@ | |||||||
| #ifdef USE_ARDUINO | #include <cstring> | ||||||
|  |  | ||||||
| #include "e131.h" | #include "e131.h" | ||||||
|  | #include "esphome/components/network/ip_address.h" | ||||||
| #include "esphome/core/log.h" | #include "esphome/core/log.h" | ||||||
| #include "esphome/core/util.h" | #include "esphome/core/util.h" | ||||||
| #include "esphome/components/network/ip_address.h" |  | ||||||
| #include <cstring> |  | ||||||
|  |  | ||||||
| #include <lwip/init.h> |  | ||||||
| #include <lwip/ip_addr.h> |  | ||||||
| #include <lwip/ip4_addr.h> |  | ||||||
| #include <lwip/igmp.h> | #include <lwip/igmp.h> | ||||||
|  | #include <lwip/init.h> | ||||||
|  | #include <lwip/ip4_addr.h> | ||||||
|  | #include <lwip/ip_addr.h> | ||||||
|  |  | ||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace e131 { | namespace e131 { | ||||||
| @@ -62,7 +60,7 @@ const size_t E131_MIN_PACKET_SIZE = reinterpret_cast<size_t>(&((E131RawPacket *) | |||||||
| bool E131Component::join_igmp_groups_() { | bool E131Component::join_igmp_groups_() { | ||||||
|   if (listen_method_ != E131_MULTICAST) |   if (listen_method_ != E131_MULTICAST) | ||||||
|     return false; |     return false; | ||||||
|   if (!udp_) |   if (this->socket_ == nullptr) | ||||||
|     return false; |     return false; | ||||||
|  |  | ||||||
|   for (auto universe : universe_consumers_) { |   for (auto universe : universe_consumers_) { | ||||||
| @@ -140,5 +138,3 @@ bool E131Component::packet_(const std::vector<uint8_t> &data, int &universe, E13 | |||||||
|  |  | ||||||
| }  // namespace e131 | }  // namespace e131 | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|  |  | ||||||
| #endif  // USE_ARDUINO |  | ||||||
|   | |||||||
| @@ -42,6 +42,39 @@ ESP32_BASE_PINS = { | |||||||
| } | } | ||||||
|  |  | ||||||
| ESP32_BOARD_PINS = { | ESP32_BOARD_PINS = { | ||||||
|  |     "adafruit_feather_esp32s2_tft": { | ||||||
|  |         "BUTTON": 0, | ||||||
|  |         "A0": 18, | ||||||
|  |         "A1": 17, | ||||||
|  |         "A2": 16, | ||||||
|  |         "A3": 15, | ||||||
|  |         "A4": 14, | ||||||
|  |         "A5": 8, | ||||||
|  |         "SCK": 36, | ||||||
|  |         "MOSI": 35, | ||||||
|  |         "MISO": 37, | ||||||
|  |         "RX": 2, | ||||||
|  |         "TX": 1, | ||||||
|  |         "D13": 13, | ||||||
|  |         "D12": 12, | ||||||
|  |         "D11": 11, | ||||||
|  |         "D10": 10, | ||||||
|  |         "D9": 9, | ||||||
|  |         "D6": 6, | ||||||
|  |         "D5": 5, | ||||||
|  |         "NEOPIXEL": 33, | ||||||
|  |         "PIN_NEOPIXEL": 33, | ||||||
|  |         "NEOPIXEL_POWER": 34, | ||||||
|  |         "SCL": 41, | ||||||
|  |         "SDA": 42, | ||||||
|  |         "TFT_I2C_POWER": 21, | ||||||
|  |         "TFT_CS": 7, | ||||||
|  |         "TFT_DC": 39, | ||||||
|  |         "TFT_RESET": 40, | ||||||
|  |         "TFT_BACKLIGHT": 45, | ||||||
|  |         "LED": 13, | ||||||
|  |         "LED_BUILTIN": 13, | ||||||
|  |     }, | ||||||
|     "adafruit_qtpy_esp32c3": { |     "adafruit_qtpy_esp32c3": { | ||||||
|         "A0": 4, |         "A0": 4, | ||||||
|         "A1": 3, |         "A1": 3, | ||||||
|   | |||||||
| @@ -244,6 +244,17 @@ void ESP32BLE::dump_config() { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { | ||||||
|  |   uint64_t u = 0; | ||||||
|  |   u |= uint64_t(address[0] & 0xFF) << 40; | ||||||
|  |   u |= uint64_t(address[1] & 0xFF) << 32; | ||||||
|  |   u |= uint64_t(address[2] & 0xFF) << 24; | ||||||
|  |   u |= uint64_t(address[3] & 0xFF) << 16; | ||||||
|  |   u |= uint64_t(address[4] & 0xFF) << 8; | ||||||
|  |   u |= uint64_t(address[5] & 0xFF) << 0; | ||||||
|  |   return u; | ||||||
|  | } | ||||||
|  |  | ||||||
| ESP32BLE *global_ble = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ESP32BLE *global_ble = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||||
|  |  | ||||||
| }  // namespace esp32_ble | }  // namespace esp32_ble | ||||||
|   | |||||||
| @@ -18,6 +18,8 @@ | |||||||
| namespace esphome { | namespace esphome { | ||||||
| namespace esp32_ble { | namespace esp32_ble { | ||||||
|  |  | ||||||
|  | uint64_t ble_addr_to_uint64(const esp_bd_addr_t address); | ||||||
|  |  | ||||||
| // NOLINTNEXTLINE(modernize-use-using) | // NOLINTNEXTLINE(modernize-use-using) | ||||||
| typedef struct { | typedef struct { | ||||||
|   void *peer_device; |   void *peer_device; | ||||||
|   | |||||||
| @@ -167,7 +167,7 @@ CONFIG_SCHEMA = cv.Schema( | |||||||
|         cv.Optional(CONF_ON_BLE_ADVERTISE): automation.validate_automation( |         cv.Optional(CONF_ON_BLE_ADVERTISE): automation.validate_automation( | ||||||
|             { |             { | ||||||
|                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPBTAdvertiseTrigger), |                 cv.GenerateID(CONF_TRIGGER_ID): cv.declare_id(ESPBTAdvertiseTrigger), | ||||||
|                 cv.Optional(CONF_MAC_ADDRESS): cv.mac_address, |                 cv.Optional(CONF_MAC_ADDRESS): cv.ensure_list(cv.mac_address), | ||||||
|             } |             } | ||||||
|         ), |         ), | ||||||
|         cv.Optional(CONF_ON_BLE_SERVICE_DATA_ADVERTISE): automation.validate_automation( |         cv.Optional(CONF_ON_BLE_SERVICE_DATA_ADVERTISE): automation.validate_automation( | ||||||
| @@ -223,7 +223,10 @@ async def to_code(config): | |||||||
|     for conf in config.get(CONF_ON_BLE_ADVERTISE, []): |     for conf in config.get(CONF_ON_BLE_ADVERTISE, []): | ||||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) |         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||||
|         if CONF_MAC_ADDRESS in conf: |         if CONF_MAC_ADDRESS in conf: | ||||||
|             cg.add(trigger.set_address(conf[CONF_MAC_ADDRESS].as_hex)) |             addr_list = [] | ||||||
|  |             for it in conf[CONF_MAC_ADDRESS]: | ||||||
|  |                 addr_list.append(it.as_hex) | ||||||
|  |             cg.add(trigger.set_addresses(addr_list)) | ||||||
|         await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf) |         await automation.build_automation(trigger, [(ESPBTDeviceConstRef, "x")], conf) | ||||||
|     for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []): |     for conf in config.get(CONF_ON_BLE_SERVICE_DATA_ADVERTISE, []): | ||||||
|         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) |         trigger = cg.new_Pvariable(conf[CONF_TRIGGER_ID], var) | ||||||
|   | |||||||
| @@ -10,18 +10,22 @@ namespace esp32_ble_tracker { | |||||||
| class ESPBTAdvertiseTrigger : public Trigger<const ESPBTDevice &>, public ESPBTDeviceListener { | class ESPBTAdvertiseTrigger : public Trigger<const ESPBTDevice &>, public ESPBTDeviceListener { | ||||||
|  public: |  public: | ||||||
|   explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } |   explicit ESPBTAdvertiseTrigger(ESP32BLETracker *parent) { parent->register_listener(this); } | ||||||
|   void set_address(uint64_t address) { this->address_ = address; } |   void set_addresses(const std::vector<uint64_t> &addresses) { this->address_vec_ = addresses; } | ||||||
|  |  | ||||||
|   bool parse_device(const ESPBTDevice &device) override { |   bool parse_device(const ESPBTDevice &device) override { | ||||||
|     if (this->address_ && device.address_uint64() != this->address_) { |     uint64_t u64_addr = device.address_uint64(); | ||||||
|  |     if (!address_vec_.empty()) { | ||||||
|  |       if (std::find(address_vec_.begin(), address_vec_.end(), u64_addr) == address_vec_.end()) { | ||||||
|         return false; |         return false; | ||||||
|       } |       } | ||||||
|  |     } | ||||||
|  |  | ||||||
|     this->trigger(device); |     this->trigger(device); | ||||||
|     return true; |     return true; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   uint64_t address_ = 0; |   std::vector<uint64_t> address_vec_; | ||||||
| }; | }; | ||||||
|  |  | ||||||
| class BLEServiceDataAdvertiseTrigger : public Trigger<const adv_data_t &>, public ESPBTDeviceListener { | class BLEServiceDataAdvertiseTrigger : public Trigger<const adv_data_t &>, public ESPBTDeviceListener { | ||||||
|   | |||||||
| @@ -34,17 +34,6 @@ static const char *const TAG = "esp32_ble_tracker"; | |||||||
|  |  | ||||||
| ESP32BLETracker *global_esp32_ble_tracker = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ESP32BLETracker *global_esp32_ble_tracker = nullptr;  // NOLINT(cppcoreguidelines-avoid-non-const-global-variables) | ||||||
|  |  | ||||||
| uint64_t ble_addr_to_uint64(const esp_bd_addr_t address) { |  | ||||||
|   uint64_t u = 0; |  | ||||||
|   u |= uint64_t(address[0] & 0xFF) << 40; |  | ||||||
|   u |= uint64_t(address[1] & 0xFF) << 32; |  | ||||||
|   u |= uint64_t(address[2] & 0xFF) << 24; |  | ||||||
|   u |= uint64_t(address[3] & 0xFF) << 16; |  | ||||||
|   u |= uint64_t(address[4] & 0xFF) << 8; |  | ||||||
|   u |= uint64_t(address[5] & 0xFF) << 0; |  | ||||||
|   return u; |  | ||||||
| } |  | ||||||
|  |  | ||||||
| float ESP32BLETracker::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } | float ESP32BLETracker::get_setup_priority() const { return setup_priority::AFTER_BLUETOOTH; } | ||||||
|  |  | ||||||
| void ESP32BLETracker::setup() { | void ESP32BLETracker::setup() { | ||||||
| @@ -114,10 +103,20 @@ void ESP32BLETracker::loop() { | |||||||
|     if (this->scan_result_index_ &&  // if it looks like we have a scan result we will take the lock |     if (this->scan_result_index_ &&  // if it looks like we have a scan result we will take the lock | ||||||
|         xSemaphoreTake(this->scan_result_lock_, 5L / portTICK_PERIOD_MS)) { |         xSemaphoreTake(this->scan_result_lock_, 5L / portTICK_PERIOD_MS)) { | ||||||
|       uint32_t index = this->scan_result_index_; |       uint32_t index = this->scan_result_index_; | ||||||
|       if (index) { |  | ||||||
|       if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { |       if (index >= ESP32BLETracker::SCAN_RESULT_BUFFER_SIZE) { | ||||||
|         ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); |         ESP_LOGW(TAG, "Too many BLE events to process. Some devices may not show up."); | ||||||
|       } |       } | ||||||
|  |  | ||||||
|  |       bool bulk_parsed = false; | ||||||
|  |  | ||||||
|  |       for (auto *listener : this->listeners_) { | ||||||
|  |         bulk_parsed |= listener->parse_devices(this->scan_result_buffer_, this->scan_result_index_); | ||||||
|  |       } | ||||||
|  |       for (auto *client : this->clients_) { | ||||||
|  |         bulk_parsed |= client->parse_devices(this->scan_result_buffer_, this->scan_result_index_); | ||||||
|  |       } | ||||||
|  |  | ||||||
|  |       if (!bulk_parsed) { | ||||||
|         for (size_t i = 0; i < index; i++) { |         for (size_t i = 0; i < index; i++) { | ||||||
|           ESPBTDevice device; |           ESPBTDevice device; | ||||||
|           device.parse_scan_rst(this->scan_result_buffer_[i]); |           device.parse_scan_rst(this->scan_result_buffer_[i]); | ||||||
| @@ -141,8 +140,8 @@ void ESP32BLETracker::loop() { | |||||||
|             this->print_bt_device_info(device); |             this->print_bt_device_info(device); | ||||||
|           } |           } | ||||||
|         } |         } | ||||||
|         this->scan_result_index_ = 0; |  | ||||||
|       } |       } | ||||||
|  |       this->scan_result_index_ = 0; | ||||||
|       xSemaphoreGive(this->scan_result_lock_); |       xSemaphoreGive(this->scan_result_lock_); | ||||||
|     } |     } | ||||||
|  |  | ||||||
| @@ -585,7 +584,7 @@ std::string ESPBTDevice::address_str() const { | |||||||
|            this->address_[3], this->address_[4], this->address_[5]); |            this->address_[3], this->address_[4], this->address_[5]); | ||||||
|   return mac; |   return mac; | ||||||
| } | } | ||||||
| uint64_t ESPBTDevice::address_uint64() const { return ble_addr_to_uint64(this->address_); } | uint64_t ESPBTDevice::address_uint64() const { return esp32_ble::ble_addr_to_uint64(this->address_); } | ||||||
|  |  | ||||||
| void ESP32BLETracker::dump_config() { | void ESP32BLETracker::dump_config() { | ||||||
|   ESP_LOGCONFIG(TAG, "BLE Tracker:"); |   ESP_LOGCONFIG(TAG, "BLE Tracker:"); | ||||||
|   | |||||||
| @@ -113,6 +113,9 @@ class ESPBTDeviceListener { | |||||||
|  public: |  public: | ||||||
|   virtual void on_scan_end() {} |   virtual void on_scan_end() {} | ||||||
|   virtual bool parse_device(const ESPBTDevice &device) = 0; |   virtual bool parse_device(const ESPBTDevice &device) = 0; | ||||||
|  |   virtual bool parse_devices(esp_ble_gap_cb_param_t::ble_scan_result_evt_param *advertisements, size_t count) { | ||||||
|  |     return false; | ||||||
|  |   }; | ||||||
|   void set_parent(ESP32BLETracker *parent) { parent_ = parent; } |   void set_parent(ESP32BLETracker *parent) { parent_ = parent; } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   | |||||||
| @@ -1,3 +1,4 @@ | |||||||
|  | #include <cinttypes> | ||||||
| #include "led_strip.h" | #include "led_strip.h" | ||||||
|  |  | ||||||
| #ifdef USE_ESP32 | #ifdef USE_ESP32 | ||||||
| @@ -195,7 +196,7 @@ void ESP32RMTLEDStripLightOutput::dump_config() { | |||||||
|       break; |       break; | ||||||
|   } |   } | ||||||
|   ESP_LOGCONFIG(TAG, "  RGB Order: %s", rgb_order); |   ESP_LOGCONFIG(TAG, "  RGB Order: %s", rgb_order); | ||||||
|   ESP_LOGCONFIG(TAG, "  Max refresh rate: %u", *this->max_refresh_rate_); |   ESP_LOGCONFIG(TAG, "  Max refresh rate: %" PRIu32, *this->max_refresh_rate_); | ||||||
|   ESP_LOGCONFIG(TAG, "  Number of LEDs: %u", this->num_leds_); |   ESP_LOGCONFIG(TAG, "  Number of LEDs: %u", this->num_leds_); | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -151,11 +151,13 @@ void FujitsuGeneralClimate::transmit_state() { | |||||||
|     case climate::CLIMATE_FAN_LOW: |     case climate::CLIMATE_FAN_LOW: | ||||||
|       SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_LOW); |       SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_LOW); | ||||||
|       break; |       break; | ||||||
|  |     case climate::CLIMATE_FAN_QUIET: | ||||||
|  |       SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_SILENT); | ||||||
|  |       break; | ||||||
|     case climate::CLIMATE_FAN_AUTO: |     case climate::CLIMATE_FAN_AUTO: | ||||||
|     default: |     default: | ||||||
|       SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_AUTO); |       SET_NIBBLE(remote_state, FUJITSU_GENERAL_FAN_NIBBLE, FUJITSU_GENERAL_FAN_AUTO); | ||||||
|       break; |       break; | ||||||
|       // TODO Quiet / Silent |  | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   // Set swing |   // Set swing | ||||||
| @@ -345,8 +347,9 @@ bool FujitsuGeneralClimate::on_receive(remote_base::RemoteReceiveData data) { | |||||||
|     const uint8_t recv_fan_mode = GET_NIBBLE(recv_message, FUJITSU_GENERAL_FAN_NIBBLE); |     const uint8_t recv_fan_mode = GET_NIBBLE(recv_message, FUJITSU_GENERAL_FAN_NIBBLE); | ||||||
|     ESP_LOGV(TAG, "Received fan mode %X", recv_fan_mode); |     ESP_LOGV(TAG, "Received fan mode %X", recv_fan_mode); | ||||||
|     switch (recv_fan_mode) { |     switch (recv_fan_mode) { | ||||||
|       // TODO No Quiet / Silent in ESPH |  | ||||||
|       case FUJITSU_GENERAL_FAN_SILENT: |       case FUJITSU_GENERAL_FAN_SILENT: | ||||||
|  |         this->fan_mode = climate::CLIMATE_FAN_QUIET; | ||||||
|  |         break; | ||||||
|       case FUJITSU_GENERAL_FAN_LOW: |       case FUJITSU_GENERAL_FAN_LOW: | ||||||
|         this->fan_mode = climate::CLIMATE_FAN_LOW; |         this->fan_mode = climate::CLIMATE_FAN_LOW; | ||||||
|         break; |         break; | ||||||
|   | |||||||
| @@ -52,7 +52,7 @@ class FujitsuGeneralClimate : public climate_ir::ClimateIR { | |||||||
|   FujitsuGeneralClimate() |   FujitsuGeneralClimate() | ||||||
|       : ClimateIR(FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX, 1.0f, true, true, |       : ClimateIR(FUJITSU_GENERAL_TEMP_MIN, FUJITSU_GENERAL_TEMP_MAX, 1.0f, true, true, | ||||||
|                   {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, |                   {climate::CLIMATE_FAN_AUTO, climate::CLIMATE_FAN_LOW, climate::CLIMATE_FAN_MEDIUM, | ||||||
|                    climate::CLIMATE_FAN_HIGH}, |                    climate::CLIMATE_FAN_HIGH, climate::CLIMATE_FAN_QUIET}, | ||||||
|                   {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL, |                   {climate::CLIMATE_SWING_OFF, climate::CLIMATE_SWING_VERTICAL, climate::CLIMATE_SWING_HORIZONTAL, | ||||||
|                    climate::CLIMATE_SWING_BOTH}) {} |                    climate::CLIMATE_SWING_BOTH}) {} | ||||||
|  |  | ||||||
|   | |||||||
| @@ -16,7 +16,7 @@ void GPSTime::from_tiny_gps_(TinyGPSPlus &tiny_gps) { | |||||||
|   if (tiny_gps.date.year() < 2019) |   if (tiny_gps.date.year() < 2019) | ||||||
|     return; |     return; | ||||||
|  |  | ||||||
|   time::ESPTime val{}; |   ESPTime val{}; | ||||||
|   val.year = tiny_gps.date.year(); |   val.year = tiny_gps.date.year(); | ||||||
|   val.month = tiny_gps.date.month(); |   val.month = tiny_gps.date.month(); | ||||||
|   val.day_of_month = tiny_gps.date.day(); |   val.day_of_month = tiny_gps.date.day(); | ||||||
|   | |||||||
| @@ -9,11 +9,42 @@ static const char *const TAG = "growatt_solar"; | |||||||
| static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04; | static const uint8_t MODBUS_CMD_READ_IN_REGISTERS = 0x04; | ||||||
| static const uint8_t MODBUS_REGISTER_COUNT[] = {33, 95};  // indexed with enum GrowattProtocolVersion | static const uint8_t MODBUS_REGISTER_COUNT[] = {33, 95};  // indexed with enum GrowattProtocolVersion | ||||||
|  |  | ||||||
|  | void GrowattSolar::loop() { | ||||||
|  |   // If update() was unable to send we retry until we can send. | ||||||
|  |   if (!this->waiting_to_update_) | ||||||
|  |     return; | ||||||
|  |   update(); | ||||||
|  | } | ||||||
|  |  | ||||||
| void GrowattSolar::update() { | void GrowattSolar::update() { | ||||||
|  |   // If our last send has had no reply yet, and it wasn't that long ago, do nothing. | ||||||
|  |   uint32_t now = millis(); | ||||||
|  |   if (now - this->last_send_ < this->get_update_interval() / 2) { | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   // The bus might be slow, or there might be other devices, or other components might be talking to our device. | ||||||
|  |   if (this->waiting_for_response()) { | ||||||
|  |     this->waiting_to_update_ = true; | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   this->waiting_to_update_ = false; | ||||||
|   this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT[this->protocol_version_]); |   this->send(MODBUS_CMD_READ_IN_REGISTERS, 0, MODBUS_REGISTER_COUNT[this->protocol_version_]); | ||||||
|  |   this->last_send_ = millis(); | ||||||
| } | } | ||||||
|  |  | ||||||
| void GrowattSolar::on_modbus_data(const std::vector<uint8_t> &data) { | void GrowattSolar::on_modbus_data(const std::vector<uint8_t> &data) { | ||||||
|  |   // Other components might be sending commands to our device. But we don't get called with enough | ||||||
|  |   // context to know what is what. So if we didn't do a send, we ignore the data. | ||||||
|  |   if (!this->last_send_) | ||||||
|  |     return; | ||||||
|  |   this->last_send_ = 0; | ||||||
|  |  | ||||||
|  |   // Also ignore the data if the message is too short. Otherwise we will publish invalid values. | ||||||
|  |   if (data.size() < MODBUS_REGISTER_COUNT[this->protocol_version_] * 2) | ||||||
|  |     return; | ||||||
|  |  | ||||||
|   auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t i, float unit) -> void { |   auto publish_1_reg_sensor_state = [&](sensor::Sensor *sensor, size_t i, float unit) -> void { | ||||||
|     if (sensor == nullptr) |     if (sensor == nullptr) | ||||||
|       return; |       return; | ||||||
|   | |||||||
| @@ -19,6 +19,7 @@ enum GrowattProtocolVersion { | |||||||
|  |  | ||||||
| class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { | class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { | ||||||
|  public: |  public: | ||||||
|  |   void loop() override; | ||||||
|   void update() override; |   void update() override; | ||||||
|   void on_modbus_data(const std::vector<uint8_t> &data) override; |   void on_modbus_data(const std::vector<uint8_t> &data) override; | ||||||
|   void dump_config() override; |   void dump_config() override; | ||||||
| @@ -55,6 +56,9 @@ class GrowattSolar : public PollingComponent, public modbus::ModbusDevice { | |||||||
|   } |   } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|  |   bool waiting_to_update_; | ||||||
|  |   uint32_t last_send_; | ||||||
|  |  | ||||||
|   struct GrowattPhase { |   struct GrowattPhase { | ||||||
|     sensor::Sensor *voltage_sensor_{nullptr}; |     sensor::Sensor *voltage_sensor_{nullptr}; | ||||||
|     sensor::Sensor *current_sensor_{nullptr}; |     sensor::Sensor *current_sensor_{nullptr}; | ||||||
|   | |||||||
| @@ -16,6 +16,7 @@ from esphome.const import ( | |||||||
| ) | ) | ||||||
|  |  | ||||||
| DEPENDENCIES = ["i2c"] | DEPENDENCIES = ["i2c"] | ||||||
|  | CODEOWNERS = ["@freekode"] | ||||||
|  |  | ||||||
| hm3301_ns = cg.esphome_ns.namespace("hm3301") | hm3301_ns = cg.esphome_ns.namespace("hm3301") | ||||||
| HM3301Component = hm3301_ns.class_( | HM3301Component = hm3301_ns.class_( | ||||||
|   | |||||||
| @@ -7,6 +7,30 @@ namespace i2c { | |||||||
|  |  | ||||||
| static const char *const TAG = "i2c"; | static const char *const TAG = "i2c"; | ||||||
|  |  | ||||||
|  | ErrorCode I2CDevice::read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop) { | ||||||
|  |   ErrorCode err = this->write(&a_register, 1, stop); | ||||||
|  |   if (err != ERROR_OK) | ||||||
|  |     return err; | ||||||
|  |   return bus_->read(address_, data, len); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | ErrorCode I2CDevice::write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop) { | ||||||
|  |   WriteBuffer buffers[2]; | ||||||
|  |   buffers[0].data = &a_register; | ||||||
|  |   buffers[0].len = 1; | ||||||
|  |   buffers[1].data = data; | ||||||
|  |   buffers[1].len = len; | ||||||
|  |   return bus_->writev(address_, buffers, 2, stop); | ||||||
|  | } | ||||||
|  |  | ||||||
|  | bool I2CDevice::read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) { | ||||||
|  |   if (read_register(a_register, reinterpret_cast<uint8_t *>(data), len * 2) != ERROR_OK) | ||||||
|  |     return false; | ||||||
|  |   for (size_t i = 0; i < len; i++) | ||||||
|  |     data[i] = i2ctohs(data[i]); | ||||||
|  |   return true; | ||||||
|  | } | ||||||
|  |  | ||||||
| bool I2CDevice::write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) { | bool I2CDevice::write_bytes_16(uint8_t a_register, const uint16_t *data, uint8_t len) { | ||||||
|   // we have to copy in order to be able to change byte order |   // we have to copy in order to be able to change byte order | ||||||
|   std::unique_ptr<uint16_t[]> temp{new uint16_t[len]}; |   std::unique_ptr<uint16_t[]> temp{new uint16_t[len]}; | ||||||
|   | |||||||
| @@ -46,22 +46,10 @@ class I2CDevice { | |||||||
|   I2CRegister reg(uint8_t a_register) { return {this, a_register}; } |   I2CRegister reg(uint8_t a_register) { return {this, a_register}; } | ||||||
|  |  | ||||||
|   ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); } |   ErrorCode read(uint8_t *data, size_t len) { return bus_->read(address_, data, len); } | ||||||
|   ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true) { |   ErrorCode read_register(uint8_t a_register, uint8_t *data, size_t len, bool stop = true); | ||||||
|     ErrorCode err = this->write(&a_register, 1, stop); |  | ||||||
|     if (err != ERROR_OK) |  | ||||||
|       return err; |  | ||||||
|     return this->read(data, len); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   ErrorCode write(const uint8_t *data, uint8_t len, bool stop = true) { return bus_->write(address_, data, len, stop); } |   ErrorCode write(const uint8_t *data, uint8_t len, bool stop = true) { return bus_->write(address_, data, len, stop); } | ||||||
|   ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true) { |   ErrorCode write_register(uint8_t a_register, const uint8_t *data, size_t len, bool stop = true); | ||||||
|     WriteBuffer buffers[2]; |  | ||||||
|     buffers[0].data = &a_register; |  | ||||||
|     buffers[0].len = 1; |  | ||||||
|     buffers[1].data = data; |  | ||||||
|     buffers[1].len = len; |  | ||||||
|     return bus_->writev(address_, buffers, 2, stop); |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   // Compat APIs |   // Compat APIs | ||||||
|  |  | ||||||
| @@ -85,13 +73,7 @@ class I2CDevice { | |||||||
|     return res; |     return res; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len) { |   bool read_bytes_16(uint8_t a_register, uint16_t *data, uint8_t len); | ||||||
|     if (read_register(a_register, reinterpret_cast<uint8_t *>(data), len * 2) != ERROR_OK) |  | ||||||
|       return false; |  | ||||||
|     for (size_t i = 0; i < len; i++) |  | ||||||
|       data[i] = i2ctohs(data[i]); |  | ||||||
|     return true; |  | ||||||
|   } |  | ||||||
|  |  | ||||||
|   bool read_byte(uint8_t a_register, uint8_t *data, bool stop = true) { |   bool read_byte(uint8_t a_register, uint8_t *data, bool stop = true) { | ||||||
|     return read_register(a_register, data, 1, stop) == ERROR_OK; |     return read_register(a_register, data, 1, stop) == ERROR_OK; | ||||||
|   | |||||||
| @@ -18,6 +18,7 @@ MULTI_CONF = True | |||||||
|  |  | ||||||
| CONF_I2S_DOUT_PIN = "i2s_dout_pin" | CONF_I2S_DOUT_PIN = "i2s_dout_pin" | ||||||
| CONF_I2S_DIN_PIN = "i2s_din_pin" | CONF_I2S_DIN_PIN = "i2s_din_pin" | ||||||
|  | CONF_I2S_MCLK_PIN = "i2s_mclk_pin" | ||||||
| CONF_I2S_BCLK_PIN = "i2s_bclk_pin" | CONF_I2S_BCLK_PIN = "i2s_bclk_pin" | ||||||
| CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin" | CONF_I2S_LRCLK_PIN = "i2s_lrclk_pin" | ||||||
|  |  | ||||||
| @@ -44,6 +45,7 @@ CONFIG_SCHEMA = cv.Schema( | |||||||
|         cv.GenerateID(): cv.declare_id(I2SAudioComponent), |         cv.GenerateID(): cv.declare_id(I2SAudioComponent), | ||||||
|         cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number, |         cv.Required(CONF_I2S_LRCLK_PIN): pins.internal_gpio_output_pin_number, | ||||||
|         cv.Optional(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number, |         cv.Optional(CONF_I2S_BCLK_PIN): pins.internal_gpio_output_pin_number, | ||||||
|  |         cv.Optional(CONF_I2S_MCLK_PIN): pins.internal_gpio_output_pin_number, | ||||||
|     } |     } | ||||||
| ) | ) | ||||||
|  |  | ||||||
| @@ -69,3 +71,5 @@ async def to_code(config): | |||||||
|     cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN])) |     cg.add(var.set_lrclk_pin(config[CONF_I2S_LRCLK_PIN])) | ||||||
|     if CONF_I2S_BCLK_PIN in config: |     if CONF_I2S_BCLK_PIN in config: | ||||||
|         cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN])) |         cg.add(var.set_bclk_pin(config[CONF_I2S_BCLK_PIN])) | ||||||
|  |     if CONF_I2S_MCLK_PIN in config: | ||||||
|  |         cg.add(var.set_mclk_pin(config[CONF_I2S_MCLK_PIN])) | ||||||
|   | |||||||
| @@ -21,7 +21,7 @@ class I2SAudioComponent : public Component { | |||||||
|  |  | ||||||
|   i2s_pin_config_t get_pin_config() const { |   i2s_pin_config_t get_pin_config() const { | ||||||
|     return { |     return { | ||||||
|         .mck_io_num = I2S_PIN_NO_CHANGE, |         .mck_io_num = this->mclk_pin_, | ||||||
|         .bck_io_num = this->bclk_pin_, |         .bck_io_num = this->bclk_pin_, | ||||||
|         .ws_io_num = this->lrclk_pin_, |         .ws_io_num = this->lrclk_pin_, | ||||||
|         .data_out_num = I2S_PIN_NO_CHANGE, |         .data_out_num = I2S_PIN_NO_CHANGE, | ||||||
| @@ -29,6 +29,7 @@ class I2SAudioComponent : public Component { | |||||||
|     }; |     }; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|  |   void set_mclk_pin(int pin) { this->mclk_pin_ = pin; } | ||||||
|   void set_bclk_pin(int pin) { this->bclk_pin_ = pin; } |   void set_bclk_pin(int pin) { this->bclk_pin_ = pin; } | ||||||
|   void set_lrclk_pin(int pin) { this->lrclk_pin_ = pin; } |   void set_lrclk_pin(int pin) { this->lrclk_pin_ = pin; } | ||||||
|  |  | ||||||
| @@ -44,6 +45,7 @@ class I2SAudioComponent : public Component { | |||||||
|   I2SAudioIn *audio_in_{nullptr}; |   I2SAudioIn *audio_in_{nullptr}; | ||||||
|   I2SAudioOut *audio_out_{nullptr}; |   I2SAudioOut *audio_out_{nullptr}; | ||||||
|  |  | ||||||
|  |   int mclk_pin_{I2S_PIN_NO_CHANGE}; | ||||||
|   int bclk_pin_{I2S_PIN_NO_CHANGE}; |   int bclk_pin_{I2S_PIN_NO_CHANGE}; | ||||||
|   int lrclk_pin_; |   int lrclk_pin_; | ||||||
|   i2s_port_t port_{}; |   i2s_port_t port_{}; | ||||||
|   | |||||||
| @@ -27,6 +27,7 @@ i2s_dac_mode_t = cg.global_ns.enum("i2s_dac_mode_t") | |||||||
| CONF_MUTE_PIN = "mute_pin" | CONF_MUTE_PIN = "mute_pin" | ||||||
| CONF_AUDIO_ID = "audio_id" | CONF_AUDIO_ID = "audio_id" | ||||||
| CONF_DAC_TYPE = "dac_type" | CONF_DAC_TYPE = "dac_type" | ||||||
|  | CONF_I2S_COMM_FMT = "i2s_comm_fmt" | ||||||
|  |  | ||||||
| INTERNAL_DAC_OPTIONS = { | INTERNAL_DAC_OPTIONS = { | ||||||
|     "left": i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN, |     "left": i2s_dac_mode_t.I2S_DAC_CHANNEL_LEFT_EN, | ||||||
| @@ -38,6 +39,8 @@ EXTERNAL_DAC_OPTIONS = ["mono", "stereo"] | |||||||
|  |  | ||||||
| NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2] | NO_INTERNAL_DAC_VARIANTS = [esp32.const.VARIANT_ESP32S2] | ||||||
|  |  | ||||||
|  | I2C_COMM_FMT_OPTIONS = ["lsb", "msb"] | ||||||
|  |  | ||||||
|  |  | ||||||
| def validate_esp32_variant(config): | def validate_esp32_variant(config): | ||||||
|     if config[CONF_DAC_TYPE] != "internal": |     if config[CONF_DAC_TYPE] != "internal": | ||||||
| @@ -69,6 +72,9 @@ CONFIG_SCHEMA = cv.All( | |||||||
|                     cv.Optional(CONF_MODE, default="mono"): cv.one_of( |                     cv.Optional(CONF_MODE, default="mono"): cv.one_of( | ||||||
|                         *EXTERNAL_DAC_OPTIONS, lower=True |                         *EXTERNAL_DAC_OPTIONS, lower=True | ||||||
|                     ), |                     ), | ||||||
|  |                     cv.Optional(CONF_I2S_COMM_FMT, default="msb"): cv.one_of( | ||||||
|  |                         *I2C_COMM_FMT_OPTIONS, lower=True | ||||||
|  |                     ), | ||||||
|                 } |                 } | ||||||
|             ).extend(cv.COMPONENT_SCHEMA), |             ).extend(cv.COMPONENT_SCHEMA), | ||||||
|         }, |         }, | ||||||
| @@ -94,6 +100,7 @@ async def to_code(config): | |||||||
|             pin = await cg.gpio_pin_expression(config[CONF_MUTE_PIN]) |             pin = await cg.gpio_pin_expression(config[CONF_MUTE_PIN]) | ||||||
|             cg.add(var.set_mute_pin(pin)) |             cg.add(var.set_mute_pin(pin)) | ||||||
|         cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1)) |         cg.add(var.set_external_dac_channels(2 if config[CONF_MODE] == "stereo" else 1)) | ||||||
|  |         cg.add(var.set_i2s_comm_fmt_lsb(config[CONF_I2S_COMM_FMT] == "lsb")) | ||||||
|  |  | ||||||
|     cg.add_library("WiFiClientSecure", None) |     cg.add_library("WiFiClientSecure", None) | ||||||
|     cg.add_library("HTTPClient", None) |     cg.add_library("HTTPClient", None) | ||||||
|   | |||||||
| @@ -148,6 +148,7 @@ void I2SAudioMediaPlayer::start_() { | |||||||
|     pin_config.data_out_num = this->dout_pin_; |     pin_config.data_out_num = this->dout_pin_; | ||||||
|     i2s_set_pin(this->parent_->get_port(), &pin_config); |     i2s_set_pin(this->parent_->get_port(), &pin_config); | ||||||
|  |  | ||||||
|  |     this->audio_->setI2SCommFMT_LSB(this->i2s_comm_fmt_lsb_); | ||||||
|     this->audio_->forceMono(this->external_dac_channels_ == 1); |     this->audio_->forceMono(this->external_dac_channels_ == 1); | ||||||
|     if (this->mute_pin_ != nullptr) { |     if (this->mute_pin_ != nullptr) { | ||||||
|       this->mute_pin_->setup(); |       this->mute_pin_->setup(); | ||||||
|   | |||||||
| @@ -39,6 +39,8 @@ class I2SAudioMediaPlayer : public Component, public media_player::MediaPlayer, | |||||||
| #endif | #endif | ||||||
|   void set_external_dac_channels(uint8_t channels) { this->external_dac_channels_ = channels; } |   void set_external_dac_channels(uint8_t channels) { this->external_dac_channels_ = channels; } | ||||||
|  |  | ||||||
|  |   void set_i2s_comm_fmt_lsb(bool lsb) { this->i2s_comm_fmt_lsb_ = lsb; } | ||||||
|  |  | ||||||
|   media_player::MediaPlayerTraits get_traits() override; |   media_player::MediaPlayerTraits get_traits() override; | ||||||
|  |  | ||||||
|   bool is_muted() const override { return this->muted_; } |   bool is_muted() const override { return this->muted_; } | ||||||
| @@ -71,6 +73,8 @@ class I2SAudioMediaPlayer : public Component, public media_player::MediaPlayer, | |||||||
| #endif | #endif | ||||||
|   uint8_t external_dac_channels_; |   uint8_t external_dac_channels_; | ||||||
|  |  | ||||||
|  |   bool i2s_comm_fmt_lsb_; | ||||||
|  |  | ||||||
|   HighFrequencyLoopRequester high_freq_; |   HighFrequencyLoopRequester high_freq_; | ||||||
|  |  | ||||||
|   optional<std::string> current_url_{}; |   optional<std::string> current_url_{}; | ||||||
|   | |||||||
| @@ -20,6 +20,7 @@ DEPENDENCIES = ["i2s_audio"] | |||||||
| CONF_ADC_PIN = "adc_pin" | CONF_ADC_PIN = "adc_pin" | ||||||
| CONF_ADC_TYPE = "adc_type" | CONF_ADC_TYPE = "adc_type" | ||||||
| CONF_PDM = "pdm" | CONF_PDM = "pdm" | ||||||
|  | CONF_BITS_PER_SAMPLE = "bits_per_sample" | ||||||
|  |  | ||||||
| I2SAudioMicrophone = i2s_audio_ns.class_( | I2SAudioMicrophone = i2s_audio_ns.class_( | ||||||
|     "I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component |     "I2SAudioMicrophone", I2SAudioIn, microphone.Microphone, cg.Component | ||||||
| @@ -30,10 +31,17 @@ CHANNELS = { | |||||||
|     "left": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_LEFT, |     "left": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_LEFT, | ||||||
|     "right": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT, |     "right": i2s_channel_fmt_t.I2S_CHANNEL_FMT_ONLY_RIGHT, | ||||||
| } | } | ||||||
|  | i2s_bits_per_sample_t = cg.global_ns.enum("i2s_bits_per_sample_t") | ||||||
|  | BITS_PER_SAMPLE = { | ||||||
|  |     16: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_16BIT, | ||||||
|  |     32: i2s_bits_per_sample_t.I2S_BITS_PER_SAMPLE_32BIT, | ||||||
|  | } | ||||||
|  |  | ||||||
| INTERNAL_ADC_VARIANTS = [esp32.const.VARIANT_ESP32] | INTERNAL_ADC_VARIANTS = [esp32.const.VARIANT_ESP32] | ||||||
| PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3] | PDM_VARIANTS = [esp32.const.VARIANT_ESP32, esp32.const.VARIANT_ESP32S3] | ||||||
|  |  | ||||||
|  | _validate_bits = cv.float_with_unit("bits", "bit") | ||||||
|  |  | ||||||
|  |  | ||||||
| def validate_esp32_variant(config): | def validate_esp32_variant(config): | ||||||
|     variant = esp32.get_esp32_variant() |     variant = esp32.get_esp32_variant() | ||||||
| @@ -54,6 +62,9 @@ BASE_SCHEMA = microphone.MICROPHONE_SCHEMA.extend( | |||||||
|         cv.GenerateID(): cv.declare_id(I2SAudioMicrophone), |         cv.GenerateID(): cv.declare_id(I2SAudioMicrophone), | ||||||
|         cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), |         cv.GenerateID(CONF_I2S_AUDIO_ID): cv.use_id(I2SAudioComponent), | ||||||
|         cv.Optional(CONF_CHANNEL, default="right"): cv.enum(CHANNELS), |         cv.Optional(CONF_CHANNEL, default="right"): cv.enum(CHANNELS), | ||||||
|  |         cv.Optional(CONF_BITS_PER_SAMPLE, default="32bit"): cv.All( | ||||||
|  |             _validate_bits, cv.enum(BITS_PER_SAMPLE) | ||||||
|  |         ), | ||||||
|     } |     } | ||||||
| ).extend(cv.COMPONENT_SCHEMA) | ).extend(cv.COMPONENT_SCHEMA) | ||||||
|  |  | ||||||
| @@ -93,6 +104,7 @@ async def to_code(config): | |||||||
|         cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN])) |         cg.add(var.set_din_pin(config[CONF_I2S_DIN_PIN])) | ||||||
|         cg.add(var.set_pdm(config[CONF_PDM])) |         cg.add(var.set_pdm(config[CONF_PDM])) | ||||||
|  |  | ||||||
|     cg.add(var.set_channel(CHANNELS[config[CONF_CHANNEL]])) |     cg.add(var.set_channel(config[CONF_CHANNEL])) | ||||||
|  |     cg.add(var.set_bits_per_sample(config[CONF_BITS_PER_SAMPLE])) | ||||||
|  |  | ||||||
|     await microphone.register_microphone(var, config) |     await microphone.register_microphone(var, config) | ||||||
|   | |||||||
| @@ -16,7 +16,13 @@ static const char *const TAG = "i2s_audio.microphone"; | |||||||
|  |  | ||||||
| void I2SAudioMicrophone::setup() { | void I2SAudioMicrophone::setup() { | ||||||
|   ESP_LOGCONFIG(TAG, "Setting up I2S Audio Microphone..."); |   ESP_LOGCONFIG(TAG, "Setting up I2S Audio Microphone..."); | ||||||
|   this->buffer_.resize(BUFFER_SIZE); |   ExternalRAMAllocator<uint8_t> allocator(ExternalRAMAllocator<uint8_t>::ALLOW_FAILURE); | ||||||
|  |   this->buffer_ = allocator.allocate(BUFFER_SIZE); | ||||||
|  |   if (this->buffer_ == nullptr) { | ||||||
|  |     ESP_LOGE(TAG, "Failed to allocate buffer!"); | ||||||
|  |     this->mark_failed(); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |  | ||||||
| #if SOC_I2S_SUPPORTS_ADC | #if SOC_I2S_SUPPORTS_ADC | ||||||
|   if (this->adc_) { |   if (this->adc_) { | ||||||
| @@ -48,7 +54,7 @@ void I2SAudioMicrophone::start_() { | |||||||
|   i2s_driver_config_t config = { |   i2s_driver_config_t config = { | ||||||
|       .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX), |       .mode = (i2s_mode_t) (I2S_MODE_MASTER | I2S_MODE_RX), | ||||||
|       .sample_rate = 16000, |       .sample_rate = 16000, | ||||||
|       .bits_per_sample = I2S_BITS_PER_SAMPLE_16BIT, |       .bits_per_sample = this->bits_per_sample_, | ||||||
|       .channel_format = this->channel_, |       .channel_format = this->channel_, | ||||||
|       .communication_format = I2S_COMM_FORMAT_STAND_I2S, |       .communication_format = I2S_COMM_FORMAT_STAND_I2S, | ||||||
|       .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, |       .intr_alloc_flags = ESP_INTR_FLAG_LEVEL1, | ||||||
| @@ -107,16 +113,35 @@ void I2SAudioMicrophone::stop_() { | |||||||
| void I2SAudioMicrophone::read_() { | void I2SAudioMicrophone::read_() { | ||||||
|   size_t bytes_read = 0; |   size_t bytes_read = 0; | ||||||
|   esp_err_t err = |   esp_err_t err = | ||||||
|       i2s_read(this->parent_->get_port(), this->buffer_.data(), BUFFER_SIZE, &bytes_read, (100 / portTICK_PERIOD_MS)); |       i2s_read(this->parent_->get_port(), this->buffer_, BUFFER_SIZE, &bytes_read, (100 / portTICK_PERIOD_MS)); | ||||||
|   if (err != ESP_OK) { |   if (err != ESP_OK) { | ||||||
|     ESP_LOGW(TAG, "Error reading from I2S microphone: %s", esp_err_to_name(err)); |     ESP_LOGW(TAG, "Error reading from I2S microphone: %s", esp_err_to_name(err)); | ||||||
|     this->status_set_warning(); |     this->status_set_warning(); | ||||||
|     return; |     return; | ||||||
|   } |   } | ||||||
|  |  | ||||||
|   this->status_clear_warning(); |   this->status_clear_warning(); | ||||||
|  |  | ||||||
|   this->data_callbacks_.call(this->buffer_); |   std::vector<int16_t> samples; | ||||||
|  |   size_t samples_read = 0; | ||||||
|  |   if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_16BIT) { | ||||||
|  |     samples_read = bytes_read / sizeof(int16_t); | ||||||
|  |   } else if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_32BIT) { | ||||||
|  |     samples_read = bytes_read / sizeof(int32_t); | ||||||
|  |   } else { | ||||||
|  |     ESP_LOGE(TAG, "Unsupported bits per sample: %d", this->bits_per_sample_); | ||||||
|  |     return; | ||||||
|  |   } | ||||||
|  |   samples.resize(samples_read); | ||||||
|  |   if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_16BIT) { | ||||||
|  |     memcpy(samples.data(), this->buffer_, bytes_read); | ||||||
|  |   } else if (this->bits_per_sample_ == I2S_BITS_PER_SAMPLE_32BIT) { | ||||||
|  |     for (size_t i = 0; i < samples_read; i++) { | ||||||
|  |       int32_t temp = reinterpret_cast<int32_t *>(this->buffer_)[i] >> 14; | ||||||
|  |       samples[i] = clamp<int16_t>(temp, INT16_MIN, INT16_MAX); | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  |  | ||||||
|  |   this->data_callbacks_.call(samples); | ||||||
| } | } | ||||||
|  |  | ||||||
| void I2SAudioMicrophone::loop() { | void I2SAudioMicrophone::loop() { | ||||||
|   | |||||||
| @@ -29,6 +29,7 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub | |||||||
| #endif | #endif | ||||||
|  |  | ||||||
|   void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; } |   void set_channel(i2s_channel_fmt_t channel) { this->channel_ = channel; } | ||||||
|  |   void set_bits_per_sample(i2s_bits_per_sample_t bits_per_sample) { this->bits_per_sample_ = bits_per_sample; } | ||||||
|  |  | ||||||
|  protected: |  protected: | ||||||
|   void start_(); |   void start_(); | ||||||
| @@ -41,8 +42,9 @@ class I2SAudioMicrophone : public I2SAudioIn, public microphone::Microphone, pub | |||||||
|   bool adc_{false}; |   bool adc_{false}; | ||||||
| #endif | #endif | ||||||
|   bool pdm_{false}; |   bool pdm_{false}; | ||||||
|   std::vector<uint8_t> buffer_; |   uint8_t *buffer_; | ||||||
|   i2s_channel_fmt_t channel_; |   i2s_channel_fmt_t channel_; | ||||||
|  |   i2s_bits_per_sample_t bits_per_sample_; | ||||||
|  |  | ||||||
|   HighFrequencyLoopRequester high_freq_; |   HighFrequencyLoopRequester high_freq_; | ||||||
| }; | }; | ||||||
|   | |||||||
| @@ -44,6 +44,8 @@ MODELS = { | |||||||
|     "ILI9486": ili9XXX_ns.class_("ILI9XXXILI9486", ili9XXXSPI), |     "ILI9486": ili9XXX_ns.class_("ILI9XXXILI9486", ili9XXXSPI), | ||||||
|     "ILI9488": ili9XXX_ns.class_("ILI9XXXILI9488", ili9XXXSPI), |     "ILI9488": ili9XXX_ns.class_("ILI9XXXILI9488", ili9XXXSPI), | ||||||
|     "ST7796": ili9XXX_ns.class_("ILI9XXXST7796", ili9XXXSPI), |     "ST7796": ili9XXX_ns.class_("ILI9XXXST7796", ili9XXXSPI), | ||||||
|  |     "S3BOX": ili9XXX_ns.class_("ILI9XXXS3Box", ili9XXXSPI), | ||||||
|  |     "S3BOX_LITE": ili9XXX_ns.class_("ILI9XXXS3BoxLite", ili9XXXSPI), | ||||||
| } | } | ||||||
|  |  | ||||||
| COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE") | COLOR_PALETTE = cv.one_of("NONE", "GRAYSCALE", "IMAGE_ADAPTIVE") | ||||||
|   | |||||||
| @@ -421,5 +421,28 @@ void ILI9XXXST7796::initialize() { | |||||||
|   } |   } | ||||||
| } | } | ||||||
|  |  | ||||||
|  | //   24_TFT rotated display | ||||||
|  | void ILI9XXXS3Box::initialize() { | ||||||
|  |   this->init_lcd_(INITCMD_S3BOX); | ||||||
|  |   if (this->width_ == 0) { | ||||||
|  |     this->width_ = 320; | ||||||
|  |   } | ||||||
|  |   if (this->height_ == 0) { | ||||||
|  |     this->height_ = 240; | ||||||
|  |   } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | //   24_TFT rotated display | ||||||
|  | void ILI9XXXS3BoxLite::initialize() { | ||||||
|  |   this->init_lcd_(INITCMD_S3BOXLITE); | ||||||
|  |   if (this->width_ == 0) { | ||||||
|  |     this->width_ = 320; | ||||||
|  |   } | ||||||
|  |   if (this->height_ == 0) { | ||||||
|  |     this->height_ = 240; | ||||||
|  |   } | ||||||
|  |   this->invert_display_(true); | ||||||
|  | } | ||||||
|  |  | ||||||
| }  // namespace ili9xxx | }  // namespace ili9xxx | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -134,5 +134,15 @@ class ILI9XXXST7796 : public ILI9XXXDisplay { | |||||||
|   void initialize() override; |   void initialize() override; | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | class ILI9XXXS3Box : public ILI9XXXDisplay { | ||||||
|  |  protected: | ||||||
|  |   void initialize() override; | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | class ILI9XXXS3BoxLite : public ILI9XXXDisplay { | ||||||
|  |  protected: | ||||||
|  |   void initialize() override; | ||||||
|  | }; | ||||||
|  |  | ||||||
| }  // namespace ili9xxx | }  // namespace ili9xxx | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -116,8 +116,8 @@ static const uint8_t PROGMEM INITCMD_ILI9486[] = { | |||||||
| }; | }; | ||||||
|  |  | ||||||
| static const uint8_t PROGMEM INITCMD_ILI9488[] = { | static const uint8_t PROGMEM INITCMD_ILI9488[] = { | ||||||
|   ILI9XXX_GMCTRP1,15, 0x00, 0x03, 0x09, 0x08, 0x16, 0x0A, 0x3F, 0x78, 0x4C, 0x09, 0x0A, 0x08, 0x16, 0x1A, 0x0F, |   ILI9XXX_GMCTRP1,15, 0x0f, 0x24, 0x1c, 0x0a, 0x0f, 0x08, 0x43, 0x88, 0x32, 0x0f, 0x10, 0x06, 0x0f, 0x07, 0x00, | ||||||
|   ILI9XXX_GMCTRN1,15, 0x00, 0x16, 0x19, 0x03, 0x0F, 0x05, 0x32, 0x45, 0x46, 0x04, 0x0E, 0x0D, 0x35, 0x37, 0x0F, |   ILI9XXX_GMCTRN1,15, 0x0F, 0x38, 0x30, 0x09, 0x0f, 0x0f, 0x4e, 0x77, 0x3c, 0x07, 0x10, 0x05, 0x23, 0x1b, 0x00, | ||||||
|  |  | ||||||
|   ILI9XXX_PWCTR1,  2, 0x17, 0x15,  // VRH1 VRH2 |   ILI9XXX_PWCTR1,  2, 0x17, 0x15,  // VRH1 VRH2 | ||||||
|   ILI9XXX_PWCTR2,  1, 0x41,  // VGH, VGL |   ILI9XXX_PWCTR2,  1, 0x41,  // VGH, VGL | ||||||
| @@ -169,6 +169,66 @@ static const uint8_t PROGMEM INITCMD_ST7796[] = { | |||||||
|   0x00                                   // End of list |   0x00                                   // End of list | ||||||
| }; | }; | ||||||
|  |  | ||||||
|  | static const uint8_t PROGMEM INITCMD_S3BOX[] = { | ||||||
|  |   0xEF, 3, 0x03, 0x80, 0x02, | ||||||
|  |   0xCF, 3, 0x00, 0xC1, 0x30, | ||||||
|  |   0xED, 4, 0x64, 0x03, 0x12, 0x81, | ||||||
|  |   0xE8, 3, 0x85, 0x00, 0x78, | ||||||
|  |   0xCB, 5, 0x39, 0x2C, 0x00, 0x34, 0x02, | ||||||
|  |   0xF7, 1, 0x20, | ||||||
|  |   0xEA, 2, 0x00, 0x00, | ||||||
|  |   ILI9XXX_PWCTR1  , 1, 0x23,             // Power control VRH[5:0] | ||||||
|  |   ILI9XXX_PWCTR2  , 1, 0x10,             // Power control SAP[2:0];BT[3:0] | ||||||
|  |   ILI9XXX_VMCTR1  , 2, 0x3e, 0x28,       // VCM control | ||||||
|  |   ILI9XXX_VMCTR2  , 1, 0x86,             // VCM control2 | ||||||
|  |   ILI9XXX_MADCTL  , 1, 0xC8,             // Memory Access Control | ||||||
|  |   ILI9XXX_VSCRSADD, 1, 0x00,             // Vertical scroll zero | ||||||
|  |   ILI9XXX_PIXFMT  , 1, 0x55, | ||||||
|  |   ILI9XXX_FRMCTR1 , 2, 0x00, 0x18, | ||||||
|  |   ILI9XXX_DFUNCTR , 3, 0x08, 0x82, 0x27, // Display Function Control | ||||||
|  |   0xF2, 1, 0x00,                         // 3Gamma Function Disable | ||||||
|  |   ILI9XXX_GAMMASET , 1, 0x01,             // Gamma curve selected | ||||||
|  |   ILI9XXX_GMCTRP1 , 15, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, // Set Gamma | ||||||
|  |                         0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, | ||||||
|  |                         0x0E, 0x09, 0x00, | ||||||
|  |   ILI9XXX_GMCTRN1 , 15, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, // Set Gamma | ||||||
|  |                         0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, | ||||||
|  |                         0x31, 0x36, 0x0F, | ||||||
|  |   ILI9XXX_SLPOUT  , 0x80,                // Exit Sleep | ||||||
|  |   ILI9XXX_DISPON  , 0x80,                // Display on | ||||||
|  |   0x00                                   // End of list | ||||||
|  | }; | ||||||
|  |  | ||||||
|  | static const uint8_t PROGMEM INITCMD_S3BOXLITE[] = { | ||||||
|  |   0xEF, 3, 0x03, 0x80, 0x02, | ||||||
|  |   0xCF, 3, 0x00, 0xC1, 0x30, | ||||||
|  |   0xED, 4, 0x64, 0x03, 0x12, 0x81, | ||||||
|  |   0xE8, 3, 0x85, 0x00, 0x78, | ||||||
|  |   0xCB, 5, 0x39, 0x2C, 0x00, 0x34, 0x02, | ||||||
|  |   0xF7, 1, 0x20, | ||||||
|  |   0xEA, 2, 0x00, 0x00, | ||||||
|  |   ILI9XXX_PWCTR1  , 1, 0x23,             // Power control VRH[5:0] | ||||||
|  |   ILI9XXX_PWCTR2  , 1, 0x10,             // Power control SAP[2:0];BT[3:0] | ||||||
|  |   ILI9XXX_VMCTR1  , 2, 0x3e, 0x28,       // VCM control | ||||||
|  |   ILI9XXX_VMCTR2  , 1, 0x86,             // VCM control2 | ||||||
|  |   ILI9XXX_MADCTL  , 1, 0x40,             // Memory Access Control | ||||||
|  |   ILI9XXX_VSCRSADD, 1, 0x00,             // Vertical scroll zero | ||||||
|  |   ILI9XXX_PIXFMT  , 1, 0x55, | ||||||
|  |   ILI9XXX_FRMCTR1 , 2, 0x00, 0x18, | ||||||
|  |   ILI9XXX_DFUNCTR , 3, 0x08, 0x82, 0x27, // Display Function Control | ||||||
|  |   0xF2, 1, 0x00,                         // 3Gamma Function Disable | ||||||
|  |   ILI9XXX_GAMMASET , 1, 0x01,             // Gamma curve selected | ||||||
|  |   ILI9XXX_GMCTRP1 , 15, 0x0F, 0x31, 0x2B, 0x0C, 0x0E, 0x08, // Set Gamma | ||||||
|  |                         0x4E, 0xF1, 0x37, 0x07, 0x10, 0x03, | ||||||
|  |                         0x0E, 0x09, 0x00, | ||||||
|  |   ILI9XXX_GMCTRN1 , 15, 0x00, 0x0E, 0x14, 0x03, 0x11, 0x07, // Set Gamma | ||||||
|  |                         0x31, 0xC1, 0x48, 0x08, 0x0F, 0x0C, | ||||||
|  |                         0x31, 0x36, 0x0F, | ||||||
|  |   ILI9XXX_SLPOUT  , 0x80,                // Exit Sleep | ||||||
|  |   ILI9XXX_DISPON  , 0x80,                // Display on | ||||||
|  |   0x00                                   // End of list | ||||||
|  | }; | ||||||
|  |  | ||||||
| // clang-format on | // clang-format on | ||||||
| }  // namespace ili9xxx | }  // namespace ili9xxx | ||||||
| }  // namespace esphome | }  // namespace esphome | ||||||
|   | |||||||
| @@ -1,5 +1,10 @@ | |||||||
| import logging | import logging | ||||||
|  |  | ||||||
|  | import io | ||||||
|  | from pathlib import Path | ||||||
|  | import re | ||||||
|  | import requests | ||||||
|  |  | ||||||
| from esphome import core | from esphome import core | ||||||
| from esphome.components import display, font | from esphome.components import display, font | ||||||
| import esphome.config_validation as cv | import esphome.config_validation as cv | ||||||
| @@ -7,129 +12,347 @@ import esphome.codegen as cg | |||||||
| from esphome.const import ( | from esphome.const import ( | ||||||
|     CONF_DITHER, |     CONF_DITHER, | ||||||
|     CONF_FILE, |     CONF_FILE, | ||||||
|  |     CONF_ICON, | ||||||
|     CONF_ID, |     CONF_ID, | ||||||
|  |     CONF_PATH, | ||||||
|     CONF_RAW_DATA_ID, |     CONF_RAW_DATA_ID, | ||||||
|     CONF_RESIZE, |     CONF_RESIZE, | ||||||
|  |     CONF_SOURCE, | ||||||
|     CONF_TYPE, |     CONF_TYPE, | ||||||
| ) | ) | ||||||
| from esphome.core import CORE, HexInt | from esphome.core import CORE, HexInt | ||||||
|  |  | ||||||
| _LOGGER = logging.getLogger(__name__) | _LOGGER = logging.getLogger(__name__) | ||||||
|  |  | ||||||
|  | DOMAIN = "image" | ||||||
| DEPENDENCIES = ["display"] | DEPENDENCIES = ["display"] | ||||||
| MULTI_CONF = True | MULTI_CONF = True | ||||||
|  |  | ||||||
| ImageType = display.display_ns.enum("ImageType") | ImageType = display.display_ns.enum("ImageType") | ||||||
| IMAGE_TYPE = { | IMAGE_TYPE = { | ||||||
|     "BINARY": ImageType.IMAGE_TYPE_BINARY, |     "BINARY": ImageType.IMAGE_TYPE_BINARY, | ||||||
|  |     "TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_BINARY, | ||||||
|     "GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE, |     "GRAYSCALE": ImageType.IMAGE_TYPE_GRAYSCALE, | ||||||
|     "RGB24": ImageType.IMAGE_TYPE_RGB24, |  | ||||||
|     "TRANSPARENT_BINARY": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY, |  | ||||||
|     "RGB565": ImageType.IMAGE_TYPE_RGB565, |     "RGB565": ImageType.IMAGE_TYPE_RGB565, | ||||||
|     "TRANSPARENT_IMAGE": ImageType.IMAGE_TYPE_TRANSPARENT_BINARY, |     "RGB24": ImageType.IMAGE_TYPE_RGB24, | ||||||
|  |     "RGBA": ImageType.IMAGE_TYPE_RGBA, | ||||||
| } | } | ||||||
|  |  | ||||||
|  | CONF_USE_TRANSPARENCY = "use_transparency" | ||||||
|  |  | ||||||
|  | # If the MDI file cannot be downloaded within this time, abort. | ||||||
|  | MDI_DOWNLOAD_TIMEOUT = 30  # seconds | ||||||
|  |  | ||||||
|  | SOURCE_LOCAL = "local" | ||||||
|  | SOURCE_MDI = "mdi" | ||||||
|  |  | ||||||
| Image_ = display.display_ns.class_("Image") | Image_ = display.display_ns.class_("Image") | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _compute_local_icon_path(value) -> Path: | ||||||
|  |     base_dir = Path(CORE.config_dir) / ".esphome" / DOMAIN / "mdi" | ||||||
|  |     return base_dir / f"{value[CONF_ICON]}.svg" | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def download_mdi(value): | ||||||
|  |     mdi_id = value[CONF_ICON] | ||||||
|  |     path = _compute_local_icon_path(value) | ||||||
|  |     if path.is_file(): | ||||||
|  |         return value | ||||||
|  |     url = f"https://raw.githubusercontent.com/Templarian/MaterialDesign/master/svg/{mdi_id}.svg" | ||||||
|  |     _LOGGER.debug("Downloading %s MDI image from %s", mdi_id, url) | ||||||
|  |     try: | ||||||
|  |         req = requests.get(url, timeout=MDI_DOWNLOAD_TIMEOUT) | ||||||
|  |         req.raise_for_status() | ||||||
|  |     except requests.exceptions.RequestException as e: | ||||||
|  |         raise cv.Invalid(f"Could not download MDI image {mdi_id} from {url}: {e}") | ||||||
|  |  | ||||||
|  |     path.parent.mkdir(parents=True, exist_ok=True) | ||||||
|  |     path.write_bytes(req.content) | ||||||
|  |     return value | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_cairosvg_installed(value): | ||||||
|  |     """Validate that cairosvg is installed""" | ||||||
|  |     try: | ||||||
|  |         import cairosvg | ||||||
|  |     except ImportError as err: | ||||||
|  |         raise cv.Invalid( | ||||||
|  |             "Please install the cairosvg python package to use this feature. " | ||||||
|  |             "(pip install cairosvg)" | ||||||
|  |         ) from err | ||||||
|  |  | ||||||
|  |     major, minor, _ = cairosvg.__version__.split(".") | ||||||
|  |     if major < "2" or major == "2" and minor < "2": | ||||||
|  |         raise cv.Invalid( | ||||||
|  |             "Please update your cairosvg installation to at least 2.2.0. " | ||||||
|  |             "(pip install -U cairosvg)" | ||||||
|  |         ) | ||||||
|  |  | ||||||
|  |     return value | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_cross_dependencies(config): | ||||||
|  |     """ | ||||||
|  |     Validate fields whose possible values depend on other fields. | ||||||
|  |     For example, validate that explicitly transparent image types | ||||||
|  |     have "use_transparency" set to True. | ||||||
|  |     Also set the default value for those kind of dependent fields. | ||||||
|  |     """ | ||||||
|  |     is_mdi = CONF_FILE in config and config[CONF_FILE][CONF_SOURCE] == SOURCE_MDI | ||||||
|  |     if CONF_TYPE not in config: | ||||||
|  |         if is_mdi: | ||||||
|  |             config[CONF_TYPE] = "TRANSPARENT_BINARY" | ||||||
|  |         else: | ||||||
|  |             config[CONF_TYPE] = "BINARY" | ||||||
|  |  | ||||||
|  |     image_type = config[CONF_TYPE] | ||||||
|  |     is_transparent_type = image_type in ["TRANSPARENT_BINARY", "RGBA"] | ||||||
|  |  | ||||||
|  |     # If the use_transparency option was not specified, set the default depending on the image type | ||||||
|  |     if CONF_USE_TRANSPARENCY not in config: | ||||||
|  |         config[CONF_USE_TRANSPARENCY] = is_transparent_type | ||||||
|  |  | ||||||
|  |     if is_transparent_type and not config[CONF_USE_TRANSPARENCY]: | ||||||
|  |         raise cv.Invalid(f"Image type {image_type} must always be transparent.") | ||||||
|  |  | ||||||
|  |     if is_mdi and config[CONF_TYPE] not in ["BINARY", "TRANSPARENT_BINARY"]: | ||||||
|  |         raise cv.Invalid("MDI images must be binary images.") | ||||||
|  |  | ||||||
|  |     return config | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def validate_file_shorthand(value): | ||||||
|  |     value = cv.string_strict(value) | ||||||
|  |     if value.startswith("mdi:"): | ||||||
|  |         validate_cairosvg_installed(value) | ||||||
|  |  | ||||||
|  |         match = re.search(r"mdi:([a-zA-Z0-9\-]+)", value) | ||||||
|  |         if match is None: | ||||||
|  |             raise cv.Invalid("Could not parse mdi icon name.") | ||||||
|  |         icon = match.group(1) | ||||||
|  |         return FILE_SCHEMA( | ||||||
|  |             { | ||||||
|  |                 CONF_SOURCE: SOURCE_MDI, | ||||||
|  |                 CONF_ICON: icon, | ||||||
|  |             } | ||||||
|  |         ) | ||||||
|  |     return FILE_SCHEMA( | ||||||
|  |         { | ||||||
|  |             CONF_SOURCE: SOURCE_LOCAL, | ||||||
|  |             CONF_PATH: value, | ||||||
|  |         } | ||||||
|  |     ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | LOCAL_SCHEMA = cv.Schema( | ||||||
|  |     { | ||||||
|  |         cv.Required(CONF_PATH): cv.file_, | ||||||
|  |     } | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | MDI_SCHEMA = cv.All( | ||||||
|  |     { | ||||||
|  |         cv.Required(CONF_ICON): cv.string, | ||||||
|  |     }, | ||||||
|  |     download_mdi, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  | TYPED_FILE_SCHEMA = cv.typed_schema( | ||||||
|  |     { | ||||||
|  |         SOURCE_LOCAL: LOCAL_SCHEMA, | ||||||
|  |         SOURCE_MDI: MDI_SCHEMA, | ||||||
|  |     }, | ||||||
|  |     key=CONF_SOURCE, | ||||||
|  | ) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def _file_schema(value): | ||||||
|  |     if isinstance(value, str): | ||||||
|  |         return validate_file_shorthand(value) | ||||||
|  |     return TYPED_FILE_SCHEMA(value) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | FILE_SCHEMA = cv.Schema(_file_schema) | ||||||
|  |  | ||||||
| IMAGE_SCHEMA = cv.Schema( | IMAGE_SCHEMA = cv.Schema( | ||||||
|  |     cv.All( | ||||||
|         { |         { | ||||||
|             cv.Required(CONF_ID): cv.declare_id(Image_), |             cv.Required(CONF_ID): cv.declare_id(Image_), | ||||||
|         cv.Required(CONF_FILE): cv.file_, |             cv.Required(CONF_FILE): FILE_SCHEMA, | ||||||
|             cv.Optional(CONF_RESIZE): cv.dimensions, |             cv.Optional(CONF_RESIZE): cv.dimensions, | ||||||
|         cv.Optional(CONF_TYPE, default="BINARY"): cv.enum(IMAGE_TYPE, upper=True), |             # Not setting default here on purpose; the default depends on the source type | ||||||
|  |             # (file or mdi), and will be set in the "validate_cross_dependencies" validator. | ||||||
|  |             cv.Optional(CONF_TYPE): cv.enum(IMAGE_TYPE, upper=True), | ||||||
|  |             # Not setting default here on purpose; the default depends on the image type, | ||||||
|  |             # and thus will be set in the "validate_cross_dependencies" validator. | ||||||
|  |             cv.Optional(CONF_USE_TRANSPARENCY): cv.boolean, | ||||||
|             cv.Optional(CONF_DITHER, default="NONE"): cv.one_of( |             cv.Optional(CONF_DITHER, default="NONE"): cv.one_of( | ||||||
|                 "NONE", "FLOYDSTEINBERG", upper=True |                 "NONE", "FLOYDSTEINBERG", upper=True | ||||||
|             ), |             ), | ||||||
|             cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), |             cv.GenerateID(CONF_RAW_DATA_ID): cv.declare_id(cg.uint8), | ||||||
|     } |         }, | ||||||
|  |         validate_cross_dependencies, | ||||||
|  |     ) | ||||||
| ) | ) | ||||||
|  |  | ||||||
| CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA) | CONFIG_SCHEMA = cv.All(font.validate_pillow_installed, IMAGE_SCHEMA) | ||||||
|  |  | ||||||
|  |  | ||||||
|  | def load_svg_image(file: str, resize: tuple[int, int]): | ||||||
|  |     from PIL import Image | ||||||
|  |  | ||||||
|  |     # This import is only needed in case of SVG images; adding it | ||||||
|  |     # to the top would force configurations not using SVG to also have it | ||||||
|  |     # installed for no reason. | ||||||
|  |     from cairosvg import svg2png | ||||||
|  |  | ||||||
|  |     if resize: | ||||||
|  |         req_width, req_height = resize | ||||||
|  |         svg_image = svg2png( | ||||||
|  |             url=file, | ||||||
|  |             output_width=req_width, | ||||||
|  |             output_height=req_height, | ||||||
|  |         ) | ||||||
|  |     else: | ||||||
|  |         svg_image = svg2png(url=file) | ||||||
|  |  | ||||||
|  |     return Image.open(io.BytesIO(svg_image)) | ||||||
|  |  | ||||||
|  |  | ||||||
| async def to_code(config): | async def to_code(config): | ||||||
|     from PIL import Image |     from PIL import Image | ||||||
|  |  | ||||||
|     path = CORE.relative_config_path(config[CONF_FILE]) |     conf_file = config[CONF_FILE] | ||||||
|  |  | ||||||
|  |     if conf_file[CONF_SOURCE] == SOURCE_LOCAL: | ||||||
|  |         path = CORE.relative_config_path(conf_file[CONF_PATH]) | ||||||
|  |  | ||||||
|  |     elif conf_file[CONF_SOURCE] == SOURCE_MDI: | ||||||
|  |         path = _compute_local_icon_path(conf_file).as_posix() | ||||||
|  |  | ||||||
|     try: |     try: | ||||||
|  |         resize = config.get(CONF_RESIZE) | ||||||
|  |         if path.lower().endswith(".svg"): | ||||||
|  |             image = load_svg_image(path, resize) | ||||||
|  |         else: | ||||||
|             image = Image.open(path) |             image = Image.open(path) | ||||||
|  |             if resize: | ||||||
|  |                 image.thumbnail(resize) | ||||||
|     except Exception as e: |     except Exception as e: | ||||||
|         raise core.EsphomeError(f"Could not load image file {path}: {e}") |         raise core.EsphomeError(f"Could not load image file {path}: {e}") | ||||||
|  |  | ||||||
|     width, height = image.size |     width, height = image.size | ||||||
|  |  | ||||||
|     if CONF_RESIZE in config: |     if CONF_RESIZE not in config and (width > 500 or height > 500): | ||||||
|         image.thumbnail(config[CONF_RESIZE]) |  | ||||||
|         width, height = image.size |  | ||||||
|     else: |  | ||||||
|         if width > 500 or height > 500: |  | ||||||
|         _LOGGER.warning( |         _LOGGER.warning( | ||||||
|                 "The image you requested is very big. Please consider using" |             'The image "%s" you requested is very big. Please consider' | ||||||
|                 " the resize parameter." |             " using the resize parameter.", | ||||||
|  |             path, | ||||||
|         ) |         ) | ||||||
|  |  | ||||||
|  |     transparent = config[CONF_USE_TRANSPARENCY] | ||||||
|  |  | ||||||
|     dither = Image.NONE if config[CONF_DITHER] == "NONE" else Image.FLOYDSTEINBERG |     dither = Image.NONE if config[CONF_DITHER] == "NONE" else Image.FLOYDSTEINBERG | ||||||
|     if config[CONF_TYPE] == "GRAYSCALE": |     if config[CONF_TYPE] == "GRAYSCALE": | ||||||
|         image = image.convert("L", dither=dither) |         image = image.convert("LA", dither=dither) | ||||||
|         pixels = list(image.getdata()) |         pixels = list(image.getdata()) | ||||||
|         data = [0 for _ in range(height * width)] |         data = [0 for _ in range(height * width)] | ||||||
|         pos = 0 |         pos = 0 | ||||||
|         for pix in pixels: |         for g, a in pixels: | ||||||
|             data[pos] = pix |             if transparent: | ||||||
|  |                 if g == 1: | ||||||
|  |                     g = 0 | ||||||
|  |                 if a < 0x80: | ||||||
|  |                     g = 1 | ||||||
|  |  | ||||||
|  |             data[pos] = g | ||||||
|  |             pos += 1 | ||||||
|  |  | ||||||
|  |     elif config[CONF_TYPE] == "RGBA": | ||||||
|  |         image = image.convert("RGBA") | ||||||
|  |         pixels = list(image.getdata()) | ||||||
|  |         data = [0 for _ in range(height * width * 4)] | ||||||
|  |         pos = 0 | ||||||
|  |         for r, g, b, a in pixels: | ||||||
|  |             data[pos] = r | ||||||
|  |             pos += 1 | ||||||
|  |             data[pos] = g | ||||||
|  |             pos += 1 | ||||||
|  |             data[pos] = b | ||||||
|  |             pos += 1 | ||||||
|  |             data[pos] = a | ||||||
|             pos += 1 |             pos += 1 | ||||||
|  |  | ||||||
|     elif config[CONF_TYPE] == "RGB24": |     elif config[CONF_TYPE] == "RGB24": | ||||||
|         image = image.convert("RGB") |         image = image.convert("RGBA") | ||||||
|         pixels = list(image.getdata()) |         pixels = list(image.getdata()) | ||||||
|         data = [0 for _ in range(height * width * 3)] |         data = [0 for _ in range(height * width * 3)] | ||||||
|         pos = 0 |         pos = 0 | ||||||
|         for pix in pixels: |         for r, g, b, a in pixels: | ||||||
|             data[pos] = pix[0] |             if transparent: | ||||||
|  |                 if r == 0 and g == 0 and b == 1: | ||||||
|  |                     b = 0 | ||||||
|  |                 if a < 0x80: | ||||||
|  |                     r = 0 | ||||||
|  |                     g = 0 | ||||||
|  |                     b = 1 | ||||||
|  |  | ||||||
|  |             data[pos] = r | ||||||
|             pos += 1 |             pos += 1 | ||||||
|             data[pos] = pix[1] |             data[pos] = g | ||||||
|             pos += 1 |             pos += 1 | ||||||
|             data[pos] = pix[2] |             data[pos] = b | ||||||
|             pos += 1 |             pos += 1 | ||||||
|  |  | ||||||
|     elif config[CONF_TYPE] == "RGB565": |     elif config[CONF_TYPE] in ["RGB565"]: | ||||||
|         image = image.convert("RGB") |         image = image.convert("RGBA") | ||||||
|         pixels = list(image.getdata()) |         pixels = list(image.getdata()) | ||||||
|         data = [0 for _ in range(height * width * 3)] |         data = [0 for _ in range(height * width * 2)] | ||||||
|         pos = 0 |         pos = 0 | ||||||
|         for pix in pixels: |         for r, g, b, a in pixels: | ||||||
|             R = pix[0] >> 3 |             R = r >> 3 | ||||||
|             G = pix[1] >> 2 |             G = g >> 2 | ||||||
|             B = pix[2] >> 3 |             B = b >> 3 | ||||||
|             rgb = (R << 11) | (G << 5) | B |             rgb = (R << 11) | (G << 5) | B | ||||||
|  |  | ||||||
|  |             if transparent: | ||||||
|  |                 if rgb == 0x0020: | ||||||
|  |                     rgb = 0 | ||||||
|  |                 if a < 0x80: | ||||||
|  |                     rgb = 0x0020 | ||||||
|  |  | ||||||
|             data[pos] = rgb >> 8 |             data[pos] = rgb >> 8 | ||||||
|             pos += 1 |             pos += 1 | ||||||
|             data[pos] = rgb & 255 |             data[pos] = rgb & 0xFF | ||||||
|             pos += 1 |             pos += 1 | ||||||
|  |  | ||||||
|     elif (config[CONF_TYPE] == "BINARY") or (config[CONF_TYPE] == "TRANSPARENT_BINARY"): |     elif config[CONF_TYPE] in ["BINARY", "TRANSPARENT_BINARY"]: | ||||||
|  |         if transparent: | ||||||
|  |             alpha = image.split()[-1] | ||||||
|  |             has_alpha = alpha.getextrema()[0] < 0xFF | ||||||
|  |             _LOGGER.debug("%s Has alpha: %s", config[CONF_ID], has_alpha) | ||||||
|         image = image.convert("1", dither=dither) |         image = image.convert("1", dither=dither) | ||||||
|         width8 = ((width + 7) // 8) * 8 |         width8 = ((width + 7) // 8) * 8 | ||||||
|         data = [0 for _ in range(height * width8 // 8)] |         data = [0 for _ in range(height * width8 // 8)] | ||||||
|         for y in range(height): |         for y in range(height): | ||||||
|             for x in range(width): |             for x in range(width): | ||||||
|                 if image.getpixel((x, y)): |                 if transparent and has_alpha: | ||||||
|                     continue |                     a = alpha.getpixel((x, y)) | ||||||
|                 pos = x + y * width8 |                     if not a: | ||||||
|                 data[pos // 8] |= 0x80 >> (pos % 8) |                         continue | ||||||
|  |                 elif image.getpixel((x, y)): | ||||||
|     elif config[CONF_TYPE] == "TRANSPARENT_IMAGE": |  | ||||||
|         image = image.convert("RGBA") |  | ||||||
|         width8 = ((width + 7) // 8) * 8 |  | ||||||
|         data = [0 for _ in range(height * width8 // 8)] |  | ||||||
|         for y in range(height): |  | ||||||
|             for x in range(width): |  | ||||||
|                 if not image.getpixel((x, y))[3]: |  | ||||||
|                     continue |                     continue | ||||||
|                 pos = x + y * width8 |                 pos = x + y * width8 | ||||||
|                 data[pos // 8] |= 0x80 >> (pos % 8) |                 data[pos // 8] |= 0x80 >> (pos % 8) | ||||||
|  |     else: | ||||||
|  |         raise core.EsphomeError( | ||||||
|  |             f"Image f{config[CONF_ID]} has an unsupported type: {config[CONF_TYPE]}." | ||||||
|  |         ) | ||||||
|  |  | ||||||
|     rhs = [HexInt(x) for x in data] |     rhs = [HexInt(x) for x in data] | ||||||
|     prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) |     prog_arr = cg.progmem_array(config[CONF_RAW_DATA_ID], rhs) | ||||||
|     cg.new_Pvariable( |     var = cg.new_Pvariable( | ||||||
|         config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]] |         config[CONF_ID], prog_arr, width, height, IMAGE_TYPE[config[CONF_TYPE]] | ||||||
|     ) |     ) | ||||||
|  |     cg.add(var.set_transparency(transparent)) | ||||||
|   | |||||||
| @@ -158,15 +158,13 @@ void LCDDisplay::clear() { | |||||||
|   for (uint8_t i = 0; i < this->rows_ * this->columns_; i++) |   for (uint8_t i = 0; i < this->rows_ * this->columns_; i++) | ||||||
|     this->buffer_[i] = ' '; |     this->buffer_[i] = ' '; | ||||||
| } | } | ||||||
| #ifdef USE_TIME | void LCDDisplay::strftime(uint8_t column, uint8_t row, const char *format, ESPTime time) { | ||||||
| void LCDDisplay::strftime(uint8_t column, uint8_t row, const char *format, time::ESPTime time) { |  | ||||||
|   char buffer[64]; |   char buffer[64]; | ||||||
|   size_t ret = time.strftime(buffer, sizeof(buffer), format); |   size_t ret = time.strftime(buffer, sizeof(buffer), format); | ||||||
|   if (ret > 0) |   if (ret > 0) | ||||||
|     this->print(column, row, buffer); |     this->print(column, row, buffer); | ||||||
| } | } | ||||||
| void LCDDisplay::strftime(const char *format, time::ESPTime time) { this->strftime(0, 0, format, time); } | void LCDDisplay::strftime(const char *format, ESPTime time) { this->strftime(0, 0, format, time); } | ||||||
| #endif |  | ||||||
| void LCDDisplay::loadchar(uint8_t location, uint8_t charmap[]) { | void LCDDisplay::loadchar(uint8_t location, uint8_t charmap[]) { | ||||||
|   location &= 0x7;  // we only have 8 locations 0-7 |   location &= 0x7;  // we only have 8 locations 0-7 | ||||||
|   this->command_(LCD_DISPLAY_COMMAND_SET_CGRAM_ADDR | (location << 3)); |   this->command_(LCD_DISPLAY_COMMAND_SET_CGRAM_ADDR | (location << 3)); | ||||||
|   | |||||||
| @@ -1,11 +1,7 @@ | |||||||
| #pragma once | #pragma once | ||||||
|  |  | ||||||
| #include "esphome/core/component.h" | #include "esphome/core/component.h" | ||||||
| #include "esphome/core/defines.h" | #include "esphome/core/time.h" | ||||||
|  |  | ||||||
| #ifdef USE_TIME |  | ||||||
| #include "esphome/components/time/real_time_clock.h" |  | ||||||
| #endif |  | ||||||
|  |  | ||||||
| #include <map> | #include <map> | ||||||
| #include <vector> | #include <vector> | ||||||
| @@ -44,13 +40,10 @@ class LCDDisplay : public PollingComponent { | |||||||
|   /// Evaluate the printf-format and print the text at column=0 and row=0. |   /// Evaluate the printf-format and print the text at column=0 and row=0. | ||||||
|   void printf(const char *format, ...) __attribute__((format(printf, 2, 3))); |   void printf(const char *format, ...) __attribute__((format(printf, 2, 3))); | ||||||
|  |  | ||||||
| #ifdef USE_TIME |  | ||||||
|   /// Evaluate the strftime-format and print the text at the specified column and row. |   /// Evaluate the strftime-format and print the text at the specified column and row. | ||||||
|   void strftime(uint8_t column, uint8_t row, const char *format, time::ESPTime time) |   void strftime(uint8_t column, uint8_t row, const char *format, ESPTime time) __attribute__((format(strftime, 4, 0))); | ||||||
|       __attribute__((format(strftime, 4, 0))); |  | ||||||
|   /// Evaluate the strftime-format and print the text at column=0 and row=0. |   /// Evaluate the strftime-format and print the text at column=0 and row=0. | ||||||
|   void strftime(const char *format, time::ESPTime time) __attribute__((format(strftime, 2, 0))); |   void strftime(const char *format, ESPTime time) __attribute__((format(strftime, 2, 0))); | ||||||
| #endif |  | ||||||
|  |  | ||||||
|   /// Load custom char to given location |   /// Load custom char to given location | ||||||
|   void loadchar(uint8_t location, uint8_t charmap[]); |   void loadchar(uint8_t location, uint8_t charmap[]); | ||||||
|   | |||||||
Some files were not shown because too many files have changed in this diff Show More
		Reference in New Issue
	
	Block a user