mirror of
https://github.com/esphome/esphome.git
synced 2025-01-05 12:21:43 +01:00
commit
5ef9cd5f86
211 changed files with 7677 additions and 2561 deletions
|
@ -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++) {
|
void DisplayBuffer::image(int x, int y, BaseImage *image, ImageAlign align, Color color_on, Color color_off) {
|
||||||
this->draw_pixel_at(x + img_x, y + img_y, image->get_pixel(img_x, img_y) ? color_on : 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,23 +56,23 @@ 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) {
|
||||||
|
@ -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
Loading…
Reference in a new issue